├── .c8rc.json
├── .editorconfig
├── .github
└── workflows
│ └── ci.yaml
├── .gitignore
├── .prettierrc.yaml
├── .remarkrc.yaml
├── LICENSE.md
├── README.md
├── eslint.config.js
├── fixtures
├── anchor
│ ├── expected.jsx
│ └── input.md
├── custom-parser
│ ├── expected.jsx
│ ├── input.md
│ └── options.js
├── default
│ ├── expected.jsx
│ └── input.md
├── named
│ ├── expected.jsx
│ ├── input.md
│ └── options.json
├── null
│ ├── expected.jsx
│ └── input.md
├── toml-with-content
│ ├── expected.jsx
│ └── input.md
├── toml
│ ├── expected.jsx
│ └── input.md
└── undefined-named
│ ├── expected.jsx
│ ├── input.md
│ └── options.json
├── package-lock.json
├── package.json
├── src
├── remark-mdx-frontmatter.test.ts
└── remark-mdx-frontmatter.ts
└── tsconfig.json
/.c8rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "100": true,
3 | "reporter": ["html", "lcov", "text"]
4 | }
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = true
9 | max_line_length = 100
10 | trim_trailing_whitespace = true
11 |
12 | [COMMIT_EDITMSG]
13 | max_line_length = 72
14 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 | tags: ['*']
8 |
9 | jobs:
10 | eslint:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/setup-node@v4
15 | with:
16 | node-version: 22
17 | - run: npm ci
18 | - run: npx eslint
19 |
20 | pack:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v4
24 | - uses: actions/setup-node@v4
25 | with:
26 | node-version: 22
27 | - run: npm ci
28 | - run: npm pack
29 | - uses: actions/upload-artifact@v4
30 | with:
31 | name: package
32 | path: '*.tgz'
33 |
34 | prettier:
35 | runs-on: ubuntu-latest
36 | steps:
37 | - uses: actions/checkout@v4
38 | - uses: actions/setup-node@v4
39 | with:
40 | node-version: 22
41 | - run: npm ci
42 | - run: npx prettier --check .
43 |
44 | remark:
45 | runs-on: ubuntu-latest
46 | steps:
47 | - uses: actions/checkout@v4
48 | - uses: actions/setup-node@v4
49 | with:
50 | node-version: 22
51 | - run: npm ci
52 | - run: npx remark --frail .
53 |
54 | test:
55 | runs-on: ubuntu-latest
56 | strategy:
57 | matrix:
58 | node-version:
59 | - 18
60 | - 20
61 | - 22
62 | steps:
63 | - uses: actions/checkout@v4
64 | - uses: actions/setup-node@v4
65 | with:
66 | node-version: ${{ matrix.node-version }}
67 | - run: npm ci
68 | - run: npm test
69 | - uses: codecov/codecov-action@v4
70 | if: ${{ matrix.node-version == 22 }}
71 |
72 | release:
73 | runs-on: ubuntu-latest
74 | needs:
75 | - eslint
76 | - test
77 | - pack
78 | - prettier
79 | - remark
80 | if: startsWith(github.ref, 'refs/tags/')
81 | permissions:
82 | id-token: write
83 | steps:
84 | - uses: actions/setup-node@v4
85 | with:
86 | node-version: 22
87 | registry-url: https://registry.npmjs.org
88 | - uses: actions/download-artifact@v4
89 | with: { name: package }
90 | - run: npm publish *.tgz --provenance
91 | env:
92 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
93 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | dist/
3 | node_modules/
4 | *.log
5 | *.tsbuildinfo
6 | *.tgz
7 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | proseWrap: always
2 | semi: false
3 | singleQuote: true
4 | trailingComma: none
5 |
--------------------------------------------------------------------------------
/.remarkrc.yaml:
--------------------------------------------------------------------------------
1 | plugins:
2 | - remark-preset-remcohaszing
3 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright © 2021 Remco Haszing
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6 | associated documentation files (the “Software”), to deal in the Software without restriction,
7 | including without limitation the rights to use, copy, modify, merge, publish, distribute,
8 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all copies or substantial
12 | portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
15 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
17 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # remark-mdx-frontmatter
2 |
3 | [](https://github.com/remcohaszing/remark-mdx-frontmatter/actions/workflows/ci.yaml)
4 | [](https://codecov.io/gh/remcohaszing/remark-mdx-frontmatter)
5 | [](https://www.npmjs.com/package/remark-mdx-frontmatter)
6 | [](https://www.npmjs.com/package/remark-mdx-frontmatter)
7 |
8 | A [remark](https://remark.js.org) plugin for converting frontmatter metadata into MDX exports
9 |
10 | ## Table of Contents
11 |
12 | - [Installation](#installation)
13 | - [Usage](#usage)
14 | - [API](#api)
15 | - [Options](#options)
16 | - [Compatibility](#compatibility)
17 | - [License](#license)
18 |
19 | ## Installation
20 |
21 | This package depends on the AST output by
22 | [remark-frontmatter](https://github.com/remarkjs/remark-frontmatter)
23 |
24 | ```sh
25 | npm install remark-frontmatter remark-mdx-frontmatter
26 | ```
27 |
28 | ## Usage
29 |
30 | This remark plugin takes frontmatter content, and outputs it as JavaScript exports. Both YAML and
31 | TOML frontmatter data are supported.
32 |
33 | For example, given a file named `example.mdx` with the following contents:
34 |
35 | ```mdx
36 | ---
37 | hello: frontmatter
38 | ---
39 |
40 | Rest of document
41 | ```
42 |
43 | The following script:
44 |
45 | ```js
46 | import { readFile } from 'node:fs/promises'
47 |
48 | import { compile } from '@mdx-js/mdx'
49 | import remarkFrontmatter from 'remark-frontmatter'
50 | import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
51 |
52 | const { value } = await compile(await readFile('example.mdx'), {
53 | jsx: true,
54 | remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter]
55 | })
56 | console.log(value)
57 | ```
58 |
59 | Roughly yields:
60 |
61 | ```jsx
62 | export const frontmatter = {
63 | hello: 'frontmatter'
64 | }
65 |
66 | export default function MDXContent() {
67 | return
Rest of document
68 | }
69 | ```
70 |
71 | ## API
72 |
73 | The default export is a [remark](https://remark.js.org) plugin.
74 |
75 | ### Options
76 |
77 | - `name`: The identifier name of the variable the frontmatter data is assigned to. (Default:
78 | `frontmatter`).
79 | - `parsers`: A mapping A mapping of node types to parsers. Each key represents a frontmatter node
80 | type. The value is a function that accepts the frontmatter data as a string, and returns the
81 | parsed data. By default `yaml` nodes will be parsed using [`yaml`](https://github.com/eemeli/yaml)
82 | and `toml` nodes using [`toml`](https://github.com/BinaryMuse/toml-node).
83 |
84 | ## Compatibility
85 |
86 | This project is compatible with Node.js 18 or greater.
87 |
88 | ## License
89 |
90 | [MIT](LICENSE.md) © [Remco Haszing](https://github.com/remcohaszing)
91 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import config from '@remcohaszing/eslint'
2 |
3 | export default [...config, { ignores: ['fixtures'] }]
4 |
--------------------------------------------------------------------------------
/fixtures/anchor/expected.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | export const frontmatter = ((
4 | $0 = {
5 | title: 'Hello frontmatter'
6 | }
7 | ) => ({
8 | original: $0,
9 | reference: $0
10 | }))()
11 | function _createMdxContent(props) {
12 | return <>>
13 | }
14 | export default function MDXContent(props = {}) {
15 | const { wrapper: MDXLayout } = props.components || {}
16 | return MDXLayout ? (
17 |
18 | <_createMdxContent {...props} />
19 |
20 | ) : (
21 | _createMdxContent(props)
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/fixtures/anchor/input.md:
--------------------------------------------------------------------------------
1 | ---
2 | original: &anchor
3 | title: Hello frontmatter
4 | reference: *anchor
5 | ---
6 |
--------------------------------------------------------------------------------
/fixtures/custom-parser/expected.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | export const frontmatter = {
4 | content: 'foo: bar'
5 | }
6 | function _createMdxContent(props) {
7 | return <>>
8 | }
9 | export default function MDXContent(props = {}) {
10 | const { wrapper: MDXLayout } = props.components || {}
11 | return MDXLayout ? (
12 |
13 | <_createMdxContent {...props} />
14 |
15 | ) : (
16 | _createMdxContent(props)
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/fixtures/custom-parser/input.md:
--------------------------------------------------------------------------------
1 | ---
2 | foo: bar
3 | ---
4 |
--------------------------------------------------------------------------------
/fixtures/custom-parser/options.js:
--------------------------------------------------------------------------------
1 | /** @type {import('remark-mdx-frontmatter').RemarkMdxFrontmatterOptions} */
2 | export default {
3 | parsers: {
4 | yaml: (content) => ({ content })
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/fixtures/default/expected.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | export const frontmatter = {
4 | title: 'Hello frontmatter',
5 | index: 1,
6 | nested: {
7 | data: {
8 | structure: {
9 | including: {
10 | numbers: 42,
11 | booleans: true,
12 | '': null,
13 | arrays: ['of', 'items']
14 | }
15 | }
16 | }
17 | }
18 | }
19 | function _createMdxContent(props) {
20 | return <>>
21 | }
22 | export default function MDXContent(props = {}) {
23 | const { wrapper: MDXLayout } = props.components || {}
24 | return MDXLayout ? (
25 |
26 | <_createMdxContent {...props} />
27 |
28 | ) : (
29 | _createMdxContent(props)
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/fixtures/default/input.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Hello frontmatter
3 | index: 1
4 | nested:
5 | data:
6 | structure:
7 | including:
8 | numbers: 42
9 | booleans: true
10 | null:
11 | arrays:
12 | - of
13 | - items
14 | ---
15 |
--------------------------------------------------------------------------------
/fixtures/named/expected.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | export const frontmatter = {
4 | title: 'Hello frontmatter',
5 | index: 1,
6 | nested: {
7 | data: {
8 | structure: {
9 | including: {
10 | numbers: 42,
11 | booleans: true,
12 | '': null,
13 | arrays: ['of', 'items']
14 | }
15 | }
16 | }
17 | }
18 | }
19 | function _createMdxContent(props) {
20 | return <>>
21 | }
22 | export default function MDXContent(props = {}) {
23 | const { wrapper: MDXLayout } = props.components || {}
24 | return MDXLayout ? (
25 |
26 | <_createMdxContent {...props} />
27 |
28 | ) : (
29 | _createMdxContent(props)
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/fixtures/named/input.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Hello frontmatter
3 | index: 1
4 | nested:
5 | data:
6 | structure:
7 | including:
8 | numbers: 42
9 | booleans: true
10 | null:
11 | arrays:
12 | - of
13 | - items
14 | ---
15 |
--------------------------------------------------------------------------------
/fixtures/named/options.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontmatter"
3 | }
4 |
--------------------------------------------------------------------------------
/fixtures/null/expected.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | export const frontmatter = null
4 | function _createMdxContent(props) {
5 | return <>>
6 | }
7 | export default function MDXContent(props = {}) {
8 | const { wrapper: MDXLayout } = props.components || {}
9 | return MDXLayout ? (
10 |
11 | <_createMdxContent {...props} />
12 |
13 | ) : (
14 | _createMdxContent(props)
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/fixtures/null/input.md:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 |
--------------------------------------------------------------------------------
/fixtures/toml-with-content/expected.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | export const frontmatter = {
4 | __proto__: null,
5 | title: 'Hello TOML'
6 | }
7 | function _createMdxContent(props) {
8 | const _components = {
9 | h1: 'h1',
10 | p: 'p',
11 | ...props.components
12 | }
13 | return (
14 | <>
15 | <_components.h1>{'Hello, World'}
16 | {'\n'}
17 | <_components.p>{'Some content'}
18 | >
19 | )
20 | }
21 | export default function MDXContent(props = {}) {
22 | const { wrapper: MDXLayout } = props.components || {}
23 | return MDXLayout ? (
24 |
25 | <_createMdxContent {...props} />
26 |
27 | ) : (
28 | _createMdxContent(props)
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/fixtures/toml-with-content/input.md:
--------------------------------------------------------------------------------
1 | +++
2 | title = "Hello TOML"
3 | +++
4 |
5 | # Hello, World
6 |
7 | Some content
8 |
--------------------------------------------------------------------------------
/fixtures/toml/expected.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | export const frontmatter = {
4 | __proto__: null,
5 | title: 'Hello TOML'
6 | }
7 | function _createMdxContent(props) {
8 | return <>>
9 | }
10 | export default function MDXContent(props = {}) {
11 | const { wrapper: MDXLayout } = props.components || {}
12 | return MDXLayout ? (
13 |
14 | <_createMdxContent {...props} />
15 |
16 | ) : (
17 | _createMdxContent(props)
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/fixtures/toml/input.md:
--------------------------------------------------------------------------------
1 | +++
2 | title = "Hello TOML"
3 | +++
4 |
--------------------------------------------------------------------------------
/fixtures/undefined-named/expected.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | export const frontmatter = undefined
4 | function _createMdxContent(props) {
5 | return <>>
6 | }
7 | export default function MDXContent(props = {}) {
8 | const { wrapper: MDXLayout } = props.components || {}
9 | return MDXLayout ? (
10 |
11 | <_createMdxContent {...props} />
12 |
13 | ) : (
14 | _createMdxContent(props)
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/fixtures/undefined-named/input.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remcohaszing/remark-mdx-frontmatter/294ea9eb3ff3f193af2c55c723ab97aff24ecb04/fixtures/undefined-named/input.md
--------------------------------------------------------------------------------
/fixtures/undefined-named/options.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontmatter"
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "remark-mdx-frontmatter",
3 | "version": "5.1.0",
4 | "description": "A remark plugin for converting frontmatter metadata into MDX exports",
5 | "author": "Remco Haszing ",
6 | "license": "MIT",
7 | "type": "module",
8 | "exports": "./dist/remark-mdx-frontmatter.js",
9 | "main": "./dist/remark-mdx-frontmatter.js",
10 | "repository": "remcohaszing/remark-mdx-frontmatter",
11 | "bugs": "https://github.com/remcohaszing/remark-mdx-frontmatter/issues",
12 | "homepage": "https://github.com/remcohaszing/remark-mdx-frontmatter#readme",
13 | "funding": "https://github.com/sponsors/remcohaszing",
14 | "keywords": [
15 | "frontmatter",
16 | "markdown",
17 | "markdown-frontmatter",
18 | "mdast",
19 | "mdx",
20 | "remark",
21 | "remark-plugin",
22 | "toml",
23 | "unified",
24 | "yaml"
25 | ],
26 | "files": [
27 | "dist",
28 | "src",
29 | "!test*"
30 | ],
31 | "scripts": {
32 | "prepack": "tsc --build",
33 | "pretest": "tsc --build",
34 | "test": "c8 node --test"
35 | },
36 | "dependencies": {
37 | "@types/mdast": "^4.0.0",
38 | "estree-util-value-to-estree": "^3.0.0",
39 | "toml": "^3.0.0",
40 | "unified": "^11.0.0",
41 | "unist-util-mdx-define": "^1.0.0",
42 | "yaml": "^2.0.0"
43 | },
44 | "devDependencies": {
45 | "@mdx-js/mdx": "^3.0.0",
46 | "@remcohaszing/eslint": "^11.0.0",
47 | "c8": "^10.0.0",
48 | "mdast-util-mdx": "^3.0.0",
49 | "prettier": "^3.0.0",
50 | "remark-cli": "^12.0.0",
51 | "remark-frontmatter": "^5.0.0",
52 | "remark-preset-remcohaszing": "^3.0.0",
53 | "snapshot-fixtures": "^1.0.0",
54 | "typescript": "^5.0.0"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/remark-mdx-frontmatter.test.ts:
--------------------------------------------------------------------------------
1 | import { compile } from '@mdx-js/mdx'
2 | import remarkFrontmatter from 'remark-frontmatter'
3 | import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
4 | import { testFixturesDirectory } from 'snapshot-fixtures'
5 |
6 | testFixturesDirectory({
7 | directory: new URL('../fixtures', import.meta.url),
8 | prettier: true,
9 | write: true,
10 | tests: {
11 | 'expected.jsx'(file, options) {
12 | return compile(file, {
13 | remarkPlugins: [
14 | [remarkFrontmatter, ['yaml', 'toml']],
15 | [remarkMdxFrontmatter, options]
16 | ],
17 | jsx: true
18 | })
19 | }
20 | }
21 | })
22 |
--------------------------------------------------------------------------------
/src/remark-mdx-frontmatter.ts:
--------------------------------------------------------------------------------
1 | import { valueToEstree } from 'estree-util-value-to-estree'
2 | import { type Literal, type Root } from 'mdast'
3 | import { parse as parseToml } from 'toml'
4 | import { type Plugin } from 'unified'
5 | import { define } from 'unist-util-mdx-define'
6 | import { parse as parseYaml } from 'yaml'
7 |
8 | type FrontmatterParsers = Record unknown>
9 |
10 | export interface RemarkMdxFrontmatterOptions extends define.Options {
11 | /**
12 | * If specified, the YAML data is exported using this name. Otherwise, each
13 | * object key will be used as an export name.
14 | */
15 | name?: string
16 |
17 | /**
18 | * A mapping of node types to parsers.
19 | *
20 | * Each key represents a frontmatter node type. The value is a function that accepts the
21 | * frontmatter data as a string, and returns the parsed data.
22 | *
23 | * By default `yaml` nodes will be parsed using [`yaml`](https://github.com/eemeli/yaml) and
24 | * `toml` nodes using [`toml`](https://github.com/BinaryMuse/toml-node).
25 | */
26 | parsers?: FrontmatterParsers
27 | }
28 |
29 | /**
30 | * A remark plugin to expose frontmatter data as named exports.
31 | *
32 | * @param options Optional options to configure the output.
33 | * @returns A unified transformer.
34 | */
35 | const remarkMdxFrontmatter: Plugin<[RemarkMdxFrontmatterOptions?], Root> = ({
36 | name = 'frontmatter',
37 | parsers,
38 | ...options
39 | } = {}) => {
40 | const allParsers: FrontmatterParsers = {
41 | yaml: parseYaml,
42 | toml: parseToml,
43 | ...parsers
44 | }
45 |
46 | return (ast, file) => {
47 | let data: unknown
48 | const node = ast.children.find((child) => Object.hasOwn(allParsers, child.type))
49 |
50 | if (node) {
51 | const parser = allParsers[node.type]
52 |
53 | const { value } = node as Literal
54 | data = parser(value)
55 | }
56 |
57 | define(ast, file, { [name]: valueToEstree(data, { preserveReferences: true }) }, options)
58 | }
59 | }
60 |
61 | export default remarkMdxFrontmatter
62 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "declaration": true,
5 | "declarationMap": true,
6 | "module": "node16",
7 | "noImplicitAny": true,
8 | "outDir": "dist",
9 | "rootDir": "src",
10 | "skipLibCheck": true,
11 | "sourceMap": true,
12 | "strict": true,
13 | "target": "es2022",
14 | "lib": ["es2022"]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------