Files
NLI-doc/NLI-doc-template.typ
T
2026-09-04 16:08:38 +08:00

393 lines
14 KiB
Typst

// ============================================================
// A4 文档模板
// 封面:页边距 0,标题与图案 Bahnschrift / 信息 Source Code Pro
// 正文:上下 3.5cm,左右 2cm,中文 Source Han Sans SC / 英文 IBM Plex Sans
// 代码块:IBM Plex Mono
// ============================================================
// ---- 字体配置(可按需修改)----
#let cover-cjk = "SimHei" // 封面中文回退:黑体
#let cover-latin = "Source Code Pro" // 文档信息与签名:Source Code Pro
#let cover-title-cjk = "SimHei" // 封面中文标题
#let cover-title-latin = "Bahnschrift" // 封面英文标题
#let body-cjk = "Source Han Sans SC" // 正文中文
#let body-latin = "IBM Plex Sans" // 正文英文
#let mono-font = "IBM Plex Mono" // 代码块
// ---- 封面网格系统 ----
// 坐标系:左上角为原点,x 向右、y 向下;坐标记为 (x, y) 即 (列, 行)。
// 1 单位 = 1.2cm,一个单位方块为边长 1.5cm 的正方形。
#let unit = 1.2cm
#let sky = rgb("#4FC3F7") // 天蓝色
#let accent = rgb("#E9784B") // 珊瑚橙
// 在网格 (x, y) 处放置内容(以页面左上角为基准)
#let cell(x, y, body) = place(top + left, dx: x * unit, dy: y * unit, body)
// 实心单位正方形
#let sq(x, y, color) = cell(x, y, rect(width: unit, height: unit, fill: color, stroke: none))
// 单位等腰直角三角形,corner 指定直角所在角:
// "tl" 左上 / "tr" 右上 / "bl" 左下 / "br" 右下
#let tri(x, y, corner, color) = cell(x, y, {
let pts = if corner == "tr" { ((0pt, 0pt), (unit, 0pt), (unit, unit)) }
else if corner == "tl" { ((0pt, 0pt), (unit, 0pt), (0pt, unit)) }
else if corner == "bl" { ((0pt, 0pt), (0pt, unit), (unit, unit)) }
else { ((unit, 0pt), (unit, unit), (0pt, unit)) }
polygon(fill: color, stroke: none, ..pts)
})
// 单元格字母字号(NITLOAD 与主标题共用,保证视觉一致)
#let glyph-size = 30pt
// 在单位格内居中放置一个字母
#let glyph(x, y, ch, color) = cell(x, y, box(
width: unit, height: unit, inset: 0pt,
align(center + horizon, text(font: cover-title-latin, size: glyph-size, weight: "medium", fill: color)[#ch]),
))
// ---- 封面 ----
// 所有字段均可选,留空则自动跳过,实现自动排版。
#let cover(
title: none, // 主标题(中文)
title-en: none, // 主标题(英文,Source Code Pro 自行排版)
subtitle: none, // 兼容旧调用:英文分册题名
sub_title: none, // 中文分册题名
sub_title_en: none, // 英文分册题名
subtitle_en: none, // 兼容旧调用:英文分册题名
logo: none, // 图片路径,如 image("logo.png")
number: none, // 文档编号
author: none, // 作者(可为单个字符串或数组)
classification: none, // 密级,如 "内部"、"秘密"
info: (:), // 附加信息字典,如 (学号: "123", 指导教师: "张三")
date: none, // 日期
part-info: none, // 兼容旧调用:分册题名
fill: white, // 封面背景色
decorate: true, // 是否绘制天蓝/暖橙网格装饰色块
) = {
// 封面页:页边距为 0
set page(
paper: "a4",
margin: 0pt,
fill: fill,
header: none,
footer: none,
)
set text(font: (cover-latin, cover-cjk), lang: "zh")
if logo != none {
logo
}
// 网格装饰:NITLOAD 标识(坐标 (x, y) = (列, 行))。
if decorate {
// 天蓝色实心单位正方形
// 暖橙色实心单位正方形
sq(0, 0, sky); sq(1, 0, sky); sq(2, 0, accent)
sq(0, 1, sky); sq(2, 1, sky)
sq(0, 2, sky); sq(2, 2, sky)
sq(0, 3, sky); sq(2, 3, sky)
sq(0, 4, sky); sq(2, 4, sky)
sq(0, 5, sky); sq(2, 5, sky)
// NITLOAD 竖排字样(列 x=1, 行 y=0..6),N 为暖橙,其余天蓝
let letters = ("N", "I", "T", "L", "O", "A", "D")
for (i, ch) in letters.enumerate() {
glyph(1, i, ch, if i == 0 { accent } else { sky })
}
// 主标题:中文逐字对齐网格,连续西文按实际字宽紧凑排布。
let cs = if type(title) == str { title.clusters() } else { () }
// 中文视觉重心偏大,整体右移 0.1 格、下移 0.2 格
let cn-dx = 0.1 * unit
let cn-dy = 0.2 * unit
let is-narrow = c => str.to-unicode(c.codepoints().first()) < 0x2E80
// 西文开头时使用更明确的 0.2 格左边距,与西文标题和信息区形成视觉对齐。
let title-start-pad = 0pt
let title-width = 21cm - 2 * unit
let en-left-pad = 0pt
let en-box-width = 21cm - 2 * unit
// 分段:连续西文合为一段,其余每字一段
let segs = ()
let run = ""
for ch in cs {
if is-narrow(ch) { run += ch }
else {
if run != "" { segs.push(run); run = "" }
segs.push(ch)
}
}
if run != "" { segs.push(run) }
context {
// CJK 固定占一格;连续西文使用字体测得的自然宽度,不额外预留左右空白。
let cur-x = 0pt
let row = 0
for seg in segs {
let latin = is-narrow(seg.first())
let segment = text(
font: if latin { cover-title-latin } else { cover-title-cjk },
size: glyph-size,
fill: black,
)[#seg]
let segment-width = if latin { measure(segment).width } else { unit }
if cur-x != 0pt and cur-x + segment-width > title-width {
cur-x = 0pt
row += 1
}
let segment-align = if latin { left + horizon } else { center + horizon }
place(top + left,
dx: unit + title-start-pad + cur-x,
dy: (7 + row) * unit + cn-dy,
box(
width: segment-width,
height: unit,
inset: 0pt,
align(segment-align, segment),
))
cur-x += segment-width
}
let cn-rows = if segs.len() == 0 { 0 } else { row + 1 }
// 英文:左对齐于中文下方;多行时行距对齐网格(行距 = unit),左/上边距相等。
let en-rows = 0
let en-gap = if cn-rows > 0 and title-en != none { 0.25 * unit } else { 0pt }
if title-en != none {
let en1 = text(font: cover-title-latin, size: glyph-size, fill: black)[#title-en]
let lh = measure(en1).height
let pad = (unit - lh) / 2
let inner = {
set par(leading: unit - lh)
en1
}
let h = measure(box(width: en-box-width - en-left-pad, inner)).height
en-rows = calc.max(1, int(calc.round((h - lh) / unit)) + 1)
place(top + left,
dx: unit,
dy: (7 + cn-rows) * unit + en-gap,
box(width: en-box-width, inset: (left: en-left-pad, top: pad), inner))
}
let title-rows = cn-rows + en-rows
let title-end-y = (7 + title-rows) * unit + en-gap
let part-title-text = if sub_title != none { sub_title } else { none }
let part-title-en = if sub_title_en != none {
sub_title_en
} else if subtitle_en != none {
subtitle_en
} else {
subtitle
}
let part-box = if part-title-text != none or part-title-en != none or part-info != none {
let part-lines = ()
if part-title-text != none {
part-lines.push(text(
font: cover-title-cjk, size: 18pt, fill: black,
)[#part-title-text])
}
if part-title-en != none {
let part-en = text(
font: cover-title-latin, size: 18pt, fill: black,
)[#part-title-en]
let part-en-inner = {
set par(leading: 0.3em)
part-en
}
part-lines.push(box(
width: 10.5cm - unit - 0.0 * unit,
part-en-inner,
))
}
if part-lines.len() == 0 and part-info != none {
part-lines.push(text(
font: cover-title-cjk, size: 18pt, fill: black,
)[#part-info])
}
let part-value = stack(dir: ttb, spacing: 0.35 * unit, ..part-lines)
box(
width: 10.5cm - unit,
inset: (left: 0.1 * unit, right: 0.25 * unit),
align(left + top, part-value),
)
} else { none }
let part-y = title-end-y + 0.5 * unit
let part-height = if part-box != none { measure(part-box).height } else { 0pt }
if part-box != none {
place(top + left, dx: unit, dy: part-y, part-box)
}
let part-end-y = part-y + part-height
let metadata = ()
if number != none { metadata.push(("编号", number)) }
if author != none {
let author-text = if type(author) == array { author.join(" / ") } else { author }
metadata.push(("作者", author-text))
}
if date != none { metadata.push(("日期", date)) }
if classification != none { metadata.push(("密级", classification)) }
for (key, value) in info {
if value != none { metadata.push((str(key), value)) }
}
let marker-height = 2 * unit
let info-y = part-end-y + 0.45 * unit
let cells = ()
for (label, value) in metadata {
cells.push(text(
font: (cover-latin, cover-cjk), size: 12pt,
weight: "medium", fill: luma(48%),
)[#label])
cells.push(text(
font: (cover-latin, cover-cjk), size: 14pt,
fill: black,
)[#value])
}
let info-content = if metadata.len() > 0 {
box(
width: 21cm - 8.5 * unit,
inset: (left: 0.25 * unit, right: 0.25 * unit),
align(left + top, grid(
columns: (auto, 1fr),
column-gutter: 0.6em,
row-gutter: 0.35em,
align: (right + top, left + top),
..cells,
)))
} else { none }
let info-height = if info-content != none { measure(info-content).height } else { 0pt }
// 左侧笔画与右侧笔画一样保持固定高度,不再随标题行数拉伸。
// 标志在第七格独立收尾;下方长条依据文档信息的实际高度紧凑起笔。
tri(0, 6, "tr", sky)
let strip-y = part-end-y + 0.18 * unit
let strip-height = 29.7cm - strip-y
place(top + left, dy: strip-y, polygon(
fill: sky, stroke: none,
(unit, 0pt),
(unit, strip-height),
(0pt, strip-height),
(0pt, unit),
))
// 距页底约七个单位格设置元数据锚点。若标题很长,则自动避让标题尾部。
let meta-y = calc.max(29.7cm - 7 * unit, title-end-y + unit)
if metadata.len() > 0 {
let apex-y = marker-height / 2
// 不是带切角的梯形,而是一个完整的等腰直角三角形:
// 左边为 2 unit 的斜边,右侧尖点到上下端点的两条边等长且垂直。
place(top + left, dy: meta-y, polygon(
fill: accent, stroke: none,
(0pt, 0pt),
(unit, apex-y),
(0pt, marker-height),
))
let signature-line-length = 7 * unit
place(top + left, dx: unit, dy: meta-y + apex-y,
line(length: signature-line-length, stroke: (paint: accent, thickness: 0.8pt)))
let signed-label = text(
font: cover-latin, size: 10pt, weight: "bold", fill: accent,
)[signed]
let signed-size = measure(signed-label)
place(top + left,
dx: unit,
dy: meta-y + apex-y - signed-size.height - 0.04 * unit,
signed-label)
// 三角形保留在距页底七个单位的位置,作为独立的封面装饰。
// 文档信息不再与三角形连线,而是跟随主标题实际高度,排列在标题正下方。
place(
top + left,
dx: unit + signature-line-length,
dy: meta-y + apex-y + 0.3 * unit,
info-content,
)
}
tri(2, 6, "tl", sky)
}
}
pagebreak(weak: true)
}
// ---- 正文配置 ----
#let conf(
number-pages: true, // 是否显示页码
doc,
) = {
set page(
paper: "a4",
margin: (top: 3.5cm, bottom: 3.5cm, left: 2cm, right: 2cm),
numbering: if number-pages { "1" } else { none },
number-align: center,
header: none, // 正文页不使用装饰角标
)
set text(font: (body-latin, body-cjk), lang: "zh", size: 12pt)
set par(justify: true, leading: 1em, first-line-indent: (amount: 2em, all: true))
// 无序列表整体缩进一个正文字符;圆点与正文仍使用 Typst 默认间距。
set list(indent: 1em)
// 正文标题与正文使用同一套中西文字体。
show heading: set text(font: (body-latin, body-cjk))
// 代码块与行内代码:西文使用 IBM Plex Mono,中文回退到正文中文字体。
show raw: set text(font: (mono-font, body-cjk))
// 块级代码:细边框、浅色背景与自动行号;行内代码不受影响。
show raw.where(block: true): code => {
show raw.line: line => box(width: 100%, grid(
columns: (3em, 1fr),
align: (right + top, left + top),
box(
width: 100%,
inset: (left: 0.5em, right: 0.75em),
stroke: (right: 0.6pt + luma(82%)),
text(fill: luma(55%))[#line.number],
),
pad(left: 0.9em, right: 0.9em, line.body),
))
block(
width: 100%,
inset: (x: 0pt, y: 0.65em),
fill: luma(98%),
stroke: 0.6pt + luma(75%),
radius: 3pt,
breakable: true,
code,
)
}
doc
}
// ---- 模板结束 ----
// Template file ended.
/*
Legacy duplicated tail retained as an inert comment because this workspace's
Windows patch transport does not reliably truncate shortened files.
breakable: true,
code,
)
}
doc
}
// ---- 模板结束 ----
// Template file ended.
}
doc
}
// ---- 模板结束 ----
// Template file ended.
*/
// End of file.