├── .eslintignore ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .mocharc.yml ├── README.md ├── lib └── front_matter.ts ├── package.json ├── test ├── .eslintrc.json └── index.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | tmp/ 4 | dist/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "hexo/ts.js", 4 | "parserOptions": { 5 | "sourceType": "module", 6 | "ecmaVersion": 2020 7 | }, 8 | "rules": { 9 | "@typescript-eslint/no-explicit-any": 0, 10 | "@typescript-eslint/ban-ts-comment": 0 11 | } 12 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | ignore: 8 | - dependency-name: "@types/node" 9 | - dependency-name: "*" 10 | update-types: ["version-update:semver-patch"] 11 | open-pull-requests-limit: 3 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | pull_request: 8 | 9 | env: 10 | default_node_version: 16 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | needs: build 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, windows-latest, macos-latest] 20 | node-version: ["16", "18", "20", "22"] 21 | fail-fast: false 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Setup Node.js ${{ matrix.node-version }} and Cache 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: npm 29 | cache-dependency-path: "package.json" 30 | 31 | - name: Install Dependencies 32 | run: npm install 33 | - name: Test 34 | run: npm run test 35 | 36 | coverage: 37 | name: Coverage 38 | needs: build 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Setup Node.js ${{env.default_node_version}} and Cache 43 | uses: actions/setup-node@v4 44 | with: 45 | node-version: ${{env.default_node_version}} 46 | cache: npm 47 | cache-dependency-path: "package.json" 48 | 49 | - name: Install Dependencies 50 | run: npm install 51 | - name: Coverage 52 | run: npm run test-cov 53 | - name: Coveralls 54 | uses: coverallsapp/github-action@v2 55 | with: 56 | github-token: ${{ secrets.github_token }} 57 | 58 | build: 59 | name: Build 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v4 63 | - name: Setup Node.js ${{env.default_node_version}} and Cache 64 | uses: actions/setup-node@v4 65 | with: 66 | node-version: ${{env.default_node_version}} 67 | cache: npm 68 | cache-dependency-path: "package.json" 69 | 70 | - name: Install Dependencies 71 | run: npm install 72 | - name: Build 73 | run: npm run build 74 | 75 | lint: 76 | name: Lint 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: actions/checkout@v4 80 | - name: Setup Node.js ${{env.default_node_version}} and Cache 81 | uses: actions/setup-node@v4 82 | with: 83 | node-version: ${{env.default_node_version}} 84 | cache: npm 85 | cache-dependency-path: "package.json" 86 | 87 | - name: Install Dependencies 88 | run: npm install 89 | - name: Lint 90 | run: npm run eslint 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | tmp/ 4 | *.log 5 | .idea/ 6 | coverage 7 | dist 8 | package-lock.json -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | reporter: spec 2 | loader: 3 | # - ts-node/esm 4 | require: 5 | - ts-node/register 6 | - chai/register-should.js 7 | 8 | spec: ["test/*.ts"] 9 | watch-files: ["lib"] 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hexo-front-matter 2 | 3 | [![CI](https://github.com/hexojs/hexo-front-matter/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/hexojs/hexo-front-matter/actions/workflows/ci.yml) 4 | [![NPM version](https://badge.fury.io/js/hexo-front-matter.svg)](https://www.npmjs.com/package/hexo-front-matter) 5 | [![Coverage Status](https://coveralls.io/repos/github/hexojs/hexo-front-matter/badge.svg)](https://coveralls.io/github/hexojs/hexo-front-matter) 6 | 7 | Front-matter parser. 8 | 9 | ## What is Front-matter? 10 | 11 | Front-matter allows you to specify data at the top of a file. Here are two formats: 12 | 13 | **YAML front-matter** 14 | 15 | ``` 16 | --- 17 | layout: false 18 | title: "Hello world" 19 | --- 20 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 21 | ``` 22 | 23 | **JSON front-matter** 24 | 25 | ``` 26 | ;;; 27 | "layout": false, 28 | "title": "Hello world" 29 | ;;; 30 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 31 | ``` 32 | 33 | Prefixing separators are optional. 34 | 35 | ## API 36 | 37 | ### parse(str, [options]) 38 | 39 | Parses front-matter. 40 | 41 | ### stringify(obj, [options]) 42 | 43 | Converts an object to a front-matter string. 44 | 45 | Option | Description | Default 46 | --- | --- | --- 47 | `mode` | The mode can be either `json` or `yaml`. | `yaml` 48 | `separator` | Separator | `---` 49 | `prefixSeparator` | Add prefixing separator. | `false` 50 | 51 | ### split(str) 52 | 53 | Splits a YAML front-matter string. 54 | 55 | ### escape(str) 56 | 57 | Converts hard tabs to soft tabs. 58 | 59 | ## License 60 | 61 | MIT -------------------------------------------------------------------------------- /lib/front_matter.ts: -------------------------------------------------------------------------------- 1 | import yaml from 'js-yaml'; 2 | const rPrefixSep = /^(-{3,}|;{3,})/; 3 | const rFrontMatter = /^(-{3,}|;{3,})\r?\n([\s\S]+?)\r?\n\1\r?\n?([\s\S]*)/; 4 | const rFrontMatterNew = /^([\s\S]+?)\r?\n(-{3,}|;{3,})\r?\n?([\s\S]*)/; 5 | 6 | function split(str: string) { 7 | if (typeof str !== 'string') throw new TypeError('str is required!'); 8 | 9 | const matchOld = str.match(rFrontMatter); 10 | if (matchOld) { 11 | return { 12 | data: matchOld[2], 13 | content: matchOld[3] || '', 14 | separator: matchOld[1], 15 | prefixSeparator: true 16 | }; 17 | } 18 | 19 | if (rPrefixSep.test(str)) return { content: str }; 20 | 21 | const matchNew = str.match(rFrontMatterNew); 22 | 23 | if (matchNew) { 24 | return { 25 | data: matchNew[1], 26 | content: matchNew[3] || '', 27 | separator: matchNew[2], 28 | prefixSeparator: false 29 | }; 30 | } 31 | 32 | return { content: str }; 33 | } 34 | 35 | function parse(str: string, options?: yaml.LoadOptions) { 36 | if (typeof str !== 'string') throw new TypeError('str is required!'); 37 | 38 | const splitData = split(str); 39 | const raw = splitData.data; 40 | 41 | if (!raw) return { _content: str }; 42 | 43 | let data; 44 | 45 | if (splitData.separator.startsWith(';')) { 46 | data = parseJSON(raw); 47 | } else { 48 | data = parseYAML(raw, options); 49 | } 50 | 51 | if (!data) return { _content: str }; 52 | 53 | // Convert timezone 54 | Object.keys(data).forEach(key => { 55 | const item = data[key]; 56 | 57 | if (item instanceof Date) { 58 | data[key] = new Date(item.getTime() + (item.getTimezoneOffset() * 60 * 1000)); 59 | } 60 | }); 61 | 62 | data._content = splitData.content; 63 | return data; 64 | } 65 | 66 | function parseYAML(str, options: yaml.LoadOptions) { 67 | const result = yaml.load(escapeYAML(str), options); 68 | if (typeof result !== 'object') return; 69 | 70 | return result; 71 | } 72 | 73 | function parseJSON(str) { 74 | try { 75 | return JSON.parse(`{${str}}`); 76 | } catch (err) { 77 | return; // eslint-disable-line 78 | } 79 | } 80 | 81 | function escapeYAML(str: string) { 82 | if (typeof str !== 'string') throw new TypeError('str is required!'); 83 | 84 | return str.replace(/\r?\n(\t+)/g, (match, tabs) => { 85 | let result = '\n'; 86 | 87 | for (let i = 0, len = tabs.length; i < len; i++) { 88 | result += ' '; 89 | } 90 | 91 | return result; 92 | }); 93 | } 94 | 95 | interface Options { 96 | mode?: 'json' | '', 97 | prefixSeparator?: boolean, 98 | separator?: string 99 | } 100 | 101 | function stringify(obj, options: Options = {}) { 102 | if (!obj) throw new TypeError('obj is required!'); 103 | 104 | const { _content: content = '' } = obj; 105 | delete obj._content; 106 | 107 | if (!Object.keys(obj).length) return content; 108 | 109 | const { mode, prefixSeparator } = options; 110 | const separator = options.separator || (mode === 'json' ? ';;;' : '---'); 111 | let result = ''; 112 | 113 | if (prefixSeparator) result += `${separator}\n`; 114 | 115 | if (mode === 'json') { 116 | result += stringifyJSON(obj); 117 | } else { 118 | result += stringifyYAML(obj, options); 119 | } 120 | 121 | result += `${separator}\n${content}`; 122 | 123 | return result; 124 | } 125 | 126 | function stringifyYAML(obj, options) { 127 | const keys = Object.keys(obj); 128 | const data = {}; 129 | const nullKeys = []; 130 | const dateKeys = []; 131 | let key, value, i, len; 132 | 133 | for (i = 0, len = keys.length; i < len; i++) { 134 | key = keys[i]; 135 | value = obj[key]; 136 | 137 | if (value == null) { 138 | nullKeys.push(key); 139 | } else if (value instanceof Date) { 140 | dateKeys.push(key); 141 | } else { 142 | data[key] = value; 143 | } 144 | } 145 | 146 | let result = yaml.dump(data, options); 147 | 148 | if (dateKeys.length) { 149 | for (i = 0, len = dateKeys.length; i < len; i++) { 150 | key = dateKeys[i]; 151 | result += `${key}: ${formatDate(obj[key])}\n`; 152 | } 153 | } 154 | 155 | if (nullKeys.length) { 156 | for (i = 0, len = nullKeys.length; i < len; i++) { 157 | result += `${nullKeys[i]}:\n`; 158 | } 159 | } 160 | 161 | return result; 162 | } 163 | 164 | function stringifyJSON(obj) { 165 | return JSON.stringify(obj, null, ' ') 166 | // Remove indention 167 | .replace(/\r?\n {2}/g, () => '\n') 168 | // Remove prefixing and trailing braces 169 | .replace(/^{\r?\n|}$/g, ''); 170 | } 171 | 172 | function doubleDigit(num) { 173 | return num.toString().padStart(2, '0'); 174 | } 175 | 176 | function formatDate(date) { 177 | return `${date.getFullYear()}-${doubleDigit(date.getMonth() + 1)}-${doubleDigit(date.getDate())} ${doubleDigit(date.getHours())}:${doubleDigit(date.getMinutes())}:${doubleDigit(date.getSeconds())}`; 178 | } 179 | 180 | export { 181 | parse, 182 | split, 183 | escapeYAML as escape, 184 | stringify 185 | }; 186 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-front-matter", 3 | "version": "4.2.1", 4 | "description": "Front-matter parser.", 5 | "main": "dist/front_matter", 6 | "scripts": { 7 | "prepublish ": "npm run clean && npm run build", 8 | "build": "tsc -b", 9 | "clean": "tsc -b --clean", 10 | "eslint": "eslint .", 11 | "test": "mocha", 12 | "test-cov": "c8 --reporter=lcovonly npm run test" 13 | }, 14 | "files": [ 15 | "dist/**" 16 | ], 17 | "types": "./dist/front_matter.d.ts", 18 | "repository": "hexojs/hexo-front-matter", 19 | "keywords": [ 20 | "front-matter", 21 | "front matter", 22 | "yaml", 23 | "yml", 24 | "hexo", 25 | "json" 26 | ], 27 | "author": "Tommy Chen (http://zespia.tw)", 28 | "license": "MIT", 29 | "dependencies": { 30 | "js-yaml": "^4.1.0" 31 | }, 32 | "devDependencies": { 33 | "@types/chai": "^5.0.1", 34 | "@types/js-yaml": "^4.0.5", 35 | "@types/mocha": "^10.0.6", 36 | "@types/node": "^18.11.7", 37 | "c8": "^10.1.2", 38 | "chai": "^5.0.0", 39 | "eslint": "^8.23.1", 40 | "eslint-config-hexo": "^5.0.0", 41 | "mocha": "^10.0.0", 42 | "ts-node": "^10.9.1", 43 | "typescript": "^5.2.2" 44 | }, 45 | "engines": { 46 | "node": ">=16" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "hexo/ts-test", 3 | "rules": { 4 | "@typescript-eslint/no-var-requires": 0, 5 | "@typescript-eslint/no-empty-function": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | 2 | const yfm = require('../lib/front_matter.ts'); 3 | 4 | describe('Front-matter', () => { 5 | 6 | describe('split', () => { 7 | it('not string', () => { 8 | (() => { 9 | const str = []; 10 | 11 | yfm.split(str).should.throw(TypeError, 'str is required!'); 12 | }).should.throw(); 13 | }); 14 | 15 | it('yaml mode', () => { 16 | const str = [ 17 | '---', 18 | 'foo', 19 | '---', 20 | 'bar' 21 | ].join('\n'); 22 | 23 | yfm.split(str).should.eql({ 24 | data: 'foo', 25 | content: 'bar', 26 | separator: '---', 27 | prefixSeparator: true 28 | }); 29 | }); 30 | 31 | it('yaml mode - no content', () => { 32 | const str = [ 33 | '---', 34 | 'foo', 35 | '---' 36 | ].join('\n'); 37 | 38 | yfm.split(str).should.eql({ 39 | data: 'foo', 40 | content: '', 41 | separator: '---', 42 | prefixSeparator: true 43 | }); 44 | }); 45 | 46 | it('yaml mode - content conflict', () => { 47 | const str = [ 48 | '---', 49 | 'foo', 50 | '---', 51 | 'bar', 52 | '---', 53 | 'baz' 54 | ].join('\n'); 55 | 56 | yfm.split(str).should.eql({ 57 | data: 'foo', 58 | content: 'bar\n---\nbaz', 59 | separator: '---', 60 | prefixSeparator: true 61 | }); 62 | }); 63 | 64 | it('json mode', () => { 65 | const str = [ 66 | ';;;', 67 | 'foo', 68 | ';;;', 69 | 'bar' 70 | ].join('\n'); 71 | 72 | yfm.split(str).should.eql({ 73 | data: 'foo', 74 | content: 'bar', 75 | separator: ';;;', 76 | prefixSeparator: true 77 | }); 78 | }); 79 | 80 | it('json mode - no content', () => { 81 | const str = [ 82 | ';;;', 83 | 'foo', 84 | ';;;' 85 | ].join('\n'); 86 | 87 | yfm.split(str).should.eql({ 88 | data: 'foo', 89 | content: '', 90 | separator: ';;;', 91 | prefixSeparator: true 92 | }); 93 | }); 94 | 95 | it('yaml mode: new', () => { 96 | const str = [ 97 | 'foo', 98 | '---', 99 | 'bar' 100 | ].join('\n'); 101 | 102 | yfm.split(str).should.eql({ 103 | data: 'foo', 104 | content: 'bar', 105 | separator: '---', 106 | prefixSeparator: false 107 | }); 108 | }); 109 | 110 | it('yaml mode: new - no content', () => { 111 | const str = [ 112 | 'foo', 113 | '---' 114 | ].join('\n'); 115 | 116 | yfm.split(str).should.eql({ 117 | data: 'foo', 118 | content: '', 119 | separator: '---', 120 | prefixSeparator: false 121 | }); 122 | }); 123 | 124 | it('yaml mode: new - content conflict', () => { 125 | const str = [ 126 | 'foo', 127 | '---', 128 | 'bar', 129 | '---', 130 | 'baz' 131 | ].join('\n'); 132 | 133 | yfm.split(str).should.eql({ 134 | data: 'foo', 135 | content: 'bar\n---\nbaz', 136 | separator: '---', 137 | prefixSeparator: false 138 | }); 139 | }); 140 | 141 | it('json mode: new', () => { 142 | const str = [ 143 | 'foo', 144 | ';;;', 145 | 'bar' 146 | ].join('\n'); 147 | 148 | yfm.split(str).should.eql({ 149 | data: 'foo', 150 | content: 'bar', 151 | separator: ';;;', 152 | prefixSeparator: false 153 | }); 154 | }); 155 | 156 | it('json mode: new - no content', () => { 157 | const str = [ 158 | 'foo', 159 | ';;;' 160 | ].join('\n'); 161 | 162 | yfm.split(str).should.eql({ 163 | data: 'foo', 164 | content: '', 165 | separator: ';;;', 166 | prefixSeparator: false 167 | }); 168 | }); 169 | 170 | it('without data', () => { 171 | const str = [ 172 | 'foo', 173 | 'bar' 174 | ].join('\n'); 175 | 176 | yfm.split(str).should.eql({ 177 | content: str 178 | }); 179 | }); 180 | 181 | it('unbalanced separator', () => { 182 | const str = [ 183 | '------', 184 | 'foo', 185 | '---', 186 | 'bar' 187 | ].join('\n'); 188 | 189 | yfm.split(str).should.eql({ 190 | content: str 191 | }); 192 | }); 193 | 194 | it('long separator', () => { 195 | const str = [ 196 | '------', 197 | 'foo', 198 | '------', 199 | 'bar' 200 | ].join('\n'); 201 | 202 | yfm.split(str).should.eql({ 203 | data: 'foo', 204 | content: 'bar', 205 | separator: '------', 206 | prefixSeparator: true 207 | }); 208 | }); 209 | 210 | it('long separator: new', () => { 211 | const str = [ 212 | 'foo', 213 | '------', 214 | 'bar' 215 | ].join('\n'); 216 | 217 | yfm.split(str).should.eql({ 218 | data: 'foo', 219 | content: 'bar', 220 | separator: '------', 221 | prefixSeparator: false 222 | }); 223 | }); 224 | 225 | it('extra separator', () => { 226 | const str = [ 227 | 'foo', 228 | '---', 229 | 'bar', 230 | '---', 231 | 'baz' 232 | ].join('\n'); 233 | 234 | yfm.split(str).should.eql({ 235 | data: 'foo', 236 | content: 'bar\n---\nbaz', 237 | separator: '---', 238 | prefixSeparator: false 239 | }); 240 | }); 241 | 242 | it('inline separator', () => { 243 | const str = [ 244 | '---foo', 245 | '---', 246 | 'bar' 247 | ].join('\n'); 248 | 249 | yfm.split(str).should.eql({ 250 | content: str 251 | }); 252 | }); 253 | 254 | it('inline separator: new', () => { 255 | const str = [ 256 | '---bar' 257 | ].join('\n'); 258 | 259 | yfm.split(str).should.eql({ 260 | content: str 261 | }); 262 | }); 263 | }); 264 | 265 | describe('escape', () => { 266 | it('not string', () => { 267 | (() => { 268 | yfm.escape([]).should.throw(TypeError, 'str is required!'); 269 | }).should.throw(); 270 | }); 271 | 272 | it('escape', () => { 273 | yfm.escape([ 274 | 'foo', 275 | '\tbar', 276 | '\t\tbaz' 277 | ].join('\n')).should.eql([ 278 | 'foo', 279 | ' bar', 280 | ' baz' 281 | ].join('\n')); 282 | }); 283 | }); 284 | 285 | describe('parse', () => { 286 | it('not string', () => { 287 | (() => { 288 | const str = []; 289 | 290 | yfm.parse(str).should.throw(TypeError, 'str is required!'); 291 | }).should.throw(); 292 | }); 293 | 294 | it('only content', () => { 295 | const str = [ 296 | 'foo', 297 | 'bar' 298 | ].join('\n'); 299 | 300 | yfm.parse(str).should.eql({ 301 | _content: str 302 | }); 303 | }); 304 | 305 | it('yaml', () => { 306 | const str = [ 307 | 'layout: post', 308 | '---', 309 | 'bar' 310 | ].join('\n'); 311 | 312 | yfm.parse(str).should.eql({ 313 | layout: 'post', 314 | _content: 'bar' 315 | }); 316 | }); 317 | 318 | it('json', () => { 319 | const str = [ 320 | '"layout": false,', 321 | '"my_list": [', 322 | ' "one",', 323 | ' "two"', 324 | ']', 325 | ';;;', 326 | 'bar' 327 | ].join('\n'); 328 | 329 | yfm.parse(str).should.eql({ 330 | layout: false, 331 | my_list: ['one', 'two'], 332 | _content: 'bar' 333 | }); 334 | }); 335 | 336 | it('invalid yaml', () => { 337 | const str = [ 338 | 'layout', 339 | '---', 340 | 'bar' 341 | ].join('\n'); 342 | 343 | yfm.parse(str).should.eql({ 344 | _content: str 345 | }); 346 | }); 347 | 348 | it('invalid json', () => { 349 | const str = [ 350 | 'layout', 351 | ';;;', 352 | 'bar' 353 | ].join('\n'); 354 | 355 | yfm.parse(str).should.eql({ 356 | _content: str 357 | }); 358 | }); 359 | 360 | // Date parsing bug (issue #1) 361 | it('date', () => { 362 | const current = new Date(); 363 | const unixTime = current.getTime(); 364 | // js-yaml only accept ISO 8601 format date string as valid date type. 365 | // Date.prototype.toJSON method automatically applied offset, so we need to revert that. 366 | const stringifyDateTime = new Date(current.getTime() - (current.getTimezoneOffset() * 60 * 1000)).toJSON(); 367 | 368 | const str = [ 369 | `date: ${stringifyDateTime}`, 370 | '---' 371 | ].join('\n'); 372 | 373 | const data = yfm.parse(str); 374 | parseInt(String(data.date.getTime() / 1000), 10).should.eql(parseInt(String(unixTime / 1000), 10)); 375 | }); 376 | }); 377 | 378 | describe('stringify', () => { 379 | it('not string', () => { 380 | (() => { 381 | yfm.stringify(null).should.throw(TypeError, 'obj is required!'); 382 | }).should.throw(); 383 | }); 384 | 385 | it('yaml', () => { 386 | const now = new Date().toJSON(); 387 | 388 | const data = { 389 | layout: 'post', 390 | created: now, 391 | blank: null, 392 | _content: '123' 393 | }; 394 | 395 | yfm.stringify(data).should.eql([ 396 | 'layout: post', 397 | `created: '${now}'`, 398 | 'blank:', 399 | '---', 400 | '123' 401 | ].join('\n')); 402 | }); 403 | 404 | it('json', () => { 405 | const now = new Date(); 406 | 407 | const data = { 408 | layout: 'post', 409 | created: now, 410 | blank: null, 411 | tags: ['foo', 'bar'], 412 | _content: '123' 413 | }; 414 | 415 | yfm.stringify(data, { mode: 'json' }).should.eql([ 416 | '"layout": "post",', 417 | `"created": "${now.toISOString()}",`, 418 | '"blank": null,', 419 | '"tags": [', 420 | ' "foo",', 421 | ' "bar"', 422 | ']', 423 | ';;;', 424 | '123' 425 | ].join('\n')); 426 | }); 427 | 428 | it('separator', () => { 429 | const data = { 430 | layout: 'post', 431 | _content: 'hello' 432 | }; 433 | 434 | yfm.stringify(data, { separator: '------' }).should.eql([ 435 | 'layout: post', 436 | '------', 437 | 'hello' 438 | ].join('\n')); 439 | }); 440 | 441 | it('prefixSeparator', () => { 442 | const data = { 443 | layout: 'post', 444 | _content: 'hello' 445 | }; 446 | 447 | yfm.stringify(data, { prefixSeparator: true }).should.eql([ 448 | '---', 449 | 'layout: post', 450 | '---', 451 | 'hello' 452 | ].join('\n')); 453 | }); 454 | 455 | it('prefixSeparator + custom separator', () => { 456 | const data = { 457 | layout: 'post', 458 | _content: 'hello' 459 | }; 460 | 461 | yfm.stringify(data, { separator: '------', prefixSeparator: true }).should.eql([ 462 | '------', 463 | 'layout: post', 464 | '------', 465 | 'hello' 466 | ].join('\n')); 467 | }); 468 | 469 | it('without data', () => { 470 | const data = { 471 | _content: 'foo' 472 | }; 473 | 474 | yfm.stringify(data).should.eql('foo'); 475 | }); 476 | 477 | it('with raw date', () => { 478 | const now = new Date('1995-12-17T03:24:00'); 479 | 480 | const data = { 481 | layout: 'post', 482 | created: now, 483 | blank: null, 484 | _content: '123' 485 | }; 486 | 487 | yfm.stringify(data).should.eql([ 488 | 'layout: post', 489 | 'created: 1995-12-17 03:24:00', 490 | 'blank:', 491 | '---', 492 | '123' 493 | ].join('\n')); 494 | }); 495 | }); 496 | }); 497 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "sourceMap": true, 6 | "outDir": "dist", 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "types": ["node"] 10 | }, 11 | "include": ["lib/front_matter.ts"], 12 | "exclude": ["node_modules"], 13 | 14 | "ts-node": { 15 | "transpileOnly": true, 16 | "compilerOptions": { 17 | "types": ["node", "mocha", "chai"] 18 | } 19 | } 20 | } 21 | --------------------------------------------------------------------------------