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",
|
||||
"@rollup/plugin-babel": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^15.0.0",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-multi-entry": "^4.0.0",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"autoprefixer": "^10.2.6",
|
||||
|
@ -40,6 +41,7 @@
|
|||
"finalhandler": "^1.1.1",
|
||||
"impunity": "^1.0.0",
|
||||
"mdn-polyfills": "^5.20.0",
|
||||
"node-html-parser": "^4.0.0",
|
||||
"postcss": "^8.1.1",
|
||||
"postcss-css-variables": "^0.17.0",
|
||||
"postcss-flexbugs-fixes": "^4.2.1",
|
||||
|
@ -52,12 +54,13 @@
|
|||
"xxhashjs": "^0.2.2"
|
||||
},
|
||||
"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",
|
||||
"another-json": "^0.2.0",
|
||||
"base64-arraybuffer": "^0.2.0",
|
||||
"bs58": "^4.0.1",
|
||||
"dompurify": "^2.3.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ const { fileURLToPath } = require('url');
|
|||
const { dirname } = require('path');
|
||||
// needed to translate commonjs modules to esm
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const json = require('@rollup/plugin-json');
|
||||
const { nodeResolve } = require('@rollup/plugin-node-resolve');
|
||||
|
||||
const projectDir = path.join(__dirname, "../");
|
||||
|
@ -53,7 +54,7 @@ async function commonjsToESM(src, dst) {
|
|||
const bundle = await rollup({
|
||||
treeshake: {moduleSideEffects: false},
|
||||
input: src,
|
||||
plugins: [commonjs(), nodeResolve({
|
||||
plugins: [commonjs(), json(), nodeResolve({
|
||||
browser: true,
|
||||
preferBuiltins: false,
|
||||
customResolveOptions: {packageIterator}
|
||||
|
@ -76,6 +77,18 @@ async function populateLib() {
|
|||
for (const file of ["olm.js", "olm.wasm", "olm_legacy.js"]) {
|
||||
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
|
||||
await fs.mkdir(path.join(libDir, "another-json/"));
|
||||
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 { getIdentifierColorNumber, avatarInitials } from "../../../avatar.js";
|
||||
|
||||
/**
|
||||
* Parse text into parts such as newline, links and text.
|
||||
|
@ -12,7 +13,7 @@ export function parsePlainBody(body) {
|
|||
// create callback outside of loop
|
||||
const linkifyCallback = (text, isLink) => {
|
||||
if (isLink) {
|
||||
parts.push(new LinkPart(text, text));
|
||||
parts.push(new LinkPart(text, [new TextPart(text)]));
|
||||
} else {
|
||||
parts.push(new TextPart(text));
|
||||
}
|
||||
|
@ -36,20 +37,99 @@ export function stringAsBody(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"; }
|
||||
}
|
||||
|
||||
class LinkPart {
|
||||
constructor(url, text) {
|
||||
export class FormatPart {
|
||||
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.text = text;
|
||||
this.inlines = inlines;
|
||||
}
|
||||
|
||||
get type() { return "link"; }
|
||||
}
|
||||
|
||||
class TextPart {
|
||||
export class TextPart {
|
||||
constructor(text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
@ -57,7 +137,7 @@ class TextPart {
|
|||
get type() { return "text"; }
|
||||
}
|
||||
|
||||
class MessageBody {
|
||||
export class MessageBody {
|
||||
constructor(sourceString, parts) {
|
||||
this.sourceString = sourceString;
|
||||
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 {stringAsBody} from "../MessageBody.js";
|
||||
import {createEnum} from "../../../../../utils/enum.js";
|
||||
|
||||
export const BodyFormat = createEnum("Plain", "Html");
|
||||
|
||||
export class BaseTextTile extends BaseMessageTile {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this._messageBody = null;
|
||||
this._format = null
|
||||
}
|
||||
|
||||
get shape() {
|
||||
return "message";
|
||||
}
|
||||
|
||||
_parseBody(bodyString) {
|
||||
return stringAsBody(bodyString);
|
||||
_parseBody(body) {
|
||||
return stringAsBody(body);
|
||||
}
|
||||
|
||||
_getBodyFormat() {
|
||||
return BodyFormat.Plain;
|
||||
}
|
||||
|
||||
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
|
||||
// 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,
|
||||
// so we should not recreate it for the same body string,
|
||||
// or else the equality check in the binding will always fail.
|
||||
// So cache it here.
|
||||
this._messageBody = this._parseBody(body);
|
||||
this._messageBody = this._parseBody(body, format);
|
||||
this._format = format;
|
||||
}
|
||||
return this._messageBody;
|
||||
}
|
||||
|
|
|
@ -33,13 +33,15 @@ export class EncryptedEventTile extends BaseTextTile {
|
|||
return "message-status"
|
||||
}
|
||||
|
||||
_getBodyAsString() {
|
||||
_getBody() {
|
||||
const decryptionError = this._entry.decryptionError;
|
||||
const code = decryptionError?.code;
|
||||
let string;
|
||||
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 {
|
||||
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.
|
||||
*/
|
||||
|
||||
import {BaseTextTile} from "./BaseTextTile.js";
|
||||
import {BaseTextTile, BodyFormat} from "./BaseTextTile.js";
|
||||
import {parsePlainBody} from "../MessageBody.js";
|
||||
import {parseHTMLBody} from "../deserialize.js";
|
||||
|
||||
export class TextTile extends BaseTextTile {
|
||||
_getBodyAsString() {
|
||||
_getContentString(key) {
|
||||
const content = this._getContent();
|
||||
let body = content?.body || "";
|
||||
let val = content?.[key] || "";
|
||||
if (content.msgtype === "m.emote") {
|
||||
body = `* ${this.displayName} ${body}`;
|
||||
val = `* ${this.displayName} ${val}`;
|
||||
}
|
||||
return body;
|
||||
return val;
|
||||
}
|
||||
|
||||
_parseBody(bodyString) {
|
||||
return parsePlainBody(bodyString);
|
||||
_getPlainBody() {
|
||||
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 {downloadInIframe} from "./dom/download.js";
|
||||
import {Disposables} from "../../utils/Disposables.js";
|
||||
import {parseHTML} from "./parsehtml.js";
|
||||
import {handleAvatarError} from "./ui/avatar.js";
|
||||
|
||||
function addScript(src) {
|
||||
|
@ -239,6 +240,10 @@ export class Platform {
|
|||
return promise;
|
||||
}
|
||||
|
||||
parseHTML(html) {
|
||||
return parseHTML(html);
|
||||
}
|
||||
|
||||
async loadImage(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);
|
||||
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;
|
||||
/* so the .media can grow horizontally and its spacer can grow vertically */
|
||||
width: 100%;
|
||||
/* Fix for pre overflow */
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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.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 {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.Timeline_messageBody a.link {
|
||||
color: #238cf5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.Timeline_messageBody .media {
|
||||
display: grid;
|
||||
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 */
|
||||
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 {
|
||||
color: #ccc;
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
|
||||
.RoomView_body ul {
|
||||
.RoomView_body > ul {
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
list-style: none;
|
||||
|
@ -61,3 +61,8 @@ limitations under the License.
|
|||
.GapView > :nth-child(2) {
|
||||
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 = {
|
||||
[HTML_NS]: [
|
||||
"br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"p", "strong", "em", "span", "img", "section", "main", "article", "aside",
|
||||
"pre", "button", "time", "input", "textarea", "label", "form", "progress", "output", "video"],
|
||||
"p", "strong", "em", "span", "img", "section", "main", "article", "aside", "del", "blockquote",
|
||||
"table", "thead", "tbody", "tr", "th", "td",
|
||||
"pre", "code", "button", "time", "input", "textarea", "label", "form", "progress", "output", "video"],
|
||||
[SVG_NS]: ["svg", "circle"]
|
||||
};
|
||||
|
||||
|
|
|
@ -14,41 +14,99 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {StaticView} from "../../../general/StaticView.js";
|
||||
import {tag, text} from "../../../general/html.js";
|
||||
import {BaseMessageView} from "./BaseMessageView.js";
|
||||
|
||||
export class TextMessageView extends BaseMessageView {
|
||||
renderMessageBody(t, vm) {
|
||||
return t.p({
|
||||
const time = t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time);
|
||||
const container = t.div({
|
||||
className: {
|
||||
"Timeline_messageBody": true,
|
||||
statusMessage: vm => vm.shape === "message-status",
|
||||
},
|
||||
}, [
|
||||
t.mapView(vm => vm.body, body => new BodyView(body)),
|
||||
t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time)
|
||||
]);
|
||||
});
|
||||
t.mapSideEffect(vm => vm.body, body => {
|
||||
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
|
||||
*/
|
||||
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),
|
||||
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()
|
||||
};
|
||||
|
||||
class BodyView extends StaticView {
|
||||
render(t, messageBody) {
|
||||
const container = t.span();
|
||||
for (const part of messageBody.parts) {
|
||||
const f = formatFunction[part.type];
|
||||
const element = f(part);
|
||||
container.appendChild(element);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
function renderPart(part) {
|
||||
const f = formatFunction[part.type];
|
||||
return f(part);
|
||||
}
|
||||
|
||||
function renderParts(parts) {
|
||||
return Array.from(parts, renderPart);
|
||||
}
|
||||
|
|
82
yarn.lock
82
yarn.lock
|
@ -886,6 +886,13 @@
|
|||
magic-string "^0.25.7"
|
||||
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":
|
||||
version "4.0.0"
|
||||
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"
|
||||
integrity sha512-pw6ziJcyjZtntQ//bkad9qXaBx665SgEL8C8KI5wO8G5iU5MPxvdWrQyVaAvjojGm9tJoS8M9Z/EEepbqieYmw==
|
||||
|
||||
"@rollup/pluginutils@^3.1.0":
|
||||
"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
|
||||
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"
|
||||
integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
|
||||
|
||||
boolbase@~1.0.0:
|
||||
boolbase@^1.0.0, boolbase@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
||||
|
@ -1237,6 +1244,17 @@ cross-spawn@^7.0.2:
|
|||
shebang-command "^2.0.0"
|
||||
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:
|
||||
version "1.2.0"
|
||||
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"
|
||||
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:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
|
||||
|
@ -1320,6 +1343,15 @@ dom-serializer@0:
|
|||
domelementtype "^2.0.1"
|
||||
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:
|
||||
version "0.1.1"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
|
||||
|
@ -1352,6 +1389,18 @@ domhandler@^2.3.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
|
||||
|
@ -1368,6 +1417,15 @@ domutils@^1.5.1:
|
|||
dom-serializer "0"
|
||||
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:
|
||||
version "1.1.1"
|
||||
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"
|
||||
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:
|
||||
version "3.10.1"
|
||||
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"
|
||||
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:
|
||||
version "1.1.60"
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
|
||||
|
|
Reference in a new issue