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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/defaults.mjs +19 -0
  4. package/dist/defaults.mjs.map +1 -0
  5. package/dist/engines.d.mts +8 -0
  6. package/dist/engines.mjs +63 -0
  7. package/dist/engines.mjs.map +1 -0
  8. package/dist/excerpt.mjs +26 -0
  9. package/dist/excerpt.mjs.map +1 -0
  10. package/dist/index.d.mts +12 -0
  11. package/dist/index.mjs +126 -0
  12. package/dist/index.mjs.map +1 -0
  13. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_chars.mjs +45 -0
  14. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_chars.mjs.map +1 -0
  15. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_dumper_state.mjs +437 -0
  16. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_dumper_state.mjs.map +1 -0
  17. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_loader_state.mjs +909 -0
  18. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_loader_state.mjs.map +1 -0
  19. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_schema.mjs +115 -0
  20. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_schema.mjs.map +1 -0
  21. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/binary.mjs +89 -0
  22. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/binary.mjs.map +1 -0
  23. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/bool.mjs +35 -0
  24. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/bool.mjs.map +1 -0
  25. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/float.mjs +55 -0
  26. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/float.mjs.map +1 -0
  27. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/int.mjs +114 -0
  28. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/int.mjs.map +1 -0
  29. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/map.mjs +15 -0
  30. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/map.mjs.map +1 -0
  31. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/merge.mjs +11 -0
  32. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/merge.mjs.map +1 -0
  33. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/nil.mjs +20 -0
  34. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/nil.mjs.map +1 -0
  35. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/omap.mjs +28 -0
  36. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/omap.mjs.map +1 -0
  37. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/pairs.mjs +19 -0
  38. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/pairs.mjs.map +1 -0
  39. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/regexp.mjs +26 -0
  40. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/regexp.mjs.map +1 -0
  41. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/seq.mjs +11 -0
  42. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/seq.mjs.map +1 -0
  43. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/set.mjs +14 -0
  44. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/set.mjs.map +1 -0
  45. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/str.mjs +11 -0
  46. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/str.mjs.map +1 -0
  47. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/timestamp.mjs +54 -0
  48. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/timestamp.mjs.map +1 -0
  49. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/undefined.mjs +19 -0
  50. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_type/undefined.mjs.map +1 -0
  51. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_utils.mjs +14 -0
  52. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/_utils.mjs.map +1 -0
  53. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/mod.mjs +4 -0
  54. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/parse.mjs +50 -0
  55. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/parse.mjs.map +1 -0
  56. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/stringify.mjs +32 -0
  57. package/dist/node_modules/.pnpm/@jsr_std__yaml@1.0.10/node_modules/@jsr/std__yaml/stringify.mjs.map +1 -0
  58. package/dist/parse.mjs +13 -0
  59. package/dist/parse.mjs.map +1 -0
  60. package/dist/stringify.mjs +52 -0
  61. package/dist/stringify.mjs.map +1 -0
  62. package/dist/to-file.mjs +44 -0
  63. package/dist/to-file.mjs.map +1 -0
  64. package/dist/types.d.mts +85 -0
  65. package/dist/utils.mjs +60 -0
  66. package/dist/utils.mjs.map +1 -0
  67. package/package.json +61 -0
  68. package/src/defaults.ts +17 -0
  69. package/src/engines.ts +217 -0
  70. package/src/excerpt.ts +146 -0
  71. package/src/index.ts +481 -0
  72. package/src/parse.ts +9 -0
  73. package/src/stringify.ts +187 -0
  74. package/src/to-file.ts +178 -0
  75. package/src/types.ts +84 -0
  76. 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
+ }
@@ -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
+ }