From f894fb24e2010cb323dbbe6c925d167d883086fe Mon Sep 17 00:00:00 2001 From: Malcolm Date: Fri, 4 Apr 2025 10:06:41 -0600 Subject: [PATCH 1/3] HDD-1725 RTF links --- lib/elements/link.ts | 89 ++++++++++++++++++++++++++++++++++++++++++++ lib/fonts.ts | 3 +- lib/index.ts | 2 + lib/rgb.ts | 3 ++ lib/rtf-utils.ts | 5 +++ lib/rtf.ts | 12 +++++- package-lock.json | 4 +- package.json | 2 +- 8 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 lib/elements/link.ts diff --git a/lib/elements/link.ts b/lib/elements/link.ts new file mode 100644 index 0000000..5e2952b --- /dev/null +++ b/lib/elements/link.ts @@ -0,0 +1,89 @@ +import { Format } from "../format"; +import { RGB } from "../rgb"; +import { Element } from "./element"; +import { getRTFSafeText } from "../rtf-utils"; + +export class LinkElement extends Element { + url: string; + displayText: string; + isExternalURL: boolean; // Flag to indicate if the URL is external + constructor(url: string, displayText: string, format?: Format, isExternalURL: boolean = false) { + // If no format provided, create a default one for links + const linkFormat = format ?? new Format(); + + // Ensure typical link styling if not overridden + if (!linkFormat.color) { + linkFormat.color = RGB.BLUE; // Default to blue + } + if (!linkFormat.underline) { + linkFormat.underline = true; // Default to underline + } + + super(linkFormat); + this.url = url; + this.displayText = displayText; + this.isExternalURL = isExternalURL; // Store the external URL flag + } + + getRTFCode( + colorTable: Array, // Use the RGB type + fontTable: string[], + callback: (err: Error | null, result?: string) => void + ): void { + try { + // Ensure format properties (color, font) are registered in tables + this.format.updateTables(colorTable, fontTable); + + // Prepare format codes (font, size, color, underline, etc.) + let formatCodes = ""; + if (this.format.fontPos >= 0) { + formatCodes += `\\f${this.format.fontPos}`; + } + if (this.format.fontSize > 0) { + formatCodes += `\\fs${this.format.fontSize * 2}`; + } + // Always add color for the link display text + if (this.format.colorPos >= 0) { + // Add 1 because color table is 1-indexed in RTF (\cf1, \cf2, ...) + formatCodes += `\\cf${this.format.colorPos + 1}`; + } + // Always add underline for the link display text + if (this.format.underline) { + formatCodes += "\\ul"; + } + if (this.format.bold) { + formatCodes += "\\b"; + } + if (this.format.italic) { + formatCodes += "\\i"; + } + // Add other format codes as needed (strike, super, sub) + + // Escape the URL and display text for safety within RTF structure + // Note: URLs themselves might have issues if they contain complex chars, + // but basic escaping handles common cases like spaces or backslashes. + const safeUrl = getRTFSafeText(this.url); + const safeDisplayText = getRTFSafeText(this.displayText); + + // Construct the RTF string for the hyperlink field + // Structure: {\pard \[format codes] {\field{\*\fldinst HYPERLINK "URL"}{\fldrslt [DISPLAY_TEXT]}}}[\format resets \pard] + // The outer {} and \pard ensures the formatting is scoped to the link. + // We apply format codes *before* the \field block. + // \fldrslt block inherits the preceding format. + let rtfString = `{\\pard ${formatCodes} ` // Start group, apply formatting + + if (this.isExternalURL) { + // If it's an external URL, we need to add the external link format + rtfString += `{\\field{\\*\\fldinst HYPERLINK "${safeUrl}"}}{\\fldrslt ${safeDisplayText}}`; + } else { + rtfString += `{\\field{\\*\\fldinst HYPERLINK A}{\\fldrslt ${safeUrl}}}` // The field itself + } + + rtfString += `\\ul0\\b0\\i0}`; // Reset specific styles and end group. \\pard might be needed depending on context. + + callback(null, rtfString); + } catch (err) { + callback(err instanceof Error ? err : new Error(String(err))); + } + } +} \ No newline at end of file diff --git a/lib/fonts.ts b/lib/fonts.ts index f6f42d6..6e13cc6 100644 --- a/lib/fonts.ts +++ b/lib/fonts.ts @@ -8,5 +8,6 @@ export const Fonts = { VERDANA: "Verdana", COURIER_NEW: "Courier New", PALATINO: "Palatino Linotype", - TIMES_NEW_ROMAN: "Times New Roman" + TIMES_NEW_ROMAN: "Times New Roman", + CALIBARI: "Calibri", } as const; diff --git a/lib/index.ts b/lib/index.ts index 8c6c8a0..a78cb83 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -17,3 +17,5 @@ export * from './elements/group'; export * from './elements/image'; export * from './elements/table'; export * from './elements/text'; +export * from './elements/link'; +export * from './elements/command'; diff --git a/lib/rgb.ts b/lib/rgb.ts index f802246..a479b25 100644 --- a/lib/rgb.ts +++ b/lib/rgb.ts @@ -8,4 +8,7 @@ export class RGB { this.green = green; this.blue = blue; } + + // Add a static constant for convenience + static readonly BLUE = new RGB(0, 0, 255); } diff --git a/lib/rtf-utils.ts b/lib/rtf-utils.ts index d9de5c8..2538a0f 100644 --- a/lib/rtf-utils.ts +++ b/lib/rtf-utils.ts @@ -10,6 +10,11 @@ export function getRTFSafeText(text: any): string { if (typeof text === "object" && text.hasOwnProperty("safe") && !text.safe) { return text.text; } + + if (typeof text !== 'string') { + return ''; + } + return text.replaceAll('\\', '\\\\') .replaceAll('{', '\\{') .replaceAll('}', '\\}') diff --git a/lib/rtf.ts b/lib/rtf.ts index 2afe018..0454f6b 100644 --- a/lib/rtf.ts +++ b/lib/rtf.ts @@ -1,4 +1,3 @@ -import { RGB } from "./rgb"; import { Element } from "./elements/element"; // adjust the types as needed import { Format } from "./format"; import * as Utils from "./rtf-utils"; @@ -8,6 +7,7 @@ import { TextElement } from "./elements/text"; import { GroupElement } from "./elements/group"; import async from "async"; import { CommandElement } from "./elements/command"; +import { LinkElement } from "./elements/link"; export class RTF { pageNumbering: boolean; @@ -52,6 +52,16 @@ export class RTF { } } + writeLink(url: string, displayText: string, format?: Format, groupName?: string) { + const element = new LinkElement(url, displayText, format); + const groupIndex = this._groupIndex(groupName); + if (groupName !== undefined && groupIndex >= 0) { + (this.elements[groupIndex] as GroupElement).addElement(element); + } else { + this.elements.push(element); + } + } + addTable(table: any) { this.elements.push(table); } diff --git a/package-lock.json b/package-lock.json index 43bc469..db610dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@happy-doc/rtf", - "version": "0.1.2", + "version": "0.1.3-beta-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@happy-doc/rtf", - "version": "0.1.2", + "version": "0.1.3-beta-1", "license": "MIT", "dependencies": { "async": "^0.9.2", diff --git a/package.json b/package.json index fb4320d..752de08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@happy-doc/rtf", - "version": "0.1.2", + "version": "0.1.3", "description": "Assists with creating rich text documents.", "main": "dist/index.js", "types": "dist/index.d.ts", From ecc2dd67e0d577ef75bd22d823868ca303263ef1 Mon Sep 17 00:00:00 2001 From: Malcolm Date: Fri, 4 Apr 2025 11:36:08 -0600 Subject: [PATCH 2/3] hell yeah --- lib/elements/link.ts | 3 +++ lib/rtf.ts | 4 ++-- package-lock.json | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/elements/link.ts b/lib/elements/link.ts index 5e2952b..a6c2754 100644 --- a/lib/elements/link.ts +++ b/lib/elements/link.ts @@ -81,6 +81,9 @@ export class LinkElement extends Element { rtfString += `\\ul0\\b0\\i0}`; // Reset specific styles and end group. \\pard might be needed depending on context. + // Add the closing braces for the RTF structure (if needed) + //rtfString += `\\pard}`; // End the paragraph and reset formatting + callback(null, rtfString); } catch (err) { callback(err instanceof Error ? err : new Error(String(err))); diff --git a/lib/rtf.ts b/lib/rtf.ts index 0454f6b..135ac9f 100644 --- a/lib/rtf.ts +++ b/lib/rtf.ts @@ -52,8 +52,8 @@ export class RTF { } } - writeLink(url: string, displayText: string, format?: Format, groupName?: string) { - const element = new LinkElement(url, displayText, format); + writeLink(url: string, displayText: string, format?: Format, groupName?: string, isExternalURL?: boolean) { + const element = new LinkElement(url, displayText, format, isExternalURL); const groupIndex = this._groupIndex(groupName); if (groupName !== undefined && groupIndex >= 0) { (this.elements[groupIndex] as GroupElement).addElement(element); diff --git a/package-lock.json b/package-lock.json index db610dc..fe6c0cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@happy-doc/rtf", - "version": "0.1.3-beta-1", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@happy-doc/rtf", - "version": "0.1.3-beta-1", + "version": "0.1.3", "license": "MIT", "dependencies": { "async": "^0.9.2", From 6c5c74c57198e34f1ab60f393304a2b7233943ce Mon Sep 17 00:00:00 2001 From: Malcolm Date: Mon, 7 Apr 2025 11:55:20 -0400 Subject: [PATCH 3/3] typo fix --- lib/fonts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/fonts.ts b/lib/fonts.ts index 6e13cc6..b411ef3 100644 --- a/lib/fonts.ts +++ b/lib/fonts.ts @@ -1,6 +1,7 @@ export const Fonts = { ARIAL: "Arial", - COMIC_SANS: "Comic Sans MS", + CALIBRI: "Calibri", + COMIC_SANS: "Comic Sans MS", GEORGIA: "Georgia", IMPACT: "Impact", TAHOMA: "Tahoma", @@ -9,5 +10,4 @@ export const Fonts = { COURIER_NEW: "Courier New", PALATINO: "Palatino Linotype", TIMES_NEW_ROMAN: "Times New Roman", - CALIBARI: "Calibri", } as const;