feat: simple markdown parser (#252)

* feat: simple markdown parser

* chore: rename test file name

* feat: add plain text link parser

* chore: update style
pull/253/head
boojack 3 years ago committed by GitHub
parent 8e63b8f289
commit 51fb8ddb07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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;
};

@ -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];

@ -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,

Loading…
Cancel
Save