instiki/vendor/plugins/maruku/lib/maruku/output/to_html.rb
2010-06-01 10:39:55 -05:00

995 lines
24 KiB
Ruby

#--
# Copyright (C) 2006 Andrea Censi <andrea (at) rubyforge.org>
#
# This file is part of Maruku.
#
# Maruku is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Maruku is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Maruku; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#++
require 'rexml/document'
begin
require 'rexml/formatters/pretty'
require 'rexml/formatters/default'
$rexml_new_version = true
rescue LoadError
$rexml_new_version = false
end
class String
# A string is rendered into HTML by creating
# a REXML::Text node. REXML takes care of all the encoding.
def to_html
REXML::Text.new(self)
end
end
# This module groups all functions related to HTML export.
module MaRuKu; module Out; module HTML
include REXML
# Render as an HTML fragment (no head, just the content of BODY). (returns a string)
def to_html(context={})
indent = context[:indent] || -1
ie_hack = context[:ie_hack] || true
div = Element.new 'dummy'
children_to_html.each do |e|
div << e
end
# render footnotes
if @doc.footnotes_order.size > 0
div << render_footnotes
end
doc = Document.new(nil,{:respect_whitespace =>:all})
doc << div
# REXML Bug? if indent!=-1 whitespace is not respected for 'pre' elements
# containing code.
xml =""
if $rexml_new_version
formatter = if indent > -1
REXML::Formatters::Pretty.new( indent, ie_hack )
else
REXML::Formatters::Default.new( ie_hack )
end
formatter.write( div, xml)
else
div.write(xml,indent,transitive=true,ie_hack)
end
xml.gsub!(/\A<dummy>\s*/,'')
xml.gsub!(/\s*<\/dummy>\Z/,'')
xml.gsub!(/\A<dummy\s*\/>/,'')
xml
end
# Render to a complete HTML document (returns a string)
def to_html_document(context={})
indent = context[:indent] || -1
ie_hack = context[:ie_hack] ||true
doc = to_html_document_tree
xml = ""
# REXML Bug? if indent!=-1 whitespace is not respected for 'pre' elements
# containing code.
doc.write(xml,indent,transitive=true,ie_hack);
Xhtml11_mathml2_svg11 + xml
end
Xhtml10strict =
"<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN'
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>\n"
Xhtml11strict_mathml2 = '<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0//EN"
"http://www.w3.org/TR/MathML2/dtd/xhtml-math11-f.dtd" [
<!ENTITY mathml "http://www.w3.org/1998/Math/MathML">
]>
'
Xhtml11_mathml2_svg11 =
'<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN"
"http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">
'
def xml_newline() Text.new("\n") end
=begin maruku_doc
Attribute: title
Scope: document
Sets the title of the document.
If a title is not specified, the first header will be used.
These should be equivalent:
Title: my document
Content
and
my document
===========
Content
In both cases, the title is set to "my document".
=end
=begin maruku_doc
Attribute: doc_prefix
Scope: document
String to disambiguate footnote links.
=end
=begin maruku_doc
Attribute: subject
Scope: document
Synonim for `title`.
=end
# Render to an HTML fragment (returns a REXML document tree)
def to_html_tree
div = Element.new 'div'
div.attributes['class'] = 'maruku_wrapper_div'
children_to_html.each do |e|
div << e
end
# render footnotes
if @doc.footnotes_order.size > 0
div << render_footnotes
end
doc = Document.new(nil,{:respect_whitespace =>:all})
doc << div
end
=begin maruku_doc
Attribute: css
Scope: document
Output: HTML
Summary: Activates CSS stylesheets for HTML.
`css` should be a space-separated list of urls.
Example:
CSS: style.css math.css
=end
METAS = %w{description keywords author revised}
# Render to a complete HTML document (returns a REXML document tree)
def to_html_document_tree
doc = Document.new(nil,{:respect_whitespace =>:all})
# doc << XMLDecl.new
root = Element.new('html', doc)
root.add_namespace('http://www.w3.org/1999/xhtml')
root.add_namespace('svg', "http://www.w3.org/2000/svg" )
lang = self.attributes[:lang] || 'en'
root.attributes['xml:lang'] = lang
root << xml_newline
head = Element.new 'head', root
#<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
me = Element.new 'meta', head
me.attributes['http-equiv'] = 'Content-type'
# me.attributes['content'] = 'text/html;charset=utf-8'
me.attributes['content'] = 'application/xhtml+xml;charset=utf-8'
METAS.each do |m|
if value = self.attributes[m.to_sym]
meta = Element.new 'meta', head
meta.attributes['name'] = m
meta.attributes['content'] = value.to_s
end
end
self.attributes.each do |k,v|
if k.to_s =~ /\Ameta-(.*)\Z/
meta = Element.new 'meta', head
meta.attributes['name'] = $1
meta.attributes['content'] = v.to_s
end
end
# Create title element
doc_title = self.attributes[:title] || self.attributes[:subject] || ""
title = Element.new 'title', head
title << Text.new(doc_title)
add_css_to(head)
root << xml_newline
body = Element.new 'body'
children_to_html.each do |e|
body << e
end
# render footnotes
if @doc.footnotes_order.size > 0
body << render_footnotes
end
# When we are rendering a whole document, we add a signature
# at the bottom.
if get_setting(:maruku_signature)
body << maruku_html_signature
end
root << body
doc
end
def add_css_to(head)
if css_list = self.attributes[:css]
css_list.split.each do |css|
# <link type="text/css" rel="stylesheet" href="..." />
link = Element.new 'link'
link.attributes['type'] = 'text/css'
link.attributes['rel'] = 'stylesheet'
link.attributes['href'] = css
head << link
head << xml_newline
end
end
end
# returns "st","nd","rd" or "th" as appropriate
def day_suffix(day)
s = {
1 => 'st',
2 => 'nd',
3 => 'rd',
21 => 'st',
22 => 'nd',
23 => 'rd',
31 => 'st'
}
return s[day] || 'th';
end
# formats a nice date
def nice_date
t = Time.now
t.strftime(" at %H:%M on ")+
t.strftime("%A, %B %d")+
day_suffix(t.day)+
t.strftime(", %Y")
end
def maruku_html_signature
div = Element.new 'div'
div.attributes['class'] = 'maruku_signature'
Element.new 'hr', div
span = Element.new 'span', div
span.attributes['style'] = 'font-size: small; font-style: italic'
span << Text.new('Created by ')
a = Element.new('a', span)
a.attributes['href'] = 'http://maruku.rubyforge.org'
a.attributes['title'] = 'Maruku: a Markdown-superset interpreter for Ruby'
a << Text.new('Maruku')
span << Text.new(nice_date+".")
div
end
def render_footnotes()
div = Element.new 'div'
div.attributes['class'] = 'footnotes'
div << Element.new('hr')
ol = Element.new 'ol'
@doc.footnotes_order.each_with_index do |fid, i| num = i+1
f = self.footnotes[fid]
if f
li = f.wrap_as_element('li')
li.attributes['id'] = "#{get_setting(:doc_prefix)}fn:#{num}"
a = Element.new 'a'
a.attributes['href'] = "\##{get_setting(:doc_prefix)}fnref:#{num}"
a.attributes['rev'] = 'footnote'
a<< Text.new('&#8617;', true, nil, true)
li.insert_after(li.children.last, a)
ol << li
else
maruku_error "Could not find footnote id '#{fid}' among ["+
self.footnotes.keys.map{|s|"'"+s+"'"}.join(', ')+"]."
end
end
div << ol
div
end
def to_html_hrule; create_html_element 'hr' end
def to_html_linebreak; Element.new 'br' end
# renders children as html and wraps into an element of given name
#
# Sets 'id' if meta is set
def wrap_as_element(name, attributes_to_copy=[])
m = create_html_element(name, attributes_to_copy)
children_to_html.each do |e| m << e; end
# m << Comment.new( "{"+self.al.to_md+"}") if not self.al.empty?
# m << Comment.new( @attributes.inspect) if not @attributes.empty?
m
end
=begin maruku_doc
Attribute: id
Scope: element
Output: LaTeX, HTML
It is copied as a standard HTML attribute.
Moreover, it used as a label name for hyperlinks in both HTML and
in PDF.
=end
=begin maruku_doc
Attribute: class
Scope: element
Output: HTML
It is copied as a standard HTML attribute.
=end
=begin maruku_doc
Attribute: style
Scope: element
Output: HTML
It is copied as a standard HTML attribute.
=end
HTML4Attributes = {}
coreattrs = [:id, :class, :style, :title, :accesskey, :contenteditable, :dir, :draggable, :spellcheck, :tabindex]
i18n = [:lang, 'xml:lang'.to_sym]
events = [
:onclick, :ondblclick, :onmousedown, :onmouseup, :onmouseover,
:onmousemove, :onmouseout,
:onkeypress, :onkeydown, :onkeyup]
attrs = coreattrs + i18n + events
cellhalign = [:align, :char, :charoff]
cellvalign = [:valign]
[
['body', attrs + [:onload, :onunload]],
['address', attrs],
['div', attrs],
['a', attrs+[:charset, :type, :name, :rel, :rev, :accesskey, :shape, :coords, :tabindex,
:onfocus,:onblur]],
['img', attrs + [:longdesc, :name, :height, :width, :alt] ],
['p', attrs],
[['h1','h2','h3','h4','h5','h6'], attrs],
[['pre'], attrs],
[['q', 'blockquote'], attrs+[:cite]],
[['ins','del'], attrs+[:cite,:datetime]],
[['ol'], attrs+[:reversed, :start]],
[['ul'], attrs],
[['li'], attrs+[:value]],
['table',attrs+[:summary, :width, :frame, :rules, :border, :cellspacing, :cellpadding]],
['caption',attrs],
[['colgroup','col'],attrs+[:span, :width]+cellhalign+cellvalign],
[['thead','tbody','tfoot'], attrs+cellhalign+cellvalign],
[['td','td','th'], attrs+[:abbr, :axis, :headers, :scope, :rowspan, :colspan, :cellvalign, :cellhalign]],
# altri
[['em','code','strong','hr','span','dl','dd','dt'], attrs]
].each do |el, a| [*el].each do |e| HTML4Attributes[e] = a end end
def create_html_element(name, attributes_to_copy=[])
m = Element.new name
if atts = HTML4Attributes[name] then
atts.each do |att|
if v = @attributes[att] then
m.attributes[att.to_s] = v.to_s
end
end
else
# puts "not atts for #{name.inspect}"
end
m
end
def to_html_ul
if @attributes[:toc]
# render toc
html_toc = @doc.toc.to_html
return html_toc
else
add_ws wrap_as_element('ul')
end
end
def to_html_paragraph; add_ws wrap_as_element('p') end
def to_html_ol; add_ws wrap_as_element('ol') end
def to_html_li; add_ws wrap_as_element('li') end
def to_html_li_span; add_ws wrap_as_element('li') end
def to_html_quote; add_ws wrap_as_element('blockquote') end
def to_html_strong; wrap_as_element('strong') end
def to_html_emphasis; wrap_as_element('em') end
=begin maruku_doc
Attribute: use_numbered_headers
Scope: document
Summary: Activates the numbering of headers.
If `true`, section headers will be numbered.
In LaTeX export, the numbering of headers is managed
by Maruku, to have the same results in both HTML and LaTeX.
=end
# nil if not applicable, else string
def section_number
return nil if not get_setting(:use_numbered_headers)
n = @attributes[:section_number]
if n && (not n.empty?)
n.join('.')+". "
else
nil
end
end
# nil if not applicable, else SPAN element
def render_section_number
# if we are bound to a section, add section number
if num = section_number
span = Element.new 'span'
span.attributes['class'] = 'maruku_section_number'
span << Text.new(section_number)
span
else
nil
end
end
def to_html_header
element_name = "h#{self.level}"
h = wrap_as_element element_name
if span = render_section_number
h.insert_before(h.children.first, span)
end
add_ws h
end
def source2html(source)
# source = source.gsub(/&/,'&amp;')
source = Text.normalize(source)
source = source.gsub(/\&apos;/,'&#39;') # IE bug
source = source.gsub(/'/,'&#39;') # IE bug
Text.new(source, true, nil, true )
end
=begin maruku_doc
Attribute: html_use_syntax
Scope: global, document, element
Output: HTML
Summary: Enables the use of the `syntax` package.
Related: lang, code_lang
Default: <?mrk md_code(Globals[:html_use_syntax].to_s) ?>
If true, the `syntax` package is used. It supports the `ruby` and `xml`
languages. Remember to set the `lang` attribute of the code block.
Examples:
require 'maruku'
{:lang=ruby html_use_syntax=true}
and
<div style="text-align:center">Div</div>
{:lang=html html_use_syntax=true}
produces:
require 'maruku'
{:lang=ruby html_use_syntax=true}
and
<div style="text-align:center">Div</div>
{:lang=html html_use_syntax=true}
=end
$syntax_loaded = false
def to_html_code;
source = self.raw_code
lang = self.attributes[:lang] || @doc.attributes[:code_lang]
lang = 'xml' if lang=='html'
lang = 'css21' if lang == 'css'
use_syntax = get_setting :html_use_syntax
element =
if use_syntax && lang
begin
if not $syntax_loaded
require 'rubygems'
require 'syntax'
require 'syntax/convertors/html'
$syntax_loaded = true
end
convertor = Syntax::Convertors::HTML.for_syntax lang
# eliminate trailing newlines otherwise Syntax crashes
source = source.gsub(/\n*\Z/,'')
html = convertor.convert( source )
html = html.gsub(/\&apos;/,'&#39;') # IE bug
html = html.gsub(/'/,'&#39;') # IE bug
# html = html.gsub(/&/,'&amp;')
code = Document.new(html, {:respect_whitespace =>:all}).root
code.name = 'code'
code.attributes['lang'] = lang
pre = Element.new 'pre'
pre.attributes['class'] = lang
pre << code
pre
rescue LoadError => e
maruku_error "Could not load package 'syntax'.\n"+
"Please install it, for example using 'gem install syntax'."
to_html_code_using_pre(source)
rescue Object => e
maruku_error"Error while using the syntax library for code:\n#{source.inspect}"+
"Lang is #{lang} object is: \n"+
self.inspect +
"\nException: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
tell_user("Using normal PRE because the syntax library did not work.")
to_html_code_using_pre(source)
end
else
to_html_code_using_pre(source)
end
color = get_setting(:code_background_color)
if color != Globals[:code_background_color]
element.attributes['style'] = "background-color: #{color};"
end
add_ws element
end
=begin maruku_doc
Attribute: code_background_color
Scope: global, document, element
Summary: Background color for code blocks.
The format is either a named color (`green`, `red`) or a CSS color
of the form `#ff00ff`.
* for **HTML output**, the value is put straight in the `background-color` CSS
property of the block.
* for **LaTeX output**, if it is a named color, it must be a color accepted
by the LaTeX `color` packages. If it is of the form `#ff00ff`, Maruku
defines a color using the `\color[rgb]{r,g,b}` macro.
For example, for `#0000ff`, the macro is called as: `\color[rgb]{0,0,1}`.
=end
def to_html_code_using_pre(source)
pre = create_html_element 'pre'
code = Element.new 'code', pre
s = source
# s = s.gsub(/&/,'&amp;')
s = Text.normalize(s)
s = s.gsub(/\&apos;/,'&#39;') # IE bug
s = s.gsub(/'/,'&#39;') # IE bug
if get_setting(:code_show_spaces)
# 187 = raquo
# 160 = nbsp
# 172 = not
s.gsub!(/\t/,'&#187;'+'&#160;'*3)
s.gsub!(/ /,'&#172;')
end
text = Text.new(s, respect_ws=true, parent=nil, raw=true )
if lang = self.attributes[:lang]
code.attributes['lang'] = lang
code.attributes['class'] = lang
end
code << text
pre
end
def to_html_inline_code;
pre = create_html_element 'code'
source = self.raw_code
pre << source2html(source)
color = get_setting(:code_background_color)
if color != Globals[:code_background_color]
pre.attributes['style'] = "background-color: #{color};"+(pre.attributes['style']||"")
end
pre
end
def add_class_to(el, cl)
el.attributes['class'] =
if already = el.attributes['class']
already + " " + cl
else
cl
end
end
def add_class_to_link(a)
return # not ready yet
# url = a.attributes['href']
# return if not url
#
# if url =~ /^#/
# add_class_to(a, 'maruku-link-samedoc')
# elsif url =~ /^http:/
# add_class_to(a, 'maruku-link-external')
# else
# add_class_to(a, 'maruku-link-local')
# end
#
# puts a.attributes['class']
end
def to_html_immediate_link
a = create_html_element 'a'
url = self.url
text = url.gsub(/^mailto:/,'') # don't show mailto
a << Text.new(text)
a.attributes['href'] = url
add_class_to_link(a)
a
end
def to_html_link
a = wrap_as_element 'a'
id = self.ref_id
if ref = @doc.refs[id]
url = ref[:url]
title = ref[:title]
a.attributes['href'] = url if url
a.attributes['title'] = title if title
else
maruku_error "Could not find ref_id = #{id.inspect} for #{self.inspect}\n"+
"Available refs are #{@doc.refs.keys.inspect}"
tell_user "Not creating a link for ref_id = #{id.inspect}."
return wrap_as_element('span')
end
# add_class_to_link(a)
return a
end
def to_html_im_link
if url = self.url
title = self.title
a = wrap_as_element 'a'
a.attributes['href'] = url
a.attributes['title'] = title if title
return a
else
maruku_error"Could not find url in #{self.inspect}"
tell_user "Not creating a link for ref_id = #{id.inspect}."
return wrap_as_element('span')
end
end
def add_ws(e)
[Text.new("\n"), e, Text.new("\n")]
end
##### Email address
def obfuscate(s)
res = ''
s.each_byte do |char|
res += "&#%03d;" % char
end
res
end
def to_html_email_address
email = self.email
a = create_html_element 'a'
#a.attributes['href'] = Text.new("mailto:"+obfuscate(email),false,nil,true)
#a.attributes.add Attribute.new('href',Text.new(
#"mailto:"+obfuscate(email),false,nil,true))
# Sorry, for the moment it doesn't work
a.attributes['href'] = "mailto:#{email}"
a << Text.new(obfuscate(email),false,nil,true)
a
end
##### Images
def to_html_image
a = create_html_element 'img'
id = self.ref_id
if ref = @doc.refs[id]
url = ref[:url]
title = ref[:title]
a.attributes['src'] = url.to_s
a.attributes['alt'] = children_to_s
else
maruku_error"Could not find id = #{id.inspect} for\n #{self.inspect}"
tell_user "Could not create image with ref_id = #{id.inspect};"+
" Using SPAN element as replacement."
return wrap_as_element('span')
end
return a
end
def to_html_im_image
if not url = self.url
maruku_error "Image with no url: #{self.inspect}"
tell_user "Could not create image with ref_id = #{id.inspect};"+
" Using SPAN element as replacement."
return wrap_as_element('span')
end
title = self.title
a = create_html_element 'img'
a.attributes['src'] = url.to_s
a.attributes['alt'] = children_to_s
return a
end
=begin maruku_doc
Attribute: filter_html
Scope: document
If true, raw HTML is discarded from the output.
=end
def to_html_raw_html
return [] if get_setting(:filter_html)
raw_html = self.raw_html
if rexml_doc = @parsed_html
root = rexml_doc.root
if root.nil?
s = "Bug in REXML: root() of Document is nil: \n#{rexml_doc.inspect}\n"+
"Raw HTML:\n#{raw_html.inspect}"
maruku_error s
tell_user 'The REXML version you have has a bug, omitting HTML'
div = Element.new 'div'
#div << Text.new(s)
return div
end
# copies the @children array (FIXME is it deep?)
elements = root.to_a
return elements
else # invalid
# Creates red box with offending HTML
tell_user "Wrapping bad html in a PRE with class 'markdown-html-error'\n"+
add_tabs(raw_html,1,'|')
pre = Element.new('pre')
pre.attributes['style'] = 'border: solid 3px red; background-color: pink'
pre.attributes['class'] = 'markdown-html-error'
pre << Text.new("REXML could not parse this XML/HTML: \n#{raw_html}", true)
return pre
end
end
def to_html_abbr
abbr = Element.new 'abbr'
abbr << Text.new(children[0])
abbr.attributes['title'] = self.title if self.title
abbr
end
def to_html_footnote_reference
id = self.footnote_id
# save the order of used footnotes
order = @doc.footnotes_order
if order.include? id
# footnote has already been used
return []
end
if not @doc.footnotes[id]
return []
end
# take next number
order << id
#num = order.size;
num = order.index(id) + 1
sup = Element.new 'sup'
sup.attributes['id'] = "#{get_setting(:doc_prefix)}fnref:#{num}"
a = Element.new 'a'
a << Text.new(num.to_s)
a.attributes['href'] = "\##{get_setting(:doc_prefix)}fn:#{num}"
a.attributes['rel'] = 'footnote'
sup << a
sup
end
## Definition lists ###
def to_html_definition_list() add_ws wrap_as_element('dl') end
def to_html_definition() children_to_html end
def to_html_definition_term() add_ws wrap_as_element('dt') end
def to_html_definition_data() add_ws wrap_as_element('dd') end
# FIXME: Ugly code
def to_html_table
align = self.align
num_columns = align.size
head = @children.slice(0, num_columns)
rows = []
i = num_columns
while i<@children.size
rows << @children.slice(i, num_columns)
i += num_columns
end
table = create_html_element 'table'
thead = Element.new 'thead'
tr = Element.new 'tr'
array_to_html(head).each do |x| tr<<x end
thead << tr
table << thead
tbody = Element.new 'tbody'
rows.each do |row|
tr = Element.new 'tr'
array_to_html(row).each_with_index do |x,i|
x.attributes['style'] ="text-align: #{align[i].to_s};"
tr<<x
end
tbody << tr << Text.new("\n")
end
table << tbody
table
end
def to_html_head_cell; wrap_as_element('th') end
def to_html_cell
if @attributes[:scope]
wrap_as_element('th', [:scope])
else
wrap_as_element('td')
end
end
def to_html_entity
MaRuKu::Out::Latex.need_entity_table
entity_name = self.entity_name
if (e = MaRuKu::Out::Latex::ENTITY_TABLE[entity_name]) && e.html_num
entity_name = e.html_num
end
# Fix for Internet Explorer
if entity_name == 'apos'
entity_name = 39
end
if entity_name.kind_of? Fixnum
# Entity.new(entity_name)
Text.new('&#%d;' % [entity_name], false, nil, true)
else
Text.new('&%s;' % [entity_name], false, nil, true)
end
end
def to_html_xml_instr
target = self.target || ''
code = self.code || ''
REXML::Instruction.new(target, code)
end
# Convert each child to html
def children_to_html
array_to_html(@children)
end
def array_to_html(array)
e = []
array.each do |c|
method = c.kind_of?(MDElement) ?
"to_html_#{c.node_type}" : "to_html"
if not c.respond_to?(method)
#raise "Object does not answer to #{method}: #{c.class} #{c.inspect}"
next
end
h = c.send(method)
if h.nil?
raise "Nil html created by method #{method}:\n#{h.inspect}\n"+
" for object #{c.inspect[0,300]}"
end
if h.kind_of?Array
e = e + h #h.each do |hh| e << hh end
else
e << h
end
end
e
end
def to_html_ref_definition; [] end
def to_latex_ref_definition; [] end
end # HTML
end # out
end # MaRuKu