diff --git a/plugins/clone-app/skills/clone-app/scripts/extract-design.py b/plugins/clone-app/skills/clone-app/scripts/extract-design.py new file mode 100644 index 0000000..5aa6833 --- /dev/null +++ b/plugins/clone-app/skills/clone-app/scripts/extract-design.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +"""Extract a design-token digest from a decompiled APK resource tree. + +Reads res/values/{colors,dimens,styles,themes}.xml, the res/font/ dir, the +drawable*/mipmap* inventory, and counts res/layout/*.xml. Stdlib-only. + +Emits a design-tokens.json object on stdout (or --out FILE) and, with --digest +FILE, a markdown design summary. Framework-aware: native apps have a rich XML +layer (confidence high); Compose keeps tokens in res/values but no layouts +(med); Flutter/React Native keep almost nothing in Android res (low) so the +caller leans on screenshots. Pass --framework to override the auto guess. +""" +import sys, os, json, re, argparse, glob +import xml.etree.ElementTree as ET + +def _find_res_dirs(root): + """All res/ dirs under root that contain a values/ subdir (jadx puts these + under /resources/res; apktool under /res โ€” search either).""" + hits = [] + for p in glob.glob(os.path.join(root, "**", "res"), recursive=True): + if os.path.isdir(os.path.join(p, "values")) or glob.glob(os.path.join(p, "values*")): + hits.append(p) + return sorted(set(hits)) + +def _iter_value_files(res_dirs, basename): + for res in res_dirs: + for vdir in glob.glob(os.path.join(res, "values*")): + f = os.path.join(vdir, basename) + if os.path.isfile(f): + yield f + +def _parse_named(res_dirs, basename, tag): + """Collect value entries from every values*/basename.""" + out = {} + for f in _iter_value_files(res_dirs, basename): + try: + tree = ET.parse(f) + except ET.ParseError: + continue + for el in tree.getroot().iter(tag): + name = el.get("name") + if name and (el.text is not None): + out[name] = el.text.strip() + return out + +def _parse_themes(res_dirs): + """First + diff --git a/plugins/clone-app/tests/test-extract-design.py b/plugins/clone-app/tests/test-extract-design.py new file mode 100644 index 0000000..40606e7 --- /dev/null +++ b/plugins/clone-app/tests/test-extract-design.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +import json, subprocess, sys, os + +HERE = os.path.dirname(os.path.abspath(__file__)) +SCRIPT = os.path.join(HERE, "..", "skills", "clone-app", "scripts", "extract-design.py") +ROOT = os.path.join(HERE, "fixtures", "design-sample") + +def run(): + out = subprocess.check_output([sys.executable, SCRIPT, ROOT]) + return json.loads(out) + +def main(): + d = run() + fails = [] + def check(name, cond): + print(f"{'PASS' if cond else 'FAIL'}: {name}") + if not cond: fails.append(name) + check("source", d["source"] == "apk-resources") + check("colors parsed", d["colors"]["values"].get("colorPrimary") == "#FF6200EE") + check("colors accent", d["colors"]["values"].get("colorAccent") == "#FF03DAC5") + check("colors confidence high", d["colors"]["confidence"] == "high") + check("dimens parsed", d["dimens"]["values"].get("spacing_small") == "8dp") + check("text size dimen", d["dimens"]["values"].get("text_size_body") == "14sp") + check("theme parent", d["theme"]["values"].get("parent") == "Theme.Material3.DayNight") + check("theme is_dark flag present", "is_dark" in d["theme"]["values"]) + check("fonts list", "inter_regular.ttf" in d["typography"]["values"]["fonts"]) + check("layout count", d["layouts"]["values"]["count"] == 1) + for k in ["package","source","framework","colors","dimens","typography","shapes","theme","icon","layouts"]: + check(f"key present: {k}", k in d) + sys.exit(1 if fails else 0) + +main()