2023-06-03 21:03:05 +02:00
|
|
|
import xml.etree.ElementTree as ET
|
2023-10-10 20:23:42 +02:00
|
|
|
import sys, os
|
2023-06-03 21:03:05 +02:00
|
|
|
|
|
|
|
warning_count = 0
|
|
|
|
|
2023-08-05 17:58:13 +02:00
|
|
|
KNOWN_NOT_LAYOUT = set([
|
2023-10-10 20:23:42 +02:00
|
|
|
"number_row", "numpad", "pin",
|
|
|
|
"bottom_row", "settings", "method",
|
|
|
|
"greekmath", "numeric", "emoji_bottom_row" ])
|
2023-08-05 17:58:13 +02:00
|
|
|
|
2023-06-03 21:03:05 +02:00
|
|
|
def warn(msg):
|
|
|
|
global warning_count
|
|
|
|
print(msg)
|
|
|
|
warning_count += 1
|
|
|
|
|
|
|
|
def key_list_str(keys):
|
|
|
|
return ", ".join(sorted(list(keys)))
|
|
|
|
|
|
|
|
def missing_some_of(keys, symbols, class_name=None):
|
|
|
|
if class_name is None:
|
|
|
|
class_name = "of [" + ", ".join(symbols) + "]"
|
|
|
|
missing = set(symbols).difference(keys)
|
|
|
|
if len(missing) > 0 and len(missing) != len(symbols):
|
|
|
|
warn("Layout includes some %s but not all, missing: %s" % (
|
|
|
|
class_name, key_list_str(missing)))
|
|
|
|
|
|
|
|
def missing_required(keys, symbols, msg):
|
|
|
|
missing = set(symbols).difference(keys)
|
|
|
|
if len(missing) > 0:
|
|
|
|
warn("%s, missing: %s" % (msg, key_list_str(missing)))
|
|
|
|
|
|
|
|
def unexpected_keys(keys, symbols, msg):
|
|
|
|
unexpected = set(symbols).intersection(keys)
|
|
|
|
if len(unexpected) > 0:
|
|
|
|
warn("%s, unexpected: %s" % (msg, key_list_str(unexpected)))
|
|
|
|
|
2024-01-10 23:00:40 +01:00
|
|
|
# Write to [keys] and [dup].
|
|
|
|
def parse_row_from_et(row, keys, dup):
|
|
|
|
for key in row:
|
|
|
|
for attr in key.keys():
|
|
|
|
if attr.startswith("key"):
|
|
|
|
k = key.get(attr).removeprefix("\\")
|
|
|
|
if k in keys: dup.add(k)
|
|
|
|
keys.add(k)
|
|
|
|
|
2023-06-03 21:03:05 +02:00
|
|
|
def parse_layout(fname):
|
|
|
|
keys = set()
|
2023-08-06 20:09:53 +02:00
|
|
|
dup = set()
|
2023-06-03 21:03:05 +02:00
|
|
|
root = ET.parse(fname).getroot()
|
|
|
|
if root.tag != "keyboard":
|
|
|
|
return None
|
|
|
|
for row in root:
|
2024-01-10 23:00:40 +01:00
|
|
|
parse_row_from_et(row, keys, dup)
|
|
|
|
return root, keys, dup
|
|
|
|
|
|
|
|
def parse_row(fname):
|
|
|
|
keys = set()
|
|
|
|
dup = set()
|
|
|
|
root = ET.parse(fname).getroot()
|
|
|
|
if root.tag != "row":
|
|
|
|
return None
|
|
|
|
parse_row_from_et(root, keys, dup)
|
2023-08-06 20:09:53 +02:00
|
|
|
return root, keys, dup
|
2023-06-03 21:03:05 +02:00
|
|
|
|
|
|
|
def check_layout(layout):
|
2023-08-06 20:09:53 +02:00
|
|
|
root, keys, dup = layout
|
|
|
|
if len(dup) > 0: warn("Duplicate keys: " + key_list_str(dup))
|
2023-06-03 21:03:05 +02:00
|
|
|
missing_some_of(keys, "~!@#$%^&*(){}`[]=\\-_;:/.,?<>'\"+|", "ASCII punctuation")
|
|
|
|
missing_some_of(keys, "0123456789", "digits")
|
2023-08-05 17:58:13 +02:00
|
|
|
missing_required(keys,
|
2024-06-09 13:18:25 +02:00
|
|
|
["loc esc", "loc tab", "backspace", "delete",
|
2023-08-05 17:58:13 +02:00
|
|
|
"f11_placeholder", "f12_placeholder"],
|
|
|
|
"Layout doesn't define some important keys")
|
2023-08-02 12:09:15 +02:00
|
|
|
unexpected_keys(keys,
|
|
|
|
["copy", "paste", "cut", "selectAll", "shareText",
|
2023-09-03 15:05:23 +02:00
|
|
|
"pasteAsPlainText", "undo", "redo" ],
|
2023-08-02 12:09:15 +02:00
|
|
|
"Layout contains editing keys")
|
|
|
|
unexpected_keys(keys,
|
|
|
|
[ "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9",
|
|
|
|
"f10", "f11", "f12" ],
|
|
|
|
"Layout contains function keys")
|
2024-01-10 00:24:13 +01:00
|
|
|
unexpected_keys(keys, [""], "Layout contains empty strings")
|
2024-01-22 20:55:36 +01:00
|
|
|
unexpected_keys(keys, ["loc"], "Special keyword cannot be a symbol")
|
|
|
|
unexpected_keys(keys, filter(lambda k: k.strip()!=k, keys), "Some keys contain whitespaces")
|
2023-06-03 21:03:05 +02:00
|
|
|
|
2024-01-10 23:00:40 +01:00
|
|
|
_, bottom_row_keys, _ = parse_row("res/xml/bottom_row.xml")
|
2023-06-03 21:03:05 +02:00
|
|
|
|
|
|
|
if root.get("bottom_row") == "false":
|
|
|
|
missing_required(keys, bottom_row_keys,
|
|
|
|
"Layout redefines the bottom row but some important keys are missing")
|
|
|
|
else:
|
|
|
|
unexpected_keys(keys, bottom_row_keys,
|
|
|
|
"Layout contains keys present in the bottom row")
|
|
|
|
|
2023-06-10 10:59:25 +02:00
|
|
|
if root.get("script") == None:
|
|
|
|
warn("Layout doesn't specify a script.")
|
|
|
|
|
2023-09-03 20:15:31 +02:00
|
|
|
for fname in sorted(sys.argv[1:]):
|
2023-10-10 20:23:42 +02:00
|
|
|
layout_id, _ = os.path.splitext(os.path.basename(fname))
|
|
|
|
if layout_id in KNOWN_NOT_LAYOUT:
|
2023-08-05 17:58:13 +02:00
|
|
|
continue
|
2023-06-03 21:03:05 +02:00
|
|
|
layout = parse_layout(fname)
|
|
|
|
if layout == None:
|
2023-10-10 20:23:42 +02:00
|
|
|
print("Not a layout file: %s" % layout_id)
|
2023-06-03 21:03:05 +02:00
|
|
|
else:
|
2023-10-10 20:23:42 +02:00
|
|
|
print("# %s" % layout_id)
|
2023-06-03 21:03:05 +02:00
|
|
|
warning_count = 0
|
|
|
|
check_layout(layout)
|
|
|
|
print("%d warnings" % warning_count)
|