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 +28 -19
- package/out/sourceRewrite.d.ts +17 -24
- package/out/sourceRewrite.js +114 -89
- package/out/types.d.ts +12 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Ranked, conflict-free rewrites of a source text, rendered as format-neutral
|
|
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
|
|
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
|
-
-
|
|
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
|
|
20
|
+
import { createSourceRewrite } from 'muggle-string';
|
|
21
21
|
|
|
22
|
-
const text = '
|
|
23
|
-
const rewrite = createSourceRewrite<string, string>('
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
//
|
|
37
|
-
//
|
|
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
|
-
|
|
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.
|
package/out/sourceRewrite.d.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
/** Generated code:
|
|
3
|
-
export type RewriteCode<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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
35
|
-
* cuts of one range the later is inside the earlier. A zero-width cut owns no char and has no
|
|
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
|
-
|
|
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
|
|
48
|
-
*
|
|
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:
|
|
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
|
}
|
package/out/sourceRewrite.js
CHANGED
|
@@ -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
|
-
|
|
58
|
+
const at = this.at;
|
|
59
|
+
let high = at.length;
|
|
58
60
|
while (low < high) {
|
|
59
61
|
const mid = (low + high) >>> 1;
|
|
60
|
-
if (
|
|
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
|
|
80
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
264
|
-
const { start, end, children
|
|
268
|
+
const { edits, head, tail } = rewrite;
|
|
269
|
+
const { start, end, children } = this;
|
|
265
270
|
if (start === end) {
|
|
266
271
|
if (this === rewrite.firstEmpty) {
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
278
|
+
pushAll(head, out);
|
|
274
279
|
}
|
|
275
|
-
const
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
294
|
+
walker.keep(child.start);
|
|
323
295
|
if (child.placeholder !== undefined) {
|
|
324
|
-
|
|
296
|
+
pushCode(child.placeholder, out);
|
|
325
297
|
}
|
|
326
|
-
|
|
327
|
-
seekRemoval();
|
|
298
|
+
walker.skip(child.end);
|
|
328
299
|
edit = edits.lowerBound(child.end, edit);
|
|
329
300
|
}
|
|
330
|
-
|
|
301
|
+
walker.keep(end);
|
|
331
302
|
if (this.isRoot && end === rewrite.text.length) {
|
|
332
|
-
|
|
303
|
+
pushAll(tail, out);
|
|
333
304
|
}
|
|
305
|
+
return out;
|
|
334
306
|
}
|
|
335
307
|
}
|
|
336
|
-
|
|
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
|
-
|
|
363
|
+
pushCode(code, out);
|
|
339
364
|
}
|
|
340
365
|
}
|
|
341
|
-
function
|
|
366
|
+
function pushCode(code, out) {
|
|
342
367
|
if (typeof code === 'string') {
|
|
343
368
|
if (code) {
|
|
344
|
-
|
|
369
|
+
out.push(code);
|
|
345
370
|
}
|
|
346
|
-
return;
|
|
347
371
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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.
|
|
4
|
-
"description": "Ranked, conflict-free rewrites of a source text, rendered as format-neutral
|
|
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",
|