#--
# Copyright protects this work.
# See LICENSE file for details.
#++
desc: web page for the Internet
code: |
ICONS_DIR = File.join(ERBook::FORMATS_DIR, 'xhtml', 'icons')
JQUERY_DIR = File.join(ERBook::FORMATS_DIR, 'xhtml', 'jquery')
# load the String#to_xhtml methods
require 'erbook/to_xhtml'
class String
##
# Transforms this UTF-8 string into XML entities.
#
def to_xml_entities
unpack('U*').map! {|c| "#{c};"}.join
end
##
# Transforms this string into a valid URI fragment.
# See http://www.w3.org/TR/REC-html40/types.html#type-name
#
def to_uri_fragment
# remove XML tags from the input
buf = gsub(/<.*?>/, '')
# The first or only character must be a letter or an underscore (_).
buf.insert(0, 'a') unless buf[0,1] =~ /[[:alpha:]_]/
# The remaining characters must be letters, digits, hyphens (-),
# underscores (_), colons (:), or periods (.) or Unicode characters
#
# However, colons (:) and periods (.) are special characters
# in jQuery CSS selector syntax, so we sanitize them as well.
#
buf.unpack('U*').map! do |code|
if code > 0xFF or code.chr =~ /[[:alnum:]\-_]/
code
else
32 # ASCII character code for a single space
end
end.pack('U*').strip.gsub(/[[:space:]-]+/, '-')
end
##
# Evaluates this string as an Ember
# template (created with the given
# options) inside the given binding.
#
def thru_ember binding, options = {}
::Ember::Template.new(self, options).render(binding)
end
end
class Hash
##
# Transforms this hash into a string of XML attribute key="value" pairs.
#
def to_xml_atts
inject([]) {|s,(k,v)| s << %( #{k}="#{v}") }.join
end
end
require 'erb'
module ERBook
##
# Encodes the given input in base64 format.
#
def ERBook.base_64_encode input #:nodoc:
[input].pack('m')
end
##
# Encodes the contents of the given file in base64 format.
#
def ERBook.base_64_encode_file path #:nodoc:
base_64_encode open(path, 'rb') {|f| f.read }
end
##
# Returns a string denoting embedded, base64 encoded data.
#
def ERBook.base_64_embed data, mime #:nodoc:
"data:#{mime.to_s.downcase};base64,#{data.tr("\n", '')}"
end
def ERBook.base_64_embed_file path # :nodoc:
data = base_64_encode_file(path)
require 'mime/types'
mime = MIME::Types.of(path).to_a.join(',')
base_64_embed data, mime
end
##
# Returns a string denoting embedded, base64 encoded image data.
#
# [format]
# type of image data (e.g. PNG, JPEG, GIF, etc.)
#
def ERBook.base_64_embed_image_data data, format #:nodoc:
base_64_embed data, "image/#{format}"
end
# admonition icons
ICON_DEFS = YAML.load_file File.join(ICONS_DIR, 'index.yaml')
class Icon < Struct.new(:origin, :path, :name, :format, :data) #:nodoc:
##
# Returns a data URI containing embedded image data.
#
def data_uri
ERBook.base_64_embed_image_data self.data, self.format
end
##
# Returns a CSS url() containing embedded image data.
#
def data_css
%{url(#{embed_uri})}
end
##
# Returns a temporary data URI that will be replaced
# with the actual data URI at runtime by javascript.
#
alias embed_uri object_id
##
# Returns an image tag that renders an embedded data URI.
#
def to_xhtml atts = {}
atts[:alt] ||= name
atts[:src] = embed_uri
atts[:class] = :icon
""
end
end
ICON_BY_NAME = {}
ICON_DEFS.each_pair do |name, path|
format = File.extname(path).sub('.', '')
origin = path[/^\w+/]
path = File.join(ICONS_DIR, path) # make the path absolute
data = base_64_encode_file(path)
ICON_BY_NAME[name] = Icon.new(origin, path, name, format, data)
end
ICONS = ICON_BY_NAME.values
class Template::Sandbox
##
# Protects the given content from the text-to-XHTML conversion process.
#
def verbatim content
::ERB::Util.html_escape content
end
##
# Returns XHTML for a hyperlink to the given
# URL of the given label and mouse-hover title.
#
def hyperlink url, label = url, title = nil
%{#{label}}
end
##
# Returns an image tag that embeds the given image file.
#
# [path]
# path to the image file
#
# [format]
# format of the image file (e.g. PNG, JPEG, GIF, etc.)
#
# [atts]
# additional attributes for the image tag
#
def embed_image_file path, format = path[/\w+/], atts = {}
data = ERBook.base_64_encode File.read(path)
embed_image_data data, format, atts
end
##
# Returns an image tag that embeds the given raw image data.
#
# [data]
# raw image data
#
# [format]
# format of the image file (e.g. PNG, JPEG, GIF, etc.)
#
# [atts]
# additional attributes for the image tag
#
def embed_image_data data, format, atts = {}
atts[:src] = ERBook.base_64_embed_image_data(data, format)
""
end
##
# Allows float nodes to be instantiated implicitly by name.
#
def method_missing name, *args, &block
if name.to_s =~ /!$/
args[2] = $` # the type of this float node
float(*args, &block)
else
super
end
end
end
class Document::Node
def index?
definition['index']
end
def index_toc?
Array(definition['index']).include? 'tree'
end
def index_lof?
Array(definition['index']).include? 'list'
end
# utility methods
def type_frag #:nodoc:
"__#{type}__"
end
def type_label #:nodoc:
ERBook::PHRASES[type.to_s.capitalize]
end
##
# Returns the title of this node as XHTML.
#
def title_xhtml
title.to_s.to_xhtml
end
##
# Returns the content of this node as XHTML.
#
def content_xhtml
content.join.to_xhtml
end
##
# Returns the result of wrapping
# this node's content in the given
# tag and converting it into XHTML.
#
def wrap_content_xhtml tag, atts = {}
%{<#{tag}#{atts.to_xml_atts}>#{content.join}#{tag}>}.to_xhtml
end
##
# Returns the content of this node as XHTML inside a
.
#
def content_xhtml_div #:nodoc:
%{
#{content_xhtml}
}
end
##
# Returns a hyperlink to this node containing its title.
#
def title_link title = nil
title || title_xhtml
end
##
# Returns a hyperlink to this node containing its section number.
#
def section_number_link
section_number
end
##
# Returns a hyperlink to this node containing its ordinal number.
#
def ordinal_number_link
[type_label, ordinal_number].compact.join(' ')
end
##
# Returns a hyperlink to this node containing
# its ocurrence number and its title.
#
def ordinal_number_and_title_link #:nodoc:
"#{ordinal_number_link}. #{title_link}"
end
##
# Returns a hyperlink to this
# node containing its section
# number and its title.
#
def section_number_and_title_link #:nodoc:
"#{section_number_link} #{title_link}"
end
##
# Returns a navigation menu for this node.
#
def navigation
self.class.navigation(
here_frag,
(list_frag if index?),
(prev_node.here_frag if prev_node),
(next_node.here_frag if next_node)
)
end
def parent_tabs_begin #:nodoc:
if p = parent and pc = p.toc_children and self == pc.first
%{
}
end
end
def parent_tabs_end #:nodoc:
if p = parent and self == p.toc_children.last
'
'
end
end
##
# Returns all children of this node which are
# configured to appear in the table of contents.
#
def toc_children
children.select {|c| c.index_toc? }
end
HERE_TEXT = ERBook::PHRASES['Here']
PREV_TEXT = ERBook::PHRASES['Previous']
NEXT_TEXT = ERBook::PHRASES['Next']
LIST_TEXT = ERBook::PHRASES['Contents']
HERE_SIGN = ICON_BY_NAME['nav_here'].to_xhtml(:alt => "[#{HERE_TEXT}]")
PREV_SIGN = ICON_BY_NAME['nav_prev'].to_xhtml(:alt => "[#{PREV_TEXT}]")
NEXT_SIGN = ICON_BY_NAME['nav_next'].to_xhtml(:alt => "[#{NEXT_TEXT}]")
LIST_SIGN = ICON_BY_NAME['nav_list'].to_xhtml(:alt => "[#{LIST_TEXT}]")
##
# Calculates a local navigation menu containing links
# to the given URI fragments (which can be nil).
#
def self.navigation here_frag, list_frag, prev_frag, next_frag
here_link = %{#{HERE_SIGN}} if here_frag
prev_link = %{#{PREV_SIGN}} if prev_frag
next_link = %{#{NEXT_SIGN}} if next_frag
list_link = %{#{LIST_SIGN}} if list_frag
%{
}
end
##
# Returns a hyperlink to this node.
#
# [label]
# Optional label (may contain XHTML) for the hyperlink.
#
# If not specified, the title and designation of
# this node will be used as the label instead.
#
def xref_link label = nil
prefix = [type_label, section_number || ordinal_number].
compact.join(' ')
caption =
if type == 'reference'
prefix
else
[prefix, (%{"#{title}"} if label && title)].compact.join('. ')
end
label_xhtml = (label || title).to_s.to_xhtml
%{#{label_xhtml}}
end
# URI fragments
@@frags = []
##
# Returns a unique URI fragment for this node.
#
def here_frag #:nodoc:
unless defined? @here_frag
salt = object_id.abs
frag = (id || title || salt).to_s.to_uri_fragment
# make it unique
while @@frags.include? frag
frag = [frag, section_number || ordinal_number || salt].
join(' ').to_uri_fragment
end
@@frags << frag
@here_frag = frag
end
@here_frag
end
##
# Returns the URI fragment for the location in the table
# of contents / list of figures that points this node.
#
def list_frag #:nodoc:
@list_frag ||= "_contents:#{here_frag}".to_uri_fragment
end
end
end
nodes:
# theory
node: &node
index: false
chain: false
number: false
silent: false
inline: true
output: <%= @node.content_xhtml %>
text: &text
<<: *node
inline: false
output: <%= @node.wrap_content_xhtml :pre %>
code:
<<: *text
params: language
output: <%= @node.wrap_content_xhtml :code, :lang => @node.language %>
# structure
header: &header
<<: *node
silent: true
header_outside_above: &header_insert
<<: *header
output: |
<%= @node.parent_tabs_begin %>
}
end
%>
% if $title || $subtitle
<%=
[$title, $subtitle].compact.map do |t|
t.to_s.to_xhtml
end.join(' — ')
%>
% if $authors
% if $date
% if $feeds
% $feeds.each_pair do |url, fmt|
% @format['styles'].each do |style|
% style.each_pair do |media, sass|
<%
text_only_browser_divider = %{
}.strip
%>
%= node.output if node = @nodes_by_type['header_outside_above'].first
%= node.output if node = @nodes_by_type['header_inside_above'].first
% if header = @nodes_by_type['header'].first
<%= header.output %>
% else
% if $logo
<%= $logo %>
% if $title
<%= $title.to_s.to_xhtml %>
% if $subtitle
<%= $subtitle.to_s.to_xhtml %>
% if $authors
<%=
$authors.map do |name, url|
if url
%{#{name}}
else
name
end
end.join(', ')
%>
% if $date
<%= $date %>
%= node.output if node = @nodes_by_type['header_inside_below'].first
%= node.output if node = @nodes_by_type['header_outside_below'].first
%= node.output if node = @nodes_by_type['footer_outside_above'].first
%= node.output if node = @nodes_by_type['footer_outside_below'].first
javascript: |
$(function() {
/*<%= File.read File.join(ERBook::INSTALL, 'LICENSE') %>*/
"use strict";
//--------------------------------------------------------------------------
// utility logic
//--------------------------------------------------------------------------
//
// Returns the tab corresponding to the
// given panel in the given tabs widget.
//
function tab_by_panel(panel, tabs_widget) {
if (!tabs_widget) {
tabs_widget = panel.parent('.ui-tabs');
}
return tabs_widget.find(
'.ui-tabs-nav > li > a[href=#'+ panel.attr('id') +']'
).parent('li');
}
//
// Returns a jQuery element containing a jQuery UI icon of the given name.
//
function ui_icon(name) {
return $('').addClass('ui-state-default ui-corner-all').append(
$('').addClass('ui-icon ui-icon-' + name)
);
}
//--------------------------------------------------------------------------
// document location
//--------------------------------------------------------------------------
var $last_hash;
//
// Sets the location bar hash to the given value.
//
// [prevent_jump]
// If true, prevents the browser from jumping to
// the element corresponding to the given hash.
//
function set_hash(hash, prevent_jump) {
if (hash === $last_hash) {
return;
}
function set_the_hash() {
// XXX: bypass on_hash_change() by setting $last_hash
window.location.hash = $last_hash = hash;
}
if (prevent_jump) {
var target = $(hash);
if (target.length) {
//
// This particular approach to solving the browser
// jumping problem comes from the jQuery.LocalScroll
// plugin, which is dual licensed under MIT and GPL:
//
// Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// The jQuery.LocalScroll plugin is documented at:
//
// http://flesler.blogspot.com/2007/10/jquerylocalscroll-10.html
//
// And its source is available at:
//
// http://flesler-plugins.googlecode.com/svn/trunk/jquery.localScroll/jquery.localScroll.js
//
var target_id = target.attr('id');
// temporarily place a dummy element at the current
// screen position and give it the ID of the target
var original_screen_position = $(window).scrollTop();
var dummy = $('').attr('id', target_id).css({
position: 'absolute',
top: original_screen_position
});
target.removeAttr('id').before(dummy);
// when we set the hash, the browser will jump to the
// dummy, which is where the browser screen currently
// is, and therefore the jump will not be visualized!
set_the_hash();
// undo the temporary changes
dummy.remove();
target.attr('id', target_id);
// the above approach does not work for Opera and IE.
// they ignore the dummy and jump to the actual target
$(window).scrollTop(original_screen_position);
return;
}
}
set_the_hash();
}
//
// Reveals the element at the given hash by (1) activating all
// tabs that contain it, (2) smoothly scrolling to it, and (3)
// updating the hash in the browser's location bar accordingly.
//
function reveal(hash_or_elem, callback) {
var target = $(hash_or_elem);
if (target.length) {
var target_is_panel = target.is('div') &&
target.parent('.ui-tabs').length;
// reveal all tabs which contain the target
if (target.is(':hidden')) {
var panels = target.parents('.ui-tabs > div:hidden').get().reverse();
if (target_is_panel) {
panels.push(target);
}
$(panels).each(function() {
var panel = $(this);
var tabs_widget = panel.parent('.ui-tabs');
var selected_index = tabs_widget.tabs('option', 'selected');
// map the panel to its tab because tabs do not
// have to be in the same order as their panels
var tab = tab_by_panel(panel, tabs_widget);
var wanted_index = tab.prevAll('li').length;
if (wanted_index !== selected_index) {
tabs_widget.tabs('select', wanted_index);
}
});
}
// scroll to nearest visible element if the target
// is naturally hidden (e.g. because of a CSS rule)
if (target.is(':hidden')) {
target = target.closest(':not(:hidden)');
}
// scroll to the tab bar instead of the target
// because it contains the title for the target
var scroll_target = target;
if (target_is_panel) {
var tabs_nav = target.parent('.ui-tabs').find('> .ui-tabs-nav');
if (!tabs_nav.is(':hidden')) {
scroll_target = tabs_nav;
}
}
var scroll_offset = Math.floor(scroll_target.offset().top);
// set body height so that any element can be
// brought to the top of the screen via scrolling
var document_height = $(document).height();
var window_height = $(window).height();
if (document_height - scroll_offset <= window_height) {
$('body').css('min-height', document_height + window_height);
}
var pending = true;
$('html, body').animate(
{ scrollTop: scroll_offset }, 'slow', 'swing', function() {
// prevent the body of this function from being
// executed twice -- because there are 2 target
// elements in the animation list and only one
// of them actually works in different browsers!
if (pending && scroll_offset === $(window).scrollTop()) {
pending = false;
var target_id = target.attr('id');
set_hash('#' + (target_id || '_reveal' + Math.random()),
target_is_panel || !target_id);
if (callback && typeof(callback) === 'function') {
callback();
}
}
}
);
}
}
//--------------------------------------------------------------------------
// search engine
//--------------------------------------------------------------------------
// Adapted from the source code of the ":contains" selector from
// the "Search on this Page" plugin by Waldek Mastykarz
// , which was (1) published online on
// 17 November 2008 at
// and , and was
// (2) updated to work with jQuery 1.3 by "anonymous" here:
// .
jQuery.expr[':'].matches = function(a, i, m) {
// compile the regexp during the first call to this
// function only, so that subsequent calls are fast
if (typeof(m[3]) === 'string') {
m[3] = new RegExp(m[3], 'i');
}
return jQuery(a).text().match(m[3]);
};
$('.tabs:first > ul').append(
$('').append(
$('').attr('id', '_search_link').attr('href', '#_search').
text(<%= ERBook::PHRASES['Search'].inspect %>)
).hide()
).append(
$('').attr('id', '_search_form').append(
$('').attr('type', 'text').attr('name', 'q').
addClass('initial').attr('title', <%=
ERBook::PHRASES['Search with regular expression'].inspect
%>).focus(function() {
var box = $(this);
if (box.is('.initial')) {
box.val('').removeClass('initial');
}
}).blur(function() {
var box = $(this);
if (! box.val().match(/\S/)) {
box.val(box.attr('title')).addClass('initial');
}
}).blur()
).append(
ui_icon('search').attr('id', '_search_start').click(function() {
$('#_search_form').submit();
})
).append(
ui_icon('cancel').attr('id', '_search_stop').hide().click(function() {
$(this).attr('clicked', 'clicked');
})
).submit(function(event) {
event.preventDefault();
var search_box = $(this).find(':text');
var query = search_box.val().replace(/^\s+|\s+$/g, '');
// ignore empty queries
if (!query.match(/\S/)) {
return;
}
// detect invalid regexps
try {
new RegExp(query);
}
catch(e) {
alert(e);
return;
}
// one search at a time
if (search_box.attr('disabled')) {
return;
}
search_box.attr('disabled', 'disabled');
// begin the search
var status = $('#_search > .status');
var results = $('#_search > .results');
// display the search results tab
$('#_search_link').click().parent().show();
// clear previous search results
results.text('');
status.text(<%= ERBook::PHRASES['Searching...'].inspect %>);
// timeout allows status updates to appear
setTimeout(function() {
var num_results = 0;
function emit_result(match) {
// exclude matches from previous search results,
// the table of contents, and jQuery UI tab bars
if (match.closest('#_search, #_contents, .ui-tabs-nav').length) {
return;
}
var excerpt;
// resolve tab panels into their section title
// because additional matches, which lie within
// the content of these tab panels, will come
if (match.is('.ui-tabs-panel')) {
excerpt = match.find('.title:first').clone().show();
}
else {
excerpt = match.clone();
}
// highlight the query inside the match excerpt
excerpt.html(excerpt.html().replace(
new RegExp('(<[^>]*)?(' + query + ')', 'ig'),
function($0, $1, $2) {
// only highlight if not inside an XML tag literal
return $1 ? $0 :
'' + $2 + '';
}
));
num_results += 1;
var result = $('').addClass('result').append(
$('').addClass('excerpt').append(excerpt)
).attr('id', '_search' + num_results).click(
function(event) {
event.stopPropagation();
// save this search result's hash in browser history
// before revealing the matching element so that
// the user can press the back button to return to
// this exact spot in the search results listing
reveal(this, function() {
if (!$(event.target).is('a')) {
reveal(match);
}
});
}
);
// show bread-crumb trail for the match
var first = true;
match.parents('.ui-tabs-panel').each(function() {
var panel = $(this);
var tab = tab_by_panel(panel);
if (tab.length) {
if (!first) {
result.prepend(' 〉 ');
}
first = false;
result.prepend(tab.find('a').clone());
}
});
results.append(result);
// unhide any hidden elements in the search result
result.find(':hidden').removeClass('ui-tabs-hide').show();
// remove tab bars (used for titles) and mini navigation menus
result.find('.ui-tabs-nav, .ui-tabs-panel > .nav').remove();
};
var matching_elems = $('#_body').
find(':matches("'+ query.replace(/"/, '\\"') +'")');
status.text('').progressbar();
var start_button = $('#_search_start').hide();
var stop_button = $('#_search_stop').show();
var prev_match;
var prev_elem;
function process_match(i, n) {
status.progressbar('value', Math.round(i / n * 100));
var elem = matching_elems.eq(i);
// ascend to a larger container for more context
var match = elem.closest('pre,ul,ol,dl,table,p,div');
if (!match.length) {
match = elem;
}
if (
prev_match && prev_match.length &&
match.get(0) !== prev_match.get(0) && // unique matches only
elem.parent().get(0) !== prev_elem.get(0) // leaf nodes only
) {
emit_result(prev_match);
}
prev_elem = elem;
prev_match = match;
if (i < n && !stop_button.attr('clicked')) {
// timeout allows status updates to appear
setTimeout(function() { process_match(i + 1, n); }, 0);
}
else {
// handle the last item in this two-stage prev/curr loop
if (prev_match && prev_match.length) {
emit_result(prev_match);
}
status.progressbar('destroy').text(<%=
ERBook::PHRASES['%d results', 0].inspect
%>.replace('0', num_results));
search_box.removeAttr('disabled');
stop_button.hide().removeAttr('clicked');
start_button.show();
}
}
process_match(0, matching_elems.length);
}, 0);
return false;
})
).after(
$('').attr('id', '_search').append(
$('').addClass('status')
).append(
$('').addClass('results')
)
);
//--------------------------------------------------------------------------
// create print preview toggle
//--------------------------------------------------------------------------
$('#_header > .authors_and_date').append(
$('').addClass('printer_friendly_toggle').append(
$('').attr('type', 'checkbox').
attr('id', 'printer_friendly_toggle').click(function() {
var checkbox = $(this);
function change_media(src, dst) {
var styles = $('style[media="'+ src +'"]');
if (styles.length) {
// try changing the media in-place
var before = checkbox.offset();
styles.attr('media', dst);
var after = checkbox.offset();
// but if that did not work
if (after.top === before.top && after.left === before.left) {
// try reinserting the