Markdown의 복잡성을 줄이기 위한 대안적 경량 마크업 문법 제안
CommonMark를 개발하면서 우리는 가능한 한 John Gruber의 원래 Markdown 문법 설명에 충실하려고 노력해 왔습니다. 모호성을 없애고 일관성을 높이기 위해, 그리고 이제는 사실상 어디에나 쓰이는 몇몇 문법 요소(가령 펜스 코드 블록과 축약 참조 링크)를 추가하기 위해서만 가끔 그 설명에서 벗어났습니다.
이처럼 보수적으로 접근해야 할 아주 좋은 이유들이 있습니다. 하지만 과거에 대한 이러한 존중은 CommonMark 사양을 매우 복잡한 괴물로 만들었습니다. 예를 들어 강조를 규율하는 원칙은 17개이며, 이 규칙들로도 결정되지 않는 경우가 남습니다. 목록 항목과 HTML 블록에 관한 규칙도 매우 복잡합니다. 이 모든 규칙은 때때로 예상치 못한 결과를 낳고, CommonMark용 파서를 작성하는 일을 복잡하게 만듭니다. 때로는 1.0이라고 부를 만한 사양에 도달할 수 있을지 절망스럽습니다.
우리가 과거에 얽매이지 않는다면 어떨까요? Markdown의 좋은 점은 유지하면서, CommonMark 사양의 비대화와 복잡성으로 이어진 일부 기능은 고쳐서 경량 마크업 문법을 만들려 한다면 어떨까요?
처음부터 분명히 해 두자면, 저는 CommonMark 프로젝트의 목표를 바꾸자고 제안하는 것이 아닙니다. 이 생각들이 무언가로 이어진다면, 그것은 아마도 새 이름 아래의 완전히 새로운 프로젝트여야 할 것입니다. 또한 현실적으로 말해, 하위 호환성을 유지하는 부담은 기존 시스템을 새로운 경량 마크업 언어로 옮기는 막대한 실무적 비용에 비하면 가볍습니다. 그래도… 공상해 보는 일은 유익할 수 있다고 생각합니다.
이하에서는 가장 큰 어려움을 만들어 낸다고 생각하는 Markdown의 여섯 가지 기능을 살펴보고, 각 문제점을 고칠 방법을 제안하겠습니다.
Markdown에서는 텍스트를 * 또는 _ 문자로 감싸 강조를 만듭니다. *like this*와 같습니다. 강한 강조는 이를 두 번 겹쳐서 만듭니다. **like this**와 같습니다. 모두 매우 단순해 보이며, 어느 쪽이 강한 강조인지도 시각적으로 분명합니다.
안타깝게도 이 단순한 설명만으로는 문법을 확정할 수 없습니다. 예를 들어 다음을 생각해 보십시오.
**this* text**
우리의 단순한 규칙은 다음 두 해석 모두와 일치합니다.
<strong>this* text</strong><em><em>this</em> text</em>*따라서 강조 파싱을 완전히 명시하려면 추가 규칙이 필요합니다. CommonMark 사양의, 낙담스러울 만큼 복잡한 17개 규칙은 사람이 가장 자연스럽다고 느낄 만한 해석을 강제하려는 것입니다.
강한 강조에 중복 문자를 사용하고, fan*tas*tic처럼 단어의 일부까지 강조할 수 있게 한 것이 사양이 해결해야 하는 모호성을 크게 늘려 강조 파싱을 명시하는 문제를 훨씬 더 어렵게 만들었다고 생각합니다. 맥락에 따라 단어 한가운데의 *** 세 글자 문자열은 다음 중 무엇이든 될 수 있습니다.
* 문자 뒤에 강한 강조의 시작이 오는 경우.* 문자가 오는 경우.* 문자, 그다음 일반 강조의 시작.**가 오는 경우.** 뒤에 일반 강조의 시작이 오는 경우.***.모호성을 크게 줄이려면 강한 강조를 위한 중복 문자 구분자를 없앨 수 있습니다. 대신 일반 강조에는 단일 _를, 강한 강조에는 단일 *를 사용합니다. 강조는 이제 왼쪽 인접이지만 오른쪽 인접은 아닌 구분자로 시작하고, 같은 종류의 오른쪽 인접이지만 왼쪽 인접은 아닌 구분자로 끝납니다.
단어 내부 강조에는 특별한 문법을 요구합니다.
fan~_tas_~tic
단어 내부 강조는 극히 드무므로, 일반적인 강조 규칙과 개념적 모델을 단순화하는 대가로 이를 조금 더 어렵게 만드는 것은 좋은 절충입니다. 여기서 특수 문자 ~는 강조를 파싱하는 목적상 공백처럼 작동하여 단어 내부의 _가 강조를 시작하고 끝낼 수 있게 하지만, 공백으로 렌더링되지는 않습니다. 따라서 reStructuredText의 이스케이프된 공백처럼 동작합니다.
참조 링크를 통상적으로 처리하면 문서 전체를 파싱하기 전에는 어떤 문법 요소도 분류할 수 없습니다. 예를 들어 다음을 보십시오.
[foo][bar][baz]
[bar]: url
이는 다음처럼 해석됩니다.
<p><a href="url">foo</a>[baz]</p>
하지만 bar 대신 baz에 대한 링크를 정의한다고 가정해 봅시다.
[foo][bar][baz]
[baz]: url
그러면 다음을 얻습니다.
<p>[foo]<a href="url">bar</a></p>
따라서 문서 전체를 파싱하기 전까지는 [foo]가 리터럴 대괄호 텍스트인지, 아니면 링크 설명이 foo인 링크인지 알 수 없습니다.
이는 문법 강조 표시를 매우 어렵게 만들고, 파서 작성도 복잡하게 합니다. 예를 들어 문서를 파싱한 뒤에 링크를 파싱하고 AST에서 참조를 해석하는 방식은 불가능합니다.
문서에서 어떤 참조가 정의되었는지와 독립적으로, 모양만으로 참조 링크를 알아볼 수 있게 합니다. 따라서 다음은
[foo][bar][baz]
참조 bar에 정의된 URL로 향하는 링크 텍스트 foo의 링크(정의된 것이 없다면 아무것도 가리키지 않는 링크)와, 그 뒤의 리터럴 텍스트 [baz]로 파싱됩니다.
다음과 같은 축약 참조는
[foo]
[foo]: url
허용하지 않아야 합니다. 모든 리터럴 대괄호 문자를 작성자가 이스케이프하도록 강제할 의향이 있다면 예외입니다. 대신 압축 형식을 사용할 수 있습니다.
[foo][]
[foo]: url
조금 더 입력해야 하지만, 링크가 있다는 점은 명확하고 모호하지 않습니다.
들여쓴 코드 블록의 파싱은 간단하지만, 그 존재는 목록 항목 규칙을 복잡하게 합니다.
목록 항목의 문법을 명시할 때는, 내용이 목록 항목의 일부로 간주되려면 얼마나 멀리 들여써야 하는지 말해야 합니다. 원래 Markdown 문법 문서는 네 칸 들여쓰기를 요구하는 “네 칸 규칙”을 암시했지만, 구현체는 이를 거의 따르지 않았고, 대부분의 사람은 다음이
- a
- b
중첩 목록으로 간주되지 않는다는 것을 직관에 어긋난다고 느낍니다. 그래서 CommonMark에서는 수많은 가능한 규칙을 검토한 끝에, 목록 항목의 내용은 목록 표식 뒤의 첫 번째 공백이 아닌 내용의 위치까지 적어도 들여써야 한다는 규칙을 채택했습니다.
- Item
^-- contents must be indented to here.
이 규칙이 나쁜 것은 아니지만 복잡성을 더합니다. 목록 표식의 위치뿐 아니라 그 뒤에 오는 첫 번째 공백이 아닌 내용의 위치도 추적해야 합니다. 그리고 빈 목록 항목이나 들여쓴 코드로 시작하는 목록 항목 같은 경우를 위한 특별 규칙도 필요합니다. 마지막으로, 예를 들어 다음이 중첩 목록이 아니라는 사실은 여전히 많은 사람에게 놀랍습니다.
- a
- b
그러므로 다음과 같이 물을 수 있습니다. 목록 항목의 내용은 목록 표식보다 적어도 한 칸 더 들여써야 한다고 요구하면 왜 안 될까요? 이것이 명백한 최소 규칙입니다. 이를 막는 것은 들여쓴 코드 블록의 존재입니다. 목록 항목 아래의 블록 수준 내용이 목록 표식 뒤 한 칸 들여쓰기에서 시작한다면, 들여쓴 코드는 목록 표식보다 다섯 칸 더 들여써야 합니다. 이는 원래 Markdown 문법 설명에서 제시한 여덟 칸과 호환되지 않을 뿐 아니라, 더 긴 목록 표식에서는 끔찍한 결과를 낳습니다.
99. Here's my list item.
And this is indented code! Even though it
lines up with the paragraph above!
요약하면, 목록 항목 규칙의 복잡성 대부분은 들여쓴 코드 블록을 다뤄야 한다는 필요성에서 비롯됩니다.
펜스 코드 블록은 이제 대개 들여쓴 코드 블록보다 선호됩니다. 강조 표시용 문법을 지정할 수 있고 코드를 복사해 붙여 넣을 때 들여쓰기와 내어쓰기를 할 필요가 없기 때문입니다. 펜스 코드 블록이 있으므로 들여쓴 코드 블록은 필요하지 않습니다. 따라서 그냥 없앨 수 있습니다.
그러면 목록 중첩을 나타내는 데 들여쓰기를 더 유연하게 사용할 수 있고, 목록 항목의 내용은 목록 표식에 비해 적어도 한 칸 들여써야 한다는 단순하고 명백한 규칙을 받아들일 수 있습니다.
들여쓴 코드 블록을 제거하는 또 다른 장점은, 목록에 영향을 미치는 경우를 제외하면 이제 일반적으로 첫 들여쓰기를 무시할 수 있다는 점입니다.
처음부터 Markdown 문서에는 원시 HTML을 삽입할 수 있었고, 이는 그대로 통과되었습니다. 일반 텍스트로 표현할 수 없는 모든 것에 대해 원시 HTML로 되돌아갈 수 있다는 발상입니다.
이는 들리는 것만큼 단순하지 않습니다. 처음부터 Markdown.pl은 인라인 HTML과 블록 수준 HTML을 구분했습니다. 인라인 HTML 태그는 그대로 통과되었지만, 그 내용은 Markdown으로 해석될 수 있었습니다.
<em>**hi**</em>
은 다음을 생성합니다.
<em><strong>hi</strong></em>
블록 수준 HTML 내용은 빈 줄로 분리해야 하며, 시작 및 종료 태그는 들여쓰지 않아야 한다고 규정되었습니다. 이런 HTML 블록에서는 모든 것이 그대로 통과되며 Markdown으로 해석되지 않습니다. 따라서 다음은
<div>
*hello*
</div>
그대로 다음을 생성합니다.
<div>
*hello*
</div>
이는 여러 문제를 낳았습니다. 첫째, 블록 수준 내용을 어떻게 식별할까요? HTML이 발전함에 따라 바뀔 수 있는 HTML 요소 목록을 하드코딩해야 할까요? 인라인 또는 블록 맥락에서 나타날 수 있는 <del> 같은 요소는 어떻게 할까요?
둘째, 제대로 분리되거나 들여쓰기되지 않은 블록 수준 HTML은 어떻게 할까요?
hi <div>
hello</div>
파서는 이를 인라인 HTML로만 처리하고 유효하지 않은 HTML을 생성해야 할까요?
셋째, HTML 블록의 끝은 어떻게 식별할까요? 태그는 중첩될 수 있으므로, 이는 자명하지 않은 HTML 파싱을 요구합니다. 공개된 Markdown.pl 버전은 두 번 중첩된 <div> 요소에 대해 유효하지 않은 HTML을 생성했습니다. 이 문제를 고치도록 설계된 베타 버전에는 심각한 성능 문제가 있었습니다.
CommonMark의 HTML 블록 사양은 무한한 미리 보기나 HTML 파싱의 완전한 구현 없이 원시 HTML 블록을 쉽게 파싱할 수 있게 하고, 작성자가 원한다면 블록 수준 HTML 태그 안에 CommonMark 내용을 포함할 수 있게 설계되었습니다. 하지만 결과는 꽤 복잡합니다. 시작과 종료 조건의 서로 다른 쌍이 일곱 개입니다. 인라인 HTML 규칙도 많은 정의를 포함해 복잡합니다.
또한 Markdown은 HTML을 만들 때뿐 아니라 여러 형식의 문서를 만들 때도 유용해졌으므로, HTML을 원시 그대로 통과시키도록 특별 취급하는 방식은 다소 자의적으로 보이게 되었습니다. 다른 형식으로 작성하는 사람에게도 원시 내용을 통과시키는 방법이 유용할 것입니다.
원시 HTML을 통과시키는 대신, 모든 형식의 원시 내용을 통과시킬 수 있는 특별한 문법을 도입해야 합니다. 이를 위해 원시 문자열용 기존 컨테이너인 코드 스팬과 코드 블록을 겸용할 수 있습니다.
This is raw HTML: `<img src="myimage.jpg">`{=html}.
And here's an HTML block:
```{=html}
<div id="main">
<div class="article">
LaTeX도 할 수 있습니다.
\begin{tikzpicture}
\node[inner sep=0pt] (russell) at (0,0)
{\includegraphics[width=.25\textwidth]{bertrand_russell.jpg}};
\node[inner sep=0pt] (whitehead) at (5,-6)
{\includegraphics[width=.25\textwidth]{alfred_north_whitehead.jpg}};
\draw[<->,thick] (russell.south east) -- (whitehead.north west)
node[midway,fill=white] {Principia Mathematica};
\end{tikzpicture}
서로 다른 형식에 서로 다른 원시 내용을 통과시킬 수도 있습니다. 예를 들어 복잡한 그림의 HTML 및 LaTeX 버전을 모두 포함할 수 있습니다.
### 목록과 빈 줄
다음처럼 목록이 문단을 끊을 수 있을까요?
Paragraph test.
원래 Markdown 문법 문서는 이를 확정하지 않지만, `Markdown.pl`과 그 테스트 모음은 문단 텍스트와 뒤따르는 목록 사이에 빈 줄을 요구합니다. 테스트 모음이 보여 주듯, 이 요구 사항은 다음과 같은 것들이 우연히 목록으로 만들어지는 일을 피하기 위해 도입되었습니다.
I think he weighed 200 pounds, maybe even 220. But he was no more than five feet tall.
하지만 한 가지 예외가 있었습니다. 문단 텍스트 자체가 목록 항목의 일부일 때는 빈 줄이 필요하지 않습니다.
Paragraph one
paragraph two
이 예외가 없다면, 다음과 같은 경우의 중첩 목록을 인식할 수 없습니다.
CommonMark의 목록 항목 사양을 생각하면서, 우리는 `Markdown.pl`의 동작이 목록 항목의 내용은 목록 항목 밖에 있을 때와 같은 의미를 가져야 한다는 _일관성 원칙_을 위반한다는 점을 깨달았습니다. 이 원칙은 다음이
a
목록을 포함하지 않는다면, 다음도
하위 목록을 포함하지 않는다는 점을 뜻합니다. 우리는 일관성 원칙이 중요하다고 생각합니다. 실제로 목록 항목과 인용 블록을 명시하는 방식은 이 원칙을 전제합니다. 즉, 우리는 문단 텍스트와 뒤따르는 목록 사이에 빈 줄을 요구하거나, 목록이 문단을 끊도록 허용하여 문단 텍스트가 목록으로 우연히 해석될 위험을 감수해야 했습니다. 앞선 빈 줄 없이 촘촘한 하위 목록을 쓰는 일은 Markdown에서 매우 흔하므로, 첫 번째 선택지는 배제했습니다. 그래서 두 번째 선택지를 택했고, 보기 흉한 발견법으로 피해를 줄였습니다. 순서 있는 목록은 목록 번호가 `1`일 때만 문단을 끊을 수 있게 한 것입니다.
#### 목록과 빈 줄을 고치는 방법
문단 텍스트와 목록 사이에는 빈 줄을 요구해야 합니다. 언제나. 하위 목록에서도 마찬가지입니다. 그러므로 하위 목록이 있는 촘촘한 목록을 만들려면 다음처럼 작성합니다.
a
d
빈 줄 없이 이어진 항목 쌍을 적어도 하나 포함하는 목록을 촘촘한 목록이라고 하겠습니다. 따라서 위 예에서 안쪽 목록은 촘촘하지만 바깥쪽 목록은 그렇지 않습니다. 두 목록을 모두 촘촘하게 하려면 다음과 같이 합니다.
a
d
### 속성
Markdown은 요소에 속성(클래스나 식별자 등)을 추가하는 일반적인 방법을 제공하지 않습니다. 이 때문에 문서의 구역으로 향하는 내부 링크를 만드는 고유한 방법이 없습니다. 많은 구현체가 헤더에서 식별자를 자동 생성하는 조금씩 다른 방법을 도입했습니다. 또한 자연스러운 확장 메커니즘도 없습니다. Markdown에는 인라인용 컨테이너(예: 강조), 블록용 컨테이너(예: 인용 블록), 원시 인라인 내용용 컨테이너(코드 스팬), 원시 블록 내용용 컨테이너(코드 블록)가 있습니다. 여기에 임의의 속성을 붙일 수 있다면 필터로 조작하여 매우 유연한 출력을 만들 수 있습니다. 예를 들어 클래스가 “warning”인 인용 블록을 경고 주의문으로 처리하거나, 클래스가 “dot”인 코드 블록을 이미지로 렌더링할 graphviz dot 다이어그램으로 처리할 수 있습니다. 하지만 현재 요소에 속성을 붙이는 유일한 방법은 원시 HTML로 내려가는 것입니다.
#### 속성을 고치는 방법
속성 명세를 위한 문법을 도입합니다. pandoc을 따라 이를 위해 중괄호 `{}`를 사용합니다. 식별자는 `#`로 나타냅니다. 단독 단어는 클래스로 취급합니다. 임의의 키/값 속성에는 `=`를 사용합니다.
모든 블록 요소의 _앞_ 줄과 모든 인라인 요소의 바로 _뒤_에 속성을 추가할 수 있게 합니다.
{#myheader}
여기서 식별자 `myheader`는 헤더 블록에 추가되고, 클래스 `blue` 및 키/값 속성 `position=left`는 강조된 텍스트 `Blue Title`에 추가됩니다.
속성 지정자는 한 줄에 들어가야 하지만 여러 개를 사용할 수 있으며, 이 경우 결합됩니다.
{#mywarning} {warning}
Don't try this at home! It might be dangerous.
pandoc처럼 꾸밈 없는 인라인 스팬용 문법과 펜스 일반 블록 컨테이너를 추가하는 것도 도움이 될 수 있습니다. 하지만 인라인 컨테이너에는 강조를, 블록 컨테이너에는 인용 블록을 사용할 수 있으므로 절대적으로 필요하지는 않습니다.
## 권고 사항 요약
1. 강조
a. 강조와 강한 강조에 서로 다른 문자를 사용합니다.
b. 중복 문자 구분자를 사용하지 않습니다.
c. 강조 규칙을 단순화합니다.
d. 단어 내부 강조를 위한 특별 문법을 도입합니다. `~`는 강조 파싱에서는 공백처럼 동작하지만 아무것도 렌더링하지 않습니다.
2. 참조 링크
a. 문서의 다른 곳에 참조 링크 정의가 존재하는지 여부에 따라 어떤 것을 링크로 파싱할지 결정하지 않습니다.
b. 축약 참조 링크를 제거합니다.
3. 코드
a. 들여쓴 코드 블록을 제거합니다. 코드에는 펜스 블록만 사용합니다.
4. 목록
a. 목록 항목 아래에 무엇이 속하는지 판단하는 단순한 규칙을 사용합니다. 목록 표식에 비해 조금이라도 들여쓴 것은 모두 항목에 속합니다.
b. 문단 내용과 뒤따르는 목록 사이에 빈 줄을 요구합니다.
c. 촘촘한 목록의 규칙을 고칩니다. 두 항목 사이에 빈 줄이 없는 항목 쌍이 하나라도 있으면 목록은 촘촘합니다.
5. HTML
a. 원시 HTML의 자동 통과를 제거합니다. `<br>` 같은 것은 이제 일반 텍스트로 처리되고 이스케이프됩니다.
b. 임의 형식의 원시 내용을 통과시키기 위한 명시적 문법을 도입합니다. 인라인 맥락에서는 `{=FORMAT}`이 뒤따르는 코드 스팬을 사용하고, 블록 맥락에서는 정보 문자열 `{=FORMAT}`을 가진 펜스 코드 블록을 사용합니다.
6. 속성
a. 다음과 같은 균일한 속성 문법을 도입합니다. `{class #identifier key=value}`.
b. 모든 블록 요소에 속성을 허용합니다. 식별자는 블록 요소 앞 줄에 단독으로 나타나야 합니다. 여러 속성은 연속된 줄에 지정할 수 있으며 결합됩니다.
c. 모든 인라인 요소에 속성을 허용합니다. 식별자는 적용할 인라인 요소의 바로 뒤에 인접하여 나타나야 합니다.