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: {
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: {
type: PatchType.DOCUMENT,
@ -47,7 +57,6 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
children: [
new TextRun({
text: "BBC News Link",
style: "Hyperlink",
}),
],
link: "https://www.bbc.co.uk/news",

View File

@ -1,13 +1,15 @@
import * as JSZip from "jszip";
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 { IMediaData, Media } from "@file/media";
import { IViewWrapper } from "@file/document-wrapper";
import { File } from "@file/file";
import { IContext } from "@file/xml-components";
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 { findLocationOfText } from "./traverser";
@ -33,11 +35,16 @@ type FilePatch = {
readonly children: readonly FileChild[];
};
interface IRelationshipReplacement {
interface IImageRelationshipAddition {
readonly key: string;
readonly mediaDatas: readonly IMediaData[];
}
interface IHyperlinkRelationshipAddition {
readonly key: string;
readonly hyperlink: { readonly id: string; readonly link: string };
}
export type IPatch = ParagraphPatch | FilePatch;
export interface PatchDocumentOptions {
@ -60,7 +67,9 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
const map = new Map<string, Element>();
// 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;
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 renderedParagraphs = findLocationOfText(json, patchText);
// 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);
if (mediaDatas.length > 0) {
hasMedia = true;
// eslint-disable-next-line functional/immutable-data
relationshipReplacement.push({
imageRelationshipAdditions.push({
key,
mediaDatas,
});
@ -87,11 +121,20 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
map.set(key, json);
}
for (const { key, mediaDatas } of relationshipReplacement) {
for (const { key, mediaDatas } of imageRelationshipAdditions) {
// 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 (!map.has(relationshipKey)) {
map.set(relationshipKey, createRelationshipFile());
}
const relationshipsJson = map.get(relationshipKey);
if (!relationshipsJson) {
throw new Error("Could not find relationships file");
}
if (relationshipsJson) {
const index = getNextRelationshipIndex(relationshipsJson);
const newJson = imageReplacer.replace(JSON.stringify(map.get(key)), mediaDatas, index);
map.set(key, JSON.parse(newJson) as Element);
@ -105,6 +148,28 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
);
}
}
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) {
@ -144,3 +209,23 @@ const toXml = (jsonObj: Element): string => {
const output = js2xml(jsonObj);
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 { RelationshipType } from "@file/relationships/relationship/relationship";
import { RelationshipType, TargetModeType } from "@file/relationships/relationship/relationship";
import { getFirstLevelElements } from "./util";
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");
// eslint-disable-next-line functional/immutable-data
relationshipElements.push({
@ -23,6 +29,7 @@ export const appendRelationship = (relationships: Element, id: number, type: Rel
Id: `rId${id}`,
Type: type,
Target: target,
TargetMode: targetMode,
},
name: "Relationship",
type: "element",