From 95eaf94cd89c12c56eb2e909fb4d9a5c8092f27f Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 15 Apr 2020 00:40:38 +0100
Subject: [PATCH 1/6] Fix pills being broken by unescaped characters
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/editor/deserialize.js | 2 +-
src/editor/serialize.js | 2 +-
test/editor/deserialize-test.js | 16 ++++++++++++++++
test/editor/serialize-test.js | 12 ++++++++++++
4 files changed, 30 insertions(+), 2 deletions(-)
diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js
index 190963f357..d80a62b981 100644
--- a/src/editor/deserialize.js
+++ b/src/editor/deserialize.js
@@ -50,7 +50,7 @@ function parseLink(a, partCreator) {
if (href === a.textContent) {
return partCreator.plain(a.textContent);
} else {
- return partCreator.plain(`[${a.textContent}](${href})`);
+ return partCreator.plain(`[${a.textContent.replace(/[\\\]]/, c => "\\" + c)}](${href})`);
}
}
}
diff --git a/src/editor/serialize.js b/src/editor/serialize.js
index ba380f2809..341d92d3c8 100644
--- a/src/editor/serialize.js
+++ b/src/editor/serialize.js
@@ -30,7 +30,7 @@ export function mdSerialize(model) {
return html + part.text;
case "room-pill":
case "user-pill":
- return html + `[${part.text}](${makeGenericPermalink(part.resourceId)})`;
+ return html + `[${part.text.replace(/[\\\]]/, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;
}
}, "");
}
diff --git a/test/editor/deserialize-test.js b/test/editor/deserialize-test.js
index 1c58a6c40b..be8fe8aeab 100644
--- a/test/editor/deserialize-test.js
+++ b/test/editor/deserialize-test.js
@@ -148,6 +148,22 @@ describe('editor/deserialize', function() {
expect(parts[1]).toStrictEqual({type: "user-pill", text: "Alice", resourceId: "@alice:hs.tld"});
expect(parts[2]).toStrictEqual({type: "plain", text: "!"});
});
+ it('user pill with displayname containing backslash', function() {
+ const html = "Hi Alice\!";
+ const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
+ expect(parts.length).toBe(3);
+ expect(parts[0]).toStrictEqual({type: "plain", text: "Hi "});
+ expect(parts[1]).toStrictEqual({type: "user-pill", text: "Alice\\", resourceId: "@alice:hs.tld"});
+ expect(parts[2]).toStrictEqual({type: "plain", text: "!"});
+ });
+ it('user pill with displayname containing closing square bracket', function() {
+ const html = "Hi Alice]!";
+ const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
+ expect(parts.length).toBe(3);
+ expect(parts[0]).toStrictEqual({type: "plain", text: "Hi "});
+ expect(parts[1]).toStrictEqual({type: "user-pill", text: "Alice]", resourceId: "@alice:hs.tld"});
+ expect(parts[2]).toStrictEqual({type: "plain", text: "!"});
+ });
it('room pill', function() {
const html = "Try #room:hs.tld?";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
diff --git a/test/editor/serialize-test.js b/test/editor/serialize-test.js
index 7517e46437..d5fb800600 100644
--- a/test/editor/serialize-test.js
+++ b/test/editor/serialize-test.js
@@ -43,4 +43,16 @@ describe('editor/serialize', function() {
const html = htmlSerializeIfNeeded(model, {});
expect(html).toBe("hello world");
});
+ it('displaynames ending in a backslash work', function () {
+ const pc = createPartCreator();
+ const model = new EditorModel([pc.userPill("Displayname\\", "@user:server")]);
+ const html = htmlSerializeIfNeeded(model, {});
+ expect(html).toBe("Displayname\");
+ });
+ it('displaynames containing a closing square bracket work', function () {
+ const pc = createPartCreator();
+ const model = new EditorModel([pc.userPill("Displayname]", "@user:server")]);
+ const html = htmlSerializeIfNeeded(model, {});
+ expect(html).toBe("Displayname]");
+ });
});
From c72139fc3f5de58371d3b4e998e1b0b1d8223c3a Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 15 Apr 2020 00:49:08 +0100
Subject: [PATCH 2/6] Convert serialize and deserialize to TypeScript
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/editor/{deserialize.js => deserialize.ts} | 36 +++++++++++--------
src/editor/{serialize.js => serialize.ts} | 19 +++++-----
2 files changed, 32 insertions(+), 23 deletions(-)
rename src/editor/{deserialize.js => deserialize.ts} (87%)
rename src/editor/{serialize.js => serialize.ts} (82%)
diff --git a/src/editor/deserialize.js b/src/editor/deserialize.ts
similarity index 87%
rename from src/editor/deserialize.js
rename to src/editor/deserialize.ts
index d80a62b981..8878a8877c 100644
--- a/src/editor/deserialize.js
+++ b/src/editor/deserialize.ts
@@ -1,6 +1,6 @@
/*
Copyright 2019 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 2020 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.
@@ -15,11 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
import { walkDOMDepthFirst } from "./dom";
import { checkBlockNode } from "../HtmlUtils";
-import {getPrimaryPermalinkEntity} from "../utils/permalinks/Permalinks";
+import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
+import { PartCreator } from "./parts";
-function parseAtRoomMentions(text, partCreator) {
+function parseAtRoomMentions(text: string, partCreator: PartCreator) {
const ATROOM = "@room";
const parts = [];
text.split(ATROOM).forEach((textPart, i, arr) => {
@@ -37,7 +40,7 @@ function parseAtRoomMentions(text, partCreator) {
return parts;
}
-function parseLink(a, partCreator) {
+function parseLink(a: HTMLAnchorElement, partCreator: PartCreator) {
const {href} = a;
const resourceId = getPrimaryPermalinkEntity(href); // The room/user ID
const prefix = resourceId ? resourceId[0] : undefined; // First character of ID
@@ -56,11 +59,11 @@ function parseLink(a, partCreator) {
}
}
-function parseCodeBlock(n, partCreator) {
+function parseCodeBlock(n: HTMLElement, partCreator: PartCreator) {
const parts = [];
let language = "";
if (n.firstChild && n.firstChild.nodeName === "CODE") {
- for (const className of n.firstChild.classList) {
+ for (const className of (n.firstChild).classList) {
if (className.startsWith("language-")) {
language = className.substr("language-".length);
break;
@@ -77,12 +80,17 @@ function parseCodeBlock(n, partCreator) {
return parts;
}
-function parseHeader(el, partCreator) {
+function parseHeader(el: HTMLElement, partCreator: PartCreator) {
const depth = parseInt(el.nodeName.substr(1), 10);
return partCreator.plain("#".repeat(depth) + " ");
}
-function parseElement(n, partCreator, lastNode, state) {
+interface IState {
+ listIndex: number[];
+ listDepth?: number;
+}
+
+function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLElement | undefined, state: IState) {
switch (n.nodeName) {
case "H1":
case "H2":
@@ -92,7 +100,7 @@ function parseElement(n, partCreator, lastNode, state) {
case "H6":
return parseHeader(n, partCreator);
case "A":
- return parseLink(n, partCreator);
+ return parseLink(n, partCreator);
case "BR":
return partCreator.newline();
case "EM":
@@ -123,7 +131,7 @@ function parseElement(n, partCreator, lastNode, state) {
break;
}
case "OL":
- state.listIndex.push(n.start || 1);
+ state.listIndex.push((n).start || 1);
// fallthrough
case "UL":
state.listDepth = (state.listDepth || 0) + 1;
@@ -174,7 +182,7 @@ function prefixQuoteLines(isFirstNode, parts, partCreator) {
}
}
-function parseHtmlMessage(html, partCreator, isQuotedMessage) {
+function parseHtmlMessage(html: string, partCreator: PartCreator, isQuotedMessage: boolean) {
// no nodes from parsing here should be inserted in the document,
// as scripts in event handlers, etc would be executed then.
// we're only taking text, so that is fine
@@ -182,7 +190,7 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) {
const parts = [];
let lastNode;
let inQuote = isQuotedMessage;
- const state = {
+ const state: IState = {
listIndex: [],
};
@@ -249,7 +257,7 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) {
return parts;
}
-export function parsePlainTextMessage(body, partCreator, isQuotedMessage) {
+export function parsePlainTextMessage(body: string, partCreator: PartCreator, isQuotedMessage: boolean) {
const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n, collapses \r\n
const parts = lines.reduce((parts, line, i) => {
if (isQuotedMessage) {
@@ -265,7 +273,7 @@ export function parsePlainTextMessage(body, partCreator, isQuotedMessage) {
return parts;
}
-export function parseEvent(event, partCreator, {isQuotedMessage = false} = {}) {
+export function parseEvent(event: MatrixEvent, partCreator: PartCreator, {isQuotedMessage = false} = {}) {
const content = event.getContent();
let parts;
if (content.format === "org.matrix.custom.html") {
diff --git a/src/editor/serialize.js b/src/editor/serialize.ts
similarity index 82%
rename from src/editor/serialize.js
rename to src/editor/serialize.ts
index 341d92d3c8..9ff1cfbd80 100644
--- a/src/editor/serialize.js
+++ b/src/editor/serialize.ts
@@ -1,6 +1,6 @@
/*
Copyright 2019 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 2020 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.
@@ -17,8 +17,9 @@ limitations under the License.
import Markdown from '../Markdown';
import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
+import EditorModel from "./model";
-export function mdSerialize(model) {
+export function mdSerialize(model: EditorModel) {
return model.parts.reduce((html, part) => {
switch (part.type) {
case "newline":
@@ -35,7 +36,7 @@ export function mdSerialize(model) {
}, "");
}
-export function htmlSerializeIfNeeded(model, {forceHTML = false} = {}) {
+export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
const md = mdSerialize(model);
const parser = new Markdown(md);
if (!parser.isPlainText() || forceHTML) {
@@ -43,7 +44,7 @@ export function htmlSerializeIfNeeded(model, {forceHTML = false} = {}) {
}
}
-export function textSerialize(model) {
+export function textSerialize(model: EditorModel) {
return model.parts.reduce((text, part) => {
switch (part.type) {
case "newline":
@@ -60,11 +61,11 @@ export function textSerialize(model) {
}, "");
}
-export function containsEmote(model) {
+export function containsEmote(model: EditorModel) {
return startsWith(model, "/me ");
}
-export function startsWith(model, prefix) {
+export function startsWith(model: EditorModel, prefix: string) {
const firstPart = model.parts[0];
// part type will be "plain" while editing,
// and "command" while composing a message.
@@ -73,18 +74,18 @@ export function startsWith(model, prefix) {
firstPart.text.startsWith(prefix);
}
-export function stripEmoteCommand(model) {
+export function stripEmoteCommand(model: EditorModel) {
// trim "/me "
return stripPrefix(model, "/me ");
}
-export function stripPrefix(model, prefix) {
+export function stripPrefix(model: EditorModel, prefix: string) {
model = model.clone();
model.removeText({index: 0, offset: 0}, prefix.length);
return model;
}
-export function unescapeMessage(model) {
+export function unescapeMessage(model: EditorModel) {
const {parts} = model;
if (parts.length) {
const firstPart = parts[0];
From f1def8b0de4eadaa6672cb87d3fa3ffc80d6c43b Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 15 Apr 2020 00:50:28 +0100
Subject: [PATCH 3/6] delint
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
test/editor/deserialize-test.js | 2 +-
test/editor/serialize-test.js | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/test/editor/deserialize-test.js b/test/editor/deserialize-test.js
index be8fe8aeab..4184552559 100644
--- a/test/editor/deserialize-test.js
+++ b/test/editor/deserialize-test.js
@@ -149,7 +149,7 @@ describe('editor/deserialize', function() {
expect(parts[2]).toStrictEqual({type: "plain", text: "!"});
});
it('user pill with displayname containing backslash', function() {
- const html = "Hi Alice\!";
+ const html = "Hi Alice\\!";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
expect(parts.length).toBe(3);
expect(parts[0]).toStrictEqual({type: "plain", text: "Hi "});
diff --git a/test/editor/serialize-test.js b/test/editor/serialize-test.js
index d5fb800600..a69e3598e3 100644
--- a/test/editor/serialize-test.js
+++ b/test/editor/serialize-test.js
@@ -43,13 +43,13 @@ describe('editor/serialize', function() {
const html = htmlSerializeIfNeeded(model, {});
expect(html).toBe("hello world");
});
- it('displaynames ending in a backslash work', function () {
+ it('displaynames ending in a backslash work', function() {
const pc = createPartCreator();
const model = new EditorModel([pc.userPill("Displayname\\", "@user:server")]);
const html = htmlSerializeIfNeeded(model, {});
- expect(html).toBe("Displayname\");
+ expect(html).toBe("Displayname\\");
});
- it('displaynames containing a closing square bracket work', function () {
+ it('displaynames containing a closing square bracket work', function() {
const pc = createPartCreator();
const model = new EditorModel([pc.userPill("Displayname]", "@user:server")]);
const html = htmlSerializeIfNeeded(model, {});
From cb10640eafed873a9dce27c9f357b33c22430b2e Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 15 Apr 2020 00:53:35 +0100
Subject: [PATCH 4/6] detslint
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/editor/deserialize.ts | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts
index 8878a8877c..6680029130 100644
--- a/src/editor/deserialize.ts
+++ b/src/editor/deserialize.ts
@@ -132,10 +132,10 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
}
case "OL":
state.listIndex.push((n).start || 1);
- // fallthrough
+ /* falls through */
case "UL":
state.listDepth = (state.listDepth || 0) + 1;
- // fallthrough
+ /* falls through */
default:
// don't textify block nodes we'll descend into
if (!checkDescendInto(n)) {
@@ -244,7 +244,7 @@ function parseHtmlMessage(html: string, partCreator: PartCreator, isQuotedMessag
break;
case "OL":
state.listIndex.pop();
- // fallthrough
+ /* falls through */
case "UL":
state.listDepth -= 1;
break;
@@ -259,7 +259,7 @@ function parseHtmlMessage(html: string, partCreator: PartCreator, isQuotedMessag
export function parsePlainTextMessage(body: string, partCreator: PartCreator, isQuotedMessage: boolean) {
const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n, collapses \r\n
- const parts = lines.reduce((parts, line, i) => {
+ return lines.reduce((parts, line, i) => {
if (isQuotedMessage) {
parts.push(partCreator.plain(QUOTE_LINE_PREFIX));
}
@@ -270,7 +270,6 @@ export function parsePlainTextMessage(body: string, partCreator: PartCreator, is
}
return parts;
}, []);
- return parts;
}
export function parseEvent(event: MatrixEvent, partCreator: PartCreator, {isQuotedMessage = false} = {}) {
From 4454db30d67e9ff82848da0a6d242f1452e9f93a Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 15 Apr 2020 01:02:08 +0100
Subject: [PATCH 5/6] Escape opening square bracket too
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/editor/deserialize.ts | 2 +-
src/editor/serialize.ts | 2 +-
test/editor/deserialize-test.js | 8 ++++++++
test/editor/serialize-test.js | 6 ++++++
4 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts
index 6680029130..5322f09f11 100644
--- a/src/editor/deserialize.ts
+++ b/src/editor/deserialize.ts
@@ -53,7 +53,7 @@ function parseLink(a: HTMLAnchorElement, partCreator: PartCreator) {
if (href === a.textContent) {
return partCreator.plain(a.textContent);
} else {
- return partCreator.plain(`[${a.textContent.replace(/[\\\]]/, c => "\\" + c)}](${href})`);
+ return partCreator.plain(`[${a.textContent.replace(/[[\\\]]/, c => "\\" + c)}](${href})`);
}
}
}
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 9ff1cfbd80..d501bdd47e 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -31,7 +31,7 @@ export function mdSerialize(model: EditorModel) {
return html + part.text;
case "room-pill":
case "user-pill":
- return html + `[${part.text.replace(/[\\\]]/, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;
+ return html + `[${part.text.replace(/[[\\\]]/, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;
}
}, "");
}
diff --git a/test/editor/deserialize-test.js b/test/editor/deserialize-test.js
index 4184552559..fb97d75752 100644
--- a/test/editor/deserialize-test.js
+++ b/test/editor/deserialize-test.js
@@ -156,6 +156,14 @@ describe('editor/deserialize', function() {
expect(parts[1]).toStrictEqual({type: "user-pill", text: "Alice\\", resourceId: "@alice:hs.tld"});
expect(parts[2]).toStrictEqual({type: "plain", text: "!"});
});
+ it('user pill with displayname containing opening square bracket', function() {
+ const html = "Hi Alice[!";
+ const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
+ expect(parts.length).toBe(3);
+ expect(parts[0]).toStrictEqual({type: "plain", text: "Hi "});
+ expect(parts[1]).toStrictEqual({type: "user-pill", text: "Alice[", resourceId: "@alice:hs.tld"});
+ expect(parts[2]).toStrictEqual({type: "plain", text: "!"});
+ });
it('user pill with displayname containing closing square bracket', function() {
const html = "Hi Alice]!";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
diff --git a/test/editor/serialize-test.js b/test/editor/serialize-test.js
index a69e3598e3..a114f89de2 100644
--- a/test/editor/serialize-test.js
+++ b/test/editor/serialize-test.js
@@ -49,6 +49,12 @@ describe('editor/serialize', function() {
const html = htmlSerializeIfNeeded(model, {});
expect(html).toBe("Displayname\\");
});
+ it('displaynames containing an opening square bracket work', function() {
+ const pc = createPartCreator();
+ const model = new EditorModel([pc.userPill("Displayname[", "@user:server")]);
+ const html = htmlSerializeIfNeeded(model, {});
+ expect(html).toBe("Displayname[");
+ });
it('displaynames containing a closing square bracket work', function() {
const pc = createPartCreator();
const model = new EditorModel([pc.userPill("Displayname]", "@user:server")]);
From 9c1939b75679980f14f3ee550af00e65346c1fd0 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 15 Apr 2020 02:31:30 +0100
Subject: [PATCH 6/6] match all, not just first instance of tokens to escape
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
src/editor/deserialize.ts | 2 +-
src/editor/serialize.ts | 2 +-
test/editor/deserialize-test.js | 4 ++--
test/editor/serialize-test.js | 4 ++--
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts
index 5322f09f11..48d1d98ae4 100644
--- a/src/editor/deserialize.ts
+++ b/src/editor/deserialize.ts
@@ -53,7 +53,7 @@ function parseLink(a: HTMLAnchorElement, partCreator: PartCreator) {
if (href === a.textContent) {
return partCreator.plain(a.textContent);
} else {
- return partCreator.plain(`[${a.textContent.replace(/[[\\\]]/, c => "\\" + c)}](${href})`);
+ return partCreator.plain(`[${a.textContent.replace(/[[\\\]]/g, c => "\\" + c)}](${href})`);
}
}
}
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index d501bdd47e..4d0b8cd03a 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -31,7 +31,7 @@ export function mdSerialize(model: EditorModel) {
return html + part.text;
case "room-pill":
case "user-pill":
- return html + `[${part.text.replace(/[[\\\]]/, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;
+ return html + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;
}
}, "");
}
diff --git a/test/editor/deserialize-test.js b/test/editor/deserialize-test.js
index fb97d75752..2bd5d7e4c6 100644
--- a/test/editor/deserialize-test.js
+++ b/test/editor/deserialize-test.js
@@ -157,11 +157,11 @@ describe('editor/deserialize', function() {
expect(parts[2]).toStrictEqual({type: "plain", text: "!"});
});
it('user pill with displayname containing opening square bracket', function() {
- const html = "Hi Alice[!";
+ const html = "Hi Alice[[!";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
expect(parts.length).toBe(3);
expect(parts[0]).toStrictEqual({type: "plain", text: "Hi "});
- expect(parts[1]).toStrictEqual({type: "user-pill", text: "Alice[", resourceId: "@alice:hs.tld"});
+ expect(parts[1]).toStrictEqual({type: "user-pill", text: "Alice[[", resourceId: "@alice:hs.tld"});
expect(parts[2]).toStrictEqual({type: "plain", text: "!"});
});
it('user pill with displayname containing closing square bracket', function() {
diff --git a/test/editor/serialize-test.js b/test/editor/serialize-test.js
index a114f89de2..bd26ae91bb 100644
--- a/test/editor/serialize-test.js
+++ b/test/editor/serialize-test.js
@@ -51,9 +51,9 @@ describe('editor/serialize', function() {
});
it('displaynames containing an opening square bracket work', function() {
const pc = createPartCreator();
- const model = new EditorModel([pc.userPill("Displayname[", "@user:server")]);
+ const model = new EditorModel([pc.userPill("Displayname[[", "@user:server")]);
const html = htmlSerializeIfNeeded(model, {});
- expect(html).toBe("Displayname[");
+ expect(html).toBe("Displayname[[");
});
it('displaynames containing a closing square bracket work', function() {
const pc = createPartCreator();