///|
pub struct HereDoc {
  name : String
  content : String
  strip_tabs : Bool
  expand : Bool
} derive(Debug, Eq, ToJson)

///|
/// Locate unquoted heredoc operators. Delimiters may be quoted or backslash escaped.
fn heredoc_headers(
  source : String,
) -> Array[(String, Bool, Bool)] raise ParseError {
  let cs = source.to_array()
  let out = []
  let mut i = 0
  let mut quote : Char? = None
  let mut arithmetic = 0
  while i < cs.length() {
    let c = cs[i]
    if c == '\\' && quote != Some('\'') {
      i += 2
      continue
    }
    if quote is Some(q) {
      if c == q {
        quote = None
      }
      i += 1
      continue
    }
    if c == '\'' || c == '"' {
      quote = Some(c)
      i += 1
      continue
    }
    if c == '#' && (i == 0 || cs[i - 1] == ' ' || cs[i - 1] == '\t') {
      break
    }
    if c == '$' && i + 2 < cs.length() && cs[i + 1] == '(' && cs[i + 2] == '(' {
      arithmetic += 2
      i += 3
      continue
    }
    if arithmetic > 0 {
      if c == '(' {
        arithmetic += 1
      } else if c == ')' {
        arithmetic -= 1
      }
      i += 1
      continue
    }
    if c != '<' || i + 1 >= cs.length() || cs[i + 1] != '<' {
      i += 1
      continue
    }
    i += 2
    if i < cs.length() && cs[i] == '<' {
      i += 1
      continue
    }
    let strip_tabs = i < cs.length() && cs[i] == '-'
    if strip_tabs {
      i += 1
    }
    while i < cs.length() && (cs[i] == ' ' || cs[i] == '\t') {
      i += 1
    }
    let name = StringBuilder()
    let mut delimiter_quote : Char? = None
    let mut expand = true
    while i < cs.length() {
      let ch = cs[i]
      if delimiter_quote is Some(q) {
        if ch == q {
          delimiter_quote = None
        } else {
          name.write_char(ch)
        }
      } else if ch == '\'' || ch == '"' {
        delimiter_quote = Some(ch)
        expand = false
      } else if ch == '\\' {
        expand = false
        i += 1
        if i == cs.length() {
          raise Invalid("truncated heredoc delimiter")
        }
        name.write_char(cs[i])
      } else if ch == ' ' ||
        ch == '\t' ||
        ch == ';' ||
        ch == '|' ||
        ch == '&' ||
        ch == '<' ||
        ch == '>' {
        break
      } else {
        name.write_char(ch)
      }
      i += 1
    }
    if delimiter_quote != None || name.is_empty() {
      raise Invalid("invalid heredoc delimiter")
    }
    if out.length() == 16 {
      raise Invalid("heredoc count limit")
    }
    out.push((name.to_string(), strip_tabs, expand))
  }
  out
}