mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-01 08:53:27 +00:00
- Updated ContractDocumentViewModelBuilder to include cargoTypeName, containerType, and cargoSummary in the schedule. - Modified contract dynamic template tests to validate the new cargo fields. - Enhanced contract renderer service tests to reflect changes in cargo data structure. - Updated contract view model interface to include new cargo-related fields. - Improved dynamic template rendering to display cargo type and container type. - Refactored exchange settings controller and service to streamline error handling and feed status management. - Introduced article HTML conversion functions to support Quill editor integration for structured article editing. - Added tests for article HTML conversion to ensure correct round-trip processing of clauses and bullets.
126 lines
4.0 KiB
TypeScript
126 lines
4.0 KiB
TypeScript
/**
|
||
* Bridge between the Quill editor (HTML) and the stored article body, which is
|
||
* the structural plain text the server parses into numbered clauses
|
||
* (`parseArticleBody` in contract-article.util.ts): one clause per line, a
|
||
* leading outline token ("2.", "2.1") for depth, and "- " for bullets.
|
||
*
|
||
* Quill owns presentation; the body format owns structure. Converting on the
|
||
* way in and out keeps the renderer, the server-side renumbering, and the
|
||
* generated PDF working exactly as before.
|
||
*/
|
||
|
||
const ESCAPES: Record<string, string> = {
|
||
"&": "&",
|
||
"<": "<",
|
||
">": ">",
|
||
};
|
||
|
||
const escapeHtml = (text: string): string =>
|
||
text.replace(/[&<>]/g, (c) => ESCAPES[c]);
|
||
|
||
/**
|
||
* Body text → Quill HTML. Clauses become `<p>` carrying their outline token so
|
||
* the author sees the real numbering; bullets become a `<ul>` under the clause
|
||
* they belong to.
|
||
*/
|
||
export function bodyToHtml(body: string): string {
|
||
const lines = (body ?? "").split("\n").filter((l) => l.trim().length > 0);
|
||
if (lines.length === 0) return "<p><br></p>";
|
||
|
||
const out: string[] = [];
|
||
let inList = false;
|
||
for (const line of lines) {
|
||
const trimmed = line.trim();
|
||
if (trimmed.startsWith("- ")) {
|
||
if (!inList) {
|
||
out.push("<ul>");
|
||
inList = true;
|
||
}
|
||
out.push(`<li>${escapeHtml(trimmed.slice(2).trim())}</li>`);
|
||
continue;
|
||
}
|
||
if (inList) {
|
||
out.push("</ul>");
|
||
inList = false;
|
||
}
|
||
out.push(`<p>${escapeHtml(trimmed)}</p>`);
|
||
}
|
||
if (inList) out.push("</ul>");
|
||
return out.join("");
|
||
}
|
||
|
||
/**
|
||
* Quill HTML → body text. `<li>` inside a `<ul>` becomes a "- " bullet; every
|
||
* other block becomes its own line. Quill's indent classes
|
||
* (`ql-indent-1`, …) map onto sub-clause depth, so indenting in the toolbar
|
||
* produces "1.1"-style nesting — the exact digits are placeholders, the server
|
||
* renumbers them.
|
||
*
|
||
* Runs through DOMParser rather than regex: the input is real HTML from a
|
||
* contenteditable, and entity handling (&, ) has to be right or the
|
||
* text lands in the PDF mangled.
|
||
*/
|
||
export function htmlToBody(html: string): string {
|
||
if (!html) return "";
|
||
const doc = new DOMParser().parseFromString(
|
||
`<div id="root">${html}</div>`,
|
||
"text/html",
|
||
);
|
||
const root = doc.getElementById("root");
|
||
if (!root) return "";
|
||
|
||
const lines: string[] = [];
|
||
|
||
const textOf = (el: Element): string =>
|
||
// is the nbsp Quill inserts for trailing spaces — plain space in the
|
||
// stored body, otherwise it survives into the contract text.
|
||
(el.textContent ?? "").replace(/ /g, " ").trim();
|
||
|
||
const indentOf = (el: Element): number => {
|
||
const match = /ql-indent-(\d+)/.exec(el.className ?? "");
|
||
return match ? Number(match[1]) : 0;
|
||
};
|
||
|
||
const walk = (node: Element, insideList: boolean) => {
|
||
for (const child of Array.from(node.children)) {
|
||
const tag = child.tagName.toLowerCase();
|
||
if (tag === "ul" || tag === "ol") {
|
||
// <ol> is authored numbering; the body format numbers clauses itself,
|
||
// so an ordered list is clause lines, not bullets.
|
||
walk(child, tag === "ul");
|
||
continue;
|
||
}
|
||
if (tag === "li") {
|
||
const text = textOf(child);
|
||
if (!text) continue;
|
||
if (insideList) {
|
||
lines.push(`- ${text}`);
|
||
} else {
|
||
const depth = indentOf(child) + 1;
|
||
lines.push(`${Array.from({ length: depth }, () => "1").join(".")}. ${text}`);
|
||
}
|
||
continue;
|
||
}
|
||
if (tag === "p" || tag === "div") {
|
||
const text = textOf(child);
|
||
if (text) {
|
||
const depth = indentOf(child);
|
||
lines.push(
|
||
depth > 0
|
||
? `${Array.from({ length: depth + 1 }, () => "1").join(".")}. ${text}`
|
||
: text,
|
||
);
|
||
}
|
||
continue;
|
||
}
|
||
// Anything else (blockquote, heading, stray span): keep its text on a
|
||
// line rather than dropping the author's words.
|
||
const text = textOf(child);
|
||
if (text) lines.push(text);
|
||
}
|
||
};
|
||
|
||
walk(root, false);
|
||
return lines.join("\n");
|
||
}
|