refactor(nlp): collapse 6 delimiter-parser implementations into one canonical helper (#17383) (#17387)
## Summary
Six sites used to read the same `parser_config.delimiter` field with
divergent grammars:
- `rag.nlp.get_delimiters` (PDF/DOCX/HTML/EPUB/JSON/CSV/XLSX/email/book)
- `rag.nlp.naive_merge` (custom-delimiter branch)
- `rag.nlp.naive_merge_with_images`
- `rag.nlp._build_cks`
- `deepdoc.parser.txt_parser.parser_txt` (.txt, code)
-
`deepdoc.parser.markdown_parser.MarkdownElementExtractor.get_delimiters`
The six implementations disagreed on bare-vs-wrapped chars, dedupe, sort
order, CRLF normalization, and `re.I` (#17384). The shipped default ``
`\n!?;。;!?` `` was a no-op for `.md` because the markdown path only
matched backtick-wrapped tokens.
## Changes
- **new:** `rag/nlp/delim.py` with `parse_delimiter_field` and
`compile_delimiter_pattern`. Single source of truth. CRLF normalization
at the top; longest-first stable sort; insertion-ordered dedupe; no
`re.I`.
- **refactor:** all six call sites delegate to the helper.
- `rag/nlp/__init__.py::get_delimiters` becomes a thin shim.
- `deepdoc/parser/txt_parser.py::parser_txt` drops the
`[encode/decode/unicode_escape]` round-trip.
- `deepdoc/parser/markdown_parser.py::get_delimiters` honors bare chars
(fixes [1]).
- **tests:** `test/unit_test/rag/test_delim.py` (85 tests) — helper,
acceptance table, frontend parity, static guard against re-inlining.
- **tests:** `test/unit_test/rag/test_delimiter_case_sensitive.py` (from
#17386) updated to retarget the static check at the new helper +
AST-based broader scan.
## Acceptance criteria
- All six sites produce the same regex pattern for the same input.
- Shipped default keeps working for `.txt` / `.pdf` / `.docx`.
- Shipped default for `.md` now splits (was a silent no-op).
- Tooltip example `` `\n##;` `` produces three effective delimiters
regardless of file type.
- Bare whitespace inputs split on every occurrence.
- Backtick-wrapped whitespace splits only on the exact N-char sequence.
- CRLF-line-ending documents split identically to LF-line-ending
documents.
- 123 tests pass (85 new + 38 existing).
## Rebase protocol
As #17385 and #17386 evolve, this branch will be rebased on top. The
only overlap between this PR's diff and the other two is
`test_delimiter_case_sensitive.py`, where #17383 modifies the static
check to point at the new helper location.
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-08-02 14:37:14 +05:30
|
|
|
//
|
|
|
|
|
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
|
|
|
|
//
|
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
|
//
|
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
//
|
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
|
// limitations under the License.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
package chunk
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"regexp"
|
|
|
|
|
"sort"
|
|
|
|
|
"strings"
|
|
|
|
|
"unicode/utf8"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Canonical parser for the parser_config.delimiter field.
|
|
|
|
|
//
|
|
|
|
|
// Mirrors Python rag/nlp/delim.py (#17383). A delimiter field is a string
|
|
|
|
|
// with the grammar:
|
|
|
|
|
//
|
|
|
|
|
// delimiter_field := token*
|
|
|
|
|
// token := backtick_wrapped | bare_char
|
|
|
|
|
// backtick_wrapped := "`" bare_char+ "`"
|
|
|
|
|
// bare_char := any single Unicode character except "`"
|
|
|
|
|
//
|
|
|
|
|
// Semantics:
|
|
|
|
|
// 1. Characters between matching backticks form one multi-character delimiter.
|
|
|
|
|
// 2. Any character outside backticks is its own single-character delimiter.
|
|
|
|
|
// 3. Results are deduplicated and sorted longest-first (stable for equal length).
|
|
|
|
|
// 4. CRLF and standalone CR are normalized to LF before parsing.
|
|
|
|
|
// 5. Matching is case-sensitive.
|
|
|
|
|
|
|
|
|
|
// backtickWrappedRE matches a backtick-wrapped multi-character token.
|
|
|
|
|
// Case-sensitive on purpose (see #17384).
|
|
|
|
|
var backtickWrappedRE = regexp.MustCompile("`([^`]+)`")
|
|
|
|
|
|
|
|
|
|
// NormalizeTextNewlines converts CRLF and standalone CR to LF.
|
|
|
|
|
func NormalizeTextNewlines(text string) string {
|
|
|
|
|
if text == "" {
|
|
|
|
|
return text
|
|
|
|
|
}
|
|
|
|
|
text = strings.ReplaceAll(text, "\r\n", "\n")
|
|
|
|
|
return strings.ReplaceAll(text, "\r", "\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HasWrappedDelimiter reports whether the delimiter field contains at least
|
|
|
|
|
// one backtick-wrapped token. Used to decide the historical "custom delimiter"
|
|
|
|
|
// mode that bypasses chunk_token_num. Separate from whether any delimiter is
|
|
|
|
|
// present after parsing (bare single-character delimiters still split).
|
|
|
|
|
func HasWrappedDelimiter(s string) bool {
|
|
|
|
|
if s == "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return backtickWrappedRE.MatchString(s)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseDelimiterField parses the delimiter field into delimiter strings.
|
|
|
|
|
//
|
|
|
|
|
// Returns nil for an empty field. Whitespace characters are valid
|
|
|
|
|
// single-character delimiters. Output is sorted longest-first and
|
|
|
|
|
// deduplicated while preserving first-occurrence order for equal-length
|
|
|
|
|
// items (the sort is stable). CRLF/CR inside the field are normalized to LF.
|
|
|
|
|
func ParseDelimiterField(s string) []string {
|
|
|
|
|
if s == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
normalized := NormalizeTextNewlines(s)
|
|
|
|
|
|
|
|
|
|
var delimiters []string
|
|
|
|
|
seen := make(map[string]struct{})
|
|
|
|
|
cursor := 0
|
|
|
|
|
for _, loc := range backtickWrappedRE.FindAllStringSubmatchIndex(normalized, -1) {
|
|
|
|
|
// loc: [fullStart, fullEnd, groupStart, groupEnd]
|
|
|
|
|
start, end := loc[0], loc[1]
|
|
|
|
|
for _, ch := range runesOf(normalized[cursor:start]) {
|
|
|
|
|
if _, ok := seen[ch]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[ch] = struct{}{}
|
|
|
|
|
delimiters = append(delimiters, ch)
|
|
|
|
|
}
|
|
|
|
|
token := normalized[loc[2]:loc[3]]
|
|
|
|
|
if token != "" {
|
|
|
|
|
if _, ok := seen[token]; !ok {
|
|
|
|
|
seen[token] = struct{}{}
|
|
|
|
|
delimiters = append(delimiters, token)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cursor = end
|
|
|
|
|
}
|
|
|
|
|
for _, ch := range runesOf(normalized[cursor:]) {
|
|
|
|
|
if _, ok := seen[ch]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[ch] = struct{}{}
|
|
|
|
|
delimiters = append(delimiters, ch)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sort.SliceStable(delimiters, func(i, j int) bool {
|
|
|
|
|
return utf8.RuneCountInString(delimiters[i]) > utf8.RuneCountInString(delimiters[j])
|
|
|
|
|
})
|
|
|
|
|
return delimiters
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CompileDelimiterPattern builds an alternation regex from delimiter strings.
|
|
|
|
|
//
|
|
|
|
|
// Each delimiter is regexp.QuoteMeta'd so whitespace and metacharacters match
|
|
|
|
|
// literally. Returns nil when delimiters is empty or yields no non-empty
|
|
|
|
|
// entries. The pattern is intended for use with a capturing split (so
|
|
|
|
|
// delimiters appear in the split output) or FindAllStringIndex.
|
|
|
|
|
func CompileDelimiterPattern(delimiters []string) *regexp.Regexp {
|
|
|
|
|
if len(delimiters) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
escaped := make([]string, 0, len(delimiters))
|
|
|
|
|
for _, d := range delimiters {
|
|
|
|
|
if d == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
escaped = append(escaped, regexp.QuoteMeta(d))
|
|
|
|
|
}
|
|
|
|
|
if len(escaped) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return regexp.MustCompile(strings.Join(escaped, "|"))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-06 16:10:30 +08:00
|
|
|
// CompileDelimiterPatternList builds an alternation regex from a
|
|
|
|
|
// TokenChunker-style []string delimiter list, with a shared policy for how
|
|
|
|
|
// backtick-wrapped and bare entries are treated. Every non-empty entry is
|
|
|
|
|
// regexp.QuoteMeta'd and the result is sorted longest-first (stable by rune
|
|
|
|
|
// count) so a longer delimiter always wins over a shorter prefix inside it.
|
refactor(nlp): collapse 6 delimiter-parser implementations into one canonical helper (#17383) (#17387)
## Summary
Six sites used to read the same `parser_config.delimiter` field with
divergent grammars:
- `rag.nlp.get_delimiters` (PDF/DOCX/HTML/EPUB/JSON/CSV/XLSX/email/book)
- `rag.nlp.naive_merge` (custom-delimiter branch)
- `rag.nlp.naive_merge_with_images`
- `rag.nlp._build_cks`
- `deepdoc.parser.txt_parser.parser_txt` (.txt, code)
-
`deepdoc.parser.markdown_parser.MarkdownElementExtractor.get_delimiters`
The six implementations disagreed on bare-vs-wrapped chars, dedupe, sort
order, CRLF normalization, and `re.I` (#17384). The shipped default ``
`\n!?;。;!?` `` was a no-op for `.md` because the markdown path only
matched backtick-wrapped tokens.
## Changes
- **new:** `rag/nlp/delim.py` with `parse_delimiter_field` and
`compile_delimiter_pattern`. Single source of truth. CRLF normalization
at the top; longest-first stable sort; insertion-ordered dedupe; no
`re.I`.
- **refactor:** all six call sites delegate to the helper.
- `rag/nlp/__init__.py::get_delimiters` becomes a thin shim.
- `deepdoc/parser/txt_parser.py::parser_txt` drops the
`[encode/decode/unicode_escape]` round-trip.
- `deepdoc/parser/markdown_parser.py::get_delimiters` honors bare chars
(fixes [1]).
- **tests:** `test/unit_test/rag/test_delim.py` (85 tests) — helper,
acceptance table, frontend parity, static guard against re-inlining.
- **tests:** `test/unit_test/rag/test_delimiter_case_sensitive.py` (from
#17386) updated to retarget the static check at the new helper +
AST-based broader scan.
## Acceptance criteria
- All six sites produce the same regex pattern for the same input.
- Shipped default keeps working for `.txt` / `.pdf` / `.docx`.
- Shipped default for `.md` now splits (was a silent no-op).
- Tooltip example `` `\n##;` `` produces three effective delimiters
regardless of file type.
- Bare whitespace inputs split on every occurrence.
- Backtick-wrapped whitespace splits only on the exact N-char sequence.
- CRLF-line-ending documents split identically to LF-line-ending
documents.
- 123 tests pass (85 new + 38 existing).
## Rebase protocol
As #17385 and #17386 evolve, this branch will be rebased on top. The
only overlap between this PR's diff and the other two is
`test_delimiter_case_sensitive.py`, where #17383 modifies the static
check to point at the new helper location.
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-08-02 14:37:14 +05:30
|
|
|
//
|
2026-08-06 16:10:30 +08:00
|
|
|
// - Backtick-wrapped entries (“ `abc` “) contribute their INNER content as
|
|
|
|
|
// the split pattern; the backticks are stripped. This is the Python
|
|
|
|
|
// token_chunker / historical Go compileDelimPattern convention.
|
|
|
|
|
// - Bare entries (e.g. ". ") behave according to keepBare:
|
|
|
|
|
// keepBare=false (the main `delimiters` list): a bare entry is IGNORED for
|
|
|
|
|
// the active pattern — it only influences merge paths when no custom
|
|
|
|
|
// pattern exists.
|
|
|
|
|
// keepBare=true (the `children_delimiters` list): a bare entry is KEPT
|
|
|
|
|
// active, because every non-empty children delimiter splits — including
|
|
|
|
|
// bare ones.
|
|
|
|
|
//
|
|
|
|
|
// Returns nil when no active entry remains. Prefer ParseDelimiterField for the
|
|
|
|
|
// single-string parser_config.delimiter field; this helper exists for the
|
|
|
|
|
// dataflow list API.
|
|
|
|
|
func CompileDelimiterPatternList(delims []string, keepBare bool) *regexp.Regexp {
|
|
|
|
|
var out []string
|
refactor(nlp): collapse 6 delimiter-parser implementations into one canonical helper (#17383) (#17387)
## Summary
Six sites used to read the same `parser_config.delimiter` field with
divergent grammars:
- `rag.nlp.get_delimiters` (PDF/DOCX/HTML/EPUB/JSON/CSV/XLSX/email/book)
- `rag.nlp.naive_merge` (custom-delimiter branch)
- `rag.nlp.naive_merge_with_images`
- `rag.nlp._build_cks`
- `deepdoc.parser.txt_parser.parser_txt` (.txt, code)
-
`deepdoc.parser.markdown_parser.MarkdownElementExtractor.get_delimiters`
The six implementations disagreed on bare-vs-wrapped chars, dedupe, sort
order, CRLF normalization, and `re.I` (#17384). The shipped default ``
`\n!?;。;!?` `` was a no-op for `.md` because the markdown path only
matched backtick-wrapped tokens.
## Changes
- **new:** `rag/nlp/delim.py` with `parse_delimiter_field` and
`compile_delimiter_pattern`. Single source of truth. CRLF normalization
at the top; longest-first stable sort; insertion-ordered dedupe; no
`re.I`.
- **refactor:** all six call sites delegate to the helper.
- `rag/nlp/__init__.py::get_delimiters` becomes a thin shim.
- `deepdoc/parser/txt_parser.py::parser_txt` drops the
`[encode/decode/unicode_escape]` round-trip.
- `deepdoc/parser/markdown_parser.py::get_delimiters` honors bare chars
(fixes [1]).
- **tests:** `test/unit_test/rag/test_delim.py` (85 tests) — helper,
acceptance table, frontend parity, static guard against re-inlining.
- **tests:** `test/unit_test/rag/test_delimiter_case_sensitive.py` (from
#17386) updated to retarget the static check at the new helper +
AST-based broader scan.
## Acceptance criteria
- All six sites produce the same regex pattern for the same input.
- Shipped default keeps working for `.txt` / `.pdf` / `.docx`.
- Shipped default for `.md` now splits (was a silent no-op).
- Tooltip example `` `\n##;` `` produces three effective delimiters
regardless of file type.
- Bare whitespace inputs split on every occurrence.
- Backtick-wrapped whitespace splits only on the exact N-char sequence.
- CRLF-line-ending documents split identically to LF-line-ending
documents.
- 123 tests pass (85 new + 38 existing).
## Rebase protocol
As #17385 and #17386 evolve, this branch will be rebased on top. The
only overlap between this PR's diff and the other two is
`test_delimiter_case_sensitive.py`, where #17383 modifies the static
check to point at the new helper location.
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-08-02 14:37:14 +05:30
|
|
|
for _, d := range delims {
|
|
|
|
|
if d == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(d, "`") && strings.HasSuffix(d, "`") && len(d) >= 2 {
|
|
|
|
|
inner := d[1 : len(d)-1]
|
|
|
|
|
if inner == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-08-06 16:10:30 +08:00
|
|
|
out = append(out, inner)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if !keepBare {
|
|
|
|
|
continue
|
refactor(nlp): collapse 6 delimiter-parser implementations into one canonical helper (#17383) (#17387)
## Summary
Six sites used to read the same `parser_config.delimiter` field with
divergent grammars:
- `rag.nlp.get_delimiters` (PDF/DOCX/HTML/EPUB/JSON/CSV/XLSX/email/book)
- `rag.nlp.naive_merge` (custom-delimiter branch)
- `rag.nlp.naive_merge_with_images`
- `rag.nlp._build_cks`
- `deepdoc.parser.txt_parser.parser_txt` (.txt, code)
-
`deepdoc.parser.markdown_parser.MarkdownElementExtractor.get_delimiters`
The six implementations disagreed on bare-vs-wrapped chars, dedupe, sort
order, CRLF normalization, and `re.I` (#17384). The shipped default ``
`\n!?;。;!?` `` was a no-op for `.md` because the markdown path only
matched backtick-wrapped tokens.
## Changes
- **new:** `rag/nlp/delim.py` with `parse_delimiter_field` and
`compile_delimiter_pattern`. Single source of truth. CRLF normalization
at the top; longest-first stable sort; insertion-ordered dedupe; no
`re.I`.
- **refactor:** all six call sites delegate to the helper.
- `rag/nlp/__init__.py::get_delimiters` becomes a thin shim.
- `deepdoc/parser/txt_parser.py::parser_txt` drops the
`[encode/decode/unicode_escape]` round-trip.
- `deepdoc/parser/markdown_parser.py::get_delimiters` honors bare chars
(fixes [1]).
- **tests:** `test/unit_test/rag/test_delim.py` (85 tests) — helper,
acceptance table, frontend parity, static guard against re-inlining.
- **tests:** `test/unit_test/rag/test_delimiter_case_sensitive.py` (from
#17386) updated to retarget the static check at the new helper +
AST-based broader scan.
## Acceptance criteria
- All six sites produce the same regex pattern for the same input.
- Shipped default keeps working for `.txt` / `.pdf` / `.docx`.
- Shipped default for `.md` now splits (was a silent no-op).
- Tooltip example `` `\n##;` `` produces three effective delimiters
regardless of file type.
- Bare whitespace inputs split on every occurrence.
- Backtick-wrapped whitespace splits only on the exact N-char sequence.
- CRLF-line-ending documents split identically to LF-line-ending
documents.
- 123 tests pass (85 new + 38 existing).
## Rebase protocol
As #17385 and #17386 evolve, this branch will be rebased on top. The
only overlap between this PR's diff and the other two is
`test_delimiter_case_sensitive.py`, where #17383 modifies the static
check to point at the new helper location.
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-08-02 14:37:14 +05:30
|
|
|
}
|
2026-08-06 16:10:30 +08:00
|
|
|
out = append(out, d)
|
refactor(nlp): collapse 6 delimiter-parser implementations into one canonical helper (#17383) (#17387)
## Summary
Six sites used to read the same `parser_config.delimiter` field with
divergent grammars:
- `rag.nlp.get_delimiters` (PDF/DOCX/HTML/EPUB/JSON/CSV/XLSX/email/book)
- `rag.nlp.naive_merge` (custom-delimiter branch)
- `rag.nlp.naive_merge_with_images`
- `rag.nlp._build_cks`
- `deepdoc.parser.txt_parser.parser_txt` (.txt, code)
-
`deepdoc.parser.markdown_parser.MarkdownElementExtractor.get_delimiters`
The six implementations disagreed on bare-vs-wrapped chars, dedupe, sort
order, CRLF normalization, and `re.I` (#17384). The shipped default ``
`\n!?;。;!?` `` was a no-op for `.md` because the markdown path only
matched backtick-wrapped tokens.
## Changes
- **new:** `rag/nlp/delim.py` with `parse_delimiter_field` and
`compile_delimiter_pattern`. Single source of truth. CRLF normalization
at the top; longest-first stable sort; insertion-ordered dedupe; no
`re.I`.
- **refactor:** all six call sites delegate to the helper.
- `rag/nlp/__init__.py::get_delimiters` becomes a thin shim.
- `deepdoc/parser/txt_parser.py::parser_txt` drops the
`[encode/decode/unicode_escape]` round-trip.
- `deepdoc/parser/markdown_parser.py::get_delimiters` honors bare chars
(fixes [1]).
- **tests:** `test/unit_test/rag/test_delim.py` (85 tests) — helper,
acceptance table, frontend parity, static guard against re-inlining.
- **tests:** `test/unit_test/rag/test_delimiter_case_sensitive.py` (from
#17386) updated to retarget the static check at the new helper +
AST-based broader scan.
## Acceptance criteria
- All six sites produce the same regex pattern for the same input.
- Shipped default keeps working for `.txt` / `.pdf` / `.docx`.
- Shipped default for `.md` now splits (was a silent no-op).
- Tooltip example `` `\n##;` `` produces three effective delimiters
regardless of file type.
- Bare whitespace inputs split on every occurrence.
- Backtick-wrapped whitespace splits only on the exact N-char sequence.
- CRLF-line-ending documents split identically to LF-line-ending
documents.
- 123 tests pass (85 new + 38 existing).
## Rebase protocol
As #17385 and #17386 evolve, this branch will be rebased on top. The
only overlap between this PR's diff and the other two is
`test_delimiter_case_sensitive.py`, where #17383 modifies the static
check to point at the new helper location.
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-08-02 14:37:14 +05:30
|
|
|
}
|
2026-08-06 16:10:30 +08:00
|
|
|
if len(out) == 0 {
|
refactor(nlp): collapse 6 delimiter-parser implementations into one canonical helper (#17383) (#17387)
## Summary
Six sites used to read the same `parser_config.delimiter` field with
divergent grammars:
- `rag.nlp.get_delimiters` (PDF/DOCX/HTML/EPUB/JSON/CSV/XLSX/email/book)
- `rag.nlp.naive_merge` (custom-delimiter branch)
- `rag.nlp.naive_merge_with_images`
- `rag.nlp._build_cks`
- `deepdoc.parser.txt_parser.parser_txt` (.txt, code)
-
`deepdoc.parser.markdown_parser.MarkdownElementExtractor.get_delimiters`
The six implementations disagreed on bare-vs-wrapped chars, dedupe, sort
order, CRLF normalization, and `re.I` (#17384). The shipped default ``
`\n!?;。;!?` `` was a no-op for `.md` because the markdown path only
matched backtick-wrapped tokens.
## Changes
- **new:** `rag/nlp/delim.py` with `parse_delimiter_field` and
`compile_delimiter_pattern`. Single source of truth. CRLF normalization
at the top; longest-first stable sort; insertion-ordered dedupe; no
`re.I`.
- **refactor:** all six call sites delegate to the helper.
- `rag/nlp/__init__.py::get_delimiters` becomes a thin shim.
- `deepdoc/parser/txt_parser.py::parser_txt` drops the
`[encode/decode/unicode_escape]` round-trip.
- `deepdoc/parser/markdown_parser.py::get_delimiters` honors bare chars
(fixes [1]).
- **tests:** `test/unit_test/rag/test_delim.py` (85 tests) — helper,
acceptance table, frontend parity, static guard against re-inlining.
- **tests:** `test/unit_test/rag/test_delimiter_case_sensitive.py` (from
#17386) updated to retarget the static check at the new helper +
AST-based broader scan.
## Acceptance criteria
- All six sites produce the same regex pattern for the same input.
- Shipped default keeps working for `.txt` / `.pdf` / `.docx`.
- Shipped default for `.md` now splits (was a silent no-op).
- Tooltip example `` `\n##;` `` produces three effective delimiters
regardless of file type.
- Bare whitespace inputs split on every occurrence.
- Backtick-wrapped whitespace splits only on the exact N-char sequence.
- CRLF-line-ending documents split identically to LF-line-ending
documents.
- 123 tests pass (85 new + 38 existing).
## Rebase protocol
As #17385 and #17386 evolve, this branch will be rebased on top. The
only overlap between this PR's diff and the other two is
`test_delimiter_case_sensitive.py`, where #17383 modifies the static
check to point at the new helper location.
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-08-02 14:37:14 +05:30
|
|
|
return nil
|
|
|
|
|
}
|
2026-08-06 16:10:30 +08:00
|
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|
|
|
|
return utf8.RuneCountInString(out[i]) > utf8.RuneCountInString(out[j])
|
refactor(nlp): collapse 6 delimiter-parser implementations into one canonical helper (#17383) (#17387)
## Summary
Six sites used to read the same `parser_config.delimiter` field with
divergent grammars:
- `rag.nlp.get_delimiters` (PDF/DOCX/HTML/EPUB/JSON/CSV/XLSX/email/book)
- `rag.nlp.naive_merge` (custom-delimiter branch)
- `rag.nlp.naive_merge_with_images`
- `rag.nlp._build_cks`
- `deepdoc.parser.txt_parser.parser_txt` (.txt, code)
-
`deepdoc.parser.markdown_parser.MarkdownElementExtractor.get_delimiters`
The six implementations disagreed on bare-vs-wrapped chars, dedupe, sort
order, CRLF normalization, and `re.I` (#17384). The shipped default ``
`\n!?;。;!?` `` was a no-op for `.md` because the markdown path only
matched backtick-wrapped tokens.
## Changes
- **new:** `rag/nlp/delim.py` with `parse_delimiter_field` and
`compile_delimiter_pattern`. Single source of truth. CRLF normalization
at the top; longest-first stable sort; insertion-ordered dedupe; no
`re.I`.
- **refactor:** all six call sites delegate to the helper.
- `rag/nlp/__init__.py::get_delimiters` becomes a thin shim.
- `deepdoc/parser/txt_parser.py::parser_txt` drops the
`[encode/decode/unicode_escape]` round-trip.
- `deepdoc/parser/markdown_parser.py::get_delimiters` honors bare chars
(fixes [1]).
- **tests:** `test/unit_test/rag/test_delim.py` (85 tests) — helper,
acceptance table, frontend parity, static guard against re-inlining.
- **tests:** `test/unit_test/rag/test_delimiter_case_sensitive.py` (from
#17386) updated to retarget the static check at the new helper +
AST-based broader scan.
## Acceptance criteria
- All six sites produce the same regex pattern for the same input.
- Shipped default keeps working for `.txt` / `.pdf` / `.docx`.
- Shipped default for `.md` now splits (was a silent no-op).
- Tooltip example `` `\n##;` `` produces three effective delimiters
regardless of file type.
- Bare whitespace inputs split on every occurrence.
- Backtick-wrapped whitespace splits only on the exact N-char sequence.
- CRLF-line-ending documents split identically to LF-line-ending
documents.
- 123 tests pass (85 new + 38 existing).
## Rebase protocol
As #17385 and #17386 evolve, this branch will be rebased on top. The
only overlap between this PR's diff and the other two is
`test_delimiter_case_sensitive.py`, where #17383 modifies the static
check to point at the new helper location.
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-08-02 14:37:14 +05:30
|
|
|
})
|
2026-08-06 16:10:30 +08:00
|
|
|
escaped := make([]string, 0, len(out))
|
|
|
|
|
for _, d := range out {
|
refactor(nlp): collapse 6 delimiter-parser implementations into one canonical helper (#17383) (#17387)
## Summary
Six sites used to read the same `parser_config.delimiter` field with
divergent grammars:
- `rag.nlp.get_delimiters` (PDF/DOCX/HTML/EPUB/JSON/CSV/XLSX/email/book)
- `rag.nlp.naive_merge` (custom-delimiter branch)
- `rag.nlp.naive_merge_with_images`
- `rag.nlp._build_cks`
- `deepdoc.parser.txt_parser.parser_txt` (.txt, code)
-
`deepdoc.parser.markdown_parser.MarkdownElementExtractor.get_delimiters`
The six implementations disagreed on bare-vs-wrapped chars, dedupe, sort
order, CRLF normalization, and `re.I` (#17384). The shipped default ``
`\n!?;。;!?` `` was a no-op for `.md` because the markdown path only
matched backtick-wrapped tokens.
## Changes
- **new:** `rag/nlp/delim.py` with `parse_delimiter_field` and
`compile_delimiter_pattern`. Single source of truth. CRLF normalization
at the top; longest-first stable sort; insertion-ordered dedupe; no
`re.I`.
- **refactor:** all six call sites delegate to the helper.
- `rag/nlp/__init__.py::get_delimiters` becomes a thin shim.
- `deepdoc/parser/txt_parser.py::parser_txt` drops the
`[encode/decode/unicode_escape]` round-trip.
- `deepdoc/parser/markdown_parser.py::get_delimiters` honors bare chars
(fixes [1]).
- **tests:** `test/unit_test/rag/test_delim.py` (85 tests) — helper,
acceptance table, frontend parity, static guard against re-inlining.
- **tests:** `test/unit_test/rag/test_delimiter_case_sensitive.py` (from
#17386) updated to retarget the static check at the new helper +
AST-based broader scan.
## Acceptance criteria
- All six sites produce the same regex pattern for the same input.
- Shipped default keeps working for `.txt` / `.pdf` / `.docx`.
- Shipped default for `.md` now splits (was a silent no-op).
- Tooltip example `` `\n##;` `` produces three effective delimiters
regardless of file type.
- Bare whitespace inputs split on every occurrence.
- Backtick-wrapped whitespace splits only on the exact N-char sequence.
- CRLF-line-ending documents split identically to LF-line-ending
documents.
- 123 tests pass (85 new + 38 existing).
## Rebase protocol
As #17385 and #17386 evolve, this branch will be rebased on top. The
only overlap between this PR's diff and the other two is
`test_delimiter_case_sensitive.py`, where #17383 modifies the static
check to point at the new helper location.
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-08-02 14:37:14 +05:30
|
|
|
escaped = append(escaped, regexp.QuoteMeta(d))
|
|
|
|
|
}
|
|
|
|
|
return regexp.MustCompile(strings.Join(escaped, "|"))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-06 16:10:30 +08:00
|
|
|
// CompileDelimiterListPattern compiles a TokenChunker-style []string delimiter
|
|
|
|
|
// list. It is CompileDelimiterPatternList with keepBare=false: backtick entries
|
|
|
|
|
// contribute their inner content and bare entries are ignored for the active
|
|
|
|
|
// pattern (they only steer merge paths when no custom pattern exists).
|
|
|
|
|
//
|
|
|
|
|
// Prefer ParseDelimiterField for the single-string parser_config.delimiter
|
|
|
|
|
// field. This helper exists for the dataflow list API.
|
|
|
|
|
func CompileDelimiterListPattern(delims []string) *regexp.Regexp {
|
|
|
|
|
return CompileDelimiterPatternList(delims, false)
|
|
|
|
|
}
|
|
|
|
|
|
refactor(nlp): collapse 6 delimiter-parser implementations into one canonical helper (#17383) (#17387)
## Summary
Six sites used to read the same `parser_config.delimiter` field with
divergent grammars:
- `rag.nlp.get_delimiters` (PDF/DOCX/HTML/EPUB/JSON/CSV/XLSX/email/book)
- `rag.nlp.naive_merge` (custom-delimiter branch)
- `rag.nlp.naive_merge_with_images`
- `rag.nlp._build_cks`
- `deepdoc.parser.txt_parser.parser_txt` (.txt, code)
-
`deepdoc.parser.markdown_parser.MarkdownElementExtractor.get_delimiters`
The six implementations disagreed on bare-vs-wrapped chars, dedupe, sort
order, CRLF normalization, and `re.I` (#17384). The shipped default ``
`\n!?;。;!?` `` was a no-op for `.md` because the markdown path only
matched backtick-wrapped tokens.
## Changes
- **new:** `rag/nlp/delim.py` with `parse_delimiter_field` and
`compile_delimiter_pattern`. Single source of truth. CRLF normalization
at the top; longest-first stable sort; insertion-ordered dedupe; no
`re.I`.
- **refactor:** all six call sites delegate to the helper.
- `rag/nlp/__init__.py::get_delimiters` becomes a thin shim.
- `deepdoc/parser/txt_parser.py::parser_txt` drops the
`[encode/decode/unicode_escape]` round-trip.
- `deepdoc/parser/markdown_parser.py::get_delimiters` honors bare chars
(fixes [1]).
- **tests:** `test/unit_test/rag/test_delim.py` (85 tests) — helper,
acceptance table, frontend parity, static guard against re-inlining.
- **tests:** `test/unit_test/rag/test_delimiter_case_sensitive.py` (from
#17386) updated to retarget the static check at the new helper +
AST-based broader scan.
## Acceptance criteria
- All six sites produce the same regex pattern for the same input.
- Shipped default keeps working for `.txt` / `.pdf` / `.docx`.
- Shipped default for `.md` now splits (was a silent no-op).
- Tooltip example `` `\n##;` `` produces three effective delimiters
regardless of file type.
- Bare whitespace inputs split on every occurrence.
- Backtick-wrapped whitespace splits only on the exact N-char sequence.
- CRLF-line-ending documents split identically to LF-line-ending
documents.
- 123 tests pass (85 new + 38 existing).
## Rebase protocol
As #17385 and #17386 evolve, this branch will be rebased on top. The
only overlap between this PR's diff and the other two is
`test_delimiter_case_sensitive.py`, where #17383 modifies the static
check to point at the new helper location.
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-08-02 14:37:14 +05:30
|
|
|
// HasCustomDelimiterList reports whether any entry in a TokenChunker-style
|
|
|
|
|
// delimiter list uses backtick syntax.
|
|
|
|
|
func HasCustomDelimiterList(delims []string) bool {
|
|
|
|
|
for _, d := range delims {
|
|
|
|
|
if strings.HasPrefix(d, "`") && strings.HasSuffix(d, "`") && len(d) >= 2 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// runesOf splits s into individual Unicode characters (as strings).
|
|
|
|
|
func runesOf(s string) []string {
|
|
|
|
|
if s == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
out := make([]string, 0, utf8.RuneCountInString(s))
|
|
|
|
|
for _, r := range s {
|
|
|
|
|
out = append(out, string(r))
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|