"Fossies" - the Fresh Open Source Software Archive

Member "asciidoctor-2.0.10/test/attributes_test.rb" (1 Jun 2019, 52402 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.

    1 # frozen_string_literal: true
    2 require_relative 'test_helper'
    3 
    4 context 'Attributes' do
    5   default_logger = Asciidoctor::LoggerManager.logger
    6 
    7   setup do
    8     Asciidoctor::LoggerManager.logger = (@logger = Asciidoctor::MemoryLogger.new)
    9   end
   10 
   11   teardown do
   12     Asciidoctor::LoggerManager.logger = default_logger
   13   end
   14 
   15   context 'Assignment' do
   16     test 'creates an attribute' do
   17       doc = document_from_string(':frog: Tanglefoot')
   18       assert_equal 'Tanglefoot', doc.attributes['frog']
   19     end
   20 
   21     test 'requires a space after colon following attribute name' do
   22       doc = document_from_string 'foo:bar'
   23       assert_nil doc.attributes['foo']
   24     end
   25 
   26     # NOTE AsciiDoc Python recognizes this entry
   27     test 'does not recognize attribute entry if name contains colon' do
   28       input = ':foo:bar: baz'
   29       doc = document_from_string input
   30       refute doc.attr?('foo:bar')
   31       assert_equal 1, doc.blocks.size
   32       assert_equal :paragraph, doc.blocks[0].context
   33     end
   34 
   35     # NOTE AsciiDoc Python recognizes this entry
   36     test 'does not recognize attribute entry if name ends with colon' do
   37       input = ':foo:: bar'
   38       doc = document_from_string input
   39       refute doc.attr?('foo:')
   40       assert_equal 1, doc.blocks.size
   41       assert_equal :dlist, doc.blocks[0].context
   42     end
   43 
   44     # NOTE AsciiDoc Python does not recognize this entry
   45     test 'allows any word character defined by Unicode in an attribute name' do
   46       [['café', 'a coffee shop'], ['سمن', %(سازمان مردمنهاد)]].each do |(name, value)|
   47         str = <<~EOS
   48         :#{name}: #{value}
   49 
   50         {#{name}}
   51         EOS
   52         result = convert_string_to_embedded str
   53         assert_includes result, %(<p>#{value}</p>)
   54       end
   55     end
   56 
   57     test 'creates an attribute by fusing a legacy multi-line value' do
   58       str = <<~'EOS'
   59       :description: This is the first      +
   60                     Ruby implementation of +
   61                     AsciiDoc.
   62       EOS
   63       doc = document_from_string(str)
   64       assert_equal 'This is the first Ruby implementation of AsciiDoc.', doc.attributes['description']
   65     end
   66 
   67     test 'creates an attribute by fusing a multi-line value' do
   68       str = <<~'EOS'
   69       :description: This is the first \
   70                     Ruby implementation of \
   71                     AsciiDoc.
   72       EOS
   73       doc = document_from_string(str)
   74       assert_equal 'This is the first Ruby implementation of AsciiDoc.', doc.attributes['description']
   75     end
   76 
   77     test 'honors line break characters in multi-line values' do
   78       str = <<~'EOS'
   79       :signature: Linus Torvalds + \
   80       Linux Hacker + \
   81       linus.torvalds@example.com
   82       EOS
   83       doc = document_from_string(str)
   84       assert_equal %(Linus Torvalds +\nLinux Hacker +\nlinus.torvalds@example.com), doc.attributes['signature']
   85     end
   86 
   87     test 'should allow pass macro to surround a multi-line value that contains line breaks' do
   88       str = <<~'EOS'
   89       :signature: pass:a[{author} + \
   90       {title} + \
   91       {email}]
   92       EOS
   93       doc = document_from_string str, attributes: { 'author' => 'Linus Torvalds', 'title' => 'Linux Hacker', 'email' => 'linus.torvalds@example.com' }
   94       assert_equal %(Linus Torvalds +\nLinux Hacker +\nlinus.torvalds@example.com), (doc.attr 'signature')
   95     end
   96 
   97     test 'should delete an attribute that ends with !' do
   98       doc = document_from_string(":frog: Tanglefoot\n:frog!:")
   99       assert_nil doc.attributes['frog']
  100     end
  101 
  102     test 'should delete an attribute that ends with ! set via API' do
  103       doc = document_from_string(":frog: Tanglefoot", attributes: { 'frog!' => '' })
  104       assert_nil doc.attributes['frog']
  105     end
  106 
  107     test 'should delete an attribute that begins with !' do
  108       doc = document_from_string(":frog: Tanglefoot\n:!frog:")
  109       assert_nil doc.attributes['frog']
  110     end
  111 
  112     test 'should delete an attribute that begins with ! set via API' do
  113       doc = document_from_string(":frog: Tanglefoot", attributes: { '!frog' => '' })
  114       assert_nil doc.attributes['frog']
  115     end
  116 
  117     test 'should delete an attribute set via API to nil value' do
  118       doc = document_from_string(":frog: Tanglefoot", attributes: { 'frog' => nil })
  119       assert_nil doc.attributes['frog']
  120     end
  121 
  122     test "doesn't choke when deleting a non-existing attribute" do
  123       doc = document_from_string(':frog!:')
  124       assert_nil doc.attributes['frog']
  125     end
  126 
  127     test "replaces special characters in attribute value" do
  128       doc = document_from_string(":xml-busters: <>&")
  129       assert_equal '&lt;&gt;&amp;', doc.attributes['xml-busters']
  130     end
  131 
  132     test "performs attribute substitution on attribute value" do
  133       doc = document_from_string(":version: 1.0\n:release: Asciidoctor {version}")
  134       assert_equal 'Asciidoctor 1.0', doc.attributes['release']
  135     end
  136 
  137     test 'assigns attribute to empty string if substitution fails to resolve attribute' do
  138       input = ':release: Asciidoctor {version}'
  139       document_from_string input, attributes: { 'attribute-missing' => 'drop-line' }
  140       assert_message @logger, :INFO, 'dropping line containing reference to missing attribute: version'
  141     end
  142 
  143     test 'assigns multi-line attribute to empty string if substitution fails to resolve attribute' do
  144       input = <<~'EOS'
  145       :release: Asciidoctor +
  146                 {version}
  147       EOS
  148       doc = document_from_string input, attributes: { 'attribute-missing' => 'drop-line' }
  149       assert_equal '', doc.attributes['release']
  150       assert_message @logger, :INFO, 'dropping line containing reference to missing attribute: version'
  151     end
  152 
  153     test 'resolves attributes inside attribute value within header' do
  154       input = <<~'EOS'
  155       = Document Title
  156       :big: big
  157       :bigfoot: {big}foot
  158 
  159       {bigfoot}
  160       EOS
  161 
  162       result = convert_string_to_embedded input
  163       assert_includes result, 'bigfoot'
  164     end
  165 
  166     test 'resolves attributes and pass macro inside attribute value outside header' do
  167       input = <<~'EOS'
  168       = Document Title
  169 
  170       content
  171 
  172       :big: pass:a,q[_big_]
  173       :bigfoot: {big}foot
  174       {bigfoot}
  175       EOS
  176 
  177       result = convert_string_to_embedded input
  178       assert_includes result, '<em>big</em>foot'
  179     end
  180 
  181     test 'should limit maximum size of attribute value if safe mode is SECURE' do
  182       expected = 'a' * 4096
  183       input = <<~EOS
  184       :name: #{'a' * 5000}
  185 
  186       {name}
  187       EOS
  188 
  189       result = convert_inline_string input
  190       assert_equal expected, result
  191       assert_equal 4096, result.bytesize
  192     end
  193 
  194     test 'should handle multibyte characters when limiting attribute value size' do
  195       expected = '日本'
  196       input = <<~'EOS'
  197       :name: 日本語
  198 
  199       {name}
  200       EOS
  201 
  202       result = convert_inline_string input, attributes: { 'max-attribute-value-size' => 6 }
  203       assert_equal expected, result
  204       assert_equal 6, result.bytesize
  205     end
  206 
  207     test 'should not mangle multibyte characters when limiting attribute value size' do
  208       expected = '日本'
  209       input = <<~'EOS'
  210       :name: 日本語
  211 
  212       {name}
  213       EOS
  214 
  215       result = convert_inline_string input, attributes: { 'max-attribute-value-size' => 8 }
  216       assert_equal expected, result
  217       assert_equal 6, result.bytesize
  218     end
  219 
  220     test 'should allow maximize size of attribute value to be disabled' do
  221       expected = 'a' * 5000
  222       input = <<~EOS
  223       :name: #{'a' * 5000}
  224 
  225       {name}
  226       EOS
  227 
  228       result = convert_inline_string input, attributes: { 'max-attribute-value-size' => nil }
  229       assert_equal expected, result
  230       assert_equal 5000, result.bytesize
  231     end
  232 
  233     test 'resolves user-home attribute if safe mode is less than SERVER' do
  234       input = <<~'EOS'
  235       :imagesdir: {user-home}/etc/images
  236 
  237       {imagesdir}
  238       EOS
  239       output = convert_inline_string input, safe: :safe
  240       assert_equal %(#{Asciidoctor::USER_HOME}/etc/images), output
  241     end
  242 
  243     test 'user-home attribute resolves to . if safe mode is SERVER or greater' do
  244       input = <<~'EOS'
  245       :imagesdir: {user-home}/etc/images
  246 
  247       {imagesdir}
  248       EOS
  249       output = convert_inline_string input, safe: :server
  250       assert_equal './etc/images', output
  251     end
  252 
  253     test "apply custom substitutions to text in passthrough macro and assign to attribute" do
  254       doc = document_from_string(":xml-busters: pass:[<>&]")
  255       assert_equal '<>&', doc.attributes['xml-busters']
  256       doc = document_from_string(":xml-busters: pass:none[<>&]")
  257       assert_equal '<>&', doc.attributes['xml-busters']
  258       doc = document_from_string(":xml-busters: pass:specialcharacters[<>&]")
  259       assert_equal '&lt;&gt;&amp;', doc.attributes['xml-busters']
  260       doc = document_from_string(":xml-busters: pass:n,-c[<(C)>]")
  261       assert_equal '<&#169;>', doc.attributes['xml-busters']
  262     end
  263 
  264     test 'should not recognize pass macro with invalid substitution list in attribute value' do
  265       [',', '42', 'a,'].each do |subs|
  266         doc = document_from_string %(:pass-fail: pass:#{subs}[whale])
  267         assert_equal %(pass:#{subs}[whale]), doc.attributes['pass-fail']
  268       end
  269     end
  270 
  271     test "attribute is treated as defined until it's not" do
  272       input = <<~'EOS'
  273       :holygrail:
  274       ifdef::holygrail[]
  275       The holy grail has been found!
  276       endif::holygrail[]
  277 
  278       :holygrail!:
  279       ifndef::holygrail[]
  280       Buggers! What happened to the grail?
  281       endif::holygrail[]
  282       EOS
  283       output = convert_string input
  284       assert_xpath '//p', output, 2
  285       assert_xpath '(//p)[1][text() = "The holy grail has been found!"]', output, 1
  286       assert_xpath '(//p)[2][text() = "Buggers! What happened to the grail?"]', output, 1
  287     end
  288 
  289     test 'attribute set via API overrides attribute set in document' do
  290       doc = document_from_string(':cash: money', attributes: { 'cash' => 'heroes' })
  291       assert_equal 'heroes', doc.attributes['cash']
  292     end
  293 
  294     test 'attribute set via API cannot be unset by document' do
  295       doc = document_from_string(':cash!:', attributes: { 'cash' => 'heroes' })
  296       assert_equal 'heroes', doc.attributes['cash']
  297     end
  298 
  299     test 'attribute soft set via API using modifier on name can be overridden by document' do
  300       doc = document_from_string(':cash: money', attributes: { 'cash@' => 'heroes' })
  301       assert_equal 'money', doc.attributes['cash']
  302     end
  303 
  304     test 'attribute soft set via API using modifier on value can be overridden by document' do
  305       doc = document_from_string(':cash: money', attributes: { 'cash' => 'heroes@' })
  306       assert_equal 'money', doc.attributes['cash']
  307     end
  308 
  309     test 'attribute soft set via API using modifier on name can be unset by document' do
  310       doc = document_from_string(':cash!:', attributes: { 'cash@' => 'heroes' })
  311       assert_nil doc.attributes['cash']
  312       doc = document_from_string(':cash!:', attributes: { 'cash@' => true })
  313       assert_nil doc.attributes['cash']
  314     end
  315 
  316     test 'attribute soft set via API using modifier on value can be unset by document' do
  317       doc = document_from_string(':cash!:', attributes: { 'cash' => 'heroes@' })
  318       assert_nil doc.attributes['cash']
  319     end
  320 
  321     test 'attribute unset via API cannot be set by document' do
  322       [
  323         { 'cash!' => '' },
  324         { '!cash' => '' },
  325         { 'cash' => nil },
  326       ].each do |attributes|
  327         doc = document_from_string(':cash: money', attributes: attributes)
  328         assert_nil doc.attributes['cash']
  329       end
  330     end
  331 
  332     test 'attribute soft unset via API can be set by document' do
  333       [
  334         { 'cash!@' => '' },
  335         { '!cash@' => '' },
  336         { 'cash!' => '@' },
  337         { '!cash' => '@' },
  338         { 'cash' => false },
  339       ].each do |attributes|
  340         doc = document_from_string(':cash: money', attributes: attributes)
  341         assert_equal 'money', doc.attributes['cash']
  342       end
  343     end
  344 
  345     test 'can soft unset built-in attribute from API and still override in document' do
  346       [
  347         { 'sectids!@' => '' },
  348         { '!sectids@' => '' },
  349         { 'sectids!' => '@' },
  350         { '!sectids' => '@' },
  351         { 'sectids' => false },
  352       ].each do |attributes|
  353         doc = document_from_string '== Heading', attributes: attributes
  354         refute doc.attr?('sectids')
  355         assert_css '#_heading', (doc.convert standalone: false), 0
  356         doc = document_from_string %(:sectids:\n\n== Heading), attributes: attributes
  357         assert doc.attr?('sectids')
  358         assert_css '#_heading', (doc.convert standalone: false), 1
  359       end
  360     end
  361 
  362     test 'backend and doctype attributes are set by default in default configuration' do
  363       input = <<~'EOS'
  364       = Document Title
  365       Author Name
  366 
  367       content
  368       EOS
  369 
  370       doc = document_from_string input
  371       expect = {
  372         'backend' => 'html5',
  373         'backend-html5' => '',
  374         'backend-html5-doctype-article' => '',
  375         'outfilesuffix' => '.html',
  376         'basebackend' => 'html',
  377         'basebackend-html' => '',
  378         'basebackend-html-doctype-article' => '',
  379         'doctype' => 'article',
  380         'doctype-article' => '',
  381         'filetype' => 'html',
  382         'filetype-html' => '',
  383       }
  384       expect.each do |key, val|
  385         assert doc.attributes.key? key
  386         assert_equal val, doc.attributes[key]
  387       end
  388     end
  389 
  390     test 'backend and doctype attributes are set by default in custom configuration' do
  391       input = <<~'EOS'
  392       = Document Title
  393       Author Name
  394 
  395       content
  396       EOS
  397 
  398       doc = document_from_string input, doctype: 'book', backend: 'docbook'
  399       expect = {
  400         'backend' => 'docbook5',
  401         'backend-docbook5' => '',
  402         'backend-docbook5-doctype-book' => '',
  403         'outfilesuffix' => '.xml',
  404         'basebackend' => 'docbook',
  405         'basebackend-docbook' => '',
  406         'basebackend-docbook-doctype-book' => '',
  407         'doctype' => 'book',
  408         'doctype-book' => '',
  409         'filetype' => 'xml',
  410         'filetype-xml' => '',
  411       }
  412       expect.each do |key, val|
  413         assert doc.attributes.key? key
  414         assert_equal val, doc.attributes[key]
  415       end
  416     end
  417 
  418     test 'backend attributes are updated if backend attribute is defined in document and safe mode is less than SERVER' do
  419       input = <<~'EOS'
  420       = Document Title
  421       Author Name
  422       :backend: docbook
  423       :doctype: book
  424 
  425       content
  426       EOS
  427 
  428       doc = document_from_string input, safe: Asciidoctor::SafeMode::SAFE
  429       expect = {
  430         'backend' => 'docbook5',
  431         'backend-docbook5' => '',
  432         'backend-docbook5-doctype-book' => '',
  433         'outfilesuffix' => '.xml',
  434         'basebackend' => 'docbook',
  435         'basebackend-docbook' => '',
  436         'basebackend-docbook-doctype-book' => '',
  437         'doctype' => 'book',
  438         'doctype-book' => '',
  439         'filetype' => 'xml',
  440         'filetype-xml' => '',
  441       }
  442       expect.each do |key, val|
  443         assert doc.attributes.key?(key)
  444         assert_equal val, doc.attributes[key]
  445       end
  446 
  447       refute doc.attributes.key?('backend-html5')
  448       refute doc.attributes.key?('backend-html5-doctype-article')
  449       refute doc.attributes.key?('basebackend-html')
  450       refute doc.attributes.key?('basebackend-html-doctype-article')
  451       refute doc.attributes.key?('doctype-article')
  452       refute doc.attributes.key?('filetype-html')
  453     end
  454 
  455     test 'backend attributes defined in document options overrides backend attribute in document' do
  456       doc = document_from_string(':backend: docbook5', safe: Asciidoctor::SafeMode::SAFE, attributes: { 'backend' => 'html5' })
  457       assert_equal 'html5', doc.attributes['backend']
  458       assert doc.attributes.key? 'backend-html5'
  459       assert_equal 'html', doc.attributes['basebackend']
  460       assert doc.attributes.key? 'basebackend-html'
  461     end
  462 
  463     test 'can only access a positional attribute from the attributes hash' do
  464       node = Asciidoctor::Block.new nil, :paragraph, attributes: { 1 => 'position 1' }
  465       assert_nil node.attr(1)
  466       refute node.attr?(1)
  467       assert_equal 'position 1', node.attributes[1]
  468     end
  469 
  470     test 'attr should not retrieve attribute from document if not set on block' do
  471       doc = document_from_string 'paragraph', :attributes => { 'name' => 'value' }
  472       para = doc.blocks[0]
  473       assert_nil para.attr 'name'
  474     end
  475 
  476     test 'attr looks for attribute on document if fallback name is true' do
  477       doc = document_from_string 'paragraph', :attributes => { 'name' => 'value' }
  478       para = doc.blocks[0]
  479       assert_equal 'value', (para.attr 'name', nil, true)
  480     end
  481 
  482     test 'attr uses fallback name when looking for attribute on document' do
  483       doc = document_from_string 'paragraph', :attributes => { 'alt-name' => 'value' }
  484       para = doc.blocks[0]
  485       assert_equal 'value', (para.attr 'name', nil, 'alt-name')
  486     end
  487 
  488     test 'attr? should not check for attribute on document if not set on block' do
  489       doc = document_from_string 'paragraph', :attributes => { 'name' => 'value' }
  490       para = doc.blocks[0]
  491       refute para.attr? 'name'
  492     end
  493 
  494     test 'attr? checks for attribute on document if fallback name is true' do
  495       doc = document_from_string 'paragraph', :attributes => { 'name' => 'value' }
  496       para = doc.blocks[0]
  497       assert para.attr? 'name', nil, true
  498     end
  499 
  500     test 'attr? checks for fallback name when looking for attribute on document' do
  501       doc = document_from_string 'paragraph', :attributes => { 'alt-name' => 'value' }
  502       para = doc.blocks[0]
  503       assert para.attr? 'name', nil, 'alt-name'
  504     end
  505 
  506     test 'set_attr should set value to empty string if no value is specified' do
  507       node = Asciidoctor::Block.new nil, :paragraph, attributes: {}
  508       node.set_attr 'foo'
  509       assert_equal '', (node.attr 'foo')
  510     end
  511 
  512     test 'remove_attr should remove attribute and return previous value' do
  513       doc = empty_document
  514       node = Asciidoctor::Block.new doc, :paragraph, attributes: { 'foo' => 'bar' }
  515       assert_equal 'bar', (node.remove_attr 'foo')
  516       assert_nil node.attr('foo')
  517     end
  518 
  519     test 'set_attr should not overwrite existing key if overwrite is false' do
  520       node = Asciidoctor::Block.new nil, :paragraph, attributes: { 'foo' => 'bar' }
  521       assert_equal 'bar', (node.attr 'foo')
  522       node.set_attr 'foo', 'baz', false
  523       assert_equal 'bar', (node.attr 'foo')
  524     end
  525 
  526     test 'set_attr should overwrite existing key by default' do
  527       node = Asciidoctor::Block.new nil, :paragraph, attributes: { 'foo' => 'bar' }
  528       assert_equal 'bar', (node.attr 'foo')
  529       node.set_attr 'foo', 'baz'
  530       assert_equal 'baz', (node.attr 'foo')
  531     end
  532 
  533     test 'set_attr should set header attribute in loaded document' do
  534       input = <<~'EOS'
  535       :uri: http://example.org
  536 
  537       {uri}
  538       EOS
  539 
  540       doc = Asciidoctor.load input, attributes: { 'uri' => 'https://github.com' }
  541       doc.set_attr 'uri', 'https://google.com'
  542       output = doc.convert
  543       assert_xpath '//a[@href="https://google.com"]', output, 1
  544     end
  545 
  546     test 'set_attribute should set attribute if key is not locked' do
  547       doc = empty_document
  548       refute doc.attr? 'foo'
  549       res = doc.set_attribute 'foo', 'baz'
  550       assert res
  551       assert_equal 'baz', (doc.attr 'foo')
  552     end
  553 
  554     test 'set_attribute should not set key if key is locked' do
  555       doc = empty_document attributes: { 'foo' => 'bar' }
  556       assert_equal 'bar', (doc.attr 'foo')
  557       res = doc.set_attribute 'foo', 'baz'
  558       refute res
  559       assert_equal 'bar', (doc.attr 'foo')
  560     end
  561 
  562     test 'set_attribute should update backend attributes' do
  563       doc = empty_document attributes: { 'backend' => 'html5@' }
  564       assert_equal '', (doc.attr 'backend-html5')
  565       res = doc.set_attribute 'backend', 'docbook5'
  566       assert res
  567       refute doc.attr? 'backend-html5'
  568       assert_equal '', (doc.attr 'backend-docbook5')
  569     end
  570 
  571     test 'verify toc attribute matrix' do
  572       expected_data = <<~'EOS'
  573       #attributes                               |toc|toc-position|toc-placement|toc-class
  574       toc                                       |   |nil         |auto         |nil
  575       toc=header                                |   |nil         |auto         |nil
  576       toc=beeboo                                |   |nil         |auto         |nil
  577       toc=left                                  |   |left        |auto         |toc2
  578       toc2                                      |   |left        |auto         |toc2
  579       toc=right                                 |   |right       |auto         |toc2
  580       toc=preamble                              |   |content     |preamble     |nil
  581       toc=macro                                 |   |content     |macro        |nil
  582       toc toc-placement=macro toc-position=left |   |content     |macro        |nil
  583       toc toc-placement!                        |   |content     |macro        |nil
  584       EOS
  585 
  586       expected = expected_data.lines.map do |l|
  587         next if l.start_with? '#'
  588         l.split('|').map {|e| (e = e.strip) == 'nil' ? nil : e }
  589       end.compact
  590 
  591       expected.each do |expect|
  592         raw_attrs, toc, toc_position, toc_placement, toc_class = expect
  593         attrs = Hash[*raw_attrs.split.map {|e| e.include?('=') ? e.split('=', 2) : [e, ''] }.flatten]
  594         doc = document_from_string '', attributes: attrs
  595         toc ? (assert doc.attr?('toc', toc)) : (refute doc.attr?('toc'))
  596         toc_position ? (assert doc.attr?('toc-position', toc_position)) : (refute doc.attr?('toc-position'))
  597         toc_placement ? (assert doc.attr?('toc-placement', toc_placement)) : (refute doc.attr?('toc-placement'))
  598         toc_class ? (assert doc.attr?('toc-class', toc_class)) : (refute doc.attr?('toc-class'))
  599       end
  600     end
  601   end
  602 
  603   context 'Interpolation' do
  604 
  605     test "convert properly with simple names" do
  606       html = convert_string(":frog: Tanglefoot\n:my_super-hero: Spiderman\n\nYo, {frog}!\nBeat {my_super-hero}!")
  607       assert_xpath %(//p[text()="Yo, Tanglefoot!\nBeat Spiderman!"]), html, 1
  608     end
  609 
  610     test 'attribute lookup is not case sensitive' do
  611       input = <<~'EOS'
  612       :He-Man: The most powerful man in the universe
  613 
  614       He-Man: {He-Man}
  615 
  616       She-Ra: {She-Ra}
  617       EOS
  618       result = convert_string_to_embedded input, attributes: { 'She-Ra' => 'The Princess of Power' }
  619       assert_xpath '//p[text()="He-Man: The most powerful man in the universe"]', result, 1
  620       assert_xpath '//p[text()="She-Ra: The Princess of Power"]', result, 1
  621     end
  622 
  623     test "convert properly with single character name" do
  624       html = convert_string(":r: Ruby\n\nR is for {r}!")
  625       assert_xpath %(//p[text()="R is for Ruby!"]), html, 1
  626     end
  627 
  628     test "collapses spaces in attribute names" do
  629       input = <<~'EOS'
  630       Main Header
  631       ===========
  632       :My frog: Tanglefoot
  633 
  634       Yo, {myfrog}!
  635       EOS
  636       output = convert_string input
  637       assert_xpath '(//p)[1][text()="Yo, Tanglefoot!"]', output, 1
  638     end
  639 
  640     test 'ignores lines with bad attributes if attribute-missing is drop-line' do
  641       input = <<~'EOS'
  642       :attribute-missing: drop-line
  643 
  644       This is
  645       blah blah {foobarbaz}
  646       all there is.
  647       EOS
  648       output = convert_string_to_embedded input
  649       para = xmlnodes_at_css 'p', output, 1
  650       refute_includes 'blah blah', para.content
  651       assert_message @logger, :INFO, 'dropping line containing reference to missing attribute: foobarbaz'
  652     end
  653 
  654     test "attribute value gets interpretted when converting" do
  655       doc = document_from_string(":google: http://google.com[Google]\n\n{google}")
  656       assert_equal 'http://google.com[Google]', doc.attributes['google']
  657       output = doc.convert
  658       assert_xpath '//a[@href="http://google.com"][text() = "Google"]', output, 1
  659     end
  660 
  661     test 'should drop line with reference to missing attribute if attribute-missing attribute is drop-line' do
  662       input = <<~'EOS'
  663       :attribute-missing: drop-line
  664 
  665       Line 1: This line should appear in the output.
  666       Line 2: Oh no, a {bogus-attribute}! This line should not appear in the output.
  667       EOS
  668 
  669       output = convert_string_to_embedded input
  670       assert_match(/Line 1/, output)
  671       refute_match(/Line 2/, output)
  672       assert_message @logger, :INFO, 'dropping line containing reference to missing attribute: bogus-attribute'
  673     end
  674 
  675     test 'should not drop line with reference to missing attribute by default' do
  676       input = <<~'EOS'
  677       Line 1: This line should appear in the output.
  678       Line 2: A {bogus-attribute}! This time, this line should appear in the output.
  679       EOS
  680 
  681       output = convert_string_to_embedded input
  682       assert_match(/Line 1/, output)
  683       assert_match(/Line 2/, output)
  684       assert_match(/\{bogus-attribute\}/, output)
  685     end
  686 
  687     test 'should drop line with attribute unassignment by default' do
  688       input = <<~'EOS'
  689       :a:
  690 
  691       Line 1: This line should appear in the output.
  692       Line 2: {set:a!}This line should not appear in the output.
  693       EOS
  694 
  695       output = convert_string_to_embedded input
  696       assert_match(/Line 1/, output)
  697       refute_match(/Line 2/, output)
  698     end
  699 
  700     test 'should not drop line with attribute unassignment if attribute-undefined is drop' do
  701       input = <<~'EOS'
  702       :attribute-undefined: drop
  703       :a:
  704 
  705       Line 1: This line should appear in the output.
  706       Line 2: {set:a!}This line should appear in the output.
  707       EOS
  708 
  709       output = convert_string_to_embedded input
  710       assert_match(/Line 1/, output)
  711       assert_match(/Line 2/, output)
  712       refute_match(/\{set:a!\}/, output)
  713     end
  714 
  715     test 'should drop line that only contains attribute assignment' do
  716       input = <<~'EOS'
  717       Line 1
  718       {set:a}
  719       Line 2
  720       EOS
  721 
  722       output = convert_string_to_embedded input
  723       assert_xpath %(//p[text()="Line 1\nLine 2"]), output, 1
  724     end
  725 
  726     test 'should drop line that only contains unresolved attribute when attribute-missing is drop' do
  727       input = <<~'EOS'
  728       Line 1
  729       {unresolved}
  730       Line 2
  731       EOS
  732 
  733       output = convert_string_to_embedded input, attributes: { 'attribute-missing' => 'drop' }
  734       assert_xpath %(//p[text()="Line 1\nLine 2"]), output, 1
  735     end
  736 
  737     test "substitutes inside unordered list items" do
  738       html = convert_string(":foo: bar\n* snort at the {foo}\n* yawn")
  739       assert_xpath %(//li/p[text()="snort at the bar"]), html, 1
  740     end
  741 
  742     test 'substitutes inside section title' do
  743       output = convert_string(":prefix: Cool\n\n== {prefix} Title\n\ncontent")
  744       assert_xpath '//h2[text()="Cool Title"]', output, 1
  745       assert_css 'h2#_cool_title', output, 1
  746     end
  747 
  748     test 'interpolates attribute defined in header inside attribute entry in header' do
  749       input = <<~'EOS'
  750       = Title
  751       Author Name
  752       :attribute-a: value
  753       :attribute-b: {attribute-a}
  754 
  755       preamble
  756       EOS
  757       doc = document_from_string(input, parse_header_only: true)
  758       assert_equal 'value', doc.attributes['attribute-b']
  759     end
  760 
  761     test 'interpolates author attribute inside attribute entry in header' do
  762       input = <<~'EOS'
  763       = Title
  764       Author Name
  765       :name: {author}
  766 
  767       preamble
  768       EOS
  769       doc = document_from_string(input, parse_header_only: true)
  770       assert_equal 'Author Name', doc.attributes['name']
  771     end
  772 
  773     test 'interpolates revinfo attribute inside attribute entry in header' do
  774       input = <<~'EOS'
  775       = Title
  776       Author Name
  777       2013-01-01
  778       :date: {revdate}
  779 
  780       preamble
  781       EOS
  782       doc = document_from_string(input, parse_header_only: true)
  783       assert_equal '2013-01-01', doc.attributes['date']
  784     end
  785 
  786     test 'attribute entries can resolve previously defined attributes' do
  787       input = <<~'EOS'
  788       = Title
  789       Author Name
  790       v1.0, 2010-01-01: First release!
  791       :a: value
  792       :a2: {a}
  793       :revdate2: {revdate}
  794 
  795       {a} == {a2}
  796 
  797       {revdate} == {revdate2}
  798       EOS
  799 
  800       doc = document_from_string input
  801       assert_equal '2010-01-01', doc.attr('revdate')
  802       assert_equal '2010-01-01', doc.attr('revdate2')
  803       assert_equal 'value', doc.attr('a')
  804       assert_equal 'value', doc.attr('a2')
  805 
  806       output = doc.convert
  807       assert_includes output, 'value == value'
  808       assert_includes output, '2010-01-01 == 2010-01-01'
  809     end
  810 
  811     test 'should warn if unterminated block comment is detected in document header' do
  812       input = <<~'EOS'
  813       = Document Title
  814       :foo: bar
  815       ////
  816       :hey: there
  817 
  818       content
  819       EOS
  820       doc = document_from_string input
  821       assert_nil doc.attr('hey')
  822       assert_message @logger, :WARN, '<stdin>: line 3: unterminated comment block', Hash
  823     end
  824 
  825     test 'substitutes inside block title' do
  826       input = <<~'EOS'
  827       :gem_name: asciidoctor
  828 
  829       .Require the +{gem_name}+ gem
  830       To use {gem_name}, the first thing to do is to import it in your Ruby source file.
  831       EOS
  832       output = convert_string_to_embedded input, attributes: { 'compat-mode' => '' }
  833       assert_xpath '//*[@class="title"]/code[text()="asciidoctor"]', output, 1
  834 
  835       input = <<~'EOS'
  836       :gem_name: asciidoctor
  837 
  838       .Require the `{gem_name}` gem
  839       To use {gem_name}, the first thing to do is to import it in your Ruby source file.
  840       EOS
  841       output = convert_string_to_embedded input
  842       assert_xpath '//*[@class="title"]/code[text()="asciidoctor"]', output, 1
  843     end
  844 
  845     test 'sets attribute until it is deleted' do
  846       input = <<~'EOS'
  847       :foo: bar
  848 
  849       Crossing the {foo}.
  850 
  851       :foo!:
  852 
  853       Belly up to the {foo}.
  854       EOS
  855       output = convert_string_to_embedded input
  856       assert_xpath '//p[text()="Crossing the bar."]', output, 1
  857       assert_xpath '//p[text()="Belly up to the bar."]', output, 0
  858     end
  859 
  860     test 'should allow compat-mode to be set and unset in middle of document' do
  861       input = <<~'EOS'
  862       :foo: bar
  863 
  864       [[paragraph-a]]
  865       `{foo}`
  866 
  867       :compat-mode!:
  868 
  869       [[paragraph-b]]
  870       `{foo}`
  871 
  872       :compat-mode:
  873 
  874       [[paragraph-c]]
  875       `{foo}`
  876       EOS
  877 
  878       result = convert_string_to_embedded input, attributes: { 'compat-mode' => '@' }
  879       assert_xpath '/*[@id="paragraph-a"]//code[text()="{foo}"]', result, 1
  880       assert_xpath '/*[@id="paragraph-b"]//code[text()="bar"]', result, 1
  881       assert_xpath '/*[@id="paragraph-c"]//code[text()="{foo}"]', result, 1
  882     end
  883 
  884     test 'does not disturb attribute-looking things escaped with backslash' do
  885       html = convert_string(":foo: bar\nThis is a \\{foo} day.")
  886       assert_xpath '//p[text()="This is a {foo} day."]', html, 1
  887     end
  888 
  889     test 'does not disturb attribute-looking things escaped with literals' do
  890       html = convert_string(":foo: bar\nThis is a +++{foo}+++ day.")
  891       assert_xpath '//p[text()="This is a {foo} day."]', html, 1
  892     end
  893 
  894     test 'does not substitute attributes inside listing blocks' do
  895       input = <<~'EOS'
  896       :forecast: snow
  897 
  898       ----
  899       puts 'The forecast for today is {forecast}'
  900       ----
  901       EOS
  902       output = convert_string(input)
  903       assert_match(/\{forecast\}/, output)
  904     end
  905 
  906     test 'does not substitute attributes inside literal blocks' do
  907       input = <<~'EOS'
  908       :foo: bar
  909 
  910       ....
  911       You insert the text {foo} to expand the value
  912       of the attribute named foo in your document.
  913       ....
  914       EOS
  915       output = convert_string(input)
  916       assert_match(/\{foo\}/, output)
  917     end
  918 
  919     test 'does not show docdir and shows relative docfile if safe mode is SERVER or greater' do
  920       input = <<~'EOS'
  921       * docdir: {docdir}
  922       * docfile: {docfile}
  923       EOS
  924 
  925       docdir = Dir.pwd
  926       docfile = File.join(docdir, 'sample.adoc')
  927       output = convert_string_to_embedded input, safe: Asciidoctor::SafeMode::SERVER, attributes: { 'docdir' => docdir, 'docfile' => docfile }
  928       assert_xpath '//li[1]/p[text()="docdir: "]', output, 1
  929       assert_xpath '//li[2]/p[text()="docfile: sample.adoc"]', output, 1
  930     end
  931 
  932     test 'shows absolute docdir and docfile paths if safe mode is less than SERVER' do
  933       input = <<~'EOS'
  934       * docdir: {docdir}
  935       * docfile: {docfile}
  936       EOS
  937 
  938       docdir = Dir.pwd
  939       docfile = File.join(docdir, 'sample.adoc')
  940       output = convert_string_to_embedded input, safe: Asciidoctor::SafeMode::SAFE, attributes: { 'docdir' => docdir, 'docfile' => docfile }
  941       assert_xpath %(//li[1]/p[text()="docdir: #{docdir}"]), output, 1
  942       assert_xpath %(//li[2]/p[text()="docfile: #{docfile}"]), output, 1
  943     end
  944 
  945     test 'assigns attribute defined in attribute reference with set prefix and value' do
  946       input = '{set:foo:bar}{foo}'
  947       output = convert_string_to_embedded input
  948       assert_xpath '//p', output, 1
  949       assert_xpath '//p[text()="bar"]', output, 1
  950     end
  951 
  952     test 'assigns attribute defined in attribute reference with set prefix and no value' do
  953       input = "{set:foo}\n{foo}yes"
  954       output = convert_string_to_embedded input
  955       assert_xpath '//p', output, 1
  956       assert_xpath '//p[normalize-space(text())="yes"]', output, 1
  957     end
  958 
  959     test 'assigns attribute defined in attribute reference with set prefix and empty value' do
  960       input = "{set:foo:}\n{foo}yes"
  961       output = convert_string_to_embedded input
  962       assert_xpath '//p', output, 1
  963       assert_xpath '//p[normalize-space(text())="yes"]', output, 1
  964     end
  965 
  966     test 'unassigns attribute defined in attribute reference with set prefix' do
  967       input = <<~'EOS'
  968       :attribute-missing: drop-line
  969       :foo:
  970 
  971       {set:foo!}
  972       {foo}yes
  973       EOS
  974       output = convert_string_to_embedded input
  975       assert_xpath '//p', output, 1
  976       assert_xpath '//p/child::text()', output, 0
  977       assert_message @logger, :INFO, 'dropping line containing reference to missing attribute: foo'
  978     end
  979   end
  980 
  981   context "Intrinsic attributes" do
  982 
  983     test "substitute intrinsics" do
  984       Asciidoctor::INTRINSIC_ATTRIBUTES.each_pair do |key, value|
  985         html = convert_string("Look, a {#{key}} is here")
  986         # can't use Nokogiri because it interprets the HTML entities and we can't match them
  987         assert_match(/Look, a #{Regexp.escape(value)} is here/, html)
  988       end
  989     end
  990 
  991     test "don't escape intrinsic substitutions" do
  992       html = convert_string('happy{nbsp}together')
  993       assert_match(/happy&#160;together/, html)
  994     end
  995 
  996     test "escape special characters" do
  997       html = convert_string('<node>&</node>')
  998       assert_match(/&lt;node&gt;&amp;&lt;\/node&gt;/, html)
  999     end
 1000 
 1001     test 'creates counter' do
 1002       input = '{counter:mycounter}'
 1003 
 1004       doc = document_from_string input
 1005       output = doc.convert
 1006       assert_equal 1, doc.attributes['mycounter']
 1007       assert_xpath '//p[text()="1"]', output, 1
 1008     end
 1009 
 1010     test 'creates counter silently' do
 1011       input = '{counter2:mycounter}'
 1012 
 1013       doc = document_from_string input
 1014       output = doc.convert
 1015       assert_equal 1, doc.attributes['mycounter']
 1016       assert_xpath '//p[text()="1"]', output, 0
 1017     end
 1018 
 1019     test 'creates counter with numeric seed value' do
 1020       input = '{counter2:mycounter:10}'
 1021 
 1022       doc = document_from_string input
 1023       doc.convert
 1024       assert_equal 10, doc.attributes['mycounter']
 1025     end
 1026 
 1027     test 'creates counter with character seed value' do
 1028       input = '{counter2:mycounter:A}'
 1029 
 1030       doc = document_from_string input
 1031       doc.convert
 1032       assert_equal 'A', doc.attributes['mycounter']
 1033     end
 1034 
 1035     test 'increments counter with numeric value' do
 1036       input = <<~'EOS'
 1037       :mycounter: 1
 1038 
 1039       {counter:mycounter}
 1040 
 1041       {mycounter}
 1042       EOS
 1043 
 1044       doc = document_from_string input
 1045       output = doc.convert
 1046       assert_equal 2, doc.attributes['mycounter']
 1047       assert_xpath '//p[text()="2"]', output, 2
 1048     end
 1049 
 1050     test 'increments counter with character value' do
 1051       input = <<~'EOS'
 1052       :mycounter: @
 1053 
 1054       {counter:mycounter}
 1055 
 1056       {mycounter}
 1057       EOS
 1058 
 1059       doc = document_from_string input
 1060       output = doc.convert
 1061       assert_equal 'A', doc.attributes['mycounter']
 1062       assert_xpath '//p[text()="A"]', output, 2
 1063     end
 1064 
 1065     test 'counter uses 0 as seed value if seed attribute is nil' do
 1066       input = <<~'EOS'
 1067       :mycounter:
 1068 
 1069       {counter:mycounter}
 1070 
 1071       {mycounter}
 1072       EOS
 1073 
 1074       doc = document_from_string input
 1075       output = doc.convert standalone: false
 1076       assert_equal 1, doc.attributes['mycounter']
 1077       assert_xpath '//p[text()="1"]', output, 2
 1078     end
 1079 
 1080     test 'counter value can be reset by attribute entry' do
 1081       input = <<~'EOS'
 1082       :mycounter:
 1083 
 1084       before: {counter:mycounter} {counter:mycounter} {counter:mycounter}
 1085 
 1086       :mycounter!:
 1087 
 1088       after: {counter:mycounter}
 1089       EOS
 1090 
 1091       doc = document_from_string input
 1092       output = doc.convert standalone: false
 1093       assert_equal 1, doc.attributes['mycounter']
 1094       assert_xpath '//p[text()="before: 1 2 3"]', output, 1
 1095       assert_xpath '//p[text()="after: 1"]', output, 1
 1096     end
 1097 
 1098     test 'nested document should use counter from parent document' do
 1099       input = <<~'EOS'
 1100       .Title for Foo
 1101       image::foo.jpg[]
 1102 
 1103       [cols="2*a"]
 1104       |===
 1105       |
 1106       .Title for Bar
 1107       image::bar.jpg[]
 1108 
 1109       |
 1110       .Title for Baz
 1111       image::baz.jpg[]
 1112       |===
 1113 
 1114       .Title for Qux
 1115       image::qux.jpg[]
 1116       EOS
 1117 
 1118       output = convert_string_to_embedded input
 1119       assert_xpath '//div[@class="title"]', output, 4
 1120       assert_xpath '//div[@class="title"][text() = "Figure 1. Title for Foo"]', output, 1
 1121       assert_xpath '//div[@class="title"][text() = "Figure 2. Title for Bar"]', output, 1
 1122       assert_xpath '//div[@class="title"][text() = "Figure 3. Title for Baz"]', output, 1
 1123       assert_xpath '//div[@class="title"][text() = "Figure 4. Title for Qux"]', output, 1
 1124     end
 1125   end
 1126 
 1127   context 'Block attributes' do
 1128     test 'parses attribute names as name token' do
 1129       input = <<~'EOS'
 1130       [normal,foo="bar",_foo="_bar",foo1="bar1",foo-foo="bar-bar",foo.foo="bar.bar"]
 1131       content
 1132       EOS
 1133 
 1134       block = block_from_string input
 1135       assert_equal 'bar', block.attr('foo')
 1136       assert_equal '_bar', block.attr('_foo')
 1137       assert_equal 'bar1', block.attr('foo1')
 1138       assert_equal 'bar-bar', block.attr('foo-foo')
 1139       assert_equal 'bar.bar', block.attr('foo.foo')
 1140     end
 1141 
 1142     test 'positional attributes assigned to block' do
 1143       input = <<~'EOS'
 1144       [quote, author, source]
 1145       ____
 1146       A famous quote.
 1147       ____
 1148       EOS
 1149       doc = document_from_string(input)
 1150       qb = doc.blocks.first
 1151       assert_equal 'quote', qb.style
 1152       assert_equal 'author', qb.attr('attribution')
 1153       assert_equal 'author', qb.attr(:attribution)
 1154       assert_equal 'author', qb.attributes['attribution']
 1155       assert_equal 'source', qb.attributes['citetitle']
 1156     end
 1157 
 1158     test 'normal substitutions are performed on single-quoted positional attribute' do
 1159       input = <<~'EOS'
 1160       [quote, author, 'http://wikipedia.org[source]']
 1161       ____
 1162       A famous quote.
 1163       ____
 1164       EOS
 1165       doc = document_from_string(input)
 1166       qb = doc.blocks.first
 1167       assert_equal 'quote', qb.style
 1168       assert_equal 'author', qb.attr('attribution')
 1169       assert_equal 'author', qb.attr(:attribution)
 1170       assert_equal 'author', qb.attributes['attribution']
 1171       assert_equal '<a href="http://wikipedia.org">source</a>', qb.attributes['citetitle']
 1172     end
 1173 
 1174     test 'normal substitutions are performed on single-quoted named attribute' do
 1175       input = <<~'EOS'
 1176       [quote, author, citetitle='http://wikipedia.org[source]']
 1177       ____
 1178       A famous quote.
 1179       ____
 1180       EOS
 1181       doc = document_from_string(input)
 1182       qb = doc.blocks.first
 1183       assert_equal 'quote', qb.style
 1184       assert_equal 'author', qb.attr('attribution')
 1185       assert_equal 'author', qb.attr(:attribution)
 1186       assert_equal 'author', qb.attributes['attribution']
 1187       assert_equal '<a href="http://wikipedia.org">source</a>', qb.attributes['citetitle']
 1188     end
 1189 
 1190     test 'normal substitutions are performed once on single-quoted named title attribute' do
 1191       input = <<~'EOS'
 1192       [title='*title*']
 1193       content
 1194       EOS
 1195       output = convert_string_to_embedded input
 1196       assert_xpath '//*[@class="title"]/strong[text()="title"]', output, 1
 1197     end
 1198 
 1199     test 'attribute list may not begin with space' do
 1200       input = <<~'EOS'
 1201       [ quote]
 1202       ____
 1203       A famous quote.
 1204       ____
 1205       EOS
 1206 
 1207       doc = document_from_string input
 1208       b1 = doc.blocks.first
 1209       assert_equal ['[ quote]'], b1.lines
 1210     end
 1211 
 1212     test 'attribute list may begin with comma' do
 1213       input = <<~'EOS'
 1214       [, author, source]
 1215       ____
 1216       A famous quote.
 1217       ____
 1218       EOS
 1219 
 1220       doc = document_from_string input
 1221       qb = doc.blocks.first
 1222       assert_equal 'quote', qb.style
 1223       assert_equal 'author', qb.attributes['attribution']
 1224       assert_equal 'source', qb.attributes['citetitle']
 1225     end
 1226 
 1227     test 'first attribute in list may be double quoted' do
 1228       input = <<~'EOS'
 1229       ["quote", "author", "source", role="famous"]
 1230       ____
 1231       A famous quote.
 1232       ____
 1233       EOS
 1234 
 1235       doc = document_from_string input
 1236       qb = doc.blocks.first
 1237       assert_equal 'quote', qb.style
 1238       assert_equal 'author', qb.attributes['attribution']
 1239       assert_equal 'source', qb.attributes['citetitle']
 1240       assert_equal 'famous', qb.attributes['role']
 1241     end
 1242 
 1243     test 'first attribute in list may be single quoted' do
 1244       input = <<~'EOS'
 1245       ['quote', 'author', 'source', role='famous']
 1246       ____
 1247       A famous quote.
 1248       ____
 1249       EOS
 1250 
 1251       doc = document_from_string input
 1252       qb = doc.blocks.first
 1253       assert_equal 'quote', qb.style
 1254       assert_equal 'author', qb.attributes['attribution']
 1255       assert_equal 'source', qb.attributes['citetitle']
 1256       assert_equal 'famous', qb.attributes['role']
 1257     end
 1258 
 1259     test 'attribute with value None without quotes is ignored' do
 1260       input = <<~'EOS'
 1261       [id=None]
 1262       paragraph
 1263       EOS
 1264 
 1265       doc = document_from_string input
 1266       para = doc.blocks.first
 1267       refute para.attributes.key?('id')
 1268     end
 1269 
 1270     test 'role? returns true if role is assigned' do
 1271       input = <<~'EOS'
 1272       [role="lead"]
 1273       A paragraph
 1274       EOS
 1275 
 1276       doc = document_from_string input
 1277       p = doc.blocks.first
 1278       assert p.role?
 1279     end
 1280 
 1281     test 'role? does not return true if role attribute is set on document' do
 1282       input = <<~'EOS'
 1283       :role: lead
 1284 
 1285       A paragraph
 1286       EOS
 1287 
 1288       doc = document_from_string input
 1289       p = doc.blocks.first
 1290       refute p.role?
 1291     end
 1292 
 1293     test 'role? can check for exact role name match' do
 1294       input = <<~'EOS'
 1295       [role="lead"]
 1296       A paragraph
 1297       EOS
 1298 
 1299       doc = document_from_string input
 1300       p = doc.blocks.first
 1301       assert p.role?('lead')
 1302       p2 = doc.blocks.last
 1303       refute p2.role?('final')
 1304     end
 1305 
 1306     test 'has_role? can check for precense of role name' do
 1307       input = <<~'EOS'
 1308       [role="lead abstract"]
 1309       A paragraph
 1310       EOS
 1311 
 1312       doc = document_from_string input
 1313       p = doc.blocks.first
 1314       refute p.role?('lead')
 1315       assert p.has_role?('lead')
 1316     end
 1317 
 1318     test 'has_role? does not look for role defined as document attribute' do
 1319       input = <<~'EOS'
 1320       :role: lead abstract
 1321 
 1322       A paragraph
 1323       EOS
 1324 
 1325       doc = document_from_string input
 1326       p = doc.blocks.first
 1327       refute p.has_role?('lead')
 1328     end
 1329 
 1330     test 'roles returns array of role names' do
 1331       input = <<~'EOS'
 1332       [role="story lead"]
 1333       A paragraph
 1334       EOS
 1335 
 1336       doc = document_from_string input
 1337       p = doc.blocks.first
 1338       assert_equal ['story', 'lead'], p.roles
 1339     end
 1340 
 1341     test 'roles returns empty array if role attribute is not set' do
 1342       input = 'a paragraph'
 1343 
 1344       doc = document_from_string input
 1345       p = doc.blocks.first
 1346       assert_equal [], p.roles
 1347     end
 1348 
 1349     test 'roles does not return value of roles document attribute' do
 1350       input = <<~'EOS'
 1351       :role: story lead
 1352 
 1353       A paragraph
 1354       EOS
 1355 
 1356       doc = document_from_string input
 1357       p = doc.blocks.first
 1358       assert_equal [], p.roles
 1359     end
 1360 
 1361     test "Attribute substitutions are performed on attribute list before parsing attributes" do
 1362       input = <<~'EOS'
 1363       :lead: role="lead"
 1364 
 1365       [{lead}]
 1366       A paragraph
 1367       EOS
 1368       doc = document_from_string(input)
 1369       para = doc.blocks.first
 1370       assert_equal 'lead', para.attributes['role']
 1371     end
 1372 
 1373     test 'id, role and options attributes can be specified on block style using shorthand syntax' do
 1374       input = <<~'EOS'
 1375       [literal#first.lead%step]
 1376       A literal paragraph.
 1377       EOS
 1378       doc = document_from_string(input)
 1379       para = doc.blocks.first
 1380       assert_equal :literal, para.context
 1381       assert_equal 'first', para.attributes['id']
 1382       assert_equal 'lead', para.attributes['role']
 1383       assert para.attributes.key?('step-option')
 1384       refute para.attributes.key?('options')
 1385     end
 1386 
 1387     test 'id, role and options attributes can be specified using shorthand syntax on block style using multiple block attribute lines' do
 1388       input = <<~'EOS'
 1389       [literal]
 1390       [#first]
 1391       [.lead]
 1392       [%step]
 1393       A literal paragraph.
 1394       EOS
 1395       doc = document_from_string(input)
 1396       para = doc.blocks.first
 1397       assert_equal :literal, para.context
 1398       assert_equal 'first', para.attributes['id']
 1399       assert_equal 'lead', para.attributes['role']
 1400       assert para.attributes.key?('step-option')
 1401       refute para.attributes.key?('options')
 1402     end
 1403 
 1404     test 'multiple roles and options can be specified in block style using shorthand syntax' do
 1405       input = <<~'EOS'
 1406       [.role1%option1.role2%option2]
 1407       Text
 1408       EOS
 1409 
 1410       doc = document_from_string input
 1411       para = doc.blocks.first
 1412       assert_equal 'role1 role2', para.attributes['role']
 1413       assert para.attributes.key?('option1-option')
 1414       assert para.attributes.key?('option2-option')
 1415       refute para.attributes.key?('options')
 1416     end
 1417 
 1418     test 'options specified using shorthand syntax on block style across multiple lines should be additive' do
 1419       input = <<~'EOS'
 1420       [%option1]
 1421       [%option2]
 1422       Text
 1423       EOS
 1424 
 1425       doc = document_from_string input
 1426       para = doc.blocks.first
 1427       assert para.attributes.key?('option1-option')
 1428       assert para.attributes.key?('option2-option')
 1429       refute para.attributes.key?('options')
 1430     end
 1431 
 1432     test 'roles specified using shorthand syntax on block style across multiple lines should be additive' do
 1433       input = <<~'EOS'
 1434       [.role1]
 1435       [.role2.role3]
 1436       Text
 1437       EOS
 1438 
 1439       doc = document_from_string input
 1440       para = doc.blocks.first
 1441       assert_equal 'role1 role2 role3', para.attributes['role']
 1442     end
 1443 
 1444     test 'setting a role using the role attribute replaces any existing roles' do
 1445       input = <<~'EOS'
 1446       [.role1]
 1447       [role=role2]
 1448       [.role3]
 1449       Text
 1450       EOS
 1451 
 1452       doc = document_from_string input
 1453       para = doc.blocks.first
 1454       assert_equal 'role2 role3', para.attributes['role']
 1455     end
 1456 
 1457     test 'setting a role using the shorthand syntax on block style should not clear the ID' do
 1458       input = <<~'EOS'
 1459       [#id]
 1460       [.role]
 1461       Text
 1462       EOS
 1463 
 1464       doc = document_from_string input
 1465       para = doc.blocks.first
 1466       assert_equal 'id', para.id
 1467       assert_equal 'role', para.role
 1468     end
 1469 
 1470     test 'a role can be added using add_role when the node has no roles' do
 1471       input = 'A normal paragraph'
 1472       doc = document_from_string(input)
 1473       para = doc.blocks.first
 1474       res = para.add_role 'role1'
 1475       assert res
 1476       assert_equal 'role1', para.attributes['role']
 1477       assert para.has_role? 'role1'
 1478     end
 1479 
 1480     test 'a role can be added using add_role when the node already has a role' do
 1481       input = <<~'EOS'
 1482       [.role1]
 1483       A normal paragraph
 1484       EOS
 1485       doc = document_from_string(input)
 1486       para = doc.blocks.first
 1487       res = para.add_role 'role2'
 1488       assert res
 1489       assert_equal 'role1 role2', para.attributes['role']
 1490       assert para.has_role? 'role1'
 1491       assert para.has_role? 'role2'
 1492     end
 1493 
 1494     test 'a role is not added using add_role if the node already has that role' do
 1495       input = <<~'EOS'
 1496       [.role1]
 1497       A normal paragraph
 1498       EOS
 1499       doc = document_from_string(input)
 1500       para = doc.blocks.first
 1501       res = para.add_role 'role1'
 1502       refute res
 1503       assert_equal 'role1', para.attributes['role']
 1504       assert para.has_role? 'role1'
 1505     end
 1506 
 1507     test 'an existing role can be removed using remove_role' do
 1508       input = <<~'EOS'
 1509       [.role1.role2]
 1510       A normal paragraph
 1511       EOS
 1512       doc = document_from_string(input)
 1513       para = doc.blocks.first
 1514       res = para.remove_role 'role1'
 1515       assert res
 1516       assert_equal 'role2', para.attributes['role']
 1517       assert para.has_role? 'role2'
 1518       refute para.has_role?('role1')
 1519     end
 1520 
 1521     test 'roles are removed when last role is removed using remove_role' do
 1522       input = <<~'EOS'
 1523       [.role1]
 1524       A normal paragraph
 1525       EOS
 1526       doc = document_from_string(input)
 1527       para = doc.blocks.first
 1528       res = para.remove_role 'role1'
 1529       assert res
 1530       refute para.role?
 1531       assert_nil para.attributes['role']
 1532       refute para.has_role? 'role1'
 1533     end
 1534 
 1535     test 'roles are not changed when a non-existent role is removed using remove_role' do
 1536       input = <<~'EOS'
 1537       [.role1]
 1538       A normal paragraph
 1539       EOS
 1540       doc = document_from_string(input)
 1541       para = doc.blocks.first
 1542       res = para.remove_role 'role2'
 1543       refute res
 1544       assert_equal 'role1', para.attributes['role']
 1545       assert para.has_role? 'role1'
 1546       refute para.has_role?('role2')
 1547     end
 1548 
 1549     test 'roles are not changed when using remove_role if the node has no roles' do
 1550       input = 'A normal paragraph'
 1551       doc = document_from_string(input)
 1552       para = doc.blocks.first
 1553       res = para.remove_role 'role1'
 1554       refute res
 1555       assert_nil para.attributes['role']
 1556       refute para.has_role?('role1')
 1557     end
 1558 
 1559     test 'option can be specified in first position of block style using shorthand syntax' do
 1560       input = <<~'EOS'
 1561       [%interactive]
 1562       - [x] checked
 1563       EOS
 1564 
 1565       doc = document_from_string input
 1566       list = doc.blocks.first
 1567       assert list.attributes.key? 'interactive-option'
 1568       refute list.attributes.key? 'options'
 1569     end
 1570 
 1571     test 'id and role attributes can be specified on section style using shorthand syntax' do
 1572       input = <<~'EOS'
 1573       [dedication#dedication.small]
 1574       == Section
 1575       Content.
 1576       EOS
 1577       output = convert_string_to_embedded input
 1578       assert_xpath '/div[@class="sect1 small"]', output, 1
 1579       assert_xpath '/div[@class="sect1 small"]/h2[@id="dedication"]', output, 1
 1580     end
 1581 
 1582     test 'id attribute specified using shorthand syntax should not create a special section' do
 1583       input = <<~'EOS'
 1584       [#idname]
 1585       == Section
 1586 
 1587       content
 1588       EOS
 1589 
 1590       doc = document_from_string input, backend: 'docbook'
 1591       section = doc.blocks[0]
 1592       refute_nil section
 1593       assert_equal :section, section.context
 1594       refute section.special
 1595       output = doc.convert
 1596       assert_css 'article:root > section', output, 1
 1597       assert_css 'article:root > section[xml|id="idname"]', output, 1
 1598     end
 1599 
 1600     test "Block attributes are additive" do
 1601       input = <<~'EOS'
 1602       [id='foo']
 1603       [role='lead']
 1604       A paragraph.
 1605       EOS
 1606       doc = document_from_string(input)
 1607       para = doc.blocks.first
 1608       assert_equal 'foo', para.id
 1609       assert_equal 'lead', para.attributes['role']
 1610     end
 1611 
 1612     test "Last wins for id attribute" do
 1613       input = <<~'EOS'
 1614       [[bar]]
 1615       [[foo]]
 1616       == Section
 1617 
 1618       paragraph
 1619 
 1620       [[baz]]
 1621       [id='coolio']
 1622       === Section
 1623       EOS
 1624       doc = document_from_string(input)
 1625       sec = doc.first_section
 1626       assert_equal 'foo', sec.id
 1627       subsec = sec.blocks.last
 1628       assert_equal 'coolio', subsec.id
 1629     end
 1630 
 1631     test "trailing block attributes transfer to the following section" do
 1632       input = <<~'EOS'
 1633       [[one]]
 1634 
 1635       == Section One
 1636 
 1637       paragraph
 1638 
 1639       [[sub]]
 1640       // try to mess this up!
 1641 
 1642       === Sub-section
 1643 
 1644       paragraph
 1645 
 1646       [role='classy']
 1647 
 1648       ////
 1649       block comment
 1650       ////
 1651 
 1652       == Section Two
 1653 
 1654       content
 1655       EOS
 1656       doc = document_from_string(input)
 1657       section_one = doc.blocks.first
 1658       assert_equal 'one', section_one.id
 1659       subsection = section_one.blocks.last
 1660       assert_equal 'sub', subsection.id
 1661       section_two = doc.blocks.last
 1662       assert_equal 'classy', section_two.attr(:role)
 1663     end
 1664   end
 1665 
 1666 end