#!/usr/bin/env python3 """Build Ferese's static website and handbook from the project Markdown docs.""" from pathlib import Path from html import escape import json import re import shutil from urllib.parse import urlsplit from markdown_it import MarkdownIt ROOT = Path(__file__).resolve().parent.parent OUTPUT = ROOT / 'build' / 'site' REPO = 'https://github.com/ferese-wm/ferese' PAGES = [ ('index', 'Introduction', ROOT / 'site/content/index.md'), ('screenshots', 'Screenshots', ROOT / 'docs/screenshots.md'), ('why-ferese', 'Why Ferese', ROOT / 'docs/why-ferese.md'), ('installation', 'Installation', ROOT / 'docs/installation.md'), ('configuration', 'Configuration', ROOT / 'docs/configuration.md'), ('theme-engine', 'Themes', ROOT / 'docs/theme-engine.md'), ('theme-palette-sources', 'Theme palette sources', ROOT / 'docs/theme-palette-sources.md'), ('shortcuts', 'Shortcuts and gestures', ROOT / 'docs/shortcuts.md'), ('desktop-widgets', 'Desktop widgets', ROOT / 'docs/desktop-widgets.md'), ('notifications', 'Notifications', ROOT / 'docs/notifications.md'), ('locking', 'Lock screen', ROOT / 'docs/locking.md'), ('screen-sharing', 'Screen sharing', ROOT / 'docs/screen-sharing.md'), ('portals', 'Desktop portals', ROOT / 'docs/portals.md'), ('development', 'Development', ROOT / 'docs/development.md'), ] md = MarkdownIt('commonmark', {'html': False}).enable('table') if OUTPUT.exists(): shutil.rmtree(OUTPUT) OUTPUT.mkdir(parents=True) for name in ('index.html', 'styles.css', 'app.js'): shutil.copy2(ROOT / 'site' / name, OUTPUT / name) (OUTPUT / 'assets').mkdir(exist_ok=True) shutil.copytree(ROOT / 'site/assets', OUTPUT / 'assets', dirs_exist_ok=True) shutil.copytree(ROOT / 'docs/images', OUTPUT / 'assets', dirs_exist_ok=True) shutil.copy2(ROOT / 'packaging/icons/ferese.svg', OUTPUT / 'assets/ferese.svg') (OUTPUT / 'docs').mkdir(exist_ok=True) (OUTPUT / '.nojekyll').touch() search_index = [] def rewrite_link(href, source): url = urlsplit(href) if url.scheme or url.netloc or not url.path: return href basename = Path(url.path).stem if url.path.endswith('.md') and basename in {page[0] for page in PAGES}: return f'{basename}.html' + (f'#{url.fragment}' if url.fragment else '') path = (source.parent / url.path).resolve() if path.is_relative_to(ROOT / 'docs/images'): return '../assets/' + path.relative_to(ROOT / 'docs/images').as_posix() destination = path.relative_to(ROOT) return f'{REPO}/blob/main/{destination}' + (f'#{url.fragment}' if url.fragment else '') for page_number, (slug, title, source) in enumerate(PAGES): tokens = md.parse(source.read_text()) toc, ids, sections = [], set(), [] current = None for i, token in enumerate(tokens): if token.type == 'heading_open': heading = tokens[i + 1].content base = re.sub(r'[^\w\s-]', '', heading.lower()).strip() base = re.sub(r'[\s_]+', '-', base) anchor = base suffix = 1 while anchor in ids: anchor = f'{base}-{suffix}' suffix += 1 ids.add(anchor) token.attrSet('id', anchor) if token.tag != 'h1': toc.append((anchor, heading)) current = {'page': title, 'heading': heading, 'url': f'{slug}.html#{anchor}', 'text': ''} sections.append(current) elif current and token.type in ('inline', 'fence', 'code_block'): current['text'] += token.content + ' ' for child in token.children or []: if child.type == 'link_open': child.attrSet('href', rewrite_link(child.attrGet('href'), source)) elif child.type == 'image': image = urlsplit(child.attrGet('src')) if not image.scheme and not image.netloc: path = (source.parent / image.path).resolve() relative = path.relative_to(ROOT / 'docs/images') if not path.is_file(): raise FileNotFoundError(path) child.attrSet('src', f'../assets/{relative.as_posix()}') child.attrSet('loading', 'lazy') child.attrSet('decoding', 'async') search_index.extend(sections) body = md.renderer.render(tokens, md.options, {}) body = body.replace('', '
').replace('
', '') navigation = ''.join(f'{escape(t)}' for s, t, _ in PAGES) contents = ''.join(f'{escape(heading)}' for anchor, heading in toc) pager = '' if page_number: prev = PAGES[page_number - 1] pager += f'PREVIOUS← {escape(prev[1])}' if page_number + 1 < len(PAGES): nxt = PAGES[page_number + 1] pager += f'NEXT{escape(nxt[1])} →' doc = f''' {escape(title)} — Ferese handbook
{body}
View this guide on GitHub
''' (OUTPUT / 'docs' / f'{slug}.html').write_text(doc) (OUTPUT / 'docs/search-index.json').write_text(json.dumps(search_index, ensure_ascii=False)) print(f'Built website and {len(PAGES)} handbook pages at {OUTPUT}')