ooxml-core 0.1.0

OOXML 公共基座:OPC 打包层 + DrawingML 公共类型系统,服务于 pptx-rs / xlsx-rs / docx-rs
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
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
//! 文本体(`<p:txBody>`):段落、Run、字体属性等。
//!
//! 对应 python-pptx 中 `text.py`/`txbody.py`/`font.py` 三层对象的合并。
//!
//! # 元素结构(OOXML 规范)
//!
//! ```text
//! <p:txBody>
//!   <a:bodyPr .../>            文本框属性
//!   <a:p>                       段落
//!     <a:pPr .../>              段落属性
//!     <a:r>                     Run(可多个)
//!       <a:rPr .../>            Run 属性
//!       <a:t>文本</a:t>
//!     </a:r>
//!     <a:endParaRPr .../>       段落末尾默认属性
//!   </a:p>
//! </p:txBody>
//! ```
//!
//! # 与 python-pptx 的对应
//!
//! - `pptx.text.text._Paragraph` ←→ [`Paragraph`];
//! - `pptx.text.text._Run` ←→ [`Run`];
//! - `pptx.text.text._ParagraphFormat` ←→ [`ParagraphProperties`];
//! - `pptx.text.text._Font` ←→ [`RunProperties`];
//! - `pptx.text.textframe.TextFrame` ←→ [`TextBody`]。
//!
//! # 序列化约束
//!
//! - Run 属性若全部为默认(无 size/bold/color/...)则**不**写出 `<a:rPr/>`;
//! - 段落若整段为空也必须输出(PowerPoint 期望至少一个 `<a:p>`);
//! - `<a:t>` 内的文本必须用 [`super::writer::XmlWriter::text`] 自动转义。

use crate::oxml::color::Color;
use crate::oxml::simpletypes::{
    Alignment, MsoAnchor, MsoAutoSize, TabAlignment, TextWrapping, Underline,
};
use crate::units::{Emu, Pt, RGBColor};

/// 制表位(`<a:tab pos="..." algn="..."/>`)。
///
/// 对标 python-pptx `_TabStop` 对象。
/// 一个制表位由位置(EMU)和对齐类型(左/居中/右/小数点)决定。
#[derive(Copy, Clone, Debug, Default)]
pub struct TabStop {
    /// 制表位位置(EMU)。
    pub pos: Emu,
    /// 对齐类型。
    pub alignment: TabAlignment,
}

/// 段落水平缩进 / 悬挂缩进(EMU)。
#[derive(Copy, Clone, Debug, Default)]
pub struct Indent {
    /// 左边缩进(EMU)。
    pub left: Option<Emu>,
    /// 右边缩进(EMU)。
    pub right: Option<Emu>,
    /// 首行缩进(EMU)。
    pub first_line: Option<Emu>,
    /// 自定义悬挂量(in 100ths of a line)。
    pub hanging: Option<i32>,
}

/// 项目符号样式(`<a:buChar>` / `<a:buAutoNum>` / `<a:buNone>` 等)。
///
/// 对应 python-pptx 中 `_ParagraphFormat.bullet` 的详细控制。
#[derive(Clone, Debug, Default)]
pub enum BulletStyle {
    /// 无项目符号(`<a:buNone/>`)。
    #[default]
    None,
    /// 自定义字符项目符号(`<a:buChar char="..."/>`)。
    Char {
        /// 项目符号字符(如 `"•"` / `"▪"` / `"→"`)。
        char: String,
    },
    /// 自动编号(`<a:buAutoNum type="..." startAt="..."/>`)。
    AutoNum {
        /// 编号类型(如 `"arabicPeriod"` / `"alphaLcParenR"` / `"romanLcParenBoth"`)。
        auto_num_type: String,
        /// 起始编号(可选)。
        start_at: Option<u32>,
    },
}

/// 段落属性 `<a:pPr>`。
#[derive(Clone, Debug, Default)]
pub struct ParagraphProperties {
    /// 水平对齐方式(`<a:pPr algn="...">`)。`None` 表示不写出 algn 属性。
    pub alignment: Option<Alignment>,
    /// 缩进(左/右/首行/悬挂)。
    pub indent: Indent,
    /// 行距(固定值,百分之一磅)。与 `line_spacing_pct` 互斥。`None` 表示不写出。
    pub line_spacing: Option<i32>,
    /// 行距(百分比,1000 = 100%)。与 `line_spacing` 互斥。`None` 表示不写出。
    pub line_spacing_pct: Option<i32>,
    /// 段前间距(EMU)。`None` 表示不写出。
    pub space_before: Option<Emu>,
    /// 段后间距(EMU)。`None` 表示不写出。
    pub space_after: Option<Emu>,
    /// 是否项目符号列表(兼容旧字段,建议使用 `bullet_style`)。
    pub bullet: bool,
    /// 项目符号详细样式(`<a:buChar>` / `<a:buAutoNum>` / `<a:buNone>`)。
    pub bullet_style: Option<BulletStyle>,
    /// 段落级别(0-8)。
    pub level: u8,
    /// 默认 Run 属性(缺省时应用于段落末尾)。
    pub default_run_properties: Option<RunProperties>,
    /// 制表位列表(`<a:tabLst><a:tab .../></a:tabLst>`)。
    ///
    /// 对标 python-pptx `_ParagraphFormat.tab_stops`。
    /// 空列表表示不写出 `<a:tabLst>`。
    pub tab_stops: Vec<TabStop>,
}

impl ParagraphProperties {
    /// 写 XML。
    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
        if self.alignment.is_none()
            && self.indent.left.is_none()
            && self.indent.right.is_none()
            && self.indent.first_line.is_none()
            && self.indent.hanging.is_none()
            && self.line_spacing.is_none()
            && self.line_spacing_pct.is_none()
            && self.space_before.is_none()
            && self.space_after.is_none()
            && !self.bullet
            && self.bullet_style.is_none()
            && self.level == 0
            && self.default_run_properties.is_none()
            && self.tab_stops.is_empty()
        {
            return;
        }
        let mut attrs: Vec<(&str, &str)> = Vec::new();
        if let Some(a) = self.alignment {
            attrs.push(("algn", a.as_str()));
        }
        // 元组赋值:先取出所有要用的字符串到块外,扩展生命周期
        let lvl_s = if self.level != 0 {
            Some(self.level.to_string())
        } else {
            None
        };
        if let Some(s) = &lvl_s {
            attrs.push(("lvl", s));
        }
        w.open_with("a:pPr", &attrs);
        // indent
        if self.indent.left.is_some()
            || self.indent.right.is_some()
            || self.indent.first_line.is_some()
            || self.indent.hanging.is_some()
        {
            // 把所有字符串先取到块外
            let l_s = self.indent.left.map(|v| v.value().to_string());
            let r_s = self.indent.right.map(|v| v.value().to_string());
            let first_s = self.indent.first_line.map(|v| v.value().to_string());
            let hang_s = self.indent.hanging.map(|v| v.to_string());
            let mut iattrs: Vec<(&str, &str)> = Vec::new();
            if let Some(s) = &l_s {
                iattrs.push(("l", s));
            }
            if let Some(s) = &r_s {
                iattrs.push(("r", s));
            }
            if let Some(s) = &first_s {
                iattrs.push(("firstLine", s));
            }
            if let Some(s) = &hang_s {
                iattrs.push(("hanging", s));
            }
            w.empty_with("a:indent", &iattrs);
        }
        if self.line_spacing.is_some() || self.line_spacing_pct.is_some() {
            // 注意:OOXML 中 <a:lnSpc> 内只能含一个子元素(<a:spcPct> 或 <a:spcPts>),
            // 因此当同时设置 pct 和 pts 时必须**分别**输出两个 <a:lnSpc> 块,
            // 而不能把多个子元素塞到同一个 <a:lnSpc> 里。
            if let Some(p) = self.line_spacing_pct {
                let s = p.to_string();
                w.open("a:lnSpc");
                w.empty_with("a:spcPct", &[("val", &s)]);
                w.close("a:lnSpc");
            }
            if let Some(sp) = self.line_spacing {
                let s = sp.to_string();
                w.open("a:lnSpc");
                w.empty_with("a:spcPts", &[("val", &s)]);
                w.close("a:lnSpc");
            }
        }
        if self.space_before.is_some() || self.space_after.is_some() {
            // 把所有字符串先取到块外
            let sb_s = self.space_before.map(|v| v.value().to_string());
            let sa_s = self.space_after.map(|v| v.value().to_string());
            let mut sattrs: Vec<(&str, &str)> = Vec::new();
            if let Some(s) = &sb_s {
                sattrs.push(("spcBef", s));
            }
            if let Some(s) = &sa_s {
                sattrs.push(("spcAft", s));
            }
            w.empty_with("a:spcBef", &sattrs);
        }
        // 项目符号样式(OOXML 顺序:buNone/buChar/buAutoNum 在 defRPr 之前)
        if let Some(bs) = &self.bullet_style {
            match bs {
                BulletStyle::None => {
                    w.empty("a:buNone");
                }
                BulletStyle::Char { char } => {
                    w.empty_with("a:buChar", &[("char", char.as_str())]);
                }
                BulletStyle::AutoNum {
                    auto_num_type,
                    start_at,
                } => {
                    if let Some(sa) = start_at {
                        let sa_s = sa.to_string();
                        w.empty_with(
                            "a:buAutoNum",
                            &[("type", auto_num_type.as_str()), ("startAt", sa_s.as_str())],
                        );
                    } else {
                        w.empty_with("a:buAutoNum", &[("type", auto_num_type.as_str())]);
                    }
                }
            }
        }
        // 制表位列表(OOXML 顺序:tabLst 在 bullet 之后、defRPr 之前)
        if !self.tab_stops.is_empty() {
            w.open("a:tabLst");
            for tab in &self.tab_stops {
                let pos_s = tab.pos.value().to_string();
                w.empty_with(
                    "a:tab",
                    &[("pos", pos_s.as_str()), ("algn", tab.alignment.as_str())],
                );
            }
            w.close("a:tabLst");
        }
        if let Some(rpr) = &self.default_run_properties {
            rpr.write_xml(w, "a:defRPr");
        }
        w.close("a:pPr");
    }
}

/// 超链接(`<a:hlinkClick>` / `<a:hlinkHover>`)。
///
/// 对应 python-pptx `Hyperlink` 对象。通过 `r:id` 引用 `rels` 中的目标 URL。
#[derive(Clone, Debug, Default)]
pub struct Hyperlink {
    /// 关系 ID(`r:id`,指向 `.rels` 中的 target URL)。
    pub rid: Option<String>,
    /// 鼠标悬停提示(`tooltip` 属性)。
    pub tooltip: Option<String>,
    /// 动作类型(`action` 属性,如 `"ppaction://hlinksldjump"` 跳转幻灯片)。
    pub action: Option<String>,
    /// 是否无效的超链接(仅写出 `<a:hlinkClick/>` 无属性,用于继承)。
    pub invalid: bool,
}

impl Hyperlink {
    /// 创建一个指向 URL 的超链接。
    pub fn new(rid: impl Into<String>) -> Self {
        Self {
            rid: Some(rid.into()),
            ..Default::default()
        }
    }

    /// 创建一个跳转幻灯片的动作超链接。
    pub fn new_slide_jump() -> Self {
        Self {
            action: Some("ppaction://hlinksldjump".to_string()),
            ..Default::default()
        }
    }
}

/// Run 属性 `<a:rPr>`。
#[derive(Clone, Debug, Default)]
pub struct RunProperties {
    /// 字号(Pt)。`None` 表示走主题继承。
    pub size: Option<Pt>,
    /// 是否加粗。
    pub bold: bool,
    /// 是否斜体。
    pub italic: bool,
    /// 下划线样式。`None` 表示不下划线。
    pub underline: Option<Underline>,
    /// 单删除线。
    pub strike: bool,
    /// 双删除线。
    pub strike_dbl: bool,
    /// 文本颜色。`Color::None` 表示走主题继承。
    pub color: Color,
    /// 高亮背景色。`None` 表示不高亮。
    pub highlight: Option<Color>,
    /// 字体名(拉丁)。
    pub latin_font: Option<String>,
    /// 东亚字体。
    pub eastasia_font: Option<String>,
    /// 复杂脚本字体。
    pub cs_font: Option<String>,
    /// baseline 偏移(百分比,正=上标,负=下标)。
    pub baseline: Option<i32>,
    /// 字距(百分之一磅)。
    pub kerning: Option<i32>,
    /// 字符间距。
    pub spc: Option<i32>,
    /// 大写(small caps / all caps)。
    pub caps: Caps,
    /// `lang`:英文/中文/...语言。
    pub lang: Option<String>,
    /// 透明度(0-100000,0=不透明,100000=完全透明)。
    ///
    /// 对标 DrawingML `<a:solidFill><a:srgbClr val="..."><a:alpha val="30000"/></a:srgbClr></a:solidFill>`。
    /// 常用值:`30_000` = 30% 不透明(70% 透明),用于水印等场景。
    pub alpha: Option<i32>,
    /// 点击超链接(`<a:hlinkClick>`)。
    pub hlink_click: Option<Hyperlink>,
    /// 悬停超链接(`<a:hlinkHover>`)。
    pub hlink_hover: Option<Hyperlink>,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
pub enum Caps {
    #[default]
    None,
    Small,
    All,
}

impl Caps {
    pub fn as_str(self) -> &'static str {
        match self {
            Caps::None => "none",
            Caps::Small => "small",
            Caps::All => "all",
        }
    }
}

impl RunProperties {
    /// 写 XML。
    pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
        // 提前取出所有要序列化的字符串,扩展到函数末尾
        let sz_s = self
            .size
            .map(|sz| ((sz.value() * 100.0) as i32).to_string());
        let baseline_s = self.baseline.map(|v| v.to_string());
        let kerning_s = self.kerning.map(|v| v.to_string());
        let spc_s = self.spc.map(|v| v.to_string());

        let mut attrs: Vec<(&str, &str)> = Vec::new();
        if let Some(s) = &sz_s {
            attrs.push(("sz", s));
        }
        if self.bold {
            attrs.push(("b", "1"));
        }
        if self.italic {
            attrs.push(("i", "1"));
        }
        if let Some(u) = self.underline {
            attrs.push(("u", u.as_str()));
        }
        if self.strike {
            attrs.push(("strike", "sngStrike"));
        }
        if self.strike_dbl {
            attrs.push(("strike", "dblStrike"));
        }
        if self.caps != Caps::None {
            attrs.push(("cap", self.caps.as_str()));
        }
        if let Some(s) = &baseline_s {
            attrs.push(("baseline", s));
        }
        if let Some(s) = &kerning_s {
            attrs.push(("kern", s));
        }
        if let Some(s) = &spc_s {
            attrs.push(("spc", s));
        }
        if let Some(l) = &self.lang {
            attrs.push(("lang", l));
        }
        // 严格 OOXML 顺序: <a:ln> <a:noFill>/<a:solidFill> ... <a:highlight> <a:latin> ...
        w.open_with(tag, &attrs);
        if !matches!(self.color, Color::None) {
            self.color.write_solid_fill_with_alpha(w, self.alpha);
        } else if self.alpha.is_some() {
            // 有 alpha 但无颜色——写一个空 solidFill + alpha(罕见但合法)
            w.open("a:solidFill");
            w.empty_with("a:srgbClr", &[("val", "000000")]);
            if let Some(a) = self.alpha {
                w.empty_with("a:alpha", &[("val", a.to_string().as_str())]);
            }
            w.close("a:srgbClr");
            w.close("a:solidFill");
        }
        if let Some(h) = &self.highlight {
            h.write_solid_fill(w);
        }
        if let Some(latin) = &self.latin_font {
            w.empty_with("a:latin", &[("typeface", latin)]);
        }
        if let Some(ea) = &self.eastasia_font {
            w.empty_with("a:ea", &[("typeface", ea)]);
        }
        if let Some(cs) = &self.cs_font {
            w.empty_with("a:cs", &[("typeface", cs)]);
        }
        // 超链接(OOXML 顺序:hlinkClick → hlinkHover,在字体之后)
        if let Some(h) = &self.hlink_click {
            let mut hattrs: Vec<(&str, &str)> = Vec::new();
            if let Some(rid) = &h.rid {
                hattrs.push(("r:id", rid.as_str()));
            }
            if let Some(tip) = &h.tooltip {
                hattrs.push(("tooltip", tip.as_str()));
            }
            if let Some(act) = &h.action {
                hattrs.push(("action", act.as_str()));
            }
            if hattrs.is_empty() {
                w.empty("a:hlinkClick");
            } else {
                w.empty_with("a:hlinkClick", &hattrs);
            }
        }
        if let Some(h) = &self.hlink_hover {
            let mut hattrs: Vec<(&str, &str)> = Vec::new();
            if let Some(rid) = &h.rid {
                hattrs.push(("r:id", rid.as_str()));
            }
            if let Some(tip) = &h.tooltip {
                hattrs.push(("tooltip", tip.as_str()));
            }
            if hattrs.is_empty() {
                w.empty("a:hlinkHover");
            } else {
                w.empty_with("a:hlinkHover", &hattrs);
            }
        }
        w.close(tag);
    }

    /// 从一段 XML 属性中解析(用于读取时)。`src` 来自父级解析器,限于 rPr 属性。
    #[doc(hidden)]
    pub fn from_attrs_unused(_attrs: &super::parser::AttrMap) -> Self {
        // 暂留接口;读路径走 [`crate::oxml::parse_sld::parse_run_properties`]
        RunProperties::default()
    }

    // --------------------- 高阶便捷 API ---------------------

    /// 克隆出 `RunProperties`(含同名字段)。
    pub fn copy(&self) -> Self {
        self.clone()
    }
}

/// 一个 Run:`<a:r>` + 文本。
#[derive(Clone, Debug, Default)]
pub struct Run {
    /// 文本内容。若以 `"\n"` 开头(且不包含其他非空白字符)则被识别为换行 Run。
    pub text: String,
    /// Run 属性。
    pub properties: RunProperties,
}

impl Run {
    /// 构造一个普通 Run。
    pub fn new(text: impl Into<String>) -> Self {
        Run {
            text: text.into(),
            properties: RunProperties::default(),
        }
    }
    /// 构造一个**换行** Run(等价于"软回车",OOXML 表达为 `<a:br/>`)。
    pub fn line_break() -> Self {
        Run {
            text: String::from("\n"),
            properties: RunProperties::default(),
        }
    }

    /// 写 XML。
    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
        // 软换行 Run:写出 `<a:br/>`,**不**套 `<a:t>`
        if self.text == "\n" {
            if !is_default_rpr(&self.properties) {
                // 即使有属性也要写出,但 OOXML 规范 `<a:br/>` 在 rPr 内——这里给最简版本
                w.empty("a:br");
            } else {
                w.empty("a:br");
            }
            return;
        }
        w.open("a:r");
        if !is_default_rpr(&self.properties) {
            self.properties.write_xml(w, "a:rPr");
        }
        w.open("a:t");
        w.text(&self.text);
        w.close("a:t");
        w.close("a:r");
    }

    // --------------------- 高阶 API(对齐 python-pptx _Run) ---------------------

    /// 取 Run 文本。
    pub fn text(&self) -> &str {
        &self.text
    }
    /// 设置 Run 文本(**不**支持 `"\n"`,换行请用 [`Run::line_break`])。
    pub fn set_text(&mut self, t: impl Into<String>) {
        self.text = t.into();
    }

    /// Run 字体大小(`Pt`)。
    pub fn size(&self) -> Option<Pt> {
        self.properties.size
    }
    /// 设置 Run 字体大小。
    pub fn set_size(&mut self, v: Pt) {
        self.properties.size = Some(v);
    }

    /// 是否加粗。
    pub fn bold(&self) -> bool {
        self.properties.bold
    }
    /// 设置加粗。
    pub fn set_bold(&mut self, v: bool) {
        self.properties.bold = v;
    }

    /// 是否斜体。
    pub fn italic(&self) -> bool {
        self.properties.italic
    }
    /// 设置斜体。
    pub fn set_italic(&mut self, v: bool) {
        self.properties.italic = v;
    }

    /// 颜色便捷访问(拷贝)。
    pub fn color(&self) -> Color {
        self.properties.color.clone()
    }
    /// 设置颜色(接受 `RGBColor` / `SchemeColor` / `PresetColor` 任一)。
    pub fn set_color(&mut self, c: impl Into<Color>) {
        self.properties.color = c.into();
    }

    /// 字体名(拉丁 / 主体)。
    pub fn font_name(&self) -> Option<&str> {
        self.properties.latin_font.as_deref()
    }
    /// 设置字体名。
    pub fn set_font_name(&mut self, name: impl Into<String>) {
        self.properties.latin_font = Some(name.into());
    }

    /// 东亚字体名(`<a:ea typeface="..."/>`,对应中文/日文/韩文字体)。
    ///
    /// 与 [`Run::font_name`](拉丁字体)配对使用:PowerPoint 根据字符脚本
    /// 自动切换 latin/ea 字体。例如同一 Run 内 "Hello 你好" 会用 latin 字体
    /// 渲染 "Hello",用 ea 字体渲染 "你好"。
    pub fn eastasia_name(&self) -> Option<&str> {
        self.properties.eastasia_font.as_deref()
    }
    /// 设置东亚字体名。
    ///
    /// # 参数
    /// - `name`:东亚字体名称(如 `"宋体"` / `"Microsoft YaHei"` / `"MS Mincho"`)。
    pub fn set_eastasia_name(&mut self, name: impl Into<String>) {
        self.properties.eastasia_font = Some(name.into());
    }

    /// 复杂脚本字体名(`<a:cs typeface="..."/>`,对应阿拉伯语/希伯来语/泰语等)。
    ///
    /// 复杂脚本需要双向排版(RTL)或连字处理,PowerPoint 用 cs 字体渲染这类字符。
    pub fn complex_script_name(&self) -> Option<&str> {
        self.properties.cs_font.as_deref()
    }
    /// 设置复杂脚本字体名。
    ///
    /// # 参数
    /// - `name`:复杂脚本字体名称(如 `"Arial"` / `"Tahoma"` / `"Traditional Arabic"`)。
    pub fn set_complex_script_name(&mut self, name: impl Into<String>) {
        self.properties.cs_font = Some(name.into());
    }

    /// 下划线便捷访问。
    pub fn underline(&self) -> Option<Underline> {
        self.properties.underline
    }
    /// 设置下划线。
    pub fn set_underline(&mut self, v: Underline) {
        self.properties.underline = Some(v);
    }

    /// 是否删除线。
    pub fn strike(&self) -> bool {
        self.properties.strike
    }
    /// 设置删除线。
    pub fn set_strike(&mut self, v: bool) {
        self.properties.strike = v;
    }

    /// 是否双删除线(TODO-017 高阶 API)。
    pub fn double_strike(&self) -> bool {
        self.properties.strike_dbl
    }
    /// 设置双删除线(TODO-017 高阶 API)。
    pub fn set_double_strike(&mut self, v: bool) {
        self.properties.strike_dbl = v;
    }

    /// 取高亮背景色(TODO-018 高阶 API)。
    ///
    /// 返回 `None` 表示未设置高亮。
    pub fn highlight(&self) -> Option<&Color> {
        self.properties.highlight.as_ref()
    }
    /// 设置高亮背景色(TODO-018 高阶 API)。
    ///
    /// 传入 `None` 清除高亮。
    pub fn set_highlight(&mut self, color: Option<Color>) {
        self.properties.highlight = color;
    }
    /// 清除高亮(TODO-018 高阶 API 便捷方法)。
    pub fn clear_highlight(&mut self) {
        self.properties.highlight = None;
    }

    /// 取点击超链接(TODO-026 高阶 API)。
    pub fn hlink_click(&self) -> Option<&Hyperlink> {
        self.properties.hlink_click.as_ref()
    }
    /// 设置点击超链接(TODO-026 高阶 API)。
    pub fn set_hlink_click(&mut self, hl: Hyperlink) {
        self.properties.hlink_click = Some(hl);
    }
    /// 清除点击超链接(TODO-026 高阶 API)。
    pub fn clear_hlink_click(&mut self) {
        self.properties.hlink_click = None;
    }

    /// 取悬停超链接(TODO-026 高阶 API)。
    pub fn hlink_hover(&self) -> Option<&Hyperlink> {
        self.properties.hlink_hover.as_ref()
    }
    /// 设置悬停超链接(TODO-026 高阶 API)。
    pub fn set_hlink_hover(&mut self, hl: Hyperlink) {
        self.properties.hlink_hover = Some(hl);
    }
    /// 清除悬停超链接(TODO-026 高阶 API)。
    pub fn clear_hlink_hover(&mut self) {
        self.properties.hlink_hover = None;
    }

    /// 设置外部 URL 超链接(TODO-026 高阶 API 便捷方法)。
    ///
    /// 对标 python-pptx `run.hyperlink.address = url`。
    ///
    /// # 参数
    /// - `rid`:关系 id(指向 slide `.rels` 中注册的外部 URL)。
    ///   **注意**:本方法只设置 run 的 `hlinkClick`,不负责创建 OPC 关系。
    ///   调用方需自行在 slide 的 `.rels` 中注册该 URL 并取得 rid。
    /// - `tooltip`:可选悬停提示文本;`None` 表示不写出 tooltip 属性。
    ///
    /// # 示例
    /// ```no_run
    /// # use ooxml_core::oxml::txbody::Run;
    /// let mut run = Run::new("点击");
    /// run.set_hyperlink("rIdHlink1", Some("打开链接"));
    /// ```
    pub fn set_hyperlink(&mut self, rid: impl Into<String>, tooltip: Option<&str>) {
        let mut hl = Hyperlink::new(rid);
        if let Some(t) = tooltip {
            hl.tooltip = Some(t.to_string());
        }
        self.properties.hlink_click = Some(hl);
    }

    /// 设置跳转幻灯片动作超链接(TODO-026 高阶 API 便捷方法)。
    ///
    /// 对标 python-pptx `run.hyperlink.action = "ppaction://hlinksldjump"`。
    ///
    /// # 说明
    /// 仅设置 `action = ppaction://hlinksldjump`,具体跳转目标由 slide `.rels`
    /// 中的关系决定(调用方需自行注册)。若要同时绑定目标 slide,需配合
    /// `Hyperlink::new(rid)` 手动设置 rid + action。
    pub fn set_slide_jump(&mut self) {
        self.properties.hlink_click = Some(Hyperlink::new_slide_jump());
    }

    /// 取 `Font` 高阶视图(用于批量设置字体属性、超链接等)。
    ///
    /// 对标 python-pptx `run.font`。
    pub fn font(&mut self) -> Font<'_> {
        Font::new(&mut self.properties)
    }
}

fn is_default_rpr(p: &RunProperties) -> bool {
    p.size.is_none()
        && !p.bold
        && !p.italic
        && p.underline.is_none()
        && !p.strike
        && !p.strike_dbl
        && matches!(p.color, Color::None)
        && p.highlight.is_none()
        && p.latin_font.is_none()
        && p.eastasia_font.is_none()
        && p.cs_font.is_none()
        && p.baseline.is_none()
        && p.kerning.is_none()
        && p.spc.is_none()
        && p.caps == Caps::None
        && p.lang.is_none()
        && p.alpha.is_none()
        && p.hlink_click.is_none()
        && p.hlink_hover.is_none()
}

/// 字段类型(`<a:fld type="...">`)。
///
/// 对标 python-pptx 中字段类型的常用子集。
/// OOXML 中 `type` 属性的取值较多,这里枚举最常见的几种。
#[derive(Clone, Debug, Default, PartialEq)]
pub enum FieldType {
    /// 幻灯片编号(`type="slidenum"`)。
    #[default]
    SlideNumber,
    /// 日期时间(`type="datetime"`),使用默认格式。
    DateTime,
    /// 日期时间格式 1(`type="datetime1"`,如 `1/1/2024`)。
    DateTime1,
    /// 日期时间格式 2(`type="datetime2"`,如 `Monday, January 1, 2024`)。
    DateTime2,
    /// 日期时间格式 3(`type="datetime3"`,如 `January 1, 2024`)。
    DateTime3,
    /// 页脚(`type="footer"`)。
    Footer,
    /// 自定义字段类型(任意字符串)。
    Custom(String),
}

impl FieldType {
    /// 转 OOXML `type` 属性字面量。
    pub fn as_str(&self) -> &str {
        match self {
            FieldType::SlideNumber => "slidenum",
            FieldType::DateTime => "datetime",
            FieldType::DateTime1 => "datetime1",
            FieldType::DateTime2 => "datetime2",
            FieldType::DateTime3 => "datetime3",
            FieldType::Footer => "footer",
            FieldType::Custom(s) => s.as_str(),
        }
    }

    /// 从 OOXML `type` 属性字面量构造。
    pub fn from_str_value(s: &str) -> Self {
        match s {
            "slidenum" => FieldType::SlideNumber,
            "datetime" => FieldType::DateTime,
            "datetime1" => FieldType::DateTime1,
            "datetime2" => FieldType::DateTime2,
            "datetime3" => FieldType::DateTime3,
            "footer" => FieldType::Footer,
            other => FieldType::Custom(other.to_string()),
        }
    }
}

/// 字段(`<a:fld>`)。
///
/// 对标 python-pptx 中字段对象。字段是段落中特殊的"动态文本",
/// 如幻灯片编号、日期时间、页脚等,由 PowerPoint 在渲染时自动填充。
///
/// # OOXML 结构
/// ```xml
/// <a:fld id="{GUID}" type="slidenum">
///   <a:rPr .../>
///   <a:t>1</a:t>
/// </a:fld>
/// ```
#[derive(Clone, Debug, Default)]
pub struct Field {
    /// 字段 GUID(`id` 属性)。PowerPoint 用此关联字段实例。
    pub id: String,
    /// 字段类型(`type` 属性)。
    pub field_type: FieldType,
    /// Run 属性(`<a:rPr>`)。
    pub properties: RunProperties,
    /// 字段文本(`<a:t>`)——渲染前的占位文本。
    pub text: String,
}

impl Field {
    /// 创建一个指定类型的字段。
    ///
    /// # 参数
    /// - `field_type`:字段类型;
    /// - `text`:占位文本(如幻灯片编号用 `"1"`,日期用 `"1/1/2024"`)。
    pub fn new(field_type: FieldType, text: impl Into<String>) -> Self {
        Field {
            id: format!("{{{}}}", uuid_like()),
            field_type,
            properties: RunProperties::default(),
            text: text.into(),
        }
    }

    /// 写 XML。
    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
        w.open_with(
            "a:fld",
            &[("id", self.id.as_str()), ("type", self.field_type.as_str())],
        );
        self.properties.write_xml(w, "a:rPr");
        w.open("a:t");
        w.text(&self.text);
        w.close("a:t");
        w.close("a:fld");
    }
}

/// 生成一个简易的 GUID 风格字符串(非标准 UUID,仅用于字段 id 占位)。
///
/// PowerPoint 对 `id` 属性的格式要求是 `{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}`,
/// 但实际上只要是大括号包裹的唯一字符串即可被正确识别。
/// 这里用时间戳 + 计数器生成,避免引入 uuid 依赖。
fn uuid_like() -> String {
    use std::sync::atomic::{AtomicU64, Ordering};
    static COUNTER: AtomicU64 = AtomicU64::new(1);
    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
    let ts = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos() as u64)
        .unwrap_or(0);
    format!("{:016x}-{:016x}", ts, n)
}

/// 段落中的"结束段落"Run(`</a:p>` 之前可附带 `a:endParaRPr`)。
#[derive(Clone, Debug, Default)]
pub struct Paragraph {
    /// 段落属性。
    pub properties: ParagraphProperties,
    /// 段落中的 Run 序列(按出现顺序)。
    pub runs: Vec<Run>,
    /// 段落中的字段序列(`<a:fld>`)。
    ///
    /// 字段在 Run 之后序列化。OOXML 允许 `<a:r>` 和 `<a:fld>` 交错出现,
    /// 但本实现简化为 runs 先于 fields 输出。
    pub fields: Vec<Field>,
    /// 段落末尾的属性(`a:endParaRPr`)。
    pub end_properties: Option<RunProperties>,
}

impl Paragraph {
    /// 新建一个空段落。
    pub fn new() -> Self {
        Paragraph::default()
    }

    /// 写 XML。
    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
        w.open("a:p");
        self.properties.write_xml(w);
        for r in &self.runs {
            r.write_xml(w);
        }
        for f in &self.fields {
            f.write_xml(w);
        }
        if let Some(rpr) = &self.end_properties {
            rpr.write_xml(w, "a:endParaRPr");
        }
        w.close("a:p");
    }

    // --------------------- 高阶 API(对齐 python-pptx _Paragraph) ---------------------

    /// 新增一个空 Run,返回其可变引用。
    ///
    /// 对应 python-pptx 中 `paragraph.add_run()`。
    pub fn add_run(&mut self) -> &mut Run {
        self.runs.push(Run::default());
        // push 后直接按索引取最后一个元素,避免 expect
        let idx = self.runs.len() - 1;
        &mut self.runs[idx]
    }

    /// 新增一个**带文本**的 Run,返回其可变引用。
    ///
    /// 对应 python-pptx 中 `paragraph.add_run(text)`。
    pub fn add_run_with_text(&mut self, text: impl Into<String>) -> &mut Run {
        self.runs.push(Run::new(text));
        // push 后直接按索引取最后一个元素,避免 expect
        let idx = self.runs.len() - 1;
        &mut self.runs[idx]
    }

    /// 追加一个**换行符 Run**(`<a:br/>`)。
    ///
    /// python-pptx 中由 `paragraph.add_run().text = "\n"` 触发;在
    /// 本库中换行被建模为独立 Run——这样既保留属性语义又简化序列化。
    pub fn add_line_break(&mut self) -> &mut Run {
        self.runs.push(Run {
            text: String::from("\n"),
            properties: RunProperties::default(),
        });
        // push 后直接按索引取最后一个元素,避免 expect
        let idx = self.runs.len() - 1;
        &mut self.runs[idx]
    }

    /// 清空段落中的全部 Run(保留段落属性)。
    ///
    /// 对应 python-pptx 中对 `_Paragraph` 重新赋值的常见用法。
    pub fn clear_runs(&mut self) {
        self.runs.clear();
    }

    /// 新增一个字段(`<a:fld>`),返回其可变引用。
    ///
    /// 对标 python-pptx 中通过 `paragraph.add_field()` 添加幻灯片编号/日期等动态字段。
    ///
    /// # 参数
    /// - `field_type`:字段类型(如 [`FieldType::SlideNumber`]);
    /// - `text`:占位文本(如 `"1"`)。
    pub fn add_field(&mut self, field_type: FieldType, text: impl Into<String>) -> &mut Field {
        self.fields.push(Field::new(field_type, text));
        let idx = self.fields.len() - 1;
        &mut self.fields[idx]
    }

    /// 清空段落中的全部字段。
    pub fn clear_fields(&mut self) {
        self.fields.clear();
    }

    /// 替换为单段单 Run 的纯文本(保留段落属性)。
    ///
    /// 与 `TextBox::set_text` 不同,**不**按 `\n` 切分多段;
    /// 适合"修改 Run 文本"场景。
    pub fn set_text(&mut self, text: impl Into<String>) -> &mut Run {
        self.runs.clear();
        self.runs.push(Run::new(text));
        // push 后直接按索引取最后一个元素,避免 expect
        let idx = self.runs.len() - 1;
        &mut self.runs[idx]
    }

    /// 取段落全部 Run 文本拼接(不含 `\n`)。
    pub fn text(&self) -> String {
        let mut out = String::new();
        for r in &self.runs {
            // 把 Run 内含的 "\n" 视作软换行 → 真实换行
            out.push_str(&r.text);
        }
        out
    }

    /// 水平对齐(便捷访问)。
    pub fn alignment(&self) -> Option<Alignment> {
        self.properties.alignment
    }
    /// 设置水平对齐。
    pub fn set_alignment(&mut self, v: Alignment) {
        self.properties.alignment = Some(v);
    }

    /// 段落级别(0-8)。
    pub fn level(&self) -> u8 {
        self.properties.level
    }
    /// 设置段落级别。
    pub fn set_level(&mut self, lvl: u8) {
        self.properties.level = lvl;
    }

    /// 行距(点数,固定值)。与 `set_line_spacing_pct` 互斥。
    pub fn line_spacing(&self) -> Option<Pt> {
        // OOXML 内部用 EMU(百分之一磅 1pt = 12700 EMU),对外用 Pt
        self.properties
            .line_spacing
            .map(|emu| Pt(emu as f64 / 12_700.0))
    }
    /// 设置行距为**固定点数**(1 pt = 12700 EMU)。
    pub fn set_line_spacing(&mut self, v: Pt) {
        let emu = (v.value() * 12_700.0) as i32;
        self.properties.line_spacing = Some(emu);
        self.properties.line_spacing_pct = None;
    }

    /// 行距(百分比,1.0 = 100%)。与 `set_line_spacing` 互斥。
    pub fn line_spacing_pct(&self) -> Option<f32> {
        self.properties.line_spacing_pct.map(|v| v as f32 / 1000.0)
    }
    /// 设置行距为**倍数**(1.0 = 100% = `1000`,1.5 = 150% = `1500`)。
    pub fn set_line_spacing_pct(&mut self, v: f32) {
        // python-pptx 的 line_spacing = 1.5 ⇒ 150000;这里用 v * 1000 与 OOXML 百分位对齐
        self.properties.line_spacing_pct = Some((v * 1000.0) as i32);
        self.properties.line_spacing = None;
    }

    /// 段前间距(EMU)。
    pub fn space_before(&self) -> Option<Emu> {
        self.properties.space_before
    }
    /// 设置段前间距。
    pub fn set_space_before(&mut self, emu: Emu) {
        self.properties.space_before = Some(emu);
    }

    /// 段后间距(EMU)。
    pub fn space_after(&self) -> Option<Emu> {
        self.properties.space_after
    }
    /// 设置段后间距。
    pub fn set_space_after(&mut self, emu: Emu) {
        self.properties.space_after = Some(emu);
    }

    /// 缩进便捷访问。
    pub fn indent(&self) -> Indent {
        self.properties.indent
    }
    /// 设置缩进(一次性给 4 个字段)。
    pub fn set_indent(
        &mut self,
        left: Option<Emu>,
        right: Option<Emu>,
        first_line: Option<Emu>,
        hanging: Option<i32>,
    ) {
        self.properties.indent = Indent {
            left,
            right,
            first_line,
            hanging,
        };
    }

    // --------------------- 段落末尾属性(endParaRPr,TODO-047) ---------------------

    /// 取段落末尾属性(`<a:endParaRPr>`)的不可变引用。
    ///
    /// `endParaRPr` 用于指定段落末尾(最后一个 Run 之后)的默认 Run 属性。
    /// PowerPoint 在用户把光标移到段落末尾时,会用此属性渲染后续输入的文本。
    ///
    /// 对应 OOXML 元素 `<a:endParaRPr>`,结构与 `<a:rPr>` 完全一致。
    pub fn end_para_rpr(&self) -> Option<&RunProperties> {
        self.end_properties.as_ref()
    }

    /// 取段落末尾属性的可变引用。
    pub fn end_para_rpr_mut(&mut self) -> Option<&mut RunProperties> {
        self.end_properties.as_mut()
    }

    /// 设置段落末尾属性(`<a:endParaRPr>`)。
    ///
    /// 若段落已有 `endParaRPr`,会被覆盖。
    ///
    /// # 示例
    /// ```no_run
    /// # use ooxml_core::oxml::txbody::{Paragraph, RunProperties};
    /// # use ooxml_core::Pt;
    /// let mut p = Paragraph::new();
    /// let mut rpr = RunProperties::default();
    /// rpr.size = Some(Pt(24.0));
    /// rpr.latin_font = Some("Calibri".to_string());
    /// p.set_end_para_rpr(rpr);
    /// assert!(p.end_para_rpr().is_some());
    /// ```
    pub fn set_end_para_rpr(&mut self, rpr: RunProperties) {
        self.end_properties = Some(rpr);
    }

    /// 清除段落末尾属性(删除 `<a:endParaRPr>`)。
    pub fn clear_end_para_rpr(&mut self) {
        self.end_properties = None;
    }
}

/// 文本体 `<p:txBody>`。
///
/// 对应 python-pptx 的 `TextFrame` 类:提供段落列表 + 自动调整 / 边距 /
/// 垂直对齐 / 换行等"文本框级"属性。
#[derive(Clone, Debug, Default)]
pub struct TextBody {
    pub body_properties: Option<BodyProperties>,
    pub paragraphs: Vec<Paragraph>,
}

impl TextBody {
    pub fn new() -> Self {
        TextBody::default()
    }

    /// 写 XML。
    ///
    /// `tag` 参数指定外层元素标签名(如 `"p:txBody"` 用于 PPTX,
    /// `"c:txBody"` 用于 chart 标题等)。这样本类型可在不同格式间复用。
    pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
        w.open(tag);
        if let Some(bp) = &self.body_properties {
            bp.write_xml(w);
        }
        for p in &self.paragraphs {
            p.write_xml(w);
        }
        w.close(tag);
    }

    // --------------------- 高阶 API(对齐 python-pptx TextFrame) ---------------------

    /// 取所有文本(段间 `\n`)。
    pub fn text(&self) -> String {
        let mut out = String::new();
        for (i, p) in self.paragraphs.iter().enumerate() {
            if i > 0 {
                out.push('\n');
            }
            for r in &p.runs {
                out.push_str(&r.text);
            }
        }
        out
    }

    /// 取首个段落(`TextFrame.paragraphs[0]` 的等价物)。
    pub fn first_paragraph(&self) -> Option<&Paragraph> {
        self.paragraphs.first()
    }

    /// 取首个段落的可变引用。
    pub fn first_paragraph_mut(&mut self) -> Option<&mut Paragraph> {
        self.paragraphs.first_mut()
    }

    /// 取第 idx 个段落的不可变引用(越界返回 None)。
    pub fn paragraph(&self, idx: usize) -> Option<&Paragraph> {
        self.paragraphs.get(idx)
    }

    /// 取第 idx 个段落的可变引用(越界返回 None)。
    pub fn paragraph_mut(&mut self, idx: usize) -> Option<&mut Paragraph> {
        self.paragraphs.get_mut(idx)
    }

    /// 新增一个空段落并返回其可变引用。
    ///
    /// 对应 `TextFrame.add_paragraph()`。
    pub fn add_paragraph(&mut self) -> &mut Paragraph {
        self.paragraphs.push(Paragraph::new());
        // push 后直接按索引取最后一个元素,避免 expect
        let idx = self.paragraphs.len() - 1;
        &mut self.paragraphs[idx]
    }

    /// 新增一个**带文本**的段落,返回其可变引用。
    ///
    /// 文本按 `\n` 切分为多段,与 python-pptx 的 `TextFrame.text = "a\nb"` 行为对齐。
    pub fn add_paragraph_with_text(&mut self, text: &str) -> &mut Paragraph {
        for line in text.split('\n') {
            let mut p = Paragraph::new();
            p.runs.push(Run::new(line));
            self.paragraphs.push(p);
        }
        // split('\n') 至少产生一个元素(空字符串也会产生一段),所以 paragraphs 非空
        let idx = self.paragraphs.len() - 1;
        &mut self.paragraphs[idx]
    }

    /// 移除第 idx 个段落。
    pub fn remove_paragraph(&mut self, idx: usize) -> Option<Paragraph> {
        if idx < self.paragraphs.len() {
            Some(self.paragraphs.remove(idx))
        } else {
            None
        }
    }

    /// 清空段落(保留一个空段,对应 python-pptx `TextFrame.clear()` 语义)。
    pub fn clear(&mut self) {
        self.paragraphs.clear();
        self.paragraphs.push(Paragraph::new());
    }

    /// 整体替换文本(按 `\n` 切分为多段,丢弃原段落属性)。
    ///
    /// 对应 python-pptx `text_frame.text = "a\nb"`。
    pub fn set_text(&mut self, text: &str) {
        self.paragraphs.clear();
        for line in text.split('\n') {
            let mut p = Paragraph::new();
            p.runs.push(Run::new(line));
            self.paragraphs.push(p);
        }
    }

    // --------------------- BodyProperties 便捷访问 ---------------------

    /// 取/建 `body_properties`。
    fn ensure_body_properties(&mut self) -> &mut BodyProperties {
        if self.body_properties.is_none() {
            self.body_properties = Some(BodyProperties::default());
        }
        // 安全:上方 if 确保了 body_properties 为 Some;使用 match 避免 expect
        match &mut self.body_properties {
            Some(bp) => bp,
            None => unreachable!("body_properties was just initialized above"),
        }
    }

    /// 自动调整策略(`TextFrame.auto_size`)。
    pub fn auto_size(&self) -> MsoAutoSize {
        self.body_properties
            .as_ref()
            .and_then(|b| b.auto_size())
            .unwrap_or(MsoAutoSize::None)
    }
    /// 设置自动调整策略。
    pub fn set_auto_size(&mut self, v: MsoAutoSize) {
        self.ensure_body_properties().set_auto_size(v);
    }

    /// 垂直对齐(`TextFrame.vertical_anchor`)。
    pub fn vertical_anchor(&self) -> Option<MsoAnchor> {
        self.body_properties.as_ref().and_then(|b| b.anchor)
    }
    /// 设置垂直对齐。
    pub fn set_vertical_anchor(&mut self, v: MsoAnchor) {
        self.ensure_body_properties().anchor = Some(v);
    }

    /// 是否自动换行(`TextFrame.word_wrap`)。
    ///
    /// 返回 `None` 表示走默认值 / 继承。
    pub fn word_wrap(&self) -> Option<bool> {
        self.body_properties.as_ref().and_then(|b| match b.wrap {
            Some(TextWrapping::Square) => Some(true),
            Some(TextWrapping::None) => Some(false),
            None => None,
        })
    }
    /// 设置是否自动换行。
    pub fn set_word_wrap(&mut self, v: bool) {
        self.ensure_body_properties().wrap = Some(if v {
            TextWrapping::Square
        } else {
            TextWrapping::None
        });
    }

    /// 左边距(EMU)。
    pub fn margin_left(&self) -> Option<Emu> {
        self.body_properties
            .as_ref()
            .and_then(|b| b.insets.as_ref().map(|i| i.left))
    }
    /// 右边距。
    pub fn margin_right(&self) -> Option<Emu> {
        self.body_properties
            .as_ref()
            .and_then(|b| b.insets.as_ref().map(|i| i.right))
    }
    /// 上边距。
    pub fn margin_top(&self) -> Option<Emu> {
        self.body_properties
            .as_ref()
            .and_then(|b| b.insets.as_ref().map(|i| i.top))
    }
    /// 下边距。
    pub fn margin_bottom(&self) -> Option<Emu> {
        self.body_properties
            .as_ref()
            .and_then(|b| b.insets.as_ref().map(|i| i.bottom))
    }
    /// 一次性设置四向边距。
    pub fn set_margins(&mut self, left: Emu, top: Emu, right: Emu, bottom: Emu) {
        let bp = self.ensure_body_properties();
        bp.insets = Some(Inset {
            left,
            top,
            right,
            bottom,
        });
    }
    /// 设置左边距。
    pub fn set_margin_left(&mut self, emu: Emu) {
        let bp = self.ensure_body_properties();
        let i = bp.insets.get_or_insert(Inset::default());
        i.left = emu;
    }
    /// 设置右边距。
    pub fn set_margin_right(&mut self, emu: Emu) {
        let bp = self.ensure_body_properties();
        let i = bp.insets.get_or_insert(Inset::default());
        i.right = emu;
    }
    /// 设置上边距。
    pub fn set_margin_top(&mut self, emu: Emu) {
        let bp = self.ensure_body_properties();
        let i = bp.insets.get_or_insert(Inset::default());
        i.top = emu;
    }
    /// 设置下边距。
    pub fn set_margin_bottom(&mut self, emu: Emu) {
        let bp = self.ensure_body_properties();
        let i = bp.insets.get_or_insert(Inset::default());
        i.bottom = emu;
    }

    /// 文本列数(`numCol` 属性)。
    ///
    /// 返回 `None` 表示未设置(默认单列);`Some(1)` 等价于单列。
    pub fn num_cols(&self) -> Option<u32> {
        self.body_properties.as_ref().and_then(|b| b.num_cols)
    }
    /// 设置文本列数。
    pub fn set_num_cols(&mut self, count: u32) {
        let bp = self.ensure_body_properties();
        bp.num_cols = Some(count);
    }
    /// 列间距(`spcCol` 属性,EMU)。
    pub fn col_spacing(&self) -> Option<Emu> {
        self.body_properties.as_ref().and_then(|b| b.col_spacing)
    }
    /// 设置列间距。
    pub fn set_col_spacing(&mut self, emu: Emu) {
        let bp = self.ensure_body_properties();
        bp.col_spacing = Some(emu);
    }
}

/// 文本体属性 `<a:bodyPr>`。
#[derive(Clone, Debug, Default)]
pub struct BodyProperties {
    /// 文本与边界框的内边距。
    pub insets: Option<Inset>,
    /// 文字方向。
    pub vertical: Option<String>,
    /// 文字方向(rotation)。
    pub rotation: Option<i32>,
    /// 自动换行(`wrap="square"/"none"`)。
    pub wrap: Option<TextWrapping>,
    /// 文本框在溢出时的行为。
    pub sp_auto_fit: bool,
    /// 形状自适应文字。
    pub norm_autofit: bool,
    /// 锚定(top/ctr/b)。
    pub anchor: Option<MsoAnchor>,
    /// 水平对齐(l/ctr/just/dist)——仅占位/组合时使用。
    pub anchor_ctr: bool,
    /// 文本列数(`numCol` 属性,1 表示单列即默认)。
    ///
    /// 对应 python-pptx `TextFrame.word_wrap` 相关的多列布局。
    /// 多列时文本从左到右依次填充各列。
    pub num_cols: Option<u32>,
    /// 列间距(`spcCol` 属性,EMU)。
    ///
    /// 仅当 `num_cols > 1` 时有效。
    pub col_spacing: Option<Emu>,
}

/// 文本框内边距(`<a:bodyPr lIns tIns rIns bIns>`)。
///
/// 在 `<a:bodyPr>` 元素上以 4 个独立属性表示,单位 EMU。
/// python-pptx 中 `TextFrame.margin_left/right/top/bottom` 即对应这 4 个值。
#[derive(Copy, Clone, Debug, Default)]
pub struct Inset {
    /// 左内边距(EMU)。
    pub left: Emu,
    /// 上内边距(EMU)。
    pub top: Emu,
    /// 右内边距(EMU)。
    pub right: Emu,
    /// 下内边距(EMU)。
    pub bottom: Emu,
}

impl BodyProperties {
    /// 写 XML。
    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
        // 提前取出所有要序列化的字符串,扩展到函数末尾
        let l_s = self.insets.as_ref().map(|i| i.left.value().to_string());
        let t_s = self.insets.as_ref().map(|i| i.top.value().to_string());
        let r_s = self.insets.as_ref().map(|i| i.right.value().to_string());
        let b_s = self.insets.as_ref().map(|i| i.bottom.value().to_string());
        let rot_s = self.rotation.map(|v| v.to_string());
        let wrap_s = self.wrap.map(|v| v.as_str());
        let anchor_s = self.anchor.map(|v| v.as_str());
        let numcol_s = self.num_cols.map(|v| v.to_string());
        let spccol_s = self.col_spacing.map(|v| v.value().to_string());

        let mut attrs: Vec<(&str, &str)> = Vec::new();
        if let Some(s) = &l_s {
            attrs.push(("lIns", s));
        }
        if let Some(s) = &t_s {
            attrs.push(("tIns", s));
        }
        if let Some(s) = &r_s {
            attrs.push(("rIns", s));
        }
        if let Some(s) = &b_s {
            attrs.push(("bIns", s));
        }
        if let Some(v) = &self.vertical {
            attrs.push(("vert", v));
        }
        if let Some(s) = &rot_s {
            attrs.push(("rot", s));
        }
        if let Some(s) = &wrap_s {
            attrs.push(("wrap", s));
        }
        if let Some(s) = &anchor_s {
            attrs.push(("anchor", s));
        }
        if let Some(s) = &numcol_s {
            attrs.push(("numCol", s));
        }
        if let Some(s) = &spccol_s {
            attrs.push(("spcCol", s));
        }
        w.open_with("a:bodyPr", &attrs);
        // 自动调整子元素:优先按 sp_auto_fit / norm_autofit 标志位写出(保留兼容路径)
        if self.sp_auto_fit {
            w.empty("a:spAutoFit");
        } else if self.norm_autofit {
            w.empty("a:normAutofit");
        }
        w.close("a:bodyPr");
    }

    /// 取 [`MsoAutoSize`] 视图(合并 `sp_auto_fit` / `norm_autofit` 两个 bool)。
    pub fn auto_size(&self) -> Option<MsoAutoSize> {
        if self.sp_auto_fit {
            Some(MsoAutoSize::ShapeToFitText)
        } else if self.norm_autofit {
            Some(MsoAutoSize::TextToFitShape)
        } else {
            None
        }
    }
    /// 设置 [`MsoAutoSize`] 视图(同时清空另一个标志位以保持互斥)。
    pub fn set_auto_size(&mut self, v: MsoAutoSize) {
        match v {
            MsoAutoSize::None => {
                self.sp_auto_fit = false;
                self.norm_autofit = false;
            }
            MsoAutoSize::ShapeToFitText => {
                self.sp_auto_fit = true;
                self.norm_autofit = false;
            }
            MsoAutoSize::TextToFitShape => {
                self.sp_auto_fit = false;
                self.norm_autofit = true;
            }
        }
    }
}

impl From<RGBColor> for Color {
    fn from(c: RGBColor) -> Self {
        Color::RGB(c)
    }
}

// ====================================================================
// 高阶 Font 视图(python-pptx 风格 `pptx.text.text.Font`)
// ====================================================================

use crate::oxml::color::ColorFormat;

/// 字体高阶视图(`pptx.text.text.Font`)。
///
/// # 与 python-pptx 的对应
///
/// - `pptx.text.text.Font` ←→ [`Font`];
/// - `font.bold = True` / `font.size = Pt(24)` / `font.color.rgb = RGBColor(...)`
///   ←→ [`Font::set_bold`] / [`Font::set_size`] / [`Font::color`].set_rgb()。
///
/// # 设计要点
///
/// - **借用 + 透明代理**:构造时传入 `&mut RunProperties`;
/// - **零分配**:颜色走 [`ColorFormat`] 借用;
/// - **可空值语义**:`name` / `size` / `lang` 等字段用 `Option<T>`,
///   `None` 表示"走主题继承"——与 python-pptx `None` 一致。
#[derive(Debug)]
pub struct Font<'a> {
    /// 底层 [`RunProperties`] 引用。
    rpr: &'a mut RunProperties,
}

impl<'a> Font<'a> {
    /// 构造。
    pub fn new(rpr: &'a mut RunProperties) -> Self {
        Font { rpr }
    }
    /// 底层 rpr 不可变引用。
    pub fn rpr(&self) -> &RunProperties {
        self.rpr
    }
    /// 底层 rpr 可变引用。
    pub fn rpr_mut(&mut self) -> &mut RunProperties {
        self.rpr
    }

    /// 颜色 [`ColorFormat`] 代理。
    pub fn color(&mut self) -> ColorFormat<'_> {
        ColorFormat::new(&mut self.rpr.color)
    }

    /// 字号(Pt)。
    pub fn size(&self) -> Option<Pt> {
        self.rpr.size
    }
    /// 设置字号。
    pub fn set_size(&mut self, v: Pt) {
        self.rpr.size = Some(v);
    }
    /// 清空字号(走主题继承)。
    pub fn clear_size(&mut self) {
        self.rpr.size = None;
    }

    /// 加粗。
    pub fn bold(&self) -> bool {
        self.rpr.bold
    }
    /// 设置加粗。
    pub fn set_bold(&mut self, v: bool) {
        self.rpr.bold = v;
    }

    /// 斜体。
    pub fn italic(&self) -> bool {
        self.rpr.italic
    }
    /// 设置斜体。
    pub fn set_italic(&mut self, v: bool) {
        self.rpr.italic = v;
    }

    /// 删除线。
    pub fn strike(&self) -> bool {
        self.rpr.strike
    }
    /// 设置删除线。
    pub fn set_strike(&mut self, v: bool) {
        self.rpr.strike = v;
    }

    /// 双删除线。
    ///
    /// 对标 python-pptx `font._rPr.attrib['strike'] == 'dblStrike'`。
    /// 当为 `true` 时,写出 `strike="dblStrike"`;普通删除线请用 [`Self::set_strike`]。
    pub fn double_strike(&self) -> bool {
        self.rpr.strike_dbl
    }
    /// 设置双删除线。
    pub fn set_double_strike(&mut self, v: bool) {
        self.rpr.strike_dbl = v;
    }

    /// 高亮色。
    ///
    /// 对标 python-pptx `Font.highlight_color`(v0.6.21+)。
    /// 返回 `None` 表示未设置高亮。
    pub fn highlight(&self) -> Option<&Color> {
        self.rpr.highlight.as_ref()
    }
    /// 设置高亮色。
    ///
    /// 传入 `None` 清除高亮;传入 `Color::None` 也清除高亮。
    pub fn set_highlight(&mut self, color: Option<Color>) {
        match color {
            None => self.rpr.highlight = None,
            Some(Color::None) => self.rpr.highlight = None,
            Some(c) => self.rpr.highlight = Some(c),
        }
    }

    /// 下划线。
    pub fn underline(&self) -> Option<Underline> {
        self.rpr.underline
    }
    /// 设置下划线(`None` 表示清除)。
    pub fn set_underline(&mut self, v: Option<Underline>) {
        self.rpr.underline = v;
    }

    /// 字体名(拉丁)。
    pub fn name(&self) -> Option<&str> {
        self.rpr.latin_font.as_deref()
    }
    /// 设置字体名。
    pub fn set_name(&mut self, n: impl Into<String>) {
        self.rpr.latin_font = Some(n.into());
    }
    /// 清空字体名(走主题继承)。
    pub fn clear_name(&mut self) {
        self.rpr.latin_font = None;
    }

    /// 东亚字体。
    pub fn eastasia_name(&self) -> Option<&str> {
        self.rpr.eastasia_font.as_deref()
    }
    /// 设置东亚字体。
    pub fn set_eastasia_name(&mut self, n: impl Into<String>) {
        self.rpr.eastasia_font = Some(n.into());
    }
    /// 清空东亚字体(走主题继承)。
    ///
    /// 对应 OOXML 中删除 `<a:ea>` 元素,PowerPoint 将使用主题的
    /// `minorFont.ea` / `majorFont.ea` 作为回退。
    pub fn clear_eastasia_name(&mut self) {
        self.rpr.eastasia_font = None;
    }

    /// 复杂脚本字体。
    pub fn complex_script_name(&self) -> Option<&str> {
        self.rpr.cs_font.as_deref()
    }
    /// 设置复杂脚本字体。
    pub fn set_complex_script_name(&mut self, n: impl Into<String>) {
        self.rpr.cs_font = Some(n.into());
    }
    /// 清空复杂脚本字体(走主题继承)。
    ///
    /// 对应 OOXML 中删除 `<a:cs>` 元素,PowerPoint 将使用主题的
    /// `minorFont.cs` / `majorFont.cs` 作为回退。
    pub fn clear_complex_script_name(&mut self) {
        self.rpr.cs_font = None;
    }

    /// baseline 偏移(百分比,正=上标,负=下标)。
    pub fn baseline(&self) -> Option<i32> {
        self.rpr.baseline
    }
    /// 设置 baseline。
    pub fn set_baseline(&mut self, v: i32) {
        self.rpr.baseline = Some(v);
    }

    /// 字符间距(百分之一磅)。
    pub fn spacing(&self) -> Option<i32> {
        self.rpr.spc
    }
    /// 设置字符间距。
    pub fn set_spacing(&mut self, v: i32) {
        self.rpr.spc = Some(v);
    }

    // ===== 超链接 API(TODO-026)=====

    /// 取点击超链接(`<a:hlinkClick>`)。
    pub fn hlink_click(&self) -> Option<&Hyperlink> {
        self.rpr.hlink_click.as_ref()
    }
    /// 设置点击超链接(直接传入 [`Hyperlink`])。
    pub fn set_hlink_click(&mut self, hl: Hyperlink) {
        self.rpr.hlink_click = Some(hl);
    }
    /// 清除点击超链接。
    pub fn clear_hlink_click(&mut self) {
        self.rpr.hlink_click = None;
    }

    /// 取悬停超链接(`<a:hlinkHover>`)。
    pub fn hlink_hover(&self) -> Option<&Hyperlink> {
        self.rpr.hlink_hover.as_ref()
    }
    /// 设置悬停超链接。
    pub fn set_hlink_hover(&mut self, hl: Hyperlink) {
        self.rpr.hlink_hover = Some(hl);
    }
    /// 清除悬停超链接。
    pub fn clear_hlink_hover(&mut self) {
        self.rpr.hlink_hover = None;
    }

    /// 便捷方法:设置一个指向 URL 的点击超链接。
    ///
    /// # 参数
    /// - `rid`:关系 ID(指向 `.rels` 中的目标 URL);
    /// - `tooltip`:可选的鼠标悬停提示。
    ///
    /// # 示例
    /// ```no_run
    /// # use ooxml_core::oxml::txbody::{Run, Font};
    /// # let mut run = Run::new("链接文本");
    /// run.font().set_hyperlink("rId1", Some("点击访问"));
    /// ```
    pub fn set_hyperlink(&mut self, rid: impl Into<String>, tooltip: Option<&str>) {
        let mut hl = Hyperlink::new(rid);
        if let Some(t) = tooltip {
            hl.tooltip = Some(t.to_string());
        }
        self.rpr.hlink_click = Some(hl);
    }

    /// 便捷方法:设置一个跳转幻灯片的动作超链接。
    ///
    /// 对应 OOXML `action="ppaction://hlinksldjump"`。
    pub fn set_slide_jump(&mut self) {
        self.rpr.hlink_click = Some(Hyperlink::new_slide_jump());
    }
}

impl<'a> From<&'a mut RunProperties> for Font<'a> {
    fn from(r: &'a mut RunProperties) -> Self {
        Font::new(r)
    }
}

// ====================================================================
// 高阶 ParagraphFormat 视图(python-pptx 风格 `pptx.text.text._ParagraphFormat`)
// ====================================================================

/// 段落格式高阶视图(`pptx.text.text._ParagraphFormat`)。
///
/// # 与 python-pptx 的对应
///
/// - `paragraph.alignment = PP_ALIGN.CENTER` ←→ [`ParagraphFormat::set_alignment`];
/// - `paragraph.line_spacing = 1.5` ←→ [`ParagraphFormat::set_line_spacing`];
/// - `paragraph.space_before = Pt(6)` ←→ [`ParagraphFormat::set_space_before`]。
///
/// # 设计要点
///
/// - **借用 + 透明代理**:构造时传入 `&mut ParagraphProperties`;
/// - **零分配**:纯字段操作;
/// - **互斥语义**:`set_line_spacing` / `set_line_spacing_pct` 互斥;
///   后调用的会清空前者。
#[derive(Debug)]
pub struct ParagraphFormat<'a> {
    /// 底层 [`ParagraphProperties`] 引用。
    ppr: &'a mut ParagraphProperties,
}

impl<'a> ParagraphFormat<'a> {
    /// 构造。
    pub fn new(ppr: &'a mut ParagraphProperties) -> Self {
        ParagraphFormat { ppr }
    }
    /// 底层 ppr 不可变引用。
    pub fn ppr(&self) -> &ParagraphProperties {
        self.ppr
    }
    /// 底层 ppr 可变引用。
    pub fn ppr_mut(&mut self) -> &mut ParagraphProperties {
        self.ppr
    }

    /// 水平对齐。
    pub fn alignment(&self) -> Option<Alignment> {
        self.ppr.alignment
    }
    /// 设置水平对齐。
    pub fn set_alignment(&mut self, v: Alignment) {
        self.ppr.alignment = Some(v);
    }
    /// 清除水平对齐(走默认)。
    pub fn clear_alignment(&mut self) {
        self.ppr.alignment = None;
    }

    /// 段落级别(0-8)。
    pub fn level(&self) -> u8 {
        self.ppr.level
    }
    /// 设置段落级别。
    pub fn set_level(&mut self, lvl: u8) {
        self.ppr.level = lvl;
    }

    /// 行距(固定值,Pt)。
    pub fn line_spacing(&self) -> Option<Pt> {
        self.ppr.line_spacing.map(|emu| Pt(emu as f64 / 12_700.0))
    }
    /// 设置行距为**固定点数**(与 `set_line_spacing_pct` 互斥)。
    pub fn set_line_spacing(&mut self, v: Pt) {
        let emu = (v.value() * 12_700.0) as i32;
        self.ppr.line_spacing = Some(emu);
        self.ppr.line_spacing_pct = None;
    }
    /// 行距(倍数,1.0 = 100%)。
    pub fn line_spacing_pct(&self) -> Option<f32> {
        self.ppr.line_spacing_pct.map(|v| v as f32 / 1000.0)
    }
    /// 设置行距为**倍数**(与 `set_line_spacing` 互斥)。
    pub fn set_line_spacing_pct(&mut self, v: f32) {
        self.ppr.line_spacing_pct = Some((v * 1000.0) as i32);
        self.ppr.line_spacing = None;
    }
    /// 清除行距。
    pub fn clear_line_spacing(&mut self) {
        self.ppr.line_spacing = None;
        self.ppr.line_spacing_pct = None;
    }

    /// 段前(EMU)。
    pub fn space_before(&self) -> Option<Emu> {
        self.ppr.space_before
    }
    /// 设置段前。
    pub fn set_space_before(&mut self, emu: Emu) {
        self.ppr.space_before = Some(emu);
    }
    /// 段后(EMU)。
    pub fn space_after(&self) -> Option<Emu> {
        self.ppr.space_after
    }
    /// 设置段后。
    pub fn set_space_after(&mut self, emu: Emu) {
        self.ppr.space_after = Some(emu);
    }

    /// 一次性设置缩进。
    pub fn set_indent(
        &mut self,
        left: Option<Emu>,
        right: Option<Emu>,
        first_line: Option<Emu>,
        hanging: Option<i32>,
    ) {
        self.ppr.indent = Indent {
            left,
            right,
            first_line,
            hanging,
        };
    }

    /// 缩进(EMU 视图)。
    pub fn indent(&self) -> Indent {
        self.ppr.indent
    }

    // --------- 项目符号样式(TODO-014) ---------

    /// 项目符号样式。
    ///
    /// 返回 `None` 表示未设置(走默认继承);`Some(BulletStyle::None)` 表示显式无项目符号。
    pub fn bullet_style(&self) -> Option<&BulletStyle> {
        self.ppr.bullet_style.as_ref()
    }

    /// 设置自定义字符项目符号(`<a:buChar char="..."/>`)。
    ///
    /// 对标 python-pptx `paragraph.font` + bullet 字符设置。
    /// 常用字符:`"•"` / `"▪"` / `"→"` / `"○"` / `"♦"`。
    pub fn set_bullet_char(&mut self, ch: impl Into<String>) {
        self.ppr.bullet = true;
        self.ppr.bullet_style = Some(BulletStyle::Char { char: ch.into() });
    }

    /// 设置自动编号项目符号(`<a:buAutoNum type="..." startAt="..."/>`)。
    ///
    /// 对标 python-pptx 编号列表。
    /// 常用 type:`"arabicPeriod"` (1.) / `"alphaLcParenR"` (a)) / `"romanLcParenBoth"` ((i))。
    ///
    /// # 参数
    /// - `auto_num_type`:编号类型字面量。
    /// - `start_at`:起始编号(可选,`None` 表示从 1 开始)。
    pub fn set_bullet_numbered(&mut self, auto_num_type: impl Into<String>, start_at: Option<u32>) {
        self.ppr.bullet = true;
        self.ppr.bullet_style = Some(BulletStyle::AutoNum {
            auto_num_type: auto_num_type.into(),
            start_at,
        });
    }

    /// 清除项目符号(`<a:buNone/>`)。
    pub fn clear_bullet(&mut self) {
        self.ppr.bullet = false;
        self.ppr.bullet_style = Some(BulletStyle::None);
    }

    /// 是否有项目符号(`bullet=true` 且 `bullet_style` 非 `None`)。
    pub fn has_bullet(&self) -> bool {
        self.ppr.bullet
            && self
                .ppr
                .bullet_style
                .as_ref()
                .map(|bs| !matches!(bs, BulletStyle::None))
                .unwrap_or(false)
    }

    // --------- 制表位(TODO-015) ---------

    /// 制表位列表(不可变)。
    ///
    /// 对标 python-pptx `_ParagraphFormat.tab_stops`。
    pub fn tab_stops(&self) -> &[TabStop] {
        &self.ppr.tab_stops
    }

    /// 添加一个制表位(`<a:tab pos="..." algn="..."/>`)。
    ///
    /// 对标 python-pptx `paragraph.tab_stops.add_tab_stop(position, alignment)`。
    ///
    /// # 参数
    /// - `pos`:制表位位置(EMU);
    /// - `alignment`:对齐类型(左/居中/右/小数点)。
    pub fn add_tab_stop(&mut self, pos: Emu, alignment: TabAlignment) {
        self.ppr.tab_stops.push(TabStop { pos, alignment });
    }

    /// 清除所有制表位。
    pub fn clear_tab_stops(&mut self) {
        self.ppr.tab_stops.clear();
    }
}

impl<'a> From<&'a mut ParagraphProperties> for ParagraphFormat<'a> {
    fn from(p: &'a mut ParagraphProperties) -> Self {
        ParagraphFormat::new(p)
    }
}

// ====================================================================
// 高阶 TextFrame 视图(python-pptx 风格 `pptx.text.textframe.TextFrame`)
// ====================================================================

/// 文本框高阶视图(`pptx.text.textframe.TextFrame`)。
///
/// # 与 python-pptx 的对应
///
/// - `shape.text_frame` ←→ `text_frame_mut().as_text_frame()`(薄包装);
/// - `text_frame.text` ←→ [`TextFrame::text_getter`] / [`TextFrame::set_text`];
/// - `text_frame.paragraphs[0]` ←→ [`TextFrame::paragraphs`];
/// - `text_frame.add_paragraph()` ←→ [`TextFrame::add_paragraph`];
/// - `text_frame.word_wrap` ←→ [`TextFrame::word_wrap`] / [`TextFrame::set_word_wrap`];
/// - `text_frame.auto_size` ←→ [`TextFrame::auto_size`] / [`TextFrame::set_auto_size`];
/// - `text_frame.vertical_anchor` ←→ [`TextFrame::vertical_anchor`] / [`TextFrame::set_vertical_anchor`];
/// - `text_frame.margin_left/...` ←→ [`TextFrame::margin_left`] / ... / [`TextFrame::set_margins`]。
///
/// # 设计要点
///
/// - **借用**:构造时传入 `&mut TextBody`;
/// - **零分配**:所有方法均不触发堆分配;
/// - **互斥语义**:[`TextFrame::add_paragraph`] 不会自动 trim,已有段落不会被移除。
#[derive(Debug)]
pub struct TextFrame<'a> {
    /// 底层 [`TextBody`] 引用。
    body: &'a mut TextBody,
}

impl<'a> TextFrame<'a> {
    /// 构造。
    pub fn new(body: &'a mut TextBody) -> Self {
        TextFrame { body }
    }

    /// 底层 body 不可变引用。
    pub fn body(&self) -> &TextBody {
        self.body
    }
    /// 底层 body 可变引用。
    pub fn body_mut(&mut self) -> &mut TextBody {
        self.body
    }

    // --------- 段落集合 ---------

    /// 段落数。
    pub fn len(&self) -> usize {
        self.body.paragraphs.len()
    }
    /// 是否为空。
    pub fn is_empty(&self) -> bool {
        self.body.paragraphs.is_empty()
    }
    /// 不可变段落迭代器。
    pub fn paragraphs(&self) -> std::slice::Iter<'_, Paragraph> {
        self.body.paragraphs.iter()
    }
    /// 可变段落迭代器。
    pub fn paragraphs_mut(&mut self) -> std::slice::IterMut<'_, Paragraph> {
        self.body.paragraphs.iter_mut()
    }
    /// 按下标取不可变段落。
    pub fn paragraph(&self, idx: usize) -> Option<&Paragraph> {
        self.body.paragraphs.get(idx)
    }
    /// 按下标取可变段落。
    pub fn paragraph_mut(&mut self, idx: usize) -> Option<&mut Paragraph> {
        self.body.paragraphs.get_mut(idx)
    }
    /// 首个段落。
    pub fn first_paragraph(&self) -> Option<&Paragraph> {
        self.body.paragraphs.first()
    }
    /// 首个段落的可变引用。
    pub fn first_paragraph_mut(&mut self) -> Option<&mut Paragraph> {
        self.body.paragraphs.first_mut()
    }

    /// 新增空段落(python-pptx `text_frame.add_paragraph()`)。
    pub fn add_paragraph(&mut self) -> &mut Paragraph {
        self.body.add_paragraph()
    }

    /// 新增带文本段落(按 `\n` 切分为多段),返回**最后**一段的可变引用。
    pub fn add_paragraph_with_text(&mut self, text: &str) -> &mut Paragraph {
        self.body.add_paragraph_with_text(text)
    }

    /// 按下标移除段落。
    pub fn remove_paragraph(&mut self, idx: usize) -> Option<Paragraph> {
        self.body.remove_paragraph(idx)
    }

    /// 清空段落(保留一个空段,对应 python-pptx `text_frame.clear()`)。
    pub fn clear(&mut self) {
        self.body.clear();
    }

    // --------- 文本便捷 ---------

    /// 整体取文本(段间 `\n`,与 python-pptx `text_frame.text` 一致)。
    pub fn text_getter(&self) -> String {
        self.body.text()
    }

    /// 整体替换文本(按 `\n` 切分;旧段落属性会被丢弃)。
    pub fn set_text(&mut self, text: &str) {
        self.body.set_text(text);
    }

    // --------- BodyProperties 便捷 ---------

    /// 自动调整策略。
    pub fn auto_size(&self) -> MsoAutoSize {
        self.body.auto_size()
    }
    /// 设置自动调整策略。
    pub fn set_auto_size(&mut self, v: MsoAutoSize) {
        self.body.set_auto_size(v);
    }

    /// 垂直对齐。
    pub fn vertical_anchor(&self) -> Option<MsoAnchor> {
        self.body.vertical_anchor()
    }
    /// 设置垂直对齐。
    pub fn set_vertical_anchor(&mut self, v: MsoAnchor) {
        self.body.set_vertical_anchor(v);
    }

    /// 是否自动换行(None = 走默认)。
    pub fn word_wrap(&self) -> Option<bool> {
        self.body.word_wrap()
    }
    /// 设置是否自动换行。
    pub fn set_word_wrap(&mut self, v: bool) {
        self.body.set_word_wrap(v);
    }

    /// 左边距。
    pub fn margin_left(&self) -> Option<Emu> {
        self.body.margin_left()
    }
    /// 右边距。
    pub fn margin_right(&self) -> Option<Emu> {
        self.body.margin_right()
    }
    /// 上边距。
    pub fn margin_top(&self) -> Option<Emu> {
        self.body.margin_top()
    }
    /// 下边距。
    pub fn margin_bottom(&self) -> Option<Emu> {
        self.body.margin_bottom()
    }
    /// 一次性设置四向边距。
    pub fn set_margins(&mut self, l: Emu, t: Emu, r: Emu, b: Emu) {
        self.body.set_margins(l, t, r, b);
    }
    /// 设置单边边距(left)。
    pub fn set_margin_left(&mut self, emu: Emu) {
        self.body.set_margin_left(emu);
    }
    /// 设置单边边距(right)。
    pub fn set_margin_right(&mut self, emu: Emu) {
        self.body.set_margin_right(emu);
    }
    /// 设置单边边距(top)。
    pub fn set_margin_top(&mut self, emu: Emu) {
        self.body.set_margin_top(emu);
    }
    /// 设置单边边距(bottom)。
    pub fn set_margin_bottom(&mut self, emu: Emu) {
        self.body.set_margin_bottom(emu);
    }

    // --------- 多列布局 ---------

    /// 文本列数(`numCol` 属性)。
    ///
    /// `None` 表示未设置(默认单列);`Some(1)` 等同于单列。
    pub fn num_cols(&self) -> Option<u32> {
        self.body.num_cols()
    }

    /// 设置文本列数(`numCol` 属性)。
    pub fn set_num_cols(&mut self, count: u32) {
        self.body.set_num_cols(count);
    }

    /// 列间距(`spcCol` 属性,EMU)。
    pub fn col_spacing(&self) -> Option<Emu> {
        self.body.col_spacing()
    }

    /// 设置列间距(`spcCol` 属性,EMU)。
    pub fn set_col_spacing(&mut self, emu: Emu) {
        self.body.set_col_spacing(emu);
    }

    /// 一次性设置多列布局(列数 + 可选列间距)。
    ///
    /// 对标 python-pptx 中 `text_frame` 的多列设置。
    /// 当 `spacing` 为 `None` 时仅设置列数,保留已有列间距。
    pub fn set_columns(&mut self, count: u32, spacing: Option<Emu>) {
        self.body.set_num_cols(count);
        if let Some(s) = spacing {
            self.body.set_col_spacing(s);
        }
    }
}

impl<'a> From<&'a mut TextBody> for TextFrame<'a> {
    fn from(b: &'a mut TextBody) -> Self {
        TextFrame::new(b)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn write_paragraph_simple() {
        let mut p = Paragraph::new();
        let mut r = Run::new("Hello");
        r.properties.size = Some(Pt(24.0));
        r.properties.bold = true;
        r.properties.color = RGBColor(0xFF, 0, 0).into();
        p.runs.push(r);
        let mut w = super::super::writer::XmlWriter::new();
        p.write_xml(&mut w);
        let s = w.into_string();
        assert!(s.contains("Hello"));
        assert!(s.contains("sz=\"2400\""));
        assert!(s.contains("b=\"1\""));
        assert!(s.contains("a:srgbClr"));
    }

    /// 验证 `TextFrame` / `ParagraphFormat` 视图能联动到底层 `TextBody`。
    #[test]
    fn textframe_view_mirrors_body() {
        let mut tb = TextBody::new();
        {
            let mut tf = TextFrame::new(&mut tb);
            // 走 view 加一段 + 走 view 设 word_wrap
            let p = tf.add_paragraph();
            p.add_run_with_text("first").set_bold(true);
            tf.add_paragraph_with_text("second\nthird");
            tf.set_word_wrap(false);
            tf.set_margins(Emu(91440), Emu(45720), Emu(91440), Emu(45720));
        }
        // 验证 view 写入已生效
        assert_eq!(tb.paragraphs.len(), 3);
        assert_eq!(tb.paragraphs[0].runs[0].text, "first");
        assert!(tb.paragraphs[0].runs[0].properties.bold);
        assert_eq!(tb.paragraphs[1].runs[0].text, "second");
        assert_eq!(tb.paragraphs[2].runs[0].text, "third");
        // 自动换行被设成 false
        assert_eq!(tb.word_wrap(), Some(false));
    }

    /// 验证 `ParagraphFormat` 的 line_spacing 互斥语义。
    #[test]
    fn paragraph_format_line_spacing_mutex() {
        let mut p = Paragraph::new();
        p.set_line_spacing(Pt(20.0));
        assert!(p.line_spacing().is_some());
        assert!(p.line_spacing_pct().is_none());
        // 改设倍数:清空固定值
        p.set_line_spacing_pct(1.5);
        assert!(p.line_spacing().is_none());
        assert_eq!(p.line_spacing_pct(), Some(1.5));
        // 走 view 验证同样互斥
        let mut p2 = Paragraph::new();
        {
            let mut pf = ParagraphFormat::new(&mut p2.properties);
            pf.set_line_spacing(Pt(15.0));
        }
        assert_eq!(p2.line_spacing(), Some(Pt(15.0)));
        {
            let mut pf = ParagraphFormat::new(&mut p2.properties);
            pf.set_line_spacing_pct(2.0);
        }
        assert!(p2.line_spacing().is_none());
        assert_eq!(p2.line_spacing_pct(), Some(2.0));
    }

    /// 验证 `Font` 的删除线/双删除线 API。
    ///
    /// 这是 TODO-017 的测试。
    #[test]
    fn font_strikethrough_api() {
        let mut r = Run::new("text");
        {
            let mut f = Font::new(&mut r.properties);
            f.set_strike(true);
        }
        assert!(r.properties.strike);
        assert!(!r.properties.strike_dbl);

        // 双删除线
        {
            let mut f = Font::new(&mut r.properties);
            f.set_double_strike(true);
        }
        assert!(r.properties.strike_dbl);

        // 验证 Font 读取
        let f = Font::new(&mut r.properties);
        assert!(f.strike());
        assert!(f.double_strike());
    }

    /// 验证 `Font` 的高亮色 API。
    ///
    /// 这是 TODO-018 的测试。
    #[test]
    fn font_highlight_api() {
        let mut r = Run::new("text");
        // 默认无高亮
        assert!(r.properties.highlight.is_none());

        // 设置高亮
        {
            let mut f = Font::new(&mut r.properties);
            f.set_highlight(Some(Color::RGB(RGBColor(0xFF, 0xFF, 0x00))));
        }
        assert!(r.properties.highlight.is_some());

        // 验证 Font 读取
        let f = Font::new(&mut r.properties);
        let hl = f.highlight().expect("应有高亮色");
        assert!(matches!(hl, Color::RGB(c) if c.0 == 0xFF && c.1 == 0xFF && c.2 == 0x00));

        // 清除高亮
        {
            let mut f = Font::new(&mut r.properties);
            f.set_highlight(None);
        }
        assert!(r.properties.highlight.is_none());

        // 用 Color::None 清除
        {
            let mut f = Font::new(&mut r.properties);
            f.set_highlight(Some(Color::RGB(RGBColor(0xFF, 0x00, 0x00))));
            f.set_highlight(Some(Color::None));
        }
        assert!(r.properties.highlight.is_none());
    }

    /// 验证 `TextBody` / `TextFrame` 的多列布局 API 与序列化。
    ///
    /// 这是 TODO-019 的测试。
    #[test]
    fn text_body_multi_column_api() {
        let mut tb = TextBody::new();
        // 默认无列数
        assert!(tb.num_cols().is_none());
        assert!(tb.col_spacing().is_none());

        // 设置 3 列 + 列间距
        tb.set_num_cols(3);
        tb.set_col_spacing(Emu(91440));
        assert_eq!(tb.num_cols(), Some(3));
        assert_eq!(tb.col_spacing(), Some(Emu(91440)));

        // 序列化验证
        let mut w = super::super::writer::XmlWriter::new();
        tb.body_properties
            .as_ref()
            .expect("应有 body_properties")
            .write_xml(&mut w);
        let s = w.into_string();
        assert!(s.contains("numCol=\"3\""), "应输出 numCol=\"3\",实际: {s}");
        assert!(
            s.contains("spcCol=\"91440\""),
            "应输出 spcCol=\"91440\",实际: {s}"
        );
    }

    /// 验证 `TextFrame::set_columns` 一次性设置列数和列间距。
    ///
    /// 这是 TODO-019 的测试。
    #[test]
    fn text_frame_set_columns() {
        let mut tb = TextBody::new();
        {
            let mut tf = TextFrame::new(&mut tb);
            // 设置 2 列 + 列间距
            tf.set_columns(2, Some(Emu(45720)));
        }
        assert_eq!(tb.num_cols(), Some(2));
        assert_eq!(tb.col_spacing(), Some(Emu(45720)));

        // 仅设置列数,不设置列间距
        {
            let mut tf = TextFrame::new(&mut tb);
            tf.set_columns(4, None);
        }
        assert_eq!(tb.num_cols(), Some(4));
        // 列间距应保持上次的值
        assert_eq!(tb.col_spacing(), Some(Emu(45720)));

        // TextFrame 代理方法
        {
            let tf = TextFrame::new(&mut tb);
            assert_eq!(tf.num_cols(), Some(4));
            assert_eq!(tf.col_spacing(), Some(Emu(45720)));
        }
    }

    /// 验证 `ParagraphFormat` 的项目符号字符 API。
    ///
    /// 这是 TODO-014 的测试。
    #[test]
    fn paragraph_format_bullet_char() {
        let mut ppr = ParagraphProperties::default();
        {
            let mut pf = ParagraphFormat::new(&mut ppr);
            pf.set_bullet_char("");
        }
        assert!(ppr.bullet, "bullet 应为 true");
        assert!(pf_has_bullet(&ppr), "has_bullet 应为 true");
        // 验证序列化
        let mut w = super::super::writer::XmlWriter::new();
        ppr.write_xml(&mut w);
        let s = w.into_string();
        assert!(s.contains("buChar"), "应输出 buChar,实际: {s}");
        assert!(s.contains("char=\"\""), "应包含 char=\"\",实际: {s}");
    }

    /// 验证 `ParagraphFormat` 的编号项目符号 API。
    ///
    /// 这是 TODO-014 的测试。
    #[test]
    fn paragraph_format_bullet_numbered() {
        let mut ppr = ParagraphProperties::default();
        {
            let mut pf = ParagraphFormat::new(&mut ppr);
            pf.set_bullet_numbered("arabicPeriod", Some(3));
        }
        assert!(ppr.bullet);
        match &ppr.bullet_style {
            Some(BulletStyle::AutoNum {
                auto_num_type,
                start_at,
            }) => {
                assert_eq!(auto_num_type, "arabicPeriod");
                assert_eq!(*start_at, Some(3));
            }
            other => panic!("期望 AutoNum,实际: {other:?}"),
        }
        // 验证序列化
        let mut w = super::super::writer::XmlWriter::new();
        ppr.write_xml(&mut w);
        let s = w.into_string();
        assert!(s.contains("buAutoNum"), "应输出 buAutoNum,实际: {s}");
        assert!(
            s.contains("type=\"arabicPeriod\""),
            "应包含 type,实际: {s}"
        );
        assert!(s.contains("startAt=\"3\""), "应包含 startAt,实际: {s}");
    }

    /// 验证 `ParagraphFormat::clear_bullet` 清除项目符号。
    ///
    /// 这是 TODO-014 的测试。
    #[test]
    fn paragraph_format_clear_bullet() {
        let mut ppr = ParagraphProperties::default();
        {
            let mut pf = ParagraphFormat::new(&mut ppr);
            pf.set_bullet_char("");
        }
        assert!(pf_has_bullet(&ppr));
        {
            let mut pf = ParagraphFormat::new(&mut ppr);
            pf.clear_bullet();
        }
        assert!(!ppr.bullet, "bullet 应为 false");
        assert!(!pf_has_bullet(&ppr), "has_bullet 应为 false");
        // 验证序列化输出 buNone
        let mut w = super::super::writer::XmlWriter::new();
        ppr.write_xml(&mut w);
        let s = w.into_string();
        assert!(s.contains("buNone"), "应输出 buNone,实际: {s}");
    }

    /// 辅助函数:检查 ParagraphProperties 是否有项目符号。
    fn pf_has_bullet(ppr: &ParagraphProperties) -> bool {
        ppr.bullet
            && ppr
                .bullet_style
                .as_ref()
                .map(|bs| !matches!(bs, BulletStyle::None))
                .unwrap_or(false)
    }

    /// 验证 `ParagraphFormat` 的制表位 API。
    ///
    /// 这是 TODO-015 的测试。
    #[test]
    fn paragraph_format_tab_stops() {
        let mut ppr = ParagraphProperties::default();
        {
            let mut pf = ParagraphFormat::new(&mut ppr);
            pf.add_tab_stop(Emu(914400), TabAlignment::Left);
            pf.add_tab_stop(Emu(1828800), TabAlignment::Right);
            pf.add_tab_stop(Emu(2743200), TabAlignment::Center);
        }
        assert_eq!(ppr.tab_stops.len(), 3);
        assert_eq!(ppr.tab_stops[0].pos.value(), 914400);
        assert_eq!(ppr.tab_stops[0].alignment, TabAlignment::Left);
        assert_eq!(ppr.tab_stops[1].pos.value(), 1828800);
        assert_eq!(ppr.tab_stops[1].alignment, TabAlignment::Right);
        assert_eq!(ppr.tab_stops[2].pos.value(), 2743200);
        assert_eq!(ppr.tab_stops[2].alignment, TabAlignment::Center);

        // 验证序列化
        let mut w = super::super::writer::XmlWriter::new();
        ppr.write_xml(&mut w);
        let s = w.into_string();
        assert!(s.contains("tabLst"), "应输出 tabLst,实际: {s}");
        assert!(s.contains("pos=\"914400\""), "应包含 pos=914400,实际: {s}");
        assert!(s.contains("algn=\"l\""), "应包含 algn=l,实际: {s}");
        assert!(s.contains("algn=\"r\""), "应包含 algn=r,实际: {s}");
        assert!(s.contains("algn=\"ctr\""), "应包含 algn=ctr,实际: {s}");

        // 清除
        {
            let mut pf = ParagraphFormat::new(&mut ppr);
            pf.clear_tab_stops();
        }
        assert!(ppr.tab_stops.is_empty());
    }

    /// 验证 `Field` 的序列化和 `Paragraph::add_field` API。
    ///
    /// 这是 TODO-016 的测试。
    #[test]
    fn paragraph_field_serialization() {
        let mut p = Paragraph::new();
        p.add_field(FieldType::SlideNumber, "1");
        p.add_field(FieldType::DateTime, "1/1/2024");

        assert_eq!(p.fields.len(), 2);
        assert_eq!(p.fields[0].field_type, FieldType::SlideNumber);
        assert_eq!(p.fields[0].text, "1");
        assert_eq!(p.fields[1].field_type, FieldType::DateTime);
        assert_eq!(p.fields[1].text, "1/1/2024");

        // 验证序列化
        let mut w = super::super::writer::XmlWriter::new();
        p.write_xml(&mut w);
        let s = w.into_string();
        assert!(s.contains("a:fld"), "应输出 a:fld,实际: {s}");
        assert!(
            s.contains("type=\"slidenum\""),
            "应包含 type=slidenum,实际: {s}"
        );
        assert!(
            s.contains("type=\"datetime\""),
            "应包含 type=datetime,实际: {s}"
        );
        assert!(s.contains(">1<"), "应包含文本 1,实际: {s}");
        assert!(s.contains(">1/1/2024<"), "应包含文本 1/1/2024,实际: {s}");
    }

    /// 验证 `FieldType` 的转换方法。
    ///
    /// 这是 TODO-016 的测试。
    #[test]
    fn field_type_conversion() {
        // as_str
        assert_eq!(FieldType::SlideNumber.as_str(), "slidenum");
        assert_eq!(FieldType::DateTime.as_str(), "datetime");
        assert_eq!(FieldType::DateTime1.as_str(), "datetime1");
        assert_eq!(FieldType::Footer.as_str(), "footer");
        assert_eq!(FieldType::Custom("custom1".to_string()).as_str(), "custom1");

        // from_str_value
        assert_eq!(
            FieldType::from_str_value("slidenum"),
            FieldType::SlideNumber
        );
        assert_eq!(FieldType::from_str_value("datetime"), FieldType::DateTime);
        assert_eq!(FieldType::from_str_value("datetime1"), FieldType::DateTime1);
        assert_eq!(FieldType::from_str_value("footer"), FieldType::Footer);
        assert_eq!(
            FieldType::from_str_value("unknown"),
            FieldType::Custom("unknown".to_string())
        );
    }

    /// 验证 `Font` 超链接高阶 API(set_hyperlink / set_slide_jump)。
    ///
    /// 这是 TODO-026 的测试。
    #[test]
    fn font_hyperlink_api() {
        let mut run = Run::new("链接文本");
        // 设置 URL 超链接
        run.font().set_hyperlink("rId3", Some("点击访问"));
        let hl = run
            .properties
            .hlink_click
            .as_ref()
            .expect("hlink_click 应存在");
        assert_eq!(hl.rid.as_deref(), Some("rId3"));
        assert_eq!(hl.tooltip.as_deref(), Some("点击访问"));

        // 设置跳转幻灯片动作
        run.font().set_slide_jump();
        let hl = run
            .properties
            .hlink_click
            .as_ref()
            .expect("hlink_click 应存在");
        assert_eq!(hl.action.as_deref(), Some("ppaction://hlinksldjump"));

        // 清除
        run.font().clear_hlink_click();
        assert!(run.properties.hlink_click.is_none());
    }

    /// 验证超链接的序列化 round-trip。
    ///
    /// 这是 TODO-026 的测试。
    #[test]
    fn hyperlink_serialization_roundtrip() {
        let mut run = Run::new("点击这里");
        run.font().set_hyperlink("rId7", Some("提示文字"));

        // 序列化
        let mut w = super::super::writer::XmlWriter::new();
        run.write_xml(&mut w);
        let s = w.into_string();
        assert!(s.contains("a:hlinkClick"), "应输出 a:hlinkClick,实际: {s}");
        assert!(s.contains("r:id=\"rId7\""), "应包含 r:id=rId7,实际: {s}");
        assert!(
            s.contains("tooltip=\"提示文字\""),
            "应包含 tooltip,实际: {s}"
        );
    }

    // --------------------- TODO-047: endParaRPr 测试 ---------------------

    /// 验证 `Paragraph::set_end_para_rpr` 高阶 API 与序列化。
    ///
    /// 设置带属性 + 子元素的 endParaRPr,验证序列化输出正确。
    #[test]
    fn end_para_rpr_set_and_serialize() {
        let mut p = Paragraph::new();
        let rpr = RunProperties {
            size: Some(Pt(24.0)),
            latin_font: Some("Calibri".to_string()),
            lang: Some("en-US".to_string()),
            ..Default::default()
        };
        p.set_end_para_rpr(rpr);

        // 验证 getter
        assert!(p.end_para_rpr().is_some());
        let got = p.end_para_rpr().unwrap();
        assert_eq!(got.size, Some(Pt(24.0)));
        assert_eq!(got.latin_font.as_deref(), Some("Calibri"));
        assert_eq!(got.lang.as_deref(), Some("en-US"));

        // 序列化
        let mut w = super::super::writer::XmlWriter::new();
        p.write_xml(&mut w);
        let s = w.into_string();
        assert!(s.contains("a:endParaRPr"), "应输出 a:endParaRPr,实际: {s}");
        assert!(s.contains("sz=\"2400\""), "应包含 sz=2400,实际: {s}");
        assert!(s.contains("lang=\"en-US\""), "应包含 lang=en-US,实际: {s}");
        assert!(s.contains("a:latin"), "应包含 a:latin,实际: {s}");
        assert!(
            s.contains("typeface=\"Calibri\""),
            "应包含 typeface=Calibri,实际: {s}"
        );
    }

    /// 验证 `Paragraph::clear_end_para_rpr` 清除 endParaRPr。
    #[test]
    fn end_para_rpr_clear() {
        let mut p = Paragraph::new();
        let rpr = RunProperties {
            size: Some(Pt(18.0)),
            ..Default::default()
        };
        p.set_end_para_rpr(rpr);
        assert!(p.end_para_rpr().is_some());

        p.clear_end_para_rpr();
        assert!(p.end_para_rpr().is_none());

        // 序列化不应包含 endParaRPr
        let mut w = super::super::writer::XmlWriter::new();
        p.write_xml(&mut w);
        let s = w.into_string();
        assert!(
            !s.contains("a:endParaRPr"),
            "不应输出 a:endParaRPr,实际: {s}"
        );
    }

    /// 验证 `Paragraph::end_para_rpr_mut` 可变引用修改。
    #[test]
    fn end_para_rpr_mut_modify() {
        let mut p = Paragraph::new();
        let rpr = RunProperties::default();
        p.set_end_para_rpr(rpr);

        // 通过可变引用修改
        if let Some(rpr) = p.end_para_rpr_mut() {
            rpr.size = Some(Pt(32.0));
            rpr.bold = true;
        }

        let got = p.end_para_rpr().expect("end_para_rpr 应存在");
        assert_eq!(got.size, Some(Pt(32.0)));
        assert!(got.bold);
    }

    // 注:end_para_rpr_roundtrip_with_children / end_para_rpr_self_closing_parse /
    // end_para_rpr_with_solid_fill 三个 round-trip 测试依赖 PPTX 特有的
    // parse_sld::parse_paragraph,保留在 pptx-rs 主 crate 的 txbody.rs 中。

    // ===================== 东亚字体便捷 API 测试(TODO-005 剩余) =====================

    /// `Run::eastasia_name` / `set_eastasia_name` 往返:设置后能正确读取。
    #[test]
    fn run_eastasia_name_setter_and_getter() {
        let mut r = Run::new("你好");
        assert!(r.eastasia_name().is_none());
        r.set_eastasia_name("宋体");
        assert_eq!(r.eastasia_name(), Some("宋体"));
        assert_eq!(r.properties.eastasia_font.as_deref(), Some("宋体"));
    }

    /// `Run::complex_script_name` / `set_complex_script_name` 往返。
    #[test]
    fn run_complex_script_name_setter_and_getter() {
        let mut r = Run::new("مرحبا");
        assert!(r.complex_script_name().is_none());
        r.set_complex_script_name("Traditional Arabic");
        assert_eq!(r.complex_script_name(), Some("Traditional Arabic"));
        assert_eq!(r.properties.cs_font.as_deref(), Some("Traditional Arabic"));
    }

    /// `Run` 上 latin / ea / cs 三种字体可独立设置,互不干扰。
    #[test]
    fn run_three_fonts_independent() {
        let mut r = Run::new("Hello 你好 مرحبا");
        r.set_font_name("Arial");
        r.set_eastasia_name("Microsoft YaHei");
        r.set_complex_script_name("Tahoma");
        assert_eq!(r.font_name(), Some("Arial"));
        assert_eq!(r.eastasia_name(), Some("Microsoft YaHei"));
        assert_eq!(r.complex_script_name(), Some("Tahoma"));
        // 验证底层 properties 字段独立
        assert_eq!(r.properties.latin_font.as_deref(), Some("Arial"));
        assert_eq!(
            r.properties.eastasia_font.as_deref(),
            Some("Microsoft YaHei")
        );
        assert_eq!(r.properties.cs_font.as_deref(), Some("Tahoma"));
    }

    /// `Font::clear_eastasia_name` 清空东亚字体字段。
    #[test]
    fn font_clear_eastasia_name() {
        let mut r = Run::new("文本");
        r.set_eastasia_name("宋体");
        assert_eq!(r.eastasia_name(), Some("宋体"));
        // 通过 Font 视图清空
        {
            let mut f = Font::new(&mut r.properties);
            f.clear_eastasia_name();
        }
        assert!(r.eastasia_name().is_none());
        assert!(r.properties.eastasia_font.is_none());
    }

    /// `Font::clear_complex_script_name` 清空复杂脚本字体字段。
    #[test]
    fn font_clear_complex_script_name() {
        let mut r = Run::new("text");
        r.set_complex_script_name("Arial");
        assert_eq!(r.complex_script_name(), Some("Arial"));
        // 通过 Font 视图清空
        {
            let mut f = Font::new(&mut r.properties);
            f.clear_complex_script_name();
        }
        assert!(r.complex_script_name().is_none());
        assert!(r.properties.cs_font.is_none());
    }

    /// `Font` 视图的 eastasia/cs 访问器与 `Run` 便捷方法一致。
    #[test]
    fn font_view_eastasia_cs_consistent_with_run() {
        let mut r = Run::new("混合文本");
        r.set_eastasia_name("黑体");
        r.set_complex_script_name("Tahoma");
        // 先用 Run 便捷方法读取(避免与 Font 视图的可变借用冲突)
        let run_ea = r.eastasia_name().map(|s| s.to_string());
        let run_cs = r.complex_script_name().map(|s| s.to_string());
        // Font 视图读取应与 Run 便捷方法一致
        let f = Font::new(&mut r.properties);
        assert_eq!(f.eastasia_name(), run_ea.as_deref());
        assert_eq!(f.complex_script_name(), run_cs.as_deref());
    }

    /// 东亚字体序列化:`<a:ea typeface="..."/>` 应在 `<a:latin>` 之后写出。
    #[test]
    fn eastasia_font_serialization_order() {
        let mut r = Run::new("你好");
        r.set_font_name("Arial");
        r.set_eastasia_name("宋体");
        r.set_complex_script_name("Tahoma");
        let mut p = Paragraph::new();
        p.runs.push(r);
        let mut w = crate::oxml::writer::XmlWriter::new();
        p.write_xml(&mut w);
        let xml = w.into_string();
        // 验证三种字体元素都存在
        assert!(
            xml.contains(r#"<a:latin typeface="Arial"/>"#),
            "xml: {}",
            xml
        );
        assert!(xml.contains(r#"<a:ea typeface="宋体"/>"#), "xml: {}", xml);
        assert!(xml.contains(r#"<a:cs typeface="Tahoma"/>"#), "xml: {}", xml);
        // 验证 OOXML 顺序:latin → ea → cs
        let pos_latin = xml.find("<a:latin").expect("latin should exist");
        let pos_ea = xml.find("<a:ea").expect("ea should exist");
        let pos_cs = xml.find("<a:cs").expect("cs should exist");
        assert!(pos_latin < pos_ea, "latin must come before ea: {}", xml);
        assert!(pos_ea < pos_cs, "ea must come before cs: {}", xml);
    }
}