# muggle-string 中文文档

规范地址：https://npmcn.com/package/muggle-string/

版本：1\.0\.2
npm 元数据更新时间：2026-11-04T04:01:57.141Z
中文译文更新时间：2026-11-04T05:34:02.292Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/muggle-string
原文对照：https://npmcn.com/package/muggle-string/?lang=original
项目主页：https://github.com/johnsoncodehk/muggle-string#readme
源码仓库：https://github.com/johnsoncodehk/muggle-string

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：131 kB，20 个文件
- 模块格式：ESM + CJS
- TypeScript 类型：包内自带
- Node.js 要求：\>=12
- 运行依赖：0 个
- 首次发布：2022-09-29T13:26:11.465Z
- 最新版本发布：2026-11-04T04:01:56.811Z
- 已发布版本数：20（近 12 个月 12 个）

## 中文简介

对源文本进行排序的、无冲突的重写，呈现为格式无关的片段

## 安装

```sh
npm install muggle-string
```

也可以使用：`pnpm add muggle-string`、`yarn add muggle-string`、`bun add muggle-string`

## README

# muggle-string

我写麻瓜串的 [vuejs /语言工具](https://github.com/vuejs/language-tools)，将Vue的单文件组件转换为语言服务器的TypeScript代码。它一次从几个地方重写源文本，并将结果映射回源，既作为语言服务器的跨度，也作为v3源映射。它没有依赖关系。

> 1.0取代了0.4的段数组帮助器(`replace`， `replaceSourceRange`， `toString`,……)。它们已经消失了，没有被弃用。

## 背景

在vuejs/language-tools中，props、emit、models、slots和refs的插件都是一样的 `<script setup>` 块。生成器移动其中的部分，比如默认导出的表达式，并在模板代码周围将块分成几部分呈现。输出的每个部分都保留了它所来自的代码的编辑器特性，比如悬停、完成和重命名。麻瓜串是它背后的模型，没有Vue在里面。我想从中得到三个东西：

-   **编辑保持正确。** 插件不知道彼此的存在。删除加起来，所以没有编辑撤消另一个。在一个位置的代码是按其作者的等级排序的，所以插件的顺序决定它，而不是调用的顺序。这些测试 `tests/` 用随机编辑检查两者。
-   **地图在工具读取的地方是准确的。** 相同的部分给出了语言服务器的范围和用于调试器的v3源代码映射。在源映射中，生成的代码映射为空，因此调试器永远不会将其显示为您的代码，即使当它像V8那样跨行查看时也是如此。在JavaScript、TypeScript、HTML和CSS中，保持代码映射恰好在令牌开始处，这是断点、堆栈跟踪和覆盖读取的地方。测试检查映射的每个生成的字符。
-   **性能。** 语言服务器在每次更改文件后都会重新生成代码，因此模型会跳过它不需要的工作。插入或替换操作只被追加到其编写器的列表中。没有文本搜索，分割或复制时，您编辑。渲染将编辑排序一次，或者当它们按顺序到达时根本不排序，之后的渲染将重用它，直到下一次编辑。渲染传递保留的文本作为源的范围，从不复制它。是否要把这些碎片拼凑成一根弦取决于你。 在Apple M2上使用Node 24，对于1 MB的文本，从4个写入器随机插入和替换100,000个，每次编辑在新过程中花费约115 ns，在加热过程中花费约37 ns，无论之前有多少，将文本渲染为字符串大约需要40 ms冷和29 ms热。

## 采用

-   [vuejs /语言工具](https://github.com/vuejs/language-tools)：生成Vue单文件组件的TypeScript代码。它的版本使用0.4；向1.0的迁移正在进行中，尚未发布。
-   [muggle-pug](https://github.com/johnsoncodehk/muggle-pug)：将Pug编译为HTML格式 `Piece` 范围。

## 安装

`npm install muggle-string`． 它只作为ES模块发布。CommonJS代码可以 `require` 在Node 20.19及更高版本，或22.12及更高版本。 `renderWithSourceMap` 还需要全球 `TextDecoder` WHATWG编码标准，如Node和Bun所定义的； `renderWithDecodedSourceMap` 没有。

## 用法

#### 编辑和渲染

```ts
import { createSourceRewrite, rewritePieceText } from 'muggle-string';

const source = { content: 'log(name);\nconst name = "Ada";\n' };
const rewrite = createSourceRewrite(source);

for (const { index: start } of source.content.matchAll(/\bname\b/g)) {
    rewrite.replace(start, start + 'name'.length, 'user');
}
rewrite.insertBefore(0, rewrite.cut(source.content.indexOf('const'), source.content.length));

let text = '';
rewrite.render(0, source.content.length, piece => text += rewritePieceText(piece));
console.log(text);
// const user = "Ada";
// log(user);
```

源是具有的任何对象 `content`． 内容更改后的渲染会抛出。

#### 作家和职级

```ts
import { createSourceRewrite, rewritePieceText } from 'muggle-string';

const source = { content: 'f(x)' };
const rewrite = createSourceRewrite(source);
const inner = rewrite.writer(0);
const outer = rewrite.writer(1);

inner.insertBefore(2, '(');
outer.insertBefore(2, '[');
rewrite.insertBefore(2, '{');
inner.insertAfter(3, ')');
outer.insertAfter(3, ']');
rewrite.insertAfter(3, '}');
inner.insertAfter(3, '!');

let text = '';
rewrite.render(0, source.content.length, piece => text += rewritePieceText(piece));
console.log(text);
// f({[(x)!]})
```

在一个位置，无论调用顺序如何，高阶代码都排在低阶代码之外。重写本身是最外层的写入器。一个人在一个位置编写代码，保持呼叫秩序。

#### 带着伤口移动

```ts
import { createSourceRewrite, rewritePieceText } from 'muggle-string';

const source = { content: 'export default { a: 1 };\n' };
const rewrite = createSourceRewrite(source);

const object = rewrite.cut(15, 23, { placeholder: '__options' });
rewrite.replace(20, 21, '2');
console.log(rewrite.unrendered().length);
// 1

rewrite.insertBefore(0, ['const __options = ', object, ';\n']);
let text = '';
rewrite.render(0, source.content.length, piece => text += rewritePieceText(piece));
console.log(text);
// const __options = { a: 2 };
// export default __options;
console.log(rewrite.unrendered().length);
// 0
```

切片渲染范围，其中包含您放置它的每个编辑。占位符呈现范围所在的位置。

切片的保存文本携带其切片的数据，也与其他数据一起在视图中：

```ts
import { createSourceRewrite, rewritePieceText } from 'muggle-string';

const source = { content: 'f(a, b)' };
const rewrite = createSourceRewrite<typeof source, { from: string }>(source);
rewrite.insertBefore(2, rewrite.cut(5, 6, { from: 'cut' }));

const parts: string[] = [];
rewrite.render(0, 7, piece => {
    parts.push(typeof piece === 'string' ? piece : `${rewritePieceText(piece)}@${piece.from}`);
}, { from: 'view' });
console.log(parts.join(' '));
// f(@view b@cut a, @view )@view
```

若要在其所在位置包装范围，请在其周围插入而不是切割它。

#### 的观点

```ts
import { createSourceRewrite, rewritePieceText } from 'muggle-string';

const source = { content: 'let a = 1;\nlet b = 2;\n' };
const rewrite = createSourceRewrite<typeof source, { view: string }>(source);
rewrite.insertAfter(0, '// start\n');
rewrite.replace(4, 5, 'x');

function view(start: number, end: number, name: string) {
    const kept: string[] = [];
    let text = '';
    rewrite.render(start, end, piece => {
        text += rewritePieceText(piece);
        if (typeof piece !== 'string' && piece.text === undefined) {
            kept.push(piece.view);
        }
    }, { view: name });
    console.log(JSON.stringify(text), kept.join());
}

view(0, 10, 'first');
// "// start\nlet x = 1;" first,first
view(4, 22, 'second');
// "x = 1;\nlet b = 2;\n" second
```

视图没有文本，因此视图可以重叠并再次呈现。保存的文本将数据传递给 `render`． 在所有文本之前插入的代码(`insertAfter(0, …)`)以0开始的视图呈现，并在所有文本之后使用以末尾结束的视图进行编码。

#### 在视图的边缘插入

```ts
import { createSourceRewrite, rewritePieceText } from 'muggle-string';

const source = { content: 'a+b' };
const rewrite = createSourceRewrite(source);
rewrite.insertAfter(1, '>');
rewrite.insertBefore(1, '<');

for (const [start, end] of [[0, 1], [1, 3], [1, 1]] as const) {
    let text = '';
    rewrite.render(start, end, piece => text += rewritePieceText(piece));
    console.log(start, end, JSON.stringify(text));
}
// 0 1 "a>"
// 1 3 "<+b"
// 1 1 ""

const empty = { content: '' };
const emptyRewrite = createSourceRewrite(empty);
emptyRewrite.insertBefore(0, 'B');
emptyRewrite.insertAfter(0, 'A');
let text = '';
emptyRewrite.render(0, 0, piece => text += rewritePieceText(piece));
console.log(JSON.stringify(text));
// "AB"
```

插入语句附在char的一侧： `insertBefore(offset, …)` 对炭 `offset`， `insertAfter(offset, …)` 到它前面的字符。视图呈现它所呈现的字符的插入，因此视图 `a`．.`b` 呈现 `insertBefore(a, …)` 和 `insertAfter(b, …)`，但不是 `insertAfter(a, …)` 或 `insertBefore(b, …)`． 文本内的空视图，比如 `1`．.`1`，不呈现插入。位于0或末尾的空视图在所有文本之前或之后呈现代码，因此在空源代码中 `render(0, 0)` 呈现。按文本选择代码所属的那一边：表达式末尾的代码是 `insertAfter(end, …)`，所以当表达式变成视图时，它仍然会呈现。

#### 部分保存文本的数据

```ts
import { createSourceRewrite, rewritePieceText, type Piece, type RewriteOrigin } from 'muggle-string';

const source = { content: 'a + b' };
const rewrite = createSourceRewrite<typeof source, { complete: boolean }>(source);
rewrite.replace(2, 3, '&&');

const parts: string[] = [];
const sink = (piece: Piece<RewriteOrigin<typeof source, { complete: boolean }>>) => {
    parts.push(typeof piece === 'string' ? piece : `${rewritePieceText(piece)}:${piece.complete}`);
};
rewrite.render(0, 4, sink, { complete: true });
rewrite.render(4, 5, sink, { complete: false });
console.log(parts.join('|'));
// a :true|&&| :true|b:false
```

保留文本从视图中获取数据，因此呈现连续视图以提供不同范围的部分数据。

#### 嵌套

```ts
import { createSourceRewrite, rewritePieceText } from 'muggle-string';

const source = { content: 'xy' };

function wrap(order: 'pre-order' | 'enter and leave' | 'pre-order with ranks') {
    const rewrite = createSourceRewrite(source);
    const outer = order === 'pre-order with ranks' ? rewrite.writer(1) : rewrite;
    const inner = order === 'pre-order with ranks' ? rewrite.writer(0) : rewrite;
    if (order === 'enter and leave') {
        rewrite.insertBefore(0, '(');
        rewrite.insertBefore(1, '[');
        rewrite.insertAfter(2, ']');
        rewrite.insertAfter(2, ')');
    }
    else {
        outer.insertBefore(0, '(');
        outer.insertAfter(2, ')');
        inner.insertBefore(1, '[');
        inner.insertAfter(2, ']');
    }
    let text = '';
    rewrite.render(0, 2, piece => text += rewritePieceText(piece));
    console.log(text, order);
}
wrap('pre-order');
// (x[y)] pre-order
wrap('enter and leave');
// (x[y]) enter and leave
wrap('pre-order with ranks');
// (x[y]) pre-order with ranks
```

要包装嵌套节点，请在输入节点时编写打开代码，在离开节点时编写关闭代码：节点的关闭代码在一个位置结束，然后按调用顺序首先进入内部。如果你把“open”和“close”写在一起，在预定顺序中，“closed”会先写在外面；每个深度都有自己的作者，等级越高越好。

#### 任意位置

```ts
import { createSourceRewrite, rewritePieceText } from 'muggle-string';

const source = { content: 'a +' };
const rewrite = createSourceRewrite(source);
const at = source.content.length;
rewrite.insertBefore(at, { source, start: at, end: at, text: ' missing' });

rewrite.render(0, at, piece => {
    if (typeof piece !== 'string') {
        console.log(piece.start, piece.end, JSON.stringify(rewritePieceText(piece)));
    }
});
// 0 3 "a +"
// 3 3 " missing"
```

要将代码映射到一个位置而不是一个范围，例如解析错误，请插入一个没有宽度的范围： `insertBefore(at, { source, start: at, end: at, text })`。一个 `replace` 的空范围，例如没有char的引用，不删除任何内容，并将其代码放在char的位置 `at`，因此替换范围的代码不需要检查是否为空。

#### 与源代码不同的代码

```ts
import { createSourceRewrite, rewritePieceText, type Piece } from 'muggle-string';

const block = { content: '{{ a &amp;&amp; b }}' };
type BlockPiece = Exclude<Piece<{ source: typeof block }>, string>;

const decoded: BlockPiece[] = [
    { source: block, start: 3, end: 5 },
    { source: block, start: 5, end: 15, text: '&&' },
    { source: block, start: 15, end: 17 },
];
function toBlock(start: number, end: number, sink: (piece: BlockPiece) => void) {
    let at = 0;
    for (const piece of decoded) {
        const length = rewritePieceText(piece).length;
        const from = Math.max(start, at);
        const to = Math.min(end, at + length);
        if (from < to) {
            sink(piece.text === undefined
                ? { source: block, start: piece.start + from - at, end: piece.start + to - at }
                : piece);
        }
        at += length;
    }
}

const code = { content: decoded.map(rewritePieceText).join('') };
const rewrite = createSourceRewrite(code);
rewrite.insertBefore(0, '__ctx.');
rewrite.insertBefore(5, '__ctx.');

let text = '';
const ranges: string[] = [];
rewrite.render(0, code.content.length, piece => {
    if (typeof piece === 'string') {
        text += piece;
        return;
    }
    toBlock(piece.start, piece.end, blockPiece => {
        text += rewritePieceText(blockPiece);
        ranges.push(`${blockPiece.start}..${blockPiece.end}`);
    });
});
console.log(text);
// __ctx.a && __ctx.b
console.log(ranges.join(' '));
// 3..5 5..15 15..16 16..17
```

如果代码是块的文本，则重写块并将代码的范围呈现为视图。在不同的地方，例如在解码实体之后，将代码重写为自己的源代码，并让生成代码的层将其作为块偏移量的片段提供，通过块偏移量保留范围映射回来。

#### 几个来源

```ts
import { createSourceRewrite, rewritePieceText } from 'muggle-string';

const script = { lang: 'ts', content: 'f();\n' };
const template = { tag: 'template', content: '{{ a }}' };
const rewrite = createSourceRewrite<typeof script | typeof template>(script);
rewrite.insertBefore(2, { source: template, start: 3, end: 4 });

rewrite.render(0, script.content.length, piece => {
    if (typeof piece !== 'string') {
        console.log(JSON.stringify(rewritePieceText(piece)), 'lang' in piece.source ? 'script' : 'template');
    }
});
// "f(" script
// "a" template
// ");\n" script
```

`S` 的 `createSourceRewrite<S, Data>` 是代码中出现的每个源，而不仅仅是它所编辑的源。传递它们的联合或公共类型。

#### 错误

```ts
import { createSourceRewrite } from 'muggle-string';

function attempt(edit: () => void) {
    try {
        edit();
    }
    catch (error) {
        console.log((error as Error).message);
    }
}

const source = { content: 'a = 1;\nb = 2;\n' };
const rewrite = createSourceRewrite(source);

attempt(() => rewrite.insertBefore(20, 'x'));
// The offset 20 is out of 0..14.
attempt(() => rewrite.writer(-1));
// The rank -1 is not a non-negative integer below the rank of the rewrite.

const first = rewrite.cut(0, 6);
attempt(() => rewrite.cut(4, 9));
// The cut 4..9 crosses the cut 0..6.
attempt(() => rewrite.render(0, 14, () => rewrite.insertBefore(0, 'x')));
// The rewrite is rendering.
source.content = 'a = 2;\nb = 2;\n';
attempt(() => rewrite.render(0, 14, () => {}));
// The content of the source changed after the rewrite was made.
```

```ts
import { createSourceRewrite } from 'muggle-string';

const source = { content: 'a = 1;\nb = 2;\n' };
const rewrite = createSourceRewrite(source);
const slice = rewrite.cut(2, 6);
rewrite.insertBefore(2, slice);
try {
    rewrite.render(0, 14, () => {});
}
catch (error) {
    console.log((error as Error).message);
}
// The cut 2..6 is placed inside itself, so it never renders.
```

`insertBefore(2, …)` 连接到切片的第一个字符，因此放置在其起始位置的切片在其内部。

抛出的编辑不会改变任何东西。渲染抛出的错误来自于停留的编辑，所以以后的渲染也会抛出错误。

#### 源地图

```ts
import { createSourceRewrite, renderWithSourceMap } from 'muggle-string';

const source = { name: 'main.js', content: 'log(name);\nconst name = "Ada";\n' };
const rewrite = createSourceRewrite<typeof source, { name?: string }>(source);

for (const { index: start } of source.content.matchAll(/\bname\b/g)) {
    const end = start + 'name'.length;
    rewrite.replace(start, end, { source, start, end, text: 'user', name: 'name' });
}
rewrite.insertBefore(0, rewrite.cut(source.content.indexOf('const'), source.content.length));

const { text, map } = renderWithSourceMap(
    sink => rewrite.render(0, source.content.length, sink),
    (from: typeof source) => from.name,
);
console.log(text);
// const user = "Ada";
// log(user);
console.log(JSON.stringify(map));
// {"version":3,"sources":["main.js"],"sourcesContent":["log(name);\nconst name = \"Ada\";\n"],"names":["name"],"mappings":"AACA,KAAK,CAACA,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AADlB,GAAG,CAACA,IAAI,CAAC;"}
```

`renderWithSourceMap(render, sourceName)` 将接收器传递给 `render`, `sourceName` 中每个源的条目 `sources`． 全球在哪里 `TextDecoder` 则第一个调用在呈现之前抛出。

`renderWithDecodedSourceMap(render, sourceName)` 给出相同的文本和地图 `mappings` 解码，用于在片段上工作的代码，而不是写出映射。生成文本的每一行都有一个片段数组， `[column, source, line, column, name]` 在以零为基础的数字中，没有源字段，其中段没有源并且没有 `name` 那里没有。它不需要 `TextDecoder`。

```ts
import { createSourceRewrite, renderWithDecodedSourceMap } from 'muggle-string';

const source = { name: 'main.js', content: 'let a = 1;\n' };
const rewrite = createSourceRewrite<typeof source, { name?: string }>(source);
rewrite.replace(4, 5, { source, start: 4, end: 5, text: 'b', name: 'a' });

const { map } = renderWithDecodedSourceMap(
    sink => rewrite.render(0, source.content.length, sink),
    (from: typeof source) => from.name,
);
console.log(JSON.stringify(map.mappings));
// [[[0,0,0,0],[3,0,0,3],[4,0,0,4,0],[5,0,0,5],[6,0,0,6],[7,0,0,7],[8,0,0,8],[9,0,0,9]],[]]
```

`encodeMappings(mappings)` 将解码后的映射写入v3 `mappings` 字符串, `renderWithSourceMap` 写入它们，用于在写入映射之前更改段的代码。如果字段不是0到2 \*\* 30 - 1之间的整数，则会抛出抛出 `TextDecoder` 没有定义。

```ts
import { encodeMappings } from 'muggle-string';

console.log(encodeMappings([[[0, 0, 0, 0], [4]], [], [[2, 0, 1, 3, 0]]]));
// AAAA,I;;EACGA
```

如果范围没有源，或者其源内容中的偏移量不是整数，则在到达接收器时抛出：

```ts
import { renderWithSourceMap } from 'muggle-string';

const source = { name: 'main.js', content: 'a = 1;\n' };
try {
    renderWithSourceMap(sink => sink({ source, start: 0, end: 9, text: 'b' }), (from: typeof source) => from.name);
}
catch (error) {
    console.log((error as Error).message);
}
// The range 0..9 of main.js is out of 0..7.
```

## 来自可变字符串编辑器

偏移总是指向原始文本，在其他编辑之后也是如此。

| 来 | 写 |
| --- | --- |
| 覆盖范围 | `replace(start, end, text)`,或 `replace(start, end, { source, start, end, text })` 把它映射到值域 |
| 移除范围 | `replace(start, end, '')` |
| 在文本前加或追加 | `insertAfter(0, code)` 或 `insertBefore(source.content.length, code)` |
| 在偏移位置插入 | `insertBefore(offset, code)` 或 `insertAfter(offset, code)` |
| 移动范围 | `insertBefore(to, rewrite.cut(start, end))` |
| 读取结果 | `rewrite.render(0, source.content.length, piece => text += rewritePieceText(piece))` |
| 获取源映射 | `renderWithSourceMap`上面, |
| 获取源地图的片段 | `renderWithDecodedSourceMap`上面, |

## 这些技术节省了什么？

表中的每种技术都添加了代码。 `npm run bench` 构建源代码，并为每种技术构建仅取出该技术的变体(`bench/variants.mjs`)，检查它们是否都通过散列给出相同的输出，并在固定输入的新进程中计算每个输入的时间：大约1 MB的代码样文本，并以4个编写者的不同偏移量进行编辑。 `bench/table.mjs` 制作下面的表格。每个数字都是一个回合，所以几个百分点的差异接近噪音。节点24.15,Apple M2，单位ms；冷是第一次运行一个过程，暖中值为20次运行后20次以上。

| 技术 | 工作负载 | 温暖，有→没有 | 冷，有→没有 |
| --- | --- | --- | --- |
| 编辑和删除添加，排序一次渲染 | 10万次随机编辑 | 36.6→183 | 54.1→193 |
| 当编辑按顺序到达时，没有排序 | 按源顺序进行100,000次编辑 | 14.6→16.9 | 32.7→37.2 |
| 作家们一次合并 | 按源顺序进行100,000次编辑 | 14.6→17.6 | 32.7→43.1 |
| 视图通过二分搜索找到它开始的位置 | 10万次编辑后，点击量达到5000次 | 15.0→318 | 33.2→262 |
| 映射写成字节，由块解码 | 按源顺序编辑10万次后的源地图 | 34.7→97.5 | 55.3→127 |
| 不带正则表达式的ASCII字符 | 相同的源图 | 34.7→51.4 | 55.3→72.8 |
| 在所有其他同等水平的切割之后，又进行了切割 | 货源订单10000次削减 | 1.45→1.58 | 15.2→15.6 |
| 保留文本作为范围传递，不复制 | 按长度计算，有10万次编辑 | 21.0→22.1 | 38.9→42.3 |
| 键和移除 `Int32Array` | 10万次随机编辑 | 36.6→39.8 | 54.1→57.9 |

在不使用技术的地方，它没有什么区别：随机编辑的顺序快捷键，文本渲染的地图技术，整体渲染的视图搜索，没有剪切的剪切快捷键。最后三行节省9%或更少。

基线是0.4段数组，它的每次编辑扫描和分割段，以及一个排序数组，将每个编辑拼接到适当的位置：

| 文本渲染 | muggle-string | 排序的数组，拼接 | 分段数组 |
| --- | --- | --- | --- |
| 一万次随机编辑，暖 | 3.11 | 4.90 | 155 |
| 十万次随机编辑，暖 | 36.6 | 408 | 21,739（冷，一次运行） |
| 10万编辑源顺序，温暖 | 14.6 | 18.1 | 35232（冷，一次运行） |

## 参考

编辑

-   `insertBefore(offset, code)` 将代码附加到字符at `offset`, `insertAfter(offset, code)` 到它前面的字符。代码跟着该字符移动。
-   `replace(start, end, code)` 去除 `start`．.`end` 并将 `code` 在它的位置上。空范围不删除任何内容并放置 `code` 在char的位置 `start`，在插页内，或在文本末尾；要将代码映射到某个位置，请写入 `{ source, start: at, end: at, text }`。
-   在0..中不是整数的编辑、剪切和视图的偏移量。内容抛出的长度。
-   `rewrite.writer(rank)` 给了一个作家自己的等级。在一个位置上，高阶代码位于低阶代码之外。重写本身是最外层的写入器。一个人在一个位置编写代码，保持呼叫秩序。
-   `rewrite.cut(start, end, options)` 取出一个范围并返回一个切片。切片渲染范围，其中包含您放置它的每个编辑。 `options.placeholder` 渲染范围所在的位置。
-   切割可以按任何顺序进行。一个人在另一个人的体内移动。一个切口穿过另一个切口，命名两个范围。
-   切片的保留文本携带其切片的数据，无论切片呈现在何处。
-   切片在它被放置和渲染的任何地方和频率呈现，就像视图一样。在自己的范围内放置的切片，也由 `insertBefore` 在开始时，在渲染时抛出。 `rewrite.unrendered()` 列出尚未呈现的切片。
-   渲染渲染之前所做的编辑，所以渲染后的编辑将在下一个渲染中显示。渲染运行时的编辑（例如从其接收器）会引发。源内容更改后的呈现将抛出。

呈现

`rewrite.render(start, end, sink, data)` 传递的碎片 `start`．.`end` 来 `sink`，按顺序。您可以呈现多个范围，它们可能会重叠。一个范围会将代码附加到它的字符上： `insertBefore(start, …)` 和 `insertAfter(end, …)`,而不是 `insertAfter(start, …)` 或 `insertBefore(end, …)`． 文本内的空区域不呈现任何代码。在所有文本之前插入的代码(`insertAfter(0, …)`)呈现的范围从0开始，所有文本后面的代码的范围以末尾结束，所以 `render(0, 0)` 的空源同时呈现。

块

```ts
type Piece<Origin = unknown> = string | (Origin & { start: number; end: number; text?: string });
```

-   字符串是新的文本。
-   没有的范围 `text` 将源文本保存在 `start`．.`end`。
-   的范围 `text` 生成的文本表示 `start`．.`end`。
-   `start` 和 `end` 是整数，0≤ `start` ≤ `end` ≤的长度 `content` 来源的。 `renderWithSourceMap` 和 `renderWithDecodedSourceMap` 当范围达到它们的sink时，抛出一个打破这个或没有源的范围。 `rewritePieceText` 扔在这样的范围没有 `text`，因为它只读取保存的文本的源。当你调用它们时，重写检查它自己的编辑、剪切和视图的偏移量，并将你写的范围作为代码传递给它们。

重写的范围也携带它们 `source` 和田野 `Data`． 保留的文本得到 `data` 传递给 `render` 或 `cut`． 你写的范围和你写的一样，比如 `name` 在源映射示例中。 `rewritePieceText(piece)` 给出任意片段的文本。单一来源的生产者，如 [muggle-pug](https://github.com/johnsoncodehk/muggle-pug)，能发出平淡 `Piece` 不带的范围 `source`。

收集碎片：

```ts
import { createSourceRewrite, rewritePieceText, type Piece, type RewriteOrigin } from 'muggle-string';

const source = { content: 'log(name);\n' };
const rewrite = createSourceRewrite<typeof source, { data: string }>(source);
rewrite.replace(4, 8, { source, start: 4, end: 8, text: 'user', data: 'rename' });

const pieces: Piece<RewriteOrigin<typeof source, { data: string }>>[] = [];
rewrite.render(0, source.content.length, piece => pieces.push(piece), { data: 'file' });
for (const piece of pieces) {
    console.log(JSON.stringify(rewritePieceText(piece)), typeof piece === 'string' ? 'new' : piece.data);
}
// "log(" file
// "user" rename
// ");\n" file
```
源地图

-   在每行、单词、空格和其他字符的开头保留文本映射。单词是Unicode字母、标记和数字的集合。识字课等 `_`， `$` 和 `-` 自己绘制地图，因为语言在它们是否属于一个名称上存在分歧。单词内的位置映射到单词的开头。
-   这个 `text` 的值映射到范围的开始。
-   新的文本映射为空。
-   `renderWithSourceMap` 需要全球 `TextDecoder` WHATWG编码标准，如Node和Bun所定义的。如果没有定义，则第一次调用在呈现之前抛出。
-   `renderWithDecodedSourceMap` 给出与其相同的地图 `mappings` 已解码：作为v3映射逐行编码，它们是 `mappings` 的 `renderWithSourceMap`，包括空的最后一行。它不需要 `TextDecoder`。
-   这个 `name` 的范围 `text` 进入 `names`． 仅当 `text` 重命名范围内的标识符。保留文本映射到自己，所以 `name` 没有的范围 `text`，例如 `name` 田野 `data` 一种观点，无处可去。文本上以另一个标识符开头的名称，例如 `__props.x` 为 `x`，显示调试器 `__props` 作为 `x`． 麻瓜麻绳本身没有名字。

跨度的映射

语言工具按范围映射生成的代码。逐字跨距将每个偏移量映射到原始范围中的相同偏移量。Atom span将其末端映射到原始范围的末端，并将其中的每个偏移量映射到原始起点。一个保持的范围是一个逐字跨度，一个与 `text` 是一个Atom跨度：

```ts
import { createSourceRewrite, rewritePieceText } from 'muggle-string';

const source = { content: 'log(name);\nconst name = "Ada";\n' };
const rewrite = createSourceRewrite(source);
for (const { index: start } of source.content.matchAll(/\bname\b/g)) {
    const end = start + 'name'.length;
    rewrite.replace(start, end, { source, start, end, text: 'user' });
}
rewrite.insertBefore(0, rewrite.cut(source.content.indexOf('const'), source.content.length));

type Span = { virtualStart: number; virtualEnd: number; originalStart: number; originalEnd: number; kind: 'Verbatim' | 'Atom' };

const spans: Span[] = [];
let virtualStart = 0;
rewrite.render(0, source.content.length, piece => {
    const { length } = rewritePieceText(piece);
    if (typeof piece !== 'string') {
        const kind = piece.text === undefined ? 'Verbatim' : 'Atom';
        spans.push({ virtualStart, virtualEnd: virtualStart + length, originalStart: piece.start, originalEnd: piece.end, kind });
    }
    virtualStart += length;
});
for (const span of spans) {
    console.log(JSON.stringify(span));
}
// {"virtualStart":0,"virtualEnd":6,"originalStart":11,"originalEnd":17,"kind":"Verbatim"}
// {"virtualStart":6,"virtualEnd":10,"originalStart":17,"originalEnd":21,"kind":"Atom"}
// {"virtualStart":10,"virtualEnd":20,"originalStart":21,"originalEnd":31,"kind":"Verbatim"}
// {"virtualStart":20,"virtualEnd":24,"originalStart":0,"originalEnd":4,"kind":"Verbatim"}
// {"virtualStart":24,"virtualEnd":28,"originalStart":4,"originalEnd":8,"kind":"Atom"}
// {"virtualStart":28,"virtualEnd":31,"originalStart":8,"originalEnd":11,"kind":"Verbatim"}
```

一个 `text` 只要它的范围（其偏移量对齐，例如将其重命名为相同长度的名称）可以是一个veratim span。Vuejs /language-tools做到了这一点，所以名称内的编辑映射回来。

## 常见问题

### muggle-string 是什么？

对源文本进行排序的、无冲突的重写，呈现为格式无关的片段

### 如何安装 muggle-string？

在项目目录运行 npm install muggle-string，或使用 pnpm add / yarn add / bun add。

### muggle-string 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### muggle-string 支持 ESM 还是 CommonJS？

同时提供 ES Module 与 CommonJS 入口，import 和 require\(\) 都可以使用，打包工具会自动选择合适的版本。

### muggle-string 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js \>=12。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### muggle-string 可以商用吗？

muggle-string 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### muggle-string 还在维护吗？

最新版本 1\.0\.2 发布于 2026/11/04（今天）；至今共发布 20 个版本，其中过去 12 个月发布了 12 个；首次发布于 2022/09/29。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

