Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "dev-v25.2",
"runtimeExecutable": "uv",
"runtimeArgs": [
"run",
"zensical",
"serve",
"-a",
"localhost:8123"
],
"cwd": "build/v25.2",
"port": 8123
},
{
"name": "serve-dist",
"runtimeExecutable": "uv",
"runtimeArgs": [
"run",
"python",
"-m",
"http.server",
"8003",
"--directory",
"dist"
],
"port": 8003
}
]
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ Thumbs.db

# Pre-commit cache
.cache

# Downloaded binaries (e.g. pypandoc)
*.deb
128 changes: 128 additions & 0 deletions scripts/assets/idf-fields.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/* Inline pill/badge styling for IDF field metadata.
* Appears directly after #### Field: headings in the IO Reference.
* Metadata is extracted from the EnergyPlus IDD file.
*/

/* Container */
.field-pills {
margin: 0.2em 0 0.5em 0;
line-height: 2;
}

/* Base pill style */
.field-pill {
display: inline-block;
font-size: 0.75em;
padding: 0.15em 0.55em;
border-radius: 999px;
font-family: var(--md-code-font-family, monospace);
vertical-align: baseline;
white-space: nowrap;
}

.field-pill .pill-label {
opacity: 0.65;
font-weight: 400;
}

/* Type pill — primary accent */
.field-pill.pill-type {
background: var(--md-accent-fg-color);
color: var(--md-accent-bg-color, #fff);
font-weight: 600;
}

/* Units pill */
.field-pill.pill-units {
background: var(--md-code-bg-color);
color: var(--md-code-fg-color);
border: 1px solid var(--md-default-fg-color--lightest);
}

/* Default pill */
.field-pill.pill-default {
background: var(--md-code-bg-color);
color: var(--md-code-fg-color);
border: 1px solid var(--md-default-fg-color--lightest);
}

/* Range pill */
.field-pill.pill-range {
background: var(--md-code-bg-color);
color: var(--md-code-fg-color);
border: 1px solid var(--md-default-fg-color--lightest);
}

/* Flag pills (Required, Autosizable, etc.) */
.field-pill.pill-flag {
background: var(--md-code-bg-color);
color: var(--md-default-fg-color--light);
border: 1px solid var(--md-default-fg-color--lightest);
font-style: italic;
}

.field-pill.pill-required {
background: hsla(15, 80%, 55%, 0.12);
color: hsl(15, 70%, 45%);
border-color: hsla(15, 70%, 55%, 0.3);
font-weight: 600;
font-style: normal;
}

/* Dark mode override for Required */
[data-md-color-scheme="slate"] .field-pill.pill-required {
background: hsla(15, 80%, 55%, 0.18);
color: hsl(15, 70%, 65%);
border-color: hsla(15, 70%, 55%, 0.35);
}

/* Choices row */
.field-choices {
margin-top: 0.15em;
line-height: 1.9;
}

.field-choices code.pill-choice {
font-size: 0.8em;
padding: 0.1em 0.4em;
border-radius: 3px;
margin-right: 0.2em;
}

/* ── Object separator ── */
hr.idf-object-separator {
border: none;
border-top: 2px solid var(--md-default-fg-color--lightest);
margin: 2.5em 0 1em;
}

/* ── Sticky object headings ──
* Any h2/h3 immediately after the separator sticks to the top of the
* viewport while the user scrolls through that object's fields.
*/
hr.idf-object-separator + h2,
hr.idf-object-separator + h3 {
position: sticky;
top: 0;
z-index: 2;
background: var(--md-default-bg-color);
padding-top: 0.4em;
padding-bottom: 0.3em;
margin-top: 0;
/* subtle bottom edge so the heading doesn't blend into content */
box-shadow: 0 1px 0 var(--md-default-fg-color--lightest);
}

/* When header is hidden on scroll (e.g. Material "header.autohide"),
* adjust sticky offset to account for the header height. */
[data-md-header="shadow"] hr.idf-object-separator + h2,
[data-md-header="shadow"] hr.idf-object-separator + h3 {
top: 0;
}

/* When the Material header is visible, offset below it.
* Material's header is ~3.6rem (default). */
.md-header--shadow ~ .md-container hr.idf-object-separator + h2,
.md-header--shadow ~ .md-container hr.idf-object-separator + h3 {
top: 0;
}
32 changes: 28 additions & 4 deletions scripts/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@
IMAGE_EXTENSIONS,
version_to_title,
)
from scripts.idd_parser import parse_idd
from scripts.latex_preprocessor import preprocess
from scripts.markdown_postprocessor import postprocess
from scripts.models import ConversionResult, DocSet, DocSetResult, LabelRef, VersionResult
from scripts.models import ConversionResult, DocSet, DocSetResult, IddObject, LabelRef, VersionResult
from scripts.nav_generator import extract_heading, generate_nav, parse_input_chain

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -294,6 +295,7 @@ def convert_tex_file(
doc_set_title: str = "",
current_md_path: str = "",
figure_numbers: list[int] | None = None,
idd_index: dict[str, IddObject] | None = None,
) -> ConversionResult:
"""Convert a single .tex file to Markdown via preprocessing -> Pandoc -> postprocessing."""
warnings: list[str] = []
Expand Down Expand Up @@ -362,6 +364,7 @@ def convert_tex_file(
rel_depth=rel_depth,
current_md_path=current_md_path,
figure_numbers=figure_numbers,
idd_index=idd_index,
)

# Write output
Expand Down Expand Up @@ -462,6 +465,7 @@ def _convert_files(
label_index: dict[str, LabelRef],
max_workers: int,
file_figure_numbers: dict[str, list[int]] | None = None,
idd_index: dict[str, IddObject] | None = None,
) -> list[tuple[str, ConversionResult]]:
"""Run file conversions, using a thread pool when *max_workers* > 1."""
if file_figure_numbers is None:
Expand All @@ -480,6 +484,7 @@ def _convert_files(
doc_set_title,
current_md_path,
file_figure_numbers.get(current_md_path),
idd_index,
): inp
for inp, tex_path, output_path, rel_depth, current_md_path in tasks
}
Expand All @@ -499,6 +504,7 @@ def _convert_files(
doc_set_title=doc_set_title,
current_md_path=current_md_path,
figure_numbers=file_figure_numbers.get(current_md_path),
idd_index=idd_index,
),
))
return converted
Expand Down Expand Up @@ -538,6 +544,7 @@ def convert_doc_set(
*,
max_workers: int = 1,
file_figure_numbers: dict[str, list[int]] | None = None,
idd_index: dict[str, IddObject] | None = None,
) -> DocSetResult:
"""Convert all files in a doc set.

Expand All @@ -553,7 +560,9 @@ def convert_doc_set(
tasks = _collect_tasks(inputs, doc_set, output_dir, parent_children, result)

# Phase 1: Convert files (parallel when max_workers > 1)
converted = _convert_files(tasks, doc_set.slug, doc_set.title, label_index, max_workers, file_figure_numbers)
converted = _convert_files(
tasks, doc_set.slug, doc_set.title, label_index, max_workers, file_figure_numbers, idd_index
)

# Phase 2: Log results and append TOCs (must happen after files are written)
for inp, file_result in converted:
Expand Down Expand Up @@ -650,7 +659,7 @@ def generate_zensical_config(
{"path": "https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.js", "async": True},
{"path": "assets/eq-tooltips.js"},
]
project["extra_css"] = ["assets/eq-tooltips.css"]
project["extra_css"] = ["assets/eq-tooltips.css", "assets/idf-fields.css"]

config_path = output_dir / "zensical.toml"
config_path.write_text(tomli_w.dumps(config))
Expand Down Expand Up @@ -720,11 +729,26 @@ def convert_version(
# Build label index across all doc sets
label_index, file_figure_numbers = build_label_index(source_dir, doc_sets)

# Parse IDD for structured field metadata (IO Reference only)
idd_index: dict[str, IddObject] | None = None
idd_path = source_dir / "idd" / "Energy+.idd.in"
if idd_path.exists():
idd_index = parse_idd(idd_path.read_text(errors="replace"))
else:
logger.warning("IDD file not found at %s, field metadata will not be injected", idd_path)

# Convert each doc set
for ds in doc_sets:
logger.info("Converting doc set: %s", ds.title)
# Only pass IDD index for IO Reference doc set (contains IDF object field docs)
ds_idd = idd_index if ds.slug == "io-reference" else None
ds_result = convert_doc_set(
ds, output_dir, label_index, max_workers=max_workers, file_figure_numbers=file_figure_numbers
ds,
output_dir,
label_index,
max_workers=max_workers,
file_figure_numbers=file_figure_numbers,
idd_index=ds_idd,
)
result.doc_set_results.append(ds_result)
logger.info(
Expand Down
4 changes: 2 additions & 2 deletions scripts/convert_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def clone_version(version: str, clone_dir: Path) -> Path:
Returns the path to the cloned repo.
"""
target = clone_dir / version
if target.exists() and (target / "doc").exists():
if target.exists() and (target / "doc").exists() and (target / "idd").exists():
logger.info("Source for %s already exists, reusing", version)
return target

Expand Down Expand Up @@ -67,7 +67,7 @@ def clone_version(version: str, clone_dir: Path) -> Path:
text=True,
)
subprocess.run(
["git", "-C", str(target), "sparse-checkout", "set", "doc"],
["git", "-C", str(target), "sparse-checkout", "set", "doc", "idd"],
check=True,
capture_output=True,
)
Expand Down
Loading