feat: 本地轉檔層補 .pptx——CP B1 最後一個 Office 格式

同 docx 路數:zip 開 OOXML、slide 按數字序、每頁「## 投影片 N」+文字+(備註);
空/純圖回 ErrNoText 明示不靜默。測試 4 案(中文/多頁排序/備註/空簡報)。
過程紅測一輪:NFKC 套整份 Markdown 把自家全形標籤「(備註)」轉半形——
改為只正規化 XML 抽出片段、結構標記正規化後組裝。collector go test 全綠(總管親跑)。
(實作=子 CC;驗證+commit=總管)
This commit is contained in:
2026-07-28 13:40:36 +08:00
parent 9df8d037ca
commit 903ca60154
4 changed files with 356 additions and 5 deletions
+58 -2
View File
@@ -58,7 +58,13 @@ var extractors = map[string]extractor{
".csv": extractCSV,
".xlsx": extractXLSX,
".pdf": extractPDF, // PDFium-via-wazero+10.43MB;用戶不必裝任何東西,見 convert_pdf.go
// .pptx → 同 docx 的 ZIP+XML 路數,數十行,體積幾乎不變
}
// preNormExtractors 裝「自己在 XML 文字片段上套完 NFKC」的抽取器。
// ConvertToText 對這類抽取器只跑 normalizeFormatting(無 NFKC),
// 確保它們後期組裝的結構標記(如全形「(備註)」)不被二次正規化。
var preNormExtractors = map[string]extractor{
".pptx": extractPPTX, // ZIP+XMLNFKC 在 pptxXMLText 的 CharData 層套,見 convert_pptx.go
}
// IsPlainText 回報這個副檔名是否本來就是純文字(不需要轉檔)。
@@ -72,7 +78,11 @@ func IsPlainText(path string) bool {
// CanConvert 回報這個副檔名是否有對應的抽取器(不含純文字)。
func CanConvert(path string) bool {
_, ok := extractors[strings.ToLower(filepath.Ext(path))]
ext := strings.ToLower(filepath.Ext(path))
if _, ok := extractors[ext]; ok {
return true
}
_, ok := preNormExtractors[ext]
return ok
}
@@ -81,11 +91,29 @@ func CanConvert(path string) bool {
// 純文字檔原樣回傳(只做正規化);其他格式派給對應抽取器。
// 抽不出文字回 ErrNoText,不支援的格式回 ErrUnsupported——兩者都**不是**「成功但空字串」,
// 呼叫端才有辦法給用戶明確訊號。
//
// 兩條抽取路徑:
// - extractors:後處理走完整 normalizeText(含 NFKC)。
// - preNormExtractors:抽取器自己在 XML CharData 層套 NFKC
// 後處理只跑 normalizeFormatting,避免結構標記被二次正規化。
func ConvertToText(path string, data []byte) (string, error) {
ext := strings.ToLower(filepath.Ext(path))
if IsPlainText(path) {
return normalizeText(string(data)), nil
}
if ex, ok := preNormExtractors[ext]; ok {
txt, err := ex(data)
if err != nil {
return "", err
}
txt = normalizeFormatting(txt)
if strings.TrimSpace(txt) == "" {
return "", ErrNoText
}
return txt, nil
}
ex, ok := extractors[ext]
if !ok {
return "", fmt.Errorf("%w%s", ErrUnsupported, ext)
@@ -134,3 +162,31 @@ func normalizeText(s string) string {
}
return strings.TrimSpace(out)
}
// normalizeFormatting 與 normalizeText 做同樣的格式清理,
// 但**不**套 NFKC 正規化。給 preNormExtractors 的抽取器用:
// 它們已在 XML CharData 層套完 NFKC,結構標記在之後才組裝,
// 不應再被 NFKC 二次轉換(例如全形「(備註)」→ 半形「(備註)」)。
func normalizeFormatting(s string) string {
s = strings.ReplaceAll(s, "\r\n", "\n")
s = strings.ReplaceAll(s, "\r", "\n")
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
if r == '\n' || r == '\t' {
b.WriteRune(r)
continue
}
if unicode.IsControl(r) {
continue
}
b.WriteRune(r)
}
out := b.String()
for strings.Contains(out, "\n\n\n") {
out = strings.ReplaceAll(out, "\n\n\n", "\n\n")
}
return strings.TrimSpace(out)
}