slow.monster

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.
8enum 줄종류 { 문단, 제목1, 제목2, 제목3, 인용, 목록, 코드, 구분 }
9enum 서식항목 { 굵게표, 기울임표, 글자(string) }
10record 서식 { 굵게: bool, 기울임: bool }
11record 셀 { 글: string, 꾸밈: 서식, 너비: int }
12// 표지는 목록의 첫 줄 앞에 붙는 글이고 원줄들은 코드 블록에서만 씁니다.
13record 블록 { 종류: 줄종류, 글: string, 표지: string, 원줄들: Array<string> }
14// 서식 줄의 글은 이미 이스케이프해 그린 HTML 이고 코드 줄의 글만 원문입니다.
15record 줄 { 글: string, 종류: 줄종류, 블록: int, 시작: bool, 끝: bool, 블록줄수: int }
17let 줄너비 = 60
18let 보통서식 = 서식 { 굵게: false, 기울임: false }
19let 높이안내 = "<div class=\"meta\">첫 줄에 페이지 높이(줄 수), 다음 줄부터 마크다운을 입력하세요.</div>"
21// 공용 도우미 시작 (이스케이프 전각인가 글자너비 숫자인가 모두숫자인가 오류알림 결과풀기 연속끝)
22// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.
24// 사용자 입력을 HTML 에 넣기 전에 반드시 거칩니다.
25function 이스케이프(원문: string) -> string {
26 원문.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
29// 고정폭에서 두 칸을 차지하는 범위입니다. 한글 음절, 호환 자모, 자모, 한자, CJK 문장부호, 전각 영숫자 순입니다.
30// 코드포인트를 10진수로 적습니다. 토파즈 과제 '16진 정수 리터럴' 의 우회입니다.
31function 전각인가(글자: string) -> bool {
32 let 코드 = 글자.codePointAt(0) ?? 0
33 (코드 >= 44032 && 코드 <= 55203)
34 || (코드 >= 12592 && 코드 <= 12687)
35 || (코드 >= 4352 && 코드 <= 4607)
36 || (코드 >= 19968 && 코드 <= 40959)
37 || (코드 >= 12288 && 코드 <= 12351)
38 || (코드 >= 65280 && 코드 <= 65519)
41function 글자너비(글자: string) -> int {
42 if 전각인가(글자) { 2 } else { 1 }
45function 숫자인가(글자: string) -> bool {
46 let 코드 = 글자.codePointAt(0) ?? 0
47 코드 >= 48 && 코드 <= 57
50// 빈 문자열은 참입니다. 부호와 소수점을 받지 않으려고 toInt 앞에서 씁니다.
51function 모두숫자인가(글: string) -> bool {
52 글.scalars().reduce(true, (모두, 글자) => 모두 && 숫자인가(글자))
55// 문구는 고정 문장이거나 이미 이스케이프한 값이어야 합니다.
56function 오류알림(문구: string) -> string {
57 "<div class=\"err\">{문구}</div>"
60// 성공 값과 안내가 모두 바로 보여 줄 HTML 조각일 때 진입점에서 한 번 풉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.
61function 결과풀기(값: Result<string, string>) -> string {
62 match 값 {
63 case Ok(결과) => 결과
64 case Err(안내) => 안내
65 }
68// 시작부터 맞는가가 참인 글자가 이어지는 끝 위치를 돌려줍니다. 맞는 글자가 없으면 시작 그대로입니다.
69function 연속끝(글자들: Array<string>, 시작: int, 맞는가: (string) -> bool) -> int {
70 let mut 끝 = 시작
71 while 끝 < 글자들.length && 맞는가(글자들[끝]) { 끝 += 1 }
72 끝
75// 공용 도우미 끝
77function 셀만들기(글자: string, 꾸밈: 서식) -> 셀 {
78 셀 { 글: 글자, 꾸밈: 꾸밈, 너비: 글자너비(글자) }
81function 셀폭(셀들: Array<셀>) -> int {
82 셀들.reduce(0, (합, 셀값) => 합 + 셀값.너비)
85// ** 를 * 보다 먼저 읽으므로 *** 는 굵게표 다음 기울임표가 됩니다.
86function 서식항목나누기(글자들: Array<string>) -> Array<서식항목> {
87 let mut 항목들: Array<서식항목> = []
88 let mut 위치 = 0
89 while 위치 < 글자들.length {
90 if 글자들[위치] == "*" && 위치 + 1 < 글자들.length && 글자들[위치 + 1] == "*" {
91 항목들.push(서식항목.굵게표)
92 위치 += 2
93 } else if 글자들[위치] == "*" {
94 항목들.push(서식항목.기울임표)
95 위치 += 1
96 } else {
97 항목들.push(서식항목.글자(글자들[위치]))
98 위치 += 1
99 }
100 }
101 항목들
104// 표지 수가 홀수면 마지막 표지 하나만 짝이 없습니다. 짝이 맞으면 -1 입니다.
105function 짝없는자리(자리들: Array<int>) -> int {
106 if 자리들.length % 2 == 1 { 자리들[자리들.length - 1] } else { -1 }
109// 짝이 맞는 표지는 서식을 켜고 끄며 셀을 남기지 않고, 짝이 없는 표지는 별표 글자로 남습니다.
110function 셀분해(원문: 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 셀들
146// 이어진 공백 셀은 빈 낱말을 만들지 않으므로 줄을 이을 때 공백 한 칸으로 줄어듭니다.
147function 낱말나누기(셀들: 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 낱말들
162// 한계보다 넓은 글자도 혼자 한 조각이 됩니다. 빈 입력은 빈 조각 하나를 냅니다.
163function 강제분할(셀들: Array<셀>, 한계: int) -> Array<Array<셀>> {
164 let mut 조각들: Array<Array<셀>> = []
165 let mut 조각: Array<셀> = []
166 let mut 조각폭 = 0
167 for 셀값 in 셀들 {
168 if 조각폭 + 셀값.너비 > 한계 && 조각.length > 0 {
169 조각들.push(조각)
170 조각 = []
171 조각폭 = 0
172 }
173 조각.push(셀값)
174 조각폭 += 셀값.너비
175 }
176 조각들.push(조각)
177 조각들
180// 낱말 사이 공백은 서식 없는 셀이라 굵은 낱말 둘 사이에서 태그가 닫혔다 열립니다.
181// 한계보다 넓은 낱말은 새 줄에서 글자 단위로 자르고 마지막 조각 뒤에 다음 낱말을 잇습니다.
182function 셀줄나눔(셀들: Array<셀>, 한계: int) -> Array<Array<셀>> {
183 let mut 줄목록: Array<Array<셀>> = []
184 let mut 현재: Array<셀> = []
185 let mut 현재폭 = 0
186 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 현재폭 = 0
199 }
200 if 현재.length > 0 {
201 현재.push(셀만들기(" ", 보통서식))
202 현재폭 += 1
203 }
204 for 셀값 in 낱말 { 현재.push(셀값) }
205 현재폭 += 낱말폭
206 }
207 }
208 if 현재.length > 0 { 줄목록.push(현재) }
209 if 줄목록.length == 0 { [[]] } else { 줄목록 }
212// 서식이 바뀌는 자리에서만 태그를 닫고 다시 열어 줄마다 태그 짝을 맞춥니다.
213function 서식바꿈(앞: 서식, 뒤: 서식) -> string {
214 if 앞.굵게 == 뒤.굵게 && 앞.기울임 == 뒤.기울임 { return "" }
215 [
216 if 앞.기울임 { "</i>" } else { "" },
217 if 앞.굵게 { "</b>" } else { "" },
218 if 뒤.굵게 { "<b>" } else { "" },
219 if 뒤.기울임 { "<i>" } else { "" },
220 ].join("")
223function 셀줄그리기(줄셀들: Array<셀>) -> string {
224 let n = 줄셀들.length
225 let 본문 = map(0..<n, i => 서식바꿈(if i == 0 { 보통서식 } else { 줄셀들[i - 1].꾸밈 }, 줄셀들[i].꾸밈) + 이스케이프(줄셀들[i].글))
226 let 끝서식 = if n == 0 { 보통서식 } else { 줄셀들[n - 1].꾸밈 }
227 본문.join("") + 서식바꿈(끝서식, 보통서식)
230// 목록의 이어지는 줄은 표지 너비만큼 들여 씁니다.
231function 서식줄글들(블록값: 블록, 한계: int) -> Array<string> {
232 let 표지너비 = 블록값.표지.scalars().length
233 let 들여쓰기 = [for i in 0..<표지너비 => " "].join("")
234 let 줄셀목록 = 셀줄나눔(셀분해(블록값.글), 한계 - 표지너비)
235 map(0..<줄셀목록.length, i => 이스케이프(if i == 0 { 블록값.표지 } else { 들여쓰기 }) + 셀줄그리기(줄셀목록[i]))
238function 코드줄글들(원줄들: Array<string>, 한계: int) -> Array<string> {
239 [for 원줄 in 원줄들 for 조각 in 강제분할(원줄.scalars().map(글자 => 셀만들기(글자, 보통서식)), 한계) => 조각.map(셀값 => 셀값.글).join("")]
242// 블록의 모든 줄이 블록 번호와 줄 수를 지녀 쪽 나눔이 코드 블록까지 한 단위로 다룹니다.
243function 블록줄들(번호: int, 블록값: 블록, 한계: int) -> Array<줄> {
244 let 글들 = match 블록값.종류 {
245 case 코드 => 코드줄글들(블록값.원줄들, 한계)
246 case 구분 => [""]
247 case 문단 | 제목1 | 제목2 | 제목3 | 인용 | 목록 => 서식줄글들(블록값, 한계)
248 }
249 let 개수 = 글들.length
250 map(0..<개수, i => 줄 { 글: 글들[i], 종류: 블록값.종류, 블록: 번호, 시작: i == 0, 끝: i == 개수 - 1, 블록줄수: 개수 })
253function 서식블록(종류: 줄종류, 글자들: Array<string>, 머리길이: int, 표지: string) -> 블록 {
254 블록 { 종류: 종류, 글: 글자들.slice(머리길이, 글자들.length).join(""), 표지: 표지, 원줄들: [] }
257// "12. 글" 처럼 숫자 뒤에 마침표와 공백이 오면 표지의 글자 수를 돌려줍니다.
258function 순서표길이(글자들: Array<string>) -> Option<int> {
259 let 자리 = 연속끝(글자들, 0, 숫자인가)
260 if 자리 > 0 && 자리 + 1 < 글자들.length && 글자들[자리] == "." && 글자들[자리 + 1] == " " { Some(자리 + 2) } else { None }
263// 한 줄로 끝나는 블록을 읽습니다. 구분선을 목록보다 먼저 보고 긴 제목 표지를 짧은 것보다 먼저 봅니다.
264function 홑줄블록(다듬: 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 => None
275 }
278function 문단블록(문단줄들: Array<string>) -> 블록 {
279 블록 { 종류: 줄종류.문단, 글: 문단줄들.join(" "), 표지: "", 원줄들: [] }
282// 빈 줄, 울타리, 한 줄 블록이 문단을 끝냅니다. 닫히지 않은 코드 블록도 줄이 있으면 블록이 됩니다.
283function 블록나누기(입력줄들: Array<string>) -> Array<블록> {
284 let mut 블록들: Array<블록> = []
285 let mut 문단줄들: Array<string> = []
286 let mut 코드중 = false
287 let mut 코드줄들: Array<string> = []
288 for 원줄 in 입력줄들 {
289 let 다듬 = 원줄.trim()
290 let 울타리 = 다듬.startsWith("```")
291 if 코드중 {
292 if 울타리 {
293 블록들.push(블록 { 종류: 줄종류.코드, 글: "", 표지: "", 원줄들: 코드줄들 })
294 코드중 = false
295 } 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 블록들
323function 마크다운줄들(입력줄들: Array<string>, 한계: int) -> Array<줄> {
324 let 블록들 = 블록나누기(입력줄들)
325 [for 번호 in 0..<블록들.length for 블록줄 in 블록줄들(번호, 블록들[번호], 한계) => 블록줄]
328function 제목인가(종류: 줄종류) -> bool {
329 match 종류 {
330 case 제목1 | 제목2 | 제목3 => true
331 case 문단 | 인용 | 목록 | 코드 | 구분 => false
332 }
335// 여러 줄 블록의 첫 줄은 고아가 되지 않게 다음 줄과, 끝에서 둘째 줄은 과부가 되지 않게 마지막 줄과 함께 둡니다.
336function 기본묶음(줄들: Array<줄>, 색인: int) -> int {
337 let 기준 = 줄들[색인]
338 let 과부앞 = 색인 + 1 < 줄들.length && 줄들[색인 + 1].끝 && !줄들[색인 + 1].시작 && 줄들[색인 + 1].블록 == 기준.블록
339 if 기준.블록줄수 > 1 && (기준.시작 || 과부앞) { 2 } else { 1 }
342// 제목은 남은 제목 줄과 다음 줄의 묶음을 함께 둡니다. 다음 줄도 제목이면 그 묶음까지 이어 연속 제목이 함께 넘어갑니다.
343function 묶음크기(줄들: Array<줄>, 색인: int) -> int {
344 let 기준 = 줄들[색인]
345 if !제목인가(기준.종류) { return 기본묶음(줄들, 색인) }
346 let mut 끝 = 색인
347 while 끝 + 1 < 줄들.length && 줄들[끝 + 1].블록 == 기준.블록 { 끝 += 1 }
348 let 블록남음 = 끝 - 색인 + 1
349 if 끝 + 1 < 줄들.length { 블록남음 + 묶음크기(줄들, 끝 + 1) } else { 블록남음 }
352// 묶음이 쪽보다 크면 쪽 하나만큼만 요구하므로 어떤 쪽도 쪽높이를 넘지 않습니다.
353function 쪽나누기(줄들: 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 쪽들
367// 빈 div 는 높이가 0 으로 접히므로 빈 줄은 &nbsp; 로 채워 줄 높이를 지킵니다.
368function 빈칸대체(글: string) -> string {
369 if 글 == "" { "&nbsp;" } else { 글 }
372function 줄그리기(그릴줄: 줄) -> 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 }
382// 높이를 고정하고 넘침을 숨겨 상자가 내용 때문에 커지지 않습니다.
383function 쪽그리기(쪽: 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>"
388function 쪽높이읽기(첫줄: 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(쪽높이)
397function 쪽만들기(입력: 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>")
407input() |> 쪽만들기 |> 결과풀기 |> print

SHA256 d39bfc74c135c8699d5a3b98c66de6351f48f349ca2202908eab2ff810e75f80