summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Allen <dan.j.allen@gmail.com>2014-02-16 01:26:04 -0700
committerDan Allen <dan.j.allen@gmail.com>2014-02-16 01:26:04 -0700
commit16b64087bc066b99e33976ed4901ec83bb3bbbb1 (patch)
tree18de00f589a6c0f1ea014d1ae859000f2c59a642
parent77091bf96780c738027a1846f4a8a0cc8809b8dd (diff)
parent02b956f70a129291bb63a1c04a03b6c08c61323c (diff)
Merge pull request #890 from mojavelinux/issue-804
resolves #804 rewrite extensions to support extension instances
-rw-r--r--Rakefile4
-rw-r--r--[-rwxr-xr-x]lib/asciidoctor.rb4
-rw-r--r--lib/asciidoctor/abstract_block.rb20
-rw-r--r--lib/asciidoctor/abstract_node.rb6
-rw-r--r--lib/asciidoctor/backends/html5.rb8
-rw-r--r--lib/asciidoctor/block.rb29
-rw-r--r--lib/asciidoctor/cli/options.rb27
-rw-r--r--lib/asciidoctor/document.rb67
-rw-r--r--lib/asciidoctor/extensions.rb1405
-rw-r--r--lib/asciidoctor/helpers.rb1
-rw-r--r--lib/asciidoctor/lexer.rb235
-rw-r--r--lib/asciidoctor/reader.rb16
-rw-r--r--lib/asciidoctor/substitutors.rb52
-rw-r--r--test/extensions_test.rb316
-rw-r--r--test/reader_test.rb42
15 files changed, 1603 insertions, 629 deletions
diff --git a/Rakefile b/Rakefile
index 5a3f9e75..2c8e2d3a 100644
--- a/Rakefile
+++ b/Rakefile
@@ -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 &lt;test&gt;"
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} &#176;C)
- elsif temperature_unit == 'F'
- f = c * 1.8 + 32
- text = %(#{f} &#176;F)
+ case units
+ when 'C'
+ %(#{c} &#176;C)
+ when 'F'
+ %(#{c * 1.8 + 32 } &#176;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 &#176;C.')
+
+ output = render_embedded_string 'Room temperature is degrees:25[].', :attributes => {'temperature-unit' => 'F'}
assert output.include?('Room temperature is 77.0 &#176;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