- 단위 규칙 한 벌(common_util_line_stack_unit) — 왼쪽 값 칸 · 식 줄 · 옵션 · 오른쪽 산출근거 · 기계 제어 구역 · 기계 보기가 같이 씀 · 메타 단위(구분 따름 · 수(…) · 원문 단위 없음) 안 그림 · %(설명) = % · 10^-7 = 10⁻⁷ · 기계 칸 연결 = 기계 표 칸 단위 · 코드 값 쉼표 없음 - 비목 · 금액 줄 = 「단가 원/단위 × (수량 식) 단위 = 금액 원」 · 소계 · 합계 · 계 = 원 · 단가 · 수량 한 덩이(줄 바뀜에 단위 안 떨어짐) - 기계경비식 겹분수 → 중간 줄(상여 · 휴지 · 손료계수 · common_util_line_stack_frac) · 한 줄 분수 한 층 - 기계 제어 구역 단위 = 칸 밖 바로 뒤 · 기계 보기 오른쪽 = 박기 전 기계 칸 단위 - 휴대폰 폭(560 px 밑) 산출근거 금액 칸 넷 걷음(가로 넘침 0) · 분수 줄 위아래 여유 - 전수 1,520 표시 탓 43,218 → 0 · 기계 겹분수 2 층 넘는 줄 1,040 → 0 · 금액 · 식 · 문서 자료 불변(그리기만) - 시험 test_845_2_unit_show(차린 줄 글 전수 지킴) · shownUnit 걷음(규칙 한 벌로) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
5.9 KiB
TypeScript
156 lines
5.9 KiB
TypeScript
/* =============================================================================
|
|
* 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 { decide, hasIf, type Values } from "../common_util/common_util_formula_doc_view";
|
|
|
|
export interface TexCtx {
|
|
side: "sym" | "val";
|
|
vals: Values;
|
|
inline?: boolean;
|
|
/** 묶음 값의 비목(기계 · 단가산출 줄 노 · 재 · 경) */
|
|
cost?: string;
|
|
/** 번지 읽는 이름(804-5) — 「번지.복사N.노무비」 → 「항목 이름 노무비」 */
|
|
place?: Record<string, string>;
|
|
/** 표 값 기호 화면 이름(요구 683 — 「Q_수량2^표」 → 「Q_작업반장」) */
|
|
alias?: Record<string, string>;
|
|
/** 이미 결정된 나무(다시 안 접음) */
|
|
decided?: boolean;
|
|
/** 남은 분수 층(845-2 — 0 이면 안쪽 ÷ 는 한 줄 · 없음 = 끝없음) */
|
|
frac?: number;
|
|
}
|
|
|
|
const HIDE = new Set(["반올림", "올림", "버림"]);
|
|
const OPS: Record<string, string> = {
|
|
"+": "+",
|
|
"-": "-",
|
|
"*": "\\times",
|
|
"/": "\\div",
|
|
"<": "<",
|
|
"<=": "\\le",
|
|
">": ">",
|
|
">=": "\\ge",
|
|
"==": "=",
|
|
"!=": "\\ne",
|
|
그리고: "\\;\\text{그리고}\\;",
|
|
또는: "\\;\\text{또는}\\;",
|
|
};
|
|
const PREC: Record<string, number> = {
|
|
또는: 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);
|
|
};
|
|
|
|
/** 칸 값 — `번지.복사1.노무비` 처럼 겹친 칸까지 */
|
|
const look = (n: Node, vals: Values): unknown => {
|
|
if (n[0] === "var") return vals[n[1]];
|
|
if (n[0] !== "field") return undefined;
|
|
const v = look(n[1], vals);
|
|
return v && typeof v === "object" ? (v as Values)[n[2]] : undefined;
|
|
};
|
|
|
|
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 {
|
|
// 만약 = 탄 갈래만 · 만약이 낳은 0 · 1 접음 — 왼쪽 결정된 식과 같은 `decide` 한 벌(요구 695)
|
|
if (!c.decided && hasIf(n)) return tex(decide(n, c.vals), { ...c, decided: true });
|
|
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": {
|
|
const id = c.alias?.[n[1]] ?? n[1];
|
|
if (c.side === "sym") return symTex(id);
|
|
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(id)) : valTex(v);
|
|
}
|
|
case "field": {
|
|
const [, of, key] = n;
|
|
if (c.side === "val") {
|
|
const got = look(n, c.vals);
|
|
if (isNum(got)) return valTex(got);
|
|
}
|
|
if (of[0] === "field" && of[1][0] === "var" && of[1][1] === "번지" && c.place?.[of[2]])
|
|
return txt(`${c.place[of[2]]} ${key}`);
|
|
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 (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;
|
|
if (o === "/" && !c.inline && c.frac !== 0) {
|
|
const d = { ...c, frac: c.frac === undefined ? undefined : c.frac - 1 };
|
|
return `\\dfrac{${tex(a, d)}}{${tex(b, d)}}`;
|
|
}
|
|
const mine = PREC[o];
|
|
// 845-2 층 상한(기계경비식) — 분수로 그리는 오른쪽 항은 괄호 없이(분수 선이 묶음)
|
|
const frac = (x: Node): boolean =>
|
|
c.frac !== undefined && c.frac > 0 && !c.inline && x[0] === "bin" && x[1] === "/";
|
|
const left = prec(a) < mine || (mine === 3 && prec(a) === 3) ? paren(go(a)) : go(a);
|
|
const right = prec(b) <= mine && !frac(b) ? 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;
|
|
}
|