|
80 | 80 | if (FULLWIDTH_OPS[c]) { out.push({ t: FULLWIDTH_OPS[c], v: FULLWIDTH_OPS[c] }); i++; continue; } |
81 | 81 | if (c === "(") { out.push({ t: "(", v: c }); i++; continue; } |
82 | 82 | if (c === ")") { out.push({ t: ")", v: c }); i++; continue; } |
| 83 | + /* ---- H1.00 扩展:列表/字典字面量 token ---- */ |
| 84 | + if (c === "[") { out.push({ t: "[", v: c }); i++; continue; } |
| 85 | + if (c === "]") { out.push({ t: "]", v: c }); i++; continue; } |
| 86 | + if (c === "{") { out.push({ t: "{", v: c }); i++; continue; } |
| 87 | + if (c === "}") { out.push({ t: "}", v: c }); i++; continue; } |
| 88 | + if (c === ",") { out.push({ t: ",", v: c }); i++; continue; } |
| 89 | + if (c === ":") { out.push({ t: ":", v: c }); i++; continue; } |
83 | 90 | if ("+-*/%".indexOf(c) >= 0) { out.push({ t: c, v: c }); i++; continue; } |
84 | 91 | const two = expr.substr(i, 2); |
85 | 92 | if (two === "==" || two === "!=" || two === "<=" || two === ">=") { out.push({ t: two, v: two }); i += 2; continue; } |
|
113 | 120 | const sL = String(l), sR = String(r); |
114 | 121 | if (op === "==") return numL && numR ? l === r : sL === sR; |
115 | 122 | if (op === "!=") return numL && numR ? l !== r : sL !== sR; |
116 | | - if (op === "in") return sR.indexOf(sL) >= 0; // 「x in container」:右侧含左侧 |
| 123 | + /* ---- H1.00 扩展:列表/字典 in 操作符 ---- */ |
| 124 | + if (op === "in") { |
| 125 | + if (Array.isArray(r)) return r.indexOf(l) >= 0; |
| 126 | + if (r && typeof r === "object") return l in r; |
| 127 | + return sR.indexOf(sL) >= 0; // 「x in container」:右侧含左侧(子串匹配) |
| 128 | + } |
117 | 129 | const a = numL && numR ? l : sL; |
118 | 130 | const b = numL && numR ? r : sR; |
119 | 131 | if (op === "<") return a < b; |
|
123 | 135 | return false; |
124 | 136 | } |
125 | 137 |
|
| 138 | + /* ---- H1.00 扩展:evalExpr 内置函数库 ---- */ |
| 139 | + const BUILTINS = { |
| 140 | + len: function (x) { |
| 141 | + if (x == null) return 0; |
| 142 | + if (typeof x === "string") return x.length; |
| 143 | + if (Array.isArray(x)) return x.length; |
| 144 | + if (typeof x === "object") return Object.keys(x).length; |
| 145 | + return String(x).length; |
| 146 | + }, |
| 147 | + range: function () { |
| 148 | + const args = [].slice.call(arguments); |
| 149 | + const arr = []; |
| 150 | + let start = 0, end = 0, step = 1; |
| 151 | + if (args.length === 1) { end = Number(args[0]); } |
| 152 | + else if (args.length === 2) { start = Number(args[0]); end = Number(args[1]); } |
| 153 | + else { start = Number(args[0]); end = Number(args[1]); step = Number(args[2]) || 1; } |
| 154 | + if (step > 0) { for (let v = start; v < end; v += step) arr.push(v); } |
| 155 | + else if (step < 0) { for (let v = start; v > end; v += step) arr.push(v); } |
| 156 | + return arr; |
| 157 | + }, |
| 158 | + max: function () { |
| 159 | + const args = [].slice.call(arguments); |
| 160 | + if (!args.length) return 0; |
| 161 | + if (args.length === 1 && Array.isArray(args[0])) return Math.max.apply(null, args[0]); |
| 162 | + return Math.max.apply(null, args); |
| 163 | + }, |
| 164 | + min: function () { |
| 165 | + const args = [].slice.call(arguments); |
| 166 | + if (!args.length) return 0; |
| 167 | + if (args.length === 1 && Array.isArray(args[0])) return Math.min.apply(null, args[0]); |
| 168 | + return Math.min.apply(null, args); |
| 169 | + }, |
| 170 | + abs: function (x) { return Math.abs(Number(x)); }, |
| 171 | + round: function (x) { return Math.round(Number(x)); }, |
| 172 | + int: function (x) { return parseInt(x, 10); }, |
| 173 | + float: function (x) { return parseFloat(x); }, |
| 174 | + str: function (x) { return String(x == null ? "" : x); }, |
| 175 | + type: function (x) { |
| 176 | + if (x === true || x === false) return "bool"; |
| 177 | + if (Array.isArray(x)) return "list"; |
| 178 | + if (typeof x === "object" && x !== null) return "dict"; |
| 179 | + if (typeof x === "number") return "num"; |
| 180 | + if (typeof x === "string") return "str"; |
| 181 | + return ""; |
| 182 | + }, |
| 183 | + contains: function (s, sub) { |
| 184 | + if (typeof s !== "string" && !Array.isArray(s)) return false; |
| 185 | + if (typeof s === "string") return String(s).indexOf(String(sub)) >= 0; |
| 186 | + return s.indexOf(sub) >= 0; |
| 187 | + } |
| 188 | + }; |
| 189 | + |
126 | 190 | function evalExpr(expr, vars) { |
127 | 191 | const toks = tokenize(expr); |
128 | 192 | let p = 0; |
|
134 | 198 | if (typeof v === "number") return v !== 0; |
135 | 199 | return String(v).length > 0; |
136 | 200 | } |
| 201 | + /* ---- H1.00 扩展:primary 处理列表、字典、内置函数调用 ---- */ |
137 | 202 | function primary() { |
138 | 203 | const t = next(); |
139 | 204 | if (!t) return ""; |
140 | 205 | if (t.t === "(") { const v = orExpr(); next(); return v; } |
141 | 206 | if (t.t === "num") return t.v; |
142 | 207 | if (t.t === "str") return t.v; |
| 208 | + if (t.t === "[") { |
| 209 | + // 列表字面量 [expr, expr, ...] |
| 210 | + const arr = []; |
| 211 | + if (!(peek() && peek().t === "]")) { |
| 212 | + arr.push(orExpr()); |
| 213 | + while (peek() && peek().t === ",") { next(); arr.push(orExpr()); } |
| 214 | + } |
| 215 | + if (peek() && peek().t === "]") next(); |
| 216 | + return arr; |
| 217 | + } |
| 218 | + if (t.t === "{") { |
| 219 | + // 字典字面量 { key: val, key: val } |
| 220 | + const obj = {}; |
| 221 | + if (!(peek() && peek().t === "}")) { |
| 222 | + let keyTok = peek(); |
| 223 | + if (keyTok && (keyTok.t === "str" || keyTok.t === "id" || keyTok.t === "num")) { |
| 224 | + next(); |
| 225 | + const key = keyTok.t === "id" ? keyTok.v : keyTok.v; |
| 226 | + if (peek() && peek().t === ":") next(); |
| 227 | + obj[key] = orExpr(); |
| 228 | + } |
| 229 | + while (peek() && peek().t === ",") { |
| 230 | + next(); |
| 231 | + keyTok = peek(); |
| 232 | + if (!keyTok || keyTok.t === "}") break; |
| 233 | + if (keyTok.t === "str" || keyTok.t === "id" || keyTok.t === "num") { |
| 234 | + next(); |
| 235 | + const key = keyTok.t === "id" ? keyTok.v : keyTok.v; |
| 236 | + if (peek() && peek().t === ":") next(); |
| 237 | + obj[key] = orExpr(); |
| 238 | + } |
| 239 | + } |
| 240 | + } |
| 241 | + if (peek() && peek().t === "}") next(); |
| 242 | + return obj; |
| 243 | + } |
143 | 244 | if (t.t === "id") { |
144 | 245 | const w = t.v; |
| 246 | + // 内置函数调用:id(args) |
| 247 | + if (peek() && peek().t === "(" && BUILTINS[w]) { |
| 248 | + next(); // 消费 ( |
| 249 | + const args = []; |
| 250 | + if (!(peek() && peek().t === ")")) { |
| 251 | + args.push(orExpr()); |
| 252 | + while (peek() && peek().t === ",") { next(); args.push(orExpr()); } |
| 253 | + } |
| 254 | + if (peek() && peek().t === ")") next(); |
| 255 | + try { return BUILTINS[w].apply(null, args); } catch (e) { return false; } |
| 256 | + } |
145 | 257 | if (w === "true") return true; |
146 | 258 | if (w === "false") return false; |
147 | 259 | if (w === "None") return ""; |
|
163 | 275 | } |
164 | 276 | return v; |
165 | 277 | } |
| 278 | + /* ---- H1.00 扩展:字符串拼接(+ 操作数含字符串时用 String() 拼接)---- */ |
166 | 279 | function addSub() { |
167 | 280 | let v = mulDiv(); |
168 | 281 | while (peek() && (peek().t === "+" || peek().t === "-")) { |
169 | 282 | const op = next().t; const r = mulDiv(); |
170 | | - v = Number(v) + (op === "+" ? Number(r) : -Number(r)); |
| 283 | + if (op === "+") { |
| 284 | + if (typeof v === "string" || typeof r === "string") v = String(v) + String(r); |
| 285 | + else v = Number(v) + Number(r); |
| 286 | + } else { |
| 287 | + if (typeof v === "string" || typeof r === "string") v = String(v) + String(r); |
| 288 | + else v = Number(v) - Number(r); |
| 289 | + } |
171 | 290 | } |
172 | 291 | return v; |
173 | 292 | } |
|
249 | 368 | // for 循环头:for 变量 in 迭代源: |
250 | 369 | const form = line.match(/^for\s+([A-Za-z_\u4e00-\u9fff][A-Za-z0-9_\u4e00-\u9fff]*)\s+in\s+(.+?)\s*:\s*$/i); |
251 | 370 | if (form) return { kind: "for", var: form[1], iter: form[2].trim() }; |
| 371 | + /* ---- H1.00 扩展:新语句分类(while / break / continue / set / let / opset / say)---- */ |
| 372 | + // while 条件: 循环头 |
| 373 | + const whileM = line.match(/^while\s+(.+?)\s*:\s*$/i); |
| 374 | + if (whileM) return { kind: "while", cond: whileM[1].replace(/:\s*$/, "").trim() }; |
| 375 | + // break / continue |
| 376 | + if (/^break\s*$/.test(line)) return { kind: "break" }; |
| 377 | + if (/^continue\s*$/.test(line)) return { kind: "continue" }; |
| 378 | + // set 名 = 表达式 — 运行时赋值 |
| 379 | + const setM = line.match(/^set\s+([A-Za-z_\u4e00-\u9fff][A-Za-z0-9_\u4e00-\u9fff]*)\s*=\s*(.+?)\s*$/); |
| 380 | + if (setM) return { kind: "set", name: setM[1], expr: setM[2] }; |
| 381 | + // let 名 be 值 — 局部临时变量 |
| 382 | + const letM = line.match(/^let\s+([A-Za-z_\u4e00-\u9fff][A-Za-z0-9_\u4e00-\u9fff]*)\s+be\s+(.+?)\s*$/); |
| 383 | + if (letM) return { kind: "let", name: letM[1], value: letM[2] }; |
| 384 | + // 简写运算:名 add/sub/mul/div 表达式 |
| 385 | + const opsetM = line.match(/^([A-Za-z_\u4e00-\u9fff][A-Za-z0-9_\u4e00-\u9fff]*)\s+(add|sub|mul|div)\s+(.+)$/i); |
| 386 | + if (opsetM) return { kind: "opset", name: opsetM[1], op: opsetM[2].toLowerCase(), expr: opsetM[3].trim() }; |
| 387 | + // say 内容 — 输出文本 |
| 388 | + const sayM = line.match(/^say\s+(.*)$/); |
| 389 | + if (sayM) return { kind: "say", text: sayM[1].trim() }; |
252 | 390 | const cond = parseCondHeader(line); |
253 | 391 | if (cond) return { kind: "cond", ...cond }; |
254 | 392 | // 导向 to / fr |
|
365 | 503 | const bodyEndFor = collectUntil(i, end, indentGoal); |
366 | 504 | out.push({ kind: "for", var: st.var, iter: st.iter, body: build(i, bodyEndFor, minIndent(i, bodyEndFor)) }); |
367 | 505 | i = bodyEndFor; |
| 506 | + /* ---- H1.00 扩展:while 块收集 ---- */ |
| 507 | + } else if (st.kind === "while") { |
| 508 | + if (pendingIf) { out.push(pendingIf); pendingIf = null; } |
| 509 | + i++; |
| 510 | + const bodyEndW = collectUntil(i, end, indentGoal); |
| 511 | + out.push({ kind: "while", cond: st.cond, body: build(i, bodyEndW, minIndent(i, bodyEndW)) }); |
| 512 | + i = bodyEndW; |
368 | 513 | } else if (st.kind === "regionStart") { |
369 | 514 | // 触发区域:cf NAME ... cf NAME stop。body 为区域内的代码块(点击/长按触发执行) |
370 | 515 | if (pendingIf) { out.push(pendingIf); pendingIf = null; } |
|
433 | 578 | collectVars(n.body, vars); |
434 | 579 | n.chains.forEach(function (c) { collectVars(c.body, vars); }); |
435 | 580 | if (n.orphan) { /* orphan body already collected */ } |
| 581 | + /* ---- H1.00 扩展:while / let / set / opset 变量收集 ---- */ |
| 582 | + } else if (n.kind === "while") { |
| 583 | + collectVars(n.body, vars); |
| 584 | + } else if (n.kind === "let") { |
| 585 | + let val = ""; |
| 586 | + try { val = evalExpr(n.value, {}); } catch (e) { val = ""; } |
| 587 | + if (val !== "" && !isNaN(Number(val))) val = Number(val); |
| 588 | + vars[n.name] = { type: "ordinary", value: val, isImg: false }; |
| 589 | + } else if (n.kind === "set" || n.kind === "opset") { |
| 590 | + let val = ""; |
| 591 | + try { |
| 592 | + if (n.kind === "opset") { |
| 593 | + const cur = vars[n.name] ? vars[n.name].value : 0; |
| 594 | + const r = evalExpr(n.expr, vars); |
| 595 | + const cn = Number(cur), rn = Number(r); |
| 596 | + if (n.op === "add") val = cn + rn; |
| 597 | + else if (n.op === "sub") val = cn - rn; |
| 598 | + else if (n.op === "mul") val = cn * rn; |
| 599 | + else if (n.op === "div") val = cn / rn; |
| 600 | + else val = r; |
| 601 | + } else { |
| 602 | + val = evalExpr(n.expr, vars); |
| 603 | + } |
| 604 | + } catch (e) { val = ""; } |
| 605 | + if (val !== "" && typeof val !== "number" && !isNaN(Number(val))) val = Number(val); |
| 606 | + if (!vars[n.name]) vars[n.name] = { type: "ordinary", value: val, isImg: false }; |
| 607 | + else vars[n.name].value = val; |
| 608 | + } else if (n.kind === "say" || n.kind === "break" || n.kind === "continue") { |
| 609 | + // 不产生变量 |
436 | 610 | } |
437 | 611 | }); |
438 | 612 | return vars; |
|
506 | 680 |
|
507 | 681 | function renderNodes(nodes, vars, ctx, out) { |
508 | 682 | nodes.forEach(function (n) { |
| 683 | + /* ---- H1.00 扩展:break/continue 传播 ---- */ |
| 684 | + if (ctx && (ctx.breakFlag || ctx.continueFlag)) return; |
509 | 685 | if (n.kind === "it") { |
510 | 686 | out.push(n); // 声明保留(供变量面板) |
511 | 687 | } else if (n.kind === "in") { |
|
526 | 702 | // 逐个取值渲染循环体,循环变量按普通变量注入 |
527 | 703 | const vals = iterVals(n.iter, vars); |
528 | 704 | vals.forEach(function (val) { |
| 705 | + if (ctx && ctx.breakFlag) { ctx.breakFlag = false; return; } |
529 | 706 | const prev = vars[n.var]; |
530 | 707 | vars[n.var] = { type: "ordinary", value: val, isImg: false }; |
531 | 708 | renderNodes(n.body, vars, ctx, out); |
532 | 709 | if (prev) vars[n.var] = prev; else delete vars[n.var]; |
| 710 | + if (ctx && ctx.breakFlag) { ctx.breakFlag = false; return; } |
| 711 | + if (ctx && ctx.continueFlag) { ctx.continueFlag = false; /* continue → 下一轮 */ } |
533 | 712 | }); |
534 | 713 | } else if (n.kind === "cond") { |
535 | 714 | // 求值 if;未匹配则依次 orif;都不匹配取 else |
|
559 | 738 | out.push({ kind: "cppBlock", lang: "cpp", code: n.code }); |
560 | 739 | } else if (n.kind === "textline") { |
561 | 740 | out.push({ kind: "textline", text: n.text }); |
| 741 | + /* ---- H1.00 扩展:break / continue / while / set / let / opset / say ---- */ |
| 742 | + } else if (n.kind === "break") { |
| 743 | + if (ctx) ctx.breakFlag = true; |
| 744 | + return; |
| 745 | + } else if (n.kind === "continue") { |
| 746 | + if (ctx) ctx.continueFlag = true; |
| 747 | + return; |
| 748 | + } else if (n.kind === "while") { |
| 749 | + // 编译期 while 展开:条件动态求值,最多 50 次(安全上限,不是语言限制) |
| 750 | + let safety = 0; |
| 751 | + while (safety++ < 50) { |
| 752 | + if (ctx && ctx.breakFlag) { ctx.breakFlag = false; break; } |
| 753 | + let condVal = false; |
| 754 | + try { condVal = evalExpr(n.cond, vars); } catch (e) {} |
| 755 | + if (!condVal) break; |
| 756 | + renderNodes(n.body, vars, ctx, out); |
| 757 | + if (ctx && ctx.breakFlag) { ctx.breakFlag = false; break; } |
| 758 | + if (ctx && ctx.continueFlag) { ctx.continueFlag = false; /* 继续下一轮 */ } |
| 759 | + } |
| 760 | + } else if (n.kind === "set" || n.kind === "opset") { |
| 761 | + let val; |
| 762 | + try { |
| 763 | + if (n.kind === "opset") { |
| 764 | + const cur = vars[n.name] ? vars[n.name].value : 0; |
| 765 | + const r = evalExpr(n.expr, vars); |
| 766 | + const cn = Number(cur), rn = Number(r); |
| 767 | + if (n.op === "add") val = cn + rn; |
| 768 | + else if (n.op === "sub") val = cn - rn; |
| 769 | + else if (n.op === "mul") val = cn * rn; |
| 770 | + else if (n.op === "div") val = cn / rn; |
| 771 | + else val = r; |
| 772 | + } else { |
| 773 | + val = evalExpr(n.expr, vars); |
| 774 | + } |
| 775 | + } catch (e) { val = ""; } |
| 776 | + if (!vars[n.name]) vars[n.name] = { type: "ordinary", value: val, isImg: false }; |
| 777 | + else vars[n.name].value = val; |
| 778 | + // set 本身不输出 HTML |
| 779 | + } else if (n.kind === "let") { |
| 780 | + let val; |
| 781 | + try { val = evalExpr(n.value, vars); } catch (e) { val = ""; } |
| 782 | + vars[n.name] = { type: "ordinary", value: val, isImg: false }; |
| 783 | + // let 本身不输出 HTML |
| 784 | + } else if (n.kind === "say") { |
| 785 | + const text = interpolate(n.text, vars); |
| 786 | + out.push({ kind: "say", text: text }); |
562 | 787 | } |
563 | 788 | }); |
564 | 789 | return out; |
|
715 | 940 | if (it.kind === "textline") { |
716 | 941 | return '<p class="hic-item hic-body">' + esc(it.text) + "</p>"; |
717 | 942 | } |
| 943 | + /* ---- H1.00 扩展:say 输出 ---- */ |
| 944 | + if (it.kind === "say") { |
| 945 | + return '<p class="hic-item hic-body">' + esc(it.text) + "</p>"; |
| 946 | + } |
718 | 947 | return ""; |
719 | 948 | }).join("\n"); |
720 | 949 | } |
|
0 commit comments