mirror of https://github.com/usememos/memos
feat: simple markdown parser (#252)
* feat: simple markdown parser * chore: rename test file name * feat: add plain text link parser * chore: update stylepull/253/head
parent
8e63b8f289
commit
51fb8ddb07
@ -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 };
|
@ -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;
|
||||
};
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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];
|
Loading…
Reference in New Issue