#!/usr/bin/env python3 """Build HTML pages from Markdown sources.""" import re from pathlib import Path from markdown_it import MarkdownIt ROOT = Path(__file__).parent md = MarkdownIt("commonmark").enable("table") CSS = """ *, *::before, *::after { box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #1a1a1a; background: #fff; margin: 0; padding: 0; } .container { max-width: 860px; margin: 0 auto; padding: 3rem 2rem 6rem; } h1 { font-size: 2.4rem; margin-bottom: 1.2rem; line-height: 1; } a.title-btn { display: inline-block; background: #6366f1; color: #fff; font-size: 2.4rem; font-weight: 700; padding: 0.2em 0.5em; border-radius: 8px; text-decoration: none; line-height: 1.2; transition: background 0.15s; } a.title-btn:hover { background: #4f46e5; text-decoration: none; } h2 { font-size: 1.4rem; margin-top: 3rem; border-bottom: 1px solid #e0e0e0; padding-bottom: 0.3rem; } h3 { font-size: 1.1rem; margin-top: 2rem; } a { color: #0066cc; text-decoration: none; } a:hover { text-decoration: underline; } blockquote { margin: 1.5rem 0; padding: 0.8rem 1.2rem; border-left: 4px solid #0066cc; background: #f4f8ff; color: #333; } blockquote p { margin: 0; } blockquote.example { border-left: 4px solid #6366f1; background: #f8f8fb; padding: 0.8rem 1.2rem 0.2rem; } blockquote.example .example-label { display: inline-block; font-size: 0.75rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: #6366f1; margin-bottom: 0.5rem; } blockquote.schema { border-left: 4px solid #6366f1; background: #f8f8fb; padding: 0.5rem 1.2rem; margin-bottom: 0; border-bottom: none; border-radius: 6px 6px 0 0; } blockquote.schema p { margin: 0; font-size: 0.85rem; font-weight: 600; color: #6366f1; } blockquote.schema + table { border: 1px solid #c7c7f0; border-top: none; border-radius: 0 0 6px 6px; overflow: hidden; margin-top: 0; } blockquote.schema + table th { background: #ebebfa; border-color: #c7c7f0; } blockquote.schema + table td { border-color: #c7c7f0; } code { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.88em; background: #f3f3f3; padding: 0.15em 0.4em; border-radius: 3px; } pre { background: #f6f6f7; color: #1a1a1a; border: 1px solid #e4e4e7; padding: 1.2rem 1.4rem; border-radius: 6px; overflow-x: auto; line-height: 1.5; } pre code { background: none; padding: 0; font-size: 0.85em; color: inherit; } table { border-collapse: collapse; width: 100%; margin: 1.2rem 0; font-size: 0.93em; display: block; overflow-x: auto; } th { background: #f0f0f0; text-align: left; padding: 0.5rem 0.8rem; border: 1px solid #d0d0d0; } td { padding: 0.45rem 0.8rem; border: 1px solid #d0d0d0; } tr:nth-child(even) td { background: #fafafa; } hr { border: none; border-top: 1px solid #e8e8e8; margin: 2.5rem 0; } ul, ol { padding-left: 1.5rem; } li { margin: 0.3rem 0; } p { margin: 0.8rem 0; } em { color: #444; } .badge { display: inline-block; background: #e4e4e7; color: #3f3f46; font-size: 0.78em; font-weight: 700; padding: 0.1em 0.45em; border-radius: 5px; vertical-align: baseline; letter-spacing: 0.01em; } .site-header { background: #fff; border-bottom: 1px solid #e4e4e7; position: sticky; top: 0; z-index: 100; } .site-header-inner { max-width: 860px; margin: 0 auto; padding: 0.6rem 2rem; display: flex; align-items: center; gap: 1.5rem; } .site-header a.header-btn { display: inline-block; background: #6366f1; color: #fff; font-size: 0.9rem; font-weight: 700; padding: 0.3em 0.75em; border-radius: 6px; text-decoration: none; transition: background 0.15s; white-space: nowrap; } .site-header a.header-btn:hover { background: #4f46e5; text-decoration: none; } .site-header .header-tagline { color: #71717a; font-size: 0.85rem; flex: 1; } .site-header nav { display: flex; gap: 1.2rem; } .site-header nav a { color: #3f3f46; font-size: 0.85rem; font-weight: 500; text-decoration: none; } .site-header nav a:hover { color: #6366f1; text-decoration: none; } @media (max-width: 600px) { .site-header-inner { padding: 0.6rem 1rem; gap: 0.8rem; } .site-header .header-tagline { display: none; } .site-header nav { gap: 0.8rem; } } """ HEADER = """ """ def process(body: str, title_link: str | None = None, title_text: str = "ifc-commit") -> str: """Apply post-processing transformations to rendered HTML body.""" # Style blockquotes that open with Example body = re.sub( r'
\s*

Example', '

Example', body, ) # Style blockquotes that open with Schema body = re.sub( r'

\s*

Schema', '

Schema', body, ) # Protect headings from badge replacement body = re.sub( r'(]*>)(.*?)()', lambda m: m.group(1) + m.group(2).replace('ifc-commit', '\x00') + m.group(3), body, flags=re.DOTALL, ) # Badge-wrap plain-text occurrences of "ifc-commit" body = re.sub(r'(?ifc-commit', body) # Restore headings body = body.replace('\x00', 'ifc-commit') # Turn the h1 into a title-button link if title_link: body = body.replace( f'

{title_text}

', f'

{title_text}

', 1, ) return body def build(source: Path, output: Path, title: str, title_link: str | None = None, title_text: str = "ifc-commit"): body = md.render(source.read_text()) body = process(body, title_link=title_link, title_text=title_text) html = f""" {title} {HEADER}
{body}
""" output.write_text(html) print(f"Written: {output}") build( source=ROOT / "README.md", output=ROOT / "index.html", title="ifc-commit — Version your BIM", title_link="/", ) build( source=ROOT / "docs" / "research.md", output=ROOT / "research.html", title="ifc-commit — Research Notes", title_link="/research", title_text="Research Notes", )