From 6ce63910af9527abc685f4cc89c07a3c9c5d864c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Jasikowski?= Date: Wed, 25 Sep 2024 14:03:29 +0200 Subject: [PATCH 1/6] Added attribute caching to img --- __tests__/ExpensiMark-HTML-test.js | 2 +- __tests__/ExpensiMark-Markdown-test.js | 18 ++++++++-- lib/ExpensiMark.ts | 48 +++++++++++++++++++------- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/__tests__/ExpensiMark-HTML-test.js b/__tests__/ExpensiMark-HTML-test.js index 1417cc5a..488088d4 100644 --- a/__tests__/ExpensiMark-HTML-test.js +++ b/__tests__/ExpensiMark-HTML-test.js @@ -2102,7 +2102,7 @@ describe('Video markdown conversion to html tag', () => { const resultString = ''; expect(parser.replace(testString, { extras: { - videoAttributeCache: { + mediaAttributeCache: { 'https://example.com/video.mp4': 'data-expensify-height="100" data-expensify-width="100"' } } diff --git a/__tests__/ExpensiMark-Markdown-test.js b/__tests__/ExpensiMark-Markdown-test.js index 1ebfd21a..9199ca7d 100644 --- a/__tests__/ExpensiMark-Markdown-test.js +++ b/__tests__/ExpensiMark-Markdown-test.js @@ -867,6 +867,18 @@ describe('Image tag conversion to markdown', () => { const resultString = '![```code```](https://example.com/image.png)'; expect(parser.htmlToMarkdown(testString)).toBe(resultString); }); + + test('Cache extra attributes for img', () => { + const cacheMediaAttributes = jest.fn(); + const testString = 'altText'; + const resultString = '![altText](https://example.com/image.png)'; + const extras = { + cacheMediaAttributes, + }; + expect(parser.htmlToMarkdown(testString, extras)).toBe(resultString); + expect(cacheMediaAttributes).toHaveBeenCalledWith("https://example.com/image.png", 'data-expensify-width="100" data-expensify-height="500" data-name="newName" data-expensify-source="expensify-source"') + }); + }); describe('Video tag conversion to markdown', () => { @@ -883,14 +895,14 @@ describe('Video tag conversion to markdown', () => { }) test('While convert video, cache some extra attributes from the video tag', () => { - const cacheVideoAttributes = jest.fn(); + const cacheMediaAttributes = jest.fn(); const testString = ''; const resultString = '![video](https://example.com/video.mp4)'; const extras = { - cacheVideoAttributes, + cacheMediaAttributes, }; expect(parser.htmlToMarkdown(testString, extras)).toBe(resultString); - expect(cacheVideoAttributes).toHaveBeenCalledWith("https://example.com/video.mp4", ' data-expensify-width="100" data-expensify-height="500" data-expensify-thumbnail-url="https://image.com/img.jpg"') + expect(cacheMediaAttributes).toHaveBeenCalledWith("https://example.com/video.mp4", ' data-expensify-width="100" data-expensify-height="500" data-expensify-thumbnail-url="https://image.com/img.jpg"') }) }) diff --git a/lib/ExpensiMark.ts b/lib/ExpensiMark.ts index 0a17a763..b125b578 100644 --- a/lib/ExpensiMark.ts +++ b/lib/ExpensiMark.ts @@ -9,8 +9,8 @@ import * as Utils from './utils'; type Extras = { reportIDToName?: Record; accountIDToName?: Record; - cacheVideoAttributes?: (vidSource: string, attrs: string) => void; - videoAttributeCache?: Record; + cacheMediaAttributes?: (mediaSource: string, attrs: string) => void; + mediaAttributeCache?: Record; }; const EXTRAS_DEFAULT = {}; @@ -171,11 +171,11 @@ export default class ExpensiMark { * @return Returns the HTML video tag */ replacement: (extras, _match, videoName, videoSource) => { - const extraAttrs = extras && extras.videoAttributeCache && extras.videoAttributeCache[videoSource]; + const extraAttrs = extras && extras.mediaAttributeCache && extras.mediaAttributeCache[videoSource]; return ``; }, rawInputReplacement: (extras, _match, videoName, videoSource) => { - const extraAttrs = extras && extras.videoAttributeCache && extras.videoAttributeCache[videoSource]; + const extraAttrs = extras && extras.mediaAttributeCache && extras.mediaAttributeCache[videoSource]; return ``; }, }, @@ -658,13 +658,37 @@ export default class ExpensiMark { { name: 'image', - regex: /<]*src\s*=\s*(['"])(.*?)\1(?:[^><]*alt\s*=\s*(['"])(.*?)\3)?[^><]*>*(?![^<][\s\S]*?(<\/pre>|<\/code>))/gi, - replacement: (_extras, _match, _g1, g2, _g3, g4) => { - if (g4) { - return `![${g4}](${g2})`; + regex: /<]*src\s*=\s*(['"])(.*?)\1(.*?)>(?![^<][\s\S]*?(<\/pre>|<\/code>))/gi, + /** + * @param extras - The extras object + * @param match - The full match + * @param _g1 - The first capture group (the quote) + * @param imgSource - The second capture group - src attribute value + * @param imgAttrs - The third capture group - any attributes after src + * @returns The markdown image tag + */ + replacement: (extras, _match, _g1, imgSource, imgAttrs) => { + // Extract alt attribute from imgAttrs + let altText = ''; + const altRegex = /alt\s*=\s*(['"])(.*?)\1/i; + const altMatch = imgAttrs.match(altRegex); + let attributes = ''; + if (altMatch) { + altText = altMatch[2]; + // Remove the alt attribute from imgAttrs + attributes = imgAttrs.replace(altRegex, ''); } - - return `!(${g2})`; + // Remove trailing slash and extra whitespace + attributes = attributes.replace(/\s*\/\s*$/, '').trim(); + // Cache attributes without alt and trailing slash + if (imgAttrs && extras && extras.cacheMediaAttributes && typeof extras.cacheMediaAttributes === 'function') { + extras.cacheMediaAttributes(imgSource, attributes); + } + // Return the markdown image tag + if (altText) { + return `![${altText}](${imgSource})`; + } + return `!(${imgSource})`; }, }, @@ -681,8 +705,8 @@ export default class ExpensiMark { * @returns The markdown video tag */ replacement: (extras, _match, _g1, videoSource, videoAttrs, videoName) => { - if (videoAttrs && extras && extras.cacheVideoAttributes && typeof extras.cacheVideoAttributes === 'function') { - extras.cacheVideoAttributes(videoSource, videoAttrs); + if (videoAttrs && extras && extras.cacheMediaAttributes && typeof extras.cacheMediaAttributes === 'function') { + extras.cacheMediaAttributes(videoSource, videoAttrs); } if (videoName) { return `![${videoName}](${videoSource})`; From 210bad35bfb6525c4705f02252d3f0035839878c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Jasikowski?= Date: Wed, 25 Sep 2024 14:36:59 +0200 Subject: [PATCH 2/6] Md to HTML for images with cached attributes --- __tests__/ExpensiMark-HTML-test.js | 12 ++++++++++++ lib/ExpensiMark.ts | 12 +++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/__tests__/ExpensiMark-HTML-test.js b/__tests__/ExpensiMark-HTML-test.js index 488088d4..32ca9e03 100644 --- a/__tests__/ExpensiMark-HTML-test.js +++ b/__tests__/ExpensiMark-HTML-test.js @@ -2220,6 +2220,18 @@ describe('Image markdown conversion to html tag', () => { '```code```'; expect(parser.replace(testString, {shouldKeepRawInput: true})).toBe(resultString); }); + + test('Single image with extra cached attribues', () => { + const testString = '![test](https://example.com/image.jpg)'; + const resultString = 'test'; + expect(parser.replace(testString, { + extras: { + mediaAttributeCache: { + 'https://example.com/image.jpg': 'data-expensify-height="100" data-expensify-width="100"' + } + } + })).toBe(resultString); + }) }); describe('room mentions', () => { diff --git a/lib/ExpensiMark.ts b/lib/ExpensiMark.ts index b125b578..b6942ba9 100644 --- a/lib/ExpensiMark.ts +++ b/lib/ExpensiMark.ts @@ -249,9 +249,14 @@ export default class ExpensiMark { { name: 'image', regex: MARKDOWN_IMAGE_REGEX, - replacement: (_extras, _match, g1, g2) => `${this.escapeAttributeContent(g1)}`, - rawInputReplacement: (_extras, _match, g1, g2) => - `${this.escapeAttributeContent(g1)}`, + replacement: (extras, _match, imgAlt, imgSource) => { + const extraAttrs = extras && extras.mediaAttributeCache && extras.mediaAttributeCache[imgSource]; + return `${this.escapeAttributeContent(imgAlt)}`; + }, + rawInputReplacement: (extras, _match, imgAlt, imgSource) => { + const extraAttrs = extras && extras.mediaAttributeCache && extras.mediaAttributeCache[imgSource]; + return `${this.escapeAttributeContent(imgAlt)}`; + }, }, /** @@ -891,6 +896,7 @@ export default class ExpensiMark { if (rule.pre) { replacedText = rule.pre(replacedText); } + const replacement = shouldKeepRawInput && rule.rawInputReplacement ? rule.rawInputReplacement : rule.replacement; if ('process' in rule) { replacedText = rule.process(replacedText, replacement, shouldKeepRawInput); From ab72e49a0e4a1920fa979db28020b96a2b2aed24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Jasikowski?= Date: Thu, 26 Sep 2024 09:36:37 +0200 Subject: [PATCH 3/6] Fixed img alt handling in ExpensiMark.ts, Renamed cacheMediaAttributes to mediaAttributeCachingFn for clarity --- __tests__/ExpensiMark-HTML-test.js | 2 +- __tests__/ExpensiMark-Markdown-test.js | 25 ++++++++++++++++++------- lib/ExpensiMark.ts | 14 +++++++------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/__tests__/ExpensiMark-HTML-test.js b/__tests__/ExpensiMark-HTML-test.js index 32ca9e03..8ae98224 100644 --- a/__tests__/ExpensiMark-HTML-test.js +++ b/__tests__/ExpensiMark-HTML-test.js @@ -2221,7 +2221,7 @@ describe('Image markdown conversion to html tag', () => { expect(parser.replace(testString, {shouldKeepRawInput: true})).toBe(resultString); }); - test('Single image with extra cached attribues', () => { + test('Single image with extra cached attributes', () => { const testString = '![test](https://example.com/image.jpg)'; const resultString = 'test'; expect(parser.replace(testString, { diff --git a/__tests__/ExpensiMark-Markdown-test.js b/__tests__/ExpensiMark-Markdown-test.js index 9199ca7d..143dab9d 100644 --- a/__tests__/ExpensiMark-Markdown-test.js +++ b/__tests__/ExpensiMark-Markdown-test.js @@ -868,15 +868,26 @@ describe('Image tag conversion to markdown', () => { expect(parser.htmlToMarkdown(testString)).toBe(resultString); }); - test('Cache extra attributes for img', () => { - const cacheMediaAttributes = jest.fn(); + test('Cache extra attributes for img with alt', () => { + const mediaAttributeCachingFn = jest.fn(); const testString = 'altText'; const resultString = '![altText](https://example.com/image.png)'; const extras = { - cacheMediaAttributes, + mediaAttributeCachingFn, }; expect(parser.htmlToMarkdown(testString, extras)).toBe(resultString); - expect(cacheMediaAttributes).toHaveBeenCalledWith("https://example.com/image.png", 'data-expensify-width="100" data-expensify-height="500" data-name="newName" data-expensify-source="expensify-source"') + expect(mediaAttributeCachingFn).toHaveBeenCalledWith("https://example.com/image.png", 'data-expensify-width="100" data-expensify-height="500" data-name="newName" data-expensify-source="expensify-source"') + }); + + test('Cache extra attributes for img without alt', () => { + const mediaAttributeCachingFn = jest.fn(); + const testString = ''; + const resultString = '!(https://example.com/image.png)'; + const extras = { + mediaAttributeCachingFn, + }; + expect(parser.htmlToMarkdown(testString, extras)).toBe(resultString); + expect(mediaAttributeCachingFn).toHaveBeenCalledWith("https://example.com/image.png", 'data-expensify-width="100" data-expensify-height="500" data-name="newName" data-expensify-source="expensify-source"') }); }); @@ -895,14 +906,14 @@ describe('Video tag conversion to markdown', () => { }) test('While convert video, cache some extra attributes from the video tag', () => { - const cacheMediaAttributes = jest.fn(); + const mediaAttributeCachingFn = jest.fn(); const testString = ''; const resultString = '![video](https://example.com/video.mp4)'; const extras = { - cacheMediaAttributes, + mediaAttributeCachingFn, }; expect(parser.htmlToMarkdown(testString, extras)).toBe(resultString); - expect(cacheMediaAttributes).toHaveBeenCalledWith("https://example.com/video.mp4", ' data-expensify-width="100" data-expensify-height="500" data-expensify-thumbnail-url="https://image.com/img.jpg"') + expect(mediaAttributeCachingFn).toHaveBeenCalledWith("https://example.com/video.mp4", ' data-expensify-width="100" data-expensify-height="500" data-expensify-thumbnail-url="https://image.com/img.jpg"') }) }) diff --git a/lib/ExpensiMark.ts b/lib/ExpensiMark.ts index b6942ba9..3b410f4e 100644 --- a/lib/ExpensiMark.ts +++ b/lib/ExpensiMark.ts @@ -9,7 +9,7 @@ import * as Utils from './utils'; type Extras = { reportIDToName?: Record; accountIDToName?: Record; - cacheMediaAttributes?: (mediaSource: string, attrs: string) => void; + mediaAttributeCachingFn?: (mediaSource: string, attrs: string) => void; mediaAttributeCache?: Record; }; const EXTRAS_DEFAULT = {}; @@ -677,17 +677,17 @@ export default class ExpensiMark { let altText = ''; const altRegex = /alt\s*=\s*(['"])(.*?)\1/i; const altMatch = imgAttrs.match(altRegex); - let attributes = ''; + let attributes = imgAttrs; if (altMatch) { altText = altMatch[2]; // Remove the alt attribute from imgAttrs - attributes = imgAttrs.replace(altRegex, ''); + attributes = attributes.replace(altRegex, ''); } // Remove trailing slash and extra whitespace attributes = attributes.replace(/\s*\/\s*$/, '').trim(); // Cache attributes without alt and trailing slash - if (imgAttrs && extras && extras.cacheMediaAttributes && typeof extras.cacheMediaAttributes === 'function') { - extras.cacheMediaAttributes(imgSource, attributes); + if (imgAttrs && extras && extras.mediaAttributeCachingFn && typeof extras.mediaAttributeCachingFn === 'function') { + extras.mediaAttributeCachingFn(imgSource, attributes); } // Return the markdown image tag if (altText) { @@ -710,8 +710,8 @@ export default class ExpensiMark { * @returns The markdown video tag */ replacement: (extras, _match, _g1, videoSource, videoAttrs, videoName) => { - if (videoAttrs && extras && extras.cacheMediaAttributes && typeof extras.cacheMediaAttributes === 'function') { - extras.cacheMediaAttributes(videoSource, videoAttrs); + if (videoAttrs && extras && extras.mediaAttributeCachingFn && typeof extras.mediaAttributeCachingFn === 'function') { + extras.mediaAttributeCachingFn(videoSource, videoAttrs); } if (videoName) { return `![${videoName}](${videoSource})`; From fd422b3d2a23dfe32fddc2aac7136ba3c4156d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Jasikowski?= Date: Thu, 26 Sep 2024 09:46:37 +0200 Subject: [PATCH 4/6] Exporting Extras from ExpensiMark.ts --- lib/ExpensiMark.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ExpensiMark.ts b/lib/ExpensiMark.ts index 3b410f4e..c9f5cc60 100644 --- a/lib/ExpensiMark.ts +++ b/lib/ExpensiMark.ts @@ -6,7 +6,7 @@ import * as UrlPatterns from './Url'; import Logger from './Logger'; import * as Utils from './utils'; -type Extras = { +export type Extras = { reportIDToName?: Record; accountIDToName?: Record; mediaAttributeCachingFn?: (mediaSource: string, attrs: string) => void; From 110a43278002ed93a6d199327b3de17ca7b5d217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Jasikowski?= Date: Thu, 26 Sep 2024 10:12:16 +0200 Subject: [PATCH 5/6] Moved Extras definition to a separate file --- lib/Extras.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 lib/Extras.ts diff --git a/lib/Extras.ts b/lib/Extras.ts new file mode 100644 index 00000000..e69de29b From a7c7cd87a4fa2be35cadb38a3172c29ad6ba0567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Jasikowski?= Date: Thu, 26 Sep 2024 10:12:32 +0200 Subject: [PATCH 6/6] Moved Extras definition to a separate file --- lib/ExpensiMark.ts | 7 +------ lib/Extras.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/ExpensiMark.ts b/lib/ExpensiMark.ts index c9f5cc60..1d702678 100644 --- a/lib/ExpensiMark.ts +++ b/lib/ExpensiMark.ts @@ -5,13 +5,8 @@ import * as Constants from './CONST'; import * as UrlPatterns from './Url'; import Logger from './Logger'; import * as Utils from './utils'; +import type Extras from './Extras'; -export type Extras = { - reportIDToName?: Record; - accountIDToName?: Record; - mediaAttributeCachingFn?: (mediaSource: string, attrs: string) => void; - mediaAttributeCache?: Record; -}; const EXTRAS_DEFAULT = {}; type ReplacementFn = (extras: Extras, ...matches: string[]) => string; diff --git a/lib/Extras.ts b/lib/Extras.ts index e69de29b..0f1075da 100644 --- a/lib/Extras.ts +++ b/lib/Extras.ts @@ -0,0 +1,8 @@ +type Extras = { + reportIDToName?: Record; + accountIDToName?: Record; + mediaAttributeCachingFn?: (mediaSource: string, attrs: string) => void; + mediaAttributeCache?: Record; +}; + +export default Extras;