initial public release
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package docx
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxDocxSize = 50 * 1024 * 1024 // 50MB
|
||||
)
|
||||
|
||||
// DocumentParser handles DOCX file parsing
|
||||
type DocumentParser struct {
|
||||
filePath string
|
||||
}
|
||||
|
||||
// NewDocumentParser creates a new DOCX parser
|
||||
func NewDocumentParser(filePath string) *DocumentParser {
|
||||
return &DocumentParser{filePath: filePath}
|
||||
}
|
||||
|
||||
// ParseDOCX extracts text from a DOCX file
|
||||
// Returns (markdown text, extracted images as bytes, error)
|
||||
func (p *DocumentParser) ParseDOCX() (string, map[string][]byte, error) {
|
||||
// Validate file size
|
||||
fileInfo, err := os.Stat(p.filePath)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to stat file: %w", err)
|
||||
}
|
||||
|
||||
if fileInfo.Size() > MaxDocxSize {
|
||||
return "", nil, fmt.Errorf("file exceeds 50MB limit: %d bytes", fileInfo.Size())
|
||||
}
|
||||
|
||||
// Validate ZIP magic number
|
||||
f, err := os.Open(p.filePath)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
magicBytes := make([]byte, 4)
|
||||
_, err = f.Read(magicBytes)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to read magic bytes: %w", err)
|
||||
}
|
||||
|
||||
if string(magicBytes) != "PK\x03\x04" {
|
||||
return "", nil, fmt.Errorf("not a valid DOCX file (invalid ZIP header)")
|
||||
}
|
||||
|
||||
// Open as ZIP
|
||||
f.Seek(0, 0)
|
||||
reader, err := zip.NewReader(f, fileInfo.Size())
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to open ZIP: %w", err)
|
||||
}
|
||||
|
||||
// Extract document.xml
|
||||
var docText string
|
||||
images := make(map[string][]byte)
|
||||
|
||||
for _, file := range reader.File {
|
||||
if file.Name == "word/document.xml" {
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to open document.xml: %w", err)
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
bytes, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to read document.xml: %w", err)
|
||||
}
|
||||
|
||||
docText, err = p.extractTextFromXML(bytes)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to extract text: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract images from media/
|
||||
if strings.HasPrefix(file.Name, "word/media/") {
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
imageBytes, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := filepath.Base(file.Name)
|
||||
images[fileName] = imageBytes
|
||||
}
|
||||
}
|
||||
|
||||
return docText, images, nil
|
||||
}
|
||||
|
||||
// extractTextFromXML parses word/document.xml and extracts plain text
|
||||
// Unsafe XML entity resolution is disabled by default in Go's xml.Decoder
|
||||
func (p *DocumentParser) extractTextFromXML(data []byte) (string, error) {
|
||||
var doc struct {
|
||||
XMLName xml.Name
|
||||
Body struct {
|
||||
Paragraphs []struct {
|
||||
Text string `xml:"w|t"`
|
||||
Runs []struct {
|
||||
Text string `xml:"w|t"`
|
||||
} `xml:"w|r"`
|
||||
} `xml:"w|p"`
|
||||
} `xml:"w|body"`
|
||||
}
|
||||
|
||||
decoder := xml.NewDecoder(strings.NewReader(string(data)))
|
||||
// XXE protection: Go's xml.Decoder is safe by default (no entity resolution)
|
||||
decoder.Strict = false
|
||||
|
||||
err := decoder.Decode(&doc)
|
||||
if err != nil && err != io.EOF {
|
||||
return "", fmt.Errorf("failed to decode XML: %w", err)
|
||||
}
|
||||
|
||||
// Extract text from paragraphs
|
||||
var result []string
|
||||
for _, p := range doc.Body.Paragraphs {
|
||||
var text []string
|
||||
for _, run := range p.Runs {
|
||||
if run.Text != "" {
|
||||
text = append(text, run.Text)
|
||||
}
|
||||
}
|
||||
if len(text) > 0 {
|
||||
result = append(result, strings.Join(text, ""))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n"), nil
|
||||
}
|
||||
Reference in New Issue
Block a user