import createReport from 'docx-templates'; import PizzaZip from "pizzip"; import { DOMParser, XMLSerializer, type Document as XmlDocument, type Element as XmlElement, type Node as XmlNode, type Attr as XmlAttr, } from "@xmldom/xmldom"; import xpath from "xpath"; const WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; export interface ExtractedData { [tagName: string]: string; } const asXpathNode = (node: XmlNode): globalThis.Node => node as unknown as globalThis.Node; function createSelector() { return xpath.useNamespaces({ w: WORD_NS }); } /** * ฟังก์ชันสำหรับดึงข้อมูลจาก docx ออกมาเป็น JSON โดยอิงจาก Bookmark (w:bookmarkStart) */ export function extractBookmark(docxBuffer: Buffer,): ExtractedData { const zip = new PizzaZip(docxBuffer); const docXmlFile = zip.file("word/document.xml"); if (!docXmlFile) { throw new Error("Invalid docx file: word/document.xml not found."); } const docXmlText: string = docXmlFile.asText(); // 3. Parse ข้อความ XML ให้เป็น DOM Object const parser = new DOMParser(); const doc = parser.parseFromString(docXmlText, "text/xml"); // 4. ตั้งค่า Namespace ของ Word XML เพื่อใช้ค้นหาด้วย XPath const select = xpath.useNamespaces({ 'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' }); const result: ExtractedData = {}; // ค้นหา Bookmark หรือ Tag ที่ใช้แบ่ง Section // const bookmarks = select('//w:bookmarkStart', doc as unknown as globalThis.Node) as Element[]; const bookmarks = select( "//w:bookmarkStart", doc as unknown as globalThis.Node ) as unknown as XmlElement[]; bookmarks.forEach((bookmark: XmlElement) => { const sectionName = bookmark.getAttribute('w:name'); const bookmarkId = bookmark.getAttribute('w:id'); // กรองเอาเฉพาะ Bookmark ที่เราสนใจ (ไม่เอาพวกระบบสร้างเองอย่าง _GoBack) if (sectionName && !sectionName.startsWith('_')) { let extractedText = ""; let currentNode: XmlNode | null = bookmark.nextSibling; // วนลูปอ่าน Node ถัดไปเรื่อยๆ จนกว่าจะเจอจุดสิ้นสุดของ Bookmark while (currentNode) { // ถ้าเจอจุดจบของ Bookmark ตัวเอง ให้หยุด if ( currentNode.nodeType === 1 && // Element node (currentNode as XmlElement).nodeName === 'w:bookmarkEnd' && (currentNode as XmlElement).getAttribute('w:id') === bookmarkId ) { break; } // ดึง Text (w:t) ที่อยู่ภายใน Node ปัจจุบัน const textNodes = select('.//w:t/text()', currentNode as unknown as globalThis.Node) as unknown as XmlNode[]; textNodes.forEach((textNode: XmlNode) => { if (textNode.nodeValue) { extractedText += textNode.nodeValue; } }); currentNode = currentNode.nextSibling; } // บันทึกลงผลลัพธ์ JSON result[sectionName] = extractedText.trim(); } }); return result; } /** * อ่าน Content Control (w:sdt) จาก DOCX Buffer * ปัจจุบันอ่านจาก word/document.xml ตาม logic ของโค้ดต้นฉบับ */ export function extractContentControls( docxBuffer: Buffer, ): ExtractedData { const zip = new PizzaZip(docxBuffer); const docXmlFile = zip.file("word/document.xml"); if (!docXmlFile) { throw new Error("Invalid docx file: word/document.xml not found."); } const parser = new DOMParser(); const doc = parser.parseFromString(docXmlFile.asText(), "text/xml"); const select = createSelector(); const result: ExtractedData = {}; const sdtNodes = select( "//w:sdt", asXpathNode(doc), ) as unknown as XmlElement[]; sdtNodes.forEach((sdt) => { const tagAttr = select( "./w:sdtPr/w:tag/@w:val", asXpathNode(sdt), true, ) as unknown as XmlAttr | undefined; const tagName = tagAttr?.value; if (!tagName) return; const sdtContent = select( "./w:sdtContent", asXpathNode(sdt), true, ) as unknown as XmlElement | undefined; if (!sdtContent) { result[tagName] = ""; return; } const nodes = select( ".//w:p | .//w:t | .//w:tab | .//w:br | .//w:noBreakHyphen | .//w:softHyphen | .//w:sym", asXpathNode(sdtContent), ) as unknown as XmlElement[]; let extractedText = ""; nodes.forEach((node) => { switch (node.nodeName) { case "w:p": if ( extractedText.length > 0 && !extractedText.endsWith("\n") ) { extractedText += "\n\n"; } break; case "w:t": extractedText += node.textContent || ""; break; case "w:tab": extractedText += "\t"; break; case "w:br": extractedText += "\n"; break; case "w:noBreakHyphen": extractedText += "\u2011"; break; case "w:softHyphen": extractedText += "\u00ad"; break; case "w:sym": { const charAttr = node.getAttribute("w:char"); if (charAttr) { const charCode = Number.parseInt(charAttr, 16); extractedText += String.fromCharCode(charCode); } break; } } }); result[tagName] = extractedText; }); return result; } type SdtControlType = "plain" | "rich" | "other"; type SdtStructure = "block" | "inline"; function detectSdtControlType( sdt: XmlElement, select: xpath.XPathSelect, ): { type: SdtControlType; multiLine: boolean } { const sdtPr = select( "./w:sdtPr", asXpathNode(sdt), true, ) as unknown as XmlElement | undefined; if (!sdtPr) { return { type: "other", multiLine: false }; } const textEl = select( "./w:text", asXpathNode(sdtPr), true, ) as unknown as XmlElement | undefined; if (textEl) { const multiLineAttr = select( "./@w:multiLine", asXpathNode(textEl), true, ) as unknown as XmlAttr | undefined; return { type: "plain", multiLine: multiLineAttr?.value === "1" || multiLineAttr?.value === "true", }; } const richTextEl = select( "./w:richText", asXpathNode(sdtPr), true, ) as unknown as XmlElement | undefined; if (richTextEl) { return { type: "rich", multiLine: true }; } const otherTypeTags = [ "w:date", "w:dropDownList", "w:comboBox", "w:checkbox", "w:citation", "w:group", "w:docPartObj", "w:docPartList", ]; for (const tag of otherTypeTags) { const found = select( `./${tag}`, asXpathNode(sdtPr), true, ) as unknown as XmlElement | undefined; if (found) { return { type: "other", multiLine: false }; } } return { type: "rich", multiLine: true }; } function detectSdtStructure( sdtContent: XmlElement, select: xpath.XPathSelect, ): SdtStructure { const directP = select( "./w:p", asXpathNode(sdtContent), ) as unknown as XmlElement[]; return directP.length > 0 ? "block" : "inline"; } function buildRunChildren( doc: XmlDocument, text: string, ): XmlElement[] { const nodes: XmlElement[] = []; const tokens = text.split(/(\t|\n)/); tokens.forEach((token) => { if (token === "") return; if (token === "\t") { nodes.push(doc.createElementNS(WORD_NS, "w:tab")); } else if (token === "\n") { nodes.push(doc.createElementNS(WORD_NS, "w:br")); } else { const t = doc.createElementNS(WORD_NS, "w:t"); t.setAttribute("xml:space", "preserve"); t.textContent = token; nodes.push(t); } }); if (nodes.length === 0) { const t = doc.createElementNS(WORD_NS, "w:t"); t.setAttribute("xml:space", "preserve"); t.textContent = ""; nodes.push(t); } return nodes; } function buildRun( doc: XmlDocument, text: string, rPr?: XmlElement, ): XmlElement { const run = doc.createElementNS(WORD_NS, "w:r"); if (rPr) { run.appendChild(rPr.cloneNode(true)); } buildRunChildren(doc, text).forEach((node) => run.appendChild(node), ); return run; } function insertInlineContent( doc: XmlDocument, sdtContent: XmlElement, select: xpath.XPathSelect, text: string, allowLineBreak: boolean, ): void { const safeText = allowLineBreak ? text : text.replace(/\n/g, " "); const existingRuns = select( "./w:r", asXpathNode(sdtContent), ) as unknown as XmlElement[]; const firstRPr = existingRuns.length > 0 ? (select( "./w:rPr", asXpathNode(existingRuns[0]), true, ) as unknown as XmlElement | undefined) : undefined; existingRuns.forEach((r) => { if (r.parentNode) r.parentNode.removeChild(r); }); sdtContent.appendChild( buildRun(doc, safeText, firstRPr), ); } // ในฟังก์ชัน insertBlockContent() function insertBlockContent( doc: XmlDocument, sdtContent: XmlElement, select: xpath.XPathSelect, text: string, allowMultiParagraph: boolean, ): void { const allParagraphs = select( "./w:p", asXpathNode(sdtContent), ) as unknown as XmlElement[]; if (allParagraphs.length === 0) { const p = doc.createElementNS(WORD_NS, "w:p"); sdtContent.appendChild(p); allParagraphs.push(p); } const firstP = allParagraphs[0]; const firstPPr = select( "./w:pPr", asXpathNode(firstP), true, ) as unknown as XmlElement | undefined; const firstRun = select( "./w:r", asXpathNode(firstP), true, ) as unknown as XmlElement | undefined; const firstRPr = firstRun ? (select( "./w:rPr", asXpathNode(firstRun), true, ) as unknown as XmlElement | undefined) : undefined; // ลบย่อหน้าเดิมทั้งหมดยกเว้นย่อหน้าแรก allParagraphs.forEach((p) => { if (p !== firstP && p.parentNode) { p.parentNode.removeChild(p); } }); // ลบ Run เดิมออกจากย่อหน้าแรก const existingRuns = select( "./w:r", asXpathNode(firstP), ) as unknown as XmlElement[]; existingRuns.forEach((r) => { if (r.parentNode) r.parentNode.removeChild(r); }); if (!allowMultiParagraph) { // ถ้าไม่ใช่ออกแบบให้ข้ามย่อหน้าได้ ให้เปลี่ยน \n ทั้งหมดเป็น Space หรือ buildRun ปกติ firstP.appendChild(buildRun(doc, text.replace(/\n+/g, "\n"), firstRPr)); return; } // แยกด้วย \n\n สำหรับ Paragraph Break (w:p) const paragraphs = text.split("\n\n"); paragraphs.forEach((pText, index) => { let targetP: XmlElement; if (index === 0) { targetP = firstP; } else { targetP = doc.createElementNS(WORD_NS, "w:p"); if (firstPPr) { targetP.appendChild(firstPPr.cloneNode(true)); } sdtContent.appendChild(targetP); } const run = doc.createElementNS(WORD_NS, "w:r"); if (firstRPr) { run.appendChild(firstRPr.cloneNode(true)); } // pText จะมี \n เดี่ยวๆ หลงเหลืออยู่ ซึ่ง buildRunChildren จะแปลงเป็น w:br ให้โดยอัตโนมัติ buildRunChildren(doc, pText).forEach((node) => run.appendChild(node), ); targetP.appendChild(run); }); } function insertFallbackContent( sdtContent: XmlElement, select: xpath.XPathSelect, text: string, ): void { const textNodes = select( ".//w:t", asXpathNode(sdtContent), ) as unknown as XmlElement[]; if (textNodes.length === 0) return; textNodes[0].textContent = text; for (let i = 1; i < textNodes.length; i++) { textNodes[i].textContent = ""; } } export type PropData = Record; export type DocumentTemplate = {info?:PropData,dc?:PropData,cc:PropData,}; function normalizeValue(value: unknown): string { if (value === null || value === undefined) { return ""; } if (typeof value === "string") { return value; } if ( typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" ) { return String(value); } // รองรับ JSON ที่ส่ง array/object มา แต่ Content Control ต้องการ text return JSON.stringify(value); } /** * นำ JSON data ไปแทนค่า Content Control ใน DOCX template * คืนค่าเป็น Buffer ของ DOCX ที่สร้างเสร็จแล้ว */ export function templateContentControls( templateBuffer: Buffer|Uint8Array, data: PropData, ): Buffer { const zip = new PizzaZip(templateBuffer); const docXmlFile = zip.file("word/document.xml"); if (!docXmlFile) { throw new Error("Invalid docx file: word/document.xml not found."); } const parser = new DOMParser(); const doc = parser.parseFromString(docXmlFile.asText(), "text/xml"); const select = createSelector(); const sdtNodes = select( "//w:sdt", asXpathNode(doc), ) as unknown as XmlElement[]; sdtNodes.forEach((sdt) => { const tagAttr = select( "./w:sdtPr/w:tag/@w:val", asXpathNode(sdt), true, ) as unknown as XmlAttr | undefined; const tagName = tagAttr?.value; if (!tagName || !Object.prototype.hasOwnProperty.call(data, tagName)) { return; } const newValue = normalizeValue(data[tagName]); const sdtContent = select( "./w:sdtContent", asXpathNode(sdt), true, ) as unknown as XmlElement | undefined; if (!sdtContent) return; const { type: controlType, multiLine } = detectSdtControlType(sdt, select); const structure = detectSdtStructure(sdtContent, select); if (controlType === "other") { insertFallbackContent(sdtContent, select, newValue); return; } if (structure === "inline") { const allowLineBreak = controlType === "plain" ? multiLine : true; insertInlineContent( doc, sdtContent, select, newValue, allowLineBreak, ); } else { insertBlockContent( doc, sdtContent, select, newValue, controlType === "rich", ); } }); const serializer = new XMLSerializer(); zip.file( "word/document.xml", serializer.serializeToString(doc), ); return zip.generate({ type: "nodebuffer", compression: "DEFLATE", }); } const CUSTOM_NS = "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"; const VT_NS = "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"; const REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"; const CONTENT_TYPES_NS = "http://schemas.openxmlformats.org/package/2006/content-types"; const XMLNS_NS = "http://www.w3.org/2000/xmlns/"; const CUSTOM_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties"; const CUSTOM_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.custom-properties+xml"; const CUSTOM_FMTID = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"; const CUSTOM_PATH = "docProps/custom.xml"; type CustomPropertyValue = | string | number | boolean | Date | null | undefined; function parseXml(xml: string, fileName: string): XmlDocument { const errors: string[] = []; const parser = new DOMParser(); const doc = parser.parseFromString( xml, "application/xml", ) as XmlDocument; if (errors.length > 0 || !doc.documentElement) { throw new Error( `Invalid ${fileName}: ${errors.join("; ")}`, ); } return doc; } function assertRoot( doc: XmlDocument, namespace: string, localName: string, fileName: string, ): XmlElement { const root = doc.documentElement; if (root===null || root.localName !== localName || root.namespaceURI !== namespace ) { throw new Error(`Invalid root element in ${fileName}`); } return root; } function createCustomXml(): XmlDocument { const xml = [ '', ``, ].join(""); return parseXml(xml, CUSTOM_PATH); } function createValueElement( doc: XmlDocument, value: unknown, ): XmlElement { let tagName: string; let text: string; if (value == null) { // null และ undefined เก็บเป็น String ว่าง tagName = "vt:lpwstr"; text = ""; } else if (typeof value === "string") { tagName = "vt:lpwstr"; text = value; } else if (typeof value === "boolean") { tagName = "vt:bool"; text = value ? "true" : "false"; } else if (typeof value === "number") { if (!Number.isFinite(value)) { throw new TypeError( `Invalid numeric property value: ${value}`, ); } if (Number.isInteger(value)) { if (!Number.isSafeInteger(value)) { throw new TypeError( `Integer property exceeds safe integer range: ${value}`, ); } tagName = "vt:i8"; } else { tagName = "vt:r8"; } text = String(value); } else if (value instanceof Date) { if (Number.isNaN(value.getTime())) { throw new TypeError("Invalid Date property value"); } tagName = "vt:filetime"; text = value.toISOString(); } else { throw new TypeError( `Unsupported custom property type: ${ Object.prototype.toString.call(value) }`, ); } const element = doc.createElementNS(VT_NS, tagName); element.appendChild(doc.createTextNode(text)); return element; } function ensureCustomRelationship(zip: PizzaZip): void { const fileName = "_rels/.rels"; const file = zip.file(fileName); if (!file) { throw new Error(`DOCX is missing ${fileName}`); } const doc = parseXml(file.asText(), fileName); const root = assertRoot( doc, REL_NS, "Relationships", fileName, ); const relationships = Array.from( root.getElementsByTagNameNS(REL_NS, "Relationship"), ) as XmlElement[]; const existing = relationships.find( (item) => item.getAttribute("Type") === CUSTOM_REL_TYPE, ); if (existing) { if (existing.getAttribute("Target") !== CUSTOM_PATH) { throw new Error( "Existing custom-properties relationship has an unexpected Target", ); } return; } const ids = new Set( relationships.map((item) => item.getAttribute("Id")), ); let index = 1; let relationshipId = `rId${index}`; while (ids.has(relationshipId)) { index++; relationshipId = `rId${index}`; } const relationship = doc.createElementNS( REL_NS, "Relationship", ); relationship.setAttribute("Id", relationshipId); relationship.setAttribute("Type", CUSTOM_REL_TYPE); relationship.setAttribute("Target", CUSTOM_PATH); root.appendChild(relationship); zip.file( fileName, new XMLSerializer().serializeToString(doc), ); } function ensureCustomContentType(zip: PizzaZip): void { const fileName = "[Content_Types].xml"; const file = zip.file(fileName); if (!file) { throw new Error(`DOCX is missing ${fileName}`); } const doc = parseXml(file.asText(), fileName); const root = assertRoot( doc, CONTENT_TYPES_NS, "Types", fileName, ); const overrides = Array.from( root.getElementsByTagNameNS( CONTENT_TYPES_NS, "Override", ), ) as XmlElement[]; const existing = overrides.find( (item) => item.getAttribute("PartName") === "/docProps/custom.xml", ); if (existing) { existing.setAttribute("ContentType", CUSTOM_CONTENT_TYPE); } else { const override = doc.createElementNS( CONTENT_TYPES_NS, "Override", ); override.setAttribute( "PartName", "/docProps/custom.xml", ); override.setAttribute( "ContentType", CUSTOM_CONTENT_TYPE, ); root.appendChild(override); } zip.file( fileName, new XMLSerializer().serializeToString(doc), ); } /** how to use const templateBuffer = readFileSync("template.docx"); const outputBuffer = setDocPropsCustom(templateBuffer, { DocumentCode: "DOC-2026-001", Department: "ฝ่ายเทคโนโลยีสารสนเทศ", Revision: 2, IsApproved: true, PublishedAt: new Date(), }); writeFileSync("output.docx", outputBuffer); */ export function setCustomDocProps( docxBuffer: Buffer | Uint8Array, data: Record, ): Buffer { const zip = new PizzaZip(docxBuffer); // อ่าน Custom Properties เดิม หรือสร้างใหม่หากไม่มี const customFile = zip.file(CUSTOM_PATH); const doc = customFile ? parseXml(customFile.asText(), CUSTOM_PATH) : createCustomXml(); const root = assertRoot( doc, CUSTOM_NS, "Properties", CUSTOM_PATH, ); // รับรองว่ามี Namespace vt สำหรับ Value Elements root.setAttributeNS(XMLNS_NS, "xmlns:vt", VT_NS); const properties = Array.from( root.getElementsByTagNameNS(CUSTOM_NS, "property"), ) as XmlElement[]; const propertyMap = new Map(); const usedPids = new Set(); for (const property of properties) { const name = property.getAttribute("name"); const pidText = property.getAttribute("pid"); const pid = Number(pidText); if (!name) { throw new Error( "Custom property is missing its name attribute", ); } if (propertyMap.has(name)) { throw new Error(`Duplicate custom property name: ${name}`); } if (pidText === null || !/^[1-9]\d*$/.test(pidText) || !Number.isSafeInteger(pid) || pid < 2 ) { throw new Error( `Invalid custom property pid for "${name}": ${pidText}`, ); } if (usedPids.has(pid)) { throw new Error(`Duplicate custom property pid: ${pid}`); } propertyMap.set(name, property); usedPids.add(pid); } let nextPid = 2; for (const [name, value] of Object.entries(data)) { if (!name.trim()) { throw new Error("Custom property name must not be empty"); } let property = propertyMap.get(name); if (!property) { while (usedPids.has(nextPid)) { nextPid++; } property = doc.createElementNS( CUSTOM_NS, "property", ); property.setAttribute("fmtid", CUSTOM_FMTID); property.setAttribute("pid", String(nextPid)); property.setAttribute("name", name); usedPids.add(nextPid); nextPid++; root.appendChild(property); propertyMap.set(name, property); } // สร้างค่าใหม่ก่อนแก้ไข Property เดิม const valueElement = createValueElement(doc, value); while (property.firstChild) { property.removeChild(property.firstChild); } property.appendChild(valueElement); } // อัปเดตไฟล์ Custom Properties zip.file( CUSTOM_PATH, new XMLSerializer().serializeToString(doc), ); // ตรวจสอบส่วนประกอบที่เชื่อมโยงกับ Custom Properties ensureCustomRelationship(zip); ensureCustomContentType(zip); return Buffer.from( zip.generate({ type: "nodebuffer", compression: "DEFLATE", }), ); } /** * ดึงค่า Custom Document Properties จาก DOCX Buffer */ export function getCustomDocProps( docxBuffer: Buffer | Uint8Array, ): Record { const zip = new PizzaZip(docxBuffer); const customFile = zip.file(CUSTOM_PATH); if (!customFile) { return {}; } const doc = parseXml(customFile.asText(), CUSTOM_PATH); const root = assertRoot( doc, CUSTOM_NS, "Properties", CUSTOM_PATH, ); const properties = Array.from( root.getElementsByTagNameNS(CUSTOM_NS, "property"), ) as XmlElement[]; const result: Record = {}; for (const property of properties) { const name = property.getAttribute("name"); if (!name) continue; // หา child element แรกที่อยู่ใน VT_NS namespace let valueNode: XmlElement | undefined; for (let i = 0; i < property.childNodes.length; i++) { const node = property.childNodes[i] as XmlElement; if (node.nodeType === 1 && node.namespaceURI === VT_NS) { valueNode = node; break; } } if (!valueNode) { result[name] = ""; continue; } const textContent = valueNode.textContent || ""; // แปลงค่ากลับตาม VT Data Type switch (valueNode.localName) { case "bool": result[name] = textContent === "true" || textContent === "1"; break; case "i1": case "i2": case "i4": case "i8": case "int": case "ui1": case "ui2": case "ui4": case "ui8": case "uint": case "r4": case "r8": case "decimal": { const num = Number(textContent); result[name] = Number.isNaN(num) ? textContent : num; break; } case "filetime": case "date": { const date = new Date(textContent); result[name] = Number.isNaN(date.getTime()) ? textContent : date; break; } default: // vt:lpwstr, vt:lpstr, vt:bstr และอื่นๆ result[name] = textContent; break; } } return result; } const CORE_NS = "http://schemas.openxmlformats.org/package/2006/metadata/core-properties"; const DC_NS = "http://purl.org/dc/elements/1.1/"; const DCTERMS_NS = "http://purl.org/dc/terms/"; const APP_NS = "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"; export interface DocPropsOptions { standard?: boolean; extended?: boolean; custom?: boolean; } export interface StandardDocProps { title?: string; subject?: string; creator?: string; keywords?: string; description?: string; lastModifiedBy?: string; revision?: string; created?: Date; modified?: Date; category?: string; contentStatus?: string; } export interface ExtendedDocProps { template?: string; totalTime?: number; pages?: number; words?: number; characters?: number; charactersWithSpaces?: number; lines?: number; paragraphs?: number; application?: string; appVersion?: string; company?: string; manager?: string; docSecurity?: number; } export interface DocPropsResult { standard?: StandardDocProps; extended?: ExtendedDocProps; custom?: Record; } /** * ดึงข้อมูล Properties ของเอกสาร DOCX ตามประเภทที่เลือก * example: * const allProps = getDocProps(docxBuffer); * const selectedProps = getDocProps(docxBuffer, { * standard: true, * custom: true, * extended: false, * }); * console.log(allProps); * console.log(selectedProps.standard?.title); */ export function getDocProps( docxBuffer: Buffer | Uint8Array, options: DocPropsOptions = { standard: true, extended: true, custom: true }, ): DocPropsResult { const zip = new PizzaZip(docxBuffer); const result: DocPropsResult = {}; // 1. Standard / Core Properties (docProps/core.xml) if (options.standard) { const coreFile = zip.file("docProps/core.xml"); if (coreFile) { const doc = parseXml(coreFile.asText(), "docProps/core.xml"); const getText = (ns: string, localName: string) => { const els = doc.getElementsByTagNameNS(ns, localName); return els.length > 0 ? els[0].textContent || "" : undefined; }; const parseDate = (dateStr?: string) => { if (!dateStr) return undefined; const d = new Date(dateStr); return Number.isNaN(d.getTime()) ? undefined : d; }; result.standard = { title: getText(DC_NS, "title"), subject: getText(DC_NS, "subject"), creator: getText(DC_NS, "creator"), keywords: getText(CORE_NS, "keywords"), description: getText(DC_NS, "description"), lastModifiedBy: getText(CORE_NS, "lastModifiedBy"), revision: getText(CORE_NS, "revision"), created: parseDate(getText(DCTERMS_NS, "created")), modified: parseDate(getText(DCTERMS_NS, "modified")), category: getText(CORE_NS, "category"), contentStatus: getText(CORE_NS, "contentStatus"), }; } } // 2. Extended / Automatically Updated Properties (docProps/app.xml) if (options.extended) { const appFile = zip.file("docProps/app.xml"); if (appFile) { const doc = parseXml(appFile.asText(), "docProps/app.xml"); const getText = (localName: string) => { const els = doc.getElementsByTagNameNS(APP_NS, localName); return els.length > 0 ? els[0].textContent || "" : undefined; }; const parseNum = (val?: string) => { if (!val) return undefined; const n = Number(val); return Number.isNaN(n) ? undefined : n; }; result.extended = { template: getText("Template"), totalTime: parseNum(getText("TotalTime")), pages: parseNum(getText("Pages")), words: parseNum(getText("Words")), characters: parseNum(getText("Characters")), charactersWithSpaces: parseNum(getText("CharactersWithSpaces")), lines: parseNum(getText("Lines")), paragraphs: parseNum(getText("Paragraphs")), application: getText("Application"), appVersion: getText("AppVersion"), company: getText("Company"), manager: getText("Manager"), docSecurity: parseNum(getText("DocSecurity")), }; } } // 3. Custom Properties (docProps/custom.xml) if (options.custom) { result.custom = getCustomDocProps(docxBuffer); } return result; } //**************************** dynamic content ******************************************* // sdtLocked(ห้ามลบ CC), contentLocked(ลบได้ห้ามแก้), sdtContentLocked(ห้ามทุกอย่าง) type LockType = 'none' | 'sdtLocked' | 'contentLocked' | 'sdtContentLocked'; /** * ฟังก์ชันสร้าง Plain Text Content Control XML String ครอบด้วย || * ระวังการเกิดหลายรัน จะทำให้ xml เสีย */ function p_cc(title:string,tag:string,text:string='',isMultiLine:boolean=true,lock:LockType='sdtLocked'): string { const lockTag = lock=='none'?"":`` const multiLineTag = isMultiLine ? '':'' const content = escapeXml(text) const xmlString =`${lockTag}${multiLineTag}${content}` return `||${xmlString}||`; } function br(n: number) { let text="" for(let i=0;i||`; } /** * Escapes XML Special Characters */ function escapeXml(unsafe: string): string { return unsafe .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } export const templateDynamicContents = async (template: Buffer, data: Record): Promise => { return await createReport({ template, data, cmdDelimiter: ['{{', '}}'], additionalJsContext: { bm,br, p_cc, } }); }