# 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 end def create_block parent, context, source, attrs, opts = {} Block.new parent, context, { :source => source, :attributes => attrs }.merge(opts) end def create_image_block parent, attrs, opts = {} create_block parent, :image, nil, attrs, opts end def create_inline parent, context, text, opts = {} Inline.new parent, context, text, opts end # Public: Parses blocks in the content and attaches the block to the parent. # # Returns nothing #-- # QUESTION is parse_content the right method name? should we wrap in open block automatically? def parse_content parent, content, attributes = {} reader = (content.is_a? Reader) ? reader : (Reader.new content) while reader.has_more_lines? block = Parser.next_block reader, parent, attributes parent << block if block end nil 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| send delegate_method_name, *args.dup.insert(1, context) 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 # need to check for both block/proc and lambda # TODO need test for this! #if block_given? || (args.size == 1 && ((block = args[0]).is_a? ::Proc)) if block_given? @process_block = block elsif @process_block # NOTE Proc automatically expands a single array argument # ...but lambda doesn't (and we want to accept lambdas too) # TODO need a test for this! @process_block.call(*args) else raise ::NotImplementedError end end #alias :process_with :process def process_block_given? defined? @process_block end end # Public: Preprocessors are run after the source text is split into lines and # normalized, but before parsing begins. # # Prior to invoking the preprocessor, Asciidoctor splits the source text into # lines and normalizes them. The normalize process strips trailing whitespace # from each line and leaves behind a line-feed character (i.e., "\n"). # # Asciidoctor passes a reference to the Reader and a copy of the lines Array # to the {Processor#process} method of an instance of each registered # Preprocessor. The Preprocessor modifies the Array as necessary and either # returns a reference to the same Reader or a reference to a new Reader. # # Preprocessor implementations must extend the Preprocessor class. class Preprocessor < Processor def process document, reader raise ::NotImplementedError end end Preprocessor::DSL = ProcessorDsl # Public: Treeprocessors are run on the Document after the source has been # parsed into an abstract syntax tree (AST), as represented by the Document # object and its child Node objects (e.g., Section, Block, List, ListItem). # # Asciidoctor invokes the {Processor#process} method on an instance of each # registered Treeprocessor. # # Treeprocessor implementations must extend Treeprocessor. #-- # QUESTION should the treeprocessor get invoked after parse header too? class Treeprocessor < Processor def process document raise ::NotImplementedError end end Treeprocessor::DSL = ProcessorDsl # Public: Postprocessors are run after the document is 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 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 to shortcut handles? class IncludeProcessor < Processor def process document, reader, target, attributes raise ::NotImplementedError end def handles? target true end end IncludeProcessor::DSL = ProcessorDsl # 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] ||= :header end def process document raise ::NotImplementedError 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 cooresponding 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) # * :positional_attributes - A list of attribute names used to map positional attributes (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 end end module BlockProcessorDsl include ProcessorDsl # FIXME this isn't the prettiest thing def named value if self.is_a? Processor @name = value else option :name, value end end alias :match_name :named alias :bind_to :named def contexts *value option :contexts, value.flatten end alias :on_contexts :contexts alias :on_context :contexts def content_model value option :content_model, value end alias :parse_content_as :content_model def positional_attributes *value option :pos_attrs, value.flatten end alias :pos_attrs :positional_attributes alias :name_attributes :positional_attributes alias :name_positional_attributes :positional_attributes def default_attrs value option :default_attrs, value end alias :seed_attributes_with :default_attrs 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 end end module MacroProcessorDsl include ProcessorDsl # QUESTION perhaps include a SyntaxDsl? def named value if self.is_a? Processor @name = value else option :name, value end end alias :match_name :named alias :bind_to :named def content_model value option :content_model, value end alias :parse_content_as :content_model def positional_attributes *value option :pos_attrs, value.flatten end alias :pos_attrs :positional_attributes alias :name_attributes :positional_attributes alias :name_positional_attributes :positional_attributes def default_attrs value option :default_attrs, value end alias :seed_attributes_with :default_attrs end # Public: BlockMacroProcessors are used to handle block macros that have a # custom name. # # BlockMacroProcessor implementations must extend BlockMacroProcessor. class BlockMacroProcessor < MacroProcessor end BlockMacroProcessor::DSL = MacroProcessorDsl # Public: InlineMacroProcessors are used to handle block macros that have a # custom name. # # InlineMacroProcessor implementations must extend InlineMacroProcessor. #-- # TODO break this out into different pattern types # for example, FormalInlineMacro, ShortInlineMacro (no target) and other patterns # FIXME for inline passthrough, we need to have some way to specify the text as a passthrough class InlineMacroProcessor < MacroProcessor def initialize name, config = {} super @config[:regexp] ||= (resolve_regexp @name, @config[:format]) end def resolve_regexp name, format # TODO memoize these regular expressions! if format == :short %r(\\?#{name}:\[((?:\\\]|[^\]])*?)\]) else %r(\\?#{name}:(\S+?)\[((?:\\\]|[^\]])*?)\]) end end end module InlineMacroProcessorDsl include MacroProcessorDsl def using_format value option :format, value end 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 :kind attr :config attr :instance def initialize kind, instance, config @kind = kind @instance = instance @config = config end end # Public: A specialization of the Extension proxy that additionally stores a # reference to the {Processor#process} method. By storing this reference, its # possible to accomodate both concrete extension implementations and Procs. class ProcessorExtension < Extension attr :process_method def initialize kind, instance, process_method = nil super kind, instance, instance.config @process_method = process_method || (instance.method :process) end end # Public: A Group is used to register one or more extensions with the Registry. # # The Group should be subclassed and registered with the Registry either by # invoking the {Group.register} method or passing the subclass to the # {Extensions.register} method. Extensions are registered with the Registry # inside the {Group#activate} method. class Group class << self def register name = nil Extensions.register name, self end end def 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 = @treeprocessor_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 |reader, lines| # ... # end # end # # Returns the [Extension] stored in the registry that proxies the # instance of this Preprocessor. def preprocessor *args, &block add_document_processor :preprocessor, args, &block end # 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 # treeprocessor ShellTreeprocessor # # # as an instance of a Treeprocessor subclass # treeprocessor ShellTreeprocessor.new # # # as a name of a Treeprocessor subclass # treeprocessor 'ShellTreeprocessor' # # # as a method block # treeprocessor do # process |document| # ... # end # end # # Returns the [Extension] stored in the registry that proxies the # instance of this Treeprocessor. def treeprocessor *args, &block add_document_processor :treeprocessor, args, &block end # Public: Checks whether any {Treeprocessor} extensions have been registered. # # Returns a [Boolean] indicating whether any Treeprocessor extensions are registered. def treeprocessors? !!@treeprocessor_extensions end # Public: Retrieves the {Extension} proxy objects for all # Treeprocessor instances in this registry. # # Returns an [Array] of Extension proxy objects. def treeprocessors @treeprocessor_extensions end # 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 (:header 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.find {|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 (:header 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 # register :shout do # process |parent, reader, attrs| # ... # end # end # # Returns an instance of the [Extension] proxy object that is stored in the # registry and manages the instance of this BlockProcessor. def block *args, &block add_syntax_processor :block, args, &block end # 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 GistBlockMacro # # # as a BlockMacroProcessor subclass with an explicit macro name # block GistBlockMacro, :gist # # # as an instance of a BlockMacroProcessor subclass # block GistBlockMacro.new # # # as an instance of a BlockMacroProcessor subclass with an explicit macro name # block GistBlockMacro.new, :gist # # # as a name of a BlockMacroProcessor subclass # block 'GistBlockMacro' # # # as a name of a BlockMacroProcessor subclass with an explicit macro name # block 'GistBlockMacro', :gist # # # as a method block # block_macro do # named :gist # process |parent, target, attrs| # ... # end # end # # # as a method block with an explicit macro name # register :gist do # process |parent, target, attrs| # ... # end # end # # Returns an instance of the [Extension] proxy object that is stored in the # registry and manages the instance of this BlockMacroProcessor. def block_macro *args, &block add_syntax_processor :block_macro, args, &block end # 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 # block ChromeInlineMacro # # # as an InlineMacroProcessor subclass with an explicit macro name # block ChromeInineMacro, :chrome # # # as an instance of an InlineMacroProcessor subclass # block ChromeInlineMacro.new # # # as an instance of an InlineMacroProcessor subclass with an explicit macro name # block ChromeInlineMacro.new, :chrome # # # as a name of an InlineMacroProcessor subclass # block 'ChromeInlineMacro' # # # as a name of an InlineMacroProcessor subclass with an explicit macro name # block 'ChromeInineMacro', :chrome # # # as a method block # inline_macro do # named :chrome # process |parent, target, attrs| # ... # end # end # # # as a method block with an explicit macro name # register :chrome do # process |parent, target, attrs| # ... # end # end # # Returns an instance of the [Extension] proxy object that is stored in the # registry and manages the instance of this InlineMacroProcessor. def inline_macro *args, &block add_syntax_processor :inline_macro, args, &block end # 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 {|word| %(#{word.chr.upcase}#{word[1..-1]}) }.join.to_sym kind_class = Extensions.const_get kind_class_symbol kind_java_class = (defined? ::AsciidoctorJ) ? (::AsciidoctorJ::Extensions.const_get kind_class_symbol) : nil kind_store = instance_variable_get(%(@#{kind}_extensions).to_sym) || instance_variable_set(%(@#{kind}_extensions).to_sym, []) # style 1: specified as block extension = if block_given? config = resolve_args args, 1 # TODO if block arity is 0, assume block is process method processor = kind_class.new config # 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.new %(No block specified to process #{kind_name} extension at #{block.source_location}) end ProcessorExtension.new kind, processor else processor, config = resolve_args args, 2 # style 2: specified as class or class name if (processor.is_a? ::Class) || ((processor.is_a? ::String) && (processor = Extensions.class_for_name processor)) unless processor < kind_class || (kind_java_class && processor < kind_java_class) raise ::ArgumentError.new %(Invalid type for #{kind_name} extension: #{processor}) end processor_instance = processor.new config processor_instance.freeze ProcessorExtension.new kind, processor_instance # style 3: specified as instance elsif (processor.is_a? kind_class) || (kind_java_class && (processor.is_a? kind_java_class)) processor.update_config config processor.freeze ProcessorExtension.new kind, processor else raise ::ArgumentError.new %(Invalid arguments specified for registering #{kind_name} extension: #{args}) end end 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_basename = kind_name.split(' ').map {|word| %(#{word.chr.upcase}#{word[1..-1]}) }.join kind_class_symbol = %(#{kind_class_basename}Processor).to_sym kind_class = Extensions.const_get kind_class_symbol kind_java_class = (defined? ::AsciidoctorJ) ? (::AsciidoctorJ::Extensions.const_get kind_class_symbol) : nil kind_store = instance_variable_get(%(@#{kind}_extensions).to_sym) || instance_variable_set(%(@#{kind}_extensions).to_sym, {}) # style 1: specified as block if block_given? name, config = resolve_args args, 2 processor = kind_class.new as_symbol(name), config # 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.new %(No name specified for #{kind_name} extension at #{block.source_location}) end unless processor.process_block_given? raise ::NoMethodError.new %(No block specified to process #{kind_name} extension at #{block.source_location}) end processor.freeze kind_store[name] = ProcessorExtension.new kind, processor else processor, name, config = resolve_args args, 3 # style 2: specified as class or class name if (processor.is_a? ::Class) || ((processor.is_a? ::String) && (processor = Extensions.class_for_name processor)) unless processor < kind_class || (kind_java_class && processor < kind_java_class) raise ::ArgumentError.new %(Class specified for #{kind_name} extension does not inherit from #{kind_class}: #{processor}) end processor_instance = processor.new as_symbol(name), config unless (name = as_symbol processor_instance.name) raise ::ArgumentError.new %(No name specified for #{kind_name} extension: #{processor}) end processor.freeze kind_store[name] = ProcessorExtension.new kind, processor_instance # style 3: specified as instance elsif (processor.is_a? kind_class) || (kind_java_class && (processor.is_a? kind_java_class)) 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.new %(No name specified for #{kind_name} extension: #{processor}) end processor.freeze kind_store[name] = ProcessorExtension.new kind, processor else raise ::ArgumentError.new %(Invalid arguments specified for registering #{kind_name} extension: #{args}) end end end def resolve_args args, expect opts = (args[-1].is_a? ::Hash) ? args.pop : {} return opts if expect == 1 num_args = args.size if (missing = expect - 1 - num_args) > 0 args.fill nil, num_args, missing elsif missing < 0 args.pop(-missing) end args << opts args end def as_symbol name name ? ((name.is_a? ::Symbol) ? 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 build_registry name = nil, &block if block_given? name ||= generate_name Registry.new({ 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.length resolved_group = if block_given? block elsif !(group = args.pop) raise ::ArgumentError.new %(Extension group to register not specified) else # QUESTION should we instantiate the group class here or defer until # activation?? case group when ::Class group when ::String class_for_name group when ::Symbol class_for_name group.to_s else group end end name = args.pop || generate_name unless args.empty? raise ::ArgumentError.new %(Wrong number of arguments (#{argc} for 1..2)) end groups[name] = resolved_group end def unregister_all @groups = {} end # unused atm, but tested def resolve_class object (object.is_a? ::Class) ? object : (class_for_name object.to_s) end # Public: Resolves the Class object for the qualified name. # # Returns Class def class_for_name qualified_name resolved_class = ::Object qualified_name.split('::').each do |name| if name.empty? # do nothing elsif resolved_class.const_defined? name resolved_class = resolved_class.const_get name else raise %(Could not resolve class for name: #{qualified_name}) end end resolved_class end end end end