mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-30 21:03:28 +00:00
875 lines
37 KiB
Python
875 lines
37 KiB
Python
"""
|
|
wmltools.py -- Python routines for working with a Battle For Wesnoth WML tree
|
|
|
|
"""
|
|
|
|
import sys, os, re, sre_constants, hashlib, glob, gzip
|
|
|
|
map_extensions = ("map", "mask")
|
|
image_extensions = ("png", "jpg", "jpeg")
|
|
sound_extensions = ("ogg", "wav")
|
|
vc_directories = (".git", ".svn")
|
|
l10n_directories = ("l10n",)
|
|
resource_extensions = map_extensions + image_extensions + sound_extensions
|
|
image_reference = r"[A-Za-z0-9{}.][A-Za-z0-9_/+{}.-]*\.(png|jpg)(?=(~.*)?)"
|
|
|
|
def pop_to_top(whoami):
|
|
"Pop upward to the top-level directory."
|
|
upwards = os.getcwd().split(os.sep)
|
|
upwards.reverse()
|
|
for pathpart in upwards:
|
|
# Loose match because people have things like git trees.
|
|
if os.path.basename(pathpart).find("wesnoth") > -1:
|
|
break
|
|
else:
|
|
os.chdir("..")
|
|
else:
|
|
sys.stderr.write(whoami + ": must be run from within a Battle "
|
|
"for Wesnoth source tree.\n")
|
|
sys.exit(1)
|
|
# In case we're looking at a whole-repo checkout in SVN
|
|
if os.path.exists("trunk"):
|
|
os.chdir("trunk")
|
|
|
|
def string_strip(value):
|
|
"String-strip the value"
|
|
if value.startswith('"'):
|
|
value = value[1:]
|
|
if value.endswith('"'):
|
|
value = value[:-1]
|
|
if value.startswith("'"):
|
|
value = value[1:]
|
|
if value.endswith("'"):
|
|
value = value[:-1]
|
|
return value
|
|
|
|
def attr_strip(value):
|
|
"Strip away an (optional) translation mark and string quotes."
|
|
value = value.strip()
|
|
if value.startswith('_'):
|
|
value = value[1:]
|
|
value = value.strip()
|
|
return string_strip(value)
|
|
|
|
def parse_attribute(line):
|
|
"Parse a WML key-value pair from a line."
|
|
if '=' not in line or line.find("#") > -1 and line.find("#") < line.find("="):
|
|
return None
|
|
where = line.find("=")
|
|
leader = line[:where]
|
|
after = line[where+1:]
|
|
after = after.lstrip()
|
|
if "#" in after:
|
|
where = after.find("#")
|
|
where -= 1
|
|
while after[where] in (" ", "\t"):
|
|
where -= 1
|
|
value = after[:where+1]
|
|
comment = after[where+1:]
|
|
else:
|
|
value = after.rstrip()
|
|
comment = ""
|
|
# Return four fields: stripped key, part of line before value,
|
|
# value, trailing whitespace and comment.
|
|
return (leader.strip(), leader+"=", string_strip(value), comment)
|
|
|
|
class Forest:
|
|
"Return an iterable directory forest object."
|
|
def __init__(self, dirpath, exclude=None):
|
|
"Get the names of all files under dirpath, ignoring version-control directories."
|
|
self.forest = []
|
|
self.dirpath = dirpath
|
|
for dir in dirpath:
|
|
subtree = []
|
|
if os.path.isdir(dir): # So we skip .cfgs in a UMC mirror
|
|
os.path.walk(dir,
|
|
lambda arg, dir, names: subtree.extend(map(lambda x: os.path.normpath(os.path.join(dir, x)), names)),
|
|
None)
|
|
# Always look at _main.cfg first
|
|
subtree.sort(lambda x, y: cmp(x, y) - 2*int(x.endswith("_main.cfg")) + 2*int(y.endswith("_main.cfg")))
|
|
self.forest.append(subtree)
|
|
for i in range(len(self.forest)):
|
|
# Ignore version-control subdirectories and Emacs tempfiles
|
|
for dirkind in vc_directories + l10n_directories:
|
|
self.forest[i] = filter(lambda x: dirkind not in x, self.forest[i])
|
|
self.forest[i] = filter(lambda x: '.#' not in x, self.forest[i])
|
|
self.forest[i] = filter(lambda x: not os.path.isdir(x), self.forest[i])
|
|
if exclude:
|
|
self.forest[i] = filter(lambda x: not re.search(exclude, x), self.forest[i])
|
|
self.forest[i] = filter(lambda x: not x.endswith("-bak"), self.forest[i])
|
|
# Compute cliques (will be used later for visibility checks)
|
|
self.clique = {}
|
|
counter = 0
|
|
for tree in self.forest:
|
|
for filename in tree:
|
|
self.clique[filename] = counter
|
|
counter += 1
|
|
def parent(self, filename):
|
|
"Return the directory root that caused this path to be included."
|
|
return self.dirpath[self.clique[filename]]
|
|
def neighbors(self, fn1, fn2):
|
|
"Are two files from the same tree?"
|
|
return self.clique[fn1] == self.clique[fn2]
|
|
def flatten(self):
|
|
"Return a flattened list of all files in the forest."
|
|
allfiles = []
|
|
for tree in self.forest:
|
|
allfiles += tree
|
|
return allfiles
|
|
def generator(self):
|
|
"Return a generator that walks through all files."
|
|
for (dir, tree) in zip(self.dirpath, self.forest):
|
|
for filename in tree:
|
|
yield (dir, filename)
|
|
|
|
def iswml(filename):
|
|
"Is the specified filename WML?"
|
|
return filename.endswith(".cfg")
|
|
|
|
def issave(filename):
|
|
"Is the specified filename a WML save? (Detects compressed saves too.)"
|
|
if isresource(filename):
|
|
return False
|
|
if filename.endswith(".gz"):
|
|
firstline = gzip.open(filename).readline()
|
|
else:
|
|
firstline = open(filename).readline()
|
|
return firstline.startswith("label=")
|
|
|
|
def isresource(filename):
|
|
"Is the specified name a resource?"
|
|
(root, ext) = os.path.splitext(filename)
|
|
return ext and ext[1:] in resource_extensions
|
|
|
|
def formaltype(f):
|
|
# Deduce the expected type of the formal
|
|
if f.startswith("_"):
|
|
f = f[1:]
|
|
if f in ("SIDE", "X", "Y", "RED", "GREEN", "BLUE", "TURN", "RADIUS", "PROB", "LAYER", "TIME") or f.endswith("_SIDE") or f.endswith("NUMBER") or f.endswith("AMOUNT") or f.endswith("COST") or f.endswith("_X") or f.endswith("_Y") or f.endswith("_INCREMENT") or f.endswith("_FACTOR") or f.endswith("_TIME"):
|
|
ftype = "numeric"
|
|
elif f in ("POSITION",) or f.endswith("_POSITION"):
|
|
ftype = "position"
|
|
elif f.endswith("_SPAN"):
|
|
ftype = "span"
|
|
elif f == "SIDES" or f.endswith("_SIDES"):
|
|
ftype = "alliance"
|
|
elif f in ("RANGE",):
|
|
ftype = "range"
|
|
elif f in ("ALIGN",):
|
|
ftype = "alignment"
|
|
elif f in ("TERRAIN",):
|
|
ftype = "terrain_code"
|
|
elif f in ("NAME", "VAR", "IMAGESTEM", "ID", "FLAG") or f.endswith("_NAME") or f.endswith("_ID"):
|
|
ftype = "name"
|
|
elif f in ("ID_STRING", "NAME_STRING", "DESCRIPTION"):
|
|
ftype = "optional_string"
|
|
elif f in ("STRING", "TYPE", "TEXT") or f.endswith("_STRING") or f.endswith("_TYPE") or f.endswith("_TEXT"):
|
|
ftype = "string"
|
|
elif f.endswith("IMAGE") or f == "PROFILE":
|
|
ftype = "image"
|
|
elif f.endswith("MUSIC",) or f.endswith("SOUND"):
|
|
ftype = "sound"
|
|
elif f in ("FILTER",):
|
|
ftype = "filter"
|
|
elif f == "WML" or f.endswith("_WML"):
|
|
ftype = "wml"
|
|
elif f in ("AFFIX",) or f.endswith("AFFIX"):
|
|
ftype = "affix"
|
|
elif f.endswith("VALUE"):
|
|
ftype = "any"
|
|
else:
|
|
ftype = None
|
|
return ftype
|
|
|
|
def actualtype(a):
|
|
if a is None:
|
|
return None
|
|
# Deduce the type of the actual
|
|
if a.isdigit() or a.startswith("-") and a[1:].isdigit():
|
|
atype = "numeric"
|
|
elif re.match(r"-?[0-9]+,-?[0-9]+\Z", a):
|
|
atype = "position"
|
|
elif re.match(r"([0-9]+\-[0-9]+,?|[0-9]+,?)+\Z", a):
|
|
atype = "span"
|
|
elif a in ("melee", "ranged"):
|
|
atype = "range"
|
|
elif a in ("lawful", "neutral", "chaotic"):
|
|
atype = "alignment"
|
|
elif a.startswith("{") or a.endswith("}") or a.startswith("$"):
|
|
atype = None # Can't tell -- it's a macro expansion
|
|
elif re.match(image_reference, a):
|
|
atype = "image"
|
|
elif re.match(r"(\*|[A-Z][a-z]+)\^([A-Z][a-z\\|/]+\Z)?", a):
|
|
atype = "terrain_code"
|
|
elif a.endswith(".wav") or a.endswith(".ogg"):
|
|
atype = "sound"
|
|
elif a.startswith('"') and a.endswith('"') or a.startswith("_"):
|
|
atype = "stringliteral"
|
|
elif "=" in a:
|
|
atype = "filter"
|
|
elif re.match(r"[A-Z][a-z][a-z]?\Z", a):
|
|
atype = "shortname"
|
|
elif a == "":
|
|
atype = "empty"
|
|
elif not ' ' in a:
|
|
atype = "name"
|
|
else:
|
|
atype = "string"
|
|
return atype
|
|
|
|
def argmatch(formals, actuals):
|
|
if len(formals) != len(actuals):
|
|
return False
|
|
for (f, a) in zip(formals, actuals):
|
|
# Here's the compatibility logic. First, we catch the situations
|
|
# in which a more restricted actual type matches a more general
|
|
# formal one. Then we have a fallback rule checking for type
|
|
# equality or wildcarding.
|
|
ftype = formaltype(f)
|
|
atype = actualtype(a)
|
|
if ftype == "any":
|
|
pass
|
|
elif atype in ("filter", "empty") and ftype == "wml":
|
|
pass
|
|
elif atype in ("numeric", "position") and ftype == "span":
|
|
pass
|
|
elif atype in ("shortname", "name", "empty") and ftype == "affix":
|
|
pass
|
|
elif atype in ("shortname", "name", "stringliteral") and ftype == "string":
|
|
pass
|
|
elif atype in ("shortname", "name", "string", "stringliteral", "empty") and ftype == "optional_string":
|
|
pass
|
|
elif atype in ("shortname",) and ftype == "terrain_code":
|
|
pass
|
|
elif atype in ("numeric", "position", "span", "empty") and ftype == "alliance":
|
|
pass
|
|
elif atype != ftype and ftype is not None and atype is not None:
|
|
return False
|
|
return True
|
|
|
|
class Reference:
|
|
"Describes a location by file and line."
|
|
def __init__(self, namespace, filename, lineno=None, docstring=None, args=None):
|
|
self.namespace = namespace
|
|
self.filename = filename
|
|
self.lineno = lineno
|
|
self.docstring = docstring
|
|
self.args = args
|
|
self.references = {}
|
|
self.undef = None
|
|
def append(self, fn, n, a=None):
|
|
if fn not in self.references:
|
|
self.references[fn] = []
|
|
self.references[fn].append((n, a))
|
|
def dump_references(self):
|
|
"Dump all known references to this definition."
|
|
for (file, refs) in self.references.items():
|
|
print " %s: %s" % (file, `map(lambda x: x[0], refs)`[1:-1])
|
|
def __cmp__(self, other):
|
|
"Compare two documentation objects for place in the sort order."
|
|
# Major sort by file, minor by line number. This presumes that the
|
|
# files correspond to coherent topics and gives us control of the
|
|
# sequence.
|
|
byfile = cmp(self.filename, other.filename)
|
|
if byfile:
|
|
return byfile
|
|
else:
|
|
return cmp(self.lineno, other.lineno)
|
|
def mismatches(self):
|
|
copy = Reference(self.namespace, self.filename, self.lineno, self.docstring, self.args)
|
|
copy.undef = self.undef
|
|
for filename in self.references:
|
|
mis = filter(lambda (ln, a): a is not None and not argmatch(self.args, a), self.references[filename])
|
|
if mis:
|
|
copy.references[filename] = mis
|
|
return copy
|
|
def __str__(self):
|
|
if self.lineno:
|
|
return '"%s", line %d' % (self.filename, self.lineno)
|
|
else:
|
|
return self.filename
|
|
__repr__ = __str__
|
|
|
|
class CrossRef:
|
|
macro_reference = re.compile(r"\{([A-Z_][A-Za-z0-9_:]*)(?!\.)\b")
|
|
file_reference = re.compile(r"[A-Za-z0-9{}.][A-Za-z0-9_/+{}.-]*\.(" + "|".join(resource_extensions) + ")(?=(~.*)?)")
|
|
tag_parse = re.compile("\s*([a-z_]+)\s*=(.*)")
|
|
def mark_matching_resources(self, pattern, fn, n):
|
|
"Mark all definitions matching a specified pattern with a reference."
|
|
pattern = pattern.replace("+", r"\+")
|
|
try:
|
|
pattern = re.compile(os.sep + pattern + "$")
|
|
except sre_constants.error:
|
|
print >>sys.stderr, "wmlscope: confused by %s" % pattern
|
|
return None
|
|
key = None
|
|
for trial in self.fileref:
|
|
if pattern.search(trial) and self.visible_from(trial, fn, n):
|
|
key = trial
|
|
self.fileref[key].append(fn, n)
|
|
return key
|
|
def visible_from(self, defn, fn, n):
|
|
"Is specified definition visible from the specified file and line?"
|
|
if type(defn) == type(""):
|
|
defn = self.fileref[defn]
|
|
if defn.undef != None:
|
|
# Local macros are only visible in the file where they were defined
|
|
return defn.filename == fn and n >= defn.lineno and n <= defn.undef
|
|
if self.exports(defn.namespace):
|
|
# Macros and resources in subtrees with export=yes are global
|
|
return True
|
|
elif not self.filelist.neighbors(defn.filename, fn):
|
|
# Otherwise, must be in the same subtree.
|
|
return False
|
|
else:
|
|
# If the two files are in the same subtree, assume visibility.
|
|
# This doesn't match the actual preprocessor semantics.
|
|
# It means any macro without an undef is visible anywhere in the
|
|
# same argument directory.
|
|
#
|
|
# We can't do better than this without a lot of hairy graph-
|
|
# coloring logic to simulate include path interpretation.
|
|
# If that logic ever gets built, it will go here.
|
|
return True
|
|
def scan_for_definitions(self, namespace, filename):
|
|
ignoreflag = False
|
|
conditionalsflag = False
|
|
dfp = open(filename)
|
|
state = "outside"
|
|
latch_unit = in_base_unit = in_theme = False
|
|
for (n, line) in enumerate(dfp):
|
|
if self.warnlevel > 1:
|
|
print `line`[1:-1]
|
|
if line.strip().startswith("#textdomain"):
|
|
continue
|
|
m = re.search("# *wmlscope: warnlevel ([0-9]*)", line)
|
|
if m:
|
|
self.warnlevel = int(m.group(1))
|
|
print '"%s", line %d: warnlevel set to %d (definition-gathering pass)' \
|
|
% (filename, n+1, self.warnlevel)
|
|
continue
|
|
m = re.search("# *wmlscope: set *([^=]*)=(.*)", line)
|
|
if m:
|
|
prop = m.group(1).strip()
|
|
value = m.group(2).strip()
|
|
if namespace not in self.properties:
|
|
self.properties[namespace] = {}
|
|
self.properties[namespace][prop] = value
|
|
m = re.search("# *wmlscope: prune (.*)", line)
|
|
if m:
|
|
name = m.group(1)
|
|
if self.warnlevel >= 2:
|
|
print '"%s", line %d: pruning definitions of %s' \
|
|
% (filename, n+1, name )
|
|
if name not in self.xref:
|
|
print >>sys.stderr, "wmlscope: can't prune undefined macro %s" % name
|
|
else:
|
|
self.xref[name] = self.xref[name][:1]
|
|
continue
|
|
if "# wmlscope: start conditionals" in line:
|
|
if self.warnlevel > 1:
|
|
print '"%s", line %d: starting conditionals' \
|
|
% (filename, n+1)
|
|
conditionalsflag = True
|
|
elif "# wmlscope: stop conditionals" in line:
|
|
if self.warnlevel > 1:
|
|
print '"%s", line %d: stopping conditionals' \
|
|
% (filename, n+1)
|
|
conditionalsflag = False
|
|
if "# wmlscope: start ignoring" in line:
|
|
if self.warnlevel > 1:
|
|
print '"%s", line %d: starting ignoring (definition pass)' \
|
|
% (filename, n+1)
|
|
ignoreflag = True
|
|
elif "# wmlscope: stop ignoring" in line:
|
|
if self.warnlevel > 1:
|
|
print '"%s", line %d: stopping ignoring (definition pass)' \
|
|
% (filename, n+1)
|
|
ignoreflag = False
|
|
elif ignoreflag:
|
|
continue
|
|
if line.strip().startswith("#define"):
|
|
tokens = line.split()
|
|
if len(tokens) < 2:
|
|
print >>sys.stderr, \
|
|
'"%s", line %d: malformed #define' \
|
|
% (filename, n+1)
|
|
else:
|
|
name = tokens[1]
|
|
here = Reference(namespace, filename, n+1, line, args=tokens[2:])
|
|
here.hash = hashlib.md5()
|
|
here.docstring = line.lstrip()[8:] # Strip off #define_
|
|
state = "macro_header"
|
|
continue
|
|
elif state != 'outside' and line.strip().endswith("#enddef"):
|
|
here.hash.update(line)
|
|
here.hash = here.hash.digest()
|
|
if name in self.xref:
|
|
for defn in self.xref[name]:
|
|
if not self.visible_from(defn, filename, n+1):
|
|
continue
|
|
elif conditionalsflag:
|
|
continue
|
|
elif defn.hash != here.hash:
|
|
print >>sys.stderr, \
|
|
"%s: overrides different %s definition at %s" \
|
|
% (here, name, defn)
|
|
elif self.warnlevel > 0:
|
|
print >>sys.stderr, \
|
|
"%s: duplicates %s definition at %s" \
|
|
% (here, name, defn)
|
|
if name not in self.xref:
|
|
self.xref[name] = []
|
|
self.xref[name].append(here)
|
|
state = "outside"
|
|
elif state == "macro_header" and line.strip() and line.strip()[0] != "#":
|
|
state = "macro_body"
|
|
if state == "macro_header":
|
|
# Ignore macro header commends that are pragmas
|
|
if "wmlscope" not in line and "wmllint:" not in line:
|
|
here.docstring += line.lstrip()[1:]
|
|
if state in ("macro_header", "macro_body"):
|
|
here.hash.update(line)
|
|
elif line.strip().startswith("#undef"):
|
|
tokens = line.split()
|
|
name = tokens[1]
|
|
if name in self.xref and self.xref[name]:
|
|
self.xref[name][-1].undef = n+1
|
|
else:
|
|
print "%s: unbalanced #undef on %s" \
|
|
% (Reference(namespace, filename, n+1), name)
|
|
if state == 'outside':
|
|
if '[unit_type]' in line:
|
|
latch_unit = True
|
|
elif '[/unit_type]' in line:
|
|
latch_unit = False
|
|
elif '[base_unit]' in line:
|
|
in_base_unit = True
|
|
elif '[/base_unit]' in line:
|
|
in_base_unit = False
|
|
elif '[theme]' in line:
|
|
in_theme = True
|
|
elif '[/theme]' in line:
|
|
in_theme = False
|
|
elif latch_unit and not in_base_unit and not in_theme and "id" in line:
|
|
m = CrossRef.tag_parse.search(line)
|
|
if m and m.group(1) == "id":
|
|
uid = m.group(2)
|
|
if uid not in self.unit_ids:
|
|
self.unit_ids[uid] = []
|
|
self.unit_ids[uid].append(Reference(namespace, filename, n+1))
|
|
latch_unit= False
|
|
dfp.close()
|
|
def __init__(self, dirpath=[], exclude="", warnlevel=0):
|
|
"Build cross-reference object from the specified filelist."
|
|
self.filelist = Forest(dirpath, exclude)
|
|
self.dirpath = filter(lambda x: not re.search(exclude, x), dirpath)
|
|
self.warnlevel = warnlevel
|
|
self.xref = {}
|
|
self.fileref = {}
|
|
self.noxref = False
|
|
self.properties = {}
|
|
self.unit_ids = {}
|
|
all_in = []
|
|
if self.warnlevel >=2:
|
|
print "*** Beginning definition-gathering pass..."
|
|
for (namespace, filename) in self.filelist.generator():
|
|
all_in.append((namespace, filename))
|
|
if self.warnlevel > 1:
|
|
print filename + ":"
|
|
if isresource(filename):
|
|
self.fileref[filename] = Reference(namespace, filename)
|
|
elif iswml(filename):
|
|
# It's a WML file, scan for macro definitions
|
|
self.scan_for_definitions(namespace, filename)
|
|
elif filename.endswith(".def"):
|
|
# It's a list of names to be considered defined
|
|
self.noxref = True
|
|
dfp = open(filename)
|
|
for line in dfp:
|
|
self.xref[line.strip()] = True
|
|
dfp.close()
|
|
# Next, decorate definitions with all references from the filelist.
|
|
self.unresolved = []
|
|
self.missing = []
|
|
formals = []
|
|
state = "outside"
|
|
if self.warnlevel >=2:
|
|
print "*** Beginning reference-gathering pass..."
|
|
for (ns, fn) in all_in:
|
|
if iswml(fn):
|
|
rfp = open(fn)
|
|
attack_name = None
|
|
beneath = 0
|
|
ignoreflag = False
|
|
for (n, line) in enumerate(rfp):
|
|
if line.strip().startswith("#define"):
|
|
formals = line.strip().split()[2:]
|
|
elif line.startswith("#enddef"):
|
|
formals = []
|
|
comment = ""
|
|
if '#' in line:
|
|
if "# wmlscope: start ignoring" in line:
|
|
if self.warnlevel > 1:
|
|
print '"%s", line %d: starting ignoring (reference pass)' \
|
|
% (fn, n+1)
|
|
ignoreflag = True
|
|
elif "# wmlscope: stop ignoring" in line:
|
|
if self.warnlevel > 1:
|
|
print '"%s", line %d: stopping ignoring (reference pass)' \
|
|
% (fn, n+1)
|
|
ignoreflag = False
|
|
m = re.search("# *wmlscope: self.warnlevel ([0-9]*)", line)
|
|
if m:
|
|
self.warnlevel = int(m.group(1))
|
|
print '"%s", line %d: self.warnlevel set to %d (reference-gathering pass)' \
|
|
% (fn, n+1, self.warnlevel)
|
|
continue
|
|
fields = line.split('#')
|
|
line = fields[0]
|
|
if len(fields) > 1:
|
|
comment = fields[1]
|
|
if ignoreflag or not line:
|
|
continue
|
|
# Find references to macros
|
|
for match in re.finditer(CrossRef.macro_reference, line):
|
|
name = match.group(1)
|
|
candidates = 0
|
|
if self.warnlevel >=2:
|
|
print '"%s", line %d: seeking definition of %s' \
|
|
% (fn, n+1, name)
|
|
if name in formals:
|
|
continue
|
|
elif name in self.xref:
|
|
# Count the number of actual arguments.
|
|
# Set args to None if the call doesn't
|
|
# close on this line
|
|
brackdepth = parendepth = 0
|
|
instring = False
|
|
args = []
|
|
arg = ""
|
|
for i in range(match.start(0), len(line)):
|
|
if instring:
|
|
if line[i] == '"':
|
|
instring = False
|
|
else:
|
|
arg += line[i]
|
|
elif line[i] == '"':
|
|
instring = not instring
|
|
elif line[i] == "{":
|
|
if brackdepth > 0:
|
|
arg += line[i]
|
|
brackdepth += 1
|
|
elif line[i] == "}":
|
|
brackdepth -= 1
|
|
if brackdepth == 0:
|
|
if not line[i-1].isspace():
|
|
args.append(arg)
|
|
arg = ""
|
|
break
|
|
else:
|
|
arg += line[i]
|
|
elif line[i] == "(":
|
|
parendepth += 1
|
|
elif line[i] == ")":
|
|
parendepth -= 1
|
|
elif not line[i-1].isspace() and \
|
|
line[i].isspace() and \
|
|
brackdepth == 1 and \
|
|
parendepth == 0:
|
|
args.append(arg)
|
|
arg = ""
|
|
elif not line[i].isspace():
|
|
arg += line[i]
|
|
if brackdepth > 0 or parendepth > 0:
|
|
args = None
|
|
else:
|
|
args.pop(0)
|
|
#if args:
|
|
# print '"%s", line %d: args of %s is %s' \
|
|
# % (fn, n+1, name, args)
|
|
# Figure out which macros might resolve this
|
|
for defn in self.xref[name]:
|
|
if self.visible_from(defn, fn, n+1):
|
|
candidates += 1
|
|
defn.append(fn, n+1, args)
|
|
if candidates > 1:
|
|
print "%s: more than one definition of %s is visible here." % (Reference(ns, fn, n), name)
|
|
if candidates == 0:
|
|
self.unresolved.append((name,Reference(ns,fn,n+1,args=args)))
|
|
# Don't be fooled by HTML image references in help strings.
|
|
if "<img>" in line:
|
|
continue
|
|
# Find references to resource files
|
|
for match in re.finditer(CrossRef.file_reference, line):
|
|
name = match.group(0)
|
|
# Catches maps that look like macro names.
|
|
if (name.endswith(".map") or name.endswith(".mask")) and name[0] == '{':
|
|
name = name[1:]
|
|
key = None
|
|
# If name is already in our resource list, it's easy.
|
|
if name in self.fileref and self.visible_from(name, fn, n):
|
|
self.fileref[name].append(fn, n+1)
|
|
continue
|
|
# If the name contains subtitutable parts, count
|
|
# it as a reference to everything the substitutions
|
|
# could potentially match.
|
|
elif '{' in name:
|
|
pattern = re.sub(r"\{[^}]*\}", '.*', name)
|
|
key = self.mark_matching_resources(pattern, fn,n+1)
|
|
if key:
|
|
self.fileref[key].append(fn, n+1)
|
|
else:
|
|
candidates = []
|
|
for trial in self.fileref:
|
|
if trial.endswith(os.sep + name) and self.visible_from(trial, fn, n):
|
|
key = trial
|
|
self.fileref[trial].append(fn, n+1)
|
|
candidates.append(trial)
|
|
if len(candidates) > 1:
|
|
print "%s: more than one resource matching %s is visible here (%s)." % (Reference(ns,fn, n), name, ", ".join(candidates))
|
|
if not key:
|
|
self.missing.append((name, Reference(ns,fn,n+1)))
|
|
# Notice implicit references through attacks
|
|
if state == "outside":
|
|
if "[attack]" in line:
|
|
beneath = 0
|
|
attack_name = default_icon = None
|
|
have_icon = False
|
|
elif "name=" in line and not "no-icon" in comment:
|
|
attack_name = line[line.find("name=")+5:].strip()
|
|
default_icon = os.path.join("attacks", attack_name + ".png")
|
|
elif "icon=" in line and beneath == 0:
|
|
have_icon = True
|
|
elif "[/attack]" in line:
|
|
if attack_name and not have_icon:
|
|
candidates = []
|
|
key = None
|
|
for trial in self.fileref:
|
|
if trial.endswith(os.sep + default_icon) and self.visible_from(trial, fn, n):
|
|
key = trial
|
|
self.fileref[trial].append(fn, n+1)
|
|
candidates.append(trial)
|
|
if len(candidates) > 1:
|
|
print "%s: more than one definition of %s is visible here (%s)." % (Reference(ns,fn, n), name, ", ".join(candidates))
|
|
if not key:
|
|
self.missing.append((default_icon, Reference(ns,fn,n+1)))
|
|
elif line.strip().startswith("[/"):
|
|
beneath -= 1
|
|
elif line.strip().startswith("["):
|
|
beneath += 1
|
|
rfp.close()
|
|
# Check whether each namespace has a defined export property
|
|
for namespace in self.dirpath:
|
|
if namespace not in self.properties or "export" not in self.properties[namespace]:
|
|
print "warning: %s has no export property" % namespace
|
|
def exports(self, namespace):
|
|
return namespace in self.properties and self.properties[namespace].get("export") == "yes"
|
|
def subtract(self, filelist):
|
|
|
|
"Transplant file references in files from filelist to a new CrossRef."
|
|
smallref = CrossRef()
|
|
for filename in self.fileref:
|
|
for (referrer, referlines) in self.fileref[filename].references.items():
|
|
if referrer in filelist:
|
|
if filename not in smallref.fileref:
|
|
smallref.fileref[filename] = Reference(None, filename)
|
|
smallref.fileref[filename].references[referrer] = referlines
|
|
del self.fileref[filename].references[referrer]
|
|
return smallref
|
|
def refcount(self, name):
|
|
"Return a reference count for the specified resource."
|
|
try:
|
|
return len(self.fileref[name].references.keys())
|
|
except KeyError:
|
|
return 0
|
|
|
|
#
|
|
# String translations from po files. The advantage of this code is that it
|
|
# does not require the gettext binary message catalogs to have been compiled.
|
|
# The disavantage is that it eats lots of core!
|
|
#
|
|
|
|
|
|
class TranslationError(Exception):
|
|
def __init__(self, textdomain, isocode):
|
|
self.isocode = isocode
|
|
self.textdomain = textdomain
|
|
def __str__(self):
|
|
return "No translations found for %s/%s.\n" % (
|
|
self.textdomain, self.isocode)
|
|
|
|
class Translation(dict):
|
|
"Parses a po file to create a translation dictionary."
|
|
def __init__(self, textdomain, isocode, topdir=""):
|
|
self.textdomain = textdomain
|
|
self.isocode = isocode
|
|
self.gettext = {}
|
|
if self.isocode != "C":
|
|
isocode2 = isocode[:isocode.rfind("_")]
|
|
for code in [isocode, isocode2]:
|
|
fn = "po/%s/%s.po" % (textdomain, code)
|
|
if topdir: fn = os.path.join(topdir, fn)
|
|
try:
|
|
f = file(fn)
|
|
break
|
|
except IOError:
|
|
pass
|
|
else:
|
|
raise TranslationError(textdomain, self.isocode)
|
|
|
|
expect = False
|
|
fuzzy = "#, fuzzy\n"
|
|
gettext = f.read().decode("utf8")
|
|
matches = re.compile("""(msgid|msgstr)((\s*".*?")+)""").finditer(gettext)
|
|
msgid = ""
|
|
for match in matches:
|
|
text = "".join(re.compile('"(.*?)"').findall(match.group(2)))
|
|
if match.group(1) == "msgid":
|
|
msgid = text.replace("\\n", "\n")
|
|
expect = gettext[match.start(1) - len(fuzzy):match.start(1)] != fuzzy
|
|
elif expect:
|
|
self.gettext[msgid] = text.replace("\\n", "\n")
|
|
def get(self, key, dflt):
|
|
if self.isocode == "C":
|
|
if key:
|
|
return key[key.find("^") + 1:]
|
|
return "?"
|
|
else:
|
|
t = self.gettext.get(key, dflt)
|
|
if not t:
|
|
if key:
|
|
return key[key.find("^") + 1:]
|
|
return "?"
|
|
return t
|
|
def __getitem__(self, key):
|
|
if self.isocode == "C":
|
|
return key
|
|
else:
|
|
return self.gettext[key]
|
|
def __contains__(self, key):
|
|
if self.isocode == "C":
|
|
return True
|
|
else:
|
|
return key in self.gettext.keys()
|
|
|
|
class Translations:
|
|
"Wraps around Translation to support multiple languages and domains."
|
|
def __init__(self, topdir = ""):
|
|
self.translations = {}
|
|
self.topdir = topdir
|
|
def get(self, textdomain, isocode, key, default):
|
|
t = (textdomain, isocode)
|
|
if not t in self.translations:
|
|
try:
|
|
self.translations[t] = Translation(textdomain, isocode, self.topdir)
|
|
except TranslationError, e:
|
|
sys.stderr.write(str(e))
|
|
self.translations[t] = Translation(textdomain, "C", self.topdir)
|
|
result = self.translations[t].get(key, default)
|
|
return result
|
|
|
|
## Namespace management
|
|
#
|
|
# This is the only part of the code that actually knows about the
|
|
# shape of the data tree.
|
|
|
|
def scopelist():
|
|
"Return a list of (separate) package scopes, core first."
|
|
return ["data/core"] + glob.glob("data/campaigns/*")
|
|
|
|
def is_namespace(name):
|
|
"Is the name either a valid campaign name or core?"
|
|
return name in map(os.path.basename, scopelist())
|
|
|
|
def namespace_directory(name):
|
|
"Go from namespace to directory."
|
|
if name == "core":
|
|
return "data/core/"
|
|
else:
|
|
return "data/campaigns/" + name + "/"
|
|
|
|
def directory_namespace(path):
|
|
"Go from directory to namespace."
|
|
if path.startswith("data/core/"):
|
|
return "core"
|
|
elif path.startswith("data/campaigns/"):
|
|
return path.split("/")[2]
|
|
else:
|
|
return None
|
|
|
|
def namespace_member(path, namespace):
|
|
"Is a path in a specified namespace?"
|
|
ns = directory_namespace(path)
|
|
return ns != None and ns == namespace
|
|
|
|
def resolve_unit_cfg(namespace, utype, resource=None):
|
|
"Get the location of a specified unit in a specified scope."
|
|
if resource:
|
|
resource = os.path.join(utype, resource)
|
|
else:
|
|
resource = utype
|
|
loc = namespace_directory(namespace) + "units/" + resource
|
|
if not loc.endswith(".cfg"):
|
|
loc += ".cfg"
|
|
return loc
|
|
|
|
def resolve_unit_image(namespace, subdir, resource):
|
|
"Construct a plausible location for given resource in specified namespace."
|
|
return os.path.join(namespace_directory(namespace), "images/units", subdir, resource)
|
|
|
|
# And this is for code that does syntax transformation
|
|
baseindent = " "
|
|
|
|
## Version-control hooks begin here.
|
|
#
|
|
# Change these if we move away from Subversion
|
|
|
|
if sys.platform.startswith("win"):
|
|
mv = "rename"
|
|
rm = "del"
|
|
else:
|
|
mv = "mv"
|
|
rm = "rm"
|
|
|
|
vcdir = ".svn"
|
|
|
|
def vcmove(src, dst):
|
|
"Move a file under version control. Only applied to unmodified files."
|
|
(path, base) = os.path.split(src)
|
|
if os.path.exists(os.path.join(path, ".svn")):
|
|
return "svn mv %s %s" % (src, dst)
|
|
else:
|
|
return mv + " " + src + " " + dst
|
|
|
|
def vcunmove(src, dst):
|
|
"Revert the result of a previous move (before commit)."
|
|
(path, base) = os.path.split(src)
|
|
if os.path.exists(os.path.join(path, ".svn")):
|
|
return "svn revert %s" % dst # Revert the add at the destination
|
|
return rm + " " + dst # Remove the moved copy
|
|
return "svn revert %s" % src # Revert the deletion
|
|
else:
|
|
return mv + " " + dst + " " + src
|
|
|
|
def vcdelete(src):
|
|
"Delete a file under version control."
|
|
(path, base) = os.path.split(src)
|
|
if os.path.exists(os.path.join(path, ".svn")):
|
|
return "svn rm %s" % src
|
|
else:
|
|
return rm + " " + src
|
|
|
|
def vcundelete(src):
|
|
"Revert the result of a previous delete (before commit)."
|
|
(path, base) = os.path.split(src)
|
|
if os.path.exists(os.path.join(path, ".svn")):
|
|
return "svn revert %s" % src # Revert the deletion
|
|
else:
|
|
return "echo 'can't undelete %s, not under version control'" % src
|
|
|
|
#
|
|
## Version-control hooks end here
|
|
|
|
# wmltools.py ends here
|