From 920e394dca711cb39f9898f7c0f9ba61fa309712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Tue, 13 Dec 2022 14:32:33 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20Markdown=20=EB=B0=8F=20Html=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=97=90=EB=94=94=ED=84=B0=20=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EB=B3=80=EA=B2=BD=20-=20#312?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/common/Content/index.tsx | 4 +++- frontend/components/edit/Editor/index.tsx | 5 ++--- frontend/utils/parser.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/components/common/Content/index.tsx b/frontend/components/common/Content/index.tsx index c62932a2..f1569cee 100644 --- a/frontend/components/common/Content/index.tsx +++ b/frontend/components/common/Content/index.tsx @@ -1,3 +1,5 @@ +import { markdown2html } from '@utils/parser'; + import { ContentBody, ContentTitle, ContentWrapper } from './styled'; import 'highlight.js/styles/github.css'; @@ -13,7 +15,7 @@ export default function Content({ title, content }: ContentProps) { {title && {title}} diff --git a/frontend/components/edit/Editor/index.tsx b/frontend/components/edit/Editor/index.tsx index 90b3d8cd..99fab747 100644 --- a/frontend/components/edit/Editor/index.tsx +++ b/frontend/components/edit/Editor/index.tsx @@ -20,7 +20,6 @@ import EditBar from '@components/edit/EditBar'; import useCodeMirror from '@components/edit/Editor/core/useCodeMirror'; import useInput from '@hooks/useInput'; import { IArticle } from '@interfaces'; -import { html2markdown, markdown2html } from '@utils/parser'; import { EditorButtonWrapper, @@ -68,7 +67,7 @@ export default function Editor({ handleModalOpen, originalArticle }: EditorProps if (!buffer.title && !buffer.content) return; title.setValue(buffer.title); - replaceDocument(html2markdown(buffer.content)); + replaceDocument(buffer.content); setBuffer({ title: '', content: '' }); }, [buffer]); @@ -77,7 +76,7 @@ export default function Editor({ handleModalOpen, originalArticle }: EditorProps setArticle({ ...article, title: title.value, - content: markdown2html(document), + content: document, }); }, [title.value, document]); diff --git a/frontend/utils/parser.ts b/frontend/utils/parser.ts index 42fea91b..77c5de88 100644 --- a/frontend/utils/parser.ts +++ b/frontend/utils/parser.ts @@ -18,7 +18,7 @@ export const markdown2html = (markdown: string) => { .toString(); return unified() - .use(rehypeParse) + .use(rehypeParse, { fragment: true }) .use(rehypeHighlight, { ignoreMissing: true }) .use(rehypeStringify) .processSync(html) From afbdb0b8e3bfea394f06d544c8421067da128b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Tue, 13 Dec 2022 15:35:48 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EA=B8=80=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=EC=97=90=EC=84=9C=20=EB=A7=88=ED=81=AC?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=20=EB=AC=B8=EB=B2=95=EC=9D=B4=20=EB=B3=B4?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20#312?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/search/ArticleList/index.tsx | 7 +- frontend/package-lock.json | 79 +++++++++++++++++++ frontend/package.json | 3 + frontend/utils/parser.ts | 19 ++++- 4 files changed, 101 insertions(+), 7 deletions(-) diff --git a/frontend/components/search/ArticleList/index.tsx b/frontend/components/search/ArticleList/index.tsx index e59f4919..55779c4b 100644 --- a/frontend/components/search/ArticleList/index.tsx +++ b/frontend/components/search/ArticleList/index.tsx @@ -1,5 +1,6 @@ import ArticleItem from '@components/search/ArticleItem'; import { IArticleBook } from '@interfaces'; +import { markdown2text } from '@utils/parser'; interface ArticleListProps { articles: IArticleBook[]; @@ -24,7 +25,7 @@ export default function ArticleList({ articles, keywords }: ArticleListProps) { let paddingIndex = 0; if (isFirst) { - const regex = /(<([^>]+)>)/g; + const regex = /\n/g; while (regex.test(text.slice(0, startIndex))) paddingIndex = regex.lastIndex; } @@ -33,7 +34,7 @@ export default function ArticleList({ articles, keywords }: ArticleListProps) { <> {text.slice(paddingIndex, startIndex)} {text.slice(startIndex, endIndex)} - {highlightWord(text.slice(endIndex).replace(/(<([^>]+)>)/gi, ''), words)} + {highlightWord(text.slice(endIndex), words)} ); }; @@ -44,7 +45,7 @@ export default function ArticleList({ articles, keywords }: ArticleListProps) { { @@ -17,19 +20,27 @@ export const markdown2html = (markdown: string) => { .processSync(markdown) .toString(); - return unified() - .use(rehypeParse, { fragment: true }) + const htmlWithSyntaxHighlight = rehype() .use(rehypeHighlight, { ignoreMissing: true }) - .use(rehypeStringify) .processSync(html) .toString(); + + return htmlWithSyntaxHighlight; }; export const html2markdown = (html: string) => { - return unified() + const markdown = unified() .use(rehypeParse) .use(rehypeRemark) .use(remarkStringify) .processSync(html) .toString(); + + return markdown; +}; + +export const markdown2text = (markdown: string) => { + const text = remark().use(stripMarkdown).processSync(markdown).toString(); + + return text; }; From bc6164c1d3b8f63360a55859bcd2e59b5db86e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=ED=98=84?= Date: Tue, 13 Dec 2022 16:09:47 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20TOC=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20#312?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/viewer/TOC/index.tsx | 3 +- frontend/package-lock.json | 83 ++++++++++++++++++++++++ frontend/package.json | 1 + frontend/pages/viewer/[...data].tsx | 6 +- frontend/utils/articleConversion.ts | 39 ----------- frontend/utils/parser.ts | 2 + frontend/utils/toc.ts | 27 ++++++++ 7 files changed, 118 insertions(+), 43 deletions(-) delete mode 100644 frontend/utils/articleConversion.ts create mode 100644 frontend/utils/toc.ts diff --git a/frontend/components/viewer/TOC/index.tsx b/frontend/components/viewer/TOC/index.tsx index 8390b2dc..2d2e4f08 100644 --- a/frontend/components/viewer/TOC/index.tsx +++ b/frontend/components/viewer/TOC/index.tsx @@ -10,6 +10,7 @@ import useBookmark from '@hooks/useBookmark'; import { IBookScraps } from '@interfaces'; import { TextMedium, TextSmall } from '@styles/common'; import { FlexCenter, FlexSpaceBetween } from '@styles/layout'; +import { text2link } from '@utils/toc'; import { TocWrapper, @@ -92,7 +93,7 @@ export default function TOC({ {isArticleShown && articleToc.map((article) => ( diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 06ac8029..eec9f602 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -39,6 +39,7 @@ "rehype-highlight": "^6.0.0", "rehype-parse": "^8.0.4", "rehype-remark": "^9.1.2", + "rehype-slug": "^5.1.0", "rehype-stringify": "^9.0.3", "remark": "^14.0.2", "remark-breaks": "^3.0.2", @@ -3221,6 +3222,11 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" + }, "node_modules/glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -3420,6 +3426,18 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-heading-rank": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-2.1.0.tgz", + "integrity": "sha512-w+Rw20Q/iWp2Bcnr6uTrYU6/ftZLbHKhvc8nM26VIWpDqDMlku2iXUVTeOlsdoih/UKQhY7PHQ+vZ0Aqq8bxtQ==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-is-body-ok-link": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-2.0.0.tgz", @@ -3521,6 +3539,18 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz", + "integrity": "sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-text": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.1.tgz", @@ -5400,6 +5430,24 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-slug": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-5.1.0.tgz", + "integrity": "sha512-Gf91dJoXneiorNEnn+Phx97CO7oRMrpi+6r155tTxzGuLtm+QrI4cTwCa9e1rtePdL4i9tSO58PeSS6HWfgsiw==", + "dependencies": { + "@types/hast": "^2.0.0", + "github-slugger": "^2.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-heading-rank": "^2.0.0", + "hast-util-to-string": "^2.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-stringify": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz", @@ -8938,6 +8986,11 @@ "integrity": "sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg==", "dev": true }, + "github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" + }, "glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -9080,6 +9133,14 @@ "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-2.0.0.tgz", "integrity": "sha512-4Qf++8o5v14us4Muv3HRj+Er6wTNGA/N9uCaZMty4JWvyFKLdhULrv4KE1b65AthsSO9TXSZnjuxS8ecIyhb0w==" }, + "hast-util-heading-rank": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-2.1.0.tgz", + "integrity": "sha512-w+Rw20Q/iWp2Bcnr6uTrYU6/ftZLbHKhvc8nM26VIWpDqDMlku2iXUVTeOlsdoih/UKQhY7PHQ+vZ0Aqq8bxtQ==", + "requires": { + "@types/hast": "^2.0.0" + } + }, "hast-util-is-body-ok-link": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-2.0.0.tgz", @@ -9157,6 +9218,14 @@ "unist-util-visit": "^4.0.0" } }, + "hast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz", + "integrity": "sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==", + "requires": { + "@types/hast": "^2.0.0" + } + }, "hast-util-to-text": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-3.1.1.tgz", @@ -10347,6 +10416,20 @@ "unified": "^10.0.0" } }, + "rehype-slug": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-5.1.0.tgz", + "integrity": "sha512-Gf91dJoXneiorNEnn+Phx97CO7oRMrpi+6r155tTxzGuLtm+QrI4cTwCa9e1rtePdL4i9tSO58PeSS6HWfgsiw==", + "requires": { + "@types/hast": "^2.0.0", + "github-slugger": "^2.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-heading-rank": "^2.0.0", + "hast-util-to-string": "^2.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + } + }, "rehype-stringify": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9bdb2b85..cdcfaf54 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,6 +40,7 @@ "rehype-highlight": "^6.0.0", "rehype-parse": "^8.0.4", "rehype-remark": "^9.1.2", + "rehype-slug": "^5.1.0", "rehype-stringify": "^9.0.3", "remark": "^14.0.2", "remark-breaks": "^3.0.2", diff --git a/frontend/pages/viewer/[...data].tsx b/frontend/pages/viewer/[...data].tsx index c6a7e488..ccc6478d 100644 --- a/frontend/pages/viewer/[...data].tsx +++ b/frontend/pages/viewer/[...data].tsx @@ -13,7 +13,7 @@ import ViewerHead from '@components/viewer/ViewerHead'; import useFetch from '@hooks/useFetch'; import { IArticleBook, IBookScraps } from '@interfaces'; import { Flex, PageGNBHide, PageNoScrollWrapper } from '@styles/layout'; -import { articleToc, articleConversion } from '@utils/articleConversion'; +import { parseHeadings } from '@utils/toc'; interface ViewerProps { article: IArticleBook; @@ -81,7 +81,7 @@ export default function Viewer({ article }: ViewerProps) { diff --git a/frontend/utils/articleConversion.ts b/frontend/utils/articleConversion.ts deleted file mode 100644 index 94e01841..00000000 --- a/frontend/utils/articleConversion.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { html2markdown } from './parser'; - -export const articleToc = (content: string) => { - // 게시물 본문을 줄바꿈 기준으로 나누고, 제목 요소인 것만 저장 - const titles = html2markdown(content) - .split(`\n`) - .filter((t) => t.includes('# ')); - - // 예외처리 - 제목은 문자열 시작부터 #을 써야함 - const result = titles - .filter((str) => str[0] === '#') - .map((item) => { - // #의 갯수에 따라 제목의 크기가 달라지므로 갯수를 센다. - let count = item.match(/#/g)?.length; - if (count) { - // 갯수에 따라 목차에 그릴때 들여쓰기 하기위해 *10을 함. - count *= 10; - } - - // 제목의 내용물만 꺼내기 위해 '# '을 기준으로 나누고, 백틱과 공백을 없애주고 count와 묶어서 리턴 - return { title: item.split('# ')[1].replace(/`/g, '').trim(), count }; - }); - - return result; -}; - -export const articleConversion = (content: string) => { - const newArticle = content.split('\n').map((v, idx) => { - if (v.includes('h1') || v.includes('h2') || v.includes('h3')) { - const title = v.replace(/<[^>]*>?/g, ''); - const result = v.split(''); - result.splice(3, 0, ' ', `id=${title}`); - return result.join(''); - } - return v; - }); - - return newArticle.join('\n'); -}; diff --git a/frontend/utils/parser.ts b/frontend/utils/parser.ts index a572759b..6803d3a0 100644 --- a/frontend/utils/parser.ts +++ b/frontend/utils/parser.ts @@ -2,6 +2,7 @@ import { rehype } from 'rehype'; import rehypeHighlight from 'rehype-highlight'; import rehypeParse from 'rehype-parse'; import rehypeRemark from 'rehype-remark'; +import rehypeSlug from 'rehype-slug'; import rehypeStringify from 'rehype-stringify'; import { remark } from 'remark'; import remarkBreaks from 'remark-breaks'; @@ -21,6 +22,7 @@ export const markdown2html = (markdown: string) => { .toString(); const htmlWithSyntaxHighlight = rehype() + .use(rehypeSlug) .use(rehypeHighlight, { ignoreMissing: true }) .processSync(html) .toString(); diff --git a/frontend/utils/toc.ts b/frontend/utils/toc.ts new file mode 100644 index 00000000..505550f8 --- /dev/null +++ b/frontend/utils/toc.ts @@ -0,0 +1,27 @@ +export const parseHeadings = (content: string) => { + // 게시물 본문을 줄바꿈 기준으로 나누고, 제목 요소인 것만 저장 + const headings = content.split('\n').filter((line) => line.includes('# ')); + + // 예외처리 - 제목은 문자열 시작부터 #을 써야함 + const parsedHeadings = headings + .filter((heading) => heading.startsWith('#')) + .map((heading) => { + // #의 갯수에 따라 제목의 크기가 달라지므로 갯수를 센다. + let count = heading.match(/#/g)?.length; + + // 갯수에 따라 목차에 그릴때 들여쓰기 하기위해 *10을 함. + if (count) count *= 16; + + // 제목의 내용물만 꺼내기 위해 '# '을 기준으로 나누고, 백틱과 공백을 없애주고 count와 묶어서 리턴 + return { + title: heading.split('# ')[1].trim(), + count, + }; + }); + + return parsedHeadings; +}; + +export const text2link = (text: string) => { + return `#${text.replace(/ /g, '-').replace(/[^\uAC00-\uD7A30-9a-zA-Z_-]/g, '')}`; +};