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
@@ -0,0 +1,85 @@
1
+ //#region src/types.d.ts
2
+ /**
3
+ * Engine interface for parsing and stringifying front matter
4
+ */
5
+ interface Engine {
6
+ parse: (str: string) => Record<string, unknown>;
7
+ stringify?: (data: Record<string, unknown>) => string;
8
+ }
9
+ /**
10
+ * Options for gray-matter
11
+ */
12
+ interface GrayMatterOptions {
13
+ /** Language to use for parsing (default: 'yaml') */
14
+ language?: "yaml" | "json";
15
+ /** Delimiters for front matter (default: '---') */
16
+ delimiters?: string | [string, string];
17
+ /**
18
+ * Extract an excerpt from the content.
19
+ * - `true`: use the default delimiter
20
+ * - `string`: use this string as the delimiter
21
+ * - `function`: custom excerpt function
22
+ */
23
+ excerpt?: boolean | string | ((file: GrayMatterFile, options: GrayMatterOptions) => void);
24
+ /** Separator for excerpt in file.data */
25
+ excerpt_separator?: string;
26
+ /** Data to merge with parsed data */
27
+ data?: Record<string, unknown>;
28
+ }
29
+ /**
30
+ * Resolved options with defaults applied
31
+ */
32
+ interface ResolvedOptions extends GrayMatterOptions {
33
+ delimiters: [string, string];
34
+ language: "yaml" | "json";
35
+ }
36
+ /**
37
+ * The file object returned by gray-matter
38
+ */
39
+ interface GrayMatterFile {
40
+ /** The parsed front matter data */
41
+ data: Record<string, unknown>;
42
+ /** The content after front matter */
43
+ content: string;
44
+ /** The extracted excerpt (if enabled) */
45
+ excerpt: string;
46
+ /** The original input as a Buffer */
47
+ orig: Buffer;
48
+ /** The detected/specified language */
49
+ language: string;
50
+ /** The raw front matter string (without delimiters) */
51
+ matter: string;
52
+ /** True if front matter block was empty */
53
+ isEmpty: boolean;
54
+ /** The original content if isEmpty is true */
55
+ empty?: string;
56
+ /** File path (set by matter.read) */
57
+ path?: string;
58
+ /** Stringify the file back to a string */
59
+ stringify: (data?: Record<string, unknown>, options?: GrayMatterOptions) => string;
60
+ }
61
+ /**
62
+ * Input that can be passed to gray-matter
63
+ */
64
+ type GrayMatterInput = string | Buffer | {
65
+ content: string;
66
+ data?: Record<string, unknown>;
67
+ };
68
+ /**
69
+ * The matter function interface with static methods
70
+ */
71
+ interface MatterFunction {
72
+ (input: GrayMatterInput, options?: GrayMatterOptions): GrayMatterFile;
73
+ stringify: (file: GrayMatterFile | string, data?: Record<string, unknown>, options?: GrayMatterOptions) => string;
74
+ read: (filepath: string, options?: GrayMatterOptions) => GrayMatterFile;
75
+ test: (str: string, options?: GrayMatterOptions) => boolean;
76
+ language: (str: string, options?: GrayMatterOptions) => {
77
+ raw: string;
78
+ name: string;
79
+ };
80
+ clearCache: () => void;
81
+ cache: Map<string, GrayMatterFile>;
82
+ }
83
+ //#endregion
84
+ export { Engine, GrayMatterFile, GrayMatterInput, GrayMatterOptions, MatterFunction, ResolvedOptions };
85
+ //# sourceMappingURL=types.d.mts.map
package/dist/utils.mjs ADDED
@@ -0,0 +1,60 @@
1
+ import { Buffer } from "node:buffer";
2
+
3
+ //#region src/utils.ts
4
+ /**
5
+ * Strip BOM (Byte Order Mark) from a string
6
+ */
7
+ function stripBom(str) {
8
+ return str.charCodeAt(0) === 65279 ? str.slice(1) : str;
9
+ }
10
+ /**
11
+ * Returns true if `val` is a Buffer
12
+ */
13
+ function isBuffer(val) {
14
+ return Buffer.isBuffer(val);
15
+ }
16
+ /**
17
+ * Returns true if `val` is a plain object (not a Buffer or other special object)
18
+ */
19
+ function isObject(val) {
20
+ return typeof val === "object" && val !== null && !Array.isArray(val) && !Buffer.isBuffer(val);
21
+ }
22
+ /**
23
+ * Cast `input` to a Buffer
24
+ */
25
+ function toBuffer(input) {
26
+ return typeof input === "string" ? Buffer.from(input) : input;
27
+ }
28
+ /**
29
+ * Cast `input` to a string, stripping BOM
30
+ */
31
+ function toString(input) {
32
+ if (isBuffer(input)) return stripBom(String(input));
33
+ if (typeof input !== "string") throw new TypeError("expected input to be a string or buffer");
34
+ return stripBom(input);
35
+ }
36
+ /**
37
+ * Cast `val` to an array
38
+ */
39
+ function arrayify(val) {
40
+ return val ? Array.isArray(val) ? val : [val] : [];
41
+ }
42
+ /**
43
+ * Asserts that `val` is a plain object and returns it typed as Record<string, unknown>
44
+ * If `val` is not a plain object, returns an empty object
45
+ */
46
+ function toRecord(val) {
47
+ return isObject(val) ? val : {};
48
+ }
49
+ /**
50
+ * Get a string property from an object with a default value
51
+ */
52
+ function getStringProp(obj, key, defaultValue = "") {
53
+ if (!isObject(obj)) return defaultValue;
54
+ const value = obj[key];
55
+ return typeof value === "string" ? value : defaultValue;
56
+ }
57
+
58
+ //#endregion
59
+ export { arrayify, getStringProp, isObject, toBuffer, toRecord, toString };
60
+ //# sourceMappingURL=utils.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.mjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import { Buffer } from \"node:buffer\";\n\n/**\n * Strip BOM (Byte Order Mark) from a string\n */\nfunction stripBom(str: string): string {\n return str.charCodeAt(0) === 0xfeff ? str.slice(1) : str;\n}\n\n/**\n * Returns true if `val` is a Buffer\n */\nfunction isBuffer(val: unknown): val is Buffer {\n return Buffer.isBuffer(val);\n}\n\n/**\n * Returns true if `val` is a plain object (not a Buffer or other special object)\n */\nexport function isObject(val: unknown): val is Record<string, unknown> {\n return typeof val === \"object\" && val !== null && !Array.isArray(val) && !Buffer.isBuffer(val);\n}\n\n/**\n * Cast `input` to a Buffer\n */\nexport function toBuffer(input: string | Buffer): Buffer {\n return typeof input === \"string\" ? Buffer.from(input) : input;\n}\n\n/**\n * Cast `input` to a string, stripping BOM\n */\nexport function toString(input: string | Buffer): string {\n if (isBuffer(input)) return stripBom(String(input));\n if (typeof input !== \"string\") {\n throw new TypeError(\"expected input to be a string or buffer\");\n }\n return stripBom(input);\n}\n\n/**\n * Cast `val` to an array\n */\nexport function arrayify<T>(val: T | T[] | undefined | null): T[] {\n return val ? (Array.isArray(val) ? val : [val]) : [];\n}\n\n/**\n * Asserts that `val` is a plain object and returns it typed as Record<string, unknown>\n * If `val` is not a plain object, returns an empty object\n */\nexport function toRecord(val: unknown): Record<string, unknown> {\n return isObject(val) ? val : {};\n}\n\n/**\n * Get a string property from an object with a default value\n */\nexport function getStringProp(obj: unknown, key: string, defaultValue = \"\"): string {\n if (!isObject(obj)) return defaultValue;\n const value = obj[key];\n return typeof value === \"string\" ? value : defaultValue;\n}\n\nif (import.meta.vitest) {\n describe(\"utils\", () => {\n describe(\"toRecord\", () => {\n it(\"should return object as-is\", () => {\n const obj = { a: 1, b: \"hello\" };\n expect(toRecord(obj)).toBe(obj);\n });\n\n it(\"should return empty object for non-objects\", () => {\n expect(toRecord(null)).toEqual({});\n expect(toRecord(undefined)).toEqual({});\n expect(toRecord(\"string\")).toEqual({});\n expect(toRecord(123)).toEqual({});\n expect(toRecord([])).toEqual({});\n });\n });\n\n describe(\"getStringProp\", () => {\n it(\"should return string property value\", () => {\n expect(getStringProp({ name: \"test\" }, \"name\")).toBe(\"test\");\n });\n\n it(\"should return default for missing property\", () => {\n expect(getStringProp({ other: \"value\" }, \"name\")).toBe(\"\");\n expect(getStringProp({ other: \"value\" }, \"name\", \"default\")).toBe(\"default\");\n });\n\n it(\"should return default for non-string property\", () => {\n expect(getStringProp({ count: 42 }, \"count\")).toBe(\"\");\n expect(getStringProp({ flag: true }, \"flag\")).toBe(\"\");\n });\n\n it(\"should return default for non-objects\", () => {\n expect(getStringProp(null, \"name\")).toBe(\"\");\n expect(getStringProp(\"string\", \"name\")).toBe(\"\");\n });\n });\n\n describe(\"stripBom\", () => {\n it(\"should strip BOM from string\", () => {\n expect(stripBom(\"\\uFEFFhello\")).toBe(\"hello\");\n });\n\n it(\"should return string unchanged if no BOM\", () => {\n expect(stripBom(\"hello\")).toBe(\"hello\");\n });\n });\n\n describe(\"isBuffer\", () => {\n it(\"should return true for Buffer\", () => {\n expect(isBuffer(Buffer.from(\"test\"))).toBe(true);\n });\n\n it(\"should return false for string\", () => {\n expect(isBuffer(\"test\")).toBe(false);\n });\n });\n\n describe(\"isObject\", () => {\n it(\"should return true for plain object\", () => {\n expect(isObject({})).toBe(true);\n expect(isObject({ a: 1 })).toBe(true);\n });\n\n it(\"should return false for array\", () => {\n expect(isObject([])).toBe(false);\n });\n\n it(\"should return false for null\", () => {\n expect(isObject(null)).toBe(false);\n });\n\n it(\"should return false for Buffer\", () => {\n expect(isObject(Buffer.from(\"test\"))).toBe(false);\n });\n });\n\n describe(\"arrayify\", () => {\n it(\"should wrap non-array in array\", () => {\n expect(arrayify(\"test\")).toEqual([\"test\"]);\n });\n\n it(\"should return array unchanged\", () => {\n expect(arrayify([\"a\", \"b\"])).toEqual([\"a\", \"b\"]);\n });\n\n it(\"should return empty array for null/undefined\", () => {\n expect(arrayify(null)).toEqual([]);\n expect(arrayify(undefined)).toEqual([]);\n });\n });\n });\n}\n"],"mappings":";;;;;;AAKA,SAAS,SAAS,KAAqB;AACrC,QAAO,IAAI,WAAW,EAAE,KAAK,QAAS,IAAI,MAAM,EAAE,GAAG;;;;;AAMvD,SAAS,SAAS,KAA6B;AAC7C,QAAO,OAAO,SAAS,IAAI;;;;;AAM7B,SAAgB,SAAS,KAA8C;AACrE,QAAO,OAAO,QAAQ,YAAY,QAAQ,QAAQ,CAAC,MAAM,QAAQ,IAAI,IAAI,CAAC,OAAO,SAAS,IAAI;;;;;AAMhG,SAAgB,SAAS,OAAgC;AACvD,QAAO,OAAO,UAAU,WAAW,OAAO,KAAK,MAAM,GAAG;;;;;AAM1D,SAAgB,SAAS,OAAgC;AACvD,KAAI,SAAS,MAAM,CAAE,QAAO,SAAS,OAAO,MAAM,CAAC;AACnD,KAAI,OAAO,UAAU,SACnB,OAAM,IAAI,UAAU,0CAA0C;AAEhE,QAAO,SAAS,MAAM;;;;;AAMxB,SAAgB,SAAY,KAAsC;AAChE,QAAO,MAAO,MAAM,QAAQ,IAAI,GAAG,MAAM,CAAC,IAAI,GAAI,EAAE;;;;;;AAOtD,SAAgB,SAAS,KAAuC;AAC9D,QAAO,SAAS,IAAI,GAAG,MAAM,EAAE;;;;;AAMjC,SAAgB,cAAc,KAAc,KAAa,eAAe,IAAY;AAClF,KAAI,CAAC,SAAS,IAAI,CAAE,QAAO;CAC3B,MAAM,QAAQ,IAAI;AAClB,QAAO,OAAO,UAAU,WAAW,QAAQ"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "gray-matter-es",
3
+ "version": "0.0.0",
4
+ "description": "ES-only gray-matter",
5
+ "keywords": [
6
+ "esm",
7
+ "front-matter",
8
+ "gray-matter",
9
+ "yaml"
10
+ ],
11
+ "homepage": "https://github.com/ryoppippi/gray-matter-es#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/ryoppippi/gray-matter-es/issues"
14
+ },
15
+ "license": "MIT",
16
+ "author": "ryoppippi",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/ryoppippi/gray-matter-es.git"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src"
24
+ ],
25
+ "type": "module",
26
+ "types": "./dist/index.d.mts",
27
+ "exports": {
28
+ ".": "./dist/index.mjs",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "devDependencies": {
32
+ "@fast-check/vitest": "^0.2.0",
33
+ "@std/yaml": "npm:@jsr/std__yaml@^1",
34
+ "@types/node": "^24.0.3",
35
+ "bumpp": "^10.2.3",
36
+ "changelogithub": "^13.16.1",
37
+ "knip": "^5.81.0",
38
+ "publint": "^0.3.5",
39
+ "tsdown": "^0.19.0",
40
+ "unplugin-unused": "^0.5.0",
41
+ "vitest": "^4.0.15"
42
+ },
43
+ "engines": {
44
+ "node": ">=20.19.6"
45
+ },
46
+ "scripts": {
47
+ "build": "tsdown",
48
+ "test": "vitest",
49
+ "typecheck": "tsgo --noEmit",
50
+ "lint": "pnpm --aggregate-output run '/^lint:/'",
51
+ "lint:oxlint": "oxlint --max-warnings=0",
52
+ "lint:oxfmt": "oxfmt --no-error-on-unmatched-pattern --check .",
53
+ "lint:knip": "knip",
54
+ "format": "pnpm --no-bail --aggregate-output run '/^format:/'",
55
+ "format:oxlint": "oxlint --max-warnings=0 --fix",
56
+ "format:oxfmt": "oxfmt --no-error-on-unmatched-pattern .",
57
+ "format:knip": "knip --fix --no-exit-code",
58
+ "prerelease": "pnpm run lint && pnpm run build",
59
+ "release": "bumpp"
60
+ }
61
+ }
@@ -0,0 +1,17 @@
1
+ import type { GrayMatterOptions, ResolvedOptions } from "./types.ts";
2
+ import { arrayify } from "./utils.ts";
3
+
4
+ /**
5
+ * Apply default options
6
+ */
7
+ export function defaults(options?: GrayMatterOptions): ResolvedOptions {
8
+ const delims = arrayify(options?.delimiters ?? "---");
9
+ const delimiters: [string, string] =
10
+ delims.length === 1 ? [delims[0]!, delims[0]!] : [delims[0]!, delims[1]!];
11
+
12
+ return {
13
+ ...options,
14
+ delimiters,
15
+ language: options?.language ?? "yaml",
16
+ };
17
+ }
package/src/engines.ts ADDED
@@ -0,0 +1,217 @@
1
+ import { parse as yamlParse, stringify as yamlStringify } from "@std/yaml";
2
+ import type { Engine, GrayMatterOptions } from "./types.ts";
3
+ import { toRecord } from "./utils.ts";
4
+
5
+ /**
6
+ * Built-in language names
7
+ */
8
+ export type BuiltinLanguage = "yaml" | "json";
9
+
10
+ /**
11
+ * Array of built-in language names for runtime validation
12
+ */
13
+ const BUILTIN_LANGUAGES = ["yaml", "json"] as const satisfies readonly BuiltinLanguage[];
14
+
15
+ /**
16
+ * Check if value is a built-in language name
17
+ */
18
+ function isBuiltinLanguage(value: unknown): value is BuiltinLanguage {
19
+ return typeof value === "string" && BUILTIN_LANGUAGES.includes(value as BuiltinLanguage);
20
+ }
21
+
22
+ /**
23
+ * Assert that value is a built-in language, returning the default if not
24
+ */
25
+ export function toBuiltinLanguage(
26
+ value: unknown,
27
+ defaultLang: BuiltinLanguage = "yaml",
28
+ ): BuiltinLanguage {
29
+ return isBuiltinLanguage(value) ? value : defaultLang;
30
+ }
31
+
32
+ /**
33
+ * YAML engine using @std/yaml
34
+ */
35
+ const yaml = {
36
+ parse: (str: string): Record<string, unknown> => {
37
+ return toRecord(yamlParse(str));
38
+ },
39
+ stringify: (data: Record<string, unknown>): string => {
40
+ return yamlStringify(data);
41
+ },
42
+ } as const satisfies Engine;
43
+
44
+ /**
45
+ * JSON engine
46
+ */
47
+ const json = {
48
+ parse: (str: string): Record<string, unknown> => {
49
+ return toRecord(JSON.parse(str));
50
+ },
51
+ stringify: (
52
+ data: Record<string, unknown>,
53
+ options?: GrayMatterOptions & { replacer?: null; space?: number },
54
+ ): string => {
55
+ const opts = { replacer: null, space: 2, ...options };
56
+ return JSON.stringify(data, opts.replacer, opts.space);
57
+ },
58
+ } as const satisfies Engine;
59
+
60
+ /**
61
+ * Get engine by language name
62
+ */
63
+ export function getEngine(language: BuiltinLanguage): Engine {
64
+ switch (language) {
65
+ case "yaml":
66
+ return yaml;
67
+ case "json":
68
+ return json;
69
+ default:
70
+ throw new Error(`Unknown language: ${language satisfies never}`);
71
+ }
72
+ }
73
+
74
+ if (import.meta.vitest) {
75
+ const { fc, test } = await import("@fast-check/vitest");
76
+
77
+ /** Arbitrary for YAML-safe values (no undefined, functions, symbols) */
78
+ const yamlSafeValue = fc.oneof(
79
+ fc.string(),
80
+ fc.integer(),
81
+ fc.double({ noNaN: true, noDefaultInfinity: true }),
82
+ fc.boolean(),
83
+ fc.constant(null),
84
+ );
85
+
86
+ /** Arbitrary for simple YAML-compatible objects */
87
+ const yamlSafeObject = fc.dictionary(
88
+ fc.string({ minLength: 1, maxLength: 20 }).filter((s) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(s)),
89
+ yamlSafeValue,
90
+ { minKeys: 1, maxKeys: 5 },
91
+ );
92
+
93
+ describe("yaml engine", () => {
94
+ it("should parse simple YAML", () => {
95
+ const result = yaml.parse("title: Hello\ncount: 42");
96
+ expect(result).toEqual({ title: "Hello", count: 42 });
97
+ });
98
+
99
+ it("should return empty object for null YAML", () => {
100
+ const result = yaml.parse("null");
101
+ expect(result).toEqual({});
102
+ });
103
+
104
+ it("should stringify object to YAML", () => {
105
+ const result = yaml.stringify({ title: "Test", count: 10 });
106
+ expect(result).toContain("title: Test");
107
+ expect(result).toContain("count: 10");
108
+ });
109
+
110
+ test.prop([yamlSafeObject])("should round-trip parse/stringify for safe objects", (data) => {
111
+ const stringified = yaml.stringify(data);
112
+ const parsed = yaml.parse(stringified);
113
+ expect(parsed).toEqual(data);
114
+ });
115
+
116
+ test.prop([fc.string({ minLength: 1, maxLength: 100 }), fc.integer()])(
117
+ "should preserve string and number types",
118
+ (str, num) => {
119
+ const data = { str, num };
120
+ const stringified = yaml.stringify(data);
121
+ const parsed = yaml.parse(stringified);
122
+ expect(parsed.str).toBe(str);
123
+ expect(parsed.num).toBe(num);
124
+ },
125
+ );
126
+ });
127
+
128
+ describe("json engine", () => {
129
+ it("should parse JSON", () => {
130
+ const result = json.parse('{"title": "Hello", "count": 42}');
131
+ expect(result).toEqual({ title: "Hello", count: 42 });
132
+ });
133
+
134
+ it("should stringify object to JSON", () => {
135
+ const result = json.stringify({ title: "Test" });
136
+ expect(JSON.parse(result)).toEqual({ title: "Test" });
137
+ });
138
+
139
+ it("should throw on invalid JSON", () => {
140
+ expect(() => json.parse("not json")).toThrow();
141
+ });
142
+
143
+ test.prop([
144
+ fc.record({
145
+ title: fc.string(),
146
+ count: fc.integer(),
147
+ active: fc.boolean(),
148
+ }),
149
+ ])("should round-trip parse/stringify", (data) => {
150
+ const stringified = json.stringify(data);
151
+ const parsed = json.parse(stringified);
152
+ expect(parsed).toEqual(data);
153
+ });
154
+
155
+ test.prop([
156
+ fc.oneof(
157
+ fc.string(),
158
+ fc.integer(),
159
+ fc.boolean(),
160
+ fc.constant(null),
161
+ fc.array(fc.oneof(fc.string(), fc.integer(), fc.boolean(), fc.constant(null))),
162
+ ),
163
+ ])("should handle common JSON values", (value) => {
164
+ const wrapped = { value };
165
+ const stringified = json.stringify(wrapped);
166
+ const parsed = json.parse(stringified);
167
+ expect(parsed).toEqual(wrapped);
168
+ });
169
+ });
170
+
171
+ describe("getEngine", () => {
172
+ it("should return yaml engine", () => {
173
+ expect(getEngine("yaml")).toBe(yaml);
174
+ });
175
+
176
+ it("should return json engine", () => {
177
+ expect(getEngine("json")).toBe(json);
178
+ });
179
+ });
180
+
181
+ describe("isBuiltinLanguage", () => {
182
+ it("should return true for yaml", () => {
183
+ expect(isBuiltinLanguage("yaml")).toBe(true);
184
+ });
185
+
186
+ it("should return true for json", () => {
187
+ expect(isBuiltinLanguage("json")).toBe(true);
188
+ });
189
+
190
+ it("should return false for unknown languages", () => {
191
+ expect(isBuiltinLanguage("toml")).toBe(false);
192
+ expect(isBuiltinLanguage("")).toBe(false);
193
+ });
194
+
195
+ it("should return false for non-strings", () => {
196
+ expect(isBuiltinLanguage(null)).toBe(false);
197
+ expect(isBuiltinLanguage(undefined)).toBe(false);
198
+ expect(isBuiltinLanguage(123)).toBe(false);
199
+ });
200
+ });
201
+
202
+ describe("toBuiltinLanguage", () => {
203
+ it("should return valid language as-is", () => {
204
+ expect(toBuiltinLanguage("yaml")).toBe("yaml");
205
+ expect(toBuiltinLanguage("json")).toBe("json");
206
+ });
207
+
208
+ it("should return default for invalid language", () => {
209
+ expect(toBuiltinLanguage("toml")).toBe("yaml");
210
+ expect(toBuiltinLanguage("")).toBe("yaml");
211
+ });
212
+
213
+ it("should use custom default", () => {
214
+ expect(toBuiltinLanguage("toml", "json")).toBe("json");
215
+ });
216
+ });
217
+ }
package/src/excerpt.ts ADDED
@@ -0,0 +1,146 @@
1
+ import { defaults } from "./defaults.ts";
2
+ import type { GrayMatterFile, GrayMatterOptions } from "./types.ts";
3
+ import { getStringProp } from "./utils.ts";
4
+
5
+ /**
6
+ * Extract excerpt from file content
7
+ */
8
+ export function excerpt(file: GrayMatterFile, options?: GrayMatterOptions): GrayMatterFile {
9
+ const opts = defaults(options);
10
+
11
+ // Ensure data is an object (defensive check for external callers)
12
+ file.data ??= {};
13
+
14
+ if (typeof opts.excerpt === "function") {
15
+ opts.excerpt(file, opts);
16
+ return file;
17
+ }
18
+
19
+ const dataSep = getStringProp(file.data, "excerpt_separator");
20
+ const sep = dataSep !== "" ? dataSep : opts.excerpt_separator;
21
+
22
+ if (sep == null && (opts.excerpt === false || opts.excerpt == null)) {
23
+ return file;
24
+ }
25
+
26
+ const delimiter = typeof opts.excerpt === "string" ? opts.excerpt : (sep ?? opts.delimiters[0]);
27
+
28
+ // if enabled, get the excerpt defined after front-matter
29
+ const idx = file.content.indexOf(delimiter);
30
+ if (idx !== -1) {
31
+ file.excerpt = file.content.slice(0, idx);
32
+ }
33
+
34
+ return file;
35
+ }
36
+
37
+ if (import.meta.vitest) {
38
+ const { fc, test } = await import("@fast-check/vitest");
39
+ const { Buffer } = await import("node:buffer");
40
+
41
+ const makeFile = (content: string, data: Record<string, unknown> = {}): GrayMatterFile => ({
42
+ content,
43
+ data,
44
+ excerpt: "",
45
+ orig: Buffer.from(content),
46
+ language: "yaml",
47
+ matter: "",
48
+ isEmpty: false,
49
+ stringify: () => "",
50
+ });
51
+
52
+ describe("excerpt", () => {
53
+ it("should extract excerpt using default delimiter", () => {
54
+ const file = makeFile("This is excerpt\n---\nThis is content");
55
+ const result = excerpt(file, { excerpt: true });
56
+ expect(result.excerpt).toBe("This is excerpt\n");
57
+ });
58
+
59
+ it("should extract excerpt using custom string delimiter", () => {
60
+ const file = makeFile("This is excerpt\n<!-- more -->\nThis is content");
61
+ const result = excerpt(file, { excerpt: "<!-- more -->" });
62
+ expect(result.excerpt).toBe("This is excerpt\n");
63
+ });
64
+
65
+ it("should use excerpt_separator from options", () => {
66
+ const file = makeFile("Excerpt here\n***\nContent here");
67
+ const result = excerpt(file, { excerpt: true, excerpt_separator: "***" });
68
+ expect(result.excerpt).toBe("Excerpt here\n");
69
+ });
70
+
71
+ it("should use excerpt_separator from file.data", () => {
72
+ const file = makeFile("Excerpt\n<!-- end -->\nContent", {
73
+ excerpt_separator: "<!-- end -->",
74
+ });
75
+ const result = excerpt(file, { excerpt: true });
76
+ expect(result.excerpt).toBe("Excerpt\n");
77
+ });
78
+
79
+ it("should not extract when excerpt is false", () => {
80
+ const file = makeFile("Some content\n---\nMore content");
81
+ const result = excerpt(file, { excerpt: false });
82
+ expect(result.excerpt).toBe("");
83
+ });
84
+
85
+ it("should not modify when delimiter not found", () => {
86
+ const file = makeFile("No delimiter here");
87
+ const result = excerpt(file, { excerpt: true });
88
+ expect(result.excerpt).toBe("");
89
+ });
90
+
91
+ it("should call custom excerpt function", () => {
92
+ const file = makeFile("Custom content");
93
+ const customFn = vi.fn((f: GrayMatterFile) => {
94
+ f.excerpt = "Custom excerpt";
95
+ });
96
+ const result = excerpt(file, { excerpt: customFn });
97
+ expect(customFn).toHaveBeenCalledOnce();
98
+ expect(result.excerpt).toBe("Custom excerpt");
99
+ });
100
+
101
+ it("should initialize file.data if null", () => {
102
+ const file = makeFile("content");
103
+ file.data = null as unknown as Record<string, unknown>;
104
+ const result = excerpt(file, {});
105
+ expect(result.data).toEqual({});
106
+ });
107
+
108
+ test.prop([
109
+ fc.string({ minLength: 1, maxLength: 50 }),
110
+ fc.string({ minLength: 1, maxLength: 50 }),
111
+ ])("should extract correct excerpt when delimiter exists", (excerptText, contentText) => {
112
+ const delimiter = "---";
113
+ const content = `${excerptText}\n${delimiter}\n${contentText}`;
114
+ const file = makeFile(content);
115
+ const result = excerpt(file, { excerpt: true });
116
+
117
+ expect(result.excerpt).toBe(`${excerptText}\n`);
118
+ });
119
+
120
+ test.prop([fc.string({ minLength: 1, maxLength: 100 })])(
121
+ "should not extract when no delimiter in content",
122
+ (content) => {
123
+ const safeContent = content.replace(/---/g, "___");
124
+ const file = makeFile(safeContent);
125
+ const result = excerpt(file, { excerpt: true });
126
+ expect(result.excerpt).toBe("");
127
+ },
128
+ );
129
+
130
+ test.prop([
131
+ fc.string({ minLength: 1, maxLength: 20 }),
132
+ fc.string({ minLength: 1, maxLength: 20 }),
133
+ fc
134
+ .string({ minLength: 1, maxLength: 10 })
135
+ .filter((s) => !s.includes("\n") && !/[[\]{}()*+?.,\\^$|#]/.test(s)),
136
+ ])("should work with any custom delimiter", (excerptText, contentText, customDelimiter) => {
137
+ const safeExcerpt = excerptText.replaceAll(customDelimiter, "");
138
+ if (safeExcerpt === "") return;
139
+ const content = `${safeExcerpt}\n${customDelimiter}\n${contentText}`;
140
+ const file = makeFile(content);
141
+ const result = excerpt(file, { excerpt: customDelimiter });
142
+
143
+ expect(result.excerpt).toBe(`${safeExcerpt}\n`);
144
+ });
145
+ });
146
+ }