1import re
2import pywmlx.state.machine
3from pywmlx.state.state import State
4import pywmlx.nodemanip
5from pywmlx.wmlerr import wmlerr
6
7
8
9class WmlIdleState:
10    def __init__(self):
11        self.regex = None
12        self.iffail = None
13
14    def run(self, xline, lineno, match):
15        _nextstate = 'wml_checkdom'
16        if pywmlx.state.machine._pending_wmlstring is not None:
17            pywmlx.state.machine._pending_wmlstring.store()
18            pywmlx.state.machine._pending_wmlstring = None
19        m = re.match(r'\s*$', xline)
20        if m:
21            xline = None
22            _nextstate = 'wml_idle'
23        return (xline, _nextstate) # 'wml_define'
24
25
26
27'''
28class WmlDefineState:
29    def __init__(self):
30        self.regex = re.compile('\s*#(define|enddef|\s+wmlxgettext:\s+)', re.I)
31        self.iffail = 'wml_checkdom'
32
33    def run(self, xline, lineno, match):
34        if match.group(1).lower() == 'define':
35            # define
36            xline = None
37            if pywmlx.nodemanip.onDefineMacro is False:
38                pywmlx.nodemanip.onDefineMacro = True
39            else:
40                err_message = ("expected an #enddef before opening ANOTHER " +
41                               "macro definition with #define")
42                finfo = pywmlx.nodemanip.fileref + ":" + str(lineno)
43                wmlerr(finfo, err_message)
44        elif match.group(1).lower() == 'enddef':
45            # enddef
46            xline = None
47            if pywmlx.nodemanip.onDefineMacro is True:
48                pywmlx.nodemanip.onDefineMacro = False
49            else:
50                err_message = ("found an #enddef, but no macro definition " +
51                               "is pending. Perhaps you forgot to put a " +
52                               "#define somewhere?")
53                finfo = pywmlx.nodemanip.fileref + ":" + str(lineno)
54                wmlerr(finfo, err_message)
55        else:
56            # wmlxgettext: {WML CODE}
57            xline = xline [ match.end(): ]
58        return (xline, 'wml_idle')
59'''
60
61
62
63class WmlCheckdomState:
64    def __init__(self):
65        self.regex = re.compile(r'\s*#textdomain\s+(\S+)', re.I)
66        self.iffail = 'wml_checkpo'
67
68    def run(self, xline, lineno, match):
69        pywmlx.state.machine._currentdomain = match.group(1)
70        xline = None
71        return (xline, 'wml_idle')
72
73
74
75class WmlCheckpoState:
76    def __init__(self):
77        rx = r'\s*#\s*(wmlxgettext|po-override|po):\s+(.+)'
78        self.regex = re.compile(rx, re.I)
79        self.iffail = 'wml_comment'
80
81    def run(self, xline, lineno, match):
82        if match.group(1) == 'wmlxgettext':
83            xline = match.group(2)
84        # on  #po: addedinfo
85        elif match.group(1) == "po":
86            xline = None
87            if pywmlx.state.machine._pending_addedinfo is None:
88                pywmlx.state.machine._pending_addedinfo = [ match.group(2) ]
89            else:
90                pywmlx.state.machine._pending_addedinfo.append(match.group(2))
91        # on -- #po-override: overrideinfo
92        elif pywmlx.state.machine._pending_overrideinfo is None:
93            pywmlx.state.machine._pending_overrideinfo = [ match.group(2) ]
94            xline = None
95        else:
96            pywmlx.state.machine._pending_overrideinfo.append(match.group(2))
97            xline = None
98        return (xline, 'wml_idle')
99
100
101
102class WmlCommentState:
103    def __init__(self):
104        self.regex = re.compile(r'\s*#.+')
105        self.iffail = 'wml_str02'
106
107    def run(self, xline, lineno, match):
108        xline = None
109        return (xline, 'wml_idle')
110
111
112
113# On WML you can also have _ << translatable string >>, even if quite rare
114# This is considered here as "WML string 02" since it is a rare case.
115# However, for code safety, it is evaluated here before evaluating tags, and
116# before string 1. Unlike all other strings, this will be evaluated ONLY if
117# translatable, to avoid possible conflicts with WmlGoLuaState (and to prevent
118# to make that state unreachable).
119# In order to ensure that the order of sentences will be respected, the regexp
120# does not match if " is found before _ <<
121# In this way the WmlStr01 state (wich is placed after) can be reached and the
122# sentence will not be lost.
123# WARNING: This also means that it is impossible to capture any wmlinfo which
124#          uses this kind of translatable string
125#          example: name = _ <<Name>>
126#          in that case the string "name" will be captured, but the wmlinfo
127#          name = Name will be NOT added to automatic informations.
128#          This solution is necessary, since extending the workaround
129#          done for _ "standard translatable strings" to _ << wmlstr02 >>
130#          can introduce serious bugs
131class WmlStr02:
132    def __init__(self):
133        rx = r'[^"]*_\s*<<(?:(.*?)>>|(.*))'
134        self.regex = re.compile(rx)
135        self.iffail = 'wml_tag'
136
137    def run(self, xline, lineno, match):
138        # if will ever happen 'wmlstr02 assertion error' than you could
139        # turn 'mydebug' to 'True' to inspect what exactly happened.
140        # However this should be never necessary
141        mydebug = False
142        _nextstate = 'wml_idle'
143        loc_translatable = True
144        loc_multiline = False
145        loc_string = None
146        # match group(1) exists, so it is a single line string
147        # (group(2) will not exist, than)
148        if match.group(1) is not None:
149            loc_multiline = False
150            loc_string = match.group(1)
151            xline = xline [ match.end(): ]
152        # match.group(2) exists, so it is a multi-line string
153        # (group(1) will not exist, than)
154        elif match.group(2) is not None:
155            loc_multiline = True
156            loc_string = match.group(2)
157            _nextstate = 'wml_str20'
158            xline = None
159        else:
160            if mydebug:
161                err_message = ("wmlstr02 assertion error (DEBUGGING)\n" +
162                               'g1: ' + str(match.group(1)) + '\n' +
163                               'g2: ' + str(match.group(2)) )
164                finfo = pywmlx.nodemanip.fileref + ":" + str(lineno)
165                wmlerr(finfo, err_message)
166            else:
167                wmlerr('wmlxgettext python sources',
168                   'wmlstr02 assertion error\n'
169                   'please report a bug if you encounter this error message')
170        pywmlx.state.machine._pending_wmlstring = (
171            pywmlx.state.machine.PendingWmlString(
172                lineno, loc_string, loc_multiline, loc_translatable, israw=True
173            )
174        )
175        return (xline, _nextstate)
176
177
178
179class WmlTagState:
180    def __init__(self):
181        # this regexp is discussed in depth in Source Documentation, chapter 6
182        rx = r'\s*(?:[^"]+\(\s*)?\[\s*([\/+-]?)\s*([A-Za-z0-9_]+)\s*\]'
183        self.regex = re.compile(rx)
184        self.iffail = 'wml_getinf'
185
186    def run(self, xline, lineno, match):
187        # xdebug = open('./debug.txt', 'a')
188        # xdebug_str = None
189        if match.group(1) == '/':
190            closetag = '[/' + match.group(2) + ']'
191            pywmlx.nodemanip.closenode(closetag,
192                                       pywmlx.state.machine._dictionary,
193                                       lineno)
194            if closetag == '[/lua]':
195                pywmlx.state.machine._pending_luafuncname = None
196                pywmlx.state.machine._on_luatag = False
197            # xdebug_str = closetag + ': ' + str(lineno)
198        else:
199            opentag = '[' + match.group(2) + ']'
200            pywmlx.nodemanip.newnode(opentag)
201            if(opentag == '[lua]'):
202                pywmlx.state.machine._on_luatag = True
203            # xdebug_str = opentag + ': ' + str(lineno)
204        # print(xdebug_str, file=xdebug)
205        # xdebug.close()
206        pywmlx.state.machine._pending_addedinfo = None
207        pywmlx.state.machine._pending_overrideinfo = None
208        xline = xline [ match.end(): ]
209        return (xline, 'wml_idle')
210
211
212
213class WmlGetinfState:
214    def __init__(self):
215        rx = ( r'\s*(speaker|id|role|description|condition|type|race)' +
216               r'\s*=\s*(.*)' )
217        self.regex = re.compile(rx, re.I)
218        self.iffail = 'wml_str01'
219    def run(self, xline, lineno, match):
220        _nextstate = 'wml_idle'
221        if '"' in match.group(2):
222            _nextstate = 'wml_str01'
223            pywmlx.state.machine._pending_winfotype = match.group(1)
224        else:
225            loc_wmlinfo = match.group(1) + '=' + match.group(2)
226            xline = None
227            pywmlx.nodemanip.addWmlInfo(loc_wmlinfo)
228        return (xline, _nextstate)
229
230
231
232class WmlStr01:
233    def __init__(self):
234        rx = r'(?:[^"]*?)\s*(_?)\s*"((?:""|[^"])*)("?)'
235        self.regex = re.compile(rx)
236        self.iffail = 'wml_golua'
237
238    def run(self, xline, lineno, match):
239        _nextstate = 'wml_idle'
240        loc_translatable = True
241        if match.group(1) == "":
242            loc_translatable = False
243        loc_multiline = False
244        if match.group(3) == "":
245            xline = None
246            loc_multiline = True
247            _nextstate = 'wml_str10'
248        else:
249            xline = xline [ match.end(): ]
250        pywmlx.state.machine._pending_wmlstring = (
251            pywmlx.state.machine.PendingWmlString(
252                lineno, match.group(2), loc_multiline, loc_translatable, israw=False
253            )
254        )
255        return (xline, _nextstate)
256
257
258
259# well... the regex will always be true on this state, so iffail will never
260# be executed
261class WmlStr10:
262    def __init__(self):
263        self.regex = re.compile(r'((?:""|[^"])*)("?)')
264        self.iffail = 'wml_str10'
265
266    def run(self, xline, lineno, match):
267        _nextstate = None
268        pywmlx.state.machine._pending_wmlstring.addline( match.group(1) )
269        if match.group(2) == "":
270            _nextstate = 'wml_str10'
271            xline = None
272        else:
273            _nextstate = 'wml_idle'
274            xline = xline [ match.end(): ]
275        return (xline, _nextstate)
276
277
278
279class WmlStr20:
280    def __init__(self):
281        self.regex = None
282        self.iffail = None
283
284    def run(self, xline, lineno, match):
285        realmatch = re.match(r'(.*?)>>', xline)
286        _nextstate = 'wml_str20'
287        if realmatch:
288            pywmlx.state.machine._pending_wmlstring.addline(
289                realmatch.group(1) )
290            xline = xline [ realmatch.end(): ]
291            _nextstate = 'wml_idle'
292        else:
293            pywmlx.state.machine._pending_wmlstring.addline(xline)
294            xline = None
295            _nextstate = 'wml_str20'
296        return (xline, _nextstate)
297
298
299
300# Only if the symbol '<<' is found inside a [lua] tag, then it means we are
301# actually starting a lua code.
302# It can happen that WML uses the '<<' symbol in a very different context
303# wich has nothing to do with lua, so switching to the lua states in that
304# case can lead to problems.
305# This happened on the file data/gui/default/widget/toggle_button_orb.cfg
306# on wesnoth 1.13.x, where there is this line inside the first [image] tag:
307#
308#     name = "('buttons/misc/orb{STATE}.png" + <<~RC(magenta>{icon})')>>
309#
310# In that case, after 'name' there is a WML string
311# "('buttons/misc/orb{STATE}.png"
312# And after that you find a concatenation with a literal string
313# <<~RC(magenta>{icon})')>>
314#
315# That second string has nothing to do with lua, and, most importantly, if
316# it is parsed with lua states, it returns an error... why?
317# Simply because of the final ' symbol, wich is a valid symbol, in lua, for
318# opening a new string; but, in that case, there is not an opening string,
319# but a ' symbol that must be used literally.
320#
321# This is why we use a global var _on_luatag in state.py wich is usually False.
322# it will be set to True only when opening a lua tag (see WmlTagState)
323# it will be set to False again when the lua tag is closed (see WmlTagState)
324class WmlGoluaState:
325    def __init__(self):
326        self.regex = re.compile(r'.*?<<\s*')
327        self.iffail = 'wml_final'
328
329    def run(self, xline, lineno, match):
330        if pywmlx.state.machine._on_luatag:
331            xline = xline [ match.end(): ]
332            return (xline, 'lua_idle')
333        else:
334            return (xline, 'wml_final')
335
336
337
338class WmlFinalState:
339    def __init__(self):
340        self.regex = None
341        self.iffail = None
342
343    def run(self, xline, lineno, match):
344        xline = None
345        if pywmlx.state.machine._pending_wmlstring is not None:
346            pywmlx.state.machine._pending_wmlstring.store()
347            pywmlx.state.machine._pending_wmlstring = None
348        return (xline, 'wml_idle')
349
350
351
352def setup_wmlstates():
353    for statename, stateclass in [ ('wml_idle', WmlIdleState),
354                                   # ('wml_define', WmlDefineState),
355                                   ('wml_checkdom', WmlCheckdomState),
356                                   ('wml_checkpo', WmlCheckpoState),
357                                   ('wml_comment', WmlCommentState),
358                                   ('wml_str02', WmlStr02),
359                                   ('wml_tag', WmlTagState),
360                                   ('wml_getinf', WmlGetinfState),
361                                   ('wml_str01', WmlStr01),
362                                   ('wml_str10', WmlStr10),
363                                   ('wml_str20', WmlStr20),
364                                   ('wml_golua', WmlGoluaState),
365                                   ('wml_final', WmlFinalState)]:
366        st = stateclass()
367        pywmlx.state.machine.addstate(statename,
368            State(st.regex, st.run, st.iffail) )
369