Files

427 lines
9.9 KiB
Go

package parser
import (
"archive/zip"
"bytes"
"encoding/xml"
"fmt"
"io"
"path/filepath"
"resume-platform/pkg/utils"
"strings"
)
type Parser interface {
Parse(fileName string, reader io.Reader) (string, error)
}
func NewParser(fileName string) (Parser, error) {
ext := strings.ToLower(filepath.Ext(fileName))
switch ext {
case ".pdf":
return &PDFParser{}, nil
case ".docx":
return &DOCXParser{}, nil
case ".xlsx":
return &ExcelParser{}, nil
case ".md", ".markdown":
return &MarkdownParser{}, nil
case ".txt":
return &TextParser{}, nil
default:
return &TextParser{}, nil
}
}
func ParseFile(fileName string, reader io.Reader) (string, error) {
data, err := io.ReadAll(reader)
if err != nil {
return "", err
}
parser, err := NewParser(fileName)
if err != nil {
return "", err
}
text, err := parser.Parse(fileName, bytes.NewReader(data))
if err != nil {
return "", err
}
if text == "" && len(data) > 0 {
text = extractTextFromBinary(data)
}
return text, nil
}
func extractTextFromBinary(data []byte) string {
content := string(data)
var builder strings.Builder
for _, r := range content {
if r >= 0x20 && r <= 0x7E || r == '\n' || r == '\r' || r == '\t' ||
(r >= 0x4E00 && r <= 0x9FFF) || (r >= 0x3000 && r <= 0x303F) ||
(r >= 0xFF00 && r <= 0xFFEF) {
builder.WriteRune(r)
} else if r > 0 {
builder.WriteRune(' ')
}
}
return cleanText(builder.String())
}
type TextParser struct{}
func (p *TextParser) Parse(fileName string, reader io.Reader) (string, error) {
data, err := io.ReadAll(reader)
if err != nil {
return "", err
}
return cleanText(string(data)), nil
}
type MarkdownParser struct{}
func (p *MarkdownParser) Parse(fileName string, reader io.Reader) (string, error) {
data, err := io.ReadAll(reader)
if err != nil {
return "", err
}
return cleanText(string(data)), nil
}
type PDFParser struct{}
func (p *PDFParser) Parse(fileName string, reader io.Reader) (string, error) {
data, err := io.ReadAll(reader)
if err != nil {
return "", err
}
content := string(data)
if !strings.HasPrefix(content, "%PDF-") {
return "", fmt.Errorf("not a valid PDF file")
}
text := extractTextFromPDF(content)
return cleanText(text), nil
}
func extractTextFromPDF(content string) string {
var builder strings.Builder
inTextBlock := false
inStream := false
lines := strings.Split(content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.Contains(line, "/Length") && !inStream {
continue
}
if strings.HasPrefix(line, "stream") {
inStream = true
continue
}
if strings.HasPrefix(line, "endstream") {
inStream = false
continue
}
if strings.Contains(line, "BT") {
inTextBlock = true
continue
}
if strings.Contains(line, "ET") {
inTextBlock = false
continue
}
if inTextBlock && !inStream {
if strings.HasPrefix(line, "(") && strings.HasSuffix(line, ")") {
text := line[1 : len(line)-1]
text = strings.ReplaceAll(text, "\\n", "\n")
text = strings.ReplaceAll(text, "\\r", "\r")
text = strings.ReplaceAll(text, "\\t", "\t")
text = strings.ReplaceAll(text, "\\(", "(")
text = strings.ReplaceAll(text, "\\)", ")")
text = strings.ReplaceAll(text, "\\/", "/")
builder.WriteString(text)
builder.WriteString(" ")
} else if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
content := line[1 : len(line)-1]
parts := strings.Fields(content)
for _, part := range parts {
if strings.HasPrefix(part, "(") && strings.HasSuffix(part, ")") {
text := part[1 : len(part)-1]
text = strings.ReplaceAll(text, "\\n", "\n")
text = strings.ReplaceAll(text, "\\r", "\r")
text = strings.ReplaceAll(text, "\\t", "\t")
builder.WriteString(text)
builder.WriteString(" ")
}
}
}
}
}
return strings.TrimSpace(builder.String())
}
type DOCXParser struct{}
func (p *DOCXParser) Parse(fileName string, reader io.Reader) (string, error) {
data, err := io.ReadAll(reader)
if err != nil {
return "", err
}
fmt.Printf("DEBUG DOCXParser: file size: %d bytes\n", len(data))
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return "", fmt.Errorf("failed to open DOCX as zip: %w", err)
}
fmt.Printf("DEBUG DOCXParser: zip contains %d files\n", len(zipReader.File))
var text strings.Builder
foundDocument := false
for _, file := range zipReader.File {
fmt.Printf("DEBUG DOCXParser: zip file: %s (size: %d)\n", file.Name, file.UncompressedSize64)
if file.Name == "word/document.xml" {
foundDocument = true
f, err := file.Open()
if err != nil {
return "", fmt.Errorf("failed to open document.xml: %w", err)
}
defer f.Close()
xmlData, err := io.ReadAll(f)
if err != nil {
return "", fmt.Errorf("failed to read document.xml: %w", err)
}
fmt.Printf("DEBUG DOCXParser: document.xml size: %d bytes\n", len(xmlData))
parsedText := parseDOCXXML(string(xmlData))
fmt.Printf("DEBUG DOCXParser: parsed text from document.xml: %d chars\n", len(parsedText))
text.WriteString(parsedText)
} else if strings.HasPrefix(file.Name, "word/header") && strings.HasSuffix(file.Name, ".xml") {
f, err := file.Open()
if err != nil {
continue
}
defer f.Close()
xmlData, _ := io.ReadAll(f)
text.WriteString(parseDOCXXML(string(xmlData)))
text.WriteString("\n")
} else if strings.HasPrefix(file.Name, "word/footer") && strings.HasSuffix(file.Name, ".xml") {
f, err := file.Open()
if err != nil {
continue
}
defer f.Close()
xmlData, _ := io.ReadAll(f)
text.WriteString(parseDOCXXML(string(xmlData)))
text.WriteString("\n")
}
}
if !foundDocument {
fmt.Println("WARN DOCXParser: document.xml not found in zip")
return cleanText(extractTextFromBinary(data)), nil
}
result := cleanText(text.String())
fmt.Printf("DEBUG DOCXParser: final result length: %d chars\n", len(result))
if len(result) > 0 {
fmt.Printf("DEBUG DOCXParser: first 500 chars:\n%s\n", result[:utils.Min(len(result), 500)])
}
return result, nil
}
func parseDOCXXML(xmlContent string) string {
fmt.Printf("DEBUG parseDOCXXML: input length: %d chars\n", len(xmlContent))
xmlContent = removeNamespacePrefixes(xmlContent)
allTexts := extractTextContent(xmlContent)
var builder strings.Builder
for _, text := range allTexts {
text = strings.TrimSpace(text)
if text != "" {
builder.WriteString(text)
builder.WriteString("\n")
}
}
result := cleanText(builder.String())
fmt.Printf("DEBUG parseDOCXXML: extracted %d text elements, result length: %d\n", len(allTexts), len(result))
if len(result) > 0 {
fmt.Printf("DEBUG parseDOCXXML: first 500 chars:\n%s\n", result[:utils.Min(len(result), 500)])
}
if result == "" {
fmt.Println("WARN: Structured parsing returned empty, using fallback")
return cleanText(extractPlainTextFromXML(xmlContent))
}
return result
}
func extractTextContent(xmlContent string) []string {
var texts []string
inTag := false
currentText := ""
for _, r := range xmlContent {
if r == '<' {
inTag = true
if currentText != "" {
texts = append(texts, currentText)
currentText = ""
}
continue
}
if r == '>' {
inTag = false
continue
}
if !inTag {
currentText += string(r)
}
}
if currentText != "" {
texts = append(texts, currentText)
}
return texts
}
func removeNamespacePrefixes(xmlContent string) string {
prefixes := []string{"w:", "r:", "wp:", "wpc:", "wps:", "wpg:", "wpi:", "wne:", "mc:", "o:", "v:", "w10:", "wpc:", "wpg:", "wps:", "a:", "m:"}
for _, prefix := range prefixes {
xmlContent = strings.ReplaceAll(xmlContent, "<"+prefix, "<")
xmlContent = strings.ReplaceAll(xmlContent, "</"+prefix, "</")
xmlContent = strings.ReplaceAll(xmlContent, " "+prefix, " ")
}
return xmlContent
}
func extractPlainTextFromXML(xmlContent string) string {
var builder strings.Builder
inTag := false
for _, r := range xmlContent {
if r == '<' {
inTag = true
continue
}
if r == '>' {
inTag = false
continue
}
if !inTag {
builder.WriteRune(r)
}
}
return builder.String()
}
type ExcelParser struct{}
func (p *ExcelParser) Parse(fileName string, reader io.Reader) (string, error) {
data, err := io.ReadAll(reader)
if err != nil {
return "", err
}
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return "", fmt.Errorf("failed to open XLSX as zip: %w", err)
}
var text strings.Builder
for _, file := range zipReader.File {
if strings.HasPrefix(file.Name, "xl/worksheets/") && strings.HasSuffix(file.Name, ".xml") {
sheetName := strings.TrimPrefix(file.Name, "xl/worksheets/sheet")
sheetName = strings.TrimSuffix(sheetName, ".xml")
text.WriteString("===" + sheetName + "===\n\n")
f, err := file.Open()
if err != nil {
return "", fmt.Errorf("failed to open worksheet: %w", err)
}
defer f.Close()
xmlData, err := io.ReadAll(f)
if err != nil {
return "", fmt.Errorf("failed to read worksheet: %w", err)
}
text.WriteString(parseXLSXXML(string(xmlData)))
text.WriteString("\n\n")
}
}
return cleanText(text.String()), nil
}
func parseXLSXXML(xmlContent string) string {
type Cell struct {
Text string `xml:",chardata"`
}
type Row struct {
Cells []Cell `xml:"c"`
}
type SheetData struct {
Rows []Row `xml:"row"`
}
type Worksheet struct {
SheetData SheetData `xml:"sheetData"`
}
var ws Worksheet
err := xml.Unmarshal([]byte(xmlContent), &ws)
if err != nil {
return ""
}
var builder strings.Builder
for _, row := range ws.SheetData.Rows {
var cells []string
for _, cell := range row.Cells {
text := strings.TrimSpace(cell.Text)
if text != "" {
cells = append(cells, text)
}
}
if len(cells) > 0 {
builder.WriteString(strings.Join(cells, "\t"))
builder.WriteString("\n")
}
}
return strings.TrimSpace(builder.String())
}
func cleanText(text string) string {
return utils.CleanText(text)
}