@barefootjs/jsx 0.3.0 → 0.5.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.
@@ -7,7 +7,12 @@ import ts from 'typescript'
7
7
  import type { AttrValue, IRTemplatePart, LoopParamBinding, FreeReference, IRNode } from '../types'
8
8
  import type { TopLevelLoop, BranchLoop } from './types'
9
9
  import { buildLoopChainExpr } from '../loop-chain'
10
- import { replaceInExprContexts } from '../scanner/js-scanner'
10
+ import {
11
+ iterateJsTokens,
12
+ isIdentifierLikeToken,
13
+ isTriviaKind,
14
+ replaceInExprContexts,
15
+ } from '../scanner/js-scanner'
11
16
  import {
12
17
  BF_KEY as DATA_KEY,
13
18
  BF_KEY_PREFIX as DATA_KEY_PREFIX,
@@ -376,124 +381,33 @@ export function tokenContainsIdent(expr: string, ident: string): boolean {
376
381
  return scanForIdentifiers(expr, (token) => token === ident)
377
382
  }
378
383
 
379
- const IDENT_START_RE = /[A-Za-z_$]/
380
- const IDENT_PART_RE = /[A-Za-z0-9_$]/
381
-
382
384
  /**
383
- * Single-pass scanner over a JS-like expression string. Walks character by
384
- * character through a small state machine and invokes `predicate` on every
385
- * identifier-like token it finds in a position where bare identifiers are
386
- * semantically possible (i.e. not inside a string/comment, not the property
387
- * name in a member-access expression). Returns true on the first hit.
385
+ * Walk a JS-like expression string via the shared `ts.createScanner`-based
386
+ * lexer and invoke `predicate` on every identifier-like token found in a
387
+ * position where bare identifiers are semantically possible i.e. not
388
+ * inside a string / template-string body / comment / regex literal, and
389
+ * not the property name of a member-access expression. Returns true on the
390
+ * first hit.
391
+ *
392
+ * Delegating to `iterateJsTokens` (rather than a hand-rolled char-by-char
393
+ * state machine) means regex literals are recognised: `/it's/.test(foo)`
394
+ * no longer reads the apostrophe as a string opener, and an identifier
395
+ * inside a regex body (`/className/`) is correctly treated as opaque (#1370).
388
396
  */
389
397
  function scanForIdentifiers(expr: string, predicate: (token: string) => boolean): boolean {
390
- const n = expr.length
391
- let i = 0
392
- // 0 = code, 1 = single-quote string, 2 = double-quote string,
393
- // 3 = template literal text, 4 = template literal expression,
394
- // 5 = line comment, 6 = block comment.
395
- type State = 0 | 1 | 2 | 3 | 4 | 5 | 6
396
- let state: State = 0
397
- // For nested template expressions: stack of brace depths at each `${` push.
398
- const tmplExprStack: number[] = []
399
- // Brace depth tracked only inside template-expression state to detect when
400
- // we close back to the surrounding template-literal text.
401
- let braceDepth = 0
402
-
403
- while (i < n) {
404
- const ch = expr[i]
405
-
406
- switch (state) {
407
- case 0: // code
408
- case 4: { // template expression — same lexing rules as code
409
- // String / template literal openers
410
- if (ch === "'") { state = 1; i++; continue }
411
- if (ch === '"') { state = 2; i++; continue }
412
- if (ch === '`') { state = 3; i++; continue }
413
- // Comment openers
414
- if (ch === '/' && i + 1 < n) {
415
- const next = expr[i + 1]
416
- if (next === '/') { state = 5; i += 2; continue }
417
- if (next === '*') { state = 6; i += 2; continue }
418
- }
419
- // Track braces only inside template-expression state, so we know when
420
- // we leave `${ ... }` back to the surrounding template text.
421
- if (state === 4) {
422
- if (ch === '{') { braceDepth++; i++; continue }
423
- if (ch === '}') {
424
- if (braceDepth === 0) {
425
- // Closing `}` of `${ ... }` — pop back to enclosing tmpl state.
426
- const restored = tmplExprStack.pop()
427
- braceDepth = restored ?? 0
428
- state = 3
429
- i++
430
- continue
431
- }
432
- braceDepth--
433
- i++
434
- continue
435
- }
436
- }
437
- // Identifier start
438
- if (IDENT_START_RE.test(ch)) {
439
- let j = i + 1
440
- while (j < n && IDENT_PART_RE.test(expr[j])) j++
441
- const token = expr.slice(i, j)
442
- // Skip member-access tail: identifier preceded by `.` (ignoring
443
- // whitespace).
444
- let prev = i - 1
445
- while (prev >= 0 && (expr[prev] === ' ' || expr[prev] === '\t' || expr[prev] === '\n' || expr[prev] === '\r')) prev--
446
- const isMemberTail = prev >= 0 && expr[prev] === '.' && (prev === 0 || expr[prev - 1] !== '.') // not `..` (spread)
447
- if (!isMemberTail && predicate(token)) return true
448
- i = j
449
- continue
450
- }
451
- i++
452
- continue
453
- }
454
- case 1: { // single-quote string
455
- if (ch === '\\' && i + 1 < n) { i += 2; continue }
456
- if (ch === "'") { state = 0; i++; continue }
457
- i++
458
- continue
459
- }
460
- case 2: { // double-quote string
461
- if (ch === '\\' && i + 1 < n) { i += 2; continue }
462
- if (ch === '"') { state = 0; i++; continue }
463
- i++
464
- continue
465
- }
466
- case 3: { // template literal text
467
- if (ch === '\\' && i + 1 < n) { i += 2; continue }
468
- if (ch === '`') {
469
- // Closing the template literal; return to whatever code state we
470
- // came from (either top-level code or an outer template expression).
471
- state = tmplExprStack.length > 0 ? 4 : 0
472
- i++
473
- continue
474
- }
475
- if (ch === '$' && i + 1 < n && expr[i + 1] === '{') {
476
- // Entering `${ ... }`: save current outer brace depth, reset for new.
477
- tmplExprStack.push(braceDepth)
478
- braceDepth = 0
479
- state = 4
480
- i += 2
481
- continue
482
- }
483
- i++
484
- continue
485
- }
486
- case 5: { // line comment
487
- if (ch === '\n' || ch === '\r') { state = 0; i++; continue }
488
- i++
489
- continue
490
- }
491
- case 6: { // block comment
492
- if (ch === '*' && i + 1 < n && expr[i + 1] === '/') { state = 0; i += 2; continue }
493
- i++
494
- continue
495
- }
398
+ // Previous *significant* (non-trivia) token kind, used to skip the tail
399
+ // of a member access (`a.foo`, `a?.foo`) while still treating the head
400
+ // (`foo.bar`) and spread targets (`...foo`) as real references.
401
+ let prevSignificant: ts.SyntaxKind | undefined
402
+ for (const tok of iterateJsTokens(expr)) {
403
+ if (isTriviaKind(tok.kind)) continue
404
+ if (isIdentifierLikeToken(tok.kind)) {
405
+ const isMemberTail =
406
+ prevSignificant === ts.SyntaxKind.DotToken
407
+ || prevSignificant === ts.SyntaxKind.QuestionDotToken
408
+ if (!isMemberTail && predicate(expr.slice(tok.pos, tok.end))) return true
496
409
  }
410
+ prevSignificant = tok.kind
497
411
  }
498
412
  return false
499
413
  }
@@ -110,7 +110,7 @@ export function* iterateJsTokens(
110
110
  }
111
111
  }
112
112
 
113
- function isTriviaKind(kind: ts.SyntaxKind): boolean {
113
+ export function isTriviaKind(kind: ts.SyntaxKind): boolean {
114
114
  return (
115
115
  kind === ts.SyntaxKind.WhitespaceTrivia
116
116
  || kind === ts.SyntaxKind.NewLineTrivia
@@ -164,6 +164,21 @@ function canRegexStartHere(prev: ts.SyntaxKind | undefined): boolean {
164
164
  // ---------------------------------------------------------------------------
165
165
  // Token classification helpers used by the consumers below.
166
166
 
167
+ /**
168
+ * Whether `kind` is an identifier-like token — a plain identifier or any
169
+ * reserved/contextual keyword. Keywords lex to their own token kinds but
170
+ * still match the `[A-Za-z_$][\w$]*` shape the previous hand-rolled
171
+ * scanners treated as candidate identifier tokens, so consumers that want
172
+ * "every bare word in code context" (e.g. `tokenContainsIdent`) include
173
+ * them and compare the slice text themselves.
174
+ */
175
+ export function isIdentifierLikeToken(kind: ts.SyntaxKind): boolean {
176
+ return (
177
+ kind === ts.SyntaxKind.Identifier
178
+ || (kind >= ts.SyntaxKind.FirstKeyword && kind <= ts.SyntaxKind.LastKeyword)
179
+ )
180
+ }
181
+
167
182
  /** A token whose textual content is a non-code region (string body, regex, comment). */
168
183
  function isOpaqueContentKind(kind: ts.SyntaxKind): boolean {
169
184
  return (