mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-29 01:35:27 +00:00
560 lines
17 KiB
Python
560 lines
17 KiB
Python
#!/usr/bin/env python
|
|
# encoding: utf8
|
|
|
|
"""
|
|
This module represents the internal appearance of WML.
|
|
"""
|
|
|
|
import re, sys, os
|
|
import wmlparser
|
|
|
|
class Data:
|
|
"""Common subclass."""
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def debug(self, show_contents = False, use_color = False, indent = [0]):
|
|
if use_color:
|
|
magenta = "\x1b[35;1m"
|
|
off = "\x1b[0m"
|
|
else:
|
|
magenta = off = ""
|
|
pos = indent[0] * " "
|
|
sys.stdout.write(pos + "\ " + magenta + self.name + off + " (" + self.__class__.__name__ + ")")
|
|
if show_contents:
|
|
sys.stdout.write(self.get_value().encode("utf8"))
|
|
sys.stdout.write("\n")
|
|
|
|
def copy(self):
|
|
c = self.__class__(self.name, self.data) # this makes a new instance or so I was told
|
|
return c
|
|
|
|
def compare(self, other):
|
|
return self.name == other.name and self.data == other.data
|
|
|
|
def get_value(self):
|
|
return ""
|
|
|
|
def get_type(self):
|
|
return self.__class__.__name__
|
|
|
|
class DataText(Data):
|
|
"""Represents any text strings."""
|
|
def __init__(self, name, text, translatable = False, textdomain = ""):
|
|
Data.__init__(self, name)
|
|
self.data = text
|
|
self.translatable = translatable
|
|
self.textdomain = textdomain
|
|
|
|
def copy(self):
|
|
return DataText(self.name, self.data, self.translatable,
|
|
self.textdomain)
|
|
|
|
def get_value(self):
|
|
return self.data
|
|
|
|
def set_value(self, data):
|
|
self.data = data
|
|
|
|
class DataBinary(Data):
|
|
"""A binary chunk of WML."""
|
|
def __init__(self, name, binary):
|
|
Data.__init__(self, name)
|
|
self.data = binary
|
|
|
|
def get_value(self):
|
|
return self.data
|
|
|
|
def set_value(self, data):
|
|
self.data = data
|
|
|
|
class DataMacro(Data):
|
|
"""A macro."""
|
|
def __init__(self, name, macro):
|
|
Data.__init__(self, name)
|
|
self.data = macro
|
|
|
|
def get_value(self):
|
|
return self.data
|
|
|
|
def set_value(self, data):
|
|
self.data = data
|
|
|
|
class DataDefine(Data):
|
|
"""A definition."""
|
|
def __init__(self, name, params, define):
|
|
Data.__init__(self, name)
|
|
self.params = params
|
|
self.data = define
|
|
|
|
def copy(self):
|
|
return DataDefine(self.name, self.params, self.define)
|
|
|
|
def get_value(self):
|
|
return self.data
|
|
|
|
def set_value(self, data):
|
|
self.data = data
|
|
|
|
class DataComment(Data):
|
|
"""A comment (normally discarded)."""
|
|
def __init__(self, name, comment):
|
|
Data.__init__(self, name)
|
|
self.data = comment
|
|
|
|
def get_value(self):
|
|
return self.data
|
|
|
|
def set_value(self, data):
|
|
self.data = data
|
|
|
|
class DataClosingTag(Data):
|
|
"""Yes, those are kept."""
|
|
def __init__(self, name):
|
|
Data.__init__(self, name)
|
|
self.data = None
|
|
|
|
class WMLException(Exception):
|
|
def __init__(self, text):
|
|
self.text = text
|
|
print text
|
|
def __str__(self):
|
|
return self.text
|
|
|
|
class DataSub(Data):
|
|
def __init__(self, name, sub = []):
|
|
"""The sub parameter is a list of sub-elements."""
|
|
Data.__init__(self, name)
|
|
self.data = []
|
|
self.dict = {}
|
|
|
|
for element in sub:
|
|
self.insert(element)
|
|
|
|
def clean_empty_ifdefs(self):
|
|
rem = []
|
|
for item in self.data:
|
|
if isinstance(item, DataIfDef):
|
|
if item.data == []:
|
|
rem.append(item)
|
|
if isinstance(item, DataSub):
|
|
item.clean_empty_ifdefs()
|
|
while rem:
|
|
item = rem.pop()
|
|
print "Removing empty #ifdef %s" % item.name
|
|
self.remove(item)
|
|
|
|
def write_file(self, f, indent = 0, textdomain = ""):
|
|
"""Write the data object to the given file object."""
|
|
ifdef = 0
|
|
|
|
self.clean_empty_ifdefs()
|
|
|
|
for item in self.data:
|
|
|
|
if ifdef:
|
|
if not isinstance(item, DataIfDef) or not item.type == "else":
|
|
f.write("#endif\n")
|
|
ifdef = 0
|
|
|
|
if isinstance(item, DataIfDef):
|
|
if item.type == "else":
|
|
f.write("#else\n")
|
|
else:
|
|
f.write("#ifdef %s\n" % item.name)
|
|
item.write_file(f, indent + 4, textdomain)
|
|
ifdef = 1
|
|
elif isinstance(item, DataSub):
|
|
f.write(" " * indent)
|
|
f.write("[%s]\n" % item.name)
|
|
item.write_file(f, indent + 4, textdomain)
|
|
f.write(" " * indent)
|
|
close = item.name
|
|
if close[0] == "+": close = close[1:]
|
|
f.write("[/%s]\n" % close)
|
|
elif isinstance(item, DataText):
|
|
f.write(" " * indent)
|
|
text = item.data
|
|
text = text.replace('"', r'\"')
|
|
# We always enclosed in quotes
|
|
# In theory, the parser will just remove then on reading in, so
|
|
# the actual value is 1:1 preserved
|
|
# TODO: is there no catch?
|
|
if textdomain and item.textdomain == "wesnoth":
|
|
f.write("#textdomain wesnoth\n")
|
|
f.write(" " * indent)
|
|
if item.translatable:
|
|
if "=" in text:
|
|
text = re.compile("=(.*?)(?=[=;]|$)"
|
|
).sub("=\" + _\"\\1\" + \"", text)
|
|
text = '"' + text + '"'
|
|
text = re.compile(r'\+ _""').sub("", text)
|
|
f.write('%s=%s\n' % (item.name, text))
|
|
else:
|
|
f.write('%s=_"%s"\n' % (item.name, text))
|
|
else:
|
|
f.write('%s="%s"\n' % (item.name, text))
|
|
if textdomain and item.textdomain == "wesnoth":
|
|
f.write(" " * indent)
|
|
f.write("#textdomain %s\n" % textdomain)
|
|
elif isinstance(item, DataMacro):
|
|
f.write(" " * indent)
|
|
f.write("%s\n" % item.data)
|
|
elif isinstance(item, DataComment):
|
|
f.write("%s\n" % item.data)
|
|
elif isinstance(item, DataDefine):
|
|
f.write("#define %s %s\n%s#enddef\n" % (item.name, item.params,
|
|
item.data))
|
|
elif isinstance(item, DataClosingTag):
|
|
f.write("[/%s]\n" % item.name)
|
|
else:
|
|
raise WMLException("Unknown item: %s" % item.__class__.__name__)
|
|
|
|
if ifdef:
|
|
f.write("#endif\n")
|
|
|
|
def is_empty(self):
|
|
return len(self.data) == 0
|
|
|
|
def children(self):
|
|
return self.data
|
|
|
|
def place_dict(self, data):
|
|
if data.name in self.dict:
|
|
self.dict[data.name] += [data]
|
|
else:
|
|
self.dict[data.name] = [data]
|
|
|
|
def insert(self, data):
|
|
"""Inserts a sub-element."""
|
|
self.data += [data]
|
|
self.place_dict(data)
|
|
|
|
def insert_first(self, data):
|
|
self.data = [data] + self.data
|
|
if data.name in self.dict:
|
|
self.dict[data.name] = [data] + self.dict[data.name]
|
|
else:
|
|
self.dict[data.name] = [data]
|
|
|
|
def insert_after(self, previous, data):
|
|
"""Insert after given node, or else insert as first."""
|
|
if not previous in self.data: return self.insert_first(data)
|
|
# completely rebuild list and dict
|
|
new_childs = []
|
|
self.dict = {}
|
|
for child in self.data:
|
|
new_childs += [child]
|
|
self.place_dict(child)
|
|
if child == previous:
|
|
new_childs += [data]
|
|
self.place_dict(data)
|
|
self.data = new_childs
|
|
|
|
def insert_at(self, pos, data):
|
|
"""Insert at given index (or as last)."""
|
|
if pos >= len(self.data): return self.insert(data)
|
|
# completely rebuild list and dict
|
|
new_childs = []
|
|
self.dict = {}
|
|
i = 0
|
|
for child in self.data:
|
|
if i == pos:
|
|
new_childs += [data]
|
|
self.place_dict(data)
|
|
new_childs += [child]
|
|
self.place_dict(child)
|
|
i += 1
|
|
|
|
self.data = new_childs
|
|
|
|
def insert_as_nth(self, pos, data):
|
|
"""Insert as nth child of same name."""
|
|
if pos == 0: return self.insert_first(data)
|
|
already = self.get_all(data.name)
|
|
before = already[pos - 1]
|
|
self.insert_after(before, data)
|
|
|
|
def remove(self, child):
|
|
"""Removes a sub-element."""
|
|
self.data.remove(child)
|
|
self.dict[child.name].remove(child)
|
|
|
|
def clear(self):
|
|
"""Remove everything."""
|
|
self.data = []
|
|
self.dict = {}
|
|
|
|
def copy(self):
|
|
"""Return a recursive copy of the element."""
|
|
copy = DataSub(self.name)
|
|
for item in self.data:
|
|
subcopy = item.copy()
|
|
copy.insert(subcopy)
|
|
return copy
|
|
|
|
def compare(self, other):
|
|
if len(self.data) != len(other.data): return False
|
|
for i in range(self.data):
|
|
if not self.data[i].compare(other.data[i]): return False
|
|
return True
|
|
|
|
def rename_child(self, child, name):
|
|
self.dict[child.name].remove(child)
|
|
child.name = name
|
|
# rebuild complete mapping for this name
|
|
if name in self.dict: del self.dict[name]
|
|
for item in self.data:
|
|
if item.name == name:
|
|
if name in self.dict:
|
|
self.dict[name] += [item]
|
|
else:
|
|
self.dict[name] = [item]
|
|
|
|
def insert_text(self, name, data, translatable = False,
|
|
textdomain = ""):
|
|
data = DataText(name, data, translatable = translatable,
|
|
textdomain = textdomain)
|
|
self.insert(data)
|
|
|
|
def insert_macro(self, name, args = None):
|
|
macrodata = "{" + name
|
|
if args: macrodata += " " + str(args)
|
|
macrodata += "}"
|
|
data = DataMacro(name, macrodata)
|
|
self.insert(data)
|
|
|
|
def get_first(self, name, default = None):
|
|
"""Return first of given tag, or default"""
|
|
if not name in self.dict or not self.dict[name]: return default
|
|
return self.dict[name][0]
|
|
|
|
def get_all_with_attributes(self, attr_name, **kw):
|
|
"""Like get_or_create_sub_with_attributes."""
|
|
ret = []
|
|
for data in self.get_all(attr_name):
|
|
if isinstance(data, DataSub):
|
|
for key in kw:
|
|
if data.get_text_val(key) != kw[key]:
|
|
break
|
|
else:
|
|
ret += [data]
|
|
return ret
|
|
|
|
def get_or_create_sub(self, name):
|
|
for data in self.get_all(name):
|
|
if isinstance(data, DataSub): return data
|
|
|
|
sub = DataSub(name, [])
|
|
self.insert(sub)
|
|
return sub
|
|
|
|
def create_sub(self, name):
|
|
sub = DataSub(name, [])
|
|
self.insert(sub)
|
|
return sub
|
|
|
|
def get_or_create_sub_with_attributes(*args, **kw):
|
|
self, name = args
|
|
"""For the uber lazy. Example:
|
|
|
|
event = scenario.get_or_create_sub_with_attribute("event", name = "prestart")
|
|
|
|
That should find the first prestart event and return it, or else
|
|
create and insert a new prestart event and return it.
|
|
"""
|
|
for data in self.get_all(name):
|
|
if isinstance(data, DataSub):
|
|
for key in kw:
|
|
if data.get_text_val(key) != kw[key]:
|
|
break
|
|
else:
|
|
return data
|
|
|
|
sub = DataSub(name, [])
|
|
for key in kw:
|
|
sub.set_text_val(key, kw[key])
|
|
self.insert(sub)
|
|
return sub
|
|
|
|
def get_or_create_ifdef(self, name):
|
|
for data in self.get_all(name):
|
|
if isinstance(data, DataIfDef): return data
|
|
|
|
ifdef = DataIfDef(name, [], "then")
|
|
self.insert(ifdef)
|
|
return ifdef
|
|
|
|
def delete_all(self, name):
|
|
while 1:
|
|
data = self.get_first(name)
|
|
if not data: break
|
|
self.remove(data)
|
|
|
|
def remove_all(self, name):
|
|
self.delete_all(name)
|
|
|
|
def find_all(self, *args):
|
|
"""Return list of multiple tags"""
|
|
return [item for item in self.data if item.name in args]
|
|
|
|
def get_all(self, name):
|
|
"""Return a list of all sub-items matching the given name."""
|
|
if not name in self.dict: return []
|
|
return self.dict[name]
|
|
|
|
def get_text(self, name):
|
|
"""Return a text element"""
|
|
for data in self.get_all(name):
|
|
if isinstance(data, DataText): return data
|
|
return None
|
|
|
|
def get_binary(self, name):
|
|
"""Return a binary element"""
|
|
for data in self.get_all(name):
|
|
if isinstance(data, DataBinary): return data
|
|
return None
|
|
|
|
def remove_text(self, name):
|
|
text = self.get_text(name)
|
|
if text: self.remove(text)
|
|
|
|
def get_macros(self, name):
|
|
"""Gets all macros matching the name"""
|
|
return [macro for macro in self.get_all(name)
|
|
if isinstance(macro, DataMacro)]
|
|
|
|
def get_all_macros(self):
|
|
"""Gets all macros"""
|
|
return [macro for macro in self.data
|
|
if isinstance(macro, DataMacro)]
|
|
|
|
def get_ifdefs(self, name):
|
|
"""Gets all ifdefs matching the name"""
|
|
return [ifdef for ifdef in self.get_all(name)
|
|
if isinstance(ifdef, DataIfDef)]
|
|
|
|
def get_subs(self, name):
|
|
"""Gets all macros matching the name"""
|
|
return [sub for sub in self.get_all(name)
|
|
if isinstance(sub, DataSub)]
|
|
|
|
def remove_macros(self, name):
|
|
for macro in self.get_macros(name):
|
|
self.remove(macro)
|
|
|
|
def get_binary_val(self, name, default = None):
|
|
"""For the lazy."""
|
|
binary = self.get_binary(name)
|
|
if binary: return binary.data
|
|
return default
|
|
|
|
def get_text_val(self, name, default = None):
|
|
"""For the lazy."""
|
|
text = self.get_text(name)
|
|
if text: return text.data
|
|
return default
|
|
|
|
def set_text_val(self, name, value, delete_if = None, translatable = False,
|
|
textdomain = ""):
|
|
"""For the lazy."""
|
|
text = self.get_text(name)
|
|
if text:
|
|
if value == delete_if:
|
|
self.remove(text)
|
|
else:
|
|
text.data = value
|
|
text.textdomain = textdomain
|
|
text.translatable = translatable
|
|
else:
|
|
if value != delete_if:
|
|
self.insert_text(name, value, translatable = translatable,
|
|
textdomain = textdomain)
|
|
|
|
def get_comment(self, start):
|
|
for item in self.get_all("comment"):
|
|
if isinstance(item, DataComment):
|
|
if item.data.startswith(start): return item
|
|
return None
|
|
|
|
def set_comment_first(self, comment):
|
|
"""For the lazy."""
|
|
for item in self.get_all("comment"):
|
|
if isinstance(item, DataComment):
|
|
if item.data == comment: return
|
|
|
|
self.insert_first(DataComment("comment", comment))
|
|
|
|
def get_quantity(self, tag, difficulty, default = None):
|
|
"""For the even lazier, looks for a value inside a difficulty ifdef.
|
|
"""
|
|
v = self.get_text_val(tag)
|
|
if v != None: return v
|
|
|
|
for ifdef in self.get_ifdefs(["EASY", "NORMAL", "HARD"][difficulty]):
|
|
v = ifdef.get_text_val(tag)
|
|
if v != None: return v
|
|
|
|
return default
|
|
|
|
def set_quantity(self, name, difficulty, value):
|
|
"""Sets one of 3 values of a quantity. If it doesn't exist yet, also the
|
|
other difficulties get the same value.
|
|
"""
|
|
value = str(value)
|
|
# read existing values
|
|
q = []
|
|
for d in range(3):
|
|
q += [self.get_quantity(name, d, value)]
|
|
q[difficulty] = value
|
|
|
|
# remove current tags
|
|
self.remove_text(name)
|
|
for ifdef in self.get_ifdefs("EASY") + self.get_ifdefs("NORMAL") + self.get_ifdefs("HARD"):
|
|
ifdef.remove_text(name)
|
|
|
|
# insert updated item
|
|
if q[0] == q[1] == q[2]:
|
|
self.set_text_val(name, value)
|
|
else:
|
|
for d in range(3):
|
|
ifdef = self.get_or_create_ifdef(["EASY", "NORMAL", "HARD"][d])
|
|
ifdef.set_text_val(name, q[d])
|
|
|
|
def debug(self, show_contents = False, use_color = False, indent = [0]):
|
|
if use_color:
|
|
red = "\x1b[31;1m"
|
|
off = "\x1b[0m"
|
|
else:
|
|
red = off = ""
|
|
pos = indent[0] * " "
|
|
print pos + "\ " + red + self.name + off + " (" + self.__class__.__name__ + ")"
|
|
indent[0] += 1
|
|
for child in self.data:
|
|
child.debug(show_contents, use_color, indent)
|
|
indent[0] -= 1
|
|
|
|
class DataIfDef(DataSub):
|
|
"""
|
|
An #ifdef section in WML.
|
|
"""
|
|
def __init__(self, name, sub, type):
|
|
DataSub.__init__(self, name, sub)
|
|
self.type = type
|
|
|
|
def copy(self):
|
|
copy = DataSub.copy(self)
|
|
copy.type = self.type
|
|
return copy
|
|
|
|
def read_file(filename, root_name = "WML"):
|
|
"""
|
|
Read in a file from disk and return a WML data object, with the WML in the
|
|
file placed under an entry with the name root_name.
|
|
"""
|
|
parser = wmlparser.Parser(None)
|
|
parser.parse_file(filename)
|
|
data = DataSub(root_name)
|
|
parser.parse_top(data)
|
|
return data
|