summaryrefslogtreecommitdiff
path: root/lib/asciidoctor/interdoc_reftext/processor.rb
diff options
context:
space:
mode:
authorJakub Jirutka <jakub@jirutka.cz>2018-01-30 18:40:49 +0100
committerJakub Jirutka <jakub@jirutka.cz>2018-01-30 19:38:24 +0100
commit9335f9fc0950004f477e86d6332b6e26dbf05025 (patch)
treed77f59a323ad778ba8951127dc84d5f960adbf62 /lib/asciidoctor/interdoc_reftext/processor.rb
parenta8e82ba99adf1a2a34b136388b088dcfe60d9593 (diff)
Implement processor and resolver
Diffstat (limited to 'lib/asciidoctor/interdoc_reftext/processor.rb')
-rw-r--r--lib/asciidoctor/interdoc_reftext/processor.rb78
1 files changed, 78 insertions, 0 deletions
diff --git a/lib/asciidoctor/interdoc_reftext/processor.rb b/lib/asciidoctor/interdoc_reftext/processor.rb
new file mode 100644
index 0000000..55ee1ae
--- /dev/null
+++ b/lib/asciidoctor/interdoc_reftext/processor.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+require 'asciidoctor/extensions'
+require 'asciidoctor/interdoc_reftext/inline_node_mixin'
+require 'asciidoctor/interdoc_reftext/resolver'
+require 'asciidoctor/interdoc_reftext/version'
+
+module Asciidoctor::InterdocReftext
+ # Asciidoctor processor that adds support for automatic cross-reference text
+ # for inter-document cross references.
+ #
+ # ### Implementation Considerations
+ #
+ # Asciidoctor does not allow to _cleanly_ change the way of resolving
+ # xreftext for `xref:path#[]` macro with path and without explicit xreflabel;
+ # it always uses path as the default xreflabel.
+ #
+ # 1. `xref:[]` macros are parsed and even converted in
+ # `Asciidoctor::Substitutors#sub_inline_xrefs` - a single, huge and nasty
+ # method that accepts a text (e.g. whole paragraph) and returns the text
+ # with converted `xref:[]` macros. The conversion is delegated to
+ # `Asciidoctor::Inline#convert` - for each macro a new instance of
+ # `Inline` node is created and then `#convert` is called.
+ #
+ # 2. `Inline#convert` just calls `converter.convert` with `self`, i.e. it's
+ # dispatched to converter's `inline_anchor` handler.
+ #
+ # 3. The built-in so called HTML5 converter looks into the catalog of
+ # references (`document.catalog[:refs]`) for reflabel for the xref's
+ # *refid*, but only if xref node does not define attribute *path* or
+ # *text* (explicit reflabel). If *text* is not set and *path* is set, i.e.
+ # it's an inter-document reference without explicit reflabel, catalog of
+ # references is bypassed and *path* is used as a reflabel.
+ #
+ # Eh, this is really nasty... The least evil way how to achieve the goal
+ # seems to be monkey-patching of the `Asciidoctor::Inline` class. This is
+ # done via {InlineNodeMixin} which is prepended into the `Inline` class on
+ # initialization of this processor.
+ #
+ # The actual logic that resolves reflabel for the given *refid* is
+ # implemented in class {Resolver}. The {Processor} is responsible for
+ # creating an instance of {Resolver} for the processed document and injecting
+ # it into instance variable {RESOLVER_VAR_NAME} in the document, so
+ # {InlineNodeMixin} can access it.
+ #
+ # Prepending {InlineNodeMixin} into the `Asciidoctor::Inline` class has
+ # (obviously) a global effect. However, if {RESOLVER_VAR_NAME} is not
+ # injected in the document object (e.g. extension is not active), `Inline`
+ # behaves the same as without {InlineNodeMixin}.
+ #
+ # NOTE: We use _reftext_ and _reflabel_ as interchangeable terms in this gem.
+ class Processor < ::Asciidoctor::Extensions::TreeProcessor
+
+ # Name of instance variable that is dynamically defined in a document
+ # object; it contains an instance of the Resolver for the document.
+ RESOLVER_VAR_NAME = :@_interdoc_reftext_resolver
+
+ # @param resolver_class [#new] the {Resolver} class to use.
+ # @param resolver_opts [Hash<Symbol, Object>] options to be passed into
+ # the resolver_class's initializer (see {Resolver#initialize}).
+ def initialize(resolver_class: Resolver, **resolver_opts)
+ super
+ @resolver_class = resolver_class
+ @resolver_opts = resolver_opts
+
+ # Monkey-patch Asciidoctor::Inline unless already patched.
+ unless ::Asciidoctor::Inline.include? InlineNodeMixin
+ ::Asciidoctor::Inline.send(:prepend, InlineNodeMixin)
+ end
+ end
+
+ # @param document [Asciidoctor::Document] the document to process.
+ def process(document)
+ resolver = @resolver_class.new(document, @resolver_opts)
+ document.instance_variable_set(RESOLVER_VAR_NAME, resolver)
+ nil
+ end
+ end
+end