├── .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 | [![github actions](https://github.com/remcohaszing/remark-mdx-frontmatter/actions/workflows/ci.yaml/badge.svg)](https://github.com/remcohaszing/remark-mdx-frontmatter/actions/workflows/ci.yaml) 4 | [![codecov](https://codecov.io/gh/remcohaszing/remark-mdx-frontmatter/branch/main/graph/badge.svg)](https://codecov.io/gh/remcohaszing/remark-mdx-frontmatter) 5 | [![npm version](https://img.shields.io/npm/v/remark-mdx-frontmatter)](https://www.npmjs.com/package/remark-mdx-frontmatter) 6 | [![npm downloads](https://img.shields.io/npm/dm/remark-mdx-frontmatter)](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 | --------------------------------------------------------------------------------