Files
llmgw/internal/kb/extract/odf.go
T
Ilker ManapandClaude Opus 5.5 a0cfafe772 Text from documents of many formats, and chunks of it, for knowledge bases
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>
2026-10-08 13:24:15 +02:00

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
}