structured-context 0.11.0 → 0.13.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/README.md +52 -198
- package/dist/api.d.ts +17 -15
- package/dist/api.js +12 -9
- package/dist/commands/diagram.d.ts +1 -0
- package/dist/commands/diagram.js +7 -1
- package/dist/commands/docs.js +1 -0
- package/dist/commands/dump.d.ts +15 -2
- package/dist/commands/dump.js +18 -4
- package/dist/commands/providers.d.ts +4 -0
- package/dist/commands/providers.js +52 -0
- package/dist/commands/render.d.ts +4 -0
- package/dist/commands/render.js +64 -9
- package/dist/commands/schemas.d.ts +2 -2
- package/dist/commands/schemas.js +133 -22
- package/dist/commands/show.d.ts +1 -0
- package/dist/commands/show.js +7 -1
- package/dist/commands/spaces.d.ts +3 -1
- package/dist/commands/spaces.js +11 -2
- package/dist/commands/validate-file.d.ts +3 -0
- package/dist/commands/validate-file.js +24 -6
- package/dist/commands/validate.d.ts +4 -0
- package/dist/commands/validate.js +79 -23
- package/dist/config.d.ts +25 -1
- package/dist/config.js +106 -12
- package/dist/filter/augment-nodes.d.ts +3 -1
- package/dist/filter/augment-nodes.js +12 -7
- package/dist/filter/filter-nodes.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +96 -31
- package/dist/judge/context.d.ts +20 -0
- package/dist/judge/context.js +105 -0
- package/dist/judge/factory.d.ts +44 -0
- package/dist/judge/factory.js +148 -0
- package/dist/judge/index.d.ts +13 -0
- package/dist/judge/index.js +8 -0
- package/dist/judge/judge-node.d.ts +27 -0
- package/dist/judge/judge-node.js +122 -0
- package/dist/judge/prompt.d.ts +10 -0
- package/dist/judge/prompt.js +92 -0
- package/dist/judge/provider/anthropic.d.ts +17 -0
- package/dist/judge/provider/anthropic.js +43 -0
- package/dist/judge/provider/fake.d.ts +26 -0
- package/dist/judge/provider/fake.js +56 -0
- package/dist/judge/provider/jev.d.ts +37 -0
- package/dist/judge/provider/jev.js +169 -0
- package/dist/judge/provider/openai-decisions.d.ts +21 -0
- package/dist/judge/provider/openai-decisions.js +84 -0
- package/dist/judge/provider/openai.d.ts +20 -0
- package/dist/judge/provider/openai.js +53 -0
- package/dist/judge/provider/per-rule.d.ts +10 -0
- package/dist/judge/provider/per-rule.js +18 -0
- package/dist/judge/trim.d.ts +27 -0
- package/dist/judge/trim.js +62 -0
- package/dist/judge/types.d.ts +92 -0
- package/dist/judge/types.js +9 -0
- package/dist/judge/verdicts.d.ts +24 -0
- package/dist/judge/verdicts.js +82 -0
- package/dist/load-space-graph.d.ts +7 -5
- package/dist/load-space-graph.js +19 -8
- package/dist/plugins/loader.js +3 -3
- package/dist/plugins/markdown/extract-content-links.js +8 -29
- package/dist/plugins/markdown/index.js +8 -2
- package/dist/plugins/markdown/parse-embedded.d.ts +8 -0
- package/dist/plugins/markdown/parse-embedded.js +195 -24
- package/dist/plugins/markdown/read-space.js +42 -14
- package/dist/plugins/markdown/render-directory.d.ts +11 -0
- package/dist/plugins/markdown/render-directory.js +130 -0
- package/dist/plugins/markdown/template-sync.js +3 -4
- package/dist/plugins/markdown/util.d.ts +26 -5
- package/dist/plugins/markdown/util.js +56 -15
- package/dist/plugins/mermaid/render-mermaid.js +2 -1
- package/dist/plugins/miro/sync.js +7 -6
- package/dist/plugins/util.d.ts +39 -1
- package/dist/plugins/util.js +139 -0
- package/dist/read/resolve-graph-edges.d.ts +1 -1
- package/dist/read/resolve-graph-edges.js +46 -27
- package/dist/read/wikilink-utils.d.ts +17 -0
- package/dist/read/wikilink-utils.js +40 -0
- package/dist/render/render.d.ts +3 -1
- package/dist/render/render.js +2 -2
- package/dist/schema/evaluate-rule.d.ts +33 -2
- package/dist/schema/evaluate-rule.js +58 -4
- package/dist/schema/gating.d.ts +69 -0
- package/dist/schema/gating.js +253 -0
- package/dist/schema/meta-schemas.d.ts +13 -0
- package/dist/schema/meta-schemas.js +50 -0
- package/dist/schema/metadata-contract.d.ts +952 -183
- package/dist/schema/metadata-contract.js +177 -33
- package/dist/schema/rule-filter.d.ts +12 -0
- package/dist/schema/rule-filter.js +46 -0
- package/dist/schema/rule-severity.d.ts +13 -0
- package/dist/schema/rule-severity.js +33 -0
- package/dist/schema/schema.d.ts +14 -8
- package/dist/schema/schema.js +308 -199
- package/dist/schema/validate-graph.d.ts +2 -1
- package/dist/schema/validate-graph.js +40 -23
- package/dist/schema/validate-rules.d.ts +16 -3
- package/dist/schema/validate-rules.js +91 -29
- package/dist/schemas/_knowledge_wiki.json +24 -24
- package/dist/schemas/_okf.json +172 -0
- package/dist/schemas/_ost_strict.json +17 -8
- package/dist/schemas/_sctx_base.json +3 -3
- package/dist/schemas/_strategy_general.json +38 -38
- package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/dist/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
- package/dist/schemas/knowledge_wiki.json +10 -9
- package/dist/schemas/okf.json +8 -0
- package/dist/schemas/strategy_general.json +15 -14
- package/dist/schemas/strict_ost.json +17 -16
- package/dist/space-context.d.ts +5 -2
- package/dist/space-context.js +9 -4
- package/dist/space-graph.d.ts +9 -7
- package/dist/space-graph.js +7 -5
- package/dist/testing.d.ts +33 -0
- package/dist/testing.js +72 -0
- package/dist/types.d.ts +64 -16
- package/dist/validate.d.ts +62 -9
- package/dist/validate.js +229 -114
- package/docs/concepts.md +65 -9
- package/docs/config.md +78 -1
- package/docs/rules.md +168 -50
- package/docs/schemas.md +152 -52
- package/package.json +17 -11
- package/schemas/_knowledge_wiki.json +24 -24
- package/schemas/_okf.json +172 -0
- package/schemas/_ost_strict.json +17 -8
- package/schemas/_sctx_base.json +3 -3
- package/schemas/_strategy_general.json +38 -38
- package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
- package/schemas/knowledge_wiki.json +10 -9
- package/schemas/okf.json +8 -0
- package/schemas/strategy_general.json +15 -14
- package/schemas/strict_ost.json +17 -16
- package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
- package/schemas/generated/_structured_context_schema_meta.json +0 -191
|
@@ -2,6 +2,7 @@ import { statSync } from 'node:fs';
|
|
|
2
2
|
import { PLUGIN_PREFIX } from '../util';
|
|
3
3
|
import { readSpaceDirectory, readSpaceOnAPage } from './read-space';
|
|
4
4
|
import { renderBullets } from './render-bullets';
|
|
5
|
+
import { renderMarkdownDirectory } from './render-directory';
|
|
5
6
|
import { templateSync } from './template-sync';
|
|
6
7
|
const TYPE_INFERENCE_CONFIG_SCHEMA = {
|
|
7
8
|
type: 'object',
|
|
@@ -36,10 +37,15 @@ export const markdownPlugin = {
|
|
|
36
37
|
parse,
|
|
37
38
|
templateSync,
|
|
38
39
|
render: {
|
|
39
|
-
formats: [
|
|
40
|
-
|
|
40
|
+
formats: [
|
|
41
|
+
{ name: 'bullets', description: 'Indented bullet list' },
|
|
42
|
+
{ name: 'directory', description: 'Markdown space directory with one file per node' },
|
|
43
|
+
],
|
|
44
|
+
render(context, graph, { format, data }) {
|
|
41
45
|
if (format === 'bullets')
|
|
42
46
|
return renderBullets(graph);
|
|
47
|
+
if (format === 'directory')
|
|
48
|
+
return renderMarkdownDirectory(context, graph, { format, data });
|
|
43
49
|
throw new Error(`Unknown markdown render format: "${format}"`);
|
|
44
50
|
},
|
|
45
51
|
},
|
|
@@ -8,6 +8,8 @@ type StackEntry = {
|
|
|
8
8
|
nodeType: string;
|
|
9
9
|
/** Preferred wikilink key used when this heading acts as a parent. */
|
|
10
10
|
refTarget: string;
|
|
11
|
+
/** Node whose section this heading opens; absent for untyped placeholders and the page's own entry. */
|
|
12
|
+
node?: BaseNode;
|
|
11
13
|
};
|
|
12
14
|
/** Detect a bare wikilink `[[...]]` and return the inner target, or undefined. */
|
|
13
15
|
export declare function isWikilink(text: string): string | undefined;
|
|
@@ -74,11 +76,17 @@ export interface ExtractEmbeddedOptions {
|
|
|
74
76
|
* Example: { "record_type": "type" } renames `record_type` to `type` in extracted data.
|
|
75
77
|
*/
|
|
76
78
|
fieldMap?: Record<string, string>;
|
|
79
|
+
/** File path of the containing document, for populating node.source. */
|
|
80
|
+
file?: string;
|
|
81
|
+
/** Line offset of the body within the source file (e.g. frontmatter lines). */
|
|
82
|
+
lineOffset?: number;
|
|
77
83
|
}
|
|
78
84
|
export interface ExtractEmbeddedResult {
|
|
79
85
|
nodes: BaseNode[];
|
|
80
86
|
preambleNodeCount: number;
|
|
81
87
|
terminatedHeadings: string[];
|
|
88
|
+
preambleContent?: string;
|
|
89
|
+
preambleFields?: Record<string, unknown>;
|
|
82
90
|
}
|
|
83
91
|
/**
|
|
84
92
|
* Extract space nodes from markdown body text.
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { load as yamlLoad } from 'js-yaml';
|
|
2
1
|
import { toString as mdastToString } from 'mdast-util-to-string';
|
|
3
2
|
import remarkGfm from 'remark-gfm';
|
|
4
3
|
import remarkParse from 'remark-parse';
|
|
5
4
|
import { unified } from 'unified';
|
|
6
5
|
import { extractLinksFromAstNode } from './extract-content-links';
|
|
7
|
-
import { applyFieldMap,
|
|
6
|
+
import { applyFieldMap, parseYaml } from './util';
|
|
8
7
|
/** Type values that identify a space_on_a_page container (not themselves space nodes). */
|
|
9
8
|
export const ON_A_PAGE_TYPES = ['ost_on_a_page', 'space_on_a_page'];
|
|
10
9
|
const DEFAULT_STATUS = 'identified';
|
|
@@ -141,16 +140,91 @@ export function defaultNodeType(stack, hierarchy) {
|
|
|
141
140
|
}
|
|
142
141
|
return hierarchy[idx + 1];
|
|
143
142
|
}
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
/** Start and end offsets of an mdast node. Throws when the parser gave no position. */
|
|
144
|
+
function offsetsOf(node) {
|
|
145
|
+
const start = node.position?.start.offset;
|
|
146
|
+
const end = node.position?.end.offset;
|
|
147
|
+
if (start === undefined || end === undefined) {
|
|
148
|
+
throw new Error(`Markdown ${node.type} node has no source position; cannot slice its content`);
|
|
149
|
+
}
|
|
150
|
+
return { start, end };
|
|
151
|
+
}
|
|
152
|
+
function addRange(claims, node, start, end) {
|
|
153
|
+
if (end <= start)
|
|
146
154
|
return;
|
|
147
|
-
const
|
|
148
|
-
|
|
155
|
+
const ranges = claims.ranges.get(node) ?? [];
|
|
156
|
+
const last = ranges[ranges.length - 1];
|
|
157
|
+
if (last && last.end === start)
|
|
158
|
+
last.end = end;
|
|
159
|
+
else
|
|
160
|
+
ranges.push({ start, end });
|
|
161
|
+
claims.ranges.set(node, ranges);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Give a node a block of source, plus the blank lines between `from` and the block's `start`.
|
|
165
|
+
* Those lines are skipped when a consumed block just before already kept its own.
|
|
166
|
+
*/
|
|
167
|
+
function claim(claims, node, from, start, end) {
|
|
168
|
+
addRange(claims, node, claims.gapKept.delete(node) ? start : from, end);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Take a block out of a node's content (it was consumed as a node, field or other syntax).
|
|
172
|
+
* The blank lines before it stay, so the content on either side does not run together.
|
|
173
|
+
*/
|
|
174
|
+
function consume(claims, node, from, start) {
|
|
175
|
+
addRange(claims, node, claims.gapKept.delete(node) ? start : from, start);
|
|
176
|
+
claims.gapKept.add(node);
|
|
177
|
+
}
|
|
178
|
+
/** Sets each node's content from its claimed source. Leading blank lines and trailing whitespace are dropped. */
|
|
179
|
+
function applyClaims(claims, body) {
|
|
180
|
+
for (const [node, ranges] of claims.ranges) {
|
|
181
|
+
let content = ranges
|
|
182
|
+
.map((r) => body.slice(r.start, r.end))
|
|
183
|
+
.join('')
|
|
184
|
+
.replace(/^(?:[ \t]*\r?\n)+/, '')
|
|
185
|
+
.trimEnd();
|
|
186
|
+
const indent = claims.contentIndent.get(node);
|
|
187
|
+
if (indent) {
|
|
188
|
+
const strip = new RegExp(`^[ \\t]{1,${indent}}`);
|
|
189
|
+
content = content
|
|
190
|
+
.split(/(?<=\n)/)
|
|
191
|
+
.map((line) => line.replace(strip, ''))
|
|
192
|
+
.join('');
|
|
193
|
+
}
|
|
194
|
+
if (!content)
|
|
195
|
+
continue;
|
|
196
|
+
node.schemaData.content = content;
|
|
197
|
+
node.content = content;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** Source lines (with terminators) of a range, as absolute offsets. */
|
|
201
|
+
function sourceLines(body, range) {
|
|
202
|
+
const lines = [];
|
|
203
|
+
const re = /[^\n]*\n|[^\n]+$/g;
|
|
204
|
+
const text = body.slice(range.start, range.end);
|
|
205
|
+
for (const m of text.matchAll(re)) {
|
|
206
|
+
lines.push({
|
|
207
|
+
start: range.start + m.index,
|
|
208
|
+
end: range.start + m.index + m[0].length,
|
|
209
|
+
text: m[0].replace(/\r?\n$/, ''),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return lines;
|
|
213
|
+
}
|
|
214
|
+
/** Whether a line holds only inline fields: bracketed `[key:: value]` pieces, or one `key:: value`. */
|
|
215
|
+
function isFieldOnlyLine(line) {
|
|
216
|
+
const { cleanText, fields } = extractBracketedFields(line);
|
|
217
|
+
const hasBracketed = Object.keys(fields).length > 0;
|
|
218
|
+
if (cleanText === '')
|
|
219
|
+
return hasBracketed;
|
|
220
|
+
return /^[a-zA-Z][a-zA-Z0-9_-]*:: *.*$/.test(cleanText);
|
|
149
221
|
}
|
|
150
|
-
function processListItem(item, parentRef, contentTarget, nodes, makeLabel, buildLinkTargets, typeAliases, fieldMap, pendingType, parentFieldAppend, activeNodeFieldAppend) {
|
|
222
|
+
function processListItem(item, parentRef, contentTarget, nodes, makeLabel, buildLinkTargets, typeAliases, fieldMap, pendingType, parentFieldAppend, activeNodeFieldAppend, file, lineOffset, walk) {
|
|
223
|
+
const itemEnd = offsetsOf(item).end;
|
|
151
224
|
const firstPara = item.children.find((c) => c.type === 'paragraph');
|
|
152
225
|
if (!firstPara) {
|
|
153
|
-
|
|
226
|
+
claim(walk.claims, contentTarget, walk.pos, offsetsOf(item).start, itemEnd);
|
|
227
|
+
walk.pos = itemEnd;
|
|
154
228
|
return;
|
|
155
229
|
}
|
|
156
230
|
const rawText = mdastToString(firstPara);
|
|
@@ -159,11 +233,15 @@ function processListItem(item, parentRef, contentTarget, nodes, makeLabel, build
|
|
|
159
233
|
if (wikiTarget) {
|
|
160
234
|
const linkRef = `[[${wikiTarget}]]`;
|
|
161
235
|
if (parentFieldAppend) {
|
|
236
|
+
consume(walk.claims, contentTarget, walk.pos, offsetsOf(item).start);
|
|
162
237
|
appendParentField(parentFieldAppend.node, parentFieldAppend.field, linkRef);
|
|
238
|
+
walk.pos = itemEnd;
|
|
163
239
|
return;
|
|
164
240
|
}
|
|
165
241
|
if (activeNodeFieldAppend) {
|
|
242
|
+
consume(walk.claims, contentTarget, walk.pos, offsetsOf(item).start);
|
|
166
243
|
appendParentField(activeNodeFieldAppend.node, activeNodeFieldAppend.field, linkRef);
|
|
244
|
+
walk.pos = itemEnd;
|
|
167
245
|
return;
|
|
168
246
|
}
|
|
169
247
|
// No field append context — fall through to node creation or content append
|
|
@@ -185,6 +263,11 @@ function processListItem(item, parentRef, contentTarget, nodes, makeLabel, build
|
|
|
185
263
|
schemaData.parent = parentRef;
|
|
186
264
|
if (summary)
|
|
187
265
|
schemaData.summary = summary;
|
|
266
|
+
const line = item.position?.start.line ? item.position.start.line + (lineOffset ?? 0) : undefined;
|
|
267
|
+
const column = item.position?.start.column;
|
|
268
|
+
const source = file
|
|
269
|
+
? { file, ...(line !== undefined ? { line } : {}), ...(column !== undefined ? { column } : {}) }
|
|
270
|
+
: undefined;
|
|
188
271
|
const linkTargets = buildLinkTargets(title);
|
|
189
272
|
const newNode = {
|
|
190
273
|
label: makeLabel(title),
|
|
@@ -192,24 +275,39 @@ function processListItem(item, parentRef, contentTarget, nodes, makeLabel, build
|
|
|
192
275
|
schemaData,
|
|
193
276
|
linkTargets,
|
|
194
277
|
type,
|
|
278
|
+
...(schemaData.content !== undefined ? { content: schemaData.content } : {}),
|
|
195
279
|
contentLinks: [],
|
|
280
|
+
...(source ? { source } : {}),
|
|
196
281
|
};
|
|
197
282
|
nodes.push(newNode);
|
|
283
|
+
const contentColumn = firstPara.position?.start.column;
|
|
284
|
+
if (contentColumn !== undefined)
|
|
285
|
+
walk.claims.contentIndent.set(newNode, contentColumn - 1);
|
|
198
286
|
if (parentFieldAppend) {
|
|
199
287
|
appendParentField(parentFieldAppend.node, parentFieldAppend.field, `[[${linkTargets[0] ?? title}]]`);
|
|
200
288
|
}
|
|
201
289
|
const nestedParentRef = `[[${linkTargets[0] ?? title}]]`;
|
|
290
|
+
consume(walk.claims, contentTarget, walk.pos, offsetsOf(item).start);
|
|
291
|
+
walk.pos = offsetsOf(firstPara).end;
|
|
202
292
|
for (const child of item.children) {
|
|
293
|
+
if (child === firstPara)
|
|
294
|
+
continue;
|
|
203
295
|
if (child.type === 'list') {
|
|
204
296
|
for (const subItem of child.children) {
|
|
205
|
-
processListItem(subItem, nestedParentRef, newNode, nodes, makeLabel, buildLinkTargets, typeAliases, fieldMap, pendingType);
|
|
297
|
+
processListItem(subItem, nestedParentRef, newNode, nodes, makeLabel, buildLinkTargets, typeAliases, fieldMap, pendingType, undefined, undefined, file, lineOffset, walk);
|
|
206
298
|
}
|
|
207
299
|
}
|
|
300
|
+
else {
|
|
301
|
+
const { start, end } = offsetsOf(child);
|
|
302
|
+
claim(walk.claims, newNode, walk.pos, start, end);
|
|
303
|
+
walk.pos = end;
|
|
304
|
+
}
|
|
208
305
|
}
|
|
209
306
|
}
|
|
210
307
|
else {
|
|
211
|
-
|
|
308
|
+
claim(walk.claims, contentTarget, walk.pos, offsetsOf(item).start, itemEnd);
|
|
212
309
|
}
|
|
310
|
+
walk.pos = itemEnd;
|
|
213
311
|
}
|
|
214
312
|
/**
|
|
215
313
|
* Extract space nodes from markdown body text.
|
|
@@ -240,7 +338,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
240
338
|
const stack = !isOnAPageMode && pageTitle !== undefined
|
|
241
339
|
? [{ depth: 0, title: pageTitle, nodeType: pageType, refTarget: pageTitle }]
|
|
242
340
|
: [];
|
|
243
|
-
let parseState = 'preamble';
|
|
341
|
+
let parseState = isOnAPageMode ? 'preamble' : 'active';
|
|
244
342
|
let preambleNodeCount = 0;
|
|
245
343
|
const terminatedHeadings = [];
|
|
246
344
|
/**
|
|
@@ -409,7 +507,32 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
409
507
|
}
|
|
410
508
|
let grouping = null;
|
|
411
509
|
let activeNode = rootNode;
|
|
510
|
+
const claims = { ranges: new Map(), gapKept: new Set(), contentIndent: new Map() };
|
|
511
|
+
/** End of the previous top-level block; a block's span starts here so the blank lines before it stay with it. */
|
|
512
|
+
let prevEnd = 0;
|
|
412
513
|
for (const child of tree.children) {
|
|
514
|
+
const { start: blockStart, end: blockEnd } = offsetsOf(child);
|
|
515
|
+
const spanStart = prevEnd;
|
|
516
|
+
prevEnd = blockEnd;
|
|
517
|
+
/** Give a node this block as source content, minus any `removed` ranges inside it. */
|
|
518
|
+
const claimBlock = (node, removed = []) => {
|
|
519
|
+
let from = blockStart;
|
|
520
|
+
let first = true;
|
|
521
|
+
for (const gap of removed) {
|
|
522
|
+
if (first)
|
|
523
|
+
claim(claims, node, spanStart, blockStart, gap.start);
|
|
524
|
+
else
|
|
525
|
+
addRange(claims, node, from, gap.start);
|
|
526
|
+
first = false;
|
|
527
|
+
from = gap.end;
|
|
528
|
+
}
|
|
529
|
+
if (first)
|
|
530
|
+
claim(claims, node, spanStart, blockStart, blockEnd);
|
|
531
|
+
else
|
|
532
|
+
addRange(claims, node, from, blockEnd);
|
|
533
|
+
};
|
|
534
|
+
/** Take this whole block out of a node's content, keeping the blank lines before it. */
|
|
535
|
+
const consumeBlock = (node) => consume(claims, node, spanStart, blockStart);
|
|
413
536
|
if (parseState === 'done') {
|
|
414
537
|
if (child.type === 'heading') {
|
|
415
538
|
const rawTitle = mdastToString(child);
|
|
@@ -427,8 +550,11 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
427
550
|
if (child.type === 'heading') {
|
|
428
551
|
const heading = child;
|
|
429
552
|
const depth = heading.depth;
|
|
430
|
-
if (depth > 5)
|
|
553
|
+
if (depth > 5) {
|
|
554
|
+
if (parseState === 'active')
|
|
555
|
+
claimBlock(activeNode);
|
|
431
556
|
continue;
|
|
557
|
+
}
|
|
432
558
|
parseState = 'active';
|
|
433
559
|
const rawText = mdastToString(heading);
|
|
434
560
|
const { cleanText: afterBracketed, fields: rawInlineFields } = extractBracketedFields(rawText);
|
|
@@ -446,6 +572,10 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
446
572
|
stack.push({ depth, title, nodeType: '', refTarget: title });
|
|
447
573
|
// Discard any pending grouping (untyped heading has no implied type)
|
|
448
574
|
grouping = null;
|
|
575
|
+
// An untyped heading is part of the body of the nearest enclosing node. Popping above removed every
|
|
576
|
+
// node at the same or a higher level, so those sections end here; deeper nodes' sections continue.
|
|
577
|
+
activeNode = stack.findLast((entry) => entry.node)?.node ?? rootNode;
|
|
578
|
+
claimBlock(activeNode);
|
|
449
579
|
continue;
|
|
450
580
|
}
|
|
451
581
|
if (isOnAPageMode && stack.length > 0) {
|
|
@@ -467,6 +597,11 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
467
597
|
};
|
|
468
598
|
if (parentRef)
|
|
469
599
|
schemaData.parent = parentRef;
|
|
600
|
+
const line = heading.position?.start.line ? heading.position.start.line + (options.lineOffset ?? 0) : undefined;
|
|
601
|
+
const column = heading.position?.start.column;
|
|
602
|
+
const source = options.file
|
|
603
|
+
? { file: options.file, ...(line !== undefined ? { line } : {}), ...(column !== undefined ? { column } : {}) }
|
|
604
|
+
: undefined;
|
|
470
605
|
const linkTargets = buildHeadingLinkTargets(rawText, title, anchor);
|
|
471
606
|
const headingNode = {
|
|
472
607
|
label: makeLabel(title),
|
|
@@ -474,11 +609,13 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
474
609
|
schemaData,
|
|
475
610
|
linkTargets,
|
|
476
611
|
type,
|
|
612
|
+
...(schemaData.content !== undefined ? { content: schemaData.content } : {}),
|
|
477
613
|
contentLinks: [],
|
|
614
|
+
...(source ? { source } : {}),
|
|
478
615
|
};
|
|
479
616
|
// Push to stack BEFORE resolving semantic parent — stack[-2] is the correct parent.
|
|
480
617
|
const refTarget = linkTargets[0] ?? title;
|
|
481
|
-
stack.push({ depth, title, nodeType: type, refTarget });
|
|
618
|
+
stack.push({ depth, title, nodeType: type, refTarget, node: headingNode });
|
|
482
619
|
// If this match came from a relationship/hierarchy and no explicit type was given,
|
|
483
620
|
// create a grouping — delay adding the node until we see the following content.
|
|
484
621
|
// Discard any previous grouping (not flushed — original agnostic-parse behaviour).
|
|
@@ -516,14 +653,16 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
516
653
|
const activeNodeFieldAppendArg = isParentLevelMatch && semanticParent.node
|
|
517
654
|
? { node: semanticParent.node, field: definition.field }
|
|
518
655
|
: undefined;
|
|
656
|
+
const walk = { claims, pos: spanStart };
|
|
519
657
|
for (const item of list.children) {
|
|
520
|
-
processListItem(item, isParentSide ? undefined : semanticParent.ref, grouping.headingNode, nodes, makeLabel, buildListItemLinkTargets, typeAliases, fieldMap, definition.type, parentFieldAppendArg, activeNodeFieldAppendArg);
|
|
658
|
+
processListItem(item, isParentSide ? undefined : semanticParent.ref, grouping.headingNode, nodes, makeLabel, buildListItemLinkTargets, typeAliases, fieldMap, definition.type, parentFieldAppendArg, activeNodeFieldAppendArg, options.file, options.lineOffset, walk);
|
|
521
659
|
}
|
|
522
660
|
grouping = null;
|
|
523
661
|
}
|
|
524
662
|
else {
|
|
663
|
+
const walk = { claims, pos: spanStart };
|
|
525
664
|
for (const item of list.children) {
|
|
526
|
-
processListItem(item, parentRef, activeNode, nodes, makeLabel, buildListItemLinkTargets, typeAliases, fieldMap);
|
|
665
|
+
processListItem(item, parentRef, activeNode, nodes, makeLabel, buildListItemLinkTargets, typeAliases, fieldMap, undefined, undefined, undefined, options.file, options.lineOffset, walk);
|
|
527
666
|
}
|
|
528
667
|
}
|
|
529
668
|
}
|
|
@@ -605,6 +744,15 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
605
744
|
schemaData[mappedColName] = cellContent;
|
|
606
745
|
}
|
|
607
746
|
}
|
|
747
|
+
const line = row.position?.start.line ? row.position.start.line + (options.lineOffset ?? 0) : undefined;
|
|
748
|
+
const column = row.position?.start.column;
|
|
749
|
+
const source = options.file
|
|
750
|
+
? {
|
|
751
|
+
file: options.file,
|
|
752
|
+
...(line !== undefined ? { line } : {}),
|
|
753
|
+
...(column !== undefined ? { column } : {}),
|
|
754
|
+
}
|
|
755
|
+
: undefined;
|
|
608
756
|
const linkTargets = buildListItemLinkTargets(title);
|
|
609
757
|
const rowNode = {
|
|
610
758
|
label: makeLabel(title),
|
|
@@ -613,6 +761,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
613
761
|
linkTargets,
|
|
614
762
|
type: rowTypeStr,
|
|
615
763
|
contentLinks: [],
|
|
764
|
+
...(source ? { source } : {}),
|
|
616
765
|
};
|
|
617
766
|
nodes.push(rowNode);
|
|
618
767
|
if (tableParentFieldAppend) {
|
|
@@ -622,7 +771,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
622
771
|
grouping = null;
|
|
623
772
|
}
|
|
624
773
|
else {
|
|
625
|
-
|
|
774
|
+
claimBlock(activeNode);
|
|
626
775
|
}
|
|
627
776
|
}
|
|
628
777
|
}
|
|
@@ -636,37 +785,59 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
636
785
|
if (child.type === 'paragraph') {
|
|
637
786
|
const rawText = mdastToString(child);
|
|
638
787
|
const { cleanText: afterBracketed, fields: bracketedFields } = extractBracketedFields(rawText);
|
|
639
|
-
const {
|
|
788
|
+
const { fields: unbracketedFields } = extractUnbracketedFields(afterBracketed);
|
|
640
789
|
const allFields = applyFieldMap({ ...unbracketedFields, ...bracketedFields }, fieldMap);
|
|
641
790
|
if ('type' in allFields) {
|
|
642
791
|
throw new Error(`Type override via paragraph field is not supported at "${activeNode.schemaData.title ?? activeNode.label}". ` +
|
|
643
792
|
`Put [type:: ${allFields.type}] directly in the heading text.`);
|
|
644
793
|
}
|
|
645
794
|
Object.assign(activeNode.schemaData, allFields);
|
|
646
|
-
|
|
647
|
-
|
|
795
|
+
// Lines holding only fields are consumed as fields; any other line, including prose that mentions a field, is content.
|
|
796
|
+
const lines = sourceLines(body, { start: blockStart, end: blockEnd });
|
|
797
|
+
const fieldLines = lines.filter((l) => isFieldOnlyLine(l.text));
|
|
798
|
+
if (fieldLines.length === lines.length) {
|
|
799
|
+
consumeBlock(activeNode);
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
// Each field line goes with its terminator; a last line without one takes the preceding line break instead.
|
|
803
|
+
claimBlock(activeNode, fieldLines.map((l) => {
|
|
804
|
+
if (/\n$/.test(body.slice(l.start, l.end)))
|
|
805
|
+
return { start: l.start, end: l.end };
|
|
806
|
+
return { start: l.start - (body[l.start - 2] === '\r' ? 2 : 1), end: l.end };
|
|
807
|
+
}));
|
|
808
|
+
}
|
|
648
809
|
activeNode.contentLinks ??= [];
|
|
649
810
|
activeNode.contentLinks.push(...extractLinksFromAstNode(child));
|
|
650
811
|
}
|
|
651
812
|
else if (child.type === 'code' && child.lang?.trim() === 'yaml') {
|
|
652
813
|
const code = child;
|
|
653
|
-
const parsed =
|
|
814
|
+
const parsed = parseYaml(code.value);
|
|
654
815
|
if (parsed && !Array.isArray(parsed) && typeof parsed === 'object') {
|
|
655
|
-
Object.assign(activeNode.schemaData,
|
|
816
|
+
Object.assign(activeNode.schemaData, applyFieldMap(parsed, fieldMap));
|
|
817
|
+
consumeBlock(activeNode);
|
|
656
818
|
}
|
|
657
819
|
else if (Array.isArray(parsed)) {
|
|
658
820
|
throw new Error(`YAML block must be an object at "${activeNode.label}".`);
|
|
659
821
|
}
|
|
660
822
|
else {
|
|
661
|
-
|
|
823
|
+
claimBlock(activeNode);
|
|
662
824
|
}
|
|
663
825
|
}
|
|
664
826
|
else {
|
|
665
|
-
|
|
827
|
+
claimBlock(activeNode);
|
|
666
828
|
activeNode.contentLinks ??= [];
|
|
667
829
|
activeNode.contentLinks.push(...extractLinksFromAstNode(child));
|
|
668
830
|
}
|
|
669
831
|
}
|
|
670
832
|
}
|
|
671
|
-
|
|
833
|
+
applyClaims(claims, body);
|
|
834
|
+
const preambleContent = rootNode.schemaData.content;
|
|
835
|
+
const { type: _rootType, content: _rootContent, ...preambleFields } = rootNode.schemaData;
|
|
836
|
+
return {
|
|
837
|
+
nodes,
|
|
838
|
+
preambleNodeCount,
|
|
839
|
+
terminatedHeadings,
|
|
840
|
+
preambleContent,
|
|
841
|
+
preambleFields: Object.keys(preambleFields).length > 0 ? preambleFields : undefined,
|
|
842
|
+
};
|
|
672
843
|
}
|
|
@@ -5,12 +5,12 @@ import matter from 'gray-matter';
|
|
|
5
5
|
import { extractSchemaTypeNames } from '../../schema/schema';
|
|
6
6
|
import { extractLinksFromBody, extractLinksFromFrontmatter, getEdgeFieldNames } from './extract-content-links';
|
|
7
7
|
import { extractEmbeddedNodes, ON_A_PAGE_TYPES } from './parse-embedded';
|
|
8
|
-
import { applyFieldMap,
|
|
8
|
+
import { applyFieldMap, inferTypeFromPath, MATTER_OPTIONS } from './util';
|
|
9
9
|
export function readSpaceOnAPage(context) {
|
|
10
10
|
const { space, resolvedSchemaPath, schema: { metadata }, } = context;
|
|
11
11
|
const filePath = resolve(space.path);
|
|
12
12
|
const raw = readFileSync(filePath, 'utf-8');
|
|
13
|
-
const { data: frontmatter, content: body } = matter(raw);
|
|
13
|
+
const { data: frontmatter, content: body } = matter(raw, MATTER_OPTIONS);
|
|
14
14
|
const pageType = frontmatter.type;
|
|
15
15
|
if (pageType !== undefined && !ON_A_PAGE_TYPES.includes(pageType)) {
|
|
16
16
|
throw new Error(`Expected a space_on_a_page file but got type "${pageType}" in ${filePath}. ` +
|
|
@@ -21,10 +21,15 @@ export function readSpaceOnAPage(context) {
|
|
|
21
21
|
throw new Error(`Schema at ${resolvedSchemaPath} must define "$metadata.hierarchy.levels" to read a space_on_a_page file.`);
|
|
22
22
|
}
|
|
23
23
|
const pageTitle = basename(filePath, '.md');
|
|
24
|
+
const fmMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
25
|
+
const lineOffset = fmMatch ? (fmMatch[0].match(/\n/g) || []).length : 0;
|
|
26
|
+
const fileName = basename(filePath);
|
|
24
27
|
const { nodes, preambleNodeCount, terminatedHeadings } = extractEmbeddedNodes(body, {
|
|
25
28
|
pageTitle,
|
|
26
29
|
pageType: 'space_on_a_page',
|
|
27
30
|
metadata,
|
|
31
|
+
file: fileName,
|
|
32
|
+
lineOffset,
|
|
28
33
|
});
|
|
29
34
|
const parseIssues = terminatedHeadings.map((heading) => ({
|
|
30
35
|
file: heading,
|
|
@@ -55,32 +60,38 @@ export async function readSpaceDirectory(context, options) {
|
|
|
55
60
|
const content = readFileSync(join(directory, file), 'utf-8');
|
|
56
61
|
let parsed;
|
|
57
62
|
try {
|
|
58
|
-
parsed = matter(content);
|
|
63
|
+
parsed = matter(content, MATTER_OPTIONS);
|
|
59
64
|
}
|
|
60
65
|
catch (err) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// See: https://github.com/jonschlinkert/gray-matter/issues/166
|
|
65
|
-
matter.clearCache();
|
|
66
|
+
const yamlErr = err;
|
|
67
|
+
const line = yamlErr.mark ? yamlErr.mark.line + 1 : undefined;
|
|
68
|
+
const column = yamlErr.mark ? yamlErr.mark.column + 1 : undefined;
|
|
66
69
|
parseIssues.push({
|
|
67
70
|
file,
|
|
68
71
|
severity: 'error',
|
|
69
72
|
type: 'parse',
|
|
70
73
|
message: err instanceof Error ? err.message : String(err),
|
|
74
|
+
...(line !== undefined ? { line } : {}),
|
|
75
|
+
...(column !== undefined ? { column } : {}),
|
|
71
76
|
});
|
|
72
77
|
continue;
|
|
73
78
|
}
|
|
74
79
|
if (!parsed.data || Object.keys(parsed.data).length === 0) {
|
|
75
|
-
parseIssues.push({
|
|
80
|
+
parseIssues.push({
|
|
81
|
+
file,
|
|
82
|
+
severity: 'warning',
|
|
83
|
+
type: 'no-type',
|
|
84
|
+
message: 'No front-matter or type specified',
|
|
85
|
+
line: 1,
|
|
86
|
+
});
|
|
76
87
|
continue;
|
|
77
88
|
}
|
|
78
|
-
const data =
|
|
89
|
+
const data = applyFieldMap(parsed.data, fieldMap);
|
|
79
90
|
if (!data.type && typeInferenceCfg && knownTypes) {
|
|
80
91
|
data.type = inferTypeFromPath(file, typeInferenceCfg, knownTypes, context.schema.metadata.typeAliases);
|
|
81
92
|
}
|
|
82
93
|
if (!data.type) {
|
|
83
|
-
parseIssues.push({ file, severity: 'warning', type: 'no-type' });
|
|
94
|
+
parseIssues.push({ file, severity: 'warning', type: 'no-type', line: 1 });
|
|
84
95
|
continue;
|
|
85
96
|
}
|
|
86
97
|
if (ON_A_PAGE_TYPES.includes(data.type) && !options?.includeOnAPageFiles) {
|
|
@@ -89,21 +100,35 @@ export async function readSpaceDirectory(context, options) {
|
|
|
89
100
|
const pageType = data.type;
|
|
90
101
|
const fileBase = basename(file, '.md');
|
|
91
102
|
const title = data.title ?? fileBase;
|
|
92
|
-
|
|
103
|
+
const fmMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
104
|
+
const lineOffset = fmMatch ? (fmMatch[0].match(/\n/g) || []).length : 0;
|
|
105
|
+
const pageNode = {
|
|
93
106
|
label: file,
|
|
94
107
|
title,
|
|
95
108
|
schemaData: { title, ...data },
|
|
96
109
|
linkTargets: [title, fileBase],
|
|
97
110
|
type: pageType,
|
|
111
|
+
source: { file, line: 1 },
|
|
112
|
+
...(data.content !== undefined ? { content: data.content } : {}),
|
|
98
113
|
contentLinks: [...extractLinksFromFrontmatter(data, edgeFields), ...extractLinksFromBody(parsed.content)],
|
|
99
|
-
}
|
|
114
|
+
};
|
|
100
115
|
if (!ON_A_PAGE_TYPES.includes(pageType)) {
|
|
101
|
-
const { nodes: embedded, terminatedHeadings } = extractEmbeddedNodes(parsed.content, {
|
|
116
|
+
const { nodes: embedded, preambleContent, preambleFields, terminatedHeadings, } = extractEmbeddedNodes(parsed.content, {
|
|
102
117
|
pageTitle: fileBase,
|
|
103
118
|
pageType,
|
|
104
119
|
metadata,
|
|
105
120
|
fieldMap,
|
|
121
|
+
file,
|
|
122
|
+
lineOffset,
|
|
106
123
|
});
|
|
124
|
+
if (preambleFields) {
|
|
125
|
+
Object.assign(pageNode.schemaData, preambleFields);
|
|
126
|
+
}
|
|
127
|
+
if (preambleContent) {
|
|
128
|
+
pageNode.schemaData.content = preambleContent;
|
|
129
|
+
pageNode.content = preambleContent;
|
|
130
|
+
}
|
|
131
|
+
nodes.push(pageNode);
|
|
107
132
|
nodes.push(...embedded);
|
|
108
133
|
for (const heading of terminatedHeadings) {
|
|
109
134
|
parseIssues.push({
|
|
@@ -114,6 +139,9 @@ export async function readSpaceDirectory(context, options) {
|
|
|
114
139
|
});
|
|
115
140
|
}
|
|
116
141
|
}
|
|
142
|
+
else {
|
|
143
|
+
nodes.push(pageNode);
|
|
144
|
+
}
|
|
117
145
|
}
|
|
118
146
|
return { nodes, parseIssues, diagnostics: { kind: 'directory' } };
|
|
119
147
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SpaceGraph } from '../../space-graph';
|
|
2
|
+
import type { PluginContext, RenderMultiFileOutput, RenderOptions } from '../util';
|
|
3
|
+
/**
|
|
4
|
+
* Sanitize a node title into a valid filesystem filename.
|
|
5
|
+
* Strips Obsidian/OS forbidden characters: \ / : * ? " < > | # ^ [ ]
|
|
6
|
+
*/
|
|
7
|
+
export declare function sanitizeFilename(title: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Render a space graph as a markdown directory with one file per node.
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderMarkdownDirectory(context: PluginContext, graph: SpaceGraph, options: RenderOptions): RenderMultiFileOutput;
|