@torpor/view 0.4.13 → 0.4.14

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 (87) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/{Cleanup-BGQ5zaxE.d.cts → Cleanup-CYsytTEc.d.cts} +1 -1
  3. package/dist/Cleanup-CYsytTEc.d.cts.map +1 -0
  4. package/dist/{Cleanup-Ca8IspaG.d.ts → Cleanup-CgjyN0lW.d.mts} +1 -1
  5. package/dist/Cleanup-CgjyN0lW.d.mts.map +1 -0
  6. package/dist/{Component-BDUCwkBQ.d.cts → Component-DmWGMgak.d.mts} +1 -1
  7. package/dist/{Component-BDUCwkBQ.d.cts.map → Component-DmWGMgak.d.mts.map} +1 -1
  8. package/dist/{Component-JVwSemf-.d.ts → Component-kGLRFYg2.d.cts} +1 -1
  9. package/dist/Component-kGLRFYg2.d.cts.map +1 -0
  10. package/dist/{Region-BrsZLZUe.d.ts → Region-Bb6HAb-F.d.cts} +2 -2
  11. package/dist/{Region-Cf7OeBIQ.d.cts.map → Region-Bb6HAb-F.d.cts.map} +1 -1
  12. package/dist/{Region-Cf7OeBIQ.d.cts → Region-CTchywkr.d.mts} +2 -2
  13. package/dist/Region-CTchywkr.d.mts.map +1 -0
  14. package/dist/compile.cjs +239 -71
  15. package/dist/compile.d.cts +1 -1
  16. package/dist/{compile.d.ts → compile.d.mts} +2 -2
  17. package/dist/compile.d.mts.map +1 -0
  18. package/dist/{compile.js → compile.mjs} +239 -70
  19. package/dist/compile.mjs.map +1 -0
  20. package/dist/dev.cjs +1 -2
  21. package/dist/dev.d.cts +2 -2
  22. package/dist/{dev.d.ts → dev.d.mts} +3 -3
  23. package/dist/dev.d.mts.map +1 -0
  24. package/dist/{dev.js → dev.mjs} +2 -2
  25. package/dist/{dev.cjs.map → dev.mjs.map} +1 -1
  26. package/dist/devContext-B3yskfhA.cjs +1 -2
  27. package/dist/{devContext-CevrTlUO.js → devContext-CQ8RhBX-.mjs} +1 -1
  28. package/dist/{devContext-B3yskfhA.cjs.map → devContext-CQ8RhBX-.mjs.map} +1 -1
  29. package/dist/{formatText-Ck04q8_X.cjs → formatText--9zsK5Nq.cjs} +4 -4
  30. package/dist/{formatText-OyD8MNAM.js → formatText-B8u71S_K.mjs} +4 -3
  31. package/dist/formatText-B8u71S_K.mjs.map +1 -0
  32. package/dist/{formatText-BlLF3PPm.d.ts → formatText-BNP3P38F.d.cts} +3 -3
  33. package/dist/formatText-BNP3P38F.d.cts.map +1 -0
  34. package/dist/{formatText-Bcb1JvW5.d.cts → formatText-D6Ov8Ub0.d.mts} +3 -3
  35. package/dist/formatText-D6Ov8Ub0.d.mts.map +1 -0
  36. package/dist/index.cjs +2 -3
  37. package/dist/index.d.cts +4 -4
  38. package/dist/{index.d.ts → index.d.mts} +5 -5
  39. package/dist/index.d.mts.map +1 -0
  40. package/dist/{index.js → index.mjs} +3 -3
  41. package/dist/{index.cjs.map → index.mjs.map} +1 -1
  42. package/dist/ssr.cjs +2 -3
  43. package/dist/ssr.d.cts +2 -2
  44. package/dist/{ssr.d.ts → ssr.d.mts} +3 -3
  45. package/dist/ssr.d.mts.map +1 -0
  46. package/dist/{ssr.js → ssr.mjs} +2 -2
  47. package/dist/{ssr.cjs.map → ssr.mjs.map} +1 -1
  48. package/package.json +17 -17
  49. package/scripts/copyToRepl.ts +6 -6
  50. package/src/compile/build/client/addMappedTextWithOffsets.ts +31 -0
  51. package/src/compile/build/client/buildAddFragment.ts +11 -11
  52. package/src/compile/build/client/buildComponentNode.ts +123 -29
  53. package/src/compile/build/client/buildElementNode.ts +65 -8
  54. package/src/compile/build/client/buildIfNode.ts +1 -1
  55. package/src/compile/build/client/buildSwitchNode.ts +1 -1
  56. package/src/compile/build/client/buildTextNode.ts +13 -6
  57. package/src/compile/build/client/getAttributeOffsets.ts +90 -0
  58. package/src/compile/build/client/stashRunWithOffsets.ts +7 -1
  59. package/src/compile/build/server/buildServerElementNode.ts +1 -1
  60. package/src/compile/parse/parseTag.ts +1 -7
  61. package/src/compile/parse/parseText.ts +1 -1
  62. package/src/render/buildClasses.ts +1 -1
  63. package/src/render/buildStyles.ts +3 -1
  64. package/src/types/ClassValue.ts +6 -1
  65. package/src/types/StyleValue.ts +6 -1
  66. package/test/source-maps/interpolated.test.ts +88 -0
  67. package/test/source-maps/parse.test.ts +1 -1
  68. package/tsdown.config.ts +2 -9
  69. package/vitest.config.ts +3 -3
  70. package/dist/Cleanup-BGQ5zaxE.d.cts.map +0 -1
  71. package/dist/Cleanup-Ca8IspaG.d.ts.map +0 -1
  72. package/dist/Component-JVwSemf-.d.ts.map +0 -1
  73. package/dist/Region-BrsZLZUe.d.ts.map +0 -1
  74. package/dist/compile.cjs.map +0 -1
  75. package/dist/compile.d.ts.map +0 -1
  76. package/dist/compile.js.map +0 -1
  77. package/dist/dev.d.ts.map +0 -1
  78. package/dist/dev.js.map +0 -1
  79. package/dist/devContext-CevrTlUO.js.map +0 -1
  80. package/dist/formatText-Bcb1JvW5.d.cts.map +0 -1
  81. package/dist/formatText-BlLF3PPm.d.ts.map +0 -1
  82. package/dist/formatText-Ck04q8_X.cjs.map +0 -1
  83. package/dist/formatText-OyD8MNAM.js.map +0 -1
  84. package/dist/index.d.ts.map +0 -1
  85. package/dist/index.js.map +0 -1
  86. package/dist/ssr.d.ts.map +0 -1
  87. package/dist/ssr.js.map +0 -1
package/package.json CHANGED
@@ -1,27 +1,27 @@
1
1
  {
2
2
  "name": "@torpor/view",
3
- "version": "0.4.13",
3
+ "version": "0.4.14",
4
4
  "description": "Torpor's view library, for writing and mounting components",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
- "types": "./dist/index.d.ts",
9
- "import": "./dist/index.js",
8
+ "types": "./dist/index.d.mts",
9
+ "import": "./dist/index.mjs",
10
10
  "require": "./dist/index.cjs"
11
11
  },
12
12
  "./compile": {
13
- "types": "./dist/compile.d.ts",
14
- "import": "./dist/compile.js",
13
+ "types": "./dist/compile.d.mts",
14
+ "import": "./dist/compile.mjs",
15
15
  "require": "./dist/compile.cjs"
16
16
  },
17
17
  "./ssr": {
18
- "types": "./dist/ssr.d.ts",
19
- "import": "./dist/ssr.js",
18
+ "types": "./dist/ssr.d.mts",
19
+ "import": "./dist/ssr.mjs",
20
20
  "require": "./dist/ssr.cjs"
21
21
  },
22
22
  "./dev": {
23
- "types": "./dist/dev.d.ts",
24
- "import": "./dist/dev.js",
23
+ "types": "./dist/dev.d.mts",
24
+ "import": "./dist/dev.mjs",
25
25
  "require": "./dist/dev.cjs"
26
26
  }
27
27
  },
@@ -35,24 +35,24 @@
35
35
  "@testing-library/dom": "^10.4.1",
36
36
  "@testing-library/jest-dom": "^6.9.1",
37
37
  "@testing-library/user-event": "^14.6.1",
38
- "@trivago/prettier-plugin-sort-imports": "^5.2.2",
38
+ "@trivago/prettier-plugin-sort-imports": "^6.0.0",
39
39
  "@types/jsdom": "^27.0.0",
40
- "@types/node": "^24.10.0",
41
- "@typescript/native-preview": "7.0.0-dev.20251103.1",
40
+ "@types/node": "^24.10.1",
41
+ "@typescript/native-preview": "7.0.0-dev.20251112.1",
42
42
  "fast-glob": "^3.3.3",
43
- "jsdom": "^27.1.0",
44
- "oxlint-tsgolint": "^0.4.0",
43
+ "jsdom": "^27.2.0",
44
+ "oxlint-tsgolint": "^0.6.0",
45
45
  "sucrase": "^3.35.0",
46
46
  "tinybench": "^5.1.0",
47
- "tsdown": "^0.15.12",
47
+ "tsdown": "^0.16.4",
48
48
  "tsx": "^4.20.6",
49
49
  "typescript": "^5.9.3",
50
- "vitest": "^4.0.6"
50
+ "vitest": "^4.0.8"
51
51
  },
52
52
  "scripts": {
53
+ "check": "tsgo --noEmit && pnpm dlx oxlint --type-aware",
53
54
  "build": "tsgo --noEmit && tsdown && tsx ./scripts/copyToRepl.ts",
54
55
  "build:watch": "tsdown --watch",
55
- "check": "tsgo --noEmit && pnpm dlx oxlint --type-aware",
56
56
  "test": "vitest",
57
57
  "test:build": "tsx ./scripts/buildTestOutput.ts",
58
58
  "bench": "tsx ./bench/bench.ts",
@@ -1,7 +1,7 @@
1
1
  import { promises as fs, readFileSync } from "node:fs";
2
2
 
3
3
  async function run() {
4
- const indexFile = "./dist/index.js";
4
+ const indexFile = "./dist/index.mjs";
5
5
  let source = await fs.readFile(indexFile, "utf8");
6
6
  source = source
7
7
  .replace(/^export \{(.+)\};/gms, (_, capture) => {
@@ -19,13 +19,13 @@ async function run() {
19
19
  );
20
20
  })
21
21
  // HACK: Why are these imported?
22
- .replaceAll(/^import \{.+} from ".\/(.+?).js";/gms, (_, capture) => {
23
- const importedFile = `./dist/${capture}.js`;
22
+ .replaceAll(/^import \{.+} from ".\/(.+?).mjs";/gm, (_, capture) => {
23
+ const importedFile = `./dist/${capture}.mjs`;
24
24
  let importedSource = readFileSync(importedFile, "utf8");
25
- return importedSource.replace(/^export \{(.+)\};/gms, "");
25
+ return importedSource.replace(/^export \{(.+)\};/gm, "");
26
26
  })
27
- .replaceAll(/^import ".\/(.+?).js";/gms, "")
28
- .replaceAll(/^\/\/.+?$/gms, "")
27
+ .replaceAll(/^import ".\/(.+?).mjs";/gm, "")
28
+ .replaceAll(/^\/\/.+?$/gm, "")
29
29
  .replaceAll(/^\/\*.+?^\*\//gms, "")
30
30
  .replaceAll(/\n+/g, "\n");
31
31
 
@@ -0,0 +1,31 @@
1
+ import type SourceSpan from "../../types/SourceSpan";
2
+ import Builder from "../../utils/Builder";
3
+ import type BuildStatus from "./BuildStatus";
4
+
5
+ export default function addMappedTextWithOffsets(
6
+ preText: string,
7
+ text: string,
8
+ postText: string,
9
+ spans: SourceSpan[],
10
+ offsets: number[],
11
+ lengths: number[],
12
+ status: BuildStatus,
13
+ b: Builder,
14
+ ): void {
15
+ if (status.options.mapped === true) {
16
+ let mappedStart = b.toString().length + preText.length;
17
+ let functionBody = preText + text + postText;
18
+ b.append(functionBody);
19
+ for (let i = 0; i < spans.length; i++) {
20
+ let start = mappedStart + offsets[i];
21
+ let end = start + lengths[i];
22
+ status.map.push({
23
+ script: functionBody,
24
+ source: spans[i],
25
+ compiled: { start, end },
26
+ });
27
+ }
28
+ } else {
29
+ b.append(preText + text + postText);
30
+ }
31
+ }
@@ -3,6 +3,7 @@ import type ElementNode from "../../types/nodes/ElementNode";
3
3
  import type RootNode from "../../types/nodes/RootNode";
4
4
  import Builder from "../../utils/Builder";
5
5
  import type BuildStatus from "./BuildStatus";
6
+ import addMappedTextWithOffsets from "./addMappedTextWithOffsets";
6
7
 
7
8
  export default function buildAddFragment(
8
9
  node: RootNode | ControlNode | ElementNode,
@@ -16,21 +17,20 @@ export default function buildAddFragment(
16
17
  const fragmentName = `t_fragment_${fragment.number}`;
17
18
  status.imports.add("t_add_fragment");
18
19
  if (fragment.effects.length > 0) {
19
- // TODO: nicer mapping
20
20
  status.imports.add("$run");
21
21
  b.append("$run(() => {");
22
22
  if (status.options.mapped === true) {
23
23
  for (let effect of fragment.effects) {
24
- for (let i = 0; i < effect.spans.length; i++) {
25
- let start = b.toString().length + effect.offsets[i];
26
- b.append(effect.functionBody);
27
- let end = start + effect.lengths[i];
28
- status.map.push({
29
- script: effect.functionBody,
30
- source: effect.spans[i],
31
- compiled: { start, end },
32
- });
33
- }
24
+ addMappedTextWithOffsets(
25
+ "",
26
+ effect.functionBody,
27
+ "",
28
+ effect.spans,
29
+ effect.offsets,
30
+ effect.lengths,
31
+ status,
32
+ b,
33
+ );
34
34
  }
35
35
  } else {
36
36
  b.append(fragment.effects.map((e) => e.functionBody).join("\n"));
@@ -6,10 +6,12 @@ import trimQuotes from "../../utils/trimQuotes";
6
6
  import nextVarName from "../utils/nextVarName";
7
7
  import type BuildStatus from "./BuildStatus";
8
8
  import addMappedText from "./addMappedText";
9
+ import addMappedTextWithOffsets from "./addMappedTextWithOffsets";
9
10
  import buildAddFragment from "./buildAddFragment";
10
11
  import buildFragment from "./buildFragment";
11
12
  import buildNode from "./buildNode";
12
13
  import buildRun from "./buildRun";
14
+ import getAttributeOffsets from "./getAttributeOffsets";
13
15
  import replaceForVarNames from "./replaceForVarNames";
14
16
 
15
17
  export default function buildComponentNode(
@@ -34,44 +36,123 @@ export default function buildComponentNode(
34
36
  status.imports.add("$watch");
35
37
 
36
38
  // Gather props, runs and binding runs, and set them all at once
37
- let props: { name: string; value: string; span: SourceSpan }[] = [];
39
+ let props: {
40
+ name: string;
41
+ // Text to be ignored in the mapping e.g. `t_class(`
42
+ preText: string;
43
+ value: string;
44
+ // Text to be ignored in the mapping e.g. `)`
45
+ postText: string;
46
+ spans: SourceSpan[];
47
+ offsets: number[];
48
+ lengths: number[];
49
+ }[] = [];
38
50
  let runs: string[] = [];
39
51
  let bindingRuns: string[] = [];
40
52
 
41
53
  for (let { name, value, reactive, fullyReactive, span } of node.attributes) {
42
54
  if (name === "self" && node.tagName === "@component") {
43
55
  // Ignore this special attribute
44
- } else if (name.startsWith("&") && value != null && fullyReactive) {
45
- // It's a bound property
46
- // Add two $runs -- one to update the props, and one to update the value
47
- // The component author will need to make sure $props.x is updated
48
- // TODO: Maybe allow adding a Bindable<T> type to the $props interface??
49
- // e.g. Component($props: { text: Bindable<string> })
50
- name = name.substring(1);
51
- props.push({ name, value, span });
52
- runs.push(`${propsName}["${name}"] = ${value};`);
53
- bindingRuns.push(`${value} = ${propsName}["${name}"];`);
54
- } else if (name === "class" && value != null) {
55
- status.imports.add("t_class");
56
- const params = [value];
57
- if (node.scopeStyles) {
58
- params.push(`"torp-${status.styleHash}"`);
56
+ } else if (value != null && fullyReactive) {
57
+ if (name.startsWith("&")) {
58
+ // It's a bound property
59
+ // Add two $runs -- one to update the props, and one to update the value
60
+ // The component author will need to make sure $props.x is updated
61
+ // TODO: Maybe allow adding a Bindable<T> type to the $props interface??
62
+ // e.g. Component($props: { text: Bindable<string> })
63
+ name = name.substring(1);
64
+ props.push({
65
+ name,
66
+ preText: "",
67
+ value,
68
+ postText: "",
69
+ spans: [span],
70
+ offsets: [],
71
+ lengths: [],
72
+ });
73
+ runs.push(`${propsName}["${name}"] = ${value};`);
74
+ bindingRuns.push(`${value} = ${propsName}["${name}"];`);
75
+ } else if (name === "class") {
76
+ status.imports.add("t_class");
77
+ const params = [value];
78
+ if (node.scopeStyles) {
79
+ params.push(`"torp-${status.styleHash}"`);
80
+ }
81
+ value = params.join(", ");
82
+ props.push({
83
+ name,
84
+ preText: "t_class(",
85
+ value,
86
+ postText: ")",
87
+ spans: [span],
88
+ offsets: [],
89
+ lengths: [],
90
+ });
91
+ runs.push(`${propsName}["${name}"] = t_class(${value});`);
92
+ } else if (name === "style") {
93
+ status.imports.add("t_style");
94
+ props.push({
95
+ name,
96
+ preText: "t_style(",
97
+ value,
98
+ postText: ")",
99
+ spans: [span],
100
+ offsets: [],
101
+ lengths: [],
102
+ });
103
+ runs.push(`${propsName}["${name}"] = t_style(${value});`);
104
+ } else {
105
+ props.push({
106
+ name,
107
+ preText: "",
108
+ value,
109
+ postText: "",
110
+ spans: [span],
111
+ offsets: [],
112
+ lengths: [],
113
+ });
114
+ runs.push(`${propsName}["${name}"] = ${value};`);
59
115
  }
60
- value = `t_class(${params.join(", ")})`;
61
- props.push({ name, value, span });
62
- runs.push(`${propsName}["${name}"] = ${value};`);
63
- } else if (name === "style" && value != null) {
64
- status.imports.add("t_style");
65
- value = `t_style(${value})`;
66
- props.push({ name, value, span });
67
- runs.push(`${propsName}["${name}"] = ${value};`);
68
- } else if (value != null) {
69
- props.push({ name, value, span });
70
- if (reactive) {
116
+ } else if (value != null && reactive) {
117
+ const { newValue, spans, offsets, lengths } = getAttributeOffsets(value, span);
118
+ value = newValue;
119
+ if (name === "class") {
120
+ status.imports.add("t_class");
121
+ const params = [value];
122
+ if (node.scopeStyles) {
123
+ params.push(`"torp-${status.styleHash}"`);
124
+ }
125
+ value = params.join(", ");
126
+ props.push({ name, preText: "t_class(", value, postText: ")", spans, offsets, lengths });
127
+ runs.push(`${propsName}["${name}"] = t_class(${value});`);
128
+ } else if (name === "style") {
129
+ status.imports.add("t_style");
130
+ props.push({ name, preText: "t_style(", value, postText: ")", spans, offsets, lengths });
131
+ runs.push(`${propsName}["${name}"] = t_style(${value});`);
132
+ } else {
133
+ props.push({ name, preText: "", value, postText: "", spans, offsets, lengths });
71
134
  runs.push(`${propsName}["${name}"] = ${value};`);
72
135
  }
136
+ } else if (value != null) {
137
+ props.push({
138
+ name,
139
+ preText: "",
140
+ value,
141
+ postText: "",
142
+ spans: [span],
143
+ offsets: [],
144
+ lengths: [],
145
+ });
73
146
  } else {
74
- props.push({ name, value: "true", span });
147
+ props.push({
148
+ name,
149
+ preText: "",
150
+ value: "true",
151
+ postText: "",
152
+ spans: [span],
153
+ offsets: [],
154
+ lengths: [],
155
+ });
75
156
  }
76
157
  }
77
158
 
@@ -79,7 +160,20 @@ export default function buildComponentNode(
79
160
  b.append(`const ${propsName} = $watch({`);
80
161
  for (let p of props) {
81
162
  let value = replaceForVarNames(p.value, status);
82
- addMappedText(`${p.name}: `, value, ",", p.span, status, b);
163
+ if (p.spans.length === 1) {
164
+ addMappedText(`${p.name}: ` + p.preText, value, p.postText + ",", p.spans[0], status, b);
165
+ } else {
166
+ addMappedTextWithOffsets(
167
+ `${p.name}: ` + p.preText,
168
+ value,
169
+ p.postText + ",",
170
+ p.spans,
171
+ p.offsets,
172
+ p.lengths,
173
+ status,
174
+ b,
175
+ );
176
+ }
83
177
  }
84
178
  b.append("});");
85
179
  // TODO: Map these things:
@@ -12,8 +12,10 @@ import buildFragment from "./buildFragment";
12
12
  import buildMount from "./buildMount";
13
13
  import buildNode from "./buildNode";
14
14
  import buildRun from "./buildRun";
15
+ import getAttributeOffsets from "./getAttributeOffsets";
15
16
  import replaceForVarNames from "./replaceForVarNames";
16
17
  import stashRun from "./stashRun";
18
+ import stashRunWithOffsets from "./stashRunWithOffsets";
17
19
 
18
20
  export default function buildElementNode(
19
21
  node: ElementNode,
@@ -178,12 +180,12 @@ function buildElementAttributes(
178
180
  }
179
181
  }
180
182
 
181
- for (let { name, value, reactive, span: range } of node.attributes) {
183
+ for (let { name, value, reactive, fullyReactive, span } of node.attributes) {
182
184
  if (name === "self" && node.tagName === "@element") {
183
185
  // Ignore this special attribute
184
186
  } else if (name === "&ref") {
185
187
  // Ignore this one, it should have been done already, above
186
- } else if (value != null && reactive) {
188
+ } else if (value != null && fullyReactive) {
187
189
  if (name === "&group") {
188
190
  buildBindGroupAttribute(node, varName, value, status, b);
189
191
  } else if (name === "&value" || name === "&checked") {
@@ -194,7 +196,7 @@ function buildElementAttributes(
194
196
  // returning a cleanup function
195
197
  buildMount("elMount", `return (${trimEnd(value.trim(), ";")})(${varName});`, status, b);
196
198
  } else if (name.startsWith("on")) {
197
- buildEventAttribute(varName, name, value, range, status, b);
199
+ buildEventAttribute(varName, name, value, span, status, b);
198
200
  } else if (name.startsWith("transition")) {
199
201
  buildTransitionAttribute(node, varName, name, value, status, b);
200
202
  } else if (name === "class") {
@@ -210,29 +212,84 @@ function buildElementAttributes(
210
212
  `${status.ns ? "// @ts-ignore\n" : ""}${varName}.${propName} = t_class(`,
211
213
  params.join(", "),
212
214
  ");",
213
- range,
215
+ span,
214
216
  status,
215
217
  );
216
218
  } else if (name === "style") {
217
219
  status.imports.add("t_style");
218
- stashRun(
220
+ stashRun(fragment, `${varName}.setAttribute("style", t_style(`, value, "));", span, status);
221
+ } else if (name.includes("-")) {
222
+ // Handle data-, aria- etc
223
+ status.imports.add("t_attribute");
224
+ stashRun(fragment, `t_attribute(${varName}, "${name}", `, value, ");", span, status);
225
+ // NOTE: dataset seems to be a tiny bit slower?
226
+ //const propName = name.substring(name.indexOf("-"));
227
+ //buildRun("setDataAttribute", `${varName}.dataset.${propName} = ${value};`, status, b);
228
+ } else {
229
+ status.imports.add("t_attribute");
230
+ stashRun(fragment, `t_attribute(${varName}, "${name}", `, value, ");", span, status);
231
+ }
232
+ } else if (value != null && reactive) {
233
+ const { newValue, spans, offsets, lengths } = getAttributeOffsets(value, span);
234
+ value = newValue;
235
+ if (name === "class") {
236
+ status.imports.add("t_class");
237
+ const params = [value];
238
+ if (node.scopeStyles) {
239
+ params.push(`"torp-${status.styleHash}"`);
240
+ }
241
+ // SVGs have a different className
242
+ const propName = status.ns ? "className.baseVal" : "className";
243
+ stashRunWithOffsets(
244
+ fragment,
245
+ `${status.ns ? "// @ts-ignore\n" : ""}${varName}.${propName} = t_class(`,
246
+ params.join(", "),
247
+ ");",
248
+ spans,
249
+ offsets,
250
+ lengths,
251
+ status,
252
+ );
253
+ } else if (name === "style") {
254
+ status.imports.add("t_style");
255
+ stashRunWithOffsets(
219
256
  fragment,
220
257
  `${varName}.setAttribute("style", t_style(`,
221
258
  value,
222
259
  "));",
223
- range,
260
+ spans,
261
+ offsets,
262
+ lengths,
224
263
  status,
225
264
  );
226
265
  } else if (name.includes("-")) {
227
266
  // Handle data-, aria- etc
228
267
  status.imports.add("t_attribute");
229
- stashRun(fragment, `t_attribute(${varName}, "${name}", `, value, ");", range, status);
268
+ stashRunWithOffsets(
269
+ fragment,
270
+ `t_attribute(${varName}, "${name}", `,
271
+ value,
272
+ ");",
273
+ spans,
274
+ offsets,
275
+ lengths,
276
+ status,
277
+ );
230
278
  // NOTE: dataset seems to be a tiny bit slower?
231
279
  //const propName = name.substring(name.indexOf("-"));
232
280
  //buildRun("setDataAttribute", `${varName}.dataset.${propName} = ${value};`, status, b);
233
281
  } else {
234
282
  status.imports.add("t_attribute");
235
- stashRun(fragment, `t_attribute(${varName}, "${name}", `, value, ");", range, status);
283
+ stashRunWithOffsets(
284
+ fragment,
285
+ `t_attribute(${varName}, "${name}", `,
286
+ value,
287
+ ");",
288
+ spans,
289
+ offsets,
290
+ lengths,
291
+ status,
292
+ );
236
293
  }
237
294
  }
238
295
  }
@@ -48,7 +48,7 @@ export default function buildIfNode(node: ControlNode, status: BuildStatus, b: B
48
48
  b.append(`
49
49
  const ${regionName} = t_region(${status.options.dev === true ? `"if"` : ""});
50
50
  let ${indexName} = -1;
51
- t_run_control(${regionName}, ${anchorName}, (t_before) => {`);
51
+ t_run_control(${regionName}, ${anchorName}, (${status.inHead ? "" : "t_before"}) => {`);
52
52
 
53
53
  let index = 0;
54
54
  for (let branch of branches) {
@@ -46,7 +46,7 @@ export default function buildSwitchNode(node: ControlNode, status: BuildStatus,
46
46
  b.append(`
47
47
  const ${regionName} = t_region(${status.options.dev === true ? `"switch"` : ""});
48
48
  let ${indexName} = -1;
49
- t_run_control(${regionName}, ${anchorName}, (t_before) => {`);
49
+ t_run_control(${regionName}, ${anchorName}, (${status.inHead ? "" : "t_before"}) => {`);
50
50
 
51
51
  // TODO: replaceForVarNames is going to throw mapping out
52
52
  addMappedText("", `${replaceForVarNames(node.statement, status)}`, " {", node.span, status, b);
@@ -16,8 +16,8 @@ export default function buildTextNode(node: TextNode, status: BuildStatus): void
16
16
  content = content.replaceAll(/\s+/g, " ");
17
17
  }
18
18
 
19
- // TODO: Move all of this logic into parse, for text nodes and attribute values
20
- // Or, we could do it based on the node's ranges...
19
+ // TODO: Should we be parsing out the spans here? Like we do with
20
+ // attributes? And should we merge this with the attributes logic?
21
21
  let textContent = "";
22
22
  let reactiveCount = 0;
23
23
  let reactiveStart = 0;
@@ -90,10 +90,17 @@ export default function buildTextNode(node: TextNode, status: BuildStatus): void
90
90
  }
91
91
 
92
92
  status.imports.add("t_fmt");
93
- let functionBody = `${node.varName}.textContent = `;
94
- offsetChange += functionBody.length;
95
93
  offsets.map((_, i) => (offsets[i] += offsetChange));
96
- functionBody += `${textContent};`;
97
- stashRunWithOffsets(fragment, functionBody, node.spans, offsets, lengths, status);
94
+
95
+ stashRunWithOffsets(
96
+ fragment,
97
+ `${node.varName}.textContent = `,
98
+ textContent,
99
+ ";",
100
+ node.spans,
101
+ offsets,
102
+ lengths,
103
+ status,
104
+ );
98
105
  }
99
106
  }
@@ -0,0 +1,90 @@
1
+ import type SourceSpan from "../../types/SourceSpan";
2
+ import endOfString from "../../utils/endOfString";
3
+ import endOfTemplateString from "../../utils/endOfTemplateString";
4
+ import trimQuotes from "../../utils/trimQuotes";
5
+
6
+ export default function getAttributeOffsets(
7
+ value: string,
8
+ span: SourceSpan,
9
+ ): {
10
+ newValue: string;
11
+ spans: SourceSpan[];
12
+ offsets: number[];
13
+ lengths: number[];
14
+ } {
15
+ // Build offsets for interpolated values e.g. attribute="value {x}
16
+ // and {y}" should have offsets for where the x and y are
17
+ // interpolated into the output
18
+ value = trimQuotes(value);
19
+ let spans: SourceSpan[] = [];
20
+ let offsets: number[] = [];
21
+ let lengths: number[] = [];
22
+
23
+ let textContent = "";
24
+ let reactiveStart = 0;
25
+ let level = 0;
26
+ for (let i = 0; i < value.length; i++) {
27
+ const char = value[i];
28
+ const nextChar = value[i + 1];
29
+ if (char === "{") {
30
+ level++;
31
+ //maxLevel = Math.max(level, maxLevel);
32
+ if (level === 1) {
33
+ //reactiveCount += 1;
34
+ textContent += "${";
35
+ reactiveStart = textContent.length;
36
+ // 2 is for the start quote plus the new `$`
37
+ spans.push({ start: span.start + i + 2, end: -1 });
38
+ offsets.push(reactiveStart + 1);
39
+ continue;
40
+ }
41
+ } else if (char === "}") {
42
+ level--;
43
+ if (level === 0) {
44
+ // 2 is for the start quote plus the new `$`
45
+ spans.at(-1)!.end = span.start + i + 2;
46
+ lengths.push(textContent.length - reactiveStart);
47
+ textContent += "}";
48
+ continue;
49
+ }
50
+ } else if (char === "`" && level === 0) {
51
+ // Escape backticks outside of braces, as they will be within a backtick string
52
+ textContent += "\\";
53
+ } else if (level > 0) {
54
+ if (char === "/" && nextChar === "/") {
55
+ // Skip one-line comments
56
+ let start = i;
57
+ i = value.indexOf("\n", i);
58
+ textContent += value.substring(start, i + 1);
59
+ continue;
60
+ } else if (char === "/" && nextChar === "*") {
61
+ // Skip block comments
62
+ let start = i;
63
+ i = value.indexOf("*/", i) + 1;
64
+ textContent += value.substring(start, i + 1);
65
+ continue;
66
+ } else if (char === '"' || char === "'") {
67
+ // Skip string contents
68
+ let start = i;
69
+ i = endOfString(char, value, i);
70
+ textContent += value.substring(start, i + 1);
71
+ continue;
72
+ } else if (char === "`") {
73
+ // Skip interpolated string contents
74
+ let start = i;
75
+ i = endOfTemplateString(value, i);
76
+ textContent += value.substring(start, i + 1);
77
+ continue;
78
+ }
79
+ }
80
+ textContent += value[i];
81
+ }
82
+ value = `\`${textContent}\``;
83
+
84
+ return {
85
+ newValue: value,
86
+ spans,
87
+ offsets,
88
+ lengths,
89
+ };
90
+ }
@@ -5,13 +5,18 @@ import replaceForVarNames from "./replaceForVarNames";
5
5
 
6
6
  export default function stashRunWithOffsets(
7
7
  fragment: Fragment,
8
- functionBody: string,
8
+ functionStart: string,
9
+ value: string,
10
+ functionEnd: string,
9
11
  spans: SourceSpan[],
10
12
  offsets: number[],
11
13
  lengths: number[],
12
14
  status: BuildStatus,
13
15
  ): void {
16
+ let functionBody = functionStart + value + functionEnd;
17
+
14
18
  for (let i = 0; i < offsets.length; i++) {
19
+ offsets[i] += functionStart.length;
15
20
  const oldLength = functionBody.length;
16
21
  functionBody =
17
22
  functionBody.substring(0, offsets[i]) +
@@ -23,6 +28,7 @@ export default function stashRunWithOffsets(
23
28
  offsets[j] += changedLength;
24
29
  }
25
30
  }
31
+
26
32
  fragment.effects.push({
27
33
  functionBody,
28
34
  spans: spans,
@@ -105,7 +105,7 @@ function buildElementAttributes(node: ElementNode, status: BuildServerStatus) {
105
105
  // TODO: Match braces, don't replace braces inside code
106
106
  status.imports.add("t_attr");
107
107
  attributes.push(
108
- `${name}="${trimQuotes(value).replaceAll("${", "${t_attr(").replaceAll("}", ")}")}"`,
108
+ `${name}="${trimQuotes(value).replaceAll("{", "${t_attr(").replaceAll("}", ")}")}"`,
109
109
  );
110
110
  } else if (name === "class" && node.scopeStyles) {
111
111
  value = value