forked from mystiq/hydrogen-web
Merge pull request #408 from vector-im/formatted-messages
Formatted messages
This commit is contained in:
commit
f2e5e34a7f
16 changed files with 1024 additions and 46 deletions
|
@ -29,6 +29,7 @@
|
||||||
"@babel/preset-env": "^7.11.0",
|
"@babel/preset-env": "^7.11.0",
|
||||||
"@rollup/plugin-babel": "^5.1.0",
|
"@rollup/plugin-babel": "^5.1.0",
|
||||||
"@rollup/plugin-commonjs": "^15.0.0",
|
"@rollup/plugin-commonjs": "^15.0.0",
|
||||||
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
"@rollup/plugin-multi-entry": "^4.0.0",
|
"@rollup/plugin-multi-entry": "^4.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||||
"autoprefixer": "^10.2.6",
|
"autoprefixer": "^10.2.6",
|
||||||
|
@ -40,6 +41,7 @@
|
||||||
"finalhandler": "^1.1.1",
|
"finalhandler": "^1.1.1",
|
||||||
"impunity": "^1.0.0",
|
"impunity": "^1.0.0",
|
||||||
"mdn-polyfills": "^5.20.0",
|
"mdn-polyfills": "^5.20.0",
|
||||||
|
"node-html-parser": "^4.0.0",
|
||||||
"postcss": "^8.1.1",
|
"postcss": "^8.1.1",
|
||||||
"postcss-css-variables": "^0.17.0",
|
"postcss-css-variables": "^0.17.0",
|
||||||
"postcss-flexbugs-fixes": "^4.2.1",
|
"postcss-flexbugs-fixes": "^4.2.1",
|
||||||
|
@ -52,12 +54,13 @@
|
||||||
"xxhashjs": "^0.2.2"
|
"xxhashjs": "^0.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||||
"aes-js": "^3.1.2",
|
"aes-js": "^3.1.2",
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
"base64-arraybuffer": "^0.2.0",
|
"base64-arraybuffer": "^0.2.0",
|
||||||
"bs58": "^4.0.1",
|
"bs58": "^4.0.1",
|
||||||
|
"dompurify": "^2.3.0",
|
||||||
"es6-promise": "https://github.com/bwindels/es6-promise.git#bwindels/expose-flush",
|
"es6-promise": "https://github.com/bwindels/es6-promise.git#bwindels/expose-flush",
|
||||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
|
||||||
"text-encoding": "^0.7.0"
|
"text-encoding": "^0.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ const { fileURLToPath } = require('url');
|
||||||
const { dirname } = require('path');
|
const { dirname } = require('path');
|
||||||
// needed to translate commonjs modules to esm
|
// needed to translate commonjs modules to esm
|
||||||
const commonjs = require('@rollup/plugin-commonjs');
|
const commonjs = require('@rollup/plugin-commonjs');
|
||||||
|
const json = require('@rollup/plugin-json');
|
||||||
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
||||||
|
|
||||||
const projectDir = path.join(__dirname, "../");
|
const projectDir = path.join(__dirname, "../");
|
||||||
|
@ -53,7 +54,7 @@ async function commonjsToESM(src, dst) {
|
||||||
const bundle = await rollup({
|
const bundle = await rollup({
|
||||||
treeshake: {moduleSideEffects: false},
|
treeshake: {moduleSideEffects: false},
|
||||||
input: src,
|
input: src,
|
||||||
plugins: [commonjs(), nodeResolve({
|
plugins: [commonjs(), json(), nodeResolve({
|
||||||
browser: true,
|
browser: true,
|
||||||
preferBuiltins: false,
|
preferBuiltins: false,
|
||||||
customResolveOptions: {packageIterator}
|
customResolveOptions: {packageIterator}
|
||||||
|
@ -76,6 +77,18 @@ async function populateLib() {
|
||||||
for (const file of ["olm.js", "olm.wasm", "olm_legacy.js"]) {
|
for (const file of ["olm.js", "olm.wasm", "olm_legacy.js"]) {
|
||||||
await fs.symlink(path.join(olmSrcDir, file), path.join(olmDstDir, file));
|
await fs.symlink(path.join(olmSrcDir, file), path.join(olmDstDir, file));
|
||||||
}
|
}
|
||||||
|
// transpile node-html-parser to esm
|
||||||
|
await fs.mkdir(path.join(libDir, "node-html-parser/"));
|
||||||
|
await commonjsToESM(
|
||||||
|
require.resolve('node-html-parser/dist/index.js'),
|
||||||
|
path.join(libDir, "node-html-parser/index.js")
|
||||||
|
);
|
||||||
|
// Symlink dompurify
|
||||||
|
await fs.mkdir(path.join(libDir, "dompurify/"));
|
||||||
|
await fs.symlink(
|
||||||
|
require.resolve('dompurify/dist/purify.es.js'),
|
||||||
|
path.join(libDir, "dompurify/index.js")
|
||||||
|
);
|
||||||
// transpile another-json to esm
|
// transpile another-json to esm
|
||||||
await fs.mkdir(path.join(libDir, "another-json/"));
|
await fs.mkdir(path.join(libDir, "another-json/"));
|
||||||
await commonjsToESM(
|
await commonjsToESM(
|
||||||
|
|
25
src/domain/session/room/timeline/FORMATTED.md
Normal file
25
src/domain/session/room/timeline/FORMATTED.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Ideas for formatted messages
|
||||||
|
|
||||||
|
* Seems like a good idea to take some
|
||||||
|
inspiration from [Pandoc's AST](https://hackage.haskell.org/package/pandoc-types-1.22/docs/Text-Pandoc-Definition.html#t:Block).
|
||||||
|
Then, much like Pandoc AST, we can turn our representation into
|
||||||
|
markdown in the editor, or HTML in the view.
|
||||||
|
* This is good for both serialization and deserialization.
|
||||||
|
We can use the representation when going input -> formatted
|
||||||
|
message, which would allow other, non-web platforms
|
||||||
|
to rely on non-Markdown input types. We can also
|
||||||
|
use it going formatted message -> display, since a frontend
|
||||||
|
can then choose to use non-HTML output (GTK JS bindings?).
|
||||||
|
* As such, we represent formatting by nesting parts
|
||||||
|
(we'd have a `ItalicsPart`, `BoldPart`, etc.)
|
||||||
|
* We keep the "inline"/"block" distinction, but only
|
||||||
|
track it as a property, so that we can avoid adding
|
||||||
|
block parts to elements that cannot contain blocks
|
||||||
|
(headers, for instance, cannot contain blocks, but
|
||||||
|
lists -- themselves blocks -- can).
|
||||||
|
* When parsing, we may need some sort of "permanent" context:
|
||||||
|
if we're parsing a header, and we are inside 3 layers of other
|
||||||
|
"inline" things (italics, strikethrough, and bold, for example),
|
||||||
|
and we encounter a block, that's still not valid.
|
||||||
|
* Element seems to not care at all about the validity of what
|
||||||
|
it's parsing. Do we assume the HTML is well-formatted, then?
|
|
@ -1,4 +1,5 @@
|
||||||
import { linkify } from "./linkify/linkify.js";
|
import { linkify } from "./linkify/linkify.js";
|
||||||
|
import { getIdentifierColorNumber, avatarInitials } from "../../../avatar.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse text into parts such as newline, links and text.
|
* Parse text into parts such as newline, links and text.
|
||||||
|
@ -12,7 +13,7 @@ export function parsePlainBody(body) {
|
||||||
// create callback outside of loop
|
// create callback outside of loop
|
||||||
const linkifyCallback = (text, isLink) => {
|
const linkifyCallback = (text, isLink) => {
|
||||||
if (isLink) {
|
if (isLink) {
|
||||||
parts.push(new LinkPart(text, text));
|
parts.push(new LinkPart(text, [new TextPart(text)]));
|
||||||
} else {
|
} else {
|
||||||
parts.push(new TextPart(text));
|
parts.push(new TextPart(text));
|
||||||
}
|
}
|
||||||
|
@ -36,20 +37,99 @@ export function stringAsBody(body) {
|
||||||
return new MessageBody(body, [new TextPart(body)]);
|
return new MessageBody(body, [new TextPart(body)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
class NewLinePart {
|
export class HeaderBlock {
|
||||||
|
constructor(level, inlines) {
|
||||||
|
this.level = level;
|
||||||
|
this.inlines = inlines;
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() { return "header"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CodeBlock {
|
||||||
|
constructor(language, text) {
|
||||||
|
this.language = language;
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() { return "codeblock"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ListBlock {
|
||||||
|
constructor(startOffset, items) {
|
||||||
|
this.items = items;
|
||||||
|
this.startOffset = startOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() { return "list"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TableBlock {
|
||||||
|
constructor(head, body) {
|
||||||
|
this.head = head;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() { return "table"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RulePart {
|
||||||
|
get type( ) { return "rule"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NewLinePart {
|
||||||
get type() { return "newline"; }
|
get type() { return "newline"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
class LinkPart {
|
export class FormatPart {
|
||||||
constructor(url, text) {
|
constructor(format, children) {
|
||||||
|
this.format = format.toLowerCase();
|
||||||
|
this.children = children;
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() { return "format"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImagePart {
|
||||||
|
constructor(src, width, height, alt, title) {
|
||||||
|
this.src = src;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.alt = alt;
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() { return "image"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PillPart {
|
||||||
|
constructor(id, href, children) {
|
||||||
|
this.id = id;
|
||||||
|
this.href = href;
|
||||||
|
this.children = children;
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() { return "pill"; }
|
||||||
|
|
||||||
|
get avatarColorNumber() {
|
||||||
|
return getIdentifierColorNumber(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
get avatarInitials() {
|
||||||
|
return avatarInitials(this.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LinkPart {
|
||||||
|
constructor(url, inlines) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.text = text;
|
this.inlines = inlines;
|
||||||
}
|
}
|
||||||
|
|
||||||
get type() { return "link"; }
|
get type() { return "link"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
class TextPart {
|
export class TextPart {
|
||||||
constructor(text) {
|
constructor(text) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
}
|
}
|
||||||
|
@ -57,7 +137,7 @@ class TextPart {
|
||||||
get type() { return "text"; }
|
get type() { return "text"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageBody {
|
export class MessageBody {
|
||||||
constructor(sourceString, parts) {
|
constructor(sourceString, parts) {
|
||||||
this.sourceString = sourceString;
|
this.sourceString = sourceString;
|
||||||
this.parts = parts;
|
this.parts = parts;
|
||||||
|
|
501
src/domain/session/room/timeline/deserialize.js
Normal file
501
src/domain/session/room/timeline/deserialize.js
Normal file
|
@ -0,0 +1,501 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MessageBody, HeaderBlock, TableBlock, ListBlock, CodeBlock, PillPart, FormatPart, NewLinePart, RulePart, TextPart, LinkPart, ImagePart } from "./MessageBody.js"
|
||||||
|
import { linkify } from "./linkify/linkify.js";
|
||||||
|
|
||||||
|
/* At the time of writing (Jul 1 2021), Matrix Spec recommends
|
||||||
|
* allowing the following HTML tags:
|
||||||
|
* font, del, h1, h2, h3, h4, h5, h6, blockquote, p, a, ul, ol, sup, sub, li, b, i, u,
|
||||||
|
* strong, em, strike, code, hr, br, div, table, thead, tbody, tr, th, td, caption, pre, span, img
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nodes that don't have any properties to them other than their tag.
|
||||||
|
* While <a> has `href`, and <img> has `src`, these have... themselves.
|
||||||
|
*/
|
||||||
|
const basicInline = ["EM", "STRONG", "CODE", "DEL", "SPAN" ];
|
||||||
|
const basicBlock = ["DIV", "BLOCKQUOTE"];
|
||||||
|
const safeSchemas = ["https", "http", "ftp", "mailto", "magnet"].map(name => `${name}://`);
|
||||||
|
const baseUrl = 'https://matrix.to';
|
||||||
|
const linkPrefix = `${baseUrl}/#/`;
|
||||||
|
|
||||||
|
class Deserializer {
|
||||||
|
constructor(result, mediaRepository) {
|
||||||
|
this.result = result;
|
||||||
|
this.mediaRepository = mediaRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsePillLink(link) {
|
||||||
|
if (!link.startsWith(linkPrefix)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const contents = link.substring(linkPrefix.length);
|
||||||
|
if (contents[0] === '@') {
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseLink(node, children) {
|
||||||
|
const href = this.result.getAttributeValue(node, "href");
|
||||||
|
const lcUrl = href?.toLowerCase();
|
||||||
|
// urls should be absolute and with a safe schema, as listed in the spec
|
||||||
|
if (!lcUrl || !safeSchemas.some(schema => lcUrl.startsWith(schema))) {
|
||||||
|
return new FormatPart("span", children);
|
||||||
|
}
|
||||||
|
const pillId = this.parsePillLink(href);
|
||||||
|
if (pillId) {
|
||||||
|
return new PillPart(pillId, href, children);
|
||||||
|
}
|
||||||
|
return new LinkPart(href, children);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseList(node) {
|
||||||
|
const result = this.result;
|
||||||
|
let start = null;
|
||||||
|
if (result.getNodeElementName(node) === "OL") {
|
||||||
|
// Will return 1 for, say, '1A', which may not be intended?
|
||||||
|
start = parseInt(result.getAttributeValue(node, "start")) || 1;
|
||||||
|
}
|
||||||
|
const items = [];
|
||||||
|
for (const child of result.getChildNodes(node)) {
|
||||||
|
if (result.getNodeElementName(child) !== "LI") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const item = this.parseAnyNodes(result.getChildNodes(child));
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
return new ListBlock(start, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensureElement(node, tag) {
|
||||||
|
return node &&
|
||||||
|
this.result.isElementNode(node) &&
|
||||||
|
this.result.getNodeElementName(node) === tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseCodeBlock(node) {
|
||||||
|
const result = this.result;
|
||||||
|
let codeNode;
|
||||||
|
for (const child of result.getChildNodes(node)) {
|
||||||
|
codeNode = child;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let language = null;
|
||||||
|
if (!this._ensureElement(codeNode, "CODE")) {
|
||||||
|
return new CodeBlock(language, this.result.getNodeText(node));
|
||||||
|
}
|
||||||
|
const cl = result.getAttributeValue(codeNode, "class") || ""
|
||||||
|
for (const clname of cl.split(" ")) {
|
||||||
|
if (clname.startsWith("language-") && !clname.startsWith("language-_")) {
|
||||||
|
language = clname.substring(9) // "language-".length
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new CodeBlock(language, this.result.getNodeText(codeNode));
|
||||||
|
}
|
||||||
|
|
||||||
|
parseImage(node) {
|
||||||
|
const result = this.result;
|
||||||
|
const src = result.getAttributeValue(node, "src") || "";
|
||||||
|
const url = this.mediaRepository.mxcUrl(src);
|
||||||
|
// We just ignore non-mxc `src` attributes.
|
||||||
|
if (!url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const width = parseInt(result.getAttributeValue(node, "width")) || null;
|
||||||
|
const height = parseInt(result.getAttributeValue(node, "height")) || null;
|
||||||
|
const alt = result.getAttributeValue(node, "alt");
|
||||||
|
const title = result.getAttributeValue(node, "title");
|
||||||
|
return new ImagePart(url, width, height, alt, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTableRow(row, tag) {
|
||||||
|
const cells = [];
|
||||||
|
for (const node of this.result.getChildNodes(row)) {
|
||||||
|
if(!this._ensureElement(node, tag)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const children = this.result.getChildNodes(node);
|
||||||
|
const inlines = this.parseInlineNodes(children);
|
||||||
|
cells.push(inlines);
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTableHead(head) {
|
||||||
|
let headRow = null;
|
||||||
|
for (const node of this.result.getChildNodes(head)) {
|
||||||
|
headRow = node;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (this._ensureElement(headRow, "TR")) {
|
||||||
|
return this.parseTableRow(headRow, "TH");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTableBody(body) {
|
||||||
|
const rows = [];
|
||||||
|
for (const node of this.result.getChildNodes(body)) {
|
||||||
|
if(!this._ensureElement(node, "TR")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rows.push(this.parseTableRow(node, "TD"));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTable(node) {
|
||||||
|
// We are only assuming iterable, so convert to arrary for indexing.
|
||||||
|
const children = Array.from(this.result.getChildNodes(node));
|
||||||
|
let head, body;
|
||||||
|
if (this._ensureElement(children[0], "THEAD") && this._ensureElement(children[1], "TBODY")) {
|
||||||
|
head = this.parseTableHead(children[0]);
|
||||||
|
body = this.parseTableBody(children[1]);
|
||||||
|
} else if (this._ensureElement(children[0], "TBODY")) {
|
||||||
|
head = null;
|
||||||
|
body = this.parseTableBody(children[0]);
|
||||||
|
}
|
||||||
|
return new TableBlock(head, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Once a node is known to be an element,
|
||||||
|
* attempt to interpret it as an inline element.
|
||||||
|
*
|
||||||
|
* @returns the inline message part, or null if the element
|
||||||
|
* is not inline or not allowed.
|
||||||
|
*/
|
||||||
|
parseInlineElement(node) {
|
||||||
|
const result = this.result;
|
||||||
|
const tag = result.getNodeElementName(node);
|
||||||
|
const children = result.getChildNodes(node);
|
||||||
|
switch (tag) {
|
||||||
|
case "A": {
|
||||||
|
const inlines = this.parseInlineNodes(children);
|
||||||
|
return this.parseLink(node, inlines);
|
||||||
|
}
|
||||||
|
case "BR":
|
||||||
|
return new NewLinePart();
|
||||||
|
default: {
|
||||||
|
if (!basicInline.includes(tag)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const inlines = this.parseInlineNodes(children);
|
||||||
|
return new FormatPart(tag, inlines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Attempt to interpret a node as inline.
|
||||||
|
*
|
||||||
|
* @returns the inline message part, or null if the
|
||||||
|
* element is not inline or not allowed.
|
||||||
|
*/
|
||||||
|
parseInlineNode(node) {
|
||||||
|
if (this.result.isElementNode(node)) {
|
||||||
|
return this.parseInlineElement(node);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Once a node is known to be an element, attempt
|
||||||
|
* to interpret it as a block element.
|
||||||
|
*
|
||||||
|
* @returns the block message part, or null of the
|
||||||
|
* element is not a block or not allowed.
|
||||||
|
*/
|
||||||
|
parseBlockElement(node) {
|
||||||
|
const result = this.result;
|
||||||
|
const tag = result.getNodeElementName(node);
|
||||||
|
const children = result.getChildNodes(node);
|
||||||
|
switch (tag) {
|
||||||
|
case "H1":
|
||||||
|
case "H2":
|
||||||
|
case "H3":
|
||||||
|
case "H4":
|
||||||
|
case "H5":
|
||||||
|
case "H6": {
|
||||||
|
const inlines = this.parseInlineNodes(children);
|
||||||
|
return new HeaderBlock(parseInt(tag[1]), inlines)
|
||||||
|
}
|
||||||
|
case "UL":
|
||||||
|
case "OL":
|
||||||
|
return this.parseList(node);
|
||||||
|
case "PRE":
|
||||||
|
return this.parseCodeBlock(node);
|
||||||
|
case "HR":
|
||||||
|
return new RulePart();
|
||||||
|
case "IMG":
|
||||||
|
return this.parseImage(node);
|
||||||
|
case "P": {
|
||||||
|
const inlines = this.parseInlineNodes(children);
|
||||||
|
return new FormatPart(tag, inlines);
|
||||||
|
}
|
||||||
|
case "TABLE":
|
||||||
|
return this.parseTable(node);
|
||||||
|
default: {
|
||||||
|
if (!basicBlock.includes(tag)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const blocks = this.parseAnyNodes(children);
|
||||||
|
return new FormatPart(tag, blocks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Attempt to parse a node as a block.
|
||||||
|
*
|
||||||
|
* @return the block message part, or null if the node
|
||||||
|
* is not a block element.
|
||||||
|
*/
|
||||||
|
parseBlockNode(node) {
|
||||||
|
if (this.result.isElementNode(node)) {
|
||||||
|
return this.parseBlockElement(node);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseTextParts(node, into) {
|
||||||
|
if(!this.result.isTextNode(node)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX pretty much identical to `MessageBody`'s.
|
||||||
|
const linkifyCallback = (text, isLink) => {
|
||||||
|
if (isLink) {
|
||||||
|
into.push(new LinkPart(text, [new TextPart(text)]));
|
||||||
|
} else {
|
||||||
|
into.push(new TextPart(text));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
linkify(this.result.getNodeText(node), linkifyCallback);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseInlineNodes(nodes, into) {
|
||||||
|
for (const htmlNode of nodes) {
|
||||||
|
if (this._parseTextParts(htmlNode, into)) {
|
||||||
|
// This was a text node, and we already
|
||||||
|
// dumped its parts into our list.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const node = this.parseInlineNode(htmlNode);
|
||||||
|
if (node) {
|
||||||
|
into.push(node);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Node is either block or unrecognized. In
|
||||||
|
// both cases, just move on to its children.
|
||||||
|
this._parseInlineNodes(this.result.getChildNodes(htmlNode), into);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseInlineNodes(nodes) {
|
||||||
|
const into = [];
|
||||||
|
this._parseInlineNodes(nodes, into);
|
||||||
|
return into;
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX very similar to `_parseInlineNodes`.
|
||||||
|
_parseAnyNodes(nodes, into) {
|
||||||
|
for (const htmlNode of nodes) {
|
||||||
|
if (this._parseTextParts(htmlNode, into)) {
|
||||||
|
// This was a text node, and we already
|
||||||
|
// dumped its parts into our list.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const node = this.parseInlineNode(htmlNode) || this.parseBlockNode(htmlNode);
|
||||||
|
if (node) {
|
||||||
|
into.push(node);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Node is unrecognized. Just move on to its children.
|
||||||
|
this._parseAnyNodes(this.result.getChildNodes(htmlNode), into);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseAnyNodes(nodes) {
|
||||||
|
const into = [];
|
||||||
|
this._parseAnyNodes(nodes, into);
|
||||||
|
return into;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseHTMLBody(platform, mediaRepository, html) {
|
||||||
|
const parseResult = platform.parseHTML(html);
|
||||||
|
const deserializer = new Deserializer(parseResult, mediaRepository);
|
||||||
|
const parts = deserializer.parseAnyNodes(parseResult.rootNodes);
|
||||||
|
return new MessageBody(html, parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
import parse from '../../../../../lib/node-html-parser/index.js';
|
||||||
|
|
||||||
|
export function tests() {
|
||||||
|
class HTMLParseResult {
|
||||||
|
constructor(bodyNode) {
|
||||||
|
this._bodyNode = bodyNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
get rootNodes() {
|
||||||
|
return this._bodyNode.childNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildNodes(node) {
|
||||||
|
return node.childNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttributeNames(node) {
|
||||||
|
return node.getAttributeNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttributeValue(node, attr) {
|
||||||
|
return node.getAttribute(attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
isTextNode(node) {
|
||||||
|
return !node.tagName;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeText(node) {
|
||||||
|
return node.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
isElementNode(node) {
|
||||||
|
return !!node.tagName;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeElementName(node) {
|
||||||
|
return node.tagName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const platform = {
|
||||||
|
parseHTML: (html) => new HTMLParseResult(parse(html))
|
||||||
|
};
|
||||||
|
|
||||||
|
function test(assert, input, output) {
|
||||||
|
assert.deepEqual(parseHTMLBody(platform, null, input), new MessageBody(input, output));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Text only": assert => {
|
||||||
|
const input = "This is a sentence";
|
||||||
|
const output = [new TextPart(input)];
|
||||||
|
test(assert, input, output);
|
||||||
|
},
|
||||||
|
"Text with inline code format": assert => {
|
||||||
|
const input = "Here's <em>some</em> <code>code</code>!";
|
||||||
|
const output = [
|
||||||
|
new TextPart("Here's "),
|
||||||
|
new FormatPart("em", [new TextPart("some")]),
|
||||||
|
new TextPart(" "),
|
||||||
|
new FormatPart("code", [new TextPart("code")]),
|
||||||
|
new TextPart("!")
|
||||||
|
];
|
||||||
|
test(assert, input, output);
|
||||||
|
},
|
||||||
|
"Text with ordered list with no attributes": assert => {
|
||||||
|
const input = "<ol><li>Lorem</li><li>Ipsum</li></ol>";
|
||||||
|
const output = [
|
||||||
|
new ListBlock(1, [
|
||||||
|
[ new TextPart("Lorem") ],
|
||||||
|
[ new TextPart("Ipsum") ]
|
||||||
|
])
|
||||||
|
];
|
||||||
|
test(assert, input, output);
|
||||||
|
},
|
||||||
|
"Text with ordered list starting at 3": assert => {
|
||||||
|
const input = '<ol start="3"><li>Lorem</li><li>Ipsum</li></ol>';
|
||||||
|
const output = [
|
||||||
|
new ListBlock(3, [
|
||||||
|
[ new TextPart("Lorem") ],
|
||||||
|
[ new TextPart("Ipsum") ]
|
||||||
|
])
|
||||||
|
];
|
||||||
|
test(assert, input, output);
|
||||||
|
},
|
||||||
|
"Text with unordered list": assert => {
|
||||||
|
const input = '<ul start="3"><li>Lorem</li><li>Ipsum</li></ul>';
|
||||||
|
const output = [
|
||||||
|
new ListBlock(null, [
|
||||||
|
[ new TextPart("Lorem") ],
|
||||||
|
[ new TextPart("Ipsum") ]
|
||||||
|
])
|
||||||
|
];
|
||||||
|
test(assert, input, output);
|
||||||
|
},
|
||||||
|
"Auto-closed tags": assert => {
|
||||||
|
const input = '<p>hello<p>world</p></p>';
|
||||||
|
const output = [
|
||||||
|
new FormatPart("p", [new TextPart("hello")]),
|
||||||
|
new FormatPart("p", [new TextPart("world")])
|
||||||
|
];
|
||||||
|
test(assert, input, output);
|
||||||
|
},
|
||||||
|
"Block elements ignored inside inline elements": assert => {
|
||||||
|
const input = '<span><p><code>Hello</code></p></span>';
|
||||||
|
const output = [
|
||||||
|
new FormatPart("span", [new FormatPart("code", [new TextPart("Hello")])])
|
||||||
|
];
|
||||||
|
test(assert, input, output);
|
||||||
|
},
|
||||||
|
"Unknown tags are ignored, but their children are kept": assert => {
|
||||||
|
const input = '<span><dfn><code>Hello</code></dfn><footer><em>World</em></footer></span>';
|
||||||
|
const output = [
|
||||||
|
new FormatPart("span", [
|
||||||
|
new FormatPart("code", [new TextPart("Hello")]),
|
||||||
|
new FormatPart("em", [new TextPart("World")])
|
||||||
|
])
|
||||||
|
];
|
||||||
|
test(assert, input, output);
|
||||||
|
},
|
||||||
|
"Unknown and invalid attributes are stripped": assert => {
|
||||||
|
const input = '<em onmouseover=alert("Bad code!")>Hello</em>';
|
||||||
|
const output = [
|
||||||
|
new FormatPart("em", [new TextPart("Hello")])
|
||||||
|
];
|
||||||
|
test(assert, input, output);
|
||||||
|
},
|
||||||
|
"Text with code block but no <code> tag": assert => {
|
||||||
|
const code = 'main :: IO ()\nmain = putStrLn "Hello"'
|
||||||
|
const input = `<pre>${code}</pre>`;
|
||||||
|
const output = [
|
||||||
|
new CodeBlock(null, code)
|
||||||
|
];
|
||||||
|
test(assert, input, output);
|
||||||
|
},
|
||||||
|
"Text with code block and 'unsupported' tag": assert => {
|
||||||
|
const code = '<em>Hello, world</em>'
|
||||||
|
const input = `<pre>${code}</pre>`;
|
||||||
|
const output = [
|
||||||
|
new CodeBlock(null, code)
|
||||||
|
];
|
||||||
|
test(assert, input, output);
|
||||||
|
}
|
||||||
|
/* Doesnt work: HTML library doesn't handle <pre><code> properly.
|
||||||
|
"Text with code block": assert => {
|
||||||
|
const code = 'main :: IO ()\nmain = putStrLn "Hello"'
|
||||||
|
const input = `<pre><code>${code}</code></pre>`;
|
||||||
|
const output = [
|
||||||
|
new CodeBlock(null, code)
|
||||||
|
];
|
||||||
|
test(assert, input, output);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
}
|
|
@ -16,31 +16,43 @@ limitations under the License.
|
||||||
|
|
||||||
import {BaseMessageTile} from "./BaseMessageTile.js";
|
import {BaseMessageTile} from "./BaseMessageTile.js";
|
||||||
import {stringAsBody} from "../MessageBody.js";
|
import {stringAsBody} from "../MessageBody.js";
|
||||||
|
import {createEnum} from "../../../../../utils/enum.js";
|
||||||
|
|
||||||
|
export const BodyFormat = createEnum("Plain", "Html");
|
||||||
|
|
||||||
export class BaseTextTile extends BaseMessageTile {
|
export class BaseTextTile extends BaseMessageTile {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this._messageBody = null;
|
this._messageBody = null;
|
||||||
|
this._format = null
|
||||||
}
|
}
|
||||||
|
|
||||||
get shape() {
|
get shape() {
|
||||||
return "message";
|
return "message";
|
||||||
}
|
}
|
||||||
|
|
||||||
_parseBody(bodyString) {
|
_parseBody(body) {
|
||||||
return stringAsBody(bodyString);
|
return stringAsBody(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getBodyFormat() {
|
||||||
|
return BodyFormat.Plain;
|
||||||
}
|
}
|
||||||
|
|
||||||
get body() {
|
get body() {
|
||||||
const body = this._getBodyAsString();
|
const body = this._getBody();
|
||||||
|
const format = this._getBodyFormat();
|
||||||
// body is a string, so we can check for difference by just
|
// body is a string, so we can check for difference by just
|
||||||
// doing an equality check
|
// doing an equality check
|
||||||
if (!this._messageBody || this._messageBody.sourceString !== body) {
|
// Even if the body hasn't changed, but the format has, we need
|
||||||
|
// to re-fill our cache.
|
||||||
|
if (!this._messageBody || this._messageBody.sourceString !== body || this._format !== format) {
|
||||||
// body with markup is an array of parts,
|
// body with markup is an array of parts,
|
||||||
// so we should not recreate it for the same body string,
|
// so we should not recreate it for the same body string,
|
||||||
// or else the equality check in the binding will always fail.
|
// or else the equality check in the binding will always fail.
|
||||||
// So cache it here.
|
// So cache it here.
|
||||||
this._messageBody = this._parseBody(body);
|
this._messageBody = this._parseBody(body, format);
|
||||||
|
this._format = format;
|
||||||
}
|
}
|
||||||
return this._messageBody;
|
return this._messageBody;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,13 +33,15 @@ export class EncryptedEventTile extends BaseTextTile {
|
||||||
return "message-status"
|
return "message-status"
|
||||||
}
|
}
|
||||||
|
|
||||||
_getBodyAsString() {
|
_getBody() {
|
||||||
const decryptionError = this._entry.decryptionError;
|
const decryptionError = this._entry.decryptionError;
|
||||||
const code = decryptionError?.code;
|
const code = decryptionError?.code;
|
||||||
|
let string;
|
||||||
if (code === "MEGOLM_NO_SESSION") {
|
if (code === "MEGOLM_NO_SESSION") {
|
||||||
return this.i18n`The sender hasn't sent us the key for this message yet.`;
|
string = this.i18n`The sender hasn't sent us the key for this message yet.`;
|
||||||
} else {
|
} else {
|
||||||
return decryptionError?.message || this.i18n`Could not decrypt message because of unknown reason.`;
|
string = decryptionError?.message || this.i18n`Could not decrypt message because of unknown reason.`;
|
||||||
}
|
}
|
||||||
|
return string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,20 +14,49 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {BaseTextTile} from "./BaseTextTile.js";
|
import {BaseTextTile, BodyFormat} from "./BaseTextTile.js";
|
||||||
import {parsePlainBody} from "../MessageBody.js";
|
import {parsePlainBody} from "../MessageBody.js";
|
||||||
|
import {parseHTMLBody} from "../deserialize.js";
|
||||||
|
|
||||||
export class TextTile extends BaseTextTile {
|
export class TextTile extends BaseTextTile {
|
||||||
_getBodyAsString() {
|
_getContentString(key) {
|
||||||
const content = this._getContent();
|
const content = this._getContent();
|
||||||
let body = content?.body || "";
|
let val = content?.[key] || "";
|
||||||
if (content.msgtype === "m.emote") {
|
if (content.msgtype === "m.emote") {
|
||||||
body = `* ${this.displayName} ${body}`;
|
val = `* ${this.displayName} ${val}`;
|
||||||
}
|
}
|
||||||
return body;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
_parseBody(bodyString) {
|
_getPlainBody() {
|
||||||
return parsePlainBody(bodyString);
|
return this._getContentString("body");
|
||||||
|
}
|
||||||
|
|
||||||
|
_getFormattedBody() {
|
||||||
|
return this._getContentString("formatted_body");
|
||||||
|
}
|
||||||
|
|
||||||
|
_getBody() {
|
||||||
|
if (this._getBodyFormat() === BodyFormat.Html) {
|
||||||
|
return this._getFormattedBody();
|
||||||
|
} else {
|
||||||
|
return this._getPlainBody();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getBodyFormat() {
|
||||||
|
if (this._getContent()?.format === "org.matrix.custom.html") {
|
||||||
|
return BodyFormat.Html;
|
||||||
|
} else {
|
||||||
|
return BodyFormat.Plain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseBody(body, format) {
|
||||||
|
if (format === BodyFormat.Html) {
|
||||||
|
return parseHTMLBody(this.platform, this._mediaRepository, body);
|
||||||
|
} else {
|
||||||
|
return parsePlainBody(body);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import {BlobHandle} from "./dom/BlobHandle.js";
|
||||||
import {hasReadPixelPermission, ImageHandle, VideoHandle} from "./dom/ImageHandle.js";
|
import {hasReadPixelPermission, ImageHandle, VideoHandle} from "./dom/ImageHandle.js";
|
||||||
import {downloadInIframe} from "./dom/download.js";
|
import {downloadInIframe} from "./dom/download.js";
|
||||||
import {Disposables} from "../../utils/Disposables.js";
|
import {Disposables} from "../../utils/Disposables.js";
|
||||||
|
import {parseHTML} from "./parsehtml.js";
|
||||||
import {handleAvatarError} from "./ui/avatar.js";
|
import {handleAvatarError} from "./ui/avatar.js";
|
||||||
|
|
||||||
function addScript(src) {
|
function addScript(src) {
|
||||||
|
@ -239,6 +240,10 @@ export class Platform {
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseHTML(html) {
|
||||||
|
return parseHTML(html);
|
||||||
|
}
|
||||||
|
|
||||||
async loadImage(blob) {
|
async loadImage(blob) {
|
||||||
return ImageHandle.fromBlob(blob);
|
return ImageHandle.fromBlob(blob);
|
||||||
}
|
}
|
||||||
|
|
67
src/platform/web/parsehtml.js
Normal file
67
src/platform/web/parsehtml.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import DOMPurify from "../../../../../lib/dompurify/index.js"
|
||||||
|
|
||||||
|
class HTMLParseResult {
|
||||||
|
constructor(bodyNode) {
|
||||||
|
this._bodyNode = bodyNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
get rootNodes() {
|
||||||
|
return Array.from(this._bodyNode.childNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildNodes(node) {
|
||||||
|
return Array.from(node.childNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttributeNames(node) {
|
||||||
|
return Array.from(node.getAttributeNames());
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttributeValue(node, attr) {
|
||||||
|
return node.getAttribute(attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
isTextNode(node) {
|
||||||
|
return node.nodeType === Node.TEXT_NODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeText(node) {
|
||||||
|
return node.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
isElementNode(node) {
|
||||||
|
return node.nodeType === Node.ELEMENT_NODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeElementName(node) {
|
||||||
|
return node.tagName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizeConfig = {
|
||||||
|
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|xxx|mxc):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseHTML(html) {
|
||||||
|
// If DOMPurify uses DOMParser, can't we just get the built tree from it
|
||||||
|
// instead of re-parsing?
|
||||||
|
const sanitized = DOMPurify.sanitize(html, sanitizeConfig);
|
||||||
|
const bodyNode = new DOMParser().parseFromString(sanitized, "text/html").body;
|
||||||
|
return new HTMLParseResult(bodyNode);
|
||||||
|
}
|
|
@ -76,3 +76,11 @@ limitations under the License.
|
||||||
line-height: var(--avatar-size);
|
line-height: var(--avatar-size);
|
||||||
font-size: calc(var(--avatar-size) * 0.6);
|
font-size: calc(var(--avatar-size) * 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hydrogen .avatar.size-12 {
|
||||||
|
--avatar-size: 12px;
|
||||||
|
width: var(--avatar-size);
|
||||||
|
height: var(--avatar-size);
|
||||||
|
line-height: var(--avatar-size);
|
||||||
|
font-size: calc(var(--avatar-size) * 0.6);
|
||||||
|
}
|
||||||
|
|
|
@ -126,6 +126,8 @@ limitations under the License.
|
||||||
line-height: 2.2rem;
|
line-height: 2.2rem;
|
||||||
/* so the .media can grow horizontally and its spacer can grow vertically */
|
/* so the .media can grow horizontally and its spacer can grow vertically */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
/* Fix for pre overflow */
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hydrogen .Timeline_messageSender.usercolor1 { color: var(--usercolor1); }
|
.hydrogen .Timeline_messageSender.usercolor1 { color: var(--usercolor1); }
|
||||||
|
@ -137,10 +139,32 @@ limitations under the License.
|
||||||
.hydrogen .Timeline_messageSender.usercolor7 { color: var(--usercolor7); }
|
.hydrogen .Timeline_messageSender.usercolor7 { color: var(--usercolor7); }
|
||||||
.hydrogen .Timeline_messageSender.usercolor8 { color: var(--usercolor8); }
|
.hydrogen .Timeline_messageSender.usercolor8 { color: var(--usercolor8); }
|
||||||
|
|
||||||
|
.Timeline_messageBody h1,
|
||||||
|
.Timeline_messageBody h2,
|
||||||
|
.Timeline_messageBody h3,
|
||||||
|
.Timeline_messageBody h4,
|
||||||
|
.Timeline_messageBody h5,
|
||||||
|
.Timeline_messageBody h6 {
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.7em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Timeline_messageBody h1 { font-size: 1.6em; }
|
||||||
|
.Timeline_messageBody h2 { font-size: 1.5em; }
|
||||||
|
.Timeline_messageBody h3 { font-size: 1.4em; }
|
||||||
|
.Timeline_messageBody h4 { font-size: 1.3em; }
|
||||||
|
.Timeline_messageBody h5 { font-size: 1.2em; }
|
||||||
|
.Timeline_messageBody h6 { font-size: 1.1em; }
|
||||||
|
|
||||||
.Timeline_messageBody a {
|
.Timeline_messageBody a {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Timeline_messageBody a.link {
|
||||||
|
color: #238cf5;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.Timeline_messageBody .media {
|
.Timeline_messageBody .media {
|
||||||
display: grid;
|
display: grid;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
@ -209,6 +233,73 @@ only loads when the top comes into view*/
|
||||||
/* don't stretch height as it is a spacer, just in case it doesn't match with image height */
|
/* don't stretch height as it is a spacer, just in case it doesn't match with image height */
|
||||||
align-self: start;
|
align-self: start;
|
||||||
}
|
}
|
||||||
|
.Timeline_messageBody code, .Timeline_messageBody pre {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Timeline_messageBody code {
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: .2em .3em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Timeline_messageBody pre {
|
||||||
|
border: 1px solid rgb(229, 229, 229);
|
||||||
|
padding: 0.5em;
|
||||||
|
max-height: 30em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Timeline_messageBody pre > code {
|
||||||
|
background-color: unset;
|
||||||
|
border-radius: unset;
|
||||||
|
display: block;
|
||||||
|
font-size: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Timeline_messageBody blockquote {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
border-left: 4px solid rgb(229, 229, 229);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Timeline_messageBody table {
|
||||||
|
border: 1px solid rgb(206, 206, 206);
|
||||||
|
border-radius: 2px;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Timeline_messageBody thead th {
|
||||||
|
border-bottom: 1px solid rgb(206, 206, 206);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Timeline_messageBody td, .Timeline_messageBody th {
|
||||||
|
padding: 2px 5px 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Timeline_messageBody tbody tr:nth-child(2n) {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Timeline_messageBody .pill {
|
||||||
|
padding: 0px 5px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
border: 1px solid rgb(206, 206, 206);
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 2rem;
|
||||||
|
vertical-align: top;
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Timeline_messageBody .pill div.avatar {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.Timeline_message.unsent .Timeline_messageBody {
|
.Timeline_message.unsent .Timeline_messageBody {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
.RoomView_body ul {
|
.RoomView_body > ul {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
@ -61,3 +61,8 @@ limitations under the License.
|
||||||
.GapView > :nth-child(2) {
|
.GapView > :nth-child(2) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Timeline_messageBody img {
|
||||||
|
max-width: 400px;
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
|
@ -93,8 +93,9 @@ export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
export const TAG_NAMES = {
|
export const TAG_NAMES = {
|
||||||
[HTML_NS]: [
|
[HTML_NS]: [
|
||||||
"br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6",
|
"br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||||
"p", "strong", "em", "span", "img", "section", "main", "article", "aside",
|
"p", "strong", "em", "span", "img", "section", "main", "article", "aside", "del", "blockquote",
|
||||||
"pre", "button", "time", "input", "textarea", "label", "form", "progress", "output", "video"],
|
"table", "thead", "tbody", "tr", "th", "td",
|
||||||
|
"pre", "code", "button", "time", "input", "textarea", "label", "form", "progress", "output", "video"],
|
||||||
[SVG_NS]: ["svg", "circle"]
|
[SVG_NS]: ["svg", "circle"]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,41 +14,99 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {StaticView} from "../../../general/StaticView.js";
|
|
||||||
import {tag, text} from "../../../general/html.js";
|
import {tag, text} from "../../../general/html.js";
|
||||||
import {BaseMessageView} from "./BaseMessageView.js";
|
import {BaseMessageView} from "./BaseMessageView.js";
|
||||||
|
|
||||||
export class TextMessageView extends BaseMessageView {
|
export class TextMessageView extends BaseMessageView {
|
||||||
renderMessageBody(t, vm) {
|
renderMessageBody(t, vm) {
|
||||||
return t.p({
|
const time = t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time);
|
||||||
|
const container = t.div({
|
||||||
className: {
|
className: {
|
||||||
"Timeline_messageBody": true,
|
"Timeline_messageBody": true,
|
||||||
statusMessage: vm => vm.shape === "message-status",
|
statusMessage: vm => vm.shape === "message-status",
|
||||||
},
|
},
|
||||||
}, [
|
});
|
||||||
t.mapView(vm => vm.body, body => new BodyView(body)),
|
t.mapSideEffect(vm => vm.body, body => {
|
||||||
t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time)
|
while (container.lastChild) {
|
||||||
]);
|
container.removeChild(container.lastChild);
|
||||||
}
|
}
|
||||||
|
for (const part of body.parts) {
|
||||||
|
container.appendChild(renderPart(part));
|
||||||
|
}
|
||||||
|
container.appendChild(time);
|
||||||
|
});
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList(listBlock) {
|
||||||
|
const items = listBlock.items.map(item => tag.li(renderParts(item)));
|
||||||
|
const start = listBlock.startOffset;
|
||||||
|
if (start) {
|
||||||
|
return tag.ol({ start }, items);
|
||||||
|
} else {
|
||||||
|
return tag.ul(items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderImage(imagePart) {
|
||||||
|
const attributes = { src: imagePart.src };
|
||||||
|
if (imagePart.width) { attributes.width = imagePart.width }
|
||||||
|
if (imagePart.height) { attributes.height = imagePart.height }
|
||||||
|
if (imagePart.alt) { attributes.alt = imagePart.alt }
|
||||||
|
if (imagePart.title) { attributes.title = imagePart.title }
|
||||||
|
return tag.img(attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPill(pillPart) {
|
||||||
|
// The classes and structure are borrowed from avatar.js;
|
||||||
|
// We don't call renderStaticAvatar because that would require
|
||||||
|
// an intermediate object that has getAvatarUrl etc.
|
||||||
|
const classes = `avatar size-12 usercolor${pillPart.avatarColorNumber}`;
|
||||||
|
const avatar = tag.div({class: classes}, text(pillPart.avatarInitials));
|
||||||
|
const children = renderParts(pillPart.children);
|
||||||
|
children.unshift(avatar);
|
||||||
|
return tag.a({class: "pill", href: pillPart.href, rel: "noopener", target: "_blank"}, children);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(tablePart) {
|
||||||
|
const children = [];
|
||||||
|
if (tablePart.head) {
|
||||||
|
const headers = tablePart.head
|
||||||
|
.map(cell => tag.th(renderParts(cell)));
|
||||||
|
children.push(tag.thead(tag.tr(headers)))
|
||||||
|
}
|
||||||
|
const rows = [];
|
||||||
|
for (const row of tablePart.body) {
|
||||||
|
const data = row.map(cell => tag.td(renderParts(cell)));
|
||||||
|
rows.push(tag.tr(data));
|
||||||
|
}
|
||||||
|
children.push(tag.tbody(rows));
|
||||||
|
return tag.table(children);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map from part to function that outputs DOM for the part
|
* Map from part to function that outputs DOM for the part
|
||||||
*/
|
*/
|
||||||
const formatFunction = {
|
const formatFunction = {
|
||||||
|
header: headerBlock => tag["h" + Math.min(6,headerBlock.level)](renderParts(headerBlock.inlines)),
|
||||||
|
codeblock: codeBlock => tag.pre(tag.code(text(codeBlock.text))),
|
||||||
|
table: tableBlock => renderTable(tableBlock),
|
||||||
|
code: codePart => tag.code(text(codePart.text)),
|
||||||
text: textPart => text(textPart.text),
|
text: textPart => text(textPart.text),
|
||||||
link: linkPart => tag.a({ href: linkPart.url, target: "_blank", rel: "noopener" }, [linkPart.text]),
|
link: linkPart => tag.a({href: linkPart.url, className: "link", target: "_blank", rel: "noopener" }, renderParts(linkPart.inlines)),
|
||||||
|
pill: renderPill,
|
||||||
|
format: formatPart => tag[formatPart.format](renderParts(formatPart.children)),
|
||||||
|
list: renderList,
|
||||||
|
image: renderImage,
|
||||||
newline: () => tag.br()
|
newline: () => tag.br()
|
||||||
};
|
};
|
||||||
|
|
||||||
class BodyView extends StaticView {
|
function renderPart(part) {
|
||||||
render(t, messageBody) {
|
|
||||||
const container = t.span();
|
|
||||||
for (const part of messageBody.parts) {
|
|
||||||
const f = formatFunction[part.type];
|
const f = formatFunction[part.type];
|
||||||
const element = f(part);
|
return f(part);
|
||||||
container.appendChild(element);
|
|
||||||
}
|
|
||||||
return container;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderParts(parts) {
|
||||||
|
return Array.from(parts, renderPart);
|
||||||
}
|
}
|
||||||
|
|
82
yarn.lock
82
yarn.lock
|
@ -886,6 +886,13 @@
|
||||||
magic-string "^0.25.7"
|
magic-string "^0.25.7"
|
||||||
resolve "^1.17.0"
|
resolve "^1.17.0"
|
||||||
|
|
||||||
|
"@rollup/plugin-json@^4.1.0":
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3"
|
||||||
|
integrity sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==
|
||||||
|
dependencies:
|
||||||
|
"@rollup/pluginutils" "^3.0.8"
|
||||||
|
|
||||||
"@rollup/plugin-multi-entry@^4.0.0":
|
"@rollup/plugin-multi-entry@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-multi-entry/-/plugin-multi-entry-4.0.0.tgz#8e105f16ec1bb26639eb3302c8db5665f44b9939"
|
resolved "https://registry.yarnpkg.com/@rollup/plugin-multi-entry/-/plugin-multi-entry-4.0.0.tgz#8e105f16ec1bb26639eb3302c8db5665f44b9939"
|
||||||
|
@ -911,7 +918,7 @@
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-virtual/-/plugin-virtual-2.0.3.tgz#0afc88d75c1e1378ab290b8e9898d4edb5be0d74"
|
resolved "https://registry.yarnpkg.com/@rollup/plugin-virtual/-/plugin-virtual-2.0.3.tgz#0afc88d75c1e1378ab290b8e9898d4edb5be0d74"
|
||||||
integrity sha512-pw6ziJcyjZtntQ//bkad9qXaBx665SgEL8C8KI5wO8G5iU5MPxvdWrQyVaAvjojGm9tJoS8M9Z/EEepbqieYmw==
|
integrity sha512-pw6ziJcyjZtntQ//bkad9qXaBx665SgEL8C8KI5wO8G5iU5MPxvdWrQyVaAvjojGm9tJoS8M9Z/EEepbqieYmw==
|
||||||
|
|
||||||
"@rollup/pluginutils@^3.1.0":
|
"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0":
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
|
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
|
||||||
integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
|
integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
|
||||||
|
@ -1059,7 +1066,7 @@ base64-arraybuffer@^0.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
|
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
|
||||||
integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
|
integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
|
||||||
|
|
||||||
boolbase@~1.0.0:
|
boolbase@^1.0.0, boolbase@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||||
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
||||||
|
@ -1237,6 +1244,17 @@ cross-spawn@^7.0.2:
|
||||||
shebang-command "^2.0.0"
|
shebang-command "^2.0.0"
|
||||||
which "^2.0.1"
|
which "^2.0.1"
|
||||||
|
|
||||||
|
css-select@^4.1.3:
|
||||||
|
version "4.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067"
|
||||||
|
integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==
|
||||||
|
dependencies:
|
||||||
|
boolbase "^1.0.0"
|
||||||
|
css-what "^5.0.0"
|
||||||
|
domhandler "^4.2.0"
|
||||||
|
domutils "^2.6.0"
|
||||||
|
nth-check "^2.0.0"
|
||||||
|
|
||||||
css-select@~1.2.0:
|
css-select@~1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
|
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
|
||||||
|
@ -1252,6 +1270,11 @@ css-what@2.1:
|
||||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
|
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
|
||||||
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
|
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
|
||||||
|
|
||||||
|
css-what@^5.0.0:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad"
|
||||||
|
integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==
|
||||||
|
|
||||||
cuint@^0.2.2:
|
cuint@^0.2.2:
|
||||||
version "0.2.2"
|
version "0.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
|
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
|
||||||
|
@ -1320,6 +1343,15 @@ dom-serializer@0:
|
||||||
domelementtype "^2.0.1"
|
domelementtype "^2.0.1"
|
||||||
entities "^2.0.0"
|
entities "^2.0.0"
|
||||||
|
|
||||||
|
dom-serializer@^1.0.1:
|
||||||
|
version "1.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"
|
||||||
|
integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==
|
||||||
|
dependencies:
|
||||||
|
domelementtype "^2.0.1"
|
||||||
|
domhandler "^4.2.0"
|
||||||
|
entities "^2.0.0"
|
||||||
|
|
||||||
dom-serializer@~0.1.1:
|
dom-serializer@~0.1.1:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
|
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
|
||||||
|
@ -1338,6 +1370,11 @@ domelementtype@^2.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
|
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d"
|
||||||
integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
|
integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==
|
||||||
|
|
||||||
|
domelementtype@^2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
|
||||||
|
integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
|
||||||
|
|
||||||
domexception@^1.0.1:
|
domexception@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
|
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
|
||||||
|
@ -1352,6 +1389,18 @@ domhandler@^2.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
domelementtype "1"
|
domelementtype "1"
|
||||||
|
|
||||||
|
domhandler@^4.2.0:
|
||||||
|
version "4.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059"
|
||||||
|
integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==
|
||||||
|
dependencies:
|
||||||
|
domelementtype "^2.2.0"
|
||||||
|
|
||||||
|
dompurify@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.0.tgz#07bb39515e491588e5756b1d3e8375b5964814e2"
|
||||||
|
integrity sha512-VV5C6Kr53YVHGOBKO/F86OYX6/iLTw2yVSI721gKetxpHCK/V5TaLEf9ODjRgl1KLSWRMY6cUhAbv/c+IUnwQw==
|
||||||
|
|
||||||
domutils@1.5.1:
|
domutils@1.5.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
|
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
|
||||||
|
@ -1368,6 +1417,15 @@ domutils@^1.5.1:
|
||||||
dom-serializer "0"
|
dom-serializer "0"
|
||||||
domelementtype "1"
|
domelementtype "1"
|
||||||
|
|
||||||
|
domutils@^2.6.0:
|
||||||
|
version "2.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.7.0.tgz#8ebaf0c41ebafcf55b0b72ec31c56323712c5442"
|
||||||
|
integrity sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==
|
||||||
|
dependencies:
|
||||||
|
dom-serializer "^1.0.1"
|
||||||
|
domelementtype "^2.2.0"
|
||||||
|
domhandler "^4.2.0"
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||||
|
@ -1726,6 +1784,11 @@ has-symbols@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
|
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
|
||||||
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
|
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
|
||||||
|
|
||||||
|
he@1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||||
|
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||||
|
|
||||||
htmlparser2@^3.9.1:
|
htmlparser2@^3.9.1:
|
||||||
version "3.10.1"
|
version "3.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
|
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
|
||||||
|
@ -2036,6 +2099,14 @@ natural-compare@^1.4.0:
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||||
|
|
||||||
|
node-html-parser@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-4.0.0.tgz#95e4fec010c48425821d5659eaf39e27b0781b6d"
|
||||||
|
integrity sha512-vuOcp3u4GrfYOcqe+FpUffQKnfVBq781MbtFEcTR6fJYnYRE2qUPdDaDk7TGdq6H9r2B3TbyU6K9Rah6/C7qvg==
|
||||||
|
dependencies:
|
||||||
|
css-select "^4.1.3"
|
||||||
|
he "1.2.0"
|
||||||
|
|
||||||
node-releases@^1.1.60:
|
node-releases@^1.1.60:
|
||||||
version "1.1.60"
|
version "1.1.60"
|
||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084"
|
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084"
|
||||||
|
@ -2051,6 +2122,13 @@ normalize-range@^0.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
|
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
|
||||||
integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
|
integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
|
||||||
|
|
||||||
|
nth-check@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
|
||||||
|
integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
|
||||||
|
dependencies:
|
||||||
|
boolbase "^1.0.0"
|
||||||
|
|
||||||
nth-check@~1.0.1:
|
nth-check@~1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
|
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
|
||||||
|
|
Loading…
Reference in a new issue