@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.
- package/README.md +835 -691
- package/dist/compiler/index.js +1297 -987
- package/dist/compiler/template.js +82 -30
- package/dist/runtime/types.d.ts +0 -14
- package/package.json +1 -1
|
@@ -175,10 +175,56 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
// HTML line → compile {expr} interpolations
|
|
178
|
-
|
|
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)}
|
|
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
|
|
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, '&')
|
|
885
|
-
.replace(/</g, '<')
|
|
886
|
-
.replace(/>/g, '>')
|
|
887
|
-
.replace(/"/g, '"')
|
|
888
|
-
.replace(/'/g, ''');
|
|
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, '&')
|
|
937
|
+
.replace(/</g, '<')
|
|
938
|
+
.replace(/>/g, '>')
|
|
939
|
+
.replace(/"/g, '"')
|
|
940
|
+
.replace(/'/g, ''');
|
|
941
|
+
};
|
|
942
|
+
${body}
|
|
891
943
|
}`;
|
|
892
944
|
}
|
|
893
945
|
import { transpileTypeScript } from './transpile.js';
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -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 {
|