@abide/abide 0.38.1 → 0.40.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.
Files changed (80) hide show
  1. package/AGENTS.md +5 -5
  2. package/CHANGELOG.md +63 -0
  3. package/package.json +9 -1
  4. package/src/build.ts +14 -0
  5. package/src/lib/bundle/exitWithParent.ts +4 -2
  6. package/src/lib/server/rpc/parseArgs.ts +10 -1
  7. package/src/lib/server/rpc/readBodyWithinLimit.ts +3 -0
  8. package/src/lib/server/rpc/runWithVerbTimeout.ts +7 -9
  9. package/src/lib/server/rpc/types/VerbHelper.ts +18 -21
  10. package/src/lib/server/runtime/SSR_SWAP_SCRIPT.ts +8 -7
  11. package/src/lib/server/runtime/createAppAssetServer.ts +37 -11
  12. package/src/lib/server/runtime/createPublicAssetServer.ts +14 -5
  13. package/src/lib/server/runtime/createUiPageRenderer.ts +53 -42
  14. package/src/lib/server/runtime/internalErrorResponse.ts +5 -2
  15. package/src/lib/server/runtime/snapshotEntryFromCache.ts +4 -1
  16. package/src/lib/server/sockets/createSocketDispatcher.ts +19 -3
  17. package/src/lib/server/sockets/defineSocket.ts +3 -1
  18. package/src/lib/shared/REF_JSON_HEADER.ts +9 -0
  19. package/src/lib/shared/REF_JSON_TAGS.ts +31 -0
  20. package/src/lib/shared/buildRpcRequest.ts +8 -1
  21. package/src/lib/shared/createRemoteFunction.ts +20 -11
  22. package/src/lib/shared/decodeRefJson.ts +110 -0
  23. package/src/lib/shared/encodeRefJson.ts +106 -0
  24. package/src/lib/shared/escapeHtml.ts +15 -0
  25. package/src/lib/shared/markFrameworkSourcesIgnored.ts +47 -0
  26. package/src/lib/shared/streamResponse.ts +8 -1
  27. package/src/lib/shared/types/RemoteCallable.ts +12 -3
  28. package/src/lib/shared/types/RpcOptions.ts +22 -0
  29. package/src/lib/shared/types/SourceMap.ts +14 -0
  30. package/src/lib/test/createTestSocketChannel.ts +6 -2
  31. package/src/lib/ui/compile/SSR_ESCAPE.ts +13 -3
  32. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +5 -0
  33. package/src/lib/ui/compile/compileModule.ts +5 -2
  34. package/src/lib/ui/compile/compileSSR.ts +32 -9
  35. package/src/lib/ui/compile/compileShadow.ts +25 -10
  36. package/src/lib/ui/compile/composeProps.ts +53 -0
  37. package/src/lib/ui/compile/desugarSignals.ts +45 -17
  38. package/src/lib/ui/compile/generateBuild.ts +87 -76
  39. package/src/lib/ui/compile/generateSSR.ts +217 -71
  40. package/src/lib/ui/compile/isWhitespaceText.ts +11 -0
  41. package/src/lib/ui/compile/lowerDocAccess.ts +53 -16
  42. package/src/lib/ui/compile/parseTemplate.ts +118 -1
  43. package/src/lib/ui/compile/renameSignalRefs.ts +12 -0
  44. package/src/lib/ui/compile/resolveBranches.ts +21 -0
  45. package/src/lib/ui/compile/spreadExcludedNames.ts +27 -0
  46. package/src/lib/ui/compile/staticAttr.ts +1 -1
  47. package/src/lib/ui/compile/staticTextPart.ts +1 -1
  48. package/src/lib/ui/compile/types/TemplateAttr.ts +4 -1
  49. package/src/lib/ui/compile/types/TemplateNode.ts +15 -3
  50. package/src/lib/ui/createScope.ts +14 -0
  51. package/src/lib/ui/dom/appendText.ts +2 -3
  52. package/src/lib/ui/dom/appendTextAt.ts +2 -3
  53. package/src/lib/ui/dom/applyResolved.ts +5 -8
  54. package/src/lib/ui/dom/awaitBlock.ts +14 -1
  55. package/src/lib/ui/dom/mergeProps.ts +32 -0
  56. package/src/lib/ui/dom/on.ts +7 -0
  57. package/src/lib/ui/dom/parseRawNodes.ts +17 -0
  58. package/src/lib/ui/dom/readCall.ts +27 -0
  59. package/src/lib/ui/dom/restProps.ts +32 -0
  60. package/src/lib/ui/dom/spreadAttrs.ts +34 -0
  61. package/src/lib/ui/dom/spreadProps.ts +32 -0
  62. package/src/lib/ui/installHotBridge.ts +10 -0
  63. package/src/lib/ui/navigate.ts +28 -3
  64. package/src/lib/ui/remoteProxy.ts +68 -36
  65. package/src/lib/ui/renderChain.ts +39 -37
  66. package/src/lib/ui/renderToStream.ts +84 -68
  67. package/src/lib/ui/resumeSeedScript.ts +27 -0
  68. package/src/lib/ui/router.ts +81 -51
  69. package/src/lib/ui/runtime/RESUME.ts +13 -6
  70. package/src/lib/ui/runtime/createEffectNode.ts +5 -0
  71. package/src/lib/ui/runtime/localStoragePersistence.ts +21 -8
  72. package/src/lib/ui/runtime/toTeardown.ts +10 -5
  73. package/src/lib/ui/runtime/types/RenderContext.ts +8 -0
  74. package/src/lib/ui/runtime/types/SsrRender.ts +16 -11
  75. package/src/lib/ui/runtime/types/UiComponent.ts +7 -1
  76. package/src/lib/ui/socketChannel.ts +5 -2
  77. package/src/lib/ui/tryEncodeResume.ts +20 -0
  78. package/src/lib/ui/types/Scope.ts +9 -3
  79. package/src/lib/ui/compile/escapeHtml.ts +0 -15
  80. /package/src/lib/{server/runtime → shared}/safeJsonForScript.ts +0 -0
@@ -2,11 +2,15 @@ import { HOLE_ATTRIBUTE } from '../runtime/HOLE_ATTRIBUTE.ts'
2
2
  import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
3
3
  import { asOutlet } from './asOutlet.ts'
4
4
  import { bindListenEvent } from './bindListenEvent.ts'
5
+ import { composeProps } from './composeProps.ts'
5
6
  import { groupBindParts } from './groupBindParts.ts'
6
7
  import { isControlFlow } from './isControlFlow.ts'
8
+ import { isWhitespaceText } from './isWhitespaceText.ts'
7
9
  import { lowerContext } from './lowerContext.ts'
10
+ import { resolveBranches } from './resolveBranches.ts'
8
11
  import { scopeAttr } from './scopeAttr.ts'
9
12
  import { skeletonContext } from './skeletonContext.ts'
13
+ import { spreadExcludedNames } from './spreadExcludedNames.ts'
10
14
  import { staticAttr } from './staticAttr.ts'
11
15
  import { staticTextPart } from './staticTextPart.ts'
12
16
  import type { TemplateNode } from './types/TemplateNode.ts'
@@ -21,6 +25,25 @@ surface (`count` → `model.count`) and then lowered to the doc patch/read API
21
25
  `hostVar` and expects the dom bindings, `doc`, `effect`, and the component's
22
26
  `model` in scope — the body the component compiler wraps and hoists cells into.
23
27
  */
28
+
29
+ /* A JS-identifier-safe frame name from an authored construct label (an attribute name
30
+ like `aria-label`, a bound property). Non-identifier chars → `_`; a leading digit gets
31
+ an `_` prefix; empty falls back to `thunk`. Callers prefix the label (`attr_`/`bind_`),
32
+ so the result is never a bare reserved word. */
33
+ function thunkName(label: string): string {
34
+ const safe = label.replace(/[^A-Za-z0-9_$]/g, '_').replace(/^(?=\d)/, '_')
35
+ return safe === '' ? 'thunk' : safe
36
+ }
37
+
38
+ /* Names a reactive thunk so a stack frame reads `name@File.abide:line` instead of
39
+ `(anonymous)` — disambiguating which binding a frame is when several share a line. Emits
40
+ a named function expression (the only form whose name a debugger displays); the named
41
+ bodies never reference `this`/`arguments`, so the arrow→function swap is behaviour-safe,
42
+ and minify strips the name, so it costs nothing in production. */
43
+ function namedThunk(name: string, body: string): string {
44
+ return `function ${thunkName(name)}() { ${body} }`
45
+ }
46
+
24
47
  export function generateBuild(
25
48
  nodes: TemplateNode[],
26
49
  hostVar: string,
@@ -76,7 +99,7 @@ export function generateBuild(
76
99
  varName: string,
77
100
  ): string {
78
101
  if (attr.kind === 'expression') {
79
- return `attr(${varName}, ${JSON.stringify(attr.name)}, () => (${lowerExpression(attr.code)}));\n`
102
+ return `attr(${varName}, ${JSON.stringify(attr.name)}, ${namedThunk(`attr_${attr.name}`, `return (${lowerExpression(attr.code)})`)});\n`
80
103
  }
81
104
  if (attr.kind === 'event') {
82
105
  return `on(${varName}, ${JSON.stringify(attr.event)}, (${lowerExpression(attr.code)}));\n`
@@ -95,12 +118,12 @@ export function generateBuild(
95
118
  const value = lowerExpression(valueCode)
96
119
  if (isRadio) {
97
120
  return (
98
- `effect(() => { ${varName}.checked = (${lowerExpression(attr.code)}) === (${value}); });\n` +
121
+ `effect(${namedThunk('bind_group', `${varName}.checked = (${lowerExpression(attr.code)}) === (${value});`)});\n` +
99
122
  `on(${varName}, "change", () => { if (${varName}.checked) { ${lowerStatement(`${attr.code} = ${valueCode}`)} } });\n`
100
123
  )
101
124
  }
102
125
  return (
103
- `effect(() => { ${varName}.checked = (${lowerExpression(attr.code)}).includes(${value}); });\n` +
126
+ `effect(${namedThunk('bind_group', `${varName}.checked = (${lowerExpression(attr.code)}).includes(${value});`)});\n` +
104
127
  `on(${varName}, "change", () => { const $groupValue = ${value}; if (${varName}.checked) { if (!(${lowerExpression(attr.code)}).includes($groupValue)) { ${lowerStatement(`${attr.code}.push($groupValue)`)} } } else { const $groupIndex = (${lowerExpression(attr.code)}).indexOf($groupValue); if ($groupIndex !== -1) { ${lowerStatement(`delete ${attr.code}[$groupIndex]`)} } } });\n`
105
128
  )
106
129
  }
@@ -111,7 +134,7 @@ export function generateBuild(
111
134
  `.get()` and writes via `.set(v)` — see `bindRead`/`bindWrite`. */
112
135
  const event = bindListenEvent(attr.property, node.tag)
113
136
  return (
114
- `effect(() => { ${varName}.${attr.property} = ${bindRead(attr.code)}; });\n` +
137
+ `effect(${namedThunk(`bind_${attr.property}`, `${varName}.${attr.property} = ${bindRead(attr.code)};`)});\n` +
115
138
  `on(${varName}, ${JSON.stringify(event)}, () => { ${bindWrite(attr.code, `${varName}.${attr.property}`)} });\n`
116
139
  )
117
140
  }
@@ -146,7 +169,7 @@ export function generateBuild(
146
169
  return staticTextPart(part.value)
147
170
  }
148
171
  binds.push(
149
- `appendTextAt(${skVar}.an[${holeIndex(anIndex, part)}], () => (${lowerExpression(part.code)}));\n`,
172
+ `appendTextAt(${skVar}.an[${holeIndex(anIndex, part)}], ${namedThunk('text', `return (${lowerExpression(part.code)})`)});\n`,
150
173
  )
151
174
  return '<!--a-->'
152
175
  })
@@ -219,7 +242,14 @@ export function generateBuild(
219
242
  binds.push(`const ${elVar} = ${skVar}.el[${holeIndex(elIndex, node)}];\n`)
220
243
  openTag += ` ${HOLE_ATTRIBUTE}`
221
244
  for (const attr of node.attrs) {
222
- if (attr.kind !== 'static') {
245
+ if (attr.kind === 'spread') {
246
+ /* `{...expr}` onto the element: each key binds as a reactive attribute
247
+ (or an `on<event>` function as a listener) via `spreadAttrs`, skipping
248
+ any key explicitly named on the element (the explicit attr wins). */
249
+ binds.push(
250
+ `spreadAttrs(${elVar}, ${namedThunk('spread', `return (${lowerExpression(attr.code)})`)}, ${JSON.stringify(spreadExcludedNames(node.attrs))});\n`,
251
+ )
252
+ } else if (attr.kind !== 'static') {
223
253
  binds.push(dynamicAttr(node, attr, elVar))
224
254
  }
225
255
  }
@@ -287,7 +317,7 @@ export function generateBuild(
287
317
  const splitAlways = index < consumers.length - 1 ? ', true' : ''
288
318
  return part.kind === 'static'
289
319
  ? `appendStatic(${parentVar}, ${JSON.stringify(part.value)}${splitAlways});\n`
290
- : `appendText(${parentVar}, () => (${lowerExpression(part.code)})${splitAlways});\n`
320
+ : `appendText(${parentVar}, ${namedThunk('text', `return (${lowerExpression(part.code)})`)}${splitAlways});\n`
291
321
  })
292
322
  .join('')
293
323
  }
@@ -415,17 +445,17 @@ export function generateBuild(
415
445
  return `if ($props && $props.$children) { ${invoke}; } else {\n${fallback}}\n`
416
446
  }
417
447
 
418
- /* The prop + slot thunks a child mount receives its props as value thunks and
419
- its slot content as a host-taking builder (`$children`). */
420
- function componentParts(node: Extract<TemplateNode, { kind: 'component' }>): string[] {
421
- const parts = node.props.map(
422
- (prop) => `${JSON.stringify(prop.name)}: () => (${lowerExpression(prop.code)})`,
423
- )
448
+ /* The child's slot content as a host-taking builder (`$children`), or undefined when
449
+ the component has no slotted children. */
450
+ function slotPart(node: Extract<TemplateNode, { kind: 'component' }>): string | undefined {
424
451
  const slotCode = generateChildren(node.children, '$slot')
425
- if (slotCode.trim() !== '') {
426
- parts.push(`"$children": ($slot) => {\n${slotCode}}`)
427
- }
428
- return parts
452
+ return slotCode.trim() === '' ? undefined : `"$children": ($slot) => {\n${slotCode}}`
453
+ }
454
+
455
+ /* The props bag a child mount receives — composed by the shared `composeProps` so the
456
+ build and SSR back-ends emit the same last-wins layering. */
457
+ function propsArg(node: Extract<TemplateNode, { kind: 'component' }>): string {
458
+ return composeProps(node.props, lowerExpression, slotPart(node))
429
459
  }
430
460
 
431
461
  /* Mounts a child component as a marker-bounded range on `parentVar`, positioned at
@@ -439,7 +469,7 @@ export function generateBuild(
439
469
  parentVar: string,
440
470
  before: string,
441
471
  ): string {
442
- return `mountChild(${parentVar}, ${node.name}, { ${componentParts(node).join(', ')} }, ${before}, ${JSON.stringify(node.name)});\n`
472
+ return `mountChild(${parentVar}, ${node.name}, ${propsArg(node)}, ${before}, ${JSON.stringify(node.name)});\n`
443
473
  }
444
474
 
445
475
  /* An await block: pending → resolved(value) / error branches. Each branch is a
@@ -449,27 +479,25 @@ export function generateBuild(
449
479
  parentVar: string,
450
480
  before: string,
451
481
  ): string {
452
- const isBranch = (which: 'then' | 'catch' | 'finally') => (child: TemplateNode) =>
453
- child.kind === 'branch' && child.branch === which
454
- const catchBranch = node.children.find(isBranch('catch'))
455
- const finallyChildren = branchChildren(node.children.find(isBranch('finally')))
482
+ const [thenBranch, catchBranch, finallyBranch] = resolveBranches(
483
+ node,
484
+ 'then',
485
+ 'catch',
486
+ 'finally',
487
+ )
488
+ const finallyChildren = finallyBranch?.children ?? []
456
489
  /* Blocking: no pending, the children are the resolved branch bound to `node.as`.
457
490
  Streaming: pending is the non-branch children, resolved is the `then` child. */
458
491
  const pending = node.blocking
459
492
  ? []
460
493
  : node.children.filter((child) => child.kind !== 'branch')
461
- const thenBranch = node.children.find(isBranch('then'))
462
494
  const thenThunk = node.blocking
463
495
  ? branchThunk(
464
496
  node.children.filter((child) => child.kind !== 'branch'),
465
497
  node.as ?? '_value',
466
498
  finallyChildren,
467
499
  )
468
- : branchThunk(
469
- branchChildren(thenBranch),
470
- branchVar(thenBranch) ?? '_value',
471
- finallyChildren,
472
- )
500
+ : branchThunk(thenBranch?.children ?? [], thenBranch?.as ?? '_value', finallyChildren)
473
501
  /* Neither catch nor finally → pass `undefined` so awaitBlock re-throws the
474
502
  rejection (surfacing it) instead of rendering an empty branch. A finally-only
475
503
  block keeps a catch thunk that renders just finally. */
@@ -477,8 +505,8 @@ export function generateBuild(
477
505
  catchBranch === undefined && finallyChildren.length === 0
478
506
  ? 'undefined'
479
507
  : branchThunk(
480
- branchChildren(catchBranch),
481
- branchVar(catchBranch) ?? '_error',
508
+ catchBranch?.children ?? [],
509
+ catchBranch?.as ?? '_error',
482
510
  finallyChildren,
483
511
  )
484
512
  const pendingThunk = hasRenderableContent(pending) ? branchThunk(pending) : 'undefined'
@@ -490,16 +518,6 @@ export function generateBuild(
490
518
  )
491
519
  }
492
520
 
493
- /* Children of a branch node (then/catch), or [] when the branch is absent. */
494
- function branchChildren(branch: TemplateNode | undefined): TemplateNode[] {
495
- return branch !== undefined && branch.kind === 'branch' ? branch.children : []
496
- }
497
-
498
- /* The value/error variable name a branch binds, if any. */
499
- function branchVar(branch: TemplateNode | undefined): string | undefined {
500
- return branch !== undefined && branch.kind === 'branch' ? branch.as : undefined
501
- }
502
-
503
521
  /* A branch's content as a void render thunk `(parent[, value]) => void` that
504
522
  builds its children — and an optional trailing `finally` branch — into
505
523
  `parent`. The full-range model tracks the built content between markers, so a
@@ -538,17 +556,6 @@ export function generateBuild(
538
556
  )
539
557
  }
540
558
 
541
- /* The branch child of a control block matching `which` (then/catch/finally). */
542
- function findBranch(
543
- children: TemplateNode[],
544
- which: 'then' | 'catch' | 'finally',
545
- ): Extract<TemplateNode, { kind: 'branch' }> | undefined {
546
- return children.find(
547
- (child): child is Extract<TemplateNode, { kind: 'branch' }> =>
548
- child.kind === 'branch' && child.branch === which,
549
- )
550
- }
551
-
552
559
  /* A sync error boundary: build the guarded subtree (++ finally); a throw while
553
560
  building swaps to the catch branch (++ finally). No catch → `undefined`, which
554
561
  makes the runtime re-throw to the nearest enclosing boundary. */
@@ -557,18 +564,14 @@ export function generateBuild(
557
564
  parentVar: string,
558
565
  before: string,
559
566
  ): string {
560
- const catchBranch = findBranch(node.children, 'catch')
561
- const finallyChildren = branchChildren(findBranch(node.children, 'finally'))
567
+ const [catchBranch, finallyBranch] = resolveBranches(node, 'catch', 'finally')
568
+ const finallyChildren = finallyBranch?.children ?? []
562
569
  const guarded = node.children.filter((child) => child.kind !== 'branch')
563
570
  const tryThunk = branchThunk(guarded, undefined, finallyChildren)
564
571
  const catchThunk =
565
572
  catchBranch === undefined
566
573
  ? 'undefined'
567
- : branchThunk(
568
- branchChildren(catchBranch),
569
- branchVar(catchBranch) ?? '_error',
570
- finallyChildren,
571
- )
574
+ : branchThunk(catchBranch.children, catchBranch.as ?? '_error', finallyChildren)
572
575
  return `tryBlock(${parentVar}, nextBlockId(), ${tryThunk}, ${catchThunk}, ${before});\n`
573
576
  }
574
577
 
@@ -579,13 +582,33 @@ export function generateBuild(
579
582
  parentVar: string,
580
583
  before: string,
581
584
  ): string {
582
- const elseBranch = node.children.find(
585
+ /* The `case` children are the chain's `elseif`/`else` branches in source order;
586
+ the rest are the `then` content. */
587
+ const branches = node.children.filter(
583
588
  (child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
584
589
  )
585
590
  const thenChildren = node.children.filter((child) => child.kind !== 'case')
586
- const thenThunk = branchThunk(thenChildren)
587
- const elseThunk = elseBranch === undefined ? 'undefined' : branchThunk(elseBranch.children)
588
- return `when(${parentVar}, () => (${lowerExpression(node.condition)}), ${thenThunk}, ${elseThunk}, ${before});\n`
591
+ const hasElseif = branches.some((branch) => branch.condition !== undefined)
592
+ /* Fast path: a plain `if` (with optional `else`) is the binary `when` runtime. */
593
+ if (!hasElseif) {
594
+ const elseBranch = branches.find((branch) => branch.condition === undefined)
595
+ const thenThunk = branchThunk(thenChildren)
596
+ const elseThunk =
597
+ elseBranch === undefined ? 'undefined' : branchThunk(elseBranch.children)
598
+ return `when(${parentVar}, () => (${lowerExpression(node.condition)}), ${thenThunk}, ${elseThunk}, ${before});\n`
599
+ }
600
+ /* if/elseif/else is a cond-chain — reuse `switchBlock` over a constant `true`
601
+ subject with `Boolean`-coerced match thunks, so the first truthy branch wins
602
+ (`else` is the match-less default). */
603
+ const entries = [
604
+ `{ match: () => Boolean(${lowerExpression(node.condition)}), render: ${branchThunk(thenChildren)} }`,
605
+ ...branches.map((branch) =>
606
+ branch.condition !== undefined
607
+ ? `{ match: () => Boolean(${lowerExpression(branch.condition)}), render: ${branchThunk(branch.children)} }`
608
+ : `{ match: undefined, render: ${branchThunk(branch.children)} }`,
609
+ ),
610
+ ]
611
+ return `switchBlock(${parentVar}, () => true, [${entries.join(', ')}], ${before});\n`
589
612
  }
590
613
 
591
614
  /* A keyed each. Each row is a content RANGE (any content, tracked between the
@@ -607,11 +630,9 @@ export function generateBuild(
607
630
  optional `<template catch>` branch rendered (after the streamed rows) when the
608
631
  iterator rejects. Absent → `undefined`, so the rejection surfaces instead. */
609
632
  const fn = node.async ? 'eachAsync' : 'each'
610
- const catchBranch = node.children.find(
611
- (child) => child.kind === 'branch' && child.branch === 'catch',
612
- )
633
+ const [catchBranch] = resolveBranches(node, 'catch')
613
634
  const catchArg = node.async
614
- ? `, ${catchBranch === undefined ? 'undefined' : branchThunk(branchChildren(catchBranch), branchVar(catchBranch) ?? '_error')}`
635
+ ? `, ${catchBranch === undefined ? 'undefined' : branchThunk(catchBranch.children, catchBranch.as ?? '_error')}`
615
636
  : ''
616
637
  return (
617
638
  `${fn}(${parentVar}, () => (${lowerExpression(node.items)}), ` +
@@ -622,16 +643,6 @@ export function generateBuild(
622
643
  return generateChildren(rootNodes, hostVar)
623
644
  }
624
645
 
625
- /* A text node that is purely whitespace (no interpolation, only blank static
626
- parts). Both back-ends drop it, so it neither contributes markup nor breaks a
627
- static clone run — it stays transparent so `<a/>\n<b/>` still coalesces. */
628
- function isWhitespaceText(node: TemplateNode): boolean {
629
- return (
630
- node.kind === 'text' &&
631
- node.parts.every((part) => part.kind === 'static' && part.value.trim() === '')
632
- )
633
- }
634
-
635
646
  /*
636
647
  Whether an element subtree is fully static — no reactive/event/bind attributes,
637
648
  no nested `<script>`, and every descendant likewise static (static text, static