<Menu style={{ padding: config.space.S100 }}>
<Box gap="100">
<TooltipProvider
- tooltip={<BtnTooltip text="Heading 1" shortCode={`${modKey} + Shift + 1`} />}
+ tooltip={<BtnTooltip text="Heading 1" shortCode={`${modKey} + 1`} />}
delay={500}
>
{(triggerRef) => (
)}
</TooltipProvider>
<TooltipProvider
- tooltip={<BtnTooltip text="Heading 2" shortCode={`${modKey} + Shift + 2`} />}
+ tooltip={<BtnTooltip text="Heading 2" shortCode={`${modKey} + 2`} />}
delay={500}
>
{(triggerRef) => (
)}
</TooltipProvider>
<TooltipProvider
- tooltip={<BtnTooltip text="Heading 3" shortCode={`${modKey} + Shift + 3`} />}
+ tooltip={<BtnTooltip text="Heading 3" shortCode={`${modKey} + 3`} />}
delay={500}
>
{(triggerRef) => (
<MarkButton
format={MarkType.StrikeThrough}
icon={Icons.Strike}
- tooltip={
- <BtnTooltip
- text="Strike Through"
- shortCode={`${modKey} + ${KeySymbol.Shift} + U`}
- />
- }
+ tooltip={<BtnTooltip text="Strike Through" shortCode={`${modKey} + S`} />}
/>
<MarkButton
format={MarkType.Code}
<BlockButton
format={BlockType.OrderedList}
icon={Icons.OrderList}
- tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + Shift + 7`} />}
+ tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`} />}
/>
<BlockButton
format={BlockType.UnorderedList}
icon={Icons.UnorderList}
- tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + Shift + 8`} />}
+ tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + 8`} />}
/>
<HeadingBlockButton />
</Box>
<Box className={css.MarkdownBtnBox} shrink="No" grow="Yes" justifyContent="End">
<TooltipProvider
align="End"
- tooltip={<BtnTooltip text="Inline Markdown" />}
+ tooltip={<BtnTooltip text="Toggle Markdown" />}
delay={500}
>
{(triggerRef) => (
HeadingElement,
HeadingLevel,
InlineElement,
- ListItemElement,
MentionElement,
OrderedListElement,
ParagraphElement,
- QuoteLineElement,
UnorderedListElement,
} from './slate';
import { parseMatrixToUrl } from '../../utils/matrix';
return [];
};
-const parseBlockquoteNode = (node: Element): BlockQuoteElement => {
- const children: QuoteLineElement[] = [];
+const parseBlockquoteNode = (node: Element): BlockQuoteElement[] | ParagraphElement[] => {
+ const quoteLines: Array<InlineElement[]> = [];
let lineHolder: InlineElement[] = [];
const appendLine = () => {
if (lineHolder.length === 0) return;
- children.push({
- type: BlockType.QuoteLine,
- children: lineHolder,
- });
+ quoteLines.push(lineHolder);
lineHolder = [];
};
if (child.name === 'p') {
appendLine();
- children.push({
- type: BlockType.QuoteLine,
- children: child.children.flatMap((c) => parseInlineNodes(c)),
- });
+ quoteLines.push(child.children.flatMap((c) => parseInlineNodes(c)));
return;
}
});
appendLine();
- return {
- type: BlockType.BlockQuote,
- children,
- };
+ if (node.attribs['data-md'] !== undefined) {
+ return quoteLines.map((lineChildren) => ({
+ type: BlockType.Paragraph,
+ children: [{ text: `${node.attribs['data-md']} ` }, ...lineChildren],
+ }));
+ }
+
+ return [
+ {
+ type: BlockType.BlockQuote,
+ children: quoteLines.map((lineChildren) => ({
+ type: BlockType.QuoteLine,
+ children: lineChildren,
+ })),
+ },
+ ];
};
-const parseCodeBlockNode = (node: Element): CodeBlockElement => {
- const children: CodeLineElement[] = [];
+const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElement[] => {
+ const codeLines = parseNodeText(node).trim().split('\n');
- const code = parseNodeText(node).trim();
- code.split('\n').forEach((lineTxt) =>
- children.push({
- type: BlockType.CodeLine,
+ if (node.attribs['data-md'] !== undefined) {
+ const pLines = codeLines.map<ParagraphElement>((lineText) => ({
+ type: BlockType.Paragraph,
children: [
{
- text: lineTxt,
+ text: lineText,
},
],
- })
- );
+ }));
+ const childCode = node.children[0];
+ const className =
+ isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs.class ?? '' : '';
+ const prefix = { text: `${node.attribs['data-md']}${className.replace('language-', '')}` };
+ const suffix = { text: node.attribs['data-md'] };
+ return [
+ { type: BlockType.Paragraph, children: [prefix] },
+ ...pLines,
+ { type: BlockType.Paragraph, children: [suffix] },
+ ];
+ }
- return {
- type: BlockType.CodeBlock,
- children,
- };
+ return [
+ {
+ type: BlockType.CodeBlock,
+ children: codeLines.map<CodeLineElement>((lineTxt) => ({
+ type: BlockType.CodeLine,
+ children: [
+ {
+ text: lineTxt,
+ },
+ ],
+ })),
+ },
+ ];
};
-const parseListNode = (node: Element): OrderedListElement | UnorderedListElement => {
- const children: ListItemElement[] = [];
+const parseListNode = (
+ node: Element
+): OrderedListElement[] | UnorderedListElement[] | ParagraphElement[] => {
+ const listLines: Array<InlineElement[]> = [];
let lineHolder: InlineElement[] = [];
const appendLine = () => {
if (lineHolder.length === 0) return;
- children.push({
- type: BlockType.ListItem,
- children: lineHolder,
- });
+ listLines.push(lineHolder);
lineHolder = [];
};
if (child.name === 'li') {
appendLine();
- children.push({
- type: BlockType.ListItem,
- children: child.children.flatMap((c) => parseInlineNodes(c)),
- });
+ listLines.push(child.children.flatMap((c) => parseInlineNodes(c)));
return;
}
});
appendLine();
- return {
- type: node.name === 'ol' ? BlockType.OrderedList : BlockType.UnorderedList,
- children,
- };
+ if (node.attribs['data-md'] !== undefined) {
+ const prefix = node.attribs['data-md'] || '-';
+ const [starOrHyphen] = prefix.match(/^\*|-$/) ?? [];
+ return listLines.map((lineChildren) => ({
+ type: BlockType.Paragraph,
+ children: [
+ { text: `${starOrHyphen ? `${starOrHyphen} ` : `${prefix}. `} ` },
+ ...lineChildren,
+ ],
+ }));
+ }
+
+ if (node.name === 'ol') {
+ return [
+ {
+ type: BlockType.OrderedList,
+ children: listLines.map((lineChildren) => ({
+ type: BlockType.ListItem,
+ children: lineChildren,
+ })),
+ },
+ ];
+ }
+
+ return [
+ {
+ type: BlockType.UnorderedList,
+ children: listLines.map((lineChildren) => ({
+ type: BlockType.ListItem,
+ children: lineChildren,
+ })),
+ },
+ ];
};
-const parseHeadingNode = (node: Element): HeadingElement => {
+const parseHeadingNode = (node: Element): HeadingElement | ParagraphElement => {
const children = node.children.flatMap((child) => parseInlineNodes(child));
const headingMatch = node.name.match(/^h([123456])$/);
const [, g1AsLevel] = headingMatch ?? ['h3', '3'];
const level = parseInt(g1AsLevel, 10);
+
+ if (node.attribs['data-md'] !== undefined) {
+ return {
+ type: BlockType.Paragraph,
+ children: [{ text: `${node.attribs['data-md']} ` }, ...children],
+ };
+ }
+
return {
type: BlockType.Heading,
level: (level <= 3 ? level : 3) as HeadingLevel,
if (node.name === 'blockquote') {
appendLine();
- children.push(parseBlockquoteNode(node));
+ children.push(...parseBlockquoteNode(node));
return;
}
if (node.name === 'pre') {
appendLine();
- children.push(parseCodeBlockNode(node));
+ children.push(...parseCodeBlockNode(node));
return;
}
if (node.name === 'ol' || node.name === 'ul') {
appendLine();
- children.push(parseListNode(node));
+ children.push(...parseListNode(node));
return;
}
'mod+b': MarkType.Bold,
'mod+i': MarkType.Italic,
'mod+u': MarkType.Underline,
- 'mod+shift+u': MarkType.StrikeThrough,
+ 'mod+s': MarkType.StrikeThrough,
'mod+[': MarkType.Code,
'mod+h': MarkType.Spoiler,
};
const INLINE_KEYS = Object.keys(INLINE_HOTKEYS);
export const BLOCK_HOTKEYS: Record<string, BlockType> = {
- 'mod+shift+7': BlockType.OrderedList,
- 'mod+shift+8': BlockType.UnorderedList,
+ 'mod+7': BlockType.OrderedList,
+ 'mod+8': BlockType.UnorderedList,
"mod+'": BlockType.BlockQuote,
'mod+;': BlockType.CodeBlock,
};
const BLOCK_KEYS = Object.keys(BLOCK_HOTKEYS);
-const isHeading1 = isKeyHotkey('mod+shift+1');
-const isHeading2 = isKeyHotkey('mod+shift+2');
-const isHeading3 = isKeyHotkey('mod+shift+3');
+const isHeading1 = isKeyHotkey('mod+1');
+const isHeading2 = isKeyHotkey('mod+2');
+const isHeading3 = isKeyHotkey('mod+3');
/**
* @return boolean true if shortcut is toggled.
import { sanitizeText } from '../../utils/sanitize';
import { BlockType } from './types';
import { CustomElement } from './slate';
-import { parseInlineMD } from '../../utils/markdown';
+import { parseBlockMD, parseInlineMD, replaceMatch } from '../../utils/markdown';
export type OutputOptions = {
allowTextFormatting?: boolean;
- allowMarkdown?: boolean;
+ allowInlineMarkdown?: boolean;
+ allowBlockMarkdown?: boolean;
};
const textToCustomHtml = (node: Text, opts: OutputOptions): string => {
if (node.spoiler) string = `<span data-mx-spoiler>${string}</span>`;
}
- if (opts.allowMarkdown && string === sanitizeText(node.text)) {
+ if (opts.allowInlineMarkdown && string === sanitizeText(node.text)) {
string = parseInlineMD(string);
}
}
};
+const HTML_TAG_REG = /<([a-z]+)(?![^>]*\/>)[^<]*<\/\1>/;
+const ignoreHTMLParseInlineMD = (text: string): string => {
+ if (text === '') return text;
+ const match = text.match(HTML_TAG_REG);
+ if (!match) return parseInlineMD(text);
+ const [matchedTxt] = match;
+ return replaceMatch((txt) => [ignoreHTMLParseInlineMD(txt)], text, match, matchedTxt).join('');
+};
+
export const toMatrixCustomHTML = (
node: Descendant | Descendant[],
opts: OutputOptions
): string => {
- const parseNode = (n: Descendant) => {
+ let markdownLines = '';
+ const parseNode = (n: Descendant, index: number, targetNodes: Descendant[]) => {
+ if (opts.allowBlockMarkdown && 'type' in n && n.type === BlockType.Paragraph) {
+ const line = toMatrixCustomHTML(n, {
+ ...opts,
+ allowInlineMarkdown: false,
+ allowBlockMarkdown: false,
+ })
+ .replace(/<br\/>$/, '\n')
+ .replace(/^>/, '>');
+ markdownLines += line;
+ if (index === targetNodes.length - 1) {
+ return parseBlockMD(markdownLines, ignoreHTMLParseInlineMD);
+ }
+ return '';
+ }
+
+ const parsedMarkdown = parseBlockMD(markdownLines, ignoreHTMLParseInlineMD);
+ markdownLines = '';
const isCodeLine = 'type' in n && n.type === BlockType.CodeLine;
- if (isCodeLine) return toMatrixCustomHTML(n, {});
- return toMatrixCustomHTML(n, opts);
+ if (isCodeLine) return `${parsedMarkdown}${toMatrixCustomHTML(n, {})}`;
+
+ return `${parsedMarkdown}${toMatrixCustomHTML(n, { ...opts, allowBlockMarkdown: false })}`;
};
if (Array.isArray(node)) return node.map(parseNode).join('');
if (Text.isText(node)) return textToCustomHtml(node, opts);
};
}, [replyEvent, mx, room, eventId]);
+ const badEncryption = replyEvent?.getContent().msgtype === 'm.bad.encrypted';
+ const bodyJSX = body ? trimReplyFromBody(body) : fallbackBody;
+
return (
<Box
className={classNames(css.Reply, className)}
<Box grow="Yes" className={css.ReplyContent}>
{replyEvent !== undefined ? (
<Text className={css.ReplyContentText} size="T300" truncate>
- {replyEvent?.getContent().msgtype === 'm.bad.encrypted' ? (
- <MessageBadEncryptedContent />
- ) : (
- (body && trimReplyFromBody(body)) ?? fallbackBody
- )}
+ {badEncryption ? <MessageBadEncryptedContent /> : bodyJSX}
</Text>
) : (
<LinePlaceholder
let customHtml = trimCustomHtml(
toMatrixCustomHTML(editor.children, {
allowTextFormatting: true,
- allowMarkdown: isMarkdown,
+ allowBlockMarkdown: isMarkdown,
+ allowInlineMarkdown: isMarkdown,
})
);
let msgType = MsgType.Text;
getReactionContent,
isMembershipChanged,
reactionOrEditEvent,
+ trimReplyFromBody,
} from '../../utils/room';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
editedEvent?.getContent()['m.new_content'] ?? mEvent.getContent();
if (typeof body !== 'string') return null;
- const jumboEmoji = JUMBO_EMOJI_REG.test(body);
+ const jumboEmoji = JUMBO_EMOJI_REG.test(trimReplyFromBody(body));
return (
<Text
const customHtml = trimCustomHtml(
toMatrixCustomHTML(editor.children, {
allowTextFormatting: true,
- allowMarkdown: isMarkdown,
+ allowBlockMarkdown: isMarkdown,
+ allowInlineMarkdown: isMarkdown,
})
);
});
export const ReactionsTooltipText = style({
- wordBreak: 'break-all',
+ wordBreak: 'break-word',
});
content={<Text variant="b3">{`Use ${isMacOS() ? KeySymbol.Command : 'Ctrl'} + ENTER to send message and ENTER for newline.`}</Text>}
/>
<SettingTile
- title="Inline Markdown formatting"
+ title="Markdown formatting"
options={(
<Toggle
isActive={isMarkdown}
onToggle={() => setIsMarkdown(!isMarkdown) }
/>
)}
- content={<Text variant="b3">Format messages with inline markdown syntax before sending.</Text>}
+ content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>}
/>
<SettingTile
title="Hide membership events"
/* eslint-disable jsx-a11y/alt-text */
import React, { ReactEventHandler, Suspense, lazy } from 'react';
-import parse, {
+import {
Element,
Text as DOMText,
HTMLReactParserOptions,
import { getMxIdLocalPart, getRoomWithCanonicalAlias } from '../utils/matrix';
import { getMemberDisplayName } from '../utils/room';
import { EMOJI_PATTERN, URL_NEG_LB } from '../utils/regex';
-import { sanitizeText } from '../utils/sanitize';
import { getHexcodeForEmoji, getShortcodeFor } from './emoji';
+import { replaceMatch } from '../utils/markdown';
const ReactPrism = lazy(() => import('./react-prism/ReactPrism'));
-const EMOJI_REG = new RegExp(`${URL_NEG_LB}(${EMOJI_PATTERN})`, 'g');
+const EMOJI_REG = new RegExp(`${URL_NEG_LB}(${EMOJI_PATTERN})`);
export const LINKIFY_OPTS: LinkifyOpts = {
attributes: {
ignoreTags: ['span'],
};
-const emojifyParserOptions: HTMLReactParserOptions = {
- replace: (domNode) => {
- if (domNode instanceof DOMText) {
- return <Linkify options={LINKIFY_OPTS}>{domNode.data}</Linkify>;
- }
- return undefined;
- },
-};
+const stringToEmojifyJSX = (text: string): (string | JSX.Element)[] => {
+ const match = text.match(EMOJI_REG);
+ if (!match) return [text];
+
+ const [emoji] = match;
-export const emojifyAndLinkify = (unsafeText: string, linkify?: boolean) => {
- const emojifyHtml = sanitizeText(unsafeText).replace(
- EMOJI_REG,
- (emoji) =>
- `<span class="${css.EmoticonBase}"><span class="${css.Emoticon()}" title="${getShortcodeFor(
- getHexcodeForEmoji(emoji)
- )}">${emoji}</span></span>`
+ return replaceMatch(
+ stringToEmojifyJSX,
+ text,
+ match,
+ <span className={css.EmoticonBase}>
+ <span className={css.Emoticon()} title={getShortcodeFor(getHexcodeForEmoji(emoji))}>
+ {emoji}
+ </span>
+ </span>
);
+};
+
+export const emojifyAndLinkify = (text: string, linkify?: boolean) => {
+ const emojifyJSX = stringToEmojifyJSX(text);
- return <>{parse(emojifyHtml, linkify ? emojifyParserOptions : undefined)}</>;
+ if (linkify) {
+ return <Linkify options={LINKIFY_OPTS}>{emojifyJSX}</Linkify>;
+ }
+ return emojifyJSX;
};
export const getReactCustomHtmlParser = (
if (typeof codeReact === 'string') {
let lang = props.className;
if (lang === 'language-rs') lang = 'language-rust';
+ else if (lang === 'language-js') lang = 'language-javascript';
+ else if (lang === 'language-ts') lang = 'language-typescript';
return (
<ErrorBoundary fallback={<code {...props}>{codeReact}</code>}>
<Suspense fallback={<code {...props}>{codeReact}</code>}>
const defaultSettings: Settings = {
themeIndex: 0,
useSystemTheme: true,
- isMarkdown: false,
+ isMarkdown: true,
editorToolbar: false,
twitterEmoji: false,
-export type PlainMDParser = (text: string) => string;
export type MatchResult = RegExpMatchArray | RegExpExecArray;
export type RuleMatch = (text: string) => MatchResult | null;
-export type MatchConverter = (parse: PlainMDParser, match: MatchResult) => string;
-export type MDRule = {
- match: RuleMatch;
- html: MatchConverter;
-};
+export const beforeMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string =>
+ text.slice(0, match.index);
+export const afterMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string =>
+ text.slice((match.index ?? 0) + match[0].length);
-export type MatchReplacer = (
- parse: PlainMDParser,
+export const replaceMatch = <C>(
+ convertPart: (txt: string) => Array<string | C>,
text: string,
match: MatchResult,
- content: string
-) => string;
+ content: C
+): Array<string | C> => [
+ ...convertPart(beforeMatch(text, match)),
+ content,
+ ...convertPart(afterMatch(text, match)),
+];
+
+/*
+ *****************
+ * INLINE PARSER *
+ *****************
+ */
+
+export type InlineMDParser = (text: string) => string;
-export type RuleRunner = (parse: PlainMDParser, text: string, rule: MDRule) => string | undefined;
-export type RulesRunner = (
- parse: PlainMDParser,
+export type InlineMatchConverter = (parse: InlineMDParser, match: MatchResult) => string;
+
+export type InlineMDRule = {
+ match: RuleMatch;
+ html: InlineMatchConverter;
+};
+
+export type InlineRuleRunner = (
+ parse: InlineMDParser,
text: string,
- rules: MDRule[]
+ rule: InlineMDRule
+) => string | undefined;
+export type InlineRulesRunner = (
+ parse: InlineMDParser,
+ text: string,
+ rules: InlineMDRule[]
) => string | undefined;
const MIN_ANY = '(.+?)';
const BOLD_REG_1 = new RegExp(
`${URL_NEG_LB}${BOLD_PREFIX_1}${MIN_ANY}${BOLD_PREFIX_1}${BOLD_NEG_LA_1}`
);
-const BoldRule: MDRule = {
+const BoldRule: InlineMDRule = {
match: (text) => text.match(BOLD_REG_1),
html: (parse, match) => {
- const [, g1] = match;
- return `<strong data-md="${BOLD_MD_1}">${parse(g1)}</strong>`;
+ const [, , g2] = match;
+ return `<strong data-md="${BOLD_MD_1}">${parse(g2)}</strong>`;
},
};
const ITALIC_REG_1 = new RegExp(
`${URL_NEG_LB}${ITALIC_PREFIX_1}${MIN_ANY}${ITALIC_PREFIX_1}${ITALIC_NEG_LA_1}`
);
-const ItalicRule1: MDRule = {
+const ItalicRule1: InlineMDRule = {
match: (text) => text.match(ITALIC_REG_1),
html: (parse, match) => {
- const [, g1] = match;
- return `<i data-md="${ITALIC_MD_1}">${parse(g1)}</i>`;
+ const [, , g2] = match;
+ return `<i data-md="${ITALIC_MD_1}">${parse(g2)}</i>`;
},
};
const ITALIC_REG_2 = new RegExp(
`${URL_NEG_LB}${ITALIC_PREFIX_2}${MIN_ANY}${ITALIC_PREFIX_2}${ITALIC_NEG_LA_2}`
);
-const ItalicRule2: MDRule = {
+const ItalicRule2: InlineMDRule = {
match: (text) => text.match(ITALIC_REG_2),
html: (parse, match) => {
- const [, g1] = match;
- return `<i data-md="${ITALIC_MD_2}">${parse(g1)}</i>`;
+ const [, , g2] = match;
+ return `<i data-md="${ITALIC_MD_2}">${parse(g2)}</i>`;
},
};
const UNDERLINE_REG_1 = new RegExp(
`${URL_NEG_LB}${UNDERLINE_PREFIX_1}${MIN_ANY}${UNDERLINE_PREFIX_1}${UNDERLINE_NEG_LA_1}`
);
-const UnderlineRule: MDRule = {
+const UnderlineRule: InlineMDRule = {
match: (text) => text.match(UNDERLINE_REG_1),
html: (parse, match) => {
- const [, g1] = match;
- return `<u data-md="${UNDERLINE_MD_1}">${parse(g1)}</u>`;
+ const [, , g2] = match;
+ return `<u data-md="${UNDERLINE_MD_1}">${parse(g2)}</u>`;
},
};
const STRIKE_REG_1 = new RegExp(
`${URL_NEG_LB}${STRIKE_PREFIX_1}${MIN_ANY}${STRIKE_PREFIX_1}${STRIKE_NEG_LA_1}`
);
-const StrikeRule: MDRule = {
+const StrikeRule: InlineMDRule = {
match: (text) => text.match(STRIKE_REG_1),
html: (parse, match) => {
- const [, g1] = match;
- return `<del data-md="${STRIKE_MD_1}">${parse(g1)}</del>`;
+ const [, , g2] = match;
+ return `<del data-md="${STRIKE_MD_1}">${parse(g2)}</del>`;
},
};
const CODE_MD_1 = '`';
const CODE_PREFIX_1 = '`';
const CODE_NEG_LA_1 = '(?!`)';
-const CODE_REG_1 = new RegExp(
- `${URL_NEG_LB}${CODE_PREFIX_1}${MIN_ANY}${CODE_PREFIX_1}${CODE_NEG_LA_1}`
-);
-const CodeRule: MDRule = {
+const CODE_REG_1 = new RegExp(`${URL_NEG_LB}${CODE_PREFIX_1}(.+?)${CODE_PREFIX_1}${CODE_NEG_LA_1}`);
+const CodeRule: InlineMDRule = {
match: (text) => text.match(CODE_REG_1),
html: (parse, match) => {
- const [, g1] = match;
- return `<code data-md="${CODE_MD_1}">${g1}</code>`;
+ const [, , g2] = match;
+ return `<code data-md="${CODE_MD_1}">${g2}</code>`;
},
};
const SPOILER_REG_1 = new RegExp(
`${URL_NEG_LB}${SPOILER_PREFIX_1}${MIN_ANY}${SPOILER_PREFIX_1}${SPOILER_NEG_LA_1}`
);
-const SpoilerRule: MDRule = {
+const SpoilerRule: InlineMDRule = {
match: (text) => text.match(SPOILER_REG_1),
html: (parse, match) => {
- const [, g1] = match;
- return `<span data-md="${SPOILER_MD_1}" data-mx-spoiler>${parse(g1)}</span>`;
+ const [, , g2] = match;
+ return `<span data-md="${SPOILER_MD_1}" data-mx-spoiler>${parse(g2)}</span>`;
},
};
const LINK_ALT = `\\[${MIN_ANY}\\]`;
const LINK_URL = `\\((https?:\\/\\/.+?)\\)`;
const LINK_REG_1 = new RegExp(`${LINK_ALT}${LINK_URL}`);
-const LinkRule: MDRule = {
+const LinkRule: InlineMDRule = {
match: (text) => text.match(LINK_REG_1),
html: (parse, match) => {
const [, g1, g2] = match;
},
};
-const beforeMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string =>
- text.slice(0, match.index);
-const afterMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string =>
- text.slice((match.index ?? 0) + match[0].length);
-
-const replaceMatch: MatchReplacer = (parse, text, match, content) =>
- `${parse(beforeMatch(text, match))}${content}${parse(afterMatch(text, match))}`;
-
-const runRule: RuleRunner = (parse, text, rule) => {
+const runInlineRule: InlineRuleRunner = (parse, text, rule) => {
const matchResult = rule.match(text);
if (matchResult) {
const content = rule.html(parse, matchResult);
- return replaceMatch(parse, text, matchResult, content);
+ return replaceMatch((txt) => [parse(txt)], text, matchResult, content).join('');
}
return undefined;
};
* Runs multiple rules at the same time to better handle nested rules.
* Rules will be run in the order they appear.
*/
-const runRules: RulesRunner = (parse, text, rules) => {
+const runInlineRules: InlineRulesRunner = (parse, text, rules) => {
const matchResults = rules.map((rule) => rule.match(text));
- let targetRule: MDRule | undefined;
+ let targetRule: InlineMDRule | undefined;
let targetResult: MatchResult | undefined;
for (let i = 0; i < matchResults.length; i += 1) {
if (targetRule && targetResult) {
const content = targetRule.html(parse, targetResult);
- return replaceMatch(parse, text, targetResult, content);
+ return replaceMatch((txt) => [parse(txt)], text, targetResult, content).join('');
}
return undefined;
};
LinkRule,
];
-export const parseInlineMD = (text: string): string => {
+export const parseInlineMD: InlineMDParser = (text) => {
+ if (text === '') return text;
let result: string | undefined;
- if (!result) result = runRule(parseInlineMD, text, CodeRule);
+ if (!result) result = runInlineRule(parseInlineMD, text, CodeRule);
+
+ if (!result) result = runInlineRules(parseInlineMD, text, LeveledRules);
+
+ return result ?? text;
+};
+
+/*
+ ****************
+ * BLOCK PARSER *
+ ****************
+ */
+
+export type BlockMDParser = (test: string, parseInline?: (txt: string) => string) => string;
- if (!result) result = runRules(parseInlineMD, text, LeveledRules);
+export type BlockMatchConverter = (
+ match: MatchResult,
+ parseInline?: (txt: string) => string
+) => string;
+
+export type BlockMDRule = {
+ match: RuleMatch;
+ html: BlockMatchConverter;
+};
+
+export type BlockRuleRunner = (
+ parse: BlockMDParser,
+ text: string,
+ rule: BlockMDRule,
+ parseInline?: (txt: string) => string
+) => string | undefined;
+
+const HEADING_REG_1 = /^(#{1,6}) +(.+)\n?/m;
+const HeadingRule: BlockMDRule = {
+ match: (text) => text.match(HEADING_REG_1),
+ html: (match, parseInline) => {
+ const [, g1, g2] = match;
+ const level = g1.length;
+ return `<h${level} data-md="${g1}">${parseInline ? parseInline(g2) : g2}</h${level}>`;
+ },
+};
+
+const CODEBLOCK_MD_1 = '```';
+const CODEBLOCK_REG_1 = /^`{3}(\S*)\n((.+\n)+)`{3} *(?!.)\n?/m;
+const CodeBlockRule: BlockMDRule = {
+ match: (text) => text.match(CODEBLOCK_REG_1),
+ html: (match) => {
+ const [, g1, g2] = match;
+ const classNameAtt = g1 ? ` class="language-${g1}"` : '';
+ return `<pre data-md="${CODEBLOCK_MD_1}"><code${classNameAtt}>${g2}</code$></pre>`;
+ },
+};
+
+const BLOCKQUOTE_MD_1 = '>';
+const QUOTE_LINE_PREFIX = /^> */;
+const BLOCKQUOTE_TRAILING_NEWLINE = /\n$/;
+const BLOCKQUOTE_REG_1 = /(^>.*\n?)+/m;
+const BlockQuoteRule: BlockMDRule = {
+ match: (text) => text.match(BLOCKQUOTE_REG_1),
+ html: (match, parseInline) => {
+ const [blockquoteText] = match;
+
+ const lines = blockquoteText
+ .replace(BLOCKQUOTE_TRAILING_NEWLINE, '')
+ .split('\n')
+ .map((lineText) => {
+ const line = lineText.replace(QUOTE_LINE_PREFIX, '');
+ if (parseInline) return `${parseInline(line)}<br/>`;
+ return `${line}<br/>`;
+ })
+ .join('');
+ return `<blockquote data-md="${BLOCKQUOTE_MD_1}">${lines}</blockquote>`;
+ },
+};
+
+const ORDERED_LIST_MD_1 = '-';
+const O_LIST_ITEM_PREFIX = /^(-|[\da-zA-Z]\.) */;
+const O_LIST_START = /^([\d])\./;
+const O_LIST_TYPE = /^([aAiI])\./;
+const O_LIST_TRAILING_NEWLINE = /\n$/;
+const ORDERED_LIST_REG_1 = /(^(-|[\da-zA-Z]\.) +.+\n?)+/m;
+const OrderedListRule: BlockMDRule = {
+ match: (text) => text.match(ORDERED_LIST_REG_1),
+ html: (match, parseInline) => {
+ const [listText] = match;
+ const [, listStart] = listText.match(O_LIST_START) ?? [];
+ const [, listType] = listText.match(O_LIST_TYPE) ?? [];
+
+ const lines = listText
+ .replace(O_LIST_TRAILING_NEWLINE, '')
+ .split('\n')
+ .map((lineText) => {
+ const line = lineText.replace(O_LIST_ITEM_PREFIX, '');
+ const txt = parseInline ? parseInline(line) : line;
+ return `<li><p>${txt}</p></li>`;
+ })
+ .join('');
+
+ const dataMdAtt = `data-md="${listType || listStart || ORDERED_LIST_MD_1}"`;
+ const startAtt = listStart ? ` start="${listStart}"` : '';
+ const typeAtt = listType ? ` type="${listType}"` : '';
+ return `<ol ${dataMdAtt}${startAtt}${typeAtt}>${lines}</ol>`;
+ },
+};
+
+const UNORDERED_LIST_MD_1 = '*';
+const U_LIST_ITEM_PREFIX = /^\* */;
+const U_LIST_TRAILING_NEWLINE = /\n$/;
+const UNORDERED_LIST_REG_1 = /(^\* +.+\n?)+/m;
+const UnorderedListRule: BlockMDRule = {
+ match: (text) => text.match(UNORDERED_LIST_REG_1),
+ html: (match, parseInline) => {
+ const [listText] = match;
+
+ const lines = listText
+ .replace(U_LIST_TRAILING_NEWLINE, '')
+ .split('\n')
+ .map((lineText) => {
+ const line = lineText.replace(U_LIST_ITEM_PREFIX, '');
+ const txt = parseInline ? parseInline(line) : line;
+ return `<li><p>${txt}</p></li>`;
+ })
+ .join('');
+
+ return `<ul data-md="${UNORDERED_LIST_MD_1}">${lines}</ul>`;
+ },
+};
+
+const runBlockRule: BlockRuleRunner = (parse, text, rule, parseInline) => {
+ const matchResult = rule.match(text);
+ if (matchResult) {
+ const content = rule.html(matchResult, parseInline);
+ return replaceMatch((txt) => [parse(txt, parseInline)], text, matchResult, content).join('');
+ }
+ return undefined;
+};
+
+export const parseBlockMD: BlockMDParser = (text, parseInline) => {
+ if (text === '') return text;
+ let result: string | undefined;
+
+ if (!result) result = runBlockRule(parseBlockMD, text, CodeBlockRule, parseInline);
+ if (!result) result = runBlockRule(parseBlockMD, text, BlockQuoteRule, parseInline);
+ if (!result) result = runBlockRule(parseBlockMD, text, OrderedListRule, parseInline);
+ if (!result) result = runBlockRule(parseBlockMD, text, UnorderedListRule, parseInline);
+ if (!result) result = runBlockRule(parseBlockMD, text, HeadingRule, parseInline);
+
+ // replace \n with <br/> because want to preserve empty lines
+ if (!result) {
+ if (parseInline) {
+ result = text
+ .split('\n')
+ .map((lineText) => parseInline(lineText))
+ .join('<br/>');
+ } else {
+ result = text.replace(/\n/g, '<br/>');
+ }
+ }
return result ?? text;
};
};
export const trimReplyFromBody = (body: string): string => {
- const match = body.match(/^>\s<.+?>\s.+\n\n/);
+ const match = body.match(/^> <.+?> .+\n(>.*\n)*?\n/m);
if (!match) return body;
return body.slice(match[0].length);
};
'data-md',
],
div: ['data-mx-maths'],
+ blockquote: ['data-md'],
+ h1: ['data-md'],
+ h2: ['data-md'],
+ h3: ['data-md'],
+ h4: ['data-md'],
+ h5: ['data-md'],
+ h6: ['data-md'],
+ pre: ['data-md', 'class'],
+ ol: ['start', 'type', 'data-md'],
+ ul: ['data-md'],
a: ['name', 'target', 'href', 'rel', 'data-md'],
img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'],
- ol: ['start'],
code: ['class', 'data-md'],
strong: ['data-md'],
i: ['data-md'],