@stxt-lang/core 0.14.1 → 0.15.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 CHANGED
@@ -5,7 +5,7 @@ Parser and schema validator for **STXT**, an indentation-based structured-text f
5
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.
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
 
@@ -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
  }
@@ -257,6 +257,13 @@ class Parser {
257
257
  closeToLevel(stack, targetLevel, result) {
258
258
  while (stack.length > targetLevel) {
259
259
  const completed = stack.pop();
260
+ // A closing block node drops its final empty lines (STXT-SPEC §10.3): they are not
261
+ // content, only visual separation or an editor's final line breaks. The validators
262
+ // and observers below already see the trimmed node; onTextLine did fire for these
263
+ // lines while the block was open, as process observation of the source.
264
+ if (completed instanceof TextNode_1.TextNode) {
265
+ completed.removeTrailingEmptyLines();
266
+ }
260
267
  // Hand it over to the validators
261
268
  this.validators.forEach(validator => {
262
269
  try {
@@ -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
  }
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stxt-lang/core",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
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",
@@ -34,7 +34,6 @@
34
34
  },
35
35
  "keywords": [
36
36
  "stxt",
37
- "semantic text",
38
37
  "parser",
39
38
  "schema",
40
39
  "validation",