# encoding: UTF-8 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 # 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. # # NOTE Inspiration for this DSL design comes from https://corcoran.io/2013/09/04/simple-pattern-ruby-dsl/ # # 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 # 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, %(Asciidoctor::Extensions::Processor subclass must implement ##{__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 doctype, level = doc.doctype, (opts[:level] || parent.level + 1) if (style = attrs.delete 'style') if style == 'abstract' && doctype == 'book' sectname, level = 'chapter', 1 else sectname, special = style, true level = 1 if level == 0 end elsif doctype == 'book' sectname = level == 0 ? 'part' : (level == 1 ? 'chapter' : 'section') elsif doctype == 'manpage' && (title.casecmp 'synopsis') == 0 sectname, special = 'synopsis', true else sectname = 'section' end sect = Section.new parent, level, false sect.title, sect.sectname = title, sectname if special sect.special = true sect.numbered = true if opts.fetch :numbered, (style == 'appendix') elsif opts.fetch :numbered, (level > 0 && (doc.attributes.key? 'sectnums')) sect.numbered = sect.special ? (parent.context == :section && parent.numbered) : true end unless (id = attrs.delete 'id') == false sect.id = attrs['id'] = id || ((doc.attributes.key? '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 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 alternate 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('_-', ' ')) 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 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) while ((block = Parser.next_block reader, parent, (attributes ? attributes.dup : {})) && parent << block) || reader.has_more_lines? end parent 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] ].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? @process_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? # @process_block = block elsif defined? @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 # TODO add exception message here raise ::NotImplementedError end end def process_block_given? defined? @process_block end end module SyntaxDsl 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 # NOTE match_name may get deprecated alias match_name named def content_model value option :content_model, value end alias parse_content_as content_model alias parses_content_as content_model #alias parse_as content_model #alias parsed_as content_model def positional_attrs *value option :pos_attrs, value.flatten end alias name_attributes positional_attrs alias name_positional_attributes positional_attrs def default_attrs value option :default_attrs, value end def resolves_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 :pos_attrs, [] option :default_attrs, {} when ::Array names, defaults = [], {} args.each do |arg| if (arg = arg.to_s).include? '=' name, value = arg.split '=', 2 if name.include? ':' idx, name = name.split ':', 2 idx = idx == '@' ? names.size : idx.to_i names[idx] = name end defaults[name] = value elsif arg.include? ':' idx, name = arg.split ':', 2 idx = idx == '@' ? names.size : idx.to_i names[idx] = name else names << arg end end option :pos_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.split ':', 2 idx = idx == '@' ? names.size : idx.to_i names[idx] = name end defaults[name] = val if val end option :pos_attrs, names.compact option :default_attrs, defaults else raise ::ArgumentError, %(unsupported attributes specification for macro: #{args.inspect}) end end # NOTE we may decide to drop this alias alias resolve_attributes resolves_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, %(Asciidoctor::Extensions::Preprocessor subclass must implement ##{__method__} method) 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 tree processor get invoked after parse header too? class TreeProcessor < Processor def process document raise ::NotImplementedError, %(Asciidoctor::Extensions::TreeProcessor subclass must implement ##{__method__} method) end end TreeProcessor::DSL = ProcessorDsl # 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 Postprocessor. class Postprocessor < Processor def process document, output raise ::NotImplementedError, %(Asciidoctor::Extensions::Postprocessor subclass must implement ##{__method__} method) end end Postprocessor::DSL = ProcessorDsl # Public: IncludeProcessors are used to process `include::[]` # directives in the source document. # # When Asciidoctor comes across a `include::[]` 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, %(Asciidoctor::Extensions::IncludeProcessor subclass must implement ##{__method__} method) end def handles? target true end end module IncludeProcessorDsl include ProcessorDsl 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 attr_accessor :location def initialize config = {} super config @config[:location] ||= :head end def process document raise ::NotImplementedError, %(Asciidoctor::Extensions::DocinfoProcessor subclass must implement ##{__method__} method) end end module DocinfoProcessorDsl include ProcessorDsl 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. # # 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) # * :pos_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, %(Asciidoctor::Extensions::BlockProcessor subclass must implement ##{__method__} method) end end module BlockProcessorDsl include SyntaxDsl def contexts *value option :contexts, value.flatten.to_set end alias on_contexts contexts alias on_context contexts alias bound_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, %(Asciidoctor::Extensions::MacroProcessor subclass must implement ##{__method__} method) end end module MacroProcessorDsl include SyntaxDsl def resolves_attributes *args if args.size == 1 && !args[0] option :content_model, :text return end super option :content_model, :attributes end # NOTE we may decide to drop this alias alias resolve_attributes resolves_attributes 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, FullInlineMacro, 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 @@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+?)'}\[(|.*?[^\\])\]/ end end module InlineMacroProcessorDsl include MacroProcessorDsl def with_format value option :format, value end alias using_format with_format def matches value option :regexp, value end alias match matches alias matching matches 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 accomodate 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 Array of {Group} classes, instances and/or Procs that have been registered. attr_reader :groups def initialize groups = {} @groups = groups @preprocessor_extensions = @tree_processor_extensions = @postprocessor_extensions = @include_processor_extensions = @docinfo_processor_extensions = nil @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 (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 group.activate self 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 |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 |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 |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 |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 additionnal 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 |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 else nil 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 |parent, reader, attrs| # ... # end # end # # # as a method block with an explicit block name # block :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 # 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 |parent, target, attrs| # ... # end # end # # # as a method block with an explicit macro name # block_macro :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 # 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 # cooresponding 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 ChromeInineMacro, :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 '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 # inline_macro :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 # 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 # cooresponding 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 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 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 # NOTE class << processor idiom doesn't work in Opal #class << processor # include_dsl #end # NOTE kind_class.contants(false) doesn't exist in Ruby 1.8.7 processor.extend kind_class.const_get :DSL if kind_class.constants.grep :DSL processor.instance_exec(&block) processor.freeze unless processor.process_block_given? raise ::ArgumentError, %(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 String class name if (processor_class = Extensions.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 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 ProcessorExtension.new kind, processor else raise ::ArgumentError, %(Invalid arguments specified for registering #{kind_name} extension: #{args}) end end if extension.config[:position] == :>> kind_store.unshift extension else kind_store << extension end end def add_syntax_processor kind, args, &block kind_name = kind.to_s.tr '_', ' ' kind_class_symbol = (kind_name.split.map {|it| it.capitalize }.push 'Processor').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 if block_given? name, config = resolve_args args, 2 processor = kind_class.new as_symbol(name), config # NOTE class << processor idiom doesn't work in Opal #class << processor # include_dsl #end # NOTE kind_class.contants(false) doesn't exist in Ruby 1.8.7 processor.extend kind_class.const_get :DSL if kind_class.constants.grep :DSL if block.arity == 1 yield processor else processor.instance_exec(&block) 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 = Extensions.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 # Deprecated: Use create instead of build_registry alias build_registry create # 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 = (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 # Internal: Resolve the specified object as a Class # # object - The object to resolve as a Class # # Returns a Class if the specified object is a Class (but not a Module) or # a String that resolves to a Class; otherwise, nil def resolve_class object case object when ::Class object when ::String class_for_name object end end # Public: Resolves the Class object for the qualified name. # # Returns Class def class_for_name qualified_name resolved = ::Object (qualified_name.split '::').each do |name| unless name.empty? || ((resolved.const_defined? name) && ::Module === (resolved = resolved.const_get name)) raise ::NameError, %(Could not resolve class for name: #{qualified_name}) end end raise ::NameError, %(Could not resolve class for name: #{qualified_name}) unless ::Class === resolved resolved end end end end