"Fossies" - the Fresh Open Source Software Archive

Member "asciidoctor-2.0.10/lib/asciidoctor/converter.rb" (1 Jun 2019, 16089 Bytes) of package /linux/www/asciidoctor-2.0.10.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Ruby source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "converter.rb": 2.0.7_vs_2.0.8.

    1 # frozen_string_literal: true
    2 module Asciidoctor
    3 # A module for defining converters that are used to convert {AbstractNode} objects in a parsed AsciiDoc document to an
    4 # output (aka backend) format such as HTML or DocBook.
    5 #
    6 # A {Converter} is typically instantiated each time an AsciiDoc document is processed (i.e., parsed and converted).
    7 # Implementing a custom converter entails:
    8 #
    9 # * Including the {Converter} module in a converter class and implementing the {Converter#convert} method or extending
   10 #   the {Converter::Base Base} class and implementing the dispatch methods that map to each node.
   11 # * Optionally registering the converter with one or more backend names statically using the +register_for+ DSL method
   12 #   contributed by the {Converter::Config Config} module.
   13 #
   14 # Examples
   15 #
   16 #   class TextConverter
   17 #     include Asciidoctor::Converter
   18 #     register_for 'text'
   19 #     def initialize *args
   20 #       super
   21 #       outfilesuffix '.txt'
   22 #     end
   23 #     def convert node, transform = node.node_name, opts = nil
   24 #       case transform
   25 #       when 'document', 'section'
   26 #         [node.title, node.content].join %(\n\n)
   27 #       when 'paragraph'
   28 #         (node.content.tr ?\n, ' ') << ?\n
   29 #       else
   30 #         (transform.start_with? 'inline_') ? node.text : node.content
   31 #       end
   32 #     end
   33 #   end
   34 #   puts Asciidoctor.convert_file 'sample.adoc', backend: :text, safe: :safe
   35 #
   36 #   class Html5Converter < (Asciidoctor::Converter.for 'html5')
   37 #     register_for 'html5'
   38 #     def convert_paragraph node
   39 #       %(<p>#{node.content}</p>)
   40 #     end
   41 #   end
   42 #   puts Asciidoctor.convert_file 'sample.adoc', safe: :safe
   43 module Converter
   44   autoload :CompositeConverter, %(#{__dir__}/converter/composite)
   45   autoload :TemplateConverter, %(#{__dir__}/converter/template)
   46 
   47   # Public: The String backend name that this converter is handling.
   48   attr_reader :backend
   49 
   50   # Public: Creates a new instance of this {Converter}.
   51   #
   52   # backend - The String backend name (aka format) to which this converter converts.
   53   # opts    - An options Hash (optional, default: {})
   54   #
   55   # Returns a new [Converter] instance.
   56   def initialize backend, opts = {}
   57     @backend = backend
   58   end
   59 
   60   # Public: Converts an {AbstractNode} using the given transform.
   61   #
   62   # This method must be implemented by a concrete converter class.
   63   #
   64   # node      - The concrete instance of AbstractNode to convert.
   65   # transform - An optional String transform that hints at which transformation should be applied to this node. If a
   66   #             transform is not given, the transform is often derived from the value of the {AbstractNode#node_name}
   67   #             property. (optional, default: nil)
   68   # opts      - An optional Hash of options hints about how to convert the node. (optional, default: nil)
   69   #
   70   # Returns the [String] result.
   71   def convert node, transform = nil, opts = nil
   72     raise ::NotImplementedError, %(#{self.class} (backend: #{@backend}) must implement the ##{__method__} method)
   73   end
   74 
   75   # Public: Reports whether the current converter is able to convert this node (by its transform name). Used by the
   76   # {CompositeConverter} to select which converter to use to handle a given node. Returns true by default.
   77   #
   78   # transform - the String name of the node transformation (typically the node name).
   79   #
   80   # Returns a [Boolean] indicating whether this converter can handle the specified transform.
   81   def handles? transform
   82     true
   83   end
   84 
   85   # Public: Derive backend traits (basebackend, filetype, outfilesuffix, htmlsyntax) from the given backend.
   86   #
   87   # backend - the String backend from which to derive the traits
   88   #
   89   # Returns the backend traits for the given backend as a [Hash].
   90   def self.derive_backend_traits backend
   91     return {} unless backend
   92     if (t_outfilesuffix = DEFAULT_EXTENSIONS[(t_basebackend = backend.sub TrailingDigitsRx, '')])
   93       t_filetype = t_outfilesuffix.slice 1, t_outfilesuffix.length
   94     else
   95       t_outfilesuffix = %(.#{t_filetype = t_basebackend})
   96     end
   97     t_filetype == 'html' ?
   98       { basebackend: t_basebackend, filetype: t_filetype, htmlsyntax: 'html', outfilesuffix: t_outfilesuffix } :
   99       { basebackend: t_basebackend, filetype: t_filetype, outfilesuffix: t_outfilesuffix }
  100   end
  101 
  102   module BackendTraits
  103     def basebackend value = nil
  104       value ? (backend_traits[:basebackend] = value) : backend_traits[:basebackend]
  105     end
  106 
  107     def filetype value = nil
  108       value ? (backend_traits[:filetype] = value) : backend_traits[:filetype]
  109     end
  110 
  111     def htmlsyntax value = nil
  112       value ? (backend_traits[:htmlsyntax] = value) : backend_traits[:htmlsyntax]
  113     end
  114 
  115     def outfilesuffix value = nil
  116       value ? (backend_traits[:outfilesuffix] = value) : backend_traits[:outfilesuffix]
  117     end
  118 
  119     def supports_templates value = true
  120       backend_traits[:supports_templates] = value
  121     end
  122 
  123     def supports_templates?
  124       backend_traits[:supports_templates]
  125     end
  126 
  127     def init_backend_traits value = nil
  128       @backend_traits = value || {}
  129     end
  130 
  131     def backend_traits
  132       @backend_traits ||= Converter.derive_backend_traits @backend
  133     end
  134 
  135     alias backend_info backend_traits
  136 
  137     # Deprecated: Use {Converter.derive_backend_traits} instead.
  138     def self.derive_backend_traits backend
  139       Converter.derive_backend_traits backend
  140     end
  141   end
  142 
  143   # A module that contributes the +register_for+ method for registering a converter with the default registry.
  144   module Config
  145     # Public: Registers this {Converter} class with the default registry to handle the specified backend name(s).
  146     #
  147     # backends - One or more String backend names with which to associate this {Converter} class.
  148     #
  149     # Returns nothing.
  150     def register_for *backends
  151       Converter.register self, *(backends.map {|backend| backend.to_s })
  152     end
  153   end
  154 
  155   # A reusable module for registering and instantiating {Converter Converter} classes used to convert an {AbstractNode}
  156   # to an output (aka backend) format such as HTML or DocBook.
  157   #
  158   # {Converter Converter} objects are instantiated by passing a String backend name and, optionally, an options Hash to
  159   # the {Factory#create} method. The backend can be thought of as an intent to convert a document to a specified format.
  160   #
  161   # Applications interact with the factory either through the global, static registry mixed into the {Converter
  162   # Converter} module or a concrete class that includes this module such as {CustomFactory}. For example:
  163   #
  164   # Examples
  165   #
  166   #   converter = Asciidoctor::Converter.create 'html5', htmlsyntax: 'xml'
  167   module Factory
  168     # Public: Create an instance of DefaultProxyFactory or CustomFactory, depending on whether the proxy_default keyword
  169     # arg is set (true by default), and optionally seed it with the specified converters map. If proxy_default is set,
  170     # entries in the proxy registry are preferred over matching entries from the default registry.
  171     #
  172     # converters    - An optional Hash of converters to use in place of ones in the default registry. The keys are
  173     #                 backend names and the values are converter classes or instances.
  174     # proxy_default - A Boolean keyword arg indicating whether to proxy the default registry (optional, default: true).
  175     #
  176     # Returns a Factory instance (DefaultFactoryProxy or CustomFactory) seeded with the optional converters map.
  177     def self.new converters = nil, proxy_default: true
  178       proxy_default ? (DefaultFactoryProxy.new converters) : (CustomFactory.new converters)
  179     end
  180 
  181     # Deprecated: Maps the old default factory instance holder to the Converter module.
  182     def self.default *args
  183       Converter
  184     end
  185 
  186     # Deprecated: Maps the create method on the old default factory instance holder to the Converter module.
  187     def self.create backend, opts = {}
  188       default.create backend, opts
  189     end
  190 
  191     # Public: Register a custom converter with this factory to handle conversion for the specified backends. If the
  192     # backend is an asterisk (i.e., +*+), the converter will handle any backend for which a converter is not registered.
  193     #
  194     # converter - The Converter class to register.
  195     # backends  - One or more String backend names that this converter should be registered to handle.
  196     #
  197     # Returns nothing
  198     def register converter, *backends
  199       backends.each {|backend| backend == '*' ? (registry.default = converter) : (registry[backend] = converter) }
  200     end
  201 
  202     # Public: Lookup the custom converter registered with this factory to handle the specified backend.
  203     #
  204     # backend - The String backend name.
  205     #
  206     # Returns the [Converter] class registered to convert the specified backend or nil if no match is found.
  207     def for backend
  208       registry[backend]
  209     end
  210 
  211     # Public: Create a new Converter object that can be used to convert {AbstractNode}s to the format associated with
  212     # the backend. This method accepts an optional Hash of options that are passed to the converter's constructor.
  213     #
  214     # If a custom Converter is found to convert the specified backend, it's instantiated (if necessary) and returned
  215     # immediately. If a custom Converter is not found, an attempt is made to find a built-in converter. If the
  216     # +:template_dirs+ key is found in the Hash passed as the second argument, a {CompositeConverter} is created that
  217     # delegates to a {TemplateConverter} and, if found, the built-in converter. If the +:template_dirs+ key is not
  218     # found, the built-in converter is returned or nil if no converter is found.
  219     #
  220     # backend - the String backend name.
  221     # opts    - a Hash of options to customize creation; also passed to the converter's constructor:
  222     #           :template_dirs - a String Array of directories used to instantiate a {TemplateConverter} (optional).
  223     #           :delegate_backend - a backend String of the last converter in the {CompositeConverter} chain (optional).
  224     #
  225     # Returns the [Converter] instance.
  226     def create backend, opts = {}
  227       if (converter = self.for backend)
  228         converter = converter.new backend, opts if ::Class === converter
  229         if (template_dirs = opts[:template_dirs]) && BackendTraits === converter && converter.supports_templates?
  230           CompositeConverter.new backend, (TemplateConverter.new backend, template_dirs, opts), converter, backend_traits_source: converter
  231         else
  232           converter
  233         end
  234       elsif (template_dirs = opts[:template_dirs])
  235         if (delegate_backend = opts[:delegate_backend]) && (converter = self.for delegate_backend)
  236           converter = converter.new delegate_backend, opts if ::Class === converter
  237           CompositeConverter.new backend, (TemplateConverter.new backend, template_dirs, opts), converter, backend_traits_source: converter
  238         else
  239           TemplateConverter.new backend, template_dirs, opts
  240         end
  241       end
  242     end
  243 
  244     # Public: Get the Hash of Converter classes keyed by backend name. Intended for testing only.
  245     def converters
  246       registry.merge
  247     end
  248 
  249     private
  250 
  251     def registry
  252       raise ::NotImplementedError, %(#{Factory} subclass #{self.class} must implement the ##{__method__} method)
  253     end
  254   end
  255 
  256   class CustomFactory
  257     include Factory
  258 
  259     def initialize seed_registry = nil
  260       if seed_registry
  261         seed_registry.default = seed_registry.delete '*'
  262         @registry = seed_registry
  263       else
  264         @registry = {}
  265       end
  266     end
  267 
  268     # Public: Unregister all Converter classes that are registered with this factory. Intended for testing only.
  269     #
  270     # Returns nothing.
  271     def unregister_all
  272       registry.clear.default = nil
  273     end
  274 
  275     private
  276 
  277     attr_reader :registry
  278   end
  279 
  280   # Mixed into the {Converter} module to provide the global registry of converters that are registered statically.
  281   #
  282   # This registry includes built-in converters for {Html5Converter HTML 5}, {DocBook5Converter DocBook 5} and
  283   # {ManPageConverter man(ual) page}, as well as any custom converters that have been discovered or explicitly
  284   # registered. Converter registration is synchronized (where applicable) and is thus guaranteed to be thread safe.
  285   module DefaultFactory
  286     include Factory
  287 
  288     private
  289 
  290     @@registry = {}
  291 
  292     def registry
  293       @@registry
  294     end
  295 
  296     unless RUBY_ENGINE == 'opal' # the following block adds support for synchronization and lazy registration
  297       public
  298 
  299       def register converter, *backends
  300         if @@mutex.owned?
  301           backends.each {|backend| backend == '*' ? (@@catch_all = converter) : (@@registry = @@registry.merge backend => converter) }
  302         else
  303           @@mutex.synchronize { register converter, *backends }
  304         end
  305       end
  306 
  307       def unregister_all
  308         @@mutex.synchronize do
  309           @@registry = @@registry.select {|backend| PROVIDED[backend] }
  310           @@catch_all = nil
  311         end
  312       end
  313 
  314       def for backend
  315         @@registry.fetch backend do
  316           PROVIDED[backend] ? (@@mutex.synchronize do
  317             # require is thread-safe, so no reason to refetch
  318             require PROVIDED[backend]
  319             @@registry[backend]
  320           end) : catch_all
  321         end
  322       end
  323 
  324       PROVIDED = {
  325         'docbook5' => %(#{__dir__}/converter/docbook5),
  326         'html5' => %(#{__dir__}/converter/html5),
  327         'manpage' => %(#{__dir__}/converter/manpage),
  328       }
  329 
  330       private
  331 
  332       def catch_all
  333         @@catch_all
  334       end
  335 
  336       @@catch_all = nil
  337       @@mutex = ::Mutex.new
  338     end
  339   end
  340 
  341   class DefaultFactoryProxy < CustomFactory
  342     include DefaultFactory # inserts module into ancestors immediately after superclass
  343 
  344     unless RUBY_ENGINE == 'opal'
  345       def unregister_all
  346         super
  347         @registry.clear.default = nil
  348       end
  349 
  350       def for backend
  351         @registry.fetch(backend) { super }
  352       end
  353 
  354       private
  355 
  356       def catch_all
  357         @registry.default || super
  358       end
  359     end
  360   end
  361 
  362   # Internal: Mixes the {Config} module into any class that includes the {Converter} module. Additionally, mixes the
  363   # {BackendTraits} method into instances of this class.
  364   #
  365   # into - The Class into which the {Converter} module is being included.
  366   #
  367   # Returns nothing.
  368   private_class_method def self.included into
  369     into.send :include, BackendTraits
  370     into.extend Config
  371   end || :included
  372 
  373   # An abstract base class for defining converters that can be used to convert {AbstractNode} objects in a parsed
  374   # AsciiDoc document to a backend format such as HTML or DocBook.
  375   class Base
  376     include Converter, Logging
  377 
  378     # Public: Converts an {AbstractNode} by delegating to a method that matches the transform value.
  379     #
  380     # This method looks for a method whose name matches the transform prefixed with "convert_" to dispatch to. If the
  381     # +opts+ argument is non-nil, this method assumes the dispatch method accepts two arguments, the node and an options
  382     # Hash. The options Hash may be used by converters to delegate back to the top-level converter. Currently, this
  383     # feature is used for the outline transform. If the +opts+ argument is nil, this method assumes the dispatch method
  384     # accepts the node as its only argument.
  385     #
  386     # See {Converter#convert} for details about the arguments and return value.
  387     def convert node, transform = node.node_name, opts = nil
  388       opts ? (send 'convert_' + transform, node, opts) : (send 'convert_' + transform, node)
  389     rescue
  390       raise unless ::NoMethodError === (ex = $!) && ex.receiver == self && ex.name.to_s == transform
  391       logger.warn %(missing convert handler for #{ex.name} node in #{@backend} backend (#{self.class}))
  392       nil
  393     end
  394 
  395     def handles? transform
  396       respond_to? %(convert_#{transform})
  397     end
  398 
  399     # Public: Converts the {AbstractNode} using only its converted content.
  400     #
  401     # Returns the converted [String] content.
  402     def content_only node
  403       node.content
  404     end
  405 
  406     # Public: Skips conversion of the {AbstractNode}.
  407     #
  408     # Returns nothing.
  409     def skip node; end
  410   end
  411 
  412   extend DefaultFactory # exports static methods
  413 end
  414 end