Skip to content

Instantly share code, notes, and snippets.

@mistivia
Created January 8, 2026 11:11
Show Gist options
  • Select an option

  • Save mistivia/b2dbe3f7b1bba5e0658c811368cdeb36 to your computer and use it in GitHub Desktop.

Select an option

Save mistivia/b2dbe3f7b1bba5e0658c811368cdeb36 to your computer and use it in GitHub Desktop.
Typst To HTML using SVG
// 标题
#import "/template.typ": doc-template
#doc-template(
title: "标题",
date: "2025-09-27",
body: [
= 章节
正文
])
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; } }
#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
]
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