/* ============================================================================= * spreadsheet_cellstyle.ts (주인 C) * 서식 → CSS — 글꼴 · 정렬(균등 분할 · 선택 영역 가운데 · 세로 · 줄 바꿈) · 채움색 · * 테두리(hair · thin · medium · thick · double · dotted · dashed · 색) · 겹친 테두리 고르기. * 칸 하나가 위 · 왼 이웃과 테두리를 나눠 가짐(표 `border-collapse` 방식) — 겹쳐 그리지 않게 * 위 · 왼만 두 이웃 중 굵은 쪽으로 그리고 오른 · 아래는 시트 끝 칸만 그림(`resolveEdgeBorders`). * 0 계약 머리 — 몸은 C 가 채움. 계약은 `spreadsheet_types.ts`. * ========================================================================== */ import { colName, toA1 } from "./spreadsheet_address"; import type { BorderLine, BorderSide, CellStyle, HAlign, Sheet, VAlign, Workbook, } from "./spreadsheet_types"; /** 선 굵기 차례 — 겹치면 굵은 쪽이 이김(엑셀과 완전히 같지 않음 · 근사). */ const BORDER_WEIGHT: Record = { hair: 0, dotted: 1, dashed: 2, thin: 3, double: 4, medium: 5, thick: 6, }; /** 두 이웃 칸이 한 자리에 그리려는 테두리 중 굵은 쪽. 없으면 있는 쪽 · 둘 다 없으면 undefined. */ export function strongerBorder(a?: BorderSide, b?: BorderSide): BorderSide | undefined { if (!a) return b; if (!b) return a; return BORDER_WEIGHT[b.선] > BORDER_WEIGHT[a.선] ? b : a; } /** 칸 하나가 실제로 그릴 네 변 — 위 · 왼은 이웃과 겹쳐 고름 · 오른 · 아래는 시트 끝(그 다음 칸이 없음)일 때만. */ export interface ResolvedBorders { 위?: BorderSide; 아래?: BorderSide; 왼?: BorderSide; 오른?: BorderSide; } /** `서식` 표 번호 — 칸 → 행 → 열 → 시트 `기본.서식` → 0(기본) 차례(칸 안의 계약 주석과 같음). */ export function styleIndexAt(sheet: Sheet, r: number, c: number, key: string): number { const cell = sheet.칸[key]; if (cell?.서식 !== undefined) return cell.서식; const rowFmt = sheet.행?.[String(r + 1)]?.서식; if (rowFmt !== undefined) return rowFmt; const colFmt = sheet.열?.[colName(c)]?.서식; if (colFmt !== undefined) return colFmt; if (sheet.기본?.서식 !== undefined) return sheet.기본.서식; return 0; } /** (r, c) 칸이 쓸 서식 — 서식 표 밖 번호는 기본(빈 `{}`)으로. */ export function cellStyleAt(book: Workbook, sheet: Sheet, r: number, c: number): CellStyle { const key = toA1(r, c); const idx = styleIndexAt(sheet, r, c, key); return book.서식[idx] ?? {}; } /** 칸 하나 · 병합 칸 한 덩이(box)의 테두리 — 네 변마다 안쪽 이웃과 겹쳐 굵은 쪽으로 고름. * 병합이면 왼위(r0,c0) · 오른아래(r1,c1) 이 다른 칸이라 네 변을 따로 봄(테두리는 칸마다 있음). */ export function resolveBoxBorders( book: Workbook, sheet: Sheet, r0: number, c0: number, r1: number, c1: number, lastRow: number, lastCol: number, ): ResolvedBorders { const top = cellStyleAt(book, sheet, r0, c0).테두리?.위; const aboveOf = r0 > 0 ? cellStyleAt(book, sheet, r0 - 1, c0).테두리?.아래 : undefined; const left = cellStyleAt(book, sheet, r0, c0).테두리?.왼; const leftOf = c0 > 0 ? cellStyleAt(book, sheet, r0, c0 - 1).테두리?.오른 : undefined; const bottomOwn = cellStyleAt(book, sheet, r1, c0).테두리?.아래; const belowOf = r1 < lastRow ? cellStyleAt(book, sheet, r1 + 1, c0).테두리?.위 : undefined; const rightOwn = cellStyleAt(book, sheet, r0, c1).테두리?.오른; const rightOf = c1 < lastCol ? cellStyleAt(book, sheet, r0, c1 + 1).테두리?.왼 : undefined; return { 위: strongerBorder(top, aboveOf), 왼: strongerBorder(left, leftOf), 아래: r1 >= lastRow ? bottomOwn : strongerBorder(bottomOwn, belowOf), 오른: c1 >= lastCol ? rightOwn : strongerBorder(rightOwn, rightOf), }; } /** 색 글을 비교용으로 정리 — 공백 · 대소문자 · `#` 3자리 · ARGB(`#FF000000`) · `rgb(0,0,0)` 를 `#rrggbb` 로. */ function normalizeColor(c: string): string { const t = c.trim().toLowerCase().replace(/\s+/g, ""); const rgb = /^rgba?\((\d+),(\d+),(\d+)(?:,1(?:\.0+)?)?\)$/.exec(t); if (rgb) return `#${[rgb[1], rgb[2], rgb[3]].map((n) => Number(n).toString(16).padStart(2, "0")).join("")}`; if (/^#[0-9a-f]{3}$/.test(t)) return `#${t[1]}${t[1]}${t[2]}${t[2]}${t[3]}${t[3]}`; if (/^#ff[0-9a-f]{6}$/.test(t)) return `#${t.slice(3)}`; return t; } /** 엑셀 「자동」 글자색 · 테두리색 — 검정 · windowtext · auto 는 테마 글자색을 따름(저장 값은 그대로 · 그릴 때만). */ export function isAutoInk(c: string | undefined): boolean { if (!c) return true; const n = normalizeColor(c); return n === "#000000" || n === "black" || n === "windowtext" || n === "auto"; } /** 엑셀 「자동」 채움 — 흰색 · window 는 채움 없음처럼(테마 바탕이 비침). */ export function isAutoPaper(c: string | undefined): boolean { if (!c) return true; const n = normalizeColor(c); return n === "#ffffff" || n === "white" || n === "window"; } /** 빈 글 = 인라인 테두리를 안 그림(css `aislo-grid--gridlines` 눈금선이 비쳐 보임). `scale` = 확대 · 축소 배율(선 굵기도 같이). */ function borderCss(side: BorderSide | undefined, scale: number): string { if (!side) return ""; const color = isAutoInk(side.색) ? "var(--aislo-grid-fg)" : (side.색 as string); const w = (px: number): number => Math.max(1, Math.round(px * scale)); switch (side.선) { case "hair": return `${w(1)}px dotted ${color}`; case "thin": return `${w(1)}px solid ${color}`; case "medium": return `${w(2)}px solid ${color}`; case "thick": return `${w(3)}px solid ${color}`; case "double": return `${w(3)}px double ${color}`; case "dotted": return `${w(1)}px dotted ${color}`; case "dashed": return `${w(1)}px dashed ${color}`; } } /** 가로 정렬 → `text-align`(균등 분할 · 선택 영역 가운데는 근사: `justify` · `center`). */ const HALIGN_CSS: Record = { general: "left", left: "left", right: "right", center: "center", distributed: "justify", centerContinuous: "center", }; const VALIGN_CSS: Record = { top: "flex-start", center: "center", bottom: "flex-end", }; /** 칸 하나에 서식 · 겹쳐 고른 테두리를 입힘. `hAlign` 은 부른 쪽이 정함(일반 정렬은 값 종류로 갈림 — B `formatValue` 의 `정렬`). * `scale` = 확대 · 축소 배율(칸 글자 크기 · 테두리 굵기가 같이 커짐 · 기본 1). */ export function applyCellStyle( cell: HTMLElement, style: CellStyle, borders: ResolvedBorders, hAlign: HAlign, scale = 1, ): void { const s = cell.style; s.fontFamily = style.글꼴 ?? ""; s.fontSize = style.크기 ? `${style.크기 * scale}pt` : ""; s.fontWeight = style.굵게 ? "700" : "400"; s.fontStyle = style.기울임 ? "italic" : "normal"; s.textDecorationLine = [style.밑줄 && "underline", style.취소선 && "line-through"] .filter(Boolean) .join(" "); s.color = isAutoInk(style.글자색) ? "" : (style.글자색 as string); s.backgroundColor = isAutoPaper(style.채움) ? "" : (style.채움 as string); s.borderTop = borderCss(borders.위, scale); s.borderLeft = borderCss(borders.왼, scale); s.borderBottom = borderCss(borders.아래, scale); s.borderRight = borderCss(borders.오른, scale); s.justifyContent = VALIGN_CSS[style.세로 ?? "bottom"]; s.whiteSpace = style.줄바꿈 ? "normal" : "nowrap"; let text = cell.firstElementChild as HTMLElement | null; if (!text || !text.classList.contains("aislo-cell__text")) { text = document.createElement("div"); text.className = "aislo-cell__text"; cell.replaceChildren(text); } text.style.textAlign = HALIGN_CSS[hAlign]; text.style.textAlignLast = hAlign === "distributed" ? "justify" : ""; } /** 칸 글 — 서식 CSS 와 따로 두어 `invalidate()` 가 값만 바꿀 때 서식을 다시 잴 필요가 없게. */ export function setCellText(cell: HTMLElement, text: string, title?: string): void { const node = cell.firstElementChild as HTMLElement | null; if (node && node.classList.contains("aislo-cell__text")) { node.textContent = text; } else { cell.textContent = text; } if (title) cell.title = title; else cell.removeAttribute("title"); }