hwpx-ts
Version:
TypeScript library for reading and writing HWPX files
1,676 lines (1,665 loc) • 53.3 kB
JavaScript
"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(``);
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
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