'''
HTML_CLEAR_FLOATS = ''
HTML_ENTITY_HORIZONTAL_BAR = '―'
HTML_ENTITY_MULTIPLICATION_SIGN = '×'
HTML_ENTITY_FIGURE_DASH = '‒'
PRE_PLACEHOLDER_CAMPAIGNS = "PLACE CAMPAIGNS HERE\n"
PRE_PLACEHOLDER_ERAS = "PLACE ERAS HERE\n"
def website_header(title='', path='../../', classes=[]):
"""Returns the website header with the specified parameters."""
return WESMERE_HEADER % {
"title": title,
"path": path,
"cssprefix": WESMERE_CSS_PREFIX,
"cssver": WESMERE_CSS_VERSION,
"classes": ' '.join(['wmlunits'] + classes)}
def website_footer():
"""Returns the website footer."""
return WESMERE_FOOTER
def build_timestamp():
"""Returns an element containing the current date and time."""
return '
Last updated on %s.
' % time.ctime()
all_written_html_files = []
error_only_once = {}
def error_message(message):
if message in error_only_once:
return
error_only_once[message] = 1
write_error(message)
helpers.error_message = error_message
def reset_errors():
error_only_once = {}
def int_fallback(str_value, int_fallback=0):
try:
return int(str_value)
except TypeError:
return int_fallback
except ValueError:
return int_fallback
def cleanurl(url):
"""
Encode the given URL to ensure it only contains valid URL characters
(also known as percent-encoding).
"""
if url is None:
return url
return urllib.parse.quote(url, encoding='utf-8')
def cleantext(text, quote=True):
"""Escape any HTML special characters in the given string."""
if text is None:
return text
return html.escape(text, quote)
def resistance_rating_color_class(resistance):
"""Return a color class adequate for the provided unit resistance value."""
if resistance < 0:
return 'red'
elif resistance <= 20:
return 'yellow'
elif resistance <= 40:
return 'olive'
else:
return 'green'
def defense_rating_color_class(defense):
"""Return a color class adequate for the provided terrain defense value."""
if defense <= 10:
return 'red'
elif defense <= 30:
return 'yellow'
elif defense <= 50:
return 'olive'
else:
return 'green'
def mvtcost_rating_color_class(str_mvtcost, str_moves):
"""Return a color class adequate for the provided movement cost value."""
cost = int_fallback(str_mvtcost, 99)
moves = int_fallback(str_moves)
if cost >= moves:
return 'gray'
elif cost > moves/2:
return 'red'
elif cost > 1:
return 'yellow'
else:
return 'green'
class MyFile:
"""
Python 2 is a bit weird with encodings, really should switch this to
Python 3.
"""
def __init__(self, filename, mode):
self.filename = filename
self.fileobj = open(filename, mode + "b")
def write(self, line):
self.fileobj.write(line.encode("utf8"))
def close(self):
self.fileobj.close()
class Translation:
def __init__(self, localedir, langcode):
self.catalog = {}
self.localedir = localedir
self.langcode = langcode
class Dummy:
def gettext(self, msgid):
if not msgid:
return ""
caret = msgid.find("^")
if caret < 0:
return msgid
return msgid[caret + 1:]
self.dummy = Dummy()
def translate(self, string, textdomain):
if textdomain not in self.catalog:
try:
self.catalog[textdomain] = gettext.translation(
textdomain, self.localedir, [self.langcode])
self.catalog[textdomain].add_fallback(self.dummy)
except IOError:
self.catalog[textdomain] = self.dummy
except AttributeError:
self.catalog[textdomain] = self.dummy
except IndexError:
# not sure why, but this happens within the
# gettext.translation call sometimes
self.catalog[textdomain] = self.dummy
except ValueError:
self.catalog[textdomain] = self.dummy
return self.catalog[textdomain].gettext(string)
class GroupByRace:
def __init__(self, wesnoth, campaign):
self.wesnoth = wesnoth
self.campaign = campaign
def unitfilter(self, unit):
if not self.campaign:
return True
return unit.campaigns and self.campaign == unit.campaigns[0]
def groups(self, unit):
return [T(unit.race, "plural_name")]
def group_name(self, group):
if not group:
return "None"
return group
class GroupByNothing:
def __init__(self):
pass
def unitfilter(self, unit):
return True
def groups(self, unit):
return ["units"]
def group_name(self, group):
return "units"
class GroupByFaction:
def __init__(self, wesnoth, era):
self.wesnoth = wesnoth
self.era = era
def unitfilter(self, unit):
return self.era in unit.eras
def groups(self, unit):
return [x for x in unit.factions if x[0] == self.era]
def group_name(self, group):
era = self.wesnoth.era_lookup[group[0]]
if group[1]:
faction = era.faction_lookup[group[1]]
name = T(faction, "name")
if name:
name = name[name.rfind("=") + 1:]
else:
name = "missing"
error_message("Warning: %s has no faction name\n" % group[1])
else:
name = "factionless"
return name
global_htmlout = None
def T(tag, att):
if not tag:
return "none"
return tag.get_text_val(att, translation=global_htmlout.translate)
class HTMLOutput:
def __init__(self, isocode, output, addon, campaign, is_era, wesnoth, verbose=False):
global global_htmlout
self.output = output
self.addon = addon
self.campaign = campaign
self.is_era = is_era
self.verbose = verbose
self.target = "index.html"
self.wesnoth = wesnoth
self.forest = None
self.translation = Translation(options.transdir, isocode)
self.isocode = isocode
global_htmlout = self
def translate(self, string, domain):
return self.translation.translate(string, domain)
def analyze_units(self, grouper, add_parents):
"""
This takes all units belonging to a campaign, then groups them either
by race or faction, and creates an advancements tree out of it.
"""
# Build an advancement tree forest of all units.
forest = self.forest = helpers.UnitForest()
units_added = {}
for uid, u in list(self.wesnoth.unit_lookup.items()):
if u.hidden:
continue
if grouper.unitfilter(u):
forest.add_node(helpers.UnitNode(u))
units_added[uid] = u
#print(" %d/%d units" % (len(units_added), len(self.wesnoth.unit_lookup)))
# Always add any child units, even if they have been filtered out..
while units_added:
new_units_added = {}
for uid, u in list(units_added.items()):
for auid in u.advance:
if not auid in forest.lookup:
try:
au = self.wesnoth.unit_lookup[auid]
except KeyError:
error_message(
"Warning: Unit %s not found as advancement of %s\n" %
(auid, repr(uid)))
continue
forest.add_node(helpers.UnitNode(au))
new_units_added[auid] = au
units_added = new_units_added
if add_parents:
# Also add parent units
added = True
while added:
added = False
for uid, u in list(self.wesnoth.unit_lookup.items()):
if uid in forest.lookup:
continue
for auid in u.advance:
if auid in forest.lookup:
forest.add_node(helpers.UnitNode(u))
added = True
break
forest.update()
# Partition trees by race/faction of first unit.
groups = {}
breadth = 0
for tree in list(forest.trees.values()):
u = tree.unit
ugroups = grouper.groups(u)
for group in ugroups:
groups[group] = groups.get(group, []) + [tree]
breadth += tree.breadth
thelist = list(groups.keys())
thelist.sort(key=lambda x: grouper.group_name(x))
rows_count = breadth + len(thelist)
# Create empty grid.
rows = []
for j in range(rows_count):
column = []
for i in range(6):
column.append((1, 1, None))
rows.append(column)
# Sort advancement trees by name of first unit and place into the grid.
def by_name(t):
x = T(t.unit, "name")
return "" if x is None else x
def grid_place(nodes, x):
nodes.sort(key=by_name)
for node in nodes:
level = max(0, min(5, node.unit.level))
rows[x][level] = (1, node.breadth, node)
for i in range(1, node.breadth):
rows[x + i][level] = (0, 0, node)
grid_place(node.children, x)
x += node.breadth
return x
x = 0
for group in thelist:
node = helpers.GroupNode(group)
node.name = grouper.group_name(group)
rows[x][0] = (6, 1, node)
for i in range(1, 6):
rows[x][i] = (0, 0, None)
nodes = groups[group]
x += 1
x = grid_place(nodes, x)
self.unitgrid = rows
return len(forest.lookup)
def write_navbar(self, report_type):
def write(line):
self.output.write(line)
def _(msgid, textdomain="wesnoth"):
return self.translate(msgid, textdomain)
all_written_html_files.append((self.isocode, self.output.filename))
languages = self.wesnoth.languages_found
langlist = list(languages.keys())
langlist.sort()
write('
')
write('
')
def abbrev(name):
abbrev = name[0]
word_separators = [" ", "_", "+", "(", ")"]
for i in range(1, len(name)):
if name[i] in ["+", "(", ")"] or \
name[i - 1] in word_separators and \
name[i] not in word_separators:
abbrev += name[i]
return abbrev
def add_menu(menuid, name, classes='', url='', is_table_container=False, container_classes=''):
"""
Writes the initial portion of a sidebar item, including the item's
label, the start tag for the popup menu container, and the label
for said menu.
If is_table_container=True, the popup menu prolog is suitable for
a table (currently used for the Language menu). This will be
removed one day, hopefully. (TODO)
The url parameter allows setting a destination URL for the menu
header, which will be a link if it is set to a non-empty string.
"""
html_name = cleantext(name)
html_classes = " ".join((cleantext(classes), "popuptrigger"))
write('
')
if not is_table_container:
if not container_classes:
write('
')
else:
write('
' % cleantext(container_classes))
def add_menuitem_placeholder():
"""Writes a horizontal bar serving as a menu placeholder."""
write('
' + HTML_ENTITY_HORIZONTAL_BAR + '
')
def add_menuitem(url, label, standalone=False, title=''):
"""
Writes a sidebar item.
If standalone=True, the item will be provided without the list
element tags so it can be used in pretty much any context. In
reality, the option is only provided for use with add_menu() with
is_table_container=True and it will be removed at some point in
the hopefully not-so-far future (TODO).
"""
if not standalone:
write('
')
extra_attr = (' title="%s"' % cleantext(title)) if title else ''
write('%s' %
(cleantext(url), extra_attr, cleantext(label, quote=False)))
if not standalone:
write('
')
def end_menu(is_table_container=False):
"""
Writes the closing tags for a menu started with start_menu().
The is_table_container value used here ought to be the same as the
original.
"""
if not is_table_container:
write('
')
for lang in langlist:
cell += 1
col += 1
write('
')
filename = self.target if self.addon == 'mainline' else 'mainline.html'
url = cleanurl('../%s/%s' % (lang, filename))
# TODO: Maybe use the language name instead of its code for the label?
add_menuitem(url, lang, title=languages[lang], standalone=True)
write('
')
if col >= colcount:
col = 0
if cell < len(langlist):
write('
')
if col:
for i in range(col + 1, colcount + 1):
write('
\n')
write('');
def pic(self, u, x, recursion=0):
if recursion >= 4:
error_message(
"Warning: Cannot find image for unit %s(%s).\n" % (
u.get_text_val("id"), x.name.decode("utf8")))
return None, None
image = self.wesnoth.get_unit_value(x, "image")
portrait = self.wesnoth.get_unit_value(x, "profile")
if not portrait:
bu = self.wesnoth.get_base_unit(u)
if bu:
portrait = self.wesnoth.get_unit_value(bu, "profile")
if not image:
if x.name == b"female":
baseunit = self.wesnoth.get_base_unit(u)
if baseunit:
female = baseunit.get_all(tag="female")
return self.pic(u, female[0], recursion=recursion + 1)
else:
return self.pic(u, u, recursion=recursion + 1)
error_message("Warning: Missing image for unit %s(%s).\n" %
(u.get_text_val("id"), x.name.decode("utf8")))
return None, None
icpic = image_collector.add_image_check(self.addon, image)
if not icpic.ipath:
error_message("Warning: No picture %s for unit %s.\n" %
(image, u.get_text_val("id")))
picname = icpic.id_name
image = os.path.join(PICS_LOCATION, picname)
if portrait:
picname = image_collector.add_image(self.addon,
portrait,
no_tc=True,
check_transparent=True)
portrait = os.path.join(PICS_LOCATION, picname)
return image, portrait
def get_abilities(self, u):
anames = []
already = {}
for abilities in u.get_all(tag="abilities"):
try:
c = abilities.get_all()
except AttributeError:
c = []
for ability in c:
try:
id = ability.get_text_val("id")
except AttributeError as e:
error_message("Error: Ignoring ability " + ability.debug())
continue
if id in already:
continue
already[id] = True
name = T(ability, "name")
if not name:
name = ability.name.decode("utf8")
# Only add abilities with a label, since those that lack one
# are normally hidden in the game and used to implement more
# complex ones.
if name:
anames.append(name)
return anames
def get_recursive_attacks(self, this_unit):
def copy_attributes(copy_from, copy_to):
for c in copy_from.data:
if isinstance(c, wmlparser3.AttributeNode):
copy_to.data.append(c)
# Use attacks of base_units as base, if we have one.
base_unit = self.wesnoth.get_base_unit(this_unit)
attacks = []
if base_unit:
attacks = copy.deepcopy(self.get_recursive_attacks(base_unit))
base_attacks_count = len(attacks)
for i, attack in enumerate(this_unit.get_all(tag="attack")):
# Attack merging is order based.
if i < base_attacks_count:
copy_attributes(attack, attacks[i])
else:
attacks.append(attack)
return attacks
def write_units(self):
def write(line):
self.output.write(line)
def _(msgid, textdomain="wesnoth"):
return self.translate(msgid, textdomain)
rows = self.unitgrid
write('
\n
')
for i in range(6):
write('
' % i)
write('
')
pic = image_collector.add_image("general",
"../../../images/misc/leader-crown.png",
no_tc=True)
crownimage = cleanurl(os.path.join(PICS_LOCATION, pic))
ms = None
for row in range(len(rows)):
write('
\n')
for column in range(6):
hspan, vspan, un = rows[row][column]
if vspan:
attributes = ""
if hspan == 1 and vspan == 1:
pass
elif hspan == 1:
attributes += ' rowspan="%d"' % vspan
elif vspan == 1:
attributes += ' colspan="%d"' % hspan
if un and isinstance(un, helpers.GroupNode):
# Find the current multiplayer side so we can show the
# little crowns..
ms = None
if self.is_era:
try:
eid, fid = un.data
era = self.wesnoth.era_lookup[eid]
if fid:
ms = era.faction_lookup[fid]
except TypeError:
pass
racename = un.name
# TODO: we need to use a unique race id instead of a potentially duplicate
# name for the header id and link target!
attributes += ' id="%s" class="raceheader"' % cleantext(racename)
write('
')
self.output.write(website_footer())
return n
def write_unit_report(self, output, unit):
def write(line):
self.output.write(line)
def _(msgid, textdomain="wesnoth"):
return self.translate(msgid, textdomain)
def find_attr(what, key):
if unit.movetype:
mtx = unit.movetype.get_all(tag=what)
mty = None
if mtx:
mty = mtx[0].get_text_val(key)
x = unit.get_all(tag=what)
y = None
if x:
y = x[0].get_text_val(key, translation=self.translation.translate)
if y:
return True, y
if unit.movetype and mty is not None:
return False, mty
return False, "-"
def uval(name):
return self.wesnoth.get_unit_value(unit, name, translation=self.translation.translate)
def clean_uval(name):
return cleantext(uval(name))
# Write unit name, picture and description.
uid = unit.get_text_val("id")
uname = uval("name")
display_name = cleantext(uname)
self.output = output
write(website_header(title=display_name,
classes=['wmlunits-unit']))
self.write_navbar("unit_report")
self.output.write('
')
female = unit.get_all(tag="female")
if female:
fname = T(female[0], "name")
if fname and fname != uname:
display_name += " / " + cleantext(fname)
write('
\n')
write('
\n')
write('
%s
\n' % display_name)
write('
')
if female:
mimage, portrait = self.pic(unit, unit)
fimage, fportrait = self.pic(unit, female[0])
if not fimage:
fimage = mimage
if not fportrait:
fportrait = portrait
write('\n' % cleanurl(mimage))
write('\n' % cleanurl(fimage))
image = mimage
else:
image, portrait = self.pic(unit, unit)
write('\n' % cleanurl(image))
write('
\n')
description = clean_uval("description")
# TODO: what is unit_description?
if not description:
description = clean_uval("unit_description")
if not description:
description = HTML_ENTITY_HORIZONTAL_BAR
write('
%s
\n' % re.sub('\n', '\n ', description))
write('
\n')
write('
\n')
for si in range(2):
if si and not female:
break
if si:
sportrait = fportrait
simage = fimage
else:
simage = image
sportrait = portrait
write('
')
if portrait:
write('\n' % cleanurl(sportrait))
else:
write('
' % cleanurl(simage))
write('
')
write('
\n')
write('
\n')
write('
\n
\n')
write('
Information
\n')
write('
\n')
# Advances-from list
write('
%s
' % cleantext(_("Advances from: "), quote=False))
have_advances = False
for pid in self.forest.get_parents(uid):
punit = self.wesnoth.unit_lookup[pid]
if "mainline" in unit.campaigns and "mainline" not in punit.campaigns:
continue
addon = "mainline" if "mainline" in unit.campaigns else self.addon
link = cleanurl("../../%s/%s/%s.html" % (addon, self.isocode, pid))
name = self.wesnoth.get_unit_value(punit, "name",
translation=self.translation.translate)
if have_advances:
write(', ')
write('%s' % (link, cleantext(name, quote=False)))
have_advances = True
if not have_advances:
write(HTML_ENTITY_FIGURE_DASH)
write('
\n')
# Advances-to list
write('
%s
' % cleantext(_("Advances to: "), quote=False))
have_advances = False
for cid in self.forest.get_children(uid):
try:
cunit = self.wesnoth.unit_lookup[cid]
addon = "mainline" if "mainline" in cunit.campaigns else self.addon
link = cleanurl("../../%s/%s/%s.html" % (addon, self.isocode, cid))
if "mainline" in unit.campaigns and "mainline" not in cunit.campaigns:
continue
name = self.wesnoth.get_unit_value(cunit, "name",
translation=self.translation.translate)
except KeyError:
error_message("Warning: Unit %s not found.\n" % cid)
name = cid
if "mainline" in unit.campaigns:
continue
link = cleanurl(self.target)
if have_advances:
write(', ')
write('%s' % (link, cleantext(name, quote=False)))
have_advances = True
if not have_advances:
write(HTML_ENTITY_FIGURE_DASH)
write('
\n')
attributes = [
("cost", _("Cost: ")),
("hitpoints", _("HP: ")),
("movement", _("Moves: ")),
("vision", _("Vision: ")),
("jamming", _("Jamming: ")),
("experience", _("XP: ")),
("level", _("Level: ")),
("alignment", _("Alignment: ")),
("id", "Id: ")
]
for attr, label in attributes:
value = uval(attr)
if not value and attr in ("jamming", "vision"):
continue
if attr == "alignment":
value = _(value)
write('
%s
%s
\n' % (cleantext(label, quote=False), cleantext(value, quote=False)))
# Write info about abilities.
anames = self.get_abilities(unit)
write('
\n')
write('
%s
' % cleantext(_("Abilities: "), quote=False))
if len(anames):
write('
' + cleantext(', '.join(anames), quote=False) + '
')
else:
write('
' + HTML_ENTITY_FIGURE_DASH + '
')
write('
\n')
write('
\n')
# Write info about attacks.
attacks = self.get_recursive_attacks(unit)
if attacks:
write('
' % (range_icon, range_alt_text, cleantext(_(r), quote=False))
write(x)
s = []
specials = attack.get_all(tag="specials")
if specials:
for special in specials[0].get_all(tag=""):
sname = T(special, "name")
if sname:
s.append(cleantext(sname, quote=False))
else:
error_message("Warning: Weapon special %s has no name for %s.\n" %
(special.name.decode("utf8"), uid))
if s:
write('
(%s)
' % ', '.join(s))
write('
')
write('
\n')
# Write info about resistances.
write('
%s
\n' % _("Resistances: ").strip(" :"))
write('
\n')
write('
')
row = 0
for rid, ricon in RESISTANCES:
special, resist_str = find_attr("resistance", rid)
r = 100 if resist_str == '-' else 100 - int(resist_str)
resist_classes = ['num']
resist_rating = resistance_rating_color_class(r)
if resist_rating:
resist_classes.append('rating-' + resist_rating)
try:
resist_str = '%d%%' % r
except ValueError:
error_message("Warning: Invalid resistance %s for %s.\n" % (
r, uid))
rcell = "td"
if special:
rcell += ' class="special"'
if row % 2 == 0:
write('
\n' % (
cleantext(campname), url, cleantext(campname, quote=False))
if i == 0 and cids[1]:
chtml += '
'
eids = [[], []]
for addon in batchlist:
for era in addon.get("eras", []):
if era["units"] == "?" or era["units"] <= 0:
continue
lang = isocode if addon["name"] == "mainline" else "en_US"
e = addon["name"], era["id"], era["translations"].get(
lang, era["name"]), lang
if addon["name"] == "mainline":
eids[0].append(e)
else:
eids[1].append(e)
for i in range(2):
eras = eids[i]
eras.sort(key=lambda x: x[2])
for era in eras:
addon, eid, eraname, lang = era
url = cleanurl("../../%s/%s/%s.html" % (addon, lang, eid))
ehtml += '