Apply prettier formatting

This commit is contained in:
Michael Weimann 2022-12-12 12:24:14 +01:00
parent 1cac306093
commit 526645c791
No known key found for this signature in database
GPG key ID: 53F535A266BB9584
1576 changed files with 65385 additions and 62478 deletions

View file

@ -17,29 +17,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ReactNode } from 'react';
import sanitizeHtml from 'sanitize-html';
import cheerio from 'cheerio';
import classNames from 'classnames';
import EMOJIBASE_REGEX from 'emojibase-regex';
import { split } from 'lodash';
import katex from 'katex';
import { decode } from 'html-entities';
import { IContent } from 'matrix-js-sdk/src/models/event';
import { Optional } from 'matrix-events-sdk';
import React, { ReactNode } from "react";
import sanitizeHtml from "sanitize-html";
import cheerio from "cheerio";
import classNames from "classnames";
import EMOJIBASE_REGEX from "emojibase-regex";
import { split } from "lodash";
import katex from "katex";
import { decode } from "html-entities";
import { IContent } from "matrix-js-sdk/src/models/event";
import { Optional } from "matrix-events-sdk";
import {
_linkifyElement,
_linkifyString,
ELEMENT_URL_PATTERN,
options as linkifyMatrixOptions,
} from './linkify-matrix';
import { IExtendedSanitizeOptions } from './@types/sanitize-html';
import SettingsStore from './settings/SettingsStore';
} from "./linkify-matrix";
import { IExtendedSanitizeOptions } from "./@types/sanitize-html";
import SettingsStore from "./settings/SettingsStore";
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
import { getEmojiFromUnicode } from "./emoji";
import { mediaFromMxc } from "./customisations/Media";
import { stripHTMLReply, stripPlainReply } from './utils/Reply';
import { stripHTMLReply, stripPlainReply } from "./utils/Reply";
// Anything outside the basic multilingual plane will be a surrogate pair
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
@ -55,7 +55,7 @@ const ZWJ_REGEX = /[\u200D\u2003]/g;
// Regex pattern for whitespace characters
const WHITESPACE_REGEX = /\s/g;
const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, "i");
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
@ -107,7 +107,7 @@ function mightContainEmoji(str: string): boolean {
*/
export function unicodeToShortcode(char: string): string {
const shortcodes = getEmojiFromUnicode(char)?.shortcodes;
return shortcodes?.length ? `:${shortcodes[0]}:` : '';
return shortcodes?.length ? `:${shortcodes[0]}:` : "";
}
/*
@ -126,7 +126,7 @@ export function getHtmlText(insaneHtml: string): string {
allowedAttributes: {},
selfClosing: [],
allowedSchemes: [],
disallowedTagsMode: 'discard',
disallowedTagsMode: "discard",
});
}
@ -147,11 +147,12 @@ export function isUrlPermitted(inputUrl: string): boolean {
}
}
const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to matrix
const transformTags: IExtendedSanitizeOptions["transformTags"] = {
// custom to matrix
// add blank targets to all hyperlinks except vector URLs
'a': function(tagName: string, attribs: sanitizeHtml.Attributes) {
"a": function (tagName: string, attribs: sanitizeHtml.Attributes) {
if (attribs.href) {
attribs.target = '_blank'; // by default
attribs.target = "_blank"; // by default
const transformed = tryTransformPermalinkToLocalHref(attribs.href); // only used to check if it is a link that can be handled locally
if (
@ -165,10 +166,10 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
delete attribs.href;
}
attribs.rel = 'noreferrer noopener'; // https://mathiasbynens.github.io/rel-noopener/
attribs.rel = "noreferrer noopener"; // https://mathiasbynens.github.io/rel-noopener/
return { tagName, attribs };
},
'img': function(tagName: string, attribs: sanitizeHtml.Attributes) {
"img": function (tagName: string, attribs: sanitizeHtml.Attributes) {
let src = attribs.src;
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
// because transformTags is used _before_ we filter by allowedSchemesByTag and
@ -208,18 +209,18 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height);
return { tagName, attribs };
},
'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {
if (typeof attribs.class !== 'undefined') {
"code": function (tagName: string, attribs: sanitizeHtml.Attributes) {
if (typeof attribs.class !== "undefined") {
// Filter out all classes other than ones starting with language- for syntax highlighting.
const classes = attribs.class.split(/\s/).filter(function(cl) {
return cl.startsWith('language-') && !cl.startsWith('language-_');
const classes = attribs.class.split(/\s/).filter(function (cl) {
return cl.startsWith("language-") && !cl.startsWith("language-_");
});
attribs.class = classes.join(' ');
attribs.class = classes.join(" ");
}
return { tagName, attribs };
},
// eslint-disable-next-line @typescript-eslint/naming-convention
'*': function(tagName: string, attribs: sanitizeHtml.Attributes) {
"*": function (tagName: string, attribs: sanitizeHtml.Attributes) {
// Delete any style previously assigned, style is an allowedTag for font, span & img,
// because attributes are stripped after transforming.
// For img this is trusted as it is generated wholly within the img transformation method.
@ -230,8 +231,8 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
// Sanitise and transform data-mx-color and data-mx-bg-color to their CSS
// equivalents
const customCSSMapper = {
'data-mx-color': 'color',
'data-mx-bg-color': 'background-color',
"data-mx-color": "color",
"data-mx-bg-color": "background-color",
// $customAttributeKey: $cssAttributeKey
};
@ -239,8 +240,9 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
Object.keys(customCSSMapper).forEach((customAttributeKey) => {
const cssAttributeKey = customCSSMapper[customAttributeKey];
const customAttributeValue = attribs[customAttributeKey];
if (customAttributeValue &&
typeof customAttributeValue === 'string' &&
if (
customAttributeValue &&
typeof customAttributeValue === "string" &&
COLOR_REGEX.test(customAttributeValue)
) {
style += cssAttributeKey + ":" + customAttributeValue + ";";
@ -258,28 +260,61 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
const sanitizeHtmlParams: IExtendedSanitizeOptions = {
allowedTags: [
'font', // custom to matrix for IRC-style font coloring
'del', // for markdown
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub',
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
'details', 'summary',
"font", // custom to matrix for IRC-style font coloring
"del", // for markdown
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"blockquote",
"p",
"a",
"ul",
"ol",
"sup",
"sub",
"nl",
"li",
"b",
"i",
"u",
"strong",
"em",
"strike",
"code",
"hr",
"br",
"div",
"table",
"thead",
"caption",
"tbody",
"tr",
"th",
"td",
"pre",
"span",
"img",
"details",
"summary",
],
allowedAttributes: {
// attribute sanitization happens after transformations, so we have to accept `style` for font, span & img
// but strip during the transformation.
// custom ones first:
font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
span: ['data-mx-maths', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style'], // custom to matrix
div: ['data-mx-maths'],
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
font: ["color", "data-mx-bg-color", "data-mx-color", "style"], // custom to matrix
span: ["data-mx-maths", "data-mx-bg-color", "data-mx-color", "data-mx-spoiler", "style"], // custom to matrix
div: ["data-mx-maths"],
a: ["href", "name", "target", "rel"], // remote target: custom to matrix
// img tags also accept width/height, we just map those to max-width & max-height during transformation
img: ['src', 'alt', 'title', 'style'],
ol: ['start'],
code: ['class'], // We don't actually allow all classes, we filter them in transformTags
img: ["src", "alt", "title", "style"],
ol: ["start"],
code: ["class"], // We don't actually allow all classes, we filter them in transformTags
},
// Lots of these won't come up by default because we don't allow them
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
selfClosing: ["img", "br", "hr", "area", "base", "basefont", "input", "link", "meta"],
// URL schemes we permit
allowedSchemes: PERMITTED_URL_SCHEMES,
allowProtocolRelative: false,
@ -292,8 +327,8 @@ const sanitizeHtmlParams: IExtendedSanitizeOptions = {
const composerSanitizeHtmlParams: IExtendedSanitizeOptions = {
...sanitizeHtmlParams,
transformTags: {
'code': transformTags['code'],
'*': transformTags['*'],
"code": transformTags["code"],
"*": transformTags["*"],
},
};
@ -301,17 +336,25 @@ const composerSanitizeHtmlParams: IExtendedSanitizeOptions = {
const topicSanitizeHtmlParams: IExtendedSanitizeOptions = {
...sanitizeHtmlParams,
allowedTags: [
'font', // custom to matrix for IRC-style font coloring
'del', // for markdown
'a', 'sup', 'sub',
'b', 'i', 'u', 'strong', 'em', 'strike', 'br', 'div',
'span',
"font", // custom to matrix for IRC-style font coloring
"del", // for markdown
"a",
"sup",
"sub",
"b",
"i",
"u",
"strong",
"em",
"strike",
"br",
"div",
"span",
],
};
abstract class BaseHighlighter<T extends React.ReactNode> {
constructor(public highlightClass: string, public highlightLink: string) {
}
constructor(public highlightClass: string, public highlightLink: string) {}
/**
* apply the highlights to a section of text
@ -408,8 +451,11 @@ export interface IOptsReturnString extends IOpts {
const emojiToHtmlSpan = (emoji: string) =>
`<span class='mx_Emoji' title='${unicodeToShortcode(emoji)}'>${emoji}</span>`;
const emojiToJsxSpan = (emoji: string, key: number) =>
<span key={key} className='mx_Emoji' title={unicodeToShortcode(emoji)}>{ emoji }</span>;
const emojiToJsxSpan = (emoji: string, key: number) => (
<span key={key} className="mx_Emoji" title={unicodeToShortcode(emoji)}>
{emoji}
</span>
);
/**
* Wraps emojis in <span> to style them separately from the rest of message. Consecutive emojis (and modifiers) are wrapped
@ -422,15 +468,15 @@ const emojiToJsxSpan = (emoji: string, key: number) =>
function formatEmojis(message: string, isHtmlMessage: boolean): (JSX.Element | string)[] {
const emojiToSpan = isHtmlMessage ? emojiToHtmlSpan : emojiToJsxSpan;
const result: (JSX.Element | string)[] = [];
let text = '';
let text = "";
let key = 0;
// We use lodash's grapheme splitter to avoid breaking apart compound emojis
for (const char of split(message, '')) {
for (const char of split(message, "")) {
if (EMOJIBASE_REGEX.test(char)) {
if (text) {
result.push(text);
text = '';
text = "";
}
result.push(emojiToSpan(char, key));
key++;
@ -480,8 +526,8 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
?.filter((highlight: string): boolean => !highlight.includes("<"))
.map((highlight: string): string => sanitizeHtml(highlight, sanitizeParams));
let formattedBody = typeof content.formatted_body === 'string' ? content.formatted_body : null;
const plainBody = typeof content.body === 'string' ? content.body : "";
let formattedBody = typeof content.formatted_body === "string" ? content.formatted_body : null;
const plainBody = typeof content.body === "string" ? content.body : "";
if (opts.stripReplyFallback && formattedBody) formattedBody = stripHTMLReply(formattedBody);
strippedBody = opts.stripReplyFallback ? stripPlainReply(plainBody) : plainBody;
@ -498,8 +544,8 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
// are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
// by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure.
sanitizeParams.textFilter = function(safeText) {
return highlighter.applyHighlights(safeText, safeHighlights).join('');
sanitizeParams.textFilter = function (safeText) {
return highlighter.applyHighlights(safeText, safeHighlights).join("");
};
}
@ -516,23 +562,21 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
if (isHtmlMessage && SettingsStore.getValue("feature_latex_maths")) {
// @ts-ignore - The types for `replaceWith` wrongly expect
// Cheerio instance to be returned.
phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
return katex.renderToString(
decode(phtml(e).attr('data-mx-maths')),
{
throwOnError: false,
// @ts-ignore - `e` can be an Element, not just a Node
displayMode: e.name == 'div',
output: "htmlAndMathml",
});
phtml('div, span[data-mx-maths!=""]').replaceWith(function (i, e) {
return katex.renderToString(decode(phtml(e).attr("data-mx-maths")), {
throwOnError: false,
// @ts-ignore - `e` can be an Element, not just a Node
displayMode: e.name == "div",
output: "htmlAndMathml",
});
});
safeBody = phtml.html();
}
if (bodyHasEmoji) {
safeBody = formatEmojis(safeBody, true).join('');
safeBody = formatEmojis(safeBody, true).join("");
}
} else if (highlighter) {
safeBody = highlighter.applyHighlights(plainBody, safeHighlights).join('');
safeBody = highlighter.applyHighlights(plainBody, safeHighlights).join("");
}
} finally {
delete sanitizeParams.textFilter;
@ -545,34 +589,34 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
let emojiBody = false;
if (!opts.disableBigEmoji && bodyHasEmoji) {
let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : '';
let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : "";
// Ignore spaces in body text. Emojis with spaces in between should
// still be counted as purely emoji messages.
contentBodyTrimmed = contentBodyTrimmed.replace(WHITESPACE_REGEX, '');
contentBodyTrimmed = contentBodyTrimmed.replace(WHITESPACE_REGEX, "");
// Remove zero width joiner characters from emoji messages. This ensures
// that emojis that are made up of multiple unicode characters are still
// presented as large.
contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, '');
contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, "");
const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed);
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length &&
// Prevent user pills expanding for users with only emoji in
// their username. Permalinks (links in pills) can be any URL
// now, so we just check for an HTTP-looking thing.
(
strippedBody === safeBody || // replies have the html fallbacks, account for that here
content.formatted_body === undefined ||
(!content.formatted_body.includes("http:") &&
!content.formatted_body.includes("https:"))
);
emojiBody =
match &&
match[0] &&
match[0].length === contentBodyTrimmed.length &&
// Prevent user pills expanding for users with only emoji in
// their username. Permalinks (links in pills) can be any URL
// now, so we just check for an HTTP-looking thing.
(strippedBody === safeBody || // replies have the html fallbacks, account for that here
content.formatted_body === undefined ||
(!content.formatted_body.includes("http:") && !content.formatted_body.includes("https:")));
}
const className = classNames({
'mx_EventTile_body': true,
'mx_EventTile_bigEmoji': emojiBody,
'markdown-body': isHtmlMessage && !emojiBody,
"mx_EventTile_body": true,
"mx_EventTile_bigEmoji": emojiBody,
"markdown-body": isHtmlMessage && !emojiBody,
});
let emojiBodyElements: JSX.Element[];
@ -580,16 +624,19 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[];
}
return safeBody ?
return safeBody ? (
<span
key="body"
ref={opts.ref}
className={className}
dangerouslySetInnerHTML={{ __html: safeBody }}
dir="auto"
/> : <span key="body" ref={opts.ref} className={className} dir="auto">
{ emojiBodyElements || strippedBody }
</span>;
/>
) : (
<span key="body" ref={opts.ref} className={className} dir="auto">
{emojiBodyElements || strippedBody}
</span>
);
}
/**
@ -620,7 +667,7 @@ export function topicToHtml(
if (isFormattedTopic) {
safeTopic = sanitizeHtml(htmlTopic, allowExtendedHtml ? sanitizeHtmlParams : topicSanitizeHtmlParams);
if (topicHasEmoji) {
safeTopic = formatEmojis(safeTopic, true).join('');
safeTopic = formatEmojis(safeTopic, true).join("");
}
}
} catch {
@ -632,15 +679,13 @@ export function topicToHtml(
emojiBodyElements = formatEmojis(topic, false);
}
return isFormattedTopic
? <span
ref={ref}
dangerouslySetInnerHTML={{ __html: safeTopic }}
dir="auto"
/>
: <span ref={ref} dir="auto">
{ emojiBodyElements || topic }
</span>;
return isFormattedTopic ? (
<span ref={ref} dangerouslySetInnerHTML={{ __html: safeTopic }} dir="auto" />
) : (
<span ref={ref} dir="auto">
{emojiBodyElements || topic}
</span>
);
}
/**