UNPKG

hwpx-ts

Version:

TypeScript library for reading and writing HWPX files

1,676 lines (1,665 loc) 53.3 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { A4_HEIGHT: () => A4_HEIGHT, A4_WIDTH: () => A4_WIDTH, HWPUNIT_PER_INCH: () => HWPUNIT_PER_INCH, HWPUNIT_PER_MM: () => HWPUNIT_PER_MM, HWPUNIT_PER_PT: () => HWPUNIT_PER_PT, HWPX_MIMETYPE: () => HWPX_MIMETYPE, HwpxEditor: () => HwpxEditor, HwpxExporter: () => HwpxExporter, HwpxReader: () => HwpxReader, NS: () => NS, createImageParagraph: () => createImageParagraph, createParagraphObject: () => createParagraphObject, createParagraphXml: () => createParagraphXml, createTableCell: () => createTableCell, createTableParagraph: () => createTableParagraph, guessMediaType: () => guessMediaType, hexColorToHwpx: () => hexColorToHwpx, hwpunitToMm: () => hwpunitToMm, hwpunitToPt: () => hwpunitToPt, mmToHwpunit: () => mmToHwpunit, ptToHwpunit: () => ptToHwpunit, readHwpxFile: () => readHwpxFile, readHwpxUrl: () => readHwpxUrl }); module.exports = __toCommonJS(index_exports); // src/utils/constants.ts var NS = { ha: "http://www.hancom.co.kr/hwpml/2011/app", hp: "http://www.hancom.co.kr/hwpml/2011/paragraph", hp10: "http://www.hancom.co.kr/hwpml/2016/paragraph", hs: "http://www.hancom.co.kr/hwpml/2011/section", hc: "http://www.hancom.co.kr/hwpml/2011/core", hh: "http://www.hancom.co.kr/hwpml/2011/head", hpf: "http://www.hancom.co.kr/schema/2011/hpf", opf: "http://www.idpf.org/2007/opf/" }; var HWPUNIT_PER_INCH = 1440; var HWPUNIT_PER_MM = 56.7; var HWPUNIT_PER_PT = 20; var A4_WIDTH = 59528; var A4_HEIGHT = 84188; var HWPX_MIMETYPE = "application/hwp+zip"; function mmToHwpunit(mm) { return Math.round(mm * HWPUNIT_PER_MM); } function hwpunitToMm(hwpunit) { return hwpunit / HWPUNIT_PER_MM; } function ptToHwpunit(pt) { return Math.round(pt * HWPUNIT_PER_PT); } function hwpunitToPt(hwpunit) { return hwpunit / HWPUNIT_PER_PT; } function guessMediaType(filename) { const lower = filename.toLowerCase(); if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; if (lower.endsWith(".png")) return "image/png"; if (lower.endsWith(".gif")) return "image/gif"; if (lower.endsWith(".bmp")) return "image/bmp"; return "application/octet-stream"; } function hexColorToHwpx(color) { if (color.startsWith("#")) return color; return `#${color}`; } // src/reader/HwpxReader.ts var import_jszip = __toESM(require("jszip")); var import_fast_xml_parser = require("fast-xml-parser"); var HwpxReader = class { constructor(data, options = {}) { this.headerXml = null; this.sectionXmls = []; this.binaryData = /* @__PURE__ */ new Map(); this.data = data instanceof ArrayBuffer ? new Uint8Array(data) : data; this.document = { blocks: [], binaryItems: [] }; this.parser = new import_fast_xml_parser.XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_", textNodeName: "#text", parseAttributeValue: true }); } /** * Parse the HWPX file */ async parse() { const zip = await import_jszip.default.loadAsync(this.data); const mimetypeFile = zip.file("mimetype"); if (mimetypeFile) { const mimetype = await mimetypeFile.async("string"); if (mimetype.trim() !== HWPX_MIMETYPE) { throw new Error(`Invalid HWPX mimetype: ${mimetype}`); } } const headerFile = zip.file("Contents/header.xml"); if (headerFile) { this.headerXml = await headerFile.async("string"); } let sectionIdx = 0; while (true) { const sectionFile = zip.file(`Contents/section${sectionIdx}.xml`); if (!sectionFile) break; const sectionXml = await sectionFile.async("string"); this.sectionXmls.push(sectionXml); this.parseSection(sectionXml); sectionIdx++; } const binDataFolder = zip.folder("BinData"); if (binDataFolder) { const files = binDataFolder.file(/.*/); for (const file of files) { const data = await file.async("uint8array"); const name = file.name.split("/").pop() || ""; const id = name.replace(/\.[^.]+$/, ""); this.binaryData.set(id, data); this.document.binaryItems.push({ id, href: `BinData/${name}`, mediaType: guessMediaType(name), data, isEmbedded: true }); } } return this.document; } /** * Parse a section XML */ parseSection(xml) { const parsed = this.parser.parse(xml); const section = parsed["hs:sec"] || parsed["sec"]; if (!section) return; this.parseParagraphs(section); } /** * Recursively find and parse paragraphs */ parseParagraphs(node, depth = 0) { if (!node || typeof node !== "object") return; if (node["hp:p"] || node["p"]) { const pElements = node["hp:p"] || node["p"]; const paragraphs = Array.isArray(pElements) ? pElements : [pElements]; for (const p of paragraphs) { const para = this.parseParagraph(p); this.document.blocks.push({ type: "paragraph", paragraph: para }); } } if (node["hp:tbl"] || node["tbl"]) { const tblElements = node["hp:tbl"] || node["tbl"]; const tables = Array.isArray(tblElements) ? tblElements : [tblElements]; for (const t of tables) { const table = this.parseTable(t); this.document.blocks.push({ type: "table", table }); } } for (const key of Object.keys(node)) { if (key.startsWith("@_") || key === "#text") continue; this.parseParagraphs(node[key], depth + 1); } } /** * Parse a paragraph element */ parseParagraph(p) { const inlines = []; const runs = this.findElements(p, ["hp:run", "run"]); for (const run of runs) { const textElements = this.findElements(run, ["hp:t", "t"]); for (const t of textElements) { const text = typeof t === "object" ? t["#text"] || "" : String(t); if (text) { const charPr = run["hp:charPr"] || run["charPr"] || {}; inlines.push({ text, bold: charPr["@_bold"] === 1 || charPr["@_bold"] === true, italic: charPr["@_italic"] === 1 || charPr["@_italic"] === true }); } } } const paraPr = p["hp:paraPr"] || p["paraPr"] || {}; const align = paraPr["hp:align"] || paraPr["align"] || {}; return { inlines, alignment: (align["@_horizontal"] || "left").toLowerCase() }; } /** * Parse a table element */ parseTable(t) { const rowCount = t["@_rowCnt"] || 0; const colCount = t["@_colCnt"] || 0; const cells = []; const rows = this.findElements(t, ["hp:tr", "tr"]); let rowIdx = 0; for (const tr of rows) { const tableCells = this.findElements(tr, ["hp:tc", "tc"]); let colIdx = 0; for (const tc of tableCells) { const cellBlocks = []; const subLists = this.findElements(tc, ["hp:subList", "subList"]); for (const subList of subLists) { const pElements = this.findElements(subList, ["hp:p", "p"]); for (const p of pElements) { const para = this.parseParagraph(p); cellBlocks.push({ type: "paragraph", paragraph: para }); } } if (cellBlocks.length === 0) { const pElements = this.findElements(tc, ["hp:p", "p"]); for (const p of pElements) { const para = this.parseParagraph(p); cellBlocks.push({ type: "paragraph", paragraph: para }); } } const cellSpan = this.findElements(tc, ["hp:cellSpan", "cellSpan"])[0]; const colSpan = cellSpan?.["@_colSpan"] || 1; const rowSpan = cellSpan?.["@_rowSpan"] || 1; const cellAddr = this.findElements(tc, ["hp:cellAddr", "cellAddr"])[0]; const col = cellAddr?.["@_colAddr"] ?? colIdx; const row = cellAddr?.["@_rowAddr"] ?? rowIdx; cells.push({ row, col, rowSpan, colSpan, blocks: cellBlocks }); colIdx++; } rowIdx++; } return { rowCount, colCount, cells }; } /** * Find elements by tag names */ findElements(node, tagNames) { if (!node || typeof node !== "object") return []; const results = []; for (const tag of tagNames) { if (node[tag]) { const elements = node[tag]; if (Array.isArray(elements)) { results.push(...elements); } else { results.push(elements); } } } return results; } // ========== Public API ========== /** * Get full text content */ getText() { const texts = []; for (const block of this.document.blocks) { if (block.paragraph) { for (const inline of block.paragraph.inlines) { if ("text" in inline) { texts.push(inline.text); } } texts.push("\n"); } } return texts.join(""); } /** * Get all paragraphs */ getParagraphs() { return this.document.blocks.filter((b) => b.paragraph).map((b) => b.paragraph); } /** * Get paragraph by index */ getParagraph(index) { const paragraphs = this.getParagraphs(); return paragraphs[index] || null; } /** * Get paragraph text by index */ getParagraphText(index) { const para = this.getParagraph(index); if (!para) return ""; return para.inlines.filter((i) => "text" in i).map((i) => i.text).join(""); } /** * Get all tables */ getTables() { return this.document.blocks.filter((b) => b.table).map((b) => b.table); } /** * Get all images */ getImages() { return this.document.blocks.filter((b) => b.image).map((b) => b.image); } /** * Get image binary data */ getImageData(imageId) { return this.binaryData.get(imageId) || null; } /** * Get document info */ getInfo() { return { paragraphCount: this.getParagraphs().length, tableCount: this.getTables().length, imageCount: this.getImages().length, sectionCount: this.sectionXmls.length, binaryItems: this.document.binaryItems.length }; } /** * Search text in document */ search(query, caseSensitive = false) { const results = []; const paragraphs = this.getParagraphs(); for (let idx = 0; idx < paragraphs.length; idx++) { const text = this.getParagraphText(idx); const searchText = caseSensitive ? text : text.toLowerCase(); const searchQuery = caseSensitive ? query : query.toLowerCase(); if (searchText.includes(searchQuery)) { const matchCount = searchText.split(searchQuery).length - 1; results.push({ paragraphIndex: idx, text, matchCount }); } } return results; } /** * Convert to JSON */ toJson() { return JSON.stringify({ info: this.getInfo(), paragraphs: this.getParagraphs().map((p, idx) => ({ index: idx, text: this.getParagraphText(idx), alignment: p.alignment })), tables: this.getTables().map((t, idx) => ({ index: idx, rows: t.rowCount, cols: t.colCount })), images: this.getImages().map((img, idx) => ({ index: idx, id: img.imageId })) }, null, 2); } /** * Convert to Markdown */ toMarkdown() { const lines = []; for (const block of this.document.blocks) { if (block.paragraph) { let text = ""; for (const inline of block.paragraph.inlines) { if ("text" in inline) { let t = inline.text; if (inline.bold) t = `**${t}**`; if (inline.italic) t = `*${t}*`; text += t; } } if (text.trim()) { lines.push(text); lines.push(""); } } else if (block.table) { const table = block.table; const rowsData = /* @__PURE__ */ new Map(); for (const cell of table.cells) { if (!rowsData.has(cell.row)) { rowsData.set(cell.row, /* @__PURE__ */ new Map()); } let cellText = ""; for (const cb of cell.blocks) { if (cb.paragraph) { for (const inline of cb.paragraph.inlines) { if ("text" in inline) { cellText += inline.text; } } } } rowsData.get(cell.row).set(cell.col, cellText); } const sortedRows = Array.from(rowsData.keys()).sort((a, b) => a - b); for (const rowIdx of sortedRows) { const row = rowsData.get(rowIdx); const cells = []; for (let c = 0; c < table.colCount; c++) { cells.push(row.get(c) || ""); } lines.push("| " + cells.join(" | ") + " |"); if (rowIdx === 0) { lines.push("|" + Array(table.colCount).fill("---").join("|") + "|"); } } lines.push(""); } else if (block.image) { lines.push(`![Image](${block.image.imageId})`); lines.push(""); } } return lines.join("\n"); } /** * Get the parsed document */ getDocument() { return this.document; } }; async function readHwpxFile(filePath) { const fs = await import("fs/promises"); const data = await fs.readFile(filePath); const reader = new HwpxReader(data); await reader.parse(); return reader; } async function readHwpxUrl(url) { const response = await fetch(url); const data = await response.arrayBuffer(); const reader = new HwpxReader(data); await reader.parse(); return reader; } // src/exporter/HwpxExporter.ts var import_jszip2 = __toESM(require("jszip")); var import_fast_xml_parser2 = require("fast-xml-parser"); var HwpxExporter = class { constructor(options = {}) { this.binaryItems = /* @__PURE__ */ new Map(); this.imageCounter = 0; this.tableCounter = 0; this.paraCounter = 0; this.templateData = /* @__PURE__ */ new Map(); this.document = { blocks: [], binaryItems: [] }; this.xmlBuilder = new import_fast_xml_parser2.XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: "@_", textNodeName: "#text", format: true }); if (options.template) { this.loadTemplate(options.template); } } /** * Load template HWPX */ async loadTemplate(data) { const zip = await import_jszip2.default.loadAsync(data); for (const [name, file] of Object.entries(zip.files)) { if (!file.dir) { const content = await file.async("uint8array"); this.templateData.set(name, content); } } } // ========== Content Addition API ========== /** * Add a paragraph */ addParagraph(text, options = {}) { const textRun = { text, bold: options.bold, italic: options.italic, fontSize: options.fontSize, color: options.color }; const para = { inlines: [textRun], alignment: options.alignment || "left" }; this.document.blocks.push({ type: "paragraph", paragraph: para }); this.paraCounter++; return this; } /** * Add a heading */ addHeading(text, level = 1, alignment = "left") { const sizeMap = { 1: 2400, 2: 2e3, 3: 1600, 4: 1400, 5: 1200, 6: 1e3 }; const fontSize = sizeMap[level] || 1e3; return this.addParagraph(text, { alignment, bold: true, fontSize }); } /** * Add a table */ addTable(data, options = {}) { if (data.length === 0) return this; const rowCount = data.length; const colCount = Math.max(...data.map((row) => row.length)); let colWidths = options.colWidths; if (!colWidths) { const usableWidth = A4_WIDTH - mmToHwpunit(40); colWidths = Array(colCount).fill(Math.floor(usableWidth / colCount)); } const cells = []; for (let rowIdx = 0; rowIdx < data.length; rowIdx++) { const row = data[rowIdx]; for (let colIdx = 0; colIdx < row.length; colIdx++) { const cellText = row[colIdx]; const textRun = { text: cellText }; const para = { inlines: [textRun] }; cells.push({ row: rowIdx, col: colIdx, blocks: [{ type: "paragraph", paragraph: para }], width: colWidths[colIdx] || colWidths[colWidths.length - 1] }); } } const table = { rowCount, colCount, cells, colWidths, repeatHeader: options.header }; this.document.blocks.push({ type: "table", table }); this.tableCounter++; return this; } /** * Add an image */ addImage(imageData, filename = "image.png", options = {}) { this.imageCounter++; const imageId = `image${this.imageCounter}`; const ext = filename.split(".").pop()?.toLowerCase() || "png"; const href = `BinData/${imageId}.${ext}`; this.binaryItems.set(imageId, imageData); const image = { imageId, width: options.widthMm ? mmToHwpunit(options.widthMm) : void 0, height: options.heightMm ? mmToHwpunit(options.heightMm) : void 0 }; this.document.blocks.push({ type: "image", image }); this.document.binaryItems.push({ id: imageId, href, mediaType: guessMediaType(filename), data: imageData }); return this; } /** * Add a page break */ addPageBreak() { const para = { inlines: [] }; this.document.blocks.push({ type: "paragraph", paragraph: para, pageBreak: true }); return this; } /** * Add a line break (empty paragraph) */ addLineBreak() { return this.addParagraph(""); } // ========== Markdown Conversion ========== /** * Create from Markdown text */ fromMarkdown(markdown) { const lines = markdown.split("\n"); let i = 0; while (i < lines.length) { const line = lines[i]; const trimmed = line.trim(); if (trimmed.startsWith("#")) { const match = trimmed.match(/^(#{1,6})\s+(.+)$/); if (match) { const level = match[1].length; const text = match[2]; this.addHeading(text, level); } i++; continue; } if (trimmed.startsWith("|") && i + 1 < lines.length) { const tableLines = [trimmed]; let j = i + 1; while (j < lines.length && lines[j].trim().startsWith("|")) { tableLines.push(lines[j].trim()); j++; } const data = []; for (const tl of tableLines) { if (tl.includes("---")) continue; const cells = tl.split("|").slice(1, -1).map((c) => c.trim()); if (cells.length > 0) { data.push(cells); } } if (data.length > 0) { this.addTable(data); } i = j; continue; } if (trimmed) { const bold = trimmed.includes("**"); const italic = trimmed.includes("*") && !trimmed.includes("**"); const text = trimmed.replace(/\*\*/g, "").replace(/\*/g, ""); this.addParagraph(text, { bold, italic }); } i++; } return this; } // ========== Output ========== /** * Build HWPX as Uint8Array */ async build() { const zip = new import_jszip2.default(); zip.file("mimetype", HWPX_MIMETYPE, { compression: "STORE" }); const requiredFiles = [ "Contents/header.xml", "META-INF/container.xml", "META-INF/manifest.xml", "Contents/content.hpf", "version.xml", "settings.xml" ]; for (const fname of requiredFiles) { if (this.templateData.has(fname)) { zip.file(fname, this.templateData.get(fname)); } else { const defaultContent = this.getDefaultFileContent(fname); if (defaultContent) { zip.file(fname, defaultContent); } } } const sectionXml = this.buildSectionXml(); zip.file("Contents/section0.xml", sectionXml); for (const item of this.document.binaryItems) { if (item.data) { zip.file(item.href, item.data); } } return await zip.generateAsync({ type: "uint8array" }); } /** * Build section XML */ buildSectionXml() { const paragraphs = []; let paraId = 0; for (const block of this.document.blocks) { if (block.paragraph) { paragraphs.push(this.buildParagraphXml(block.paragraph, paraId)); paraId++; } else if (block.table) { const anchorP = { "@_id": paraId, "@_paraPrIDRef": 0, "@_styleIDRef": 0, "hp:tbl": this.buildTableXml(block.table) }; paragraphs.push(anchorP); paraId++; } else if (block.image) { const anchorP = { "@_id": paraId, "@_paraPrIDRef": 0, "@_styleIDRef": 0, "hp:pic": this.buildImageXml(block.image) }; paragraphs.push(anchorP); paraId++; } } const section = { "hs:sec": { "@_xmlns:hs": NS.hs, "@_xmlns:hp": NS.hp, "@_xmlns:hc": NS.hc, "hp:p": paragraphs } }; return '<?xml version="1.0" encoding="UTF-8"?>\n' + this.xmlBuilder.build(section); } /** * Build paragraph XML object */ buildParagraphXml(para, paraId) { const runs = []; for (const inline of para.inlines) { if ("text" in inline) { runs.push({ "@_charPrIDRef": 0, "hp:t": inline.text }); } } return { "@_id": paraId, "@_paraPrIDRef": 0, "@_styleIDRef": 0, "hp:run": runs }; } /** * Build table XML object */ buildTableXml(table) { const rows = /* @__PURE__ */ new Map(); for (const cell of table.cells) { if (!rows.has(cell.row)) { rows.set(cell.row, []); } rows.get(cell.row).push(cell); } const trElements = []; const sortedRows = Array.from(rows.keys()).sort((a, b) => a - b); for (const rowIdx of sortedRows) { const cells = rows.get(rowIdx).sort((a, b) => a.col - b.col); const tcElements = []; for (const cell of cells) { const cellParagraphs = []; for (const cb of cell.blocks) { if (cb.paragraph) { cellParagraphs.push(this.buildParagraphXml(cb.paragraph, 0)); } } tcElements.push({ "@_rowSpan": cell.rowSpan || 1, "@_colSpan": cell.colSpan || 1, "hp:p": cellParagraphs }); } trElements.push({ "hp:tc": tcElements }); } return { "@_rowCnt": table.rowCount, "@_colCnt": table.colCount, "hp:tr": trElements }; } /** * Build image XML object */ buildImageXml(image) { const obj = { "@_binaryItemIDRef": image.imageId }; if (image.width && image.height) { obj["hp:sz"] = { "@_width": image.width, "@_height": image.height }; } return obj; } /** * Get default file content */ getDefaultFileContent(filename) { const defaults = { "Contents/header.xml": `<?xml version="1.0" encoding="UTF-8"?> <hh:head xmlns:hh="${NS.hh}" version="1.5" secCnt="1"> </hh:head>`, "META-INF/container.xml": `<?xml version="1.0" encoding="UTF-8"?> <container> <rootfiles> <rootfile full-path="Contents/content.hpf" media-type="application/hwpml-package+xml"/> </rootfiles> </container>`, "META-INF/manifest.xml": `<?xml version="1.0" encoding="UTF-8"?> <manifest></manifest>`, "Contents/content.hpf": `<?xml version="1.0" encoding="UTF-8"?> <opf:package xmlns:opf="${NS.opf}"> <opf:manifest> <opf:item id="header" href="header.xml" media-type="application/xml"/> <opf:item id="section0" href="section0.xml" media-type="application/xml"/> </opf:manifest> </opf:package>`, "version.xml": `<?xml version="1.0" encoding="UTF-8"?> <hv:version xmlns:hv="urn:hancom:hwp:version"> <hv:application>HWPX Tools</hv:application> </hv:version>`, "settings.xml": `<?xml version="1.0" encoding="UTF-8"?> <ha:settings xmlns:ha="${NS.ha}"></ha:settings>` }; return defaults[filename] || null; } /** * Save to file (Node.js) */ async saveToFile(filePath) { const fs = await import("fs/promises"); const data = await this.build(); await fs.writeFile(filePath, data); } /** * Get the document */ getDocument() { return this.document; } }; // src/editor/HwpxEditor.ts var import_fast_xml_parser3 = require("fast-xml-parser"); // src/editor/builders/paragraph.ts function createParagraphObject(text, paraPrId, charPrId) { return { "@_id": "0", "@_paraPrIDRef": String(paraPrId), "@_styleIDRef": "0", "@_pageBreak": "0", "@_columnBreak": "0", "@_merged": "0", "hp:run": { "@_charPrIDRef": String(charPrId), "hp:t": text }, "hp:linesegarray": { "hp:lineseg": { "@_textpos": "0", "@_vertpos": "0", "@_vertsize": "1000", "@_textheight": "1000", "@_baseline": "850", "@_spacing": "600", "@_horzpos": "0", "@_horzsize": "0", "@_flags": "393216" } } }; } function createParagraphXml(text, paraPrId, charPrId) { const escapedText = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); return `<hp:p id="0" paraPrIDRef="${paraPrId}" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"> <hp:run charPrIDRef="${charPrId}"> <hp:t>${escapedText}</hp:t> </hp:run> <hp:linesegarray> <hp:lineseg textpos="0" vertpos="0" vertsize="1000" textheight="1000" baseline="850" spacing="600" horzpos="0" horzsize="0" flags="393216"/> </hp:linesegarray> </hp:p>`; } // src/editor/builders/table.ts function createTableCell(row, col, width, height, text, borderFillId) { return { "@_name": "", "@_header": "0", "@_hasMargin": "0", "@_protect": "0", "@_editable": "0", "@_dirty": "0", "@_borderFillIDRef": String(borderFillId), "hp:subList": { "@_id": "", "@_textDirection": "HORIZONTAL", "@_lineWrap": "BREAK", "@_vertAlign": "CENTER", "@_linkListIDRef": "0", "@_linkListNextIDRef": "0", "@_textWidth": "0", "@_textHeight": "0", "@_hasTextRef": "0", "@_hasNumRef": "0", "hp:p": { "@_id": "0", "@_paraPrIDRef": "0", "@_styleIDRef": "0", "@_pageBreak": "0", "@_columnBreak": "0", "@_merged": "0", "hp:run": { "@_charPrIDRef": "0", "hp:t": text }, "hp:linesegarray": { "hp:lineseg": { "@_textpos": "0", "@_vertpos": "0", "@_vertsize": "1000", "@_textheight": "1000", "@_baseline": "850", "@_spacing": "600", "@_horzpos": "0", "@_horzsize": "0", "@_flags": "393216" } } } }, "hp:cellAddr": { "@_colAddr": String(col), "@_rowAddr": String(row) }, "hp:cellSpan": { "@_colSpan": "1", "@_rowSpan": "1" }, "hp:cellSz": { "@_width": String(width), "@_height": String(height) }, "hp:cellMargin": { "@_left": "141", "@_right": "141", "@_top": "141", "@_bottom": "141" } }; } function createTableParagraph(rows, cols, data, colWidths, borderFillId = 1) { if (!colWidths) { const defaultWidth = Math.floor((A4_WIDTH - mmToHwpunit(40)) / cols); colWidths = Array(cols).fill(defaultWidth); } const totalWidth = colWidths.reduce((a, b) => a + b, 0); const rowHeight = 1e3; const tableRows = []; for (let rowIdx = 0; rowIdx < rows; rowIdx++) { const cells = []; for (let colIdx = 0; colIdx < cols; colIdx++) { const cellText = data && data[rowIdx] && data[rowIdx][colIdx] ? data[rowIdx][colIdx] : ""; cells.push( createTableCell(rowIdx, colIdx, colWidths[colIdx], rowHeight, cellText, borderFillId) ); } tableRows.push({ "hp:tc": cells }); } return { "@_id": "0", "@_paraPrIDRef": "0", "@_styleIDRef": "0", "@_pageBreak": "0", "@_columnBreak": "0", "@_merged": "0", "hp:run": { "@_charPrIDRef": "0", "hp:tbl": { "@_id": "0", "@_zOrder": "0", "@_numberingType": "TABLE", "@_textWrap": "TOP_AND_BOTTOM", "@_textFlow": "BOTH_SIDES", "@_lock": "0", "@_dropcapstyle": "None", "@_pageBreak": "CELL", "@_repeatHeader": "0", "@_rowCnt": String(rows), "@_colCnt": String(cols), "@_cellSpacing": "0", "@_borderFillIDRef": String(borderFillId), "@_noAdjust": "0", "hp:sz": { "@_width": String(totalWidth), "@_widthRelTo": "ABSOLUTE", "@_height": String(rowHeight * rows), "@_heightRelTo": "ABSOLUTE", "@_protect": "0" }, "hp:pos": { "@_treatAsChar": "1", "@_affectLSpacing": "0", "@_flowWithText": "1", "@_allowOverlap": "0", "@_holdAnchorAndSO": "0", "@_vertRelTo": "PARA", "@_horzRelTo": "COLUMN", "@_vertAlign": "TOP", "@_horzAlign": "LEFT", "@_vertOffset": "0", "@_horzOffset": "0" }, "hp:outMargin": { "@_left": "0", "@_right": "0", "@_top": "0", "@_bottom": "0" }, "hp:inMargin": { "@_left": "141", "@_right": "141", "@_top": "141", "@_bottom": "141" }, "hp:tr": tableRows } }, "hp:linesegarray": { "hp:lineseg": { "@_textpos": "0", "@_vertpos": "0", "@_vertsize": String(rowHeight * rows), "@_textheight": String(rowHeight * rows), "@_baseline": "850", "@_spacing": "600", "@_horzpos": "0", "@_horzsize": String(totalWidth), "@_flags": "393216" } } }; } // src/editor/builders/image.ts function createImageParagraph(binaryItemId, width, height) { return { "@_id": "0", "@_paraPrIDRef": "0", "@_styleIDRef": "0", "@_pageBreak": "0", "@_columnBreak": "0", "@_merged": "0", "hp:run": { "@_charPrIDRef": "0", "hp:pic": { "@_id": "0", "@_zOrder": "0", "@_numberingType": "PICTURE", "@_textWrap": "TOP_AND_BOTTOM", "@_textFlow": "BOTH_SIDES", "@_lock": "0", "@_dropcapstyle": "None", "@_href": "", "@_groupLevel": "0", "@_instid": "0", "@_reverse": "0", "hp:offset": { "@_x": "0", "@_y": "0" }, "hp:orgSz": { "@_width": String(width), "@_height": String(height) }, "hp:curSz": { "@_width": String(width), "@_height": String(height) }, "hp:flip": { "@_horizontal": "0", "@_vertical": "0" }, "hp:rotationInfo": { "@_angle": "0", "@_centerX": String(Math.floor(width / 2)), "@_centerY": String(Math.floor(height / 2)), "@_rotateimage": "1" }, "hp:renderingInfo": { "hc:transMatrix": { "@_e1": "1", "@_e2": "0", "@_e3": "0", "@_e4": "0", "@_e5": "1", "@_e6": "0" }, "hc:scaMatrix": { "@_e1": "1.000000", "@_e2": "0", "@_e3": "0", "@_e4": "0", "@_e5": "1.000000", "@_e6": "0" }, "hc:rotMatrix": { "@_e1": "1.000000", "@_e2": "0", "@_e3": "0", "@_e4": "0", "@_e5": "1.000000", "@_e6": "0" } }, "hc:img": { "@_binaryItemIDRef": binaryItemId, "@_effect": "REAL_PIC", "@_alpha": "0", "@_bright": "0", "@_contrast": "0" }, "hp:imgRect": { "hc:pt0": { "@_x": "0", "@_y": "0" }, "hc:pt1": { "@_x": String(width), "@_y": "0" }, "hc:pt2": { "@_x": String(width), "@_y": String(height) }, "hc:pt3": { "@_x": "0", "@_y": String(height) } }, "hp:imgClip": { "@_left": "0", "@_right": String(width), "@_top": "0", "@_bottom": String(height) }, "hp:inMargin": { "@_left": "0", "@_right": "0", "@_top": "0", "@_bottom": "0" }, "hp:imgDim": { "@_dimwidth": String(width), "@_dimheight": String(height) }, "hp:effects": {}, "hp:sz": { "@_width": String(width), "@_widthRelTo": "ABSOLUTE", "@_height": String(height), "@_heightRelTo": "ABSOLUTE", "@_protect": "0" }, "hp:pos": { "@_treatAsChar": "1", "@_affectLSpacing": "0", "@_flowWithText": "1", "@_allowOverlap": "0", "@_holdAnchorAndSO": "0", "@_vertRelTo": "PARA", "@_horzRelTo": "COLUMN", "@_vertAlign": "TOP", "@_horzAlign": "LEFT", "@_vertOffset": "0", "@_horzOffset": "0" }, "hp:outMargin": { "@_left": "0", "@_right": "0", "@_top": "0", "@_bottom": "0" }, "hp:shapeComment": {} } }, "hp:linesegarray": { "hp:lineseg": { "@_textpos": "0", "@_vertpos": "0", "@_vertsize": String(height), "@_textheight": String(height), "@_baseline": "850", "@_spacing": "600", "@_horzpos": "0", "@_horzsize": String(width), "@_flags": "393216" } } }; } // src/editor/utils.ts function escapeRegExp(input) { return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function extractTextFromHwpxNode(node) { const texts = []; const visit = (value) => { if (value === null || value === void 0) return; if (typeof value === "string") { texts.push(value); return; } if (Array.isArray(value)) { value.forEach(visit); return; } if (typeof value !== "object") return; if (value["hp:t"] !== void 0) { const t = value["hp:t"]; if (typeof t === "string") { texts.push(t); } else if (t && typeof t === "object" && typeof t["#text"] === "string") { texts.push(t["#text"]); } } for (const key of Object.keys(value)) { if (key.startsWith("@_")) continue; if (key === "hp:t") continue; visit(value[key]); } }; visit(node); return texts.join(""); } // src/editor/stringOps.ts function insertParagraphAfterTextInXml(xml, afterText, newText, options = {}) { const pattern = new RegExp(`<hp:t[^>]*>${escapeRegExp(afterText)}</hp:t>`); const match = xml.match(pattern); if (!match || match.index === void 0) return { ok: false, xml }; const searchStart = match.index + match[0].length; const pEndMatch = xml.substring(searchStart).match(/<\/hp:p>/); if (!pEndMatch || pEndMatch.index === void 0) return { ok: false, xml }; const insertPos = searchStart + pEndMatch.index + pEndMatch[0].length; const newPXml = createParagraphXml(newText, options.paraPrId || 0, options.charPrId || 0); const nextXml = xml.substring(0, insertPos) + newPXml + xml.substring(insertPos); return { ok: true, xml: nextXml }; } function insertParagraphsAfterTextInXml(xml, afterText, texts, options = {}) { const pattern = new RegExp(`<hp:t[^>]*>${escapeRegExp(afterText)}</hp:t>`); const match = xml.match(pattern); if (!match || match.index === void 0) return { inserted: 0, xml }; const searchStart = match.index + match[0].length; const pEndMatch = xml.substring(searchStart).match(/<\/hp:p>/); if (!pEndMatch || pEndMatch.index === void 0) return { inserted: 0, xml }; const insertPos = searchStart + pEndMatch.index + pEndMatch[0].length; let newParagraphsXml = ""; for (const text of texts) { newParagraphsXml += createParagraphXml(text, options.paraPrId || 0, options.charPrId || 0); } const nextXml = xml.substring(0, insertPos) + newParagraphsXml + xml.substring(insertPos); return { inserted: texts.length, xml: nextXml }; } function appendParagraphRawInXml(xml, text, options = {}) { const endTag = "</hs:sec>"; const insertPos = xml.lastIndexOf(endTag); if (insertPos === -1) return { ok: false, xml }; const newPXml = createParagraphXml(text, options.paraPrId || 0, options.charPrId || 0); const nextXml = xml.substring(0, insertPos) + newPXml + xml.substring(insertPos); return { ok: true, xml: nextXml }; } function copyParagraphStyleAndInsertInXml(xml, sourceText, newText) { const pattern = new RegExp( `<hp:p[^>]*>.*?<hp:t[^>]*>${escapeRegExp(sourceText)}</hp:t>.*?<\\/hp:p>`, "s" ); const match = xml.match(pattern); if (!match || match.index === void 0) return { ok: false, xml }; const sourceP = match[0]; const insertPos = match.index + match[0].length; const paraPrMatch = sourceP.match(/paraPrIDRef=\"(\d+)\"/); const charPrMatch = sourceP.match(/charPrIDRef=\"(\d+)\"/); const paraPrId = paraPrMatch ? parseInt(paraPrMatch[1], 10) : 0; const charPrId = charPrMatch ? parseInt(charPrMatch[1], 10) : 0; const newPXml = createParagraphXml(newText, paraPrId, charPrId); const nextXml = xml.substring(0, insertPos) + newPXml + xml.substring(insertPos); return { ok: true, xml: nextXml }; } // src/editor/HwpxEditor.ts var HwpxEditor = class { constructor(sectionXml) { this.useRawMode = true; this.modified = false; this.originalXml = typeof sectionXml === "string" ? sectionXml : new TextDecoder().decode(sectionXml); this.xmlStr = this.originalXml; this.parser = new import_fast_xml_parser3.XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_", textNodeName: "#text", preserveOrder: false, parseAttributeValue: false }); this.builder = new import_fast_xml_parser3.XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: "@_", textNodeName: "#text", format: true }); this.root = this.parser.parse(this.originalXml); } /** * Check if document has been modified */ isModified() { return this.modified; } /** * Export as Uint8Array */ toBytes() { if (this.useRawMode) { return new TextEncoder().encode(this.xmlStr); } const body = this.builder.build(this.root); const header = this.originalXml.startsWith("<?xml") ? this.originalXml.substring(0, this.originalXml.indexOf("?>") + 2) : '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>'; return new TextEncoder().encode(header + body); } /** * Export as string */ toString() { return new TextDecoder().decode(this.toBytes()); } // ============================================================ // Paragraph Operations // ============================================================ getParagraphs() { const sec = this.root["hs:sec"]; if (!sec) return []; const paragraphs = sec["hp:p"]; if (!paragraphs) return []; return Array.isArray(paragraphs) ? paragraphs : [paragraphs]; } /** * Get paragraph count */ getParagraphCount() { return this.getParagraphs().length; } /** * Get paragraph text by index */ getParagraphText(index) { const paragraphs = this.getParagraphs(); if (index < 0 || index >= paragraphs.length) return null; return extractTextFromHwpxNode(paragraphs[index]); } /** * Replace text in document (string-based, safe mode) */ replaceText(oldText, newText, count = -1) { if (!this.xmlStr.includes(oldText)) return 0; let replaced; if (count === -1) { replaced = this.xmlStr.split(oldText).length - 1; this.xmlStr = this.xmlStr.split(oldText).join(newText); } else { replaced = 0; let result = this.xmlStr; for (let i = 0; i < count && result.includes(oldText); i++) { result = result.replace(oldText, newText); replaced++; } this.xmlStr = result; } if (replaced > 0) this.modified = true; return replaced; } /** * Insert paragraph after specific index */ insertParagraphAfter(afterIndex, text, options = {}) { const paragraphs = this.getParagraphs(); if (afterIndex < 0 || afterIndex >= paragraphs.length) return false; const newPara = createParagraphObject(text, options.paraPrId || 0, options.charPrId || 0); paragraphs.splice(afterIndex + 1, 0, newPara); this.root["hs:sec"]["hp:p"] = paragraphs; this.useRawMode = false; this.modified = true; return true; } /** * Insert paragraph before specific index */ insertParagraphBefore(beforeIndex, text, options = {}) { const paragraphs = this.getParagraphs(); if (beforeIndex < 0 || beforeIndex >= paragraphs.length) return false; const newPara = createParagraphObject(text, options.paraPrId || 0, options.charPrId || 0); paragraphs.splice(beforeIndex, 0, newPara); this.root["hs:sec"]["hp:p"] = paragraphs; this.useRawMode = false; this.modified = true; return true; } /** * Append paragraph at document end */ appendParagraph(text, options = {}) { const sec = this.root["hs:sec"]; if (!sec) return false; const newPara = createParagraphObject(text, options.paraPrId || 0, options.charPrId || 0); let paragraphs = sec["hp:p"]; if (!paragraphs) { paragraphs = [newPara]; } else if (Array.isArray(paragraphs)) { paragraphs.push(newPara); } else { paragraphs = [paragraphs, newPara]; } sec["hp:p"] = paragraphs; this.useRawMode = false; this.modified = true; return true; } // ============================================================ // Table Operations // ============================================================ /** * Insert table after specific paragraph */ insertTableAfter(afterIndex, rows, cols, options = {}) { const paragraphs = this.getParagraphs(); if (afterIndex < 0 || afterIndex >= paragraphs.length) return false; const tablePara = createTableParagraph( rows, cols, options.data, options.colWidths, options.borderFillId || 1 ); paragraphs.splice(afterIndex + 1, 0, tablePara); this.root["hs:sec"]["hp:p"] = paragraphs; this.useRawMode = false; this.modified = true; return true; } /** * Append table at document end */ appendTable(rows, cols, options = {}) { const sec = this.root["hs:sec"]; if (!sec) return false; const tablePara = createTableParagraph( rows, cols, options.data, options.colWidths, options.borderFillId || 1 ); let paragraphs = sec["hp:p"]; if (!paragraphs) { paragraphs = [tablePara]; } else if (Array.isArray(paragraphs)) { paragraphs.push(tablePara); } else { paragraphs = [paragraphs, tablePara]; } sec["hp:p"] = paragraphs; this.useRawMode = false; this.modified = true; return true; } // ============================================================ // Image Operations // ============================================================ /** * Insert image after specific paragraph */ insertImageAfter(afterIndex, options) { const paragraphs = this.getParagraphs(); if (afterIndex < 0 || afterIndex >= paragraphs.length) return false; const imagePara = createImageParagraph( options.binaryItemId, options.width, options.height ); paragraphs.splice(afterIndex + 1, 0, imagePara); this.root["hs:sec"]["hp:p"] = paragraphs; this.useRawMode = false; this.modified = true; return true; } /** * Append image at document end */ appendImage(options) { const sec = this.root["hs:sec"]; if (!sec) return false; const imagePara = createImageParagraph( options.binaryItemId, options.width, options.height ); let paragraphs = sec["hp:p"]; if (!paragraphs) { paragraphs = [imagePara]; } else if (Array.isArray(paragraphs)) { paragraphs.push(imagePara); } else { paragraphs = [paragraphs, imagePara]; } sec["hp:p"] = paragraphs; this.useRawMode = false; this.modified = true; return true; } // ============================================================ // Paragraph Deletion // ============================================================ /** * Delete paragraph by index */ deleteParagraph(index) { const paragraphs = this.getParagraphs(); if (index < 0 || index >= paragraphs.length) return false; paragraphs.splice(index, 1); this.root["hs:sec"]["hp:p"] = paragraphs; this.useRawMode = false; this.modified = true; return true; } /** * Delete paragraphs in range */ deleteParagraphsRange(start, end) { const paragraphs = this.getParagraphs(); const validStart = Math.max(0, start); const validEnd = Math.min(paragraphs.length, end); if (validStart >= validEnd) return 0; const deleted = validEnd - validStart; paragraphs.splice(validStart, deleted); this.root["hs:sec"]["hp:p"] = paragraphs; this.useRawMode = false; this.modified = true; return deleted; } // ============================================================ // Paragraph Copy/Move Operations // ============================================================ /** * Copy paragraph from one index to another */ copyParagraph(fromIndex, toIndex) { const paragraphs = this.getParagraphs(); if (fromIndex < 0 || fromIndex >= paragraphs.length) return false; if (toIndex < 0 || toIndex > paragraphs.length) return false; const source = paragraphs[fromIndex]; const copy = JSON.parse(JSON.stringify(source)); if (toIndex >= paragraphs.length) { paragraphs.push(copy); } else { paragraphs.splice(toIndex, 0, copy); } this.root["hs:sec"]["hp:p"] = paragraphs; this.useRawMode = false; this.modified = true; return true; } /** * Move paragraph from one index to another */ moveParagraph(fromIndex, toIndex) { const paragraphs = this.getParagraphs(); if (fromIndex < 0 || fromIndex >= paragraphs.length) return false; if (toIndex < 0 || toIndex > paragraphs.length) return false; if (fromIndex === toIndex) return true; const source = paragraphs.splice(fromIndex, 1)[0]; const adjustedIndex = toIndex > fromIndex ? toIndex - 1 : toIndex; if (adjustedIndex >= paragraphs.length) { paragraphs.push(source); } else { paragraphs.splice(adjustedIndex, 0, source); } this.root["hs:sec"]["hp:p"] = paragraphs; this.useRawMode = false; this.modified = true; return true; } /** * Set paragraph text (replace all runs in paragraph) */ setParagraphText(index, text, charPrId = 0) { const paragraphs = this.getParagraphs(); if (index < 0 || index >= paragraphs.length) return false; const p = paragraphs[index]; p["hp:run"] = { "@_charPrIDRef": String(charPrId), "hp:t": text }; this.root["hs:sec"]["hp:p"] = paragraphs; this.useRawMode = false; this.modified = true; return true; } // ============================================================ // Page/Column Break Operations // ============================================================ /** * Set page break for paragraph */ setPageBreak(index, enable = true) { const paragraphs = this.getParagraphs(); if (index < 0 || index >= paragraphs.length) return false; paragraphs[index]["@_pageBreak"] = enable ? "1" : "0"; this.root["hs:sec"]["hp:p"] = paragraphs; this.useRawMode = false; this.modified = true; return true; } /** * Set column break for paragraph */ setColumnBreak(index, enable = true) { const paragraphs = this.getParagraphs(); if (index < 0 || index >= paragraphs.length) return false; paragraphs[index]["@_columnBreak"] = enable ? "1" : "0"; this.root["hs:sec"]["hp:p"] = para