expensify-common
Advanced tools
Comparing version 2.0.18 to 2.0.19
@@ -1,100 +0,99 @@ | ||
import type Logger from './Logger'; | ||
declare type Replacement = (...args: string[], extras?: ExtrasObject) => string; | ||
declare type Name = | ||
| 'codeFence' | ||
| 'inlineCodeBlock' | ||
| 'email' | ||
| 'link' | ||
| 'hereMentions' | ||
| 'userMentions' | ||
| 'reportMentions' | ||
| 'autoEmail' | ||
| 'autolink' | ||
| 'quote' | ||
| 'italic' | ||
| 'bold' | ||
| 'strikethrough' | ||
| 'heading1' | ||
| 'newline' | ||
| 'replacepre' | ||
| 'listItem' | ||
| 'exclude' | ||
| 'anchor' | ||
| 'breakline' | ||
| 'blockquoteWrapHeadingOpen' | ||
| 'blockquoteWrapHeadingClose' | ||
| 'blockElementOpen' | ||
| 'blockElementClose' | ||
| 'stripTag'; | ||
declare type Rule = { | ||
name: Name; | ||
process?: (textToProcess: string, replacement: Replacement) => string; | ||
regex?: RegExp; | ||
replacement: Replacement | string; | ||
pre?: (input: string) => string; | ||
post?: (input: string) => string; | ||
}; | ||
declare type ExtrasObject = { | ||
import Logger from './Logger'; | ||
type Extras = { | ||
reportIDToName?: Record<string, string>; | ||
accountIDToName?: Record<string, string>; | ||
cacheVideoAttributes?: (vidSource: string, attrs: string) => void; | ||
}; | ||
declare type ExtraParamsForReplaceFunc = { | ||
videoAttributeCache?: Record<string, string>; | ||
}; | ||
type ReplacementFn = (extras: Extras, ...matches: string[]) => string; | ||
type Replacement = ReplacementFn | string; | ||
type ProcessFn = (textToProcess: string, replacement: Replacement, shouldKeepRawInput: boolean) => string; | ||
type CommonRule = { | ||
name: string; | ||
replacement: Replacement; | ||
rawInputReplacement?: Replacement; | ||
pre?: (input: string) => string; | ||
post?: (input: string) => string; | ||
}; | ||
type RuleWithRegex = CommonRule & { | ||
regex: RegExp; | ||
}; | ||
type RuleWithProcess = CommonRule & { | ||
process: ProcessFn; | ||
}; | ||
type Rule = RuleWithRegex | RuleWithProcess; | ||
type ReplaceOptions = { | ||
extras?: Extras; | ||
filterRules?: string[]; | ||
disabledRules?: string[]; | ||
shouldEscapeText?: boolean; | ||
shouldKeepRawInput?: boolean; | ||
}; | ||
export default class ExpensiMark { | ||
static Log: Logger; | ||
/** | ||
* Set the logger to use for logging inside of the ExpensiMark class | ||
* @param logger - The logger object to use | ||
*/ | ||
static setLogger(logger: Logger): void; | ||
/** Rules to apply to the text */ | ||
rules: Rule[]; | ||
htmlToMarkdownRules: Rule[]; | ||
htmlToTextRules: Rule[]; | ||
/** | ||
* The list of regex replacements to do on a HTML comment for converting it to markdown. | ||
* Order of rules is important | ||
*/ | ||
htmlToMarkdownRules: RuleWithRegex[]; | ||
/** | ||
* The list of rules to covert the HTML to text. | ||
* Order of rules is important | ||
*/ | ||
htmlToTextRules: RuleWithRegex[]; | ||
/** | ||
* The list of rules that we have to exclude in shouldKeepWhitespaceRules list. | ||
*/ | ||
whitespaceRulesToDisable: string[]; | ||
/** | ||
* The list of rules that have to be applied when shouldKeepWhitespace flag is true. | ||
*/ | ||
filterRules: (rule: Rule) => boolean; | ||
/** | ||
* Filters rules to determine which should keep whitespace. | ||
*/ | ||
shouldKeepWhitespaceRules: Rule[]; | ||
/** | ||
* maxQuoteDepth is the maximum depth of nested quotes that we want to support. | ||
*/ | ||
maxQuoteDepth: number; | ||
/** | ||
* currentQuoteDepth is the current depth of nested quotes that we are processing. | ||
*/ | ||
currentQuoteDepth: number; | ||
constructor(); | ||
/** | ||
* Retrieves the HTML ruleset based on the provided filter rules, disabled rules, and shouldKeepRawInput flag. | ||
* @param filterRules - An array of rule names to filter the ruleset. | ||
* @param disabledRules - An array of rule names to disable in the ruleset. | ||
* @param shouldKeepRawInput - A boolean flag indicating whether to keep raw input. | ||
*/ | ||
getHtmlRuleset(filterRules: string[], disabledRules: string[], shouldKeepRawInput: boolean): Rule[]; | ||
/** | ||
* Replaces markdown with html elements | ||
* | ||
* @param text - Text to parse as markdown | ||
* @param options - Options to customize the markdown parser | ||
* @param options.filterRules=[] - An array of name of rules as defined in this class. | ||
* If not provided, all available rules will be applied. If provided, only the rules in the array will be applied. | ||
* @param options.disabledRules=[] - An array of name of rules as defined in this class. | ||
* @param [options] - Options to customize the markdown parser | ||
* @param [options.filterRules=[]] - An array of name of rules as defined in this class. | ||
* If not provided, all available rules will be applied. | ||
* @param [options.shouldEscapeText=true] - Whether or not the text should be escaped | ||
* @param [options.disabledRules=[]] - An array of name of rules as defined in this class. | ||
* If not provided, all available rules will be applied. If provided, the rules in the array will be skipped. | ||
* @param options.shouldEscapeText=true - Whether or not the text should be escaped | ||
* @param options.shouldKeepRawInput=false - Whether or not the raw input should be kept and returned | ||
*/ | ||
replace( | ||
text: string, | ||
{ | ||
filterRules, | ||
shouldEscapeText, | ||
shouldKeepRawInput, | ||
extras, | ||
}?: { | ||
filterRules?: Name[]; | ||
disabledRules?: Name[]; | ||
shouldEscapeText?: boolean; | ||
shouldKeepRawInput?: boolean; | ||
extras?: ExtraParamsForReplaceFunc; | ||
}, | ||
): string; | ||
replace(text: string, { filterRules, shouldEscapeText, shouldKeepRawInput, disabledRules, extras }?: ReplaceOptions): string; | ||
/** | ||
* Checks matched URLs for validity and replace valid links with html elements | ||
* | ||
* @param regex | ||
* @param textToCheck | ||
* @param replacement | ||
*/ | ||
modifyTextForUrlLinks(regex: RegExp, textToCheck: string, replacement: Replacement): string; | ||
modifyTextForUrlLinks(regex: RegExp, textToCheck: string, replacement: ReplacementFn): string; | ||
/** | ||
* Checks matched Emails for validity and replace valid links with html elements | ||
* | ||
* @param regex | ||
* @param textToCheck | ||
* @param replacement | ||
*/ | ||
modifyTextForEmailLinks(regex: RegExp, textToCheck: string, replacement: Replacement): string; | ||
modifyTextForEmailLinks(regex: RegExp, textToCheck: string, replacement: ReplacementFn, shouldKeepRawInput: boolean): string; | ||
/** | ||
@@ -106,4 +105,2 @@ * replace block element with '\n' if : | ||
* 4. It's not the last element in the string. | ||
* | ||
* @param htmlString | ||
*/ | ||
@@ -113,45 +110,44 @@ replaceBlockElementWithNewLine(htmlString: string): string; | ||
* Replaces HTML with markdown | ||
* | ||
* @param htmlString | ||
* @param extras | ||
*/ | ||
htmlToMarkdown(htmlString: string, extras?: ExtrasObject): string; | ||
htmlToMarkdown(htmlString: string, extras?: Extras): string; | ||
/** | ||
* Convert HTML to text | ||
* | ||
* @param htmlString | ||
* @param extras | ||
*/ | ||
htmlToText(htmlString: string, extras?: ExtrasObject): string; | ||
htmlToText(htmlString: string, extras?: Extras): string; | ||
/** | ||
* Modify text for Quotes replacing chevrons with html elements | ||
* | ||
* @param regex | ||
* @param textToCheck | ||
* @param replacement | ||
*/ | ||
modifyTextForQuote(regex: RegExp, textToCheck: string, replacement: Replacement): string; | ||
modifyTextForQuote(regex: RegExp, textToCheck: string, replacement: ReplacementFn): string; | ||
/** | ||
* Format the content of blockquote if the text matches the regex or else just return the original text | ||
* | ||
* @param regex | ||
* @param textToCheck | ||
* @param replacement | ||
*/ | ||
formatTextForQuote(regex: RegExp, textToCheck: string, replacement: Replacement): string; | ||
formatTextForQuote(regex: RegExp, textToCheck: string, replacement: ReplacementFn): string; | ||
/** | ||
* Check if the input text includes only the open or the close tag of an element. | ||
* | ||
* @param textToCheck - Text to check | ||
*/ | ||
containsNonPairTag(textToCheck: string): boolean; | ||
/** | ||
* @returns array or undefined if exception occurs when executing regex matching | ||
*/ | ||
extractLinksInMarkdownComment(comment: string): string[] | undefined; | ||
/** | ||
* Compares two markdown comments and returns a list of the links removed in a new comment. | ||
* | ||
* @param oldComment | ||
* @param newComment | ||
*/ | ||
getRemovedMarkdownLinks(oldComment: string, newComment: string): string[]; | ||
/** | ||
* Escapes the content of an HTML attribute value | ||
* @param content - string content that possible contains HTML | ||
* @returns original MD content escaped for use in HTML attribute value | ||
*/ | ||
escapeAttributeContent(content: string): string; | ||
/** | ||
* Replaces text with a replacement based on a regex | ||
* @param text - The text to replace | ||
* @param regexp - The regex to match | ||
* @param extras - The extras object | ||
* @param replacement - The replacement string or function | ||
* @returns The replaced text | ||
*/ | ||
replaceTextWithExtras(text: string, regexp: RegExp, extras: Extras, replacement: Replacement): string; | ||
} | ||
export {}; |
@@ -34,2 +34,3 @@ "use strict"; | ||
const Utils = __importStar(require("./utils")); | ||
const EXTRAS_DEFAULT = {}; | ||
const MARKDOWN_LINK_REGEX = new RegExp(`\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)]\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, 'gi'); | ||
@@ -42,3 +43,3 @@ const MARKDOWN_IMAGE_REGEX = new RegExp(`\\!(?:\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)])?\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, 'gi'); | ||
* Set the logger to use for logging inside of the ExpensiMark class | ||
* @param {Object} logger - The logger object to use | ||
* @param logger - The logger object to use | ||
*/ | ||
@@ -50,6 +51,8 @@ static setLogger(logger) { | ||
/** | ||
* The list of rules that we have to exclude in shouldKeepWhitespaceRules list. | ||
*/ | ||
this.whitespaceRulesToDisable = ['newline', 'replacepre', 'replacebr', 'replaceh1br']; | ||
/** | ||
* The list of regex replacements to do on a comment. Check the link regex is first so links are processed | ||
* before other delimiters | ||
* | ||
* @type {Object[]} | ||
*/ | ||
@@ -61,3 +64,3 @@ this.rules = [ | ||
regex: Constants.CONST.REG_EXP.EMOJI_RULE, | ||
replacement: (match) => `<emoji>${match}</emoji>`, | ||
replacement: (_extras, match) => `<emoji>${match}</emoji>`, | ||
}, | ||
@@ -78,7 +81,7 @@ /** | ||
// will create styling issues so use   | ||
replacement: (match, __, textWithinFences) => { | ||
replacement: (_extras, _match, _g1, textWithinFences) => { | ||
const group = textWithinFences.replace(/(?:(?![\n\r])\s)/g, ' '); | ||
return `<pre>${group}</pre>`; | ||
}, | ||
rawInputReplacement: (match, __, textWithinFences) => { | ||
rawInputReplacement: (_extras, _match, _g1, textWithinFences) => { | ||
const group = textWithinFences.replace(/(?:(?![\n\r])\s)/g, ' ').replace(/<emoji>|<\/emoji>/g, ''); | ||
@@ -111,3 +114,3 @@ return `<pre>${group}</pre>`; | ||
}, | ||
replacement: (match, g1, g2) => { | ||
replacement: (_extras, match, g1, g2) => { | ||
if (g1.match(Constants.CONST.REG_EXP.EMOJIS) || !g1.trim()) { | ||
@@ -121,3 +124,3 @@ return match; | ||
}, | ||
rawInputReplacement: (match, g1, g2, g3) => { | ||
rawInputReplacement: (_extras, match, g1, g2, g3) => { | ||
if (g1.match(Constants.CONST.REG_EXP.EMOJIS) || !g1.trim()) { | ||
@@ -135,3 +138,3 @@ return match; | ||
const regexp = shouldKeepRawInput ? /^# ( *(?! )(?:(?!<pre>|\n|\r\n).)+)/gm : /^# +(?! )((?:(?!<pre>|\n|\r\n).)+)/gm; | ||
return textToProcess.replace(regexp, replacement); | ||
return this.replaceTextWithExtras(textToProcess, regexp, EXTRAS_DEFAULT, replacement); | ||
}, | ||
@@ -149,15 +152,12 @@ replacement: '<h1>$1</h1>', | ||
/** | ||
* @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 | ||
* @param extras - The extras object | ||
* @param videoName - The first capture group - video name | ||
* @param videoSource - The second capture group - video URL | ||
* @return Returns the HTML video tag | ||
*/ | ||
replacement: (match, videoName, videoSource, ...args) => { | ||
const extras = args[args.length - 1]; | ||
replacement: (extras, _match, videoName, videoSource) => { | ||
const extraAttrs = extras && extras.videoAttributeCache && extras.videoAttributeCache[videoSource]; | ||
return `<video data-expensify-source="${str_1.default.sanitizeURL(videoSource)}" ${extraAttrs || ''}>${videoName ? `${videoName}` : ''}</video>`; | ||
}, | ||
rawInputReplacement: (match, videoName, videoSource, ...args) => { | ||
const extras = args[args.length - 1]; | ||
rawInputReplacement: (extras, _match, videoName, videoSource) => { | ||
const extraAttrs = extras && extras.videoAttributeCache && extras.videoAttributeCache[videoSource]; | ||
@@ -177,4 +177,4 @@ 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>`; | ||
regex: MARKDOWN_IMAGE_REGEX, | ||
replacement: (match, g1, g2) => `<img src="${str_1.default.sanitizeURL(g2)}"${g1 ? ` alt="${this.escapeAttributeContent(g1)}"` : ''} />`, | ||
rawInputReplacement: (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'}" />`, | ||
replacement: (_extras, _match, g1, g2) => `<img src="${str_1.default.sanitizeURL(g2)}"${g1 ? ` alt="${this.escapeAttributeContent(g1)}"` : ''} />`, | ||
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'}" />`, | ||
}, | ||
@@ -189,3 +189,3 @@ /** | ||
process: (textToProcess, replacement) => this.modifyTextForUrlLinks(MARKDOWN_LINK_REGEX, textToProcess, replacement), | ||
replacement: (match, g1, g2) => { | ||
replacement: (_extras, match, g1, g2) => { | ||
if (g1.match(Constants.CONST.REG_EXP.EMOJIS) || !g1.trim()) { | ||
@@ -196,3 +196,3 @@ return match; | ||
}, | ||
rawInputReplacement: (match, g1, g2) => { | ||
rawInputReplacement: (_extras, match, g1, g2) => { | ||
if (g1.match(Constants.CONST.REG_EXP.EMOJIS) || !g1.trim()) { | ||
@@ -214,3 +214,3 @@ return match; | ||
regex: /([a-zA-Z0-9.!$%&+/=?^`{|}_-]?)(@here)([.!$%&+/=?^`{|}_-]?)(?=\b)(?!([\w'#%+-]*@(?:[a-z\d-]+\.)+[a-z]{2,}(?:\s|$|@here))|((?:(?!<a).)+)?<\/a>|[^<]*(<\/pre>|<\/code>))/gm, | ||
replacement: (match, g1, g2, g3) => { | ||
replacement: (_extras, match, g1, g2, g3) => { | ||
if (!str_1.default.isValidMention(match)) { | ||
@@ -245,3 +245,3 @@ return match; | ||
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'), | ||
replacement: (match, g1, g2) => { | ||
replacement: (_extras, match, g1, g2) => { | ||
const phoneNumberRegex = new RegExp(`^${Constants.CONST.REG_EXP.PHONE_PART}$`); | ||
@@ -256,3 +256,3 @@ const mention = g2.slice(1); | ||
}, | ||
rawInputReplacement: (match, g1, g2) => { | ||
rawInputReplacement: (_extras, match, g1, g2) => { | ||
const phoneNumberRegex = new RegExp(`^${Constants.CONST.REG_EXP.PHONE_PART}$`); | ||
@@ -282,7 +282,7 @@ const mention = g2.slice(1); | ||
}, | ||
replacement: (match, g1, g2) => { | ||
replacement: (_extras, _match, g1, g2) => { | ||
const href = str_1.default.sanitizeURL(g2); | ||
return `${g1}<a href="${href}" target="_blank" rel="noreferrer noopener">${g2}</a>${g1}`; | ||
}, | ||
rawInputReplacement: (_match, g1, g2) => { | ||
rawInputReplacement: (_extras, _match, g1, g2) => { | ||
const href = str_1.default.sanitizeURL(g2); | ||
@@ -299,19 +299,34 @@ return `${g1}<a href="${href}" data-raw-href="${g2}" data-link-variant="auto" target="_blank" rel="noreferrer noopener">${g2}</a>${g1}`; | ||
const regex = /^(?:>)+ +(?! )(?![^<]*(?:<\/pre>|<\/code>))([^\v\n\r]+)/gm; | ||
const replaceFunction = (g1) => replacement(g1, shouldKeepRawInput); | ||
if (shouldKeepRawInput) { | ||
const rawInputRegex = /^(?:>)+ +(?! )(?![^<]*(?:<\/pre>|<\/code>))([^\v\n\r]*)/gm; | ||
return textToProcess.replace(rawInputRegex, replaceFunction); | ||
return this.replaceTextWithExtras(textToProcess, rawInputRegex, EXTRAS_DEFAULT, replacement); | ||
} | ||
return this.modifyTextForQuote(regex, textToProcess, replacement); | ||
}, | ||
replacement: (g1, shouldKeepRawInput = false) => { | ||
replacement: (_extras, g1) => { | ||
// We want to enable 2 options of nested heading inside the blockquote: "># heading" and "> # heading". | ||
// To do this we need to parse body of the quote without first space | ||
const handleMatch = (match) => match; | ||
const textToReplace = g1.replace(/^>( )?/gm, handleMatch); | ||
const filterRules = ['heading1']; | ||
// if we don't reach the max quote depth we allow the recursive call to process possible quote | ||
if (this.currentQuoteDepth < this.maxQuoteDepth - 1) { | ||
filterRules.push('quote'); | ||
this.currentQuoteDepth++; | ||
} | ||
const replacedText = this.replace(textToReplace, { | ||
filterRules, | ||
shouldEscapeText: false, | ||
shouldKeepRawInput: false, | ||
}); | ||
this.currentQuoteDepth = 0; | ||
return `<blockquote>${replacedText}</blockquote>`; | ||
}, | ||
rawInputReplacement: (_extras, g1) => { | ||
// We want to enable 2 options of nested heading inside the blockquote: "># heading" and "> # heading". | ||
// To do this we need to parse body of the quote without first space | ||
let isStartingWithSpace = false; | ||
const handleMatch = (match, g2) => { | ||
if (shouldKeepRawInput) { | ||
isStartingWithSpace = !!g2; | ||
return ''; | ||
} | ||
return match; | ||
const handleMatch = (_match, g2) => { | ||
isStartingWithSpace = !!g2; | ||
return ''; | ||
}; | ||
@@ -328,3 +343,3 @@ const textToReplace = g1.replace(/^>( )?/gm, handleMatch); | ||
shouldEscapeText: false, | ||
shouldKeepRawInput, | ||
shouldKeepRawInput: true, | ||
}); | ||
@@ -343,3 +358,3 @@ this.currentQuoteDepth = 0; | ||
regex: /(<(pre|code|a|mention-user)[^>]*>(.*?)<\/\2>)|((\b_+|\b)_((?![\s_])[\s\S]*?[^\s_](?<!\s))_(?![^\W_])(?![^<]*>)(?![^<]*(<\/pre>|<\/code>|<\/a>|<\/mention-user>)))/g, | ||
replacement: (match, html, tag, content, text, extraLeadingUnderscores, textWithinUnderscores) => { | ||
replacement: (_extras, match, html, tag, content, text, extraLeadingUnderscores, textWithinUnderscores) => { | ||
// Skip any <pre>, <code>, <a>, <mention-user> tag contents | ||
@@ -376,3 +391,3 @@ if (html) { | ||
regex: /(?<!<[^>]*)\B\*(?![^<]*(?:<\/pre>|<\/code>|<\/a>))((?![\s*])[\s\S]*?[^\s*](?<!\s))\*\B(?![^<]*>)(?![^<]*(<\/pre>|<\/code>|<\/a>))/g, | ||
replacement: (match, g1) => (g1.includes('</pre>') || this.containsNonPairTag(g1) ? match : `<strong>${g1}</strong>`), | ||
replacement: (_extras, match, g1) => (g1.includes('</pre>') || this.containsNonPairTag(g1) ? match : `<strong>${g1}</strong>`), | ||
}, | ||
@@ -382,3 +397,3 @@ { | ||
regex: /(?<!<[^>]*)\B~((?![\s~])[\s\S]*?[^\s~](?<!\s))~\B(?![^<]*>)(?![^<]*(<\/pre>|<\/code>|<\/a>))/g, | ||
replacement: (match, g1) => (g1.includes('</pre>') || this.containsNonPairTag(g1) ? match : `<del>${g1}</del>`), | ||
replacement: (_extras, match, g1) => (g1.includes('</pre>') || this.containsNonPairTag(g1) ? match : `<del>${g1}</del>`), | ||
}, | ||
@@ -406,3 +421,2 @@ { | ||
* Order of rules is important | ||
* @type {Object[]} | ||
*/ | ||
@@ -470,3 +484,3 @@ this.htmlToMarkdownRules = [ | ||
regex: /<(blockquote|q)(?:"[^"]*"|'[^']*'|[^'">])*>([\s\S]*?)<\/\1>(?![^<]*(<\/pre>|<\/code>))/gi, | ||
replacement: (match, g1, g2) => { | ||
replacement: (_extras, _match, _g1, g2) => { | ||
// We remove the line break before heading inside quote to avoid adding extra line | ||
@@ -506,3 +520,3 @@ let resultString = g2 | ||
regex: /<(pre)(?:"[^"]*"|'[^']*'|[^'">])*>([\s\S]*?)(\n?)<\/\1>(?![^<]*(<\/pre>|<\/code>))/gi, | ||
replacement: (match, g1, g2) => `\`\`\`\n${g2}\n\`\`\``, | ||
replacement: (_extras, _match, _g1, g2) => `\`\`\`\n${g2}\n\`\`\``, | ||
}, | ||
@@ -512,3 +526,3 @@ { | ||
regex: /<(a)[^><]*href\s*=\s*(['"])(.*?)\2(?:".*?"|'.*?'|[^'"><])*>([\s\S]*?)<\/\1>(?![^<]*(<\/pre>|<\/code>))/gi, | ||
replacement: (match, g1, g2, g3, g4) => { | ||
replacement: (_extras, _match, _g1, _g2, g3, g4) => { | ||
const email = g3.startsWith('mailto:') ? g3.slice(7) : ''; | ||
@@ -524,3 +538,3 @@ if (email === g4) { | ||
regex: /<img[^><]*src\s*=\s*(['"])(.*?)\1(?:[^><]*alt\s*=\s*(['"])(.*?)\3)?[^><]*>*(?![^<][\s\S]*?(<\/pre>|<\/code>))/gi, | ||
replacement: (match, g1, g2, g3, g4) => { | ||
replacement: (_extras, _match, _g1, g2, _g3, g4) => { | ||
if (g4) { | ||
@@ -536,12 +550,11 @@ return ``; | ||
/** | ||
* @param {string} match The full match | ||
* @param {string} g1 {string} The first capture group | ||
* @param {string} videoSource - the second capture group - video source (video URL) | ||
* @param {string} videoAttrs - the third capture group - video attributes (data-expensify-width, data-expensify-height, etc...) | ||
* @param {string} videoName - the fourth capture group will be the video file name (the text between opening and closing video tags) | ||
* @param {any[]} args The rest of the arguments. args[args.length-1] will the `extras` object | ||
* @returns {string} Returns the markdown video tag | ||
* @param extras - The extras object | ||
* @param match The full match | ||
* @param _g1 The first capture group | ||
* @param videoSource - the second capture group - video source (video URL) | ||
* @param videoAttrs - the third capture group - video attributes (data-expensify-width, data-expensify-height, etc...) | ||
* @param videoName - the fourth capture group will be the video file name (the text between opening and closing video tags) | ||
* @returns The markdown video tag | ||
*/ | ||
replacement: (match, g1, videoSource, videoAttrs, videoName, ...args) => { | ||
const extras = args[args.length - 1]; | ||
replacement: (extras, _match, _g1, videoSource, videoAttrs, videoName) => { | ||
if (videoAttrs && extras && extras.cacheVideoAttributes && typeof extras.cacheVideoAttributes === 'function') { | ||
@@ -559,3 +572,3 @@ extras.cacheVideoAttributes(videoSource, videoAttrs); | ||
regex: /<mention-report reportID="(\d+)" *\/>/gi, | ||
replacement: (match, g1, offset, string, extras) => { | ||
replacement: (extras, _match, g1, _offset, _string) => { | ||
const reportToNameMap = extras.reportIDToName; | ||
@@ -572,3 +585,4 @@ if (!reportToNameMap || !reportToNameMap[g1]) { | ||
regex: /(?:<mention-user accountID="(\d+)" *\/>)|(?:<mention-user>(.*?)<\/mention-user>)/gi, | ||
replacement: (match, g1, g2, offset, string, extras) => { | ||
replacement: (extras, _match, g1, g2, _offset, _string) => { | ||
var _a; | ||
if (g1) { | ||
@@ -580,3 +594,3 @@ const accountToNameMap = extras.accountIDToName; | ||
} | ||
return `@${extras.accountIDToName[g1]}`; | ||
return `@${(_a = extras.accountIDToName) === null || _a === void 0 ? void 0 : _a[g1]}`; | ||
} | ||
@@ -590,3 +604,2 @@ return str_1.default.removeSMSDomain(g2); | ||
* Order of rules is important | ||
* @type {Object[]} | ||
*/ | ||
@@ -632,3 +645,3 @@ this.htmlToTextRules = [ | ||
regex: /<mention-report reportID="(\d+)" *\/>/gi, | ||
replacement: (match, g1, offset, string, extras) => { | ||
replacement: (extras, _match, g1, _offset, _string) => { | ||
const reportToNameMap = extras.reportIDToName; | ||
@@ -645,3 +658,4 @@ if (!reportToNameMap || !reportToNameMap[g1]) { | ||
regex: /<mention-user accountID="(\d+)" *\/>/gi, | ||
replacement: (match, g1, offset, string, extras) => { | ||
replacement: (extras, _match, g1, _offset, _string) => { | ||
var _a; | ||
const accountToNameMap = extras.accountIDToName; | ||
@@ -652,3 +666,3 @@ if (!accountToNameMap || !accountToNameMap[g1]) { | ||
} | ||
return `@${extras.accountIDToName[g1]}`; | ||
return `@${(_a = extras.accountIDToName) === null || _a === void 0 ? void 0 : _a[g1]}`; | ||
}, | ||
@@ -664,3 +678,2 @@ }, | ||
* The list of rules that we have to exclude in shouldKeepWhitespaceRules list. | ||
* @type {Object[]} | ||
*/ | ||
@@ -670,4 +683,4 @@ this.whitespaceRulesToDisable = ['newline', 'replacepre', 'replacebr', 'replaceh1br']; | ||
* The list of rules that have to be applied when shouldKeepWhitespace flag is true. | ||
* @param {Object} rule - The rule to check. | ||
* @returns {boolean} Returns true if the rule should be applied, otherwise false. | ||
* @param rule - The rule to check. | ||
* @returns true if the rule should be applied, otherwise false. | ||
*/ | ||
@@ -677,3 +690,3 @@ this.filterRules = (rule) => !this.whitespaceRulesToDisable.includes(rule.name); | ||
* Filters rules to determine which should keep whitespace. | ||
* @returns {Object[]} The filtered rules. | ||
* @returns The filtered rules. | ||
*/ | ||
@@ -683,3 +696,2 @@ this.shouldKeepWhitespaceRules = this.rules.filter(this.filterRules); | ||
* maxQuoteDepth is the maximum depth of nested quotes that we want to support. | ||
* @type {Number} | ||
*/ | ||
@@ -689,6 +701,11 @@ this.maxQuoteDepth = 3; | ||
* currentQuoteDepth is the current depth of nested quotes that we are processing. | ||
* @type {Number} | ||
*/ | ||
this.currentQuoteDepth = 0; | ||
} | ||
/** | ||
* Retrieves the HTML ruleset based on the provided filter rules, disabled rules, and shouldKeepRawInput flag. | ||
* @param filterRules - An array of rule names to filter the ruleset. | ||
* @param disabledRules - An array of rule names to disable in the ruleset. | ||
* @param shouldKeepRawInput - A boolean flag indicating whether to keep raw input. | ||
*/ | ||
getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput) { | ||
@@ -712,13 +729,11 @@ let rules = this.rules; | ||
* | ||
* @param {String} text - Text to parse as markdown | ||
* @param {Object} [options] - Options to customize the markdown parser | ||
* @param {String[]} [options.filterRules=[]] - An array of name of rules as defined in this class. | ||
* @param text - Text to parse as markdown | ||
* @param [options] - Options to customize the markdown parser | ||
* @param [options.filterRules=[]] - An array of name of rules as defined in this class. | ||
* If not provided, all available rules will be applied. | ||
* @param {Boolean} [options.shouldEscapeText=true] - Whether or not the text should be escaped | ||
* @param {String[]} [options.disabledRules=[]] - An array of name of rules as defined in this class. | ||
* @param [options.shouldEscapeText=true] - Whether or not the text should be escaped | ||
* @param [options.disabledRules=[]] - An array of name of rules as defined in this class. | ||
* If not provided, all available rules will be applied. If provided, the rules in the array will be skipped. | ||
* | ||
* @returns {String} | ||
*/ | ||
replace(text, { filterRules = [], shouldEscapeText = true, shouldKeepRawInput = false, disabledRules = [], extras } = {}) { | ||
replace(text, { filterRules = [], shouldEscapeText = true, shouldKeepRawInput = false, disabledRules = [], extras = EXTRAS_DEFAULT } = {}) { | ||
// This ensures that any html the user puts into the comment field shows as raw html | ||
@@ -732,9 +747,8 @@ let replacedText = shouldEscapeText ? Utils.escape(text) : text; | ||
} | ||
const replacementFunction = shouldKeepRawInput && rule.rawInputReplacement ? rule.rawInputReplacement : rule.replacement; | ||
const replacementFnWithExtraParams = typeof replacementFunction === 'function' ? (...args) => replacementFunction(...args, extras) : replacementFunction; | ||
if (rule.process) { | ||
replacedText = rule.process(replacedText, replacementFnWithExtraParams, shouldKeepRawInput); | ||
const replacement = shouldKeepRawInput && rule.rawInputReplacement ? rule.rawInputReplacement : rule.replacement; | ||
if ('process' in rule) { | ||
replacedText = rule.process(replacedText, replacement, shouldKeepRawInput); | ||
} | ||
else { | ||
replacedText = replacedText.replace(rule.regex, replacementFnWithExtraParams); | ||
replacedText = this.replaceTextWithExtras(replacedText, rule.regex, extras, replacement); | ||
} | ||
@@ -750,4 +764,3 @@ // Post-process text after applying regex | ||
catch (e) { | ||
// eslint-disable-next-line no-console | ||
console.warn('Error replacing text with html in ExpensiMark.replace', { error: e }); | ||
ExpensiMark.Log.alert('Error replacing text with html in ExpensiMark.replace', { error: e }); | ||
// We want to return text without applying rules if exception occurs during replacing | ||
@@ -760,8 +773,2 @@ return shouldEscapeText ? Utils.escape(text) : text; | ||
* Checks matched URLs for validity and replace valid links with html elements | ||
* | ||
* @param {RegExp} regex | ||
* @param {String} textToCheck | ||
* @param {Function} replacement | ||
* | ||
* @returns {String} | ||
*/ | ||
@@ -849,3 +856,3 @@ modifyTextForUrlLinks(regex, textToCheck, replacement) { | ||
}); | ||
replacedText = replacedText.concat(replacement(match[0], linkText, url)); | ||
replacedText = replacedText.concat(replacement(EXTRAS_DEFAULT, match[0], linkText, url)); | ||
} | ||
@@ -863,9 +870,2 @@ startIndex = match.index + match[0].length; | ||
* Checks matched Emails for validity and replace valid links with html elements | ||
* | ||
* @param {RegExp} regex | ||
* @param {String} textToCheck | ||
* @param {Function} replacement | ||
* @param {Boolean} shouldKeepRawInput | ||
* | ||
* @returns {String} | ||
*/ | ||
@@ -884,4 +884,4 @@ modifyTextForEmailLinks(regex, textToCheck, replacement, shouldKeepRawInput) { | ||
}); | ||
// rawInputReplacment needs to be called with additional parameters from match | ||
const replacedMatch = shouldKeepRawInput ? replacement(match[0], linkText, match[2], match[3]) : replacement(match[0], linkText, match[3]); | ||
// rawInputReplacement needs to be called with additional parameters from match | ||
const replacedMatch = shouldKeepRawInput ? replacement(EXTRAS_DEFAULT, match[0], linkText, match[2], match[3]) : replacement(EXTRAS_DEFAULT, match[0], linkText, match[3]); | ||
replacedText = replacedText.concat(replacedMatch); | ||
@@ -903,5 +903,2 @@ startIndex = match.index + match[0].length; | ||
* 4. It's not the last element in the string. | ||
* | ||
* @param {String} htmlString | ||
* @returns {String} | ||
*/ | ||
@@ -938,9 +935,4 @@ replaceBlockElementWithNewLine(htmlString) { | ||
* Replaces HTML with markdown | ||
* | ||
* @param {String} htmlString | ||
* @param {Object} extras | ||
* | ||
* @returns {String} | ||
*/ | ||
htmlToMarkdown(htmlString, extras = {}) { | ||
htmlToMarkdown(htmlString, extras = EXTRAS_DEFAULT) { | ||
let generatedMarkdown = htmlString; | ||
@@ -958,5 +950,3 @@ const body = /<(body)(?:"[^"]*"|'[^']*'|[^'"><])*>(?:\n|\r\n)?([\s\S]*?)(?:\n|\r\n)?<\/\1>(?![^<]*(<\/pre>|<\/code>))/im; | ||
} | ||
// if replacement is a function, we want to pass optional extras to it | ||
const replacementFunction = Utils.isFunction(rule.replacement) ? (...args) => rule.replacement(...args, extras) : rule.replacement; | ||
generatedMarkdown = generatedMarkdown.replace(rule.regex, replacementFunction); | ||
generatedMarkdown = this.replaceTextWithExtras(generatedMarkdown, rule.regex, extras, rule.replacement); | ||
}; | ||
@@ -968,14 +958,7 @@ this.htmlToMarkdownRules.forEach(processRule); | ||
* Convert HTML to text | ||
* | ||
* @param {String} htmlString | ||
* @param {Object} extras | ||
* | ||
* @returns {String} | ||
*/ | ||
htmlToText(htmlString, extras = {}) { | ||
htmlToText(htmlString, extras = EXTRAS_DEFAULT) { | ||
let replacedText = htmlString; | ||
const processRule = (rule) => { | ||
// if replacement is a function, we want to pass optional extras to it | ||
const replacementFunction = Utils.isFunction(rule.replacement) ? (...args) => rule.replacement(...args, extras) : rule.replacement; | ||
replacedText = replacedText.replace(rule.regex, replacementFunction); | ||
replacedText = this.replaceTextWithExtras(replacedText, rule.regex, extras, rule.replacement); | ||
}; | ||
@@ -990,8 +973,2 @@ this.htmlToTextRules.forEach(processRule); | ||
* Modify text for Quotes replacing chevrons with html elements | ||
* | ||
* @param {RegExp} regex | ||
* @param {String} textToCheck | ||
* @param {Function} replacement | ||
* | ||
* @returns {String} | ||
*/ | ||
@@ -1048,8 +1025,2 @@ modifyTextForQuote(regex, textToCheck, replacement) { | ||
* Format the content of blockquote if the text matches the regex or else just return the original text | ||
* | ||
* @param {RegExp} regex | ||
* @param {String} textToCheck | ||
* @param {Function} replacement | ||
* | ||
* @returns {String} | ||
*/ | ||
@@ -1069,3 +1040,3 @@ formatTextForQuote(regex, textToCheck, replacement) { | ||
textToFormat = textToFormat.replace(/^\n+|\n+$/g, ''); | ||
return replacement(textToFormat); | ||
return replacement(EXTRAS_DEFAULT, textToFormat); | ||
} | ||
@@ -1076,6 +1047,2 @@ return textToCheck; | ||
* Check if the input text includes only the open or the close tag of an element. | ||
* | ||
* @param {String} textToCheck - Text to check | ||
* | ||
* @returns {Boolean} | ||
*/ | ||
@@ -1110,4 +1077,3 @@ containsNonPairTag(textToCheck) { | ||
/** | ||
* @param {String} comment | ||
* @returns {Array} or undefined if exception occurs when executing regex matching | ||
* @returns array or undefined if exception occurs when executing regex matching | ||
*/ | ||
@@ -1126,4 +1092,3 @@ extractLinksInMarkdownComment(comment) { | ||
catch (e) { | ||
// eslint-disable-next-line no-console | ||
console.warn('Error parsing url in ExpensiMark.extractLinksInMarkdownComment', { error: e }); | ||
ExpensiMark.Log.alert('Error parsing url in ExpensiMark.extractLinksInMarkdownComment', { error: e }); | ||
return undefined; | ||
@@ -1134,6 +1099,2 @@ } | ||
* Compares two markdown comments and returns a list of the links removed in a new comment. | ||
* | ||
* @param {String} oldComment | ||
* @param {String} newComment | ||
* @returns {Array} | ||
*/ | ||
@@ -1147,4 +1108,4 @@ getRemovedMarkdownLinks(oldComment, newComment) { | ||
* Escapes the content of an HTML attribute value | ||
* @param {String} content - string content that possible contains HTML | ||
* @returns {String} - original MD content escaped for use in HTML attribute value | ||
* @param content - string content that possible contains HTML | ||
* @returns original MD content escaped for use in HTML attribute value | ||
*/ | ||
@@ -1161,5 +1122,20 @@ escapeAttributeContent(content) { | ||
} | ||
/** | ||
* Replaces text with a replacement based on a regex | ||
* @param text - The text to replace | ||
* @param regexp - The regex to match | ||
* @param extras - The extras object | ||
* @param replacement - The replacement string or function | ||
* @returns The replaced text | ||
*/ | ||
replaceTextWithExtras(text, regexp, extras, replacement) { | ||
if (typeof replacement === 'function') { | ||
// if the replacement is a function, we pass the extras object to it | ||
return text.replace(regexp, (...args) => replacement(extras, ...args)); | ||
} | ||
return text.replace(regexp, replacement); | ||
} | ||
} | ||
ExpensiMark.Log = new Logger_1.default({ | ||
serverLoggingCallback: () => { }, | ||
serverLoggingCallback: () => undefined, | ||
// eslint-disable-next-line no-console | ||
@@ -1166,0 +1142,0 @@ clientLoggingCallback: (message) => console.warn(message), |
{ | ||
"name": "expensify-common", | ||
"version": "2.0.18", | ||
"version": "2.0.19", | ||
"author": "Expensify, Inc.", | ||
@@ -5,0 +5,0 @@ "description": "Expensify libraries and components shared across different repos", |
489216
11004