structured-context 0.9.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 +348 -0
- package/dist/commands/diagram.d.ts +5 -0
- package/dist/commands/diagram.js +12 -0
- package/dist/commands/docs.d.ts +1 -0
- package/dist/commands/docs.js +67 -0
- package/dist/commands/dump.d.ts +2 -0
- package/dist/commands/dump.js +6 -0
- package/dist/commands/plugins.d.ts +1 -0
- package/dist/commands/plugins.js +23 -0
- package/dist/commands/render.d.ts +6 -0
- package/dist/commands/render.js +35 -0
- package/dist/commands/schemas.d.ts +6 -0
- package/dist/commands/schemas.js +268 -0
- package/dist/commands/show.d.ts +4 -0
- package/dist/commands/show.js +7 -0
- package/dist/commands/spaces.d.ts +1 -0
- package/dist/commands/spaces.js +36 -0
- package/dist/commands/template-sync.d.ts +3 -0
- package/dist/commands/template-sync.js +13 -0
- package/dist/commands/validate-file.d.ts +28 -0
- package/dist/commands/validate-file.js +133 -0
- package/dist/commands/validate.d.ts +16 -0
- package/dist/commands/validate.js +349 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.js +179 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +6 -0
- package/dist/filter/augment-nodes.d.ts +23 -0
- package/dist/filter/augment-nodes.js +95 -0
- package/dist/filter/expand-include.d.ts +62 -0
- package/dist/filter/expand-include.js +181 -0
- package/dist/filter/filter-nodes.d.ts +21 -0
- package/dist/filter/filter-nodes.js +73 -0
- package/dist/filter/parse-expression.d.ts +20 -0
- package/dist/filter/parse-expression.js +60 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +161 -0
- package/dist/integrations/miro/cache.d.ts +21 -0
- package/dist/integrations/miro/cache.js +55 -0
- package/dist/integrations/miro/client.d.ts +99 -0
- package/dist/integrations/miro/client.js +118 -0
- package/dist/integrations/miro/layout.d.ts +28 -0
- package/dist/integrations/miro/layout.js +72 -0
- package/dist/integrations/miro/styles.d.ts +11 -0
- package/dist/integrations/miro/styles.js +65 -0
- package/dist/integrations/miro/sync.d.ts +8 -0
- package/dist/integrations/miro/sync.js +347 -0
- package/dist/plugin-api.d.ts +12 -0
- package/dist/plugin-api.js +7 -0
- package/dist/plugins/index.d.ts +3 -0
- package/dist/plugins/index.js +3 -0
- package/dist/plugins/loader.d.ts +21 -0
- package/dist/plugins/loader.js +104 -0
- package/dist/plugins/markdown/index.d.ts +48 -0
- package/dist/plugins/markdown/index.js +51 -0
- package/dist/plugins/markdown/parse-embedded.d.ts +90 -0
- package/dist/plugins/markdown/parse-embedded.js +663 -0
- package/dist/plugins/markdown/read-space.d.ts +7 -0
- package/dist/plugins/markdown/read-space.js +89 -0
- package/dist/plugins/markdown/render-bullets.d.ts +2 -0
- package/dist/plugins/markdown/render-bullets.js +42 -0
- package/dist/plugins/markdown/render-mermaid.d.ts +2 -0
- package/dist/plugins/markdown/render-mermaid.js +57 -0
- package/dist/plugins/markdown/template-sync.d.ts +16 -0
- package/dist/plugins/markdown/template-sync.js +294 -0
- package/dist/plugins/markdown/util.d.ts +19 -0
- package/dist/plugins/markdown/util.js +80 -0
- package/dist/plugins/util.d.ts +60 -0
- package/dist/plugins/util.js +7 -0
- package/dist/read/read-space.d.ts +2 -0
- package/dist/read/read-space.js +22 -0
- package/dist/read/resolve-graph-edges.d.ts +11 -0
- package/dist/read/resolve-graph-edges.js +201 -0
- package/dist/read/wikilink-utils.d.ts +16 -0
- package/dist/read/wikilink-utils.js +38 -0
- package/dist/render/registry.d.ts +13 -0
- package/dist/render/registry.js +22 -0
- package/dist/render/render.d.ts +4 -0
- package/dist/render/render.js +28 -0
- package/dist/schema/evaluate-rule.d.ts +30 -0
- package/dist/schema/evaluate-rule.js +82 -0
- package/dist/schema/metadata-contract.d.ts +538 -0
- package/dist/schema/metadata-contract.js +115 -0
- package/dist/schema/schema-refs.d.ts +22 -0
- package/dist/schema/schema-refs.js +168 -0
- package/dist/schema/schema.d.ts +27 -0
- package/dist/schema/schema.js +378 -0
- package/dist/schema/validate-graph.d.ts +24 -0
- package/dist/schema/validate-graph.js +141 -0
- package/dist/schema/validate-rules.d.ts +10 -0
- package/dist/schema/validate-rules.js +51 -0
- package/dist/schemas/_ost_strict.json +81 -0
- package/dist/schemas/_sctx_base.json +72 -0
- package/dist/schemas/general.json +261 -0
- package/dist/schemas/generated/_structured_context_schema_meta.json +191 -0
- package/dist/schemas/knowledge_wiki.json +206 -0
- package/dist/schemas/strict_ost.json +97 -0
- package/dist/space-graph.d.ts +28 -0
- package/dist/space-graph.js +82 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.js +0 -0
- package/docs/concepts.md +391 -0
- package/docs/config.md +140 -0
- package/docs/rules.md +120 -0
- package/docs/schemas.md +340 -0
- package/package.json +69 -0
- package/schemas/_ost_strict.json +81 -0
- package/schemas/_sctx_base.json +72 -0
- package/schemas/general.json +261 -0
- package/schemas/generated/_structured_context_schema_meta.json +191 -0
- package/schemas/knowledge_wiki.json +206 -0
- package/schemas/strict_ost.json +97 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
import { load as yamlLoad } from 'js-yaml';
|
|
2
|
+
import { toString as mdastToString } from 'mdast-util-to-string';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import remarkParse from 'remark-parse';
|
|
5
|
+
import { unified } from 'unified';
|
|
6
|
+
import { applyFieldMap, coerceDates } from './util';
|
|
7
|
+
/** Type values that identify a space_on_a_page container (not themselves space nodes). */
|
|
8
|
+
export const ON_A_PAGE_TYPES = ['ost_on_a_page', 'space_on_a_page'];
|
|
9
|
+
const DEFAULT_STATUS = 'identified';
|
|
10
|
+
/** Detect a bare wikilink `[[...]]` and return the inner target, or undefined. */
|
|
11
|
+
export function isWikilink(text) {
|
|
12
|
+
const match = text.match(/^\[\[(.+?)\]\]$/);
|
|
13
|
+
return match ? match[1] : undefined;
|
|
14
|
+
}
|
|
15
|
+
/** Evaluate a list of matchers against a heading title. */
|
|
16
|
+
function matchesPattern(title, lowerTitle, matchers) {
|
|
17
|
+
for (const matcher of matchers) {
|
|
18
|
+
if (matcher.startsWith('^') && matcher.endsWith('$')) {
|
|
19
|
+
if (new RegExp(matcher, 'i').test(title))
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
else if (matcher.startsWith('/') && matcher.endsWith('/')) {
|
|
23
|
+
if (new RegExp(matcher.slice(1, -1), 'i').test(title))
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
else if (lowerTitle === matcher.toLowerCase()) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Append a wikilink reference to a field array on a node.
|
|
34
|
+
* Creates the array if missing; throws if the field exists but is not an array.
|
|
35
|
+
*/
|
|
36
|
+
function appendParentField(parentNode, field, linkRef) {
|
|
37
|
+
const fieldValue = parentNode.schemaData[field];
|
|
38
|
+
if (fieldValue === undefined) {
|
|
39
|
+
parentNode.schemaData[field] = [linkRef];
|
|
40
|
+
}
|
|
41
|
+
else if (Array.isArray(fieldValue)) {
|
|
42
|
+
fieldValue.push(linkRef);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw new Error(`Cannot append child link to field '${field}' on node '${parentNode.label}': ` +
|
|
46
|
+
`field exists but is not an array (found ${typeof fieldValue}). ` +
|
|
47
|
+
`Child link: ${linkRef}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Extract [key:: value] bracketed inline fields, return cleaned text and fields. */
|
|
51
|
+
export function extractBracketedFields(text) {
|
|
52
|
+
const fields = {};
|
|
53
|
+
const cleanText = text
|
|
54
|
+
.replace(/\[([^\]]+?):: *([^\]]*)\]/g, (_, key, value) => {
|
|
55
|
+
fields[key.trim()] = value.trim();
|
|
56
|
+
return '';
|
|
57
|
+
})
|
|
58
|
+
.trim();
|
|
59
|
+
return { cleanText, fields };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Extract unbracketed dataview fields (key:: value on own line).
|
|
63
|
+
* Keys must be identifier-style (letters, digits, hyphens, underscores - no spaces).
|
|
64
|
+
* Lines matching the pattern are consumed as fields; other lines kept as content.
|
|
65
|
+
*/
|
|
66
|
+
export function extractUnbracketedFields(text) {
|
|
67
|
+
const fields = {};
|
|
68
|
+
const remaining = [];
|
|
69
|
+
for (const line of text.split('\n')) {
|
|
70
|
+
const match = line.match(/^([a-zA-Z][a-zA-Z0-9_-]*):: *(.*)$/);
|
|
71
|
+
if (match) {
|
|
72
|
+
fields[match[1].trim()] = match[2].trim();
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
remaining.push(line);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { remainingText: remaining.join('\n').trim(), fields };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Extract a trailing Obsidian block anchor from heading text.
|
|
82
|
+
* e.g. "My Title ^anchor-id" -> { cleanText: "My Title", anchor: "anchor-id" }
|
|
83
|
+
*/
|
|
84
|
+
export function extractAnchor(text) {
|
|
85
|
+
const match = text.match(/\s+\^([a-zA-Z0-9][a-zA-Z0-9_-]*)$/);
|
|
86
|
+
if (match) {
|
|
87
|
+
return {
|
|
88
|
+
cleanText: text.slice(0, text.length - match[0].length).trim(),
|
|
89
|
+
anchor: match[1],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { cleanText: text };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* If the anchor name exactly matches a space node type (optionally followed by digits),
|
|
96
|
+
* return that type. Otherwise return undefined.
|
|
97
|
+
* Examples: "mission" -> "mission", "goal1" -> "goal", "myanchor" -> undefined
|
|
98
|
+
*
|
|
99
|
+
* Also checks relationship types (for parent-side relationships where child type may not be in hierarchy).
|
|
100
|
+
*/
|
|
101
|
+
export function anchorToNodeType(anchor, hierarchy, relationships) {
|
|
102
|
+
for (const type of hierarchy) {
|
|
103
|
+
if (anchor === type || new RegExp(`^${type}\\d+$`).test(anchor)) {
|
|
104
|
+
return type;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Check relationship types (for parent-side relationships)
|
|
108
|
+
if (relationships) {
|
|
109
|
+
for (const rel of relationships) {
|
|
110
|
+
if (anchor === rel.type || new RegExp(`^${rel.type}\\d+$`).test(anchor)) {
|
|
111
|
+
return rel.type;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Turn a full heading string into an Obsidian section-target key component.
|
|
119
|
+
* - normalizes observed Obsidian separators (#, ^, :, \) to spaces
|
|
120
|
+
* - compresses whitespace runs to single spaces
|
|
121
|
+
* - does _NOT_ (and should not) manipulate anchors or inline fields
|
|
122
|
+
*/
|
|
123
|
+
export function normalizeHeadingSectionTarget(rawHeadingText) {
|
|
124
|
+
return rawHeadingText
|
|
125
|
+
.replace(/[#^:\\]/g, ' ')
|
|
126
|
+
.replace(/\s+/g, ' ')
|
|
127
|
+
.trim();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Returns the default space node type for a new heading based on its parent's effective type.
|
|
131
|
+
* The first heading in a document defaults to the first type in the hierarchy; each child is the next in sequence.
|
|
132
|
+
*/
|
|
133
|
+
export function defaultNodeType(stack, hierarchy) {
|
|
134
|
+
if (stack.length === 0)
|
|
135
|
+
return hierarchy[0];
|
|
136
|
+
const parentType = stack[stack.length - 1].nodeType;
|
|
137
|
+
const idx = hierarchy.indexOf(parentType);
|
|
138
|
+
if (idx === -1 || idx >= hierarchy.length - 1) {
|
|
139
|
+
throw new Error(`No node type follows "${parentType}" - cannot determine type for child heading`);
|
|
140
|
+
}
|
|
141
|
+
return hierarchy[idx + 1];
|
|
142
|
+
}
|
|
143
|
+
function appendContent(node, text) {
|
|
144
|
+
if (!text)
|
|
145
|
+
return;
|
|
146
|
+
const existing = node.schemaData.content;
|
|
147
|
+
node.schemaData.content = existing ? `${existing}\n${text}` : text;
|
|
148
|
+
}
|
|
149
|
+
function processListItem(item, parentRef, contentTarget, nodes, makeLabel, buildLinkTargets, typeAliases, fieldMap, pendingType, parentFieldAppend, activeNodeFieldAppend) {
|
|
150
|
+
const firstPara = item.children.find((c) => c.type === 'paragraph');
|
|
151
|
+
if (!firstPara) {
|
|
152
|
+
appendContent(contentTarget, `- ${mdastToString(item)}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const rawText = mdastToString(firstPara);
|
|
156
|
+
// Wikilink detection: bare wikilinks populate a field without creating a node
|
|
157
|
+
const wikiTarget = isWikilink(rawText.trim());
|
|
158
|
+
if (wikiTarget) {
|
|
159
|
+
const linkRef = `[[${wikiTarget}]]`;
|
|
160
|
+
if (parentFieldAppend) {
|
|
161
|
+
appendParentField(parentFieldAppend.node, parentFieldAppend.field, linkRef);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (activeNodeFieldAppend) {
|
|
165
|
+
appendParentField(activeNodeFieldAppend.node, activeNodeFieldAppend.field, linkRef);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// No field append context — fall through to node creation or content append
|
|
169
|
+
}
|
|
170
|
+
const { cleanText, fields: rawFields } = extractBracketedFields(rawText);
|
|
171
|
+
const fields = applyFieldMap(rawFields, fieldMap);
|
|
172
|
+
const type = fields.type ?? pendingType;
|
|
173
|
+
if (type) {
|
|
174
|
+
const dashIdx = cleanText.indexOf(' - ');
|
|
175
|
+
const title = (dashIdx >= 0 ? cleanText.slice(0, dashIdx) : cleanText).trim();
|
|
176
|
+
const summary = dashIdx >= 0 ? cleanText.slice(dashIdx + 3).trim() : undefined;
|
|
177
|
+
const schemaData = {
|
|
178
|
+
title,
|
|
179
|
+
type,
|
|
180
|
+
status: DEFAULT_STATUS,
|
|
181
|
+
...fields,
|
|
182
|
+
};
|
|
183
|
+
if (parentRef && !parentFieldAppend)
|
|
184
|
+
schemaData.parent = parentRef;
|
|
185
|
+
if (summary)
|
|
186
|
+
schemaData.summary = summary;
|
|
187
|
+
const linkTargets = buildLinkTargets(title);
|
|
188
|
+
const newNode = {
|
|
189
|
+
label: makeLabel(title),
|
|
190
|
+
title,
|
|
191
|
+
schemaData,
|
|
192
|
+
linkTargets,
|
|
193
|
+
type,
|
|
194
|
+
};
|
|
195
|
+
nodes.push(newNode);
|
|
196
|
+
if (parentFieldAppend) {
|
|
197
|
+
appendParentField(parentFieldAppend.node, parentFieldAppend.field, `[[${linkTargets[0] ?? title}]]`);
|
|
198
|
+
}
|
|
199
|
+
const nestedParentRef = `[[${linkTargets[0] ?? title}]]`;
|
|
200
|
+
for (const child of item.children) {
|
|
201
|
+
if (child.type === 'list') {
|
|
202
|
+
for (const subItem of child.children) {
|
|
203
|
+
processListItem(subItem, nestedParentRef, newNode, nodes, makeLabel, buildLinkTargets, typeAliases, fieldMap, pendingType);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
appendContent(contentTarget, `- ${rawText}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Extract space nodes from markdown body text.
|
|
214
|
+
*
|
|
215
|
+
* Used by both readSpaceOnAPage (single space_on_a_page file) and readSpaceDirectory
|
|
216
|
+
* (directory) to find embedded sub-nodes within a page's content.
|
|
217
|
+
*/
|
|
218
|
+
export function extractEmbeddedNodes(body, options) {
|
|
219
|
+
const { pageTitle, pageType, metadata, fieldMap } = options;
|
|
220
|
+
const levels = metadata.hierarchy?.levels ?? [];
|
|
221
|
+
const hierarchy = levels.map((l) => l.type);
|
|
222
|
+
const relationships = metadata.relationships ?? [];
|
|
223
|
+
const typeAliases = metadata.typeAliases ?? {};
|
|
224
|
+
const isOnAPageMode = pageType === undefined || ON_A_PAGE_TYPES.includes(pageType);
|
|
225
|
+
const nodes = [];
|
|
226
|
+
// Preamble/root content sink - never added to nodes
|
|
227
|
+
const rootNode = {
|
|
228
|
+
label: '_root_',
|
|
229
|
+
title: '_root_',
|
|
230
|
+
schemaData: { type: 'space_on_a_page' },
|
|
231
|
+
linkTargets: [],
|
|
232
|
+
type: 'space_on_a_page',
|
|
233
|
+
};
|
|
234
|
+
const tree = unified().use(remarkParse).use(remarkGfm).parse(body);
|
|
235
|
+
// In typed-page mode: stack starts with the page's own virtual entry (depth 0).
|
|
236
|
+
// In space_on_a_page mode: stack starts empty (first heading has no parent).
|
|
237
|
+
const stack = !isOnAPageMode && pageTitle !== undefined
|
|
238
|
+
? [{ depth: 0, title: pageTitle, nodeType: pageType, refTarget: pageTitle }]
|
|
239
|
+
: [];
|
|
240
|
+
let parseState = 'preamble';
|
|
241
|
+
let preambleNodeCount = 0;
|
|
242
|
+
const terminatedHeadings = [];
|
|
243
|
+
/**
|
|
244
|
+
* Returns the nearest typed parent context, skipping stack entries at depth >= headingDepth
|
|
245
|
+
* so that sibling headings don't masquerade as parents.
|
|
246
|
+
*/
|
|
247
|
+
function getParentContextType(headingDepth) {
|
|
248
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
249
|
+
const entry = stack[i];
|
|
250
|
+
if (headingDepth !== undefined && entry.depth >= headingDepth)
|
|
251
|
+
continue;
|
|
252
|
+
if (entry.nodeType)
|
|
253
|
+
return entry.nodeType;
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
/** Convert a relationship definition to a normalised EmbeddingDefinition. */
|
|
258
|
+
function relationshipToEmbedding(rel) {
|
|
259
|
+
return {
|
|
260
|
+
parent: rel.parent,
|
|
261
|
+
type: rel.type,
|
|
262
|
+
field: rel.field,
|
|
263
|
+
fieldOn: rel.fieldOn,
|
|
264
|
+
multiple: rel.multiple,
|
|
265
|
+
templateFormat: rel.templateFormat ?? 'heading',
|
|
266
|
+
source: 'relationship',
|
|
267
|
+
matchers: rel.matchers,
|
|
268
|
+
embeddedTemplateFields: rel.embeddedTemplateFields,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/** Convert a hierarchy level to a normalised EmbeddingDefinition for child-level matching. */
|
|
272
|
+
function hierarchyLevelToEmbedding(level, parentType) {
|
|
273
|
+
return {
|
|
274
|
+
parent: parentType,
|
|
275
|
+
type: level.type,
|
|
276
|
+
field: level.field,
|
|
277
|
+
fieldOn: level.fieldOn,
|
|
278
|
+
multiple: level.multiple,
|
|
279
|
+
templateFormat: level.templateFormat ?? 'heading',
|
|
280
|
+
source: 'hierarchy',
|
|
281
|
+
matchers: level.matchers,
|
|
282
|
+
embeddedTemplateFields: level.embeddedTemplateFields,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Attempt to match a heading title to an embedding definition given the parent context type.
|
|
287
|
+
*
|
|
288
|
+
* Priority:
|
|
289
|
+
* 1. Relationships (explicit matchers or type name fallback)
|
|
290
|
+
* 2. Hierarchy child level (next level in hierarchy, using matchers or type name)
|
|
291
|
+
* 3. Hierarchy parent-level matching (immediate parent type — populates current node's field)
|
|
292
|
+
*/
|
|
293
|
+
function matchEmbedding(title, parentType) {
|
|
294
|
+
if (!parentType)
|
|
295
|
+
return undefined;
|
|
296
|
+
const lowerTitle = title.toLowerCase();
|
|
297
|
+
// 1. Check relationships first (explicit matches)
|
|
298
|
+
for (const rel of relationships) {
|
|
299
|
+
if (rel.parent === parentType) {
|
|
300
|
+
if (rel.matchers && matchesPattern(title, lowerTitle, rel.matchers)) {
|
|
301
|
+
return relationshipToEmbedding(rel);
|
|
302
|
+
}
|
|
303
|
+
if (lowerTitle === rel.type.toLowerCase()) {
|
|
304
|
+
return relationshipToEmbedding(rel); // fallback implicit match
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// 2. Check hierarchy child level matching
|
|
309
|
+
const parentIdx = hierarchy.indexOf(parentType);
|
|
310
|
+
if (parentIdx !== -1 && parentIdx < hierarchy.length - 1) {
|
|
311
|
+
const nextLevel = levels[parentIdx + 1];
|
|
312
|
+
if (nextLevel.matchers && matchesPattern(title, lowerTitle, nextLevel.matchers)) {
|
|
313
|
+
return hierarchyLevelToEmbedding(nextLevel, parentType);
|
|
314
|
+
}
|
|
315
|
+
if (lowerTitle === nextLevel.type.toLowerCase()) {
|
|
316
|
+
return hierarchyLevelToEmbedding(nextLevel, parentType);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// 3. Check parent-level matching: immediate parent type referenced from current node.
|
|
320
|
+
// e.g. if parentType is 'application' and heading is 'capabilities', match using
|
|
321
|
+
// the 'application' level's own field definition.
|
|
322
|
+
if (parentIdx > 0) {
|
|
323
|
+
const immediateParentLevel = levels[parentIdx - 1];
|
|
324
|
+
const matchesByMatchers = immediateParentLevel.matchers
|
|
325
|
+
? matchesPattern(title, lowerTitle, immediateParentLevel.matchers)
|
|
326
|
+
: false;
|
|
327
|
+
const matchesByType = lowerTitle === immediateParentLevel.type.toLowerCase();
|
|
328
|
+
if (matchesByMatchers || matchesByType) {
|
|
329
|
+
const currentLevel = levels[parentIdx];
|
|
330
|
+
return {
|
|
331
|
+
parent: parentType,
|
|
332
|
+
type: immediateParentLevel.type,
|
|
333
|
+
field: currentLevel.field,
|
|
334
|
+
fieldOn: currentLevel.fieldOn,
|
|
335
|
+
multiple: currentLevel.multiple,
|
|
336
|
+
templateFormat: currentLevel.templateFormat ?? 'list',
|
|
337
|
+
source: 'hierarchy',
|
|
338
|
+
matchers: immediateParentLevel.matchers,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
function makeLabel(title) {
|
|
345
|
+
return title;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Walk the stack backwards to find the deepest typed node entry (nodeType !== '').
|
|
349
|
+
* Untyped-heading placeholders (nodeType === '') are skipped so that typed headings
|
|
350
|
+
* beneath an untyped heading correctly inherit the last typed ancestor.
|
|
351
|
+
*/
|
|
352
|
+
function currentParentRef() {
|
|
353
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
354
|
+
const entry = stack[i];
|
|
355
|
+
if (entry.nodeType === '')
|
|
356
|
+
continue;
|
|
357
|
+
return `[[${entry.refTarget}]]`;
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Walk the stack from the second-to-last entry backwards to find the deepest typed node.
|
|
363
|
+
* Must be called AFTER the new heading is pushed to the stack so stack[-2] is its parent.
|
|
364
|
+
*/
|
|
365
|
+
function resolveSemanticParent() {
|
|
366
|
+
for (let i = stack.length - 2; i >= 0; i--) {
|
|
367
|
+
if (stack[i].nodeType !== '') {
|
|
368
|
+
const refTarget = stack[i].refTarget;
|
|
369
|
+
return {
|
|
370
|
+
ref: `[[${refTarget}]]`,
|
|
371
|
+
node: nodes.find((n) => n.linkTargets.includes(refTarget)),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return { ref: undefined, node: undefined };
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Emit the grouping heading node if not already emitted.
|
|
379
|
+
*/
|
|
380
|
+
function flushGrouping(g) {
|
|
381
|
+
if (!g.emitted) {
|
|
382
|
+
nodes.push(g.headingNode);
|
|
383
|
+
g.emitted = true;
|
|
384
|
+
}
|
|
385
|
+
return g.headingNode;
|
|
386
|
+
}
|
|
387
|
+
function buildHeadingLinkTargets(rawHeadingText, title, anchor) {
|
|
388
|
+
if (!pageTitle) {
|
|
389
|
+
return [title];
|
|
390
|
+
}
|
|
391
|
+
const targets = [];
|
|
392
|
+
const sectionTarget = normalizeHeadingSectionTarget(rawHeadingText);
|
|
393
|
+
if (sectionTarget) {
|
|
394
|
+
targets.push(`${pageTitle}#${sectionTarget}`);
|
|
395
|
+
}
|
|
396
|
+
if (anchor) {
|
|
397
|
+
targets.push(`${pageTitle}#^${anchor}`);
|
|
398
|
+
}
|
|
399
|
+
return targets.length > 0 ? targets : [title];
|
|
400
|
+
}
|
|
401
|
+
function buildListItemLinkTargets(title) {
|
|
402
|
+
if (!pageTitle)
|
|
403
|
+
return [title];
|
|
404
|
+
const normalized = normalizeHeadingSectionTarget(title);
|
|
405
|
+
return normalized ? [`${pageTitle}#${normalized}`] : [title];
|
|
406
|
+
}
|
|
407
|
+
let grouping = null;
|
|
408
|
+
let activeNode = rootNode;
|
|
409
|
+
for (const child of tree.children) {
|
|
410
|
+
if (parseState === 'done') {
|
|
411
|
+
if (child.type === 'heading') {
|
|
412
|
+
const rawTitle = mdastToString(child);
|
|
413
|
+
const { cleanText: afterBracketed } = extractBracketedFields(rawTitle);
|
|
414
|
+
const { cleanText: title } = extractAnchor(afterBracketed);
|
|
415
|
+
terminatedHeadings.push(title);
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (child.type === 'thematicBreak') {
|
|
420
|
+
if (parseState === 'active')
|
|
421
|
+
parseState = 'done';
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (child.type === 'heading') {
|
|
425
|
+
const heading = child;
|
|
426
|
+
const depth = heading.depth;
|
|
427
|
+
if (depth > 5)
|
|
428
|
+
continue;
|
|
429
|
+
parseState = 'active';
|
|
430
|
+
const rawText = mdastToString(heading);
|
|
431
|
+
const { cleanText: afterBracketed, fields: rawInlineFields } = extractBracketedFields(rawText);
|
|
432
|
+
const inlineFields = applyFieldMap(rawInlineFields, fieldMap);
|
|
433
|
+
const { cleanText: title, anchor } = extractAnchor(afterBracketed);
|
|
434
|
+
const parentContextType = getParentContextType(depth);
|
|
435
|
+
const anchorType = anchor ? anchorToNodeType(anchor, hierarchy, relationships) : undefined;
|
|
436
|
+
const embeddingMatch = matchEmbedding(title, parentContextType);
|
|
437
|
+
const hasExplicitType = !!inlineFields.type;
|
|
438
|
+
const hasImpliedType = !!anchorType || !!embeddingMatch;
|
|
439
|
+
if (!isOnAPageMode && !hasExplicitType && !hasImpliedType) {
|
|
440
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
|
|
441
|
+
stack.pop();
|
|
442
|
+
}
|
|
443
|
+
stack.push({ depth, title, nodeType: '', refTarget: title });
|
|
444
|
+
// Discard any pending grouping (untyped heading has no implied type)
|
|
445
|
+
grouping = null;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (isOnAPageMode && stack.length > 0) {
|
|
449
|
+
const topDepth = stack[stack.length - 1].depth;
|
|
450
|
+
if (depth > topDepth + 1) {
|
|
451
|
+
throw new Error(`Heading level skipped: jumped from H${topDepth} to H${depth} at "${title}"`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
|
|
455
|
+
stack.pop();
|
|
456
|
+
}
|
|
457
|
+
const type = inlineFields.type ?? anchorType ?? embeddingMatch?.type ?? defaultNodeType(stack, hierarchy);
|
|
458
|
+
const parentRef = currentParentRef();
|
|
459
|
+
const schemaData = {
|
|
460
|
+
title,
|
|
461
|
+
type,
|
|
462
|
+
status: DEFAULT_STATUS,
|
|
463
|
+
...inlineFields,
|
|
464
|
+
};
|
|
465
|
+
if (parentRef)
|
|
466
|
+
schemaData.parent = parentRef;
|
|
467
|
+
const linkTargets = buildHeadingLinkTargets(rawText, title, anchor);
|
|
468
|
+
const headingNode = {
|
|
469
|
+
label: makeLabel(title),
|
|
470
|
+
title,
|
|
471
|
+
schemaData,
|
|
472
|
+
linkTargets,
|
|
473
|
+
type,
|
|
474
|
+
};
|
|
475
|
+
// Push to stack BEFORE resolving semantic parent — stack[-2] is the correct parent.
|
|
476
|
+
const refTarget = linkTargets[0] ?? title;
|
|
477
|
+
stack.push({ depth, title, nodeType: type, refTarget });
|
|
478
|
+
// If this match came from a relationship/hierarchy and no explicit type was given,
|
|
479
|
+
// create a grouping — delay adding the node until we see the following content.
|
|
480
|
+
// Discard any previous grouping (not flushed — original agnostic-parse behaviour).
|
|
481
|
+
if (!hasExplicitType && !anchorType && embeddingMatch) {
|
|
482
|
+
grouping = {
|
|
483
|
+
definition: embeddingMatch,
|
|
484
|
+
semanticParent: resolveSemanticParent(),
|
|
485
|
+
headingNode,
|
|
486
|
+
emitted: false,
|
|
487
|
+
};
|
|
488
|
+
activeNode = headingNode;
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
// Explicit type or anchor type: discard any pending grouping and emit immediately.
|
|
492
|
+
grouping = null;
|
|
493
|
+
nodes.push(headingNode);
|
|
494
|
+
activeNode = headingNode;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else if (parseState !== 'active') {
|
|
498
|
+
preambleNodeCount++;
|
|
499
|
+
}
|
|
500
|
+
else if (child.type === 'list') {
|
|
501
|
+
const parentRef = currentParentRef();
|
|
502
|
+
const list = child;
|
|
503
|
+
if (grouping) {
|
|
504
|
+
const { definition, semanticParent } = grouping;
|
|
505
|
+
const isParentSide = definition.fieldOn === 'parent';
|
|
506
|
+
const parentFieldAppendArg = isParentSide && semanticParent.node ? { node: semanticParent.node, field: definition.field } : undefined;
|
|
507
|
+
// Parent-level match: definition.type is an ancestor of definition.parent in hierarchy.
|
|
508
|
+
// e.g. definition.type='capabilities', definition.parent='application' → capabilities is above application.
|
|
509
|
+
const typeIdx = hierarchy.indexOf(definition.type);
|
|
510
|
+
const parentIdx = hierarchy.indexOf(definition.parent);
|
|
511
|
+
const isParentLevelMatch = definition.source === 'hierarchy' && typeIdx !== -1 && parentIdx !== -1 && typeIdx < parentIdx;
|
|
512
|
+
const activeNodeFieldAppendArg = isParentLevelMatch && semanticParent.node
|
|
513
|
+
? { node: semanticParent.node, field: definition.field }
|
|
514
|
+
: undefined;
|
|
515
|
+
for (const item of list.children) {
|
|
516
|
+
processListItem(item, isParentSide ? undefined : semanticParent.ref, grouping.headingNode, nodes, makeLabel, buildListItemLinkTargets, typeAliases, fieldMap, definition.type, parentFieldAppendArg, activeNodeFieldAppendArg);
|
|
517
|
+
}
|
|
518
|
+
grouping = null;
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
for (const item of list.children) {
|
|
522
|
+
processListItem(item, parentRef, activeNode, nodes, makeLabel, buildListItemLinkTargets, typeAliases, fieldMap);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else if (child.type === 'table') {
|
|
527
|
+
const parentRef = currentParentRef();
|
|
528
|
+
const parentContextType = getParentContextType();
|
|
529
|
+
const table = child;
|
|
530
|
+
if (table.children && table.children.length > 0) {
|
|
531
|
+
const headerRow = table.children[0];
|
|
532
|
+
const rows = table.children.slice(1);
|
|
533
|
+
const columnNames = headerRow.children.map((cell) => mdastToString(cell).trim());
|
|
534
|
+
const firstColName = columnNames[0]?.toLowerCase();
|
|
535
|
+
let rowTypeStr;
|
|
536
|
+
let activeMatch = grouping?.definition;
|
|
537
|
+
if (activeMatch) {
|
|
538
|
+
rowTypeStr = activeMatch.type;
|
|
539
|
+
}
|
|
540
|
+
else if (firstColName) {
|
|
541
|
+
if (hierarchy.includes(firstColName) || typeAliases[firstColName]) {
|
|
542
|
+
rowTypeStr = firstColName;
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
const rootRel = matchEmbedding(firstColName, parentContextType);
|
|
546
|
+
if (rootRel) {
|
|
547
|
+
rowTypeStr = rootRel.type;
|
|
548
|
+
activeMatch = rootRel;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (!rowTypeStr && activeNode !== rootNode && activeNode.schemaData.type) {
|
|
553
|
+
const contextAsParentRel = matchEmbedding(firstColName || '', activeNode.schemaData.type);
|
|
554
|
+
if (contextAsParentRel) {
|
|
555
|
+
rowTypeStr = contextAsParentRel.type;
|
|
556
|
+
activeMatch = contextAsParentRel;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (rowTypeStr) {
|
|
560
|
+
let semanticParentRef = parentRef;
|
|
561
|
+
let semanticParentNode;
|
|
562
|
+
if (grouping) {
|
|
563
|
+
// Use already-resolved semantic parent from grouping
|
|
564
|
+
semanticParentRef = grouping.semanticParent.ref;
|
|
565
|
+
semanticParentNode = grouping.semanticParent.node;
|
|
566
|
+
}
|
|
567
|
+
else if (activeMatch || rowTypeStr === parentContextType) {
|
|
568
|
+
for (let i = stack.length - 2; i >= 0; i--) {
|
|
569
|
+
if (stack[i].nodeType !== '') {
|
|
570
|
+
semanticParentRef = `[[${stack[i].refTarget}]]`;
|
|
571
|
+
const refTarget = stack[i].refTarget;
|
|
572
|
+
semanticParentNode = nodes.find((n) => n.linkTargets.includes(refTarget));
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const isParentSide = activeMatch?.fieldOn === 'parent';
|
|
578
|
+
const tableParentFieldAppend = isParentSide && semanticParentNode && activeMatch?.field
|
|
579
|
+
? { node: semanticParentNode, field: activeMatch.field }
|
|
580
|
+
: undefined;
|
|
581
|
+
for (const row of rows) {
|
|
582
|
+
const cells = row.children;
|
|
583
|
+
if (!cells || cells.length === 0)
|
|
584
|
+
continue;
|
|
585
|
+
const titleRaw = mdastToString(cells[0]).trim();
|
|
586
|
+
const { cleanText: title, fields: rawInlineFields } = extractBracketedFields(titleRaw);
|
|
587
|
+
const inlineFields = applyFieldMap(rawInlineFields, fieldMap);
|
|
588
|
+
const schemaData = {
|
|
589
|
+
title,
|
|
590
|
+
type: rowTypeStr,
|
|
591
|
+
status: DEFAULT_STATUS,
|
|
592
|
+
...inlineFields,
|
|
593
|
+
};
|
|
594
|
+
if (semanticParentRef && !tableParentFieldAppend)
|
|
595
|
+
schemaData.parent = semanticParentRef;
|
|
596
|
+
for (let i = 1; i < columnNames.length; i++) {
|
|
597
|
+
const colName = columnNames[i];
|
|
598
|
+
const cellContent = i < cells.length ? mdastToString(cells[i]).trim() : '';
|
|
599
|
+
if (colName && cellContent) {
|
|
600
|
+
const mappedColName = fieldMap?.[colName] ?? colName;
|
|
601
|
+
schemaData[mappedColName] = cellContent;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const linkTargets = buildListItemLinkTargets(title);
|
|
605
|
+
const rowNode = {
|
|
606
|
+
label: makeLabel(title),
|
|
607
|
+
title,
|
|
608
|
+
schemaData,
|
|
609
|
+
linkTargets,
|
|
610
|
+
type: rowTypeStr,
|
|
611
|
+
};
|
|
612
|
+
nodes.push(rowNode);
|
|
613
|
+
if (tableParentFieldAppend) {
|
|
614
|
+
appendParentField(tableParentFieldAppend.node, tableParentFieldAppend.field, `[[${linkTargets[0] ?? title}]]`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
grouping = null;
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
appendContent(activeNode, mdastToString(child));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
// For any other content (paragraph, code, etc), if we had a grouping,
|
|
626
|
+
// it means the heading itself is the node. Flush it now.
|
|
627
|
+
if (grouping) {
|
|
628
|
+
flushGrouping(grouping);
|
|
629
|
+
grouping = null;
|
|
630
|
+
}
|
|
631
|
+
if (child.type === 'paragraph') {
|
|
632
|
+
const rawText = mdastToString(child);
|
|
633
|
+
const { cleanText: afterBracketed, fields: bracketedFields } = extractBracketedFields(rawText);
|
|
634
|
+
const { remainingText, fields: unbracketedFields } = extractUnbracketedFields(afterBracketed);
|
|
635
|
+
const allFields = applyFieldMap({ ...unbracketedFields, ...bracketedFields }, fieldMap);
|
|
636
|
+
if ('type' in allFields) {
|
|
637
|
+
throw new Error(`Type override via paragraph field is not supported at "${activeNode.schemaData.title ?? activeNode.label}". ` +
|
|
638
|
+
`Put [type:: ${allFields.type}] directly in the heading text.`);
|
|
639
|
+
}
|
|
640
|
+
Object.assign(activeNode.schemaData, allFields);
|
|
641
|
+
if (remainingText)
|
|
642
|
+
appendContent(activeNode, remainingText);
|
|
643
|
+
}
|
|
644
|
+
else if (child.type === 'code' && child.lang?.trim() === 'yaml') {
|
|
645
|
+
const code = child;
|
|
646
|
+
const parsed = yamlLoad(code.value);
|
|
647
|
+
if (parsed && !Array.isArray(parsed) && typeof parsed === 'object') {
|
|
648
|
+
Object.assign(activeNode.schemaData, coerceDates(applyFieldMap(parsed, fieldMap)));
|
|
649
|
+
}
|
|
650
|
+
else if (Array.isArray(parsed)) {
|
|
651
|
+
throw new Error(`YAML block must be an object at "${activeNode.label}".`);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
appendContent(activeNode, code.value);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
appendContent(activeNode, mdastToString(child));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return { nodes, preambleNodeCount, terminatedHeadings };
|
|
663
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ParseResult, PluginContext } from '../util';
|
|
2
|
+
type ReadSpaceDirectoryOptions = {
|
|
3
|
+
includeOnAPageFiles?: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare function readSpaceOnAPage(context: PluginContext): ParseResult;
|
|
6
|
+
export declare function readSpaceDirectory(context: PluginContext, options?: ReadSpaceDirectoryOptions): Promise<ParseResult>;
|
|
7
|
+
export {};
|