expensify-common 2.0.18 → 2.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ExpensiMark.d.ts +99 -103
- package/dist/ExpensiMark.js +123 -147
- package/package.json +1 -1
package/dist/ExpensiMark.d.ts
CHANGED
|
@@ -1,155 +1,151 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
declare type Replacement = (...args: string[], extras?: ExtrasObject) => string;
|
|
4
|
-
declare type Name =
|
|
5
|
-
| 'codeFence'
|
|
6
|
-
| 'inlineCodeBlock'
|
|
7
|
-
| 'email'
|
|
8
|
-
| 'link'
|
|
9
|
-
| 'hereMentions'
|
|
10
|
-
| 'userMentions'
|
|
11
|
-
| 'reportMentions'
|
|
12
|
-
| 'autoEmail'
|
|
13
|
-
| 'autolink'
|
|
14
|
-
| 'quote'
|
|
15
|
-
| 'italic'
|
|
16
|
-
| 'bold'
|
|
17
|
-
| 'strikethrough'
|
|
18
|
-
| 'heading1'
|
|
19
|
-
| 'newline'
|
|
20
|
-
| 'replacepre'
|
|
21
|
-
| 'listItem'
|
|
22
|
-
| 'exclude'
|
|
23
|
-
| 'anchor'
|
|
24
|
-
| 'breakline'
|
|
25
|
-
| 'blockquoteWrapHeadingOpen'
|
|
26
|
-
| 'blockquoteWrapHeadingClose'
|
|
27
|
-
| 'blockElementOpen'
|
|
28
|
-
| 'blockElementClose'
|
|
29
|
-
| 'stripTag';
|
|
30
|
-
declare type Rule = {
|
|
31
|
-
name: Name;
|
|
32
|
-
process?: (textToProcess: string, replacement: Replacement) => string;
|
|
33
|
-
regex?: RegExp;
|
|
34
|
-
replacement: Replacement | string;
|
|
35
|
-
pre?: (input: string) => string;
|
|
36
|
-
post?: (input: string) => string;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
declare type ExtrasObject = {
|
|
1
|
+
import Logger from './Logger';
|
|
2
|
+
type Extras = {
|
|
40
3
|
reportIDToName?: Record<string, string>;
|
|
41
4
|
accountIDToName?: Record<string, string>;
|
|
42
5
|
cacheVideoAttributes?: (vidSource: string, attrs: string) => void;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
declare type ExtraParamsForReplaceFunc = {
|
|
46
6
|
videoAttributeCache?: Record<string, string>;
|
|
47
7
|
};
|
|
48
|
-
|
|
8
|
+
type ReplacementFn = (extras: Extras, ...matches: string[]) => string;
|
|
9
|
+
type Replacement = ReplacementFn | string;
|
|
10
|
+
type ProcessFn = (textToProcess: string, replacement: Replacement, shouldKeepRawInput: boolean) => string;
|
|
11
|
+
type CommonRule = {
|
|
12
|
+
name: string;
|
|
13
|
+
replacement: Replacement;
|
|
14
|
+
rawInputReplacement?: Replacement;
|
|
15
|
+
pre?: (input: string) => string;
|
|
16
|
+
post?: (input: string) => string;
|
|
17
|
+
};
|
|
18
|
+
type RuleWithRegex = CommonRule & {
|
|
19
|
+
regex: RegExp;
|
|
20
|
+
};
|
|
21
|
+
type RuleWithProcess = CommonRule & {
|
|
22
|
+
process: ProcessFn;
|
|
23
|
+
};
|
|
24
|
+
type Rule = RuleWithRegex | RuleWithProcess;
|
|
25
|
+
type ReplaceOptions = {
|
|
26
|
+
extras?: Extras;
|
|
27
|
+
filterRules?: string[];
|
|
28
|
+
disabledRules?: string[];
|
|
29
|
+
shouldEscapeText?: boolean;
|
|
30
|
+
shouldKeepRawInput?: boolean;
|
|
31
|
+
};
|
|
49
32
|
export default class ExpensiMark {
|
|
50
33
|
static Log: Logger;
|
|
34
|
+
/**
|
|
35
|
+
* Set the logger to use for logging inside of the ExpensiMark class
|
|
36
|
+
* @param logger - The logger object to use
|
|
37
|
+
*/
|
|
51
38
|
static setLogger(logger: Logger): void;
|
|
52
|
-
|
|
39
|
+
/** Rules to apply to the text */
|
|
53
40
|
rules: Rule[];
|
|
54
|
-
|
|
55
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The list of regex replacements to do on a HTML comment for converting it to markdown.
|
|
43
|
+
* Order of rules is important
|
|
44
|
+
*/
|
|
45
|
+
htmlToMarkdownRules: RuleWithRegex[];
|
|
46
|
+
/**
|
|
47
|
+
* The list of rules to covert the HTML to text.
|
|
48
|
+
* Order of rules is important
|
|
49
|
+
*/
|
|
50
|
+
htmlToTextRules: RuleWithRegex[];
|
|
51
|
+
/**
|
|
52
|
+
* The list of rules that we have to exclude in shouldKeepWhitespaceRules list.
|
|
53
|
+
*/
|
|
54
|
+
whitespaceRulesToDisable: string[];
|
|
55
|
+
/**
|
|
56
|
+
* The list of rules that have to be applied when shouldKeepWhitespace flag is true.
|
|
57
|
+
*/
|
|
58
|
+
filterRules: (rule: Rule) => boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Filters rules to determine which should keep whitespace.
|
|
61
|
+
*/
|
|
62
|
+
shouldKeepWhitespaceRules: Rule[];
|
|
63
|
+
/**
|
|
64
|
+
* maxQuoteDepth is the maximum depth of nested quotes that we want to support.
|
|
65
|
+
*/
|
|
66
|
+
maxQuoteDepth: number;
|
|
67
|
+
/**
|
|
68
|
+
* currentQuoteDepth is the current depth of nested quotes that we are processing.
|
|
69
|
+
*/
|
|
70
|
+
currentQuoteDepth: number;
|
|
56
71
|
constructor();
|
|
72
|
+
/**
|
|
73
|
+
* Retrieves the HTML ruleset based on the provided filter rules, disabled rules, and shouldKeepRawInput flag.
|
|
74
|
+
* @param filterRules - An array of rule names to filter the ruleset.
|
|
75
|
+
* @param disabledRules - An array of rule names to disable in the ruleset.
|
|
76
|
+
* @param shouldKeepRawInput - A boolean flag indicating whether to keep raw input.
|
|
77
|
+
*/
|
|
78
|
+
getHtmlRuleset(filterRules: string[], disabledRules: string[], shouldKeepRawInput: boolean): Rule[];
|
|
57
79
|
/**
|
|
58
80
|
* Replaces markdown with html elements
|
|
59
81
|
*
|
|
60
82
|
* @param text - Text to parse as markdown
|
|
61
|
-
* @param options - Options to customize the markdown parser
|
|
62
|
-
* @param options.filterRules=[] - An array of name of rules as defined in this class.
|
|
63
|
-
* If not provided, all available rules will be applied.
|
|
64
|
-
* @param options.
|
|
83
|
+
* @param [options] - Options to customize the markdown parser
|
|
84
|
+
* @param [options.filterRules=[]] - An array of name of rules as defined in this class.
|
|
85
|
+
* If not provided, all available rules will be applied.
|
|
86
|
+
* @param [options.shouldEscapeText=true] - Whether or not the text should be escaped
|
|
87
|
+
* @param [options.disabledRules=[]] - An array of name of rules as defined in this class.
|
|
65
88
|
* If not provided, all available rules will be applied. If provided, the rules in the array will be skipped.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*/
|
|
69
|
-
replace(
|
|
70
|
-
text: string,
|
|
71
|
-
{
|
|
72
|
-
filterRules,
|
|
73
|
-
shouldEscapeText,
|
|
74
|
-
shouldKeepRawInput,
|
|
75
|
-
extras,
|
|
76
|
-
}?: {
|
|
77
|
-
filterRules?: Name[];
|
|
78
|
-
disabledRules?: Name[];
|
|
79
|
-
shouldEscapeText?: boolean;
|
|
80
|
-
shouldKeepRawInput?: boolean;
|
|
81
|
-
extras?: ExtraParamsForReplaceFunc;
|
|
82
|
-
},
|
|
83
|
-
): string;
|
|
89
|
+
*/
|
|
90
|
+
replace(text: string, { filterRules, shouldEscapeText, shouldKeepRawInput, disabledRules, extras }?: ReplaceOptions): string;
|
|
84
91
|
/**
|
|
85
92
|
* Checks matched URLs for validity and replace valid links with html elements
|
|
86
|
-
*
|
|
87
|
-
* @param regex
|
|
88
|
-
* @param textToCheck
|
|
89
|
-
* @param replacement
|
|
90
93
|
*/
|
|
91
|
-
modifyTextForUrlLinks(regex: RegExp, textToCheck: string, replacement:
|
|
94
|
+
modifyTextForUrlLinks(regex: RegExp, textToCheck: string, replacement: ReplacementFn): string;
|
|
92
95
|
/**
|
|
93
96
|
* Checks matched Emails for validity and replace valid links with html elements
|
|
94
|
-
*
|
|
95
|
-
* @param regex
|
|
96
|
-
* @param textToCheck
|
|
97
|
-
* @param replacement
|
|
98
97
|
*/
|
|
99
|
-
modifyTextForEmailLinks(regex: RegExp, textToCheck: string, replacement:
|
|
98
|
+
modifyTextForEmailLinks(regex: RegExp, textToCheck: string, replacement: ReplacementFn, shouldKeepRawInput: boolean): string;
|
|
100
99
|
/**
|
|
101
100
|
* replace block element with '\n' if :
|
|
102
101
|
* 1. We have text within the element.
|
|
103
102
|
* 2. The text does not end with a new line.
|
|
104
103
|
* 3. The text does not have quote mark '>' .
|
|
105
104
|
* 4. It's not the last element in the string.
|
|
106
|
-
*
|
|
107
|
-
* @param htmlString
|
|
108
105
|
*/
|
|
109
106
|
replaceBlockElementWithNewLine(htmlString: string): string;
|
|
110
107
|
/**
|
|
111
108
|
* Replaces HTML with markdown
|
|
112
|
-
*
|
|
113
|
-
* @param htmlString
|
|
114
|
-
* @param extras
|
|
115
109
|
*/
|
|
116
|
-
htmlToMarkdown(htmlString: string, extras?:
|
|
110
|
+
htmlToMarkdown(htmlString: string, extras?: Extras): string;
|
|
117
111
|
/**
|
|
118
112
|
* Convert HTML to text
|
|
119
|
-
*
|
|
120
|
-
* @param htmlString
|
|
121
|
-
* @param extras
|
|
122
113
|
*/
|
|
123
|
-
htmlToText(htmlString: string, extras?:
|
|
114
|
+
htmlToText(htmlString: string, extras?: Extras): string;
|
|
124
115
|
/**
|
|
125
116
|
* Modify text for Quotes replacing chevrons with html elements
|
|
126
|
-
*
|
|
127
|
-
* @param regex
|
|
128
|
-
* @param textToCheck
|
|
129
|
-
* @param replacement
|
|
130
117
|
*/
|
|
131
|
-
modifyTextForQuote(regex: RegExp, textToCheck: string, replacement:
|
|
118
|
+
modifyTextForQuote(regex: RegExp, textToCheck: string, replacement: ReplacementFn): string;
|
|
132
119
|
/**
|
|
133
120
|
* Format the content of blockquote if the text matches the regex or else just return the original text
|
|
134
|
-
*
|
|
135
|
-
* @param regex
|
|
136
|
-
* @param textToCheck
|
|
137
|
-
* @param replacement
|
|
138
121
|
*/
|
|
139
|
-
formatTextForQuote(regex: RegExp, textToCheck: string, replacement:
|
|
122
|
+
formatTextForQuote(regex: RegExp, textToCheck: string, replacement: ReplacementFn): string;
|
|
140
123
|
/**
|
|
141
124
|
* Check if the input text includes only the open or the close tag of an element.
|
|
142
|
-
*
|
|
143
|
-
* @param textToCheck - Text to check
|
|
144
125
|
*/
|
|
145
126
|
containsNonPairTag(textToCheck: string): boolean;
|
|
127
|
+
/**
|
|
128
|
+
* @returns array or undefined if exception occurs when executing regex matching
|
|
129
|
+
*/
|
|
146
130
|
extractLinksInMarkdownComment(comment: string): string[] | undefined;
|
|
147
131
|
/**
|
|
148
132
|
* Compares two markdown comments and returns a list of the links removed in a new comment.
|
|
149
|
-
*
|
|
150
|
-
* @param oldComment
|
|
151
|
-
* @param newComment
|
|
152
133
|
*/
|
|
153
134
|
getRemovedMarkdownLinks(oldComment: string, newComment: string): string[];
|
|
135
|
+
/**
|
|
136
|
+
* Escapes the content of an HTML attribute value
|
|
137
|
+
* @param content - string content that possible contains HTML
|
|
138
|
+
* @returns original MD content escaped for use in HTML attribute value
|
|
139
|
+
*/
|
|
140
|
+
escapeAttributeContent(content: string): string;
|
|
141
|
+
/**
|
|
142
|
+
* Replaces text with a replacement based on a regex
|
|
143
|
+
* @param text - The text to replace
|
|
144
|
+
* @param regexp - The regex to match
|
|
145
|
+
* @param extras - The extras object
|
|
146
|
+
* @param replacement - The replacement string or function
|
|
147
|
+
* @returns The replaced text
|
|
148
|
+
*/
|
|
149
|
+
replaceTextWithExtras(text: string, regexp: RegExp, extras: Extras, replacement: Replacement): string;
|
|
154
150
|
}
|
|
155
151
|
export {};
|
package/dist/ExpensiMark.js
CHANGED
|
@@ -31,6 +31,7 @@ const Constants = __importStar(require("./CONST"));
|
|
|
31
31
|
const UrlPatterns = __importStar(require("./Url"));
|
|
32
32
|
const Logger_1 = __importDefault(require("./Logger"));
|
|
33
33
|
const Utils = __importStar(require("./utils"));
|
|
34
|
+
const EXTRAS_DEFAULT = {};
|
|
34
35
|
const MARKDOWN_LINK_REGEX = new RegExp(`\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)]\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, 'gi');
|
|
35
36
|
const MARKDOWN_IMAGE_REGEX = new RegExp(`\\!(?:\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)])?\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, 'gi');
|
|
36
37
|
const MARKDOWN_VIDEO_REGEX = new RegExp(`\\!(?:\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)])?\\(((${UrlPatterns.MARKDOWN_URL_REGEX})\\.(?:${Constants.CONST.VIDEO_EXTENSIONS.join('|')}))\\)(?![^<]*(<\\/pre>|<\\/code>))`, 'gi');
|
|
@@ -38,24 +39,26 @@ const SLACK_SPAN_NEW_LINE_TAG = '<span class="c-mrkdwn__br" data-stringify-type=
|
|
|
38
39
|
class ExpensiMark {
|
|
39
40
|
/**
|
|
40
41
|
* Set the logger to use for logging inside of the ExpensiMark class
|
|
41
|
-
* @param
|
|
42
|
+
* @param logger - The logger object to use
|
|
42
43
|
*/
|
|
43
44
|
static setLogger(logger) {
|
|
44
45
|
ExpensiMark.Log = logger;
|
|
45
46
|
}
|
|
46
47
|
constructor() {
|
|
48
|
+
/**
|
|
49
|
+
* The list of rules that we have to exclude in shouldKeepWhitespaceRules list.
|
|
50
|
+
*/
|
|
51
|
+
this.whitespaceRulesToDisable = ['newline', 'replacepre', 'replacebr', 'replaceh1br'];
|
|
47
52
|
/**
|
|
48
53
|
* The list of regex replacements to do on a comment. Check the link regex is first so links are processed
|
|
49
54
|
* before other delimiters
|
|
50
|
-
*
|
|
51
|
-
* @type {Object[]}
|
|
52
55
|
*/
|
|
53
56
|
this.rules = [
|
|
54
57
|
// Apply the emoji first avoid applying any other formatting rules inside of it
|
|
55
58
|
{
|
|
56
59
|
name: 'emoji',
|
|
57
60
|
regex: Constants.CONST.REG_EXP.EMOJI_RULE,
|
|
58
|
-
replacement: (match) => `<emoji>${match}</emoji>`,
|
|
61
|
+
replacement: (_extras, match) => `<emoji>${match}</emoji>`,
|
|
59
62
|
},
|
|
60
63
|
/**
|
|
61
64
|
* Apply the code-fence to avoid replacing anything inside of it that we're not supposed to
|
|
@@ -71,11 +74,11 @@ class ExpensiMark {
|
|
|
71
74
|
// with the new lines here since they need to be converted into <br>. And we don't
|
|
72
75
|
// want to do this anywhere else since that would break HTML.
|
|
73
76
|
// will create styling issues so use  
|
|
74
|
-
replacement: (
|
|
77
|
+
replacement: (_extras, _match, _g1, textWithinFences) => {
|
|
75
78
|
const group = textWithinFences.replace(/(?:(?![\n\r])\s)/g, ' ');
|
|
76
79
|
return `<pre>${group}</pre>`;
|
|
77
80
|
},
|
|
78
|
-
rawInputReplacement: (
|
|
81
|
+
rawInputReplacement: (_extras, _match, _g1, textWithinFences) => {
|
|
79
82
|
const group = textWithinFences.replace(/(?:(?![\n\r])\s)/g, ' ').replace(/<emoji>|<\/emoji>/g, '');
|
|
80
83
|
return `<pre>${group}</pre>`;
|
|
81
84
|
},
|
|
@@ -103,7 +106,7 @@ class ExpensiMark {
|
|
|
103
106
|
const regex = new RegExp(`(?!\\[\\s*\\])\\[([^[\\]]*)]\\((mailto:)?${Constants.CONST.REG_EXP.MARKDOWN_EMAIL}\\)`, 'gim');
|
|
104
107
|
return this.modifyTextForEmailLinks(regex, textToProcess, replacement, shouldKeepRawInput);
|
|
105
108
|
},
|
|
106
|
-
replacement: (match, g1, g2) => {
|
|
109
|
+
replacement: (_extras, match, g1, g2) => {
|
|
107
110
|
if (g1.match(Constants.CONST.REG_EXP.EMOJIS) || !g1.trim()) {
|
|
108
111
|
return match;
|
|
109
112
|
}
|
|
@@ -112,7 +115,7 @@ class ExpensiMark {
|
|
|
112
115
|
const formattedLabel = label === href ? g2 : label;
|
|
113
116
|
return `<a href="${href}">${formattedLabel}</a>`;
|
|
114
117
|
},
|
|
115
|
-
rawInputReplacement: (match, g1, g2, g3) => {
|
|
118
|
+
rawInputReplacement: (_extras, match, g1, g2, g3) => {
|
|
116
119
|
if (g1.match(Constants.CONST.REG_EXP.EMOJIS) || !g1.trim()) {
|
|
117
120
|
return match;
|
|
118
121
|
}
|
|
@@ -125,7 +128,7 @@ class ExpensiMark {
|
|
|
125
128
|
name: 'heading1',
|
|
126
129
|
process: (textToProcess, replacement, shouldKeepRawInput = false) => {
|
|
127
130
|
const regexp = shouldKeepRawInput ? /^# ( *(?! )(?:(?!<pre>|\n|\r\n).)+)/gm : /^# +(?! )((?:(?!<pre>|\n|\r\n).)+)/gm;
|
|
128
|
-
return
|
|
131
|
+
return this.replaceTextWithExtras(textToProcess, regexp, EXTRAS_DEFAULT, replacement);
|
|
129
132
|
},
|
|
130
133
|
replacement: '<h1>$1</h1>',
|
|
131
134
|
},
|
|
@@ -138,19 +141,16 @@ class ExpensiMark {
|
|
|
138
141
|
name: 'video',
|
|
139
142
|
regex: MARKDOWN_VIDEO_REGEX,
|
|
140
143
|
/**
|
|
141
|
-
* @param
|
|
142
|
-
* @param
|
|
143
|
-
* @param
|
|
144
|
-
* @
|
|
145
|
-
* @return {string} Returns the HTML video tag
|
|
144
|
+
* @param extras - The extras object
|
|
145
|
+
* @param videoName - The first capture group - video name
|
|
146
|
+
* @param videoSource - The second capture group - video URL
|
|
147
|
+
* @return Returns the HTML video tag
|
|
146
148
|
*/
|
|
147
|
-
replacement: (
|
|
148
|
-
const extras = args[args.length - 1];
|
|
149
|
+
replacement: (extras, _match, videoName, videoSource) => {
|
|
149
150
|
const extraAttrs = extras && extras.videoAttributeCache && extras.videoAttributeCache[videoSource];
|
|
150
151
|
return `<video data-expensify-source="${str_1.default.sanitizeURL(videoSource)}" ${extraAttrs || ''}>${videoName ? `${videoName}` : ''}</video>`;
|
|
151
152
|
},
|
|
152
|
-
rawInputReplacement: (
|
|
153
|
-
const extras = args[args.length - 1];
|
|
153
|
+
rawInputReplacement: (extras, _match, videoName, videoSource) => {
|
|
154
154
|
const extraAttrs = extras && extras.videoAttributeCache && extras.videoAttributeCache[videoSource];
|
|
155
155
|
return `<video data-expensify-source="${str_1.default.sanitizeURL(videoSource)}" data-raw-href="${videoSource}" data-link-variant="${typeof videoName === 'string' ? 'labeled' : 'auto'}" ${extraAttrs || ''}>${videoName ? `${videoName}` : ''}</video>`;
|
|
156
156
|
},
|
|
@@ -165,8 +165,8 @@ class ExpensiMark {
|
|
|
165
165
|
{
|
|
166
166
|
name: 'image',
|
|
167
167
|
regex: MARKDOWN_IMAGE_REGEX,
|
|
168
|
-
replacement: (
|
|
169
|
-
rawInputReplacement: (
|
|
168
|
+
replacement: (_extras, _match, g1, g2) => `<img src="${str_1.default.sanitizeURL(g2)}"${g1 ? ` alt="${this.escapeAttributeContent(g1)}"` : ''} />`,
|
|
169
|
+
rawInputReplacement: (_extras, _match, g1, g2) => `<img src="${str_1.default.sanitizeURL(g2)}"${g1 ? ` alt="${this.escapeAttributeContent(g1)}"` : ''} data-raw-href="${g2}" data-link-variant="${typeof g1 === 'string' ? 'labeled' : 'auto'}" />`,
|
|
170
170
|
},
|
|
171
171
|
/**
|
|
172
172
|
* Converts markdown style links to anchor tags e.g. [Expensify](https://www.expensify.com)
|
|
@@ -176,13 +176,13 @@ class ExpensiMark {
|
|
|
176
176
|
{
|
|
177
177
|
name: 'link',
|
|
178
178
|
process: (textToProcess, replacement) => this.modifyTextForUrlLinks(MARKDOWN_LINK_REGEX, textToProcess, replacement),
|
|
179
|
-
replacement: (match, g1, g2) => {
|
|
179
|
+
replacement: (_extras, match, g1, g2) => {
|
|
180
180
|
if (g1.match(Constants.CONST.REG_EXP.EMOJIS) || !g1.trim()) {
|
|
181
181
|
return match;
|
|
182
182
|
}
|
|
183
183
|
return `<a href="${str_1.default.sanitizeURL(g2)}" target="_blank" rel="noreferrer noopener">${g1.trim()}</a>`;
|
|
184
184
|
},
|
|
185
|
-
rawInputReplacement: (match, g1, g2) => {
|
|
185
|
+
rawInputReplacement: (_extras, match, g1, g2) => {
|
|
186
186
|
if (g1.match(Constants.CONST.REG_EXP.EMOJIS) || !g1.trim()) {
|
|
187
187
|
return match;
|
|
188
188
|
}
|
|
@@ -199,7 +199,7 @@ class ExpensiMark {
|
|
|
199
199
|
{
|
|
200
200
|
name: 'hereMentions',
|
|
201
201
|
regex: /([a-zA-Z0-9.!$%&+/=?^`{|}_-]?)(@here)([.!$%&+/=?^`{|}_-]?)(?=\b)(?!([\w'#%+-]*@(?:[a-z\d-]+\.)+[a-z]{2,}(?:\s|$|@here))|((?:(?!<a).)+)?<\/a>|[^<]*(<\/pre>|<\/code>))/gm,
|
|
202
|
-
replacement: (match, g1, g2, g3) => {
|
|
202
|
+
replacement: (_extras, match, g1, g2, g3) => {
|
|
203
203
|
if (!str_1.default.isValidMention(match)) {
|
|
204
204
|
return match;
|
|
205
205
|
}
|
|
@@ -229,7 +229,7 @@ class ExpensiMark {
|
|
|
229
229
|
{
|
|
230
230
|
name: 'userMentions',
|
|
231
231
|
regex: new RegExp(`(@here|[a-zA-Z0-9.!$%&+=?^\`{|}-]?)(@${Constants.CONST.REG_EXP.EMAIL_PART}|@${Constants.CONST.REG_EXP.PHONE_PART})(?!((?:(?!<a).)+)?<\\/a>|[^<]*(<\\/pre>|<\\/code>))`, 'gim'),
|
|
232
|
-
replacement: (match, g1, g2) => {
|
|
232
|
+
replacement: (_extras, match, g1, g2) => {
|
|
233
233
|
const phoneNumberRegex = new RegExp(`^${Constants.CONST.REG_EXP.PHONE_PART}$`);
|
|
234
234
|
const mention = g2.slice(1);
|
|
235
235
|
const mentionWithoutSMSDomain = str_1.default.removeSMSDomain(mention);
|
|
@@ -239,7 +239,7 @@ class ExpensiMark {
|
|
|
239
239
|
const phoneRegex = new RegExp(`^@${Constants.CONST.REG_EXP.PHONE_PART}$`);
|
|
240
240
|
return `${g1}<mention-user>${g2}${phoneRegex.test(g2) ? `@${Constants.CONST.SMS.DOMAIN}` : ''}</mention-user>`;
|
|
241
241
|
},
|
|
242
|
-
rawInputReplacement: (match, g1, g2) => {
|
|
242
|
+
rawInputReplacement: (_extras, match, g1, g2) => {
|
|
243
243
|
const phoneNumberRegex = new RegExp(`^${Constants.CONST.REG_EXP.PHONE_PART}$`);
|
|
244
244
|
const mention = g2.slice(1);
|
|
245
245
|
const mentionWithoutSMSDomain = str_1.default.removeSMSDomain(mention);
|
|
@@ -264,11 +264,11 @@ class ExpensiMark {
|
|
|
264
264
|
const regex = new RegExp(`(?![^<]*>|[^<>]*<\\/(?!h1>))([_*~]*?)${UrlPatterns.MARKDOWN_URL_REGEX}\\1(?!((?:(?!<a).)+)?<\\/a>|[^<]*(<\\/pre>|<\\/code>|.+\\/>))`, 'gi');
|
|
265
265
|
return this.modifyTextForUrlLinks(regex, textToProcess, replacement);
|
|
266
266
|
},
|
|
267
|
-
replacement: (
|
|
267
|
+
replacement: (_extras, _match, g1, g2) => {
|
|
268
268
|
const href = str_1.default.sanitizeURL(g2);
|
|
269
269
|
return `${g1}<a href="${href}" target="_blank" rel="noreferrer noopener">${g2}</a>${g1}`;
|
|
270
270
|
},
|
|
271
|
-
rawInputReplacement: (_match, g1, g2) => {
|
|
271
|
+
rawInputReplacement: (_extras, _match, g1, g2) => {
|
|
272
272
|
const href = str_1.default.sanitizeURL(g2);
|
|
273
273
|
return `${g1}<a href="${href}" data-raw-href="${g2}" data-link-variant="auto" target="_blank" rel="noreferrer noopener">${g2}</a>${g1}`;
|
|
274
274
|
},
|
|
@@ -280,23 +280,38 @@ class ExpensiMark {
|
|
|
280
280
|
// inline code blocks. A single prepending space should be stripped if it exists
|
|
281
281
|
process: (textToProcess, replacement, shouldKeepRawInput = false) => {
|
|
282
282
|
const regex = /^(?:>)+ +(?! )(?![^<]*(?:<\/pre>|<\/code>))([^\v\n\r]+)/gm;
|
|
283
|
-
const replaceFunction = (g1) => replacement(g1, shouldKeepRawInput);
|
|
284
283
|
if (shouldKeepRawInput) {
|
|
285
284
|
const rawInputRegex = /^(?:>)+ +(?! )(?![^<]*(?:<\/pre>|<\/code>))([^\v\n\r]*)/gm;
|
|
286
|
-
return
|
|
285
|
+
return this.replaceTextWithExtras(textToProcess, rawInputRegex, EXTRAS_DEFAULT, replacement);
|
|
287
286
|
}
|
|
288
287
|
return this.modifyTextForQuote(regex, textToProcess, replacement);
|
|
289
288
|
},
|
|
290
|
-
replacement: (
|
|
289
|
+
replacement: (_extras, g1) => {
|
|
290
|
+
// We want to enable 2 options of nested heading inside the blockquote: "># heading" and "> # heading".
|
|
291
|
+
// To do this we need to parse body of the quote without first space
|
|
292
|
+
const handleMatch = (match) => match;
|
|
293
|
+
const textToReplace = g1.replace(/^>( )?/gm, handleMatch);
|
|
294
|
+
const filterRules = ['heading1'];
|
|
295
|
+
// if we don't reach the max quote depth we allow the recursive call to process possible quote
|
|
296
|
+
if (this.currentQuoteDepth < this.maxQuoteDepth - 1) {
|
|
297
|
+
filterRules.push('quote');
|
|
298
|
+
this.currentQuoteDepth++;
|
|
299
|
+
}
|
|
300
|
+
const replacedText = this.replace(textToReplace, {
|
|
301
|
+
filterRules,
|
|
302
|
+
shouldEscapeText: false,
|
|
303
|
+
shouldKeepRawInput: false,
|
|
304
|
+
});
|
|
305
|
+
this.currentQuoteDepth = 0;
|
|
306
|
+
return `<blockquote>${replacedText}</blockquote>`;
|
|
307
|
+
},
|
|
308
|
+
rawInputReplacement: (_extras, g1) => {
|
|
291
309
|
// We want to enable 2 options of nested heading inside the blockquote: "># heading" and "> # heading".
|
|
292
310
|
// To do this we need to parse body of the quote without first space
|
|
293
311
|
let isStartingWithSpace = false;
|
|
294
|
-
const handleMatch = (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return '';
|
|
298
|
-
}
|
|
299
|
-
return match;
|
|
312
|
+
const handleMatch = (_match, g2) => {
|
|
313
|
+
isStartingWithSpace = !!g2;
|
|
314
|
+
return '';
|
|
300
315
|
};
|
|
301
316
|
const textToReplace = g1.replace(/^>( )?/gm, handleMatch);
|
|
302
317
|
const filterRules = ['heading1'];
|
|
@@ -308,7 +323,7 @@ class ExpensiMark {
|
|
|
308
323
|
const replacedText = this.replace(textToReplace, {
|
|
309
324
|
filterRules,
|
|
310
325
|
shouldEscapeText: false,
|
|
311
|
-
shouldKeepRawInput,
|
|
326
|
+
shouldKeepRawInput: true,
|
|
312
327
|
});
|
|
313
328
|
this.currentQuoteDepth = 0;
|
|
314
329
|
return `<blockquote>${isStartingWithSpace ? ' ' : ''}${replacedText}</blockquote>`;
|
|
@@ -322,7 +337,7 @@ class ExpensiMark {
|
|
|
322
337
|
{
|
|
323
338
|
name: 'italic',
|
|
324
339
|
regex: /(<(pre|code|a|mention-user)[^>]*>(.*?)<\/\2>)|((\b_+|\b)_((?![\s_])[\s\S]*?[^\s_](?<!\s))_(?![^\W_])(?![^<]*>)(?![^<]*(<\/pre>|<\/code>|<\/a>|<\/mention-user>)))/g,
|
|
325
|
-
replacement: (match, html, tag, content, text, extraLeadingUnderscores, textWithinUnderscores) => {
|
|
340
|
+
replacement: (_extras, match, html, tag, content, text, extraLeadingUnderscores, textWithinUnderscores) => {
|
|
326
341
|
// Skip any <pre>, <code>, <a>, <mention-user> tag contents
|
|
327
342
|
if (html) {
|
|
328
343
|
return html;
|
|
@@ -354,12 +369,12 @@ class ExpensiMark {
|
|
|
354
369
|
// for * and ~: https://www.rexegg.com/regex-boundaries.html#notb
|
|
355
370
|
name: 'bold',
|
|
356
371
|
regex: /(?<!<[^>]*)\B\*(?![^<]*(?:<\/pre>|<\/code>|<\/a>))((?![\s*])[\s\S]*?[^\s*](?<!\s))\*\B(?![^<]*>)(?![^<]*(<\/pre>|<\/code>|<\/a>))/g,
|
|
357
|
-
replacement: (match, g1) => (g1.includes('</pre>') || this.containsNonPairTag(g1) ? match : `<strong>${g1}</strong>`),
|
|
372
|
+
replacement: (_extras, match, g1) => (g1.includes('</pre>') || this.containsNonPairTag(g1) ? match : `<strong>${g1}</strong>`),
|
|
358
373
|
},
|
|
359
374
|
{
|
|
360
375
|
name: 'strikethrough',
|
|
361
376
|
regex: /(?<!<[^>]*)\B~((?![\s~])[\s\S]*?[^\s~](?<!\s))~\B(?![^<]*>)(?![^<]*(<\/pre>|<\/code>|<\/a>))/g,
|
|
362
|
-
replacement: (match, g1) => (g1.includes('</pre>') || this.containsNonPairTag(g1) ? match : `<del>${g1}</del>`),
|
|
377
|
+
replacement: (_extras, match, g1) => (g1.includes('</pre>') || this.containsNonPairTag(g1) ? match : `<del>${g1}</del>`),
|
|
363
378
|
},
|
|
364
379
|
{
|
|
365
380
|
name: 'newline',
|
|
@@ -382,7 +397,6 @@ class ExpensiMark {
|
|
|
382
397
|
/**
|
|
383
398
|
* The list of regex replacements to do on a HTML comment for converting it to markdown.
|
|
384
399
|
* Order of rules is important
|
|
385
|
-
* @type {Object[]}
|
|
386
400
|
*/
|
|
387
401
|
this.htmlToMarkdownRules = [
|
|
388
402
|
// Used to Exclude tags
|
|
@@ -445,7 +459,7 @@ class ExpensiMark {
|
|
|
445
459
|
{
|
|
446
460
|
name: 'quote',
|
|
447
461
|
regex: /<(blockquote|q)(?:"[^"]*"|'[^']*'|[^'">])*>([\s\S]*?)<\/\1>(?![^<]*(<\/pre>|<\/code>))/gi,
|
|
448
|
-
replacement: (
|
|
462
|
+
replacement: (_extras, _match, _g1, g2) => {
|
|
449
463
|
// We remove the line break before heading inside quote to avoid adding extra line
|
|
450
464
|
let resultString = g2
|
|
451
465
|
.replace(/\n?(<h1># )/g, '$1')
|
|
@@ -480,12 +494,12 @@ class ExpensiMark {
|
|
|
480
494
|
{
|
|
481
495
|
name: 'codeFence',
|
|
482
496
|
regex: /<(pre)(?:"[^"]*"|'[^']*'|[^'">])*>([\s\S]*?)(\n?)<\/\1>(?![^<]*(<\/pre>|<\/code>))/gi,
|
|
483
|
-
replacement: (
|
|
497
|
+
replacement: (_extras, _match, _g1, g2) => `\`\`\`\n${g2}\n\`\`\``,
|
|
484
498
|
},
|
|
485
499
|
{
|
|
486
500
|
name: 'anchor',
|
|
487
501
|
regex: /<(a)[^><]*href\s*=\s*(['"])(.*?)\2(?:".*?"|'.*?'|[^'"><])*>([\s\S]*?)<\/\1>(?![^<]*(<\/pre>|<\/code>))/gi,
|
|
488
|
-
replacement: (
|
|
502
|
+
replacement: (_extras, _match, _g1, _g2, g3, g4) => {
|
|
489
503
|
const email = g3.startsWith('mailto:') ? g3.slice(7) : '';
|
|
490
504
|
if (email === g4) {
|
|
491
505
|
return email;
|
|
@@ -496,7 +510,7 @@ class ExpensiMark {
|
|
|
496
510
|
{
|
|
497
511
|
name: 'image',
|
|
498
512
|
regex: /<img[^><]*src\s*=\s*(['"])(.*?)\1(?:[^><]*alt\s*=\s*(['"])(.*?)\3)?[^><]*>*(?![^<][\s\S]*?(<\/pre>|<\/code>))/gi,
|
|
499
|
-
replacement: (
|
|
513
|
+
replacement: (_extras, _match, _g1, g2, _g3, g4) => {
|
|
500
514
|
if (g4) {
|
|
501
515
|
return ``;
|
|
502
516
|
}
|
|
@@ -507,16 +521,15 @@ class ExpensiMark {
|
|
|
507
521
|
name: 'video',
|
|
508
522
|
regex: /<video[^><]*data-expensify-source\s*=\s*(['"])(\S*?)\1(.*?)>([^><]*)<\/video>*(?![^<][\s\S]*?(<\/pre>|<\/code>))/gi,
|
|
509
523
|
/**
|
|
510
|
-
* @param
|
|
511
|
-
* @param
|
|
512
|
-
* @param
|
|
513
|
-
* @param
|
|
514
|
-
* @param
|
|
515
|
-
* @param
|
|
516
|
-
* @returns
|
|
524
|
+
* @param extras - The extras object
|
|
525
|
+
* @param match The full match
|
|
526
|
+
* @param _g1 The first capture group
|
|
527
|
+
* @param videoSource - the second capture group - video source (video URL)
|
|
528
|
+
* @param videoAttrs - the third capture group - video attributes (data-expensify-width, data-expensify-height, etc...)
|
|
529
|
+
* @param videoName - the fourth capture group will be the video file name (the text between opening and closing video tags)
|
|
530
|
+
* @returns The markdown video tag
|
|
517
531
|
*/
|
|
518
|
-
replacement: (
|
|
519
|
-
const extras = args[args.length - 1];
|
|
532
|
+
replacement: (extras, _match, _g1, videoSource, videoAttrs, videoName) => {
|
|
520
533
|
if (videoAttrs && extras && extras.cacheVideoAttributes && typeof extras.cacheVideoAttributes === 'function') {
|
|
521
534
|
extras.cacheVideoAttributes(videoSource, videoAttrs);
|
|
522
535
|
}
|
|
@@ -529,7 +542,7 @@ class ExpensiMark {
|
|
|
529
542
|
{
|
|
530
543
|
name: 'reportMentions',
|
|
531
544
|
regex: /<mention-report reportID="(\d+)" *\/>/gi,
|
|
532
|
-
replacement: (
|
|
545
|
+
replacement: (extras, _match, g1, _offset, _string) => {
|
|
533
546
|
const reportToNameMap = extras.reportIDToName;
|
|
534
547
|
if (!reportToNameMap || !reportToNameMap[g1]) {
|
|
535
548
|
ExpensiMark.Log.alert('[ExpensiMark] Missing report name', { reportID: g1 });
|
|
@@ -541,14 +554,15 @@ class ExpensiMark {
|
|
|
541
554
|
{
|
|
542
555
|
name: 'userMention',
|
|
543
556
|
regex: /(?:<mention-user accountID="(\d+)" *\/>)|(?:<mention-user>(.*?)<\/mention-user>)/gi,
|
|
544
|
-
replacement: (
|
|
557
|
+
replacement: (extras, _match, g1, g2, _offset, _string) => {
|
|
558
|
+
var _a;
|
|
545
559
|
if (g1) {
|
|
546
560
|
const accountToNameMap = extras.accountIDToName;
|
|
547
561
|
if (!accountToNameMap || !accountToNameMap[g1]) {
|
|
548
562
|
ExpensiMark.Log.alert('[ExpensiMark] Missing account name', { accountID: g1 });
|
|
549
563
|
return '@Hidden';
|
|
550
564
|
}
|
|
551
|
-
return `@${extras.accountIDToName[g1]}`;
|
|
565
|
+
return `@${(_a = extras.accountIDToName) === null || _a === void 0 ? void 0 : _a[g1]}`;
|
|
552
566
|
}
|
|
553
567
|
return str_1.default.removeSMSDomain(g2);
|
|
554
568
|
},
|
|
@@ -557,7 +571,6 @@ class ExpensiMark {
|
|
|
557
571
|
/**
|
|
558
572
|
* The list of rules to covert the HTML to text.
|
|
559
573
|
* Order of rules is important
|
|
560
|
-
* @type {Object[]}
|
|
561
574
|
*/
|
|
562
575
|
this.htmlToTextRules = [
|
|
563
576
|
{
|
|
@@ -598,7 +611,7 @@ class ExpensiMark {
|
|
|
598
611
|
{
|
|
599
612
|
name: 'reportMentions',
|
|
600
613
|
regex: /<mention-report reportID="(\d+)" *\/>/gi,
|
|
601
|
-
replacement: (
|
|
614
|
+
replacement: (extras, _match, g1, _offset, _string) => {
|
|
602
615
|
const reportToNameMap = extras.reportIDToName;
|
|
603
616
|
if (!reportToNameMap || !reportToNameMap[g1]) {
|
|
604
617
|
ExpensiMark.Log.alert('[ExpensiMark] Missing report name', { reportID: g1 });
|
|
@@ -610,13 +623,14 @@ class ExpensiMark {
|
|
|
610
623
|
{
|
|
611
624
|
name: 'userMention',
|
|
612
625
|
regex: /<mention-user accountID="(\d+)" *\/>/gi,
|
|
613
|
-
replacement: (
|
|
626
|
+
replacement: (extras, _match, g1, _offset, _string) => {
|
|
627
|
+
var _a;
|
|
614
628
|
const accountToNameMap = extras.accountIDToName;
|
|
615
629
|
if (!accountToNameMap || !accountToNameMap[g1]) {
|
|
616
630
|
ExpensiMark.Log.alert('[ExpensiMark] Missing account name', { accountID: g1 });
|
|
617
631
|
return '@Hidden';
|
|
618
632
|
}
|
|
619
|
-
return `@${extras.accountIDToName[g1]}`;
|
|
633
|
+
return `@${(_a = extras.accountIDToName) === null || _a === void 0 ? void 0 : _a[g1]}`;
|
|
620
634
|
},
|
|
621
635
|
},
|
|
622
636
|
{
|
|
@@ -627,31 +641,34 @@ class ExpensiMark {
|
|
|
627
641
|
];
|
|
628
642
|
/**
|
|
629
643
|
* The list of rules that we have to exclude in shouldKeepWhitespaceRules list.
|
|
630
|
-
* @type {Object[]}
|
|
631
644
|
*/
|
|
632
645
|
this.whitespaceRulesToDisable = ['newline', 'replacepre', 'replacebr', 'replaceh1br'];
|
|
633
646
|
/**
|
|
634
647
|
* The list of rules that have to be applied when shouldKeepWhitespace flag is true.
|
|
635
|
-
* @param
|
|
636
|
-
* @returns
|
|
648
|
+
* @param rule - The rule to check.
|
|
649
|
+
* @returns true if the rule should be applied, otherwise false.
|
|
637
650
|
*/
|
|
638
651
|
this.filterRules = (rule) => !this.whitespaceRulesToDisable.includes(rule.name);
|
|
639
652
|
/**
|
|
640
653
|
* Filters rules to determine which should keep whitespace.
|
|
641
|
-
* @returns
|
|
654
|
+
* @returns The filtered rules.
|
|
642
655
|
*/
|
|
643
656
|
this.shouldKeepWhitespaceRules = this.rules.filter(this.filterRules);
|
|
644
657
|
/**
|
|
645
658
|
* maxQuoteDepth is the maximum depth of nested quotes that we want to support.
|
|
646
|
-
* @type {Number}
|
|
647
659
|
*/
|
|
648
660
|
this.maxQuoteDepth = 3;
|
|
649
661
|
/**
|
|
650
662
|
* currentQuoteDepth is the current depth of nested quotes that we are processing.
|
|
651
|
-
* @type {Number}
|
|
652
663
|
*/
|
|
653
664
|
this.currentQuoteDepth = 0;
|
|
654
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* Retrieves the HTML ruleset based on the provided filter rules, disabled rules, and shouldKeepRawInput flag.
|
|
668
|
+
* @param filterRules - An array of rule names to filter the ruleset.
|
|
669
|
+
* @param disabledRules - An array of rule names to disable in the ruleset.
|
|
670
|
+
* @param shouldKeepRawInput - A boolean flag indicating whether to keep raw input.
|
|
671
|
+
*/
|
|
655
672
|
getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput) {
|
|
656
673
|
let rules = this.rules;
|
|
657
674
|
const hasRuleName = (rule) => filterRules.includes(rule.name);
|
|
@@ -670,17 +687,15 @@ class ExpensiMark {
|
|
|
670
687
|
/**
|
|
671
688
|
* Replaces markdown with html elements
|
|
672
689
|
*
|
|
673
|
-
* @param
|
|
674
|
-
* @param
|
|
675
|
-
* @param
|
|
690
|
+
* @param text - Text to parse as markdown
|
|
691
|
+
* @param [options] - Options to customize the markdown parser
|
|
692
|
+
* @param [options.filterRules=[]] - An array of name of rules as defined in this class.
|
|
676
693
|
* If not provided, all available rules will be applied.
|
|
677
|
-
* @param
|
|
678
|
-
* @param
|
|
694
|
+
* @param [options.shouldEscapeText=true] - Whether or not the text should be escaped
|
|
695
|
+
* @param [options.disabledRules=[]] - An array of name of rules as defined in this class.
|
|
679
696
|
* If not provided, all available rules will be applied. If provided, the rules in the array will be skipped.
|
|
680
|
-
*
|
|
681
|
-
* @returns {String}
|
|
682
697
|
*/
|
|
683
|
-
replace(text, { filterRules = [], shouldEscapeText = true, shouldKeepRawInput = false, disabledRules = [], extras } = {}) {
|
|
698
|
+
replace(text, { filterRules = [], shouldEscapeText = true, shouldKeepRawInput = false, disabledRules = [], extras = EXTRAS_DEFAULT } = {}) {
|
|
684
699
|
// This ensures that any html the user puts into the comment field shows as raw html
|
|
685
700
|
let replacedText = shouldEscapeText ? Utils.escape(text) : text;
|
|
686
701
|
const rules = this.getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput);
|
|
@@ -689,13 +704,12 @@ class ExpensiMark {
|
|
|
689
704
|
if (rule.pre) {
|
|
690
705
|
replacedText = rule.pre(replacedText);
|
|
691
706
|
}
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
replacedText = rule.process(replacedText, replacementFnWithExtraParams, shouldKeepRawInput);
|
|
707
|
+
const replacement = shouldKeepRawInput && rule.rawInputReplacement ? rule.rawInputReplacement : rule.replacement;
|
|
708
|
+
if ('process' in rule) {
|
|
709
|
+
replacedText = rule.process(replacedText, replacement, shouldKeepRawInput);
|
|
696
710
|
}
|
|
697
711
|
else {
|
|
698
|
-
replacedText =
|
|
712
|
+
replacedText = this.replaceTextWithExtras(replacedText, rule.regex, extras, replacement);
|
|
699
713
|
}
|
|
700
714
|
// Post-process text after applying regex
|
|
701
715
|
if (rule.post) {
|
|
@@ -706,8 +720,7 @@ class ExpensiMark {
|
|
|
706
720
|
rules.forEach(processRule);
|
|
707
721
|
}
|
|
708
722
|
catch (e) {
|
|
709
|
-
|
|
710
|
-
console.warn('Error replacing text with html in ExpensiMark.replace', { error: e });
|
|
723
|
+
ExpensiMark.Log.alert('Error replacing text with html in ExpensiMark.replace', { error: e });
|
|
711
724
|
// We want to return text without applying rules if exception occurs during replacing
|
|
712
725
|
return shouldEscapeText ? Utils.escape(text) : text;
|
|
713
726
|
}
|
|
@@ -715,12 +728,6 @@ class ExpensiMark {
|
|
|
715
728
|
}
|
|
716
729
|
/**
|
|
717
730
|
* Checks matched URLs for validity and replace valid links with html elements
|
|
718
|
-
*
|
|
719
|
-
* @param {RegExp} regex
|
|
720
|
-
* @param {String} textToCheck
|
|
721
|
-
* @param {Function} replacement
|
|
722
|
-
*
|
|
723
|
-
* @returns {String}
|
|
724
731
|
*/
|
|
725
732
|
modifyTextForUrlLinks(regex, textToCheck, replacement) {
|
|
726
733
|
let match = regex.exec(textToCheck);
|
|
@@ -803,7 +810,7 @@ class ExpensiMark {
|
|
|
803
810
|
filterRules: ['bold', 'strikethrough', 'italic'],
|
|
804
811
|
shouldEscapeText: false,
|
|
805
812
|
});
|
|
806
|
-
replacedText = replacedText.concat(replacement(match[0], linkText, url));
|
|
813
|
+
replacedText = replacedText.concat(replacement(EXTRAS_DEFAULT, match[0], linkText, url));
|
|
807
814
|
}
|
|
808
815
|
startIndex = match.index + match[0].length;
|
|
809
816
|
// Now we move to the next match that the js regex found in the text
|
|
@@ -816,13 +823,6 @@ class ExpensiMark {
|
|
|
816
823
|
}
|
|
817
824
|
/**
|
|
818
825
|
* Checks matched Emails for validity and replace valid links with html elements
|
|
819
|
-
*
|
|
820
|
-
* @param {RegExp} regex
|
|
821
|
-
* @param {String} textToCheck
|
|
822
|
-
* @param {Function} replacement
|
|
823
|
-
* @param {Boolean} shouldKeepRawInput
|
|
824
|
-
*
|
|
825
|
-
* @returns {String}
|
|
826
826
|
*/
|
|
827
827
|
modifyTextForEmailLinks(regex, textToCheck, replacement, shouldKeepRawInput) {
|
|
828
828
|
let match = regex.exec(textToCheck);
|
|
@@ -836,8 +836,8 @@ class ExpensiMark {
|
|
|
836
836
|
filterRules: ['bold', 'strikethrough', 'italic'],
|
|
837
837
|
shouldEscapeText: false,
|
|
838
838
|
});
|
|
839
|
-
//
|
|
840
|
-
const replacedMatch = shouldKeepRawInput ? replacement(match[0], linkText, match[2], match[3]) : replacement(match[0], linkText, match[3]);
|
|
839
|
+
// rawInputReplacement needs to be called with additional parameters from match
|
|
840
|
+
const replacedMatch = shouldKeepRawInput ? replacement(EXTRAS_DEFAULT, match[0], linkText, match[2], match[3]) : replacement(EXTRAS_DEFAULT, match[0], linkText, match[3]);
|
|
841
841
|
replacedText = replacedText.concat(replacedMatch);
|
|
842
842
|
startIndex = match.index + match[0].length;
|
|
843
843
|
// Now we move to the next match that the js regex found in the text
|
|
@@ -854,9 +854,6 @@ class ExpensiMark {
|
|
|
854
854
|
* 2. The text does not end with a new line.
|
|
855
855
|
* 3. The text does not have quote mark '>' .
|
|
856
856
|
* 4. It's not the last element in the string.
|
|
857
|
-
*
|
|
858
|
-
* @param {String} htmlString
|
|
859
|
-
* @returns {String}
|
|
860
857
|
*/
|
|
861
858
|
replaceBlockElementWithNewLine(htmlString) {
|
|
862
859
|
// eslint-disable-next-line max-len
|
|
@@ -888,13 +885,8 @@ class ExpensiMark {
|
|
|
888
885
|
}
|
|
889
886
|
/**
|
|
890
887
|
* Replaces HTML with markdown
|
|
891
|
-
*
|
|
892
|
-
* @param {String} htmlString
|
|
893
|
-
* @param {Object} extras
|
|
894
|
-
*
|
|
895
|
-
* @returns {String}
|
|
896
888
|
*/
|
|
897
|
-
htmlToMarkdown(htmlString, extras =
|
|
889
|
+
htmlToMarkdown(htmlString, extras = EXTRAS_DEFAULT) {
|
|
898
890
|
let generatedMarkdown = htmlString;
|
|
899
891
|
const body = /<(body)(?:"[^"]*"|'[^']*'|[^'"><])*>(?:\n|\r\n)?([\s\S]*?)(?:\n|\r\n)?<\/\1>(?![^<]*(<\/pre>|<\/code>))/im;
|
|
900
892
|
const parseBodyTag = generatedMarkdown.match(body);
|
|
@@ -907,27 +899,18 @@ class ExpensiMark {
|
|
|
907
899
|
if (rule.pre) {
|
|
908
900
|
generatedMarkdown = rule.pre(generatedMarkdown);
|
|
909
901
|
}
|
|
910
|
-
|
|
911
|
-
const replacementFunction = Utils.isFunction(rule.replacement) ? (...args) => rule.replacement(...args, extras) : rule.replacement;
|
|
912
|
-
generatedMarkdown = generatedMarkdown.replace(rule.regex, replacementFunction);
|
|
902
|
+
generatedMarkdown = this.replaceTextWithExtras(generatedMarkdown, rule.regex, extras, rule.replacement);
|
|
913
903
|
};
|
|
914
904
|
this.htmlToMarkdownRules.forEach(processRule);
|
|
915
905
|
return str_1.default.htmlDecode(this.replaceBlockElementWithNewLine(generatedMarkdown));
|
|
916
906
|
}
|
|
917
907
|
/**
|
|
918
908
|
* Convert HTML to text
|
|
919
|
-
*
|
|
920
|
-
* @param {String} htmlString
|
|
921
|
-
* @param {Object} extras
|
|
922
|
-
*
|
|
923
|
-
* @returns {String}
|
|
924
909
|
*/
|
|
925
|
-
htmlToText(htmlString, extras =
|
|
910
|
+
htmlToText(htmlString, extras = EXTRAS_DEFAULT) {
|
|
926
911
|
let replacedText = htmlString;
|
|
927
912
|
const processRule = (rule) => {
|
|
928
|
-
|
|
929
|
-
const replacementFunction = Utils.isFunction(rule.replacement) ? (...args) => rule.replacement(...args, extras) : rule.replacement;
|
|
930
|
-
replacedText = replacedText.replace(rule.regex, replacementFunction);
|
|
913
|
+
replacedText = this.replaceTextWithExtras(replacedText, rule.regex, extras, rule.replacement);
|
|
931
914
|
};
|
|
932
915
|
this.htmlToTextRules.forEach(processRule);
|
|
933
916
|
// Unescaping because the text is escaped in 'replace' function
|
|
@@ -937,12 +920,6 @@ class ExpensiMark {
|
|
|
937
920
|
}
|
|
938
921
|
/**
|
|
939
922
|
* Modify text for Quotes replacing chevrons with html elements
|
|
940
|
-
*
|
|
941
|
-
* @param {RegExp} regex
|
|
942
|
-
* @param {String} textToCheck
|
|
943
|
-
* @param {Function} replacement
|
|
944
|
-
*
|
|
945
|
-
* @returns {String}
|
|
946
923
|
*/
|
|
947
924
|
modifyTextForQuote(regex, textToCheck, replacement) {
|
|
948
925
|
let replacedText = '';
|
|
@@ -994,12 +971,6 @@ class ExpensiMark {
|
|
|
994
971
|
}
|
|
995
972
|
/**
|
|
996
973
|
* Format the content of blockquote if the text matches the regex or else just return the original text
|
|
997
|
-
*
|
|
998
|
-
* @param {RegExp} regex
|
|
999
|
-
* @param {String} textToCheck
|
|
1000
|
-
* @param {Function} replacement
|
|
1001
|
-
*
|
|
1002
|
-
* @returns {String}
|
|
1003
974
|
*/
|
|
1004
975
|
formatTextForQuote(regex, textToCheck, replacement) {
|
|
1005
976
|
if (textToCheck.match(regex)) {
|
|
@@ -1014,16 +985,12 @@ class ExpensiMark {
|
|
|
1014
985
|
let textToFormat = textToCheck.split('\n').map(formatRow).join('\n');
|
|
1015
986
|
// Remove leading and trailing line breaks
|
|
1016
987
|
textToFormat = textToFormat.replace(/^\n+|\n+$/g, '');
|
|
1017
|
-
return replacement(textToFormat);
|
|
988
|
+
return replacement(EXTRAS_DEFAULT, textToFormat);
|
|
1018
989
|
}
|
|
1019
990
|
return textToCheck;
|
|
1020
991
|
}
|
|
1021
992
|
/**
|
|
1022
993
|
* Check if the input text includes only the open or the close tag of an element.
|
|
1023
|
-
*
|
|
1024
|
-
* @param {String} textToCheck - Text to check
|
|
1025
|
-
*
|
|
1026
|
-
* @returns {Boolean}
|
|
1027
994
|
*/
|
|
1028
995
|
containsNonPairTag(textToCheck) {
|
|
1029
996
|
// Create a regular expression to match HTML tags
|
|
@@ -1053,8 +1020,7 @@ class ExpensiMark {
|
|
|
1053
1020
|
return tagStack.length !== 0;
|
|
1054
1021
|
}
|
|
1055
1022
|
/**
|
|
1056
|
-
* @
|
|
1057
|
-
* @returns {Array} or undefined if exception occurs when executing regex matching
|
|
1023
|
+
* @returns array or undefined if exception occurs when executing regex matching
|
|
1058
1024
|
*/
|
|
1059
1025
|
extractLinksInMarkdownComment(comment) {
|
|
1060
1026
|
try {
|
|
@@ -1068,17 +1034,12 @@ class ExpensiMark {
|
|
|
1068
1034
|
return links;
|
|
1069
1035
|
}
|
|
1070
1036
|
catch (e) {
|
|
1071
|
-
|
|
1072
|
-
console.warn('Error parsing url in ExpensiMark.extractLinksInMarkdownComment', { error: e });
|
|
1037
|
+
ExpensiMark.Log.alert('Error parsing url in ExpensiMark.extractLinksInMarkdownComment', { error: e });
|
|
1073
1038
|
return undefined;
|
|
1074
1039
|
}
|
|
1075
1040
|
}
|
|
1076
1041
|
/**
|
|
1077
1042
|
* Compares two markdown comments and returns a list of the links removed in a new comment.
|
|
1078
|
-
*
|
|
1079
|
-
* @param {String} oldComment
|
|
1080
|
-
* @param {String} newComment
|
|
1081
|
-
* @returns {Array}
|
|
1082
1043
|
*/
|
|
1083
1044
|
getRemovedMarkdownLinks(oldComment, newComment) {
|
|
1084
1045
|
const linksInOld = this.extractLinksInMarkdownComment(oldComment);
|
|
@@ -1087,8 +1048,8 @@ class ExpensiMark {
|
|
|
1087
1048
|
}
|
|
1088
1049
|
/**
|
|
1089
1050
|
* Escapes the content of an HTML attribute value
|
|
1090
|
-
* @param
|
|
1091
|
-
* @returns
|
|
1051
|
+
* @param content - string content that possible contains HTML
|
|
1052
|
+
* @returns original MD content escaped for use in HTML attribute value
|
|
1092
1053
|
*/
|
|
1093
1054
|
escapeAttributeContent(content) {
|
|
1094
1055
|
let originalContent = this.htmlToMarkdown(content);
|
|
@@ -1100,9 +1061,24 @@ class ExpensiMark {
|
|
|
1100
1061
|
originalContent = str_1.default.replaceAll(originalContent, '\n', '');
|
|
1101
1062
|
return Utils.escape(originalContent);
|
|
1102
1063
|
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Replaces text with a replacement based on a regex
|
|
1066
|
+
* @param text - The text to replace
|
|
1067
|
+
* @param regexp - The regex to match
|
|
1068
|
+
* @param extras - The extras object
|
|
1069
|
+
* @param replacement - The replacement string or function
|
|
1070
|
+
* @returns The replaced text
|
|
1071
|
+
*/
|
|
1072
|
+
replaceTextWithExtras(text, regexp, extras, replacement) {
|
|
1073
|
+
if (typeof replacement === 'function') {
|
|
1074
|
+
// if the replacement is a function, we pass the extras object to it
|
|
1075
|
+
return text.replace(regexp, (...args) => replacement(extras, ...args));
|
|
1076
|
+
}
|
|
1077
|
+
return text.replace(regexp, replacement);
|
|
1078
|
+
}
|
|
1103
1079
|
}
|
|
1104
1080
|
ExpensiMark.Log = new Logger_1.default({
|
|
1105
|
-
serverLoggingCallback: () =>
|
|
1081
|
+
serverLoggingCallback: () => undefined,
|
|
1106
1082
|
// eslint-disable-next-line no-console
|
|
1107
1083
|
clientLoggingCallback: (message) => console.warn(message),
|
|
1108
1084
|
isDebug: true,
|