///|
fn json_string(value : String) -> String {
  @json.to_json(value).stringify()
}

///|
fn kind_name(kind : @fs.FileKind) -> String {
  match kind {
    Directory => "directory"
    Regular => "file"
    SymLink => "link"
    Pipe => "fifo"
    Socket => "socket"
    BlockDevice => "block"
    CharDevice => "char"
    _ => "unknown"
  }
}

///|
priv enum JsonJob {
  Node(TreeNode, Int32, Bool)
  Text(String)
  End(Int32)
}

///|
async fn render_json(
  roots : Array[TreeNode],
  options : TreeOptions,
  counts : TreeCounts,
  output : TreeOutput,
) -> Unit {
  output.write("[\n")
  let pending : Array[JsonJob] = []
  for index = roots.length() - 1; index >= 0; index = index - 1 {
    pending.push(Text(if index + 1 < roots.length() { ",\n" } else { "\n" }))
    pending.push(Node(roots[index], 0, true))
  }
  while pending.pop() is Some(job) {
    match job {
      Text(text) => output.write(text)
      End(depth) => output.write("  ".repeat(depth + 1) + "]}")
      Node(node, depth, root) => {
        let indent = "  ".repeat(depth + 1)
        let out = StringBuilder()
        out.write_string(
          indent +
          "{\"type\":" +
          json_string(kind_name(node.kind)) +
          ",\"name\":" +
          json_string(node_label(node, options, root)),
        )
        if node.size is Some(size) {
          out.write_string(
            ",\"size\":" +
            (if options.size is (HumanSize | SiSize) {
              json_string(size_text(size, options.size).trim().to_owned())
            } else {
              size.to_string()
            }),
          )
        }
        if !node.children.is_empty() {
          out.write_string(",\"contents\":[\n")
          pending.push(End(depth))
          for index = node.children.length() - 1; index >= 0; index = index - 1 {
            pending.push(
              Text(
                if index + 1 < node.children.length() {
                  ",\n"
                } else {
                  "\n"
                },
              ),
            )
            pending.push(Node(node.children[index], depth + 1, false))
          }
        } else if node_note(node, options, root) is Some(note) {
          out.write_string(
            ",\"contents\":[{\"error\": " +
            json_string(note) +
            "}\n" +
            indent +
            "]}",
          )
        } else {
          out.write_char('}')
        }
        output.write(out.to_string())
      }
    }
  }
  let out = StringBuilder()
  if !options.no_report {
    out.write_string(",\n  {\"type\":\"report\"")
    if options.disk_usage {
      out.write_string(",\"size\":" + counts.size.to_string())
    }
    out.write_string(",\"directories\":" + counts.directories.to_string())
    if !options.directories_only {
      out.write_string(",\"files\":" + counts.files.to_string())
    }
    out.write_char('}')
  }
  out.write_string("\n]\n")
  output.write(out.to_string())
}

///|
fn html_escape(value : String) -> String {
  let out = StringBuilder()
  for c in value {
    out.write_string(
      match c {
        '&' => "&"
        '<' => "<"
        '>' => ">"
        '"' => """
        _ => c.to_string()
      },
    )
  }
  out.to_string()
}

///|
fn url_path(value : String) -> String {
  let out = StringBuilder()
  let hex = b"0123456789ABCDEF"
  for byte in @utf8.encode(value) {
    if byte
      is (b'A'..<=b'Z'
      | b'a'..<=b'z'
      | b'0'..<=b'9'
      | b'-'
      | b'.'
      | b'_'
      | b'~'
      | b'/') {
      out.write_char(byte.to_int().unsafe_to_char())
    } else {
      out.write_char('%')
      out.write_char(hex[byte.to_int() / 16].to_int().unsafe_to_char())
      out.write_char(hex[byte.to_int() % 16].to_int().unsafe_to_char())
    }
  }
  out.to_string()
}

///|
fn html_intro(title : String) -> String {
  "\n\n\n \n \n \n " +
  html_escape(title) +
  "\n" +
  " \n\n\n\t

" + html_escape(title) + "

\n" } ///| fn html_node_line( node : TreeNode, options : TreeOptions, base : String, root_path : String, prefix : String, last : Bool, root : Bool, ) -> String { let out = StringBuilder() out.write_char('\t') if !root { out.write_string(prefix + (if last { "`-- " } else { "|-- " })) } if node.size is Some(size) { out.write_string( ("[" + size_text(size, options.size)[1:].to_owned() + "] ").replace_all( old=" ", new=" ", ), ) } let offset = base.has_prefix("-") let host = if offset { base[1:].to_owned() } else { base } let path = if offset && node.path.has_prefix(root_path) { node.path[root_path.length():].to_owned() } else { node.path } let href = host + url_path(path) + (if node.kind == Directory { "/" } else { "" }) out.write_string(" "DIR" Pipe => "FIFO" Socket => "SOCK" BlockDevice => "BLOCK" CharDevice => "CHAR" _ => "NORM" } out.write_string(" class=\"" + class_name + "\"") } out.write_string( " href=\"" + html_escape(href) + "\">" + html_escape(node_label(node, options, root)) + "", ) if node_note(node, options, root) is Some(note) { out.write_string(" [" + html_escape(note) + "]") } out.write_string("
\n") out.to_string() } ///| async fn render_html( roots : Array[TreeNode], options : TreeOptions, base : String, counts : TreeCounts, output : TreeOutput, ) -> Unit { output.write(html_intro(options.title)) let pending : Array[(TreeNode, String, Bool, Bool, String)] = [] for index = roots.length() - 1; index >= 0; index = index - 1 { pending.push((roots[index], "", true, true, roots[index].path)) } while pending.pop() is Some((node, prefix, last, root, root_path)) { output.write( html_node_line(node, options, base, root_path, prefix, last, root), ) let child_prefix = if root { "" } else { prefix + (if last { "    " } else { "| " }) } for index = node.children.length() - 1; index >= 0; index = index - 1 { pending.push( ( node.children[index], child_prefix, index + 1 == node.children.length(), false, root_path, ), ) } } let out = StringBuilder() if !options.no_report { out.write_string("

\n\n") write_report(counts, out, options) out.write_string("\n

\n") } out.write_string( "\t
\n\t

cli/tree " + command_version + "

\n\n\n", ) output.write(out.to_string()) }