mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
refactor: return jsx element instead of string in marked (#910)
* refactor: return jsx element instead of string in marked * chore: update
This commit is contained in:
@ -2,6 +2,7 @@ import { TextField } from "@mui/joy";
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTagStore } from "../store/module";
|
import { useTagStore } from "../store/module";
|
||||||
import { getTagSuggestionList } from "../helpers/api";
|
import { getTagSuggestionList } from "../helpers/api";
|
||||||
|
import { matcher } from "../labs/marked/matcher";
|
||||||
import Tag from "../labs/marked/parser/Tag";
|
import Tag from "../labs/marked/parser/Tag";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import toastHelper from "./Toast";
|
import toastHelper from "./Toast";
|
||||||
@ -10,7 +11,7 @@ import { generateDialog } from "./Dialog";
|
|||||||
type Props = DialogProps;
|
type Props = DialogProps;
|
||||||
|
|
||||||
const validateTagName = (tagName: string): boolean => {
|
const validateTagName = (tagName: string): boolean => {
|
||||||
const matchResult = Tag.matcher(`#${tagName}`);
|
const matchResult = matcher(`#${tagName}`, Tag.regexp);
|
||||||
if (!matchResult || matchResult[1] !== tagName) {
|
if (!matchResult || matchResult[1] !== tagName) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ import "../less/memo.less";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
memo: Memo;
|
memo: Memo;
|
||||||
highlightWord?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
|
export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
|
||||||
@ -27,7 +26,7 @@ export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Memo: React.FC<Props> = (props: Props) => {
|
const Memo: React.FC<Props> = (props: Props) => {
|
||||||
const { memo, highlightWord } = props;
|
const { memo } = props;
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const editorStore = useEditorStore();
|
const editorStore = useEditorStore();
|
||||||
@ -143,9 +142,6 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
if (imgUrl) {
|
if (imgUrl) {
|
||||||
showPreviewImageDialog([imgUrl], 0);
|
showPreviewImageDialog([imgUrl], 0);
|
||||||
}
|
}
|
||||||
} else if (targetEl.tagName === "BUTTON" && targetEl.className === "codeblock-copy-btn") {
|
|
||||||
copy(targetEl.parentElement?.children[1].textContent ?? "");
|
|
||||||
toastHelper.success(t("message.succeed-copy-code"));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -228,7 +224,6 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<MemoContent
|
<MemoContent
|
||||||
content={memo.content}
|
content={memo.content}
|
||||||
highlightWord={highlightWord}
|
|
||||||
onMemoContentClick={handleMemoContentClick}
|
onMemoContentClick={handleMemoContentClick}
|
||||||
onMemoContentDoubleClick={handleMemoContentDoubleClick}
|
onMemoContentDoubleClick={handleMemoContentDoubleClick}
|
||||||
/>
|
/>
|
||||||
|
@ -2,7 +2,6 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useUserStore } from "../store/module";
|
import { useUserStore } from "../store/module";
|
||||||
import { marked } from "../labs/marked";
|
import { marked } from "../labs/marked";
|
||||||
import { highlightWithWord } from "../labs/highlighter";
|
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import "../less/memo-content.less";
|
import "../less/memo-content.less";
|
||||||
|
|
||||||
@ -12,7 +11,6 @@ export interface DisplayConfig {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
content: string;
|
content: string;
|
||||||
highlightWord?: string;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
displayConfig?: Partial<DisplayConfig>;
|
displayConfig?: Partial<DisplayConfig>;
|
||||||
onMemoContentClick?: (e: React.MouseEvent) => void;
|
onMemoContentClick?: (e: React.MouseEvent) => void;
|
||||||
@ -30,14 +28,14 @@ const defaultDisplayConfig: DisplayConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MemoContent: React.FC<Props> = (props: Props) => {
|
const MemoContent: React.FC<Props> = (props: Props) => {
|
||||||
const { className, content, highlightWord, onMemoContentClick, onMemoContentDoubleClick } = props;
|
const { className, content, onMemoContentClick, onMemoContentDoubleClick } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const user = userStore.state.user;
|
||||||
const foldedContent = useMemo(() => {
|
const foldedContent = useMemo(() => {
|
||||||
const firstHorizontalRuleIndex = content.search(/^---$|^\*\*\*$|^___$/m);
|
const firstHorizontalRuleIndex = content.search(/^---$|^\*\*\*$|^___$/m);
|
||||||
return firstHorizontalRuleIndex !== -1 ? content.slice(0, firstHorizontalRuleIndex) : content;
|
return firstHorizontalRuleIndex !== -1 ? content.slice(0, firstHorizontalRuleIndex) : content;
|
||||||
}, [content]);
|
}, [content]);
|
||||||
const { t } = useTranslation();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const user = userStore.state.user;
|
|
||||||
|
|
||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
expandButtonStatus: -1,
|
expandButtonStatus: -1,
|
||||||
@ -97,10 +95,9 @@ const MemoContent: React.FC<Props> = (props: Props) => {
|
|||||||
className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`}
|
className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`}
|
||||||
onClick={handleMemoContentClick}
|
onClick={handleMemoContentClick}
|
||||||
onDoubleClick={handleMemoContentDoubleClick}
|
onDoubleClick={handleMemoContentDoubleClick}
|
||||||
dangerouslySetInnerHTML={{
|
>
|
||||||
__html: highlightWithWord(marked(state.expandButtonStatus === 0 ? foldedContent : content), highlightWord),
|
{marked(state.expandButtonStatus === 0 ? foldedContent : content)}
|
||||||
}}
|
</div>
|
||||||
></div>
|
|
||||||
{state.expandButtonStatus !== -1 && (
|
{state.expandButtonStatus !== -1 && (
|
||||||
<div className="expand-btn-container">
|
<div className="expand-btn-container">
|
||||||
<span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleExpandBtnClick}>
|
<span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleExpandBtnClick}>
|
||||||
|
@ -19,7 +19,6 @@ const MemoList = () => {
|
|||||||
const memoDisplayTsOption = userStore.state.user?.setting.memoDisplayTsOption;
|
const memoDisplayTsOption = userStore.state.user?.setting.memoDisplayTsOption;
|
||||||
const { memos, isFetching } = memoStore.state;
|
const { memos, isFetching } = memoStore.state;
|
||||||
const [isComplete, setIsComplete] = useState<boolean>(false);
|
const [isComplete, setIsComplete] = useState<boolean>(false);
|
||||||
const [highlightWord, setHighlightWord] = useState<string | undefined>("");
|
|
||||||
|
|
||||||
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = query ?? {};
|
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = query ?? {};
|
||||||
const shortcut = shortcutId ? shortcutStore.getShortcutById(shortcutId) : null;
|
const shortcut = shortcutId ? shortcutStore.getShortcutById(shortcutId) : null;
|
||||||
@ -107,7 +106,6 @@ const MemoList = () => {
|
|||||||
if (pageWrapper) {
|
if (pageWrapper) {
|
||||||
pageWrapper.scrollTo(0, 0);
|
pageWrapper.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
setHighlightWord(query?.text);
|
|
||||||
}, [query]);
|
}, [query]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -136,7 +134,7 @@ const MemoList = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="memo-list-container">
|
<div className="memo-list-container">
|
||||||
{sortedMemos.map((memo) => (
|
{sortedMemos.map((memo) => (
|
||||||
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} highlightWord={highlightWord} />
|
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} />
|
||||||
))}
|
))}
|
||||||
{isFetching ? (
|
{isFetching ? (
|
||||||
<div className="status-text-container fetching-tip">
|
<div className="status-text-container fetching-tip">
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
import { escape } from "lodash";
|
|
||||||
|
|
||||||
const walkthroughNodeWithKeyword = (node: HTMLElement, keyword: string) => {
|
|
||||||
if (node.nodeType === 3) {
|
|
||||||
const span = document.createElement("span");
|
|
||||||
span.innerHTML = node.nodeValue?.replace(new RegExp(keyword, "g"), `<mark>${keyword}</mark>`) ?? "";
|
|
||||||
node.parentNode?.insertBefore(span, node);
|
|
||||||
node.parentNode?.removeChild(node);
|
|
||||||
}
|
|
||||||
for (const child of Array.from(node.childNodes)) {
|
|
||||||
walkthroughNodeWithKeyword(<HTMLElement>child, keyword);
|
|
||||||
}
|
|
||||||
return node.innerHTML;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const highlightWithWord = (html: string, keyword?: string): string => {
|
|
||||||
if (!keyword) {
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
keyword = escape(keyword);
|
|
||||||
const wrap = document.createElement("div");
|
|
||||||
wrap.innerHTML = escape(html);
|
|
||||||
return walkthroughNodeWithKeyword(wrap, keyword);
|
|
||||||
};
|
|
@ -1,8 +1,13 @@
|
|||||||
|
import { matcher } from "./matcher";
|
||||||
import { blockElementParserList, inlineElementParserList } from "./parser";
|
import { blockElementParserList, inlineElementParserList } from "./parser";
|
||||||
|
|
||||||
export const marked = (markdownStr: string, blockParsers = blockElementParserList, inlineParsers = inlineElementParserList): string => {
|
export const marked = (
|
||||||
|
markdownStr: string,
|
||||||
|
blockParsers = blockElementParserList,
|
||||||
|
inlineParsers = inlineElementParserList
|
||||||
|
): string | JSX.Element => {
|
||||||
for (const parser of blockParsers) {
|
for (const parser of blockParsers) {
|
||||||
const matchResult = parser.matcher(markdownStr);
|
const matchResult = matcher(markdownStr, parser.regexp);
|
||||||
if (!matchResult) {
|
if (!matchResult) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -10,12 +15,22 @@ export const marked = (markdownStr: string, blockParsers = blockElementParserLis
|
|||||||
const retainContent = markdownStr.slice(matchedStr.length);
|
const retainContent = markdownStr.slice(matchedStr.length);
|
||||||
|
|
||||||
if (parser.name === "br") {
|
if (parser.name === "br") {
|
||||||
return parser.renderer(matchedStr) + marked(retainContent, blockParsers, inlineParsers);
|
return (
|
||||||
|
<>
|
||||||
|
{parser.renderer(matchedStr)}
|
||||||
|
{marked(retainContent, blockParsers, inlineParsers)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (retainContent === "") {
|
if (retainContent === "") {
|
||||||
return parser.renderer(matchedStr);
|
return parser.renderer(matchedStr);
|
||||||
} else if (retainContent.startsWith("\n")) {
|
} else if (retainContent.startsWith("\n")) {
|
||||||
return parser.renderer(matchedStr) + marked(retainContent.slice(1), blockParsers, inlineParsers);
|
return (
|
||||||
|
<>
|
||||||
|
{parser.renderer(matchedStr)}
|
||||||
|
{marked(retainContent.slice(1), blockParsers, inlineParsers)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -24,7 +39,7 @@ export const marked = (markdownStr: string, blockParsers = blockElementParserLis
|
|||||||
let matchedIndex = -1;
|
let matchedIndex = -1;
|
||||||
|
|
||||||
for (const parser of inlineParsers) {
|
for (const parser of inlineParsers) {
|
||||||
const matchResult = parser.matcher(markdownStr);
|
const matchResult = matcher(markdownStr, parser.regexp);
|
||||||
if (!matchResult) {
|
if (!matchResult) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -41,17 +56,23 @@ export const marked = (markdownStr: string, blockParsers = blockElementParserLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matchedInlineParser) {
|
if (matchedInlineParser) {
|
||||||
const matchResult = matchedInlineParser.matcher(markdownStr);
|
const matchResult = matcher(markdownStr, matchedInlineParser.regexp);
|
||||||
if (matchResult) {
|
if (matchResult) {
|
||||||
const matchedStr = matchResult[0];
|
const matchedStr = matchResult[0];
|
||||||
const matchedLength = matchedStr.length;
|
const matchedLength = matchedStr.length;
|
||||||
const prefixStr = markdownStr.slice(0, matchedIndex);
|
const prefixStr = markdownStr.slice(0, matchedIndex);
|
||||||
const suffixStr = markdownStr.slice(matchedIndex + matchedLength);
|
const suffixStr = markdownStr.slice(matchedIndex + matchedLength);
|
||||||
return marked(prefixStr, [], inlineParsers) + matchedInlineParser.renderer(matchedStr) + marked(suffixStr, [], inlineParsers);
|
return (
|
||||||
|
<>
|
||||||
|
{marked(prefixStr, [], inlineParsers)}
|
||||||
|
{matchedInlineParser.renderer(matchedStr)}
|
||||||
|
{marked(suffixStr, [], inlineParsers)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return markdownStr;
|
return <>{markdownStr}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface MatchedNode {
|
interface MatchedNode {
|
||||||
@ -64,7 +85,7 @@ export const getMatchedNodes = (markdownStr: string): MatchedNode[] => {
|
|||||||
|
|
||||||
const walkthough = (markdownStr: string, blockParsers = blockElementParserList, inlineParsers = inlineElementParserList): string => {
|
const walkthough = (markdownStr: string, blockParsers = blockElementParserList, inlineParsers = inlineElementParserList): string => {
|
||||||
for (const parser of blockParsers) {
|
for (const parser of blockParsers) {
|
||||||
const matchResult = parser.matcher(markdownStr);
|
const matchResult = matcher(markdownStr, parser.regexp);
|
||||||
if (!matchResult) {
|
if (!matchResult) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -79,6 +100,7 @@ export const getMatchedNodes = (markdownStr: string): MatchedNode[] => {
|
|||||||
return walkthough(retainContent, blockParsers, inlineParsers);
|
return walkthough(retainContent, blockParsers, inlineParsers);
|
||||||
} else {
|
} else {
|
||||||
if (retainContent.startsWith("\n")) {
|
if (retainContent.startsWith("\n")) {
|
||||||
|
walkthough(matchedStr, [], inlineParsers);
|
||||||
return walkthough(retainContent.slice(1), blockParsers, inlineParsers);
|
return walkthough(retainContent.slice(1), blockParsers, inlineParsers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,7 +110,7 @@ export const getMatchedNodes = (markdownStr: string): MatchedNode[] => {
|
|||||||
let matchedIndex = -1;
|
let matchedIndex = -1;
|
||||||
|
|
||||||
for (const parser of inlineParsers) {
|
for (const parser of inlineParsers) {
|
||||||
const matchResult = parser.matcher(markdownStr);
|
const matchResult = matcher(markdownStr, parser.regexp);
|
||||||
if (!matchResult) {
|
if (!matchResult) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -105,7 +127,7 @@ export const getMatchedNodes = (markdownStr: string): MatchedNode[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matchedInlineParser) {
|
if (matchedInlineParser) {
|
||||||
const matchResult = matchedInlineParser.matcher(markdownStr);
|
const matchResult = matcher(markdownStr, matchedInlineParser.regexp);
|
||||||
if (matchResult) {
|
if (matchResult) {
|
||||||
const matchedStr = matchResult[0];
|
const matchedStr = matchResult[0];
|
||||||
const matchedLength = matchedStr.length;
|
const matchedLength = matchedStr.length;
|
@ -1,174 +0,0 @@
|
|||||||
/* eslint-disable no-irregular-whitespace */
|
|
||||||
import { describe, expect, test } from "@jest/globals";
|
|
||||||
import { unescape } from "lodash-es";
|
|
||||||
import { marked } from ".";
|
|
||||||
|
|
||||||
describe("test marked parser", () => {
|
|
||||||
test("horizontal rule", () => {
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
markdown: `---
|
|
||||||
This is some text after the horizontal rule.
|
|
||||||
___
|
|
||||||
This is some text after the horizontal rule.
|
|
||||||
***
|
|
||||||
This is some text after the horizontal rule.`,
|
|
||||||
want: `<hr><p>This is some text after the horizontal rule.</p><hr><p>This is some text after the horizontal rule.</p><hr><p>This is some text after the horizontal rule.</p>`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
for (const t of tests) {
|
|
||||||
expect(unescape(marked(t.markdown))).toBe(t.want);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test("parse code block", () => {
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
markdown: `\`\`\`
|
|
||||||
hello world!
|
|
||||||
\`\`\``,
|
|
||||||
want: `<pre><button class="codeblock-copy-btn">copy</button><code class="language-plaintext">hello world!
|
|
||||||
</code></pre>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
markdown: `test code block
|
|
||||||
|
|
||||||
\`\`\`js
|
|
||||||
console.log("hello world!")
|
|
||||||
\`\`\``,
|
|
||||||
want: `<p>test code block</p><br><pre><button class="codeblock-copy-btn">copy</button><code class="language-js"><span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">"hello world!"</span>)
|
|
||||||
</code></pre>`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const t of tests) {
|
|
||||||
expect(unescape(marked(t.markdown))).toBe(t.want);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test("parse todo list block", () => {
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
markdown: `My task:
|
|
||||||
- [ ] finish my homework
|
|
||||||
- [x] yahaha`,
|
|
||||||
want: `<p>My task:</p><p class='li-container'><span class='todo-block todo' data-value='TODO'></span><span>finish my homework</span></p><p class='li-container'><span class='todo-block done' data-value='DONE'>✓</span><span>yahaha</span></p>`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const t of tests) {
|
|
||||||
expect(unescape(marked(t.markdown))).toBe(t.want);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test("parse list block", () => {
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
markdown: `This is a list
|
|
||||||
* list 123
|
|
||||||
1. 123123`,
|
|
||||||
want: `<p>This is a list</p><p class='li-container'><span class='ul-block'>•</span><span>list 123</span></p><p class='li-container'><span class='ol-block'>1.</span><span>123123</span></p>`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const t of tests) {
|
|
||||||
expect(unescape(marked(t.markdown))).toBe(t.want);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test("parse inline element", () => {
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
markdown: `Link: [baidu](https://baidu.com#1231)`,
|
|
||||||
want: `<p>Link: <a class='link' target='_blank' rel='noreferrer' href='https://baidu.com#1231'>baidu</a></p>`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const t of tests) {
|
|
||||||
expect(unescape(marked(t.markdown))).toBe(t.want);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test("parse inline code within inline element", () => {
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
markdown: `Link: [\`baidu\`](https://baidu.com)`,
|
|
||||||
want: `<p>Link: <a class='link' target='_blank' rel='noreferrer' href='https://baidu.com'><code>baidu</code></a></p>`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const t of tests) {
|
|
||||||
expect(unescape(marked(t.markdown))).toBe(t.want);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test("parse plain link", () => {
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
markdown: `Link:https://baidu.com#1231`,
|
|
||||||
want: `<p>Link:<a class='link' target='_blank' rel='noreferrer' href='https://baidu.com#1231'>https://baidu.com#1231</a></p>`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const t of tests) {
|
|
||||||
expect(unescape(marked(t.markdown))).toBe(t.want);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test("parse inline code", () => {
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
markdown: `Code: \`console.log("Hello world!")\``,
|
|
||||||
want: `<p>Code: <code>console.log("Hello world!")</code></p>`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const t of tests) {
|
|
||||||
expect(unescape(marked(t.markdown))).toBe(t.want);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test("parse bold and em text", () => {
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
markdown: `Important: **Minecraft**`,
|
|
||||||
want: `<p>Important: <strong>Minecraft</strong></p>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
markdown: `Em: *Minecraft*`,
|
|
||||||
want: `<p>Em: <em>Minecraft</em></p>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
markdown: `Important: ***Minecraft/123***`,
|
|
||||||
want: `<p>Important: <strong><em>Minecraft/123</em></strong></p>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
markdown: `Important: ***[baidu](https://baidu.com)***`,
|
|
||||||
want: `<p>Important: <strong><em><a class='link' target='_blank' rel='noreferrer' href='https://baidu.com'>baidu</a></em></strong></p>`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const t of tests) {
|
|
||||||
expect(unescape(marked(t.markdown))).toBe(t.want);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test("parse full width space", () => {
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
markdown: ` line1
|
|
||||||
line2`,
|
|
||||||
want: `<p> line1</p><p> line2</p>`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
for (const t of tests) {
|
|
||||||
expect(unescape(marked(t.markdown))).toBe(t.want);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test("parse heading", () => {
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
markdown: `# 123 `,
|
|
||||||
want: `<h1>123 </h1>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
markdown: `## 123 `,
|
|
||||||
want: `<h2>123 </h2>`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
for (const t of tests) {
|
|
||||||
expect(unescape(marked(t.markdown))).toBe(t.want);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
4
web/src/labs/marked/matcher.ts
Normal file
4
web/src/labs/marked/matcher.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const matcher = (rawStr: string, regexp: RegExp) => {
|
||||||
|
const matchResult = rawStr.match(regexp);
|
||||||
|
return matchResult;
|
||||||
|
};
|
@ -1,24 +0,0 @@
|
|||||||
import { escape } from "lodash";
|
|
||||||
|
|
||||||
export const BLOCKQUOTE_REG = /^> ([^\n]+)/;
|
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(BLOCKQUOTE_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
|
||||||
return rawStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<blockquote>${escape(matchResult[1])}</blockquote>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "blockquote",
|
|
||||||
regex: BLOCKQUOTE_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
19
web/src/labs/marked/parser/Blockquote.tsx
Normal file
19
web/src/labs/marked/parser/Blockquote.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { escape } from "lodash";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
|
||||||
|
export const BLOCKQUOTE_REG = /^> ([^\n]+)/;
|
||||||
|
|
||||||
|
const renderer = (rawStr: string) => {
|
||||||
|
const matchResult = matcher(rawStr, BLOCKQUOTE_REG);
|
||||||
|
if (!matchResult) {
|
||||||
|
return <>{rawStr}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <blockquote>{escape(matchResult[1])}</blockquote>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "blockquote",
|
||||||
|
regexp: BLOCKQUOTE_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
@ -1,27 +0,0 @@
|
|||||||
import { marked } from "..";
|
|
||||||
import Link from "./Link";
|
|
||||||
import PlainText from "./PlainText";
|
|
||||||
|
|
||||||
export const BOLD_REG = /\*\*(.+?)\*\*/;
|
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(BOLD_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
|
||||||
return rawStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedContent = marked(matchResult[1], [], [Link, PlainText]);
|
|
||||||
return `<strong>${parsedContent}</strong>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "bold",
|
|
||||||
regex: BOLD_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
22
web/src/labs/marked/parser/Bold.tsx
Normal file
22
web/src/labs/marked/parser/Bold.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { marked } from "..";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
import Link from "./Link";
|
||||||
|
import PlainText from "./PlainText";
|
||||||
|
|
||||||
|
export const BOLD_REG = /\*\*(.+?)\*\*/;
|
||||||
|
|
||||||
|
const renderer = (rawStr: string) => {
|
||||||
|
const matchResult = matcher(rawStr, BOLD_REG);
|
||||||
|
if (!matchResult) {
|
||||||
|
return <>{rawStr}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedContent = marked(matchResult[1], [], [Link, PlainText]);
|
||||||
|
return <strong>{parsedContent}</strong>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "bold",
|
||||||
|
regexp: BOLD_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
@ -1,27 +1,26 @@
|
|||||||
import { marked } from "..";
|
import { marked } from "..";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
import Link from "./Link";
|
import Link from "./Link";
|
||||||
import PlainText from "./PlainText";
|
import PlainText from "./PlainText";
|
||||||
|
|
||||||
export const BOLD_EMPHASIS_REG = /\*\*\*(.+?)\*\*\*/;
|
export const BOLD_EMPHASIS_REG = /\*\*\*(.+?)\*\*\*/;
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
const renderer = (rawStr: string) => {
|
||||||
const matchResult = rawStr.match(BOLD_EMPHASIS_REG);
|
const matchResult = matcher(rawStr, BOLD_EMPHASIS_REG);
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
if (!matchResult) {
|
||||||
return rawStr;
|
return rawStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedContent = marked(matchResult[1], [], [Link, PlainText]);
|
const parsedContent = marked(matchResult[1], [], [Link, PlainText]);
|
||||||
return `<strong><em>${parsedContent}</em></strong>`;
|
return (
|
||||||
|
<strong>
|
||||||
|
<em>${parsedContent}</em>
|
||||||
|
</strong>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "bold emphasis",
|
name: "bold emphasis",
|
||||||
regex: BOLD_EMPHASIS_REG,
|
regexp: BOLD_EMPHASIS_REG,
|
||||||
matcher,
|
|
||||||
renderer,
|
renderer,
|
||||||
};
|
};
|
@ -1,17 +0,0 @@
|
|||||||
export const BR_REG = /^(\n+)/;
|
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(BR_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
return rawStr.replaceAll("\n", "<br>");
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "br",
|
|
||||||
regex: BR_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
16
web/src/labs/marked/parser/Br.tsx
Normal file
16
web/src/labs/marked/parser/Br.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export const BR_REG = /^(\n+)/;
|
||||||
|
|
||||||
|
const renderer = (rawStr: string) => {
|
||||||
|
const length = rawStr.split("\n").length - 1;
|
||||||
|
const brList = [];
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
brList.push(<br />);
|
||||||
|
}
|
||||||
|
return <>{...brList}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "br",
|
||||||
|
regexp: BR_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
@ -1,37 +0,0 @@
|
|||||||
import { escape } from "lodash-es";
|
|
||||||
import hljs from "highlight.js";
|
|
||||||
|
|
||||||
export const CODE_BLOCK_REG = /^```(\S*?)\s([\s\S]*?)```/;
|
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(CODE_BLOCK_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
|
||||||
return rawStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const language = escape(matchResult[1]) || "plaintext";
|
|
||||||
let highlightedCode = hljs.highlightAuto(matchResult[2]).value;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const temp = hljs.highlight(matchResult[2], {
|
|
||||||
language,
|
|
||||||
}).value;
|
|
||||||
highlightedCode = temp;
|
|
||||||
} catch (error) {
|
|
||||||
// do nth
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<pre><button class="codeblock-copy-btn">copy</button><code class="language-${language}">${highlightedCode}</code></pre>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "code block",
|
|
||||||
regex: CODE_BLOCK_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
51
web/src/labs/marked/parser/CodeBlock.tsx
Normal file
51
web/src/labs/marked/parser/CodeBlock.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { escape } from "lodash-es";
|
||||||
|
import hljs from "highlight.js";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
import toastHelper from "../../../components/Toast";
|
||||||
|
|
||||||
|
export const CODE_BLOCK_REG = /^```(\S*?)\s([\s\S]*?)```/;
|
||||||
|
|
||||||
|
const renderer = (rawStr: string) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const matchResult = matcher(rawStr, CODE_BLOCK_REG);
|
||||||
|
if (!matchResult) {
|
||||||
|
return <>{rawStr}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = escape(matchResult[1]) || "plaintext";
|
||||||
|
let highlightedCode = hljs.highlightAuto(matchResult[2]).value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const temp = hljs.highlight(matchResult[2], {
|
||||||
|
language,
|
||||||
|
}).value;
|
||||||
|
highlightedCode = temp;
|
||||||
|
} catch (error) {
|
||||||
|
// do nth
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCopyButtonClick = () => {
|
||||||
|
copy(matchResult[2]);
|
||||||
|
toastHelper.success(t("message.succeed-copy-code"));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre>
|
||||||
|
<button
|
||||||
|
className="text-xs font-mono italic absolute top-0 right-0 px-2 leading-6 border btn-text rounded opacity-60"
|
||||||
|
onClick={handleCopyButtonClick}
|
||||||
|
>
|
||||||
|
copy
|
||||||
|
</button>
|
||||||
|
<code className={`language-${language}`}>{highlightedCode}</code>
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "code block",
|
||||||
|
regexp: CODE_BLOCK_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
@ -1,26 +0,0 @@
|
|||||||
import { inlineElementParserList } from ".";
|
|
||||||
import { marked } from "..";
|
|
||||||
|
|
||||||
export const DONE_LIST_REG = /^- \[[xX]\] ([^\n]+)/;
|
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(DONE_LIST_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
|
||||||
return rawStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
|
||||||
return `<p class='li-container'><span class='todo-block done' data-value='DONE'>✓</span><span>${parsedContent}</span></p>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "done list",
|
|
||||||
regex: DONE_LIST_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
28
web/src/labs/marked/parser/DoneList.tsx
Normal file
28
web/src/labs/marked/parser/DoneList.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { inlineElementParserList } from ".";
|
||||||
|
import { marked } from "..";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
|
||||||
|
export const DONE_LIST_REG = /^- \[[xX]\] ([^\n]+)/;
|
||||||
|
|
||||||
|
const renderer = (rawStr: string) => {
|
||||||
|
const matchResult = matcher(rawStr, DONE_LIST_REG);
|
||||||
|
if (!matchResult) {
|
||||||
|
return rawStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
||||||
|
return (
|
||||||
|
<p className="li-container">
|
||||||
|
<span className="todo-block done" data-value="DONE">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
<span>{parsedContent}</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "done list",
|
||||||
|
regexp: DONE_LIST_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
@ -1,27 +1,22 @@
|
|||||||
import { marked } from "..";
|
import { marked } from "..";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
import Link from "./Link";
|
import Link from "./Link";
|
||||||
import PlainText from "./PlainText";
|
import PlainText from "./PlainText";
|
||||||
|
|
||||||
export const EMPHASIS_REG = /\*(.+?)\*/;
|
export const EMPHASIS_REG = /\*(.+?)\*/;
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
const renderer = (rawStr: string) => {
|
||||||
const matchResult = rawStr.match(EMPHASIS_REG);
|
const matchResult = matcher(rawStr, EMPHASIS_REG);
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
if (!matchResult) {
|
||||||
return rawStr;
|
return rawStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedContent = marked(matchResult[1], [], [Link, PlainText]);
|
const parsedContent = marked(matchResult[1], [], [Link, PlainText]);
|
||||||
return `<em>${parsedContent}</em>`;
|
return <em>{parsedContent}</em>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "emphasis",
|
name: "emphasis",
|
||||||
regex: EMPHASIS_REG,
|
regexp: EMPHASIS_REG,
|
||||||
matcher,
|
|
||||||
renderer,
|
renderer,
|
||||||
};
|
};
|
@ -1,25 +0,0 @@
|
|||||||
import { escape } from "lodash";
|
|
||||||
|
|
||||||
export const HEADING_REG = /^(#+) ([^\n]+)/;
|
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(HEADING_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
|
||||||
return rawStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const level = matchResult[1].length;
|
|
||||||
return `<h${level}>${escape(matchResult[2])}</h${level}>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "heading",
|
|
||||||
regex: HEADING_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
29
web/src/labs/marked/parser/Heading.tsx
Normal file
29
web/src/labs/marked/parser/Heading.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { escape } from "lodash";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
|
||||||
|
export const HEADING_REG = /^(#+) ([^\n]+)/;
|
||||||
|
|
||||||
|
const renderer = (rawStr: string) => {
|
||||||
|
const matchResult = matcher(rawStr, HEADING_REG);
|
||||||
|
if (!matchResult) {
|
||||||
|
return rawStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const level = matchResult[1].length;
|
||||||
|
if (level === 1) {
|
||||||
|
return <h1>{escape(matchResult[2])}</h1>;
|
||||||
|
} else if (level === 2) {
|
||||||
|
return <h2>{escape(matchResult[2])}</h2>;
|
||||||
|
} else if (level === 3) {
|
||||||
|
return <h3>{escape(matchResult[2])}</h3>;
|
||||||
|
} else if (level === 4) {
|
||||||
|
return <h4>{escape(matchResult[2])}</h4>;
|
||||||
|
}
|
||||||
|
return <h5>{escape(matchResult[2])}</h5>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "heading",
|
||||||
|
regexp: HEADING_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
@ -1,18 +0,0 @@
|
|||||||
export const HORIZONTAL_RULES_REG = /^_{3}|^-{3}|^\*{3}/;
|
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(HORIZONTAL_RULES_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
export const renderer = (rawStr: string): string => {
|
|
||||||
return `<hr>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "horizontal rules",
|
|
||||||
regex: HORIZONTAL_RULES_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
12
web/src/labs/marked/parser/HorizontalRules.tsx
Normal file
12
web/src/labs/marked/parser/HorizontalRules.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export const HORIZONTAL_RULES_REG = /^_{3}|^-{3}|^\*{3}/;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
export const renderer = (rawStr: string) => {
|
||||||
|
return <hr />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "horizontal rules",
|
||||||
|
regexp: HORIZONTAL_RULES_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
@ -1,26 +1,21 @@
|
|||||||
import { escape } from "lodash-es";
|
import { escape } from "lodash-es";
|
||||||
import { absolutifyLink } from "../../../helpers/utils";
|
import { absolutifyLink } from "../../../helpers/utils";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
|
||||||
export const IMAGE_REG = /!\[.*?\]\((.+?)\)/;
|
export const IMAGE_REG = /!\[.*?\]\((.+?)\)/;
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
const renderer = (rawStr: string) => {
|
||||||
const matchResult = rawStr.match(IMAGE_REG);
|
const matchResult = matcher(rawStr, IMAGE_REG);
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
if (!matchResult) {
|
||||||
return rawStr;
|
return rawStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageUrl = absolutifyLink(escape(matchResult[1]));
|
const imageUrl = absolutifyLink(escape(matchResult[1]));
|
||||||
return `<img class='img' src='${imageUrl}' />`;
|
return <img className="img" src={imageUrl} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "image",
|
name: "image",
|
||||||
regex: IMAGE_REG,
|
regexp: IMAGE_REG,
|
||||||
matcher,
|
|
||||||
renderer,
|
renderer,
|
||||||
};
|
};
|
@ -1,24 +0,0 @@
|
|||||||
import { escape } from "lodash-es";
|
|
||||||
|
|
||||||
export const INLINE_CODE_REG = /`(.+?)`/;
|
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(INLINE_CODE_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
|
||||||
return rawStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<code>${escape(matchResult[1])}</code>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "inline code",
|
|
||||||
regex: INLINE_CODE_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
19
web/src/labs/marked/parser/InlineCode.tsx
Normal file
19
web/src/labs/marked/parser/InlineCode.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { escape } from "lodash-es";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
|
||||||
|
export const INLINE_CODE_REG = /`(.+?)`/;
|
||||||
|
|
||||||
|
const renderer = (rawStr: string) => {
|
||||||
|
const matchResult = matcher(rawStr, INLINE_CODE_REG);
|
||||||
|
if (!matchResult) {
|
||||||
|
return rawStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <code>{escape(matchResult[1])}</code>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "inline code",
|
||||||
|
regexp: INLINE_CODE_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
@ -5,26 +5,25 @@ import { marked } from "..";
|
|||||||
import InlineCode from "./InlineCode";
|
import InlineCode from "./InlineCode";
|
||||||
import BoldEmphasis from "./BoldEmphasis";
|
import BoldEmphasis from "./BoldEmphasis";
|
||||||
import PlainText from "./PlainText";
|
import PlainText from "./PlainText";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
|
||||||
export const LINK_REG = /\[(.*?)\]\((.+?)\)+/;
|
export const LINK_REG = /\[(.*?)\]\((.+?)\)+/;
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
const renderer = (rawStr: string) => {
|
||||||
const matchResult = rawStr.match(LINK_REG);
|
const matchResult = matcher(rawStr, LINK_REG);
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
if (!matchResult) {
|
||||||
return rawStr;
|
return rawStr;
|
||||||
}
|
}
|
||||||
const parsedContent = marked(matchResult[1], [], [InlineCode, BoldEmphasis, Emphasis, Bold, PlainText]);
|
const parsedContent = marked(matchResult[1], [], [InlineCode, BoldEmphasis, Emphasis, Bold, PlainText]);
|
||||||
return `<a class='link' target='_blank' rel='noreferrer' href='${escape(matchResult[2])}'>${parsedContent}</a>`;
|
return (
|
||||||
|
<a className="link" target="_blank" rel="noreferrer" href={escape(matchResult[2])}>
|
||||||
|
{parsedContent}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "link",
|
name: "link",
|
||||||
regex: LINK_REG,
|
regexp: LINK_REG,
|
||||||
matcher,
|
|
||||||
renderer,
|
renderer,
|
||||||
};
|
};
|
@ -1,26 +0,0 @@
|
|||||||
import { inlineElementParserList } from ".";
|
|
||||||
import { marked } from "..";
|
|
||||||
|
|
||||||
export const ORDERED_LIST_REG = /^(\d+)\. (.+)/;
|
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(ORDERED_LIST_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
|
||||||
return rawStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedContent = marked(matchResult[2], [], inlineElementParserList);
|
|
||||||
return `<p class='li-container'><span class='ol-block'>${matchResult[1]}.</span><span>${parsedContent}</span></p>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "ordered list",
|
|
||||||
regex: ORDERED_LIST_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
26
web/src/labs/marked/parser/OrderedList.tsx
Normal file
26
web/src/labs/marked/parser/OrderedList.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { inlineElementParserList } from ".";
|
||||||
|
import { marked } from "..";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
|
||||||
|
export const ORDERED_LIST_REG = /^(\d+)\. (.+)/;
|
||||||
|
|
||||||
|
const renderer = (rawStr: string) => {
|
||||||
|
const matchResult = matcher(rawStr, ORDERED_LIST_REG);
|
||||||
|
if (!matchResult) {
|
||||||
|
return rawStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedContent = marked(matchResult[2], [], inlineElementParserList);
|
||||||
|
return (
|
||||||
|
<p className="li-container">
|
||||||
|
<span className="ol-block">{matchResult[1]}.</span>
|
||||||
|
<span>{parsedContent}</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ordered list",
|
||||||
|
regexp: ORDERED_LIST_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
@ -3,19 +3,13 @@ import { marked } from "..";
|
|||||||
|
|
||||||
export const PARAGRAPH_REG = /^([^\n]+)/;
|
export const PARAGRAPH_REG = /^([^\n]+)/;
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
const renderer = (rawStr: string) => {
|
||||||
const matchResult = rawStr.match(PARAGRAPH_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const parsedContent = marked(rawStr, [], inlineElementParserList);
|
const parsedContent = marked(rawStr, [], inlineElementParserList);
|
||||||
return `<p>${parsedContent}</p>`;
|
return <p>{parsedContent}</p>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "paragraph",
|
name: "paragraph",
|
||||||
regex: PARAGRAPH_REG,
|
regexp: PARAGRAPH_REG,
|
||||||
matcher,
|
|
||||||
renderer,
|
renderer,
|
||||||
};
|
};
|
@ -1,24 +0,0 @@
|
|||||||
import { escape } from "lodash-es";
|
|
||||||
|
|
||||||
export const PLAIN_LINK_REG = /(https?:\/\/[^ ]+)/;
|
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(PLAIN_LINK_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
|
||||||
return rawStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<a class='link' target='_blank' rel='noreferrer' href='${escape(matchResult[1])}'>${escape(matchResult[1])}</a>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "plain link",
|
|
||||||
regex: PLAIN_LINK_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
23
web/src/labs/marked/parser/PlainLink.tsx
Normal file
23
web/src/labs/marked/parser/PlainLink.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { escape } from "lodash-es";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
|
||||||
|
export const PLAIN_LINK_REG = /(https?:\/\/[^ ]+)/;
|
||||||
|
|
||||||
|
const renderer = (rawStr: string) => {
|
||||||
|
const matchResult = matcher(rawStr, PLAIN_LINK_REG);
|
||||||
|
if (!matchResult) {
|
||||||
|
return rawStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a className="link" target="_blank" rel="noreferrer" href={escape(matchResult[1])}>
|
||||||
|
{escape(matchResult[1])}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "plain link",
|
||||||
|
regexp: PLAIN_LINK_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
@ -1,14 +1,10 @@
|
|||||||
import { escape } from "lodash-es";
|
import { escape } from "lodash-es";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
|
||||||
export const PLAIN_TEXT_REG = /(.+)/;
|
export const PLAIN_TEXT_REG = /(.+)/;
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(PLAIN_TEXT_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
const renderer = (rawStr: string): string => {
|
||||||
const matchResult = matcher(rawStr);
|
const matchResult = matcher(rawStr, PLAIN_TEXT_REG);
|
||||||
if (!matchResult) {
|
if (!matchResult) {
|
||||||
return rawStr;
|
return rawStr;
|
||||||
}
|
}
|
||||||
@ -18,7 +14,6 @@ const renderer = (rawStr: string): string => {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "plain text",
|
name: "plain text",
|
||||||
regex: PLAIN_TEXT_REG,
|
regexp: PLAIN_TEXT_REG,
|
||||||
matcher,
|
|
||||||
renderer,
|
renderer,
|
||||||
};
|
};
|
@ -1,24 +0,0 @@
|
|||||||
import { escape } from "lodash";
|
|
||||||
|
|
||||||
export const STRIKETHROUGH_REG = /~~(.+?)~~/;
|
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(STRIKETHROUGH_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
|
||||||
return rawStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<del>${escape(matchResult[1])}</del>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "Strikethrough",
|
|
||||||
regex: STRIKETHROUGH_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
19
web/src/labs/marked/parser/Strikethrough.tsx
Normal file
19
web/src/labs/marked/parser/Strikethrough.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { escape } from "lodash";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
|
||||||
|
export const STRIKETHROUGH_REG = /~~(.+?)~~/;
|
||||||
|
|
||||||
|
const renderer = (rawStr: string) => {
|
||||||
|
const matchResult = matcher(rawStr, STRIKETHROUGH_REG);
|
||||||
|
if (!matchResult) {
|
||||||
|
return rawStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <del>{escape(matchResult[1])}</del>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Strikethrough",
|
||||||
|
regexp: STRIKETHROUGH_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
@ -1,27 +0,0 @@
|
|||||||
import { escape } from "lodash-es";
|
|
||||||
|
|
||||||
export const TAG_REG = /#([^\s#]+)/;
|
|
||||||
|
|
||||||
export const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(TAG_REG);
|
|
||||||
if (matchResult) {
|
|
||||||
return matchResult;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
|
||||||
return rawStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<span class='tag-span'>#${escape(matchResult[1])}</span>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "tag",
|
|
||||||
regex: TAG_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
19
web/src/labs/marked/parser/Tag.tsx
Normal file
19
web/src/labs/marked/parser/Tag.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { escape } from "lodash-es";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
|
||||||
|
export const TAG_REG = /#([^\s#]+)/;
|
||||||
|
|
||||||
|
const renderer = (rawStr: string) => {
|
||||||
|
const matchResult = matcher(rawStr, TAG_REG);
|
||||||
|
if (!matchResult) {
|
||||||
|
return rawStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span className="tag-span">#{escape(matchResult[1])}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "tag",
|
||||||
|
regexp: TAG_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
@ -1,26 +0,0 @@
|
|||||||
import { inlineElementParserList } from ".";
|
|
||||||
import { marked } from "..";
|
|
||||||
|
|
||||||
export const TODO_LIST_REG = /^- \[ \] ([^\n]+)/;
|
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(TODO_LIST_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
|
||||||
return rawStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
|
||||||
return `<p class='li-container'><span class='todo-block todo' data-value='TODO'></span><span>${parsedContent}</span></p>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "todo list",
|
|
||||||
regex: TODO_LIST_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
26
web/src/labs/marked/parser/TodoList.tsx
Normal file
26
web/src/labs/marked/parser/TodoList.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { inlineElementParserList } from ".";
|
||||||
|
import { marked } from "..";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
|
||||||
|
export const TODO_LIST_REG = /^- \[ \] ([^\n]+)/;
|
||||||
|
|
||||||
|
const renderer = (rawStr: string) => {
|
||||||
|
const matchResult = matcher(rawStr, TODO_LIST_REG);
|
||||||
|
if (!matchResult) {
|
||||||
|
return rawStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
||||||
|
return (
|
||||||
|
<p className="li-container">
|
||||||
|
<span className="todo-block todo" data-value="TODO"></span>
|
||||||
|
<span>{parsedContent}</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "todo list",
|
||||||
|
regexp: TODO_LIST_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
@ -1,26 +0,0 @@
|
|||||||
import { inlineElementParserList } from ".";
|
|
||||||
import { marked } from "..";
|
|
||||||
|
|
||||||
export const UNORDERED_LIST_REG = /^[*-] ([^\n]+)/;
|
|
||||||
|
|
||||||
const matcher = (rawStr: string) => {
|
|
||||||
const matchResult = rawStr.match(UNORDERED_LIST_REG);
|
|
||||||
return matchResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = matcher(rawStr);
|
|
||||||
if (!matchResult) {
|
|
||||||
return rawStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
|
||||||
return `<p class='li-container'><span class='ul-block'>•</span><span>${parsedContent}</span></p>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "unordered list",
|
|
||||||
regex: UNORDERED_LIST_REG,
|
|
||||||
matcher,
|
|
||||||
renderer,
|
|
||||||
};
|
|
26
web/src/labs/marked/parser/UnorderedList.tsx
Normal file
26
web/src/labs/marked/parser/UnorderedList.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { inlineElementParserList } from ".";
|
||||||
|
import { marked } from "..";
|
||||||
|
import { matcher } from "../matcher";
|
||||||
|
|
||||||
|
export const UNORDERED_LIST_REG = /^[*-] ([^\n]+)/;
|
||||||
|
|
||||||
|
const renderer = (rawStr: string) => {
|
||||||
|
const matchResult = matcher(rawStr, UNORDERED_LIST_REG);
|
||||||
|
if (!matchResult) {
|
||||||
|
return rawStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
||||||
|
return (
|
||||||
|
<p className="li-container">
|
||||||
|
<span className="ul-block">•</span>
|
||||||
|
<span>${parsedContent}</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "unordered list",
|
||||||
|
regexp: UNORDERED_LIST_REG,
|
||||||
|
renderer,
|
||||||
|
};
|
Reference in New Issue
Block a user