gray-matter-es 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/defaults.mjs +19 -0
- package/dist/defaults.mjs.map +1 -0
- package/dist/engines.d.mts +8 -0
- package/dist/engines.mjs +63 -0
- package/dist/engines.mjs.map +1 -0
- package/dist/excerpt.mjs +26 -0
- package/dist/excerpt.mjs.map +1 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.mjs +126 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_chars.mjs +45 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_chars.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_dumper_state.mjs +437 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_dumper_state.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_loader_state.mjs +909 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_loader_state.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_schema.mjs +115 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_schema.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/binary.mjs +89 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/binary.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/bool.mjs +35 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/bool.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/float.mjs +55 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/float.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/int.mjs +114 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/int.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/map.mjs +15 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/map.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/merge.mjs +11 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/merge.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/nil.mjs +20 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/nil.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/omap.mjs +28 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/omap.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/pairs.mjs +19 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/pairs.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/regexp.mjs +26 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/regexp.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/seq.mjs +11 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/seq.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/set.mjs +14 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/set.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/str.mjs +11 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/str.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/timestamp.mjs +54 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/timestamp.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/undefined.mjs +19 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/undefined.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_utils.mjs +14 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_utils.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/mod.mjs +4 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/parse.mjs +50 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/parse.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/stringify.mjs +32 -0
- package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/stringify.mjs.map +1 -0
- package/dist/parse.mjs +13 -0
- package/dist/parse.mjs.map +1 -0
- package/dist/stringify.mjs +52 -0
- package/dist/stringify.mjs.map +1 -0
- package/dist/to-file.mjs +44 -0
- package/dist/to-file.mjs.map +1 -0
- package/dist/types.d.mts +85 -0
- package/dist/utils.mjs +60 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +61 -0
- package/src/defaults.ts +17 -0
- package/src/engines.ts +217 -0
- package/src/excerpt.ts +146 -0
- package/src/index.ts +481 -0
- package/src/parse.ts +9 -0
- package/src/stringify.ts +187 -0
- package/src/to-file.ts +178 -0
- package/src/types.ts +84 -0
- package/src/utils.ts +158 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { defaults } from "./defaults.ts";
|
|
3
|
+
import { toBuiltinLanguage } from "./engines.ts";
|
|
4
|
+
import { excerpt } from "./excerpt.ts";
|
|
5
|
+
import { parse } from "./parse.ts";
|
|
6
|
+
import { stringify } from "./stringify.ts";
|
|
7
|
+
import { toFile } from "./to-file.ts";
|
|
8
|
+
import type {
|
|
9
|
+
GrayMatterFile,
|
|
10
|
+
GrayMatterInput,
|
|
11
|
+
GrayMatterOptions,
|
|
12
|
+
MatterFunction,
|
|
13
|
+
} from "./types.ts";
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
Engine,
|
|
17
|
+
GrayMatterFile,
|
|
18
|
+
GrayMatterInput,
|
|
19
|
+
GrayMatterOptions,
|
|
20
|
+
MatterFunction,
|
|
21
|
+
ResolvedOptions,
|
|
22
|
+
} from "./types.ts";
|
|
23
|
+
|
|
24
|
+
export type { BuiltinLanguage } from "./engines.ts";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Cache for parsed results
|
|
28
|
+
*/
|
|
29
|
+
const cache = new Map<string, GrayMatterFile>();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Takes a string or object with `content` property, extracts
|
|
33
|
+
* and parses front-matter from the string, then returns an object
|
|
34
|
+
* with `data`, `content` and other useful properties.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import matter from 'gray-matter-es';
|
|
39
|
+
* console.log(matter('---\ntitle: Home\n---\nOther stuff'));
|
|
40
|
+
* //=> { data: { title: 'Home'}, content: 'Other stuff' }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
function matterImpl(input: GrayMatterInput, options?: GrayMatterOptions): GrayMatterFile {
|
|
44
|
+
if (input === "") {
|
|
45
|
+
return { ...toFile(input), isEmpty: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let file = toFile(input);
|
|
49
|
+
const cached = cache.get(file.content);
|
|
50
|
+
|
|
51
|
+
if (!options) {
|
|
52
|
+
if (cached) {
|
|
53
|
+
file = { ...cached };
|
|
54
|
+
file.orig = cached.orig;
|
|
55
|
+
return file;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// only cache if there are no options passed
|
|
59
|
+
cache.set(file.content, file);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return parseMatter(file, options);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse front matter from file
|
|
67
|
+
*/
|
|
68
|
+
function parseMatter(file: GrayMatterFile, options?: GrayMatterOptions): GrayMatterFile {
|
|
69
|
+
const opts = defaults(options);
|
|
70
|
+
const open = opts.delimiters[0];
|
|
71
|
+
const close = "\n" + opts.delimiters[1];
|
|
72
|
+
let str = file.content;
|
|
73
|
+
|
|
74
|
+
if (opts.language) {
|
|
75
|
+
file.language = opts.language;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// get the length of the opening delimiter
|
|
79
|
+
const openLen = open.length;
|
|
80
|
+
if (!str.startsWith(open)) {
|
|
81
|
+
excerpt(file, opts);
|
|
82
|
+
return file;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// if the next character after the opening delimiter is
|
|
86
|
+
// a character from the delimiter, then it's not a front-
|
|
87
|
+
// matter delimiter
|
|
88
|
+
if (str.at(openLen) === open.at(-1)) {
|
|
89
|
+
return file;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// strip the opening delimiter
|
|
93
|
+
str = str.slice(openLen);
|
|
94
|
+
const len = str.length;
|
|
95
|
+
|
|
96
|
+
// use the language defined after first delimiter, if it exists
|
|
97
|
+
const lang = matterLanguage(str, opts);
|
|
98
|
+
if (lang.name) {
|
|
99
|
+
file.language = lang.name;
|
|
100
|
+
str = str.slice(lang.raw.length);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// get the index of the closing delimiter
|
|
104
|
+
let closeIndex = str.indexOf(close);
|
|
105
|
+
if (closeIndex === -1) {
|
|
106
|
+
closeIndex = len;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// get the raw front-matter block
|
|
110
|
+
file.matter = str.slice(0, closeIndex);
|
|
111
|
+
|
|
112
|
+
const block = file.matter.replace(/^\s*#[^\n]+/gm, "").trim();
|
|
113
|
+
if (block === "") {
|
|
114
|
+
file.isEmpty = true;
|
|
115
|
+
file.empty = file.content;
|
|
116
|
+
file.data = {};
|
|
117
|
+
} else {
|
|
118
|
+
// create file.data by parsing the raw file.matter block
|
|
119
|
+
file.data = parse(toBuiltinLanguage(file.language), file.matter);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// update file.content
|
|
123
|
+
if (closeIndex === len) {
|
|
124
|
+
file.content = "";
|
|
125
|
+
} else {
|
|
126
|
+
file.content = str.slice(closeIndex + close.length);
|
|
127
|
+
if (file.content.at(0) === "\r") {
|
|
128
|
+
file.content = file.content.slice(1);
|
|
129
|
+
}
|
|
130
|
+
if (file.content.at(0) === "\n") {
|
|
131
|
+
file.content = file.content.slice(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
excerpt(file, opts);
|
|
136
|
+
return file;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Detect the language to use, if one is defined after the
|
|
141
|
+
* first front-matter delimiter.
|
|
142
|
+
*/
|
|
143
|
+
function matterLanguage(str: string, options?: GrayMatterOptions): { raw: string; name: string } {
|
|
144
|
+
const opts = defaults(options);
|
|
145
|
+
const open = opts.delimiters[0];
|
|
146
|
+
|
|
147
|
+
if (matterTest(str, opts)) {
|
|
148
|
+
str = str.slice(open.length);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const language = str.slice(0, str.search(/\r?\n/));
|
|
152
|
+
return {
|
|
153
|
+
raw: language,
|
|
154
|
+
name: language ? language.trim() : "",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns true if the given string has front matter.
|
|
160
|
+
*/
|
|
161
|
+
function matterTest(str: string, options?: GrayMatterOptions): boolean {
|
|
162
|
+
return str.startsWith(defaults(options).delimiters[0]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The matter function with all static methods
|
|
167
|
+
*/
|
|
168
|
+
const matter: MatterFunction = Object.assign(matterImpl, {
|
|
169
|
+
/**
|
|
170
|
+
* Stringify an object to YAML or the specified language, and
|
|
171
|
+
* append it to the given string.
|
|
172
|
+
*/
|
|
173
|
+
stringify: (
|
|
174
|
+
file: GrayMatterFile | string,
|
|
175
|
+
data?: Record<string, unknown>,
|
|
176
|
+
options?: GrayMatterOptions,
|
|
177
|
+
): string => {
|
|
178
|
+
if (typeof file === "string") file = matterImpl(file, options);
|
|
179
|
+
return stringify(file, data, options);
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Synchronously read a file from the file system and parse front matter.
|
|
184
|
+
*/
|
|
185
|
+
read: (filepath: string, options?: GrayMatterOptions): GrayMatterFile => {
|
|
186
|
+
const str = readFileSync(filepath, "utf8");
|
|
187
|
+
const file = matterImpl(str, options);
|
|
188
|
+
file.path = filepath;
|
|
189
|
+
return file;
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Returns true if the given string has front matter.
|
|
194
|
+
*/
|
|
195
|
+
test: matterTest,
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Detect the language to use, if one is defined after the
|
|
199
|
+
* first front-matter delimiter.
|
|
200
|
+
*/
|
|
201
|
+
language: matterLanguage,
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Clear the cache
|
|
205
|
+
*/
|
|
206
|
+
clearCache: (): void => {
|
|
207
|
+
cache.clear();
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Expose cache (read-only access)
|
|
212
|
+
*/
|
|
213
|
+
cache,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
export default matter;
|
|
217
|
+
|
|
218
|
+
if (import.meta.vitest) {
|
|
219
|
+
const { fc, test } = await import("@fast-check/vitest");
|
|
220
|
+
const { Buffer } = await import("node:buffer");
|
|
221
|
+
|
|
222
|
+
describe("matter", () => {
|
|
223
|
+
beforeEach(() => {
|
|
224
|
+
matter.clearCache();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should extract YAML front matter", () => {
|
|
228
|
+
const actual = matter("---\nabc: xyz\n---");
|
|
229
|
+
expect(actual.data).toEqual({ abc: "xyz" });
|
|
230
|
+
expect(actual.content).toBe("");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should extract YAML front matter with content", () => {
|
|
234
|
+
const actual = matter("---\nabc: xyz\n---\nfoo");
|
|
235
|
+
expect(actual.data).toEqual({ abc: "xyz" });
|
|
236
|
+
expect(actual.content).toBe("foo");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should return empty object for empty string", () => {
|
|
240
|
+
const actual = matter("");
|
|
241
|
+
expect(actual.data).toEqual({});
|
|
242
|
+
expect(actual.content).toBe("");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("should return content when no front matter", () => {
|
|
246
|
+
const actual = matter("foo bar");
|
|
247
|
+
expect(actual.data).toEqual({});
|
|
248
|
+
expect(actual.content).toBe("foo bar");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("should handle CRLF line endings", () => {
|
|
252
|
+
const actual = matter("---\r\nabc: xyz\r\n---\r\ncontent");
|
|
253
|
+
expect(actual.data).toEqual({ abc: "xyz" });
|
|
254
|
+
expect(actual.content).toBe("content");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should detect language after delimiter", () => {
|
|
258
|
+
const actual = matter('---json\n{"abc": "xyz"}\n---\ncontent');
|
|
259
|
+
expect(actual.data).toEqual({ abc: "xyz" });
|
|
260
|
+
expect(actual.language).toBe("json");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should handle custom delimiters", () => {
|
|
264
|
+
const actual = matter("~~~\nabc: xyz\n~~~\ncontent", {
|
|
265
|
+
delimiters: "~~~",
|
|
266
|
+
});
|
|
267
|
+
expect(actual.data).toEqual({ abc: "xyz" });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("should extract excerpt when enabled", () => {
|
|
271
|
+
const actual = matter("---\nabc: xyz\n---\nexcerpt\n---\ncontent", {
|
|
272
|
+
excerpt: true,
|
|
273
|
+
});
|
|
274
|
+
expect(actual.excerpt).toBe("excerpt\n");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should use custom excerpt separator", () => {
|
|
278
|
+
const actual = matter("---\nabc: xyz\n---\nexcerpt\n<!-- more -->\ncontent", {
|
|
279
|
+
excerpt: true,
|
|
280
|
+
excerpt_separator: "<!-- more -->",
|
|
281
|
+
});
|
|
282
|
+
expect(actual.excerpt).toBe("excerpt\n");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should cache results when no options", () => {
|
|
286
|
+
const input = "---\nabc: xyz\n---";
|
|
287
|
+
const first = matter(input);
|
|
288
|
+
const second = matter(input);
|
|
289
|
+
expect(first.data).toEqual(second.data);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("should not cache when options provided", () => {
|
|
293
|
+
const input = "---\nabc: xyz\n---";
|
|
294
|
+
matter(input);
|
|
295
|
+
const second = matter(input, { language: "yaml" });
|
|
296
|
+
expect(second.data).toEqual({ abc: "xyz" });
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("matter.stringify", () => {
|
|
301
|
+
it("should stringify data to YAML front matter", () => {
|
|
302
|
+
const result = matter.stringify("content", { title: "Hello" });
|
|
303
|
+
expect(result).toContain("---");
|
|
304
|
+
expect(result).toContain("title: Hello");
|
|
305
|
+
expect(result).toContain("content");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("should stringify file object", () => {
|
|
309
|
+
const file = matter("---\ntitle: Test\n---\ncontent");
|
|
310
|
+
const result = matter.stringify(file, { title: "Updated" });
|
|
311
|
+
expect(result).toContain("title: Updated");
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("matter.test", () => {
|
|
316
|
+
it("should return true for string with front matter", () => {
|
|
317
|
+
expect(matter.test("---\nabc: xyz\n---")).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("should return false for string without front matter", () => {
|
|
321
|
+
expect(matter.test("foo bar")).toBe(false);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("matter.language", () => {
|
|
326
|
+
it("should detect language after delimiter", () => {
|
|
327
|
+
const result = matter.language("---json\n{}\n---");
|
|
328
|
+
expect(result.name).toBe("json");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("should return empty for no language", () => {
|
|
332
|
+
const result = matter.language("---\nabc: xyz\n---");
|
|
333
|
+
expect(result.name).toBe("");
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("property-based tests", () => {
|
|
338
|
+
beforeEach(() => {
|
|
339
|
+
matter.clearCache();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
/** Arbitrary for YAML-safe keys */
|
|
343
|
+
const yamlKey = fc
|
|
344
|
+
.string({ minLength: 1, maxLength: 20 })
|
|
345
|
+
.filter((s) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(s));
|
|
346
|
+
|
|
347
|
+
/** Arbitrary for YAML-safe values */
|
|
348
|
+
const yamlSafeValue = fc.oneof(
|
|
349
|
+
fc.string({ maxLength: 50 }),
|
|
350
|
+
fc.integer({ min: -10000, max: 10000 }),
|
|
351
|
+
fc.boolean(),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
/** Arbitrary for simple YAML-compatible objects */
|
|
355
|
+
const yamlSafeObject = fc.dictionary(yamlKey, yamlSafeValue, { minKeys: 1, maxKeys: 5 });
|
|
356
|
+
|
|
357
|
+
test.prop([fc.string({ minLength: 1, maxLength: 100 })])(
|
|
358
|
+
"Buffer and string input should produce equivalent results",
|
|
359
|
+
(content) => {
|
|
360
|
+
matter.clearCache();
|
|
361
|
+
const fromString = matter(content);
|
|
362
|
+
matter.clearCache();
|
|
363
|
+
const fromBuffer = matter(Buffer.from(content));
|
|
364
|
+
|
|
365
|
+
expect(fromString.content).toBe(fromBuffer.content);
|
|
366
|
+
expect(fromString.data).toEqual(fromBuffer.data);
|
|
367
|
+
expect(fromString.excerpt).toBe(fromBuffer.excerpt);
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
test.prop([yamlSafeObject, fc.string({ minLength: 0, maxLength: 50 })])(
|
|
372
|
+
"parse then stringify should preserve data",
|
|
373
|
+
(data, content) => {
|
|
374
|
+
const frontMatter = Object.entries(data)
|
|
375
|
+
.map(([k, v]) => `${k}: ${typeof v === "string" ? JSON.stringify(v) : v}`)
|
|
376
|
+
.join("\n");
|
|
377
|
+
const input = `---\n${frontMatter}\n---\n${content}`;
|
|
378
|
+
|
|
379
|
+
matter.clearCache();
|
|
380
|
+
const parsed = matter(input);
|
|
381
|
+
const stringified = matter.stringify(parsed);
|
|
382
|
+
matter.clearCache();
|
|
383
|
+
const reparsed = matter(stringified);
|
|
384
|
+
|
|
385
|
+
expect(reparsed.data).toEqual(parsed.data);
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
test.prop([fc.string({ minLength: 0, maxLength: 100 })])(
|
|
390
|
+
"content without front matter should be preserved",
|
|
391
|
+
(content) => {
|
|
392
|
+
const safeContent = content.replace(/^---/gm, "___");
|
|
393
|
+
matter.clearCache();
|
|
394
|
+
const result = matter(safeContent);
|
|
395
|
+
|
|
396
|
+
expect(result.content).toBe(safeContent);
|
|
397
|
+
expect(result.data).toEqual({});
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
test.prop([
|
|
402
|
+
yamlSafeObject,
|
|
403
|
+
fc.string({ minLength: 0, maxLength: 50 }),
|
|
404
|
+
fc.string({ minLength: 1, maxLength: 50 }),
|
|
405
|
+
])("matter.test should correctly detect front matter", (data, content, noFrontMatter) => {
|
|
406
|
+
const frontMatter = Object.entries(data)
|
|
407
|
+
.map(([k, v]) => `${k}: ${typeof v === "string" ? JSON.stringify(v) : v}`)
|
|
408
|
+
.join("\n");
|
|
409
|
+
const withFrontMatter = `---\n${frontMatter}\n---\n${content}`;
|
|
410
|
+
const withoutFrontMatter = noFrontMatter.replace(/^---/gm, "___");
|
|
411
|
+
|
|
412
|
+
expect(matter.test(withFrontMatter)).toBe(true);
|
|
413
|
+
expect(matter.test(withoutFrontMatter)).toBe(withoutFrontMatter.startsWith("---"));
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test.prop([fc.constantFrom("yaml", "json"), yamlSafeObject, fc.string({ maxLength: 30 })])(
|
|
417
|
+
"should handle different languages",
|
|
418
|
+
(language, data, content) => {
|
|
419
|
+
let frontMatterContent: string;
|
|
420
|
+
if (language === "json") {
|
|
421
|
+
frontMatterContent = JSON.stringify(data);
|
|
422
|
+
} else {
|
|
423
|
+
frontMatterContent = Object.entries(data)
|
|
424
|
+
.map(([k, v]) => `${k}: ${typeof v === "string" ? JSON.stringify(v) : v}`)
|
|
425
|
+
.join("\n");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const input = `---${language}\n${frontMatterContent}\n---\n${content}`;
|
|
429
|
+
matter.clearCache();
|
|
430
|
+
const result = matter(input);
|
|
431
|
+
|
|
432
|
+
expect(result.language).toBe(language);
|
|
433
|
+
expect(result.data).toEqual(data);
|
|
434
|
+
},
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
test.prop([fc.constantFrom("---", "~~~", "***", "+++")])(
|
|
438
|
+
"should handle custom delimiters",
|
|
439
|
+
(delimiter) => {
|
|
440
|
+
const input = `${delimiter}\ntitle: Test\n${delimiter}\ncontent`;
|
|
441
|
+
matter.clearCache();
|
|
442
|
+
const result = matter(input, { delimiters: delimiter });
|
|
443
|
+
|
|
444
|
+
expect(result.data).toEqual({ title: "Test" });
|
|
445
|
+
expect(result.content).toBe("content");
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
test.prop([
|
|
450
|
+
fc.string({ minLength: 1, maxLength: 20 }),
|
|
451
|
+
fc.string({ minLength: 1, maxLength: 20 }),
|
|
452
|
+
])("should extract excerpt with custom separator", (excerptText, contentText) => {
|
|
453
|
+
const separator = "<!-- more -->";
|
|
454
|
+
const safeExcerpt = excerptText.replace(separator, "");
|
|
455
|
+
const safeContent = contentText.replace(separator, "");
|
|
456
|
+
const input = `---\ntitle: Test\n---\n${safeExcerpt}\n${separator}\n${safeContent}`;
|
|
457
|
+
|
|
458
|
+
matter.clearCache();
|
|
459
|
+
const result = matter(input, { excerpt: true, excerpt_separator: separator });
|
|
460
|
+
|
|
461
|
+
expect(result.excerpt).toBe(`${safeExcerpt}\n`);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test.prop([fc.string({ minLength: 0, maxLength: 50 })])(
|
|
465
|
+
"should handle CRLF and LF consistently",
|
|
466
|
+
(content) => {
|
|
467
|
+
const yamlData = "title: Test";
|
|
468
|
+
const inputLF = `---\n${yamlData}\n---\n${content}`;
|
|
469
|
+
const inputCRLF = `---\r\n${yamlData}\r\n---\r\n${content}`;
|
|
470
|
+
|
|
471
|
+
matter.clearCache();
|
|
472
|
+
const resultLF = matter(inputLF);
|
|
473
|
+
matter.clearCache();
|
|
474
|
+
const resultCRLF = matter(inputCRLF);
|
|
475
|
+
|
|
476
|
+
expect(resultLF.data).toEqual(resultCRLF.data);
|
|
477
|
+
expect(resultLF.content).toBe(resultCRLF.content);
|
|
478
|
+
},
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
}
|
package/src/parse.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type BuiltinLanguage, getEngine } from "./engines.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse front matter string using the specified language engine
|
|
5
|
+
*/
|
|
6
|
+
export function parse(language: BuiltinLanguage, str: string): Record<string, unknown> {
|
|
7
|
+
const engine = getEngine(language);
|
|
8
|
+
return engine.parse(str);
|
|
9
|
+
}
|
package/src/stringify.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { defaults } from "./defaults.ts";
|
|
2
|
+
import { getEngine, toBuiltinLanguage } from "./engines.ts";
|
|
3
|
+
import type { GrayMatterFile, GrayMatterOptions } from "./types.ts";
|
|
4
|
+
import { isObject } from "./utils.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Type guard for GrayMatterFile
|
|
8
|
+
*/
|
|
9
|
+
function isGrayMatterFile(val: unknown): val is GrayMatterFile {
|
|
10
|
+
return isObject(val) && "content" in val && "data" in val;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Ensure string ends with newline
|
|
15
|
+
*/
|
|
16
|
+
function newline(str: string): string {
|
|
17
|
+
return str.slice(-1) !== "\n" ? str + "\n" : str;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Stringify file object to string with front matter
|
|
22
|
+
*/
|
|
23
|
+
export function stringify(
|
|
24
|
+
file: GrayMatterFile | string,
|
|
25
|
+
data?: Record<string, unknown> | null,
|
|
26
|
+
options?: GrayMatterOptions,
|
|
27
|
+
): string {
|
|
28
|
+
if (data == null && options == null) {
|
|
29
|
+
if (isGrayMatterFile(file)) {
|
|
30
|
+
data = file.data;
|
|
31
|
+
options = {};
|
|
32
|
+
} else if (typeof file === "string") {
|
|
33
|
+
return file;
|
|
34
|
+
} else {
|
|
35
|
+
throw new TypeError("expected file to be a string or object");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!isGrayMatterFile(file)) {
|
|
40
|
+
throw new TypeError("expected file to be a string or object");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const str = file.content;
|
|
44
|
+
const opts = defaults(options);
|
|
45
|
+
|
|
46
|
+
if (data == null) {
|
|
47
|
+
if (!opts.data) return str;
|
|
48
|
+
data = opts.data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const language = toBuiltinLanguage(file.language || opts.language);
|
|
52
|
+
const engine = getEngine(language);
|
|
53
|
+
|
|
54
|
+
data = { ...file.data, ...data };
|
|
55
|
+
const open = opts.delimiters[0];
|
|
56
|
+
const close = opts.delimiters[1];
|
|
57
|
+
const matter = engine.stringify!(data).trim();
|
|
58
|
+
let buf = "";
|
|
59
|
+
|
|
60
|
+
if (matter !== "{}") {
|
|
61
|
+
buf = newline(open) + newline(matter) + newline(close);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof file.excerpt === "string" && file.excerpt !== "") {
|
|
65
|
+
if (str.indexOf(file.excerpt.trim()) === -1) {
|
|
66
|
+
buf += newline(file.excerpt) + newline(close);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return buf + newline(str);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (import.meta.vitest) {
|
|
74
|
+
const { fc, test } = await import("@fast-check/vitest");
|
|
75
|
+
|
|
76
|
+
describe("stringify", () => {
|
|
77
|
+
it("should stringify file object with data", () => {
|
|
78
|
+
const file = {
|
|
79
|
+
content: "hello world",
|
|
80
|
+
data: { title: "Test" },
|
|
81
|
+
excerpt: "",
|
|
82
|
+
orig: Buffer.from(""),
|
|
83
|
+
language: "yaml",
|
|
84
|
+
matter: "",
|
|
85
|
+
isEmpty: false,
|
|
86
|
+
stringify: () => "",
|
|
87
|
+
};
|
|
88
|
+
const result = stringify(file);
|
|
89
|
+
expect(result).toContain("---");
|
|
90
|
+
expect(result).toContain("title: Test");
|
|
91
|
+
expect(result).toContain("hello world");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should return string as-is when only string provided", () => {
|
|
95
|
+
expect(stringify("hello")).toBe("hello");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should throw for invalid input", () => {
|
|
99
|
+
expect(() => stringify(123 as unknown as string)).toThrow(TypeError);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should ensure trailing newline", () => {
|
|
103
|
+
const file = {
|
|
104
|
+
content: "no newline",
|
|
105
|
+
data: { key: "value" },
|
|
106
|
+
excerpt: "",
|
|
107
|
+
orig: Buffer.from(""),
|
|
108
|
+
language: "yaml",
|
|
109
|
+
matter: "",
|
|
110
|
+
isEmpty: false,
|
|
111
|
+
stringify: () => "",
|
|
112
|
+
};
|
|
113
|
+
const result = stringify(file);
|
|
114
|
+
expect(result.endsWith("\n")).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should not add front matter for empty data", () => {
|
|
118
|
+
const file = {
|
|
119
|
+
content: "content only",
|
|
120
|
+
data: {},
|
|
121
|
+
excerpt: "",
|
|
122
|
+
orig: Buffer.from(""),
|
|
123
|
+
language: "yaml",
|
|
124
|
+
matter: "",
|
|
125
|
+
isEmpty: false,
|
|
126
|
+
stringify: () => "",
|
|
127
|
+
};
|
|
128
|
+
const result = stringify(file, {});
|
|
129
|
+
expect(result).toBe("content only\n");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should include excerpt when present and not in content", () => {
|
|
133
|
+
const file = {
|
|
134
|
+
content: "main content",
|
|
135
|
+
data: { title: "Test" },
|
|
136
|
+
excerpt: "This is excerpt",
|
|
137
|
+
orig: Buffer.from(""),
|
|
138
|
+
language: "yaml",
|
|
139
|
+
matter: "",
|
|
140
|
+
isEmpty: false,
|
|
141
|
+
stringify: () => "",
|
|
142
|
+
};
|
|
143
|
+
const result = stringify(file);
|
|
144
|
+
expect(result).toContain("This is excerpt");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test.prop([
|
|
148
|
+
fc.record({
|
|
149
|
+
title: fc.string({ minLength: 1, maxLength: 50 }),
|
|
150
|
+
count: fc.integer({ min: 0, max: 1000 }),
|
|
151
|
+
enabled: fc.boolean(),
|
|
152
|
+
}),
|
|
153
|
+
])("should always produce string ending with newline for any data", (data) => {
|
|
154
|
+
const file = {
|
|
155
|
+
content: "test content",
|
|
156
|
+
data,
|
|
157
|
+
excerpt: "",
|
|
158
|
+
orig: Buffer.from(""),
|
|
159
|
+
language: "yaml",
|
|
160
|
+
matter: "",
|
|
161
|
+
isEmpty: false,
|
|
162
|
+
stringify: () => "",
|
|
163
|
+
};
|
|
164
|
+
const result = stringify(file);
|
|
165
|
+
expect(result.endsWith("\n")).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test.prop([fc.string({ minLength: 0, maxLength: 100 })])(
|
|
169
|
+
"should handle any content string",
|
|
170
|
+
(content) => {
|
|
171
|
+
const file = {
|
|
172
|
+
content,
|
|
173
|
+
data: { key: "value" },
|
|
174
|
+
excerpt: "",
|
|
175
|
+
orig: Buffer.from(""),
|
|
176
|
+
language: "yaml",
|
|
177
|
+
matter: "",
|
|
178
|
+
isEmpty: false,
|
|
179
|
+
stringify: () => "",
|
|
180
|
+
};
|
|
181
|
+
const result = stringify(file);
|
|
182
|
+
expect(typeof result).toBe("string");
|
|
183
|
+
expect(result.endsWith("\n")).toBe(true);
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
}
|