@d10f/asciidoc-astro-loader
v0.0.6
Published
An Astro collections loader for Asciidoc files
Downloads
226
Maintainers
Readme
Astro + Asciidoc combined
This package will allow you to load Asciidoc files (with either an .asciidoc or .adoc extension) into an Astro Collection. It leverages Asciidoctor.js to do the heavy lifting, with a few but substantial configuration options added to make it more versatile.
Features
- [x] Out of the box syntax highlighting, powered by Shiki.
- [x] Support for custom templating engines.
- [x] Support for custom converters for maximum control over the output HTML.
- [x] Full TypeScript support.
- [x] Restructure configuration options regarding Shiki transformer integration.
Roadmap
- [ ] Async support on custom template and converter class render methods.
- [ ] Include support for more template engines out of the box.
Getting Started
Install the package from npm:
npm install @d10f/asciidoc-astro-loaderAnd import the loader function inside your content.config.ts file to define a new collection:
import { defineCollection, z } from "astro:content";
import { asciidocLoader } from "asciidoc-astro-loader";
const blog = defineCollection({
loader: asciidocLoader({
base: ".src/content/blog",
}),
schema: z.object({
title: z.string(),
preamble: z.string().optional(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date().optional(),
}),
});Configuration
Syntax Highlighting
Syntax highlighting is already taken care of out of the box, but you can provide additional options to tweak things to your liking. For example, the default theme is Catppuccin, but you might want to use something else:
const blog = defineCollection({
loader: asciidocLoader({
base: ".src/content/blog",
syntaxHighlighting: {
theme: 'one-dark-pro',
}
}),
});You can specify an object for different light and dark themes, as well:
const blog = defineCollection({
loader: asciidocLoader({
base: ".src/content/blog",
syntaxHighlighting: {
theme: {
light: "everforest-light",
dark: "everforest-dark",
},
}
}),
});And even provide additional themes. Note that you will have to take care of implementing the logic to switch to that theme. Checkout Shiki's documentation to learn more.
const blog = defineCollection({
loader: asciidocLoader({
base: ".src/content/blog",
syntaxHighlighting: {
theme: {
light: "gruvbox-light-hard",
dark: "gruvbox-dark-hard",
dim: "gruvbox-dark-medium"
},
}
}),
});Shiki Trasnformers
One of the coolest features from Shiki are transformers. You can provide a list of the transformers that you want to use at the loader configuration:
import {
transformerNotationDiff,
transformerNotationFocus,
transformerNotationHighlight,
} from '@shikijs/transformers';
const blog = defineCollection({
loader: asciidocLoader({
base: ".src/content/blog",
syntaxHighlighting: {
transformers: [
transformerNotationDiff(),
transformerNotationHighlight(),
transformerNotationFocus(),
],
}
}),
});If you want to write your own transformers, you can just follow Shiki's documentation and provide them here. However, you might also be interested in performing some conditional logic based on the type of Asciidoc node you're working with. To gain access to the node, define the transformer as a factory function using the ShikiTransformerFactory type:
import type { ShikiTransformerFactory } from '../../../types/index.js';
type TranformerOptions = {
cssClasses: string;
};
export const transformerAsciidocSomething: ShikiTransformerFactory<
TransformerOptions
> = ({ cssClasses }) => {
return (node) => {
return {
preprocess() {
if (node.getAttribute('custom-attribute')) {
// Maybe you only want to do something based on
// some custom attribute?
}
}
}
}
}import { transformerAsciidocSomething } from './usr/share/transformers';
const blog = defineCollection({
loader: asciidocLoader({
base: ".src/content/blog",
syntaxHighlighting: {
transformers: [
transformerAsciidocSomething({
cssClasses: 'text-red-500'
}),
],
}
}),
});Custom Templates
A nice feature of Asciidoctor.js is the use of default templates for rendering the different nodes, which can be overwritten by simply providing your own. With asciidoc-astro-loader you can easily provide a directory that contains your custom templates:
const blog = defineCollection({
loader: asciidocLoader({
base: ".src/content/blog",
document: {
template: "./usr/share/templates"
}
}),
});Just like regular Asciidoctor.js, any files located here will be used to replace content for nodes whose context matches the name of the template. That is, for a paragraph node, you would have a file named "paragraph.hbs", for example. This would use the Handlebars templating engine.
Registering A Templating Engine
Templates can be written as plain functions that return an HTML string, or as templates written in a format supported by one of the registered templating engines. By default, only Handlebars and Nunjucks are supported, but you can create your own. A templating engine in the context of this package is a class that extends the AbstractEngine class:
import { Php, Request } from '@platformatic/php-node';
import { AbstractEngine } from '@d10f/asciidoc-astro-loader/engines';
import type { AsciidocTemplate, FilesystemTemplate } from '@d10f/asciidoc-astro-loader';
export class PhpEngine extends AbstractEngine implements AsciidocTemplate, FilesystemTemplate {
private server: Php;
constructor({ name = 'php', extensions = ['php'], root: string }) {
super(name, extensions);
this.server = new Php({ docroot: root });
}
/**
* This method is enforced by the AsciidocTemplate interface.
*/
renderNode(node: AbstractBlock, options?: Record<string, unknown>) {}
/**
* This method is enforced by the FilesystemTemplate interface.
*/
renderFile(filepath: string, options?: Record<string, unknown>) {}
}Implementing these template interfaces is completely optional, but they help keeping things organized, predictable, easy to test, etc. The AsciidocTemplate interface is particularly important, however, as it enforces the implementation of the renderNode method, which will be invoked automatically whenever a template file exists with a file extension supported by this engine.
[!WARNING] (WIP): The render methods must not return a Promise.
When your engine is ready, you can import it and pass it to the loader configuration object:
import { PhpEngine } from './usr/share/engines';
const blog = defineCollection({
loader: asciidocLoader({
base: ".src/content/blog",
document: {
template: "./usr/share/templates",
templateEngines: [
New PhpEngine({ name: 'php', extensions: ['php'], root: './usr/share/templates' })
],
}
}),
});For completeness, this is what a basic implementation of the above example might look like:
import { Php, Request } from '@platformatic/php-node';
import { AbstractEngine } from '@d10f/asciidoc-astro-loader/engines';
import type { AbstractBlock } from 'asciidoctor';
import type {
AsciidocTemplate,
FilesystemTemplate,
NodeContext
} from '@d10f/asciidoc-astro-loader';
export class PhpEngine extends AbstractEngine implements AsciidocTemplate, FilesystemTemplate {
private server: Php;
constructor({ name = 'php', extensions = ['php'], root: string }) {
super(name, extensions);
this.server = new Php({ docroot: root });
}
renderNode(node: AbstractBlock, options?: Record<string, unknown>) {
const context = node.getNodeName() as NodeContext;
const content = node.getContent();
const templateFile = this.templateList.get(context);
if (templateFile) {
const filepath = templateFile.replace(/^.+\/(.+)$/, '$1');
return this.renderFile(filepath, { content });
}
}
renderFile(filepath: string, options?: Record<string, unknown>): string {
const request = new Request({
method: 'POST',
url: 'http://localhost/' + filepath,
body: Buffer.from(JSON.stringify(options)),
});
const response = this.server.handleRequestSync(request);
return response.body.toString();
}
}We can now have a template file such as "paragraph.php" that will be processed by this engine. Leveraging Platformatic's module, we can write template files in native PHP via WebAssembly!
<?php
$body = json_decode(file_get_contents('php://input'));
?>
<p><?= strtoupper($body->content) ?></p>Custom Converters
Custom converters are refined versions of templates. The main difference is that they can be configured further, which gives more granular control over the conversion of Asciidoc blocks. An example of custom converters in action comes from the syntax highlighting that's built into asciidoc-astro-loader, which processes the node through Shiki.
For maximum flexibility, both custom converters and templates can be used at the same time. You can even define templates that aren't designed to be used to render nodes directly, but called directly from within your custom converters. This is why the use of interfaces when defining custom templates is important, to ensure consistency and type-safety.
In general, prefer using converters for anything that requires heavy use of logic or that reads configuration options provided by the user, and templates for simpler HTML output.
Custom converters are provided as an array to the document.converters option.
// src/content.config.ts
const blog = defineCollection({
loader: asciidocLoader({
base: ".src/content/blog",
document: {
converters: [
myCustomConverter({ /* ... */ })
]
},
}),
});Registering a custom converter
A custom converter is declared as a factory function that accepts a configuration object, and returns an inner function that gets called automatically, receiving the options provided to the loader and the instance of the Shiki highlighter.
import type { CustomConverterFactoryFn, NodeContext } from '@d10f/asciidoc-astro-loader';
export const myCustomConverter: CustomConverterFactoryFn = ({ nodeContext }) => {
return (options, highlighter) => {
return {
/**
* The type of node that this converter will act upon. It
* can either be hard-coded here, or passed as an option.
*/
nodeContext,
/**
* The convert function that will produce the HTML output.
* It receives the node that it will convert, and an
* instance of the template engine registry, which you can
* use to access any and all available templat engines to
* customize the output even further.
*/
convert(node, templateEngine) {
return '<p>Result!</p>';
}
};
}
}In addition, the convert method receives the node that is being processed, as well as an instance of the template engine registry. You can use this to render the HTML from a template as well, combining both to get the best of both worlds.
