Merge pull request #3013 from matrix-org/bwindels/editor-formatting
Message editing: preserve and re-apply formatting
This commit is contained in:
commit
3468cef654
3 changed files with 108 additions and 26 deletions
|
@ -22,7 +22,7 @@ import dis from '../../../dispatcher';
|
||||||
import EditorModel from '../../../editor/model';
|
import EditorModel from '../../../editor/model';
|
||||||
import {setCaretPosition} from '../../../editor/caret';
|
import {setCaretPosition} from '../../../editor/caret';
|
||||||
import {getCaretOffsetAndText} from '../../../editor/dom';
|
import {getCaretOffsetAndText} from '../../../editor/dom';
|
||||||
import {htmlSerialize, textSerialize, requiresHtml} from '../../../editor/serialize';
|
import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize';
|
||||||
import {parseEvent} from '../../../editor/deserialize';
|
import {parseEvent} from '../../../editor/deserialize';
|
||||||
import Autocomplete from '../rooms/Autocomplete';
|
import Autocomplete from '../rooms/Autocomplete';
|
||||||
import {PartCreator} from '../../../editor/parts';
|
import {PartCreator} from '../../../editor/parts';
|
||||||
|
@ -128,9 +128,10 @@ export default class MessageEditor extends React.Component {
|
||||||
msgtype: newContent.msgtype,
|
msgtype: newContent.msgtype,
|
||||||
body: ` * ${newContent.body}`,
|
body: ` * ${newContent.body}`,
|
||||||
};
|
};
|
||||||
if (requiresHtml(this.model)) {
|
const formattedBody = htmlSerializeIfNeeded(this.model);
|
||||||
|
if (formattedBody) {
|
||||||
newContent.format = "org.matrix.custom.html";
|
newContent.format = "org.matrix.custom.html";
|
||||||
newContent.formatted_body = htmlSerialize(this.model);
|
newContent.formatted_body = formattedBody;
|
||||||
contentBody.format = newContent.format;
|
contentBody.format = newContent.format;
|
||||||
contentBody.formatted_body = ` * ${newContent.formatted_body}`;
|
contentBody.formatted_body = ` * ${newContent.formatted_body}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,40 +18,111 @@ limitations under the License.
|
||||||
import { MATRIXTO_URL_PATTERN } from '../linkify-matrix';
|
import { MATRIXTO_URL_PATTERN } from '../linkify-matrix';
|
||||||
import { PlainPart, UserPillPart, RoomPillPart, NewlinePart } from "./parts";
|
import { PlainPart, UserPillPart, RoomPillPart, NewlinePart } from "./parts";
|
||||||
|
|
||||||
function parseHtmlMessage(html, room) {
|
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
||||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
|
||||||
// no nodes from parsing here should be inserted in the document,
|
function parseLink(a, parts, room) {
|
||||||
// as scripts in event handlers, etc would be executed then.
|
const {href} = a;
|
||||||
// we're only taking text, so that is fine
|
|
||||||
const nodes = Array.from(new DOMParser().parseFromString(html, "text/html").body.childNodes);
|
|
||||||
const parts = nodes.map(n => {
|
|
||||||
switch (n.nodeType) {
|
|
||||||
case Node.TEXT_NODE:
|
|
||||||
return new PlainPart(n.nodeValue);
|
|
||||||
case Node.ELEMENT_NODE:
|
|
||||||
switch (n.nodeName) {
|
|
||||||
case "MX-REPLY":
|
|
||||||
return null;
|
|
||||||
case "A": {
|
|
||||||
const {href} = n;
|
|
||||||
const pillMatch = REGEX_MATRIXTO.exec(href) || [];
|
const pillMatch = REGEX_MATRIXTO.exec(href) || [];
|
||||||
const resourceId = pillMatch[1]; // The room/user ID
|
const resourceId = pillMatch[1]; // The room/user ID
|
||||||
const prefix = pillMatch[2]; // The first character of prefix
|
const prefix = pillMatch[2]; // The first character of prefix
|
||||||
switch (prefix) {
|
switch (prefix) {
|
||||||
case "@": return new UserPillPart(resourceId, n.textContent, room.getMember(resourceId));
|
case "@":
|
||||||
case "#": return new RoomPillPart(resourceId);
|
parts.push(new UserPillPart(
|
||||||
default: return new PlainPart(n.textContent);
|
resourceId,
|
||||||
|
a.textContent,
|
||||||
|
room.getMember(resourceId),
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case "#":
|
||||||
|
parts.push(new RoomPillPart(resourceId));
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
if (href === a.textContent) {
|
||||||
|
parts.push(new PlainPart(a.textContent));
|
||||||
|
} else {
|
||||||
|
parts.push(new PlainPart(`[${a.textContent}](${href})`));
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHtmlMessage(html, room) {
|
||||||
|
// 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
|
||||||
|
const root = new DOMParser().parseFromString(html, "text/html").body;
|
||||||
|
let n = root.firstChild;
|
||||||
|
const parts = [];
|
||||||
|
let isFirstNode = true;
|
||||||
|
while (n && n !== root) {
|
||||||
|
switch (n.nodeType) {
|
||||||
|
case Node.TEXT_NODE:
|
||||||
|
// the plainpart doesn't accept \n and will cause
|
||||||
|
// a newlinepart to be created.
|
||||||
|
if (n.nodeValue !== "\n") {
|
||||||
|
parts.push(new PlainPart(n.nodeValue));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Node.ELEMENT_NODE:
|
||||||
|
switch (n.nodeName) {
|
||||||
|
case "DIV":
|
||||||
|
case "P": {
|
||||||
|
// block element should cause line break if not first
|
||||||
|
if (!isFirstNode) {
|
||||||
|
parts.push(new NewlinePart("\n"));
|
||||||
|
}
|
||||||
|
// decend into paragraph or div
|
||||||
|
if (n.firstChild) {
|
||||||
|
n = n.firstChild;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "A": {
|
||||||
|
parseLink(n, parts, room);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
case "BR":
|
case "BR":
|
||||||
return new NewlinePart("\n");
|
parts.push(new NewlinePart("\n"));
|
||||||
default:
|
break;
|
||||||
return new PlainPart(n.textContent);
|
case "EM":
|
||||||
|
parts.push(new PlainPart(`*${n.textContent}*`));
|
||||||
|
break;
|
||||||
|
case "STRONG":
|
||||||
|
parts.push(new PlainPart(`**${n.textContent}**`));
|
||||||
|
break;
|
||||||
|
case "PRE": {
|
||||||
|
// block element should cause line break if not first
|
||||||
|
if (!isFirstNode) {
|
||||||
|
parts.push(new NewlinePart("\n"));
|
||||||
}
|
}
|
||||||
default:
|
const preLines = `\`\`\`\n${n.textContent}\`\`\``.split("\n");
|
||||||
return null;
|
preLines.forEach((l, i) => {
|
||||||
|
parts.push(new PlainPart(l));
|
||||||
|
if (i < preLines.length - 1) {
|
||||||
|
parts.push(new NewlinePart("\n"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "CODE":
|
||||||
|
parts.push(new PlainPart(`\`${n.textContent}\``));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
parts.push(new PlainPart(n.textContent));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// go up if we can't go next
|
||||||
|
if (!n.nextSibling) {
|
||||||
|
n = n.parentElement;
|
||||||
|
}
|
||||||
|
n = n.nextSibling;
|
||||||
|
isFirstNode = false;
|
||||||
}
|
}
|
||||||
}).filter(p => !!p);
|
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,21 +15,31 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function htmlSerialize(model) {
|
import Markdown from '../Markdown';
|
||||||
|
|
||||||
|
export function mdSerialize(model) {
|
||||||
return model.parts.reduce((html, part) => {
|
return model.parts.reduce((html, part) => {
|
||||||
switch (part.type) {
|
switch (part.type) {
|
||||||
case "newline":
|
case "newline":
|
||||||
return html + "<br />";
|
return html + "\n";
|
||||||
case "plain":
|
case "plain":
|
||||||
case "pill-candidate":
|
case "pill-candidate":
|
||||||
return html + part.text;
|
return html + part.text;
|
||||||
case "room-pill":
|
case "room-pill":
|
||||||
case "user-pill":
|
case "user-pill":
|
||||||
return html + `<a href="https://matrix.to/#/${part.resourceId}">${part.text}</a>`;
|
return html + `[${part.text}](https://matrix.to/#/${part.resourceId})`;
|
||||||
}
|
}
|
||||||
}, "");
|
}, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function htmlSerializeIfNeeded(model) {
|
||||||
|
const md = mdSerialize(model);
|
||||||
|
const parser = new Markdown(md);
|
||||||
|
if (!parser.isPlainText()) {
|
||||||
|
return parser.toHTML();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function textSerialize(model) {
|
export function textSerialize(model) {
|
||||||
return model.parts.reduce((text, part) => {
|
return model.parts.reduce((text, part) => {
|
||||||
switch (part.type) {
|
switch (part.type) {
|
||||||
|
|
Loading…
Reference in a new issue