muggle-string 1.0.0-alpha.0 → 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- Ranked, conflict-free rewrites of a source text, rendered as format-neutral segments.
1
+ Ranked, conflict-free rewrites of a source text, rendered as format-neutral pieces.
2
2
 
3
- muggle-string has no dependencies and knows no mapping format: a rewrite renders text and source runs to a sink, and the consumer turns them into whatever its tooling maps with.
3
+ muggle-string has no dependencies and knows no mapping format: a rewrite renders to pieces, new text and ranges of the source, and the consumer turns them into whatever its tooling maps with.
4
4
 
5
5
  > 1.0 replaces the segment-array helpers of 0.4 (`replace`, `replaceSourceRange`, `toString`, ...). They are gone, not deprecated.
6
6
 
@@ -9,7 +9,7 @@ muggle-string has no dependencies and knows no mapping format: a rewrite renders
9
9
  - Writers have ranks. A higher rank sits outside a lower one, and `core` sits outside all.
10
10
  - `insertBefore(offset, code)` and `insertAfter(offset, code)` attach code to a side of a char, so it goes wherever that char goes. `replace(start, end, code)` removes the text and puts `code` in the place of its first char.
11
11
  - Removals add up, so edits of different writers never conflict.
12
- - Only `core` moves text, with `cut(start, end, { data, placeholder })`. Cuts are nested or disjoint. A cut renders its text, with the edits of all writers in it, wherever its slice is placed, exactly once; the cut around it renders `placeholder` instead.
12
+ - Text moves only by `cut(start, end, { data, placeholder })`, which every writer has. The cuts of all writers are nested or disjoint. A cut renders its text, with the edits of all writers in it, wherever its slice is placed, exactly once; the cut around it renders `placeholder` instead.
13
13
  - Code at the head or tail of a cut belongs to the outermost cut that has that edge.
14
14
  - `order` sorts the entries of one writer at one side of one char; equal keys keep call order.
15
15
  - The first render freezes the rewrite.
@@ -17,24 +17,33 @@ muggle-string has no dependencies and knows no mapping format: a rewrite renders
17
17
  ## Usage
18
18
 
19
19
  ```ts
20
- import { createSourceRewrite, type MappedSegment, type Segment } from 'muggle-string';
20
+ import { createSourceRewrite } from 'muggle-string';
21
21
 
22
- const text = 'const props = defineProps<{ a: string }>();';
23
- const rewrite = createSourceRewrite<string, string>('setup', text);
22
+ const text = 'log(name);\nconst name = "Ada";\n';
23
+ const rewrite = createSourceRewrite<string, string>('main.js', text);
24
24
  const { core } = rewrite;
25
+ const renamer = rewrite.writer(0);
25
26
 
26
- const body = core.cut(0, text.length, { data: 'body' });
27
- const typeArg = core.cut(text.indexOf('{'), text.lastIndexOf('}') + 1, { data: 'alias', placeholder: '__VLS_Props' });
28
- core.insertBefore(0, () => ['type __VLS_Props = ', typeArg, ';\n']);
29
-
30
- const segments: Segment<string, string>[] = [];
31
- body.render({
32
- text: text => segments.push(text),
33
- source: (rewrite, start, end, data) => segments.push([rewrite.text.slice(start, end), rewrite.source, start, data]),
34
- segment: (segment: MappedSegment<string, string>) => segments.push(segment),
35
- });
36
- // type __VLS_Props = { a: string };
37
- // const props = defineProps<__VLS_Props>();
27
+ for (const { index } of text.matchAll(/\bname\b/g)) {
28
+ renamer.replace(index, index + 'name'.length, 'user');
29
+ }
30
+
31
+ const file = core.cut(0, text.length, { data: 'file' });
32
+ const declaration = core.cut(text.indexOf('const'), text.length, { data: 'declaration' });
33
+ core.insertBefore(0, declaration);
34
+
35
+ const pieces = file.pieces();
36
+ // [{ source: 'main.js', start: 11, end: 17, data: 'declaration' }, 'user', ..., { source: 'main.js', start: 0, end: 4, data: 'file' }, ...]
37
+ // const user = "Ada";
38
+ // log(user);
38
39
  ```
39
40
 
40
- Code is a string, or a function, called at most once and only if the code renders, that yields strings, mapped segments, and slices.
41
+ ## Pieces
42
+
43
+ ```ts
44
+ type Piece<Source, Data> = string | { source: Source; start: number; end: number; data: Data; text?: string };
45
+ ```
46
+
47
+ A string is new text, which maps nowhere. An object is the range `start`..`end` of the text of `source`: without `text` it copies the text there, and with `text` it is code that stands for the range, mapped as the consumer of the pieces decides. The kept text of a rewrite renders as pieces without `text`, with the data of the cut that owns them.
48
+
49
+ Code is a piece, a slice, which renders its cut in place, or an array of code. `slice.pieces(out)` appends to `out` and returns it.
@@ -1,18 +1,15 @@
1
- import type { MappedSegment, Segment } from './types';
2
- /** Generated code: eager text, or a function that is called at most once, and only if the code renders. */
3
- export type RewriteCode<Source, Data> = string | (() => Iterable<Segment<Source, Data> | RewriteSlice<Source, Data>>);
1
+ import type { Piece } from './types';
2
+ /** Generated code: text, pieces, and slices, which render their cut in place. */
3
+ export type RewriteCode<Source, Data> = Piece<Source, Data> | RewriteSlice<Source, Data> | readonly RewriteCode<Source, Data>[];
4
4
  export interface InsertOptions {
5
5
  /** Ascending among the entries of one writer at one side of one char; equal keys keep call order. Default 0. */
6
6
  order?: number;
7
7
  }
8
- /** Where a slice renders to. */
9
- export interface RewriteSink<Source, Data> {
10
- /** Generated text, never empty. */
11
- text(text: string): void;
12
- /** The kept chars `start`..`end` (never empty) of the text of `rewrite`, owned by a cut whose data is `data`. */
13
- source(rewrite: SourceRewrite<Source, Data>, start: number, end: number, data: Data): void;
14
- /** A mapped segment that generated code carries. */
15
- segment(segment: MappedSegment<Source, Data>): void;
8
+ export interface CutOptions<Source, Data> {
9
+ /** The data of the kept pieces of the cut. */
10
+ data: Data;
11
+ /** The code that takes the place of the cut in the cut that contains it. */
12
+ placeholder?: RewriteCode<Source, Data>;
16
13
  }
17
14
  export interface RewriteWriter<Source, Data> {
18
15
  /**
@@ -27,30 +24,26 @@ export interface RewriteWriter<Source, Data> {
27
24
  insertAfter(offset: number, code: RewriteCode<Source, Data>, options?: InsertOptions): void;
28
25
  /** The text at `start`..`end` (not empty) is not rendered; `code` takes the place of its first char. */
29
26
  replace(start: number, end: number, code: RewriteCode<Source, Data>, options?: InsertOptions): void;
30
- }
31
- export interface CoreRewriteWriter<Source, Data> extends RewriteWriter<Source, Data> {
32
27
  /**
33
28
  * The text at `start`..`end`, with the edits of all writers in it, rendered where the returned slice is placed,
34
- * exactly once. In the cut that contains it, `placeholder` takes its place. Cuts are nested or disjoint, and of two
35
- * cuts of one range the later is inside the earlier. A zero-width cut owns no char and has no placeholder; on an
36
- * empty text, the first one renders the inserts.
29
+ * exactly once. In the cut that contains it, `placeholder` takes its place. The cuts of all writers are nested or
30
+ * disjoint, and of two cuts of one range the later is inside the earlier. A zero-width cut owns no char and has no
31
+ * placeholder; on an empty text, the first one renders the inserts.
37
32
  */
38
- cut(start: number, end: number, options: {
39
- data: Data;
40
- placeholder?: RewriteCode<Source, Data>;
41
- }): RewriteSlice<Source, Data>;
33
+ cut(start: number, end: number, options: CutOptions<Source, Data>): RewriteSlice<Source, Data>;
42
34
  }
43
35
  export interface RewriteSlice<Source, Data> {
44
- render(sink: RewriteSink<Source, Data>): void;
36
+ /** Appends the pieces of the slice to `out`, and returns `out`. A kept piece has no `text`. */
37
+ pieces(out?: Piece<Source, Data>[]): Piece<Source, Data>[];
45
38
  }
46
39
  /**
47
- * Edits of one source text by ranked writers. Inserts attach to a side of a char, removals add up, and only core
48
- * moves text, by nested cuts, so edits never conflict. The first render freezes the edits.
40
+ * Edits of one source text by ranked writers. Inserts attach to a side of a char, removals add up, and text moves
41
+ * only by nested cuts, so edits never conflict. The first render freezes the edits.
49
42
  */
50
43
  export interface SourceRewrite<Source, Data> {
51
44
  readonly source: Source;
52
45
  readonly text: string;
53
- readonly core: CoreRewriteWriter<Source, Data>;
46
+ readonly core: RewriteWriter<Source, Data>;
54
47
  /** The writer of `rank`, a non-negative integer. A higher rank sits outside a lower one, and core outside all. */
55
48
  writer(rank: number): RewriteWriter<Source, Data>;
56
49
  }
@@ -51,13 +51,15 @@ class Edits {
51
51
  this.side[i] = record.side;
52
52
  this.code[i] = record.code;
53
53
  }
54
+ this.records = [];
54
55
  }
55
56
  /** The first index from `low` whose anchor is at or after `offset`. */
56
57
  lowerBound(offset, low) {
57
- let high = this.at.length;
58
+ const at = this.at;
59
+ let high = at.length;
58
60
  while (low < high) {
59
61
  const mid = (low + high) >>> 1;
60
- if (this.at[mid] < offset) {
62
+ if (at[mid] < offset) {
61
63
  low = mid + 1;
62
64
  }
63
65
  else {
@@ -76,29 +78,14 @@ class Rewrite {
76
78
  this.head = new Edits();
77
79
  /** `insertBefore(length)`: after all text. */
78
80
  this.tail = new Edits();
79
- /** Removed ranges as flat start, end pairs; merged when frozen. */
80
- this.removals = [];
81
+ /** Removed ranges as flat start, end pairs. */
82
+ this.removalPairs = [];
83
+ /** The removed ranges, sorted and merged, filled when frozen. */
84
+ this.removals = new Int32Array(0);
81
85
  /** The top-level cuts, sorted and disjoint. */
82
86
  this.roots = [];
83
87
  this.frozen = false;
84
- const core = this.createWriter(coreRank);
85
- core.cut = (start, end, { data, placeholder }) => {
86
- this.check(start, end);
87
- const cut = new Cut(this, start, end, data, placeholder);
88
- if (start === end) {
89
- if (placeholder !== undefined) {
90
- throw new RangeError(`The empty cut ${start}..${end} cannot have a placeholder.`);
91
- }
92
- if (!this.text.length) {
93
- this.firstEmpty ??= cut;
94
- }
95
- }
96
- else {
97
- this.insertCut(cut);
98
- }
99
- return cut;
100
- };
101
- this.core = core;
88
+ this.core = this.createWriter(coreRank);
102
89
  }
103
90
  writer(rank) {
104
91
  if (!Number.isInteger(rank) || rank < 0 || rank >= coreRank) {
@@ -134,7 +121,23 @@ class Rewrite {
134
121
  throw new RangeError(`The replace at ${start}..${end} removes no char.`);
135
122
  }
136
123
  this.edits.push(start, 1 /* Side.Slot */, rank, getOrder(options), code);
137
- this.removals.push(start, end);
124
+ this.removalPairs.push(start, end);
125
+ },
126
+ cut: (start, end, { data, placeholder }) => {
127
+ this.check(start, end);
128
+ const cut = new Cut(this, start, end, data, placeholder);
129
+ if (start === end) {
130
+ if (placeholder !== undefined) {
131
+ throw new RangeError(`The empty cut ${start}..${end} cannot have a placeholder.`);
132
+ }
133
+ if (!this.text.length) {
134
+ this.firstEmpty ??= cut;
135
+ }
136
+ }
137
+ else {
138
+ this.insertCut(cut);
139
+ }
140
+ return cut;
138
141
  },
139
142
  };
140
143
  }
@@ -201,7 +204,7 @@ class Rewrite {
201
204
  this.edits.freeze();
202
205
  this.head.freeze();
203
206
  this.tail.freeze();
204
- const removals = this.removals;
207
+ const removals = this.removalPairs;
205
208
  const pairs = removals.length >> 1;
206
209
  for (let pair = 1; pair < pairs; pair++) {
207
210
  if (removals[2 * pair - 2] > removals[2 * pair]) {
@@ -229,6 +232,8 @@ class Rewrite {
229
232
  }
230
233
  }
231
234
  removals.length = merged;
235
+ this.removals = Int32Array.from(removals);
236
+ this.removalPairs = [];
232
237
  for (const root of this.roots) {
233
238
  root.isRoot = true;
234
239
  }
@@ -253,59 +258,26 @@ class Cut {
253
258
  this.isRoot = false;
254
259
  this.placed = false;
255
260
  }
256
- render(sink) {
261
+ pieces(out = []) {
257
262
  if (this.placed) {
258
263
  throw new Error(`The cut ${this.start}..${this.end} is placed twice.`);
259
264
  }
260
265
  this.placed = true;
261
266
  const rewrite = this.rewrite;
262
267
  rewrite.freeze();
263
- const { edits, removals, head, tail } = rewrite;
264
- const { start, end, children, data } = this;
268
+ const { edits, head, tail } = rewrite;
269
+ const { start, end, children } = this;
265
270
  if (start === end) {
266
271
  if (this === rewrite.firstEmpty) {
267
- renderAll(head, sink);
268
- renderAll(tail, sink);
272
+ pushAll(head, out);
273
+ pushAll(tail, out);
269
274
  }
270
- return;
275
+ return out;
271
276
  }
272
277
  if (this.isRoot && start === 0) {
273
- renderAll(head, sink);
278
+ pushAll(head, out);
274
279
  }
275
- const removalsEnd = removals.length;
276
- let pos = start;
277
- let removal = 0;
278
- // the first removal that ends after `pos`, from `removal` on
279
- const seekRemoval = () => {
280
- let low = removal >> 1;
281
- let high = removalsEnd >> 1;
282
- while (low < high) {
283
- const mid = (low + high) >>> 1;
284
- if (removals[2 * mid + 1] <= pos) {
285
- low = mid + 1;
286
- }
287
- else {
288
- high = mid;
289
- }
290
- }
291
- removal = low * 2;
292
- };
293
- // the kept chars from `pos` to `to`, as runs between the removals
294
- const renderText = (to) => {
295
- while (pos < to) {
296
- while (removal < removalsEnd && removals[removal + 1] <= pos) {
297
- removal += 2;
298
- }
299
- if (removal < removalsEnd && removals[removal] <= pos) {
300
- pos = Math.min(removals[removal + 1], to);
301
- continue;
302
- }
303
- const stop = removal < removalsEnd ? Math.min(removals[removal], to) : to;
304
- sink.source(rewrite, pos, stop, data);
305
- pos = stop;
306
- }
307
- };
308
- seekRemoval();
280
+ const walker = new Walker(rewrite.removals, rewrite.source, this.data, out, start);
309
281
  const { at, side, code } = edits;
310
282
  const size = at.length;
311
283
  let edit = edits.lowerBound(start, 0);
@@ -313,50 +285,103 @@ class Cut {
313
285
  const child = children[index];
314
286
  const until = child ? child.start : end;
315
287
  for (; edit < size && at[edit] < until; edit++) {
316
- renderText(side[edit] === 2 /* Side.After */ ? at[edit] + 1 : at[edit]);
317
- renderCode(code[edit], sink);
288
+ walker.keep(side[edit] === 2 /* Side.After */ ? at[edit] + 1 : at[edit]);
289
+ pushCode(code[edit], out);
318
290
  }
319
291
  if (!child) {
320
292
  break;
321
293
  }
322
- renderText(child.start);
294
+ walker.keep(child.start);
323
295
  if (child.placeholder !== undefined) {
324
- renderCode(child.placeholder, sink);
296
+ pushCode(child.placeholder, out);
325
297
  }
326
- pos = child.end;
327
- seekRemoval();
298
+ walker.skip(child.end);
328
299
  edit = edits.lowerBound(child.end, edit);
329
300
  }
330
- renderText(end);
301
+ walker.keep(end);
331
302
  if (this.isRoot && end === rewrite.text.length) {
332
- renderAll(tail, sink);
303
+ pushAll(tail, out);
333
304
  }
305
+ return out;
334
306
  }
335
307
  }
336
- function renderAll(edits, sink) {
308
+ /** Pushes the kept chars of one cut, as pieces between the removals. */
309
+ class Walker {
310
+ constructor(removals, source, data, out, pos) {
311
+ this.removals = removals;
312
+ this.source = source;
313
+ this.data = data;
314
+ this.out = out;
315
+ this.pos = pos;
316
+ /** The first removal that ends after `pos`. */
317
+ this.removal = 0;
318
+ this.seek();
319
+ }
320
+ /** The kept chars from `pos` to `to`. */
321
+ keep(to) {
322
+ const { removals, out } = this;
323
+ const size = removals.length;
324
+ let { pos, removal } = this;
325
+ while (pos < to) {
326
+ while (removal < size && removals[removal + 1] <= pos) {
327
+ removal += 2;
328
+ }
329
+ if (removal < size && removals[removal] <= pos) {
330
+ pos = removals[removal + 1] < to ? removals[removal + 1] : to;
331
+ continue;
332
+ }
333
+ const stop = removal < size && removals[removal] < to ? removals[removal] : to;
334
+ out.push({ source: this.source, start: pos, end: stop, data: this.data });
335
+ pos = stop;
336
+ }
337
+ this.pos = pos;
338
+ this.removal = removal;
339
+ }
340
+ /** Moves past the chars up to `to`, which a child cut owns. */
341
+ skip(to) {
342
+ this.pos = to;
343
+ this.seek();
344
+ }
345
+ seek() {
346
+ const removals = this.removals;
347
+ let low = this.removal >> 1;
348
+ let high = removals.length >> 1;
349
+ while (low < high) {
350
+ const mid = (low + high) >>> 1;
351
+ if (removals[2 * mid + 1] <= this.pos) {
352
+ low = mid + 1;
353
+ }
354
+ else {
355
+ high = mid;
356
+ }
357
+ }
358
+ this.removal = low * 2;
359
+ }
360
+ }
361
+ function pushAll(edits, out) {
337
362
  for (const code of edits.code) {
338
- renderCode(code, sink);
363
+ pushCode(code, out);
339
364
  }
340
365
  }
341
- function renderCode(code, sink) {
366
+ function pushCode(code, out) {
342
367
  if (typeof code === 'string') {
343
368
  if (code) {
344
- sink.text(code);
369
+ out.push(code);
345
370
  }
346
- return;
347
371
  }
348
- for (const item of code()) {
349
- if (typeof item === 'string') {
350
- if (item) {
351
- sink.text(item);
352
- }
353
- }
354
- else if (Array.isArray(item)) {
355
- sink.segment(item);
356
- }
357
- else {
358
- item.render(sink);
372
+ else if (code instanceof Cut) {
373
+ code.pieces(out);
374
+ }
375
+ else if (isCodeArray(code)) {
376
+ for (let index = 0; index < code.length; index++) {
377
+ pushCode(code[index], out);
359
378
  }
360
379
  }
380
+ else {
381
+ out.push(code);
382
+ }
383
+ }
384
+ function isCodeArray(code) {
385
+ return Array.isArray(code);
361
386
  }
362
387
  //# sourceMappingURL=sourceRewrite.js.map
package/out/types.d.ts CHANGED
@@ -1,4 +1,12 @@
1
- /** Generated code: plain text, or text that maps to `sourceOffset` of `source`. */
2
- export type Segment<Source, Data> = string | [text: string, source: Source, sourceOffset: number, data: Data];
3
- /** A segment of generated code that maps to a source. */
4
- export type MappedSegment<Source, Data> = Exclude<Segment<Source, Data>, string>;
1
+ /**
2
+ * Generated code: new text, which maps nowhere, or the range `start`..`end` of the text of `source`. A range without
3
+ * `text` is a copy of the text there; `text` is code that stands for the range, mapped as its consumer decides. An
4
+ * empty range is an anchor.
5
+ */
6
+ export type Piece<Source, Data> = string | {
7
+ source: Source;
8
+ start: number;
9
+ end: number;
10
+ data: Data;
11
+ text?: string;
12
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muggle-string",
3
- "version": "1.0.0-alpha.0",
4
- "description": "Ranked, conflict-free rewrites of a source text, rendered as format-neutral segments",
3
+ "version": "1.0.0-alpha.1",
4
+ "description": "Ranked, conflict-free rewrites of a source text, rendered as format-neutral pieces",
5
5
  "main": "out/index.js",
6
6
  "types": "out/index.d.ts",
7
7
  "license": "MIT",