# textkit：字面文本手术刀

`textkit` 按**字面标记**和**行**处理文本：没有正则、不需要转义、没有位置参数，区分大小写。共 8 个函数，返回普通字符串。

[AI 提示词](agent.html) 是一段可直接放入 agent 系统提示的独立章节。

## 统一约定

- 缺少标记返回 `null`；找到但内容为空返回 `""`。aardio 中 `""` 是真值，可直接写 `textkit.after(s,mark) : 默认值`。
- 输入文本为 `null` 时返回 `null`，链式调用只需在最后判断一次。
- 标记与分隔符必须是非空字符串，否则抛出错误。都只取第一处，不解析嵌套、引号或转义。
- 不规范化原文：不改换行、不修剪空白，只改变调用所针对的部分。

## 快速上手

```aardio
import textkit;

var title = textkit.between("<title>首页</title>","<title>","</title>"); // "首页"
var token = textkit.after("Bearer abc.def","Bearer ");                   // "abc.def"
var key,value = textkit.splitOnce("name=a=b","=");                       // "name","a=b"
var page = textkit.replaceBetween("<!--A-->旧<!--B-->","<!--A-->","<!--B-->","新");
// page == "<!--A-->新<!--B-->"

var html = "<h1>x</h1><table><tr><td>1</td></tr></table>";
var cell = textkit.between(textkit.after(html,"<table"),"<td>","</td>") : "无"; // 任一步缺失都得到 "无"
```

## 函数一览

| 函数 | 结果 |
|---|---|
| `between(s,beginMark,endMark)` | 第一对标记之间的文本，不含标记；结束标记从开始标记之后查找 |
| `before(s,mark)` / `after(s,mark)` | 第一个标记之前 / 之后的文本 |
| `splitOnce(s,sep)` | 第一个分隔符两侧，**两个返回值** `left,right`，不是数组；未找到都为 `null` |
| `replaceBetween(s,beginMark,endMark,replacement)` | 替换第一对标记之间的内容并保留标记；`replacement` 必须指定，`""` 表示清空 |
| `mapLines(s,fn)` | 逐行替换、删除或保留 |
| `indent(s,prefix)` | 给每个非空白行加字面前缀 |
| `dedent(s)` | 去除非空白行共同的 SPACE/TAB 缩进 |

## 逐行处理

`fn(line,lineNo)` 的返回值决定该行的结果：

- 字符串：替换该行正文；
- `false`：删除该行（连同行尾）；
- `null`（没有返回值）或 `true`：保留。

每行原有的 CRLF/LF/CR 原样保留，`fn` 只返回正文，不要附加行尾。末尾换行结束最后一行，不会多出一个空行；空文本没有行。删除最后一行时，文本末尾原来有无换行保持不变。

```aardio
import textkit;

var src = 'keep();\r\nDEBUG print(1);\r\n  done();  \n';
var cleaned = textkit.mapLines(src,function(line,lineNo){
	if(string.indexOf(line,"DEBUG")) return false; // 删除整行
	return string.trimRight(line,' \t');          // 去掉行尾空白
});
// cleaned == 'keep();\r\n  done();\n'
```

注意 aardio 的 `cond ? false : null` 总是得到 `null`。删除行请写 `if(cond) return false;`，或返回布尔谓词，例如 `lambda(line) !string.indexOf(line,"DEBUG")`。

## 缩进

- `indent(s,prefix)`：给每个非空白行加 prefix。可以是 `'\t'`、`"    "`，也可以是 `"> "`、`"// "` 这类引用或注释前缀。TAB 必须写单引号 `'\t'`。
- `dedent(s)`：去除所有非空白行共同的 SPACE/TAB 前导缩进，保留相对缩进。TAB 与空格不互换，不猜测列宽；缩进混用而没有共同前缀时原样返回。
- 两者都不改变空白行（空行或只含 SPACE/TAB 的行），也不改变行尾。
- 搬移代码块到目标缩进：`textkit.indent(textkit.dedent(block),prefix)`。

```aardio
import textkit;

var block = '        if(ok){\r\n            work();\n        }';
var moved = textkit.indent(textkit.dedent(block),"    ");
// moved == '    if(ok){\r\n        work();\n    }'
```

这是语言无关的字面布局操作，不是代码格式化器，不能保证多行字符串内部的缩进语义。

## 什么时候直接用 string

| 任务 | 用法 |
|---|---|
| 按完整字面分隔串拆分全部 | `string.splitLiteral(s,sep)`；`string.split` 的分隔参数是字符集合 |
| 全局字面替换 | `string.replaceLiteral(s,old,new)` |
| 统一换行 | `string.crlf(s,'\n')` |
| 模式匹配、位置、长度 | `string.match`、`string.find`、`string.slice`、`#s` |

JSON、HTML 树、源码语法等嵌套或带转义的格式，请用专用解析器。

## 编辑引擎：textkit.patch / textkit.editor

这两个子库是文件编辑工具使用的底层引擎：唯一匹配、整单原子、保留未改动的字节。它们只处理字符串，不读写文件。日常处理文本一般用不到；通过工具编辑文件时也不需要直接调用。

[精确补丁](patch/index.html) · [结构化编辑](editor/index.html)
