├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── index.js ├── lib └── index.js ├── license ├── package.json ├── readme.md ├── test.js └── tsconfig.json /.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 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | runs-on: ubuntu-latest 4 | steps: 5 | - uses: unifiedjs/beep-boop-beta@main 6 | with: 7 | repo-token: ${{secrets.GITHUB_TOKEN}} 8 | name: bb 9 | on: 10 | issues: 11 | types: [closed, edited, labeled, opened, reopened, unlabeled] 12 | pull_request_target: 13 | types: [closed, edited, labeled, opened, reopened, unlabeled] 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | name: '${{matrix.node}} on ${{matrix.os}}' 4 | runs-on: ${{matrix.os}} 5 | steps: 6 | - uses: actions/checkout@v4 7 | - uses: actions/setup-node@v4 8 | with: 9 | node-version: ${{matrix.node}} 10 | - run: npm install 11 | - run: npm test 12 | - uses: codecov/codecov-action@v5 13 | strategy: 14 | matrix: 15 | node: 16 | - lts/hydrogen 17 | - node 18 | os: 19 | - ubuntu-latest 20 | - windows-latest 21 | name: main 22 | on: 23 | - pull_request 24 | - push 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | *.log 3 | *.map 4 | *.tsbuildinfo 5 | .DS_Store 6 | coverage/ 7 | node_modules/ 8 | yarn.lock 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md 2 | coverage/ 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./lib/index.js').Options} Options 3 | * @typedef {import('./lib/index.js').YamlOptions} YamlOptions 4 | */ 5 | 6 | export {matter} from './lib/index.js' 7 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {VFile} from 'vfile' 3 | * @import { 4 | * DocumentOptions, 5 | * ParseOptions, 6 | * SchemaOptions, 7 | * ToJSOptions as ToJsOptions 8 | * } from 'yaml' 9 | */ 10 | 11 | /** 12 | * @typedef Options 13 | * Configuration (optional). 14 | * @property {boolean | null | undefined} [strip=false] 15 | * Remove the YAML front matter from the file (default: `false`). 16 | * @property {Readonly | null | undefined} [yaml={}] 17 | * Configuration for the YAML parser, passed to `yaml` as `x` in 18 | * `yaml.parse('', x)` (default: `{}`). 19 | */ 20 | 21 | /** 22 | * @template Type 23 | * Type. 24 | * @typedef {{[Key in keyof Type]: Type[Key]} & {}} Prettify 25 | * Flatten a TypeScript record. 26 | */ 27 | 28 | /** 29 | * @typedef {Prettify} YamlOptions 30 | * Options for the YAML parser. 31 | * 32 | * Equivalent to `DocumentOptions`, `ParseOptions`, `SchemaOptions`, and `ToJsOptions`. 33 | */ 34 | 35 | import yaml from 'yaml' 36 | 37 | const regex = /^---(?:\r?\n|\r)(?:([\s\S]*?)(?:\r?\n|\r))?---(?:\r?\n|\r|$)/ 38 | 39 | /** @type {Readonly} */ 40 | const emptyOptions = {} 41 | /** @type {Readonly} */ 42 | const emptyYamlOptions = {} 43 | 44 | /** 45 | * Parse the YAML front matter in a file and expose it as `file.data.matter`. 46 | * 47 | * If no matter is found in the file, nothing happens, except that 48 | * `file.data.matter` is set to an empty object (`{}`). 49 | * 50 | * If the file value is an `Uint8Array`, assumes it is encoded in UTF-8. 51 | * 52 | * @param {VFile} file 53 | * Virtual file. 54 | * @param {Readonly | null | undefined} [options={}] 55 | * Configuration (optional). 56 | * @returns {undefined} 57 | * Nothing. 58 | */ 59 | export function matter(file, options) { 60 | const options_ = options || emptyOptions 61 | const strip = options_.strip 62 | const yamlOptions = options_.yaml || emptyYamlOptions 63 | let document = String(file) 64 | const match = regex.exec(document) 65 | 66 | if (match) { 67 | file.data.matter = yaml.parse(match[1] || '', yamlOptions) || {} 68 | 69 | if (strip) { 70 | document = document.slice(match[0].length) 71 | 72 | file.value = 73 | file.value && typeof file.value === 'object' 74 | ? new TextEncoder().encode(document) 75 | : document 76 | } 77 | } else { 78 | file.data.matter = {} 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 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 | "author": "Titus Wormer (https://wooorm.com)", 3 | "bugs": "https://github.com/vfile/vfile-matter/issues", 4 | "contributors": [ 5 | "Titus Wormer (https://wooorm.com)" 6 | ], 7 | "dependencies": { 8 | "vfile": "^6.0.0", 9 | "yaml": "^2.0.0" 10 | }, 11 | "description": "vfile utility to parse the YAML front matter in a file", 12 | "devDependencies": { 13 | "@types/node": "^22.0.0", 14 | "c8": "^10.0.0", 15 | "prettier": "^3.0.0", 16 | "remark-cli": "^12.0.0", 17 | "remark-preset-wooorm": "^11.0.0", 18 | "type-coverage": "^2.0.0", 19 | "typescript": "^5.0.0", 20 | "xo": "^0.60.0" 21 | }, 22 | "exports": "./index.js", 23 | "files": [ 24 | "index.d.ts.map", 25 | "index.d.ts", 26 | "index.js", 27 | "lib/" 28 | ], 29 | "funding": { 30 | "type": "opencollective", 31 | "url": "https://opencollective.com/unified" 32 | }, 33 | "keywords": [ 34 | "file", 35 | "frontmatter", 36 | "matter", 37 | "utility", 38 | "util", 39 | "vfile-util", 40 | "vfile", 41 | "virtual", 42 | "yaml" 43 | ], 44 | "license": "MIT", 45 | "name": "vfile-matter", 46 | "prettier": { 47 | "bracketSpacing": false, 48 | "semi": false, 49 | "singleQuote": true, 50 | "tabWidth": 2, 51 | "trailingComma": "none", 52 | "useTabs": false 53 | }, 54 | "remarkConfig": { 55 | "plugins": [ 56 | "remark-preset-wooorm" 57 | ] 58 | }, 59 | "repository": "vfile/vfile-matter", 60 | "scripts": { 61 | "build": "tsc --build --clean && tsc --build && type-coverage", 62 | "format": "remark --frail --output --quiet -- . && prettier --log-level warn --write -- . && xo --fix", 63 | "test-api": "node --conditions development test.js", 64 | "test-coverage": "c8 --100 --reporter lcov -- npm run test-api", 65 | "test": "npm run build && npm run format && npm run test-coverage" 66 | }, 67 | "sideEffects": false, 68 | "typeCoverage": { 69 | "atLeast": 100, 70 | "strict": true 71 | }, 72 | "type": "module", 73 | "version": "5.0.1", 74 | "xo": { 75 | "prettier": true 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # vfile-matter 2 | 3 | [![Build][badge-build-image]][badge-build-url] 4 | [![Coverage][badge-coverage-image]][badge-coverage-url] 5 | [![Downloads][badge-downloads-image]][badge-downloads-url] 6 | [![Size][badge-size-image]][badge-size-url] 7 | 8 | [vfile][github-vfile] utility parse YAML front matter. 9 | 10 | ## Contents 11 | 12 | * [What is this?](#what-is-this) 13 | * [When should I use this?](#when-should-i-use-this) 14 | * [Install](#install) 15 | * [Use](#use) 16 | * [API](#api) 17 | * [`matter(file[, options])`](#matterfile-options) 18 | * [`Options`](#options) 19 | * [`YamlOptions`](#yamloptions) 20 | * [Types](#types) 21 | * [Compatibility](#compatibility) 22 | * [Contribute](#contribute) 23 | * [License](#license) 24 | 25 | ## What is this? 26 | 27 | This package parses YAML frontmatter, 28 | when found in a file, 29 | and exposes it as `file.data.matter`. 30 | It can optionally strip the frontmatter, 31 | which is useful for languages that do not understand frontmatter, 32 | but stripping can make it harder to deal with languages that *do* understand it, 33 | such as markdown, 34 | because it messes up positional info of warnings and errors. 35 | 36 | ## When should I use this? 37 | 38 | Frontmatter is a metadata format in front of content. 39 | It’s typically written in YAML and is often used with markdown. 40 | This mechanism works well when you want authors, 41 | that have some markup experience, 42 | to configure where or how the content is displayed or supply metadata about 43 | content. 44 | 45 | When using vfiles with markdown, 46 | you are likely also using [remark][github-remark], 47 | in which case you should use [`remark-frontmatter`][github-remark-frontmatter], 48 | instead of 49 | stripping frontmatter. 50 | 51 | ## Install 52 | 53 | This package is [ESM only][github-gist-esm]. 54 | In Node.js (version 16+), 55 | install with [npm][npmjs-install]: 56 | 57 | ```sh 58 | npm install vfile-matter 59 | ``` 60 | 61 | In Deno with [`esm.sh`][esmsh]: 62 | 63 | ```js 64 | import {matter} from 'https://esm.sh/vfile-matter@5' 65 | ``` 66 | 67 | In browsers with [`esm.sh`][esmsh]: 68 | 69 | ```html 70 | 73 | ``` 74 | 75 | ## Use 76 | 77 | Say our document `example.html` contains: 78 | 79 | ```html 80 | --- 81 | layout: solar-system 82 | --- 83 |

Jupiter

84 | ``` 85 | 86 | …and our module `example.js` looks as follows: 87 | 88 | ```js 89 | import {read} from 'to-vfile' 90 | import {matter} from 'vfile-matter' 91 | 92 | const file = await read('example.html') 93 | 94 | matter(file, {strip: true}) 95 | 96 | console.log(file.data) 97 | console.log(String(file)) 98 | ``` 99 | 100 | …now running `node example.js` yields: 101 | 102 | ```js 103 | { matter: { layout: 'solar-system' } } 104 | ``` 105 | 106 | ```html 107 |

Jupiter

108 | ``` 109 | 110 | ## API 111 | 112 | This package exports the identifier [`matter`][api-matter]. 113 | It exports the [TypeScript][] types 114 | [`Options`][api-options] and 115 | [`YamlOptions`][api-yaml-options]. 116 | There is no default export. 117 | 118 | ### `matter(file[, options])` 119 | 120 | Parse the YAML front matter in a file and expose it as `file.data.matter`. 121 | 122 | If no matter is found in the file, 123 | nothing happens, 124 | except that `file.data.matter` is set to an empty object (`{}`). 125 | 126 | If the file value is an `Uint8Array`, 127 | assumes it is encoded in UTF-8. 128 | 129 | ###### Parameters 130 | 131 | * `file` 132 | ([`VFile`][github-vfile]) 133 | — virtual file 134 | * `options` 135 | ([`Options`][api-options], default: `{}`) 136 | — configuration 137 | 138 | ###### Returns 139 | 140 | Nothing (`undefined`). 141 | 142 | ### `Options` 143 | 144 | Configuration (TypeScript type). 145 | 146 | ###### Fields 147 | 148 | * `strip` 149 | (`boolean`, default: `false`) 150 | — remove the YAML front matter from the file 151 | * `yaml` 152 | ([`YamlOptions`][api-yaml-options], default: `{}`) 153 | — configuration for the YAML parser, 154 | passed to [`yaml`][github-yaml] as `x` in `yaml.parse('', x)` 155 | 156 | ### `YamlOptions` 157 | 158 | Options for the YAML parser (TypeScript type). 159 | 160 | Equivalent to the combination of 161 | [`DocumentOptions`](https://eemeli.org/yaml/#document-options), 162 | [`ParseOptions`](https://eemeli.org/yaml/#parse-options), 163 | [`SchemaOptions`](https://eemeli.org/yaml/#schema-options), 164 | and 165 | [`ToJsOptions`](https://eemeli.org/yaml/#tojs-options). 166 | 167 | ###### Type 168 | 169 | ```ts 170 | type YamlOptions = DocumentOptions & 171 | ParseOptions & 172 | SchemaOptions & 173 | ToJsOptions 174 | ``` 175 | 176 | ## Types 177 | 178 | To type `file.data.matter` with [TypeScript][], 179 | you can augment `DataMap` from `vfile` as follows: 180 | 181 | ```ts 182 | declare module 'vfile' { 183 | interface DataMap { 184 | matter: { 185 | // `file.data.matter.string` is typed as `string | undefined`. 186 | title?: string | undefined 187 | } 188 | } 189 | } 190 | 191 | // You may not need this, 192 | // but it makes sure the file is a module. 193 | export {} 194 | ``` 195 | 196 | ## Compatibility 197 | 198 | Projects maintained by the unified collective are compatible with maintained 199 | versions of Node.js. 200 | 201 | When we cut a new major release, 202 | we drop support for unmaintained versions of Node. 203 | This means we try to keep the current release line, 204 | `vfile-matter@5`, 205 | compatible with Node.js 16. 206 | 207 | ## Contribute 208 | 209 | See [`contributing.md`][health-contributing] in [`vfile/.github`][health] 210 | for ways to get started. 211 | See [`support.md`][health-support] for ways to get help. 212 | 213 | This project has a [code of conduct][health-coc]. 214 | By interacting with this repository, 215 | organization, 216 | or community you agree to abide by its terms. 217 | 218 | ## License 219 | 220 | [MIT][file-license] © [Titus Wormer][wooorm] 221 | 222 | 223 | 224 | [api-matter]: #matterfile-options 225 | 226 | [api-options]: #options 227 | 228 | [api-yaml-options]: #yamloptions 229 | 230 | [badge-build-image]: https://github.com/vfile/vfile-matter/workflows/main/badge.svg 231 | 232 | [badge-build-url]: https://github.com/vfile/vfile-matter/actions 233 | 234 | [badge-coverage-image]: https://img.shields.io/codecov/c/github/vfile/vfile-matter.svg 235 | 236 | [badge-coverage-url]: https://codecov.io/github/vfile/vfile-matter 237 | 238 | [badge-downloads-image]: https://img.shields.io/npm/dm/vfile-matter.svg 239 | 240 | [badge-downloads-url]: https://www.npmjs.com/package/vfile-matter 241 | 242 | [badge-size-image]: https://img.shields.io/bundlejs/size/vfile-matter 243 | 244 | [badge-size-url]: https://bundlejs.com/?q=vfile-matter 245 | 246 | [esmsh]: https://esm.sh 247 | 248 | [file-license]: license 249 | 250 | [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 251 | 252 | [github-remark]: https://github.com/remarkjs/remark 253 | 254 | [github-remark-frontmatter]: https://github.com/remarkjs/remark-frontmatter 255 | 256 | [github-vfile]: https://github.com/vfile/vfile 257 | 258 | [github-yaml]: https://github.com/eemeli/yaml 259 | 260 | [health]: https://github.com/vfile/.github 261 | 262 | [health-coc]: https://github.com/vfile/.github/blob/main/code-of-conduct.md 263 | 264 | [health-contributing]: https://github.com/vfile/.github/blob/main/contributing.md 265 | 266 | [health-support]: https://github.com/vfile/.github/blob/main/support.md 267 | 268 | [npmjs-install]: https://docs.npmjs.com/cli/install 269 | 270 | [typescript]: https://www.typescriptlang.org 271 | 272 | [wooorm]: https://wooorm.com 273 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import test from 'node:test' 3 | import {VFile} from 'vfile' 4 | import {matter} from 'vfile-matter' 5 | 6 | const someYaml = '---\nkey: value\nlist:\n - 1\n - 2\n---' 7 | const document = 'Here is a document\nMore of the document\nOther lines\n' 8 | const both = someYaml + '\n' + document 9 | 10 | test('matter', async function (t) { 11 | await t.test('should expose the public api', async function () { 12 | assert.deepEqual(Object.keys(await import('vfile-matter')).sort(), [ 13 | 'matter' 14 | ]) 15 | }) 16 | 17 | await t.test('should add data', async function () { 18 | const file = new VFile(both) 19 | matter(file) 20 | 21 | assert.deepEqual(file.data, {matter: {key: 'value', list: [1, 2]}}) 22 | }) 23 | 24 | await t.test('should support no matter', async function () { 25 | const file = new VFile(document) 26 | matter(file) 27 | assert.deepEqual(file.data, {matter: {}}) 28 | }) 29 | 30 | await t.test('should strip matter', async function () { 31 | const file = new VFile(both) 32 | matter(file, {strip: true}) 33 | assert.deepEqual(String(file), document) 34 | }) 35 | 36 | await t.test('should strip matter completely', async function () { 37 | const file = new VFile(someYaml) 38 | matter(file, {strip: true}) 39 | assert.deepEqual(String(file), '') 40 | }) 41 | 42 | await t.test('should support no matter w/ strip', async function () { 43 | const file = new VFile(document) 44 | matter(file, {strip: true}) 45 | assert.deepEqual(String(file), document) 46 | }) 47 | 48 | await t.test('should supporting `Uint8Array`s (parse)', async function () { 49 | const file = new VFile( 50 | new TextEncoder().encode('---\na: "hi"\n---\n\n# hi') 51 | ) 52 | matter(file) 53 | assert.deepEqual(file.data.matter, {a: 'hi'}) 54 | assert.ok( 55 | file.value && typeof file.value === 'object', 56 | 'should supporting `Uint8Array`s' 57 | ) 58 | }) 59 | 60 | await t.test('should supporting `Uint8Array`s (strip)', async function () { 61 | const file = new VFile( 62 | new TextEncoder().encode('---\na: "hi"\n---\n\n# hi') 63 | ) 64 | matter(file, {strip: true}) 65 | assert.deepEqual(file.data.matter, {a: 'hi'}) 66 | assert.ok(file.value && typeof file.value === 'object') 67 | }) 68 | 69 | await t.test('should handle thematic breaks', async function () { 70 | const extra = 'Here is a thematic break\n---\nEnd' 71 | const file = new VFile(both + extra) 72 | matter(file, {strip: true}) 73 | assert.deepEqual(String(file), document + extra) 74 | }) 75 | 76 | await t.test('should support files w/o value', async function () { 77 | const file = new VFile() 78 | matter(file, {strip: true}) 79 | assert.ok(file.value === undefined) 80 | }) 81 | 82 | await t.test('should support empty frontmatter', async function () { 83 | const file = new VFile('---\n---\n') 84 | matter(file) 85 | assert.deepEqual(file.data, {matter: {}}) 86 | }) 87 | 88 | await t.test('should support blank line in frontmatter', async function () { 89 | const file = new VFile('---\n\n---\n') 90 | matter(file) 91 | assert.deepEqual(file.data, {matter: {}}) 92 | }) 93 | 94 | await t.test( 95 | 'should support whitespace-only blank line in frontmatter', 96 | async function () { 97 | const file = new VFile('---\n \t\n---\n') 98 | matter(file) 99 | assert.deepEqual(file.data, {matter: {}}) 100 | } 101 | ) 102 | 103 | await t.test('should pass yaml options (1)', async function () { 104 | const file = new VFile('---\nyes: no\n---\n') 105 | matter(file, {yaml: {version: '1.2'}}) 106 | assert.deepEqual(file.data, {matter: {yes: 'no'}}) 107 | }) 108 | 109 | await t.test('should pass yaml options (2)', async function () { 110 | const file = new VFile('---\nyes: no\n---\n') 111 | matter(file, {yaml: {version: '1.1'}}) 112 | assert.deepEqual(file.data, {matter: {true: false}}) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declarationMap": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | "strict": true, 12 | "target": "es2022" 13 | }, 14 | "exclude": ["coverage/", "node_modules/"], 15 | "include": ["**/*.js"] 16 | } 17 | --------------------------------------------------------------------------------