Add spec explorer: structured interactive view of SX spec files
- _spec_explorer_data() helper: parses spec files into sections, defines, effects, params, source blocks, and Python translations via PyEmitter - specs-explorer.sx: 10 defcomp components for explorer UI — cards with effect badges, typed param lists, collapsible SX/Python translation panels - Route at /language/specs/explore/<slug> via docs.sx - "Explore" link on existing spec detail pages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
194
sx/sx/specs-explorer.sx
Normal file
194
sx/sx/specs-explorer.sx
Normal file
@@ -0,0 +1,194 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Spec Explorer — structured interactive view of SX spec files
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~spec-explorer-content (&key data)
|
||||
(~doc-page :title (str (get data "title") " — Explorer")
|
||||
|
||||
;; Header with filename and source link
|
||||
(~spec-explorer-header
|
||||
:filename (get data "filename")
|
||||
:title (get data "title")
|
||||
:desc (get data "desc")
|
||||
:slug (replace (get data "filename") ".sx" ""))
|
||||
|
||||
;; Stats bar
|
||||
(~spec-explorer-stats :stats (get data "stats"))
|
||||
|
||||
;; Sections
|
||||
(map (fn (section)
|
||||
(~spec-explorer-section :section section))
|
||||
(get data "sections"))
|
||||
|
||||
;; Platform interface
|
||||
(when (not (empty? (get data "platform-interface")))
|
||||
(~spec-platform-interface :items (get data "platform-interface")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Header
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~spec-explorer-header (&key filename title desc slug)
|
||||
(div :class "mb-6"
|
||||
(div :class "flex items-center justify-between"
|
||||
(div
|
||||
(h1 :class "text-2xl font-bold text-stone-800" title)
|
||||
(p :class "text-sm text-stone-500 mt-1" desc))
|
||||
(a :href (str "/language/specs/" slug)
|
||||
:sx-get (str "/language/specs/" slug)
|
||||
:sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:class "text-sm text-violet-600 hover:text-violet-800 font-medium"
|
||||
"View Source"))
|
||||
(p :class "text-xs text-stone-400 font-mono mt-2" filename)))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Stats bar
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~spec-explorer-stats (&key stats)
|
||||
(div :class "flex flex-wrap gap-2 mb-6 text-xs"
|
||||
(span :class "bg-stone-100 text-stone-600 px-2 py-0.5 rounded font-medium"
|
||||
(str (get stats "total-defines") " defines"))
|
||||
(when (> (get stats "pure-count") 0)
|
||||
(span :class "bg-green-100 text-green-700 px-2 py-0.5 rounded"
|
||||
(str (get stats "pure-count") " pure")))
|
||||
(when (> (get stats "mutation-count") 0)
|
||||
(span :class "bg-amber-100 text-amber-700 px-2 py-0.5 rounded"
|
||||
(str (get stats "mutation-count") " mutation")))
|
||||
(when (> (get stats "io-count") 0)
|
||||
(span :class "bg-orange-100 text-orange-700 px-2 py-0.5 rounded"
|
||||
(str (get stats "io-count") " io")))
|
||||
(when (> (get stats "render-count") 0)
|
||||
(span :class "bg-sky-100 text-sky-700 px-2 py-0.5 rounded"
|
||||
(str (get stats "render-count") " render")))
|
||||
(span :class "bg-stone-100 text-stone-500 px-2 py-0.5 rounded"
|
||||
(str (get stats "lines") " lines"))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~spec-explorer-section (&key section)
|
||||
(div :class "mb-8"
|
||||
(h2 :class "text-lg font-semibold text-stone-700 border-b border-stone-200 pb-1 mb-3"
|
||||
:id (replace (lower-case (get section "title")) " " "-")
|
||||
(get section "title"))
|
||||
(when (get section "comment")
|
||||
(p :class "text-sm text-stone-500 mb-3" (get section "comment")))
|
||||
(div :class "space-y-4"
|
||||
(map (fn (d) (~spec-explorer-define :d d))
|
||||
(get section "defines")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Define card — one function/constant
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~spec-explorer-define (&key d)
|
||||
(div :class "rounded border border-stone-200 p-4"
|
||||
:id (str "fn-" (get d "name"))
|
||||
|
||||
;; Name + effect badges
|
||||
(div :class "flex items-center gap-2 flex-wrap"
|
||||
(span :class "font-mono font-semibold text-stone-800" (get d "name"))
|
||||
(span :class "text-xs text-stone-400" (get d "kind"))
|
||||
(if (empty? (get d "effects"))
|
||||
(span :class "text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700" "pure")
|
||||
(map (fn (eff) (~spec-effect-badge :effect eff))
|
||||
(get d "effects"))))
|
||||
|
||||
;; Params
|
||||
(when (not (empty? (get d "params")))
|
||||
(~spec-param-list :params (get d "params")))
|
||||
|
||||
;; Translation panels
|
||||
(~spec-ring-translations
|
||||
:source (get d "source")
|
||||
:python (get d "python"))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Effect badge
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~spec-effect-badge (&key effect)
|
||||
(span :class (str "text-xs px-1.5 py-0.5 rounded "
|
||||
(case effect
|
||||
"mutation" "bg-amber-100 text-amber-700"
|
||||
"io" "bg-orange-100 text-orange-700"
|
||||
"render" "bg-sky-100 text-sky-700"
|
||||
:else "bg-stone-100 text-stone-500"))
|
||||
effect))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Param list
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~spec-param-list (&key params)
|
||||
(div :class "mt-1 flex flex-wrap gap-1"
|
||||
(map (fn (p)
|
||||
(let ((name (get p "name"))
|
||||
(typ (get p "type")))
|
||||
(if (or (= name "&rest") (= name "&key"))
|
||||
(span :class "text-xs font-mono text-violet-500" name)
|
||||
(span :class "text-xs font-mono px-1 py-0.5 rounded bg-stone-50 border border-stone-200"
|
||||
(if typ
|
||||
(<> (span :class "text-stone-700" name)
|
||||
(span :class "text-stone-400" " : ")
|
||||
(span :class "text-violet-600" typ))
|
||||
(span :class "text-stone-700" name))))))
|
||||
params)))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Translation panels (Ring 2)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~spec-ring-translations (&key source python)
|
||||
(when (not (= source ""))
|
||||
(div :class "mt-3 border border-stone-200 rounded-lg overflow-hidden"
|
||||
;; SX source — always open
|
||||
(details :open "true"
|
||||
(summary :class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer"
|
||||
"SX")
|
||||
(pre :class "text-xs p-3 overflow-x-auto bg-white"
|
||||
(code (highlight source "sx"))))
|
||||
;; Python
|
||||
(when python
|
||||
(details
|
||||
(summary :class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer border-t border-stone-200"
|
||||
"Python")
|
||||
(pre :class "text-xs p-3 overflow-x-auto bg-white"
|
||||
(code (highlight python "python"))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Platform interface table (Ring 3)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~spec-platform-interface (&key items)
|
||||
(div :class "mt-8"
|
||||
(h2 :class "text-lg font-semibold text-stone-700 border-b border-stone-200 pb-1 mb-3"
|
||||
"Platform Interface")
|
||||
(p :class "text-sm text-stone-500 mb-3"
|
||||
"Functions the host platform must provide.")
|
||||
(div :class "overflow-x-auto rounded border border-stone-200"
|
||||
(table :class "w-full text-left text-sm"
|
||||
(thead (tr :class "border-b border-stone-200 bg-stone-50"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Name")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Params")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Returns")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Description")))
|
||||
(tbody
|
||||
(map (fn (item)
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700" (get item "name"))
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" (get item "params"))
|
||||
(td :class "px-3 py-2 font-mono text-xs text-stone-500" (get item "returns"))
|
||||
(td :class "px-3 py-2 text-stone-600" (get item "doc"))))
|
||||
items))))))
|
||||
@@ -334,9 +334,15 @@ deps.sx depends on: eval")))
|
||||
|
||||
(defcomp ~spec-detail-content (&key (spec-title :as string) (spec-desc :as string) (spec-filename :as string) (spec-source :as string) (spec-prose :as string?))
|
||||
(~doc-page :title spec-title
|
||||
(div :class "flex items-baseline gap-3 mb-4"
|
||||
(div :class "flex items-center gap-3 mb-4"
|
||||
(span :class "text-sm text-stone-400 font-mono" spec-filename)
|
||||
(span :class "text-sm text-stone-500" spec-desc))
|
||||
(span :class "text-sm text-stone-500 flex-1" spec-desc)
|
||||
(a :href (str "/language/specs/explore/" (replace spec-filename ".sx" ""))
|
||||
:sx-get (str "/language/specs/explore/" (replace spec-filename ".sx" ""))
|
||||
:sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:class "text-sm text-violet-600 hover:text-violet-800 font-medium whitespace-nowrap"
|
||||
"Explore"))
|
||||
(when spec-prose
|
||||
(div :class "mb-6 space-y-3"
|
||||
(p :class "text-stone-600 leading-relaxed" spec-prose)
|
||||
|
||||
@@ -349,15 +349,22 @@
|
||||
"extensions" (~spec-overview-content
|
||||
:spec-title "Extensions"
|
||||
:spec-files (make-spec-files extension-spec-items))
|
||||
:else (let ((spec (find-spec slug)))
|
||||
(if spec
|
||||
(~spec-detail-content
|
||||
:spec-title (get spec "title")
|
||||
:spec-desc (get spec "desc")
|
||||
:spec-filename (get spec "filename")
|
||||
:spec-source (read-spec-file (get spec "filename"))
|
||||
:spec-prose (get spec "prose"))
|
||||
(~spec-not-found :slug slug)))))))
|
||||
:else (cond
|
||||
(starts-with? slug "explore/")
|
||||
(let ((spec-slug (slice slug 8 (string-length slug)))
|
||||
(data (spec-explorer-data spec-slug)))
|
||||
(if data
|
||||
(~spec-explorer-content :data data)
|
||||
(~spec-not-found :slug spec-slug)))
|
||||
:else (let ((spec (find-spec slug)))
|
||||
(if spec
|
||||
(~spec-detail-content
|
||||
:spec-title (get spec "title")
|
||||
:spec-desc (get spec "desc")
|
||||
:spec-filename (get spec "filename")
|
||||
:spec-source (read-spec-file (get spec "filename"))
|
||||
:spec-prose (get spec "prose"))
|
||||
(~spec-not-found :slug slug))))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Bootstrappers section
|
||||
|
||||
@@ -34,6 +34,7 @@ def _register_sx_helpers() -> None:
|
||||
"offline-demo-data": _offline_demo_data,
|
||||
"prove-data": _prove_data,
|
||||
"page-helpers-demo-data": _page_helpers_demo_data,
|
||||
"spec-explorer-data": _spec_explorer_data,
|
||||
})
|
||||
|
||||
|
||||
@@ -141,6 +142,298 @@ def _read_spec_file(filename: str) -> str:
|
||||
return ";; spec file not found"
|
||||
|
||||
|
||||
def _spec_explorer_data(slug: str) -> dict | None:
|
||||
"""Parse a spec file into structured metadata for the spec explorer.
|
||||
|
||||
Returns sections with defines, effects, params, source, and translations.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
from shared.sx.parser import parse_all
|
||||
from shared.sx.types import Symbol, Keyword
|
||||
|
||||
# Look up spec metadata from nav-data (via find-spec helper)
|
||||
from shared.sx.jinja_bridge import get_component_env
|
||||
env = get_component_env()
|
||||
all_specs = env.get("all-spec-items", [])
|
||||
spec_meta = None
|
||||
for item in all_specs:
|
||||
if isinstance(item, dict) and item.get("slug") == slug:
|
||||
spec_meta = item
|
||||
break
|
||||
if not spec_meta:
|
||||
return None
|
||||
|
||||
filename = spec_meta.get("filename", "")
|
||||
title = spec_meta.get("title", slug)
|
||||
desc = spec_meta.get("desc", "")
|
||||
|
||||
# Read the raw source
|
||||
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref")
|
||||
if not os.path.isdir(ref_dir):
|
||||
ref_dir = "/app/shared/sx/ref"
|
||||
filepath = os.path.join(ref_dir, filename)
|
||||
try:
|
||||
with open(filepath, encoding="utf-8") as f:
|
||||
source = f.read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
lines = source.split("\n")
|
||||
|
||||
# --- 1. Section splitting ---
|
||||
sections: list[dict] = []
|
||||
current_section: dict | None = None
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
# Detect section dividers: ;; ---...
|
||||
if re.match(r"^;; -{10,}", line):
|
||||
# Look for title in following comment lines
|
||||
title_lines = []
|
||||
j = i + 1
|
||||
while j < len(lines) and lines[j].startswith(";;"):
|
||||
content = lines[j][2:].strip()
|
||||
if re.match(r"^-{10,}", content):
|
||||
j += 1
|
||||
break
|
||||
if content:
|
||||
title_lines.append(content)
|
||||
j += 1
|
||||
if title_lines:
|
||||
section_title = title_lines[0]
|
||||
# Collect comment block after section header
|
||||
comment_lines = []
|
||||
k = j
|
||||
while k < len(lines) and lines[k].startswith(";;"):
|
||||
c = lines[k][2:].strip()
|
||||
if re.match(r"^-{5,}", c) or re.match(r"^={5,}", c):
|
||||
break
|
||||
if c:
|
||||
comment_lines.append(c)
|
||||
k += 1
|
||||
current_section = {
|
||||
"title": section_title,
|
||||
"comment": " ".join(comment_lines) if comment_lines else None,
|
||||
"defines": [],
|
||||
}
|
||||
sections.append(current_section)
|
||||
i = j
|
||||
continue
|
||||
i += 1
|
||||
|
||||
# If no sections found, create a single implicit one
|
||||
if not sections:
|
||||
current_section = {"title": filename, "comment": None, "defines": []}
|
||||
sections.append(current_section)
|
||||
|
||||
# --- 2. Parse AST ---
|
||||
try:
|
||||
exprs = parse_all(source)
|
||||
except Exception:
|
||||
exprs = []
|
||||
|
||||
# --- 3. Process each top-level define ---
|
||||
# Build a line-number index: find where each top-level form starts
|
||||
def _find_source_block(name: str, form: str = "define") -> tuple[str, int]:
|
||||
"""Find the source text of a define form by scanning raw source."""
|
||||
patterns = [
|
||||
f"({form} {name} ",
|
||||
f"({form} {name}\n",
|
||||
]
|
||||
for pat in patterns:
|
||||
idx = source.find(pat)
|
||||
if idx >= 0:
|
||||
# Count balanced parens from idx
|
||||
depth = 0
|
||||
end = idx
|
||||
for ci, ch in enumerate(source[idx:], idx):
|
||||
if ch == "(":
|
||||
depth += 1
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
end = ci + 1
|
||||
break
|
||||
line_num = source[:idx].count("\n") + 1
|
||||
return source[idx:end], line_num
|
||||
return "", 0
|
||||
|
||||
def _extract_effects(expr: list) -> list[str]:
|
||||
"""Extract :effects [...] from a define form."""
|
||||
if len(expr) >= 4 and isinstance(expr[2], Keyword) and expr[2].name == "effects":
|
||||
eff_list = expr[3]
|
||||
if isinstance(eff_list, list):
|
||||
return [s.name if isinstance(s, Symbol) else str(s) for s in eff_list]
|
||||
return []
|
||||
|
||||
def _extract_params(expr: list) -> list[dict]:
|
||||
"""Extract params from the fn/lambda body of a define."""
|
||||
# Find the fn/lambda form
|
||||
val_expr = expr[4] if (len(expr) >= 5 and isinstance(expr[2], Keyword)
|
||||
and expr[2].name == "effects") else expr[2] if len(expr) >= 3 else None
|
||||
if not isinstance(val_expr, list) or not val_expr:
|
||||
return []
|
||||
if not isinstance(val_expr[0], Symbol):
|
||||
return []
|
||||
if val_expr[0].name not in ("fn", "lambda"):
|
||||
return []
|
||||
if len(val_expr) < 2 or not isinstance(val_expr[1], list):
|
||||
return []
|
||||
params_list = val_expr[1]
|
||||
result = []
|
||||
i = 0
|
||||
while i < len(params_list):
|
||||
p = params_list[i]
|
||||
if isinstance(p, Symbol) and p.name in ("&rest", "&key"):
|
||||
result.append({"name": p.name, "type": None})
|
||||
i += 1
|
||||
continue
|
||||
if isinstance(p, Symbol):
|
||||
result.append({"name": p.name, "type": None})
|
||||
elif isinstance(p, list) and len(p) == 3:
|
||||
# (name :as type)
|
||||
name_s, kw, type_s = p
|
||||
if isinstance(name_s, Symbol) and isinstance(kw, Keyword) and kw.name == "as":
|
||||
type_str = type_s.name if isinstance(type_s, Symbol) else str(type_s)
|
||||
result.append({"name": name_s.name, "type": type_str})
|
||||
else:
|
||||
result.append({"name": str(p), "type": None})
|
||||
else:
|
||||
result.append({"name": str(p), "type": None})
|
||||
i += 1
|
||||
return result
|
||||
|
||||
def _assign_to_section(line_num: int) -> dict:
|
||||
"""Find which section a line belongs to."""
|
||||
best = sections[0]
|
||||
for s in sections:
|
||||
# Section starts at its first define or earlier
|
||||
if s["defines"]:
|
||||
first_line = s["defines"][0].get("line", 0)
|
||||
else:
|
||||
first_line = 0
|
||||
# Use section order — defines after a section header belong to it
|
||||
# Simple approach: find section by scanning source position
|
||||
return best
|
||||
|
||||
# Process defines
|
||||
all_defines: list[dict] = []
|
||||
py_emitter = None
|
||||
|
||||
for expr in exprs:
|
||||
if not isinstance(expr, list) or len(expr) < 2:
|
||||
continue
|
||||
if not isinstance(expr[0], Symbol):
|
||||
continue
|
||||
|
||||
head = expr[0].name
|
||||
if head not in ("define", "define-async"):
|
||||
continue
|
||||
|
||||
name_node = expr[1]
|
||||
name = name_node.name if isinstance(name_node, Symbol) else str(name_node)
|
||||
|
||||
effects = _extract_effects(expr)
|
||||
params = _extract_params(expr)
|
||||
src, line_num = _find_source_block(name, head)
|
||||
|
||||
kind = "function"
|
||||
# Check if it's a constant (no fn/lambda body)
|
||||
val_idx = 4 if (len(expr) >= 5 and isinstance(expr[2], Keyword)
|
||||
and expr[2].name == "effects") else 2
|
||||
if val_idx < len(expr):
|
||||
val = expr[val_idx]
|
||||
if isinstance(val, list) and val and isinstance(val[0], Symbol) and val[0].name in ("fn", "lambda"):
|
||||
kind = "async-function" if head == "define-async" else "function"
|
||||
else:
|
||||
kind = "constant"
|
||||
if head == "define-async":
|
||||
kind = "async-function"
|
||||
|
||||
# --- Python translation ---
|
||||
py_code = None
|
||||
try:
|
||||
if py_emitter is None:
|
||||
from shared.sx.ref.bootstrap_py import PyEmitter
|
||||
py_emitter = PyEmitter()
|
||||
if head == "define-async":
|
||||
py_code = py_emitter._emit_define_async(expr)
|
||||
else:
|
||||
py_code = py_emitter._emit_define(expr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
define_entry = {
|
||||
"name": name,
|
||||
"kind": kind,
|
||||
"effects": effects,
|
||||
"params": params,
|
||||
"source": src,
|
||||
"line": line_num,
|
||||
"python": py_code,
|
||||
}
|
||||
all_defines.append(define_entry)
|
||||
|
||||
# --- Assign defines to sections ---
|
||||
# Match by line number: each define belongs to the section whose header
|
||||
# precedes it in the source
|
||||
section_line_map: list[tuple[int, dict]] = []
|
||||
for s in sections:
|
||||
# Find the line where section title appears
|
||||
t = s["title"]
|
||||
for li, line in enumerate(lines, 1):
|
||||
if t in line:
|
||||
section_line_map.append((li, s))
|
||||
break
|
||||
section_line_map.sort(key=lambda x: x[0])
|
||||
|
||||
for d in all_defines:
|
||||
dl = d.get("line", 0)
|
||||
target_section = sections[0]
|
||||
for sl, s in section_line_map:
|
||||
if dl >= sl:
|
||||
target_section = s
|
||||
target_section["defines"].append(d)
|
||||
|
||||
# --- Stats ---
|
||||
pure_count = sum(1 for d in all_defines if not d["effects"])
|
||||
mutation_count = sum(1 for d in all_defines if "mutation" in d["effects"])
|
||||
io_count = sum(1 for d in all_defines if "io" in d["effects"])
|
||||
render_count = sum(1 for d in all_defines if "render" in d["effects"])
|
||||
|
||||
# --- Platform interface ---
|
||||
platform_items = []
|
||||
for line in lines:
|
||||
m = re.match(r"^;;\s+\((\S+)\s+(.*?)\)\s+→\s+(\S+)\s+—\s+(.+)", line)
|
||||
if m:
|
||||
platform_items.append({
|
||||
"name": m.group(1),
|
||||
"params": m.group(2),
|
||||
"returns": m.group(3),
|
||||
"doc": m.group(4).strip(),
|
||||
})
|
||||
|
||||
# Filter out empty sections
|
||||
sections = [s for s in sections if s["defines"]]
|
||||
|
||||
return {
|
||||
"filename": filename,
|
||||
"title": title,
|
||||
"desc": desc,
|
||||
"sections": sections,
|
||||
"platform-interface": platform_items,
|
||||
"stats": {
|
||||
"total-defines": len(all_defines),
|
||||
"pure-count": pure_count,
|
||||
"mutation-count": mutation_count,
|
||||
"io-count": io_count,
|
||||
"render-count": render_count,
|
||||
"lines": len(lines),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _bootstrapper_data(target: str) -> dict:
|
||||
"""Return bootstrapper source and generated output for a target.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user