jetty / jetty.website

Antora-based jetty.org website.
https://jetty.org
Eclipse Public License 2.0
1 stars 3 forks source link

Implement the jetty block processor #16

Closed mojavelinux closed 4 months ago

mojavelinux commented 7 months ago

The Jetty documentation makes use of an AsciiDoc syntax extension that runs Jetty with a given set of parameters, filters its output, and renders those lines of output as a literal block. This extension needs to be ported to the Antora-based rendition of the docs.

Current state

The current implementation employs an include processor for AsciidoctorJ that handles include directives with the target jetty. The include processor intercepts the directive, runs Jetty, filters its output, and pushes the filtered output back onto the reader.

The jetty include directive must be placed inside a source block by the author so the output is displayed as preformatted text. Here's an example:

[source,options=nowrap,subs="+quotes"] 
---- 
include::jetty[setupArgs="--add-modules=http,deploy,{ee-current}-demo-simple",highlight="WebAppContext"]
----

However, I don't think this is an appropriate use of a custom include processor. There are a several reasons for this:

  1. A custom include processor should strictly be for resolving files and filtering the content of that file.
  2. The author has to wrap the include directive in another block in order for it to be displayed properly.
  3. The author has to manage the subs attribute for the output (a required coordination between subs and highlight)
  4. The include directive can only be a single line, so the argument list often wraps, making it hard to read.
  5. The jetty target is artificial, so it will always be displayed as a warning in the IDE, making this set up not IDE friendly

Proposed syntax

All of these drawbacks can be resolved by using a custom block instead. A custom block is for feeding a set of inputs into the processor and rendering the output. A good example is Asciidoctor Diagram, which converts text-based diagram inputs into images (and other representations).

Here's how the jetty include mechanism would look as a custom block, a jetty literal block:

[jetty%nowrap]
....
setupArgs=--add-modules=http,deploy,{ee-current}-demo-simple
highlight=WebAppContext
....

This block will be handled by the jetty block processor, which will live in the playbook repository.

Since there's only a single syntax element at play here (the jetty block), the author no longer has to worry about enclosing the custom syntax in another block. (There's no need for this verbosity). The jetty block gets converted into a literal block that contains the filtered output lines from running Jetty. Since that block is generated, the author no longer has to worry about managing the subs attribute as the subs can be added based on the needs of the input. (Even the attribute references get resolved automatically by the jetty block processor since that's implied behavior when attribute references are present). In other words, the custom block encapsulates all the presentation concerns that are better handled by the extension itself. Since a custom block may contain multiple lines, the arguments are now easier to read (and the value no longer has to be enclosed in double quotes). By default, a custom block is treated according to the rules of the structural container (in this case a literal block), so there will be no warnings about it in the IDE, making it IDE friendly.

To make it a little extra IDE friendly, an optional [jetty] directive line can be added above the parameter lines so it's easier to spot in the IDE preview window.

[jetty%nowrap]
....
[jetty]
setupArgs=--add-modules=http,deploy,{ee-current}-demo-simple
highlight=WebAppContext
....

All attributes (such as role) and options (such as nowrap) on the jetty block will be passed through to the generated literal block. Think of the jetty block as the output block, except that the inputs will be swapped with the output.

You may wondering why the jetty block is a literal block instead of a listing block. Listing blocks are intended for samples. Instruction sets that produce/display output (like a diagram block) are what the literal block is intended for. (listing for sample, literal for output). The use of a literal block also makes it stand out more in the AsciiDoc source. While it won't break anything to use a listing block, but my recommendation is still to use a literal block in this case.

Alternate syntax

An alternative to the jetty literal block would be the jetty block macro. Here's how that would look.

jetty::[setupArgs="--add-modules=http,deploy,{ee-current}-demo-simple",highlight="WebAppContext",options="nowrap"]

The benefit is it's a single line if that's the form you prefer. Personally, I find the single-line to be very difficult to read, especially as the number of arguments increases. But there are more significant drawbacks. Most notably, it's not clear which attributes configure the input and which attributes configure the output. It also doesn't preview well in the IDE since it just appears as a paragraph when the extension is not registered. And you're back to having to worry about quoting the attribute values when they contain commas. With that said, it's still a viable alternative and it wouldn't be wrong to choose it. But I would only consider it if you really can't live with the jetty block for whatever reason.

Integration

The trickiest part of this syntax extension is not the syntax itself, but the integration. Since the documentation was previously built using AsciidoctorJ, the custom include processor was able to run within the same Java runtime as a Java-based AsciidoctorJ extension. Since Antora is written in Node.js, the Jetty runner has to be run in a separate process (which itself uses a Java process to run Jetty).

The separate process could either be a Java call or a web request to a service that runs Jetty. For now, I think the Java call fits best with the raw materials that are already there.

We can reuse most of the existing Jetty include processor by converting it to a main class instead named RunJetty. (We can put this class in the jetty-testers project since that's where the classes it uses are located). When a jetty block is encountered, the extension calls the main class, tidying up and passing in the arguments from the block. The output from that call is then used as the contents of the listing block. Using this approach, we don't have to rewrite any of the logic of running Jetty and filtering/decorating the output. However, the setupModules argument does have to be handled by the jetty block processor since it has to resolve and copy files that are stored in Antora's content catalog. So the RunJetty class will not be responsible for that argument, unlike the former include processor.

What remains is preparing a Jetty installation (Jetty home) from which to run Jetty and a Java classpath in order to run the RunJetty main class. That's where Antora Collector comes in.

We're using Antora Collector to run Maven on the worktree for each Antora content root (basically, the Jetty project branch). When using a local repository, this will happen within the cloned repository in the normal way. When it's a remote repository (such as when running in CI), Antora Collector will create a temporary worktree for the branch in which to run Maven (then cleans up that worktree afterwards).

The point of running Maven is two fold. First, we need it to create Jetty home for us. Second, it needs to compile the RunJetty main class and export the classpath to a property we can pass to Antora. We'll introduce a documentation/build-helpers project to set this up. With that in place, Antora Collector must run the following command:

mvn install -B -Pfast -Dmaven.test.skip=true -am -pl documentation/build-helpers

That build will filter the antora.yml file to populate the jetty-home and run-jetty-classpath attributes by replacing the placeholders. Collector will then import that filtered file, making those attributes available to the jetty block processor (as well as the includes that start with {jetty-home}).

Improvements

One quirky part is building the classpath to enable the jetty block processor to run the RunJetty main class. We're using the dependencies:build-classpath goal to export the classpath to a property, which then gets inserted into the value of an AsciiDoc attribute in antora.yml with the help of Maven and Antora Collector. However, dependencies:build-classpath wants to resolve jetty-testers to the jar in the target directory of the jetty-testers project instead of the local Maven repository. That's a problem since Antora Collector removes the worktree after it finishes, so the location in the target folder won't exist anymore. As a workaround, we have to build the location of the installed jar explicitly in the value of the attribute. An alternate approach would be to have Maven build an application dist that the jetty block processor can run so it doesn't have to worry about the classpath.

mojavelinux commented 7 months ago

This proposal has been implemented in the staging site. You can find the jetty block processor here:

https://github.com/webtide/jetty.website/blob/main/lib/jetty-block.js

Here are some of the pages in the preview site on which you can see the result:

Here are some pages in the preview site that include files from $JETTY_HOME, which was implemented as part of this change:

mojavelinux commented 5 months ago

From our perspective, this issue is now solved. The block processor works on the migrated branches without having to make any manual changes. That means when the migration PRs are merged, it will just work in the upstream.

mojavelinux commented 4 months ago

I've submitted a PR with documentation for this block. See #28.