wesnoth/data/tools/wmldata.py

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