liepress 0.1.0-beta.1

A Markdown to PDF/SVG/PNG converter with CSS styling support
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
# LiePress 设计文档

## 项目概述

LiePress 是一个 Rust 实现的 Markdown 到多格式文档生成器,支持将 Markdown 转换为 PDF、SVG 和 PNG 格式。

## 核心设计原则

1. **三层 AST 架构**:清晰的转换管道,每层负责单一职责
2. **纯数据描述**:视觉元素(VisualElement)与渲染后端解耦
3. **统一渲染接口**:PageRenderer trait 支持多种输出格式
4. **流式布局引擎**:支持分页、断行等复杂排版需求

## 系统架构

```
┌─────────────────────────────────────────────────────────────────┐
│                          输入层                                  │
│                    Markdown 文本 / 文件                          │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                      Layer 1: MDAST                             │
│              markdown crate 解析的原始 AST                       │
│         (mdast::Node - 符合 CommonMark + GFM 规范)              │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                      Layer 2: Styled AST                        │
│              带样式的抽象语法树 (ast 模块)                        │
│              Node + Style = 内容与样式分离                        │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                      Layer 3: Layout AST                        │
│              布局后的文档结构 (generator 模块)                    │
│              Document → Page → VisualElement                     │
└─────────────────────────────────────────────────────────────────┘
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│   PDF 渲染器     │ │   SVG 渲染器     │ │   PNG 渲染器     │
│  (krilla)       │ │  (字符串拼接)     │ │  (vello_cpu)    │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```

## 模块详解

### 1. AST 模块 (`src/ast/`)

负责 Markdown 到带样式 AST 的转换。使用 `markdown` crate 以 GFM (GitHub Flavored Markdown) 模式解析。

#### 1.1 核心类型

- **`Node`** ([src/ast/node.rs]../src/ast/node.rs): 带样式的 AST 节点
  - 包含 `NodeKind`(内容类型)和 `Style`(样式信息)
  - 支持嵌套结构(如列表项包含段落)
  - 包含 `splittable` 标志,控制是否允许跨页分割

- **`Style`** ([src/ast/style.rs]../src/ast/style.rs): 样式定义
  - 字体家族(支持多字体回退列表)
  - 字体大小、粗细、样式
  - 颜色、行高、边距、对齐方式
  - 表格相关样式(边框、填充、表头/交替行背景)
  - 链接 URL

#### 1.2 节点类型 (NodeKind)

| 类型 | 说明 | 支持状态 |
|------|------|---------|
| `Document` | 文档根节点 ||
| `Paragraph` | 段落 ||
| `Heading { level }` | 标题 H1-H6 ||
| `List { ordered, start }` | 列表(有序/无序) ||
| `ListItem` | 列表项 ||
| `CodeBlock { lang, code }` | 代码块 ||
| `Blockquote` | 引用块 ||
| `ThematicBreak` | 分隔线 ||
| `Image { src, alt }` | 图片 ||
| `Table { align }` | 表格 ||
| `TableRow` | 表格行 ||
| `Text` | 纯文本(叶节点) ||
| `Strong` | 粗体 ||
| `Emphasis` | 斜体 ||
| `InlineCode` | 行内代码 ||
| `Link { url }` | 超链接 ||
| `Delete` | 删除线 | ⚠️ 已定义,未实现样式渲染 |

#### 1.3 样式属性 ([src/ast/style.rs]../src/ast/style.rs)

| 属性 | 类型 | 说明 | 支持状态 |
|------|------|------|---------|
| `font_family` | `Vec<String>` | 字体家族列表(优先级从高到低) ||
| `font_size_pt` | `f32` | 字号(pt) ||
| `font_weight` | `FontWeight` | 字重(Normal/Bold) ||
| `font_style` | `FontStyle` | 字体样式(Normal/Italic) ||
| `color` | `Color` | 文本颜色(RGBA) ||
| `line_height_pt` | `f32` | 行高(pt) ||
| `letter_spacing` | `f32` | 字间距(pt) ||
| `text_align` | `TextAlign` | 对齐方式(Left/Center/Right/Justify) | ✅(Justify 回退为 Left) |
| `display` | `Display` | 显示类型(Block/Inline/InlineBlock/None) ||
| `margin_top_pt` | `f32` | 上边距(pt) ||
| `margin_bottom_pt` | `f32` | 下边距(pt) ||
| `margin_left_pt` | `f32` | 左边距(pt) ||
| `margin_right_pt` | `f32` | 右边距(pt) ||
| `padding_top_pt` | `f32` | 上内边距(pt) ||
| `padding_bottom_pt` | `f32` | 下内边距(pt) ||
| `padding_left_pt` | `f32` | 左内边距(pt) ||
| `padding_right_pt` | `f32` | 右内边距(pt) ||
| `width` | `Option<f32>` | 显式宽度(pt) ||
| `height` | `Option<f32>` | 显式高度(pt) ||
| `object_fit` | `ObjectFit` | 图片适应方式(Contain/Cover/Fill/None) ||
| `background_color` | `Option<Color>` | 背景色 ||
| `page_break_before` | `PageBreak` | 分页前控制(Auto/Always/Avoid/Left/Right) ||
| `page_break_after` | `PageBreak` | 分页后控制(Auto/Always/Avoid/Left/Right) ||
| `table_border_color` | `Color` | 表格边框颜色 ||
| `table_border_width_pt` | `f32` | 表格边框宽度(pt) ||
| `table_cell_padding_h_pt` | `f32` | 单元格水平内边距(pt) ||
| `table_cell_padding_v_pt` | `f32` | 单元格垂直内边距(pt) ||
| `table_header_bg` | `Option<Color>` | 表头背景色 ||
| `table_alt_row_bg` | `Option<Color>` | 交替行背景色 ||
| `link_url` | `Option<String>` | 链接 URL ||
| `list_indent_pt` | `Option<f32>` | 列表缩进(pt,None 表示使用 2em 默认值) ||

#### 1.4 内置样式表 ([src/ast/presets.rs]../src/ast/presets.rs)

内置默认 CSS(`DEFAULT_CSS` 常量),使用标准 CSS 语法定义。用户可通过 `<style>` 标签或 `ConvertOptions` 覆盖。

| 选择器 | 属性 | 说明 |
|--------|------|------|
| `body` | `font-family: serif; font-size: 10.5pt; line-height: 1.5; color: #000` | 根容器,通过 CSS 继承影响所有元素 |
| `h1`~`h6` | `font-family: serif; font-weight: bold;` `font-size: 24pt~10.5pt` | 逐级递减,粗体 |
| `p` | `margin-top: 0; margin-bottom: 12pt` | 段落 |
| `ul`, `ol` | `margin-top: 6pt; margin-bottom: 6pt` | 列表容器 |
| `li` | `margin-top: 0; margin-bottom: 4pt` | 列表项 |
| `pre` | `font-family: monospace; font-size: 9pt; background-color: #F5F5F5` | 代码块,灰色背景 |
| `blockquote` | `margin-top: 12pt; margin-bottom: 12pt` | 引用块 |
| `hr` | `margin-top: 18pt; margin-bottom: 18pt` | 分隔线 |
| `table` | `border-collapse: collapse; font-size: 10pt` | 表格 |
| `th` | `font-weight: bold; background-color: #F0F0F0` | 表头 |
| `tr:nth-child(even)` | `background-color: #F8F8F8` | 交替行 |
| `code` | `font-family: monospace; color: #333` | 行内代码 |
| `a` | `color: #0000FF; font-style: italic` | 超链接(蓝色斜体) |
| `strong` | `font-weight: bold` | 粗体 |
| `em` | `font-style: italic` | 斜体 |
| `span` | `display: inline` | 普通文本 |

`list_marker_style()` 函数保留为硬编码(标记是自动生成的布局元素,不由 CSS 系统控制)。

#### 1.5 CSS 样式解析 ([src/ast/css.rs]../src/ast/css.rs)

实现轻量级 CSS 解析器,支持 CSS 核心子集:

**选择器支持**:
| 类型 | 示例 | 特异性 | 说明 |
|------|------|--------|------|
| 通用选择器 | `*` | 0 | 匹配所有元素 |
| 标签选择器 | `h1`, `p`, `table` | 1 | 按标签名匹配 |
| 类选择器 | `.title` | 10 | 按类名匹配 |
| 后代组合器 | `blockquote p` | 总和 | 空格分隔的祖先链匹配 |

**属性支持**:`font-family`、`font-size`、`font-weight`、`font-style`、`color`、`line-height`、`text-align`、`display`、`margin-*`、`padding-*`、`width`、`height`、`background-color`、`object-fit`、`page-break-before/after`、`letter-spacing`、表格属性、`list-indent`

**长度单位**:`pt`、`px`、`em`(相对父元素字号)、`%`(相对父元素字号)

**值解析**:
- 颜色:`#RGB``#RRGGBB``rgb(r,g,b)`、命名颜色(20+ CSS 标准色名)
- `auto`:移除显式宽/高,回退到自动计算
- 无单位数字解析为 `pt`

**StyleResolver**([css.rs](../src/ast/css.rs#L870-L935)):
- 合并内置 CSS + 用户 CSS + `<style>` 内联 CSS
- CSS 特异性排序:低优先级先应用,高优先级覆盖
- 严格模式(`strict: true`):CSS 解析失败时返回 `CssParseError`
- 非严格模式(默认):静默忽略错误,沿用已有有效样式

**样式解析流程**:
```
build_node(node, resolver, ancestor_tags, parent_style)
Style::inherit_from(parent_style) → 拷贝可继承属性
收集所有匹配选择器的规则 + 计算特异性
按特异性升序排序 → 依次 apply_declaration 覆盖
返回最终的 Style
```

#### 1.6 Markdown `<style>` 标签提取 ([src/ast/mod.rs]../src/ast/mod.rs)

自动从 Markdown 的 `<style>` HTML 块中提取 CSS 内容:

- 递归搜索 MDAST 树中的 `Node::Html` 节点
- 提取 `<style>...</style>` 之间的文本
- 支持 HTML 属性(如 `<style>`, `<style type="text/css">`- 提取的 CSS 优先级最高(用户 CSS > `<style>` CSS > 内置 CSS)
-`parse_markdown_with_css` 函数中自动完成合并

**示例**:
```markdown
<style>
body { font-family: "Noto Sans SC", serif; }
h1 { color: #c00; }
</style>

# 标题会继承 body 的字体
段落也会使用 Noto Sans SC 字体。
```

**继承模型**:
- 可继承属性(CSS 标准):`font-family``font-size``font-weight``font-style``color``line-height``letter-spacing``text-align``page-break-*``table-*`
- 不可继承属性:`margin-*``padding-*``display``width``height``background-color``object-fit`
- `body` 上设置的 `font-family` 会自动传递到所有未显式设置字体的元素(`code`/`pre` 等设置了 `monospace` 的除外)

#### 1.7 页面设置 (@page) 与页眉页脚

通过 `@page` at-rule 可配置页面尺寸、页边距以及页眉页脚。

**尺寸与边距属性**:
- `margin-top`, `margin-bottom`, `margin-left`, `margin-right`:页边距(pt/px/in/cm 等绝对单位)
- `margin`:简写,支持 1~4 个值(同 CSS 标准)
- `size`:页面尺寸,支持命名值(`A4`, `Letter`)或两个长度值(宽 高)

**页眉页脚属性**:
- `header`:页眉文本,支持 `{page}`(当前页码)和 `{total}`(总页数)模板变量
- `footer`:页脚文本,模板变量同上。默认值 `- {page} -`
- `header-font-size`:页眉字号(pt),默认 9pt
- `footer-font-size`:页脚字号(pt),默认 9pt

示例:
```css
@page {
    margin: 36pt 54pt;
    size: A4;
    header: "项目文档";
    footer: "第 {page} 页 / 共 {total} 页";
    header-font-size: 10pt;
}
```

**页眉渲染**:
- 文本居中对齐,灰色(`#646464`),9pt
- 页眉文字下方有一条 0.5pt 浅灰(`#C8C8C8`)分隔线
- 绘制在顶部边距区域,内容区上方 4pt

**页脚渲染**:
- 文本居中对齐,灰色(`#646464`),9pt
- 无分隔线
- 绘制在底部边距区域,内容区下方 4pt

**优先级规则**:
1. `markdown_to_document_with_settings()` 传入的 `PageSettings` 覆盖一切
2. `ConvertOptions.page_config` 中的页眉页脚字段
3. CSS 中的 `@page` 规则(来自用户 CSS 或 Markdown `<style>`4. 内置默认(页脚显示 `- {page} -`,无页眉)

#### 1.8 自动字体检测(Auto Font)([src/lib.rs]../src/lib.rs)

内置启发式语言检测引擎,根据 Markdown 文档内容自动选择匹配的字体家族。

**ScriptRange 检测**:定义四个有效脚本范围:

| 脚本 | Unicode 范围 | 示例字符 |
|------|-------------|---------|
| Han(中文) | CJK 统一表意文字 (4E00-9FFF)、扩展 A-F (3400-2EBE0)、兼容表意文字 (F900-FAFF, 2F800-2FA1F) | 中文汉字 |
| Japanese(日文) | 平假名 (3040-309F)、片假名 (30A0-30FF, 31F0-31FF) | あア |
| Korean(韩文) | 韩文音节 (AC00-D7AF) | 한글 |
| Latin(拉丁文) | 基本拉丁 (0000-00FF)、通用标点 (2000-206F)、其他字母字符 | abc |

检测时会跳过代码块(``` 包围)、链接内容 `[text](url)` 和行内代码 `` ` ``,避免误判。

**流程**:
1. 遍历 Markdown 每行内容,按字符统计各脚本的出现次数
2. 找出出现次数最多的脚本作为主要语言
3. 根据主要语言返回推荐字体列表

**字体映射**:

| 主要语言 | 推荐字体列表 |
|----------|-------------|
| Han(中文) | `Noto Serif SC`, `Source Han Serif SC`, `SimSun`, `SimSun-ExtB`, `serif` |
| Japanese(日文) | `Noto Serif CJK JP`, `Noto Serif JP`, `serif` |
| Korean(韩文) | `Noto Serif CJK KR`, `Noto Serif KR`, `serif` |
| Latin(拉丁文) | `Noto Serif`, `Georgia`, `Times New Roman`, `serif` |

**集成方式**:

自动字体检测通过 `resolve_user_css()` 函数集成到 CSS 解析管道中。当 `auto_font = true` 且用户未显式设置 `font_family` 或 CSS 中不包含 `font-family` 时,生成的 `body { font-family: ... }` 规则会覆盖内置 DEFAULT_CSS 的 `font-family: serif`。

**启用方式**:
- **默认启用**(自 v0.1.0 起):`ConvertOptions::default()``auto_font = true`
- **CLI 关闭**`liepress --no-auto-font -i input.md -o output.pdf`
- **编程关闭**`ConvertOptions::new().with_auto_font(false)`

### 2. Generator 模块 (`src/generator/`)

将 Styled AST 转换为布局后的 VisualElement。这是布局引擎的核心。

#### 2.1 核心组件 ([src/generator/types.rs]../src/generator/types.rs)

- **`DocumentGenerator`**: 文档生成器主入口
- **`PageContext`**: 页面上下文,管理当前页状态
  - 剩余高度跟踪(`remaining_height()`  - 元素收集(`add_element()`  - 分页控制(`start_new_page()`- **`PageSettings`**: 页面尺寸和边距配置(默认 A4)
- **`Document`**: 最终文档结构,包含 `Vec<Page>` 和页面尺寸
- **`Page`**: 单页结构,包含 `Vec<VisualElement>`

#### 2.2 布局流程

```
parse_markdown()
build_ast() ──► Node (Styled AST)
markdown_to_document()
    ├──► layout_node() ──► 根据 NodeKind 分发
    │       │
    │       ├──► layout_paragraph_with_indent()
    │       ├──► layout_heading()
    │       ├──► layout_list()
    │       │       └──► layout_list_with_indent()
    │       │               └──► layout_list_item_first_child()
    │       ├──► layout_code()
    │       ├──► layout_image()
    │       ├──► layout_blockquote()
    │       ├──► layout_thematic_break()
    │       └──► layout_table()
Document { pages: Vec<Page> }
```

#### 2.3 关键布局算法

**段落布局** ([src/generator/mod.rs](../src/generator/mod.rs)):
1. 收集所有内联子节点(Text/Strong/Emphasis/InlineCode/Link)的文本段和样式
2. 调用 `layout_text_with_contexts()` 生成 `TextLayout`(包含已断行的 `TextLine` 集合)
3. 使用 `annotate_runs_with_urls` 将超链接 URL 回填到对应的 run 中
4. `place_text_lines` 负责将行放置到页面上,处理自动分页和缩进

**列表布局**:
- 无序列表: 固定标记区域 (10pt)
- 有序列表: 根据最大编号动态计算标记区域宽度(最小12pt,最大30pt)
- 缩进: 基于字体大小的 2 个空格宽度(`font_size * 1.2`,范围10~20pt)
- 标记右对齐,内容左对齐

**图片布局** ([src/generator/mod.rs](../src/generator/mod.rs)):
1. 尝试原始大小放入当前页
2. 如放不下,按宽度缩放后尝试
3. 仍放不下则换页
4. 新页面按页面尺寸比例缩放
5. 支持图片标题(使用 alt 文本,9pt 灰色居中)

**代码块分页** ([src/generator/mod.rs](../src/generator/mod.rs)):
- 浅灰背景 (#F5F5F5)
- 背景框跟随分页,每页单独绘制背景
- 左偏移 8pt,使用内容区全宽

**标题布局**:
- 不可跨页分割(splittable = false)
- 高度超过页面内容区时 panic 报错

**引用块布局**:
- 左侧背景色块标记(使用 Rect 元素)
- 内容在色块右侧显示

**分隔线布局**:
- 居中水平线,宽度为内容区 60%

#### 2.4 表格布局引擎 ([src/generator/table.rs]../src/generator/table.rs)

两阶段设计:
1. **`compute_layout_info()`**: 纯计算,返回列宽、行高等布局数据
   - 测量每个单元格的理想宽度和最小宽度
   - 启发式列宽分配
   - 基于换行的行高计算
2. **`generate_rows()`**: 按行区间生成视觉元素,支持跨页分割
   - 表头背景填充
   - 交替行背景填充
   - 单元格文本渲染
   - 表格边框绘制

#### 2.5 内联文本处理 ([src/generator/text.rs]../src/generator/text.rs)

core 函数:

- **`collect_inline_segments()`**: 递归收集内联子节点(Strong、Emphasis、Link、InlineCode、Text)的文本段和样式
- **`annotate_runs_with_urls()`**: 通过 run 的 `text_range` 匹配对应的 segment,将超链接 URL 回填到 TextRun 的 `url` 字段

这两个函数是 generator 层面的辅助工具,不涉及 parley 布局细节。
Text 引擎的布局细节全部封装在 [`src/text.rs`](../src/text.rs) 中。

### 3. Visual 模块 ([src/visual.rs]../src/visual.rs)

纯数据描述的视觉元素,与渲染后端解耦。

#### 3.1 元素类型

```rust
enum VisualElement {
    // 基础图形
    Rect { rect, style },
    Circle { center, radius, style },
    Line { start, end, style },
    Polyline { points, style },
    Path { path, style },

    // 渐变
    GradientPath { path, gradient, stroke },

    // 文本
    TextLine { runs, bounds, line_height },

    // 图片
    Image { position, size, pixel_size, data, format, alt },

    // 组合
    Group { children, transform },
    ZGroup { z_index, children },
}
```

#### 3.2 样式类型

- **`FillStrokeStyle`**: 填充 + 描边样式
- **`StrokeStyle`**: 纯描边样式
- **`GradientDef`**: 渐变定义(线性/径向)
- **`Transform`**: 2D 变换(平移、旋转、缩放)
- **`Color`**: RGBA 颜色

### 4. Text 模块 ([src/text.rs]../src/text.rs)

文本排版核心,基于 parley 库,不与 AST 或 generator 耦合。
职责:接收文本 + 样式,返回已断行的 `TextLayout`。分页、缩进等由 generator 负责。

#### 4.1 核心类型

- **`TextLayout`**: 排版结果,包含 `lines: Vec<TextLine>``width: f64``height: f64`
  - 所有坐标相对 layout 原点(段落左上角)
- **`TextLine`**: 一行文本
  - `runs: Vec<TextRun>` — 该行的所有 Run
  - `bounds: Rect` — 相对 layout 原点的边界框
  - `line_height: f32` — 行高
- **`TextRun`**: 具有相同样式的文本片段
  - `text`: 文本内容
  - `text_range`: 在段落中的字节范围
  - `font_data`: parley FontData(用于渲染)
  - `font_size`: 字号
  - `color`: 颜色
  - `glyphs: Vec<Glyph>` — 字形列表(坐标相对 `TextLine.bounds.origin`  - `baseline_x/y`: 基线位置
  - `url`: 超链接 URL(可选)
- **`Glyph`**: 单个字形信息(id, x, y, advance)
- **`TextStyle`**: 样式输入(color, font_family, font_size, font_weight, font_style, align, url)

#### 4.2 排版流程

```
文本内容 + TextStyle
parley RangedBuilder (多段样式拼接)
Layout.break_all_lines(max_width)
Layout.align(Alignment)
extract_lines_from_parley() 提取行和字形
返回 TextLayout { lines: Vec<TextLine>, width, height }
[generator 层] annotate_runs_with_urls() 回填 URL
[generator 层] place_text_lines() 分页 + 绝对定位
渲染时遍历 TextLine → TextRun → Glyph,逐个绘制
```

Text 引擎负责前三步(排版 → 断行 → 提取为自有结构),
generator 负责后三步(URL 回填 → 分页定位 → 渲染)。

#### 4.3 多段样式布局

`layout_text_with_contexts()` 支持将多段不同样式的文本合并为一个 Layout:
1. 拼接所有文本段
2. 以第一段样式为默认样式
3. 后续各段通过 `RangedBuilder.push()` 覆盖特定范围的样式属性
4. 支持字体家族、字号、颜色、字重、字体样式

#### 4.4 样式映射

| TextStyle 字段 | Parley StyleProperty | 说明 |
|---------------|---------------------|------|
| `font_family` | `FontFamily(List)` | 自动识别 CSS 通用家族关键字 |
| `font_size` | `FontSize` | pt 值 |
| `color` | `Brush` | RGBA |
| `font_weight` | `FontWeight` | 支持 full range (100-900) |
| `font_style` | `FontStyle` | Normal/Italic/Oblique |

#### 4.5 线程安全

使用 thread_local 存储字体上下文:

```rust
thread_local! {
    static FONT_CONTEXT: RefCell<FontContext>;
    static LAYOUT_CONTEXT: RefCell<LayoutContext<Color>>;
    static FONT_BYTES: RefCell<HashMap<String, Arc<Vec<u8>>>>;
}
```

提供便捷函数:`with_font_context()`, `with_layout_context()`, `with_text_contexts()`

#### 4.6 字体注册

- `register_font()`: 支持从文件路径或内存数据加载字体
- 自动缓存字体字节供 PDF 渲染器使用

### 5. 渲染模块 (`src/render/`)

多后端渲染实现。

#### 5.1 PageRenderer Trait

```rust
trait PageRenderer {
    fn draw_rect(&mut self, rect: Rect, style: &FillStrokeStyle);
    fn draw_circle(&mut self, center: Point, radius: f64, style: &FillStrokeStyle);
    fn draw_line(&mut self, start: Point, end: Point, style: &StrokeStyle);
    fn draw_polyline(&mut self, points: &[Point], style: &StrokeStyle);
    fn draw_path(&mut self, path: &BezPath, style: &FillStrokeStyle);
    fn draw_gradient_path(&mut self, path: &BezPath, gradient: &GradientDef, stroke: Option<&Stroke>);
    fn begin_group(&mut self, transform: Option<&Transform>);
    fn end_group(&mut self);
    fn draw_text_run(&mut self, run: &TextRun, position: Point);
    fn draw_image(&mut self, data: &[u8], format: &str, position: Point, size: (f64, f64));
}
```

#### 5.2 渲染后端

| 后端 | 实现 | 用途 | 状态 |
|-----|------|------|------|
| PDF | krilla | 高质量文档输出 ||
| SVG | 字符串拼接 | 矢量图形、Web 展示 ||
| PNG | vello_cpu | 位图渲染、预览 ||

#### 5.3 PDF 渲染器 ([src/render/pdf.rs]../src/render/pdf.rs)

- 使用 krilla 库生成 PDF
- **字体缓存**: 通过 `FontCacheKey`(data_ptr + data_len + index)去重字体
- **超链接注释**: 在绘制文本 run 时,收集有 URL 的 run 的位置和尺寸,渲染完成后统一添加 `LinkAnnotation`
- 坐标系: PDF 左下角原点 (y 向上),布局系统使用左上角原点,渲染时翻转 y 坐标
- 支持图片格式: PNG、JPEG、GIF、WebP

#### 5.4 坐标系处理

- **布局引擎**: 左上角原点 (y 向下)
- **SVG**: 左上角原点 (y 向下)
- **PDF**: 左下角原点 (y 向上)
- 转换: `y_pdf = page_height - y_svg`

### 6. 超链接处理

#### 6.1 数据流

```
Markdown: [text](url)
collect_inline_segments() → ("text", TextStyle { url: Some(url), ... })
layout_text_with_contexts() → TextLayout { lines: Vec<TextLine> }
    │                        extract_lines_from_parley() 内部自动完成
annotate_runs_with_urls() → 匹配 text_range → 回填 run.url
draw_text_run() → 收集有 url 的 run 的矩形区域
render_page() → 为每个区域创建 LinkAnnotation + Action(URI)
```

#### 6.2 断行超链接

当超链接文本被断行时,Parley 会为每行中属于该链接的部分创建独立的 GlyphRun,每个 run 保留正确的 `text_range`。`annotate_runs_with_urls` 通过 `text_range` 中点匹配,为每个断行 run 正确关联 URL。渲染时每行中的链接片段会生成独立的 LinkAnnotation,确保断行文本的每个部分都可点击。

### 7. CLI 模块 ([src/bin/liepress.rs]../src/bin/liepress.rs)

命令行接口,基于 clap。

```bash
# 基本用法(自动字体检测默认开启)
liepress -i input.md -o output.pdf
liepress -i input.md -o output.svg
liepress -i input.md -o output.png

# 使用自定义 CSS 覆盖样式
liepress -i input.md -o output.pdf -s my-style.css

# 严格模式(CSS 解析错误时直接报错)
liepress -i input.md -o output.pdf -S

# 关闭自动字体检测(使用内置 serif 默认字体)
liepress -i input.md -o output.pdf --no-auto-font

# 指定输出格式
liepress -i input.md -o output.png -f png

# 页眉页脚控制
liepress -i input.md -o output.pdf --header "项目报告"
liepress -i input.md -o output.pdf --footer "第 {page} 页 / 共 {total} 页"
liepress -i input.md -o output.pdf --no-page-number

# 同时控制页眉和页脚
liepress -i input.md -o output.pdf --header "机密" --footer "- {page} -"
```

**参数说明**:

| 参数 | 缩写 | 说明 |
|------|------|------|
| `--input <FILE>` | `-i` | 输入 Markdown 文件路径 |
| `--output <FILE>` | `-o` | 输出文件路径 |
| `--format <FORMAT>` | `-f` | 输出格式:`pdf`(默认)、`svg``png` |
| `--style <CSS_FILE>` | `-s` | 可选 CSS 样式表文件,覆盖默认样式 |
| `--strict` | `-S` | 严格模式:CSS 解析失败时直接报错(默认关闭) |
| `--no-auto-font` | | 关闭自动字体检测(默认开启) |
| `--header <TEXT>` | | 页眉文本,支持 `{page}``{total}` 模板变量 |
| `--footer <TEXT>` | | 页脚文本,默认显示 `- {page} -`,空字符串清除页脚 |
| `--no-page-number` | | 移除默认页码,等价于 `--footer ""` |

### 8. 常量定义 ([src/generator/constants.rs]../src/generator/constants.rs)

- 页面尺寸: A4 (595.276 × 841.890 pt)
- 默认边距: 上下 36pt (0.5"),左右 54pt (0.75")
- 默认 DPI: 72

### 9. 库 API ([src/lib.rs]../src/lib.rs)

#### 9.1 公共函数

| 函数 | 说明 |
|------|------|
| `markdown_to_pdf(markdown)` | Markdown 字符串 → PDF 字节 |
| `markdown_to_svg(markdown)` | Markdown 字符串 → SVG 字符串列表 |
| `markdown_to_png(markdown)` | Markdown 字符串 → PNG 字节列表 |
| `markdown_file_to_pdf(path)` | Markdown 文件 → PDF 字节 |
| `markdown_file_to_svg(path)` | Markdown 文件 → SVG 字符串列表 |
| `markdown_file_to_png(path)` | Markdown 文件 → PNG 字节列表 |
| `markdown_to_pdf_with_options(md, opts)` | 带配置的 PDF 转换 |
| `markdown_to_svg_with_options(md, opts)` | 带配置的 SVG 转换 |
| `markdown_to_png_with_options(md, opts)` | 带配置的 PNG 转换 |
| `markdown_file_to_pdf_with_options(path, opts)` | 文件 + 配置 PDF 转换 |
| `markdown_file_to_svg_with_options(path, opts)` | 文件 + 配置 SVG 转换 |
| `markdown_file_to_png_with_options(path, opts)` | 文件 + 配置 PNG 转换 |

#### 9.2 ConvertOptions ([src/lib.rs]../src/lib.rs)

配置结构体,支持 builder 风格链式调用:

```rust
use liepress::ConvertOptions;
use std::path::PathBuf;

let opts = ConvertOptions::new()
    .with_font_family(&["Noto Sans SC", "sans-serif"])
    .with_css("h1 { color: red; }")
    .with_css_file(PathBuf::from("style.css"))
    .with_strict(true)
    .with_auto_font(false);
```

**字段说明**:

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `font_family` | `Vec<String>` | `[]` | 全局默认字体家族列表,自动生成 `body { font-family: ... }` 通过继承应用到所有元素 |
| `user_css` | `String` | `""` | 用户 CSS 样式字符串,叠加在默认样式之上 |
| `css_file` | `Option<PathBuf>` | `None` | 用户 CSS 样式文件路径,与 `user_css` 合并 |
| `strict` | `bool` | `false` | 严格模式:CSS 解析失败时返回错误 |
| `auto_font` | `bool` | `true` | 自动字体检测:根据文档内容选择匹配字体(中文→Noto Serif SC/宋体,日文→Noto Serif CJK JP) |
| `page_config` | `Option<PageConfig>` | `None` | 页面配置(尺寸、边距、页眉页脚),优先级高于 CSS `@page` |

**Builder 方法**:

| 方法 | 参数 | 说明 |
|------|------|------|
| `with_font_family(&[&str])` | 字体家族列表 | 设置全局默认字体家族 |
| `with_css(&str)` | CSS 字符串 | 设置用户 CSS 样式字符串 |
| `with_css_file(PathBuf)` | CSS 文件路径 | 设置用户 CSS 样式文件路径 |
| `with_strict(bool)` | 是否严格 | 设置严格模式 |
| `with_auto_font(bool)` | 是否自动 | 设置自动字体检测 |
| `with_page_config(PageConfig)` | 页面配置 | 设置页面尺寸、边距等 |
| `with_header(&str)` | 页眉文本 | 设置页眉,支持 `{page}` / `{total}` 模板 |
| `with_footer(&str)` | 页脚文本 | 设置页脚,支持 `{page}` / `{total}` 模板 |
| `with_header_font_size(f32)` | 字号(pt) | 设置页眉字体大小,默认 9pt |
| `with_footer_font_size(f32)` | 字号(pt) | 设置页脚字体大小,默认 9pt |

**优先级(从低到高)**:
1. 内置默认 CSS(`DEFAULT_CSS`2. 自动字体检测 `auto_font`(生成 `body { font-family }`,仅在未显式设置字体时生效)
3. `ConvertOptions.font_family`(自动生成 `body { font-family }`4. `ConvertOptions.user_css`
5. `ConvertOptions.css_file`
6. Markdown 内联 `<style>` 标签

> **注意**`auto_font``font_family` 互斥。当 `font_family` 非空或任意用户 CSS 中包含 `font-family` 时,`auto_font` 自动失效,以用户指定为准。

#### 9.3 错误处理 ([src/error.rs]../src/error.rs)

```rust
pub type Result<T> = std::result::Result<T, Error>;

pub enum Error {
    VisualElementError(String),
    FontLoadError(String),
    LayoutError(String),
    CssParseError(String),
    IoError(std::io::Error),  // 自动转换
}
```

使用 `thiserror` 派生宏。`IoError` 通过 `#[from]` 支持 `?` 自动转换。

## 特性支持状态

### Markdown 元素

| 特性 | 状态 | 说明 |
|------|------|------|
| 标题 (H1-H6) || 逐级递减字号,粗体 |
| 段落 || 自动换行、分页 |
| 粗体 || 字体加粗 |
| 斜体 || 字体倾斜 |
| 行内代码 || 等宽字体 |
| 超链接 || 蓝色斜体,PDF 可点击注释 |
| 图片 || 自适应缩放,支持标题 |
| 无序列表 || 支持嵌套 |
| 有序列表 || 自动编号,支持嵌套 |
| 代码块 || 灰色背景,支持分页 |
| 引用块 || 左侧色块标记 |
| 分隔线 || 居中水平线 |
| 表格 || 列宽自适应,跨页分割 |
| CSS 自定义样式 || 内置样式表 + 用户 CSS 覆盖 + `<style>` 标签 |
| 字体配置 || 自动字体检测(auto_font)、`body { font-family }` CSS 继承、`ConvertOptions.font_family` |
| 自动字体检测 || 基于文档内容的启发式语言检测(中文/日文/韩文/拉丁文),自动选择匹配字体 |
| 严格模式 || CLI `--strict` 参数或 `ConvertOptions.strict` |
| 删除线 | ⚠️ | NodeKind 已定义,样式未渲染 |
| 任务列表 || 未实现 |
| 脚注 || 未实现 |
| 定义列表 || 未实现 |
| 数学公式 || 未实现 |
| Emoji || 依赖系统字体支持 |

### 样式属性

| 属性 | 状态 | 说明 |
|------|------|------|
| 字体家族 || 支持 fallback 列表,CSS 通用家族关键字,引号支持 |
| 字号 || pt 单位,支持 em/% 相对值 |
| 字重 || Normal/Bold + CSS 数值 (100-900) |
| 字体样式 || Normal/Italic/Oblique |
| 颜色 || #RGB、#RRGGBB、rgb()、命名颜色 |
| 行高 || pt/em/无单位倍数 |
| 文本对齐 || 左/中/右(两端对齐回退为左对齐) |
| 显示类型 || Block/Inline/InlineBlock/None |
| 上边距 || pt/px/em/% |
| 下边距 || pt/px/em/% |
| 左边距 || pt/px/em/% |
| 右边距 || pt/px/em/% |
| 上内边距 || pt/px/em/% |
| 下内边距 || pt/px/em/% |
| 左内边距 || pt/px/em/% |
| 右内边距 || pt/px/em/% |
| 宽/高 || pt/px/em/%, auto |
| 背景色 || 所有颜色格式均支持 |
| 字间距 || pt/px/em/% |
| 分页前控制 || auto/always/avoid/left/right |
| 分页后控制 || auto/always/avoid/left/right |
| 表格边框 || 颜色、宽度 |
| 单元格填充 || 水平/垂直分别可配置 |
| 表头背景 || 可配置颜色 |
| 交替行背景 || 可配置颜色 |
| 图片适应 || Contain/Cover/Fill/None |
| 列表缩进 || 支持 CSS `list-indent` 属性自定义,默认 2em |
| 删除线样式 || 未实现 |

### 布局功能

| 功能 | 状态 | 说明 |
|------|------|------|
| A4 页面 || 默认 |
| 自定义页面尺寸 || 通过 PageSettings |
| 自定义边距 || 通过 PageSettings |
| 页眉 || 支持 `{page}` / `{total}` 模板变量,带分隔线 |
| 页脚/页码 || 默认显示 `- {page} -`,支持模板变量 |
| 自动分页 || 行级分页,确保行完整 |
| 列表缩进 || 基于字体大小 2em 动态计算,支持 CSS `list-indent` 自定义 |
| 有序列表起始编号 || 支持 start 属性 |
| 图片缩放 || 多策略:原始/适配宽度/适配页面 |
| 图片标题 || 使用 alt 文本,灰色小字 |
| 代码块背景 || 浅灰色,跟随分页 |
| 表格跨页 || 按行区间分割 |
| 表头样式 || 背景色 |
| 交替行样式 || 背景色 |
| 表格对齐 || 列级左/中/右对齐 |
| 超链接注释 || PDF 可点击 |
| 断行超链接 || 多段注释,各段独立可点击 |
| 字体回退 || CSS 风格字体列表 |
| Unicode 支持 || 中文、日文、韩文、特殊符号 |
| 分页符控制 | ⚠️ | 仅标题不可分割,无显式分页符 |

## 关键设计决策

### 1. 三层 AST 架构

**决策**: 将转换过程分为 MDAST → Styled AST → Layout AST 三层。

**理由**:
- 每层职责单一,易于维护
- 样式与结构分离,支持主题定制
- 布局与渲染分离,支持多后端

### 2. VisualElement 纯数据设计

**决策**: VisualElement 不包含任何渲染逻辑,仅描述"画什么"。

**理由**:
- 渲染后端可独立演进
- 易于测试(可序列化比较)
- 支持新后端时无需修改核心逻辑

### 3. 分页策略

**决策**: 以 TextLine 为单位分页,确保行不被截断。

**理由**:
- 避免文字被截断的可读性问题
- 代码块背景需要跨页连续绘制
- 图片作为整体处理

### 4. 字体回退机制

**决策**: 支持 CSS 风格的字体家族列表(如 `["SimSun", "serif"]`)。

**理由**:
- 跨平台兼容性
- 中文字体优先,西文字体回退
- 与 Web 字体机制一致

### 5. 超链接 URL 回填

**决策**: 文本布局完成后,通过 `text_range` 中点匹配方式将 URL 回填到 TextRun。

**理由**:
- parley 的 Run 的 `text_range` 在布局后保持正确
- 中点匹配可靠处理断行场景
- 避免依赖易变的颜色匹配

### 6. 行级相对坐标系

**决策**: TextRun 中的字形坐标使用相对行左上角的偏移量,行的绝对位置由 TextLine.bounds 决定。

**理由**:
- 渲染时只需计算一次行位置
- 字形坐标在线性变换后无需调整
- 分页时只需更新 bounds

### 7. CSS 继承机制

**决策**: 使用 `body` 作为根元素,通过 `Style::inherit_from()` 实现 CSS 标准继承。

**理由**:
- 用户只需在 `<style>body { font-family: ... }</style>` 中配置一次,所有元素自动继承
- 与 Web CSS 行为一致,降低学习成本
- 显式设置(如 `pre { font-family: monospace }`)自然覆盖继承值
- 支持 `ConvertOptions.font_family` 作为编程式快捷方式,无需手写 CSS
- 支持 `auto_font` 自动检测,无需任何配置即可获得合理字体

## 扩展指南

### 添加新的 NodeKind

1. [ast/node.rs]../src/ast/node.rs 添加新的 NodeKind 变体
2.[ast/presets.rs]../src/ast/presets.rs 添加默认样式(如需要)
3.[generator/mod.rs]../src/generator/mod.rs`layout_node()` 添加对应的布局分支
4. 添加测试
5. 更新此文檔

### 添加新的渲染后端

1. 实现 `PageRenderer` trait
2. 处理所有 VisualElement 类型
3. 注意坐标系转换
4. 添加测试

### 自定义样式

通过 CSS 覆盖机制提供四种方式:

1. **自动字体检测**(默认启用):自动根据文档语言选择合适的字体家族,无需任何配置
2. **Markdown 中 `<style>` 标签**:最便捷,直接在文档头部写 CSS 规则
   ```markdown
   <style>
   body { font-family: "Noto Sans SC", serif; }
   h1 { color: #c00; border-bottom: 1px solid #ccc; }
   </style>
   ```

3. **外部 CSS 文件**:通过 CLI `-s` 参数或 `ConvertOptions.css_file` 指定
   ```bash
   liepress -i input.md -o output.pdf -s my-theme.css
   ```

4. **`ConvertOptions.font_family`**:编程式快捷设置全局字体,自动注入 CSS

**优先级**(从低到高):
1. 内置 CSS(`DEFAULT_CSS`2. 自动字体检测 `auto_font`(仅在未显式设置字体时生效)
3. `ConvertOptions.font_family`
4. `ConvertOptions.user_css`
5. `ConvertOptions.css_file`
6. Markdown 内联 `<style>` 标签

**继承**:在 `body` 上设置的字体、颜色等可继承属性会自动应用到所有未显式覆盖的元素。

## 测试策略

### 测试分层

| 层级 | 位置 | 说明 |
|------|------|------|
| 单元测试 | src/ 中各模块 | 测试内部函数 |
| 集成测试 | tests/integration/ | 端到端管道测试 PDF/SVG/PNG |
| PDF 验证测试 | tests/integration/pdf_validation.rs | 使用 lopdf 解析和验证 PDF |
| 渲染测试 | tests/render/pdf.rs | 验证 PDF 基本结构 |

### PDF 验证工具 ([tests/common/mod.rs]../tests/common/mod.rs)

- `load_pdf()`: 加载 PDF 数据
- `assert_valid_pdf()`: 验证 header + 至少一页
- `extract_links()`: 提取所有链接注释
- `assert_has_link()`: 验证指定 URL 存在
- `assert_link_count()`: 验证链接数量
- `validate_pdf_structure()`: 完整结构验证(页数、每页链接)
- `group_links_by_url()`: 按 URL 分组链接矩形(用于断行验证)

## 性能考虑

1. **字体缓存**: 使用 thread_local 缓存 FontContext 和 LayoutContext
2. **字体字节缓存**: 缓存已注册字体的原始字节,供 PDF 渲染器使用
3. **流式处理**: 支持大文档的分页处理
4. **图片优化**: 根据显示尺寸选择合适的 DPI (150 DPI)

## 依赖说明

| 依赖 | 用途 | 版本 |
|-----|------|------|
| markdown | MDAST 解析 (GFM) | 最新 |
| parley | 文本布局引擎 | 0.9 |
| krilla | PDF 生成 | 0.7 |
| vello_cpu | 2D 图形渲染 | 最新 |
| image | 图片解码 | 最新 |
| clap | CLI 参数解析 | 最新 |
| thiserror | 错误类型派生 | 最新 |
| lopdf | 测试中 PDF 解析和验证 | 最新 |

## 未来方向

### 短期

1. **删除线渲染**: 实现 Delete NodeKind 的样式渲染(text-decoration: line-through)
2. **两端对齐**: 实现 TextAlign::Justify 的完整支持
3. **行内代码高亮**: 为 InlineCode 添加背景色和边框
4. **代码语法高亮**: 基于代码块 lang 实现语法着色
5. **显式分页符**: 支持 `\pagebreak``---` 分页控制

### 中期

6. **主题系统**: 外部主题文件(YAML/TOML)支持
7. **目录生成**: 自动生成 Table of Contents
8. **图片对齐**: 支持 float、居中、环绕等布局
9. **自定义字体注册**: 改进字体注册 API,支持按文档配置字体文件

### 长期

11. **数学公式**: 支持 LaTeX 公式渲染(KaTeX/MathJax)
12. **Web 服务**: HTTP API 接口
13. **增量渲染**: 大文档的流式输出
14. **任务列表**: 支持 GFM 任务列表(checkbox)
15. **脚注**: 支持脚注引用和渲染