///|
/// Parse a bounded Fountain block grammar. Original input is always retained.
fn parse_unchecked(source : String) -> Document {
  let (visible, annotations, diagnostics) = mask_annotations(source)
  let lines = source_lines(visible)
  let elements : Array[Element] = []
  let body_start = parse_title(lines, elements)
  let mut in_dialogue = false
  for i = body_start; i < lines.length(); i = i + 1 {
    let line = lines[i]
    let text = stripped(line.content())
    if text == "" {
      if in_dialogue && source[line.start:line.end] == "  " {
        elements.push(element(line, Dialogue, ""))
        continue
      }
      in_dialogue = false
      continue
    }
    let before = i == 0 || stripped(lines[i - 1].content()) == ""
    let after = i + 1 == lines.length() ||
      stripped(lines[i + 1].content()) == ""
    if in_dialogue {
      let kind = if text.has_prefix("(") && text.has_suffix(")") {
        Parenthetical
      } else {
        Dialogue
      }
      elements.push(element(line, kind, text))
    } else if text.has_prefix("!") {
      elements.push(element(line, Action, text[1:].to_owned()))
    } else if before && after && is_scene(text) {
      let heading = if text.has_prefix(".") {
        text[1:].to_owned()
      } else {
        text
      }
      let (body, number) = scene_number(heading)
      elements.push(element(line, SceneHeading, body, detail=number))
    } else if special_block(line, text, before, after) is Some(block) {
      elements.push(block)
    } else if before && !after && is_character(text) {
      let cue = if text.has_prefix("@") {
        stripped(text[1:].to_owned())
      } else {
        text
      }
      let requested_dual = cue.has_suffix("^")
      let clean_cue = if requested_dual {
        stripped(cue[:cue.length() - 1].to_owned())
      } else {
        cue
      }
      let (name, extension) = character_parts(clean_cue)
      let dual = requested_dual && can_pair_dual(elements)
      if requested_dual && !dual {
        diagnostics.push({
          code: "FNT003",
          line: line.number,
          message: "Dual dialogue requires an adjacent preceding ordinary dialogue turn",
        })
      }
      elements.push(element(line, Character, name, detail=extension, dual~))
      in_dialogue = true
    } else {
      elements.push(element(line, Action, line.content()))
    }
  }
  { source, elements, diagnostics, annotations, }
}

///|
fn is_scene(text : String) -> Bool {
  if text.has_prefix(".") &&
    text.length() > 1 &&
    text[1] != '.' &&
    text[1] != ' ' &&
    text[1] != '\t' {
    return true
  }
  let upper = text.to_upper()
  for prefix in ["INT", "EXT", "EST", "INT./EXT", "INT/EXT", "I/E"] {
    if upper.has_prefix(prefix + ".") || upper.has_prefix(prefix + " ") {
      return true
    }
  }
  false
}

///|
fn scene_number(text : String) -> (String, String) {
  if !text.has_suffix("#") {
    return (text, "")
  }
  let mut i = text.length() - 2
  while i >= 0 {
    if text[i] == '#' {
      let n = text[i + 1:text.length() - 1].to_owned()
      if n != "" &&
        n
        .to_array()
        .all(fn(c) {
          (c >= 'A' && c <= 'Z') ||
          (c >= 'a' && c <= 'z') ||
          (c >= '0' && c <= '9') ||
          c == '-' ||
          c == '.'
        }) {
        return (stripped(text[:i].to_owned()), n)
      }
      break
    }
    i -= 1
  }
  (text, "")
}