Multi-thread support for applyTemplate
This commit is contained in:
parent
f7e84e3ee7
commit
bfb07c633d
10 changed files with 1260 additions and 154 deletions
|
|
@ -7,34 +7,34 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
const EXTERNAL_API_KEY = process.env.EXTERNAL_API_KEY||'';
|
const EXTERNAL_API_KEY = process.env.EXTERNAL_API_KEY || '';
|
||||||
const TMPL_PATH = process.env.TMPL_PATH || "./public/tmpl";
|
const TMPL_PATH = process.env.TMPL_PATH || "./public/tmpl";
|
||||||
const DOCEDIT_BASE_URL = process.env.DOCEDIT_BASE_URL || '';
|
const DOCEDIT_BASE_URL = process.env.DOCEDIT_BASE_URL || '';
|
||||||
|
|
||||||
async function processRequest(fid: string,tid:string,uid:string,uname:string):Promise<NextResponse<unknown>>
|
async function processRequest(fid: string, tid: string, uid: string, uname: string): Promise<NextResponse<unknown>> {
|
||||||
{
|
|
||||||
//กัน path traversal attack
|
//กัน path traversal attack
|
||||||
const TID = path.basename(tid)
|
const TID = path.basename(tid)
|
||||||
const FID = path.basename(fid)
|
const FID = path.basename(fid)
|
||||||
const ticketObj = {fid:FID,tid:TID,uid,uname}
|
const ticketObj = { fid: FID, tid: TID, uid, uname }
|
||||||
const ticket_data = JSON.stringify({fid:FID,tid:TID,uid,uname})
|
const ticket_data = JSON.stringify({ fid: FID, tid: TID, uid, uname })
|
||||||
const filePath = path.join(TMPL_PATH,`${FID}.json`);
|
const filePath = path.join(TMPL_PATH, `${FID}.json`);
|
||||||
// สำหรับการ demo ในแอปเดียวเท่านั้น สามารถคำนวนหา base path ได้ด้วยวิธีนี้
|
// สำหรับการ demo ในแอปเดียวเท่านั้น สามารถคำนวนหา base path ได้ด้วยวิธีนี้
|
||||||
// const host = headers().get("host"); // e.g., localhost:3000
|
// const host = headers().get("host"); // e.g., localhost:3000
|
||||||
// const proto = headers().get("x-forwarded-proto") || "http";
|
// const proto = headers().get("x-forwarded-proto") || "http";
|
||||||
// const baseUrl = process.env.NEXT_PUBLIC_BASE_PATH || ""
|
// const baseUrl = process.env.NEXT_PUBLIC_BASE_PATH || ""
|
||||||
// const fullUrl = `${proto}://${host}${baseUrl}`;
|
// const fullUrl = `${proto}://${host}${baseUrl}`;
|
||||||
// const DOCEDIT_BASE_URL = process.env.DOCEDIT_BASE_URL || fullUrl;
|
// const DOCEDIT_BASE_URL = process.env.DOCEDIT_BASE_URL || fullUrl;
|
||||||
if(!DOCEDIT_BASE_URL || !EXTERNAL_API_KEY) {
|
if (!DOCEDIT_BASE_URL || !EXTERNAL_API_KEY) {
|
||||||
return NextResponse.json({ error: 'Server must set Environment variable DOCEDIT_BASE_URL or EXTERNAL_API_KEY' }, { status: 500 });
|
return NextResponse.json({ error: 'Server must set Environment variable DOCEDIT_BASE_URL or EXTERNAL_API_KEY' }, { status: 500 });
|
||||||
}
|
}
|
||||||
if (!FID||!TID||!uid||!uname) {
|
if (!FID || !TID || !uid || !uname) {
|
||||||
return NextResponse.json({ error: 'Missing fid, tid, uid or uname' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing fid, tid, uid or uname' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try { //ตรวจสอบ docx เวอร์ชั่นล่าสุดก่อนถ้าไม่พบค่อยเอาจากฐานข้อมูลมาสร้าง raw Json
|
try { //ตรวจสอบ docx เวอร์ชั่นล่าสุดก่อนถ้าไม่พบค่อยเอาจากฐานข้อมูลมาสร้าง raw Json
|
||||||
const params = new URLSearchParams({type:'docx',ticket_data});
|
const params = new URLSearchParams({ type: 'docx', ticket_data });
|
||||||
|
console.log(`search ${DOCEDIT_BASE_URL}/api/document/latest?${params.toString()}`);
|
||||||
const lastestDocxResponse = await fetch(`${DOCEDIT_BASE_URL}/api/document/latest?${params.toString()}`, {
|
const lastestDocxResponse = await fetch(`${DOCEDIT_BASE_URL}/api/document/latest?${params.toString()}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -42,34 +42,51 @@ async function processRequest(fid: string,tid:string,uid:string,uname:string):Pr
|
||||||
'x-api-key': EXTERNAL_API_KEY,
|
'x-api-key': EXTERNAL_API_KEY,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// เช็ค Content-Type และ Status ก่อน
|
||||||
|
const contentType = lastestDocxResponse.headers.get('content-type') || '';
|
||||||
|
if (lastestDocxResponse.ok && !!contentType.includes('text/html')) {
|
||||||
|
console.log('Fund latest docx from docedit, returning stream to browser');
|
||||||
|
const stream = lastestDocxResponse.body;
|
||||||
|
//ส่ง Binary Stream กลับไปยัง Browser พร้อม Header กำหนดประเภทไฟล์
|
||||||
|
const download_file = `${FID}_${uid}_latest`
|
||||||
|
return new NextResponse(stream as unknown as BodyInit, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type':
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'Content-Disposition': `attachment; filename="${encodeURIComponent(download_file)}.docx"`,
|
||||||
|
'Cache-Control': 'no-store, max-age=0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if(!lastestDocxResponse.ok && lastestDocxResponse.status !== 404){//เกิดปัญหาที่ไม่ใช่ Not Found
|
if (lastestDocxResponse.status !== 404) {//เกิดปัญหาที่ไม่ใช่ Not Found น่าจะเป็นปัญหาที่ gateway
|
||||||
const errorText = await lastestDocxResponse.text();
|
const errorText = await lastestDocxResponse.text();
|
||||||
console.error('download-docx:Invalid latest docx Response:', errorText);
|
console.error('download-docx:Invalid latest docx Response:', errorText);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error:`Error from docedit latest: Expected docx stream but received ${lastestDocxResponse.status} ${lastestDocxResponse.statusText}`},
|
{ error: `Error from docedit latest: Expected docx stream but received ${lastestDocxResponse.status} ${lastestDocxResponse.statusText}` },
|
||||||
{ status: 502 }
|
{ status: 502 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
return NextResponse.json({error:'Internal Server Error'}, { status: 500 });
|
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
console.log('No version found, generating docx from template and JSON data...');
|
console.log(`Not found latest docx from docedit, fallback to generate from template and database: ${filePath}`);
|
||||||
|
|
||||||
try { // ไม่พบเวอร์ชั้นให้สร้าง docx จาก template และ json(ฐานข้อมูล)
|
try { // ไม่พบเวอร์ชั้นให้สร้าง docx จาก template และ json(ฐานข้อมูล)
|
||||||
// อ่านไฟล์ JSON จำลองการอ่านจากฐานข้อมูล
|
// อ่านไฟล์ JSON จำลองการอ่านจากฐานข้อมูล
|
||||||
const rawJson = await fs.readFile(filePath, 'utf-8');
|
const rawJson = await fs.readFile(filePath, 'utf-8');
|
||||||
const jsonData = JSON.parse(rawJson);
|
const jsonData = JSON.parse(rawJson);
|
||||||
if (!jsonData || typeof jsonData !== 'object' ) {
|
if (!jsonData || typeof jsonData !== 'object') {
|
||||||
return NextResponse.json({error:'Invalid data format'}, { status: 400 });
|
return NextResponse.json({ error: 'Invalid data format' }, { status: 400 });
|
||||||
}
|
}
|
||||||
/////// !! ตรวจสอบสิทธิ์ผู้ใช้ uid ว่ามีสิทธิ์เข้าถึงไฟล์ fid หรือไม่เพื่อกันการการยิง API ตรงๆ ไม่ผ่าน Frontend !!//
|
/////// !! ตรวจสอบสิทธิ์ผู้ใช้ uid ว่ามีสิทธิ์เข้าถึงไฟล์ fid หรือไม่เพื่อกันการการยิง API ตรงๆ ไม่ผ่าน Frontend !!//
|
||||||
/////// ไม่มี session return 401, ถ้าเจอแต่ไม่มีสิทธิ์ให้ return 403 , อ่านจากฐานข้อมูล ถ้าไม่พบข้อมูลให้ return 404,
|
/////// ไม่มี session return 401, ถ้าเจอแต่ไม่มีสิทธิ์ให้ return 403 , อ่านจากฐานข้อมูล ถ้าไม่พบข้อมูลให้ return 404,
|
||||||
|
|
||||||
// ข้อมูลจำเป็นต้องมี jsonData.info.ticket_data ถ้าไม่มีให้สร้างขึ้นมาเอง
|
// ข้อมูลจำเป็นต้องมี jsonData.info.ticket_data ถ้าไม่มีให้สร้างขึ้นมาเอง
|
||||||
if(!Object.hasOwn(jsonData, 'info'))
|
if (!Object.hasOwn(jsonData, 'info'))
|
||||||
jsonData['info']={}
|
jsonData['info'] = {}
|
||||||
jsonData.info['ticket_data'] = JSON.stringify(ticketObj)
|
jsonData.info['ticket_data'] = JSON.stringify(ticketObj)
|
||||||
// sign เองตรงนี้ให้อยู่ในรูปแบบ data.info={ticket_data,HmacSha256,Ed25519} หรือว่างไว้ให้ docedit สร้างให้
|
// sign เองตรงนี้ให้อยู่ในรูปแบบ data.info={ticket_data,HmacSha256,Ed25519} หรือว่างไว้ให้ docedit สร้างให้
|
||||||
|
|
||||||
|
|
@ -90,7 +107,7 @@ async function processRequest(fid: string,tid:string,uid:string,uname:string):Pr
|
||||||
console.error('download-docx:Invalid docx Response:', errorText);
|
console.error('download-docx:Invalid docx Response:', errorText);
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error:`Error from docedit generate: Expected docx stream but received ${contentType} (Status ${docxResponse.status})`},
|
{ error: `Error from docedit generate: Expected docx stream but received ${contentType} (Status ${docxResponse.status})` },
|
||||||
{ status: 502 }
|
{ status: 502 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -108,26 +125,25 @@ async function processRequest(fid: string,tid:string,uid:string,uname:string):Pr
|
||||||
'Cache-Control': 'no-store, max-age=0',
|
'Cache-Control': 'no-store, max-age=0',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error:any) {
|
} catch (error: any) {
|
||||||
if ( error.code === 'ENOENT') {// ไฟล์ JSON ไม่พบ ถ้าใช้ฐานข้อมูลจริงให้ return 404 แทน
|
if (error.code === 'ENOENT') {// ไฟล์ JSON ไม่พบ ถ้าใช้ฐานข้อมูลจริงให้ return 404 ตั้งแต่แต่แรก และเอา ENOENT ออก
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: `Data not found: ${filePath}`},
|
{ error: `Data not found: ${filePath}` },
|
||||||
{ status: 404 }
|
{ status: 404 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.error(error)
|
console.error(error)
|
||||||
return NextResponse.json({error:'Internal Server Error'}, { status: 500 });
|
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest)
|
export async function GET(request: NextRequest) {
|
||||||
{
|
|
||||||
const searchParams = request.nextUrl.searchParams;
|
const searchParams = request.nextUrl.searchParams;
|
||||||
const fid = searchParams.get('fid') || '';
|
const fid = searchParams.get('fid') || '';
|
||||||
const tid = searchParams.get('tid') || '';
|
const tid = searchParams.get('tid') || '';
|
||||||
const uid = searchParams.get('uid') || '';
|
const uid = searchParams.get('uid') || '';
|
||||||
const uname = searchParams.get('uname') || '';
|
const uname = searchParams.get('uname') || '';
|
||||||
return await processRequest(fid,tid,uid,uname)
|
return await processRequest(fid, tid, uid, uname)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
|
|
@ -136,5 +152,5 @@ export async function POST(request: NextRequest) {
|
||||||
const tid = String(formData.get('tid'))
|
const tid = String(formData.get('tid'))
|
||||||
const uid = String(formData.get('uid'))
|
const uid = String(formData.get('uid'))
|
||||||
const uname = String(formData.get('uname'))
|
const uname = String(formData.get('uname'))
|
||||||
return await processRequest(fid,tid,uid,uname)
|
return await processRequest(fid, tid, uid, uname)
|
||||||
}
|
}
|
||||||
|
|
@ -6,8 +6,11 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||||
import {readFile} from 'fs/promises';
|
import {readFile} from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {DocumentTemplate,applyTemplate} from "@/lib/lib-tmpl";
|
import {DocumentTemplate,applyTemplate} from "@/lib/lib-tmpl";
|
||||||
|
import { docxPool } from '@/lib/docx-pool';
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
const EXTERNAL_API_KEY = process.env.EXTERNAL_API_KEY || 'secret-api-key-1234';
|
|
||||||
|
const EXTERNAL_API_KEY = process.env.EXTERNAL_API_KEY || '';
|
||||||
const PRIVATE_KEY_HEX = process.env.PRIVATE_KEY_HEX ||''
|
const PRIVATE_KEY_HEX = process.env.PRIVATE_KEY_HEX ||''
|
||||||
const SECRET = process.env.SECRET || 'super_secret'
|
const SECRET = process.env.SECRET || 'super_secret'
|
||||||
const TMPL_PATH = process.env.TMPL_PATH || "./public/tmpl";
|
const TMPL_PATH = process.env.TMPL_PATH || "./public/tmpl";
|
||||||
|
|
@ -64,7 +67,16 @@ export async function POST(request: NextRequest) {
|
||||||
const tmplPath = path.join(TMPL_PATH, `${path.basename(tid)}.docx`);
|
const tmplPath = path.join(TMPL_PATH, `${path.basename(tid)}.docx`);
|
||||||
let templateBuffer: Buffer = await readFile(tmplPath)
|
let templateBuffer: Buffer = await readFile(tmplPath)
|
||||||
|
|
||||||
const output = await applyTemplate(templateBuffer, templateData, SECRET, PRIVATE_KEY_HEX, TICKET_SEC);
|
// const output = await applyTemplate(templateBuffer, templateData, SECRET, PRIVATE_KEY_HEX, TICKET_SEC);
|
||||||
|
const output = await docxPool.run({
|
||||||
|
templateBuffer,
|
||||||
|
templateData,
|
||||||
|
secret: SECRET,
|
||||||
|
privateKeyHex: PRIVATE_KEY_HEX,
|
||||||
|
ticketSec: TICKET_SEC
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputBuffer = Buffer.from(output);
|
||||||
|
|
||||||
// สร้าง ReadableStream ของไฟล์ .docx
|
// สร้าง ReadableStream ของไฟล์ .docx
|
||||||
// ในโค้ดจริง ส่วนนี้จะดึง Binary Buffer จาก Library เช่น docx, docxtemplater หรืออ่านจาก File System
|
// ในโค้ดจริง ส่วนนี้จะดึง Binary Buffer จาก Library เช่น docx, docxtemplater หรืออ่านจาก File System
|
||||||
|
|
@ -72,7 +84,7 @@ export async function POST(request: NextRequest) {
|
||||||
|
|
||||||
const docxStream = new ReadableStream({
|
const docxStream = new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
controller.enqueue(output);
|
controller.enqueue(outputBuffer);
|
||||||
controller.close();
|
controller.close();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
35
lib/docx-pool.ts
Normal file
35
lib/docx-pool.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import Piscina from 'piscina';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { availableParallelism } from 'node:os';
|
||||||
|
|
||||||
|
const configuredThreads = Number(
|
||||||
|
process.env.DOCX_WORKERS || 4
|
||||||
|
);
|
||||||
|
|
||||||
|
const maxThreads = Math.max(
|
||||||
|
1,
|
||||||
|
Math.min(
|
||||||
|
Number.isFinite(configuredThreads) && configuredThreads > 0
|
||||||
|
? Math.floor(configuredThreads)
|
||||||
|
: 4,
|
||||||
|
Math.max(1, availableParallelism() - 1)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const globalForDocx = globalThis as typeof globalThis & {
|
||||||
|
docxPool?: Piscina;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const docxPool =
|
||||||
|
globalForDocx.docxPool ??=
|
||||||
|
new Piscina({
|
||||||
|
filename: path.resolve(
|
||||||
|
process.cwd(),
|
||||||
|
'workers/dist/docx.worker.mjs'
|
||||||
|
),
|
||||||
|
minThreads: 1,
|
||||||
|
maxThreads,
|
||||||
|
idleTimeout: 30000,
|
||||||
|
maxQueue: 'auto',
|
||||||
|
concurrentTasksPerWorker: 1
|
||||||
|
});
|
||||||
255
lib/lib-tmpl.ts
255
lib/lib-tmpl.ts
|
|
@ -1,6 +1,26 @@
|
||||||
|
// สำหรับ run dev ถ้ามีการแก้โค้ดในนี้อย่าลืมเรียก npm run build:worker ก่อน
|
||||||
import { generateHmacSha256, signEd25519, sortObjectKeys, verifyEd25519, verifyHmacSha256 } from '@/lib/lib-utils';
|
import { generateHmacSha256, signEd25519, sortObjectKeys, verifyEd25519, verifyHmacSha256 } from '@/lib/lib-utils';
|
||||||
import createReport from 'docx-templates';
|
|
||||||
import PizzaZip from "pizzip";
|
import PizzaZip from "pizzip";
|
||||||
|
// import createReport from 'docx-templates';
|
||||||
|
import type { createReport as CreateReportType } from 'docx-templates';
|
||||||
|
const getCreateReport = async () => {
|
||||||
|
const mod = await import('docx-templates');
|
||||||
|
|
||||||
|
const createReportFn =
|
||||||
|
typeof mod.createReport === 'function'
|
||||||
|
? mod.createReport
|
||||||
|
: typeof mod.default === 'function'
|
||||||
|
? mod.default
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (!createReportFn) {
|
||||||
|
throw new Error('docx-templates createReport is not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
return createReportFn as typeof CreateReportType;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DOMParser,
|
DOMParser,
|
||||||
XMLSerializer,
|
XMLSerializer,
|
||||||
|
|
@ -29,69 +49,69 @@ function createSelector() {
|
||||||
* ฟังก์ชันสำหรับดึงข้อมูลจาก docx ออกมาเป็น JSON โดยอิงจาก Bookmark (w:bookmarkStart)
|
* ฟังก์ชันสำหรับดึงข้อมูลจาก docx ออกมาเป็น JSON โดยอิงจาก Bookmark (w:bookmarkStart)
|
||||||
*/
|
*/
|
||||||
export function extractBookmark(docxBuffer: Buffer,): ExtractedData {
|
export function extractBookmark(docxBuffer: Buffer,): ExtractedData {
|
||||||
const zip = new PizzaZip(docxBuffer);
|
const zip = new PizzaZip(docxBuffer);
|
||||||
const docXmlFile = zip.file("word/document.xml");
|
const docXmlFile = zip.file("word/document.xml");
|
||||||
|
|
||||||
if (!docXmlFile) {
|
if (!docXmlFile) {
|
||||||
throw new Error("Invalid docx file: word/document.xml not found.");
|
throw new Error("Invalid docx file: word/document.xml not found.");
|
||||||
}
|
}
|
||||||
const docXmlText: string = docXmlFile.asText();
|
const docXmlText: string = docXmlFile.asText();
|
||||||
|
|
||||||
// 3. Parse ข้อความ XML ให้เป็น DOM Object
|
// 3. Parse ข้อความ XML ให้เป็น DOM Object
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(docXmlText, "text/xml");
|
const doc = parser.parseFromString(docXmlText, "text/xml");
|
||||||
|
|
||||||
// 4. ตั้งค่า Namespace ของ Word XML เพื่อใช้ค้นหาด้วย XPath
|
// 4. ตั้งค่า Namespace ของ Word XML เพื่อใช้ค้นหาด้วย XPath
|
||||||
const select = xpath.useNamespaces({
|
const select = xpath.useNamespaces({
|
||||||
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: ExtractedData = {};
|
const result: ExtractedData = {};
|
||||||
|
|
||||||
// ค้นหา Bookmark หรือ Tag ที่ใช้แบ่ง Section
|
// ค้นหา 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 Element[];
|
||||||
const bookmarks = select(
|
const bookmarks = select(
|
||||||
"//w:bookmarkStart",
|
"//w:bookmarkStart",
|
||||||
doc as unknown as globalThis.Node
|
doc as unknown as globalThis.Node
|
||||||
) as unknown as XmlElement[];
|
) as unknown as XmlElement[];
|
||||||
|
|
||||||
bookmarks.forEach((bookmark: XmlElement) => {
|
bookmarks.forEach((bookmark: XmlElement) => {
|
||||||
const sectionName = bookmark.getAttribute('w:name');
|
const sectionName = bookmark.getAttribute('w:name');
|
||||||
const bookmarkId = bookmark.getAttribute('w:id');
|
const bookmarkId = bookmark.getAttribute('w:id');
|
||||||
|
|
||||||
// กรองเอาเฉพาะ Bookmark ที่เราสนใจ (ไม่เอาพวกระบบสร้างเองอย่าง _GoBack)
|
// กรองเอาเฉพาะ Bookmark ที่เราสนใจ (ไม่เอาพวกระบบสร้างเองอย่าง _GoBack)
|
||||||
if (sectionName && !sectionName.startsWith('_')) {
|
if (sectionName && !sectionName.startsWith('_')) {
|
||||||
let extractedText = "";
|
let extractedText = "";
|
||||||
let currentNode: XmlNode | null = bookmark.nextSibling;
|
let currentNode: XmlNode | null = bookmark.nextSibling;
|
||||||
|
|
||||||
// วนลูปอ่าน Node ถัดไปเรื่อยๆ จนกว่าจะเจอจุดสิ้นสุดของ Bookmark
|
// วนลูปอ่าน Node ถัดไปเรื่อยๆ จนกว่าจะเจอจุดสิ้นสุดของ Bookmark
|
||||||
while (currentNode) {
|
while (currentNode) {
|
||||||
// ถ้าเจอจุดจบของ Bookmark ตัวเอง ให้หยุด
|
// ถ้าเจอจุดจบของ Bookmark ตัวเอง ให้หยุด
|
||||||
if (
|
if (
|
||||||
currentNode.nodeType === 1 && // Element node
|
currentNode.nodeType === 1 && // Element node
|
||||||
(currentNode as XmlElement).nodeName === 'w:bookmarkEnd' &&
|
(currentNode as XmlElement).nodeName === 'w:bookmarkEnd' &&
|
||||||
(currentNode as XmlElement).getAttribute('w:id') === bookmarkId
|
(currentNode as XmlElement).getAttribute('w:id') === bookmarkId
|
||||||
) {
|
) {
|
||||||
break;
|
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;
|
// ดึง 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -402,10 +422,10 @@ function insertBlockContent(
|
||||||
|
|
||||||
const firstRPr = firstRun
|
const firstRPr = firstRun
|
||||||
? (select(
|
? (select(
|
||||||
"./w:rPr",
|
"./w:rPr",
|
||||||
asXpathNode(firstRun),
|
asXpathNode(firstRun),
|
||||||
true,
|
true,
|
||||||
) as unknown as XmlElement | undefined)
|
) as unknown as XmlElement | undefined)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// ลบย่อหน้าเดิมทั้งหมดยกเว้นย่อหน้าแรก
|
// ลบย่อหน้าเดิมทั้งหมดยกเว้นย่อหน้าแรก
|
||||||
|
|
@ -481,7 +501,7 @@ function insertFallbackContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PropData = Record<string, unknown>;
|
export type PropData = Record<string, unknown>;
|
||||||
export type DocumentTemplate = {info?:PropData,dc?:PropData,cc:PropData,};
|
export type DocumentTemplate = { info?: PropData, dc?: PropData, cc: PropData, };
|
||||||
|
|
||||||
function normalizeValue(value: unknown): string {
|
function normalizeValue(value: unknown): string {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
|
|
@ -509,7 +529,7 @@ function normalizeValue(value: unknown): string {
|
||||||
* คืนค่าเป็น Buffer ของ DOCX ที่สร้างเสร็จแล้ว
|
* คืนค่าเป็น Buffer ของ DOCX ที่สร้างเสร็จแล้ว
|
||||||
*/
|
*/
|
||||||
export function templateContentControls(
|
export function templateContentControls(
|
||||||
templateBuffer: Buffer|Uint8Array<ArrayBufferLike>,
|
templateBuffer: Buffer | Uint8Array<ArrayBufferLike>,
|
||||||
data: PropData,
|
data: PropData,
|
||||||
): Buffer {
|
): Buffer {
|
||||||
const zip = new PizzaZip(templateBuffer);
|
const zip = new PizzaZip(templateBuffer);
|
||||||
|
|
@ -656,7 +676,7 @@ function assertRoot(
|
||||||
): XmlElement {
|
): XmlElement {
|
||||||
const root = doc.documentElement;
|
const root = doc.documentElement;
|
||||||
|
|
||||||
if (root===null ||
|
if (root === null ||
|
||||||
root.localName !== localName ||
|
root.localName !== localName ||
|
||||||
root.namespaceURI !== namespace
|
root.namespaceURI !== namespace
|
||||||
) {
|
) {
|
||||||
|
|
@ -721,8 +741,7 @@ function createValueElement(
|
||||||
text = value.toISOString();
|
text = value.toISOString();
|
||||||
} else {
|
} else {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
`Unsupported custom property type: ${
|
`Unsupported custom property type: ${Object.prototype.toString.call(value)
|
||||||
Object.prototype.toString.call(value)
|
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1290,22 +1309,20 @@ type LockType = 'none' | 'sdtLocked' | 'contentLocked' | 'sdtContentLocked';
|
||||||
* ฟังก์ชันสร้าง Plain Text Content Control XML String ครอบด้วย ||
|
* ฟังก์ชันสร้าง Plain Text Content Control XML String ครอบด้วย ||
|
||||||
* ระวังการเกิดหลายรัน จะทำให้ xml เสีย
|
* ระวังการเกิดหลายรัน จะทำให้ xml เสีย
|
||||||
*/
|
*/
|
||||||
function p_cc(title:string,tag:string,text:string='',isMultiLine:boolean=true,lock:LockType='sdtLocked'): string {
|
function p_cc(title: string, tag: string, text: string = '', isMultiLine: boolean = true, lock: LockType = 'sdtLocked'): string {
|
||||||
const lockTag = lock=='none'?"":`<w:lock w:val="${lock}" />`
|
const lockTag = lock == 'none' ? "" : `<w:lock w:val="${lock}" />`
|
||||||
const multiLineTag = isMultiLine ? '<w:text w:multiLine="1" />':''
|
const multiLineTag = isMultiLine ? '<w:text w:multiLine="1" />' : ''
|
||||||
const content = escapeXml(text)
|
const content = escapeXml(text)
|
||||||
const xmlString =`<w:sdt><w:sdtPr><w:alias w:val="${title}" /><w:tag w:val="${tag}" /><w:id w:val="-1948841888" />${lockTag}<w:placeholder><w:docPart w:val="C3E615E9D8424D6ABEA7EADA60C3EFB6" /></w:placeholder>${multiLineTag}</w:sdtPr><w:sdtEndPr /><w:sdtContent><w:r><w:rPr><w:rFonts w:cs="TH SarabunPSK" /></w:rPr><w:t xml:space="preserve">${content}</w:t></w:r></w:sdtContent></w:sdt>`
|
const xmlString = `<w:sdt><w:sdtPr><w:alias w:val="${title}" /><w:tag w:val="${tag}" /><w:id w:val="-1948841888" />${lockTag}<w:placeholder><w:docPart w:val="C3E615E9D8424D6ABEA7EADA60C3EFB6" /></w:placeholder>${multiLineTag}</w:sdtPr><w:sdtEndPr /><w:sdtContent><w:r><w:rPr><w:rFonts w:cs="TH SarabunPSK" /></w:rPr><w:t xml:space="preserve">${content}</w:t></w:r></w:sdtContent></w:sdt>`
|
||||||
return `||${xmlString}||`;
|
return `||${xmlString}||`;
|
||||||
}
|
}
|
||||||
function br(n: number)
|
function br(n: number) {
|
||||||
{
|
let text = ""
|
||||||
let text=""
|
for (let i = 0; i < n; i++) text += "<w:br/>"
|
||||||
for(let i=0;i<n;i++) text += "<w:br/>"
|
return `||${text}||`;
|
||||||
return `||${text}||`;
|
|
||||||
}
|
}
|
||||||
function bm(id: number, name: string)
|
function bm(id: number, name: string) {
|
||||||
{
|
return `||<w:bookmarkStart w:id="${id}" w:name="${name}" /><w:bookmarkEnd w:id="${id}" />||`;
|
||||||
return `||<w:bookmarkStart w:id="${id}" w:name="${name}" /><w:bookmarkEnd w:id="${id}" />||`;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Escapes XML Special Characters
|
* Escapes XML Special Characters
|
||||||
|
|
@ -1319,13 +1336,14 @@ function escapeXml(unsafe: string): string {
|
||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
export const templateDynamicContents = async (template: Buffer, data: Record<string, unknown>): Promise<Uint8Array> => {
|
export const templateDynamicContents = async (template: Buffer, data: Record<string, unknown>): Promise<Uint8Array> => {
|
||||||
return await createReport({
|
const createReport = await getCreateReport();
|
||||||
template, data, cmdDelimiter: ['{{', '}}'],
|
return await createReport({
|
||||||
additionalJsContext: {
|
template, data, cmdDelimiter: ['{{', '}}'],
|
||||||
bm,br,
|
additionalJsContext: {
|
||||||
p_cc,
|
bm, br,
|
||||||
}
|
p_cc,
|
||||||
});
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// สร้างเอกสารจาก เทมเพลต+data render และ generate ใช้งานเหมือนกัน
|
// สร้างเอกสารจาก เทมเพลต+data render และ generate ใช้งานเหมือนกัน
|
||||||
|
|
@ -1334,47 +1352,46 @@ export const templateDynamicContents = async (template: Buffer, data: Record<str
|
||||||
// - สร้าง signature/hash จาก ticket_data
|
// - สร้าง signature/hash จาก ticket_data
|
||||||
// - สร้าง Dynamic Content (docx-templates)
|
// - สร้าง Dynamic Content (docx-templates)
|
||||||
// - เอาข้อมูลใส่ Content Control
|
// - เอาข้อมูลใส่ Content Control
|
||||||
export async function applyTemplate(templateBuffer:Buffer,templateData:DocumentTemplate,
|
export async function applyTemplate(templateBuffer: Buffer, templateData: DocumentTemplate,
|
||||||
secret:string,private_key:string,ttl:number):Promise<Buffer<ArrayBufferLike>>
|
secret: string, private_key: string, ttl: number): Promise<Buffer<ArrayBufferLike>> {
|
||||||
{
|
//Calculate hash
|
||||||
//Calculate hash
|
const originalTicket_data = templateData?.info?.ticket_data //expect valid json string
|
||||||
const originalTicket_data = templateData?.info?.ticket_data //expect valid json string
|
// console.log("templateData",templateData?.info?.ticket_data)
|
||||||
// console.log("templateData",templateData?.info?.ticket_data)
|
if (originalTicket_data && typeof originalTicket_data === 'string') {
|
||||||
if (originalTicket_data && typeof originalTicket_data === 'string') {
|
try {
|
||||||
try {
|
const originalTicketObj = JSON.parse(originalTicket_data)//sort and add expire to ticket data
|
||||||
const originalTicketObj = JSON.parse(originalTicket_data)//sort and add expire to ticket data
|
if (originalTicketObj && typeof originalTicketObj === 'object') {
|
||||||
if (originalTicketObj && typeof originalTicketObj === 'object') {
|
console.log("Found valid ticket data: hash it")
|
||||||
console.log("Found valid ticket data: hash it")
|
// คำนวณค่า expire (ปัจจุบัน + จำนวนวินาทีที่ต้องการให้หมดอายุ)
|
||||||
// คำนวณค่า expire (ปัจจุบัน + จำนวนวินาทีที่ต้องการให้หมดอายุ)
|
// หาร 1000 เพราะต้องการหน่วยวินาที (sec) ไม่ใช่มิลลิวินาที (ms)
|
||||||
// หาร 1000 เพราะต้องการหน่วยวินาที (sec) ไม่ใช่มิลลิวินาที (ms)
|
const expireTimestamp = Math.floor(Date.now() / 1000) + ttl;
|
||||||
const expireTimestamp = Math.floor(Date.now() / 1000) + ttl;
|
const fullObj = { ...originalTicketObj, exp: expireTimestamp };
|
||||||
const fullObj = { ...originalTicketObj, exp: expireTimestamp };
|
const sortedObj = sortObjectKeys(fullObj);
|
||||||
const sortedObj = sortObjectKeys(fullObj);
|
|
||||||
|
|
||||||
// แปลงกลับเป็น JSON string เพื่อนำไปใช้ส่งและทำ Hash
|
// แปลงกลับเป็น JSON string เพื่อนำไปใช้ส่งและทำ Hash
|
||||||
const ticket_data = JSON.stringify(sortedObj);
|
const ticket_data = JSON.stringify(sortedObj);
|
||||||
// console.log("check secret:",TEST_SECRET,TEST_PRIVATE_KEY_HEX,ticket_data)
|
// console.log("check secret:",TEST_SECRET,TEST_PRIVATE_KEY_HEX,ticket_data)
|
||||||
const HmacSha256 = generateHmacSha256(ticket_data, secret)
|
const HmacSha256 = generateHmacSha256(ticket_data, secret)
|
||||||
|
|
||||||
const Ed25519 = signEd25519(ticket_data, private_key)
|
const Ed25519 = signEd25519(ticket_data, private_key)
|
||||||
templateBuffer = setCustomDocProps(templateBuffer, { ticket_data, HmacSha256, Ed25519 })
|
templateBuffer = setCustomDocProps(templateBuffer, { ticket_data, HmacSha256, Ed25519 })
|
||||||
}
|
|
||||||
} catch (error) { //just skip
|
|
||||||
console.error("Set custom property fail",error);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) { //just skip
|
||||||
|
console.error("Set custom property fail", error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Apply Dynamic Content
|
// Apply Dynamic Content
|
||||||
if (templateData?.dc && typeof templateData?.dc === "object") {
|
if (templateData?.dc && typeof templateData?.dc === "object") {
|
||||||
// console.log("Apply Dynamic Content")
|
// console.log("Apply Dynamic Content")
|
||||||
templateBuffer = Buffer.from(await templateDynamicContents(templateBuffer, templateData.dc))
|
templateBuffer = Buffer.from(await templateDynamicContents(templateBuffer, templateData.dc))
|
||||||
}
|
}
|
||||||
// Apply Content Control
|
// Apply Content Control
|
||||||
if (templateData?.cc && typeof templateData?.cc === "object") {
|
if (templateData?.cc && typeof templateData?.cc === "object") {
|
||||||
// console.log("Apply Content Control")
|
// console.log("Apply Content Control")
|
||||||
templateBuffer = templateContentControls(templateBuffer, templateData.cc);
|
templateBuffer = templateContentControls(templateBuffer, templateData.cc);
|
||||||
}
|
}
|
||||||
return templateBuffer
|
return templateBuffer
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ const nextConfig = {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
experimental: {
|
||||||
|
serverComponentsExternalPackages: ['piscina']
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
319
package-lock.json
generated
319
package-lock.json
generated
|
|
@ -18,6 +18,7 @@
|
||||||
"next": "^14.2.35",
|
"next": "^14.2.35",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdfjs-dist": "^4.10.38",
|
"pdfjs-dist": "^4.10.38",
|
||||||
|
"piscina": "^5.3.2",
|
||||||
"pizzip": "^3.3.0",
|
"pizzip": "^3.3.0",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"postgres": "^3.4.9",
|
"postgres": "^3.4.9",
|
||||||
|
|
@ -35,6 +36,7 @@
|
||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
"autoprefixer": "^10.5.5",
|
"autoprefixer": "^10.5.5",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
|
"esbuild": "^0.28.2",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
"office-addin-debugging": "^7.0.1",
|
"office-addin-debugging": "^7.0.1",
|
||||||
"office-addin-dev-settings": "^4.0.1",
|
"office-addin-dev-settings": "^4.0.1",
|
||||||
|
|
@ -3673,6 +3675,311 @@
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@napi-rs/nice": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/nice-android-arm-eabi": "1.1.1",
|
||||||
|
"@napi-rs/nice-android-arm64": "1.1.1",
|
||||||
|
"@napi-rs/nice-darwin-arm64": "1.1.1",
|
||||||
|
"@napi-rs/nice-darwin-x64": "1.1.1",
|
||||||
|
"@napi-rs/nice-freebsd-x64": "1.1.1",
|
||||||
|
"@napi-rs/nice-linux-arm-gnueabihf": "1.1.1",
|
||||||
|
"@napi-rs/nice-linux-arm64-gnu": "1.1.1",
|
||||||
|
"@napi-rs/nice-linux-arm64-musl": "1.1.1",
|
||||||
|
"@napi-rs/nice-linux-ppc64-gnu": "1.1.1",
|
||||||
|
"@napi-rs/nice-linux-riscv64-gnu": "1.1.1",
|
||||||
|
"@napi-rs/nice-linux-s390x-gnu": "1.1.1",
|
||||||
|
"@napi-rs/nice-linux-x64-gnu": "1.1.1",
|
||||||
|
"@napi-rs/nice-linux-x64-musl": "1.1.1",
|
||||||
|
"@napi-rs/nice-openharmony-arm64": "1.1.1",
|
||||||
|
"@napi-rs/nice-win32-arm64-msvc": "1.1.1",
|
||||||
|
"@napi-rs/nice-win32-ia32-msvc": "1.1.1",
|
||||||
|
"@napi-rs/nice-win32-x64-msvc": "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-android-arm-eabi": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-android-arm64": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-darwin-arm64": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-darwin-x64": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-freebsd-x64": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-linux-arm64-gnu": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-linux-arm64-musl": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-linux-ppc64-gnu": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-linux-riscv64-gnu": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-linux-s390x-gnu": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-linux-x64-gnu": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-linux-x64-musl": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-openharmony-arm64": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-win32-arm64-msvc": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-win32-ia32-msvc": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/nice-win32-x64-msvc": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "14.2.35",
|
"version": "14.2.35",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
|
||||||
|
|
@ -10861,6 +11168,18 @@
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/piscina": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/piscina/-/piscina-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-vv7l/mM7B9WgrhDaudIqv1x6v4LOBRYYmpWGdWushhpbCmVHp11sUB2v4hj1CAeTGXVxuZFz6xOU1RalAiPKfQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.x"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@napi-rs/nice": "^1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pizzip": {
|
"node_modules/pizzip": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pizzip/-/pizzip-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pizzip/-/pizzip-3.3.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
"build:worker": "node scripts/build-worker.mjs",
|
||||||
|
"prebuild": "npm run build:worker",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
|
|
@ -23,6 +25,7 @@
|
||||||
"next": "^14.2.35",
|
"next": "^14.2.35",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdfjs-dist": "^4.10.38",
|
"pdfjs-dist": "^4.10.38",
|
||||||
|
"piscina": "^5.3.2",
|
||||||
"pizzip": "^3.3.0",
|
"pizzip": "^3.3.0",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"postgres": "^3.4.9",
|
"postgres": "^3.4.9",
|
||||||
|
|
@ -40,6 +43,7 @@
|
||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
"autoprefixer": "^10.5.5",
|
"autoprefixer": "^10.5.5",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
|
"esbuild": "^0.28.2",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
"office-addin-debugging": "^7.0.1",
|
"office-addin-debugging": "^7.0.1",
|
||||||
"office-addin-dev-settings": "^4.0.1",
|
"office-addin-dev-settings": "^4.0.1",
|
||||||
|
|
|
||||||
29
scripts/build-worker.mjs
Normal file
29
scripts/build-worker.mjs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { build } from 'esbuild';
|
||||||
|
import path from 'node:path';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
|
||||||
|
const root = process.cwd();
|
||||||
|
|
||||||
|
const outputDir = path.join(root, 'workers/dist');
|
||||||
|
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
await build({
|
||||||
|
entryPoints: [
|
||||||
|
path.join(root, 'workers/docx.worker.ts')
|
||||||
|
],
|
||||||
|
outfile: path.join(outputDir, 'docx.worker.mjs'),
|
||||||
|
|
||||||
|
bundle: true,
|
||||||
|
platform: 'node',
|
||||||
|
format: 'esm',
|
||||||
|
packages: 'external',
|
||||||
|
|
||||||
|
alias: {
|
||||||
|
'@': root
|
||||||
|
},
|
||||||
|
|
||||||
|
sourcemap: false
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('DOCX Worker compiled successfully');
|
||||||
641
workers/dist/docx.worker.mjs
vendored
Normal file
641
workers/dist/docx.worker.mjs
vendored
Normal file
|
|
@ -0,0 +1,641 @@
|
||||||
|
// lib/lib-utils.ts
|
||||||
|
import { createHmac, createPrivateKey, createPublicKey, timingSafeEqual, verify, sign } from "node:crypto";
|
||||||
|
function generateHmacSha256(data, secretKey) {
|
||||||
|
const payload = typeof data === "string" ? data : JSON.stringify(sortObjectKeys(data));
|
||||||
|
return createHmac("sha256", secretKey).update(payload, "utf8").digest("hex");
|
||||||
|
}
|
||||||
|
function signEd25519(message, privateKeyHex) {
|
||||||
|
const rawKeyBuffer = Buffer.from(privateKeyHex, "hex");
|
||||||
|
const seed = rawKeyBuffer.length === 64 ? rawKeyBuffer.subarray(0, 32) : rawKeyBuffer;
|
||||||
|
const pkcs8Header = Buffer.from("302e020100300506032b657004220420", "hex");
|
||||||
|
const pkcs8Buffer = Buffer.concat([pkcs8Header, seed]);
|
||||||
|
const privateKey = createPrivateKey({
|
||||||
|
key: pkcs8Buffer,
|
||||||
|
format: "der",
|
||||||
|
type: "pkcs8"
|
||||||
|
});
|
||||||
|
const dataBuffer = typeof message === "string" ? Buffer.from(message, "utf-8") : message;
|
||||||
|
const signatureBuffer = sign(null, dataBuffer, privateKey);
|
||||||
|
return signatureBuffer.toString("hex");
|
||||||
|
}
|
||||||
|
function sortObjectKeys(obj) {
|
||||||
|
return Object.keys(obj).sort().reduce((result, key) => {
|
||||||
|
result[key] = obj[key];
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// lib/lib-tmpl.ts
|
||||||
|
import PizzaZip from "pizzip";
|
||||||
|
import {
|
||||||
|
DOMParser,
|
||||||
|
XMLSerializer
|
||||||
|
} from "@xmldom/xmldom";
|
||||||
|
import xpath from "xpath";
|
||||||
|
var getCreateReport = async () => {
|
||||||
|
const mod = await import("docx-templates");
|
||||||
|
const createReportFn = typeof mod.createReport === "function" ? mod.createReport : typeof mod.default === "function" ? mod.default : void 0;
|
||||||
|
if (!createReportFn) {
|
||||||
|
throw new Error("docx-templates createReport is not available");
|
||||||
|
}
|
||||||
|
return createReportFn;
|
||||||
|
};
|
||||||
|
var WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||||
|
var asXpathNode = (node) => node;
|
||||||
|
function createSelector() {
|
||||||
|
return xpath.useNamespaces({ w: WORD_NS });
|
||||||
|
}
|
||||||
|
function detectSdtControlType(sdt, select) {
|
||||||
|
const sdtPr = select(
|
||||||
|
"./w:sdtPr",
|
||||||
|
asXpathNode(sdt),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (!sdtPr) {
|
||||||
|
return { type: "other", multiLine: false };
|
||||||
|
}
|
||||||
|
const textEl = select(
|
||||||
|
"./w:text",
|
||||||
|
asXpathNode(sdtPr),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (textEl) {
|
||||||
|
const multiLineAttr = select(
|
||||||
|
"./@w:multiLine",
|
||||||
|
asXpathNode(textEl),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
type: "plain",
|
||||||
|
multiLine: multiLineAttr?.value === "1" || multiLineAttr?.value === "true"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const richTextEl = select(
|
||||||
|
"./w:richText",
|
||||||
|
asXpathNode(sdtPr),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
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
|
||||||
|
);
|
||||||
|
if (found) {
|
||||||
|
return { type: "other", multiLine: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { type: "rich", multiLine: true };
|
||||||
|
}
|
||||||
|
function detectSdtStructure(sdtContent, select) {
|
||||||
|
const directP = select(
|
||||||
|
"./w:p",
|
||||||
|
asXpathNode(sdtContent)
|
||||||
|
);
|
||||||
|
return directP.length > 0 ? "block" : "inline";
|
||||||
|
}
|
||||||
|
function buildRunChildren(doc, text) {
|
||||||
|
const nodes = [];
|
||||||
|
const tokens = text.split(/(\t|\n)/);
|
||||||
|
tokens.forEach((token) => {
|
||||||
|
if (token === "") return;
|
||||||
|
if (token === " ") {
|
||||||
|
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, text, rPr) {
|
||||||
|
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, sdtContent, select, text, allowLineBreak) {
|
||||||
|
const safeText = allowLineBreak ? text : text.replace(/\n/g, " ");
|
||||||
|
const existingRuns = select(
|
||||||
|
"./w:r",
|
||||||
|
asXpathNode(sdtContent)
|
||||||
|
);
|
||||||
|
const firstRPr = existingRuns.length > 0 ? select(
|
||||||
|
"./w:rPr",
|
||||||
|
asXpathNode(existingRuns[0]),
|
||||||
|
true
|
||||||
|
) : void 0;
|
||||||
|
existingRuns.forEach((r) => {
|
||||||
|
if (r.parentNode) r.parentNode.removeChild(r);
|
||||||
|
});
|
||||||
|
sdtContent.appendChild(
|
||||||
|
buildRun(doc, safeText, firstRPr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function insertBlockContent(doc, sdtContent, select, text, allowMultiParagraph) {
|
||||||
|
const allParagraphs = select(
|
||||||
|
"./w:p",
|
||||||
|
asXpathNode(sdtContent)
|
||||||
|
);
|
||||||
|
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
|
||||||
|
);
|
||||||
|
const firstRun = select(
|
||||||
|
"./w:r",
|
||||||
|
asXpathNode(firstP),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
const firstRPr = firstRun ? select(
|
||||||
|
"./w:rPr",
|
||||||
|
asXpathNode(firstRun),
|
||||||
|
true
|
||||||
|
) : void 0;
|
||||||
|
allParagraphs.forEach((p) => {
|
||||||
|
if (p !== firstP && p.parentNode) {
|
||||||
|
p.parentNode.removeChild(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const existingRuns = select(
|
||||||
|
"./w:r",
|
||||||
|
asXpathNode(firstP)
|
||||||
|
);
|
||||||
|
existingRuns.forEach((r) => {
|
||||||
|
if (r.parentNode) r.parentNode.removeChild(r);
|
||||||
|
});
|
||||||
|
if (!allowMultiParagraph) {
|
||||||
|
firstP.appendChild(buildRun(doc, text.replace(/\n+/g, "\n"), firstRPr));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const paragraphs = text.split("\n\n");
|
||||||
|
paragraphs.forEach((pText, index) => {
|
||||||
|
let targetP;
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
buildRunChildren(doc, pText).forEach(
|
||||||
|
(node) => run.appendChild(node)
|
||||||
|
);
|
||||||
|
targetP.appendChild(run);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function insertFallbackContent(sdtContent, select, text) {
|
||||||
|
const textNodes = select(
|
||||||
|
".//w:t",
|
||||||
|
asXpathNode(sdtContent)
|
||||||
|
);
|
||||||
|
if (textNodes.length === 0) return;
|
||||||
|
textNodes[0].textContent = text;
|
||||||
|
for (let i = 1; i < textNodes.length; i++) {
|
||||||
|
textNodes[i].textContent = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function normalizeValue(value) {
|
||||||
|
if (value === null || value === void 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
function templateContentControls(templateBuffer, data) {
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
sdtNodes.forEach((sdt) => {
|
||||||
|
const tagAttr = select(
|
||||||
|
"./w:sdtPr/w:tag/@w:val",
|
||||||
|
asXpathNode(sdt),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
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
|
||||||
|
);
|
||||||
|
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"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var CUSTOM_NS = "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties";
|
||||||
|
var VT_NS = "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes";
|
||||||
|
var REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
|
||||||
|
var CONTENT_TYPES_NS = "http://schemas.openxmlformats.org/package/2006/content-types";
|
||||||
|
var XMLNS_NS = "http://www.w3.org/2000/xmlns/";
|
||||||
|
var CUSTOM_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
|
||||||
|
var CUSTOM_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.custom-properties+xml";
|
||||||
|
var CUSTOM_FMTID = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}";
|
||||||
|
var CUSTOM_PATH = "docProps/custom.xml";
|
||||||
|
function parseXml(xml, fileName) {
|
||||||
|
const errors = [];
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(
|
||||||
|
xml,
|
||||||
|
"application/xml"
|
||||||
|
);
|
||||||
|
if (errors.length > 0 || !doc.documentElement) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid ${fileName}: ${errors.join("; ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
function assertRoot(doc, namespace, localName, fileName) {
|
||||||
|
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() {
|
||||||
|
const xml = [
|
||||||
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
|
||||||
|
`<Properties xmlns="${CUSTOM_NS}" xmlns:vt="${VT_NS}"/>`
|
||||||
|
].join("");
|
||||||
|
return parseXml(xml, CUSTOM_PATH);
|
||||||
|
}
|
||||||
|
function createValueElement(doc, value) {
|
||||||
|
let tagName;
|
||||||
|
let text;
|
||||||
|
if (value == null) {
|
||||||
|
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) {
|
||||||
|
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")
|
||||||
|
);
|
||||||
|
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) {
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function setCustomDocProps(docxBuffer, data) {
|
||||||
|
const zip = new PizzaZip(docxBuffer);
|
||||||
|
const customFile = zip.file(CUSTOM_PATH);
|
||||||
|
const doc = customFile ? parseXml(customFile.asText(), CUSTOM_PATH) : createCustomXml();
|
||||||
|
const root = assertRoot(
|
||||||
|
doc,
|
||||||
|
CUSTOM_NS,
|
||||||
|
"Properties",
|
||||||
|
CUSTOM_PATH
|
||||||
|
);
|
||||||
|
root.setAttributeNS(XMLNS_NS, "xmlns:vt", VT_NS);
|
||||||
|
const properties = Array.from(
|
||||||
|
root.getElementsByTagNameNS(CUSTOM_NS, "property")
|
||||||
|
);
|
||||||
|
const propertyMap = /* @__PURE__ */ new Map();
|
||||||
|
const usedPids = /* @__PURE__ */ 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);
|
||||||
|
}
|
||||||
|
const valueElement = createValueElement(doc, value);
|
||||||
|
while (property.firstChild) {
|
||||||
|
property.removeChild(property.firstChild);
|
||||||
|
}
|
||||||
|
property.appendChild(valueElement);
|
||||||
|
}
|
||||||
|
zip.file(
|
||||||
|
CUSTOM_PATH,
|
||||||
|
new XMLSerializer().serializeToString(doc)
|
||||||
|
);
|
||||||
|
ensureCustomRelationship(zip);
|
||||||
|
ensureCustomContentType(zip);
|
||||||
|
return Buffer.from(
|
||||||
|
zip.generate({
|
||||||
|
type: "nodebuffer",
|
||||||
|
compression: "DEFLATE"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function p_cc(title, tag, text = "", isMultiLine = true, lock = "sdtLocked") {
|
||||||
|
const lockTag = lock == "none" ? "" : `<w:lock w:val="${lock}" />`;
|
||||||
|
const multiLineTag = isMultiLine ? '<w:text w:multiLine="1" />' : "";
|
||||||
|
const content = escapeXml(text);
|
||||||
|
const xmlString = `<w:sdt><w:sdtPr><w:alias w:val="${title}" /><w:tag w:val="${tag}" /><w:id w:val="-1948841888" />${lockTag}<w:placeholder><w:docPart w:val="C3E615E9D8424D6ABEA7EADA60C3EFB6" /></w:placeholder>${multiLineTag}</w:sdtPr><w:sdtEndPr /><w:sdtContent><w:r><w:rPr><w:rFonts w:cs="TH SarabunPSK" /></w:rPr><w:t xml:space="preserve">${content}</w:t></w:r></w:sdtContent></w:sdt>`;
|
||||||
|
return `||${xmlString}||`;
|
||||||
|
}
|
||||||
|
function br(n) {
|
||||||
|
let text = "";
|
||||||
|
for (let i = 0; i < n; i++) text += "<w:br/>";
|
||||||
|
return `||${text}||`;
|
||||||
|
}
|
||||||
|
function bm(id, name) {
|
||||||
|
return `||<w:bookmarkStart w:id="${id}" w:name="${name}" /><w:bookmarkEnd w:id="${id}" />||`;
|
||||||
|
}
|
||||||
|
function escapeXml(unsafe) {
|
||||||
|
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
var templateDynamicContents = async (template, data) => {
|
||||||
|
const createReport = await getCreateReport();
|
||||||
|
return await createReport({
|
||||||
|
template,
|
||||||
|
data,
|
||||||
|
cmdDelimiter: ["{{", "}}"],
|
||||||
|
additionalJsContext: {
|
||||||
|
bm,
|
||||||
|
br,
|
||||||
|
p_cc
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
async function applyTemplate(templateBuffer, templateData, secret, private_key, ttl) {
|
||||||
|
const originalTicket_data = templateData?.info?.ticket_data;
|
||||||
|
if (originalTicket_data && typeof originalTicket_data === "string") {
|
||||||
|
try {
|
||||||
|
const originalTicketObj = JSON.parse(originalTicket_data);
|
||||||
|
if (originalTicketObj && typeof originalTicketObj === "object") {
|
||||||
|
console.log("Found valid ticket data: hash it");
|
||||||
|
const expireTimestamp = Math.floor(Date.now() / 1e3) + ttl;
|
||||||
|
const fullObj = { ...originalTicketObj, exp: expireTimestamp };
|
||||||
|
const sortedObj = sortObjectKeys(fullObj);
|
||||||
|
const ticket_data = JSON.stringify(sortedObj);
|
||||||
|
const HmacSha256 = generateHmacSha256(ticket_data, secret);
|
||||||
|
const Ed25519 = signEd25519(ticket_data, private_key);
|
||||||
|
templateBuffer = setCustomDocProps(templateBuffer, { ticket_data, HmacSha256, Ed25519 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Set custom property fail", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (templateData?.dc && typeof templateData?.dc === "object") {
|
||||||
|
templateBuffer = Buffer.from(await templateDynamicContents(templateBuffer, templateData.dc));
|
||||||
|
}
|
||||||
|
if (templateData?.cc && typeof templateData?.cc === "object") {
|
||||||
|
templateBuffer = templateContentControls(templateBuffer, templateData.cc);
|
||||||
|
}
|
||||||
|
return templateBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// workers/docx.worker.ts
|
||||||
|
async function generateDocx(input) {
|
||||||
|
const templateBuffer = Buffer.from(input.templateBuffer);
|
||||||
|
const output = await applyTemplate(
|
||||||
|
templateBuffer,
|
||||||
|
input.templateData,
|
||||||
|
input.secret,
|
||||||
|
input.privateKeyHex,
|
||||||
|
input.ticketSec
|
||||||
|
);
|
||||||
|
return Buffer.from(output);
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
generateDocx as default
|
||||||
|
};
|
||||||
29
workers/docx.worker.ts
Normal file
29
workers/docx.worker.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import {
|
||||||
|
applyTemplate,
|
||||||
|
type DocumentTemplate
|
||||||
|
} from '../lib/lib-tmpl';
|
||||||
|
|
||||||
|
interface DocxWorkerInput {
|
||||||
|
templateBuffer: Uint8Array;
|
||||||
|
templateData: DocumentTemplate;
|
||||||
|
secret: string;
|
||||||
|
privateKeyHex: string;
|
||||||
|
ticketSec: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function generateDocx(
|
||||||
|
input: DocxWorkerInput
|
||||||
|
): Promise<Buffer> {
|
||||||
|
|
||||||
|
const templateBuffer = Buffer.from(input.templateBuffer);
|
||||||
|
|
||||||
|
const output = await applyTemplate(
|
||||||
|
templateBuffer,
|
||||||
|
input.templateData,
|
||||||
|
input.secret,
|
||||||
|
input.privateKeyHex,
|
||||||
|
input.ticketSec
|
||||||
|
);
|
||||||
|
|
||||||
|
return Buffer.from(output);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue