/noddity-micromark-renderer

Use micromark and mdast to render Noddity files.

Primary LanguageJavaScriptOtherNOASSERTION

Noddity Micromark Renderer

This is a re-work of Noddity that uses Micromark and mdast to render the Markdown content, and has no opinion about non-Markdown content.

Included is a legacy renderer, if you want to use classic Noddity syntax and the legacy Ractive renderer. (See the demo for an example of it.)

Install

The usual ways:

npm install noddity-micromark-renderer

Using

You'll need to initialize the renderer with several options (all of these are required):

import { noddityRenderer } from 'noddity-micromark-renderer'
const render = noddityRenderer({
	// Noddity-specific functions
	loadFile,
	metadataParser,
	nonMarkdownRenderer,
	urlRenderer,
	// Micromark-specific functions
	hastToHtml,
	markdownToMdast,
	mdastToHast,
})
// for later examples
const NODDITY_FOLDER = '/path/to/noddity/content'
const DOMAIN = 'my-site.com'

If you're using the legacy renderer, which uses Ractive, you don't need nearly as much:

import { noddityRenderer } from 'noddity-micromark-renderer/legacy'
const render = noddityRenderer({
	directory: NODDITY_FOLDER,
	domain: 'site.com',
	pathPrefix: '#!/',
	pagePathPrefix: 'post/',
	name: 'My Cool Website',
})

API of non-legacy noddityRenderer

Each input property is defined here:

loadFile

Typed: (filename: string, options?: Object) => Promise<string>

Used by the renderer to lookup Noddity templates. If you're rendering from disk, you could do:

import { join } from 'node:path'
import { readFile } from 'node:fs/promises'
const loadFile = async filename => readFile(join(NODDITY_FOLDER, filename), 'utf8')

The options object is passed along to your non-Markdown renderer, merged into the overall options.

metadataMdastMutator

Typed: async ({ filename: string, mdastTree: MdastTree }): void

Called by the renderer to mutate the mdast (Markdown Abstract State Tree) to optionally parse the frontmatter section of each file and set it.

After this is called, mdast should have these properties set to properly parse the remaining content:

  • mdast.children[0].metadata - This is the fully parsed file metadata, if present.
  • mdast.children[0].position.end.offset - This is the unist positional information about the character offset at the very end of the metadata, including frontmatter fence characters.

If you want to use js-yaml that would be (defining the schema is not required by this renderer):

import { load, JSON_SCHEMA } from 'js-yaml'
const metadataParser = frontmatter => load(frontmatter, { schema: JSON_SCHEMA })

nonMarkdownRenderer

Typed: ({ filename: string, template: string, string: string, metadata: Object, variables: Array<NoddityVariables> }) => Promise<string>

Where NoddityVariables: { name: string, positional: boolean, value: string }

This is where you would render non-Markdown content. In classic Noddity this means Ractive (at v0.7) styled templates, but for this renderer there are no opinions about what you should use.

The returned value is the fully rendered HTML.

Properties passed to the function:

  • filename: string - The file doing the calling of this as a template, if applicable, e.g. folder/my-file.md.
  • innerHtml?: string - If loading a post, this will be the fully rendered post content, while the templateString will be the surrounding post non-rendered string.
  • metadata?: Object - The parsed metadata from the files frontmatter section.
  • templateName: string - The name of the template, e.g. if the above file had ::img|face.jpg:: this would be img.
  • templateString: string - The string extracted from the Noddity file, e.g. in classic Noddity this would be the Ractive component.
  • variables?: Array<NoddityVariables> - An optional ordered list of variables passed along when calling this template.

Properties on the NoddityVariables objects are:

  • name: string - The name of the variable. For a reference like ::img|face.jpg|size=big:: the first variable's name would be face.jpg and the second would be size.
  • positional: boolean - Set to true if it is not a key=value named variable.
  • value?: string - The value, if it is a named variable.

urlRenderer

Typed: ({ filename: string, link: string }) => Promise<string>

It's up to you to render the correct URL string, but it's usually something like this:

const urlRenderer = ({ link }) => `https://${DOMAIN}/#!/post/${link}`

markdownToMdast

Typed: (markdown: string) => Promise<Mdast>

Async function that resolves to an mdast (Markdown Abstract State Tree), for example mdast-util-from-markdown, and that needs to contain the Noddity-specific nodes defined in mdast-util-noddity.

Here's how you might set this function up:

import { fromMarkdown } from 'mdast-util-from-markdown'
import { frontmatter } from 'micromark-extension-frontmatter'
import { frontmatterFromMarkdown } from 'mdast-util-frontmatter'
import { gfm } from 'micromark-extension-gfm'
import { gfmFromMarkdown } from 'mdast-util-gfm'

const markdownToMdast = string => fromMarkdown(string, {
	extensions: [
		// if you need more extensions, e.g. for additional Markdown functionality, you'd configure it here
		frontmatter([ 'yaml' ]),
		gfm(),
		micromarkFromNoddity(),
	],
	mdastExtensions: [
		// (and here)
		frontmatterFromMarkdown([ 'yaml' ]),
		gfmFromMarkdown(),
		mdastFromNoddity(),
	],
})

The function must return a promise which resolves to an mdast with the Noddity-specific nodes.

mdastToHast

Typed: (tree: MdastTree) => HastTree

Given an mdast tree, return an hast (HTML Abstract State Tree).

Note that, if you use templates of any kind, you'll probably want to allow HTML. Here's one way to do this:

import { toHast } from 'mdast-util-to-hast'
const mdastToHast = mdastTree => toHast(mdastTree, { allowDangerousHtml: true })

hastToHtml

Typed: (tree: HastTree) => string

Given an hast (HTML Abstract State Tree) output an HTML string.

Note that, if you use templates of any kind, the hast will contain text nodes that are HTML (as opposed to a strict hast) so you'll probably want to allow that. Here's one way:

import { toHtml } from 'hast-util-to-html'
const hastToHtml = hastTree => toHtml(hastTree, { allowDangerousHtml: true })

render

An initialized render is an object containing the following properties:

fromString

Typed: (markdown: string, virtualFilename?: string) => Promise<string>

This function is used to render free-hand sections of Markdown as noddity, e.g. instead of rendering a file you can render a chunk of Markdown with not other context.

The virtualFilename is used only for logging purposes, and if not provided will default to VIRTUAL_FILE.md.

loadFile

Typed: (filename: string) => Promise<string>

This is a per-file renderer function. It renders a file by loading the provided filename using the defined loadFile function, which can load files from anywhere, e.g. from disk, database, cloud storage, etc.

It then passes through the flow Markdown -> mdast -> Noddity (templates and links) -> hast -> html

loadMetadata

Typed: (filename: string) => Promise<Object>

This is a convenience method, which will use the defined loadFile to read in a file and parse out the frontmatter metadata section, using your provided metadataParser function to turn that string into an object.

loadPost

Typed: (templateFilename: string, postFilename: string) => Promise<string>

Similar to the loadFile function, except it renders the postFilename inside the context of the templateFilename (inside Noddity, by default this is the content/post file).

License

Published and released under the Very Open License.

If you need a commercial license, contact me here.