snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
import proxyquire from 'proxyquire';
const mocks = {
'meteor/meteor': {
'Meteor': {
absoluteUrl() {
return 'http://localhost:3000/';
},
},
'@global': true,
},
'@rocket.chat/random': {
'Random': {
id() {
return Math.random().toString().replace('0.', 'A');
},
},
'@global': true,
},
};
export const { Markdown } = proxyquire.noCallThru().load('../../../../app/markdown/lib/markdown', mocks);
export const { original } = proxyquire.noCallThru().load('../../../../app/markdown/lib/parser/original/original', mocks);
export const { filtered } = proxyquire.noCallThru().load('../../../../app/markdown/lib/parser/filtered/filtered', mocks);

View File

@@ -0,0 +1,479 @@
import { escapeHTML } from '@rocket.chat/tools';
import { expect } from 'chai';
import { Markdown, original, filtered } from './client.mocks';
const wrapper = (text, tag) => `<span class="copyonly">${tag}</span>${text}<span class="copyonly">${tag}</span>`;
const boldWrapper = (text) => wrapper(`<strong>${text}</strong>`, '*');
const italicWrapper = (text) => wrapper(`<em>${text}</em>`, '_');
const strikeWrapper = (text) => wrapper(`<strike>${text}</strike>`, '~');
const headerWrapper = (text, level) => `<h${level}>${text}</h${level}>`;
const quoteWrapper = (text) =>
`<blockquote class="background-transparent-darker-before"><span class="copyonly">&gt;</span>${text}</blockquote>`;
const linkWrapped = (link, title) => `<a data-title="${link}" href="${link}" target="_blank" rel="noopener noreferrer">${title}</a>`;
const inlinecodeWrapper = (text) => wrapper(`<span><code class="code-colors inline">${text}</code></span>`, '`');
const codeWrapper = (text, lang) =>
`<pre><code class='code-colors hljs ${lang}'><span class='copyonly'>\`\`\`<br></span>${text}<span class='copyonly'><br>\`\`\`</span></code></pre>`;
const bold = {
'**': '**',
'* *': '* *',
'** *': '** *',
'** **': '** **',
'* Hello*': '* Hello*',
'*Hello *': '*Hello *',
':custom*emoji*case:': `:custom${boldWrapper('emoji')}case:`,
'text*hello*text': `text${boldWrapper('hello')}text`,
'*hello*text': `${boldWrapper('hello')}text`,
'text*hello*': `text${boldWrapper('hello')}`,
'*Hel lo*': boldWrapper('Hel lo'),
'*Hello*': boldWrapper('Hello'),
'**Hello**': boldWrapper('Hello'),
'**Hello*': `*${boldWrapper('Hello')}`,
'*Hello**': `${boldWrapper('Hello')}*`,
'Hello': 'Hello',
'*Hello': '*Hello',
'Hello*': 'Hello*',
'He*llo': 'He*llo',
'***Hello***': `*${boldWrapper('Hello')}*`,
'***Hello**': `*${boldWrapper('Hello')}`,
'*Hello* this is dog': `${boldWrapper('Hello')} this is dog`,
'Rocket cat says *Hello*': `Rocket cat says ${boldWrapper('Hello')}`,
'He said *Hello* to her': `He said ${boldWrapper('Hello')} to her`,
'**Hello** this is dog': `${boldWrapper('Hello')} this is dog`,
'Rocket cat says **Hello**': `Rocket cat says ${boldWrapper('Hello')}`,
'He said **Hello** to her': `He said ${boldWrapper('Hello')} to her`,
'He was a**nn**oyed': `He was a${boldWrapper('nn')}oyed`,
'There are two o in f*oo*tball': `There are two o in f${boldWrapper('oo')}tball`,
};
const italic = {
'__': '__',
'_ _': '_ _',
'__ _': '__ _',
'__ __': '__ __',
'_ Hello_': '_ Hello_',
'_Hello _': '_Hello _',
':custom_emoji_case:': ':custom_emoji_case:',
'text_hello_text': 'text_hello_text',
'_hello_text': '_hello_text',
'text_hello_': 'text_hello_',
'_Hel lo_': italicWrapper('Hel lo'),
'_Hello_': italicWrapper('Hello'),
'__Hello__': italicWrapper('Hello'),
'__Hello_': `_${italicWrapper('Hello')}`,
'_Hello__': `${italicWrapper('Hello')}_`,
'Hello': 'Hello',
'_Hello': '_Hello',
'Hello_': 'Hello_',
'He_llo': 'He_llo',
'___Hello___': `_${italicWrapper('Hello')}_`,
'___Hello__': `_${italicWrapper('Hello')}`,
'_Hello_ this is dog': `${italicWrapper('Hello')} this is dog`,
'Rocket cat says _Hello_': `Rocket cat says ${italicWrapper('Hello')}`,
'He said _Hello_ to her': `He said ${italicWrapper('Hello')} to her`,
'__Hello__ this is dog': `${italicWrapper('Hello')} this is dog`,
'Rocket cat says __Hello__': `Rocket cat says ${italicWrapper('Hello')}`,
'He said __Hello__ to her': `He said ${italicWrapper('Hello')} to her`,
};
const strike = {
'~~': '~~',
'~ ~': '~ ~',
'~~ ~': '~~ ~',
'~~ ~~': '~~ ~~',
'~ Hello~': '~ Hello~',
'~Hello ~': '~Hello ~',
':custom~emoji~case:': `:custom${strikeWrapper('emoji')}case:`,
'text~hello~text': `text${strikeWrapper('hello')}text`,
'~hello~text': `${strikeWrapper('hello')}text`,
'text~hello~': `text${strikeWrapper('hello')}`,
'~Hel lo~': strikeWrapper('Hel lo'),
'~Hello~': strikeWrapper('Hello'),
'~~Hello~~': strikeWrapper('Hello'),
'~~Hello~': `~${strikeWrapper('Hello')}`,
'~Hello~~': `${strikeWrapper('Hello')}~`,
'Hello': 'Hello',
'~Hello': '~Hello',
'Hello~': 'Hello~',
'He~llo': 'He~llo',
'~~~Hello~~~': `~${strikeWrapper('Hello')}~`,
'~~~Hello~~': `~${strikeWrapper('Hello')}`,
'~Hello~ this is dog': `${strikeWrapper('Hello')} this is dog`,
'Rocket cat says ~Hello~': `Rocket cat says ${strikeWrapper('Hello')}`,
'He said ~Hello~ to her': `He said ${strikeWrapper('Hello')} to her`,
'~~Hello~~ this is dog': `${strikeWrapper('Hello')} this is dog`,
'Rocket cat says ~~Hello~~': `Rocket cat says ${strikeWrapper('Hello')}`,
'He said ~~Hello~~ to her': `He said ${strikeWrapper('Hello')} to her`,
};
const headersLevel1 = {
'# Hello': headerWrapper('Hello', 1),
'# Rocket.Cat': headerWrapper('Rocket.Cat', 1),
'# Hi': headerWrapper('Hi', 1),
'# Hello this is dog': headerWrapper('Hello this is dog', 1),
'# Rocket cat says Hello': headerWrapper('Rocket cat says Hello', 1),
'# He said Hello to her': headerWrapper('He said Hello to her', 1),
'#Hello': '#Hello',
'#Hello#': '#Hello#',
'He#llo': 'He#llo',
};
const headersLevel2 = {
'## Hello': headerWrapper('Hello', 2),
'## Rocket.Cat': headerWrapper('Rocket.Cat', 2),
'## Hi': headerWrapper('Hi', 2),
'## Hello this is dog': headerWrapper('Hello this is dog', 2),
'## Rocket cat says Hello': headerWrapper('Rocket cat says Hello', 2),
'## He said Hello to her': headerWrapper('He said Hello to her', 2),
'##Hello': '##Hello',
'##Hello##': '##Hello##',
'He##llo': 'He##llo',
};
const headersLevel3 = {
'### Hello': headerWrapper('Hello', 3),
'### Rocket.Cat': headerWrapper('Rocket.Cat', 3),
'### Hi': headerWrapper('Hi', 3),
'### Hello this is dog': headerWrapper('Hello this is dog', 3),
'### Rocket cat says Hello': headerWrapper('Rocket cat says Hello', 3),
'### He said Hello to her': headerWrapper('He said Hello to her', 3),
'###Hello': '###Hello',
'###Hello###': '###Hello###',
'He###llo': 'He###llo',
};
const headersLevel4 = {
'#### Hello': headerWrapper('Hello', 4),
'#### Rocket.Cat': headerWrapper('Rocket.Cat', 4),
'#### Hi': headerWrapper('Hi', 4),
'#### Hello this is dog': headerWrapper('Hello this is dog', 4),
'#### Rocket cat says Hello': headerWrapper('Rocket cat says Hello', 4),
'#### He said Hello to her': headerWrapper('He said Hello to her', 4),
'####Hello': '####Hello',
'####Hello####': '####Hello####',
'He####llo': 'He####llo',
};
const quote = {
'&gt;Hello': escapeHTML('&gt;Hello'),
'&gt;Rocket.Cat': escapeHTML('&gt;Rocket.Cat'),
'&gt;Hi': escapeHTML('&gt;Hi'),
'&gt; Hello this is dog': escapeHTML('&gt; Hello this is dog'),
'&gt; Rocket cat says Hello': escapeHTML('&gt; Rocket cat says Hello'),
'&gt; He said Hello to her': escapeHTML('&gt; He said Hello to her'),
'&gt; He said Hello to her ': escapeHTML('&gt; He said Hello to her '),
'&lt;Hello': escapeHTML('&lt;Hello'),
'&lt;Rocket.Cat&gt;': escapeHTML('&lt;Rocket.Cat&gt;'),
' &gt;Hi': escapeHTML(' &gt;Hi'),
'Hello &gt; this is dog': escapeHTML('Hello &gt; this is dog'),
'Roc&gt;ket cat says Hello': escapeHTML('Roc&gt;ket cat says Hello'),
'He said Hello to her&gt;': escapeHTML('He said Hello to her&gt;'),
'>Hello': quoteWrapper('Hello'),
'>Rocket.Cat': quoteWrapper('Rocket.Cat'),
'>Hi': quoteWrapper('Hi'),
'> Hello this is dog': quoteWrapper(' Hello this is dog'),
'> Rocket cat says Hello': quoteWrapper(' Rocket cat says Hello'),
'> He said Hello to her': quoteWrapper(' He said Hello to her'),
'<Hello': escapeHTML('<Hello'),
'<Rocket.Cat>': escapeHTML('<Rocket.Cat>'),
' >Hi': escapeHTML(' >Hi'),
'Hello > this is dog': escapeHTML('Hello > this is dog'),
'Roc>ket cat says Hello': escapeHTML('Roc>ket cat says Hello'),
'He said Hello to her>': escapeHTML('He said Hello to her>'),
};
const link = {
'&lt;http://link|Text&gt;': escapeHTML('&lt;http://link|Text&gt;'),
'&lt;https://open.rocket.chat/|Open Site For Rocket.Chat&gt;': escapeHTML('&lt;https://open.rocket.chat/|Open Site For Rocket.Chat&gt;'),
'&lt;https://open.rocket.chat/ | Open Site For Rocket.Chat&gt;': escapeHTML(
'&lt;https://open.rocket.chat/ | Open Site For Rocket.Chat&gt;',
),
'&lt;https://rocket.chat/|Rocket.Chat Site&gt;': '&amp;lt;https://rocket.chat/|Rocket.Chat Site&amp;gt;',
'&lt;https://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site&gt;': escapeHTML(
'&lt;https://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site&gt;',
),
'&lt;http://linkText&gt;': escapeHTML('&lt;http://linkText&gt;'),
'&lt;https:open.rocket.chat/ | Open Site For Rocket.Chat&gt;': escapeHTML('&lt;https:open.rocket.chat/ | Open Site For Rocket.Chat&gt;'),
'https://open.rocket.chat/|Open Site For Rocket.Chat': escapeHTML('https://open.rocket.chat/|Open Site For Rocket.Chat'),
'&lt;www.open.rocket.chat/|Open Site For Rocket.Chat&gt;': escapeHTML('&lt;www.open.rocket.chat/|Open Site For Rocket.Chat&gt;'),
'&lt;htps://rocket.chat/|Rocket.Chat Site&gt;': escapeHTML('&lt;htps://rocket.chat/|Rocket.Chat Site&gt;'),
'&lt;ttps://rocket.chat/|Rocket.Chat Site&gt;': escapeHTML('&lt;ttps://rocket.chat/|Rocket.Chat Site&gt;'),
'&lt;tps://rocket.chat/|Rocket.Chat Site&gt;': escapeHTML('&lt;tps://rocket.chat/|Rocket.Chat Site&gt;'),
'&lt;open.rocket.chat/|Open Site For Rocket.Chat&gt;': escapeHTML('&lt;open.rocket.chat/|Open Site For Rocket.Chat&gt;'),
'&lt;htts://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site&gt;': escapeHTML(
'&lt;htts://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site&gt;',
),
'<http://invalid link|Text>': escapeHTML('<http://invalid link|Text>'),
'<http://link|Text>': linkWrapped('http://link', 'Text'),
'<https://open.rocket.chat/|Open Site For Rocket.Chat>': linkWrapped('https://open.rocket.chat/', 'Open Site For Rocket.Chat'),
'<https://open.rocket.chat/ | Open Site For Rocket.Chat>': linkWrapped(
encodeURI('https://open.rocket.chat/ '),
' Open Site For Rocket.Chat',
),
'<https://rocket.chat/|Rocket.Chat Site>': linkWrapped('https://rocket.chat/', 'Rocket.Chat Site'),
'<https://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>': linkWrapped(
'https://rocket.chat/docs/developer-guides/testing/#testing',
'Testing Entry on Rocket.Chat Docs Site',
),
'<http://linkText>': escapeHTML('<http://linkText>'),
'<https:open.rocket.chat/ | Open Site For Rocket.Chat>': escapeHTML('<https:open.rocket.chat/ | Open Site For Rocket.Chat>'),
'<www.open.rocket.chat/|Open Site For Rocket.Chat>': escapeHTML('<www.open.rocket.chat/|Open Site For Rocket.Chat>'),
'<htps://rocket.chat/|Rocket.Chat Site>': escapeHTML('<htps://rocket.chat/|Rocket.Chat Site>'),
'<ttps://rocket.chat/|Rocket.Chat Site>': escapeHTML('<ttps://rocket.chat/|Rocket.Chat Site>'),
'<tps://rocket.chat/|Rocket.Chat Site>': escapeHTML('<tps://rocket.chat/|Rocket.Chat Site>'),
'<open.rocket.chat/|Open Site For Rocket.Chat>': escapeHTML('<open.rocket.chat/|Open Site For Rocket.Chat>'),
'<htts://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>': escapeHTML(
'<htts://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>',
),
'[Text](http://invalid link)': '[Text](http://invalid link)',
'[Text](http://link)': linkWrapped('http://link', 'Text'),
'[Open Site For Rocket.Chat](https://open.rocket.chat/)': linkWrapped('https://open.rocket.chat/', 'Open Site For Rocket.Chat'),
'[ Open Site For Rocket.Chat ](https://open.rocket.chat/)': linkWrapped('https://open.rocket.chat/', ' Open Site For Rocket.Chat '),
'[Rocket.Chat Site](https://rocket.chat/)': linkWrapped('https://rocket.chat/', 'Rocket.Chat Site'),
'[Testing Entry on Rocket.Chat Docs Site](https://rocket.chat/docs/developer-guides/testing/#testing)': linkWrapped(
'https://rocket.chat/docs/developer-guides/testing/#testing',
'Testing Entry on Rocket.Chat Docs Site',
),
'[](http://linkText)': '[](http://linkText)',
'[text]': '[text]',
'[Open Site For Rocket.Chat](https:open.rocket.chat/)': '[Open Site For Rocket.Chat](https:open.rocket.chat/)',
'[Open Site For Rocket.Chat](www.open.rocket.chat/)': '[Open Site For Rocket.Chat](www.open.rocket.chat/)',
'[Rocket.Chat Site](htps://rocket.chat/)': '[Rocket.Chat Site](htps://rocket.chat/)',
'[Rocket.Chat Site](ttps://rocket.chat/)': '[Rocket.Chat Site](ttps://rocket.chat/)',
'[Rocket.Chat Site](tps://rocket.chat/)': '[Rocket.Chat Site](tps://rocket.chat/)',
'[Open Site For Rocket.Chat](open.rocket.chat/)': '[Open Site For Rocket.Chat](open.rocket.chat/)',
'[Testing Entry on Rocket.Chat Docs Site](htts://rocket.chat/docs/developer-guides/testing/#testing)':
'[Testing Entry on Rocket.Chat Docs Site](htts://rocket.chat/docs/developer-guides/testing/#testing)',
'[Text](http://link?param1=1&param2=2)': linkWrapped('http://link?param1=1&amp;param2=2', 'Text'),
'[Testing Double parentheses](https://en.wikipedia.org/wiki/Disambiguation_(disambiguation))': linkWrapped(
'https://en.wikipedia.org/wiki/Disambiguation_(disambiguation)',
'Testing Double parentheses',
),
'[Testing data after Double parentheses](https://en.wikipedia.org/wiki/Disambiguation_(disambiguation)/blabla/bla)': linkWrapped(
'https://en.wikipedia.org/wiki/Disambiguation_(disambiguation)/blabla/bla',
'Testing data after Double parentheses',
),
};
Object.entries(link).forEach(([key, value]) => {
link[`before (test) ${key} after (test)`] = `before (test) ${value} after (test)`;
});
const inlinecode = {
'`code`': inlinecodeWrapper('code'),
'`code` begin': `${inlinecodeWrapper('code')} begin`,
'End `code`': `End ${inlinecodeWrapper('code')}`,
'Middle `code` middle': `Middle ${inlinecodeWrapper('code')} middle`,
'`code`begin': `${inlinecodeWrapper('code')}begin`,
'End`code`': `End${inlinecodeWrapper('code')}`,
'Middle`code`middle': `Middle${inlinecodeWrapper('code')}middle`,
};
const code = {
'```code```': codeWrapper('<span class="hljs-keyword">code</span>', 'clean'),
'```code': codeWrapper('<span class="hljs-keyword">code</span>\n', 'clean'),
'```code\n': codeWrapper('<span class="hljs-keyword">code</span>\n', 'clean'),
'```\ncode\n```': codeWrapper('<span class="hljs-keyword">code</span>\n', 'clean'),
'```code\n```': codeWrapper('<span class="hljs-keyword">code</span>\n', 'clean'),
'```\ncode```': codeWrapper('<span class="hljs-keyword">code</span>', 'clean'),
"```javascript\nvar a = 'log';\nconsole.log(a);```": codeWrapper(
'<span class="hljs-keyword">var</span> a = <span class="hljs-string">&#x27;log&#x27;</span>;\n<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(a);',
'javascript',
),
'```*code*```': codeWrapper('<span class="hljs-emphasis">*code*</span>', 'markdown'),
'```**code**```': codeWrapper('<span class="hljs-strong">**code**</span>', 'markdown'),
'```__code__```': codeWrapper('<span class="hljs-strong">__code__</span>', 'markdown'),
};
const nested = {
'> some quote\n`window.location.reload();`': `${quoteWrapper(' some quote')}${inlinecodeWrapper('window.location.reload();')}`,
};
/*
* Markdown Filters
*/
const boldFiltered = {
'*Hello*': 'Hello',
'**Hello**': 'Hello',
'*Hello**': 'Hello',
'He*llo': 'He*llo',
'*Hello': '*Hello',
'Hello*': 'Hello*',
'***Hello***': '***Hello***',
'***Hello**': '***Hello**',
'*Hello* there': 'Hello there',
'**Hello** there': 'Hello there',
'Hi, *Hello*': 'Hi, Hello',
'Hi, **Hello**': 'Hi, Hello',
'Hi, *Hello* how are you?': 'Hi, Hello how are you?',
'Hi, **Hello** how are you?': 'Hi, Hello how are you?',
};
const italicFiltered = {
'_Hello_': 'Hello',
'__Hello__': 'Hello',
'_Hello__': 'Hello',
'He_llo': 'He_llo',
'_Hello': '_Hello',
'__Hello': '__Hello',
'Hello_': 'Hello_',
'___Hello___': '___Hello___',
'___Hello__': '___Hello__',
'_Hello_ there': 'Hello there',
'__Hello__ there': 'Hello there',
'Hi, _Hello_': 'Hi, Hello',
'Hi, __Hello__': 'Hi, Hello',
'Hi, _Hello_ how are you?': 'Hi, Hello how are you?',
'Hi, __Hello__ how are you?': 'Hi, Hello how are you?',
};
const strikeFiltered = {
'~Hello~': 'Hello',
'~~Hello~~': 'Hello',
'~~Hello': '~~Hello',
'~Hello~~': 'Hello',
'He~llo': 'He~llo',
'~Hello': '~Hello',
'Hello~': 'Hello~',
'~~~Hello~~~': '~~~Hello~~~',
'~~~Hello~~': '~~~Hello~~',
'~Hello~ there': 'Hello there',
'~~Hello~~ there': 'Hello there',
'Hi, ~Hello~': 'Hi, Hello',
'Hi, ~~Hello~~': 'Hi, Hello',
'Hi, ~Hello~ how are you?': 'Hi, Hello how are you?',
'Hi, ~~Hello~~ how are you?': 'Hi, Hello how are you?',
};
const headingFiltered = {
'# Hello': 'Hello',
'## Hello': 'Hello',
'### Hello': 'Hello',
'#### Hello': 'Hello',
'#Hello': '#Hello',
'##Hello': '##Hello',
'###Hello': '###Hello',
'####Hello': '####Hello',
'He#llo': 'He#llo',
'# Hello there': 'Hello there',
'Hi, # Hello': 'Hi, # Hello',
'Hi, # Hello there': 'Hi, # Hello there',
};
const quoteFiltered = {
'>Hello': 'Hello',
'> Hello': ' Hello',
'>>>\nHello\n<<<': 'Hello',
'>>>\nHello there!\n<<<': 'Hello there!',
'>>>\n Hello there! \n<<<': ' Hello there! ',
};
const linkFiltered = {
'[Text](http://link)': 'Text',
'[Open Site For Rocket.Chat](https://open.rocket.chat/)': 'Open Site For Rocket.Chat',
'[ Open Site For Rocket.Chat](https://open.rocket.chat/ )': ' Open Site For Rocket.Chat',
'[Rocket.Chat Site](https://rocket.chat/)': 'Rocket.Chat Site',
'<http://link|Text>': 'Text',
'<http://link|Text for test>': 'Text for test',
};
const inlinecodeFiltered = {
'`code`': 'code',
'`code` begin': 'code begin',
'End `code`': 'End code',
'Middle `code` middle': 'Middle code middle',
'`code`begin': 'codebegin',
'End`code`': 'Endcode',
'Middle`code`middle': 'Middlecodemiddle',
};
const blockcodeFiltered = {
'```code```': 'code',
'```code': 'code',
'code```': 'code',
'Here ```code``` lies': 'Here code lies',
'Here```code```lies': 'Herecodelies',
};
const defaultObjectTest = (result, object, objectKey) => expect(result.html).to.be.equal(object[objectKey]);
const testObject = (object, parser = original, test = defaultObjectTest) => {
Object.keys(object).forEach((objectKey) => {
describe(objectKey, () => {
const message = parser === original ? { html: escapeHTML(objectKey) } : objectKey;
const result = parser === original ? Markdown.mountTokensBack(parser(message)) : { html: parser(message) };
it(`should be equal to ${object[objectKey]}`, () => {
test(result, object, objectKey);
});
});
});
};
describe('Original', () => {
describe('Bold', () => testObject(bold));
describe('Italic', () => testObject(italic));
describe('Strike', () => testObject(strike));
describe('Headers', () => {
describe('Level 1', () => testObject(headersLevel1));
describe('Level 2', () => testObject(headersLevel2));
describe('Level 3', () => testObject(headersLevel3));
describe('Level 4', () => testObject(headersLevel4));
});
describe('Quote', () => testObject(quote));
describe('Link', () => testObject(link));
describe('Inline Code', () => testObject(inlinecode));
describe('Code', () => testObject(code));
describe('Nested', () => testObject(nested));
});
describe('Filtered', () => {
describe('BoldFilter', () => testObject(boldFiltered, filtered));
describe('Italic', () => testObject(italicFiltered, filtered));
describe('StrikeFilter', () => testObject(strikeFiltered, filtered));
describe('HeadingFilter', () => testObject(headingFiltered, filtered));
describe('QuoteFilter', () => testObject(quoteFiltered, filtered));
describe('LinkFilter', () => testObject(linkFiltered, filtered));
describe('inlinecodeFilter', () => testObject(inlinecodeFiltered, filtered));
describe('blockcodeFilter', () => testObject(blockcodeFiltered, filtered));
});
// describe('Marked', function() {
// describe('Bold', () => testObject(bold, marked));
// describe('Italic', () => testObject(italic, marked));
// describe('Strike', () => testObject(strike, marked));
// describe('Headers', () => {
// describe('Level 1', () => testObject(headersLevel1, marked));
// describe('Level 2', () => testObject(headersLevel2, marked));
// describe('Level 3', () => testObject(headersLevel3, marked));
// describe('Level 4', () => testObject(headersLevel4, marked));
// });
// describe('Quote', () => testObject(quote, marked));
// });

View File

@@ -0,0 +1,212 @@
import { expect } from 'chai';
import hljs, { register } from '../../../../app/markdown/lib/hljs';
const lazyLanguages: string[] = [
'onec',
'abnf',
'accesslog',
'actionscript',
'ada',
'apache',
'applescript',
'arduino',
'armasm',
'asciidoc',
'aspectj',
'autohotkey',
'autoit',
'avrasm',
'awk',
'axapta',
'bash',
'basic',
'bnf',
'brainfuck',
'cal',
'capnproto',
'ceylon',
'clean',
'clojure',
'clojure-repl',
'cmake',
'coffeescript',
'coq',
'cos',
'cpp',
'crmsh',
'crystal',
'csp',
'css',
'd',
'dart',
'delphi',
'diff',
'django',
'dns',
'dockerfile',
'dos',
'dsconfig',
'dts',
'dust',
'ebnf',
'elixir',
'elm',
'erb',
'erlang',
'excel',
'fix',
'flix',
'fortran',
'fsharp',
'gams',
'gauss',
'gcode',
'gherkin',
'glsl',
'go',
'golo',
'gradle',
'groovy',
'haml',
'handlebars',
'haskell',
'haxe',
'hsp',
'http',
'hy',
'inform7',
'ini',
'irpf90',
'java',
'javascript',
'jboss-cli',
'json',
'julia',
'julia-repl',
'kotlin',
'lasso',
'ldif',
'leaf',
'less',
'lisp',
'livecodeserver',
'livescript',
'llvm',
'lsl',
'lua',
'makefile',
'markdown',
'mathematica',
'matlab',
'maxima',
'mel',
'mercury',
'mipsasm',
'mizar',
'perl',
'mojolicious',
'monkey',
'moonscript',
'n1ql',
'nginx',
'nix',
'nsis',
'objectivec',
'ocaml',
'openscad',
'oxygene',
'parser3',
'pf',
'php',
'pony',
'powershell',
'processing',
'profile',
'prolog',
'protobuf',
'puppet',
'purebasic',
'python',
'q',
'qml',
'r',
'rib',
'roboconf',
'rsl',
'ruleslanguage',
'rust',
'scala',
'scheme',
'scilab',
'scss',
'shell',
'smali',
'smalltalk',
'sml',
'sqf',
'sql',
'stan',
'stata',
'step21',
'stylus',
'subunit',
'swift',
'taggerscript',
'yaml',
'tap',
'tcl',
'thrift',
'tp',
'twig',
'typescript',
'vala',
'vbnet',
'vbscript',
'verilog',
'vhdl',
'vim',
'x86asm',
'xl',
'xquery',
'zephir',
];
const eagerLanguages = ['markdown', 'clean', 'javascript'];
describe('hljs', () => {
describe('eagerly registered languages', () => {
eagerLanguages.forEach((lang) => {
it(`should register '${lang}' when the module is loaded`, () => {
expect(hljs.getLanguage(lang)).to.be.an('object');
});
});
});
describe('register', () => {
afterEach(() => {
hljs
.listLanguages()
.filter((lang) => !eagerLanguages.includes(lang))
.forEach((lang) => hljs.unregisterLanguage(lang));
});
lazyLanguages.forEach((lang) => {
it(`should register a syntax definition for '${lang}'`, async () => {
hljs.unregisterLanguage(lang);
await register(lang);
expect(hljs.listLanguages()).to.include(lang);
});
});
['', 'not-a-language'].forEach((lang) => {
it(`should fall back to plaintext for '${lang}'`, async () => {
await register(lang);
expect(hljs.listLanguages()).to.include('plaintext');
});
});
});
});

View File

@@ -0,0 +1,43 @@
import proxyquire from 'proxyquire';
export type MarkdownToken = { token: string; type?: string; text: string; noHtml?: string };
export type ParsedMessage = { html?: string; msg?: string; tokens?: MarkdownToken[] };
export type MarkdownOptions = { supportSchemesForLink?: string; headers?: boolean; rootUrl?: string };
const mocks = {
'meteor/meteor': {
Meteor: {
absoluteUrl() {
return 'http://localhost:3000/';
},
},
},
};
export const { Markdown, filterMarkdown, createMarkdownMessageRenderer, createMarkdownNotificationRenderer } = proxyquire
.noCallThru()
.load('../../../../app/markdown/lib/markdown', mocks) as typeof import('../../../../app/markdown/lib/markdown');
export const { original } = proxyquire.noCallThru().load('../../../../app/markdown/lib/parser/original/original', mocks) as {
original: (message: ParsedMessage, options?: MarkdownOptions) => ParsedMessage;
};
export const { code } = proxyquire
.noCallThru()
.load('../../../../app/markdown/lib/parser/original/code', mocks) as typeof import('../../../../app/markdown/lib/parser/original/code');
const copyonly = (text: string, marker: string) => `<span class="copyonly">${marker}</span>${text}<span class="copyonly">${marker}</span>`;
export const markup = {
bold: (text: string) => copyonly(`<strong>${text}</strong>`, '*'),
inlineCode: (text: string) => copyonly(`<span><code class="code-colors inline">${text}</code></span>`, '`'),
anchor: (url: string, title: string, target = '_blank') =>
`<a data-title="${url}" href="${url}" target="${target}" rel="noopener noreferrer">${title}</a>`,
image: (url: string, title: string, target = '_blank') =>
`<a data-title="${url}" href="${url}" title="${title}" target="${target}" rel="noopener noreferrer">` +
`<div class="inline-image" style="background-image: url(${url});"></div></a>`,
blockquote: (text: string, marker = '&gt;') =>
`<blockquote class="background-transparent-darker-before"><span class="copyonly">${marker}</span>${text}</blockquote>`,
};

View File

@@ -0,0 +1,235 @@
import { escapeHTML } from '@rocket.chat/tools';
import { expect } from 'chai';
import sinon from 'sinon';
import type { MarkdownOptions, ParsedMessage } from './markdown.mocks';
import {
Markdown,
original,
code,
filterMarkdown,
createMarkdownMessageRenderer,
createMarkdownNotificationRenderer,
markup,
} from './markdown.mocks';
import hljs from '../../../../app/markdown/lib/hljs';
const { bold, inlineCode, anchor, image, blockquote } = markup;
const rootUrl = 'http://localhost:3000/';
const defaultOptions = { supportSchemesForLink: 'http,https', headers: true, rootUrl };
const render = (text: string, options: MarkdownOptions = defaultOptions) =>
Markdown.mountTokensBack(original({ html: text }, options)).html;
const renderEscaped = (text: string, options: MarkdownOptions = defaultOptions) => render(escapeHTML(text), options);
describe('Markdown entry points', () => {
it('should escape HTML before parsing', () => {
expect(Markdown.parse('<img src=x onerror=alert(1)> *Hello*')).to.equal(`&lt;img src=x onerror=alert(1)&gt; ${bold('Hello')}`);
});
it('should keep HTML when parsing unescaped input', () => {
expect(Markdown.parseNotEscaped('<b>x</b> *Hello*')).to.equal(`<b>x</b> ${bold('Hello')}`);
});
it('should leave the tokens unmounted when parsing a message', () => {
const message = Markdown.parseMessageNotEscaped({ html: '`code`' });
expect(message.tokens).to.have.lengthOf(1);
expect(message.tokens?.[0].type).to.equal('inlinecode');
expect(message.html).to.equal(message.tokens?.[0].token);
});
it('should expose the code parser', () => {
const message = Markdown.code({ html: '`code`' });
expect(Markdown.mountTokensBack(message).html).to.equal(inlineCode('code'));
});
it('should strip the markdown syntax when filtering', () => {
expect(Markdown.filterMarkdownFromMessage('*Hello* `there`')).to.equal('Hello there');
expect(filterMarkdown('*Hello* `there`')).to.equal('Hello there');
});
});
describe('Markdown token restoration', () => {
it('should return the message untouched when it has no tokens', () => {
const message = { html: 'plain text' };
expect(Markdown.mountTokensBack(message)).to.equal(message);
expect(message.html).to.equal('plain text');
});
it('should restore the plain text representation when useHtml is false', () => {
const message = Markdown.parseMessageNotEscaped({ html: 'see `code`' });
expect(Markdown.mountTokensBack(message, false).html).to.equal('see `code`');
});
it('should restore tokens that are only revealed by another token', () => {
const message = {
html: 'start =!=outer=!=',
tokens: [
{ token: '=!=inner=!=', text: '<em>inner</em>' },
{ token: '=!=outer=!=', text: '<b>=!=inner=!=</b>' },
],
};
expect(Markdown.mountTokensBack(message).html).to.equal('start <b><em>inner</em></b>');
});
it('should leave tokens that never appear in the message', () => {
const message = {
html: 'start =!=present=!=',
tokens: [
{ token: '=!=absent=!=', text: '<em>absent</em>' },
{ token: '=!=present=!=', text: '<b>present</b>' },
],
};
expect(Markdown.mountTokensBack(message).html).to.equal('start <b>present</b>');
});
it('should not treat $ sequences in the token text as replacement patterns', () => {
const message = { html: '=!=t=!=', tokens: [{ token: '=!=t=!=', text: '$& $1 $$' }] };
expect(Markdown.mountTokensBack(message).html).to.equal('$& $1 $$');
});
});
describe('Markdown renderers', () => {
const renderMessage = createMarkdownMessageRenderer({
rootUrl: 'http://localhost:3000/',
supportSchemesForLink: 'http,https',
headers: true,
});
it('should render markdown of a non-empty message', () => {
expect(renderMessage({ html: '# Hello' })?.html).to.equal('<h1>Hello</h1>');
});
const emptyMessages: (ParsedMessage | undefined)[] = [undefined, { html: '' }, { html: ' \n\t ' }];
emptyMessages.forEach((message, index) => {
it(`should return the message as-is when there is nothing to render (case ${index})`, () => {
expect(renderMessage(message)).to.equal(message);
});
});
it('should strip the markdown syntax when rendering a notification', () => {
expect(createMarkdownNotificationRenderer()('*Hello* there')).to.equal('Hello there');
});
});
describe('Original parser', () => {
it('should convert every new line to a <br>', () => {
expect(render('a\nb\nc')).to.equal('a<br>b<br>c');
});
it('should not render headings when the option is disabled', () => {
expect(render('# Hello', { ...defaultOptions, headers: false })).to.equal('# Hello');
});
describe('blockquotes', () => {
it('should render consecutive single-line blockquotes without a line break in between', () => {
expect(renderEscaped('>a\n>b')).to.equal(`${blockquote('a')}${blockquote('b')}`);
});
it('should render a multiline blockquote delimited by >>> and <<<', () => {
expect(renderEscaped('>>>\nline1\nline2\n<<<')).to.equal(
blockquote('line1<br>line2<span class="copyonly">&lt;&lt;&lt;</span>', '&gt;&gt;&gt;'),
);
});
});
describe('links', () => {
it('should render an internal link without a target', () => {
expect(render(`[Home](${rootUrl}channel/general)`)).to.equal(anchor(`${rootUrl}channel/general`, 'Home', ''));
expect(render(`<${rootUrl}channel/general|Home>`)).to.equal(anchor(`${rootUrl}channel/general`, 'Home', ''));
});
it('should not render a link whose URL is not parseable', () => {
expect(render('[Text](http://[)')).to.equal('[Text](http://[)');
expect(render('<http://[|Text>')).to.equal('<http://[|Text>');
});
it('should not render a link whose URL contains another token', () => {
expect(render('[Text](http://example.com/`code`)')).to.equal(`[Text](http://example.com/${inlineCode('code')})`);
});
it('should not render a link whose title holds a disallowed token', () => {
expect(render('[`code`](http://example.com/)')).to.equal(`[${inlineCode('code')}](http://example.com/)`);
expect(render('<http://example.com/|`code`>')).to.equal(`<http://example.com/|${inlineCode('code')}>`);
});
it('should render a link whose title only looks like a token', () => {
expect(render('[=!=abcdefghijklmnopq=!=](http://example.com/)')).to.equal(anchor('http://example.com/', '=!=abcdefghijklmnopq=!='));
});
it('should only render the schemes it was configured with', () => {
expect(render('[Text](ftp://example.com/)', { ...defaultOptions, supportSchemesForLink: 'ftp' })).to.equal(
anchor('ftp://example.com/', 'Text'),
);
expect(render('[Text](http://example.com/)', { ...defaultOptions, supportSchemesForLink: 'ftp' })).to.equal(
'[Text](http://example.com/)',
);
});
});
describe('images', () => {
it('should render an external image with target _blank', () => {
expect(render('![alt](http://example.com/a.png)')).to.equal(image('http://example.com/a.png', 'alt'));
});
it('should render an internal image without a target', () => {
expect(render(`![alt](${rootUrl}a.png)`)).to.equal(image(`${rootUrl}a.png`, 'alt', ''));
});
it('should not render an image whose URL is not parseable', () => {
expect(render('![alt](http://[)')).to.equal('![alt](http://[)');
});
it('should not render an image whose title holds a disallowed token', () => {
expect(render('![`code`](http://example.com/a.png)')).to.equal(`![${inlineCode('code')}](http://example.com/a.png)`);
});
});
});
describe('Code parser', () => {
it('should leave a message without code untouched', () => {
expect(code({ html: 'no code here' }).tokens).to.be.undefined;
expect(code({}).html).to.be.undefined;
});
it('should close an unclosed code block in both html and msg', () => {
const message: ParsedMessage = { html: '```code', msg: '```code' };
code(message);
expect(message.msg).to.equal('```code\n```');
expect(message.html).to.equal(message.tokens?.[0].token);
});
it('should restore the original fenced block when tokens are mounted back as text', () => {
const message = original({ html: '```\ncode\n```' }, defaultOptions);
expect(Markdown.mountTokensBack(message, false).html).to.equal('```\ncode\n```');
});
it('should fall back to automatic highlighting when highlighting the explicit language fails', () => {
const consoleError = sinon.stub(console, 'error');
const highlight = sinon.stub(hljs, 'highlight').throws(new Error('highlighting failed'));
const highlightAuto = sinon.spy(hljs, 'highlightAuto');
try {
const message = code({ html: '```javascript\nvar a;\n```' });
expect(highlight.args).to.deep.equal([['javascript', 'var a;\n']]);
expect(highlightAuto.calledOnce).to.be.true;
expect(message.tokens?.[0].text).to.contain(`code-colors hljs ${highlightAuto.firstCall.returnValue.language}'`);
expect(consoleError.calledOnce).to.be.true;
} finally {
sinon.restore();
}
});
});