arcdown
v2.3.0
Published
A small stack of Markdown tools configured using the Architect's team preferred conventions.
Downloads
55
Readme
Arcdown: Architect's Markdown Renderer
A small stack of Markdown tools (built on
markdown-it
) configured using the Architect team's preferred conventions for creating documentation and articles rendered and served from a cloud function.
Contents
What is this?
Arcdown is an opinionated toolchain to create technical content from Markdown source files as quickly as possible to enable on-the-fly rendering in a Lambda (or any server) runtime.
Features
- Document table of contents creator
- Code syntax highlighter
- Automatic frontmatter parsing
- Generated HTML optimizations
- Helpful return values
All built-ins are configurable and extensible.
Usage
Installation
npm install arcdown
ESM only, requires Node.js v14+
Example
The simplest usage is to just pass Arcdown.render
a string of Markdown:
import { readFileSync } from 'node:fs'
import { Arcdown } from 'arcdown'
const mdString = `
---
title: Hello World
category: Examples
---
## Foo Bar
lorem ipsum _dolor_ sit **amet**
[Architect](https://arc.codes/)
`.trim()
const arcdown = new Arcdown()
const {
frontmatter, // attributes from frontmatter
html, // the good stuff: HTML!
slug, // a URL-friendly slug
title, // document title from the frontmatter
tocHtml, // an HTML table of contents
} = await arcdown.render(mdString)
const fromFile = await arcdown.render(readFileSync('../docs/some-markdown.md', 'utf-8'))
⚙️ See below for configuration options.
Render Result
Arcdown.render
returns a RenderResult
object with 4 strings plus any document "frontmatter".
html: string
The Markdown document contents as HTML, unmodified, rendered by markdown-it
.
const { html } = await arcdown.render(mdString)
const document = `
<html>
<body>
<main>${html}</main>
</body>
</html>
`
tocHtml: string
The document's table of contents as HTML (nested unordered lists).
const { tocHtml, html } = await arcdown.render(mdString)
const document = `
<html>
<body>
<article>${html}</article>
<aside>${tocHtml}</aside>
</body>
</html>
`
title: string
The document title, lifted from the document's frontmatter.
const { title } = await arcdown.render(mdString)
console.log(`Rendered "${title}"`)
slug: string
A URL-friendly slug of the title. (possibly empty) Synonymous with links in the table of contents.
const { slug } = await arcdown.render(mdString)
const docLink = `http://my-site.com/docs/${slug}`
frontmatter: object
All remaining frontmatter. (possibly empty)
The document's frontmatter is parsed by gray-matter
and directly returned here.
const { frontmatter } = await arcdown.render(file, options)
const sortedTags = frontmatter.tags.sort()
Configuration
Arcdown is set up to be used without any configuration. Out-of-the-box it uses defaults and conventions preferred by the Architect team (Architect project not required).
However, the renderer is customizable and extensible with a RendererOptions
object.
🪧 See ./example/ for a kitchen sink demo.
markdown-it
Config: markdownIt
markdownIt
Configure the core markdown-it
renderer.
This config is passed directly to new MarkdownIt()
const arcdown = new Arcdown({
markdownIt: { linkify: false },
})
By default, html
, linkify
, and typographer
are enabled.
Plugin Overrides: pluginOverrides
Three plugins are provided out-of-the-box and applied in a specific order.
Set configuration for each plugin by passing a keyed RendererOptions.pluginOverrides
object.
⛔️ Disable a plugin by setting its key in
pluginOverrides
tofalse
.
markdownItClass
Apply class names to each generated element based on its tag name. Provide a map of element names to an array of classes to be applied.
Perfect for utility class libraries.
This plugin is disabled unless configuration is provided.
const arcdown = new Arcdown({
pluginOverrides: {
markdownItClass: {
// an element => class map
h2: [ 'title' ],
p: [ 'prose' ],
}
},
})
For performance reasons, this plugin was modified and bundled to ./src/vendor/
markdownItExternalAnchor
Mark all external links (links starting with "http[s]://") with target=_blank
and an optional class.
markdown-it-external-anchor
defaults are used in Arcdown.
markdown-it-external-anchor
docs
const arcdown = new Arcdown({
pluginOverrides: {
markdownItExternalAnchor: {
domain: 'arc.codes',
class:'external',
},
},
})
markdownItAnchor
A markdown-it plugin that adds an id attribute to headings and optionally permalinks.
const arcdown = new Arcdown({
pluginOverrides: {
markdownItAnchor: {
tocClassName: 'pageToC',
},
},
})
markdown-it-anchor
is pre-configured with:
{
tabIndex: false,
}
markdownItToc
A table of contents (TOC) plugin for Markdown-it with focus on semantic and security. Made to work gracefully with markdown-it-anchor.
markdown-it-toc-done-right
docs
const arcdown = new Arcdown({
pluginOverrides: {
markdownItToc: {
containerClass: 'pageToC',
},
},
})
markdown-it-toc-done-right
enables users to include a copy of the table of contents in their markdown:
My Table of Contents:
${toc}
# The rest of
## My document
User-Provided Plugins: plugins
It is possible to pass additional markdown-it
plugins to Arcdown's renderer by populating RendererOptions.plugins
.
Plugins can be provided in two ways and will be applied after the default plugins bundled with Arcdown.
plugins
The simplest method for extending markdown-it
is to import a plugin function and provide it directly.
import markdownItAttrs from 'markdown-it-attrs'
const arcdown = new Arcdown({
plugins: { markdownItAttrs },
})
plugins
with options
If a plugin requires options, provide the markdown-it
plugin as a tuple where the first item is the function and the second is the plugin options.
Here the key name provided does not matter.
import markdownItEmoji from 'markdown-it-emoji'
const arcdown = new Arcdown({
plugins: {
mdMoji: [
markdownItEmoji, // the plugin function
{ shortcuts: { laughing: ':D' } }, // options
],
},
})
Highlight.js (hljs) Config: hljs
A custom highlight()
method backed by Highlight.js is provided to the internal markdown-it
renderer. Arcdown will detect languages used in fenced code blocks in the provided Markdown string and attempt to register just those languages in hljs.
⚠️ Currently, shorthand aliases for languages are not supported.
Full language names should be used with Markdown code fences. Instead ofjs
, usejavascript
Set Highlight.js configuration by passing a keyed RendererOptions.hljs
object.
classString: string
A string that will be added to each <pre class="">
wrapper tag for highlighted code blocks.
const arcdown = new Arcdown({
hljs: {
classString: 'hljs relative mb-2',
},
})
ignoreIllegals: boolean
Passed directly to hljs.highlight()
. The docs say:
when true forces highlighting to finish even in case of detecting illegal syntax for the language[...]
const arcdown = new Arcdown({
hljs: {
ignoreIllegals: false,
},
})
ignoreIllegals: true
is the default, but can be set by the user.
languages: object
Additional language syntaxes can be added from third party libraries.
If needed, Highlight.js built-in languages can be disabled by setting their key to false
.
import leanSyntax from 'highlightjs-lean'
const arcdown = new Arcdown({
hljs: {
languages: {
lean: leanSyntax, // add lean
powershell: false, // disallow powershell
},
},
})
sublanguages: object
Declare languages that should be registered when a specific language is detected.
A common use-case is registering 'xml'
for 'javascript'
to enable HTML highlighting for html
string templates.
import leanSyntax from 'highlightjs-lean'
const arcdown = new Arcdown({
hljs: {
sublanguages: {
javascript: [ 'xml' ],
},
},
})
plugins: object[]
Highlight.js plugins can be passed to Arcdown's highlighter as an array of objects or class instances with functions keyed as hljs callbacks.
See the hljs plugin docs for more info.
class CodeFlipper {
constructor(options) {
this.token = options.token
}
'after:highlight'(result) {
result.value = result.value
.split(this.token)
.reverse()
.join(this.token)
}
}
const arcdown = new Arcdown({
hljs: {
plugins: [new CodeFlipper({ token: '\n' })],
},
})
Development & Contributing
A couple plugins have been forked and/or vendored locally to this package. This has been done to increase performance and render speed.
Arcdown is not attached to any single package, plugin, or even to the core rendering engine, so long as the resulting features are maintained.
Suggestions and PRs welcome 🙏
FAQs & Decisions
Why markdown-it
?
A great balance of speed, stability, adoption, and extensibility.
Why Highlight.js?
Most syntax highlighters are not fast enough for server-side rendering. hljs was tuned to work on slow client machines and performs well on a server.
That said, starry-night
is really interesting.
Why plugin ___?
Because we used it a lot building docs sites and technical blogs.
Credits
In no particular order
- markdown-it and their community for a solid .md ecosystem
- highlight.js for a battle-tested highlighter
- Architect and Begin for helping test/break things
- @galvez for the rad readme.md formatting conventions
Todo
- [x] additional testing
- [x] type defs
- [x] benchmarks (try against remark)
- [x] look for hljs perf increases
- [ ] expand typings with definitions from markdown-it
- [ ] web component enhancements 😏
- [ ] CLI for static file creation