3737 import zstandard
3838
3939# ── Paths ────────────────────────────────────────────────────────────────────
40-
4140DOCS_DIR = os .path .dirname (os .path .abspath (__file__ ))
4241SRC_DIR = os .path .join (DOCS_DIR , "src" )
4342OUT_DIR = os .path .join (DOCS_DIR , "site" )
4443
4544# ── Sidebar definition (order matters) ──────────────────────────────────────
46-
4745SIDEBAR = [
4846 ("Getting Started" , [
4947 ("getting_started/index" , "Home" ),
147145 ]),
148146]
149147
150-
151148def slug_basename (slug ):
152149 """Extract the filename part from a possibly prefixed slug (e.g. 'core/entity' → 'entity')."""
153150 return slug .rsplit ("/" , 1 )[- 1 ]
154151
155152# ── Frontmatter parser ──────────────────────────────────────────────────────
156-
157153def parse_frontmatter (text ):
158154 """Extract YAML-like frontmatter and return (metadata_dict, remaining_text)."""
159155 if not text .startswith ("---" ):
160156 return {}, text
157+
161158 end = text .find ("---" , 3 )
162159 if end == - 1 :
163160 return {}, text
161+
164162 fm_block = text [3 :end ].strip ()
165163 body = text [end + 3 :].strip ()
166164 meta = {}
167165 for line in fm_block .splitlines ():
168166 if ":" in line :
169167 key , val = line .split (":" , 1 )
170168 meta [key .strip ()] = val .strip ()
169+
171170 return meta , body
172171
173172# ── Syntax highlighter (simple Python-aware) ────────────────────────────────
174-
175173PYTHON_KEYWORDS = {
176174 'False' , 'None' , 'True' , 'and' , 'as' , 'assert' , 'async' , 'await' ,
177175 'break' , 'class' , 'continue' , 'def' , 'del' , 'elif' , 'else' , 'except' ,
@@ -203,21 +201,24 @@ def highlight_python(code):
203201 classes = [cls for cls , _ in token_specs ]
204202
205203 def _replacer (m ):
204+ """Return the highlighted span for the first matching token group."""
206205 for i , cls in enumerate (classes ):
207206 if m .group (f'g{ i } ' ) is not None :
208207 return f'<span class="{ cls } ">{ m .group (f"g{ i } " )} </span>'
208+
209209 return m .group (0 )
210210
211211 return re .sub (combined , _replacer , code , flags = re .DOTALL )
212212
213-
214213def highlight_code_blocks (html_text ):
215214 """Find <pre><code class="language-python"> blocks and apply highlighting."""
216215 def replace_block (m ):
216+ """Replace a fenced code block with syntax-highlighted HTML."""
217217 lang = m .group (1 ) or ""
218218 inner = m .group (2 )
219219 if "python" in lang or "py" in lang :
220220 inner = highlight_python (inner )
221+
221222 return f'<pre><code class="language-{ lang } ">{ inner } </code></pre>'
222223
223224 html_text = re .sub (
@@ -228,7 +229,6 @@ def replace_block(m):
228229 return html_text
229230
230231# ── Markdown → HTML conversion ──────────────────────────────────────────────
231-
232232def convert_markdown (text ):
233233 """Convert markdown text to HTML with extensions."""
234234 md = markdown .Markdown (extensions = [
@@ -241,15 +241,14 @@ def convert_markdown(text):
241241 md .reset ()
242242 return html , toc_tokens
243243
244-
245244def rewrite_md_links (html_text ):
246245 """Rewrite .md links to .html links."""
247246 return re .sub (r'href="([^"#]+)\.md(#[^"]*)?"' , lambda m : f'href="{ m .group (1 )} .html{ m .group (2 ) or "" } "' , html_text )
248247
249-
250248def process_blockquotes (html_text ):
251249 """Convert blockquotes starting with bold markers into styled callouts."""
252250 def classify (m ):
251+ """Classify a blockquote as a callout based on its leading marker."""
253252 content = m .group (1 )
254253 if content .strip ().startswith ("<strong>Warning" ):
255254 cls = "callout callout-warn"
@@ -261,23 +260,21 @@ def classify(m):
261260 cls = "callout callout-info"
262261 else :
263262 cls = "callout callout-info"
263+
264264 return f'<div class="{ cls } ">{ content } </div>'
265- return re .sub (r'<blockquote>\s*(.*?)\s*</blockquote>' , classify , html_text , flags = re .DOTALL )
266265
266+ return re .sub (r'<blockquote>\s*(.*?)\s*</blockquote>' , classify , html_text , flags = re .DOTALL )
267267
268268# ── Tag formatting ───────────────────────────────────────────────────────────
269-
270269def format_ext_tags (html_text ):
271270 """Replace [ext] with a styled badge span."""
272271 return html_text .replace ('[ext]' , '<span class="ext-tag">ext</span>' )
273272
274273# ── HTML template ────────────────────────────────────────────────────────────
275-
276274def _section_key (name ):
277275 """Turn a section name into a compact localStorage key."""
278276 return re .sub (r'[^a-z0-9]+' , '-' , name .lower ()).strip ('-' )
279277
280-
281278def build_sidebar_html (current_slug ):
282279 """Generate the sidebar HTML."""
283280 parts = [
@@ -306,8 +303,8 @@ def build_sidebar_html(current_slug):
306303 parts .append (f' <li><a href="{ href } "{ active } >{ display } </a></li>' )
307304
308305 parts .extend ((' </ul>' , '</div>' ))
309- return "\n " .join (parts )
310306
307+ return "\n " .join (parts )
311308
312309def build_toc_sidebar (toc_tokens , current_slug ):
313310 """Build the sidebar with 'On This Page' TOC at the top, then nav sections."""
@@ -328,14 +325,15 @@ def build_toc_sidebar(toc_tokens, current_slug):
328325 toc_parts .append (f' <li><ul class="toc-sub" data-parent="{ token ["id" ]} ">' )
329326 for child in children :
330327 toc_parts .append (f' <li><a href="#{ child ["id" ]} ">{ child ["name" ]} </a></li>' )
328+
331329 toc_parts .append (' </ul></li>' )
330+
332331 toc_parts .extend ((' </ul>' , '</div>' ))
333332
334333 # Insert TOC right after the search box (first </div>)
335334 search_end = nav .find ('</div>' ) + len ('</div>' )
336335 return nav [:search_end ] + '\n ' + '\n ' .join (toc_parts ) + '\n ' + nav [search_end :]
337336
338-
339337TEMPLATE = """\
340338 <!DOCTYPE html>
341339<html lang="en">
@@ -354,33 +352,41 @@ def build_toc_sidebar(toc_tokens, current_slug):
354352 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
355353 <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,400..800&family=JetBrains+Mono:wght@400..700&display=swap" rel="stylesheet">
356354 <link rel="stylesheet" href="style.css">
355+
357356</head>
358357<body>
359358 <!-- Header -->
360359 <header class="site-header">
361360 <button class="menu-toggle" aria-label="Toggle menu">
362361 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
363362 <path d="M3 12h18M3 6h18M3 18h18"/>
363+
364364 </svg>
365+
365366 </button>
366367 <a href="index.html" class="header-brand">
367368 <span class="logo">Py</span>
368369 PyJavaBridge
370+
369371 </a>
370372 <div class="header-search">
371373 <svg class="header-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
372374 <input type="text" id="header-search" placeholder="Search docs… (Ctrl+K)" autocomplete="off">
373375 <div id="search-results" class="search-results"></div>
376+
374377 </div>
375378 <nav class="header-nav">
376379 <a href="index.html">Docs</a>
377380 <a href="examples.html">Examples</a>
378381 <a href="https://github.com/Omena0/PyJavaBridge">GitHub</a>
382+
379383 </nav>
384+
380385 </header>
381386
382387 <!-- Sidebar -->
383388 <aside class="sidebar">
389+
384390{sidebar}
385391 </aside>
386392 <div class="sidebar-overlay"></div>
@@ -390,28 +396,33 @@ def build_toc_sidebar(toc_tokens, current_slug):
390396 <div class="content">
391397 {subtitle_html}
392398 {body}
399+
393400 </div>
394401 <footer class="site-footer">
395402 © Omena0 2026 · PyJavaBridge Documentation
403+
396404 </footer>
405+
397406 </main>
398407
399408 <!-- Back to top -->
400409 <button class="back-to-top" aria-label="Back to top">
401410 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
402411 <path d="M18 15l-6-6-6 6"/>
412+
403413 </svg>
414+
404415 </button>
405416
406417 <script id="zstd-data" type="text/plain">{search_index_zstd_b64}</script>
407418 <script src="https://cdn.jsdelivr.net/npm/fzstd@0.1.1/umd/index.js" async></script>
408419 <script src="script.js"></script>
420+
409421</body>
410422</html>
411423"""
412424
413425# ── Builder ──────────────────────────────────────────────────────────────────
414-
415426def build_page (slug ):
416427 """Build a single page from its markdown source."""
417428 src_path = os .path .join (SRC_DIR , f"{ slug } .md" )
@@ -463,18 +474,18 @@ def build_page(slug):
463474 with open (out_path , "w" , encoding = "utf-8" ) as f :
464475 f .write (out_html )
465476
466-
467477def get_all_slugs ():
468478 """Get all markdown file slugs from the sidebar definition."""
469479 slugs = []
470480 for _ , pages in SIDEBAR :
471481 slugs .extend (slug for slug , _ in pages )
472- return slugs
473482
483+ return slugs
474484
475485_search_index_zstd_b64 = ""
476486
477487def main ():
488+ """Build the static documentation site from markdown sources."""
478489 global _search_index_zstd_b64
479490 print ("📖 Building PyJavaBridge docs..." )
480491 print (f" Source: { SRC_DIR } " )
@@ -499,63 +510,86 @@ def main():
499510 # Build search index first (needed for inlining into pages)
500511 for slug in slugs :
501512 src = os .path .join (SRC_DIR , f"{ slug } .md" )
513+
502514 if os .path .exists (src ):
503515 with open (src , "r" , encoding = "utf-8" ) as f :
504516 raw = f .read ()
517+
505518 meta , body_md = parse_frontmatter (raw )
519+
506520 title = meta .get ("title" , slug .capitalize ())
507521 current_heading = title
522+
508523 sections = []
509524 in_table = False
525+
510526 table_first_cols = []
511527 table_header_seen = False
528+
512529 for line in body_md .split ("\n " ):
513530 stripped = line .strip ()
531+
514532 if stripped .startswith ("|" ) and "|" in stripped [1 :]:
515533 # Table row
516534 if re .match (r'^\|[\s\-:|]+\|$' , stripped ):
517535 table_header_seen = True # separator marks end of header
518536 continue
537+
519538 if not in_table :
520539 # First row is the header — skip it
521540 in_table = True
522541 table_header_seen = False
523542 continue
543+
524544 if not table_header_seen :
525545 continue # still in header somehow
546+
526547 cols = [c .strip () for c in stripped .strip ("|" ).split ("|" )]
548+
527549 if cols :
528550 col = re .sub (r'[`*\[\]()]' , '' , cols [0 ]).strip ()
551+
529552 if col :
530553 table_first_cols .append (col )
554+
531555 continue
556+
532557 else :
533558 if in_table and table_first_cols :
534559 sections .append ({"heading" : current_heading , "text" : ", " .join (table_first_cols )})
535560 table_first_cols = []
561+
536562 in_table = False
537563 table_header_seen = False
538564
539565 if stripped .startswith ("#" ):
540566 current_heading = stripped .lstrip ("#" ).strip ()
567+
541568 elif stripped and not stripped .startswith ("```" ) and not stripped .startswith ("---" ):
542569 clean = re .sub (r'[`*\[\]()]' , '' , stripped )
570+
543571 if clean :
544572 sections .append ({"heading" : current_heading , "text" : clean })
573+
545574 # Flush any remaining table
546575 if table_first_cols :
547576 sections .append ({"heading" : current_heading , "text" : ", " .join (table_first_cols )})
577+
548578 url = "index.html" if slug_basename (slug ) == "index" else f"{ slug_basename (slug )} .html"
549579 search_index .append ({"slug" : slug_basename (slug ), "title" : title , "url" : url , "sections" : sections })
550580
551581 import json
582+
552583 search_json = json .dumps (search_index , separators = (',' , ':' ))
553584 cctx = zstandard .ZstdCompressor (level = 22 )
585+
554586 compressed = cctx .compress (search_json .encode ('utf-8' ))
555587 _search_index_zstd_b64 = base64 .b64encode (compressed ).decode ('ascii' )
588+
556589 raw_size = len (search_json .encode ('utf-8' ))
557590 compressed_size = len (compressed )
558591 b64_size = len (_search_index_zstd_b64 )
592+
559593 print (f" Search index: { raw_size :,} bytes → { compressed_size :,} zstd → { b64_size :,} base64 ({ 100 * b64_size / raw_size :.1f} %)" )
560594
561595 # Build pages (with search index inlined)
@@ -568,6 +602,5 @@ def main():
568602
569603 print (f"\n ✅ Built { built } pages" )
570604
571-
572605if __name__ == "__main__" :
573606 main ()
0 commit comments