← 제니앱살아있는 문서계산하는 마크다운 에디터. topaz 코드 블록이 실제로 실행되고 렌더러도 토파즈로 작성했습니다.
render-ko.tpz · 토파즈 마크다운 렌더러
// Markdown -> HTML renderer, written entirely in Topaz. Pure & deterministic: same input
// yields identical HTML across the interpreter and native/wasm builds. Strings are viewed as
// arrays of characters via `.scalars()` and processed with the array stdlib (slice/join).
// Key identifiers (functions): 글자화=toChars, 로시작함=startsWith, 글자정리=escapeChar,
// 전체정리=escapeAll, 닫기위치=findClose, 슬래시앞콜론=colonBeforeSlash, 안전한주소=isSafeUrl,
// 링크닫기=findLinkClose, 속성정리=escapeAttr, 인라인변환=renderInline, 캐리지제거=stripCr,
// 줄로분리=splitLines, 숫자인지=isDigit, 순서표식=orderedMarker, 줄이같음=lineEquals,
// 양끝공백=trim, 파이프있음=hasPipe, 칸으로분리=splitCells, 구분칸인지=isDividerCell,
// 구분행인지=isDividerRow, 행html로=rowToHtml, 블록시작인지=isBlockStart, 들여쓰기=indent,
// 목록종류=listKind, 목록글시작=listTextStart, 목록렌더=renderList, 마크다운변환=renderMarkdown.
// Key identifiers (variables): 글자들=chars, 색인=index, 출력=output, 결과=result, 줄들=lines,
// 줄=line, 글자=char, 접두=prefix, 구분자=delimiter, 시작=start, 깊이=depth, 종류/케이=kind,
// 들여깊이=indentDepth, 블록번호=blockNo, 칸들=cells, 한도=limit, 분류=category, 금액=amount.
// 마크다운 → HTML 렌더러입니다. 전부 토파즈로 작성했습니다. 한글 식별자판입니다.
// 순수 함수입니다. 같은 입력은 인터프리터와 네이티브/wasm 빌드에서 동일한 HTML이 됩니다.
// 문자열은 `.scalars()`로 글자 배열로 보고, 배열 stdlib(slice/join)로 깔끔하게 처리합니다.
// 닫힌 렌더러 태그. Markdown 텍스트와 URL은 입력 경계라 string으로 두고, 내부 HTML 태그 선택만 좁힌다.
// Closed renderer tags. Markdown text and URLs stay string at the input boundary; internal HTML tag choices are narrowed.
type 목록종류값 = "ul" | "ol" | ""
type 표칸태그 = "th" | "td"
function 글자화(문자열: string) -> Array<string> { 문자열.scalars() }
function 로시작함(글자들: Array<string>, 접두: string) -> bool {
let 접두글자 = 글자화(접두)
if 글자들.length < 접두글자.length { return false }
let mut 색인 = 0
while 색인 < 접두글자.length {
if 글자들[색인] != 접두글자[색인] { return false }
색인 = 색인 + 1
}
true
}
function 글자정리(글자: string) -> string {
if 글자 == "&" { "&" }
else if 글자 == "<" { "<" }
else if 글자 == ">" { ">" }
else { 글자 }
}
function 전체정리(글자들: Array<string>) -> string {
let mut 출력 = ""
let mut 색인 = 0
while 색인 < 글자들.length {
출력 = 출력 + 글자정리(글자들[색인])
색인 = 색인 + 1
}
출력
}
function 닫기위치(글자들: Array<string>, 시작: int, 구분자: Array<string>) -> int {
let 구분길이 = 구분자.length
let mut 색인 = 시작
while 색인 + 구분길이 <= 글자들.length {
let mut 일치 = true
let mut 제이 = 0
while 제이 < 구분길이 {
if 글자들[색인 + 제이] != 구분자[제이] { 일치 = false }
제이 = 제이 + 1
}
if 일치 { return 색인 }
색인 = 색인 + 1
}
-1
}
// --- 안전한 링크: 기본-거부 URL 허용목록 + 속성 이스케이프 ---
// --- Safe links: default-deny URL allowlist + attribute escaping ---
function 슬래시앞콜론(글자들: Array<string>) -> bool {
let mut 색인 = 0
while 색인 < 글자들.length {
if 글자들[색인] == ":" { return true }
if 글자들[색인] == "/" { return false }
색인 = 색인 + 1
}
false
}
function 안전한주소(주소: string) -> bool {
// 브라우저는 URL 앞 공백/제어문자를 제거하므로 다듬은 형태로 검사합니다.
// Browsers strip leading whitespace/control chars from URLs, so we check the trimmed form.
// (문자열 stdlib: `trimStart` + `로시작함`. 수작업 글자 스캔 없이 처리합니다.)
// (string stdlib: `trimStart` + `로시작함`/startsWith. no manual char scan.)
let 정리주소 = 주소.trimStart()
// 프로토콜-상대(`//host`, `/\host`, `\host`)는 외부 이동이라 거부합니다 (아래
// `/` 분기는 동일-출처 루트-상대 전용입니다).
// Protocol-relative URLs (`//host`, `/\host`, `\host`) navigate off-site, so reject them
// (the `/` branch below is for same-origin root-relative paths only).
if 정리주소.startsWith("//") { return false }
if 정리주소.startsWith("/\\") { return false }
if 정리주소.startsWith("\\") { return false }
if 정리주소.startsWith("http://") { return true }
if 정리주소.startsWith("https://") { return true }
if 정리주소.startsWith("mailto:") { return true }
if 정리주소.startsWith("/") { return true }
if 정리주소.startsWith("#") { return true }
if 정리주소.startsWith("./") { return true }
if 정리주소.startsWith("../") { return true }
// 스킴 없는 상대 경로(첫 '/' 앞 ':' 없음)는 안전합니다. 그 외 스킴
// (javascript:/data:/vbscript:/…)은 거부되어 링크가 무력화됩니다.
// Scheme-less relative paths (no ':' before the first '/') are safe; any other scheme
// (javascript:/data:/vbscript:/...) is rejected, which neutralizes the link.
!슬래시앞콜론(정리주소.scalars())
}
// 링크 `(url)`을 닫는 `)`입니다. 안쪽 괄호를 균형 맞춰 `foo(1)`/`alert(1)` 같은
// 주소를 통째로 잡습니다 (첫 `)`에서 잘리지 않게 합니다).
// Finds the `)` that closes a link `(url)`. Balances inner parens so addresses like
// `foo(1)`/`alert(1)` are captured whole (not cut off at the first `)`).
function 링크닫기(글자들: Array<string>, 시작: int) -> int {
let mut 깊이 = 0
let mut 색인 = 시작
while 색인 < 글자들.length {
if 글자들[색인] == "(" { 깊이 = 깊이 + 1 }
else if 글자들[색인] == ")" {
if 깊이 == 0 { return 색인 }
깊이 = 깊이 - 1
}
색인 = 색인 + 1
}
-1
}
function 속성정리(글자들: Array<string>) -> string {
let mut 출력 = ""
let mut 색인 = 0
while 색인 < 글자들.length {
let 글자 = 글자들[색인]
if 글자 == "&" { 출력 = 출력 + "&" }
else if 글자 == "\"" { 출력 = 출력 + """ }
else if 글자 == "<" { 출력 = 출력 + "<" }
else if 글자 == ">" { 출력 = 출력 + ">" }
else { 출력 = 출력 + 글자 }
색인 = 색인 + 1
}
출력
}
// --- 인라인: 코드/굵게/기울임/링크. 글자 슬라이스 위에서 처리합니다 ---
// --- Inline: code/bold/italic/links, processed over character slices ---
function 인라인변환(글자들: Array<string>) -> string {
let mut 출력 = ""
let mut 색인 = 0
while 색인 < 글자들.length {
let 글자 = 글자들[색인]
if 글자 == "\\" && 색인 + 1 < 글자들.length {
// 백슬래시 이스케이프입니다. 다음 글자는 리터럴입니다 (HTML 이스케이프, 마크다운 의미 없음)
// Backslash escape: the next char is literal (HTML-escaped, no markdown meaning).
출력 = 출력 + 글자정리(글자들[색인 + 1])
색인 = 색인 + 2
} else if 글자 == "!" && 색인 + 1 < 글자들.length && 글자들[색인 + 1] == "[" {
// 이미지 입니다. 링크와 동일한 기본-거부 URL 안전성을 적용합니다
// Image : applies the same default-deny URL safety as links.
let 대괄호닫기 = 닫기위치(글자들, 색인 + 2, ["]"])
if 대괄호닫기 >= 0 && 대괄호닫기 + 1 < 글자들.length && 글자들[대괄호닫기 + 1] == "(" {
let 소괄호닫기 = 링크닫기(글자들, 대괄호닫기 + 2)
if 소괄호닫기 >= 0 {
let 대체글자 = 글자들.slice(색인 + 2, 대괄호닫기)
let 주소글자 = 글자들.slice(대괄호닫기 + 2, 소괄호닫기)
if 안전한주소(주소글자.join("")) {
출력 = 출력 + "<img src=\"" + 속성정리(주소글자) + "\" alt=\"" + 속성정리(대체글자) + "\">"
} else {
출력 = 출력 + 전체정리(대체글자)
}
색인 = 소괄호닫기 + 1
} else {
출력 = 출력 + 글자정리(글자)
색인 = 색인 + 1
}
} else {
출력 = 출력 + 글자정리(글자)
색인 = 색인 + 1
}
} else if 글자 == "`" {
let 닫기 = 닫기위치(글자들, 색인 + 1, ["`"])
if 닫기 >= 0 {
출력 = 출력 + "<code>" + 전체정리(글자들.slice(색인 + 1, 닫기)) + "</code>"
색인 = 닫기 + 1
} else {
출력 = 출력 + 글자정리(글자)
색인 = 색인 + 1
}
} else if 글자 == "*" && 색인 + 1 < 글자들.length && 글자들[색인 + 1] == "*" {
let 닫기 = 닫기위치(글자들, 색인 + 2, ["*", "*"])
if 닫기 >= 0 {
출력 = 출력 + "<strong>" + 인라인변환(글자들.slice(색인 + 2, 닫기)) + "</strong>"
색인 = 닫기 + 2
} else {
출력 = 출력 + 글자정리(글자)
색인 = 색인 + 1
}
} else if 글자 == "*" {
let 닫기 = 닫기위치(글자들, 색인 + 1, ["*"])
if 닫기 >= 0 {
출력 = 출력 + "<em>" + 인라인변환(글자들.slice(색인 + 1, 닫기)) + "</em>"
색인 = 닫기 + 1
} else {
출력 = 출력 + 글자정리(글자)
색인 = 색인 + 1
}
} else if 글자 == "~" && 색인 + 1 < 글자들.length && 글자들[색인 + 1] == "~" {
let 닫기 = 닫기위치(글자들, 색인 + 2, ["~", "~"])
if 닫기 >= 0 {
출력 = 출력 + "<del>" + 인라인변환(글자들.slice(색인 + 2, 닫기)) + "</del>"
색인 = 닫기 + 2
} else {
출력 = 출력 + 글자정리(글자)
색인 = 색인 + 1
}
} else if 글자 == "[" {
let 대괄호닫기 = 닫기위치(글자들, 색인 + 1, ["]"])
if 대괄호닫기 >= 0 && 대괄호닫기 + 1 < 글자들.length && 글자들[대괄호닫기 + 1] == "(" {
let 소괄호닫기 = 링크닫기(글자들, 대괄호닫기 + 2)
if 소괄호닫기 >= 0 {
let 텍스트글자 = 글자들.slice(색인 + 1, 대괄호닫기)
let 주소글자 = 글자들.slice(대괄호닫기 + 2, 소괄호닫기)
let 주소 = 주소글자.join("")
if 안전한주소(주소) {
출력 = 출력 + "<a href=\"" + 속성정리(주소글자) + "\">" + 인라인변환(텍스트글자) + "</a>"
} else {
출력 = 출력 + 인라인변환(텍스트글자)
}
색인 = 소괄호닫기 + 1
} else {
출력 = 출력 + 글자정리(글자)
색인 = 색인 + 1
}
} else {
출력 = 출력 + 글자정리(글자)
색인 = 색인 + 1
}
} else {
출력 = 출력 + 글자정리(글자)
색인 = 색인 + 1
}
}
출력
}
// 모든 CR을 제거합니다. CRLF(및 떠도는 CR) 입력도 깨끗이 매칭합니다 (`---\r\n` → <hr>)
// Strips all CRs so CRLF (and stray CR) input still matches cleanly (`---\r\n` -> <hr>).
function 캐리지제거(글자들: Array<string>) -> Array<string> {
let mut 출력: Array<string> = []
for 글자 in 글자들 {
if 글자 != "\r" { 출력.push(글자) }
}
출력
}
function 줄로분리(문자열: string) -> Array<Array<string>> {
// `str.split`로 줄바꿈 분리 후 줄마다 CR을 제거합니다 (split은 CRLF의 `\r`을
// 남기므로 옛 수작업 스캐너와 다릅니다).
// Splits on newlines with `str.split`, then drops the CR per line (split keeps the
// `\r` from CRLF, unlike the old hand-written scanner).
let mut 줄들: Array<Array<string>> = []
for 원시 in 문자열.split("\n") {
줄들.push(캐리지제거(원시.scalars()))
}
줄들
}
function 숫자인지(글자: string) -> bool {
글자 == "0" || 글자 == "1" || 글자 == "2" || 글자 == "3" || 글자 == "4"
|| 글자 == "5" || 글자 == "6" || 글자 == "7" || 글자 == "8" || 글자 == "9"
}
// 순서 목록 마커 `N. `의 글자 길이를 반환합니다. 없으면 0을 반환합니다
// Returns the char length of an ordered-list marker `N. `; returns 0 if absent.
function 순서표식(글자들: Array<string>) -> int {
let mut 색인 = 0
while 색인 < 글자들.length && 숫자인지(글자들[색인]) { 색인 = 색인 + 1 }
if 색인 > 0 && 색인 + 1 < 글자들.length && 글자들[색인] == "." && 글자들[색인 + 1] == " " { 색인 + 2 } else { 0 }
}
function 줄이같음(글자들: Array<string>, 문자열: string) -> bool {
let 비교글자 = 글자화(문자열)
if 글자들.length != 비교글자.length { return false }
let mut 색인 = 0
while 색인 < 비교글자.length {
if 글자들[색인] != 비교글자[색인] { return false }
색인 = 색인 + 1
}
true
}
// --- 블록: 제목/목록/인용/펜스코드/구분선/문단 ---
// --- Block: headings/lists/blockquotes/fenced code/horizontal rules/paragraphs ---
function 양끝공백(글자들: Array<string>) -> Array<string> {
let mut 앞 = 0
while 앞 < 글자들.length && (글자들[앞] == " " || 글자들[앞] == "\t") { 앞 = 앞 + 1 }
let mut 뒤 = 글자들.length
while 뒤 > 앞 && (글자들[뒤 - 1] == " " || 글자들[뒤 - 1] == "\t") { 뒤 = 뒤 - 1 }
글자들.slice(앞, 뒤)
}
function 파이프있음(글자들: Array<string>) -> bool {
let mut 색인 = 0
while 색인 < 글자들.length {
if 글자들[색인] == "|" { return true }
색인 = 색인 + 1
}
false
}
// 표 행을 '|'로 나누고 칸을 다듬고 바깥 파이프가 만든 빈 칸은 버립니다
// Splits a table row on '|', trims each cell, and drops empty cells made by outer pipes.
function 칸으로분리(글자들: Array<string>) -> Array<Array<string>> {
let mut 원시: Array<Array<string>> = []
let mut 현재: Array<string> = []
let mut 색인 = 0
while 색인 < 글자들.length {
if 글자들[색인] == "\\" && 색인 + 1 < 글자들.length {
// 이스케이프된 글자(특히 GFM `\|`)는 칸에 통째로 들어갑니다. 분리점이 아닙니다.
// An escaped char (notably GFM `\|`) goes into the cell verbatim; it is not a split point.
// `인라인변환`이 칸을 렌더할 때 백슬래시 이스케이프를 처리합니다.
// `인라인변환`/renderInline handles the backslash escape when rendering the cell.
현재.push(글자들[색인])
현재.push(글자들[색인 + 1])
색인 = 색인 + 2
} else if 글자들[색인] == "|" {
원시.push(양끝공백(현재))
현재 = []
색인 = 색인 + 1
} else {
현재.push(글자들[색인])
색인 = 색인 + 1
}
}
원시.push(양끝공백(현재))
let mut 칸들: Array<Array<string>> = []
let mut 케이 = 0
while 케이 < 원시.length {
let 끝인지 = 케이 == 0 || 케이 == 원시.length - 1
if !(끝인지 && 원시[케이].length == 0) { 칸들.push(원시[케이]) }
케이 = 케이 + 1
}
칸들
}
// 구분 칸입니다. 앞 `:` 선택, `-` 한 개 이상, 뒤 `:` 선택이고 그 외엔 아무것도
// 없습니다 (`:--`,`--:`,`:-:`,`---`는 통과하고 `:--:--`,`:`,`a-`는 불가합니다).
// Divider cell: optional leading `:`, one or more `-`, optional trailing `:`, nothing else
// (`:--`,`--:`,`:-:`,`---` pass; `:--:--`,`:`,`a-` fail).
function 구분칸인지(칸: Array<string>) -> bool {
if 칸.length == 0 { return false }
let mut 제이 = 0
if 칸[제이] == ":" { 제이 = 제이 + 1 }
let mut 대시수 = 0
while 제이 < 칸.length && 칸[제이] == "-" {
대시수 = 대시수 + 1
제이 = 제이 + 1
}
if 대시수 == 0 { return false }
if 제이 < 칸.length && 칸[제이] == ":" { 제이 = 제이 + 1 }
제이 == 칸.length
}
// 구분 행입니다. 파이프가 있고 모든 칸이 유효한 구분 칸입니다.
// Divider row: has a pipe and every cell is a valid divider cell.
function 구분행인지(글자들: Array<string>) -> bool {
if !파이프있음(글자들) { return false }
let 칸들 = 칸으로분리(글자들)
if 칸들.length == 0 { return false }
let mut 케이 = 0
while 케이 < 칸들.length {
if !구분칸인지(칸들[케이]) { return false }
케이 = 케이 + 1
}
true
}
function 행html로(칸들: Array<Array<string>>, 태그: 표칸태그) -> string {
let mut 출력 = "<tr>"
let mut 케이 = 0
while 케이 < 칸들.length {
출력 = 출력 + "<" + 태그 + ">" + 인라인변환(칸들[케이]) + "</" + 태그 + ">"
케이 = 케이 + 1
}
출력 + "</tr>\n"
}
// 이 줄이 (표가 아닌) 블록의 시작인지 판정합니다. 표 본문은 그런 줄에서 멈춥니다
// Tests whether this line starts a (non-table) block; table bodies stop at such lines
// (GFM: a table ends at a blank line or the start of another block structure).
// (GFM: 표는 빈 줄이나 다른 블록 구조의 시작에서 끝납니다).
function 블록시작인지(글자들: Array<string>) -> bool {
글자들.length == 0
|| 로시작함(글자들, "# ")
|| 로시작함(글자들, "## ")
|| 로시작함(글자들, "### ")
|| 로시작함(글자들, "- ")
|| 순서표식(글자들) > 0
|| 로시작함(글자들, "> ")
|| 로시작함(글자들, "```")
|| 줄이같음(글자들, "---")
}
// --- 중첩 목록: 엄격합니다. 공백만, 레벨당 2칸, ul/ol, loose 목록 없음 ---
// --- Nested lists: strict, spaces only, 2 per level, ul/ol, no loose lists ---
function 들여쓰기(글자들: Array<string>) -> int {
let mut 색인 = 0
while 색인 < 글자들.length && 글자들[색인] == " " { 색인 = 색인 + 1 }
색인
}
function 목록종류(글자들: Array<string>) -> 목록종류값 {
let 엔 = 들여쓰기(글자들)
let 나머지 = 글자들.slice(엔, 글자들.length)
if 로시작함(나머지, "- ") { "ul" }
else if 순서표식(나머지) > 0 { "ol" }
else { "" }
}
function 목록없음(종류: 목록종류값) -> bool {
match 종류 {
case "" => true
case "ul" => false
case "ol" => false
}
}
function 목록태그(종류: 목록종류값) -> string {
match 종류 {
case "ul" => "ul"
case "ol" => "ol"
case "" => ""
}
}
function 목록글시작(글자들: Array<string>) -> int {
let 엔 = 들여쓰기(글자들)
let 나머지 = 글자들.slice(엔, 글자들.length)
if 로시작함(나머지, "- ") { 엔 + 2 } else { 엔 + 순서표식(나머지) }
}
// 목록과 그 중첩 자식을 렌더합니다. 정확히 `들여깊이`칸·같은 종류의 항목입니다.
// Renders a list and its nested children: items at exactly `들여깊이`/indentDepth and same kind.
// 더 깊은 항목(들여깊이+2)은 이전 <li> 안에 중첩합니다. html과 다음 줄을 반환합니다.
// Deeper items (indentDepth+2) nest inside the previous <li>. Returns html and the next line.
function 목록렌더(줄들: Array<Array<string>>, 시작점: int, 들여깊이: int) -> { 결과: string, 다음: int } {
let 종류 = 목록종류(줄들[시작점])
let 태그 = 목록태그(종류)
let mut 결과 = "<" + 태그 + ">\n"
let mut 색인 = 시작점
while 색인 < 줄들.length {
let 들여수 = 들여쓰기(줄들[색인])
let 케이 = 목록종류(줄들[색인])
if 목록없음(케이) || 들여수 != 들여깊이 || 케이 != 종류 { break }
let 글시작 = 목록글시작(줄들[색인])
결과 = 결과 + "<li>" + 인라인변환(줄들[색인].slice(글시작, 줄들[색인].length))
색인 = 색인 + 1
if 색인 < 줄들.length && !목록없음(목록종류(줄들[색인])) && 들여쓰기(줄들[색인]) == 들여깊이 + 2 {
let 자식 = 목록렌더(줄들, 색인, 들여깊이 + 2)
결과 = 결과 + "\n" + 자식.결과 + "</li>\n"
색인 = 자식.다음
} else {
결과 = 결과 + "</li>\n"
}
}
결과 = 결과 + "</" + 태그 + ">\n"
{ 결과: 결과, 다음: 색인 }
}
function 마크다운변환(소스: string) -> string {
let 줄들 = 줄로분리(소스)
let mut 결과 = ""
let mut 색인 = 0
let mut 블록번호 = 0
while 색인 < 줄들.length {
let 줄 = 줄들[색인]
if 로시작함(줄, "```") {
// ```topaz 펜스는 순번 계산 플레이스홀더가 됩니다. JS 셸이 블록 소스를
// (문서 순서대로) 추출해 실행 결과를 주입합니다. 그 외 펜스는
// 정적인 HTML-이스케이프 코드 블록으로 남습니다.
// A ```topaz fence becomes an indexed placeholder; the JS shell extracts block sources
// (in document order) and injects their run results. Other fences stay as static,
// HTML-escaped code blocks.
let 토파즈인지 = 줄이같음(줄, "```topaz")
색인 = 색인 + 1
let mut 코드 = ""
while 색인 < 줄들.length && !로시작함(줄들[색인], "```") {
코드 = 코드 + 전체정리(줄들[색인]) + "\n"
색인 = 색인 + 1
}
if 색인 < 줄들.length { 색인 = 색인 + 1 }
if 토파즈인지 {
결과 = 결과 + "<div class=\"topaz-block\" data-topaz-block=\"{블록번호}\"></div>\n"
블록번호 = 블록번호 + 1
} else {
결과 = 결과 + "<pre><code>" + 코드 + "</code></pre>\n"
}
} else if !목록없음(목록종류(줄)) && 들여쓰기(줄) == 0 {
let 결과값 = 목록렌더(줄들, 색인, 0)
결과 = 결과 + 결과값.결과
색인 = 결과값.다음
} else if 로시작함(줄, "> ") {
결과 = 결과 + "<blockquote>"
let mut 처음 = true
while 색인 < 줄들.length && 로시작함(줄들[색인], "> ") {
if !처음 { 결과 = 결과 + " " }
결과 = 결과 + 인라인변환(줄들[색인].slice(2, 줄들[색인].length))
처음 = false
색인 = 색인 + 1
}
결과 = 결과 + "</blockquote>\n"
} else if 줄이같음(줄, "---") {
결과 = 결과 + "<hr>\n"
색인 = 색인 + 1
} else if 로시작함(줄, "### ") {
결과 = 결과 + "<h3>" + 인라인변환(줄.slice(4, 줄.length)) + "</h3>\n"
색인 = 색인 + 1
} else if 로시작함(줄, "## ") {
결과 = 결과 + "<h2>" + 인라인변환(줄.slice(3, 줄.length)) + "</h2>\n"
색인 = 색인 + 1
} else if 로시작함(줄, "# ") {
결과 = 결과 + "<h1>" + 인라인변환(줄.slice(2, 줄.length)) + "</h1>\n"
색인 = 색인 + 1
} else if 색인 + 1 < 줄들.length && 파이프있음(줄) && 구분행인지(줄들[색인 + 1])
&& 칸으로분리(줄).length == 칸으로분리(줄들[색인 + 1]).length {
// GFM 표입니다. 제목/목록/인용/hr 다음에 검사하므로 `#`/`-`/`>` 줄이 헤더로
// 도둑맞지 않습니다. 구분 행은 헤더와 칸 수가 같아야 합니다.
// GFM table. Checked after headings/lists/blockquotes/hr so `#`/`-`/`>` lines are not
// stolen as a header. The divider row must have the same cell count as the header.
결과 = 결과 + "<table>\n<thead>\n" + 행html로(칸으로분리(줄), "th") + "</thead>\n<tbody>\n"
색인 = 색인 + 2
while 색인 < 줄들.length && 파이프있음(줄들[색인]) && !블록시작인지(줄들[색인]) {
결과 = 결과 + 행html로(칸으로분리(줄들[색인]), "td")
색인 = 색인 + 1
}
결과 = 결과 + "</tbody>\n</table>\n"
} else if 줄.length == 0 {
색인 = 색인 + 1
} else {
결과 = 결과 + "<p>" + 인라인변환(줄) + "</p>\n"
색인 = 색인 + 1
}
}
결과
}
// 진입점입니다. 호스트의 텍스트 페이로드(에디터 textarea / 파이프 stdin)를 렌더합니다.
// Entry point. Renders the host's text payload (editor textarea / piped stdin).
print(마크다운변환(input()))
토파즈 문법 · ```topaz 블록에서 쓸 수 있는 것
값과 변수
let 이름 = 10 고정 값
let mut 합계 = 0 변경 가능한 값
정수 "문자열" true false ()
모음
[1, 2, 3] 배열
{ 분류: "식비", 한도: 400 } 레코드
항목.분류 필드 접근
목록[0] 목록.length 목록.push(값)
맵
let mut m: Map<string, int> = Map.new()
m.insert("식비", 70)
m.get("식비") 없으면 None
연산
+ - * / % 산술
== != < > <= >= 비교
&& || ! 논리
값 ?? 기본 None이면 기본값