mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: simple markdown parser (#252)
* feat: simple markdown parser * chore: rename test file name * feat: add plain text link parser * chore: update style
This commit is contained in:
@ -6,7 +6,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import "dayjs/locale/zh";
|
||||
import { UNKNOWN_ID } from "../helpers/consts";
|
||||
import { DONE_BLOCK_REG, TODO_BLOCK_REG } from "../helpers/marked";
|
||||
import { editorStateService, locationService, memoService, userService } from "../services";
|
||||
import Icon from "./Icon";
|
||||
import toastHelper from "./Toast";
|
||||
@ -134,7 +133,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
for (const element of todoElementList) {
|
||||
if (element === targetEl) {
|
||||
const index = indexOf(todoElementList, element);
|
||||
const tempList = memo.content.split(status === "DONE" ? DONE_BLOCK_REG : TODO_BLOCK_REG);
|
||||
const tempList = memo.content.split(status === "DONE" ? /- \[x\] / : /- \[ \] /);
|
||||
let finalContent = "";
|
||||
|
||||
for (let i = 0; i < tempList.length; i++) {
|
||||
|
@ -4,7 +4,9 @@ import { editorStateService, memoService, userService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import { UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { formatMemoContent, MEMO_LINK_REG, parseHtmlToRawText } from "../helpers/marked";
|
||||
import { parseHTMLToRawText } from "../helpers/utils";
|
||||
import { marked } from "../labs/marked";
|
||||
import { MARK_REG } from "../labs/marked/parser";
|
||||
import toastHelper from "./Toast";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import Icon from "./Icon";
|
||||
@ -43,7 +45,7 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
const fetchLinkedMemos = async () => {
|
||||
try {
|
||||
const linkMemos: LinkedMemo[] = [];
|
||||
const matchedArr = [...memo.content.matchAll(MEMO_LINK_REG)];
|
||||
const matchedArr = [...memo.content.matchAll(MARK_REG)];
|
||||
for (const matchRes of matchedArr) {
|
||||
if (matchRes && matchRes.length === 3) {
|
||||
const id = Number(matchRes[2]);
|
||||
@ -208,7 +210,7 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
<div className="linked-memos-wrapper">
|
||||
<p className="normal-text">{linkMemos.length} related MEMO</p>
|
||||
{linkMemos.map((memo, index) => {
|
||||
const rawtext = parseHtmlToRawText(formatMemoContent(memo.content)).replaceAll("\n", " ");
|
||||
const rawtext = parseHTMLToRawText(marked(memo.content)).replaceAll("\n", " ");
|
||||
return (
|
||||
<div className="linked-memo-container" key={`${index}-${memo.id}`} onClick={() => handleLinkedMemoClick(memo)}>
|
||||
<span className="time-text">{memo.dateStr} </span>
|
||||
@ -222,7 +224,7 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
<div className="linked-memos-wrapper">
|
||||
<p className="normal-text">{linkedMemos.length} linked MEMO</p>
|
||||
{linkedMemos.map((memo, index) => {
|
||||
const rawtext = parseHtmlToRawText(formatMemoContent(memo.content)).replaceAll("\n", " ");
|
||||
const rawtext = parseHTMLToRawText(marked(memo.content)).replaceAll("\n", " ");
|
||||
return (
|
||||
<div className="linked-memo-container" key={`${index}-${memo.id}`} onClick={() => handleLinkedMemoClick(memo)}>
|
||||
<span className="time-text">{memo.dateStr} </span>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { formatMemoContent } from "../helpers/marked";
|
||||
import { marked } from "../labs/marked";
|
||||
import Icon from "./Icon";
|
||||
import "../less/memo-content.less";
|
||||
|
||||
@ -79,7 +79,7 @@ const MemoContent: React.FC<Props> = (props: Props) => {
|
||||
className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`}
|
||||
onClick={handleMemoContentClick}
|
||||
onDoubleClick={handleMemoContentDoubleClick}
|
||||
dangerouslySetInnerHTML={{ __html: formatMemoContent(content) }}
|
||||
dangerouslySetInnerHTML={{ __html: marked(content) }}
|
||||
></div>
|
||||
{state.expandButtonStatus !== -1 && (
|
||||
<div className="expand-btn-container">
|
||||
|
@ -2,7 +2,7 @@ import { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { memoService, shortcutService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import { IMAGE_URL_REG, LINK_URL_REG, MEMO_LINK_REG, TAG_REG } from "../helpers/marked";
|
||||
import { TAG_REG, LINK_REG } from "../labs/marked/parser";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { checkShouldShowMemoWithFilters } from "../helpers/filter";
|
||||
import toastHelper from "./Toast";
|
||||
@ -57,11 +57,7 @@ const MemoList = () => {
|
||||
if (memoType) {
|
||||
if (memoType === "NOT_TAGGED" && memo.content.match(TAG_REG) !== null) {
|
||||
shouldShow = false;
|
||||
} else if (memoType === "LINKED" && memo.content.match(LINK_URL_REG) === null) {
|
||||
shouldShow = false;
|
||||
} else if (memoType === "IMAGED" && memo.content.match(IMAGE_URL_REG) === null) {
|
||||
shouldShow = false;
|
||||
} else if (memoType === "CONNECTED" && memo.content.match(MEMO_LINK_REG) === null) {
|
||||
} else if (memoType === "LINKED" && memo.content.match(LINK_REG) === null) {
|
||||
shouldShow = false;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IMAGE_URL_REG, LINK_URL_REG, MEMO_LINK_REG, TAG_REG } from "./marked";
|
||||
import { TAG_REG, LINK_REG } from "../labs/marked/parser";
|
||||
|
||||
export const relationConsts = [
|
||||
{ text: "And", value: "AND" },
|
||||
@ -34,10 +34,6 @@ export const filterConsts = {
|
||||
},
|
||||
],
|
||||
values: [
|
||||
{
|
||||
text: "Connected",
|
||||
value: "CONNECTED",
|
||||
},
|
||||
{
|
||||
text: "No tags",
|
||||
value: "NOT_TAGGED",
|
||||
@ -46,10 +42,6 @@ export const filterConsts = {
|
||||
text: "Has links",
|
||||
value: "LINKED",
|
||||
},
|
||||
{
|
||||
text: "Has images",
|
||||
value: "IMAGED",
|
||||
},
|
||||
],
|
||||
},
|
||||
TEXT: {
|
||||
@ -142,11 +134,7 @@ export const checkShouldShowMemo = (memo: Memo, filter: Filter) => {
|
||||
let matched = false;
|
||||
if (value === "NOT_TAGGED" && memo.content.match(TAG_REG) === null) {
|
||||
matched = true;
|
||||
} else if (value === "LINKED" && memo.content.match(LINK_URL_REG) !== null) {
|
||||
matched = true;
|
||||
} else if (value === "IMAGED" && memo.content.match(IMAGE_URL_REG) !== null) {
|
||||
matched = true;
|
||||
} else if (value === "CONNECTED" && memo.content.match(MEMO_LINK_REG) !== null) {
|
||||
} else if (value === "LINKED" && memo.content.match(LINK_REG) !== null) {
|
||||
matched = true;
|
||||
}
|
||||
if (operator === "IS_NOT") {
|
||||
|
@ -1,50 +0,0 @@
|
||||
import { escape } from "lodash-es";
|
||||
|
||||
const CODE_BLOCK_REG = /```([\s\S]*?)```\n?/g;
|
||||
const BOLD_TEXT_REG = /\*\*(.+?)\*\*/g;
|
||||
const EM_TEXT_REG = /\*(.+?)\*/g;
|
||||
const DOT_LI_REG = /[*-] /g;
|
||||
const NUM_LI_REG = /(\d+)\. /g;
|
||||
export const TODO_BLOCK_REG = /- \[ \] /g;
|
||||
export const DONE_BLOCK_REG = /- \[x\] /g;
|
||||
// tag regex
|
||||
export const TAG_REG = /#([^\s#]+?) /g;
|
||||
// markdown image regex
|
||||
export const IMAGE_URL_REG = /!\[.*?\]\((.+?)\)\n?/g;
|
||||
// markdown link regex
|
||||
export const LINK_URL_REG = /\[(.*?)\]\((.+?)\)/g;
|
||||
// linked memo regex
|
||||
export const MEMO_LINK_REG = /@\[(.+?)\]\((.+?)\)/g;
|
||||
|
||||
const parseMarkedToHtml = (markedStr: string): string => {
|
||||
const htmlText = markedStr
|
||||
.replace(CODE_BLOCK_REG, "<pre lang=''>$1</pre>")
|
||||
.replace(TODO_BLOCK_REG, "<span class='todo-block todo' data-value='TODO'></span>")
|
||||
.replace(DONE_BLOCK_REG, "<span class='todo-block done' data-value='DONE'>✓</span>")
|
||||
.replace(DOT_LI_REG, "<span class='counter-block'>•</span>")
|
||||
.replace(NUM_LI_REG, "<span class='counter-block'>$1.</span>")
|
||||
.replace(BOLD_TEXT_REG, "<strong>$1</strong>")
|
||||
.replace(EM_TEXT_REG, "<em>$1</em>");
|
||||
return htmlText;
|
||||
};
|
||||
|
||||
const parseHtmlToRawText = (htmlStr: string): string => {
|
||||
const tempEl = document.createElement("div");
|
||||
tempEl.className = "memo-content-text";
|
||||
tempEl.innerHTML = htmlStr;
|
||||
const text = tempEl.innerText;
|
||||
return text;
|
||||
};
|
||||
|
||||
const formatMemoContent = (content: string) => {
|
||||
const tempElement = document.createElement("div");
|
||||
tempElement.innerHTML = parseMarkedToHtml(escape(content));
|
||||
|
||||
return tempElement.innerHTML
|
||||
.replace(IMAGE_URL_REG, "<img class='img' src='$1' />")
|
||||
.replace(MEMO_LINK_REG, "<span class='memo-link-text' data-value='$2'>$1</span>")
|
||||
.replace(LINK_URL_REG, "<a class='link' target='_blank' rel='noreferrer' href='$2'>$1</a>")
|
||||
.replace(TAG_REG, "<span class='tag-span'>#$1</span> ");
|
||||
};
|
||||
|
||||
export { formatMemoContent, parseHtmlToRawText };
|
@ -80,102 +80,6 @@ export function getDateTimeString(t: Date | number | string): string {
|
||||
return `${year}/${monthStr}/${dateStr} ${hoursStr}:${minsStr}:${secsStr}`;
|
||||
}
|
||||
|
||||
export function dedupe<T>(data: T[]): T[] {
|
||||
return Array.from(new Set(data));
|
||||
}
|
||||
|
||||
export function dedupeObjectWithId<T extends { id: string | number }>(data: T[]): T[] {
|
||||
const idSet = new Set<string | number>();
|
||||
const result = [];
|
||||
|
||||
for (const d of data) {
|
||||
if (!idSet.has(d.id)) {
|
||||
idSet.add(d.id);
|
||||
result.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function debounce(fn: FunctionType, delay: number) {
|
||||
let timer: number | null = null;
|
||||
|
||||
return () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(fn, delay);
|
||||
} else {
|
||||
timer = setTimeout(fn, delay);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function throttle(fn: FunctionType, delay: number) {
|
||||
let valid = true;
|
||||
|
||||
return () => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
valid = false;
|
||||
setTimeout(() => {
|
||||
fn();
|
||||
valid = true;
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
|
||||
export function filterObjectNullKeys(object: KVObject): KVObject {
|
||||
if (!object) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const finalObject: KVObject = {};
|
||||
const keys = Object.keys(object).sort();
|
||||
|
||||
for (const key of keys) {
|
||||
const val = object[key];
|
||||
if (typeof val === "object") {
|
||||
const temp = filterObjectNullKeys(JSON.parse(JSON.stringify(val)));
|
||||
if (temp && Object.keys(temp).length > 0) {
|
||||
finalObject[key] = temp;
|
||||
}
|
||||
} else {
|
||||
if (val) {
|
||||
finalObject[key] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalObject;
|
||||
}
|
||||
|
||||
export function getImageSize(src: string): Promise<{ width: number; height: number }> {
|
||||
return new Promise((resolve) => {
|
||||
const imgEl = new Image();
|
||||
|
||||
imgEl.onload = () => {
|
||||
const { width, height } = imgEl;
|
||||
|
||||
if (width > 0 && height > 0) {
|
||||
resolve({ width, height });
|
||||
} else {
|
||||
resolve({ width: 0, height: 0 });
|
||||
}
|
||||
};
|
||||
|
||||
imgEl.onerror = () => {
|
||||
resolve({ width: 0, height: 0 });
|
||||
};
|
||||
|
||||
imgEl.className = "hidden";
|
||||
imgEl.src = src;
|
||||
document.body.appendChild(imgEl);
|
||||
imgEl.remove();
|
||||
});
|
||||
}
|
||||
|
||||
export const getElementBounding = (element: HTMLElement, relativeEl?: HTMLElement) => {
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft;
|
||||
@ -224,3 +128,11 @@ export const getElementBounding = (element: HTMLElement, relativeEl?: HTMLElemen
|
||||
left: elementRect.left + scrollLeft,
|
||||
});
|
||||
};
|
||||
|
||||
export const parseHTMLToRawText = (htmlStr: string): string => {
|
||||
const tempEl = document.createElement("div");
|
||||
tempEl.className = "memo-content-text";
|
||||
tempEl.innerHTML = htmlStr;
|
||||
const text = tempEl.innerText;
|
||||
return text;
|
||||
};
|
||||
|
18
web/src/labs/marked/index.ts
Normal file
18
web/src/labs/marked/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { parserList } from "./parser";
|
||||
|
||||
export const marked = (markdownStr: string, parsers = parserList) => {
|
||||
for (const parser of parsers) {
|
||||
const startIndex = markdownStr.search(parser.regex);
|
||||
const matchedLength = parser.match(markdownStr);
|
||||
|
||||
if (startIndex > -1 && matchedLength > 0) {
|
||||
const prefixStr = markdownStr.slice(0, startIndex);
|
||||
const matchedStr = markdownStr.slice(startIndex, startIndex + matchedLength);
|
||||
const suffixStr = markdownStr.slice(startIndex + matchedLength);
|
||||
markdownStr = marked(prefixStr, parsers) + parser.renderer(matchedStr) + marked(suffixStr, parsers);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return markdownStr;
|
||||
};
|
89
web/src/labs/marked/marked.test.ts
Normal file
89
web/src/labs/marked/marked.test.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { describe, expect, test } from "@jest/globals";
|
||||
import { marked } from ".";
|
||||
|
||||
describe("test marked parser", () => {
|
||||
test("parse code block", () => {
|
||||
const tests = [
|
||||
{
|
||||
markdown: `\`\`\`
|
||||
hello world!
|
||||
\`\`\``,
|
||||
want: `<pre lang=''>
|
||||
hello world!
|
||||
</pre>`,
|
||||
},
|
||||
{
|
||||
markdown: `test code block
|
||||
|
||||
\`\`\`js
|
||||
console.log("hello world!")
|
||||
\`\`\``,
|
||||
want: `<p>test code block</p>
|
||||
<p></p>
|
||||
<pre lang='js'>
|
||||
console.log("hello world!")
|
||||
</pre>`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const t of tests) {
|
||||
expect(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><span class='todo-block todo' data-value='TODO'></span>finish my homework</p>
|
||||
<p><span class='todo-block done' data-value='DONE'>✓</span>yahaha</p>`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const t of tests) {
|
||||
expect(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><span class='ul-block'>•</span>list 123</p>
|
||||
<p><span class='ol-block'>1.</span>123123</p>`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const t of tests) {
|
||||
expect(marked(t.markdown)).toBe(t.want);
|
||||
}
|
||||
});
|
||||
test("parse inline element", () => {
|
||||
const tests = [
|
||||
{
|
||||
markdown: `Link: [baidu](https://baidu.com)`,
|
||||
want: `<p>Link: <a class='link' target='_blank' rel='noreferrer' href='https://baidu.com'>baidu</a></p>`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const t of tests) {
|
||||
expect(marked(t.markdown)).toBe(t.want);
|
||||
}
|
||||
});
|
||||
test("parse plain link", () => {
|
||||
const tests = [
|
||||
{
|
||||
markdown: `Link:https://baidu.com`,
|
||||
want: `<p>Link:<a class='link' target='_blank' rel='noreferrer' href='https://baidu.com'>https://baidu.com</a></p>`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const t of tests) {
|
||||
expect(marked(t.markdown)).toBe(t.want);
|
||||
}
|
||||
});
|
||||
});
|
23
web/src/labs/marked/parser/Bold.ts
Normal file
23
web/src/labs/marked/parser/Bold.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const BOLD_REG = /\*\*([\S ]+?)\*\*/;
|
||||
|
||||
const match = (rawStr: string): number => {
|
||||
const matchResult = rawStr.match(BOLD_REG);
|
||||
if (!matchResult) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const matchStr = matchResult[0];
|
||||
return matchStr.length;
|
||||
};
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const parsedStr = rawStr.replace(BOLD_REG, "<strong>$1</strong>");
|
||||
return parsedStr;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "bold",
|
||||
regex: BOLD_REG,
|
||||
match,
|
||||
renderer,
|
||||
};
|
23
web/src/labs/marked/parser/CodeBlock.ts
Normal file
23
web/src/labs/marked/parser/CodeBlock.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const CODE_BLOCK_REG = /^```(\S*?)\s([\s\S]*?)```(\n?)/;
|
||||
|
||||
const match = (rawStr: string): number => {
|
||||
const matchResult = rawStr.match(CODE_BLOCK_REG);
|
||||
if (!matchResult) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const matchStr = matchResult[0];
|
||||
return matchStr.length;
|
||||
};
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const parsedStr = rawStr.replace(CODE_BLOCK_REG, "<pre lang='$1'>\n$2</pre>$3");
|
||||
return parsedStr;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "code block",
|
||||
regex: CODE_BLOCK_REG,
|
||||
match,
|
||||
renderer,
|
||||
};
|
31
web/src/labs/marked/parser/DoneList.ts
Normal file
31
web/src/labs/marked/parser/DoneList.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { inlineElementParserList } from ".";
|
||||
import { marked } from "..";
|
||||
|
||||
export const DONE_LIST_REG = /^- \[x\] ([\S ]+)(\n?)/;
|
||||
|
||||
const match = (rawStr: string): number => {
|
||||
const matchResult = rawStr.match(DONE_LIST_REG);
|
||||
if (!matchResult) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const matchStr = matchResult[0];
|
||||
return matchStr.length;
|
||||
};
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const matchResult = rawStr.match(DONE_LIST_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
const parsedContent = marked(matchResult[1], inlineElementParserList);
|
||||
return `<p><span class='todo-block done' data-value='DONE'>✓</span>${parsedContent}</p>${matchResult[2]}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "done list",
|
||||
regex: DONE_LIST_REG,
|
||||
match,
|
||||
renderer,
|
||||
};
|
23
web/src/labs/marked/parser/Emphasis.ts
Normal file
23
web/src/labs/marked/parser/Emphasis.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const EMPHASIS_REG = /\*([\S ]+?)\*/;
|
||||
|
||||
const match = (rawStr: string): number => {
|
||||
const matchResult = rawStr.match(EMPHASIS_REG);
|
||||
if (!matchResult) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const matchStr = matchResult[0];
|
||||
return matchStr.length;
|
||||
};
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const parsedStr = rawStr.replace(EMPHASIS_REG, "<em>$1</em>");
|
||||
return parsedStr;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "emphasis",
|
||||
regex: EMPHASIS_REG,
|
||||
match,
|
||||
renderer,
|
||||
};
|
23
web/src/labs/marked/parser/Image.ts
Normal file
23
web/src/labs/marked/parser/Image.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const IMAGE_REG = /!\[.*?\]\((.+?)\)/;
|
||||
|
||||
const match = (rawStr: string): number => {
|
||||
const matchResult = rawStr.match(IMAGE_REG);
|
||||
if (!matchResult) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const matchStr = matchResult[0];
|
||||
return matchStr.length;
|
||||
};
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const parsedStr = rawStr.replace(IMAGE_REG, "<img class='img' src='$1' />");
|
||||
return parsedStr;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "image",
|
||||
regex: IMAGE_REG,
|
||||
match,
|
||||
renderer,
|
||||
};
|
23
web/src/labs/marked/parser/Link.ts
Normal file
23
web/src/labs/marked/parser/Link.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const LINK_REG = /\[(.*?)\]\((.+?)\)/;
|
||||
|
||||
const match = (rawStr: string): number => {
|
||||
const matchResult = rawStr.match(LINK_REG);
|
||||
if (!matchResult) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const matchStr = matchResult[0];
|
||||
return matchStr.length;
|
||||
};
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const parsedStr = rawStr.replace(LINK_REG, "<a class='link' target='_blank' rel='noreferrer' href='$2'>$1</a>");
|
||||
return parsedStr;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "link",
|
||||
regex: LINK_REG,
|
||||
match,
|
||||
renderer,
|
||||
};
|
23
web/src/labs/marked/parser/Mark.ts
Normal file
23
web/src/labs/marked/parser/Mark.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const MARK_REG = /@\[([\S ]+?)\]\((\S+?)\)/;
|
||||
|
||||
const match = (rawStr: string): number => {
|
||||
const matchResult = rawStr.match(MARK_REG);
|
||||
if (!matchResult) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const matchStr = matchResult[0];
|
||||
return matchStr.length;
|
||||
};
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const parsedStr = rawStr.replace(MARK_REG, "<span class='memo-link-text' data-value='$2'>$1</span>");
|
||||
return parsedStr;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "mark",
|
||||
regex: MARK_REG,
|
||||
match,
|
||||
renderer,
|
||||
};
|
31
web/src/labs/marked/parser/OrderedList.ts
Normal file
31
web/src/labs/marked/parser/OrderedList.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { inlineElementParserList } from ".";
|
||||
import { marked } from "..";
|
||||
|
||||
export const ORDERED_LIST_REG = /^(\d+)\. ([\S ]+)(\n?)/;
|
||||
|
||||
const match = (rawStr: string): number => {
|
||||
const matchResult = rawStr.match(ORDERED_LIST_REG);
|
||||
if (!matchResult) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const matchStr = matchResult[0];
|
||||
return matchStr.length;
|
||||
};
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const matchResult = rawStr.match(ORDERED_LIST_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
const parsedContent = marked(matchResult[2], inlineElementParserList);
|
||||
return `<p><span class='ol-block'>${matchResult[1]}.</span>${parsedContent}</p>${matchResult[3]}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "ordered list",
|
||||
regex: ORDERED_LIST_REG,
|
||||
match,
|
||||
renderer,
|
||||
};
|
31
web/src/labs/marked/parser/Paragraph.ts
Normal file
31
web/src/labs/marked/parser/Paragraph.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { inlineElementParserList } from ".";
|
||||
import { marked } from "..";
|
||||
|
||||
export const PARAGRAPH_REG = /^([\S ]*)(\n?)/;
|
||||
|
||||
const match = (rawStr: string): number => {
|
||||
const matchResult = rawStr.match(PARAGRAPH_REG);
|
||||
if (!matchResult) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const matchStr = matchResult[0];
|
||||
return matchStr.length;
|
||||
};
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const matchResult = rawStr.match(PARAGRAPH_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
const parsedContent = marked(matchResult[1], inlineElementParserList);
|
||||
return `<p>${parsedContent}</p>${matchResult[2]}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "ordered list",
|
||||
regex: PARAGRAPH_REG,
|
||||
match,
|
||||
renderer,
|
||||
};
|
23
web/src/labs/marked/parser/PlainLink.ts
Normal file
23
web/src/labs/marked/parser/PlainLink.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const PLAIN_LINK_REG = /(https?:\/\/[^ ]+)/;
|
||||
|
||||
const match = (rawStr: string): number => {
|
||||
const matchResult = rawStr.match(PLAIN_LINK_REG);
|
||||
if (!matchResult) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const matchStr = matchResult[0];
|
||||
return matchStr.length;
|
||||
};
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const parsedStr = rawStr.replace(PLAIN_LINK_REG, "<a class='link' target='_blank' rel='noreferrer' href='$1'>$1</a>");
|
||||
return parsedStr;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "plain link",
|
||||
regex: PLAIN_LINK_REG,
|
||||
match,
|
||||
renderer,
|
||||
};
|
23
web/src/labs/marked/parser/Tag.ts
Normal file
23
web/src/labs/marked/parser/Tag.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const TAG_REG = /#([^\s#]+?) /;
|
||||
|
||||
const match = (rawStr: string): number => {
|
||||
const matchResult = rawStr.match(TAG_REG);
|
||||
if (!matchResult) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const matchStr = matchResult[0];
|
||||
return matchStr.length;
|
||||
};
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const parsedStr = rawStr.replace(TAG_REG, "<span class='tag-span'>#$1</span> ");
|
||||
return parsedStr;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "tag",
|
||||
regex: TAG_REG,
|
||||
match,
|
||||
renderer,
|
||||
};
|
31
web/src/labs/marked/parser/TodoList.ts
Normal file
31
web/src/labs/marked/parser/TodoList.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { inlineElementParserList } from ".";
|
||||
import { marked } from "..";
|
||||
|
||||
export const TODO_LIST_REG = /^- \[ \] ([\S ]+)(\n?)/;
|
||||
|
||||
const match = (rawStr: string): number => {
|
||||
const matchResult = rawStr.match(TODO_LIST_REG);
|
||||
if (!matchResult) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const matchStr = matchResult[0];
|
||||
return matchStr.length;
|
||||
};
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const matchResult = rawStr.match(TODO_LIST_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
const parsedContent = marked(matchResult[1], inlineElementParserList);
|
||||
return `<p><span class='todo-block todo' data-value='TODO'></span>${parsedContent}</p>${matchResult[2]}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "todo list",
|
||||
regex: TODO_LIST_REG,
|
||||
match,
|
||||
renderer,
|
||||
};
|
31
web/src/labs/marked/parser/UnorderedList.ts
Normal file
31
web/src/labs/marked/parser/UnorderedList.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { inlineElementParserList } from ".";
|
||||
import { marked } from "..";
|
||||
|
||||
export const UNORDERED_LIST_REG = /^[*-] ([\S ]+)(\n?)/;
|
||||
|
||||
const match = (rawStr: string): number => {
|
||||
const matchResult = rawStr.match(UNORDERED_LIST_REG);
|
||||
if (!matchResult) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const matchStr = matchResult[0];
|
||||
return matchStr.length;
|
||||
};
|
||||
|
||||
const renderer = (rawStr: string): string => {
|
||||
const matchResult = rawStr.match(UNORDERED_LIST_REG);
|
||||
if (!matchResult) {
|
||||
return rawStr;
|
||||
}
|
||||
|
||||
const parsedContent = marked(matchResult[1], inlineElementParserList);
|
||||
return `<p><span class='ul-block'>•</span>${parsedContent}</p>${matchResult[2]}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "unordered list",
|
||||
regex: UNORDERED_LIST_REG,
|
||||
match,
|
||||
renderer,
|
||||
};
|
31
web/src/labs/marked/parser/index.ts
Normal file
31
web/src/labs/marked/parser/index.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import CodeBlock from "./CodeBlock";
|
||||
import TodoList from "./TodoList";
|
||||
import DoneList from "./DoneList";
|
||||
import OrderedList from "./OrderedList";
|
||||
import UnorderedList from "./UnorderedList";
|
||||
import Paragraph from "./Paragraph";
|
||||
import Tag from "./Tag";
|
||||
import Image from "./Image";
|
||||
import Link from "./Link";
|
||||
import Mark from "./Mark";
|
||||
import Bold from "./Bold";
|
||||
import Emphasis from "./Emphasis";
|
||||
import PlainLink from "./PlainLink";
|
||||
|
||||
export { CODE_BLOCK_REG } from "./CodeBlock";
|
||||
export { TODO_LIST_REG } from "./TodoList";
|
||||
export { DONE_LIST_REG } from "./DoneList";
|
||||
export { ORDERED_LIST_REG } from "./OrderedList";
|
||||
export { UNORDERED_LIST_REG } from "./UnorderedList";
|
||||
export { PARAGRAPH_REG } from "./Paragraph";
|
||||
export { TAG_REG } from "./Tag";
|
||||
export { IMAGE_REG } from "./Image";
|
||||
export { LINK_REG } from "./Link";
|
||||
export { MARK_REG } from "./Mark";
|
||||
export { BOLD_REG } from "./Bold";
|
||||
export { EMPHASIS_REG } from "./Emphasis";
|
||||
|
||||
// The order determines the order of execution.
|
||||
export const blockElementParserList = [CodeBlock, TodoList, DoneList, OrderedList, UnorderedList, Paragraph];
|
||||
export const inlineElementParserList = [Image, Mark, Link, Bold, Emphasis, Tag, PlainLink];
|
||||
export const parserList = [...blockElementParserList, ...inlineElementParserList];
|
@ -20,8 +20,8 @@
|
||||
> .memo-container {
|
||||
@apply w-full overflow-x-hidden flex flex-col justify-start items-start;
|
||||
|
||||
> .memo-content-container {
|
||||
@apply flex flex-col justify-start items-start w-full overflow-x-hidden p-0 text-base;
|
||||
.memo-content-text {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
@apply w-full flex flex-col justify-start items-start;
|
||||
|
||||
> .memo-content-text {
|
||||
@apply w-full whitespace-pre-wrap break-words text-base leading-7;
|
||||
@apply w-full break-words text-base leading-7;
|
||||
|
||||
&.expanded {
|
||||
display: -webkit-box;
|
||||
@ -14,7 +14,8 @@
|
||||
}
|
||||
|
||||
> p {
|
||||
@apply inline-block w-full h-auto mb-1 last:mb-0 text-base leading-7 whitespace-pre-wrap break-words;
|
||||
@apply w-full h-auto mb-1 last:mb-0 text-base leading-6 whitespace-pre-wrap break-words;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.img {
|
||||
@ -30,7 +31,7 @@
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply inline-block text-blue-600 cursor-pointer underline break-all hover:opacity-80;
|
||||
@apply text-blue-600 cursor-pointer underline break-all hover:opacity-80;
|
||||
}
|
||||
|
||||
.counter-block,
|
||||
|
Reference in New Issue
Block a user