Javascript로 Markdown 파서 구현하기

row:off 일때 화면

row:on 일때 화면

이번에는 javascript로 markdown 파서를 구현하는 것을 살펴보겠습니다.

html

일단 html 부분을 살펴보겠습니다.
간단히 입력부분은 asidecontenteditable 옵션을 줘서 수정 가능하게 했습니다.
그러고 출력부분을 section로 만들었습니다.
마지막에 markdown.js을 넣어서 입력한 부분에 대한 처리(이벤트)를 하도록 하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>

<head>
<meta charset='utf-8'>
<link rel="stylesheet" href="md_css.css">
</head>

<body>
<a href="#" id="raw-switch" class="raw-switch">Raw: <span></span></a>
<aside id="markdown" contenteditable></aside>
<section id="output-html"></section>
<script src="markdown.js">
</script>
</body>

</html>

css

css는 asidesection의 넓이를 50%로 주어 각각 위치하도록 하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
* {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}

html,
body {
height: 100%;
margin: 0;
}

body {
font: 300 16px 'Cousine';
color: #444;
line-height: 1.5em;
}

.raw-switch {
font-size: .9em;
color: #444;
position: fixed;
top: 20px;
left: 40px;
z-index: 1;
}

aside,
section {
width: 50%;
padding: 40px 80px;
}

aside {
background: #f1f1f1;
white-space: pre-wrap;
position: fixed;
top: 0;
left: 0;
bottom: 0;
overflow-y: scroll;
float: left;
}

aside:focus {
outline: none;
}

section {
height: 100%;
margin-left: 50%;
overflow-y: scroll;
overflow-x: hidden;
}

section h1 {
font-size: 1.8em;
}

section b {
font-weight: 700;
}

section pre {
overflow-y: scroll;
}

변환 javascript식

javascript는 일단 parseMd라는 함수에서 markdown 문법에 따라 html 형식으로 변형시켜줍니다.
해당 변형에는 javascript의 정규식 표현을 따릅니다.

raw-switch버튼은 off일때 innerText, on일때 innerHTML형식을 써서 마크다운/html 형식으로 결과물을 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
function parseMd(md){

//ul
md = md.replace(/^\s*\n\*/gm, '<ul>\n*');
md = md.replace(/^(\*.+)\s*\n([^\*])/gm, '$1\n</ul>\n\n$2');
md = md.replace(/^\*(.+)/gm, '<li>$1</li>');

//ol
md = md.replace(/^\s*\n\d\./gm, '<ol>\n1.');
md = md.replace(/^(\d\..+)\s*\n([^\d\.])/gm, '$1\n</ol>\n\n$2');
md = md.replace(/^\d\.(.+)/gm, '<li>$1</li>');

//blockquote
md = md.replace(/^\>(.+)/gm, '<blockquote>$1</blockquote>');

//h
md = md.replace(/[\#]{6}(.+)/g, '<h6>$1</h6>');
md = md.replace(/[\#]{5}(.+)/g, '<h5>$1</h5>');
md = md.replace(/[\#]{4}(.+)/g, '<h4>$1</h4>');
md = md.replace(/[\#]{3}(.+)/g, '<h3>$1</h3>');
md = md.replace(/[\#]{2}(.+)/g, '<h2>$1</h2>');
md = md.replace(/[\#]{1}(.+)/g, '<h1>$1</h1>');

//alt h
md = md.replace(/^(.+)\n\=+/gm, '<h1>$1</h1>');
md = md.replace(/^(.+)\n\-+/gm, '<h2>$1</h2>');

//images
md = md.replace(/\!\[([^\]]+)\]\(([^\)]+)\)/g, '<img src="$2" alt="$1" />');

//links
md = md.replace(/[\[]{1}([^\]]+)[\]]{1}[\(]{1}([^\)\"]+)(\"(.+)\")?[\)]{1}/g, '<a href="$2" title="$4">$1</a>');

//font styles
md = md.replace(/[\*\_]{2}([^\*\_]+)[\*\_]{2}/g, '<b>$1</b>');
md = md.replace(/[\*\_]{1}([^\*\_]+)[\*\_]{1}/g, '<i>$1</i>');
md = md.replace(/[\~]{2}([^\~]+)[\~]{2}/g, '<del>$1</del>');

//pre
md = md.replace(/^\s*\n\`\`\`(([^\s]+))?/gm, '<pre class="$2">');
md = md.replace(/^\`\`\`\s*\n/gm, '</pre>\n\n');

//code
md = md.replace(/[\`]{1}([^\`]+)[\`]{1}/g, '<code>$1</code>');

//p
md = md.replace(/^\s*(\n)?(.+)/gm, function(m){
return /\<(\/)?(h\d|ul|ol|li|blockquote|pre|img)/.test(m) ? m : '<p>'+m+'</p>';
});

//strip p from pre
md = md.replace(/(\<pre.+\>)\s*\n\<p\>(.+)\<\/p\>/gm, '$1$2');

return md;

}

var rawMode = true;
mdEl = document.getElementById('markdown'),
outputEl = document.getElementById('output-html'),
parse = function(){
outputEl[rawMode ? "innerText" : "innerHTML"] = parseMd(mdEl.innerText);
};

parse();
mdEl.addEventListener('keyup', parse, false);

//Raw mode trigger btn
(function(){

var trigger = document.getElementById('raw-switch'),
status = trigger.getElementsByTagName('span')[0],
updateStatus = function(){
status.innerText = rawMode ? 'On' : 'Off';
};

updateStatus();
trigger.addEventListener('click', function(e){
e.preventDefault();
rawMode = rawMode ? false : true;
updateStatus();
parse();
}, false);

}());
공유하기