mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-30 04:29:24 +08:00
Porting change from #17477 python to go, parse table into chunk for markdown file Before: <img width="3606" height="1805" alt="image" src="https://github.com/user-attachments/assets/363698be-c182-40cf-a167-0f0c13a68b12" /> After: <img width="3606" height="1805" alt="image" src="https://github.com/user-attachments/assets/9d0d18cb-843e-4a1a-b176-16ac32998310" />
This commit is contained in:
@@ -30,8 +30,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
markdownlib "github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/ast"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
mdparser "github.com/gomarkdown/markdown/parser"
|
||||
)
|
||||
|
||||
// mdImagePattern matches markdown inline image syntax: .
|
||||
@@ -94,8 +95,18 @@ func (p *MarkdownParser) ConfigureFromSetup(setup map[string]any) {
|
||||
// the base64-encoded image payload. The legacy debug-print path has
|
||||
// been removed; callers consume ParseResult directly.
|
||||
func (p *MarkdownParser) ParseWithResult(ctx context.Context, filename string, data []byte) ParseResult {
|
||||
doc := markdownNew().Parse(data)
|
||||
rawText := string(data)
|
||||
if rendered, ok := renderMarkdownTablesInline(rawText); ok {
|
||||
return ParseResult{
|
||||
OutputFormat: "json",
|
||||
File: map[string]any{
|
||||
"name": filename,
|
||||
},
|
||||
JSON: []map[string]any{{"text": rendered, "doc_type_kwd": "text"}},
|
||||
}
|
||||
}
|
||||
|
||||
doc := markdownNew().Parse(data)
|
||||
|
||||
var items []map[string]any
|
||||
walkMarkdownBlocksWithImages(doc, rawText, &items, p.FlattenMediaToText)
|
||||
@@ -117,9 +128,108 @@ func (p *MarkdownParser) String() string {
|
||||
|
||||
// markdownNew is a thin constructor so the extension set is owned
|
||||
// in one place (both Parse and ParseWithResult consume it).
|
||||
func markdownNew() *parser.Parser {
|
||||
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
|
||||
return parser.NewWithExtensions(extensions)
|
||||
func markdownNew() *mdparser.Parser {
|
||||
extensions := mdparser.CommonExtensions | mdparser.AutoHeadingIDs | mdparser.NoEmptyLineBeforeBlock
|
||||
return mdparser.NewWithExtensions(extensions)
|
||||
}
|
||||
|
||||
func renderMarkdownTablesInline(text string) (string, bool) {
|
||||
lines := strings.SplitAfter(strings.ReplaceAll(strings.ReplaceAll(text, "\r\n", "\n"), "\r", "\n"), "\n")
|
||||
var buf strings.Builder
|
||||
changed := false
|
||||
inFence := false
|
||||
var fenceChar byte
|
||||
var fenceLen int
|
||||
|
||||
for i := 0; i < len(lines); {
|
||||
line := strings.TrimRight(lines[i], "\n")
|
||||
if ch, n, ok := markdownFenceMarker(line); ok {
|
||||
if inFence && ch == fenceChar && n >= fenceLen {
|
||||
inFence = false
|
||||
} else if !inFence {
|
||||
inFence = true
|
||||
fenceChar = ch
|
||||
fenceLen = n
|
||||
}
|
||||
buf.WriteString(lines[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if !inFence && i+1 < len(lines) && isMarkdownTableRow(line) && isMarkdownTableSeparator(strings.TrimRight(lines[i+1], "\n")) {
|
||||
start := i
|
||||
i += 2
|
||||
for i < len(lines) && isMarkdownTableRow(strings.TrimRight(lines[i], "\n")) {
|
||||
i++
|
||||
}
|
||||
for i < len(lines) && strings.TrimSpace(lines[i]) == "" {
|
||||
i++
|
||||
}
|
||||
tableHTML := markdownlib.ToHTML([]byte(strings.Join(lines[start:i], "")), markdownNew(), nil)
|
||||
buf.WriteString(strings.TrimRight(string(tableHTML), "\r\n"))
|
||||
buf.WriteByte('\n')
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
buf.WriteString(lines[i])
|
||||
i++
|
||||
}
|
||||
return buf.String(), changed
|
||||
}
|
||||
|
||||
func markdownFenceMarker(line string) (byte, int, bool) {
|
||||
trimmed := strings.TrimLeft(line, " \t")
|
||||
if len(line)-len(trimmed) > 3 || len(trimmed) < 3 {
|
||||
return 0, 0, false
|
||||
}
|
||||
ch := trimmed[0]
|
||||
if ch != '`' && ch != '~' {
|
||||
return 0, 0, false
|
||||
}
|
||||
n := 0
|
||||
for n < len(trimmed) && trimmed[n] == ch {
|
||||
n++
|
||||
}
|
||||
return ch, n, n >= 3
|
||||
}
|
||||
|
||||
func isMarkdownTableRow(line string) bool {
|
||||
cells := markdownTableCells(line)
|
||||
if len(cells) < 2 {
|
||||
return false
|
||||
}
|
||||
for _, cell := range cells {
|
||||
if strings.TrimSpace(cell) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isMarkdownTableSeparator(line string) bool {
|
||||
cells := markdownTableCells(line)
|
||||
if len(cells) < 2 {
|
||||
return false
|
||||
}
|
||||
for _, cell := range cells {
|
||||
cell = strings.ReplaceAll(strings.TrimSpace(cell), " ", "")
|
||||
if cell == "" {
|
||||
return false
|
||||
}
|
||||
cell = strings.TrimPrefix(strings.TrimSuffix(cell, ":"), ":")
|
||||
if strings.Trim(cell, "-") != "" || !strings.Contains(cell, "-") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func markdownTableCells(line string) []string {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.Contains(line, "|") {
|
||||
return nil
|
||||
}
|
||||
line = strings.TrimPrefix(strings.TrimSuffix(line, "|"), "|")
|
||||
return strings.Split(line, "|")
|
||||
}
|
||||
|
||||
// walkMarkdownBlocksWithImages emits one normalized item per
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -88,6 +89,32 @@ func TestMarkdownParser_ParseWithResult_NoImage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownParser_ParseWithResult_RendersTableInline(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
p, _ := NewMarkdownParser(GoMarkdown)
|
||||
md := "[M03] Health check package comparison:\n\n| Check item | Basic 699 CNY | Advanced 1299 CNY |\n| --- | --- | --- |\n| Blood routine / Urine routine | Yes | Yes |\n\nNote: All packages require fasting.\n"
|
||||
res := p.ParseWithResult(ctx, "test.md", []byte(md))
|
||||
if res.Err != nil {
|
||||
t.Fatalf("ParseWithResult: %v", res.Err)
|
||||
}
|
||||
if len(res.JSON) != 1 {
|
||||
t.Fatalf("len(JSON) = %d, want 1", len(res.JSON))
|
||||
}
|
||||
text, _ := res.JSON[0]["text"].(string)
|
||||
if got, _ := res.JSON[0]["doc_type_kwd"].(string); got != "text" {
|
||||
t.Fatalf("doc_type_kwd = %q, want text", got)
|
||||
}
|
||||
if !strings.Contains(text, "<table>") || !strings.Contains(text, "<th>Check item</th>") {
|
||||
t.Fatalf("table was not rendered inline: %q", text)
|
||||
}
|
||||
if strings.Contains(text, "| Check item |") {
|
||||
t.Fatalf("raw markdown table leaked into text: %q", text)
|
||||
}
|
||||
if gap := text[strings.Index(text, "</table>"):strings.Index(text, "Note:")]; gap != "</table>\n" {
|
||||
t.Fatalf("gap after table = %q, want %q", gap, "</table>\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownParser_ConfigureFromSetup(t *testing.T) {
|
||||
p, _ := NewMarkdownParser(GoMarkdown)
|
||||
p.ConfigureFromSetup(map[string]any{
|
||||
|
||||
Reference in New Issue
Block a user