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. Dan Allen | http://google.com/profiles/dan.j.allen |
Administrator
|
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). |
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. |
Administrator
|
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() { 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 |
Free forum by Nabble | Edit this page |