@stxt-lang/core 0.14.1 → 0.16.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.
Files changed (44) hide show
  1. package/README.md +3 -3
  2. package/out/core/InlineNode.js +1 -0
  3. package/out/core/LineParser.js +1 -1
  4. package/out/core/NameNamespace.d.ts +5 -5
  5. package/out/core/NameNamespace.js +5 -5
  6. package/out/core/NamespaceValidator.d.ts +7 -7
  7. package/out/core/NamespaceValidator.js +7 -7
  8. package/out/core/Parser.d.ts +5 -0
  9. package/out/core/Parser.js +22 -17
  10. package/out/core/StringUtils.d.ts +0 -7
  11. package/out/core/StringUtils.js +0 -10
  12. package/out/core/TextNode.d.ts +7 -0
  13. package/out/core/TextNode.js +11 -0
  14. package/out/discovery/DiscoveryResolver.d.ts +1 -1
  15. package/out/discovery/DiscoveryResolver.js +36 -17
  16. package/out/discovery/DiscoveryResult.js +6 -4
  17. package/out/exceptions/ParseException.d.ts +8 -1
  18. package/out/exceptions/ParseException.js +7 -0
  19. package/out/runtime/Formatter.d.ts +4 -4
  20. package/out/runtime/Formatter.js +15 -8
  21. package/out/runtime/NodeWriter.js +10 -2
  22. package/out/runtime/UnifiedSchemaProvider.d.ts +1 -3
  23. package/out/runtime/UnifiedSchemaProvider.js +14 -32
  24. package/out/schema/DefinitionCompiler.d.ts +27 -0
  25. package/out/schema/DefinitionCompiler.js +53 -0
  26. package/out/schema/NodeDefinition.js +6 -3
  27. package/out/schema/Schema.d.ts +2 -0
  28. package/out/schema/Schema.js +7 -4
  29. package/out/schema/SchemaParser.js +13 -12
  30. package/out/schema/SchemaProviderMemory.js +3 -19
  31. package/out/schema/SchemaProviderMeta.d.ts +7 -1
  32. package/out/schema/SchemaProviderMeta.js +10 -9
  33. package/out/schema/SchemaValidator.js +5 -3
  34. package/out/schema/type/BASE64.d.ts +4 -2
  35. package/out/schema/type/BASE64.js +34 -10
  36. package/out/schema/type/MARKDOWN.d.ts +2 -1
  37. package/out/schema/type/MARKDOWN.js +4 -8
  38. package/out/template/ChildLineParser.js +12 -9
  39. package/out/template/MetaTemplateSchemaProvider.d.ts +3 -1
  40. package/out/template/MetaTemplateSchemaProvider.js +12 -11
  41. package/out/template/TemplateParser.d.ts +1 -2
  42. package/out/template/TemplateParser.js +116 -89
  43. package/out/template/TemplateSchemaProviderMemory.js +3 -19
  44. package/package.json +2 -3
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # @stxt-lang/core
2
2
 
3
- Parser and schema validator for **STXT**, an indentation-based structured-text format.
3
+ Parser and schema validator for **STXT**, an indentation-based structured-text language.
4
4
 
5
- STXT is a plain-text format for writing structured, semantic documents: no braces, no closing tags, just indentation. It is designed to be equally readable by humans and by machines, and it comes with an optional schema layer so documents can be validated.
5
+ STXT is a plain-text language for writing structured, semantic documents: no braces, no closing tags, just indentation. It is designed to be equally readable by humans and by machines, and it comes with an optional schema layer so documents can be validated.
6
6
 
7
7
  - Website and language reference: <https://stxt.dev>
8
- - VSCode extension: [STXT - Semantic Text](https://marketplace.visualstudio.com/items?itemName=stxt-lang.stxt)
8
+ - VSCode extension: [STXT Language](https://marketplace.visualstudio.com/items?itemName=stxt-lang.stxt)
9
9
  - Java implementation: [`dev.stxt:stxt-core`](https://central.sonatype.com/artifact/dev.stxt/stxt-core) on Maven Central
10
10
  - Python implementation: [`stxt`](https://pypi.org/project/stxt/) on PyPI
11
11
 
@@ -66,6 +66,7 @@ class InlineNode extends Node_1.Node {
66
66
  if (child.getParent() !== null) {
67
67
  throw new RuntimeException_1.RuntimeException("NODE_ALREADY_ATTACHED", `Node '${child.getName()}' already has a parent: detach it first`);
68
68
  }
69
+ // eslint-disable-next-line @typescript-eslint/no-this-alias -- cursor of the ancestor walk, not an alias kept around
69
70
  for (let p = this; p !== null; p = p.getParent()) {
70
71
  if (p === child) {
71
72
  throw new RuntimeException_1.RuntimeException("NODE_CYCLE", `Node '${child.getName()}' cannot be a child of itself or of one of its descendants`);
@@ -64,7 +64,7 @@ function parseLine(line, lastNodeBlock, lastLevel, numLine, validate = true) {
64
64
  if (lastNodeBlock && level > lastLevel) {
65
65
  const text = StringUtils_1.StringUtils.rightTrim(line.substring(pointer + 1));
66
66
  // The prefix covering the block level must be homogeneous (spec 10.2, rule 2);
67
- // empty lines are always preserved and are exempt from it (spec 10.3)
67
+ // empty lines are never an error and are exempt from it (spec 10.3)
68
68
  if (validate && sawSpace && sawTab && text.length > 0) {
69
69
  throw new ParseException_1.ParseException(numLine, "INDENTATION_MIXED", `Mixed tabs and spaces in indentation`);
70
70
  }
@@ -3,11 +3,11 @@ export declare class NameNamespace {
3
3
  private readonly name;
4
4
  private readonly namespace;
5
5
  /**
6
- * Creates a resolved name and namespace pair.
7
- *
8
- * @param name name of the node without the namespace part.
9
- * @param namespace resolved namespace (its own or inherited).
10
- */
6
+ * Creates a resolved name and namespace pair.
7
+ *
8
+ * @param name name of the node without the namespace part.
9
+ * @param namespace resolved namespace (its own or inherited).
10
+ */
11
11
  constructor(name: string, namespace: string);
12
12
  /** @returns the name of the node, without the namespace part. */
13
13
  getName(): string;
@@ -4,11 +4,11 @@ exports.NameNamespace = void 0;
4
4
  /** Result of splitting a raw node name into its resolved name and namespace. */
5
5
  class NameNamespace {
6
6
  /**
7
- * Creates a resolved name and namespace pair.
8
- *
9
- * @param name name of the node without the namespace part.
10
- * @param namespace resolved namespace (its own or inherited).
11
- */
7
+ * Creates a resolved name and namespace pair.
8
+ *
9
+ * @param name name of the node without the namespace part.
10
+ * @param namespace resolved namespace (its own or inherited).
11
+ */
12
12
  constructor(name, namespace) {
13
13
  this.name = name;
14
14
  this.namespace = namespace;
@@ -11,13 +11,6 @@ export declare class NamespaceValidator {
11
11
  * valid examples: "xxx", "xxx.ddd", "zzz.ttt.ooo", "@xxx", "@xxx.ddd".
12
12
  */
13
13
  private static readonly NAMESPACE_FORMAT;
14
- /**
15
- * Validates the format of a namespace.
16
- *
17
- * @param namespace already normalized namespace to validate; ignored when null or empty.
18
- * @param lineNumber line number, for the error message.
19
- * @throws ParseException with code `INVALID_NAMESPACE` if it does not match the format.
20
- */
21
14
  /**
22
15
  * Tells whether a namespace matches the format, without throwing.
23
16
  *
@@ -25,5 +18,12 @@ export declare class NamespaceValidator {
25
18
  * @returns true if it matches the format; false when it is null, empty or malformed.
26
19
  */
27
20
  static isValid(namespace: string | null | undefined): boolean;
21
+ /**
22
+ * Validates the format of a namespace.
23
+ *
24
+ * @param namespace already normalized namespace to validate; ignored when null or empty.
25
+ * @param lineNumber line number, for the error message.
26
+ * @throws ParseException with code `INVALID_NAMESPACE` if it does not match the format.
27
+ */
28
28
  static validateNamespaceFormat(namespace: string | null | undefined, lineNumber: number): void;
29
29
  }
@@ -4,13 +4,6 @@ exports.NamespaceValidator = void 0;
4
4
  const ParseException_1 = require("../exceptions/ParseException");
5
5
  /** Validates the format of STXT `(a.b.c)` namespaces. */
6
6
  class NamespaceValidator {
7
- /**
8
- * Validates the format of a namespace.
9
- *
10
- * @param namespace already normalized namespace to validate; ignored when null or empty.
11
- * @param lineNumber line number, for the error message.
12
- * @throws ParseException with code `INVALID_NAMESPACE` if it does not match the format.
13
- */
14
7
  /**
15
8
  * Tells whether a namespace matches the format, without throwing.
16
9
  *
@@ -20,6 +13,13 @@ class NamespaceValidator {
20
13
  static isValid(namespace) {
21
14
  return !!namespace && NamespaceValidator.NAMESPACE_FORMAT.test(namespace);
22
15
  }
16
+ /**
17
+ * Validates the format of a namespace.
18
+ *
19
+ * @param namespace already normalized namespace to validate; ignored when null or empty.
20
+ * @param lineNumber line number, for the error message.
21
+ * @throws ParseException with code `INVALID_NAMESPACE` if it does not match the format.
22
+ */
23
23
  static validateNamespaceFormat(namespace, lineNumber) {
24
24
  if (!namespace) {
25
25
  return;
@@ -100,6 +100,11 @@ export declare class Parser {
100
100
  * registered callback fires the same.
101
101
  */
102
102
  private parseLines;
103
+ /**
104
+ * Processes one source line. Errors of this line are collected into the result and the
105
+ * traversal continues with the next line: returns true to keep going, false when a limit
106
+ * aborted the parse (its error is already emitted) — parseLines stops on it.
107
+ */
103
108
  private processLine;
104
109
  /**
105
110
  * Records an error raised while parsing or validating a line. Typed exceptions travel as they
@@ -139,20 +139,18 @@ class Parser {
139
139
  this.emitError(new LimitException_1.LimitException(lineNumber, "LIMIT_INPUT_SIZE_EXCEEDED", `Input larger than ${this.maxInputSize} characters`), result);
140
140
  return;
141
141
  }
142
- try {
143
- this.processLine(line, lineNumber, stack, result);
144
- }
145
- catch (e) {
146
- if (e instanceof LimitException_1.LimitException) {
147
- this.emitError(e, result);
148
- return;
149
- }
150
- throw e;
142
+ if (!this.processLine(line, lineNumber, stack, result)) {
143
+ return; // a limit aborted the parse; its error is already emitted
151
144
  }
152
145
  }
153
146
  // Close every node still open at EOF
154
147
  this.closeToLevel(stack, 0, result);
155
148
  }
149
+ /**
150
+ * Processes one source line. Errors of this line are collected into the result and the
151
+ * traversal continues with the next line: returns true to keep going, false when a limit
152
+ * aborted the parse (its error is already emitted) — parseLines stops on it.
153
+ */
156
154
  processLine(lineString, lineNumber, stack, result) {
157
155
  try {
158
156
  const lastNode = stack.length === 0 ? null : stack[stack.length - 1];
@@ -176,7 +174,7 @@ class Parser {
176
174
  this.observers.forEach(observer => {
177
175
  observer.onComment(lineNumber, lineString);
178
176
  });
179
- return;
177
+ return true;
180
178
  }
181
179
  const currentLevel = line.level;
182
180
  // When we are inside a text node and the level says it is still text,
@@ -188,17 +186,18 @@ class Parser {
188
186
  this.observers.forEach(observer => {
189
187
  observer.onTextLine(textNode, lineNumber, lineString, line);
190
188
  });
191
- return;
189
+ return true;
192
190
  }
193
191
  // Empty lines are ignored
194
192
  if (line.isEmpty()) {
195
- return;
193
+ return true;
196
194
  }
197
195
  // Nesting limit (spec 11.2): only a node line can open a new level. Comment and
198
196
  // block text lines returned above; with the consecutive-level rule this triggers
199
197
  // exactly when the first node at level maxNesting opens.
200
198
  if (this.maxNesting !== -1 && currentLevel >= this.maxNesting) {
201
- throw new LimitException_1.LimitException(lineNumber, "LIMIT_NESTING_EXCEEDED", `Nesting deeper than ${this.maxNesting} levels`);
199
+ this.emitError(new LimitException_1.LimitException(lineNumber, "LIMIT_NESTING_EXCEEDED", `Nesting deeper than ${this.maxNesting} levels`), result);
200
+ return false;
202
201
  }
203
202
  // Close the nodes down to the current level (this "finishes" them: validators and observers run)
204
203
  this.closeToLevel(stack, currentLevel, result);
@@ -220,11 +219,9 @@ class Parser {
220
219
  stack.push(node);
221
220
  }
222
221
  catch (e) {
223
- if (e instanceof LimitException_1.LimitException) {
224
- throw e;
225
- }
226
222
  this.handleError(e, lineNumber, result);
227
223
  }
224
+ return true;
228
225
  }
229
226
  /**
230
227
  * Records an error raised while parsing or validating a line. Typed exceptions travel as they
@@ -256,7 +253,15 @@ class Parser {
256
253
  }
257
254
  closeToLevel(stack, targetLevel, result) {
258
255
  while (stack.length > targetLevel) {
259
- const completed = stack.pop();
256
+ const completed = stack[stack.length - 1];
257
+ stack.pop();
258
+ // A closing block node drops its final empty lines (STXT-SPEC §10.3): they are not
259
+ // content, only visual separation or an editor's final line breaks. The validators
260
+ // and observers below already see the trimmed node; onTextLine did fire for these
261
+ // lines while the block was open, as process observation of the source.
262
+ if (completed instanceof TextNode_1.TextNode) {
263
+ completed.removeTrailingEmptyLines();
264
+ }
260
265
  // Hand it over to the validators
261
266
  this.validators.forEach(validator => {
262
267
  try {
@@ -27,13 +27,6 @@ export declare class StringUtils {
27
27
  * @returns the string without trailing blanks; null/undefined is treated as the empty string.
28
28
  */
29
29
  static rightTrim(s: string | null | undefined): string;
30
- /**
31
- * Removes every whitespace character of a string.
32
- *
33
- * @param input string to remove the spaces from.
34
- * @returns the string without any whitespace at all.
35
- */
36
- static cleanSpaces(input: string): string;
37
30
  /**
38
31
  * Lower-cases a string.
39
32
  *
@@ -33,16 +33,6 @@ class StringUtils {
33
33
  static rightTrim(s) {
34
34
  return (s ?? "").replace(this.TRAILING_BLANKS, "");
35
35
  }
36
- // Used for BASE64 and HEXADECIMAL nodes
37
- /**
38
- * Removes every whitespace character of a string.
39
- *
40
- * @param input string to remove the spaces from.
41
- * @returns the string without any whitespace at all.
42
- */
43
- static cleanSpaces(input) {
44
- return input.replace(/\s+/g, "");
45
- }
46
36
  // Used to normalize namespaces
47
37
  /**
48
38
  * Lower-cases a string.
@@ -56,6 +56,13 @@ export declare class TextNode extends Node {
56
56
  addTextLine(line: string): void;
57
57
  /** Removes every text line. */
58
58
  clearText(): void;
59
+ /**
60
+ * Removes the final empty lines (`""` elements at the end of the lines). The {@link Parser}
61
+ * calls it when the block closes (STXT-SPEC §10.3: the final empty lines of a block are not
62
+ * content); it is public because a programmatically built node may want the same
63
+ * normalization before writing.
64
+ */
65
+ removeTrailingEmptyLines(): void;
59
66
  getText(): string;
60
67
  isTextNode(): boolean;
61
68
  private static splitLines;
@@ -61,6 +61,17 @@ class TextNode extends Node_1.Node {
61
61
  clearText() {
62
62
  this.lines.length = 0;
63
63
  }
64
+ /**
65
+ * Removes the final empty lines (`""` elements at the end of the lines). The {@link Parser}
66
+ * calls it when the block closes (STXT-SPEC §10.3: the final empty lines of a block are not
67
+ * content); it is public because a programmatically built node may want the same
68
+ * normalization before writing.
69
+ */
70
+ removeTrailingEmptyLines() {
71
+ while (this.lines.length > 0 && this.lines[this.lines.length - 1] === "") {
72
+ this.lines.pop();
73
+ }
74
+ }
64
75
  getText() {
65
76
  return this.lines.join("\n");
66
77
  }
@@ -68,7 +68,7 @@ export declare class DiscoveryResolver {
68
68
  private existingUnique;
69
69
  private loadLevel;
70
70
  private collectFiles;
71
+ private collectFilesAt;
71
72
  private loadFile;
72
73
  private loadRootNode;
73
- private compile;
74
74
  }
@@ -4,8 +4,9 @@ exports.DiscoveryResolver = void 0;
4
4
  const Parser_1 = require("../core/Parser");
5
5
  const StringUtils_1 = require("../core/StringUtils");
6
6
  const ParseException_1 = require("../exceptions/ParseException");
7
+ const Schema_1 = require("../schema/Schema");
8
+ const DefinitionCompiler_1 = require("../schema/DefinitionCompiler");
7
9
  const SchemaProviderMeta_1 = require("../schema/SchemaProviderMeta");
8
- const SchemaValidator_1 = require("../schema/SchemaValidator");
9
10
  const SchemaParser_1 = require("../schema/SchemaParser");
10
11
  const MetaTemplateSchemaProvider_1 = require("../template/MetaTemplateSchemaProvider");
11
12
  const TemplateParser_1 = require("../template/TemplateParser");
@@ -17,6 +18,12 @@ const STXT_DIR = ".stxt";
17
18
  const STXT_EXTENSION = ".stxt";
18
19
  /** Default value of {@link DiscoveryOptions.maxAscent}. */
19
20
  const DEFAULT_MAX_ASCENT = 32;
21
+ /**
22
+ * Maximum depth of the recursive descent through a resolution directory's subtree. An
23
+ * internal safeguard (STXT-DISCOVERY-SPEC sections 3 and 10) against symlink loops and
24
+ * pathological trees; not part of the public API.
25
+ */
26
+ const DEFAULT_MAX_DESCENT = 32;
20
27
  /**
21
28
  * Reference implementation of STXT-DISCOVERY-SPEC: builds the resolution chain of a
22
29
  * document (project ascent, user level, system level, or the `STXT_PATH` override), loads
@@ -135,13 +142,34 @@ class DiscoveryResolver {
135
142
  return level;
136
143
  }
137
144
  // Collects every file under a directory, recursively, sorted by path so that results
138
- // and error messages do not depend on the listing order of the file system.
145
+ // and error messages do not depend on the listing order of the file system. The descent
146
+ // is bounded by DEFAULT_MAX_DESCENT and tolerant of listing failures (STXT-DISCOVERY-SPEC
147
+ // sections 3 and 10): a subdirectory that reaches the depth limit or cannot be listed
148
+ // contributes no files, never an exception. Together with adapters that do not follow
149
+ // directory symlinks, this stops symlink loops and pathological trees from turning
150
+ // resolution into unbounded recursion or an escaping error.
139
151
  async collectFiles(dir) {
152
+ return this.collectFilesAt(dir, 0);
153
+ }
154
+ async collectFilesAt(dir, depth) {
140
155
  const files = [];
141
- const entries = [...await this.fs.listDirectory(dir)].sort((a, b) => a.path < b.path ? -1 : 1);
156
+ // Safeguard against symlink loops and pathological trees (spec section 10): stop
157
+ // descending once the depth limit is reached.
158
+ if (depth >= DEFAULT_MAX_DESCENT) {
159
+ return files;
160
+ }
161
+ let entries;
162
+ try {
163
+ entries = [...await this.fs.listDirectory(dir)].sort((a, b) => a.path < b.path ? -1 : 1);
164
+ }
165
+ catch {
166
+ // A directory that cannot be listed contributes no files (spec section 3); it
167
+ // does not stop the resolution of the rest of the level.
168
+ return files;
169
+ }
142
170
  for (const entry of entries) {
143
171
  if (entry.isDirectory) {
144
- files.push(...await this.collectFiles(entry.path));
172
+ files.push(...await this.collectFilesAt(entry.path, depth + 1));
145
173
  }
146
174
  else {
147
175
  files.push(entry.path);
@@ -179,11 +207,11 @@ class DiscoveryResolver {
179
207
  const namespace = node.getNamespace();
180
208
  let schema;
181
209
  try {
182
- if (namespace === "@stxt.template") {
183
- schema = this.compile(node, this.templateMeta, TemplateParser_1.transformTemplateNodeToSchema);
210
+ if (namespace === Schema_1.Schema.TEMPLATE_NAMESPACE) {
211
+ schema = (0, DefinitionCompiler_1.compileDefinitionNode)(node, this.templateMeta, TemplateParser_1.transformTemplateNodeToSchema);
184
212
  }
185
- else if (namespace === "@stxt.schema") {
186
- schema = this.compile(node, this.schemaMeta, SchemaParser_1.transformNodeToSchema);
213
+ else if (namespace === Schema_1.Schema.SCHEMA_NAMESPACE) {
214
+ schema = (0, DefinitionCompiler_1.compileDefinitionNode)(node, this.schemaMeta, SchemaParser_1.transformNodeToSchema);
187
215
  }
188
216
  else {
189
217
  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}`));
@@ -217,15 +245,6 @@ class DiscoveryResolver {
217
245
  };
218
246
  level.definitions.set(key, definition);
219
247
  }
220
- // Validates a root node against a meta-schema and transforms it into a Schema,
221
- // throwing the first validation error (same policy as UnifiedSchemaProvider).
222
- compile(node, meta, transform) {
223
- const errors = new SchemaValidator_1.SchemaValidator(meta, true).validate(node);
224
- if (errors.length > 0) {
225
- throw errors[0];
226
- }
227
- return transform(node);
228
- }
229
248
  }
230
249
  exports.DiscoveryResolver = DiscoveryResolver;
231
250
  //# sourceMappingURL=DiscoveryResolver.js.map
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DiscoveryResult = void 0;
4
4
  const StringUtils_1 = require("../core/StringUtils");
5
+ const Schema_1 = require("../schema/Schema");
5
6
  /**
6
7
  * The outcome of resolving a document's definitions (STXT-DISCOVERY-SPEC): the chain of
7
8
  * levels, the active definition per namespace (nearest level wins) and every resolution
@@ -34,11 +35,12 @@ class DiscoveryResult {
34
35
  * @returns the schema of the namespace, or null if the chain has no definition for it.
35
36
  */
36
37
  getSchema(namespace) {
37
- if (namespace === "@stxt.template") {
38
- return this.templateMeta.getSchema(namespace);
38
+ const key = StringUtils_1.StringUtils.lowerCase(namespace);
39
+ if (key === Schema_1.Schema.TEMPLATE_NAMESPACE) {
40
+ return this.templateMeta.getSchema(key);
39
41
  }
40
- else if (namespace === "@stxt.schema") {
41
- return this.schemaMeta.getSchema(namespace);
42
+ else if (key === Schema_1.Schema.SCHEMA_NAMESPACE) {
43
+ return this.schemaMeta.getSchema(key);
42
44
  }
43
45
  return this.getDefinition(namespace)?.schema ?? null;
44
46
  }
@@ -3,7 +3,14 @@
3
3
  * carries an UPPERCASE code and the line of the document where it was detected.
4
4
  */
5
5
  export declare class ParseException extends Error {
6
- /** Line number of the document where the error was detected. */
6
+ /**
7
+ * Line of an error that has no single source line: it concerns the document as a whole
8
+ * (`SCHEMA_MULTIPLE_ROOTS`) or a condition with no one line to point at (`NODE_DUPLICATED`,
9
+ * `CHILD_NOT_DEFINED`). The value 0 is part of the conformance surface (the kit asserts it);
10
+ * it is not `Node.NO_LINE` (-1), which marks nodes built programmatically, never errors.
11
+ */
12
+ static readonly NO_LINE = 0;
13
+ /** Line number of the document where the error was detected, or {@link ParseException.NO_LINE}. */
7
14
  readonly line: number;
8
15
  /** Error code in UPPERCASE (e.g. `INVALID_LINE`). */
9
16
  readonly code: string;
@@ -32,4 +32,11 @@ class ParseException extends Error {
32
32
  }
33
33
  }
34
34
  exports.ParseException = ParseException;
35
+ /**
36
+ * Line of an error that has no single source line: it concerns the document as a whole
37
+ * (`SCHEMA_MULTIPLE_ROOTS`) or a condition with no one line to point at (`NODE_DUPLICATED`,
38
+ * `CHILD_NOT_DEFINED`). The value 0 is part of the conformance surface (the kit asserts it);
39
+ * it is not `Node.NO_LINE` (-1), which marks nodes built programmatically, never errors.
40
+ */
41
+ ParseException.NO_LINE = 0;
35
42
  //# sourceMappingURL=ParseException.js.map
@@ -33,10 +33,10 @@ export interface FormatResult {
33
33
  * - A **text line of a block** gets the indentation of the block (its level plus one) in the
34
34
  * requested style, followed by its content; any indentation the line had beyond the block's
35
35
  * is content (STXT-SPEC §10.2, relative indentation is preserved) and is kept exactly. A
36
- * blank line of the block is `""` in the content whatever it looks like in the source
37
- * (STXT-SPEC §10.3), so it is written with the indentation of the block too: the block reads
38
- * as one piece and, at the end of the file, the line is not lost — an empty last line would
39
- * be indistinguishable from the final line ending.
36
+ * blank line that precedes more block text is `""` in the content whatever it looks like in
37
+ * the source (STXT-SPEC §10.3), so it is written with the indentation of the block too: the
38
+ * block reads as one piece. The final blank lines of a block are not content (STXT-SPEC
39
+ * §10.3: the parser drops them when the block closes) and fall under the next rule.
40
40
  * - Every **other line** — a comment, a blank line outside a block, or a line the parse tree
41
41
  * does not describe because of a syntax error — is kept as the author wrote it, except that
42
42
  * its trailing blanks are removed and the whole indentation units at its start are converted
@@ -23,10 +23,10 @@ const NodeWriter_1 = require("./NodeWriter");
23
23
  * - A **text line of a block** gets the indentation of the block (its level plus one) in the
24
24
  * requested style, followed by its content; any indentation the line had beyond the block's
25
25
  * is content (STXT-SPEC §10.2, relative indentation is preserved) and is kept exactly. A
26
- * blank line of the block is `""` in the content whatever it looks like in the source
27
- * (STXT-SPEC §10.3), so it is written with the indentation of the block too: the block reads
28
- * as one piece and, at the end of the file, the line is not lost — an empty last line would
29
- * be indistinguishable from the final line ending.
26
+ * blank line that precedes more block text is `""` in the content whatever it looks like in
27
+ * the source (STXT-SPEC §10.3), so it is written with the indentation of the block too: the
28
+ * block reads as one piece. The final blank lines of a block are not content (STXT-SPEC
29
+ * §10.3: the parser drops them when the block closes) and fall under the next rule.
30
30
  * - Every **other line** — a comment, a blank line outside a block, or a line the parse tree
31
31
  * does not describe because of a syntax error — is kept as the author wrote it, except that
32
32
  * its trailing blanks are removed and the whole indentation units at its start are converted
@@ -85,8 +85,11 @@ class Formatter {
85
85
  if (node) {
86
86
  return Formatter.renderNode(node, line, style);
87
87
  }
88
+ // A final empty line of a block is not content (STXT-SPEC §10.3): the parser removed it
89
+ // from the node when the block closed, so its index falls beyond the logical lines. It
90
+ // is kept as any other line: blank, unindented.
88
91
  const text = sourceLines.textAt(lineNumber);
89
- if (text) {
92
+ if (text && text.index < text.node.getTextLines().length) {
90
93
  return Formatter.indent(text.node.getLevel() + 1, style) + text.line.content;
91
94
  }
92
95
  return Formatter.convertUnits(StringUtils_1.StringUtils.rightTrim(line), style);
@@ -172,7 +175,10 @@ class SourceLines {
172
175
  // Comment lines need no bookkeeping: every line that opens no node is treated alike.
173
176
  }
174
177
  onTextLine(node, lineNumber, lineString, line) {
175
- this.textByLine.set(lineNumber, { node, line });
178
+ // The line was just appended: its 0-based index in the block is the current last. After
179
+ // the block closes and drops its final empty lines (STXT-SPEC §10.3), an index beyond
180
+ // getTextLines() marks the line as a final empty line, not content.
181
+ this.textByLine.set(lineNumber, { node, line, index: node.getTextLines().length - 1 });
176
182
  }
177
183
  /**
178
184
  * @param lineNumber line number, 1-indexed.
@@ -183,8 +189,9 @@ class SourceLines {
183
189
  }
184
190
  /**
185
191
  * @param lineNumber line number, 1-indexed.
186
- * @returns the block node this line is text of and the line already split into indentation
187
- * and content, or undefined if the line is not text of a block.
192
+ * @returns the block node this line is text of, the line already split into indentation and
193
+ * content, and its 0-based index in the block; or undefined if the line is not text
194
+ * of a block.
188
195
  */
189
196
  textAt(lineNumber) {
190
197
  return this.textByLine.get(lineNumber);
@@ -59,9 +59,17 @@ class NodeWriter {
59
59
  }
60
60
  if (n instanceof TextNode_1.TextNode) {
61
61
  out.push(" >>\n");
62
- for (const line of n.getTextLines()) {
62
+ // Final empty lines are not emitted (STXT-TREE-SPEC 11.1 rule 6): parsing never
63
+ // produces them (STXT-SPEC 10.3), and on a programmatically built node they would
64
+ // not survive the round trip.
65
+ const lines = n.getTextLines();
66
+ let last = lines.length;
67
+ while (last > 0 && lines[last - 1] === "") {
68
+ last--;
69
+ }
70
+ for (let i = 0; i < last; i++) {
63
71
  NodeWriter.indent(out, depth + 1, style);
64
- out.push(line, "\n");
72
+ out.push(lines[i], "\n");
65
73
  }
66
74
  }
67
75
  else if (n instanceof InlineNode_1.InlineNode) {
@@ -30,9 +30,7 @@ export declare class UnifiedSchemaProvider implements SchemaProvider {
30
30
  * a schema or a template does not validate against its meta-schema.
31
31
  */
32
32
  addFile(text: string): void;
33
- private addTemplateNode;
34
- private addSchemaNode;
35
- private static throwIfInvalid;
33
+ private addNode;
36
34
  /** Removes every schema and template registered in this provider. */
37
35
  clear(): void;
38
36
  /** @returns every schema registered in this provider, in registration order. */
@@ -3,9 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UnifiedSchemaProvider = void 0;
4
4
  const Parser_1 = require("../core/Parser");
5
5
  const StringUtils_1 = require("../core/StringUtils");
6
+ const Schema_1 = require("../schema/Schema");
7
+ const DefinitionCompiler_1 = require("../schema/DefinitionCompiler");
6
8
  const SchemaProviderMeta_1 = require("../schema/SchemaProviderMeta");
7
9
  const SchemaParser_1 = require("../schema/SchemaParser");
8
- const SchemaValidator_1 = require("../schema/SchemaValidator");
9
10
  const MetaTemplateSchemaProvider_1 = require("../template/MetaTemplateSchemaProvider");
10
11
  const TemplateParser_1 = require("../template/TemplateParser");
11
12
  /**
@@ -31,14 +32,13 @@ class UnifiedSchemaProvider {
31
32
  */
32
33
  getSchema(namespace) {
33
34
  const key = StringUtils_1.StringUtils.lowerCase(namespace);
34
- if (namespace === "@stxt.template") {
35
+ if (key === Schema_1.Schema.TEMPLATE_NAMESPACE) {
35
36
  return this.templateMeta.getSchema(key);
36
37
  }
37
- else if (namespace === "@stxt.schema") {
38
+ else if (key === Schema_1.Schema.SCHEMA_NAMESPACE) {
38
39
  return this.schemaMeta.getSchema(key);
39
40
  }
40
- let result = this.schemas.get(key);
41
- return result;
41
+ return this.schemas.get(key);
42
42
  }
43
43
  /**
44
44
  * Parses a document and registers every schema or template it defines, each one under its own
@@ -53,37 +53,19 @@ class UnifiedSchemaProvider {
53
53
  const nodes = parser.parse(text);
54
54
  for (const node of nodes) {
55
55
  const namespace = node.getNamespace();
56
- if (namespace === "@stxt.template") {
57
- this.addTemplateNode(node);
56
+ if (namespace === Schema_1.Schema.TEMPLATE_NAMESPACE) {
57
+ this.addNode(node, this.templateMeta, TemplateParser_1.transformTemplateNodeToSchema);
58
58
  }
59
- else if (namespace === "@stxt.schema") {
60
- this.addSchemaNode(node);
59
+ else if (namespace === Schema_1.Schema.SCHEMA_NAMESPACE) {
60
+ this.addNode(node, this.schemaMeta, SchemaParser_1.transformNodeToSchema);
61
61
  }
62
62
  }
63
63
  }
64
- addTemplateNode(node) {
65
- // Validate against the meta-schema of templates
66
- const schemaValidator = new SchemaValidator_1.SchemaValidator(this.templateMeta, true);
67
- UnifiedSchemaProvider.throwIfInvalid(schemaValidator.validate(node));
68
- // Transform the template into a schema
69
- const schema = (0, TemplateParser_1.transformTemplateNodeToSchema)(node);
70
- const key = StringUtils_1.StringUtils.lowerCase(schema.getNamespace());
71
- this.schemas.set(key, schema);
72
- }
73
- addSchemaNode(node) {
74
- // Validate against the meta-schema of schemas
75
- const schemaValidator = new SchemaValidator_1.SchemaValidator(this.schemaMeta, true);
76
- UnifiedSchemaProvider.throwIfInvalid(schemaValidator.validate(node));
77
- // Transform the node into a schema
78
- const schema = (0, SchemaParser_1.transformNodeToSchema)(node);
79
- const key = StringUtils_1.StringUtils.lowerCase(schema.getNamespace());
80
- this.schemas.set(key, schema);
81
- }
82
- // A schema/template that does not validate against its meta-schema must not be loaded
83
- static throwIfInvalid(errors) {
84
- if (errors.length > 0) {
85
- throw errors[0];
86
- }
64
+ // Compiles a definition root through the shared pipeline (see DefinitionCompiler)
65
+ // and registers it; a definition that does not validate is never registered.
66
+ addNode(node, meta, transform) {
67
+ const schema = (0, DefinitionCompiler_1.compileDefinitionNode)(node, meta, transform);
68
+ this.schemas.set(StringUtils_1.StringUtils.lowerCase(schema.getNamespace()), schema);
87
69
  }
88
70
  /** Removes every schema and template registered in this provider. */
89
71
  clear() {