Created
January 8, 2026 11:11
-
-
Save mistivia/b2dbe3f7b1bba5e0658c811368cdeb36 to your computer and use it in GitHub Desktop.
Typst To HTML using SVG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 标题 | |
| #import "/template.typ": doc-template | |
| #doc-template( | |
| title: "标题", | |
| date: "2025-09-27", | |
| body: [ | |
| = 章节 | |
| 正文 | |
| ]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| body a { | |
| color: #1e70bf; | |
| text-decoration: none | |
| } | |
| body a:visited { | |
| color: #1e70bf | |
| } | |
| body a:hover { | |
| text-decoration: underline | |
| } | |
| body code:not(pre>code) { | |
| background-color: #eee; | |
| color: #212121; | |
| border-radius: 0.5em; | |
| padding: 0.2em; | |
| } | |
| body figcaption { | |
| color: #666 | |
| } | |
| html { | |
| height: 100% | |
| } | |
| @font-face { | |
| font-family: "sans-chinese"; | |
| src: local("WenQuanYi Micro Hei"), local("PingFang SC"), local("Noto Sans SC"), local("Noto Sans CJK SC"), local("Source Han Sans CN"), local("Microsoft YaHei"), local("PingFang TC"), local("Noto Sans TC"), local("Noto Sans CJK TC"), local("Source Han Sans TW"), local("Microsoft JhengHei"), local("sans-serif"); | |
| unicode-range: U+2000-FFFF | |
| } | |
| body { | |
| font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", sans-chinese, monospace; | |
| line-height: 1.4; | |
| margin: 0; | |
| min-height: 100%; | |
| overflow-wrap: break-word; | |
| padding: 20px; | |
| font-size: 1.2em; | |
| text-autospace: normal; | |
| max-width: 1024px; | |
| margin: auto; | |
| } | |
| .post-meta { | |
| text-align: right | |
| } | |
| h2, | |
| h3, | |
| h4, | |
| h5, | |
| h6 { | |
| margin-top: 3rem | |
| } | |
| p { | |
| margin: 1rem 0 | |
| } | |
| li { | |
| margin: 0.4rem 0 | |
| } | |
| *:target { | |
| background: yellow | |
| } | |
| .w { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| padding: 4rem 2rem | |
| } | |
| .toc { | |
| border: thin solid black; | |
| padding: 1rem | |
| } | |
| pre { | |
| background-color: #eee; | |
| border-radius: 0.5em; | |
| color: #212121; | |
| padding: 1em; | |
| font-family: 'Menlo', 'Monaco', 'Consolas', 'Lucida Console', 'Courier New', | |
| 'Liberation Mono', 'Ubuntu Mono', 'DejaVu Sans Mono', 'Source Code Pro', | |
| Courier, 'PingFang SC', 'Microsoft YaHei', 'WenQuanYi Micro Hei', monospace; | |
| overflow-x: auto; | |
| } | |
| .back { | |
| background-color: #fff; | |
| padding: 0; | |
| } | |
| table { | |
| width: 100%; | |
| } | |
| table, | |
| th, | |
| td { | |
| border: thin solid black; | |
| border-collapse: collapse; | |
| padding: 0.4rem | |
| } | |
| code:not(pre>code) { | |
| padding: 0.1em 0.2em; | |
| font-size: 90% | |
| } | |
| code.has-jax { | |
| -webkit-font-smoothing: antialiased; | |
| background: inherit !important; | |
| border: none !important; | |
| font-size: 100% | |
| } | |
| code { | |
| font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", sans-chinese, monospace | |
| } | |
| blockquote { | |
| font-style: normal; | |
| margin-left: 32px; | |
| border-left: 4px solid #CCC; | |
| padding-left: 8px | |
| } | |
| img { | |
| max-width: 100%; | |
| display: block; | |
| margin: 0 auto | |
| } | |
| figcaption { | |
| text-align: center; | |
| opacity: 0.5 | |
| } | |
| .tag { | |
| white-space: nowrap; | |
| } | |
| body { background-color: #eee; padding: 0;} | |
| .back {padding:20px; background-color: #eee;} | |
| .main { text-align: center; } | |
| .container { background-color: #f1e9d2; margin: 3px 2vw; display: inline-block; } | |
| .typst-doc { width: 90vw; height: auto; } | |
| @media (min-width: 730px) { .typst-doc { width: 700px; height: auto; } } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #let doc-template( | |
| title: "TITLE", | |
| date: "1970-01-01", | |
| body: "" | |
| ) = [ | |
| #show link: underline | |
| #show raw.where(block: true): block.with( | |
| // fill: luma(240), | |
| inset: (left: 2.5em, top: 1em, bottom: 1em), | |
| width: 100% | |
| ) | |
| #show raw: set text(font: ( | |
| "Courier Prime", | |
| "KingHwa_OldSong" | |
| )) | |
| #set text(font: ( | |
| // "Libertinus Serif", | |
| // "Source Han Serif", | |
| "KingHwa_OldSong" | |
| )) | |
| #set heading(numbering: "1.") | |
| #set page( | |
| paper: "a5", | |
| number-align: center, | |
| ) | |
| #set page(numbering: "1") | |
| #counter(page).update(1) | |
| #align(center, text(27pt)[ | |
| #title | |
| ]) | |
| #align(center, [#par(first-line-indent: 0em)[*#date*] #v(0.5em)]) | |
| #set par( | |
| first-line-indent: 2em, | |
| justify: true, | |
| leading: 0.8em, | |
| spacing: 0.8em, | |
| ) | |
| #set list(indent: 2em) | |
| #set enum(indent: 2em) | |
| #set terms(indent: 2em) | |
| #show heading: it => { | |
| it | |
| par()[#text(size:0.5em)[#h(0.0em)]] | |
| } | |
| #show list: it => { | |
| par()[#text(size:0.5em)[#h(0.0em)]] | |
| it | |
| par()[#text(size:0.5em)[#h(0.0em)]] | |
| } | |
| #show raw.where(block: true): it => { | |
| par()[#text(size:0.5em)[#h(0.0em)]] | |
| it | |
| par()[#text(size:0.5em)[#h(0.0em)]] | |
| } | |
| #body | |
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import argparse | |
| import os | |
| import shutil | |
| import subprocess | |
| import re | |
| import glob | |
| def get_title(typ_path): | |
| """读取typ文件的第一行获取标题""" | |
| title = "Untitled" | |
| try: | |
| with open(typ_path, 'r', encoding='utf-8') as f: | |
| first_line = f.readline().strip() | |
| # 匹配 // 后面的内容 | |
| if first_line.startswith("//"): | |
| title = first_line.lstrip("/").strip() | |
| except Exception as e: | |
| print(f"Warning: Could not read title from {typ_path}: {e}") | |
| return title | |
| def clean_svgs(directory): | |
| """删除指定目录下的所有 svg 文件""" | |
| if not os.path.exists(directory): | |
| return | |
| svgs = glob.glob(os.path.join(directory, "*.svg")) | |
| for svg in svgs: | |
| try: | |
| os.remove(svg) | |
| except OSError as e: | |
| print(f"Error removing {svg}: {e}") | |
| def natural_sort_key(s): | |
| """用于文件名的自然排序 (index1, index2, index10)""" | |
| return [int(text) if text.isdigit() else text.lower() | |
| for text in re.split('([0-9]+)', s)] | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Compile Typst to SVG and embed in HTML.") | |
| parser.add_argument("typ_path", help="Path to the .typ file") | |
| args = parser.parse_args() | |
| typ_path = args.typ_path | |
| # 1. 计算路径 | |
| # 假设输入是 a/b/index.typ | |
| # 目标目录是 output/a/b/ | |
| input_dir = os.path.dirname(typ_path) | |
| if not input_dir: | |
| input_dir = "." # 处理当前目录的情况 | |
| output_dir = os.path.join("output", input_dir) | |
| # 确保输出目录存在 | |
| os.makedirs(output_dir, exist_ok=True) | |
| # 2. 获取标题 | |
| title = get_title(typ_path) | |
| print(f"Detected Title: {title}") | |
| # 3. 预清理:删除旧的 SVG | |
| print(f"Cleaning old SVGs in {output_dir}...") | |
| clean_svgs(output_dir) | |
| # 4. 编译 Typst | |
| # 构建输出文件模式,例如 output/xxx/xxx/index{0p}.svg | |
| # 注意:typst 命令行通常使用 index{n}.svg 或 index{p}.svg,这里严格按照你的要求 index{0p}.svg | |
| output_pattern = os.path.join(output_dir, "index{0p}.svg") | |
| cmd = [ | |
| "typst", "compile", | |
| "--root", ".", | |
| typ_path, | |
| output_pattern | |
| ] | |
| print(f"Running: {' '.join(cmd)}") | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| if result.returncode != 0: | |
| print("Typst compilation failed:") | |
| print(result.stderr) | |
| return | |
| # 5. 读取生成的 SVG 并生成 HTML | |
| # 查找生成的文件,通常是 index1.svg, index2.svg ... | |
| generated_svgs = glob.glob(os.path.join(output_dir, "index*.svg")) | |
| # 按文件名中的数字进行自然排序 | |
| generated_svgs.sort(key=natural_sort_key) | |
| if not generated_svgs: | |
| print("No SVG files were generated.") | |
| return | |
| svg_contents = [] | |
| for svg_file in generated_svgs: | |
| with open(svg_file, 'r', encoding='utf-8') as f: | |
| content = f.read() | |
| # 将 SVG 内容包装在 container 中 | |
| div_block = f'<div class="container">\n{content}\n</div><br>' | |
| svg_contents.append(div_block) | |
| # 拼接所有 SVG 内容 | |
| all_svgs_html = "\n".join(svg_contents) | |
| # HTML 模板 | |
| html_content = f"""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <link rel="stylesheet" href="./style.css"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>{title}</title> | |
| </head> | |
| <body> | |
| <pre class="back"><a href="../">../</a></pre> | |
| <div class="main">{all_svgs_html}</div> | |
| </body> | |
| </html>""" | |
| # 写入 HTML 文件 | |
| output_html_path = os.path.join(output_dir, "index.html") | |
| with open(output_html_path, 'w', encoding='utf-8') as f: | |
| f.write(html_content) | |
| print(f"Generated HTML at: {output_html_path}") | |
| # 6. 后清理:删除生成的 SVG | |
| print("Cleaning up intermediate SVG files...") | |
| clean_svgs(output_dir) | |
| print("Done.") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment