@notrealstudio/nr-md 0.1.1 → 0.2.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 +1 -1
- package/dist/includes.d.ts +20 -0
- package/dist/includes.js +63 -19
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -2
package/LICENSE
CHANGED
package/dist/includes.d.ts
CHANGED
|
@@ -3,6 +3,26 @@ export interface IncludeOptions {
|
|
|
3
3
|
maxDepth?: number;
|
|
4
4
|
/** Active sigil — only this sigil's includes are resolved. */
|
|
5
5
|
sigil?: '$' | '@';
|
|
6
|
+
/**
|
|
7
|
+
* Canonical id (e.g. file path) of the root document. Passed to
|
|
8
|
+
* `resolvePath` as `from` for top-level includes.
|
|
9
|
+
*/
|
|
10
|
+
from?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Map a target as written in document `from` to a canonical id. When set,
|
|
13
|
+
* `readFile` receives the id (not the raw target), nested includes resolve
|
|
14
|
+
* against the file they are written in, and cycles are detected by id.
|
|
15
|
+
* `undefined` = not found (include left as-is). Without it targets are
|
|
16
|
+
* passed through verbatim (legacy behaviour).
|
|
17
|
+
*/
|
|
18
|
+
resolvePath?: (target: string, from: string | undefined) => string | undefined;
|
|
19
|
+
/** On cycle: 'keep' (default) leaves the include as-is, 'throw' raises IncludeCycleError. */
|
|
20
|
+
onCycle?: 'keep' | 'throw';
|
|
21
|
+
}
|
|
22
|
+
/** Include cycle. `chain` — ids from the first repeated one back to itself. */
|
|
23
|
+
export declare class IncludeCycleError extends Error {
|
|
24
|
+
readonly chain: string[];
|
|
25
|
+
constructor(chain: string[]);
|
|
6
26
|
}
|
|
7
27
|
/**
|
|
8
28
|
* Resolve file includes in text. Replaces `$[[path]]` / `$[[path#section]]`
|
package/dist/includes.js
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
// File includes: $[[path]] / @[[path]] preprocessor (format-spec §2.6).
|
|
2
2
|
// Text-level substitution BEFORE parsing. Recursive with cycle detection.
|
|
3
3
|
// Supports sections: $[[path#section]], $[[#section]] (from current text).
|
|
4
|
+
// Path resolution is the caller's: `resolvePath(target, from)` makes nested
|
|
5
|
+
// includes relative to the file they are written in (the core stays IO-free).
|
|
6
|
+
/** Include cycle. `chain` — ids from the first repeated one back to itself. */
|
|
7
|
+
export class IncludeCycleError extends Error {
|
|
8
|
+
chain;
|
|
9
|
+
constructor(chain) {
|
|
10
|
+
super(`include cycle: ${chain.join(' -> ')}`);
|
|
11
|
+
this.name = 'IncludeCycleError';
|
|
12
|
+
this.chain = chain;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function makeCtx(readFile, opts) {
|
|
16
|
+
return {
|
|
17
|
+
readFile,
|
|
18
|
+
sigil: opts?.sigil ?? '$',
|
|
19
|
+
maxDepth: opts?.maxDepth ?? 10,
|
|
20
|
+
resolvePath: opts?.resolvePath,
|
|
21
|
+
onCycle: opts?.onCycle ?? 'keep',
|
|
22
|
+
stack: opts?.from !== undefined && opts.resolvePath ? [opts.from] : [],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Resolve target → id; `undefined` = not found. */
|
|
26
|
+
function toId(ctx, target) {
|
|
27
|
+
if (!ctx.resolvePath)
|
|
28
|
+
return target;
|
|
29
|
+
const from = ctx.stack.length ? ctx.stack[ctx.stack.length - 1] : undefined;
|
|
30
|
+
return ctx.resolvePath(target, from);
|
|
31
|
+
}
|
|
32
|
+
/** true — cycle, include must be left as-is (or it threw). */
|
|
33
|
+
function isCycle(ctx, id) {
|
|
34
|
+
const at = ctx.stack.indexOf(id);
|
|
35
|
+
if (at < 0)
|
|
36
|
+
return false;
|
|
37
|
+
if (ctx.onCycle === 'throw')
|
|
38
|
+
throw new IncludeCycleError([...ctx.stack.slice(at), id]);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
4
41
|
/**
|
|
5
42
|
* Resolve file includes in text. Replaces `$[[path]]` / `$[[path#section]]`
|
|
6
43
|
* with file/section content, recursively. Cycle detection by path.
|
|
@@ -11,17 +48,13 @@
|
|
|
11
48
|
* Async version: resolveIncludesAsync.
|
|
12
49
|
*/
|
|
13
50
|
export function resolveIncludes(text, readFile, opts) {
|
|
14
|
-
|
|
15
|
-
const maxDepth = opts?.maxDepth ?? 10;
|
|
16
|
-
return expand(text, readFile, sigil, maxDepth, new Set(), 0, text);
|
|
51
|
+
return expand(text, makeCtx(readFile, opts), 0, text);
|
|
17
52
|
}
|
|
18
53
|
/**
|
|
19
54
|
* Async version — readFile returns Promise.
|
|
20
55
|
*/
|
|
21
56
|
export async function resolveIncludesAsync(text, readFile, opts) {
|
|
22
|
-
|
|
23
|
-
const maxDepth = opts?.maxDepth ?? 10;
|
|
24
|
-
return expandAsync(text, readFile, sigil, maxDepth, new Set(), 0, text);
|
|
57
|
+
return expandAsync(text, makeCtx(readFile, opts), 0, text);
|
|
25
58
|
}
|
|
26
59
|
// ---------- Extract section from markdown ----------
|
|
27
60
|
/**
|
|
@@ -69,7 +102,8 @@ function headingLevel(line) {
|
|
|
69
102
|
return i;
|
|
70
103
|
}
|
|
71
104
|
// ---------- Sync expand ----------
|
|
72
|
-
function expand(text,
|
|
105
|
+
function expand(text, ctx, depth, currentText) {
|
|
106
|
+
const { sigil, maxDepth } = ctx;
|
|
73
107
|
if (depth > maxDepth)
|
|
74
108
|
return text;
|
|
75
109
|
let out = '';
|
|
@@ -116,12 +150,12 @@ function expand(text, readFile, sigil, maxDepth, visited, depth, currentText) {
|
|
|
116
150
|
}
|
|
117
151
|
continue;
|
|
118
152
|
}
|
|
119
|
-
|
|
120
|
-
if (
|
|
153
|
+
const id = toId(ctx, filePath);
|
|
154
|
+
if (id === undefined || isCycle(ctx, id)) {
|
|
121
155
|
out += text.slice(start, i);
|
|
122
156
|
continue;
|
|
123
157
|
}
|
|
124
|
-
const content = readFile(
|
|
158
|
+
const content = ctx.readFile(id);
|
|
125
159
|
if (content === undefined) {
|
|
126
160
|
out += text.slice(start, i);
|
|
127
161
|
continue;
|
|
@@ -137,9 +171,13 @@ function expand(text, readFile, sigil, maxDepth, visited, depth, currentText) {
|
|
|
137
171
|
result = extracted;
|
|
138
172
|
}
|
|
139
173
|
// Recurse
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
174
|
+
ctx.stack.push(id);
|
|
175
|
+
try {
|
|
176
|
+
out += expand(result, ctx, depth + 1, content);
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
ctx.stack.pop();
|
|
180
|
+
}
|
|
143
181
|
continue;
|
|
144
182
|
}
|
|
145
183
|
out += text[i];
|
|
@@ -148,7 +186,8 @@ function expand(text, readFile, sigil, maxDepth, visited, depth, currentText) {
|
|
|
148
186
|
return out;
|
|
149
187
|
}
|
|
150
188
|
// ---------- Async expand ----------
|
|
151
|
-
async function expandAsync(text,
|
|
189
|
+
async function expandAsync(text, ctx, depth, currentText) {
|
|
190
|
+
const { sigil, maxDepth } = ctx;
|
|
152
191
|
if (depth > maxDepth)
|
|
153
192
|
return text;
|
|
154
193
|
// Collect all include positions first, then resolve in order
|
|
@@ -188,11 +227,12 @@ async function expandAsync(text, readFile, sigil, maxDepth, visited, depth, curr
|
|
|
188
227
|
out += extracted ?? text.slice(start, i);
|
|
189
228
|
continue;
|
|
190
229
|
}
|
|
191
|
-
|
|
230
|
+
const id = toId(ctx, filePath);
|
|
231
|
+
if (id === undefined || isCycle(ctx, id)) {
|
|
192
232
|
out += text.slice(start, i);
|
|
193
233
|
continue;
|
|
194
234
|
}
|
|
195
|
-
const content = await readFile(
|
|
235
|
+
const content = await ctx.readFile(id);
|
|
196
236
|
if (content === undefined) {
|
|
197
237
|
out += text.slice(start, i);
|
|
198
238
|
continue;
|
|
@@ -206,9 +246,13 @@ async function expandAsync(text, readFile, sigil, maxDepth, visited, depth, curr
|
|
|
206
246
|
}
|
|
207
247
|
result = extracted;
|
|
208
248
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
249
|
+
ctx.stack.push(id);
|
|
250
|
+
try {
|
|
251
|
+
out += await expandAsync(result, ctx, depth + 1, content);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
ctx.stack.pop();
|
|
255
|
+
}
|
|
212
256
|
continue;
|
|
213
257
|
}
|
|
214
258
|
out += text[i];
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,6 @@ export { serialize, serializeValue, serializeTable, serializeTypedTable, parseTa
|
|
|
6
6
|
export type { TableSerializeOptions, TableRecord } from './serialize.js';
|
|
7
7
|
export { parseHeaderCell, emitHeaderCell, coerceTyped, tableSchema, TableParseError } from './typed-header.js';
|
|
8
8
|
export type { TypedColumn, ColumnType, JSONSchema } from './typed-header.js';
|
|
9
|
-
export { resolveIncludes, resolveIncludesAsync, extractSection } from './includes.js';
|
|
9
|
+
export { resolveIncludes, resolveIncludesAsync, extractSection, IncludeCycleError } from './includes.js';
|
|
10
10
|
export type { IncludeOptions } from './includes.js';
|
|
11
11
|
export type { Sigil, ParseOptions, SerializeOptions, Document, Block, Attribute, Pos, AttributeValue, BodyValue, Scalar, ListItem, Json5Object, Json5Value, InterpolatedValue, InterpolationSpan, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -20,4 +20,4 @@ export { serialize, serializeValue, serializeTable, serializeTypedTable, parseTa
|
|
|
20
20
|
// ---------- Typed tbl header + tableSchema (serialize-spec §2.3) ----------
|
|
21
21
|
export { parseHeaderCell, emitHeaderCell, coerceTyped, tableSchema, TableParseError } from './typed-header.js';
|
|
22
22
|
// ---------- Includes (format-spec §2.6) ----------
|
|
23
|
-
export { resolveIncludes, resolveIncludesAsync, extractSection } from './includes.js';
|
|
23
|
+
export { resolveIncludes, resolveIncludesAsync, extractSection, IncludeCycleError } from './includes.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@notrealstudio/nr-md",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Markdown as structured data: headings become blocks, $key: value lines become attributes. Parser and serializer for the mdd/mdz block grammar.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsc",
|
|
36
36
|
"test": "vitest run",
|
|
37
|
-
"test:watch": "vitest"
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"prepublishOnly": "node -e \"const p=require('./package.json');const bad=Object.entries({...p.dependencies,...p.peerDependencies}).filter(([,v])=>String(v).startsWith('file:'));if(bad.length){console.error('publish blocked: file: deps '+bad.map(([k])=>k).join(', '));process.exit(1)}\""
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"@notrealstudio/nr-json5": "^0.1.1"
|