structured-context 0.9.3 → 0.11.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 +22 -3
- package/dist/api.d.ts +33 -0
- package/dist/api.js +26 -0
- package/dist/commands/diagram.js +1 -1
- package/dist/commands/dump.js +1 -1
- package/dist/commands/miro-sync.d.ts +3 -0
- package/dist/commands/miro-sync.js +8 -0
- package/dist/commands/schemas.js +3 -2
- package/dist/commands/spaces.js +0 -2
- package/dist/commands/validate-file.d.ts +5 -24
- package/dist/commands/validate-file.js +26 -108
- package/dist/commands/validate.d.ts +1 -12
- package/dist/commands/validate.js +22 -157
- package/dist/config.d.ts +8 -12
- package/dist/config.js +25 -23
- package/dist/index.js +15 -23
- package/dist/load-space-graph.d.ts +40 -0
- package/dist/load-space-graph.js +42 -0
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/loader.js +2 -2
- package/dist/plugins/markdown/extract-content-links.d.ts +23 -0
- package/dist/plugins/markdown/extract-content-links.js +144 -0
- package/dist/plugins/markdown/index.d.ts +42 -32
- package/dist/plugins/markdown/index.js +10 -15
- package/dist/plugins/markdown/parse-embedded.d.ts +1 -1
- package/dist/plugins/markdown/parse-embedded.js +9 -0
- package/dist/plugins/markdown/read-space.js +6 -3
- package/dist/plugins/markdown/template-sync.d.ts +1 -1
- package/dist/plugins/markdown/template-sync.js +2 -2
- package/dist/plugins/mermaid/index.d.ts +2 -0
- package/dist/plugins/mermaid/index.js +14 -0
- package/dist/plugins/{markdown → mermaid}/render-mermaid.js +17 -16
- package/dist/{integrations → plugins}/miro/cache.d.ts +1 -1
- package/dist/plugins/miro/index.d.ts +2 -0
- package/dist/plugins/miro/index.js +27 -0
- package/dist/{integrations → plugins}/miro/layout.d.ts +2 -2
- package/dist/{integrations → plugins}/miro/layout.js +3 -16
- package/dist/{integrations → plugins}/miro/styles.d.ts +2 -2
- package/dist/plugins/miro/sync.d.ts +7 -0
- package/dist/{integrations → plugins}/miro/sync.js +23 -41
- package/dist/plugins/util.d.ts +9 -0
- package/dist/plugins/util.js +4 -0
- package/dist/read/resolve-graph-edges.js +32 -0
- package/dist/render/render.d.ts +1 -0
- package/dist/render/render.js +20 -16
- package/dist/schema/evaluate-rule.js +1 -0
- package/dist/schema/schema.d.ts +3 -1
- package/dist/schema/schema.js +7 -3
- package/dist/schemas/_knowledge_wiki.json +1 -1
- package/dist/space-context.d.ts +24 -0
- package/dist/space-context.js +41 -0
- package/dist/types.d.ts +72 -2
- package/dist/validate.d.ts +59 -0
- package/dist/validate.js +316 -0
- package/docs/concepts.md +23 -0
- package/docs/config.md +2 -0
- package/package.json +11 -5
- package/schemas/_knowledge_wiki.json +1 -1
- package/dist/integrations/miro/sync.d.ts +0 -8
- package/dist/plugin-api.d.ts +0 -12
- package/dist/plugin-api.js +0 -7
- /package/dist/plugins/{markdown → mermaid}/render-mermaid.d.ts +0 -0
- /package/dist/{integrations → plugins}/miro/cache.js +0 -0
- /package/dist/{integrations → plugins}/miro/client.d.ts +0 -0
- /package/dist/{integrations → plugins}/miro/client.js +0 -0
- /package/dist/{integrations → plugins}/miro/styles.js +0 -0
package/dist/config.d.ts
CHANGED
|
@@ -2,14 +2,18 @@ export type SpaceConfig = {
|
|
|
2
2
|
name: string;
|
|
3
3
|
path: string;
|
|
4
4
|
schema?: string;
|
|
5
|
-
miroBoardId?: string;
|
|
6
|
-
miroFrameId?: string;
|
|
7
5
|
/** Plugin name → plugin config map. Overrides top-level plugins when set. */
|
|
8
6
|
plugins?: Record<string, Record<string, unknown>>;
|
|
9
7
|
/** Named filter views for this space. Keys are view names; values contain the filter expression. */
|
|
10
8
|
views?: Record<string, {
|
|
11
9
|
expression: string;
|
|
12
10
|
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Directory of the config file that defined this space, used to anchor relative plugin-config
|
|
13
|
+
* paths. Set by loadConfig; absent for a hand-assembled Config, whose callers must pass an
|
|
14
|
+
* explicit configDir to createSpaceContext. Not persisted by updateSpaceField.
|
|
15
|
+
*/
|
|
16
|
+
sourceDir?: string;
|
|
13
17
|
};
|
|
14
18
|
export type Config = {
|
|
15
19
|
spaces: SpaceConfig[];
|
|
@@ -20,19 +24,11 @@ export type Config = {
|
|
|
20
24
|
export declare function setConfigPath(path: string | undefined): void;
|
|
21
25
|
/** Get all config file paths that were loaded (main config + included configs). */
|
|
22
26
|
export declare function getConfigSourceFiles(): Set<string>;
|
|
23
|
-
/**
|
|
24
|
-
* Get the directory of the config file that defines a given space.
|
|
25
|
-
*/
|
|
26
|
-
export declare function getSpaceConfigDir(spaceName: string): string;
|
|
27
27
|
export declare function configPath(): string;
|
|
28
28
|
export declare function loadConfig(): Config;
|
|
29
29
|
/** Get the full space config entry by name. Throws if not found. */
|
|
30
30
|
export declare function getSpaceConfig(name: string, config: Config): SpaceConfig;
|
|
31
31
|
/** Resolve schema path: CLI arg > space-level config > global config. Throws if none configured. */
|
|
32
32
|
export declare function resolveSchema(config: Config, space?: SpaceConfig): string;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}[keyof T];
|
|
36
|
-
/** Update a string field on a space entry and persist config. */
|
|
37
|
-
export declare function updateSpaceField(spaceName: string, field: StringFields<SpaceConfig>, value: string): void;
|
|
38
|
-
export {};
|
|
33
|
+
/** Update a string field on a space entry and persist config. When `plugin` is given, updates that field inside `space.plugins[plugin]` instead. */
|
|
34
|
+
export declare function updateSpaceField(spaceName: string, field: string, value: string, plugin?: string): void;
|
package/dist/config.js
CHANGED
|
@@ -2,9 +2,9 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import Ajv from 'ajv';
|
|
5
|
-
import
|
|
5
|
+
import JSON5 from 'json5';
|
|
6
6
|
import { ENV_CONFIG_VAR, XDG_CONFIG_DIR } from './constants';
|
|
7
|
-
import { normalizePluginName } from './plugins/util';
|
|
7
|
+
import { normalizePluginName, shortenPluginName } from './plugins/util';
|
|
8
8
|
import { bundledSchemasDir } from './schema/schema';
|
|
9
9
|
const CONFIG_SCHEMA = {
|
|
10
10
|
type: 'object',
|
|
@@ -17,8 +17,6 @@ const CONFIG_SCHEMA = {
|
|
|
17
17
|
name: { type: 'string', pattern: '^[a-z0-9_-]+$' },
|
|
18
18
|
path: { type: 'string' },
|
|
19
19
|
schema: { type: 'string' },
|
|
20
|
-
miroBoardId: { type: 'string' },
|
|
21
|
-
miroFrameId: { type: 'string' },
|
|
22
20
|
plugins: { type: 'object', additionalProperties: { type: 'object' } },
|
|
23
21
|
views: {
|
|
24
22
|
type: 'object',
|
|
@@ -37,7 +35,7 @@ const CONFIG_SCHEMA = {
|
|
|
37
35
|
schema: { type: 'string' },
|
|
38
36
|
includeSpacesFrom: { type: 'array', items: { type: 'string' } },
|
|
39
37
|
},
|
|
40
|
-
required: ['spaces'],
|
|
38
|
+
anyOf: [{ required: ['spaces'] }, { required: ['includeSpacesFrom'] }],
|
|
41
39
|
additionalProperties: false,
|
|
42
40
|
};
|
|
43
41
|
let _configPathOverride;
|
|
@@ -54,18 +52,6 @@ export function getConfigSourceFiles() {
|
|
|
54
52
|
}
|
|
55
53
|
return new Set(_spaceSourceFiles.values());
|
|
56
54
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Get the directory of the config file that defines a given space.
|
|
59
|
-
*/
|
|
60
|
-
export function getSpaceConfigDir(spaceName) {
|
|
61
|
-
if (_spaceSourceFiles.size === 0) {
|
|
62
|
-
loadConfig();
|
|
63
|
-
}
|
|
64
|
-
const spaceConfigPath = _spaceSourceFiles.get(spaceName);
|
|
65
|
-
if (!spaceConfigPath)
|
|
66
|
-
throw new Error('Space config path not found');
|
|
67
|
-
return dirname(spaceConfigPath);
|
|
68
|
-
}
|
|
69
55
|
export function configPath() {
|
|
70
56
|
if (_configPathOverride) {
|
|
71
57
|
return _configPathOverride;
|
|
@@ -130,6 +116,8 @@ function _loadConfig(path) {
|
|
|
130
116
|
if (!validate(config)) {
|
|
131
117
|
throw new Error(`Invalid config in ${path}: ${JSON.stringify(validate.errors)}`);
|
|
132
118
|
}
|
|
119
|
+
if (!config.spaces)
|
|
120
|
+
config.spaces = [];
|
|
133
121
|
return config;
|
|
134
122
|
}
|
|
135
123
|
export function loadConfig() {
|
|
@@ -141,8 +129,10 @@ export function loadConfig() {
|
|
|
141
129
|
const config = _loadConfig(path);
|
|
142
130
|
_spaceSourceFiles.clear();
|
|
143
131
|
// Track which spaces come from the main config file
|
|
132
|
+
const mainSourceDir = dirname(resolve(path));
|
|
144
133
|
for (const space of config.spaces) {
|
|
145
134
|
_spaceSourceFiles.set(space.name, path);
|
|
135
|
+
space.sourceDir = mainSourceDir;
|
|
146
136
|
}
|
|
147
137
|
// Load includeSpacesFrom configs and merge their spaces in, with later entries taking precedence over earlier ones
|
|
148
138
|
if (config.includeSpacesFrom) {
|
|
@@ -154,8 +144,10 @@ export function loadConfig() {
|
|
|
154
144
|
throw new Error(`Included config contains spaces with duplicate names: ${resolvedIncludePath}`);
|
|
155
145
|
}
|
|
156
146
|
// Track which spaces come from this included config file
|
|
147
|
+
const includeSourceDir = dirname(resolvedIncludePath);
|
|
157
148
|
for (const space of includedConfig.spaces) {
|
|
158
149
|
_spaceSourceFiles.set(space.name, resolvedIncludePath);
|
|
150
|
+
space.sourceDir = includeSourceDir;
|
|
159
151
|
}
|
|
160
152
|
config.spaces.push(...includedConfig.spaces);
|
|
161
153
|
}
|
|
@@ -178,17 +170,27 @@ export function resolveSchema(config, space) {
|
|
|
178
170
|
}
|
|
179
171
|
return schema;
|
|
180
172
|
}
|
|
181
|
-
/** Update a string field on a space entry and persist config. */
|
|
182
|
-
export function updateSpaceField(spaceName, field, value) {
|
|
173
|
+
/** Update a string field on a space entry and persist config. When `plugin` is given, updates that field inside `space.plugins[plugin]` instead. */
|
|
174
|
+
export function updateSpaceField(spaceName, field, value, plugin) {
|
|
183
175
|
const sourcePath = _spaceSourceFiles.get(spaceName);
|
|
184
|
-
if (!sourcePath)
|
|
176
|
+
if (!sourcePath)
|
|
185
177
|
throw new Error(`Space "${spaceName}" not found in any config file`);
|
|
186
|
-
}
|
|
187
178
|
const config = _loadConfig(sourcePath);
|
|
188
179
|
const space = config.spaces?.find((s) => s.name === spaceName);
|
|
189
|
-
if (!space)
|
|
180
|
+
if (!space)
|
|
190
181
|
throw new Error(`Unknown space config: "${spaceName}". Check config.`);
|
|
182
|
+
if (plugin !== undefined) {
|
|
183
|
+
if (!space.plugins)
|
|
184
|
+
space.plugins = {};
|
|
185
|
+
const normalized = normalizePluginName(plugin);
|
|
186
|
+
const shortName = shortenPluginName(normalized);
|
|
187
|
+
const existingKey = Object.keys(space.plugins).find((k) => k === normalized || k === shortName) ?? normalized;
|
|
188
|
+
if (!space.plugins[existingKey])
|
|
189
|
+
space.plugins[existingKey] = {};
|
|
190
|
+
space.plugins[existingKey][field] = value;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
space[field] = value;
|
|
191
194
|
}
|
|
192
|
-
space[field] = value;
|
|
193
195
|
writeFileSync(sourcePath, `${JSON5.stringify(config, null, 2)}\n`);
|
|
194
196
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Command } from 'commander';
|
|
|
4
4
|
import { diagram } from './commands/diagram';
|
|
5
5
|
import { docs } from './commands/docs';
|
|
6
6
|
import { dump } from './commands/dump';
|
|
7
|
+
import { miroSyncCommand } from './commands/miro-sync';
|
|
7
8
|
import { listPlugins } from './commands/plugins';
|
|
8
9
|
import { render, renderList } from './commands/render';
|
|
9
10
|
import { listSchemas, showSchema } from './commands/schemas';
|
|
@@ -11,30 +12,21 @@ import { show } from './commands/show';
|
|
|
11
12
|
import { listSpaces } from './commands/spaces';
|
|
12
13
|
import { templateSync } from './commands/template-sync';
|
|
13
14
|
import { validate, watchValidate } from './commands/validate';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
15
|
+
import { validateFileCommand } from './commands/validate-file';
|
|
16
|
+
import { loadConfig, setConfigPath } from './config';
|
|
16
17
|
import { CLI_NAME } from './constants';
|
|
17
|
-
import {
|
|
18
|
-
import { loadSchema } from './schema/schema';
|
|
18
|
+
import { createSpaceContext, SpaceNotFoundError } from './space-context';
|
|
19
19
|
export function buildSpaceContext(spaceName) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
try {
|
|
21
|
+
return createSpaceContext(spaceName, loadConfig());
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err instanceof SpaceNotFoundError) {
|
|
25
|
+
console.error(`Error: ${err.message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
throw err;
|
|
25
29
|
}
|
|
26
|
-
const resolvedSchemaPath = resolveSchema(config, space);
|
|
27
|
-
const { schema, schemaRefRegistry, schemaValidator } = loadSchema(resolvedSchemaPath);
|
|
28
|
-
const configDir = getSpaceConfigDir(space.name);
|
|
29
|
-
return {
|
|
30
|
-
space,
|
|
31
|
-
config,
|
|
32
|
-
resolvedSchemaPath,
|
|
33
|
-
schema,
|
|
34
|
-
schemaRefRegistry,
|
|
35
|
-
schemaValidator,
|
|
36
|
-
configDir,
|
|
37
|
-
};
|
|
38
30
|
}
|
|
39
31
|
const require = createRequire(import.meta.url);
|
|
40
32
|
const packageJson = require('../package.json');
|
|
@@ -53,7 +45,7 @@ program
|
|
|
53
45
|
.argument('<path>', 'Path to the file to validate')
|
|
54
46
|
.option('--json', 'Output results as JSON (machine-readable, for hooks)')
|
|
55
47
|
.action(async (filePath, options) => {
|
|
56
|
-
const exitCode = await
|
|
48
|
+
const exitCode = await validateFileCommand(filePath, { json: options.json });
|
|
57
49
|
process.exit(exitCode);
|
|
58
50
|
});
|
|
59
51
|
program
|
|
@@ -97,7 +89,7 @@ program
|
|
|
97
89
|
.option('--new-frame <title>', 'Create a new frame on the board and sync into it')
|
|
98
90
|
.option('--dry-run', 'Show what would change without touching Miro')
|
|
99
91
|
.option('-v, --verbose', 'Detailed output')
|
|
100
|
-
.action((spaceName, options) =>
|
|
92
|
+
.action((spaceName, options) => miroSyncCommand(buildSpaceContext(spaceName), options));
|
|
101
93
|
program
|
|
102
94
|
.command('template-sync')
|
|
103
95
|
.description('Sync schema examples to templates')
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Config } from './config';
|
|
2
|
+
import { type SpaceGraph } from './space-graph';
|
|
3
|
+
import type { ReadSpaceResult, SpaceContext } from './types';
|
|
4
|
+
export interface AssembleSpaceGraphOptions {
|
|
5
|
+
/** Named view (resolved via space.views) or a raw filter DSL expression. */
|
|
6
|
+
filter?: string;
|
|
7
|
+
/** Pre-loaded read result to assemble from, avoiding a re-read of the space. */
|
|
8
|
+
readResult?: ReadSpaceResult;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Assemble a SpaceGraph from a space context: read the space, drop nodes that
|
|
12
|
+
* fail schema validation, build the hierarchy graph, and apply an optional
|
|
13
|
+
* view/filter expression.
|
|
14
|
+
*
|
|
15
|
+
* Nodes that fail schema validation are silently excluded so the graph is
|
|
16
|
+
* well-formed enough to render and traverse. This is NOT a validation pass —
|
|
17
|
+
* schema-valid nodes may still have broken references, rule violations, or
|
|
18
|
+
* duplicates. Use validateSpace for a full validation report.
|
|
19
|
+
*
|
|
20
|
+
* Shared by the render pipeline and the public loadSpaceGraph API so their
|
|
21
|
+
* graph-assembly behaviour cannot drift.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assembleSpaceGraph(context: SpaceContext, options?: AssembleSpaceGraphOptions): Promise<SpaceGraph>;
|
|
24
|
+
export interface LoadSpaceGraphOptions extends AssembleSpaceGraphOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Directory to anchor relative plugin paths against. Required when config is
|
|
27
|
+
* hand-assembled (has no source file); see {@link createSpaceContext}.
|
|
28
|
+
*/
|
|
29
|
+
configDir?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Load a named space and return its assembled, filtered SpaceGraph.
|
|
33
|
+
*
|
|
34
|
+
* Convenience entry point over createSpaceContext + assembleSpaceGraph: pass a
|
|
35
|
+
* space name, config, and optional filter. Pass options.readResult to reuse an
|
|
36
|
+
* already-loaded read (e.g. when also calling validateSpace) and skip re-reading.
|
|
37
|
+
* When you already hold a SpaceContext, call assembleSpaceGraph directly to avoid
|
|
38
|
+
* rebuilding (and recompiling the schema for) the context.
|
|
39
|
+
*/
|
|
40
|
+
export declare function loadSpaceGraph(spaceName: string, config: Config, options?: LoadSpaceGraphOptions): Promise<SpaceGraph>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { filterNodes } from './filter/filter-nodes';
|
|
2
|
+
import { readSpace } from './read/read-space';
|
|
3
|
+
import { createSpaceContext } from './space-context';
|
|
4
|
+
import { buildSpaceGraph } from './space-graph';
|
|
5
|
+
/**
|
|
6
|
+
* Assemble a SpaceGraph from a space context: read the space, drop nodes that
|
|
7
|
+
* fail schema validation, build the hierarchy graph, and apply an optional
|
|
8
|
+
* view/filter expression.
|
|
9
|
+
*
|
|
10
|
+
* Nodes that fail schema validation are silently excluded so the graph is
|
|
11
|
+
* well-formed enough to render and traverse. This is NOT a validation pass —
|
|
12
|
+
* schema-valid nodes may still have broken references, rule violations, or
|
|
13
|
+
* duplicates. Use validateSpace for a full validation report.
|
|
14
|
+
*
|
|
15
|
+
* Shared by the render pipeline and the public loadSpaceGraph API so their
|
|
16
|
+
* graph-assembly behaviour cannot drift.
|
|
17
|
+
*/
|
|
18
|
+
export async function assembleSpaceGraph(context, options = {}) {
|
|
19
|
+
const { nodes: allNodes } = options.readResult ?? (await readSpace(context));
|
|
20
|
+
const { schemaValidator } = context;
|
|
21
|
+
const validNodes = allNodes.filter((node) => schemaValidator(node.schemaData));
|
|
22
|
+
const levels = context.schema.metadata.hierarchy?.levels ?? [];
|
|
23
|
+
let graph = buildSpaceGraph(validNodes, levels);
|
|
24
|
+
if (options.filter) {
|
|
25
|
+
const expression = context.space.views?.[options.filter]?.expression ?? options.filter;
|
|
26
|
+
graph = await filterNodes(expression, graph);
|
|
27
|
+
}
|
|
28
|
+
return graph;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Load a named space and return its assembled, filtered SpaceGraph.
|
|
32
|
+
*
|
|
33
|
+
* Convenience entry point over createSpaceContext + assembleSpaceGraph: pass a
|
|
34
|
+
* space name, config, and optional filter. Pass options.readResult to reuse an
|
|
35
|
+
* already-loaded read (e.g. when also calling validateSpace) and skip re-reading.
|
|
36
|
+
* When you already hold a SpaceContext, call assembleSpaceGraph directly to avoid
|
|
37
|
+
* rebuilding (and recompiling the schema for) the context.
|
|
38
|
+
*/
|
|
39
|
+
export async function loadSpaceGraph(spaceName, config, options = {}) {
|
|
40
|
+
const context = createSpaceContext(spaceName, config, { configDir: options.configDir });
|
|
41
|
+
return assembleSpaceGraph(context, options);
|
|
42
|
+
}
|
package/dist/plugins/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { markdownPlugin } from './markdown';
|
|
2
|
+
import { mermaidPlugin } from './mermaid';
|
|
3
|
+
import { miroPlugin } from './miro';
|
|
2
4
|
/** All built-in plugins, in default load order. */
|
|
3
|
-
export const builtinPlugins = [markdownPlugin];
|
|
5
|
+
export const builtinPlugins = [markdownPlugin, mermaidPlugin, miroPlugin];
|
package/dist/plugins/loader.js
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
|
3
3
|
import Ajv, {} from 'ajv';
|
|
4
4
|
import { getConfigSourceFiles } from '../config';
|
|
5
5
|
import { builtinPlugins } from '.';
|
|
6
|
-
import { CONFIG_PLUGINS_DIR, normalizePluginName, PLUGIN_PREFIX } from './util';
|
|
6
|
+
import { CONFIG_PLUGINS_DIR, normalizePluginName, PLUGIN_PREFIX, shortenPluginName, } from './util';
|
|
7
7
|
/**
|
|
8
8
|
* Walk a plugin's configSchema and resolve any string fields annotated with
|
|
9
9
|
* format:'path' relative to configDir.
|
|
@@ -92,7 +92,7 @@ export async function loadPlugins(pluginMap, configDir) {
|
|
|
92
92
|
}
|
|
93
93
|
// Built-in plugins: always loaded, config taken from map if declared (with or without prefix)
|
|
94
94
|
for (const builtin of builtinPlugins) {
|
|
95
|
-
const rawConfig = pluginMap[builtin.name] ?? pluginMap[builtin.name
|
|
95
|
+
const rawConfig = pluginMap[builtin.name] ?? pluginMap[shortenPluginName(builtin.name)] ?? {};
|
|
96
96
|
const pluginConfig = resolveConfigPaths(builtin.configSchema, rawConfig, configDir);
|
|
97
97
|
const validate = ajv.compile(builtin.configSchema);
|
|
98
98
|
if (!validate(pluginConfig)) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Node } from 'mdast';
|
|
2
|
+
import type { ContentLink, SchemaMetadata } from '../../types';
|
|
3
|
+
/**
|
|
4
|
+
* Recursively walk an mdast subtree and collect all links.
|
|
5
|
+
* Handles standard markdown link/image nodes and scans text nodes for wikilinks.
|
|
6
|
+
*/
|
|
7
|
+
export declare function extractLinksFromAstNode(node: Node): ContentLink[];
|
|
8
|
+
/**
|
|
9
|
+
* Extract all links from a raw markdown body string.
|
|
10
|
+
* Parses the full document and collects links from every node in the tree.
|
|
11
|
+
*/
|
|
12
|
+
export declare function extractLinksFromBody(body: string): ContentLink[];
|
|
13
|
+
/**
|
|
14
|
+
* Build the set of field names that serve as graph edges (hierarchy + relationship fields).
|
|
15
|
+
* Used to exclude edge fields from frontmatter link extraction.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getEdgeFieldNames(metadata: SchemaMetadata): Set<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Extract links from frontmatter data fields, excluding known graph edge fields.
|
|
20
|
+
* Parses each string value as markdown (via mdast) to keep link extraction consistent
|
|
21
|
+
* with body content parsing — handles wikilinks, markdown links, images, and bare URLs.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractLinksFromFrontmatter(data: Record<string, unknown>, edgeFields: Set<string>): ContentLink[];
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { toString as mdastToString } from 'mdast-util-to-string';
|
|
2
|
+
import remarkGfm from 'remark-gfm';
|
|
3
|
+
import remarkParse from 'remark-parse';
|
|
4
|
+
import { unified } from 'unified';
|
|
5
|
+
/**
|
|
6
|
+
* Parse an Obsidian wikilink inner string into its components.
|
|
7
|
+
* Handles: [[target]], [[target#anchor]], [[target|alias]], [[target#anchor|alias]]
|
|
8
|
+
*/
|
|
9
|
+
function parseWikilinkInner(inner) {
|
|
10
|
+
// Alias: [[target|alias]] or [[target#anchor|alias]]
|
|
11
|
+
const pipeIdx = inner.indexOf('|');
|
|
12
|
+
let core = inner;
|
|
13
|
+
let displayText;
|
|
14
|
+
if (pipeIdx >= 0) {
|
|
15
|
+
core = inner.slice(0, pipeIdx);
|
|
16
|
+
const alias = inner.slice(pipeIdx + 1).trim();
|
|
17
|
+
if (alias)
|
|
18
|
+
displayText = alias;
|
|
19
|
+
}
|
|
20
|
+
// Anchor: [[target#anchor]] or [[target#^block]]
|
|
21
|
+
const hashIdx = core.indexOf('#');
|
|
22
|
+
if (hashIdx >= 0) {
|
|
23
|
+
const target = core.slice(0, hashIdx).trim();
|
|
24
|
+
const anchor = core.slice(hashIdx + 1).trim() || undefined;
|
|
25
|
+
return { target, anchor, displayText };
|
|
26
|
+
}
|
|
27
|
+
return { target: core.trim(), displayText };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Extract wikilinks (and Obsidian embed wikilinks) from a plain text string.
|
|
31
|
+
* Matches [[target]], ![[target]], [[target#anchor]], [[target|alias]], etc.
|
|
32
|
+
*/
|
|
33
|
+
function extractWikilinksFromText(text) {
|
|
34
|
+
const links = [];
|
|
35
|
+
for (const match of text.matchAll(/(!?)\[\[([^\]]+)\]\]/g)) {
|
|
36
|
+
const isEmbed = match[1] === '!';
|
|
37
|
+
const inner = match[2];
|
|
38
|
+
const { target, anchor, displayText } = parseWikilinkInner(inner);
|
|
39
|
+
if (!target)
|
|
40
|
+
continue;
|
|
41
|
+
links.push({
|
|
42
|
+
text: displayText ?? target,
|
|
43
|
+
target,
|
|
44
|
+
action: isEmbed ? 'embed' : 'link',
|
|
45
|
+
...(anchor !== undefined ? { anchor } : {}),
|
|
46
|
+
linkSyntax: 'wikilink',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return links;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Recursively walk an mdast subtree and collect all links.
|
|
53
|
+
* Handles standard markdown link/image nodes and scans text nodes for wikilinks.
|
|
54
|
+
*/
|
|
55
|
+
export function extractLinksFromAstNode(node) {
|
|
56
|
+
const links = [];
|
|
57
|
+
function walk(n) {
|
|
58
|
+
switch (n.type) {
|
|
59
|
+
case 'link': {
|
|
60
|
+
const linkNode = n;
|
|
61
|
+
links.push({
|
|
62
|
+
text: mdastToString(linkNode),
|
|
63
|
+
target: linkNode.url,
|
|
64
|
+
action: 'link',
|
|
65
|
+
linkSyntax: 'markdown',
|
|
66
|
+
});
|
|
67
|
+
// Don't recurse into children — they are display text, not link targets
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case 'image': {
|
|
71
|
+
const imgNode = n;
|
|
72
|
+
links.push({
|
|
73
|
+
text: imgNode.alt ?? '',
|
|
74
|
+
target: imgNode.url,
|
|
75
|
+
action: 'embed',
|
|
76
|
+
linkSyntax: 'markdown',
|
|
77
|
+
});
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case 'text': {
|
|
81
|
+
// Remark does not parse wikilinks natively; scan text nodes for [[...]] patterns
|
|
82
|
+
links.push(...extractWikilinksFromText(n.value));
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
default: {
|
|
86
|
+
if ('children' in n) {
|
|
87
|
+
for (const child of n.children) {
|
|
88
|
+
walk(child);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
walk(node);
|
|
96
|
+
return links;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Extract all links from a raw markdown body string.
|
|
100
|
+
* Parses the full document and collects links from every node in the tree.
|
|
101
|
+
*/
|
|
102
|
+
export function extractLinksFromBody(body) {
|
|
103
|
+
const tree = unified().use(remarkParse).use(remarkGfm).parse(body);
|
|
104
|
+
return extractLinksFromAstNode(tree);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Build the set of field names that serve as graph edges (hierarchy + relationship fields).
|
|
108
|
+
* Used to exclude edge fields from frontmatter link extraction.
|
|
109
|
+
*/
|
|
110
|
+
export function getEdgeFieldNames(metadata) {
|
|
111
|
+
const fields = new Set();
|
|
112
|
+
for (const level of metadata.hierarchy?.levels ?? []) {
|
|
113
|
+
fields.add(level.field);
|
|
114
|
+
if (level.selfRefField)
|
|
115
|
+
fields.add(level.selfRefField);
|
|
116
|
+
}
|
|
117
|
+
for (const rel of metadata.relationships ?? []) {
|
|
118
|
+
fields.add(rel.field);
|
|
119
|
+
}
|
|
120
|
+
return fields;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Extract links from frontmatter data fields, excluding known graph edge fields.
|
|
124
|
+
* Parses each string value as markdown (via mdast) to keep link extraction consistent
|
|
125
|
+
* with body content parsing — handles wikilinks, markdown links, images, and bare URLs.
|
|
126
|
+
*/
|
|
127
|
+
export function extractLinksFromFrontmatter(data, edgeFields) {
|
|
128
|
+
const links = [];
|
|
129
|
+
for (const [key, value] of Object.entries(data)) {
|
|
130
|
+
if (edgeFields.has(key))
|
|
131
|
+
continue;
|
|
132
|
+
if (typeof value === 'string') {
|
|
133
|
+
links.push(...extractLinksFromBody(value));
|
|
134
|
+
}
|
|
135
|
+
else if (Array.isArray(value)) {
|
|
136
|
+
for (const item of value) {
|
|
137
|
+
if (typeof item === 'string') {
|
|
138
|
+
links.push(...extractLinksFromBody(item));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return links;
|
|
144
|
+
}
|
|
@@ -1,48 +1,58 @@
|
|
|
1
|
+
import type { FromSchema } from 'json-schema-to-ts';
|
|
1
2
|
import type { StructuredContextPlugin } from '../util';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
declare const TYPE_INFERENCE_CONFIG_SCHEMA: {
|
|
4
|
+
readonly type: "object";
|
|
5
|
+
readonly properties: {
|
|
6
|
+
readonly mode: {
|
|
7
|
+
readonly type: "string";
|
|
8
|
+
readonly enum: readonly ["folder-name", "off"];
|
|
9
|
+
};
|
|
10
|
+
readonly folderMap: {
|
|
11
|
+
readonly type: "object";
|
|
12
|
+
readonly additionalProperties: {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
readonly additionalProperties: false;
|
|
11
18
|
};
|
|
12
19
|
export declare const MARKDOWN_CONFIG_SCHEMA: {
|
|
13
|
-
type:
|
|
14
|
-
properties: {
|
|
15
|
-
templateDir: {
|
|
16
|
-
type: string;
|
|
17
|
-
format:
|
|
20
|
+
readonly type: "object";
|
|
21
|
+
readonly properties: {
|
|
22
|
+
readonly templateDir: {
|
|
23
|
+
readonly type: "string";
|
|
24
|
+
readonly format: "path";
|
|
18
25
|
};
|
|
19
|
-
fieldMap: {
|
|
20
|
-
type:
|
|
21
|
-
additionalProperties: {
|
|
22
|
-
type: string;
|
|
26
|
+
readonly fieldMap: {
|
|
27
|
+
readonly type: "object";
|
|
28
|
+
readonly additionalProperties: {
|
|
29
|
+
readonly type: "string";
|
|
23
30
|
};
|
|
24
31
|
};
|
|
25
|
-
templatePrefix: {
|
|
26
|
-
type: string;
|
|
32
|
+
readonly templatePrefix: {
|
|
33
|
+
readonly type: "string";
|
|
27
34
|
};
|
|
28
|
-
typeInference: {
|
|
29
|
-
type:
|
|
30
|
-
properties: {
|
|
31
|
-
mode: {
|
|
32
|
-
type: string;
|
|
33
|
-
enum:
|
|
35
|
+
readonly typeInference: {
|
|
36
|
+
readonly type: "object";
|
|
37
|
+
readonly properties: {
|
|
38
|
+
readonly mode: {
|
|
39
|
+
readonly type: "string";
|
|
40
|
+
readonly enum: readonly ["folder-name", "off"];
|
|
34
41
|
};
|
|
35
|
-
folderMap: {
|
|
36
|
-
type:
|
|
37
|
-
additionalProperties: {
|
|
38
|
-
type: string;
|
|
42
|
+
readonly folderMap: {
|
|
43
|
+
readonly type: "object";
|
|
44
|
+
readonly additionalProperties: {
|
|
45
|
+
readonly type: "string";
|
|
39
46
|
};
|
|
40
47
|
};
|
|
41
48
|
};
|
|
42
|
-
additionalProperties:
|
|
49
|
+
readonly additionalProperties: false;
|
|
43
50
|
};
|
|
44
51
|
};
|
|
45
|
-
additionalProperties:
|
|
52
|
+
readonly additionalProperties: false;
|
|
46
53
|
};
|
|
54
|
+
export type MarkdownPluginConfig = FromSchema<typeof MARKDOWN_CONFIG_SCHEMA>;
|
|
55
|
+
export type TypeInferenceConfig = FromSchema<typeof TYPE_INFERENCE_CONFIG_SCHEMA>;
|
|
47
56
|
export declare function getMarkdownConfig(plugins?: Record<string, Record<string, unknown>>): MarkdownPluginConfig;
|
|
48
57
|
export declare const markdownPlugin: StructuredContextPlugin;
|
|
58
|
+
export {};
|
|
@@ -2,22 +2,22 @@ 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 { renderMermaid } from './render-mermaid';
|
|
6
5
|
import { templateSync } from './template-sync';
|
|
6
|
+
const TYPE_INFERENCE_CONFIG_SCHEMA = {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
mode: { type: 'string', enum: ['folder-name', 'off'] },
|
|
10
|
+
folderMap: { type: 'object', additionalProperties: { type: 'string' } },
|
|
11
|
+
},
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
};
|
|
7
14
|
export const MARKDOWN_CONFIG_SCHEMA = {
|
|
8
15
|
type: 'object',
|
|
9
16
|
properties: {
|
|
10
17
|
templateDir: { type: 'string', format: 'path' }, // format is hint to config loader to resolve relative directories
|
|
11
18
|
fieldMap: { type: 'object', additionalProperties: { type: 'string' } },
|
|
12
19
|
templatePrefix: { type: 'string' },
|
|
13
|
-
typeInference:
|
|
14
|
-
type: 'object',
|
|
15
|
-
properties: {
|
|
16
|
-
mode: { type: 'string', enum: ['folder-name', 'off'] },
|
|
17
|
-
folderMap: { type: 'object', additionalProperties: { type: 'string' } },
|
|
18
|
-
},
|
|
19
|
-
additionalProperties: false,
|
|
20
|
-
},
|
|
20
|
+
typeInference: TYPE_INFERENCE_CONFIG_SCHEMA,
|
|
21
21
|
},
|
|
22
22
|
additionalProperties: false,
|
|
23
23
|
};
|
|
@@ -36,15 +36,10 @@ export const markdownPlugin = {
|
|
|
36
36
|
parse,
|
|
37
37
|
templateSync,
|
|
38
38
|
render: {
|
|
39
|
-
formats: [
|
|
40
|
-
{ name: 'bullets', description: 'Indented bullet list' },
|
|
41
|
-
{ name: 'mermaid', description: 'Mermaid graph TD diagram' },
|
|
42
|
-
],
|
|
39
|
+
formats: [{ name: 'bullets', description: 'Indented bullet list' }],
|
|
43
40
|
render(_context, graph, { format }) {
|
|
44
41
|
if (format === 'bullets')
|
|
45
42
|
return renderBullets(graph);
|
|
46
|
-
if (format === 'mermaid')
|
|
47
|
-
return renderMermaid(graph);
|
|
48
43
|
throw new Error(`Unknown markdown render format: "${format}"`);
|
|
49
44
|
},
|
|
50
45
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BaseNode, Relationship, SchemaMetadata } from '../../
|
|
1
|
+
import type { BaseNode, Relationship, SchemaMetadata } from '../../api';
|
|
2
2
|
/** Type values that identify a space_on_a_page container (not themselves space nodes). */
|
|
3
3
|
export declare const ON_A_PAGE_TYPES: string[];
|
|
4
4
|
type StackEntry = {
|