Extensions overhaul

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
4 messages Options
Reply | Threaded
Open this post in threaded view
|

Extensions overhaul

mojavelinux
Administrator
How does this look for rapid prototyping AsciiDoc extensions in Asciidoctor?

```ruby

require 'asciidoctor'
require 'asciidoctor/extensions'

Asciidoctor::Extensions.register {
  block {
    handles_name :shout
    on_contexts :paragraph
    parse_as :simple
    process { |parent, reader, attrs|
      create_paragraph parent, (reader.lines.map &:upcase), attrs
    }
  }
}

Asciidoctor.render_file 'sample.adoc'

```

for handling the following AsciiDoc content:

.sample.adoc
```
[shout]
Just do it.
```

The big improvement, which will allow for extensions to be written easier in other JVM languages, is that Asciidoctor now accept class instances (issue #804). This should open up the door to getting this same closure-style declaration for Groovy (and perhaps Java 8).

Using a Ruby example, here's how that looks:

```ruby

class ShoutBlock < Extensions::BlockProcessor
  extend DSL

  handles_name :shout
  on_contexts :paragraph
  parse_as :simple

  def process parent, reader, attrs
    create_paragraph parent, (reader.lines.map &:upcase), attrs
  end
end

Asciidoctor::Extensions.register {
  block ShoutBlock.new
}

Asciidoctor.render_file 'sample.adoc'

```

Here's how I envision this might look in a Gradle build:

.build.gradle
```groovy

asciidoctor {
  // ...
  extensions = {
    block(':shout', [contexts: [':paragraph'], content_model: ':simple']) { parent, reader, attrs ->
      createBlock(parent.document, 'paragraph', reader.lines()*.toUpperCase().join(''), attrs)
    }
  }
}

```

I'm working on allowing the extension registry to be passed as an option to Asciidoctor so global registration and the activation callback aren't the only two options.

== Overview

In all, the extension API recognizes 4 cases for registering an extension processor:

. processor class reference
. processor class name (as String)
. processor instance
. processor block (Ruby proc or lambda)

To support the Groovy closure / Java 8 lamba style on the Java side (even from a Gradle build!), it's likely that we will leverage the processor instance option. The Java API will handle wrapping the closure in an adapter and delegating to it when invoked from Ruby.

NOTE: If we can sort out how to map a Groovy closure / Java 8 lamda directly to a Ruby block, that's just icing on the cake.

I've also taken out any reference Ruby makes to static methods. Instead, I keep all that stuff on the Ruby side and instead accept a config map returned from the instance that you can setup in the constructor. That way, the Java API can read annotations and build a config map from them. In that case, I'm imagining an extension that looks something like:

```groovy

@Name(':shout')
@Context(':paragraph')
@ContentModel(SIMPLE)
class ShoutBlock extends BlockProcessor {
  def process(AbstractBlock parent, Reader reader, Map<String, Object> attrs) {
    createBlock(parent.document, 'paragraph', reader.lines()*.toUpperCase().join(''), attrs)
  }
}

```

Note that there are some changes the the Processor APIs and with how things are registered. In particular, the extension processors no longer store a reference to the Document. Therefore, some of the process methods now accept the Document as the first argument.

Here's the branch where this is implemented.


I'll publish 1.5.0.preview.2 in a day or so to give you a chance to experiment with it without having to checkout the code locally.

Btw, you can try the extensions from the Asciidoctor CLI. I've added a -r option for loading additional Ruby scripts or gems before the processor is called (issue #574). Just add the extension code in a Ruby file (named extensions.rb, for example), then invoke the asciidoctor command as follows:

 asciidoctor -r extensions.rb sample.adoc

The value of the -r option is passed to the require keyword in Ruby.

== Design

I've also done some rethinking about the terminology and how things are wired together. Here's what I've come up with.

Extension group:: An object or block that registers one or more extensions. The code inside the "block" method in the first example is an extension group.

Extension:: A proxy object for an extension implementation (such as a Processor subclass). Encapsulates the kind (e.g., block macro), config Hash and extension instance. It allows the preparation of the extension instance to be separated from its usage.

Extension registry:: Primary entry point into the extension system. Responsible for accepting the registration of extension groups, activating the extensions defined in a group when the document is first loaded and looking up extensions during parsing.

Here's the order of how this plays out.

An extension group is registered with the extension system using the Extensions::register method. For instance,

.As a block
```ruby

Extensions.register {
  block ShoutBlock
}

```

.As a class
```

Extension.register MyExtensionGroup

// where MyExtensionGroup is defined as:

class MyExtensionGroup < Extensions::sGroup
  def activate registry
    registry.block ShoutBlock
  end
end
```

.As an instance
```

Extension.register MyExtensionGroup.new

```

When Asciidoctor::load is called, it builds an Extension::Registry and then calls its ::activate method, passing a reference to the current Document, in order to activate the various extension groups. This is when the "block ShoutBlock" in the example is executed.

```ruby

reg = Extensions::Registry.new
reg.activate self

```

Asciidoctor will create an instance of each registered extension processor at this time.

As I mentioned above, I'm working on getting Asciidoctor to accept a predefined instance of Extensions::Registry. If one is passed in (perhaps via the :extensions_registry or :extensions options), then it will be used in place of creating a new one.

As parsing ensues, Asciidoctor uses the registry to find and invoke the extension instances. For example, here's the code for loading and invoking the preprocessors.

```ruby

if @extensions && @extensions.preprocessors?
  @extensions.preprocessors.each do |process_method|
    @reader = process_method[@reader, @reader.lines] || @reader
  end
end

```

That's all I have for now. Feel free to chime in here or on issue #804 if you have questions or comments about the changes coming.

--
Reply | Threaded
Open this post in threaded view
|

Re: Extensions overhaul

mojavelinux
Administrator
mojavelinux wrote
Extension group:: An object or block that registers one or more extensions.
The code inside the "block" method in the first example is an extension
group.
Correction. That should have read, "The code inside the "register" method in the first example is an extension group". It's an extension group defined using a block (i.e., Ruby Proc).
Reply | Threaded
Open this post in threaded view
|

Re: Extensions overhaul

asotobu
Cool! When you release the preview2, then I will start the integration. Only one question, if I don't modify nothing extensions test will fail, but following https://github.com/mojavelinux/asciidoctor/blame/issue-804/test/extensions_test.rb would be enough to integrate right?

One last question for BlockProcessor which was required in Java to do that strange thing of static method, now it is not even required, is it?

Thank you so muchI will try to integrate as fast as possible, but maybe it will take me until weekend. Hope the scheduled is fits for you too.

Alex.
Reply | Threaded
Open this post in threaded view
|

Re: Extensions overhaul

mojavelinux
Administrator


On Feb 3, 2014 12:54 PM, "asotobu [via Asciidoctor :: Discussion]" <[hidden email]> wrote:
>
> Cool! When you release the preview2, then I will start the integration. Only one question, if I don't modify nothing extensions test will fail, but following https://github.com/mojavelinux/asciidoctor/blame/issue-804/test/extensions_test.rb would be enough to integrate right?

Yep, that should be sufficient. I also plan to add some Cucumber tests that may be easier to follow.

> One last question for BlockProcessor which was required in Java to do that strange thing of static method, now it is not even required, is it?

Yep, that coupling is now removed. The use of static config is only something the Ruby implementations use as a way of emulating annotations in Java. The config is now just a map that is setup in the Processor. The processor should expose this map via an instance method.

public Map<String, Object> getConfig() {
  return this.config;
}

The constructor will also receive a config map that serves as overrides specified at the point of registration. No static config you have to worry about.

> Thank you so muchI will try to integrate as fast as possible, but maybe it will take me until weekend. Hope the scheduled is fits for you too.

Indeed! Sounds great!

-Dan