summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Allen <dan.j.allen@gmail.com>2014-02-20 04:29:02 -0700
committerDan Allen <dan.j.allen@gmail.com>2014-02-26 14:04:41 -0700
commitd98c8e9cd5e5793fdab6da7a0a6f37eb8349878b (patch)
tree4a5993b8e23f787017e1abcac51dce3ded60d4d9
parent0c4a21b4ade1df7767986c9bce84a7c3fdbee3c9 (diff)
resolves #778, rewrite converter API; resolves #638, integrate thread_safe gem
- rewrite converter API - separate built-in converters from template converter - rename renderer/render to converter/convert - make converter an extension point (resolves #778) - base built-in converters on the converter API - rename template_name property to node_name on AbstractNode - make block_ prefix on file name of block-level templates optional - use thread_safe gem for template and converter caches (resolves #638) - introduce Stylesheets API to manage stylesheets - move file write logic to Document - delegate file write logic to converter that implements Writer - remove compact logic, deprecate related options - duplicate options and attributes passed to APIs, add tests - assign doctype / backend attributes correctly when document is loaded, add tests - report proper error if nil is passed to load_file and convert_file - use span tag to group kbd combination in html5 backend - setup toc in preamble if toc attribute is preamble - Opal compatibility fixes, use built-in HTML5 converter - make the outline method accessible to all html converters - document the converter APIs along with some minor cleanups in terminology - load stylesheets from data directory - rename ruler block to thematic_break - add inline? and block? query methods to AbstractNode - use Timings class to measure and report timings from processor steps - fix cucumber tests - upgrade tilt dependency to 2.0.0 - minor optimizations
-rw-r--r--asciidoctor.gemspec3
-rw-r--r--data/stylesheets/asciidoctor-default.css (renamed from lib/asciidoctor/backends/_stylesheets.rb)116
-rw-r--r--data/stylesheets/coderay-asciidoctor.css86
-rw-r--r--features/open_block.feature20
-rw-r--r--features/pass_block.feature12
-rw-r--r--features/step_definitions.rb12
-rw-r--r--features/xref.feature4
-rw-r--r--lib/asciidoctor.rb295
-rw-r--r--lib/asciidoctor/abstract_block.rb31
-rw-r--r--lib/asciidoctor/abstract_node.rb39
-rw-r--r--lib/asciidoctor/backends/base_template.rb102
-rw-r--r--lib/asciidoctor/backends/docbook45.rb95
-rw-r--r--lib/asciidoctor/backends/docbook5.rb898
-rw-r--r--lib/asciidoctor/backends/html5-erb.rb37
-rw-r--r--lib/asciidoctor/backends/html5.rb1311
-rw-r--r--lib/asciidoctor/block.rb31
-rw-r--r--lib/asciidoctor/callouts.rb6
-rw-r--r--lib/asciidoctor/cli/invoker.rb21
-rw-r--r--lib/asciidoctor/cli/options.rb14
-rw-r--r--lib/asciidoctor/converter.rb151
-rw-r--r--lib/asciidoctor/converter/base.rb56
-rw-r--r--lib/asciidoctor/converter/composite.rb62
-rw-r--r--lib/asciidoctor/converter/docbook45.rb63
-rw-r--r--lib/asciidoctor/converter/docbook5.rb665
-rw-r--r--lib/asciidoctor/converter/factory.rb203
-rw-r--r--lib/asciidoctor/converter/html5.rb1054
-rw-r--r--lib/asciidoctor/converter/template.rb286
-rw-r--r--lib/asciidoctor/document.rb283
-rw-r--r--lib/asciidoctor/extensions.rb16
-rw-r--r--lib/asciidoctor/inline.rb19
-rw-r--r--lib/asciidoctor/list.rb5
-rw-r--r--lib/asciidoctor/parser.rb2
-rw-r--r--lib/asciidoctor/renderer.rb273
-rw-r--r--lib/asciidoctor/section.rb1
-rw-r--r--lib/asciidoctor/stylesheets.rb91
-rw-r--r--lib/asciidoctor/substitutors.rb80
-rw-r--r--lib/asciidoctor/table.rb8
-rw-r--r--lib/asciidoctor/timings.rb48
-rw-r--r--man/asciidoctor.adoc8
-rwxr-xr-xrun-tests.sh4
-rw-r--r--test/attributes_test.rb92
-rw-r--r--test/blocks_test.rb49
-rw-r--r--test/converter_test.rb304
-rw-r--r--test/document_test.rb96
-rw-r--r--test/invoker_test.rb25
-rw-r--r--test/renderer_test.rb166
-rw-r--r--test/sections_test.rb21
-rw-r--r--test/substitutions_test.rb14
-rw-r--r--test/tables_test.rb2
49 files changed, 3778 insertions, 3502 deletions
diff --git a/asciidoctor.gemspec b/asciidoctor.gemspec
index 5bf18fd3..06a13715 100644
--- a/asciidoctor.gemspec
+++ b/asciidoctor.gemspec
@@ -42,7 +42,8 @@ EOS
s.add_development_dependency 'rake', '~> 10.0.0'
s.add_development_dependency 'rspec-expectations', '~> 2.14.0'
s.add_development_dependency 'slim', '~> 2.0.0'
- s.add_development_dependency 'tilt', '~> 1.4.1'
+ s.add_development_dependency 'thread_safe', '~> 0.1.3'
+ s.add_development_dependency 'tilt', '~> 2.0.0'
s.add_development_dependency 'yard', '~> 0.8.7'
s.add_development_dependency 'yard-tomdoc', '~> 0.7.0'
if RUBY_VERSION == '2.1.0' && RUBY_ENGINE == 'rbx'
diff --git a/lib/asciidoctor/backends/_stylesheets.rb b/data/stylesheets/asciidoctor-default.css
index e61b3841..e927bef7 100644
--- a/lib/asciidoctor/backends/_stylesheets.rb
+++ b/data/stylesheets/asciidoctor-default.css
@@ -1,115 +1,3 @@
-module Asciidoctor
-module HTML5
- # Internal: Generate the default stylesheet for CodeRay
- #
- # returns the default CodeRay stylesheet as a String
- def self.default_coderay_stylesheet
- # use the following two lines to load a built-in theme instead
- #::Asciidoctor::Helpers.require_library 'coderay'
- #::CodeRay::Encoders[:html]::CSS.new(:default).stylesheet
- <<'DEFAULT_CODERAY_STYLESHEET'.chomp
-/* Foundation stylesheet for CodeRay (to match GitHub theme) | MIT License | http://foundation.zurb.com */
-table.CodeRay { border-collapse: collapse; padding: 2px; margin-bottom: 0; border: 0; background: transparent; }
-table.CodeRay td { padding: 0 .5em; vertical-align: top; }
-table.CodeRay td.line-numbers { text-align: right; color: #999; border-right: 1px solid #e5e5e5; padding-left: 0; }
-span.line-numbers { border-right: 1px solid #E5E5E5; color: #999; display: inline-block; margin-right: 0.5em; padding-right: 0.5em; }
-.CodeRay td.line-numbers strong, .CodeRay span.line-numbers strong { font-weight: normal; }
-.CodeRay .debug { color: white !important; background: blue !important; }
-.CodeRay .annotation { color: #007; }
-.CodeRay .attribute-name { color: #f08; }
-.CodeRay .attribute-value { color: #700; }
-.CodeRay .binary { color: #509; }
-.CodeRay .comment { color: #999; font-style: italic; }
-.CodeRay .char { color: #04D; }
-.CodeRay .char .content { color: #04D; }
-.CodeRay .char .delimiter { color: #039; }
-.CodeRay .class { color: #458; }
-.CodeRay .complex { color: #A08; }
-.CodeRay .constant { color: teal; }
-.CodeRay .color { color: #0A0; }
-.CodeRay .class-variable { color: #369; }
-.CodeRay .decorator { color: #B0B; }
-.CodeRay .definition { color: #099; }
-.CodeRay .directive { color: #088; }
-.CodeRay .delimiter { color: black; }
-.CodeRay .doc { color: #970; }
-.CodeRay .doctype { color: #34b; }
-.CodeRay .doc-string { color: #D42; }
-.CodeRay .escape { color: #666; }
-.CodeRay .entity { color: #800; }
-.CodeRay .error { color: #808; }
-.CodeRay .exception { color: #C00; }
-.CodeRay .filename { color: #099; }
-.CodeRay .function { color: #900; }
-.CodeRay .global-variable { color: teal; }
-.CodeRay .hex { color: #058; }
-.CodeRay .integer { color: #099; }
-.CodeRay .include { color: #B44; }
-.CodeRay .inline { color: black; }
-.CodeRay .inline .inline { background: #ccc; }
-.CodeRay .inline .inline .inline { background: #bbb; }
-.CodeRay .inline .inline-delimiter { color: #D14; }
-.CodeRay .inline-delimiter { color: #D14; }
-.CodeRay .important { color: #f00; }
-.CodeRay .interpreted { color: #B2B; }
-.CodeRay .instance-variable { color: teal; }
-.CodeRay .label { color: #970; }
-.CodeRay .local-variable { color: #963; }
-.CodeRay .octal { color: #40E; }
-.CodeRay .predefined { color: #369; }
-.CodeRay .preprocessor { color: #579; }
-.CodeRay .pseudo-class { color: #00C; }
-.CodeRay .predefined-type { color: #074; }
-.CodeRay .reserved, .keyword { color: #000; }
-.CodeRay .key { color: #808; }
-.CodeRay .key .delimiter { color: #606; }
-.CodeRay .key .char { color: #80f; }
-.CodeRay .value { color: #088; }
-.CodeRay .regexp { background-color: #fff0ff; }
-.CodeRay .regexp .content { color: #808; }
-.CodeRay .regexp .delimiter { color: #404; }
-.CodeRay .regexp .modifier { color: #C2C; }
-.CodeRay .regexp .function { color: #404; font-weight: bold; }
-.CodeRay .string { color: #D20; }
-.CodeRay .string .string { }
-.CodeRay .string .string .string { background-color: #ffd0d0; }
-.CodeRay .string .content { color: #D14; }
-.CodeRay .string .char { color: #D14; }
-.CodeRay .string .delimiter { color: #D14; }
-.CodeRay .shell { color: #D14; }
-.CodeRay .shell .content { }
-.CodeRay .shell .delimiter { color: #D14; }
-.CodeRay .symbol { color: #990073; }
-.CodeRay .symbol .content { color: #A60; }
-.CodeRay .symbol .delimiter { color: #630; }
-.CodeRay .tag, .CodeRay .attribute-name { color: #070; }
-.CodeRay .tag-special { color: #D70; }
-.CodeRay .type { color: #339; }
-.CodeRay .variable { color: #036; }
-.CodeRay .insert { background: #afa; }
-.CodeRay .delete { background: #faa; }
-.CodeRay .change { color: #aaf; background: #007; }
-.CodeRay .head { color: #f8f; background: #505; }
-.CodeRay .insert .insert { color: #080; }
-.CodeRay .delete .delete { color: #800; }
-.CodeRay .change .change { color: #66f; }
-.CodeRay .head .head { color: #f4f; }
-DEFAULT_CODERAY_STYLESHEET
- end
-
- # Internal: Generate the default stylesheet for Pygments
- #
- # returns the default Pygments stylesheet as a String
- def self.pygments_stylesheet(style = nil)
- ::Asciidoctor::Helpers.require_library 'pygments', 'pygments.rb'
- ::Pygments.css '.listingblock pre.highlight', :classprefix => 'tok-', :style => (style || 'pastie')
- end
-
- # Internal: Generate the default stylesheet for Asciidoctor
- #
- # returns the default Asciidoctor stylesheet as a String
- def self.default_asciidoctor_stylesheet
- <<'DEFAULT_ASCIIDOCTOR_STYLESHEET'.chomp
/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */
article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; }
audio, canvas, video { display: inline-block; }
@@ -481,7 +369,3 @@ span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { cursor: default; }
.conum:not([data-value]):empty { display: none; }
#toc.toc2 { background: white; }
.literalblock > .content > pre, .listingblock > .content > pre { -webkit-border-radius: 0; border-radius: 0; }
-DEFAULT_ASCIIDOCTOR_STYLESHEET
- end
-end
-end
diff --git a/data/stylesheets/coderay-asciidoctor.css b/data/stylesheets/coderay-asciidoctor.css
new file mode 100644
index 00000000..c9457503
--- /dev/null
+++ b/data/stylesheets/coderay-asciidoctor.css
@@ -0,0 +1,86 @@
+/* Foundation stylesheet for CodeRay (to match GitHub theme) | MIT License | http://foundation.zurb.com */
+table.CodeRay { border-collapse: collapse; padding: 2px; margin-bottom: 0; border: 0; background: transparent; }
+table.CodeRay td { padding: 0 .5em; vertical-align: top; }
+table.CodeRay td.line-numbers { text-align: right; color: #999; border-right: 1px solid #e5e5e5; padding-left: 0; }
+span.line-numbers { border-right: 1px solid #E5E5E5; color: #999; display: inline-block; margin-right: 0.5em; padding-right: 0.5em; }
+.CodeRay td.line-numbers strong, .CodeRay span.line-numbers strong { font-weight: normal; }
+.CodeRay .debug { color: white !important; background: blue !important; }
+.CodeRay .annotation { color: #007; }
+.CodeRay .attribute-name { color: #f08; }
+.CodeRay .attribute-value { color: #700; }
+.CodeRay .binary { color: #509; }
+.CodeRay .comment { color: #999; font-style: italic; }
+.CodeRay .char { color: #04D; }
+.CodeRay .char .content { color: #04D; }
+.CodeRay .char .delimiter { color: #039; }
+.CodeRay .class { color: #458; }
+.CodeRay .complex { color: #A08; }
+.CodeRay .constant { color: teal; }
+.CodeRay .color { color: #0A0; }
+.CodeRay .class-variable { color: #369; }
+.CodeRay .decorator { color: #B0B; }
+.CodeRay .definition { color: #099; }
+.CodeRay .directive { color: #088; }
+.CodeRay .delimiter { color: black; }
+.CodeRay .doc { color: #970; }
+.CodeRay .doctype { color: #34b; }
+.CodeRay .doc-string { color: #D42; }
+.CodeRay .escape { color: #666; }
+.CodeRay .entity { color: #800; }
+.CodeRay .error { color: #808; }
+.CodeRay .exception { color: #C00; }
+.CodeRay .filename { color: #099; }
+.CodeRay .function { color: #900; }
+.CodeRay .global-variable { color: teal; }
+.CodeRay .hex { color: #058; }
+.CodeRay .integer { color: #099; }
+.CodeRay .include { color: #B44; }
+.CodeRay .inline { color: black; }
+.CodeRay .inline .inline { background: #ccc; }
+.CodeRay .inline .inline .inline { background: #bbb; }
+.CodeRay .inline .inline-delimiter { color: #D14; }
+.CodeRay .inline-delimiter { color: #D14; }
+.CodeRay .important { color: #f00; }
+.CodeRay .interpreted { color: #B2B; }
+.CodeRay .instance-variable { color: teal; }
+.CodeRay .label { color: #970; }
+.CodeRay .local-variable { color: #963; }
+.CodeRay .octal { color: #40E; }
+.CodeRay .predefined { color: #369; }
+.CodeRay .preprocessor { color: #579; }
+.CodeRay .pseudo-class { color: #00C; }
+.CodeRay .predefined-type { color: #074; }
+.CodeRay .reserved, .keyword { color: #000; }
+.CodeRay .key { color: #808; }
+.CodeRay .key .delimiter { color: #606; }
+.CodeRay .key .char { color: #80f; }
+.CodeRay .value { color: #088; }
+.CodeRay .regexp { background-color: #fff0ff; }
+.CodeRay .regexp .content { color: #808; }
+.CodeRay .regexp .delimiter { color: #404; }
+.CodeRay .regexp .modifier { color: #C2C; }
+.CodeRay .regexp .function { color: #404; font-weight: bold; }
+.CodeRay .string { color: #D20; }
+.CodeRay .string .string { }
+.CodeRay .string .string .string { background-color: #ffd0d0; }
+.CodeRay .string .content { color: #D14; }
+.CodeRay .string .char { color: #D14; }
+.CodeRay .string .delimiter { color: #D14; }
+.CodeRay .shell { color: #D14; }
+.CodeRay .shell .content { }
+.CodeRay .shell .delimiter { color: #D14; }
+.CodeRay .symbol { color: #990073; }
+.CodeRay .symbol .content { color: #A60; }
+.CodeRay .symbol .delimiter { color: #630; }
+.CodeRay .tag, .CodeRay .attribute-name { color: #070; }
+.CodeRay .tag-special { color: #D70; }
+.CodeRay .type { color: #339; }
+.CodeRay .variable { color: #036; }
+.CodeRay .insert { background: #afa; }
+.CodeRay .delete { background: #faa; }
+.CodeRay .change { color: #aaf; background: #007; }
+.CodeRay .head { color: #f8f; background: #505; }
+.CodeRay .insert .insert { color: #080; }
+.CodeRay .delete .delete { color: #800; }
+.CodeRay .change .change { color: #66f; }
+.CodeRay .head .head { color: #f4f; }
diff --git a/features/open_block.feature b/features/open_block.feature
index 000b29a7..f50070eb 100644
--- a/features/open_block.feature
+++ b/features/open_block.feature
@@ -12,8 +12,8 @@ Feature: Open Blocks
A paragraph in an open block.
--
"""
- When it is rendered using the html backend
- Then the output should match the HTML source
+ When it is converted to html
+ Then the result should match the HTML source
"""
<div class="openblock">
<div class="content">
@@ -32,8 +32,8 @@ Feature: Open Blocks
A paragraph in an open block.
--
"""
- When it is rendered using the docbook backend
- Then the output should match the XML source
+ When it is converted to docbook
+ Then the result should match the XML source
"""
<simpara>A paragraph in an open block.</simpara>
"""
@@ -46,8 +46,8 @@ Feature: Open Blocks
A paragraph in an open block.
--
"""
- When it is rendered using the html backend
- Then the output should match the HTML structure
+ When it is converted to html
+ Then the result should match the HTML structure
"""
.openblock
.content
@@ -63,8 +63,8 @@ Feature: Open Blocks
A paragraph in an open block.
--
"""
- When it is rendered using the docbook backend
- Then the output should match the XML structure
+ When it is converted to docbook
+ Then the result should match the XML structure
"""
simpara A paragraph in an open block.
"""
@@ -79,8 +79,8 @@ Feature: Open Blocks
* three
--
"""
- When it is rendered using the html backend
- Then the output should match the HTML structure
+ When it is converted to html
+ Then the result should match the HTML structure
"""
.openblock
.content
diff --git a/features/pass_block.feature b/features/pass_block.feature
index 24b7212d..77d4f8a8 100644
--- a/features/pass_block.feature
+++ b/features/pass_block.feature
@@ -16,8 +16,8 @@ Feature: Open Blocks
image:tiger.png[]
++++
"""
- When it is rendered using the html backend
- Then the output should match the HTML source
+ When it is converted to html
+ Then the result should match the HTML source
"""
<p>{name}</p>
@@ -36,8 +36,8 @@ Feature: Open Blocks
image:tiger.png[]
++++
"""
- When it is rendered using the docbook backend
- Then the output should match the XML source
+ When it is converted to docbook
+ Then the result should match the XML source
"""
<simpara>{name}</simpara>
@@ -57,8 +57,8 @@ Feature: Open Blocks
image:tiger.png[]
++++
"""
- When it is rendered using the html backend
- Then the output should match the HTML source
+ When it is converted to html
+ Then the result should match the HTML source
"""
<p>value</p>
diff --git a/features/step_definitions.rb b/features/step_definitions.rb
index 382d9923..0f84b42f 100644
--- a/features/step_definitions.rb
+++ b/features/step_definitions.rb
@@ -7,22 +7,22 @@ Given /the AsciiDoc source/ do |source|
@source = source
end
-When /it is rendered using the html backend/ do
- @output = Asciidoctor.render @source
+When /it is converted to html/ do
+ @output = Asciidoctor.convert @source
#File.open('/tmp/test.adoc', 'w') {|f| f.write @source }
#@output = %x{asciidoc -f compat/asciidoc.conf -o - -s /tmp/test.adoc | XMLLINT_INDENT='' xmllint --format - | tail -n +2}.rstrip
##@output = %x{asciidoc -f compat/asciidoc.conf -o - -s /tmp/test.adoc}
end
-When /it is rendered using the docbook backend/ do
- @output = Asciidoctor.render @source, :backend => :docbook
+When /it is converted to docbook/ do
+ @output = Asciidoctor.convert @source, :backend => :docbook
end
-Then /the output should match the (HTML|XML) source/ do |format, expect|
+Then /the result should match the (HTML|XML) source/ do |format, expect|
@output.should == expect
end
-Then /the output should match the (HTML|XML) structure/ do |format, expect|
+Then /the result should match the (HTML|XML) structure/ do |format, expect|
case format
when 'HTML'
options = {:format => :html5}
diff --git a/features/xref.feature b/features/xref.feature
index 720136ed..d9c29025 100644
--- a/features/xref.feature
+++ b/features/xref.feature
@@ -16,8 +16,8 @@ Feature: Cross References
Instructions go here.
"""
- When it is rendered using the html backend
- Then the output should match the HTML structure
+ When it is converted to html
+ Then the result should match the HTML structure
"""
table.tableblock.frame-all.grid-all style='width: 100%;'
colgroup
diff --git a/lib/asciidoctor.rb b/lib/asciidoctor.rb
index b0d0c815..77eaa654 100644
--- a/lib/asciidoctor.rb
+++ b/lib/asciidoctor.rb
@@ -13,12 +13,12 @@ if RUBY_ENGINE_OPAL
end
# ideally we should use require_relative instead of modifying the LOAD_PATH
-$:.unshift(File.dirname(__FILE__))
+$:.unshift File.dirname __FILE__
-# Public: Methods for parsing Asciidoc input files and rendering documents
+# Public: Methods for parsing AsciiDoc input files and converting documents
# using eRuby templates.
#
-# Asciidoc documents comprise a header followed by zero or more sections.
+# AsciiDoc documents comprise a header followed by zero or more sections.
# Sections are composed of blocks of content. For example:
#
# = Doc Title
@@ -36,23 +36,14 @@ $:.unshift(File.dirname(__FILE__))
#
# Examples:
#
-# Use built-in templates:
+# Use built-in converter:
#
-# lines = File.readlines("your_file.asc")
-# doc = Asciidoctor::Document.new(lines)
-# html = doc.render
-# File.open("your_file.html", "w+") do |file|
-# file.puts html
-# end
+# Asciidoctor.convert_file 'sample.adoc'
#
# Use custom (Tilt-supported) templates:
#
-# lines = File.readlines("your_file.asc")
-# doc = Asciidoctor::Document.new(lines, :template_dir => 'templates')
-# html = doc.render
-# File.open("your_file.html", "w+") do |file|
-# file.puts html
-# end
+# Asciidoctor.convert_file 'sample.adoc', :template_dir => 'path/to/templates'
+#
module Asciidoctor
unless ::RUBY_ENGINE_OPAL
@@ -76,7 +67,7 @@ module Asciidoctor
SAFE = 1;
# A safe mode level that disallows the document from setting attributes
- # that would affect the rendering of the document, in addition to all the
+ # that would affect the conversion of the document, in addition to all the
# security features of SafeMode::SAFE. For instance, this level disallows
# changing the backend or the source-highlighter using an attribute defined
# in the source document. This is the most fundamental level of security
@@ -170,11 +161,14 @@ module Asciidoctor
define :markdown_syntax, true
end
+ # The absolute root path of the Asciidoctor RubyGem
+ ROOT_PATH = ::File.dirname ::File.dirname ::File.expand_path __FILE__
+
# The absolute lib path of the Asciidoctor RubyGem
- LIB_PATH = ::File.expand_path(::File.dirname(__FILE__))
+ LIB_PATH = ::File.join ROOT_PATH, 'lib'
- # The absolute root path of the Asciidoctor RubyGem
- ROOT_PATH = ::File.dirname LIB_PATH
+ # The absolute data path of the Asciidoctor RubyGem
+ DATA_PATH = ::File.join ROOT_PATH, 'data'
# The user's home directory, as best we can determine it
USER_HOME = ::Dir.home rescue ::ENV['HOME'] || ::Dir.pwd
@@ -200,7 +194,7 @@ module Asciidoctor
# Flag to indicate whether gsub can use a Hash to map matches to replacements
SUPPORTS_GSUB_RESULT_HASH = ::RUBY_MIN_VERSION_1_9 && !::RUBY_ENGINE_OPAL
- # The endline character to use when rendering output
+ # The endline character used for output; stored in constant table as an optimization
EOL = "\n"
# The null character to use for splitting attribute values
@@ -213,10 +207,10 @@ module Asciidoctor
TAB_PATTERN = /\t/
# The default document type
- # Can influence markup generated by render templates
+ # Can influence markup generated by the converters
DEFAULT_DOCTYPE = 'article'
- # The backend determines the format of the rendered output, default to html5
+ # The backend determines the format of the converted output, default to html5
DEFAULT_BACKEND = 'html5'
DEFAULT_STYLESHEET_KEYS = ['', 'DEFAULT'].to_set
@@ -286,10 +280,10 @@ module Asciidoctor
DELIMITED_BLOCK_LEADERS = DELIMITED_BLOCKS.keys.map {|key| key[0..1] }.to_set
LAYOUT_BREAK_LINES = {
- '\'' => :ruler,
- '-' => :ruler,
- '*' => :ruler,
- '_' => :ruler,
+ '\'' => :thematic_break,
+ '-' => :thematic_break,
+ '*' => :thematic_break,
+ '_' => :thematic_break,
'<' => :page_break
}
@@ -316,8 +310,6 @@ module Asciidoctor
# alternatively, we can enforce everywhere it must be a space
LINE_BREAK = ' +'
- LINE_FEED_ENTITY = '&#10;' # or &#x0A;
-
BLOCK_MATH_DELIMITERS = {
:asciimath => ['\\$', '\\$'],
:latexmath => ['\\[', '\\]'],
@@ -685,8 +677,8 @@ module Asciidoctor
# <1> <2> (multiple callouts on one line)
# <!--1--> (for XML-based languages)
#
- # NOTE special characters are already be replaced at this point during render
- CalloutRenderRx = /(?:(?:\/\/|#|;;) ?)?(\\)?&lt;!?(--|)(\d+)\2&gt;(?=(?: ?\\?&lt;!?\2\d+\2&gt;)*#{CC_EOL})/
+ # NOTE special characters are already be replaced at this point during conversion to an SGML format
+ CalloutConvertRx = /(?:(?:\/\/|#|;;) ?)?(\\)?&lt;!?(--|)(\d+)\2&gt;(?=(?: ?\\?&lt;!?\2\d+\2&gt;)*#{CC_EOL})/
# NOTE (con't) ...but not while scanning
CalloutQuickScanRx = /\\?<!?(--|)(\d+)\1>(?=(?: ?\\?<!?\1\d+\1>)*#{CC_EOL})/
CalloutScanRx = /(?:(?:\/\/|#|;;) ?)?(\\)?<!?(--|)(\d+)\2>(?=(?: ?\\?<!?\2\d+\2>)*#{CC_EOL})/
@@ -1163,7 +1155,9 @@ module Asciidoctor
[/\\?(&)amp;((?:[a-zA-Z]+|#\d{2,5}|#x[a-fA-F0-9]{2,4});)/, '', :bounding]
]
- # Public: Parse the AsciiDoc source input into an Asciidoctor::Document
+ class << self
+
+ # Public: Parse the AsciiDoc source input into a {Document}
#
# Accepts input as an IO (or StringIO), String or String Array object. If the
# input is a File, information about the file is stored in attributes on the
@@ -1172,19 +1166,21 @@ module Asciidoctor
# input - the AsciiDoc source as a IO, String or Array.
# options - a String, Array or Hash of options to control processing (default: {})
# String and Array values are converted into a Hash.
- # See Asciidoctor::Document#initialize for details about options.
+ # See {Document#initialize} for details about these options.
#
- # returns the Asciidoctor::Document
- def self.load(input, options = {})
- if (monitor = options[:monitor])
- start = ::Time.now.to_f
+ # Returns the Document
+ def load input, options = {}
+ options = options.dup
+ if (timings = options[:timings])
+ timings.start :read
end
- attrs = (options[:attributes] ||= {})
- if attrs.is_a?(::Hash) || (::RUBY_ENGINE_JRUBY && attrs.is_a?(::Java::JavaUtil::Map))
- # all good; placed here as optimization
+ attributes = options[:attributes] = if !(attrs = options[:attributes])
+ {}
+ elsif (attrs.is_a? ::Hash) || (::RUBY_ENGINE_JRUBY && (attrs.is_a? ::Java::JavaUtil::Map))
+ attrs.dup
elsif attrs.is_a? ::Array
- attrs = options[:attributes] = attrs.inject({}) do |accum, entry|
+ attrs.inject({}) do |accum, entry|
k, v = entry.split '=', 2
accum[k] = v || ''
accum
@@ -1195,57 +1191,53 @@ module Asciidoctor
capture_1 = ::RUBY_ENGINE_OPAL ? '$1' : '\1'
attrs = attrs.gsub(SpaceDelimiterRx, %(#{capture_1}#{NULL})).gsub(EscapedSpaceRx, capture_1)
- attrs = options[:attributes] = attrs.split(NULL).inject({}) do |accum, entry|
+ attrs.split(NULL).inject({}) do |accum, entry|
k, v = entry.split '=', 2
accum[k] = v || ''
accum
end
- elsif attrs.respond_to?(:keys) && attrs.respond_to?(:[])
+ elsif (attrs.respond_to? :keys) && (attrs.respond_to? :[])
# convert it to a Hash as we know it
original_attrs = attrs
- attrs = options[:attributes] = {}
+ attrs = {}
original_attrs.keys.each do |key|
attrs[key] = original_attrs[key]
end
+ attrs
else
- raise ::ArgumentError, "illegal type for attributes option: #{attrs.class.ancestors}"
+ raise ::ArgumentError, %(illegal type for attributes option: #{attrs.class.ancestors})
end
lines = nil
if input.is_a? ::File
lines = input.readlines
input_mtime = input.mtime
- input_path = ::File.expand_path(input.path)
+ input_path = ::File.expand_path input.path
# hold off on setting infile and indir until we get a better sense of their purpose
- attrs['docfile'] = input_path
- attrs['docdir'] = ::File.dirname(input_path)
- attrs['docname'] = ::File.basename(input_path, ::File.extname(input_path))
- attrs['docdate'] = docdate = input_mtime.strftime('%Y-%m-%d')
- attrs['doctime'] = doctime = input_mtime.strftime('%H:%M:%S %Z')
- attrs['docdatetime'] = %(#{docdate} #{doctime})
- elsif input.respond_to?(:readlines)
- input.rewind rescue nil
+ attributes['docfile'] = input_path
+ attributes['docdir'] = ::File.dirname input_path
+ attributes['docname'] = ::File.basename input_path, (::File.extname input_path)
+ attributes['docdate'] = docdate = input_mtime.strftime('%Y-%m-%d')
+ attributes['doctime'] = doctime = input_mtime.strftime('%H:%M:%S %Z')
+ attributes['docdatetime'] = %(#{docdate} #{doctime})
+ elsif input.respond_to? :readlines
+ input.rewind if input.respond_to? :rewind
lines = input.readlines
- elsif input.is_a?(::String)
+ elsif input.is_a? ::String
lines = input.lines.entries
- elsif input.is_a?(::Array)
+ elsif input.is_a? ::Array
lines = input.dup
else
- raise ::ArgumentError, "Unsupported input type: #{input.class}"
+ raise ::ArgumentError, %(Unsupported input type: #{input.class})
end
- if monitor
- read_time = ::Time.now.to_f - start
- start = ::Time.now.to_f
+ if timings
+ timings.record :read
+ timings.start :parse
end
- doc = Document.new(lines, options)
- if monitor
- parse_time = ::Time.now.to_f - start
- monitor[:read] = read_time
- monitor[:parse] = parse_time
- monitor[:load] = read_time + parse_time
- end
+ doc = Document.new lines, options
+ timings.record :parse if timings
doc
end
@@ -1260,13 +1252,13 @@ module Asciidoctor
# String and Array values are converted into a Hash.
# See Asciidoctor::Document#initialize for details about options.
#
- # returns the Asciidoctor::Document
- def self.load_file(filename, options = {})
- ::Asciidoctor.load(::File.new(filename), options)
+ # Returns the Asciidoctor::Document
+ def load_file filename, options = {}
+ self.load ::File.new(filename || ''), options
end
- # Public: Parse the AsciiDoc source input into an Asciidoctor::Document and render it
- # to the specified backend format
+ # Public: Parse the AsciiDoc source input into an Asciidoctor::Document and
+ # convert it to the specified backend format.
#
# Accepts input as an IO, String or String Array object. If the
# input is a File, information about the file is stored in
@@ -1283,27 +1275,28 @@ module Asciidoctor
# outside of the Document#base_dir in safe mode, an IOError is raised.
#
# If the output is going to be written to a file, the header and footer are
- # rendered unless specified otherwise (writing to a file implies creating a
- # standalone document). Otherwise, the header and footer are not rendered by
- # default and the rendered output is returned.
+ # included unless specified otherwise (writing to a file implies creating a
+ # standalone document). Otherwise, the header and footer are not included by
+ # default and the converted result is returned.
#
# input - the String AsciiDoc source filename
# options - a String, Array or Hash of options to control processing (default: {})
# String and Array values are converted into a Hash.
# See Asciidoctor::Document#initialize for details about options.
#
- # returns the Document object if the rendered result String is written to a
- # file, otherwise the rendered result String
- def self.render(input, options = {})
+ # Returns the Document object if the converted String is written to a
+ # file, otherwise the converted String
+ def convert input, options = {}
+ options = options.dup
in_place = options.delete(:in_place) || false
to_file = options.delete(:to_file)
to_dir = options.delete(:to_dir)
mkdirs = options.delete(:mkdirs) || false
- monitor = options[:monitor]
+ timings = options[:timings]
write_in_place = in_place && input.is_a?(::File)
write_to_target = to_file || to_dir
- stream_output = !to_file.nil? && to_file.respond_to?(:write)
+ stream_output = to_file && to_file.respond_to?(:write)
if write_in_place && write_to_target
raise ::ArgumentError, 'the option :in_place cannot be used with either the :to_dir or :to_file option'
@@ -1313,7 +1306,7 @@ module Asciidoctor
options[:header_footer] = true
end
- doc = ::Asciidoctor.load(input, options)
+ doc = self.load input, options
if to_file == '/dev/null'
return doc
@@ -1347,32 +1340,28 @@ module Asciidoctor
end
end
- start = ::Time.now.to_f if monitor
- output = doc.render
+ # concept::
+ #if to_file
+ # doc.convert_to to_file, :timings => timings
+ # # write stylesheets
+ # doc
+ #else
+ # doc.convert, :timings => timings
+ #end
- if monitor
- render_time = ::Time.now.to_f - start
- monitor[:render] = render_time
- monitor[:load_render] = monitor[:load] + render_time
- end
+ timings.start :convert if timings
+ output = doc.convert
+ timings.record :convert if timings
if to_file
- start = ::Time.now.to_f if monitor
- if stream_output
- to_file.write output.rstrip
- # ensure there's a trailing endline
- to_file.write EOL
- else
- ::File.open(to_file, 'w') {|file| file.write output }
- # these assignments primarily for testing, diagnostics or reporting
- doc.attributes['outfile'] = outfile = ::File.expand_path(to_file)
- doc.attributes['outdir'] = ::File.dirname(outfile)
- end
- if monitor
- write_time = ::Time.now.to_f - start
- monitor[:write] = write_time
- monitor[:total] = monitor[:load_render] + write_time
+ timings.start :write if timings
+ unless stream_output
+ to_file = ::File.expand_path to_file
+ doc.attributes['outfile'] = ::File.expand_path to_file
+ doc.attributes['outdir'] = ::File.dirname to_file
end
+ doc.write output, to_file
+ timings.record :write if timings
# NOTE document cannot control this behavior if safe >= SafeMode::SERVER
if !stream_output && doc.safe < SafeMode::SECURE && (doc.attr? 'basebackend-html') &&
@@ -1386,13 +1375,11 @@ module Asciidoctor
stylesoutdir = doc.normalize_system_path(doc.attr('stylesdir'), outdir,
doc.safe >= SafeMode::SAFE ? outdir : nil)
Helpers.mkdir_p stylesoutdir if mkdirs
- if copy_asciidoctor_stylesheet
- ::File.open(::File.join(stylesoutdir, DEFAULT_STYLESHEET_NAME), 'w') {|f|
- f.write HTML5.default_asciidoctor_stylesheet
- }
- end
- if copy_user_stylesheet
+ if copy_asciidoctor_stylesheet
+ Stylesheets.instance.write_primary_stylesheet stylesoutdir
+ # FIXME should Stylesheets also handle the user stylesheet?
+ elsif copy_user_stylesheet
if (stylesheet_src = (doc.attr 'copycss')).empty?
stylesheet_src = doc.normalize_system_path stylesheet
else
@@ -1407,15 +1394,9 @@ module Asciidoctor
end
if copy_coderay_stylesheet
- ::File.open(::File.join(stylesoutdir, 'asciidoctor-coderay.css'), 'w') {|f|
- f.write HTML5.default_coderay_stylesheet
- }
- end
-
- if copy_pygments_stylesheet
- ::File.open(::File.join(stylesoutdir, 'asciidoctor-pygments.css'), 'w') {|f|
- f.write HTML5.pygments_stylesheet(doc.attr 'pygments-style')
- }
+ Stylesheets.instance.write_coderay_stylesheet stylesoutdir
+ elsif copy_pygments_stylesheet
+ Stylesheets.instance.write_pygments_stylesheet stylesoutdir, (doc.attr 'pygments-style')
end
end
end
@@ -1425,53 +1406,59 @@ module Asciidoctor
end
end
- # Public: Parse the contents of the AsciiDoc source file into an Asciidoctor::Document
- # and render it to the specified backend format
+ # Alias render to convert to maintain backwards compatibility
+ alias :render :convert
+
+ # Public: Parse the contents of the AsciiDoc source file into an
+ # Asciidoctor::Document and convert it to the specified backend format.
#
# input - the String AsciiDoc source filename
# options - a String, Array or Hash of options to control processing (default: {})
# String and Array values are converted into a Hash.
# See Asciidoctor::Document#initialize for details about options.
#
- # returns the Document object if the rendered result String is written to a
- # file, otherwise the rendered result String
- def self.render_file(filename, options = {})
- ::Asciidoctor.render(::File.new(filename), options)
+ # Returns the Document object if the converted String is written to a
+ # file, otherwise the converted String
+ def convert_file filename, options = {}
+ self.convert ::File.new(filename || ''), options
+ end
+
+ # Alias render_file to convert_file to maintain backwards compatibility
+ alias :render_file :convert_file
+
end
# autoload
unless ::RUBY_ENGINE_OPAL
autoload :Debug, 'asciidoctor/debug'
autoload :VERSION, 'asciidoctor/version'
- end
-
- # core extensions
- require 'asciidoctor/core_ext'
-
- # modules
- require 'asciidoctor/helpers'
- require 'asciidoctor/substitutors'
-
- # abstract classes
- require 'asciidoctor/abstract_node'
- require 'asciidoctor/abstract_block'
-
- # concrete classes
- require 'asciidoctor/attribute_list'
- require 'asciidoctor/block'
- require 'asciidoctor/callouts'
- require 'asciidoctor/document'
- require 'asciidoctor/inline'
- require 'asciidoctor/list'
- require 'asciidoctor/parser'
- require 'asciidoctor/path_resolver'
- require 'asciidoctor/reader'
- require 'asciidoctor/renderer'
- require 'asciidoctor/section'
- require 'asciidoctor/table'
-
- # backends
- if ::RUBY_ENGINE_OPAL
- require 'asciidoctor/backends/html5-erb'
+ autoload :Timings, 'asciidoctor/timings'
end
end
+
+# core extensions
+require 'asciidoctor/core_ext'
+
+# modules
+require 'asciidoctor/helpers'
+require 'asciidoctor/substitutors'
+
+# abstract classes
+require 'asciidoctor/abstract_node'
+require 'asciidoctor/abstract_block'
+
+# concrete classes
+require 'asciidoctor/attribute_list'
+require 'asciidoctor/block'
+require 'asciidoctor/callouts'
+require 'asciidoctor/converter'
+require 'asciidoctor/converter/html5' if RUBY_ENGINE_OPAL
+require 'asciidoctor/document'
+require 'asciidoctor/inline'
+require 'asciidoctor/list'
+require 'asciidoctor/parser'
+require 'asciidoctor/path_resolver'
+require 'asciidoctor/reader'
+require 'asciidoctor/section'
+require 'asciidoctor/stylesheets'
+require 'asciidoctor/table'
diff --git a/lib/asciidoctor/abstract_block.rb b/lib/asciidoctor/abstract_block.rb
index 0d904437..b476e709 100644
--- a/lib/asciidoctor/abstract_block.rb
+++ b/lib/asciidoctor/abstract_block.rb
@@ -6,9 +6,6 @@ class AbstractBlock < AbstractNode
# Public: Substitutions to be applied to content in this block
attr_reader :subs
- # Public: Get/Set the String name of the render template
- attr_accessor :template_name
-
# Public: Get the Array of Asciidoctor::AbstractBlock sub-blocks for this block
attr_reader :blocks
@@ -29,7 +26,6 @@ class AbstractBlock < AbstractNode
@content_model = :compound
@subs = []
@default_subs = nil
- @template_name = %(block_#{context})
@blocks = []
@id = nil
@title = nil
@@ -44,28 +40,39 @@ class AbstractBlock < AbstractNode
@next_section_number = 1
end
+ def block?
+ true
+ end
+
+ def inline?
+ false
+ end
+
# Public: Update the context of this block.
#
# This method changes the context of this block. It also
- # updates the template name accordingly.
+ # updates the node name accordingly.
def context=(context)
@context = context
- @template_name = %(block_#{context})
+ @node_name = context.to_s
end
- # Public: Get the rendered String content for this Block. If the block
+ # Public: Get the converted String content for this Block. If the block
# has child blocks, the content method should cause them to be
- # rendered and returned as content that can be included in the
+ # converted and returned as content that can be included in the
# parent block's template.
- def render
+ def convert
@document.playback_attributes @attributes
- renderer.render(@template_name, self)
+ converter.convert self
end
- # Public: Get an rendered version of the block content, rendering the
+ # Alias render to convert to maintain backwards compatibility
+ alias :render :convert
+
+ # Public: Get the converted result of the child blocks by converting the
# children appropriate to content model that this block supports.
def content
- @blocks.map {|b| b.render } * EOL
+ @blocks.map {|b| b.convert } * EOL
end
# Public: A convenience method that checks whether the specified
diff --git a/lib/asciidoctor/abstract_node.rb b/lib/asciidoctor/abstract_node.rb
index d3fc6b9a..6cd21662 100644
--- a/lib/asciidoctor/abstract_node.rb
+++ b/lib/asciidoctor/abstract_node.rb
@@ -15,7 +15,10 @@ class AbstractNode
# Public: Get the Symbol context for this node
attr_reader :context
- # Public: Get or set the id of this node
+ # Public: Get the String name of this node
+ attr_reader :node_name
+
+ # Public: Get/Set the id of this node
attr_accessor :id
# Public: Get the Hash of attributes for this node
@@ -34,6 +37,7 @@ class AbstractNode
end
end
@context = context
+ @node_name = context.to_s
@attributes = {}
@passthroughs = []
end
@@ -49,6 +53,20 @@ class AbstractNode
nil
end
+ # Public: Returns whether this {AbstractNode} is an instance of {Inline}
+ #
+ # Returns [Boolean]
+ def inline?
+ raise ::NotImplementedError
+ end
+
+ # Public: Returns whether this {AbstractNode} is an instance of {Block}
+ #
+ # Returns [Boolean]
+ def block?
+ raise ::NotImplementedError
+ end
+
# Public: Get the value of the specified attribute
#
# Get the value for the specified attribute. First look in the attributes on
@@ -181,10 +199,10 @@ class AbstractNode
nil
end
- # Public: Get the Asciidoctor::Renderer instance being used for the
- # Asciidoctor::Document to which this node belongs
- def renderer
- @document.renderer
+ # Public: Get the Asciidoctor::Converter instance being used to convert the
+ # current Asciidoctor::Document.
+ def converter
+ @document.converter
end
# Public: A convenience method that checks if the role attribute is specified
@@ -230,11 +248,6 @@ class AbstractNode
@attributes['reftext'] || @document.attributes['reftext']
end
- # Public: Returns a forward slash if the attribute htmlsyntax has the value "xml".
- def short_tag_slash
- @document.attributes['htmlsyntax'] == 'xml' ? '/' : nil
- end
-
# Public: Construct a reference or data URI to an icon image for the
# specified icon name.
#
@@ -353,7 +366,7 @@ class AbstractNode
else
bindata = File.open(image_path, 'rb') {|file| file.read }
end
- "data:#{mimetype};base64,#{Base64.encode64(bindata).delete("\n")}"
+ "data:#{mimetype};base64,#{Base64.encode64(bindata).delete EOL}"
end
# Public: Read the contents of the file at the specified path.
@@ -378,7 +391,7 @@ class AbstractNode
# Public: Normalize the web page using the PathResolver.
#
- # See {PathResolver.web_path} for details.
+ # See {PathResolver#web_path} for details.
#
# target - the String target path
# start - the String start (i.e, parent) path (optional, default: nil)
@@ -391,7 +404,7 @@ class AbstractNode
# Public: Resolve and normalize a secure path from the target and start paths
# using the PathResolver.
#
- # See {PathResolver.system_path} for details.
+ # See {PathResolver#system_path} for details.
#
# The most important functionality in this method is to prevent resolving a
# path outside of the jail (which defaults to the directory of the source
diff --git a/lib/asciidoctor/backends/base_template.rb b/lib/asciidoctor/backends/base_template.rb
deleted file mode 100644
index 58f4621c..00000000
--- a/lib/asciidoctor/backends/base_template.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-module Asciidoctor
-# An abstract base class that provides methods for definining and rendering the
-# backend templates. Concrete subclasses must implement the template method.
-#
-# NOTE we must use double quotes for attribute values in the HTML/XML output to
-# prevent quote processing. This requirement seems hackish, but AsciiDoc has
-# this same issue.
-class BaseTemplate
-
- attr_reader :view
- attr_reader :backend
-
- def initialize(view, backend)
- @view = view
- @backend = backend
- end
-
- def self.inherited(klass)
- if self == BaseTemplate
- @template_classes ||= []
- @template_classes << klass
- else
- self.superclass.inherited(klass)
- end
- end
-
- def self.template_classes
- @template_classes
- end
-
- # Public: Render this template in the execution context of
- # the supplied concrete instance of Asciidoctor::AbstractNode.
- #
- # This method invokes the template method on this instance to retrieve the
- # template data and then evaluates that template in the context of the
- # supplied concrete instance of Asciidoctor::AbstractNode. This instance is
- # accessible to the template data via the local variable named 'template'.
- #
- # If the compact flag on the document's renderer is true and the view context is
- # document or embedded, then blank lines in the output are compacted. Otherwise,
- # the rendered output is returned unprocessed.
- #
- # node - The concrete instance of AsciiDoctor::AbstractNode to render
- # locals - A Hash of additional variables. Not currently in use.
- def render(node = Object.new, locals = {})
- tmpl = template
- case tmpl
- when :invoke_result
- return result(node)
- when :invoke_result_document
- output = result(node)
- when :content
- output = node.content
- else
- output = tmpl.result(node.get_binding(self))
- end
-
- if (@view == 'document' || @view == 'embedded') &&
- node.renderer.compact && !node.document.nested?
- compact output
- else
- output
- end
- end
-
- # Public: Compact blank lines in the provided text. This method also restores
- # every HTML line feed entity found with an endline character.
- #
- # text - the String to process
- #
- # returns the text with blank lines removed and HTML line feed entities
- # converted to an endline character.
- def compact(str)
- str.gsub(BlankLineRx, '').gsub(LINE_FEED_ENTITY, EOL)
- end
-
- # Public: Preserve endlines by replacing them with the HTML line feed entity.
- #
- # If the compact flag on the document's renderer is true, perform the
- # replacement. Otherwise, return the text unprocessed.
- #
- # text - the String to process
- # node - the concrete instance of Asciidoctor::AbstractNode being rendered
- def preserve_endlines(str, node)
- node.renderer.compact ? str.gsub(EOL, LINE_FEED_ENTITY) : str
- end
-
- def template
- raise "You chilluns need to make your own template"
- end
-end
-
-module EmptyTemplate
- def result node
- ''
- end
-
- def template
- :invoke_result
- end
-end
-end
diff --git a/lib/asciidoctor/backends/docbook45.rb b/lib/asciidoctor/backends/docbook45.rb
deleted file mode 100644
index ae3fefc7..00000000
--- a/lib/asciidoctor/backends/docbook45.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-require 'asciidoctor/backends/docbook5'
-
-module Asciidoctor
-module DocBook45
-class DocumentTemplate < DocBook5::DocumentTemplate
- def namespace_attributes doc
- (doc.attr? 'noxmlns') ? nil : ' xmlns="http://docbook.org/ns/docbook"'
- end
-
- def author doc, index = nil
- firstname_key = index ? %(firstname_#{index}) : 'firstname'
- middlename_key = index ? %(middlename_#{index}) : 'middlename'
- lastname_key = index ? %(lastname_#{index}) : 'lastname'
- email_key = index ? %(email_#{index}) : 'email'
-
- result_buffer = []
- result_buffer << '<author>'
- result_buffer << %(<firstname>#{doc.attr firstname_key}</firstname>) if doc.attr? firstname_key
- result_buffer << %(<othername>#{doc.attr middlename_key}</othername>) if doc.attr? middlename_key
- result_buffer << %(<surname>#{doc.attr lastname_key}</surname>) if doc.attr? lastname_key
- result_buffer << %(<email>#{doc.attr email_key}</email>) if doc.attr? email_key
- result_buffer << '</author>'
-
- result_buffer * EOL
- end
-
- def docinfo_header doc, info_tag_prefix
- super doc, info_tag_prefix, true
- end
-
- def doctype_declaration root_tag_name
- %(<!DOCTYPE #{root_tag_name} PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">)
- end
-end
-
-class EmbeddedTemplate < DocBook5::EmbeddedTemplate; end
-class BlockTocTemplate < DocBook5::BlockTocTemplate; end
-class BlockPreambleTemplate < DocBook5::BlockPreambleTemplate; end
-class SectionTemplate < DocBook5::SectionTemplate; end
-class BlockFloatingTitleTemplate < DocBook5::BlockFloatingTitleTemplate; end
-class BlockParagraphTemplate < DocBook5::BlockParagraphTemplate; end
-class BlockAdmonitionTemplate < DocBook5::BlockAdmonitionTemplate; end
-class BlockUlistTemplate < DocBook5::BlockUlistTemplate; end
-class BlockOlistTemplate < DocBook5::BlockOlistTemplate; end
-class BlockColistTemplate < DocBook5::BlockColistTemplate; end
-class BlockDlistTemplate < DocBook5::BlockDlistTemplate; end
-class BlockOpenTemplate < DocBook5::BlockOpenTemplate; end
-class BlockListingTemplate < DocBook5::BlockListingTemplate; end
-class BlockLiteralTemplate < DocBook5::BlockLiteralTemplate; end
-class BlockExampleTemplate < DocBook5::BlockExampleTemplate; end
-class BlockSidebarTemplate < DocBook5::BlockSidebarTemplate; end
-class BlockQuoteTemplate < DocBook5::BlockQuoteTemplate; end
-class BlockVerseTemplate < DocBook5::BlockVerseTemplate; end
-class BlockPassTemplate < DocBook5::BlockPassTemplate; end
-class BlockMathTemplate < DocBook5::BlockMathTemplate; end
-class BlockTableTemplate < DocBook5::BlockTableTemplate; end
-class BlockImageTemplate < DocBook5::BlockImageTemplate; end
-class BlockAudioTemplate < DocBook5::BlockAudioTemplate; end
-class BlockVideoTemplate < DocBook5::BlockVideoTemplate; end
-class BlockRulerTemplate < DocBook5::BlockRulerTemplate; end
-class BlockPageBreakTemplate < DocBook5::BlockPageBreakTemplate; end
-class InlineBreakTemplate < DocBook5::InlineBreakTemplate; end
-class InlineQuotedTemplate < DocBook5::InlineQuotedTemplate; end
-class InlineButtonTemplate < DocBook5::InlineButtonTemplate; end
-class InlineKbdTemplate < DocBook5::InlineKbdTemplate; end
-class InlineMenuTemplate < DocBook5::InlineMenuTemplate; end
-class InlineImageTemplate < DocBook5::InlineImageTemplate; end
-
-class InlineAnchorTemplate < DocBook5::InlineAnchorTemplate
- def anchor(target, text, type, node)
- case type
- when :ref
- %(<anchor#{common_attrs target, nil, text}/>)
- when :xref
- if node.attr? 'path', nil
- linkend = (node.attr 'fragment') || target
- text.nil? ? %(<xref linkend="#{linkend}"/>) : %(<link linkend="#{linkend}">#{text}</link>)
- else
- text = text || (node.attr 'path')
- %(<ulink url="#{target}">#{text}</ulink>)
- end
- when :link
- %(<ulink url="#{target}">#{text}</ulink>)
- when :bibref
- %(<anchor#{common_attrs target, nil, "[#{target}]"}/>[#{target}])
- end
- end
-end
-
-class InlineFootnoteTemplate < DocBook5::InlineFootnoteTemplate; end
-class InlineCalloutTemplate < DocBook5::InlineCalloutTemplate; end
-class InlineIndextermTemplate < DocBook5::InlineIndextermTemplate; end
-
-end # module DocBook45
-end # module Asciidoctor
diff --git a/lib/asciidoctor/backends/docbook5.rb b/lib/asciidoctor/backends/docbook5.rb
deleted file mode 100644
index 2fd35550..00000000
--- a/lib/asciidoctor/backends/docbook5.rb
+++ /dev/null
@@ -1,898 +0,0 @@
-module Asciidoctor
-# FIXME move these into a base DocBook template or other helper class
-class BaseTemplate
- def title_element node, optional = true
- !optional || node.title? ? %(<title>#{node.title}</title>\n) : nil
- end
-
- def common_attrs id, role = nil, reftext = nil
- res = ''
- if id
- res = (@backend == 'docbook5' ? %( xml:id="#{id}") : %( id="#{id}"))
- end
- if role
- res = %(#{res} role="#{role}")
- end
- if reftext
- res = %(#{res} xreflabel="#{reftext}")
- end
- res
- end
-
- # QUESTION should we remove this method?
- def content(node)
- node.blocks? ? node.content : "<simpara>#{node.content}</simpara>"
- end
-end
-
-module DocBook5
-class DocumentTemplate < BaseTemplate
- def result doc
- result_buffer = []
- root_tag_name = doc.doctype
- result_buffer << '<?xml version="1.0" encoding="UTF-8"?>'
- if (doctype_line = doctype_declaration root_tag_name)
- result_buffer << doctype_line
- end
- result_buffer << '<?asciidoc-toc?>' if doc.attr? 'toc'
- result_buffer << '<?asciidoc-numbered?>' if doc.attr? 'numbered'
- lang_attribute = (doc.attr? 'nolang') ? nil : %( lang="#{doc.attr 'lang', 'en'}")
- result_buffer << %(<#{root_tag_name}#{namespace_attributes doc}#{lang_attribute}>)
- result_buffer << (docinfo_header doc, root_tag_name)
- result_buffer << doc.content if doc.blocks?
- unless (user_docinfo_footer = doc.docinfo :footer).empty?
- result_buffer << user_docinfo_footer
- end
- result_buffer << %(</#{root_tag_name}>)
-
- result_buffer * EOL
- end
-
- def namespace_attributes doc
- ' xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0"'
- end
-
- def author doc, index = nil
- firstname_key = index ? %(firstname_#{index}) : 'firstname'
- middlename_key = index ? %(middlename_#{index}) : 'middlename'
- lastname_key = index ? %(lastname_#{index}) : 'lastname'
- email_key = index ? %(email_#{index}) : 'email'
-
- result_buffer = []
- result_buffer << '<author>'
- result_buffer << '<personname>'
- result_buffer << %(<firstname>#{doc.attr firstname_key}</firstname>) if doc.attr? firstname_key
- result_buffer << %(<othername>#{doc.attr middlename_key}</othername>) if doc.attr? middlename_key
- result_buffer << %(<surname>#{doc.attr lastname_key}</surname>) if doc.attr? lastname_key
- result_buffer << '</personname>'
- result_buffer << %(<email>#{doc.attr email_key}</email>) if doc.attr? email_key
- result_buffer << '</author>'
-
- result_buffer * EOL
- end
-
- def docinfo_header doc, info_tag_prefix, use_info_tag_prefix = false
- info_tag_prefix = '' unless use_info_tag_prefix
- result_buffer = []
- result_buffer << %(<#{info_tag_prefix}info>)
- result_buffer << (doc.header? ? (title_tags doc.header.title) : %(<title>#{doc.attr 'untitled-label'}</title>)) unless doc.notitle
- result_buffer << %(<date>#{(doc.attr? 'revdate') ? (doc.attr 'revdate') : (doc.attr 'docdate')}</date>)
- if doc.has_header?
- if doc.attr? 'author'
- if (authorcount = (doc.attr 'authorcount').to_i) < 2
- result_buffer << (author doc)
- result_buffer << %(<authorinitials>#{doc.attr 'authorinitials'}</authorinitials>) if doc.attr? 'authorinitials'
- else
- result_buffer << '<authorgroup>'
- authorcount.times do |index|
- result_buffer << (author doc, index + 1)
- end
- result_buffer << '</authorgroup>'
- end
- end
- if (doc.attr? 'revdate') && ((doc.attr? 'revnumber') || (doc.attr? 'revremark'))
- result_buffer << %(<revhistory>
-<revision>)
- result_buffer << %(<revnumber>#{doc.attr 'revnumber'}</revnumber>) if doc.attr? 'revnumber'
- result_buffer << %(<date>#{doc.attr 'revdate'}</date>) if doc.attr? 'revdate'
- result_buffer << %(<authorinitials>#{doc.attr 'authorinitials'}</authorinitials>) if doc.attr? 'authorinitials'
- result_buffer << %(<revremark>#{doc.attr 'revremark'}</revremark>) if doc.attr? 'revremark'
- result_buffer << %(</revision>
-</revhistory>)
- end
- unless (user_docinfo_header = doc.docinfo :header).empty?
- result_buffer << user_docinfo_header
- end
- result_buffer << %(<orgname>#{doc.attr 'orgname'}</orgname>) if doc.attr? 'orgname'
- end
- result_buffer << %(</#{info_tag_prefix}info>)
-
- result_buffer * EOL
- end
-
- def doctype_declaration root_tag_name
- nil
- end
-
- # FIXME this splitting should handled in the AST!
- def title_tags title
- if title.include? ': '
- title, _, subtitle = title.rpartition ': '
- %(<title>#{title}</title>
-<subtitle>#{subtitle}</subtitle>)
- else
- %(<title>#{title}</title>)
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class EmbeddedTemplate < BaseTemplate
- def template
- :content
- end
-end
-
-class BlockTocTemplate < BaseTemplate
- include EmptyTemplate
-end
-
-class BlockPreambleTemplate < BaseTemplate
- def result node
- if node.document.doctype == 'book'
- %(<preface#{common_attrs node.id, node.role, node.reftext}>
-#{title_element node, false}#{node.content}
-</preface>)
- else
- node.content
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class SectionTemplate < BaseTemplate
- def result sec
- if sec.special
- tag_name = sec.level <= 1 ? sec.sectname : 'section'
- else
- tag_name = sec.document.doctype == 'book' && sec.level <= 1 ? (sec.level == 0 ? 'part' : 'chapter') : 'section'
- end
- %(<#{tag_name}#{common_attrs sec.id, sec.role, sec.reftext}>
-<title>#{sec.title}</title>
-#{sec.content}
-</#{tag_name}>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockFloatingTitleTemplate < BaseTemplate
- def result node
- %(<bridgehead#{common_attrs node.id, node.role, node.reftext} renderas="sect#{node.level}">#{node.title}</bridgehead>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockParagraphTemplate < BaseTemplate
- def result node
- if node.title?
- %(<formalpara#{common_attrs node.id, node.role, node.reftext}>
-<title>#{node.title}</title>
-<para>#{node.content}</para>
-</formalpara>)
- else
- %(<simpara#{common_attrs node.id, node.role, node.reftext}>#{node.content}</simpara>)
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockAdmonitionTemplate < BaseTemplate
- def result node
- %(<#{tag_name = node.attr 'name'}#{common_attrs node.id, node.role, node.reftext}>
-#{title_element node}#{content node}
-</#{tag_name}>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockUlistTemplate < BaseTemplate
- def result node
- result_buffer = []
- if node.style == 'bibliography'
- result_buffer << %(<bibliodiv#{common_attrs node.id, node.role, node.reftext}>)
- result_buffer << %(<title>#{node.title}</title>) if node.title?
- node.items.each do |item|
- result_buffer << '<bibliomixed>'
- result_buffer << %(<bibliomisc>#{item.text}</bibliomisc>)
- result_buffer << item.content if item.blocks?
- result_buffer << '</bibliomixed>'
- end
- result_buffer << '</bibliodiv>'
- else
- mark_type = (checklist = node.option? 'checklist') ? 'none' : node.style
- mark_attribute = mark_type ? %( mark="#{mark_type}") : nil
- result_buffer << %(<itemizedlist#{common_attrs node.id, node.role, node.reftext}#{mark_attribute}>)
- result_buffer << %(<title>#{node.title}</title>) if node.title?
- node.items.each do |item|
- text_marker = if checklist && (item.attr? 'checkbox')
- (item.attr? 'checked') ? '&#10003; ' : '&#10063; '
- else
- nil
- end
- result_buffer << '<listitem>'
- result_buffer << %(<simpara>#{text_marker}#{item.text}</simpara>)
- result_buffer << item.content if item.blocks?
- result_buffer << '</listitem>'
- end
- result_buffer << '</itemizedlist>'
- end
-
- result_buffer * EOL
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockOlistTemplate < BaseTemplate
- def result node
- result_buffer = []
- num_attribute = node.style ? %( numeration="#{node.style}") : nil
- result_buffer << %(<orderedlist#{common_attrs node.id, node.role, node.reftext}#{num_attribute}>)
- result_buffer << %(<title>#{node.title}</title>) if node.title?
- node.items.each do |item|
- result_buffer << '<listitem>'
- result_buffer << %(<simpara>#{item.text}</simpara>)
- result_buffer << item.content if item.blocks?
- result_buffer << '</listitem>'
- end
- result_buffer << %(</orderedlist>)
- result_buffer * EOL
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockColistTemplate < BaseTemplate
- def result node
- result_buffer = []
- result_buffer << %(<calloutlist#{common_attrs node.id, node.role, node.reftext}>)
- result_buffer << %(<title>#{node.title}</title>) if node.title?
- node.items.each do |item|
- result_buffer << %(<callout arearefs="#{item.attr 'coids'}">)
- result_buffer << %(<para>#{item.text}</para>)
- result_buffer << item.content if item.blocks?
- result_buffer << '</callout>'
- end
- result_buffer << %(</calloutlist>)
- result_buffer * EOL
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockDlistTemplate < BaseTemplate
- LIST_TAGS = {
- 'labeled' => {
- :list => 'variablelist',
- :entry => 'varlistentry',
- :term => 'term',
- :item => 'listitem'
- },
- 'qanda' => {
- :list => 'qandaset',
- :entry => 'qandaentry',
- :label => 'question',
- :term => 'simpara',
- :item => 'answer'
- },
- 'glossary' => {
- :list => nil,
- :entry => 'glossentry',
- :term => 'glossterm',
- :item => 'glossdef'
- }
- }
-
- LIST_TAGS_DEFAULT = LIST_TAGS['labeled']
-
- def result node
- result_buffer = []
- if node.style == 'horizontal'
- result_buffer << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attrs node.id, node.role, node.reftext} tabstyle="horizontal" frame="none" colsep="0" rowsep="0">
-#{title_element node}<tgroup cols="2">
-<colspec colwidth="#{node.attr 'labelwidth', 15}*"/>
-<colspec colwidth="#{node.attr 'itemwidth', 85}*"/>
-<tbody valign="top">)
- node.items.each do |terms, dd|
- result_buffer << %(<row>
-<entry>)
- [*terms].each do |dt|
- result_buffer << %(<simpara>#{dt.text}</simpara>)
- end
- result_buffer << %(</entry>
-<entry>)
- unless dd.nil?
- result_buffer << %(<simpara>#{dd.text}</simpara>) if dd.text?
- result_buffer << dd.content if dd.blocks?
- end
- result_buffer << %(</entry>
-</row>)
- end
- result_buffer << %(</tbody>
-</tgroup>
-</#{tag_name}>)
- else
- tags = LIST_TAGS[node.style] || LIST_TAGS_DEFAULT
- list_tag = tags[:list]
- entry_tag = tags[:entry]
- label_tag = tags[:label]
- term_tag = tags[:term]
- item_tag = tags[:item]
- if list_tag
- result_buffer << %(<#{list_tag}#{common_attrs node.id, node.role, node.reftext}>)
- result_buffer << %(<title>#{node.title}</title>) if node.title?
- end
-
- node.items.each do |terms, dd|
- result_buffer << %(<#{entry_tag}>)
- result_buffer << %(<#{label_tag}>) if label_tag
-
- [*terms].each do |dt|
- result_buffer << %(<#{term_tag}>#{dt.text}</#{term_tag}>)
- end
-
- result_buffer << %(</#{label_tag}>) if label_tag
- result_buffer << %(<#{item_tag}>)
- unless dd.nil?
- result_buffer << %(<simpara>#{dd.text}</simpara>) if dd.text?
- result_buffer << dd.content if dd.blocks?
- end
- result_buffer << %(</#{item_tag}>)
- result_buffer << %(</#{entry_tag}>)
- end
-
- result_buffer << %(</#{list_tag}>) if list_tag
- end
-
- result_buffer * EOL
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockOpenTemplate < BaseTemplate
- def result node
- open_block(node, node.id, node.style, node.role, node.reftext, node.title? ? node.title : nil)
- end
-
- def open_block(node, id, style, role, reftext, title)
- case style
- when 'abstract'
- if node.parent == node.document && node.document.attr?('doctype', 'book')
- warn 'asciidoctor: WARNING: abstract block cannot be used in a document without a title when doctype is book. Excluding block content.'
- ''
- else
- %(<abstract>#{title && "\n<title>#{title}</title>"}
-#{content node}
-</abstract>)
- end
- when 'partintro'
- unless node.level == 0 && node.parent.context == :section && node.document.doctype == 'book'
- warn 'asciidoctor: ERROR: partintro block can only be used when doctype is book and it\'s a child of a part section. Excluding block content.'
- ''
- else
- %(<partintro#{common_attrs id, role, reftext}>#{title && "\n<title>#{title}</title>"}
-#{content node}
-</partintro>)
- end
- else
- node.content
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockListingTemplate < BaseTemplate
- def result node
- informal = !node.title?
- listing_attributes = (common_attrs node.id, node.role, node.reftext)
- if node.style == 'source' && (node.attr? 'language')
- numbering = (node.attr? 'linenums') ? 'numbered' : 'unnumbered'
- listing_content = %(<programlisting#{informal ? listing_attributes : nil} language="#{node.attr 'language'}" linenumbering="#{numbering}">#{preserve_endlines node.content, node}</programlisting>)
- else
- listing_content = %(<screen#{informal ? listing_attributes : nil}>#{preserve_endlines node.content, node}</screen>)
- end
- if informal
- listing_content
- else
- %(<formalpara#{listing_attributes}>
-<title>#{node.title}</title>
-<para>
-#{listing_content}
-</para>
-</formalpara>)
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockLiteralTemplate < BaseTemplate
- def result node
- if node.title?
- %(<formalpara#{common_attrs node.id, node.role, node.reftext}>
-<title>#{node.title}</title>
-<para>
-<literallayout class="monospaced">#{preserve_endlines node.content, node}</literallayout>
-</para>
-</formalpara>)
- else
- %(<literallayout#{common_attrs node.id, node.role, node.reftext} class="monospaced">#{preserve_endlines node.content, node}</literallayout>)
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockExampleTemplate < BaseTemplate
- def result node
- if node.title?
- %(<example#{common_attrs node.id, node.role, node.reftext}>
-<title>#{node.title}</title>
-#{content node}
-</example>)
- else
- %(<informalexample#{common_attrs node.id, node.role, node.reftext}>
-#{content node}
-</informalexample>)
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockSidebarTemplate < BaseTemplate
- def result node
- %(<sidebar#{common_attrs node.id, node.role, node.reftext}>
-#{title_element node}#{content node}
-</sidebar>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockQuoteTemplate < BaseTemplate
- def result node
- result_buffer = []
- result_buffer << %(<blockquote#{common_attrs node.id, node.role, node.reftext}>)
- result_buffer << %(<title>#{node.title}</title>) if node.title?
- if (node.attr? 'attribution') || (node.attr? 'citetitle')
- result_buffer << '<attribution>'
- if node.attr? 'attribution'
- result_buffer << (node.attr 'attribution')
- end
- if node.attr? 'citetitle'
- result_buffer << %(<citetitle>#{node.attr 'citetitle'}</citetitle>)
- end
- result_buffer << '</attribution>'
- end
- result_buffer << (content node)
- result_buffer << '</blockquote>'
- result_buffer * EOL
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockVerseTemplate < BaseTemplate
- def result node
- result_buffer = []
- result_buffer << %(<blockquote#{common_attrs node.id, node.role, node.reftext}>)
- result_buffer << %(<title>#{node.title}</title>) if node.title?
- if (node.attr? 'attribution') || (node.attr? 'citetitle')
- result_buffer << '<attribution>'
- if node.attr? 'attribution'
- result_buffer << (node.attr 'attribution')
- end
- if node.attr? 'citetitle'
- result_buffer << %(<citetitle>#{node.attr 'citetitle'}</citetitle>)
- end
- result_buffer << '</attribution>'
- end
- result_buffer << %(<literallayout>#{node.content}</literallayout>)
- result_buffer << '</blockquote>'
- result_buffer * EOL
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockPassTemplate < BaseTemplate
- def template
- :content
- end
-end
-
-class BlockMathTemplate < BaseTemplate
- def result node
- equation = node.content.strip
- if node.style == 'latexmath'
- equation_data = %(<alt><![CDATA[#{equation}]]></alt>
-<mediaobject><textobject><phrase></phrase></textobject></mediaobject>)
- else # asciimath
- # DocBook backends can't handle AsciiMath, so output raw expression in text object
- equation_data = %(<mediaobject><textobject><phrase><![CDATA[#{equation}]]></phrase></textobject></mediaobject>)
- end
- if node.title?
- %(<equation#{common_attrs node.id, node.role, node.reftext}>
-<title>#{node.title}</title>
-#{equation_data}
-</equation>)
- else
- %(<informalequation#{common_attrs node.id, node.role, node.reftext}>
-#{equation_data}
-</informalequation>)
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockTableTemplate < BaseTemplate
- TABLE_PI_NAMES = ['dbhtml', 'dbfo', 'dblatex']
- TABLE_SECTIONS = [:head, :foot, :body]
-
- def result node
- result_buffer = []
- pgwide_attribute = (node.option? 'pgwide') ? ' pgwide="1"' : nil
- result_buffer << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attrs node.id, node.role, node.reftext}#{pgwide_attribute} frame="#{node.attr 'frame', 'all'}" rowsep="#{['none', 'cols'].include?(node.attr 'grid') ? 0 : 1}" colsep="#{['none', 'rows'].include?(node.attr 'grid') ? 0 : 1}">)
- result_buffer << %(<title>#{node.title}</title>) if tag_name == 'table'
- if (width = (node.attr? 'width') ? (node.attr 'width') : nil)
- TABLE_PI_NAMES.each do |pi_name|
- result_buffer << %(<?#{pi_name} table-width="#{width}"?>)
- end
- end
- result_buffer << %(<tgroup cols="#{node.attr 'colcount'}">)
- node.columns.each do |col|
- result_buffer << %(<colspec colname="col_#{col.attr 'colnumber'}" colwidth="#{col.attr(width ? 'colabswidth' : 'colpcwidth')}*"/>)
- end
- TABLE_SECTIONS.select {|tblsec| !node.rows[tblsec].empty? }.each do |tblsec|
- result_buffer << %(<t#{tblsec}>)
- node.rows[tblsec].each do |row|
- result_buffer << '<row>'
- row.each do |cell|
- halign_attribute = (cell.attr? 'halign') ? %( align="#{cell.attr 'halign'}") : nil
- valign_attribute = (cell.attr? 'valign') ? %( valign="#{cell.attr 'valign'}") : nil
- colspan_attribute = cell.colspan ? %( namest="col_#{colnum = cell.column.attr 'colnumber'}" nameend="col_#{colnum + cell.colspan - 1}") : nil
- rowspan_attribute = cell.rowspan ? %( morerows="#{cell.rowspan - 1}") : nil
- # NOTE <entry> may not have whitespace (e.g., line breaks) as a direct descendant according to DocBook rules
- entry_start = %(<entry#{halign_attribute}#{valign_attribute}#{colspan_attribute}#{rowspan_attribute}>)
- cell_content = if tblsec == :head
- cell.text
- else
- case cell.style
- when :asciidoc
- cell.content
- when :verse
- %(<literallayout>#{preserve_endlines cell.text, node}</literallayout>)
- when :literal
- %(<literallayout class="monospaced">#{preserve_endlines cell.text, node}</literallayout>)
- when :header
- cell.content.map {|text| %(<simpara><emphasis role="strong">#{text}</emphasis></simpara>) }.join
- else
- cell.content.map {|text| %(<simpara>#{text}</simpara>) }.join
- end
- end
- entry_end = (node.document.attr? 'cellbgcolor') ? %(<?dbfo bgcolor="#{node.document.attr 'cellbgcolor'}"?></entry>) : '</entry>'
- result_buffer << %(#{entry_start}#{cell_content}#{entry_end})
- end
- result_buffer << '</row>'
- end
- result_buffer << %(</t#{tblsec}>)
- end
- result_buffer << '</tgroup>'
- result_buffer << %(</#{tag_name}>)
-
- result_buffer * EOL
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockImageTemplate < BaseTemplate
- def result node
- width_attribute = (node.attr? 'width') ? %( contentwidth="#{node.attr 'width'}") : nil
- depth_attribute = (node.attr? 'height') ? %( contentdepth="#{node.attr 'height'}") : nil
- swidth_attribute = (node.attr? 'scaledwidth') ? %( width="#{node.attr 'scaledwidth'}" scalefit="1") : nil
- scale_attribute = (node.attr? 'scale') ? %( scale="#{node.attr 'scale'}") : nil
- align_attribute = (node.attr? 'align') ? %( align="#{node.attr 'align'}") : nil
-
- %(<figure#{common_attrs node.id, node.role, node.reftext}>
-#{title_element node}<mediaobject>
-<imageobject>
-<imagedata fileref="#{node.image_uri(node.attr 'target')}"#{width_attribute}#{depth_attribute}#{swidth_attribute}#{scale_attribute}#{align_attribute}/>
-</imageobject>
-<textobject><phrase>#{node.attr 'alt'}</phrase></textobject>
-</mediaobject>
-</figure>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockAudioTemplate < BaseTemplate
- include EmptyTemplate
-end
-
-class BlockVideoTemplate < BaseTemplate
- include EmptyTemplate
-end
-
-class BlockRulerTemplate < BaseTemplate
- def result node
- '<simpara><?asciidoc-hr?></simpara>'
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockPageBreakTemplate < BaseTemplate
- def result node
- '<simpara><?asciidoc-pagebreak?></simpara>'
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineBreakTemplate < BaseTemplate
- def result node
- %(#{node.text}<?asciidoc-br?>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineQuotedTemplate < BaseTemplate
- NO_TAGS = [nil, nil]
-
- QUOTED_TAGS = {
- :emphasis => ['<emphasis>', '</emphasis>'],
- :strong => ['<emphasis role="strong">', '</emphasis>'],
- :monospaced => ['<literal>', '</literal>'],
- :superscript => ['<superscript>', '</superscript>'],
- :subscript => ['<subscript>', '</subscript>'],
- :double => ['&#8220;', '&#8221;'],
- :single => ['&#8216;', '&#8217;']
- }
-
- def quote_text(text, type, id, role)
- if type == :latexmath
- %(<inlineequation>
-<alt><![CDATA[#{text}]]></alt>
-<inlinemediaobject><textobject><phrase><![CDATA[#{text}]]></phrase></textobject></inlinemediaobject>
-</inlineequation>)
- else
- start_tag, end_tag = QUOTED_TAGS[type] || NO_TAGS
- anchor = id.nil? ? nil : %(<anchor#{common_attrs id, nil, text}/>)
- if role
- quoted_text = "#{start_tag}<phrase role=\"#{role}\">#{text}</phrase>#{end_tag}"
- elsif start_tag.nil?
- quoted_text = text
- else
- quoted_text = %(#{start_tag}#{text}#{end_tag})
- end
-
- anchor.nil? ? quoted_text : %(#{anchor}#{quoted_text})
- end
- end
-
- def result node
- quote_text(node.text, node.type, node.id, node.role)
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineButtonTemplate < BaseTemplate
- def result node
- %(<guibutton>#{node.text}</guibutton>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineKbdTemplate < BaseTemplate
- def result node
- keys = node.attr 'keys'
- if keys.size == 1
- %(<keycap>#{keys[0]}</keycap>)
- else
- key_combo = keys.map{|key| %(<keycap>#{key}</keycap>) }.join
- %(<keycombo>#{key_combo}</keycombo>)
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineMenuTemplate < BaseTemplate
- def menu(menu, submenus, menuitem)
- if !submenus.empty?
- submenu_path = submenus.map{|submenu| %(<guisubmenu>#{submenu}</guisubmenu> ) }.join.chop
- %(<menuchoice><guimenu>#{menu}</guimenu> #{submenu_path} <guimenuitem>#{menuitem}</guimenuitem></menuchoice>)
- elsif !menuitem.nil?
- %(<menuchoice><guimenu>#{menu}</guimenu> <guimenuitem>#{menuitem}</guimenuitem></menuchoice>)
- else
- %(<guimenu>#{menu}</guimenu>)
- end
- end
-
- def result node
- menu(node.attr('menu'), node.attr('submenus'), node.attr('menuitem'))
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineAnchorTemplate < BaseTemplate
- def anchor(target, text, type, node)
- case type
- when :ref
- %(<anchor#{common_attrs target, nil, text}/>)
- when :xref
- if node.attr? 'path', nil
- linkend = (node.attr 'fragment') || target
- text.nil? ? %(<xref linkend="#{linkend}"/>) : %(<link linkend="#{linkend}">#{text}</link>)
- else
- text = text || (node.attr 'path')
- %(<link xlink:href="#{target}">#{text}</link>)
- end
- when :link
- %(<link xlink:href="#{target}">#{text}</link>)
- when :bibref
- %(<anchor#{common_attrs target, nil, "[#{target}]"}/>[#{target}])
- end
- end
-
- def result node
- anchor(node.target, node.text, node.type, node)
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineImageTemplate < BaseTemplate
- def result node
- width_attribute = (node.attr? 'width') ? %( contentwidth="#{node.attr 'width'}") : nil
- depth_attribute = (node.attr? 'height') ? %( contentdepth="#{node.attr 'height'}") : nil
- %(<inlinemediaobject>
-<imageobject>
-<imagedata fileref="#{node.type == 'icon' ? (node.icon_uri node.target) : (node.image_uri node.target)}"#{width_attribute}#{depth_attribute}/>
-</imageobject>
-<textobject><phrase>#{node.attr 'alt'}</phrase></textobject>
-</inlinemediaobject>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineFootnoteTemplate < BaseTemplate
- def result node
- if node.type == :xref
- %(<footnoteref linkend="#{node.target}"/>)
- else
- %(<footnote#{common_attrs node.id}><simpara>#{node.text}</simpara></footnote>)
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineCalloutTemplate < BaseTemplate
- def result node
- %(<co#{common_attrs node.id, nil, nil}/>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineIndextermTemplate < BaseTemplate
- def result node
- if node.type == :visible
- %(<indexterm><primary>#{node.text}</primary></indexterm>#{node.text})
- else
- terms = node.attr 'terms'
- result_buffer = []
- if (numterms = terms.size) > 2
- result_buffer << %(<indexterm>
-<primary>#{terms[0]}</primary><secondary>#{terms[1]}</secondary><tertiary>#{terms[2]}</tertiary>
-</indexterm>)
- end
- if numterms > 1
- result_buffer << %(<indexterm>
-<primary>#{terms[-2]}</primary><secondary>#{terms[-1]}</secondary>
-</indexterm>)
- end
- result_buffer << %(<indexterm>
-<primary>#{terms[-1]}</primary>
-</indexterm>)
- result_buffer * EOL
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-end # module DocBook5
-end # module Asciidoctor
diff --git a/lib/asciidoctor/backends/html5-erb.rb b/lib/asciidoctor/backends/html5-erb.rb
deleted file mode 100644
index aa67792f..00000000
--- a/lib/asciidoctor/backends/html5-erb.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'erb'
-require 'asciidoctor/backends/erb/html5/block_admonition'
-require 'asciidoctor/backends/erb/html5/block_audio'
-require 'asciidoctor/backends/erb/html5/block_colist'
-require 'asciidoctor/backends/erb/html5/block_dlist'
-require 'asciidoctor/backends/erb/html5/block_example'
-require 'asciidoctor/backends/erb/html5/block_floating_title'
-require 'asciidoctor/backends/erb/html5/block_image'
-require 'asciidoctor/backends/erb/html5/block_listing'
-require 'asciidoctor/backends/erb/html5/block_literal'
-require 'asciidoctor/backends/erb/html5/block_olist'
-require 'asciidoctor/backends/erb/html5/block_open'
-require 'asciidoctor/backends/erb/html5/block_page_break'
-require 'asciidoctor/backends/erb/html5/block_paragraph'
-require 'asciidoctor/backends/erb/html5/block_pass'
-require 'asciidoctor/backends/erb/html5/block_preamble'
-require 'asciidoctor/backends/erb/html5/block_quote'
-require 'asciidoctor/backends/erb/html5/block_ruler'
-require 'asciidoctor/backends/erb/html5/block_sidebar'
-require 'asciidoctor/backends/erb/html5/block_table'
-require 'asciidoctor/backends/erb/html5/block_toc'
-require 'asciidoctor/backends/erb/html5/block_ulist'
-require 'asciidoctor/backends/erb/html5/block_verse'
-require 'asciidoctor/backends/erb/html5/block_video'
-require 'asciidoctor/backends/erb/html5/document'
-require 'asciidoctor/backends/erb/html5/embedded'
-require 'asciidoctor/backends/erb/html5/inline_anchor'
-require 'asciidoctor/backends/erb/html5/inline_break'
-require 'asciidoctor/backends/erb/html5/inline_button'
-require 'asciidoctor/backends/erb/html5/inline_callout'
-require 'asciidoctor/backends/erb/html5/inline_footnote'
-require 'asciidoctor/backends/erb/html5/inline_image'
-require 'asciidoctor/backends/erb/html5/inline_indexterm'
-require 'asciidoctor/backends/erb/html5/inline_kbd'
-require 'asciidoctor/backends/erb/html5/inline_menu'
-require 'asciidoctor/backends/erb/html5/inline_quoted'
-require 'asciidoctor/backends/erb/html5/section'
diff --git a/lib/asciidoctor/backends/html5.rb b/lib/asciidoctor/backends/html5.rb
deleted file mode 100644
index 205112b0..00000000
--- a/lib/asciidoctor/backends/html5.rb
+++ /dev/null
@@ -1,1311 +0,0 @@
-require 'asciidoctor/backends/_stylesheets'
-
-module Asciidoctor
-module HTML5
-
-class DocumentTemplate < BaseTemplate
- # FIXME make this outline generic
- def self.outline(node, to_depth = 2, sectnumlevels = nil)
- return if (sections = node.sections).empty?
- sectnumlevels = (node.document.attr 'sectnumlevels', 3).to_i unless sectnumlevels
- toc_level_buffer = []
- # FIXME the level for special sections should be set correctly in the model
- # slevel will only be 0 if we have a book doctype with parts
- slevel = (first_section = sections[0]).level
- slevel = 1 if slevel == 0 && first_section.special
- toc_level_buffer << %(<ul class="sectlevel#{slevel}">)
- sections.each do |section|
- section_num = (section.numbered && !section.caption && section.level <= sectnumlevels) ? %(#{section.sectnum} ) : nil
- toc_level_buffer << %(<li><a href="##{section.id}">#{section_num}#{section.captioned_title}</a></li>)
- if section.level < to_depth && (child_toc_level = outline(section, to_depth, sectnumlevels))
- toc_level_buffer << '<li>'
- toc_level_buffer << child_toc_level
- toc_level_buffer << '</li>'
- end
- end
- toc_level_buffer << '</ul>'
- toc_level_buffer * EOL
- end
-
- def result node
- result_buffer = []
- short_tag_slash_local = node.short_tag_slash
- br = %(<br#{short_tag_slash_local}>)
- linkcss = node.safe >= SafeMode::SECURE || (node.attr? 'linkcss')
- result_buffer << '<!DOCTYPE html>'
- result_buffer << ((node.attr? 'nolang') ? '<html>' : %(<html lang="#{node.attr 'lang', 'en'}">))
- result_buffer << %(<head>
-<meta http-equiv="Content-Type" content="text/html; charset=#{node.attr 'encoding'}"#{short_tag_slash_local}>
-<meta name="generator" content="Asciidoctor #{node.attr 'asciidoctor-version'}"#{short_tag_slash_local}>
-<meta name="viewport" content="width=device-width, initial-scale=1.0"#{short_tag_slash_local}>)
-
- ['description', 'keywords', 'author', 'copyright'].each do |key|
- result_buffer << %(<meta name="#{key}" content="#{node.attr key}"#{short_tag_slash_local}>) if node.attr? key
- end
-
- result_buffer << %(<title>#{node.doctitle(:sanitize => true) || node.attr('untitled-label')}</title>)
- if DEFAULT_STYLESHEET_KEYS.include?(node.attr 'stylesheet')
- if linkcss
- result_buffer << %(<link rel="stylesheet" href="#{node.normalize_web_path DEFAULT_STYLESHEET_NAME, (node.attr 'stylesdir', '')}"#{short_tag_slash_local}>)
- else
- result_buffer << %(<style>
-#{HTML5.default_asciidoctor_stylesheet}
-</style>)
- end
- elsif node.attr? 'stylesheet'
- if linkcss
- result_buffer << %(<link rel="stylesheet" href="#{node.normalize_web_path((node.attr 'stylesheet'), (node.attr 'stylesdir', ''))}"#{short_tag_slash_local}>)
- else
- result_buffer << %(<style>
-#{node.read_asset node.normalize_system_path((node.attr 'stylesheet'), (node.attr 'stylesdir', '')), true}
-</style>)
- end
- end
-
- if node.attr? 'icons', 'font'
- if !(node.attr 'iconfont-remote', '').nil?
- result_buffer << %(<link rel="stylesheet" href="#{node.attr 'iconfont-cdn', 'http://cdnjs.cloudflare.com/ajax/libs/font-awesome/3.2.1/css/font-awesome.min.css'}"#{short_tag_slash_local}>)
- else
- iconfont_stylesheet = %(#{node.attr 'iconfont-name', 'font-awesome'}.css)
- result_buffer << %(<link rel="stylesheet" href="#{node.normalize_web_path iconfont_stylesheet, (node.attr 'stylesdir', '')}"#{short_tag_slash_local}>)
- end
- end
-
- case node.attr 'source-highlighter'
- when 'coderay'
- if (node.attr 'coderay-css', 'class') == 'class'
- if linkcss
- result_buffer << %(<link rel="stylesheet" href="#{node.normalize_web_path 'asciidoctor-coderay.css', (node.attr 'stylesdir', '')}"#{short_tag_slash_local}>)
- else
- result_buffer << %(<style>
-#{HTML5.default_coderay_stylesheet}
-</style>)
- end
- end
- when 'pygments'
- if (node.attr 'pygments-css', 'class') == 'class'
- if linkcss
- result_buffer << %(<link rel="stylesheet" href="#{node.normalize_web_path 'asciidoctor-pygments.css', (node.attr 'stylesdir', '')}"#{short_tag_slash_local}>)
- else
- result_buffer << %(<style>
-#{HTML5.pygments_stylesheet(node.attr 'pygments-style')}
-</style>)
- end
- end
- when 'highlightjs', 'highlight.js'
- result_buffer << %(<link rel="stylesheet" href="#{node.attr 'highlightjsdir', 'http://cdnjs.cloudflare.com/ajax/libs/highlight.js/7.4'}/styles/#{node.attr 'highlightjs-theme', 'googlecode'}.min.css"#{short_tag_slash_local}>
-<script src="#{node.attr 'highlightjsdir', 'http://cdnjs.cloudflare.com/ajax/libs/highlight.js/7.4'}/highlight.min.js"></script>
-<script src="#{node.attr 'highlightjsdir', 'http://cdnjs.cloudflare.com/ajax/libs/highlight.js/7.4'}/lang/common.min.js"></script>
-<script>hljs.initHighlightingOnLoad()</script>)
- when 'prettify'
- result_buffer << %(<link rel="stylesheet" href="#{node.attr 'prettifydir', 'http://cdnjs.cloudflare.com/ajax/libs/prettify/r298'}/#{node.attr 'prettify-theme', 'prettify'}.min.css"#{short_tag_slash_local}>
-<script src="#{node.attr 'prettifydir', 'http://cdnjs.cloudflare.com/ajax/libs/prettify/r298'}/prettify.min.js"></script>
-<script>document.addEventListener('DOMContentLoaded', prettyPrint)</script>)
- end
-
- if node.attr? 'math'
- result_buffer << %(<script type="text/x-mathjax-config">
-MathJax.Hub.Config({
- tex2jax: {
- inlineMath: [#{INLINE_MATH_DELIMITERS[:latexmath]}],
- displayMath: [#{BLOCK_MATH_DELIMITERS[:latexmath]}],
- ignoreClass: "nomath|nolatexmath"
- },
- asciimath2jax: {
- delimiters: [#{BLOCK_MATH_DELIMITERS[:asciimath]}],
- ignoreClass: "nomath|noasciimath"
- }
-});
-</script>
-<script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_HTMLorMML"></script>
-<script>document.addEventListener('DOMContentLoaded', MathJax.Hub.TypeSet)</script>)
- end
-
- unless (docinfo_content = node.docinfo).empty?
- result_buffer << docinfo_content
- end
-
- result_buffer << '</head>'
- body_attrs = []
- if node.id
- body_attrs << %(id="#{node.id}")
- end
- if (node.attr? 'toc-class') && (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
- body_attrs << %(class="#{node.doctype} #{node.attr 'toc-class'} toc-#{node.attr 'toc-position', 'left'}")
- else
- body_attrs << %(class="#{node.doctype}")
- end
- if node.attr? 'max-width'
- body_attrs << %(style="max-width: #{node.attr 'max-width'};")
- end
- result_buffer << %(<body #{body_attrs * ' '}>)
-
- unless node.noheader
- result_buffer << '<div id="header">'
- if node.doctype == 'manpage'
- result_buffer << %(<h1>#{node.doctitle} Manual Page</h1>)
- if (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
- result_buffer << %(<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
-<div id="toctitle">#{node.attr 'toc-title'}</div>
-#{DocumentTemplate.outline node, (node.attr 'toclevels', 2).to_i}
-</div>)
- end
- result_buffer << %(<h2>#{node.attr 'manname-title'}</h2>
-<div class="sectionbody">
-<p>#{node.attr 'manname'} - #{node.attr 'manpurpose'}</p>
-</div>)
- else
- if node.has_header?
- result_buffer << %(<h1>#{node.header.title}</h1>) unless node.notitle
- if node.attr? 'author'
- result_buffer << %(<span id="author" class="author">#{node.attr 'author'}</span>#{br})
- if node.attr? 'email'
- result_buffer << %(<span id="email" class="email">#{node.sub_macros(node.attr 'email')}</span>#{br})
- end
- if (authorcount = (node.attr 'authorcount').to_i) > 1
- (2..authorcount).each do |idx|
- result_buffer << %(<span id="author#{idx}" class="author">#{node.attr "author_#{idx}"}</span>#{br})
- if node.attr? %(email_#{idx})
- result_buffer << %(<span id="email#{idx}" class="email">#{node.sub_macros(node.attr "email_#{idx}")}</span>#{br})
- end
- end
- end
- end
- if node.attr? 'revnumber'
- result_buffer << %(<span id="revnumber">#{((node.attr 'version-label') || '').downcase} #{node.attr 'revnumber'}#{(node.attr? 'revdate') ? ',' : ''}</span>)
- end
- if node.attr? 'revdate'
- result_buffer << %(<span id="revdate">#{node.attr 'revdate'}</span>)
- end
- if node.attr? 'revremark'
- result_buffer << %(#{br}<span id="revremark">#{node.attr 'revremark'}</span>)
- end
- end
-
- if (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
- result_buffer << %(<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
-<div id="toctitle">#{node.attr 'toc-title'}</div>
-#{DocumentTemplate.outline node, (node.attr 'toclevels', 2).to_i}
-</div>)
- end
- end
- result_buffer << '</div>'
- end
-
- result_buffer << %(<div id="content">
-#{node.content}
-</div>)
-
- if node.footnotes? && !(node.attr? 'nofootnotes')
- result_buffer << %(<div id="footnotes">
-<hr#{short_tag_slash_local}>)
- node.footnotes.each do |footnote|
- result_buffer << %(<div class="footnote" id="_footnote_#{footnote.index}">
-<a href="#_footnoteref_#{footnote.index}">#{footnote.index}</a>. #{footnote.text}
-</div>)
- end
- result_buffer << '</div>'
- end
- unless node.nofooter
- result_buffer << '<div id="footer">'
- result_buffer << '<div id="footer-text">'
- if node.attr? 'revnumber'
- result_buffer << %(#{node.attr 'version-label'} #{node.attr 'revnumber'}#{br})
- end
- if node.attr? 'last-update-label'
- result_buffer << %(#{node.attr 'last-update-label'} #{node.attr 'docdatetime'})
- end
- result_buffer << '</div>'
- unless (docinfo_content = node.docinfo :footer).empty?
- result_buffer << docinfo_content
- end
- result_buffer << '</div>'
- end
-
- result_buffer << '</body>'
- result_buffer << '</html>'
- result_buffer * EOL
- end
-
- def template
- # FIXME remove need for this special case!!
- :invoke_result_document
- end
-end
-
-class EmbeddedTemplate < BaseTemplate
- def result(node)
- result_buffer = []
- if !node.notitle && node.has_header?
- id_attr = node.id ? %( id="#{node.id}") : nil
- result_buffer << %(<h1#{id_attr}>#{node.header.title}</h1>)
- end
-
- result_buffer << node.content
-
- if node.footnotes? && !(node.attr? 'nofootnotes')
- result_buffer << %(<div id="footnotes">
-<hr#{node.short_tag_slash}>)
- node.footnotes.each do |footnote|
- result_buffer << %(<div class="footnote" id="_footnote_#{footnote.index}">
-<a href="#_footnoteref_#{footnote.index}">#{footnote.index}</a> #{footnote.text}
-</div>)
- end
-
- result_buffer << '</div>'
- end
-
- result_buffer * EOL
- end
-
- def template
- :invoke_result_document
- end
-end
-
-class BlockTocTemplate < BaseTemplate
- def result(node)
- doc = node.document
-
- return '' unless (doc.attr? 'toc')
-
- if node.id
- id_attr = %( id="#{node.id}")
- title_id_attr = ''
- elsif doc.embedded? || !(doc.attr? 'toc-placement')
- id_attr = ' id="toc"'
- title_id_attr = ' id="toctitle"'
- else
- id_attr = ''
- title_id_attr = ''
- end
- title = node.title? ? node.title : (doc.attr 'toc-title')
- levels = (node.attr? 'levels') ? (node.attr 'levels').to_i : (doc.attr 'toclevels', 2).to_i
- role = node.role? ? node.role : (doc.attr 'toc-class', 'toc')
-
- %(<div#{id_attr} class="#{role}">
-<div#{title_id_attr} class="title">#{title}</div>
-#{DocumentTemplate.outline(doc, levels)}
-</div>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockPreambleTemplate < BaseTemplate
- def toc(node)
- if (node.attr? 'toc') && (node.attr? 'toc-placement', 'preamble')
- %(\n<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
-<div id="toctitle">#{node.attr 'toc-title'}</div>
-#{DocumentTemplate.outline(node.document, (node.attr 'toclevels', 2).to_i)}
-</div>)
- else
- ''
- end
- end
-
- def result(node)
- %(<div id="preamble">
-<div class="sectionbody">
-#{node.content}
-</div>#{toc node}
-</div>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class SectionTemplate < BaseTemplate
- def result(sec)
- slevel = sec.level
- # QUESTION should this check be done in section?
- if slevel == 0 && sec.special
- slevel = 1
- end
- htag = %(h#{slevel + 1})
- id_attr = anchor = link_start = link_end = nil
- if sec.id
- id_attr = %( id="#{sec.id}")
- if sec.document.attr? 'sectanchors'
- #if sec.document.attr? 'icons', 'font'
- # anchor = %(<a class="anchor" href="##{sec.id}"><i class="icon-anchor"></i></a>)
- #else
- anchor = %(<a class="anchor" href="##{sec.id}"></a>)
- #end
- elsif sec.document.attr? 'sectlinks'
- link_start = %(<a class="link" href="##{sec.id}">)
- link_end = '</a>'
- end
- end
-
- if slevel == 0
- %(<h1#{id_attr} class="sect0">#{anchor}#{link_start}#{sec.title}#{link_end}</h1>
-#{sec.content})
- else
- class_attr = (role = sec.role) ? %( class="sect#{slevel} #{role}") : %( class="sect#{slevel}")
- sectnum = nil
- if sec.numbered && !sec.caption && slevel <= (sec.document.attr 'sectnumlevels', 3).to_i
- sectnum = %(#{sec.sectnum} )
- end
-
- %(<div#{class_attr}>
-<#{htag}#{id_attr}>#{anchor}#{link_start}#{sectnum}#{sec.captioned_title}#{link_end}</#{htag}>
-#{slevel == 1 ? %[<div class="sectionbody">#{sec.content}</div>] : sec.content}
-</div>)
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockFloatingTitleTemplate < BaseTemplate
- def result(node)
- tag_name = %(h#{node.level + 1})
- id_attribute = node.id ? %( id="#{node.id}") : nil
- classes = [node.style, node.role].compact
- %(<#{tag_name}#{id_attribute} class="#{classes * ' '}">#{node.title}</#{tag_name}>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockDlistTemplate < BaseTemplate
- def result(node)
- result_buffer = []
- id_attribute = node.id ? %( id="#{node.id}") : nil
-
- case node.style
- when 'qanda'
- classes = ['qlist', 'qanda', node.role].compact
- when 'horizontal'
- classes = ['hdlist', node.role].compact
- else
- classes = ['dlist', node.style, node.role].compact
- end
-
- class_attribute = %( class="#{classes * ' '}")
-
- result_buffer << %(<div#{id_attribute}#{class_attribute}>)
- result_buffer << %(<div class="title">#{node.title}</div>) if node.title?
- case node.style
- when 'qanda'
- result_buffer << '<ol>'
- node.items.each do |terms, dd|
- result_buffer << '<li>'
- [*terms].each do |dt|
- result_buffer << %(<p><em>#{dt.text}</em></p>)
- end
- unless dd.nil?
- result_buffer << %(<p>#{dd.text}</p>) if dd.text?
- result_buffer << dd.content if dd.blocks?
- end
- result_buffer << '</li>'
- end
- result_buffer << '</ol>'
- when 'horizontal'
- short_tag_slash_local = node.short_tag_slash
- result_buffer << '<table>'
- if (node.attr? 'labelwidth') || (node.attr? 'itemwidth')
- result_buffer << '<colgroup>'
- col_style_attribute = (node.attr? 'labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : nil
- result_buffer << %(<col#{col_style_attribute}#{short_tag_slash_local}>)
- col_style_attribute = (node.attr? 'itemwidth') ? %( style="width: #{(node.attr 'itemwidth').chomp '%'}%;") : nil
- result_buffer << %(<col#{col_style_attribute}#{short_tag_slash_local}>)
- result_buffer << '</colgroup>'
- end
- node.items.each do |terms, dd|
- result_buffer << '<tr>'
- result_buffer << %(<td class="hdlist1#{(node.option? 'strong') ? ' strong' : nil}">)
- terms_array = [*terms]
- last_term = terms_array[-1]
- terms_array.each do |dt|
- result_buffer << dt.text
- result_buffer << %(<br#{short_tag_slash_local}>) if dt != last_term
- end
- result_buffer << '</td>'
- result_buffer << '<td class="hdlist2">'
- unless dd.nil?
- result_buffer << %(<p>#{dd.text}</p>) if dd.text?
- result_buffer << dd.content if dd.blocks?
- end
- result_buffer << '</td>'
- result_buffer << '</tr>'
- end
- result_buffer << '</table>'
- else
- result_buffer << '<dl>'
- dt_style_attribute = node.style.nil? ? ' class="hdlist1"' : nil
- node.items.each do |terms, dd|
- [*terms].each do |dt|
- result_buffer << %(<dt#{dt_style_attribute}>#{dt.text}</dt>)
- end
- unless dd.nil?
- result_buffer << '<dd>'
- result_buffer << %(<p>#{dd.text}</p>) if dd.text?
- result_buffer << dd.content if dd.blocks?
- result_buffer << '</dd>'
- end
- end
- result_buffer << '</dl>'
- end
-
- result_buffer << '</div>'
- result_buffer * EOL
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockListingTemplate < BaseTemplate
- def result(node)
- nowrap = !(node.document.attr? 'prewrap') || (node.option? 'nowrap')
- if node.style == 'source'
- language = node.attr 'language'
- language_classes = language ? %(#{language} language-#{language}) : nil
- case node.attr 'source-highlighter'
- when 'coderay'
- pre_class = nowrap ? ' class="CodeRay nowrap"' : ' class="CodeRay"'
- code_class = language ? %( class="#{language_classes}") : nil
- when 'pygments'
- pre_class = nowrap ? ' class="pygments highlight nowrap"' : ' class="pygments highlight"'
- code_class = language ? %( class="#{language_classes}") : nil
- when 'highlightjs', 'highlight.js'
- pre_class = nowrap ? ' class="highlight nowrap"' : ' class="highlight"'
- code_class = language ? %( class="#{language_classes}") : nil
- when 'prettify'
- pre_class = %( class="prettyprint#{nowrap ? ' nowrap' : nil}#{(node.attr? 'linenums') ? ' linenums' : nil})
- pre_class = language ? %(#{pre_class} #{language_classes}") : %(#{pre_class}")
- code_class = nil
- when 'html-pipeline'
- pre_class = language ? %( lang="#{language}") : nil
- code_class = nil
- else
- pre_class = nowrap ? ' class="highlight nowrap"' : ' class="highlight"'
- code_class = language ? %( class="#{language_classes}") : nil
- end
- pre = %(<pre#{pre_class}><code#{code_class}>#{preserve_endlines(node.content, node)}</code></pre>)
- else
- pre = %(<pre#{nowrap ? ' class="nowrap"' : nil}>#{preserve_endlines(node.content, node)}</pre>)
- end
-
- %(<div#{node.id && " id=\"#{node.id}\""} class="listingblock#{node.role && " #{node.role}"}">#{node.title? ? "
-<div class=\"title\">#{node.captioned_title}</div>" : nil}
-<div class="content">
-#{pre}
-</div>
-</div>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockLiteralTemplate < BaseTemplate
- def result(node)
- nowrap = !(node.document.attr? 'prewrap') || (node.option? 'nowrap')
- %(<div#{node.id && " id=\"#{node.id}\""} class="literalblock#{node.role && " #{node.role}"}">#{node.title? ? "
-<div class=\"title\">#{node.title}</div>" : nil}
-<div class="content">
-<pre#{nowrap ? ' class="nowrap"' : nil}>#{preserve_endlines(node.content, node)}</pre>
-</div>
-</div>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockAdmonitionTemplate < BaseTemplate
- def result(node)
- id_attr = node.id ? %( id="#{node.id}") : nil
- title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : nil
- name = node.attr 'name'
- caption = if node.document.attr? 'icons'
- if node.document.attr? 'icons', 'font'
- %(<i class="icon-#{name}" title="#{node.caption}"></i>)
- else
- %(<img src="#{node.icon_uri name}" alt="#{node.caption}"#{node.short_tag_slash}>)
- end
- else
- %(<div class="title">#{node.caption}</div>)
- end
- %(<div#{id_attr} class="admonitionblock #{name}#{(role = node.role) && " #{role}"}">
-<table>
-<tr>
-<td class="icon">
-#{caption}
-</td>
-<td class="content">
-#{title_element}#{node.content}
-</td>
-</tr>
-</table>
-</div>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockParagraphTemplate < BaseTemplate
- def result(node)
- id_attr = node.id ? %( id="#{node.id}") : nil
- title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : nil
- class_attr = (role = node.role) ? %( class="paragraph #{role}") : ' class="paragraph"'
- %(<div#{id_attr}#{class_attr}>
-#{title_element}<p>#{node.content}</p>
-</div>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockSidebarTemplate < BaseTemplate
- def result(node)
- id_attribute = node.id ? %( id="#{node.id}") : nil
- title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : nil
-
- %(<div#{id_attribute} class="#{!node.role? ? 'sidebarblock' : ['sidebarblock', node.role] * ' '}">
-<div class="content">
-#{title_element}#{node.content}
-</div>
-</div>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockExampleTemplate < BaseTemplate
- def result(node)
- id_attribute = node.id ? %( id="#{node.id}") : nil
- title_element = node.title? ? %(<div class="title">#{node.captioned_title}</div>\n) : nil
-
- %(<div#{id_attribute} class="#{!node.role? ? 'exampleblock' : ['exampleblock', node.role] * ' '}">
-#{title_element}<div class="content">
-#{node.content}
-</div>
-</div>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockOpenTemplate < BaseTemplate
- def result(node)
- open_block(node, node.id, node.style, node.role, node.title? ? node.title : nil, node.content)
- end
-
- def open_block(node, id, style, role, title, content)
- if style == 'abstract'
- if node.parent == node.document && node.document.doctype == 'book'
- warn 'asciidoctor: WARNING: abstract block cannot be used in a document without a title when doctype is book. Excluding block content.'
- ''
- else
- %(<div#{id && " id=\"#{id}\""} class="quoteblock abstract#{role && " #{role}"}">#{title && "
-<div class=\"title\">#{title}</div>"}
-<blockquote>
-#{content}
-</blockquote>
-</div>)
- end
- elsif style == 'partintro' && (node.level != 0 || node.parent.context != :section || node.document.doctype != 'book')
- warn 'asciidoctor: ERROR: partintro block can only be used when doctype is book and it\'s a child of a book part. Excluding block content.'
- ''
- else
- %(<div#{id && " id=\"#{id}\""} class="openblock#{style && style != 'open' ? " #{style}" : ''}#{role && " #{role}"}">#{title && "
-<div class=\"title\">#{title}</div>"}
-<div class="content">
-#{content}
-</div>
-</div>)
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockPassTemplate < BaseTemplate
- def template
- :content
- end
-end
-
-class BlockMathTemplate < BaseTemplate
- def result node
- id_attribute = node.id ? %( id="#{node.id}") : nil
- title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : nil
- open, close = BLOCK_MATH_DELIMITERS[node.style.to_sym]
- equation = node.content.strip
- if (node.subs.nil? || node.subs.empty?) && !(node.attr? 'subs')
- equation = node.sub_specialcharacters(equation)
- end
-
- unless (equation.start_with? open) && (equation.end_with? close)
- equation = %(#{open}#{equation}#{close})
- end
-
- %(<div#{id_attribute} class="#{node.role? ? ['mathblock', node.role] * ' ' : 'mathblock'}">
-#{title_element}<div class="content">
-#{equation}
-</div>
-</div>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockQuoteTemplate < BaseTemplate
- def result(node)
- id_attribute = node.id ? %( id="#{node.id}") : nil
- classes = ['quoteblock', node.role].compact
- class_attribute = %( class="#{classes * ' '}")
- title_element = node.title? ? %(\n<div class="title">#{node.title}</div>) : nil
- attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil
- citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil
- if attribution || citetitle
- cite_element = citetitle ? %(<cite>#{citetitle}</cite>) : nil
- attribution_text = attribution ? %(#{citetitle ? "<br#{node.short_tag_slash}>\n" : nil}&#8212; #{attribution}) : nil
- attribution_element = %(\n<div class="attribution">\n#{cite_element}#{attribution_text}\n</div>)
- else
- attribution_element = nil
- end
-
- %(<div#{id_attribute}#{class_attribute}>#{title_element}
-<blockquote>
-#{node.content}
-</blockquote>#{attribution_element}
-</div>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockVerseTemplate < BaseTemplate
- def result(node)
- id_attribute = node.id ? %( id="#{node.id}") : nil
- classes = ['verseblock', node.role].compact
- class_attribute = %( class="#{classes * ' '}")
- title_element = node.title? ? %(\n<div class="title">#{node.title}</div>) : nil
- attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil
- citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil
- if attribution || citetitle
- cite_element = citetitle ? %(<cite>#{citetitle}</cite>) : nil
- attribution_text = attribution ? %(#{citetitle ? "<br#{node.short_tag_slash}>\n" : nil}&#8212; #{attribution}) : nil
- attribution_element = %(\n<div class="attribution">\n#{cite_element}#{attribution_text}\n</div>)
- else
- attribution_element = nil
- end
-
- %(<div#{id_attribute}#{class_attribute}>#{title_element}
-<pre class="content">#{preserve_endlines node.content, node}</pre>#{attribution_element}
-</div>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockUlistTemplate < BaseTemplate
- def result(node)
- result_buffer = []
- id_attribute = node.id ? %( id="#{node.id}") : nil
- div_classes = ['ulist', node.style, node.role].compact
- marker_checked = nil
- marker_unchecked = nil
- if (checklist = (node.option? 'checklist'))
- div_classes.insert(1, 'checklist')
- ul_class_attribute = ' class="checklist"'
- if node.option? 'interactive'
- if node.document.attr? 'htmlsyntax', 'xml'
- marker_checked = '<input type="checkbox" data-item-complete="1" checked="checked"/> '
- marker_unchecked = '<input type="checkbox" data-item-complete="0"/> '
- else
- marker_checked = '<input type="checkbox" data-item-complete="1" checked> '
- marker_unchecked = '<input type="checkbox" data-item-complete="0"> '
- end
- else
- if node.document.attr? 'icons', 'font'
- marker_checked = '<i class="icon-check"></i> '
- marker_unchecked = '<i class="icon-check-empty"></i> '
- else
- marker_checked = '&#10003; '
- marker_unchecked = '&#10063; '
- end
- end
- elsif !node.style.nil?
- ul_class_attribute = %( class="#{node.style}")
- else
- ul_class_attribute = nil
- end
- div_class_attribute = %( class="#{div_classes * ' '}")
- result_buffer << %(<div#{id_attribute}#{div_class_attribute}>)
- result_buffer << %(<div class="title">#{node.title}</div>) if node.title?
- result_buffer << %(<ul#{ul_class_attribute}>)
-
- node.items.each do |item|
- result_buffer << '<li>'
- if checklist && (item.attr? 'checkbox')
- result_buffer << %(<p>#{(item.attr? 'checked') ? marker_checked : marker_unchecked}#{item.text}</p>)
- else
- result_buffer << %(<p>#{item.text}</p>)
- end
- result_buffer << item.content if item.blocks?
- result_buffer << '</li>'
- end
-
- result_buffer << '</ul>'
- result_buffer << '</div>'
-
- result_buffer * EOL
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockOlistTemplate < BaseTemplate
- def result(node)
- result_buffer = []
- id_attribute = node.id ? %( id="#{node.id}") : nil
- classes = ['olist', node.style, node.role].compact
- class_attribute = %( class="#{classes * ' '}")
-
- result_buffer << %(<div#{id_attribute}#{class_attribute}>)
- result_buffer << %(<div class="title">#{node.title}</div>) if node.title?
-
- type_attribute = (keyword = node.list_marker_keyword) ? %( type="#{keyword}") : nil
- start_attribute = (node.attr? 'start') ? %( start="#{node.attr 'start'}") : nil
- result_buffer << %(<ol class="#{node.style}"#{type_attribute}#{start_attribute}>)
-
- node.items.each do |item|
- result_buffer << '<li>'
- result_buffer << %(<p>#{item.text}</p>)
- result_buffer << item.content if item.blocks?
- result_buffer << '</li>'
- end
-
- result_buffer << '</ol>'
- result_buffer << '</div>'
-
- result_buffer * EOL
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockColistTemplate < BaseTemplate
- def result(node)
- result_buffer = []
- id_attribute = node.id ? %( id="#{node.id}") : nil
- classes = ['colist', node.style, node.role].compact
- class_attribute = %( class="#{classes * ' '}")
-
- result_buffer << %(<div#{id_attribute}#{class_attribute}>)
- result_buffer << %(<div class="title">#{node.title}</div>) if node.title?
-
- if node.document.attr? 'icons'
- result_buffer << '<table>'
-
- font_icons = node.document.attr? 'icons', 'font'
- node.items.each_with_index do |item, i|
- num = i + 1
- num_element = font_icons ?
- %(<i class="conum" data-value="#{num}"></i><b>#{num}</b>) :
- %(<img src="#{node.icon_uri "callouts/#{num}"}" alt="#{num}"#{node.short_tag_slash}>)
- result_buffer << %(<tr>
-<td>#{num_element}</td>
-<td>#{item.text}</td>
-</tr>)
- end
-
- result_buffer << '</table>'
- else
- result_buffer << '<ol>'
- node.items.each do |item|
- result_buffer << %(<li>
-<p>#{item.text}</p>
-</li>)
- end
- result_buffer << '</ol>'
- end
-
- result_buffer << '</div>'
- result_buffer * EOL
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockTableTemplate < BaseTemplate
- def result(node)
- result_buffer = []
- id_attribute = node.id ? %( id="#{node.id}") : nil
- classes = ['tableblock', %(frame-#{node.attr 'frame', 'all'}), %(grid-#{node.attr 'grid', 'all'})]
- if (role_class = node.role)
- classes << role_class
- end
- class_attribute = %( class="#{classes * ' '}")
- styles = [(node.option? 'autowidth') ? nil : %(width: #{node.attr 'tablepcwidth'}%;), (node.attr? 'float') ? %(float: #{node.attr 'float'};) : nil].compact
- if styles.size > 0
- style_attribute = %( style="#{styles * ' '}")
- else
- style_attribute = nil
- end
-
- result_buffer << %(<table#{id_attribute}#{class_attribute}#{style_attribute}>)
- if node.title?
- result_buffer << %(<caption class="title">#{node.captioned_title}</caption>)
- end
- if (node.attr 'rowcount') > 0
- result_buffer << '<colgroup>'
- if node.option? 'autowidth'
- tag = %(<col#{node.short_tag_slash}>)
- node.columns.size.times do
- result_buffer << tag
- end
- else
- short_tag_slash_local = node.short_tag_slash
- node.columns.each do |col|
- result_buffer << %(<col style="width: #{col.attr 'colpcwidth'}%;"#{short_tag_slash_local}>)
- end
- end
- result_buffer << '</colgroup>'
- [:head, :foot, :body].select {|tsec| !node.rows[tsec].empty? }.each do |tsec|
- result_buffer << %(<t#{tsec}>)
- node.rows[tsec].each do |row|
- result_buffer << '<tr>'
- row.each do |cell|
- if tsec == :head
- cell_content = cell.text
- else
- case cell.style
- when :asciidoc
- cell_content = %(<div>#{cell.content}</div>)
- when :verse
- cell_content = %(<div class="verse">#{preserve_endlines cell.text, node}</div>)
- when :literal
- cell_content = %(<div class="literal"><pre>#{preserve_endlines cell.text, node}</pre></div>)
- else
- cell_content = ''
- cell.content.each do |text|
- cell_content = %(#{cell_content}<p class="tableblock">#{text}</p>)
- end
- end
- end
-
- cell_tag_name = (tsec == :head || cell.style == :header ? 'th' : 'td')
- cell_class_attribute = %( class="tableblock halign-#{cell.attr 'halign'} valign-#{cell.attr 'valign'}")
- cell_colspan_attribute = cell.colspan ? %( colspan="#{cell.colspan}") : nil
- cell_rowspan_attribute = cell.rowspan ? %( rowspan="#{cell.rowspan}") : nil
- cell_style_attribute = (node.document.attr? 'cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'};") : nil
- result_buffer << %(<#{cell_tag_name}#{cell_class_attribute}#{cell_colspan_attribute}#{cell_rowspan_attribute}#{cell_style_attribute}>#{cell_content}</#{cell_tag_name}>)
- end
- result_buffer << '</tr>'
- end
- result_buffer << %(</t#{tsec}>)
- end
- end
- result_buffer << %(</table>)
- result_buffer * EOL
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockImageTemplate < BaseTemplate
- def image(target, alt, title, link, node)
- align = (node.attr? 'align') ? (node.attr 'align') : nil
- float = (node.attr? 'float') ? (node.attr 'float') : nil
- if align || float
- styles = [align ? %(text-align: #{align}) : nil, float ? %(float: #{float}) : nil].compact
- style_attribute = %( style="#{styles * ';'}")
- else
- style_attribute = nil
- end
-
- width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil
- height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil
-
- img_element = %(<img src="#{node.image_uri target}" alt="#{alt}"#{width_attribute}#{height_attribute}#{node.short_tag_slash}>)
- if link
- img_element = %(<a class="image" href="#{link}">#{img_element}</a>)
- end
- id_attribute = node.id ? %( id="#{node.id}") : nil
- classes = ['imageblock', node.style, node.role].compact
- class_attribute = %( class="#{classes * ' '}")
- title_element = title ? %(\n<div class="title">#{title}</div>) : nil
-
- %(<div#{id_attribute}#{class_attribute}#{style_attribute}>
-<div class="content">
-#{img_element}
-</div>#{title_element}
-</div>)
- end
-
- def result(node)
- image(node.attr('target'), node.attr('alt'), node.title? ? node.captioned_title : nil, node.attr('link'), node)
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockAudioTemplate < BaseTemplate
- def result(node)
- xml = node.document.attr? 'htmlsyntax', 'xml'
- id_attribute = node.id ? %( id="#{node.id}") : nil
- classes = ['audioblock', node.style, node.role].compact
- class_attribute = %( class="#{classes * ' '}")
- title_element = node.title? ? %(\n<div class="title">#{node.captioned_title}</div>) : nil
- %(<div#{id_attribute}#{class_attribute}>#{title_element}
-<div class="content">
-<audio src="#{node.media_uri(node.attr 'target')}"#{(node.option? 'autoplay') ? (boolean_attribute 'autoplay', xml) : nil}#{(node.option? 'nocontrols') ? nil : (boolean_attribute 'controls', xml)}#{(node.option? 'loop') ? (boolean_attribute 'loop', xml) : nil}>
-Your browser does not support the audio tag.
-</audio>
-</div>
-</div>)
- end
-
- def boolean_attribute name, xml
- xml ? %( #{name}="#{name}") : %( #{name})
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockVideoTemplate < BaseTemplate
- def result(node)
- xml = node.document.attr? 'htmlsyntax', 'xml'
- id_attribute = node.id ? %( id="#{node.id}") : nil
- classes = ['videoblock', node.style, node.role].compact
- class_attribute = %( class="#{classes * ' '}")
- title_element = node.title? ? %(\n<div class="title">#{node.captioned_title}</div>) : nil
- width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil
- height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil
- case node.attr 'poster'
- when 'vimeo'
- start_anchor = (node.attr? 'start') ? "#at=#{node.attr 'start'}" : nil
- delimiter = '?'
- autoplay_param = (node.option? 'autoplay') ? "#{delimiter}autoplay=1" : nil
- delimiter = '&amp;' if autoplay_param
- loop_param = (node.option? 'loop') ? "#{delimiter}loop=1" : nil
- %(<div#{id_attribute}#{class_attribute}>#{title_element}
-<div class="content">
-<iframe#{width_attribute}#{height_attribute} src="//player.vimeo.com/video/#{node.attr 'target'}#{start_anchor}#{autoplay_param}#{loop_param}" frameborder="0"#{boolean_attribute 'webkitAllowFullScreen', xml}#{boolean_attribute 'mozallowfullscreen', xml}#{boolean_attribute 'allowFullScreen', xml}></iframe>
-</div>
-</div>)
- when 'youtube'
- start_param = (node.attr? 'start') ? "&amp;start=#{node.attr 'start'}" : nil
- end_param = (node.attr? 'end') ? "&amp;end=#{node.attr 'end'}" : nil
- autoplay_param = (node.option? 'autoplay') ? '&amp;autoplay=1' : nil
- loop_param = (node.option? 'loop') ? '&amp;loop=1' : nil
- controls_param = (node.option? 'nocontrols') ? '&amp;controls=0' : nil
- %(<div#{id_attribute}#{class_attribute}>#{title_element}
-<div class="content">
-<iframe#{width_attribute}#{height_attribute} src="//www.youtube.com/embed/#{node.attr 'target'}?rel=0#{start_param}#{end_param}#{autoplay_param}#{loop_param}#{controls_param}" frameborder="0"#{(node.option? 'nofullscreen') ? nil : (boolean_attribute 'allowfullscreen', xml)}></iframe>
-</div>
-</div>)
- else
- poster_attribute = %(#{poster = node.attr 'poster'}).empty? ? nil : %( poster="#{node.media_uri poster}")
- time_anchor = ((node.attr? 'start') || (node.attr? 'end')) ? %(#t=#{node.attr 'start'}#{(node.attr? 'end') ? ',' : nil}#{node.attr 'end'}) : nil
- %(<div#{id_attribute}#{class_attribute}>#{title_element}
-<div class="content">
-<video src="#{node.media_uri(node.attr 'target')}#{time_anchor}"#{width_attribute}#{height_attribute}#{poster_attribute}#{(node.option? 'autoplay') ? (boolean_attribute 'autoplay', xml) : nil}#{(node.option? 'nocontrols') ? nil : (boolean_attribute 'controls', xml)}#{(node.option? 'loop') ? (boolean_attribute 'loop', xml) : nil}>
-Your browser does not support the video tag.
-</video>
-</div>
-</div>)
- end
- end
-
- def boolean_attribute name, xml
- xml ? %( #{name}="#{name}") : %( #{name})
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockRulerTemplate < BaseTemplate
- def result(node)
- (node.document.attr? 'htmlsyntax', 'xml') ? '<hr/>' : '<hr>'
- end
-
- def template
- :invoke_result
- end
-end
-
-class BlockPageBreakTemplate < BaseTemplate
- def result(node)
- '<div style="page-break-after: always;"></div>'
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineBreakTemplate < BaseTemplate
- def result(node)
- (node.document.attr? 'htmlsyntax', 'xml') ? %(#{node.text}<br/>) : %(#{node.text}<br>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineCalloutTemplate < BaseTemplate
- def result(node)
- if node.document.attr? 'icons', 'font'
- %(<i class="conum" data-value="#{node.text}"></i><b>(#{node.text})</b>)
- elsif node.document.attr? 'icons'
- src = node.icon_uri("callouts/#{node.text}")
- %(<img src="#{src}" alt="#{node.text}"#{node.short_tag_slash}>)
- else
- "<b>(#{node.text})</b>"
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineQuotedTemplate < BaseTemplate
- QUOTE_TAGS = {
- :emphasis => ['<em>', '</em>', true],
- :strong => ['<strong>', '</strong>', true],
- :monospaced => ['<code>', '</code>', true],
- :superscript => ['<sup>', '</sup>', true],
- :subscript => ['<sub>', '</sub>', true],
- :double => ['&#8220;', '&#8221;', false],
- :single => ['&#8216;', '&#8217;', false],
- :asciimath => INLINE_MATH_DELIMITERS[:asciimath] + [false],
- :latexmath => INLINE_MATH_DELIMITERS[:latexmath] + [false]
- }
- QUOTE_TAGS.default = [nil, nil, nil]
-
- def result node
- open, close, is_tag = QUOTE_TAGS[node.type]
- quoted_text = if (role = node.role)
- if is_tag
- %(#{open.chop} class="#{role}">#{node.text}#{close})
- else
- %(<span class="#{role}">#{open}#{node.text}#{close}</span>)
- end
- else
- %(#{open}#{node.text}#{close})
- end
-
- node.id ? %(<a id="#{node.id}"></a>#{quoted_text}) : quoted_text
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineButtonTemplate < BaseTemplate
- def result(node)
- %(<b class="button">#{node.text}</b>)
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineKbdTemplate < BaseTemplate
- def result(node)
- keys = node.attr 'keys'
- if keys.size == 1
- %(<kbd>#{keys[0]}</kbd>)
- else
- key_combo = keys.map{|key| %(<kbd>#{key}</kbd>+) }.join.chop
- %(<kbd class="keyseq">#{key_combo}</kbd>)
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineMenuTemplate < BaseTemplate
- def menu(menu, submenus, menuitem)
- if !submenus.empty?
- submenu_path = submenus.map{|submenu| %(<span class="submenu">#{submenu}</span>&#160;&#9656; ) }.join.chop
- %(<span class="menuseq"><span class="menu">#{menu}</span>&#160;&#9656; #{submenu_path} <span class="menuitem">#{menuitem}</span></span>)
- elsif !menuitem.nil?
- %(<span class="menuseq"><span class="menu">#{menu}</span>&#160;&#9656; <span class="menuitem">#{menuitem}</span></span>)
- else
- %(<span class="menu">#{menu}</span>)
- end
- end
-
- def result(node)
- menu(node.attr('menu'), node.attr('submenus'), node.attr('menuitem'))
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineAnchorTemplate < BaseTemplate
- def anchor(target, text, type, document, node)
- case type
- when :xref
- refid = (node.attr 'refid') || target
- # FIXME this seems like it should be prepared already
- text ||= (document.references[:ids][refid] || %([#{refid}]))
- %(<a href="#{target}">#{text}</a>)
- when :ref
- %(<a id="#{target}"></a>)
- when :link
- %(<a href="#{target}"#{node.role? ? " class=\"#{node.role}\"" : nil}#{(node.attr? 'window') ? " target=\"#{node.attr 'window'}\"" : nil}>#{text}</a>)
- when :bibref
- %(<a id="#{target}"></a>[#{target}])
- end
- end
-
- def result(node)
- anchor(node.target, node.text, node.type, node.document, node)
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineImageTemplate < BaseTemplate
- def image(target, type, node)
- if type == 'icon' && (node.document.attr? 'icons', 'font')
- style_class = "icon-#{target}"
- if node.attr? 'size'
- style_class = "#{style_class} icon-#{node.attr 'size'}"
- end
- if node.attr? 'rotate'
- style_class = "#{style_class} icon-rotate-#{node.attr 'rotate'}"
- end
- if node.attr? 'flip'
- style_class = "#{style_class} icon-flip-#{node.attr 'flip'}"
- end
- title_attribute = (node.attr? 'title') ? %( title="#{node.attr 'title'}") : nil
- img = %(<i class="#{style_class}"#{title_attribute}></i>)
- elsif type == 'icon' && !(node.document.attr? 'icons')
- img = "[#{node.attr 'alt'}]"
- else
- if type == 'icon'
- resolved_target = node.icon_uri target
- else
- resolved_target = node.image_uri target
- end
-
- attrs = ['alt', 'width', 'height', 'title'].map {|name|
- if node.attr? name
- %( #{name}="#{node.attr name}")
- else
- nil
- end
- }.join
-
- img = %(<img src="#{resolved_target}"#{attrs}#{node.short_tag_slash}>)
- end
-
- if node.attr? 'link'
- img = %(<a class="image" href="#{node.attr 'link'}"#{(node.attr? 'window') ? " target=\"#{node.attr 'window'}\"" : nil}>#{img}</a>)
- end
-
- if node.role?
- style_classes = %(#{type} #{node.role})
- else
- style_classes = type
- end
-
- style_attr = (node.attr? 'float') ? %( style="float: #{node.attr 'float'}") : nil
-
- %(<span class="#{style_classes}"#{style_attr}>#{img}</span>)
- end
-
- def result(node)
- image(node.target, node.type, node)
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineFootnoteTemplate < BaseTemplate
- def result(node)
- if (index = node.attr 'index')
- if node.type == :xref
- %(<span class="footnoteref">[<a class="footnote" href="#_footnote_#{index}" title="View footnote.">#{index}</a>]</span>)
- else
- id_attribute = node.id ? %( id="_footnote_#{node.id}") : nil
- %(<span class="footnote"#{id_attribute}>[<a id="_footnoteref_#{index}" class="footnote" href="#_footnote_#{index}" title="View footnote.">#{index}</a>]</span>)
- end
- else
- if node.type == :xref
- %(<span class="footnoteref red" title="Unresolved footnote reference.">[#{node.text}]</span>)
- end
- end
- end
-
- def template
- :invoke_result
- end
-end
-
-class InlineIndextermTemplate < BaseTemplate
- def result(node)
- node.type == :visible ? node.text : ''
- end
-
- def template
- :invoke_result
- end
-end
-
-end # module HTML5
-end # module Asciidoctor
diff --git a/lib/asciidoctor/block.rb b/lib/asciidoctor/block.rb
index f6fa772c..f7d51f0f 100644
--- a/lib/asciidoctor/block.rb
+++ b/lib/asciidoctor/block.rb
@@ -10,16 +10,16 @@ class Block < AbstractBlock
DEFAULT_CONTENT_MODEL = ::Hash.new(:simple).merge({
# TODO should probably fill in all known blocks
- :audio => :empty,
- :image => :empty,
- :listing => :verbatim,
- :literal => :verbatim,
- :math => :raw,
- :open => :compound,
+ :audio => :empty,
+ :image => :empty,
+ :listing => :verbatim,
+ :literal => :verbatim,
+ :math => :raw,
+ :open => :compound,
:page_break => :empty,
- :pass => :raw,
- :ruler => :empty,
- :video => :empty
+ :pass => :raw,
+ :thematic_break => :empty,
+ :video => :empty
})
# Public: Create alias for context to be consistent w/ AsciiDoc
@@ -42,7 +42,12 @@ class Block < AbstractBlock
def initialize(parent, context, opts = {})
super(parent, context)
@content_model = opts[:content_model] || DEFAULT_CONTENT_MODEL[context]
- @attributes = opts[:attributes] || {}
+ if (attrs = opts[:attributes]).nil_or_empty?
+ @attributes = {}
+ else
+ # QUESTION are we correct in duplicating the attributes (seems to be just as fast)
+ @attributes = attrs.dup
+ end
if opts.has_key? :subs
# FIXME this is a bit funky
# we have to be defensive to avoid lock_in_subs wiping out the override
@@ -63,8 +68,8 @@ class Block < AbstractBlock
end
end
- # Public: Get an rendered version of the block content, performing
- # any substitutions on the content.
+ # Public: Get the converted result of the child blocks by converting the
+ # children appropriate to content model that this block supports.
#
# Examples
#
@@ -82,6 +87,8 @@ class Block < AbstractBlock
when :verbatim, :raw
#((apply_subs @lines.join(EOL), @subs).sub StripLineWiseRx, '\1')
+ # QUESTION could we use strip here instead of popping empty lines?
+ # maybe apply_subs can know how to strip whitespace?
result = apply_subs @lines, @subs
if result.size < 2
result[0]
diff --git a/lib/asciidoctor/callouts.rb b/lib/asciidoctor/callouts.rb
index 075a8d37..c6f9bb71 100644
--- a/lib/asciidoctor/callouts.rb
+++ b/lib/asciidoctor/callouts.rb
@@ -36,8 +36,8 @@ class Callouts
# Public: Get the next callout index in the document
#
# Reads the next callout index in the document and advances the pointer.
- # This method is used during rendering to retrieve the unique id of the
- # callout that was generated during lexing.
+ # This method is used during conversion to retrieve the unique id of the
+ # callout that was generated during parsing.
#
# Returns The unique String id of the next callout in the document
def read_next_id
@@ -86,7 +86,7 @@ class Callouts
end
# Public: Rewind the list index pointer, intended to be used when switching
- # from the parsing to rendering phase.
+ # from the parsing to conversion phase.
#
# Returns nothing
def rewind
diff --git a/lib/asciidoctor/cli/invoker.rb b/lib/asciidoctor/cli/invoker.rb
index 6f419785..585fd408 100644
--- a/lib/asciidoctor/cli/invoker.rb
+++ b/lib/asciidoctor/cli/invoker.rb
@@ -42,7 +42,7 @@ module Asciidoctor
begin
opts = {}
- profile = false
+ show_timings = false
infiles = []
outfile = nil
tofile = nil
@@ -58,7 +58,7 @@ module Asciidoctor
when :attributes
opts[:attributes] = v.dup
when :verbose
- profile = true if v == 2
+ show_timings = true if v == 2
when :trace
# currently, nothing
else
@@ -88,22 +88,11 @@ module Asciidoctor
original_opts = opts
inputs.each do |input|
-
opts = Helpers.clone_options(original_opts) if inputs.size > 1
opts[:to_file] = tofile unless tofile.nil?
- opts[:monitor] = {} if profile
-
- @documents ||= []
- @documents.push ::Asciidoctor.render(input, opts)
-
- if profile
- monitor = opts[:monitor]
- err = (@err || $stderr)
- err.puts "Input file: #{input.respond_to?(:path) ? input.path : '-'}"
- err.puts " Time to read and parse source: #{'%05.5f' % monitor[:parse]}"
- err.puts " Time to render document: #{monitor.has_key?(:render) ? '%05.5f' % monitor[:render] : 'n/a'}"
- err.puts " Total time to read, parse and render: #{'%05.5f' % (monitor[:load_render] || monitor[:parse])}"
- end
+ timings = opts[:timings] = Timings.new if show_timings
+ @documents << (::Asciidoctor.convert input, opts)
+ timings.print_report((@err || $stderr), ((input.respond_to? :path) ? input.path : '-')) if show_timings
end
rescue ::Exception => e
raise e if @options[:trace] || ::SystemExit === e
diff --git a/lib/asciidoctor/cli/options.rb b/lib/asciidoctor/cli/options.rb
index ace634f0..5c6a8249 100644
--- a/lib/asciidoctor/cli/options.rb
+++ b/lib/asciidoctor/cli/options.rb
@@ -19,7 +19,6 @@ module Asciidoctor
self[:attributes]['backend'] = options[:backend]
end
self[:eruby] = options[:eruby] || nil
- self[:compact] = options[:compact] || false
self[:verbose] = options[:verbose] || 1
self[:load_paths] = options[:load_paths] || nil
self[:requires] = options[:requires] || nil
@@ -46,7 +45,7 @@ Example: asciidoctor -b html5 source.asciidoc
self[:attributes]['backend'] = backend
end
opts.on('-d', '--doctype DOCTYPE', ['article', 'book', 'manpage', 'inline'],
- 'document type to use when rendering output: [article, book, manpage, inline] (default: article)') do |doc_type|
+ 'document type to use when converting document: [article, book, manpage, inline] (default: article)') do |doc_type|
self[:attributes]['doctype'] = doc_type
end
opts.on('-o', '--out-file FILE', 'output file (default: based on input file path); use - to output to STDOUT') do |output_file|
@@ -70,11 +69,10 @@ Example: asciidoctor -b html5 source.asciidoc
self[:attributes]['numbered'] = ''
end
opts.on('-e', '--eruby ERUBY', ['erb', 'erubis'],
- 'specify eRuby implementation to render built-in templates: [erb, erubis] (default: erb)') do |eruby|
+ 'specify eRuby implementation to use when rendering custom ERB templates: [erb, erubis] (default: erb)') do |eruby|
self[:eruby] = eruby
end
- opts.on('-C', '--compact', 'compact the output by removing blank lines (default: false)') do
- self[:compact] = true
+ opts.on('-C', '--compact', 'compact the output by removing blank lines. (No longer in use)') do
end
opts.on('-a', '--attribute key[=value],key2[=value2],...', ::Array,
'a list of document attributes to set in the form of key, key! or key=value pair',
@@ -89,7 +87,7 @@ Example: asciidoctor -b html5 source.asciidoc
self[:attributes][key] = val || ''
end
end
- opts.on('-T', '--template-dir DIR', 'a directory containing custom render templates that override the built-in set (requires tilt gem)',
+ opts.on('-T', '--template-dir DIR', 'a directory containing custom converter templates that override the built-in converter (requires tilt gem)',
'may be specified multiple times') do |template_dir|
if self[:template_dirs].nil?
self[:template_dirs] = [template_dir]
@@ -99,7 +97,7 @@ Example: asciidoctor -b html5 source.asciidoc
self[:template_dirs] = [self[:template_dirs], template_dir]
end
end
- opts.on('-E', '--template-engine NAME', 'template engine to use for the custom render templates (loads gem on demand)') do |template_engine|
+ opts.on('-E', '--template-engine NAME', 'template engine to use for the custom converter templates (loads gem on demand)') do |template_engine|
self[:template_engine] = template_engine
end
opts.on('-B', '--base-dir DIR', 'base directory containing the document and resources (default: directory of source file)') do |base_dir|
@@ -186,7 +184,7 @@ Example: asciidoctor -b html5 source.asciidoc
if self[:template_dirs]
begin
- require 'tilt'
+ require 'tilt' unless defined? ::Tilt
rescue ::LoadError
$stderr.puts 'asciidoctor: FAILED: tilt could not be loaded; to use a custom backend, you must have the tilt gem installed (gem install tilt)'
return 1
diff --git a/lib/asciidoctor/converter.rb b/lib/asciidoctor/converter.rb
new file mode 100644
index 00000000..9698dbf1
--- /dev/null
+++ b/lib/asciidoctor/converter.rb
@@ -0,0 +1,151 @@
+module Asciidoctor
+ # A base module for defining converters that can be used to convert {AbstractNode}
+ # objects in a parsed AsciiDoc document to a backend format such as HTML or
+ # DocBook.
+ #
+ # Implementing a converter involves:
+ #
+ # * including this module in a {Converter} implementation class
+ # * overriding the {Converter#convert} method
+ # * optionally associating the converter with one or more backends using
+ # the {#register_for} DSL method imported by the {Config Converter::Config} module
+ #
+ # Examples
+ #
+ # class TextConverter
+ # include Asciidoctor::Converter
+ # register_for 'text'
+ # def convert node, transform = nil
+ # case (transform ||= node.node_name)
+ # when 'document'
+ # node.content
+ # when 'section'
+ # [node.title, node.content] * "\n\n"
+ # when 'paragraph'
+ # node.content.tr("\n", ' ') << "\n"
+ # else
+ # if transform.start_with? 'inline_'
+ # node.text
+ # else
+ # %(<#{transform}>\n)
+ # end
+ # end
+ # end
+ # end
+ #
+ # puts Asciidoctor.convert_file 'sample.adoc', backend: :text
+ module Converter
+ # A module that provides the {#register_for} method for statically
+ # registering a converter with the default {Factory Converter::Factory} instance.
+ module Config
+ # Public: Statically registers the current {Converter} class with the default
+ # {Factory Converter::Factory} to handle conversion to the specified backends.
+ #
+ # This method also defines the converts? method on the class which returns whether
+ # the class is registered to convert a specified backend.
+ #
+ # backends - A String Array of backends with which to associate this {Converter} class.
+ #
+ # Returns nothing
+ def register_for *backends
+ Factory.register self, backends
+ metaclass = class << self; self; end
+ if backends == ['*']
+ metaclass.send :define_method, :converts? do |name|
+ true
+ end
+ else
+ metaclass.send :define_method, :converts? do |name|
+ backends.include? name
+ end
+ end
+ nil
+ end
+ end
+
+ class << self
+ # Mixes the {Config Converter::Config} module into any class that includes the {Converter} module.
+ #
+ # converter - The Class that includes the {Converter} module
+ #
+ # Returns nothing
+ def included converter
+ converter.extend Config
+ end
+ end
+
+ include Config
+
+ # Public: Creates a new instance of Converter
+ #
+ # backend - The String backend format to which this converter converts.
+ # opts - An options Hash (optional, default: {})
+ #
+ # Returns a new instance of [Converter]
+ def initialize backend, opts = {}
+ @backend = backend
+ end
+
+ # Public: Converts an {AbstractNode} using the specified transform. If a
+ # transform is not specified, implementations typically derive one from the
+ # {AbstractNode#node_name} property.
+ #
+ # Implementations are free to decide how to carry out the conversion. In
+ # the case of the built-in converters, the tranform value is used to
+ # dispatch to a handler method. The {TemplateConverter} uses the value of
+ # the transform to select a template to render.
+ #
+ # node - The concrete instance of AbstractNode to convert
+ # transform - An optional String transform that hints at which transformation
+ # should be applied to this node. If a transform is not specified,
+ # the transform is typically derived from the value of the
+ # node's node_name property. (optional, default: nil)
+ #
+ # Returns the [String] result
+ def convert node, transform = nil
+ raise ::NotImplementedError
+ end
+
+ # Public: Converts an {AbstractNode} using the specified transform along
+ # with additional options. Delegates to {#convert} without options by default.
+ #
+ # node - The concrete instance of AbstractNode to convert
+ # transform - An optional String transform that hints at which transformation
+ # should be applied to this node. If a transform is not specified,
+ # the transform is typically derived from the value of the
+ # node's node_name property. (optional, default: nil)
+ # opts - An optional Hash of options that provide additional hints about
+ # how to convert the node.
+ #
+ # Returns the [String] result
+ def convert_with_options node, transform = nil, opts = {}
+ convert node, transform
+ end
+ end
+
+ # A module that can be used to mix the {#write} method into a {Converter}
+ # implementation to allow the converter to control how the output is written
+ # to disk.
+ module Writer
+ # Public: Writes the output to the specified target file name or stream.
+ #
+ # output - The output String to write
+ # target - The String file name or stream object to which the output should
+ # be written.
+ #
+ # Returns nothing
+ def write output, target
+ if target.respond_to? :write
+ target.write output.chomp
+ # ensure there's a trailing endline to be nice to terminals
+ target.write EOL
+ else
+ ::File.open(target, 'w') {|f| f.write output }
+ end
+ nil
+ end
+ end
+end
+
+require 'asciidoctor/converter/base'
+require 'asciidoctor/converter/factory'
diff --git a/lib/asciidoctor/converter/base.rb b/lib/asciidoctor/converter/base.rb
new file mode 100644
index 00000000..d43d764d
--- /dev/null
+++ b/lib/asciidoctor/converter/base.rb
@@ -0,0 +1,56 @@
+module Asciidoctor
+ # An abstract base class for defining converters that can be used to convert
+ # {AbstractNode} objects in a parsed AsciiDoc document to a backend format
+ # such as HTML or DocBook.
+ #
+ # Concrete subclasses must implement the {#convert} method and, optionally,
+ # the {#convert_with_options} method.
+ class Converter::Base
+ include Converter
+ end
+
+ # An abstract base class for built-in {Converter} classes.
+ class Converter::BuiltIn
+ def initialize opts = {}
+ end
+
+ # Public: Converts the specified {AbstractNode} using the specified transform.
+ #
+ # See {Converter#convert} for more details.
+ #
+ # Returns the [String] result of conversion
+ def convert node, transform = nil
+ transform ||= node.node_name
+ send transform, node
+ end
+
+ # Public: Converts the specified {AbstractNode} using the specified transform
+ # with additional options.
+ #
+ # See {Converter#convert_with_options} for more details.
+ #
+ # Returns the [String] result of conversion
+ def convert_with_options node, transform = nil, opts = {}
+ transform ||= node.node_name
+ send transform, node, opts
+ end
+
+ alias :handles? :respond_to?
+
+ # Public: Returns the converted content of the {AbstractNode}.
+ #
+ # Returns the converted [String] content of the {AbstractNode}.
+ def content node
+ node.content
+ end
+
+ alias :pass :content
+
+ # Public: Skips conversion of the {AbstractNode}.
+ #
+ # Returns [NilClass]
+ def skip node
+ nil
+ end
+ end
+end
diff --git a/lib/asciidoctor/converter/composite.rb b/lib/asciidoctor/converter/composite.rb
new file mode 100644
index 00000000..d6f7fc99
--- /dev/null
+++ b/lib/asciidoctor/converter/composite.rb
@@ -0,0 +1,62 @@
+module Asciidoctor
+ # A {Converter} implementation that delegates to the chain of {Converter}
+ # objects passed to the constructor. Selects the first {Converter} that
+ # identifies itself as the handler for a given transform.
+ class Converter::CompositeConverter < Converter::Base
+ # Get the Array of Converter objects in the chain
+ attr_reader :converters
+
+ def initialize *converters
+ @converters = converters.flatten.compact
+ @converter_map = {}
+ end
+
+ # Public: Delegates to the first converter that identifies itself as the
+ # handler for the given transform.
+ #
+ # node - the AbstractNode to convert
+ # transform - the optional String transform, or the name of the node if no
+ # transform is specified. (default: nil)
+ #
+ # Returns the String result returned from the delegate's convert method
+ def convert node, transform = nil
+ transform ||= node.node_name
+ # QUESTION is there a way we can control whether to use convert or send?
+ (converter_for transform).convert node, transform
+ end
+
+ # Public: Delegates to the first converter that identifies itself as the
+ # handler for the given transform. The optional Hash is passed as the last
+ # option to the delegate's convert method.
+ #
+ # node - the AbstractNode to convert
+ # transform - the optional String transform, or the name of the node if no
+ # transform is specified. (default: nil)
+ # opts - a optional Hash that is passed to the delegate's convert method. (default: {})
+ #
+ # Returns the String result returned from the delegate's convert method
+ def convert_with_options node, transform = nil, opts = {}
+ transform ||= node.node_name
+ # QUESTION should we check arity, or perhaps do a rescue ::ArgumentError?
+ (converter_for transform).convert_with_options node, transform, opts
+ end
+
+ # Public: Retrieve the converter for the specified transform.
+ #
+ # Returns the matching [Converter] object
+ def converter_for transform
+ @converter_map[transform] ||= find_converter transform
+ end
+
+ # Internal: Find the converter for the specified transform.
+ # Raise an exception if no converter is found.
+ #
+ # Returns the matching [Converter] object
+ def find_converter transform
+ @converters.each do |candidate|
+ return candidate if candidate.handles? transform
+ end
+ raise %(Could not find a converter to handle transform: #{transform})
+ end
+ end
+end
diff --git a/lib/asciidoctor/converter/docbook45.rb b/lib/asciidoctor/converter/docbook45.rb
new file mode 100644
index 00000000..5e145ebb
--- /dev/null
+++ b/lib/asciidoctor/converter/docbook45.rb
@@ -0,0 +1,63 @@
+require 'asciidoctor/converter/docbook5'
+
+module Asciidoctor
+ # A built-in {Converter} implementation that generates DocBook 4.5 output
+ # consistent with the docbook45 backend from AsciiDoc Python.
+ class Converter::DocBook45Converter < Converter::DocBook5Converter
+ def inline_anchor node
+ target = node.target
+ case node.type
+ when :ref
+ %(<anchor#{common_attributes target, nil, node.text}/>)
+ when :xref
+ if node.attr? 'path', nil
+ linkend = (node.attr 'fragment') || target
+ (text = node.text) ? %(<link linkend="#{linkend}">#{text}</link>) : %(<xref linkend="#{linkend}"/>)
+ else
+ text = node.text || (node.attr 'path')
+ %(<ulink url="#{target}">#{text}</ulink>)
+ end
+ when :link
+ %(<ulink url="#{target}">#{node.text}</ulink>)
+ when :bibref
+ %(<anchor#{common_attributes target, nil, "[#{target}]"}/>[#{target}])
+ end
+ end
+
+ def author_element doc, index = nil
+ firstname_key = index ? %(firstname_#{index}) : 'firstname'
+ middlename_key = index ? %(middlename_#{index}) : 'middlename'
+ lastname_key = index ? %(lastname_#{index}) : 'lastname'
+ email_key = index ? %(email_#{index}) : 'email'
+
+ result = []
+ result << '<author>'
+ result << %(<firstname>#{doc.attr firstname_key}</firstname>) if doc.attr? firstname_key
+ result << %(<othername>#{doc.attr middlename_key}</othername>) if doc.attr? middlename_key
+ result << %(<surname>#{doc.attr lastname_key}</surname>) if doc.attr? lastname_key
+ result << %(<email>#{doc.attr email_key}</email>) if doc.attr? email_key
+ result << '</author>'
+
+ result * EOL
+ end
+
+ def common_attributes id, role = nil, reftext = nil
+ res = id ? %( id="#{id}") : ''
+ res = %(#{res} role="#{role}") if role
+ res = %(#{res} xreflabel="#{reftext}") if reftext
+ res
+ end
+
+ def doctype_declaration root_tag_name
+ %(<!DOCTYPE #{root_tag_name} PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">)
+ end
+
+ def document_info_element doc, info_tag_prefix
+ super doc, info_tag_prefix, true
+ end
+
+ def document_ns_attributes doc
+ (doc.attr? 'noxmlns') ? nil : ' xmlns="http://docbook.org/ns/docbook"'
+ end
+ end
+end
diff --git a/lib/asciidoctor/converter/docbook5.rb b/lib/asciidoctor/converter/docbook5.rb
new file mode 100644
index 00000000..81d594d1
--- /dev/null
+++ b/lib/asciidoctor/converter/docbook5.rb
@@ -0,0 +1,665 @@
+module Asciidoctor
+ # A built-in {Converter} implementation that generates DocBook 5 output
+ # similar to the docbook45 backend from AsciiDoc Python, but migrated to the
+ # DocBook 5 specification.
+ class Converter::DocBook5Converter < Converter::BuiltIn
+ def document node
+ result = []
+ root_tag_name = node.doctype
+ result << '<?xml version="1.0" encoding="UTF-8"?>'
+ if (doctype_line = doctype_declaration root_tag_name)
+ result << doctype_line
+ end
+ result << '<?asciidoc-toc?>' if node.attr? 'toc'
+ result << '<?asciidoc-numbered?>' if node.attr? 'numbered'
+ lang_attribute = (node.attr? 'nolang') ? nil : %( lang="#{node.attr 'lang', 'en'}")
+ result << %(<#{root_tag_name}#{document_ns_attributes node}#{lang_attribute}>)
+ result << (document_info_element node, root_tag_name)
+ result << node.content if node.blocks?
+ unless (footer_docinfo = node.docinfo :footer).empty?
+ result << footer_docinfo
+ end
+ result << %(</#{root_tag_name}>)
+
+ result * EOL
+ end
+
+ alias :embedded :content
+
+ def section node
+ tag_name = if node.special
+ node.level <= 1 ? node.sectname : 'section'
+ else
+ node.document.doctype == 'book' && node.level <= 1 ? (node.level == 0 ? 'part' : 'chapter') : 'section'
+ end
+ %(<#{tag_name}#{common_attributes node.id, node.role, node.reftext}>
+<title>#{node.title}</title>
+#{node.content}
+</#{tag_name}>)
+ end
+
+ def admonition node
+ %(<#{tag_name = node.attr 'name'}#{common_attributes node.id, node.role, node.reftext}>
+#{title_tag node}#{resolve_content node}
+</#{tag_name}>)
+ end
+
+ alias :audio :skip
+
+ def colist node
+ result = []
+ result << %(<calloutlist#{common_attributes node.id, node.role, node.reftext}>)
+ result << %(<title>#{node.title}</title>) if node.title?
+ node.items.each do |item|
+ result << %(<callout arearefs="#{item.attr 'coids'}">)
+ result << %(<para>#{item.text}</para>)
+ result << item.content if item.blocks?
+ result << '</callout>'
+ end
+ result << %(</calloutlist>)
+ result * EOL
+ end
+
+ DLIST_TAGS = {
+ 'labeled' => {
+ :list => 'variablelist',
+ :entry => 'varlistentry',
+ :term => 'term',
+ :item => 'listitem'
+ },
+ 'qanda' => {
+ :list => 'qandaset',
+ :entry => 'qandaentry',
+ :label => 'question',
+ :term => 'simpara',
+ :item => 'answer'
+ },
+ 'glossary' => {
+ :list => nil,
+ :entry => 'glossentry',
+ :term => 'glossterm',
+ :item => 'glossdef'
+ }
+ }
+ DLIST_TAGS.default = DLIST_TAGS['labeled']
+
+ def dlist node
+ result = []
+ if node.style == 'horizontal'
+ result << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attributes node.id, node.role, node.reftext} tabstyle="horizontal" frame="none" colsep="0" rowsep="0">
+#{title_tag node}<tgroup cols="2">
+<colspec colwidth="#{node.attr 'labelwidth', 15}*"/>
+<colspec colwidth="#{node.attr 'itemwidth', 85}*"/>
+<tbody valign="top">)
+ node.items.each do |terms, dd|
+ result << %(<row>
+<entry>)
+ [*terms].each do |dt|
+ result << %(<simpara>#{dt.text}</simpara>)
+ end
+ result << %(</entry>
+<entry>)
+ unless dd.nil?
+ result << %(<simpara>#{dd.text}</simpara>) if dd.text?
+ result << dd.content if dd.blocks?
+ end
+ result << %(</entry>
+</row>)
+ end
+ result << %(</tbody>
+</tgroup>
+</#{tag_name}>)
+ else
+ tags = DLIST_TAGS[node.style]
+ list_tag = tags[:list]
+ entry_tag = tags[:entry]
+ label_tag = tags[:label]
+ term_tag = tags[:term]
+ item_tag = tags[:item]
+ if list_tag
+ result << %(<#{list_tag}#{common_attributes node.id, node.role, node.reftext}>)
+ result << %(<title>#{node.title}</title>) if node.title?
+ end
+
+ node.items.each do |terms, dd|
+ result << %(<#{entry_tag}>)
+ result << %(<#{label_tag}>) if label_tag
+
+ [*terms].each do |dt|
+ result << %(<#{term_tag}>#{dt.text}</#{term_tag}>)
+ end
+
+ result << %(</#{label_tag}>) if label_tag
+ result << %(<#{item_tag}>)
+ unless dd.nil?
+ result << %(<simpara>#{dd.text}</simpara>) if dd.text?
+ result << dd.content if dd.blocks?
+ end
+ result << %(</#{item_tag}>)
+ result << %(</#{entry_tag}>)
+ end
+
+ result << %(</#{list_tag}>) if list_tag
+ end
+
+ result * EOL
+ end
+
+ def example node
+ if node.title?
+ %(<example#{common_attributes node.id, node.role, node.reftext}>
+<title>#{node.title}</title>
+#{resolve_content node}
+</example>)
+ else
+ %(<informalexample#{common_attributes node.id, node.role, node.reftext}>
+#{resolve_content node}
+</informalexample>)
+ end
+ end
+
+ def floating_title node
+ %(<bridgehead#{common_attributes node.id, node.role, node.reftext} renderas="sect#{node.level}">#{node.title}</bridgehead>)
+ end
+
+ def image node
+ width_attribute = (node.attr? 'width') ? %( contentwidth="#{node.attr 'width'}") : nil
+ depth_attribute = (node.attr? 'height') ? %( contentdepth="#{node.attr 'height'}") : nil
+ swidth_attribute = (node.attr? 'scaledwidth') ? %( width="#{node.attr 'scaledwidth'}" scalefit="1") : nil
+ scale_attribute = (node.attr? 'scale') ? %( scale="#{node.attr 'scale'}") : nil
+ align_attribute = (node.attr? 'align') ? %( align="#{node.attr 'align'}") : nil
+
+ %(<figure#{common_attributes node.id, node.role, node.reftext}>
+#{title_tag node}<mediaobject>
+<imageobject>
+<imagedata fileref="#{node.image_uri(node.attr 'target')}"#{width_attribute}#{depth_attribute}#{swidth_attribute}#{scale_attribute}#{align_attribute}/>
+</imageobject>
+<textobject><phrase>#{node.attr 'alt'}</phrase></textobject>
+</mediaobject>
+</figure>)
+ end
+
+ def listing node
+ informal = !node.title?
+ listing_attributes = (common_attributes node.id, node.role, node.reftext)
+ if node.style == 'source' && (node.attr? 'language')
+ numbering = (node.attr? 'linenums') ? 'numbered' : 'unnumbered'
+ listing_content = %(<programlisting#{informal ? listing_attributes : nil} language="#{node.attr 'language'}" linenumbering="#{numbering}">#{node.content}</programlisting>)
+ else
+ listing_content = %(<screen#{informal ? listing_attributes : nil}>#{node.content}</screen>)
+ end
+ if informal
+ listing_content
+ else
+ %(<formalpara#{listing_attributes}>
+<title>#{node.title}</title>
+<para>
+#{listing_content}
+</para>
+</formalpara>)
+ end
+ end
+
+ def literal node
+ if node.title?
+ %(<formalpara#{common_attributes node.id, node.role, node.reftext}>
+<title>#{node.title}</title>
+<para>
+<literallayout class="monospaced">#{node.content}</literallayout>
+</para>
+</formalpara>)
+ else
+ %(<literallayout#{common_attributes node.id, node.role, node.reftext} class="monospaced">#{node.content}</literallayout>)
+ end
+ end
+
+ def math node
+ # QUESTION should the content be stripped already?
+ equation = node.content.strip
+ if node.style == 'latexmath'
+ equation_data = %(<alt><![CDATA[#{equation}]]></alt>
+<mediaobject><textobject><phrase></phrase></textobject></mediaobject>)
+ # asciimath
+ else
+ # DocBook backends can't handle AsciiMath, so output raw expression in text object
+ equation_data = %(<mediaobject><textobject><phrase><![CDATA[#{equation}]]></phrase></textobject></mediaobject>)
+ end
+ if node.title?
+ %(<equation#{common_attributes node.id, node.role, node.reftext}>
+<title>#{node.title}</title>
+#{equation_data}
+</equation>)
+ else
+ %(<informalequation#{common_attributes node.id, node.role, node.reftext}>
+#{equation_data}
+</informalequation>)
+ end
+ end
+
+ def olist node
+ result = []
+ num_attribute = node.style ? %( numeration="#{node.style}") : nil
+ result << %(<orderedlist#{common_attributes node.id, node.role, node.reftext}#{num_attribute}>)
+ result << %(<title>#{node.title}</title>) if node.title?
+ node.items.each do |item|
+ result << '<listitem>'
+ result << %(<simpara>#{item.text}</simpara>)
+ result << item.content if item.blocks?
+ result << '</listitem>'
+ end
+ result << %(</orderedlist>)
+ result * EOL
+ end
+
+ def open node
+ case node.style
+ when 'abstract'
+ if node.parent == node.document && node.document.attr?('doctype', 'book')
+ warn 'asciidoctor: WARNING: abstract block cannot be used in a document without a title when doctype is book. Excluding block content.'
+ ''
+ else
+ %(<abstract>
+#{title_tag node}#{resolve_content node}
+</abstract>)
+ end
+ when 'partintro'
+ unless node.level == 0 && node.parent.context == :section && node.document.doctype == 'book'
+ warn 'asciidoctor: ERROR: partintro block can only be used when doctype is book and it\'s a child of a part section. Excluding block content.'
+ ''
+ else
+ %(<partintro#{common_attributes node.id, node.role, node.reftext}>
+#{title_tag node}#{resolve_content node}
+</partintro>)
+ end
+ else
+ node.content
+ end
+ end
+
+ def page_break node
+ '<simpara><?asciidoc-pagebreak?></simpara>'
+ end
+
+ def paragraph node
+ if node.title?
+ %(<formalpara#{common_attributes node.id, node.role, node.reftext}>
+<title>#{node.title}</title>
+<para>#{node.content}</para>
+</formalpara>)
+ else
+ %(<simpara#{common_attributes node.id, node.role, node.reftext}>#{node.content}</simpara>)
+ end
+ end
+
+ def preamble node
+ if node.document.doctype == 'book'
+ %(<preface#{common_attributes node.id, node.role, node.reftext}>
+#{title_tag node, false}#{node.content}
+</preface>)
+ else
+ node.content
+ end
+ end
+
+ def quote node
+ result = []
+ result << %(<blockquote#{common_attributes node.id, node.role, node.reftext}>)
+ result << %(<title>#{node.title}</title>) if node.title?
+ if (node.attr? 'attribution') || (node.attr? 'citetitle')
+ result << '<attribution>'
+ if node.attr? 'attribution'
+ result << (node.attr 'attribution')
+ end
+ if node.attr? 'citetitle'
+ result << %(<citetitle>#{node.attr 'citetitle'}</citetitle>)
+ end
+ result << '</attribution>'
+ end
+ result << (resolve_content node)
+ result << '</blockquote>'
+ result * EOL
+ end
+
+ def thematic_break node
+ '<simpara><?asciidoc-hr?></simpara>'
+ end
+
+ def sidebar node
+ %(<sidebar#{common_attributes node.id, node.role, node.reftext}>
+#{title_tag node}#{resolve_content node}
+</sidebar>)
+ end
+
+ TABLE_PI_NAMES = ['dbhtml', 'dbfo', 'dblatex']
+ TABLE_SECTIONS = [:head, :foot, :body]
+
+ def table node
+ result = []
+ pgwide_attribute = (node.option? 'pgwide') ? ' pgwide="1"' : nil
+ result << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attributes node.id, node.role, node.reftext}#{pgwide_attribute} frame="#{node.attr 'frame', 'all'}" rowsep="#{['none', 'cols'].include?(node.attr 'grid') ? 0 : 1}" colsep="#{['none', 'rows'].include?(node.attr 'grid') ? 0 : 1}">)
+ result << %(<title>#{node.title}</title>) if tag_name == 'table'
+ if (width = (node.attr? 'width') ? (node.attr 'width') : nil)
+ TABLE_PI_NAMES.each do |pi_name|
+ result << %(<?#{pi_name} table-width="#{width}"?>)
+ end
+ end
+ result << %(<tgroup cols="#{node.attr 'colcount'}">)
+ node.columns.each do |col|
+ result << %(<colspec colname="col_#{col.attr 'colnumber'}" colwidth="#{col.attr(width ? 'colabswidth' : 'colpcwidth')}*"/>)
+ end
+ TABLE_SECTIONS.select {|tblsec| !node.rows[tblsec].empty? }.each do |tblsec|
+ result << %(<t#{tblsec}>)
+ node.rows[tblsec].each do |row|
+ result << '<row>'
+ row.each do |cell|
+ halign_attribute = (cell.attr? 'halign') ? %( align="#{cell.attr 'halign'}") : nil
+ valign_attribute = (cell.attr? 'valign') ? %( valign="#{cell.attr 'valign'}") : nil
+ colspan_attribute = cell.colspan ? %( namest="col_#{colnum = cell.column.attr 'colnumber'}" nameend="col_#{colnum + cell.colspan - 1}") : nil
+ rowspan_attribute = cell.rowspan ? %( morerows="#{cell.rowspan - 1}") : nil
+ # NOTE <entry> may not have whitespace (e.g., line breaks) as a direct descendant according to DocBook rules
+ entry_start = %(<entry#{halign_attribute}#{valign_attribute}#{colspan_attribute}#{rowspan_attribute}>)
+ cell_content = if tblsec == :head
+ cell.text
+ else
+ case cell.style
+ when :asciidoc
+ cell.content
+ when :verse
+ %(<literallayout>#{cell.text}</literallayout>)
+ when :literal
+ %(<literallayout class="monospaced">#{cell.text}</literallayout>)
+ when :header
+ cell.content.map {|text| %(<simpara><emphasis role="strong">#{text}</emphasis></simpara>) }.join
+ else
+ cell.content.map {|text| %(<simpara>#{text}</simpara>) }.join
+ end
+ end
+ entry_end = (node.document.attr? 'cellbgcolor') ? %(<?dbfo bgcolor="#{node.document.attr 'cellbgcolor'}"?></entry>) : '</entry>'
+ result << %(#{entry_start}#{cell_content}#{entry_end})
+ end
+ result << '</row>'
+ end
+ result << %(</t#{tblsec}>)
+ end
+ result << '</tgroup>'
+ result << %(</#{tag_name}>)
+
+ result * EOL
+ end
+
+ alias :toc :skip
+
+ def ulist node
+ result = []
+ if node.style == 'bibliography'
+ result << %(<bibliodiv#{common_attributes node.id, node.role, node.reftext}>)
+ result << %(<title>#{node.title}</title>) if node.title?
+ node.items.each do |item|
+ result << '<bibliomixed>'
+ result << %(<bibliomisc>#{item.text}</bibliomisc>)
+ result << item.content if item.blocks?
+ result << '</bibliomixed>'
+ end
+ result << '</bibliodiv>'
+ else
+ mark_type = (checklist = node.option? 'checklist') ? 'none' : node.style
+ mark_attribute = mark_type ? %( mark="#{mark_type}") : nil
+ result << %(<itemizedlist#{common_attributes node.id, node.role, node.reftext}#{mark_attribute}>)
+ result << %(<title>#{node.title}</title>) if node.title?
+ node.items.each do |item|
+ text_marker = if checklist && (item.attr? 'checkbox')
+ (item.attr? 'checked') ? '&#10003; ' : '&#10063; '
+ else
+ nil
+ end
+ result << '<listitem>'
+ result << %(<simpara>#{text_marker}#{item.text}</simpara>)
+ result << item.content if item.blocks?
+ result << '</listitem>'
+ end
+ result << '</itemizedlist>'
+ end
+
+ result * EOL
+ end
+
+ def verse node
+ result = []
+ result << %(<blockquote#{common_attributes node.id, node.role, node.reftext}>)
+ result << %(<title>#{node.title}</title>) if node.title?
+ if (node.attr? 'attribution') || (node.attr? 'citetitle')
+ result << '<attribution>'
+ if node.attr? 'attribution'
+ result << (node.attr 'attribution')
+ end
+ if node.attr? 'citetitle'
+ result << %(<citetitle>#{node.attr 'citetitle'}</citetitle>)
+ end
+ result << '</attribution>'
+ end
+ result << %(<literallayout>#{node.content}</literallayout>)
+ result << '</blockquote>'
+ result * EOL
+ end
+
+ alias :video :skip
+
+ def inline_anchor node
+ case node.type
+ when :ref
+ %(<anchor#{common_attributes node.target, nil, node.text}/>)
+ when :xref
+ if node.attr? 'path', nil
+ linkend = (node.attr 'fragment') || node.target
+ (text = node.text) ? %(<link linkend="#{linkend}">#{text}</link>) : %(<xref linkend="#{linkend}"/>)
+ else
+ %(<link xlink:href="#{target}">#{node.text || (node.attr 'path')}</link>)
+ end
+ when :link
+ %(<link xlink:href="#{node.target}">#{node.text}</link>)
+ when :bibref
+ %(<anchor#{common_attributes target, nil, "[#{node.target}]"}/>[#{node.target}])
+ else
+ warn %(asciidoctor: WARNING: unknown anchor type: #{node.type.inspect})
+ end
+ end
+
+ def inline_break node
+ %(#{node.text}<?asciidoc-br?>)
+ end
+
+ def inline_button node
+ %(<guibutton>#{node.text}</guibutton>)
+ end
+
+ def inline_callout node
+ %(<co#{common_attributes node.id}/>)
+ end
+
+ def inline_footnote node
+ if node.type == :xref
+ %(<footnoteref linkend="#{node.target}"/>)
+ else
+ %(<footnote#{common_attributes node.id}><simpara>#{node.text}</simpara></footnote>)
+ end
+ end
+
+ def inline_image node
+ width_attribute = (node.attr? 'width') ? %( contentwidth="#{node.attr 'width'}") : nil
+ depth_attribute = (node.attr? 'height') ? %( contentdepth="#{node.attr 'height'}") : nil
+ %(<inlinemediaobject>
+<imageobject>
+<imagedata fileref="#{node.type == 'icon' ? (node.icon_uri node.target) : (node.image_uri node.target)}"#{width_attribute}#{depth_attribute}/>
+</imageobject>
+<textobject><phrase>#{node.attr 'alt'}</phrase></textobject>
+</inlinemediaobject>)
+ end
+
+ def inline_indexterm node
+ if node.type == :visible
+ %(<indexterm><primary>#{node.text}</primary></indexterm>#{node.text})
+ else
+ terms = node.attr 'terms'
+ result = []
+ if (numterms = terms.size) > 2
+ result << %(<indexterm>
+<primary>#{terms[0]}</primary><secondary>#{terms[1]}</secondary><tertiary>#{terms[2]}</tertiary>
+</indexterm>)
+ end
+ if numterms > 1
+ result << %(<indexterm>
+<primary>#{terms[-2]}</primary><secondary>#{terms[-1]}</secondary>
+</indexterm>)
+ end
+ result << %(<indexterm>
+<primary>#{terms[-1]}</primary>
+</indexterm>)
+ result * EOL
+ end
+ end
+
+ def inline_kbd node
+ if (keys = node.attr 'keys').size == 1
+ %(<keycap>#{keys[0]}</keycap>)
+ else
+ key_combo = keys.map {|key| %(<keycap>#{key}</keycap>) }.join
+ %(<keycombo>#{key_combo}</keycombo>)
+ end
+ end
+
+ def inline_menu node
+ menu = node.attr 'menu'
+ if !(submenus = node.attr 'submenus').empty?
+ submenu_path = submenus.map {|submenu| %(<guisubmenu>#{submenu}</guisubmenu> ) }.join.chop
+ %(<menuchoice><guimenu>#{menu}</guimenu> #{submenu_path} <guimenuitem>#{node.attr 'menuitem'}</guimenuitem></menuchoice>)
+ elsif (menuitem = node.attr 'menuitem')
+ %(<menuchoice><guimenu>#{menu}</guimenu> <guimenuitem>#{menuitem}</guimenuitem></menuchoice>)
+ else
+ %(<guimenu>#{menu}</guimenu>)
+ end
+ end
+
+ QUOTED_TAGS = {
+ :emphasis => ['<emphasis>', '</emphasis>'],
+ :strong => ['<emphasis role="strong">', '</emphasis>'],
+ :monospaced => ['<literal>', '</literal>'],
+ :superscript => ['<superscript>', '</superscript>'],
+ :subscript => ['<subscript>', '</subscript>'],
+ :double => ['&#8220;', '&#8221;'],
+ :single => ['&#8216;', '&#8217;']
+ }
+ QUOTED_TAGS.default = [nil, nil]
+
+ def inline_quoted node
+ if (type = node.type) == :latexmath
+ %(<inlineequation>
+<alt><![CDATA[#{node.text}]]></alt>
+<inlinemediaobject><textobject><phrase><![CDATA[#{node.text}]]></phrase></textobject></inlinemediaobject>
+</inlineequation>)
+ else
+ open, close = QUOTED_TAGS[type]
+ text = node.text
+ quoted_text = if (role = node.role)
+ %(#{open}<phrase role="#{role}">#{text}</phrase>#{close})
+ else
+ %(#{open}#{text}#{close})
+ end
+
+ node.id ? %(<anchor#{common_attributes node.id, nil, text}/>#{quoted_text}) : quoted_text
+ end
+ end
+
+ def author_element doc, index = nil
+ firstname_key = index ? %(firstname_#{index}) : 'firstname'
+ middlename_key = index ? %(middlename_#{index}) : 'middlename'
+ lastname_key = index ? %(lastname_#{index}) : 'lastname'
+ email_key = index ? %(email_#{index}) : 'email'
+
+ result = []
+ result << '<author>'
+ result << '<personname>'
+ result << %(<firstname>#{doc.attr firstname_key}</firstname>) if doc.attr? firstname_key
+ result << %(<othername>#{doc.attr middlename_key}</othername>) if doc.attr? middlename_key
+ result << %(<surname>#{doc.attr lastname_key}</surname>) if doc.attr? lastname_key
+ result << '</personname>'
+ result << %(<email>#{doc.attr email_key}</email>) if doc.attr? email_key
+ result << '</author>'
+
+ result * EOL
+ end
+
+ def common_attributes id, role = nil, reftext = nil
+ res = id ? %( xml:id="#{id}") : ''
+ res = %(#{res} role="#{role}") if role
+ res = %(#{res} xreflabel="#{reftext}") if reftext
+ res
+ end
+
+ def doctype_declaration root_tag_name
+ ''
+ end
+
+ def document_info_element doc, info_tag_prefix, use_info_tag_prefix = false
+ info_tag_prefix = '' unless use_info_tag_prefix
+ result = []
+ result << %(<#{info_tag_prefix}info>)
+ result << (doc.header? ? (document_title_tags doc.header.title) : %(<title>#{doc.attr 'untitled-label'}</title>)) unless doc.notitle
+ result << %(<date>#{(doc.attr? 'revdate') ? (doc.attr 'revdate') : (doc.attr 'docdate')}</date>)
+ if doc.has_header?
+ if doc.attr? 'author'
+ if (authorcount = (doc.attr 'authorcount').to_i) < 2
+ result << (author_element doc)
+ result << %(<authorinitials>#{doc.attr 'authorinitials'}</authorinitials>) if doc.attr? 'authorinitials'
+ else
+ result << '<authorgroup>'
+ authorcount.times do |index|
+ result << (author_element doc, index + 1)
+ end
+ result << '</authorgroup>'
+ end
+ end
+ if (doc.attr? 'revdate') && ((doc.attr? 'revnumber') || (doc.attr? 'revremark'))
+ result << %(<revhistory>
+<revision>)
+ result << %(<revnumber>#{doc.attr 'revnumber'}</revnumber>) if doc.attr? 'revnumber'
+ result << %(<date>#{doc.attr 'revdate'}</date>) if doc.attr? 'revdate'
+ result << %(<authorinitials>#{doc.attr 'authorinitials'}</authorinitials>) if doc.attr? 'authorinitials'
+ result << %(<revremark>#{doc.attr 'revremark'}</revremark>) if doc.attr? 'revremark'
+ result << %(</revision>
+</revhistory>)
+ end
+ unless (header_docinfo = doc.docinfo :header).empty?
+ result << header_docinfo
+ end
+ result << %(<orgname>#{doc.attr 'orgname'}</orgname>) if doc.attr? 'orgname'
+ end
+ result << %(</#{info_tag_prefix}info>)
+
+ result * EOL
+ end
+
+ def document_ns_attributes doc
+ ' xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0"'
+ end
+
+ # FIXME this splitting should handled in the AST!
+ def document_title_tags title
+ if title.include? ': '
+ title, _, subtitle = title.rpartition ': '
+ %(<title>#{title}</title>
+<subtitle>#{subtitle}</subtitle>)
+ else
+ %(<title>#{title}</title>)
+ end
+ end
+
+ # FIXME this should be handled through a template mechanism
+ def resolve_content node
+ node.content_model == :compound ? node.content : %(<simpara>#{node.content}</simpara>)
+ end
+
+ def title_tag node, optional = true
+ !optional || node.title? ? %(<title>#{node.title}</title>\n) : nil
+ end
+ end
+end
diff --git a/lib/asciidoctor/converter/factory.rb b/lib/asciidoctor/converter/factory.rb
new file mode 100644
index 00000000..26e2cc09
--- /dev/null
+++ b/lib/asciidoctor/converter/factory.rb
@@ -0,0 +1,203 @@
+module Asciidoctor
+ module Converter
+ # A factory for instantiating converters that are used to convert a
+ # {Document} (i.e., a parsed AsciiDoc tree structure) or {AbstractNode} to
+ # a backend format such as HTML or DocBook. {Factory Converter::Factory} is
+ # the primary entry point for creating, registering and accessing
+ # converters.
+ #
+ # {Converter} objects are instantiated by passing a String backend name
+ # and, optionally, an options Hash to the {Factory#create} method. The
+ # backend can be thought of as an intent to convert a document to a
+ # specified format. For example:
+ #
+ # converter = Asciidoctor::Converter::Factory.create 'html5', :htmlsyntax => 'xml'
+ #
+ # Converter objects are thread safe. They only survive the lifetime of a single conversion.
+ #
+ # A singleton instance of {Factory Converter::Factory} can be accessed
+ # using the {Factory.default} method. This instance maintains the global
+ # registry of statically registered converters. The registery includes
+ # built-in converters for {Html5Converter HTML 5}, {DocBook5Converter
+ # DocBook 5} and {DocBook45Converter DocBook 4.5}, as well as any custom
+ # converters that have been discovered or explicitly registered.
+ #
+ # If the {https://rubygems.org/gems/thread_safe thread_safe} gem is
+ # installed, access to the default factory is guaranteed to be thread safe.
+ # Otherwise, a warning is issued to the user.
+ class Factory
+ @__default__ = nil
+ class << self
+
+ # Public: Retrieves a singleton instance of {Factory Converter::Factory}.
+ #
+ # If the thread_safe gem is installed, the registry of converters is
+ # initialized as a ThreadSafe::Cache. Otherwise, a warning is issued and
+ # the registry of converters is initialized using a normal Hash.
+ #
+ # initialize_singleton - A Boolean to indicate whether the singleton should
+ # be initialize if it has not already been created.
+ # If false, and a singleton has not been previously
+ # initialized, a fresh instance is returned.
+ #
+ # Returns the default [Factory] singleton instance
+ def default initialize_singleton = true
+ return @__default__ || new unless initialize_singleton
+ @__default__ ||= begin
+ require 'thread_safe' unless defined? ::ThreadSafe
+ new ::ThreadSafe::Cache.new
+ rescue ::LoadError
+ warn 'asciidoctor: WARNING: gem \'thread_safe\' is not installed. This gem recommended when registering custom converters.'
+ new
+ end
+ end
+
+ # Public: Register a custom converter in the global converter factory to
+ # handle conversion to the specified backends. If the backend value is an
+ # asterisk, the converter is used to handle any backend that does not have
+ # an explicit converter.
+ #
+ # converter - The Converter class to register
+ # backends - A String Array of backend names that this converter should
+ # be registered to handle (optional, default: ['*'])
+ #
+ # Returns nothing
+ def register converter, backends = ['*']
+ default.register converter, backends
+ end
+
+ # Public: Lookup the custom converter for the specified backend in the
+ # global factory.
+ #
+ # This method does not resolve the built-in converters.
+ #
+ # backend - The String backend name
+ #
+ # Returns the [Converter] class registered to convert the specified backend
+ # or nil if no match is found
+ def resolve backend
+ default.resolve backend
+ end
+
+ # Public: Retrieve the global Hash of custom Converter classes keyed by backend.
+ #
+ # Returns the the global [Hash] of custom Converter classes
+ def converters
+ default.converters
+ end
+
+ # Public: Unregister all Converter classes in the global factory.
+ #
+ # Returns nothing
+ def unregister_all
+ default.unregister_all
+ end
+ end
+
+ # Public: Get the Hash of Converter classes keyed by backend name
+ attr_reader :converters
+
+ def initialize converters = nil
+ @converters = converters || {}
+ @star_converter = nil
+ end
+
+ # Public: Register a custom converter with this factory to handle conversion
+ # to the specified backends. If the backend value is an asterisk, the
+ # converter is used to handle any backend that does not have an explicit
+ # converter.
+ #
+ # converter - The Converter class to register
+ # backends - A String Array of backend names that this converter should
+ # be registered to handle (optional, default: ['*'])
+ #
+ # Returns nothing
+ def register converter, backends = ['*']
+ backends.each do |backend|
+ @converters[backend] = converter
+ if backend == '*'
+ @star_converter = converter
+ end
+ end
+ nil
+ end
+
+ # Public: Lookup the custom converter registered with this factory to handle
+ # the specified backend.
+ #
+ # backend - The String backend name
+ #
+ # Returns the [Converter] class registered to convert the specified backend
+ # or nil if no match is found
+ def resolve backend
+ @converters && (@converters[backend] || @star_converter)
+ end
+
+ # Public: Unregister all Converter classes that are registered with this
+ # factory.
+ #
+ # Returns nothing
+ def unregister_all
+ @converters.clear
+ @star_converter = nil
+ end
+
+ # Public: Create a new Converter object that can be used to convert the
+ # {AbstractNode} (typically a {Document}) to the specified String backend.
+ # This method accepts an optional Hash of options that are passed to the
+ # converter's constructor.
+ #
+ # If a custom Converter is found to convert the specified backend, it is
+ # instantiated (if necessary) and returned immediately. If a custom
+ # Converter is not found, an attempt is made to resolve a built-in
+ # converter. If the `:template_dirs` key is found in the Hash passed as the
+ # second argument, a {CompositeConverter} is created that delegates to a
+ # {TemplateConverter} and, if resolved, the built-in converter. If the
+ # `:template_dirs` key is not found, the built-in converter is returned
+ # or nil if no converter is resolved.
+ #
+ # backend - the String backend name
+ # opts - an optional Hash of options that get passed on to the converter's
+ # constructor. If the :template_dirs key is found in the options
+ # Hash, this method returns a {CompositeConverter} that delegates
+ # to a {TemplateConverter}. (optional, default: {})
+ #
+ # Returns the [Converter] object
+ def create backend, opts = {}
+ if (converter = resolve backend)
+ return (converter.is_a? ::Class) ? (converter.new backend, opts) : converter
+ end
+
+ base_converter = case backend
+ when 'html5'
+ unless defined? ::Asciidoctor::Converter::Html5Converter
+ require 'asciidoctor/converter/html5'.to_s
+ end
+ Html5Converter.new opts
+ when 'docbook5'
+ unless defined? ::Asciidoctor::Converter::DocBook5Converter
+ require 'asciidoctor/converter/docbook5'.to_s
+ end
+ DocBook5Converter.new opts
+ when 'docbook45'
+ unless defined? ::Asciidoctor::Converter::DocBook45Converter
+ require 'asciidoctor/converter/docbook45'.to_s
+ end
+ DocBook45Converter.new opts
+ end
+
+ return base_converter unless opts.key? :template_dirs
+
+ unless defined? ::Asciidoctor::Converter::TemplateConverter
+ require 'asciidoctor/converter/template'.to_s
+ end
+ unless defined? ::Asciidoctor::Converter::CompositeConverter
+ require 'asciidoctor/converter/composite'.to_s
+ end
+ template_converter = TemplateConverter.new backend, opts[:template_dirs], opts
+ # QUESTION should we omit the composite converter if built_in_converter is nil?
+ CompositeConverter.new template_converter, base_converter
+ end
+ end
+ end
+end
diff --git a/lib/asciidoctor/converter/html5.rb b/lib/asciidoctor/converter/html5.rb
new file mode 100644
index 00000000..cfc3d094
--- /dev/null
+++ b/lib/asciidoctor/converter/html5.rb
@@ -0,0 +1,1054 @@
+module Asciidoctor
+ # A built-in {Converter} implementation that generates HTML 5 output
+ # consistent with the html5 backend from AsciiDoc Python.
+ class Converter::Html5Converter < Converter::BuiltIn
+ QUOTE_TAGS = {
+ :emphasis => ['<em>', '</em>', true],
+ :strong => ['<strong>', '</strong>', true],
+ :monospaced => ['<code>', '</code>', true],
+ :superscript => ['<sup>', '</sup>', true],
+ :subscript => ['<sub>', '</sub>', true],
+ :double => ['&#8220;', '&#8221;', false],
+ :single => ['&#8216;', '&#8217;', false],
+ :asciimath => ['\\$', '\\$', false],
+ :latexmath => ['\\(', '\\)', false]
+ # Opal can't resolve these constants when referenced here
+ #:asciimath => INLINE_MATH_DELIMITERS[:asciimath] + [false],
+ #:latexmath => INLINE_MATH_DELIMITERS[:latexmath] + [false]
+ }
+ QUOTE_TAGS.default = [nil, nil, nil]
+
+ def initialize opts = {}
+ @short_tag_slash = ((@htmlsyntax = opts[:htmlsyntax]) == 'xml' ? '/' : nil)
+ @stylesheets = Stylesheets.instance
+ end
+
+ def document node
+ result = []
+ short_tag_slash_local = @short_tag_slash
+ br = %(<br#{short_tag_slash_local}>)
+ linkcss = node.safe >= SafeMode::SECURE || (node.attr? 'linkcss')
+ result << '<!DOCTYPE html>'
+ result << ((node.attr? 'nolang') ? '<html>' : %(<html lang="#{node.attr 'lang', 'en'}">))
+ result << %(<head>
+<meta http-equiv="Content-Type" content="text/html; charset=#{node.attr 'encoding'}"#{short_tag_slash_local}>
+<meta name="generator" content="Asciidoctor #{node.attr 'asciidoctor-version'}"#{short_tag_slash_local}>
+<meta name="viewport" content="width=device-width, initial-scale=1.0"#{short_tag_slash_local}>)
+
+ ['description', 'keywords', 'author', 'copyright'].each do |key|
+ result << %(<meta name="#{key}" content="#{node.attr key}"#{short_tag_slash_local}>) if node.attr? key
+ end
+
+ result << %(<title>#{node.doctitle(:sanitize => true) || node.attr('untitled-label')}</title>)
+ if DEFAULT_STYLESHEET_KEYS.include?(node.attr 'stylesheet')
+ if linkcss
+ result << %(<link rel="stylesheet" href="#{node.normalize_web_path DEFAULT_STYLESHEET_NAME, (node.attr 'stylesdir', '')}"#{short_tag_slash_local}>)
+ else
+ result << @stylesheets.embed_primary_stylesheet
+ end
+ elsif node.attr? 'stylesheet'
+ if linkcss
+ result << %(<link rel="stylesheet" href="#{node.normalize_web_path((node.attr 'stylesheet'), (node.attr 'stylesdir', ''))}"#{short_tag_slash_local}>)
+ else
+ result << %(<style>
+#{node.read_asset node.normalize_system_path((node.attr 'stylesheet'), (node.attr 'stylesdir', '')), true}
+</style>)
+ end
+ end
+
+ if node.attr? 'icons', 'font'
+ if !(node.attr 'iconfont-remote', '').nil?
+ result << %(<link rel="stylesheet" href="#{node.attr 'iconfont-cdn', 'http://cdnjs.cloudflare.com/ajax/libs/font-awesome/3.2.1/css/font-awesome.min.css'}"#{short_tag_slash_local}>)
+ else
+ iconfont_stylesheet = %(#{node.attr 'iconfont-name', 'font-awesome'}.css)
+ result << %(<link rel="stylesheet" href="#{node.normalize_web_path iconfont_stylesheet, (node.attr 'stylesdir', '')}"#{short_tag_slash_local}>)
+ end
+ end
+
+ case node.attr 'source-highlighter'
+ when 'coderay'
+ if (node.attr 'coderay-css', 'class') == 'class'
+ if linkcss
+ result << %(<link rel="stylesheet" href="#{node.normalize_web_path @stylesheets.coderay_stylesheet_name, (node.attr 'stylesdir', '')}"#{short_tag_slash_local}>)
+ else
+ result << @stylesheets.embed_coderay_stylesheet
+ end
+ end
+ when 'pygments'
+ if (node.attr 'pygments-css', 'class') == 'class'
+ pygments_style = (doc.attr 'pygments-style', 'pastie')
+ if linkcss
+ result << %(<link rel="stylesheet" href="#{node.normalize_web_path @stylesheets.pygments_stylesheet_name(pygments_style), (node.attr 'stylesdir', '')}"#{short_tag_slash_local}>)
+ else
+ result << (@stylesheets.instance.embed_pygments_stylesheet pygments_style)
+ end
+ end
+ when 'highlightjs', 'highlight.js'
+ result << %(<link rel="stylesheet" href="#{node.attr 'highlightjsdir', 'http://cdnjs.cloudflare.com/ajax/libs/highlight.js/7.4'}/styles/#{node.attr 'highlightjs-theme', 'googlecode'}.min.css"#{short_tag_slash_local}>
+<script src="#{node.attr 'highlightjsdir', 'http://cdnjs.cloudflare.com/ajax/libs/highlight.js/7.4'}/highlight.min.js"></script>
+<script src="#{node.attr 'highlightjsdir', 'http://cdnjs.cloudflare.com/ajax/libs/highlight.js/7.4'}/lang/common.min.js"></script>
+<script>hljs.initHighlightingOnLoad()</script>)
+ when 'prettify'
+ result << %(<link rel="stylesheet" href="#{node.attr 'prettifydir', 'http://cdnjs.cloudflare.com/ajax/libs/prettify/r298'}/#{node.attr 'prettify-theme', 'prettify'}.min.css"#{short_tag_slash_local}>
+<script src="#{node.attr 'prettifydir', 'http://cdnjs.cloudflare.com/ajax/libs/prettify/r298'}/prettify.min.js"></script>
+<script>document.addEventListener('DOMContentLoaded', prettyPrint)</script>)
+ end
+
+ if node.attr? 'math'
+ result << %(<script type="text/x-mathjax-config">
+MathJax.Hub.Config({
+ tex2jax: {
+ inlineMath: [#{INLINE_MATH_DELIMITERS[:latexmath]}],
+ displayMath: [#{BLOCK_MATH_DELIMITERS[:latexmath]}],
+ ignoreClass: "nomath|nolatexmath"
+ },
+ asciimath2jax: {
+ delimiters: [#{BLOCK_MATH_DELIMITERS[:asciimath]}],
+ ignoreClass: "nomath|noasciimath"
+ }
+});
+</script>
+<script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_HTMLorMML"></script>
+<script>document.addEventListener('DOMContentLoaded', MathJax.Hub.TypeSet)</script>)
+ end
+
+ unless (docinfo_content = node.docinfo).empty?
+ result << docinfo_content
+ end
+
+ result << '</head>'
+ body_attrs = []
+ if node.id
+ body_attrs << %(id="#{node.id}")
+ end
+ if (node.attr? 'toc-class') && (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
+ body_attrs << %(class="#{node.doctype} #{node.attr 'toc-class'} toc-#{node.attr 'toc-position', 'left'}")
+ else
+ body_attrs << %(class="#{node.doctype}")
+ end
+ if node.attr? 'max-width'
+ body_attrs << %(style="max-width: #{node.attr 'max-width'};")
+ end
+ result << %(<body #{body_attrs * ' '}>)
+
+ unless node.noheader
+ result << '<div id="header">'
+ if node.doctype == 'manpage'
+ result << %(<h1>#{node.doctitle} Manual Page</h1>)
+ if (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
+ result << %(<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
+<div id="toctitle">#{node.attr 'toc-title'}</div>
+#{outline node}
+</div>)
+ end
+ result << %(<h2>#{node.attr 'manname-title'}</h2>
+<div class="sectionbody">
+<p>#{node.attr 'manname'} - #{node.attr 'manpurpose'}</p>
+</div>)
+ else
+ if node.has_header?
+ result << %(<h1>#{node.header.title}</h1>) unless node.notitle
+ if node.attr? 'author'
+ result << %(<span id="author" class="author">#{node.attr 'author'}</span>#{br})
+ if node.attr? 'email'
+ result << %(<span id="email" class="email">#{node.sub_macros(node.attr 'email')}</span>#{br})
+ end
+ if (authorcount = (node.attr 'authorcount').to_i) > 1
+ (2..authorcount).each do |idx|
+ result << %(<span id="author#{idx}" class="author">#{node.attr "author_#{idx}"}</span>#{br})
+ if node.attr? %(email_#{idx})
+ result << %(<span id="email#{idx}" class="email">#{node.sub_macros(node.attr "email_#{idx}")}</span>#{br})
+ end
+ end
+ end
+ end
+ if node.attr? 'revnumber'
+ result << %(<span id="revnumber">#{((node.attr 'version-label') || '').downcase} #{node.attr 'revnumber'}#{(node.attr? 'revdate') ? ',' : ''}</span>)
+ end
+ if node.attr? 'revdate'
+ result << %(<span id="revdate">#{node.attr 'revdate'}</span>)
+ end
+ if node.attr? 'revremark'
+ result << %(#{br}<span id="revremark">#{node.attr 'revremark'}</span>)
+ end
+ end
+
+ if (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
+ result << %(<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
+<div id="toctitle">#{node.attr 'toc-title'}</div>
+#{outline node}
+</div>)
+ end
+ end
+ result << '</div>'
+ end
+
+ result << %(<div id="content">
+#{node.content}
+</div>)
+
+ if node.footnotes? && !(node.attr? 'nofootnotes')
+ result << %(<div id="footnotes">
+<hr#{short_tag_slash_local}>)
+ node.footnotes.each do |footnote|
+ result << %(<div class="footnote" id="_footnote_#{footnote.index}">
+<a href="#_footnoteref_#{footnote.index}">#{footnote.index}</a>. #{footnote.text}
+</div>)
+ end
+ result << '</div>'
+ end
+ unless node.nofooter
+ result << '<div id="footer">'
+ result << '<div id="footer-text">'
+ if node.attr? 'revnumber'
+ result << %(#{node.attr 'version-label'} #{node.attr 'revnumber'}#{br})
+ end
+ if node.attr? 'last-update-label'
+ result << %(#{node.attr 'last-update-label'} #{node.attr 'docdatetime'})
+ end
+ result << '</div>'
+ unless (docinfo_content = node.docinfo :footer).empty?
+ result << docinfo_content
+ end
+ result << '</div>'
+ end
+
+ result << '</body>'
+ result << '</html>'
+ result * EOL
+ end
+
+ def embedded node
+ result = []
+ if !node.notitle && node.has_header?
+ id_attr = node.id ? %( id="#{node.id}") : nil
+ result << %(<h1#{id_attr}>#{node.header.title}</h1>)
+ end
+
+ result << node.content
+
+ if node.footnotes? && !(node.attr? 'nofootnotes')
+ result << %(<div id="footnotes">
+<hr#{@short_tag_slash}>)
+ node.footnotes.each do |footnote|
+ result << %(<div class="footnote" id="_footnote_#{footnote.index}">
+<a href="#_footnoteref_#{footnote.index}">#{footnote.index}</a> #{footnote.text}
+</div>)
+ end
+
+ result << '</div>'
+ end
+
+ result * EOL
+ end
+
+ def outline node, opts = {}
+ return if (sections = node.sections).empty?
+ sectnumlevels = opts[:sectnumlevels] || (node.document.attr 'sectnumlevels', 3).to_i
+ toclevels = opts[:toclevels] || (node.document.attr 'toclevels', 2).to_i
+ result = []
+ # FIXME the level for special sections should be set correctly in the model
+ # slevel will only be 0 if we have a book doctype with parts
+ slevel = (first_section = sections[0]).level
+ slevel = 1 if slevel == 0 && first_section.special
+ result << %(<ul class="sectlevel#{slevel}">)
+ sections.each do |section|
+ section_num = (section.numbered && !section.caption && section.level <= sectnumlevels) ? %(#{section.sectnum} ) : nil
+ result << %(<li><a href="##{section.id}">#{section_num}#{section.captioned_title}</a></li>)
+ if section.level < toclevels && (child_toc_level = outline section, :toclevels => toclevels, :secnumlevels => sectnumlevels)
+ result << '<li>'
+ result << child_toc_level
+ result << '</li>'
+ end
+ end
+ result << '</ul>'
+ result * EOL
+ end
+
+ def section node
+ slevel = node.level
+ # QUESTION should the check for slevel be done in section?
+ slevel = 1 if slevel == 0 && node.special
+ htag = %(h#{slevel + 1})
+ id_attr = anchor = link_start = link_end = nil
+ if node.id
+ id_attr = %( id="#{node.id}")
+ if node.document.attr? 'sectanchors'
+ anchor = %(<a class="anchor" href="##{node.id}"></a>)
+ # possible idea - anchor icons GitHub-style
+ #if node.document.attr? 'icons', 'font'
+ # anchor = %(<a class="anchor" href="##{node.id}"><i class="icon-anchor"></i></a>)
+ #else
+ elsif node.document.attr? 'sectlinks'
+ link_start = %(<a class="link" href="##{node.id}">)
+ link_end = '</a>'
+ end
+ end
+
+ if slevel == 0
+ %(<h1#{id_attr} class="sect0">#{anchor}#{link_start}#{node.title}#{link_end}</h1>
+#{node.content})
+ else
+ class_attr = (role = node.role) ? %( class="sect#{slevel} #{role}") : %( class="sect#{slevel}")
+ sectnum = if node.numbered && !node.caption && slevel <= (node.document.attr 'sectnumlevels', 3).to_i
+ %(#{node.sectnum} )
+ end
+ %(<div#{class_attr}>
+<#{htag}#{id_attr}>#{anchor}#{link_start}#{sectnum}#{node.captioned_title}#{link_end}</#{htag}>
+#{slevel == 1 ? %[<div class="sectionbody">\n#{node.content}\n</div>] : node.content}
+</div>)
+ end
+ end
+
+ def admonition node
+ id_attr = node.id ? %( id="#{node.id}") : nil
+ name = node.attr 'name'
+ title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : nil
+ caption = if node.document.attr? 'icons'
+ if node.document.attr? 'icons', 'font'
+ %(<i class="icon-#{name}" title="#{node.caption}"></i>)
+ else
+ %(<img src="#{node.icon_uri name}" alt="#{node.caption}"#{@short_tag_slash}>)
+ end
+ else
+ %(<div class="title">#{node.caption}</div>)
+ end
+ %(<div#{id_attr} class="admonitionblock #{name}#{(role = node.role) && " #{role}"}">
+<table>
+<tr>
+<td class="icon">
+#{caption}
+</td>
+<td class="content">
+#{title_element}#{node.content}
+</td>
+</tr>
+</table>
+</div>)
+ end
+
+ def audio node
+ xml = node.document.attr? 'htmlsyntax', 'xml'
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ classes = ['audioblock', node.style, node.role].compact
+ class_attribute = %( class="#{classes * ' '}")
+ title_element = node.title? ? %(<div class="title">#{node.captioned_title}</div>\n) : nil
+ %(<div#{id_attribute}#{class_attribute}>
+#{title_element}<div class="content">
+<audio src="#{node.media_uri(node.attr 'target')}"#{(node.option? 'autoplay') ? (append_boolean_attribute 'autoplay', xml) : nil}#{(node.option? 'nocontrols') ? nil : (append_boolean_attribute 'controls', xml)}#{(node.option? 'loop') ? (append_boolean_attribute 'loop', xml) : nil}>
+Your browser does not support the audio tag.
+</audio>
+</div>
+</div>)
+ end
+
+ def colist node
+ result = []
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ classes = ['colist', node.style, node.role].compact
+ class_attribute = %( class="#{classes * ' '}")
+
+ result << %(<div#{id_attribute}#{class_attribute}>)
+ result << %(<div class="title">#{node.title}</div>) if node.title?
+
+ if node.document.attr? 'icons'
+ result << '<table>'
+
+ font_icons = node.document.attr? 'icons', 'font'
+ node.items.each_with_index do |item, i|
+ num = i + 1
+ num_element = if font_icons
+ %(<i class="conum" data-value="#{num}"></i><b>#{num}</b>)
+ else
+ %(<img src="#{node.icon_uri "callouts/#{num}"}" alt="#{num}"#{@short_tag_slash}>)
+ end
+ result << %(<tr>
+<td>#{num_element}</td>
+<td>#{item.text}</td>
+</tr>)
+ end
+
+ result << '</table>'
+ else
+ result << '<ol>'
+ node.items.each do |item|
+ result << %(<li>
+<p>#{item.text}</p>
+</li>)
+ end
+ result << '</ol>'
+ end
+
+ result << '</div>'
+ result * EOL
+ end
+
+ def dlist node
+ result = []
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+
+ classes = case node.style
+ when 'qanda'
+ ['qlist', 'qanda', node.role]
+ when 'horizontal'
+ ['hdlist', node.role]
+ else
+ ['dlist', node.style, node.role]
+ end.compact
+
+ class_attribute = %( class="#{classes * ' '}")
+
+ result << %(<div#{id_attribute}#{class_attribute}>)
+ result << %(<div class="title">#{node.title}</div>) if node.title?
+ case node.style
+ when 'qanda'
+ result << '<ol>'
+ node.items.each do |terms, dd|
+ result << '<li>'
+ [*terms].each do |dt|
+ result << %(<p><em>#{dt.text}</em></p>)
+ end
+ if dd
+ result << %(<p>#{dd.text}</p>) if dd.text?
+ result << dd.content if dd.blocks?
+ end
+ result << '</li>'
+ end
+ result << '</ol>'
+ when 'horizontal'
+ short_tag_slash_local = @short_tag_slash
+ result << '<table>'
+ if (node.attr? 'labelwidth') || (node.attr? 'itemwidth')
+ result << '<colgroup>'
+ col_style_attribute = (node.attr? 'labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : nil
+ result << %(<col#{col_style_attribute}#{short_tag_slash_local}>)
+ col_style_attribute = (node.attr? 'itemwidth') ? %( style="width: #{(node.attr 'itemwidth').chomp '%'}%;") : nil
+ result << %(<col#{col_style_attribute}#{short_tag_slash_local}>)
+ result << '</colgroup>'
+ end
+ node.items.each do |terms, dd|
+ result << '<tr>'
+ result << %(<td class="hdlist1#{(node.option? 'strong') ? ' strong' : nil}">)
+ terms_array = [*terms]
+ last_term = terms_array[-1]
+ terms_array.each do |dt|
+ result << dt.text
+ result << %(<br#{short_tag_slash_local}>) if dt != last_term
+ end
+ result << '</td>'
+ result << '<td class="hdlist2">'
+ if dd
+ result << %(<p>#{dd.text}</p>) if dd.text?
+ result << dd.content if dd.blocks?
+ end
+ result << '</td>'
+ result << '</tr>'
+ end
+ result << '</table>'
+ else
+ result << '<dl>'
+ dt_style_attribute = node.style ? nil : ' class="hdlist1"'
+ node.items.each do |terms, dd|
+ [*terms].each do |dt|
+ result << %(<dt#{dt_style_attribute}>#{dt.text}</dt>)
+ end
+ if dd
+ result << '<dd>'
+ result << %(<p>#{dd.text}</p>) if dd.text?
+ result << dd.content if dd.blocks?
+ result << '</dd>'
+ end
+ end
+ result << '</dl>'
+ end
+
+ result << '</div>'
+ result * EOL
+ end
+
+ def example node
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ title_element = node.title? ? %(<div class="title">#{node.captioned_title}</div>\n) : nil
+
+ %(<div#{id_attribute} class="#{(role = node.role) ? ['exampleblock', role] * ' ' : 'exampleblock'}">
+#{title_element}<div class="content">
+#{node.content}
+</div>
+</div>)
+ end
+
+ def floating_title node
+ tag_name = %(h#{node.level + 1})
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ classes = [node.style, node.role].compact
+ %(<#{tag_name}#{id_attribute} class="#{classes * ' '}">#{node.title}</#{tag_name}>)
+ end
+
+ def image node
+ align = (node.attr? 'align') ? (node.attr 'align') : nil
+ float = (node.attr? 'float') ? (node.attr 'float') : nil
+ style_attribute = if align || float
+ styles = [align ? %(text-align: #{align}) : nil, float ? %(float: #{float}) : nil].compact
+ %( style="#{styles * ';'}")
+ end
+
+ width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil
+ height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil
+
+ img_element = %(<img src="#{node.image_uri node.attr('target')}" alt="#{node.attr 'alt'}"#{width_attribute}#{height_attribute}#{@short_tag_slash}>)
+ if (link = node.attr 'link')
+ img_element = %(<a class="image" href="#{link}">#{img_element}</a>)
+ end
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ classes = ['imageblock', node.style, node.role].compact
+ class_attribute = %( class="#{classes * ' '}")
+ title_element = node.title? ? %(\n<div class="title">#{node.captioned_title}</div>) : nil
+
+ %(<div#{id_attribute}#{class_attribute}#{style_attribute}>
+<div class="content">
+#{img_element}
+</div>#{title_element}
+</div>)
+ end
+
+ def listing node
+ nowrap = !(node.document.attr? 'prewrap') || (node.option? 'nowrap')
+ if node.style == 'source'
+ language = node.attr 'language'
+ language_classes = language ? %(#{language} language-#{language}) : nil
+ case node.attr 'source-highlighter'
+ when 'coderay'
+ pre_class = nowrap ? ' class="CodeRay nowrap"' : ' class="CodeRay"'
+ code_class = language ? %( class="#{language_classes}") : nil
+ when 'pygments'
+ pre_class = nowrap ? ' class="pygments highlight nowrap"' : ' class="pygments highlight"'
+ code_class = language ? %( class="#{language_classes}") : nil
+ when 'highlightjs', 'highlight.js'
+ pre_class = nowrap ? ' class="highlight nowrap"' : ' class="highlight"'
+ code_class = language ? %( class="#{language_classes}") : nil
+ when 'prettify'
+ pre_class = %( class="prettyprint#{nowrap ? ' nowrap' : nil}#{(node.attr? 'linenums') ? ' linenums' : nil})
+ pre_class = language ? %(#{pre_class} #{language_classes}") : %(#{pre_class}")
+ code_class = nil
+ when 'html-pipeline'
+ pre_class = language ? %( lang="#{language}") : nil
+ code_class = nil
+ else
+ pre_class = nowrap ? ' class="highlight nowrap"' : ' class="highlight"'
+ code_class = language ? %( class="#{language_classes}") : nil
+ end
+ pre_start = %(<pre#{pre_class}><code#{code_class}>)
+ pre_end = '</code></pre>'
+ else
+ pre_start = %(<pre#{nowrap ? ' class="nowrap"' : nil}>)
+ pre_end = '</pre>'
+ end
+
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ title_element = node.title? ? %(<div class="title">#{node.captioned_title}</div>\n) : nil
+ %(<div#{id_attribute} class="listingblock#{(role = node.role) && " #{role}"}">
+#{title_element}<div class="content">
+#{pre_start}#{node.content}#{pre_end}
+</div>
+</div>)
+ end
+
+ def literal node
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : nil
+ nowrap = !(node.document.attr? 'prewrap') || (node.option? 'nowrap')
+ %(<div#{id_attribute} class="literalblock#{(role = node.role) && " #{role}"}">
+#{title_element}<div class="content">
+<pre#{nowrap ? ' class="nowrap"' : nil}>#{node.content}</pre>
+</div>
+</div>)
+ end
+
+ def math node
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : nil
+ open, close = BLOCK_MATH_DELIMITERS[node.style.to_sym]
+ # QUESTION should the content be stripped already?
+ equation = node.content.strip
+ if node.subs.nil_or_empty? && !(node.attr? 'subs')
+ equation = node.sub_specialcharacters equation
+ end
+
+ unless (equation.start_with? open) && (equation.end_with? close)
+ equation = %(#{open}#{equation}#{close})
+ end
+
+ %(<div#{id_attribute} class="#{(role = node.role) ? ['mathblock', role] * ' ' : 'mathblock'}">
+#{title_element}<div class="content">
+#{equation}
+</div>
+</div>)
+ end
+
+ def olist node
+ result = []
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ classes = ['olist', node.style, node.role].compact
+ class_attribute = %( class="#{classes * ' '}")
+
+ result << %(<div#{id_attribute}#{class_attribute}>)
+ result << %(<div class="title">#{node.title}</div>) if node.title?
+
+ type_attribute = (keyword = node.list_marker_keyword) ? %( type="#{keyword}") : nil
+ start_attribute = (node.attr? 'start') ? %( start="#{node.attr 'start'}") : nil
+ result << %(<ol class="#{node.style}"#{type_attribute}#{start_attribute}>)
+
+ node.items.each do |item|
+ result << '<li>'
+ result << %(<p>#{item.text}</p>)
+ result << item.content if item.blocks?
+ result << '</li>'
+ end
+
+ result << '</ol>'
+ result << '</div>'
+ result * EOL
+ end
+
+ def open node
+ if (style = node.style) == 'abstract'
+ if node.parent == node.document && node.document.doctype == 'book'
+ warn 'asciidoctor: WARNING: abstract block cannot be used in a document without a title when doctype is book. Excluding block content.'
+ ''
+ else
+ id_attr = node.id ? %( id="#{node.id}") : nil
+ title_el = node.title? ? %(<div class="title">#{node.title}</div>) : nil
+ %(<div#{id_attr} class="quoteblock abstract#{(role = node.role) && " #{role}"}">
+#{title_el}<blockquote>
+#{node.content}
+</blockquote>
+</div>)
+ end
+ elsif style == 'partintro' && (node.level != 0 || node.parent.context != :section || node.document.doctype != 'book')
+ warn 'asciidoctor: ERROR: partintro block can only be used when doctype is book and it\'s a child of a book part. Excluding block content.'
+ ''
+ else
+ id_attr = node.id ? %( id="#{node.id}") : nil
+ title_el = node.title? ? %(<div class="title">#{node.title}</div>) : nil
+ %(<div#{id_attr} class="openblock#{style && style != 'open' ? " #{style}" : ''}#{(role = node.role) && " #{role}"}">
+#{title_el}<div class="content">
+#{node.content}
+</div>
+</div>)
+ end
+ end
+
+ def page_break node
+ '<div style="page-break-after: always;"></div>'
+ end
+
+ def paragraph node
+ attributes = if node.id
+ if node.role
+ %( id="#{node.id}" class="paragraph #{node.role}")
+ else
+ %( id="#{node.id}" class="paragraph")
+ end
+ elsif node.role
+ %( class="paragraph #{node.role}")
+ else
+ ' class="paragraph"'
+ end
+
+ if node.title?
+ %(<div#{attributes}>
+<div class="title">#{node.title}</div>
+<p>#{node.content}</p>
+</div>)
+ else
+ %(<div#{attributes}>
+<p>#{node.content}</p>
+</div>)
+ end
+ end
+
+ def preamble node
+ toc = if (node.attr? 'toc') && (node.attr? 'toc-placement', 'preamble')
+ %(\n<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
+<div id="toctitle">#{node.attr 'toc-title'}</div>
+#{outline node.document}
+</div>)
+ end
+
+ %(<div id="preamble">
+<div class="sectionbody">
+#{node.content}
+</div>#{toc}
+</div>)
+ end
+
+ def quote node
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ classes = ['quoteblock', node.role].compact
+ class_attribute = %( class="#{classes * ' '}")
+ title_element = node.title? ? %(\n<div class="title">#{node.title}</div>) : nil
+ attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil
+ citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil
+ if attribution || citetitle
+ cite_element = citetitle ? %(<cite>#{citetitle}</cite>) : nil
+ attribution_text = attribution ? %(#{citetitle ? "<br#{@short_tag_slash}>\n" : nil}&#8212; #{attribution}) : nil
+ attribution_element = %(\n<div class="attribution">\n#{cite_element}#{attribution_text}\n</div>)
+ else
+ attribution_element = nil
+ end
+
+ %(<div#{id_attribute}#{class_attribute}>#{title_element}
+<blockquote>
+#{node.content}
+</blockquote>#{attribution_element}
+</div>)
+ end
+
+ def thematic_break node
+ %(<hr#{@short_tag_slash}>)
+ end
+
+ def sidebar node
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : nil
+ %(<div#{id_attribute} class="#{(role = node.role) ? ['sidebarblock', role] * ' ' : 'sidebarblock'}">
+<div class="content">
+#{title_element}#{node.content}
+</div>
+</div>)
+ end
+
+ def table node
+ result = []
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ classes = ['tableblock', %(frame-#{node.attr 'frame', 'all'}), %(grid-#{node.attr 'grid', 'all'})]
+ if (role_class = node.role)
+ classes << role_class
+ end
+ class_attribute = %( class="#{classes * ' '}")
+ styles = [(node.option? 'autowidth') ? nil : %(width: #{node.attr 'tablepcwidth'}%;), (node.attr? 'float') ? %(float: #{node.attr 'float'};) : nil].compact
+ style_attribute = styles.size > 0 ? %( style="#{styles * ' '}") : nil
+
+ result << %(<table#{id_attribute}#{class_attribute}#{style_attribute}>)
+ result << %(<caption class="title">#{node.captioned_title}</caption>) if node.title?
+ if (node.attr 'rowcount') > 0
+ short_tag_slash_local = @short_tag_slash
+ result << '<colgroup>'
+ if node.option? 'autowidth'
+ tag = %(<col#{short_tag_slash_local}>)
+ node.columns.size.times do
+ result << tag
+ end
+ else
+ node.columns.each do |col|
+ result << %(<col style="width: #{col.attr 'colpcwidth'}%;"#{short_tag_slash_local}>)
+ end
+ end
+ result << '</colgroup>'
+ [:head, :foot, :body].select {|tsec| !node.rows[tsec].empty? }.each do |tsec|
+ result << %(<t#{tsec}>)
+ node.rows[tsec].each do |row|
+ result << '<tr>'
+ row.each do |cell|
+ if tsec == :head
+ cell_content = cell.text
+ else
+ case cell.style
+ when :asciidoc
+ cell_content = %(<div>#{cell.content}</div>)
+ when :verse
+ cell_content = %(<div class="verse">#{cell.text}</div>)
+ when :literal
+ cell_content = %(<div class="literal"><pre>#{cell.text}</pre></div>)
+ else
+ cell_content = ''
+ cell.content.each do |text|
+ cell_content = %(#{cell_content}<p class="tableblock">#{text}</p>)
+ end
+ end
+ end
+
+ cell_tag_name = (tsec == :head || cell.style == :header ? 'th' : 'td')
+ cell_class_attribute = %( class="tableblock halign-#{cell.attr 'halign'} valign-#{cell.attr 'valign'}")
+ cell_colspan_attribute = cell.colspan ? %( colspan="#{cell.colspan}") : nil
+ cell_rowspan_attribute = cell.rowspan ? %( rowspan="#{cell.rowspan}") : nil
+ cell_style_attribute = (node.document.attr? 'cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'};") : nil
+ result << %(<#{cell_tag_name}#{cell_class_attribute}#{cell_colspan_attribute}#{cell_rowspan_attribute}#{cell_style_attribute}>#{cell_content}</#{cell_tag_name}>)
+ end
+ result << '</tr>'
+ end
+ result << %(</t#{tsec}>)
+ end
+ end
+ result << %(</table>)
+ result * EOL
+ end
+
+ def toc node
+ return '<!-- toc disabled -->' unless (doc = node.document).attr? 'toc'
+
+ if node.id
+ id_attr = %( id="#{node.id}")
+ title_id_attr = ''
+ elsif doc.embedded? || !(doc.attr? 'toc-placement')
+ id_attr = ' id="toc"'
+ title_id_attr = ' id="toctitle"'
+ else
+ id_attr = nil
+ title_id_attr = nil
+ end
+ title = node.title? ? node.title : (doc.attr 'toc-title')
+ levels = (node.attr? 'levels') ? (node.attr 'levels').to_i : nil
+ role = node.role? ? node.role : (doc.attr 'toc-class', 'toc')
+
+ %(<div#{id_attr} class="#{role}">
+<div#{title_id_attr} class="title">#{title}</div>
+#{outline doc, :toclevels => levels}
+</div>)
+ end
+
+ def ulist node
+ result = []
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ div_classes = ['ulist', node.style, node.role].compact
+ marker_checked = nil
+ marker_unchecked = nil
+ if (checklist = node.option? 'checklist')
+ div_classes.insert 1, 'checklist'
+ ul_class_attribute = ' class="checklist"'
+ if node.option? 'interactive'
+ if node.document.attr? 'htmlsyntax', 'xml'
+ marker_checked = '<input type="checkbox" data-item-complete="1" checked="checked"/> '
+ marker_unchecked = '<input type="checkbox" data-item-complete="0"/> '
+ else
+ marker_checked = '<input type="checkbox" data-item-complete="1" checked> '
+ marker_unchecked = '<input type="checkbox" data-item-complete="0"> '
+ end
+ else
+ if node.document.attr? 'icons', 'font'
+ marker_checked = '<i class="icon-check"></i> '
+ marker_unchecked = '<i class="icon-check-empty"></i> '
+ else
+ marker_checked = '&#10003; '
+ marker_unchecked = '&#10063; '
+ end
+ end
+ else
+ ul_class_attribute = node.style ? %( class="#{node.style}") : nil
+ end
+ div_class_attribute = %( class="#{div_classes * ' '}")
+ result << %(<div#{id_attribute}#{div_class_attribute}>)
+ result << %(<div class="title">#{node.title}</div>) if node.title?
+ result << %(<ul#{ul_class_attribute}>)
+
+ node.items.each do |item|
+ result << '<li>'
+ if checklist && (item.attr? 'checkbox')
+ result << %(<p>#{(item.attr? 'checked') ? marker_checked : marker_unchecked}#{item.text}</p>)
+ else
+ result << %(<p>#{item.text}</p>)
+ end
+ result << item.content if item.blocks?
+ result << '</li>'
+ end
+
+ result << '</ul>'
+ result << '</div>'
+ result * EOL
+ end
+
+ def verse node
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ classes = ['verseblock', node.role].compact
+ class_attribute = %( class="#{classes * ' '}")
+ title_element = node.title? ? %(\n<div class="title">#{node.title}</div>) : nil
+ attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil
+ citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil
+ if attribution || citetitle
+ cite_element = citetitle ? %(<cite>#{citetitle}</cite>) : nil
+ attribution_text = attribution ? %(#{citetitle ? "<br#{@short_tag_slash}>\n" : nil}&#8212; #{attribution}) : nil
+ attribution_element = %(\n<div class="attribution">\n#{cite_element}#{attribution_text}\n</div>)
+ else
+ attribution_element = nil
+ end
+
+ %(<div#{id_attribute}#{class_attribute}>#{title_element}
+<pre class="content">#{node.content}</pre>#{attribution_element}
+</div>)
+ end
+
+ def video node
+ xml = node.document.attr? 'htmlsyntax', 'xml'
+ id_attribute = node.id ? %( id="#{node.id}") : nil
+ classes = ['videoblock', node.style, node.role].compact
+ class_attribute = %( class="#{classes * ' '}")
+ title_element = node.title? ? %(\n<div class="title">#{node.captioned_title}</div>) : nil
+ width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil
+ height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : nil
+ case node.attr 'poster'
+ when 'vimeo'
+ start_anchor = (node.attr? 'start') ? "#at=#{node.attr 'start'}" : nil
+ delimiter = '?'
+ autoplay_param = (node.option? 'autoplay') ? "#{delimiter}autoplay=1" : nil
+ delimiter = '&amp;' if autoplay_param
+ loop_param = (node.option? 'loop') ? "#{delimiter}loop=1" : nil
+ %(<div#{id_attribute}#{class_attribute}>#{title_element}
+<div class="content">
+<iframe#{width_attribute}#{height_attribute} src="//player.vimeo.com/video/#{node.attr 'target'}#{start_anchor}#{autoplay_param}#{loop_param}" frameborder="0"#{append_boolean_attribute 'webkitAllowFullScreen', xml}#{append_boolean_attribute 'mozallowfullscreen', xml}#{append_boolean_attribute 'allowFullScreen', xml}></iframe>
+</div>
+</div>)
+ when 'youtube'
+ start_param = (node.attr? 'start') ? "&amp;start=#{node.attr 'start'}" : nil
+ end_param = (node.attr? 'end') ? "&amp;end=#{node.attr 'end'}" : nil
+ autoplay_param = (node.option? 'autoplay') ? '&amp;autoplay=1' : nil
+ loop_param = (node.option? 'loop') ? '&amp;loop=1' : nil
+ controls_param = (node.option? 'nocontrols') ? '&amp;controls=0' : nil
+ %(<div#{id_attribute}#{class_attribute}>#{title_element}
+<div class="content">
+<iframe#{width_attribute}#{height_attribute} src="//www.youtube.com/embed/#{node.attr 'target'}?rel=0#{start_param}#{end_param}#{autoplay_param}#{loop_param}#{controls_param}" frameborder="0"#{(node.option? 'nofullscreen') ? nil : (append_boolean_attribute 'allowfullscreen', xml)}></iframe>
+</div>
+</div>)
+ else
+ poster_attribute = %(#{poster = node.attr 'poster'}).empty? ? nil : %( poster="#{node.media_uri poster}")
+ time_anchor = ((node.attr? 'start') || (node.attr? 'end')) ? %(#t=#{node.attr 'start'}#{(node.attr? 'end') ? ',' : nil}#{node.attr 'end'}) : nil
+ %(<div#{id_attribute}#{class_attribute}>#{title_element}
+<div class="content">
+<video src="#{node.media_uri(node.attr 'target')}#{time_anchor}"#{width_attribute}#{height_attribute}#{poster_attribute}#{(node.option? 'autoplay') ? (append_boolean_attribute 'autoplay', xml) : nil}#{(node.option? 'nocontrols') ? nil : (append_boolean_attribute 'controls', xml)}#{(node.option? 'loop') ? (append_boolean_attribute 'loop', xml) : nil}>
+Your browser does not support the video tag.
+</video>
+</div>
+</div>)
+ end
+ end
+
+ def inline_anchor node
+ target = node.target
+ case node.type
+ when :xref
+ refid = (node.attr 'refid') || target
+ # FIXME seems like text should be prepared already
+ text = node.text || (node.document.references[:ids][refid] || %([#{refid}]))
+ %(<a href="#{target}">#{text}</a>)
+ when :ref
+ %(<a id="#{target}"></a>)
+ when :link
+ class_attr = (role = node.role) ? %( class="#{role}") : nil
+ window_attr = (node.attr? 'window') ? %( target="#{node.attr 'window'}") : nil
+ %(<a href="#{target}"#{class_attr}#{window_attr}>#{node.text}</a>)
+ when :bibref
+ %(<a id="#{target}"></a>[#{target}])
+ else
+ warn %(asciidoctor: WARNING: unknown anchor type: #{node.type.inspect})
+ end
+ end
+
+ def inline_break node
+ %(#{node.text}<br#{@short_tag_slash}>)
+ end
+
+ def inline_button node
+ %(<b class="button">#{node.text}</b>)
+ end
+
+ def inline_callout node
+ if node.document.attr? 'icons', 'font'
+ %(<i class="conum" data-value="#{node.text}"></i><b>(#{node.text})</b>)
+ elsif node.document.attr? 'icons'
+ src = node.icon_uri("callouts/#{node.text}")
+ %(<img src="#{src}" alt="#{node.text}"#{@short_tag_slash}>)
+ else
+ %(<b>(#{node.text})</b>)
+ end
+ end
+
+ def inline_footnote node
+ if (index = node.attr 'index')
+ if node.type == :xref
+ %(<span class="footnoteref">[<a class="footnote" href="#_footnote_#{index}" title="View footnote.">#{index}</a>]</span>)
+ else
+ id_attr = node.id ? %( id="_footnote_#{node.id}") : nil
+ %(<span class="footnote"#{id_attr}>[<a id="_footnoteref_#{index}" class="footnote" href="#_footnote_#{index}" title="View footnote.">#{index}</a>]</span>)
+ end
+ elsif node.type == :xref
+ %(<span class="footnoteref red" title="Unresolved footnote reference.">[#{node.text}]</span>)
+ end
+ end
+
+ def inline_image node
+ if (type = node.type) == 'icon' && (node.document.attr? 'icons', 'font')
+ style_class = "icon-#{node.target}"
+ if node.attr? 'size'
+ style_class = %(#{style_class} icon-#{node.attr 'size'})
+ end
+ if node.attr? 'rotate'
+ style_class = %(#{style_class} icon-rotate-#{node.attr 'rotate'})
+ end
+ if node.attr? 'flip'
+ style_class = %(#{style_class} icon-flip-#{node.attr 'flip'})
+ end
+ title_attribute = (node.attr? 'title') ? %( title="#{node.attr 'title'}") : nil
+ img = %(<i class="#{style_class}"#{title_attribute}></i>)
+ elsif type == 'icon' && !(node.document.attr? 'icons')
+ img = %([#{node.attr 'alt'}])
+ else
+ resolved_target = (type == 'icon') ? (node.icon_uri node.target) : (node.image_uri node.target)
+
+ attrs = ['alt', 'width', 'height', 'title'].map {|name|
+ (node.attr? name) ? %( #{name}="#{node.attr name}") : nil
+ }.join
+
+ img = %(<img src="#{resolved_target}"#{attrs}#{@short_tag_slash}>)
+ end
+
+ if node.attr? 'link'
+ window_attr = (node.attr? 'window') ? %( target="#{node.attr 'window'}") : nil
+ img = %(<a class="image" href="#{node.attr 'link'}"#{window_attr}>#{img}</a>)
+ end
+
+ style_classes = (role = node.role) ? %(#{type} #{role}) : type
+ style_attr = (node.attr? 'float') ? %( style="float: #{node.attr 'float'}") : nil
+
+ %(<span class="#{style_classes}"#{style_attr}>#{img}</span>)
+ end
+
+ def inline_indexterm node
+ node.type == :visible ? node.text : ''
+ end
+
+ def inline_kbd node
+ if (keys = node.attr 'keys').size == 1
+ %(<kbd>#{keys[0]}</kbd>)
+ else
+ key_combo = keys.map {|key| %(<kbd>#{key}</kbd>+) }.join.chop
+ %(<span class="keyseq">#{key_combo}</span>)
+ end
+ end
+
+ def inline_menu node
+ menu = node.attr 'menu'
+ if !(submenus = node.attr 'submenus').empty?
+ submenu_path = submenus.map {|submenu| %(<span class="submenu">#{submenu}</span>&#160;&#9656; ) }.join.chop
+ %(<span class="menuseq"><span class="menu">#{menu}</span>&#160;&#9656; #{submenu_path} <span class="menuitem">#{node.attr 'menuitem'}</span></span>)
+ elsif (menuitem = node.attr 'menuitem')
+ %(<span class="menuseq"><span class="menu">#{menu}</span>&#160;&#9656; <span class="menuitem">#{menuitem}</span></span>)
+ else
+ %(<span class="menu">#{menu}</span>)
+ end
+ end
+
+ def inline_quoted node
+ open, close, is_tag = QUOTE_TAGS[node.type]
+ quoted_text = if (role = node.role)
+ is_tag ? %(#{open.chop} class="#{role}">#{node.text}#{close}) : %(<span class="#{role}">#{open}#{node.text}#{close}</span>)
+ else
+ %(#{open}#{node.text}#{close})
+ end
+
+ node.id ? %(<a id="#{node.id}"></a>#{quoted_text}) : quoted_text
+ end
+
+ def append_boolean_attribute name, xml
+ xml ? %( #{name}="#{name}") : %( #{name})
+ end
+ end
+end
diff --git a/lib/asciidoctor/converter/template.rb b/lib/asciidoctor/converter/template.rb
new file mode 100644
index 00000000..765f5d0c
--- /dev/null
+++ b/lib/asciidoctor/converter/template.rb
@@ -0,0 +1,286 @@
+module Asciidoctor
+ # A {Converter} implementation that uses templates composed in template
+ # languages supported by {https://github.com/rtomayko/tilt Tilt} to convert
+ # {AbstractNode} objects from a parsed AsciiDoc document tree to the backend
+ # format.
+ #
+ # The converter scans the provided directories for template files that are
+ # supported by Tilt. If an engine name (e.g., "slim") is specified in the
+ # options Hash passed to the constructor, the scan is limited to template
+ # files that have a matching extension (e.g., ".slim"). The scanner trims any
+ # extensions from the basename of the file and uses the resulting name as the
+ # key under which to store the template. When the {Converter#convert} method
+ # is invoked, the transform argument is used to select the template from this
+ # table and use it to convert the node.
+ #
+ # For example, the template file "path/to/templates/paragraph.html.slim" will
+ # be registered as the "paragraph" transform. The template would then be used
+ # to convert a paragraph {Block} object from the parsed AsciiDoc tree to an
+ # HTML backend format (e.g., "html5").
+ #
+ # As an optimization, scan results and templates are cached for the lifetime
+ # of the Ruby process. If the {https://rubygems.org/gems/thread_safe
+ # thread_safe} gem is installed, these caches are guaranteed to be thread
+ # safe. If this gem is not present, a warning is issued.
+ class Converter::TemplateConverter < Converter::Base
+ DEFAULT_ENGINE_OPTIONS = {
+ :erb => { :trim => '<' },
+ # TODO line 466 of haml/compiler.rb sorts the attributes; file an issue to make this configurable
+ # NOTE AsciiDoc syntax expects HTML/XML output to use double quotes around attribute values
+ :haml => { :format => :xhtml, :attr_wrapper => '"', :ugly => true, :escape_attrs => false },
+ :slim => { :disable_escape => true, :sort_attrs => false, :pretty => false }
+ }
+
+ # QUESTION are we handling how we load the thread_safe support correctly?
+ begin
+ require 'thread_safe' unless defined? ::ThreadSafe
+ @caches = { :scans => ::ThreadSafe::Cache.new, :templates => ::ThreadSafe::Cache.new }
+ rescue ::LoadError
+ @caches = {}
+ # FIXME perhaps only warn if the cache option is enabled?
+ warn 'asciidoctor: WARNING: gem \'thread_safe\' is not installed. This gem recommended when using custom backend templates.'
+ end
+
+ def self.caches
+ @caches
+ end
+
+ def self.clear_caches
+ @caches[:scans].clear if @caches[:scans]
+ @caches[:templates].clear if @caches[:templates]
+ end
+
+ def initialize backend, template_dirs, opts = {}
+ @backend = backend
+ @templates = {}
+ @template_dirs = template_dirs
+ @eruby = opts[:eruby]
+ @engine = opts[:template_engine]
+ @engine_options = DEFAULT_ENGINE_OPTIONS.inject({}) do |accum, (engine, default_opts)|
+ accum[engine] = default_opts.dup
+ accum
+ end
+ if (overrides = opts[:template_engine_options])
+ overrides.each do |engine, override_opts|
+ (@engine_options[engine] ||= {}).update override_opts
+ end
+ end
+ @engine_options[:haml][:format] = @engine_options[:slim][:format] = :html5 if opts[:htmlsyntax] == 'html'
+ case opts[:template_cache]
+ when true
+ @caches = self.class.caches
+ when ::Hash
+ @caches = opts[:template_cache]
+ else
+ @caches = {}
+ end
+ scan
+ #create_handlers
+ end
+
+ # Internal: Scans the template directories specified in the constructor for Tilt-supported
+ # templates, loads the templates and stores the in a Hash that is accessible via the
+ # {TemplateConverter#templates} method.
+ #
+ # Returns nothing
+ def scan
+ path_resolver = PathResolver.new
+ backend = @backend
+ engine = @engine
+ @template_dirs.each do |template_dir|
+ # FIXME need to think about safe mode restrictions here
+ template_dir = path_resolver.system_path template_dir, nil
+ # NOTE last matching template wins for template name if no engine is given
+ file_pattern = '*'
+ if engine
+ file_pattern = %(*.#{engine})
+ # example: templates/haml
+ if ::File.directory?(engine_dir = (::File.join template_dir, engine))
+ template_dir = engine_dir
+ end
+ end
+
+ # example: templates/html5 or templates/haml/html5
+ if ::File.directory?(backend_dir = (::File.join template_dir, backend))
+ template_dir = backend_dir
+ end
+
+ pattern = ::File.join template_dir, file_pattern
+
+ if (scan_cache = @caches[:scans])
+ template_cache = @caches[:templates]
+ unless (templates = scan_cache[pattern])
+ templates = (scan_cache[pattern] = (scan_dir template_dir, pattern, template_cache))
+ end
+ templates.each do |name, template|
+ @templates[name] = template_cache[template.file] = template
+ end
+ else
+ @templates.update scan_dir(template_dir, pattern, @caches[:templates])
+ end
+ nil
+ end
+ end
+
+=begin
+ # Internal: Creates convert methods (e.g., inline_anchor) that delegate to the discovered templates.
+ #
+ # Returns nothing
+ def create_handlers
+ @templates.each do |name, template|
+ create_handler name, template
+ end
+ nil
+ end
+
+ # Internal: Creates a convert method for the specified name that delegates to the specified template.
+ #
+ # Returns nothing
+ def create_handler name, template
+ metaclass = class << self; self; end
+ if name == 'document'
+ metaclass.send :define_method, name do |node|
+ (template.render node).strip
+ end
+ else
+ metaclass.send :define_method, name do |node|
+ (template.render node).chomp
+ end
+ end
+ end
+=end
+
+ # Public: Convert an {AbstractNode} to the backend format using the named template.
+ #
+ # Looks for a template that matches the value of the
+ # {AbstractNode#node_name} property if a template name is not specified.
+ #
+ # node - the AbstractNode to convert
+ # template_name - the String name of the template to use, or the value of
+ # the node_name property on the node if a template name is
+ # not specified. (optional, default: nil)
+ #
+ # Returns the [String] result from rendering the template
+ def convert node, template_name = nil
+ template_name ||= node.node_name
+ unless (template = @templates[template_name])
+ raise %(Could not find a custom template to handle transform: #{template_name})
+ end
+ if template_name == 'document'
+ (template.render node).strip
+ else
+ (template.render node).chomp
+ end
+ end
+
+ # Public: Convert an {AbstractNode} using the named template with the
+ # additional options provided.
+ #
+ # Looks for a template that matches the value of the
+ # {AbstractNode#node_name} property if a template name is not specified.
+ #
+ # node - the AbstractNode to convert
+ # template_name - the String name of the template to use, or the value of
+ # the node_name property on the node if a template name is
+ # not specified. (optional, default: nil)
+ # opts - an optional Hash that is passed as local variables to the
+ # template. (optional, default: {})
+ #
+ # Returns the [String] result from rendering the template
+ def convert_with_options node, template_name = nil, opts = {}
+ template_name ||= node.node_name
+ unless (template = @templates[template_name])
+ raise %(Could not find a custom template to handle transform: #{template_name})
+ end
+ (template.render node, opts).chomp
+ end
+
+ # Public: Checks whether there is a Tilt template registered with the specified name.
+ #
+ # name - the String template name
+ #
+ # Returns a [Boolean] that indicates whether a Tilt template is registered for the
+ # specified template name.
+ def handles? name
+ @templates.key? name
+ end
+
+ # Public: Retrieves the templates that this converter manages.
+ #
+ # Returns a [Hash] of Tilt template objects keyed by template name.
+ def templates
+ @templates.dup.freeze
+ end
+
+ # Public: Registers a Tilt template with this converter.
+ #
+ # name - the String template name
+ # template - the Tilt template object to register
+ #
+ # Returns the Tilt template object
+ def register name, template
+ @templates[name] = if (template_cache = @caches[:templates])
+ template_cache[template.file] = template
+ else
+ template
+ end
+ #create_handler name, template
+ end
+
+ # Internal: Scan the specified directory for template files matching pattern and instantiate
+ # a Tilt template for each matched file.
+ #
+ # Returns the scan result as a [Hash]
+ def scan_dir template_dir, pattern, template_cache = nil
+ result = {}
+ eruby_loaded = nil
+ # Grab the files in the top level of the directory (do not recurse)
+ ::Dir.glob(pattern).select {|match| ::File.file? match }.each do |file|
+ if (basename = ::File.basename file) == 'helpers.rb' || (path_segments = basename.split '.').size < 2
+ next
+ end
+ # TODO we could derive the basebackend from the minor extension of the template file
+ #name, *rest, ext_name = *path_segments # this form only works in Ruby >= 1.9
+ name = path_segments[0]
+ if name == 'block_ruler'
+ name = 'thematic_break'
+ elsif name.start_with? 'block_'
+ name = name[6..-1]
+ end
+ ext_name = path_segments[-1]
+ if ext_name == 'slim'
+ # slim doesn't get loaded by Tilt, so we have to load it explicitly
+ Helpers.require_library 'slim' unless defined? ::Slim
+ elsif ext_name == 'erb'
+ eruby_loaded = load_eruby @eruby unless eruby_loaded
+ end
+ next unless ::Tilt.registered? ext_name
+ unless template_cache && (template = template_cache[file])
+ template = ::Tilt.new file, 1, @engine_options[ext_name.to_sym]
+ end
+ result[name] = template
+ end
+ if ::File.file?(helpers = (::File.join template_dir, 'helpers.rb'))
+ require helpers
+ end
+ result
+ end
+
+ # Internal: Load the eRuby implementation
+ #
+ # name - the String name of the eRuby implementation
+ #
+ # Returns the eRuby implementation [Class]
+ def load_eruby name
+ if !name || name == 'erb'
+ require 'erb' unless defined? ::ERB
+ ::ERB
+ elsif name == 'erubis'
+ Helpers.require_library 'erubis' unless defined? ::Erubis
+ ::Erubis::FastEruby
+ else
+ raise ::ArgumentError, %(Unknown ERB implementation: #{name})
+ end
+ end
+ end
+end
diff --git a/lib/asciidoctor/document.rb b/lib/asciidoctor/document.rb
index 426c7e56..e133c116 100644
--- a/lib/asciidoctor/document.rb
+++ b/lib/asciidoctor/document.rb
@@ -1,6 +1,5 @@
module Asciidoctor
-# Public: Methods for parsing Asciidoc documents and rendering them
-# using erb templates.
+# Public: Methods for parsing and converting AsciiDoc documents.
#
# There are several strategies for getting the title of the document:
#
@@ -47,7 +46,7 @@ class Document < AbstractBlock
# of the source file and disables any macro other than the include macro.
#
# A value of 10 (SERVER) disallows the document from setting attributes that
- # would affect the rendering of the document, in addition to all the security
+ # would affect the conversion of the document, in addition to all the security
# features of SafeMode::SAFE. For instance, this value disallows changing the
# backend or the source-highlighter using an attribute defined in the source
# document. This is the most fundamental level of security for server-side
@@ -82,13 +81,16 @@ class Document < AbstractBlock
# Public: The section level 0 block
attr_reader :header
- # Public: Base directory for rendering this document. Defaults to directory of the source file.
+ # Public: Base directory for converting this document. Defaults to directory of the source file.
# If the source is a string, defaults to the current directory.
attr_reader :base_dir
# Public: A reference to the parent document of this nested document.
attr_reader :parent_document
+ # Public: The Converter associated with this document
+ attr_reader :converter
+
# Public: The extensions registry
attr_reader :extensions
@@ -103,7 +105,7 @@ class Document < AbstractBlock
#
# data = File.readlines(filename)
# doc = Asciidoctor::Document.new(data)
- # puts doc.render
+ # puts doc.convert
def initialize(data = [], options = {})
super(self, :document)
@@ -123,7 +125,7 @@ class Document < AbstractBlock
@attribute_overrides = @parent_document.attributes.dup
@attribute_overrides.delete 'doctype'
@safe = @parent_document.safe
- @renderer = @parent_document.renderer
+ @converter = @parent_document.converter
initialize_extensions = false
@extensions = @parent_document.extensions
else
@@ -153,7 +155,7 @@ class Document < AbstractBlock
end
@attribute_overrides = overrides
@safe = nil
- @renderer = nil
+ @converter = nil
initialize_extensions = defined? ::Asciidoctor::Extensions
@extensions = nil # initialize furthur down
end
@@ -301,11 +303,15 @@ class Document < AbstractBlock
verdict
}
- if !@parent_document
+ if @parent_document
+ # don't need to do the extra processing within our own document
+ # FIXME line info isn't reported correctly within include files in nested document
+ @reader = Reader.new data, options[:cursor]
+ else
# setup default backend and doctype
@attributes['backend'] ||= DEFAULT_BACKEND
@attributes['doctype'] ||= DEFAULT_DOCTYPE
- update_backend_attributes
+ update_backend_attributes @attributes['backend'], true
#@attributes['indir'] = @attributes['docdir']
#@attributes['infile'] = @attributes['docfile']
@@ -345,10 +351,6 @@ class Document < AbstractBlock
@reader = ext.process_method[self, @reader] || @reader
end
end
- else
- # don't need to do the extra processing within our own document
- # FIXME line info isn't reported correctly within include files in nested document
- @reader = Reader.new data, options[:cursor]
end
# Now parse the lines in the reader into blocks
@@ -465,11 +467,11 @@ class Document < AbstractBlock
end
def doctype
- @_doctype ||= @attributes['doctype']
+ @doctype ||= @attributes['doctype']
end
def backend
- @_backend ||= @attributes['backend']
+ @backend ||= @attributes['backend']
end
def basebackend? base
@@ -589,11 +591,11 @@ class Document < AbstractBlock
toc2_val = @attributes['toc2']
toc_position_val = @attributes['toc-position']
- if (!toc_val.nil? && (toc_val != '' || toc_position_val.to_s != '')) || !toc2_val.nil?
+ if (toc_val && (toc_val != '' || !toc_position_val.nil_or_empty?)) || toc2_val
default_toc_position = 'left'
default_toc_class = 'toc2'
- position = [toc_position_val, toc2_val, toc_val].find {|pos| pos.to_s != ''}
- position = default_toc_position if !position && !toc2_val.nil?
+ position = [toc_position_val, toc2_val, toc_val].find {|pos| !pos.nil_or_empty? }
+ position = default_toc_position if !position && toc2_val
@attributes['toc'] = ''
case position
when 'left', '<', '&lt;'
@@ -604,10 +606,15 @@ class Document < AbstractBlock
@attributes['toc-position'] = 'top'
when 'bottom', 'v'
@attributes['toc-position'] = 'bottom'
- when 'center'
- @attributes.delete('toc2')
+ when 'preamble'
+ @attributes.delete 'toc2'
+ @attributes['toc-placement'] = 'preamble'
default_toc_class = nil
- default_toc_position = 'center'
+ default_toc_position = nil
+ when 'default'
+ @attributes.delete 'toc2'
+ default_toc_class = nil
+ default_toc_position = 'default'
end
@attributes['toc-class'] ||= default_toc_class if default_toc_class
@attributes['toc-position'] ||= default_toc_position if default_toc_position
@@ -629,7 +636,7 @@ class Document < AbstractBlock
# Internal: Restore the attributes to the previously saved state
def restore_attributes
- # QUESTION shouldn't this be a dup in case we render again?
+ # QUESTION shouldn't this be a dup in case we convert again?
@attributes = @original_attributes
end
@@ -666,11 +673,15 @@ class Document < AbstractBlock
if attribute_locked?(name)
false
else
- @attributes[name] = apply_attribute_value_subs(value)
- @attributes_modified << name
- if name == 'backend'
- update_backend_attributes
+ case name
+ when 'backend'
+ update_backend_attributes apply_attribute_value_subs(value)
+ when 'doctype'
+ update_doctype_attributes apply_attribute_value_subs(value)
+ else
+ @attributes[name] = apply_attribute_value_subs(value)
end
+ @attributes_modified << name
true
end
end
@@ -725,78 +736,120 @@ class Document < AbstractBlock
end
# Public: Update the backend attributes to reflect a change in the selected backend
- def update_backend_attributes
- backend = @attributes['backend']
- if backend.start_with? 'xhtml'
- @attributes['htmlsyntax'] = 'xml'
- backend = @attributes['backend'] = backend[1..-1]
- elsif backend.start_with? 'html'
- @attributes['htmlsyntax'] = 'html'
- end
- if BACKEND_ALIASES.has_key? backend
- backend = @attributes['backend'] = BACKEND_ALIASES[backend]
- end
- basebackend = backend.sub(TrailingDigitsRx, '')
- page_width = DEFAULT_PAGE_WIDTHS[basebackend]
- if page_width
- @attributes['pagewidth'] = page_width
+ #
+ # This method also handles updating the related doctype attributes if the
+ # doctype attribute is assigned at the time this method is called.
+ def update_backend_attributes new_backend, force = false
+ if force || (new_backend && new_backend != @attributes['backend'])
+ attrs = @attributes
+ current_backend = attrs['backend']
+ current_basebackend = attrs['basebackend']
+ current_doctype = attrs['doctype']
+ if new_backend.start_with? 'xhtml'
+ attrs['htmlsyntax'] = 'xml'
+ new_backend = new_backend[1..-1]
+ elsif new_backend.start_with? 'html'
+ attrs['htmlsyntax'] = 'html'
+ end
+ if (resolved_name = BACKEND_ALIASES[new_backend])
+ new_backend = resolved_name
+ end
+ new_basebackend = new_backend.sub TrailingDigitsRx, ''
+ if (page_width = DEFAULT_PAGE_WIDTHS[new_basebackend])
+ attrs['pagewidth'] = page_width
+ else
+ attrs.delete 'pagewidth'
+ end
+ if current_backend
+ attrs.delete %(backend-#{current_backend})
+ if current_doctype
+ attrs.delete %(backend-#{current_backend}-doctype-#{current_doctype})
+ end
+ end
+ attrs['backend'] = new_backend
+ attrs[%(backend-#{new_backend})] = ''
+ if current_doctype
+ attrs[%(doctype-#{current_doctype})] = ''
+ attrs[%(backend-#{new_backend}-doctype-#{current_doctype})] = ''
+ end
+ if new_basebackend != current_basebackend
+ if current_basebackend
+ attrs.delete %(basebackend-#{current_basebackend})
+ if current_doctype
+ attrs.delete %(basebackend-#{current_basebackend}-doctype-#{current_doctype})
+ end
+ end
+ attrs['basebackend'] = new_basebackend
+ attrs[%(basebackend-#{new_basebackend})] = ''
+ attrs[%(basebackend-#{new_basebackend}-doctype-#{current_doctype})] = '' if current_doctype
+ end
+ ext = DEFAULT_EXTENSIONS[new_basebackend] || '.html'
+ new_file_type = ext[1..-1]
+ current_file_type = attrs['filetype']
+ attrs['outfilesuffix'] = ext unless attribute_locked? 'outfilesuffix'
+ attrs.delete %(filetype-#{current_file_type}) if current_file_type
+ attrs['filetype'] = new_file_type
+ attrs[%(filetype-#{new_file_type})] = ''
+ # clear cached value
+ @backend = nil
+ # (re)initialize converter
+ @converter = create_converter
+ end
+ end
+
+ def update_doctype_attributes new_doctype
+ if new_doctype && new_doctype != @attributes['doctype']
+ attrs = @attributes
+ current_doctype = attrs['doctype']
+ current_backend = attrs['backend']
+ current_basebackend = attrs['basebackend']
+ if current_doctype
+ attrs.delete %(doctype-#{current_doctype})
+ attrs.delete %(backend-#{current_backend}-doctype-#{current_doctype}) if current_backend
+ attrs.delete %(basebackend-#{current_basebackend}-doctype-#{current_doctype}) if current_basebackend
+ end
+ attrs['doctype'] = new_doctype
+ attrs[%(doctype-#{new_doctype})] = ''
+ attrs[%(backend-#{current_backend}-doctype-#{new_doctype})] = '' if current_backend
+ attrs[%(basebackend-#{current_basebackend}-doctype-#{new_doctype})] = '' if current_basebackend
+ # clear cached value
+ @doctype = nil
+ end
+ end
+
+ def create_converter
+ converter_opts = {}
+ converter_opts[:htmlsyntax] = @attributes['htmlsyntax']
+ template_dirs = if (template_dir = @options[:template_dir])
+ converter_opts[:template_dirs] = [template_dir]
+ elsif (template_dirs = @options[:template_dirs])
+ converter_opts[:template_dirs] = template_dirs
+ end
+ if template_dirs
+ converter_opts[:template_cache] = @options.fetch :template_cache, true
+ converter_opts[:template_engine] = @options[:template_engine]
+ converter_opts[:template_engine_options] = @options[:template_engine_options]
+ converter_opts[:eruby] = @options[:eruby]
+ end
+ converter_factory = if (converter = @options[:converter])
+ Converter::Factory.new Hash[backend, converter]
else
- @attributes.delete('pagewidth')
- end
- @attributes["backend-#{backend}"] = ''
- @attributes['basebackend'] = basebackend
- @attributes["basebackend-#{basebackend}"] = ''
- # REVIEW cases for the next two assignments
- @attributes["#{backend}-#{@attributes['doctype']}"] = ''
- @attributes["#{basebackend}-#{@attributes['doctype']}"] = ''
- ext = DEFAULT_EXTENSIONS[basebackend] || '.html'
- @attributes['outfilesuffix'] = ext unless (attribute_locked? 'outfilesuffix')
- file_type = ext[1..-1]
- @attributes['filetype'] = file_type
- @attributes["filetype-#{file_type}"] = ''
- @_doctype = nil
- @_backend = nil
- end
-
- def renderer(opts = {})
- return @renderer if @renderer
-
- render_options = {}
-
- # Load up relevant Document @options
- if @options.has_key? :template_dir
- render_options[:template_dirs] = [@options[:template_dir]]
- elsif @options.has_key? :template_dirs
- render_options[:template_dirs] = @options[:template_dirs]
+ Converter::Factory.default false
end
-
- render_options[:template_cache] = @options.fetch(:template_cache, true)
- render_options[:backend] = @attributes.fetch('backend', 'html5')
- render_options[:htmlsyntax] = @attributes['htmlsyntax']
- render_options[:template_engine] = @options[:template_engine]
- render_options[:eruby] = @options.fetch(:eruby, 'erb')
- render_options[:compact] = @options.fetch(:compact, false)
-
- # Override Document @option settings with options passed in
- render_options.merge! opts
-
- @renderer = Renderer.new(render_options)
+ # QUESTION should we honor the convert_opts?
+ # QUESTION should we pass through all options and attributes too?
+ #converter_opts.update opts
+ converter_factory.create backend, converter_opts
end
- # Public: Render the Asciidoc document using the templates
- # loaded by Renderer. If a :template_dir is not specified,
- # or a template is missing, the renderer will fall back to
+ # Public: Convert the AsciiDoc document using the templates
+ # loaded by the Converter. If a :template_dir is not specified,
+ # or a template is missing, the converter will fall back to
# using the appropriate built-in template.
- def render(opts = {})
+ def convert opts = {}
restore_attributes
- r = renderer(opts)
- # QUESTION should we add Prerenderprocessors? is it the right name?
- #if @extensions && !@parent_document && @extensions.prerenderprocessors?
- # @extensions.prerenderprocessors.each do |ext|
- # ext.process_method[self, r]
- # end
- #end
+ # QUESTION should we add processors that execute before conversion begins?
if doctype == 'inline'
# QUESTION should we warn if @blocks.size > 0 and the first block is not a paragraph?
@@ -806,7 +859,8 @@ class Document < AbstractBlock
output = ''
end
else
- output = @options.merge(opts)[:header_footer] ? r.render('document', self).strip : r.render('embedded', self)
+ transform = ((opts.key? :header_footer) ? opts[:header_footer] : @options[:header_footer]) ? 'document' : 'embedded'
+ output = @converter.convert self, transform
end
if @extensions && !@parent_document
@@ -815,15 +869,54 @@ class Document < AbstractBlock
output = ext.process_method[self, output]
end
end
- #@extensions.reset
end
output
end
+ # Alias render to convert to maintain backwards compatibility
+ alias :render :convert
+
+ # Public: Write the output to the specified file
+ #
+ # If the converter responds to :write, delegate the work of writing the file
+ # to that method. Otherwise, write the output the specified file.
+ def write output, target
+ if @converter.is_a? Writer
+ @converter.write output, target
+ else
+ if target.respond_to? :write
+ target.write output.chomp
+ # ensure there's a trailing endline
+ target.write EOL
+ else
+ ::File.open(target, 'w') {|f| f.write output }
+ end
+ nil
+ end
+ end
+
+=begin
+ def convert_to target, opts = {}
+ start = ::Time.now.to_f if (monitor = opts[:monitor])
+ output = (r = converter opts).convert
+ monitor[:convert] = ::Time.now.to_f - start if monitor
+
+ unless target.respond_to? :write
+ @attributes['outfile'] = target = ::File.expand_path target
+ @attributes['outdir'] = ::File.dirname target
+ end
+
+ start = ::Time.now.to_f if monitor
+ r.write output, target
+ monitor[:write] = ::Time.now.to_f - start if monitor
+
+ output
+ end
+=end
+
def content
- # per AsciiDoc-spec, remove the title before rendering the body,
- # regardless of whether the header is rendered)
+ # NOTE per AsciiDoc-spec, remove the title before converting the body
@attributes.delete('title')
super
end
diff --git a/lib/asciidoctor/extensions.rb b/lib/asciidoctor/extensions.rb
index 4fc4584c..f7aab458 100644
--- a/lib/asciidoctor/extensions.rb
+++ b/lib/asciidoctor/extensions.rb
@@ -1,5 +1,5 @@
module Asciidoctor
-# Extensions provide a way to participate in the parsing and rendering
+# Extensions provide a way to participate in the parsing and converting
# phases of the AsciiDoc processor or extend the AsciiDoc syntax.
#
# The various extensions participate in AsciiDoc processing as follows:
@@ -11,9 +11,9 @@ module Asciidoctor
# Custom blocks and block macros are processed by associated {BlockProcessor}s
# and {BlockMacroProcessor}s, respectively.
# 3. {Treeprocessor}s are run on the abstract syntax tree.
-# 4. Rendering of the document begins, at which point inline markup is processed
-# and rendered. Custom inline macros are processed by associated {InlineMacroProcessor}s.
-# 5. {Postprocessor}s modify or replace the rendered document.
+# 4. Conversion of the document begins, at which point inline markup is processed
+# and converted. Custom inline macros are processed by associated {InlineMacroProcessor}s.
+# 5. {Postprocessor}s modify or replace the converted document.
# 6. The output is written to the output stream.
#
# Extensions may be registered globally using the {Extensions.register} method
@@ -198,14 +198,14 @@ module Extensions
end
Treeprocessor::DSL = ProcessorDsl
- # Public: Postprocessors are run after the document is rendered, but before
+ # Public: Postprocessors are run after the document is converted, but before
# it is written to the output stream.
#
- # Asciidoctor passes a reference to the rendered String to the {Processor#process}
+ # Asciidoctor passes a reference to the converted String to the {Processor#process}
# method of each registered Postprocessor. The Preprocessor modifies the
# String as necessary and returns the String replacement.
#
- # The markup format in the String is determined by the backend used to render
+ # The markup format in the String is determined by the backend used to convert
# the Document. The backend and be looked up using the backend method on the
# Document object, as well as various backend-related document attributes.
#
@@ -617,7 +617,7 @@ module Extensions
end
# Public: Registers a {Postprocessor} with the extension registry to process
- # the output after rendering is complete.
+ # the output after conversion is complete.
#
# The Postprocessor may be one of four types:
#
diff --git a/lib/asciidoctor/inline.rb b/lib/asciidoctor/inline.rb
index e74191e2..5f6e0745 100644
--- a/lib/asciidoctor/inline.rb
+++ b/lib/asciidoctor/inline.rb
@@ -1,9 +1,6 @@
module Asciidoctor
# Public: Methods for managing inline elements in AsciiDoc block
class Inline < AbstractNode
- # Public: Get/Set the String name of the render template
- attr_accessor :template_name
-
# Public: Get the text of this inline element
attr_reader :text
@@ -15,7 +12,7 @@ class Inline < AbstractNode
def initialize(parent, context, text = nil, opts = {})
super(parent, context)
- @template_name = %(inline_#{context})
+ @node_name = %(inline_#{context})
@text = text
@@ -28,9 +25,19 @@ class Inline < AbstractNode
end
end
- def render
- renderer.render(@template_name, self)
+ def block?
+ false
+ end
+
+ def inline?
+ true
end
+ def convert
+ converter.convert self
+ end
+
+ # Alias render to convert to maintain backwards compatibility
+ alias :render :convert
end
end
diff --git a/lib/asciidoctor/list.rb b/lib/asciidoctor/list.rb
index 43384fd2..e5ad42c7 100644
--- a/lib/asciidoctor/list.rb
+++ b/lib/asciidoctor/list.rb
@@ -15,7 +15,7 @@ class List < AbstractBlock
@blocks
end
- def render
+ def convert
if @context == :colist
result = super
@document.callouts.next_list
@@ -25,6 +25,9 @@ class List < AbstractBlock
end
end
+ # Alias render to convert to maintain backwards compatibility
+ alias :render :convert
+
def to_s
%(#{self.class}@#{object_id} { context: #{@context.inspect}, style: #{@style.inspect}, items: #{items.size} })
end
diff --git a/lib/asciidoctor/parser.rb b/lib/asciidoctor/parser.rb
index b4b56463..fdd1e76a 100644
--- a/lib/asciidoctor/parser.rb
+++ b/lib/asciidoctor/parser.rb
@@ -489,7 +489,7 @@ class Parser
# process lines normally
unless text_only
first_char = Compliance.markdown_syntax ? this_line.lstrip.chr : this_line.chr
- # NOTE we're letting break lines (ruler, page_break, etc) have attributes
+ # NOTE we're letting break lines (horizontal rule, page_break, etc) have attributes
if (LAYOUT_BREAK_LINES.has_key? first_char) && this_line.length >= 3 &&
(Compliance.markdown_syntax ? LayoutBreakLinePlusRx : LayoutBreakLineRx) =~ this_line
block = Block.new(parent, LAYOUT_BREAK_LINES[first_char], :content_model => :empty)
diff --git a/lib/asciidoctor/renderer.rb b/lib/asciidoctor/renderer.rb
deleted file mode 100644
index 4128047c..00000000
--- a/lib/asciidoctor/renderer.rb
+++ /dev/null
@@ -1,273 +0,0 @@
-module Asciidoctor
-# Public: Methods for rendering Asciidoc Documents, Sections, and Blocks
-# using eRuby templates.
-class Renderer
- RE_ASCIIDOCTOR_NAMESPACE = /^Asciidoctor::/
- RE_TEMPLATE_CLASS_SUFFIX = /Template$/
- RE_CAMELCASE_BOUNDARY_1 = /([[:upper:]]+)([[:upper:]][a-zA-Z])/
- RE_CAMELCASE_BOUNDARY_2 = /([[:lower:]])([[:upper:]])/
-
- attr_reader :compact
- attr_reader :cache
-
- @@global_cache = nil
-
- # Public: Initialize an Asciidoctor::Renderer object.
- #
- def initialize(options={})
- @debug = !!options[:debug]
-
- @views = {}
- @compact = options[:compact]
- @cache = nil
- @chomp_result = false
-
- backend = options[:backend]
- if ::RUBY_ENGINE_OPAL
- @chomp_result = true
- ::Template.instance_variable_get('@_cache').each do |path, tmpl|
- @views[(::File.basename path)] = tmpl
- end
- return
- end
- case backend
- when 'html5', 'docbook45', 'docbook5'
- require 'asciidoctor/backends/base_template'
- require "asciidoctor/backends/#{backend}"
- # Load up all the template classes that we know how to render for this backend
- BaseTemplate.template_classes.each do |tc|
- if tc.to_s.downcase.include?('::' + backend + '::') # optimization
- view_name, view_backend = self.class.extract_view_mapping(tc)
- if view_backend == backend
- @views[view_name] = tc.new(view_name, backend)
- end
- end
- end
- else
- Debug.debug { "No built-in templates for backend: #{backend}" }
- end
-
- # If user passed in a template dir, let them override our base templates
- if (template_dirs = options.delete(:template_dirs))
- Helpers.require_library 'tilt'
-
- # chomp result when using custom templates since template engines
- # tend to add a trailing newline character
- # this setting should really be applied per template
- @chomp_result = true
-
- if (template_cache = options[:template_cache]) == true
- # FIXME probably want to use our own cache object for more control
- @cache = (@@global_cache ||= TemplateCache.new)
- elsif template_cache
- @cache = template_cache
- end
-
- view_opts = {
- :erb => { :trim => '<' },
- :haml => { :format => :xhtml, :attr_wrapper => '"', :ugly => true, :escape_attrs => false },
- :slim => { :disable_escape => true, :sort_attrs => false, :pretty => false }
- }
-
- # workaround until we have a proper way to configure view options
- if options[:htmlsyntax] == 'html'
- view_opts[:haml][:format] = view_opts[:slim][:format] = :html5
- end
-
- eruby = nil
- path_resolver = PathResolver.new
- engine = options[:template_engine]
-
- template_dirs.each do |template_dir|
- # TODO need to think about safe mode restrictions here
- template_dir = path_resolver.system_path template_dir, nil
- template_glob = '*'
- if engine
- template_glob = "*.#{engine}"
- # example: templates/haml
- if File.directory? File.join(template_dir, engine)
- template_dir = File.join template_dir, engine
- end
- end
-
- # example: templates/html5 or templates/haml/html5
- if File.directory? File.join(template_dir, backend)
- template_dir = File.join template_dir, backend
- end
-
- # skip scanning folder if we've already done it for same backend/engine
- if @cache && @cache.cached?(:scan, template_dir, template_glob)
- @views.update(@cache.fetch :scan, template_dir, template_glob)
- next
- end
-
- helpers = nil
- scan_result = {}
- # Grab the files in the top level of the directory (we're not traversing)
- ::Dir.glob(::File.join(template_dir, template_glob)).
- select{|f| ::File.file? f }.each do |template|
- basename = ::File.basename(template)
- if basename == 'helpers.rb'
- helpers = template
- next
- end
- name_parts = basename.split('.')
- next if name_parts.size < 2
- view_name = name_parts[0]
- ext_name = name_parts[-1]
- if ext_name == 'slim'
- # slim doesn't get loaded by Tilt, so we have to load it explicitly
- Helpers.require_library 'slim' unless defined? ::Slim
- elsif ext_name == 'erb'
- eruby = load_eruby options[:eruby] unless eruby
- end
- next unless ::Tilt.registered? ext_name
- opts = view_opts[ext_name.to_sym]
- if @cache
- @views[view_name] = scan_result[view_name] = @cache.fetch(:view, template) {
- ::Tilt.new(template, nil, opts)
- }
- else
- @views[view_name] = ::Tilt.new template, nil, opts
- end
- end
-
- require helpers unless helpers.nil?
- @cache.store(scan_result, :scan, template_dir, template_glob) if @cache
- end
- end
- end
-
- # Public: Render an Asciidoc object with a specified view template.
- #
- # view - the String view template name.
- # object - the Object to be used as an evaluation scope.
- # locals - the optional Hash of locals to be passed to Tilt (default {}) (also ignored, really)
- def render(view, object, locals = {})
- unless (view_impl = @views[view])
- raise "Couldn't find a view in @views for #{view}"
- end
-
- @chomp_result ? view_impl.render(object, locals).chomp : view_impl.render(object, locals)
- end
-
- def views
- readonly_views = @views.dup
- readonly_views.freeze
- readonly_views
- end
-
- def register_view(view_name, tilt_template)
- # TODO need to figure out how to cache this
- @views[view_name] = tilt_template
- end
-
- # Internal: Load the eRuby implementation
- #
- # name - the String name of the eRuby implementation (default: 'erb')
- #
- # returns the eRuby implementation class
- def load_eruby(name)
- if name.nil? || !['erb', 'erubis'].include?(name)
- name = 'erb'
- end
-
- if name == 'erb'
- Helpers.require_library 'erb'
- ::ERB
- elsif name == 'erubis'
- Helpers.require_library 'erubis'
- ::Erubis::FastEruby
- end
- end
-
- # TODO better name for this method (and/or field)
- def self.global_cache
- @@global_cache
- end
-
- # TODO better name for this method (and/or field)
- def self.reset_global_cache
- @@global_cache.clear if @@global_cache
- end
-
- # Internal: Extracts the view name and backend from a qualified Ruby class
- #
- # The purpose of this method is to determine the view name and backend to
- # which a built-in template class maps. We can make certain assumption since
- # we have control over these class names. The Asciidoctor:: prefix and
- # Template suffix are stripped as the first step in the conversion.
- #
- # qualified_class - The Class or String qualified class name from which to extract the view name and backend
- #
- # Examples
- #
- # Renderer.extract_view_mapping(Asciidoctor::HTML5::DocumentTemplate)
- # # => ['document', 'html5']
- #
- # Renderer.extract_view_mapping(Asciidoctor::DocBook45::BlockSidebarTemplate)
- # # => ['block_sidebar', 'docbook45']
- #
- # Returns A two-element String Array mapped as [view_name, backend], where backend may be nil
- def self.extract_view_mapping(qualified_class)
- view_name, backend = qualified_class.to_s.
- sub(RE_ASCIIDOCTOR_NAMESPACE, '').
- sub(RE_TEMPLATE_CLASS_SUFFIX, '').
- split('::').reverse
- view_name = camelcase_to_underscore(view_name)
- backend = backend.downcase unless backend.nil?
- [view_name, backend]
- end
-
- # Internal: Convert a CamelCase word to an underscore-delimited word
- #
- # Examples
- #
- # Renderer.camelcase_to_underscore('BlockSidebar')
- # # => 'block_sidebar'
- #
- # Renderer.camelcase_to_underscore('BlockUlist')
- # # => 'block_ulist'
- #
- # Returns the String converted from CamelCase to underscore-delimited
- def self.camelcase_to_underscore(str)
- str.gsub(RE_CAMELCASE_BOUNDARY_1, '\1_\2').
- gsub(RE_CAMELCASE_BOUNDARY_2, '\1_\2').downcase
- end
-
-end
-
-class TemplateCache
- attr_reader :cache
-
- def initialize
- @cache = {}
- end
-
- # check if a key is available in the cache
- def cached? *key
- @cache.has_key? key
- end
-
- # retrieves an item from the cache stored in the cache key
- # if a block is given, the block is called and the return
- # value stored in the cache under the specified key
- def fetch(*key)
- if block_given?
- @cache[key] ||= yield
- else
- @cache[key]
- end
- end
-
- # stores an item in the cache under the specified key
- def store(value, *key)
- @cache[key] = value
- end
-
- # Clears the cache
- def clear
- @cache = {}
- end
-end
-end
diff --git a/lib/asciidoctor/section.rb b/lib/asciidoctor/section.rb
index e8f83174..8f9503cd 100644
--- a/lib/asciidoctor/section.rb
+++ b/lib/asciidoctor/section.rb
@@ -41,7 +41,6 @@ class Section < AbstractBlock
# parent - The parent Asciidoc Object.
def initialize(parent = nil, level = nil, numbered = true)
super(parent, :section)
- @template_name = 'section'
if level.nil?
if parent
@level = parent.level + 1
diff --git a/lib/asciidoctor/stylesheets.rb b/lib/asciidoctor/stylesheets.rb
new file mode 100644
index 00000000..5333a95b
--- /dev/null
+++ b/lib/asciidoctor/stylesheets.rb
@@ -0,0 +1,91 @@
+module Asciidoctor
+# A utility class for working with the built-in stylesheets.
+#--
+# QUESTION create methods for link_*_stylesheet?
+# QUESTION create method for user stylesheet?
+class Stylesheets
+ DEFAULT_STYLESHEET_NAME = 'asciidoctor.css'
+ #DEFAULT_CODERAY_STYLE = 'asciidoctor'
+ DEFAULT_PYGMENTS_STYLE = 'pastie'
+ STYLESHEETS_DATA_PATH = ::File.join DATA_PATH, 'stylesheets'
+
+ @__instance__ = new
+
+ def self.instance
+ @__instance__
+ end
+
+ def primary_stylesheet_name
+ DEFAULT_STYLESHEET_NAME
+ end
+
+ # Public: Read the contents of the default Asciidoctor stylesheet
+ #
+ # returns the [String] Asciidoctor stylesheet data
+ def primary_stylesheet_data
+ @primary_stylesheet_data ||= ::IO.read(::File.join(STYLESHEETS_DATA_PATH, 'asciidoctor-default.css')).chomp
+ end
+
+ def embed_primary_stylesheet
+ %(<style>
+#{primary_stylesheet_data}
+</style>)
+ end
+
+ def write_primary_stylesheet target_dir
+ ::File.open(::File.join(target_dir, primary_stylesheet_name), 'w') {|f| f.write primary_stylesheet_data }
+ end
+
+ def coderay_stylesheet_name
+ 'coderay-asciidoctor.css'
+ end
+
+ # Public: Read the contents of the default CodeRay stylesheet
+ #
+ # returns the [String] CodeRay stylesheet data
+ def coderay_stylesheet_data
+ # NOTE use the following two lines to load a built-in theme instead
+ # Helpers.require_library 'coderay'
+ # ::CodeRay::Encoders[:html]::CSS.new(:default).stylesheet
+ @coderay_stylesheet_data ||= ::IO.read(::File.join(STYLESHEETS_DATA_PATH, 'coderay-asciidoctor.css')).chomp
+ end
+
+ def embed_coderay_stylesheet
+ %(<style>
+#{coderay_stylesheet_data}
+</style>)
+ end
+
+ def write_coderay_stylesheet target_dir
+ ::File.open(::File.join(target_dir, coderay_stylesheet_name), 'w') {|f| f.write coderay_stylesheet_data }
+ end
+
+ def pygments_stylesheet_name style = nil
+ style ||= DEFAULT_PYGMENTS_STYLE
+ %(pygments-#{style}.css)
+ end
+
+ # Public: Generate the Pygments stylesheet with the specified style.
+ #
+ # returns the [String] Pygments stylesheet data
+ def pygments_stylesheet_data style = nil
+ style ||= DEFAULT_PYGMENTS_STYLE
+ (@pygments_stylesheet_data ||= load_pygments)[style] ||= ::Pygments.css '.listingblock pre.highlight', :classprefix => 'tok-', :style => style
+ end
+
+ def embed_pygments_stylesheet style = nil
+ %(<style>
+#{pygments_stylesheet_data style}
+</style>)
+ end
+
+ def write_pygments_stylesheet target_dir, style = nil
+ ::File.open(::File.join(target_dir, pygments_stylesheet_name(style)), 'w') {|f| f.write pygments_stylesheet_data(style) }
+ end
+
+ def load_pygments
+ Helpers.require_library 'pygments', 'pygments.rb' unless defined? ::Pygments
+ {}
+ end
+end
+end
diff --git a/lib/asciidoctor/substitutors.rb b/lib/asciidoctor/substitutors.rb
index 8cf55ab3..cb4d9604 100644
--- a/lib/asciidoctor/substitutors.rb
+++ b/lib/asciidoctor/substitutors.rb
@@ -19,6 +19,7 @@ module Substitutors
:title => [:specialcharacters, :quotes, :replacements, :macros, :attributes, :post_replacements],
:header => [:specialcharacters, :attributes],
# by default, AsciiDoc performs :attributes and :macros on a pass block
+ # TODO make this a compliance setting
:pass => []
}
@@ -95,8 +96,7 @@ module Substitutors
return source if subs.empty?
- multiline = source.is_a? ::Array
- text = multiline ? (source * EOL) : source
+ text = (multiline = source.is_a? ::Array) ? (source * EOL) : source
if (has_passthroughs = subs.include? :macros)
text = extract_passthroughs text
@@ -249,7 +249,7 @@ module Substitutors
pass = @passthroughs[$~[1].to_i]
subbed_text = (subs = pass[:subs]) ? (apply_subs pass[:text], subs) : pass[:text]
if (type = pass[:type])
- Inline.new(self, :quoted, subbed_text, :type => type, :attributes => pass[:attributes]).render
+ Inline.new(self, :quoted, subbed_text, :type => type, :attributes => pass[:attributes]).convert
else
subbed_text
end
@@ -274,19 +274,19 @@ module Substitutors
#
# text - The String text to process
#
- # returns The String text with quoted text rendered using the backend templates
+ # returns The converted String text
def sub_quotes(text)
if ::RUBY_ENGINE_OPAL
result = text
QUOTE_SUBS.each {|type, scope, pattern|
- result = result.gsub(pattern) { transform_quoted_text $~, type, scope }
+ result = result.gsub(pattern) { convert_quoted_text $~, type, scope }
}
else
# NOTE interpolation is faster than String#dup
result = %(#{text})
# NOTE using gsub! as optimization
QUOTE_SUBS.each {|type, scope, pattern|
- result.gsub!(pattern) { transform_quoted_text $~, type, scope }
+ result.gsub!(pattern) { convert_quoted_text $~, type, scope }
}
end
@@ -429,7 +429,7 @@ module Substitutors
#
# source - The String text to process
#
- # returns The String with the inline macros rendered using the backend templates
+ # returns The converted String text
def sub_macros(source)
return source if source.nil_or_empty?
@@ -473,10 +473,10 @@ module Substitutors
c
}
end
- Inline.new(self, :kbd, nil, :attributes => {'keys' => keys}).render
+ Inline.new(self, :kbd, nil, :attributes => {'keys' => keys}).convert
elsif captured.start_with?('btn')
label = unescape_bracketed_text m[1]
- Inline.new(self, :button, label).render
+ Inline.new(self, :button, label).convert
end
}
end
@@ -506,7 +506,7 @@ module Substitutors
end
end
- Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).render
+ Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).convert
}
end
@@ -523,7 +523,7 @@ module Substitutors
menu, *submenus = input.split('&gt;').map {|it| it.strip }
menuitem = submenus.pop
- Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).render
+ Inline.new(self, :menu, nil, :attributes => {'menu' => menu, 'submenus' => submenus, 'menuitem' => menuitem}).convert
}
end
end
@@ -579,7 +579,7 @@ module Substitutors
end
attrs = parse_attributes(raw_attrs, posattrs)
attrs['alt'] ||= File.basename(target, File.extname(target))
- Inline.new(self, :image, nil, :type => type, :target => target, :attributes => attrs).render
+ Inline.new(self, :image, nil, :type => type, :target => target, :attributes => attrs).convert
}
end
@@ -618,7 +618,7 @@ module Substitutors
terms = split_simple_csv normalize_string(m[2], true)
end
@document.register(:indexterms, [*terms])
- Inline.new(self, :indexterm, nil, :attributes => {'terms' => terms}).render
+ Inline.new(self, :indexterm, nil, :attributes => {'terms' => terms}).convert
# visible
else
if !macro_name
@@ -629,7 +629,7 @@ module Substitutors
text = normalize_string m[2], true
end
@document.register(:indexterms, [text])
- Inline.new(self, :indexterm, text, :type => :visible).render
+ Inline.new(self, :indexterm, text, :type => :visible).convert
end
}
end
@@ -710,7 +710,7 @@ module Substitutors
end
end
- "#{prefix}#{Inline.new(self, :anchor, text, :type => :link, :target => target, :attributes => attrs).render}#{suffix}"
+ "#{prefix}#{Inline.new(self, :anchor, text, :type => :link, :target => target, :attributes => attrs).convert}#{suffix}"
}
end
@@ -762,7 +762,7 @@ module Substitutors
end
end
- Inline.new(self, :anchor, text, :type => :link, :target => target, :attributes => attrs).render
+ Inline.new(self, :anchor, text, :type => :link, :target => target, :attributes => attrs).convert
}
end
@@ -784,7 +784,7 @@ module Substitutors
# QUESTION should this be registered as an e-mail address?
@document.register(:links, target)
- Inline.new(self, :anchor, address, :type => :link, :target => target).render
+ Inline.new(self, :anchor, address, :type => :link, :target => target).convert
}
end
@@ -827,7 +827,7 @@ module Substitutors
type = :xref
end
end
- Inline.new(self, :footnote, text, :attributes => {'index' => index}, :id => id, :target => target, :type => type).render
+ Inline.new(self, :footnote, text, :attributes => {'index' => index}, :id => id, :target => target, :type => type).convert
}
end
@@ -845,7 +845,7 @@ module Substitutors
next m[0][1..-1]
end
id = reftext = m[1]
- Inline.new(self, :anchor, reftext, :type => :bibref, :target => id).render
+ Inline.new(self, :anchor, reftext, :type => :bibref, :target => id).convert
}
end
@@ -876,7 +876,7 @@ module Substitutors
else
Debug.debug { "Missing reference for anchor #{id}" }
end
- Inline.new(self, :anchor, reftext, :type => :ref, :target => id).render
+ Inline.new(self, :anchor, reftext, :type => :ref, :target => id).convert
}
end
@@ -927,7 +927,7 @@ module Substitutors
target = fragment ? %(#{path}##{fragment}) : path
end
end
- Inline.new(self, :anchor, reftext, :type => :xref, :target => target, :attributes => {'path' => path, 'fragment' => fragment, 'refid' => refid}).render
+ Inline.new(self, :anchor, reftext, :type => :xref, :target => target, :attributes => {'path' => path, 'fragment' => fragment, 'refid' => refid}).convert
}
end
@@ -938,9 +938,9 @@ module Substitutors
#
# text - The String text to process
#
- # returns The String with the callout references rendered using the backend templates
+ # Returns the converted String text
def sub_callouts(text)
- text.gsub(CalloutRenderRx) {
+ text.gsub(CalloutConvertRx) {
# alias match for Ruby 1.8.7 compat
m = $~
# honor the escape
@@ -948,7 +948,7 @@ module Substitutors
# we have to do a sub since we aren't sure it's the first char
next m[0].sub('\\', '')
end
- Inline.new(self, :callout, m[3], :id => @document.callouts.read_next_id).render
+ Inline.new(self, :callout, m[3], :id => @document.callouts.read_next_id).convert
}
end
@@ -956,49 +956,49 @@ module Substitutors
#
# text - The String text to process
#
- # returns The String with the post replacements rendered using the backend templates
+ # Returns the converted String text
def sub_post_replacements(text)
if (@document.attributes.has_key? 'hardbreaks') || (@attributes.has_key? 'hardbreaks-option')
lines = (text.split EOL)
return text if lines.size == 1
last = lines.pop
- lines.map {|line| Inline.new(self, :break, line.rstrip.chomp(LINE_BREAK), :type => :line).render }.push(last) * EOL
+ lines.map {|line| Inline.new(self, :break, line.rstrip.chomp(LINE_BREAK), :type => :line).convert }.push(last) * EOL
elsif text.include? '+'
- text.gsub(LineBreakRx) { Inline.new(self, :break, $~[1], :type => :line).render }
+ text.gsub(LineBreakRx) { Inline.new(self, :break, $~[1], :type => :line).convert }
else
text
end
end
- # Internal: Transform (render) a quoted text region
+ # Internal: Convert a quoted text region
#
# match - The MatchData for the quoted text region
# type - The quoting type (single, double, strong, emphasis, monospaced, etc)
# scope - The scope of the quoting (constrained or unconstrained)
#
- # returns The rendered text for the quoted text region
- def transform_quoted_text(match, type, scope)
+ # Returns The converted String text for the quoted text region
+ def convert_quoted_text(match, type, scope)
unescaped_attrs = nil
if match[0].start_with? '\\'
- if scope == :constrained && match[2]
- unescaped_attrs = %([#{match[2]}])
+ if scope == :constrained && (attrs = match[2])
+ unescaped_attrs = %([#{attrs}])
else
return match[0][1..-1]
end
end
if scope == :constrained
- if !unescaped_attrs
+ if unescaped_attrs
+ %(#{unescaped_attrs}#{Inline.new(self, :quoted, match[3], :type => type).convert})
+ else
attributes = parse_quoted_text_attributes(match[2])
id = attributes ? attributes.delete('id') : nil
- %(#{match[1]}#{Inline.new(self, :quoted, match[3], :type => type, :id => id, :attributes => attributes).render})
- else
- %(#{unescaped_attrs}#{Inline.new(self, :quoted, match[3], :type => type).render})
+ %(#{match[1]}#{Inline.new(self, :quoted, match[3], :type => type, :id => id, :attributes => attributes).convert})
end
else
attributes = parse_quoted_text_attributes(match[1])
id = attributes ? attributes.delete('id') : nil
- Inline.new(self, :quoted, match[2], :type => type, :id => id, :attributes => attributes).render
+ Inline.new(self, :quoted, match[2], :type => type, :id => id, :attributes => attributes).convert
end
end
@@ -1224,7 +1224,7 @@ module Substitutors
# on the document, otherwise return the text unprocessed
#
# Callout marks are stripped from the source prior to passing it to the
- # highlighter, then later restored in rendered form, so they are not
+ # highlighter, then later restored in converted form, so they are not
# incorrectly processed by the source highlighter.
#
# source - the source code String to highlight
@@ -1318,9 +1318,9 @@ module Substitutors
line = line[0...pos]
end
if conums.size == 1
- %(#{line}#{Inline.new(self, :callout, conums[0], :id => @document.callouts.read_next_id).render }#{tail})
+ %(#{line}#{Inline.new(self, :callout, conums[0], :id => @document.callouts.read_next_id).convert }#{tail})
else
- conums_markup = conums.map {|conum| Inline.new(self, :callout, conum, :id => @document.callouts.read_next_id).render } * ' '
+ conums_markup = conums.map {|conum| Inline.new(self, :callout, conum, :id => @document.callouts.read_next_id).convert } * ' '
%(#{line}#{conums_markup}#{tail})
end
else
diff --git a/lib/asciidoctor/table.rb b/lib/asciidoctor/table.rb
index 0a23768d..cd96c9b3 100644
--- a/lib/asciidoctor/table.rb
+++ b/lib/asciidoctor/table.rb
@@ -250,11 +250,11 @@ class Table::Cell < AbstractNode
# Public: Handles the body data (tbody, tfoot), applying styles and partitioning into paragraphs
def content
if @style == :asciidoc
- @inner_document.render
+ @inner_document.convert
else
- text.split(BlankLineRx).map {|p|
- !@style || @style == :header ? p : Inline.new(parent, :quoted, p, :type => @style).render
- }
+ text.split(BlankLineRx).map do |p|
+ !@style || @style == :header ? p : Inline.new(parent, :quoted, p, :type => @style).convert
+ end
end
end
diff --git a/lib/asciidoctor/timings.rb b/lib/asciidoctor/timings.rb
new file mode 100644
index 00000000..309cfbda
--- /dev/null
+++ b/lib/asciidoctor/timings.rb
@@ -0,0 +1,48 @@
+module Asciidoctor
+ class Timings
+ attr_reader :read, :parse, :convert, :write
+ def initialize
+ @read = @parse = @convert = @write = nil
+ @timers = {}
+ end
+
+ def start key
+ @timers[key] = ::Time.now
+ end
+
+ def record key
+ instance_variable_set %(@#{key}), (::Time.now - (@timers.delete key))
+ end
+
+ def read_parse
+ if @read || @parse
+ (@read || 0) + (@parse || 0)
+ else
+ nil
+ end
+ end
+
+ def read_parse_convert
+ if @read || @parse || @convert
+ (@read || 0) + (@parse || 0) + (@convert || 0)
+ else
+ nil
+ end
+ end
+
+ def total
+ if @read || @parse || @convert || @write
+ (@read || 0) + (@parse || 0) + (@convert || 0) + (@write || 0)
+ else
+ nil
+ end
+ end
+
+ def print_report to = $stdout, subject = nil
+ to.puts %(Input file: #{subject}) if subject
+ to.puts %( Time to read and parse source: #{'%05.5f' % read_parse})
+ to.puts %( Time to convert document: #{@convert ? '%05.5f' % @convert : 'n/a'})
+ to.puts %( Total time to read, parse and convert: #{'%05.5f' % read_parse_convert})
+ end
+ end
+end
diff --git a/man/asciidoctor.adoc b/man/asciidoctor.adoc
index 420b5d37..043a5e3d 100644
--- a/man/asciidoctor.adoc
+++ b/man/asciidoctor.adoc
@@ -79,7 +79,7 @@ This option may be specified more than once.
=== Rendering Control
*-C, --compact*::
- Compact the output by removing blank lines. Not enabled by default.
+ Compact the output by removing blank lines. (No longer in use).
*-D, --destination-dir*='DIR'::
Destination output directory. Defaults to the directory containing the
@@ -89,10 +89,12 @@ This option may be specified more than once.
*-E, --template-engine*='NAME'::
Template engine to use for the custom render templates. The gem with the
same name as the engine will be loaded automatically. This name is also
- used to build the full path to the custom templates.
+ used to build the full path to the custom templates. If a template engine
+ is not specified, it will be auto-detected based on the file extension
+ of the custom templates found.
*-e, --eruby*::
- Specifies the eRuby implementation to use for rendering the built-in
+ Specifies the eRuby implementation to use for rendering the custom ERB
templates. Supported values are 'erb' and 'erubis'. Defaults to 'erb'.
*-n, --section-numbers*::
diff --git a/run-tests.sh b/run-tests.sh
new file mode 100755
index 00000000..5529a4f4
--- /dev/null
+++ b/run-tests.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+# A convenience script to run tests without delays caused by incrementally writing to the terminal buffer
+rake > /tmp/asciidoctor-test-results.txt 2>&1; cat /tmp/asciidoctor-test-results.txt
diff --git a/test/attributes_test.rb b/test/attributes_test.rb
index 087bb8de..f6b588a8 100644
--- a/test/attributes_test.rb
+++ b/test/attributes_test.rb
@@ -149,12 +149,94 @@ endif::holygrail[]
assert_equal nil, doc.attributes['cash']
end
+ test 'backend and doctype attributes are set by default in default configuration' do
+ input = <<-EOS
+= Document Title
+Author Name
+
+content
+ EOS
+
+ doc = document_from_string input
+ expect = {
+ 'backend' => 'html5',
+ 'backend-html5' => '',
+ 'backend-html5-doctype-article' => '',
+ 'basebackend' => 'html',
+ 'basebackend-html' => '',
+ 'basebackend-html-doctype-article' => '',
+ 'doctype' => 'article',
+ 'doctype-article' => '',
+ 'filetype' => 'html',
+ 'filetype-html' => ''
+ }
+ expect.each do |key, val|
+ assert doc.attributes.key? key
+ assert_equal val, doc.attributes[key]
+ end
+ end
+
+ test 'backend and doctype attributes are set by default in custom configuration' do
+ input = <<-EOS
+= Document Title
+Author Name
+
+content
+ EOS
+
+ doc = document_from_string input, :doctype => 'book', :backend => 'docbook'
+ expect = {
+ 'backend' => 'docbook5',
+ 'backend-docbook5' => '',
+ 'backend-docbook5-doctype-book' => '',
+ 'basebackend' => 'docbook',
+ 'basebackend-docbook' => '',
+ 'basebackend-docbook-doctype-book' => '',
+ 'doctype' => 'book',
+ 'doctype-book' => '',
+ 'filetype' => 'xml',
+ 'filetype-xml' => ''
+ }
+ expect.each do |key, val|
+ assert doc.attributes.key? key
+ assert_equal val, doc.attributes[key]
+ end
+ end
+
test 'backend attributes are updated if backend attribute is defined in document and safe mode is less than SERVER' do
- doc = document_from_string(':backend: docbook45', :safe => Asciidoctor::SafeMode::SAFE)
- 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'
+ input = <<-EOS
+= Document Title
+Author Name
+:backend: docbook
+:doctype: book
+
+content
+ EOS
+
+ doc = document_from_string input, :safe => Asciidoctor::SafeMode::SAFE
+ expect = {
+ 'backend' => 'docbook5',
+ 'backend-docbook5' => '',
+ 'backend-docbook5-doctype-book' => '',
+ 'basebackend' => 'docbook',
+ 'basebackend-docbook' => '',
+ 'basebackend-docbook-doctype-book' => '',
+ 'doctype' => 'book',
+ 'doctype-book' => '',
+ 'filetype' => 'xml',
+ 'filetype-xml' => ''
+ }
+ expect.each do |key, val|
+ assert doc.attributes.key?(key)
+ assert_equal val, doc.attributes[key]
+ end
+
+ assert !doc.attributes.key?('backend-html5')
+ assert !doc.attributes.key?('backend-html5-doctype-article')
+ assert !doc.attributes.key?('basebackend-html')
+ assert !doc.attributes.key?('basebackend-html-doctype-article')
+ assert !doc.attributes.key?('doctype-article')
+ assert !doc.attributes.key?('filetype-html')
end
test 'backend attributes defined in document options overrides backend attribute in document' do
diff --git a/test/blocks_test.rb b/test/blocks_test.rb
index bbe94140..1003ed5c 100644
--- a/test/blocks_test.rb
+++ b/test/blocks_test.rb
@@ -692,8 +692,8 @@ line two
line three
....
EOS
- [[true, true], [true, false], [false, true], [false, false]].each {|compact, header_footer|
- output = render_string input, :header_footer => header_footer, :compact => compact
+ [true, false].each {|header_footer|
+ output = render_string input, :header_footer => header_footer
assert_xpath '//pre', output, 1
assert_xpath '//pre/text()', output, 1
text = xmlnodes_at_xpath('//pre/text()', output, 1).text
@@ -702,11 +702,7 @@ EOS
expected = "line one\n\nline two\n\nline three".lines.entries
assert_equal expected, lines
blank_lines = output.scan(/\n[ \t]*\n/).size
- if compact
- assert_equal 2, blank_lines
- else
- assert blank_lines >= 2
- end
+ assert blank_lines >= 2
}
end
@@ -721,8 +717,8 @@ line two
line three
----
EOS
- [[true, true], [true, false], [false, true], [false, false]].each {|(compact,header_footer)|
- output = render_string input, header_footer => header_footer, :compact => compact
+ [true, false].each {|header_footer|
+ output = render_string input, header_footer => header_footer
assert_xpath '//pre/code', output, 1
assert_xpath '//pre/code/text()', output, 1
text = xmlnodes_at_xpath('//pre/code/text()', output, 1).text
@@ -731,11 +727,7 @@ EOS
expected = "line one\n\nline two\n\nline three".lines.entries
assert_equal expected, lines
blank_lines = output.scan(/\n[ \t]*\n/).size
- if compact
- assert_equal 2, blank_lines
- else
- assert blank_lines >= 2
- end
+ assert blank_lines >= 2
}
end
@@ -752,8 +744,8 @@ line three
____
--
EOS
- [[true, true], [true, false], [false, true], [false, false]].each {|compact, header_footer|
- output = render_string input, :header_footer => header_footer, :compact => compact
+ [true, false].each {|header_footer|
+ output = render_string input, :header_footer => header_footer
assert_xpath '//*[@class="verseblock"]/pre', output, 1
assert_xpath '//*[@class="verseblock"]/pre/text()', output, 1
text = xmlnodes_at_xpath('//*[@class="verseblock"]/pre/text()', output, 1).text
@@ -762,11 +754,7 @@ EOS
expected = "line one\n\nline two\n\nline three".lines.entries
assert_equal expected, lines
blank_lines = output.scan(/\n[ \t]*\n/).size
- if compact
- assert_equal 2, blank_lines
- else
- assert blank_lines >= 2
- end
+ assert blank_lines >= 2
}
end
@@ -792,23 +780,6 @@ last line
assert_xpath %(//pre[text()=" first line\n\nlast line"]), result, 1
end
- test 'should not compact nested document twice' do
- input = <<-EOS
-|===
-a|....
-line one
-
-line two
-
-line three
-....
-|===
- EOS
-
- output = render_string input, :compact => true
- assert_xpath %(//pre[text() = "line one\n\nline two\n\nline three"]), output, 1
- end
-
test 'should process block with CRLF endlines' do
input = <<-EOS
[source]\r
@@ -2098,7 +2069,7 @@ html = CodeRay.scan("puts 'Hello, world!'", :ruby).div(:line_numbers => :table)
EOS
output = render_string input, :safe => Asciidoctor::SafeMode::SAFE, :attributes => {'linkcss' => ''}
assert_xpath '//pre[@class="CodeRay"]/code[@class="ruby language-ruby"]//span[@class = "constant"][text() = "CodeRay"]', output, 1
- assert_css 'link[rel="stylesheet"][href="./asciidoctor-coderay.css"]', output, 1
+ assert_css 'link[rel="stylesheet"][href="./coderay-asciidoctor.css"]', output, 1
end
test 'should highlight source inline if source-highlighter attribute is coderay and coderay-css is style' do
diff --git a/test/converter_test.rb b/test/converter_test.rb
new file mode 100644
index 00000000..2ab1562f
--- /dev/null
+++ b/test/converter_test.rb
@@ -0,0 +1,304 @@
+# encoding: UTF-8
+unless defined? ASCIIDOCTOR_PROJECT_DIR
+ $: << File.dirname(__FILE__); $:.uniq!
+ require 'test_helper'
+end
+require 'tilt' unless defined? ::Tilt
+
+context 'Converter' do
+
+ context 'View options' do
+ test 'should set Haml format to html5 for html5 backend' do
+ doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false
+ assert doc.converter.is_a?(Asciidoctor::Converter::CompositeConverter)
+ selected = doc.converter.find_converter('paragraph')
+ assert selected.is_a? Asciidoctor::Converter::TemplateConverter
+ assert selected.templates['paragraph'].is_a? Tilt::HamlTemplate
+ assert_equal :html5, selected.templates['paragraph'].options[:format]
+ end
+
+ test 'should set Haml format to xhtml for docbook backend' do
+ doc = Asciidoctor::Document.new [], :backend => 'docbook45', :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false
+ assert doc.converter.is_a?(Asciidoctor::Converter::CompositeConverter)
+ selected = doc.converter.find_converter('paragraph')
+ assert selected.is_a? Asciidoctor::Converter::TemplateConverter
+ assert selected.templates['paragraph'].is_a? Tilt::HamlTemplate
+ assert_equal :xhtml, selected.templates['paragraph'].options[:format]
+ end
+
+ test 'should set Slim format to html5 for html5 backend' do
+ doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'slim'), :template_cache => false
+ assert doc.converter.is_a?(Asciidoctor::Converter::CompositeConverter)
+ selected = doc.converter.find_converter('paragraph')
+ assert selected.is_a? Asciidoctor::Converter::TemplateConverter
+ assert selected.templates['paragraph'].is_a? Slim::Template
+ assert_equal :html5, selected.templates['paragraph'].options[:format]
+ end
+
+ test 'should set Slim format to nil for docbook backend' do
+ doc = Asciidoctor::Document.new [], :backend => 'docbook45', :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'slim'), :template_cache => false
+ assert doc.converter.is_a?(Asciidoctor::Converter::CompositeConverter)
+ selected = doc.converter.find_converter('paragraph')
+ assert selected.is_a? Asciidoctor::Converter::TemplateConverter
+ assert selected.templates['paragraph'].is_a? Slim::Template
+ assert_nil selected.templates['paragraph'].options[:format]
+ end
+
+ test 'should support custom template engine options for known engine' do
+ doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'slim'), :template_cache => false, :template_engine_options => { :slim => { :pretty => true } }
+ assert doc.converter.is_a?(Asciidoctor::Converter::CompositeConverter)
+ selected = doc.converter.find_converter('paragraph')
+ assert selected.is_a? Asciidoctor::Converter::TemplateConverter
+ assert selected.templates['paragraph'].is_a? Slim::Template
+ assert_equal true, selected.templates['paragraph'].options[:pretty]
+ end
+
+ test 'should support custom template engine options' do
+ doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'slim'), :template_cache => false, :template_engine_options => { :slim => { :pretty => true } }
+ assert doc.converter.is_a?(Asciidoctor::Converter::CompositeConverter)
+ selected = doc.converter.find_converter('paragraph')
+ assert selected.is_a? Asciidoctor::Converter::TemplateConverter
+ assert selected.templates['paragraph'].is_a? Slim::Template
+ assert_equal false, selected.templates['paragraph'].options[:sort_attrs]
+ assert_equal true, selected.templates['paragraph'].options[:pretty]
+ end
+ end
+
+ context 'Custom backends' do
+ test 'should load Haml templates for default backend' do
+ doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false
+ assert doc.converter.is_a?(Asciidoctor::Converter::CompositeConverter)
+ ['paragraph', 'sidebar'].each do |node_name|
+ selected = doc.converter.find_converter node_name
+ assert selected.is_a? Asciidoctor::Converter::TemplateConverter
+ assert selected.templates[node_name].is_a? Tilt::HamlTemplate
+ assert_equal %(block_#{node_name}.html.haml), File.basename(selected.templates[node_name].file)
+ end
+ end
+
+ test 'should load Haml templates for docbook45 backend' do
+ doc = Asciidoctor::Document.new [], :backend => 'docbook45', :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false
+ assert doc.converter.is_a?(Asciidoctor::Converter::CompositeConverter)
+ ['paragraph'].each do |node_name|
+ selected = doc.converter.find_converter node_name
+ assert selected.is_a? Asciidoctor::Converter::TemplateConverter
+ assert selected.templates[node_name].is_a? Tilt::HamlTemplate
+ assert_equal %(block_#{node_name}.xml.haml), File.basename(selected.templates[node_name].file)
+ end
+ end
+
+ test 'should use Haml templates in place of built-in templates' do
+ input = <<-EOS
+= Document Title
+Author Name
+
+== Section One
+
+Sample paragraph
+
+.Related
+****
+Sidebar content
+****
+ EOS
+
+ output = render_embedded_string input, :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false
+ assert_xpath '/*[@class="sect1"]/*[@class="sectionbody"]/p', output, 1
+ assert_xpath '//aside', output, 1
+ assert_xpath '/*[@class="sect1"]/*[@class="sectionbody"]/p/following-sibling::aside', output, 1
+ assert_xpath '//aside/header/h1[text()="Related"]', output, 1
+ assert_xpath '//aside/header/following-sibling::p[text()="Sidebar content"]', output, 1
+ end
+
+ test 'should use built-in global cache to cache templates' do
+ begin
+ # clear out any cache, just to be sure
+ Asciidoctor::Converter::TemplateConverter.clear_caches if defined? Asciidoctor::Converter::TemplateConverter
+
+ template_dir = File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml')
+ doc = Asciidoctor::Document.new [], :template_dir => template_dir
+ doc.converter
+ caches = Asciidoctor::Converter::TemplateConverter.caches
+ if defined? ::ThreadSafe::Cache
+ assert caches[:templates].is_a?(::ThreadSafe::Cache)
+ assert !caches[:templates].empty?
+ paragraph_template_before = caches[:templates].values.find {|t| File.basename(t.file) == 'block_paragraph.html.haml' }
+ assert !paragraph_template_before.nil?
+
+ # should use cache
+ doc = Asciidoctor::Document.new [], :template_dir => template_dir
+ template_converter = doc.converter.find_converter('paragraph')
+ paragraph_template_after = template_converter.templates['paragraph']
+ assert !paragraph_template_after.nil?
+ assert paragraph_template_before.eql?(paragraph_template_after)
+
+ # should not use cache
+ doc = Asciidoctor::Document.new [], :template_dir => template_dir, :template_cache => false
+ template_converter = doc.converter.find_converter('paragraph')
+ paragraph_template_after = template_converter.templates['paragraph']
+ assert !paragraph_template_after.nil?
+ assert !paragraph_template_before.eql?(paragraph_template_after)
+ else
+ assert caches.empty?
+ end
+ ensure
+ # clean up
+ Asciidoctor::Converter::TemplateConverter.clear_caches if defined? Asciidoctor::Converter::TemplateConverter
+ end
+ end
+
+ test 'should use custom cache to cache templates' do
+ template_dir = File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml')
+ Asciidoctor::PathResolver.new.system_path(File.join(template_dir, 'html5', 'block_paragraph.html.haml'), nil)
+ caches = { :scans => {}, :templates => {} }
+ doc = Asciidoctor::Document.new [], :template_dir => template_dir, :template_cache => caches
+ doc.converter
+ assert !caches[:scans].empty?
+ assert !caches[:templates].empty?
+ paragraph_template = caches[:templates].values.find {|t| File.basename(t.file) == 'block_paragraph.html.haml' }
+ assert !paragraph_template.nil?
+ assert paragraph_template.is_a? ::Tilt::HamlTemplate
+ end
+
+ test 'should be able to disable template cache' do
+ begin
+ # clear out any cache, just to be sure
+ Asciidoctor::Converter::TemplateConverter.clear_caches if defined? Asciidoctor::Converter::TemplateConverter
+
+ doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'),
+ :template_cache => false
+ doc.converter
+ caches = Asciidoctor::Converter::TemplateConverter.caches
+ assert caches.empty? || caches[:scans].empty?
+ assert caches.empty? || caches[:templates].empty?
+ ensure
+ # clean up
+ Asciidoctor::Converter::TemplateConverter.clear_caches if defined? Asciidoctor::Converter::TemplateConverter
+ end
+ end
+
+ test 'should load Slim templates for default backend' do
+ doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'slim'), :template_cache => false
+ assert doc.converter.is_a?(Asciidoctor::Converter::CompositeConverter)
+ ['paragraph', 'sidebar'].each do |node_name|
+ selected = doc.converter.find_converter node_name
+ assert selected.is_a? Asciidoctor::Converter::TemplateConverter
+ assert selected.templates[node_name].is_a? Slim::Template
+ assert_equal %(block_#{node_name}.html.slim), File.basename(selected.templates[node_name].file)
+ end
+ end
+
+ test 'should load Slim templates for docbook45 backend' do
+ doc = Asciidoctor::Document.new [], :backend => 'docbook45', :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'slim'), :template_cache => false
+ assert doc.converter.is_a?(Asciidoctor::Converter::CompositeConverter)
+ ['paragraph'].each do |node_name|
+ selected = doc.converter.find_converter node_name
+ assert selected.is_a? Asciidoctor::Converter::TemplateConverter
+ assert selected.templates[node_name].is_a? Slim::Template
+ assert_equal %(block_#{node_name}.xml.slim), File.basename(selected.templates[node_name].file)
+ end
+ end
+
+ test 'should use Slim templates in place of built-in templates' do
+ input = <<-EOS
+= Document Title
+Author Name
+
+== Section One
+
+Sample paragraph
+
+.Related
+****
+Sidebar content
+****
+ EOS
+
+ output = render_embedded_string input, :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'slim'), :template_cache => false
+ assert_xpath '/*[@class="sect1"]/*[@class="sectionbody"]/p', output, 1
+ assert_xpath '//aside', output, 1
+ assert_xpath '/*[@class="sect1"]/*[@class="sectionbody"]/p/following-sibling::aside', output, 1
+ assert_xpath '//aside/header/h1[text()="Related"]', output, 1
+ assert_xpath '//aside/header/following-sibling::p[text()="Sidebar content"]', output, 1
+ end
+
+ test 'should use custom converter if specified' do
+ input = <<-EOS
+= Document Title
+
+preamble
+
+== Section
+
+content
+ EOS
+
+ class CustomConverterA
+ def initialize backend, opts = {}
+ end
+
+ def convert node, name = nil
+ 'document'
+ end
+
+ def self.converts? backend
+ true
+ end
+ end
+
+ output = render_string input, :converter => CustomConverterA
+ assert 'document', output
+ end
+
+ test 'should use converter registered for backend' do
+ input = <<-EOS
+content
+ EOS
+
+ begin
+ Asciidoctor::Converter::Factory.unregister_all
+
+ class CustomConverterB
+ include Asciidoctor::Converter
+ register_for 'foobar'
+ def convert node, name = nil
+ 'foobar content'
+ end
+ end
+
+ converters = Asciidoctor::Converter::Factory.converters
+ assert converters.size == 1
+ assert converters['foobar'] == CustomConverterB
+ output = render_string input, :backend => 'foobar'
+ assert 'foobar content', output
+ ensure
+ Asciidoctor::Converter::Factory.unregister_all
+ end
+ end
+
+ test 'should fall back to catch all converter' do
+ input = <<-EOS
+content
+ EOS
+
+ begin
+ Asciidoctor::Converter::Factory.unregister_all
+
+ class CustomConverterC
+ include Asciidoctor::Converter
+ register_for '*'
+ def convert node, name = nil
+ 'foobaz content'
+ end
+ end
+
+ converters = Asciidoctor::Converter::Factory.converters
+ assert converters['*'] == CustomConverterC
+ output = render_string input, :backend => 'foobaz'
+ assert 'foobaz content', output
+ ensure
+ Asciidoctor::Converter::Factory.unregister_all
+ end
+ end
+ end
+end
diff --git a/test/document_test.rb b/test/document_test.rb
index a9fc6023..f0a01168 100644
--- a/test/document_test.rb
+++ b/test/document_test.rb
@@ -4,6 +4,8 @@ unless defined? ASCIIDOCTOR_PROJECT_DIR
require 'test_helper'
end
+BUILT_IN_ELEMENTS = %w(admonition audio colist dlist document embedded example floating_title image inline_anchor inline_break inline_button inline_callout inline_footnote inline_image inline_indexterm inline_kbd inline_menu inline_quoted listing literal math olist open page_break paragraph pass preamble quote section sidebar table thematic_break toc ulist verse video)
+
context 'Document' do
context 'Example document' do
@@ -223,6 +225,34 @@ preamble
assert doc.attributes.is_a?(Hash)
assert doc.attributes.has_key?('toc')
end
+
+ test 'should not modify options argument' do
+ options = {
+ :safe => Asciidoctor::SafeMode::SAFE
+ }
+ options.freeze
+ sample_input_path = fixture_path('sample.asciidoc')
+ begin
+ Asciidoctor.load_file sample_input_path, options
+ rescue
+ flunk %(options argument should not be modified)
+ end
+ end
+
+ test 'should not modify attributes Hash argument' do
+ attributes = {}
+ attributes.freeze
+ options = {
+ :safe => Asciidoctor::SafeMode::SAFE,
+ :attributes => attributes
+ }
+ sample_input_path = fixture_path('sample.asciidoc')
+ begin
+ Asciidoctor.load_file sample_input_path, options
+ rescue
+ flunk %(attributes argument should not be modified)
+ end
+ end
end
context 'Render APIs' do
@@ -472,6 +502,19 @@ text
FileUtils.rmdir output_dir
end
end
+
+ test 'should not modify options argument' do
+ options = {
+ :safe => Asciidoctor::SafeMode::SAFE
+ }
+ options.freeze
+ sample_input_path = fixture_path('sample.asciidoc')
+ begin
+ Asciidoctor.render_file sample_input_path, options
+ rescue
+ flunk %(options argument should not be modified)
+ end
+ end
end
context 'Docinfo files' do
@@ -642,63 +685,56 @@ text
end
end
- context 'Renderer' do
+ context 'Converter' 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 37, views.size
- assert views.has_key? 'document'
- assert Asciidoctor.const_defined?(:HTML5)
- assert Asciidoctor::HTML5.const_defined?(:DocumentTemplate)
+ converter = doc.converter
+ assert converter.is_a? Asciidoctor::Converter::Html5Converter
+ BUILT_IN_ELEMENTS.each do |element|
+ assert converter.respond_to? element
+ end
end
test 'built-in DocBook45 views are registered when backend is docbook45' do
doc = document_from_string '', :attributes => {'backend' => 'docbook45'}
- renderer = doc.renderer
+ converter = doc.converter
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 37, views.size
- assert views.has_key? 'document'
- assert Asciidoctor.const_defined?(:DocBook45)
- assert Asciidoctor::DocBook45.const_defined?(:DocumentTemplate)
+ converter = doc.converter
+ assert converter.is_a? Asciidoctor::Converter::DocBook45Converter
+ BUILT_IN_ELEMENTS.each do |element|
+ assert converter.respond_to? element
+ end
end
test 'built-in DocBook5 views are registered when backend is docbook5' do
doc = document_from_string '', :attributes => {'backend' => 'docbook5'}
- renderer = doc.renderer
+ converter = doc.converter
assert_equal 'docbook5', doc.attributes['backend']
assert doc.attributes.has_key? 'backend-docbook5'
assert_equal 'docbook', doc.attributes['basebackend']
assert doc.attributes.has_key? 'basebackend-docbook'
- assert !renderer.nil?
- views = renderer.views
- assert !views.nil?
- assert_equal 37, views.size
- assert views.has_key? 'document'
- assert Asciidoctor.const_defined?(:DocBook5)
- assert Asciidoctor::DocBook5.const_defined?(:DocumentTemplate)
+ converter = doc.converter
+ assert converter.is_a? Asciidoctor::Converter::DocBook5Converter
+ BUILT_IN_ELEMENTS.each do |element|
+ assert converter.respond_to? element
+ end
end
# NOTE The eruby tests are no longer relevant as we no longer use ERB internally
-# These should be rewritten to test the selection of ERB for use with the template renderer
+# These should be rewritten to test the selection of ERB for use with the template converter
=begin
test 'eRuby implementation should default to ERB' do
# intentionally use built-in templates for this test
doc = Asciidoctor::Document.new [], :backend => 'docbook', :header_footer => true
- renderer = doc.renderer
- views = renderer.views
+ converter = doc.converter
+ views = converter.views
assert !views.nil?
assert views.has_key? 'document'
assert views['document'].is_a?(Asciidoctor::DocBook45::DocumentTemplate)
@@ -710,9 +746,9 @@ text
# intentionally use built-in templates for this test
doc = Asciidoctor::Document.new [], :backend => 'docbook', :eruby => 'erubis', :header_footer => true
assert $LOADED_FEATURES.detect {|p| p == 'erubis.rb' || p.end_with?('/erubis.rb') }.nil?
- renderer = doc.renderer
+ converter = doc.converter
assert $LOADED_FEATURES.detect {|p| p == 'erubis.rb' || p.end_with?('/erubis.rb') }
- views = renderer.views
+ views = converter.views
assert !views.nil?
assert views.has_key? 'document'
assert views['document'].is_a?(Asciidoctor::DocBook45::DocumentTemplate)
diff --git a/test/invoker_test.rb b/test/invoker_test.rb
index 7a99fba1..daa8745e 100644
--- a/test/invoker_test.rb
+++ b/test/invoker_test.rb
@@ -198,7 +198,7 @@ context 'Invoker' do
test 'should copy default stylesheet to target directory if linkcss is specified' do
sample_outpath = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'sample-output.html'))
asciidoctor_stylesheet = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'asciidoctor.css'))
- coderay_stylesheet = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'asciidoctor-coderay.css'))
+ coderay_stylesheet = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'coderay-asciidoctor.css'))
begin
invoker = invoke_cli %W(-o #{sample_outpath} -a linkcss -a source-highlighter=coderay)
invoker.document
@@ -308,24 +308,6 @@ context 'Invoker' do
assert_xpath '/*[@id="preamble"]', output, 1
end
- # no longer relevant
- #test 'should not compact output by default' do
- # # NOTE we are relying on the fact that the template leaves blank lines
- # # this will always fail when using a template engine which strips blank lines by default
- # invoker = invoke_cli_to_buffer(%w(-o -), '-') { '* content' }
- # output = invoker.read_output
- # puts output
- # assert_match(/\n[ \t]*\n/, output)
- #end
-
- test 'should compact output if specified' do
- # NOTE we are relying on the fact that the template leaves blank lines
- # this will always succeed when using a template engine which strips blank lines by default
- invoker = invoke_cli_to_buffer(%w(-C -s -o -), '-') { '* content' }
- output = invoker.read_output
- assert_no_match(/\n[ \t]*\n/, output)
- end
-
test 'should output a trailing endline to stdout' do
invoker = nil
output = nil
@@ -376,7 +358,10 @@ context 'Invoker' do
custom_backend_root = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends'))
invoker = invoke_cli_to_buffer %W(-E haml -T #{custom_backend_root} -o -)
doc = invoker.document
- assert doc.renderer.views['block_paragraph'].is_a? Tilt::HamlTemplate
+ assert doc.converter.is_a? Asciidoctor::Converter::CompositeConverter
+ selected = doc.converter.find_converter 'paragraph'
+ assert selected.is_a? Asciidoctor::Converter::TemplateConverter
+ assert selected.templates['paragraph'].is_a? Tilt::HamlTemplate
end
test 'should load custom templates from multiple template directories' do
diff --git a/test/renderer_test.rb b/test/renderer_test.rb
deleted file mode 100644
index 6af3a572..00000000
--- a/test/renderer_test.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-# encoding: UTF-8
-unless defined? ASCIIDOCTOR_PROJECT_DIR
- $: << File.dirname(__FILE__); $:.uniq!
- require 'test_helper'
-end
-require 'tilt'
-
-context 'Renderer' do
-
- context 'View mapping' do
- test 'should extract view mapping from built-in template with one segment and backend' do
- view_name, view_backend = Asciidoctor::Renderer.extract_view_mapping('Asciidoctor::HTML5::DocumentTemplate')
- assert_equal 'document', view_name
- assert_equal 'html5', view_backend
- end
-
- test 'should extract view mapping from built-in template with two segments and backend' do
- view_name, view_backend = Asciidoctor::Renderer.extract_view_mapping('Asciidoctor::DocBook45::BlockSidebarTemplate')
- assert_equal 'block_sidebar', view_name
- assert_equal 'docbook45', view_backend
- end
-
- test 'should extract view mapping from built-in template without backend' do
- view_name, view_backend = Asciidoctor::Renderer.extract_view_mapping('Asciidoctor::DocumentTemplate')
- assert_equal 'document', view_name
- assert view_backend.nil?
- end
- end
-
- context 'View options' do
- test 'should set Haml format to html5 for html5 backend' do
- doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false
- assert doc.renderer.views['block_paragraph'].is_a? Tilt::HamlTemplate
- assert_equal :html5, doc.renderer.views['block_paragraph'].options[:format]
- end
-
- test 'should set Haml format to xhtml for docbook backend' do
- doc = Asciidoctor::Document.new [], :backend => 'docbook45', :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false
- assert doc.renderer.views['block_paragraph'].is_a? Tilt::HamlTemplate
- assert_equal :xhtml, doc.renderer.views['block_paragraph'].options[:format]
- end
- end
-
- context 'Custom backends' do
- test 'should load Haml templates for default backend' do
- doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false
- assert doc.renderer.views['block_paragraph'].is_a? Tilt::HamlTemplate
- assert doc.renderer.views['block_paragraph'].file.end_with? 'block_paragraph.html.haml'
- assert doc.renderer.views['block_sidebar'].is_a? Tilt::HamlTemplate
- assert doc.renderer.views['block_sidebar'].file.end_with? 'block_sidebar.html.haml'
- end
-
- test 'should load Haml templates for docbook45 backend' do
- doc = Asciidoctor::Document.new [], :backend => 'docbook45', :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false
- assert doc.renderer.views['block_paragraph'].is_a? Tilt::HamlTemplate
- assert doc.renderer.views['block_paragraph'].file.end_with? 'block_paragraph.xml.haml'
- end
-
- test 'should use Haml templates in place of built-in templates' do
- input = <<-EOS
-= Document Title
-Author Name
-
-== Section One
-
-Sample paragraph
-
-.Related
-****
-Sidebar content
-****
- EOS
-
- output = render_embedded_string input, :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'), :template_cache => false
- assert_xpath '/*[@class="sect1"]/*[@class="sectionbody"]/p', output, 1
- assert_xpath '//aside', output, 1
- assert_xpath '/*[@class="sect1"]/*[@class="sectionbody"]/p/following-sibling::aside', output, 1
- assert_xpath '//aside/header/h1[text()="Related"]', output, 1
- assert_xpath '//aside/header/following-sibling::p[text()="Sidebar content"]', output, 1
- end
-
- test 'should use built-in global cache to cache templates' do
- # clear out any cache, just to be sure
- Asciidoctor::Renderer.reset_global_cache
-
- template_dir = File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml')
- doc = Asciidoctor::Document.new [], :template_dir => template_dir
- doc.renderer
- template_cache = Asciidoctor::Renderer.global_cache
- assert template_cache.is_a? Asciidoctor::TemplateCache
- cache = template_cache.cache
- assert_not_nil cache
- assert cache.size > 0
-
- # ensure we don't scan a second time (using the view option hash to mark the cached view object)
- template_path = Asciidoctor::PathResolver.new.system_path(File.join(template_dir, 'html5', 'block_paragraph.html.haml'), nil)
- view = template_cache.fetch(:view, template_path)
- view.options[:foo] = 'bar'
- doc = Asciidoctor::Document.new [], :template_dir => template_dir
- doc.renderer
- template_cache = Asciidoctor::Renderer.global_cache
- view = template_cache.fetch(:view, template_path)
- assert_equal 'bar', view.options[:foo]
-
- # clean up
- Asciidoctor::Renderer.reset_global_cache
- end
-
- test 'should use custom cache to cache templates' do
- template_dir = File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml')
- template_path = Asciidoctor::PathResolver.new.system_path(File.join(template_dir, 'html5', 'block_paragraph.html.haml'), nil)
- doc = Asciidoctor::Document.new [], :template_dir => template_dir,
- :template_cache => Asciidoctor::TemplateCache.new
- template_cache = doc.renderer.cache
- assert_not_nil template_cache
- cache = template_cache.cache
- assert_not_nil cache
- assert cache.size > 0
- view = template_cache.fetch(:view, template_path)
- assert view.is_a? Tilt::HamlTemplate
- end
-
- test 'should be able to disable template cache' do
- doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'haml'),
- :template_cache => false
- assert_nil doc.renderer.cache
- end
-
- test 'should load Slim templates for default backend' do
- doc = Asciidoctor::Document.new [], :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'slim'), :template_cache => false
- assert doc.renderer.views['block_paragraph'].is_a? Slim::Template
- assert doc.renderer.views['block_paragraph'].file.end_with? 'block_paragraph.html.slim'
- assert doc.renderer.views['block_sidebar'].is_a? Slim::Template
- assert doc.renderer.views['block_sidebar'].file.end_with? 'block_sidebar.html.slim'
- end
-
- test 'should load Slim templates for docbook45 backend' do
- doc = Asciidoctor::Document.new [], :backend => 'docbook45', :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'slim'), :template_cache => false
- assert doc.renderer.views['block_paragraph'].is_a? Slim::Template
- assert doc.renderer.views['block_paragraph'].file.end_with? 'block_paragraph.xml.slim'
- end
-
- test 'should use Slim templates in place of built-in templates' do
- input = <<-EOS
-= Document Title
-Author Name
-
-== Section One
-
-Sample paragraph
-
-.Related
-****
-Sidebar content
-****
- EOS
-
- output = render_embedded_string input, :template_dir => File.join(File.dirname(__FILE__), 'fixtures', 'custom-backends', 'slim'), :template_cache => false
- assert_xpath '/*[@class="sect1"]/*[@class="sectionbody"]/p', output, 1
- assert_xpath '//aside', output, 1
- assert_xpath '/*[@class="sect1"]/*[@class="sectionbody"]/p/following-sibling::aside', output, 1
- assert_xpath '//aside/header/h1[text()="Related"]', output, 1
- assert_xpath '//aside/header/following-sibling::p[text()="Sidebar content"]', output, 1
- end
- end
-end
diff --git a/test/sections_test.rb b/test/sections_test.rb
index 0583df61..2975c20f 100644
--- a/test/sections_test.rb
+++ b/test/sections_test.rb
@@ -1849,6 +1849,27 @@ They couldn't believe their eyes when...
assert_xpath '//*[@id="header"]//*[@id="toc"]/ul/li[1]/a[@href="#_section_one"][text()="1. Section One"]', output, 1
end
+ test 'should set toc placement to preamble if toc attribute is set to preamble' do
+ input = <<-EOS
+= Article
+:toc: preamble
+
+Yada yada
+
+== Section One
+
+It was a dark and stormy night...
+
+== Section Two
+
+They couldn't believe their eyes when...
+ EOS
+
+ output = render_string input
+ assert_css '#preamble #toc', output, 1
+ assert_css '#preamble .sectionbody + #toc', output, 1
+ end
+
test 'should use document attributes toc-class, toc-title and toclevels to create toc' do
input = <<-EOS
= Article
diff --git a/test/substitutions_test.rb b/test/substitutions_test.rb
index 92a317d4..ef04f0e3 100644
--- a/test/substitutions_test.rb
+++ b/test/substitutions_test.rb
@@ -898,37 +898,37 @@ EOS
test 'kbd macro with key combination' do
para = block_from_string('kbd:[Ctrl+Shift+T]', :attributes => {'experimental' => ''})
- assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></kbd>}, para.sub_macros(para.source)
+ assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></span>}, para.sub_macros(para.source)
end
test 'kbd macro with key combination with spaces' do
para = block_from_string('kbd:[Ctrl + Shift + T]', :attributes => {'experimental' => ''})
- assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></kbd>}, para.sub_macros(para.source)
+ assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></span>}, para.sub_macros(para.source)
end
test 'kbd macro with key combination delimited by commas' do
para = block_from_string('kbd:[Ctrl,Shift,T]', :attributes => {'experimental' => ''})
- assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></kbd>}, para.sub_macros(para.source)
+ assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></span>}, para.sub_macros(para.source)
end
test 'kbd macro with key combination containing a plus key no spaces' do
para = block_from_string('kbd:[Ctrl++]', :attributes => {'experimental' => ''})
- assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>+</kbd></kbd>}, para.sub_macros(para.source)
+ assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>+</kbd></span>}, para.sub_macros(para.source)
end
test 'kbd macro with key combination delimited by commands containing a comma key' do
para = block_from_string('kbd:[Ctrl,,]', :attributes => {'experimental' => ''})
- assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>,</kbd></kbd>}, para.sub_macros(para.source)
+ assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>,</kbd></span>}, para.sub_macros(para.source)
end
test 'kbd macro with key combination containing a plus key with spaces' do
para = block_from_string('kbd:[Ctrl + +]', :attributes => {'experimental' => ''})
- assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>+</kbd></kbd>}, para.sub_macros(para.source)
+ assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>+</kbd></span>}, para.sub_macros(para.source)
end
test 'kbd macro with key combination containing escaped bracket' do
para = block_from_string('kbd:[Ctrl + \]]', :attributes => {'experimental' => ''})
- assert_equal %q{<kbd class="keyseq"><kbd>Ctrl</kbd>+<kbd>]</kbd></kbd>}, para.sub_macros(para.source)
+ assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>]</kbd></span>}, para.sub_macros(para.source)
end
test 'kbd macro with key combination, docbook backend' do
diff --git a/test/tables_test.rb b/test/tables_test.rb
index 14962dd3..192b9788 100644
--- a/test/tables_test.rb
+++ b/test/tables_test.rb
@@ -636,7 +636,7 @@ output file name is used.
assert !body_cell_1_3.inner_document.nil?
assert body_cell_1_3.inner_document.nested?
assert_equal doc, body_cell_1_3.inner_document.parent_document
- assert_equal doc.renderer, body_cell_1_3.inner_document.renderer
+ assert_equal doc.converter, body_cell_1_3.inner_document.converter
output = doc.render
assert_css 'table > tbody > tr', output, 2