PDF, Word, Excel, PowerPoint, OpenDocument, RTF, EPUB, HTML, MHTML, mail with its attachments, notebooks, subtitles, zip archives and text and source files in any common encoding, in pure Go; legacy Office formats through LibreOffice and scanned pages through Tesseract when they are installed. Chunks of about 500 tokens under their headings, overlapping by a paragraph. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
118 lines
2.5 KiB
Go
118 lines
2.5 KiB
Go
package extract
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
)
|
|
|
|
// odfText reads an OpenDocument file (LibreOffice's ODT, ODS, ODP): its
|
|
// content.xml.
|
|
func odfText(b []byte) (string, error) {
|
|
zr, err := openZip(b)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
content, err := member(zr, "content.xml")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if content == nil {
|
|
return "", errors.New("not an OpenDocument file: no content.xml")
|
|
}
|
|
return odfXML(content)
|
|
}
|
|
|
|
// flatODFText reads the single-file XML form (.fodt, .fods, .fodp).
|
|
func flatODFText(b []byte) (string, error) { return odfXML(b) }
|
|
|
|
// odfXML reads OpenDocument's text model, which all three kinds share:
|
|
// text:h with an outline level is a heading, text:p a paragraph, list items,
|
|
// table rows and cells, slides (draw:page) and sheets (table:table) named as
|
|
// headings.
|
|
func odfXML(b []byte) (string, error) {
|
|
var o out
|
|
skip, slide := 0, 0
|
|
// Paragraphs inside a cell or a list item do not end the row or the list.
|
|
inCell, inItem := 0, 0
|
|
err := walkXML(b, func(e xmlEvent) {
|
|
switch {
|
|
case e.start:
|
|
switch e.name {
|
|
case "tracked-changes", "notes-configuration", "annotation", "frame-name":
|
|
skip++
|
|
case "h":
|
|
lvl, _ := strconv.Atoi(e.get("outline-level"))
|
|
o.heading(max(lvl, 1))
|
|
case "list-item":
|
|
inItem++
|
|
o.item()
|
|
case "table-cell":
|
|
inCell++
|
|
case "tab":
|
|
o.raw("\t")
|
|
case "s":
|
|
n, _ := strconv.Atoi(e.get("c"))
|
|
for range max(n, 1) {
|
|
o.raw(" ")
|
|
}
|
|
case "line-break":
|
|
o.newline()
|
|
case "page":
|
|
slide++
|
|
o.heading(2)
|
|
name := e.get("name")
|
|
if name == "" || isDefaultPageName(name) {
|
|
name = fmt.Sprintf("Slide %d", slide)
|
|
}
|
|
o.text(name)
|
|
o.para()
|
|
case "table":
|
|
if n := e.get("name"); n != "" {
|
|
o.heading(2)
|
|
o.text(n)
|
|
o.para()
|
|
}
|
|
}
|
|
case e.end:
|
|
switch e.name {
|
|
case "tracked-changes", "notes-configuration", "annotation", "frame-name":
|
|
skip--
|
|
case "h", "p":
|
|
switch {
|
|
case inCell > 0:
|
|
o.text(" ")
|
|
case inItem > 0:
|
|
o.newline()
|
|
default:
|
|
o.para()
|
|
}
|
|
case "list-item":
|
|
inItem--
|
|
o.newline()
|
|
case "table-cell":
|
|
inCell--
|
|
o.tab()
|
|
case "table-row":
|
|
o.newline()
|
|
case "table", "page":
|
|
o.para()
|
|
}
|
|
default:
|
|
if skip == 0 {
|
|
o.raw(e.text)
|
|
}
|
|
}
|
|
})
|
|
return o.String(), err
|
|
}
|
|
|
|
// isDefaultPageName is LibreOffice's own name for an unnamed slide, "page1".
|
|
func isDefaultPageName(s string) bool {
|
|
if len(s) < 5 || s[:4] != "page" {
|
|
return false
|
|
}
|
|
_, err := strconv.Atoi(s[4:])
|
|
return err == nil
|
|
}
|