const zlib = require("node:zlib"); const quantity = require("./native-quantity.cjs"); const same = (a, b) => JSON.stringify(a) === JSON.stringify(b); function escapeText(text) { return [...text] .map((c) => { if (c === "&") return "&"; if (c === "<") return "<"; if (c === ">") return ">"; const code = c.codePointAt(0); return code > 126 ? `&#${code};` : c; }) .join(""); } // Change text inside an existing styled run, never WPTools tags or bookmarks. function editDocument(text, oldSheet, sheet) { if ( !same({ ...oldSheet, rows: [] }, { ...sheet, rows: [] }) || oldSheet.rows.length !== sheet.rows.length ) throw Error("수량산출서 목록·연결·행 구조 변경은 ESTX 저장을 지원하지 않습니다."); const declarations = [...text.matchAll(/<(?:cs|begin)\b[^>]*>/gi)].map((m) => m[0]).join(""); const parse = (div) => quantity.fromWpt("", "", declarations + div, [], {}).rows; let rowIndex = 0; const result = text.replace(/]*>[\s\S]*?<\/div>/gi, (div) => { const parsed = parse(div); if (!parsed.length) return div; const before = oldSheet.rows[rowIndex], after = sheet.rows[rowIndex++]; if (!before || !after || !same(parsed[0], before)) throw Error("수량산출서 원본 행 대응 오류"); if (same(before, after)) return div; if (!same(before.markers, after.markers)) throw Error("수량산출서 표지 변경은 저장할 수 없습니다."); for (const match of div.matchAll(/>([^<>]+)(?=<)/g)) { const raw = match[1], decoded = quantity.entities(raw.replace(/\r?\n/g, "")); if (!decoded) continue; let position = -1; while ((position = before.LineData.indexOf(decoded, position + 1)) >= 0) { const prefix = before.LineData.slice(0, position), suffix = before.LineData.slice(position + decoded.length); if ( !after.LineData.startsWith(prefix) || !after.LineData.endsWith(suffix) || after.LineData.length < prefix.length + suffix.length ) continue; const replacement = after.LineData.slice( prefix.length, after.LineData.length - suffix.length, ); const at = match.index + 1; const candidate = div.slice(0, at) + escapeText(replacement) + div.slice(at + raw.length); if (same(parse(candidate)[0], after)) return candidate; } } throw Error( `수량산출서 ${rowIndex}행: 기존 서식 구간을 넘는 편집은 ESTX에 저장할 수 없습니다.`, ); }); if (rowIndex !== oldSheet.rows.length) throw Error("수량산출서 행 수 불일치"); if (!same(quantity.fromWpt(sheet.Code, sheet.Name, result, sheet.items, sheet.options), sheet)) throw Error("수량산출서 문서 재읽기 검산 실패"); return result; } function updateQuantities(project, base, cp949) { const sheets = project.quantitySheets || [], originals = base.quantitySheets || []; if (sheets.length !== originals.length) throw Error("수량산출서 추가·삭제는 지원하지 않습니다."); const rows = project.tables.MulMainDat.map((r) => ({ ...r })); for (let i = 0; i < sheets.length; i++) { if (same(sheets[i], originals[i])) continue; const row = rows.find((r) => r.Code === originals[i].Code); const oldRow = base.tables.MulMainDat.find((r) => r.Code === originals[i].Code); if (!row?.Editor?.binary || !same(row.Editor, oldRow.Editor)) throw Error("수량산출서 원본 편집기 데이터 충돌"); const packed = Buffer.from(row.Editor.binary, "base64"), compressed = packed[0] === 0x78; const bytes = compressed ? zlib.inflateSync(packed, { maxOutputLength: 256 * 1024 * 1024 }) : packed; const text = new TextDecoder("euc-kr", { fatal: true }).decode(bytes); if (!cp949(text).equals(bytes)) throw Error("수량산출서 원본 인코딩 보존 불가"); const output = cp949(editDocument(text, originals[i], sheets[i])); row.Editor = { binary: (compressed ? zlib.deflateSync(output) : output).toString("base64") }; } return { ...project, tables: { ...project.tables, MulMainDat: rows } }; } module.exports = { editDocument, updateQuantities };