@b9g/crank 0.7.5 → 0.7.7

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/jsx-tag.d.ts CHANGED
@@ -2,3 +2,44 @@ import type { Element } from "./crank.js";
2
2
  export declare function jsx(spans: TemplateStringsArray, ...expressions: Array<unknown>): Element;
3
3
  /** Alias for `jsx` template tag. */
4
4
  export declare const html: typeof jsx;
5
+ export interface ParseElement {
6
+ type: "element";
7
+ open: ParseTag;
8
+ close: ParseTag | null;
9
+ props: Array<ParseProp | ParseValue>;
10
+ children: Array<ParseElement | ParseValue>;
11
+ }
12
+ export interface ParseValue {
13
+ type: "value";
14
+ value: any;
15
+ }
16
+ export interface ParseTag {
17
+ type: "tag";
18
+ slash: string;
19
+ value: any;
20
+ spanIndex?: number;
21
+ charIndex?: number;
22
+ }
23
+ export interface ParseProp {
24
+ type: "prop";
25
+ name: string;
26
+ value: ParseValue | ParsePropString;
27
+ }
28
+ export interface ParsePropString {
29
+ type: "propString";
30
+ parts: Array<string | ParseValue>;
31
+ }
32
+ export interface ParseError {
33
+ type: "error";
34
+ message: string;
35
+ value: any;
36
+ spanIndex?: number;
37
+ charIndex?: number;
38
+ }
39
+ export type ExpressionTarget = ParseValue | ParseTag | ParseProp | ParseError;
40
+ export interface ParseResult {
41
+ element: ParseElement;
42
+ targets: Array<ExpressionTarget | null>;
43
+ spans: ArrayLike<string>;
44
+ }
45
+ export declare function parse(spans: ArrayLike<string>): ParseResult;
package/jsx-tag.js CHANGED
@@ -7,7 +7,17 @@ function jsx(spans, ...expressions) {
7
7
  let parseResult = cache.get(key);
8
8
  if (parseResult == null) {
9
9
  parseResult = parse(spans.raw);
10
- cache.set(key, parseResult);
10
+ let hasError = false;
11
+ for (let i = 0; i < parseResult.targets.length; i++) {
12
+ const t = parseResult.targets[i];
13
+ if (t && t.type === "error") {
14
+ hasError = true;
15
+ break;
16
+ }
17
+ }
18
+ if (!hasError) {
19
+ cache.set(key, parseResult);
20
+ }
11
21
  }
12
22
  const { element, targets } = parseResult;
13
23
  for (let i = 0; i < expressions.length; i++) {
@@ -15,12 +25,15 @@ function jsx(spans, ...expressions) {
15
25
  const target = targets[i];
16
26
  if (target) {
17
27
  if (target.type === "error") {
18
- throw new SyntaxError(target.message.replace("${}", formatTagForError(exp)));
28
+ const msg = target.message.replace("${}", formatTagForError(exp));
29
+ throw new SyntaxError(target.spanIndex != null && target.charIndex != null
30
+ ? formatSyntaxError(msg, spans.raw, target.spanIndex, target.charIndex)
31
+ : msg);
19
32
  }
20
33
  target.value = exp;
21
34
  }
22
35
  }
23
- return build(element);
36
+ return build(element, parseResult.spans);
24
37
  }
25
38
  /** Alias for `jsx` template tag. */
26
39
  const html = jsx;
@@ -112,16 +125,20 @@ function parse(spans) {
112
125
  type: "tag",
113
126
  slash: closingSlash,
114
127
  value: tagName,
128
+ spanIndex: s,
129
+ charIndex: match.index,
115
130
  };
116
131
  if (!stack.length) {
117
132
  if (end !== span.length) {
118
- throw new SyntaxError(`Unmatched closing tag "${tagName}"`);
133
+ throw new SyntaxError(formatSyntaxError(`Unmatched closing tag "${tagName}"`, spans, s, match.index));
119
134
  }
120
135
  // ERROR EXPRESSION
121
136
  expressionTarget = {
122
137
  type: "error",
123
138
  message: "Unmatched closing tag ${}",
124
139
  value: null,
140
+ spanIndex: s,
141
+ charIndex: match.index,
125
142
  };
126
143
  }
127
144
  else {
@@ -140,6 +157,8 @@ function parse(spans) {
140
157
  type: "tag",
141
158
  slash: "",
142
159
  value: tagName,
160
+ spanIndex: s,
161
+ charIndex: match.index,
143
162
  },
144
163
  close: null,
145
164
  props: [],
@@ -174,7 +193,7 @@ function parse(spans) {
174
193
  if (match) {
175
194
  const [, tagEnd, spread, name, equals, string] = match;
176
195
  if (i < match.index) {
177
- throw new SyntaxError(`Unexpected text \`${span.slice(i, match.index).trim()}\``);
196
+ throw new SyntaxError(formatSyntaxError(`Unexpected text \`${span.slice(i, match.index).trim()}\``, spans, s, i));
178
197
  }
179
198
  if (tagEnd) {
180
199
  if (tagEnd[0] === "/") {
@@ -193,7 +212,7 @@ function parse(spans) {
193
212
  // SPREAD PROP EXPRESSION
194
213
  expressionTarget = value;
195
214
  if (!(expressing && end === span.length)) {
196
- throw new SyntaxError('Expression expected after "..."');
215
+ throw new SyntaxError(formatSyntaxError('Expression expected after "..."', spans, s, match.index));
197
216
  }
198
217
  }
199
218
  else if (name) {
@@ -203,14 +222,14 @@ function parse(spans) {
203
222
  value = { type: "value", value: true };
204
223
  }
205
224
  else if (end < span.length) {
206
- throw new SyntaxError(`Unexpected text \`${span.slice(end, end + 20)}\``);
225
+ throw new SyntaxError(formatSyntaxError(`Unexpected text \`${span.slice(end, end + 20)}\``, spans, s, end));
207
226
  }
208
227
  else {
209
228
  value = { type: "value", value: null };
210
229
  // PROP EXPRESSION
211
230
  expressionTarget = value;
212
231
  if (!(expressing && end === span.length)) {
213
- throw new SyntaxError(`Expression expected for prop "${name}"`);
232
+ throw new SyntaxError(formatSyntaxError(`Expression expected for prop "${name}"`, spans, s, match.index));
214
233
  }
215
234
  }
216
235
  }
@@ -236,10 +255,10 @@ function parse(spans) {
236
255
  else {
237
256
  if (!expressing) {
238
257
  if (i === span.length) {
239
- throw new SyntaxError(`Expected props but reached end of document`);
258
+ throw new SyntaxError(formatSyntaxError(`Expected props but reached end of document`, spans, s, i));
240
259
  }
241
260
  else {
242
- throw new SyntaxError(`Unexpected text \`${span.slice(i, i + 20).trim()}\``);
261
+ throw new SyntaxError(formatSyntaxError(`Unexpected text \`${span.slice(i, i + 20).trim()}\``, spans, s, i));
243
262
  }
244
263
  }
245
264
  // Unexpected expression errors are handled in the outer loop.
@@ -254,13 +273,13 @@ function parse(spans) {
254
273
  // We're in a closing tag and looking for the >.
255
274
  if (match) {
256
275
  if (i < match.index) {
257
- throw new SyntaxError(`Unexpected text \`${span.slice(i, match.index).trim()}\``);
276
+ throw new SyntaxError(formatSyntaxError(`Unexpected text \`${span.slice(i, match.index).trim()}\``, spans, s, i));
258
277
  }
259
278
  matcher = CHILDREN_RE;
260
279
  }
261
280
  else {
262
281
  if (!expressing) {
263
- throw new SyntaxError(`Unexpected text \`${span.slice(i, i + 20).trim()}\``);
282
+ throw new SyntaxError(formatSyntaxError(`Unexpected text \`${span.slice(i, i + 20).trim()}\``, spans, s, i));
264
283
  }
265
284
  }
266
285
  break;
@@ -276,7 +295,7 @@ function parse(spans) {
276
295
  }
277
296
  else {
278
297
  if (!expressing) {
279
- throw new SyntaxError(`Missing \`${matcher === CLOSING_SINGLE_QUOTE_RE ? "'" : '"'}\``);
298
+ throw new SyntaxError(formatSyntaxError(`Missing \`${matcher === CLOSING_SINGLE_QUOTE_RE ? "'" : '"'}\``, spans, s, i));
280
299
  }
281
300
  }
282
301
  break;
@@ -287,7 +306,7 @@ function parse(spans) {
287
306
  }
288
307
  else {
289
308
  if (!expressing) {
290
- throw new SyntaxError("Expected `-->` but reached end of template");
309
+ throw new SyntaxError(formatSyntaxError("Expected `-->` but reached end of template", spans, s, i));
291
310
  }
292
311
  }
293
312
  break;
@@ -321,40 +340,45 @@ function parse(spans) {
321
340
  targets.push(null);
322
341
  break;
323
342
  default:
324
- throw new SyntaxError("Unexpected expression");
343
+ throw new SyntaxError(formatSyntaxError("Unexpected expression", spans, s, spans[s].length));
325
344
  }
326
345
  }
327
346
  else if (expressionTarget) {
328
- throw new SyntaxError("Expression expected");
347
+ throw new SyntaxError(formatSyntaxError("Expression expected", spans, s, spans[s].length));
329
348
  }
330
349
  lineStart = false;
331
350
  }
332
351
  if (stack.length) {
333
352
  const ti = targets.indexOf(element.open);
334
353
  if (ti === -1) {
335
- throw new SyntaxError(`Unmatched opening tag "${element.open.value}"`);
354
+ throw new SyntaxError(formatSyntaxError(`Unmatched opening tag "${element.open.value}"`, spans, element.open.spanIndex ?? 0, element.open.charIndex ?? 0));
336
355
  }
337
356
  targets[ti] = {
338
357
  type: "error",
339
358
  message: "Unmatched opening tag ${}",
340
359
  value: null,
360
+ spanIndex: element.open.spanIndex,
361
+ charIndex: element.open.charIndex,
341
362
  };
342
363
  }
343
364
  if (element.children.length === 1 && element.children[0].type === "element") {
344
365
  element = element.children[0];
345
366
  }
346
- return { element, targets };
367
+ return { element, targets, spans };
347
368
  }
348
- function build(parsed) {
369
+ function build(parsed, spans) {
349
370
  if (parsed.close !== null &&
350
371
  parsed.close.slash !== "//" &&
351
372
  parsed.open.value !== parsed.close.value) {
352
- throw new SyntaxError(`Unmatched closing tag ${formatTagForError(parsed.close.value)}, expected ${formatTagForError(parsed.open.value)}`);
373
+ const msg = `Unmatched closing tag ${formatTagForError(parsed.close.value)}, expected ${formatTagForError(parsed.open.value)}`;
374
+ throw new SyntaxError(spans && parsed.close.spanIndex != null && parsed.close.charIndex != null
375
+ ? formatSyntaxError(msg, spans, parsed.close.spanIndex, parsed.close.charIndex)
376
+ : msg);
353
377
  }
354
378
  const children = [];
355
379
  for (let i = 0; i < parsed.children.length; i++) {
356
380
  const child = parsed.children[i];
357
- children.push(child.type === "element" ? build(child) : child.value);
381
+ children.push(child.type === "element" ? build(child, spans) : child.value);
358
382
  }
359
383
  let props = parsed.props.length ? {} : null;
360
384
  for (let i = 0; i < parsed.props.length; i++) {
@@ -425,6 +449,46 @@ function formatTagForError(tag) {
425
449
  ? `"${tag}"`
426
450
  : JSON.stringify(tag);
427
451
  }
452
+ function formatSyntaxError(message, spans, spanIndex, charIndex) {
453
+ // Reconstruct full template source with ${} placeholders
454
+ let source = spans[0];
455
+ for (let i = 1; i < spans.length; i++) {
456
+ source += "${}" + spans[i];
457
+ }
458
+ // Compute absolute offset
459
+ let offset = 0;
460
+ for (let i = 0; i < spanIndex; i++) {
461
+ offset += spans[i].length + 3; // 3 = "${}".length
462
+ }
463
+ offset += charIndex;
464
+ // Split into lines and find line/column
465
+ const lines = source.split(/\n/);
466
+ let line = 0;
467
+ let col = offset;
468
+ for (let i = 0; i < lines.length; i++) {
469
+ if (col <= lines[i].length) {
470
+ line = i;
471
+ break;
472
+ }
473
+ col -= lines[i].length + 1; // +1 for the newline
474
+ }
475
+ // Build context lines
476
+ let result = `${message}\n\n`;
477
+ const start = Math.max(0, line - 1);
478
+ const end = Math.min(lines.length - 1, line + 1);
479
+ const gutterWidth = String(end + 1).length;
480
+ for (let i = start; i <= end; i++) {
481
+ const num = String(i + 1).padStart(gutterWidth);
482
+ if (i === line) {
483
+ result += `> ${num} | ${lines[i]}\n`;
484
+ result += ` ${" ".repeat(gutterWidth)} | ${" ".repeat(col)}^\n`;
485
+ }
486
+ else {
487
+ result += ` ${num} | ${lines[i]}\n`;
488
+ }
489
+ }
490
+ return result.trimEnd();
491
+ }
428
492
 
429
- export { html, jsx };
493
+ export { html, jsx, parse };
430
494
  //# sourceMappingURL=jsx-tag.js.map