diff --git a/.gitignore b/.gitignore index 09109dfa2..e347f0b1d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ Thumbs.db # Pre-commit cache .cache + +# Downloaded binaries (e.g. pypandoc) +*.deb diff --git a/scripts/assets/idf-fields.css b/scripts/assets/idf-fields.css index fe7fbfcc8..1dacfd5e9 100644 --- a/scripts/assets/idf-fields.css +++ b/scripts/assets/idf-fields.css @@ -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; +} diff --git a/scripts/latex_preprocessor.py b/scripts/latex_preprocessor.py index 49428468a..67e8a0014 100644 --- a/scripts/latex_preprocessor.py +++ b/scripts/latex_preprocessor.py @@ -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) diff --git a/scripts/markdown_postprocessor.py b/scripts/markdown_postprocessor.py index 92fae90ba..e3b99b928 100644 --- a/scripts/markdown_postprocessor.py +++ b/scripts/markdown_postprocessor.py @@ -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 ``
`` 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) @@ -483,8 +485,6 @@ 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: @@ -492,9 +492,18 @@ def inject_field_metadata(text: str, idd_index: dict[str, IddObject]) -> str: # 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('
') + 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: