Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/architect/arcdown

A small stack of Markdown tools configured using some preferred conventions for creating technical content rendered and served from a cloud function.
https://github.com/architect/arcdown

markdown

Last synced: 6 days ago
JSON representation

A small stack of Markdown tools configured using some preferred conventions for creating technical content rendered and served from a cloud function.

Awesome Lists containing this project

README

        


Architect Logo


GitHub CI status
Apache-2.0 License

# Arcdown: [Architect](https://arc.codes)'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

1. [Usage](#usage)
1. [Installation](#installation)
1. [Example](#example)
1. [Render Result](#render-result)
1. [Configuration](#configuration)
1. [MarkdownIt Renderer](#markdown-it-config-markdownit)
1. [Plugin Overrides](#plugin-overrides-pluginoverrides)
1. [User-Provided Plugins](#user-provided-plugins-plugins)
1. [Highlight.js Config](#highlightjs-hljs-config-hljs)
1. [Development & Contributing](#development--contributing)
1. [FAQs & Decisions](#faqs--decisions)
1. [Credits](#credits)
1. [Todo](#todo)

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

```shell
npm install arcdown
```

_ESM only, requires Node.js v14+_

## Example

The simplest usage is to just pass `Arcdown.render` a string of Markdown:

```javascript
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](#configuration).

## 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`.


```javascript
const { html } = await arcdown.render(mdString)

const document = `

${html}

`
```

### `tocHtml: string`

The document's table of contents as HTML (nested unordered lists).


```javascript
const { tocHtml, html } = await arcdown.render(mdString)

const document = `

${html}
${tocHtml}

`
```

### `title: string`

The document title, lifted from the document's frontmatter.


```javascript
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.


```javascript
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`](https://github.com/jonschlinkert/gray-matter) and directly returned here.


```javascript
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](https://github.com/markdown-it/markdown-it).
This config is passed directly to `new MarkdownIt()`


```javascript
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` to `false`.

### `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.

[`markdown-it-class` docs ](https://github.com/HiroshiOkada/markdown-it-class)


```javascript
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](https://github.com/binyamin/markdown-it-external-anchor)


```javascript
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.

[`markdown-it-anchor` docs](https://github.com/valeriangalliat/markdown-it-anchor)


```javascript
const arcdown = new Arcdown({
pluginOverrides: {
markdownItAnchor: {
tocClassName: 'pageToC',
},
},
})
```

`markdown-it-anchor` is pre-configured with:

```javascript
{
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](https://github.com/nagaozen/markdown-it-toc-done-right)


```javascript
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:

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


```javascript
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.


```javascript
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](https://highlightjs.org/) 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 of `js`, use `javascript`

Set Highlight.js configuration by passing a keyed `RendererOptions.hljs` object.

### `classString: string`

A string that will be added to each `

` wrapper tag for highlighted code blocks.



```javascript
const arcdown = new Arcdown({
hljs: {
classString: 'hljs relative mb-2',
},
})
```

### `ignoreIllegals: boolean`

Passed directly to `hljs.highlight()`. [The docs](https://highlightjs.readthedocs.io/en/latest/api.html#highlight) say:

> when true forces highlighting to finish even in case of detecting illegal syntax for the language[...]


```javascript
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`.


```javascript
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.


```javascript
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](https://highlightjs.readthedocs.io/en/latest/plugin-api.html) for more info.


```javascript
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`](https://github.com/wooorm/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