Skip to main content

nagisa_render/
model.rs

1//! 文档模型(IR)—— 两个前端(标记解析 [`markup`](crate::parse_markup) / 构建器
2//! [`Doc`](crate::Doc))的共同产物,也是版式引擎的唯一输入。块级 + 行内 + 可叠加的文字样式,
3//! 全是普通数据,不含渲染状态。一般不直接构造,用构建器或标记文本得到。
4
5use std::path::PathBuf;
6
7/// 一份文档:从上到下排布的块序列。
8#[derive(Clone, Debug, Default)]
9pub struct Document {
10    /// 块序列。
11    pub blocks: Vec<Block>,
12}
13
14/// 块级元素。
15#[derive(Clone, Debug)]
16pub enum Block {
17    /// 标题(h1–h6)。
18    Heading {
19        /// 级别,取 1..=6。
20        level: u8,
21        /// 行内内容。
22        inlines: Vec<Inline>,
23        /// 水平对齐。
24        align: Align,
25    },
26    /// 段落。
27    Paragraph {
28        /// 行内内容。
29        inlines: Vec<Inline>,
30        /// 水平对齐。
31        align: Align,
32    },
33    /// 有序 / 无序列表(项内容是块序列,可嵌套、可多段)。
34    List(List),
35    /// 引用块(裹块,可嵌套)。
36    Quote(Vec<Block>),
37    /// 代码块(等宽、带底色;按语言标签做轻量语法上色,认不出整块默认色)。
38    Code {
39        /// 语言标签;渲染在代码盒题头栏右侧,可缺(缺则题头栏只留 `</>` 符)。
40        lang: Option<String>,
41        /// 代码原文(保留换行)。
42        text: String,
43    },
44    /// 分割线。
45    Divider,
46    /// 块级图片(可带宽度 / 对齐 / 图注)。
47    Image(BlockImage),
48    /// 多栏并排。
49    Columns(Columns),
50    /// 表格。
51    Table(Table),
52    /// 进度条。
53    Progress(Progress),
54    /// 面板(卡片容器:底色 / 边框 / 圆角 / 内边距 / 投影,内层是块容器)。
55    Panel(Panel),
56}
57
58/// 面板:带装饰的块容器。作并排栏某栏唯一块时,装饰盒拉齐到本行最高栏(卡片排整齐)。
59#[derive(Clone, Debug)]
60pub struct Panel {
61    /// 内容块。
62    pub blocks: Vec<Block>,
63    /// 装饰。
64    pub decor: PanelDecor,
65}
66
67/// 面板装饰。`bg` 与 `border` 都缺省时按主题给「浅底 + 细边」的默认卡片样。
68#[derive(Clone, Debug, Default)]
69pub struct PanelDecor {
70    /// 底色;`None` 且无边框时用主题代码底色。
71    pub bg: Option<Color>,
72    /// 边框;`None` 且无底色时用主题边框色细线。
73    pub border: Option<ImageBorder>,
74    /// 圆角半径(逻辑像素);`None` = 12。
75    pub radius: Option<f32>,
76    /// 内边距(逻辑像素);`None` = 0.6 倍基准字号。
77    pub pad: Option<f32>,
78    /// 投影。
79    pub shadow: Option<Shadow>,
80}
81
82/// 表格。`cols` 给各列对齐与可选限宽(短于列数时,缺的列按默认:左对齐 + 自适应)。
83#[derive(Clone, Debug)]
84pub struct Table {
85    /// 表头行;`None` = 无表头。
86    pub header: Option<Vec<Cell>>,
87    /// 数据行。
88    pub rows: Vec<Vec<Cell>>,
89    /// 各列规格(对齐 / 限宽)。
90    pub cols: Vec<ColSpec>,
91    /// 紧凑度与网格样式。
92    pub style: TableStyle,
93}
94
95/// 表格的紧凑度与网格样式。
96#[derive(Clone, Debug)]
97pub struct TableStyle {
98    /// 单元格左右内边距(逻辑像素);`None` = 默认。越小列越紧凑。
99    pub pad_x: Option<f32>,
100    /// 单元格上下内边距(逻辑像素);`None` = 默认。越小行越紧凑(行距越小)。
101    pub pad_y: Option<f32>,
102    /// 网格线开关。
103    pub grid: TableGrid,
104    /// 表头浅底,默认开。
105    pub header_fill: bool,
106    /// 拉伸铺满可用宽:列宽合计不足时把富余宽度按比例分给自适应列(全是固定列则整体
107    /// 等比放大)。默认关——窄表保持自然宽。
108    pub expand: bool,
109    /// 整表水平对齐(窄于内容宽时生效;`expand` 开了自然铺满,无所谓对齐)。
110    pub align: Align,
111}
112
113impl Default for TableStyle {
114    fn default() -> Self {
115        Self {
116            pad_x: None,
117            pad_y: None,
118            grid: TableGrid::default(),
119            header_fill: true,
120            expand: false,
121            align: Align::Left,
122        }
123    }
124}
125
126/// 网格线开关,默认全开。
127#[derive(Clone, Copy, Debug)]
128pub struct TableGrid {
129    /// 外框线。
130    pub outer: bool,
131    /// 列竖线。
132    pub vertical: bool,
133    /// 行横线。
134    pub horizontal: bool,
135}
136
137impl Default for TableGrid {
138    fn default() -> Self {
139        Self { outer: true, vertical: true, horizontal: true }
140    }
141}
142
143/// 列规格:对齐 + 可选限宽。
144#[derive(Clone, Debug)]
145pub struct ColSpec {
146    /// 该列对齐。
147    pub align: Align,
148    /// 限宽;`None` = 按内容自适应。
149    pub width: Option<Length>,
150}
151
152impl Default for ColSpec {
153    fn default() -> Self {
154        Self { align: Align::Left, width: None }
155    }
156}
157
158/// 单元格:行内内容(按列宽自动换行)+ 可选背景填色。
159#[derive(Clone, Debug)]
160pub struct Cell {
161    /// 单元格的行内内容。
162    pub inlines: Vec<Inline>,
163    /// 背景填色;`None` = 无(随表)。
164    pub bg: Option<Color>,
165}
166
167/// 多栏容器:各栏并排,行高取最高栏。
168#[derive(Clone, Debug)]
169pub struct Columns {
170    /// 各栏。
171    pub cols: Vec<Column>,
172    /// 栏间距(逻辑像素);`None` = 主题默认。
173    pub gap: Option<f32>,
174}
175
176/// 一栏:块内容 + 宽度权重(按权重瓜分可用宽,默认 1.0)。
177#[derive(Clone, Debug)]
178pub struct Column {
179    /// 栏内容。
180    pub blocks: Vec<Block>,
181    /// 宽度权重。
182    pub weight: f32,
183}
184
185/// 进度条:`value` 按比例填充,余下露出底槽。
186#[derive(Clone, Debug)]
187pub struct Progress {
188    /// 进度值(0–1;越界与非有限值渲染时夹取)。
189    pub value: f32,
190    /// 条高(逻辑像素)。
191    pub height: f32,
192    /// 填充色;`None` = 主题强调色。
193    pub fill: Option<Color>,
194    /// 底槽色;`None` = 主题边框色。
195    pub track: Option<Color>,
196    /// 圆角半径(逻辑像素);`None` = 半高(胶囊形)。渲染时夹到半高以内。
197    pub radius: Option<f32>,
198    /// 条宽;`None` = 铺满内容宽。
199    pub width: Option<Length>,
200    /// 水平对齐(窄于内容宽时生效)。
201    pub align: Align,
202}
203
204/// 列表。
205#[derive(Clone, Debug)]
206pub struct List {
207    /// 有序 / 无序。
208    pub kind: ListKind,
209    /// 有序列表的起始序号(无序忽略)。
210    pub start: u32,
211    /// 列表项。
212    pub items: Vec<ListItem>,
213}
214
215/// 列表种类。
216#[derive(Clone, Copy, Debug, PartialEq, Eq)]
217pub enum ListKind {
218    /// 无序(项目符号)。
219    Unordered,
220    /// 有序(序号)。
221    Ordered,
222}
223
224/// 列表项:内容是块序列,故支持多段与嵌套子列表。
225#[derive(Clone, Debug)]
226pub struct ListItem {
227    /// 项内容。
228    pub blocks: Vec<Block>,
229    /// 任务复选标记:`None` = 普通项;`Some(已完成)` = 渲染成复选标记(`□` / `✓`),
230    /// 对应标记文本的 `- [ ]` / `- [x]`。
231    pub check: Option<bool>,
232}
233
234/// 块级图片。
235#[derive(Clone, Debug)]
236pub struct BlockImage {
237    /// 图片来源。
238    pub src: ImageSource,
239    /// 显示宽度;`None` = 适配内容宽(不超出)。
240    pub width: Option<Length>,
241    /// 水平对齐。
242    pub align: Align,
243    /// 图注(排在图下方,居中小字);`None` = 无。
244    pub caption: Option<Vec<Inline>>,
245    /// 装饰层(角标/边框/水印/圆角/阴影);默认全无。
246    pub decor: ImageDecor,
247}
248
249/// 图片装饰层 —— 叠在图面上的附加呈现,**不改变布局尺寸**(阴影溢出照画)。
250#[derive(Clone, Debug, Default)]
251pub struct ImageDecor {
252    /// 角标:小标签贴在图的一角(如「动图」「GIF」)。
253    pub badge: Option<Badge>,
254    /// 边框:沿图片边缘描边(圆角时随圆角走)。
255    pub border: Option<ImageBorder>,
256    /// 水印:半透明文字叠在图面。
257    pub watermark: Option<Watermark>,
258    /// 圆角半径(逻辑像素,0 = 直角):裁切图面,边框/阴影随之。
259    pub radius: f32,
260    /// 投影;`None` = 无。
261    pub shadow: Option<Shadow>,
262}
263
264/// 图面上的锚点位置(角标 / 水印的停靠处)。
265#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
266pub enum Anchor {
267    /// 左上。
268    TopLeft,
269    /// 右上(角标默认)。
270    #[default]
271    TopRight,
272    /// 左下。
273    BottomLeft,
274    /// 右下(水印默认)。
275    BottomRight,
276    /// 正中。
277    Center,
278}
279
280/// 图片角标:圆角底板 + 短文字,贴在图的一角。
281#[derive(Clone, Debug)]
282pub struct Badge {
283    /// 标签文字(宜短,如「动图」)。
284    pub text: String,
285    /// 停靠角。
286    pub anchor: Anchor,
287    /// 底板色(默认黑 72%)。
288    pub bg: Color,
289    /// 文字色(默认白)。
290    pub fg: Color,
291    /// 相对基准字号的倍率(默认 0.75)。
292    pub size: f32,
293}
294
295impl Badge {
296    /// 默认形态的角标(右上角、黑底白字)。
297    pub fn new(text: impl Into<String>) -> Self {
298        Self {
299            text: text.into(),
300            anchor: Anchor::TopRight,
301            bg: Color::rgba(0, 0, 0, 184),
302            fg: Color::rgb(255, 255, 255),
303            size: 0.75,
304        }
305    }
306}
307
308/// 图片边框(沿图缘描边;有圆角时随圆角)。
309#[derive(Clone, Copy, Debug, PartialEq)]
310pub struct ImageBorder {
311    /// 线宽(逻辑像素)。
312    pub width: f32,
313    /// 颜色。
314    pub color: Color,
315}
316
317/// 图片水印:无底板的半透明文字。
318#[derive(Clone, Debug)]
319pub struct Watermark {
320    /// 水印文字。
321    pub text: String,
322    /// 停靠处。
323    pub anchor: Anchor,
324    /// 颜色(含 alpha;默认白 40%)。
325    pub color: Color,
326    /// 相对基准字号的倍率(默认 0.9)。
327    pub size: f32,
328}
329
330impl Watermark {
331    /// 默认形态的水印(右下角、白 40%)。
332    pub fn new(text: impl Into<String>) -> Self {
333        Self { text: text.into(), anchor: Anchor::BottomRight, color: Color::rgba(255, 255, 255, 102), size: 0.9 }
334    }
335}
336
337/// 行内元素。
338#[derive(Clone, Debug)]
339pub enum Inline {
340    /// 一段带样式的文字。
341    Text {
342        /// 文字。
343        text: String,
344        /// 样式。
345        style: TextStyle,
346    },
347    /// 行内代码(等宽 + 浅底)。
348    Code(String),
349    /// 硬换行。
350    LineBreak,
351}
352
353/// 可叠加的文字样式。span 嵌套时逐字段合并。
354#[derive(Clone, Debug, PartialEq)]
355pub struct TextStyle {
356    /// 字重(CSS 习惯值:细 300 / 常规 400 / 粗 700,内置字体 100–900 都有真实例)。
357    /// `None` = 跟随语境:正文常规,标题 / 表头加粗。
358    pub weight: Option<u16>,
359    /// 斜体。
360    pub italic: bool,
361    /// 下划线。
362    pub underline: bool,
363    /// 删除线。
364    pub strike: bool,
365    /// 文字色;`None` = 用主题文字色。
366    pub color: Option<Color>,
367    /// 高亮底色;`None` = 无高亮。
368    pub highlight: Option<Highlight>,
369    /// 相对基准字号的倍率(默认 1.0)。
370    pub size: f32,
371    /// 字族角色。
372    pub font: FontRole,
373    /// 链接文字(标记文本 `[文字](URL)` 的产物):无显式 `color` 时按主题强调色渲染。
374    /// 图片不可点,URL 本身不展示。
375    pub link: bool,
376    /// 文字阴影;`None` = 无。
377    pub shadow: Option<Shadow>,
378    /// 圈注:以这段文字为中心画一圈椭圆描边(醒目标注,如圈出日历上的某天;不参与
379    /// 布局尺寸,圈溢出到行距里)。
380    pub ring: Option<RingMark>,
381    /// 着重点:这段文字正下方一枚实心小点(中文「着重号」式标注;画进行距,不占高度)。
382    pub dot: Option<DotMark>,
383    /// 边注:这段文字挂到本行内容的外侧(左或右),**参与绘制、不参与布局**——行宽与
384    /// 居中 / 对齐都按其余内容算,边注不挤不偏(与圈注 / 着重点同一哲学)。适合
385    /// 「当前」「✓」这类行尾行首标记。多行段落里右边注跟末行、左边注跟首行;整段
386    /// 只有边注没有正文时按普通内容排(边注失去锚点)。
387    pub aside: Option<AsideSide>,
388}
389
390/// 边注的停靠侧。
391#[derive(Clone, Copy, Debug, PartialEq, Eq)]
392pub enum AsideSide {
393    /// 行首左外侧。
394    Left,
395    /// 行尾右外侧。
396    Right,
397}
398
399/// 圈注参数。半径给定后圈的大小与文字宽窄无关——日历里「1」和「10」能圈出同样大的圈。
400#[derive(Clone, Copy, Debug, Default, PartialEq)]
401pub struct RingMark {
402    /// 描边颜色;`None` = 跟随文字墨色。
403    pub color: Option<Color>,
404    /// 横向半径(逻辑像素);`None` = 按文字宽自适应。
405    pub rx: Option<f32>,
406    /// 纵向半径(逻辑像素);`None` 且 `rx` 有值 = 取 `rx`(正圆),都缺 = 按字高自适应。
407    pub ry: Option<f32>,
408    /// 线宽(逻辑像素);`None` = 0.07 倍字号。
409    pub width: Option<f32>,
410    /// 逐字圈:整段一字一圈(空白跳过),全自适应时缺省**正圆**;`false` = 整段一个圈
411    /// (范围圈,自适应为扁椭圆)。
412    pub each: bool,
413}
414
415/// 着重点参数。
416#[derive(Clone, Copy, Debug, Default, PartialEq)]
417pub struct DotMark {
418    /// 点色;`None` = 跟随文字墨色。
419    pub color: Option<Color>,
420    /// 点半径(逻辑像素);`None` = 0.09 倍字号。
421    pub radius: Option<f32>,
422    /// 逐字点:一字一点(中文着重号的正字法;空白跳过);`false` = 整段中线下一点。
423    pub each: bool,
424}
425
426impl Default for TextStyle {
427    fn default() -> Self {
428        Self {
429            weight: None,
430            italic: false,
431            underline: false,
432            strike: false,
433            color: None,
434            highlight: None,
435            size: 1.0,
436            font: FontRole::Sans,
437            link: false,
438            shadow: None,
439            ring: None,
440            dot: None,
441            aside: None,
442        }
443    }
444}
445
446/// 阴影(文字与图片共用):偏移 + 软化半径 + 颜色,尺寸皆**逻辑像素**。
447/// 不参与布局(不撑大占位),溢出块界照画。
448#[derive(Clone, Copy, Debug, PartialEq)]
449pub struct Shadow {
450    /// 水平偏移(右正)。
451    pub dx: f32,
452    /// 垂直偏移(下正)。
453    pub dy: f32,
454    /// 软化半径(0 = 实边)。
455    pub blur: f32,
456    /// 颜色(含 alpha,通常用半透明)。
457    pub color: Color,
458}
459
460impl Default for Shadow {
461    fn default() -> Self {
462        // 默认一枚朴素下坠软影。
463        Self { dx: 0.0, dy: 2.0, blur: 6.0, color: Color::rgba(0, 0, 0, 64) }
464    }
465}
466
467/// 高亮底色来源。
468#[derive(Clone, Copy, Debug, PartialEq, Eq)]
469pub enum Highlight {
470    /// 跟随主题默认高亮色(随亮 / 暗变)。
471    Theme,
472    /// 指定具体色。
473    Custom(Color),
474}
475
476/// 字族角色。`Named` 直接按字族名匹配,匹配不到回退 Sans。
477#[derive(Clone, Debug, PartialEq)]
478pub enum FontRole {
479    /// 无衬线(默认正文)。
480    Sans,
481    /// 衬线。
482    Serif,
483    /// 等宽。
484    Mono,
485    /// 楷体。
486    Kai,
487    /// 指定字族名。
488    Named(String),
489}
490
491/// 水平对齐。
492#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
493pub enum Align {
494    /// 左对齐(默认)。
495    #[default]
496    Left,
497    /// 居中。
498    Center,
499    /// 右对齐。
500    Right,
501    /// 两端对齐。
502    Justify,
503}
504
505/// RGBA 颜色(每通道 8 位,非预乘)。
506#[derive(Clone, Copy, Debug, PartialEq, Eq)]
507pub struct Color {
508    /// 红。
509    pub r: u8,
510    /// 绿。
511    pub g: u8,
512    /// 蓝。
513    pub b: u8,
514    /// 不透明度(255 = 不透明)。
515    pub a: u8,
516}
517
518impl Color {
519    /// 不透明色。
520    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
521        Self { r, g, b, a: 255 }
522    }
523
524    /// 带 alpha 的色。
525    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
526        Self { r, g, b, a }
527    }
528
529    /// 解析十六进制色:`#rgb` / `#rrggbb` / `#rrggbbaa`(井号可省,大小写不限)。
530    /// 非法返回 `None`。`#rgb` 每位扩成两位(`f` → `ff`)。
531    pub fn hex(s: &str) -> Option<Self> {
532        let h = s.strip_prefix('#').unwrap_or(s);
533        if !h.bytes().all(|b| b.is_ascii_hexdigit()) {
534            return None;
535        }
536        let n = |slice: &str| u8::from_str_radix(slice, 16).ok();
537        match h.len() {
538            3 => {
539                let b = h.as_bytes();
540                let dup = |c: u8| {
541                    let d = (c as char).to_digit(16)? as u8;
542                    Some(d << 4 | d)
543                };
544                Some(Self::rgb(dup(b[0])?, dup(b[1])?, dup(b[2])?))
545            }
546            6 => Some(Self::rgb(n(&h[0..2])?, n(&h[2..4])?, n(&h[4..6])?)),
547            8 => Some(Self::rgba(n(&h[0..2])?, n(&h[2..4])?, n(&h[4..6])?, n(&h[6..8])?)),
548            _ => None,
549        }
550    }
551}
552
553/// 长度:绝对像素,或相对内容宽的百分比。
554#[derive(Clone, Copy, Debug, PartialEq)]
555pub enum Length {
556    /// 绝对逻辑像素。
557    Px(f32),
558    /// 内容宽的百分比(0–100)。
559    Percent(f32),
560}
561
562/// 图片来源。引擎不联网:URL 由调用方下好,以 `Bytes` 传入。
563#[derive(Clone, Debug)]
564pub enum ImageSource {
565    /// 已加载的图片字节。
566    Bytes(Vec<u8>),
567    /// 磁盘路径,渲染时读取。
568    Path(PathBuf),
569    /// 具名引用(标记文本里的 `@名字`),渲染时从 [`RenderOptions::images`](crate::RenderOptions::images) 取字节。
570    Named(String),
571}