"Fossies" - the Fresh Open Source Software Archive

Member "asciidoctor-2.0.10/test/substitutions_test.rb" (1 Jun 2019, 115012 Bytes) of package /linux/www/asciidoctor-2.0.10.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Ruby source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "substitutions_test.rb": 2.0.8_vs_2.0.9.

    1 # frozen_string_literal: true
    2 require_relative 'test_helper'
    3 
    4 # TODO
    5 # - test negatives
    6 # - test role on every quote type
    7 context 'Substitutions' do
    8   BACKSLASH = ?\\
    9   context 'Dispatcher' do
   10     test 'apply normal substitutions' do
   11       para = block_from_string("[blue]_http://asciidoc.org[AsciiDoc]_ & [red]*Ruby*\n&#167; Making +++<u>documentation</u>+++ together +\nsince (C) {inception_year}.")
   12       para.document.attributes['inception_year'] = '2012'
   13       result = para.apply_subs(para.source)
   14       assert_equal %{<em class="blue"><a href="http://asciidoc.org">AsciiDoc</a></em> &amp; <strong class="red">Ruby</strong>\n&#167; Making <u>documentation</u> together<br>\nsince &#169; 2012.}, result
   15     end
   16 
   17     test 'apply_subs should not modify string directly' do
   18       input = '<html> -- the root of all web'
   19       para = block_from_string input
   20       para_source = para.source
   21       result = para.apply_subs para_source
   22       assert_equal '&lt;html&gt;&#8201;&#8212;&#8201;the root of all web', result
   23       assert_equal input, para_source
   24     end
   25 
   26     test 'should not drop trailing blank lines when performing substitutions' do
   27       para = block_from_string %([%hardbreaks]\nthis\nis\n-> {program})
   28       para.lines << ''
   29       para.lines << ''
   30       para.document.attributes['program'] = 'Asciidoctor'
   31       result = para.apply_subs(para.lines)
   32       assert_equal ['this<br>', 'is<br>', '&#8594; Asciidoctor<br>', '<br>', ''], result
   33       result = para.apply_subs(para.lines * "\n")
   34       assert_equal %(this<br>\nis<br>\n&#8594; Asciidoctor<br>\n<br>\n), result
   35     end
   36 
   37     test 'should expand subs passed to expand_subs' do
   38       para = block_from_string %({program}\n*bold*\n2 > 1)
   39       para.document.attributes['program'] = 'Asciidoctor'
   40       assert_equal [:specialcharacters], (para.expand_subs [:specialchars])
   41       refute para.expand_subs([:none])
   42       assert_equal [:specialcharacters, :quotes, :attributes, :replacements, :macros, :post_replacements], (para.expand_subs [:normal])
   43     end
   44 
   45     test 'apply_subs should allow the subs argument to be nil' do
   46       block = block_from_string %([pass]\n*raw*)
   47       result = block.apply_subs block.source, nil
   48       assert_equal '*raw*', result
   49     end
   50   end
   51 
   52   context 'Quotes' do
   53     test 'single-line double-quoted string' do
   54       para = block_from_string(%q{``a few quoted words''}, attributes: { 'compat-mode' => '' })
   55       assert_equal '&#8220;a few quoted words&#8221;', para.sub_quotes(para.source)
   56 
   57       para = block_from_string(%q{"`a few quoted words`"})
   58       assert_equal '&#8220;a few quoted words&#8221;', para.sub_quotes(para.source)
   59 
   60       para = block_from_string(%q{"`a few quoted words`"}, backend: 'docbook')
   61       assert_equal '<quote>a few quoted words</quote>', para.sub_quotes(para.source)
   62     end
   63 
   64     test 'escaped single-line double-quoted string' do
   65       para = block_from_string %(#{BACKSLASH}``a few quoted words''), attributes: { 'compat-mode' => '' }
   66       assert_equal %q(&#8216;`a few quoted words&#8217;'), para.sub_quotes(para.source)
   67 
   68       para = block_from_string %(#{BACKSLASH * 2}``a few quoted words''), attributes: { 'compat-mode' => '' }
   69       assert_equal %q(``a few quoted words''), para.sub_quotes(para.source)
   70 
   71       para = block_from_string(%(#{BACKSLASH}"`a few quoted words`"))
   72       assert_equal %q("`a few quoted words`"), para.sub_quotes(para.source)
   73 
   74       para = block_from_string(%(#{BACKSLASH * 2}"`a few quoted words`"))
   75       assert_equal %(#{BACKSLASH}"`a few quoted words`"), para.sub_quotes(para.source)
   76     end
   77 
   78     test 'multi-line double-quoted string' do
   79       para = block_from_string(%Q{``a few\nquoted words''}, attributes: { 'compat-mode' => '' })
   80       assert_equal "&#8220;a few\nquoted words&#8221;", para.sub_quotes(para.source)
   81 
   82       para = block_from_string(%Q{"`a few\nquoted words`"})
   83       assert_equal "&#8220;a few\nquoted words&#8221;", para.sub_quotes(para.source)
   84     end
   85 
   86     test 'double-quoted string with inline single quote' do
   87       para = block_from_string(%q{``Here's Johnny!''}, attributes: { 'compat-mode' => '' })
   88       assert_equal %q{&#8220;Here's Johnny!&#8221;}, para.sub_quotes(para.source)
   89 
   90       para = block_from_string(%q{"`Here's Johnny!`"})
   91       assert_equal %q{&#8220;Here's Johnny!&#8221;}, para.sub_quotes(para.source)
   92     end
   93 
   94     test 'double-quoted string with inline backquote' do
   95       para = block_from_string(%q{``Here`s Johnny!''}, attributes: { 'compat-mode' => '' })
   96       assert_equal %q{&#8220;Here`s Johnny!&#8221;}, para.sub_quotes(para.source)
   97 
   98       para = block_from_string(%q{"`Here`s Johnny!`"})
   99       assert_equal %q{&#8220;Here`s Johnny!&#8221;}, para.sub_quotes(para.source)
  100     end
  101 
  102     test 'double-quoted string around monospaced text' do
  103       para = block_from_string(%q("``E=mc^2^` is the solution!`"))
  104       assert_equal %q(&#8220;`E=mc<sup>2</sup>` is the solution!&#8221;), para.apply_subs(para.source);
  105 
  106       para = block_from_string(%q("```E=mc^2^`` is the solution!`"))
  107       assert_equal %q(&#8220;<code>E=mc<sup>2</sup></code> is the solution!&#8221;), para.apply_subs(para.source);
  108     end
  109 
  110     test 'single-line single-quoted string' do
  111       para = block_from_string(%q{`a few quoted words'}, attributes: { 'compat-mode' => '' })
  112       assert_equal '&#8216;a few quoted words&#8217;', para.sub_quotes(para.source)
  113 
  114       para = block_from_string(%q{'`a few quoted words`'})
  115       assert_equal '&#8216;a few quoted words&#8217;', para.sub_quotes(para.source)
  116 
  117       para = block_from_string(%q{'`a few quoted words`'}, backend: 'docbook')
  118       assert_equal '<quote>a few quoted words</quote>', para.sub_quotes(para.source)
  119     end
  120 
  121     test 'escaped single-line single-quoted string' do
  122       para = block_from_string(%(#{BACKSLASH}`a few quoted words'), attributes: { 'compat-mode' => '' })
  123       assert_equal %(`a few quoted words'), para.sub_quotes(para.source)
  124 
  125       para = block_from_string(%(#{BACKSLASH}'`a few quoted words`'))
  126       assert_equal %('`a few quoted words`'), para.sub_quotes(para.source)
  127     end
  128 
  129     test 'multi-line single-quoted string' do
  130       para = block_from_string(%Q{`a few\nquoted words'}, attributes: { 'compat-mode' => '' })
  131       assert_equal "&#8216;a few\nquoted words&#8217;", para.sub_quotes(para.source)
  132 
  133       para = block_from_string(%Q{'`a few\nquoted words`'})
  134       assert_equal "&#8216;a few\nquoted words&#8217;", para.sub_quotes(para.source)
  135     end
  136 
  137     test 'single-quoted string with inline single quote' do
  138       para = block_from_string(%q{`That isn't what I did.'}, attributes: { 'compat-mode' => '' })
  139       assert_equal %q{&#8216;That isn't what I did.&#8217;}, para.sub_quotes(para.source)
  140 
  141       para = block_from_string(%q{'`That isn't what I did.`'})
  142       assert_equal %q{&#8216;That isn't what I did.&#8217;}, para.sub_quotes(para.source)
  143     end
  144 
  145     test 'single-quoted string with inline backquote' do
  146       para = block_from_string(%q{`Here`s Johnny!'}, attributes: { 'compat-mode' => '' })
  147       assert_equal %q{&#8216;Here`s Johnny!&#8217;}, para.sub_quotes(para.source)
  148 
  149       para = block_from_string(%q{'`Here`s Johnny!`'})
  150       assert_equal %q{&#8216;Here`s Johnny!&#8217;}, para.sub_quotes(para.source)
  151     end
  152 
  153     test 'single-line constrained marked string' do
  154       #para = block_from_string(%q{#a few words#}, attributes: { 'compat-mode' => '' })
  155       #assert_equal 'a few words', para.sub_quotes(para.source)
  156 
  157       para = block_from_string(%q{#a few words#})
  158       assert_equal '<mark>a few words</mark>', para.sub_quotes(para.source)
  159     end
  160 
  161     test 'escaped single-line constrained marked string' do
  162       para = block_from_string(%(#{BACKSLASH}#a few words#))
  163       assert_equal '#a few words#', para.sub_quotes(para.source)
  164     end
  165 
  166     test 'multi-line constrained marked string' do
  167       #para = block_from_string(%Q{#a few\nwords#}, attributes: { 'compat-mode' => '' })
  168       #assert_equal "a few\nwords", para.sub_quotes(para.source)
  169 
  170       para = block_from_string(%Q{#a few\nwords#})
  171       assert_equal "<mark>a few\nwords</mark>", para.sub_quotes(para.source)
  172     end
  173 
  174     test 'constrained marked string should not match entity references' do
  175       para = block_from_string('111 #mark a# 222 "`quote a`" 333 #mark b# 444')
  176       assert_equal %(111 <mark>mark a</mark> 222 &#8220;quote a&#8221; 333 <mark>mark b</mark> 444), para.sub_quotes(para.source)
  177     end
  178 
  179     test 'single-line unconstrained marked string' do
  180       #para = block_from_string(%q{##--anything goes ##}, attributes: { 'compat-mode' => '' })
  181       #assert_equal '--anything goes ', para.sub_quotes(para.source)
  182 
  183       para = block_from_string(%q{##--anything goes ##})
  184       assert_equal '<mark>--anything goes </mark>', para.sub_quotes(para.source)
  185     end
  186 
  187     test 'escaped single-line unconstrained marked string' do
  188       para = block_from_string(%(#{BACKSLASH}#{BACKSLASH}##--anything goes ##))
  189       assert_equal '##--anything goes ##', para.sub_quotes(para.source)
  190     end
  191 
  192     test 'multi-line unconstrained marked string' do
  193       #para = block_from_string(%Q{##--anything\ngoes ##}, attributes: { 'compat-mode' => '' })
  194       #assert_equal "--anything\ngoes ", para.sub_quotes(para.source)
  195 
  196       para = block_from_string(%Q{##--anything\ngoes ##})
  197       assert_equal "<mark>--anything\ngoes </mark>", para.sub_quotes(para.source)
  198     end
  199 
  200     test 'single-line constrained marked string with role' do
  201       para = block_from_string(%q{[statement]#a few words#})
  202       assert_equal '<span class="statement">a few words</span>', para.sub_quotes(para.source)
  203     end
  204 
  205     test 'single-line constrained strong string' do
  206       para = block_from_string(%q{*a few strong words*})
  207       assert_equal '<strong>a few strong words</strong>', para.sub_quotes(para.source)
  208     end
  209 
  210     test 'escaped single-line constrained strong string' do
  211       para = block_from_string(%(#{BACKSLASH}*a few strong words*))
  212       assert_equal '*a few strong words*', para.sub_quotes(para.source)
  213     end
  214 
  215     test 'multi-line constrained strong string' do
  216       para = block_from_string(%Q{*a few\nstrong words*})
  217       assert_equal "<strong>a few\nstrong words</strong>", para.sub_quotes(para.source)
  218     end
  219 
  220     test 'constrained strong string containing an asterisk' do
  221       para = block_from_string(%q{*bl*ck*-eye})
  222       assert_equal '<strong>bl*ck</strong>-eye', para.sub_quotes(para.source)
  223     end
  224 
  225     test 'constrained strong string containing an asterisk and multibyte word chars' do
  226       para = block_from_string(%q{*黑*眼圈*})
  227       assert_equal '<strong>黑*眼圈</strong>', para.sub_quotes(para.source)
  228     end
  229 
  230     test 'single-line constrained quote variation emphasized string' do
  231       para = block_from_string(%q{_a few emphasized words_})
  232       assert_equal '<em>a few emphasized words</em>', para.sub_quotes(para.source)
  233     end
  234 
  235     test 'escaped single-line constrained quote variation emphasized string' do
  236       para = block_from_string(%(#{BACKSLASH}_a few emphasized words_))
  237       assert_equal %q(_a few emphasized words_), para.sub_quotes(para.source)
  238     end
  239 
  240     test 'escaped single quoted string' do
  241       para = block_from_string(%(#{BACKSLASH}'a few emphasized words'))
  242       # NOTE the \' is replaced with ' by the :replacements substitution, later in the substitution pipeline
  243       assert_equal %(#{BACKSLASH}'a few emphasized words'), para.sub_quotes(para.source)
  244     end
  245 
  246     test 'multi-line constrained emphasized quote variation string' do
  247       para = block_from_string(%Q{_a few\nemphasized words_})
  248       assert_equal "<em>a few\nemphasized words</em>", para.sub_quotes(para.source)
  249     end
  250 
  251     test 'single-quoted string containing an emphasized phrase' do
  252       para = block_from_string(%q{`I told him, 'Just go for it!''}, attributes: { 'compat-mode' => '' })
  253       assert_equal '&#8216;I told him, <em>Just go for it!</em>&#8217;', para.sub_quotes(para.source)
  254 
  255       para = block_from_string(%q{'`I told him, 'Just go for it!'`'})
  256       assert_equal %q(&#8216;I told him, 'Just go for it!'&#8217;), para.sub_quotes(para.source)
  257     end
  258 
  259     test 'escaped single-quotes inside emphasized words are restored' do
  260       para = block_from_string(%('Here#{BACKSLASH}'s Johnny!'), attributes: { 'compat-mode' => '' })
  261       assert_equal %q(<em>Here's Johnny!</em>), para.apply_subs(para.source)
  262 
  263       para = block_from_string(%('Here#{BACKSLASH}'s Johnny!'))
  264       assert_equal %q('Here's Johnny!'), para.apply_subs(para.source)
  265     end
  266 
  267     test 'single-line constrained emphasized underline variation string' do
  268       para = block_from_string(%q{_a few emphasized words_})
  269       assert_equal '<em>a few emphasized words</em>', para.sub_quotes(para.source)
  270     end
  271 
  272     test 'escaped single-line constrained emphasized underline variation string' do
  273       para = block_from_string(%(#{BACKSLASH}_a few emphasized words_))
  274       assert_equal '_a few emphasized words_', para.sub_quotes(para.source)
  275     end
  276 
  277     test 'multi-line constrained emphasized underline variation string' do
  278       para = block_from_string(%Q{_a few\nemphasized words_})
  279       assert_equal "<em>a few\nemphasized words</em>", para.sub_quotes(para.source)
  280     end
  281 
  282     # NOTE must use apply_subs because constrained monospaced is handled as a passthrough
  283     test 'single-line constrained monospaced string' do
  284       para = block_from_string(%(`a few <{monospaced}> words`), attributes: { 'monospaced' => 'monospaced', 'compat-mode' => '' })
  285       assert_equal '<code>a few &lt;{monospaced}&gt; words</code>', para.apply_subs(para.source)
  286 
  287       para = block_from_string(%(`a few <{monospaced}> words`), attributes: { 'monospaced' => 'monospaced' })
  288       assert_equal '<code>a few &lt;monospaced&gt; words</code>', para.apply_subs(para.source)
  289     end
  290 
  291     # NOTE must use apply_subs because constrained monospaced is handled as a passthrough
  292     test 'single-line constrained monospaced string with role' do
  293       para = block_from_string(%([input]`a few <{monospaced}> words`), attributes: { 'monospaced' => 'monospaced', 'compat-mode' => '' })
  294       assert_equal '<code class="input">a few &lt;{monospaced}&gt; words</code>', para.apply_subs(para.source)
  295 
  296       para = block_from_string(%([input]`a few <{monospaced}> words`), attributes: { 'monospaced' => 'monospaced' })
  297       assert_equal '<code class="input">a few &lt;monospaced&gt; words</code>', para.apply_subs(para.source)
  298     end
  299 
  300     # NOTE must use apply_subs because constrained monospaced is handled as a passthrough
  301     test 'escaped single-line constrained monospaced string' do
  302       para = block_from_string(%(#{BACKSLASH}`a few <monospaced> words`), attributes: { 'compat-mode' => '' })
  303       assert_equal '`a few &lt;monospaced&gt; words`', para.apply_subs(para.source)
  304 
  305       para = block_from_string(%(#{BACKSLASH}`a few <monospaced> words`))
  306       assert_equal '`a few &lt;monospaced&gt; words`', para.apply_subs(para.source)
  307     end
  308 
  309     # NOTE must use apply_subs because constrained monospaced is handled as a passthrough
  310     test 'escaped single-line constrained monospaced string with role' do
  311       para = block_from_string(%([input]#{BACKSLASH}`a few <monospaced> words`), attributes: { 'compat-mode' => '' })
  312       assert_equal '[input]`a few &lt;monospaced&gt; words`', para.apply_subs(para.source)
  313 
  314       para = block_from_string(%([input]#{BACKSLASH}`a few <monospaced> words`))
  315       assert_equal '[input]`a few &lt;monospaced&gt; words`', para.apply_subs(para.source)
  316     end
  317 
  318     # NOTE must use apply_subs because constrained monospaced is handled as a passthrough
  319     test 'escaped role on single-line constrained monospaced string' do
  320       para = block_from_string(%(#{BACKSLASH}[input]`a few <monospaced> words`), attributes: { 'compat-mode' => '' })
  321       assert_equal '[input]<code>a few &lt;monospaced&gt; words</code>', para.apply_subs(para.source)
  322 
  323       para = block_from_string(%(#{BACKSLASH}[input]`a few <monospaced> words`))
  324       assert_equal '[input]<code>a few &lt;monospaced&gt; words</code>', para.apply_subs(para.source)
  325     end
  326 
  327     # NOTE must use apply_subs because constrained monospaced is handled as a passthrough
  328     test 'escaped role on escaped single-line constrained monospaced string' do
  329       para = block_from_string(%(#{BACKSLASH}[input]#{BACKSLASH}`a few <monospaced> words`), attributes: { 'compat-mode' => '' })
  330       assert_equal %(#{BACKSLASH}[input]`a few &lt;monospaced&gt; words`), para.apply_subs(para.source)
  331 
  332       para = block_from_string(%(#{BACKSLASH}[input]#{BACKSLASH}`a few <monospaced> words`))
  333       assert_equal %(#{BACKSLASH}[input]`a few &lt;monospaced&gt; words`), para.apply_subs(para.source)
  334     end
  335 
  336     # NOTE must use apply_subs because constrained monospaced is handled as a passthrough
  337     test 'multi-line constrained monospaced string' do
  338       para = block_from_string(%(`a few\n<{monospaced}> words`), attributes: { 'monospaced' => 'monospaced', 'compat-mode' => '' })
  339       assert_equal "<code>a few\n&lt;{monospaced}&gt; words</code>", para.apply_subs(para.source)
  340 
  341       para = block_from_string(%(`a few\n<{monospaced}> words`), attributes: { 'monospaced' => 'monospaced' })
  342       assert_equal "<code>a few\n&lt;monospaced&gt; words</code>", para.apply_subs(para.source)
  343     end
  344 
  345     test 'single-line unconstrained strong chars' do
  346       para = block_from_string(%q{**Git**Hub})
  347       assert_equal '<strong>Git</strong>Hub', para.sub_quotes(para.source)
  348     end
  349 
  350     test 'escaped single-line unconstrained strong chars' do
  351       para = block_from_string(%(#{BACKSLASH}**Git**Hub))
  352       assert_equal '<strong>*Git</strong>*Hub', para.sub_quotes(para.source)
  353     end
  354 
  355     test 'multi-line unconstrained strong chars' do
  356       para = block_from_string(%Q{**G\ni\nt\n**Hub})
  357       assert_equal "<strong>G\ni\nt\n</strong>Hub", para.sub_quotes(para.source)
  358     end
  359 
  360     test 'unconstrained strong chars with inline asterisk' do
  361       para = block_from_string(%q{**bl*ck**-eye})
  362       assert_equal '<strong>bl*ck</strong>-eye', para.sub_quotes(para.source)
  363     end
  364 
  365     test 'unconstrained strong chars with role' do
  366       para = block_from_string(%q{Git[blue]**Hub**})
  367       assert_equal %q{Git<strong class="blue">Hub</strong>}, para.sub_quotes(para.source)
  368     end
  369 
  370     # TODO this is not the same result as AsciiDoc, though I don't understand why AsciiDoc gets what it gets
  371     test 'escaped unconstrained strong chars with role' do
  372       para = block_from_string(%(Git#{BACKSLASH}[blue]**Hub**))
  373       assert_equal %q{Git[blue]<strong>*Hub</strong>*}, para.sub_quotes(para.source)
  374     end
  375 
  376     test 'single-line unconstrained emphasized chars' do
  377       para = block_from_string(%q{__Git__Hub})
  378       assert_equal '<em>Git</em>Hub', para.sub_quotes(para.source)
  379     end
  380 
  381     test 'escaped single-line unconstrained emphasized chars' do
  382       para = block_from_string(%(#{BACKSLASH}__Git__Hub))
  383       assert_equal '__Git__Hub', para.sub_quotes(para.source)
  384     end
  385 
  386     test 'escaped single-line unconstrained emphasized chars around word' do
  387       para = block_from_string(%(#{BACKSLASH}#{BACKSLASH}__GitHub__))
  388       assert_equal '__GitHub__', para.sub_quotes(para.source)
  389     end
  390 
  391     test 'multi-line unconstrained emphasized chars' do
  392       para = block_from_string(%Q{__G\ni\nt\n__Hub})
  393       assert_equal "<em>G\ni\nt\n</em>Hub", para.sub_quotes(para.source)
  394     end
  395 
  396     test 'unconstrained emphasis chars with role' do
  397       para = block_from_string(%q{[gray]__Git__Hub})
  398       assert_equal %q{<em class="gray">Git</em>Hub}, para.sub_quotes(para.source)
  399     end
  400 
  401     test 'escaped unconstrained emphasis chars with role' do
  402       para = block_from_string(%(#{BACKSLASH}[gray]__Git__Hub))
  403       assert_equal %q{[gray]__Git__Hub}, para.sub_quotes(para.source)
  404     end
  405 
  406     test 'single-line constrained monospaced chars' do
  407       para = block_from_string(%q{call +save()+ to persist the changes}, attributes: { 'compat-mode' => '' })
  408       assert_equal 'call <code>save()</code> to persist the changes', para.sub_quotes(para.source)
  409 
  410       para = block_from_string(%q{call [x-]+save()+ to persist the changes})
  411       assert_equal 'call <code>save()</code> to persist the changes', para.apply_subs(para.source)
  412 
  413       para = block_from_string(%q{call `save()` to persist the changes})
  414       assert_equal 'call <code>save()</code> to persist the changes', para.sub_quotes(para.source)
  415     end
  416 
  417     test 'single-line constrained monospaced chars with role' do
  418       para = block_from_string(%q{call [method]+save()+ to persist the changes}, attributes: { 'compat-mode' => '' })
  419       assert_equal 'call <code class="method">save()</code> to persist the changes', para.sub_quotes(para.source)
  420 
  421       para = block_from_string(%q{call [method x-]+save()+ to persist the changes})
  422       assert_equal 'call <code class="method">save()</code> to persist the changes', para.apply_subs(para.source)
  423 
  424       para = block_from_string(%q{call [method]`save()` to persist the changes})
  425       assert_equal 'call <code class="method">save()</code> to persist the changes', para.sub_quotes(para.source)
  426     end
  427 
  428     test 'escaped single-line constrained monospaced chars' do
  429       para = block_from_string(%(call #{BACKSLASH}+save()+ to persist the changes), attributes: { 'compat-mode' => '' })
  430       assert_equal 'call +save()+ to persist the changes', para.sub_quotes(para.source)
  431 
  432       para = block_from_string(%(call #{BACKSLASH}`save()` to persist the changes))
  433       assert_equal 'call `save()` to persist the changes', para.sub_quotes(para.source)
  434     end
  435 
  436     test 'escaped single-line constrained monospaced chars with role' do
  437       para = block_from_string(%(call [method]#{BACKSLASH}+save()+ to persist the changes), attributes: { 'compat-mode' => '' })
  438       assert_equal 'call [method]+save()+ to persist the changes', para.sub_quotes(para.source)
  439 
  440       para = block_from_string(%(call [method]#{BACKSLASH}`save()` to persist the changes))
  441       assert_equal 'call [method]`save()` to persist the changes', para.sub_quotes(para.source)
  442     end
  443 
  444     test 'escaped role on single-line constrained monospaced chars' do
  445       para = block_from_string(%(call #{BACKSLASH}[method]+save()+ to persist the changes), attributes: { 'compat-mode' => '' })
  446       assert_equal 'call [method]<code>save()</code> to persist the changes', para.sub_quotes(para.source)
  447 
  448       para = block_from_string(%(call #{BACKSLASH}[method]`save()` to persist the changes))
  449       assert_equal 'call [method]<code>save()</code> to persist the changes', para.sub_quotes(para.source)
  450     end
  451 
  452     test 'escaped role on escaped single-line constrained monospaced chars' do
  453       para = block_from_string(%(call #{BACKSLASH}[method]#{BACKSLASH}+save()+ to persist the changes), attributes: { 'compat-mode' => '' })
  454       assert_equal %(call #{BACKSLASH}[method]+save()+ to persist the changes), para.sub_quotes(para.source)
  455 
  456       para = block_from_string(%(call #{BACKSLASH}[method]#{BACKSLASH}`save()` to persist the changes))
  457       assert_equal %(call #{BACKSLASH}[method]`save()` to persist the changes), para.sub_quotes(para.source)
  458     end
  459 
  460     test 'single-line unconstrained monospaced chars' do
  461       para = block_from_string(%q{Git++Hub++}, attributes: { 'compat-mode' => '' })
  462       assert_equal 'Git<code>Hub</code>', para.sub_quotes(para.source)
  463 
  464       para = block_from_string(%q{Git[x-]++Hub++})
  465       assert_equal 'Git<code>Hub</code>', para.apply_subs(para.source)
  466 
  467       para = block_from_string(%q{Git``Hub``})
  468       assert_equal 'Git<code>Hub</code>', para.sub_quotes(para.source)
  469     end
  470 
  471     test 'escaped single-line unconstrained monospaced chars' do
  472       para = block_from_string(%(Git#{BACKSLASH}++Hub++), attributes: { 'compat-mode' => '' })
  473       assert_equal 'Git+<code>Hub</code>+', para.sub_quotes(para.source)
  474 
  475       para = block_from_string(%(Git#{BACKSLASH * 2}++Hub++), attributes: { 'compat-mode' => '' })
  476       assert_equal 'Git++Hub++', para.sub_quotes(para.source)
  477 
  478       para = block_from_string(%(Git#{BACKSLASH}``Hub``))
  479       assert_equal 'Git``Hub``', para.sub_quotes(para.source)
  480     end
  481 
  482     test 'multi-line unconstrained monospaced chars' do
  483       para = block_from_string(%Q{Git++\nH\nu\nb++}, attributes: { 'compat-mode' => '' })
  484       assert_equal "Git<code>\nH\nu\nb</code>", para.sub_quotes(para.source)
  485 
  486       para = block_from_string(%Q{Git[x-]++\nH\nu\nb++})
  487       assert_equal %(Git<code>\nH\nu\nb</code>), para.apply_subs(para.source)
  488 
  489       para = block_from_string(%Q{Git``\nH\nu\nb``})
  490       assert_equal "Git<code>\nH\nu\nb</code>", para.sub_quotes(para.source)
  491     end
  492 
  493     test 'single-line superscript chars' do
  494       para = block_from_string(%(x^2^ = x * x, e = mc^2^, there's a 1^st^ time for everything))
  495       assert_equal %(x<sup>2</sup> = x * x, e = mc<sup>2</sup>, there\'s a 1<sup>st</sup> time for everything), para.sub_quotes(para.source)
  496     end
  497 
  498     test 'escaped single-line superscript chars' do
  499       para = block_from_string(%(x#{BACKSLASH}^2^ = x * x))
  500       assert_equal 'x^2^ = x * x', para.sub_quotes(para.source)
  501     end
  502 
  503     test 'does not match superscript across whitespace' do
  504       para = block_from_string(%Q{x^(n\n-\n1)^})
  505       assert_equal para.source, para.sub_quotes(para.source)
  506     end
  507 
  508     test 'does not match adjacent superscript chars' do
  509       para = block_from_string 'a ^^ b'
  510       assert_equal 'a ^^ b', para.sub_quotes(para.source)
  511     end
  512 
  513     test 'does not confuse superscript and links with blank window shorthand' do
  514       para = block_from_string(%Q{http://localhost[Text^] on the 21^st^ and 22^nd^})
  515       assert_equal '<a href="http://localhost" target="_blank" rel="noopener">Text</a> on the 21<sup>st</sup> and 22<sup>nd</sup>', para.content
  516     end
  517 
  518     test 'single-line subscript chars' do
  519       para = block_from_string(%q{H~2~O})
  520       assert_equal 'H<sub>2</sub>O', para.sub_quotes(para.source)
  521     end
  522 
  523     test 'escaped single-line subscript chars' do
  524       para = block_from_string(%(H#{BACKSLASH}~2~O))
  525       assert_equal 'H~2~O', para.sub_quotes(para.source)
  526     end
  527 
  528     test 'does not match subscript across whitespace' do
  529       para = block_from_string(%Q{project~ view\non\nGitHub~})
  530       assert_equal para.source, para.sub_quotes(para.source)
  531     end
  532 
  533     test 'does not match adjacent subscript chars' do
  534       para = block_from_string 'a ~~ b'
  535       assert_equal 'a ~~ b', para.sub_quotes(para.source)
  536     end
  537 
  538     test 'does not match subscript across distinct URLs' do
  539       para = block_from_string(%Q{http://www.abc.com/~def[DEF] and http://www.abc.com/~ghi[GHI]})
  540       assert_equal para.source, para.sub_quotes(para.source)
  541     end
  542 
  543     test 'quoted text with role shorthand' do
  544       para = block_from_string(%q{[.white.red-background]#alert#})
  545       assert_equal '<span class="white red-background">alert</span>', para.sub_quotes(para.source)
  546     end
  547 
  548     test 'quoted text with id shorthand' do
  549       para = block_from_string(%q{[#bond]#007#})
  550       assert_equal '<span id="bond">007</span>', para.sub_quotes(para.source)
  551     end
  552 
  553     test 'quoted text with id and role shorthand' do
  554       para = block_from_string(%q{[#bond.white.red-background]#007#})
  555       assert_equal '<span id="bond" class="white red-background">007</span>', para.sub_quotes(para.source)
  556     end
  557 
  558     test 'quoted text with id and role shorthand using docbook backend' do
  559       para = block_from_string(%q{[#bond.white.red-background]#007#}, backend: 'docbook')
  560       assert_equal '<anchor xml:id="bond" xreflabel="007"/><phrase role="white red-background">007</phrase>', para.sub_quotes(para.source)
  561     end
  562 
  563     test 'should ignore attributes after comma' do
  564       para = block_from_string(%q{[red, foobar]#alert#})
  565       assert_equal '<span class="red">alert</span>', para.sub_quotes(para.source)
  566     end
  567 
  568     test 'inline passthrough with id and role set using shorthand' do
  569       %w(#idname.rolename .rolename#idname).each do |attrlist|
  570         para = block_from_string %([#{attrlist}]+pass+)
  571         assert_equal '<span id="idname" class="rolename">pass</span>', para.content
  572       end
  573     end
  574 
  575     test 'should not assign role attribute if shorthand style has no roles' do
  576       para = block_from_string '[#idname]*blah*'
  577       assert_equal '<strong id="idname">blah</strong>', para.content
  578     end
  579   end
  580 
  581   context 'Macros' do
  582     test 'a single-line link macro should be interpreted as a link' do
  583       para = block_from_string('link:/home.html[]')
  584       assert_equal %q{<a href="/home.html" class="bare">/home.html</a>}, para.sub_macros(para.source)
  585     end
  586 
  587     test 'a single-line link macro with text should be interpreted as a link' do
  588       para = block_from_string('link:/home.html[Home]')
  589       assert_equal %q{<a href="/home.html">Home</a>}, para.sub_macros(para.source)
  590     end
  591 
  592     test 'a mailto macro should be interpreted as a mailto link' do
  593       para = block_from_string('mailto:doc.writer@asciidoc.org[]')
  594       assert_equal %q{<a href="mailto:doc.writer@asciidoc.org">doc.writer@asciidoc.org</a>}, para.sub_macros(para.source)
  595     end
  596 
  597     test 'a mailto macro with text should be interpreted as a mailto link' do
  598       para = block_from_string('mailto:doc.writer@asciidoc.org[Doc Writer]')
  599       assert_equal %q{<a href="mailto:doc.writer@asciidoc.org">Doc Writer</a>}, para.sub_macros(para.source)
  600     end
  601 
  602     test 'a mailto macro with text and subject should be interpreted as a mailto link' do
  603       para = block_from_string('mailto:doc.writer@asciidoc.org[Doc Writer, Pull request]')
  604       assert_equal %q{<a href="mailto:doc.writer@asciidoc.org?subject=Pull+request">Doc Writer</a>}, para.sub_macros(para.source)
  605     end
  606 
  607     test 'a mailto macro with text, subject and body should be interpreted as a mailto link' do
  608       para = block_from_string('mailto:doc.writer@asciidoc.org[Doc Writer, Pull request, Please accept my pull request]')
  609       assert_equal %q{<a href="mailto:doc.writer@asciidoc.org?subject=Pull+request&amp;body=Please+accept+my+pull+request">Doc Writer</a>}, para.sub_macros(para.source)
  610     end
  611 
  612     test 'a mailto macro with subject and body only should use e-mail as text' do
  613       para = block_from_string('mailto:doc.writer@asciidoc.org[,Pull request,Please accept my pull request]')
  614       assert_equal %q{<a href="mailto:doc.writer@asciidoc.org?subject=Pull+request&amp;body=Please+accept+my+pull+request">doc.writer@asciidoc.org</a>}, para.sub_macros(para.source)
  615     end
  616 
  617     test 'should recognize inline email addresses' do
  618       %w(
  619         doc.writer@asciidoc.org
  620         author+website@4fs.no
  621         john@domain.uk.co
  622         name@somewhere.else.com
  623         joe_bloggs@mail_server.com
  624         joe-bloggs@mail-server.com
  625         joe.bloggs@mail.server.com
  626         FOO@BAR.COM
  627         docs@writing.ninja
  628       ).each do |input|
  629         para = block_from_string input
  630         assert_equal %(<a href="mailto:#{input}">#{input}</a>), (para.sub_macros para.source)
  631       end
  632     end
  633 
  634     test 'should recognize inline email address containing an ampersand' do
  635       para = block_from_string('bert&ernie@sesamestreet.com')
  636       assert_equal %q{<a href="mailto:bert&amp;ernie@sesamestreet.com">bert&amp;ernie@sesamestreet.com</a>}, para.apply_subs(para.source)
  637     end
  638 
  639     test 'should recognize inline email address surrounded by angle brackets' do
  640       para = block_from_string('<doc.writer@asciidoc.org>')
  641       assert_equal %q{&lt;<a href="mailto:doc.writer@asciidoc.org">doc.writer@asciidoc.org</a>&gt;}, para.apply_subs(para.source)
  642     end
  643 
  644     test 'should ignore escaped inline email address' do
  645       para = block_from_string(%(#{BACKSLASH}doc.writer@asciidoc.org))
  646       assert_equal %q{doc.writer@asciidoc.org}, para.sub_macros(para.source)
  647     end
  648 
  649     test 'a single-line raw url should be interpreted as a link' do
  650       para = block_from_string('http://google.com')
  651       assert_equal %q{<a href="http://google.com" class="bare">http://google.com</a>}, para.sub_macros(para.source)
  652     end
  653 
  654     test 'a single-line raw url with text should be interpreted as a link' do
  655       para = block_from_string('http://google.com[Google]')
  656       assert_equal %q{<a href="http://google.com">Google</a>}, para.sub_macros(para.source)
  657     end
  658 
  659     test 'a multi-line raw url with text should be interpreted as a link' do
  660       para = block_from_string("http://google.com[Google\nHomepage]")
  661       assert_equal %{<a href="http://google.com">Google\nHomepage</a>}, para.sub_macros(para.source)
  662     end
  663 
  664     test 'a single-line raw url with attribute as text should be interpreted as a link with resolved attribute' do
  665       para = block_from_string("http://google.com[{google_homepage}]")
  666       para.document.attributes['google_homepage'] = 'Google Homepage'
  667       assert_equal %q{<a href="http://google.com">Google Homepage</a>}, para.sub_macros(para.sub_attributes(para.source))
  668     end
  669 
  670     test 'should not resolve an escaped attribute in link text' do
  671       {
  672         'http://google.com' => "http://google.com[#{BACKSLASH}{google_homepage}]",
  673         'http://google.com?q=,' => "link:http://google.com?q=,[#{BACKSLASH}{google_homepage}]",
  674       }.each do |uri, macro|
  675         para = block_from_string macro
  676         para.document.attributes['google_homepage'] = 'Google Homepage'
  677         assert_equal %(<a href="#{uri}">{google_homepage}</a>), para.sub_macros(para.sub_attributes(para.source))
  678       end
  679     end
  680 
  681     test 'a single-line escaped raw url should not be interpreted as a link' do
  682       para = block_from_string(%(#{BACKSLASH}http://google.com))
  683       assert_equal %q{http://google.com}, para.sub_macros(para.source)
  684     end
  685 
  686     test 'a comma separated list of links should not include commas in links' do
  687       para = block_from_string('http://foo.com, http://bar.com, http://example.org')
  688       assert_equal %q{<a href="http://foo.com" class="bare">http://foo.com</a>, <a href="http://bar.com" class="bare">http://bar.com</a>, <a href="http://example.org" class="bare">http://example.org</a>}, para.sub_macros(para.source)
  689     end
  690 
  691     test 'a single-line image macro should be interpreted as an image' do
  692       para = block_from_string('image:tiger.png[]')
  693       assert_equal %{<span class="image"><img src="tiger.png" alt="tiger"></span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  694     end
  695 
  696     test 'should replace underscore and hyphen with space in generated alt text for an inline image' do
  697       para = block_from_string('image:tiger-with-family_1.png[]')
  698       assert_equal %{<span class="image"><img src="tiger-with-family_1.png" alt="tiger with family 1"></span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  699     end
  700 
  701     test 'a single-line image macro with text should be interpreted as an image with alt text' do
  702       para = block_from_string('image:tiger.png[Tiger]')
  703       assert_equal %{<span class="image"><img src="tiger.png" alt="Tiger"></span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  704     end
  705 
  706     test 'should encode special characters in alt text of inline image' do
  707       input = 'A tiger\'s "roar" is < a bear\'s "growl"'
  708       expected = 'A tiger&#8217;s &quot;roar&quot; is &lt; a bear&#8217;s &quot;growl&quot;'
  709       output = (convert_inline_string %(image:tiger-roar.png[#{input}])).gsub(/>\s+</, '><')
  710       assert_equal %(<span class="image"><img src="tiger-roar.png" alt="#{expected}"></span>), output
  711     end
  712 
  713     test 'an image macro with SVG image and text should be interpreted as an image with alt text' do
  714       para = block_from_string('image:tiger.svg[Tiger]')
  715       assert_equal %{<span class="image"><img src="tiger.svg" alt="Tiger"></span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  716     end
  717 
  718     test 'an image macro with an interactive SVG image and alt text should be converted to an object element' do
  719       para = block_from_string('image:tiger.svg[Tiger,opts=interactive]', safe: Asciidoctor::SafeMode::SERVER, attributes: { 'imagesdir' => 'images' })
  720       assert_equal %{<span class="image"><object type="image/svg+xml" data="images/tiger.svg"><span class="alt">Tiger</span></object></span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  721     end
  722 
  723     test 'an image macro with an interactive SVG image, fallback and alt text should be converted to an object element' do
  724       para = block_from_string('image:tiger.svg[Tiger,fallback=tiger.png,opts=interactive]', safe: Asciidoctor::SafeMode::SERVER, attributes: { 'imagesdir' => 'images' })
  725       assert_equal %{<span class="image"><object type="image/svg+xml" data="images/tiger.svg"><img src="images/tiger.png" alt="Tiger"></object></span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  726     end
  727 
  728     test 'an image macro with an inline SVG image should be converted to an svg element' do
  729       para = block_from_string('image:circle.svg[Tiger,100,opts=inline]', safe: Asciidoctor::SafeMode::SERVER, attributes: { 'imagesdir' => 'fixtures', 'docdir' => testdir })
  730       result = para.sub_macros(para.source).gsub(/>\s+</, '><')
  731       assert_match(/<svg\s[^>]*width="100px"[^>]*>/, result)
  732       refute_match(/<svg\s[^>]*width="500px"[^>]*>/, result)
  733       refute_match(/<svg\s[^>]*height="500px"[^>]*>/, result)
  734       refute_match(/<svg\s[^>]*style="width:500px;height:500px"[^>]*>/, result)
  735     end
  736 
  737     test 'an image macro with an inline SVG image should be converted to an svg element even when data-uri is set' do
  738       para = block_from_string('image:circle.svg[Tiger,100,opts=inline]', safe: Asciidoctor::SafeMode::SERVER, attributes: { 'data-uri' => '', 'imagesdir' => 'fixtures', 'docdir' => testdir })
  739       assert_match(/<svg\s[^>]*width="100px">/, para.sub_macros(para.source).gsub(/>\s+</, '><'))
  740     end
  741 
  742     test 'an image macro with an SVG image should not use an object element when safe mode is secure' do
  743       para = block_from_string('image:tiger.svg[Tiger,opts=interactive]', attributes: { 'imagesdir' => 'images' })
  744       assert_equal %{<span class="image"><img src="images/tiger.svg" alt="Tiger"></span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  745     end
  746 
  747     test 'a single-line image macro with text containing escaped square bracket should be interpreted as an image with alt text' do
  748       para = block_from_string(%(image:tiger.png[[Another#{BACKSLASH}] Tiger]))
  749       assert_equal %{<span class="image"><img src="tiger.png" alt="[Another] Tiger"></span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  750     end
  751 
  752     test 'a single-line image macro with text and dimensions should be interpreted as an image with alt text and dimensions' do
  753       para = block_from_string('image:tiger.png[Tiger, 200, 100]')
  754       assert_equal %{<span class="image"><img src="tiger.png" alt="Tiger" width="200" height="100"></span>},
  755           para.sub_macros(para.source).gsub(/>\s+</, '><')
  756     end
  757 
  758     test 'a single-line image macro with text and dimensions should be interpreted as an image with alt text and dimensions in docbook' do
  759       para = block_from_string 'image:tiger.png[Tiger, 200, 100]', backend: 'docbook'
  760       assert_equal %{<inlinemediaobject><imageobject><imagedata fileref="tiger.png" contentwidth="200" contentdepth="100"/></imageobject><textobject><phrase>Tiger</phrase></textobject></inlinemediaobject>},
  761           para.sub_macros(para.source).gsub(/>\s+</, '><')
  762     end
  763 
  764     test 'a single-line image macro with text and link should be interpreted as a linked image with alt text' do
  765       para = block_from_string('image:tiger.png[Tiger, link="http://en.wikipedia.org/wiki/Tiger"]')
  766       assert_equal %{<span class="image"><a class="image" href="http://en.wikipedia.org/wiki/Tiger"><img src="tiger.png" alt="Tiger"></a></span>},
  767           para.sub_macros(para.source).gsub(/>\s+</, '><')
  768     end
  769 
  770     test 'rel=noopener should be added to an image with a link that targets the _blank window' do
  771       para = block_from_string 'image:tiger.png[Tiger,link=http://en.wikipedia.org/wiki/Tiger,window=_blank]'
  772       assert_equal %{<span class="image"><a class="image" href="http://en.wikipedia.org/wiki/Tiger" target="_blank" rel="noopener"><img src="tiger.png" alt="Tiger"></a></span>},
  773           para.sub_macros(para.source).gsub(/>\s+</, '><')
  774     end
  775 
  776     test 'rel=noopener should be added to an image with a link that targets a named window when the noopener option is set' do
  777       para = block_from_string 'image:tiger.png[Tiger,link=http://en.wikipedia.org/wiki/Tiger,window=name,opts=noopener]'
  778       assert_equal %{<span class="image"><a class="image" href="http://en.wikipedia.org/wiki/Tiger" target="name" rel="noopener"><img src="tiger.png" alt="Tiger"></a></span>},
  779           para.sub_macros(para.source).gsub(/>\s+</, '><')
  780     end
  781 
  782     test 'rel=nofollow should be added to an image with a link when the nofollow option is set' do
  783       para = block_from_string 'image:tiger.png[Tiger,link=http://en.wikipedia.org/wiki/Tiger,opts=nofollow]'
  784       assert_equal %{<span class="image"><a class="image" href="http://en.wikipedia.org/wiki/Tiger" rel="nofollow"><img src="tiger.png" alt="Tiger"></a></span>},
  785           para.sub_macros(para.source).gsub(/>\s+</, '><')
  786     end
  787 
  788     test 'a multi-line image macro with text and dimensions should be interpreted as an image with alt text and dimensions' do
  789       para = block_from_string(%(image:tiger.png[Another\nAwesome\nTiger, 200,\n100]))
  790       assert_equal %{<span class="image"><img src="tiger.png" alt="Another Awesome Tiger" width="200" height="100"></span>},
  791           para.sub_macros(para.source).gsub(/>\s+</, '><')
  792     end
  793 
  794     test 'an inline image macro with a url target should be interpreted as an image' do
  795       para = block_from_string %(Beware of the image:http://example.com/images/tiger.png[tiger].)
  796       assert_equal %{Beware of the <span class="image"><img src="http://example.com/images/tiger.png" alt="tiger"></span>.},
  797           para.sub_macros(para.source).gsub(/>\s+</, '><')
  798     end
  799 
  800     test 'an inline image macro with a float attribute should be interpreted as a floating image' do
  801       para = block_from_string %(image:http://example.com/images/tiger.png[tiger, float="right"] Beware of the tigers!)
  802       assert_equal %{<span class="image right"><img src="http://example.com/images/tiger.png" alt="tiger"></span> Beware of the tigers!},
  803           para.sub_macros(para.source).gsub(/>\s+</, '><')
  804     end
  805 
  806     test 'should prepend value of imagesdir attribute to inline image target if target is relative path' do
  807       para = block_from_string %(Beware of the image:tiger.png[tiger].), attributes: { 'imagesdir' => './images' }
  808       assert_equal %{Beware of the <span class="image"><img src="./images/tiger.png" alt="tiger"></span>.},
  809           para.sub_macros(para.source).gsub(/>\s+</, '><')
  810     end
  811 
  812     test 'should not prepend value of imagesdir attribute to inline image target if target is absolute path' do
  813       para = block_from_string %(Beware of the image:/tiger.png[tiger].), attributes: { 'imagesdir' => './images' }
  814       assert_equal %{Beware of the <span class="image"><img src="/tiger.png" alt="tiger"></span>.},
  815           para.sub_macros(para.source).gsub(/>\s+</, '><')
  816     end
  817 
  818     test 'should not prepend value of imagesdir attribute to inline image target if target is url' do
  819       para = block_from_string %(Beware of the image:http://example.com/images/tiger.png[tiger].), attributes: { 'imagesdir' => './images' }
  820       assert_equal %{Beware of the <span class="image"><img src="http://example.com/images/tiger.png" alt="tiger"></span>.},
  821           para.sub_macros(para.source).gsub(/>\s+</, '><')
  822     end
  823 
  824     test 'should match an inline image macro if target contains a space character' do
  825       para = block_from_string(%(Beware of the image:big cats.png[] around here.))
  826       assert_equal %(Beware of the <span class="image"><img src="big%20cats.png" alt="big cats"></span> around here.),
  827           para.sub_macros(para.source).gsub(/>\s+</, '><')
  828     end
  829 
  830     test 'should not match an inline image macro if target contains a newline character' do
  831       para = block_from_string(%(Fear not. There are no image:big\ncats.png[] around here.))
  832       result = para.sub_macros(para.source)
  833       refute_includes result, '<img '
  834       assert_includes result, %(image:big\ncats.png[])
  835     end
  836 
  837     test 'should not match an inline image macro if target begins or ends with space character' do
  838       ['image: big cats.png[]', 'image:big cats.png []'].each do |input|
  839         para = block_from_string %(Fear not. There are no #{input} around here.)
  840         result = para.sub_macros(para.source)
  841         refute_includes result, '<img '
  842         assert_includes result, input
  843       end
  844     end
  845 
  846     test 'should not detect a block image macro found inline' do
  847       para = block_from_string(%(Not an inline image macro image::tiger.png[].))
  848       result = para.sub_macros(para.source)
  849       refute_includes result, '<img '
  850       assert_includes result, 'image::tiger.png[]'
  851     end
  852 
  853     # NOTE this test verifies attributes get substituted eagerly in target of image in title
  854     test 'should substitute attributes in target of inline image in section title' do
  855       input = '== image:{iconsdir}/dot.gif[dot] Title'
  856 
  857       using_memory_logger do |logger|
  858         sect = block_from_string input, attributes: { 'data-uri' => '', 'iconsdir' => 'fixtures', 'docdir' => testdir }, safe: :server, catalog_assets: true
  859         assert 1, sect.document.catalog[:images].size
  860         assert_equal 'fixtures/dot.gif', sect.document.catalog[:images][0].to_s
  861         assert_nil sect.document.catalog[:images][0].imagesdir
  862         assert logger.empty?
  863       end
  864     end
  865 
  866     test 'an icon macro should be interpreted as an icon if icons are enabled' do
  867       para = block_from_string 'icon:github[]', attributes: { 'icons' => '' }
  868       assert_equal %{<span class="icon"><img src="./images/icons/github.png" alt="github"></span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  869     end
  870 
  871     test 'an icon macro should be interpreted as alt text if icons are disabled' do
  872       para = block_from_string 'icon:github[]'
  873       assert_equal %{<span class="icon">[github]</span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  874     end
  875 
  876     test 'an icon macro should output alt text if icons are disabled and alt is given' do
  877       para = block_from_string 'icon:github[alt="GitHub"]'
  878       assert_equal %{<span class="icon">[GitHub]</span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  879     end
  880 
  881     test 'an icon macro should be interpreted as a font-based icon when icons=font' do
  882       para = block_from_string 'icon:github[]', attributes: { 'icons' => 'font' }
  883       assert_equal %{<span class="icon"><i class="fa fa-github"></i></span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  884     end
  885 
  886     test 'an icon macro with a size should be interpreted as a font-based icon with a size when icons=font' do
  887       para = block_from_string 'icon:github[4x]', attributes: { 'icons' => 'font' }
  888       assert_equal %{<span class="icon"><i class="fa fa-github fa-4x"></i></span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  889     end
  890 
  891     test 'an icon macro with a role and title should be interpreted as a font-based icon with a class and title when icons=font' do
  892       para = block_from_string 'icon:heart[role="red", title="Heart me"]', attributes: { 'icons' => 'font' }
  893       assert_equal %{<span class="icon red"><i class="fa fa-heart" title="Heart me"></i></span>}, para.sub_macros(para.source).gsub(/>\s+</, '><')
  894     end
  895 
  896     test 'a single-line footnote macro should be registered and output as a footnote' do
  897       para = block_from_string('Sentence text footnote:[An example footnote.].')
  898       assert_equal %(Sentence text <sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>.), para.sub_macros(para.source)
  899       assert_equal 1, para.document.catalog[:footnotes].size
  900       footnote = para.document.catalog[:footnotes].first
  901       assert_equal 1, footnote.index
  902       assert_nil footnote.id
  903       assert_equal 'An example footnote.', footnote.text
  904     end
  905 
  906     test 'a multi-line footnote macro should be registered and output as a footnote without newline' do
  907       para = block_from_string("Sentence text footnote:[An example footnote\nwith wrapped text.].")
  908       assert_equal %(Sentence text <sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>.), para.sub_macros(para.source)
  909       assert_equal 1, para.document.catalog[:footnotes].size
  910       footnote = para.document.catalog[:footnotes].first
  911       assert_equal 1, footnote.index
  912       assert_nil footnote.id
  913       assert_equal "An example footnote with wrapped text.", footnote.text
  914     end
  915 
  916     test 'an escaped closing square bracket in a footnote should be unescaped when converted' do
  917       para = block_from_string(%(footnote:[a #{BACKSLASH}] b].))
  918       assert_equal %(<sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>.), para.sub_macros(para.source)
  919       assert_equal 1, para.document.catalog[:footnotes].size
  920       footnote = para.document.catalog[:footnotes].first
  921       assert_equal "a ] b", footnote.text
  922     end
  923 
  924     test 'a footnote macro can be directly adjacent to preceding word' do
  925       para = block_from_string('Sentence textfootnote:[An example footnote.].')
  926       assert_equal %(Sentence text<sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>.), para.sub_macros(para.source)
  927     end
  928 
  929     test 'a footnote macro may contain an escaped backslash' do
  930       para = block_from_string("footnote:[\\]]\nfootnote:[a \\] b]\nfootnote:[a \\]\\] b]")
  931       para.sub_macros(para.source)
  932       assert_equal 3, para.document.catalog[:footnotes].size
  933       footnote1 = para.document.catalog[:footnotes][0]
  934       assert_equal ']', footnote1.text
  935       footnote2 = para.document.catalog[:footnotes][1]
  936       assert_equal 'a ] b', footnote2.text
  937       footnote3 = para.document.catalog[:footnotes][2]
  938       assert_equal 'a ]] b', footnote3.text
  939     end
  940 
  941     test 'a footnote macro may contain a link macro' do
  942       para = block_from_string('Share your code. footnote:[https://github.com[GitHub]]')
  943       assert_equal %(Share your code. <sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>), para.sub_macros(para.source)
  944       assert_equal 1, para.document.catalog[:footnotes].size
  945       footnote1 = para.document.catalog[:footnotes][0]
  946       assert_equal '<a href="https://github.com">GitHub</a>', footnote1.text
  947     end
  948 
  949     test 'a footnote macro may contain a plain URL' do
  950       para = block_from_string %(the JLine footnote:[https://github.com/jline/jline2]\nlibrary.)
  951       result = para.sub_macros para.source
  952       assert_equal %(the JLine <sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>\nlibrary.), result
  953       assert_equal 1, para.document.catalog[:footnotes].size
  954       fn1 = para.document.catalog[:footnotes].first
  955       assert_equal '<a href="https://github.com/jline/jline2" class="bare">https://github.com/jline/jline2</a>', fn1.text
  956     end
  957 
  958     test 'a footnote macro followed by a semi-colon may contain a plain URL' do
  959       para = block_from_string %(the JLine footnote:[https://github.com/jline/jline2];\nlibrary.)
  960       result = para.sub_macros para.source
  961       assert_equal %(the JLine <sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>;\nlibrary.), result
  962       assert_equal 1, para.document.catalog[:footnotes].size
  963       fn1 = para.document.catalog[:footnotes].first
  964       assert_equal '<a href="https://github.com/jline/jline2" class="bare">https://github.com/jline/jline2</a>', fn1.text
  965     end
  966 
  967     test 'a footnote macro may contain a shorthand xref' do
  968       # specialcharacters escaping is simulated
  969       para = block_from_string('text footnote:[&lt;&lt;_install,install&gt;&gt;]')
  970       doc = para.document
  971       doc.register :refs, ['_install', (Asciidoctor::Inline.new doc, :anchor, 'Install', type: :ref, target: '_install'), 'Install']
  972       catalog = doc.catalog
  973       assert_equal %(text <sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>), para.sub_macros(para.source)
  974       assert_equal 1, catalog[:footnotes].size
  975       footnote1 = catalog[:footnotes][0]
  976       assert_equal '<a href="#_install">install</a>', footnote1.text
  977     end
  978 
  979     test 'a footnote macro may contain an xref macro' do
  980       para = block_from_string('text footnote:[xref:_install[install]]')
  981       doc = para.document
  982       doc.register :refs, ['_install', (Asciidoctor::Inline.new doc, :anchor, 'Install', type: :ref, target: '_install'), 'Install']
  983       catalog = doc.catalog
  984       assert_equal %(text <sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>), para.sub_macros(para.source)
  985       assert_equal 1, catalog[:footnotes].size
  986       footnote1 = catalog[:footnotes][0]
  987       assert_equal '<a href="#_install">install</a>', footnote1.text
  988     end
  989 
  990     test 'a footnote macro may contain an anchor macro' do
  991       para = block_from_string('text footnote:[a [[b]] [[c\]\] d]')
  992       assert_equal %(text <sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>), para.sub_macros(para.source)
  993       assert_equal 1, para.document.catalog[:footnotes].size
  994       footnote1 = para.document.catalog[:footnotes][0]
  995       assert_equal 'a <a id="b"></a> [[c]] d', footnote1.text
  996     end
  997 
  998     test 'subsequent footnote macros with escaped URLs should be restored in DocBook' do
  999       input = 'foofootnote:[+http://example.com+]barfootnote:[+http://acme.com+]baz'
 1000 
 1001       result = convert_string_to_embedded input, doctype: 'inline', backend: 'docbook'
 1002       assert_equal 'foo<footnote><simpara>http://example.com</simpara></footnote>bar<footnote><simpara>http://acme.com</simpara></footnote>baz', result
 1003     end
 1004 
 1005     test 'should increment index of subsequent footnote macros' do
 1006       para = block_from_string("Sentence text footnote:[An example footnote.]. Sentence text footnote:[Another footnote.].")
 1007       assert_equal %(Sentence text <sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>. Sentence text <sup class="footnote">[<a id="_footnoteref_2" class="footnote" href="#_footnotedef_2" title="View footnote.">2</a>]</sup>.), para.sub_macros(para.source)
 1008       assert_equal 2, para.document.catalog[:footnotes].size
 1009       footnote1 = para.document.catalog[:footnotes][0]
 1010       assert_equal 1, footnote1.index
 1011       assert_nil footnote1.id
 1012       assert_equal "An example footnote.", footnote1.text
 1013       footnote2 = para.document.catalog[:footnotes][1]
 1014       assert_equal 2, footnote2.index
 1015       assert_nil footnote2.id
 1016       assert_equal "Another footnote.", footnote2.text
 1017     end
 1018 
 1019     test 'a footnoteref macro with id and single-line text should be registered and output as a footnote' do
 1020       para = block_from_string 'Sentence text footnoteref:[ex1, An example footnote.].', attributes: { 'compat-mode' => '' }
 1021       assert_equal %(Sentence text <sup class="footnote" id="_footnote_ex1">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>.), para.sub_macros(para.source)
 1022       assert_equal 1, para.document.catalog[:footnotes].size
 1023       footnote = para.document.catalog[:footnotes].first
 1024       assert_equal 1, footnote.index
 1025       assert_equal 'ex1', footnote.id
 1026       assert_equal 'An example footnote.', footnote.text
 1027     end
 1028 
 1029     test 'a footnoteref macro with id and multi-line text should be registered and output as a footnote without newlines' do
 1030       para = block_from_string "Sentence text footnoteref:[ex1, An example footnote\nwith wrapped text.].", attributes: { 'compat-mode' => '' }
 1031       assert_equal %(Sentence text <sup class="footnote" id="_footnote_ex1">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>.), para.sub_macros(para.source)
 1032       assert_equal 1, para.document.catalog[:footnotes].size
 1033       footnote = para.document.catalog[:footnotes].first
 1034       assert_equal 1, footnote.index
 1035       assert_equal 'ex1', footnote.id
 1036       assert_equal "An example footnote with wrapped text.", footnote.text
 1037     end
 1038 
 1039     test 'a footnoteref macro with id should refer to footnoteref with same id' do
 1040       para = block_from_string 'Sentence text footnoteref:[ex1, An example footnote.]. Sentence text footnoteref:[ex1].', attributes: { 'compat-mode' => '' }
 1041       assert_equal %(Sentence text <sup class="footnote" id="_footnote_ex1">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>. Sentence text <sup class="footnoteref">[<a class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>.), para.sub_macros(para.source)
 1042       assert_equal 1, para.document.catalog[:footnotes].size
 1043       footnote = para.document.catalog[:footnotes].first
 1044       assert_equal 1, footnote.index
 1045       assert_equal 'ex1', footnote.id
 1046       assert_equal 'An example footnote.', footnote.text
 1047     end
 1048 
 1049     test 'an unresolved footnote reference should produce a warning message' do
 1050       input = 'Sentence text.footnote:ex1[]'
 1051       using_memory_logger do |logger|
 1052         para = block_from_string input
 1053         para.sub_macros para.source
 1054         assert_message logger, :WARN, 'invalid footnote reference: ex1'
 1055       end
 1056     end
 1057 
 1058     test 'using a footnoteref macro should generate a warning when compat mode is not enabled' do
 1059       input = 'Sentence text.footnoteref:[fn1,Commentary on this sentence.]'
 1060       using_memory_logger do |logger|
 1061         para = block_from_string input
 1062         para.sub_macros para.source
 1063         assert_message logger, :WARN, 'found deprecated footnoteref macro: footnoteref:[fn1,Commentary on this sentence.]; use footnote macro with target instead'
 1064       end
 1065     end
 1066 
 1067     test 'inline footnote macro can be used to define and reference a footnote reference' do
 1068       input = <<~'EOS'
 1069       You can download the software from the product page.footnote:sub[Option only available if you have an active subscription.]
 1070 
 1071       You can also file a support request.footnote:sub[]
 1072 
 1073       If all else fails, you can give us a call.footnoteref:[sub]
 1074       EOS
 1075 
 1076       using_memory_logger do |logger|
 1077         output = convert_string_to_embedded input, attributes: { 'compat-mode' => '' }
 1078         assert_css '#_footnotedef_1', output, 1
 1079         assert_css 'p a[href="#_footnotedef_1"]', output, 3
 1080         assert_css '#footnotes .footnote', output, 1
 1081         assert logger.empty?
 1082       end
 1083     end
 1084 
 1085     test 'should parse multiple footnote references in a single line' do
 1086       input = 'notable text.footnote:id[about this [text\]], footnote:id[], footnote:id[]'
 1087       output = convert_string_to_embedded input
 1088       assert_xpath '(//p)[1]/sup[starts-with(@class,"footnote")]', output, 3
 1089       assert_xpath '(//p)[1]/sup[@class="footnote"]', output, 1
 1090       assert_xpath '(//p)[1]/sup[@class="footnoteref"]', output, 2
 1091       assert_xpath '(//p)[1]/sup[starts-with(@class,"footnote")]/a[@class="footnote"][text()="1"]', output, 3
 1092       assert_css '#footnotes .footnote', output, 1
 1093     end
 1094 
 1095     test 'should not resolve an inline footnote macro missing both id and text' do
 1096       input = <<~'EOS'
 1097       The footnote:[] macro can be used for defining and referencing footnotes.
 1098 
 1099       The footnoteref:[] macro is now deprecated.
 1100       EOS
 1101 
 1102       output = convert_string_to_embedded input
 1103       assert_includes output, 'The footnote:[] macro'
 1104       assert_includes output, 'The footnoteref:[] macro'
 1105     end
 1106 
 1107     test 'inline footnote macro can define a numeric id without conflicting with auto-generated ID' do
 1108       input = 'You can download the software from the product page.footnote:1[Option only available if you have an active subscription.]'
 1109 
 1110       output = convert_string_to_embedded input
 1111       assert_css '#_footnote_1', output, 1
 1112       assert_css 'p sup#_footnote_1', output, 1
 1113       assert_css 'p a#_footnoteref_1', output, 1
 1114       assert_css 'p a[href="#_footnotedef_1"]', output, 1
 1115       assert_css '#footnotes #_footnotedef_1', output, 1
 1116     end
 1117 
 1118     test 'inline footnote macro can define an id that uses any word characters in Unicode' do
 1119       input = <<~'EOS'
 1120       L'origine du mot forêt{blank}footnote:forêt[un massif forestier] est complexe.
 1121 
 1122       Qu'est-ce qu'une forêt ?{blank}footnote:forêt[]
 1123       EOS
 1124       output = convert_string_to_embedded input
 1125       assert_css '#_footnote_forêt', output, 1
 1126       assert_css '#_footnotedef_1', output, 1
 1127       assert_xpath '//a[@class="footnote"][text()="1"]', output, 2
 1128     end
 1129 
 1130     test 'a single-line index term macro with a primary term should be registered as an index reference' do
 1131       sentence = "The tiger (Panthera tigris) is the largest cat species.\n"
 1132       macros = ['indexterm:[Tigers]', '(((Tigers)))']
 1133       macros.each do |macro|
 1134         para = block_from_string("#{sentence}#{macro}")
 1135         output = para.sub_macros(para.source)
 1136         assert_equal sentence, output
 1137         #assert_equal 1, para.document.catalog[:indexterms].size
 1138         #assert_equal ['Tigers'], para.document.catalog[:indexterms].first
 1139       end
 1140     end
 1141 
 1142     test 'a single-line index term macro with primary and secondary terms should be registered as an index reference' do
 1143       sentence = "The tiger (Panthera tigris) is the largest cat species.\n"
 1144       macros = ['indexterm:[Big cats, Tigers]', '(((Big cats, Tigers)))']
 1145       macros.each do |macro|
 1146         para = block_from_string("#{sentence}#{macro}")
 1147         output = para.sub_macros(para.source)
 1148         assert_equal sentence, output
 1149         #assert_equal 1, para.document.catalog[:indexterms].size
 1150         #assert_equal ['Big cats', 'Tigers'], para.document.catalog[:indexterms].first
 1151       end
 1152     end
 1153 
 1154     test 'a single-line index term macro with primary, secondary and tertiary terms should be registered as an index reference' do
 1155       sentence = "The tiger (Panthera tigris) is the largest cat species.\n"
 1156       macros = ['indexterm:[Big cats,Tigers , Panthera tigris]', '(((Big cats,Tigers , Panthera tigris)))']
 1157       macros.each do |macro|
 1158         para = block_from_string("#{sentence}#{macro}")
 1159         output = para.sub_macros(para.source)
 1160         assert_equal sentence, output
 1161         #assert_equal 1, para.document.catalog[:indexterms].size
 1162         #assert_equal ['Big cats', 'Tigers', 'Panthera tigris'], para.document.catalog[:indexterms].first
 1163       end
 1164     end
 1165 
 1166     test 'a multi-line index term macro should be compacted and registered as an index reference' do
 1167       sentence = "The tiger (Panthera tigris) is the largest cat species.\n"
 1168       macros = ["indexterm:[Panthera\ntigris]", "(((Panthera\ntigris)))"]
 1169       macros.each do |macro|
 1170         para = block_from_string("#{sentence}#{macro}")
 1171         output = para.sub_macros(para.source)
 1172         assert_equal sentence, output
 1173         #assert_equal 1, para.document.catalog[:indexterms].size
 1174         #assert_equal ['Panthera tigris'], para.document.catalog[:indexterms].first
 1175       end
 1176     end
 1177 
 1178     test 'should escape concealed index term if second bracket is preceded by a backslash' do
 1179       input = %[National Institute of Science and Technology (#{BACKSLASH}((NIST)))]
 1180       doc = document_from_string input, standalone: false
 1181       output = doc.convert
 1182       assert_xpath '//p[text()="National Institute of Science and Technology (((NIST)))"]', output, 1
 1183       #assert doc.catalog[:indexterms].empty?
 1184     end
 1185 
 1186     test 'should only escape enclosing brackets if concealed index term is preceded by a backslash' do
 1187       input = %[National Institute of Science and Technology #{BACKSLASH}(((NIST)))]
 1188       doc = document_from_string input, standalone: false
 1189       output = doc.convert
 1190       assert_xpath '//p[text()="National Institute of Science and Technology (NIST)"]', output, 1
 1191       #term = doc.catalog[:indexterms].first
 1192       #assert_equal 1, term.size
 1193       #assert_equal 'NIST', term.first
 1194     end
 1195 
 1196     test 'should not split index terms on commas inside of quoted terms' do
 1197       inputs = []
 1198       inputs.push <<~'EOS'
 1199       Tigers are big, scary cats.
 1200       indexterm:[Tigers, "[Big\],
 1201       scary cats"]
 1202       EOS
 1203       inputs.push <<~'EOS'
 1204       Tigers are big, scary cats.
 1205       (((Tigers, "[Big],
 1206       scary cats")))
 1207       EOS
 1208 
 1209       inputs.each do |input|
 1210         para = block_from_string input
 1211         output = para.sub_macros(para.source)
 1212         assert_equal input.lines.first, output
 1213         #assert_equal 1, para.document.catalog[:indexterms].size
 1214         #terms = para.document.catalog[:indexterms].first
 1215         #assert_equal 2, terms.size
 1216         #assert_equal 'Tigers', terms.first
 1217         #assert_equal '[Big], scary cats', terms.last
 1218       end
 1219     end
 1220 
 1221     test 'normal substitutions are performed on an index term macro' do
 1222       sentence = "The tiger (Panthera tigris) is the largest cat species.\n"
 1223       macros = ['indexterm:[*Tigers*]', '(((*Tigers*)))']
 1224       macros.each do |macro|
 1225         para = block_from_string("#{sentence}#{macro}")
 1226         output = para.apply_subs(para.source)
 1227         assert_equal sentence, output
 1228         #assert_equal 1, para.document.catalog[:indexterms].size
 1229         #assert_equal ['<strong>Tigers</strong>'], para.document.catalog[:indexterms].first
 1230       end
 1231     end
 1232 
 1233     test 'registers multiple index term macros' do
 1234       sentence = "The tiger (Panthera tigris) is the largest cat species."
 1235       macros = "(((Tigers)))\n(((Animals,Cats)))"
 1236       para = block_from_string("#{sentence}\n#{macros}")
 1237       output = para.sub_macros(para.source)
 1238       assert_equal sentence, output.rstrip
 1239       #assert_equal 2, para.document.catalog[:indexterms].size
 1240       #assert_equal ['Tigers'], para.document.catalog[:indexterms][0]
 1241       #assert_equal ['Animals', 'Cats'], para.document.catalog[:indexterms][1]
 1242     end
 1243 
 1244     test 'an index term macro with round bracket syntax may contain round brackets in term' do
 1245       sentence = "The tiger (Panthera tigris) is the largest cat species.\n"
 1246       macro = '(((Tiger (Panthera tigris))))'
 1247       para = block_from_string("#{sentence}#{macro}")
 1248       output = para.sub_macros(para.source)
 1249       assert_equal sentence, output
 1250       #assert_equal 1, para.document.catalog[:indexterms].size
 1251       #assert_equal ['Tiger (Panthera tigris)'], para.document.catalog[:indexterms].first
 1252     end
 1253 
 1254     test 'visible shorthand index term macro should not consume trailing round bracket' do
 1255       input = '(text with ((index term)))'
 1256       expected = <<~'EOS'.chop
 1257       (text with <indexterm>
 1258       <primary>index term</primary>
 1259       </indexterm>index term)
 1260       EOS
 1261       #expected_term = ['index term']
 1262       para = block_from_string input, backend: :docbook
 1263       output = para.sub_macros para.source
 1264       assert_equal expected, output
 1265       #indexterms_table = para.document.catalog[:indexterms]
 1266       #assert_equal 1, indexterms_table.size
 1267       #assert_equal expected_term, indexterms_table[0]
 1268     end
 1269 
 1270     test 'visible shorthand index term macro should not consume leading round bracket' do
 1271       input = '(((index term)) for text)'
 1272       expected = <<~'EOS'.chop
 1273       (<indexterm>
 1274       <primary>index term</primary>
 1275       </indexterm>index term for text)
 1276       EOS
 1277       #expected_term = ['index term']
 1278       para = block_from_string input, backend: :docbook
 1279       output = para.sub_macros para.source
 1280       assert_equal expected, output
 1281       #indexterms_table = para.document.catalog[:indexterms]
 1282       #assert_equal 1, indexterms_table.size
 1283       #assert_equal expected_term, indexterms_table[0]
 1284     end
 1285 
 1286     test 'an index term macro with square bracket syntax may contain square brackets in term' do
 1287       sentence = "The tiger (Panthera tigris) is the largest cat species.\n"
 1288       macro = 'indexterm:[Tiger [Panthera tigris\\]]'
 1289       para = block_from_string("#{sentence}#{macro}")
 1290       output = para.sub_macros(para.source)
 1291       assert_equal sentence, output
 1292       #assert_equal 1, para.document.catalog[:indexterms].size
 1293       #assert_equal ['Tiger [Panthera tigris]'], para.document.catalog[:indexterms].first
 1294     end
 1295 
 1296     test 'a single-line index term 2 macro should be registered as an index reference and retain term inline' do
 1297       sentence = 'The tiger (Panthera tigris) is the largest cat species.'
 1298       macros = ['The indexterm2:[tiger] (Panthera tigris) is the largest cat species.', 'The ((tiger)) (Panthera tigris) is the largest cat species.']
 1299       macros.each do |macro|
 1300         para = block_from_string(macro)
 1301         output = para.sub_macros(para.source)
 1302         assert_equal sentence, output
 1303         #assert_equal 1, para.document.catalog[:indexterms].size
 1304         #assert_equal ['tiger'], para.document.catalog[:indexterms].first
 1305       end
 1306     end
 1307 
 1308     test 'a multi-line index term 2 macro should be compacted and registered as an index reference and retain term inline' do
 1309       sentence = 'The panthera tigris is the largest cat species.'
 1310       macros = ["The indexterm2:[ panthera\ntigris ] is the largest cat species.", "The (( panthera\ntigris )) is the largest cat species."]
 1311       macros.each do |macro|
 1312         para = block_from_string(macro)
 1313         output = para.sub_macros(para.source)
 1314         assert_equal sentence, output
 1315         #assert_equal 1, para.document.catalog[:indexterms].size
 1316         #assert_equal ['panthera tigris'], para.document.catalog[:indexterms].first
 1317       end
 1318     end
 1319 
 1320     test 'registers multiple index term 2 macros' do
 1321       sentence = "The ((tiger)) (Panthera tigris) is the largest ((cat)) species."
 1322       para = block_from_string(sentence)
 1323       output = para.sub_macros(para.source)
 1324       assert_equal 'The tiger (Panthera tigris) is the largest cat species.', output
 1325       #assert_equal 2, para.document.catalog[:indexterms].size
 1326       #assert_equal ['tiger'], para.document.catalog[:indexterms][0]
 1327       #assert_equal ['cat'], para.document.catalog[:indexterms][1]
 1328     end
 1329 
 1330     test 'should escape visible index term if preceded by a backslash' do
 1331       sentence = "The #{BACKSLASH}((tiger)) (Panthera tigris) is the largest #{BACKSLASH}((cat)) species."
 1332       para = block_from_string(sentence)
 1333       output = para.sub_macros(para.source)
 1334       assert_equal 'The ((tiger)) (Panthera tigris) is the largest ((cat)) species.', output
 1335       #assert para.document.catalog[:indexterms].empty?
 1336     end
 1337 
 1338     test 'normal substitutions are performed on an index term 2 macro' do
 1339       sentence = 'The ((*tiger*)) (Panthera tigris) is the largest cat species.'
 1340       para = block_from_string sentence
 1341       output = para.apply_subs(para.source)
 1342       assert_equal 'The <strong>tiger</strong> (Panthera tigris) is the largest cat species.', output
 1343       #assert_equal 1, para.document.catalog[:indexterms].size
 1344       #assert_equal ['<strong>tiger</strong>'], para.document.catalog[:indexterms].first
 1345     end
 1346 
 1347     test 'index term 2 macro with round bracket syntex should not interfer with index term macro with round bracket syntax' do
 1348       sentence = "The ((panthera tigris)) is the largest cat species.\n(((Big cats,Tigers)))"
 1349       para = block_from_string sentence
 1350       output = para.sub_macros(para.source)
 1351       assert_equal "The panthera tigris is the largest cat species.\n", output
 1352       #terms = para.document.catalog[:indexterms]
 1353       #assert_equal 2, terms.size
 1354       #assert_equal ['panthera tigris'], terms[0]
 1355       #assert_equal ['Big cats', 'Tigers'], terms[1]
 1356     end
 1357 
 1358     test 'should parse visible shorthand index term with see and seealso' do
 1359       sentence = '((Flash >> HTML 5)) has been supplanted by ((HTML 5 &> CSS 3 &> SVG)).'
 1360       output = convert_string_to_embedded sentence, backend: 'docbook'
 1361       indexterm_flash = <<~'EOS'.chop
 1362       <indexterm>
 1363       <primary>Flash</primary>
 1364       <see>HTML 5</see>
 1365       </indexterm>
 1366       EOS
 1367       indexterm_html5 = <<~'EOS'.chop
 1368       <indexterm>
 1369       <primary>HTML 5</primary>
 1370       <seealso>CSS 3</seealso>
 1371       <seealso>SVG</seealso>
 1372       </indexterm>
 1373       EOS
 1374       assert_includes output, indexterm_flash
 1375       assert_includes output, indexterm_html5
 1376     end
 1377 
 1378     test 'should parse concealed shorthand index term with see and seealso' do
 1379       sentence = 'Flash(((Flash >> HTML 5))) has been supplanted by HTML 5(((HTML 5 &> CSS 3 &> SVG))).'
 1380       output = convert_string_to_embedded sentence, backend: 'docbook'
 1381       indexterm_flash = <<~'EOS'.chop
 1382       <indexterm>
 1383       <primary>Flash</primary>
 1384       <see>HTML 5</see>
 1385       </indexterm>
 1386       EOS
 1387       indexterm_html5 = <<~'EOS'.chop
 1388       <indexterm>
 1389       <primary>HTML 5</primary>
 1390       <seealso>CSS 3</seealso>
 1391       <seealso>SVG</seealso>
 1392       </indexterm>
 1393       EOS
 1394       assert_includes output, indexterm_flash
 1395       assert_includes output, indexterm_html5
 1396     end
 1397 
 1398     test 'should parse visible index term macro with see and seealso' do
 1399       sentence = 'indexterm2:[Flash,see=HTML 5] has been supplanted by indexterm2:[HTML 5,see-also="CSS 3, SVG"].'
 1400       output = convert_string_to_embedded sentence, backend: 'docbook'
 1401       indexterm_flash = <<~'EOS'.chop
 1402       <indexterm>
 1403       <primary>Flash</primary>
 1404       <see>HTML 5</see>
 1405       </indexterm>
 1406       EOS
 1407       indexterm_html5 = <<~'EOS'.chop
 1408       <indexterm>
 1409       <primary>HTML 5</primary>
 1410       <seealso>CSS 3</seealso>
 1411       <seealso>SVG</seealso>
 1412       </indexterm>
 1413       EOS
 1414       assert_includes output, indexterm_flash
 1415       assert_includes output, indexterm_html5
 1416     end
 1417 
 1418     test 'should parse concealed index term macro with see and seealso' do
 1419       sentence = 'Flashindexterm:[Flash,see=HTML 5] has been supplanted by HTML 5indexterm:[HTML 5,see-also="CSS 3, SVG"].'
 1420       output = convert_string_to_embedded sentence, backend: 'docbook'
 1421       indexterm_flash = <<~'EOS'.chop
 1422       <indexterm>
 1423       <primary>Flash</primary>
 1424       <see>HTML 5</see>
 1425       </indexterm>
 1426       EOS
 1427       indexterm_html5 = <<~'EOS'.chop
 1428       <indexterm>
 1429       <primary>HTML 5</primary>
 1430       <seealso>CSS 3</seealso>
 1431       <seealso>SVG</seealso>
 1432       </indexterm>
 1433       EOS
 1434       assert_includes output, indexterm_flash
 1435       assert_includes output, indexterm_html5
 1436     end
 1437 
 1438     context 'Button macro' do
 1439       test 'btn macro' do
 1440         para = block_from_string('btn:[Save]', attributes: { 'experimental' => '' })
 1441         assert_equal %q{<b class="button">Save</b>}, para.sub_macros(para.source)
 1442       end
 1443 
 1444       test 'btn macro that spans multiple lines' do
 1445         para = block_from_string(%(btn:[Rebase and\nmerge]), attributes: { 'experimental' => '' })
 1446         assert_equal %q{<b class="button">Rebase and merge</b>}, para.sub_macros(para.source)
 1447       end
 1448 
 1449       test 'btn macro for docbook backend' do
 1450         para = block_from_string('btn:[Save]', backend: 'docbook', attributes: { 'experimental' => '' })
 1451         assert_equal %q{<guibutton>Save</guibutton>}, para.sub_macros(para.source)
 1452       end
 1453     end
 1454 
 1455     context 'Keyboard macro' do
 1456       test 'kbd macro with single key' do
 1457         para = block_from_string('kbd:[F3]', attributes: { 'experimental' => '' })
 1458         assert_equal %q{<kbd>F3</kbd>}, para.sub_macros(para.source)
 1459       end
 1460 
 1461       test 'kbd macro with single backslash key' do
 1462         para = block_from_string("kbd:[#{BACKSLASH} ]", attributes: { 'experimental' => '' })
 1463         assert_equal %q(<kbd>\</kbd>), para.sub_macros(para.source)
 1464       end
 1465 
 1466       test 'kbd macro with single key, docbook backend' do
 1467         para = block_from_string('kbd:[F3]', backend: 'docbook', attributes: { 'experimental' => '' })
 1468         assert_equal %q{<keycap>F3</keycap>}, para.sub_macros(para.source)
 1469       end
 1470 
 1471       test 'kbd macro with key combination' do
 1472         para = block_from_string('kbd:[Ctrl+Shift+T]', attributes: { 'experimental' => '' })
 1473         assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></span>}, para.sub_macros(para.source)
 1474       end
 1475 
 1476       test 'kbd macro with key combination that spans multiple lines' do
 1477         para = block_from_string(%(kbd:[Ctrl +\nT]), attributes: { 'experimental' => '' })
 1478         assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>T</kbd></span>}, para.sub_macros(para.source)
 1479       end
 1480 
 1481       test 'kbd macro with key combination, docbook backend' do
 1482         para = block_from_string('kbd:[Ctrl+Shift+T]', backend: 'docbook', attributes: { 'experimental' => '' })
 1483         assert_equal %q{<keycombo><keycap>Ctrl</keycap><keycap>Shift</keycap><keycap>T</keycap></keycombo>}, para.sub_macros(para.source)
 1484       end
 1485 
 1486       test 'kbd macro with key combination delimited by pluses with spaces' do
 1487         para = block_from_string('kbd:[Ctrl + Shift + T]', attributes: { 'experimental' => '' })
 1488         assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></span>}, para.sub_macros(para.source)
 1489       end
 1490 
 1491       test 'kbd macro with key combination delimited by commas' do
 1492         para = block_from_string('kbd:[Ctrl,Shift,T]', attributes: { 'experimental' => '' })
 1493         assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></span>}, para.sub_macros(para.source)
 1494       end
 1495 
 1496       test 'kbd macro with key combination delimited by commas with spaces' do
 1497         para = block_from_string('kbd:[Ctrl, Shift, T]', attributes: { 'experimental' => '' })
 1498         assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd></span>}, para.sub_macros(para.source)
 1499       end
 1500 
 1501       test 'kbd macro with key combination delimited by plus containing a comma key' do
 1502         para = block_from_string('kbd:[Ctrl+,]', attributes: { 'experimental' => '' })
 1503         assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>,</kbd></span>}, para.sub_macros(para.source)
 1504       end
 1505 
 1506       test 'kbd macro with key combination delimited by commas containing a plus key' do
 1507         para = block_from_string('kbd:[Ctrl, +, Shift]', attributes: { 'experimental' => '' })
 1508         assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>+</kbd>+<kbd>Shift</kbd></span>}, para.sub_macros(para.source)
 1509       end
 1510 
 1511       test 'kbd macro with key combination where last key matches plus delimiter' do
 1512         para = block_from_string('kbd:[Ctrl + +]', attributes: { 'experimental' => '' })
 1513         assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>+</kbd></span>}, para.sub_macros(para.source)
 1514       end
 1515 
 1516       test 'kbd macro with key combination where last key matches comma delimiter' do
 1517         para = block_from_string('kbd:[Ctrl, ,]', attributes: { 'experimental' => '' })
 1518         assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>,</kbd></span>}, para.sub_macros(para.source)
 1519       end
 1520 
 1521       test 'kbd macro with key combination containing escaped bracket' do
 1522         para = block_from_string('kbd:[Ctrl + \]]', attributes: { 'experimental' => '' })
 1523         assert_equal %q{<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>]</kbd></span>}, para.sub_macros(para.source)
 1524       end
 1525 
 1526       test 'kbd macro with key combination ending in backslash' do
 1527         para = block_from_string("kbd:[Ctrl + #{BACKSLASH} ]", attributes: { 'experimental' => '' })
 1528         assert_equal %q(<span class="keyseq"><kbd>Ctrl</kbd>+<kbd>\\</kbd></span>), para.sub_macros(para.source)
 1529       end
 1530 
 1531       test 'kbd macro looks for delimiter beyond first character' do
 1532         para = block_from_string('kbd:[,te]', attributes: { 'experimental' => '' })
 1533         assert_equal %q(<kbd>,te</kbd>), para.sub_macros(para.source)
 1534       end
 1535 
 1536       test 'kbd macro restores trailing delimiter as key value' do
 1537         para = block_from_string('kbd:[te,]', attributes: { 'experimental' => '' })
 1538         assert_equal %q(<kbd>te,</kbd>), para.sub_macros(para.source)
 1539       end
 1540     end
 1541 
 1542     context 'Menu macro' do
 1543       test 'should process menu using macro sytnax' do
 1544         para = block_from_string('menu:File[]', attributes: { 'experimental' => '' })
 1545         assert_equal %q{<b class="menuref">File</b>}, para.sub_macros(para.source)
 1546       end
 1547 
 1548       test 'should process menu for docbook backend' do
 1549         para = block_from_string('menu:File[]', backend: 'docbook', attributes: { 'experimental' => '' })
 1550         assert_equal %q{<guimenu>File</guimenu>}, para.sub_macros(para.source)
 1551       end
 1552 
 1553       test 'should process multiple menu macros in same line' do
 1554         para = block_from_string('menu:File[] and menu:Edit[]', attributes: { 'experimental' => '' })
 1555         assert_equal '<b class="menuref">File</b> and <b class="menuref">Edit</b>', para.sub_macros(para.source)
 1556       end
 1557 
 1558       test 'should process menu with menu item using macro syntax' do
 1559         para = block_from_string('menu:File[Save As&#8230;]', attributes: { 'experimental' => '' })
 1560         assert_equal %q{<span class="menuseq"><b class="menu">File</b>&#160;<b class="caret">&#8250;</b> <b class="menuitem">Save As&#8230;</b></span>}, para.sub_macros(para.source)
 1561       end
 1562 
 1563       test 'should process menu macro that spans multiple lines' do
 1564         input = %(menu:Preferences[Compile\non\nSave])
 1565         para = block_from_string input, attributes: { 'experimental' => '' }
 1566         assert_equal %(<span class="menuseq"><b class="menu">Preferences</b>&#160;<b class="caret">&#8250;</b> <b class="menuitem">Compile\non\nSave</b></span>), para.sub_macros(para.source)
 1567       end
 1568 
 1569       test 'should unescape escaped closing bracket in menu macro' do
 1570         input = 'menu:Preferences[Compile [on\\] Save]'
 1571         para = block_from_string input, attributes: { 'experimental' => '' }
 1572         assert_equal %q(<span class="menuseq"><b class="menu">Preferences</b>&#160;<b class="caret">&#8250;</b> <b class="menuitem">Compile [on] Save</b></span>), para.sub_macros(para.source)
 1573       end
 1574 
 1575       test 'should process menu with menu item using macro syntax when fonts icons are enabled' do
 1576         para = block_from_string('menu:Tools[More Tools &gt; Extensions]', attributes: { 'experimental' => '', 'icons' => 'font' })
 1577         assert_equal %q{<span class="menuseq"><b class="menu">Tools</b>&#160;<i class="fa fa-angle-right caret"></i> <b class="submenu">More Tools</b>&#160;<i class="fa fa-angle-right caret"></i> <b class="menuitem">Extensions</b></span>}, para.sub_macros(para.source)
 1578       end
 1579 
 1580       test 'should process menu with menu item for docbook backend' do
 1581         para = block_from_string('menu:File[Save As&#8230;]', backend: 'docbook', attributes: { 'experimental' => '' })
 1582         assert_equal %q{<menuchoice><guimenu>File</guimenu> <guimenuitem>Save As&#8230;</guimenuitem></menuchoice>}, para.sub_macros(para.source)
 1583       end
 1584 
 1585       test 'should process menu with menu item in submenu using macro syntax' do
 1586         para = block_from_string('menu:Tools[Project &gt; Build]', attributes: { 'experimental' => '' })
 1587         assert_equal %q{<span class="menuseq"><b class="menu">Tools</b>&#160;<b class="caret">&#8250;</b> <b class="submenu">Project</b>&#160;<b class="caret">&#8250;</b> <b class="menuitem">Build</b></span>}, para.sub_macros(para.source)
 1588       end
 1589 
 1590       test 'should process menu with menu item in submenu for docbook backend' do
 1591         para = block_from_string('menu:Tools[Project &gt; Build]', backend: 'docbook', attributes: { 'experimental' => '' })
 1592         assert_equal %q{<menuchoice><guimenu>Tools</guimenu> <guisubmenu>Project</guisubmenu> <guimenuitem>Build</guimenuitem></menuchoice>}, para.sub_macros(para.source)
 1593       end
 1594 
 1595       test 'should process menu with menu item in submenu using macro syntax and comma delimiter' do
 1596         para = block_from_string('menu:Tools[Project, Build]', attributes: { 'experimental' => '' })
 1597         assert_equal %q{<span class="menuseq"><b class="menu">Tools</b>&#160;<b class="caret">&#8250;</b> <b class="submenu">Project</b>&#160;<b class="caret">&#8250;</b> <b class="menuitem">Build</b></span>}, para.sub_macros(para.source)
 1598       end
 1599 
 1600       test 'should process menu with menu item using inline syntax' do
 1601         para = block_from_string('"File &gt; Save As&#8230;"', attributes: { 'experimental' => '' })
 1602         assert_equal %q{<span class="menuseq"><b class="menu">File</b>&#160;<b class="caret">&#8250;</b> <b class="menuitem">Save As&#8230;</b></span>}, para.sub_macros(para.source)
 1603       end
 1604 
 1605       test 'should process menu with menu item in submenu using inline syntax' do
 1606         para = block_from_string('"Tools &gt; Project &gt; Build"', attributes: { 'experimental' => '' })
 1607         assert_equal %q{<span class="menuseq"><b class="menu">Tools</b>&#160;<b class="caret">&#8250;</b> <b class="submenu">Project</b>&#160;<b class="caret">&#8250;</b> <b class="menuitem">Build</b></span>}, para.sub_macros(para.source)
 1608       end
 1609 
 1610       test 'inline menu syntax should not match closing quote of XML attribute' do
 1611         para = block_from_string('<span class="xmltag">&lt;node&gt;</span><span class="classname">r</span>', attributes: { 'experimental' => '' })
 1612         assert_equal %q{<span class="xmltag">&lt;node&gt;</span><span class="classname">r</span>}, para.sub_macros(para.source)
 1613       end
 1614 
 1615       test 'should process menu macro with items containing multibyte characters' do
 1616         para = block_from_string('menu:视图[放大, 重置]', attributes: { 'experimental' => '' })
 1617         assert_equal %q{<span class="menuseq"><b class="menu">视图</b>&#160;<b class="caret">&#8250;</b> <b class="submenu">放大</b>&#160;<b class="caret">&#8250;</b> <b class="menuitem">重置</b></span>}, para.sub_macros(para.source)
 1618       end
 1619 
 1620       test 'should process inline menu with items containing multibyte characters' do
 1621         para = block_from_string('"视图 &gt; 放大 &gt; 重置"', attributes: { 'experimental' => '' })
 1622         assert_equal %q{<span class="menuseq"><b class="menu">视图</b>&#160;<b class="caret">&#8250;</b> <b class="submenu">放大</b>&#160;<b class="caret">&#8250;</b> <b class="menuitem">重置</b></span>}, para.sub_macros(para.source)
 1623       end
 1624 
 1625       test 'should process a menu macro with a target that begins with a character reference' do
 1626         para = block_from_string('menu:&#8942;[More Tools, Extensions]', attributes: { 'experimental' => '' })
 1627         assert_equal %q{<span class="menuseq"><b class="menu">&#8942;</b>&#160;<b class="caret">&#8250;</b> <b class="submenu">More Tools</b>&#160;<b class="caret">&#8250;</b> <b class="menuitem">Extensions</b></span>}, para.sub_macros(para.source)
 1628       end
 1629 
 1630       test 'should not process a menu macro with a target that ends with a space' do
 1631         input = 'menu:foo [bar] menu:File[Save]'
 1632         para = block_from_string input, attributes: { 'experimental' => '' }
 1633         result = para.sub_macros para.source
 1634         assert_xpath '/span[@class="menuseq"]', result, 1
 1635         assert_xpath '//b[@class="menu"][text()="File"]', result, 1
 1636       end
 1637 
 1638       test 'should process an inline menu that begins with a character reference' do
 1639         para = block_from_string('"&#8942; &gt; More Tools &gt; Extensions"', attributes: { 'experimental' => '' })
 1640         assert_equal %q{<span class="menuseq"><b class="menu">&#8942;</b>&#160;<b class="caret">&#8250;</b> <b class="submenu">More Tools</b>&#160;<b class="caret">&#8250;</b> <b class="menuitem">Extensions</b></span>}, para.sub_macros(para.source)
 1641       end
 1642     end
 1643   end
 1644 
 1645   context 'Passthroughs' do
 1646     test 'collect inline triple plus passthroughs' do
 1647       para = block_from_string('+++<code>inline code</code>+++')
 1648       result = para.extract_passthroughs(para.source)
 1649       passthroughs = para.instance_variable_get :@passthroughs
 1650       assert_equal Asciidoctor::Substitutors::PASS_START + '0' + Asciidoctor::Substitutors::PASS_END, result
 1651       assert_equal 1, passthroughs.size
 1652       assert_equal '<code>inline code</code>', passthroughs[0][:text]
 1653       assert_empty passthroughs[0][:subs]
 1654     end
 1655 
 1656     test 'collect multi-line inline triple plus passthroughs' do
 1657       para = block_from_string("+++<code>inline\ncode</code>+++")
 1658       result = para.extract_passthroughs(para.source)
 1659       passthroughs = para.instance_variable_get :@passthroughs
 1660       assert_equal Asciidoctor::Substitutors::PASS_START + '0' + Asciidoctor::Substitutors::PASS_END, result
 1661       assert_equal 1, passthroughs.size
 1662       assert_equal "<code>inline\ncode</code>", passthroughs[0][:text]
 1663       assert_empty passthroughs[0][:subs]
 1664     end
 1665 
 1666     test 'collect inline double dollar passthroughs' do
 1667       para = block_from_string('$$<code>{code}</code>$$')
 1668       result = para.extract_passthroughs(para.source)
 1669       passthroughs = para.instance_variable_get :@passthroughs
 1670       assert_equal Asciidoctor::Substitutors::PASS_START + '0' + Asciidoctor::Substitutors::PASS_END, result
 1671       assert_equal 1, passthroughs.size
 1672       assert_equal '<code>{code}</code>', passthroughs[0][:text]
 1673       assert_equal [:specialcharacters], passthroughs[0][:subs]
 1674     end
 1675 
 1676     test 'collect inline double plus passthroughs' do
 1677       para = block_from_string('++<code>{code}</code>++')
 1678       result = para.extract_passthroughs(para.source)
 1679       passthroughs = para.instance_variable_get :@passthroughs
 1680       assert_equal Asciidoctor::Substitutors::PASS_START + '0' + Asciidoctor::Substitutors::PASS_END, result
 1681       assert_equal 1, passthroughs.size
 1682       assert_equal '<code>{code}</code>', passthroughs[0][:text]
 1683       assert_equal [:specialcharacters], passthroughs[0][:subs]
 1684     end
 1685 
 1686     test 'should not crash if role on passthrough is enclosed in quotes' do
 1687       %W(
 1688         ['role']#{BACKSLASH}++This++++++++++++
 1689         ['role']#{BACKSLASH}+++++++++This++++++++++++
 1690       ).each do |input|
 1691         para = block_from_string input
 1692         assert_includes para.content, %(<span class="'role'">)
 1693       end
 1694     end
 1695 
 1696     test 'should allow inline double plus passthrough to be escaped using backslash' do
 1697       para = block_from_string("you need to replace `int a = n#{BACKSLASH}++;` with `int a = ++n;`!")
 1698       result = para.apply_subs para.source
 1699       assert_equal 'you need to replace <code>int a = n++;</code> with <code>int a = ++n;</code>!', result
 1700     end
 1701 
 1702     test 'should allow inline double plus passthrough with attributes to be escaped using backslash' do
 1703       para = block_from_string("=[attrs]#{BACKSLASH}#{BACKSLASH}++text++")
 1704       result = para.apply_subs para.source
 1705       assert_equal '=[attrs]++text++', result
 1706     end
 1707 
 1708     test 'collect multi-line inline double dollar passthroughs' do
 1709       para = block_from_string("$$<code>\n{code}\n</code>$$")
 1710       result = para.extract_passthroughs(para.source)
 1711       passthroughs = para.instance_variable_get :@passthroughs
 1712       assert_equal Asciidoctor::Substitutors::PASS_START + '0' + Asciidoctor::Substitutors::PASS_END, result
 1713       assert_equal 1, passthroughs.size
 1714       assert_equal "<code>\n{code}\n</code>", passthroughs[0][:text]
 1715       assert_equal [:specialcharacters], passthroughs[0][:subs]
 1716     end
 1717 
 1718     test 'collect multi-line inline double plus passthroughs' do
 1719       para = block_from_string("++<code>\n{code}\n</code>++")
 1720       result = para.extract_passthroughs(para.source)
 1721       passthroughs = para.instance_variable_get :@passthroughs
 1722       assert_equal Asciidoctor::Substitutors::PASS_START + '0' + Asciidoctor::Substitutors::PASS_END, result
 1723       assert_equal 1, passthroughs.size
 1724       assert_equal "<code>\n{code}\n</code>", passthroughs[0][:text]
 1725       assert_equal [:specialcharacters], passthroughs[0][:subs]
 1726     end
 1727 
 1728     test 'collect passthroughs from inline pass macro' do
 1729       para = block_from_string(%Q{pass:specialcharacters,quotes[<code>['code'\\]</code>]})
 1730       result = para.extract_passthroughs(para.source)
 1731       passthroughs = para.instance_variable_get :@passthroughs
 1732       assert_equal Asciidoctor::Substitutors::PASS_START + '0' + Asciidoctor::Substitutors::PASS_END, result
 1733       assert_equal 1, passthroughs.size
 1734       assert_equal %q{<code>['code']</code>}, passthroughs[0][:text]
 1735       assert_equal [:specialcharacters, :quotes], passthroughs[0][:subs]
 1736     end
 1737 
 1738     test 'collect multi-line passthroughs from inline pass macro' do
 1739       para = block_from_string(%Q{pass:specialcharacters,quotes[<code>['more\ncode'\\]</code>]})
 1740       result = para.extract_passthroughs(para.source)
 1741       passthroughs = para.instance_variable_get :@passthroughs
 1742       assert_equal Asciidoctor::Substitutors::PASS_START + '0' + Asciidoctor::Substitutors::PASS_END, result
 1743       assert_equal 1, passthroughs.size
 1744       assert_equal %Q{<code>['more\ncode']</code>}, passthroughs[0][:text]
 1745       assert_equal [:specialcharacters, :quotes], passthroughs[0][:subs]
 1746     end
 1747 
 1748     test 'should find and replace placeholder duplicated by substitution' do
 1749       input = %q(+first passthrough+ followed by link:$$http://example.com/__u_no_format_me__$$[] with passthrough)
 1750       result = convert_inline_string input
 1751       assert_equal 'first passthrough followed by <a href="http://example.com/__u_no_format_me__" class="bare">http://example.com/__u_no_format_me__</a> with passthrough', result
 1752     end
 1753 
 1754     test 'resolves sub shorthands on inline pass macro' do
 1755       para = block_from_string 'pass:q,a[*<{backend}>*]'
 1756       result = para.extract_passthroughs para.source
 1757       passthroughs = para.instance_variable_get :@passthroughs
 1758       assert_equal 1, passthroughs.size
 1759       assert_equal [:quotes, :attributes], passthroughs[0][:subs]
 1760       result = para.restore_passthroughs result
 1761       assert_equal '<strong><html5></strong>', result
 1762     end
 1763 
 1764     test 'inline pass macro supports incremental subs' do
 1765       para = block_from_string 'pass:n,-a[<{backend}>]'
 1766       result = para.extract_passthroughs para.source
 1767       passthroughs = para.instance_variable_get :@passthroughs
 1768       assert_equal 1, passthroughs.size
 1769       result = para.restore_passthroughs result
 1770       assert_equal '&lt;{backend}&gt;', result
 1771     end
 1772 
 1773     test 'should not recognize pass macro with invalid subsitution list' do
 1774       [',', '42', 'a,'].each do |subs|
 1775         para = block_from_string %(pass:#{subs}[foobar])
 1776         result = para.extract_passthroughs para.source
 1777         assert_equal %(pass:#{subs}[foobar]), result
 1778       end
 1779     end
 1780 
 1781     test 'should allow content of inline pass macro to be empty' do
 1782       para = block_from_string 'pass:[]'
 1783       result = para.extract_passthroughs para.source
 1784       passthroughs = para.instance_variable_get :@passthroughs
 1785       assert_equal 1, passthroughs.size
 1786       assert_equal '', para.restore_passthroughs(result)
 1787     end
 1788 
 1789     # NOTE placeholder is surrounded by text to prevent reader from stripping trailing boundary char (unique to test scenario)
 1790     test 'restore inline passthroughs without subs' do
 1791       para = block_from_string("some #{Asciidoctor::Substitutors::PASS_START}" + '0' + "#{Asciidoctor::Substitutors::PASS_END} to study")
 1792       para.extract_passthroughs ''
 1793       passthroughs = para.instance_variable_get :@passthroughs
 1794       passthroughs[0] = { text: '<code>inline code</code>', subs: [] }
 1795       result = para.restore_passthroughs(para.source)
 1796       assert_equal "some <code>inline code</code> to study", result
 1797     end
 1798 
 1799     # NOTE placeholder is surrounded by text to prevent reader from stripping trailing boundary char (unique to test scenario)
 1800     test 'restore inline passthroughs with subs' do
 1801       para = block_from_string("some #{Asciidoctor::Substitutors::PASS_START}" + '0' + "#{Asciidoctor::Substitutors::PASS_END} to study in the #{Asciidoctor::Substitutors::PASS_START}" + '1' + "#{Asciidoctor::Substitutors::PASS_END} programming language")
 1802       para.extract_passthroughs ''
 1803       passthroughs = para.instance_variable_get :@passthroughs
 1804       passthroughs[0] = { text: '<code>{code}</code>', subs: [:specialcharacters] }
 1805       passthroughs[1] = { text: '{language}', subs: [:specialcharacters] }
 1806       result = para.restore_passthroughs(para.source)
 1807       assert_equal 'some &lt;code&gt;{code}&lt;/code&gt; to study in the {language} programming language', result
 1808     end
 1809 
 1810     test 'should restore nested passthroughs' do
 1811       result = convert_inline_string %q(+Sometimes you feel pass:q[`mono`].+ Sometimes you +$$don't$$+.)
 1812       assert_equal %q(Sometimes you feel <code>mono</code>. Sometimes you don't.), result
 1813     end
 1814 
 1815     test 'should not fail to restore remaining passthroughs after processing inline passthrough with macro substitution' do
 1816       input = 'pass:m[.] pass:[.]'
 1817       assert_equal '. .', (convert_inline_string input)
 1818     end
 1819 
 1820     test 'should honor role on double plus passthrough' do
 1821       result = convert_inline_string 'Print the version using [var]++{asciidoctor-version}++.'
 1822       assert_equal 'Print the version using <span class="var">{asciidoctor-version}</span>.', result
 1823     end
 1824 
 1825     test 'complex inline passthrough macro' do
 1826       text_to_escape = %q{[(] <'basic form'> <'logical operator'> <'basic form'> [)]}
 1827       para = block_from_string %($$#{text_to_escape}$$)
 1828       para.extract_passthroughs(para.source)
 1829       passthroughs = para.instance_variable_get :@passthroughs
 1830       assert_equal 1, passthroughs.size
 1831       assert_equal text_to_escape, passthroughs[0][:text]
 1832 
 1833       text_to_escape_escaped = %q{[(\] <'basic form'> <'logical operator'> <'basic form'> [)\]}
 1834       para = block_from_string %(pass:specialcharacters[#{text_to_escape_escaped}])
 1835       para.extract_passthroughs(para.source)
 1836       passthroughs = para.instance_variable_get :@passthroughs
 1837       assert_equal 1, passthroughs.size
 1838       assert_equal text_to_escape, passthroughs[0][:text]
 1839     end
 1840 
 1841     test 'inline pass macro with a composite sub' do
 1842       para = block_from_string %(pass:verbatim[<{backend}>])
 1843       assert_equal '&lt;{backend}&gt;', para.content
 1844     end
 1845 
 1846     context 'Math macros' do
 1847       test 'should passthrough text in asciimath macro and surround with AsciiMath delimiters' do
 1848         input = 'asciimath:[x/x={(1,if x!=0),(text{undefined},if x=0):}]'
 1849         para = block_from_string input
 1850         assert_equal '\$x/x={(1,if x!=0),(text{undefined},if x=0):}\$', para.content
 1851       end
 1852 
 1853       test 'should not recognize asciimath macro with no content' do
 1854         input = 'asciimath:[]'
 1855         para = block_from_string input
 1856         assert_equal 'asciimath:[]', para.content
 1857       end
 1858 
 1859       test 'should perform specialcharacters subs on asciimath macro content in html backend by default' do
 1860         input = 'asciimath:[a < b]'
 1861         para = block_from_string input
 1862         assert_equal '\$a &lt; b\$', para.content
 1863       end
 1864 
 1865       test 'should convert contents of asciimath macro to MathML in DocBook output if asciimath gem is available' do
 1866         asciimath_available = !(Asciidoctor::Helpers.require_library 'asciimath', true, :ignore).nil?
 1867         input = 'asciimath:[a < b]'
 1868         expected = '<inlineequation><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML"><mml:mi>a</mml:mi><mml:mo>&lt;</mml:mo><mml:mi>b</mml:mi></mml:math></inlineequation>'
 1869         using_memory_logger do |logger|
 1870           para = block_from_string input, backend: :docbook
 1871           actual = para.content
 1872           if asciimath_available
 1873             assert_equal expected, actual
 1874             assert_equal :loaded, para.document.converter.instance_variable_get(:@asciimath_status)
 1875           else
 1876             assert_message logger, :WARN, 'optional gem \'asciimath\' is not available. Functionality disabled.'
 1877             assert_equal :unavailable, para.document.converter.instance_variable_get(:@asciimath_status)
 1878           end
 1879         end
 1880       end
 1881 
 1882       test 'should not perform specialcharacters subs on asciimath macro content in Docbook output if asciimath gem not available' do
 1883         asciimath_available = !(Asciidoctor::Helpers.require_library 'asciimath', true, :ignore).nil?
 1884         input = 'asciimath:[a < b]'
 1885         para = block_from_string input, backend: :docbook
 1886         para.document.converter.instance_variable_set :@asciimath_status, :unavailable
 1887         if asciimath_available
 1888           old_asciimath = ::AsciiMath
 1889           Object.send :remove_const, 'AsciiMath'
 1890         end
 1891         assert_equal '<inlineequation><mathphrase><![CDATA[a < b]]></mathphrase></inlineequation>', para.content
 1892         ::AsciiMath = old_asciimath if asciimath_available
 1893       end
 1894 
 1895       test 'should honor explicit subslist on asciimath macro' do
 1896         input = 'asciimath:attributes[{expr}]'
 1897         para = block_from_string input, attributes: { 'expr' => 'x != 0' }
 1898         assert_equal '\$x != 0\$', para.content
 1899       end
 1900 
 1901       test 'should passthrough text in latexmath macro and surround with LaTeX math delimiters' do
 1902         input = 'latexmath:[C = \alpha + \beta Y^{\gamma} + \epsilon]'
 1903         para = block_from_string input
 1904         assert_equal '\(C = \alpha + \beta Y^{\gamma} + \epsilon\)', para.content
 1905       end
 1906 
 1907       test 'should strip legacy LaTeX math delimiters around latexmath content if present' do
 1908         input = 'latexmath:[$C = \alpha + \beta Y^{\gamma} + \epsilon$]'
 1909         para = block_from_string input
 1910         assert_equal '\(C = \alpha + \beta Y^{\gamma} + \epsilon\)', para.content
 1911       end
 1912 
 1913       test 'should not recognize latexmath macro with no content' do
 1914         input = 'latexmath:[]'
 1915         para = block_from_string input
 1916         assert_equal 'latexmath:[]', para.content
 1917       end
 1918 
 1919       test 'should unescape escaped square bracket in equation' do
 1920         input = 'latexmath:[\sqrt[3\]{x}]'
 1921         para = block_from_string input
 1922         assert_equal '\(\sqrt[3]{x}\)', para.content
 1923       end
 1924 
 1925       test 'should perform specialcharacters subs on latexmath macro in html backend by default' do
 1926         input = 'latexmath:[a < b]'
 1927         para = block_from_string input
 1928         assert_equal '\(a &lt; b\)', para.content
 1929       end
 1930 
 1931       test 'should not perform specialcharacters subs on latexmath macro content in docbook backend by default' do
 1932         input = 'latexmath:[a < b]'
 1933         para = block_from_string input, backend: :docbook
 1934         assert_equal '<inlineequation><alt><![CDATA[a < b]]></alt><mathphrase><![CDATA[a < b]]></mathphrase></inlineequation>', para.content
 1935       end
 1936 
 1937       test 'should honor explicit subslist on latexmath macro' do
 1938         input = 'latexmath:attributes[{expr}]'
 1939         para = block_from_string input, attributes: { 'expr' => '\sqrt{4} = 2' }
 1940         assert_equal '\(\sqrt{4} = 2\)', para.content
 1941       end
 1942 
 1943       test 'should passthrough math macro inside another passthrough' do
 1944         input = 'the text `asciimath:[x = y]` should be passed through as +literal+ text'
 1945         para = block_from_string input, attributes: { 'compat-mode' => '' }
 1946         assert_equal 'the text <code>asciimath:[x = y]</code> should be passed through as <code>literal</code> text', para.content
 1947 
 1948         input = 'the text [x-]`asciimath:[x = y]` should be passed through as `literal` text'
 1949         para = block_from_string input
 1950         assert_equal 'the text <code>asciimath:[x = y]</code> should be passed through as <code>literal</code> text', para.content
 1951 
 1952         input = 'the text `+asciimath:[x = y]+` should be passed through as `literal` text'
 1953         para = block_from_string input
 1954         assert_equal 'the text <code>asciimath:[x = y]</code> should be passed through as <code>literal</code> text', para.content
 1955       end
 1956 
 1957       test 'should support attrlist on a literal monospace phrase' do
 1958         input = '[.baz]`+foo--bar+`'
 1959         para = block_from_string input
 1960         assert_equal '<code class="baz">foo--bar</code>', para.content
 1961       end
 1962 
 1963       test 'should not process an escaped passthrough macro inside a monospaced phrase' do
 1964         input = 'use the `\pass:c[]` macro'
 1965         para = block_from_string input
 1966         assert_equal 'use the <code>pass:c[]</code> macro', para.content
 1967       end
 1968 
 1969       test 'should not process an escaped passthrough macro inside a monospaced phrase with attributes' do
 1970         input = 'use the [syntax]`\pass:c[]` macro'
 1971         para = block_from_string input
 1972         assert_equal 'use the <code class="syntax">pass:c[]</code> macro', para.content
 1973       end
 1974 
 1975       test 'should honor an escaped single plus passthrough inside a monospaced phrase' do
 1976         input = 'use `\+{author}+` to show an attribute reference'
 1977         para = block_from_string input
 1978         assert_equal 'use <code>+{author}+</code> to show an attribute reference', para.content
 1979       end
 1980 
 1981       test 'should not recognize stem macro with no content' do
 1982         input = 'stem:[]'
 1983         para = block_from_string input
 1984         assert_equal input, para.content
 1985       end
 1986 
 1987       test 'should passthrough text in stem macro and surround with AsciiMath delimiters if stem attribute is asciimath, empty, or not set' do
 1988         [
 1989           {},
 1990           { 'stem' => '' },
 1991           { 'stem' => 'asciimath' },
 1992           { 'stem' => 'bogus' },
 1993         ].each do |attributes|
 1994           input = 'stem:[x/x={(1,if x!=0),(text{undefined},if x=0):}]'
 1995           para = block_from_string input, attributes: attributes
 1996           assert_equal '\$x/x={(1,if x!=0),(text{undefined},if x=0):}\$', para.content
 1997         end
 1998       end
 1999 
 2000       test 'should passthrough text in stem macro and surround with LaTeX math delimiters if stem attribute is latexmath, latex, or tex' do
 2001         [
 2002           { 'stem' => 'latexmath' },
 2003           { 'stem' => 'latex' },
 2004           { 'stem' => 'tex' },
 2005         ].each do |attributes|
 2006           input = 'stem:[C = \alpha + \beta Y^{\gamma} + \epsilon]'
 2007           para = block_from_string input, attributes: attributes
 2008           assert_equal '\(C = \alpha + \beta Y^{\gamma} + \epsilon\)', para.content
 2009         end
 2010       end
 2011 
 2012       test 'should apply substitutions specified on stem macro' do
 2013         ['stem:c,a[sqrt(x) <=> {solve-for-x}]', 'stem:n,-r[sqrt(x) <=> {solve-for-x}]'].each do |input|
 2014           para = block_from_string input, attributes: { 'stem' => 'asciimath', 'solve-for-x' => '13' }
 2015           assert_equal '\$sqrt(x) &lt;=&gt; 13\$', para.content
 2016         end
 2017       end
 2018 
 2019       test 'should not recognize stem macro with invalid substitution list' do
 2020         [',', '42', 'a,'].each do |subs|
 2021           input = %(stem:#{subs}[x^2])
 2022           para = block_from_string input, attributes: { 'stem' => 'asciimath' }
 2023           assert_equal %(stem:#{subs}[x^2]), para.content
 2024         end
 2025       end
 2026     end
 2027   end
 2028 
 2029   context 'Replacements' do
 2030     test 'unescapes XML entities' do
 2031       para = block_from_string '< &quot; &there4; &#34; &#x22; >'
 2032       assert_equal '&lt; &quot; &there4; &#34; &#x22; &gt;', para.apply_subs(para.source)
 2033     end
 2034 
 2035     test 'replaces arrows' do
 2036       para = block_from_string '<- -> <= => \<- \-> \<= \=>'
 2037       assert_equal '&#8592; &#8594; &#8656; &#8658; &lt;- -&gt; &lt;= =&gt;', para.apply_subs(para.source)
 2038     end
 2039 
 2040     test 'replaces dashes' do
 2041       input = <<~'EOS'
 2042       -- foo foo--bar foo\--bar foo -- bar foo \-- bar
 2043       stuff in between
 2044       -- foo
 2045       stuff in between
 2046       foo --
 2047       stuff in between
 2048       foo --
 2049       EOS
 2050       expected = <<~'EOS'.chop
 2051       &#8201;&#8212;&#8201;foo foo&#8212;&#8203;bar foo--bar foo&#8201;&#8212;&#8201;bar foo -- bar
 2052       stuff in between&#8201;&#8212;&#8201;foo
 2053       stuff in between
 2054       foo&#8201;&#8212;&#8201;stuff in between
 2055       foo&#8201;&#8212;&#8201;
 2056       EOS
 2057       para = block_from_string input
 2058       assert_equal expected, para.sub_replacements(para.source)
 2059     end
 2060 
 2061     test 'replaces dashes between multibyte word characters' do
 2062       para = block_from_string %(富--巴)
 2063       expected = '富&#8212;&#8203;巴'
 2064       assert_equal expected, para.sub_replacements(para.source)
 2065     end
 2066 
 2067     test 'replaces marks' do
 2068       para = block_from_string '(C) (R) (TM) \(C) \(R) \(TM)'
 2069       assert_equal '&#169; &#174; &#8482; (C) (R) (TM)', para.sub_replacements(para.source)
 2070     end
 2071 
 2072     test 'preserves entity references' do
 2073       input = '&amp; &#169; &#10004; &#128512; &#x2022; &#x1f600;'
 2074       result = convert_inline_string input
 2075       assert_equal input, result
 2076     end
 2077 
 2078     test 'only preserves named entities with two or more letters' do
 2079       input = '&amp; &a; &gt;'
 2080       result = convert_inline_string input
 2081       assert_equal '&amp; &amp;a; &gt;', result
 2082     end
 2083 
 2084     test 'replaces punctuation' do
 2085       para = block_from_string %(John's Hideout is the Whites`' place... foo\\'bar)
 2086       assert_equal "John&#8217;s Hideout is the Whites&#8217; place&#8230;&#8203; foo'bar", para.sub_replacements(para.source)
 2087     end
 2088 
 2089     test 'should replace right single quote marks' do
 2090       given = [
 2091         %(`'Twas the night),
 2092         %(a `'57 Chevy!),
 2093         %(the whites`' place),
 2094         %(the whites`'.),
 2095         %(the whites`'--where the wild things are),
 2096         %(the whites`'\nhave),
 2097         %(It's Mary`'s little lamb.),
 2098         %(consecutive single quotes '' are not modified),
 2099         %(he is 6' tall),
 2100         %(\\`')
 2101       ]
 2102       expected = [
 2103         %(&#8217;Twas the night),
 2104         %(a &#8217;57 Chevy!),
 2105         %(the whites&#8217; place),
 2106         %(the whites&#8217;.),
 2107         %(the whites&#8217;--where the wild things are),
 2108         %(the whites&#8217;\nhave),
 2109         %(It&#8217;s Mary&#8217;s little lamb.),
 2110         %(consecutive single quotes '' are not modified),
 2111         %(he is 6' tall),
 2112         %(`')
 2113       ]
 2114       given.size.times do |i|
 2115         para = block_from_string given[i]
 2116         assert_equal expected[i], para.sub_replacements(para.source)
 2117       end
 2118     end
 2119   end
 2120 
 2121   context 'Post replacements' do
 2122     test 'line break inserted after line with line break character' do
 2123       para = block_from_string("First line +\nSecond line")
 2124       result = para.apply_subs para.lines, (para.expand_subs :post_replacements)
 2125       assert_equal 'First line<br>', result.first
 2126     end
 2127 
 2128     test 'line break inserted after line wrap with hardbreaks enabled' do
 2129       para = block_from_string("First line\nSecond line", attributes: { 'hardbreaks' => '' })
 2130       result = para.apply_subs para.lines, (para.expand_subs :post_replacements)
 2131       assert_equal 'First line<br>', result.first
 2132     end
 2133 
 2134     test 'line break character stripped from end of line with hardbreaks enabled' do
 2135       para = block_from_string("First line +\nSecond line", attributes: { 'hardbreaks' => '' })
 2136       result = para.apply_subs para.lines, (para.expand_subs :post_replacements)
 2137       assert_equal 'First line<br>', result.first
 2138     end
 2139 
 2140     test 'line break not inserted for single line with hardbreaks enabled' do
 2141       para = block_from_string('First line', attributes: { 'hardbreaks' => '' })
 2142       result = para.apply_subs para.lines, (para.expand_subs :post_replacements)
 2143       assert_equal 'First line', result.first
 2144     end
 2145   end
 2146 
 2147   context 'Resolve subs' do
 2148     test 'should resolve subs for block' do
 2149       doc = empty_document parse: true
 2150       block = Asciidoctor::Block.new doc, :paragraph
 2151       block.attributes['subs'] = 'quotes,normal'
 2152       block.commit_subs
 2153       assert_equal [:quotes, :specialcharacters, :attributes, :replacements, :macros, :post_replacements], block.subs
 2154     end
 2155 
 2156     test 'should resolve specialcharacters sub as highlight for source block when source highlighter is coderay' do
 2157       doc = empty_document attributes: { 'source-highlighter' => 'coderay' }, parse: true
 2158       block = Asciidoctor::Block.new doc, :listing, content_model: :verbatim
 2159       block.style = 'source'
 2160       block.attributes['subs'] = 'specialcharacters'
 2161       block.attributes['language'] = 'ruby'
 2162       block.commit_subs
 2163       assert_equal [:highlight], block.subs
 2164     end
 2165 
 2166     test 'should resolve specialcharacters sub as highlight for source block when source highlighter is pygments' do
 2167       doc = empty_document attributes: { 'source-highlighter' => 'pygments' }, parse: true
 2168       block = Asciidoctor::Block.new doc, :listing, content_model: :verbatim
 2169       block.style = 'source'
 2170       block.attributes['subs'] = 'specialcharacters'
 2171       block.attributes['language'] = 'ruby'
 2172       block.commit_subs
 2173       assert_equal [:highlight], block.subs
 2174     end if ENV['PYGMENTS']
 2175 
 2176     test 'should not replace specialcharacters sub with highlight for source block when source highlighter is not set' do
 2177       doc = empty_document parse: true
 2178       block = Asciidoctor::Block.new doc, :listing, content_model: :verbatim
 2179       block.style = 'source'
 2180       block.attributes['subs'] = 'specialcharacters'
 2181       block.attributes['language'] = 'ruby'
 2182       block.commit_subs
 2183       assert_equal [:specialcharacters], block.subs
 2184     end
 2185 
 2186     test 'should not use subs if subs option passed to block constructor is nil' do
 2187       doc = empty_document parse: true
 2188       block = Asciidoctor::Block.new doc, :paragraph, source: '*bold* _italic_', subs: nil, attributes: { 'subs' => 'quotes' }
 2189       assert_empty block.subs
 2190       block.commit_subs
 2191       assert_empty block.subs
 2192     end
 2193 
 2194     test 'should not use subs if subs option passed to block constructor is empty array' do
 2195       doc = empty_document parse: true
 2196       block = Asciidoctor::Block.new doc, :paragraph, source: '*bold* _italic_', subs: [], attributes: { 'subs' => 'quotes' }
 2197       assert_empty block.subs
 2198       block.commit_subs
 2199       assert_empty block.subs
 2200     end
 2201 
 2202     test 'should use subs from subs option passed to block constructor' do
 2203       doc = empty_document parse: true
 2204       block = Asciidoctor::Block.new doc, :paragraph, source: '*bold* _italic_', subs: [:specialcharacters], attributes: { 'subs' => 'quotes' }
 2205       assert_equal [:specialcharacters], block.subs
 2206       block.commit_subs
 2207       assert_equal [:specialcharacters], block.subs
 2208     end
 2209 
 2210     test 'should use subs from subs attribute if subs option is not passed to block constructor' do
 2211       doc = empty_document parse: true
 2212       block = Asciidoctor::Block.new doc, :paragraph, source: '*bold* _italic_', attributes: { 'subs' => 'quotes' }
 2213       assert_empty block.subs
 2214       # in this case, we have to call commit_subs to resolve the subs
 2215       block.commit_subs
 2216       assert_equal [:quotes], block.subs
 2217     end
 2218 
 2219     test 'should use subs from subs attribute if subs option passed to block constructor is :default' do
 2220       doc = empty_document parse: true
 2221       block = Asciidoctor::Block.new doc, :paragraph, source: '*bold* _italic_', subs: :default, attributes: { 'subs' => 'quotes' }
 2222       assert_equal [:quotes], block.subs
 2223       block.commit_subs
 2224       assert_equal [:quotes], block.subs
 2225     end
 2226 
 2227     test 'should use built-in subs if subs option passed to block constructor is :default and subs attribute is absent' do
 2228       doc = empty_document parse: true
 2229       block = Asciidoctor::Block.new doc, :paragraph, source: '*bold* _italic_', subs: :default
 2230       assert_equal [:specialcharacters, :quotes, :attributes, :replacements, :macros, :post_replacements], block.subs
 2231       block.commit_subs
 2232       assert_equal [:specialcharacters, :quotes, :attributes, :replacements, :macros, :post_replacements], block.subs
 2233     end
 2234   end
 2235 end