@oh-my-pi/pi-utils 17.2.10 → 17.2.11
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/CHANGELOG.md +10 -0
- package/dist/types/frontmatter.d.ts +21 -0
- package/package.json +2 -2
- package/src/frontmatter.ts +44 -14
- package/src/marked/core.ts +17 -17
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.2.11] - 2026-08-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `repair` and `rawKeys` options to `parseFrontmatter` to support spec-conformant loading (disabling lenient recovery and preserving keys verbatim), and exported `normalizeFrontmatterKeys` for manual key normalization.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed the in-house `marked` list tokenizer incorrectly consuming trailing blank lines at the end of input, ensuring correct list tightness and token generation matching standard `marked` behavior.
|
|
14
|
+
|
|
5
15
|
## [17.2.10] - 2026-08-06
|
|
6
16
|
|
|
7
17
|
### Added
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively normalize object keys from kebab-case to camelCase — the
|
|
3
|
+
* representation convention for frontmatter consumed inside this codebase.
|
|
4
|
+
* Exported for loaders that parse with `rawKeys: true` to validate exact
|
|
5
|
+
* spec-defined keys, then normalize for storage.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeFrontmatterKeys<T>(obj: T): T;
|
|
1
8
|
export declare class FrontmatterError extends Error {
|
|
2
9
|
readonly source?: unknown;
|
|
3
10
|
constructor(error: Error, source?: unknown);
|
|
@@ -14,6 +21,20 @@ export interface FrontmatterOptions {
|
|
|
14
21
|
normalize?: boolean;
|
|
15
22
|
/** Level of error handling */
|
|
16
23
|
level?: "off" | "warn" | "fatal";
|
|
24
|
+
/**
|
|
25
|
+
* Attempt lenient recovery of near-miss input before failing: quote
|
|
26
|
+
* ambiguous plain scalars, replace tabs with spaces, and strip leading HTML
|
|
27
|
+
* comments ahead of the opening delimiter. Default `true`. Spec-conformant
|
|
28
|
+
* loaders set `false` so malformed input is rejected instead of silently
|
|
29
|
+
* repaired (CRLF newline normalization still applies).
|
|
30
|
+
*/
|
|
31
|
+
repair?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Preserve frontmatter keys verbatim instead of normalizing kebab-case to
|
|
34
|
+
* camelCase. Default `false`. Strict spec loaders use this so a standard
|
|
35
|
+
* key (e.g. `allowed-tools`) is never aliased with its camelCase form.
|
|
36
|
+
*/
|
|
37
|
+
rawKeys?: boolean;
|
|
17
38
|
}
|
|
18
39
|
/**
|
|
19
40
|
* Parse YAML frontmatter from markdown content
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "17.2.
|
|
4
|
+
"version": "17.2.11",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "17.2.
|
|
34
|
+
"@oh-my-pi/pi-natives": "17.2.11"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/bun": "^1.3.14"
|
package/src/frontmatter.ts
CHANGED
|
@@ -12,15 +12,20 @@ function kebabToCamel(key: string): string {
|
|
|
12
12
|
return key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Recursively normalize object keys from kebab-case to camelCase — the
|
|
17
|
+
* representation convention for frontmatter consumed inside this codebase.
|
|
18
|
+
* Exported for loaders that parse with `rawKeys: true` to validate exact
|
|
19
|
+
* spec-defined keys, then normalize for storage.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeFrontmatterKeys<T>(obj: T): T {
|
|
17
22
|
if (obj === null || typeof obj !== "object") return obj;
|
|
18
23
|
if (Array.isArray(obj)) {
|
|
19
24
|
let changed = false;
|
|
20
25
|
const out: unknown[] = new Array(obj.length);
|
|
21
26
|
for (let i = 0; i < obj.length; i++) {
|
|
22
27
|
const v = obj[i];
|
|
23
|
-
const nv =
|
|
28
|
+
const nv = normalizeFrontmatterKeys(v);
|
|
24
29
|
out[i] = nv;
|
|
25
30
|
if (nv !== v) changed = true;
|
|
26
31
|
}
|
|
@@ -30,7 +35,7 @@ function normalizeKeys<T>(obj: T): T {
|
|
|
30
35
|
const result: Record<string, unknown> = {};
|
|
31
36
|
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
32
37
|
const nk = key.includes("-") ? kebabToCamel(key) : key;
|
|
33
|
-
const nv =
|
|
38
|
+
const nv = normalizeFrontmatterKeys(value);
|
|
34
39
|
result[nk] = nv;
|
|
35
40
|
if (nk !== key || nv !== value) changed = true;
|
|
36
41
|
}
|
|
@@ -55,8 +60,8 @@ function quoteAmbiguousPlainScalars(metadata: string): string | undefined {
|
|
|
55
60
|
return changed ? lines.join("\n") : undefined;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
function parseYamlRecord(metadata: string): Record<string, unknown> | null {
|
|
59
|
-
const loaded = YAML.parse(metadata.replaceAll("\t", " "));
|
|
63
|
+
function parseYamlRecord(metadata: string, repairTabs: boolean): Record<string, unknown> | null {
|
|
64
|
+
const loaded = YAML.parse(repairTabs ? metadata.replaceAll("\t", " ") : metadata);
|
|
60
65
|
if (loaded === null || loaded === undefined) return null;
|
|
61
66
|
if (typeof loaded !== "object" || Array.isArray(loaded)) return null;
|
|
62
67
|
return loaded as Record<string, unknown>;
|
|
@@ -97,6 +102,20 @@ export interface FrontmatterOptions {
|
|
|
97
102
|
normalize?: boolean;
|
|
98
103
|
/** Level of error handling */
|
|
99
104
|
level?: "off" | "warn" | "fatal";
|
|
105
|
+
/**
|
|
106
|
+
* Attempt lenient recovery of near-miss input before failing: quote
|
|
107
|
+
* ambiguous plain scalars, replace tabs with spaces, and strip leading HTML
|
|
108
|
+
* comments ahead of the opening delimiter. Default `true`. Spec-conformant
|
|
109
|
+
* loaders set `false` so malformed input is rejected instead of silently
|
|
110
|
+
* repaired (CRLF newline normalization still applies).
|
|
111
|
+
*/
|
|
112
|
+
repair?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Preserve frontmatter keys verbatim instead of normalizing kebab-case to
|
|
115
|
+
* camelCase. Default `false`. Strict spec loaders use this so a standard
|
|
116
|
+
* key (e.g. `allowed-tools`) is never aliased with its camelCase form.
|
|
117
|
+
*/
|
|
118
|
+
rawKeys?: boolean;
|
|
100
119
|
}
|
|
101
120
|
|
|
102
121
|
/**
|
|
@@ -107,11 +126,22 @@ export function parseFrontmatter(
|
|
|
107
126
|
content: string,
|
|
108
127
|
options?: FrontmatterOptions,
|
|
109
128
|
): { frontmatter: Record<string, unknown>; body: string } {
|
|
110
|
-
const {
|
|
129
|
+
const {
|
|
130
|
+
location,
|
|
131
|
+
source,
|
|
132
|
+
fallback,
|
|
133
|
+
normalize = true,
|
|
134
|
+
level = "warn",
|
|
135
|
+
repair = true,
|
|
136
|
+
rawKeys = false,
|
|
137
|
+
} = options ?? {};
|
|
138
|
+
const finalizeKeys = (fm: Record<string, unknown>): Record<string, unknown> =>
|
|
139
|
+
rawKeys ? fm : normalizeFrontmatterKeys(fm);
|
|
111
140
|
const loc = location ?? source;
|
|
112
141
|
const frontmatter: Record<string, unknown> = { ...fallback };
|
|
113
142
|
|
|
114
|
-
const
|
|
143
|
+
const newlineNormalized = normalize ? content.replace(/\r\n?/g, "\n") : content;
|
|
144
|
+
const normalized = normalize && repair ? stripHtmlComments(newlineNormalized) : newlineNormalized;
|
|
115
145
|
if (!normalized.startsWith("---")) {
|
|
116
146
|
return { frontmatter, body: normalized };
|
|
117
147
|
}
|
|
@@ -125,14 +155,14 @@ export function parseFrontmatter(
|
|
|
125
155
|
const body = normalized.slice(endIndex + 4).trim();
|
|
126
156
|
|
|
127
157
|
try {
|
|
128
|
-
const loaded = parseYamlRecord(metadata);
|
|
129
|
-
return { frontmatter:
|
|
158
|
+
const loaded = parseYamlRecord(metadata, repair);
|
|
159
|
+
return { frontmatter: finalizeKeys({ ...frontmatter, ...loaded }), body };
|
|
130
160
|
} catch (error) {
|
|
131
|
-
const quotedMetadata = quoteAmbiguousPlainScalars(metadata);
|
|
161
|
+
const quotedMetadata = repair ? quoteAmbiguousPlainScalars(metadata) : undefined;
|
|
132
162
|
if (quotedMetadata) {
|
|
133
163
|
try {
|
|
134
|
-
const loaded = parseYamlRecord(quotedMetadata);
|
|
135
|
-
return { frontmatter:
|
|
164
|
+
const loaded = parseYamlRecord(quotedMetadata, true);
|
|
165
|
+
return { frontmatter: finalizeKeys({ ...frontmatter, ...loaded }), body };
|
|
136
166
|
} catch {
|
|
137
167
|
// Fall through to the existing warning + simple key/value fallback.
|
|
138
168
|
}
|
|
@@ -170,6 +200,6 @@ export function parseFrontmatter(
|
|
|
170
200
|
frontmatter[match[1]] = value;
|
|
171
201
|
}
|
|
172
202
|
|
|
173
|
-
return { frontmatter:
|
|
203
|
+
return { frontmatter: finalizeKeys(frontmatter), body };
|
|
174
204
|
}
|
|
175
205
|
}
|
package/src/marked/core.ts
CHANGED
|
@@ -826,23 +826,23 @@ function parseList(lines: string[], index: number, lexer: Lexer): { token: Token
|
|
|
826
826
|
if (/^\s*\n$/.test(next)) {
|
|
827
827
|
let lookahead = cursor + 1;
|
|
828
828
|
while (lookahead < lines.length && /^\s*\n$/.test(lines[lookahead]!)) lookahead++;
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
829
|
+
// A blank line closes the list unless the next top-level line is a
|
|
830
|
+
// compatible item (same bullet char / ordered delimiter) or indented
|
|
831
|
+
// item content. The blank must stay OUTSIDE the list raw (it becomes
|
|
832
|
+
// a `space` token) so token shape never depends on what follows —
|
|
833
|
+
// real marked does the same, and the TUI's streaming freeze relies
|
|
834
|
+
// on that append-stability. A blank run at end of input closes the
|
|
835
|
+
// list the same way, keeping it tight and its raw blank-free.
|
|
836
|
+
if (lookahead >= lines.length) break;
|
|
837
|
+
const following = lines[lookahead]!;
|
|
838
|
+
const followingIndent = /^ */.exec(following)![0].length;
|
|
839
|
+
if (followingIndent <= first[1]!.length) {
|
|
840
|
+
const followingList = isList(following);
|
|
841
|
+
const compatible =
|
|
842
|
+
followingList !== null &&
|
|
843
|
+
/^\d/.test(followingList[2]!) === ordered &&
|
|
844
|
+
(ordered ? followingList[2]!.at(-1) === delimiter : followingList[2] === delimiter);
|
|
845
|
+
if (!compatible) break;
|
|
846
846
|
}
|
|
847
847
|
text += next;
|
|
848
848
|
} else {
|