Skip to content

Commit

Permalink
Merge pull request #686 from software-mansion-labs/war-in/convert-men…
Browse files Browse the repository at this point in the history
…tions-with-ids-to-markdown

Pass extra data (fe. accountIDs) to HTMLToMarkdown method & add possibility to disable certain rules
  • Loading branch information
rlinoz authored Apr 23, 2024
2 parents bd92f2f + f41aae6 commit 9a68635
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 19 deletions.
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
*
* @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
*
* @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;
replacedText = replacedText.replace(rule.regex, replacementFunction);
});

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

0 comments on commit 9a68635

Please sign in to comment.