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

Pass extra data (fe. accountIDs) to HTMLToMarkdown method & add possibility to disable certain rules #686

Merged
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
7 changes: 7 additions & 0 deletions __tests__/ExpensiMark-HTML-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2030,6 +2030,13 @@ describe('room mentions', () => {
expect(parser.replace(testString)).toBe(resultString);
});

test('room mention shouldn\'t be parsed when rule is disabled', () => {
const testString = '*hello* @[email protected] in #room!';
const resultString = '<strong>hello</strong> <mention-user>@[email protected]</mention-user> in #room!';
const disabledRules = ['reportMentions'];
expect(parser.replace(testString, {disabledRules})).toBe(resultString);
});

test('room mention with italic, bold and strikethrough styles', () => {
const testString = '#room'
+ ' _#room_'
Expand Down
38 changes: 37 additions & 1 deletion __tests__/ExpensiMark-HTMLToText-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ test('Test remove style tag', () => {
expect(parser.htmlToText(testString)).toBe('a text');
});

test('Mention html to text', () => {
test('Mention user html to text', () => {
let testString = '<mention-user>@[email protected]</mention-user>';
expect(parser.htmlToText(testString)).toBe('@[email protected]');

Expand All @@ -145,6 +145,42 @@ test('Mention html to text', () => {

testString = '<mention-user>@[email protected]</mention-user>';
expect(parser.htmlToText(testString)).toBe('@[email protected]');

const extras = {
accountIdToName: {
'1234': '[email protected]',
},
};
testString = '<mention-user accountID="1234"/>';
expect(parser.htmlToText(testString, extras)).toBe('@[email protected]');

testString = '<mention-user accountID="1234" />';
expect(parser.htmlToText(testString, extras)).toBe('@[email protected]');
});

test('Mention report html to text', () => {
let testString = '<mention-report>#room-name</mention-report>';
expect(parser.htmlToText(testString)).toBe('#room-name');

testString = '<mention-report>#ROOM-NAME</mention-report>';
expect(parser.htmlToText(testString)).toBe('#ROOM-NAME');

testString = '<mention-report>#ROOM-name</mention-report>';
expect(parser.htmlToText(testString)).toBe('#ROOM-name');

testString = '<mention-report>#room-NAME</mention-report>';
expect(parser.htmlToText(testString)).toBe('#room-NAME');

const extras = {
reportIdToName: {
'1234': '#room-name',
},
};
testString = '<mention-report reportID="1234"/>';
expect(parser.htmlToText(testString, extras)).toBe('#room-name');

testString = '<mention-report reportID="1234" />';
expect(parser.htmlToText(testString, extras)).toBe('#room-name');
});

test('Test replacement for <img> tags', () => {
Expand Down
38 changes: 37 additions & 1 deletion __tests__/ExpensiMark-Markdown-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ test('Linebreak should be remained for text between code block', () => {
});
});

test('Mention html to markdown', () => {
test('Mention user html to markdown', () => {
let testString = '<mention-user>@[email protected]</mention-user>';
expect(parser.htmlToMarkdown(testString)).toBe('@[email protected]');

Expand All @@ -755,6 +755,42 @@ test('Mention html to markdown', () => {

testString = '<mention-user>@[email protected]</mention-user>';
expect(parser.htmlToMarkdown(testString)).toBe('@[email protected]');

const extras = {
accountIdToName: {
'1234': '[email protected]',
},
};
testString = '<mention-user accountID="1234"/>';
expect(parser.htmlToMarkdown(testString, extras)).toBe('@[email protected]');

testString = '<mention-user accountID="1234" />';
expect(parser.htmlToMarkdown(testString, extras)).toBe('@[email protected]');
});

test('Mention report html to markdown', () => {
let testString = '<mention-report>#room-name</mention-report>';
expect(parser.htmlToMarkdown(testString)).toBe('#room-name');

testString = '<mention-report>#ROOM-NAME</mention-report>';
expect(parser.htmlToMarkdown(testString)).toBe('#ROOM-NAME');

testString = '<mention-report>#ROOM-name</mention-report>';
expect(parser.htmlToMarkdown(testString)).toBe('#ROOM-name');

testString = '<mention-report>#room-NAME</mention-report>';
expect(parser.htmlToMarkdown(testString)).toBe('#room-NAME');

const extras = {
reportIdToName: {
'1234': '#room-name',
},
};
testString = '<mention-report reportID="1234"/>';
expect(parser.htmlToMarkdown(testString, extras)).toBe('#room-name');

testString = '<mention-report reportID="1234" />';
expect(parser.htmlToMarkdown(testString, extras)).toBe('#room-name');
});

describe('Image tag conversion to markdown', () => {
Expand Down
21 changes: 16 additions & 5 deletions lib/ExpensiMark.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
declare type Replacement = (...args: string[]) => string;
declare type Replacement = (...args: string[], extras?: ExtrasObject) => string;
declare type Name =
| 'codeFence'
| 'inlineCodeBlock'
| 'email'
| 'link'
| 'hereMentions'
| 'userMentions'
| 'reportMentions'
| 'autoEmail'
| 'autolink'
| 'quote'
Expand All @@ -32,6 +33,11 @@ declare type Rule = {
pre?: (input: string) => string;
post?: (input: string) => string;
};

declare type ExtrasObject = {
reportIdToName?: Record<string, string>;
accountIDToName?: Record<string, string>;
};
export default class ExpensiMark {
rules: Rule[];
htmlToMarkdownRules: Rule[];
Expand All @@ -43,7 +49,9 @@ export default class ExpensiMark {
* @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 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.
* 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
*/
Expand All @@ -54,7 +62,8 @@ export default class ExpensiMark {
shouldEscapeText,
shouldKeepRawInput,
}?: {
filterRules?: string[];
filterRules?: Name[];
disabledRules?: Name[];
shouldEscapeText?: boolean;
shouldKeepRawInput?: boolean;
},
Expand Down Expand Up @@ -89,14 +98,16 @@ export default class ExpensiMark {
* Replaces HTML with markdown
*
* @param htmlString
* @param extras
*/
htmlToMarkdown(htmlString: string): string;
htmlToMarkdown(htmlString: string, extras?: ExtrasObject): string;
/**
* Convert HTML to text
*
* @param htmlString
* @param extras
*/
htmlToText(htmlString: string): string;
htmlToText(htmlString: string, extras?: ExtrasObject): string;
/**
* Modify text for Quotes replacing chevrons with html elements
*
Expand Down
101 changes: 89 additions & 12 deletions lib/ExpensiMark.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export default class ExpensiMark {
name: 'image',
regex: MARKDOWN_IMAGE_REGEX,
replacement: (match, g1, g2) => `<img src="${Str.sanitizeURL(g2)}"${g1 ? ` alt="${this.escapeAttributeContent(g1)}"` : ''} />`,
rawInputReplacement: (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'}" />`
rawInputReplacement: (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'}" />`
},

/**
Expand Down Expand Up @@ -174,7 +174,7 @@ export default class ExpensiMark {
* combination of letters and hyphens
*/
{
name: 'roomMentions',
name: 'reportMentions',

regex: /(?<![^ \n*~_])(#[\p{Ll}0-9-]{1,80})/gmiu,
replacement: '<mention-report>$1</mention-report>',
Expand Down Expand Up @@ -264,7 +264,11 @@ export default class ExpensiMark {
this.currentQuoteDepth++;
}

const replacedText = this.replace(textToReplace, {filterRules, shouldEscapeText: false, shouldKeepRawInput});
const replacedText = this.replace(textToReplace, {
filterRules,
shouldEscapeText: false,
shouldKeepRawInput
});
this.currentQuoteDepth = 0;
return `<blockquote>${isStartingWithSpace ? ' ' : ''}${replacedText}</blockquote>`;
},
Expand Down Expand Up @@ -453,7 +457,31 @@ export default class ExpensiMark {

return `!(${g2})`;
}
}
},
{
name: 'reportMentions',
regex: /<mention-report reportID="(\d+)" *\/>/gi,
replacement: (match, g1, offset, string, extras) => {
const reportToNameMap = extras.reportIdToName;
if (!reportToNameMap || !reportToNameMap[g1]) {
return '';
}

return reportToNameMap[g1];
},
},
{
name: 'userMention',
regex: /<mention-user accountID="(\d+)" *\/>/gi,
replacement: (match, g1, offset, string, extras) => {
const accountToNameMap = extras.accountIdToName;
if (!accountToNameMap || !accountToNameMap[g1]) {
return '';
}

return `@${extras.accountIdToName[g1]}`;
},
},
];

/**
Expand Down Expand Up @@ -497,11 +525,35 @@ export default class ExpensiMark {
regex: /<img[^><]*src\s*=\s*(['"])(.*?)\1(?:[^><]*alt\s*=\s*(['"])(.*?)\3)?[^><]*>*(?![^<][\s\S]*?(<\/pre>|<\/code>))/gi,
replacement: '[Attachment]',
},
{
name: 'reportMentions',
regex: /<mention-report reportID="(\d+)" *\/>/gi,
replacement: (match, g1, offset, string, extras) => {
const reportToNameMap = extras.reportIdToName;
if (!reportToNameMap || !reportToNameMap[g1]) {
return '';
}

return reportToNameMap[g1];
},
},
{
name: 'userMention',
regex: /<mention-user accountID="(\d+)" *\/>/gi,
replacement: (match, g1, offset, string, extras) => {
const accountToNameMap = extras.accountIdToName;
if (!accountToNameMap || !accountToNameMap[g1]) {
return '';
}

return `@${extras.accountIdToName[g1]}`;
},
},
{
name: 'stripTag',
regex: /(<([^>]+)>)/gi,
replacement: '',
},
}
];

/**
Expand Down Expand Up @@ -529,6 +581,20 @@ export default class ExpensiMark {
this.currentQuoteDepth = 0;
}

getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput) {
let rules = this.rules;
if(shouldKeepRawInput) {
rules = this.shouldKeepWhitespaceRules;
}
if (!_.isEmpty(filterRules)) {
rules = _.filter(this.rules, (rule) => _.contains(filterRules, rule.name));
}
if (!_.isEmpty(disabledRules)) {
rules = _.filter(rules, (rule) => !_.contains(disabledRules, rule.name));
}
return rules;
}

/**
* Replaces markdown with html elements
*
Expand All @@ -537,14 +603,16 @@ export default class ExpensiMark {
* @param {String[]} [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.
* 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} = {}) {
replace(text, {filterRules = [], shouldEscapeText = true, shouldKeepRawInput = false, disabledRules = []} = {}) {
// This ensures that any html the user puts into the comment field shows as raw html
let replacedText = shouldEscapeText ? _.escape(text) : text;
const enabledRules = shouldKeepRawInput ? this.shouldKeepWhitespaceRules : this.rules;
const rules = _.isEmpty(filterRules) ? enabledRules : _.filter(this.rules, (rule) => _.contains(filterRules, rule.name));
const rules = this.getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput);

try {
rules.forEach((rule) => {
// Pre-process text before applying regex
Expand Down Expand Up @@ -768,10 +836,11 @@ export default class ExpensiMark {
* Replaces HTML with markdown
*
* @param {String} htmlString
* @param {Object} extras
robertKozik marked this conversation as resolved.
Show resolved Hide resolved
*
* @returns {String}
*/
htmlToMarkdown(htmlString) {
htmlToMarkdown(htmlString, extras = {}) {
let generatedMarkdown = htmlString;
const body = /<(body)(?:"[^"]*"|'[^']*'|[^'"><])*>(?:\n|\r\n)?([\s\S]*?)(?:\n|\r\n)?<\/\1>(?![^<]*(<\/pre>|<\/code>))/im;
const parseBodyTag = generatedMarkdown.match(body);
Expand All @@ -786,7 +855,10 @@ export default class ExpensiMark {
if (rule.pre) {
generatedMarkdown = rule.pre(generatedMarkdown);
}
generatedMarkdown = generatedMarkdown.replace(rule.regex, rule.replacement);

// if replacement is a function, we want to pass optional extras to it
const replacementFunction = typeof rule.replacement === 'function' ? (...args) => rule.replacement(...args, extras) : rule.replacement;
generatedMarkdown = generatedMarkdown.replace(rule.regex, replacementFunction);
});
return Str.htmlDecode(this.replaceBlockElementWithNewLine(generatedMarkdown));
}
Expand All @@ -795,13 +867,18 @@ export default class ExpensiMark {
* Convert HTML to text
*
* @param {String} htmlString
* @param {Object} extras
robertKozik marked this conversation as resolved.
Show resolved Hide resolved
*
* @returns {String}
*/
htmlToText(htmlString) {
htmlToText(htmlString, extras) {
let replacedText = htmlString;

this.htmlToTextRules.forEach((rule) => {
replacedText = replacedText.replace(rule.regex, rule.replacement);

// if replacement is a function, we want to pass optional extras to it
const replacementFunction = typeof rule.replacement === 'function' ? (...args) => rule.replacement(...args, extras) : rule.replacement;
robertKozik marked this conversation as resolved.
Show resolved Hide resolved
replacedText = replacedText.replace(rule.regex, replacementFunction);
});

// Unescaping because the text is escaped in 'replace' function
Expand Down
Loading