From c4c2a65bef6dea9b2e55a62b70aaf4baecf25a6b Mon Sep 17 00:00:00 2001 From: szdytom Date: Sun, 6 Jul 2025 22:41:13 +0800 Subject: [PATCH] init Signed-off-by: szdytom --- .gitignore | 5 + README.md | 10 ++ build.toml | 26 ++++++ main.typ | 26 ++++++ make.py | 202 ++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 + styles.typ | 237 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 509 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.toml create mode 100644 main.typ create mode 100644 make.py create mode 100644 requirements.txt create mode 100644 styles.typ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d0edd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pdf +build/ +.vscode +*.swp +fonts/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d35676a --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# 《线性代数应该这样学(第四版)》习题解答 + +## 构建 PDF + +1. 安装 [Python 3](https://www.python.org/downloads/),请不要安装过于老旧的版本; +1. 安装 Python 依赖:`pip install -r requirements.txt`; +1. 执行构建脚本:`python3 make.py`; +1. 得到输出文件 `main.pdf`。 + +如需要使用 `watch` 模式监听 Typst 源代码变化,可以用命令:`python3 make.py --mode w` diff --git a/build.toml b/build.toml new file mode 100644 index 0000000..53e0884 --- /dev/null +++ b/build.toml @@ -0,0 +1,26 @@ +[[fonts]] +url = "https://github.com/notofonts/noto-cjk/releases/download/Serif2.003/09_NotoSerifCJKsc.zip" +patterns = [".otf"] + +[[fonts]] +url = "https://github.com/notofonts/noto-cjk/releases/download/Sans2.004/08_NotoSansCJKsc.zip" +patterns = [".otf"] + +[[fonts]] +url = "https://github.com/TrionesType/zhuque/releases/download/v0.211/ZhuqueFangsong-v0.211.zip" +patterns = [".ttf"] + +#[[fonts]] +#url = "https://github.com/tonsky/FiraCode/releases/download/6.2/Fira_Code_v6.2.zip" +#patterns = [".ttf"] + +[typst] +version = "0.13.1" +aarch64-darwin = "https://github.com/typst/typst/releases/download/v0.13.1/typst-aarch64-apple-darwin.tar.xz" +aarch64-windows = "https://github.com/typst/typst/releases/download/v0.13.1/typst-aarch64-pc-windows-msvc.zip" +aarch64-linux = "https://github.com/typst/typst/releases/download/v0.13.1/typst-aarch64-unknown-linux-musl.tar.xz" +armv7-linux = "https://github.com/typst/typst/releases/download/v0.13.1/typst-armv7-unknown-linux-musleabi.tar.xz" +riscv64-linux = "https://github.com/typst/typst/releases/download/v0.13.1/typst-riscv64gc-unknown-linux-gnu.tar.xz" +x86_64-darwin = "https://github.com/typst/typst/releases/download/v0.13.1/typst-x86_64-apple-darwin.tar.xz" +x86_64-windows = "https://github.com/typst/typst/releases/download/v0.13.1/typst-x86_64-pc-windows-msvc.zip" +x86_64-linux = "https://github.com/typst/typst/releases/download/v0.13.1/typst-x86_64-unknown-linux-musl.tar.xz" diff --git a/main.typ b/main.typ new file mode 100644 index 0000000..dc133e5 --- /dev/null +++ b/main.typ @@ -0,0 +1,26 @@ +#import "styles.typ": project, setup_main_text + +#show: project.with("线性代数应该这样学 习题解答") + +#let toc = (( + title: [向量空间], + sections: 1 +),) + +#[ +#show: setup_main_text + +#{ + +for i in range(0, toc.len()) { + pagebreak(weak: true) + let chapter = toc.at(i) + heading(chapter.title, level: 1) + for j in range(0, chapter.sections) { + include "sections/" + numbering("1A", i + 1, j + 1) + ".typ" + } +} + +} + +] \ No newline at end of file diff --git a/make.py b/make.py new file mode 100644 index 0000000..d692a52 --- /dev/null +++ b/make.py @@ -0,0 +1,202 @@ +import toml +import requests +import zipfile +import tarfile +import platform +import os +import argparse +from pathlib import Path +from tqdm import tqdm + +def executable_name(name): + if platform.system().lower() == "windows": + return f"{name}.exe" + return name + +# 读取build.toml文件 +with open('build.toml', 'r') as f: + config = toml.load(f) + +# 创建临时目录和字体目录 +temp_dir = Path('build') +fonts_dir = Path('fonts') +typst_ver = config["typst"]["version"] +typst_bin_path = temp_dir / executable_name(f"typst-{typst_ver}") +temp_dir.mkdir(parents=True, exist_ok=True) +fonts_dir.mkdir(parents=True, exist_ok=True) + +# 下载文件的函数 +def download_file(url, destination): + """ + 下载文件并显示进度条 + + :param url: 文件的URL + :param destination: 文件保存的路径 + """ + try: + # 使用流式下载 + with requests.get(url, stream=True) as response: + response.raise_for_status() # 检查请求是否成功 + total_size = int(response.headers.get('content-length', 0)) + # 使用 tqdm 显示下载进度 + with open(destination, 'wb') as f, tqdm( + desc=destination.name, + total=total_size, + unit='B', + unit_scale=True, + unit_divisor=1024, + ) as pbar: + for chunk in response.iter_content(chunk_size=8192): + if chunk: # 过滤掉保持连接的块 + f.write(chunk) + pbar.update(len(chunk)) + print(f"Downloaded {destination.name} to {destination.parent}.") + except requests.exceptions.RequestException as e: + print(f"Failed to download {url}: {e}") + # 如果下载失败,删除 .part 文件 + if destination.exists(): + destination.unlink() + raise # 抛出异常,让调用者处理 + +# 下载并提取字体文件 +def prepare_fonts(): + ok = True + for font in config['fonts']: + url = font['url'] + patterns = font['patterns'] + zip_name = Path(url).name # 获取压缩包文件名 + zip_path = temp_dir / zip_name # 压缩包本地路径 + zip_part_path = temp_dir / f"{zip_name}.part" # 下载中的临时文件 + + # 检查压缩包是否已存在 + if zip_path.exists(): + print(f"{zip_name} already exists in {temp_dir}. Skipping download.") + else: + # 下载压缩包 + print(f"Downloading {url}...") + try: + download_file(url, zip_part_path) + # 下载完成后,将 .part 文件重命名为最终文件名 + zip_part_path.rename(zip_path) + except requests.exceptions.RequestException: + ok = False + continue # 如果下载失败,跳过当前字体 + + # 解压缩并提取匹配的文件 + print(f"Extracting {zip_path}...") + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + for file in zip_ref.namelist(): + if any(file.endswith(pattern) for pattern in patterns): + target_file = fonts_dir / Path(file).name + # 检查文件是否已经存在于fonts目录 + if target_file.exists(): + print(f"{target_file.name} already exists in {fonts_dir}. Skipping extraction.") + continue + # 提取文件 + print(f"Extracting {file}...") + zip_ref.extract(file, temp_dir) + # 将文件移动到fonts目录 + extracted_file = temp_dir / file + extracted_file.rename(target_file) + + if ok: + print("Fonts download and extraction completed.") + else: + print("Fonts download and extraction completed with errors.") + return ok + +def get_system_info(): + """获取当前系统的操作系统和架构信息""" + system = platform.system().lower() + machine = platform.machine().lower() + + if machine == "amd64": + machine = "x86_64" + elif machine == "arm64": + machine = "aarch64" + elif machine == "armv7l": + machine = "armv7" + + return f"{machine}-{system}" + +def prepare_typst(): + """下载并解压 typst 可执行文件""" + system_info = get_system_info() + typst_url = config["typst"][system_info] + typst_archive_name = Path(typst_url).name + typst_archive_path = temp_dir / typst_archive_name + typst_part_path = temp_dir / f"{typst_archive_name}.part" + + # 检查是否已经下载 + if typst_bin_path.exists(): + print(f"Typst already exists in {temp_dir}. Skipping download.") + return True + + # 下载 typst + if not typst_archive_path.exists(): + print(f"Downloading {typst_url}...") + try: + download_file(typst_url, typst_part_path) + typst_part_path.rename(typst_archive_path) + except requests.exceptions.RequestException as e: + print(f"Failed to download typst: {e}") + return False + else: + print(f"Skipped download of {typst_archive_name}") + + # 解压 typst + print(f"Extracting {typst_archive_path}...") + typst_exe_name = executable_name("typst") + + if typst_archive_path.suffix == ".zip": + with zipfile.ZipFile(typst_archive_path, 'r') as zip_ref: + for file in zip_ref.namelist(): + if file.endswith(typst_exe_name): + zip_ref.extract(file, temp_dir) + extracted_file = temp_dir / file + extracted_file.rename(typst_bin_path) + elif typst_archive_path.suffixes == [".tar", ".xz"]: + with tarfile.open(typst_archive_path, 'r:xz') as tar_ref: + for file in tar_ref.getmembers(): + if file.name.endswith(typst_exe_name): + tar_ref.extract(file, temp_dir) + extracted_file = temp_dir / file.path + extracted_file.rename(typst_bin_path) + + # 确保 typst 可执行文件存在 + if not typst_bin_path.exists(): + print(f"Failed to find typst executable in {temp_dir}.") + return False + + print(f"Typst downloaded and extracted to {typst_bin_path}.") + return True + +def invoke_typst(mode="c"): + """调用 typst 命令""" + if not typst_bin_path.exists(): + print("Typst executable not found.") + return False + + command = [str(typst_bin_path), mode, "--font-path", str(fonts_dir), "--ignore-system-fonts", "main.typ"] + print("Executing command: ", " ".join(command)) + try: + os.execv(str(typst_bin_path), command) + except OSError as e: + print(f"Failed to execute typst command: {e}") + return False + +def main(): + parser = argparse.ArgumentParser(description="Run Typst with specified mode.") + parser.add_argument('--mode', type=str, choices=['w', 'c'], default='c', + help="Mode to run Typst: 'w' for watch mode, 'c' for compile mode.") + args = parser.parse_args() + + if not prepare_fonts(): + return + if not prepare_typst(): + return + + invoke_typst(mode=args.mode) + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..70f9a5f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +toml==0.10.2 +requests==2.32.3 +tqdm==4.67.1 diff --git a/styles.typ b/styles.typ new file mode 100644 index 0000000..8186a5d --- /dev/null +++ b/styles.typ @@ -0,0 +1,237 @@ +#import "@preview/showybox:2.0.4": showybox +#import "@preview/tableau-icons:0.1.0" as tbl +#import "@preview/cetz:0.3.2" +#import "@preview/cetz-plot:0.1.1": plot + +#let zhfont_sans = ("Noto Sans CJK SC") +#let zhfont_serif = ("Noto Serif CJK SC") +#let zhfont_fangsong = ("Zhuque Fangsong (technical preview)", "Noto Serif CJK SC") +#let monofont = ("Fira Code") + +#let theme_color = color.blue + +#let tab = h(2em) +#let halftab = h(1em) +#let ee = "e" +#let ii = "i" + +#let showy_wrapper(title: "", wrap-border: none, ..args) = { + let b = if title != "" { + showybox(title: text(font: zhfont_sans, title), ..args) + } else { + showybox(title: title, ..args) + } + + if wrap-border == none { + b + } else { + set align(center) + block(inset: ( + left: wrap-border, + ), width: 100%, b) + } +} + +#let simple_box = showy_wrapper.with( + breakable: true, + title-style: ( + weight: 900, + color: theme_color.darken(40%), + sep-thickness: 0pt, + font: zhfont_sans + //align: center + ), + frame: ( + title-color: theme_color.lighten(80%), + border-color: theme_color.darken(40%), + thickness: (left: 1pt), + radius: 0pt + ), +) + +#let problem_box = showy_wrapper.with( + breakable: false, + frame: ( + title-color: theme_color.lighten(80%), + border-color: theme_color.darken(40%), + thickness: (left: 1pt), + radius: 0pt, + align: left, + ), +) + +#let unset-list-indent(body) = { + set list(indent: 0.5em) + set enum(indent: 0.5em) + body +} + +#let project(title, body) = { + set document(title: title) + set text(font: zhfont_serif, lang: "zh") + set page( + // 1/16 787 x 1092 + width: 787mm / 4, + height: 1092mm / 4, + ) + + set par( + spacing: 1.2em, + leading: 0.75em, + ) + set list(marker: (sym.square.filled.small, [--]), indent: 2.5em) + set enum(indent: 2.5em) + show heading: set text(font: zhfont_sans, weight: "semibold") + set par(justify: true) + set text(11pt) + show heading.where(level: 3): set text(14pt) + show figure.caption: set text(9pt, font: zhfont_fangsong) + show footnote.entry: set text(9pt, font: zhfont_fangsong) + set table(stroke: 1pt + theme_color, inset: 5pt) + set grid(stroke: 1pt + theme_color) + set highlight(fill: none, stroke: ( + bottom: 4pt + theme_color.lighten(80%) + )) + + v(10fr) + h(2fr) + [ + #set text(2em, weight: "light", font: zhfont_sans) + #title + ] + v(3fr) + + body +} + +#let note(body) = { + text(body, 9pt, font: zhfont_fangsong) +} + +#let boxed-figure(body, wrap-placed: false, ..args) = { + // TODO: wrap placed figures: https://github.com/typst/typst/issues/5181 + figure(box( + inset: 5pt, + stroke: 1pt + theme_color, + body + ), ..args) +} + +#let fancy_term_box(title, value) = { + box(baseline: 3pt,{ + box(fill: theme_color.lighten(80%), inset: 3pt, text([#title], fill: theme_color.darken(40%), font: zhfont_sans, weight: "medium")) + box(fill: theme_color.lighten(20%), inset: 3pt, text([#value], fill: white, font: zhfont_sans, weight: "medium"))}, + ) +} + +#let setup_main_text(body) = { + pagebreak(to: "odd") + counter(page).update(1) + show heading.where(level: 1): it => { + counter("chapter_N").step() + counter("section_N").update(0) + block(width: 100%, { + set text(15pt, font: zhfont_sans, weight: "medium") + grid( + columns: (auto, 1fr), + inset: 0.4em, + stroke: none, + grid.cell( + fill: theme_color.lighten(20%), + { + set text(fill: white) + "第" + context counter("chapter_N").display("1") + "章" + }), + grid.cell( + fill: theme_color.lighten(80%), + { + it.body + }) + ) + v(0.5em) + }) + } + + show heading.where(level: 2): it => { + counter("section_N").step() + counter(figure.where(kind: "exercise-problem")).update(0) + block(width: 100%, { + set text(30pt, font: zhfont_sans, weight: "light") + block(stroke: (bottom: 10pt + theme_color.lighten(80%),), inset: -2pt)[ + #context{ + counter("chapter_N").display("1") + counter("section_N").display("A") + } + #h(10pt) + #it.body + ] + v(10pt) + }) + } + + set page(footer: context { + let this_page = counter(page).get().at(0) + let isleft = calc.even(this_page) + set align(left) if isleft + set align(right) if not isleft + set text(9pt, font: zhfont_sans, fill: theme_color.darken(20%)) + + let prev_headers = query(selector().before(here())) + let book_title = query(selector()).first().text + let footer_content = if isleft { + book_title + } else { + if prev_headers.len() > 0 { + prev_headers.last() + } else { + "" + } + } + + stack(dir: if isleft { ltr } else { rtl }, + spacing: 1em, + str(this_page), + footer_content, + ) + }) + + show figure.where(kind: "exercise-problem"): it => { + let cat_display = "习题" + set align(left) + problem_box({ + context fancy_term_box(cat_display, it.counter.get().at(0)) + h(0.5em) + it.body + }) + } + + body +} + +#let exercise_sol(e, s, type: "proof") = { + figure(e, kind: "exercise-problem", supplement: "习题") + s +} + +#let ploting-styles = ( + mark: (fill: theme_color.lighten(80%), stroke: theme_color), + + nothing: (fill: none, stroke: none), + + s_l20: (stroke: theme_color.lighten(20%)), + s: (stroke: theme_color), + s_d20: (stroke: theme_color.darken(20%)), + s_hl: (stroke: theme_color.darken(20%) + 2pt), + s_hl_l20: (stroke: theme_color.lighten(20%) + 2pt), + s_black: (stroke: black), + + f_l80: (stroke: none, fill: theme_color.lighten(80%)), + f_l90: (stroke: none, fill: theme_color.lighten(90%)), + f_l95: (stroke: none, fill: theme_color.lighten(95%)), + + axis: cetz.draw.set-style(axes: (stroke: .5pt, tick: (stroke: .5pt))), +) + +#let plot-point(x, y) = plot.add(((x, y),), mark: "o", mark-style: ploting-styles.mark, style: ploting-styles.s)