diff options
| author | Dan Allen <dan.j.allen@gmail.com> | 2014-02-16 01:26:04 -0700 |
|---|---|---|
| committer | Dan Allen <dan.j.allen@gmail.com> | 2014-02-16 01:26:04 -0700 |
| commit | 16b64087bc066b99e33976ed4901ec83bb3bbbb1 (patch) | |
| tree | 18de00f589a6c0f1ea014d1ae859000f2c59a642 | |
| parent | 77091bf96780c738027a1846f4a8a0cc8809b8dd (diff) | |
| parent | 02b956f70a129291bb63a1c04a03b6c08c61323c (diff) | |
Merge pull request #890 from mojavelinux/issue-804
resolves #804 rewrite extensions to support extension instances
| -rw-r--r-- | Rakefile | 4 | ||||
| -rw-r--r--[-rwxr-xr-x] | lib/asciidoctor.rb | 4 | ||||
| -rw-r--r-- | lib/asciidoctor/abstract_block.rb | 20 | ||||
| -rw-r--r-- | lib/asciidoctor/abstract_node.rb | 6 | ||||
| -rw-r--r-- | lib/asciidoctor/backends/html5.rb | 8 | ||||
| -rw-r--r-- | lib/asciidoctor/block.rb | 29 | ||||
| -rw-r--r-- | lib/asciidoctor/cli/options.rb | 27 | ||||
| -rw-r--r-- | lib/asciidoctor/document.rb | 67 | ||||
| -rw-r--r-- | lib/asciidoctor/extensions.rb | 1405 | ||||
| -rw-r--r-- | lib/asciidoctor/helpers.rb | 1 | ||||
| -rw-r--r-- | lib/asciidoctor/lexer.rb | 235 | ||||
| -rw-r--r-- | lib/asciidoctor/reader.rb | 16 | ||||
| -rw-r--r-- | lib/asciidoctor/substitutors.rb | 52 | ||||
| -rw-r--r-- | test/extensions_test.rb | 316 | ||||
| -rw-r--r-- | test/reader_test.rb | 42 |
15 files changed, 1603 insertions, 629 deletions
@@ -65,11 +65,11 @@ begin # Prevent YARD from breaking command statements in literal paragraphs class CommandBlockPostprocessor < Asciidoctor::Extensions::Postprocessor - def process output + def process document, output output.gsub(/<pre>\$ (.+?)<\/pre>/m, '<pre class="command code"><span class="const">$</span> \1</pre>') end end - Asciidoctor::Extensions.register do |doc| + Asciidoctor::Extensions.register do postprocessor CommandBlockPostprocessor end diff --git a/lib/asciidoctor.rb b/lib/asciidoctor.rb index c2f465cd..bbe07d95 100755..100644 --- a/lib/asciidoctor.rb +++ b/lib/asciidoctor.rb @@ -733,7 +733,9 @@ module Asciidoctor # # gist::123456[] # - GenericBlockMacroRx = /^(\w[\w\-]*)::(\S+?)\[((?:\\\]|[^\]])*?)\]$/ + #-- + # NOTE we've relaxed the match for target to accomodate the short format (e.g., name::[attrlist]) + GenericBlockMacroRx = /^(\w[\w\-]*)::(\S*?)\[((?:\\\]|[^\]])*?)\]$/ # Matches an image, video or audio block macro. # diff --git a/lib/asciidoctor/abstract_block.rb b/lib/asciidoctor/abstract_block.rb index e2025b26..0d904437 100644 --- a/lib/asciidoctor/abstract_block.rb +++ b/lib/asciidoctor/abstract_block.rb @@ -28,6 +28,7 @@ class AbstractBlock < AbstractNode super @content_model = :compound @subs = [] + @default_subs = nil @template_name = %(block_#{context}) @blocks = [] @id = nil @@ -200,26 +201,21 @@ class AbstractBlock < AbstractNode # # returns nothing def assign_caption(caption = nil, key = nil) - unless title? || @caption.nil? - return nil - end + return unless title? || !@caption - if caption.nil? - if @document.attributes.has_key? 'caption' - @caption = @document.attributes['caption'] + if caption + @caption = caption + else + if (value = @document.attributes['caption']) + @caption = value elsif title? key ||= @context.to_s caption_key = "#{key}-caption" - if @document.attributes.has_key? caption_key - caption_title = @document.attributes["#{key}-caption"] + if (caption_title = @document.attributes[caption_key]) caption_num = @document.counter_increment("#{key}-number", self) @caption = "#{caption_title} #{caption_num}. " end - else - @caption = caption end - else - @caption = caption end nil end diff --git a/lib/asciidoctor/abstract_node.rb b/lib/asciidoctor/abstract_node.rb index da6288d5..3219ccb1 100644 --- a/lib/asciidoctor/abstract_node.rb +++ b/lib/asciidoctor/abstract_node.rb @@ -385,7 +385,7 @@ class AbstractNode # # returns the resolved String path def normalize_web_path(target, start = nil) - PathResolver.new.web_path(target, start) + (@path_resolver ||= PathResolver.new).web_path(target, start) end # Public: Resolve and normalize a secure path from the target and start paths @@ -420,7 +420,7 @@ class AbstractNode if jail.nil? && @document.safe >= SafeMode::SAFE jail = @document.base_dir end - PathResolver.new.system_path(target, start, jail, opts) + (@path_resolver ||= PathResolver.new).system_path(target, start, jail, opts) end # Public: Normalize the asset file or directory to a concrete and rinsed path @@ -434,7 +434,7 @@ class AbstractNode # Public: Calculate the relative path to this absolute filename from the Document#base_dir def relative_path(filename) - PathResolver.new.relative_path filename, @document.base_dir + (@path_resolver ||= PathResolver.new).relative_path filename, @document.base_dir end # Public: Retrieve the list marker keyword for the specified list type. diff --git a/lib/asciidoctor/backends/html5.rb b/lib/asciidoctor/backends/html5.rb index b425defe..5426a126 100644 --- a/lib/asciidoctor/backends/html5.rb +++ b/lib/asciidoctor/backends/html5.rb @@ -634,8 +634,8 @@ class BlockOpenTemplate < BaseTemplate warn 'asciidoctor: WARNING: abstract block cannot be used in a document without a title when doctype is book. Excluding block content.' '' else - %(<div#{id && " id=\"#{id}\""} class="quoteblock abstract#{role && " #{role}"}">#{title && -"<div class=\"title\">#{title}</div>"} + %(<div#{id && " id=\"#{id}\""} class="quoteblock abstract#{role && " #{role}"}">#{title && " +<div class=\"title\">#{title}</div>"} <blockquote> #{content} </blockquote> @@ -645,8 +645,8 @@ class BlockOpenTemplate < BaseTemplate warn 'asciidoctor: ERROR: partintro block can only be used when doctype is book and it\'s a child of a book part. Excluding block content.' '' else - %(<div#{id && " id=\"#{id}\""} class="openblock#{style != 'open' ? " #{style}" : ''}#{role && " #{role}"}">#{title && -"<div class=\"title\">#{title}</div>"} + %(<div#{id && " id=\"#{id}\""} class="openblock#{style && style != 'open' ? " #{style}" : ''}#{role && " #{role}"}">#{title && " +<div class=\"title\">#{title}</div>"} <div class="content"> #{content} </div> diff --git a/lib/asciidoctor/block.rb b/lib/asciidoctor/block.rb index cb11bc1f..f6fa772c 100644 --- a/lib/asciidoctor/block.rb +++ b/lib/asciidoctor/block.rb @@ -8,6 +8,20 @@ module Asciidoctor # => "<em>This</em> is a <test>" class Block < AbstractBlock + DEFAULT_CONTENT_MODEL = ::Hash.new(:simple).merge({ + # TODO should probably fill in all known blocks + :audio => :empty, + :image => :empty, + :listing => :verbatim, + :literal => :verbatim, + :math => :raw, + :open => :compound, + :page_break => :empty, + :pass => :raw, + :ruler => :empty, + :video => :empty + }) + # Public: Create alias for context to be consistent w/ AsciiDoc alias :blockname :context @@ -27,10 +41,19 @@ class Block < AbstractBlock # QUESTION should we store source_data as lines for blocks that have compound content models? def initialize(parent, context, opts = {}) super(parent, context) - @content_model = opts[:content_model] || :simple + @content_model = opts[:content_model] || DEFAULT_CONTENT_MODEL[context] @attributes = opts[:attributes] || {} - @subs = opts[:subs] || [] - raw_source = opts.fetch(:source, nil) || nil + if opts.has_key? :subs + # FIXME this is a bit funky + # we have to be defensive to avoid lock_in_subs wiping out the override + if !(subs = opts[:subs]) || (subs.is_a? ::Array) + @subs = subs || [] + @default_subs = @subs.dup + @attributes.delete('subs') + else + @attributes['subs'] = %(#{subs}) + end + end if !(raw_source = opts[:source]) @lines = [] elsif raw_source.is_a? ::String diff --git a/lib/asciidoctor/cli/options.rb b/lib/asciidoctor/cli/options.rb index b93bd935..84118a83 100644 --- a/lib/asciidoctor/cli/options.rb +++ b/lib/asciidoctor/cli/options.rb @@ -23,6 +23,7 @@ module Asciidoctor self[:eruby] = options[:eruby] || nil self[:compact] = options[:compact] || false self[:verbose] = options[:verbose] || 1 + self[:requires] = options[:requires] || nil self[:base_dir] = options[:base_dir] self[:destination_dir] = options[:destination_dir] || nil self[:trace] = false @@ -108,6 +109,11 @@ Example: asciidoctor -b html5 source.asciidoc opts.on('-D', '--destination-dir DIR', 'destination output directory (default: directory of source file)') do |dest_dir| self[:destination_dir] = dest_dir end + opts.on('-r', '--require LIBRARY', ::Array, + 'require the specified library before executing the processor (calls Kernel::require)', + 'may be specified more than once') do |paths| + self[:requires] = paths + end opts.on('-q', '--quiet', 'suppress warnings (default: false)') do |verbose| self[:verbose] = 0 end @@ -176,7 +182,7 @@ Example: asciidoctor -b html5 source.asciidoc self[:input_files] = infiles - if !self[:template_dirs].nil? + if self[:template_dirs] begin require 'tilt' rescue ::LoadError @@ -185,12 +191,27 @@ Example: asciidoctor -b html5 source.asciidoc end end + if (requires = self[:requires]) + requires.each do |path| + begin + require path + rescue ::LoadError => e + raise e if self[:trace] + $stderr.puts %(asciidoctor: FAILED: '#{path}' could not be loaded) + $stderr.puts ' Use --trace for backtrace' + return 1 + rescue ::SystemExit + # not permitted here + end + end + end + rescue ::OptionParser::MissingArgument - $stderr.puts "asciidoctor: option #{$!.message}" + $stderr.puts %(asciidoctor: option #{$!.message}) $stdout.puts opts_parser return 1 rescue ::OptionParser::InvalidOption, ::OptionParser::InvalidArgument - $stderr.puts "asciidoctor: #{$!.message}" + $stderr.puts %(asciidoctor: #{$!.message}) $stdout.puts opts_parser return 1 end diff --git a/lib/asciidoctor/document.rb b/lib/asciidoctor/document.rb index ab050341..55449a8a 100644 --- a/lib/asciidoctor/document.rb +++ b/lib/asciidoctor/document.rb @@ -154,7 +154,7 @@ class Document < AbstractBlock @attribute_overrides = overrides @safe = nil @renderer = nil - initialize_extensions = defined?(::Asciidoctor::Extensions) + initialize_extensions = defined? ::Asciidoctor::Extensions @extensions = nil # initialize furthur down end @@ -165,7 +165,7 @@ class Document < AbstractBlock @options = options unless @parent_document # safely resolve the safe mode from const, int or string - if @safe.nil? && !(safe_mode = @options[:safe]) + if !@safe && !(safe_mode = options[:safe]) @safe = SafeMode::SECURE elsif safe_mode.is_a?(::Fixnum) # be permissive in case API user wants to define new levels @@ -178,14 +178,14 @@ class Document < AbstractBlock end end end - @options[:header_footer] ||= false + options[:header_footer] ||= false @attributes['encoding'] = 'UTF-8' @attributes['sectids'] = '' - @attributes['notitle'] = '' unless @options[:header_footer] + @attributes['notitle'] = '' unless options[:header_footer] @attributes['toc-placement'] = 'auto' @attributes['stylesheet'] = '' - @attributes['copycss'] = '' if @options[:header_footer] + @attributes['copycss'] = '' if options[:header_footer] @attributes['prewrap'] = '' @attributes['attribute-undefined'] = Compliance.attribute_undefined @attributes['attribute-missing'] = Compliance.attribute_missing @@ -217,7 +217,7 @@ class Document < AbstractBlock @attribute_overrides['safe-mode-level'] = @safe # sync the embedded attribute w/ the value of options...do not allow override - @attribute_overrides['embedded'] = @options[:header_footer] ? nil : '' + @attribute_overrides['embedded'] = options[:header_footer] ? nil : '' # the only way to set the max-include-depth attribute is via the document options # 64 is the AsciiDoc default @@ -233,7 +233,7 @@ class Document < AbstractBlock # if the base_dir option is specified, it overrides docdir as the root for relative paths # otherwise, the base_dir is the directory of the source file (docdir) or the current # directory of the input is a string - if @options[:base_dir].nil? + if !options[:base_dir] if @attribute_overrides['docdir'] @base_dir = @attribute_overrides['docdir'] = ::File.expand_path(@attribute_overrides['docdir']) else @@ -241,16 +241,16 @@ class Document < AbstractBlock @base_dir = @attribute_overrides['docdir'] = ::File.expand_path(::Dir.pwd) end else - @base_dir = @attribute_overrides['docdir'] = ::File.expand_path(@options[:base_dir]) + @base_dir = @attribute_overrides['docdir'] = ::File.expand_path(options[:base_dir]) end # allow common attributes backend and doctype to be set using options hash - unless @options[:backend].nil? - @attribute_overrides['backend'] = @options[:backend].to_s + if (value = options[:backend]) + @attribute_overrides['backend'] = %(#{value}) end - unless @options[:doctype].nil? - @attribute_overrides['doctype'] = @options[:doctype].to_s + if (value = options[:doctype]) + @attribute_overrides['doctype'] = %(#{value}) end if @safe >= SafeMode::SERVER @@ -326,12 +326,23 @@ class Document < AbstractBlock @attributes['stylesdir'] ||= '.' @attributes['iconsdir'] ||= File.join(@attributes.fetch('imagesdir', './images'), 'icons') - @extensions = initialize_extensions ? Extensions::Registry.new(self) : nil + @extensions = if initialize_extensions + registry = if (ext_registry = options[:extensions_registry]) + if (ext_registry.is_a? Extensions::Registry) || + (::RUBY_ENGINE_JRUBY && (ext_registry.is_a? ::AsciidoctorJ::Extensions::ExtensionRegistry)) + ext_registry + end + elsif (ext_block = options[:extensions]) && (ext_block.is_a? ::Proc) + Extensions.build_registry(&ext_block) + end + (registry ||= Extensions::Registry.new).activate self + end + @reader = PreprocessorReader.new self, data, Reader::Cursor.new(@attributes['docfile'], @base_dir) if @extensions && @extensions.preprocessors? - @extensions.load_preprocessors(self).each do |processor| - @reader = processor.process(@reader, @reader.lines) || @reader + @extensions.preprocessors.each do |ext| + @reader = ext.process_method[@reader, @reader.lines] || @reader end end else @@ -341,13 +352,13 @@ class Document < AbstractBlock end # Now parse the lines in the reader into blocks - Lexer.parse(@reader, self, :header_only => @options.fetch(:parse_header_only, false)) + Lexer.parse @reader, self, :header_only => !!options[:parse_header_only] @callouts.rewind - if !@parent_document && @extensions && @extensions.treeprocessors? - @extensions.load_treeprocessors(self).each do |processor| - processor.process + if @extensions && !@parent_document && @extensions.treeprocessors? + @extensions.treeprocessors.each do |ext| + ext.process_method[self] end end end @@ -482,7 +493,7 @@ class Document < AbstractBlock elsif (sect = first_section) && sect.title? val = sect.title else - return nil + return end if opts[:sanitize] && val.include?('<') @@ -780,10 +791,10 @@ class Document < AbstractBlock restore_attributes r = renderer(opts) - # QUESTION should we add Preserializeprocessors? is it the right name? - #if !@parent_document && @extensions && @extensions.preserializeprocessors? - # @extensions.load_preserializeprocessors(self).each do |processor| - # processor.process r + # QUESTION should we add Prerenderprocessors? is it the right name? + #if @extensions && !@parent_document && @extensions.prerenderprocessors? + # @extensions.prerenderprocessors.each do |ext| + # ext.process_method[self, r] # end #end @@ -798,13 +809,13 @@ class Document < AbstractBlock output = @options.merge(opts)[:header_footer] ? r.render('document', self).strip : r.render('embedded', self) end - if !@parent_document && @extensions + if @extensions && !@parent_document if @extensions.postprocessors? - @extensions.load_postprocessors(self).each do |processor| - output = processor.process output + @extensions.postprocessors.each do |ext| + output = ext.process_method[self, output] end end - @extensions.reset + #@extensions.reset end output diff --git a/lib/asciidoctor/extensions.rb b/lib/asciidoctor/extensions.rb index 89f79b3c..d76504b6 100644 --- a/lib/asciidoctor/extensions.rb +++ b/lib/asciidoctor/extensions.rb @@ -1,443 +1,1232 @@ module Asciidoctor +# Extensions provide a way to participate in the parsing and rendering +# phases of the AsciiDoc processor or extend the AsciiDoc syntax. +# +# The various extensions participate in AsciiDoc processing as follows: +# +# 1. After the source lines are normalized, {Preprocessor}s modify or replace +# the source lines before parsing begins. {IncludeProcessor}s are used to +# process include directives for targets which they claim to handle. +# 2. The Parser parses the block-level content into an abstract syntax tree. +# Custom blocks and block macros are processed by associated {BlockProcessor}s +# and {BlockMacroProcessor}s, respectively. +# 3. {Treeprocessor}s are run on the abstract syntax tree. +# 4. Rendering of the document begins, at which point inline markup is processed +# and rendered. Custom inline macros are processed by associated {InlineMacroProcessor}s. +# 5. {Postprocessor}s modify or replace the rendered document. +# 6. The output is written to the output stream. +# +# Extensions may be registered globally using the {Extensions.register} method +# or added to a custom {Registry} instance and passed as an option to a single +# Asciidoctor processor. module Extensions - class Extension + + # Public: An abstract base class for document and syntax processors. + # + # This class provides access to a class-level Hash for holding default + # configuration options defined using the {Processor.option} method. This + # style of default configuration is specific to the native Ruby environment + # and is only consulted inside the initializer. An overriding configuration + # Hash can be passed to the initializer. Once the processor is initialized, + # the configuration is accessed using the {Processor#config} instance variable. + # + # Instances of the Processor class provide convenience methods for creating + # AST nodes, such as Block and Inline, and for parsing child content. + class Processor class << self - def register - ::Asciidoctor::Extensions.register self + # Public: Get the static configuration for this processor class. + # + # Returns a configuration [Hash] + def config + @config ||= {} + end + + # Public: Assigns a default value for the specified option that gets + # applied to all instances of this processor. + # + # Examples + # + # option :contexts, [:open, :paragraph] + # + # Returns nothing + def option key, default_value + config[key] = default_value end - def activate registry, document + # Include the DSL class for this processor into this processor class or instance. + # + # This method automatically detects whether to use the include or extend keyword + # based on what is appropriate. + # + # Returns nothing + def use_dsl + if self.name.nil_or_empty? + # NOTE contants(false) doesn't exist in Ruby 1.8.7 + #include const_get :DSL if constants(false).grep :DSL + include const_get :DSL if constants.grep :DSL + else + # NOTE contants(false) doesn't exist in Ruby 1.8.7 + #extend const_get :DSL if constants(false).grep :DSL + extend const_get :DSL if constants.grep :DSL + end end + alias :extend_dsl :use_dsl + alias :include_dsl :use_dsl end - end - class << self - def registered? - !@registered.nil? + # Public: Get the configuration Hash for this processor instance. + attr_reader :config + + def initialize config = {} + @config = self.class.config.merge config end - def registered - @registered ||= [] + def update_config config + @config.update config end - # QUESTION should we require extensions to have names? - # how about autogenerate name for class, assume extension - # is name of block if block is given - # having a name makes it easier to unregister an extension - def register extension = nil, &block - if block_given? - registered << block - elsif extension - registered << resolve_class(extension) - end + def process *args + raise ::NotImplementedError end - def resolve_class(object) - object.is_a?(::Class) ? object : class_for_name(object.to_s) + def create_block parent, context, source, attrs, opts = {} + Block.new parent, context, { :source => source, :attributes => attrs }.merge(opts) end - def class_for_name(qualified_name) - qualified_name.split('::').inject(::Object) do |module_, name| - if name.empty? - module_ - elsif module_.const_defined? name - module_.const_get(name) - else - raise "Could not resolve class for name: #{qualified_name}" - end + def create_image_block parent, attrs, opts = {} + create_block parent, :image, nil, attrs, opts + end + + def create_inline parent, context, text, opts = {} + Inline.new parent, context, text, opts + end + + # Public: Parses blocks in the content and attaches the block to the parent. + # + # Returns nothing + #-- + # QUESTION is parse_content the right method name? should we wrap in open block automatically? + def parse_content parent, content, attributes = {} + reader = (content.is_a? Reader) ? reader : (Reader.new content) + while reader.has_more_lines? + block = Lexer.next_block(reader, parent, attributes) + parent << block if block end + nil end - def unregister_all - @registered = [] + # TODO fill out remaining methods + [ + [:create_paragraph, :create_block, :paragraph], + [:create_open_block, :create_block, :open], + [:create_example_block, :create_block, :example], + [:create_pass_block, :create_block, :pass], + [:create_listing_block, :create_block, :listing], + [:create_literal_block, :create_block, :literal], + [:create_anchor, :create_inline, :anchor] + ].each do |method_name, delegate_method_name, context| + define_method method_name do |*args| + send delegate_method_name, *args.dup.insert(1, context) + end end end - class Registry - attr_accessor :preprocessors - attr_accessor :treeprocessors - attr_accessor :postprocessors - attr_accessor :include_processors - attr_accessor :blocks - attr_accessor :block_macros - attr_accessor :inline_macros - - def initialize document = nil - @preprocessors = [] - @treeprocessors = [] - @postprocessors = [] - @include_processors = [] - @include_processor_cache = {} - @block_delimiters = {} - @blocks = {} - @block_processor_cache = {} - @block_macros = {} - @block_macro_processor_cache = {} - @inline_macros = {} - @inline_macro_processor_cache = {} - - Extensions.registered.each do |extension| - if extension.is_a? ::Proc - register document, &extension - else - extension.activate self, document - end - end - end - - def preprocessor processor, position = :<< - processor = resolve_processor_class processor - if position == :<< || @preprocessors.empty? - @preprocessors.push processor - elsif position == :>> - @preprocessors.unshift processor + # Internal: Overlays a builder DSL for configuring the Processor instance. + # Includes a method to define configuration options and another to define the + # {Processor#process} method. + module ProcessorDsl + def option key, value + config[key] = value + end + + def process *args, &block + # need to check for both block/proc and lambda + # TODO need test for this! + #if block_given? || (args.size == 1 && ((block = args[0]).is_a? ::Proc)) + if block_given? + @process_block = block + elsif @process_block + # NOTE Proc automatically expands a single array argument + # ...but lambda doesn't (and we want to accept lambdas too) + # TODO need a test for this! + @process_block.call(*args) else - @preprocessors.push processor + raise ::NotImplementedError end end + #alias :process_with :process - def preprocessors? - !@preprocessors.empty? + def process_block_given? + defined? @process_block end + end - def load_preprocessors *args - @preprocessors.map do |processor| - processor.new(*args) - end + # Public: Preprocessors are run after the source text is split into lines and + # normalized, but before parsing begins. + # + # Prior to invoking the preprocessor, Asciidoctor splits the source text into + # lines and normalizes them. The normalize process strips trailing whitespace + # from each line and leaves behind a line-feed character (i.e., "\n"). + # + # Asciidoctor passes a reference to the Reader and a copy of the lines Array + # to the {Processor#process} method of an instance of each registered + # Preprocessor. The Preprocessor modifies the Array as necessary and either + # returns a reference to the same Reader or a reference to a new Reader. + # + # Preprocessor implementations must extend the Preprocessor class. + class Preprocessor < Processor + def process reader, lines + raise ::NotImplementedError + end + end + Preprocessor::DSL = ProcessorDsl + + # Public: Treeprocessors are run on the Document after the source has been + # parsed into an abstract syntax tree (AST), as represented by the Document + # object and its child Node objects (e.g., Section, Block, List, ListItem). + # + # Asciidoctor invokes the {Processor#process} method on an instance of each + # registered Treeprocessor. + # + # Treeprocessor implementations must extend Treeprocessor. + #-- + # QUESTION should the treeprocessor get invoked after parse header too? + class Treeprocessor < Processor + def process document + raise ::NotImplementedError + end + end + Treeprocessor::DSL = ProcessorDsl + + # Public: Postprocessors are run after the document is rendered, but before + # it is written to the output stream. + # + # Asciidoctor passes a reference to the rendered String to the {Processor#process} + # method of each registered Postprocessor. The Preprocessor modifies the + # String as necessary and returns the String replacement. + # + # The markup format in the String is determined by the backend used to render + # the Document. The backend and be looked up using the backend method on the + # Document object, as well as various backend-related document attributes. + # + # TIP: Postprocessors can also be used to relocate assets needed by the published + # document. + # + # Postprocessor implementations must Postprocessor. + class Postprocessor < Processor + def process document, output + raise ::NotImplementedError + end + end + Postprocessor::DSL = ProcessorDsl + + # Public: IncludeProcessors are used to process `include::<target>[]` + # directives in the source document. + # + # When Asciidoctor comes across a `include::<target>[]` directive in the + # source document, it iterates through the IncludeProcessors and delegates + # the work of reading the content to the first processor that identifies + # itself as capable of handling that target. + # + # IncludeProcessor implementations must extend IncludeProcessor. + #-- + # TODO add file extension or regexp to shortcut handles? + class IncludeProcessor < Processor + def process reader, target, attributes + raise ::NotImplementedError + end + + def handles? target + true end + end + IncludeProcessor::DSL = ProcessorDsl + + # Public: BlockProcessors are used to handle delimited blocks and paragraphs + # that have a custom name. + # + # When Asciidoctor encounters a delimited block or paragraph with an + # unrecognized name while parsing the document, it looks for a BlockProcessor + # registered to handle this name and, if found, invokes its {Processor#process} + # method to build a cooresponding node in the document tree. + # + # AsciiDoc example: + # + # [shout] + # Get a move on. + # + # Recognized options: + # + # * :contexts - The blocks contexts on which this style can be used (default: [:paragraph, :open] + # * :content_model - The structure of the content supported in this block (default: :compound) + # * :pos_attrs - A list of attribute names used to map positional attributes (default: nil) + # * ... + # + # BlockProcessor implementations must extend BlockProcessor. + class BlockProcessor < Processor + attr :name - def treeprocessor processor, position = :<< - processor = resolve_processor_class processor - if position == :<< || @treeprocessors.empty? - @treeprocessors.push processor - elsif position == :>> - @treeprocessors.unshift processor + def initialize name = nil, config = {} + super config + @name = name || @config[:name] + # assign fallbacks + case @config[:contexts] + when ::NilClass + @config[:contexts] ||= [:open, :paragraph].to_set + when ::Symbol + @config[:contexts] = [@config[:contexts]].to_set else - @treeprocessors.push processor + @config[:contexts] = @config[:contexts].to_set end + # QUESTION should the default content model be raw?? + @config[:content_model] ||= :compound end - def treeprocessors? - !@treeprocessors.empty? + def process parent, reader, attributes + raise ::NotImplementedError end + end - def load_treeprocessors *args - @treeprocessors.map do |processor| - processor.new(*args) + module BlockProcessorDsl + include ProcessorDsl + + # FIXME this isn't the prettiest thing + def named value + if self.is_a? Processor + @name = value + else + option :name, value end end + alias :match_name :named + alias :bind_to :named + + def contexts *value + option :contexts, value.flatten + end + alias :on_contexts :contexts + alias :on_context :contexts + + def content_model value + option :content_model, value + end + alias :parse_content_as :content_model - def postprocessor processor, position = :<< - processor = resolve_processor_class processor - if position == :<< || @postprocessors.empty? - @postprocessors.push processor - elsif position == :>> - @postprocessors.unshift processor + def pos_attrs *value + option :pos_attrs, value.flatten + end + alias :map_attributes :pos_attrs + alias :name_positional_attributes :pos_attrs + + def default_attrs value + option :default_attrs, value + end + alias :seed_attributes_with :default_attrs + end + BlockProcessor::DSL = BlockProcessorDsl + + class MacroProcessor < Processor + attr :name + + def initialize name = nil, config = {} + super config + @name = name || @config[:name] + @config[:content_model] ||= :attributes + end + + def process parent, target, attributes + raise ::NotImplementedError + end + end + + module MacroProcessorDsl + include ProcessorDsl + # QUESTION perhaps include a SyntaxDsl? + + def named value + if self.is_a? Processor + @name = value else - @postprocessors.push processor + option :name, value end end + alias :match_name :named + alias :bind_to :named - def postprocessors? - !@postprocessors.empty? + def content_model value + option :content_model, value end + alias :parse_content_as :content_model - def load_postprocessors *args - @postprocessors.map do |processor| - processor.new(*args) - end + def pos_attrs *value + option :pos_attrs, value.flatten + end + alias :map_attributes :pos_attrs + alias :name_positional_attributes :pos_attrs + + def default_attrs value + option :default_attrs, value + end + alias :seed_attributes_with :default_attrs + end + + # Public: BlockMacroProcessors are used to handle block macros that have a + # custom name. + # + # BlockMacroProcessor implementations must extend BlockMacroProcessor. + class BlockMacroProcessor < MacroProcessor + end + BlockMacroProcessor::DSL = MacroProcessorDsl + + # Public: InlineMacroProcessors are used to handle block macros that have a + # custom name. + # + # InlineMacroProcessor implementations must extend InlineMacroProcessor. + #-- + # TODO break this out into different pattern types + # for example, FormalInlineMacro, ShortInlineMacro (no target) and other patterns + # FIXME for inline passthrough, we need to have some way to specify the text as a passthrough + class InlineMacroProcessor < MacroProcessor + def initialize name, config = {} + super + @config[:regexp] ||= (resolve_regexp @name, @config[:format]) end - def include_processor processor, position = :<< - processor = resolve_processor_class processor - if position == :<< || @include_processors.empty? - @include_processors.push processor - elsif position == :>> - @include_processors.unshift processor + def resolve_regexp name, format + # TODO memoize these regular expressions! + if format == :short + %r(\\?#{name}:\[((?:\\\]|[^\]])*?)\]) else - @include_processors.push processor + %r(\\?#{name}:(\S+?)\[((?:\\\]|[^\]])*?)\]) end end + end - def include_processors? - !@include_processors.empty? + module InlineMacroProcessorDsl + include MacroProcessorDsl + + def using_format value + option :format, value end - def load_include_processors *args - @include_processors.map do |processor| - processor.new(*args) - end - # QUESTION do we need/want the cache? - #@include_processors.map do |processor| - # @include_processor_cache[processor] ||= processor.new(*args) - #end + def match value + option :regexp, value end + end + InlineMacroProcessor::DSL = InlineMacroProcessorDsl - # TODO allow contexts to be specified here, perhaps as [:upper, [:paragraph, :sidebar]] - def block name, processor, delimiter = nil, &block - processor = resolve_processor_class processor - @blocks[name] = processor - if block_given? - @block_delimiters[block] = name - elsif delimiter && delimiter.is_a?(::Regexp) - @block_delimiters[delimiter] = name + # Public: Extension is a proxy object for an extension implementation such as + # a processor. It allows the preparation of the extension instance to be + # separated from its usage to provide consistency between different + # interfaces and avoid tight coupling with the extension type. + # + # The proxy encapsulates the extension kind (e.g., :block), its config Hash + # and the extension instance. This Proxy is what gets stored in the extension + # registry when activated. + #-- + # QUESTION call this ExtensionInfo? + class Extension + attr :kind + attr :config + attr :instance + + def initialize kind, instance, config + @kind = kind + @instance = instance + @config = config + end + end + + # Public: A specialization of the Extension proxy that additionally stores a + # reference to the {Processor#process} method. By storing this reference, its + # possible to accomodate both concrete extension implementations and Procs. + class ProcessorExtension < Extension + attr :process_method + + def initialize kind, instance, process_method = nil + super kind, instance, instance.config + @process_method = process_method || instance.method(:process) + end + end + + # Public: A Group is used to register one or more extensions with the Registry. + # + # The Group should be subclassed and registered with the Registry either by + # invoking the {Group.register} method or passing the subclass to the + # {Extensions.register} method. Extensions are registered with the Registry + # inside the {Group#activate} method. + class Group + class << self + def register name = nil + Extensions.register name, self end end - def blocks? - !@blocks.empty? + def activate registry + raise ::NotImplementedError end + end - def block_delimiters? - !@block_delimiters.empty? + # Public: The primary entry point into the extension system. + # + # Registry holds the extensions which have been registered and activated, has + # methods for registering or defining a processor and looks up extensions + # stored in the registry during parsing. + class Registry + # Public: Returns the {Asciidoctor::Document} on which the extensions in this registry are being used. + attr_reader :document + + # Public: Returns the Array of {Group} classes, instances and/or Procs that have been registered. + attr_reader :groups + + def initialize groups = {} + @groups = groups + @preprocessor_extensions = @treeprocessor_extensions = @postprocessor_extensions = @include_processor_extensions = nil + @block_extensions = @block_macro_extensions = @inline_macro_extensions = nil + @document = nil end - # NOTE block delimiters not yet implemented - def at_block_delimiter? line - @block_delimiters.each do |delimiter, name| - if delimiter.is_a? ::Proc - if delimiter.call(line) - return name + # Public: Activates all the global extension {Group}s and the extension {Group}s + # associated with this registry. + # + # document - the {Asciidoctor::Document} on which the extensions are to be used. + # + # Returns the instance of this [Registry]. + def activate document + @document = document + (Extensions.groups.values + @groups.values).each do |group| + case group + when ::Proc + case group.arity + when 0, -1 + instance_exec(&group) + when 1 + group.call self end + when ::Class + group.new.activate self else - if delimiter =~ line - return name - end + group.activate self end end - false + self end - def load_block_processor name, *args - @block_processor_cache[name] ||= @blocks[name].new(name.to_sym, *args) + # Public: Registers a {Preprocessor} with the extension registry to process + # the AsciiDoc source before parsing begins. + # + # The Preprocessor may be one of four types: + # + # * A Preprocessor subclass + # * An instance of a Preprocessor subclass + # * The String name of a Preprocessor subclass + # * A method block (i.e., Proc) that conforms to the Preprocessor contract + # + # Unless the Preprocessor is passed as the method block, it must be the + # first argument to this method. + # + # Examples + # + # # as a Preprocessor subclass + # preprocessor FrontMatterPreprocessor + # + # # as an instance of a Preprocessor subclass + # preprocessor FrontMatterPreprocessor.new + # + # # as a name of a Preprocessor subclass + # preprocessor 'FrontMatterPreprocessor' + # + # # as a method block + # preprocessor do + # process |reader, lines| + # ... + # end + # end + # + # Returns the [Extension] stored in the registry that proxies the + # instance of this Preprocessor. + def preprocessor *args, &block + add_document_processor :preprocessor, args, &block end - def processor_registered_for_block? name, context - if @blocks.has_key? name.to_sym - (@blocks[name.to_sym].config.fetch(:contexts, nil) || []).include?(context) - else - false - end + # Public: Checks whether any {Preprocessor} extensions have been registered. + # + # Returns a [Boolean] indicating whether any Preprocessor extensions are registered. + def preprocessors? + !!@preprocessor_extensions end - def block_macro name, processor - processor = resolve_processor_class processor - @block_macros[name.to_s] = processor + # Public: Retrieves the {Extension} proxy objects for all + # Preprocessor instances in this registry. + # + # Returns an [Array] of Extension proxy objects. + def preprocessors + @preprocessor_extensions end - def block_macros? - !@block_macros.empty? + # Public: Registers a {Treeprocessor} with the extension registry to process + # the AsciiDoc source after parsing is complete. + # + # The Treeprocessor may be one of four types: + # + # * A Treeprocessor subclass + # * An instance of a Treeprocessor subclass + # * The String name of a Treeprocessor subclass + # * A method block (i.e., Proc) that conforms to the Treeprocessor contract + # + # Unless the Treeprocessor is passed as the method block, it must be the + # first argument to this method. + # + # Examples + # + # # as a Treeprocessor subclass + # treeprocessor ShellTreeprocessor + # + # # as an instance of a Treeprocessor subclass + # treeprocessor ShellTreeprocessor.new + # + # # as a name of a Treeprocessor subclass + # treeprocessor 'ShellTreeprocessor' + # + # # as a method block + # treeprocessor do + # process |document| + # ... + # end + # end + # + # Returns the [Extension] stored in the registry that proxies the + # instance of this Treeprocessor. + def treeprocessor *args, &block + add_document_processor :treeprocessor, args, &block end - def load_block_macro_processor name, *args - @block_macro_processor_cache[name] ||= @block_macros[name].new(name, *args) + # Public: Checks whether any {Treeprocessor} extensions have been registered. + # + # Returns a [Boolean] indicating whether any Treeprocessor extensions are registered. + def treeprocessors? + !!@treeprocessor_extensions end - def processor_registered_for_block_macro? name - @block_macros.has_key? name + # Public: Retrieves the {Extension} proxy objects for all + # Treeprocessor instances in this registry. + # + # Returns an [Array] of Extension proxy objects. + def treeprocessors + @treeprocessor_extensions end - # TODO probably need ordering control before/after other inline macros - def inline_macro name, processor - processor = resolve_processor_class processor - @inline_macros[name.to_s] = processor + # Public: Registers a {Postprocessor} with the extension registry to process + # the output after rendering is complete. + # + # The Postprocessor may be one of four types: + # + # * A Postprocessor subclass + # * An instance of a Postprocessor subclass + # * The String name of a Postprocessor subclass + # * A method block (i.e., Proc) that conforms to the Postprocessor contract + # + # Unless the Postprocessor is passed as the method block, it must be the + # first argument to this method. + # + # Examples + # + # # as a Postprocessor subclass + # postprocessor AnalyticsPostprocessor + # + # # as an instance of a Postprocessor subclass + # postprocessor AnalyticsPostprocessor.new + # + # # as a name of a Postprocessor subclass + # postprocessor 'AnalyticsPostprocessor' + # + # # as a method block + # postprocessor do + # process |document, output| + # ... + # end + # end + # + # Returns the [Extension] stored in the registry that proxies the + # instance of this Postprocessor. + def postprocessor *args, &block + add_document_processor :postprocessor, args, &block end - def inline_macros? - !@inline_macros.empty? + # Public: Checks whether any {Postprocessor} extensions have been registered. + # + # Returns a [Boolean] indicating whether any Postprocessor extensions are registered. + def postprocessors? + !!@postprocessor_extensions end - def load_inline_macro_processor name, *args - @inline_macro_processor_cache[name] ||= @inline_macros[name].new(name, *args) + # Public: Retrieves the {Extension} proxy objects for all + # Postprocessor instances in this registry. + # + # Returns an [Array] of Extension proxy objects. + def postprocessors + @postprocessor_extensions end - def load_inline_macro_processors *args - @inline_macros.map do |name, processor| - load_inline_macro_processor name, *args - end + # Public: Registers an {IncludeProcessor} with the extension registry to have + # a shot at handling the include directive. + # + # The IncludeProcessor may be one of four types: + # + # * A IncludeProcessor subclass + # * An instance of a IncludeProcessor subclass + # * The String name of a IncludeProcessor subclass + # * A method block (i.e., Proc) that conforms to the IncludeProcessor contract + # + # Unless the IncludeProcessor is passed as the method block, it must be the + # first argument to this method. + # + # Examples + # + # # as an IncludeProcessor subclass + # include_processor GitIncludeProcessor + # + # # as an instance of a Postprocessor subclass + # include_processor GitIncludeProcessor.new + # + # # as a name of a Postprocessor subclass + # include_processor 'GitIncludeProcessor' + # + # # as a method block + # include_processor do + # process |document, output| + # ... + # end + # end + # + # Returns the [Extension] stored in the registry that proxies the + # instance of this IncludeProcessor. + def include_processor *args, &block + add_document_processor :include_processor, args, &block end - def processor_registered_for_inline_macro? name - @inline_macros.has_key? name + # Public: Checks whether any {IncludeProcessor} extensions have been registered. + # + # Returns a [Boolean] indicating whether any IncludeProcessor extensions are registered. + def include_processors? + !!@include_processor_extensions end - def register document, &block - instance_exec document, &block + # Public: Retrieves the {Extension} proxy objects for all the + # IncludeProcessor instances stored in this registry. + # + # Returns an [Array] of Extension proxy objects. + def include_processors + @include_processor_extensions end - def resolve_processor_class object - ::Asciidoctor::Extensions.resolve_class object + # Public: Registers a {BlockProcessor} with the extension registry to + # process the block content (i.e., delimited block or paragraph) in the + # AsciiDoc source annotated with the specified block name (i.e., style). + # + # The BlockProcessor may be one of four types: + # + # * A BlockProcessor subclass + # * An instance of a BlockProcessor subclass + # * The String name of a BlockProcessor subclass + # * A method block (i.e., Proc) that conforms to the BlockProcessor contract + # + # Unless the BlockProcessor is passed as the method block, it must be the + # first argument to this method. The second argument is the name (coersed + # to a Symbol) of the AsciiDoc block content (i.e., delimited block or + # paragraph) that this processor is registered to handle. If a block name + # is not passed as an argument, it gets read from the name property of the + # BlockProcessor instance. If a name still cannot be determined, an error + # is raised. + # + # Examples + # + # # as a BlockProcessor subclass + # block ShoutBlock + # + # # as a BlockProcessor subclass with an explicit block name + # block ShoutBlock, :shout + # + # # as an instance of a BlockProcessor subclass + # block ShoutBlock.new + # + # # as an instance of a BlockProcessor subclass with an explicit block name + # block ShoutBlock.new, :shout + # + # # as a name of a BlockProcessor subclass + # block 'ShoutBlock' + # + # # as a name of a BlockProcessor subclass with an explicit block name + # block 'ShoutBlock', :shout + # + # # as a method block + # block do + # named :shout + # process |parent, reader, attrs| + # ... + # end + # end + # + # # as a method block with an explicit block name + # register :shout do + # process |parent, reader, attrs| + # ... + # end + # end + # + # Returns an instance of the [Extension] proxy object that is stored in the + # registry and manages the instance of this BlockProcessor. + def block *args, &block + add_syntax_processor :block, args, &block end - def reset - @block_processor_cache = {} - @block_macro_processor_cache = {} - @inline_macro_processor_cache = {} + # Public: Checks whether any {BlockProcessor} extensions have been registered. + # + # Returns a [Boolean] indicating whether any BlockProcessor extensions are registered. + def blocks? + !!@block_extensions end - end - class Processor - def initialize(document) - @document = document + # Public: Checks whether any {BlockProcessor} extensions are registered to + # handle the specified block name appearing on the specified context. + # + # Returns the [Extension] proxy object for the BlockProcessor that matches + # the block name and context or false if no match is found. + def registered_for_block? name, context + if (ext = @block_extensions[name.to_sym]) + (ext.config[:contexts].include? context) ? ext : false + else + false + end end - end - # Public: Preprocessors are run after the source text is split into lines and - # before parsing begins. - # - # Prior to invoking the preprocessor, Asciidoctor splits the source text into - # lines and normalizes them. The normalize process strips trailing whitespace - # from each line and leaves behind a line-feed character (i.e., "\n"). - # - # Asciidoctor passes a reference to the Reader and a copy of the lines Array - # to the process method of an instance of each registered Preprocessor. The - # Preprocessor modifies the Array as necessary and either returns a reference - # to the same Reader or a reference to a new one. - # - # Preprocessors must extend Asciidoctor::Extensions::Preprocessor. - class Preprocessor < Processor - # Public: Accepts the Reader and an Array of lines, modifies them as - # needed, then returns the Reader or a reference to a new one. + # Public: Retrieves the {Extension} proxy object for the BlockProcessor registered + # to handle block content with the name. # - # Each subclass of Preprocessor should override this method. - def process reader, lines - reader + # name - the String or Symbol (coersed to a Symbol) macro name + # + # Returns the [Extension] object stored in the registry that proxies the + # corresponding BlockProcessor or nil if a match is not found. + def find_block_extension name + @block_extensions[name.to_sym] end - end - # Public: Treeprocessors are run on the Document after the source has been - # parsed into an abstract syntax tree, as represented by the Document object - # and its child Node objects. - # - # Asciidoctor invokes the process method on an instance of each registered - # Treeprocessor. - # - # QUESTION should the treeprocessor get invoked after parse header too? - # - # Treeprocessors must extend Asciidoctor::Extensions::Treeprocessor. - class Treeprocessor < Processor - def process + # Public: Registers a {BlockMacroProcessor} with the extension registry to + # process a block macro with the specified name. + # + # The BlockMacroProcessor may be one of four types: + # + # * A BlockMacroProcessor subclass + # * An instance of a BlockMacroProcessor subclass + # * The String name of a BlockMacroProcessor subclass + # * A method block (i.e., Proc) that conforms to the BlockMacroProcessor contract + # + # Unless the BlockMacroProcessor is passed as the method block, it must be + # the first argument to this method. The second argument is the name + # (coersed to a Symbol) of the AsciiDoc block macro that this processor is + # registered to handle. If a block macro name is not passed as an argument, + # it gets read from the name property of the BlockMacroProcessor instance. + # If a name still cannot be determined, an error is raised. + # + # Examples + # + # # as a BlockMacroProcessor subclass + # block GistBlockMacro + # + # # as a BlockMacroProcessor subclass with an explicit macro name + # block GistBlockMacro, :gist + # + # # as an instance of a BlockMacroProcessor subclass + # block GistBlockMacro.new + # + # # as an instance of a BlockMacroProcessor subclass with an explicit macro name + # block GistBlockMacro.new, :gist + # + # # as a name of a BlockMacroProcessor subclass + # block 'GistBlockMacro' + # + # # as a name of a BlockMacroProcessor subclass with an explicit macro name + # block 'GistBlockMacro', :gist + # + # # as a method block + # block_macro do + # named :gist + # process |parent, target, attrs| + # ... + # end + # end + # + # # as a method block with an explicit macro name + # register :gist do + # process |parent, target, attrs| + # ... + # end + # end + # + # Returns an instance of the [Extension] proxy object that is stored in the + # registry and manages the instance of this BlockMacroProcessor. + def block_macro *args, &block + add_syntax_processor :block_macro, args, &block end - end - # Public: Postprocessors are run after the document is rendered and before - # it's written to the output stream. - # - # Asciidoctor passes a reference to the output String to the process method - # of each registered Postprocessor. The Preprocessor modifies the String as - # necessary and returns the String replacement. - # - # The markup format in the String is determined from the backend used to - # render the Document. The backend and be looked up using the backend method - # on the Document object, as well as various backend-related document - # attributes. - # - # Postprocessors can also be used to relocate assets needed by the published - # document. - # - # Postprocessors must extend Asciidoctor::Extensions::Postprocessor. - class Postprocessor < Processor - def process output - output + # Public: Checks whether any {BlockMacroProcessor} extensions have been registered. + # + # Returns a [Boolean] indicating whether any BlockMacroProcessor extensions are registered. + def block_macros? + !!@block_macro_extensions end - end - # Public: IncludeProcessors are used to process include::[] macros in the - # source document. - # - # When Asciidoctor discovers an include::[] macro in the source document, it - # iterates through the IncludeProcessors and delegates the work of reading - # the content to the first processor that identifies itself as capable of - # handling that target. - # - # IncludeProcessors must extend Asciidoctor::Extensions::IncludeProcessor. - class IncludeProcessor < Processor - def process target, attributes - output + # Public: Checks whether any {BlockMacroProcessor} extensions are registered to + # handle the block macro with the specified name. + # + # name - the String or Symbol (coersed to a Symbol) macro name + # + # Returns the [Extension] proxy object for the BlockMacroProcessor that matches + # the macro name or false if no match is found. + #-- + # TODO only allow blank target if format is :short + def registered_for_block_macro? name + (ext = @block_macro_extensions[name.to_sym]) ? ext : false end - end - # Supported options: - # * :contexts - The blocks contexts (types) on which this style can be used (default: [:paragraph, :open] - # * :content_model - The structure of the content supported in this block (default: :compound) - # * :pos_attrs - A list of attribute names used to map positional attributes (default: nil) - # * :default_attrs - Set default values for attributes (default: nil) - # * ... - class BlockProcessor < Processor - class << self - def config - @config ||= {:contexts => [:paragraph, :open]} - end + # Public: Retrieves the {Extension} proxy object for the BlockMacroProcessor registered + # to handle a block macro with the specified name. + # + # name - the String or Symbol (coersed to a Symbol) macro name + # + # Returns the [Extension] object stored in the registry that proxies the + # cooresponding BlockMacroProcessor or nil if a match is not found. + def find_block_macro_extension name + @block_macro_extensions[name.to_sym] + end - def option(key, default_value) - config[key] = default_value - end + # Public: Registers a {InlineMacroProcessor} with the extension registry to + # process an inline macro with the specified name. + # + # The InlineMacroProcessor may be one of four types: + # + # * An InlineMacroProcessor subclass + # * An instance of an InlineMacroProcessor subclass + # * The String name of an InlineMacroProcessor subclass + # * A method block (i.e., Proc) that conforms to the InlineMacroProcessor contract + # + # Unless the InlineMacroProcessor is passed as the method block, it must be + # the first argument to this method. The second argument is the name + # (coersed to a Symbol) of the AsciiDoc block macro that this processor is + # registered to handle. If a block macro name is not passed as an argument, + # it gets read from the name property of the InlineMacroProcessor instance. + # If a name still cannot be determined, an error is raised. + # + # Examples + # + # # as an InlineMacroProcessor subclass + # block ChromeInlineMacro + # + # # as an InlineMacroProcessor subclass with an explicit macro name + # block ChromeInineMacro, :chrome + # + # # as an instance of an InlineMacroProcessor subclass + # block ChromeInlineMacro.new + # + # # as an instance of an InlineMacroProcessor subclass with an explicit macro name + # block ChromeInlineMacro.new, :chrome + # + # # as a name of an InlineMacroProcessor subclass + # block 'ChromeInlineMacro' + # + # # as a name of an InlineMacroProcessor subclass with an explicit macro name + # block 'ChromeInineMacro', :chrome + # + # # as a method block + # inline_macro do + # named :chrome + # process |parent, target, attrs| + # ... + # end + # end + # + # # as a method block with an explicit macro name + # register :chrome do + # process |parent, target, attrs| + # ... + # end + # end + # + # Returns an instance of the [Extension] proxy object that is stored in the + # registry and manages the instance of this InlineMacroProcessor. + def inline_macro *args, &block + add_syntax_processor :inline_macro, args, &block end - attr_reader :document - attr_reader :context - attr_reader :options + # Public: Checks whether any {InlineMacroProcessor} extensions have been registered. + # + # Returns a [Boolean] indicating whether any IncludeMacroProcessor extensions are registered. + def inline_macros? + !!@inline_macro_extensions + end - def initialize(context, document, opts = {}) - super(document) - @context = context - @options = self.class.config.dup - opts.delete(:contexts) # contexts can't be overridden - @options.update(opts) - #@options[:contexts] ||= [:paragraph, :open] - @options[:content_model] ||= :compound + # Public: Checks whether any {InlineMacroProcessor} extensions are registered to + # handle the inline macro with the specified name. + # + # name - the String or Symbol (coersed to a Symbol) macro name + # + # Returns the [Extension] proxy object for the InlineMacroProcessor that matches + # the macro name or false if no match is found. + def registered_for_inline_macro? name + (ext = @inline_macro_extensions[name.to_sym]) ? ext : false end - def process parent, reader, attributes - nil + # Public: Retrieves the {Extension} proxy object for the InlineMacroProcessor registered + # to handle an inline macro with the specified name. + # + # name - the String or Symbol (coersed to a Symbol) macro name + # + # Returns the [Extension] object stored in the registry that proxies the + # cooresponding InlineMacroProcessor or nil if a match is not found. + def find_inline_macro_extension name + @inline_macro_extensions[name.to_sym] end - end - class MacroProcessor < Processor - class << self - def config - @config ||= {} + # Public: Retrieves the {Extension} proxy objects for all + # InlineMacroProcessor instances in this registry. + # + # Returns an [Array] of Extension proxy objects. + def inline_macros + @inline_macro_extensions.values + end + + private + + def add_document_processor kind, args, &block + kind_name = kind.to_s.tr '_', ' ' + kind_class_symbol = kind_name.split(' ').map {|word| %(#{word.chr.upcase}#{word[1..-1]}) }.join.to_sym + kind_class = Extensions.const_get kind_class_symbol + kind_java_class = (defined? ::AsciidoctorJ) ? (::AsciidoctorJ::Extensions.const_get kind_class_symbol) : nil + kind_store = instance_variable_get(%(@#{kind}_extensions).to_sym) || instance_variable_set(%(@#{kind}_extensions).to_sym, []) + # style 1: specified as block + extension = if block_given? + config = resolve_args args, 1 + # TODO if block arity is 0, assume block is process method + processor = kind_class.new config + class << processor + include_dsl + end + processor.instance_exec(&block) + processor.freeze + unless processor.process_block_given? + raise ::ArgumentError.new %(No block specified to process #{kind_name} extension at #{block.source_location}) + end + ProcessorExtension.new kind, processor + else + processor, config = resolve_args args, 2 + # style 2: specified as class or class name + if (processor.is_a? ::Class) || ((processor.is_a? ::String) && (processor = Extensions.class_for_name processor)) + unless processor < kind_class || (kind_java_class && processor < kind_java_class) + raise ::ArgumentError.new %(Invalid type for #{kind_name} extension: #{processor}) + end + processor_instance = processor.new config + processor_instance.freeze + ProcessorExtension.new kind, processor_instance + # style 3: specified as instance + elsif (processor.is_a? kind_class) || (kind_java_class && (processor.is_a? kind_java_class)) + processor.update_config config + processor.freeze + ProcessorExtension.new kind, processor + else + raise ::ArgumentError.new %(Invalid arguments specified for registering #{kind_name} extension: #{args}) + end end - def option(key, default_value) - config[key] = default_value + if extension.config[:position] == :>> + kind_store.unshift extension + else + kind_store << extension end end - attr_reader :document - attr_reader :name - attr_reader :options + def add_syntax_processor kind, args, &block + kind_name = kind.to_s.tr '_', ' ' + kind_class_basename = kind_name.split(' ').map {|word| %(#{word.chr.upcase}#{word[1..-1]}) }.join + kind_class_symbol = %(#{kind_class_basename}Processor).to_sym + kind_class = Extensions.const_get kind_class_symbol + kind_java_class = (defined? ::AsciidoctorJ) ? (::AsciidoctorJ::Extensions.const_get kind_class_symbol) : nil + kind_store = instance_variable_get(%(@#{kind}_extensions).to_sym) || instance_variable_set(%(@#{kind}_extensions).to_sym, {}) + # style 1: specified as block + if block_given? + name, config = resolve_args args, 2 + processor = kind_class.new as_symbol(name), config + class << processor + include_dsl + end + if block.arity == 1 + yield processor + else + processor.instance_exec(&block) + end + processor.freeze + unless (name = processor.name) + raise ::ArgumentError.new %(No name specified for #{kind_name} extension at #{block.source_location}) + end + unless processor.process_block_given? + raise ::NoMethodError.new %(No block specified to process #{kind_name} extension at #{block.source_location}) + end + kind_store[name] = ProcessorExtension.new kind, processor + else + processor, name, config = resolve_args args, 3 + # style 2: specified as class or class name + if (processor.is_a? ::Class) || ((processor.is_a? ::String) && (processor = Extensions.class_for_name processor)) + unless processor < kind_class || (kind_java_class && processor < kind_java_class) + raise ::ArgumentError.new %(Class specified for #{kind_name} extension does not inherit from #{kind_class}: #{processor}) + end + processor_instance = processor.new as_symbol(name), config + processor.freeze + unless (name = processor_instance.name) + raise ::ArgumentError.new %(No name specified for #{kind_name} extension: #{processor}) + end + kind_store[name] = ProcessorExtension.new kind, processor_instance + # style 3: specified as instance + elsif (processor.is_a? kind_class) || (kind_java_class && (processor.is_a? kind_java_class)) + name = as_symbol(name || processor.name) + processor.update_config config + processor.freeze + unless (name = processor.name) + raise ::ArgumentError.new %(No name specified for #{kind_name} extension: #{processor}) + end + kind_store[name] = ProcessorExtension.new kind, processor + else + raise ::ArgumentError.new %(Invalid arguments specified for registering #{kind_name} extension: #{args}) + end + end + end - def initialize(name, document, opts = {}) - super(document) - @name = name - @options = self.class.config.dup - @options.update(opts) + def resolve_args args, expect + opts = (args[-1].is_a? ::Hash) ? args.pop : {} + return opts if expect == 1 + num_args = args.size + if (missing = expect - 1 - num_args) > 0 + args.fill nil, num_args, missing + elsif missing < 0 + args.pop(-missing) + end + args << opts + args end - def process parent, target, attributes, source = nil - nil + def as_symbol name + name ? ((name.is_a? ::Symbol) ? name : name.to_sym) : nil end end - class BlockMacroProcessor < MacroProcessor - end + class << self + def generate_name + %(extgrp#{next_auto_id}) + end - # TODO break this out into different pattern types - # for example, FormalInlineMacro, ShortInlineMacro (no target) and other patterns - class InlineMacroProcessor < MacroProcessor - def initialize(name, document, opts = {}) - super - @regexp = nil + def next_auto_id + @auto_id ||= -1 + @auto_id += 1 + end + + def groups + @groups ||= {} end - def regexp - if @options[:short_form] - @regexp ||= %r(\\?#{@name}:\[((?:\\\]|[^\]])*?)\]) + def build_registry name = nil, &block + if block_given? + name ||= generate_name + Registry.new({ name => block }) else - @regexp ||= %r(\\?#{@name}:(\S+?)\[((?:\\\]|[^\]])*?)\]) + Registry.new end end + + # Public: Registers an extension Group that subsequently registers a + # collection of extensions. + # + # Registers the extension Group specified under the given name. If a name is + # not given, one is calculated by appending the next value in a 0-based + # index to the string "extgrp". For instance, the first unnamed extension + # group to be registered is assigned the name "extgrp0" if a name is not + # specified. + # + # The names are not yet used, but are intended for selectively activating + # extensions in the future. + # + # If the extension group argument is a String or a Symbol, it gets resolved + # to a Class before being registered. + # + # name - The name under which this extension group is registered (optional, default: nil) + # group - A block (Proc), a Class, a String or Symbol name of a Class or + # an Object instance of a Class. + # + # Examples + # + # Asciidoctor::Extensions.register UmlExtensions + # + # Asciidoctor::Extensions.register :uml, UmlExtensions + # + # Asciidoctor::Extensions.register do + # block_processor :plantuml, PlantUmlBlock + # end + # + # Asciidoctor::Extensions.register :uml do + # block_processor :plantuml, PlantUmlBlock + # end + # + # Returns the [Proc, Class or Object] instance, matching the type passed to this method. + def register *args, &block + argc = args.length + resolved_group = if block_given? + block + elsif !(group = args.pop) + raise ::ArgumentError.new %(Extension group to register not specified) + else + # QUESTION should we instantiate the group class here or defer until + # activation?? + case group + when ::Class + group + when ::String + class_for_name group + when ::Symbol + class_for_name group.to_s + else + group + end + end + name = args.pop || generate_name + unless args.empty? + raise ::ArgumentError.new %(Wrong number of arguments (#{argc} for 1..2)) + end + groups[name] = resolved_group + end + + def unregister_all + @groups = {} + end + + # unused atm, but tested + def resolve_class object + (object.is_a? ::Class) ? object : (class_for_name object.to_s) + end + + # Public: Resolves the Class object for the qualified name. + # + # Returns Class + def class_for_name qualified_name + resolved_class = ::Object + qualified_name.split('::').each do |name| + if name.empty? + # do nothing + elsif resolved_class.const_defined? name + resolved_class = resolved_class.const_get name + else + raise %(Could not resolve class for name: #{qualified_name}) + end + end + resolved_class + end end + end end diff --git a/lib/asciidoctor/helpers.rb b/lib/asciidoctor/helpers.rb index ca0501d0..c754800d 100644 --- a/lib/asciidoctor/helpers.rb +++ b/lib/asciidoctor/helpers.rb @@ -133,6 +133,7 @@ module Helpers # # Returns the String filename with the file extension removed def self.rootname(file_name) + # alternatively, this could be written as ::File.basename file_name, ((::File.extname file_name) || '') (ext = ::File.extname(file_name)).empty? ? file_name : file_name[0...-ext.length] end diff --git a/lib/asciidoctor/lexer.rb b/lib/asciidoctor/lexer.rb index ef19e19b..151a40d6 100644 --- a/lib/asciidoctor/lexer.rb +++ b/lib/asciidoctor/lexer.rb @@ -50,7 +50,7 @@ class Lexer unless options[:header_only] while reader.has_more_lines? new_section, block_attributes = next_section(reader, document, block_attributes) - document << new_section unless new_section.nil? + document << new_section if new_section end # NOTE we could try to avoid creating a preamble in the first place, though # that would require reworking assumptions in next_section since the preamble @@ -112,7 +112,7 @@ class Lexer end document.attributes['doctitle'] = section_title = doctitle # QUESTION: should the id assignment on Document be encapsulated in the Document class? - if document.id.nil? + unless document.id document.id = block_attributes.delete('id') end parse_header_metadata(reader, document) @@ -280,7 +280,7 @@ class Lexer if next_level > current_level || (section.context == :document && next_level == 0) if next_level == 0 && doctype != 'book' warn %(asciidoctor: ERROR: #{reader.line_info}: only book doctypes can contain level 0 sections) - elsif !expected_next_levels.nil? && !expected_next_levels.include?(next_level) + elsif expected_next_levels && !expected_next_levels.include?(next_level) warn %(asciidoctor: WARNING: #{reader.line_info}: section title out of sequence: ) + %(expected #{expected_next_levels.size > 1 ? 'levels' : 'level'} #{expected_next_levels * ' or '}, ) + %(got level #{next_level}) @@ -298,8 +298,7 @@ class Lexer else # just take one block or else we run the risk of overrunning section boundaries block_line_info = reader.line_info - new_block = next_block(reader, (intro || section), attributes, :parse_metadata => false) - unless new_block.nil? + if (new_block = next_block reader, (intro || section), attributes, :parse_metadata => false) # REVIEW this may be doing too much if part if !section.blocks? @@ -405,13 +404,12 @@ class Lexer skipped = reader.skip_blank_lines # bail if we've reached the end of the parent block or document - return nil unless reader.has_more_lines? + return unless reader.has_more_lines? - text_only = options[:text] # check for option to find list item text only # if skipped a line, assume a list continuation was # used and block content is acceptable - if text_only && skipped > 0 + if (text_only = options[:text]) && skipped > 0 options.delete(:text) text_only = false end @@ -422,9 +420,9 @@ class Lexer document = parent.document if (extensions = document.extensions) block_extensions = extensions.blocks? - macro_extensions = extensions.block_macros? + block_macro_extensions = extensions.block_macros? else - block_extensions = macro_extensions = false + block_extensions = block_macro_extensions = false end #parent_context = parent.is_a?(Block) ? parent.context : nil in_list = (parent.is_a? List) @@ -432,12 +430,12 @@ class Lexer style = nil explicit_style = nil - while reader.has_more_lines? && block.nil? + while !block && reader.has_more_lines? # if parsing metadata, read until there is no more to read if parse_metadata && parse_block_metadata_line(reader, document, attributes, options) reader.advance next - #elsif parse_sections && parent_context.nil? && is_next_line_section?(reader, attributes) + #elsif parse_sections && !parent_context && is_next_line_section?(reader, attributes) # block, attributes = next_section(reader, parent, attributes) # break end @@ -453,7 +451,7 @@ class Lexer style, explicit_style = parse_style_attribute(attributes, reader) end - if delimited_blk_match = is_delimited_block?(this_line, true) + if (delimited_blk_match = is_delimited_block? this_line, true) delimited_block = true block_context = cloaked_context = delimited_blk_match.context terminator = delimited_blk_match.terminator @@ -464,7 +462,7 @@ class Lexer block_context = style.to_sym elsif delimited_blk_match.masq.include?('admonition') && ADMONITION_STYLES.include?(style) block_context = :admonition - elsif block_extensions && extensions.processor_registered_for_block?(style, block_context) + elsif block_extensions && extensions.registered_for_block?(style, block_context) block_context = style.to_sym else warn %(asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for #{block_context} block: #{style}) @@ -473,7 +471,7 @@ class Lexer end end - if !delimited_block + unless delimited_block # this loop only executes once; used for flow control # break once a block is found or at end of loop @@ -482,7 +480,7 @@ class Lexer while true # process lines verbatim - if !style.nil? && Compliance.strict_verbatim_paragraphs && VERBATIM_STYLES.include?(style) + if style && Compliance.strict_verbatim_paragraphs && VERBATIM_STYLES.include?(style) block_context = style.to_sym reader.unshift_line this_line # advance to block parsing => @@ -509,7 +507,7 @@ class Lexer posattrs = [] end - unless style.nil? || explicit_style + unless !style || explicit_style attributes['alt'] = style if blk_ctx == :image attributes.delete('style') style = nil @@ -522,29 +520,31 @@ class Lexer :into => attributes) target = block.sub_attributes(match[2], :attribute_missing => 'drop-line') if target.empty? + # retain as unparsed if attribute-missing is skip if document.attributes.fetch('attribute-missing', Compliance.attribute_missing) == 'skip' - # retain as unparsed - return Block.new(parent, :paragraph, :source => [this_line]) + return Block.new(parent, :paragraph, :content_model => :simple, :source => [this_line]) + # otherwise, drop the line else - # drop the line if target resolves to nothing - return nil + attributes.clear + return end end attributes['target'] = target - block.title = attributes.delete('title') if attributes.has_key?('title') - if blk_ctx == :image - if attributes.has_key? 'scaledwidth' - # append % to scaledwidth if ends in number (no units present) - if (48..57).include?((attributes['scaledwidth'][-1] || 0).ord) - attributes['scaledwidth'] = %(#{attributes['scaledwidth']}%) - end - end - document.register(:images, target) - attributes['alt'] ||= ::File.basename(target, ::File.extname(target)).tr('_-', ' ') - # QUESTION should video or audio have an auto-numbered caption? - block.assign_caption attributes.delete('caption'), 'figure' - end + # now done down below + #block.title = attributes.delete('title') if attributes.has_key?('title') + #if blk_ctx == :image + # if attributes.has_key? 'scaledwidth' + # # append % to scaledwidth if ends in number (no units present) + # if (48..57).include?((attributes['scaledwidth'][-1] || 0).ord) + # attributes['scaledwidth'] = %(#{attributes['scaledwidth']}%) + # end + # end + # document.register(:images, target) + # attributes['alt'] ||= ::File.basename(target, ::File.extname(target)).tr('_-', ' ') + # # QUESTION should video or audio have an auto-numbered caption? + # block.assign_caption attributes.delete('caption'), 'figure' + #end break # NOTE we're letting the toc macro have attributes @@ -553,21 +553,27 @@ class Lexer block.parse_attributes(match[1], [], :sub_result => false, :into => attributes) break - elsif macro_extensions && (match = GenericBlockMacroRx.match(this_line)) && - extensions.processor_registered_for_block_macro?(match[1]) - name = match[1] + elsif block_macro_extensions && (match = GenericBlockMacroRx.match(this_line)) && + (extension = extensions.registered_for_block_macro?(match[1])) target = match[2] raw_attributes = match[3] - processor = extensions.load_block_macro_processor name, document - unless raw_attributes.empty? - document.parse_attributes(raw_attributes, processor.options.fetch(:pos_attrs, []), - :sub_input => true, :sub_result => false, :into => attributes) + if extension.config[:content_model] == :attributes + unless raw_attributes.empty? + document.parse_attributes(raw_attributes, (extension.config[:pos_attrs] || []), + :sub_input => true, :sub_result => false, :into => attributes) + end + else + attributes['text'] = raw_attributes end - if !(default_attrs = processor.options.fetch(:default_attrs, {})).empty? + if (default_attrs = extension.config[:default_attrs]) default_attrs.each {|k, v| attributes[k] ||= v } end - block = processor.process parent, target, attributes - return nil if block.nil? + if (block = extension.process_method[parent, target, attributes.dup]) + attributes.replace block.attributes + else + attributes.clear + return + end break end end @@ -586,7 +592,7 @@ class Lexer end list_item = next_list_item(reader, block, match) expected_index += 1 - if !list_item.nil? + if list_item block << list_item coids = document.callouts.callout_ids(block.items.size) if !coids.empty? @@ -662,7 +668,7 @@ class Lexer reader.unshift_line this_line # advance to block parsing => break - elsif block_extensions && extensions.processor_registered_for_block?(style, :paragraph) + elsif block_extensions && extensions.registered_for_block?(style, :paragraph) block_context = style.to_sym cloaked_context = :paragraph reader.unshift_line this_line @@ -722,7 +728,7 @@ class Lexer if lines.empty? # call advance since the reader preserved the last line reader.advance - return nil + return end catalog_inline_anchors(lines.join(EOL), document) @@ -733,7 +739,7 @@ class Lexer attributes['style'] = admonition_match[1] attributes['name'] = admonition_name = admonition_match[1].downcase attributes['caption'] ||= document.attributes[%(#{admonition_name}-caption)] - block = Block.new(parent, :admonition, :source => lines, :attributes => attributes) + block = Block.new(parent, :admonition, :content_model => :simple, :source => lines, :attributes => attributes) elsif !text_only && Compliance.markdown_syntax && first_line.start_with?('> ') lines.map! {|line| if line == '>' @@ -752,8 +758,8 @@ class Lexer attribution, citetitle = nil end attributes['style'] = 'quote' - attributes['attribution'] = attribution unless attribution.nil? - attributes['citetitle'] = citetitle unless citetitle.nil? + attributes['attribution'] = attribution if attribution + attributes['citetitle'] = citetitle if citetitle # NOTE will only detect headings that are floating titles (not section titles) # TODO could assume a floating title when inside a block context # FIXME Reader needs to be created w/ line info @@ -766,11 +772,9 @@ class Lexer # strip trailing quote lines[-1] = lines[-1].chop attributes['style'] = 'quote' - attributes['attribution'] = attribution unless attribution.nil? - attributes['citetitle'] = citetitle unless citetitle.nil? - block = Block.new(parent, :quote, :source => lines, :attributes => attributes) - #block = Block.new(parent, :quote, :content_model => :compound, :attributes => attributes) - #block << Block.new(block, :paragraph, :source => lines) + attributes['attribution'] = attribution if attribution + attributes['citetitle'] = citetitle if citetitle + block = Block.new(parent, :quote, :content_model => :simple, :source => lines, :attributes => attributes) else # if [normal] is used over an indented paragraph, unindent it if style == 'normal' && ((first_char = lines[0].chr) == ' ' || first_char == TAB) @@ -784,7 +788,7 @@ class Lexer end end - block = Block.new(parent, :paragraph, :source => lines, :attributes => attributes) + block = Block.new(parent, :paragraph, :content_model => :simple, :source => lines, :attributes => attributes) end end @@ -794,7 +798,7 @@ class Lexer end # either delimited block or styled paragraph - if block.nil? && !block_context.nil? + if !block && block_context # abstract and partintro should be handled by open block # FIXME kind of hackish...need to sort out how to generalize this block_context = :open if block_context == :abstract || block_context == :partintro @@ -807,10 +811,10 @@ class Lexer when :comment build_block(block_context, :skip, terminator, parent, reader, attributes) - return nil + return when :example - block = build_block(block_context, :compound, terminator, parent, reader, attributes, {:supports_caption => true}) + block = build_block(block_context, :compound, terminator, parent, reader, attributes) when :listing, :fenced_code, :source if block_context == :fenced_code @@ -824,7 +828,7 @@ class Lexer elsif block_context == :source AttributeList.rekey(attributes, [nil, 'language', 'linenums']) end - block = build_block(:listing, :verbatim, terminator, parent, reader, attributes, {:supports_caption => true}) + block = build_block(:listing, :verbatim, terminator, parent, reader, attributes) when :literal block = build_block(block_context, :verbatim, terminator, parent, reader, attributes) @@ -857,19 +861,21 @@ class Lexer block = build_block(block_context, (block_context == :verse ? :verbatim : :compound), terminator, parent, reader, attributes) else - if block_extensions && extensions.processor_registered_for_block?(block_context, cloaked_context) - processor = extensions.load_block_processor block_context, document - - if (content_model = processor.options[:content_model]) != :skip - if !(pos_attrs = processor.options.fetch(:pos_attrs, [])).empty? + if block_extensions && (extension = extensions.registered_for_block?(block_context, cloaked_context)) + # TODO pass cloaked_context to extension somehow (perhaps a new instance for each cloaked_context?) + if (content_model = extension.config[:content_model]) != :skip + if !(pos_attrs = extension.config[:pos_attrs] || []).empty? AttributeList.rekey(attributes, [nil].concat(pos_attrs)) end - if !(default_attrs = processor.options.fetch(:default_attrs, {})).empty? + if (default_attrs = extension.config[:default_attrs]) default_attrs.each {|k, v| attributes[k] ||= v } end end - block = build_block(block_context, content_model, terminator, parent, reader, attributes, :processor => processor) - return nil if block.nil? + block = build_block block_context, content_model, terminator, parent, reader, attributes, :extension => extension + unless block && content_model != :skip + attributes.clear + return + end else # this should only happen if there's a misconfiguration raise %(Unsupported block type #{block_context} at #{reader.line_info}) @@ -882,10 +888,25 @@ class Lexer # blocks or trailing attribute lists could leave us without a block, # so handle accordingly # REVIEW we may no longer need this nil check - unless block.nil? + # FIXME we've got to clean this up, it's horrible! + if block # REVIEW seems like there is a better way to organize this wrap-up block.title = attributes['title'] unless block.title? - block.caption ||= attributes.delete('caption') + # FIXME HACK don't hardcode logic for alt, caption and scaledwidth on images down here + if block.context == :image + resolved_target = attributes['target'] + block.document.register(:images, resolved_target) + attributes['alt'] ||= ::File.basename(resolved_target, ::File.extname(resolved_target)).tr('_-', ' ') + block.assign_caption attributes.delete('caption'), 'figure' + if (scaledwidth = attributes['scaledwidth']) + # append % to scaledwidth if ends in number (no units present) + if (48..57).include?((scaledwidth[-1] || 0).ord) + attributes['scaledwidth'] = %(#{scaledwidth}%) + end + end + else + block.caption ||= attributes.delete('caption') + end # TODO eventualy remove the style attribute from the attributes hash #block.style = attributes.delete('style') block.style = attributes['style'] @@ -895,7 +916,8 @@ class Lexer # TODO sub reftext document.register(:ids, [block_id, (attributes['reftext'] || (block.title? ? block.title : nil))]) end - block.update_attributes(attributes) + # FIXME remove the need for this update! + block.attributes.update(attributes) unless attributes.empty? block.lock_in_subs #if document.attributes.has_key? :pending_attribute_entries @@ -920,7 +942,7 @@ class Lexer # returns the match data if this line is the first line of a delimited block or nil if not def self.is_delimited_block? line, return_match_data = false # highly optimized for best performance - return nil unless (line_len = line.length) > 1 && (DELIMITED_BLOCK_LEADERS.include? line[0..1]) + return unless (line_len = line.length) > 1 && (DELIMITED_BLOCK_LEADERS.include? line[0..1]) # catches open block if line_len == 2 tip = line @@ -942,14 +964,14 @@ class Lexer tip_3 = (tl == 4 ? tip.chop : tip) if tip_3 == '```' if tl == 4 && tip.end_with?('`') - return nil + return end tip = tip_3 tl = 3 fenced_code = true elsif tip_3 == '~~~' if tl == 4 && tip.end_with?('~') - return nil + return end tip = tip_3 tl = 3 @@ -958,7 +980,7 @@ class Lexer end # short circuit if not a fenced code block - return nil if tl == 3 && !fenced_code + return if tl == 3 && !fenced_code end if DELIMITED_BLOCKS.has_key? tip @@ -1036,24 +1058,37 @@ class Lexer if content_model == :skip attributes.clear + # FIXME we shouldn't be mixing return types return lines end - if content_model == :verbatim && attributes.has_key?('indent') - reset_block_indent! lines, attributes['indent'].to_i + if content_model == :verbatim && (indent = attributes['indent']) + reset_block_indent! lines, indent.to_i end - if (processor = options[:processor]) + if (extension = options[:extension]) + # QUESTION do we want to delete the style? attributes.delete('style') - processor.options[:content_model] = content_model - block = processor.process(parent, block_reader || Reader.new(lines), attributes) + if (block = extension.process_method[parent, block_reader || (Reader.new lines), attributes.dup]) + attributes.replace block.attributes + # FIXME if the content model is set to compound, but we only have simple in this context, then + # forcefully set the content_model to simple to prevent parsing blocks from children + # TODO document this behavior!! + if block.content_model == :compound && !(lines = block.lines).nil_or_empty? + content_model = :compound + block_reader = Reader.new lines + end + else + # FIXME need a test to verify this returns nil at the right time + return + end else - block = Block.new(parent, block_context, :content_model => content_model, :attributes => attributes, :source => lines) + block = Block.new(parent, block_context, :content_model => content_model, :source => lines, :attributes => attributes) end - # should supports_caption be necessary? - if options.fetch(:supports_caption, false) - block.title = attributes.delete('title') if attributes.has_key?('title') + # QUESTION should we have an explicit map or can we rely on check for *-caption attribute? + if (attributes.has_key? 'title') && (block.document.attr? %(#{block.context}-caption)) + block.title = attributes.delete 'title' block.assign_caption attributes.delete('caption') end @@ -1079,7 +1114,7 @@ class Lexer def self.parse_blocks(reader, parent) while reader.has_more_lines? block = Lexer.next_block(reader, parent) - parent << block unless block.nil? + parent << block if block end end @@ -1131,7 +1166,7 @@ class Lexer list_block.items[-1] << next_block(reader, list_block) end - list_block << list_item unless list_item.nil? + list_block << list_item if list_item list_item = nil reader.skip_blank_lines @@ -1178,7 +1213,7 @@ class Lexer reftext = m[2] || m[4] # enable if we want to allow double quoted values #id = id.sub(DoubleQuotedRx, '\2') - #if !reftext.nil? + #if reftext # reftext = reftext.sub(DoubleQuotedMultiRx, '\2') #end document.register(:ids, [id, reftext]) @@ -1301,7 +1336,7 @@ class Lexer # list while list_item_reader.has_more_lines? new_block = next_block(list_item_reader, list_block, {}, options) - list_item << new_block unless new_block.nil? + list_item << new_block if new_block end list_item.fold_first(continuation_connects_first_block, content_adjacent) @@ -1488,7 +1523,7 @@ class Lexer this_line = nil end - reader.unshift_line this_line if !this_line.nil? + reader.unshift_line this_line if this_line if detached_continuation buffer.delete_at detached_continuation @@ -1539,7 +1574,7 @@ class Lexer section.sectname = %(sect#{section.level}) end - if section.id.nil? && (id = attributes['id']) + if !section.id && (id = attributes['id']) section.id = id else # generate an id if one was not *embedded* in the heading line @@ -1757,7 +1792,7 @@ class Lexer author_metadata = process_authors reader.read_line unless author_metadata.empty? - if !document.nil? + if document # apply header subs and assign to document author_metadata.each do |key, val| unless document.attributes.has_key? key @@ -1790,7 +1825,7 @@ class Lexer end unless rev_metadata.empty? - if !document.nil? + if document # apply header subs and assign to document rev_metadata.each do |key, val| unless document.attributes.has_key? key @@ -1808,7 +1843,7 @@ class Lexer reader.skip_blank_lines end - if !document.nil? + if document # process author attribute entries that override (or stand in for) the implicit author line author_metadata = nil if document.attributes.has_key?('author') && @@ -1835,7 +1870,7 @@ class Lexer end end - unless author_metadata.nil? + if author_metadata document.attributes.update author_metadata # special case @@ -2021,7 +2056,7 @@ class Lexer end end - store_attribute(name, value, parent.nil? ? nil : parent.document, attributes) + store_attribute(name, value, (parent ? parent.document : nil), attributes) true else false @@ -2049,7 +2084,7 @@ class Lexer name = sanitize_attribute_name(name) accessible = true - unless doc.nil? + if doc accessible = value.nil? ? doc.delete_attribute(name) : doc.set_attribute(name, value) end @@ -2190,8 +2225,10 @@ class Lexer # returns an instance of Asciidoctor::Table parsed from the provided reader def self.next_table(table_reader, parent, attributes) table = Table.new(parent, attributes) - table.title = attributes.delete('title') if attributes.has_key?('title') - table.assign_caption attributes.delete('caption') + if (attributes.has_key? 'title') + table.title = attributes.delete 'title' + table.assign_caption attributes.delete('caption') + end if attributes.has_key? 'cols' table.create_columns(parse_col_specs(attributes['cols'])) @@ -2546,7 +2583,7 @@ class Lexer #-- # FIXME refactor gsub matchers into compiled regex def self.reset_block_indent!(lines, indent = 0) - return if indent.nil? || lines.empty? + return if !indent || lines.empty? tab_detected = false # TODO make tab size configurable diff --git a/lib/asciidoctor/reader.rb b/lib/asciidoctor/reader.rb index 0fd63252..50f606de 100644 --- a/lib/asciidoctor/reader.rb +++ b/lib/asciidoctor/reader.rb @@ -88,7 +88,7 @@ class Reader if opts[:normalize] Helpers.normalize_lines_from_string data else - data.each_line.to_a + data.split EOL end else if opts[:normalize] @@ -560,7 +560,7 @@ class PreprocessorReader < Reader @includes = (document.references[:includes] ||= []) @skipping = false @conditional_stack = [] - @include_processors = nil + @include_processor_extensions = nil end def prepare_lines data, opts = {} @@ -810,10 +810,10 @@ class PreprocessorReader < Reader # assume that if an include processor is given, the developer wants # to handle when and how to process the include elsif include_processors? && - (processor = @include_processors.find {|candidate| candidate.handles? target }) + (extension = @include_processor_extensions.find {|candidate| candidate.instance.handles? target }) advance # QUESTION should we use @document.parse_attribues? - processor.process self, target, AttributeList.new(raw_attributes).parse + extension.process_method[self, target, AttributeList.new(raw_attributes).parse] true # if running in SafeMode::SECURE or greater, don't process this directive # however, be friendly and at least make it a link to the source document @@ -1136,16 +1136,16 @@ class PreprocessorReader < Reader end def include_processors? - if !@include_processors + if !@include_processor_extensions if @document.extensions? && @document.extensions.include_processors? - @include_processors = @document.extensions.load_include_processors(@document) + @include_processor_extensions = @document.extensions.include_processors true else - @include_processors = false + @include_processor_extensions = false false end else - @include_processors != false + @include_processor_extensions != false end end diff --git a/lib/asciidoctor/substitutors.rb b/lib/asciidoctor/substitutors.rb index 5195b901..817adbb3 100644 --- a/lib/asciidoctor/substitutors.rb +++ b/lib/asciidoctor/substitutors.rb @@ -531,8 +531,8 @@ module Substitutors # FIXME this location is somewhat arbitrary, probably need to be able to control ordering # TODO this handling needs some cleanup if (extensions = @document.extensions) && extensions.inline_macros? && found[:macroish] - extensions.load_inline_macro_processors(@document).each do |processor| - result = result.gsub(processor.regexp) { + extensions.inline_macros.each do |extension| + result = result.gsub(extension.config[:regexp]) { # alias match for Ruby 1.8.7 compat m = $~ # honor the escape @@ -541,13 +541,16 @@ module Substitutors end target = m[1] - if processor.options[:short_form] - attributes = {} + attributes = if extension.config[:format] == :short + {} else - posattrs = processor.options.fetch(:pos_attrs, []) - attributes = parse_attributes(m[2], posattrs, :sub_input => true, :unescape_input => true) + if extension.config[:content_model] == :attributes + parse_attributes m[2], (extension.config[:pos_attrs] || []), :sub_input => true, :unescape_input => true + else + { 'text' => (unescape_bracketed_text m[2]) } + end end - processor.process self, target, attributes + extension.process_method[self, target, attributes] } end end @@ -982,7 +985,7 @@ module Substitutors # # returns nil if str is nil, an empty Hash if str is empty, otherwise a Hash of attributes (role and id only) def parse_quoted_text_attributes(str) - return nil unless str + return unless str return {} if str.empty? str = sub_attributes(str) if str.include?('{') str = str.strip @@ -1026,7 +1029,7 @@ module Substitutors # # returns nil if attrline is nil, an empty Hash if attrline is empty, otherwise a Hash of parsed attributes def parse_attributes(attrline, posattrs = ['role'], opts = {}) - return nil unless attrline + return unless attrline return {} if attrline.empty? attrline = @document.sub_attributes(attrline) if opts[:sub_input] attrline = unescape_bracketed_text(attrline) if opts[:unescape_input] @@ -1313,22 +1316,25 @@ module Substitutors # # Returns nothing def lock_in_subs - default_subs = [] - case @content_model - when :simple - default_subs = SUBS[:normal] - when :verbatim - if @context == :listing || (@context == :literal && !(option? 'listparagraph')) - default_subs = SUBS[:verbatim] - elsif @context == :verse - default_subs = SUBS[:normal] + default_subs = if @default_subs + @default_subs + else + case @content_model + when :simple + SUBS[:normal] + when :verbatim + if @context == :listing || (@context == :literal && !(option? 'listparagraph')) + SUBS[:verbatim] + elsif @context == :verse + SUBS[:normal] + else + SUBS[:basic] + end + when :raw + SUBS[:pass] else - default_subs = SUBS[:basic] + return end - when :raw - default_subs = SUBS[:pass] - else - return end if (custom_subs = @attributes['subs']) diff --git a/test/extensions_test.rb b/test/extensions_test.rb index e70b676b..033fe239 100644 --- a/test/extensions_test.rb +++ b/test/extensions_test.rb @@ -6,6 +6,9 @@ end require 'asciidoctor/extensions' class SamplePreprocessor < Asciidoctor::Extensions::Preprocessor + def process reader, raw_lines + nil + end end class SampleIncludeProcessor < Asciidoctor::Extensions::IncludeProcessor @@ -32,7 +35,6 @@ class ScrubHeaderPreprocessor < Asciidoctor::Extensions::Preprocessor lines.shift reader.advance end - #lines reader end end @@ -54,84 +56,98 @@ class BoilerplateTextIncludeProcessor < Asciidoctor::Extensions::IncludeProcesso end class ReplaceAuthorTreeprocessor < Asciidoctor::Extensions::Treeprocessor - def process - @document.attributes['firstname'] = 'Ghost' - @document.attributes['author'] = 'Ghost Writer' + def process document + document.attributes['firstname'] = 'Ghost' + document.attributes['author'] = 'Ghost Writer' end end class StripAttributesPostprocessor < Asciidoctor::Extensions::Postprocessor - def process output + def process document, output output.gsub(/<(\w+).*?>/m, "<\\1>") end end -class UppercaseBlock < Asciidoctor::Extensions::BlockProcessor +class UppercaseBlock < Asciidoctor::Extensions::BlockProcessor; use_dsl + match_name :yell + on_contexts :paragraph + parse_content_as :simple def process parent, reader, attributes - Asciidoctor::Block.new parent, :paragraph, :source => reader.lines.map {|line| line.upcase } + create_paragraph parent, reader.lines.map(&:upcase), attributes end end class SnippetMacro < Asciidoctor::Extensions::BlockMacroProcessor def process parent, target, attributes - Asciidoctor::Block.new parent, :pass, :content_model => :raw, :source => %(<script src="http://example.com/#{target}.js"></script>) + create_pass_block parent, %(<script src="http://example.com/#{target}.js"></script>), {}, :content_model => :raw end end -class TemperatureMacro < Asciidoctor::Extensions::InlineMacroProcessor +class TemperatureMacro < Asciidoctor::Extensions::InlineMacroProcessor; use_dsl + match_name :degrees + map_attributes ['units'] def process parent, target, attributes - temperature_unit = @document.attr('temperature-unit', 'C') + units = attributes['units'] || (parent.document.attr 'temperature-unit', 'C') c = target.to_f - if temperature_unit == 'C' - text = %(#{c} °C) - elsif temperature_unit == 'F' - f = c * 1.8 + 32 - text = %(#{f} °F) + case units + when 'C' + %(#{c} °C) + when 'F' + %(#{c * 1.8 + 32 } °F) else - text = target + c end - - text end end -class SampleExtension < Asciidoctor::Extensions::Extension - def self.activate(registry, document) - document.attributes['activate-method-called'] = '' +class SampleExtensionGroup < Asciidoctor::Extensions::Group + def activate registry + registry.document.attributes['activate-method-called'] = '' registry.preprocessor SamplePreprocessor end end context 'Extensions' do context 'Register' do - test 'should register extension class' do + test 'should register extension group class' do + begin + Asciidoctor::Extensions.register :sample, SampleExtensionGroup + assert_not_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 - Asciidoctor::Extensions.register SampleExtension - assert_not_nil Asciidoctor::Extensions.registered - assert_equal 1, Asciidoctor::Extensions.registered.size - assert_equal SampleExtension, Asciidoctor::Extensions.registered.first + SampleExtensionGroup.register :sample + assert_not_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 be able to self register extension class' do + test 'should register extension group from class name' do begin - SampleExtension.register - assert_not_nil Asciidoctor::Extensions.registered - assert_equal 1, Asciidoctor::Extensions.registered.size - assert_equal SampleExtension, Asciidoctor::Extensions.registered.first + Asciidoctor::Extensions.register :sample, 'SampleExtensionGroup' + assert_not_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 class from string' do + test 'should register extension group from instance' do begin - Asciidoctor::Extensions.register 'SampleExtension' - assert_not_nil Asciidoctor::Extensions.registered - assert_equal 1, Asciidoctor::Extensions.registered.size - assert_equal SampleExtension, Asciidoctor::Extensions.registered.first + Asciidoctor::Extensions.register :sample, SampleExtensionGroup.new + assert_not_nil Asciidoctor::Extensions.groups + assert_equal 1, Asciidoctor::Extensions.groups.size + assert Asciidoctor::Extensions.groups[:sample].is_a? SampleExtensionGroup ensure Asciidoctor::Extensions.unregister_all end @@ -139,11 +155,11 @@ context 'Extensions' do test 'should register extension block' do begin - Asciidoctor::Extensions.register do |document| + Asciidoctor::Extensions.register(:sample) do end - assert_not_nil Asciidoctor::Extensions.registered - assert_equal 1, Asciidoctor::Extensions.registered.size - assert Asciidoctor::Extensions.registered.first.is_a?(Proc) + assert_not_nil Asciidoctor::Extensions.groups + assert_equal 1, Asciidoctor::Extensions.groups.size + assert Asciidoctor::Extensions.groups[:sample].is_a? Proc ensure Asciidoctor::Extensions.unregister_all end @@ -169,8 +185,8 @@ context 'Extensions' do test 'should raise exception if cannot find class for name' do begin - Asciidoctor::Extensions.class_for_name('InvalidModule::InvalidClass') - flunk 'Expecting RuntimeError to be raised' + Asciidoctor::Extensions.class_for_name('InvalidModule::InvalidClass') + flunk 'Expecting RuntimeError to be raised' rescue RuntimeError => e assert_equal 'Could not resolve class for name: InvalidModule::InvalidClass', e.message end @@ -190,11 +206,12 @@ context 'Extensions' do end context 'Activate' do - test 'should call activate on extension class' do + test 'should call activate on extension group class' do begin doc = Asciidoctor::Document.new - Asciidoctor::Extensions.register SampleExtension - registry = Asciidoctor::Extensions::Registry.new doc + Asciidoctor::Extensions.register :sample, SampleExtensionGroup + registry = Asciidoctor::Extensions::Registry.new + registry.activate doc assert doc.attr? 'activate-method-called' assert registry.preprocessors? ensure @@ -205,11 +222,12 @@ context 'Extensions' do test 'should invoke extension block' do begin doc = Asciidoctor::Document.new - Asciidoctor::Extensions.register do |document| - document.attributes['block-called'] = '' + Asciidoctor::Extensions.register do + @document.attributes['block-called'] = '' preprocessor SamplePreprocessor end - registry = Asciidoctor::Extensions::Registry.new doc + registry = Asciidoctor::Extensions::Registry.new + registry.activate doc assert doc.attr? 'block-called' assert registry.preprocessors? ensure @@ -219,7 +237,7 @@ context 'Extensions' do test 'should create registry in Document if extensions are loaded' do begin - SampleExtension.register + SampleExtensionGroup.register doc = Asciidoctor::Document.new assert doc.extensions? assert doc.extensions.is_a? Asciidoctor::Extensions::Registry @@ -234,79 +252,102 @@ context 'Extensions' do test 'should instantiate preprocessors' do registry = Asciidoctor::Extensions::Registry.new registry.preprocessor SamplePreprocessor + registry.activate Asciidoctor::Document.new assert registry.preprocessors? - processors = registry.load_preprocessors Asciidoctor::Document.new - assert_equal 1, processors.size - assert processors.first.is_a? SamplePreprocessor + extensions = registry.preprocessors + assert_equal 1, extensions.size + assert extensions.first.is_a? Asciidoctor::Extensions::ProcessorExtension + assert extensions.first.instance.is_a? SamplePreprocessor + assert extensions.first.process_method.is_a? ::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? - processors = registry.load_include_processors Asciidoctor::Document.new - assert_equal 1, processors.size - assert processors.first.is_a? SampleIncludeProcessor + extensions = registry.include_processors + assert_equal 1, extensions.size + assert extensions.first.is_a? Asciidoctor::Extensions::ProcessorExtension + assert extensions.first.instance.is_a? SampleIncludeProcessor + assert extensions.first.process_method.is_a? ::Method end test 'should instantiate treeprocessors' do registry = Asciidoctor::Extensions::Registry.new registry.treeprocessor SampleTreeprocessor + registry.activate Asciidoctor::Document.new assert registry.treeprocessors? - processors = registry.load_treeprocessors Asciidoctor::Document.new - assert_equal 1, processors.size - assert processors.first.is_a? SampleTreeprocessor + extensions = registry.treeprocessors + assert_equal 1, extensions.size + assert extensions.first.is_a? Asciidoctor::Extensions::ProcessorExtension + assert extensions.first.instance.is_a? SampleTreeprocessor + assert extensions.first.process_method.is_a? ::Method end test 'should instantiate postprocessors' do registry = Asciidoctor::Extensions::Registry.new registry.postprocessor SamplePostprocessor + registry.activate Asciidoctor::Document.new assert registry.postprocessors? - processors = registry.load_postprocessors Asciidoctor::Document.new - assert_equal 1, processors.size - assert processors.first.is_a? SamplePostprocessor + extensions = registry.postprocessors + assert_equal 1, extensions.size + assert extensions.first.is_a? Asciidoctor::Extensions::ProcessorExtension + assert extensions.first.instance.is_a? SamplePostprocessor + assert extensions.first.process_method.is_a? ::Method end test 'should instantiate block processor' do registry = Asciidoctor::Extensions::Registry.new - registry.block :sample, SampleBlock + registry.block SampleBlock, :sample + registry.activate Asciidoctor::Document.new assert registry.blocks? - assert registry.processor_registered_for_block? :sample, :paragraph - processor = registry.load_block_processor :sample, Asciidoctor::Document.new - assert processor.is_a? SampleBlock + assert registry.registered_for_block? :sample, :paragraph + extension = registry.find_block_extension :sample + assert extension.is_a? Asciidoctor::Extensions::ProcessorExtension + assert extension.instance.is_a? SampleBlock + assert extension.process_method.is_a? ::Method end test 'should not match block processor for unsupported context' do registry = Asciidoctor::Extensions::Registry.new - registry.block :sample, SampleBlock - assert !(registry.processor_registered_for_block? :sample, :sidebar) + registry.block SampleBlock, :sample + registry.activate Asciidoctor::Document.new + assert !(registry.registered_for_block? :sample, :sidebar) end test 'should instantiate block macro processor' do registry = Asciidoctor::Extensions::Registry.new - registry.block_macro 'sample', SampleBlockMacro + registry.block_macro SampleBlockMacro, 'sample' + registry.activate Asciidoctor::Document.new assert registry.block_macros? - assert registry.processor_registered_for_block_macro? 'sample' - processor = registry.load_block_macro_processor 'sample', Asciidoctor::Document.new - assert processor.is_a? SampleBlockMacro + assert registry.registered_for_block_macro? 'sample' + extension = registry.find_block_macro_extension 'sample' + assert extension.is_a? Asciidoctor::Extensions::ProcessorExtension + assert extension.instance.is_a? SampleBlockMacro + assert extension.process_method.is_a? ::Method end test 'should instantiate inline macro processor' do registry = Asciidoctor::Extensions::Registry.new - registry.inline_macro 'sample', SampleInlineMacro + registry.inline_macro SampleInlineMacro, 'sample' + registry.activate Asciidoctor::Document.new assert registry.inline_macros? - assert registry.processor_registered_for_inline_macro? 'sample' - processor = registry.load_inline_macro_processor 'sample', Asciidoctor::Document.new - assert processor.is_a? SampleInlineMacro + assert registry.registered_for_inline_macro? 'sample' + extension = registry.find_inline_macro_extension 'sample' + assert extension.is_a? Asciidoctor::Extensions::ProcessorExtension + assert extension.instance.is_a? SampleInlineMacro + assert extension.process_method.is_a? ::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? - processors = registry.load_preprocessors Asciidoctor::Document.new - assert_equal 1, processors.size - assert processors.first.is_a? SamplePreprocessor + extensions = registry.preprocessors + assert_equal 1, extensions.size + assert extensions.first.is_a? Asciidoctor::Extensions::ProcessorExtension end end @@ -321,7 +362,7 @@ sample content EOS begin - Asciidoctor::Extensions.register do |document| + Asciidoctor::Extensions.register do preprocessor ScrubHeaderPreprocessor end @@ -343,7 +384,7 @@ after EOS begin - Asciidoctor::Extensions.register do |document| + Asciidoctor::Extensions.register do include_processor BoilerplateTextIncludeProcessor end @@ -356,6 +397,39 @@ after Asciidoctor::Extensions.unregister_all end end + + test 'should call include processor to process include directive' do + input = <<-EOS +first line + +include::include-file.asciidoc[] + +last line + EOS + + # Safe Mode is not required here + document = empty_document :base_dir => File.expand_path(File.dirname(__FILE__)) + document.extensions.include_processor do + process do |reader, target, attributes| + # demonstrate that push_include normalizes endlines + content = ["include target:: #{target}\n", "\n", "middle line\n"] + reader.push_include content, target, target, 1, attributes + end + end + reader = Asciidoctor::PreprocessorReader.new document, input + lines = [] + lines << reader.read_line + lines << reader.read_line + lines << reader.read_line + assert_equal 'include target:: include-file.asciidoc', lines.last + assert_equal 'include-file.asciidoc: line 2', reader.line_info + while reader.has_more_lines? + lines << reader.read_line + end + source = lines * ::Asciidoctor::EOL + assert_match(/^include target:: include-file.asciidoc$/, source) + assert_match(/^middle line$/, source) + end test 'should invoke treeprocessors after parsing document' do input = <<-EOS @@ -366,7 +440,7 @@ content EOS begin - Asciidoctor::Extensions.register do |document| + Asciidoctor::Extensions.register do treeprocessor ReplaceAuthorTreeprocessor end @@ -385,7 +459,7 @@ content EOS begin - Asciidoctor::Extensions.register do |document| + Asciidoctor::Extensions.register do postprocessor StripAttributesPostprocessor end @@ -403,8 +477,8 @@ Hi there! EOS begin - Asciidoctor::Extensions.register do |document| - block :yell, UppercaseBlock + Asciidoctor::Extensions.register do + block UppercaseBlock end output = render_embedded_string input @@ -421,8 +495,8 @@ snippet::12345[] EOS begin - Asciidoctor::Extensions.register do |document| - block_macro :snippet, SnippetMacro + Asciidoctor::Extensions.register do + block_macro SnippetMacro, :snippet end output = render_embedded_string input @@ -433,20 +507,76 @@ snippet::12345[] end test 'should invoke processor for custom inline macro' do - input = <<-EOS -Room temperature is degrees:25[]. - EOS - begin - Asciidoctor::Extensions.register do |document| - inline_macro :degrees, TemperatureMacro + Asciidoctor::Extensions.register do + inline_macro TemperatureMacro, :degrees end - output = render_embedded_string input, :attributes => {'temperature-unit' => 'F'} + output = render_embedded_string 'Room temperature is degrees:25[C].', :attributes => {'temperature-unit' => 'F'} + assert output.include?('Room temperature is 25.0 °C.') + + output = render_embedded_string 'Room temperature is degrees:25[].', :attributes => {'temperature-unit' => 'F'} assert output.include?('Room temperature is 77.0 °F.') ensure Asciidoctor::Extensions.unregister_all end end + + test 'should not carry over attributes if block processor returns nil' do + begin + Asciidoctor::Extensions.register do + block do + named :skip + on_context :paragraph + parse_content_as :raw + process do |parent, reader, attrs| + nil + end + end + end + input = <<-EOS +.unused title +[skip] +not rendered + +-- +rendered +-- + EOS + doc = document_from_string input + assert_equal 1, doc.blocks.size + assert_nil doc.blocks[0].attributes['title'] + ensure + Asciidoctor::Extensions.unregister_all + end + end + + test 'should pass attributes by value to block processor' do + begin + Asciidoctor::Extensions.register do + block do + named :foo + on_context :paragraph + parse_content_as :raw + process do |parent, reader, attrs| + original_attrs = attrs.dup + attrs.delete('title') + create_paragraph parent, reader.read_lines, original_attrs.merge('id' => 'value') + end + end + end + input = <<-EOS +.title +[foo] +content + EOS + doc = document_from_string input + assert_equal 1, doc.blocks.size + assert_equal 'title', doc.blocks[0].attributes['title'] + assert_equal 'value', doc.blocks[0].id + ensure + Asciidoctor::Extensions.unregister_all + end + end end end diff --git a/test/reader_test.rb b/test/reader_test.rb index f6cc31e9..5eb19da7 100644 --- a/test/reader_test.rb +++ b/test/reader_test.rb @@ -732,48 +732,6 @@ include::fixtures/basic-docinfo.xml[lines=2..3, indent=0] result = xmlnodes_at_xpath('//pre', output, 1).text assert_equal "<year>2013</year>\n<holder>Acme™, Inc.</holder>", result end - - test 'include processor is called to process include directive' do - input = <<-EOS -first line - -include::include-file.asciidoc[] - -last line - EOS - - include_processor = Class.new { - def initialize document - end - - def handles? target - true - end - - def process reader, target, attributes - # demonstrate that push_include normalizes endlines - content = ["include target:: #{target}\n", "\n", "middle line\n"] - reader.push_include content, target, target, 1, attributes - end - } - - # Safe Mode is not required - document = empty_document :base_dir => DIRNAME - reader = Asciidoctor::PreprocessorReader.new document, input - reader.instance_variable_set '@include_processors', [include_processor.new(document)] - lines = [] - lines << reader.read_line - lines << reader.read_line - lines << reader.read_line - assert_equal 'include target:: include-file.asciidoc', lines.last - assert_equal 'include-file.asciidoc: line 2', reader.line_info - while reader.has_more_lines? - lines << reader.read_line - end - source = lines * ::Asciidoctor::EOL - assert_match(/^include target:: include-file.asciidoc$/, source) - assert_match(/^middle line$/, source) - end test 'should fall back to built-in include directive behavior when not handled by include processor' do input = <<-EOS |
