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, "' { 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) }