)·그림 개체( 원문 그대로)와
// 글자 굵게(문자 서식 cs 의 CharStyleON 1번 비트). 쓰기: 굴림체(영문 이름 GulimChe) 보통·굵게 두 서식, 표는 테두리 있는 기본 꼴,
// 그림은 읽은 원문 그대로, ASCII 밖 글자는 코드; — cp949 변환 없이 원본이 읽는 꼴. 문단 서식(간격·들여쓰기·글꼴 크기)은 버린다.
const EstMemoWpt = (() => {
const decode = (text) =>
text
.replace(/(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/ /g, " ")
.replace(/&/g, "&");
const encode = (text) =>
[...String(text)]
.map((ch) => {
if (ch === "&") return "&";
if (ch === "<") return "<";
if (ch === ">") return ">";
const code = ch.codePointAt(0);
return code < 32 || code > 126 ? `${code};` : ch;
})
.join("");
// source → 마디 목록: { p: [{ text, bold } | { object }] } · { table: [[ [마디…] ]] }
function read(source) {
const objects = [];
const text = source.replace(
//g,
(m) => ``,
);
const bold = new Map();
for (const m of text.matchAll(//g)) {
const on = /CharStyleON:(\d+)/.exec(m[2]);
bold.set(m[1], Boolean(on && Number(on[1]) & 1));
}
const root = [],
stack = [{ list: root }];
let para = null,
current = false;
const top = () => stack.at(-1);
const push = (run) => {
if (!para) {
para = { p: [] };
top().list.push(para);
}
const last = para.p.at(-1);
if (run.text != null && last?.text != null && last.bold === run.bold) last.text += run.text;
else para.p.push(run);
};
for (const part of text.split(/(<[^>]*>)/)) {
if (!part) continue;
if (!part.startsWith("<")) {
const t = decode(part.replace(/\r?\n/g, ""));
if (t && para) push({ text: t, bold: current });
continue;
}
const tag = /^<\/?([a-zA-Z]+)/.exec(part)?.[1]?.toLowerCase(),
closing = part.startsWith("");
if (tag === "div") {
if (closing) para = null;
else {
// 칸 첫 문단이 비어 있으면(칸 자체가 문단) 그 자리를 씀
if (!(para && !para.p.length && top().list.at(-1) === para)) {
para = { p: [] };
top().list.push(para);
}
const cs = /\bcs=(\d+)/.exec(part);
current = cs ? bold.get(cs[1]) || false : false;
}
} else if (tag === "c") current = bold.get(/nr=(\d+)/.exec(part)?.[1]) || false;
else if (tag === "obj") push({ object: objects[Number(/obj (\d+)/.exec(part)[1])] });
else if (tag === "table" && !closing) {
const table = { table: [] };
top().list.push(table);
stack.push({ table });
} else if (tag === "tr" && !closing) {
const row = [];
stack.findLast((s) => s.table)?.table.table.push(row);
stack.push({ row });
} else if (tag === "td" && !closing) {
const cell = [];
stack.findLast((s) => s.row)?.row.push(cell);
stack.push({ list: cell });
// 칸 = 문단 하나(글이 td 바로 안에) — 안에 div 가 더 있으면 이어지는 문단
para = { p: [] };
cell.push(para);
const cs = /\bcs=(\d+)/.exec(part);
current = cs ? bold.get(cs[1]) || false : false;
} else if (["table", "tr", "td"].includes(tag) && closing) {
const key = { table: "table", tr: "row", td: "list" }[tag];
while (stack.length > 1 && !stack.at(-1)[key]) stack.pop();
if (stack.length > 1) stack.pop();
para = null;
}
}
return root;
}
function write(nodes) {
const font = "CharFont:'GulimChe';CharFontSize:1000;";
const runs = (p) =>
p.map((r) => (r.object ? r.object : `${encode(r.text)}`)).join("");
const list = (items) =>
items
.map((n) =>
n.table
? `${n.table
.map(
(row) =>
`${row.map((cell) => `| ${runs(cell[0]?.p || [])}${list(cell.slice(1))} | `).join("")} `,
)
.join("")} \r\n`
: `${runs(n.p)} \r\n`,
)
.join("");
return (
"\r\n" +
'' +
"\r\n" +
"\r\n" +
`` +
list(nodes)
);
}
// 편집 창용 html(그림은 고칠 수 없는 자리표 — data-obj 번호로 원문을 되찾음)
function html(nodes, objects = []) {
const esc = (s) =>
s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
const runs = (p) =>
p
.map((r) =>
r.object
? `[그림]`
: r.bold
? `${esc(r.text)}`
: esc(r.text),
)
.join("") || " ";
const list = (items) =>
items
.map((n) =>
n.table
? `${n.table.map((row) => `${row.map((cell) => `| ${list(cell)} | `).join("")} `).join("")} `
: `${runs(n.p)} `,
)
.join("");
return list(nodes);
}
return { read, write, html, encode, decode };
})();
if (typeof module !== "undefined" && module.exports) module.exports = EstMemoWpt;
|