Skip to content

Commit 2e95042

Browse files
committed
feat: use source map to map from lua errors to markdown position
1 parent 2e198db commit 2e95042

12 files changed

+353
-120
lines changed

mdc/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"remark-frontmatter": "^5.0.0",
4646
"remark-join-cjk-lines": "^1.0.9",
4747
"remark-parse": "^10.0.2",
48+
"source-map-js": "^1.2.0",
4849
"unified": "^10.1.2",
4950
"unist-util-visit-parents": "^5.1.3",
5051
"uuid": "^9.0.0",

mdc/src/ast-compiler.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,14 @@ class AstCompiler {
6060
constructor(root: LuaArray, vfile: VFile) {
6161
this.root = root;
6262
this.vfile = vfile;
63-
this.builder = new LuaTableGenerator('_');
63+
this.builder = new LuaTableGenerator('_', vfile.path);
6464
}
6565

6666
compile() {
6767
this.visitAll();
6868
this.vfile.data.gettext = collectGettextData(this.root, this.vfile);
69+
const sourceMap = this.builder.toSourceNode();
70+
this.vfile.data.sourceMap = sourceMap;
6971
return this.builder.toString();
7072
}
7173

@@ -102,14 +104,16 @@ class AstCompiler {
102104

103105
serializeLink(child: LuaLink) {
104106
this.builder
107+
.startTable(child.node.position)
108+
.pair('link')
105109
.startTable()
106-
.pair('link').startTable().raw(child.labels.map((label) => JSON.stringify(label)).join(','))
110+
.raw(child.labels.map((label) => JSON.stringify(label)).join(','))
107111
.endTable();
108112
if (child.coroutine) {
109113
this.builder.pair('coroutine').value(true);
110114
}
111115
if (child.params !== undefined) {
112-
this.builder.pair('params').raw(child.params === '' ? 'true' : `function()return${child.params}end`);
116+
this.builder.pair('params').raw(child.params === '' ? 'true' : `function()\nreturn ${child.params}\nend`);
113117
}
114118
if (child.root) {
115119
this.builder.pair('root').value(child.root);
@@ -119,9 +123,9 @@ class AstCompiler {
119123

120124
serializeText(child: LuaText) {
121125
if (!child.plural && isEmpty(child.tags) && isEmpty(child.values)) {
122-
this.builder.value(child.text);
126+
this.builder.value(child.text, child.node.position);
123127
} else {
124-
this.builder.startTable();
128+
this.builder.startTable(child.node.position);
125129
if (child.plural) {
126130
this.builder.pair('plural').value(child.plural);
127131
}
@@ -150,13 +154,13 @@ class AstCompiler {
150154
if (node.children.length !== 0) {
151155
this.builder.endTable();
152156
}
153-
this.builder.pair('func').raw(`function(args)${node.code.trim()}\nend}`);
157+
this.builder.pair('func').raw(`function(args)\n${node.code.trim()}\nend`).endTable();
154158
return;
155159
}
156160

157161
switch (node.type) {
158162
case 'array': {
159-
this.builder.startTable().startTable();
163+
this.builder.startTable(node.node.position).startTable();
160164
if (this.vfile.data.debug) {
161165
const positions = [serializePosition(node)];
162166
node.children.forEach((child) => positions.push(serializePosition(child)));
@@ -189,13 +193,13 @@ class AstCompiler {
189193
break;
190194
}
191195
case 'func':
192-
this.builder.startTable();
196+
this.builder.startTable(node.node.position);
193197
if (node.children.length !== 0) {
194198
this.builder.pair('args').startTable().startTable().endTable();
195199
}
196200
break;
197201
case 'if-else':
198-
this.builder.startTable().raw(`function()return(${node.condition})end`);
202+
this.builder.startTable(node.node.position).raw(`function()return(${node.condition})end`);
199203
break;
200204
default:
201205
throw new Error('unreachable');

mdc/src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ compiler.compileAll(filename, async (f) => {
3737
const gettextOutput: VFile = output.data.gettext as VFile;
3838
fs.writeFile(path.join(dir, gettextOutput.path), gettextOutput.value);
3939
Object
40-
.entries(output.data.input as { [f: string]: VFile })
40+
.entries(output.data.inputs as { [f: string]: VFile })
4141
.forEach(([f, vfile]) => displayWarnings(path.join(dir, `${f}.md`), vfile));
4242
});

mdc/src/index.ts

Lines changed: 113 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
import remarkFrontmatter from 'remark-frontmatter';
22
import remarkJoinCJKLines from 'remark-join-cjk-lines';
33
import remarkParse from 'remark-parse';
4+
import { SourceMapConsumer, SourceNode } from 'source-map-js';
45
import { Processor, unified } from 'unified';
56
import { Position } from 'unist';
67
import { VFile } from 'vfile';
78
import _wasmoon from 'wasmoon';
89

910
import { directiveForMarkdown, mdxForMarkdown } from '@brocatel/md';
1011

11-
import astCompiler, { serializeTableInner } from './ast-compiler';
12+
import astCompiler from './ast-compiler';
1213
import expandMacro from './expander';
1314
import remapLineNumbers from './line-remap';
1415
import transformAst from './transformer';
1516
import { LuaGettextData, compileGettextData } from './lgettext';
1617
import { StoryRunner } from './lua';
18+
import LuaTableGenerator from './lua-table';
19+
import { sourceNode } from './utils';
1720

1821
export {
19-
StoryRunner, type SelectLine, type TextLine, type StoryLine,
22+
StoryRunner,
23+
type SelectLine,
24+
type TextLine,
25+
type StoryLine,
2026
} from './lua';
2127

2228
const VERSION = 1;
@@ -48,29 +54,102 @@ function removeMdExt(name: string): string {
4854
return name;
4955
}
5056

57+
function packBundle(
58+
name: string,
59+
target: VFile,
60+
inputs: Record<string, VFile>,
61+
outputs: Record<string, VFile | null>,
62+
globalLua: SourceNode[],
63+
): [SourceNode, LuaGettextData[]] {
64+
const gettextData: LuaGettextData[] = [];
65+
const contents = new LuaTableGenerator();
66+
contents.startTable().pair('').startTable();
67+
if (target.data.IFID) {
68+
contents.pair('IFID').startTable();
69+
(target.data.IFID as string[]).forEach((u) => {
70+
contents.value(`UUID://${u}//`);
71+
});
72+
contents.endTable();
73+
}
74+
contents
75+
.pair('version')
76+
.value(VERSION)
77+
.pair('entry')
78+
.value(removeMdExt(name))
79+
.endTable();
80+
Object.entries(outputs).forEach(([file, v]) => {
81+
contents
82+
.pair(file);
83+
if (v?.data.sourceMap) {
84+
const source = v.data.sourceMap as SourceNode;
85+
source.setSourceContent(file, inputs[file].toString());
86+
contents.raw(source);
87+
} else {
88+
contents.raw(v?.toString() ?? 'nil');
89+
}
90+
if (v?.data.gettext) {
91+
gettextData.push(v.data.gettext as LuaGettextData);
92+
}
93+
});
94+
contents.endTable();
95+
const bundle = sourceNode(
96+
undefined,
97+
undefined,
98+
undefined,
99+
[
100+
globalLua.map((item) => [item, '\n']).flat(),
101+
['_={}\n', 'return ', contents.toSourceNode()],
102+
].flat(),
103+
);
104+
return [bundle, gettextData];
105+
}
106+
51107
interface InvalidLink {
52-
node: { root?: string, link: string[] };
108+
node: { root?: string; link: string[] };
53109
root: string;
54110
source?: string;
55111
}
56112

57-
export async function validateLinks(vfile: VFile) {
113+
export async function validate(vfile: VFile) {
114+
const inputs = vfile.data.inputs as Record<string, VFile>;
58115
const story = new StoryRunner();
59-
await story.loadStory(vfile.value.toString());
116+
try {
117+
await story.loadStory(vfile.value.toString());
118+
} catch (e) {
119+
const info = (e as Error).message.split('\n')[0];
120+
const match = /\[string "<input>"\]:(\d+): .*$/.exec(info);
121+
const sourceMap = vfile.data.sourceMap as SourceNode | undefined;
122+
if (match && sourceMap) {
123+
const line = Number(match[1]);
124+
const column = 1;
125+
const mapper = new SourceMapConsumer(
126+
JSON.parse(sourceMap.toStringWithSourceMap().map.toString()),
127+
);
128+
const position = mapper.originalPositionFor({ line, column });
129+
const file = inputs[removeMdExt(position.source)];
130+
(file ?? vfile).message('invalid lua code', { line: position.line, column: position.column + 1 });
131+
} else {
132+
vfile.message(`invalid lua code found: ${info}`);
133+
}
134+
return;
135+
}
60136
if (!story.L) {
61137
throw new Error('story not loaded');
62138
}
63-
const invalidLinks: InvalidLink[] | Record<string, InvalidLink> = story
64-
.L.doStringSync('return story:validate_links()');
65-
const inputs = vfile.data.input as { [f: string]: VFile };
66-
(Array.isArray(invalidLinks) ? invalidLinks : Object.values(invalidLinks)).forEach((l) => {
139+
const invalidLinks: InvalidLink[] | Record<string, InvalidLink> = story.L.doStringSync(
140+
'return story:validate_links()',
141+
);
142+
(Array.isArray(invalidLinks)
143+
? invalidLinks
144+
: Object.values(invalidLinks)
145+
).forEach((l) => {
67146
let position: Position | null = null;
68147
if (l.source) {
69148
const [line, column] = l.source.split(':');
70149
const point = { line: Number(line) - 1, column: Number(column) };
71150
position = { start: point, end: point };
72151
}
73-
inputs[l.root].message(
152+
inputs[l.root]?.message(
74153
`link target not found: ${l.node.root ?? ''}#${l.node.link.join('#')}`,
75154
position,
76155
);
@@ -114,9 +193,9 @@ export class BrocatelCompiler {
114193
const stem = removeMdExt(name);
115194
const target = new VFile({ path: `${stem}.lua` });
116195
const gettextTarget = new VFile({ path: `${stem}.pot` });
117-
const files: Record<string, VFile | null> = {};
118-
const input: Record<string, VFile> = {};
119-
const globalLua: string[] = [];
196+
const outputs: Record<string, VFile | null> = {};
197+
const inputs: Record<string, VFile> = {}; // processed files
198+
const globalLua: SourceNode[] = [];
120199

121200
const asyncCompile = async (filename: string) => {
122201
const task = removeMdExt(filename);
@@ -128,17 +207,17 @@ export class BrocatelCompiler {
128207
if (file.data.IFID && !target.data.IFID) {
129208
target.data.IFID = file.data.IFID;
130209
}
131-
files[task] = file;
132-
const processed = new VFile({ path: task });
133-
input[task] = processed;
210+
outputs[task] = file;
211+
const processed = new VFile({ path: filename });
212+
inputs[task] = processed;
134213
processed.messages.push(...file.messages);
135214
processed.value = content;
136215

137-
globalLua.push(...file.data.globalLua as string[]);
216+
globalLua.push(...(file.data.globalLua as SourceNode[]));
138217
const tasks: Promise<any>[] = [];
139218
(file.data.dependencies as Set<string>).forEach((f) => {
140-
if (typeof files[removeMdExt(f)] === 'undefined') {
141-
files[task] = null;
219+
if (typeof outputs[removeMdExt(f)] === 'undefined') {
220+
outputs[f] = null;
142221
tasks.push(asyncCompile(f));
143222
}
144223
});
@@ -147,33 +226,18 @@ export class BrocatelCompiler {
147226
};
148227
await asyncCompile(name);
149228

150-
const gettextData: LuaGettextData[] = [];
151-
const contents = serializeTableInner(files, (v) => {
152-
if (!v) {
153-
return 'nil';
154-
}
155-
if (v.data.gettext) {
156-
gettextData.push(v.data.gettext as LuaGettextData);
157-
}
158-
return v.toString();
159-
});
160-
const uuids = target.data.IFID ? `IFID={${
161-
(target.data.IFID as string[])
162-
.map((u) => JSON.stringify(`UUID://${u}//`))
163-
.join(',')
164-
}},` : '';
165-
target.value = `${globalLua.join('\n')}
166-
_={}
167-
return {[""]={\
168-
${uuids}\
169-
version=${VERSION},\
170-
entry=${JSON.stringify(removeMdExt(name))}\
171-
},${contents}}`;
172-
target.data.input = input;
229+
const [bundle, gettextData] = packBundle(name, target, inputs, outputs, globalLua);
230+
target.value = bundle.toString();
231+
target.data.sourceMap = bundle;
232+
target.data.inputs = inputs;
173233
gettextTarget.value = compileGettextData(gettextData);
174234
target.data.gettext = gettextTarget;
175-
await validateLinks(target);
176-
Object.values(input).forEach((v) => {
235+
try {
236+
await validate(target);
237+
} catch (e) {
238+
target.message(e as Error);
239+
}
240+
Object.values(inputs).forEach((v) => {
177241
target.messages.push(...v.messages);
178242
});
179243
return target;
@@ -228,7 +292,11 @@ entry=${JSON.stringify(removeMdExt(name))}\
228292
async compileToString(content: string): Promise<string> {
229293
const file = await this.compile(content);
230294
if (file.messages.length > 0) {
231-
throw new Error(`${file.message.length} compilation warning(s): \n${file.messages.join('\n')}`);
295+
throw new Error(
296+
`${file.message.length} compilation warning(s): \n${file.messages.join(
297+
'\n',
298+
)}`,
299+
);
232300
}
233301
return file.toString();
234302
}

0 commit comments

Comments
 (0)