diff options
| author | Dan Allen <dan.j.allen@gmail.com> | 2014-02-20 04:29:02 -0700 |
|---|---|---|
| committer | Dan Allen <dan.j.allen@gmail.com> | 2014-02-26 14:04:41 -0700 |
| commit | d98c8e9cd5e5793fdab6da7a0a6f37eb8349878b (patch) | |
| tree | 4a5993b8e23f787017e1abcac51dce3ded60d4d9 | |
| parent | 0c4a21b4ade1df7767986c9bce84a7c3fdbee3c9 (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
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 = ' ' # or 
 - 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 = /(?:(?:\/\/|#|;;) ?)?(\\)?<!?(--|)(\d+)\2>(?=(?: ?\\?<!?\2\d+\2>)*#{CC_EOL})/ + # NOTE special characters are already be replaced at this point during conversion to an SGML format + CalloutConvertRx = /(?:(?:\/\/|#|;;) ?)?(\\)?<!?(--|)(\d+)\2>(?=(?: ?\\?<!?\2\d+\2>)*#{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') ? '✓ ' : '❏ ' - 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 => ['“', '”'], - :single => ['‘', '’'] - } - - 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}— #{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}— #{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 = '✓ ' - marker_unchecked = '❏ ' - 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 = '&' 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') ? "&start=#{node.attr 'start'}" : nil - end_param = (node.attr? 'end') ? "&end=#{node.attr 'end'}" : nil - autoplay_param = (node.option? 'autoplay') ? '&autoplay=1' : nil - loop_param = (node.option? 'loop') ? '&loop=1' : nil - controls_param = (node.option? 'nocontrols') ? '&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 => ['“', '”', false], - :single => ['‘', '’', 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> ▸ ) }.join.chop - %(<span class="menuseq"><span class="menu">#{menu}</span> ▸ #{submenu_path} <span class="menuitem">#{menuitem}</span></span>) - elsif !menuitem.nil? - %(<span class="menuseq"><span class="menu">#{menu}</span> ▸ <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') ? '✓ ' : '❏ ' + 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 => ['“', '”'], + :single => ['‘', '’'] + } + 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 => ['“', '”', false], + :single => ['‘', '’', 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}— #{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 = '✓ ' + marker_unchecked = '❏ ' + 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}— #{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 = '&' 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') ? "&start=#{node.attr 'start'}" : nil + end_param = (node.attr? 'end') ? "&end=#{node.attr 'end'}" : nil + autoplay_param = (node.option? 'autoplay') ? '&autoplay=1' : nil + loop_param = (node.option? 'loop') ? '&loop=1' : nil + controls_param = (node.option? 'nocontrols') ? '&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> ▸ ) }.join.chop + %(<span class="menuseq"><span class="menu">#{menu}</span> ▸ #{submenu_path} <span class="menuitem">#{node.attr 'menuitem'}</span></span>) + elsif (menuitem = node.attr 'menuitem') + %(<span class="menuseq"><span class="menu">#{menu}</span> ▸ <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', '<', '<' @@ -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('>').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 |
