Allow patching of ExternalHyperlinks

This commit is contained in:
Dolan Miu
2023-03-08 23:30:51 +00:00
parent 6ad18420e5
commit 0fba450c9a
3 changed files with 125 additions and 24 deletions

View File

@ -28,7 +28,17 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
}, },
item_1: { item_1: {
type: PatchType.PARAGRAPH, type: PatchType.PARAGRAPH,
children: [new TextRun("#657")], children: [
new TextRun("#657"),
new ExternalHyperlink({
children: [
new TextRun({
text: "BBC News Link",
}),
],
link: "https://www.bbc.co.uk/news",
}),
],
}, },
paragraph_replace: { paragraph_replace: {
type: PatchType.DOCUMENT, type: PatchType.DOCUMENT,
@ -47,7 +57,6 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
children: [ children: [
new TextRun({ new TextRun({
text: "BBC News Link", text: "BBC News Link",
style: "Hyperlink",
}), }),
], ],
link: "https://www.bbc.co.uk/news", link: "https://www.bbc.co.uk/news",

View File

@ -1,13 +1,15 @@
import * as JSZip from "jszip"; import * as JSZip from "jszip";
import { Element, js2xml } from "xml-js"; import { Element, js2xml } from "xml-js";
import { ParagraphChild } from "@file/paragraph"; import { ConcreteHyperlink, ExternalHyperlink, ParagraphChild } from "@file/paragraph";
import { FileChild } from "@file/file-child"; import { FileChild } from "@file/file-child";
import { IMediaData, Media } from "@file/media"; import { IMediaData, Media } from "@file/media";
import { IViewWrapper } from "@file/document-wrapper"; import { IViewWrapper } from "@file/document-wrapper";
import { File } from "@file/file"; import { File } from "@file/file";
import { IContext } from "@file/xml-components"; import { IContext } from "@file/xml-components";
import { ImageReplacer } from "@export/packer/image-replacer"; import { ImageReplacer } from "@export/packer/image-replacer";
import { TargetModeType } from "@file/relationships/relationship/relationship";
import { uniqueId } from "@util/convenience-functions";
import { replacer } from "./replacer"; import { replacer } from "./replacer";
import { findLocationOfText } from "./traverser"; import { findLocationOfText } from "./traverser";
@ -33,11 +35,16 @@ type FilePatch = {
readonly children: readonly FileChild[]; readonly children: readonly FileChild[];
}; };
interface IRelationshipReplacement { interface IImageRelationshipAddition {
readonly key: string; readonly key: string;
readonly mediaDatas: readonly IMediaData[]; readonly mediaDatas: readonly IMediaData[];
} }
interface IHyperlinkRelationshipAddition {
readonly key: string;
readonly hyperlink: { readonly id: string; readonly link: string };
}
export type IPatch = ParagraphPatch | FilePatch; export type IPatch = ParagraphPatch | FilePatch;
export interface PatchDocumentOptions { export interface PatchDocumentOptions {
@ -60,7 +67,9 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
const map = new Map<string, Element>(); const map = new Map<string, Element>();
// eslint-disable-next-line functional/prefer-readonly-type // eslint-disable-next-line functional/prefer-readonly-type
const relationshipReplacement: IRelationshipReplacement[] = []; const imageRelationshipAdditions: IImageRelationshipAddition[] = [];
// eslint-disable-next-line functional/prefer-readonly-type
const hyperlinkRelationshipAdditions: IHyperlinkRelationshipAddition[] = [];
let hasMedia = false; let hasMedia = false;
for (const [key, value] of Object.entries(zipContent.files)) { for (const [key, value] of Object.entries(zipContent.files)) {
@ -70,14 +79,39 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
const patchText = `{{${patchKey}}}`; const patchText = `{{${patchKey}}}`;
const renderedParagraphs = findLocationOfText(json, patchText); const renderedParagraphs = findLocationOfText(json, patchText);
// TODO: mutates json. Make it immutable // TODO: mutates json. Make it immutable
replacer(json, patchValue, patchText, renderedParagraphs, context); replacer(
json,
{
...patchValue,
children: patchValue.children.map((element) => {
// We need to replace external hyperlinks with concrete hyperlinks
if (element instanceof ExternalHyperlink) {
const concreteHyperlink = new ConcreteHyperlink(element.options.children, uniqueId());
// eslint-disable-next-line functional/immutable-data
hyperlinkRelationshipAdditions.push({
key,
hyperlink: {
id: concreteHyperlink.linkId,
link: element.options.link,
},
});
return concreteHyperlink;
} else {
return element;
}
}),
},
patchText,
renderedParagraphs,
context,
);
} }
const mediaDatas = imageReplacer.getMediaData(JSON.stringify(json), context.file.Media); const mediaDatas = imageReplacer.getMediaData(JSON.stringify(json), context.file.Media);
if (mediaDatas.length > 0) { if (mediaDatas.length > 0) {
hasMedia = true; hasMedia = true;
// eslint-disable-next-line functional/immutable-data // eslint-disable-next-line functional/immutable-data
relationshipReplacement.push({ imageRelationshipAdditions.push({
key, key,
mediaDatas, mediaDatas,
}); });
@ -87,24 +121,55 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
map.set(key, json); map.set(key, json);
} }
for (const { key, mediaDatas } of relationshipReplacement) { for (const { key, mediaDatas } of imageRelationshipAdditions) {
// eslint-disable-next-line functional/immutable-data // eslint-disable-next-line functional/immutable-data
const relationshipsJson = map.get(`word/_rels/${key.split("/").pop()}.rels`); const relationshipKey = `word/_rels/${key.split("/").pop()}.rels`;
if (relationshipsJson) { if (!map.has(relationshipKey)) {
const index = getNextRelationshipIndex(relationshipsJson); map.set(relationshipKey, createRelationshipFile());
const newJson = imageReplacer.replace(JSON.stringify(map.get(key)), mediaDatas, index);
map.set(key, JSON.parse(newJson) as Element);
for (const { fileName } of mediaDatas) {
appendRelationship(
relationshipsJson,
index,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${fileName}`,
);
}
} }
const relationshipsJson = map.get(relationshipKey);
if (!relationshipsJson) {
throw new Error("Could not find relationships file");
}
const index = getNextRelationshipIndex(relationshipsJson);
const newJson = imageReplacer.replace(JSON.stringify(map.get(key)), mediaDatas, index);
map.set(key, JSON.parse(newJson) as Element);
for (const { fileName } of mediaDatas) {
appendRelationship(
relationshipsJson,
index,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${fileName}`,
);
}
}
for (const { key, hyperlink } of hyperlinkRelationshipAdditions) {
// eslint-disable-next-line functional/immutable-data
const relationshipKey = `word/_rels/${key.split("/").pop()}.rels`;
if (!map.has(relationshipKey)) {
map.set(relationshipKey, createRelationshipFile());
}
const relationshipsJson = map.get(relationshipKey);
if (!relationshipsJson) {
throw new Error("Could not find relationships file");
}
appendRelationship(
relationshipsJson,
hyperlink.id,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
hyperlink.link,
TargetModeType.EXTERNAL,
);
} }
if (hasMedia) { if (hasMedia) {
@ -144,3 +209,23 @@ const toXml = (jsonObj: Element): string => {
const output = js2xml(jsonObj); const output = js2xml(jsonObj);
return output; return output;
}; };
const createRelationshipFile = (): Element => ({
declaration: {
attributes: {
version: "1.0",
encoding: "UTF-8",
standalone: "yes",
},
},
elements: [
{
type: "element",
name: "Relationships",
attributes: {
xmlns: "http://schemas.openxmlformats.org/package/2006/relationships",
},
elements: [],
},
],
});

View File

@ -1,6 +1,6 @@
import { Element } from "xml-js"; import { Element } from "xml-js";
import { RelationshipType } from "@file/relationships/relationship/relationship"; import { RelationshipType, TargetModeType } from "@file/relationships/relationship/relationship";
import { getFirstLevelElements } from "./util"; import { getFirstLevelElements } from "./util";
const getIdFromRelationshipId = (relationshipId: string): number => parseInt(relationshipId.substring(3), 10); const getIdFromRelationshipId = (relationshipId: string): number => parseInt(relationshipId.substring(3), 10);
@ -15,7 +15,13 @@ export const getNextRelationshipIndex = (relationships: Element): number => {
); );
}; };
export const appendRelationship = (relationships: Element, id: number, type: RelationshipType, target: string): void => { export const appendRelationship = (
relationships: Element,
id: number | string,
type: RelationshipType,
target: string,
targetMode?: TargetModeType,
): void => {
const relationshipElements = getFirstLevelElements(relationships, "Relationships"); const relationshipElements = getFirstLevelElements(relationships, "Relationships");
// eslint-disable-next-line functional/immutable-data // eslint-disable-next-line functional/immutable-data
relationshipElements.push({ relationshipElements.push({
@ -23,6 +29,7 @@ export const appendRelationship = (relationships: Element, id: number, type: Rel
Id: `rId${id}`, Id: `rId${id}`,
Type: type, Type: type,
Target: target, Target: target,
TargetMode: targetMode,
}, },
name: "Relationship", name: "Relationship",
type: "element", type: "element",