#!/usr/bin/env python3
"""
One-shot repair for docs/index.html + website/index.html after Windows-1252
round-trips destroyed emojis.
What this does:
1. Reads docs/index.html as raw bytes.
2. Replaces literal '?' / '??' / '???' placeholders left over from the
cp1252 round-trip with the actual emoji that was there originally.
3. Replaces any lingering 0x97 byte (cp1252 em-dash) with proper UTF-8 em-dash.
4. Writes BOTH docs/index.html and website/index.html as UTF-8 *with BOM*.
The BOM is intentional: it forces every editor (including PS5 Set-Content
and Notepad) to detect UTF-8 on open, so a later round-trip cannot silently
re-encode to cp1252 and re-destroy the emojis.
This is paired with .editorconfig + .gitattributes to make UTF-8 sticky
repo-wide.
"""
from __future__ import annotations
import pathlib
import sys
ROOT = pathlib.Path(__file__).resolve().parent.parent
DOCS = ROOT / "docs" / "index.html"
WEB = ROOT / "website" / "index.html"
# Replacements are keyed on the FULL line text (after stripping trailing
# whitespace) so we never accidentally rewrite a different occurrence of '??'.
# Left side: exact current line content (one line, no newline)
# Right side: replacement (one line, no newline)
LINE_FIXES: list[tuple[str, str]] = [
# favicon SVG
(
" ?\">",
" \u2B21\">",
),
# NAV brand: hexagon + HAVEN
("
? HAVEN
",
"
\u2B21 HAVEN
"),
# Mobile menu toggle: hamburger
(" ",
" "),
# Hero: Download Haven
(" ? Download Haven",
" \u2B07 Download Haven"),
# Hero: Try it Live
(" ? Try it Live",
" \u25B6 Try it Live"),
# Community banner heading
("
?? Try Haven — No Download Required
",
"
\U0001F310 Try Haven — No Download Required
"),
# Join Community Server button
(" ? Join Community Server",
" \U0001F310 Join Community Server"),
# Channel-code hint
("
?? After signing up, enter this channel code to join:
",
"
\U0001F511 After signing up, enter this channel code to join:
"),
(" Built with ?? by Amnibro — thank you for your incredible work building the Haven Android app from the ground up.",
" Built with \u2764\uFE0F by Amnibro — thank you for your incredible work building the Haven Android app from the ground up."),
# Support card
(" ??",
" \u2764\uFE0F"),
(" ? Buy me a coffee on Ko-fi",
" \u2615 Buy me a coffee on Ko-fi"),
# Footer brand
(" ",
" "),
# Changelog v3.15.0 entry: "new ?? button opens" → media-gallery icon (frames)
("
v3.15.0 — Channel Media Gallery: new ?? button opens a five-tab modal (Photos, Videos, Audio, Files, Links) showing every shared media item in the channel. Personas: create up to 25 named alter-egos in your profile and send messages as one by typing >>Name your message, with persona badges and full moderation
",
"
v3.15.0 — Channel Media Gallery: new \U0001F5BC\uFE0F button opens a five-tab modal (Photos, Videos, Audio, Files, Links) showing every shared media item in the channel. Personas: create up to 25 named alter-egos in your profile and send messages as one by typing >>Name your message, with persona badges and full moderation
"),
]
# Positional fixes (in document order): list of (placeholder, replacement_emoji)
STEP_NUMS = ["1\uFE0F\u20E3", "2\uFE0F\u20E3", "3\uFE0F\u20E3"]
SNUMS = ["\U0001F512", "\U0001F4BB"] # secure (lock), what do you need (laptop)
CARDICONS = [
"\U0001F30D", # globe (host downloads, friends join from anywhere)
"\U0001F399\uFE0F", # mic for voice
"\U0001F512", # lock for private/E2E DMs
"\U0001F4F1", # mobile
]
FICONS_2CHAR = [
"\U0001F4E5", # Discord Import - inbox
"\U0001F3B5", # Music streaming - musical note
"\U0001F4AC", # Rich Chat - speech bubble
"\U0001F512", # Encrypted DMs - lock
"\U0001F3A8", # Themes - palette
"\U0001F4C1", # File Sharing - folder
"\U0001F517", # Link Previews - link
"\u26A1", # Slash Commands - lightning
"\U0001F50D", # Message Search - magnifying glass
"\U0001F4CC", # Pinned - pushpin
"\U0001F310", # Multi-Server - globe
"\U0001F514", # Notification Sounds - bell
"\U0001F3AE", # Games - game controller
]
FICONS_3CHAR = [
"\U0001F399\uFE0F", # Voice Chat - mic
"\U0001F5A5\uFE0F", # Screen Sharing - desktop
"\U0001F39E\uFE0F", # GIF Search - film frames
"\U0001F6E1\uFE0F", # Moderation - shield
]
def apply_line_fixes(text: str) -> str:
"""Apply per-line substitutions. Lines are processed once each."""
lines = text.split("\n")
# Strip trailing \r so we can compare line content cleanly
had_cr = [ln.endswith("\r") for ln in lines]
bare = [ln[:-1] if cr else ln for ln, cr in zip(lines, had_cr)]
step_idx = 0
snum_idx = 0
card_idx = 0
f2_idx = 0
f3_idx = 0
out = []
for ln in bare:
# Try the explicit table first
matched = False
for old, new in LINE_FIXES:
if ln == old:
ln = new
matched = True
break
# Resolve positional placeholders
if "__STEPNUM__" in ln:
ln = ln.replace("__STEPNUM__", STEP_NUMS[step_idx])
step_idx += 1
if "__SNUM__" in ln:
ln = ln.replace("__SNUM__", SNUMS[snum_idx])
snum_idx += 1
if "__CARDICON__" in ln:
ln = ln.replace("__CARDICON__", CARDICONS[card_idx])
card_idx += 1
if "__FICON__" in ln:
ln = ln.replace("__FICON__", FICONS_2CHAR[f2_idx])
f2_idx += 1
if "__FICON3__" in ln:
ln = ln.replace("__FICON3__", FICONS_3CHAR[f3_idx])
f3_idx += 1
out.append(ln)
# Re-attach \r where originals had it
out = [ln + "\r" if cr else ln for ln, cr in zip(out, had_cr)]
return "\n".join(out), step_idx, snum_idx, card_idx, f2_idx, f3_idx
def main() -> int:
raw = DOCS.read_bytes()
if raw.startswith(b"\xEF\xBB\xBF"):
text = raw[3:].decode("utf-8")
else:
# Original file had stray 0x97 (cp1252 em-dash) bytes; decode that way.
text = raw.decode("cp1252")
fixed, step_n, snum_n, card_n, f2_n, f3_n = apply_line_fixes(text)
print(f"step numbers replaced: {step_n}/3")
print(f"s-nums replaced: {snum_n}/{len(SNUMS)}")
print(f"card icons replaced: {card_n}/{len(CARDICONS)}")
print(f"f-icons (2ch): {f2_n}/{len(FICONS_2CHAR)}")
print(f"f-icons (3ch): {f3_n}/{len(FICONS_3CHAR)}")
# Write UTF-8 WITH BOM so future editor saves do not silently downgrade.
out_bytes = b"\xEF\xBB\xBF" + fixed.encode("utf-8")
DOCS.write_bytes(out_bytes)
WEB.write_bytes(out_bytes)
print(f"wrote {DOCS} ({len(out_bytes)} bytes)")
print(f"wrote {WEB} ({len(out_bytes)} bytes)")
return 0
if __name__ == "__main__":
sys.exit(main())