diff --git a/src/app/atoms/math/Math.jsx b/src/app/atoms/math/Math.jsx
index 87f8589..ab52a47 100644
--- a/src/app/atoms/math/Math.jsx
+++ b/src/app/atoms/math/Math.jsx
@@ -1,5 +1,6 @@
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
+import './Math.scss';
import katex from 'katex';
import 'katex/dist/katex.min.css';
diff --git a/src/app/atoms/math/Math.scss b/src/app/atoms/math/Math.scss
new file mode 100644
index 0000000..306b147
--- /dev/null
+++ b/src/app/atoms/math/Math.scss
@@ -0,0 +1,3 @@
+.katex-display {
+ margin: 0 !important;
+}
diff --git a/src/app/molecules/message/Message.jsx b/src/app/molecules/message/Message.jsx
index ab05e0e..02a5562 100644
--- a/src/app/molecules/message/Message.jsx
+++ b/src/app/molecules/message/Message.jsx
@@ -8,7 +8,9 @@ import './Message.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
-import { getUsername, getUsernameOfRoomMember, parseReply } from '../../../util/matrixUtil';
+import {
+ getUsername, getUsernameOfRoomMember, parseReply, trimHTMLReply,
+} from '../../../util/matrixUtil';
import colorMXID from '../../../util/colorMXID';
import { getEventCords } from '../../../util/common';
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
@@ -248,7 +250,7 @@ const MessageBody = React.memo(({
if (!isCustomHTML) {
// If this is a plaintext message, wrap it in a
element (automatically applying
// white-space: pre-wrap) in order to preserve newlines
- content = (
{content}
);
+ content = ({content}
);
}
return (
@@ -729,23 +731,23 @@ function Message({
let { body } = content;
const username = mEvent.sender ? getUsernameOfRoomMember(mEvent.sender) : getUsername(senderId);
const avatarSrc = mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
+ let isCustomHTML = content.format === 'org.matrix.custom.html';
+ let customHTML = isCustomHTML ? content.formatted_body : null;
const edit = useCallback(() => {
setEdit(eventId);
}, []);
const reply = useCallback(() => {
- replyTo(senderId, mEvent.getId(), body);
- }, [body]);
+ replyTo(senderId, mEvent.getId(), body, customHTML);
+ }, [body, customHTML]);
if (msgType === 'm.emote') className.push('message--type-emote');
- let isCustomHTML = content.format === 'org.matrix.custom.html';
const isEdited = roomTimeline ? editedTimeline.has(eventId) : false;
const haveReactions = roomTimeline
? reactionTimeline.has(eventId) || !!mEvent.getServerAggregatedRelation('m.annotation')
: false;
const isReply = !!mEvent.replyEventId;
- let customHTML = isCustomHTML ? content.formatted_body : null;
if (isEdited) {
const editedList = editedTimeline.get(eventId);
@@ -755,6 +757,7 @@ function Message({
if (isReply) {
body = parseReply(body)?.body ?? body;
+ customHTML = trimHTMLReply(customHTML);
}
if (typeof body !== 'string') body = '';
diff --git a/src/app/molecules/message/Message.scss b/src/app/molecules/message/Message.scss
index 66d0c7e..5dda9c9 100644
--- a/src/app/molecules/message/Message.scss
+++ b/src/app/molecules/message/Message.scss
@@ -163,7 +163,7 @@
.message__body {
word-break: break-word;
- & > .text > * {
+ & > .text > .message__body-plain {
white-space: pre-wrap;
}
@@ -174,8 +174,8 @@
white-space: initial !important;
}
- & p:not(:last-child) {
- margin-bottom: var(--sp-normal);
+ & > .text > p + p {
+ margin-top: var(--sp-normal);
}
& span[data-mx-pill] {
diff --git a/src/app/organisms/room/RoomViewInput.jsx b/src/app/organisms/room/RoomViewInput.jsx
index de72e2b..c43eb60 100644
--- a/src/app/organisms/room/RoomViewInput.jsx
+++ b/src/app/organisms/room/RoomViewInput.jsx
@@ -143,9 +143,11 @@ function RoomViewInput({
textAreaRef.current.focus();
}
- function setUpReply(userId, eventId, body) {
+ function setUpReply(userId, eventId, body, formattedBody) {
setReplyTo({ userId, eventId, body });
- roomsInput.setReplyTo(roomId, { userId, eventId, body });
+ roomsInput.setReplyTo(roomId, {
+ userId, eventId, body, formattedBody,
+ });
focusInput();
}
diff --git a/src/client/action/navigation.js b/src/client/action/navigation.js
index 1292d56..4ee78a6 100644
--- a/src/client/action/navigation.js
+++ b/src/client/action/navigation.js
@@ -139,12 +139,13 @@ export function openViewSource(event) {
});
}
-export function replyTo(userId, eventId, body) {
+export function replyTo(userId, eventId, body, formattedBody) {
appDispatcher.dispatch({
type: cons.actions.navigation.CLICK_REPLY_TO,
userId,
eventId,
body,
+ formattedBody,
});
}
diff --git a/src/client/state/RoomsInput.js b/src/client/state/RoomsInput.js
index e677871..fb4b6c3 100644
--- a/src/client/state/RoomsInput.js
+++ b/src/client/state/RoomsInput.js
@@ -6,11 +6,9 @@ import { getBlobSafeMimeType } from '../../util/mimetypes';
import { sanitizeText } from '../../util/sanitize';
import cons from './cons';
import settings from './settings';
-import { htmlOutput, parser } from '../../util/markdown';
+import { markdown, plain } from '../../util/markdown';
const blurhashField = 'xyz.amorgan.blurhash';
-const MXID_REGEX = /\B@\S+:\S+\.\S+[^.,:;?!\s]/g;
-const SHORTCODE_REGEX = /\B:([\w-]+):\B/g;
function encodeBlurhash(img) {
const canvas = document.createElement('canvas');
@@ -100,91 +98,6 @@ function getVideoThumbnail(video, width, height, mimeType) {
});
}
-function getFormattedBody(markdown) {
- let content = parser(markdown);
- if (content.length === 1 && content[0].type === 'paragraph') {
- content = content[0].content;
- }
- return htmlOutput(content);
-}
-
-function getReplyFormattedBody(roomId, reply) {
- const replyToLink = `In reply to`;
- const userLink = `${reply.userId}`;
- const formattedReply = getFormattedBody(reply.body.replace(/\n/g, '\n> '));
- return `${replyToLink}${userLink}
${formattedReply}
`;
-}
-
-function bindReplyToContent(roomId, reply, content) {
- const newContent = { ...content };
- newContent.body = `> <${reply.userId}> ${reply.body.replace(/\n/g, '\n> ')}`;
- newContent.body += `\n\n${content.body}`;
- newContent.format = 'org.matrix.custom.html';
- newContent['m.relates_to'] = content['m.relates_to'] || {};
- newContent['m.relates_to']['m.in_reply_to'] = { event_id: reply.eventId };
-
- const formattedReply = getReplyFormattedBody(roomId, reply);
- newContent.formatted_body = formattedReply + (content.formatted_body || content.body);
- return newContent;
-}
-
-function findAndReplace(text, regex, filter, replace) {
- let copyText = text;
- Array.from(copyText.matchAll(regex))
- .filter(filter)
- .reverse() /* to replace backward to forward */
- .forEach((match) => {
- const matchText = match[0];
- const tag = replace(match);
-
- copyText = copyText.substr(0, match.index)
- + tag
- + copyText.substr(match.index + matchText.length);
- });
- return copyText;
-}
-
-function formatUserPill(room, text) {
- const { userIdsToDisplayNames } = room.currentState;
- return findAndReplace(
- text,
- MXID_REGEX,
- (match) => userIdsToDisplayNames[match[0]],
- (match) => (
- `@${userIdsToDisplayNames[match[0]]}`
- ),
- );
-}
-
-function formatEmoji(mx, room, roomList, text) {
- const parentIds = roomList.getAllParentSpaces(room.roomId);
- const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
- const allEmoji = getShortcodeToEmoji(mx, [room, ...parentRooms]);
-
- return findAndReplace(
- text,
- SHORTCODE_REGEX,
- (match) => allEmoji.has(match[1]),
- (match) => {
- const emoji = allEmoji.get(match[1]);
-
- let tag;
- if (emoji.mxc) {
- tag = ``;
- } else {
- tag = emoji.unicode;
- }
- return tag;
- },
- );
-}
-
class RoomsInput extends EventEmitter {
constructor(mx, roomList) {
super();
@@ -274,9 +187,76 @@ class RoomsInput extends EventEmitter {
return this.roomIdToInput.get(roomId)?.isSending || false;
}
- async sendInput(roomId, options) {
- const { msgType, autoMarkdown } = options;
+ getContent(roomId, options, message, reply, edit) {
+ const msgType = options?.msgType || 'm.text';
+ const autoMarkdown = options?.autoMarkdown ?? true;
+
const room = this.matrixClient.getRoom(roomId);
+
+ const userNames = room.currentState.userIdsToDisplayNames;
+ const parentIds = this.roomList.getAllParentSpaces(room.roomId);
+ const parentRooms = [...parentIds].map((id) => this.matrixClient.getRoom(id));
+ const emojis = getShortcodeToEmoji(this.matrixClient, [room, ...parentRooms]);
+
+ const output = settings.isMarkdown && autoMarkdown ? markdown : plain;
+ const body = output(message, { userNames, emojis });
+
+ const content = {
+ body: body.plain,
+ msgtype: msgType,
+ };
+
+ if (!body.onlyPlain || reply) {
+ content.format = 'org.matrix.custom.html';
+ content.formatted_body = body.html;
+ }
+
+ if (edit) {
+ content['m.new_content'] = { ...content };
+ content['m.relates_to'] = {
+ event_id: edit.getId(),
+ rel_type: 'm.replace',
+ };
+
+ const isReply = edit.getWireContent()['m.relates_to']?.['m.in_reply_to'];
+ if (isReply) {
+ content.format = 'org.matrix.custom.html';
+ content.formatted_body = body.html;
+ }
+
+ content.body = ` * ${content.body}`;
+ if (content.formatted_body) content.formatted_body = ` * ${content.formatted_body}`;
+
+ if (isReply) {
+ const eBody = edit.getContent().body;
+ const replyHead = eBody.substring(0, eBody.indexOf('\n\n'));
+ if (replyHead) content.body = `${replyHead}\n\n${content.body}`;
+
+ const eFBody = edit.getContent().formatted_body;
+ const fReplyHead = eFBody.substring(0, eFBody.indexOf(''));
+ if (fReplyHead) content.formatted_body = `${fReplyHead}${content.formatted_body}`;
+ }
+ }
+
+ if (reply) {
+ content['m.relates_to'] = {
+ 'm.in_reply_to': {
+ event_id: reply.eventId,
+ },
+ };
+
+ content.body = `> <${reply.userId}> ${reply.body.replace(/\n/g, '\n> ')}\n\n${content.body}`;
+
+ const replyToLink = `In reply to`;
+ const userLink = `${sanitizeText(reply.userId)}`;
+ const fallback = `${replyToLink}${userLink}
${reply.formattedBody || sanitizeText(reply.body)}
`;
+ content.formatted_body = fallback + content.formatted_body;
+ }
+
+ return content;
+ }
+
+ async sendInput(roomId, options) {
const input = this.getInput(roomId);
input.isSending = true;
this.roomIdToInput.set(roomId, input);
@@ -286,38 +266,7 @@ class RoomsInput extends EventEmitter {
}
if (this.getMessage(roomId).trim() !== '') {
- const rawMessage = input.message;
- let content = {
- body: rawMessage,
- msgtype: msgType ?? 'm.text',
- };
-
- // Apply formatting if relevant
- let formattedBody = settings.isMarkdown && autoMarkdown
- ? getFormattedBody(rawMessage)
- : sanitizeText(rawMessage);
-
- if (autoMarkdown) {
- formattedBody = formatUserPill(room, formattedBody);
- formattedBody = formatEmoji(this.matrixClient, room, this.roomList, formattedBody);
-
- content.body = findAndReplace(
- content.body,
- MXID_REGEX,
- (match) => room.currentState.userIdsToDisplayNames[match[0]],
- (match) => `@${room.currentState.userIdsToDisplayNames[match[0]]}`,
- );
- }
-
- if (formattedBody !== sanitizeText(rawMessage)) {
- // Formatting was applied, and we need to switch to custom HTML
- content.format = 'org.matrix.custom.html';
- content.formatted_body = formattedBody;
- }
-
- if (typeof input.replyTo !== 'undefined') {
- content = bindReplyToContent(roomId, input.replyTo, content);
- }
+ const content = this.getContent(roomId, options, input.message, input.replyTo);
this.matrixClient.sendMessage(roomId, content);
}
@@ -460,55 +409,13 @@ class RoomsInput extends EventEmitter {
}
async sendEditedMessage(roomId, mEvent, editedBody) {
- const room = this.matrixClient.getRoom(roomId);
- const isReply = typeof mEvent.getWireContent()['m.relates_to']?.['m.in_reply_to'] !== 'undefined';
-
- const msgtype = mEvent.getWireContent().msgtype ?? 'm.text';
-
- const content = {
- body: ` * ${editedBody}`,
- msgtype,
- 'm.new_content': {
- body: editedBody,
- msgtype,
- },
- 'm.relates_to': {
- event_id: mEvent.getId(),
- rel_type: 'm.replace',
- },
- };
-
- // Apply formatting if relevant
- let formattedBody = settings.isMarkdown
- ? getFormattedBody(editedBody)
- : sanitizeText(editedBody);
- formattedBody = formatUserPill(room, formattedBody);
- formattedBody = formatEmoji(this.matrixClient, room, this.roomList, formattedBody);
-
- content.body = findAndReplace(
- content.body,
- MXID_REGEX,
- (match) => room.currentState.userIdsToDisplayNames[match[0]],
- (match) => `@${room.currentState.userIdsToDisplayNames[match[0]]}`,
+ const content = this.getContent(
+ roomId,
+ { msgType: mEvent.getWireContent().msgtype },
+ editedBody,
+ null,
+ mEvent,
);
- if (formattedBody !== sanitizeText(editedBody)) {
- content.formatted_body = ` * ${formattedBody}`;
- content.format = 'org.matrix.custom.html';
- content['m.new_content'].formatted_body = formattedBody;
- content['m.new_content'].format = 'org.matrix.custom.html';
- }
- if (isReply) {
- const evBody = mEvent.getContent().body;
- const replyHead = evBody.slice(0, evBody.indexOf('\n\n'));
- const evFBody = mEvent.getContent().formatted_body;
- const fReplyHead = evFBody.slice(0, evFBody.indexOf(''));
-
- content.format = 'org.matrix.custom.html';
- content.formatted_body = `${fReplyHead}${(content.formatted_body || content.body)}`;
-
- content.body = `${replyHead}\n\n${content.body}`;
- }
-
this.matrixClient.sendMessage(roomId, content);
}
}
diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js
index 7b13dd1..07231cd 100644
--- a/src/client/state/navigation.js
+++ b/src/client/state/navigation.js
@@ -375,6 +375,7 @@ class Navigation extends EventEmitter {
action.userId,
action.eventId,
action.body,
+ action.formattedBody,
);
},
[cons.actions.navigation.OPEN_SEARCH]: () => {
diff --git a/src/util/markdown.js b/src/util/markdown.js
index 2e4f53d..324a12b 100644
--- a/src/util/markdown.js
+++ b/src/util/markdown.js
@@ -1,25 +1,82 @@
import SimpleMarkdown from '@khanacademy/simple-markdown';
const {
- defaultRules, parserFor, outputFor, anyScopeRegex, blockRegex, inlineRegex, htmlTag, sanitizeText,
+ defaultRules, parserFor, outputFor, anyScopeRegex, blockRegex, inlineRegex,
+ sanitizeText, sanitizeUrl,
} = SimpleMarkdown;
+function htmlTag(tagName, content, attributes, isClosed) {
+ let s = '';
+ Object.entries(attributes || {}).forEach(([k, v]) => {
+ if (v !== undefined) {
+ s += ` ${sanitizeText(k)}`;
+ if (v !== null) s += `="${sanitizeText(v)}"`;
+ }
+ });
+
+ s = `<${tagName}${s}>`;
+
+ if (isClosed === false) {
+ return s;
+ }
+ return `${s}${content}${tagName}>`;
+}
+
function mathHtml(wrap, node) {
return htmlTag(wrap, htmlTag('code', sanitizeText(node.content)), { 'data-mx-maths': node.content });
}
-const rules = {
- ...defaultRules,
+const emojiRegex = /^:([\w-]+):/;
+
+const plainRules = {
Array: {
...defaultRules.Array,
plain: (arr, output, state) => arr.map((node) => output(node, state)).join(''),
},
- displayMath: {
- order: defaultRules.list.order + 0.5,
- match: blockRegex(/^\$\$\n*([\s\S]+?)\n*\$\$/),
- parse: (capture) => ({ content: capture[1] }),
- plain: (node) => `$$\n${node.content}\n$$`,
- html: (node) => mathHtml('div', node),
+ userMention: {
+ order: defaultRules.em.order - 0.9,
+ match: inlineRegex(/^(@\S+:\S+)/),
+ parse: (capture, _, state) => ({
+ content: state.userNames[capture[1]] ? `@${state.userNames[capture[1]]}` : capture[1],
+ id: capture[1],
+ }),
+ plain: (node) => node.content,
+ html: (node) => htmlTag('a', sanitizeText(node.content), {
+ href: `https://matrix.to/#/${encodeURIComponent(node.id)}`,
+ }),
+ },
+ roomMention: {
+ order: defaultRules.em.order - 0.8,
+ match: inlineRegex(/^(#\S+:\S+)/), // TODO: Handle line beginning with roomMention (instead of heading)
+ parse: (capture) => ({ content: capture[1], id: capture[1] }),
+ plain: (node) => node.content,
+ html: (node) => htmlTag('a', sanitizeText(node.content), {
+ href: `https://matrix.to/#/${encodeURIComponent(node.id)}`,
+ }),
+ },
+ emoji: {
+ order: defaultRules.em.order - 0.1,
+ match: (source, state) => {
+ if (!state.inline) return null;
+ const capture = emojiRegex.exec(source);
+ if (!capture) return null;
+ const emoji = state.emojis.get(capture[1]);
+ if (emoji) return capture;
+ return null;
+ },
+ parse: (capture, _, state) => ({ content: capture[1], emoji: state.emojis.get(capture[1]) }),
+ plain: ({ emoji }) => (emoji.mxc
+ ? `:${emoji.shortcode}:`
+ : emoji.unicode),
+ html: ({ emoji }) => (emoji.mxc
+ ? htmlTag('img', null, {
+ 'data-mx-emoticon': null,
+ src: emoji.mxc,
+ alt: `:${emoji.shortcode}:`,
+ title: `:${emoji.shortcode}:`,
+ height: 32,
+ }, false)
+ : emoji.unicode),
},
newline: {
...defaultRules.newline,
@@ -30,10 +87,163 @@ const rules = {
plain: (node, output, state) => `${output(node.content, state)}\n\n`,
html: (node, output, state) => htmlTag('p', output(node.content, state)),
},
+ br: {
+ ...defaultRules.br,
+ match: anyScopeRegex(/^ *\n/),
+ plain: () => '\n',
+ },
+ text: {
+ ...defaultRules.text,
+ match: anyScopeRegex(/^[\s\S]+?(?=[^0-9A-Za-z\s\u00c0-\uffff]| *\n|\w+:\S|$)/),
+ plain: (node) => node.content,
+ },
+};
+
+const markdownRules = {
+ ...defaultRules,
+ ...plainRules,
+ heading: {
+ ...defaultRules.heading,
+ plain: (node, output, state) => {
+ const out = output(node.content, state);
+ if (node.level <= 2) {
+ return `${out}\n${(node.level === 1 ? '=' : '-').repeat(out.length)}\n\n`;
+ }
+ return `${'#'.repeat(node.level)} ${out}\n\n`;
+ },
+ },
+ hr: {
+ ...defaultRules.hr,
+ plain: () => '---\n\n',
+ },
+ codeBlock: {
+ ...defaultRules.codeBlock,
+ plain: (node) => `\`\`\`${node.lang || ''}\n${node.content}\n\`\`\``,
+ },
+ fence: {
+ ...defaultRules.fence,
+ match: blockRegex(/^ *(`{3,}|~{3,}) *(?:(\S+) *)?\n([\s\S]+?)\n?\1 *(?:\n *)*\n/),
+ },
+ blockQuote: {
+ ...defaultRules.blockQuote,
+ plain: (node, output, state) => `> ${output(node.content, state).trim().replace(/\n/g, '\n> ')}\n\n`,
+ },
+ list: {
+ ...defaultRules.list,
+ plain: (node, output, state) => `${node.items.map((item, i) => {
+ const prefix = node.ordered ? `${node.start + i + 1}. ` : '* ';
+ return prefix + output(item, state).replace(/\n/g, `\n${' '.repeat(prefix.length)}`);
+ }).join('\n')}\n`,
+ },
+ def: undefined,
+ table: {
+ ...defaultRules.table,
+ plain: (node, output, state) => {
+ const header = node.header.map((content) => output(content, state));
+
+ function lineWidth(i) {
+ switch (node.align[i]) {
+ case 'left':
+ case 'right':
+ return 2;
+ case 'center':
+ return 3;
+ default:
+ return 1;
+ }
+ }
+ const colWidth = header.map((s, i) => Math.max(s.length, lineWidth(i)));
+
+ const cells = node.cells.map((row) => row.map((content, i) => {
+ const s = output(content, state);
+ if (s.length > colWidth[i]) {
+ colWidth[i] = s.length;
+ }
+ return s;
+ }));
+
+ function pad(s, i) {
+ switch (node.align[i]) {
+ case 'right':
+ return s.padStart(colWidth[i]);
+ case 'center':
+ return s
+ .padStart(s.length + Math.floor((colWidth[i] - s.length) / 2))
+ .padEnd(colWidth[i]);
+ default:
+ return s.padEnd(colWidth[i]);
+ }
+ }
+
+ const line = colWidth.map((len, i) => {
+ switch (node.align[i]) {
+ case 'left':
+ return `:${'-'.repeat(len - 1)}`;
+ case 'center':
+ return `:${'-'.repeat(len - 2)}:`;
+ case 'right':
+ return `${'-'.repeat(len - 1)}:`;
+ default:
+ return '-'.repeat(len);
+ }
+ });
+
+ const table = [
+ header.map(pad),
+ line,
+ ...cells.map((row) => row.map(pad))];
+
+ return table.map((row) => `| ${row.join(' | ')} |\n`).join('');
+ },
+ },
+ displayMath: {
+ order: defaultRules.table.order + 0.1,
+ match: blockRegex(/^ *\$\$ *\n?([\s\S]+?)\n?\$\$ *(?:\n *)*\n/),
+ parse: (capture) => ({ content: capture[1] }),
+ plain: (node) => (node.content.includes('\n')
+ ? `$$\n${node.content}\n$$\n`
+ : `$$${node.content}$$\n`),
+ html: (node) => mathHtml('div', node),
+ },
+ shrug: {
+ order: defaultRules.escape.order - 0.1,
+ match: inlineRegex(/^¯\\_\(ツ\)_\/¯/),
+ parse: (capture) => ({ type: 'text', content: capture[0] }),
+ },
escape: {
...defaultRules.escape,
plain: (node, output, state) => `\\${output(node.content, state)}`,
},
+ tableSeparator: {
+ ...defaultRules.tableSeparator,
+ plain: () => ' | ',
+ },
+ link: {
+ ...defaultRules.link,
+ plain: (node, output, state) => {
+ const out = output(node.content, state);
+ const target = sanitizeUrl(node.target) || '';
+ if (out !== target || node.title) {
+ return `[${out}](${target}${node.title ? ` "${node.title}"` : ''})`;
+ }
+ return out;
+ },
+ html: (node, output, state) => htmlTag('a', output(node.content, state), {
+ href: sanitizeUrl(node.target) || '',
+ title: node.title,
+ }),
+ },
+ image: {
+ ...defaultRules.image,
+ plain: (node) => `![${node.alt}](${sanitizeUrl(node.target) || ''}${node.title ? ` "${node.title}"` : ''})`,
+ html: (node) => htmlTag('img', '', {
+ src: sanitizeUrl(node.target) || '',
+ alt: node.alt,
+ title: node.title,
+ }, false),
+ },
+ reflink: undefined,
+ refimage: undefined,
em: {
...defaultRules.em,
plain: (node, output, state) => `_${output(node.content, state)}_`,
@@ -50,40 +260,59 @@ const rules = {
...defaultRules.del,
plain: (node, output, state) => `~~${output(node.content, state)}~~`,
},
+ inlineCode: {
+ ...defaultRules.inlineCode,
+ plain: (node) => `\`${node.content}\``,
+ },
spoiler: {
- order: defaultRules.em.order - 0.5,
+ order: defaultRules.inlineCode.order + 0.1,
match: inlineRegex(/^\|\|([\s\S]+?)\|\|(?:\(([\s\S]+?)\))?/),
parse: (capture, parse, state) => ({
content: parse(capture[1], state),
reason: capture[2],
}),
- plain: (node) => `[spoiler${node.reason ? `: ${node.reason}` : ''}](mxc://somewhere)`,
- html: (node, output, state) => `${output(node.content, state)}`,
+ plain: (node, output, state) => `[spoiler${node.reason ? `: ${node.reason}` : ''}](${output(node.content, state)})`,
+ html: (node, output, state) => htmlTag(
+ 'span',
+ output(node.content, state),
+ { 'data-mx-spoiler': node.reason || null },
+ ),
},
inlineMath: {
- order: defaultRules.del.order + 0.5,
+ order: defaultRules.del.order + 0.2,
match: inlineRegex(/^\$(\S[\s\S]+?\S|\S)\$(?!\d)/),
parse: (capture) => ({ content: capture[1] }),
plain: (node) => `$${node.content}$`,
html: (node) => mathHtml('span', node),
},
- br: {
- ...defaultRules.br,
- match: anyScopeRegex(/^ *\n/),
- plain: () => '\n',
- },
- text: {
- ...defaultRules.text,
- match: anyScopeRegex(/^[\s\S]+?(?=[^0-9A-Za-z\s\u00c0-\uffff]| *\n|\w+:\S|$)/),
- plain: (node) => node.content,
- },
};
-const parser = parserFor(rules);
+function genOut(rules) {
+ const parser = parserFor(rules);
-const plainOutput = outputFor(rules, 'plain');
-const htmlOutput = outputFor(rules, 'html');
+ const plainOut = outputFor(rules, 'plain');
+ const htmlOut = outputFor(rules, 'html');
-export {
- parser, plainOutput, htmlOutput,
-};
+ return (source, state) => {
+ let content = parser(source, state);
+
+ if (content.length === 1 && content[0].type === 'paragraph') {
+ content = content[0].content;
+ }
+
+ const plain = plainOut(content, state).trim();
+ const html = htmlOut(content, state);
+
+ const plainHtml = html.replace(/
/g, '\n').replace(/<\/p>/g, '\n\n').replace(/<\/?p>/g, '');
+ const onlyPlain = sanitizeText(plain) === plainHtml;
+
+ return {
+ onlyPlain,
+ plain,
+ html,
+ };
+ };
+}
+
+export const plain = genOut(plainRules);
+export const markdown = genOut(markdownRules);
diff --git a/src/util/matrixUtil.js b/src/util/matrixUtil.js
index ef016ed..54ee31b 100644
--- a/src/util/matrixUtil.js
+++ b/src/util/matrixUtil.js
@@ -79,6 +79,16 @@ export function parseReply(rawBody) {
};
}
+export function trimHTMLReply(html) {
+ if (!html) return html;
+ const suffix = '';
+ const i = html.indexOf(suffix);
+ if (i < 0) {
+ return html;
+ }
+ return html.slice(i + suffix.length);
+}
+
export function hasDMWith(userId) {
const mx = initMatrix.matrixClient;
const directIds = [...initMatrix.roomList.directs];