docxmlater 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1132,76 +1132,168 @@ class Document {
1132
1132
  }
1133
1133
  return counts;
1134
1134
  }
1135
- applyCustomFormattingToExistingStyles() {
1135
+ static DEFAULT_HEADING1_CONFIG = {
1136
+ run: {
1137
+ font: 'Verdana',
1138
+ size: 18,
1139
+ bold: true,
1140
+ color: '000000',
1141
+ },
1142
+ paragraph: {
1143
+ alignment: 'left',
1144
+ spacing: { before: 0, after: 240, line: 240, lineRule: 'auto' },
1145
+ },
1146
+ };
1147
+ static DEFAULT_HEADING2_CONFIG = {
1148
+ run: {
1149
+ font: 'Verdana',
1150
+ size: 14,
1151
+ bold: true,
1152
+ color: '000000',
1153
+ },
1154
+ paragraph: {
1155
+ alignment: 'left',
1156
+ spacing: { before: 120, after: 120, line: 240, lineRule: 'auto' },
1157
+ },
1158
+ tableOptions: {
1159
+ shading: 'BFBFBF',
1160
+ marginTop: 0,
1161
+ marginBottom: 0,
1162
+ marginLeft: 115,
1163
+ marginRight: 115,
1164
+ tableWidthPercent: 5000,
1165
+ },
1166
+ };
1167
+ static DEFAULT_HEADING3_CONFIG = {
1168
+ run: {
1169
+ font: 'Verdana',
1170
+ size: 12,
1171
+ bold: true,
1172
+ color: '000000',
1173
+ },
1174
+ paragraph: {
1175
+ alignment: 'left',
1176
+ spacing: { before: 60, after: 60, line: 240, lineRule: 'auto' },
1177
+ },
1178
+ };
1179
+ static DEFAULT_NORMAL_CONFIG = {
1180
+ run: {
1181
+ font: 'Verdana',
1182
+ size: 12,
1183
+ color: '000000',
1184
+ },
1185
+ paragraph: {
1186
+ alignment: 'left',
1187
+ spacing: { before: 60, after: 60, line: 240, lineRule: 'auto' },
1188
+ },
1189
+ };
1190
+ static DEFAULT_LIST_PARAGRAPH_CONFIG = {
1191
+ run: {
1192
+ font: 'Verdana',
1193
+ size: 12,
1194
+ color: '000000',
1195
+ },
1196
+ paragraph: {
1197
+ alignment: 'left',
1198
+ spacing: { before: 0, after: 60, line: 240, lineRule: 'auto' },
1199
+ indentation: { left: 360, hanging: 360 },
1200
+ contextualSpacing: true,
1201
+ },
1202
+ };
1203
+ applyCustomFormattingToExistingStyles(options) {
1136
1204
  const results = { heading1: false, heading2: false, heading3: false, normal: false, listParagraph: false };
1137
1205
  const heading1 = this.stylesManager.getStyle('Heading1');
1138
1206
  const heading2 = this.stylesManager.getStyle('Heading2');
1139
1207
  const heading3 = this.stylesManager.getStyle('Heading3');
1140
1208
  const normal = this.stylesManager.getStyle('Normal');
1141
1209
  const listParagraph = this.stylesManager.getStyle('ListParagraph');
1142
- if (heading1) {
1143
- heading1.setRunFormatting({
1144
- font: 'Verdana',
1145
- size: 18,
1146
- bold: true,
1147
- color: '000000'
1148
- });
1149
- heading1.setParagraphFormatting({
1150
- alignment: 'left',
1151
- spacing: { before: 0, after: 240, line: 240, lineRule: 'auto' }
1152
- });
1210
+ const h1Config = {
1211
+ run: {
1212
+ ...(heading1?.getRunFormatting() || Document.DEFAULT_HEADING1_CONFIG.run),
1213
+ ...options?.heading1?.run
1214
+ },
1215
+ paragraph: {
1216
+ ...(heading1?.getParagraphFormatting() || Document.DEFAULT_HEADING1_CONFIG.paragraph),
1217
+ ...options?.heading1?.paragraph
1218
+ },
1219
+ };
1220
+ const h2Config = {
1221
+ run: {
1222
+ ...(heading2?.getRunFormatting() || Document.DEFAULT_HEADING2_CONFIG.run),
1223
+ ...options?.heading2?.run
1224
+ },
1225
+ paragraph: {
1226
+ ...(heading2?.getParagraphFormatting() || Document.DEFAULT_HEADING2_CONFIG.paragraph),
1227
+ ...options?.heading2?.paragraph
1228
+ },
1229
+ tableOptions: {
1230
+ ...Document.DEFAULT_HEADING2_CONFIG.tableOptions,
1231
+ ...options?.heading2?.tableOptions
1232
+ },
1233
+ };
1234
+ const h3Config = {
1235
+ run: {
1236
+ ...(heading3?.getRunFormatting() || Document.DEFAULT_HEADING3_CONFIG.run),
1237
+ ...options?.heading3?.run
1238
+ },
1239
+ paragraph: {
1240
+ ...(heading3?.getParagraphFormatting() || Document.DEFAULT_HEADING3_CONFIG.paragraph),
1241
+ ...options?.heading3?.paragraph
1242
+ },
1243
+ };
1244
+ const normalConfig = {
1245
+ run: {
1246
+ ...(normal?.getRunFormatting() || Document.DEFAULT_NORMAL_CONFIG.run),
1247
+ ...options?.normal?.run
1248
+ },
1249
+ paragraph: {
1250
+ ...(normal?.getParagraphFormatting() || Document.DEFAULT_NORMAL_CONFIG.paragraph),
1251
+ ...options?.normal?.paragraph
1252
+ },
1253
+ };
1254
+ const listParaConfig = {
1255
+ run: {
1256
+ ...(listParagraph?.getRunFormatting() || Document.DEFAULT_LIST_PARAGRAPH_CONFIG.run),
1257
+ ...options?.listParagraph?.run
1258
+ },
1259
+ paragraph: {
1260
+ ...(listParagraph?.getParagraphFormatting() || Document.DEFAULT_LIST_PARAGRAPH_CONFIG.paragraph),
1261
+ ...options?.listParagraph?.paragraph
1262
+ },
1263
+ };
1264
+ if (heading1 && h1Config.run && h1Config.paragraph) {
1265
+ if (h1Config.run)
1266
+ heading1.setRunFormatting(h1Config.run);
1267
+ if (h1Config.paragraph)
1268
+ heading1.setParagraphFormatting(h1Config.paragraph);
1153
1269
  results.heading1 = true;
1154
1270
  }
1155
- if (heading2) {
1156
- heading2.setRunFormatting({
1157
- font: 'Verdana',
1158
- size: 14,
1159
- bold: true,
1160
- color: '000000'
1161
- });
1162
- heading2.setParagraphFormatting({
1163
- alignment: 'left',
1164
- spacing: { before: 120, after: 120, line: 240, lineRule: 'auto' }
1165
- });
1271
+ if (heading2 && h2Config.run && h2Config.paragraph) {
1272
+ if (h2Config.run)
1273
+ heading2.setRunFormatting(h2Config.run);
1274
+ if (h2Config.paragraph)
1275
+ heading2.setParagraphFormatting(h2Config.paragraph);
1166
1276
  results.heading2 = true;
1167
1277
  }
1168
- if (heading3) {
1169
- heading3.setRunFormatting({
1170
- font: 'Verdana',
1171
- size: 12,
1172
- bold: true,
1173
- color: '000000'
1174
- });
1175
- heading3.setParagraphFormatting({
1176
- alignment: 'left',
1177
- spacing: { before: 60, after: 60, line: 240, lineRule: 'auto' }
1178
- });
1278
+ if (heading3 && h3Config.run && h3Config.paragraph) {
1279
+ if (h3Config.run)
1280
+ heading3.setRunFormatting(h3Config.run);
1281
+ if (h3Config.paragraph)
1282
+ heading3.setParagraphFormatting(h3Config.paragraph);
1179
1283
  results.heading3 = true;
1180
1284
  }
1181
- if (normal) {
1182
- normal.setRunFormatting({
1183
- font: 'Verdana',
1184
- size: 12,
1185
- color: '000000'
1186
- });
1187
- normal.setParagraphFormatting({
1188
- alignment: 'left',
1189
- spacing: { before: 60, after: 60, line: 240, lineRule: 'auto' }
1190
- });
1285
+ if (normal && normalConfig.run && normalConfig.paragraph) {
1286
+ if (normalConfig.run)
1287
+ normal.setRunFormatting(normalConfig.run);
1288
+ if (normalConfig.paragraph)
1289
+ normal.setParagraphFormatting(normalConfig.paragraph);
1191
1290
  results.normal = true;
1192
1291
  }
1193
- if (listParagraph) {
1194
- listParagraph.setRunFormatting({
1195
- font: 'Verdana',
1196
- size: 12,
1197
- color: '000000'
1198
- });
1199
- listParagraph.setParagraphFormatting({
1200
- alignment: 'left',
1201
- spacing: { before: 0, after: 60, line: 240, lineRule: 'auto' },
1202
- indentation: { left: 360, hanging: 360 },
1203
- contextualSpacing: true
1204
- });
1292
+ if (listParagraph && listParaConfig.run && listParaConfig.paragraph) {
1293
+ if (listParaConfig.run)
1294
+ listParagraph.setRunFormatting(listParaConfig.run);
1295
+ if (listParaConfig.paragraph)
1296
+ listParagraph.setParagraphFormatting(listParaConfig.paragraph);
1205
1297
  results.listParagraph = true;
1206
1298
  }
1207
1299
  const processedParagraphs = new Set();
@@ -1240,30 +1332,42 @@ class Document {
1240
1332
  }
1241
1333
  const { inTable, cell } = this.isParagraphInTable(para);
1242
1334
  if (inTable && cell) {
1243
- cell.setShading({ fill: 'BFBFBF' });
1244
- cell.setMargins({ top: 0, bottom: 0, left: 115, right: 115 });
1245
- const table = this.getAllTables().find(t => {
1246
- for (const row of t.getRows()) {
1247
- for (const c of row.getCells()) {
1248
- if (c === cell)
1249
- return true;
1335
+ if (h2Config.tableOptions?.shading) {
1336
+ cell.setShading({ fill: h2Config.tableOptions.shading });
1337
+ }
1338
+ if (h2Config.tableOptions?.marginTop !== undefined || h2Config.tableOptions?.marginBottom !== undefined ||
1339
+ h2Config.tableOptions?.marginLeft !== undefined || h2Config.tableOptions?.marginRight !== undefined) {
1340
+ cell.setMargins({
1341
+ top: h2Config.tableOptions.marginTop ?? 0,
1342
+ bottom: h2Config.tableOptions.marginBottom ?? 0,
1343
+ left: h2Config.tableOptions.marginLeft ?? 115,
1344
+ right: h2Config.tableOptions.marginRight ?? 115,
1345
+ });
1346
+ }
1347
+ if (h2Config.tableOptions?.tableWidthPercent) {
1348
+ const table = this.getAllTables().find(t => {
1349
+ for (const row of t.getRows()) {
1350
+ for (const c of row.getCells()) {
1351
+ if (c === cell)
1352
+ return true;
1353
+ }
1250
1354
  }
1355
+ return false;
1356
+ });
1357
+ if (table) {
1358
+ table.setWidth(h2Config.tableOptions.tableWidthPercent);
1359
+ table.setWidthType('pct');
1251
1360
  }
1252
- return false;
1253
- });
1254
- if (table) {
1255
- table.setWidth(5000);
1256
- table.setWidthType('pct');
1257
1361
  }
1258
1362
  }
1259
1363
  else {
1260
1364
  this.wrapParagraphInTable(para, {
1261
- shading: 'BFBFBF',
1262
- marginTop: 0,
1263
- marginBottom: 0,
1264
- marginLeft: 115,
1265
- marginRight: 115,
1266
- tableWidthPercent: 5000
1365
+ shading: h2Config.tableOptions?.shading ?? 'BFBFBF',
1366
+ marginTop: h2Config.tableOptions?.marginTop ?? 0,
1367
+ marginBottom: h2Config.tableOptions?.marginBottom ?? 0,
1368
+ marginLeft: h2Config.tableOptions?.marginLeft ?? 115,
1369
+ marginRight: h2Config.tableOptions?.marginRight ?? 115,
1370
+ tableWidthPercent: h2Config.tableOptions?.tableWidthPercent ?? 5000,
1267
1371
  });
1268
1372
  }
1269
1373
  processedParagraphs.add(para);
@@ -1305,23 +1409,234 @@ class Document {
1305
1409
  processedParagraphs.add(para);
1306
1410
  }
1307
1411
  }
1308
- const hyperlinks = this.getHyperlinks();
1309
- for (const { hyperlink, paragraph } of hyperlinks) {
1310
- const text = hyperlink.getText().trim();
1311
- if (text === 'Top of the Document' || text === 'Top of Document') {
1312
- paragraph.setAlignment('right');
1313
- paragraph.setSpaceBefore(0);
1314
- paragraph.setSpaceAfter(0);
1412
+ return results;
1413
+ }
1414
+ applyStylesFromObjects(...styles) {
1415
+ const options = {};
1416
+ for (const style of styles) {
1417
+ const styleId = style.getStyleId();
1418
+ switch (styleId) {
1419
+ case 'Heading1':
1420
+ options.heading1 = {
1421
+ run: style.getRunFormatting(),
1422
+ paragraph: style.getParagraphFormatting(),
1423
+ };
1424
+ break;
1425
+ case 'Heading2':
1426
+ options.heading2 = {
1427
+ run: style.getRunFormatting(),
1428
+ paragraph: style.getParagraphFormatting(),
1429
+ tableOptions: style.getHeading2TableOptions(),
1430
+ };
1431
+ break;
1432
+ case 'Heading3':
1433
+ options.heading3 = {
1434
+ run: style.getRunFormatting(),
1435
+ paragraph: style.getParagraphFormatting(),
1436
+ };
1437
+ break;
1438
+ case 'Normal':
1439
+ options.normal = {
1440
+ run: style.getRunFormatting(),
1441
+ paragraph: style.getParagraphFormatting(),
1442
+ };
1443
+ break;
1444
+ case 'ListParagraph':
1445
+ options.listParagraph = {
1446
+ run: style.getRunFormatting(),
1447
+ paragraph: style.getParagraphFormatting(),
1448
+ };
1449
+ break;
1450
+ default:
1451
+ break;
1452
+ }
1453
+ }
1454
+ return this.applyCustomFormattingToExistingStyles(options);
1455
+ }
1456
+ parseTOCFieldInstruction(instrText) {
1457
+ const levels = new Set();
1458
+ const outlineMatch = instrText.match(/\\o\s+"(\d+)-(\d+)"/);
1459
+ if (outlineMatch && outlineMatch[1] && outlineMatch[2]) {
1460
+ const start = parseInt(outlineMatch[1], 10);
1461
+ const end = parseInt(outlineMatch[2], 10);
1462
+ for (let i = start; i <= end; i++) {
1463
+ if (i >= 1 && i <= 9) {
1464
+ levels.add(i);
1465
+ }
1466
+ }
1467
+ }
1468
+ const styleMatches = instrText.matchAll(/\\t\s+"([^"]+)",(\d+),"/g);
1469
+ for (const match of styleMatches) {
1470
+ const styleName = match[1];
1471
+ const levelStr = match[2];
1472
+ if (!styleName || !levelStr)
1473
+ continue;
1474
+ const level = parseInt(levelStr, 10);
1475
+ const headingMatch = styleName.match(/Heading\s*(\d+)/i);
1476
+ if (headingMatch && headingMatch[1]) {
1477
+ const headingLevel = parseInt(headingMatch[1], 10);
1478
+ if (headingLevel >= 1 && headingLevel <= 9) {
1479
+ levels.add(headingLevel);
1480
+ }
1481
+ }
1482
+ else if (level >= 1 && level <= 9) {
1483
+ levels.add(level);
1315
1484
  }
1316
1485
  }
1317
- this.updateAllHyperlinkColors('0000FF');
1318
- const tocElements = this.getTableOfContentsElements();
1319
- for (const tocElement of tocElements) {
1320
- const toc = tocElement.getTableOfContents();
1321
- toc.setShowPageNumbers(false);
1322
- toc.setHideInWebLayout(true);
1486
+ if (levels.size === 0) {
1487
+ return [1, 2, 3];
1323
1488
  }
1324
- return results;
1489
+ return Array.from(levels).sort((a, b) => a - b);
1490
+ }
1491
+ findHeadingsForTOC(levels) {
1492
+ const headings = [];
1493
+ const levelSet = new Set(levels);
1494
+ for (const element of this.bodyElements) {
1495
+ if (element instanceof Paragraph_1.Paragraph) {
1496
+ const para = element;
1497
+ const formatting = para.getFormatting();
1498
+ if (formatting.style) {
1499
+ const styleMatch = formatting.style.match(/Heading(\d+)/i);
1500
+ if (styleMatch && styleMatch[1]) {
1501
+ const headingLevel = parseInt(styleMatch[1], 10);
1502
+ if (levelSet.has(headingLevel)) {
1503
+ const text = para.getText().trim();
1504
+ if (text) {
1505
+ const bookmark = this.bookmarkManager.createHeadingBookmark(text);
1506
+ headings.push({
1507
+ level: headingLevel,
1508
+ text: text,
1509
+ bookmark: bookmark.getName()
1510
+ });
1511
+ }
1512
+ }
1513
+ }
1514
+ }
1515
+ }
1516
+ }
1517
+ return headings;
1518
+ }
1519
+ generateTOCXML(headings, originalInstrText) {
1520
+ const sdtId = Math.floor(Math.random() * 2000000000) - 1000000000;
1521
+ let tocXml = '<w:sdt>';
1522
+ tocXml += '<w:sdtPr>';
1523
+ tocXml += `<w:id w:val="${sdtId}"/>`;
1524
+ tocXml += '<w:docPartObj>';
1525
+ tocXml += '<w:docPartGallery w:val="Table of Contents"/>';
1526
+ tocXml += '<w:docPartUnique w:val="1"/>';
1527
+ tocXml += '</w:docPartObj>';
1528
+ tocXml += '</w:sdtPr>';
1529
+ tocXml += '<w:sdtContent>';
1530
+ tocXml += '<w:p>';
1531
+ tocXml += '<w:pPr>';
1532
+ tocXml += '<w:spacing w:after="0" w:before="0" w:line="240" w:lineRule="auto"/>';
1533
+ tocXml += '</w:pPr>';
1534
+ tocXml += '<w:r><w:fldChar w:fldCharType="begin"/></w:r>';
1535
+ tocXml += '<w:r>';
1536
+ tocXml += `<w:instrText xml:space="preserve">${this.escapeXml(originalInstrText)}</w:instrText>`;
1537
+ tocXml += '</w:r>';
1538
+ tocXml += '<w:r><w:fldChar w:fldCharType="separate"/></w:r>';
1539
+ if (headings.length > 0 && headings[0]) {
1540
+ tocXml += this.buildTOCEntryXML(headings[0]);
1541
+ }
1542
+ tocXml += '</w:p>';
1543
+ for (let i = 1; i < headings.length; i++) {
1544
+ const heading = headings[i];
1545
+ if (!heading)
1546
+ continue;
1547
+ tocXml += '<w:p>';
1548
+ tocXml += '<w:pPr>';
1549
+ tocXml += '<w:spacing w:after="0" w:before="0" w:line="240" w:lineRule="auto"/>';
1550
+ const indent = (heading.level - 1) * 720;
1551
+ if (indent > 0) {
1552
+ tocXml += `<w:ind w:left="${indent}"/>`;
1553
+ }
1554
+ tocXml += '</w:pPr>';
1555
+ tocXml += this.buildTOCEntryXML(heading);
1556
+ tocXml += '</w:p>';
1557
+ }
1558
+ tocXml += '<w:p>';
1559
+ tocXml += '<w:pPr>';
1560
+ tocXml += '<w:spacing w:after="0" w:before="0" w:line="240" w:lineRule="auto"/>';
1561
+ tocXml += '</w:pPr>';
1562
+ tocXml += '<w:r><w:fldChar w:fldCharType="end"/></w:r>';
1563
+ tocXml += '</w:p>';
1564
+ tocXml += '</w:sdtContent>';
1565
+ tocXml += '</w:sdt>';
1566
+ return tocXml;
1567
+ }
1568
+ buildTOCEntryXML(heading) {
1569
+ const escapedText = this.escapeXml(heading.text);
1570
+ let xml = '';
1571
+ xml += `<w:hyperlink w:anchor="${this.escapeXml(heading.bookmark)}">`;
1572
+ xml += '<w:r>';
1573
+ xml += '<w:rPr>';
1574
+ xml += '<w:rFonts w:ascii="Verdana" w:hAnsi="Verdana" w:cs="Verdana" w:eastAsia="Verdana"/>';
1575
+ xml += '<w:color w:val="0000FF"/>';
1576
+ xml += '<w:sz w:val="24"/>';
1577
+ xml += '<w:szCs w:val="24"/>';
1578
+ xml += '<w:u w:val="single"/>';
1579
+ xml += '</w:rPr>';
1580
+ xml += `<w:t xml:space="preserve">${escapedText}</w:t>`;
1581
+ xml += '</w:r>';
1582
+ xml += '</w:hyperlink>';
1583
+ return xml;
1584
+ }
1585
+ escapeXml(text) {
1586
+ return text
1587
+ .replace(/&/g, '&amp;')
1588
+ .replace(/</g, '&lt;')
1589
+ .replace(/>/g, '&gt;')
1590
+ .replace(/"/g, '&quot;')
1591
+ .replace(/'/g, '&apos;');
1592
+ }
1593
+ async replaceTableOfContents(filePath) {
1594
+ const handler = new ZipHandler_1.ZipHandler();
1595
+ await handler.load(filePath);
1596
+ const docXml = handler.getFileAsString('word/document.xml');
1597
+ if (!docXml) {
1598
+ throw new Error('word/document.xml not found in document');
1599
+ }
1600
+ const tocRegex = /<w:sdt>[\s\S]*?<w:docPartGallery w:val="Table of Contents"[\s\S]*?<\/w:sdt>/g;
1601
+ const tocMatches = Array.from(docXml.matchAll(tocRegex));
1602
+ if (tocMatches.length === 0) {
1603
+ return 0;
1604
+ }
1605
+ let replacedCount = 0;
1606
+ let modifiedXml = docXml;
1607
+ for (const match of tocMatches) {
1608
+ try {
1609
+ const tocXml = match[0];
1610
+ const instrMatch = tocXml.match(/<w:instrText[^>]*>([\s\S]*?)<\/w:instrText>/);
1611
+ if (!instrMatch || !instrMatch[1]) {
1612
+ continue;
1613
+ }
1614
+ let fieldInstruction = instrMatch[1];
1615
+ fieldInstruction = fieldInstruction
1616
+ .replace(/&lt;/g, '<')
1617
+ .replace(/&gt;/g, '>')
1618
+ .replace(/&quot;/g, '"')
1619
+ .replace(/&apos;/g, "'")
1620
+ .replace(/&amp;/g, '&');
1621
+ const levels = this.parseTOCFieldInstruction(fieldInstruction);
1622
+ const headings = this.findHeadingsForTOC(levels);
1623
+ if (headings.length === 0) {
1624
+ continue;
1625
+ }
1626
+ const newTocXml = this.generateTOCXML(headings, fieldInstruction);
1627
+ modifiedXml = modifiedXml.replace(tocXml, newTocXml);
1628
+ replacedCount++;
1629
+ }
1630
+ catch (error) {
1631
+ console.error(`Error replacing TOC: ${error}`);
1632
+ continue;
1633
+ }
1634
+ }
1635
+ if (replacedCount > 0) {
1636
+ handler.updateFile('word/document.xml', modifiedXml);
1637
+ await handler.save(filePath);
1638
+ }
1639
+ return replacedCount;
1325
1640
  }
1326
1641
  getSection() {
1327
1642
  return this.section;