# frozen_string_literal: true require_relative 'test_helper' class ExtensionsInitTest < Minitest::Test def test_autoload doc = empty_document refute doc.extensions?, 'Extensions should not be enabled by default' begin # NOTE trigger extensions to autoload by registering empty group Asciidoctor::Extensions.register do end rescue; end doc = empty_document assert doc.extensions?, 'Extensions should be enabled after being autoloaded' self.class.remove_tests self.class ensure Asciidoctor::Extensions.unregister_all end self end.new(nil).test_autoload class SamplePreprocessor < Asciidoctor::Extensions::Preprocessor def process doc, reader nil end end class SampleIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor end class SampleDocinfoProcessor < Asciidoctor::Extensions::DocinfoProcessor end # NOTE intentionally using the deprecated name class SampleTreeprocessor < Asciidoctor::Extensions::Treeprocessor def process document nil end end SampleTreeProcessor = SampleTreeprocessor class SamplePostprocessor < Asciidoctor::Extensions::Postprocessor end class SampleBlock < Asciidoctor::Extensions::BlockProcessor end class SampleBlockMacro < Asciidoctor::Extensions::BlockMacroProcessor end class SampleInlineMacro < Asciidoctor::Extensions::InlineMacroProcessor end class ScrubHeaderPreprocessor < Asciidoctor::Extensions::Preprocessor def process doc, reader lines = reader.lines skipped = [] while !lines.empty? && !lines.first.start_with?('=') skipped << lines.shift reader.advance end doc.set_attr 'skipped', (skipped * "\n") reader end end class BoilerplateTextIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor def handles? target target.end_with? '.txt' end def process document, reader, target, attributes case target when 'lorem-ipsum.txt' content = ["Lorem ipsum dolor sit amet...\n"] reader.push_include content, target, target, 1, attributes else nil end end end class ReplaceAuthorTreeProcessor < Asciidoctor::Extensions::TreeProcessor def process document document.attributes['firstname'] = 'Ghost' document.attributes['author'] = 'Ghost Writer' document end end class ReplaceTreeTreeProcessor < Asciidoctor::Extensions::TreeProcessor def process document if document.doctitle == 'Original Document' Asciidoctor.load %(== Replacement Document\nReplacement Author\n\ncontent) else document end end end class SelfSigningTreeProcessor < Asciidoctor::Extensions::TreeProcessor def process document document << (create_paragraph document, self.class.name, {}) nil end end class StripAttributesPostprocessor < Asciidoctor::Extensions::Postprocessor def process document, output output.gsub(/<(\w+).*?>/m, "<\\1>") end end class UppercaseBlock < Asciidoctor::Extensions::BlockProcessor; use_dsl named :yell on_context :paragraph name_positional_attributes 'chars' parse_content_as :simple def process parent, reader, attributes if (chars = attributes['chars']) upcase_chars = chars.upcase create_paragraph parent, reader.lines.map {|l| l.downcase.tr chars, upcase_chars }, attributes else create_paragraph parent, reader.lines.map(&:upcase), attributes end end end class SnippetMacro < Asciidoctor::Extensions::BlockMacroProcessor def process parent, target, attributes create_pass_block parent, %(), {}, content_model: :raw end end class TemperatureMacro < Asciidoctor::Extensions::InlineMacroProcessor; use_dsl named :degrees resolve_attributes '1:units', 'precision=1' def process parent, target, attributes units = attributes['units'] || (parent.document.attr 'temperature-unit', 'C') precision = attributes['precision'].to_i c = target.to_f case units when 'C' %(#{c.round precision} °C) when 'F' %(#{(c * 1.8 + 32).round precision} °F) else raise ::ArgumentError, %(Unknown temperature units: #{units}) end end end class MetaRobotsDocinfoProcessor < Asciidoctor::Extensions::DocinfoProcessor def process document '' end end class MetaAppDocinfoProcessor < Asciidoctor::Extensions::DocinfoProcessor; use_dsl at_location :head def process document '' end end class SampleExtensionGroup < Asciidoctor::Extensions::Group def activate registry registry.document.attributes['activate-method-called'] = '' registry.preprocessor SamplePreprocessor end end def create_cat_in_sink_block_macro Asciidoctor::Extensions.create do block_macro do named :cat_in_sink process do |parent, target, attrs| image_attrs = {} unless target.nil_or_empty? image_attrs['target'] = %(cat-in-sink-day-#{target}.png) end if (title = attrs.delete 'title') image_attrs['title'] = title end if (alt = attrs.delete 1) image_attrs['alt'] = alt end create_image_block parent, image_attrs end end end end def create_santa_list_block_macro Asciidoctor::Extensions.create do block_macro do named :santa_list process do |parent, target| list = create_list parent, target guillaume = (create_list_item list, 'Guillaume') guillaume.add_role('friendly') guillaume.id = 'santa-list-guillaume' list << guillaume robert = (create_list_item list, 'Robert') robert.add_role('kind') robert.add_role('contributor') robert.add_role('java') list << robert pepijn = (create_list_item list, 'Pepijn') pepijn.id = 'santa-list-pepijn' list << pepijn dan = (create_list_item list, 'Dan') dan.add_role('naughty') dan.id = 'santa-list-dan' list << dan sarah = (create_list_item list, 'Sarah') list << sarah list end end end end context 'Extensions' do context 'Register' do test 'should not activate registry if no extension groups are registered' do assert defined? Asciidoctor::Extensions doc = empty_document refute doc.extensions?, 'Extensions should not be enabled if not groups are registered' end test 'should register extension group class' do begin Asciidoctor::Extensions.register :sample, SampleExtensionGroup refute_nil Asciidoctor::Extensions.groups assert_equal 1, Asciidoctor::Extensions.groups.size assert_equal SampleExtensionGroup, Asciidoctor::Extensions.groups[:sample] ensure Asciidoctor::Extensions.unregister_all end end test 'should self register extension group class' do begin SampleExtensionGroup.register :sample refute_nil Asciidoctor::Extensions.groups assert_equal 1, Asciidoctor::Extensions.groups.size assert_equal SampleExtensionGroup, Asciidoctor::Extensions.groups[:sample] ensure Asciidoctor::Extensions.unregister_all end end test 'should register extension group from class name' do begin Asciidoctor::Extensions.register :sample, 'SampleExtensionGroup' refute_nil Asciidoctor::Extensions.groups assert_equal 1, Asciidoctor::Extensions.groups.size assert_equal SampleExtensionGroup, Asciidoctor::Extensions.groups[:sample] ensure Asciidoctor::Extensions.unregister_all end end test 'should register extension group from instance' do begin Asciidoctor::Extensions.register :sample, SampleExtensionGroup.new refute_nil Asciidoctor::Extensions.groups assert_equal 1, Asciidoctor::Extensions.groups.size assert_kind_of SampleExtensionGroup, Asciidoctor::Extensions.groups[:sample] ensure Asciidoctor::Extensions.unregister_all end end test 'should register extension block' do begin Asciidoctor::Extensions.register :sample do end refute_nil Asciidoctor::Extensions.groups assert_equal 1, Asciidoctor::Extensions.groups.size assert_kind_of Proc, Asciidoctor::Extensions.groups[:sample] ensure Asciidoctor::Extensions.unregister_all end end test 'should coerce group name to symbol when registering' do begin Asciidoctor::Extensions.register 'sample', SampleExtensionGroup refute_nil Asciidoctor::Extensions.groups assert_equal 1, Asciidoctor::Extensions.groups.size assert_equal SampleExtensionGroup, Asciidoctor::Extensions.groups[:sample] ensure Asciidoctor::Extensions.unregister_all end end test 'should unregister extension group by symbol name' do begin Asciidoctor::Extensions.register :sample, SampleExtensionGroup refute_nil Asciidoctor::Extensions.groups assert_equal 1, Asciidoctor::Extensions.groups.size Asciidoctor::Extensions.unregister :sample assert_equal 0, Asciidoctor::Extensions.groups.size ensure Asciidoctor::Extensions.unregister_all end end test 'should unregister extension group by string name' do begin Asciidoctor::Extensions.register :sample, SampleExtensionGroup refute_nil Asciidoctor::Extensions.groups assert_equal 1, Asciidoctor::Extensions.groups.size Asciidoctor::Extensions.unregister 'sample' assert_equal 0, Asciidoctor::Extensions.groups.size ensure Asciidoctor::Extensions.unregister_all end end test 'should unregister multiple extension groups by name' do begin Asciidoctor::Extensions.register :sample1, SampleExtensionGroup Asciidoctor::Extensions.register :sample2, SampleExtensionGroup refute_nil Asciidoctor::Extensions.groups assert_equal 2, Asciidoctor::Extensions.groups.size Asciidoctor::Extensions.unregister :sample1, :sample2 assert_equal 0, Asciidoctor::Extensions.groups.size ensure Asciidoctor::Extensions.unregister_all end end test 'should raise NameError if extension class cannot be resolved from string' do begin Asciidoctor::Extensions.register do block 'foobar' end empty_document flunk 'Expecting RuntimeError to be raised' rescue NameError => e assert_equal 'Could not resolve class for name: foobar', e.message ensure Asciidoctor::Extensions.unregister_all end end test 'should allow standalone registry to be created but not registered' do registry = Asciidoctor::Extensions.create 'sample' do block do named :whisper on_context :paragraph parse_content_as :simple def process parent, reader, attributes create_paragraph parent, reader.lines.map(&:downcase), attributes end end end assert_instance_of Asciidoctor::Extensions::Registry, registry refute_nil registry.groups assert_equal 1, registry.groups.size assert_equal 'sample', registry.groups.keys.first assert_equal 0, Asciidoctor::Extensions.groups.size end end context 'Activate' do test 'should call activate on extension group class' do begin doc = Asciidoctor::Document.new Asciidoctor::Extensions.register :sample, SampleExtensionGroup registry = Asciidoctor::Extensions::Registry.new registry.activate doc assert doc.attr? 'activate-method-called' assert registry.preprocessors? ensure Asciidoctor::Extensions.unregister_all end end test 'should invoke extension block' do begin doc = Asciidoctor::Document.new Asciidoctor::Extensions.register do @document.attributes['block-called'] = '' preprocessor SamplePreprocessor end registry = Asciidoctor::Extensions::Registry.new registry.activate doc assert doc.attr? 'block-called' assert registry.preprocessors? ensure Asciidoctor::Extensions.unregister_all end end test 'should create registry in Document if extensions are loaded' do begin SampleExtensionGroup.register doc = Asciidoctor::Document.new assert doc.extensions? assert_kind_of Asciidoctor::Extensions::Registry, doc.extensions ensure Asciidoctor::Extensions.unregister_all end end end context 'Instantiate' do test 'should instantiate preprocessors' do registry = Asciidoctor::Extensions::Registry.new registry.preprocessor SamplePreprocessor registry.activate Asciidoctor::Document.new assert registry.preprocessors? extensions = registry.preprocessors assert_equal 1, extensions.size assert_kind_of Asciidoctor::Extensions::ProcessorExtension, extensions.first assert_kind_of SamplePreprocessor, extensions.first.instance assert_kind_of Method, extensions.first.process_method end test 'should instantiate include processors' do registry = Asciidoctor::Extensions::Registry.new registry.include_processor SampleIncludeProcessor registry.activate Asciidoctor::Document.new assert registry.include_processors? extensions = registry.include_processors assert_equal 1, extensions.size assert_kind_of Asciidoctor::Extensions::ProcessorExtension, extensions.first assert_kind_of SampleIncludeProcessor, extensions.first.instance assert_kind_of Method, extensions.first.process_method end test 'should instantiate docinfo processors' do registry = Asciidoctor::Extensions::Registry.new registry.docinfo_processor SampleDocinfoProcessor registry.activate Asciidoctor::Document.new assert registry.docinfo_processors? assert registry.docinfo_processors?(:head) extensions = registry.docinfo_processors assert_equal 1, extensions.size assert_kind_of Asciidoctor::Extensions::ProcessorExtension, extensions.first assert_kind_of SampleDocinfoProcessor, extensions.first.instance assert_kind_of Method, extensions.first.process_method end # NOTE intentionally using the legacy names test 'should instantiate tree processors' do registry = Asciidoctor::Extensions::Registry.new registry.treeprocessor SampleTreeprocessor registry.activate Asciidoctor::Document.new assert registry.treeprocessors? extensions = registry.treeprocessors assert_equal 1, extensions.size assert_kind_of Asciidoctor::Extensions::ProcessorExtension, extensions.first assert_kind_of SampleTreeprocessor, extensions.first.instance assert_kind_of Method, extensions.first.process_method end test 'should instantiate postprocessors' do registry = Asciidoctor::Extensions::Registry.new registry.postprocessor SamplePostprocessor registry.activate Asciidoctor::Document.new assert registry.postprocessors? extensions = registry.postprocessors assert_equal 1, extensions.size assert_kind_of Asciidoctor::Extensions::ProcessorExtension, extensions.first assert_kind_of SamplePostprocessor, extensions.first.instance assert_kind_of Method, extensions.first.process_method end test 'should instantiate block processor' do registry = Asciidoctor::Extensions::Registry.new registry.block SampleBlock, :sample registry.activate Asciidoctor::Document.new assert registry.blocks? assert registry.registered_for_block? :sample, :paragraph extension = registry.find_block_extension :sample assert_kind_of Asciidoctor::Extensions::ProcessorExtension, extension assert_kind_of SampleBlock, extension.instance assert_kind_of Method, extension.process_method end test 'should not match block processor for unsupported context' do registry = Asciidoctor::Extensions::Registry.new registry.block SampleBlock, :sample registry.activate Asciidoctor::Document.new refute registry.registered_for_block? :sample, :sidebar end test 'should instantiate block macro processor' do registry = Asciidoctor::Extensions::Registry.new registry.block_macro SampleBlockMacro, 'sample' registry.activate Asciidoctor::Document.new assert registry.block_macros? assert registry.registered_for_block_macro? 'sample' extension = registry.find_block_macro_extension 'sample' assert_kind_of Asciidoctor::Extensions::ProcessorExtension, extension assert_kind_of SampleBlockMacro, extension.instance assert_kind_of Method, extension.process_method end test 'should instantiate inline macro processor' do registry = Asciidoctor::Extensions::Registry.new registry.inline_macro SampleInlineMacro, 'sample' registry.activate Asciidoctor::Document.new assert registry.inline_macros? assert registry.registered_for_inline_macro? 'sample' extension = registry.find_inline_macro_extension 'sample' assert_kind_of Asciidoctor::Extensions::ProcessorExtension, extension assert_kind_of SampleInlineMacro, extension.instance assert_kind_of Method, extension.process_method end test 'should allow processors to be registered by a string name' do registry = Asciidoctor::Extensions::Registry.new registry.preprocessor 'SamplePreprocessor' registry.activate Asciidoctor::Document.new assert registry.preprocessors? extensions = registry.preprocessors assert_equal 1, extensions.size assert_kind_of Asciidoctor::Extensions::ProcessorExtension, extensions.first end end context 'Integration' do test 'can provide extension registry as an option' do registry = Asciidoctor::Extensions.create do tree_processor SampleTreeProcessor end doc = document_from_string %(= Document Title\n\ncontent), extension_registry: registry refute_nil doc.extensions assert_equal 1, doc.extensions.groups.size assert doc.extensions.tree_processors? assert_equal 1, doc.extensions.tree_processors.size assert_equal 0, Asciidoctor::Extensions.groups.size end # NOTE I'm not convinced we want to continue to support this use case test 'can provide extension registry created without any groups as option' do registry = Asciidoctor::Extensions.create registry.tree_processor SampleTreeProcessor doc = document_from_string %(= Document Title\n\ncontent), extension_registry: registry refute_nil doc.extensions assert_equal 0, doc.extensions.groups.size assert doc.extensions.tree_processors? assert_equal 1, doc.extensions.tree_processors.size assert_equal 0, Asciidoctor::Extensions.groups.size end test 'can provide extensions proc as option' do doc = document_from_string %(= Document Title\n\ncontent), extensions: proc { tree_processor SampleTreeProcessor } refute_nil doc.extensions assert_equal 1, doc.extensions.groups.size assert doc.extensions.tree_processors? assert_equal 1, doc.extensions.tree_processors.size assert_equal 0, Asciidoctor::Extensions.groups.size end test 'should invoke preprocessors before parsing document' do input = <<~'EOS' junk line = Document Title sample content EOS begin Asciidoctor::Extensions.register do preprocessor ScrubHeaderPreprocessor end doc = document_from_string input assert doc.attr? 'skipped' assert_equal 'junk line', (doc.attr 'skipped').strip assert doc.has_header? assert_equal 'Document Title', doc.doctitle ensure Asciidoctor::Extensions.unregister_all end end test 'should invoke include processor to process include directive' do input = <<~'EOS' before include::lorem-ipsum.txt[] after EOS begin Asciidoctor::Extensions.register do include_processor BoilerplateTextIncludeProcessor end # a custom include processor is not affected by the safe mode result = convert_string input, safe: :secure assert_css '.paragraph > p', result, 3 assert_includes result, 'before' assert_includes result, 'Lorem ipsum' assert_includes result, 'after' ensure Asciidoctor::Extensions.unregister_all end end test 'should invoke include processor if it offers to handle include directive' do input = <<~'EOS' include::skip-me.adoc[] line after skip include::include-file.adoc[] include::fixtures/grandchild-include.adoc[] last line EOS registry = Asciidoctor::Extensions.create do include_processor do handles? do |target| target == 'skip-me.adoc' end process do |doc, reader, target, attributes| end end include_processor do handles? do |target| target == 'include-file.adoc' end process do |doc, reader, target, attributes| # demonstrates that push_include normalizes newlines content = [ %(found include target '#{target}' at line #{reader.cursor_at_prev_line.lineno}\r\n), %(\r\n), %(middle line\r\n) ] reader.push_include content, target, target, 1, attributes end end end # safe mode only required for built-in include processor document = empty_document base_dir: testdir, extension_registry: registry, safe: :safe reader = Asciidoctor::PreprocessorReader.new document, input, nil, normalize: true lines = [] lines << reader.read_line assert_equal 'line after skip', lines.last lines << reader.read_line lines << reader.read_line assert_equal 'found include target \'include-file.adoc\' at line 4', lines.last assert_equal 'include-file.adoc: line 2', reader.line_info while reader.has_more_lines? lines << reader.read_line end source = lines * ::Asciidoctor::LF assert_match(/^found include target 'include-file.adoc' at line 4$/, source) assert_match(/^middle line$/, source) assert_match(/^last line of grandchild$/, source) assert_match(/^last line$/, source) end test 'should invoke tree processors after parsing document' do input = <<~'EOS' = Document Title Doc Writer content EOS begin Asciidoctor::Extensions.register do tree_processor ReplaceAuthorTreeProcessor end doc = document_from_string input assert_equal 'Ghost Writer', doc.author ensure Asciidoctor::Extensions.unregister_all end end test 'should set source_location on document before invoking tree processors' do begin Asciidoctor::Extensions.register do tree_processor do process do |doc| para = create_paragraph doc.blocks.last.parent, %(file: #{doc.file}, lineno: #{doc.lineno}), {} doc << para end end end sample_doc = fixture_path 'sample.adoc' doc = Asciidoctor.load_file sample_doc, sourcemap: true assert_includes doc.convert, 'file: sample.adoc, lineno: 1' ensure Asciidoctor::Extensions.unregister_all end end test 'should allow tree processor to replace tree' do input = <<~'EOS' = Original Document Doc Writer content EOS begin Asciidoctor::Extensions.register do tree_processor ReplaceTreeTreeProcessor end doc = document_from_string input assert_equal 'Replacement Document', doc.doctitle ensure Asciidoctor::Extensions.unregister_all end end test 'should honor block title assigned in tree processor' do input = <<~'EOS' = Document Title :!example-caption: .Old block title ==== example block content ==== EOS old_title = nil begin Asciidoctor::Extensions.register do tree_processor do process do |doc| ex = (doc.find_by context: :example)[0] old_title = ex.title ex.title = 'New block title' end end end doc = document_from_string input assert_equal 'Old block title', old_title assert_equal 'New block title', (doc.find_by context: :example)[0].title ensure Asciidoctor::Extensions.unregister_all end end test 'should be able to register preferred tree processor' do begin Asciidoctor::Extensions.register do tree_processor do process do |doc| doc << (create_paragraph doc, 'd', {}) nil end end tree_processor do prefer process do |doc| doc << (create_paragraph doc, 'c', {}) nil end end prefer :tree_processor do process do |doc| doc << (create_paragraph doc, 'b', {}) nil end end prefer (tree_processor do process do |doc| doc << (create_paragraph doc, 'a', {}) nil end end) prefer :tree_processor, SelfSigningTreeProcessor end (doc = empty_document).convert assert_equal %w(SelfSigningTreeProcessor a b c d), doc.blocks.map {|b| b.lines[0] } ensure Asciidoctor::Extensions.unregister_all end end test 'should invoke postprocessors after converting document' do input = <<~'EOS' * one * two * three EOS begin Asciidoctor::Extensions.register do postprocessor StripAttributesPostprocessor end output = convert_string input refute_match(/
'
end
test 'should create an image block if mandatory attributes are provided' do
input = 'cat_in_sink::30[cat in sink (yes)]'
doc = document_from_string input, header_footer: false, extension_registry: create_cat_in_sink_block_macro
image = doc.blocks[0]
assert_equal 'cat in sink (yes)', (image.attr 'alt')
refute(image.attr? 'default-alt')
output = doc.convert
assert_includes output, '
'
end
test 'should not assign caption on image block if title is not set on custom block macro' do
input = 'cat_in_sink::30[]'
doc = document_from_string input, header_footer: false, extension_registry: create_cat_in_sink_block_macro
output = doc.convert
assert_xpath '/*[@class="imageblock"]/*[@class="title"]', output, 0
end
test 'should assign caption on image block if title is set on custom block macro' do
input = <<~'EOS'
.Cat in Sink?
cat_in_sink::30[]
EOS
doc = document_from_string input, header_footer: false, extension_registry: create_cat_in_sink_block_macro
output = doc.convert
assert_xpath '/*[@class="imageblock"]/*[@class="title"][text()="Figure 1. Cat in Sink?"]', output, 1
end
test 'should assign id and role on list items unordered' do
input = 'santa_list::ulist[]'
doc = document_from_string input, header_footer: false, extension_registry: create_santa_list_block_macro
output = doc.convert
assert_xpath '/div[@class="ulist"]/ul/li[@class="friendly"][@id="santa-list-guillaume"]', output, 1
assert_xpath '/div[@class="ulist"]/ul/li[@class="kind contributor java"]', output, 1
assert_xpath '/div[@class="ulist"]/ul/li[@class="kind contributor java"][not(@id)]', output, 1
assert_xpath '/div[@class="ulist"]/ul/li[@id="santa-list-pepijn"][not(@class)]', output, 1
assert_xpath '/div[@class="ulist"]/ul/li[@id="santa-list-dan"][@class="naughty"]', output, 1
assert_xpath '/div[@class="ulist"]/ul/li[not(@id)][not(@class)]/p[text()="Sarah"]', output, 1
end
test 'should assign id and role on list items ordered' do
input = 'santa_list::olist[]'
doc = document_from_string input, header_footer: false, extension_registry: create_santa_list_block_macro
output = doc.convert
assert_xpath '/div[@class="olist"]/ol/li[@class="friendly"][@id="santa-list-guillaume"]', output, 1
assert_xpath '/div[@class="olist"]/ol/li[@class="kind contributor java"]', output, 1
assert_xpath '/div[@class="olist"]/ol/li[@class="kind contributor java"][not(@id)]', output, 1
assert_xpath '/div[@class="olist"]/ol/li[@id="santa-list-pepijn"][not(@class)]', output, 1
assert_xpath '/div[@class="olist"]/ol/li[@id="santa-list-dan"][@class="naughty"]', output, 1
assert_xpath '/div[@class="olist"]/ol/li[not(@id)][not(@class)]/p[text()="Sarah"]', output, 1
end
end
end