원래는 Article 생성시 본문을 Markdown 원본으로 저장하려고 했다.
근데 굳이 한번 파싱된 HTML 로 저장한 이유는 해당 html 을 곧바로 넣어주면 Toast UI Editor 우측에 나온 것처럼 화면에 나오길 기대해서이다.
근데 실제로 넣어보니 저대로 안나옴;;;
Toast UI Viewer 를 사용해서 의도대로 이쁘게 나오도록 만들자. (https://nhn.github.io/tui.editor/latest/tutorial-example04-viewer 참조)
추가로 처음 생각한대로 Article 본문을 Markdown 원본으로 저장하자.
기존 HTML 을 다시 Markdown 원본으로 바꿔야 하는 문제가 있으니 백엔드에서 하지 말고 프론트에서 Toast UI Editor 를 사용하자.
백엔드에서 하기 귀찮기도 하고, 기존 HTML 변환을 Toast UI Editor 에서 했으니 Markdown 변환도 Toast UI Editor 에서 하는게 안전해 보여서... (Toast UI Editor 는 백엔드용이 아님.)
뭐 어쨌든 Toast UI Editor Wrapper 컴포넌트에는 Markdown 본문을 get / set 할 수 있도록 getMarkdown
, setMarkdown
함수를 추가하자.
ToastUIEditor.svelte
export function getMarkdown() {
editorInstance.setMarkdown(editorInstance.getMarkdown().trim());
return editorInstance.getMarkdown();
}
export function setMarkdown(contents) {
editorInstance.setMarkdown(contents);
}
변환 작업은 Toast UI Editor 를 사용할 수 있는 Admin Article 생성 화면에서 하자.
AdminAddArticle.svelte
- 임시 코드
const fix = async () => {
const response = await getArticle();
const arr = [];
for (let i = 0; i < response.data.length; i++) {
const article = response.data[i];
const contents = editor.getMarkdown(editor.setHTML(article.contents));
arr.push(new Promise(resolve => {
patchArticle({
query: {
_id: article._id
},
data: {
contents: contents
}
}).then(() => {resolve();});
}));
}
await new Promise.all(arr).then();
};
이걸로 DB 에 있는 모든 Article 의 본문을 Markdown 으로 변환했다.
뭐 제목은 거창하지만 결국 기존에 쓰던 ToastUIEditor.svelte
의 setHTMl
, getHTML
대신에 getMarkdown
, setMarkdown
을 사용하도록 수정했다.
Viewer 는 말그대로 Showing 동작만 하면 되기 때문에 별 다른 동작은 필요 없음.
ToastUIViewer.svelte
<div bind:this={viewer} ></div>
<script>
import { onMount } from 'svelte';
import '@toast-ui/editor/dist/toastui-editor.css';
import '@toast-ui/editor/dist/theme/toastui-editor-dark.css';
import Viewer from '@toast-ui/editor/dist/toastui-editor-viewer';
export let initContents = '';
let viewerInstance;
let viewer;
onMount(() => {
viewerInstance = new Viewer({
el: viewer,
// height: '500px',
initialValue:initContents,
theme: 'dark',
});
viewerInstance.setMarkdown(initContents);
});
</script>
Article 상세 화면에 ToastUIViewer.svelte
를 적용해서 잘 나오는지 확인해보자.
ArticleDetail.svelte
<h1>상세화면~~</h1>
{#if !!article.contents}
<ToastUIViewer initContents={article.contents}></ToastUIViewer>
{/if}
<script>
import { getArticle } from '../../api/article.js';
import { location } from 'svelte-spa-router';
import ToastUIViewer from '../external-wrapper/ToastUIViewer.svelte';
let article = {};
const init = async () => {
const _id = $location.split('/').pop();
const response = await getArticle({ _id });
if (response.success) {
article = response.data[0];
console.log(article);
}
};
location.subscribe(() => {
init();
});
</script>
의도대로 잘 나온다 ㅎㅎ
Article List Item 에서는 대표 이미지와 본문 요약을 HTML 데이터를 파싱해서 쓰고 있었다. (https://velog.io/@crazydj/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%A7%81%EC%A0%91-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EB%8C%80%EC%9E%91%EC%A0%84-ArticleListItem.svelte 참조)
본문 데이터가 HTML 에서 Markdown 으로 바뀌었기 때문에, 파싱하는 로직을 바꾸어야 한다.
근데 생각해보니... 이런 로직들... util 적인 성격을 띄는 로직들은 별도로 빼 놓는게 좋을 것 같아서 util 로직들을 한 곳에 - src/utils/common.js
에 모아두기로 결정했다.
일단은 기존 HTML 파싱 로직을 옮기자.
src/utils/common.js
// article contents (html) 에서 image 및 summery 추출
const makeSummeryFromHtml = contents => {
let image = '';
let summery = '';
try {
// article.contents 내에서 첫번째 이미지 src 추출해서 image 로 설정
const reg = new RegExp(/(<img[^>]+src\s*=\s*[\"']?([^>\"']+)[\"']?[^>]*>)/, 'i');
const regResult = reg.exec(contents);
if (regResult?.length > 1) {
image = regResult[2];
}
// <img>, <h1~5>, <div>, <blockquote>, <table>, <code> 은 그냥 빈스트링으로 replace
// <br>, <li> 는 ' ' 로 replace
// 나머지 태그들 - <~> 전부 빈스트링으로 replace
summery = contents
.replace(/(<img[^>]*>)|(<h1[^>]*>.*<\/h1>)|(<h2[^>]*>.*<\/h2>)|(<h3[^>]*>.*<\/h3>)|(<h4[^>]*>.*<\/h4>)|(<h5[^>]*>.*<\/h5>)|(<h6[^>]*>.*<\/h6>)|(<div[^>]*>(?!<div).+?<\/div>)|(<blockquote[^>]*>(?!<blockquote).+?<\/blockquote>)|(<table[^>]*>(?!<table).+?<\/table>)|(<code[^>]*>(?!<code).+?<\/code>)/gi, '')
.replace(/(<br[^>]*>)|(<li[^>]*>)/gi, ' ')
.replace(/<[^>]*>?/gi, '')
.replace(/\n/gi, ' ')
.slice(0, 200);
} catch (e) {
console.error(e);
}
return { image, summery };
};
export { makeSummeryFromHtml };
이제 Markdown 을 파싱하는 로직을 추가하자.
Toaust UI Editor 를 쓰고 싶은데, 파싱 로직만 제공하는 건 없는 듯...
해서 Markdown 파싱은 markdonw-it
플러그인을 사용하기로 한다. (https://github.com/markdown-it/markdown-it 참조)
추가로 하는 김에 계속 눈에 거슬렸던 created
날짜 변환 로직도 같이 추가하자.
src/utils/common.js
import MarkdownIt from 'markdown-it';
// article contents (html) 에서 image 및 summery 추출
const makeSummeryFromHtml = contents => {...};
// article contents (markdown) 에서 image 및 summery 추출
const mdi = new MarkdownIt();
const makeSummeryFromMarkdown = contents => {
let image = '';
let summery = '';
try {
const arrContents = mdi
.parse(contents)
.filter(content => content.type === 'inline')
.flatMap(content => content?.children.filter(c => c.type === 'text' || c.type === 'image'));
arrContents.map(content => {
if (content.type === 'text' && summery.length < 200) {
summery += (content.content + ' ');
} else if (content.type === 'image' && !image) {
image = content.attrs[0][1];
}
});
} catch (e) {
console.error(e);
const result = makeSummeryFromHtml(mdi.render(contents));
image = result.image;
summery = result.summery;
}
return { image, summery };
};
const getArticleCreated = created => {
try {
const date = new Date(created);
return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일 ${date.getHours()}시 ${date.getMinutes()}분 ${date.getSeconds()}초`;
} catch (e) {
console.error(e);
return '';
}
};
export { makeSummeryFromHtml, makeSummeryFromMarkdown, getArticleCreated };
왜인지 모르겠지만... mdi.parse()
호출시 에러가 나는 경우도 있어서... 기존 HTML 파싱하는 로직을 예외처리로 사용했음.
ArticleListItem.svelte
에 추가하는 로직을 아래와 같이 적용하고 잘 동작하는 것까지 확인~.
ArticleListItem.svelte
<script>
import { makeSummeryFromMarkdown, getArticleCreated } from '../../utils/common.js'
...
$: {
const result = makeSummeryFromMarkdown(article.contents);
image = result.image || defaultImage;
contents = result.summery || 'Contents....';
created = getArticleCreated(article.created) || '';
};
</script>
이제 상세 화면을 개발할 차례다.
그건 다음에....