STUDIO HAZE · jeniapp
Topaz source
Open by house policy. Topaz is our own language.
Topaz 5.20.2 · 63 files
apps/md-pdf/mdpdf.tpz
Topaz · 19241 B · Original file
1
// 마크다운 쪽 나눔. 마크다운을 고정 크기 쪽으로 흘리며 제목을 다음 줄과 붙이고 고아와 과부를 막습니다.2
// 입력 첫 줄은 쪽당 줄 수이고 다음 줄부터 마크다운입니다. 출력은 요약 한 줄과 쪽마다 고정 크기 상자 하나를 담은 HTML 조각입니다.3
// 같은 입력이면 인터프리터와 웹 빌드에서 바이트까지 같은 출력을 냅니다.4
// 너비는 반각 1칸, 전각 2칸의 고정폭이고 굵게와 기울임 표지는 너비에 세지 않습니다. PDF 바이트는 만들지 않고 저장은 브라우저 인쇄에 맡깁니다.5
// 표, 이미지, 링크, 인라인 HTML, 두 단계 이상 중첩 목록, 글꼴 너비는 다루지 않습니다.6
// Markdown paginator: fixed-width pages with keep-with-next headings and widow and orphan control.8
enum 줄종류 { 문단, 제목1, 제목2, 제목3, 인용, 목록, 코드, 구분 }9
enum 서식항목 { 굵게표, 기울임표, 글자(string) }10
record 서식 { 굵게: bool, 기울임: bool }11
record 셀 { 글: string, 꾸밈: 서식, 너비: int }12
// 표지는 목록의 첫 줄 앞에 붙는 글이고 원줄들은 코드 블록에서만 씁니다.13
record 블록 { 종류: 줄종류, 글: string, 표지: string, 원줄들: Array<string> }14
// 서식 줄의 글은 이미 이스케이프해 그린 HTML 이고 코드 줄의 글만 원문입니다.15
record 줄 { 글: string, 종류: 줄종류, 블록: int, 시작: bool, 끝: bool, 블록줄수: int }17
let 줄너비 = 6018
let 보통서식 = 서식 { 굵게: false, 기울임: false }19
let 높이안내 = "<div class=\"meta\">첫 줄에 페이지 높이(줄 수), 다음 줄부터 마크다운을 입력하세요.</div>"21
// 공용 도우미 시작 (이스케이프 전각인가 글자너비 숫자인가 모두숫자인가 오류알림 결과풀기 연속끝)22
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.24
// 사용자 입력을 HTML 에 넣기 전에 반드시 거칩니다.25
function 이스케이프(원문: string) -> string {26
원문.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)27
}29
// 고정폭에서 두 칸을 차지하는 범위입니다. 한글 음절, 호환 자모, 자모, 한자, CJK 문장부호, 전각 영숫자 순입니다.30
// 코드포인트를 10진수로 적습니다. 토파즈 과제 '16진 정수 리터럴' 의 우회입니다.31
function 전각인가(글자: string) -> bool {32
let 코드 = 글자.codePointAt(0) ?? 033
(코드 >= 44032 && 코드 <= 55203)34
|| (코드 >= 12592 && 코드 <= 12687)35
|| (코드 >= 4352 && 코드 <= 4607)36
|| (코드 >= 19968 && 코드 <= 40959)37
|| (코드 >= 12288 && 코드 <= 12351)38
|| (코드 >= 65280 && 코드 <= 65519)39
}41
function 글자너비(글자: string) -> int {42
if 전각인가(글자) { 2 } else { 1 }43
}45
function 숫자인가(글자: string) -> bool {46
let 코드 = 글자.codePointAt(0) ?? 047
코드 >= 48 && 코드 <= 5748
}50
// 빈 문자열은 참입니다. 부호와 소수점을 받지 않으려고 toInt 앞에서 씁니다.51
function 모두숫자인가(글: string) -> bool {52
글.scalars().reduce(true, (모두, 글자) => 모두 && 숫자인가(글자))53
}55
// 문구는 고정 문장이거나 이미 이스케이프한 값이어야 합니다.56
function 오류알림(문구: string) -> string {57
"<div class=\"err\">{문구}</div>"58
}60
// 성공 값과 안내가 모두 바로 보여 줄 HTML 조각일 때 진입점에서 한 번 풉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.61
function 결과풀기(값: Result<string, string>) -> string {62
match 값 {63
case Ok(결과) => 결과64
case Err(안내) => 안내65
}66
}68
// 시작부터 맞는가가 참인 글자가 이어지는 끝 위치를 돌려줍니다. 맞는 글자가 없으면 시작 그대로입니다.69
function 연속끝(글자들: Array<string>, 시작: int, 맞는가: (string) -> bool) -> int {70
let mut 끝 = 시작71
while 끝 < 글자들.length && 맞는가(글자들[끝]) { 끝 += 1 }72
끝73
}75
// 공용 도우미 끝77
function 셀만들기(글자: string, 꾸밈: 서식) -> 셀 {78
셀 { 글: 글자, 꾸밈: 꾸밈, 너비: 글자너비(글자) }79
}81
function 셀폭(셀들: Array<셀>) -> int {82
셀들.reduce(0, (합, 셀값) => 합 + 셀값.너비)83
}85
// ** 를 * 보다 먼저 읽으므로 *** 는 굵게표 다음 기울임표가 됩니다.86
function 서식항목나누기(글자들: Array<string>) -> Array<서식항목> {87
let mut 항목들: Array<서식항목> = []88
let mut 위치 = 089
while 위치 < 글자들.length {90
if 글자들[위치] == "*" && 위치 + 1 < 글자들.length && 글자들[위치 + 1] == "*" {91
항목들.push(서식항목.굵게표)92
위치 += 293
} else if 글자들[위치] == "*" {94
항목들.push(서식항목.기울임표)95
위치 += 196
} else {97
항목들.push(서식항목.글자(글자들[위치]))98
위치 += 199
}100
}101
항목들102
}104
// 표지 수가 홀수면 마지막 표지 하나만 짝이 없습니다. 짝이 맞으면 -1 입니다.105
function 짝없는자리(자리들: Array<int>) -> int {106
if 자리들.length % 2 == 1 { 자리들[자리들.length - 1] } else { -1 }107
}109
// 짝이 맞는 표지는 서식을 켜고 끄며 셀을 남기지 않고, 짝이 없는 표지는 별표 글자로 남습니다.110
function 셀분해(원문: string) -> Array<셀> {111
let 항목들 = 서식항목나누기(원문.scalars())112
let mut 굵게표자리들: Array<int> = []113
let mut 기울임표자리들: Array<int> = []114
for i in 0..<항목들.length {115
match 항목들[i] {116
case 굵게표 => 굵게표자리들.push(i)117
case 기울임표 => 기울임표자리들.push(i)118
case 글자(_) => {}119
}120
}121
let 짝없는굵게표 = 짝없는자리(굵게표자리들)122
let 짝없는기울임표 = 짝없는자리(기울임표자리들)123
let mut 셀들: Array<셀> = []124
let mut 현재서식 = 보통서식125
for i in 0..<항목들.length {126
match 항목들[i] {127
case 굵게표 =>128
if i == 짝없는굵게표 {129
셀들.push(셀만들기("*", 현재서식))130
셀들.push(셀만들기("*", 현재서식))131
} else {132
현재서식 = 서식 { ...현재서식, 굵게: !현재서식.굵게 }133
}134
case 기울임표 =>135
if i == 짝없는기울임표 {136
셀들.push(셀만들기("*", 현재서식))137
} else {138
현재서식 = 서식 { ...현재서식, 기울임: !현재서식.기울임 }139
}140
case 글자(글자값) => 셀들.push(셀만들기(글자값, 현재서식))141
}142
}143
셀들144
}146
// 이어진 공백 셀은 빈 낱말을 만들지 않으므로 줄을 이을 때 공백 한 칸으로 줄어듭니다.147
function 낱말나누기(셀들: Array<셀>) -> Array<Array<셀>> {148
let mut 낱말들: Array<Array<셀>> = []149
let mut 낱말: Array<셀> = []150
for 셀값 in 셀들 {151
if 셀값.글 != " " {152
낱말.push(셀값)153
} else if 낱말.length > 0 {154
낱말들.push(낱말)155
낱말 = []156
}157
}158
if 낱말.length > 0 { 낱말들.push(낱말) }159
낱말들160
}162
// 한계보다 넓은 글자도 혼자 한 조각이 됩니다. 빈 입력은 빈 조각 하나를 냅니다.163
function 강제분할(셀들: Array<셀>, 한계: int) -> Array<Array<셀>> {164
let mut 조각들: Array<Array<셀>> = []165
let mut 조각: Array<셀> = []166
let mut 조각폭 = 0167
for 셀값 in 셀들 {168
if 조각폭 + 셀값.너비 > 한계 && 조각.length > 0 {169
조각들.push(조각)170
조각 = []171
조각폭 = 0172
}173
조각.push(셀값)174
조각폭 += 셀값.너비175
}176
조각들.push(조각)177
조각들178
}180
// 낱말 사이 공백은 서식 없는 셀이라 굵은 낱말 둘 사이에서 태그가 닫혔다 열립니다.181
// 한계보다 넓은 낱말은 새 줄에서 글자 단위로 자르고 마지막 조각 뒤에 다음 낱말을 잇습니다.182
function 셀줄나눔(셀들: Array<셀>, 한계: int) -> Array<Array<셀>> {183
let mut 줄목록: Array<Array<셀>> = []184
let mut 현재: Array<셀> = []185
let mut 현재폭 = 0186
for 낱말 in 낱말나누기(셀들) {187
let 낱말폭 = 셀폭(낱말)188
if 낱말폭 > 한계 {189
if 현재.length > 0 { 줄목록.push(현재) }190
let 조각들 = 강제분할(낱말, 한계)191
for 조각 in 조각들.slice(0, 조각들.length - 1) { 줄목록.push(조각) }192
현재 = 조각들[조각들.length - 1]193
현재폭 = 셀폭(현재)194
} else {195
if 현재.length > 0 && 현재폭 + 1 + 낱말폭 > 한계 {196
줄목록.push(현재)197
현재 = []198
현재폭 = 0199
}200
if 현재.length > 0 {201
현재.push(셀만들기(" ", 보통서식))202
현재폭 += 1203
}204
for 셀값 in 낱말 { 현재.push(셀값) }205
현재폭 += 낱말폭206
}207
}208
if 현재.length > 0 { 줄목록.push(현재) }209
if 줄목록.length == 0 { [[]] } else { 줄목록 }210
}212
// 서식이 바뀌는 자리에서만 태그를 닫고 다시 열어 줄마다 태그 짝을 맞춥니다.213
function 서식바꿈(앞: 서식, 뒤: 서식) -> string {214
if 앞.굵게 == 뒤.굵게 && 앞.기울임 == 뒤.기울임 { return "" }215
[216
if 앞.기울임 { "</i>" } else { "" },217
if 앞.굵게 { "</b>" } else { "" },218
if 뒤.굵게 { "<b>" } else { "" },219
if 뒤.기울임 { "<i>" } else { "" },220
].join("")221
}223
function 셀줄그리기(줄셀들: Array<셀>) -> string {224
let n = 줄셀들.length225
let 본문 = map(0..<n, i => 서식바꿈(if i == 0 { 보통서식 } else { 줄셀들[i - 1].꾸밈 }, 줄셀들[i].꾸밈) + 이스케이프(줄셀들[i].글))226
let 끝서식 = if n == 0 { 보통서식 } else { 줄셀들[n - 1].꾸밈 }227
본문.join("") + 서식바꿈(끝서식, 보통서식)228
}230
// 목록의 이어지는 줄은 표지 너비만큼 들여 씁니다.231
function 서식줄글들(블록값: 블록, 한계: int) -> Array<string> {232
let 표지너비 = 블록값.표지.scalars().length233
let 들여쓰기 = [for i in 0..<표지너비 => " "].join("")234
let 줄셀목록 = 셀줄나눔(셀분해(블록값.글), 한계 - 표지너비)235
map(0..<줄셀목록.length, i => 이스케이프(if i == 0 { 블록값.표지 } else { 들여쓰기 }) + 셀줄그리기(줄셀목록[i]))236
}238
function 코드줄글들(원줄들: Array<string>, 한계: int) -> Array<string> {239
[for 원줄 in 원줄들 for 조각 in 강제분할(원줄.scalars().map(글자 => 셀만들기(글자, 보통서식)), 한계) => 조각.map(셀값 => 셀값.글).join("")]240
}242
// 블록의 모든 줄이 블록 번호와 줄 수를 지녀 쪽 나눔이 코드 블록까지 한 단위로 다룹니다.243
function 블록줄들(번호: int, 블록값: 블록, 한계: int) -> Array<줄> {244
let 글들 = match 블록값.종류 {245
case 코드 => 코드줄글들(블록값.원줄들, 한계)246
case 구분 => [""]247
case 문단 | 제목1 | 제목2 | 제목3 | 인용 | 목록 => 서식줄글들(블록값, 한계)248
}249
let 개수 = 글들.length250
map(0..<개수, i => 줄 { 글: 글들[i], 종류: 블록값.종류, 블록: 번호, 시작: i == 0, 끝: i == 개수 - 1, 블록줄수: 개수 })251
}253
function 서식블록(종류: 줄종류, 글자들: Array<string>, 머리길이: int, 표지: string) -> 블록 {254
블록 { 종류: 종류, 글: 글자들.slice(머리길이, 글자들.length).join(""), 표지: 표지, 원줄들: [] }255
}257
// "12. 글" 처럼 숫자 뒤에 마침표와 공백이 오면 표지의 글자 수를 돌려줍니다.258
function 순서표길이(글자들: Array<string>) -> Option<int> {259
let 자리 = 연속끝(글자들, 0, 숫자인가)260
if 자리 > 0 && 자리 + 1 < 글자들.length && 글자들[자리] == "." && 글자들[자리 + 1] == " " { Some(자리 + 2) } else { None }261
}263
// 한 줄로 끝나는 블록을 읽습니다. 구분선을 목록보다 먼저 보고 긴 제목 표지를 짧은 것보다 먼저 봅니다.264
function 홑줄블록(다듬: string) -> Option<블록> {265
if 다듬 == "---" || 다듬 == "***" { return Some(블록 { 종류: 줄종류.구분, 글: "", 표지: "", 원줄들: [] }) }266
let 글자들 = 다듬.scalars()267
if 다듬.startsWith("### ") { return Some(서식블록(줄종류.제목3, 글자들, 4, "")) }268
if 다듬.startsWith("## ") { return Some(서식블록(줄종류.제목2, 글자들, 3, "")) }269
if 다듬.startsWith("# ") { return Some(서식블록(줄종류.제목1, 글자들, 2, "")) }270
if 다듬.startsWith("> ") { return Some(서식블록(줄종류.인용, 글자들, 2, "")) }271
if 다듬.startsWith("- ") || 다듬.startsWith("* ") { return Some(서식블록(줄종류.목록, 글자들, 2, "• ")) }272
match 순서표길이(글자들) {273
case Some(길이) => Some(서식블록(줄종류.목록, 글자들, 길이, 글자들.slice(0, 길이).join("")))274
case None => None275
}276
}278
function 문단블록(문단줄들: Array<string>) -> 블록 {279
블록 { 종류: 줄종류.문단, 글: 문단줄들.join(" "), 표지: "", 원줄들: [] }280
}282
// 빈 줄, 울타리, 한 줄 블록이 문단을 끝냅니다. 닫히지 않은 코드 블록도 줄이 있으면 블록이 됩니다.283
function 블록나누기(입력줄들: Array<string>) -> Array<블록> {284
let mut 블록들: Array<블록> = []285
let mut 문단줄들: Array<string> = []286
let mut 코드중 = false287
let mut 코드줄들: Array<string> = []288
for 원줄 in 입력줄들 {289
let 다듬 = 원줄.trim()290
let 울타리 = 다듬.startsWith("```")291
if 코드중 {292
if 울타리 {293
블록들.push(블록 { 종류: 줄종류.코드, 글: "", 표지: "", 원줄들: 코드줄들 })294
코드중 = false295
} else {296
코드줄들.push(원줄)297
}298
} else if 울타리 || 다듬 == "" {299
if 문단줄들.length > 0 {300
블록들.push(문단블록(문단줄들))301
문단줄들 = []302
}303
코드중 = 울타리304
코드줄들 = []305
} else {306
match 홑줄블록(다듬) {307
case Some(블록값) => {308
if 문단줄들.length > 0 {309
블록들.push(문단블록(문단줄들))310
문단줄들 = []311
}312
블록들.push(블록값)313
}314
case None => 문단줄들.push(다듬)315
}316
}317
}318
if 문단줄들.length > 0 { 블록들.push(문단블록(문단줄들)) }319
if 코드중 && 코드줄들.length > 0 { 블록들.push(블록 { 종류: 줄종류.코드, 글: "", 표지: "", 원줄들: 코드줄들 }) }320
블록들321
}323
function 마크다운줄들(입력줄들: Array<string>, 한계: int) -> Array<줄> {324
let 블록들 = 블록나누기(입력줄들)325
[for 번호 in 0..<블록들.length for 블록줄 in 블록줄들(번호, 블록들[번호], 한계) => 블록줄]326
}328
function 제목인가(종류: 줄종류) -> bool {329
match 종류 {330
case 제목1 | 제목2 | 제목3 => true331
case 문단 | 인용 | 목록 | 코드 | 구분 => false332
}333
}335
// 여러 줄 블록의 첫 줄은 고아가 되지 않게 다음 줄과, 끝에서 둘째 줄은 과부가 되지 않게 마지막 줄과 함께 둡니다.336
function 기본묶음(줄들: Array<줄>, 색인: int) -> int {337
let 기준 = 줄들[색인]338
let 과부앞 = 색인 + 1 < 줄들.length && 줄들[색인 + 1].끝 && !줄들[색인 + 1].시작 && 줄들[색인 + 1].블록 == 기준.블록339
if 기준.블록줄수 > 1 && (기준.시작 || 과부앞) { 2 } else { 1 }340
}342
// 제목은 남은 제목 줄과 다음 줄의 묶음을 함께 둡니다. 다음 줄도 제목이면 그 묶음까지 이어 연속 제목이 함께 넘어갑니다.343
function 묶음크기(줄들: Array<줄>, 색인: int) -> int {344
let 기준 = 줄들[색인]345
if !제목인가(기준.종류) { return 기본묶음(줄들, 색인) }346
let mut 끝 = 색인347
while 끝 + 1 < 줄들.length && 줄들[끝 + 1].블록 == 기준.블록 { 끝 += 1 }348
let 블록남음 = 끝 - 색인 + 1349
if 끝 + 1 < 줄들.length { 블록남음 + 묶음크기(줄들, 끝 + 1) } else { 블록남음 }350
}352
// 묶음이 쪽보다 크면 쪽 하나만큼만 요구하므로 어떤 쪽도 쪽높이를 넘지 않습니다.353
function 쪽나누기(줄들: Array<줄>, 쪽높이: int) -> Array<Array<줄>> {354
let mut 쪽들: Array<Array<줄>> = []355
let mut 현재쪽: Array<줄> = []356
for 색인 in 0..<줄들.length {357
if 현재쪽.length > 0 && 쪽높이 - 현재쪽.length < 묶음크기(줄들, 색인).atMost(쪽높이) {358
쪽들.push(현재쪽)359
현재쪽 = []360
}361
현재쪽.push(줄들[색인])362
}363
if 현재쪽.length > 0 { 쪽들.push(현재쪽) }364
쪽들365
}367
// 빈 div 는 높이가 0 으로 접히므로 빈 줄은 로 채워 줄 높이를 지킵니다.368
function 빈칸대체(글: string) -> string {369
if 글 == "" { " " } else { 글 }370
}372
function 줄그리기(그릴줄: 줄) -> string {373
match 그릴줄.종류 {374
case 구분 => "<div class=\"mp-hr\"></div>"375
case 코드 => "<div class=\"mp-code\">" + 빈칸대체(이스케이프(그릴줄.글)) + "</div>"376
case 제목1 | 제목2 | 제목3 => "<div class=\"mp-h\">" + 빈칸대체(그릴줄.글) + "</div>"377
case 인용 => "<div class=\"mp-quote\">" + 빈칸대체(그릴줄.글) + "</div>"378
case 문단 | 목록 => "<div class=\"mp-line\">" + 빈칸대체(그릴줄.글) + "</div>"379
}380
}382
// 높이를 고정하고 넘침을 숨겨 상자가 내용 때문에 커지지 않습니다.383
function 쪽그리기(쪽: Array<줄>, 쪽번호: int, 쪽높이: int) -> string {384
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"385
"<div class=\"mp-page\" style=\"" + 스타일 + "\">" + 쪽.map(줄그리기).join("") + "<div style=\"position:absolute;bottom:0.6em;right:1.4em;color:#888;font-size:0.85em\">{쪽번호}</div></div>"386
}388
function 쪽높이읽기(첫줄: string) -> Result<int, string> {389
if 첫줄 == "" { return Err(높이안내) }390
if !모두숫자인가(첫줄) { return Err(오류알림("페이지 높이는 양의 정수여야 합니다.")) }391
let 쪽높이 = toInt(첫줄).okOr(오류알림("페이지 높이가 너무 큽니다."))?392
if 쪽높이 < 3 { return Err(오류알림("페이지 높이는 3 이상이어야 합니다.")) }393
if 쪽높이 > 200 { return Err(오류알림("페이지 높이는 200 이하여야 합니다.")) }394
Ok(쪽높이)395
}397
function 쪽만들기(입력: string) -> Result<string, string> {398
let 입력줄들 = 입력.split("\n")399
let 쪽높이 = 쪽높이읽기(입력줄들[0].trim())?400
let 줄들 = 마크다운줄들(입력줄들.slice(1, 입력줄들.length), 줄너비)401
if 줄들.length == 0 { return Err("<div class=\"meta\">마크다운 본문을 입력하세요.</div>") }402
let 쪽들 = 쪽나누기(줄들, 쪽높이)403
let 그림 = map(0..<쪽들.length, i => 쪽그리기(쪽들[i], i + 1, 쪽높이))404
Ok("<div class=\"meta\">{쪽들.length}쪽, 페이지당 {쪽높이}줄</div><div class=\"mp-pages\">" + 그림.join("") + "</div>")405
}407
input() |> 쪽만들기 |> 결과풀기 |> printSHA256 d39bfc74c135c8699d5a3b98c66de6351f48f349ca2202908eab2ff810e75f80