Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added attribute caching to img #805

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion __tests__/ExpensiMark-HTML-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2102,7 +2102,7 @@ describe('Video markdown conversion to html tag', () => {
const resultString = '<video data-expensify-source="https://example.com/video.mp4" data-expensify-height="100" data-expensify-width="100">test</video>';
expect(parser.replace(testString, {
extras: {
videoAttributeCache: {
mediaAttributeCache: {
'https://example.com/video.mp4': 'data-expensify-height="100" data-expensify-width="100"'
}
}
Expand Down Expand Up @@ -2220,6 +2220,18 @@ describe('Image markdown conversion to html tag', () => {
'<img src="https://example.com/image.png" alt="&#x60;&#x60;&#x60;code&#x60;&#x60;&#x60;" data-raw-href="https://example.com/image.png" data-link-variant="labeled" />';
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 = '<img src="https://example.com/image.jpg" alt="test" data-expensify-height="100" data-expensify-width="100"/>';
expect(parser.replace(testString, {
extras: {
mediaAttributeCache: {
'https://example.com/image.jpg': 'data-expensify-height="100" data-expensify-width="100"'
}
}
})).toBe(resultString);
})
});

describe('room mentions', () => {
Expand Down
29 changes: 26 additions & 3 deletions __tests__/ExpensiMark-Markdown-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<img src="https://example.com/image.png" alt="altText" data-expensify-width="100" data-expensify-height="500" data-name="newName" data-expensify-source="expensify-source" />';
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 = '<img src="https://example.com/image.png" data-expensify-width="100" data-expensify-height="500" data-name="newName" data-expensify-source="expensify-source" />';
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', () => {
Expand All @@ -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 = '<video data-expensify-source="https://example.com/video.mp4" data-expensify-width="100" data-expensify-height="500" data-expensify-thumbnail-url="https://image.com/img.jpg">video</video>';
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"')
})
})

Expand Down
63 changes: 44 additions & 19 deletions lib/ExpensiMark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
accountIDToName?: Record<string, string>;
cacheVideoAttributes?: (vidSource: string, attrs: string) => void;
videoAttributeCache?: Record<string, string>;
};
const EXTRAS_DEFAULT = {};

type ReplacementFn = (extras: Extras, ...matches: string[]) => string;
Expand Down Expand Up @@ -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 `<video data-expensify-source="${Str.sanitizeURL(videoSource)}" ${extraAttrs || ''}>${videoName ? `${videoName}` : ''}</video>`;
},
rawInputReplacement: (extras, _match, videoName, videoSource) => {
const extraAttrs = extras && extras.videoAttributeCache && extras.videoAttributeCache[videoSource];
const extraAttrs = extras && extras.mediaAttributeCache && extras.mediaAttributeCache[videoSource];
return `<video data-expensify-source="${Str.sanitizeURL(videoSource)}" data-raw-href="${videoSource}" data-link-variant="${typeof videoName === 'string' ? 'labeled' : 'auto'}" ${extraAttrs || ''}>${videoName ? `${videoName}` : ''}</video>`;
},
},
Expand Down Expand Up @@ -249,9 +244,14 @@ export default class ExpensiMark {
{
name: 'image',
regex: MARKDOWN_IMAGE_REGEX,
replacement: (_extras, _match, g1, g2) => `<img src="${Str.sanitizeURL(g2)}"${g1 ? ` alt="${this.escapeAttributeContent(g1)}"` : ''} />`,
rawInputReplacement: (_extras, _match, g1, g2) =>
`<img src="${Str.sanitizeURL(g2)}"${g1 ? ` alt="${this.escapeAttributeContent(g1)}"` : ''} data-raw-href="${g2}" data-link-variant="${typeof g1 === 'string' ? 'labeled' : 'auto'}" />`,
replacement: (extras, _match, imgAlt, imgSource) => {
const extraAttrs = extras && extras.mediaAttributeCache && extras.mediaAttributeCache[imgSource];
return `<img src="${Str.sanitizeURL(imgSource)}"${imgAlt ? ` alt="${this.escapeAttributeContent(imgAlt)}"` : ''} ${extraAttrs || ''}/>`;
},
rawInputReplacement: (extras, _match, imgAlt, imgSource) => {
const extraAttrs = extras && extras.mediaAttributeCache && extras.mediaAttributeCache[imgSource];
return `<img src="${Str.sanitizeURL(imgSource)}"${imgAlt ? ` alt="${this.escapeAttributeContent(imgAlt)}"` : ''} data-raw-href="${imgSource}" data-link-variant="${typeof imgAlt === 'string' ? 'labeled' : 'auto'}" ${extraAttrs || ''}/>`;
},
},

/**
Expand Down Expand Up @@ -658,13 +658,37 @@ export default class ExpensiMark {

{
name: 'image',
regex: /<img[^><]*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: /<img[^><]*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})`;
},
},

Expand All @@ -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})`;
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions lib/Extras.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
type Extras = {
reportIDToName?: Record<string, string>;
accountIDToName?: Record<string, string>;
mediaAttributeCachingFn?: (mediaSource: string, attrs: string) => void;
mediaAttributeCache?: Record<string, string>;
};

export default Extras;
Loading