1
1
import remarkFrontmatter from 'remark-frontmatter' ;
2
2
import remarkJoinCJKLines from 'remark-join-cjk-lines' ;
3
3
import remarkParse from 'remark-parse' ;
4
+ import { SourceMapConsumer , SourceNode } from 'source-map-js' ;
4
5
import { Processor , unified } from 'unified' ;
5
6
import { Position } from 'unist' ;
6
7
import { VFile } from 'vfile' ;
7
8
import _wasmoon from 'wasmoon' ;
8
9
9
10
import { directiveForMarkdown , mdxForMarkdown } from '@brocatel/md' ;
10
11
11
- import astCompiler , { serializeTableInner } from './ast-compiler' ;
12
+ import astCompiler from './ast-compiler' ;
12
13
import expandMacro from './expander' ;
13
14
import remapLineNumbers from './line-remap' ;
14
15
import transformAst from './transformer' ;
15
16
import { LuaGettextData , compileGettextData } from './lgettext' ;
16
17
import { StoryRunner } from './lua' ;
18
+ import LuaTableGenerator from './lua-table' ;
19
+ import { sourceNode } from './utils' ;
17
20
18
21
export {
19
- StoryRunner , type SelectLine , type TextLine , type StoryLine ,
22
+ StoryRunner ,
23
+ type SelectLine ,
24
+ type TextLine ,
25
+ type StoryLine ,
20
26
} from './lua' ;
21
27
22
28
const VERSION = 1 ;
@@ -48,29 +54,102 @@ function removeMdExt(name: string): string {
48
54
return name ;
49
55
}
50
56
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
+
51
107
interface InvalidLink {
52
- node : { root ?: string , link : string [ ] } ;
108
+ node : { root ?: string ; link : string [ ] } ;
53
109
root : string ;
54
110
source ?: string ;
55
111
}
56
112
57
- export async function validateLinks ( vfile : VFile ) {
113
+ export async function validate ( vfile : VFile ) {
114
+ const inputs = vfile . data . inputs as Record < string , VFile > ;
58
115
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 = / \[ s t r i n g " < i n p u t > " \] : ( \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
+ }
60
136
if ( ! story . L ) {
61
137
throw new Error ( 'story not loaded' ) ;
62
138
}
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 ) => {
67
146
let position : Position | null = null ;
68
147
if ( l . source ) {
69
148
const [ line , column ] = l . source . split ( ':' ) ;
70
149
const point = { line : Number ( line ) - 1 , column : Number ( column ) } ;
71
150
position = { start : point , end : point } ;
72
151
}
73
- inputs [ l . root ] . message (
152
+ inputs [ l . root ] ? .message (
74
153
`link target not found: ${ l . node . root ?? '' } #${ l . node . link . join ( '#' ) } ` ,
75
154
position ,
76
155
) ;
@@ -114,9 +193,9 @@ export class BrocatelCompiler {
114
193
const stem = removeMdExt ( name ) ;
115
194
const target = new VFile ( { path : `${ stem } .lua` } ) ;
116
195
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 [ ] = [ ] ;
120
199
121
200
const asyncCompile = async ( filename : string ) => {
122
201
const task = removeMdExt ( filename ) ;
@@ -128,17 +207,17 @@ export class BrocatelCompiler {
128
207
if ( file . data . IFID && ! target . data . IFID ) {
129
208
target . data . IFID = file . data . IFID ;
130
209
}
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 ;
134
213
processed . messages . push ( ...file . messages ) ;
135
214
processed . value = content ;
136
215
137
- globalLua . push ( ...file . data . globalLua as string [ ] ) ;
216
+ globalLua . push ( ...( file . data . globalLua as SourceNode [ ] ) ) ;
138
217
const tasks : Promise < any > [ ] = [ ] ;
139
218
( 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 ;
142
221
tasks . push ( asyncCompile ( f ) ) ;
143
222
}
144
223
} ) ;
@@ -147,33 +226,18 @@ export class BrocatelCompiler {
147
226
} ;
148
227
await asyncCompile ( name ) ;
149
228
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 ;
173
233
gettextTarget . value = compileGettextData ( gettextData ) ;
174
234
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 ) => {
177
241
target . messages . push ( ...v . messages ) ;
178
242
} ) ;
179
243
return target ;
@@ -228,7 +292,11 @@ entry=${JSON.stringify(removeMdExt(name))}\
228
292
async compileToString ( content : string ) : Promise < string > {
229
293
const file = await this . compile ( content ) ;
230
294
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
+ ) ;
232
300
}
233
301
return file . toString ( ) ;
234
302
}
0 commit comments