#!/usr/bin/env python3 """Generate interactive timeline HTML from JSON configuration.""" import argparse import html import json import sys from textwrap import dedent def escape(text): if text is None: return "" return html.escape(str(text), quote=True) import re _CSS_UNSAFE = re.compile(r'[<>{}\x00-\x08\x0b\x0e-\x1f]|' if icon: dot_html += f'{icon}' category_style = f' style="background:{color}"' if color else "" category_html = f'{category}' if category else "" details_html = "" expand_hint = "" if details: details_html = f'''
{details}
''' expand_hint = '点击展开 ▾' onclick = f' onclick="toggleDetail({index})"' if details else "" has_details_attr = ' data-has-details="true"' if details else "" side_label = "" if layout == "dual-side" and side: side_label = f'{escape(side)}' card_html = f'''
{category_html}
{date}
{title}
{"
" + summary + "
" if summary else ""} {details_html} {expand_hint}
''' if layout == "horizontal": return f'''
{dot_html}
{side_label}{card_html}
''' else: return f'''
{dot_html}
{side_label}{card_html}
''' def build_js(): return dedent("""\ function toggleDetail(index) { var detail = document.getElementById('detail-' + index); var card = document.getElementById('card-' + index); if (!detail) return; detail.classList.toggle('open'); card.classList.toggle('expanded'); var hint = card.querySelector('.tl-expand-hint'); if (hint) { hint.textContent = detail.classList.contains('open') ? '点击收起 ▴' : '点击展开 ▾'; } } document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { var openDetails = document.querySelectorAll('.tl-card-details.open'); openDetails.forEach(function(d) { d.classList.remove('open'); var card = d.closest('.tl-card'); if (card) { card.classList.remove('expanded'); var hint = card.querySelector('.tl-expand-hint'); if (hint) hint.textContent = '点击展开 ▾'; } }); } }); """) def build_html(config): layout = config["layout"] theme = config["theme"] title = escape(config["title"]) events = config["events"] css = build_css(theme, layout) js = build_js() events_html_parts = [] for i, ev in enumerate(events): events_html_parts.append(build_event_html(ev, i, layout)) events_joined = "\n".join(events_html_parts) if layout == "horizontal": timeline_body = f'''
{events_joined}
← 左右滑动查看更多 →
''' else: timeline_body = f'''
{events_joined}
''' return f''' {title}

{title}

{timeline_body}
''' def main(): args = parse_args() try: config = load_config(args) except FileNotFoundError: print(f"Error: config file not found: {args.config}", file=sys.stderr) sys.exit(1) except json.JSONDecodeError as e: print(f"Error: invalid JSON - {e}", file=sys.stderr) sys.exit(1) if not config["events"]: print("Error: no events in config", file=sys.stderr) sys.exit(1) result = build_html(config) if args.output: with open(args.output, "w", encoding="utf-8") as f: f.write(result) print(f"Timeline generated: {args.output}", file=sys.stderr) else: sys.stdout.write(result) if __name__ == "__main__": main()