ROQ - Creating a custom section helper
But why?
Why a car? Because it’s a custom one.
Now that we have answered this crucial question, let’s push it aside and move on.
No I meant why a custom section helper?
Ohhhhh. Ok. Sometimes you need to execute code to generate content, not just compose templating bits.
To do this Qute lets you define custom section helper that allows java code execution, to transform content. Here are some existing section helpers :
- Mardown/Asciidoc
-
They will generate HTML content from doc-as-code content
- QRCode
-
That will generate an image based on a given URL
What will we do?
In this blog post, we will implement a custom section helper that allows to invoke a Kroki.io server to generate images from almost every possible diagram as code language.
What I’d like to have is a simple and common way for the user to let us know that he is writing a diagram and would like us to generate the rendering for him.
Something like :
----
title: A blog post with a diagram
tags: qute,diagram
----
Here's my blog post:
++++ (1)
{#diagram language="dbml" outputFormat="svg" alt="Blog tables diagram" width=400 height=500}
@startuml
!theme hacker
left to right direction
object users {
id : integer
username : varchar
role : varchar
created_at : timestamp
}
object posts {
id : integer [primary key]
title : varchar
body : text
user_id : integer
status : post_status
created_at : timestamp
}
note left: **body** => Content of the post
enum post_status {
draft
published
private
}
note right of post_status: **private** => visible via URL only
posts::user_id --> users::id
posts::post_status --> post_status
@enduml
{/}
++++
1 | Just to tell the asciidoc engine it’s a passthrough |
And I would like to be automatically converted to :
Let’s implement this
If you try the above code right now, Roq will bark that no section helper is available to process this section.
Let’s fix this. We will just create a new SectionHelper
and its corresponding factory
, usually declared in a single source file
import io.quarkus.qute.EngineConfiguration;
import io.quarkus.qute.ResultNode;
import io.quarkus.qute.SectionHelper;
import io.quarkus.qute.SectionHelperFactory;
import java.util.List;
import java.util.concurrent.CompletionStage;
@EngineConfiguration (1)
public class DiagramsSectionHelperFactory implements SectionHelperFactory<DiagramsSectionHelperFactory.DiagramSectionHelper> {
@Override
public List<String> getDefaultAliases() {
return List.of("diagrams"); (2)
}
@Override
public DiagramSectionHelper initialize(SectionInitContext context) { (3)
new DiagramSectionHelper(context);
}
public class DiagramSectionHelper implements SectionHelper {
@Override
public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
return context.execute(); (4)
}
}
}
1 | This annotation tells the Qute engine that this instance is here for him. Namely that he must register a new section helper. |
2 | Here is our new section alias |
3 | Where we can initialize an helper for this specific resolution context |
4 | Returns exactly what was given in. |
For the moment, that’s not much better, the content of the section is rendered untouched, but at least, it doesn’t bark at us anymore.
I will spare you the Kroki.io client implementation details, but let’s just say we have the following interface:
public interface DiagramConverter {
String encode(String diagramSource, DiagramParams params); (1)
record DiagramParams(String diagramLanguage, String alt, Integer width, Integer height, DiagramConverter.DiagramOutputFormat diagramOutputFormat) {
}
}
1 | Generates the needed HTML code to display the diagram using the data uri scheme |
Let’s dive a bit more
They are a few more methods available in the DiagramsSectionHelperFactory
. Let’s take a closer look at the initialize
one.
For the sake of brevity, the code pretends there is only the single language parameter to deal with :
public class DiagramsSectionHelperFactory implements SectionHelperFactory<DiagramsSectionHelperFactory.DiagramSectionHelper> {
private DiagramConverter converter;
//more code here
@Override
public DiagramSectionHelper initialize(SectionInitContext context) {
String diagramLanguage = context.getParameter("language"); (1)
new DiagramSectionHelper(language, converter);
}
//and more here
}
1 | Gets the language param value. Be aware that it will always be a String , so you may have to do conversions if needed |
Should you need to use expressions instead of literals as value, you would need a more convoluted path. That would not bite you though. Let me know if you are interested. |
That’s it we are done!
Want to dig a bit more ?
You can first check I am Roq which is already full of insights or even take a look at the QRCode plugin in the github repository