@maccesar/aiskills 1.12.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -9
- package/lib/cleanup.js +29 -0
- package/lib/commands/skills.js +110 -9
- package/lib/config.js +18 -9
- package/lib/installer.js +5 -3
- package/lib/symlink.js +45 -3
- package/lib/utils.js +41 -0
- package/package.json +2 -2
- package/skills/audit-codebase/SKILL.md +70 -0
- package/skills/audit-codebase/agents/openai.yaml +4 -0
- package/skills/audit-codebase/references/comprehensive-audit.md +220 -0
- package/skills/audit-codebase/references/report-format.md +119 -0
- package/skills/humaniza/SKILL.md +55 -4
- package/skills/humaniza/references/ai-patterns-es.md +40 -0
- package/skills/humaniza/references/checklist.md +9 -0
- package/skills/humaniza/references/examples.md +16 -0
- package/skills/humaniza/references/lexicon-es-mx.md +18 -0
- package/skills/humaniza/references/structures-es.md +132 -0
- package/skills/humaniza/scripts/check_ai_patterns.py +216 -0
- package/skills/refactoring-ui/SKILL.md +65 -29
- package/skills/refactoring-ui/references/05-motion.md +124 -0
- package/skills/refactoring-ui/references/06-dark-mode.md +117 -0
- package/skills/refactoring-ui/references/07-component-patterns.md +181 -0
- package/skills/session-log/SKILL.md +524 -0
- package/skills/session-log/evals/README.md +79 -0
- package/skills/session-log/evals/ab-ronda-1.md +115 -0
- package/skills/session-log/evals/ab-ronda-2.md +174 -0
- package/skills/session-log/evals/defecto-experimento.md +12 -0
- package/skills/session-log/evals/evals.json +147 -0
- package/skills/session-log/evals/trigger-eval.json +23 -0
- package/skills/session-log/references/file-layout.md +336 -0
- package/skills/session-log/references/verification.md +173 -0
- package/skills/stitch-showcase/SKILL.md +24 -232
- package/skills/stitch-showcase/references/07-theme-system.md +12 -0
- package/skills/stitch-showcase/references/08-type-detection.md +9 -1
- package/skills/stitch-showcase/references/10-component-standardization.md +25 -0
- package/skills/stitch-showcase/references/12-video-embedding.md +113 -0
- package/skills/stitch-showcase/references/13-language-detection.md +82 -0
- package/skills/stitch-showcase/references/14-troubleshooting-known-issues.md +122 -0
- package/skills/stitch-showcase/references/15-build-flags.md +71 -0
- package/skills/stitch-showcase/references/16-design-md-format.md +107 -0
- package/skills/stitch-showcase/references/index.html +25 -19
- package/skills/stitch-showcase/references/viewer.html +24 -12
- package/skills/stitch-showcase/scripts/__pycache__/build_showcase.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/slug_demangle.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/build_showcase.py +150 -10
- package/skills/stitch-showcase/scripts/parse_design_md.py +145 -12
- package/skills/stitch-showcase/scripts/slug_demangle.py +209 -0
- package/skills/vscode-extension-dev/SKILL.md +90 -41
- package/skills/vscode-extension-dev/references/api-additional.md +168 -0
- package/skills/vscode-extension-dev/references/api-progress.md +55 -0
- package/skills/vscode-extension-dev/references/api-quickpick.md +75 -0
- package/skills/vscode-extension-dev/references/api-secretstorage.md +57 -0
- package/skills/vscode-extension-dev/references/api-statusbar.md +38 -0
- package/skills/vscode-extension-dev/references/api-treeview.md +78 -0
- package/skills/vscode-extension-dev/references/api-webview.md +149 -0
- package/skills/vscode-extension-dev/references/architecture.md +67 -0
- package/skills/vscode-extension-dev/references/debugger.md +179 -0
- package/skills/vscode-extension-dev/references/lsp.md +175 -0
- package/skills/vscode-extension-dev/references/notebooks.md +208 -0
- package/skills/vscode-extension-dev/references/testing.md +208 -0
- package/skills/vscode-extension-dev/references/api-patterns.md +0 -625
|
@@ -40,6 +40,7 @@ import parse_design_md
|
|
|
40
40
|
import extract_text
|
|
41
41
|
import detect_components
|
|
42
42
|
import extract_catalog
|
|
43
|
+
from slug_demangle import slug_to_title
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def _extract_source_zip(zip_path: Path) -> Path:
|
|
@@ -150,6 +151,9 @@ def build(source_dir: str, project_type: str = None, project_name: str = None) -
|
|
|
150
151
|
# Generate catalog.html (always included)
|
|
151
152
|
_generate_catalog(output_dir, assets_dir, metadata)
|
|
152
153
|
|
|
154
|
+
# Sanity-check the generated output — warnings only, never aborts.
|
|
155
|
+
_validate_showcase(output_dir, screens)
|
|
156
|
+
|
|
153
157
|
print(f"\n🎉 Showcase ready: {output_dir}/index.html\n")
|
|
154
158
|
return output_dir
|
|
155
159
|
|
|
@@ -344,6 +348,11 @@ def _prepare_source(source_dir: str, project_type: str, project_name: str) -> tu
|
|
|
344
348
|
# Defer type detection — will resolve after screen extraction via _detect_type_from_screens()
|
|
345
349
|
metadata["_type_pending"] = True
|
|
346
350
|
|
|
351
|
+
# Language: showcase.json wins over DESIGN.md if both present, else fall through.
|
|
352
|
+
if config.get("lang"):
|
|
353
|
+
metadata["lang"] = str(config["lang"])
|
|
354
|
+
metadata.setdefault("lang", "en")
|
|
355
|
+
|
|
347
356
|
ptype = metadata.get("type", "unknown")
|
|
348
357
|
if ptype != "unknown":
|
|
349
358
|
print(f"📱 Type: {ptype} | Project: {metadata['project_name']}")
|
|
@@ -518,16 +527,21 @@ def _enrich_screens(screens_raw: list, metadata: dict, source: Path) -> list:
|
|
|
518
527
|
for raw in screens_raw:
|
|
519
528
|
slug = raw["slug"]
|
|
520
529
|
design = design_screens.get(slug, {})
|
|
530
|
+
html_path = raw.get("html_path", "")
|
|
521
531
|
|
|
522
|
-
# Title
|
|
523
|
-
title =
|
|
532
|
+
# Title resolution order: explicit DESIGN.md → <title> HTML → demangled slug
|
|
533
|
+
title = (
|
|
534
|
+
design.get("title")
|
|
535
|
+
or _read_html_title_tag(html_path)
|
|
536
|
+
or slug_to_title(slug)
|
|
537
|
+
)
|
|
524
538
|
|
|
525
539
|
# Description priority: DESIGN.md → individual .md → meta/h1/title/visible body text → ""
|
|
526
540
|
desc = design.get("description") or ""
|
|
527
541
|
if not desc:
|
|
528
542
|
desc = _read_md_desc(slug, source)
|
|
529
543
|
if not desc:
|
|
530
|
-
desc = _read_html_title(
|
|
544
|
+
desc = _read_html_title(html_path)
|
|
531
545
|
|
|
532
546
|
enriched.append({
|
|
533
547
|
"slug": slug,
|
|
@@ -540,6 +554,50 @@ def _enrich_screens(screens_raw: list, metadata: dict, source: Path) -> list:
|
|
|
540
554
|
return enriched
|
|
541
555
|
|
|
542
556
|
|
|
557
|
+
def _read_html_title_tag(html_path: str) -> str:
|
|
558
|
+
"""
|
|
559
|
+
Read the <title> tag of an HTML file and return a usable screen title.
|
|
560
|
+
|
|
561
|
+
Skips generic placeholders (Untitled, index, etc.). Strips a leading
|
|
562
|
+
"Brand - " or "Brand | " prefix when the title contains a separator,
|
|
563
|
+
so "Lumiere Dining - Rústico & Artesanal" becomes "Rústico & Artesanal".
|
|
564
|
+
Decodes basic HTML entities (&, …).
|
|
565
|
+
"""
|
|
566
|
+
if not html_path:
|
|
567
|
+
return ""
|
|
568
|
+
try:
|
|
569
|
+
text = Path(html_path).read_text(encoding="utf-8", errors="ignore")
|
|
570
|
+
except OSError:
|
|
571
|
+
return ""
|
|
572
|
+
|
|
573
|
+
m = re.search(r"<title[^>]*>(.+?)</title>", text, re.IGNORECASE | re.DOTALL)
|
|
574
|
+
if not m:
|
|
575
|
+
return ""
|
|
576
|
+
|
|
577
|
+
val = re.sub(r"<[^>]+>", "", m.group(1)).strip()
|
|
578
|
+
for entity, char in [
|
|
579
|
+
("&", "&"), ("<", "<"), (">", ">"),
|
|
580
|
+
(" ", " "), ("'", "'"), (""", '"'),
|
|
581
|
+
]:
|
|
582
|
+
val = val.replace(entity, char)
|
|
583
|
+
val = re.sub(r"\s+", " ", val).strip()
|
|
584
|
+
|
|
585
|
+
if not val:
|
|
586
|
+
return ""
|
|
587
|
+
if re.match(r"^(untitled|index|screen|page|document|\d+)$", val, re.IGNORECASE):
|
|
588
|
+
return ""
|
|
589
|
+
|
|
590
|
+
# Strip a "Brand - " / "Brand | " / "Brand: " prefix and return the last segment.
|
|
591
|
+
for sep in (" - ", " — ", " | ", ": "):
|
|
592
|
+
if sep in val:
|
|
593
|
+
tail = val.split(sep)[-1].strip()
|
|
594
|
+
if tail:
|
|
595
|
+
val = tail
|
|
596
|
+
break
|
|
597
|
+
|
|
598
|
+
return val
|
|
599
|
+
|
|
600
|
+
|
|
543
601
|
def _read_md_desc(slug: str, source: Path) -> str:
|
|
544
602
|
"""Look for an individual .md file with the screen description."""
|
|
545
603
|
for md_file in source.glob("*.md"):
|
|
@@ -718,6 +776,23 @@ def _generate_viewer(output_dir: Path, ptype: str, metadata: dict, screens: list
|
|
|
718
776
|
|
|
719
777
|
template = template_path.read_text(encoding="utf-8")
|
|
720
778
|
|
|
779
|
+
# Reorder screens to follow DESIGN.md section order (mirrors _build_sections_html)
|
|
780
|
+
# so prev/next navigation matches the visual order in index.html.
|
|
781
|
+
sections = metadata.get("sections", [])
|
|
782
|
+
if sections:
|
|
783
|
+
slug_to_screen = {s["slug"]: s for s in screens}
|
|
784
|
+
ordered = []
|
|
785
|
+
seen = set()
|
|
786
|
+
for section in sections:
|
|
787
|
+
for slug in section.get("slugs", []):
|
|
788
|
+
if slug in slug_to_screen and slug not in seen:
|
|
789
|
+
ordered.append(slug_to_screen[slug])
|
|
790
|
+
seen.add(slug)
|
|
791
|
+
for s in screens:
|
|
792
|
+
if s["slug"] not in seen:
|
|
793
|
+
ordered.append(s)
|
|
794
|
+
screens = ordered
|
|
795
|
+
|
|
721
796
|
# Build screens JSON for prev/next navigation
|
|
722
797
|
screens_data = [
|
|
723
798
|
{"title": s["title"], "desc": s["description"], "html_file": s["html_file"]}
|
|
@@ -738,6 +813,7 @@ def _generate_viewer(output_dir: Path, ptype: str, metadata: dict, screens: list
|
|
|
738
813
|
font_family = font or "system-ui"
|
|
739
814
|
|
|
740
815
|
html = template
|
|
816
|
+
html = html.replace("{{HTML_LANG}}", metadata.get("lang", "en"))
|
|
741
817
|
html = html.replace("{{PROJECT_NAME}}", escape(metadata["project_name"]))
|
|
742
818
|
html = html.replace("{{SCREENS_JSON}}", json.dumps(screens_data, ensure_ascii=False))
|
|
743
819
|
html = html.replace("{{DEFAULT_THEME}}", metadata.get("default_theme", "light"))
|
|
@@ -796,12 +872,13 @@ def _generate_index(output_dir: Path, ptype: str, metadata: dict, screens: list)
|
|
|
796
872
|
)
|
|
797
873
|
|
|
798
874
|
# New template variables
|
|
799
|
-
type_labels = {"mobile": "
|
|
875
|
+
type_labels = {"mobile": "App Móvil", "web": "App Web"}
|
|
800
876
|
type_label = type_labels.get(metadata.get("type", ""), "Showcase")
|
|
801
877
|
project_desc = _project_description_html(metadata, filtered_screens)
|
|
802
878
|
screens_intro = _screens_intro_text(metadata)
|
|
803
879
|
|
|
804
880
|
html = template
|
|
881
|
+
html = html.replace("{{HTML_LANG}}", metadata.get("lang", "en"))
|
|
805
882
|
html = html.replace("{{PROJECT_NAME}}", escape(metadata["project_name"]))
|
|
806
883
|
html = html.replace("{{SCREEN_COUNT}}", str(len(filtered_screens)))
|
|
807
884
|
html = html.replace("{{SCREENS_HTML}}", sections_html)
|
|
@@ -925,7 +1002,7 @@ def _design_system_html(ds_screen: dict | None, metadata: dict) -> str:
|
|
|
925
1002
|
f'style="font-family:\'{escape(font)}\',sans-serif">Aa</span>'
|
|
926
1003
|
f'<div>'
|
|
927
1004
|
f'<span class="text-sm font-medium text-gray-700 dark:text-gray-200">{escape(font)}</span>'
|
|
928
|
-
f'<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">
|
|
1005
|
+
f'<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">Tipografía principal</span>'
|
|
929
1006
|
f'</div>'
|
|
930
1007
|
f'</div>'
|
|
931
1008
|
)
|
|
@@ -1292,10 +1369,73 @@ def _card_html(screen: dict, ptype: str) -> str:
|
|
|
1292
1369
|
</a>"""
|
|
1293
1370
|
|
|
1294
1371
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1372
|
+
# ─── Post-build validation ───────────────────────────────────────────────────
|
|
1373
|
+
|
|
1374
|
+
def _validate_showcase(output_dir: Path, screens: list) -> None:
|
|
1375
|
+
"""
|
|
1376
|
+
Sanity-check the generated showcase. Prints warnings only — never aborts.
|
|
1377
|
+
|
|
1378
|
+
Checks:
|
|
1379
|
+
1. JSON embedded in viewer.html (`var SCREENS = [...]`) parses.
|
|
1380
|
+
2. Each screen has both `assets/<slug>.html` and `assets/<slug>.png`.
|
|
1381
|
+
3. Local `src=` references (img / video / source) inside each screen
|
|
1382
|
+
resolve on disk (URLs starting with http/https/data: are skipped).
|
|
1383
|
+
"""
|
|
1384
|
+
assets_dir = output_dir / "assets"
|
|
1385
|
+
issues: list[str] = []
|
|
1386
|
+
|
|
1387
|
+
# 1) Embedded SCREENS JSON in viewer.html
|
|
1388
|
+
viewer_path = output_dir / "viewer.html"
|
|
1389
|
+
if viewer_path.exists():
|
|
1390
|
+
viewer_text = viewer_path.read_text(encoding="utf-8")
|
|
1391
|
+
m = re.search(r"var\s+SCREENS\s*=\s*(\[.*?\]);", viewer_text, re.DOTALL)
|
|
1392
|
+
if not m:
|
|
1393
|
+
issues.append("viewer.html: no encontré `var SCREENS = [...]`")
|
|
1394
|
+
else:
|
|
1395
|
+
try:
|
|
1396
|
+
json.loads(m.group(1))
|
|
1397
|
+
except json.JSONDecodeError as e:
|
|
1398
|
+
issues.append(f"viewer.html: SCREENS JSON inválido — {e}")
|
|
1399
|
+
|
|
1400
|
+
# 2) Per-screen assets
|
|
1401
|
+
for s in screens:
|
|
1402
|
+
slug = s["slug"]
|
|
1403
|
+
if not (assets_dir / f"{slug}.html").exists():
|
|
1404
|
+
issues.append(f"{slug}: falta assets/{slug}.html")
|
|
1405
|
+
if not (assets_dir / f"{slug}.png").exists():
|
|
1406
|
+
issues.append(f"{slug}: falta assets/{slug}.png")
|
|
1407
|
+
|
|
1408
|
+
# 3) Local img / video / source references inside each screen HTML
|
|
1409
|
+
src_re = re.compile(
|
|
1410
|
+
r"""<(?:img|video|source)\s[^>]*src=["']([^"']+)["']""",
|
|
1411
|
+
re.IGNORECASE,
|
|
1412
|
+
)
|
|
1413
|
+
for s in screens:
|
|
1414
|
+
slug = s["slug"]
|
|
1415
|
+
html_path = assets_dir / f"{slug}.html"
|
|
1416
|
+
if not html_path.exists():
|
|
1417
|
+
continue
|
|
1418
|
+
try:
|
|
1419
|
+
text = html_path.read_text(encoding="utf-8", errors="ignore")
|
|
1420
|
+
except OSError:
|
|
1421
|
+
continue
|
|
1422
|
+
for src in src_re.findall(text):
|
|
1423
|
+
if src.startswith(("http://", "https://", "data:", "//")):
|
|
1424
|
+
continue
|
|
1425
|
+
ref = (html_path.parent / src).resolve()
|
|
1426
|
+
if not ref.exists():
|
|
1427
|
+
issues.append(f"{slug}: src inexistente '{src}'")
|
|
1428
|
+
|
|
1429
|
+
# 4) Report
|
|
1430
|
+
print()
|
|
1431
|
+
if not issues:
|
|
1432
|
+
print(f"✅ {len(screens)}/{len(screens)} screens válidos")
|
|
1433
|
+
else:
|
|
1434
|
+
print(f"⚠ {len(issues)} problema(s) detectado(s):")
|
|
1435
|
+
for issue in issues[:20]:
|
|
1436
|
+
print(f" - {issue}")
|
|
1437
|
+
if len(issues) > 20:
|
|
1438
|
+
print(f" ...({len(issues) - 20} más)")
|
|
1299
1439
|
|
|
1300
1440
|
|
|
1301
1441
|
def _project_slug(name: str) -> str:
|
|
@@ -1422,7 +1562,7 @@ def _init(source_dir: str, project_type: str, project_name: str) -> None:
|
|
|
1422
1562
|
sys.exit(1)
|
|
1423
1563
|
|
|
1424
1564
|
# Auto-group slugs for skeleton
|
|
1425
|
-
fake_screens = [{"slug": s, "title":
|
|
1565
|
+
fake_screens = [{"slug": s, "title": slug_to_title(s), "description": ""} for s in slugs]
|
|
1426
1566
|
groups = _auto_group_screens(fake_screens)
|
|
1427
1567
|
|
|
1428
1568
|
pname = project_name or source.name.replace("_", " ").replace("-", " ").title()
|
|
@@ -3,13 +3,26 @@ parse_design_md.py — Extracts metadata from DESIGN.md for stitch-showcase.
|
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
5
|
python parse_design_md.py /path/to/DESIGN.md
|
|
6
|
-
→ prints JSON with project_name, type, colors, color_tokens, default_theme, font_family, screens, sections
|
|
6
|
+
→ prints JSON with project_name, type, lang, colors, color_tokens, default_theme, font_family, screens, sections
|
|
7
7
|
"""
|
|
8
8
|
import re
|
|
9
9
|
import sys
|
|
10
10
|
import json
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
+
# Ensure sibling modules resolve when this file is invoked directly.
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
15
|
+
from slug_demangle import slug_to_title
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
SPANISH_STOPWORDS = {
|
|
19
|
+
"de", "el", "la", "los", "las", "para", "con", "por", "una", "uno",
|
|
20
|
+
"del", "que", "es", "se", "su", "sus", "como", "más", "muy", "este",
|
|
21
|
+
"esta", "estos", "estas", "pantalla", "pantallas", "aplicación",
|
|
22
|
+
"aplicacion", "usuario", "usuarios", "diseño", "diseno", "móvil",
|
|
23
|
+
"movil", "perfil", "configuración", "configuracion",
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
|
|
14
27
|
def parse(design_md_path: str) -> dict:
|
|
15
28
|
path = Path(design_md_path)
|
|
@@ -17,6 +30,7 @@ def parse(design_md_path: str) -> dict:
|
|
|
17
30
|
return {
|
|
18
31
|
"project_name": "",
|
|
19
32
|
"type": "unknown",
|
|
33
|
+
"lang": "en",
|
|
20
34
|
"colors": {},
|
|
21
35
|
"color_tokens": {},
|
|
22
36
|
"default_theme": "light",
|
|
@@ -26,9 +40,17 @@ def parse(design_md_path: str) -> dict:
|
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
text = path.read_text(encoding="utf-8")
|
|
43
|
+
frontmatter = _parse_yaml_frontmatter(text)
|
|
44
|
+
lang = _extract_lang_override(text) or _detect_lang(text)
|
|
29
45
|
colors = _extract_colors(text)
|
|
30
46
|
color_tokens = _extract_color_tokens(text)
|
|
31
47
|
|
|
48
|
+
# Frontmatter fills in fields the Markdown body doesn't define.
|
|
49
|
+
if not colors and frontmatter.get("colors"):
|
|
50
|
+
colors = dict(frontmatter["colors"])
|
|
51
|
+
if not color_tokens and frontmatter.get("colors"):
|
|
52
|
+
color_tokens = _semantic_tokens_from_dict(frontmatter["colors"])
|
|
53
|
+
|
|
32
54
|
# Determine default_theme from surface token
|
|
33
55
|
surface_hex = color_tokens.get("surface") or color_tokens.get("background")
|
|
34
56
|
if not surface_hex:
|
|
@@ -58,7 +80,7 @@ def parse(design_md_path: str) -> dict:
|
|
|
58
80
|
# Apply titles/descriptions to screens already in the list
|
|
59
81
|
for s in screens:
|
|
60
82
|
slug = s["slug"]
|
|
61
|
-
if not s.get("title") or s["title"] ==
|
|
83
|
+
if not s.get("title") or s["title"] == slug_to_title(slug):
|
|
62
84
|
if slug in section_titles:
|
|
63
85
|
s["title"] = section_titles[slug]
|
|
64
86
|
if not s.get("description") and slug in section_descs:
|
|
@@ -69,29 +91,145 @@ def parse(design_md_path: str) -> dict:
|
|
|
69
91
|
if slug not in existing_slugs:
|
|
70
92
|
screens.append({
|
|
71
93
|
"slug": slug,
|
|
72
|
-
"title": section_titles.get(slug) or
|
|
94
|
+
"title": section_titles.get(slug) or slug_to_title(slug),
|
|
73
95
|
"description": section_descs.get(slug, ""),
|
|
74
96
|
})
|
|
75
97
|
existing_slugs.add(slug)
|
|
76
98
|
|
|
99
|
+
project_name = _extract_project_name(text) or frontmatter.get("name", "")
|
|
100
|
+
font_family = _extract_typography(text) or frontmatter.get("font_family")
|
|
101
|
+
|
|
77
102
|
return {
|
|
78
|
-
"project_name":
|
|
103
|
+
"project_name": project_name,
|
|
79
104
|
"type": _detect_type(text),
|
|
105
|
+
"lang": lang,
|
|
80
106
|
"colors": colors,
|
|
81
107
|
"color_tokens": color_tokens,
|
|
82
108
|
"default_theme": default_theme,
|
|
83
|
-
"font_family":
|
|
109
|
+
"font_family": font_family,
|
|
84
110
|
"screens": screens,
|
|
85
111
|
"sections": sections,
|
|
86
112
|
}
|
|
87
113
|
|
|
88
114
|
|
|
115
|
+
def _parse_yaml_frontmatter(text: str) -> dict:
|
|
116
|
+
"""
|
|
117
|
+
Parse a Stitch-style YAML frontmatter at the top of DESIGN.md.
|
|
118
|
+
|
|
119
|
+
Recognizes (everything else is ignored):
|
|
120
|
+
- top-level ``name:`` → ``"name"``
|
|
121
|
+
- ``colors:`` block with hex values → ``"colors"`` dict
|
|
122
|
+
- ``typography:`` block — returns the first ``fontFamily`` → ``"font_family"``
|
|
123
|
+
|
|
124
|
+
Returns an empty dict if no frontmatter delimiter is present.
|
|
125
|
+
"""
|
|
126
|
+
m = re.match(r"^---\s*\n(.*?)\n---\s*\n", text, re.DOTALL)
|
|
127
|
+
if not m:
|
|
128
|
+
return {}
|
|
129
|
+
fm = m.group(1)
|
|
130
|
+
result: dict = {}
|
|
131
|
+
|
|
132
|
+
# name: <value>
|
|
133
|
+
name_m = re.search(r"^name:\s*(.+?)\s*$", fm, re.MULTILINE)
|
|
134
|
+
if name_m:
|
|
135
|
+
val = name_m.group(1).strip().strip("'\"")
|
|
136
|
+
if val:
|
|
137
|
+
result["name"] = val
|
|
138
|
+
|
|
139
|
+
# colors: block — collect indented "key: '#hex'" pairs until indentation drops.
|
|
140
|
+
colors_m = re.search(r"^colors:\s*\n((?:[ \t]+\S.*\n?)+)", fm, re.MULTILINE)
|
|
141
|
+
if colors_m:
|
|
142
|
+
colors: dict = {}
|
|
143
|
+
for line in colors_m.group(1).splitlines():
|
|
144
|
+
cm = re.match(
|
|
145
|
+
r"^\s+([a-z][a-z0-9\-]*):\s*['\"]?(#[0-9A-Fa-f]{3,8})['\"]?\s*$",
|
|
146
|
+
line,
|
|
147
|
+
)
|
|
148
|
+
if cm:
|
|
149
|
+
colors[cm.group(1).lower()] = "#" + cm.group(2).lstrip("#").upper()
|
|
150
|
+
if colors:
|
|
151
|
+
result["colors"] = colors
|
|
152
|
+
|
|
153
|
+
# typography: pick the first fontFamily declared anywhere in the block.
|
|
154
|
+
typo_m = re.search(r"fontFamily:\s*['\"]?([^'\"\n,]+)['\"]?", fm)
|
|
155
|
+
if typo_m:
|
|
156
|
+
font = typo_m.group(1).strip().rstrip(",;")
|
|
157
|
+
if font:
|
|
158
|
+
result["font_family"] = font
|
|
159
|
+
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _semantic_tokens_from_dict(colors: dict) -> dict:
|
|
164
|
+
"""
|
|
165
|
+
Compute `accent` / `surface` semantic tokens from a flat color dict.
|
|
166
|
+
|
|
167
|
+
Mirrors the rules used by :func:`_extract_color_tokens`:
|
|
168
|
+
- ``accent`` → first key containing ``primary`` (not ``on-primary``)
|
|
169
|
+
- ``surface`` → first key named ``surface`` or containing ``background``/``bg``
|
|
170
|
+
"""
|
|
171
|
+
tokens = dict(colors)
|
|
172
|
+
for name, val in colors.items():
|
|
173
|
+
if "primary" in name and not name.startswith("on-"):
|
|
174
|
+
tokens["accent"] = val
|
|
175
|
+
break
|
|
176
|
+
for name, val in colors.items():
|
|
177
|
+
if name == "surface" or "background" in name or name == "bg":
|
|
178
|
+
tokens["surface"] = val
|
|
179
|
+
break
|
|
180
|
+
return tokens
|
|
181
|
+
|
|
182
|
+
|
|
89
183
|
def _extract_project_name(text: str) -> str:
|
|
90
184
|
"""First H1 heading in the document."""
|
|
91
185
|
m = re.search(r"^#\s+(.+)$", text, re.MULTILINE)
|
|
92
186
|
return m.group(1).strip() if m else ""
|
|
93
187
|
|
|
94
188
|
|
|
189
|
+
def _extract_lang_override(text: str) -> str | None:
|
|
190
|
+
"""
|
|
191
|
+
Explicit override via ``## Lang`` (or ``## Language``) section in DESIGN.md.
|
|
192
|
+
|
|
193
|
+
Expected format:
|
|
194
|
+
## Lang
|
|
195
|
+
es
|
|
196
|
+
|
|
197
|
+
Returns a BCP-47-ish code (``es``, ``en``, ``pt-BR``…) or None if absent.
|
|
198
|
+
"""
|
|
199
|
+
m = re.search(
|
|
200
|
+
r"^##\s+Lang(?:uage)?\s*\n\s*([A-Za-z]{2}(?:-[A-Za-z]{2,4})?)\s*$",
|
|
201
|
+
text, re.IGNORECASE | re.MULTILINE
|
|
202
|
+
)
|
|
203
|
+
if not m:
|
|
204
|
+
return None
|
|
205
|
+
raw = m.group(1)
|
|
206
|
+
# Normalize: language subtag lowercase, region subtag uppercase (BCP-47).
|
|
207
|
+
if "-" in raw:
|
|
208
|
+
lang, region = raw.split("-", 1)
|
|
209
|
+
return f"{lang.lower()}-{region.upper()}"
|
|
210
|
+
return raw.lower()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _detect_lang(text: str) -> str:
|
|
214
|
+
"""
|
|
215
|
+
Heuristic language detection. Distinguishes Spanish from everything else.
|
|
216
|
+
|
|
217
|
+
Signals:
|
|
218
|
+
- Spanish-only accented characters (á é í ó ú ñ ü).
|
|
219
|
+
- Common Spanish stop words from SPANISH_STOPWORDS.
|
|
220
|
+
|
|
221
|
+
Returns 'es' when either signal is present, else 'en'.
|
|
222
|
+
"""
|
|
223
|
+
if not text:
|
|
224
|
+
return "en"
|
|
225
|
+
lower = text.lower()
|
|
226
|
+
if re.search(r"[áéíóúñü]", lower):
|
|
227
|
+
return "es"
|
|
228
|
+
words = re.findall(r"[a-záéíóúñü]+", lower)
|
|
229
|
+
stop_hits = sum(1 for w in words if w in SPANISH_STOPWORDS)
|
|
230
|
+
return "es" if stop_hits >= 3 else "en"
|
|
231
|
+
|
|
232
|
+
|
|
95
233
|
def _detect_type(text: str) -> str:
|
|
96
234
|
"""Returns 'mobile', 'web', or 'unknown'.
|
|
97
235
|
|
|
@@ -292,7 +430,7 @@ def _parse_table(text: str) -> list:
|
|
|
292
430
|
cols = [c.strip() for c in line.split("|") if c.strip()]
|
|
293
431
|
if len(cols) >= 2:
|
|
294
432
|
slug = _to_slug(cols[0])
|
|
295
|
-
title = cols[1] if len(cols) > 1 else
|
|
433
|
+
title = cols[1] if len(cols) > 1 else slug_to_title(slug)
|
|
296
434
|
desc = cols[2] if len(cols) > 2 else ""
|
|
297
435
|
rows.append({"slug": slug, "title": title, "description": desc})
|
|
298
436
|
|
|
@@ -324,7 +462,7 @@ def _parse_screen_list(text: str) -> list:
|
|
|
324
462
|
title = title_part.strip()
|
|
325
463
|
desc = desc_part.strip()
|
|
326
464
|
else:
|
|
327
|
-
title =
|
|
465
|
+
title = slug_to_title(slug)
|
|
328
466
|
desc = raw
|
|
329
467
|
screens.append({"slug": slug, "title": title, "description": desc})
|
|
330
468
|
|
|
@@ -384,11 +522,6 @@ def _to_slug(s: str) -> str:
|
|
|
384
522
|
return s
|
|
385
523
|
|
|
386
524
|
|
|
387
|
-
def _slug_to_title(slug: str) -> str:
|
|
388
|
-
"""splash_screen → 'Splash Screen'"""
|
|
389
|
-
return slug.replace("_", " ").replace("-", " ").title()
|
|
390
|
-
|
|
391
|
-
|
|
392
525
|
if __name__ == "__main__":
|
|
393
526
|
if len(sys.argv) < 2:
|
|
394
527
|
print("Usage: python parse_design_md.py /path/to/DESIGN.md", file=sys.stderr)
|