diff --git a/__tests__/ExpensiMark-HTML-test.js b/__tests__/ExpensiMark-HTML-test.js
index 763df5b6..ecafb223 100644
--- a/__tests__/ExpensiMark-HTML-test.js
+++ b/__tests__/ExpensiMark-HTML-test.js
@@ -1958,6 +1958,45 @@ describe('multi-level blockquote', () => {
});
});
+describe('Video markdown conversion to html tag', () => {
+ test('Single video with source', () => {
+ const testString = '![test](https://example.com/video.mp4)';
+ const resultString = '';
+ expect(parser.replace(testString)).toBe(resultString);
+ });
+
+ test('Text containing videos', () => {
+ const testString = 'A video of a banana: ![banana](https://example.com/banana.mp4) a video without name: !(https://example.com/developer.mp4)';
+ const resultString = 'A video of a banana: a video without name: ';
+ expect(parser.replace(testString)).toBe(resultString);
+ });
+
+ test('Video with raw data attributes', () => {
+ const testString = '![test](https://example.com/video.mp4)';
+ const resultString = '';
+ expect(parser.replace(testString, {shouldKeepRawInput: true})).toBe(resultString);
+ })
+
+ test('Single video with extra cached attribues', () => {
+ const testString = '![test](https://example.com/video.mp4)';
+ const resultString = '';
+ expect(parser.replace(testString, {
+ extras: {
+ videoAttributeCache: {
+ 'https://example.com/video.mp4': 'data-expensify-height="100" data-expensify-width="100"'
+ }
+ }
+ })).toBe(resultString);
+ })
+
+ test('Text containing image and video', () => {
+ const testString = 'An image of a banana: ![banana](https://example.com/banana.png) and a video of a banana: ![banana](https://example.com/banana.mp4)';
+ const resultString = 'An image of a banana: and a video of a banana: ';
+ expect(parser.replace(testString)).toBe(resultString);
+ });
+
+})
+
describe('Image markdown conversion to html tag', () => {
test('Single image with alt text', () => {
const testString = '![test](https://example.com/image.png)';
diff --git a/__tests__/ExpensiMark-Markdown-test.js b/__tests__/ExpensiMark-Markdown-test.js
index 093f5ca5..bb1baf1f 100644
--- a/__tests__/ExpensiMark-Markdown-test.js
+++ b/__tests__/ExpensiMark-Markdown-test.js
@@ -848,3 +848,28 @@ describe('Image tag conversion to markdown', () => {
expect(parser.htmlToMarkdown(testString)).toBe(resultString);
});
});
+
+describe('Video tag conversion to markdown', () => {
+ test('Video with name', () => {
+ const testString = '';
+ const resultString = '![video](https://example.com/video.mp4)';
+ expect(parser.htmlToMarkdown(testString)).toBe(resultString);
+ })
+
+ test('Video without name', () => {
+ const testString = '';
+ const resultString = '!(https://example.com/video.mp4)';
+ expect(parser.htmlToMarkdown(testString)).toBe(resultString);
+ })
+
+ test('While convert video, cache some extra attributes from the video tag', () => {
+ const cacheVideoAttributes = jest.fn();
+ const testString = '';
+ const resultString = '![video](https://example.com/video.mp4)';
+ const extras = {
+ cacheVideoAttributes,
+ };
+ 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"')
+ })
+})
diff --git a/lib/CONST.ts b/lib/CONST.ts
index 5c62fd97..0d80b0b9 100644
--- a/lib/CONST.ts
+++ b/lib/CONST.ts
@@ -885,6 +885,7 @@ const CONST = {
prompt: 'What software are you moving to and why?',
},
},
+ VIDEO_EXTENSIONS: ['mp4', 'mov', 'avi', 'wmv', 'flv', 'mkv', 'webm', '3gp', 'm4v', 'mpg', 'mpeg', 'ogv'],
} as const;
/**
diff --git a/lib/ExpensiMark.d.ts b/lib/ExpensiMark.d.ts
index a10aa20c..8a67ae27 100644
--- a/lib/ExpensiMark.d.ts
+++ b/lib/ExpensiMark.d.ts
@@ -39,7 +39,13 @@ declare type Rule = {
declare type ExtrasObject = {
reportIDToName?: Record;
accountIDToName?: Record;
+ cacheVideoAttributes?: (vidSource: string, attrs: string) => void;
};
+
+declare type ExtraParamsForReplaceFunc = {
+ videoAttributeCache?: Record;
+};
+
export default class ExpensiMark {
static Log: Logger;
static setLogger(logger: Logger): void;
@@ -66,11 +72,13 @@ export default class ExpensiMark {
filterRules,
shouldEscapeText,
shouldKeepRawInput,
+ extras,
}?: {
filterRules?: Name[];
disabledRules?: Name[];
shouldEscapeText?: boolean;
shouldKeepRawInput?: boolean;
+ extras?: ExtraParamsForReplaceFunc;
},
): string;
/**
diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js
index 6bc9981b..6f42c065 100644
--- a/lib/ExpensiMark.js
+++ b/lib/ExpensiMark.js
@@ -7,6 +7,11 @@ import * as Utils from './utils';
const MARKDOWN_LINK_REGEX = new RegExp(`\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)]\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, 'gi');
const MARKDOWN_IMAGE_REGEX = new RegExp(`\\!(?:\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)])?\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, 'gi');
+const MARKDOWN_VIDEO_REGEX = new RegExp(
+ `\\!(?:\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)])?\\(((${UrlPatterns.MARKDOWN_URL_REGEX})\\.(?:${Constants.CONST.VIDEO_EXTENSIONS.join('|')}))\\)(?![^<]*(<\\/pre>|<\\/code>))`,
+ 'gi',
+);
+
const SLACK_SPAN_NEW_LINE_TAG = '';
export default class ExpensiMark {
@@ -121,7 +126,34 @@ export default class ExpensiMark {
},
/**
- * Converts markdown style images to img tags e.g. ![Expensify](https://www.expensify.com/attachment.png)
+ * Converts markdown style video to video tags e.g. ![Expensify](https://www.expensify.com/attachment.mp4)
+ * We need to convert before image rules since they will not try to create a image tag from an existing video URL
+ * Extras arg could contain the attribute cache for the video tag which is cached during the html-to-markdown conversion
+ */
+ {
+ name: 'video',
+ regex: MARKDOWN_VIDEO_REGEX,
+ /**
+ * @param {string} match
+ * @param {string} videoName - The first capture group - video name
+ * @param {string} videoSource - The second capture group - video URL
+ * @param {any[]} args - The rest capture groups and `extras` object. args[args.length-1] will the `extras` object
+ * @return {string} Returns the HTML video tag
+ */
+ replacement: (match, videoName, videoSource, ...args) => {
+ const extras = args[args.length - 1];
+ const extraAttrs = extras && extras.videoAttributeCache && extras.videoAttributeCache[videoSource];
+ return ``;
+ },
+ rawInputReplacement: (match, videoName, videoSource, ...args) => {
+ const extras = args[args.length - 1];
+ const extraAttrs = extras && extras.videoAttributeCache && extras.videoAttributeCache[videoSource];
+ return ``;
+ },
+ },
+
+ /**
+ * Converts markdown style images to image tags e.g. ![Expensify](https://www.expensify.com/attachment.png)
* We need to convert before linking rules since they will not try to create a link from an existing img
* tag.
* Additional sanitization is done to the alt attribute to prevent parsing it further to html by later
@@ -487,6 +519,7 @@ export default class ExpensiMark {
return `[${g4}](${email || g3})`;
},
},
+
{
name: 'image',
regex: /<]*src\s*=\s*(['"])(.*?)\1(?:[^><]*alt\s*=\s*(['"])(.*?)\3)?[^><]*>*(?![^<][\s\S]*?(<\/pre>|<\/code>))/gi,
@@ -498,6 +531,32 @@ export default class ExpensiMark {
return `!(${g2})`;
},
},
+
+ {
+ name: 'video',
+ regex: /