task p-4.5
This commit is contained in:
@@ -1,22 +1,34 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { Template } from '@pdfme/common';
|
||||
import type { ResolvedDocumentTemplate } from '@/features/foundation/document-template/types';
|
||||
import type { QuotationDocumentData } from '../types';
|
||||
import { buildQuotationPdfRuntime } from './quotation-pdf-runtime';
|
||||
import { resolveTemplatePages } from './page-resolver';
|
||||
import { assembleTemplate } from './template-assembler';
|
||||
import type { ResolvedTemplate } from './runtime-contracts';
|
||||
import { adaptTemplateForSectionRuntime } from './template-compatibility-adapter';
|
||||
|
||||
function createField(name: string, y: number, height = 7) {
|
||||
function createField(name: string, y: number, height = 7, type = 'text', content?: string) {
|
||||
return {
|
||||
name,
|
||||
type: 'text',
|
||||
type,
|
||||
position: { x: 10, y },
|
||||
width: 100,
|
||||
height,
|
||||
content: `{${name}}`,
|
||||
content: content ?? `{${name}}`,
|
||||
};
|
||||
}
|
||||
|
||||
function createMarker(role: string) {
|
||||
return {
|
||||
name: `__section_role__${role}`,
|
||||
type: 'text',
|
||||
position: { x: 1, y: 1 },
|
||||
width: 8,
|
||||
height: 2,
|
||||
content: role,
|
||||
readOnly: true,
|
||||
opacity: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,12 +38,54 @@ function createLegacyTemplate(): Template {
|
||||
schemas: [
|
||||
[
|
||||
createField('customer_name', 20),
|
||||
createField('quotation_price_data', 40, 20),
|
||||
createField('quotation_price_data', 40, 20, 'table', '[["Price (Exclude VAT)","-","","THB"]]'),
|
||||
createField('quotation_code_data', 70),
|
||||
],
|
||||
[
|
||||
createField('topic', 35),
|
||||
createField('data_topic', 45, 7),
|
||||
createField('topic', 35, 10, 'table', '[["Topic:"]]'),
|
||||
createField('data_topic', 45, 10, 'table', '[["{item_topic}"]]'),
|
||||
createField('Please_do_not', 225),
|
||||
createField('yours_faithfuly', 232),
|
||||
createField('app1', 240),
|
||||
createField('app1_position', 247),
|
||||
createField('app2', 240),
|
||||
createField('app2_position', 247),
|
||||
createField('app3', 240),
|
||||
createField('app3_position', 247),
|
||||
],
|
||||
],
|
||||
} as unknown as Template;
|
||||
}
|
||||
|
||||
function createProductTemplate(): Template {
|
||||
return {
|
||||
basePdf: { width: 210, height: 297, padding: [0, 0, 0, 0] },
|
||||
schemas: [
|
||||
[
|
||||
createMarker('customer'),
|
||||
createField('customer_name', 20),
|
||||
createField('quotation_code_data', 30),
|
||||
],
|
||||
[
|
||||
createMarker('product_items'),
|
||||
{
|
||||
name: 'items_table',
|
||||
type: 'table',
|
||||
position: { x: 10, y: 40 },
|
||||
width: 180,
|
||||
height: 120,
|
||||
content: '[["1","Sample","1","EA","THB 1.00","THB 0.00","THB 1.00"]]',
|
||||
showHead: true,
|
||||
repeatHead: true,
|
||||
head: ['Item', 'Description', 'Qty', 'Unit', 'Unit Price', 'Discount', 'Total'],
|
||||
headWidthPercentages: [8, 34, 8, 10, 14, 12, 14],
|
||||
},
|
||||
],
|
||||
[
|
||||
createMarker('topics'),
|
||||
createMarker('signature'),
|
||||
createField('topic', 35, 10, 'table', '[["Topic:"]]'),
|
||||
createField('data_topic', 45, 10, 'table', '[["{item_topic}"]]'),
|
||||
createField('Please_do_not', 225),
|
||||
createField('yours_faithfuly', 232),
|
||||
createField('app1', 240),
|
||||
@@ -53,7 +107,7 @@ function createResolvedDocumentTemplate(schema: Template): ResolvedDocumentTempl
|
||||
documentType: 'quotation',
|
||||
productType: 'default',
|
||||
fileType: 'pdfme',
|
||||
templateName: 'Legacy quotation',
|
||||
templateName: 'Quotation template',
|
||||
description: null,
|
||||
isDefault: true,
|
||||
isActive: true,
|
||||
@@ -64,7 +118,7 @@ function createResolvedDocumentTemplate(schema: Template): ResolvedDocumentTempl
|
||||
updatedBy: 'system',
|
||||
},
|
||||
version: {
|
||||
id: 'version-1',
|
||||
id: 'template-version-1',
|
||||
organizationId: 'org-1',
|
||||
templateId: 'template-1',
|
||||
version: '1.0',
|
||||
@@ -80,7 +134,26 @@ function createResolvedDocumentTemplate(schema: Template): ResolvedDocumentTempl
|
||||
};
|
||||
}
|
||||
|
||||
function createDocumentData(topicCount: number): QuotationDocumentData {
|
||||
function createItem(index: number): QuotationDocumentData['items'][number] {
|
||||
return {
|
||||
id: `item-${index}`,
|
||||
itemNumber: index + 1,
|
||||
productTypeCode: null,
|
||||
productTypeLabel: null,
|
||||
description: `Product item ${index + 1}`,
|
||||
quantity: 1,
|
||||
unitCode: 'EA',
|
||||
unitLabel: 'EA',
|
||||
unitPrice: 1200 + index,
|
||||
discount: index * 10,
|
||||
discountTypeCode: null,
|
||||
taxRate: 0,
|
||||
totalPrice: 1200 + index - index * 10,
|
||||
notes: null,
|
||||
};
|
||||
}
|
||||
|
||||
function createDocumentData(topicCount: number, itemCount = 0): QuotationDocumentData {
|
||||
return {
|
||||
company: {
|
||||
id: 'company-1',
|
||||
@@ -128,7 +201,7 @@ function createDocumentData(topicCount: number): QuotationDocumentData {
|
||||
totalAmount: 0,
|
||||
notes: null,
|
||||
},
|
||||
items: [],
|
||||
items: Array.from({ length: itemCount }, (_, index) => createItem(index)),
|
||||
quotationCustomers: [],
|
||||
topics: {
|
||||
scope: [],
|
||||
@@ -157,7 +230,7 @@ function createDocumentData(topicCount: number): QuotationDocumentData {
|
||||
location: 'Location',
|
||||
},
|
||||
quotation_date: '2026-06-29',
|
||||
quotation_price: '0.00',
|
||||
quotation_price: 'THB 0.00',
|
||||
quotation_price_data: [],
|
||||
exclusion_data: [],
|
||||
topic_inputs: {},
|
||||
@@ -172,24 +245,15 @@ function createDocumentData(topicCount: number): QuotationDocumentData {
|
||||
approvers: [],
|
||||
},
|
||||
signatures: {
|
||||
preparedBy: {
|
||||
name: 'Prepared',
|
||||
position: 'Sales',
|
||||
},
|
||||
approvedBy: {
|
||||
name: 'Approved',
|
||||
position: 'Manager',
|
||||
},
|
||||
authorizedBy: {
|
||||
name: 'Authorized',
|
||||
position: 'Director',
|
||||
},
|
||||
preparedBy: { name: 'Prepared', position: 'Sales' },
|
||||
approvedBy: { name: 'Approved', position: 'Manager' },
|
||||
authorizedBy: { name: 'Authorized', position: 'Director' },
|
||||
},
|
||||
watermarkStatus: null,
|
||||
} as QuotationDocumentData;
|
||||
};
|
||||
}
|
||||
|
||||
test('legacy template page roles are inferred without page-index assumptions', () => {
|
||||
test('legacy template page roles are still inferred without page-index assumptions', () => {
|
||||
const schema = createLegacyTemplate();
|
||||
const template = createResolvedDocumentTemplate(schema);
|
||||
const runtimeTemplate: ResolvedTemplate = {
|
||||
@@ -203,12 +267,10 @@ test('legacy template page roles are inferred without page-index assumptions', (
|
||||
assert.equal(resolved.byRole.customer?.[0]?.pageIndex, 0);
|
||||
assert.equal(resolved.byRole.topics?.[0]?.pageIndex, 1);
|
||||
assert.equal(resolved.byRole.signature?.[0]?.pageIndex, 1);
|
||||
assert.ok(
|
||||
compatible.issues.some((issue) => issue.code === 'LEGACY_COMPAT_MODE'),
|
||||
);
|
||||
assert.ok(compatible.issues.some((issue) => issue.code === 'LEGACY_COMPAT_MODE'));
|
||||
});
|
||||
|
||||
test('runtime keeps signature block on the last generated topic page', async () => {
|
||||
test('runtime keeps signature block on last generated topic page for legacy templates', async () => {
|
||||
const schema = createLegacyTemplate();
|
||||
const template = createResolvedDocumentTemplate(schema);
|
||||
const runtime = await buildQuotationPdfRuntime({
|
||||
@@ -221,9 +283,7 @@ test('runtime keeps signature block on the last generated topic page', async ()
|
||||
});
|
||||
|
||||
assert.ok(runtime.assembled.template.schemas.length > 2);
|
||||
assert.ok(
|
||||
Object.keys(runtime.topicInputs).some((key) => key.startsWith('topic_1_')),
|
||||
);
|
||||
assert.ok(Object.keys(runtime.topicInputs).some((key) => key.startsWith('topic_1_')));
|
||||
|
||||
const topicPages = runtime.assembled.template.schemas.slice(1);
|
||||
const lastPageNames = new Set(
|
||||
@@ -257,38 +317,50 @@ test('runtime restores legacy static labels into final template input', async ()
|
||||
assert.equal(runtime.assembled.templateInput.att_label, 'Att');
|
||||
assert.equal(runtime.assembled.templateInput.project_label, 'Project');
|
||||
assert.equal(runtime.assembled.templateInput.site_label, 'Location');
|
||||
assert.equal(runtime.assembled.templateInput.quotation_date, '2026-06-29');
|
||||
assert.equal(runtime.assembled.templateInput.quotation_date_data, '2026-06-29');
|
||||
});
|
||||
|
||||
test('template assembler does not overwrite populated values with nullish or blank patches', () => {
|
||||
const assembled = assembleTemplate({
|
||||
template: createLegacyTemplate(),
|
||||
test('explicit section markers resolve product item page and runtime injects items_table', async () => {
|
||||
const schema = createProductTemplate();
|
||||
const template = createResolvedDocumentTemplate(schema);
|
||||
const runtimeTemplate: ResolvedTemplate = {
|
||||
documentTemplate: template,
|
||||
schema,
|
||||
};
|
||||
|
||||
const compatible = adaptTemplateForSectionRuntime(runtimeTemplate);
|
||||
const resolved = resolveTemplatePages(compatible);
|
||||
|
||||
assert.equal(resolved.byRole.customer?.[0]?.pageIndex, 0);
|
||||
assert.equal(resolved.byRole.product_items?.[0]?.pageIndex, 1);
|
||||
assert.equal(resolved.byRole.topics?.[0]?.pageIndex, 2);
|
||||
assert.equal(resolved.byRole.signature?.[0]?.pageIndex, 2);
|
||||
assert.equal(compatible.issues.some((issue) => issue.code === 'LEGACY_COMPAT_MODE'), false);
|
||||
|
||||
const runtime = await buildQuotationPdfRuntime({
|
||||
documentData: createDocumentData(4, 4),
|
||||
template,
|
||||
mappings: [],
|
||||
templateInput: {
|
||||
tel_label: 'Tel',
|
||||
customer_name: 'Customer',
|
||||
},
|
||||
staticInputPatch: {
|
||||
tel_label: '',
|
||||
email_label: 'Email',
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
role: 'topics',
|
||||
enabled: true,
|
||||
rendered: false,
|
||||
anchorPageIndex: 1,
|
||||
pages: [],
|
||||
templateInputPatch: {
|
||||
tel_label: undefined,
|
||||
project_label: 'Project',
|
||||
},
|
||||
issues: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(assembled.templateInput.tel_label, 'Tel');
|
||||
assert.equal(assembled.templateInput.email_label, 'Email');
|
||||
assert.equal(assembled.templateInput.project_label, 'Project');
|
||||
assert.ok(runtime.assembled.template.schemas.length >= 3);
|
||||
const productPageNames = new Set(
|
||||
runtime.assembled.template.schemas[1]?.map((field) =>
|
||||
String((field as { name?: string }).name ?? ''),
|
||||
) ?? [],
|
||||
);
|
||||
assert.ok(productPageNames.has('items_table'));
|
||||
assert.ok(Array.isArray(runtime.assembled.templateInput.items_table));
|
||||
assert.equal((runtime.assembled.templateInput.items_table as string[][]).length, 4);
|
||||
assert.deepEqual((runtime.assembled.templateInput.items_table as string[][])[0], [
|
||||
'1',
|
||||
'Product item 1',
|
||||
'1',
|
||||
'EA',
|
||||
'THB 1,200.00',
|
||||
'THB 0.00',
|
||||
'THB 1,200.00',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { DocumentTemplateMappingWithColumns, ResolvedDocumentTemplate } from '@/features/foundation/document-template/types';
|
||||
import type { Template } from '@pdfme/common';
|
||||
import type {
|
||||
DocumentTemplateMappingWithColumns,
|
||||
ResolvedDocumentTemplate,
|
||||
} from '@/features/foundation/document-template/types';
|
||||
import type { QuotationDocumentData } from '../types';
|
||||
import { buildPdfTopicPages } from './pdf-topic-engine';
|
||||
import { resolveTemplatePages } from './page-resolver';
|
||||
@@ -21,6 +25,29 @@ function cloneTemplate<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function buildMissingPageSection<TRole extends BuiltSection['role']>(args: {
|
||||
role: TRole;
|
||||
enabled: boolean;
|
||||
required: boolean;
|
||||
message: string;
|
||||
}): BuiltSection<TRole> {
|
||||
return {
|
||||
role: args.role,
|
||||
enabled: args.enabled,
|
||||
rendered: false,
|
||||
anchorPageIndex: null,
|
||||
pages: [],
|
||||
templateInputPatch: {},
|
||||
issues: [
|
||||
{
|
||||
code: 'MISSING_MARKER',
|
||||
severity: args.required ? 'error' : 'warning',
|
||||
message: args.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function createCustomerSectionBuilder(): SectionBuilder {
|
||||
return {
|
||||
role: 'customer',
|
||||
@@ -29,21 +56,12 @@ export function createCustomerSectionBuilder(): SectionBuilder {
|
||||
const policy = context.policyBySection.get('customer');
|
||||
|
||||
if (!page) {
|
||||
return {
|
||||
return buildMissingPageSection({
|
||||
role: 'customer',
|
||||
enabled: policy?.enabled ?? false,
|
||||
rendered: false,
|
||||
anchorPageIndex: null,
|
||||
pages: [],
|
||||
templateInputPatch: {},
|
||||
issues: [
|
||||
{
|
||||
code: 'MISSING_MARKER',
|
||||
severity: policy?.required ? 'error' : 'warning',
|
||||
message: 'Customer section page could not be resolved.',
|
||||
},
|
||||
],
|
||||
};
|
||||
required: policy?.required ?? true,
|
||||
message: 'Customer section page could not be resolved.',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -63,18 +81,45 @@ export function createProductItemSectionBuilder(): SectionBuilder {
|
||||
return {
|
||||
role: 'product_items',
|
||||
build(context: RenderContext): ProductItemSection {
|
||||
const page = context.pages.byRole.product_items?.[0] ?? null;
|
||||
const policy = context.policyBySection.get('product_items');
|
||||
const built = buildProductItemSectionModel({
|
||||
items: context.documentData.items,
|
||||
currencyCode: context.documentData.quotation.currencyCode,
|
||||
});
|
||||
|
||||
if (!page) {
|
||||
return {
|
||||
...buildMissingPageSection({
|
||||
role: 'product_items',
|
||||
enabled: policy?.enabled ?? false,
|
||||
required: policy?.required ?? false,
|
||||
message: 'Product item section page could not be resolved.',
|
||||
}),
|
||||
rows: built.rows,
|
||||
tableModel: built.tableModel,
|
||||
pagination: built.pagination,
|
||||
issues: [
|
||||
...built.issues,
|
||||
...buildMissingPageSection({
|
||||
role: 'product_items',
|
||||
enabled: policy?.enabled ?? false,
|
||||
required: policy?.required ?? false,
|
||||
message: 'Product item section page could not be resolved.',
|
||||
}).issues,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
role: 'product_items',
|
||||
enabled: true,
|
||||
rendered: false,
|
||||
anchorPageIndex: null,
|
||||
pages: [],
|
||||
templateInputPatch: {},
|
||||
rendered: true,
|
||||
anchorPageIndex: page.pageIndex,
|
||||
pages: [cloneTemplate(page.schema)],
|
||||
templateInputPatch: {
|
||||
items_table: built.tableModel.rows,
|
||||
},
|
||||
issues: built.issues,
|
||||
rows: built.rows,
|
||||
tableModel: built.tableModel,
|
||||
@@ -92,21 +137,12 @@ export function createTopicSectionBuilder(): SectionBuilder {
|
||||
const policy = context.policyBySection.get('topics');
|
||||
|
||||
if (!page) {
|
||||
return {
|
||||
return buildMissingPageSection({
|
||||
role: 'topics',
|
||||
enabled: policy?.enabled ?? false,
|
||||
rendered: false,
|
||||
anchorPageIndex: null,
|
||||
pages: [],
|
||||
templateInputPatch: {},
|
||||
issues: [
|
||||
{
|
||||
code: 'MISSING_MARKER',
|
||||
severity: policy?.required ? 'error' : 'warning',
|
||||
message: 'Topic section page could not be resolved.',
|
||||
},
|
||||
],
|
||||
};
|
||||
required: policy?.required ?? true,
|
||||
message: 'Topic section page could not be resolved.',
|
||||
});
|
||||
}
|
||||
|
||||
const built = buildPdfTopicPages(page.schema, context.documentData.topics.all, {
|
||||
@@ -137,7 +173,7 @@ export function getQuotationSectionBuilders(): SectionBuilder[] {
|
||||
function createResolvedTemplate(template: ResolvedDocumentTemplate): ResolvedTemplate {
|
||||
return {
|
||||
documentTemplate: template,
|
||||
schema: cloneTemplate(template.version.schemaJson as import('@pdfme/common').Template),
|
||||
schema: cloneTemplate(template.version.schemaJson as Template),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -154,7 +190,10 @@ export async function buildQuotationPdfRuntime(args: {
|
||||
const resolvedTemplate = createResolvedTemplate(args.template);
|
||||
const compatibleTemplate = adaptTemplateForSectionRuntime(resolvedTemplate);
|
||||
const pages = resolveTemplatePages(compatibleTemplate);
|
||||
const policies = resolveRenderPolicies();
|
||||
const policies = resolveRenderPolicies({
|
||||
pages,
|
||||
documentData: args.documentData,
|
||||
});
|
||||
const staticInputPatch = resolveQuotationStaticTemplateInputs(args.documentData);
|
||||
const context = createRenderContext({
|
||||
documentData: args.documentData,
|
||||
@@ -172,7 +211,9 @@ export async function buildQuotationPdfRuntime(args: {
|
||||
sections: composed.sections,
|
||||
});
|
||||
const issues = [...pages.issues, ...composed.issues, ...assembled.issues];
|
||||
const topicSection = composed.sections.find((section) => section.role === 'topics');
|
||||
const topicSection = composed.sections.find(
|
||||
(section): section is BuiltSection<'topics'> => section.role === 'topics',
|
||||
);
|
||||
|
||||
if (issues.some((issue) => issue.severity === 'error')) {
|
||||
throw new Error(createRuntimeErrorMessage(), { cause: issues });
|
||||
|
||||
@@ -10,7 +10,10 @@ import type {
|
||||
} from './runtime-contracts';
|
||||
import { SECTION_ORDER } from './runtime-contracts';
|
||||
|
||||
function buildDefaultPolicy(section: SectionRole): RenderPolicy {
|
||||
function buildDefaultPolicy(
|
||||
section: SectionRole,
|
||||
args?: { pages?: ResolvedPages; documentData?: QuotationDocumentData },
|
||||
): RenderPolicy {
|
||||
switch (section) {
|
||||
case 'customer':
|
||||
return {
|
||||
@@ -19,6 +22,13 @@ function buildDefaultPolicy(section: SectionRole): RenderPolicy {
|
||||
required: true,
|
||||
visibleWhenEmpty: true,
|
||||
};
|
||||
case 'product_items':
|
||||
return {
|
||||
section,
|
||||
enabled: Boolean(args?.pages?.byRole.product_items?.length),
|
||||
required: false,
|
||||
visibleWhenEmpty: true,
|
||||
};
|
||||
case 'topics':
|
||||
return {
|
||||
section,
|
||||
@@ -36,8 +46,11 @@ function buildDefaultPolicy(section: SectionRole): RenderPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveRenderPolicies(): RenderPolicy[] {
|
||||
return SECTION_ORDER.map((section) => buildDefaultPolicy(section));
|
||||
export function resolveRenderPolicies(args?: {
|
||||
pages?: ResolvedPages;
|
||||
documentData?: QuotationDocumentData;
|
||||
}): RenderPolicy[] {
|
||||
return SECTION_ORDER.map((section) => buildDefaultPolicy(section, args));
|
||||
}
|
||||
|
||||
export function createRenderContext(args: {
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
SectionRole,
|
||||
} from './runtime-contracts';
|
||||
|
||||
const EXPLICIT_ROLE_PREFIX = '__page_role__';
|
||||
const EXPLICIT_ROLE_PREFIXES = ['__section_role__', '__page_role__'] as const;
|
||||
|
||||
const LEGACY_MARKERS: Record<Exclude<SectionRole, 'unknown'>, string[]> = {
|
||||
customer: ['customer_name', 'quotation_price_data', 'quotation_code_data'],
|
||||
@@ -48,7 +48,7 @@ function isTemplatePageArray(
|
||||
function getFieldNames(page: Template['schemas'][number]): string[] {
|
||||
return page
|
||||
.map((field) => {
|
||||
if (typeof field !== 'object' || field === null || !('name' in field)) {
|
||||
if (!field || typeof field !== 'object' || !('name' in field)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -57,60 +57,8 @@ function getFieldNames(page: Template['schemas'][number]): string[] {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function getExplicitRole(page: Template['schemas'][number]): SectionRole | null {
|
||||
for (const field of page) {
|
||||
if (!field || typeof field !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name =
|
||||
'name' in field && typeof field.name === 'string' ? field.name : null;
|
||||
const content =
|
||||
'content' in field && typeof field.content === 'string'
|
||||
? field.content.trim()
|
||||
: null;
|
||||
|
||||
const byName = name?.startsWith(EXPLICIT_ROLE_PREFIX)
|
||||
? name.slice(EXPLICIT_ROLE_PREFIX.length)
|
||||
: null;
|
||||
const candidate = byName ?? content;
|
||||
|
||||
if (candidate && isSectionRole(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function inferLegacyRoles(page: Template['schemas'][number]): SectionRole[] {
|
||||
const names = new Set(getFieldNames(page));
|
||||
const inferred: SectionRole[] = [];
|
||||
|
||||
for (const [role, markers] of Object.entries(LEGACY_MARKERS) as Array<
|
||||
[Exclude<SectionRole, 'unknown'>, string[]]
|
||||
>) {
|
||||
if (markers.every((marker) => names.has(marker))) {
|
||||
inferred.push(role);
|
||||
}
|
||||
}
|
||||
|
||||
if (!inferred.length && names.has('topic')) {
|
||||
inferred.push('topics');
|
||||
}
|
||||
|
||||
if (
|
||||
!inferred.includes('signature') &&
|
||||
['app1', 'app2', 'app3'].some((name) => names.has(name))
|
||||
) {
|
||||
inferred.push('signature');
|
||||
}
|
||||
|
||||
return inferred;
|
||||
}
|
||||
|
||||
function isSectionRole(value: string): value is SectionRole {
|
||||
return (
|
||||
function isSectionRole(value: string | null | undefined): value is SectionRole {
|
||||
return value !== undefined && value !== null && (
|
||||
value === 'customer' ||
|
||||
value === 'product_items' ||
|
||||
value === 'topics' ||
|
||||
@@ -124,11 +72,56 @@ function isSectionRole(value: string): value is SectionRole {
|
||||
);
|
||||
}
|
||||
|
||||
function getExplicitRoles(page: Template['schemas'][number]): SectionRole[] {
|
||||
const roles = new Set<SectionRole>();
|
||||
|
||||
for (const field of page) {
|
||||
if (!field || typeof field !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = 'name' in field && typeof field.name === 'string' ? field.name : null;
|
||||
const content =
|
||||
'content' in field && typeof field.content === 'string'
|
||||
? field.content.trim()
|
||||
: null;
|
||||
|
||||
for (const prefix of EXPLICIT_ROLE_PREFIXES) {
|
||||
const byName = name?.startsWith(prefix) ? name.slice(prefix.length) : null;
|
||||
|
||||
if (isSectionRole(byName)) {
|
||||
roles.add(byName);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSectionRole(content)) {
|
||||
roles.add(content);
|
||||
}
|
||||
}
|
||||
|
||||
return [...roles];
|
||||
}
|
||||
|
||||
function inferLegacyRoles(page: Template['schemas'][number]): SectionRole[] {
|
||||
const fieldNames = new Set(getFieldNames(page));
|
||||
const roles: SectionRole[] = [];
|
||||
|
||||
for (const [role, markers] of Object.entries(LEGACY_MARKERS) as Array<
|
||||
[Exclude<SectionRole, 'unknown'>, string[]]
|
||||
>) {
|
||||
if (markers.every((marker) => fieldNames.has(marker))) {
|
||||
roles.push(role);
|
||||
}
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
export function adaptTemplateForSectionRuntime(
|
||||
template: ResolvedTemplate,
|
||||
): CompatibleTemplate {
|
||||
const issues: RuntimeIssue[] = [];
|
||||
const schemaPages = template.schema.schemas;
|
||||
const issues: RuntimeIssue[] = [];
|
||||
|
||||
if (!Array.isArray(schemaPages)) {
|
||||
return {
|
||||
@@ -159,15 +152,18 @@ export function adaptTemplateForSectionRuntime(
|
||||
continue;
|
||||
}
|
||||
|
||||
const explicitRole = getExplicitRole(rawPage);
|
||||
const explicitRoles = getExplicitRoles(rawPage);
|
||||
const inferredRoles = inferLegacyRoles(rawPage);
|
||||
const primaryRole = explicitRole ?? inferredRoles[0] ?? 'unknown';
|
||||
const primaryRole = explicitRoles[0] ?? inferredRoles[0] ?? 'unknown';
|
||||
const roles = Array.from(
|
||||
new Set([primaryRole, ...inferredRoles].filter(isSectionRole)),
|
||||
new Set(
|
||||
[primaryRole, ...explicitRoles, ...inferredRoles].filter(isSectionRole),
|
||||
),
|
||||
);
|
||||
|
||||
let strategy: CompatibleTemplatePage['strategy'] = 'fallback';
|
||||
if (explicitRole) {
|
||||
|
||||
if (explicitRoles.length > 0) {
|
||||
strategy = 'explicit_marker';
|
||||
} else if (inferredRoles.length > 0) {
|
||||
strategy = 'legacy_inference';
|
||||
@@ -175,8 +171,7 @@ export function adaptTemplateForSectionRuntime(
|
||||
createIssue({
|
||||
code: 'LEGACY_COMPAT_MODE',
|
||||
severity: 'warning',
|
||||
message:
|
||||
'PDF template page role was inferred from legacy field markers.',
|
||||
message: 'PDF template page role inferred from legacy field markers.',
|
||||
details: { pageIndex, roles },
|
||||
}),
|
||||
);
|
||||
|
||||
1774
src/pdfme_template/ALLA_template_pdfme_product_v1.json
Normal file
1774
src/pdfme_template/ALLA_template_pdfme_product_v1.json
Normal file
File diff suppressed because one or more lines are too long
1747
src/pdfme_template/ONVALLA_template_pdfme_product_v1.json
Normal file
1747
src/pdfme_template/ONVALLA_template_pdfme_product_v1.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user