@datamoc/mw_games 0.8.1 → 0.9.0-alpha
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/CHANGELOG.md +56 -0
- package/MWG - Best Practices.md +97 -0
- package/MWG - Deep Code and Architecture Review.md +238 -0
- package/README.md +7 -4
- package/dist/core/Sanitize.d.ts +8 -0
- package/dist/core/Sanitize.d.ts.map +1 -1
- package/dist/core/Sanitize.js +20 -0
- package/dist/core/Sanitize.js.map +1 -1
- package/dist/core/Save.d.ts +8 -1
- package/dist/core/Save.d.ts.map +1 -1
- package/dist/core/Save.js +26 -7
- package/dist/core/Save.js.map +1 -1
- package/dist/mw_games.global.js +107 -91
- package/dist/mwl/compiler.d.ts +7 -7
- package/dist/mwl/compiler.js +7 -7
- package/dist/mwl/content.d.ts +1 -1
- package/dist/mwl/content.js +1 -1
- package/dist/mwl/grammar.d.ts +18 -8
- package/dist/mwl/grammar.d.ts.map +1 -1
- package/dist/mwl/grammar.js +227 -92
- package/dist/mwl/grammar.js.map +1 -1
- package/dist/mwl/persistence.d.ts +4 -1
- package/dist/mwl/persistence.d.ts.map +1 -1
- package/dist/mwl/persistence.js +6 -1
- package/dist/mwl/persistence.js.map +1 -1
- package/dist/mwl/readers.d.ts +2 -2
- package/dist/mwl/readers.js +2 -2
- package/dist/mwl/report.d.ts +2 -2
- package/dist/mwl/report.js +2 -2
- package/dist/mwl/runtime.d.ts +30 -1
- package/dist/mwl/runtime.d.ts.map +1 -1
- package/dist/mwl/runtime.js +204 -209
- package/dist/mwl/runtime.js.map +1 -1
- package/dist/mwl/schema.d.ts +1 -1
- package/dist/mwl/schema.js +1 -1
- package/dist/two-d/Game.d.ts +10 -4
- package/dist/two-d/Game.d.ts.map +1 -1
- package/dist/two-d/Game.js.map +1 -1
- package/dist/two-d/render/Types2D.d.ts +6 -4
- package/dist/two-d/render/Types2D.d.ts.map +1 -1
- package/dist/two-d/render/Types2D.js +6 -4
- package/dist/two-d/render/Types2D.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +5 -2
package/dist/mwl/compiler.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface MwlEmitOptions {
|
|
|
35
35
|
* ```ts
|
|
36
36
|
* import { compile } from '@datamoc/mw_games/mwl';
|
|
37
37
|
*
|
|
38
|
-
* const game = compile('
|
|
38
|
+
* const game = compile("[{ tag: 'game', schema: 0.1 }]");
|
|
39
39
|
* console.log(game.schema); // '0.1'
|
|
40
40
|
* ```
|
|
41
41
|
*/
|
|
@@ -51,7 +51,7 @@ export interface MwlSourceFile {
|
|
|
51
51
|
* ```ts
|
|
52
52
|
* import { compileSources } from '@datamoc/mw_games/mwl';
|
|
53
53
|
*
|
|
54
|
-
* const game = compileSources([{ file: 'a.mwl', source: '
|
|
54
|
+
* const game = compileSources([{ file: 'a.mwl', source: "[{ tag: 'game', schema: 0.1 }]" }]);
|
|
55
55
|
* console.log(game.roots.length); // 1
|
|
56
56
|
* ```
|
|
57
57
|
*/
|
|
@@ -65,7 +65,7 @@ export declare function compileSources(files: readonly MwlSourceFile[], options?
|
|
|
65
65
|
* ```ts
|
|
66
66
|
* import { compileNodes, parse } from '@datamoc/mw_games/mwl';
|
|
67
67
|
*
|
|
68
|
-
* const game = compileNodes(parse('
|
|
68
|
+
* const game = compileNodes(parse("[{ tag: 'game', schema: 0.1 }]"));
|
|
69
69
|
* console.log(game.schema); // '0.1'
|
|
70
70
|
* ```
|
|
71
71
|
*/
|
|
@@ -89,7 +89,7 @@ export interface MwlCatalog {
|
|
|
89
89
|
* ```ts
|
|
90
90
|
* import { compile, extractCatalog } from '@datamoc/mw_games/mwl';
|
|
91
91
|
*
|
|
92
|
-
* const game = compile('
|
|
92
|
+
* const game = compile("[{ tag: 'game', schema: 0.1 }]");
|
|
93
93
|
* console.log(extractCatalog(game, { locale: 'fr' }).locale); // 'fr'
|
|
94
94
|
* ```
|
|
95
95
|
*/
|
|
@@ -101,7 +101,7 @@ export declare function extractCatalog(game: MwlCompiledGame, options?: MwlCatal
|
|
|
101
101
|
* ```ts
|
|
102
102
|
* import { compile, emitModule } from '@datamoc/mw_games/mwl';
|
|
103
103
|
*
|
|
104
|
-
* console.log(emitModule(compile('
|
|
104
|
+
* console.log(emitModule(compile("[{ tag: 'game', schema: 0.1 }]")).startsWith('export const gameData')); // true
|
|
105
105
|
* ```
|
|
106
106
|
*/
|
|
107
107
|
export declare function emitModule(game: MwlCompiledGame, variable?: string): string;
|
|
@@ -112,7 +112,7 @@ export declare function emitModule(game: MwlCompiledGame, variable?: string): st
|
|
|
112
112
|
* ```ts
|
|
113
113
|
* import { compile, emitArtifacts } from '@datamoc/mw_games/mwl';
|
|
114
114
|
*
|
|
115
|
-
* const artifacts = emitArtifacts(compile('
|
|
115
|
+
* const artifacts = emitArtifacts(compile("[{ tag: 'game', schema: 0.1 }]"));
|
|
116
116
|
* console.log(artifacts.map((artifact) => artifact.name)); // ['game-data.ts', 'i18n.json', 'assets.json']
|
|
117
117
|
* ```
|
|
118
118
|
*/
|
|
@@ -124,7 +124,7 @@ export declare function emitArtifacts(game: MwlCompiledGame, options?: MwlEmitOp
|
|
|
124
124
|
* ```ts
|
|
125
125
|
* import { compileAndEmitSources } from '@datamoc/mw_games/mwl';
|
|
126
126
|
*
|
|
127
|
-
* const artifacts = compileAndEmitSources([{ file: 'a.mwl', source: '
|
|
127
|
+
* const artifacts = compileAndEmitSources([{ file: 'a.mwl', source: "[{ tag: 'game', schema: 0.1 }]" }]);
|
|
128
128
|
* console.log(artifacts.length); // 3
|
|
129
129
|
* ```
|
|
130
130
|
*/
|
package/dist/mwl/compiler.js
CHANGED
|
@@ -7,7 +7,7 @@ import { validate } from './schema.js';
|
|
|
7
7
|
* ```ts
|
|
8
8
|
* import { compile } from '@datamoc/mw_games/mwl';
|
|
9
9
|
*
|
|
10
|
-
* const game = compile('
|
|
10
|
+
* const game = compile("[{ tag: 'game', schema: 0.1 }]");
|
|
11
11
|
* console.log(game.schema); // '0.1'
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
@@ -22,7 +22,7 @@ export function compile(source, options = {}) {
|
|
|
22
22
|
* ```ts
|
|
23
23
|
* import { compileSources } from '@datamoc/mw_games/mwl';
|
|
24
24
|
*
|
|
25
|
-
* const game = compileSources([{ file: 'a.mwl', source: '
|
|
25
|
+
* const game = compileSources([{ file: 'a.mwl', source: "[{ tag: 'game', schema: 0.1 }]" }]);
|
|
26
26
|
* console.log(game.roots.length); // 1
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
@@ -43,7 +43,7 @@ export function compileSources(files, options = {}) {
|
|
|
43
43
|
* ```ts
|
|
44
44
|
* import { compileNodes, parse } from '@datamoc/mw_games/mwl';
|
|
45
45
|
*
|
|
46
|
-
* const game = compileNodes(parse('
|
|
46
|
+
* const game = compileNodes(parse("[{ tag: 'game', schema: 0.1 }]"));
|
|
47
47
|
* console.log(game.schema); // '0.1'
|
|
48
48
|
* ```
|
|
49
49
|
*/
|
|
@@ -140,7 +140,7 @@ function stripAssetTransform(value) {
|
|
|
140
140
|
* ```ts
|
|
141
141
|
* import { compile, extractCatalog } from '@datamoc/mw_games/mwl';
|
|
142
142
|
*
|
|
143
|
-
* const game = compile('
|
|
143
|
+
* const game = compile("[{ tag: 'game', schema: 0.1 }]");
|
|
144
144
|
* console.log(extractCatalog(game, { locale: 'fr' }).locale); // 'fr'
|
|
145
145
|
* ```
|
|
146
146
|
*/
|
|
@@ -157,7 +157,7 @@ export function extractCatalog(game, options = {}) {
|
|
|
157
157
|
* ```ts
|
|
158
158
|
* import { compile, emitModule } from '@datamoc/mw_games/mwl';
|
|
159
159
|
*
|
|
160
|
-
* console.log(emitModule(compile('
|
|
160
|
+
* console.log(emitModule(compile("[{ tag: 'game', schema: 0.1 }]")).startsWith('export const gameData')); // true
|
|
161
161
|
* ```
|
|
162
162
|
*/
|
|
163
163
|
export function emitModule(game, variable = 'gameData') {
|
|
@@ -170,7 +170,7 @@ export function emitModule(game, variable = 'gameData') {
|
|
|
170
170
|
* ```ts
|
|
171
171
|
* import { compile, emitArtifacts } from '@datamoc/mw_games/mwl';
|
|
172
172
|
*
|
|
173
|
-
* const artifacts = emitArtifacts(compile('
|
|
173
|
+
* const artifacts = emitArtifacts(compile("[{ tag: 'game', schema: 0.1 }]"));
|
|
174
174
|
* console.log(artifacts.map((artifact) => artifact.name)); // ['game-data.ts', 'i18n.json', 'assets.json']
|
|
175
175
|
* ```
|
|
176
176
|
*/
|
|
@@ -194,7 +194,7 @@ export function emitArtifacts(game, options = {}) {
|
|
|
194
194
|
* ```ts
|
|
195
195
|
* import { compileAndEmitSources } from '@datamoc/mw_games/mwl';
|
|
196
196
|
*
|
|
197
|
-
* const artifacts = compileAndEmitSources([{ file: 'a.mwl', source: '
|
|
197
|
+
* const artifacts = compileAndEmitSources([{ file: 'a.mwl', source: "[{ tag: 'game', schema: 0.1 }]" }]);
|
|
198
198
|
* console.log(artifacts.length); // 3
|
|
199
199
|
* ```
|
|
200
200
|
*/
|
package/dist/mwl/content.d.ts
CHANGED
|
@@ -133,7 +133,7 @@ export interface MwlContentCatalog {
|
|
|
133
133
|
* ```ts
|
|
134
134
|
* import { compile, contentCatalog } from '@datamoc/mw_games/mwl';
|
|
135
135
|
*
|
|
136
|
-
* console.log(contentCatalog(compile('
|
|
136
|
+
* console.log(contentCatalog(compile("[{ tag: 'game', schema: 0.1 }]")).campaigns); // []
|
|
137
137
|
* ```
|
|
138
138
|
*/
|
|
139
139
|
export declare function contentCatalog(game: MwlCompiledGame): MwlContentCatalog;
|
package/dist/mwl/content.js
CHANGED
|
@@ -13,7 +13,7 @@ const enumValue = enumAttribute;
|
|
|
13
13
|
* ```ts
|
|
14
14
|
* import { compile, contentCatalog } from '@datamoc/mw_games/mwl';
|
|
15
15
|
*
|
|
16
|
-
* console.log(contentCatalog(compile('
|
|
16
|
+
* console.log(contentCatalog(compile("[{ tag: 'game', schema: 0.1 }]")).campaigns); // []
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
19
|
export function contentCatalog(game) {
|
package/dist/mwl/grammar.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface MwlNode {
|
|
|
9
9
|
readonly children: readonly MwlNode[];
|
|
10
10
|
readonly location: MwlLocation;
|
|
11
11
|
/**
|
|
12
|
-
* Names of the attributes that carried the gettext marker `_
|
|
12
|
+
* Names of the attributes that carried the gettext marker `_("...")`.
|
|
13
13
|
* Present (possibly empty) on nodes parsed from text, so the compiler can
|
|
14
14
|
* extract exactly the author-marked display strings; absent on nodes built
|
|
15
15
|
* programmatically by a converter.
|
|
@@ -30,7 +30,7 @@ export interface MwlDiagnostic {
|
|
|
30
30
|
* import { parse, MwlSyntaxError } from '@datamoc/mw_games/mwl';
|
|
31
31
|
*
|
|
32
32
|
* try {
|
|
33
|
-
* parse('[game', 'broken.mwl');
|
|
33
|
+
* parse('[{ tag: "game" }]', 'broken.mwl');
|
|
34
34
|
* } catch (error) {
|
|
35
35
|
* if (error instanceof MwlSyntaxError) console.log(error.diagnostic.code);
|
|
36
36
|
* }
|
|
@@ -46,36 +46,46 @@ export interface MwlPreprocessOptions {
|
|
|
46
46
|
readonly includes?: Readonly<Record<string, string>>;
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
|
-
* Expands MWL macros, `#ifdef` blocks and `#include`s into flat source text
|
|
49
|
+
* Expands MWL macros, `#ifdef` blocks and `#include`s into flat source text,
|
|
50
|
+
* then rewrites the `_("...")` gettext marker into a form the JSON5 parser
|
|
51
|
+
* accepts. The marker keeps working because this pass already runs before
|
|
52
|
+
* structural parsing, the same reason macros survive a change of grammar.
|
|
50
53
|
*
|
|
51
54
|
* @example
|
|
52
55
|
* ```ts
|
|
53
56
|
* import { preprocess } from '@datamoc/mw_games/mwl';
|
|
54
57
|
*
|
|
55
|
-
* console.log(preprocess('
|
|
58
|
+
* console.log(preprocess("[{ tag: 'game', schema: 0.1 }]"));
|
|
56
59
|
* ```
|
|
57
60
|
*/
|
|
58
61
|
export declare function preprocess(source: string, options?: MwlPreprocessOptions): string;
|
|
62
|
+
/**
|
|
63
|
+
* Parses MWL source (a top-level array of `{ tag, ...attrs, children }` objects)
|
|
64
|
+
* into nodes. Numbers and booleans become their string form on the node, so the
|
|
65
|
+
* schema, compiler, and runtime see exactly the strings they always have; the
|
|
66
|
+
* JSON5 literal only decides what an author types and what a JSON5-aware tool
|
|
67
|
+
* can check before validation runs.
|
|
68
|
+
*/
|
|
59
69
|
export declare function parse(source: string, file?: string): MwlNode[];
|
|
60
70
|
/**
|
|
61
|
-
* Unquotes one raw attribute value and strips the `_
|
|
71
|
+
* Unquotes one raw attribute value and strips the `_("...")` gettext marker.
|
|
62
72
|
*
|
|
63
73
|
* @example
|
|
64
74
|
* ```ts
|
|
65
75
|
* import { parseValue } from '@datamoc/mw_games/mwl';
|
|
66
76
|
*
|
|
67
|
-
* console.log(parseValue('_
|
|
77
|
+
* console.log(parseValue('_("Hold the line")', { file: 'a.mwl', line: 1, column: 1 })); // 'Hold the line'
|
|
68
78
|
* ```
|
|
69
79
|
*/
|
|
70
80
|
export declare function parseValue(raw: string, location: MwlLocation, lineText?: string): string;
|
|
71
81
|
/**
|
|
72
|
-
* True when a raw attribute value carries the gettext marker `_
|
|
82
|
+
* True when a raw attribute value carries the gettext marker `_("...")`.
|
|
73
83
|
*
|
|
74
84
|
* @example
|
|
75
85
|
* ```ts
|
|
76
86
|
* import { isGettext } from '@datamoc/mw_games/mwl';
|
|
77
87
|
*
|
|
78
|
-
* console.log(isGettext('_
|
|
88
|
+
* console.log(isGettext('_("Hello")')); // true
|
|
79
89
|
* console.log(isGettext('Hello')); // false
|
|
80
90
|
* ```
|
|
81
91
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/mwl/grammar.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/mwl/grammar.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACxC,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;IAEnC,YAAY,UAAU,EAAE,aAAa,EAMpC;CACD;AAED,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAUD;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CA0ErF;AA6ED;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,SAAU,GAAG,OAAO,EAAE,CA2B/D;AAsID;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,SAAM,GAAG,MAAM,CAgBrF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9C"}
|
package/dist/mwl/grammar.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import JSON5 from 'json5';
|
|
1
2
|
/**
|
|
2
3
|
* The error a malformed MWL document throws, carrying the diagnostic that describes it.
|
|
3
4
|
*
|
|
@@ -6,7 +7,7 @@
|
|
|
6
7
|
* import { parse, MwlSyntaxError } from '@datamoc/mw_games/mwl';
|
|
7
8
|
*
|
|
8
9
|
* try {
|
|
9
|
-
* parse('[game', 'broken.mwl');
|
|
10
|
+
* parse('[{ tag: "game" }]', 'broken.mwl');
|
|
10
11
|
* } catch (error) {
|
|
11
12
|
* if (error instanceof MwlSyntaxError) console.log(error.diagnostic.code);
|
|
12
13
|
* }
|
|
@@ -21,13 +22,16 @@ export class MwlSyntaxError extends Error {
|
|
|
21
22
|
}
|
|
22
23
|
const directiveNames = new Set(['define', 'enddef', 'arg', 'ifdef', 'ifndef', 'else', 'endif', 'include']);
|
|
23
24
|
/**
|
|
24
|
-
* Expands MWL macros, `#ifdef` blocks and `#include`s into flat source text
|
|
25
|
+
* Expands MWL macros, `#ifdef` blocks and `#include`s into flat source text,
|
|
26
|
+
* then rewrites the `_("...")` gettext marker into a form the JSON5 parser
|
|
27
|
+
* accepts. The marker keeps working because this pass already runs before
|
|
28
|
+
* structural parsing, the same reason macros survive a change of grammar.
|
|
25
29
|
*
|
|
26
30
|
* @example
|
|
27
31
|
* ```ts
|
|
28
32
|
* import { preprocess } from '@datamoc/mw_games/mwl';
|
|
29
33
|
*
|
|
30
|
-
* console.log(preprocess('
|
|
34
|
+
* console.log(preprocess("[{ tag: 'game', schema: 0.1 }]"));
|
|
31
35
|
* ```
|
|
32
36
|
*/
|
|
33
37
|
export function preprocess(source, options = {}) {
|
|
@@ -104,7 +108,7 @@ export function preprocess(source, options = {}) {
|
|
|
104
108
|
throw syntax('MWL_ENDDEF', `macro ${macro.name} is missing #enddef`, options.file, lines.length - 1, lines.at(-1) ?? '');
|
|
105
109
|
if (active.length !== 1)
|
|
106
110
|
throw syntax('MWL_ENDIF', 'conditional is missing #endif', options.file, lines.length - 1, lines.at(-1) ?? '');
|
|
107
|
-
return expandMacros(output.join('\n'), macros, options);
|
|
111
|
+
return rewriteGettextMarkers(expandMacros(output.join('\n'), macros, options));
|
|
108
112
|
}
|
|
109
113
|
function expandMacros(source, macros, options) {
|
|
110
114
|
return source.replace(/\{([^{}]*)\}/g, (whole, inner) => {
|
|
@@ -122,134 +126,265 @@ function expandMacros(source, macros, options) {
|
|
|
122
126
|
return expandMacros(body, macros, options);
|
|
123
127
|
});
|
|
124
128
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Rewrites `_("text")` to `{"$gettext": "text"}` before the JSON5 parser runs.
|
|
131
|
+
* The scan skips string literals and `//`/`block` comments, so prose that merely
|
|
132
|
+
* mentions the marker (a dialogue line about the syntax itself) survives intact.
|
|
133
|
+
* An `_("...")` the rewrite does not match, such as one with an unquoted string,
|
|
134
|
+
* is left for the JSON5 parser to reject with its own line and column.
|
|
135
|
+
*/
|
|
136
|
+
function rewriteGettextMarkers(source) {
|
|
137
|
+
const marker = /^_\(\s*("(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*')\s*\)/;
|
|
138
|
+
let out = '';
|
|
139
|
+
let index = 0;
|
|
140
|
+
while (index < source.length) {
|
|
141
|
+
const character = source[index];
|
|
142
|
+
const comment = source[index + 1];
|
|
143
|
+
if (character === '/' && (comment === '/' || comment === '*')) {
|
|
144
|
+
const end = comment === '/' ? source.indexOf('\n', index) : source.indexOf('*/', index + 2);
|
|
145
|
+
const stop = end < 0 ? source.length : comment === '/' ? end : end + 2;
|
|
146
|
+
out += source.slice(index, stop);
|
|
147
|
+
index = stop;
|
|
143
148
|
continue;
|
|
144
149
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
throw new MwlSyntaxError({
|
|
150
|
-
code: 'MWL_TAG',
|
|
151
|
-
message: `mismatched closing tag ${closing[1]}`,
|
|
152
|
-
location,
|
|
153
|
-
lineText: raw,
|
|
154
|
-
});
|
|
150
|
+
if (character === '"' || character === "'") {
|
|
151
|
+
const stop = copyJson5String(source, index);
|
|
152
|
+
out += source.slice(index, stop);
|
|
153
|
+
index = stop;
|
|
155
154
|
continue;
|
|
156
155
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
node.attributes[attribute[1]] = block.text;
|
|
163
|
-
if (block.gettext)
|
|
164
|
-
node.gettext.push(attribute[1]);
|
|
165
|
-
lineIndex += block.extraLines;
|
|
156
|
+
if (character === '_') {
|
|
157
|
+
const match = marker.exec(source.slice(index));
|
|
158
|
+
if (match) {
|
|
159
|
+
out += `{"$gettext": ${match[1]}}`;
|
|
160
|
+
index += match[0].length;
|
|
166
161
|
continue;
|
|
167
162
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
163
|
+
}
|
|
164
|
+
out += character;
|
|
165
|
+
index++;
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
/** Copies one JSON5 string literal verbatim, escapes included, so the rewrite never edits inside it. */
|
|
170
|
+
function copyJson5String(source, from) {
|
|
171
|
+
const quote = source[from];
|
|
172
|
+
let index = from + 1;
|
|
173
|
+
while (index < source.length) {
|
|
174
|
+
const character = source[index];
|
|
175
|
+
if (character === '\\') {
|
|
176
|
+
index += 2;
|
|
171
177
|
continue;
|
|
172
178
|
}
|
|
179
|
+
if (character === quote)
|
|
180
|
+
return index + 1;
|
|
181
|
+
// A raw newline ends the literal here; the JSON5 parser reports it.
|
|
182
|
+
if (character === '\n')
|
|
183
|
+
return index;
|
|
184
|
+
index++;
|
|
185
|
+
}
|
|
186
|
+
return index;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Parses MWL source (a top-level array of `{ tag, ...attrs, children }` objects)
|
|
190
|
+
* into nodes. Numbers and booleans become their string form on the node, so the
|
|
191
|
+
* schema, compiler, and runtime see exactly the strings they always have; the
|
|
192
|
+
* JSON5 literal only decides what an author types and what a JSON5-aware tool
|
|
193
|
+
* can check before validation runs.
|
|
194
|
+
*/
|
|
195
|
+
export function parse(source, file = '<mwl>') {
|
|
196
|
+
// The marker rewrite is idempotent, so parsing already-preprocessed source
|
|
197
|
+
// (the `compile` path) is a no-op while a direct `parse` of authored source
|
|
198
|
+
// keeps working, the way the old tag parser read `_ "..."` inline.
|
|
199
|
+
const rewritten = rewriteGettextMarkers(source);
|
|
200
|
+
if (rewritten.trim() === '')
|
|
201
|
+
return [];
|
|
202
|
+
let value;
|
|
203
|
+
try {
|
|
204
|
+
value = JSON5.parse(rewritten);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
throw json5SyntaxError(error, rewritten, file);
|
|
208
|
+
}
|
|
209
|
+
if (value === undefined)
|
|
210
|
+
return [];
|
|
211
|
+
if (!Array.isArray(value))
|
|
212
|
+
throw new MwlSyntaxError({
|
|
213
|
+
code: 'MWL_DOCUMENT',
|
|
214
|
+
message: 'MWL document must be a top-level array of nodes',
|
|
215
|
+
location: { file, line: 1, column: 1 },
|
|
216
|
+
lineText: rewritten.split(/\r?\n/)[0] ?? '',
|
|
217
|
+
});
|
|
218
|
+
const context = {
|
|
219
|
+
file,
|
|
220
|
+
sites: collectTagSites(rewritten, file),
|
|
221
|
+
cursor: { index: 0 },
|
|
222
|
+
parent: undefined,
|
|
223
|
+
};
|
|
224
|
+
return value.map((entry) => toNode(entry, context));
|
|
225
|
+
}
|
|
226
|
+
function toNode(entry, context) {
|
|
227
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry))
|
|
173
228
|
throw new MwlSyntaxError({
|
|
174
|
-
code: '
|
|
175
|
-
message: '
|
|
229
|
+
code: 'MWL_TAG',
|
|
230
|
+
message: 'MWL node must be an object with a tag',
|
|
231
|
+
location: context.parent ?? { file: context.file, line: 1, column: 1 },
|
|
232
|
+
});
|
|
233
|
+
const record = entry;
|
|
234
|
+
if (typeof record.tag !== 'string' || !namePattern.test(record.tag))
|
|
235
|
+
throw new MwlSyntaxError({
|
|
236
|
+
code: 'MWL_TAG',
|
|
237
|
+
message: 'MWL node needs a tag name',
|
|
238
|
+
location: context.parent ?? { file: context.file, line: 1, column: 1 },
|
|
239
|
+
});
|
|
240
|
+
const tag = record.tag;
|
|
241
|
+
const location = locate(context, tag);
|
|
242
|
+
const rawChildren = record.children ?? [];
|
|
243
|
+
if (!Array.isArray(rawChildren))
|
|
244
|
+
throw new MwlSyntaxError({
|
|
245
|
+
code: 'MWL_TAG',
|
|
246
|
+
message: `children of [${tag}] must be an array`,
|
|
176
247
|
location,
|
|
177
|
-
lineText: raw,
|
|
178
248
|
});
|
|
249
|
+
const attributes = {};
|
|
250
|
+
const gettext = [];
|
|
251
|
+
for (const [name, raw] of Object.entries(record)) {
|
|
252
|
+
if (name === 'tag' || name === 'children')
|
|
253
|
+
continue;
|
|
254
|
+
attributes[name] = toAttribute(tag, name, raw, location, gettext);
|
|
179
255
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
256
|
+
const childContext = { ...context, parent: location };
|
|
257
|
+
return {
|
|
258
|
+
tag,
|
|
259
|
+
attributes,
|
|
260
|
+
children: rawChildren.map((child) => toNode(child, childContext)),
|
|
261
|
+
location,
|
|
262
|
+
gettext,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function toAttribute(tag, name, raw, location, gettext) {
|
|
266
|
+
if (!namePattern.test(name) || name.startsWith('$') || name.startsWith('_'))
|
|
267
|
+
throw new MwlSyntaxError({
|
|
268
|
+
code: 'MWL_ATTRIBUTE',
|
|
269
|
+
message: `unknown attribute "${name}" on [${tag}]`,
|
|
270
|
+
location,
|
|
271
|
+
});
|
|
272
|
+
if (typeof raw === 'string')
|
|
273
|
+
return raw;
|
|
274
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
275
|
+
return String(raw);
|
|
276
|
+
if (typeof raw === 'boolean')
|
|
277
|
+
return raw ? 'true' : 'false';
|
|
278
|
+
if (typeof raw === 'object' && raw !== null && !Array.isArray(raw)) {
|
|
279
|
+
const keys = Object.keys(raw);
|
|
280
|
+
const text = raw.$gettext;
|
|
281
|
+
if (keys.length === 1 && keys[0] === '$gettext' && typeof text === 'string') {
|
|
282
|
+
gettext.push(name);
|
|
283
|
+
return text;
|
|
284
|
+
}
|
|
183
285
|
}
|
|
184
|
-
|
|
286
|
+
throw new MwlSyntaxError({
|
|
287
|
+
code: 'MWL_ATTRIBUTE',
|
|
288
|
+
message: `attribute "${name}" on [${tag}] must be a string, number, boolean, or _("...")`,
|
|
289
|
+
location,
|
|
290
|
+
});
|
|
185
291
|
}
|
|
292
|
+
const namePattern = /^[A-Za-z_][\w-]*$/;
|
|
186
293
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* `
|
|
294
|
+
* Every `tag: 'name'` occurrence in source order. Pre-order node traversal visits
|
|
295
|
+
* tags in that same order (a parent literal opens before its children), so `locate`
|
|
296
|
+
* can zip the two sequences and recover a useful line and column per node without
|
|
297
|
+
* a schema-aware parser. A `tag: '...'` string inside prose can shift a location,
|
|
298
|
+
* never a value, which is why this stays best-effort rather than load-bearing.
|
|
191
299
|
*/
|
|
192
|
-
function
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
let index = from;
|
|
199
|
-
let end = text.indexOf('"""');
|
|
200
|
-
while (end < 0) {
|
|
201
|
-
index++;
|
|
202
|
-
if (index >= lines.length)
|
|
203
|
-
throw new MwlSyntaxError({ code: 'MWL_STRING', message: 'unterminated """ value', location });
|
|
204
|
-
text += `\n${lines[index]}`;
|
|
205
|
-
end = text.indexOf('"""');
|
|
300
|
+
function collectTagSites(source, file) {
|
|
301
|
+
const sites = [];
|
|
302
|
+
const pattern = /\btag\s*:\s*(['"])([A-Za-z_][\w-]*)\1/g;
|
|
303
|
+
let match;
|
|
304
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
305
|
+
sites.push({ tag: match[2], location: offsetToLocation(source, file, match.index) });
|
|
206
306
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
307
|
+
return sites;
|
|
308
|
+
}
|
|
309
|
+
function locate(context, tag) {
|
|
310
|
+
for (let index = context.cursor.index; index < context.sites.length; index++) {
|
|
311
|
+
if (context.sites[index].tag === tag) {
|
|
312
|
+
context.cursor.index = index + 1;
|
|
313
|
+
return context.sites[index].location;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return context.parent ?? { file: context.file, line: 1, column: 1 };
|
|
317
|
+
}
|
|
318
|
+
function offsetToLocation(source, file, offset) {
|
|
319
|
+
let line = 1;
|
|
320
|
+
let column = 1;
|
|
321
|
+
for (let index = 0; index < offset; index++) {
|
|
322
|
+
if (source[index] === '\n') {
|
|
323
|
+
line++;
|
|
324
|
+
column = 1;
|
|
325
|
+
}
|
|
326
|
+
else
|
|
327
|
+
column++;
|
|
328
|
+
}
|
|
329
|
+
return { file, line, column };
|
|
330
|
+
}
|
|
331
|
+
function json5SyntaxError(error, source, file) {
|
|
332
|
+
const lines = source.split(/\r?\n/);
|
|
333
|
+
const detail = error;
|
|
334
|
+
const line = typeof detail?.lineNumber === 'number' ? detail.lineNumber : 1;
|
|
335
|
+
const column = typeof detail?.columnNumber === 'number' ? detail.columnNumber : 1;
|
|
336
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
337
|
+
if (message.includes("'_'"))
|
|
338
|
+
message += ' (write translatable text as _("..."))';
|
|
339
|
+
return new MwlSyntaxError({
|
|
340
|
+
code: 'MWL_SYNTAX',
|
|
341
|
+
message,
|
|
342
|
+
location: { file, line: Math.max(1, line), column: Math.max(1, column) },
|
|
343
|
+
lineText: lines[Math.max(1, line) - 1],
|
|
344
|
+
});
|
|
214
345
|
}
|
|
215
346
|
/**
|
|
216
|
-
* Unquotes one raw attribute value and strips the `_
|
|
347
|
+
* Unquotes one raw attribute value and strips the `_("...")` gettext marker.
|
|
217
348
|
*
|
|
218
349
|
* @example
|
|
219
350
|
* ```ts
|
|
220
351
|
* import { parseValue } from '@datamoc/mw_games/mwl';
|
|
221
352
|
*
|
|
222
|
-
* console.log(parseValue('_
|
|
353
|
+
* console.log(parseValue('_("Hold the line")', { file: 'a.mwl', line: 1, column: 1 })); // 'Hold the line'
|
|
223
354
|
* ```
|
|
224
355
|
*/
|
|
225
356
|
export function parseValue(raw, location, lineText = raw) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
357
|
+
const value = raw.trim();
|
|
358
|
+
const marker = /^_\(\s*([\s\S]*?)\s*\)$/.exec(value);
|
|
359
|
+
const literal = marker ? marker[1] : value;
|
|
360
|
+
if (literal.startsWith('"') || literal.startsWith("'")) {
|
|
361
|
+
try {
|
|
362
|
+
const parsed = JSON5.parse(literal);
|
|
363
|
+
if (typeof parsed === 'string')
|
|
364
|
+
return parsed;
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Fall through to the syntax error below.
|
|
368
|
+
}
|
|
369
|
+
throw new MwlSyntaxError({ code: 'MWL_STRING', message: 'unterminated string value', location, lineText });
|
|
237
370
|
}
|
|
371
|
+
if (marker)
|
|
372
|
+
throw new MwlSyntaxError({ code: 'MWL_STRING', message: '_("...") needs a quoted string', location, lineText });
|
|
238
373
|
return value;
|
|
239
374
|
}
|
|
240
375
|
/**
|
|
241
|
-
* True when a raw attribute value carries the gettext marker `_
|
|
376
|
+
* True when a raw attribute value carries the gettext marker `_("...")`.
|
|
242
377
|
*
|
|
243
378
|
* @example
|
|
244
379
|
* ```ts
|
|
245
380
|
* import { isGettext } from '@datamoc/mw_games/mwl';
|
|
246
381
|
*
|
|
247
|
-
* console.log(isGettext('_
|
|
382
|
+
* console.log(isGettext('_("Hello")')); // true
|
|
248
383
|
* console.log(isGettext('Hello')); // false
|
|
249
384
|
* ```
|
|
250
385
|
*/
|
|
251
386
|
export function isGettext(raw) {
|
|
252
|
-
return /^_\s*"/.test(raw.trim());
|
|
387
|
+
return /^_\(\s*["']/.test(raw.trim());
|
|
253
388
|
}
|
|
254
389
|
function splitWords(value) {
|
|
255
390
|
const result = [];
|