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/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) {
(
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 08e0cf5b..eec9f602 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -35,14 +35,18 @@
"react-dom": "18.2.0",
"react-toastify": "^9.1.1",
"recoil": "^0.7.6",
+ "rehype": "^12.0.1",
"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",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"remark-stringify": "^10.0.2",
+ "strip-markdown": "^5.0.0",
"styled-components": "^5.3.6",
"styled-reset": "^4.4.2",
"typescript": "4.8.4",
@@ -3218,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",
@@ -3417,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",
@@ -3518,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",
@@ -5319,6 +5352,21 @@
"url": "https://github.com/sponsors/mysticatea"
}
},
+ "node_modules/rehype": {
+ "version": "12.0.1",
+ "resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz",
+ "integrity": "sha512-ey6kAqwLM3X6QnMDILJthGvG1m1ULROS9NT4uG9IDCuv08SFyLlreSuvOa//DgEvbXx62DS6elGVqusWhRUbgw==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "rehype-parse": "^8.0.0",
+ "rehype-stringify": "^9.0.0",
+ "unified": "^10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/rehype-highlight": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-6.0.0.tgz",
@@ -5382,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",
@@ -5396,6 +5462,21 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/remark": {
+ "version": "14.0.2",
+ "resolved": "https://registry.npmjs.org/remark/-/remark-14.0.2.tgz",
+ "integrity": "sha512-A3ARm2V4BgiRXaUo5K0dRvJ1lbogrbXnhkJRmD0yw092/Yl0kOCZt1k9ZeElEwkZsWGsMumz6qL5MfNJH9nOBA==",
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "remark-parse": "^10.0.0",
+ "remark-stringify": "^10.0.0",
+ "unified": "^10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/remark-breaks": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-3.0.2.tgz",
@@ -5783,6 +5864,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/strip-markdown": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/strip-markdown/-/strip-markdown-5.0.0.tgz",
+ "integrity": "sha512-PXSts6Ta9A/TwGxVVSRlQs1ukJTAwwtbip2OheJEjPyfykaQ4sJSTnQWjLTI2vYWNts/R/91/csagp15W8n9gA==",
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.6",
+ "unified": "^10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
@@ -8891,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",
@@ -9033,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",
@@ -9110,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",
@@ -10242,6 +10358,17 @@
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
"integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg=="
},
+ "rehype": {
+ "version": "12.0.1",
+ "resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz",
+ "integrity": "sha512-ey6kAqwLM3X6QnMDILJthGvG1m1ULROS9NT4uG9IDCuv08SFyLlreSuvOa//DgEvbXx62DS6elGVqusWhRUbgw==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "rehype-parse": "^8.0.0",
+ "rehype-stringify": "^9.0.0",
+ "unified": "^10.0.0"
+ }
+ },
"rehype-highlight": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-6.0.0.tgz",
@@ -10289,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",
@@ -10299,6 +10440,17 @@
"unified": "^10.0.0"
}
},
+ "remark": {
+ "version": "14.0.2",
+ "resolved": "https://registry.npmjs.org/remark/-/remark-14.0.2.tgz",
+ "integrity": "sha512-A3ARm2V4BgiRXaUo5K0dRvJ1lbogrbXnhkJRmD0yw092/Yl0kOCZt1k9ZeElEwkZsWGsMumz6qL5MfNJH9nOBA==",
+ "requires": {
+ "@types/mdast": "^3.0.0",
+ "remark-parse": "^10.0.0",
+ "remark-stringify": "^10.0.0",
+ "unified": "^10.0.0"
+ }
+ },
"remark-breaks": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-3.0.2.tgz",
@@ -10557,6 +10709,16 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="
},
+ "strip-markdown": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/strip-markdown/-/strip-markdown-5.0.0.tgz",
+ "integrity": "sha512-PXSts6Ta9A/TwGxVVSRlQs1ukJTAwwtbip2OheJEjPyfykaQ4sJSTnQWjLTI2vYWNts/R/91/csagp15W8n9gA==",
+ "requires": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.6",
+ "unified": "^10.0.0"
+ }
+ },
"style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 7622954b..cdcfaf54 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -36,14 +36,18 @@
"react-dom": "18.2.0",
"react-toastify": "^9.1.1",
"recoil": "^0.7.6",
+ "rehype": "^12.0.1",
"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",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"remark-stringify": "^10.0.2",
+ "strip-markdown": "^5.0.0",
"styled-components": "^5.3.6",
"styled-reset": "^4.4.2",
"typescript": "4.8.4",
diff --git a/frontend/pages/viewer/[...data].tsx b/frontend/pages/viewer/[...data].tsx
index 347f098e..b5097ed5 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;
@@ -82,7 +82,7 @@ export default function Viewer({ article }: ViewerProps) {
diff --git a/frontend/utils/parser.ts b/frontend/utils/parser.ts
index 42fea91b..6803d3a0 100644
--- a/frontend/utils/parser.ts
+++ b/frontend/utils/parser.ts
@@ -1,11 +1,15 @@
+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';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import remarkStringify from 'remark-stringify';
+import stripMarkdown from 'strip-markdown';
import { unified } from 'unified';
export const markdown2html = (markdown: string) => {
@@ -17,19 +21,28 @@ export const markdown2html = (markdown: string) => {
.processSync(markdown)
.toString();
- return unified()
- .use(rehypeParse)
+ const htmlWithSyntaxHighlight = rehype()
+ .use(rehypeSlug)
.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;
};
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, '')}`;
+};