@cube-drone/marquee-parser 0.7.2 → 0.9.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/dist/blocks.js CHANGED
@@ -46,6 +46,53 @@ function mark(T, cur, node, startLine, endLine) {
46
46
  }
47
47
  return node;
48
48
  }
49
+ /** mark() for a node that ends mid-line: a leaf directive that shares its
50
+ * line with what follows its closer. */
51
+ function markTo(T, cur, node, startLine, endOffset) {
52
+ if (T !== null && cur.starts !== null) {
53
+ T.spans.set(node, { start: lineStartOf(cur, startLine), end: endOffset });
54
+ }
55
+ return node;
56
+ }
57
+ /** The leaf closer on a directive open line: the first `:::` after the
58
+ * name that sits outside a quoted value and is followed by end-of-line or
59
+ * whitespace (spec: Directives). `closerEnd` is the index just past it;
60
+ * `restStart` is where non-blank text after it begins, or null. Null for a
61
+ * close line or a line with no delimited closer (a container open). The
62
+ * cut is lexical - it happens before the head is judged well-formed - so
63
+ * the words after a closer are never lost to a bad attribute. */
64
+ function leafCut(line) {
65
+ const rest = line.slice(3);
66
+ if (rest === "" || rest.startsWith(" ") || rest.startsWith("\t")) {
67
+ return null; // a close line
68
+ }
69
+ let quoted = false;
70
+ for (let i = 3; i < line.length; i += 1) {
71
+ const c = line[i];
72
+ if (quoted) {
73
+ if (c === "\\") {
74
+ i += 1;
75
+ }
76
+ else if (c === '"') {
77
+ quoted = false;
78
+ }
79
+ continue;
80
+ }
81
+ if (c === '"') {
82
+ quoted = true;
83
+ continue;
84
+ }
85
+ if (c === ":" && line.startsWith(":::", i)) {
86
+ const after = line[i + 3];
87
+ if (after === undefined || after === " " || after === "\t") {
88
+ const closerEnd = i + 3;
89
+ const trimmed = line.slice(closerEnd).replace(/^[ \t]+/, "");
90
+ return { closerEnd, restStart: trimmed === "" ? null : line.length - trimmed.length };
91
+ }
92
+ }
93
+ }
94
+ return null;
95
+ }
49
96
  /** Build the logical inline text (segments joined by real source newlines)
50
97
  * plus the code-point -> source-offset map the inline parser records from. */
51
98
  function inlineCtx(T, segs) {
@@ -90,7 +137,14 @@ function parseContainer(cur, ctx, openDir, T) {
90
137
  if (line.startsWith(":::")) {
91
138
  const li = cur.pos;
92
139
  cur.pos += 1;
93
- const dir = classifyDirective(line);
140
+ // A leaf's closer ends the directive line; whatever follows it is
141
+ // parsed as if it began on the next line (spec: Directives). The
142
+ // head is judged on its own, and the remainder is re-injected below.
143
+ const cut = leafCut(line);
144
+ const head = cut === null ? line : line.slice(0, cut.closerEnd);
145
+ const headEnd = () => cut === null ? lineEndOf(cur, li) : lineStartOf(cur, li) + cut.closerEnd;
146
+ const one = (node) => T !== null && cur.starts !== null ? markTo(T, cur, node, li, headEnd()) : node;
147
+ const dir = classifyDirective(head);
94
148
  if (dir.kind === "close") {
95
149
  if (openDir !== null && dir.name === null) {
96
150
  return out;
@@ -98,29 +152,41 @@ function parseContainer(cur, ctx, openDir, T) {
98
152
  if (openDir !== null && dir.name === openDir) {
99
153
  return out;
100
154
  }
101
- out.push(mark(T, cur, invalid(openDir !== null ? "mismatched_close" : "stray_close"), li, li));
155
+ out.push(one(invalid(openDir !== null ? "mismatched_close" : "stray_close")));
102
156
  }
103
157
  else if (dir.kind === "open") {
104
158
  if (ctx.dirDepth >= MAX_DIRECTIVE_DEPTH) {
105
- out.push(mark(T, cur, invalid("depth_exceeded"), li, li));
106
- continue;
107
- }
108
- const attrs = parseAttrs(dir.attrsSrc);
109
- if (!attrs.ok) {
110
- out.push(mark(T, cur, invalid(attrs.reason), li, li));
159
+ out.push(one(invalid("depth_exceeded")));
111
160
  }
112
161
  else {
113
- const children = dir.leaf
114
- ? []
115
- : parseContainer(cur, { ...ctx, dirDepth: ctx.dirDepth + 1 }, dir.name, T);
116
- // A container's close line (or EOF auto-close) has been consumed
117
- // by the child parse: the span runs to the last consumed line.
118
- const endLi = dir.leaf ? li : Math.max(li, cur.pos - 1);
119
- out.push(mark(T, cur, { type: "directive", name: dir.name, attrs: attrs.attrs, children }, li, endLi));
162
+ const attrs = parseAttrs(dir.attrsSrc);
163
+ if (!attrs.ok) {
164
+ out.push(one(invalid(attrs.reason)));
165
+ }
166
+ else if (dir.leaf) {
167
+ out.push(one({ type: "directive", name: dir.name, attrs: attrs.attrs, children: [] }));
168
+ }
169
+ else {
170
+ const children = parseContainer(cur, { ...ctx, dirDepth: ctx.dirDepth + 1 }, dir.name, T);
171
+ // A container's close line (or EOF auto-close) has been consumed
172
+ // by the child parse: the span runs to the last consumed line.
173
+ const endLi = Math.max(li, cur.pos - 1);
174
+ out.push(mark(T, cur, { type: "directive", name: dir.name, attrs: attrs.attrs, children }, li, endLi));
175
+ }
120
176
  }
121
177
  }
122
178
  else {
123
- out.push(mark(T, cur, invalid(dir.reason), li, li));
179
+ out.push(one(invalid(dir.reason)));
180
+ }
181
+ if (cut !== null && cut.restStart !== null) {
182
+ // The words after the closer become the current line, and the
183
+ // cursor steps back onto it: a leaf never recurses, so nothing has
184
+ // advanced past li. Positions follow the cut.
185
+ cur.lines[li] = line.slice(cut.restStart);
186
+ if (cur.starts !== null) {
187
+ cur.starts[li] = cur.starts[li] + cut.restStart;
188
+ }
189
+ cur.pos = li;
124
190
  }
125
191
  continue;
126
192
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cube-drone/marquee-parser",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
4
4
  "description": "Reference parser for the Marquee markup language",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {
package/src/blocks.ts CHANGED
@@ -80,6 +80,54 @@ function mark(T: PosTracker | null, cur: Cursor, node: Node, startLine: number,
80
80
  return node;
81
81
  }
82
82
 
83
+ /** mark() for a node that ends mid-line: a leaf directive that shares its
84
+ * line with what follows its closer. */
85
+ function markTo(T: PosTracker | null, cur: Cursor, node: Node, startLine: number, endOffset: number): Node {
86
+ if (T !== null && cur.starts !== null) {
87
+ T.spans.set(node, { start: lineStartOf(cur, startLine), end: endOffset });
88
+ }
89
+ return node;
90
+ }
91
+
92
+ /** The leaf closer on a directive open line: the first `:::` after the
93
+ * name that sits outside a quoted value and is followed by end-of-line or
94
+ * whitespace (spec: Directives). `closerEnd` is the index just past it;
95
+ * `restStart` is where non-blank text after it begins, or null. Null for a
96
+ * close line or a line with no delimited closer (a container open). The
97
+ * cut is lexical - it happens before the head is judged well-formed - so
98
+ * the words after a closer are never lost to a bad attribute. */
99
+ function leafCut(line: string): { closerEnd: number; restStart: number | null } | null {
100
+ const rest = line.slice(3);
101
+ if (rest === "" || rest.startsWith(" ") || rest.startsWith("\t")) {
102
+ return null; // a close line
103
+ }
104
+ let quoted = false;
105
+ for (let i = 3; i < line.length; i += 1) {
106
+ const c = line[i]!;
107
+ if (quoted) {
108
+ if (c === "\\") {
109
+ i += 1;
110
+ } else if (c === '"') {
111
+ quoted = false;
112
+ }
113
+ continue;
114
+ }
115
+ if (c === '"') {
116
+ quoted = true;
117
+ continue;
118
+ }
119
+ if (c === ":" && line.startsWith(":::", i)) {
120
+ const after = line[i + 3];
121
+ if (after === undefined || after === " " || after === "\t") {
122
+ const closerEnd = i + 3;
123
+ const trimmed = line.slice(closerEnd).replace(/^[ \t]+/, "");
124
+ return { closerEnd, restStart: trimmed === "" ? null : line.length - trimmed.length };
125
+ }
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+
83
131
  /** Build the logical inline text (segments joined by real source newlines)
84
132
  * plus the code-point -> source-offset map the inline parser records from. */
85
133
  function inlineCtx(
@@ -129,7 +177,16 @@ function parseContainer(cur: Cursor, ctx: Ctx, openDir: string | null, T: PosTra
129
177
  if (line.startsWith(":::")) {
130
178
  const li = cur.pos;
131
179
  cur.pos += 1;
132
- const dir = classifyDirective(line);
180
+ // A leaf's closer ends the directive line; whatever follows it is
181
+ // parsed as if it began on the next line (spec: Directives). The
182
+ // head is judged on its own, and the remainder is re-injected below.
183
+ const cut = leafCut(line);
184
+ const head = cut === null ? line : line.slice(0, cut.closerEnd);
185
+ const headEnd = (): number =>
186
+ cut === null ? lineEndOf(cur, li) : lineStartOf(cur, li) + cut.closerEnd;
187
+ const one = (node: Node): Node =>
188
+ T !== null && cur.starts !== null ? markTo(T, cur, node, li, headEnd()) : node;
189
+ const dir = classifyDirective(head);
133
190
  if (dir.kind === "close") {
134
191
  if (openDir !== null && dir.name === null) {
135
192
  return out;
@@ -137,28 +194,38 @@ function parseContainer(cur: Cursor, ctx: Ctx, openDir: string | null, T: PosTra
137
194
  if (openDir !== null && dir.name === openDir) {
138
195
  return out;
139
196
  }
140
- out.push(mark(T, cur, invalid(openDir !== null ? "mismatched_close" : "stray_close"), li, li));
197
+ out.push(one(invalid(openDir !== null ? "mismatched_close" : "stray_close")));
141
198
  } else if (dir.kind === "open") {
142
199
  if (ctx.dirDepth >= MAX_DIRECTIVE_DEPTH) {
143
- out.push(mark(T, cur, invalid("depth_exceeded"), li, li));
144
- continue;
145
- }
146
- const attrs = parseAttrs(dir.attrsSrc);
147
- if (!attrs.ok) {
148
- out.push(mark(T, cur, invalid(attrs.reason), li, li));
200
+ out.push(one(invalid("depth_exceeded")));
149
201
  } else {
150
- const children = dir.leaf
151
- ? []
152
- : parseContainer(cur, { ...ctx, dirDepth: ctx.dirDepth + 1 }, dir.name, T);
153
- // A container's close line (or EOF auto-close) has been consumed
154
- // by the child parse: the span runs to the last consumed line.
155
- const endLi = dir.leaf ? li : Math.max(li, cur.pos - 1);
156
- out.push(
157
- mark(T, cur, { type: "directive", name: dir.name, attrs: attrs.attrs, children }, li, endLi),
158
- );
202
+ const attrs = parseAttrs(dir.attrsSrc);
203
+ if (!attrs.ok) {
204
+ out.push(one(invalid(attrs.reason)));
205
+ } else if (dir.leaf) {
206
+ out.push(one({ type: "directive", name: dir.name, attrs: attrs.attrs, children: [] }));
207
+ } else {
208
+ const children = parseContainer(cur, { ...ctx, dirDepth: ctx.dirDepth + 1 }, dir.name, T);
209
+ // A container's close line (or EOF auto-close) has been consumed
210
+ // by the child parse: the span runs to the last consumed line.
211
+ const endLi = Math.max(li, cur.pos - 1);
212
+ out.push(
213
+ mark(T, cur, { type: "directive", name: dir.name, attrs: attrs.attrs, children }, li, endLi),
214
+ );
215
+ }
159
216
  }
160
217
  } else {
161
- out.push(mark(T, cur, invalid(dir.reason), li, li));
218
+ out.push(one(invalid(dir.reason)));
219
+ }
220
+ if (cut !== null && cut.restStart !== null) {
221
+ // The words after the closer become the current line, and the
222
+ // cursor steps back onto it: a leaf never recurses, so nothing has
223
+ // advanced past li. Positions follow the cut.
224
+ cur.lines[li] = line.slice(cut.restStart);
225
+ if (cur.starts !== null) {
226
+ cur.starts[li] = cur.starts[li]! + cut.restStart;
227
+ }
228
+ cur.pos = li;
162
229
  }
163
230
  continue;
164
231
  }