├── .prettierignore ├── .npmrc ├── .gitignore ├── .editorconfig ├── index.js ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── tsconfig.json ├── test-types.d.ts ├── license ├── package.json ├── lib └── index.js ├── readme.md └── test.js /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | .DS_Store 4 | index.d.ts 5 | test.d.ts 6 | *.log 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./lib/index.js').Options} Options 3 | * @typedef {import('./lib/index.js').Matter} Matter 4 | * @typedef {import('./lib/index.js').Info} Info 5 | */ 6 | 7 | export {frontmatterFromMarkdown, frontmatterToMarkdown} from './lib/index.js' 8 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | name: bb 2 | on: 3 | issues: 4 | types: [opened, reopened, edited, closed, labeled, unlabeled] 5 | pull_request_target: 6 | types: [opened, reopened, edited, closed, labeled, unlabeled] 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: unifiedjs/beep-boop-beta@main 12 | with: 13 | repo-token: ${{secrets.GITHUB_TOKEN}} 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "exactOptionalPropertyTypes": true, 8 | "lib": ["es2022"], 9 | "module": "node16", 10 | "strict": true, 11 | "target": "es2022" 12 | }, 13 | "exclude": ["coverage/", "node_modules/"], 14 | "include": ["**/*.js", "test-types.d.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /test-types.d.ts: -------------------------------------------------------------------------------- 1 | import type {Literal} from 'mdast' 2 | 3 | interface Toml extends Literal { 4 | type: 'toml' 5 | } 6 | 7 | interface Json extends Literal { 8 | type: 'json' 9 | } 10 | 11 | interface Custom extends Literal { 12 | type: 'custom' 13 | } 14 | 15 | declare module 'mdast' { 16 | interface RootContentMap { 17 | custom: Custom 18 | json: Json 19 | toml: Toml 20 | } 21 | 22 | interface FrontmatterContentMap { 23 | custom: Custom 24 | json: Json 25 | toml: Toml 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | main: 7 | name: ${{matrix.node}} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v3 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/gallium 21 | - node 22 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2020 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mdast-util-frontmatter", 3 | "version": "2.0.1", 4 | "description": "mdast extension to parse and serialize frontmatter (YAML, TOML, etc)", 5 | "license": "MIT", 6 | "keywords": [ 7 | "unist", 8 | "mdast", 9 | "mdast-util", 10 | "util", 11 | "utility", 12 | "markdown", 13 | "markup", 14 | "frontmatter", 15 | "yaml", 16 | "toml", 17 | "gfm" 18 | ], 19 | "repository": "syntax-tree/mdast-util-frontmatter", 20 | "bugs": "https://github.com/syntax-tree/mdast-util-frontmatter/issues", 21 | "funding": { 22 | "type": "opencollective", 23 | "url": "https://opencollective.com/unified" 24 | }, 25 | "author": "Titus Wormer (https://wooorm.com)", 26 | "contributors": [ 27 | "Titus Wormer (https://wooorm.com)" 28 | ], 29 | "sideEffects": false, 30 | "type": "module", 31 | "exports": "./index.js", 32 | "files": [ 33 | "lib/", 34 | "index.d.ts", 35 | "index.js" 36 | ], 37 | "dependencies": { 38 | "@types/mdast": "^4.0.0", 39 | "devlop": "^1.0.0", 40 | "escape-string-regexp": "^5.0.0", 41 | "mdast-util-from-markdown": "^2.0.0", 42 | "mdast-util-to-markdown": "^2.0.0", 43 | "micromark-extension-frontmatter": "^2.0.0" 44 | }, 45 | "devDependencies": { 46 | "@types/node": "^20.0.0", 47 | "c8": "^8.0.0", 48 | "prettier": "^3.0.0", 49 | "remark-cli": "^11.0.0", 50 | "remark-preset-wooorm": "^9.0.0", 51 | "type-coverage": "^2.0.0", 52 | "typescript": "^5.0.0", 53 | "unist-util-remove-position": "^5.0.0", 54 | "xo": "^0.56.0" 55 | }, 56 | "scripts": { 57 | "prepack": "npm run build && npm run format", 58 | "build": "tsc --build --clean && tsc --build && type-coverage", 59 | "format": "remark . -qfo && prettier . -w --log-level warn && xo --fix", 60 | "test-api-prod": "node --conditions production test.js", 61 | "test-api-dev": "node --conditions development test.js", 62 | "test-api": "npm run test-api-dev && npm run test-api-prod", 63 | "test-coverage": "c8 --100 --reporter lcov npm run test-api", 64 | "test": "npm run build && npm run format && npm run test-coverage" 65 | }, 66 | "prettier": { 67 | "bracketSpacing": false, 68 | "semi": false, 69 | "singleQuote": true, 70 | "tabWidth": 2, 71 | "trailingComma": "none", 72 | "useTabs": false 73 | }, 74 | "remarkConfig": { 75 | "plugins": [ 76 | "remark-preset-wooorm" 77 | ] 78 | }, 79 | "typeCoverage": { 80 | "atLeast": 100, 81 | "detail": true, 82 | "ignoreCatch": true, 83 | "strict": true 84 | }, 85 | "xo": { 86 | "overrides": [ 87 | { 88 | "files": [ 89 | "**/*.ts" 90 | ], 91 | "rules": { 92 | "@typescript-eslint/consistent-type-definitions": "off" 93 | } 94 | } 95 | ], 96 | "prettier": true, 97 | "rules": { 98 | "unicorn/prefer-at": "off", 99 | "unicorn/prefer-string-replace-all": "off" 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('mdast').Literal} Literal 3 | * 4 | * @typedef {import('mdast-util-from-markdown').CompileContext} CompileContext 5 | * @typedef {import('mdast-util-from-markdown').Extension} FromMarkdownExtension 6 | * @typedef {import('mdast-util-from-markdown').Handle} FromMarkdownHandle 7 | * @typedef {import('mdast-util-to-markdown').Options} ToMarkdownExtension 8 | * 9 | * @typedef {import('micromark-extension-frontmatter').Info} Info 10 | * @typedef {import('micromark-extension-frontmatter').Matter} Matter 11 | * @typedef {import('micromark-extension-frontmatter').Options} Options 12 | */ 13 | 14 | import {ok as assert} from 'devlop' 15 | import {toMatters} from 'micromark-extension-frontmatter' 16 | import escapeStringRegexp from 'escape-string-regexp' 17 | 18 | /** 19 | * Create an extension for `mdast-util-from-markdown`. 20 | * 21 | * @param {Options | null | undefined} [options] 22 | * Configuration (optional). 23 | * @returns {FromMarkdownExtension} 24 | * Extension for `mdast-util-from-markdown`. 25 | */ 26 | export function frontmatterFromMarkdown(options) { 27 | const matters = toMatters(options) 28 | /** @type {FromMarkdownExtension['enter']} */ 29 | const enter = {} 30 | /** @type {FromMarkdownExtension['exit']} */ 31 | const exit = {} 32 | let index = -1 33 | 34 | while (++index < matters.length) { 35 | const matter = matters[index] 36 | enter[matter.type] = opener(matter) 37 | exit[matter.type] = close 38 | exit[matter.type + 'Value'] = value 39 | } 40 | 41 | return {enter, exit} 42 | } 43 | 44 | /** 45 | * @param {Matter} matter 46 | * @returns {FromMarkdownHandle} enter 47 | */ 48 | function opener(matter) { 49 | return open 50 | 51 | /** 52 | * @this {CompileContext} 53 | * @type {FromMarkdownHandle} 54 | */ 55 | function open(token) { 56 | // @ts-expect-error: custom. 57 | this.enter({type: matter.type, value: ''}, token) 58 | this.buffer() 59 | } 60 | } 61 | 62 | /** 63 | * @this {CompileContext} 64 | * @type {FromMarkdownHandle} 65 | */ 66 | function close(token) { 67 | const data = this.resume() 68 | const node = this.stack[this.stack.length - 1] 69 | assert('value' in node) 70 | this.exit(token) 71 | // Remove the initial and final eol. 72 | node.value = data.replace(/^(\r?\n|\r)|(\r?\n|\r)$/g, '') 73 | } 74 | 75 | /** 76 | * @this {CompileContext} 77 | * @type {FromMarkdownHandle} 78 | */ 79 | function value(token) { 80 | this.config.enter.data.call(this, token) 81 | this.config.exit.data.call(this, token) 82 | } 83 | 84 | /** 85 | * Create an extension for `mdast-util-to-markdown`. 86 | * 87 | * @param {Options | null | undefined} [options] 88 | * Configuration (optional). 89 | * @returns {ToMarkdownExtension} 90 | * Extension for `mdast-util-to-markdown`. 91 | */ 92 | export function frontmatterToMarkdown(options) { 93 | /** @type {ToMarkdownExtension['unsafe']} */ 94 | const unsafe = [] 95 | /** @type {ToMarkdownExtension['handlers']} */ 96 | const handlers = {} 97 | const matters = toMatters(options) 98 | let index = -1 99 | 100 | while (++index < matters.length) { 101 | const matter = matters[index] 102 | 103 | // @ts-expect-error: this can add custom frontmatter nodes. 104 | // Typing those is the responsibility of the end user. 105 | handlers[matter.type] = handler(matter) 106 | 107 | const open = fence(matter, 'open') 108 | 109 | unsafe.push({ 110 | atBreak: true, 111 | character: open.charAt(0), 112 | after: escapeStringRegexp(open.charAt(1)) 113 | }) 114 | } 115 | 116 | return {unsafe, handlers} 117 | } 118 | 119 | /** 120 | * Create a handle that can serialize a frontmatter node as markdown. 121 | * 122 | * @param {Matter} matter 123 | * Structure. 124 | * @returns {(node: Literal) => string} enter 125 | * Handler. 126 | */ 127 | function handler(matter) { 128 | const open = fence(matter, 'open') 129 | const close = fence(matter, 'close') 130 | 131 | return handle 132 | 133 | /** 134 | * Serialize a frontmatter node as markdown. 135 | * 136 | * @param {Literal} node 137 | * Node to serialize. 138 | * @returns {string} 139 | * Serialized node. 140 | */ 141 | function handle(node) { 142 | return open + (node.value ? '\n' + node.value : '') + '\n' + close 143 | } 144 | } 145 | 146 | /** 147 | * Get an `open` or `close` fence. 148 | * 149 | * @param {Matter} matter 150 | * Structure. 151 | * @param {'close' | 'open'} prop 152 | * Field to get. 153 | * @returns {string} 154 | * Fence. 155 | */ 156 | function fence(matter, prop) { 157 | return matter.marker 158 | ? pick(matter.marker, prop).repeat(3) 159 | : // @ts-expect-error: They’re mutually exclusive. 160 | pick(matter.fence, prop) 161 | } 162 | 163 | /** 164 | * Take `open` or `close` fields when schema is an info object, or use the 165 | * given value when it is a string. 166 | * 167 | * @param {Info | string} schema 168 | * Info object or value. 169 | * @param {'close' | 'open'} prop 170 | * Field to get. 171 | * @returns {string} 172 | * Thing to use for the opening or closing. 173 | */ 174 | function pick(schema, prop) { 175 | return typeof schema === 'string' ? schema : schema[prop] 176 | } 177 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # mdast-util-frontmatter 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![Downloads][downloads-badge]][downloads] 6 | [![Size][size-badge]][size] 7 | [![Sponsors][sponsors-badge]][collective] 8 | [![Backers][backers-badge]][collective] 9 | [![Chat][chat-badge]][chat] 10 | 11 | [mdast][] extensions to parse and serialize frontmatter (YAML, TOML, and more). 12 | 13 | ## Contents 14 | 15 | * [What is this?](#what-is-this) 16 | * [When to use this](#when-to-use-this) 17 | * [Install](#install) 18 | * [Use](#use) 19 | * [API](#api) 20 | * [`frontmatterFromMarkdown(options?)`](#frontmatterfrommarkdownoptions) 21 | * [`frontmatterToMarkdown(options?)`](#frontmattertomarkdownoptions) 22 | * [`Info`](#info) 23 | * [`Matter`](#matter) 24 | * [`Options`](#options) 25 | * [Syntax](#syntax) 26 | * [Syntax tree](#syntax-tree) 27 | * [Nodes](#nodes) 28 | * [Content model](#content-model) 29 | * [Types](#types) 30 | * [Compatibility](#compatibility) 31 | * [Related](#related) 32 | * [Contribute](#contribute) 33 | * [License](#license) 34 | 35 | ## What is this? 36 | 37 | This package contains two extensions that add support for frontmatter syntax 38 | as often used in markdown to [mdast][]. 39 | These extensions plug into 40 | [`mdast-util-from-markdown`][mdast-util-from-markdown] (to support parsing 41 | frontmatter in markdown into a syntax tree) and 42 | [`mdast-util-to-markdown`][mdast-util-to-markdown] (to support serializing 43 | frontmatter in syntax trees to markdown). 44 | 45 | Frontmatter is a metadata format in front of the content. 46 | It’s typically written in YAML and is often used with markdown. 47 | Frontmatter does not work everywhere so it makes markdown less portable. 48 | 49 | These extensions follow how GitHub handles frontmatter. 50 | GitHub only supports YAML frontmatter, but these extensions also support 51 | different flavors (such as TOML). 52 | 53 | ## When to use this 54 | 55 | You can use these extensions when you are working with 56 | `mdast-util-from-markdown` and `mdast-util-to-markdown` already. 57 | 58 | When working with `mdast-util-from-markdown`, you must combine this package 59 | with [`micromark-extension-frontmatter`][micromark-extension-frontmatter]. 60 | 61 | When you don’t need a syntax tree, you can use [`micromark`][micromark] 62 | directly with 63 | [`micromark-extension-frontmatter`][micromark-extension-frontmatter]. 64 | 65 | All these packages are used [`remark-frontmatter`][remark-frontmatter], which 66 | focusses on making it easier to transform content by abstracting these 67 | internals away. 68 | 69 | ## Install 70 | 71 | This package is [ESM only][esm]. 72 | In Node.js (version 16+), install with [npm][]: 73 | 74 | ```sh 75 | npm install mdast-util-frontmatter 76 | ``` 77 | 78 | In Deno with [`esm.sh`][esmsh]: 79 | 80 | ```js 81 | import {frontmatterFromMarkdown, frontmatterToMarkdown} from 'https://esm.sh/mdast-util-frontmatter@2' 82 | ``` 83 | 84 | In browsers with [`esm.sh`][esmsh]: 85 | 86 | ```html 87 | 90 | ``` 91 | 92 | ## Use 93 | 94 | Say our document `example.md` contains: 95 | 96 | ```markdown 97 | +++ 98 | title = "New Website" 99 | +++ 100 | 101 | # Other markdown 102 | ``` 103 | 104 | …and our module `example.js` looks as follows: 105 | 106 | ```js 107 | import fs from 'node:fs/promises' 108 | import {frontmatter} from 'micromark-extension-frontmatter' 109 | import {fromMarkdown} from 'mdast-util-from-markdown' 110 | import {frontmatterFromMarkdown, frontmatterToMarkdown} from 'mdast-util-frontmatter' 111 | import {toMarkdown} from 'mdast-util-to-markdown' 112 | 113 | const doc = await fs.readFile('example.md') 114 | 115 | const tree = fromMarkdown(doc, { 116 | extensions: [frontmatter(['yaml', 'toml'])], 117 | mdastExtensions: [frontmatterFromMarkdown(['yaml', 'toml'])] 118 | }) 119 | 120 | console.log(tree) 121 | 122 | const out = toMarkdown(tree, {extensions: [frontmatterToMarkdown(['yaml', 'toml'])]}) 123 | 124 | console.log(out) 125 | ``` 126 | 127 | …now running `node example.js` yields (positional info removed for brevity): 128 | 129 | ```js 130 | { 131 | type: 'root', 132 | children: [ 133 | {type: 'toml', value: 'title = "New Website"'}, 134 | { 135 | type: 'heading', 136 | depth: 1, 137 | children: [{type: 'text', value: 'Other markdown'}] 138 | } 139 | ] 140 | } 141 | ``` 142 | 143 | ```markdown 144 | +++ 145 | title = "New Website" 146 | +++ 147 | 148 | # Other markdown 149 | ``` 150 | 151 | ## API 152 | 153 | This package exports the identifiers 154 | [`frontmatterFromMarkdown`][api-frontmatter-from-markdown] and 155 | [`frontmatterToMarkdown`][api-frontmatter-to-markdown]. 156 | There is no default export. 157 | 158 | ### `frontmatterFromMarkdown(options?)` 159 | 160 | Create an extension for 161 | [`mdast-util-from-markdown`][mdast-util-from-markdown]. 162 | 163 | ###### Parameters 164 | 165 | * `options` ([`Options`][api-options], optional) 166 | — configuration 167 | 168 | ###### Returns 169 | 170 | Extension for `mdast-util-from-markdown` 171 | ([`FromMarkdownExtension`][from-markdown-extension]). 172 | 173 | ### `frontmatterToMarkdown(options?)` 174 | 175 | Create an extension for 176 | [`mdast-util-to-markdown`][mdast-util-to-markdown]. 177 | 178 | ###### Parameters 179 | 180 | * `options` ([`Options`][api-options], optional) 181 | — configuration 182 | 183 | ###### Returns 184 | 185 | Extension for `mdast-util-to-markdown` 186 | ([`ToMarkdownExtension`][to-markdown-extension]). 187 | 188 | ### `Info` 189 | 190 | Structure of marker or fence (TypeScript type). 191 | 192 | Same as [`Info` from `micromark-extension-frontmatter`][micromark-info]. 193 | 194 | ### `Matter` 195 | 196 | Structure of matter (TypeScript type). 197 | 198 | Same as [`Matter` from `micromark-extension-frontmatter`][micromark-matter]. 199 | 200 | ### `Options` 201 | 202 | Configuration (TypeScript type). 203 | 204 | Same as [`Options` from `micromark-extension-frontmatter`][micromark-options]. 205 | 206 | ## Syntax 207 | 208 | See [Syntax in `micromark-extension-frontmatter`][syntax]. 209 | 210 | ## Syntax tree 211 | 212 | The following interfaces are added to **[mdast][]** by this utility. 213 | 214 | ### Nodes 215 | 216 | > 👉 **Note**: other nodes are not enabled by default, but when passing options 217 | > to enable them, they work the same as YAML. 218 | 219 | #### `YAML` 220 | 221 | ```idl 222 | interface YAML <: Literal { 223 | type: "yaml" 224 | } 225 | ``` 226 | 227 | **YAML** (**[Literal][dfn-literal]**) represents a collection of metadata for 228 | the document in the YAML data serialization language. 229 | 230 | **YAML** can be used where **[frontmatter][dfn-frontmatter-content]** content 231 | is expected. 232 | Its content is represented by its `value` field. 233 | 234 | For example, the following markdown: 235 | 236 | ```markdown 237 | --- 238 | foo: bar 239 | --- 240 | ``` 241 | 242 | Yields: 243 | 244 | ```js 245 | {type: 'yaml', value: 'foo: bar'} 246 | ``` 247 | 248 | ### Content model 249 | 250 | #### `FrontmatterContent` 251 | 252 | ```idl 253 | type FrontmatterContent = YAML 254 | ``` 255 | 256 | **Frontmatter** content represent out-of-band information about the document. 257 | 258 | If frontmatter is present, it must be limited to one node in the 259 | *[tree][term-tree]*, and can only exist as a *[head][term-head]*. 260 | 261 | #### `FlowContent` (frontmatter) 262 | 263 | ```idl 264 | type FlowContentFrontmatter = FrontmatterContent | FlowContent 265 | ``` 266 | 267 | ## Types 268 | 269 | This package is fully typed with [TypeScript][]. 270 | It exports the additional types [`Info`][api-info], [`Matter`][api-matter], 271 | and [`Options`][api-options]. 272 | 273 | The YAML node type is supported in `@types/mdast` by default. 274 | To add other node types, register them by adding them to 275 | `FrontmatterContentMap`: 276 | 277 | ```ts 278 | import type {Literal} from 'mdast' 279 | 280 | interface Toml extends Literal { 281 | type: 'toml' 282 | } 283 | 284 | declare module 'mdast' { 285 | interface FrontmatterContentMap { 286 | // Allow using TOML nodes defined by `mdast-util-frontmatter`. 287 | toml: Toml 288 | } 289 | } 290 | ``` 291 | 292 | ## Compatibility 293 | 294 | Projects maintained by the unified collective are compatible with maintained 295 | versions of Node.js. 296 | 297 | When we cut a new major release, we drop support for unmaintained versions of 298 | Node. 299 | This means we try to keep the current release line, 300 | `mdast-util-frontmatter@^2`, compatible with Node.js 16. 301 | 302 | This utility works with `mdast-util-from-markdown` version 2+ and 303 | `mdast-util-to-markdown` version 2+. 304 | 305 | ## Related 306 | 307 | * [`remark-frontmatter`][remark-frontmatter] 308 | — remark plugin to support frontmatter 309 | * [`micromark-extension-frontmatter`][micromark-extension-frontmatter] 310 | — micromark extension to parse frontmatter 311 | 312 | ## Contribute 313 | 314 | See [`contributing.md`][contributing] in [`syntax-tree/.github`][health] for 315 | ways to get started. 316 | See [`support.md`][support] for ways to get help. 317 | 318 | This project has a [code of conduct][coc]. 319 | By interacting with this repository, organization, or community you agree to 320 | abide by its terms. 321 | 322 | ## License 323 | 324 | [MIT][license] © [Titus Wormer][author] 325 | 326 | 327 | 328 | [build-badge]: https://github.com/syntax-tree/mdast-util-frontmatter/workflows/main/badge.svg 329 | 330 | [build]: https://github.com/syntax-tree/mdast-util-frontmatter/actions 331 | 332 | [coverage-badge]: https://img.shields.io/codecov/c/github/syntax-tree/mdast-util-frontmatter.svg 333 | 334 | [coverage]: https://codecov.io/github/syntax-tree/mdast-util-frontmatter 335 | 336 | [downloads-badge]: https://img.shields.io/npm/dm/mdast-util-frontmatter.svg 337 | 338 | [downloads]: https://www.npmjs.com/package/mdast-util-frontmatter 339 | 340 | [size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=mdast-util-frontmatter 341 | 342 | [size]: https://bundlejs.com/?q=mdast-util-frontmatter 343 | 344 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 345 | 346 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 347 | 348 | [collective]: https://opencollective.com/unified 349 | 350 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 351 | 352 | [chat]: https://github.com/syntax-tree/unist/discussions 353 | 354 | [npm]: https://docs.npmjs.com/cli/install 355 | 356 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 357 | 358 | [esmsh]: https://esm.sh 359 | 360 | [typescript]: https://www.typescriptlang.org 361 | 362 | [license]: license 363 | 364 | [author]: https://wooorm.com 365 | 366 | [health]: https://github.com/syntax-tree/.github 367 | 368 | [contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md 369 | 370 | [support]: https://github.com/syntax-tree/.github/blob/main/support.md 371 | 372 | [coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md 373 | 374 | [mdast]: https://github.com/syntax-tree/mdast 375 | 376 | [remark-frontmatter]: https://github.com/remarkjs/remark-frontmatter 377 | 378 | [mdast-util-from-markdown]: https://github.com/syntax-tree/mdast-util-from-markdown 379 | 380 | [mdast-util-to-markdown]: https://github.com/syntax-tree/mdast-util-to-markdown 381 | 382 | [micromark]: https://github.com/micromark/micromark 383 | 384 | [micromark-extension-frontmatter]: https://github.com/micromark/micromark-extension-frontmatter 385 | 386 | [micromark-info]: https://github.com/micromark/micromark-extension-frontmatter#info 387 | 388 | [micromark-matter]: https://github.com/micromark/micromark-extension-frontmatter#matter 389 | 390 | [micromark-options]: https://github.com/micromark/micromark-extension-frontmatter#options 391 | 392 | [syntax]: https://github.com/micromark/micromark-extension-frontmatter#syntax 393 | 394 | [dfn-literal]: https://github.com/syntax-tree/mdast#literal 395 | 396 | [term-tree]: https://github.com/syntax-tree/unist#tree 397 | 398 | [term-head]: https://github.com/syntax-tree/unist#head 399 | 400 | [from-markdown-extension]: https://github.com/syntax-tree/mdast-util-from-markdown#extension 401 | 402 | [to-markdown-extension]: https://github.com/syntax-tree/mdast-util-to-markdown#options 403 | 404 | [dfn-frontmatter-content]: #frontmattercontent 405 | 406 | [api-frontmatter-from-markdown]: #frontmatterfrommarkdownoptions 407 | 408 | [api-frontmatter-to-markdown]: #frontmattertomarkdownoptions 409 | 410 | [api-info]: #info 411 | 412 | [api-matter]: #matter 413 | 414 | [api-options]: #options 415 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./test-types.js')} DoNotTouchThisIncludesCustomNodesInTree 3 | */ 4 | 5 | import assert from 'node:assert/strict' 6 | import test from 'node:test' 7 | import {frontmatter} from 'micromark-extension-frontmatter' 8 | import {fromMarkdown} from 'mdast-util-from-markdown' 9 | import { 10 | frontmatterFromMarkdown, 11 | frontmatterToMarkdown 12 | } from 'mdast-util-frontmatter' 13 | import {toMarkdown} from 'mdast-util-to-markdown' 14 | import {removePosition} from 'unist-util-remove-position' 15 | 16 | const custom = {type: 'custom', marker: {open: '<', close: '>'}} 17 | const json = {type: 'json', fence: {open: '{', close: '}'}} 18 | const yamlAnywhere = {type: 'yaml', marker: '-', anywhere: true} 19 | 20 | test('core', async function (t) { 21 | await t.test('should expose the public api', async function () { 22 | assert.deepEqual( 23 | Object.keys(await import('mdast-util-frontmatter')).sort(), 24 | ['frontmatterFromMarkdown', 'frontmatterToMarkdown'] 25 | ) 26 | }) 27 | }) 28 | 29 | test('frontmatterFromMarkdown', async function (t) { 30 | await t.test( 31 | 'should not support a single yaml fence (thematic break)', 32 | async function () { 33 | const tree = fromMarkdown('---', { 34 | extensions: [frontmatter()], 35 | mdastExtensions: [frontmatterFromMarkdown()] 36 | }) 37 | 38 | removePosition(tree, {force: true}) 39 | 40 | assert.deepEqual(tree, { 41 | type: 'root', 42 | children: [{type: 'thematicBreak'}] 43 | }) 44 | } 45 | ) 46 | 47 | await t.test('should parse empty yaml', async function () { 48 | const tree = fromMarkdown('---\n---', { 49 | extensions: [frontmatter()], 50 | mdastExtensions: [frontmatterFromMarkdown()] 51 | }) 52 | 53 | removePosition(tree, {force: true}) 54 | 55 | assert.deepEqual(tree, { 56 | type: 'root', 57 | children: [{type: 'yaml', value: ''}] 58 | }) 59 | }) 60 | 61 | await t.test( 62 | 'should not support a prefix (indent) before a yaml opening fence', 63 | async function () { 64 | const tree = fromMarkdown(' ---\n---', { 65 | extensions: [frontmatter()], 66 | mdastExtensions: [frontmatterFromMarkdown()] 67 | }) 68 | 69 | removePosition(tree, {force: true}) 70 | 71 | assert.deepEqual(tree, { 72 | type: 'root', 73 | children: [{type: 'thematicBreak'}, {type: 'thematicBreak'}] 74 | }) 75 | } 76 | ) 77 | 78 | await t.test( 79 | 'should not support a prefix (indent) before a yaml closing fence', 80 | async function () { 81 | const tree = fromMarkdown('---\n ---', { 82 | extensions: [frontmatter()], 83 | mdastExtensions: [frontmatterFromMarkdown()] 84 | }) 85 | 86 | removePosition(tree, {force: true}) 87 | 88 | assert.deepEqual(tree, { 89 | type: 'root', 90 | children: [{type: 'thematicBreak'}, {type: 'thematicBreak'}] 91 | }) 92 | } 93 | ) 94 | 95 | await t.test( 96 | 'should parse an arbitrary suffix after the opening and closing fence of yaml', 97 | async function () { 98 | const tree = fromMarkdown('--- \n---\t ', { 99 | extensions: [frontmatter()], 100 | mdastExtensions: [frontmatterFromMarkdown()] 101 | }) 102 | 103 | removePosition(tree, {force: true}) 104 | 105 | assert.deepEqual(tree, { 106 | type: 'root', 107 | children: [{type: 'yaml', value: ''}] 108 | }) 109 | } 110 | ) 111 | 112 | await t.test( 113 | 'should not support other characters after the suffix on the opening fence of yaml', 114 | async function () { 115 | const tree = fromMarkdown('--- --\n---', { 116 | extensions: [frontmatter()], 117 | mdastExtensions: [frontmatterFromMarkdown()] 118 | }) 119 | 120 | removePosition(tree, {force: true}) 121 | 122 | assert.deepEqual(tree, { 123 | type: 'root', 124 | children: [{type: 'thematicBreak'}, {type: 'thematicBreak'}] 125 | }) 126 | } 127 | ) 128 | 129 | await t.test( 130 | 'should not support other characters after the suffix on the closing fence of yaml', 131 | async function () { 132 | const tree = fromMarkdown('---\n--- x', { 133 | extensions: [frontmatter()], 134 | mdastExtensions: [frontmatterFromMarkdown()] 135 | }) 136 | 137 | removePosition(tree, {force: true}) 138 | 139 | assert.deepEqual(tree, { 140 | type: 'root', 141 | children: [ 142 | {type: 'thematicBreak'}, 143 | {type: 'paragraph', children: [{type: 'text', value: '--- x'}]} 144 | ] 145 | }) 146 | } 147 | ) 148 | 149 | await t.test( 150 | 'should not support an opening yaml fence of more than 3 characters', 151 | async function () { 152 | const tree = fromMarkdown('----\n---', { 153 | extensions: [frontmatter()], 154 | mdastExtensions: [frontmatterFromMarkdown()] 155 | }) 156 | 157 | removePosition(tree, {force: true}) 158 | 159 | assert.deepEqual(tree, { 160 | type: 'root', 161 | children: [{type: 'thematicBreak'}, {type: 'thematicBreak'}] 162 | }) 163 | } 164 | ) 165 | 166 | await t.test( 167 | 'should not support a closing yaml fence of more than 3 characters', 168 | async function () { 169 | const tree = fromMarkdown('---\n----', { 170 | extensions: [frontmatter()], 171 | mdastExtensions: [frontmatterFromMarkdown()] 172 | }) 173 | 174 | removePosition(tree, {force: true}) 175 | 176 | assert.deepEqual(tree, { 177 | type: 'root', 178 | children: [{type: 'thematicBreak'}, {type: 'thematicBreak'}] 179 | }) 180 | } 181 | ) 182 | 183 | await t.test( 184 | 'should not support an opening yaml fence of less than 3 characters', 185 | async function () { 186 | const tree = fromMarkdown('--\n---', { 187 | extensions: [frontmatter()], 188 | mdastExtensions: [frontmatterFromMarkdown()] 189 | }) 190 | 191 | removePosition(tree, {force: true}) 192 | 193 | assert.deepEqual(tree, { 194 | type: 'root', 195 | children: [ 196 | {type: 'heading', depth: 2, children: [{type: 'text', value: '--'}]} 197 | ] 198 | }) 199 | } 200 | ) 201 | 202 | await t.test( 203 | 'should not support a closing yaml fence of less than 3 characters', 204 | async function () { 205 | const tree = fromMarkdown('---\n--', { 206 | extensions: [frontmatter()], 207 | mdastExtensions: [frontmatterFromMarkdown()] 208 | }) 209 | 210 | removePosition(tree, {force: true}) 211 | 212 | assert.deepEqual(tree, { 213 | type: 'root', 214 | children: [ 215 | {type: 'thematicBreak'}, 216 | {type: 'paragraph', children: [{type: 'text', value: '--'}]} 217 | ] 218 | }) 219 | } 220 | ) 221 | 222 | await t.test('should support content in yaml', async function () { 223 | const tree = fromMarkdown('---\na\nb\n---', { 224 | extensions: [frontmatter()], 225 | mdastExtensions: [frontmatterFromMarkdown()] 226 | }) 227 | 228 | removePosition(tree, {force: true}) 229 | 230 | assert.deepEqual(tree, { 231 | type: 'root', 232 | children: [{type: 'yaml', value: 'a\nb'}] 233 | }) 234 | }) 235 | 236 | await t.test('should support blank lines in yaml', async function () { 237 | const tree = fromMarkdown('---\na\n\nb\n---', { 238 | extensions: [frontmatter()], 239 | mdastExtensions: [frontmatterFromMarkdown()] 240 | }) 241 | 242 | removePosition(tree, {force: true}) 243 | 244 | assert.deepEqual(tree, { 245 | type: 'root', 246 | children: [{type: 'yaml', value: 'a\n\nb'}] 247 | }) 248 | }) 249 | 250 | await t.test('should support toml', async function () { 251 | const tree = fromMarkdown('+++\na\n\nb\n+++', { 252 | extensions: [frontmatter('toml')], 253 | mdastExtensions: [frontmatterFromMarkdown('toml')] 254 | }) 255 | 256 | removePosition(tree, {force: true}) 257 | 258 | assert.deepEqual(tree, { 259 | type: 'root', 260 | children: [{type: 'toml', value: 'a\n\nb'}] 261 | }) 262 | }) 263 | 264 | await t.test('should support a custom matter (1)', async function () { 265 | const tree = fromMarkdown('<<<\na\n\nb\n>>>', { 266 | extensions: [frontmatter(custom)], 267 | mdastExtensions: [frontmatterFromMarkdown(custom)] 268 | }) 269 | 270 | removePosition(tree, {force: true}) 271 | 272 | assert.deepEqual(tree, { 273 | type: 'root', 274 | children: [{type: 'custom', value: 'a\n\nb'}] 275 | }) 276 | }) 277 | 278 | await t.test('should support a custom matter (2)', async function () { 279 | const tree = fromMarkdown('{\na\n\nb\n}', { 280 | extensions: [frontmatter(json)], 281 | mdastExtensions: [frontmatterFromMarkdown(json)] 282 | }) 283 | 284 | removePosition(tree, {force: true}) 285 | 286 | assert.deepEqual(tree, { 287 | type: 'root', 288 | children: [{type: 'json', value: 'a\n\nb'}] 289 | }) 290 | }) 291 | 292 | await t.test( 293 | 'should not support yaml frontmatter in the middle', 294 | async function () { 295 | const tree = fromMarkdown('# Hello\n---\na\n\nb\n---\n+++', { 296 | extensions: [frontmatter()], 297 | mdastExtensions: [frontmatterFromMarkdown()] 298 | }) 299 | 300 | removePosition(tree, {force: true}) 301 | 302 | assert.deepEqual(tree, { 303 | type: 'root', 304 | children: [ 305 | { 306 | type: 'heading', 307 | depth: 1, 308 | children: [{type: 'text', value: 'Hello'}] 309 | }, 310 | {type: 'thematicBreak'}, 311 | {type: 'paragraph', children: [{type: 'text', value: 'a'}]}, 312 | {type: 'heading', depth: 2, children: [{type: 'text', value: 'b'}]}, 313 | {type: 'paragraph', children: [{type: 'text', value: '+++'}]} 314 | ] 315 | }) 316 | } 317 | ) 318 | 319 | await t.test( 320 | 'should not support custom matters in the middle', 321 | async function () { 322 | const tree = fromMarkdown('# Hello\n---\na\n\nb\n---\n+++', { 323 | extensions: [frontmatter(yamlAnywhere)], 324 | mdastExtensions: [frontmatterFromMarkdown(yamlAnywhere)] 325 | }) 326 | 327 | removePosition(tree, {force: true}) 328 | 329 | assert.deepEqual(tree, { 330 | type: 'root', 331 | children: [ 332 | { 333 | type: 'heading', 334 | depth: 1, 335 | children: [{type: 'text', value: 'Hello'}] 336 | }, 337 | {type: 'yaml', value: 'a\n\nb'}, 338 | {type: 'paragraph', children: [{type: 'text', value: '+++'}]} 339 | ] 340 | }) 341 | } 342 | ) 343 | 344 | await t.test( 345 | 'should support regexp special characters as markers', 346 | async function () { 347 | const funky = {type: 'funky', marker: '*'} 348 | const tree = fromMarkdown('***\na\n***\n\n*a', { 349 | extensions: [frontmatter(funky)], 350 | mdastExtensions: [frontmatterFromMarkdown(funky)] 351 | }) 352 | 353 | removePosition(tree, {force: true}) 354 | 355 | assert.deepEqual(tree, { 356 | type: 'root', 357 | children: [ 358 | {type: 'funky', value: 'a'}, 359 | {type: 'paragraph', children: [{type: 'text', value: '*a'}]} 360 | ] 361 | }) 362 | 363 | assert.deepEqual( 364 | toMarkdown(tree, {extensions: [frontmatterToMarkdown(funky)]}), 365 | '***\na\n***\n\n\\*a\n' 366 | ) 367 | } 368 | ) 369 | }) 370 | 371 | test('frontmatterToMarkdown', async function (t) { 372 | await t.test('should serialize empty yaml', async function () { 373 | assert.deepEqual( 374 | toMarkdown( 375 | {type: 'root', children: [{type: 'yaml', value: ''}]}, 376 | {extensions: [frontmatterToMarkdown()]} 377 | ), 378 | '---\n---\n' 379 | ) 380 | }) 381 | 382 | await t.test('should support content in yaml', async function () { 383 | assert.deepEqual( 384 | toMarkdown( 385 | {type: 'root', children: [{type: 'yaml', value: 'a\nb'}]}, 386 | {extensions: [frontmatterToMarkdown()]} 387 | ), 388 | '---\na\nb\n---\n' 389 | ) 390 | }) 391 | 392 | await t.test('should support blank lines in yaml', async function () { 393 | assert.deepEqual( 394 | toMarkdown( 395 | {type: 'root', children: [{type: 'yaml', value: 'a\n\nb'}]}, 396 | {extensions: [frontmatterToMarkdown()]} 397 | ), 398 | '---\na\n\nb\n---\n' 399 | ) 400 | }) 401 | 402 | await t.test('should support blank lines in yaml', async function () { 403 | assert.deepEqual( 404 | toMarkdown( 405 | {type: 'root', children: [{type: 'toml', value: 'a\n\nb'}]}, 406 | {extensions: [frontmatterToMarkdown('toml')]} 407 | ), 408 | '+++\na\n\nb\n+++\n' 409 | ) 410 | }) 411 | 412 | await t.test('should support a custom matter (1)', async function () { 413 | assert.deepEqual( 414 | toMarkdown( 415 | {type: 'root', children: [{type: 'custom', value: 'a\n\nb'}]}, 416 | {extensions: [frontmatterToMarkdown(custom)]} 417 | ), 418 | '<<<\na\n\nb\n>>>\n' 419 | ) 420 | }) 421 | 422 | await t.test('should support a custom matter (2)', async function () { 423 | assert.deepEqual( 424 | toMarkdown( 425 | {type: 'root', children: [{type: 'json', value: 'a\n\nb'}]}, 426 | {extensions: [frontmatterToMarkdown(json)]} 427 | ), 428 | '{\na\n\nb\n}\n' 429 | ) 430 | }) 431 | 432 | await t.test( 433 | 'should escape what would otherwise be custom matter', 434 | async function () { 435 | assert.deepEqual( 436 | toMarkdown( 437 | {type: 'root', children: [{type: 'text', value: '<<<\na\n\nb\n>>>'}]}, 438 | {extensions: [frontmatterToMarkdown(custom)]} 439 | ), 440 | '\\<<<\na\n\nb\n\\>>>\n' 441 | ) 442 | } 443 | ) 444 | }) 445 | --------------------------------------------------------------------------------