diff options
| -rw-r--r-- | README.adoc | 5 | ||||
| -rw-r--r-- | compat/asciidoc.conf | 22 | ||||
| -rwxr-xr-x | lib/asciidoctor.rb | 17 | ||||
| -rw-r--r-- | lib/asciidoctor/lexer.rb | 60 | ||||
| -rw-r--r-- | test/blocks_test.rb | 110 |
5 files changed, 199 insertions, 15 deletions
diff --git a/README.adoc b/README.adoc index c3404aed..409bcd3c 100644 --- a/README.adoc +++ b/README.adoc @@ -321,7 +321,7 @@ NOTE: In general, Asciidoctor handles whitespace much more intelligently * Use can set the extension for icons using the +icontype+ attribute (AsciiDoc defaults to .png) -* AsciiDoc uses the +<blockquote>+ for the content and +<cite>+ tag for +* Asciidoctor uses the +<blockquote>+ for the content and +<cite>+ tag for attribution title in the HTML output for quote blocks, requiring some additional styling to match AsciiDoc + @@ -329,6 +329,9 @@ NOTE: In general, Asciidoctor handles whitespace much more intelligently cite { color: navy; } + +* Asciidoctor supports markdown-style blockquotes as well as a shorthand + for a blockquote paragraph. + * Asciidoctor does not support the deprecated index term syntax (`++` and `+++`) diff --git a/compat/asciidoc.conf b/compat/asciidoc.conf index 68b524c5..bb5fc416 100644 --- a/compat/asciidoc.conf +++ b/compat/asciidoc.conf @@ -21,11 +21,31 @@ space=" " tilde=~ # enables fenced code blocks -# I haven't sorted out yet how to do syntax highlighting +# FIXME I haven't sorted out yet how to do syntax highlighting [blockdef-fenced-code] delimiter=^```\w*$ template::[blockdef-listing] +# enables blockquotes to be defined using two double quotes +[blockdef-air-quote] +delimiter=^""$ +template::[blockdef-quote] + +# markdown-style blockquote (paragraph only) +# FIXME does not strip leading > on subsequent lines +[paradef-markdown-quote] +delimiter=(?s)>\s*(?P<text>\S.*) +style=quote +quote-style=template="quoteparagraph",posattrs=("style","attribution","citetitle") + +# fix regex for callout list to require number; also makes markdown-style blockquote work +[listdef-callout] +posattrs=style +delimiter=^<?(?P<index>\d+>) +(?P<text>.+)$ +type=callout +tags=callout +style=arabic + # enables literal block to be used as code block [blockdef-literal] template::[source-filter-style] diff --git a/lib/asciidoctor.rb b/lib/asciidoctor.rb index ced5391d..c305dad8 100755 --- a/lib/asciidoctor.rb +++ b/lib/asciidoctor.rb @@ -152,6 +152,7 @@ module Asciidoctor '====' => [:example, ['admonition'].to_set], '****' => [:sidebar, Set.new], '____' => [:quote, ['verse'].to_set], + '""' => [:quote, ['verse'].to_set], '++++' => [:pass, Set.new], '|===' => [:table, Set.new], ',===' => [:table, Set.new], @@ -208,7 +209,12 @@ module Asciidoctor # a block to be different lengths # this option requires that they be the same # Compliance value: false - :congruent_block_delimiters => true + :congruent_block_delimiters => true, + + # AsciiDoc will recognize commonly-used Markdown syntax + # to the degree it does not interfere with existing + # AsciiDoc behavior. + :markdown_syntax => true } # The following pattern, which appears frequently, captures the contents between square brackets, @@ -231,10 +237,11 @@ module Asciidoctor # [[ref]] (anywhere inline) :anchor_macro => /\\?\[\[([\w":].*?)\]\]/, - # matches any block delimiter: - # open, listing, example, literal, comment, quote, sidebar, passthrough, table - # NOTE position the most common blocks towards the front of the pattern - :any_blk => %r{^(?:--|(?:-|\.|=|\*|_|\+|/){4,}|[\|,;!]={3,}|(?:`|~){3,}.*)$}, + # matches any unbounded block delimiter: + # listing, literal, example, sidebar, quote, passthrough, table, fenced code + # does not include open block or air quotes + # TIP position the most common blocks towards the front of the pattern + :any_blk => %r{^(?:(?:-|\.|=|\*|_|\+|/){4,}|[\|,;!]={3,}|(?:`|~){3,}.*)$}, # detect a list item of any sort # [[:graph:]] is a non-blank character diff --git a/lib/asciidoctor/lexer.rb b/lib/asciidoctor/lexer.rb index bb892dec..efa9bb55 100644 --- a/lib/asciidoctor/lexer.rb +++ b/lib/asciidoctor/lexer.rb @@ -292,10 +292,11 @@ class Lexer # bail if we've reached the end of the parent block or document return nil unless reader.has_more_lines? + text_only = options[:text] # check for option to find list item text only # if skipped a line, assume a list continuation was # used and block content is acceptable - if options[:text] && skipped > 0 + if text_only && skipped > 0 options.delete(:text) end @@ -363,7 +364,7 @@ class Lexer end # process lines normally - if !options[:text] + if !text_only # NOTE we're letting break lines (ruler, page_break, etc) have attributes if (match = this_line.match(REGEXP[:break_line])) block = Block.new(parent, BREAK_LINES[match[0][0..2]]) @@ -565,12 +566,49 @@ class Lexer catalog_inline_anchors(buffer.join, document) - if !options[:text] && (admonition_match = buffer.first.match(REGEXP[:admonition_inline])) + first_line = buffer.first + if !text_only && (admonition_match = first_line.match(REGEXP[:admonition_inline])) buffer[0] = admonition_match.post_match.lstrip block = Block.new(parent, :admonition, buffer) attributes['style'] = admonition_match[1] attributes['name'] = admonition_name = admonition_match[1].downcase attributes['caption'] ||= document.attributes["#{admonition_name}-caption"] + elsif !text_only && COMPLIANCE[:markdown_syntax] && first_line.start_with?('> ') + buffer.map! {|line| + if line.start_with?('> ') + line[2..-1] + elsif line.chomp == '>' + line[1..-1] + else + line + end + } + + if buffer.last.start_with?('-- ') + attribution, citetitle = buffer.pop[3..-1].split(', ') + buffer.pop while buffer.last.chomp.empty? + buffer[-1] = buffer.last.chomp + else + attribution, citetitle = nil + end + attributes['style'] = 'quote' + attributes['attribution'] = attribution unless attribution.nil? + attributes['citetitle'] = citetitle unless citetitle.nil? + # NOTE will only detect headings that are floating titles (not section titles) + # TODO could assume a floating title when inside a block context + block = build_block(:quote, :complex, false, parent, Reader.new(buffer), attributes) + elsif !text_only && buffer.size > 1 && first_line.start_with?('"') && + buffer.last.start_with?('-- ') && buffer[-2].chomp.end_with?('"') + buffer[0] = first_line[1..-1] + attribution, citetitle = buffer.pop[3..-1].split(', ') + buffer.pop while buffer.last.chomp.empty? + buffer[-1] = buffer.last.chomp.chop + attributes['style'] = 'quote' + attributes['attribution'] = attribution unless attribution.nil? + attributes['citetitle'] = citetitle unless citetitle.nil? + block = Block.new(parent, :quote, buffer) + #block = Block.new(parent, :quote) + #block << Block.new(block, :paragraph, buffer) else # QUESTION is this necessary? #if style == 'normal' && [' ', "\t"].include?(buffer.first[0..0]) @@ -685,11 +723,13 @@ class Lexer tip = line[0..3] tl = 4 - # special case for fenced code blocks - tip_alt = tip.chop - if tip_alt == '```' || tip_alt == '~~~' - tip = tip_alt - tl = 3 + if COMPLIANCE[:markdown_syntax] + # special case for fenced code blocks + tip_alt = tip.chop + if tip_alt == '```' || tip_alt == '~~~' + tip = tip_alt + tl = 3 + end end end @@ -723,6 +763,7 @@ class Lexer end # whether a block supports complex content should be a config setting + # if terminator is false, that means the all the lines in the reader should be parsed # NOTE could invoke filter in here, before and after parsing def self.build_block(block_context, content_type, terminator, parent, reader, attributes, options = {}) if terminator.nil? @@ -740,6 +781,9 @@ class Lexer end elsif content_type != :complex buffer = reader.grab_lines_until(:terminator => terminator, :chomp_last_line => true) + elsif terminator == false + buffer = nil + block_reader = reader else buffer = nil block_reader = Reader.new reader.grab_lines_until(:terminator => terminator) diff --git a/test/blocks_test.rb b/test/blocks_test.rb index c948b01e..a98a9fe1 100644 --- a/test/blocks_test.rb +++ b/test/blocks_test.rb @@ -181,6 +181,116 @@ ____ assert_css '.quoteblock > blockquote > .paragraph + .admonitionblock', output, 1 end + test 'quote block using air quotes with no attribution' do + input = <<-EOS +"" +A famous quote. +"" + EOS + output = render_string input + assert_css '.quoteblock', output, 1 + assert_css '.quoteblock > blockquote', output, 1 + assert_css '.quoteblock > blockquote > .paragraph > p', output, 1 + assert_css '.quoteblock > .attribution', output, 0 + assert_xpath '//*[@class = "quoteblock"]//p[text() = "A famous quote."]', output, 1 + end + + test 'markdown-style quote block with single paragraph and no attribution' do + input = <<-EOS +> A famous quote. +> Some more inspiring words. + EOS + output = render_string input + assert_css '.quoteblock', output, 1 + assert_css '.quoteblock > blockquote', output, 1 + assert_css '.quoteblock > blockquote > .paragraph > p', output, 1 + assert_css '.quoteblock > .attribution', output, 0 + assert_xpath %(//*[@class = "quoteblock"]//p[text() = "A famous quote.\nSome more inspiring words."]), output, 1 + end + + test 'lazy markdown-style quote block with single paragraph and no attribution' do + input = <<-EOS +> A famous quote. +Some more inspiring words. + EOS + output = render_string input + assert_css '.quoteblock', output, 1 + assert_css '.quoteblock > blockquote', output, 1 + assert_css '.quoteblock > blockquote > .paragraph > p', output, 1 + assert_css '.quoteblock > .attribution', output, 0 + assert_xpath %(//*[@class = "quoteblock"]//p[text() = "A famous quote.\nSome more inspiring words."]), output, 1 + end + + test 'markdown-style quote block with multiple paragraphs and no attribution' do + input = <<-EOS +> A famous quote. +> +> Some more inspiring words. + EOS + output = render_string input + assert_css '.quoteblock', output, 1 + assert_css '.quoteblock > blockquote', output, 1 + assert_css '.quoteblock > blockquote > .paragraph > p', output, 2 + assert_css '.quoteblock > .attribution', output, 0 + assert_xpath %((//*[@class = "quoteblock"]//p)[1][text() = "A famous quote."]), output, 1 + assert_xpath %((//*[@class = "quoteblock"]//p)[2][text() = "Some more inspiring words."]), output, 1 + end + + test 'markdown-style quote block with multiple blocks and no attribution' do + input = <<-EOS +> A famous quote. +> +> NOTE: Some more inspiring words. + EOS + output = render_string input + assert_css '.quoteblock', output, 1 + assert_css '.quoteblock > blockquote', output, 1 + assert_css '.quoteblock > blockquote > .paragraph > p', output, 1 + assert_css '.quoteblock > blockquote > .admonitionblock', output, 1 + assert_css '.quoteblock > .attribution', output, 0 + assert_xpath %((//*[@class = "quoteblock"]//p)[1][text() = "A famous quote."]), output, 1 + assert_xpath %((//*[@class = "quoteblock"]//*[@class = "admonitionblock note"]//*[@class="content"])[1][normalize-space(text()) = "Some more inspiring words."]), output, 1 + end + + test 'markdown-style quote block with single paragraph and attribution' do + input = <<-EOS +> A famous quote. +> Some more inspiring words. +> -- Famous Person, Famous Source (1999) + EOS + output = render_string input + assert_css '.quoteblock', output, 1 + assert_css '.quoteblock > blockquote', output, 1 + assert_css '.quoteblock > blockquote > .paragraph > p', output, 1 + assert_xpath %(//*[@class = "quoteblock"]//p[text() = "A famous quote.\nSome more inspiring words."]), output, 1 + assert_css '.quoteblock > .attribution', output, 1 + assert_css '.quoteblock > .attribution > cite', output, 1 + assert_css '.quoteblock > .attribution > cite + br', output, 1 + assert_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]/cite[text() = "Famous Source (1999)"]', output, 1 + attribution = xmlnodes_at_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]', output, 1 + author = attribution.children.last + assert_equal "#{expand_entity 8212} Famous Person", author.text.strip + end + + test 'quoted paragraph-style quote block with attribution' do + input = <<-EOS +"A famous quote. +Some more inspiring words." +-- Famous Person, Famous Source (1999) + EOS + output = render_string input + assert_css '.quoteblock', output, 1 + assert_css '.quoteblock > blockquote', output, 1 + assert_xpath %(//*[@class = "quoteblock"]/blockquote[normalize-space(text()) = "A famous quote. Some more inspiring words."]), output, 1 + assert_css '.quoteblock > .attribution', output, 1 + assert_css '.quoteblock > .attribution > cite', output, 1 + assert_css '.quoteblock > .attribution > cite + br', output, 1 + assert_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]/cite[text() = "Famous Source (1999)"]', output, 1 + attribution = xmlnodes_at_xpath '//*[@class = "quoteblock"]/*[@class = "attribution"]', output, 1 + author = attribution.children.last + assert_equal "#{expand_entity 8212} Famous Person", author.text.strip + end + test 'single-line verse block without attribution' do input = <<-EOS [verse] |
