mirror of
https://github.com/imputnet/helium.git
synced 2026-08-20 00:57:32 +02:00
192 lines
5.7 KiB
Python
192 lines
5.7 KiB
Python
# Copyright 2025 The Helium Authors
|
|
# You can use, redistribute, and/or modify this source code under
|
|
# the terms of the GPL-3.0 license that can be found in the LICENSE file.
|
|
"""Util library for name_substitution.py"""
|
|
|
|
import re
|
|
import sys
|
|
import xml.etree.ElementTree as xml
|
|
|
|
REPLACEMENT_REGEXES_STR = [
|
|
# stuff we don't want to replace
|
|
(r'(\w+) Root Program', r'\1_unreplace Root Program'),
|
|
(r'(\w+) Web( S|s)tore', r'\1_unreplace Web Store'),
|
|
(r'(\w+) Remote Desktop', r'\1_unreplace Remote Desktop'),
|
|
|
|
# main replacement(s)
|
|
(r'(\b)chrome://', r'\1helium://'),
|
|
(r'(?:Google )?Chrom(e|ium)(?!\w)', r'Helium'),
|
|
|
|
# post-replacement cleanup
|
|
(r'((?:Google )?Chrom(e|ium))_unreplace', r'\1'),
|
|
(r'_unreplace', r'')
|
|
]
|
|
|
|
REPLACEMENT_REGEXES = list(map(lambda line: (re.compile(line[0]), line[1]),
|
|
REPLACEMENT_REGEXES_STR))
|
|
|
|
|
|
def add_grit_to_path(tree):
|
|
"""Inserts the grit/ folder into the module loading path."""
|
|
grit_path = tree / 'tools' / 'grit'
|
|
if not grit_path.exists():
|
|
raise FileNotFoundError(f"grit directory not found at {grit_path}")
|
|
sys.path.insert(0, str(grit_path))
|
|
|
|
|
|
def strip_message_text_for_fp(text):
|
|
"""
|
|
Removes whitespace markers from GRIT text.
|
|
See for example: IDS_EXTENSIONS_SETTINGS_API_SECOND_LINE_START_PAGES
|
|
in //chrome/app/chromium_strings.grd.
|
|
"""
|
|
text = text.strip()
|
|
if text.startswith("'''"):
|
|
text = text[3:]
|
|
if text.endswith("'''"):
|
|
text = text[:-3]
|
|
return text.strip()
|
|
|
|
|
|
def compute_fp(message):
|
|
"""
|
|
Computes the fingerprint for a particular .grd <message>, which is
|
|
later used to replace the fingerprints used in .xtb l10n files.
|
|
"""
|
|
# We need to import this here, because it only becomes
|
|
# available after add_grit_to_path() is called.
|
|
# pylint: disable=import-outside-toplevel,import-error
|
|
import grit.extern.tclib
|
|
|
|
text = message.text or ''
|
|
for elem in message.findall('ph'):
|
|
text = text + elem.get('name').upper() + (elem.tail or '')
|
|
text = strip_message_text_for_fp(text)
|
|
|
|
meaning = message.get('meaning', '')
|
|
return grit.extern.tclib.GenerateMessageId(text, meaning)
|
|
|
|
|
|
def replace_text(text):
|
|
"""Replaces instances of Chrom(e | ium) with Helium in strings"""
|
|
had_match = False
|
|
for regex, replacement in REPLACEMENT_REGEXES:
|
|
if regex.search(text):
|
|
had_match = True
|
|
text = regex.sub(replacement, text)
|
|
|
|
return text, had_match
|
|
|
|
|
|
def should_skip_tail(elem):
|
|
"""
|
|
Whether the `tail` text should be left as-is on
|
|
a particular element.
|
|
"""
|
|
return elem.get('name') == 'BEGIN_LINK_CHROMIUM' \
|
|
and elem.tail == 'Chromium'
|
|
|
|
|
|
def replace_grit_message(msg):
|
|
"""
|
|
Replaces instances of Chrom(e | ium) with Helium
|
|
in a <message> or <translation> element (and its children).
|
|
"""
|
|
had_any_match = False
|
|
|
|
if msg.text:
|
|
text, match = replace_text(msg.text)
|
|
msg.text = text
|
|
had_any_match |= match
|
|
|
|
if msg.tail and not should_skip_tail(msg):
|
|
tail, match = replace_text(msg.tail)
|
|
msg.tail = tail
|
|
had_any_match |= match
|
|
|
|
for child in msg:
|
|
had_any_match |= replace_grit_message(child)
|
|
|
|
return had_any_match
|
|
|
|
|
|
def replace_xtb_translation(msg, fp_map):
|
|
"""
|
|
Replaces instances of Chrom(e | ium) with Helium in a
|
|
.xtb <translation>.
|
|
"""
|
|
if not replace_grit_message(msg):
|
|
return False
|
|
|
|
msg_id = msg.get('id')
|
|
if msg_id in fp_map:
|
|
msg.set('id', fp_map[msg_id])
|
|
return True
|
|
|
|
|
|
def get_parser():
|
|
"""Returns a parser configured to preserve comments."""
|
|
return xml.XMLParser(target=xml.TreeBuilder(insert_comments=True))
|
|
|
|
|
|
def replace_grit_tree(text):
|
|
"""Replaces instances of Chrom(e | ium) with Helium, where desired"""
|
|
xml_tree = xml.fromstring(text, get_parser())
|
|
fp_map = {}
|
|
|
|
for message in xml_tree.findall('.//message'):
|
|
old_fp = compute_fp(message)
|
|
if replace_grit_message(message):
|
|
new_fp = compute_fp(message)
|
|
if old_fp != new_fp:
|
|
fp_map[old_fp] = new_fp
|
|
|
|
return xml.tostring(xml_tree, encoding='unicode', xml_declaration=True), fp_map
|
|
|
|
|
|
def dedup_translations_in_place(translation, seen):
|
|
"""
|
|
Ensures the .xtb file never has duplicate fingerprints if two
|
|
strings are the same.
|
|
"""
|
|
|
|
# If there are colliding strings after name substitution, offset the
|
|
# additional translations so that they don't error out the build.
|
|
# This usually applies to strings that have one "Chromium" and one
|
|
# "Chrome" variant. Since we substituted both of those with Helium,
|
|
# their fingerprints collide and crash the build.
|
|
translation_id = translation.get('id')
|
|
while translation_id in seen and translation_id.isdigit():
|
|
translation_id = str(int(translation_id) + 1)
|
|
translation.set('id', translation_id)
|
|
seen.add(translation_id)
|
|
|
|
|
|
def replace_xtb_tree(text, fp_map):
|
|
"""Same as replace_grit_tree(), but on .xtb translation files."""
|
|
xml_tree = xml.fromstring(text, get_parser())
|
|
changed = False
|
|
ids_seen = set()
|
|
|
|
for translation in xml_tree.findall('.//translation'):
|
|
assert translation.get('id')
|
|
changed |= replace_xtb_translation(translation, fp_map)
|
|
dedup_translations_in_place(translation, ids_seen)
|
|
|
|
if changed:
|
|
return xml.tostring(xml_tree, encoding='unicode', xml_declaration=True)
|
|
|
|
return None
|
|
|
|
|
|
def merge_fp_maps(results):
|
|
"""
|
|
Takes all the results from substitute_file() calls, and merges
|
|
their fingerprint mappings.
|
|
"""
|
|
merged_fp_map = {}
|
|
for result in results:
|
|
_, fp_map = result
|
|
merged_fp_map.update(fp_map)
|
|
return merged_fp_map
|