@stxt-lang/core 0.5.3 → 0.6.1
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 +115 -1
- package/out/all.d.ts +7 -0
- package/out/all.js +10 -1
- package/out/discovery/DiscoveryEnvironment.d.ts +35 -0
- package/out/discovery/DiscoveryEnvironment.js +3 -0
- package/out/discovery/DiscoveryError.d.ts +30 -0
- package/out/discovery/DiscoveryError.js +36 -0
- package/out/discovery/DiscoveryFileSystem.d.ts +57 -0
- package/out/discovery/DiscoveryFileSystem.js +3 -0
- package/out/discovery/DiscoveryResolver.d.ts +74 -0
- package/out/discovery/DiscoveryResolver.js +232 -0
- package/out/discovery/DiscoveryResult.d.ts +94 -0
- package/out/discovery/DiscoveryResult.js +111 -0
- package/out/runtime/TreeJson.d.ts +40 -0
- package/out/runtime/TreeJson.js +46 -0
- package/out/schema/SchemaProviderMemory.js +7 -2
- package/out/template/TemplateSchemaProviderMemory.d.ts +2 -1
- package/out/template/TemplateSchemaProviderMemory.js +8 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -115,6 +115,113 @@ for (const error of result.getErrors()) {
|
|
|
115
115
|
|
|
116
116
|
Available value types: `INLINE`, `BLOCK`, `TEXT`, `BOOLEAN`, `INTEGER`, `NATURAL`, `NUMBER`, `DATE`, `TIMESTAMP`, `EMAIL`, `URL`, `HEXADECIMAL`, `BASE64`, `GROUP`, `ENUM`.
|
|
117
117
|
|
|
118
|
+
## Finding the schemas: discovery
|
|
119
|
+
|
|
120
|
+
`UnifiedSchemaProvider` expects you to hand it the schema text. **Discovery** answers the previous question: *given this document, which schema definitions apply to it?* `DiscoveryResolver` is the reference implementation of the STXT discovery specification, so a command line, an editor and a build step all agree on the answer by construction.
|
|
121
|
+
|
|
122
|
+
Definitions live in `.stxt/` directories. For a given document the resolution chain is, highest precedence first:
|
|
123
|
+
|
|
124
|
+
1. every ancestor `.stxt/` directory, nearest first — the ascent does **not** stop at the first one, so in a monorepo both the subproject's and the repo root's participate;
|
|
125
|
+
2. the user level, `$HOME/.stxt` (`%USERPROFILE%\.stxt` on Windows);
|
|
126
|
+
3. the system level, `/etc/stxt` (`%ProgramData%\stxt` on Windows).
|
|
127
|
+
|
|
128
|
+
Precedence is **per namespace**: the nearest level that defines a namespace wins, and the rest of the chain still contributes the namespaces that level does not define. Defining one namespace twice at the same level is a resolution error, and leaves that namespace without an active definition. When `STXT_PATH` is defined it replaces the whole chain — useful in CI and tests.
|
|
129
|
+
|
|
130
|
+
The resolver never touches the file system or the environment itself: you inject a `DiscoveryFileSystem` and a `DiscoveryEnvironment`. That is what lets the same logic run over Node's `fs`, over an editor's virtual file system (`vscode.workspace.fs`) or over an in-memory tree in a test. Here are the Node adapters:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import * as fs from 'fs/promises';
|
|
134
|
+
import * as os from 'os';
|
|
135
|
+
import * as path from 'path';
|
|
136
|
+
import {
|
|
137
|
+
DiscoveryEntry,
|
|
138
|
+
DiscoveryEnvironment,
|
|
139
|
+
DiscoveryFileSystem,
|
|
140
|
+
DiscoveryResolver,
|
|
141
|
+
} from '@stxt-lang/core';
|
|
142
|
+
|
|
143
|
+
class NodeFileSystem implements DiscoveryFileSystem {
|
|
144
|
+
async isDirectory(p: string): Promise<boolean> {
|
|
145
|
+
try {
|
|
146
|
+
return (await fs.stat(p)).isDirectory();
|
|
147
|
+
} catch {
|
|
148
|
+
return false; // not existing is the normal case, not an error
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async listDirectory(p: string): Promise<DiscoveryEntry[]> {
|
|
152
|
+
const entries = await fs.readdir(p, { withFileTypes: true });
|
|
153
|
+
return entries.map(entry => ({
|
|
154
|
+
path: path.join(p, entry.name),
|
|
155
|
+
name: entry.name,
|
|
156
|
+
isDirectory: entry.isDirectory(),
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
readFile(p: string): Promise<string> {
|
|
160
|
+
return fs.readFile(p, 'utf-8');
|
|
161
|
+
}
|
|
162
|
+
parentOf(p: string): string | null {
|
|
163
|
+
const parent = path.dirname(p);
|
|
164
|
+
return parent === p ? null : parent; // null at the file-system root
|
|
165
|
+
}
|
|
166
|
+
join(p: string, name: string): string {
|
|
167
|
+
return path.join(p, name);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
class NodeEnvironment implements DiscoveryEnvironment {
|
|
172
|
+
getStxtPath(): string[] | null {
|
|
173
|
+
const value = process.env.STXT_PATH;
|
|
174
|
+
// null (not defined) and [] (defined but empty) mean different things
|
|
175
|
+
return value === undefined ? null : value.split(path.delimiter).filter(e => e !== '');
|
|
176
|
+
}
|
|
177
|
+
getUserLevelDir(): string | null {
|
|
178
|
+
return path.join(os.homedir(), '.stxt');
|
|
179
|
+
}
|
|
180
|
+
getSystemLevelDir(): string | null {
|
|
181
|
+
return '/etc/stxt';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
With those in place, resolving a document and validating it is two steps — and note that `DiscoveryResult` implements `SchemaProvider`, so it goes straight into the validator:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { Parser, SchemaValidator, ConditionalValidator } from '@stxt-lang/core';
|
|
190
|
+
|
|
191
|
+
const resolver = new DiscoveryResolver(new NodeFileSystem(), new NodeEnvironment());
|
|
192
|
+
|
|
193
|
+
// The chain is per document: pass the directory the document lives in
|
|
194
|
+
// (null for stdin or an unsaved buffer, which starts the chain at the user level).
|
|
195
|
+
const result = await resolver.resolve('/repo/site/posts');
|
|
196
|
+
|
|
197
|
+
console.log(result.getChain());
|
|
198
|
+
// [ '/repo/site/.stxt', '/repo/.stxt' ] ← both ancestors, nearest first
|
|
199
|
+
|
|
200
|
+
// Resolution errors are collected, never thrown: report them and carry on
|
|
201
|
+
for (const error of result.getErrors()) {
|
|
202
|
+
console.error(`[${error.code}] ${error.message}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const parser = new Parser();
|
|
206
|
+
parser.registerValidator(new ConditionalValidator(new SchemaValidator(result)));
|
|
207
|
+
|
|
208
|
+
const parsed = parser.parseResult(documentText);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`DiscoveryResult` also tells you *where* a schema came from, which is what an editor needs for "go to definition" or a diagnostic that explains itself:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const definition = result.getDefinition('blog.post');
|
|
215
|
+
|
|
216
|
+
console.log(definition?.file); // '/repo/site/.stxt/blog.stxt'
|
|
217
|
+
console.log(definition?.levelDir); // '/repo/site/.stxt' ← the level that won
|
|
218
|
+
|
|
219
|
+
result.getActiveDefinitions(); // one entry per namespace, precedence applied
|
|
220
|
+
result.getAllSchemas(); // just the schemas of the above
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Levels are cached by directory, so resolving many documents that share ancestors reads each `.stxt/` once. Call `resolver.clearCache()` when the definition files may have changed — from a file watcher, for instance.
|
|
224
|
+
|
|
118
225
|
## Observing the parse
|
|
119
226
|
|
|
120
227
|
`Observer` receives streaming callbacks while the document is parsed — useful for syntax highlighting, indexes or any per-line bookkeeping.
|
|
@@ -150,7 +257,14 @@ const doc = NodeWriter.toSTXTDocs(result.getNodes(), IndentStyle.SPACES_4);
|
|
|
150
257
|
|
|
151
258
|
## API surface
|
|
152
259
|
|
|
153
|
-
Everything importable from the package:
|
|
260
|
+
Everything importable from the package:
|
|
261
|
+
|
|
262
|
+
- **Parsing** — `Node`, `Parser`, `ParseResult`, `Line`, `Constants`, `parseLine`, `StringUtils`
|
|
263
|
+
- **Exceptions** — `ParseException`, `ValidationException`
|
|
264
|
+
- **Extension points** — `Observer`
|
|
265
|
+
- **Schemas** — `Schema`, `SchemaValidator`, `SchemaProvider`, `NodeDefinition`, `ChildDefinition`, `transformNodeToSchema`, `transformTemplateNodeToSchema`
|
|
266
|
+
- **Runtime** — `UnifiedSchemaProvider`, `ConditionalValidator`, `NodeWriter`, `IndentStyle`
|
|
267
|
+
- **Discovery** — `DiscoveryResolver`, `DiscoveryOptions`, `DiscoveryResult`, `DiscoveryDefinition`, `DiscoveryLevel`, `DiscoveryError`, `DiscoveryFileSystem`, `DiscoveryEntry`, `DiscoveryEnvironment`
|
|
154
268
|
|
|
155
269
|
## License
|
|
156
270
|
|
package/out/all.d.ts
CHANGED
|
@@ -17,4 +17,11 @@ export { transformNodeToSchema } from "./schema/SchemaParser";
|
|
|
17
17
|
export { UnifiedSchemaProvider } from "./runtime/UnifiedSchemaProvider";
|
|
18
18
|
export { ConditionalValidator } from "./runtime/ConditionalValidator";
|
|
19
19
|
export { NodeWriter, IndentStyle } from "./runtime/NodeWriter";
|
|
20
|
+
export { toCanonicalTree, toCanonicalJson } from "./runtime/TreeJson";
|
|
21
|
+
export type { CanonicalDocument, CanonicalNode, CanonicalInlineNode, CanonicalBlockNode } from "./runtime/TreeJson";
|
|
20
22
|
export { transformTemplateNodeToSchema } from "./template/TemplateParser";
|
|
23
|
+
export { DiscoveryResolver, DiscoveryOptions } from "./discovery/DiscoveryResolver";
|
|
24
|
+
export { DiscoveryResult, DiscoveryDefinition, DiscoveryLevel } from "./discovery/DiscoveryResult";
|
|
25
|
+
export { DiscoveryError } from "./discovery/DiscoveryError";
|
|
26
|
+
export { DiscoveryFileSystem, DiscoveryEntry } from "./discovery/DiscoveryFileSystem";
|
|
27
|
+
export { DiscoveryEnvironment } from "./discovery/DiscoveryEnvironment";
|
package/out/all.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Anything that should be consumable by third parties (e.g. the VSCode extension)
|
|
4
4
|
// has to be re-exported from here.
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.transformTemplateNodeToSchema = exports.IndentStyle = exports.NodeWriter = exports.ConditionalValidator = exports.UnifiedSchemaProvider = exports.transformNodeToSchema = exports.ChildDefinition = exports.NodeDefinition = exports.SchemaValidator = exports.Schema = exports.ValidationException = exports.ParseException = exports.StringUtils = exports.parseLine = exports.Constants = exports.Line = exports.ParseResult = exports.Parser = exports.Node = void 0;
|
|
6
|
+
exports.DiscoveryError = exports.DiscoveryResult = exports.DiscoveryResolver = exports.transformTemplateNodeToSchema = exports.toCanonicalJson = exports.toCanonicalTree = exports.IndentStyle = exports.NodeWriter = exports.ConditionalValidator = exports.UnifiedSchemaProvider = exports.transformNodeToSchema = exports.ChildDefinition = exports.NodeDefinition = exports.SchemaValidator = exports.Schema = exports.ValidationException = exports.ParseException = exports.StringUtils = exports.parseLine = exports.Constants = exports.Line = exports.ParseResult = exports.Parser = exports.Node = void 0;
|
|
7
7
|
var Node_1 = require("./core/Node");
|
|
8
8
|
Object.defineProperty(exports, "Node", { enumerable: true, get: function () { return Node_1.Node; } });
|
|
9
9
|
var Parser_1 = require("./core/Parser");
|
|
@@ -39,6 +39,15 @@ Object.defineProperty(exports, "ConditionalValidator", { enumerable: true, get:
|
|
|
39
39
|
var NodeWriter_1 = require("./runtime/NodeWriter");
|
|
40
40
|
Object.defineProperty(exports, "NodeWriter", { enumerable: true, get: function () { return NodeWriter_1.NodeWriter; } });
|
|
41
41
|
Object.defineProperty(exports, "IndentStyle", { enumerable: true, get: function () { return NodeWriter_1.IndentStyle; } });
|
|
42
|
+
var TreeJson_1 = require("./runtime/TreeJson");
|
|
43
|
+
Object.defineProperty(exports, "toCanonicalTree", { enumerable: true, get: function () { return TreeJson_1.toCanonicalTree; } });
|
|
44
|
+
Object.defineProperty(exports, "toCanonicalJson", { enumerable: true, get: function () { return TreeJson_1.toCanonicalJson; } });
|
|
42
45
|
var TemplateParser_1 = require("./template/TemplateParser");
|
|
43
46
|
Object.defineProperty(exports, "transformTemplateNodeToSchema", { enumerable: true, get: function () { return TemplateParser_1.transformTemplateNodeToSchema; } });
|
|
47
|
+
var DiscoveryResolver_1 = require("./discovery/DiscoveryResolver");
|
|
48
|
+
Object.defineProperty(exports, "DiscoveryResolver", { enumerable: true, get: function () { return DiscoveryResolver_1.DiscoveryResolver; } });
|
|
49
|
+
var DiscoveryResult_1 = require("./discovery/DiscoveryResult");
|
|
50
|
+
Object.defineProperty(exports, "DiscoveryResult", { enumerable: true, get: function () { return DiscoveryResult_1.DiscoveryResult; } });
|
|
51
|
+
var DiscoveryError_1 = require("./discovery/DiscoveryError");
|
|
52
|
+
Object.defineProperty(exports, "DiscoveryError", { enumerable: true, get: function () { return DiscoveryError_1.DiscoveryError; } });
|
|
44
53
|
//# sourceMappingURL=all.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment abstraction used by {@link DiscoveryResolver} (STXT-DISCOVERY-SPEC).
|
|
3
|
+
*
|
|
4
|
+
* It answers the three questions that depend on the platform and the process environment:
|
|
5
|
+
* the `STXT_PATH` override, the user-level directory and the system-level directory.
|
|
6
|
+
* Injecting it keeps the resolver free of `process`/`os` access, deterministic in tests
|
|
7
|
+
* and usable from any host (Node, editor extension, browser).
|
|
8
|
+
*/
|
|
9
|
+
export interface DiscoveryEnvironment {
|
|
10
|
+
/**
|
|
11
|
+
* The value of the `STXT_PATH` environment variable, already split into entries.
|
|
12
|
+
*
|
|
13
|
+
* The distinction between "not defined" and "defined but empty" is normative
|
|
14
|
+
* (STXT-DISCOVERY-SPEC section 6): when defined, `STXT_PATH` completely replaces the
|
|
15
|
+
* resolution chain, and an empty value leaves the chain empty.
|
|
16
|
+
*
|
|
17
|
+
* @returns the list of directories (highest precedence first), an empty array when the
|
|
18
|
+
* variable is defined but empty, or null when it is not defined at all.
|
|
19
|
+
*/
|
|
20
|
+
getStxtPath(): string[] | null;
|
|
21
|
+
/**
|
|
22
|
+
* The user-level resolution directory (`$HOME/.stxt` on Unix, `%USERPROFILE%\.stxt` on
|
|
23
|
+
* Windows), already resolved to a full path.
|
|
24
|
+
*
|
|
25
|
+
* @returns the user-level directory, or null when the host has no notion of a user home.
|
|
26
|
+
*/
|
|
27
|
+
getUserLevelDir(): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* The system-level resolution directory (`/etc/stxt` on Unix, `%ProgramData%\stxt` on
|
|
30
|
+
* Windows), already resolved to a full path.
|
|
31
|
+
*
|
|
32
|
+
* @returns the system-level directory, or null when the host has no system level.
|
|
33
|
+
*/
|
|
34
|
+
getSystemLevelDir(): string | null;
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A resolution error (STXT-DISCOVERY-SPEC section 8).
|
|
3
|
+
*
|
|
4
|
+
* Resolution errors are collected, not thrown: the spec mandates reporting them while
|
|
5
|
+
* allowing the tool to keep loading the remaining definitions, so a resolve pass returns
|
|
6
|
+
* every error it found instead of aborting at the first one.
|
|
7
|
+
*/
|
|
8
|
+
export declare class DiscoveryError {
|
|
9
|
+
readonly code: string;
|
|
10
|
+
readonly file: string;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
readonly namespace?: string | undefined;
|
|
13
|
+
/** Two definitions for the same target namespace at the same level (spec 8.1). */
|
|
14
|
+
static readonly DUPLICATE_NAMESPACE = "DISCOVERY_DUPLICATE_NAMESPACE";
|
|
15
|
+
/** A file under a resolution directory that does not parse as STXT (spec 8.2). */
|
|
16
|
+
static readonly NOT_PARSEABLE = "DISCOVERY_NOT_PARSEABLE";
|
|
17
|
+
/** A file whose root node belongs neither to @stxt.schema nor to @stxt.template (spec 8.3). */
|
|
18
|
+
static readonly NOT_A_DEFINITION = "DISCOVERY_NOT_A_DEFINITION";
|
|
19
|
+
/** A definition that does not validate against its meta-schema (spec 8.4). */
|
|
20
|
+
static readonly INVALID_DEFINITION = "DISCOVERY_INVALID_DEFINITION";
|
|
21
|
+
/**
|
|
22
|
+
* Creates a resolution error.
|
|
23
|
+
*
|
|
24
|
+
* @param code one of the `DISCOVERY_*` constants of this class.
|
|
25
|
+
* @param file full path of the offending file.
|
|
26
|
+
* @param message human-readable description of the error.
|
|
27
|
+
* @param namespace target namespace involved, when the error is about a namespace.
|
|
28
|
+
*/
|
|
29
|
+
constructor(code: string, file: string, message: string, namespace?: string | undefined);
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DiscoveryError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A resolution error (STXT-DISCOVERY-SPEC section 8).
|
|
6
|
+
*
|
|
7
|
+
* Resolution errors are collected, not thrown: the spec mandates reporting them while
|
|
8
|
+
* allowing the tool to keep loading the remaining definitions, so a resolve pass returns
|
|
9
|
+
* every error it found instead of aborting at the first one.
|
|
10
|
+
*/
|
|
11
|
+
class DiscoveryError {
|
|
12
|
+
/**
|
|
13
|
+
* Creates a resolution error.
|
|
14
|
+
*
|
|
15
|
+
* @param code one of the `DISCOVERY_*` constants of this class.
|
|
16
|
+
* @param file full path of the offending file.
|
|
17
|
+
* @param message human-readable description of the error.
|
|
18
|
+
* @param namespace target namespace involved, when the error is about a namespace.
|
|
19
|
+
*/
|
|
20
|
+
constructor(code, file, message, namespace) {
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.file = file;
|
|
23
|
+
this.message = message;
|
|
24
|
+
this.namespace = namespace;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.DiscoveryError = DiscoveryError;
|
|
28
|
+
/** Two definitions for the same target namespace at the same level (spec 8.1). */
|
|
29
|
+
DiscoveryError.DUPLICATE_NAMESPACE = "DISCOVERY_DUPLICATE_NAMESPACE";
|
|
30
|
+
/** A file under a resolution directory that does not parse as STXT (spec 8.2). */
|
|
31
|
+
DiscoveryError.NOT_PARSEABLE = "DISCOVERY_NOT_PARSEABLE";
|
|
32
|
+
/** A file whose root node belongs neither to @stxt.schema nor to @stxt.template (spec 8.3). */
|
|
33
|
+
DiscoveryError.NOT_A_DEFINITION = "DISCOVERY_NOT_A_DEFINITION";
|
|
34
|
+
/** A definition that does not validate against its meta-schema (spec 8.4). */
|
|
35
|
+
DiscoveryError.INVALID_DEFINITION = "DISCOVERY_INVALID_DEFINITION";
|
|
36
|
+
//# sourceMappingURL=DiscoveryError.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An entry of a directory listing, as returned by {@link DiscoveryFileSystem.listDirectory}.
|
|
3
|
+
*/
|
|
4
|
+
export interface DiscoveryEntry {
|
|
5
|
+
/** Full path of the entry, in the same form the file system uses for every other path. */
|
|
6
|
+
path: string;
|
|
7
|
+
/** Base name of the entry (last path segment). */
|
|
8
|
+
name: string;
|
|
9
|
+
/** True if the entry is a directory. */
|
|
10
|
+
isDirectory: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Minimal file-system abstraction used by {@link DiscoveryResolver} (STXT-DISCOVERY-SPEC).
|
|
14
|
+
*
|
|
15
|
+
* The resolver treats paths as opaque strings: it never parses or concatenates them itself,
|
|
16
|
+
* so an implementation may back them with plain OS paths (Node `fs`), editor URIs
|
|
17
|
+
* (`vscode.workspace.fs`) or an in-memory tree for tests. All paths returned by an
|
|
18
|
+
* implementation must be canonical enough that string equality means "same directory".
|
|
19
|
+
*/
|
|
20
|
+
export interface DiscoveryFileSystem {
|
|
21
|
+
/**
|
|
22
|
+
* Whether a path exists and is a directory.
|
|
23
|
+
*
|
|
24
|
+
* @param path path to check.
|
|
25
|
+
* @returns true if the path is an existing directory; false otherwise (including I/O errors).
|
|
26
|
+
*/
|
|
27
|
+
isDirectory(path: string): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Lists the immediate entries of a directory.
|
|
30
|
+
*
|
|
31
|
+
* @param path directory to list.
|
|
32
|
+
* @returns the entries of the directory, in any order.
|
|
33
|
+
*/
|
|
34
|
+
listDirectory(path: string): Promise<DiscoveryEntry[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Reads a file as UTF-8 text.
|
|
37
|
+
*
|
|
38
|
+
* @param path file to read.
|
|
39
|
+
* @returns the text content of the file.
|
|
40
|
+
*/
|
|
41
|
+
readFile(path: string): Promise<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Returns the parent directory of a path, or null when the path is the file-system root.
|
|
44
|
+
*
|
|
45
|
+
* @param path path whose parent is wanted.
|
|
46
|
+
* @returns the parent path, or null at the root.
|
|
47
|
+
*/
|
|
48
|
+
parentOf(path: string): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Joins a directory path and a child name.
|
|
51
|
+
*
|
|
52
|
+
* @param path base directory.
|
|
53
|
+
* @param name child segment to append.
|
|
54
|
+
* @returns the joined path.
|
|
55
|
+
*/
|
|
56
|
+
join(path: string, name: string): string;
|
|
57
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { DiscoveryEnvironment } from "./DiscoveryEnvironment";
|
|
2
|
+
import { DiscoveryFileSystem } from "./DiscoveryFileSystem";
|
|
3
|
+
import { DiscoveryResult } from "./DiscoveryResult";
|
|
4
|
+
/** Options of a {@link DiscoveryResolver}. */
|
|
5
|
+
export interface DiscoveryOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Maximum number of ancestor directories examined during the project-level ascent.
|
|
8
|
+
* A safeguard against pathological paths (circular links, virtual file systems), as
|
|
9
|
+
* allowed by STXT-DISCOVERY-SPEC section 4.1. Defaults to 32.
|
|
10
|
+
*/
|
|
11
|
+
maxAscent?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Reference implementation of STXT-DISCOVERY-SPEC: builds the resolution chain of a
|
|
15
|
+
* document (project ascent, user level, system level, or the `STXT_PATH` override), loads
|
|
16
|
+
* every definition of every level and applies the per-namespace precedence.
|
|
17
|
+
*
|
|
18
|
+
* The resolver is host-agnostic: all file-system and environment access goes through the
|
|
19
|
+
* injected {@link DiscoveryFileSystem} and {@link DiscoveryEnvironment}, so the same logic
|
|
20
|
+
* serves a command line (Node `fs`), an editor (`vscode.workspace.fs`) or a test (an
|
|
21
|
+
* in-memory tree).
|
|
22
|
+
*
|
|
23
|
+
* Loaded levels are cached by directory: resolving many documents that share levels loads
|
|
24
|
+
* each directory once, which is the sharing that STXT-DISCOVERY-SPEC section 7 allows —
|
|
25
|
+
* a level's content does not depend on which document is being resolved. Call
|
|
26
|
+
* {@link clearCache} when the underlying files may have changed.
|
|
27
|
+
*/
|
|
28
|
+
export declare class DiscoveryResolver {
|
|
29
|
+
private readonly fs;
|
|
30
|
+
private readonly env;
|
|
31
|
+
private readonly maxAscent;
|
|
32
|
+
private readonly schemaMeta;
|
|
33
|
+
private readonly templateMeta;
|
|
34
|
+
private readonly levelCache;
|
|
35
|
+
/**
|
|
36
|
+
* Creates a resolver.
|
|
37
|
+
*
|
|
38
|
+
* @param fs file-system access.
|
|
39
|
+
* @param env environment access (`STXT_PATH`, user and system directories).
|
|
40
|
+
* @param options optional settings.
|
|
41
|
+
*/
|
|
42
|
+
constructor(fs: DiscoveryFileSystem, env: DiscoveryEnvironment, options?: DiscoveryOptions);
|
|
43
|
+
/**
|
|
44
|
+
* Builds the resolution chain of a document (STXT-DISCOVERY-SPEC sections 4 and 6)
|
|
45
|
+
* without loading any definition.
|
|
46
|
+
*
|
|
47
|
+
* @param documentDir directory containing the document, or null for a document with no
|
|
48
|
+
* file-system location (standard input, an unsaved buffer), whose chain starts
|
|
49
|
+
* at the user level.
|
|
50
|
+
* @returns the existing resolution directories, highest precedence first.
|
|
51
|
+
*/
|
|
52
|
+
resolveChain(documentDir: string | null): Promise<string[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Resolves the definitions applicable to a document: builds its chain, loads every
|
|
55
|
+
* level (from the cache when already loaded) and returns the result with the
|
|
56
|
+
* per-namespace precedence applied.
|
|
57
|
+
*
|
|
58
|
+
* @param documentDir directory containing the document, or null for a document with no
|
|
59
|
+
* file-system location.
|
|
60
|
+
* @returns the resolution result, usable directly as a `SchemaProvider`.
|
|
61
|
+
*/
|
|
62
|
+
resolve(documentDir: string | null): Promise<DiscoveryResult>;
|
|
63
|
+
/**
|
|
64
|
+
* Empties the level cache, so that the next resolve re-reads every directory. Call it
|
|
65
|
+
* when the definition files may have changed (e.g. from a file watcher).
|
|
66
|
+
*/
|
|
67
|
+
clearCache(): void;
|
|
68
|
+
private existingUnique;
|
|
69
|
+
private loadLevel;
|
|
70
|
+
private collectFiles;
|
|
71
|
+
private loadFile;
|
|
72
|
+
private loadRootNode;
|
|
73
|
+
private compile;
|
|
74
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DiscoveryResolver = void 0;
|
|
4
|
+
const Parser_1 = require("../core/Parser");
|
|
5
|
+
const StringUtils_1 = require("../core/StringUtils");
|
|
6
|
+
const ParseException_1 = require("../exceptions/ParseException");
|
|
7
|
+
const SchemaProviderMeta_1 = require("../schema/SchemaProviderMeta");
|
|
8
|
+
const SchemaValidator_1 = require("../schema/SchemaValidator");
|
|
9
|
+
const SchemaParser_1 = require("../schema/SchemaParser");
|
|
10
|
+
const MetaTemplateSchemaProvider_1 = require("../template/MetaTemplateSchemaProvider");
|
|
11
|
+
const TemplateParser_1 = require("../template/TemplateParser");
|
|
12
|
+
const DiscoveryError_1 = require("./DiscoveryError");
|
|
13
|
+
const DiscoveryResult_1 = require("./DiscoveryResult");
|
|
14
|
+
/** Name of the resolution directories (STXT-DISCOVERY-SPEC section 3). */
|
|
15
|
+
const STXT_DIR = ".stxt";
|
|
16
|
+
/** File extension of STXT documents. */
|
|
17
|
+
const STXT_EXTENSION = ".stxt";
|
|
18
|
+
/** Default value of {@link DiscoveryOptions.maxAscent}. */
|
|
19
|
+
const DEFAULT_MAX_ASCENT = 32;
|
|
20
|
+
/**
|
|
21
|
+
* Reference implementation of STXT-DISCOVERY-SPEC: builds the resolution chain of a
|
|
22
|
+
* document (project ascent, user level, system level, or the `STXT_PATH` override), loads
|
|
23
|
+
* every definition of every level and applies the per-namespace precedence.
|
|
24
|
+
*
|
|
25
|
+
* The resolver is host-agnostic: all file-system and environment access goes through the
|
|
26
|
+
* injected {@link DiscoveryFileSystem} and {@link DiscoveryEnvironment}, so the same logic
|
|
27
|
+
* serves a command line (Node `fs`), an editor (`vscode.workspace.fs`) or a test (an
|
|
28
|
+
* in-memory tree).
|
|
29
|
+
*
|
|
30
|
+
* Loaded levels are cached by directory: resolving many documents that share levels loads
|
|
31
|
+
* each directory once, which is the sharing that STXT-DISCOVERY-SPEC section 7 allows —
|
|
32
|
+
* a level's content does not depend on which document is being resolved. Call
|
|
33
|
+
* {@link clearCache} when the underlying files may have changed.
|
|
34
|
+
*/
|
|
35
|
+
class DiscoveryResolver {
|
|
36
|
+
/**
|
|
37
|
+
* Creates a resolver.
|
|
38
|
+
*
|
|
39
|
+
* @param fs file-system access.
|
|
40
|
+
* @param env environment access (`STXT_PATH`, user and system directories).
|
|
41
|
+
* @param options optional settings.
|
|
42
|
+
*/
|
|
43
|
+
constructor(fs, env, options) {
|
|
44
|
+
this.fs = fs;
|
|
45
|
+
this.env = env;
|
|
46
|
+
this.schemaMeta = new SchemaProviderMeta_1.SchemaProviderMeta();
|
|
47
|
+
this.templateMeta = new MetaTemplateSchemaProvider_1.MetaTemplateSchemaProvider();
|
|
48
|
+
this.levelCache = new Map();
|
|
49
|
+
this.maxAscent = options?.maxAscent ?? DEFAULT_MAX_ASCENT;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Builds the resolution chain of a document (STXT-DISCOVERY-SPEC sections 4 and 6)
|
|
53
|
+
* without loading any definition.
|
|
54
|
+
*
|
|
55
|
+
* @param documentDir directory containing the document, or null for a document with no
|
|
56
|
+
* file-system location (standard input, an unsaved buffer), whose chain starts
|
|
57
|
+
* at the user level.
|
|
58
|
+
* @returns the existing resolution directories, highest precedence first.
|
|
59
|
+
*/
|
|
60
|
+
async resolveChain(documentDir) {
|
|
61
|
+
// STXT_PATH, when defined, completely replaces the chain (spec section 6).
|
|
62
|
+
const stxtPath = this.env.getStxtPath();
|
|
63
|
+
if (stxtPath !== null) {
|
|
64
|
+
return this.existingUnique(stxtPath);
|
|
65
|
+
}
|
|
66
|
+
const chain = [];
|
|
67
|
+
// Project level: every .stxt directory from the document's directory upward.
|
|
68
|
+
if (documentDir !== null) {
|
|
69
|
+
let dir = documentDir;
|
|
70
|
+
for (let level = 0; level < this.maxAscent && dir !== null; level++) {
|
|
71
|
+
const candidate = this.fs.join(dir, STXT_DIR);
|
|
72
|
+
if (await this.fs.isDirectory(candidate)) {
|
|
73
|
+
chain.push(candidate);
|
|
74
|
+
}
|
|
75
|
+
dir = this.fs.parentOf(dir);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// User and system levels. The ascent may have reached them already (a document
|
|
79
|
+
// under the user's home finds $HOME/.stxt as a project candidate): deduplicate.
|
|
80
|
+
const userDir = this.env.getUserLevelDir();
|
|
81
|
+
const systemDir = this.env.getSystemLevelDir();
|
|
82
|
+
for (const dir of [userDir, systemDir]) {
|
|
83
|
+
if (dir !== null && !chain.includes(dir) && await this.fs.isDirectory(dir)) {
|
|
84
|
+
chain.push(dir);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return chain;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolves the definitions applicable to a document: builds its chain, loads every
|
|
91
|
+
* level (from the cache when already loaded) and returns the result with the
|
|
92
|
+
* per-namespace precedence applied.
|
|
93
|
+
*
|
|
94
|
+
* @param documentDir directory containing the document, or null for a document with no
|
|
95
|
+
* file-system location.
|
|
96
|
+
* @returns the resolution result, usable directly as a `SchemaProvider`.
|
|
97
|
+
*/
|
|
98
|
+
async resolve(documentDir) {
|
|
99
|
+
const chain = await this.resolveChain(documentDir);
|
|
100
|
+
const levels = [];
|
|
101
|
+
for (const dir of chain) {
|
|
102
|
+
levels.push(await this.loadLevel(dir));
|
|
103
|
+
}
|
|
104
|
+
return new DiscoveryResult_1.DiscoveryResult(levels, this.schemaMeta, this.templateMeta);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Empties the level cache, so that the next resolve re-reads every directory. Call it
|
|
108
|
+
* when the definition files may have changed (e.g. from a file watcher).
|
|
109
|
+
*/
|
|
110
|
+
clearCache() {
|
|
111
|
+
this.levelCache.clear();
|
|
112
|
+
}
|
|
113
|
+
// Filters a list of directories down to the existing ones, removing duplicates.
|
|
114
|
+
async existingUnique(dirs) {
|
|
115
|
+
const result = [];
|
|
116
|
+
for (const dir of dirs) {
|
|
117
|
+
if (!result.includes(dir) && await this.fs.isDirectory(dir)) {
|
|
118
|
+
result.push(dir);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
// Loads a resolution directory (or returns it from the cache): every file under it,
|
|
124
|
+
// recursively, with the level-local duplicate detection of spec section 5.
|
|
125
|
+
async loadLevel(dir) {
|
|
126
|
+
const cached = this.levelCache.get(dir);
|
|
127
|
+
if (cached) {
|
|
128
|
+
return cached;
|
|
129
|
+
}
|
|
130
|
+
const level = { dir, definitions: new Map(), errors: [] };
|
|
131
|
+
const conflicted = new Set();
|
|
132
|
+
for (const file of await this.collectFiles(dir)) {
|
|
133
|
+
await this.loadFile(file, level, conflicted);
|
|
134
|
+
}
|
|
135
|
+
this.levelCache.set(dir, level);
|
|
136
|
+
return level;
|
|
137
|
+
}
|
|
138
|
+
// Collects every file under a directory, recursively, sorted by path so that results
|
|
139
|
+
// and error messages do not depend on the listing order of the file system.
|
|
140
|
+
async collectFiles(dir) {
|
|
141
|
+
const files = [];
|
|
142
|
+
const entries = [...await this.fs.listDirectory(dir)].sort((a, b) => a.path < b.path ? -1 : 1);
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (entry.isDirectory) {
|
|
145
|
+
files.push(...await this.collectFiles(entry.path));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
files.push(entry.path);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return files;
|
|
152
|
+
}
|
|
153
|
+
// Loads one file of a level: parses it and registers every root as a definition,
|
|
154
|
+
// reporting the errors of spec section 8.
|
|
155
|
+
async loadFile(file, level, conflicted) {
|
|
156
|
+
// Spec section 3: every file under a resolution directory must be a definition.
|
|
157
|
+
if (!file.endsWith(STXT_EXTENSION)) {
|
|
158
|
+
level.errors.push(new DiscoveryError_1.DiscoveryError(DiscoveryError_1.DiscoveryError.NOT_A_DEFINITION, file, `Not an STXT definition file: ${file}`));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
let nodes;
|
|
162
|
+
try {
|
|
163
|
+
nodes = new Parser_1.Parser().parse(await this.fs.readFile(file));
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
level.errors.push(new DiscoveryError_1.DiscoveryError(DiscoveryError_1.DiscoveryError.NOT_PARSEABLE, file, `Cannot parse ${file}: ${e instanceof Error ? e.message : String(e)}`));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (nodes.length === 0) {
|
|
170
|
+
level.errors.push(new DiscoveryError_1.DiscoveryError(DiscoveryError_1.DiscoveryError.NOT_A_DEFINITION, file, `Empty document, not a definition: ${file}`));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
for (const node of nodes) {
|
|
174
|
+
this.loadRootNode(node, file, level, conflicted);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Validates one root node against its meta-schema, compiles it to a schema and
|
|
178
|
+
// registers it in the level, detecting same-level duplicates.
|
|
179
|
+
loadRootNode(node, file, level, conflicted) {
|
|
180
|
+
const namespace = node.getNamespace();
|
|
181
|
+
let schema;
|
|
182
|
+
try {
|
|
183
|
+
if (namespace === "@stxt.template") {
|
|
184
|
+
schema = this.compile(node, this.templateMeta, TemplateParser_1.transformTemplateNodeToSchema);
|
|
185
|
+
}
|
|
186
|
+
else if (namespace === "@stxt.schema") {
|
|
187
|
+
schema = this.compile(node, this.schemaMeta, SchemaParser_1.transformNodeToSchema);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
level.errors.push(new DiscoveryError_1.DiscoveryError(DiscoveryError_1.DiscoveryError.NOT_A_DEFINITION, file, `Root node belongs to '${namespace ?? ""}', not to @stxt.schema or @stxt.template: ${file}`));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
const message = e instanceof ParseException_1.ParseException ? `[${e.code}] ${e.message}` : String(e);
|
|
196
|
+
level.errors.push(new DiscoveryError_1.DiscoveryError(DiscoveryError_1.DiscoveryError.INVALID_DEFINITION, file, `Invalid definition in ${file}: ${message}`));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const key = StringUtils_1.StringUtils.lowerCase(schema.getNamespace());
|
|
200
|
+
const existing = level.definitions.get(key);
|
|
201
|
+
// Spec section 8: on a same-level duplicate, never silently pick one of the
|
|
202
|
+
// definitions — the namespace has no active definition while the conflict exists.
|
|
203
|
+
if (conflicted.has(key) || existing) {
|
|
204
|
+
if (existing) {
|
|
205
|
+
level.definitions.delete(key);
|
|
206
|
+
conflicted.add(key);
|
|
207
|
+
}
|
|
208
|
+
const firstFile = existing ? existing.file : "another file of this level";
|
|
209
|
+
level.errors.push(new DiscoveryError_1.DiscoveryError(DiscoveryError_1.DiscoveryError.DUPLICATE_NAMESPACE, file, `Duplicate definition for namespace '${schema.getNamespace()}' at level ${level.dir}: ` +
|
|
210
|
+
`already defined in ${firstFile}`, schema.getNamespace()));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const definition = {
|
|
214
|
+
namespace: schema.getNamespace(),
|
|
215
|
+
schema,
|
|
216
|
+
file,
|
|
217
|
+
levelDir: level.dir,
|
|
218
|
+
};
|
|
219
|
+
level.definitions.set(key, definition);
|
|
220
|
+
}
|
|
221
|
+
// Validates a root node against a meta-schema and transforms it into a Schema,
|
|
222
|
+
// throwing the first validation error (same policy as UnifiedSchemaProvider).
|
|
223
|
+
compile(node, meta, transform) {
|
|
224
|
+
const errors = new SchemaValidator_1.SchemaValidator(meta, true).validate(node);
|
|
225
|
+
if (errors.length > 0) {
|
|
226
|
+
throw errors[0];
|
|
227
|
+
}
|
|
228
|
+
return transform(node);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
exports.DiscoveryResolver = DiscoveryResolver;
|
|
232
|
+
//# sourceMappingURL=DiscoveryResolver.js.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Schema } from "../schema/Schema";
|
|
2
|
+
import { SchemaProvider } from "../schema/SchemaProvider";
|
|
3
|
+
import { DiscoveryError } from "./DiscoveryError";
|
|
4
|
+
/**
|
|
5
|
+
* An active definition: a schema or template that won the per-namespace precedence for a
|
|
6
|
+
* document's resolution chain, together with where it came from.
|
|
7
|
+
*/
|
|
8
|
+
export interface DiscoveryDefinition {
|
|
9
|
+
/** Target namespace of the definition, as written in the definition document. */
|
|
10
|
+
namespace: string;
|
|
11
|
+
/** The compiled schema (templates are compiled to schemas at load time). */
|
|
12
|
+
schema: Schema;
|
|
13
|
+
/** Full path of the file the definition was read from. */
|
|
14
|
+
file: string;
|
|
15
|
+
/** Resolution directory (level) the file belongs to. */
|
|
16
|
+
levelDir: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A loaded resolution directory: its definitions indexed by lowercased target namespace.
|
|
20
|
+
* Namespaces in conflict inside the level (spec 8.1) are excluded from the map.
|
|
21
|
+
*/
|
|
22
|
+
export interface DiscoveryLevel {
|
|
23
|
+
/** Full path of the resolution directory. */
|
|
24
|
+
dir: string;
|
|
25
|
+
/** Definitions of the level by lowercased target namespace, conflicts excluded. */
|
|
26
|
+
definitions: Map<string, DiscoveryDefinition>;
|
|
27
|
+
/** Resolution errors found while loading this level. */
|
|
28
|
+
errors: DiscoveryError[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The outcome of resolving a document's definitions (STXT-DISCOVERY-SPEC): the chain of
|
|
32
|
+
* levels, the active definition per namespace (nearest level wins) and every resolution
|
|
33
|
+
* error found along the way.
|
|
34
|
+
*
|
|
35
|
+
* It implements {@link SchemaProvider}, so it can be handed directly to a
|
|
36
|
+
* `SchemaValidator`/`ConditionalValidator` to validate the document it was resolved for.
|
|
37
|
+
* Like `UnifiedSchemaProvider`, it serves the meta-schemas of the two reserved namespaces
|
|
38
|
+
* itself, so schema and template documents also validate against it.
|
|
39
|
+
*/
|
|
40
|
+
export declare class DiscoveryResult implements SchemaProvider {
|
|
41
|
+
private readonly levels;
|
|
42
|
+
private readonly schemaMeta;
|
|
43
|
+
private readonly templateMeta;
|
|
44
|
+
/**
|
|
45
|
+
* Creates a result. Built by {@link DiscoveryResolver}; not meant to be constructed
|
|
46
|
+
* directly.
|
|
47
|
+
*
|
|
48
|
+
* @param levels loaded levels of the chain, highest precedence first.
|
|
49
|
+
* @param schemaMeta provider of the @stxt.schema meta-schema.
|
|
50
|
+
* @param templateMeta provider of the @stxt.template meta-schema.
|
|
51
|
+
*/
|
|
52
|
+
constructor(levels: ReadonlyArray<DiscoveryLevel>, schemaMeta: SchemaProvider, templateMeta: SchemaProvider);
|
|
53
|
+
/**
|
|
54
|
+
* Resolves the schema that applies to a namespace: the meta-schemas for the two
|
|
55
|
+
* reserved namespaces, and otherwise the active definition of the nearest level.
|
|
56
|
+
*
|
|
57
|
+
* @param namespace namespace whose schema is wanted.
|
|
58
|
+
* @returns the schema of the namespace, or null if the chain has no definition for it.
|
|
59
|
+
*/
|
|
60
|
+
getSchema(namespace: string): Schema | null | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* The active definition of a namespace: the one from the nearest level that defines it
|
|
63
|
+
* (STXT-DISCOVERY-SPEC section 5), with its provenance.
|
|
64
|
+
*
|
|
65
|
+
* @param namespace namespace whose definition is wanted.
|
|
66
|
+
* @returns the active definition, or undefined if the chain has none for the namespace.
|
|
67
|
+
*/
|
|
68
|
+
getDefinition(namespace: string): DiscoveryDefinition | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Every active definition of the chain, with per-namespace precedence already applied:
|
|
71
|
+
* one entry per namespace, from its nearest defining level.
|
|
72
|
+
*
|
|
73
|
+
* @returns the active definitions, ordered by level (nearest level's definitions first).
|
|
74
|
+
*/
|
|
75
|
+
getActiveDefinitions(): ReadonlyArray<DiscoveryDefinition>;
|
|
76
|
+
/**
|
|
77
|
+
* Every active schema of the chain (the schemas of {@link getActiveDefinitions}).
|
|
78
|
+
*
|
|
79
|
+
* @returns the active schemas, ordered by level (nearest level's schemas first).
|
|
80
|
+
*/
|
|
81
|
+
getAllSchemas(): ReadonlyArray<Schema>;
|
|
82
|
+
/**
|
|
83
|
+
* The resolution chain: the loaded level directories, highest precedence first.
|
|
84
|
+
*
|
|
85
|
+
* @returns the directories of the chain, in precedence order.
|
|
86
|
+
*/
|
|
87
|
+
getChain(): ReadonlyArray<string>;
|
|
88
|
+
/**
|
|
89
|
+
* Every resolution error found while loading the chain (STXT-DISCOVERY-SPEC section 8).
|
|
90
|
+
*
|
|
91
|
+
* @returns the errors, ordered by level and then by file.
|
|
92
|
+
*/
|
|
93
|
+
getErrors(): ReadonlyArray<DiscoveryError>;
|
|
94
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DiscoveryResult = void 0;
|
|
4
|
+
const StringUtils_1 = require("../core/StringUtils");
|
|
5
|
+
/**
|
|
6
|
+
* The outcome of resolving a document's definitions (STXT-DISCOVERY-SPEC): the chain of
|
|
7
|
+
* levels, the active definition per namespace (nearest level wins) and every resolution
|
|
8
|
+
* error found along the way.
|
|
9
|
+
*
|
|
10
|
+
* It implements {@link SchemaProvider}, so it can be handed directly to a
|
|
11
|
+
* `SchemaValidator`/`ConditionalValidator` to validate the document it was resolved for.
|
|
12
|
+
* Like `UnifiedSchemaProvider`, it serves the meta-schemas of the two reserved namespaces
|
|
13
|
+
* itself, so schema and template documents also validate against it.
|
|
14
|
+
*/
|
|
15
|
+
class DiscoveryResult {
|
|
16
|
+
/**
|
|
17
|
+
* Creates a result. Built by {@link DiscoveryResolver}; not meant to be constructed
|
|
18
|
+
* directly.
|
|
19
|
+
*
|
|
20
|
+
* @param levels loaded levels of the chain, highest precedence first.
|
|
21
|
+
* @param schemaMeta provider of the @stxt.schema meta-schema.
|
|
22
|
+
* @param templateMeta provider of the @stxt.template meta-schema.
|
|
23
|
+
*/
|
|
24
|
+
constructor(levels, schemaMeta, templateMeta) {
|
|
25
|
+
this.levels = levels;
|
|
26
|
+
this.schemaMeta = schemaMeta;
|
|
27
|
+
this.templateMeta = templateMeta;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the schema that applies to a namespace: the meta-schemas for the two
|
|
31
|
+
* reserved namespaces, and otherwise the active definition of the nearest level.
|
|
32
|
+
*
|
|
33
|
+
* @param namespace namespace whose schema is wanted.
|
|
34
|
+
* @returns the schema of the namespace, or null if the chain has no definition for it.
|
|
35
|
+
*/
|
|
36
|
+
getSchema(namespace) {
|
|
37
|
+
if (namespace === "@stxt.template") {
|
|
38
|
+
return this.templateMeta.getSchema(namespace);
|
|
39
|
+
}
|
|
40
|
+
else if (namespace === "@stxt.schema") {
|
|
41
|
+
return this.schemaMeta.getSchema(namespace);
|
|
42
|
+
}
|
|
43
|
+
return this.getDefinition(namespace)?.schema ?? null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The active definition of a namespace: the one from the nearest level that defines it
|
|
47
|
+
* (STXT-DISCOVERY-SPEC section 5), with its provenance.
|
|
48
|
+
*
|
|
49
|
+
* @param namespace namespace whose definition is wanted.
|
|
50
|
+
* @returns the active definition, or undefined if the chain has none for the namespace.
|
|
51
|
+
*/
|
|
52
|
+
getDefinition(namespace) {
|
|
53
|
+
const key = StringUtils_1.StringUtils.lowerCase(namespace);
|
|
54
|
+
for (const level of this.levels) {
|
|
55
|
+
const definition = level.definitions.get(key);
|
|
56
|
+
if (definition) {
|
|
57
|
+
return definition;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Every active definition of the chain, with per-namespace precedence already applied:
|
|
64
|
+
* one entry per namespace, from its nearest defining level.
|
|
65
|
+
*
|
|
66
|
+
* @returns the active definitions, ordered by level (nearest level's definitions first).
|
|
67
|
+
*/
|
|
68
|
+
getActiveDefinitions() {
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
const result = [];
|
|
71
|
+
for (const level of this.levels) {
|
|
72
|
+
for (const [key, definition] of level.definitions) {
|
|
73
|
+
if (!seen.has(key)) {
|
|
74
|
+
seen.add(key);
|
|
75
|
+
result.push(definition);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Every active schema of the chain (the schemas of {@link getActiveDefinitions}).
|
|
83
|
+
*
|
|
84
|
+
* @returns the active schemas, ordered by level (nearest level's schemas first).
|
|
85
|
+
*/
|
|
86
|
+
getAllSchemas() {
|
|
87
|
+
return this.getActiveDefinitions().map(definition => definition.schema);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The resolution chain: the loaded level directories, highest precedence first.
|
|
91
|
+
*
|
|
92
|
+
* @returns the directories of the chain, in precedence order.
|
|
93
|
+
*/
|
|
94
|
+
getChain() {
|
|
95
|
+
return this.levels.map(level => level.dir);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Every resolution error found while loading the chain (STXT-DISCOVERY-SPEC section 8).
|
|
99
|
+
*
|
|
100
|
+
* @returns the errors, ordered by level and then by file.
|
|
101
|
+
*/
|
|
102
|
+
getErrors() {
|
|
103
|
+
const result = [];
|
|
104
|
+
for (const level of this.levels) {
|
|
105
|
+
result.push(...level.errors);
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.DiscoveryResult = DiscoveryResult;
|
|
111
|
+
//# sourceMappingURL=DiscoveryResult.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Node } from "../core/Node";
|
|
2
|
+
/** Canonical JSON representation of a parsed STXT document (STXT-TREE-SPEC). */
|
|
3
|
+
export type CanonicalDocument = CanonicalNode[];
|
|
4
|
+
/** A node in the canonical JSON representation of an STXT document. */
|
|
5
|
+
export type CanonicalNode = CanonicalInlineNode | CanonicalBlockNode;
|
|
6
|
+
/** Canonical representation of an INLINE (`:`) node. */
|
|
7
|
+
export interface CanonicalInlineNode {
|
|
8
|
+
name: string;
|
|
9
|
+
canonicalName: string;
|
|
10
|
+
namespace: string;
|
|
11
|
+
form: "inline";
|
|
12
|
+
value: string;
|
|
13
|
+
children: CanonicalNode[];
|
|
14
|
+
}
|
|
15
|
+
/** Canonical representation of a BLOCK (`>>`) node. */
|
|
16
|
+
export interface CanonicalBlockNode {
|
|
17
|
+
name: string;
|
|
18
|
+
canonicalName: string;
|
|
19
|
+
namespace: string;
|
|
20
|
+
form: "block";
|
|
21
|
+
lines: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Converts every root node of a parsed document to the logical tree defined by
|
|
25
|
+
* STXT-TREE-SPEC. The result deliberately excludes source positions, indentation
|
|
26
|
+
* style, comments and derived values such as a qualified name.
|
|
27
|
+
*
|
|
28
|
+
* @param nodes root nodes of an already parsed STXT document.
|
|
29
|
+
* @returns the canonical document tree, ready to be serialized as JSON.
|
|
30
|
+
*/
|
|
31
|
+
export declare function toCanonicalTree(nodes: ReadonlyArray<Node>): CanonicalDocument;
|
|
32
|
+
/**
|
|
33
|
+
* Serializes the canonical tree of a parsed document as human-readable JSON.
|
|
34
|
+
* JSON whitespace is not part of STXT-TREE-SPEC; two-space indentation is this
|
|
35
|
+
* implementation's deterministic presentation for command-line use.
|
|
36
|
+
*
|
|
37
|
+
* @param nodes root nodes of an already parsed STXT document.
|
|
38
|
+
* @returns the canonical document tree encoded as JSON, without a final line break.
|
|
39
|
+
*/
|
|
40
|
+
export declare function toCanonicalJson(nodes: ReadonlyArray<Node>): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toCanonicalTree = toCanonicalTree;
|
|
4
|
+
exports.toCanonicalJson = toCanonicalJson;
|
|
5
|
+
/**
|
|
6
|
+
* Converts every root node of a parsed document to the logical tree defined by
|
|
7
|
+
* STXT-TREE-SPEC. The result deliberately excludes source positions, indentation
|
|
8
|
+
* style, comments and derived values such as a qualified name.
|
|
9
|
+
*
|
|
10
|
+
* @param nodes root nodes of an already parsed STXT document.
|
|
11
|
+
* @returns the canonical document tree, ready to be serialized as JSON.
|
|
12
|
+
*/
|
|
13
|
+
function toCanonicalTree(nodes) {
|
|
14
|
+
return nodes.map(node => toCanonicalNode(node));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Serializes the canonical tree of a parsed document as human-readable JSON.
|
|
18
|
+
* JSON whitespace is not part of STXT-TREE-SPEC; two-space indentation is this
|
|
19
|
+
* implementation's deterministic presentation for command-line use.
|
|
20
|
+
*
|
|
21
|
+
* @param nodes root nodes of an already parsed STXT document.
|
|
22
|
+
* @returns the canonical document tree encoded as JSON, without a final line break.
|
|
23
|
+
*/
|
|
24
|
+
function toCanonicalJson(nodes) {
|
|
25
|
+
return JSON.stringify(toCanonicalTree(nodes), null, 2);
|
|
26
|
+
}
|
|
27
|
+
function toCanonicalNode(node) {
|
|
28
|
+
if (node.isTextNode()) {
|
|
29
|
+
return {
|
|
30
|
+
name: node.getName(),
|
|
31
|
+
canonicalName: node.getNormalizedName(),
|
|
32
|
+
namespace: node.getNamespace(),
|
|
33
|
+
form: "block",
|
|
34
|
+
lines: [...node.getTextLines()],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
name: node.getName(),
|
|
39
|
+
canonicalName: node.getNormalizedName(),
|
|
40
|
+
namespace: node.getNamespace(),
|
|
41
|
+
form: "inline",
|
|
42
|
+
value: node.getValue(),
|
|
43
|
+
children: node.getChildren().map(child => toCanonicalNode(child)),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=TreeJson.js.map
|
|
@@ -52,9 +52,14 @@ class SchemaProviderMemory {
|
|
|
52
52
|
addSchema(txt) {
|
|
53
53
|
const parser = new Parser_1.Parser();
|
|
54
54
|
const node = parser.parse(txt)[0];
|
|
55
|
-
|
|
55
|
+
// A schema that does not validate against its meta-schema must not be
|
|
56
|
+
// registered (same policy as UnifiedSchemaProvider/DiscoveryResolver)
|
|
56
57
|
const schemaValidator = new SchemaValidator_1.SchemaValidator(new SchemaProviderMeta_1.SchemaProviderMeta(), true);
|
|
57
|
-
schemaValidator.validate(node);
|
|
58
|
+
const errors = schemaValidator.validate(node);
|
|
59
|
+
if (errors.length > 0) {
|
|
60
|
+
throw errors[0];
|
|
61
|
+
}
|
|
62
|
+
const schema = (0, SchemaParser_1.transformNodeToSchema)(node);
|
|
58
63
|
const key = schema.getNamespace();
|
|
59
64
|
this.schemas.set(key, schema);
|
|
60
65
|
}
|
|
@@ -18,7 +18,8 @@ export declare class TemplateSchemaProviderMemory extends SchemaProviderMemory {
|
|
|
18
18
|
*
|
|
19
19
|
* @param template text of the `@stxt.template` document.
|
|
20
20
|
* @throws ValidationException with code `INVALID_SCHEMA` if the document does not hold exactly
|
|
21
|
-
* one template
|
|
21
|
+
* one template or the resulting schema has no namespace, or the first validation error
|
|
22
|
+
* if the template does not validate against the template meta-schema.
|
|
22
23
|
*/
|
|
23
24
|
addTemplate(template: string): void;
|
|
24
25
|
}
|
|
@@ -31,7 +31,8 @@ class TemplateSchemaProviderMemory extends SchemaProviderMemory_1.SchemaProvider
|
|
|
31
31
|
*
|
|
32
32
|
* @param template text of the `@stxt.template` document.
|
|
33
33
|
* @throws ValidationException with code `INVALID_SCHEMA` if the document does not hold exactly
|
|
34
|
-
* one template
|
|
34
|
+
* one template or the resulting schema has no namespace, or the first validation error
|
|
35
|
+
* if the template does not validate against the template meta-schema.
|
|
35
36
|
*/
|
|
36
37
|
addTemplate(template) {
|
|
37
38
|
const parser = new Parser_1.Parser();
|
|
@@ -39,9 +40,13 @@ class TemplateSchemaProviderMemory extends SchemaProviderMemory_1.SchemaProvider
|
|
|
39
40
|
if (nodes.length !== 1) {
|
|
40
41
|
throw new ValidationException_1.ValidationException(0, "INVALID_SCHEMA", `There are ${nodes.length}, and expected is 1`);
|
|
41
42
|
}
|
|
42
|
-
//
|
|
43
|
+
// A template that does not validate against the template meta-schema must not
|
|
44
|
+
// be registered (same policy as UnifiedSchemaProvider/DiscoveryResolver)
|
|
43
45
|
const schemaValidator = new SchemaValidator_1.SchemaValidator(new MetaTemplateSchemaProvider_1.MetaTemplateSchemaProvider(), true);
|
|
44
|
-
schemaValidator.validate(nodes[0]);
|
|
46
|
+
const errors = schemaValidator.validate(nodes[0]);
|
|
47
|
+
if (errors.length > 0) {
|
|
48
|
+
throw errors[0];
|
|
49
|
+
}
|
|
45
50
|
// Build the schema out of the template
|
|
46
51
|
const sch = (0, TemplateParser_1.transformTemplateNodeToSchema)(nodes[0]);
|
|
47
52
|
// Minimum safety check (Java checked the expected namespace here too)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stxt-lang/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Parser and schema validator for STXT, an indentation-based structured-text format.",
|
|
5
5
|
"main": "out/all.js",
|
|
6
6
|
"types": "out/all.d.ts",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"out/all.js",
|
|
12
12
|
"out/all.d.ts",
|
|
13
13
|
"out/core",
|
|
14
|
+
"out/discovery",
|
|
14
15
|
"out/exceptions",
|
|
15
16
|
"out/processors",
|
|
16
17
|
"out/runtime",
|