mdpdf.tpz
// 마크다운을 고정 크기 페이지로 흘려보내 페이지 나눔과 고아/미망 줄 제어를 보여준다.
// 결과는 페이지 박스(흑백)로 그리며, PDF로 저장은 브라우저 인쇄 기능을 쓴다(PDF 바이트 생성은 안 함).
// Markdown to paginated pages. Flows blocks into fixed-size pages with page breaks and
// orphan/widow control, drawn as page boxes. Export to PDF uses the browser print dialog
// (this app does not generate PDF bytes).
//
// 줄/높이 모델은 고정폭이다: 라틴/공백 1칸, 한글/CJK 2칸. 한 줄은 폭(60칸)에서 줄바꿈하고,
// 페이지는 높이(사용자 지정 줄 수)만큼 담는다. 인라인 서식(**굵게**, *기울임*)은 폭에 안 세고,
// 줄바꿈은 보이는 너비로 한다. 짝이 없는 마커는 리터럴로 그린다.
// Fixed-width model: Latin/space = 1 unit, Hangul/CJK = 2. Inline markup (**bold**, *italic*) does
// not count toward width; wrapping is by visible width. An unpaired marker renders as literal text.
//
// 페이지 나눔(한 번에 일관 처리, 용량 초과 없음) / Page breaks (one consistent pass, never over capacity):
// - 제목은 페이지 맨 아래 홀로 두지 않는다. keep-with-next for headings.
// - 여러 줄 블록의 첫 줄을 페이지 맨 아래 홀로 두지 않는다. no orphan.
// - 여러 줄 블록의 마지막 줄을 페이지 맨 위 홀로 두지 않는다. no widow.
//
// Key identifiers: 줄=line, 셀=styled char cell, 종류=kind, 블록=block index, 시작/끝=first/last line,
// 블록줄수=lines in block, 이스케이프=escape, 시제이케이=isCJK, 너비글=charWidth, 본문너비=textWidth,
// 셀분해=toCells, 셀줄나눔=wrapCells, 셀줄그리기=renderCells, 마크업줄들=markupLines,
// 블록줄들=codeLines, 마크다운파싱=parseMarkdown, 배치=paginate, 페이지그리기=renderPage, 처리=run
//
// 결정적이다. 같은 (페이지 높이, 마크다운)이면 인터프리터와 네이티브/wasm 빌드에서 같은 HTML이 나온다.
// 안 하는 것: 실제 PDF 바이트, 전체 CSS, 웹폰트, 이미지, 표, 2단계 이상 중첩 목록, 인라인 HTML, 폰트 메트릭.
// 닫힌 줄 종류. 마크다운 문법 원문은 입력 경계라 string으로 두고, 레이아웃 분기 태그만 좁힌다.
// Closed line kinds. Raw Markdown remains string at the input boundary; layout dispatch tags are narrowed.
type 줄종류 = "문단" | "제목1" | "제목2" | "제목3" | "인용" | "목록" | "코드" | "구분"
type 줄 = { 글: string, 종류: 줄종류, 블록: int, 시작: bool, 끝: bool, 블록줄수: int }
type 셀 = { 글: string, 굵게: bool, 기울임: bool, 너비: int }
// 레코드 리터럴은 문자열 리터럴을 넓히므로 줄 레코드는 헬퍼에서 만든다.
// Record literals widen string literals, so line records are built through this helper.
function 줄만들기(글: string, 종류: 줄종류, 블록: int, 시작: bool, 끝: bool, 블록줄수: int) -> 줄 {
{ 글: 글, 종류: 종류, 블록: 블록, 시작: 시작, 끝: 끝, 블록줄수: 블록줄수 }
}
let 폭 = 60
// HTML 이스케이프. 마크다운은 사용자 입력이라 출력 전에 반드시 이스케이프한다.
// HTML escape. Markdown is user input, so always escape before output.
function 이스케이프(원문: string) -> string {
let mut 결과 = ""
for 글 in 원문.scalars() {
if 글 == "&" { 결과 = 결과 + "&" }
else if 글 == "<" { 결과 = 결과 + "<" }
else if 글 == ">" { 결과 = 결과 + ">" }
else if 글 == "\"" { 결과 = 결과 + """ }
else { 결과 = 결과 + 글 }
}
결과
}
// ASCII 숫자(0~9)인가.
// Is the scalar an ASCII digit (0-9)?
function 숫자글(글: string) -> bool {
let 코드 = 글.codePointAt(0) ?? 0
코드 >= 48 && 코드 <= 57
}
// CJK(한글, 한자, CJK 문장부호 등)인가. 고정폭에서 2칸을 차지한다.
// Is the scalar CJK? Two fixed-width units.
function 시제이케이(글: string) -> bool {
let 코드 = 글.codePointAt(0) ?? 0
(코드 >= 44032 && 코드 <= 55203)
|| (코드 >= 12592 && 코드 <= 12687)
|| (코드 >= 4352 && 코드 <= 4607)
|| (코드 >= 19968 && 코드 <= 40959)
|| (코드 >= 12288 && 코드 <= 12351)
|| (코드 >= 65280 && 코드 <= 65519)
}
// 한 글자의 고정폭 너비.
// Fixed-width width of one scalar.
function 너비글(글: string) -> int {
if 시제이케이(글) { 2 } else { 1 }
}
// 문자열의 시각 너비(고정폭 합).
// Visual width of a string (sum of fixed-width units).
function 본문너비(글: string) -> int {
let mut 합 = 0
for 자 in 글.scalars() { 합 = 합 + 너비글(자) }
합
}
// 공백 n개로 된 문자열.
// A string of n spaces.
function 공백문자열(개수: int) -> string {
let mut 결과 = ""
let mut 차례 = 0
while 차례 < 개수 { 결과 = 결과 + " "; 차례 = 차례 + 1 }
결과
}
// 인라인 마크업을 스타일 셀 목록으로 분해한다. **굵게**/*기울임*은 짝을 맞춰 토글하고,
// 짝이 없는 마커는 리터럴 글자로 남긴다. 마커는 폭에 세지 않는다.
// Break inline markup into styled cells. Paired **/* toggle bold/italic; an unpaired marker stays
// literal. Markers do not count toward width.
function 셀분해(원문: string) -> Array<셀> {
let 글자들 = 원문.scalars()
let 수 = 글자들.length
// 1단계: 항목 토큰화. 종류 "굵"=**, "기"=*, "글"=한 글자.
let mut 종류들: Array<string> = []
let mut 값들: Array<string> = []
let mut 차례 = 0
while 차례 < 수 {
if 차례 + 1 < 수 && 글자들[차례] == "*" && 글자들[차례 + 1] == "*" {
종류들.push("굵"); 값들.push("**"); 차례 = 차례 + 2
} else if 글자들[차례] == "*" {
종류들.push("기"); 값들.push("*"); 차례 = 차례 + 1
} else {
종류들.push("글"); 값들.push(글자들[차례]); 차례 = 차례 + 1
}
}
// 2단계: 짝 맞추기. 굵/기 개수가 홀수면 마지막 하나를 리터럴로 본다.
let mut 굵수 = 0
let mut 기수 = 0
let mut 굵끝 = -1
let mut 기끝 = -1
let mut 색인 = 0
while 색인 < 종류들.length {
if 종류들[색인] == "굵" { 굵수 = 굵수 + 1; 굵끝 = 색인 }
else if 종류들[색인] == "기" { 기수 = 기수 + 1; 기끝 = 색인 }
색인 = 색인 + 1
}
let 굵리터럴 = if 굵수 % 2 == 1 { 굵끝 } else { -1 }
let 기리터럴 = if 기수 % 2 == 1 { 기끝 } else { -1 }
// 3단계: 셀 생성.
let mut 셀들: Array<셀> = []
let mut 굵게 = false
let mut 기울임 = false
let mut 자리 = 0
while 자리 < 종류들.length {
let 종 = 종류들[자리]
if 종 == "굵" && 자리 != 굵리터럴 { 굵게 = !굵게 }
else if 종 == "기" && 자리 != 기리터럴 { 기울임 = !기울임 }
else {
for 자 in 값들[자리].scalars() {
셀들.push({ 글: 자, 굵게: 굵게, 기울임: 기울임, 너비: 너비글(자) })
}
}
자리 = 자리 + 1
}
셀들
}
// 셀들을 폭에 맞춰 줄(셀 배열)로 나눈다. 공백 셀에서 단어를 끊고, 폭보다 긴 단어는 강제 분할한다.
// Wrap cells to width into lines of cells. Break words at space cells; hard-break overlong words.
function 셀줄나눔(셀들: Array<셀>, 한계: int) -> Array<Array<셀>> {
let mut 줄목록: Array<Array<셀>> = []
let mut 현재: Array<셀> = []
let mut 현재폭 = 0
let mut 단어: Array<셀> = []
let mut 단어폭 = 0
let 수 = 셀들.length
let mut 차례 = 0
while 차례 <= 수 {
let 경계 = 차례 == 수 || 셀들[차례].글 == " "
if 경계 {
if 단어.length > 0 {
if 단어폭 > 한계 {
// 폭보다 긴 단어: 현재 줄을 비우고 글자 단위로 강제 분할.
if 현재.length > 0 { 줄목록.push(현재); 현재 = []; 현재폭 = 0 }
let mut 조각: Array<셀> = []
let mut 조각폭 = 0
for 자 in 단어 {
if 조각폭 + 자.너비 > 한계 && 조각.length > 0 {
줄목록.push(조각); 조각 = []; 조각폭 = 0
}
조각.push(자); 조각폭 = 조각폭 + 자.너비
}
현재 = 조각; 현재폭 = 조각폭
} else {
let 추가 = if 현재.length == 0 { 단어폭 } else { 단어폭 + 1 }
if 현재폭 + 추가 > 한계 {
줄목록.push(현재); 현재 = []; 현재폭 = 0
for 자 in 단어 { 현재.push(자) }
현재폭 = 단어폭
} else {
if 현재.length > 0 { 현재.push({ 글: " ", 굵게: false, 기울임: false, 너비: 1 }); 현재폭 = 현재폭 + 1 }
for 자 in 단어 { 현재.push(자) }
현재폭 = 현재폭 + 단어폭
}
}
단어 = []; 단어폭 = 0
}
} else {
단어.push(셀들[차례]); 단어폭 = 단어폭 + 셀들[차례].너비
}
차례 = 차례 + 1
}
if 현재.length > 0 { 줄목록.push(현재) }
if 줄목록.length == 0 { 줄목록.push([]) }
줄목록
}
// 한 줄(셀 배열)을 HTML로 그린다. 굵게/기울임이 바뀔 때만 태그를 닫고 다시 연다(줄마다 균형 유지).
// Render one line of cells to HTML, opening/closing tags only on style changes (balanced per line).
function 셀줄그리기(줄셀들: Array<셀>) -> string {
let mut 결과 = ""
let mut 굵게 = false
let mut 기울임 = false
for 셀 in 줄셀들 {
if 셀.굵게 != 굵게 || 셀.기울임 != 기울임 {
if 기울임 { 결과 = 결과 + "</i>" }
if 굵게 { 결과 = 결과 + "</b>" }
굵게 = 셀.굵게
기울임 = 셀.기울임
if 굵게 { 결과 = 결과 + "<b>" }
if 기울임 { 결과 = 결과 + "<i>" }
}
결과 = 결과 + 이스케이프(셀.글)
}
if 기울임 { 결과 = 결과 + "</i>" }
if 굵게 { 결과 = 결과 + "</b>" }
결과
}
// 인라인 서식이 있는 블록(문단/제목/인용/목록)을 줄 레코드로 만든다. 보이는 너비로 줄바꿈하고,
// 목록은 접두(• 또는 번호)를 첫 줄에 붙이고 이어지는 줄은 그만큼 들여쓴다.
// Build line records for a markup block. Wrap by visible width; a list puts its marker on the first
// line and indents continuation lines by the marker width.
function 마크업줄들(번호: int, 종류: 줄종류, 원문: string, 한계: int, 접두첫: string, 접두너비: int) -> Array<줄> {
let 셀들 = 셀분해(원문)
let 줄셀목록 = 셀줄나눔(셀들, 한계 - 접두너비)
let 개수 = 줄셀목록.length
let mut 결과: Array<줄> = []
let mut 색인 = 0
while 색인 < 개수 {
let 접두 = if 색인 == 0 { 접두첫 } else { 공백문자열(접두너비) }
let 글 = 이스케이프(접두) + 셀줄그리기(줄셀목록[색인])
결과.push(줄만들기(글, 종류, 번호, 색인 == 0, 색인 == 개수 - 1, 개수))
색인 = 색인 + 1
}
결과
}
// 코드 블록 줄(서식 없이 그대로, 너무 길면 강제 분할). 보이는 너비로 줄바꿈한다.
// Code-block lines (verbatim, hard-broken if too long), wrapped by visible width.
function 강제분할(단어: string, 한계: int) -> Array<string> {
let mut 결과: Array<string> = []
let mut 조각 = ""
let mut 조각폭 = 0
for 자 in 단어.scalars() {
let 자폭 = 너비글(자)
if 조각폭 + 자폭 > 한계 && 조각 != "" {
결과.push(조각); 조각 = 자; 조각폭 = 자폭
} else {
조각 = 조각 + 자; 조각폭 = 조각폭 + 자폭
}
}
if 조각 != "" { 결과.push(조각) }
if 결과.length == 0 { 결과.push("") }
결과
}
function 블록줄들(번호: int, 종류: 줄종류, 글: string, 한계: int) -> Array<줄> {
let 조각들 = 강제분할(글, 한계)
let 개수 = 조각들.length
let mut 결과: Array<줄> = []
let mut 색인 = 0
while 색인 < 개수 {
결과.push(줄만들기(조각들[색인], 종류, 번호, 색인 == 0, 색인 == 개수 - 1, 개수))
색인 = 색인 + 1
}
결과
}
// 코드 블록의 모든 원본 줄을 한 블록으로 묶는다(고아/미망 제어가 코드 블록을 하나의 단위로 본다).
// Build all source lines of a fenced code block as ONE block so orphan/widow control treats it as a unit.
function 코드블록줄들(번호: int, 원줄들: Array<string>, 한계: int) -> Array<줄> {
let mut 조각들: Array<string> = []
for 원 in 원줄들 { for 조각 in 강제분할(원, 한계) { 조각들.push(조각) } }
let 개수 = 조각들.length
let mut 결과: Array<줄> = []
let mut 색인 = 0
while 색인 < 개수 {
결과.push(줄만들기(조각들[색인], "코드", 번호, 색인 == 0, 색인 == 개수 - 1, 개수))
색인 = 색인 + 1
}
결과
}
// 문자열이 접두사로 시작하는가.
// Does the string start with the given prefix?
function 시작하는가(글: string, 접두: string) -> bool {
let 글자들 = 글.scalars()
let 접두자 = 접두.scalars()
if 접두자.length > 글자들.length { return false }
let mut 색인 = 0
let mut 같음 = true
while 색인 < 접두자.length {
if 글자들[색인] != 접두자[색인] { 같음 = false }
색인 = 색인 + 1
}
같음
}
// 접두사를 떼고 남은 문자열.
// Drop a prefix and return the rest.
function 접두제거(글: string, 길이: int) -> string {
let 글자들 = 글.scalars()
글자들.slice(길이, 글자들.length).join("")
}
// 앞에서 n글자.
// First n scalars.
function 앞부분(글: string, 길이: int) -> string {
let 글자들 = 글.scalars()
글자들.slice(0, 길이).join("")
}
// 순서 목록 표시(예: "1. 글")인가. 숫자 다음에 ". "가 오면 참.
// Is this an ordered-list marker like "1. text"?
function 순서목록인가(글: string) -> bool {
let 글자들 = 글.scalars()
let mut 색인 = 0
while 색인 < 글자들.length && 숫자글(글자들[색인]) { 색인 = 색인 + 1 }
색인 > 0 && 색인 + 1 < 글자들.length && 글자들[색인] == "." && 글자들[색인 + 1] == " "
}
// 순서 목록 표시의 칸 수(숫자 자릿수 + ". ").
// Width of an ordered-list marker (digit count + ". ").
function 순서표너비(글: string) -> int {
let 글자들 = 글.scalars()
let mut 색인 = 0
while 색인 < 글자들.length && 숫자글(글자들[색인]) { 색인 = 색인 + 1 }
색인 + 2
}
// 마크다운을 줄바꿈된 줄 목록으로 파싱한다.
// Parse markdown into a flat list of wrapped lines with block info.
function 마크다운파싱(원문: string, 한계: int) -> Array<줄> {
let 입력줄들 = 원문.split("\n")
let mut 결과: Array<줄> = []
let mut 블록번호 = 0
let mut 코드중 = false
let mut 문단 = ""
let mut 코드줄들: Array<string> = []
let mut 입력차례 = 0
while 입력차례 < 입력줄들.length {
let 원줄 = 입력줄들[입력차례]
let 다듬 = 원줄.trim()
if 시작하는가(다듬, "```") {
if 코드중 {
for 줄 in 코드블록줄들(블록번호, 코드줄들, 한계) { 결과.push(줄) }
블록번호 = 블록번호 + 1
코드중 = false
} else {
if 문단 != "" {
for 줄 in 마크업줄들(블록번호, "문단", 문단, 한계, "", 0) { 결과.push(줄) }
블록번호 = 블록번호 + 1; 문단 = ""
}
코드중 = true
코드줄들 = []
}
} else if 코드중 {
코드줄들.push(원줄)
} else if 다듬 == "" {
if 문단 != "" {
for 줄 in 마크업줄들(블록번호, "문단", 문단, 한계, "", 0) { 결과.push(줄) }
블록번호 = 블록번호 + 1; 문단 = ""
}
} else if 다듬 == "---" || 다듬 == "***" {
if 문단 != "" {
for 줄 in 마크업줄들(블록번호, "문단", 문단, 한계, "", 0) { 결과.push(줄) }
블록번호 = 블록번호 + 1; 문단 = ""
}
결과.push(줄만들기("", "구분", 블록번호, true, true, 1))
블록번호 = 블록번호 + 1
} else if 시작하는가(다듬, "### ") {
if 문단 != "" { for 줄 in 마크업줄들(블록번호, "문단", 문단, 한계, "", 0) { 결과.push(줄) }; 블록번호 = 블록번호 + 1; 문단 = "" }
for 줄 in 마크업줄들(블록번호, "제목3", 접두제거(다듬, 4), 한계, "", 0) { 결과.push(줄) }
블록번호 = 블록번호 + 1
} else if 시작하는가(다듬, "## ") {
if 문단 != "" { for 줄 in 마크업줄들(블록번호, "문단", 문단, 한계, "", 0) { 결과.push(줄) }; 블록번호 = 블록번호 + 1; 문단 = "" }
for 줄 in 마크업줄들(블록번호, "제목2", 접두제거(다듬, 3), 한계, "", 0) { 결과.push(줄) }
블록번호 = 블록번호 + 1
} else if 시작하는가(다듬, "# ") {
if 문단 != "" { for 줄 in 마크업줄들(블록번호, "문단", 문단, 한계, "", 0) { 결과.push(줄) }; 블록번호 = 블록번호 + 1; 문단 = "" }
for 줄 in 마크업줄들(블록번호, "제목1", 접두제거(다듬, 2), 한계, "", 0) { 결과.push(줄) }
블록번호 = 블록번호 + 1
} else if 시작하는가(다듬, "> ") {
if 문단 != "" { for 줄 in 마크업줄들(블록번호, "문단", 문단, 한계, "", 0) { 결과.push(줄) }; 블록번호 = 블록번호 + 1; 문단 = "" }
for 줄 in 마크업줄들(블록번호, "인용", 접두제거(다듬, 2), 한계, "", 0) { 결과.push(줄) }
블록번호 = 블록번호 + 1
} else if 시작하는가(다듬, "- ") || 시작하는가(다듬, "* ") {
if 문단 != "" { for 줄 in 마크업줄들(블록번호, "문단", 문단, 한계, "", 0) { 결과.push(줄) }; 블록번호 = 블록번호 + 1; 문단 = "" }
for 줄 in 마크업줄들(블록번호, "목록", 접두제거(다듬, 2), 한계, "• ", 2) { 결과.push(줄) }
블록번호 = 블록번호 + 1
} else if 순서목록인가(다듬) {
if 문단 != "" { for 줄 in 마크업줄들(블록번호, "문단", 문단, 한계, "", 0) { 결과.push(줄) }; 블록번호 = 블록번호 + 1; 문단 = "" }
let 표너비 = 순서표너비(다듬)
for 줄 in 마크업줄들(블록번호, "목록", 접두제거(다듬, 표너비), 한계, 앞부분(다듬, 표너비), 표너비) { 결과.push(줄) }
블록번호 = 블록번호 + 1
} else {
문단 = if 문단 == "" { 다듬 } else { 문단 + " " + 다듬 }
}
입력차례 = 입력차례 + 1
}
if 문단 != "" {
for 줄 in 마크업줄들(블록번호, "문단", 문단, 한계, "", 0) { 결과.push(줄) }
}
if 코드중 && 코드줄들.length > 0 {
for 줄 in 코드블록줄들(블록번호, 코드줄들, 한계) { 결과.push(줄) }
}
결과
}
// 제목 종류인가.
// Is this a heading kind?
function 제목인가(종류: 줄종류) -> bool {
match 종류 {
case "제목1" => true
case "제목2" => true
case "제목3" => true
case "문단" => false
case "인용" => false
case "목록" => false
case "코드" => false
case "구분" => false
}
}
// 색인 줄이 다음 줄과 함께 있어야 하는 최소 줄 수(고아/미망 방지). 여러 줄 블록의 첫 줄은 다음 줄과,
// 끝에서 둘째 줄은 마지막 줄과 함께 둔다.
// Minimum lines that must stay together starting at 색인 (orphan/widow). A multi-line block's first
// line stays with the next; its second-to-last stays with the last.
function 기본묶음(줄들: Array<줄>, 색인: int, 총: int) -> int {
let 줄 = 줄들[색인]
if 줄.시작 && 줄.블록줄수 > 1 { return 2 }
if 색인 + 1 < 총 {
let 다음 = 줄들[색인 + 1]
if 다음.끝 && !다음.시작 && 다음.블록 == 줄.블록 && 줄.블록줄수 > 1 { return 2 }
}
1
}
// 색인 줄을 둘 때 페이지에 있어야 하는 최소 줄 수. 제목은 자신 + 다음 줄의 묶음과 함께 있어야 한다
// (keep-with-next, 다음이 여러 줄 블록의 첫 줄이면 그 줄도 고아가 안 되게 한 줄 더).
// Lines needed when placing 색인. A heading needs itself plus the next line's group (keep-with-next,
// and one extra so the next line is not orphaned).
function 묶음크기(줄들: Array<줄>, 색인: int, 총: int) -> int {
let 줄 = 줄들[색인]
if 제목인가(줄.종류) {
let mut 끝 = 색인
while 끝 + 1 < 총 && 줄들[끝 + 1].블록 == 줄.블록 { 끝 = 끝 + 1 }
let 블록남음 = 끝 - 색인 + 1
let 다음색인 = 끝 + 1
// 다음 줄도 제목이면 그 제목의 keep-with-next까지 더해(연속 제목 전파), 아니면 다음 줄의 기본 묶음.
// If the next line is also a heading, include its keep-with-next too (transitive across a run).
if 다음색인 < 총 { return 블록남음 + 묶음크기(줄들, 다음색인, 총) }
return 블록남음
}
기본묶음(줄들, 색인, 총)
}
// 줄들을 높이만큼의 페이지로 나눈다. 한 줄을 둘 때 그 줄의 묶음이 남은 공간에 안 들어가면 새 페이지로
// 넘긴다. 용량을 항상 지켜 어떤 페이지도 높이를 넘지 않으며, keep-with-next/고아/미망이 함께 보장된다.
// Paginate into pages of the given height. Break before a line if its required group does not fit in
// the remaining space. Capacity is always honored, and keep-with-next/orphan/widow hold together.
function 배치(줄들: Array<줄>, 높이: int) -> Array<Array<줄>> {
let mut 페이지들: Array<Array<줄>> = []
let mut 현재: Array<줄> = []
let 총 = 줄들.length
let mut 색인 = 0
while 색인 < 총 {
let mut 끊기 = false
if 현재.length > 0 {
let 남은 = 높이 - 현재.length
let 필요 = 묶음크기(줄들, 색인, 총)
let 맞춤필요 = if 필요 > 높이 { 높이 } else { 필요 }
if 남은 <= 0 { 끊기 = true }
else if 남은 < 맞춤필요 { 끊기 = true }
}
if 끊기 { 페이지들.push(현재); 현재 = [] }
현재.push(줄들[색인])
색인 = 색인 + 1
}
if 현재.length > 0 { 페이지들.push(현재) }
페이지들
}
// 한 줄을 종류에 맞춰 그린다(모두 같은 줄높이). 인라인 서식 줄은 이미 그려진 HTML이다.
// Render one line per kind (same line height). Markup lines carry already-rendered HTML.
function 줄그리기(줄: 줄) -> string {
let 종류 = 줄.종류
match 종류 {
case "구분" => return "<div class=\"mp-hr\"></div>"
case "코드" => {
let 안전 = if 줄.글 == "" { " " } else { 이스케이프(줄.글) }
return "<div class=\"mp-code\">" + 안전 + "</div>"
}
case "제목1" => {}
case "제목2" => {}
case "제목3" => {}
case "인용" => {}
case "문단" => {}
case "목록" => {}
}
let 안전 = if 줄.글 == "" { " " } else { 줄.글 }
if 제목인가(종류) { "<div class=\"mp-h\">" + 안전 + "</div>" }
else if 종류 == "인용" { "<div class=\"mp-quote\">" + 안전 + "</div>" }
else { "<div class=\"mp-line\">" + 안전 + "</div>" }
}
// 페이지 하나를 고정 크기 박스로 그린다(높이 고정, 넘침 숨김으로 절대 커지지 않음).
// Render one page as a fixed-size box (fixed height + overflow hidden, never grows).
function 페이지그리기(페이지: Array<줄>, 번호: int, 높이: int) -> string {
let mut 본문 = ""
for 줄 in 페이지 { 본문 = 본문 + 줄그리기(줄) }
let 박스높이 = 높이 * 1
let 스타일 = "width:{폭}ch;height:{박스높이}em;font-family:'Nanum Gothic Coding','D2Coding',monospace;line-height:1em;background:#fff;border:1px solid #111;padding:1.4em 1.4em 2.2em;margin:0 auto 18px;position:relative;white-space:pre-wrap;overflow:hidden"
"<div class=\"mp-page\" style=\"" + 스타일 + "\">" + 본문 + "<div style=\"position:absolute;bottom:0.6em;right:1.4em;color:#888;font-size:0.85em\">{번호}</div></div>"
}
function 처리(입력: string) -> string {
let 입력줄들 = 입력.split("\n")
if 입력줄들.length == 0 {
return "<div class=\"meta\">첫 줄에 페이지 높이(줄 수), 다음 줄부터 마크다운을 입력하세요.</div>"
}
let 높이글 = 입력줄들[0].trim()
if 높이글 == "" {
return "<div class=\"meta\">첫 줄에 페이지 높이(줄 수), 다음 줄부터 마크다운을 입력하세요.</div>"
}
let mut 숫자만 = true
for 글 in 높이글.scalars() { if !숫자글(글) { 숫자만 = false } }
if !숫자만 {
return "<div class=\"err\">페이지 높이는 양의 정수여야 합니다.</div>"
}
let 높이 = toInt(높이글) ?? 0
if 높이 < 3 {
return "<div class=\"err\">페이지 높이는 3 이상이어야 합니다.</div>"
}
if 높이 > 200 {
return "<div class=\"err\">페이지 높이는 200 이하여야 합니다.</div>"
}
let mut 마크다운 = ""
let mut 줄차례 = 1
while 줄차례 < 입력줄들.length {
마크다운 = 마크다운 + 입력줄들[줄차례]
if 줄차례 < 입력줄들.length - 1 { 마크다운 = 마크다운 + "\n" }
줄차례 = 줄차례 + 1
}
let 줄들 = 마크다운파싱(마크다운, 폭)
if 줄들.length == 0 {
return "<div class=\"meta\">마크다운 본문을 입력하세요.</div>"
}
let 페이지들 = 배치(줄들, 높이)
let mut 결과 = "<div class=\"meta\">{페이지들.length}쪽, 페이지당 {높이}줄</div><div class=\"mp-pages\">"
let mut 쪽차례 = 0
while 쪽차례 < 페이지들.length {
결과 = 결과 + 페이지그리기(페이지들[쪽차례], 쪽차례 + 1, 높이)
쪽차례 = 쪽차례 + 1
}
결과 + "</div>"
}
print(처리(input()))