easy-template-x 6.2.1 → 6.2.2
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.
- package/dist/cjs/easy-template-x.cjs +39 -31
- package/dist/es/easy-template-x.mjs +38 -32
- package/dist/types/office/contentTypesFile.d.ts +1 -1
- package/dist/types/office/mediaFiles.d.ts +1 -0
- package/dist/types/plugins/index.d.ts +1 -0
- package/dist/types/utils/txt.d.ts +1 -0
- package/package.json +1 -1
- package/src/office/contentTypesFile.ts +9 -11
- package/src/office/mediaFiles.ts +19 -12
- package/src/plugins/image/imagePlugin.ts +7 -8
- package/src/plugins/index.ts +1 -0
- package/src/utils/txt.ts +5 -0
|
@@ -404,6 +404,10 @@ function stringValue(val) {
|
|
|
404
404
|
function normalizeDoubleQuotes(text) {
|
|
405
405
|
return text.replace(nonStandardDoubleQuotesRegex, standardDoubleQuotes);
|
|
406
406
|
}
|
|
407
|
+
function countOccurrences(text, substring) {
|
|
408
|
+
// https://stackoverflow.com/questions/4009756/how-to-count-string-occurrence-in-string
|
|
409
|
+
return (text.match(new RegExp(substring, 'g')) || []).length;
|
|
410
|
+
}
|
|
407
411
|
|
|
408
412
|
class JsZipHelper {
|
|
409
413
|
static toJsZipOutputType(binaryOrType) {
|
|
@@ -1215,13 +1219,12 @@ class ContentTypesFile {
|
|
|
1215
1219
|
// Parse the content types file
|
|
1216
1220
|
await this.parseContentTypesFile();
|
|
1217
1221
|
|
|
1218
|
-
// Mime type already exists
|
|
1219
|
-
if (this.contentTypes[mime]) return;
|
|
1220
|
-
|
|
1221
1222
|
// Extension already exists
|
|
1222
|
-
//
|
|
1223
|
+
//
|
|
1224
|
+
// Multiple extensions may map to the same mime type, but a single
|
|
1225
|
+
// extension must only map to one mime type.
|
|
1223
1226
|
const extension = MimeTypeHelper.getDefaultExtension(mime);
|
|
1224
|
-
if (
|
|
1227
|
+
if (this.contentTypes[extension]) return;
|
|
1225
1228
|
|
|
1226
1229
|
// Add new node
|
|
1227
1230
|
const typeNode = xml.create.generalNode('Default');
|
|
@@ -1233,11 +1236,11 @@ class ContentTypesFile {
|
|
|
1233
1236
|
|
|
1234
1237
|
// Update state
|
|
1235
1238
|
this.addedNew = true;
|
|
1236
|
-
this.contentTypes[
|
|
1239
|
+
this.contentTypes[extension] = mime;
|
|
1237
1240
|
}
|
|
1238
|
-
async
|
|
1241
|
+
async xmlString() {
|
|
1239
1242
|
await this.parseContentTypesFile();
|
|
1240
|
-
return
|
|
1243
|
+
return xml.parser.serializeFile(this.root);
|
|
1241
1244
|
}
|
|
1242
1245
|
|
|
1243
1246
|
/**
|
|
@@ -1266,7 +1269,7 @@ class ContentTypesFile {
|
|
|
1266
1269
|
if (!contentTypeAttribute) continue;
|
|
1267
1270
|
const extensionAttribute = genNode.attributes['Extension'];
|
|
1268
1271
|
if (!extensionAttribute) continue;
|
|
1269
|
-
this.contentTypes[
|
|
1272
|
+
this.contentTypes[extensionAttribute] = contentTypeAttribute;
|
|
1270
1273
|
}
|
|
1271
1274
|
}
|
|
1272
1275
|
}
|
|
@@ -1286,39 +1289,38 @@ class MediaFiles {
|
|
|
1286
1289
|
* Returns the media file path.
|
|
1287
1290
|
*/
|
|
1288
1291
|
async add(mediaFile, mime) {
|
|
1289
|
-
//
|
|
1292
|
+
// Check if already added
|
|
1290
1293
|
if (this.files.has(mediaFile)) return this.files.get(mediaFile);
|
|
1291
1294
|
|
|
1292
|
-
//
|
|
1295
|
+
// Hash existing media files
|
|
1293
1296
|
await this.hashMediaFiles();
|
|
1294
1297
|
|
|
1295
|
-
//
|
|
1298
|
+
// Hash the new file
|
|
1296
1299
|
// Note: Even though hashing the base64 string may seem inefficient
|
|
1297
1300
|
// (requires extra step in some cases) in practice it is significantly
|
|
1298
1301
|
// faster than hashing a 'binarystring'.
|
|
1299
1302
|
const base64 = await Binary.toBase64(mediaFile);
|
|
1300
1303
|
const hash = sha1(base64);
|
|
1301
1304
|
|
|
1302
|
-
//
|
|
1303
|
-
//
|
|
1305
|
+
// Check if file already exists
|
|
1306
|
+
// Note: this can be optimized by keeping both mapping by filename as well as by hash
|
|
1304
1307
|
let path = Object.keys(this.hashes).find(p => this.hashes[p] === hash);
|
|
1305
1308
|
if (path) return path;
|
|
1306
1309
|
|
|
1307
|
-
//
|
|
1310
|
+
// Generate unique media file name
|
|
1311
|
+
const baseFilename = this.baseFilename(mime);
|
|
1308
1312
|
const extension = MimeTypeHelper.getDefaultExtension(mime);
|
|
1309
1313
|
do {
|
|
1310
1314
|
this.nextFileId++;
|
|
1311
|
-
path = `${MediaFiles.mediaDir}
|
|
1315
|
+
path = `${MediaFiles.mediaDir}/${baseFilename}${this.nextFileId}.${extension}`;
|
|
1312
1316
|
} while (this.hashes[path]);
|
|
1313
1317
|
|
|
1314
|
-
//
|
|
1315
|
-
|
|
1318
|
+
// Add media to zip
|
|
1319
|
+
this.zip.setFile(path, mediaFile);
|
|
1316
1320
|
|
|
1317
|
-
//
|
|
1321
|
+
// Add media to our lookups
|
|
1318
1322
|
this.hashes[path] = hash;
|
|
1319
1323
|
this.files.set(mediaFile, path);
|
|
1320
|
-
|
|
1321
|
-
// return
|
|
1322
1324
|
return path;
|
|
1323
1325
|
}
|
|
1324
1326
|
async count() {
|
|
@@ -1334,9 +1336,15 @@ class MediaFiles {
|
|
|
1334
1336
|
if (!filename) continue;
|
|
1335
1337
|
const fileData = await this.zip.getFile(path).getContentBase64();
|
|
1336
1338
|
const fileHash = sha1(fileData);
|
|
1337
|
-
this.hashes[
|
|
1339
|
+
this.hashes[path] = fileHash;
|
|
1338
1340
|
}
|
|
1339
1341
|
}
|
|
1342
|
+
baseFilename(mime) {
|
|
1343
|
+
// Naive heuristic.
|
|
1344
|
+
// May need to be modified if we're going to support more mime types.
|
|
1345
|
+
const parts = mime.split('/');
|
|
1346
|
+
return parts[0];
|
|
1347
|
+
}
|
|
1340
1348
|
}
|
|
1341
1349
|
|
|
1342
1350
|
/**
|
|
@@ -2603,19 +2611,17 @@ class ImagePlugin extends TemplatePlugin {
|
|
|
2603
2611
|
if (!context.pluginContext[this.contentType]) {
|
|
2604
2612
|
context.pluginContext[this.contentType] = {};
|
|
2605
2613
|
}
|
|
2606
|
-
if (!context.pluginContext[this.contentType]) {
|
|
2607
|
-
context.pluginContext[this.contentType] = {};
|
|
2608
|
-
}
|
|
2609
2614
|
const pluginContext = context.pluginContext[this.contentType];
|
|
2610
2615
|
if (!pluginContext.lastDrawingObjectId) {
|
|
2611
2616
|
pluginContext.lastDrawingObjectId = {};
|
|
2612
2617
|
}
|
|
2613
2618
|
const lastIdMap = pluginContext.lastDrawingObjectId;
|
|
2619
|
+
const lastIdKey = context.currentPart.path;
|
|
2614
2620
|
|
|
2615
2621
|
// Get next image ID if already initialized.
|
|
2616
|
-
if (lastIdMap[
|
|
2617
|
-
lastIdMap[
|
|
2618
|
-
return lastIdMap[
|
|
2622
|
+
if (lastIdMap[lastIdKey]) {
|
|
2623
|
+
lastIdMap[lastIdKey]++;
|
|
2624
|
+
return lastIdMap[lastIdKey];
|
|
2619
2625
|
}
|
|
2620
2626
|
|
|
2621
2627
|
// Init next image ID.
|
|
@@ -2632,8 +2638,8 @@ class ImagePlugin extends TemplatePlugin {
|
|
|
2632
2638
|
// Start counting from the current max
|
|
2633
2639
|
const ids = docProps.map(prop => parseInt(prop.attributes.id)).filter(isNumber);
|
|
2634
2640
|
const maxId = Math.max(...ids, 0);
|
|
2635
|
-
lastIdMap[
|
|
2636
|
-
return lastIdMap[
|
|
2641
|
+
lastIdMap[lastIdKey] = maxId + 1;
|
|
2642
|
+
return lastIdMap[lastIdKey];
|
|
2637
2643
|
}
|
|
2638
2644
|
createMarkup(imageId, relId, content) {
|
|
2639
2645
|
// http://officeopenxml.com/drwPicInline.php
|
|
@@ -4691,7 +4697,7 @@ class TemplateHandler {
|
|
|
4691
4697
|
/**
|
|
4692
4698
|
* Version number of the `easy-template-x` library.
|
|
4693
4699
|
*/
|
|
4694
|
-
version = "6.2.
|
|
4700
|
+
version = "6.2.2" ;
|
|
4695
4701
|
constructor(options) {
|
|
4696
4702
|
this.options = new TemplateHandlerOptions(options);
|
|
4697
4703
|
|
|
@@ -4836,6 +4842,7 @@ class TemplateHandler {
|
|
|
4836
4842
|
exports.Base64 = Base64;
|
|
4837
4843
|
exports.Binary = Binary;
|
|
4838
4844
|
exports.COMMENT_NODE_NAME = COMMENT_NODE_NAME;
|
|
4845
|
+
exports.ChartPlugin = ChartPlugin;
|
|
4839
4846
|
exports.DelimiterSearcher = DelimiterSearcher;
|
|
4840
4847
|
exports.Delimiters = Delimiters;
|
|
4841
4848
|
exports.Docx = Docx;
|
|
@@ -4887,6 +4894,7 @@ exports.XmlTreeIterator = XmlTreeIterator;
|
|
|
4887
4894
|
exports.XmlUtils = XmlUtils;
|
|
4888
4895
|
exports.Zip = Zip;
|
|
4889
4896
|
exports.ZipObject = ZipObject;
|
|
4897
|
+
exports.countOccurrences = countOccurrences;
|
|
4890
4898
|
exports.createDefaultPlugins = createDefaultPlugins;
|
|
4891
4899
|
exports.first = first;
|
|
4892
4900
|
exports.inheritsFrom = inheritsFrom;
|
|
@@ -402,6 +402,10 @@ function stringValue(val) {
|
|
|
402
402
|
function normalizeDoubleQuotes(text) {
|
|
403
403
|
return text.replace(nonStandardDoubleQuotesRegex, standardDoubleQuotes);
|
|
404
404
|
}
|
|
405
|
+
function countOccurrences(text, substring) {
|
|
406
|
+
// https://stackoverflow.com/questions/4009756/how-to-count-string-occurrence-in-string
|
|
407
|
+
return (text.match(new RegExp(substring, 'g')) || []).length;
|
|
408
|
+
}
|
|
405
409
|
|
|
406
410
|
class JsZipHelper {
|
|
407
411
|
static toJsZipOutputType(binaryOrType) {
|
|
@@ -1213,13 +1217,12 @@ class ContentTypesFile {
|
|
|
1213
1217
|
// Parse the content types file
|
|
1214
1218
|
await this.parseContentTypesFile();
|
|
1215
1219
|
|
|
1216
|
-
// Mime type already exists
|
|
1217
|
-
if (this.contentTypes[mime]) return;
|
|
1218
|
-
|
|
1219
1220
|
// Extension already exists
|
|
1220
|
-
//
|
|
1221
|
+
//
|
|
1222
|
+
// Multiple extensions may map to the same mime type, but a single
|
|
1223
|
+
// extension must only map to one mime type.
|
|
1221
1224
|
const extension = MimeTypeHelper.getDefaultExtension(mime);
|
|
1222
|
-
if (
|
|
1225
|
+
if (this.contentTypes[extension]) return;
|
|
1223
1226
|
|
|
1224
1227
|
// Add new node
|
|
1225
1228
|
const typeNode = xml.create.generalNode('Default');
|
|
@@ -1231,11 +1234,11 @@ class ContentTypesFile {
|
|
|
1231
1234
|
|
|
1232
1235
|
// Update state
|
|
1233
1236
|
this.addedNew = true;
|
|
1234
|
-
this.contentTypes[
|
|
1237
|
+
this.contentTypes[extension] = mime;
|
|
1235
1238
|
}
|
|
1236
|
-
async
|
|
1239
|
+
async xmlString() {
|
|
1237
1240
|
await this.parseContentTypesFile();
|
|
1238
|
-
return
|
|
1241
|
+
return xml.parser.serializeFile(this.root);
|
|
1239
1242
|
}
|
|
1240
1243
|
|
|
1241
1244
|
/**
|
|
@@ -1264,7 +1267,7 @@ class ContentTypesFile {
|
|
|
1264
1267
|
if (!contentTypeAttribute) continue;
|
|
1265
1268
|
const extensionAttribute = genNode.attributes['Extension'];
|
|
1266
1269
|
if (!extensionAttribute) continue;
|
|
1267
|
-
this.contentTypes[
|
|
1270
|
+
this.contentTypes[extensionAttribute] = contentTypeAttribute;
|
|
1268
1271
|
}
|
|
1269
1272
|
}
|
|
1270
1273
|
}
|
|
@@ -1284,39 +1287,38 @@ class MediaFiles {
|
|
|
1284
1287
|
* Returns the media file path.
|
|
1285
1288
|
*/
|
|
1286
1289
|
async add(mediaFile, mime) {
|
|
1287
|
-
//
|
|
1290
|
+
// Check if already added
|
|
1288
1291
|
if (this.files.has(mediaFile)) return this.files.get(mediaFile);
|
|
1289
1292
|
|
|
1290
|
-
//
|
|
1293
|
+
// Hash existing media files
|
|
1291
1294
|
await this.hashMediaFiles();
|
|
1292
1295
|
|
|
1293
|
-
//
|
|
1296
|
+
// Hash the new file
|
|
1294
1297
|
// Note: Even though hashing the base64 string may seem inefficient
|
|
1295
1298
|
// (requires extra step in some cases) in practice it is significantly
|
|
1296
1299
|
// faster than hashing a 'binarystring'.
|
|
1297
1300
|
const base64 = await Binary.toBase64(mediaFile);
|
|
1298
1301
|
const hash = sha1(base64);
|
|
1299
1302
|
|
|
1300
|
-
//
|
|
1301
|
-
//
|
|
1303
|
+
// Check if file already exists
|
|
1304
|
+
// Note: this can be optimized by keeping both mapping by filename as well as by hash
|
|
1302
1305
|
let path = Object.keys(this.hashes).find(p => this.hashes[p] === hash);
|
|
1303
1306
|
if (path) return path;
|
|
1304
1307
|
|
|
1305
|
-
//
|
|
1308
|
+
// Generate unique media file name
|
|
1309
|
+
const baseFilename = this.baseFilename(mime);
|
|
1306
1310
|
const extension = MimeTypeHelper.getDefaultExtension(mime);
|
|
1307
1311
|
do {
|
|
1308
1312
|
this.nextFileId++;
|
|
1309
|
-
path = `${MediaFiles.mediaDir}
|
|
1313
|
+
path = `${MediaFiles.mediaDir}/${baseFilename}${this.nextFileId}.${extension}`;
|
|
1310
1314
|
} while (this.hashes[path]);
|
|
1311
1315
|
|
|
1312
|
-
//
|
|
1313
|
-
|
|
1316
|
+
// Add media to zip
|
|
1317
|
+
this.zip.setFile(path, mediaFile);
|
|
1314
1318
|
|
|
1315
|
-
//
|
|
1319
|
+
// Add media to our lookups
|
|
1316
1320
|
this.hashes[path] = hash;
|
|
1317
1321
|
this.files.set(mediaFile, path);
|
|
1318
|
-
|
|
1319
|
-
// return
|
|
1320
1322
|
return path;
|
|
1321
1323
|
}
|
|
1322
1324
|
async count() {
|
|
@@ -1332,9 +1334,15 @@ class MediaFiles {
|
|
|
1332
1334
|
if (!filename) continue;
|
|
1333
1335
|
const fileData = await this.zip.getFile(path).getContentBase64();
|
|
1334
1336
|
const fileHash = sha1(fileData);
|
|
1335
|
-
this.hashes[
|
|
1337
|
+
this.hashes[path] = fileHash;
|
|
1336
1338
|
}
|
|
1337
1339
|
}
|
|
1340
|
+
baseFilename(mime) {
|
|
1341
|
+
// Naive heuristic.
|
|
1342
|
+
// May need to be modified if we're going to support more mime types.
|
|
1343
|
+
const parts = mime.split('/');
|
|
1344
|
+
return parts[0];
|
|
1345
|
+
}
|
|
1338
1346
|
}
|
|
1339
1347
|
|
|
1340
1348
|
/**
|
|
@@ -2601,19 +2609,17 @@ class ImagePlugin extends TemplatePlugin {
|
|
|
2601
2609
|
if (!context.pluginContext[this.contentType]) {
|
|
2602
2610
|
context.pluginContext[this.contentType] = {};
|
|
2603
2611
|
}
|
|
2604
|
-
if (!context.pluginContext[this.contentType]) {
|
|
2605
|
-
context.pluginContext[this.contentType] = {};
|
|
2606
|
-
}
|
|
2607
2612
|
const pluginContext = context.pluginContext[this.contentType];
|
|
2608
2613
|
if (!pluginContext.lastDrawingObjectId) {
|
|
2609
2614
|
pluginContext.lastDrawingObjectId = {};
|
|
2610
2615
|
}
|
|
2611
2616
|
const lastIdMap = pluginContext.lastDrawingObjectId;
|
|
2617
|
+
const lastIdKey = context.currentPart.path;
|
|
2612
2618
|
|
|
2613
2619
|
// Get next image ID if already initialized.
|
|
2614
|
-
if (lastIdMap[
|
|
2615
|
-
lastIdMap[
|
|
2616
|
-
return lastIdMap[
|
|
2620
|
+
if (lastIdMap[lastIdKey]) {
|
|
2621
|
+
lastIdMap[lastIdKey]++;
|
|
2622
|
+
return lastIdMap[lastIdKey];
|
|
2617
2623
|
}
|
|
2618
2624
|
|
|
2619
2625
|
// Init next image ID.
|
|
@@ -2630,8 +2636,8 @@ class ImagePlugin extends TemplatePlugin {
|
|
|
2630
2636
|
// Start counting from the current max
|
|
2631
2637
|
const ids = docProps.map(prop => parseInt(prop.attributes.id)).filter(isNumber);
|
|
2632
2638
|
const maxId = Math.max(...ids, 0);
|
|
2633
|
-
lastIdMap[
|
|
2634
|
-
return lastIdMap[
|
|
2639
|
+
lastIdMap[lastIdKey] = maxId + 1;
|
|
2640
|
+
return lastIdMap[lastIdKey];
|
|
2635
2641
|
}
|
|
2636
2642
|
createMarkup(imageId, relId, content) {
|
|
2637
2643
|
// http://officeopenxml.com/drwPicInline.php
|
|
@@ -4689,7 +4695,7 @@ class TemplateHandler {
|
|
|
4689
4695
|
/**
|
|
4690
4696
|
* Version number of the `easy-template-x` library.
|
|
4691
4697
|
*/
|
|
4692
|
-
version = "6.2.
|
|
4698
|
+
version = "6.2.2" ;
|
|
4693
4699
|
constructor(options) {
|
|
4694
4700
|
this.options = new TemplateHandlerOptions(options);
|
|
4695
4701
|
|
|
@@ -4831,4 +4837,4 @@ class TemplateHandler {
|
|
|
4831
4837
|
}
|
|
4832
4838
|
}
|
|
4833
4839
|
|
|
4834
|
-
export { Base64, Binary, COMMENT_NODE_NAME, DelimiterSearcher, Delimiters, Docx, ImagePlugin, InternalArgumentMissingError, InternalError, LOOP_CONTENT_TYPE, LinkPlugin, LoopPlugin, MalformedFileError, MaxXmlDepthError, MimeType, MimeTypeHelper, MissingCloseDelimiterError, MissingStartDelimiterError, OfficeMarkup, OmlAttribute, OmlNode, OpenXmlPart, Path, PluginContent, RawXmlPlugin, Regex, RelType, Relationship, ScopeData, TEXT_CONTENT_TYPE, TEXT_NODE_NAME, TagDisposition, TagOptionsParseError, TagParser, TemplateCompiler, TemplateDataError, TemplateExtension, TemplateHandler, TemplateHandlerOptions, TemplatePlugin, TemplateSyntaxError, TextPlugin, UnclosedTagError, UnidentifiedFileTypeError, UnknownContentTypeError, UnopenedTagError, UnsupportedFileTypeError, Xlsx, XmlDepthTracker, XmlNodeType, XmlTreeIterator, XmlUtils, Zip, ZipObject, createDefaultPlugins, first, inheritsFrom, isNumber, isPromiseLike, last, normalizeDoubleQuotes, officeMarkup, pushMany, sha1, stringValue, toDictionary, xml };
|
|
4840
|
+
export { Base64, Binary, COMMENT_NODE_NAME, ChartPlugin, DelimiterSearcher, Delimiters, Docx, ImagePlugin, InternalArgumentMissingError, InternalError, LOOP_CONTENT_TYPE, LinkPlugin, LoopPlugin, MalformedFileError, MaxXmlDepthError, MimeType, MimeTypeHelper, MissingCloseDelimiterError, MissingStartDelimiterError, OfficeMarkup, OmlAttribute, OmlNode, OpenXmlPart, Path, PluginContent, RawXmlPlugin, Regex, RelType, Relationship, ScopeData, TEXT_CONTENT_TYPE, TEXT_NODE_NAME, TagDisposition, TagOptionsParseError, TagParser, TemplateCompiler, TemplateDataError, TemplateExtension, TemplateHandler, TemplateHandlerOptions, TemplatePlugin, TemplateSyntaxError, TextPlugin, UnclosedTagError, UnidentifiedFileTypeError, UnknownContentTypeError, UnopenedTagError, UnsupportedFileTypeError, Xlsx, XmlDepthTracker, XmlNodeType, XmlTreeIterator, XmlUtils, Zip, ZipObject, countOccurrences, createDefaultPlugins, first, inheritsFrom, isNumber, isPromiseLike, last, normalizeDoubleQuotes, officeMarkup, pushMany, sha1, stringValue, toDictionary, xml };
|
|
@@ -8,7 +8,7 @@ export declare class ContentTypesFile {
|
|
|
8
8
|
private readonly zip;
|
|
9
9
|
constructor(zip: Zip);
|
|
10
10
|
ensureContentType(mime: MimeType): Promise<void>;
|
|
11
|
-
|
|
11
|
+
xmlString(): Promise<string>;
|
|
12
12
|
save(): Promise<void>;
|
|
13
13
|
private parseContentTypesFile;
|
|
14
14
|
}
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ export class ContentTypesFile {
|
|
|
13
13
|
|
|
14
14
|
private root: XmlNode;
|
|
15
15
|
|
|
16
|
-
private contentTypes: Partial<Record<
|
|
16
|
+
private contentTypes: Partial<Record<string, MimeType>>;
|
|
17
17
|
|
|
18
18
|
private readonly zip: Zip;
|
|
19
19
|
|
|
@@ -26,14 +26,12 @@ export class ContentTypesFile {
|
|
|
26
26
|
// Parse the content types file
|
|
27
27
|
await this.parseContentTypesFile();
|
|
28
28
|
|
|
29
|
-
// Mime type already exists
|
|
30
|
-
if (this.contentTypes[mime])
|
|
31
|
-
return;
|
|
32
|
-
|
|
33
29
|
// Extension already exists
|
|
34
|
-
//
|
|
30
|
+
//
|
|
31
|
+
// Multiple extensions may map to the same mime type, but a single
|
|
32
|
+
// extension must only map to one mime type.
|
|
35
33
|
const extension = MimeTypeHelper.getDefaultExtension(mime);
|
|
36
|
-
if (
|
|
34
|
+
if (this.contentTypes[extension])
|
|
37
35
|
return;
|
|
38
36
|
|
|
39
37
|
// Add new node
|
|
@@ -46,12 +44,12 @@ export class ContentTypesFile {
|
|
|
46
44
|
|
|
47
45
|
// Update state
|
|
48
46
|
this.addedNew = true;
|
|
49
|
-
this.contentTypes[
|
|
47
|
+
this.contentTypes[extension] = mime;
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
public async
|
|
50
|
+
public async xmlString(): Promise<string> {
|
|
53
51
|
await this.parseContentTypesFile();
|
|
54
|
-
return
|
|
52
|
+
return xml.parser.serializeFile(this.root);
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
/**
|
|
@@ -92,7 +90,7 @@ export class ContentTypesFile {
|
|
|
92
90
|
if (!extensionAttribute)
|
|
93
91
|
continue;
|
|
94
92
|
|
|
95
|
-
this.contentTypes[
|
|
93
|
+
this.contentTypes[extensionAttribute] = contentTypeAttribute;
|
|
96
94
|
}
|
|
97
95
|
}
|
|
98
96
|
}
|
package/src/office/mediaFiles.ts
CHANGED
|
@@ -25,41 +25,41 @@ export class MediaFiles {
|
|
|
25
25
|
*/
|
|
26
26
|
public async add(mediaFile: Binary, mime: MimeType): Promise<string> {
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// Check if already added
|
|
29
29
|
if (this.files.has(mediaFile))
|
|
30
30
|
return this.files.get(mediaFile);
|
|
31
31
|
|
|
32
|
-
//
|
|
32
|
+
// Hash existing media files
|
|
33
33
|
await this.hashMediaFiles();
|
|
34
34
|
|
|
35
|
-
//
|
|
35
|
+
// Hash the new file
|
|
36
36
|
// Note: Even though hashing the base64 string may seem inefficient
|
|
37
37
|
// (requires extra step in some cases) in practice it is significantly
|
|
38
38
|
// faster than hashing a 'binarystring'.
|
|
39
39
|
const base64 = await Binary.toBase64(mediaFile);
|
|
40
40
|
const hash = sha1(base64);
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
42
|
+
// Check if file already exists
|
|
43
|
+
// Note: this can be optimized by keeping both mapping by filename as well as by hash
|
|
44
44
|
let path = Object.keys(this.hashes).find(p => this.hashes[p] === hash);
|
|
45
45
|
if (path)
|
|
46
46
|
return path;
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// Generate unique media file name
|
|
49
|
+
const baseFilename = this.baseFilename(mime);
|
|
49
50
|
const extension = MimeTypeHelper.getDefaultExtension(mime);
|
|
50
51
|
do {
|
|
51
52
|
this.nextFileId++;
|
|
52
|
-
path = `${MediaFiles.mediaDir}
|
|
53
|
+
path = `${MediaFiles.mediaDir}/${baseFilename}${this.nextFileId}.${extension}`;
|
|
53
54
|
} while (this.hashes[path]);
|
|
54
55
|
|
|
55
|
-
//
|
|
56
|
-
|
|
56
|
+
// Add media to zip
|
|
57
|
+
this.zip.setFile(path, mediaFile);
|
|
57
58
|
|
|
58
|
-
//
|
|
59
|
+
// Add media to our lookups
|
|
59
60
|
this.hashes[path] = hash;
|
|
60
61
|
this.files.set(mediaFile, path);
|
|
61
62
|
|
|
62
|
-
// return
|
|
63
63
|
return path;
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -84,7 +84,14 @@ export class MediaFiles {
|
|
|
84
84
|
|
|
85
85
|
const fileData = await this.zip.getFile(path).getContentBase64();
|
|
86
86
|
const fileHash = sha1(fileData);
|
|
87
|
-
this.hashes[
|
|
87
|
+
this.hashes[path] = fileHash;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
private baseFilename(mime: MimeType): string {
|
|
92
|
+
// Naive heuristic.
|
|
93
|
+
// May need to be modified if we're going to support more mime types.
|
|
94
|
+
const parts = mime.split('/');
|
|
95
|
+
return parts[0];
|
|
96
|
+
}
|
|
90
97
|
}
|
|
@@ -48,20 +48,19 @@ export class ImagePlugin extends TemplatePlugin {
|
|
|
48
48
|
if (!context.pluginContext[this.contentType]) {
|
|
49
49
|
context.pluginContext[this.contentType] = {};
|
|
50
50
|
}
|
|
51
|
-
if (!context.pluginContext[this.contentType]) {
|
|
52
|
-
context.pluginContext[this.contentType] = {};
|
|
53
|
-
}
|
|
54
51
|
|
|
55
52
|
const pluginContext: ImagePluginContext = context.pluginContext[this.contentType];
|
|
56
53
|
if (!pluginContext.lastDrawingObjectId) {
|
|
57
54
|
pluginContext.lastDrawingObjectId = {};
|
|
58
55
|
}
|
|
56
|
+
|
|
59
57
|
const lastIdMap = pluginContext.lastDrawingObjectId;
|
|
58
|
+
const lastIdKey = context.currentPart.path;
|
|
60
59
|
|
|
61
60
|
// Get next image ID if already initialized.
|
|
62
|
-
if (lastIdMap[
|
|
63
|
-
lastIdMap[
|
|
64
|
-
return lastIdMap[
|
|
61
|
+
if (lastIdMap[lastIdKey]) {
|
|
62
|
+
lastIdMap[lastIdKey]++;
|
|
63
|
+
return lastIdMap[lastIdKey];
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
// Init next image ID.
|
|
@@ -79,8 +78,8 @@ export class ImagePlugin extends TemplatePlugin {
|
|
|
79
78
|
const ids = docProps.map(prop => parseInt(prop.attributes.id)).filter(isNumber);
|
|
80
79
|
const maxId = Math.max(...ids, 0);
|
|
81
80
|
|
|
82
|
-
lastIdMap[
|
|
83
|
-
return lastIdMap[
|
|
81
|
+
lastIdMap[lastIdKey] = maxId + 1;
|
|
82
|
+
return lastIdMap[lastIdKey];
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
private createMarkup(imageId: number, relId: string, content: ImageContent): XmlNode {
|
package/src/plugins/index.ts
CHANGED
package/src/utils/txt.ts
CHANGED
|
@@ -32,3 +32,8 @@ export function stringValue(val: unknown): string {
|
|
|
32
32
|
export function normalizeDoubleQuotes(text: string): string {
|
|
33
33
|
return text.replace(nonStandardDoubleQuotesRegex, standardDoubleQuotes);
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
export function countOccurrences(text: string, substring: string): number {
|
|
37
|
+
// https://stackoverflow.com/questions/4009756/how-to-count-string-occurrence-in-string
|
|
38
|
+
return (text.match(new RegExp(substring, 'g')) || []).length;
|
|
39
|
+
}
|