// ============================================================
// Office(OOXML:docx / xlsx / pptx)
//
// 这三种格式本质都是 ZIP 包,内部是 XML 文档:
// docx → word/document.xml
// xlsx → xl/worksheets/sheet1.xml + xl/sharedStrings.xml
// pptx → ppt/slides/slideN.xml
//
// 解压交给 hustcer/fzip,XML 用内置解析器解析,这里只做文本抽取。
// ============================================================
///|
/// 读取 docx 全部文本(段落按换行拼接)
pub fn read_docx_text(path : String) -> String raise ReaderError {
let entries = read_zip_entries(path)
let doc = parse_xml(entry_text(entries, "word/document.xml"))
docx_paragraphs(doc).join("\n")
}
///|
/// 读取 docx 各段落文本(每段一个字符串)
pub fn read_docx_paragraphs(path : String) -> Array[String] raise ReaderError {
let entries = read_zip_entries(path)
let doc = parse_xml(entry_text(entries, "word/document.xml"))
docx_paragraphs(doc)
}
///|
/// 一个工作表:名称 + 二维表(行 → 单元格)
pub(all) struct ExcelSheet {
name : String
rows : Array[Array[String]]
} derive(Eq, @debug.Debug)
///|
/// 读取 xlsx 全部工作表(含多 sheet),共享字符串已解析为文本
pub fn read_excel_sheets(path : String) -> Array[ExcelSheet] raise ReaderError {
let entries = read_zip_entries(path)
let shared = parse_shared_strings(entries)
let refs = sheet_refs(entries)
let out : Array[ExcelSheet] = []
for kv in refs {
let (name, target) = kv
let sheet = parse_xml(entry_text(entries, target))
out.push({ name, rows: sheet_rows(sheet, shared), })
}
out
}
///|
/// 读取 xlsx 指定下标工作表为二维表(行 → 单元格),共享字符串已解析为文本。
/// 下标越界返回空表。
pub fn read_excel_sheet_rows(
path : String,
sheet_index : Int,
) -> Array[Array[String]] raise ReaderError {
let sheets = read_excel_sheets(path)
if sheet_index >= 0 && sheet_index < sheets.length() {
return sheets[sheet_index].rows
}
[]
}
///|
/// 读取 xlsx 第一个工作表为二维表(行 → 单元格)。
/// 多 sheet 时仅取第一张;需要其它 sheet 请用 read_excel_sheet_rows。
pub fn read_excel_first_sheet_rows(
path : String,
) -> Array[Array[String]] raise ReaderError {
read_excel_sheet_rows(path, 0)
}
///|
/// 读取 xlsx 指定下标工作表为字符串(单元格用制表符分隔,行用换行分隔)
pub fn read_excel_sheet_text(
path : String,
sheet_index : Int,
) -> String raise ReaderError {
read_excel_sheet_rows(path, sheet_index)
.map(fn(r) { r.join("\t") })
.join("\n")
}
///|
/// 读取 xlsx 第一个工作表为字符串(单元格用制表符分隔,行用换行分隔)
pub fn read_excel_first_sheet_text(path : String) -> String raise ReaderError {
read_excel_sheet_text(path, 0)
}
///|
/// 读取 pptx 每页文本(一页一个字符串)
pub fn read_pptx_text_by_slide(
path : String,
) -> Array[String] raise ReaderError {
let entries = read_zip_entries(path)
let slides = slide_names(entries)
slides.map(s => slide_text(parse_xml(entry_text(entries, s))))
}
///|
/// 读取 pptx 全部文本(各页按换行拼接)
pub fn read_pptx_text(path : String) -> String raise ReaderError {
read_pptx_text_by_slide(path).join("\n")
}
// ============================================================
// 内部辅助函数
// ============================================================
///|
/// 在包内条目中查找指定名称并解码为 UTF-8 字符串
fn entry_text(
entries : Array[ZipEntry],
name : String,
) -> String raise ReaderError {
for e in entries {
if e.name == name {
return @utf8.decode_lossy(e.content)
}
}
raise ReaderError::Parse("Office 包内未找到: " + name)
}
///|
/// 判断包内是否存在指定名称条目
fn has_entry(entries : Array[ZipEntry], name : String) -> Bool {
for e in entries {
if e.name == name {
return true
}
}
false
}
///|
/// 收集所有名称为 name 的后代元素(含自身),委托给 XmlElement::find_all
fn collect_elems(root : XmlElement, name : String) -> Array[XmlElement] {
root.find_all(name)
}
///|
/// docx:按段落收集 w:t 文本
fn docx_paragraphs(doc : XmlElement) -> Array[String] {
let paras = collect_elems(doc, "w:p")
paras.map(fn(p) { collect_elems(p, "w:t").map(fn(t) { t.text() }).join("") })
}
///|
/// xlsx:解析共享字符串表,返回按索引排列的字符串数组(无表则为空)
fn parse_shared_strings(
entries : Array[ZipEntry],
) -> Array[String] raise ReaderError {
if !has_entry(entries, "xl/sharedStrings.xml") {
return []
}
let sst = parse_xml(entry_text(entries, "xl/sharedStrings.xml"))
let sis = collect_elems(sst, "si")
sis.map(fn(si) { collect_elems(si, "t").map(fn(t) { t.text() }).join("") })
}
///|
/// xlsx:从 workbook.xml + 关系表解析出 (工作表名, 表文件路径) 列表,按显示顺序
fn sheet_refs(
entries : Array[ZipEntry],
) -> Array[(String, String)] raise ReaderError {
if !has_entry(entries, "xl/workbook.xml") {
return [("Sheet1", "xl/worksheets/sheet1.xml")]
}
let wb = parse_xml(entry_text(entries, "xl/workbook.xml"))
let sheets = collect_elems(wb, "sheet")
// 关系表:Id -> Target(Target 相对 xl/ 目录)
let rels : Array[(String, String)] = []
if has_entry(entries, "xl/_rels/workbook.xml.rels") {
let rel_doc = parse_xml(entry_text(entries, "xl/_rels/workbook.xml.rels"))
for rel in collect_elems(rel_doc, "Relationship") {
let id = match rel.attr("Id") {
Some(v) => v
None => continue
}
let target = match rel.attr("Target") {
Some(v) => v
None => continue
}
rels.push((id, target))
}
}
let out : Array[(String, String)] = []
let mut i = 1
for s in sheets {
let name = match s.attr("name") {
Some(n) => n
None => "Sheet" + i.to_string()
}
let target = match s.attr("r:id") {
Some(rid) => resolve_target(rels, rid)
None => ""
}
let full = if target == "" {
"xl/worksheets/sheet" + i.to_string() + ".xml"
} else {
"xl/" + target
}
out.push((name, full))
i = i + 1
}
if out.length() > 0 {
return out
}
[("Sheet1", "xl/worksheets/sheet1.xml")]
}
///|
/// xlsx:在关系表中按 Id 查找 Target
fn resolve_target(rels : Array[(String, String)], id : String) -> String {
for kv in rels {
let (rid, target) = kv
if rid == id {
return target
}
}
""
}
///|
/// xlsx:把 sheet 解析为二维表
fn sheet_rows(
sheet : XmlElement,
shared : Array[String],
) -> Array[Array[String]] {
let rows = collect_elems(sheet, "row")
rows.map(fn(r) { collect_elems(r, "c").map(fn(c) { cell_text(c, shared) }) })
}
///|
/// xlsx:读取单个单元格文本(支持共享字符串 / 内联字符串 / 数字 / 布尔)
fn cell_text(c : XmlElement, shared : Array[String]) -> String {
match c.attr("t") {
Some("s") => {
let vs = collect_elems(c, "v")
if vs.length() > 0 {
let idx = parse_int(vs[0].text())
if idx >= 0 && idx < shared.length() {
return shared[idx]
}
}
""
}
Some("inlineStr") => collect_elems(c, "t").map(fn(t) { t.text() }).join("")
_ => {
let vs = collect_elems(c, "v")
if vs.length() > 0 {
return vs[0].text()
}
let ts = collect_elems(c, "t")
if ts.length() > 0 {
return ts[0].text()
}
""
}
}
}
///|
/// pptx:按编号升序收集幻灯片文件名
fn slide_names(entries : Array[ZipEntry]) -> Array[String] {
let names : Array[String] = []
for e in entries {
if e.name.has_prefix("ppt/slides/slide".view()) &&
e.name.has_suffix(".xml".view()) {
names.push(e.name)
}
}
names.sort_by(fn(a, b) { slide_number(a) - slide_number(b) })
names
}
///|
/// pptx:从文件名提取幻灯片编号,如 "ppt/slides/slide12.xml" → 12
fn slide_number(name : String) -> Int {
let n = name.length()
let mut v = 0
let mut i = 0
while i < n {
let ch = name[i]
if ch >= '0' && ch <= '9' {
v = v * 10 + (ch.to_int() - '0'.to_int())
} else if ch == '.' {
break
} else {
v = 0
}
i = i + 1
}
v
}
///|
/// pptx:抽取单页文本(按 a:p 段落、a:t 文本)
fn slide_text(slide : XmlElement) -> String {
let paras = collect_elems(slide, "a:p")
paras
.map(fn(p) { collect_elems(p, "a:t").map(fn(t) { t.text() }).join("") })
.join("\n")
}
///|
/// 解析整数(支持可选负号),供共享字符串索引、幻灯片编号等使用
fn parse_int(s : String) -> Int {
let n = s.length()
let mut i = 0
let mut sign = 1
if n > 0 && s[0] == '-' {
sign = -1
i = 1
}
let mut v = 0
while i < n {
let ch = s[i]
if ch >= '0' && ch <= '9' {
v = v * 10 + (ch.to_int() - '0'.to_int())
} else {
break
}
i = i + 1
}
sign * v
}