structured-context 0.11.0 → 0.12.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 +63 -22
- package/dist/api.d.ts +7 -5
- package/dist/api.js +4 -1
- package/dist/commands/diagram.d.ts +1 -0
- package/dist/commands/diagram.js +7 -1
- package/dist/commands/dump.d.ts +15 -2
- package/dist/commands/dump.js +18 -4
- 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 +114 -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.js +7 -4
- package/dist/commands/validate.js +38 -21
- package/dist/config.d.ts +18 -1
- package/dist/config.js +81 -10
- 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.js +39 -15
- package/dist/load-space-graph.d.ts +4 -2
- package/dist/load-space-graph.js +15 -4
- package/dist/plugins/loader.js +2 -2
- 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 +6 -0
- package/dist/plugins/markdown/parse-embedded.js +41 -7
- package/dist/plugins/markdown/read-space.js +38 -5
- package/dist/plugins/markdown/render-directory.d.ts +11 -0
- package/dist/plugins/markdown/render-directory.js +130 -0
- 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/meta-schemas.d.ts +13 -0
- package/dist/schema/meta-schemas.js +50 -0
- package/dist/schema/metadata-contract.d.ts +343 -187
- package/dist/schema/metadata-contract.js +68 -30
- package/dist/schema/schema.d.ts +14 -8
- package/dist/schema/schema.js +277 -198
- package/dist/schema/validate-graph.d.ts +2 -1
- package/dist/schema/validate-graph.js +40 -23
- package/dist/schema/validate-rules.d.ts +5 -1
- package/dist/schema/validate-rules.js +20 -1
- package/dist/schemas/_knowledge_wiki.json +20 -20
- package/dist/schemas/_ost_strict.json +2 -2
- package/dist/schemas/_sctx_base.json +2 -2
- 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/latest/_structured_context_schema_meta.json +229 -0
- package/dist/schemas/knowledge_wiki.json +10 -9
- package/dist/schemas/strategy_general.json +15 -14
- package/dist/schemas/strict_ost.json +17 -16
- package/dist/space-context.d.ts +3 -0
- package/dist/space-context.js +7 -2
- 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 +54 -13
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +19 -9
- package/docs/concepts.md +14 -4
- package/docs/config.md +8 -0
- package/docs/rules.md +49 -22
- package/docs/schemas.md +110 -35
- package/package.json +13 -8
- package/schemas/_knowledge_wiki.json +20 -20
- package/schemas/_ost_strict.json +2 -2
- package/schemas/_sctx_base.json +2 -2
- package/schemas/_strategy_general.json +38 -38
- package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
- package/schemas/knowledge_wiki.json +10 -9
- 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
package/dist/plugins/util.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
1
4
|
import { PLUGIN_PREFIX as _PLUGIN_PREFIX } from '../constants';
|
|
2
5
|
export const PLUGIN_PREFIX = _PLUGIN_PREFIX;
|
|
3
6
|
export const CONFIG_PLUGINS_DIR = 'plugins';
|
|
@@ -9,3 +12,139 @@ export function normalizePluginName(name) {
|
|
|
9
12
|
export function shortenPluginName(name) {
|
|
10
13
|
return name.startsWith(PLUGIN_PREFIX) ? name.slice(PLUGIN_PREFIX.length) : name;
|
|
11
14
|
}
|
|
15
|
+
/** Type guard for multi-file render result. */
|
|
16
|
+
export function isRenderMultiFileOutput(result) {
|
|
17
|
+
return typeof result === 'object' && result !== null && Array.isArray(result.files);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Synchronously locate the root directory of an external plugin.
|
|
21
|
+
* Resolution order:
|
|
22
|
+
* 1. Config-adjacent: {configDir}/plugins/{name} or {configDir}/plugins/sctx-{name}
|
|
23
|
+
* 2. Node module: resolve from configDir using standard module resolution
|
|
24
|
+
*/
|
|
25
|
+
export function resolvePluginDir(pluginName, configDir) {
|
|
26
|
+
const normalized = normalizePluginName(pluginName);
|
|
27
|
+
const short = shortenPluginName(pluginName);
|
|
28
|
+
for (const candidate of [normalized, short]) {
|
|
29
|
+
const localDir = resolve(configDir, CONFIG_PLUGINS_DIR, candidate);
|
|
30
|
+
if (existsSync(localDir)) {
|
|
31
|
+
try {
|
|
32
|
+
if (statSync(localDir).isDirectory()) {
|
|
33
|
+
return localDir;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Node module resolution from configDir
|
|
40
|
+
for (const candidate of [normalized, short]) {
|
|
41
|
+
try {
|
|
42
|
+
const req = createRequire(join(configDir, 'package.json'));
|
|
43
|
+
const pkgPath = req.resolve(`${candidate}/package.json`);
|
|
44
|
+
return dirname(pkgPath);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
const nmCandidate = resolve(configDir, 'node_modules', candidate);
|
|
48
|
+
if (existsSync(nmCandidate)) {
|
|
49
|
+
try {
|
|
50
|
+
if (statSync(nmCandidate).isDirectory()) {
|
|
51
|
+
return nmCandidate;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Synchronously locate the schemas directory for a plugin.
|
|
62
|
+
* Convention:
|
|
63
|
+
* 1. package.json "sctx": { "schemas": "..." }
|
|
64
|
+
* 2. Convention: <pluginDir>/schemas
|
|
65
|
+
* 3. Fallback for test fixtures: {configDir}/plugins/schemas
|
|
66
|
+
*/
|
|
67
|
+
export function resolvePluginSchemasDir(pluginName, configDir) {
|
|
68
|
+
const dir = resolvePluginDir(pluginName, configDir);
|
|
69
|
+
if (dir) {
|
|
70
|
+
// 1. package.json sctx.schemas
|
|
71
|
+
const pkgJsonPath = join(dir, 'package.json');
|
|
72
|
+
if (existsSync(pkgJsonPath)) {
|
|
73
|
+
try {
|
|
74
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
|
75
|
+
if (pkg.sctx?.schemas && typeof pkg.sctx.schemas === 'string') {
|
|
76
|
+
const customSchemas = resolve(dir, pkg.sctx.schemas);
|
|
77
|
+
if (existsSync(customSchemas) && statSync(customSchemas).isDirectory()) {
|
|
78
|
+
return customSchemas;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
}
|
|
84
|
+
// 2. Convention: <pluginDir>/schemas
|
|
85
|
+
const conventionDir = join(dir, 'schemas');
|
|
86
|
+
if (existsSync(conventionDir)) {
|
|
87
|
+
try {
|
|
88
|
+
if (statSync(conventionDir).isDirectory()) {
|
|
89
|
+
return conventionDir;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch { }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 3. Check config-adjacent plugins/schemas as fallback (e.g. test fixtures)
|
|
96
|
+
const fallbackSchemas = resolve(configDir, CONFIG_PLUGINS_DIR, 'schemas');
|
|
97
|
+
if (existsSync(fallbackSchemas)) {
|
|
98
|
+
try {
|
|
99
|
+
if (statSync(fallbackSchemas).isDirectory()) {
|
|
100
|
+
return fallbackSchemas;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Synchronously collect all plugin schema sources available to a space.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveSpacePluginSchemas(space, config, configDir) {
|
|
111
|
+
const pluginSources = [];
|
|
112
|
+
const seenPlugins = new Set();
|
|
113
|
+
const maybeAddPlugin = (name) => {
|
|
114
|
+
const shortName = shortenPluginName(name);
|
|
115
|
+
if (seenPlugins.has(shortName))
|
|
116
|
+
return;
|
|
117
|
+
seenPlugins.add(shortName);
|
|
118
|
+
const schemasDir = resolvePluginSchemasDir(name, configDir);
|
|
119
|
+
if (schemasDir) {
|
|
120
|
+
pluginSources.push({
|
|
121
|
+
pluginName: shortName,
|
|
122
|
+
schemasDir,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
// 1. Plugin referenced by space.schema prefix if any (e.g. sctx-wardley-mapping/wardley_map.json)
|
|
127
|
+
const rawSchema = space?.schema ?? config.schema;
|
|
128
|
+
if (rawSchema && !isAbsolute(rawSchema) && !rawSchema.startsWith('.')) {
|
|
129
|
+
const normalized = rawSchema.replace(/\\/g, '/');
|
|
130
|
+
const slashIdx = normalized.indexOf('/');
|
|
131
|
+
if (slashIdx !== -1) {
|
|
132
|
+
const prefix = normalized.slice(0, slashIdx);
|
|
133
|
+
maybeAddPlugin(prefix);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// 2. Plugins configured on space
|
|
137
|
+
if (space?.plugins) {
|
|
138
|
+
for (const name of Object.keys(space.plugins)) {
|
|
139
|
+
maybeAddPlugin(name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// 3. Plugins configured globally
|
|
143
|
+
const globalPlugins = config.plugins;
|
|
144
|
+
if (globalPlugins) {
|
|
145
|
+
for (const name of Object.keys(globalPlugins)) {
|
|
146
|
+
maybeAddPlugin(name);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return pluginSources;
|
|
150
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BaseNode, SchemaMetadata, SpaceNode, UnresolvedRef } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Enrich parsed nodes into SpaceNodes by applying type alias resolution and resolving
|
|
4
|
-
* parent links using the hierarchy
|
|
4
|
+
* parent links using the levels of every hierarchy and the relationships from schema metadata.
|
|
5
5
|
*
|
|
6
6
|
* Returns the enriched nodes and any unresolved refs (broken/invalid wikilinks).
|
|
7
7
|
*/
|
|
@@ -21,19 +21,27 @@ function getRefs(rawField, multiple) {
|
|
|
21
21
|
* @param nodesByType Map of node type to nodes
|
|
22
22
|
* @param targetIndex Map of link targets to nodes
|
|
23
23
|
* @param edge The edge definition (child type, parent type, field, fieldOn, multiple)
|
|
24
|
-
* @param source Whether this edge comes from hierarchy
|
|
24
|
+
* @param source Whether this edge comes from hierarchy levels or relationships
|
|
25
|
+
* @param hierarchy Name of the hierarchy the edge belongs to (hierarchy edges only)
|
|
25
26
|
* @param selfRef Whether child and parent are the same node type
|
|
26
27
|
* @param unresolvedRefs Array to push broken/invalid link entries into
|
|
27
28
|
* @param typeAliases Optional type aliases for resolution
|
|
28
29
|
*/
|
|
29
|
-
function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolvedRefs, typeAliases) {
|
|
30
|
+
function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef, unresolvedRefs, typeAliases) {
|
|
30
31
|
const { type: rawChildType, parent: rawParentType, field, fieldOn, multiple } = edge;
|
|
31
32
|
const childType = resolveNodeType(rawChildType, typeAliases);
|
|
32
33
|
const parentType = resolveNodeType(rawParentType, typeAliases);
|
|
33
34
|
function pushParentRef(childNode, parentTitle) {
|
|
34
35
|
// Deduplicate by (field, title) — same parent via different fields is intentional
|
|
35
36
|
if (!childNode.resolvedParents.some((r) => r.field === field && r.title === parentTitle)) {
|
|
36
|
-
childNode.resolvedParents.push({
|
|
37
|
+
childNode.resolvedParents.push({
|
|
38
|
+
title: parentTitle,
|
|
39
|
+
field,
|
|
40
|
+
fieldOn,
|
|
41
|
+
source,
|
|
42
|
+
...(hierarchy !== undefined ? { hierarchy } : {}),
|
|
43
|
+
selfRef,
|
|
44
|
+
});
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
function checkShape(ownerNode, rawField) {
|
|
@@ -54,6 +62,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolved
|
|
|
54
62
|
field,
|
|
55
63
|
reason: 'invalid_shape',
|
|
56
64
|
message: `Field "${field}" must be a wikilink string, got ${typeof rawField}`,
|
|
65
|
+
source: ownerNode.source,
|
|
57
66
|
});
|
|
58
67
|
return false;
|
|
59
68
|
}
|
|
@@ -69,6 +78,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolved
|
|
|
69
78
|
field,
|
|
70
79
|
reason: 'not_found',
|
|
71
80
|
message: `Link target "${target}" in field "${field}" not found`,
|
|
81
|
+
source: ownerNode.source,
|
|
72
82
|
});
|
|
73
83
|
return null;
|
|
74
84
|
}
|
|
@@ -79,6 +89,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolved
|
|
|
79
89
|
field,
|
|
80
90
|
reason: 'ambiguous',
|
|
81
91
|
message: `Link target "${target}" in field "${field}" is ambiguous (matches multiple nodes)`,
|
|
92
|
+
source: ownerNode.source,
|
|
82
93
|
});
|
|
83
94
|
return null;
|
|
84
95
|
}
|
|
@@ -155,12 +166,12 @@ function classifyContentLink(link, targetIndex) {
|
|
|
155
166
|
}
|
|
156
167
|
/**
|
|
157
168
|
* Enrich parsed nodes into SpaceNodes by applying type alias resolution and resolving
|
|
158
|
-
* parent links using the hierarchy
|
|
169
|
+
* parent links using the levels of every hierarchy and the relationships from schema metadata.
|
|
159
170
|
*
|
|
160
171
|
* Returns the enriched nodes and any unresolved refs (broken/invalid wikilinks).
|
|
161
172
|
*/
|
|
162
173
|
export function resolveGraphEdges(nodes, metadata) {
|
|
163
|
-
const
|
|
174
|
+
const hierarchies = metadata.hierarchies ?? [];
|
|
164
175
|
const relationships = metadata.relationships ?? [];
|
|
165
176
|
const typeAliases = metadata.typeAliases;
|
|
166
177
|
const unresolvedRefs = [];
|
|
@@ -180,27 +191,35 @@ export function resolveGraphEdges(nodes, metadata) {
|
|
|
180
191
|
}
|
|
181
192
|
nodesByType.get(type).push(node);
|
|
182
193
|
}
|
|
183
|
-
// 1. Process
|
|
184
|
-
for (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
194
|
+
// 1. Process the levels of each hierarchy
|
|
195
|
+
for (const { name, levels } of hierarchies) {
|
|
196
|
+
for (let i = 0; i < levels.length; i++) {
|
|
197
|
+
const level = levels[i];
|
|
198
|
+
// Regular relationship (child type → parent type)
|
|
199
|
+
if (i > 0) {
|
|
200
|
+
const parentLevel = levels[i - 1];
|
|
201
|
+
resolveEdge(nodesByType, targetIndex, {
|
|
202
|
+
type: level.type,
|
|
203
|
+
parent: parentLevel.type,
|
|
204
|
+
field: level.field,
|
|
205
|
+
fieldOn: level.fieldOn,
|
|
206
|
+
multiple: level.multiple,
|
|
207
|
+
}, 'hierarchy', name, false, unresolvedRefs, typeAliases);
|
|
208
|
+
}
|
|
209
|
+
// Same-type relationship (child type → same type) via primary field
|
|
210
|
+
if (level.selfRef) {
|
|
211
|
+
resolveEdge(nodesByType, targetIndex, {
|
|
212
|
+
type: level.type,
|
|
213
|
+
parent: level.type,
|
|
214
|
+
field: level.field,
|
|
215
|
+
fieldOn: level.fieldOn,
|
|
216
|
+
multiple: level.multiple,
|
|
217
|
+
}, 'hierarchy', name, true, unresolvedRefs, typeAliases);
|
|
218
|
+
}
|
|
219
|
+
// Same-type relationship via explicit selfRefField
|
|
220
|
+
if (level.selfRefField) {
|
|
221
|
+
resolveEdge(nodesByType, targetIndex, { type: level.type, parent: level.type, field: level.selfRefField, fieldOn: 'child', multiple: false }, 'hierarchy', name, true, unresolvedRefs, typeAliases);
|
|
222
|
+
}
|
|
204
223
|
}
|
|
205
224
|
}
|
|
206
225
|
// 2. Process additional relationships
|
|
@@ -212,7 +231,7 @@ export function resolveGraphEdges(nodes, metadata) {
|
|
|
212
231
|
fieldOn: rel.fieldOn,
|
|
213
232
|
multiple: rel.multiple,
|
|
214
233
|
};
|
|
215
|
-
resolveEdge(nodesByType, targetIndex, edge, 'relationship', rel.type === rel.parent, unresolvedRefs, typeAliases);
|
|
234
|
+
resolveEdge(nodesByType, targetIndex, edge, 'relationship', undefined, rel.type === rel.parent, unresolvedRefs, typeAliases);
|
|
216
235
|
}
|
|
217
236
|
// 3. Resolve content links — classify each raw ContentLink into a ResolvedContentLink.
|
|
218
237
|
for (const node of spaceNodes) {
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import type { BaseNode } from '../types';
|
|
2
|
+
export interface ParsedWikilink {
|
|
3
|
+
target: string;
|
|
4
|
+
anchor?: string;
|
|
5
|
+
displayText?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Format a wikilink string.
|
|
9
|
+
* formatWikilink('My Goal') -> '[[My Goal]]'
|
|
10
|
+
* formatWikilink('My Goal', 'Custom Display') -> '[[My Goal|Custom Display]]'
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatWikilink(target: string, alias?: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Parse a wikilink or link inner string into target, anchor, and displayText.
|
|
15
|
+
* Handles: [[target]], [[target#anchor]], [[target|alias]], [[target#anchor|alias]]
|
|
16
|
+
* Also handles bare forms without enclosing brackets.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseWikilink(wikilink: string): ParsedWikilink;
|
|
2
19
|
/**
|
|
3
20
|
* Extract the lookup key from a wikilink string such as:
|
|
4
21
|
* [[Personal Vision]] → "Personal Vision"
|
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a wikilink string.
|
|
3
|
+
* formatWikilink('My Goal') -> '[[My Goal]]'
|
|
4
|
+
* formatWikilink('My Goal', 'Custom Display') -> '[[My Goal|Custom Display]]'
|
|
5
|
+
*/
|
|
6
|
+
export function formatWikilink(target, alias) {
|
|
7
|
+
const trimmedTarget = target.trim();
|
|
8
|
+
const trimmedAlias = alias?.trim();
|
|
9
|
+
if (trimmedAlias && trimmedAlias !== trimmedTarget) {
|
|
10
|
+
return `[[${trimmedTarget}|${trimmedAlias}]]`;
|
|
11
|
+
}
|
|
12
|
+
return `[[${trimmedTarget}]]`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Parse a wikilink or link inner string into target, anchor, and displayText.
|
|
16
|
+
* Handles: [[target]], [[target#anchor]], [[target|alias]], [[target#anchor|alias]]
|
|
17
|
+
* Also handles bare forms without enclosing brackets.
|
|
18
|
+
*/
|
|
19
|
+
export function parseWikilink(wikilink) {
|
|
20
|
+
const cleaned = wikilink.replace(/^"|"$/g, '').trim();
|
|
21
|
+
const inner = cleaned.startsWith('[[') && cleaned.endsWith(']]') ? cleaned.slice(2, -2).trim() : cleaned;
|
|
22
|
+
// Alias: [[target|alias]] or [[target#anchor|alias]]
|
|
23
|
+
const pipeIdx = inner.indexOf('|');
|
|
24
|
+
let core = inner;
|
|
25
|
+
let displayText;
|
|
26
|
+
if (pipeIdx >= 0) {
|
|
27
|
+
core = inner.slice(0, pipeIdx);
|
|
28
|
+
const alias = inner.slice(pipeIdx + 1).trim();
|
|
29
|
+
if (alias)
|
|
30
|
+
displayText = alias;
|
|
31
|
+
}
|
|
32
|
+
// Anchor: [[target#anchor]] or [[target#^block]]
|
|
33
|
+
const hashIdx = core.indexOf('#');
|
|
34
|
+
if (hashIdx >= 0) {
|
|
35
|
+
const target = core.slice(0, hashIdx).trim();
|
|
36
|
+
const anchor = core.slice(hashIdx + 1).trim() || undefined;
|
|
37
|
+
return { target, anchor, displayText };
|
|
38
|
+
}
|
|
39
|
+
return { target: core.trim(), displayText };
|
|
40
|
+
}
|
|
1
41
|
/**
|
|
2
42
|
* Extract the lookup key from a wikilink string such as:
|
|
3
43
|
* [[Personal Vision]] → "Personal Vision"
|
package/dist/render/render.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type RenderResult } from '../plugins/util';
|
|
1
2
|
import type { SpaceContext } from '../types';
|
|
2
3
|
export declare function executeRender(formatName: string, context: SpaceContext, options: {
|
|
3
4
|
filter?: string;
|
|
5
|
+
hierarchy?: string;
|
|
4
6
|
data?: Record<string, unknown>;
|
|
5
|
-
}): Promise<
|
|
7
|
+
}): Promise<RenderResult>;
|
package/dist/render/render.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { shortenPluginName } from '
|
|
1
|
+
import { shortenPluginName } from '../plugins/util';
|
|
2
2
|
import { updateSpaceField } from '../config';
|
|
3
3
|
import { assembleSpaceGraph } from '../load-space-graph';
|
|
4
4
|
import { loadPlugins } from '../plugins/loader';
|
|
@@ -12,7 +12,7 @@ export async function executeRender(formatName, context, options) {
|
|
|
12
12
|
const available = registry.map((r) => r.qualifiedName).join(', ');
|
|
13
13
|
throw new Error(`Unknown render format: "${formatName}".${available ? ` Available: ${available}` : ' No formats registered.'}`);
|
|
14
14
|
}
|
|
15
|
-
const graph = await assembleSpaceGraph(context, { filter: options.filter });
|
|
15
|
+
const graph = await assembleSpaceGraph(context, { filter: options.filter, hierarchy: options.hierarchy });
|
|
16
16
|
const shortName = shortenPluginName(entry.plugin.plugin.name);
|
|
17
17
|
const pluginContext = {
|
|
18
18
|
...context,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AnySchemaObject } from 'ajv';
|
|
2
|
+
/** Version folders under `generatedDir` (named after the release that first shipped the dialect), oldest first. */
|
|
3
|
+
export declare function listMetaVersions(generatedDir: string): string[];
|
|
4
|
+
/** The newest released meta-schema and its version folder, if any. */
|
|
5
|
+
export declare function readNewestMetaSchema(generatedDir: string): {
|
|
6
|
+
version: string;
|
|
7
|
+
schema: AnySchemaObject;
|
|
8
|
+
} | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Meta-schemas shipped in `<schemasDir>/generated/`, keyed by `$id`: every versioned folder at its own `$id`, and
|
|
11
|
+
* `latest` registered under the `latest` URL so `$schema` references to either form resolve.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadMetaSchemas(schemasDir: string): Map<string, AnySchemaObject>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { SCHEMA_META_FILE, SCHEMA_META_ID } from './metadata-contract';
|
|
4
|
+
const LATEST_FOLDER = 'latest';
|
|
5
|
+
const VERSION_FOLDER = /^\d+\.\d+\.\d+$/;
|
|
6
|
+
function compareVersions(a, b) {
|
|
7
|
+
const [pa, pb] = [a, b].map((v) => v.split('.').map(Number));
|
|
8
|
+
for (let i = 0; i < 3; i++) {
|
|
9
|
+
const diff = (pa?.[i] ?? 0) - (pb?.[i] ?? 0);
|
|
10
|
+
if (diff !== 0)
|
|
11
|
+
return diff;
|
|
12
|
+
}
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
/** Version folders under `generatedDir` (named after the release that first shipped the dialect), oldest first. */
|
|
16
|
+
export function listMetaVersions(generatedDir) {
|
|
17
|
+
if (!existsSync(generatedDir))
|
|
18
|
+
return [];
|
|
19
|
+
return readdirSync(generatedDir)
|
|
20
|
+
.filter((name) => VERSION_FOLDER.test(name))
|
|
21
|
+
.sort(compareVersions);
|
|
22
|
+
}
|
|
23
|
+
function readMetaSchema(generatedDir, folder) {
|
|
24
|
+
const path = join(generatedDir, folder, SCHEMA_META_FILE);
|
|
25
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf-8')) : undefined;
|
|
26
|
+
}
|
|
27
|
+
/** The newest released meta-schema and its version folder, if any. */
|
|
28
|
+
export function readNewestMetaSchema(generatedDir) {
|
|
29
|
+
const version = listMetaVersions(generatedDir).at(-1);
|
|
30
|
+
const schema = version && readMetaSchema(generatedDir, version);
|
|
31
|
+
return version && schema ? { version, schema } : undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Meta-schemas shipped in `<schemasDir>/generated/`, keyed by `$id`: every versioned folder at its own `$id`, and
|
|
35
|
+
* `latest` registered under the `latest` URL so `$schema` references to either form resolve.
|
|
36
|
+
*/
|
|
37
|
+
export function loadMetaSchemas(schemasDir) {
|
|
38
|
+
const generatedDir = join(schemasDir, 'generated');
|
|
39
|
+
const schemas = new Map();
|
|
40
|
+
for (const version of listMetaVersions(generatedDir)) {
|
|
41
|
+
const schema = readMetaSchema(generatedDir, version);
|
|
42
|
+
if (schema && typeof schema.$id === 'string')
|
|
43
|
+
schemas.set(schema.$id, schema);
|
|
44
|
+
}
|
|
45
|
+
// The `latest` copy carries its versioned `$id`, which a version folder may already own; register it under its own URL.
|
|
46
|
+
const latest = readMetaSchema(generatedDir, LATEST_FOLDER);
|
|
47
|
+
if (latest)
|
|
48
|
+
schemas.set(SCHEMA_META_ID, { ...latest, $id: SCHEMA_META_ID });
|
|
49
|
+
return schemas;
|
|
50
|
+
}
|