diff options
| author | Dan Allen <dan.j.allen@gmail.com> | 2013-01-03 05:43:43 -0700 |
|---|---|---|
| committer | Dan Allen <dan.j.allen@gmail.com> | 2013-01-03 05:43:43 -0700 |
| commit | fe13ace9ad2bef848a7105f86290d3e4b09a5be2 (patch) | |
| tree | 8871027042c1322e54c61e8863e19ddc6023a6a4 | |
| parent | c8251210e324053a26c4884d8d86341c12dc8f88 (diff) | |
Add DocBook backend templates + loads of improvements
- make templates for docbook45 backend
- move backend templates to backends/ folder
- load backend templates lazily (based on backend attribute)
- namespace backend templates to avoid conflicts
- extend backend templates from a base template
- add view property to template class
- change InlineLink to InlineAnchor and assign type (:link or :xref)
- simplify shorthand methods (e.g., define attribute) in template classes
- set default backend to html5
- set backend attribute family (backend-*, basebackend, etc)
- set docdate and doctime attributes (match local* w/o file ref)
- prevent Reader from overriding attributes passed to Document.new
- fix list continuation bug in outline and labeled lists
- fold first paragraph properly in outline lists; document in TomDoc
- add convenience methods to String (trim, nuke)
- add TomDoc to methods added to String
- add tests for String monkeypatches
- fix compliance of attribute continuations in Reader
- perform attribute substitutions on document attributes and attribute lists
- apply normal subs to single-quoted attribute values
- cleanup how substitutions are called
- don't need Asciidoctor:: prefix in Substituter
- honor line pass: macro in document attribute value
- move regexs in Reader to Asciidoctor module
- use %r{} syntax to make some regex easier to read
- fix order of replacements
- add ellipsis and single quote replacements
- add space, quot and apos to instrinsics
- move Substituters mixin to AbstractBlock
- make Document an AbstractBlock
- use blocks instance variable in Document instead of elements
- document should store text of reference to match how docbook works
- allow Document.new to be called w/ no arguments
- rename level* regex to section*
- loads of tests to verify numerous compliance checks and for new functionality
- more TomDoc
31 files changed, 2190 insertions, 981 deletions
diff --git a/asciidoctor.gemspec b/asciidoctor.gemspec index 8e70f0b0..13cd7f59 100644 --- a/asciidoctor.gemspec +++ b/asciidoctor.gemspec @@ -97,6 +97,7 @@ Gem::Specification.new do |s| test/paragraphs_test.rb test/preamble_test.rb test/reader_test.rb + test/string_test.rb test/substitutions_test.rb test/test_helper.rb test/text_test.rb diff --git a/lib/asciidoctor.rb b/lib/asciidoctor.rb index 10c64067..c66d5789 100755 --- a/lib/asciidoctor.rb +++ b/lib/asciidoctor.rb @@ -49,6 +49,9 @@ module Asciidoctor # Can influence markup generated by render templates DEFAULT_DOCTYPE = 'article' + # Backend determines the format of the rendered output, default to html5 + DEFAULT_BACKEND = 'html5' + LIST_CONTEXTS = [:ulist, :olist, :dlist] ORDERED_LIST_STYLES = [:arabic, :loweralpha, :lowerroman, :upperalpha, :upperroman] @@ -73,14 +76,23 @@ module Asciidoctor # matches any block delimiter: # open, listing, example, literal, comment, quote, sidebar, passthrough, table # NOTE position most common blocks towards the front of the pattern - :any_blk => /^(?:\-\-|(?:\-|=|\.|\/|_|\*\+){4,}|\|={3,})\s*$/, + :any_blk => %r{^(?:\-\-|(?:\-|=|\.|/|_|\*\+){4,}|\|={3,})\s*$}, + + # :foo: bar + :attr_assign => /^:([^:!]+):\s*(.*)\s*$/, + + # {name?value} + :attr_conditional => /^\s*\{([^\?]+)\?\s*([^\}]+)\s*\}/, # + Attribute values treat lines ending with ' +' as a continuation, # not a line-break as elsewhere in the document, where this is # a forced line break. This should be the same regexp as :line_break, # below, but it gets its own entry because readability ftw, even # though repeating regexps ftl. - :attr_continue => /^(.*)[[:blank:]]\+[[:blank:]]*$/, + :attr_continue => /^[[:blank:]]*(.*)[[:blank:]]\+[[:blank:]]*$/, + + # :foo!: + :attr_delete => /^:([^:]+)!:\s*$/, # An attribute list above a block element # @@ -88,10 +100,12 @@ module Asciidoctor # [quote, Adam Smith, Wealth of Nations] # Or can have name/value pairs # [NOTE, caption="Good to know"] - :attr_list_blk => /^\[(\w.*)\]$/, + # Can be defined by an attribute + # [{lead}] + :attr_list_blk => /^\[([\w\{].*)\]$/, # attribute list or anchor (indicates a paragraph break) - :attr_line => /^\[(\w.*|\[[^\[\]]+\])\]$/, + :attr_line => /^\[([\w\{].*|\[[^\[\]]+\])\]$/, # attribute reference # {foo} @@ -115,10 +129,10 @@ module Asciidoctor # //// # comment block # //// - :comment_blk => /^\/{4,}\s*$/, + :comment_blk => %r{^/{4,}\s*$}, # // (and then whatever) - :comment => /^\/\/([^\/].*|)$/, + :comment => %r{^//([^/].*|)$}, # foo:: || foo;; # Should be followed by a definition line, e.g., @@ -157,7 +171,7 @@ module Asciidoctor # match[1] is the delimiter, whose length determines the level # match[2] is the title itself # match[3] is an optional repeat of the delimiter, which is dropped - :level_title => /^(={1,5})\s+(\S.*?)\s*(?:\[\[([^\[]+)\]\]\s*)?(\s\1)?$/, + :section_heading => /^(={1,5})\s+(\S.*?)\s*(?:\[\[([^\[]+)\]\]\s*)?(\s\1)?$/, # + From the Asciidoc User Guide: "A plus character preceded by at # least one space character at the end of a non-blank line forces @@ -170,7 +184,7 @@ module Asciidoctor # inline link and some inline link macro # FIXME revisit! - :link_inline => /(^|link:|\s|>)(\\?https?:\/\/[^\[ ]*[^\. \[])(?:\[((?:\\\]|[^\]])*?)\])?/, + :link_inline => %r{(^|link:|\s|>)(\\?https?://[^\[ ]*[^\. \[])(?:\[((?:\\\]|[^\]])*?)\])?}, # inline link macro # link:path[label] @@ -200,11 +214,15 @@ module Asciidoctor # +++text+++ # $$text$$ # pass:quotes[text] - :passthrough_macro => /\\?(?:(\+{3}|\${2})(.*?)\1|pass:([a-z,]*)\[((?:\\\]|[^\]])*?)\])/, + :pass_macro => /\\?(?:(\+{3}|\${2})(.*?)\1|pass:([a-z,]*)\[((?:\\\]|[^\]])*?)\])/, + + # passthrough macro allowed in value of attribute assignment + # pass:[text] + :pass_macro_basic => /^pass:([a-z,]*)\[(.*)\]$/, # inline literal passthrough macro # `text` - :passthrough_lit => /(^|[^`\w])(\\?`([^`\s]|[^`\s].*?\S)`)(?![`\w])/m, + :pass_lit => /(^|[^`\w])(\\?`([^`\s]|[^`\s].*?\S)`)(?![`\w])/m, # placeholder for extracted passthrough text :pass_placeholder => /\x0(\d+)\x0/, @@ -225,12 +243,9 @@ module Asciidoctor # \' within a word :single_quote_esc => /(\w)\\'(\w)/, + # an alternative if our backend generated single-quoted html/xml attributes #:single_quote_esc => /(\w|=)\\'(\w)/, - # and blah blah blah - # ^^^^ <--- whitespace - :starts_with_whitespace => /\s+(.+)\s+\+\s*$/, - # .Foo but not . Foo or ..Foo :title => /^\.([^\s\.].*)\s*$/, @@ -247,7 +262,18 @@ module Asciidoctor # inline xref macro # <<id,reftext>> (special characters have already been escaped, hence the entity references) # xref:id[reftext] - :xref_macro => /\\?(?:<<([\w":].*?)>>|xref:([\w":].*?)\[(.*?)\])/m + :xref_macro => /\\?(?:<<([\w":].*?)>>|xref:([\w":].*?)\[(.*?)\])/m, + + # ifdef::basebackend-html[] + # ifndef::theme[] + :ifdef_macro => /^(ifdef|ifndef)::([^\[]+)\[\]/, + + # endif::theme[] + # endif::basebackend-html[] + :endif_macro => /^endif::/, + + # include::chapter1.ad[] + :include_macro => /^include::([^\[]+)\[\]\s*\n?\z/ } ADMONITION_STYLES = ['NOTE', 'TIP', 'IMPORTANT', 'WARNING', 'CAUTION'] @@ -267,11 +293,14 @@ module Asciidoctor 'backtick' => '`', 'empty' => '', 'sp' => ' ', + 'space' => ' ', 'two-colons' => '::', 'two-semicolons' => ';;', 'nbsp' => ' ', 'deg' => '°', 'zwsp' => '​', + 'quot' => '"', + 'apos' => ''', 'lsquo' => '‘', 'rsquo' => '’', 'ldquo' => '“', @@ -279,7 +308,7 @@ module Asciidoctor 'wj' => '⁠', 'amp' => '&', 'lt' => '<', - 'gt' => '>', + 'gt' => '>' } ) @@ -339,20 +368,27 @@ module Asciidoctor # NOTE in Ruby 1.8.7, [^\\] does not match start of line, # so we need to match it explicitly - REPLACEMENTS = { + # order is significant + REPLACEMENTS = [ # (C) - /(^|[^\\])\(C\)/ => '\1©', + [/(^|[^\\])\(C\)/, '\1©'], # (R) - /(^|[^\\])\(R\)/ => '\1®', + [/(^|[^\\])\(R\)/, '\1®'], # (TM) - /(^|[^\\])\(TM\)/ => '\1™', + [/(^|[^\\])\(TM\)/, '\1™'], # foo--bar - /(\w)--(?=\w)/ => '\1—', + [/(\w)--(?=\w)/, '\1—'], + # ellipsis + [/(^|[^\\])\.\.\./, '\1…'], + # single quotes + [/(\w)'(\w)/, '\1’\2'], + # escaped single quotes + [/(\w)\\'(\w)/, '\1\'\2'], # and so on... # restore entities; TODO needs cleanup - /&(#[a-z0-9]+;)/i => '&\1' - } + [/&(#[a-z0-9]+;)/i, '&\1'] + ] # modules require 'asciidoctor/substituters' @@ -363,6 +399,7 @@ module Asciidoctor # concrete classes require 'asciidoctor/attribute_list' + require 'asciidoctor/backends/base_template' require 'asciidoctor/block' require 'asciidoctor/debug' require 'asciidoctor/document' @@ -371,7 +408,6 @@ module Asciidoctor require 'asciidoctor/lexer' require 'asciidoctor/list_item' require 'asciidoctor/reader' - require 'asciidoctor/render_templates' require 'asciidoctor/renderer' require 'asciidoctor/section' require 'asciidoctor/string' diff --git a/lib/asciidoctor/abstract_block.rb b/lib/asciidoctor/abstract_block.rb index 65fe760d..97390230 100644 --- a/lib/asciidoctor/abstract_block.rb +++ b/lib/asciidoctor/abstract_block.rb @@ -10,14 +10,23 @@ class Asciidoctor::AbstractBlock < Asciidoctor::AbstractNode # QUESTION should this be writable? and for Block, should it delegate to parent? attr_accessor :level - # Public: Get/Set the caption for this block - attr_accessor :caption - def initialize(parent, context) super(parent, context) @blocks = [] @id = nil - @level = nil + @level = (context == :document ? 0 : nil) + end + + # Public: Determine whether this Block contains block content + # + # returns Whether this Block has block content + # + #-- + # TODO we still need another method that answers + # whether this Block *can* have block content + # that should be the option 'sectionbody' + def has_section_body? + !blocks.empty? end # Public: Append a content block to this block's list of blocks. diff --git a/lib/asciidoctor/abstract_node.rb b/lib/asciidoctor/abstract_node.rb index 8475ef4a..01d60acc 100644 --- a/lib/asciidoctor/abstract_node.rb +++ b/lib/asciidoctor/abstract_node.rb @@ -1,4 +1,6 @@ class Asciidoctor::AbstractNode + include Asciidoctor::Substituters + # Public: Get the element which is the parent of this node attr_reader :parent @@ -15,7 +17,7 @@ class Asciidoctor::AbstractNode attr_reader :attributes def initialize(parent, context) - @parent = parent + @parent = (context != :document ? parent : nil) if !parent.nil? @document = parent.is_a?(Asciidoctor::Document) ? parent : parent.document else @@ -23,15 +25,24 @@ class Asciidoctor::AbstractNode end @context = context @attributes = {} + @passthroughs = [] end def attr(name, default = nil) - default.nil? ? @attributes.fetch(name.to_s, self.document.attr(name)) : - @attributes.fetch(name.to_s, self.document.attr(name, default)) + if self == @document + default.nil? ? @attributes[name.to_s] : @attributes.fetch(name.to_s, default) + else + default.nil? ? @attributes.fetch(name.to_s, @document.attr(name)) : + @attributes.fetch(name.to_s, @document.attr(name, default)) + end end def attr?(name) - @attributes.has_key?(name.to_s) || self.document.attr?(name) + if self == @document + @attributes.has_key? name.to_s + else + @attributes.has_key?(name.to_s) || @document.attr?(name) + end end def update_attributes(attributes) diff --git a/lib/asciidoctor/attribute_list.rb b/lib/asciidoctor/attribute_list.rb index d7de036d..440d8363 100644 --- a/lib/asciidoctor/attribute_list.rb +++ b/lib/asciidoctor/attribute_list.rb @@ -49,9 +49,9 @@ class Asciidoctor::AttributeList # Public: A regular expression for splitting a comma-separated string CSV_SPLIT_PATTERN = /[ \t]*,[ \t]*/ - def initialize(source, document = nil, quotes = ['\'', '"'], delimiter = ',', escape_char = '\\') + def initialize(source, block = nil, quotes = ['\'', '"'], delimiter = ',', escape_char = '\\') @scanner = ::StringScanner.new source - @document = document + @block = block @quotes = quotes @escape_char = escape_char @delimiter = delimiter @@ -97,11 +97,16 @@ class Asciidoctor::AttributeList end def parse_attribute(index = 0, posattrs = []) + single_quoted_value = false skip_blank + first = @scanner.peek(1) # example: "quote" || 'quote' - if @quotes.include? @scanner.peek(1) + if @quotes.include? first value = nil name = parse_attribute_value @scanner.get_byte + if first == '\'' + single_quoted_value = true + end else name = scan_name @@ -137,6 +142,9 @@ class Asciidoctor::AttributeList # example: foo="bar" || foo='bar' || foo="ba\"zaar" || foo='ba\'zaar' || foo='ba"zaar' (all spaces ignored) if @quotes.include? c value = parse_attribute_value c + if c == '\'' + single_quoted_value = true + end # example: foo=bar (all spaces ignored) elsif !c.nil? value = c + scan_to_delimiter @@ -146,7 +154,7 @@ class Asciidoctor::AttributeList end if value.nil? - resolved_name = @document ? Asciidoctor::Substituters.sub_attributes(name, @document) : name + resolved_name = single_quoted_value && !@block.nil? ? @block.apply_normal_subs(name) : name if !(posname = posattrs[index]).nil? @attributes[posname] = resolved_name else @@ -157,13 +165,14 @@ class Asciidoctor::AttributeList # not sure if I want this assignment or not #@attributes[resolved_name] = nil else - # TODO single-quoted attributes should get normal substitutions, not just attributes - resolved_value = @document ? Asciidoctor::Substituters.sub_attributes(value, @document) : value + resolved_value = value # example: options="opt1,opt2,opt3" if name == 'options' resolved_value.split(CSV_SPLIT_PATTERN).each do |o| @attributes['option-' + o] = nil end + elsif single_quoted_value && !@block.nil? + resolved_value = @block.apply_normal_subs(value) end @attributes[name] = resolved_value end diff --git a/lib/asciidoctor/backends/base_template.rb b/lib/asciidoctor/backends/base_template.rb new file mode 100644 index 00000000..02ae7095 --- /dev/null +++ b/lib/asciidoctor/backends/base_template.rb @@ -0,0 +1,54 @@ +class Asciidoctor::BaseTemplate + BLANK_LINES_PATTERN = /^\s*\n/ + LINE_FEED_ENTITY = ' ' # or 
 + + attr_reader :view + + def initialize(view) + @view = view + end + + def self.inherited(klass) + @template_classes ||= [] + @template_classes << klass + end + + def self.template_classes + @template_classes + end + + # We're ignoring locals for now. Shut up. + def render(obj = Object.new, locals = {}) + output = template.result(obj.instance_eval { binding }) + (view == 'document' || view == 'embedded') ? output.gnuke(BLANK_LINES_PATTERN).gsub(LINE_FEED_ENTITY, "\n") : output + end + + def template + raise "You chilluns need to make your own template" + end + + # create template matter to insert an attribute if the variable has a value + def attribute(name, key) + type = key.is_a?(Symbol) ? :attr : :var + key = key.to_s + if type == :attr + # example: <% if attr? 'foo' %> bar="<%= attr 'foo' %>"<% end %> + '<% if attr? \'' + key + '\' %> ' + name + '="<%= attr \'' + key.to_s + '\' %>"<% end %>' + else + # example: <% if foo %> bar="<%= foo %>"<% end %> + '<% if ' + key + ' %> ' + name + '="<%= ' + key + ' %>"<% end %>' + end + end + + # create template matter to insert a style class if the variable has a value + def attrvalue(key, sibling = true) + delimiter = sibling ? ' ' : '' + # example: <% if attr? 'foo' %><%= attr 'foo' %><% end %> + '<% if attr? \'' + key.to_s + '\' %>' + delimiter + '<%= attr \'' + key.to_s + '\' %><% end %>' + end + + # create template matter to insert an id if one is specified for the block + def id + attribute('id', 'id') + end +end diff --git a/lib/asciidoctor/backends/docbook45.rb b/lib/asciidoctor/backends/docbook45.rb new file mode 100644 index 00000000..32a42617 --- /dev/null +++ b/lib/asciidoctor/backends/docbook45.rb @@ -0,0 +1,420 @@ +class Asciidoctor::BaseTemplate + def role + attribute('role', :role) + end + + def xreflabel + attribute('xreflabel', :reftext) + end + + def title + tag('title', 'title') + end + + def tag(name, key) + type = key.is_a?(Symbol) ? :attr : :var + key = key.to_s + if type == :attr + # example: <% if attr? 'foo' %><bar><%= attr 'foo' %></bar><% end %> + '<% if attr? \'' + key + '\' %><' + name + '><%= attr \'' + key + '\' %></' + name + '><% end %>' + else + # example: <% unless foo.nil? %><bar><%= foo %></bar><% end %> + '<% unless ' + key + '.nil? %><' + name + '><%= ' + key + ' %></' + name + '><% end %>' + end + end +end + +module Asciidoctor::DocBook45 +class DocumentTemplate < ::Asciidoctor::BaseTemplate + def docinfo + <<-EOF + <% if has_header? && !notitle %> + #{tag 'title', 'header.name'} + <% end %> + <% if attr? :revdate %> + <date><%= attr :revdate %></date> + <% else %> + <date><%= attr :docdate %></date> + <% end %> + <% if has_header? %> + <% if attr? :author %> + <author> + #{tag 'firstname', :firstname} + #{tag 'othername', :middlename} + #{tag 'surname', :lastname} + #{tag 'email', :email} + </author> + #{tag 'authorinitials', :authorinitials} + <% end %> + <% if (attr? :revnumber) || (attr? :revremark) %> + <revhistory> + #{tag 'revision', :revnumber} + #{tag 'revdate', :revdate} + #{tag 'authorinitials', :authorinitials} + #{tag 'revremark', :revremark} + </revhistory> + <% end %> + <% end %> + EOF + end + + def template + @template ||= ::ERB.new <<-EOF +<%#encoding:UTF-8%> +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE <%= doctype %> PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd"> +<% if doctype == 'book' %> +<book lang="en"> + <bookinfo> +#{docinfo} + </bookinfo> +<%= content %> +</book> +<% else %> +<article lang="en"> + <articleinfo> +#{docinfo} + </articleinfo> +<%= content %> +</article> +<% end %> + EOF + end +end + +class EmbeddedTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ::ERB.new <<-EOS +<%#encoding:UTF-8%> +<%= content %> + EOS + end +end + +class BlockPreambleTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ::ERB.new <<-EOF +<%#encoding:UTF-8%> +<% if document.doctype == 'book' %> +<preface#{id}#{role}#{xreflabel}> + <title><%= title %></title> +<%= content %> +</preface> +<% else %> +<%= content %> +<% end %> + EOF + end +end + +class SectionTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%#encoding:UTF-8%> +<<%= document.doctype == 'book' && level <= 1 ? 'chapter' : 'section' %>#{id}#{role}#{xreflabel}> + #{title} +<%= content %> +</<%= document.doctype == 'book' && level <= 1 ? 'chapter' : 'section' %>> + EOF + end +end + +class BlockParagraphTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%#encoding:UTF-8%> +<% if title.nil? %> +<simpara#{id}#{role}#{xreflabel}><%= content %></simpara> +<% else %> +<formalpara#{id}#{role}#{xreflabel}> + <title><%= title %></title> + <para><%= content %></para> +</formalpara> +<% end %> + EOF + end +end + +class BlockAdmonitionTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%#encoding:UTF-8%> +<<%= attr :name %>#{id}#{role}#{xreflabel}> + #{title} + <% if has_section_body? %> +<%= content %> + <% else %> + <simpara><%= content.chomp %></simpara> + <% end %> +</<%= attr :name %>> + EOF + end +end + +class BlockUlistTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%#encoding:UTF-8%> +<itemizedlist#{id}#{role}#{xreflabel}> + #{title} + <% content.each do |li| %> + <listitem> + <simpara><%= li.text %></simpara> + <% if li.has_section_body? %> +<%= li.content %> + <% end %> + </listitem> + <% end %> +</itemizedlist> + EOF + end +end + +class BlockOlistTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%#encoding:UTF-8%> +<orderedlist#{id}#{role}#{xreflabel}#{attribute('numeration', :style)}> + #{title} + <% content.each do |li| %> + <listitem> + <simpara><%= li.text %></simpara> + <% if li.has_section_body? %> +<%= li.content %> + <% end %> + </listitem> + <% end %> +</orderedlist> + EOF + end +end + +class BlockDlistTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%#encoding:UTF-8%> +<variablelist#{id}#{role}#{xreflabel}> + #{title} + <% content.each do |dt, dd| %> + <varlistentry> + <term> + <% unless dt.id.to_s.empty? %> + <anchor id="<%= dt.id %>" xreflabel="<%= dt.attr(:reftext) %>"/> + <% end %> + <%= dt.text %> + </term> + <% unless dd.nil? %> + <listitem> + <simpara><%= dd.text %></simpara> + <% if dd.has_section_body? %> +<%= dd.content %> + <% end %> + </listitem> + <% end %> + </varlistentry> + <% end %> +</variablelist> + EOF + end +end + +class BlockOpenTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<%= content %> + EOS + end +end + +class BlockListingTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%#encoding:UTF-8%> +<% if title.nil? %> +<programlisting#{id}#{role}#{xreflabel} language="<%= attr :language %>" linenumbering="<%= (attr? :linenums) ? 'numbered' : 'unnumbered' %>"><%= content.gsub("\n", LINE_FEED_ENTITY) %></programlisting> +<% else %> +<formalpara#{id}#{role}#{xreflabel}> + <title><%= title %></title> + <para> + <programlisting language="<%= attr :language %>" linenumbering="<%= (attr? :linenums) ? 'numbered' : 'unnumbered' %>"><%= content.gsub("\n", LINE_FEED_ENTITY) %></programlisting> + </para> +</formalpara> +<% end %> + EOF + end +end + +class BlockLiteralTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%#encoding:UTF-8%> +<% if title.nil? %> +<literallayout#{id}#{role}#{xreflabel} class="monospaced"><%= content.gsub("\n", LINE_FEED_ENTITY) %></literallayout> +<% else %> +<formalpara#{id}#{role}#{xreflabel}> + <title><%= title %></title> + <literallayout class="monospaced"><%= content.gsub("\n", LINE_FEED_ENTITY) %></literallayout> +</formalpara> +<% end %> + EOF + end +end + +class BlockExampleTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%#encoding:UTF-8%> +<example#{id}#{role}#{xreflabel}> + #{title} +<%= content %> +</example> + EOF + end +end + +class BlockSidebarTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%#encoding:UTF-8%> +<sidebar#{id}#{role}#{xreflabel}> + #{title} +<%= content %> +</sidebar> + EOF + end +end + +class BlockQuoteTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%#encoding:UTF-8%> +<blockquote#{id}#{role}#{xreflabel}> + #{title} + <% if (attr? :attribution) || (attr? :citetitle) %> + <attribution> + <% if attr? :attribution %> + <%= attr(:attribution) %> + <% end %> + #{tag 'citetitle', :citetitle} + </attribution> + <% end %> +<%= content %> +</blockquote> + EOF + end +end + +class BlockVerseTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%#encoding:UTF-8%> +<blockquote#{id}#{role}#{xreflabel}> + #{title} + <% if (attr? :attribution) || (attr? :citetitle) %> + <attribution> + <% if attr? :attribution %> + <%= attr(:attribution) %> + <% end %> + #{tag 'citetitle', :citetitle} + </attribution> + <% end %> + <literallayout><%= content %></literallayout> +</blockquote> + EOF + end +end + +class BlockImageTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%#encoding:UTF-8%> +<figure#{id}#{role}#{xreflabel}> + #{title} + <mediaobject> + <imageobject> + <imagedata fileref="<%= attr :target %>"#{attribute('contentwidth', :width)}#{attribute('contentdepth', :height)}/> + </imageobject> + <textobject><phrase><%= attr :alt %></phrase></textobject> + </mediaobject> +</figure> + EOF + end +end + +class BlockRulerTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<simpara><?asciidoc-hr?></simpara> + EOF + end +end + +class InlineBreakTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<%= text %><?asciidoc-br?> + EOF + end +end + +class InlineQuotedTemplate < ::Asciidoctor::BaseTemplate + QUOTED_TAGS = { + :emphasis => ['<emphasis>', '</emphasis>'], + :strong => ['<emphasis role="strong">', '</emphasis>'], + :monospaced => ['<literal>', '</literal>'], + :superscript => ['<superscript>', '</superscript>'], + :subscript => ['<subscript>', '</subscript>'], + :double => [Asciidoctor::INTRINSICS['ldquo'], Asciidoctor::INTRINSICS['rdquo']], + :single => [Asciidoctor::INTRINSICS['lsquo'], Asciidoctor::INTRINSICS['rsquo']], + :none => ['', ''] + } + + def template + @template ||= ERB.new <<-EOF +<%= #{self.class}::QUOTED_TAGS[type].first %><% +if attr? :role %><phrase#{role}><% +end %><%= text %><% +if attr? :role %></phrase><% +end %><%= #{self.class}::QUOTED_TAGS[type].last %> + EOF + end +end + +class InlineAnchorTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<% if type == :xref +%><% if text.nil? +%><xref linkend="<%= target %>"/><% +else +%><link linkend="<%= target %>"><%= text %></link><% +end %><% +else +%><ulink url="<%= target %>"><%= text %></ulink><% +end %> + EOF + end +end + +class InlineImageTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<inlinemediaobject> + <imageobject> + <imagedata fileref="<%= target %>"#{attribute('width', :width)}#{attribute('depth', :height)}/> + </imageobject> + <textobject><phrase><%= attr :alt %></phrase></textobject> +</inlinemediaobject> + EOF + end +end + +class InlineCalloutTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOF +<co#{id}/> + EOF + end +end +end diff --git a/lib/asciidoctor/backends/html5.rb b/lib/asciidoctor/backends/html5.rb new file mode 100644 index 00000000..3e2d16fc --- /dev/null +++ b/lib/asciidoctor/backends/html5.rb @@ -0,0 +1,447 @@ +class Asciidoctor::BaseTemplate + + # create template matter to insert a style class from the role attribute if specified + def role + attrvalue(:role) + end +end + +module Asciidoctor::HTML5 +class DocumentTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ::ERB.new <<-EOS +<%#encoding:UTF-8%> +<!DOCTYPE html> +<html lang="en"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=<%= attr :encoding %>"> + <meta name="generator" content="Asciidoctor <%= attr 'asciidoctor-version' %>"> + <% if attr? :description %><meta name="description" content="<%= attr :description %>"><% end %> + <% if attr? :keywords %><meta name="keywords" content="<%= attr :keywords %>"><% end %> + <title><%= doctitle %></title> + <% unless attr(:stylesheet, '').empty? %> + <link rel="stylesheet" href="<%= attr(:stylesdir, '') + attr(:stylesheet) %>" type="text/css"> + <% end %> + </head> + <body class="<%= doctype %>"> + <% unless noheader %> + <div id="header"> + <% if has_header? %> + <% unless notitle %> + <h1><%= header.title %></h1> + <% end %> + <% if attr? :author %><span id="author"><%= attr :author %></span><br><% end %> + <% if attr? :email %><span id="email" class="monospaced"><<%= attr :email %>></span><br><% end %> + <% if attr? :revnumber %><span id="revnumber">version <%= attr :revnumber %><%= attr?(:revdate) ? ',' : '' %></span><% end %> + <% if attr? :revdate %><span id="revdate"><%= attr :revdate %></span><% end %> + <% if attr? :revremark %><br><span id="revremark"><%= attr :revremark %></span><% end %> + <% end %> + </div> + <% end %> + <div id="content"> +<%= content %> + </div> + <div id="footer"> + <div id="footer-text"> + Last updated <%= attr :localdatetime %> + </div> + </div> + </body> +</html> + EOS + end +end + +class EmbeddedTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ::ERB.new <<-EOS +<%#encoding:UTF-8%> +<%= content %> + EOS + end +end + +class BlockPreambleTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ::ERB.new <<-EOS +<%#encoding:UTF-8%> +<div id="preamble"> + <div class="sectionbody"> +<%= content %> + </div> +</div> + EOS + end +end + +class SectionTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<% if level == 0 %> +<h1#{id}><%= title %></h1> +<%= content %> +<% else %> +<div class="sect<%= level %>#{role}"> + <h<%= level + 1 %>#{id}><%= title %></h<%= level + 1 %>> + <% if level == 1 %> + <div class="sectionbody"> +<%= content %> + </div> + <% else %> +<%= content %> + <% end %> +</div> +<% end %> + EOS + end +end + +class BlockDlistTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<div#{id} class="dlist#{role}"> + <% if title %> + <div class="title"><%= title %></div> + <% end %> + <dl> + <% content.each do |dt, dd| %> + <dt class="hdlist1"> + <% unless dt.id.to_s.empty? %> + <a id="<%= dt.id %>"></a> + <% end %> + <%= dt.text %> + </dt> + <% unless dd.nil? %> + <dd> + <% unless dd.text.to_s.empty? %> + <p><%= dd.text %></p> + <% end %> + <% if dd.has_section_body? %> +<%= dd.content %> + <% end %> + </dd> + <% end %> + <% end %> + </dl> +</div> + EOS + end +end + +class BlockListingTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<div#{id} class="listingblock#{role}"> + <% if title %> + <div class="title"><%= title %></div> + <% end %> + <div class="content monospaced"> + <pre class="highlight#{attrvalue(:language)}"><code><%= content.gsub("\n", LINE_FEED_ENTITY) %></code></pre> + </div> +</div> + EOS + end +end + +class BlockLiteralTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<div#{id} class="literalblock#{role}"> + <% if title %> + <div class="title"><%= title %></div> + <% end %> + <div class="content monospaced"> + <pre><%= content.gsub("\n", LINE_FEED_ENTITY) %></pre> + </div> +</div> + EOS + end +end + +class BlockAdmonitionTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<div#{id} class="admonitionblock#{role}"> + <table> + <tr> + <td class="icon"> + <% if attr? :caption %> + <div class="title"><%= attr :caption %></div> + <% end %> + </td> + <td class="content"> + <% unless title.nil? %> + <div class="title"><%= title %></div> + <% end %> + <%= content %> + </td> + </tr> + </table> +</div> + EOS + end +end + +class BlockParagraphTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<div#{id} class="paragraph#{role}"> + <% unless title.nil? %> + <div class="title"><%= title %></div> + <% end %> + <p><%= content %></p> +</div> + EOS + end +end + +class BlockSidebarTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<div#{id} class="sidebarblock#{role}"> + <div class="content"> + <% unless title.nil? %> + <div class="title"><%= title %></div> + <% end %> +<%= content %> + </div> +</div> + EOS + end +end + +class BlockExampleTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<div#{id} class="exampleblock#{role}"> + <div class="content"> + <% unless title.nil? %> + <div class="title"><%= title %></div> + <% end %> +<%= content %> + </div> +</div> + EOS + end +end + +class BlockOpenTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<div#{id} class="openblock#{role}"> + <% unless title.nil? %> + <div class="title"><%= title %></div> + <% end %> + <div class="content"> +<%= content %> + </div> +</div> + EOS + end +end + +class BlockQuoteTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<div#{id} class="quoteblock#{role}"> + <% unless title.nil? %> + <div class="title"><%= title %></div> + <% end %> + <div class="content"> +<%= content %> + </div> + <div class="attribution"> + <% if attr? :citetitle %> + <em><%= attr :citetitle %></em> + <% end %> + <% if attr? :attribution %> + <% if attr? :citetitle %> + <br/> + <% end %> + <%= '— ' + attr(:attribution) %> + <% end %> + </div> +</div> + EOS + end +end + +class BlockVerseTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<div#{id} class="verseblock#{role}"> + <% unless title.nil? %> + <div class="title"><%= title %></div> + <% end %> + <pre class="content"><%= content.gsub("\n", LINE_FEED_ENTITY) %></pre> + <div class="attribution"> + <% if attr? :citetitle %> + <em><%= attr :citetitle %></em> + <% end %> + <% if attr? :attribution %> + <% if attr? :citetitle %> + <br/> + <% end %> + <%= '— ' + attr(:attribution) %> + <% end %> + </div> +</div> + EOS + end +end + +class BlockUlistTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<div#{id} class="ulist#{attrvalue(:style)}#{role}"> + <% unless title.nil? %> + <div class="title"><%= title %></div> + <% end %> + <ul> + <% content.each do |li| %> + <li> + <p><%= li.text %></p> + <% if li.has_section_body? %> +<%= li.content %> + <% end %> + </li> + <% end %> + </ul> +</div> + EOS + end +end + +class BlockOlistTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<div#{id} class="olist <%= attr :style %>#{role}"> + <% unless title.nil? %> + <div class="title"><%= title %></div> + <% end %> + <ol class="<%= attr :style %>"#{attribute('start', :start)}> + <% content.each do |li| %> + <li> + <p><%= li.text %></p> + <% if li.has_section_body? %> +<%= li.content %> + <% end %> + </li> + <% end %> + </ol> +</div> + EOS + end +end + +class BlockImageTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%#encoding:UTF-8%> +<div#{id} class="imageblock#{role}"> + <div class="content"> + <% if attr :link %> + <a class="image" href="<%= attr :link %>"><img src="<%= attr :target %>" alt="<%= attr :alt %>"#{attribute('width', :width)}#{attribute('height', :height)}></a> + <% else %> + <img src="<%= attr :target %>" alt="<%= attr :alt %>"#{attribute('width', :width)}#{attribute('height', :height)}> + <% end %> + </div> + <% if title %> + <div class="title"><%= title %></div> + <% end %> +</div> + EOS + end +end + +class BlockRulerTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<hr> + EOS + end +end + +class InlineBreakTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<%= text %><br> + EOS + end +end + +class InlineCalloutTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<b><%= text %></b> + EOS + end +end + +class InlineQuotedTemplate < ::Asciidoctor::BaseTemplate + QUOTED_TAGS = { + :emphasis => ['<em>', '</em>'], + :strong => ['<strong>', '</strong>'], + :monospaced => ['<tt>', '</tt>'], + :superscript => ['<sup>', '</sup>'], + :subscript => ['<sub>', '</sub>'], + :double => [Asciidoctor::INTRINSICS['ldquo'], Asciidoctor::INTRINSICS['rdquo']], + :single => [Asciidoctor::INTRINSICS['lsquo'], Asciidoctor::INTRINSICS['rsquo']], + :none => ['', ''] + } + + # we use double quotes for the class attribute to prevent quote processing + # seems hackish, though AsciiDoc has this same issue + def template + @template ||= ERB.new <<-EOS +<%= #{self.class}::QUOTED_TAGS[type].first %><% +if attr? :role %><span#{attribute('class', :role)}><% +end %><%= text %><% +if attr? :role %></span><% +end %><%= #{self.class}::QUOTED_TAGS[type].last %> + EOS + end +end + +class InlineAnchorTemplate < ::Asciidoctor::BaseTemplate + def template + @template ||= ERB.new <<-EOS +<% +if type == :xref +%><a href="#<%= target %>"><%= text || document.references.fetch(target, '[' + target + ']') %></a><% +else +%><a href="<%= target %>"><%= text %></a><% +end +%> + EOS + end +end + +class InlineImageTemplate < ::Asciidoctor::BaseTemplate + def template + # care is taken here to avoid a space inside the optional <a> tag + @template ||= ERB.new <<-EOS +<span class="image#{role}"> + <% + if attr :link %><a class="image" href="<%= attr :link %>"><% + end %><img src="<%= target %>" alt="<%= attr :alt %>"#{attribute('width', :width)}#{attribute('height', :height)}#{attribute('title', :title)}><% + if attr :link%></a><% end + %> +</span> + EOS + end +end +end diff --git a/lib/asciidoctor/block.rb b/lib/asciidoctor/block.rb index f020b362..c9b52315 100644 --- a/lib/asciidoctor/block.rb +++ b/lib/asciidoctor/block.rb @@ -7,8 +7,6 @@ # => ["<em>This</em> is a <test>"] class Asciidoctor::Block < Asciidoctor::AbstractBlock - include Asciidoctor::Substituters - # Public: Create alias for context to be consistent w/ AsciiDoc alias :blockname :context @@ -18,6 +16,9 @@ class Asciidoctor::Block < Asciidoctor::AbstractBlock # Public: Get/Set the String block title. attr_accessor :title + # Public: Get/Set the caption for this block + attr_accessor :caption + # Public: Initialize an Asciidoctor::Block object. # # parent - The parent Asciidoc Object. @@ -28,7 +29,6 @@ class Asciidoctor::Block < Asciidoctor::AbstractBlock super(parent, context) @buffer = buffer @title = nil - @passthroughs = [] end # Public: Get the rendered String content for this Block. If the block @@ -89,7 +89,7 @@ class Asciidoctor::Block < Asciidoctor::AbstractBlock # # Examples # - # doc = Asciidoctor::Document.new([]) + # doc = Asciidoctor::Document.new # block = Asciidoctor::Block.new(doc, :paragraph, # ['`This` is what happens when you <meet> a stranger in the <alps>!']) # block.content diff --git a/lib/asciidoctor/document.rb b/lib/asciidoctor/document.rb index f7115430..5bbc6161 100644 --- a/lib/asciidoctor/document.rb +++ b/lib/asciidoctor/document.rb @@ -13,54 +13,56 @@ # # Keep in mind that you'll want to honor these document settings: # -# notitle - The h1 heading should not be shown +# notitle - The h1 heading should not be shown # noheader - The header block (h1 heading, author, revision info) should not be shown -class Asciidoctor::Document +class Asciidoctor::Document < Asciidoctor::AbstractBlock include Asciidoctor - # Public: The context of this node. Always :document. - attr_reader :context - - # Public: Get the Hash of attributes - attr_reader :attributes - # Public: Get the Hash of document references attr_reader :references - # The section level 0 element + # The section level 0 block attr_reader :header - # Public: Get the Array of elements (really Blocks or Sections) for the document - attr_reader :elements - # Public: Initialize an Asciidoc object. # - # data - The Array of Strings holding the Asciidoc source document. + # data - The Array of Strings holding the Asciidoc source document. (default: []) # options - A Hash of options to control processing, such as disabling # the header/footer (:header_footer) or attribute overrides (:attributes) - # block - A block that can be used to retrieve external Asciidoc - # data to include in this document. + # (default: {}) + # block - A block that can be used to retrieve external Asciidoc + # data to include in this document. # # Examples # # data = File.readlines(filename) # doc = Asciidoctor::Document.new(data) - def initialize(data, options = {}, &block) - @context = :document - @elements = [] + def initialize(data = [], options = {}, &block) + super(self, :document) + @references = {} @renderer = nil @options = options @options[:header_footer] = @options.fetch(:header_footer, true) - @attributes = {} - @attributes['sectids'] = nil + @attributes['sectids'] = true @attributes['encoding'] = 'UTF-8' - @reader = Reader.new(data, @attributes, &block) + attribute_overrides = options[:attributes] || {} + attribute_overrides.each {|key, val| + # a nil or negative key undefines the attribute + if (val.nil? || key[-1..-1] == '!') + @attributes.delete(key.chomp '!') + # otherwise it's an attribute assignment + else + @attributes[key] = val + end + } - # pseudo-delegation :) - @references = @reader.references + @attributes['backend'] ||= DEFAULT_BACKEND + update_backend_attributes() + + @reader = Reader.new(data, self, attribute_overrides, &block) # dynamic intrinstic attribute values @attributes['doctype'] ||= DEFAULT_DOCTYPE @@ -68,76 +70,52 @@ class Asciidoctor::Document @attributes['localdate'] ||= now.strftime('%Y-%m-%d') @attributes['localtime'] ||= now.strftime('%H:%m:%S %Z') @attributes['localdatetime'] ||= [@attributes['localdate'], @attributes['localtime']].join(' ') + # docdate and doctime should default to localdate and localtime if not otherwise set + @attributes['docdate'] ||= @attributes['localdate'] + @attributes['doctime'] ||= @attributes['localtime'] @attributes['asciidoctor-version'] = VERSION - if options.has_key? :attributes - options[:attributes].delete_if {|k, v| - negative_key = (v.nil? || k[-1] == '!') - @attributes.delete(k.chomp '!') if negative_key - negative_key - } - - @attributes.update(options[:attributes]) unless options[:attributes].empty? - end - # Now parse @lines into elements + # Now parse @lines into blocks while @reader.has_lines? @reader.skip_blank if @reader.has_lines? block = Lexer.next_block(@reader, self) - @elements << block unless block.nil? + self << block unless block.nil? end end - Asciidoctor.debug "Found #{@elements.size} elements in this document:" - @elements.each do |el| + Asciidoctor.debug "Found #{@blocks.size} blocks in this document:" + @blocks.each do |el| Asciidoctor.debug el end # split off the level 0 section, if present - root = @elements.first + root = @blocks.first @header = nil if root.is_a?(Section) && root.level == 0 - @header = @elements.shift + @header = @blocks.shift # a book has multiple level 0 sections if doctype == 'book' - @elements = @header.blocks + @elements + @blocks = @header.blocks + @blocks # an article only has one level 0 section else - @elements = @header.blocks + @blocks = @header.blocks end @header.clear_blocks end end - def document - self - end - # Make the raw source for the Document available. def source @reader.source if @reader end - def attr(name, default = nil) - default.nil? ? @attributes[name.to_s] : @attributes.fetch(name.to_s, default) - #default.nil? ? @attributes[name.to_s.tr('_', '-')] : @attributes.fetch(name.to_s.tr('_', '-'), default) - end - - def attr?(name) - @attributes.has_key? name.to_s - #@attributes.has_key? name.to_s.tr('_', '-') - end - def doctype @attributes['doctype'] end - def level - 0 - end - # The title explicitly defined in the document attributes def title @attributes['title'] @@ -164,13 +142,22 @@ class Asciidoctor::Document end def first_section - has_header ? @header : @elements.detect{|e| e.is_a? Section} + has_header? ? @header : @blocks.detect{|e| e.is_a? Section} end - def has_header + def has_header? !@header.nil? end + # Public: Update the backend attributes to reflect a change in the selected backend + def update_backend_attributes() + backend = @attributes['backend'] + basebackend = backend.nuke(/[[:digit:]]+$/) + @attributes['backend-' + backend] = 1 + @attributes['basebackend'] = basebackend + @attributes['basebackend-' + basebackend] = 1 + end + def splain if @header Asciidoctor.debug "Header is #{@header}" @@ -178,8 +165,8 @@ class Asciidoctor::Document Asciidoctor.debug "No header" end - Asciidoctor.debug "I have #{@elements.count} elements" - @elements.each_with_index do |block, i| + Asciidoctor.debug "I have #{@blocks.count} blocks" + @blocks.each_with_index do |block, i| Asciidoctor.debug "v" * 60 Asciidoctor.debug "Block ##{i} is a #{block.class}" Asciidoctor.debug "Name is #{block.title rescue 'n/a'}" @@ -196,6 +183,7 @@ class Asciidoctor::Document if @options[:template_dir] render_options[:template_dir] = @options[:template_dir] end + render_options[:backend] = @attributes.fetch('backend', 'html5') # Override Document @option settings with options passed in render_options.merge! options @@ -215,12 +203,12 @@ class Asciidoctor::Document # per AsciiDoc-spec, remove the title after rendering the header @attributes.delete('title') - html_pieces = [] - @elements.each do |element| - Asciidoctor::debug "Rendering element: #{element}" - html_pieces << element.render + buffer = [] + @blocks.each do |block| + Asciidoctor::debug "Rendering block: #{block}" + buffer << block.render end - html_pieces.join + buffer.join end end diff --git a/lib/asciidoctor/lexer.rb b/lib/asciidoctor/lexer.rb index f5f46e60..1ac33d71 100644 --- a/lib/asciidoctor/lexer.rb +++ b/lib/asciidoctor/lexer.rb @@ -15,8 +15,8 @@ # # Create a Reader for the AsciiDoc lines and retrieve the next block from it. # # Lexer::next_block requires a parent, so we begin by instantiating an empty Document. # -# doc = Document.new [] -# reader = Reader.new(lines) +# doc = Document.new +# reader = Reader.new lines # block = Lexer.next_block(reader, doc) # block.class # # => Asciidoctor::Block @@ -74,10 +74,14 @@ class Asciidoctor::Lexer if match = this_line.match(REGEXP[:anchor]) Asciidoctor.debug "Found an anchor in line:\n\t#{this_line}" id, reftext = match[1].split(',') - reftext ||= '[' + id + ']' attributes['id'] = id - # AsciiDoc always use [id] as the reftext, but I'd like to do better in Asciidoctor - parent.document.references[id] = reftext + # AsciiDoc always use [id] as the reftext in HTML output, + # but I'd like to do better in Asciidoctor + #parent.document.references[id] = '[' + id + ']' + if reftext + attributes['reftext'] = reftext + parent.document.references[id] = reftext + end reader.skip_blank elsif this_line.match(REGEXP[:comment_blk]) @@ -88,7 +92,7 @@ class Asciidoctor::Lexer reader.skip_blank elsif match = this_line.match(REGEXP[:attr_list_blk]) - AttributeList.new(match[1], parent.document).parse_into(attributes) + AttributeList.new(parent.document.sub_attributes(match[1]), parent).parse_into(attributes) reader.skip_blank # we're letting ruler have attributes @@ -117,21 +121,27 @@ class Asciidoctor::Lexer elsif match = this_line.match(REGEXP[:image_blk]) block = Block.new(parent, :image) - AttributeList.new(match[2], parent.document).parse_into(attributes, ['alt', 'width', 'height']) - attributes['target'] = target = Asciidoctor::Substituters.sub_attributes(match[1], parent.document) - attributes['alt'] ||= File.basename(target, File.extname(target)) + AttributeList.new(parent.document.sub_attributes(match[2])).parse_into(attributes, ['alt', 'width', 'height']) + target = block.sub_attributes(match[1]) + if !target.to_s.empty? + attributes['target'] = target + attributes['alt'] ||= File.basename(target, File.extname(target)) + else + # drop the line if target resolves to nothing + block = nil + end reader.skip_blank elsif this_line.match(REGEXP[:open_blk]) # an open block is surrounded by '--' lines and has zero or more blocks inside - buffer = Reader.new(reader.grab_lines_until { |line| line.match(REGEXP[:open_blk]) }) + buffer = Reader.new reader.grab_lines_until { |line| line.match(REGEXP[:open_blk]) } # Strip lines off end of block - not implemented yet # while buffer.has_lines? && buffer.last.strip.empty? # buffer.pop # end - block = Block.new(parent, :open, []) + block = Block.new(parent, :open) while buffer.has_lines? new_block = next_block(buffer, block) block.blocks << new_block unless new_block.nil? @@ -142,7 +152,7 @@ class Asciidoctor::Lexer # sidebar is surrounded by '****' (4 or more '*' chars) lines # FIXME violates DRY because it's a duplication of quote parsing block = Block.new(parent, :sidebar) - buffer = Reader.new(reader.grab_lines_until {|line| line.match( REGEXP[:sidebar_blk] ) }) + buffer = Reader.new reader.grab_lines_until {|line| line.match( REGEXP[:sidebar_blk] ) } while buffer.has_lines? new_block = next_block(buffer, block) @@ -211,7 +221,7 @@ class Asciidoctor::Lexer else block = Block.new(parent, :example) end - buffer = Reader.new(reader.grab_lines_until {|line| line.match( REGEXP[:example] ) }) + buffer = Reader.new reader.grab_lines_until {|line| line.match( REGEXP[:example] ) } while buffer.has_lines? new_block = next_block(buffer, block) @@ -229,20 +239,20 @@ class Asciidoctor::Lexer # multi-line verse or quote is surrounded by a block delimiter AttributeList.rekey(attributes, ['style', 'attribution', 'citetitle']) quote_context = (attributes['style'] == 'verse' ? :verse : :quote) - buffer = Reader.new(reader.grab_lines_until {|line| line.match( REGEXP[:quote] ) }) + block_reader = Reader.new reader.grab_lines_until {|line| line.match( REGEXP[:quote] ) } # only quote can have other section elements (as as section block) section_body = (quote_context == :quote) if section_body block = Block.new(parent, quote_context) - while buffer.has_lines? - new_block = next_block(buffer, block) + while block_reader.has_lines? + new_block = next_block(block_reader, block) block.blocks << new_block unless new_block.nil? end else - buffer.lines.last.chomp! unless buffer.lines.empty? - block = Block.new(parent, quote_context, buffer.lines) + block_reader.chomp_last! + block = Block.new(parent, quote_context, block_reader.lines) end elsif this_line.match(REGEXP[:lit_blk]) @@ -265,7 +275,7 @@ class Asciidoctor::Lexer if !buffer.empty? offset = buffer.map {|line| line.match(REGEXP[:leading_blanks])[1].length }.min if offset > 0 - buffer = buffer.map {|l| l.sub(/^\s{1,#{offset}}/, '') } + buffer = buffer.map {|l| l.nuke(/^\s{1,#{offset}}/) } end buffer.last.chomp! end @@ -288,7 +298,7 @@ class Asciidoctor::Lexer block = Block.new(parent, :listing, buffer) elsif admonition_style = ADMONITION_STYLES.detect{|s| attributes[1] == s} - # an admonition preceded by [*TYPE*] and lasts until a blank line + # an admonition preceded by [<TYPE>] and lasts until a blank line reader.unshift(this_line) buffer = reader.grab_lines_until(:break_on_blank_lines => true) buffer.last.chomp! unless buffer.empty? @@ -337,7 +347,12 @@ class Asciidoctor::Lexer if !block.nil? block.id = attributes['id'] if attributes.has_key?('id') block.title ||= title - block.caption ||= caption + block.caption ||= caption unless block.is_a?(Section) + # AsciiDoc always use [id] as the reftext in HTML output, + # but I'd like to do better in Asciidoctor + if block.id && block.title && !attributes.has_key?('reftext') + block.document.references[block.id] = block.title + end block.update_attributes(attributes) # if the block ended with unrooted attributes, then give them # to the next block; this seems like a hack, but it really @@ -429,16 +444,23 @@ class Asciidoctor::Lexer begin dt = ListItem.new(block, match[2]) - dt.id = match[1] unless match[1].nil? + unless match[1].nil? + dt.id = match[1] + dt.attributes['reftext'] = '[' + match[1] + ']' + end dd = ListItem.new(block, match[5]) dd_reader = Reader.new grab_lines_for_list_item(reader, :dlist, sibling_pattern) + continuation_connects_first_block = (dd_reader.has_lines? && dd_reader.peek_line.chomp == LIST_CONTINUATION) + if continuation_connects_first_block + dd_reader.get_line + end while dd_reader.has_lines? new_block = next_block(dd_reader, block) dd.blocks << new_block unless new_block.nil? end - dd.fold_first + dd.fold_first(continuation_connects_first_block) pairs << [dt, dd] @@ -478,6 +500,7 @@ class Asciidoctor::Lexer # first skip the line with the marker reader.get_line list_item_reader = Reader.new grab_lines_for_list_item(reader, list_block.context) + continuation_connects_first_block = list_item_reader.peek_line == "\n" while list_item_reader.has_lines? new_block = next_block(list_item_reader, list_block) list_item.blocks << new_block unless new_block.nil? @@ -485,6 +508,7 @@ class Asciidoctor::Lexer Asciidoctor.debug "\n\nlist_item has #{list_item.blocks.count} blocks, and first is a #{list_item.blocks.first.class} with context #{list_item.blocks.first.context rescue 'n/a'}\n\n" + list_item.fold_first(continuation_connects_first_block) list_item end @@ -523,7 +547,7 @@ class Asciidoctor::Lexer if prev_line == LIST_CONTINUATION if continuation == :inactive continuation = :active - buffer.pop if phase == :process + buffer.pop if phase == :process && list_type != :dlist end if this_line.chomp == LIST_CONTINUATION @@ -601,11 +625,27 @@ class Asciidoctor::Lexer reader.unshift this_line if !this_line.nil? if phase == :process + + # NOTE this is hackish, but since we process differently than ulist & olist + # we need to do the line continuation substitution post-scan + # we also need to hold on to the first line continuation because an endline + # alone doesn't tell us that the first paragraph was attached via a line continuation + if list_type == :dlist && buffer.size > 0 && buffer.first == "+\n" + first = buffer.shift + buffer = buffer.map {|l| l == "+\n" ? "\n" : l} + buffer.unshift(first) + end + # QUESTION should we strip these trailing endlines? - # I think we do have to strip the line continuation - buffer.pop while buffer.last == "\n" - buffer.pop if buffer.last == "+\n" buffer.pop while buffer.last == "\n" + + # We do need to replace the trailing continuation + if list_type != :dlist && buffer.last == "+\n" + buffer.pop + # QUESTION do we strip the endlines exposed by popping the list continuation? + #buffer.pop while buffer.last == "\n" + #buffer.push "\n" + end end buffer @@ -633,7 +673,7 @@ class Asciidoctor::Lexer end def self.is_single_line_section_heading?(line) - !line.nil? && line.match(REGEXP[:level_title]) + !line.nil? && line.match(REGEXP[:section_heading]) end def self.is_two_line_section_heading?(line1, line2) @@ -649,7 +689,7 @@ class Asciidoctor::Lexer end def self.is_title_section?(section, parent) - section.level == 0 && parent.is_a?(Document) && parent.elements.empty? + section.level == 0 && parent.is_a?(Document) && parent.blocks.empty? end # Private: Extracts the title, level and (optional) embedded id from a @@ -689,29 +729,29 @@ class Asciidoctor::Lexer # def self.extract_section_heading(line1, line2 = nil) Asciidoctor.debug "#{__method__} -> line1: #{line1.chomp rescue 'nil'}, line2: #{line2.chomp rescue 'nil'}" - sect_title = sect_anchor = nil + sect_title = sect_id = nil sect_level = 0 single_line = false if is_single_line_section_heading?(line1) - header_match = line1.match(REGEXP[:level_title]) + header_match = line1.match(REGEXP[:section_heading]) sect_title = header_match[2] - sect_anchor = header_match[3] + sect_id = header_match[3] sect_level = single_line_section_level(header_match[1]) single_line = true elsif is_two_line_section_heading?(line1, line2) # TODO could be optimized into a single regexp header_match = line1.match(REGEXP[:heading_name]) if anchor_match = header_match[1].match(REGEXP[:anchor_embedded]) - sect_title = anchor_match[1] - sect_anchor = anchor_match[2] + sect_title = anchor_match[1] + sect_id = anchor_match[2] else sect_title = header_match[1] end sect_level = section_level(line2) end - Asciidoctor.debug "#{__method__} -> Returning #{sect_title}, #{sect_level} (anchor: '#{sect_anchor || '<none>'}')" - return [sect_title, sect_level, sect_anchor, single_line] + Asciidoctor.debug "#{__method__} -> Returning #{sect_title}, #{sect_level} (id: '#{sect_id || '<none>'}')" + return [sect_title, sect_level, sect_id, single_line] end # Public: Consume and parse the two header lines (line 1 = author info, line 2 = revision info). @@ -792,10 +832,10 @@ class Asciidoctor::Lexer # source # # => "GREETINGS\n---------\nThis is my doc.\n\nSALUTATIONS\n-----------\nIt is awesome." # - # reader = Reader.new(source.lines.entries) + # reader = Reader.new source.lines.entries # # create empty document to parent the section # # and hold attributes extracted from header - # doc = Document.new [] + # doc = Document.new # # Lexer.next_section(reader, doc).title # # => "GREETINGS" @@ -874,7 +914,7 @@ class Asciidoctor::Lexer end end - section_reader = Reader.new(section_lines) + section_reader = Reader.new section_lines # Now parse section_lines into Blocks belonging to the current Section while section_reader.has_lines? new_block = next_block(section_reader, section) diff --git a/lib/asciidoctor/list_item.rb b/lib/asciidoctor/list_item.rb index c9e3e0ae..a2822dc0 100644 --- a/lib/asciidoctor/list_item.rb +++ b/lib/asciidoctor/list_item.rb @@ -25,19 +25,21 @@ class Asciidoctor::ListItem < Asciidoctor::AbstractBlock end def content - # create method for !blocks.empty? - if !blocks.empty? - blocks.map{|block| block.render}.join - else - nil - end + has_section_body? ? blocks.map {|b| b.render }.join : nil end # Public: Fold the first paragraph block into the text - def fold_first - if parent.context == :dlist && !blocks.empty? && blocks.first.is_a?(Asciidoctor::Block) && - ((blocks.first.context == :paragraph && blocks.first.buffer != Asciidoctor::LIST_CONTINUATION) || - (blocks.first.context == :literal && blocks.first.attr(:options, []).include?('listparagraph'))) + # + # Here are the rules for when a folding occurs: + # + # Given: this list item has at least one block + # When: the first block is not connected by a list continuation + # And: the first block is a paragraph or additionally, for labeled lists, a literal paragraph (indented line), + # Then: then join the list text and the first block with an endline + def fold_first(continuation_connects_first_block = false) + if !blocks.empty? && blocks.first.is_a?(Asciidoctor::Block) && + ((blocks.first.context == :paragraph && !continuation_connects_first_block) || + (parent.context == :dlist && blocks.first.context == :literal && blocks.first.attr(:options, []).include?('listparagraph'))) block = blocks.shift if !@text.nil? && !@text.empty? block.buffer.unshift(@text) diff --git a/lib/asciidoctor/reader.rb b/lib/asciidoctor/reader.rb index b01d560e..668dd9a1 100644 --- a/lib/asciidoctor/reader.rb +++ b/lib/asciidoctor/reader.rb @@ -9,58 +9,37 @@ class Asciidoctor::Reader # Public: Get the String Array of lines parsed from the source attr_reader :lines - # Public: Get the Hash of attributes - attr_reader :attributes - - attr_reader :references - - # Public: Convert a string to a legal attribute name. - # - # name - The String holding the Asciidoc attribute name. - # - # Returns a String with the legal name. - # - # Examples - # - # sanitize_attribute_name('Foo Bar') - # => 'foobar' - # - # sanitize_attribute_name('foo') - # => 'foo' - # - # sanitize_attribute_name('Foo 3 #-Billy') - # => 'foo3-billy' - def sanitize_attribute_name(name) - name.gsub(/[^\w\-]/, '').downcase - end - # Public: Initialize the Reader object. # - # data - The Array of Strings holding the Asciidoc source document. - # block - A block that can be used to retrieve external Asciidoc - # data to include in this document. + # data - The Array of Strings holding the Asciidoc source document. The + # original instance of this Array is not modified + # document - The document with which this reader is associated. Used to access + # document attributes + # overrides - A Hash of attributes that were passed to the Document and should + # prevent attribute assignments or removals of matching keys found in + # the document + # block - A block that can be used to retrieve external Asciidoc + # data to include in this document. # # Examples # # data = File.readlines(filename) - # reader = Asciidoctor::Reader.new(data) - def initialize(data = [], attributes = nil, &block) - @references = {} - - data = data.lines.entries if data.is_a? String - - # if attributes are nil, we assume this is a preprocessed string - if attributes.nil? - @lines = data + # reader = Asciidoctor::Reader.new data + def initialize(data = [], document = nil, overrides = nil, &block) + # if document is nil, we assume this is a preprocessed string + if document.nil? + @lines = data.is_a?(String) ? data.lines.entries : data.dup + elsif !data.empty? + @overrides = overrides || {} + @document = document + process(data.is_a?(String) ? data.lines.entries : data, &block) else - @attributes = attributes - process(data, &block) + @lines = [] end # just in case we got some nils floating at the end of our lines after reading a funky document - @lines.pop while !@lines.empty? && @lines.last.nil? + @lines.pop until @lines.empty? || !@lines.last.nil? - #Asciidoctor.debug "About to leave Reader#init, and references is #{@references.inspect}" @source = @lines.join Asciidoctor.debug "Leaving Reader#init, and I have #{@lines.count} lines" Asciidoctor.debug "Also, has_lines? is #{self.has_lines?}" @@ -73,6 +52,13 @@ class Asciidoctor::Reader !@lines.empty? end + # Public: Check whether this reader is empty (contains no lines) + # + # Returns true if @lines.empty? is true, otherwise false. + def empty? + @lines.empty? + end + # Private: Strip off leading blank lines in the Array of lines. # # Returns nil. @@ -156,8 +142,18 @@ class Asciidoctor::Reader # Public: Push Array of string `lines` onto queue of source data lines, unless `lines` has no non-nil values. # # Returns nil - def unshift(*lines) - @lines.unshift(*lines) if lines.any? + def unshift(*new_lines) + @lines.unshift(*new_lines) if !new_lines.empty? + nil + end + + # Public: Chomp the String on the last line if this reader contains at least one line + # + # Delegates to chomp! + # + # Returns nil + def chomp_last! + @lines.last.chomp! unless @lines.empty? nil end @@ -204,14 +200,33 @@ class Asciidoctor::Reader buffer end + # Public: Convert a string to a legal attribute name. + # + # name - The String holding the Asciidoc attribute name. + # + # Returns a String with the legal name. + # + # Examples + # + # sanitize_attribute_name('Foo Bar') + # => 'foobar' + # + # sanitize_attribute_name('foo') + # => 'foo' + # + # sanitize_attribute_name('Foo 3 #-Billy') + # => 'foo3-billy' + def sanitize_attribute_name(name) + name.gnuke(/[^\w\-]/).downcase + end + # Private: Process raw input, used for the outermost reader. def process(data, &block) raw_source = [] - include_regexp = /^include::([^\[]+)\[\]\s*\n?\z/ data.each do |line| - if inc = line.match(include_regexp) + if inc = line.match(REGEXP[:include_macro]) if block_given? raw_source.concat yield(inc[1]) else @@ -222,12 +237,6 @@ class Asciidoctor::Reader end end - ifdef_regexp = /^(ifdef|ifndef)::([^\[]+)\[\]/ - endif_regexp = /^endif::/ - defattr_regexp = /^:([^:!]+):\s*(.*)\s*$/ - delete_attr_regexp = /^:([^:]+)!:\s*$/ - conditional_regexp = /^\s*\{([^\?]+)\?\s*([^\}]+)\s*\}/ - skip_to = nil continuing_value = nil continuing_key = nil @@ -239,51 +248,59 @@ class Asciidoctor::Reader close_continue = false # Lines that start with whitespace and end with a '+' are # a continuation, so gobble them up into `value` - if match = line.match(/\s+(.+)\s+\+\s*$/) - continuing_value += ' ' + match[1] - elsif match = line.match(/\s+(.+)/) - # If this continued line doesn't end with a +, then this - # is the end of the continuation, no matter what the next - # line does. - continuing_value += ' ' + match[1] + if line.match(REGEXP[:attr_continue]) + continuing_value += ' ' + $1 + # An empty line ends a continuation + elsif line.strip.empty? + raw_source.unshift(line) close_continue = true else - # If this line doesn't start with whitespace, then it's - # not a valid continuation line, so push it back for processing + # If this continued line isn't empty and doesn't end with a +, then + # this is the end of the continuation, no matter what the next line + # does. + continuing_value += ' ' + line.strip close_continue = true - raw_source.unshift(line) end if close_continue - @attributes[continuing_key] = continuing_value + unless attribute_overridden? continuing_key + @document.attributes[continuing_key] = apply_attribute_value_subs(continuing_value) + end continuing_key = nil continuing_value = nil end - elsif match = line.match(ifdef_regexp) - attr = match[2] - skip = case match[1] - when 'ifdef'; !@attributes.has_key?(attr) - when 'ifndef'; @attributes.has_key?(attr) + elsif line.match(REGEXP[:ifdef_macro]) + attr = $2 + skip = case $1 + when 'ifdef'; !@document.attributes.has_key?(attr) + when 'ifndef'; @document.attributes.has_key?(attr) end skip_to = /^endif::#{attr}\[\]\s*\n/ if skip - elsif match = line.match(defattr_regexp) - key = sanitize_attribute_name(match[1]) - value = match[2] - if match = value.match(Asciidoctor::REGEXP[:attr_continue]) + elsif line.match(REGEXP[:attr_assign]) + key = sanitize_attribute_name($1) + value = $2 + if value.match(REGEXP[:attr_continue]) # attribute value continuation line; grab lines until we run out # of continuation lines continuing_key = key - continuing_value = match[1] # strip off the spaces and + + continuing_value = $1 # strip off the spaces and + Asciidoctor.debug "continuing key: #{continuing_key} with partial value: '#{continuing_value}'" else - @attributes[key] = value - Asciidoctor.debug "Defines[#{key}] is '#{value}'" + unless attribute_overridden? key + @document.attributes[key] = apply_attribute_value_subs(value) + Asciidoctor.debug "Defines[#{key}] is '#{@document.attributes[key]}'" + if key == 'backend' + @document.update_backend_attributes() + end + end + end + elsif line.match(REGEXP[:attr_delete]) + key = sanitize_attribute_name($1) + unless attribute_overridden? key + @document.attributes.delete(key) end - elsif match = line.match(delete_attr_regexp) - key = sanitize_attribute_name(match[1]) - @attributes.delete(key) - elsif !line.match(endif_regexp) - while match = line.match(conditional_regexp) - value = @attributes.has_key?(match[1]) ? match[2] : '' + elsif !line.match(REGEXP[:endif_macro]) + while line.match(REGEXP[:attr_conditional]) + value = @document.attributes.has_key?($1) ? $2 : '' line.sub!(conditional_regexp, value) end # leave line comments in as they play a role in flow (such as a list divider) @@ -293,11 +310,51 @@ class Asciidoctor::Reader # Process bibliography references, so they're available when text # before the reference is being rendered. - @lines.each do |line| - if biblio = line.match(REGEXP[:biblio]) - @references[biblio[1]] = "[#{biblio[1]}]" + # FIXME we don't have support for bibliography lists yet, so disable for now + # plus, this should be done while we are walking lines above + #@lines.each do |line| + # if biblio = line.match(REGEXP[:biblio]) + # @document.references[biblio[1]] = "[#{biblio[1]}]" + # end + #end + + #Asciidoctor.debug "About to leave Reader#process, and references is #{@document.references.inspect}" + end + + # Internal: Determine if the attribute has been overridden in the document options + # + # key - The attribute key to check + # + # Returns true if the attribute has been overridden, false otherwise + def attribute_overridden?(key) + @overrides.has_key?(key) || @overrides.has_key?(key + '!') + end + + # Internal: Apply substitutions to the attribute value + # + # If the value is an inline passthrough macro (e.g., pass:[text]), then + # apply the substitutions defined on the macro to the text. Otherwise, + # apply the verbatim substitutions to the value. + # + # value - The String attribute value on which to perform substitutions + # + # Returns The String value with substitutions performed. + def apply_attribute_value_subs(value) + if value.match(REGEXP[:pass_macro_basic]) + # copy match for Ruby 1.8.7 compat + m = $~ + subs = [] + if !m[1].empty? + sub_options = Asciidoctor::Substituters::COMPOSITE_SUBS.keys + Asciidoctor::Substituters::COMPOSITE_SUBS[:normal] + subs = m[1].split(',').map {|sub| sub.to_sym} & sub_options end + if !subs.empty? + @document.apply_subs(m[2], subs) + else + m[2] + end + else + @document.apply_header_subs(value) end end - end diff --git a/lib/asciidoctor/render_templates.rb b/lib/asciidoctor/render_templates.rb deleted file mode 100644 index c56fde7b..00000000 --- a/lib/asciidoctor/render_templates.rb +++ /dev/null @@ -1,469 +0,0 @@ -class BaseTemplate - BLANK_LINES_PATTERN = /^\s*\n/ - LINE_FEED_ENTITY = ' ' # or 
 - - QUOTED_TAGS = { - :emphasis => ['<em>', '</em>'], - :strong => ['<strong>', '</strong>'], - :monospaced => ['<tt>', '</tt>'], - :superscript => ['<sup>', '</sup>'], - :subscript => ['<sub>', '</sub>'], - :double => [Asciidoctor::INTRINSICS['ldquo'], Asciidoctor::INTRINSICS['rdquo']], - :single => [Asciidoctor::INTRINSICS['lsquo'], Asciidoctor::INTRINSICS['rsquo']], - :none => ['', ''] - } - - def initialize - end - - def self.inherited(klass) - @template_classes ||= [] - @template_classes << klass - end - - def self.template_classes - @template_classes - end - - # We're ignoring locals for now. Shut up. - def render(obj = Object.new, locals = {}) - output = template.result(obj.instance_eval {binding}) - (self.is_a?(DocumentTemplate) || self.is_a?(EmbeddedTemplate)) ? output.gsub(BLANK_LINES_PATTERN, '').gsub(LINE_FEED_ENTITY, "\n") : output - end - - def template - raise "You chilluns need to make your own template" - end - - # create template matter to insert an attribute if the variable has a value - def attribute(name, var = nil) - var = var.nil? ? name : var - if var.is_a? Symbol - '<%= attr?(:' + var.to_s + ') ? ' + '\' ' + name + '=\\\'\' + attr(:' + var.to_s + ') + \'\\\'\' : \'\' %>' - else - '<%= ' + var + ' ? ' + '\' ' + name + '=\\\'\' + ' + var + ' + \'\\\'\' : \'\' %>' - end - end - - # create template matter to insert a style class if the variable has a value - def styleclass(key, offset = true) - '<%= attr?(:' + key.to_s + ') ? ' + (offset ? '\' \' + ' : '') + 'attr(:' + key.to_s + ') : \'\' %>' - end - - # create template matter to insert an id if one is specified for the block - def id - attribute('id') - end - - # create template matter to insert a style class from the role attribute if specified - def role - styleclass(:role) - end -end - -class DocumentTemplate < BaseTemplate - def template - @template ||= ::ERB.new <<-EOF -<%#encoding:UTF-8%> -<!DOCTYPE html> -<html lang='en'> - <head> - <meta http-equiv='Content-Type' content='text/html; charset=<%= attr :encoding %>'> - <meta name='generator' content='Asciidoctor <%= attr 'asciidoctor-version' %>'> - <% if attr? :description %><meta name='description' content='<%= attr :description %>'><% end %> - <% if attr? :keywords %><meta name='keywords' content='<%= attr :keywords %>'><% end %> - <title><%= doctitle %></title> - <% unless attr(:stylesheet, '').empty? %> - <link rel='stylesheet' href='<%= attr(:stylesdir, '') + attr(:stylesheet) %>' type='text/css'> - <% end %> - </head> - <body class='<%= doctype %>'> - <% unless noheader %> - <div id='header'> - <% if has_header %> - <% unless notitle %> - <h1><%= header.title %></h1> - <% end %> - <% if attr? :author %><span id='author'><%= attr :author %></span><br><% end %> - <% if attr? :email %><span id='email' class='monospaced'><<%= attr :email %>></span><br><% end %> - <% if attr? :revnumber %><span id='revnumber'>version <%= attr :revnumber %><%= attr?(:revdate) ? ',' : '' %></span><% end %> - <% if attr? :revdate %><span id='revdate'><%= attr :revdate %></span><% end %> - <% if attr? :revremark %><br><span id='revremark'><%= attr :revremark %></span><% end %> - <% end %> - </div> - <% end %> - <div id='content'> -<%= content %> - </div> - <div id='footer'> - <div id='footer-text'> - Last updated <%= attr :localdatetime %> - </div> - </div> - </body> -</html> - EOF - end -end - -class EmbeddedTemplate < BaseTemplate - def template - @template ||= ::ERB.new <<-EOF -<%#encoding:UTF-8%> -<%= content %> - EOF - end -end - -class BlockPreambleTemplate < BaseTemplate - def template - @template ||= ::ERB.new <<-EOF -<div id='preamble'> - <div class='sectionbody'> -<%= content %> - </div> -</div> - EOF - end -end - -class SectionTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<% if level == 0 %> -<h1#{id}><%= title %></h1> -<%= content %> -<% else %> -<div class='sect<%= level %>#{role}'> - <h<%= level + 1 %>#{id}><%= title %></h<%= level + 1 %>> - <% if level == 1 %> - <div class='sectionbody'> -<%= content %> - </div> - <% else %> -<%= content %> - <% end %> -</div> -<% end %> - EOF - end -end - -class BlockDlistTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<div#{id} class='dlist#{role}'> - <% if title %> - <div class='title'><%= title %></div> - <% end %> - <dl> - <% content.each do |dt, dd| %> - <dt class='hdlist1'> - <% unless dt.anchor.nil? || dt.anchor.empty? %> - <a id='<%= dt.anchor %>'></a> - <% end %> - <%= dt.text %> - </dt> - <% unless dd.nil? %> - <dd> - <% unless dd.text.empty? %> - <p><%= dd.text %></p> - <% end %> - <% unless dd.blocks.empty? %> -<%= dd.content %> - <% end %> - </dd> - <% end %> - <% end %> - </dl> -</div> - EOF - end -end - -class BlockListingTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<div#{id} class='listingblock#{role}'> - <% if title %> - <div class='title'><%= title %></div> - <% end %> - <div class='content monospaced'> - <pre class='highlight#{styleclass(:language)}'><code><%= content.gsub("\n", LINE_FEED_ENTITY) %></code></pre> - </div> -</div> - EOF - end -end - -class BlockLiteralTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<div#{id} class='literalblock#{role}'> - <% if title %> - <div class='title'><%= title %></div> - <% end %> - <div class='content monospaced'> - <pre><%= content.gsub("\n", LINE_FEED_ENTITY) %></pre> - </div> -</div> - EOF - end -end - -class BlockAdmonitionTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<div#{id} class='admonitionblock#{role}'> - <table> - <tr> - <td class='icon'> - <% if attr? :caption %> - <div class='title'><%= attr :caption %></div> - <% end %> - </td> - <td class='content'> - <% unless title.nil? %> - <div class='title'><%= title %></div> - <% end %> - <%= content %> - </td> - </tr> - </table> -</div> - EOF - end -end - -class BlockParagraphTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<%#encoding:UTF-8%> -<div#{id} class='paragraph#{role}'> - <% unless title.nil? %> - <div class='title'><%= title %></div> - <% end %> - <p><%= content %></p> -</div> - EOF - end -end - -class BlockSidebarTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<div#{id} class='sidebarblock#{role}'> - <div class='content'> - <% unless title.nil? %> - <div class='title'><%= title %></div> - <% end %> -<%= content %> - </div> -</div> - EOF - end -end - -class BlockExampleTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<div#{id} class='exampleblock#{role}'> - <div class='content'> - <% unless title.nil? %> - <div class='title'><%= title %></div> - <% end %> -<%= content %> - </div> -</div> - EOF - end -end - -class BlockOpenTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<div#{id} class='openblock#{role}'> - <% unless title.nil? %> - <div class='title'><%= title %></div> - <% end %> - <div class='content'> -<%= content %> - </div> -</div> - EOF - end -end - -class BlockQuoteTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<div#{id} class='quoteblock#{role}'> - <% unless title.nil? %> - <div class='title'><%= title %></div> - <% end %> - <div class='content'> -<%= content %> - </div> - <div class='attribution'> - <% if attr? :citetitle %> - <em><%= attr :citetitle %></em> - <% end %> - <% if attr? :attribution %> - <% if attr? :citetitle %> - <br/> - <% end %> - <%= '— ' + attr(:attribution) %> - <% end %> - </div> -</div> - EOF - end -end - -class BlockVerseTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<div#{id} class='verseblock#{role}'> - <% unless title.nil? %> - <div class='title'><%= title %></div> - <% end %> - <pre class='content'><%= content.gsub("\n", LINE_FEED_ENTITY) %></pre> - <div class='attribution'> - <% if attr? :citetitle %> - <em><%= attr :citetitle %></em> - <% end %> - <% if attr? :attribution %> - <% if attr? :citetitle %> - <br/> - <% end %> - <%= '— ' + attr(:attribution) %> - <% end %> - </div> -</div> - EOF - end -end - -class BlockUlistTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<div#{id} class='ulist#{styleclass(:style)}#{role}'> - <% unless title.nil? %> - <div class='title'><%= title %></div> - <% end %> - <ul> - <% content.each do |li| %> - <li> - <p><%= li.text %></p> - <% unless li.blocks.empty? %> -<%= li.content %> - <% end %> - </li> - <% end %> - </ul> -</div> - EOF - end -end - -class BlockOlistTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<div#{id} class='olist <%= attr :style %>#{role}'> - <% unless title.nil? %> - <div class='title'><%= title %></div> - <% end %> - <ol class='<%= attr :style %>'#{attribute('start', :start)}> - <% content.each do |li| %> - <li> - <p><%= li.text %></p> - <% unless li.blocks.empty? %> -<%= li.content %> - <% end %> - </li> - <% end %> - </ol> -</div> - EOF - end -end - -class BlockImageTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<div#{id} class='imageblock#{role}'> - <div class='content'> - <% if attr :link %> - <a class='image' href='<%= attr :link %>'><img src='<%= attr :target %>' alt='<%= attr :alt %>'#{attribute('width', :width)}#{attribute('height', :height)}></a> - <% else %> - <img src='<%= attr :target %>' alt='<%= attr :alt %>'#{attribute('width', :width)}#{attribute('height', :height)}> - <% end %> - </div> - <% if title %> - <div class='title'><%= title %></div> - <% end %> -</div> - EOF - end -end - -class BlockRulerTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<hr> - EOF - end -end - -class InlineBreakTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<%= text %><br> - EOF - end -end - -class InlineCalloutTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<b><%= text %></b> - EOF - end -end - -class InlineQuotedTemplate < BaseTemplate - # we use double quotes for the class attribute to prevent quote processing - # seems hackish, though AsciiDoc has this same issue - def template - @template ||= ERB.new <<-EOF -<%= QUOTED_TAGS[type].first %><% -if attr? :role %><span class="#{styleclass(:role, false)}"><% -end %><%= text %><% -if attr? :role %></span><% -end %><%= QUOTED_TAGS[type].last %> - EOF - end -end - -class InlineLinkTemplate < BaseTemplate - def template - @template ||= ERB.new <<-EOF -<a href='<%= target %>'><%= text %></a> - EOF - end -end - -class InlineImageTemplate < BaseTemplate - def template - # care is taken here to avoid a space inside the optional <a> tag - @template ||= ERB.new <<-EOF -<span class='image#{role}'> - <% - if attr :link %><a class='image' href='<%= attr :link %>'><% - end %><img src='<%= target %>' alt='<%= attr :alt %>'#{attribute('width', :width)}#{attribute('height', :height)}#{attribute('title', :title)}><% - if attr :link%></a><% end - %> -</span> - EOF - end -end diff --git a/lib/asciidoctor/renderer.rb b/lib/asciidoctor/renderer.rb index 18c350f5..b3c42d5d 100644 --- a/lib/asciidoctor/renderer.rb +++ b/lib/asciidoctor/renderer.rb @@ -8,10 +8,19 @@ class Asciidoctor::Renderer @views = {} - # Load up all the template classes that we know how to render - BaseTemplate.template_classes.each do |tc| - view = tc.to_s.underscore.gsub(/_template$/, '') - @views[view] = tc.new + backend = options[:backend] + case backend + when 'html5', 'docbook45' + require 'asciidoctor/backends/' + backend + # Load up all the template classes that we know how to render for this backend + ::Asciidoctor::BaseTemplate.template_classes.each do |tc| + if tc.to_s.downcase.include?('::' + backend + '::') + view = tc.to_s.nuke(/^.*::/).underscore.nuke(/_template$/) + @views[view] = tc.new(view) + end + end + else + Asciidoctor.debug 'No built-in templates for backend: ' + backend end # If user passed in a template dir, let them override our base templates @@ -80,4 +89,10 @@ class Asciidoctor::Renderer @render_stack.pop ret end + + def views + readonly_views = @views.dup + readonly_views.freeze + readonly_views + end end diff --git a/lib/asciidoctor/section.rb b/lib/asciidoctor/section.rb index d1fff303..db53a416 100644 --- a/lib/asciidoctor/section.rb +++ b/lib/asciidoctor/section.rb @@ -19,8 +19,6 @@ # => 1 class Asciidoctor::Section < Asciidoctor::AbstractBlock - include Asciidoctor::Substituters - # Public: Set the String section title. attr_writer :title @@ -61,6 +59,8 @@ class Asciidoctor::Section < Asciidoctor::AbstractBlock # # Section ID synthesis can be disabled by undefining the sectids attribute. # + # TODO document the substitutions + # # Examples # # section = Section.new(parent) @@ -69,7 +69,8 @@ class Asciidoctor::Section < Asciidoctor::AbstractBlock # => "_foo" def generate_id if self.document.attributes.has_key? 'sectids' - self.document.attributes.fetch('idprefix', '_') + "#{title && title.downcase.gsub(/\W+/,'_').gsub(/_+$/, '')}".tr_s('_', '_') + (self.document.attributes.fetch('idprefix', '_') + + (title ? title.downcase.gsub(/&#[0-9]+;/, '_').gsub(/\W+/, '_').trim('_').tr_s('_', '_') : '')) else nil end diff --git a/lib/asciidoctor/string.rb b/lib/asciidoctor/string.rb index efd38b55..2eb46d8c 100644 --- a/lib/asciidoctor/string.rb +++ b/lib/asciidoctor/string.rb @@ -1,12 +1,100 @@ -unless String.instance_methods.include? 'underscore' - class String - # Yes, oh Rails, I stealz you so bad - def underscore - self.gsub(/::/, '/'). - gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). - gsub(/([a-z\d])([A-Z])/,'\1_\2'). - tr("-", "_"). - downcase - end - end -end
\ No newline at end of file +# Public: String monkeypatching +class String + # Public: Makes an underscored, lowercase form from the expression in the string. + # + # Changes '::' to '/' to convert namespaces to paths. + # Changes camelcase words to underscore delimited and lowercase words + # + # (Yes, oh Rails, I stealz you so bad) + # + # Examples + # + # "ActiveRecord".underscore + # # => "active_record" + # + # "ActiveRecord::Errors".underscore + # # => active_record/errors + # + # Returns A copy of this String with the underscore rules applied + def underscore + self.gsub('::', '/'). + gsub(/([[:upper:]]+)([[:upper:]][[:alpha:]])/, '\1_\2'). + gsub(/([[:lower:][:digit:]])([[:upper:]])/, '\1_\2'). + tr('-', '_'). + downcase + end unless method_defined?(:underscore) + + # Public: Return a copy of this string with the specified character removed + # from the beginning and the end of the original string. + # + # The character will be removed until it is no longer found in the first or + # last character position of the String. + # + # char - The single-character String to remove + # + # Returns A copy of this String with the specified character removed from the + # beginning and end of the original string + def trim(char) + self.rtrim(char).ltrim(char) + end + + # Public: Return a copy of this string with the specified character removed + # from the beginning of the original string + # + # The character will be removed until it is no longer found in the first + # character position of the String. + # + # char - The single-character String to remove + # + # Returns A copy of this String with the specified character removed from the + # beginning of the original string + def ltrim(char) + # optimization + return self.dup if self[0..0] != char + + result = self.dup + result = result[1..-1] while result[0..0] == char + result + end + + # Public: Return a copy of this string with the specified character removed + # from the end of the original string + # + # The character will be removed until it is no longer found in the last + # character position of the String. + # + # char - The single-character String to remove + # + # Returns A copy of this String with the specified character removed from the + # end of the original string + def rtrim(char) + # optimization + return self.dup if self[-1..-1] != char + + result = self.dup + result = result[0..-2] while result[-1..-1] == char + result + end + + # Public: Return a copy of this String with the first occurrence of the characters that match the specified pattern removed + # + # A convenience method for sub(pattern, '') + # + # pattern - The Regexp matching characters to remove + # + # Returns A copy of this String with the first occurrence of the match characters removed + def nuke(pattern) + self.sub(pattern, '') + end + + # Public: Return a copy of this String with all the occurrences of the characters that match the specified pattern removed + # + # A convenience method for gsub(pattern, '') + # + # pattern - The Regexp matching characters to remove + # + # Returns A copy of this String with all occurrences of the match characters removed + def gnuke(pattern) + self.gsub(pattern, '') + end +end diff --git a/lib/asciidoctor/substituters.rb b/lib/asciidoctor/substituters.rb index acf08895..3f18b9e9 100644 --- a/lib/asciidoctor/substituters.rb +++ b/lib/asciidoctor/substituters.rb @@ -46,7 +46,7 @@ module Asciidoctor when :quotes text = sub_quotes(text) when :attributes - text = Substituters.sub_attributes(text.lines.entries, self.document).join + text = sub_attributes(text.lines.entries).join when :replacements text = sub_replacements(text) when :macros @@ -66,11 +66,11 @@ module Asciidoctor # Public: Apply normal substitutions. # - # lines - A String Array containing the lines of text process + # lines - The lines of text to process. Can be a String or a String Array # # returns - A String with normal substitutions performed def apply_normal_subs(lines) - apply_subs(lines.join) + apply_subs(lines.is_a?(Array) ? lines.join : lines) end # Public: Apply substitutions for titles. @@ -91,13 +91,13 @@ module Asciidoctor apply_subs(lines.join, COMPOSITE_SUBS[:verbatim]) end - # Public: Apply substitutions for header metadata + # Public: Apply substitutions for header metadata and attribute assignments # - # lines - A String Array containing the lines of text process + # text - String containing the text process # - # returns - A String Array with header substitutions performed - def apply_header_subs(lines) - apply_subs(lines, [:specialcharacters, :attributes]) + # returns - A String with header substitutions performed + def apply_header_subs(text) + apply_subs(text, [:specialcharacters, :attributes]) end # Public: Apply substitutions for passthrough text @@ -117,7 +117,7 @@ module Asciidoctor def extract_passthroughs(text) result = text.dup - result.gsub!(Asciidoctor::REGEXP[:passthrough_macro]) { + result.gsub!(REGEXP[:pass_macro]) { # copy match for Ruby 1.8.7 compat m = $~ # honor the escape @@ -136,7 +136,7 @@ module Asciidoctor "\x0" + (@passthroughs.size - 1).to_s + "\x0" } unless !(result.include?('+++') || result.include?('$$') || result.include?('pass:')) - result.gsub!(Asciidoctor::REGEXP[:passthrough_lit]) { + result.gsub!(REGEXP[:pass_lit]) { # copy match for Ruby 1.8.7 compat m = $~ # honor the escape @@ -156,8 +156,8 @@ module Asciidoctor # # returns The String text with the passthrough text restored def restore_passthroughs(text) - return text if !text.include?("\x0") - text.gsub(Asciidoctor::REGEXP[:pass_placeholder]) { + return text if @passthroughs.nil? || @passthroughs.empty? || !text.include?("\x0") + text.gsub(REGEXP[:pass_placeholder]) { pass = @passthroughs[$1.to_i]; text = apply_subs(pass[:text], pass.fetch(:subs, [])) pass[:literal] ? Inline.new(self, :quoted, text, :type => :monospaced).render : text @@ -173,9 +173,9 @@ module Asciidoctor # returns The String text with special characters replaced def sub_specialcharacters(text) # this syntax only available in Ruby 1.9 - #text.gsub(Asciidoctor::SPECIAL_CHARS_PATTERN, Asciidoctor::SPECIAL_CHARS) + #text.gsub(SPECIAL_CHARS_PATTERN, SPECIAL_CHARS) - text.gsub(Asciidoctor::SPECIAL_CHARS_PATTERN) { Asciidoctor::SPECIAL_CHARS[$&] } + text.gsub(SPECIAL_CHARS_PATTERN) { SPECIAL_CHARS[$&] } end # Public: Substitute quoted text (includes emphasis, strong, monospaced, etc) @@ -185,12 +185,10 @@ module Asciidoctor # returns The String text with quoted text rendered using the backend templates def sub_quotes(text) result = text.dup - Asciidoctor::QUOTE_SUBS.each {|type, scope, pattern| + QUOTE_SUBS.each {|type, scope, pattern| result.gsub!(pattern) { transform_quoted_text($~, type, scope) } } - - # unescape escaped single quotes after processing - result.gsub(Asciidoctor::REGEXP[:single_quote_esc], '\1\'\2') + result end # Public: Substitute replacement characters (e.g., copyright, trademark, etc) @@ -200,7 +198,7 @@ module Asciidoctor # returns The String text with the replacement characters substituted def sub_replacements(text) result = text.dup - Asciidoctor::REPLACEMENTS.each {|pattern, replacement| + REPLACEMENTS.each {|pattern, replacement| result.gsub!(pattern, replacement) } result @@ -219,20 +217,21 @@ module Asciidoctor #-- # NOTE it's necessary to perform this substitution line-by-line # so that a missing key doesn't wipe out the whole block of data - def self.sub_attributes(data, document) + def sub_attributes(data) return data if data.nil? || data.empty? - lines = data.is_a?(String) ? [data] : data + # normalizes data type to an array (string becomes single-element array) + lines = Array(data) result = lines.map {|line| reject = false subject = line.dup - subject.gsub!(Asciidoctor::REGEXP[:attr_ref]) { + subject.gsub!(REGEXP[:attr_ref]) { if !$1.empty? || !$3.empty? '{' + $2 + '}' elsif document.attributes.has_key? $2 document.attributes[$2] - elsif Asciidoctor::INTRINSICS.has_key? $2 - Asciidoctor::INTRINSICS[$2] + elsif INTRINSICS.has_key? $2 + INTRINSICS[$2] else Asciidoctor.debug 'Missing attribute: ' + $2 + ', line marked for removal' reject = true @@ -259,14 +258,14 @@ module Asciidoctor result = text.dup # inline images, image:target.ext[Alt] - result.gsub!(Asciidoctor::REGEXP[:image_macro]) { + result.gsub!(REGEXP[:image_macro]) { # copy match for Ruby 1.8.7 compat m = $~ # honor the escape if m[0].start_with? '\\' next m[0][1..-1] end - target = Substituters.sub_attributes(m[1], self.document) + target = sub_attributes(m[1]) attrs = parse_attributes(m[2], ['alt', 'width', 'height']) if !attrs.has_key?('alt') || attrs['alt'].empty? attrs['alt'] = File.basename(target, File.extname(target)) @@ -275,7 +274,7 @@ module Asciidoctor } unless !result.include?('image:') # inline urls, target[text] (optionally prefixed with link: and optionally surrounded by <>) - result.gsub!(Asciidoctor::REGEXP[:link_inline]) { + result.gsub!(REGEXP[:link_inline]) { # copy match for Ruby 1.8.7 compat m = $~ # honor the escape @@ -288,12 +287,12 @@ module Asciidoctor end prefix = (m[1] != 'link:' ? m[1] : '') target = m[2] - text = !m[3].nil? ? Substituters.sub_attributes(m[3].gsub('\]', ']'), self.document) : '' - prefix + Inline.new(self, :link, (!text.empty? ? text : target), :target => target).render + text = !m[3].nil? ? sub_attributes(m[3].gsub('\]', ']')) : '' + prefix + Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target).render } unless !result.include?('http') # inline link macros, link:target[text] - result.gsub!(Asciidoctor::REGEXP[:link_macro]) { + result.gsub!(REGEXP[:link_macro]) { # copy match for Ruby 1.8.7 compat m = $~ # honor the escape @@ -301,11 +300,11 @@ module Asciidoctor next m[0][1..-1] end target = m[1] - text = Substituters.sub_attributes(m[2].gsub('\]', ']'), self.document) - Inline.new(self, :link, (!text.empty? ? text : target), :target => target).render + text = sub_attributes(m[2].gsub('\]', ']')) + Inline.new(self, :anchor, (!text.empty? ? text : target), :type => :link, :target => target).render } unless !result.include?('link:') - result.gsub!(Asciidoctor::REGEXP[:xref_macro]) { + result.gsub!(REGEXP[:xref_macro]) { # copy match for Ruby 1.8.7 compat m = $~ # honor the escape @@ -318,7 +317,7 @@ module Asciidoctor id = m[2] reftext = !m[3].empty? ? m[3] : nil end - Inline.new(self, :link, reftext || document.references.fetch(id, '[' + id + ']'), :target => '#' + id).render + Inline.new(self, :anchor, reftext, :type => :xref, :target => id).render } result @@ -330,7 +329,7 @@ module Asciidoctor # # returns The String with the callout references rendered using the backend templates def sub_callouts(text) - text.gsub(Asciidoctor::REGEXP[:calloutref]) { Inline.new(self, :callout, $1).render } + text.gsub(REGEXP[:calloutref]) { Inline.new(self, :callout, $1).render } end # Public: Substitute post replacements diff --git a/test/attributes_test.rb b/test/attributes_test.rb index 88882882..ff781b06 100644 --- a/test/attributes_test.rb +++ b/test/attributes_test.rb @@ -1,104 +1,236 @@ require 'test_helper' -context "Attributes" do - test "creates an attribute" do - doc = document_from_string(":frog: Tanglefoot") - assert_equal doc.attributes['frog'], 'Tanglefoot' - end +context 'Attributes' do + context 'Assignment' do + test 'creates an attribute' do + doc = document_from_string(':frog: Tanglefoot') + assert_equal 'Tanglefoot', doc.attributes['frog'] + end - test "creates an attribute by fusing a multi-line value" do - str = <<-EOS + test 'creates an attribute by fusing a multi-line value' do + str = <<-EOS :description: This is the first + Ruby implementation of + AsciiDoc. - EOS - doc = document_from_string(str) - assert_equal doc.attributes['description'], 'This is the first Ruby implementation of AsciiDoc.' - end + EOS + doc = document_from_string(str) + assert_equal 'This is the first Ruby implementation of AsciiDoc.', doc.attributes['description'] + end - test "deletes an attribute" do - doc = document_from_string(":frog: Tanglefoot\n:frog!:") - assert_equal nil, doc.attributes['frog'] - end + test 'deletes an attribute' do + doc = document_from_string(":frog: Tanglefoot\n:frog!:") + assert_equal nil, doc.attributes['frog'] + end - test "doesn't choke when deleting a non-existing attribute" do - doc = document_from_string(":frog!:") - assert_equal nil, doc.attributes['frog'] - end + test "doesn't choke when deleting a non-existing attribute" do + doc = document_from_string(':frog!:') + assert_equal nil, doc.attributes['frog'] + end - test "render properly with simple names" do - html = render_string(":frog: Tanglefoot\nYo, {frog}!") - result = Nokogiri::HTML(html) - assert_equal 'Yo, Tanglefoot!', result.css("p").first.content.strip - end + test "replaces special characters in attribute value" do + doc = document_from_string(":xml-busters: <>&") + assert_equal '<>&', doc.attributes['xml-busters'] + end - test "render properly with single character name" do - html = render_string(":r: Ruby\n\nR is for {r}!") - result = Nokogiri::HTML(html) - assert_equal 'R is for Ruby!', result.css("p").first.content.strip - end + test "performs attribute substitution on attribute value" do + doc = document_from_string(":version: 1.0\n:release: Asciidoctor {version}") + assert_equal 'Asciidoctor 1.0', doc.attributes['release'] + end - test "convert multi-word names and render" do - html = render_string("Main Header\n===========\n:My frog: Tanglefoot\n\nYo, {myfrog}!") - result = Nokogiri::HTML(html) - assert_equal 'Yo, Tanglefoot!', result.css("p").first.content.strip - end + test "assigns attribute to empty string if substitution fails to resolve attribute" do + doc = document_from_string(":release: Asciidoctor {version}") + assert_equal '', doc.attributes['release'] + end - test "ignores lines with bad attributes" do - html = render_string("This is\nblah blah {foobarbaz}\nall there is.") - result = Nokogiri::HTML(html) - assert_no_match /blah blah/m, result.css("p").first.content.strip - end + test "assigns multi-line attribute to empty string if substitution fails to resolve attribute" do + doc = document_from_string(":release: Asciidoctor +\n {version}") + assert_equal '', doc.attributes['release'] + end - # See above - AsciiDoc says we're supposed to delete lines with bad - # attribute refs in them. AsciiDoc is strange. - # - # test "Unknowns" do - # html = render_string("Look, a {gobbledygook}") - # result = Nokogiri::HTML(html) - # assert_equal("Look, a {gobbledygook}", result.css("p").first.content.strip) - # end - - test "substitutes inside unordered list items" do - html = render_string(":foo: bar\n* snort at the {foo}\n* yawn") - result = Nokogiri::HTML(html) - assert_match /snort at the bar/, result.css("li").first.content.strip - end + test "apply custom substitutions to text in passthrough macro and assign to attribute" do + doc = document_from_string(":xml-busters: pass:[<>&]") + assert_equal '<>&', doc.attributes['xml-busters'] + doc = document_from_string(":xml-busters: pass:none[<>&]") + assert_equal '<>&', doc.attributes['xml-busters'] + doc = document_from_string(":xml-busters: pass:specialcharacters[<>&]") + assert_equal '<>&', doc.attributes['xml-busters'] + end - test "substitutes inside heading" do - output = render_string(":prefix: Cool\n\n== {prefix} Title\n\ncontent") - result = Nokogiri::HTML(output) - assert_match /Cool Title/, result.css('h2').first.content - assert_match /_cool_title/, result.css('h2').first.attr('id') - end + test "attribute is treated as defined until it's not" do + input = <<-EOS +:holygrail: +ifdef::holygrail[] +The holy grail has been found! +endif::holygrail[] + +:holygrail!: +ifndef::holygrail[] +Buggers! What happened to the grail? +endif::holygrail[] + EOS + output = render_string input + assert_xpath '//p', output, 2 + assert_xpath '(//p)[1][text() = "The holy grail has been found!"]', output, 1 + assert_xpath '(//p)[2][text() = "Buggers! What happened to the grail?"]', output, 1 + end - test "renders attribute until it's deleted" do - pending "Not working yet (will require adding element-specific attributes or early attr substitution during parsing)" - # html = render_string(":foo: bar\nCrossing the {foo}\n\n:foo!:\nBelly up to the {foo}") - # result = Nokogiri::HTML(html) - # assert_match /Crossing the bar/, result.css("p").first.content.strip - # assert_no_match /Belly up to the bar/, result.css("p").last.content.strip - end + # Validates requirement: "Header attributes are overridden by command-line attributes." + test 'attribute defined in document options overrides attribute in document' do + doc = document_from_string(':cash: money', :attributes => {'cash' => 'heroes'}) + assert_equal 'heroes', doc.attributes['cash'] + end - test "doesn't disturb attribute-looking things escaped with backslash" do - html = render_string(":foo: bar\nThis is a \\{foo} day.") - result = Nokogiri::HTML(html) - assert_equal 'This is a {foo} day.', result.css('p').first.content.strip - end + test 'attribute defined in document options cannot be unassigned in document' do + doc = document_from_string(':cash!:', :attributes => {'cash' => 'heroes'}) + assert_equal 'heroes', doc.attributes['cash'] + end + + test 'attribute undefined in document options cannot be assigned in document' do + doc = document_from_string(':cash: money', :attributes => {'cash!' => 1 }) + assert_equal nil, doc.attributes['cash'] + doc = document_from_string(':cash: money', :attributes => {'cash' => nil }) + assert_equal nil, doc.attributes['cash'] + end + + test 'backend attributes are updated if backend attribute is defined in document' do + doc = document_from_string(':backend: docbook45') + assert_equal 'docbook45', doc.attributes['backend'] + assert doc.attributes.has_key? 'backend-docbook45' + assert_equal 'docbook', doc.attributes['basebackend'] + assert doc.attributes.has_key? 'basebackend-docbook' + end + + test 'backend attributes defined in document options overrides backend attribute in document' do + doc = document_from_string(':backend: docbook45', :attributes => {'backend' => 'html5'}) + assert_equal 'html5', doc.attributes['backend'] + assert doc.attributes.has_key? 'backend-html5' + assert_equal 'html', doc.attributes['basebackend'] + assert doc.attributes.has_key? 'basebackend-html' + end - test "doesn't disturb attribute-looking things escaped with literals" do - #html = render_string(":foo: bar\nThis is a +++{foo}+++ day.") - #result = Nokogiri::HTML(html) - #assert_equal 'This is a {foo} day.', result.css('p').first.content.strip - pending "Don't yet have inline passthrough working" end - test "doesn't substitute attributes inside code blocks" do - pending "whut?" + context 'Interpolation' do + + test "render properly with simple names" do + html = render_string(":frog: Tanglefoot\n:my_super-hero: Spiderman\n\nYo, {frog}!\nBeat {my_super-hero}!") + result = Nokogiri::HTML(html) + assert_equal "Yo, Tanglefoot!\nBeat Spiderman!", result.css("p").first.content.strip + end + + test "render properly with single character name" do + html = render_string(":r: Ruby\n\nR is for {r}!") + result = Nokogiri::HTML(html) + assert_equal 'R is for Ruby!', result.css("p").first.content.strip + end + + test "convert multi-word names and render" do + html = render_string("Main Header\n===========\n:My frog: Tanglefoot\n\nYo, {myfrog}!") + result = Nokogiri::HTML(html) + assert_equal 'Yo, Tanglefoot!', result.css("p").first.content.strip + end + + test "ignores lines with bad attributes" do + html = render_string("This is\nblah blah {foobarbaz}\nall there is.") + result = Nokogiri::HTML(html) + assert_no_match /blah blah/m, result.css("p").first.content.strip + end + + test "attribute value gets interpretted when rendering" do + doc = document_from_string(":google: http://google.com[Google]\n\n{google}") + assert_equal 'http://google.com[Google]', doc.attributes['google'] + output = doc.render + assert_xpath '//a[@href="http://google.com"][text() = "Google"]', output, 1 + end + + # See above - AsciiDoc says we're supposed to delete lines with bad + # attribute refs in them. AsciiDoc is strange. + # + # test "Unknowns" do + # html = render_string("Look, a {gobbledygook}") + # result = Nokogiri::HTML(html) + # assert_equal("Look, a {gobbledygook}", result.css("p").first.content.strip) + # end + + test "substitutes inside unordered list items" do + html = render_string(":foo: bar\n* snort at the {foo}\n* yawn") + result = Nokogiri::HTML(html) + assert_match /snort at the bar/, result.css("li").first.content.strip + end + + test "substitutes inside heading" do + output = render_string(":prefix: Cool\n\n== {prefix} Title\n\ncontent") + result = Nokogiri::HTML(output) + assert_match /Cool Title/, result.css('h2').first.content + assert_match /_cool_title/, result.css('h2').first.attr('id') + end + + test 'renders attribute until it is deleted' do + pending 'This requires that we consume attributes as the document is being lexed, not up front' + #output = render_string(":foo: bar\n\nCrossing the {foo}\n\n:foo!:\nBelly up to the {foo}") + # result = Nokogiri::HTML(html) + # assert_match /Crossing the bar/, result.css("p").first.content.strip + # assert_no_match /Belly up to the bar/, result.css("p").last.content.strip + end + + test 'does not disturb attribute-looking things escaped with backslash' do + html = render_string(":foo: bar\nThis is a \\{foo} day.") + result = Nokogiri::HTML(html) + assert_equal 'This is a {foo} day.', result.css('p').first.content.strip + end + + test 'does not disturb attribute-looking things escaped with literals' do + html = render_string(":foo: bar\nThis is a +++{foo}+++ day.") + result = Nokogiri::HTML(html) + assert_equal 'This is a {foo} day.', result.css('p').first.content.strip + end + + test 'does not substitute attributes inside listing blocks' do + input = <<-EOS +:forecast: snow + +---- +puts 'The forecast for today is {forecast}' +---- + EOS + output = render_string(input) + assert_match /\{forecast\}/, output + end + + test 'does not substitute attributes inside literal blocks' do + input = <<-EOS +:foo: bar + +.... +You insert the text {foo} to expand the value +of the attribute named foo in your document. +.... + EOS + output = render_string(input) + assert_match /\{foo\}/, output + end end - test "doesn't substitute attributes inside literal blocks" do - pending "whut?" + context "Intrinsic attributes" do + + test "substitute intrinsics" do + Asciidoctor::INTRINSICS.each_pair do |key, value| + html = render_string("Look, a {#{key}} is here") + # can't use Nokogiri because it interprets the HTML entities and we can't match them + assert_match /Look, a #{Regexp.escape(value)} is here/, html + end + end + + test "don't escape intrinsic substitutions" do + html = render_string('happy{nbsp}together') + assert_match /happy together/, html + end + + test "escape special characters" do + html = render_string('<node>&</node>') + assert_match /<node>&<\/node>/, html + end + end context "Block attributes" do @@ -131,13 +263,40 @@ A famous quote. ____ EOS doc = document_from_string(input) - qb = doc.elements.first + qb = doc.blocks.first assert_equal 'quote', qb.attributes['style'] assert_equal 'quote', qb.attr(:style) assert_equal 'Name', qb.attributes['attribution'] assert_equal 'Source', qb.attributes['citetitle'] end + test "Normal substitutions are performed on single-quoted attributes" do + input = <<-EOS +[quote, Name, 'http://wikipedia.org[Source]'] +____ +A famous quote. +____ + EOS + doc = document_from_string(input) + qb = doc.blocks.first + assert_equal 'quote', qb.attributes['style'] + assert_equal 'quote', qb.attr(:style) + assert_equal 'Name', qb.attributes['attribution'] + assert_equal '<a href="http://wikipedia.org">Source</a>', qb.attributes['citetitle'] + end + + test "Attribute substitutions are performed on attribute list before parsing attributes" do + input = <<-EOS +:lead: role="lead" + +[{lead}] +A paragraph + EOS + doc = document_from_string(input) + para = doc.blocks.first + assert_equal 'lead', para.attributes['role'] + end + test "Block attributes are additive" do input = <<-EOS [id='foo'] @@ -145,7 +304,7 @@ ____ A paragraph. EOS doc = document_from_string(input) - para = doc.elements.first + para = doc.blocks.first assert_equal 'foo', para.id assert_equal 'lead', para.attributes['role'] end @@ -195,36 +354,14 @@ block comment content EOS doc = document_from_string(input) - section_one = doc.elements.first + section_one = doc.blocks.first assert_equal 'one', section_one.id subsection = section_one.blocks.last assert_equal 'sub', subsection.id - section_two = doc.elements.last + section_two = doc.blocks.last assert_equal 'classy', section_two.attr(:role) assert !doc.attributes.has_key?('orphaned') end end - context "intrinsics" do - - test "substitute intrinsics" do - Asciidoctor::INTRINSICS.each_pair do |key, value| - html = render_string("Look, a {#{key}} is here") - # can't use Nokogiri because it interprets the HTML entities and we can't match them - assert_match /Look, a #{Regexp.escape(value)} is here/, html - end - end - - test "don't escape intrinsic substitutions" do - html = render_string('happy{nbsp}together') - assert_match /happy together/, html - end - - test "escape special characters" do - html = render_string('<node>&</node>') - assert_match /<node>&<\/node>/, html - end - - end - end diff --git a/test/blocks_test.rb b/test/blocks_test.rb index 11a83f2c..17544140 100644 --- a/test/blocks_test.rb +++ b/test/blocks_test.rb @@ -36,7 +36,7 @@ context "Blocks" do test "trailing endlines after block comment at end of document does not create paragraph" do d = document_from_string("Paragraph\n\n////\nblock comment\n////\n\n") - assert_equal 1, d.elements.size + assert_equal 1, d.blocks.size end end diff --git a/test/document_test.rb b/test/document_test.rb index 10a0b263..a7c6a052 100644 --- a/test/document_test.rb +++ b/test/document_test.rb @@ -1,48 +1,86 @@ require 'test_helper' -context "Document" do +context 'Document' do - context "Example document" do - setup do + context 'Example document' do + test 'test_title' do @doc = example_document(:asciidoc_index) + assert_equal 'AsciiDoc Home Page', @doc.doctitle + assert_equal 'AsciiDoc Home Page', @doc.name + assert_equal 14, @doc.blocks.size + assert_equal :preamble, @doc.blocks[0].context + assert @doc.blocks[1].is_a? ::Asciidoctor::Section end + end - test "test_title" do - assert_equal "AsciiDoc Home Page", @doc.doctitle - assert_equal "AsciiDoc Home Page", @doc.name - assert_equal 14, @doc.elements.size - assert_equal :preamble, @doc.elements[0].context - assert @doc.elements[1].is_a? ::Asciidoctor::Section + context 'Renderer' do + test 'built-in HTML5 views are registered by default' do + doc = document_from_string '' + assert_equal 'html5', doc.attributes['backend'] + assert doc.attributes.has_key? 'backend-html5' + assert_equal 'html', doc.attributes['basebackend'] + assert doc.attributes.has_key? 'basebackend-html' + renderer = doc.renderer + assert !renderer.nil? + views = renderer.views + assert !views.nil? + assert_equal 23, views.size + assert views.has_key? 'document' + assert views['document'].is_a?(Asciidoctor::HTML5::DocumentTemplate) end - end - test "test_with_no_title" do - d = document_from_string("Snorf") - assert_nil d.doctitle - assert_nil d.name - assert !d.has_header - assert_nil d.header + test 'built-in DocBook45 views are registered when backend is docbook45' do + doc = document_from_string '', :attributes => {'backend' => 'docbook45'} + renderer = doc.renderer + assert_equal 'docbook45', doc.attributes['backend'] + assert doc.attributes.has_key? 'backend-docbook45' + assert_equal 'docbook', doc.attributes['basebackend'] + assert doc.attributes.has_key? 'basebackend-docbook' + assert !renderer.nil? + views = renderer.views + assert !views.nil? + assert_equal 23, views.size + assert views.has_key? 'document' + assert views['document'].is_a?(Asciidoctor::DocBook45::DocumentTemplate) + end end - test "test_with_explicit_title" do - d = document_from_string("= Title\n:title: Document Title\n\npreamble\n\n== Section") - assert_equal 'Document Title', d.doctitle - assert_equal 'Document Title', d.title - assert d.has_header - assert_equal 'Title', d.header.title - assert_equal 'Title', d.first_section.title - end + context 'Structure' do + test 'test_with_no_title' do + doc = document_from_string('Snorf') + assert_nil doc.doctitle + assert_nil doc.name + assert !doc.has_header? + assert_nil doc.header + end - test "test_empty_document" do - d = document_from_string('') - assert d.elements.empty? - assert_nil d.doctitle - assert !d.has_header - assert_nil d.header - end + test 'test_with_explicit_title' do + input = <<-EOS += Title +:title: Document Title + +preamble + +== First Section + EOS + doc = document_from_string input + assert_equal 'Document Title', doc.doctitle + assert_equal 'Document Title', doc.title + assert doc.has_header? + assert_equal 'Title', doc.header.title + assert_equal 'Title', doc.first_section.title + end - test "test_with_metadata" do - input = <<-EOS + test 'test_empty_document' do + doc = document_from_string('') + assert doc.blocks.empty? + assert_nil doc.doctitle + assert !doc.has_header? + assert_nil doc.header + end + + test 'test_with_metadata' do + input = <<-EOS = AsciiDoc Stuart Rackham <founder@asciidoc.org> v8.6.8, 2012-07-12: See changelog. @@ -50,28 +88,99 @@ v8.6.8, 2012-07-12: See changelog. == Version 8.6.8 more info... - EOS - output = render_string input - assert_xpath '//*[@id="header"]/span[@id="author"][text() = "Stuart Rackham"]', output, 1 - assert_xpath '//*[@id="header"]/span[@id="email"][contains(text(), "founder@asciidoc.org")]', output, 1 - assert_xpath '//*[@id="header"]/span[@id="revnumber"][text() = "version 8.6.8,"]', output, 1 - assert_xpath '//*[@id="header"]/span[@id="revdate"][text() = "2012-07-12"]', output, 1 - assert_xpath '//*[@id="header"]/span[@id="revremark"][text() = "See changelog."]', output, 1 - end + EOS + output = render_string input + assert_xpath '//*[@id="header"]/span[@id="author"][text() = "Stuart Rackham"]', output, 1 + assert_xpath '//*[@id="header"]/span[@id="email"][contains(text(), "founder@asciidoc.org")]', output, 1 + assert_xpath '//*[@id="header"]/span[@id="revnumber"][text() = "version 8.6.8,"]', output, 1 + assert_xpath '//*[@id="header"]/span[@id="revdate"][text() = "2012-07-12"]', output, 1 + assert_xpath '//*[@id="header"]/span[@id="revremark"][text() = "See changelog."]', output, 1 + end - test "test_with_header_footer" do - result = render_string("= Title\n\npreamble") - assert_xpath '/html', result, 1 - assert_xpath '//*[@id="header"]', result, 1 - assert_xpath '//*[@id="footer"]', result, 1 - assert_xpath '//*[@id="preamble"]', result, 1 + test 'test_with_header_footer' do + result = render_string("= Title\n\npreamble") + assert_xpath '/html', result, 1 + assert_xpath '//*[@id="header"]', result, 1 + assert_xpath '//*[@id="footer"]', result, 1 + assert_xpath '//*[@id="preamble"]', result, 1 + end + + test 'test_with_no_header_footer' do + result = render_string("= Title\n\npreamble", :header_footer => false) + assert_xpath '/html', result, 0 + assert_xpath '/*[@id="header"]', result, 0 + assert_xpath '/*[@id="footer"]', result, 0 + assert_xpath '/*[@id="preamble"]', result, 1 + end end - test "test_with_no_header_footer" do - result = render_string("= Title\n\npreamble", :header_footer => false) - assert_xpath '/html', result, 0 - assert_xpath '/*[@id="header"]', result, 0 - assert_xpath '/*[@id="footer"]', result, 0 - assert_xpath '/*[@id="preamble"]', result, 1 + context 'Backends and Doctypes' do + test 'test_html5_backend_doctype_article' do + result = render_string("= Title\n\npreamble", :attributes => {'backend' => 'html5'}) + assert_xpath '/html', result, 1 + assert_xpath '/html/body[@class="article"]', result, 1 + assert_xpath '/html//*[@id="header"]/h1[text() = "Title"]', result, 1 + assert_xpath '/html//*[@id="preamble"]//p[text() = "preamble"]', result, 1 + end + + test 'test_html5_backend_doctype_book' do + result = render_string("= Title\n\npreamble", :attributes => {'backend' => 'html5', 'doctype' => 'book'}) + assert_xpath '/html', result, 1 + assert_xpath '/html/body[@class="book"]', result, 1 + assert_xpath '/html//*[@id="header"]/h1[text() = "Title"]', result, 1 + assert_xpath '/html//*[@id="preamble"]//p[text() = "preamble"]', result, 1 + end + + test 'test_docbook45_backend_doctype_article' do + input = <<-EOS += Title + +preamble + +== First Section + +section body + EOS + result = render_string(input, :attributes => {'backend' => 'docbook45'}) + assert_xpath '/article', result, 1 + assert_xpath '/article/articleinfo/title[text() = "Title"]', result, 1 + assert_xpath '/article/simpara[text() = "preamble"]', result, 1 + assert_xpath '/article/section', result, 1 + assert_xpath '/article/section[@id = "_first_section"]/title[text() = "First Section"]', result, 1 + assert_xpath '/article/section[@id = "_first_section"]/simpara[text() = "section body"]', result, 1 + end + + test 'test_docbook45_backend_doctype_article_no_title' do + result = render_string('text', :attributes => {'backend' => 'docbook45'}) + assert_xpath '/article', result, 1 + assert_xpath '/article/articleinfo/date', result, 1 + assert_xpath '/article/simpara[text() = "text"]', result, 1 + end + + test 'test_docbook45_backend_doctype_book' do + input = <<-EOS += Title + +preamble + +== First Chapter + +chapter body + EOS + result = render_string(input, :attributes => {'backend' => 'docbook45', 'doctype' => 'book'}) + assert_xpath '/book', result, 1 + assert_xpath '/book/bookinfo/title[text() = "Title"]', result, 1 + assert_xpath '/book/preface/simpara[text() = "preamble"]', result, 1 + assert_xpath '/book/chapter', result, 1 + assert_xpath '/book/chapter[@id = "_first_chapter"]/title[text() = "First Chapter"]', result, 1 + assert_xpath '/book/chapter[@id = "_first_chapter"]/simpara[text() = "chapter body"]', result, 1 + end + + test 'test_docbook45_backend_doctype_book_no_title' do + result = render_string('text', :attributes => {'backend' => 'docbook45', 'doctype' => 'book'}) + assert_xpath '/book', result, 1 + assert_xpath '/book/bookinfo/date', result, 1 + assert_xpath '/book/simpara[text() = "text"]', result, 1 + end end end diff --git a/test/headers_test.rb b/test/headers_test.rb index d8a05a1f..4acc4320 100644 --- a/test/headers_test.rb +++ b/test/headers_test.rb @@ -76,7 +76,7 @@ context "Headers" do end test "with non-word character" do - assert_xpath "//h2[@id='_where_s_the_love'][text() = \"Where's the love?\"]", render_string("== Where's the love?") + assert_xpath "//h2[@id='_where_s_the_love'][text() = \"Where#{[8217].pack('U*')}s the love?\"]", render_string("== Where's the love?") end test "with sequential non-word characters" do diff --git a/test/lexer_test.rb b/test/lexer_test.rb index 6a33fd30..b212f4e6 100644 --- a/test/lexer_test.rb +++ b/test/lexer_test.rb @@ -8,19 +8,19 @@ context "Lexer" do end test "test_is_title_section" do - section = Asciidoctor::Section.new(Asciidoctor::Document.new([])) + section = Asciidoctor::Section.new(Asciidoctor::Document.new) section.level = 0 assert Asciidoctor::Lexer.is_title_section?(section, section.document) end test "test_is_not_title_section" do - section = Asciidoctor::Section.new(Asciidoctor::Document.new([])) + section = Asciidoctor::Section.new(Asciidoctor::Document.new) section.level = 1 assert !Asciidoctor::Lexer.is_title_section?(section, section.document) section.level = 0 - another_section = Asciidoctor::Section.new(Asciidoctor::Document.new([])) - another_section.level = 0 - section.document.elements << section + another_section = Asciidoctor::Section.new(Asciidoctor::Document.new) + another_section.level = 0 + section.document << section assert !Asciidoctor::Lexer.is_title_section?(another_section, section.document) end diff --git a/test/lists_test.rb b/test/lists_test.rb index fd88899e..5128ebfd 100644 --- a/test/lists_test.rb +++ b/test/lists_test.rb @@ -16,7 +16,7 @@ List assert_xpath '//ul/li', output, 3 end - test "dash elements with blank lines should merge lists" do + test "dash elements separated by blank lines should merge lists" do input = <<-EOS List ==== @@ -25,6 +25,7 @@ List - Boo + - Blech EOS output = render_string input @@ -148,7 +149,7 @@ List assert_xpath '//ul/li', output, 3 end - test "asterisk elements with blank lines should merge lists" do + test "asterisk elements separated by blank lines should merge lists" do input = <<-EOS List ==== @@ -157,6 +158,7 @@ List * Boo + * Blech EOS output = render_string input @@ -340,6 +342,7 @@ List * Boo + - Blech EOS output = render_string input @@ -496,6 +499,7 @@ List . Boo + * Blech EOS output = render_string input @@ -559,8 +563,8 @@ Item one, paragraph two assert_xpath '//ul/li', output, 2 assert_xpath '//ul/li[1]/p', output, 1 assert_xpath '//ul/li[1]//p', output, 2 - assert_xpath '//ul/li[1]//p[text() = "Item one, paragraph one"]', output, 1 - assert_xpath '//ul/li[1]//p[text() = "Item one, paragraph two"]', output, 1 + assert_xpath '//ul/li[1]/p[text() = "Item one, paragraph one"]', output, 1 + assert_xpath '//ul/li[1]/*[@class = "paragraph"]/p[text() = "Item one, paragraph two"]', output, 1 end test "adjacent list continuation line attaches following block" do @@ -610,7 +614,7 @@ ____ # NOTE this differs from AsciiDoc behavior, but is more logical test "consecutive list continuation lines are folded" do - return pending "rework test to support more compliant behavior" + return pending "Rework test to support more compliant behavior" input = <<-EOS Lists ===== @@ -652,7 +656,7 @@ List assert_xpath '//ol/li', output, 3 end - test "dot elements with blank lines should merge lists" do + test "dot elements separated by blank lines should merge lists" do input = <<-EOS List ==== @@ -661,6 +665,7 @@ List . Boo + . Blech EOS output = render_string input @@ -668,7 +673,7 @@ List assert_xpath '//ol/li', output, 3 end - test "dot elements with blank lines separated by line comment should not merge lists" do + test "dot elements separated by line comment offset by blank lines should not merge lists" do input = <<-EOS List ==== @@ -972,6 +977,35 @@ anotherterm:: def assert_xpath '(//dl/dd)[1]//*[@class="openblock"]//p', output, 2 end + test "paragraph attached by a list continuation in a labeled list" do + input = <<-EOS +term1:: def ++ +more detail ++ +term2:: def + EOS + output = render_string input + assert_xpath '(//dl/dd)[1]//p', output, 2 + assert_xpath '(//dl/dd)[1]/p/following-sibling::*[@class="paragraph"]/p[text() = "more detail"]', output, 1 + end + + # FIXME! + test "paragraph attached by a list continuation to a multi-line element in a labeled list" do + input = <<-EOS +term1:: +def ++ +more detail ++ +term2:: def + EOS + output = render_string input + pending "We're assuming the list continuation would be the first line after the term" + #assert_xpath '(//dl/dd)[1]//p', output, 2 + #assert_xpath '(//dl/dd)[1]/p/following-sibling::*[@class="paragraph"]/p[text() = "more detail"]', output, 1 + end + test "verse paragraph inside a labeled list" do input = <<-EOS term1:: def diff --git a/test/paragraphs_test.rb b/test/paragraphs_test.rb index 840751af..e0047abc 100644 --- a/test/paragraphs_test.rb +++ b/test/paragraphs_test.rb @@ -1,20 +1,22 @@ require 'test_helper' context "Paragraphs" do - test "rendered correctly" do - assert_xpath "//p", render_string("Plain text for the win.\n\nYes, plainly."), 2 - end + context 'Normal' do + test "rendered correctly" do + assert_xpath "//p", render_string("Plain text for the win.\n\nYes, plainly."), 2 + end - test "with title" do - rendered = render_string(".Titled\nParagraph.\n\nWinning") - - assert_xpath "//div[@class='title']", rendered - assert_xpath "//p", rendered, 2 - end + test "with title" do + rendered = render_string(".Titled\nParagraph.\n\nWinning") + + assert_xpath "//div[@class='title']", rendered + assert_xpath "//p", rendered, 2 + end - test "no duplicate block before next section" do - rendered = render_string("Title\n=====\n\nPreamble.\n\n== First Section\n\nParagraph 1\n\nParagraph 2\n\n\n== Second Section\n\nLast words") - assert_xpath '//p[text()="Paragraph 2"]', rendered, 1 + test "no duplicate block before next section" do + rendered = render_string("Title\n=====\n\nPreamble.\n\n== First Section\n\nParagraph 1\n\nParagraph 2\n\n\n== Second Section\n\nLast words") + assert_xpath '//p[text()="Paragraph 2"]', rendered, 1 + end end context "code" do diff --git a/test/preamble_test.rb b/test/preamble_test.rb index 5ae997c8..45d43032 100644 --- a/test/preamble_test.rb +++ b/test/preamble_test.rb @@ -106,7 +106,7 @@ They couldn't believe their eyes when... assert_equal 'book', d.doctype output = d.render assert_xpath '//h1', output, 3 - assert_xpath '//*[@id="preamble"]//p[text() = "Back then..."]', output, 1 + assert_xpath %{//*[@id="preamble"]//p[text() = "Back then#{[8230].pack('U*')}"]}, output, 1 end end diff --git a/test/reader_test.rb b/test/reader_test.rb index 5c846bb9..f9fda29c 100644 --- a/test/reader_test.rb +++ b/test/reader_test.rb @@ -9,7 +9,7 @@ class ReaderTest < Test::Unit::TestCase context "has_lines?" do test "returns false for empty document" do - assert ! Asciidoctor::Reader.new.has_lines? + assert !Asciidoctor::Reader.new.has_lines? end test "returns true with lines remaining" do @@ -44,6 +44,114 @@ class ReaderTest < Test::Unit::TestCase end end + context "Grab lines" do + test "Grab until end" do + input = <<-EOS +This is one paragraph. + +This is another paragraph. + EOS + + lines = input.lines.entries + reader = Asciidoctor::Reader.new(lines) + result = reader.grab_lines_until + assert_equal 3, result.size + assert_equal lines, result + assert !reader.has_lines? + assert reader.empty? + end + + test "Grab until blank line" do + input = <<-EOS +This is one paragraph. + +This is another paragraph. + EOS + + lines = input.lines.entries + reader = Asciidoctor::Reader.new(lines) + result = reader.grab_lines_until :break_on_blank_lines => true + assert_equal 1, result.size + assert_equal lines.first, result.first + assert_equal lines.last, reader.peek_line + end + + test "Grab until blank line preserving last line" do + input = <<-EOS +This is one paragraph. + +This is another paragraph. + EOS + + lines = input.lines.entries + reader = Asciidoctor::Reader.new(lines) + result = reader.grab_lines_until :break_on_blank_lines => true, :preserve_last_line => true + assert_equal 1, result.size + assert_equal lines.first, result.first + assert_equal "\n", reader.peek_line + end + + test "Grab until condition" do + input = <<-EOS +-- +This is one paragraph inside the block. + +This is another paragraph inside the block. +-- + +This is a paragraph outside the block. + EOS + + lines = input.lines.entries + reader = Asciidoctor::Reader.new(lines) + reader.get_line + result = reader.grab_lines_until {|line| line.chomp == '--' } + assert_equal 3, result.size + assert_equal lines[1, 3], result + assert_equal "\n", reader.peek_line + end + + test "Grab until condition with last line" do + input = <<-EOS +-- +This is one paragraph inside the block. + +This is another paragraph inside the block. +-- + +This is a paragraph outside the block. + EOS + + lines = input.lines.entries + reader = Asciidoctor::Reader.new(lines) + reader.get_line + result = reader.grab_lines_until(:grab_last_line => true) {|line| line.chomp == '--' } + assert_equal 4, result.size + assert_equal lines[1, 4], result + assert_equal "\n", reader.peek_line + end + + test "Grab until condition with last line and preserving last line" do + input = <<-EOS +-- +This is one paragraph inside the block. + +This is another paragraph inside the block. +-- + +This is a paragraph outside the block. + EOS + + lines = input.lines.entries + reader = Asciidoctor::Reader.new(lines) + reader.get_line + result = reader.grab_lines_until(:grab_last_line => true, :preserve_last_line => true) {|line| line.chomp == '--' } + assert_equal 4, result.size + assert_equal lines[1, 4], result + assert_equal "--\n", reader.peek_line + end + end + context "Include files" do test "block is called to handle an include macro" do input = <<-EOS @@ -53,22 +161,46 @@ include::include-file.asciidoc[] last line EOS - attributes = {} - reader = Asciidoctor::Reader.new(input.lines.entries, attributes) {|inc| + doc = Asciidoctor::Document.new + reader = Asciidoctor::Reader.new(input.lines.entries, doc) {|inc| ":file: #{inc}\n\nmiddle line".lines.entries } - expected = {'file' => 'include-file.asciidoc'} - assert_equal expected, attributes + assert_equal 'include-file.asciidoc', doc.attributes['file'] end end - def test_grab_lines_until - pending "Not tested yet" + # TODO these tests could be expanded + context 'Conditional blocks' do + test 'ifdef with defined attribute includes block' do + input = <<-EOS +:holygrail: + +ifdef::holygrail[] +There is a holy grail! +endif::holygrail[] + EOS + + reader = Asciidoctor::Reader.new(input.lines.entries, Asciidoctor::Document.new) + assert_match /There is a holy grail!/, reader.lines.join + end + + test 'ifndef with undefined attribute includes block' do + input = <<-EOS +ifndef::holygrail[] +Our quest continues to find the holy grail! +endif::holygrail[] + EOS + + reader = Asciidoctor::Reader.new(input.lines.entries, Asciidoctor::Document.new) + assert_match /Our quest continues to find the holy grail!/, reader.lines.join + end end - def test_sanitize_attribute_name - assert_equal 'foobar', @reader.sanitize_attribute_name("Foo Bar") - assert_equal 'foo', @reader.sanitize_attribute_name("foo") - assert_equal 'foo3-bar', @reader.sanitize_attribute_name("Foo 3^ # - Bar[") + context 'Text processing' do + test 'sanitize attribute name' do + assert_equal 'foobar', @reader.sanitize_attribute_name("Foo Bar") + assert_equal 'foo', @reader.sanitize_attribute_name("foo") + assert_equal 'foo3-bar', @reader.sanitize_attribute_name("Foo 3^ # - Bar[") + end end end diff --git a/test/string_test.rb b/test/string_test.rb new file mode 100644 index 00000000..055e82d3 --- /dev/null +++ b/test/string_test.rb @@ -0,0 +1,63 @@ +require 'test_helper' + +context 'String' do + + test 'underscore should turn module into path and class name into words delimited by an underscore' do + assert_equal 'asciidoctor/abstract_block', Asciidoctor::AbstractBlock.to_s.underscore + end + + test 'underscore should convert hypens to underscores' do + assert_equal 'one_on_one', 'one-on-one'.underscore + end + + test 'underscore should convert camelcase word into words delimited by an underscore' do + assert_equal 'big_voodoo_daddy', 'BigVoodooDaddy'.underscore + end + + test 'ltrim should trim sequence of char from left of string' do + assert_equal 'abc', '_abc'.ltrim('_') + assert_equal 'abc', '___abc'.ltrim('_') + assert_equal 'abc', 'abc'.ltrim('_') + end + + test 'ltrim should not trim sequence of char from middle of string' do + assert_equal 'a_b_c', 'a_b_c'.ltrim('_') + assert_equal 'a___c', 'a___c'.ltrim('_') + assert_equal 'a___c', '_a___c'.ltrim('_') + end + + test 'rtrim should trim sequence of char from right of string' do + assert_equal 'abc', 'abc_'.rtrim('_') + assert_equal 'abc', 'abc___'.rtrim('_') + assert_equal 'abc', 'abc'.rtrim('_') + end + + test 'rtrim should not trim sequence of char from middle of string' do + assert_equal 'a_b_c', 'a_b_c'.rtrim('_') + assert_equal 'a___c', 'a___c'.rtrim('_') + assert_equal 'a___c', 'a___c_'.rtrim('_') + end + + test 'trim should trim sequence of char from boundaries of string' do + assert_equal 'abc', '_abc_'.trim('_') + assert_equal 'abc', '___abc___'.trim('_') + assert_equal 'abc', '___abc_'.trim('_') + assert_equal 'abc', '_abc___'.trim('_') + end + + test 'trim should not trim sequence of char from middle of string' do + assert_equal 'a_b_c', 'a_b_c'.trim('_') + assert_equal 'a___c', 'a___c'.trim('_') + assert_equal 'a___c', '_a___c_'.trim('_') + end + + test 'nuke should remove first occurrence of matched pattern' do + assert_equal 'ab_c', 'a_b_c'.nuke(/_/) + end + + test 'gnuke should remove all occurrences of matched pattern' do + assert_equal 'abc', 'a_b_c'.gnuke(/_/) + assert_equal '-foo-bar', '#-?foo #-?bar'.gnuke(/[^\w-]/) + end + +end diff --git a/test/substitutions_test.rb b/test/substitutions_test.rb index ad424d57..a8e4a200 100644 --- a/test/substitutions_test.rb +++ b/test/substitutions_test.rb @@ -9,7 +9,7 @@ context 'Substitutions' do para = block_from_string("[blue]'http://asciidoc.org[AsciiDoc]' & [red]*Ruby*\n§ Making +++<u>documentation</u>+++ together +\nsince (C) {inception_year}.") para.document.attributes['inception_year'] = '2012' result = para.apply_normal_subs(para.buffer) - assert_equal %{<em><span class="blue"><a href='http://asciidoc.org'>AsciiDoc</a></span></em> & <strong><span class="red">Ruby</span></strong>\n§ Making <u>documentation</u> together<br>\nsince © 2012.}, result + assert_equal %{<em><span class="blue"><a href="http://asciidoc.org">AsciiDoc</a></span></em> & <strong><span class="red">Ruby</span></strong>\n§ Making <u>documentation</u> together<br>\nsince © 2012.}, result end end @@ -106,7 +106,8 @@ context 'Substitutions' do test 'escaped single-quotes inside emphasized words are restored' do para = block_from_string(%q{'Here\'s Johnny!'}) - assert_equal %q{<em>Here's Johnny!</em>}, para.sub_quotes(para.buffer.join) + # NOTE the \' is replaced with ' by the :replacements substitution, later in the substitution pipeline + assert_equal %q{<em>Here\'s Johnny!</em>}, para.sub_quotes(para.buffer.join) end test 'single-line constrained emphasized underline variation string' do @@ -200,33 +201,33 @@ context 'Substitutions' do context 'Macros' do test 'a single-line link macro should be interpreted as a link' do para = block_from_string('link:/home.html[]') - assert_equal %q{<a href='/home.html'>/home.html</a>}, para.sub_macros(para.buffer.join) + assert_equal %q{<a href="/home.html">/home.html</a>}, para.sub_macros(para.buffer.join) end test 'a single-line link macro with text should be interpreted as a link' do para = block_from_string('link:/home.html[Home]') - assert_equal %q{<a href='/home.html'>Home</a>}, para.sub_macros(para.buffer.join) + assert_equal %q{<a href="/home.html">Home</a>}, para.sub_macros(para.buffer.join) end test 'a single-line raw url should be interpreted as a link' do para = block_from_string('http://google.com') - assert_equal %q{<a href='http://google.com'>http://google.com</a>}, para.sub_macros(para.buffer.join) + assert_equal %q{<a href="http://google.com">http://google.com</a>}, para.sub_macros(para.buffer.join) end test 'a single-line raw url with text should be interpreted as a link' do para = block_from_string('http://google.com[Google]') - assert_equal %q{<a href='http://google.com'>Google</a>}, para.sub_macros(para.buffer.join) + assert_equal %q{<a href="http://google.com">Google</a>}, para.sub_macros(para.buffer.join) end test 'a multi-line raw url with text should be interpreted as a link' do para = block_from_string("http://google.com[Google\nHomepage]") - assert_equal %{<a href='http://google.com'>Google\nHomepage</a>}, para.sub_macros(para.buffer.join) + assert_equal %{<a href="http://google.com">Google\nHomepage</a>}, para.sub_macros(para.buffer.join) end test 'a multi-line raw url with attribute as text should be interpreted as a link with resolved attribute' do para = block_from_string("http://google.com[{google_homepage}]") para.document.attributes['google_homepage'] = 'Google Homepage' - assert_equal %q{<a href='http://google.com'>Google Homepage</a>}, para.sub_macros(para.buffer.join) + assert_equal %q{<a href="http://google.com">Google Homepage</a>}, para.sub_macros(para.buffer.join) end test 'a single-line escaped raw url should not be interpreted as a link' do @@ -236,22 +237,22 @@ context 'Substitutions' do test 'a single-line image macro should be interpreted as an image' do para = block_from_string('image:tiger.png[]') - assert_equal %{<span class='image'>\n <img src='tiger.png' alt='tiger'>\n</span>}, para.sub_macros(para.buffer.join) + assert_equal %{<span class="image">\n <img src="tiger.png" alt="tiger">\n</span>}, para.sub_macros(para.buffer.join) end test 'a single-line image macro with text should be interpreted as an image with alt text' do para = block_from_string('image:tiger.png[Tiger]') - assert_equal %{<span class='image'>\n <img src='tiger.png' alt='Tiger'>\n</span>}, para.sub_macros(para.buffer.join) + assert_equal %{<span class="image">\n <img src="tiger.png" alt="Tiger">\n</span>}, para.sub_macros(para.buffer.join) end test 'a single-line image macro with text and dimensions should be interpreted as an image with alt text and dimensions' do para = block_from_string('image:tiger.png[Tiger, 200, 100]') - assert_equal %{<span class='image'>\n <img src='tiger.png' alt='Tiger' width='200' height='100'>\n</span>}, para.sub_macros(para.buffer.join) + assert_equal %{<span class="image">\n <img src="tiger.png" alt="Tiger" width="200" height="100">\n</span>}, para.sub_macros(para.buffer.join) end test 'a single-line image macro with text and link should be interpreted as a linked image with alt text' do para = block_from_string('image:tiger.png[Tiger, link="http://en.wikipedia.org/wiki/Tiger"]') - assert_equal %{<span class='image'>\n <a class='image' href='http://en.wikipedia.org/wiki/Tiger'><img src='tiger.png' alt='Tiger'></a>\n</span>}, para.sub_macros(para.buffer.join) + assert_equal %{<span class="image">\n <a class="image" href="http://en.wikipedia.org/wiki/Tiger"><img src="tiger.png" alt="Tiger"></a>\n</span>}, para.sub_macros(para.buffer.join) end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 9ab2d497..bb72d32a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -62,14 +62,21 @@ class Test::Unit::TestCase end end - def assert_xpath(xpath, html, count = nil) - doc = (html =~ /\s*<!DOCTYPE/) ? Nokogiri::HTML::Document.parse(html) : Nokogiri::HTML::DocumentFragment.parse(html) + def assert_xpath(xpath, content, count = nil) + match = content.match(/\s*<!DOCTYPE (.*)/) + if !match + doc = Nokogiri::HTML::DocumentFragment.parse(content) + elsif match[1].start_with? 'html' + doc = Nokogiri::HTML::Document.parse(content) + else + doc = Nokogiri::XML::Document.parse(content) + end results = doc.xpath("#{xpath.sub('/', './')}") if (count && results.length != count) - flunk "XPath #{xpath} yielded #{results.length} elements rather than #{count} for:\n#{html}" + flunk "XPath #{xpath} yielded #{results.length} elements rather than #{count} for:\n#{content}" elsif (count.nil? && results.empty?) - flunk "XPath #{xpath} not found in:\n#{html}" + flunk "XPath #{xpath} not found in:\n#{content}" else assert true end @@ -82,7 +89,7 @@ class Test::Unit::TestCase def block_from_string(src, opts = {}) opts[:header_footer] = false doc = Asciidoctor::Document.new(src.lines.entries, opts) - doc.elements.first + doc.blocks.first end def render_string(src, opts = {}) diff --git a/test/text_test.rb b/test/text_test.rb index 24234092..f32fc21a 100644 --- a/test/text_test.rb +++ b/test/text_test.rb @@ -9,8 +9,20 @@ context "Text" do assert_xpath "//p", example_document(:encoding).render(:header_footer => false), 1 end + # NOTE this test ensures we have the encoding line on block templates too + test "proper encoding to handle utf8 characters in arbitrary block" do + input = [] + input << "[verse]\n" + input.concat(File.readlines(sample_doc_path(:encoding))) + doc = Asciidoctor::Document.new + reader = Asciidoctor::Reader.new input + block = Asciidoctor::Lexer.next_block(reader, doc) + assert_xpath '//pre', block.render.gnuke(/^\s*\n/), 1 + end + test 'escaped text markup' do - pending "Not done yet" + assert_match /All your <em>inline<\/em> markup belongs to <strong>us<\/strong>!/, + render_string('All your <em>inline</em> markup belongs to <strong>us</strong>!') end test "line breaks" do @@ -32,11 +44,15 @@ context "Text" do assert_xpath "//em", render_string("An 'emphatic' no") end + test "emphasized text with single quote" do + assert_xpath "//em[text()=\"Johnny#{[8217].pack('U*')}s\"]", render_string("It's 'Johnny's' phone") + end + test "emphasized text with escaped single quote" do assert_xpath "//em[text()=\"Johnny's\"]", render_string("It's 'Johnny\\'s' phone") end - test "escaped single quote is restore as single quote" do + test "escaped single quote is restored as single quote" do assert_xpath "//p[contains(text(), \"Let's do it!\")]", render_string("Let\\'s do it!") end |
