Skip to content
Merged
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
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
38 changes: 38 additions & 0 deletions scripts/assets/idf-fields.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,41 @@
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;
}
48 changes: 28 additions & 20 deletions scripts/latex_preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,34 +146,42 @@ def _find_brace_content(text: str, start: int) -> tuple[str, int] | None:
}


_BRACKET_MACRO_RE = re.compile(r"\\(?:PB|RB|CB)\{")


def _expand_all_bracket_macros(text: str) -> str:
r"""Expand all ``\PB``, ``\RB``, ``\CB`` macros in *text*, inside-out.

When an outer macro wraps inner macros (e.g. ``\PB{a \PB{b}}``) the
inner content is recursively expanded first, so the final result
contains no bracket macros regardless of nesting depth.

Uses regex to jump to the next macro occurrence instead of scanning
every character, which is critical for large files.
"""
result: list[str] = []
i = 0
while i < len(text):
matched_macro = None
for macro in _BRACKET_MACROS:
if text[i:].startswith(macro + "{"):
matched_macro = macro
break
if matched_macro:
brace_start = i + len(matched_macro)
found = _find_brace_content(text, brace_start)
if found:
content, end = found
# Recursively expand any bracket macros inside the content
content = _expand_all_bracket_macros(content)
left, right = _BRACKET_MACROS[matched_macro]
result.append(f"{left} {content} {right}")
i = end
continue
result.append(text[i])
i += 1
pos = 0
while True:
m = _BRACKET_MACRO_RE.search(text, pos)
if m is None:
result.append(text[pos:])
break
# Append everything before this macro
result.append(text[pos : m.start()])
macro = text[m.start() : m.end() - 1] # e.g. "\\PB"
brace_start = m.end() - 1 # position of the '{'
found = _find_brace_content(text, brace_start)
if found:
content, end = found
# Recursively expand any bracket macros inside the content
content = _expand_all_bracket_macros(content)
left, right = _BRACKET_MACROS[macro]
result.append(f"{left} {content} {right}")
pos = end
else:
# Unbalanced brace — emit macro text literally and continue
result.append(text[m.start() : m.end()])
pos = m.end()
return "".join(result)


Expand Down
15 changes: 12 additions & 3 deletions scripts/markdown_postprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,11 +470,13 @@ def inject_field_metadata(text: str, idd_index: dict[str, IddObject]) -> str:

Scans the markdown for heading patterns to determine the current IDF object,
then looks up field metadata from the IDD index and inserts styled attribute
blocks after each field heading.
blocks after each field heading. Also inserts ``<hr>`` separators between
consecutive IDF objects so long group pages are visually segmented.
"""
lines = text.split("\n")
result: list[str] = []
current_object: IddObject | None = None
seen_first_object = False

# Heading patterns
# Object headings: # ObjectName or ## ObjectName (for group pages)
Expand All @@ -483,18 +485,25 @@ def inject_field_metadata(text: str, idd_index: dict[str, IddObject]) -> str:
field_pattern = re.compile(r"^####\s+Field:\s*(.+)$")

for line in lines:
result.append(line)

# Check for object-level headings (h1, h2, h3) that match an IDD object
h_match = h1_pattern.match(line)
if h_match:
heading_text = h_match.group(2).strip()
# Strip any Pandoc attributes like {#id .class}
heading_text = re.sub(r"\s*\{[#.][^}]*\}", "", heading_text).strip()
if heading_text in idd_index:
# Insert a visual separator between objects (skip the first)
if seen_first_object:
result.append("")
result.append('<hr class="idf-object-separator">')
result.append("")
seen_first_object = True
current_object = idd_index[heading_text]
result.append(line)
continue

result.append(line)

# Check for field headings
field_match = field_pattern.match(line)
if field_match and current_object:
Expand Down
Loading