@miragon/wardley-dsl 0.6.0 → 0.7.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 +18 -5
- package/dist/index.cjs +53 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +53 -31
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @miragon/wardley-dsl
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@miragon/wardley-dsl)
|
|
4
|
-
[](https://github.com/Miragon/modeler-suite/blob/main/modelers/wardley-maps/LICENSE)
|
|
5
5
|
|
|
6
6
|
DOM-free bridge between the [Online-Wardley-Maps](https://docs.onlinewardleymaps.com/) text DSL and
|
|
7
|
-
the `WardleyMap` model
|
|
7
|
+
the `WardleyMap` model, as a lossless round-trip.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ npm install @miragon/wardley-dsl
|
|
|
15
15
|
## Usage
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import { parseDSL, serializeDSL } from
|
|
18
|
+
import { parseDSL, serializeDSL } from "@miragon/wardley-dsl";
|
|
19
19
|
|
|
20
20
|
const map = parseDSL(`title Tea Shop
|
|
21
21
|
component Kettle [0.43, 0.35]
|
|
@@ -31,7 +31,7 @@ the end, numbers like `0.80` as `0.8`. Where the text file is the source of trut
|
|
|
31
31
|
file under Git), write an edited map back with `patchDSL` instead:
|
|
32
32
|
|
|
33
33
|
```ts
|
|
34
|
-
import { parseDSL, patchDSL } from
|
|
34
|
+
import { parseDSL, patchDSL } from "@miragon/wardley-dsl";
|
|
35
35
|
|
|
36
36
|
const map = parseDSL(fileText);
|
|
37
37
|
// … the user moves, renames, adds or deletes something …
|
|
@@ -49,7 +49,20 @@ The result always reads back as the same map as `serializeDSL(map)`. When an edi
|
|
|
49
49
|
safely into the source layout, `patchDSL` falls back to the canonical `serializeDSL(map)` text (in
|
|
50
50
|
the source's line break style). It never throws.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
### Ids in the document
|
|
53
|
+
|
|
54
|
+
The OWM DSL writes no ids: the parser derives them from names (`cmp_cup_of_tea`), note texts and
|
|
55
|
+
annotation numbers, with a `_2` suffix for a repeat in line order. An editor that creates or renames
|
|
56
|
+
an element holds an id the text does not give it until the text is read again. `readBackIds(map)`
|
|
57
|
+
tells, by the ids of `map`, which id each element carries once `serializeDSL(map)` is read back:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { readBackIds } from "@miragon/wardley-dsl";
|
|
61
|
+
|
|
62
|
+
readBackIds(editedMap).get("shape_42"); // 'cmp_hot_water'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Part of the [Wardley Maps Modeler](https://github.com/Miragon/modeler-suite/tree/main/modelers/wardley-maps) in the [Miragon modeler suite](https://github.com/Miragon/modeler-suite).
|
|
53
66
|
|
|
54
67
|
## License
|
|
55
68
|
|
package/dist/index.cjs
CHANGED
|
@@ -465,7 +465,7 @@ function unknownStatementMessage(line) {
|
|
|
465
465
|
const core = linkCore(line);
|
|
466
466
|
if (/->|\+('[^']*')?(<>|>|<)/.test(core)) return "Link needs a name on both sides";
|
|
467
467
|
const flowValue = /\+'[^']*'/.exec(core);
|
|
468
|
-
if (flowValue) return `Flow ${flowValue[0]} has no direction
|
|
468
|
+
if (flowValue) return `Flow ${flowValue[0]} has no direction: add ">", "<" or "<>"`;
|
|
469
469
|
return `Unknown statement "${line.split(/\s/)[0]}"`;
|
|
470
470
|
}
|
|
471
471
|
function buildEdges(links, idOfName, unresolved, labelOf, built) {
|
|
@@ -595,7 +595,7 @@ var MapReader = class {
|
|
|
595
595
|
}
|
|
596
596
|
/** For a line that looks like a known construct but cannot be parsed. */
|
|
597
597
|
failed(line) {
|
|
598
|
-
this.keep(line, "Line
|
|
598
|
+
this.keep(line, "Line not understood: kept as written");
|
|
599
599
|
}
|
|
600
600
|
// A reference binds to the name as written first, so `X 2` stays bound to an explicit "X 2" even
|
|
601
601
|
// when a duplicate "X" was renamed to "X 2"; the serializer then writes that reference by the
|
|
@@ -610,7 +610,7 @@ var MapReader = class {
|
|
|
610
610
|
setConfig(keyword, update) {
|
|
611
611
|
const earlier = this.lineThatSetConfig.get(keyword);
|
|
612
612
|
if (earlier !== void 0) {
|
|
613
|
-
this.diag(`Repeated "${keyword}"
|
|
613
|
+
this.diag(`Repeated "${keyword}": replaces the one on line ${earlier}`);
|
|
614
614
|
}
|
|
615
615
|
this.lineThatSetConfig.set(keyword, this.lineNo);
|
|
616
616
|
const configLines2 = this.configLinesByKeyword.get(keyword) ?? [];
|
|
@@ -648,7 +648,7 @@ var MapReader = class {
|
|
|
648
648
|
return true;
|
|
649
649
|
}
|
|
650
650
|
this.keywordLinks.push(link);
|
|
651
|
-
if (leftDeclared) this.diag(`Link: "${link.right}" not found
|
|
651
|
+
if (leftDeclared) this.diag(`Link: "${link.right}" not found; read as a "${kw}" statement`);
|
|
652
652
|
return false;
|
|
653
653
|
}
|
|
654
654
|
/** Clamps a normalized value to [0,1] — with a diagnostic instead of a later validation crash. */
|
|
@@ -664,7 +664,7 @@ var MapReader = class {
|
|
|
664
664
|
}
|
|
665
665
|
/** A suffix written in front of the coordinates is part of the name or text (OWM). */
|
|
666
666
|
reportMisplacedSuffix(groups, where, fix) {
|
|
667
|
-
if (groups.length) this.diag(`"${groups.join(" ")}" ${where}
|
|
667
|
+
if (groups.length) this.diag(`"${groups.join(" ")}" ${where}: ${fix}`);
|
|
668
668
|
}
|
|
669
669
|
/**
|
|
670
670
|
* A text as the serializer writes it. Its comment markers are protected only by flow values
|
|
@@ -675,7 +675,7 @@ var MapReader = class {
|
|
|
675
675
|
const written = escapeCommentMarkers(text);
|
|
676
676
|
if (written !== text) {
|
|
677
677
|
this.diag(
|
|
678
|
-
`"${text}" read as "${written}"
|
|
678
|
+
`"${text}" read as "${written}", because "//" or "/*" there would start a comment once saved`,
|
|
679
679
|
line,
|
|
680
680
|
source
|
|
681
681
|
);
|
|
@@ -688,7 +688,7 @@ var MapReader = class {
|
|
|
688
688
|
const name = this.namesInUse.claim(base);
|
|
689
689
|
if (name !== base) {
|
|
690
690
|
this.diag(
|
|
691
|
-
`Duplicate name "${sourceName}"
|
|
691
|
+
`Duplicate name "${sourceName}": renamed to "${name}"; links bind to the first "${sourceName}"`,
|
|
692
692
|
atLine,
|
|
693
693
|
atText
|
|
694
694
|
);
|
|
@@ -702,20 +702,20 @@ var MapReader = class {
|
|
|
702
702
|
this.blockPipeline = this.lastPipeline;
|
|
703
703
|
this.recordBlockLine(this.blockPipeline, "open");
|
|
704
704
|
this.blockOpening = { line: this.lineNo, text: this.currentLine };
|
|
705
|
-
} else this.diag('Ignored "{"
|
|
705
|
+
} else this.diag('Ignored "{": a block must directly follow a pipeline line');
|
|
706
706
|
this.lastPipeline = null;
|
|
707
707
|
}
|
|
708
708
|
closeBlock() {
|
|
709
709
|
if (this.blockPipeline) {
|
|
710
710
|
this.recordBlockLine(this.blockPipeline, "close");
|
|
711
711
|
this.blockPipeline = null;
|
|
712
|
-
} else this.diag('Ignored "}"
|
|
712
|
+
} else this.diag('Ignored "}": no pipeline block is open');
|
|
713
713
|
this.lastPipeline = null;
|
|
714
714
|
}
|
|
715
715
|
endUnclosedBlock() {
|
|
716
716
|
if (!this.blockPipeline) return;
|
|
717
717
|
this.diag(
|
|
718
|
-
`Pipeline block of "${this.blockPipeline.name}" is never closed
|
|
718
|
+
`Pipeline block of "${this.blockPipeline.name}" is never closed: add "}"`,
|
|
719
719
|
this.blockOpening.line,
|
|
720
720
|
this.blockOpening.text
|
|
721
721
|
);
|
|
@@ -755,7 +755,7 @@ var MapReader = class {
|
|
|
755
755
|
if (blockCommentStart) {
|
|
756
756
|
const { line, text: startText } = blockCommentStart;
|
|
757
757
|
this.diag(
|
|
758
|
-
'Block comment "/*" is never closed
|
|
758
|
+
'Block comment "/*" is never closed: the rest of the file is a comment',
|
|
759
759
|
line,
|
|
760
760
|
startText
|
|
761
761
|
);
|
|
@@ -856,7 +856,7 @@ var MapReader = class {
|
|
|
856
856
|
}
|
|
857
857
|
if (splitAtCoords(after)?.name) {
|
|
858
858
|
this.diag(
|
|
859
|
-
`Component with [visibility, maturity] inside the block of pipeline "${blockPipeline.name}" is not a child
|
|
859
|
+
`Component with [visibility, maturity] inside the block of pipeline "${blockPipeline.name}" is not a child: use [maturity]`
|
|
860
860
|
);
|
|
861
861
|
}
|
|
862
862
|
return false;
|
|
@@ -924,7 +924,7 @@ var MapReader = class {
|
|
|
924
924
|
let body = after;
|
|
925
925
|
let opensBlock = false;
|
|
926
926
|
while (body.endsWith("{")) {
|
|
927
|
-
if (opensBlock) this.diag('Ignored "{"
|
|
927
|
+
if (opensBlock) this.diag('Ignored "{": a block must directly follow a pipeline line');
|
|
928
928
|
opensBlock = true;
|
|
929
929
|
body = body.slice(0, -1).trim();
|
|
930
930
|
}
|
|
@@ -991,7 +991,7 @@ var MapReader = class {
|
|
|
991
991
|
if (problem) this.diag(problem);
|
|
992
992
|
if (!Number.isInteger(coords.a) || !Number.isInteger(coords.b)) {
|
|
993
993
|
this.diag(
|
|
994
|
-
`Map size ${coords.a}\xD7${coords.b} is not in whole pixels
|
|
994
|
+
`Map size ${coords.a}\xD7${coords.b} is not in whole pixels: OWM only reads whole-pixel sizes`
|
|
995
995
|
);
|
|
996
996
|
}
|
|
997
997
|
}
|
|
@@ -1020,7 +1020,7 @@ var MapReader = class {
|
|
|
1020
1020
|
});
|
|
1021
1021
|
if (written.length !== 1 && !(written.length === 3 && written.every(Boolean))) {
|
|
1022
1022
|
const read = [label, ...ends ?? []].join("->");
|
|
1023
|
-
this.diag(`y-axis takes "Label" or "Label->Bottom->Top"
|
|
1023
|
+
this.diag(`y-axis takes "Label" or "Label->Bottom->Top": read as "${read}"`);
|
|
1024
1024
|
}
|
|
1025
1025
|
}
|
|
1026
1026
|
readAnnotationsBox(line, after) {
|
|
@@ -1028,7 +1028,7 @@ var MapReader = class {
|
|
|
1028
1028
|
if (!split) {
|
|
1029
1029
|
return this.keep(
|
|
1030
1030
|
line,
|
|
1031
|
-
"Annotations box has no position
|
|
1031
|
+
"Annotations box has no position: not used; add [visibility, maturity]"
|
|
1032
1032
|
);
|
|
1033
1033
|
}
|
|
1034
1034
|
this.reportTextBefore(split.name);
|
|
@@ -1043,7 +1043,7 @@ var MapReader = class {
|
|
|
1043
1043
|
if (!Number.isSafeInteger(number)) return this.failed(line);
|
|
1044
1044
|
const found = parsePositions(after.replace(/^\s*\d+\s*/, ""));
|
|
1045
1045
|
if (!found) {
|
|
1046
|
-
return this.keep(line, "Annotation has no position
|
|
1046
|
+
return this.keep(line, "Annotation has no position: not drawn; add [visibility, maturity]");
|
|
1047
1047
|
}
|
|
1048
1048
|
if (!found.tuples.length) return this.failed(line);
|
|
1049
1049
|
for (const unreadable of found.unreadable) this.diag(unreadableNumberMessage(unreadable));
|
|
@@ -1110,7 +1110,7 @@ var MapReader = class {
|
|
|
1110
1110
|
*/
|
|
1111
1111
|
readMethod(line, after, kw) {
|
|
1112
1112
|
if (this.pushComponent(after, kw)) return;
|
|
1113
|
-
if (!after) return this.keep(line, `${kw}: needs a component name
|
|
1113
|
+
if (!after) return this.keep(line, `${kw}: needs a component name; not used`);
|
|
1114
1114
|
this.pendingMethod.push({
|
|
1115
1115
|
name: after,
|
|
1116
1116
|
method: kw,
|
|
@@ -1282,7 +1282,7 @@ var MapReader = class {
|
|
|
1282
1282
|
this.keepLine(p.raw, p.lineNo, p.text);
|
|
1283
1283
|
if (!component) {
|
|
1284
1284
|
this.diag(
|
|
1285
|
-
`Pipeline "${p.name}" has no range
|
|
1285
|
+
`Pipeline "${p.name}" has no range: not drawn; add [start, end] or child components`,
|
|
1286
1286
|
p.lineNo,
|
|
1287
1287
|
p.text
|
|
1288
1288
|
);
|
|
@@ -1295,7 +1295,7 @@ var MapReader = class {
|
|
|
1295
1295
|
const widened = range.start !== writtenRange.start || range.end !== writtenRange.end;
|
|
1296
1296
|
if (widened && !writtenRange.fromChildren) {
|
|
1297
1297
|
this.diag(
|
|
1298
|
-
`Pipeline "${p.name}" range is empty or reversed
|
|
1298
|
+
`Pipeline "${p.name}" range is empty or reversed: changed to [${range.start}, ${range.end}]`,
|
|
1299
1299
|
p.lineNo,
|
|
1300
1300
|
p.text
|
|
1301
1301
|
);
|
|
@@ -1344,7 +1344,7 @@ var MapReader = class {
|
|
|
1344
1344
|
const component = this.elements[index];
|
|
1345
1345
|
const earlier = evolveLineOf.get(component.id);
|
|
1346
1346
|
if (earlier !== void 0) {
|
|
1347
|
-
const message = `Repeated evolve for "${ev.name}"
|
|
1347
|
+
const message = `Repeated evolve for "${ev.name}": replaces the one on line ${earlier}`;
|
|
1348
1348
|
this.diag(message, ev.lineNo, ev.text);
|
|
1349
1349
|
}
|
|
1350
1350
|
evolveLineOf.set(component.id, ev.lineNo);
|
|
@@ -1372,7 +1372,7 @@ var MapReader = class {
|
|
|
1372
1372
|
const el = this.elements[index];
|
|
1373
1373
|
const replaced = el.decorators?.method;
|
|
1374
1374
|
if (replaced && replaced !== pm.method) {
|
|
1375
|
-
const message = `${pm.method}:
|
|
1375
|
+
const message = `${pm.method}: replaces the method "${replaced}" of "${pm.name}"`;
|
|
1376
1376
|
this.diag(message, pm.lineNo, pm.text);
|
|
1377
1377
|
}
|
|
1378
1378
|
this.elements[index] = { ...el, decorators: { ...el.decorators, method: pm.method } };
|
|
@@ -1383,7 +1383,7 @@ var MapReader = class {
|
|
|
1383
1383
|
}
|
|
1384
1384
|
};
|
|
1385
1385
|
function renamedMessage(sourceName, name) {
|
|
1386
|
-
return `Name "${sourceName}" renamed to "${name}"
|
|
1386
|
+
return `Name "${sourceName}" renamed to "${name}", because a name cannot hold link operators ("->", "+>", "+<", "+'"), comment markers or a leading "{"`;
|
|
1387
1387
|
}
|
|
1388
1388
|
function withoutTrailingCarriageReturns(line) {
|
|
1389
1389
|
let end = line.length;
|
|
@@ -1397,7 +1397,7 @@ function withoutClosersOfKeptComments(entries, report) {
|
|
|
1397
1397
|
if (!entry.isComment && nextParse.insideBlockComment && text.includes("*/")) {
|
|
1398
1398
|
text = text.replaceAll("*/", " ");
|
|
1399
1399
|
report(
|
|
1400
|
-
'Ignored "*/"
|
|
1400
|
+
'Ignored "*/": this line is kept after an unclosed "/*" and would end that comment',
|
|
1401
1401
|
entry.lineNo,
|
|
1402
1402
|
entry.source
|
|
1403
1403
|
);
|
|
@@ -1416,7 +1416,7 @@ function replacedConfigLines(unusedLines, config, report) {
|
|
|
1416
1416
|
const replaced = configIsSet[unused.keyword];
|
|
1417
1417
|
if (replaced) replacedIndexes.add(unused.index);
|
|
1418
1418
|
const outcome = replaced ? `removed; another "${unused.keyword}" line sets it` : "not used";
|
|
1419
|
-
report(`${unused.reason}
|
|
1419
|
+
report(`${unused.reason}: ${outcome}`, unused.line, unused.text);
|
|
1420
1420
|
}
|
|
1421
1421
|
return replacedIndexes;
|
|
1422
1422
|
}
|
|
@@ -1428,7 +1428,7 @@ function resolveUrlReferences(elements, refs, definitions, report, resolved) {
|
|
|
1428
1428
|
if (earlier) {
|
|
1429
1429
|
replaced.add(earlier);
|
|
1430
1430
|
report(
|
|
1431
|
-
`Repeated url definition "${definition.name}"
|
|
1431
|
+
`Repeated url definition "${definition.name}": replaces the one on line ${earlier.lineNo}, which is kept unused`,
|
|
1432
1432
|
definition.lineNo,
|
|
1433
1433
|
definition.text
|
|
1434
1434
|
);
|
|
@@ -1441,7 +1441,7 @@ function resolveUrlReferences(elements, refs, definitions, report, resolved) {
|
|
|
1441
1441
|
if (definition) used.add(definition);
|
|
1442
1442
|
const address = definition?.address ?? (/^[a-z][\w+.-]*:\/\//i.test(ref) ? ref : void 0);
|
|
1443
1443
|
if (!address) {
|
|
1444
|
-
report(`url(${ref}) has no matching "url ${ref} [address]" line
|
|
1444
|
+
report(`url(${ref}) has no matching "url ${ref} [address]" line: ignored`, line, text);
|
|
1445
1445
|
continue;
|
|
1446
1446
|
}
|
|
1447
1447
|
const el = elements[index];
|
|
@@ -1576,7 +1576,7 @@ function splitPipelineLine(body) {
|
|
|
1576
1576
|
return { name: rest, range: null, suffixText: groups.join(" "), misplaced: [] };
|
|
1577
1577
|
}
|
|
1578
1578
|
function unreadableNumberMessage(text) {
|
|
1579
|
-
return `Unreadable number in "${text}"
|
|
1579
|
+
return `Unreadable number in "${text}": ignored`;
|
|
1580
1580
|
}
|
|
1581
1581
|
function pipelineRange(p) {
|
|
1582
1582
|
if (p.start !== void 0 && p.end !== void 0) {
|
|
@@ -1668,7 +1668,7 @@ function splitMisplacedEvolveSuffix(name) {
|
|
|
1668
1668
|
return rest ? { name: rest, suffix: m[0] } : null;
|
|
1669
1669
|
}
|
|
1670
1670
|
function misplacedEvolveSuffixHint({ name, suffix }) {
|
|
1671
|
-
return suffix.startsWith("(") ?
|
|
1671
|
+
return suffix.startsWith("(") ? `; for component "${name}", write "${suffix}" after the target` : `; for component "${name}", write "${suffix}" on its component line`;
|
|
1672
1672
|
}
|
|
1673
1673
|
var NUMBER_TOKEN_RE = /(^|\s)([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[-+]?\d+)?)(?=\s|\(|$)/gi;
|
|
1674
1674
|
function parseEvolve(after, report) {
|
|
@@ -1707,7 +1707,7 @@ function sizeOutsideLimits(width, height) {
|
|
|
1707
1707
|
const minimum = `${wardleySchemaModel.PLOT_MIN.width}\xD7${wardleySchemaModel.PLOT_MIN.height}`;
|
|
1708
1708
|
const maximum = `${wardleySchemaModel.PLOT_MAX.width}\xD7${wardleySchemaModel.PLOT_MAX.height}`;
|
|
1709
1709
|
const limit = below && above ? `outside ${minimum} to ${maximum}` : below ? `below the minimum ${minimum}` : `above the maximum ${maximum}`;
|
|
1710
|
-
return `Map size ${width}\xD7${height} is ${limit}
|
|
1710
|
+
return `Map size ${width}\xD7${height} is ${limit}: drawn at ${drawnWidth}\xD7${drawnHeight}`;
|
|
1711
1711
|
}
|
|
1712
1712
|
|
|
1713
1713
|
// src/serializer.ts
|
|
@@ -2485,6 +2485,28 @@ function fingerprint(map) {
|
|
|
2485
2485
|
return entries.sort().join("\n");
|
|
2486
2486
|
}
|
|
2487
2487
|
|
|
2488
|
+
// src/read-back-ids.ts
|
|
2489
|
+
var ELEMENT_KEY = /^(?:element|child):(.*)$/s;
|
|
2490
|
+
function elementIdOf(key) {
|
|
2491
|
+
return ELEMENT_KEY.exec(key)?.[1];
|
|
2492
|
+
}
|
|
2493
|
+
function readBackIds(map) {
|
|
2494
|
+
const lines = serializeKeyedLines(map);
|
|
2495
|
+
const { sourceMap } = parseDSLWithSourceMap([...lines.map((line) => line.text), ""].join("\n"));
|
|
2496
|
+
const readIdByLine = /* @__PURE__ */ new Map();
|
|
2497
|
+
for (const [key, line] of sourceMap.primaryLineOf) {
|
|
2498
|
+
const id = elementIdOf(key);
|
|
2499
|
+
if (id !== void 0) readIdByLine.set(line, id);
|
|
2500
|
+
}
|
|
2501
|
+
const ids = /* @__PURE__ */ new Map();
|
|
2502
|
+
lines.forEach((line, index) => {
|
|
2503
|
+
const id = elementIdOf(line.key);
|
|
2504
|
+
const readId = readIdByLine.get(index + 1);
|
|
2505
|
+
if (id !== void 0 && readId !== void 0) ids.set(id, readId);
|
|
2506
|
+
});
|
|
2507
|
+
return ids;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2488
2510
|
// src/raw-passthrough.ts
|
|
2489
2511
|
function withoutRawStatements(rawPassthrough, keyword) {
|
|
2490
2512
|
if (!rawPassthrough) return rawPassthrough;
|
|
@@ -2524,6 +2546,7 @@ exports.parseDSLWithDiagnostics = parseDSLWithDiagnostics;
|
|
|
2524
2546
|
exports.parseDecorators = parseDecorators;
|
|
2525
2547
|
exports.parseLabelOffset = parseLabelOffset;
|
|
2526
2548
|
exports.patchDSL = patchDSL;
|
|
2549
|
+
exports.readBackIds = readBackIds;
|
|
2527
2550
|
exports.serializeDSL = serializeDSL;
|
|
2528
2551
|
exports.slug = slug;
|
|
2529
2552
|
exports.withoutRawStatements = withoutRawStatements;
|