Link and Copy External Resources

In Implement a Syntax Highlighter Adapter we have seen how to implement a basic syntax highlighter that embeds all required resources as DocInfo in the document. When the document is converted with the attributes :linkcss and :copycss we expect though that these resources are also written to disk next to the document, and that the document only references them.

Looking at our current example of the highlight.js adapter we referenced the resources from the internet. For scenarios where it should also be possible to read the document while offline, the syntax highlighter can implement the interface org.asciidoctor.syntaxhighlighter.StylesheetWriter:

public class HighlightJsWithOfflineStylesHighlighter implements SyntaxHighlighterAdapter, Formatter, StylesheetWriter { (1)

    @Override
    public boolean hasDocInfo(LocationType location) {
        return location == LocationType.FOOTER;
    }

    @Override
    public String getDocinfo(LocationType location, Document document, Map<String, Object> options) {
        if (document.hasAttribute("linkcss") && document.hasAttribute("copycss")) { (2)
            return "<link rel=\"stylesheet\" href=\"github.min.css\">\n" +
                "<script src=\"highlight.min.js\"></script>\n" +
                "<script>hljs.initHighlighting()</script>";
        } else {
            return "<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/github.min.css\">\n" +
                "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js\"></script>\n" +
                "<script>hljs.initHighlighting()</script>";
        }
    }

    @Override
    public String format(Block node, String lang, Map<String, Object> opts) {
        return "<pre class='highlight'><code class='" + lang + "'>"
            + node.getContent()
            + "</code></pre>";
    }

    @Override
    public boolean isWriteStylesheet(Document doc) {
        return true; (3)
    }

    @Override
    public void writeStylesheet(Document doc, File toDir) {
        try {    (4)
            URL url1 = new URL("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/github.min.css");
            URL url2 = new URL("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js");

            try (InputStream in1 = url1.openStream();
                 OutputStream fout1 = new FileOutputStream(new File(toDir, "github.min.css"))) {
                IOUtils.copy(in1, fout1);
            } catch (IOException ioe) {
                throw new RuntimeException(ioe);
            }

            try (InputStream in2 = url2.openStream();
                 OutputStream fout2 = new FileOutputStream(new File(toDir, "highlight.min.js"))) {
                IOUtils.copy(in2, fout2);
            } catch (IOException ioe) {
                throw new RuntimeException(ioe);
            }

        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }
}
1 A syntax highlighter that writes additional resources to the filesystem next to the document must implement the interface org.asciidoctor.syntaxhighlighter.StylesheetWriter.
2 If the document is converted with the attributes :copycss and :linkcss the DocInfo that is added to the converted document should link to the local resources.
3 The syntax highlighter should return if it wants to write stylesheets in isWriteStylesheet(). This method could for example examine the document if it really needs external resources and return the corresponding result.
4 The method writeStylesheet() gets the org.asciidoctor.ast.Document and the File for the target directory where the document should be written. External resources should be written to this directory as well.

This highlighter writes the css and js resources to files in the same directory as the document if it is converted with the attributes :linkcss and :copycss:

        File toDir = // ...

        asciidoctor.syntaxHighlighterRegistry()
            .register(HighlightJsWithOfflineStylesHighlighter.class, "myhighlightjs");

        asciidoctor.convertFile(sources_adoc,
            Options.builder()
                .standalone(true)
                .toDir(toDir)              (1)
                .safe(SafeMode.UNSAFE)
                .attributes(Attributes.builder()
                    .sourceHighlighter("myhighlightjs")
                    .copyCss(true)         (1)
                    .linkCss(true)
                    .build())
                .build());

        File docFile = new File(toDir, "sources.html");
        assertTrue(docFile.exists());

        File cssFile = new File(toDir, "github.min.css");
        assertTrue(cssFile.exists());

        File jsFile = new File(toDir, "highlight.min.js");
        assertTrue(jsFile.exists());

        String html = Files.readString(Path.of(toDir.toURI()).resolve("sources.html"));
        assertThat(html, containsString("<link rel=\"stylesheet\" href=\"github.min.css\">"));
        assertThat(html, containsString("<script src=\"highlight.min.js\"></script>"));
1 External stylesheets are only written when converting to a file, not when converting to a stream or a string, and when the attributes :linkcss and :copycss are set.