npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@d10f/asciidoc-astro-loader

v0.0.6

Published

An Astro collections loader for Asciidoc files

Downloads

226

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-loader

And 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.