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,53 @@
import { getFilteredLinkRegexes } from '../linkRegexes';
const inlineCodeRegex = /`([^`\r\n]+)\`/gm;
/**
* Filter markdown tags in message
* Use case: notifications
* @param {string} message
*/
export const filtered = (
message,
options = {
supportSchemesForLink: 'http,https',
},
) => {
const schemes = (options.supportSchemesForLink || 'http,https').split(',').join('|');
const linkRegexes = getFilteredLinkRegexes(schemes);
// Remove block code backticks
message = message.replace(/```/g, '');
// Remove inline code backticks
message = message.replace(inlineCodeRegex, (match) => match.substr(1, match.length - 2));
// Filter [text](url), ![alt_text](image_url)
message = message.replace(linkRegexes.link, (match, title) => title);
// Filter <http://link|Text>
message = message.replace(linkRegexes.pipedLink, (match, title) => title);
// Filter headings
message = message.replace(
/(^#{1,4}) (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm,
'$2',
);
// Filter bold
message = message.replace(/(^|>|[ >_~`])\*{1,2}([^\*\r\n]+)\*{1,2}([<_~`]|\B|\b|$)/gm, '$1$2$3');
// Filter italics
message = message.replace(/(^|>|[ >*~`])\_{1,2}([^\_\r\n]+)\_{1,2}([<*~`]|\B|\b|$)/gm, '$1$2$3');
// Filter strike-through text
message = message.replace(/(^|>|[ >_*`])\~{1,2}([^~\r\n]+)\~{1,2}([<_*`]|\B|\b|$)/gm, '$1$2$3');
// Filter block quotes
message = message.replace(/(?:>){3}\n+([\s\S]*?)\n+(?:<){3}/g, '$1');
// Filter > quote
message = message.replace(/^>(.*)$/gm, '$1');
return message;
};

View File

@@ -0,0 +1,20 @@
import mem from 'mem';
// The link regexes only vary with the configured URL schemes, which come from
// a setting that rarely changes; cache the compiled regexes per schemes value
// instead of recompiling them for every message.
//
// The two parsers intentionally use different patterns: `filtered` only strips
// markup (no URL capture, URLs may contain `)` escapes) while `original`
// renders it (captures the URL, splits image from link).
export const getMarkdownLinkRegexes = mem((schemes) => ({
image: new RegExp(`!\\[([^\\]]+)\\]\\(((?:${schemes}):\\/\\/[^\\s]+)\\)`, 'gm'),
link: new RegExp(`\\[([^\\]]+)\\]\\(((?:${schemes}):\\/\\/[^\\s]+)\\)`, 'gm'),
pipedLink: new RegExp(`(?:<|&lt;)((?:${schemes}):\\/\\/[^\\|]+)\\|(.+?)(?=>|&gt;)(?:>|&gt;)`, 'gm'),
}));
export const getFilteredLinkRegexes = mem((schemes) => ({
link: new RegExp(`!?\\[([^\\]]+)\\]\\((?:${schemes}):\\/\\/[^\\)]+\\)`, 'gm'),
pipedLink: new RegExp(`(?:<|&lt;)(?:${schemes}):\\/\\/[^\\|]+\\|(.+?)(?=>|&gt;)(?:>|&gt;)`, 'gm'),
}));

View File

@@ -0,0 +1,87 @@
/*
* code() is a named function that will parse `inline code` and ```codeblock``` syntaxes
* @param {Object} message - The message object
*/
import { unescapeHTML } from '@rocket.chat/tools';
import { addAsToken } from './token';
import hljs, { register } from '../../hljs';
const inlinecode = (message) => {
// Support `text`
message.html = message.html.replace(/\`([^`\r\n]+)\`([<_*~]|\B|\b|$)/gm, (match, p1, p2) =>
addAsToken(
message,
`<span class=\"copyonly\">\`</span><span><code class=\"code-colors inline\">${p1}</code></span><span class=\"copyonly\">\`</span>${p2}`,
'inlinecode',
{ noHtml: match },
),
);
};
const codeblocks = (message) => {
// Count occurencies of ```
const count = (message.html.match(/```/gm) || []).length;
if (count) {
// Check if we need to add a final ```
if (count % 2 > 0) {
message.html = `${message.html}\n\`\`\``;
message.msg = `${message.msg}\n\`\`\``;
}
// Separate text in code blocks and non code blocks
const msgParts = message.html.split(/(^.*)(```(?:[a-zA-Z]+)?(?:(?:.|\r|\n)*?)```)(.*\n?)$/gm);
for (let index = 0; index < msgParts.length; index++) {
// Verify if this part is code
const part = msgParts[index];
const codeMatch = part.match(/^```[\r\n]*(.*[\r\n\ ]?)[\r\n]*([\s\S]*?)```+?$/);
if (codeMatch) {
// Process highlight if this part is code
const singleLine = codeMatch[0].indexOf('\n') === -1;
const lang = !singleLine && Array.from(hljs.listLanguages()).includes(codeMatch[1].trim()) ? codeMatch[1].trim() : '';
const emptyLanguage = lang === '' ? unescapeHTML(codeMatch[1] + codeMatch[2]) : unescapeHTML(codeMatch[2]);
const code = singleLine ? unescapeHTML(codeMatch[1]) : emptyLanguage;
const result = (() => {
if (lang) {
try {
register(lang);
return hljs.highlight(lang, code);
} catch (error) {
console.error(error);
}
}
return hljs.highlightAuto(lang + code);
})();
const token = addAsToken(
message,
`<pre><code class='code-colors hljs ${result.language}'><span class='copyonly'>\`\`\`<br></span>${result.value}<span class='copyonly'><br>\`\`\`</span></code></pre>`,
'code',
{
noHtml: codeMatch[0],
highlight: true,
},
);
msgParts[index] = token;
} else {
msgParts[index] = part;
}
}
// Re-mount message
message.html = msgParts.join('');
}
};
export const code = (message) => {
if (message.html?.trim()) {
codeblocks(message);
inlinecode(message);
}
return message;
};

View File

@@ -0,0 +1,194 @@
import { addAsToken, isToken, validateAllowedTokens } from './token';
import { getMarkdownLinkRegexes } from '../linkRegexes';
const validateUrl = (url, message) => {
// Don't render markdown inside links
if (message?.tokens?.some((token) => url.includes(token.token))) {
return false;
}
// Valid urls don't contain whitespaces
if (/\s/.test(url.trim())) {
return false;
}
try {
new URL(url);
return true;
} catch (e) {
return false;
}
};
const endsWithWhitespace = (text) => text.substring(text.length - 1).match(/\s/);
const getParseableMarkersCount = (start, end) => {
const usableMarkers = start.length > 1 ? 2 : 1;
return end.length - usableMarkers >= 0 ? usableMarkers : 1;
};
const getTextWrapper = (marker, tagName) => (textPrepend, wrappedText, textAppend) =>
`${textPrepend}<span class="copyonly">${marker}</span><${tagName}>${wrappedText}</${tagName}><span class="copyonly">${marker}</span>${textAppend}`;
const getRegexReplacer = (replaceFunction, getRegex) => (marker, tagName) => {
const wrapper = getTextWrapper(marker, tagName);
// The regex only depends on the marker, so build it once when the parser is
// created instead of on every message. Safe with /g because
// String.prototype.replace resets lastIndex on each call.
const regex = getRegex(marker);
return (msg) => msg.replace(regex, (...args) => replaceFunction(wrapper, ...args));
};
const getParserWithCustomMarker = getRegexReplacer(
(wrapper, match, p1, p2, p3) => {
if (endsWithWhitespace(p2)) {
return match;
}
const finalMarkerCount = getParseableMarkersCount(p1, p3);
return wrapper(p1.substring(finalMarkerCount), p2, p3.substring(finalMarkerCount));
},
(marker) => new RegExp(`(\\${marker}+(?!\\s))([^\\${marker}\\r\\n]+)(\\${marker}+)`, 'gm'),
);
const parseBold = getParserWithCustomMarker('*', 'strong');
const parseStrike = getParserWithCustomMarker('~', 'strike');
const parseItalic = getRegexReplacer(
(wrapper, match, p1, p2, p3, p4, p5) => {
if (p1 || p5 || endsWithWhitespace(p3)) {
return match;
}
const finalMarkerCount = getParseableMarkersCount(p2, p4);
return wrapper(p2.substring(finalMarkerCount), p3, p4.substring(finalMarkerCount));
},
() => new RegExp('([^\\r\\n\\s~*_]){0,1}(\\_+(?!\\s))([^\\_\\r\\n]+)(\\_+)([^\\r\\n\\s]){0,1}', 'gm'),
)('_', 'em');
const parseNotEscaped = (message, { supportSchemesForLink, headers, rootUrl }) => {
let msg = message.html;
if (!message.tokens) {
message.tokens = [];
}
const schemes = (supportSchemesForLink || '').split(',').join('|');
const linkRegexes = getMarkdownLinkRegexes(schemes);
if (headers) {
// Support # Text for h1
msg = msg.replace(
/^# (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm,
'<h1>$1</h1>',
);
// Support # Text for h2
msg = msg.replace(
/^## (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm,
'<h2>$1</h2>',
);
// Support # Text for h3
msg = msg.replace(
/^### (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm,
'<h3>$1</h3>',
);
// Support # Text for h4
msg = msg.replace(
/^#### (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm,
'<h4>$1</h4>',
);
}
// Support *text* to make bold
msg = parseBold(msg);
// Support _text_ to make italics
msg = parseItalic(msg);
// // Support ~text~ to strike through text
msg = parseStrike(msg);
// Support for block quote
// >>>
// Text
// <<<
msg = msg.replace(
/(?:&gt;){3}\n+([\s\S]*?)\n+(?:&lt;){3}/g,
'<blockquote class="background-transparent-darker-before"><span class="copyonly">&gt;&gt;&gt;</span>$1<span class="copyonly">&lt;&lt;&lt;</span></blockquote>',
);
// Support >Text for quote
msg = msg.replace(
/^&gt;(.*)$/gm,
'<blockquote class="background-transparent-darker-before"><span class="copyonly">&gt;</span>$1</blockquote>',
);
// Remove white-space around blockquote (prevent <br>). Because blockquote is block element.
msg = msg.replace(
/\s*<blockquote class="background-transparent-darker-before">/gm,
'<blockquote class="background-transparent-darker-before">',
);
msg = msg.replace(/<\/blockquote>\s*/gm, '</blockquote>');
// Remove new-line between blockquotes.
msg = msg.replace(/<\/blockquote>\n<blockquote/gm, '</blockquote><blockquote');
// Support ![alt text](http://image url)
msg = msg.replace(linkRegexes.image, (match, title, url) => {
if (!validateUrl(url, message)) {
return match;
}
if (isToken(title) && !validateAllowedTokens(message, title, ['bold', 'italic', 'strike'])) {
return match;
}
url = encodeURI(url);
const target = url.indexOf(rootUrl) === 0 ? '' : '_blank';
return addAsToken(
message,
`<a data-title="${url}" href="${url}" title="${title}" target="${target}" rel="noopener noreferrer"><div class="inline-image" style="background-image: url(${url});"></div></a>`,
'link',
);
});
// Support [Text](http://link)
msg = msg.replace(linkRegexes.link, (match, title, url) => {
if (!validateUrl(url, message)) {
return match;
}
if (isToken(title) && !validateAllowedTokens(message, title, ['bold', 'italic', 'strike'])) {
return match;
}
const target = url.indexOf(rootUrl) === 0 ? '' : '_blank';
title = title.replace(/&amp;/g, '&');
const escapedUrl = encodeURI(url);
return addAsToken(
message,
`<a data-title="${escapedUrl}" href="${escapedUrl}" target="${target}" rel="noopener noreferrer">${title}</a>`,
'link',
);
});
// Support <http://link|Text>
msg = msg.replace(linkRegexes.pipedLink, (match, url, title) => {
if (!validateUrl(url, message)) {
return match;
}
if (isToken(title) && !validateAllowedTokens(message, title, ['bold', 'italic', 'strike'])) {
return match;
}
url = encodeURI(url);
const target = url.indexOf(rootUrl) === 0 ? '' : '_blank';
return addAsToken(message, `<a data-title="${url}" href="${url}" target="${target}" rel="noopener noreferrer">${title}</a>`, 'link');
});
return msg;
};
export const markdown = (message, options) => {
message.html = parseNotEscaped(message, options);
return message;
};

View File

@@ -0,0 +1,25 @@
/*
* Markdown is a named function that will parse markdown syntax
* @param {Object} message - The message object
*/
import { code } from './code.js';
import { markdown } from './markdown.js';
export const original = (
message,
options = {
supportSchemesForLink: 'http,https',
headers: true,
},
) => {
// Parse markdown code
message = code(message);
// Parse markdown
message = markdown(message, options);
// Replace linebreak to br
message.html = message.html.replace(/\n/gm, '<br>');
return message;
};

View File

@@ -0,0 +1,38 @@
/*
* Markdown is a named function that will parse markdown syntax
* @param {String} msg - The message html
*/
import type { TokenType, TokenExtra } from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
type MessageTokens = {
tokens?: {
token: string;
type: TokenType;
text: string;
extra?: TokenExtra;
}[];
};
export const addAsToken = (message: MessageTokens, html: string, type: TokenType, extra?: TokenExtra): string => {
if (!message.tokens) {
message.tokens = [];
}
const token = `=!=${Random.id()}=!=`;
message.tokens.push({
token,
type,
text: html,
...(extra && { ...extra }),
});
return token;
};
export const isToken = (msg: string): boolean => /=!=[.a-z0-9]{17}=!=/gim.test(msg.trim());
export const validateAllowedTokens = (message: MessageTokens, id: string, desiredTokens: TokenType[]): boolean => {
const tokens: string[] = id.match(/=!=[.a-z0-9]{17}=!=/gim) || [];
const tokensFound = message.tokens?.filter(({ token }) => tokens.includes(token)) || [];
return tokensFound.length === 0 || tokensFound.every((token) => token.type && desiredTokens.includes(token.type));
};