#!/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'''
'''
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'''
← 左右滑动查看更多 →
'''
else:
timeline_body = f'''
{events_joined}
'''
return f'''
{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()