@khanhcan148/mk 0.1.21 → 0.1.22

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.
@@ -0,0 +1,219 @@
1
+ /**
2
+ * toml-emit.js — Inline TOML serialiser (no @iarna/toml runtime dependency).
3
+ *
4
+ * Scope: top-level table + array-of-tables ([[key]]) + string + boolean + number.
5
+ * Does NOT support inline arrays of mixed types or nested tables beyond
6
+ * array-of-tables — throws a TypeError for unsupported shapes.
7
+ *
8
+ * Hard constraints met:
9
+ * HC #2: no @iarna/toml runtime import.
10
+ * HC #4: in-table key order = matcher first, command second, remaining lex-sorted.
11
+ * Array-of-tables blocks sorted by (matcher, command) lex.
12
+ * Always uses Array.from(...).sort() — never raw Object.keys iteration.
13
+ *
14
+ * @iarna/toml remains a devDep used ONLY in test round-trip assertions.
15
+ */
16
+
17
+ /**
18
+ * Escape a raw string value to a TOML basic-string literal (without wrapping quotes).
19
+ * Handles TOML 1.0 required escapes: \\ \" \b \f \n \r \t \uXXXX for control chars.
20
+ *
21
+ * @param {string} s
22
+ * @returns {string}
23
+ */
24
+ function escapeString(s) {
25
+ let out = '';
26
+ for (let i = 0; i < s.length; i++) {
27
+ const ch = s[i];
28
+ const cp = s.charCodeAt(i);
29
+ if (ch === '\\') { out += '\\\\'; }
30
+ else if (ch === '"') { out += '\\"'; }
31
+ else if (ch === '\b') { out += '\\b'; }
32
+ else if (ch === '\f') { out += '\\f'; }
33
+ else if (ch === '\n') { out += '\\n'; }
34
+ else if (ch === '\r') { out += '\\r'; }
35
+ else if (ch === '\t') { out += '\\t'; }
36
+ else if (cp < 0x20 || cp === 0x7f) {
37
+ out += '\\u' + cp.toString(16).padStart(4, '0');
38
+ } else {
39
+ out += ch;
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+
45
+ /**
46
+ * Emit a scalar value as a TOML RHS token.
47
+ *
48
+ * @param {string|boolean|number|null|undefined} val
49
+ * @returns {string}
50
+ */
51
+ export function emitScalar(val) {
52
+ if (val === null || val === undefined) {
53
+ throw new TypeError(`[toml-emit] null/undefined is not a valid TOML scalar`);
54
+ }
55
+ if (typeof val === 'boolean') return val ? 'true' : 'false';
56
+ if (typeof val === 'number') return String(val);
57
+ if (typeof val === 'string') return `"${escapeString(val)}"`;
58
+ throw new TypeError(`[toml-emit] Unsupported scalar type: ${typeof val} (${String(val)})`);
59
+ }
60
+
61
+ /**
62
+ * Sort keys in the canonical order:
63
+ * 1. matcher (first)
64
+ * 2. command (second)
65
+ * 3. all other keys, lexicographically sorted
66
+ *
67
+ * @param {string[]} keys
68
+ * @returns {string[]}
69
+ */
70
+ function sortKeys(keys) {
71
+ const filtered = Array.from(keys).filter(k => k !== '__leadingComments');
72
+ const priority = ['matcher', 'command'];
73
+ const fixed = priority.filter(k => filtered.includes(k));
74
+ const rest = Array.from(filtered)
75
+ .filter(k => !priority.includes(k))
76
+ .sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
77
+ return [...fixed, ...rest];
78
+ }
79
+
80
+ /**
81
+ * Emit a TOML table section: `[key]` header + key=value lines.
82
+ *
83
+ * Respects `__leadingComments` synthetic key: if present, emits those comment
84
+ * lines (verbatim) before the section header; the key itself is NOT emitted.
85
+ *
86
+ * @param {string} key Section header name (e.g. "features", "hooks.PreToolUse")
87
+ * @param {object} obj Key-value pairs; values must be scalars.
88
+ * @returns {string}
89
+ */
90
+ export function emitTable(key, obj) {
91
+ const lines = [];
92
+
93
+ if (obj.__leadingComments) {
94
+ for (const line of obj.__leadingComments.split('\n')) {
95
+ lines.push(line);
96
+ }
97
+ }
98
+
99
+ lines.push(`[${key}]`);
100
+
101
+ for (const k of sortKeys(Object.keys(obj))) {
102
+ const v = obj[k];
103
+ if (typeof v === 'object' && v !== null) {
104
+ throw new TypeError(
105
+ `[toml-emit] emitTable does not support nested objects. Key "${k}" in table "${key}" ` +
106
+ `has type object. Use emitArrayOfTables for array-of-tables sections.`
107
+ );
108
+ }
109
+ lines.push(`${k} = ${emitScalar(v)}`);
110
+ }
111
+
112
+ return lines.join('\n') + '\n';
113
+ }
114
+
115
+ /**
116
+ * Emit an array-of-tables section: multiple `[[key]]` blocks.
117
+ *
118
+ * Each block is sorted by `(matcher, command)` lex before emission.
119
+ * Within each block, key order follows the canonical sort: matcher, command, rest lex.
120
+ *
121
+ * @param {string} key Section header name (e.g. "hooks.PreToolUse")
122
+ * @param {object[]} arr Array of objects; each object is one [[key]] block.
123
+ * @returns {string}
124
+ */
125
+ export function emitArrayOfTables(key, arr) {
126
+ // Sort by (matcher, command) — both fields optional but present in hooks usage
127
+ const sorted = Array.from(arr).sort((a, b) => {
128
+ const am = String(a.matcher ?? '');
129
+ const bm = String(b.matcher ?? '');
130
+ if (am !== bm) return am < bm ? -1 : 1;
131
+ const ac = String(a.command ?? '');
132
+ const bc = String(b.command ?? '');
133
+ return ac < bc ? -1 : ac > bc ? 1 : 0;
134
+ });
135
+
136
+ const chunks = [];
137
+ for (const block of sorted) {
138
+ const lines = [];
139
+
140
+ // Block-level leading comment
141
+ if (block.__leadingComments) {
142
+ for (const line of block.__leadingComments.split('\n')) {
143
+ lines.push(line);
144
+ }
145
+ }
146
+
147
+ lines.push(`[[${key}]]`);
148
+
149
+ for (const k of sortKeys(Object.keys(block))) {
150
+ const v = block[k];
151
+ if (typeof v === 'object' && v !== null) {
152
+ throw new TypeError(
153
+ `[toml-emit] emitArrayOfTables does not support nested objects. ` +
154
+ `Key "${k}" in [[${key}]] has type object.`
155
+ );
156
+ }
157
+ lines.push(`${k} = ${emitScalar(v)}`);
158
+ }
159
+
160
+ chunks.push(lines.join('\n'));
161
+ }
162
+
163
+ return chunks.join('\n') + '\n';
164
+ }
165
+
166
+ /**
167
+ * Emit a complete TOML document from a structured descriptor object.
168
+ *
169
+ * Document descriptor shape:
170
+ * ```js
171
+ * {
172
+ * __leadingComments?: string, // file-level comments, emitted first
173
+ * features?: { codex_hooks: boolean, ... }, // [features] table, emitted first among sections
174
+ * 'hooks.PreToolUse'?: { __type: 'array', items: [...] }, // array-of-tables
175
+ * 'hooks.PostToolUse'?: { __type: 'array', items: [...] },
176
+ * 'hooks.SessionStart'?: { __type: 'array', items: [...] },
177
+ * // other plain table keys
178
+ * }
179
+ * ```
180
+ *
181
+ * Emission order:
182
+ * 1. File-level `__leadingComments`
183
+ * 2. `[features]` block (if present)
184
+ * 3. All other sections in insertion order (caller controls order)
185
+ *
186
+ * @param {object} doc
187
+ * @returns {string}
188
+ */
189
+ export function emitToml(doc) {
190
+ const parts = [];
191
+
192
+ // File-level leading comments
193
+ if (doc.__leadingComments) {
194
+ parts.push(doc.__leadingComments + '\n');
195
+ }
196
+
197
+ // [features] always first among sections
198
+ if (doc.features) {
199
+ parts.push(emitTable('features', doc.features));
200
+ }
201
+
202
+ // Remaining sections in iteration order (caller sorts)
203
+ for (const key of Object.keys(doc)) {
204
+ if (key === '__leadingComments' || key === 'features') continue;
205
+
206
+ const val = doc[key];
207
+ if (val && typeof val === 'object' && val.__type === 'array') {
208
+ parts.push(emitArrayOfTables(key, val.items));
209
+ } else if (val && typeof val === 'object') {
210
+ parts.push(emitTable(key, val));
211
+ } else {
212
+ throw new TypeError(
213
+ `[toml-emit] Unsupported document-level value for key "${key}": expected object or array-descriptor`
214
+ );
215
+ }
216
+ }
217
+
218
+ return parts.join('\n');
219
+ }