// 마크다운 쪽 나눔. 마크다운을 고정 크기 쪽으로 흘리며 제목을 다음 줄과 붙이고 고아와 과부를 막습니다.
// 입력 첫 줄은 쪽당 줄 수이고 다음 줄부터 마크다운입니다. 출력은 요약 한 줄과 쪽마다 고정 크기 상자 하나를 담은 HTML 조각입니다.
// 같은 입력이면 인터프리터와 웹 빌드에서 바이트까지 같은 출력을 냅니다.
// 너비는 반각 1칸, 전각 2칸의 고정폭이고 굵게와 기울임 표지는 너비에 세지 않습니다. PDF 바이트는 만들지 않고 저장은 브라우저 인쇄에 맡깁니다.
// 표, 이미지, 링크, 인라인 HTML, 두 단계 이상 중첩 목록, 글꼴 너비는 다루지 않습니다.
// Markdown paginator: fixed-width pages with keep-with-next headings and widow and orphan control.

enum 줄종류 { 문단, 제목1, 제목2, 제목3, 인용, 목록, 코드, 구분 }
enum 서식항목 { 굵게표, 기울임표, 글자(string) }
record 서식 { 굵게: bool, 기울임: bool }
record 셀 { 글: string, 꾸밈: 서식, 너비: int }
// 표지는 목록의 첫 줄 앞에 붙는 글이고 원줄들은 코드 블록에서만 씁니다.
record 블록 { 종류: 줄종류, 글: string, 표지: string, 원줄들: Array<string> }
// 서식 줄의 글은 이미 이스케이프해 그린 HTML 이고 코드 줄의 글만 원문입니다.
record 줄 { 글: string, 종류: 줄종류, 블록: int, 시작: bool, 끝: bool, 블록줄수: int }

let 줄너비 = 60
let 보통서식 = 서식 { 굵게: false, 기울임: false }
let 높이안내 = "<div class=\"meta\">첫 줄에 페이지 높이(줄 수), 다음 줄부터 마크다운을 입력하세요.</div>"

// 공용 도우미 시작 (이스케이프 전각인가 글자너비 숫자인가 모두숫자인가 오류알림 결과풀기 연속끝)
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.

// 사용자 입력을 HTML 에 넣기 전에 반드시 거칩니다.
function 이스케이프(원문: string) -> string {
  원문.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
}

// 고정폭에서 두 칸을 차지하는 범위입니다. 한글 음절, 호환 자모, 자모, 한자, CJK 문장부호, 전각 영숫자 순입니다.
// 코드포인트를 10진수로 적습니다. 토파즈 과제 '16진 정수 리터럴' 의 우회입니다.
function 전각인가(글자: string) -> bool {
  let 코드 = 글자.codePointAt(0) ?? 0
  (코드 >= 44032 && 코드 <= 55203)
    || (코드 >= 12592 && 코드 <= 12687)
    || (코드 >= 4352 && 코드 <= 4607)
    || (코드 >= 19968 && 코드 <= 40959)
    || (코드 >= 12288 && 코드 <= 12351)
    || (코드 >= 65280 && 코드 <= 65519)
}

function 글자너비(글자: string) -> int {
  if 전각인가(글자) { 2 } else { 1 }
}

function 숫자인가(글자: string) -> bool {
  let 코드 = 글자.codePointAt(0) ?? 0
  코드 >= 48 && 코드 <= 57
}

// 빈 문자열은 참입니다. 부호와 소수점을 받지 않으려고 toInt 앞에서 씁니다.
function 모두숫자인가(글: string) -> bool {
  글.scalars().reduce(true, (모두, 글자) => 모두 && 숫자인가(글자))
}

// 문구는 고정 문장이거나 이미 이스케이프한 값이어야 합니다.
function 오류알림(문구: string) -> string {
  "<div class=\"err\">{문구}</div>"
}

// 성공 값과 안내가 모두 바로 보여 줄 HTML 조각일 때 진입점에서 한 번 풉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.
function 결과풀기(값: Result<string, string>) -> string {
  match 값 {
    case Ok(결과) => 결과
    case Err(안내) => 안내
  }
}

// 시작부터 맞는가가 참인 글자가 이어지는 끝 위치를 돌려줍니다. 맞는 글자가 없으면 시작 그대로입니다.
function 연속끝(글자들: Array<string>, 시작: int, 맞는가: (string) -> bool) -> int {
  let mut 끝 = 시작
  while 끝 < 글자들.length && 맞는가(글자들[끝]) { 끝 += 1 }
  끝
}

// 공용 도우미 끝

function 셀만들기(글자: string, 꾸밈: 서식) -> 셀 {
  셀 { 글: 글자, 꾸밈: 꾸밈, 너비: 글자너비(글자) }
}

function 셀폭(셀들: Array<셀>) -> int {
  셀들.reduce(0, (합, 셀값) => 합 + 셀값.너비)
}

// ** 를 * 보다 먼저 읽으므로 *** 는 굵게표 다음 기울임표가 됩니다.
function 서식항목나누기(글자들: Array<string>) -> Array<서식항목> {
  let mut 항목들: Array<서식항목> = []
  let mut 위치 = 0
  while 위치 < 글자들.length {
    if 글자들[위치] == "*" && 위치 + 1 < 글자들.length && 글자들[위치 + 1] == "*" {
      항목들.push(서식항목.굵게표)
      위치 += 2
    } else if 글자들[위치] == "*" {
      항목들.push(서식항목.기울임표)
      위치 += 1
    } else {
      항목들.push(서식항목.글자(글자들[위치]))
      위치 += 1
    }
  }
  항목들
}

// 표지 수가 홀수면 마지막 표지 하나만 짝이 없습니다. 짝이 맞으면 -1 입니다.
function 짝없는자리(자리들: Array<int>) -> int {
  if 자리들.length % 2 == 1 { 자리들[자리들.length - 1] } else { -1 }
}

// 짝이 맞는 표지는 서식을 켜고 끄며 셀을 남기지 않고, 짝이 없는 표지는 별표 글자로 남습니다.
function 셀분해(원문: string) -> Array<셀> {
  let 항목들 = 서식항목나누기(원문.scalars())
  let mut 굵게표자리들: Array<int> = []
  let mut 기울임표자리들: Array<int> = []
  for i in 0..<항목들.length {
    match 항목들[i] {
      case 굵게표 => 굵게표자리들.push(i)
      case 기울임표 => 기울임표자리들.push(i)
      case 글자(_) => {}
    }
  }
  let 짝없는굵게표 = 짝없는자리(굵게표자리들)
  let 짝없는기울임표 = 짝없는자리(기울임표자리들)
  let mut 셀들: Array<셀> = []
  let mut 현재서식 = 보통서식
  for i in 0..<항목들.length {
    match 항목들[i] {
      case 굵게표 =>
        if i == 짝없는굵게표 {
          셀들.push(셀만들기("*", 현재서식))
          셀들.push(셀만들기("*", 현재서식))
        } else {
          현재서식 = 서식 { ...현재서식, 굵게: !현재서식.굵게 }
        }
      case 기울임표 =>
        if i == 짝없는기울임표 {
          셀들.push(셀만들기("*", 현재서식))
        } else {
          현재서식 = 서식 { ...현재서식, 기울임: !현재서식.기울임 }
        }
      case 글자(글자값) => 셀들.push(셀만들기(글자값, 현재서식))
    }
  }
  셀들
}

// 이어진 공백 셀은 빈 낱말을 만들지 않으므로 줄을 이을 때 공백 한 칸으로 줄어듭니다.
function 낱말나누기(셀들: Array<셀>) -> Array<Array<셀>> {
  let mut 낱말들: Array<Array<셀>> = []
  let mut 낱말: Array<셀> = []
  for 셀값 in 셀들 {
    if 셀값.글 != " " {
      낱말.push(셀값)
    } else if 낱말.length > 0 {
      낱말들.push(낱말)
      낱말 = []
    }
  }
  if 낱말.length > 0 { 낱말들.push(낱말) }
  낱말들
}

// 한계보다 넓은 글자도 혼자 한 조각이 됩니다. 빈 입력은 빈 조각 하나를 냅니다.
function 강제분할(셀들: Array<셀>, 한계: int) -> Array<Array<셀>> {
  let mut 조각들: Array<Array<셀>> = []
  let mut 조각: Array<셀> = []
  let mut 조각폭 = 0
  for 셀값 in 셀들 {
    if 조각폭 + 셀값.너비 > 한계 && 조각.length > 0 {
      조각들.push(조각)
      조각 = []
      조각폭 = 0
    }
    조각.push(셀값)
    조각폭 += 셀값.너비
  }
  조각들.push(조각)
  조각들
}

// 낱말 사이 공백은 서식 없는 셀이라 굵은 낱말 둘 사이에서 태그가 닫혔다 열립니다.
// 한계보다 넓은 낱말은 새 줄에서 글자 단위로 자르고 마지막 조각 뒤에 다음 낱말을 잇습니다.
function 셀줄나눔(셀들: Array<셀>, 한계: int) -> Array<Array<셀>> {
  let mut 줄목록: Array<Array<셀>> = []
  let mut 현재: Array<셀> = []
  let mut 현재폭 = 0
  for 낱말 in 낱말나누기(셀들) {
    let 낱말폭 = 셀폭(낱말)
    if 낱말폭 > 한계 {
      if 현재.length > 0 { 줄목록.push(현재) }
      let 조각들 = 강제분할(낱말, 한계)
      for 조각 in 조각들.slice(0, 조각들.length - 1) { 줄목록.push(조각) }
      현재 = 조각들[조각들.length - 1]
      현재폭 = 셀폭(현재)
    } else {
      if 현재.length > 0 && 현재폭 + 1 + 낱말폭 > 한계 {
        줄목록.push(현재)
        현재 = []
        현재폭 = 0
      }
      if 현재.length > 0 {
        현재.push(셀만들기(" ", 보통서식))
        현재폭 += 1
      }
      for 셀값 in 낱말 { 현재.push(셀값) }
      현재폭 += 낱말폭
    }
  }
  if 현재.length > 0 { 줄목록.push(현재) }
  if 줄목록.length == 0 { [[]] } else { 줄목록 }
}

// 서식이 바뀌는 자리에서만 태그를 닫고 다시 열어 줄마다 태그 짝을 맞춥니다.
function 서식바꿈(앞: 서식, 뒤: 서식) -> string {
  if 앞.굵게 == 뒤.굵게 && 앞.기울임 == 뒤.기울임 { return "" }
  [
    if 앞.기울임 { "</i>" } else { "" },
    if 앞.굵게 { "</b>" } else { "" },
    if 뒤.굵게 { "<b>" } else { "" },
    if 뒤.기울임 { "<i>" } else { "" },
  ].join("")
}

function 셀줄그리기(줄셀들: Array<셀>) -> string {
  let n = 줄셀들.length
  let 본문 = map(0..<n, i => 서식바꿈(if i == 0 { 보통서식 } else { 줄셀들[i - 1].꾸밈 }, 줄셀들[i].꾸밈) + 이스케이프(줄셀들[i].글))
  let 끝서식 = if n == 0 { 보통서식 } else { 줄셀들[n - 1].꾸밈 }
  본문.join("") + 서식바꿈(끝서식, 보통서식)
}

// 목록의 이어지는 줄은 표지 너비만큼 들여 씁니다.
function 서식줄글들(블록값: 블록, 한계: int) -> Array<string> {
  let 표지너비 = 블록값.표지.scalars().length
  let 들여쓰기 = [for i in 0..<표지너비 => " "].join("")
  let 줄셀목록 = 셀줄나눔(셀분해(블록값.글), 한계 - 표지너비)
  map(0..<줄셀목록.length, i => 이스케이프(if i == 0 { 블록값.표지 } else { 들여쓰기 }) + 셀줄그리기(줄셀목록[i]))
}

function 코드줄글들(원줄들: Array<string>, 한계: int) -> Array<string> {
  [for 원줄 in 원줄들 for 조각 in 강제분할(원줄.scalars().map(글자 => 셀만들기(글자, 보통서식)), 한계) => 조각.map(셀값 => 셀값.글).join("")]
}

// 블록의 모든 줄이 블록 번호와 줄 수를 지녀 쪽 나눔이 코드 블록까지 한 단위로 다룹니다.
function 블록줄들(번호: int, 블록값: 블록, 한계: int) -> Array<줄> {
  let 글들 = match 블록값.종류 {
    case 코드 => 코드줄글들(블록값.원줄들, 한계)
    case 구분 => [""]
    case 문단 | 제목1 | 제목2 | 제목3 | 인용 | 목록 => 서식줄글들(블록값, 한계)
  }
  let 개수 = 글들.length
  map(0..<개수, i => 줄 { 글: 글들[i], 종류: 블록값.종류, 블록: 번호, 시작: i == 0, 끝: i == 개수 - 1, 블록줄수: 개수 })
}

function 서식블록(종류: 줄종류, 글자들: Array<string>, 머리길이: int, 표지: string) -> 블록 {
  블록 { 종류: 종류, 글: 글자들.slice(머리길이, 글자들.length).join(""), 표지: 표지, 원줄들: [] }
}

// "12. 글" 처럼 숫자 뒤에 마침표와 공백이 오면 표지의 글자 수를 돌려줍니다.
function 순서표길이(글자들: Array<string>) -> Option<int> {
  let 자리 = 연속끝(글자들, 0, 숫자인가)
  if 자리 > 0 && 자리 + 1 < 글자들.length && 글자들[자리] == "." && 글자들[자리 + 1] == " " { Some(자리 + 2) } else { None }
}

// 한 줄로 끝나는 블록을 읽습니다. 구분선을 목록보다 먼저 보고 긴 제목 표지를 짧은 것보다 먼저 봅니다.
function 홑줄블록(다듬: string) -> Option<블록> {
  if 다듬 == "---" || 다듬 == "***" { return Some(블록 { 종류: 줄종류.구분, 글: "", 표지: "", 원줄들: [] }) }
  let 글자들 = 다듬.scalars()
  if 다듬.startsWith("### ") { return Some(서식블록(줄종류.제목3, 글자들, 4, "")) }
  if 다듬.startsWith("## ") { return Some(서식블록(줄종류.제목2, 글자들, 3, "")) }
  if 다듬.startsWith("# ") { return Some(서식블록(줄종류.제목1, 글자들, 2, "")) }
  if 다듬.startsWith("> ") { return Some(서식블록(줄종류.인용, 글자들, 2, "")) }
  if 다듬.startsWith("- ") || 다듬.startsWith("* ") { return Some(서식블록(줄종류.목록, 글자들, 2, "• ")) }
  match 순서표길이(글자들) {
    case Some(길이) => Some(서식블록(줄종류.목록, 글자들, 길이, 글자들.slice(0, 길이).join("")))
    case None => None
  }
}

function 문단블록(문단줄들: Array<string>) -> 블록 {
  블록 { 종류: 줄종류.문단, 글: 문단줄들.join(" "), 표지: "", 원줄들: [] }
}

// 빈 줄, 울타리, 한 줄 블록이 문단을 끝냅니다. 닫히지 않은 코드 블록도 줄이 있으면 블록이 됩니다.
function 블록나누기(입력줄들: Array<string>) -> Array<블록> {
  let mut 블록들: Array<블록> = []
  let mut 문단줄들: Array<string> = []
  let mut 코드중 = false
  let mut 코드줄들: Array<string> = []
  for 원줄 in 입력줄들 {
    let 다듬 = 원줄.trim()
    let 울타리 = 다듬.startsWith("```")
    if 코드중 {
      if 울타리 {
        블록들.push(블록 { 종류: 줄종류.코드, 글: "", 표지: "", 원줄들: 코드줄들 })
        코드중 = false
      } else {
        코드줄들.push(원줄)
      }
    } else if 울타리 || 다듬 == "" {
      if 문단줄들.length > 0 {
        블록들.push(문단블록(문단줄들))
        문단줄들 = []
      }
      코드중 = 울타리
      코드줄들 = []
    } else {
      match 홑줄블록(다듬) {
        case Some(블록값) => {
          if 문단줄들.length > 0 {
            블록들.push(문단블록(문단줄들))
            문단줄들 = []
          }
          블록들.push(블록값)
        }
        case None => 문단줄들.push(다듬)
      }
    }
  }
  if 문단줄들.length > 0 { 블록들.push(문단블록(문단줄들)) }
  if 코드중 && 코드줄들.length > 0 { 블록들.push(블록 { 종류: 줄종류.코드, 글: "", 표지: "", 원줄들: 코드줄들 }) }
  블록들
}

function 마크다운줄들(입력줄들: Array<string>, 한계: int) -> Array<줄> {
  let 블록들 = 블록나누기(입력줄들)
  [for 번호 in 0..<블록들.length for 블록줄 in 블록줄들(번호, 블록들[번호], 한계) => 블록줄]
}

function 제목인가(종류: 줄종류) -> bool {
  match 종류 {
    case 제목1 | 제목2 | 제목3 => true
    case 문단 | 인용 | 목록 | 코드 | 구분 => false
  }
}

// 여러 줄 블록의 첫 줄은 고아가 되지 않게 다음 줄과, 끝에서 둘째 줄은 과부가 되지 않게 마지막 줄과 함께 둡니다.
function 기본묶음(줄들: Array<줄>, 색인: int) -> int {
  let 기준 = 줄들[색인]
  let 과부앞 = 색인 + 1 < 줄들.length && 줄들[색인 + 1].끝 && !줄들[색인 + 1].시작 && 줄들[색인 + 1].블록 == 기준.블록
  if 기준.블록줄수 > 1 && (기준.시작 || 과부앞) { 2 } else { 1 }
}

// 제목은 남은 제목 줄과 다음 줄의 묶음을 함께 둡니다. 다음 줄도 제목이면 그 묶음까지 이어 연속 제목이 함께 넘어갑니다.
function 묶음크기(줄들: Array<줄>, 색인: int) -> int {
  let 기준 = 줄들[색인]
  if !제목인가(기준.종류) { return 기본묶음(줄들, 색인) }
  let mut 끝 = 색인
  while 끝 + 1 < 줄들.length && 줄들[끝 + 1].블록 == 기준.블록 { 끝 += 1 }
  let 블록남음 = 끝 - 색인 + 1
  if 끝 + 1 < 줄들.length { 블록남음 + 묶음크기(줄들, 끝 + 1) } else { 블록남음 }
}

// 묶음이 쪽보다 크면 쪽 하나만큼만 요구하므로 어떤 쪽도 쪽높이를 넘지 않습니다.
function 쪽나누기(줄들: Array<줄>, 쪽높이: int) -> Array<Array<줄>> {
  let mut 쪽들: Array<Array<줄>> = []
  let mut 현재쪽: Array<줄> = []
  for 색인 in 0..<줄들.length {
    if 현재쪽.length > 0 && 쪽높이 - 현재쪽.length < 묶음크기(줄들, 색인).atMost(쪽높이) {
      쪽들.push(현재쪽)
      현재쪽 = []
    }
    현재쪽.push(줄들[색인])
  }
  if 현재쪽.length > 0 { 쪽들.push(현재쪽) }
  쪽들
}

// 빈 div 는 높이가 0 으로 접히므로 빈 줄은 &nbsp; 로 채워 줄 높이를 지킵니다.
function 빈칸대체(글: string) -> string {
  if 글 == "" { "&nbsp;" } else { 글 }
}

function 줄그리기(그릴줄: 줄) -> string {
  match 그릴줄.종류 {
    case 구분 => "<div class=\"mp-hr\"></div>"
    case 코드 => "<div class=\"mp-code\">" + 빈칸대체(이스케이프(그릴줄.글)) + "</div>"
    case 제목1 | 제목2 | 제목3 => "<div class=\"mp-h\">" + 빈칸대체(그릴줄.글) + "</div>"
    case 인용 => "<div class=\"mp-quote\">" + 빈칸대체(그릴줄.글) + "</div>"
    case 문단 | 목록 => "<div class=\"mp-line\">" + 빈칸대체(그릴줄.글) + "</div>"
  }
}

// 높이를 고정하고 넘침을 숨겨 상자가 내용 때문에 커지지 않습니다.
function 쪽그리기(쪽: Array<줄>, 쪽번호: int, 쪽높이: int) -> string {
  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=\"" + 스타일 + "\">" + 쪽.map(줄그리기).join("") + "<div style=\"position:absolute;bottom:0.6em;right:1.4em;color:#888;font-size:0.85em\">{쪽번호}</div></div>"
}

function 쪽높이읽기(첫줄: string) -> Result<int, string> {
  if 첫줄 == "" { return Err(높이안내) }
  if !모두숫자인가(첫줄) { return Err(오류알림("페이지 높이는 양의 정수여야 합니다.")) }
  let 쪽높이 = toInt(첫줄).okOr(오류알림("페이지 높이가 너무 큽니다."))?
  if 쪽높이 < 3 { return Err(오류알림("페이지 높이는 3 이상이어야 합니다.")) }
  if 쪽높이 > 200 { return Err(오류알림("페이지 높이는 200 이하여야 합니다.")) }
  Ok(쪽높이)
}

function 쪽만들기(입력: string) -> Result<string, string> {
  let 입력줄들 = 입력.split("\n")
  let 쪽높이 = 쪽높이읽기(입력줄들[0].trim())?
  let 줄들 = 마크다운줄들(입력줄들.slice(1, 입력줄들.length), 줄너비)
  if 줄들.length == 0 { return Err("<div class=\"meta\">마크다운 본문을 입력하세요.</div>") }
  let 쪽들 = 쪽나누기(줄들, 쪽높이)
  let 그림 = map(0..<쪽들.length, i => 쪽그리기(쪽들[i], i + 1, 쪽높이))
  Ok("<div class=\"meta\">{쪽들.length}쪽, 페이지당 {쪽높이}줄</div><div class=\"mp-pages\">" + 그림.join("") + "</div>")
}

input() |> 쪽만들기 |> 결과풀기 |> print
