chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
"""Shared constants and helpers for kami build scripts."""
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
|
||||
class TemplateSpec(NamedTuple):
|
||||
"""Per-template configuration.
|
||||
|
||||
build_max_pages: hard ceiling enforced by `build.py --verify`. 0 = no limit.
|
||||
"""
|
||||
source: str
|
||||
build_max_pages: int
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
TEMPLATES = ROOT / "assets" / "templates"
|
||||
DIAGRAMS = ROOT / "assets" / "diagrams"
|
||||
EXAMPLES = ROOT / "assets" / "examples"
|
||||
TOKENS_FILE = ROOT / "references" / "tokens.json"
|
||||
CHECKS_THRESHOLDS_FILE = ROOT / "references" / "checks_thresholds.json"
|
||||
|
||||
PUBLIC_REPO = "tw93/kami"
|
||||
CLAUDE_CODE_MIN_VERSION = "2.1.142"
|
||||
CLAUDE_CODE_INSTALL_COMMANDS = (
|
||||
f"/plugin marketplace add {PUBLIC_REPO}",
|
||||
"/plugin install kami@kami",
|
||||
)
|
||||
CODEX_PLUGIN_INSTALL_COMMANDS = (
|
||||
f"codex plugin marketplace add {PUBLIC_REPO}",
|
||||
"codex plugin add kami@kami",
|
||||
)
|
||||
GENERIC_AGENT_INSTALL_COMMAND = f"npx skills add {PUBLIC_REPO}/plugins/kami -a universal -g -y"
|
||||
CLAUDE_DESKTOP_PACKAGE_URL = "https://github.com/tw93/kami/releases/latest/download/kami.zip"
|
||||
|
||||
# Canonical parchment background color, kept here so build/density
|
||||
# checks share one source of truth instead of redefining the RGB triple.
|
||||
PARCHMENT_RGB = (0xF5, 0xF4, 0xED)
|
||||
|
||||
_HOMEBREW_PREFIXES = (Path("/opt/homebrew"), Path("/usr/local"))
|
||||
|
||||
|
||||
def _default_cache_dir() -> Path:
|
||||
"""Return a sensible per-platform fontconfig cache directory."""
|
||||
if sys.platform == "darwin":
|
||||
return Path("/private/tmp/kami-fontconfig-cache")
|
||||
xdg = os.environ.get("XDG_CACHE_HOME")
|
||||
if xdg:
|
||||
return Path(xdg) / "kami"
|
||||
return Path.home() / ".cache" / "kami"
|
||||
|
||||
|
||||
def configure_weasyprint_runtime() -> None:
|
||||
"""Make platform-native libraries discoverable before importing WeasyPrint.
|
||||
|
||||
On macOS, also surface Homebrew's gobject lib so cairo/pango can load.
|
||||
On Linux/other, only the fontconfig cache hint is set; the system loader
|
||||
is expected to find the libraries.
|
||||
"""
|
||||
os.environ.setdefault("XDG_CACHE_HOME", str(_default_cache_dir()))
|
||||
|
||||
if sys.platform != "darwin":
|
||||
return
|
||||
|
||||
brew_lib = next(
|
||||
(p / "lib" for p in _HOMEBREW_PREFIXES if (p / "lib" / "libgobject-2.0.dylib").exists()),
|
||||
None,
|
||||
)
|
||||
if brew_lib is None:
|
||||
return
|
||||
|
||||
existing = os.environ.get("DYLD_FALLBACK_LIBRARY_PATH", "")
|
||||
paths = [path for path in existing.split(":") if path]
|
||||
brew_lib_str = str(brew_lib)
|
||||
if brew_lib_str in paths:
|
||||
return
|
||||
|
||||
os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = ":".join([brew_lib_str, *paths])
|
||||
|
||||
# Cool / neutral gray hex values that violate the "warm undertone only" rule.
|
||||
COOL_GRAY_BLOCKLIST = {
|
||||
"#888", "#888888", "#666", "#666666", "#999", "#999999",
|
||||
"#ccc", "#cccccc", "#ddd", "#dddddd", "#eee", "#eeeeee",
|
||||
"#111", "#111111", "#222", "#222222", "#333", "#333333",
|
||||
"#444", "#444444", "#555", "#555555", "#777", "#777777",
|
||||
"#aaa", "#aaaaaa", "#bbb", "#bbbbbb",
|
||||
# Tailwind cool grays
|
||||
"#6b7280", "#9ca3af", "#d1d5db", "#e5e7eb", "#f3f4f6",
|
||||
"#4b5563", "#374151", "#1f2937", "#111827",
|
||||
# Bootstrap-like neutrals
|
||||
"#f8f9fa", "#e9ecef", "#dee2e6", "#ced4da", "#adb5bd",
|
||||
"#6c757d", "#495057", "#343a40", "#212529",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Template registry
|
||||
#
|
||||
# Single source of truth for HTML targets used by build.py.
|
||||
# See TemplateSpec for field meanings.
|
||||
# ---------------------------------------------------------------------------
|
||||
HTML_TEMPLATES: dict[str, TemplateSpec] = {
|
||||
# Core six
|
||||
"one-pager": TemplateSpec("one-pager.html", 1),
|
||||
"letter": TemplateSpec("letter.html", 1),
|
||||
"long-doc": TemplateSpec("long-doc.html", 0),
|
||||
"portfolio": TemplateSpec("portfolio.html", 0),
|
||||
"resume": TemplateSpec("resume.html", 2),
|
||||
"one-pager-en": TemplateSpec("one-pager-en.html", 1),
|
||||
"letter-en": TemplateSpec("letter-en.html", 1),
|
||||
"long-doc-en": TemplateSpec("long-doc-en.html", 0),
|
||||
"portfolio-en": TemplateSpec("portfolio-en.html", 0),
|
||||
"resume-en": TemplateSpec("resume-en.html", 2),
|
||||
# Korean
|
||||
"one-pager-ko": TemplateSpec("one-pager-ko.html", 1),
|
||||
"letter-ko": TemplateSpec("letter-ko.html", 1),
|
||||
"long-doc-ko": TemplateSpec("long-doc-ko.html", 0),
|
||||
"portfolio-ko": TemplateSpec("portfolio-ko.html", 0),
|
||||
"resume-ko": TemplateSpec("resume-ko.html", 2),
|
||||
"equity-report-ko": TemplateSpec("equity-report-ko.html", 3),
|
||||
"changelog-ko": TemplateSpec("changelog-ko.html", 2),
|
||||
"slides-weasy-ko": TemplateSpec("slides-weasy-ko.html", 0),
|
||||
# Equity report
|
||||
"equity-report": TemplateSpec("equity-report.html", 3),
|
||||
"equity-report-en": TemplateSpec("equity-report-en.html", 3),
|
||||
# Changelog
|
||||
"changelog": TemplateSpec("changelog.html", 2),
|
||||
"changelog-en": TemplateSpec("changelog-en.html", 2),
|
||||
# Slides (WeasyPrint default)
|
||||
"slides-weasy": TemplateSpec("slides-weasy.html", 0),
|
||||
"slides-weasy-en": TemplateSpec("slides-weasy-en.html", 0),
|
||||
}
|
||||
|
||||
SCREEN_TEMPLATES: dict[str, str] = {
|
||||
"landing-page": "landing-page.html",
|
||||
"landing-page-en": "landing-page-en.html",
|
||||
"landing-page-ko": "landing-page-ko.html",
|
||||
}
|
||||
|
||||
# Diagram HTMLs live in assets/diagrams and have no page-count contract.
|
||||
# Registered here (not in build.py) so all template registries share one home.
|
||||
# The Mermaid-sourced ones are produced via scripts/mermaid_normalize.py.
|
||||
DIAGRAM_TEMPLATES: dict[str, str] = {
|
||||
"diagram-architecture": "architecture.html",
|
||||
"diagram-architecture-board": "architecture-board.html",
|
||||
"diagram-flowchart": "flowchart.html",
|
||||
"diagram-quadrant": "quadrant.html",
|
||||
"diagram-bar-chart": "bar-chart.html",
|
||||
"diagram-line-chart": "line-chart.html",
|
||||
"diagram-donut-chart": "donut-chart.html",
|
||||
"diagram-state-machine": "state-machine.html",
|
||||
"diagram-timeline": "timeline.html",
|
||||
"diagram-swimlane": "swimlane.html",
|
||||
"diagram-tree": "tree.html",
|
||||
"diagram-layer-stack": "layer-stack.html",
|
||||
"diagram-venn": "venn.html",
|
||||
"diagram-candlestick": "candlestick.html",
|
||||
"diagram-waterfall": "waterfall.html",
|
||||
# Mermaid-sourced (beautiful-mermaid + scripts/mermaid_normalize.py)
|
||||
"diagram-sequence": "sequence.html",
|
||||
"diagram-class": "class.html",
|
||||
"diagram-er": "er.html",
|
||||
}
|
||||
|
||||
|
||||
PUBLIC_DOCUMENT_TEMPLATE_KINDS = {
|
||||
"one-pager",
|
||||
"letter",
|
||||
"long-doc",
|
||||
"portfolio",
|
||||
"resume",
|
||||
"slides",
|
||||
"equity-report",
|
||||
"changelog",
|
||||
}
|
||||
|
||||
|
||||
def _public_template_kind(name: str) -> str:
|
||||
for suffix in ("-en", "-ko"):
|
||||
if name.endswith(suffix):
|
||||
name = name[: -len(suffix)]
|
||||
break
|
||||
if name.startswith("slides-weasy"):
|
||||
return "slides"
|
||||
return name
|
||||
|
||||
|
||||
def public_document_template_kinds() -> set[str]:
|
||||
"""Return public document-template kinds represented by HTML_TEMPLATES."""
|
||||
return {
|
||||
_public_template_kind(name)
|
||||
for name in HTML_TEMPLATES
|
||||
if _public_template_kind(name) in PUBLIC_DOCUMENT_TEMPLATE_KINDS
|
||||
}
|
||||
|
||||
|
||||
def public_document_template_count() -> int:
|
||||
return len(public_document_template_kinds())
|
||||
|
||||
|
||||
def rel_to_root(path: Path) -> Path:
|
||||
"""Return `path` relative to ROOT when possible, else the path unchanged."""
|
||||
return path.relative_to(ROOT) if path.is_relative_to(ROOT) else path
|
||||
|
||||
|
||||
def default_example_pdfs() -> list[str]:
|
||||
"""Return every rendered example PDF, the default scan set for PDF checks."""
|
||||
return [str(p) for p in sorted(EXAMPLES.glob("*.pdf"))]
|
||||
|
||||
|
||||
def iter_template_files(
|
||||
*,
|
||||
include_py: bool = False,
|
||||
include_diagrams: bool = False,
|
||||
include_marp_css: bool = False,
|
||||
) -> list[Path]:
|
||||
"""Collect template-family files for scanning.
|
||||
|
||||
One shared walker so lint, token-sync, and future checks cannot silently
|
||||
diverge in coverage (a divergence is how the Marp CSS family once slipped
|
||||
out of the lint scan while staying in the token scan).
|
||||
"""
|
||||
targets: list[Path] = list(TEMPLATES.glob("*.html"))
|
||||
if include_py:
|
||||
targets.extend(TEMPLATES.glob("*.py"))
|
||||
if include_diagrams and DIAGRAMS.exists():
|
||||
targets.extend(DIAGRAMS.glob("*.html"))
|
||||
if include_marp_css:
|
||||
marp_dir = TEMPLATES / "marp"
|
||||
if marp_dir.exists():
|
||||
targets.extend(marp_dir.glob("*.css"))
|
||||
return sorted(targets)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def kami_version() -> str:
|
||||
"""Return the canonical Kami version from the tracked VERSION file."""
|
||||
return (ROOT / "VERSION").read_text(encoding="utf-8").strip()
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def load_tokens() -> dict[str, str]:
|
||||
return json.loads(TOKENS_FILE.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def token_value(name: str) -> str:
|
||||
key = name if name.startswith("--") else f"--{name}"
|
||||
return load_tokens()[key]
|
||||
|
||||
|
||||
def build_targets() -> dict[str, tuple[str, int]]:
|
||||
"""Return target -> (source, max_pages) mapping for build.py."""
|
||||
return {name: (spec.source, spec.build_max_pages) for name, spec in HTML_TEMPLATES.items()}
|
||||
|
||||
|
||||
def screen_targets() -> dict[str, str]:
|
||||
"""Return target -> source mapping for browser-only HTML templates."""
|
||||
return dict(SCREEN_TEMPLATES)
|
||||
|
||||
|
||||
def diagram_targets() -> dict[str, str]:
|
||||
"""Return target -> source mapping for assets/diagrams HTML templates."""
|
||||
return dict(DIAGRAM_TEMPLATES)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def load_checks_thresholds() -> dict[str, Any]:
|
||||
"""Return rhythm / density / orphan thresholds.
|
||||
|
||||
Falls back to baked-in defaults if the JSON is missing so build.py works
|
||||
on a half-installed checkout.
|
||||
"""
|
||||
if CHECKS_THRESHOLDS_FILE.exists():
|
||||
return json.loads(CHECKS_THRESHOLDS_FILE.read_text(encoding="utf-8"))
|
||||
return {
|
||||
"rhythm": {"max_content_run": 5, "divider_min_deck_size": 12},
|
||||
"density": {"warn_pct": 0.25, "sparse_pct": 0.50, "dpi": 36},
|
||||
"orphan": {"max_words": 2, "max_chars": 15},
|
||||
}
|
||||
Reference in New Issue
Block a user