@kuratchi/js 0.0.13 → 0.0.15

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.
@@ -175,10 +175,56 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
175
175
  }
176
176
  }
177
177
  // HTML line → compile {expr} interpolations
178
- out.push(compileHtmlLine(line, actionNames, rpcNameMap));
178
+ // Handle multi-line expressions: if a line has unclosed {, join continuation lines
179
+ let htmlLine = line;
180
+ let extraLines = 0;
181
+ if (hasUnclosedBrace(htmlLine)) {
182
+ let j = i + 1;
183
+ while (j < lines.length && hasUnclosedBrace(htmlLine)) {
184
+ htmlLine += '\n' + lines[j];
185
+ extraLines++;
186
+ j++;
187
+ }
188
+ i += extraLines;
189
+ }
190
+ out.push(compileHtmlLine(htmlLine, actionNames, rpcNameMap));
179
191
  }
180
192
  return out.join('\n');
181
193
  }
194
+ /**
195
+ * Check if a string has unclosed template braces (more { than }).
196
+ * Respects string quotes to avoid false positives.
197
+ */
198
+ function hasUnclosedBrace(src) {
199
+ let depth = 0;
200
+ let quote = null;
201
+ let escaped = false;
202
+ for (let i = 0; i < src.length; i++) {
203
+ const ch = src[i];
204
+ if (quote) {
205
+ if (escaped) {
206
+ escaped = false;
207
+ continue;
208
+ }
209
+ if (ch === '\\') {
210
+ escaped = true;
211
+ continue;
212
+ }
213
+ if (ch === quote)
214
+ quote = null;
215
+ continue;
216
+ }
217
+ if (ch === '"' || ch === "'" || ch === '`') {
218
+ quote = ch;
219
+ continue;
220
+ }
221
+ if (ch === '{')
222
+ depth++;
223
+ if (ch === '}')
224
+ depth--;
225
+ }
226
+ return depth > 0;
227
+ }
182
228
  function advanceHtmlTagState(src, startInTag, startQuote) {
183
229
  let inTag = startInTag;
184
230
  let quote = startQuote;
@@ -718,7 +764,7 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
718
764
  continue;
719
765
  }
720
766
  else if (attrName === 'data-poll') {
721
- // data-poll={fn(args)} → data-poll="fnName" data-poll-args="[serialized]"
767
+ // data-poll={fn(args)} data-poll="fnName" data-poll-args="[serialized]" data-poll-id="stable-id"
722
768
  const pollCallMatch = inner.match(/^(\w+)\((.*)\)$/);
723
769
  if (pollCallMatch) {
724
770
  const fnName = pollCallMatch[1];
@@ -726,10 +772,16 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
726
772
  const argsExpr = pollCallMatch[2].trim();
727
773
  // Remove the trailing "data-poll=" we already appended
728
774
  result = result.replace(/\s*data-poll=$/, '');
729
- // Emit data-poll and data-poll-args attributes
775
+ // Emit data-poll, data-poll-args, and stable data-poll-id (based on fn + args expression)
730
776
  result += ` data-poll="${rpcName}"`;
731
777
  if (argsExpr) {
732
778
  result += ` data-poll-args="\${__esc(JSON.stringify([${argsExpr}]))}"`;
779
+ // Stable ID based on args so same data produces same ID across renders
780
+ result += ` data-poll-id="\${__esc('__poll_' + String(${argsExpr}).replace(/[^a-zA-Z0-9]/g, '_'))}"`;
781
+ }
782
+ else {
783
+ // No args - use function name as ID
784
+ result += ` data-poll-id="__poll_${rpcName}"`;
733
785
  }
734
786
  }
735
787
  hasExpr = true;
@@ -861,33 +913,33 @@ function findClosingBrace(src, openPos) {
861
913
  */
862
914
  export function generateRenderFunction(template) {
863
915
  const body = compileTemplate(template);
864
- return `function render(data) {
865
- const __rawHtml = (v) => {
866
- if (v == null) return '';
867
- return String(v);
868
- };
869
- const __sanitizeHtml = (v) => {
870
- let html = __rawHtml(v);
871
- html = html.replace(/<script\\b[^>]*>[\\s\\S]*?<\\/script>/gi, '');
872
- html = html.replace(/<iframe\\b[^>]*>[\\s\\S]*?<\\/iframe>/gi, '');
873
- html = html.replace(/<object\\b[^>]*>[\\s\\S]*?<\\/object>/gi, '');
874
- html = html.replace(/<embed\\b[^>]*>/gi, '');
875
- html = html.replace(/\\son[a-z]+\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, '');
876
- html = html.replace(/\\s(href|src|xlink:href)\\s*=\\s*([\"'])\\s*javascript:[\\s\\S]*?\\2/gi, ' $1="#"');
877
- html = html.replace(/\\s(href|src|xlink:href)\\s*=\\s*javascript:[^\\s>]+/gi, ' $1="#"');
878
- html = html.replace(/\\ssrcdoc\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, '');
879
- return html;
880
- };
881
- const __esc = (v) => {
882
- if (v == null) return '';
883
- return String(v)
884
- .replace(/&/g, '&amp;')
885
- .replace(/</g, '&lt;')
886
- .replace(/>/g, '&gt;')
887
- .replace(/"/g, '&quot;')
888
- .replace(/'/g, '&#39;');
889
- };
890
- ${body}
916
+ return `function render(data) {
917
+ const __rawHtml = (v) => {
918
+ if (v == null) return '';
919
+ return String(v);
920
+ };
921
+ const __sanitizeHtml = (v) => {
922
+ let html = __rawHtml(v);
923
+ html = html.replace(/<script\\b[^>]*>[\\s\\S]*?<\\/script>/gi, '');
924
+ html = html.replace(/<iframe\\b[^>]*>[\\s\\S]*?<\\/iframe>/gi, '');
925
+ html = html.replace(/<object\\b[^>]*>[\\s\\S]*?<\\/object>/gi, '');
926
+ html = html.replace(/<embed\\b[^>]*>/gi, '');
927
+ html = html.replace(/\\son[a-z]+\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, '');
928
+ html = html.replace(/\\s(href|src|xlink:href)\\s*=\\s*([\"'])\\s*javascript:[\\s\\S]*?\\2/gi, ' $1="#"');
929
+ html = html.replace(/\\s(href|src|xlink:href)\\s*=\\s*javascript:[^\\s>]+/gi, ' $1="#"');
930
+ html = html.replace(/\\ssrcdoc\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)/gi, '');
931
+ return html;
932
+ };
933
+ const __esc = (v) => {
934
+ if (v == null) return '';
935
+ return String(v)
936
+ .replace(/&/g, '&amp;')
937
+ .replace(/</g, '&lt;')
938
+ .replace(/>/g, '&gt;')
939
+ .replace(/"/g, '&quot;')
940
+ .replace(/'/g, '&#39;');
941
+ };
942
+ ${body}
891
943
  }`;
892
944
  }
893
945
  import { transpileTypeScript } from './transpile.js';
@@ -129,20 +129,6 @@ export interface kuratchiConfig<E extends Env = Env> {
129
129
  /** DO source files (e.g. ['auth.do.ts', 'sites.do.ts']) */
130
130
  files?: string[];
131
131
  }>;
132
- /** Container classes exported into the generated worker entry. */
133
- containers?: Record<string, string | {
134
- /** Relative path from project root. Must end in `.container.ts|js|mjs|cjs`. */
135
- file: string;
136
- /** Optional override; inferred from exported class in `file` when omitted. */
137
- className?: string;
138
- }>;
139
- /** Workflow classes exported into the generated worker entry. */
140
- workflows?: Record<string, string | {
141
- /** Relative path from project root. Must end in `.workflow.ts|js|mjs|cjs`. */
142
- file: string;
143
- /** Optional override; inferred from exported class in `file` when omitted. */
144
- className?: string;
145
- }>;
146
132
  }
147
133
  /** Auth configuration for kuratchi.config.ts */
148
134
  export interface AuthConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuratchi/js",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "A thin, Cloudflare Workers-native web framework with Svelte-inspired syntax",
5
5
  "license": "MIT",
6
6
  "type": "module",