diff --git a/__tests__/ExpensiMark-HTML-test.js b/__tests__/ExpensiMark-HTML-test.js index 1417cc5a..8ae98224 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"' } } @@ -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 attributes', () => { + 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/__tests__/ExpensiMark-Markdown-test.js b/__tests__/ExpensiMark-Markdown-test.js index 1ebfd21a..143dab9d 100644 --- a/__tests__/ExpensiMark-Markdown-test.js +++ b/__tests__/ExpensiMark-Markdown-test.js @@ -867,6 +867,29 @@ 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 with alt', () => { + const mediaAttributeCachingFn = jest.fn(); + const testString = 'altText'; + const resultString = '![altText](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"') + }); + + 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"') + }); + }); describe('Video tag conversion to markdown', () => { @@ -883,14 +906,14 @@ describe('Video tag conversion to markdown', () => { }) test('While convert video, cache some extra attributes from the video tag', () => { - const cacheVideoAttributes = jest.fn(); + const mediaAttributeCachingFn = jest.fn(); const testString = ''; const resultString = '![video](https://example.com/video.mp4)'; const extras = { - cacheVideoAttributes, + mediaAttributeCachingFn, }; 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(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 0a17a763..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'; -type Extras = { - reportIDToName?: Record; - accountIDToName?: Record; - cacheVideoAttributes?: (vidSource: string, attrs: string) => void; - videoAttributeCache?: Record; -}; const EXTRAS_DEFAULT = {}; type ReplacementFn = (extras: Extras, ...matches: string[]) => string; @@ -171,11 +166,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 ``; }, }, @@ -249,9 +244,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)}`; + }, }, /** @@ -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 = imgAttrs; + if (altMatch) { + altText = altMatch[2]; + // Remove the alt attribute from imgAttrs + attributes = attributes.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.mediaAttributeCachingFn && typeof extras.mediaAttributeCachingFn === 'function') { + extras.mediaAttributeCachingFn(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.mediaAttributeCachingFn && typeof extras.mediaAttributeCachingFn === 'function') { + extras.mediaAttributeCachingFn(videoSource, videoAttrs); } if (videoName) { return `![${videoName}](${videoSource})`; @@ -867,6 +891,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); diff --git a/lib/Extras.ts b/lib/Extras.ts new file mode 100644 index 00000000..0f1075da --- /dev/null +++ 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;