// 结构化导出:CSV(RFC 4180 转义)与 JSONL(逐行 JSON)。
//
// 输出以行为单位累积成字符串,由 CLI 决定写往 stdout 还是文件,
// 框架核心因此不依赖任何文件 IO —— 在 native / js / wasm 上行为一致。
///|
/// 单字段 CSV 转义:含逗号、引号、换行时整体加引号,内部引号翻倍。
pub fn csv_escape(field : String) -> String {
let need_quote = {
let cs = chars_of(field)
let mut n = false
for c in cs {
if c == ',' || c == '"' || c == '\n' || c == '\r' {
n = true
break
}
}
n
}
if !need_quote {
return field
}
let buf = StringBuilder()
buf.write_char('"')
for c in field {
if c == '"' {
buf.write_char('"')
buf.write_char('"')
} else {
buf.write_char(c)
}
}
buf.write_char('"')
buf.to_string()
}
///|
/// CSV 行组装(字段按传入顺序,CRLF 行尾符合 RFC 4180)。
pub fn csv_row(fields : Array[String]) -> String {
let buf = StringBuilder()
let mut first : Bool = true
for f in fields {
if !first {
buf.write_char(',')
}
first = false
buf.write_string(csv_escape(f))
}
buf.write_string("\r\n")
buf.to_string()
}
///|
/// JSON 字符串值转义(引号/反斜杠/控制字符;其余 UTF-8 原样透传)。
pub fn json_escape(s : String) -> String {
let buf = StringBuilder()
buf.write_char('"')
for c in s {
let n = c.to_int()
if c == '"' {
buf.write_string("\\\"")
} else if c == '\\' {
buf.write_string("\\\\")
} else if c == '\n' {
buf.write_string("\\n")
} else if c == '\r' {
buf.write_string("\\r")
} else if c == '\t' {
buf.write_string("\\t")
} else if n < 0x20 {
// 固定 4 位小写十六进制
let hex = "0123456789abcdef"
let hcs = chars_of(hex)
let mut v : Int32 = n
let digits : Array[Char] = ['0', '0', '0', '0']
let mut k : Int32 = 3
while k >= 0 {
digits[k] = hcs[v % 16]
v = v / 16
k = k - 1
}
buf.write_string("\\u")
for d in digits {
buf.write_char(d)
}
} else {
buf.write_char(c)
}
}
buf.write_char('"')
buf.to_string()
}
///|
/// 通用记录行:字段对按传入顺序输出为一行 JSON。
///
/// 与 `jsonl_page` 的区别是不绑定任何结构体——配合 `extract_fields` /
/// `extract_records` 使用,字段名来自调用方的 `FieldSpec`。
pub fn json_record(fields : Array[(String, String)]) -> String {
let buf = StringBuilder()
buf.write_char('{')
let mut first : Bool = true
for kv in fields {
let (k, v) = kv
if !first {
buf.write_char(',')
}
first = false
buf.write_string(json_escape(k))
buf.write_char(':')
buf.write_string(json_escape(v))
}
buf.write_char('}')
buf.to_string()
}
///|
/// 通用记录行:字段对按传入顺序输出为一行 CSV(字段名作为表头来源)。
pub fn csv_record(fields : Array[(String, String)]) -> String {
let values : Array[String] = []
for kv in fields {
let (_, v) = kv
values.push(v)
}
csv_row(values)
}
///|
/// 通用表头行。
pub fn csv_record_header(fields : Array[(String, String)]) -> String {
let names : Array[String] = []
for kv in fields {
let (k, _) = kv
names.push(k)
}
csv_row(names)
}
///|
/// JSONL 行:抓取结果一行一条记录。
pub fn jsonl_page(p : PageResult) -> String {
let buf = StringBuilder()
buf.write_string("{\"url\":")
buf.write_string(json_escape(p.url))
buf.write_string(",\"status\":")
buf.write_string(p.status.to_string())
buf.write_string(",\"depth\":")
buf.write_string(p.depth.to_string())
buf.write_string(",\"links_found\":")
buf.write_string(p.links_found.to_string())
match p.title {
Some(t) => {
buf.write_string(",\"title\":")
buf.write_string(json_escape(t))
}
None => ()
}
match p.error {
Some(e) => {
buf.write_string(",\"error\":")
buf.write_string(json_escape(e))
}
None => ()
}
buf.write_char('}')
buf.to_string()
}
///|
/// CSV 表头(与 jsonl_page 字段对应)。
pub fn csv_header() -> String {
csv_row(["url", "status", "depth", "links_found", "title", "error"])
}
///|
/// CSV 数据行:Option 字段以空串表示缺失。
pub fn csv_page(p : PageResult) -> String {
csv_row([
p.url,
p.status.to_string(),
p.depth.to_string(),
p.links_found.to_string(),
match p.title {
Some(t) => t
None => ""
},
match p.error {
Some(e) => e
None => ""
},
])
}