typeset.tpz
// 여러 문단을 Knuth-Plass 최적 줄나눔으로 배치하고, 페이지로 나눠 과부와 고아를 방지한다.
// 한국어 금칙(줄 처음/끝 금지 문자)을 지키고 각 문단의 마지막 줄을 뺀 모든 줄을 양끝맞춤해
// 페이지마다 SVG 한 상자로 그린다.
// Multi-paragraph typesetter. Optimal (Knuth-Plass) line breaking per paragraph with Korean
// line-break prohibition (금칙), pagination with widow/orphan (과부/고아) control, justification of
// every line but each paragraph's last, rendered as one SVG box per page.
//
// 폭 모델은 고정폭이다: 라틴/공백 1칸, 한글/CJK 2칸. 폰트 엔진을 쓰지 않는다(비례 폰트 메트릭은
// 범위 밖). 배드니스 계산에 toFloat를 쓴다(이 앱이 견인한 기능).
// Width model is fixed-width: Latin/space = 1 unit, Hangul/CJK = 2 units. No font engine
// (proportional metrics are out of scope). Badness math uses toFloat (the feature this app drives).
//
// 과부/고아 규칙: 한 문단의 첫 줄을 페이지 맨 아래 한 칸에 혼자 두지 않는다(고아). 한 문단의 마지막
// 줄을 다음 페이지 맨 위에 혼자 두지 않도록 앞 줄을 함께 내린다(과부). 두 줄 이상인 문단에만 쓴다.
// Widow/orphan: never leave a paragraph's first line alone in a page's last slot (orphan); never
// leave a paragraph's last line alone atop the next page (widow), pulling the previous line down.
// Applied to paragraphs of two or more lines only.
//
// Key identifiers: 조각=piece, 텍스트=text, 너비=width, 못시작=cannotStartLine, 못끝=cannotEndLine,
// 앞공백=spaceBefore, 조각내기=splitPieces, 분할가능=breakAllowed, 라인너비=lineWidth,
// 배드니스=badness, 최적=best, 부모=parent, 줄=line, 양끝맞춤=justify, 단위=unitPx,
// 이스케이프=escape, 시제이케이=isCJK, 너비글=charWidth, 금칙무시=ignoredRules,
// 문단처리=layOutParagraph, 문단결과=paragraphResult, 배치줄=placedLine, 위치=indexInParagraph,
// 문단줄수=paragraphLineCount, 페이지높이=linesPerPage, 고아막음=orphansPrevented,
// 과부막음=widowsPrevented, 앞부분=dropLast, 페이지그리기=drawPage, 전체그림=drawAllPages
//
// 결정적이다. 같은 입력이면 인터프리터와 네이티브/wasm 빌드에서 같은 SVG가 나온다.
// Deterministic: same input yields the same SVG across interpreter and builds.
// 안 하는 것: 비례 폰트 메트릭, 하이프네이션, 리치텍스트.
// Won't do: proportional font metrics, hyphenation, rich text.
type 조각 = { 텍스트: string, 너비: int, 못시작: bool, 못끝: bool, 앞공백: bool }
type 나눔이유 = "none" | "full" | "orphan"
let 무한 = 1000000000000.0
function 나눔이유정하기(꽉참: bool, 고아조건: bool) -> 나눔이유 {
if 꽉참 { "full" } else if 고아조건 { "orphan" } else { "none" }
}
// HTML 이스케이프. 본문은 사용자 입력이라 출력 전에 반드시 이스케이프한다.
// HTML escape. The paragraph is user input, so always escape before output.
function 이스케이프(원문: string) -> string {
let mut 결과 = ""
for 글 in 원문.scalars() {
if 글 == "&" { 결과 = 결과 + "&" }
else if 글 == "<" { 결과 = 결과 + "<" }
else if 글 == ">" { 결과 = 결과 + ">" }
else if 글 == "\"" { 결과 = 결과 + """ }
else { 결과 = 결과 + 글 }
}
결과
}
// 숫자(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 (Hangul, CJK ideographs, CJK punctuation)? It takes 2 fixed-width units.
function 시제이케이(글: string) -> bool {
let 코드 = 글.codePointAt(0) ?? 0
(코드 >= 44032 && 코드 <= 55203) // 한글 음절 / Hangul syllables
|| (코드 >= 12592 && 코드 <= 12687) // 한글 호환 자모 / Hangul compatibility jamo
|| (코드 >= 4352 && 코드 <= 4607) // 한글 자모 / Hangul jamo
|| (코드 >= 19968 && 코드 <= 40959) // CJK 한자 / CJK ideographs
|| (코드 >= 12288 && 코드 <= 12351) // CJK 기호/문장부호 / CJK symbols & punctuation
|| (코드 >= 65280 && 코드 <= 65519) // 전각 영숫자 / fullwidth forms
}
// 한 글자의 고정폭 너비.
// Fixed-width width of one scalar.
function 너비글(글: string) -> int {
if 시제이케이(글) { 2 } else { 1 }
}
// 줄 처음에 올 수 없는 문자(금칙): 닫는 괄호류와 뒤따르는 문장부호.
// Cannot begin a line (금칙): closing brackets and trailing punctuation.
function 못시작글(글: string) -> bool {
글 == ")" || 글 == "]" || 글 == "\}" || 글 == "」" || 글 == "』" || 글 == "›" || 글 == "》"
|| 글 == "," || 글 == "." || 글 == "?" || 글 == "!" || 글 == ";" || 글 == ":"
|| 글 == "。" || 글 == "、" || 글 == "," || 글 == "." || 글 == "?" || 글 == "!"
|| 글 == ":" || 글 == ";" || 글 == "”" || 글 == "’"
}
// 줄 끝에 올 수 없는 문자(금칙): 여는 괄호류.
// Cannot end a line (금칙): opening brackets.
function 못끝글(글: string) -> bool {
글 == "(" || 글 == "[" || 글 == "\{" || 글 == "「" || 글 == "『" || 글 == "‹" || 글 == "《"
|| 글 == "“" || 글 == "‘" || 글 == "("
}
// 본문을 조각들로 나눈다. 라틴 낱말은 한 조각, CJK 글자는 글자마다 한 조각이다. 공백은 글루로,
// 조각의 앞공백 표시로만 남긴다(줄 끝의 공백은 그릴 때 버린다).
// Split text into pieces: a Latin word is one piece, each CJK char is its own piece. Spaces become
// glue, recorded as a piece's 앞공백 flag (a trailing space at a break is dropped when drawing).
function 조각내기(본문: string) -> Array<조각> {
let mut 조각들: Array<조각> = []
let mut 라틴 = ""
let mut 라틴너비 = 0
let mut 라틴앞공백 = false
let mut 라틴첫 = ""
let mut 라틴끝 = ""
let mut 공백대기 = false
for 글 in 본문.scalars() {
let 코드 = 글.codePointAt(0) ?? 0
let 공백 = 코드 == 32 || 코드 == 9 || 코드 == 10 || 코드 == 13
if 공백 {
if 라틴 != "" {
조각들.push({ 텍스트: 라틴, 너비: 라틴너비, 못시작: 못시작글(라틴첫), 못끝: 못끝글(라틴끝), 앞공백: 라틴앞공백 })
라틴 = ""; 라틴너비 = 0
}
공백대기 = true
} else if 시제이케이(글) {
if 라틴 != "" {
조각들.push({ 텍스트: 라틴, 너비: 라틴너비, 못시작: 못시작글(라틴첫), 못끝: 못끝글(라틴끝), 앞공백: 라틴앞공백 })
라틴 = ""; 라틴너비 = 0
}
조각들.push({ 텍스트: 글, 너비: 2, 못시작: 못시작글(글), 못끝: 못끝글(글), 앞공백: 공백대기 })
공백대기 = false
} else {
if 라틴 == "" { 라틴앞공백 = 공백대기; 라틴첫 = 글; 공백대기 = false }
라틴 = 라틴 + 글
라틴너비 = 라틴너비 + 1
라틴끝 = 글
}
}
if 라틴 != "" {
조각들.push({ 텍스트: 라틴, 너비: 라틴너비, 못시작: 못시작글(라틴첫), 못끝: 못끝글(라틴끝), 앞공백: 라틴앞공백 })
}
조각들
}
// 줄 너비: 조각 a..b의 너비 합 + 내부 공백(앞공백) 칸. 줄 처음의 공백은 버린다.
// Line width: sum of piece widths a..b plus interior space units. A leading space is dropped.
function 라인너비(조각들: Array<조각>, 너비누적: Array<int>, 공백누적: Array<int>, 에이: int, 비: int) -> int {
let 폭합 = 너비누적[비 + 1] - 너비누적[에이]
let 공백수 = 공백누적[비 + 1] - 공백누적[에이 + 1]
폭합 + 공백수
}
// 배드니스: 목표에서 벗어난 만큼의 제곱(작을수록 좋다). float로 계산해 누적한다.
// Badness: squared deviation from the target (smaller is better). Computed and summed as float.
function 배드니스(목표: int, 폭: int) -> float {
let 차 = if 폭 > 목표 { 폭 - 목표 } else { 목표 - 폭 }
let 차에프 = toFloat(차)
if 폭 > 목표 { 차에프 * 차에프 * 8.0 } else { 차에프 * 차에프 }
}
// 줄 끝(글자열) 결과: 줄마다 (시작조각, 끝조각).
// Line spans: each line is (firstPiece, lastPiece).
type 줄 = { 시작: int, 끝: int }
// Knuth-Plass DP. 분할가능[k] = 조각 k-1과 k 사이에서 줄을 바꿔도 되는가(금칙 반영). 양끝(0, n)은 참.
// Knuth-Plass DP. 분할가능[k] = may break between piece k-1 and k (respects 금칙). Ends (0, n) true.
function 줄나누기(조각들: Array<조각>, 너비누적: Array<int>, 공백누적: Array<int>, 분할가능: Array<bool>, 목표: int) -> Array<줄> {
let 엔 = 조각들.length
let mut 최적: Array<float> = []
let mut 부모: Array<int> = []
let mut 채움 = 0
while 채움 <= 엔 { 최적.push(무한); 부모.push(-1); 채움 = 채움 + 1 }
최적[0] = 0.0
let mut 아이 = 1
while 아이 <= 엔 {
// 줄의 끝 끊기(조각 아이-1 다음)가 합법이어야 한다.
// The end break (after piece 아이-1) must be legal.
if 분할가능[아이] {
let mut 제이 = 아이 - 1
let mut 계속 = true
while 제이 >= 0 && 계속 {
let 폭 = 라인너비(조각들, 너비누적, 공백누적, 제이, 아이 - 1)
let 단일 = 아이 - 1 == 제이
if 폭 > 목표 && !단일 {
계속 = false
} else {
if 최적[제이] < 무한 {
let 마지막 = 아이 == 엔
let mut 비용 = 0.0
if !마지막 { 비용 = 배드니스(목표, 폭) }
let 합 = 최적[제이] + 비용
if 합 < 최적[아이] { 최적[아이] = 합; 부모[아이] = 제이 }
}
if 폭 > 목표 && 단일 { 계속 = false }
}
제이 = 제이 - 1
}
}
아이 = 아이 + 1
}
// 끝에 도달하는 합법 경로가 없으면(금칙 때문에 폭이 너무 좁음) 빈 결과로 알려 호출부가 금칙을 완화하게 한다.
// If no legal path reaches the end (금칙 makes the width infeasible) return empty so the caller relaxes 금칙.
if 최적[엔] >= 무한 {
let mut 없음: Array<줄> = []
return 없음
}
// 역추적. 부모를 따라 줄 경계를 모은다.
// Backtrack: collect line spans by following parents.
let mut 뒤집힘: Array<줄> = []
let mut 끝 = 엔
while 끝 > 0 {
let 시 = 부모[끝]
뒤집힘.push({ 시작: 시, 끝: 끝 - 1 })
끝 = 시
}
let mut 줄들: Array<줄> = []
let mut 인덱스 = 뒤집힘.length - 1
while 인덱스 >= 0 { 줄들.push(뒤집힘[인덱스]); 인덱스 = 인덱스 - 1 }
줄들
}
// 한 문단의 줄나눔 결과: 조각들과 줄 경계, 금칙 완화 여부.
// One paragraph's layout: its pieces, line spans, and whether 금칙 was relaxed.
type 문단결과 = { 조각들: Array<조각>, 줄들: Array<줄>, 금칙무시: bool }
// 한 문단을 조각내고 Knuth-Plass로 줄을 나눈다. 폭이 좁아 금칙을 다 지킬 수 없으면 금칙을 풀고 다시 나눈다.
// Lay out one paragraph: split into pieces and break lines (Knuth-Plass). If the width is too narrow
// for 금칙, relax 금칙 and retry.
function 문단처리(문단본문: string, 목표: int) -> 문단결과 {
let 조각들 = 조각내기(문단본문)
if 조각들.length == 0 {
let mut 빈줄: Array<줄> = []
return { 조각들: 조각들, 줄들: 빈줄, 금칙무시: false }
}
let 엔 = 조각들.length
let mut 너비누적: Array<int> = []
let mut 공백누적: Array<int> = []
너비누적.push(0); 공백누적.push(0)
let mut 채움 = 0
while 채움 < 엔 {
너비누적.push(너비누적[채움] + 조각들[채움].너비)
let mut 더 = 0
if 조각들[채움].앞공백 { 더 = 1 }
공백누적.push(공백누적[채움] + 더)
채움 = 채움 + 1
}
let mut 분할가능: Array<bool> = []
let mut 비 = 0
while 비 <= 엔 {
if 비 == 0 || 비 == 엔 { 분할가능.push(true) }
else { 분할가능.push(!조각들[비 - 1].못끝 && !조각들[비].못시작) }
비 = 비 + 1
}
let mut 줄들 = 줄나누기(조각들, 너비누적, 공백누적, 분할가능, 목표)
let mut 금칙무시 = false
if 줄들.length == 0 {
let mut 모두가능: Array<bool> = []
let mut 시 = 0
while 시 <= 엔 { 모두가능.push(true); 시 = 시 + 1 }
줄들 = 줄나누기(조각들, 너비누적, 공백누적, 모두가능, 목표)
금칙무시 = true
}
{ 조각들: 조각들, 줄들: 줄들, 금칙무시: 금칙무시 }
}
// 페이지에 놓인 한 줄. 어느 문단의 몇 번째 줄인지(과부/고아 판정용)와 줄 경계를 담는다.
// A line placed on a page: which paragraph and its index in that paragraph (for widow/orphan), plus its span.
type 배치줄 = { 문단: int, 시작: int, 끝: int, 위치: int, 문단줄수: int }
// 배열의 마지막 원소를 뺀 새 배열(과부 방지에서 끝앞 줄을 다음 페이지로 내릴 때 쓴다).
// A new array without the last element (used when the widow rule moves the next-to-last line down).
function 앞부분(원: Array<배치줄>) -> Array<배치줄> {
if 원.length == 0 {
[]
} else {
원.slice(0, 원.length - 1)
}
}
function 빈페이지() -> Array<배치줄> {
let mut 비: Array<배치줄> = []
비
}
// 한 줄을 SVG로 그린다. 양끝맞춤이면 남는 폭을 내부 경계에 고르게 나눈다(문단의 마지막 줄은 제외).
// Draw one line as SVG. When justified, spread the leftover width over interior boundaries (not the
// paragraph's last line).
function 줄그리기(조각들: Array<조각>, 줄정보: 줄, 목표: int, 마지막: bool, 단위: float, 세로: float) -> string {
let 에이 = 줄정보.시작
let 비 = 줄정보.끝
let mut 폭합 = 0
let mut 공백수 = 0
let mut 칸 = 에이
while 칸 <= 비 {
폭합 = 폭합 + 조각들[칸].너비
if 칸 > 에이 && 조각들[칸].앞공백 { 공백수 = 공백수 + 1 }
칸 = 칸 + 1
}
let 자연폭 = 폭합 + 공백수
let 경계수 = 비 - 에이
let mut 늘림 = 0.0
if !마지막 && 경계수 > 0 && 자연폭 < 목표 {
늘림 = toFloat(목표 - 자연폭) / toFloat(경계수)
}
let mut 본문 = ""
let mut 엑스 = 0.0
let mut 케이 = 에이
while 케이 <= 비 {
if 케이 > 에이 {
let mut 사이 = 0.0
if 조각들[케이].앞공백 { 사이 = 1.0 }
엑스 = 엑스 + (사이 + 늘림) * 단위
}
let 픽셀와이 = 세로
본문 = 본문 + "<text x=\"{엑스}\" y=\"{픽셀와이}\" font-size=\"15\" font-family=\"monospace\" fill=\"#111\">{이스케이프(조각들[케이].텍스트)}</text>"
엑스 = 엑스 + toFloat(조각들[케이].너비) * 단위
케이 = 케이 + 1
}
본문
}
// 한 페이지를 SVG 한 상자로 그린다. 페이지 라벨과 양끝맞춤 목표를 보여주는 옅은 오른쪽 경계선을 넣는다.
// Draw one page as one SVG box, with a page label and a faint right margin line at the target width.
function 페이지그리기(페이지: Array<배치줄>, 문단결과들: Array<문단결과>, 목표: int, 단위: float, 줄높이: float, 페이지높이: int, 페이지번호: int) -> string {
let 본문폭 = toFloat(목표) * 단위 + 2.0
let 박스높이 = toFloat(페이지높이) * 줄높이 + 28.0
let mut 본문 = ""
본문 = 본문 + "<text x=\"2\" y=\"14\" font-size=\"11\" font-family=\"monospace\" fill=\"#888\">{페이지번호}페이지</text>"
본문 = 본문 + "<line x1=\"{본문폭 - 2.0}\" y1=\"20\" x2=\"{본문폭 - 2.0}\" y2=\"{박스높이}\" stroke=\"#ddd\" />"
let mut 줄번 = 0
while 줄번 < 페이지.length {
let 항 = 페이지[줄번]
let 조각들 = 문단결과들[항.문단].조각들
let 마지막 = 항.위치 == 항.문단줄수 - 1
let 세로 = toFloat(줄번) * 줄높이 + 38.0
본문 = 본문 + 줄그리기(조각들, { 시작: 항.시작, 끝: 항.끝 }, 목표, 마지막, 단위, 세로)
줄번 = 줄번 + 1
}
"<svg viewBox=\"0 0 {본문폭} {박스높이}\" width=\"100%\" style=\"border:1px solid #bbb;margin-top:10px;display:block\">" + 본문 + "</svg>"
}
function 전체그림(페이지들: Array<Array<배치줄>>, 문단결과들: Array<문단결과>, 목표: int, 페이지높이: int) -> string {
let 단위 = 9.0
let 줄높이 = 24.0
let mut 본문 = ""
let mut 피 = 0
while 피 < 페이지들.length {
본문 = 본문 + 페이지그리기(페이지들[피], 문단결과들, 목표, 단위, 줄높이, 페이지높이, 피 + 1)
피 = 피 + 1
}
본문
}
function 처리(입력: string) -> string {
// 입력이 너무 길면 거른다(O(폭) 폭의 줄나눔이라도 글자 수가 크면 느려질 수 있다).
// Reject very long input (line breaking stays bounded but huge text can still be slow).
if 입력.scalars().length > 12000 {
return "<div class=\"err\">입력이 너무 깁니다. 12000자 이하로 줄여주세요.</div>"
}
let 줄목록 = 입력.split("\n")
let 첫줄 = 줄목록[0].trim()
if 첫줄 == "" {
return "<div class=\"meta\">첫 줄에 줄 너비와 선택 페이지 줄 수를 적고, 다음 줄부터 문단을 입력하세요. 문단은 빈 줄로 구분합니다.</div>"
}
let 토큰들 = filter(첫줄.split(" "), 토 => 토 != "")
if 토큰들.length > 2 {
return "<div class=\"err\">첫 줄은 줄 너비와 선택 페이지 줄 수, 최대 두 개입니다.</div>"
}
let 폭문자 = 토큰들[0]
let mut 숫자만 = true
for 글 in 폭문자.scalars() { if !숫자글(글) { 숫자만 = false } }
if !숫자만 {
return "<div class=\"err\">줄 너비는 양의 정수여야 합니다.</div>"
}
if 폭문자.scalars().length > 4 {
return "<div class=\"err\">줄 너비가 너무 큽니다.</div>"
}
let 목표 = toInt(폭문자) ?? 0
if 목표 < 1 {
return "<div class=\"err\">줄 너비는 1 이상이어야 합니다.</div>"
}
let mut 페이지높이 = 6
let mut 높이경고 = false
if 토큰들.length >= 2 {
let 높이문자 = 토큰들[1]
let mut 높이숫자만 = true
for 글 in 높이문자.scalars() { if !숫자글(글) { 높이숫자만 = false } }
if !높이숫자만 {
return "<div class=\"err\">페이지 줄 수는 양의 정수여야 합니다.</div>"
}
if 높이문자.scalars().length > 3 {
return "<div class=\"err\">페이지 줄 수가 너무 큽니다.</div>"
}
페이지높이 = toInt(높이문자) ?? 6
if 페이지높이 < 2 { 페이지높이 = 2; 높이경고 = true }
if 페이지높이 > 30 { 페이지높이 = 30 }
}
// 문단 분리: 빈 줄로 나누고, 한 문단의 여러 줄은 공백으로 잇는다.
// Split paragraphs on blank lines; join a paragraph's lines with a space.
let mut 문단들: Array<string> = []
let mut 현재 = ""
let mut 줄인덱스 = 1
while 줄인덱스 < 줄목록.length {
let 줄 = 줄목록[줄인덱스]
if 줄.trim() == "" {
if 현재 != "" { 문단들.push(현재); 현재 = "" }
} else {
if 현재 != "" { 현재 = 현재 + " " }
현재 = 현재 + 줄
}
줄인덱스 = 줄인덱스 + 1
}
if 현재 != "" { 문단들.push(현재) }
if 문단들.length == 0 {
return "<div class=\"meta\">문단을 입력하면 최적 줄나눔과 페이지 배치로 보여줍니다.</div>"
}
// 문단별 줄나눔.
// Lay out each paragraph.
let mut 문단결과들: Array<문단결과> = []
let mut 금칙무시any = false
let mut 피 = 0
while 피 < 문단들.length {
let 결과 = 문단처리(문단들[피], 목표)
문단결과들.push(결과)
if 결과.금칙무시 { 금칙무시any = true }
피 = 피 + 1
}
// 평탄화: 모든 문단의 줄을 한 시퀀스로, 문단 정보(위치/문단줄수)와 함께 담는다.
// Flatten every paragraph's lines into one sequence with paragraph metadata.
let mut 시퀀스: Array<배치줄> = []
let mut 피2 = 0
while 피2 < 문단결과들.length {
let 줄들 = 문단결과들[피2].줄들
let mut 라 = 0
while 라 < 줄들.length {
시퀀스.push({ 문단: 피2, 시작: 줄들[라].시작, 끝: 줄들[라].끝, 위치: 라, 문단줄수: 줄들.length })
라 = 라 + 1
}
피2 = 피2 + 1
}
if 시퀀스.length == 0 {
return "<div class=\"meta\">문단을 입력하면 최적 줄나눔과 페이지 배치로 보여줍니다.</div>"
}
// 페이지네이션 + 과부/고아. 고아: 다줄 문단의 첫 줄을 페이지 맨 아래 한 칸에 두지 않는다.
// 과부: 다줄 문단의 끝앞 줄을 두었더니 페이지가 꽉 차면, 그 줄을 다음 페이지로 내려 마지막 두 줄을 붙인다.
// Pagination with widow/orphan. Orphan: keep a multi-line paragraph's first line off a page's last
// slot. Widow: if placing the next-to-last line fills the page, move it down so the last two lines stay together.
let mut 페이지들: Array<Array<배치줄>> = []
let mut 현재페이지: Array<배치줄> = 빈페이지()
let mut 고아막음 = 0
let mut 과부막음 = 0
let mut 엘 = 0
while 엘 < 시퀀스.length {
let 항 = 시퀀스[엘]
let 첫줄여부 = 항.위치 == 0
let 다줄 = 항.문단줄수 >= 2
let 꽉참 = 현재페이지.length == 페이지높이
let 고아조건 = 첫줄여부 && 다줄 && 현재페이지.length == 페이지높이 - 1
let 이유 = 나눔이유정하기(꽉참, 고아조건)
match 이유 {
case "full" => {
페이지들.push(현재페이지)
현재페이지 = 빈페이지()
}
case "orphan" => {
고아막음 = 고아막음 + 1
페이지들.push(현재페이지)
현재페이지 = 빈페이지()
}
case "none" => {}
}
현재페이지.push(항)
let 끝앞 = 항.위치 == 항.문단줄수 - 2
if 끝앞 && 다줄 && 현재페이지.length == 페이지높이 {
현재페이지 = 앞부분(현재페이지)
페이지들.push(현재페이지)
let mut 새페이지: Array<배치줄> = 빈페이지()
새페이지.push(항)
현재페이지 = 새페이지
과부막음 = 과부막음 + 1
}
엘 = 엘 + 1
}
if 현재페이지.length > 0 { 페이지들.push(현재페이지) }
let mut 머리 = "<div class=\"meta\">{페이지들.length}페이지, 너비 {목표}칸, 페이지당 {페이지높이}줄. 고아 {고아막음}건과 과부 {과부막음}건을 방지했습니다"
if 높이경고 { 머리 = 머리 + ". 페이지 줄 수는 최소 2이므로 2로 맞췄습니다" }
if 금칙무시any { 머리 = 머리 + ". 폭이 좁아 일부 문단에서 금칙을 완화했습니다" }
머리 = 머리 + "</div>"
머리 + 전체그림(페이지들, 문단결과들, 목표, 페이지높이)
}
print(처리(input()))