Files
Aislo/ui_template/sheet/ui_template_sheet_labels.ts
T

104 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* ui_template_sheet_labels.ts
* 사람이 읽는 글 — 식의 열 id 를 열 이름(머리 글)으로 · 머리 글이 안 잘리는 최소 열 폭.
*
* 열 이름 = 맨 위 글(종류) + 맨 아래 글 · 다른 열과 겹치면 가운데 층을 더 붙임(자리 글 「(…마다)」 는 뺌).
* 보이기만 바꿈 — 문서의 식은 그대로 id 식(고칠 때도 id 식).
* ========================================================================== */
import { layoutHead } from "./ui_template_sheet_header";
import type { SheetColumn } from "./ui_template_sheet_types";
const PLACEHOLDER = /[((][^))]*마다[))]/;
const NAME_PAD = 16;
const MAX_MIN_WIDTH = 220;
export interface ColumnName {
/** 맨 위 글(종류) + 아래 글 — 표 전체에서 안 겹침. */
full: string;
/** 같은 종류 안에서 부를 이름 — 겹치면 `full`. */
short: string;
top: string;
}
/** 열 id → 열 이름. */
export function columnNames(cols: SheetColumn[]): Map<string, ColumnName> {
const paths = cols.map((c) => {
const path = c.머리.filter((label): label is string => !!label && !PLACEHOLDER.test(label));
return path.length ? path : [c.id];
});
/** 맨 위 + 아래서 `take` 층 — 모자라면 통째. */
const nameOf = (path: string[], take: number): string =>
(take + 1 >= path.length ? path : [path[0], ...path.slice(-take)]).join(" ");
const names = new Map<string, ColumnName>();
cols.forEach((col, j) => {
const path = paths[j];
let full = path.join(" ");
for (let take = 1; take + 1 < path.length; take += 1) {
const name = nameOf(path, take);
if (!paths.some((other, k) => k !== j && nameOf(other, take) === name)) {
full = name;
break;
}
}
const leaf = path.length > 1 ? path.slice(1).join(" ") : path[0];
const clash = paths.some(
(other, k) =>
k !== j &&
other[0] === path[0] &&
(other.length > 1 ? other.slice(1).join(" ") : other[0]) === leaf,
);
names.set(col.id, { full, short: clash ? full : leaf, top: path[0] });
});
return names;
}
/** 식 → 읽는 글 — `[열id]` → 열 이름 · `[열id@줄]` → 이름@줄 · `[$변수]` → 변수 이름 · `*` `/` → × ÷.
* `host` 를 주면 같은 종류 열은 짧은 이름. */
export function formulaLabel(
formula: string,
names: Map<string, ColumnName>,
host?: string,
): string {
const top = host ? names.get(host)?.top : undefined;
return formula
.replace(/\[([^\]]+)\]/g, (_, ref: string) => {
const text = ref.trim();
if (text.startsWith("$")) return text.slice(1).replace(/_/g, " ");
const [col, row] = text.split("@");
const hit = names.get(col.trim());
const name = !hit ? col.trim() : top !== undefined && hit.top === top ? hit.short : hit.full;
return row ? `${name}@${row.trim()}` : name;
})
.replace(/\s*\*\s*/g, " × ")
.replace(/\s*\/\s*/g, " ÷ ");
}
let ruler: CanvasRenderingContext2D | null = null;
export function textWidth(text: string, font: string): number {
ruler ??= document.createElement("canvas").getContext("2d");
if (!ruler) return text.length * 13;
ruler.font = font;
return ruler.measureText(text).width;
}
/** 열마다 머리 글 · 단위가 안 잘리는 최소 폭(px) — 여러 열을 덮는 칸은 모자란 만큼 나눠 얹음. */
export function headMinWidths(
cols: SheetColumn[],
depth: number,
font: string,
): Map<string, number> {
const mins = cols.map((c) => textWidth(c.단위 ?? "", font) + NAME_PAD);
for (const level of layoutHead(cols, depth).reverse()) {
for (const cell of level) {
const need = textWidth(cell.label, font) + NAME_PAD;
const have = mins.slice(cell.col, cell.col + cell.colspan).reduce((a, b) => a + b, 0);
if (need <= have) continue;
const extra = (need - have) / cell.colspan;
for (let k = cell.col; k < cell.col + cell.colspan; k += 1) mins[k] += extra;
}
}
return new Map(cols.map((c, j) => [c.id, Math.min(MAX_MIN_WIDTH, Math.ceil(mins[j]))]));
}