|
1 | 1 | /* ============================================================ |
2 | | - * HC v0.01 —— HIC 语言引擎 |
| 2 | + * HC v0.02 —— HIC 语言引擎 |
3 | 3 | * 负责:HIC 词法/语法解析、HIC→HTML 转译、轻量 ZIP 写入、下载 |
4 | 4 | * 说明:本文件为纯逻辑,不依赖 DOM(localStorage/document), |
5 | 5 | * 唯一例外是 download()(仅浏览器调用);可用 Node 单元测试。 |
|
11 | 11 | })(typeof self !== 'undefined' ? self : null, function () { |
12 | 12 | "use strict"; |
13 | 13 |
|
14 | | - const APP = { version: "v0.01", name: "HiCode", lang: "HIC" }; |
| 14 | + const APP = { version: "v0.02", name: "HiCode", lang: "HIC" }; |
15 | 15 |
|
16 | 16 | /* ---------------- 工具 ---------------- */ |
17 | 17 | function esc(s) { |
18 | 18 | return String(s == null ? "" : s) |
19 | 19 | .replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") |
20 | 20 | .replace(/"/g, """).replace(/'/g, "'"); |
21 | 21 | } |
22 | | - function isName(s) { return /^[A-Za-z_][A-Za-z0-9_]*$/.test(s); } |
| 22 | + // 标识符:支持英文与中文(中日韩统一表意文字)开头的变量名 |
| 23 | + const ID_START = "A-Za-z_\\u4e00-\\u9fff"; |
| 24 | + const ID_CHAR = "A-Za-z0-9_\\u4e00-\\u9fff"; |
| 25 | + const NAME_RE = new RegExp("^[" + ID_START + "][" + ID_CHAR + "]*$"); |
| 26 | + function isName(s) { return NAME_RE.test(String(s || "")); } |
23 | 27 | const PY_TYPES = ["int", "float", "str", "bool", "list", "dict", "tuple"]; |
24 | 28 |
|
25 | 29 | /* ---------------- 词法:表达式求值(安全,无 eval) ---------------- */ |
|
30 | 34 | if (c === " " || c === "\t") { i++; continue; } |
31 | 35 | if (c === "(") { out.push({ t: "(", v: c }); i++; continue; } |
32 | 36 | if (c === ")") { out.push({ t: ")", v: c }); i++; continue; } |
33 | | - if (c === "+") { out.push({ t: "+", v: c }); i++; continue; } |
| 37 | + if ("+-*/%".indexOf(c) >= 0) { out.push({ t: c, v: c }); i++; continue; } |
34 | 38 | const two = expr.substr(i, 2); |
35 | 39 | if (two === "==" || two === "!=" || two === "<=" || two === ">=") { out.push({ t: two, v: two }); i += 2; continue; } |
36 | 40 | if (c === "<" || c === ">") { out.push({ t: c, v: c }); i++; continue; } |
|
39 | 43 | while (j < n && expr[j] !== q) { s += expr[j]; j++; } |
40 | 44 | out.push({ t: "str", v: s }); i = Math.min(j + 1, n); continue; |
41 | 45 | } |
42 | | - if (/[0-9]/.test(c) || (c === "-" && /[0-9]/.test(expr[i + 1]))) { |
| 46 | + if (/[0-9]/.test(c)) { |
43 | 47 | let j = i; let num = ""; |
44 | | - while (j < n && /[0-9._]/.test(expr[j])) { num += expr[j]; j++; } |
45 | | - out.push({ t: "num", v: Number(num.replace(/[^0-9.]/g, "")) }); i = j; continue; |
| 48 | + while (j < n && /[0-9.]/.test(expr[j])) { num += expr[j]; j++; } |
| 49 | + out.push({ t: "num", v: Number(num) }); i = j; continue; |
46 | 50 | } |
47 | | - if (/[A-Za-z_0-9]/.test(c)) { |
| 51 | + if (/[A-Za-z_\u4e00-\u9fff]/.test(c)) { |
48 | 52 | let j = i; let w = ""; |
49 | | - while (j < n && /[A-Za-z0-9_]/.test(expr[j])) { w += expr[j]; j++; } |
| 53 | + while (j < n && /[A-Za-z0-9_\u4e00-\u9fff]/.test(expr[j])) { w += expr[j]; j++; } |
50 | 54 | out.push({ t: (["and", "or", "not", "in"].indexOf(w) >= 0 ? w : "id"), v: w }); |
51 | 55 | i = j; continue; |
52 | 56 | } |
|
95 | 99 | if (w === "false") return false; |
96 | 100 | if (w === "None") return ""; |
97 | 101 | if (Object.prototype.hasOwnProperty.call(vars, w)) return vars[w].value; |
98 | | - return ""; // 未知裸标识符当空串处理(v0.01 约定) |
| 102 | + return ""; // 未知裸标识符当空串处理 |
99 | 103 | } |
100 | 104 | return ""; |
101 | 105 | } |
102 | | - function factor() { return primary(); } |
103 | | - function term() { |
104 | | - let v = factor(); |
105 | | - while (peek() && peek().t === "+") { next(); v = Number(v) + Number(factor()); } |
| 106 | + function unary() { |
| 107 | + if (peek() && peek().t === "-") { next(); return -Number(unary()); } |
| 108 | + return primary(); |
| 109 | + } |
| 110 | + function mulDiv() { |
| 111 | + let v = unary(); |
| 112 | + while (peek() && (peek().t === "*" || peek().t === "/" || peek().t === "%")) { |
| 113 | + const op = next().t; const r = unary(); |
| 114 | + const a = Number(v), b = Number(r); |
| 115 | + v = op === "*" ? a * b : (op === "/" ? a / b : a % b); |
| 116 | + } |
| 117 | + return v; |
| 118 | + } |
| 119 | + function addSub() { |
| 120 | + let v = mulDiv(); |
| 121 | + while (peek() && (peek().t === "+" || peek().t === "-")) { |
| 122 | + const op = next().t; const r = mulDiv(); |
| 123 | + v = Number(v) + (op === "+" ? Number(r) : -Number(r)); |
| 124 | + } |
106 | 125 | return v; |
107 | 126 | } |
108 | 127 | function cmp() { |
109 | | - let l = term(); |
| 128 | + let l = addSub(); |
110 | 129 | while (peek() && ["==", "!=", "<", ">", "<=", ">=", "in"].indexOf(peek().t) >= 0) { |
111 | | - const op = next().t; const r = term(); l = applyCmp(op, l, r); |
| 130 | + const op = next().t; const r = addSub(); l = applyCmp(op, l, r); |
112 | 131 | } |
113 | 132 | return l; |
114 | 133 | } |
115 | 134 | function notExpr() { if (peek() && peek().t === "not") { next(); return !truthy(notExpr()); } return cmp(); } |
116 | 135 | function andExpr() { let v = notExpr(); while (peek() && peek().t === "and") { next(); v = truthy(v) && truthy(notExpr()); } return v; } |
117 | 136 | function orExpr() { let v = andExpr(); while (peek() && peek().t === "or") { next(); v = truthy(v) || truthy(andExpr()); } return v; } |
118 | | - try { return truthy(orExpr()); } catch (e) { return false; } |
| 137 | + try { return orExpr(); } catch (e) { return false; } |
119 | 138 | } |
120 | 139 |
|
121 | 140 | /* ---------------- 判断头解析 ---------------- */ |
|
136 | 155 | function classify(line) { |
137 | 156 | // 返回 {kind, ...} |
138 | 157 | if (!line || line.trim() === "" ) return { kind: "blank" }; |
139 | | - // it 变量声明 |
140 | | - const itm = line.match(/^it\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s+([A-Za-z0-9_]+))?(?:\s+(.*))?$/); |
| 158 | + // it 变量声明(支持中文变量名) |
| 159 | + const itm = line.match(/^it\s+([A-Za-z_\u4e00-\u9fff][A-Za-z0-9_\u4e00-\u9fff]*)(?:\s+([A-Za-z0-9_]+))?(?:\s+(.*))?$/); |
141 | 160 | if (itm) { |
142 | 161 | const name = itm[1]; |
143 | 162 | let type = itm[2] || ""; |
|
150 | 169 | if (type !== "t" && type !== "p" && type) type = "ordinary"; // int/str... 归普通 |
151 | 170 | return { kind: "it", name, vtype: kind, type, value }; |
152 | 171 | } |
153 | | - // in 展示:name in t / name in p (坐标在本版本不启用,默认居中) |
154 | | - const inm = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s+in\s+(t|p|text|img)\b/i); |
| 172 | + // in 展示:name in t 标题 / in s 副标题 / in b 正文 / in p 图片 / in link 链接 |
| 173 | + const inm = line.match(/^([A-Za-z_\u4e00-\u9fff][A-Za-z0-9_\u4e00-\u9fff]*)\s+in\s+(t|p|s|b|text|img|sub|body|link)\b/i); |
155 | 174 | if (inm) { |
156 | 175 | const mode = inm[2].toLowerCase(); |
157 | | - return { kind: "in", name: inm[1], mode: (mode === "p" || mode === "img") ? "img" : "text" }; |
| 176 | + const map = { t: "text", text: "text", p: "img", img: "img", s: "sub", sub: "sub", b: "body", body: "body", link: "link" }; |
| 177 | + return { kind: "in", name: inm[1], mode: map[mode] || "text" }; |
158 | 178 | } |
159 | 179 | const cond = parseCondHeader(line); |
160 | 180 | if (cond) return { kind: "cond", ...cond }; |
|
255 | 275 | nodes.forEach(function (n) { |
256 | 276 | if (n.kind === "it") { |
257 | 277 | if (!vars[n.name]) { |
258 | | - vars[n.name] = { type: n.vtype || "ordinary", value: n.value || "", isImg: (n.vtype === "img") }; |
| 278 | + let val = n.value || ""; |
| 279 | + // 数值变量:普通变量的值为纯数字时存为 number,便于算术与数值比较 |
| 280 | + if (n.vtype !== "text" && n.vtype !== "img" && val !== "" && !isNaN(Number(val))) { |
| 281 | + val = Number(val); |
| 282 | + } |
| 283 | + vars[n.name] = { type: n.vtype || "ordinary", value: val, isImg: (n.vtype === "img") }; |
259 | 284 | } |
260 | 285 | } else if (n.kind === "cond") { |
261 | 286 | collectVars(n.body, vars); |
|
321 | 346 | " background:transparent;color:#f2e9da;cursor:pointer;text-decoration:none;font-size:13px;}", |
322 | 347 | ".hic-bar button:hover,.hic-bar a:hover{border-color:#d9ae6b;color:#d9ae6b;}", |
323 | 348 | ".hic-bar .on{border-color:#d9ae6b;color:#15100b;background:#d9ae6b;font-weight:600;}", |
324 | | - ".hic-item{max-width:860px;margin:40px auto;padding:0 20px;text-align:center;}", |
| 349 | + ".hic-item{max-width:860px;margin:34px auto;padding:0 20px;text-align:center;}", |
325 | 350 | ".hic-text{font-size:42px;font-weight:800;letter-spacing:.5px;line-height:1.3;}", |
326 | | - ".hic-sub{font-size:20px;color:#cbb995;margin-top:8px;}", |
| 351 | + ".hic-sub{font-size:24px;font-weight:600;color:#cbb995;margin-top:10px;}", |
| 352 | + ".hic-body{max-width:640px;text-align:left;font-size:16px;color:#cfc3ab;line-height:1.9;}", |
| 353 | + ".hic-linkwrap{text-align:center;}", |
| 354 | + ".hic-href{display:inline-block;margin-top:8px;padding:12px 26px;border-radius:999px;border:1px solid rgba(217,174,107,.5);color:#d9ae6b;font-size:15px;text-decoration:none;transition:background .2s,color .2s;}", |
| 355 | + ".hic-href:hover{background:#d9ae6b;color:#15100b;}", |
327 | 356 | ".hic-link{margin-top:14px;font-size:14px;color:#8d7f63;}", |
328 | 357 | ".hic-imgwrap{max-width:860px;margin:40px auto;padding:0 20px;text-align:center;}", |
329 | 358 | ".hic-img{max-width:100%;max-height:70vh;border-radius:16px;box-shadow:0 18px 50px rgba(0,0,0,.4);}", |
|
340 | 369 | if (it.kind === "display" && it.mode === "text") { |
341 | 370 | return '<p class="hic-item hic-text">' + esc(it.value) + "</p>"; |
342 | 371 | } |
| 372 | + if (it.kind === "display" && it.mode === "sub") { |
| 373 | + return '<p class="hic-item hic-sub">' + esc(it.value) + "</p>"; |
| 374 | + } |
| 375 | + if (it.kind === "display" && it.mode === "body") { |
| 376 | + return '<p class="hic-item hic-body">' + esc(it.value) + "</p>"; |
| 377 | + } |
| 378 | + if (it.kind === "display" && it.mode === "link") { |
| 379 | + const href = it.value ? String(it.value) : "#"; |
| 380 | + return '<div class="hic-item hic-linkwrap"><a class="hic-href" href="' + esc(href) + |
| 381 | + '" target="_blank" rel="noopener">' + esc(it.value) + "</a></div>"; |
| 382 | + } |
343 | 383 | if (it.kind === "display" && it.mode === "img") { |
344 | 384 | const src = it.value ? esc(it.value) : ""; |
345 | 385 | return '<div class="hic-item hic-imgwrap"><img class="hic-img' + (src ? "" : " hic-void") + '" src="' + src + |
|
357 | 397 | (href.sameDoc ? ' data-hc-goto="' + esc(href.pageKey) + '"' : "") + ">前往 " + esc(href.label || label) + " 页面 →</a></div>"; |
358 | 398 | } |
359 | 399 | if (it.kind === "textline") { |
360 | | - return '<p class="hic-item hic-sub">' + esc(it.text) + "</p>"; |
| 400 | + return '<p class="hic-item hic-body">' + esc(it.text) + "</p>"; |
361 | 401 | } |
362 | 402 | return ""; |
363 | 403 | }).join("\n"); |
|
0 commit comments