# frozen_string_literal: true
(require 'asciidoctor' unless defined? Asciidoctor.load) unless RUBY_ENGINE == 'opal'
module Asciidoctor
# Extensions provide a way to participate in the parsing and converting
# 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. Conversion of the document begins, at which point inline markup is processed
# and converted. Custom inline macros are processed by associated {InlineMacroProcessor}s.
# 5. {Postprocessor}s modify or replace the converted 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
# 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
# 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
# Mixes the DSL class for this processor into this processor class or instance.
#
# This method automatically detects whether to use the include or extend keyword to mix in the module.
#
# NOTE Inspiration for this DSL design comes from https://corcoran.io/2013/09/04/simple-pattern-ruby-dsl/
#
# Returns self
def enable_dsl
if const_defined? :DSL
if singleton_class?
include const_get :DSL
else
extend const_get :DSL
end
end
end
alias use_dsl enable_dsl
end
# Public: Get the configuration Hash for this processor instance.
attr_reader :config
def initialize config = {}
@config = self.class.config.merge config
end
def update_config config
@config.update config
end
def process *args
raise ::NotImplementedError, %(#{Processor} subclass #{self.class} must implement the ##{__method__} method)
end
# QUESTION should attributes be an option instead of a parameter?
# Public: Creates a new Section node.
#
# Creates a Section node in the same manner as the parser.
#
# parent - The parent Section (or Document) of this new Section.
# title - The String title of the new Section.
# attrs - A Hash of attributes to control how the section is built.
# Use the style attribute to set the name of a special section (ex. appendix).
# Use the id attribute to assign an explicit ID or set the value to false to
# disable automatic ID generation (when sectids document attribute is set).
# opts - An optional Hash of options (default: {}):
# :level - [Integer] The level to assign to this section; defaults to
# one greater than the parent level (optional).
# :numbered - [Boolean] A flag to force numbering, which falls back to the
# state of the sectnums document attribute (optional).
#
# Returns a [Section] node with all properties properly initialized.
def create_section parent, title, attrs, opts = {}
doc = parent.document
book = (doctype = doc.doctype) == 'book'
level = opts[:level] || parent.level + 1
if (style = attrs.delete 'style')
if book && style == 'abstract'
sectname, level = 'chapter', 1
else
sectname, special = style, true
level = 1 if level == 0
end
elsif book
sectname = level == 0 ? 'part' : (level > 1 ? 'section' : 'chapter')
elsif doctype == 'manpage' && (title.casecmp 'synopsis') == 0
sectname, special = 'synopsis', true
else
sectname = 'section'
end
sect = Section.new parent, level
sect.title, sect.sectname = title, sectname
if special
sect.special = true
if opts.fetch :numbered, (style == 'appendix')
sect.numbered = true
elsif !(opts.key? :numbered) && (doc.attr? 'sectnums', 'all')
sect.numbered = (book && level == 1 ? :chapter : true)
end
elsif level > 0
if opts.fetch :numbered, (doc.attr? 'sectnums')
sect.numbered = sect.special ? parent.numbered && true : true
end
elsif opts.fetch :numbered, (book && (doc.attr? 'partnums'))
sect.numbered = true
end
if (id = attrs['id']) == false
attrs.delete 'id'
else
sect.id = attrs['id'] = id || ((doc.attr? 'sectids') ? (Section.generate_id sect.title, doc) : nil)
end
sect.update_attributes attrs
sect
end
def create_block parent, context, source, attrs, opts = {}
Block.new parent, context, { source: source, attributes: attrs }.merge(opts)
end
# Public: Creates a list node and links it to the specified parent.
#
# parent - The parent Block (Block, Section, or Document) of this new list block.
# context - The list context (e.g., :ulist, :olist, :colist, :dlist)
# attrs - A Hash of attributes to set on this list block
#
# Returns a [List] node with all properties properly initialized.
def create_list parent, context, attrs = nil
list = List.new parent, context
list.update_attributes attrs if attrs
list
end
# Public: Creates a list item node and links it to the specified parent.
#
# parent - The parent List of this new list item block.
# text - The text of the list item.
#
# Returns a [ListItem] node with all properties properly initialized.
def create_list_item parent, text = nil
ListItem.new parent, text
end
# Public: Creates an image block node and links it to the specified parent.
#
# parent - The parent Block (Block, Section, or Document) of this new image block.
# attrs - A Hash of attributes to control how the image block is built.
# Use the target attribute to set the source of the image.
# Use the alt attribute to specify an alternative text for the image.
# opts - An optional Hash of options (default: {})
#
# Returns a [Block] node with all properties properly initialized.
def create_image_block parent, attrs, opts = {}
unless (target = attrs['target'])
raise ::ArgumentError, 'Unable to create an image block, target attribute is required'
end
attrs['alt'] ||= (attrs['default-alt'] = Helpers.basename(target, true).tr('_-', ' '))
title = (attrs.key? 'title') ? (attrs.delete 'title') : nil
block = create_block parent, :image, nil, attrs, opts
if title
block.title = title
block.assign_caption (attrs.delete 'caption'), 'figure'
end
block
end
def create_inline parent, context, text, opts = {}
Inline.new parent, context, text, context == :quoted ? ({ type: :unquoted }.merge opts) : opts
end
# Public: Parses blocks in the content and attaches the block to the parent.
#
# Returns The parent node into which the blocks are parsed.
#--
# QUESTION is parse_content the right method name? should we wrap in open block automatically?
def parse_content parent, content, attributes = nil
reader = Reader === content ? content : (Reader.new content)
Parser.parse_blocks reader, parent, attributes
parent
end
# Public: Parses the attrlist String into a Hash of attributes
#
# block - the current AbstractBlock or the parent AbstractBlock if there is no current block (used for applying subs)
# attrlist - the list of attributes as a String
# opts - an optional Hash of options to control processing:
# :positional_attributes - an Array of attribute names to map positional arguments to (optional, default: false)
# :sub_attributes - enables attribute substitution on the attrlist argument (optional, default: false)
#
# Returns a Hash of parsed attributes
def parse_attributes block, attrlist, opts = {}
return {} if attrlist ? attrlist.empty? : true
attrlist = block.sub_attributes attrlist if opts[:sub_attributes] && (attrlist.include? ATTR_REF_HEAD)
(AttributeList.new attrlist).parse opts[:positional_attributes] || []
end
# 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],
[:create_inline_pass, :create_inline, :quoted],
].each do |method_name, delegate_method_name, context|
define_method method_name do |*args|
args.unshift args.shift, context
send delegate_method_name, *args
end
end
end
# 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
if block_given?
raise ::ArgumentError, %(wrong number of arguments (given #{args.size}, expected 0)) unless args.empty?
unless block.binding && self == block.binding.receiver
# NOTE remap self in process method to processor instance
context = self
block.define_singleton_method(:call) {|*m_args| context.instance_exec(*m_args, &block) }
end
@process_block = block
# TODO enable if we want to support passing proc or lambda as argument instead of block
#elsif ::Proc === args[0]
# raise ::ArgumentError, %(wrong number of arguments (given #{args.size - 1}, expected 0)) unless args.size == 1
# @process_block = args.shift
elsif defined? @process_block
@process_block.call(*args)
else
raise ::NotImplementedError, %(#{self.class} ##{__method__} method called before being registered)
end
end
def process_block_given?
defined? @process_block
end
end
module DocumentProcessorDsl
include ProcessorDsl
def prefer
option :position, :>>
end
end
module SyntaxProcessorDsl
include ProcessorDsl
def named value
# NOTE due to how processors get initialized, we must defer this assignment in some scenarios
if Processor === self
@name = value
else
option :name, value
end
end
def content_model value
option :content_model, value
end
alias parse_content_as content_model
def positional_attributes *value
option :positional_attrs, value.flatten
end
alias name_positional_attributes positional_attributes
# NOTE positional_attrs alias is deprecated
alias positional_attrs positional_attributes
def default_attributes value
option :default_attrs, value
end
# NOTE default_attrs alias is deprecated
alias default_attrs default_attributes
def resolve_attributes *args
# NOTE assume true as default value; rewrap single-argument string or symbol
if (args = args.fetch 0, true).respond_to? :to_sym
args = [args]
end unless args.size > 1
case args
when true
option :positional_attrs, []
option :default_attrs, {}
when ::Array
names, defaults = [], {}
args.each do |arg|
if (arg = arg.to_s).include? '='
name, _, value = arg.partition '='
if name.include? ':'
idx, _, name = name.partition ':'
idx = idx == '@' ? names.size : idx.to_i
names[idx] = name
end
defaults[name] = value
elsif arg.include? ':'
idx, _, name = arg.partition ':'
idx = idx == '@' ? names.size : idx.to_i
names[idx] = name
else
names << arg
end
end
option :positional_attrs, names.compact
option :default_attrs, defaults
when ::Hash
names, defaults = [], {}
args.each do |key, val|
if (name = key.to_s).include? ':'
idx, _, name = name.partition ':'
idx = idx == '@' ? names.size : idx.to_i
names[idx] = name
end
defaults[name] = val if val
end
option :positional_attrs, names.compact
option :default_attrs, defaults
else
raise ::ArgumentError, %(unsupported attributes specification for macro: #{args.inspect})
end
end
# NOTE resolves_attributes alias is deprecated
alias resolves_attributes resolve_attributes
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
# and the end of line character sequence from each line.
#
# Asciidoctor passes the document and the document's Reader to the
# {Processor#process} method of the Preprocessor instance. The Preprocessor
# can modify the Reader as necessary and either return the same Reader (or
# falsy, which is equivalent) or a reference to a substitute Reader.
#
# Preprocessor implementations must extend the Preprocessor class.
class Preprocessor < Processor
def process document, reader
raise ::NotImplementedError, %(#{Preprocessor} subclass #{self.class} must implement the ##{__method__} method)
end
end
Preprocessor::DSL = DocumentProcessorDsl
# 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 tree processor get invoked after parse header too?
class TreeProcessor < Processor
def process document
raise ::NotImplementedError, %(#{TreeProcessor} subclass #{self.class} must implement the ##{__method__} method)
end
end
TreeProcessor::DSL = DocumentProcessorDsl
# Alias deprecated class name for backwards compatibility
Treeprocessor = TreeProcessor
# Public: Postprocessors are run after the document is converted, but before
# it is written to the output stream.
#
# Asciidoctor passes a reference to the converted 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 convert
# 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 extend Postprocessor.
class Postprocessor < Processor
def process document, output
raise ::NotImplementedError, %(#{Postprocessor} subclass #{self.class} must implement the ##{__method__} method)
end
end
Postprocessor::DSL = DocumentProcessorDsl
# 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 as shortcut for handles? method
class IncludeProcessor < Processor
def process document, reader, target, attributes
raise ::NotImplementedError, %(#{IncludeProcessor} subclass #{self.class} must implement the ##{__method__} method)
end
def handles? target
true
end
end
module IncludeProcessorDsl
include DocumentProcessorDsl
def handles? *args, &block
if block_given?
raise ::ArgumentError, %(wrong number of arguments (given #{args.size}, expected 0)) unless args.empty?
@handles_block = block
# TODO enable if we want to support passing proc or lambda as argument instead of block
#elsif ::Proc === args[0]
# block = args.shift
# raise ::ArgumentError, %(wrong number of arguments (given #{args.size}, expected 0)) unless args.empty?
# @handles_block = block
elsif defined? @handles_block
@handles_block.call args[0]
else
true
end
end
end
IncludeProcessor::DSL = IncludeProcessorDsl
# Public: DocinfoProcessors are used to add additional content to
# the header and/or footer of the generated document.
#
# The placement of docinfo content is controlled by the converter.
#
# DocinfoProcessors implementations must extend DocinfoProcessor.
# If a location is not specified, the DocinfoProcessor is assumed
# to add content to the header.
class DocinfoProcessor < Processor
def initialize config = {}
super config
@config[:location] ||= :head
end
def process document
raise ::NotImplementedError, %(#{DocinfoProcessor} subclass #{self.class} must implement the ##{__method__} method)
end
end
module DocinfoProcessorDsl
include DocumentProcessorDsl
def at_location value
option :location, value
end
end
DocinfoProcessor::DSL = DocinfoProcessorDsl
# 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 corresponding node in the document tree.
#
# If the process method returns an instance of Block, the content model of that
# Block is :compound, and the Block contains at least one line, the parser will
# parse those lines into blocks an assigned them to the returned block.
#
# AsciiDoc example:
#
# [shout]
# Get a move on.
#
# Recognized options:
#
# * :named - The name of the block (required: true)
# * :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)
# * :positional_attrs - A list of attribute names used to map positional attributes (default: nil)
# * :default_attrs - A hash of attribute names and values used to seed the attributes hash (default: nil)
# * ...
#
# BlockProcessor implementations must extend BlockProcessor.
class BlockProcessor < Processor
attr_accessor :name
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
@config[:contexts] = @config[:contexts].to_set
end
# QUESTION should the default content model be raw??
@config[:content_model] ||= :compound
end
def process parent, reader, attributes
raise ::NotImplementedError, %(#{BlockProcessor} subclass #{self.class} must implement the ##{__method__} method)
end
end
module BlockProcessorDsl
include SyntaxProcessorDsl
def contexts *value
option :contexts, value.flatten.to_set
end
alias on_contexts contexts
alias on_context contexts
alias bind_to contexts
end
BlockProcessor::DSL = BlockProcessorDsl
class MacroProcessor < Processor
attr_accessor :name
def initialize name = nil, config = {}
super config
@name = name || @config[:name]
@config[:content_model] ||= :attributes
end
def process parent, target, attributes
raise ::NotImplementedError, %(#{MacroProcessor} subclass #{self.class} must implement the ##{__method__} method)
end
end
module MacroProcessorDsl
include SyntaxProcessorDsl
def resolve_attributes *args
if args.size == 1 && !args[0]
option :content_model, :text
else
super
option :content_model, :attributes
end
end
# NOTE resolves_attributes alias is deprecated
alias resolves_attributes resolve_attributes
end
# Public: BlockMacroProcessors are used to handle block macros that have a
# custom name.
#
# If the process method returns an instance of Block, the content model of that
# Block is :compound, and the Block contains at least one line, the parser will
# parse those lines into blocks an assigned them to the returned block.
#
# BlockMacroProcessor implementations must extend BlockMacroProcessor.
class BlockMacroProcessor < MacroProcessor
def name
raise ::ArgumentError, %(invalid name for block macro: #{@name}) unless MacroNameRx.match? @name.to_s
@name
end
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, FullInlineMacro, ShortInlineMacro (no target) and other patterns
# FIXME for inline macro, we need to have some way to specify the text as a passthrough
class InlineMacroProcessor < MacroProcessor
@@rx_cache = {}
# Lookup the regexp option, resolving it first if necessary.
# Once this method is called, the regexp is considered frozen.
def regexp
@config[:regexp] ||= resolve_regexp @name.to_s, @config[:format]
end
def resolve_regexp name, format
raise ::ArgumentError, %(invalid name for inline macro: #{name}) unless MacroNameRx.match? name
@@rx_cache[[name, format]] ||= /\\?#{name}:#{format == :short ? '(){0}' : '(\S+?)'}\[(|#{CC_ANY}*?[^\\])\]/
end
end
module InlineMacroProcessorDsl
include MacroProcessorDsl
def format value
option :format, value
end
alias match_format format
# NOTE using_format alias is deprecated
alias using_format format
def match value
option :regexp, value
end
end
InlineMacroProcessor::DSL = InlineMacroProcessorDsl
# 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_reader :kind
attr_reader :config
attr_reader :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 accommodate both concrete extension implementations and Procs.
class ProcessorExtension < Extension
attr_reader :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 activate registry
raise ::NotImplementedError
end
end
# 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 Hash of {Group} classes, instances, and/or Procs that have been registered with this registry.
attr_reader :groups
def initialize groups = {}
@groups = groups
@preprocessor_extensions = @tree_processor_extensions = @postprocessor_extensions = @include_processor_extensions = @docinfo_processor_extensions = @block_extensions = @block_macro_extensions = @inline_macro_extensions = nil
@document = nil
end
# 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
unless (ext_groups = Extensions.groups.values + @groups.values).empty?
ext_groups.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
group.activate self
end
end
end
self
end
# 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 do |doc, reader|
# ...
# 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
# Public: Checks whether any {Preprocessor} extensions have been registered.
#
# Returns a [Boolean] indicating whether any Preprocessor extensions are registered.
def preprocessors?
!!@preprocessor_extensions
end
# 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
# 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
# tree_processor ShellTreeProcessor
#
# # as an instance of a TreeProcessor subclass
# tree_processor ShellTreeProcessor.new
#
# # as a name of a TreeProcessor subclass
# tree_processor 'ShellTreeProcessor'
#
# # as a method block
# tree_processor do
# process do |document|
# ...
# end
# end
#
# Returns the [Extension] stored in the registry that proxies the
# instance of this TreeProcessor.
def tree_processor *args, &block
add_document_processor :tree_processor, args, &block
end
# Public: Checks whether any {TreeProcessor} extensions have been registered.
#
# Returns a [Boolean] indicating whether any TreeProcessor extensions are registered.
def tree_processors?
!!@tree_processor_extensions
end
# Public: Retrieves the {Extension} proxy objects for all
# TreeProcessor instances in this registry.
#
# Returns an [Array] of Extension proxy objects.
def tree_processors
@tree_processor_extensions
end
# Alias deprecated methods for backwards compatibility
alias treeprocessor tree_processor
alias treeprocessors? tree_processors?
alias treeprocessors tree_processors
# Public: Registers a {Postprocessor} with the extension registry to process
# the output after conversion 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 do |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
# Public: Checks whether any {Postprocessor} extensions have been registered.
#
# Returns a [Boolean] indicating whether any Postprocessor extensions are registered.
def postprocessors?
!!@postprocessor_extensions
end
# 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
# 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 do |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
# 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
# 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
# Public: Registers an {DocinfoProcessor} with the extension registry to
# add additional docinfo to the document.
#
# The DocinfoProcessor may be one of four types:
#
# * A DocinfoProcessor subclass
# * An instance of a DocinfoProcessor subclass
# * The String name of a DocinfoProcessor subclass
# * A method block (i.e., Proc) that conforms to the DocinfoProcessor contract
#
# Unless the DocinfoProcessor is passed as the method block, it must be the
# first argument to this method.
#
# Examples
#
# # as an DocinfoProcessor subclass
# docinfo_processor MetaRobotsDocinfoProcessor
#
# # as an instance of a DocinfoProcessor subclass with an explicit location
# docinfo_processor JQueryDocinfoProcessor.new, location: :footer
#
# # as a name of a DocinfoProcessor subclass
# docinfo_processor 'MetaRobotsDocinfoProcessor'
#
# # as a method block
# docinfo_processor do
# process do |doc|
# at_location :footer
# 'footer content'
# end
# end
#
# Returns the [Extension] stored in the registry that proxies the
# instance of this DocinfoProcessor.
def docinfo_processor *args, &block
add_document_processor :docinfo_processor, args, &block
end
# Public: Checks whether any {DocinfoProcessor} extensions have been registered.
#
# location - A Symbol for selecting docinfo extensions at a given location (:head or :footer) (default: nil)
#
# Returns a [Boolean] indicating whether any DocinfoProcessor extensions are registered.
def docinfo_processors? location = nil
if @docinfo_processor_extensions
if location
@docinfo_processor_extensions.any? {|ext| ext.config[:location] == location }
else
true
end
else
false
end
end
# Public: Retrieves the {Extension} proxy objects for all the
# DocinfoProcessor instances stored in this registry.
#
# location - A Symbol for selecting docinfo extensions at a given location (:head or :footer) (default: nil)
#
# Returns an [Array] of Extension proxy objects.
def docinfo_processors location = nil
if @docinfo_processor_extensions
if location
@docinfo_processor_extensions.select {|ext| ext.config[:location] == location }
else
@docinfo_processor_extensions
end
end
end
# 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 do |parent, reader, attrs|
# ...
# end
# end
#
# # as a method block with an explicit block name
# block :shout do
# process do |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
# Public: Checks whether any {BlockProcessor} extensions have been registered.
#
# Returns a [Boolean] indicating whether any BlockProcessor extensions are registered.
def blocks?
!!@block_extensions
end
# 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
# Public: Retrieves the {Extension} proxy object for the BlockProcessor registered
# to handle block content with the name.
#
# 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
# 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_macro GistBlockMacro
#
# # as a BlockMacroProcessor subclass with an explicit macro name
# block_macro GistBlockMacro, :gist
#
# # as an instance of a BlockMacroProcessor subclass
# block_macro GistBlockMacro.new
#
# # as an instance of a BlockMacroProcessor subclass with an explicit macro name
# block_macro GistBlockMacro.new, :gist
#
# # as a name of a BlockMacroProcessor subclass
# block_macro 'GistBlockMacro'
#
# # as a name of a BlockMacroProcessor subclass with an explicit macro name
# block_macro 'GistBlockMacro', :gist
#
# # as a method block
# block_macro do
# named :gist
# process do |parent, target, attrs|
# ...
# end
# end
#
# # as a method block with an explicit macro name
# block_macro :gist do
# process do |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
# 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
# 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
# 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
# corresponding BlockMacroProcessor or nil if a match is not found.
def find_block_macro_extension name
@block_macro_extensions[name.to_sym]
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
# inline_macro ChromeInlineMacro
#
# # as an InlineMacroProcessor subclass with an explicit macro name
# inline_macro ChromeInlineMacro, :chrome
#
# # as an instance of an InlineMacroProcessor subclass
# inline_macro ChromeInlineMacro.new
#
# # as an instance of an InlineMacroProcessor subclass with an explicit macro name
# inline_macro ChromeInlineMacro.new, :chrome
#
# # as a name of an InlineMacroProcessor subclass
# inline_macro 'ChromeInlineMacro'
#
# # as a name of an InlineMacroProcessor subclass with an explicit macro name
# inline_macro 'ChromeInlineMacro', :chrome
#
# # as a method block
# inline_macro do
# named :chrome
# process do |parent, target, attrs|
# ...
# end
# end
#
# # as a method block with an explicit macro name
# inline_macro :chrome do
# process do |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
# 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
# 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
# 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
# corresponding InlineMacroProcessor or nil if a match is not found.
def find_inline_macro_extension name
@inline_macro_extensions[name.to_sym]
end
# 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
# Public: Inserts the document processor {Extension} instance as the first
# processor of its kind in the extension registry.
#
# Examples
#
# prefer :include_processor do
# process do |document, reader, target, attrs|
# ...
# end
# end
#
# Returns the [Extension] stored in the registry that proxies the instance
# of this processor.
def prefer *args, &block
extension = ProcessorExtension === (arg0 = args.shift) ? arg0 : (send arg0, *args, &block)
extensions_store = instance_variable_get(%(@#{extension.kind}_extensions).to_sym)
extensions_store.unshift extensions_store.delete extension
extension
end
private
def add_document_processor kind, args, &block
kind_name = kind.to_s.tr '_', ' '
kind_class_symbol = kind_name.split.map {|it| it.capitalize }.join.to_sym
kind_class = Extensions.const_get kind_class_symbol, false
kind_java_class = (defined? ::AsciidoctorJ) ? (::AsciidoctorJ::Extensions.const_get kind_class_symbol, false) : 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?
config = resolve_args args, 1
(processor = kind_class.new config).singleton_class.enable_dsl
if block.arity == 0
processor.instance_exec(&block)
else
yield processor
end
unless processor.process_block_given?
raise ::ArgumentError, %(No block specified to process #{kind_name} extension at #{block.source_location})
end
processor.freeze
extension = ProcessorExtension.new kind, processor
else
processor, config = resolve_args args, 2
# style 2: specified as Class or String class name
if (processor_class = Helpers.resolve_class processor)
unless processor_class < kind_class || (kind_java_class && processor_class < kind_java_class)
raise ::ArgumentError, %(Invalid type for #{kind_name} extension: #{processor})
end
processor_instance = processor_class.new config
processor_instance.freeze
extension = ProcessorExtension.new kind, processor_instance
# style 3: specified as instance
elsif kind_class === processor || (kind_java_class && kind_java_class === processor)
processor.update_config config
processor.freeze
extension = ProcessorExtension.new kind, processor
else
raise ::ArgumentError, %(Invalid arguments specified for registering #{kind_name} extension: #{args})
end
end
extension.config[:position] == :>> ? (kind_store.unshift extension) : (kind_store << extension)
extension
end
def add_syntax_processor kind, args, &block
kind_name = kind.to_s.tr '_', ' '
kind_class_symbol = (kind_name.split.map {|it| it.capitalize } << 'Processor').join.to_sym
kind_class = Extensions.const_get kind_class_symbol, false
kind_java_class = (defined? ::AsciidoctorJ) ? (::AsciidoctorJ::Extensions.const_get kind_class_symbol, false) : 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).singleton_class.enable_dsl
if block.arity == 0
processor.instance_exec(&block)
else
yield processor
end
unless (name = as_symbol processor.name)
raise ::ArgumentError, %(No name specified for #{kind_name} extension at #{block.source_location})
end
unless processor.process_block_given?
raise ::NoMethodError, %(No block specified to process #{kind_name} extension at #{block.source_location})
end
processor.freeze
kind_store[name] = ProcessorExtension.new kind, processor
else
processor, name, config = resolve_args args, 3
# style 2: specified as Class or String class name
if (processor_class = Helpers.resolve_class processor)
unless processor_class < kind_class || (kind_java_class && processor_class < kind_java_class)
raise ::ArgumentError, %(Class specified for #{kind_name} extension does not inherit from #{kind_class}: #{processor})
end
processor_instance = processor_class.new as_symbol(name), config
unless (name = as_symbol processor_instance.name)
raise ::ArgumentError, %(No name specified for #{kind_name} extension: #{processor})
end
processor_instance.freeze
kind_store[name] = ProcessorExtension.new kind, processor_instance
# style 3: specified as instance
elsif kind_class === processor || (kind_java_class && kind_java_class === processor)
processor.update_config config
# TODO need a test for this override!
unless (name = name ? (processor.name = as_symbol name) : (as_symbol processor.name))
raise ::ArgumentError, %(No name specified for #{kind_name} extension: #{processor})
end
processor.freeze
kind_store[name] = ProcessorExtension.new kind, processor
else
raise ::ArgumentError, %(Invalid arguments specified for registering #{kind_name} extension: #{args})
end
end
end
def resolve_args args, expect
opts = ::Hash === args[-1] ? args.pop : {}
return opts if expect == 1
if (missing = expect - 1 - args.size) > 0
args += (::Array.new missing)
elsif missing < 0
args.pop(-missing)
end
args << opts
args
end
def as_symbol name
name ? name.to_sym : nil
end
end
class << self
def generate_name
%(extgrp#{next_auto_id})
end
def next_auto_id
@auto_id ||= -1
@auto_id += 1
end
def groups
@groups ||= {}
end
def create name = nil, &block
if block_given?
Registry.new (name || generate_name) => block
else
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.size
if block_given?
resolved_group = block
elsif (group = args.pop)
# QUESTION should we instantiate the group class here or defer until activation??
resolved_group = (Helpers.resolve_class group) || group
else
raise ::ArgumentError, %(Extension group to register not specified)
end
name = args.pop || generate_name
unless args.empty?
raise ::ArgumentError, %(Wrong number of arguments (#{argc} for 1..2))
end
groups[name.to_sym] = resolved_group
end
# Public: Unregister all statically-registered extension groups.
#
# Returns nothing
def unregister_all
@groups = {}
nil
end
# Public: Unregister statically-registered extension groups by name.
#
# names - one or more Symbol or String group names to unregister
#
# Returns nothing
def unregister *names
names.each {|group| @groups.delete group.to_sym }
nil
end
end
end
end