mirror of
https://github.com/wesnoth/wesnoth
synced 2025-05-15 09:02:30 +00:00

The main difference is that these functions return generators instead of lists. This actually breaks wmlscope's collision detection, which will be fixed in my next commit.
304 lines
12 KiB
Python
Executable File
304 lines
12 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""\
|
|
wmlindent - re-indent WML in a uniform way.
|
|
|
|
By Eric S. Raymond, June 2007.
|
|
|
|
Call with no arguments to filter WML on stdin to reindented WML on
|
|
stdout. If arguments are specified, they are taken to be files to be
|
|
re-indented in place; a directory name causes reindenting on all WML
|
|
beneath it.
|
|
|
|
The indent unit is four spaces. Absence of an option to change this is
|
|
deliberate; the purpose of this tool is to *prevent* style wars, not encourage
|
|
them.
|
|
|
|
On non-empty lines, this code never modifies anything but leading and
|
|
trailing whitespace. Leading whitespace will be regularized to the
|
|
current indent; trailing whitespace will be stripped. After processing
|
|
all lines will end with a Unix-style \n end-of-line marker.
|
|
|
|
Runs of entirely blank lines will be reduced to one blank line, except
|
|
in two cases where they will be discarded: (a) before WML closing
|
|
tags, and (b) after WML opening tags.
|
|
|
|
It is possible to wrap a section of lines in special comments so that
|
|
wmlindent will ignore them. You may need to do this for unbalanced
|
|
macros (it's better, though, to get rid of those where possible).
|
|
Use 'wmlindent: {start,stop} ignoring' anywhere in a comment.
|
|
|
|
It is also possible to declare custom openers an closers, e.g for macros
|
|
that are actually control constructs. To do this, use declarations
|
|
|
|
# wmlindent: opener "{EXCEPTIONAL_OPENER "
|
|
# wmlindent: closer "{EXCEPTIONAL_CLOSER "
|
|
|
|
The lines after an opener will be indented an extra level; a closer
|
|
and lines following will be indented one level less. Note that these
|
|
declare prefixes; any prefix match to the non-whitespace text of a line
|
|
will be recognized.
|
|
|
|
The public utility macros "{FOREACH" and "{NEXT" come as wired-in exceptions,
|
|
because it is not guaranteed that their indent declarations will be processed
|
|
before the macro library is reached.
|
|
|
|
Interrupting will be safe, as each reindenting will be done to a copy
|
|
that is atomically renamed when it's done. If the output file is identical
|
|
to the input, the output file will simply be deleted, so the timestamp
|
|
on the input file won't be touched.
|
|
|
|
The --dryrun option detects and reports files that would be changed
|
|
without changing them. The --verbose or -v option enables reporting
|
|
of files that are (or would be, under --dryrun) changed. With -v -v,
|
|
unchanged files are also reported. The --exclude option takes a regexp
|
|
and excludes files matching it.
|
|
|
|
Note: This does not include a parser. It will produce bad results on WML
|
|
that is syntactically unbalanced. Unbalanced double quotes that aren't part
|
|
of a multiline literal will also confuse it. You will receive warnings
|
|
if there's an indent open at end of file or if a closer occurs with
|
|
indent already zero; these two conditions strongly suggest unbalanced WML.
|
|
"""
|
|
|
|
from __future__ import print_function, unicode_literals
|
|
from future_builtins import filter, map, zip
|
|
|
|
import sys, os, getopt, filecmp, re, codecs
|
|
from wesnoth import wmltools
|
|
|
|
closer_prefixes = ["{NEXT "]
|
|
opener_prefixes = ["{FOREACH "]
|
|
|
|
def is_directive(str):
|
|
"Identify things that shouldn't be indented."
|
|
return str.startswith(("#ifdef", "#ifndef", "#ifhave", "#ifnhave", "#ifver", "#ifnver", "#else", "#endif", "#define", "#enddef", "#undef"))
|
|
|
|
def closer(str):
|
|
"Are we looking at a closing tag?"
|
|
if str.startswith("#"):
|
|
return False
|
|
elif str.startswith("[/") or str.startswith(")"):
|
|
return True
|
|
else:
|
|
return str.startswith(tuple(closer_prefixes))
|
|
|
|
def opener(str):
|
|
"Are we looking at an opening tag?"
|
|
if str.startswith("#"):
|
|
return False
|
|
# This logic is a bit obscure. The 'not "[/" in str' catches the
|
|
# obvious case of a closing tag, but it also catches the idiom
|
|
# [allow_undo][/allow_undo] which we want to treat as a no-op.
|
|
elif (str.startswith("[") and not closer(str) and not "[/" in str):
|
|
return True
|
|
# Trailing ( opens a scope to be closed by ).
|
|
elif str.endswith("(\n") and '#' not in str:
|
|
return True
|
|
else:
|
|
return str.startswith(tuple(opener_prefixes))
|
|
|
|
class bailout:
|
|
def __init__(self, filename, lineno, msg):
|
|
self.filename = filename
|
|
self.lineno = lineno
|
|
self.msg = msg
|
|
|
|
def reindent(name, infp, outfp):
|
|
"Reindent WML."
|
|
dostrip = True
|
|
in_lua = False
|
|
seen_wml = False
|
|
inmacro = False
|
|
ignoring = False
|
|
indent = ""
|
|
lasttag = ""
|
|
countlines = 0
|
|
countblanks = 0
|
|
multitag = re.compile(r"\[a-z]].*\[[a-z]") # Avoid triggering on arrays
|
|
for line in infp:
|
|
countlines += 1
|
|
# User may declare indentation exceptions
|
|
if "wmlindent: opener" in line:
|
|
tag = line.split('"')[1]
|
|
if verbose and not quiet:
|
|
print("wmlindent: declaring indent exception for %s" % tag, file=sys.stderr)
|
|
opener_prefixes.append(tag)
|
|
elif "wmlindent: closer" in line:
|
|
tag = line.split('"')[1]
|
|
if verbose and not quiet:
|
|
print("wmlindent: declaring outdent exception for %s" % tag, file=sys.stderr)
|
|
closer_prefixes.append(tag)
|
|
# Implement passthrough mode
|
|
if "wmlindent: start ignoring" in line:
|
|
ignoring = True
|
|
if countblanks > 0:
|
|
countblanks = 0
|
|
outfp.write("\n")
|
|
outfp.write(line)
|
|
continue
|
|
elif ignoring:
|
|
outfp.write(line)
|
|
if "wmlindent: stop ignoring" in line:
|
|
ignoring = False
|
|
continue
|
|
# Detect things we can't handle
|
|
if multitag.search(line):
|
|
raise bailout(name, countlines, "multiple tags on the line")
|
|
# Strip each line, unless we're in something like a multiline string.
|
|
if dostrip:
|
|
transformed = line.strip() + "\n"
|
|
else:
|
|
transformed = line
|
|
# Track whether we've seen real WML rather than just macro definitions
|
|
if transformed.startswith("#define"):
|
|
saved_indent = indent
|
|
indent = wmltools.baseindent
|
|
inmacro = True
|
|
# Be sure to ignore the newlines and comments
|
|
elif transformed.rstrip().endswith("#enddef") and transformed.find("#") == transformed.find("#enddef"):
|
|
indent = saved_indent
|
|
inmacro = False
|
|
elif not inmacro and transformed[0] in ('[', ']'):
|
|
seen_wml = True
|
|
# In the close case, we must compute new indent *before* emitting
|
|
# the new line so the close tag will be at the same level as the
|
|
# one that started the block.
|
|
if closer(transformed):
|
|
if indent == "":
|
|
print('wmlindent: "%s", line %d: close tag %s with indent already zero.' % (name, countlines, transformed.strip()), file=sys.stderr)
|
|
else:
|
|
indent = indent[:-len(wmltools.baseindent)]
|
|
# Cope with blank lines outside of multiline literals
|
|
if dostrip:
|
|
if transformed == "\n":
|
|
countblanks += 1
|
|
continue
|
|
elif countblanks > 0:
|
|
countblanks = 0
|
|
# All sequences of blank lines get mapped to one blank
|
|
# line, except (a) before closing tags and (b) after
|
|
# opening tags, In these cases they are ignored.
|
|
if not closer(transformed) and not opener(lasttag):
|
|
outfp.write("\n")
|
|
# Here's where we apply the current indent
|
|
if dostrip and transformed and not is_directive(transformed):
|
|
output = indent + transformed
|
|
else:
|
|
output = transformed
|
|
# Nuke trailing space and canonicalize to Unix-style end-of-line
|
|
if dostrip:
|
|
output = output.rstrip() + "\n"
|
|
# And ship the line
|
|
outfp.write(output)
|
|
# May need to indent based on the line we just saw.
|
|
if opener(transformed):
|
|
indent += wmltools.baseindent
|
|
# Compute the dostrip state likewise.
|
|
# We look for unbalanced string quotes.
|
|
if dostrip:
|
|
eligible = re.split("\s#", transformed)[0]
|
|
else:
|
|
eligible = transformed
|
|
if dostrip and "<<" in eligible and not ">>" in eligible.split("<<", 1)[1]:
|
|
dostrip = False
|
|
in_lua = True
|
|
elif in_lua:
|
|
if ">>" in eligible and not "<<" in eligible.split(">>", 1)[1]:
|
|
dostrip = True
|
|
in_lua = False
|
|
elif eligible.count('"') % 2:
|
|
dostrip = not dostrip
|
|
# Are we going to be immediately following a tag?
|
|
if opener(transformed) or closer(transformed):
|
|
lasttag = transformed
|
|
else:
|
|
lasttag = ""
|
|
# Pure macro files look like they have unbalanced indents. That's OK
|
|
if indent != "" and seen_wml:
|
|
print('wmlindent: "%s". line %d: end of file with indent nonzero.' % (name, countlines), file=sys.stderr)
|
|
|
|
def allwmlfiles(directory):
|
|
"Get names of all WML files under dir, or dir itself if not a directory."
|
|
datafiles = []
|
|
if not os.path.isdir(directory):
|
|
if directory.endswith(".cfg"):
|
|
datafiles.append(directory)
|
|
else:
|
|
for root, dirs, files in os.walk(directory):
|
|
if wmltools.vcdir in dirs:
|
|
dirs.remove(wmltools.vcdir)
|
|
for name in files:
|
|
if name.endswith(".cfg"):
|
|
datafiles.append(os.path.join(root, name))
|
|
return datafiles
|
|
|
|
def convertor(linefilter, arglist, exclude):
|
|
"Apply a filter to command-line arguments."
|
|
if not arglist:
|
|
linefilter("standard input", sys.stdin, sys.stdout)
|
|
else:
|
|
for arg in arglist:
|
|
for filename in allwmlfiles(arg):
|
|
if exclude and re.search(exclude, filename):
|
|
if verbose:
|
|
print("wmlindent: %s excluded" % filename, file=sys.stderr)
|
|
continue
|
|
else:
|
|
try:
|
|
with codecs.open(filename,"r","utf8") as infp, codecs.open(filename + ".out", "w","utf8") as outfp:
|
|
linefilter(filename, infp, outfp)
|
|
except bailout as e:
|
|
print('wmlindent: "%s", %d: %s' % (e.filename, e.lineno, e.msg), file=sys.stderr)
|
|
os.remove(filename + ".out")
|
|
except KeyboardInterrupt:
|
|
os.remove(filename + ".out")
|
|
print("wmlindent: %s interrupted" % filename, file=sys.stderr)
|
|
else:
|
|
if filecmp.cmp(filename, filename + ".out"):
|
|
if verbose >= 2:
|
|
print("wmlindent: %s unchanged" % filename, file=sys.stderr)
|
|
os.remove(filename + ".out")
|
|
else:
|
|
if not quiet:
|
|
print("wmlindent: %s changed" % filename, file=sys.stderr)
|
|
if dryrun:
|
|
os.remove(filename + ".out")
|
|
else:
|
|
os.remove(filename) # For Windows portability
|
|
# There's a tiny window open if you keyboard-
|
|
# interrupt here. It's unavoidable, because
|
|
# there's no known way to do an atomic rename
|
|
# under Windows when the target exists -- see
|
|
# Python manual 14.1.4::rename()
|
|
os.rename(filename + ".out", filename)
|
|
|
|
if __name__ == '__main__':
|
|
(options, arguments) = getopt.getopt(sys.argv[1:], "h?de:qv",
|
|
['help', 'dryrun', 'exclude=', 'quiet', 'verbose'])
|
|
verbose = 0
|
|
quiet = False
|
|
dryrun = False
|
|
exclude = []
|
|
for (opt, val) in options:
|
|
if opt in ('-?', '-h', '--help'):
|
|
print(__doc__)
|
|
sys.exit(0)
|
|
elif opt in ('-d', '--dryrun'):
|
|
dryrun = True
|
|
elif opt in ('-e', '--exclude'):
|
|
exclude.append(val)
|
|
elif opt in ('-q', '--quiet'):
|
|
quiet = True
|
|
elif opt in ('-v', '--verbose'):
|
|
verbose += 1
|
|
if dryrun:
|
|
verbose = max(1, verbose)
|
|
# in certain situations, Windows' command prompt appends a double quote
|
|
# to the command line parameters. This block takes care of this issue.
|
|
for i,arg in enumerate(arguments):
|
|
if arg.endswith('"'):
|
|
arguments[i] = arg[:-1]
|
|
convertor(lambda n, f1, f2: reindent(n, f1, f2),
|
|
arguments, "|".join(exclude))
|