Aubade • Data Authoring Framework

0.8.0 Changelog

Aubade provides a set of modules that can be used in conjunction with each other.


A lightweight core module with minimal features and dependencies that does not rely on platform-specific modules so that it could be used anywhere safely.


Parse accepts a source string and returns a { body, metadata } structure. This function is mainly used to separate the front matter from the content or in this case body.

export function parse(source: string): { body: string; metadata: Record<string, any> & { readonly estimate: number; readonly table: AubadeTable[]; }; };

If you need to read from a file or folder, use the visit and traverse function from the /compass module.


Where the metadata or front matter index gets constructed, it is used in the parse function.

type Primitives = null | boolean | string; type ValueIndex = Primitives | Primitives[]; type FrontMatter = { [key: string]: ValueIndex | FrontMatter }; export function construct(raw: string): ValueIndex | FrontMatter;



This isn’t usually necessary, but in case you want to handle the markdown parsing and rendering by yourself, here’s how you can tap into the transform function provided by the module.

export interface Dataset { file?: string; language?: string; [data: string]: string | undefined; } export function transform(source: string, dataset: Dataset): string;

A simple example would be passing a raw source code as a string.

import { transform } from 'aubade/artisan'; const source = ` interface User { id: number; name: string; } const user: User = { id: 0, name: 'User' } `; transform(source, { lang: 'typescript' });

Another one would be to use as a highlighter function.

import markdown from 'markdown-it'; import { transform } from 'aubade/artisan'; // passing as a 'markdown-it' options const marker = markdown({ highlight: (source, language) => transform(source, { language }); });


The artisan module also exposes the marker import that is a markdown-it object.

import { marker } from 'aubade/artisan'; import plugin from 'markdown-it-plugin'; // some markdown-it plugin marker.use(plugin); // add this before calling 'visit' or 'traverse'

Importing marker to extend with plugins is optional, it is usually used to enable you to write LaTeX in your markdown for example, which is useful for math typesetting and writing abstract symbols using TeX function. Here’s a working example with a plugin that uses KaTeX.

import { marker } from 'aubade/artisan'; import { visit } from 'aubade/compass'; import TexMath from 'markdown-it-texmath'; import KaTeX from 'katex'; marker.use(TexMath, { engine: KaTeX, delimiters: 'dollars', }); const data = visit(/* source path */);



This is the browser module to hydrate and give interactivity to your HTML.

import type { ActionReturn } from 'svelte/action'; export function hydrate(node: HTMLElement, key: any): ActionReturn;

The hydrate function can be used to make the rendered code blocks from your markdown interactive, some of which are

  • toggle code line numbers
  • copy block to clipboard

Usage using SvelteKit would simply be

<script> import { hydrate } from 'aubade/browser'; import { navigating } from '$app/stores'; </script> <main use:hydrate={$navigating}> <!-- content here --> </main>

Passing in the navigating store into the key parameter is used to trigger the update inside hydrate function and re-hydrate the DOM when the page changes but is not remounted.



interface AubadeTable { id: string; level: number; title: string; } export function visit<Output>(entry: string): Output & { readonly estimate: number; readonly table: AubadeTable[]; content: string; };

The first argument of visit is the source entry point.


interface FileChunk { type: 'file'; path: string; breadcrumb: string[]; buffer: Buffer; } interface DirChunk { type: 'directory'; path: string; breadcrumb: string[]; buffer: undefined; } export function scan<T extends 'all' | 'files' | 'directories'>( type: T, entry: string, ): FileChunk[] | DirChunk[];

The first argument of scan is the type of item to scan, and the second argument is the entrypoint. It returns an array of FileChunk when type is 'files', DirChunk when type is 'directories', and both when type is 'all'. The entry argument can be anything as long as it exists in the filesystem, though it would return an empty array if it’s not a directory.


export function traverse( entry: string, options?: { depth?: number; }, ): { files: FileChunk[]; hydrate( load: (chunk: HydrateChunk) => undefined | Output, filter?: (path: string) => boolean, ): Output[]; };

The first argument of traverse is the directory entrypoint, and the second argument is its options. It returns an object with the hydrate method that accepts a load callback and an optional files function to filter item to process into load, it takes the full path of a file and returns a boolean. If the function returns true, the load callback will be called upon the file, else it will ignored and filtered out from the final output.

content ├── posts │ ├── │ │ └── │ ├── │ │ └── │ ├── my-first-post │ │ ├── │ │ └── thumbnail.jpeg │ └── my-amazing-journey │ ├── │ ├── photo.jpeg │ └── thumbnail.jpeg └── reviews ├── game │ └── doki-doki-literature-club │ ├── │ └── thumbnail.jpeg ├── book │ ├── amazing-book-one │ │ ├── │ │ └── thumbnail.jpeg │ └── manga-is-literature │ ├── │ └── thumbnail.jpeg └── movie ├── spirited-away │ ├── │ └── thumbnail.jpeg └── your-name ├── └── thumbnail.jpeg

An example usage from a hypothetical content folder structure above should look like

import { visit, traverse } from 'aubade/compass'; /* visit - parse a single source file */ const article = visit('content/posts/'); // ^- { content: '...', metadata: { ... } } /* traverse - scans a directory for sources */ const data = traverse('content/posts', { depth: -1 }).hydrate( ({ breadcrumb: [file, slug], buffer, marker, parse }) => { if (file.startsWith('draft')) return; const { body, metadata } = parse(buffer.toString('utf-8')); return { ...metadata, slug, content: marker.render(body) }; }, ); /* traverse - nested directories infinite recursive traversal */ const data = traverse('content/reviews', { depth: -1 }).hydrate( ({ breadcrumb: [file, slug, category], buffer, parse }) => { const { body, metadata } = parse(buffer.toString('utf-8')); return { ...metadata, slug, category, content: marker.render(body) }; }, );


This is a standalone module which provides a set of transformer function. They can be used in conjunction with each other by utilizing the pipe function provided from the 'mauss' package and re-exported by this module, you can do the following

import { traverse } from 'aubade/compass'; import { chain } from 'aubade/transform'; const items = traverse('path/to/content').hydrate(() => {}); chain(items, { ... });


The chain transformer is used to add a flank property to each items and attaches the previous (idx - 1) and the item after (idx + 1) as flank: { back, next }, be sure to sort it the way you intend it to be before running this transformer.

export function chain<T extends { slug?: string; title?: any }>( items: T[], options: { base?: string; breakpoint?: (next: T) => boolean; sort?: (x: T, y: T) => number; }, ): Array<T & Attachment>;
  • A base string can be passed as a prefix in the slug property of each items.
  • A breakpoint function can be passed to stop the chain on a certain condition.
  • A sort function can be passed to sort the items before chaining them.