Use float text rows for header meta without table borders; rewrite seal images to float over party fields while keeping title underlines. Co-authored-by: Cursor <cursoragent@cursor.com>
195 lines
8.0 KiB
TypeScript
195 lines
8.0 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { describe, it } from 'node:test'
|
|
import {
|
|
buildContractDocxFileName,
|
|
inlineRemoteImagesAsDataUrls,
|
|
normalizeHeaderMetaFlexLayoutForDocx,
|
|
normalizePartyFlexLayoutForDocx,
|
|
normalizeSealImagesForDocx,
|
|
parseContractSupplierCode,
|
|
prepareContractHtmlForDocx,
|
|
stripContractLogoHtml,
|
|
stripWatermarkHtml,
|
|
toAbsoluteAssetUrls
|
|
} from './contract-html-to-docx'
|
|
|
|
describe('parseContractSupplierCode', () => {
|
|
it('parses supplier code from contractNo', () => {
|
|
assert.equal(parseContractSupplierCode('SX/X-JT20250926002'), 'SX')
|
|
assert.equal(parseContractSupplierCode('YD/X-JT20250926001'), 'YD')
|
|
})
|
|
|
|
it('defaults to SX when missing or unmatched', () => {
|
|
assert.equal(parseContractSupplierCode(undefined), 'SX')
|
|
assert.equal(parseContractSupplierCode(''), 'SX')
|
|
assert.equal(parseContractSupplierCode('NO-MATCH'), 'SX')
|
|
})
|
|
})
|
|
|
|
describe('stripContractLogoHtml', () => {
|
|
it('removes logo img and logo-only wrapper div', () => {
|
|
const html =
|
|
'<div><img src="/logo.gif" alt="logo" /></div><p>正文</p><img src="/watermask.jpg" alt="水印" />'
|
|
const out = stripContractLogoHtml(html)
|
|
assert.equal(out.includes('logo.gif'), false)
|
|
assert.equal(out.includes('正文'), true)
|
|
assert.equal(out.includes('watermask.jpg'), true)
|
|
})
|
|
})
|
|
|
|
describe('stripWatermarkHtml', () => {
|
|
it('removes watermark images', () => {
|
|
const html =
|
|
'<div style="text-align:center"><img src="/watermask.jpg" alt="水印" /></div><p>正文</p>'
|
|
const out = stripWatermarkHtml(html)
|
|
assert.equal(out.includes('watermask'), false)
|
|
assert.equal(out.includes('水印'), false)
|
|
assert.equal(out.includes('正文'), true)
|
|
})
|
|
})
|
|
|
|
describe('toAbsoluteAssetUrls', () => {
|
|
it('prefixes root-relative src with origin', () => {
|
|
const out = toAbsoluteAssetUrls(
|
|
'<img src="/seals/a.png" /><img src="https://cdn.example/x.png" />',
|
|
'http://localhost:3006'
|
|
)
|
|
assert.equal(out.includes('src="http://localhost:3006/seals/a.png"'), true)
|
|
assert.equal(out.includes('src="https://cdn.example/x.png"'), true)
|
|
})
|
|
})
|
|
|
|
describe('normalizeHeaderMetaFlexLayoutForDocx', () => {
|
|
it('converts supplier/contractNo and demander/signPlace rows into float text rows', () => {
|
|
const html = `
|
|
<div style="display: flex; margin-bottom: 0; position: relative;">
|
|
<div style="flex: 1;">供方:安庆市山峡化工有限公司</div>
|
|
<div style="position: absolute; right: 0px; min-width: 300px;">合同编号:SX/X-JT20260828004</div>
|
|
</div>
|
|
<div style="display: flex; margin-bottom: 0; position: relative;">
|
|
<div style="flex: 1;">需方:安徽海康药业股份有限公司</div>
|
|
<div style="position: absolute; right: 0px; min-width: 300px;">签订地点:安徽安庆</div>
|
|
</div>`
|
|
const out = normalizeHeaderMetaFlexLayoutForDocx(html)
|
|
assert.equal(out.includes('<table'), false)
|
|
assert.equal(out.includes('float:right'), true)
|
|
assert.equal(out.includes('position: absolute'), false)
|
|
assert.equal(out.includes('供方:安庆市山峡化工有限公司'), true)
|
|
assert.equal(out.includes('合同编号:SX/X-JT20260828004'), true)
|
|
assert.equal(out.includes('需方:安徽海康药业股份有限公司'), true)
|
|
assert.equal(out.includes('签订地点:安徽安庆'), true)
|
|
})
|
|
|
|
it('does not rewrite bottom party space-between block', () => {
|
|
const html = `
|
|
<div style="display: flex; justify-content: space-between; margin-top: 10px;">
|
|
<div style="flex: 1;"><div>供方</div><div>单位名称(章):甲</div></div>
|
|
<div style="flex: 1;"><div>需方</div><div>单位名称(章):乙</div></div>
|
|
</div>`
|
|
const out = normalizeHeaderMetaFlexLayoutForDocx(html)
|
|
assert.equal(out.includes('justify-content: space-between'), true)
|
|
assert.equal(out.includes('<table'), false)
|
|
})
|
|
})
|
|
|
|
describe('normalizeSealImagesForDocx', () => {
|
|
it('rewrites absolute seal images to float-over-text styles', () => {
|
|
const html =
|
|
'<img src="/yz.png" alt="印章" style="position: absolute; top: 50%; left: 50%; width: 140px; height: 140px;" />'
|
|
const out = normalizeSealImagesForDocx(html)
|
|
assert.equal(out.includes('position: absolute'), false)
|
|
assert.equal(out.includes('float:left'), true)
|
|
assert.equal(out.includes('alt="印章"'), true)
|
|
})
|
|
})
|
|
|
|
describe('normalizePartyFlexLayoutForDocx', () => {
|
|
it('converts bottom party flex columns into a two-cell table', () => {
|
|
const html = `
|
|
<div style="display: flex; justify-content: space-between; margin-top: 10px;">
|
|
<div style="flex: 1; border: 1px solid #000; padding: 8px;">
|
|
<div style="font-weight: bold;">供方</div>
|
|
<div>单位名称(章):甲公司</div>
|
|
</div>
|
|
<div style="flex: 1; border: 1px solid #000; padding: 8px;">
|
|
<div style="font-weight: bold;">需方</div>
|
|
<div>单位名称(章):乙公司</div>
|
|
</div>
|
|
</div>`
|
|
const out = normalizePartyFlexLayoutForDocx(html)
|
|
assert.equal(out.includes('display: flex'), false)
|
|
assert.equal(out.includes('<table'), true)
|
|
assert.equal(out.includes('width:50%'), true)
|
|
assert.equal(out.includes('供方'), true)
|
|
assert.equal(out.includes('需方'), true)
|
|
assert.equal(out.includes('甲公司'), true)
|
|
assert.equal(out.includes('乙公司'), true)
|
|
})
|
|
})
|
|
|
|
describe('prepareContractHtmlForDocx', () => {
|
|
it('keeps logo and does not inject watermark for SX', () => {
|
|
const out = prepareContractHtmlForDocx({
|
|
html: '<div><img src="/logo.gif" /></div><p>正文</p>',
|
|
contractNo: 'SX/X-JT20250926002',
|
|
origin: 'http://localhost:3006'
|
|
})
|
|
assert.equal(out.includes('watermask'), false)
|
|
assert.equal(out.includes('logo.gif'), true)
|
|
assert.equal(out.includes('http://localhost:3006/logo.gif'), true)
|
|
})
|
|
|
|
it('strips logo and watermark for non-SX', () => {
|
|
const out = prepareContractHtmlForDocx({
|
|
html: '<div><img src="/logo.gif" /></div><img src="/watermask.jpg" alt="水印" /><p>正文</p>',
|
|
contractNo: 'YD/X-JT20250926001',
|
|
origin: 'http://localhost:3006'
|
|
})
|
|
assert.equal(out.includes('watermask'), false)
|
|
assert.equal(out.includes('logo.gif'), false)
|
|
assert.equal(out.includes('正文'), true)
|
|
})
|
|
|
|
it('normalizes header meta and party flex layout during prepare', () => {
|
|
const html = `
|
|
<div style="display: flex; margin-bottom: 0; position: relative;">
|
|
<div style="flex: 1;">供方:甲</div>
|
|
<div style="position: absolute; right: 0px;">合同编号:NO1</div>
|
|
</div>
|
|
<img src="/yz.png" alt="印章" style="position: absolute; top: 50%; left: 50%;" />
|
|
<div style="display: flex; justify-content: space-between; margin-top: 10px;">
|
|
<div style="flex: 1;"><div style="border-bottom: 1px solid #000;">供方</div><div>A</div></div>
|
|
<div style="flex: 1;"><div style="border-bottom: 1px solid #000;">需方</div><div>B</div></div>
|
|
</div>`
|
|
const out = prepareContractHtmlForDocx({
|
|
html,
|
|
contractNo: 'SX/X-JT1',
|
|
origin: 'http://localhost:3006'
|
|
})
|
|
assert.equal(out.includes('float:right'), true)
|
|
assert.equal(out.includes('<table'), true)
|
|
assert.equal(out.includes('float:left'), true)
|
|
assert.equal(/position:\s*absolute/.test(out), false)
|
|
assert.equal(/justify-content:\s*space-between/.test(out), false)
|
|
assert.equal(out.includes('border-bottom: 1px solid #000'), true)
|
|
})
|
|
})
|
|
|
|
describe('buildContractDocxFileName', () => {
|
|
it('uses contract name and sanitizes illegal chars', () => {
|
|
assert.equal(buildContractDocxFileName('甲/乙合同', 'SX/X-JT1'), '甲-乙合同合同.docx')
|
|
})
|
|
|
|
it('falls back to contractNo', () => {
|
|
assert.equal(buildContractDocxFileName('', 'SX/X-JT1'), 'SX-X-JT1合同.docx')
|
|
})
|
|
})
|
|
|
|
describe('inlineRemoteImagesAsDataUrls', () => {
|
|
it('keeps data URLs and leaves html without remote images unchanged', async () => {
|
|
const html = '<p>无图</p><img src="data:image/png;base64,aaa" />'
|
|
const out = await inlineRemoteImagesAsDataUrls(html)
|
|
assert.equal(out, html)
|
|
})
|
|
})
|