extractText extracts readable text from HTML, removing scripts, styles, and tags.
(htmlContent string)
| 521 | |
| 522 | // extractText extracts readable text from HTML, removing scripts, styles, and tags. |
| 523 | func extractText(htmlContent string) string { |
| 524 | doc, err := html.Parse(strings.NewReader(htmlContent)) |
| 525 | if err != nil { |
| 526 | // Fallback: strip tags with regex |
| 527 | return stripHTMLTags(htmlContent) |
| 528 | } |
| 529 | |
| 530 | var sb strings.Builder |
| 531 | var extractNode func(*html.Node) |
| 532 | extractNode = func(n *html.Node) { |
| 533 | if n.Type == html.ElementNode { |
| 534 | // Skip script, style, head |
| 535 | if n.Data == "script" || n.Data == "style" || n.Data == "head" || n.Data == "noscript" { |
| 536 | return |
| 537 | } |
| 538 | // Add newlines for block elements |
| 539 | if isBlockElement(n.Data) { |
| 540 | sb.WriteString("\n") |
| 541 | } |
| 542 | } |
| 543 | |
| 544 | if n.Type == html.TextNode { |
| 545 | text := strings.TrimSpace(n.Data) |
| 546 | if text != "" { |
| 547 | sb.WriteString(text + " ") |
| 548 | } |
| 549 | } |
| 550 | |
| 551 | for c := n.FirstChild; c != nil; c = c.NextSibling { |
| 552 | extractNode(c) |
| 553 | } |
| 554 | |
| 555 | if n.Type == html.ElementNode && isBlockElement(n.Data) { |
| 556 | sb.WriteString("\n") |
| 557 | } |
| 558 | } |
| 559 | |
| 560 | extractNode(doc) |
| 561 | |
| 562 | // Clean up excessive whitespace |
| 563 | result := sb.String() |
| 564 | re := regexp.MustCompile(`\n{3,}`) |
| 565 | result = re.ReplaceAllString(result, "\n\n") |
| 566 | return strings.TrimSpace(result) |
| 567 | } |
| 568 | |
| 569 | func isBlockElement(tag string) bool { |
| 570 | switch tag { |