partial-tsx 0.0.2 → 0.0.3

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/partial.ts +49 -9
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "partial-tsx",
3
3
  "description": "Utilities for completing prefix-growing TSX.",
4
- "version": "0.0.2",
4
+ "version": "0.0.3",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
package/src/partial.ts CHANGED
@@ -278,6 +278,21 @@ const isLikelyStatementBlock = (code: string, index: number) => {
278
278
  return /\bfunction(?:\s+[\w$]+)?\s*\([^)]*\)$/.test(before) || /\b(?:if|for|while|switch|with)\s*\([^)]*\)$/.test(before) || /\bcatch(?:\s*\([^)]*\))?$/.test(before) || /(?:^|[^\w$])(?:else|try|finally|do)$/.test(before) || before.endsWith("=>");
279
279
  };
280
280
 
281
+ // `null` when `index` isn't a comment start, `-1` when the comment is unterminated, else the index just past it.
282
+ // The three tag/expression scanners below all need this and drifted apart once already — #1600 was one of them missing it.
283
+ const findCommentEnd = (code: string, index: number) => {
284
+ if (code[index] !== "/") return null;
285
+ if (code[index + 1] === "/") {
286
+ const end = code.indexOf("\n", index + 2);
287
+ return end === -1 ? -1 : end + 1;
288
+ }
289
+ if (code[index + 1] === "*") {
290
+ const end = code.indexOf("*/", index + 2);
291
+ return end === -1 ? -1 : end + 2;
292
+ }
293
+ return null;
294
+ };
295
+
281
296
  const findJsxTagEnd = (code: string, start: number) => {
282
297
  const stack: string[] = [];
283
298
  let quote: string | null = null;
@@ -288,6 +303,20 @@ const findJsxTagEnd = (code: string, start: number) => {
288
303
  else if (char === quote) quote = null;
289
304
  continue;
290
305
  }
306
+ // A regex ending in an escaped slash (`/\//`) puts a literal `//` in the source, so it must be consumed before the
307
+ // comment scan below mistakes it for one — same ordering `findJsxAttributeExpressionEnd` uses.
308
+ if (stack.length > 0 && isLikelyRegexLiteralStart(code, index)) {
309
+ index = findRegexLiteralEnd(code, index) - 1;
310
+ continue;
311
+ }
312
+ // Comments are legal anywhere in a tag, including its own attribute list — not just inside an attribute's `{…}`.
313
+ // Skipping them keeps an apostrophe in `// framework's Link` from opening a string that swallows the rest of the tag (#1600).
314
+ const commentEnd = findCommentEnd(code, index);
315
+ if (commentEnd !== null) {
316
+ if (commentEnd === -1) return -1;
317
+ index = commentEnd - 1;
318
+ continue;
319
+ }
291
320
  if (char === '"' || char === "'" || char === "`") {
292
321
  quote = char;
293
322
  continue;
@@ -307,14 +336,9 @@ const findJsxTagEnd = (code: string, start: number) => {
307
336
  const findJsxAttributeExpressionEnd = (code: string, index: number) => {
308
337
  let depth = 1;
309
338
  while (index < code.length) {
310
- if (code[index] === "/" && code[index + 1] === "/") {
311
- const end = code.indexOf("\n", index + 2);
312
- index = end === -1 ? code.length : end;
313
- continue;
314
- }
315
- if (code[index] === "/" && code[index + 1] === "*") {
316
- const close = code.indexOf("*/", index + 2);
317
- index = close === -1 ? code.length : close + 2;
339
+ const commentEnd = findCommentEnd(code, index);
340
+ if (commentEnd !== null) {
341
+ index = commentEnd === -1 ? code.length : commentEnd;
318
342
  continue;
319
343
  }
320
344
  if (isLikelyRegexLiteralStart(code, index)) {
@@ -349,6 +373,18 @@ const completeIncompleteJsxTag = (code: string, start: number) => {
349
373
  let tag = "";
350
374
  while (index < code.length) {
351
375
  const char = code[index];
376
+ const commentEnd = findCommentEnd(code, index);
377
+ if (commentEnd !== null) {
378
+ // A still-streaming comment has to be dropped, not carried: emitting it would bury the `>` we synthesize below
379
+ // inside the comment. A closed one is kept verbatim — comments are legal in a JSX attribute list (#1600).
380
+ if (commentEnd === -1) {
381
+ index = code.length;
382
+ continue;
383
+ }
384
+ tag += code.slice(index, commentEnd);
385
+ index = commentEnd;
386
+ continue;
387
+ }
352
388
  if (char === "`") {
353
389
  // A backtick is illegal as a JSX attribute value (`title=`x`` → "JSX value should be either an expression or a quoted JSX text"); template literals are only valid inside `{...}`. Drop the attribute.
354
390
  tag = tag.replace(/\s*[\w$:-]+\s*=\s*$/, "");
@@ -396,7 +432,11 @@ const completeIncompleteJsxTag = (code: string, start: number) => {
396
432
  if (/=\s*$/.test(tag)) tag = tag.replace(/\s*[\w$:-]+\s*=\s*$/, "");
397
433
  // Otherwise a trailing name with no following space is still streaming (`valu` may become `value`); a trailing space commits it as a boolean attr.
398
434
  else if (!/\s$/.test(tag)) tag = tag.replace(/\s+[A-Za-z_$][\w$:-]*$/, "");
399
- const completedTag = VOID_TAGS.has(tagName) ? `${tag.replace(/\s*\/\s*$/, "")} />` : `${tag}>`;
435
+ // That trim can eat the newline terminating a trailing comment. The closer synthesized below must stay clear of it:
436
+ // after `//` it would be buried inside the comment, after `*/` it would fuse into a spurious self-closing `/>`.
437
+ if (/\/\/[^\n]*$|\*\/$/.test(tag)) tag += "\n";
438
+ // The `\/` strip below must not mistake a block comment's terminating slash for a stray self-closing one.
439
+ const completedTag = VOID_TAGS.has(tagName) ? `${tag.replace(/\s*(?<!\*)\/\s*$/, "")} />` : `${tag}>`;
400
440
  return { tag: completedTag, tagName };
401
441
  };
402
442