summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Allen <dan.j.allen@gmail.com>2013-01-03 05:43:43 -0700
committerDan Allen <dan.j.allen@gmail.com>2013-01-03 05:43:43 -0700
commitfe13ace9ad2bef848a7105f86290d3e4b09a5be2 (patch)
tree8871027042c1322e54c61e8863e19ddc6023a6a4
parentc8251210e324053a26c4884d8d86341c12dc8f88 (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
-rw-r--r--asciidoctor.gemspec1
-rwxr-xr-xlib/asciidoctor.rb84
-rw-r--r--lib/asciidoctor/abstract_block.rb17
-rw-r--r--lib/asciidoctor/abstract_node.rb19
-rw-r--r--lib/asciidoctor/attribute_list.rb21
-rw-r--r--lib/asciidoctor/backends/base_template.rb54
-rw-r--r--lib/asciidoctor/backends/docbook45.rb420
-rw-r--r--lib/asciidoctor/backends/html5.rb447
-rw-r--r--lib/asciidoctor/block.rb8
-rw-r--r--lib/asciidoctor/document.rb122
-rw-r--r--lib/asciidoctor/lexer.rb118
-rw-r--r--lib/asciidoctor/list_item.rb22
-rw-r--r--lib/asciidoctor/reader.rb225
-rw-r--r--lib/asciidoctor/render_templates.rb469
-rw-r--r--lib/asciidoctor/renderer.rb23
-rw-r--r--lib/asciidoctor/section.rb7
-rw-r--r--lib/asciidoctor/string.rb112
-rw-r--r--lib/asciidoctor/substituters.rb69
-rw-r--r--test/attributes_test.rb349
-rw-r--r--test/blocks_test.rb2
-rw-r--r--test/document_test.rb215
-rw-r--r--test/headers_test.rb2
-rw-r--r--test/lexer_test.rb10
-rw-r--r--test/lists_test.rb48
-rw-r--r--test/paragraphs_test.rb26
-rw-r--r--test/preamble_test.rb2
-rw-r--r--test/reader_test.rb154
-rw-r--r--test/string_test.rb63
-rw-r--r--test/substitutions_test.rb25
-rw-r--r--test/test_helper.rb17
-rw-r--r--test/text_test.rb20
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 => /\\?(?:&lt;&lt;([\w":].*?)&gt;&gt;|xref:([\w":].*?)\[(.*?)\])/m
+ :xref_macro => /\\?(?:&lt;&lt;([\w":].*?)&gt;&gt;|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' => '&#160;',
'deg' => '&#176;',
'zwsp' => '&#8203;',
+ 'quot' => '&#34;',
+ 'apos' => '&#39;',
'lsquo' => '&#8216;',
'rsquo' => '&#8217;',
'ldquo' => '&#8220;',
@@ -279,7 +308,7 @@ module Asciidoctor
'wj' => '&#8288;',
'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&#169;',
+ [/(^|[^\\])\(C\)/, '\1&#169;'],
# (R)
- /(^|[^\\])\(R\)/ => '\1&#174;',
+ [/(^|[^\\])\(R\)/, '\1&#174;'],
# (TM)
- /(^|[^\\])\(TM\)/ => '\1&#8482;',
+ [/(^|[^\\])\(TM\)/, '\1&#8482;'],
# foo--bar
- /(\w)--(?=\w)/ => '\1&#8212;',
+ [/(\w)--(?=\w)/, '\1&#8212;'],
+ # ellipsis
+ [/(^|[^\\])\.\.\./, '\1&#8230;'],
+ # single quotes
+ [/(\w)'(\w)/, '\1&#8217;\2'],
+ # escaped single quotes
+ [/(\w)\\'(\w)/, '\1\'\2'],
# and so on...
# restore entities; TODO needs cleanup
- /&amp;(#[a-z0-9]+;)/i => '&\1'
- }
+ [/&amp;(#[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 = '&#10;' # or &#x0A;
+
+ 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">&lt;<%= attr :email %>&gt;</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 %>
+ <%= '&#8212; ' + 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 %>
+ <%= '&#8212; ' + 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 &lt;test&gt;"]
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 = '&#10;' # or &#x0A;
-
- 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'>&lt;<%= attr :email %>&gt;</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 %>
- <%= '&#8212; ' + 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 %>
- <%= '&#8212; ' + 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 '&lt;&gt;&amp;', 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 '&lt;&gt;&amp;', 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&#160;together/, html
+ end
+
+ test "escape special characters" do
+ html = render_string('<node>&</node>')
+ assert_match /&lt;node&gt;&amp;&lt;\/node&gt;/, 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&#160;together/, html
- end
-
- test "escape special characters" do
- html = render_string('<node>&</node>')
- assert_match /&lt;node&gt;&amp;&lt;\/node&gt;/, 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&#167; 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> &amp; <strong><span class="red">Ruby</span></strong>\n&#167; Making <u>documentation</u> together<br>\nsince &#169; 2012.}, result
+ assert_equal %{<em><span class="blue"><a href="http://asciidoc.org">AsciiDoc</a></span></em> &amp; <strong><span class="red">Ruby</span></strong>\n&#167; Making <u>documentation</u> together<br>\nsince &#169; 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 &lt;em&gt;inline&lt;\/em&gt; markup belongs to &lt;strong&gt;us&lt;\/strong&gt;!/,
+ 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