forked from tweecode/twee
-
Notifications
You must be signed in to change notification settings - Fork 4
/
twee2sam.py
386 lines (288 loc) · 12.8 KB
/
twee2sam.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
import argparse, sys, os, glob, re, shutil, io
import logging
from operator import itemgetter
scriptPath = os.path.realpath(os.path.dirname(sys.argv[0]))
sys.path.append(os.path.join(scriptPath, 'tw'))
sys.path.append(os.path.join(scriptPath, 'lib'))
from tiddlywiki import TiddlyWiki
from twparser import TwParser
import twexpression
__version__ = "0.8.0"
def main (argv):
parser = argparse.ArgumentParser(description="Convert twee source code into SAM source code")
parser.add_argument("-a", "--author", default="twee")
parser.add_argument("-m", "--merge", default="")
parser.add_argument("-p", "--plugins", nargs="*", default=[])
parser.add_argument("-r", "--rss", default="")
parser.add_argument("-t", "--target", default="jonah")
parser.add_argument("sources")
parser.add_argument("destination")
opts = parser.parse_args()
# construct a TW object
tw = TiddlyWiki(opts.author)
# read in a file to be merged
if opts.merge:
with io.open(opts.merge, encoding="utf-8-sig") as f:
tw.addHtml(f.read())
# read source files
sources = glob.glob(opts.sources)
if not sources:
logging.error('twee2sam: no source files specified\n')
sys.exit(2)
for source in sources:
with io.open(source, encoding="utf-8-sig") as f:
tw.addTwee(f.read())
src_dir = os.path.dirname(sources[0])
#
# Parse the file
#
twp = TwParser(tw)
#
# Number the passages
#
passage_indexes = {}
def process_passage_index(passage):
global next_seq
if not passage.title in passage_indexes:
passage_indexes[passage.title] = process_passage_index.next_seq
process_passage_index.next_seq += 1
process_passage_index.next_seq = 0
# 'Start' _must_ be the first script
if not 'Start' in twp.passages:
logging.error('twee2sam: "Start" passage not found.\n')
sys.exit(2)
process_passage_index(twp.passages['Start'])
for passage in twp.passages.values():
process_passage_index(passage)
#
# Generate the file list
#
passage_order = [psg for psg, idx in sorted(passage_indexes.items(), key=itemgetter(1))]
def name_to_identifier(s):
return re.sub(r'[^0-9A-Za-z]', '_', s)
def script_name(s):
return name_to_identifier(s) + '.twsam'
if not os.path.exists(opts.destination):
os.makedirs(opts.destination)
with io.open(os.path.join(opts.destination, 'Script.list.txt'), 'w', encoding="utf-8") as f_list:
for passage_name in passage_order:
passage = twp.passages[passage_name]
f_list.write(u"%s" % script_name(passage.title))
f_list.write(u'\n')
#
# Generate SAM scripts
#
# A is used as a temp var for menu selection
# B is used as a temp var for menu selection
# C and above are available
variables = VariableFactory(2)
image_list = []
music_list = []
for passage in twp.passages.values():
with io.open(os.path.join(opts.destination, script_name(passage.title)), 'w', encoding="utf-8") as script:
def check_print():
if check_print.pending:
script.write(u'!\n')
check_print.in_buffer = 0
check_print.pending = False
check_print.pending = False
check_print.in_buffer = 0
def warning(msg):
logging.warning("Warning on \'{0}\': {1}".format(passage.title, msg))
def out_string(msg):
MAX_LEN = 512
# go through the string and replace characters
msg = ''.join(map(lambda x: {'"': "'", '[': '{', ']':'}'}[x] if x in ('"','[','{') else x, msg))
msg_len = len(msg)
# Checks for buffer overflow
if check_print.in_buffer + msg_len > MAX_LEN - 1:
warning("The text exceeds the maximum buffer size; try to intersperse the text with some <<pause>> macros")
remaining = max(0, MAX_LEN - 1 - check_print.in_buffer)
msg = msg[:remaining]
script.write(u'"{0}"'.format(msg))
script.write(u'\n')
check_print.in_buffer += len(msg)
def out_set(cmd):
out_expr(cmd.expr)
script.write(u' ')
target = variables.set_var(cmd.target)
script.write(u"%s\n" % target)
def out_if(cmd):
out_expr(cmd.expr)
script.write(u'[\n')
process_command_list(cmd.children, True)
script.write(u' 0]\n')
def out_print(cmd):
# print a numeric qvariable
out_expr(cmd.expr)
script.write(u'"\#"')
def out_expr(expr):
def var_locator(name):
return variables.get_var(name).replace(':', '')
generated = twexpression.to_sam(expr, var_locator = var_locator)
script.write(u"%s" % generated)
def out_call(cmd):
call_target = None
for k in passage_indexes.keys():
if cmd.target == k:
call_target = passage_indexes[k]
if call_target:
script.write(u"%s" % call_target)
script.write(u'c\n')
def out_jump(cmd):
call_target = None
for k in passage_indexes.keys():
if cmd.target == k:
call_target = passage_indexes[k]
if call_target:
script.write(u"%s" % call_target)
script.write(u'j\n')
# Outputs all the text
links = []
def register_link(cmd, is_conditional):
temp_var = variables.new_temp_var() if is_conditional else None
links.append((cmd, temp_var))
if temp_var:
script.write(u'1%s' % variables.set_var(temp_var))
def process_command_list(commands, is_conditional=False):
for cmd in commands:
if cmd.kind == 'text':
text = cmd.text.strip()
if text:
out_string(cmd.text)
check_print.pending = True
elif cmd.kind == 'print':
out_print(cmd)
elif cmd.kind == 'image':
check_print()
if not cmd.path in image_list:
image_list.append(cmd.path)
script.write(u'{0}i\n'.format(image_list.index(cmd.path)))
elif cmd.kind == 'link':
register_link(cmd, is_conditional)
out_string(cmd.actual_label())
elif cmd.kind == 'list':
for lcmd in cmd.children:
if lcmd.kind == 'link':
register_link(lcmd, is_conditional)
elif cmd.kind == 'pause':
check_print.pending = True
check_print()
elif cmd.kind == 'set':
out_set(cmd)
elif cmd.kind == 'if':
out_if(cmd)
elif cmd.kind == 'call':
out_call(cmd)
elif cmd.kind == 'jump':
out_jump(cmd)
elif cmd.kind == 'return':
script.write(u'$\n')
elif cmd.kind == 'music':
if not cmd.path in music_list:
music_list.append(cmd.path)
script.write(u'{0}m\n'.format(music_list.index(cmd.path)))
elif cmd.kind == 'display':
try:
target = twp.passages[cmd.target]
except KeyError:
logging.error("Display macro target passage {0} not found!".format(cmd.target), file=sys.stderr)
return
process_command_list(target.commands)
process_command_list(passage.commands)
check_print()
# Builds the menu from the links
if links:
# Outputs the options separated by line breaks, max 28 chars per line
for link, temp_var in links:
if temp_var:
script.write(u'{0}['.format(variables.get_var(temp_var)))
out_string(link.actual_label()[:28] + '\n')
if temp_var:
script.write(u'0]\n')
script.write(u'?A.\n')
check_print.in_buffer = 0
# Outputs the menu destinations
script.write(u'0B.\n');
for link, temp_var in links:
if temp_var:
script.write(u'{0}['.format(variables.get_var(temp_var)))
if not link.target in passage_indexes:
# TODO: Create a better exception
raise BaseException('Link points to a nonexisting passage: "{0}"'.format(link.target))
script.write(u'A:B:=[{0}j]'.format(passage_indexes[link.target]))
script.write(u'B:1+B.\n')
if temp_var:
script.write(u'0]\n')
else:
# No links? Generates an infinite loop.
script.write(u'1[1]\n')
#
# Function to copy the files on a list and generate a list file
#
def copy_and_build_list(list_file_name, file_list, item_extension, item_suffix = '', empty_item = 'blank'):
with io.open(os.path.join(opts.destination, list_file_name), 'w', encoding="utf-8") as list_file:
for file_path in file_list:
item_name = name_to_identifier(os.path.splitext(os.path.basename(file_path))[0])
list_file.write("%s%s\n" % (item_name, item_suffix))
shutil.copyfile(os.path.join(src_dir, file_path), os.path.join(opts.destination, '%s.%s' % (item_name, item_extension)))
if not file_list:
list_file.write(u"%s%s\n" % (empty_item, item_suffix))
#
# Copy images and builds the image list
#
copy_and_build_list('Images.txt', image_list, 'png')
#
# Copy music and builds the music list
#
copy_and_build_list('Music.list.txt', music_list, 'epsgmod', '.epsgmod', 'empty')
class VariableFactory(object):
def __init__(self, first_available):
self.next_available = first_available
self.vars = {}
self.never_used = []
self.never_set = []
self.next_temp = 0;
self.temps = []
def set_var(self, name):
name = self._normalize_name(name)
if not name in self.vars:
self._create_var(name)
self.never_used.append(name)
if name in self.never_set:
self.never_set.remove(name)
return '{0}.'.format(self.vars[name])
def get_var(self, name):
name = self._normalize_name(name)
if not name in self.vars:
self._create_var(name)
self.never_set.append(name)
if name in self.never_used:
self.never_used.remove(name)
return '{0}:'.format(self.vars[name])
def new_temp_var(self):
if self.next_temp >= len(self.temps):
self.temps.append('*temp{0}'.format(self.next_temp))
temp = self.temps[self.next_temp]
self.next_temp += 1
return temp
def clear_temp_vars(self):
self.next_temp = 0
def _create_var(self, name):
self.vars[name] = self._num_to_ref(self.next_available)
self.next_available += 1
def _num_to_ref(self, num):
return chr(ord('A') + num) if num < 26 else str(num)
def _normalize_name(self, name):
return name.replace('$', '').strip()
if __name__ == '__main__':
logging.basicConfig(filename='twee2sam.log', level=logging.DEBUG)
console = logging.StreamHandler()
console.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(levelname)s: %(message)s')
console.setFormatter(formatter)
logging.getLogger('').addHandler(console)
main(sys.argv)