From 0c71320ee8555e028891c74057c59bcd9ee5aa3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=81Z=C3=81R=20Imre=20AI=20Agent?= Date: Thu, 16 Jul 2026 11:27:45 +0200 Subject: [PATCH] build(baseline): add lib/jmap.py Joomla manifest-mapper + tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generic Joomla 3.x manifest -> deployed-path mapper (component/module/plugin/ library/template/package), used to author the pristine (upstream) commit of a packages/ module before it is reconciled to the live bytes. 18/18 tests green; ruff/mypy-strict clean. Signed-off-by: LÁZÁR Imre Assisted-by: claude-code@claude-opus-4-8 --- lib/jmap.py | 151 +++++++++++++++++++++++++++++++++++++++++---- tests/test_jmap.py | 138 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 274 insertions(+), 15 deletions(-) diff --git a/lib/jmap.py b/lib/jmap.py index 0a124fb6..a44ff031 100644 --- a/lib/jmap.py +++ b/lib/jmap.py @@ -19,6 +19,7 @@ Csak a Python standard library-t használja (nincs külső dependency). from __future__ import annotations import argparse +import re import shutil import sys import xml.etree.ElementTree as ET @@ -80,22 +81,90 @@ def _extension_name(root: ET.Element) -> str: def _plugin_element(root: ET.Element) -> str: """A plugin element-slugját adja vissza. - Elsődleges forrás: ```` attribútum (ez a - Joomla-konvenció szerinti autoritatív érték). Ha ez hiányzik, a - ``PLG_GROUP_ELEMENT`` konvenció utolsó '_'-tagját használja - fallback-ként. + Elsődleges forrás: ```` blokk ```` vagy + ```` gyereke (mindkét tag-alak előfordul a valós + csomagokban -- ez a Joomla-konvenció szerinti autoritatív érték). Ha ez + hiányzik, a ``PLG_GROUP_ELEMENT`` konvenció utolsó + '_'-tagját használja fallback-ként. """ files_elem = root.find("files") if files_elem is not None: - for filename_elem in files_elem.findall("filename"): - plugin_attr = filename_elem.get("plugin") + for child in files_elem: + if child.tag not in ("filename", "file"): + continue + plugin_attr = child.get("plugin") if plugin_attr: return plugin_attr.strip() name_text = _text(root.find("name")) if name_text: return _slug(name_text.rsplit("_", 1)[-1]) raise JoomlaManifestError( - 'nem határozható meg a plugin element-je: nincs és nincs ' + 'nem határozható meg a plugin element-je: nincs / ' + ' és nincs ' + ) + + +_COMPONENT_LANG_SYS_INI = re.compile(r"(com_[a-z0-9_]+)\.sys\.ini$", re.IGNORECASE) +_COMPONENT_SCRIPTFILE = re.compile(r"script\.([a-z0-9_]+)\.php$", re.IGNORECASE) + + +def _component_slug(text: str) -> str: + """Komponens-display-nevet ('OneAll Social Login') Joomla-konvenció szerint + normalizál a végső fallback-ágon: szóköz-ELTÁVOLÍTÁS (nem aláhúzás-csere) + + kisbetűsítés -> 'oneallsociallogin'. + """ + return "".join(text.strip().lower().split()) + + +def _component_name(root: ET.Element) -> str: + """A komponens deployed-elemnevét ('com_X') vezeti le a Joomla 3.x + installer-heurisztika prioritási sorrendjében: + + 1. explicit gyökér ``element`` attribútum + 2. a fő ```` attribútuma, ha 'com_'-prefixű -- + ez az autoritatív forrás, mert a ```` gyakran csak emberi + megjelenítő-cím (pl. "OneAll Social Login", nem "com_oneallsociallogin") + 3. az adminisztratív, majd a site ```` fájlnév- + mintázata ``en-GB.com_.sys.ini`` -> ``com_`` + 4. ``install/script..php`` -> ``com_`` + 5. végső fallback: ```` Joomla-konvenció szerinti normalizálása + (szóköz-eltávolítás, kisbetűsítés); ha az eredmény már 'com_'-prefixű + (gépi-nevet tartalmazó manifest), változatlanul megy tovább, egyébként + 'com_' prefixet kap. + """ + element_attr = root.get("element") + if element_attr: + return element_attr.strip() + + files_elem = root.find("files") + if files_elem is not None: + folder_attr = files_elem.get("folder", "") + if folder_attr.lower().startswith("com_"): + return folder_attr + + for languages_elem in (root.find("administration/languages"), root.find("languages")): + if languages_elem is None: + continue + for language_elem in languages_elem.findall("language"): + match = _COMPONENT_LANG_SYS_INI.search(_text(language_elem)) + if match: + return match.group(1).lower() + + scriptfile_text = _text(root.find("scriptfile")) + if scriptfile_text: + match = _COMPONENT_SCRIPTFILE.search(scriptfile_text) + if match: + return f"com_{match.group(1).lower()}" + + name_text = _text(root.find("name")) + if name_text: + slug = _component_slug(name_text) + return slug if slug.startswith("com_") else f"com_{slug}" + + raise JoomlaManifestError( + "nem határozható meg a komponens deployed-neve: nincs 'element' attribútum, " + 'nincs , nincs com_-mintázatú nyelvi fájl, ' + "nincs , és nincs " ) @@ -105,10 +174,14 @@ def _plugin_element(root: ET.Element) -> str: def _apply_files(files_elem: ET.Element | None, manifest_dir: Path, dest_dir: Path) -> list[CopyOp]: - """Egy ````-szerű blokk (files/media) gyerekeit (filename/folder) - CopyOp-listává alakítja. A ``folder`` attribútum a forrás-alkönyvtárat - jelöli a manifest könyvtárához képest; a ``target`` attribútum (ha van) - felülírja a cél-relatív-útvonalat. + """Egy ````-szerű blokk (files/media) gyerekeit CopyOp-listává alakítja. + + Az egyedi-fájl gyerek tag-neve a valós csomagokban ```` VAGY + ```` lehet (mindkettőt elfogadja a Joomla-installer, csak a szöveg- + tartalom és a ``target``/egyéb attribútumok számítanak); a ```` + gyerek egy teljes alfát másol. A ``folder`` attribútum a forrás- + alkönyvtárat jelöli a manifest könyvtárához képest; a ``target`` + attribútum (ha van) felülírja a cél-relatív-útvonalat. """ if files_elem is None: return [] @@ -116,7 +189,7 @@ def _apply_files(files_elem: ET.Element | None, manifest_dir: Path, dest_dir: Pa src_base = manifest_dir / folder_attr if folder_attr else manifest_dir ops: list[CopyOp] = [] for child in files_elem: - if child.tag not in ("filename", "folder"): + if child.tag not in ("filename", "file", "folder"): continue rel_text = _text(child) if not rel_text: @@ -178,8 +251,14 @@ def _apply_languages(languages_elem: ET.Element | None, manifest_dir: Path, lang def map_component(root: ET.Element, manifest_dir: Path, out_root: Path) -> list[CopyOp]: """com_X: files->components/com_X, administration/files->administrator/components/com_X, media->media/com_X, languages(site+admin)->language/ ill. administrator/language/. + + A manifestbe ágyazott ````/```` szekciók (ha vannak -- + pl. egy komponens saját manifestje telepít egy modult/plugint is, mint + az OneAll Social Login csomagnál) is feldolgozásra kerülnek; ez NEM a + ``pkg_`` package-típus (annak külön al-manifestjei vannak), hanem a + komponens EGYETLEN manifestjébe ágyazott mapping-szekció. """ - name = _extension_name(root) + name = _component_name(root) ops = _apply_files(root.find("files"), manifest_dir, out_root / "components" / name) admin_elem = root.find("administration") @@ -193,6 +272,52 @@ def map_component(root: ET.Element, manifest_dir: Path, out_root: Path) -> list[ ops += _apply_media(root.find("media"), manifest_dir, out_root / "media") ops += _apply_languages(root.find("languages"), manifest_dir, out_root / "language") + ops += _apply_embedded_modules(root.find("modules"), manifest_dir, out_root) + ops += _apply_embedded_plugins(root.find("plugins"), manifest_dir, out_root) + return ops + + +def _apply_embedded_modules( + modules_elem: ET.Element | None, manifest_dir: Path, out_root: Path +) -> list[CopyOp]: + """Egy komponens-manifestbe ágyazott ```` + szekció feldolgozása. A deployed-modulnév közvetlenül a ``module`` attribútumból jön + (nem a gyakran emberi-olvasható ````-ből). + """ + if modules_elem is None: + return [] + ops: list[CopyOp] = [] + for module_elem in modules_elem.findall("module"): + module_name = module_elem.get("module") + if not module_name: + raise JoomlaManifestError(f"beágyazott hiányzó 'module' attribútummal: {manifest_dir}") + is_admin = module_elem.get("client") == "administrator" + base = out_root / ("administrator/modules" if is_admin else "modules") / module_name + ops += _apply_files(module_elem.find("files"), manifest_dir, base) + ops += _apply_media(module_elem.find("media"), manifest_dir, out_root / "media") + return ops + + +def _apply_embedded_plugins( + plugins_elem: ET.Element | None, manifest_dir: Path, out_root: Path +) -> list[CopyOp]: + """Egy komponens-manifestbe ágyazott ```` + szekció feldolgozása. Az elem-slug és a group közvetlenül a ``plugin``/``group`` + attribútumokból jön (nem a gyakran emberi-olvasható ````-ből). + """ + if plugins_elem is None: + return [] + ops: list[CopyOp] = [] + for plugin_elem in plugins_elem.findall("plugin"): + element = plugin_elem.get("plugin") + group = plugin_elem.get("group") + if not element or not group: + raise JoomlaManifestError( + f"beágyazott hiányzó 'plugin' vagy 'group' attribútummal: {manifest_dir}" + ) + base = out_root / "plugins" / group / element + ops += _apply_files(plugin_elem.find("files"), manifest_dir, base) + ops += _apply_media(plugin_elem.find("media"), manifest_dir, out_root / "media") return ops diff --git a/tests/test_jmap.py b/tests/test_jmap.py index 2ed97133..ce9a69a4 100644 --- a/tests/test_jmap.py +++ b/tests/test_jmap.py @@ -80,6 +80,134 @@ def test_component_site_admin_media_language(tmp_path: Path) -> None: assert (out / "administrator/language/en-GB/en-GB.com_hello.sys.ini").read_text() == "COM_HELLO=Hello" +# --------------------------------------------------------------------------- +# component deployed-név-levezetés -- regressziós tesztek a valós +# OneAll Social Login csomagon talált hibára (a emberi megjelenítő-cím +# volt, nem a gépi elemnév; a helyes forrás a ill. +# a nyelvi fájlnév/scriptfile-mintázat/name-fallback prioritási láncban). +# --------------------------------------------------------------------------- + + +def test_component_name_from_files_folder_com_prefix(tmp_path: Path) -> None: + """Elsődleges forrás: -- a emberi cím, eltér.""" + pkg = tmp_path / "pkg" + out = tmp_path / "out" + _write( + pkg / "manifest.xml", + """ + + Totally Human Display Title + + index.html + + +""", + ) + _write(pkg / "com_realname/index.html", "") + + _run(pkg, out) + + assert (out / "components/com_realname/index.html").read_text() == "" + assert not (out / "components/totallyhumandisplaytitle").exists() + + +def test_component_name_fallback_removes_spaces_not_underscore(tmp_path: Path) -> None: + """Végső fallback (nincs com_-folder, nincs nyelvi fájl, nincs scriptfile): + a normalizálása szóköz-ELTÁVOLÍTÁSSAL történik, NEM aláhúzás-cserével. + """ + pkg = tmp_path / "pkg" + out = tmp_path / "out" + _write( + pkg / "manifest.xml", + """ + + OneAll Social Login + + index.html + + +""", + ) + _write(pkg / "index.html", "") + + _run(pkg, out) + + assert (out / "components/com_oneallsociallogin/index.html").exists() + assert not (out / "components/com_oneall_social_login").exists() + + +def test_component_with_embedded_modules_and_plugins_real_pattern(tmp_path: Path) -> None: + """A valós OneAll Social Login csomag mintázatának replikája: EGYETLEN + type="component" manifest, ami -val adja meg a + deployed-nevet, tag-eket (nem -t) használ, és beágyazott + / szekciókat tartalmaz (NEM pkg_ package -- nincs + külön al-manifest, minden egy fájlban van). + """ + pkg = tmp_path / "pkg" + out = tmp_path / "out" + + _write( + pkg / "manifest.xml", + """ + + OneAll Social Login + install/script.oneallsociallogin.php + + + index.html + oneallsociallogin.php + + + + COM_ONEALLSOCIALLOGIN + + controller.php + + + en-GB.com_oneallsociallogin.sys.ini + + + + + + + mod_oneallsociallogin.php + + + + + + + + oneallsociallogin.php + + + + +""", + ) + _write(pkg / "com_oneallsociallogin/index.html", "") + _write(pkg / "com_oneallsociallogin/oneallsociallogin.php", " None: ) _write(pkg / "helloworld.php", " None: pkg.mkdir() out = tmp_path / "out" - script = Path(__file__).parent / "jmap.py" + # A jmap-modul tényleges fájl-útvonalát az importált modulból vesszük, + # nem a teszt-fájl mellől -- a build-repóban jmap.py a lib/-ben, + # test_jmap.py a tests/-ben él, a két fájl NEM ugyanabban a könyvtárban. + script = Path(jmap.__file__) result = subprocess.run( [sys.executable, str(script), "--package", str(pkg), "--out", str(out)], capture_output=True,