# "Text Processing" _Path: en/lua/text/text_ > "Regular expressions, text diffing, and semantic text splitting." ## Table of Contents - Text Processing ## Content # Text Processing The `text` module provides regular expressions, text comparison and patching, and document splitting. This page is an API reference. Its short blocks are isolated calls; longer splitting blocks are partial recipes whose documents, configured filesystem resources, and downstream processing belong to the surrounding application. ## Loading ```lua local text = require("text") ``` ### `text.regexp.compile` Compile an RE2-compatible regular expression. ```lua local re, err = text.regexp.compile("[0-9]+") if err then return nil, err end ``` | Parameter | Type | Description | |-----------|------|-------------| | `pattern` | string | RE2 compatible regex pattern | **Returns:** `Regexp, error` ### `re:match_string` Match a string against the compiled expression. ```lua local ok = re:match_string("abc123") ``` | Parameter | Type | Description | |-----------|------|-------------| | `s` | string | String to match | **Returns:** `boolean` ### `re:find_string` Find the first matching substring. ```lua local match = re:find_string("abc123def") ``` | Parameter | Type | Description | |-----------|------|-------------| | `s` | string | String to search | **Returns:** `string | nil` At this runtime pin, an empty-string match is also represented as `nil`; use a pattern that consumes at least one character when an empty match must be distinguished from no match. ### `re:find_all_string` Find all matching substrings. ```lua local matches = re:find_all_string("a1b2c3") ``` | Parameter | Type | Description | |-----------|------|-------------| | `s` | string | String to search | **Returns:** `string[]` ### `re:find_string_submatch` Find the first match and its capture groups. ```lua local match = re:find_string_submatch("user@example.com") ``` | Parameter | Type | Description | |-----------|------|-------------| | `s` | string | String to search | **Returns:** `string[] | nil` (full match + capture groups) ### `re:find_all_string_submatch` Find all matches and their capture groups. ```lua local matches = re:find_all_string_submatch("a=1 b=2") ``` | Parameter | Type | Description | |-----------|------|-------------| | `s` | string | String to search | **Returns:** `string[][]` ### `re:find_string_index` Find the 1-based bounds of the first match. ```lua local pos = re:find_string_index("abc123") ``` | Parameter | Type | Description | |-----------|------|-------------| | `s` | string | String to search | **Returns:** `table | nil` ({start, end}, 1-based) ### `re:find_all_string_index` Find the bounds of all matches. ```lua local positions = re:find_all_string_index("a1b2c3") ``` | Parameter | Type | Description | |-----------|------|-------------| | `s` | string | String to search | **Returns:** `table[] | nil` (nil when there are no matches) ### `re:replace_all_string` Replace every matching substring. ```lua local result = re:replace_all_string("a1b2", "X") ``` | Parameter | Type | Description | |-----------|------|-------------| | `s` | string | Input string | | `repl` | string | Replacement string | **Returns:** `string` ### `re:split` Split a string at matches of the compiled expression. ```lua local parts = re:split("a,b,c", -1) ``` | Parameter | Type | Description | |-----------|------|-------------| | `s` | string | String to split | | `n` | integer | Max parts, -1 for all | **Returns:** `string[]` ### `re:num_subexp` Return the number of capturing subexpressions. ```lua local count = re:num_subexp() ``` **Returns:** `number` ### `re:subexp_names` Return the names of capturing subexpressions. ```lua local names = re:subexp_names() ``` **Returns:** `string[]` ### `re:string` Return the compiled pattern string. ```lua local pattern = re:string() ``` **Returns:** `string` ## Text Diffing Compare text versions and generate patches with [go-diff](https://github.com/sergi/go-diff), an implementation of Google's diff-match-patch algorithm. ### `text.diff.new` Create a text differ with default or custom options. ```lua local diff, err = text.diff.new() local diff, err = text.diff.new(options) ``` **Returns:** `Differ, error` #### Options {id="diff-options"} | Field | Type | Default | Description | |-------|------|---------|-------------| | `diff_timeout` | number | 1.0 | Timeout in seconds | | `diff_edit_cost` | integer | 4 | Cost of an empty edit | | `match_threshold` | number | 0.5 | Match tolerance 0-1 | | `match_distance` | integer | 1000 | Distance to search for match | | `patch_delete_threshold` | number | 0.5 | Delete threshold | | `patch_margin` | integer | 4 | Context margin | ### `diff:compare` Compare two strings and return operations that transform `text1` into `text2`. ```lua local diff, diff_err = text.diff.new() if diff_err then return nil, diff_err end local diffs, err = diff:compare("hello world", "hello there") if err then return nil, err end -- diffs contains: -- {operation = "equal", text = "hello "} -- {operation = "delete", text = "world"} -- {operation = "insert", text = "there"} ``` | Parameter | Type | Description | |-----------|------|-------------| | `text1` | string | Original text | | `text2` | string | Modified text | **Returns:** `table, error` (array of {operation, text}) Operations: `"equal"`, `"delete"`, `"insert"` ### `diff:summarize` Count the unchanged, inserted, and deleted UTF-8 bytes. For non-ASCII text, these totals are not Unicode code-point or grapheme counts. ```lua -- `diffs` is the checked result from diff:compare. local summary = diff:summarize(diffs) -- summary.equals = 6 (bytes unchanged) -- summary.deletions = 5 (bytes removed) -- summary.insertions = 5 (bytes added) ``` | Parameter | Type | Description | |-----------|------|-------------| | `diffs` | table | Diff array from compare | **Returns:** `table` ({insertions, deletions, equals}) ### `diff:pretty_text` Format a diff with ANSI colors for terminal output. ```lua local formatted, err = diff:pretty_text(diffs) if err then return nil, err end print(formatted) ``` | Parameter | Type | Description | |-----------|------|-------------| | `diffs` | table | Diff array from compare | **Returns:** `string, error` ### `diff:pretty_html` Format a diff as HTML with `` and `` elements. ```lua local html, err = diff:pretty_html(diffs) if err then return nil, err end -- `html` is an HTML fragment with equal, deleted, and inserted spans. ``` | Parameter | Type | Description | |-----------|------|-------------| | `diffs` | table | Diff array from compare | **Returns:** `string, error` ### `diff:patch_make` Create patches that transform one string into another. The patches can be serialized and applied later. ```lua local text1 = "The quick brown fox jumps over the lazy dog" local text2 = "The quick red fox jumps over the lazy cat" local patches, err = diff:patch_make(text1, text2) if err then return nil, err end ``` | Parameter | Type | Description | |-----------|------|-------------| | `text1` | string | Original text | | `text2` | string | Modified text | **Returns:** `table, error` ### `diff:patch_apply` Apply patches to a string and return the result and whether every patch succeeded. ```lua local result, success = diff:patch_apply(patches, text1) -- result = "The quick red fox jumps over the lazy cat" -- success = true ``` Check `success` before treating `result` as the requested transformation. Pass patch tables produced by `patch_make`; at this runtime pin, malformed serialized patch text inside a hand-built table can be skipped rather than reported separately. | Parameter | Type | Description | |-----------|------|-------------| | `patches` | table | Patches from patch_make | | `text` | string | Text to apply patches to | **Returns:** `string, boolean` ## Text Splitting Split documents into chunks while preserving semantic boundaries. The splitters are based on the [langchaingo](https://github.com/tmc/langchaingo) implementation. ### `text.splitter.recursive` The recursive splitter tries double newlines, single newlines, spaces, and then individual characters. It moves to the next separator when a chunk exceeds the size limit. ```lua local splitter, err = text.splitter.recursive({ chunk_size = 1000, chunk_overlap = 100 }) if err then return nil, err end local long_text = "This is a long text that needs splitting..." local chunks, split_err = splitter:split_text(long_text) if split_err then return nil, split_err end ``` **Returns:** `Splitter, error` #### Options {id="recursive-splitter-options"} | Field | Type | Default | Description | |-------|------|---------|-------------| | `chunk_size` | integer | 4000 | Max characters per chunk | | `chunk_overlap` | integer | 200 | Characters repeated between adjacent chunks | | `keep_separator` | boolean | false | Keep separators in output | | `separators` | string[] | nil | Custom separator list | ### `text.splitter.markdown` The Markdown splitter can keep headings with their content, preserve code blocks, and group table rows. ```lua local splitter, err = text.splitter.markdown({ chunk_size = 2000, code_blocks = true, heading_hierarchy = true }) if err then return nil, err end local readme = fs.get("app:docs"):readfile("README.md") local chunks, err = splitter:split_text(readme) ``` This partial recipe requires the entry to enable both `text` and `fs`, a configured `app:docs` filesystem resource, and a readable `README.md` within that resource. **Returns:** `Splitter, error` #### Options {id="markdown-splitter-options"} | Field | Type | Default | Description | |-------|------|---------|-------------| | `chunk_size` | integer | 4000 | Max characters per chunk | | `chunk_overlap` | integer | 200 | Characters repeated between adjacent chunks | | `code_blocks` | boolean | false | Keep code blocks together | | `reference_links` | boolean | false | Preserve reference links | | `heading_hierarchy` | boolean | false | Respect heading levels | | `join_table_rows` | boolean | false | Keep table rows together | | `separators` | string[] | nil | Custom separator list | ### `splitter:split_text` Split a single document into an array of chunks. ```lua local chunks, err = splitter:split_text(document) if err then return nil, err end for i, chunk in ipairs(chunks) do -- Process each chunk (e.g., create embedding, send to LLM) process(chunk) end ``` Here, `splitter` is a successfully created splitter, while `document` and `process` are supplied by the application. | Parameter | Type | Description | |-----------|------|-------------| | `text` | string | Text to split | **Returns:** `string[], error` ### `splitter:split_batch` Split multiple documents while preserving their metadata. One input document can produce several chunks, each with the source document's metadata. ```lua -- Input: pages from a PDF with page numbers local pages = { {content = "First page content...", metadata = {page = 1}}, {content = "Second page content...", metadata = {page = 2}} } local chunks, err = splitter:split_batch(pages) if err then return nil, err end -- Output: each chunk knows which page it came from for _, chunk in ipairs(chunks) do print("Page " .. chunk.metadata.page .. ": " .. chunk.content:sub(1, 50)) end ``` | Parameter | Type | Description | |-----------|------|-------------| | `pages` | table | Array of {content, metadata} | **Returns:** `table, error` (array of {content, metadata}) `split_batch` silently skips an item when the item is not a table, its `content` field is missing, empty, or not a string, or splitting that item fails. It still returns the remaining chunks with a `nil` error. Validate every input item before the call and verify any cardinality requirements in application code; do not treat a successful call as proof that every input was represented. ## Errors | Condition | Kind | Retryable | |-----------|------|-----------| | Invalid pattern syntax | `errors.INVALID` | no | | Internal error | `errors.INTERNAL` | no | See [Error Handling](lua/core/errors.md) for working with errors. ## Navigation Previous: "Hub" (lua/system/hub) Next: "Template Engine" (lua/text/template)