{.push raises: [].}

import std/tables

import io/chafile
import types/opt
import utils/twtstr

type
  BracketState = enum
    bsNone, bsInBracket

  BlockType = enum
    btNone, btPar, btParBlank, btTableHeader, btTableBody, btList, btPre,
    btTabPre, btSpacePre, btBlockquote, btHTML, btHTMLPre, btComment,
    btLinkDef, btDefinition, btDefinitionPar, btDefinitionIndent,
    btDefinitionBlank

  ListState = enum
    lsNormal, lsAfterBlank, lsLastLine

  ListType = enum
    ltOl, ltUl, ltNoMark

  ListItemDesc = object
    t: ListType
    start: int32
    depth: int
    len: int

  List = object
    depth: int
    t: ListType
    par: bool

  LinkDefState = enum
    ldsLink, ldsTitle

  ParseState = object
    ofile: ChaFile
    blockData: string
    line: string # current line to be processed
    lists: seq[List]
    numBlockLines: int
    numTableCells: int
    numDLSpaces: int
    linkDefIdx: int
    linkDefName: string
    linkDefLink: string
    refMap: TableRef[string, tuple[link, title: string]]
    slurpBuf: string
    slurpIdx: int
    reprocess: bool
    hasp: bool
    skipp: bool
    listState: ListState
    blockType: BlockType
    linkDefState: LinkDefState

# Forward declarations
proc parse(state: var ParseState): Opt[void]

proc write(state: ParseState; c: char): Opt[void] =
  if state.ofile != nil:
    return state.ofile.write(c)
  ok()

proc write(state: ParseState; s: openArray[char]): Opt[void] =
  if state.ofile != nil:
    return state.ofile.write(s)
  ok()

proc writeLine(state: ParseState): Opt[void] =
  if state.ofile != nil:
    return state.ofile.writeLine()
  ok()

proc writeLine(state: ParseState; s: openArray[char]): Opt[void] =
  if state.ofile != nil:
    return state.ofile.writeLine(s)
  ok()

proc slurp(state: var ParseState): Opt[void] =
  if state.slurpIdx != -1:
    return ok()
  var state2 = ParseState(slurpIdx: -2, ofile: nil, refMap: state.refMap)
  if state.reprocess:
    state2.reprocess = true
    state2.line = state.line
    ?state2.parse()
  else:
    ?state2.parse()
  state.slurpBuf = move(state2.slurpBuf)
  state.slurpIdx = 0
  return ok()

proc getId(line: openArray[char]): string =
  result = ""
  var i = 0
  var bs = bsNone
  while i < line.len:
    case (let c = line[i]; c)
    of AsciiAlphaNumeric, '-', '_', '.': result &= c.toLowerAscii()
    of ' ': result &= '-'
    of '[':
      bs = bsInBracket
    of ']':
      if bs == bsInBracket:
        if i + 1 < line.len and line[i + 1] == '(':
          inc i
          while i < line.len:
            let c = line[i]
            if c == '\\':
              inc i
            elif c == ')':
              break
            inc i
        bs = bsNone
    else: discard
    inc i

type InlineFlag = enum
  ifItalic, ifBold, ifDel

proc startsWithScheme(s: string): bool =
  for i, c in s:
    if i > 0 and c == ':':
      return true
    if c notin AsciiAlphaNumeric:
      break
  false

type ParseInlineContext = object
  i: int
  bracketChars: string
  bs: BracketState
  flags: set[InlineFlag]

const PreTags = ["pre", "script", "style", "textarea", "head"]

proc parseInTag(ctx: var ParseInlineContext; line: string; state: ParseState):
    Opt[void] =
  var buf = ""
  var i = ctx.i + 1
  while i < line.len:
    let c = line[i]
    if c == '>': # done
      if buf.startsWithScheme(): # link
        ?state.write("<A HREF='" & buf.htmlEscape() & "'>" & buf & "</A>")
      else: # tag
        let s = '<' & buf & '>'
        ?state.write(s)
        if PreTags.containsIgnoreCase(buf):
          let pi = i + 1
          i = line.find(s, i)
          if i == -1:
            i = line.high
          else:
            i += s.len
          if pi <= i:
            ?state.write(line.toOpenArray(pi, i))
      ctx.i = i
      return ok()
    elif c == '<':
      dec i
      break
    else:
      buf &= c
    inc i
  ?state.write("&lt;")
  ?state.write(buf)
  ctx.i = i
  ok()

proc append(ctx: var ParseInlineContext; s: string; state: ParseState):
    Opt[void] =
  if ctx.bs == bsInBracket:
    ctx.bracketChars &= s
  else:
    ?state.write(s)
  ok()

proc append(ctx: var ParseInlineContext; c: char; state: ParseState):
    Opt[void] =
  if ctx.bs == bsInBracket:
    ctx.bracketChars &= c
  else:
    ?state.write(c)
  ok()

type CommentState = enum
  csNone, csDash, csDashDash

proc parseComment(ctx: var ParseInlineContext; line: openArray[char];
    state: ParseState): Opt[void] =
  var i = ctx.i
  var cs = csNone
  var buf = ""
  while i < line.len:
    let c = line[i]
    if cs in {csNone, csDash} and c == '-':
      inc cs
    elif cs == csDashDash and c == '>':
      buf &= '>'
      break
    else:
      cs = csNone
    buf &= c
    inc i
  ?ctx.append(buf, state)
  ctx.i = i
  ok()

proc parseCode(ctx: var ParseInlineContext; line: openArray[char];
    state: ParseState): Opt[void] =
  let i = ctx.i + 1
  let j = line.find('`', i)
  if j != -1:
    ?ctx.append("<CODE>", state)
    ?ctx.append(line.toOpenArray(i, j - 1).htmlEscape(), state)
    ?ctx.append("</CODE>", state)
    ctx.i = j
  else:
    ?ctx.append('`', state)
  ok()

proc parseLinkDestination(link: var string; line: openArray[char];
    i: int): int =
  var i = i
  var quote = false
  var parens = 0
  let sc = line[i]
  if sc == '<':
    inc i
  while i < line.len:
    let c = line[i]
    if quote:
      quote = false
    elif sc == '<' and c == '>':
      break
    elif sc == '<' and c in {'<', '\n'} or sc != '<' and c in Controls:
      return -1
    elif sc != '<' and c in AsciiWhitespace:
      i = line.skipBlanks(i)
      break
    elif c == '\\':
      quote = true
    elif c == '(':
      inc parens
      link &= c
    elif c == ')' and sc != '>':
      if parens == 0:
        break
      dec parens
      link &= c
    else:
      link &= c
    inc i
  if sc != '<' and parens != 0 or quote:
    return -1
  return line.skipBlanks(i + int(sc == '<'))

proc parseTitle(title: var string; line: openArray[char]; i: int): int =
  let ec = line[i]
  var i = i + 1
  var quote = false
  while i < line.len:
    let c = line[i]
    if quote:
      quote = false
    elif c == '\\':
      quote = true
    elif c == ec:
      inc i
      break
    else:
      title &= c
    inc i
  return line.skipBlanks(i)

proc parseLinkBail(ctx: var ParseInlineContext; i: int; state: ParseState):
    Opt[void] =
  ctx.i = i
  return state.write('[' & ctx.bracketChars & ']')

proc parseLinkWrite(ctx: ParseInlineContext; url, title: string;
    state: ParseState): Opt[void] =
  ?state.write("<A HREF='" & url.htmlEscape())
  if title != "":
    ?state.write("' TITLE='" & title.htmlEscape())
  return state.write("'>" & ctx.bracketChars & "</A>")

proc parseLink(ctx: var ParseInlineContext; line: string;
    state: var ParseState): Opt[void] =
  var i = ctx.i + 1
  if i >= line.len:
    return ctx.parseLinkBail(i, state)
  if (let c = line[i]; c != '('):
    ?state.slurp()
    if c == '[':
      let j = line.find(']', i + 1)
      if j == -1:
        return ctx.parseLinkBail(i - 1, state)
      let s = line.substr(i + 1, j - 1).toLowerAscii()
      if s != "":
        let (link, title) = state.refMap.getOrDefault(s)
        if link == "":
          return ctx.parseLinkBail(i - 1, state)
        ctx.i = j
        return ctx.parseLinkWrite(link, title, state)
      else: # [link][]
        i += 2
    let s = ctx.bracketChars.toLowerAscii()
    let (link, title) = state.refMap.getOrDefault(s)
    if link == "":
      if c == '[':
        i -= 2
      return ctx.parseLinkBail(i - 1, state)
    ctx.i = i - 1
    return ctx.parseLinkWrite(link, title, state)
  let bi = i - 1
  i = line.skipBlanks(i + 1)
  if i >= line.len:
    return ctx.parseLinkBail(bi, state)
  var url = ""
  var j = url.parseLinkDestination(line, i)
  var title = ""
  if j != -1 and j < line.len and line[j] in {'(', '"', '\''}:
    j = title.parseTitle(line, j)
  if j == -1 or j >= line.len or line[j] != ')':
    return ctx.parseLinkBail(bi, state)
  ctx.i = j
  return ctx.parseLinkWrite(url, title, state)

proc parseImageAlt(text: var string; line: openArray[char]; i: int): int =
  var i = i
  var brackets = 0
  while i < line.len:
    let c = line[i]
    if c == '\\':
      inc i
    elif c == '<':
      while i < line.len and line[i] != '>':
        text &= c
        inc i
    elif c == '[':
      inc brackets
      text &= c
    elif line[i] == ']':
      if brackets == 0:
        break
      dec brackets
      text &= c
    else:
      text &= c
    inc i
  return i

proc parseImageWrite(ctx: var ParseInlineContext; link, title, alt: string;
    state: ParseState): Opt[void] =
  ?ctx.append("<IMG SRC='" & link.htmlEscape(), state)
  if title != "":
    ?ctx.append("' TITLE='" & title.htmlEscape(), state)
  if alt != "":
    ?ctx.append("' ALT='" & alt.htmlEscape(), state)
  return ctx.append("'>", state)

proc parseImage(ctx: var ParseInlineContext; line: string;
    state: var ParseState): Opt[void] =
  var alt = ""
  var i = alt.parseImageAlt(line, ctx.i + 2)
  if i == -1 or i + 1 >= line.len or line[i] != ']':
    return ctx.append("!", state)
  inc i
  let c = line[i]
  if c == '[':
    ?state.slurp()
    let j = line.find(']', i + 1)
    if j == -1:
      return ctx.append("!", state)
    let s = line.substr(i + 1, j - 1).toLowerAscii()
    let (link, title) = state.refMap.getOrDefault(s)
    if link == "":
      return ctx.append("!", state)
    ctx.i = j
    return ctx.parseImageWrite(link, title, alt, state)
  if c != '(':
    return ctx.append("!", state)
  var link = ""
  var j = link.parseLinkDestination(line, line.skipBlanks(i + 1))
  var title = ""
  if j != -1 and j < line.len and line[j] in {'(', '"', '\''}:
    j = title.parseTitle(line, j)
  if j == -1 or j >= line.len or line[j] != ')':
    return ctx.append("!", state)
  ctx.i = j
  return ctx.parseImageWrite(link, title, alt, state)

proc appendToggle(ctx: var ParseInlineContext; f: InlineFlag; s, e: string;
    state: ParseState): Opt[void] =
  if f notin ctx.flags:
    ctx.flags.incl(f)
    ?ctx.append(s, state)
  else:
    ctx.flags.excl(f)
    ?ctx.append(e, state)
  ok()

proc parseInline(state: var ParseState; line: string): Opt[void] =
  var ctx = ParseInlineContext()
  while ctx.i < line.len:
    let c = line[ctx.i]
    if c == '\\':
      inc ctx.i
      if ctx.i < line.len:
        let c = line[ctx.i]
        if c == '\n':
          ?ctx.append("<br>", state)
        else:
          ?ctx.append(c, state)
    elif (ctx.i > 0 and line[ctx.i - 1] notin AsciiWhitespace or
          ctx.i + 1 < line.len and line[ctx.i + 1] notin AsciiWhitespace) and
        (c == '*' or
          c == '_' and
            (ctx.i == 0 or line[ctx.i - 1] notin AsciiAlphaNumeric or
              ctx.i + 1 >= line.len or
              line[ctx.i + 1] notin AsciiAlphaNumeric + {'_'})):
      if ctx.i + 1 < line.len and line[ctx.i + 1] == c:
        ?ctx.appendToggle(ifBold, "<B>", "</B>", state)
        inc ctx.i
      else:
        ?ctx.appendToggle(ifItalic, "<I>", "</I>", state)
    elif c == '`':
      ?ctx.parseCode(line, state)
    elif c == '~' and ctx.i + 1 < line.len and line[ctx.i + 1] == '~':
      ?ctx.appendToggle(ifDel, "<DEL>", "</DEL>", state)
      inc ctx.i
    elif c == '!' and ctx.i + 1 < line.len and line[ctx.i + 1] == '[':
      ?ctx.parseImage(line, state)
    elif c == '[':
      if ctx.bs == bsInBracket:
        ?state.write('[' & ctx.bracketChars)
        ctx.bracketChars = ""
      ctx.bs = bsInBracket
    elif c == ']' and ctx.bs == bsInBracket:
      ?ctx.parseLink(line, state)
      ctx.bracketChars = ""
      ctx.bs = bsNone
    elif c == '<':
      ?ctx.parseInTag(line, state)
    elif ctx.i + 4 < line.len and line.toOpenArray(ctx.i, ctx.i + 3) == "<!--":
      ?ctx.append("<!--", state)
      ctx.i += 3
      ?ctx.parseComment(line, state)
    elif c == '\n' and ctx.i >= 2 and line[ctx.i - 1] == ' ' and
        line[ctx.i - 2] == ' ':
      ?ctx.append("<BR>", state)
    else:
      ?ctx.append(c, state)
    inc ctx.i
  if ctx.bs == bsInBracket:
    ?state.write("[")
  if ctx.bracketChars != "":
    ?state.write(ctx.bracketChars)
  if ifBold in ctx.flags:
    ?state.write("</B>")
  if ifItalic in ctx.flags:
    ?state.write("</I>")
  if ifDel in ctx.flags:
    ?state.write("</DEL>")
  ok()

proc getListDepth(line: string): ListItemDesc =
  var depth = 0
  for i, c in line:
    if c == '\t':
      depth += 8
    elif c == ' ':
      inc depth
    elif c in {'*', '-', '+'}:
      if i + 1 < line.len and line[i + 1] in {' ', '\t'}:
        return ListItemDesc(t: ltUl, depth: depth + 1, len: i + 1)
      return ListItemDesc(t: ltNoMark, depth: depth, len: i)
    elif c in AsciiDigit:
      let j = i
      var i = i + 1
      while i < line.len and line[i] in AsciiDigit:
        inc i
      let start = parseInt32(line.toOpenArray(j, i - 1)).get(-1)
      if i + 1 < line.len and line[i] == '.' and line[i + 1] in {' ', '\t'}:
        return ListItemDesc(t: ltOl, depth: depth + 1, len: i + 1, start: start)
      return ListItemDesc(t: ltNoMark, depth: depth, len: j)
    else:
      return ListItemDesc(t: ltNoMark, depth: depth, len: i)
  return ListItemDesc(t: ltNoMark, depth: -1, len: -1)

proc matchHTMLPreStart(line: string): bool =
  var tagn = ""
  for c in line.toOpenArray(1, line.high):
    if c in {' ', '\t', '>'}:
      break
    if c notin AsciiAlpha:
      return false
    tagn &= c.toLowerAscii()
  return tagn in PreTags

proc matchHTMLPreEnd(line: string): bool =
  var tagn = ""
  for i, c in line:
    if i == 0:
      if c != '<':
        return false
      continue
    if i == 1:
      if c != '/':
        return false
      continue
    if c in {' ', '\t', '>'}:
      break
    if c notin AsciiAlpha:
      return false
    tagn &= c.toLowerAscii()
  return tagn in PreTags

proc pushList(state: var ParseState; desc: ListItemDesc): Opt[void] =
  case desc.t
  of ltOl:
    if desc.start == 1:
      ?state.write("<OL>\n<LI>")
    else:
      ?state.write("<OL start=" & $desc.start & ">\n<LI>")
  of ltUl: ?state.write("<UL>\n<LI>")
  of ltNoMark: assert false
  state.lists.add(List(t: desc.t, depth: desc.depth))
  ok()

proc popList(state: var ParseState): Opt[void] =
  case state.lists.pop().t
  of ltOl: ?state.writeLine("</OL>")
  of ltUl: ?state.writeLine("</UL>")
  of ltNoMark: assert false
  ok()

proc writeHeading(state: var ParseState; n: int; text: string): Opt[void] =
  state.hasp = false
  let id = text.getId()
  ?state.write("<H" & $n & " id='" & id & "'><A HREF='#" & id &
    "' CLASS=heading>" & '#'.repeat(n) & "</A> ")
  ?state.parseInline(text)
  ?state.writeLine("</H" & $n & '>')
  ok()

const ThematicBreakChars = {'-', '*', '_'}

proc isThematicBreak(line: string): bool =
  if line.len < 3:
    return false
  let c0 = line[0]
  return c0 in ThematicBreakChars and AllChars - {c0} notin line

proc countTableCells(line: string; i: int): int =
  var i = i
  var pipe = false
  if i < line.len and line[i] == '|': # skip initial pipe
    pipe = true
    inc i
  var n = 1
  var seen = false # skip last pipe
  for i in i ..< line.len:
    let c = line[i]
    if c == '|':
      if seen:
        inc n
      seen = true
      pipe = true
    elif seen and c notin AsciiWhitespace:
      seen = false
      inc n
  if not pipe:
    return 0
  return n

proc closeP(state: var ParseState): Opt[void] =
  if state.hasp:
    state.hasp = false
    return state.write("</P>")
  ok()

# called after ": " or "~ " is seen.
# par should be set when calling after blank line
proc openDD(state: var ParseState; line: string; par: bool): Opt[void] =
  state.blockType = if par: btDefinitionPar else: btDefinition
  if line[0] == '\t':
    state.blockData = line.substr(2) & '\n'
    # pandoc thinks a tab stop is 4 spaces
    state.numDLSpaces = 4
  else:
    var i = line.find(AllChars - {' '}, 1)
    if i < 0:
      i = line.high
    state.blockData = line.substr(i) & '\n'
    state.numDLSpaces = i
  return state.write("<DD>")

proc parseNone(state: var ParseState; line: string): Opt[void] =
  let i = line.skipBlanks(0)
  if i == line.len:
    return ok()
  let c0 = line[0]
  if (let n = line.find(AllChars - {'#'}); n in 1..6 and line[n] == ' '):
    ?state.closeP()
    let L = n + 1
    var H = line.rfind(AllChars - {'#'})
    if H != -1 and line[H] == ' ':
      H = max(L - 1, H - 1)
    else:
      H = line.high
    ?state.writeHeading(n, line.substr(L, H))
  elif line.startsWith("<!--"):
    state.blockType = btComment
    state.reprocess = true
  elif c0 == '<' and line[^1] == '>':
    state.blockType = if line.matchHTMLPreStart(): btHTMLPre else: btHTML
    state.reprocess = true
  elif line.startsWith("```") or line.startsWith("~~~"):
    state.blockType = btPre
    state.blockData = line.substr(0, 2)
    ?state.write("<PRE>")
  elif c0 == '[' and (var i = line.find(']', i);
      i != -1 and i > 1 and i + 1 < line.len and line[i + 1] == ':'):
    state.blockType = btLinkDef
    state.linkDefState = ldsLink
    state.linkDefIdx = i + 2
    state.linkDefName = line.substr(1, i - 1).toLowerAscii()
    state.linkDefLink = ""
    state.reprocess = true
  elif c0 == '\t':
    state.blockType = btTabPre
    ?state.closeP()
    ?state.write("<PRE>")
    ?state.writeLine(line.substr(1))
  elif line.startsWith("    "):
    state.blockType = btSpacePre
    ?state.closeP()
    ?state.write("<PRE>")
    ?state.writeLine(line.substr(4))
  elif c0 == '>':
    state.blockType = btBlockquote
    ?state.closeP()
    let i = if line.len < 2 or line[1] != ' ': 1 else: 2
    state.blockData = line.substr(i) & "\n"
    ?state.write("<BLOCKQUOTE>")
  elif (let desc = line.getListDepth(); desc.t != ltNoMark):
    state.blockType = btList
    state.listState = lsNormal
    state.hasp = false
    ?state.pushList(desc)
    state.blockData = line.substr(desc.len + 1) & '\n'
  elif line.isThematicBreak():
    # avoid entering par state so we don't get mistaken for setext heading
    ?state.write("<HR>\n")
    state.hasp = false
  elif (let n = line.countTableCells(i); n > 0):
    state.blockData = line
    state.blockType = btTableHeader
    state.numTableCells = n
  else:
    state.blockType = btPar
    state.numBlockLines = 0
    state.reprocess = true
  ok()

proc parsePre(state: var ParseState; line: string): Opt[void] =
  if line.startsWith(state.blockData):
    state.blockType = btNone
    state.blockData = ""
    ?state.writeLine("</PRE>")
  else:
    ?state.writeLine(line.htmlEscape())
  ok()

proc flushPar(state: var ParseState): Opt[void] =
  if state.blockData != "":
    state.hasp = true
    if not state.skipp:
      ?state.writeLine("<P>")
    state.skipp = false
    ?state.parseInline(state.blockData)
    state.blockData = ""
  ok()

proc reparseBlockData(state: var ParseState; skipp: bool): Opt[void] =
  var state2 = ParseState(
    slurpIdx: 0,
    ofile: state.ofile,
    refMap: state.refMap,
    slurpBuf: move(state.blockData),
    skipp: skipp
  )
  state2.parse()

proc flushList(state: var ParseState): Opt[void] =
  if state.lists[^1].par and state.blockData != "":
    ?state.writeLine("<P>")
  ?state.reparseBlockData(skipp = true)
  while state.lists.len > 0:
    ?state.popList()
  state.blockType = btNone
  ok()

proc parseList(state: var ParseState; line: string): Opt[void] =
  if state.listState == lsLastLine:
    ?state.flushList()
  elif AllChars - {' ', '\t'} notin line:
    state.listState = lsAfterBlank
  elif line.isThematicBreak():
    ?state.flushList()
    state.reprocess = true
  else:
    let desc = line.getListDepth()
    if desc.t == ltNoMark:
      if state.lists[0].depth > desc.depth:
        if state.listState == lsAfterBlank:
          ?state.flushList()
          state.reprocess = true
        else:
          state.blockData &= line & '\n'
      else:
        if state.listState == lsAfterBlank:
          state.lists[^1].par = true
          ?state.reparseBlockData(skipp = false)
          while desc.depth < state.lists[^1].depth:
            ?state.popList()
        state.blockData &= line.substr(desc.len) & '\n'
    else:
      if state.listState == lsAfterBlank and state.lists[^1].t == desc.t:
        state.lists[^1].par = true
      if state.lists[^1].par:
        ?state.writeLine("<P>")
      ?state.reparseBlockData(skipp = true)
      while state.lists.len > 1 and (desc.depth < state.lists[^1].depth or
          desc.depth == state.lists[^1].depth and desc.t != state.lists[^1].t):
        ?state.popList()
      if state.lists.len == 0 or state.lists[^1].depth < desc.depth:
        ?state.pushList(desc)
      elif state.lists[^1].t != desc.t:
        ?state.popList()
        ?state.pushList(desc)
      else:
        ?state.write("<LI>")
      state.blockData = line.substr(desc.len + 1) & '\n'
    state.listState = lsNormal
  ok()

const DLStart = {':', '~'}

proc startsDD(line: string): bool =
  line.len >= 2 and line[0] in DLStart and line[1] in {' ', '\t'}

proc parsePar(state: var ParseState; line: string): Opt[void] =
  if line == "":
    if state.numBlockLines != 1 or false:
      state.blockType = btNone
      return state.flushPar()
    state.blockType = btParBlank
    return ok()
  let c0 = line[0]
  if c0 == '<' and line[^1] == '>':
    ?state.flushPar()
    if line.matchHTMLPreStart():
      state.blockType = btHTMLPre
    else:
      state.blockType = btHTML
    state.reprocess = true
  elif state.numBlockLines == 1 and line.startsDD():
    # DL closes P
    state.hasp = false
    ?state.write("<DL>")
    if state.blockData != "":
      ?state.write("<DT>")
      ?state.parseInline(state.blockData)
    ?state.openDD(line, par = false)
  elif line.startsWith("```") or line.startsWith("~~~"):
    ?state.flushPar()
    state.blockData = line.substr(0, 2)
    state.blockType = btPre
    state.hasp = false
    ?state.write("<PRE>")
  elif (let desc = line.getListDepth(); desc.t != ltNoMark):
    ?state.flushPar()
    state.blockType = btList
    state.listState = lsNormal
    state.hasp = false
    ?state.pushList(desc)
    state.blockData = line.substr(desc.len + 1) & '\n'
  elif c0 in {'-', '=', '*', '_', ' ', '\t'} and AllChars - {c0} notin line:
    if c0 in {' ', '\t'}: # lines with space only also count as blank
      ?state.flushPar()
      state.blockType = btNone
    elif state.blockData != "" and c0 in {'-', '='}: # setext heading
      let n = if c0 == '=': 1 else: 2
      ?state.writeHeading(n, state.blockData)
      state.blockData = ""
    elif line.len >= 3 and c0 in ThematicBreakChars:
      # thematic break
      ?state.flushPar()
      ?state.write("<HR>\n")
      state.hasp = false
      state.blockType = btNone
    else:
      state.blockData = line & '\n'
  else:
    state.blockData &= line & '\n'
  inc state.numBlockLines
  ok()

proc parseParBlank(state: var ParseState; line: string): Opt[void] =
  if line.startsDD():
    # DL closes P
    state.hasp = false
    ?state.write("<DL>")
    if state.blockData != "":
      ?state.write("<DT>")
      ?state.flushPar()
    return state.openDD(line, par = true)
  state.reprocess = true
  state.blockType = btNone
  return state.flushPar()

proc parseDefinition(state: var ParseState; line: string): Opt[void] =
  if line.startsDD():
    ?state.reparseBlockData(skipp = state.blockType != btDefinitionPar)
    return state.openDD(line, par = false)
  if AllChars - {' ', '\t'} notin line:
    ?state.reparseBlockData(skipp = state.blockType != btDefinitionPar)
    state.blockType = btDefinitionBlank
    return ok()
  state.blockData &= line & '\n'
  ok()

proc parseDefinitionIndent(state: var ParseState; line: string): Opt[void] =
  if line.startsDD():
    ?state.reparseBlockData(skipp = false)
    return state.openDD(line, par = false)
  elif AllChars - {' ', '\t'} notin line:
    ?state.reparseBlockData(skipp = false)
    state.blockType = btDefinitionBlank
  elif line.startsWith("\t"):
    state.blockData &= line.substr(1) & '\n'
  elif line.find(AllChars - {' '}) >= state.numDLSpaces:
    state.blockData &= line.substr(state.numDLSpaces) & '\n'
  else:
    state.reprocess = true
    state.blockType = btNone
    state.hasp = false
    return state.write("</DL>")
  ok()

proc parseDefinitionBlank(state: var ParseState; line: string): Opt[void] =
  if line.startsDD():
    if state.blockData != "":
      ?state.write("<DT>")
      ?state.flushPar()
    return state.openDD(line, par = true)
  if (line.find(AllChars - {' '}) >= state.numDLSpaces or
        line.startsWith("\t")) and
      AllChars - {' ', '\t'} in line:
    ?state.flushPar()
    state.blockType = btDefinitionIndent
    state.reprocess = true
    return ok()
  state.reprocess = true
  state.blockType = btNone
  state.hasp = false
  return state.write("</DL>")

proc parseTableHeaderBail(state: var ParseState; line: string): Opt[void] =
  var oline = move(state.blockData)
  state.blockType = btPar
  ?state.parsePar(oline)
  state.reprocess = true
  ok()

type TextAlign = enum
  taNone, taNoneDash, taLeft = "left", taRight = "right", taCenter = "center"

proc flushTableHeader(state: ParseState; buf: var string;
    textAlign: var TextAlign; n, j: var int) =
  buf &= " <TH"
  #TODO I guess the right thing to do here is to use col, but layout
  # doesn't do col yet...
  case textAlign
  of taNone, taNoneDash: discard
  of taLeft, taRight, taCenter: buf &= " align=" & $textAlign
  buf &= "> "
  let s = state.blockData.until('|', j)
  buf &= s
  j += s.len + 1
  textAlign = taNone
  inc n

proc parseTableHeader(state: var ParseState; line: string): Opt[void] =
  var i = line.skipBlanks(0)
  if i >= line.len:
    return state.parseTableHeaderBail(line)
  let c = line[i]
  if c == '|':
    inc i # skip initial pipe
  elif c == '-' and i + 1 < line.len and line[i + 1] == ' ':
    # oops, this is a list
    return state.parseTableHeaderBail(line)
  var j = state.blockData.skipBlanks(0)
  if state.blockData[j] == '|':
    inc j
  var buf = "<TABLE border><TR>"
  var textAlign = taNone
  var n = 0
  for i in i ..< line.len:
    let c = line[i]
    case c
    of AsciiWhitespace: discard
    of '|': state.flushTableHeader(buf, textAlign, n, j)
    of '-':
      case textAlign
      of taNone: textAlign = taNoneDash
      of taNoneDash, taLeft: discard
      of taCenter, taRight: # dash after right-flanking colon
        return state.parseTableHeaderBail(line)
    of ':':
      case textAlign
      of taNone: textAlign = taLeft # colon before dash
      of taNoneDash: textAlign = taRight # colon after dash
      of taLeft: textAlign = taCenter # colon after dash + center
      of taCenter, taRight: # colon after right-flanking colon
        return state.parseTableHeaderBail(line)
    else: # wrong char
      return state.parseTableHeaderBail(line)
  if textAlign != taNone:
    state.flushTableHeader(buf, textAlign, n, j)
  if n != state.numTableCells:
    return state.parseTableHeaderBail(line)
  ?state.parseInline(buf)
  state.blockType = btTableBody
  state.blockData = ""
  ok()

proc parseTableBody(state: var ParseState; line: string): Opt[void] =
  var buf = "<TR>"
  var i = line.skipBlanks(0)
  var n = 0
  var pipe = false
  if i < line.len and line[i] == '|':
    pipe = true
    inc i
  var hasCell = false
  var esc = false
  for i in i ..< line.len:
    let c = line[i]
    if c in AsciiWhitespace:
      buf &= c
    elif not esc and c == '|':
      pipe = true
      inc n
      if n >= state.numTableCells:
        break
      if not hasCell:
        buf &= " <TD> "
      hasCell = false
    elif c == '\\':
      esc = true
    else:
      if esc:
        buf &= '\\'
        esc = false
      if not hasCell:
        buf &= " <TD> "
        hasCell = true
      buf &= c
  if pipe:
    if hasCell:
      inc n
    while n < state.numTableCells:
      buf &= " <TD>"
      inc n
    ?state.parseInline(buf)
  else:
    ?state.write("</TABLE>")
    state.blockType = btNone
    state.reprocess = true
  ok()

proc parseHTML(state: var ParseState; line: string): Opt[void] =
  ?state.closeP()
  if AllChars - {' ', '\t'} notin line:
    ?state.parseInline(state.blockData)
    state.blockData = ""
    state.blockType = btNone
  else:
    state.blockData &= line & '\n'
  ok()

proc parseHTMLPre(state: var ParseState; line: string): Opt[void] =
  ?state.closeP()
  if line.matchHTMLPreEnd():
    ?state.write(state.blockData)
    ?state.write(line)
    state.blockData = ""
    state.blockType = btNone
  else:
    state.blockData &= line & '\n'
  ok()

proc parseTabPre(state: var ParseState; line: string): Opt[void] =
  if line.len == 0:
    inc state.numBlockLines
  elif line[0] != '\t':
    state.numBlockLines = 0
    ?state.write("</PRE>")
    state.reprocess = true
    state.blockType = btNone
  else:
    while state.numBlockLines > 0:
      ?state.writeLine()
      dec state.numBlockLines
    ?state.writeLine(line.toOpenArray(1, line.high).htmlEscape())
  ok()

proc parseSpacePre(state: var ParseState; line: string): Opt[void] =
  if line.len == 0:
    inc state.numBlockLines
  elif not line.startsWith("    "):
    state.numBlockLines = 0
    ?state.write("</PRE>")
    state.reprocess = true
    state.blockType = btNone
  else:
    while state.numBlockLines > 0:
      ?state.writeLine()
      dec state.numBlockLines
    ?state.writeLine(line.toOpenArray(4, line.high).htmlEscape())
  ok()

proc parseBlockquote(state: var ParseState; line: string): Opt[void] =
  if line.len == 0 or line[0] != '>':
    ?state.reparseBlockData(skipp = true)
    ?state.write("</BLOCKQUOTE>")
    state.reprocess = true
    state.blockType = btNone
  else:
    let i = if line.len < 2 or line[1] != ' ': 1 else: 2
    state.blockData &= line.substr(i) & '\n'
  ok()

proc parseComment(state: var ParseState; line: string): Opt[void] =
  let i = line.find("-->")
  if i != -1:
    ?state.write(line.substr(0, i + 2))
    state.blockType = btNone
    ?state.parseInline(line.substr(i + 3))
  else:
    ?state.writeLine(line)
  ok()

proc parseLinkDef(state: var ParseState; line: string): Opt[void] =
  let pi = state.linkDefIdx
  state.linkDefIdx = 0
  var i = line.skipBlanks(pi)
  if i >= line.len:
    if pi == 0:
      if state.linkDefLink != "":
        discard state.refMap.mgetOrPut(state.linkDefName,
          (move(state.linkDefLink), ""))
        state.blockData = ""
        state.blockType = btNone
      else:
        state.blockType = btPar
      state.reprocess = true
    else:
      state.blockData &= line
    return ok()
  if state.linkDefLink == "":
    i = state.linkDefLink.parseLinkDestination(line, i)
    if i == -1:
      state.blockType = btPar
      state.reprocess = true
      return ok()
    if i < line.len:
      i = line.skipBlanks(i)
    state.linkDefState = ldsTitle
  if i >= line.len: # next line
    state.blockData &= line
    return ok()
  var title = ""
  if line[i] in {'"', '\''}:
    i = title.parseTitle(line, i)
  if i == -1 or i < line.len:
    if pi == 0: # not the first line. put & reprocess
      discard state.refMap.mgetOrPut(state.linkDefName,
        (move(state.linkDefLink), move(title)))
      state.blockType = btNone
      state.blockData = ""
    else:
      state.blockType = btPar
    state.reprocess = true
    return ok()
  discard state.refMap.mgetOrPut(state.linkDefName,
    (move(state.linkDefLink), move(title)))
  state.blockData = ""
  state.blockType = btNone
  ok()

proc readLine(state: var ParseState; line: var string): Opt[bool] =
  let stdin = cast[ChaFile](stdin)
  let hadLine = line != "" or
    state.blockType in {btList, btDefinitionBlank, btParBlank}
  if state.slurpIdx < 0:
    if ?stdin.readLine(line):
      if state.slurpIdx == -2:
        state.slurpBuf &= line & '\n'
      return ok(true)
  else:
    if state.slurpIdx < state.slurpBuf.len:
      line = state.slurpBuf.until('\n', state.slurpIdx)
      state.slurpIdx += line.len + 1
      return ok(true)
  line = ""
  state.listState = lsLastLine
  ok(hadLine) # add one last iteration with a blank after EOF

proc parse(state: var ParseState): Opt[void] =
  while state.reprocess or ?state.readLine(state.line):
    state.reprocess = false
    case state.blockType
    of btNone: ?state.parseNone(state.line)
    of btPre: ?state.parsePre(state.line)
    of btTabPre: ?state.parseTabPre(state.line)
    of btSpacePre: ?state.parseSpacePre(state.line)
    of btBlockquote: ?state.parseBlockquote(state.line)
    of btList: ?state.parseList(state.line)
    of btPar: ?state.parsePar(state.line)
    of btParBlank: ?state.parseParBlank(state.line)
    of btDefinition, btDefinitionPar: ?state.parseDefinition(state.line)
    of btDefinitionIndent: ?state.parseDefinitionIndent(state.line)
    of btDefinitionBlank: ?state.parseDefinitionBlank(state.line)
    of btTableHeader: ?state.parseTableHeader(state.line)
    of btTableBody: ?state.parseTableBody(state.line)
    of btHTML: ?state.parseHTML(state.line)
    of btHTMLPre: ?state.parseHTMLPre(state.line)
    of btComment: ?state.parseComment(state.line)
    of btLinkDef: ?state.parseLinkDef(state.line)
  ok()

proc main*() =
  var state = ParseState(
    slurpIdx: -1,
    ofile: cast[ChaFile](stdout),
    refMap: newTable[string, tuple[link, title: string]]()
  )
  discard state.parse()

{.pop.} # raises: []
