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:
+58
-2
@@ -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+XML;NFKC 在 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user