/* ============================================================================= * line_stack_tex.ts * 줄 쌓기 산출근거 수식 조판(PLAN 804-2 · 요구 677) — 식 나무 → TeX → KaTeX(MIT) · 분수 · 첨자 · 괄호 * 기호 식(side "sym") = 기호(첨자) · 값 식(side "val") = 값(모르면 기호 빨강) · 만약 = 탄 갈래만 · 반올림 · 올림 · 버림 감싸개 숨김 * 금액 줄 꼴(`inline`) = ÷ 한 줄(경쟁사 「노무비 : 59,020 ÷ Q」) · 공식 줄 = 분수 * 규칙은 수식 탭 그림(`formula_doc_render.ts` renderNode)과 같음 — 셈 정본은 계산기 * ========================================================================== */ import katex from "katex"; import "../config/node_modules/katex/dist/katex.min.css"; import { eqText } from "@ui/ui_template_equation"; import { splitId, type Node } from "../common_util/common_util_formula_doc"; import { isNum } from "../common_util/common_util_formula_doc_calc"; import { leafNum, takenOf, type Values } from "../common_util/common_util_formula_doc_view"; export interface TexCtx { side: "sym" | "val"; vals: Values; inline?: boolean; /** 묶음 값의 비목(기계 · 단가산출 줄 노 · 재 · 경) */ cost?: string; } const HIDE = new Set(["반올림", "올림", "버림"]); const OPS: Record = { "+": "+", "-": "-", "*": "\\times", "/": "\\div", "<": "<", "<=": "\\le", ">": ">", ">=": "\\ge", "==": "=", "!=": "\\ne", 그리고: "\\;\\text{그리고}\\;", 또는: "\\;\\text{또는}\\;", }; const PREC: Record = { 또는: 1, 그리고: 2, "<": 3, "<=": 3, ">": 3, ">=": 3, "==": 3, "!=": 3, "+": 4, "-": 4, "*": 5, "/": 5, }; const prec = (n: Node): number => (n[0] === "bin" ? PREC[n[1]] : n[0] === "neg" ? 6 : 9); /** TeX 글 안전 — \text{} 안 */ const txt = (s: string): string => `\\text{${s.replace(/[\\{}$&#^_%~]/g, (c) => `\\${c}`)}}`; const word = (s: string): string => /^[A-Za-z]$/.test(s) ? s : /^[A-Za-z]+$/.test(s) ? `\\mathit{${s}}` : txt(s); /** 기호 글(`Q_작업^표`) → TeX(첨자) */ export function symTex(id: string): string { const [s, lo, up] = splitId(id); return `${word(s)}${lo ? `_{${word(lo)}}` : ""}${up ? `^{${word(up)}}` : ""}`; } /** 값 → TeX(천 자리 쉼표) */ export const valTex = (v: unknown): string => { const t = eqText(v); return isNum(v) ? t.replace(/,/g, "{,}") : txt(t); }; const paren = (t: string): string => `\\left(${t}\\right)`; const miss = (t: string): string => `\\htmlClass{lstk-miss}{${t}}`; export function tex(n: Node, c: TexCtx): string { const go = (x: Node): string => tex(x, c); switch (n[0]) { case "num": return valTex(n[1]); case "str": return txt(`'${n[1]}'`); case "var": { if (c.side === "sym") return symTex(n[1]); let v = c.vals[n[1]]; if (v && typeof v === "object" && c.cost) v = (v as Values)[c.cost]; return v === null || v === undefined || typeof v === "object" ? miss(symTex(n[1])) : valTex(v); } case "field": { if (c.side === "val" && n[1][0] === "var") { const v = c.vals[n[1][1]]; const got = v && typeof v === "object" ? (v as Values)[n[2]] : undefined; if (isNum(got)) return valTex(got); } return `${n[1][0] === "var" ? symTex(n[1][1]) : go(n[1])}.${txt(n[2])}`; } case "neg": return `-${n[1][0] === "bin" ? paren(go(n[1])) : go(n[1])}`; case "pct": return `${prec(n[1]) < 9 ? paren(go(n[1])) : go(n[1])}\\,\\%`; case "call": { const [, name, items] = n; if (name === "만약") { const pick = takenOf(n, c.vals); if (pick) return go(items[pick]); } if (HIDE.has(name) && items.length >= 1 && items.length <= 2) return go(items[0]); const fn = name === "최소" ? "\\min" : name === "최대" ? "\\max" : txt(name); return `${fn}${paren(items.map(go).join(",\\,"))}`; } case "bin": { const [, o, a, b] = n; const na = leafNum(a, c.vals); const nb = leafNum(b, c.vals); if ((o === "+" || o === "-") && nb === 0) return go(a); if ((o === "*" || o === "/") && nb === 1) return go(a); if ((o === "+" && na === 0) || (o === "*" && na === 1)) return go(b); if (o === "/" && !c.inline) return `\\dfrac{${go(a)}}{${go(b)}}`; const mine = PREC[o]; const left = prec(a) < mine || (mine === 3 && prec(a) === 3) ? paren(go(a)) : go(a); const right = prec(b) <= mine ? paren(go(b)) : go(b); return `${left} ${OPS[o] ?? txt(o)} ${right}`; } } } /** TeX → 조판 요소(못 그리면 글 그대로) */ export function katexNode(t: string): HTMLElement { const box = document.createElement("span"); box.className = "lstk-tex"; katex.render(t, box, { throwOnError: false, strict: "ignore", trust: (x) => x.command === "\\htmlClass", output: "html", }); return box; }