Skip to content
This repository has been archived by the owner on Apr 29, 2020. It is now read-only.

Can't selectively apply a template based on a role name #13

Open
s-leroux opened this issue Mar 22, 2018 · 10 comments
Open

Can't selectively apply a template based on a role name #13

s-leroux opened this issue Mar 22, 2018 · 10 comments

Comments

@s-leroux
Copy link

I have an interesting use case I cannot solve using the template system (neither asciidoctor-template.js nor my own asciidoctor.js-pug implementation):

I am using Asciidoctor right now to write a blog post. It contains several blocks of code. Some of them need to be processed differently as I want to replace them by RunKit notebooks. My idea was to add a custom role on the corresponding blocks:

[.runkit]
----
console.log("hello");
----

The problem is I can't see how I could write a custom template to process blocks of code with the runkit role while still using the default HTML5 convert() method for the other blocks of code.

I hope I'm clear enough. Any idea?

@ggrossetie
Copy link
Member

It contains several blocks of code. Some of them need to be processed differently as I want to replace them by RunKit notebooks. My idea was to add a custom role on the corresponding blocks:

I think this use case should be solved by a custom extension. You should check what @oncletom has done with RunKit: https://github.com/oncletom/asciidoctor-extension-interactive-runner

@s-leroux
Copy link
Author

s-leroux commented Mar 24, 2018

Thank you Guillaume for your reply,

I will take a look at that this WE.
A question though: is there a particular reason you used a CompositeConverter in your implementation? I think I will refactor that part in asciidoctor.js-pug so I will be able to pass control to the base_converter if the template is unable to handle a node by itself.

Something like the next() call in Express.js middleware. What do you think of that?

@ggrossetie
Copy link
Member

A question though: is there a particular reason you used a CompositeConverter in your implementation? I think I will refactor that part in asciidoctor.js-pug so I will be able to pass control to the base_converter if the template is unable to handle a node by itself.
Something like the next() call in Express.js middleware. What do you think of that?

I might be wrong but I think it's exactly the purpose of the CompositeConverter.

A {Converter} implementation that delegates to the chain of {Converter} objects passed to the constructor. Selects the first {Converter} that identifies itself as the handler for a given transform.

Basically you configure a list of converters and the composite converter will delegate to the first converter that identifies itself as the handler.
So for instance you can use a composite converter with your template converter and a fallback converter (most likely the HTML5 converter). Doing so you will be able to override just one node and fallback to the HTML5 converter for all other nodes.
Is this what you had in mind ?

@danShumway
Copy link

Some of them need to be processed differently as I want to replace them by RunKit notebooks. My idea was to add a custom role on the corresponding blocks:

:) I was also running into a very similar problem just a day or two ago - you can have custom templates for everything, but if you want to have a custom block type you have to write it as an extension. I was trying to think if there was some way I could jerry-rig something to auto-register an extension whenever templates were passed in that weren't already part of the AsciiDoctor core.

So if you did:

[custom_block]
----
Some content here
----

You could just include a template called custom_block.html.hbs and that would get treated like a normal template.

<div id="my-element"></div>
<script>var notebook = RunKit.createNotebook({
    // the parent element for the new notebook
    element: document.getElementById("my-element"),
    // specify the source of the notebook
    source: "{{node.content}}"
})</script>

I didn't get the time to actually prototype anything or do any additional research though, so there are likely concerns I didn't think about.

@s-leroux
Copy link
Author

s-leroux commented Mar 24, 2018

@Mogztter

Doing so you will be able to override just one node and fallback to the HTML5 converter for all other nodes. Is this what you had in mind ?

I need finer granularity than that. As I understand it, the CompositeConverter will delegate to a given "child" converter depending on the node name. As a matter of fact, I even think the handler is cached based on that name of the node here (as far as I understand the Ruby syntax :/ )

So I can't see how/if it could be used to, say, override the default handler only if a given role or attribute is set for a node.

@danShumway
That was mostly what I had in mind. But since you can attach roles to blocks, I was thinking of using that feature instead of creating a custom node. That way, in my case, I can still process my documents through non-RunKit aware backends (like the DocBook converter)--and code blocks will still be rendered properly.

@danShumway
Copy link

danShumway commented Mar 24, 2018

That way, in my case, I can still process my documents through non-RunKit aware backends (like the DocBook converter)--and code clocks will still be rendered properly.

That's a really good point - you should be able to (for the most part) take the same content and render anywhere.

@ggrossetie
Copy link
Member

ggrossetie commented Mar 25, 2018

I need finer granularity than that. As I understand it, the CompositeConverter will delegate to a given "child" converter depending on the node name. As a matter of fact, I even think the handler is cached based on that name of the node here (as far as I understand the Ruby syntax :/ )

Indeed CompositeConverter may not be the right tool for your use case.

So I can't see how/if it could be used to, say, override the default handler only if a given role or attribute is set for a node.

You could extend the default html5 converter and do something like:

Pseudo-code

def listing node
  if node.role? 'runkit'
    # ... do something special
  else
    super node # fallback to the default implementation
  end
end

But I still think the best approach is to use an extension. Do you have any constraints for not using an extension ?

If you want to use a template then you could extend the default TemplateConverter and override the convert method:

Pseudo-code

def convert node, template_name = nil, opts = {}
  if node.node_name == 'listing' and node.role? 'runkit'
    template_name = 'listing_runkit'
  end
  super node, template_name, opts
end

Just throwing some ideas 😉

NOTE: A generic solution could be to create a method to resolve the template name from a node (in Asciidoctor core):

def resolve_template_name node
  node.node_name # default implementation
end

@mojavelinux What do you think about this idea ?

@s-leroux
Copy link
Author

s-leroux commented Mar 25, 2018

Thanks for your suggestions Guillaume.
Indeed you have some interesting ideas. On my side, I worked on that today and opted for a solution allowing to conditionally pass control to the next template in the template chain.

Basically, what I have now is (pseudo JS):

// Pre-load the templates for efficiency
const pug_templates = adt.load_templates('./path/to/template/directory');

files.forEach((file) => {
  const doc = asciidoctor.loadFile(file, {
  templates: [{
    pug_templates, // my default "stylesheet"
    {
      listing: (ctx) => {
        if (ctx.node.roles.has("RunKit") {
           do something specific
        }
        else
          return ctx.next(); // pass control to the next template in the chain
                             // think of that like a "super" call in OOP
      }
    }
  }],
});

I think this gives me a lot of freedom, while working fully at JS level without writing a line of Ruby/Opal code. What do you think of that solution?

@s-leroux
Copy link
Author

s-leroux commented Mar 25, 2018

FWIW, with the latest version of asciidoctor.js-pug, I was able to convert listings to RunKit notebooks just by adding that 5-lines Pug template to my project:

- if (!node.roles.has("runkit")) return next();

// RunKit listing
script(src="https://embed.runkit.com" data-element-id="sample-code")
div#sample-code!= next()

And in the source document, I just had to add the "runkit" role to the code block:

[.runkit]
----
const tags = [
  { name: "Linux", score: .6 },
  { name: "Open Source", score: .7 },
  ...
----

And here is the result:

image

Cool, isn't it?

@mojavelinux
Copy link
Member

I worked on that today and opted for a solution allowing to conditionally pass control to the next template in the template chain.

I love that idea. It could even be something we introduce into the Ruby version.

The idea of the handles? / converter_for is to find the top converter for a transform (aka node name) in the chain. While overriding that logic is possible, it incurs a large cost for 99% of the nodes. Your next() idea is going to be much more efficient as it only has to be called for the exception cases.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants