all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
blob 08cc4dbd0fbeeffbe8ef2c5f7620acabbd136141 166347 bytes (raw)
name: lisp/calendar/icalendar-parser.el 	 # note: path name is non-authoritative(*)

   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
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
 
;;; icalendar-parser.el --- Parse iCalendar grammar  -*- lexical-binding: t; -*-

;; Copyright (C) 2024 Free Software Foundation, Inc.

;; Author: Richard Lawrence <rwl@recursewithless.net>
;; Created: October 2024
;; Keywords: calendar
;; Human-Keywords: calendar, iCalendar

;; This file is part of GNU Emacs.

;; This file is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this file.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; This file defines regular expressions, constants and functions that
;; implement the iCalendar grammar according to RFC5545.
;;
;; iCalendar data is grouped into *components*, such as events or
;; to-do items. Each component contains one or more *content lines*,
;; which each contain a *property* name and its *value*, and possibly
;; also property *parameters* with additional data that affects the
;; interpretation of the property.
;;
;; The macros `ical:define-type', `ical:define-param',
;; `ical:define-property' and `ical:define-component', defined in
;; icalendar-macs.el, each create rx-style regular expressions for one
;; of these categories in the grammar and are used here to define the
;; particular value types, parameters, properties and components in the
;; standard as type symbols. These type symbols store all the metadata
;; about the relevant types, and are used for type-based dispatch in the
;; parser and printer functions. In the abstract syntax tree, each node
;; contains a type symbol naming its type. A number of other regular
;; expressions which encode basic categories of the grammar are also
;; defined in this file.
;;
;; The following functions provide the high-level interface to the parser:
;;
;;   `icalendar-parse-component'
;;   `icalendar-parse-property'
;;   `icalendar-parse-params'
;;
;; The format of the abstract syntax tree which these functions create
;; is documented in icalendar-ast.el. Nodes in this tree can be
;; serialized to iCalendar format with the corresponding printer
;; functions:
;;
;;   `icalendar-print-component-node'
;;   `icalendar-print-property-node'
;;   `icalendar-print-params'

;;; Code:
\f
(require 'icalendar-macs)
(require 'icalendar-ast)
(require 'cl-lib)
(require 'subr-x)
(require 'seq)
(require 'rx)
(require 'calendar)
(require 'time-date)
(require 'simple)
(require 'help-mode)

;;; Functions for folding and unfolding
;;
;; According to RFC5545, iCalendar content lines longer than 75 octets
;; should be *folded* by inserting extra line breaks and leading
;; whitespace to continue the line. Such lines must be *unfolded*
;; before they can be parsed.  Unfolding can only reliably happen
;; before Emacs decodes a region of text, because decoding potentially
;; replaces the CR-LF line endings which terminate content lines.
;; Programs that can control when decoding happens should use the
;; stricter `ical:unfold-undecoded-region' to unfold text; programs
;; that must work with decoded data should use the looser
;; `ical:unfold-region'. `ical:fold-region' will fold content lines
;; using line breaks appropriate to the buffer's coding system.
;;
;; All the parsing-related code belows assumes that lines have
;; already been unfolded if necessary.

(defun ical:unfold-undecoded-region (start end &optional buffer)
  "Unfold an undecoded region in BUFFER between START and END.
If omitted, BUFFER defaults to the current buffer.

\"Unfolding\" means removing the whitespace characters inserted to
continue lines longer than 75 octets (see `icalendar-fold-region'
for the folding operation). RFC5545 specifies these whitespace
characters to be a CR-LF sequence followed by a single space or
tab character. Unfolding can only be done reliably before a
region is decoded, since decoding potentially replaces CR-LF line
endings. This function searches strictly for CR-LF sequences, and
will fail if they have already been replaced, so it should only
be called with a region that has not yet been decoded."
  (with-current-buffer (or buffer (current-buffer))
    (with-restriction start end
      (goto-char (point-min))
      (while (re-search-forward (rx (seq "\r\n" (or " " "\t")))
                                nil t)
        (replace-match "" nil nil)))))

(defun ical:unfold-region (start end &optional buffer)
  "Unfold a region in BUFFER between START and END. If omitted,
BUFFER defaults to the current buffer.

\"Unfolding\" means removing the whitespace characters inserted to
continue lines longer than 75 octets (see `icalendar-fold-region'
for the folding operation).

WARNING: Unfolding can only be done reliably before text is
decoded, since decoding potentially replaces CR-LF line endings.
Unfolding an already-decoded region could lead to unexpected
results, such as displaying multibyte characters incorrectly,
depending on the contents and the coding system used.

This function attempts to do the right thing even if the region
is already decoded. If it is still undecoded, it is better to
call `icalendar-unfold-undecoded-region' directly instead, and
decode it afterward."
  ;; TODO: also make this a command so it can be run manually?
  (with-current-buffer (or buffer (current-buffer))
    (let ((was-multibyte enable-multibyte-characters)
          (start-char (position-bytes start))
          (end-char (position-bytes end)))
      ;; we put the buffer in unibyte mode and later restore its
      ;; previous state, so that if the buffer was already multibyte,
      ;; any multibyte characters where line folds broke up their
      ;; bytes can be reinterpreted:
      (set-buffer-multibyte nil)
      (with-restriction start-char end-char
        (goto-char (point-min))
        ;; since we can't be sure that line folds have a leading CR
        ;; in already-decoded regions, do the best we can:
        (while (re-search-forward (rx (seq (zero-or-one "\r") "\n"
                                           (or " " "\t")))
                                  nil t)
          (replace-match "" nil nil)))
      ;; restore previous state, possibly reinterpreting characters:
      (set-buffer-multibyte was-multibyte))))

(defun ical:unfolded-buffer-from-region (start end &optional buffer)
  "Create a new buffer with the same contents as the region between
START and END (in BUFFER, if provided) and perform line unfolding
in the new buffer with `icalendar-unfold-region'. That function
can in some cases have undesirable effects; see its docstring. If
BUFFER is visiting a file, it may be better to reload its
contents from that file and perform line unfolding before
decoding; see `icalendar-unfolded-buffer-from-file'. Returns the
new buffer."
  (let* ((old-buffer (or buffer (current-buffer)))
         (contents (with-current-buffer old-buffer
                     (buffer-substring start end)))
         (uf-buffer (generate-new-buffer
                     (concat (buffer-name old-buffer)
                             "~UNFOLDED")))) ;; TODO: again, move to modeline?
    (with-current-buffer uf-buffer
      (insert contents)
      (ical:unfold-region (point-min) (point-max))
      ;; ensure we'll use CR-LF line endings on write, even if they weren't
      ;; in the source data. The standard also says UTF-8 is the default
      ;; encoding, so use 'prefer-utf-8-dos when last-coding-system-used
      ;; is nil.
      (setq buffer-file-coding-system
            (if last-coding-system-used
                (coding-system-change-eol-conversion last-coding-system-used
                                                     'dos)
              'prefer-utf-8-dos)))
    uf-buffer))

(defun ical:unfolded-buffer-from-buffer (buffer)
  "Create a new buffer with the same contents as BUFFER and perform
line unfolding with `icalendar-unfold-region'. That function can in
some cases have undesirable effects; see its docstring. If BUFFER
is visiting a file, it may be better to reload its contents from
that file and perform line unfolding before decoding; see
`icalendar-unfolded-buffer-from-file'. Returns the new buffer."
  (with-current-buffer buffer
    (ical:unfolded-buffer-from-region (point-min) (point-max) buffer)))

(defun ical:unfolded-buffer-from-file (filename &optional visit beg end)
    "Create a new buffer with the contents of FILENAME and perform
line unfolding with `icalendar-unfold-undecoded-region', then
decode the buffer, setting an appropriate value for
`buffer-file-coding-system'. Optional arguments VISIT, BEG, END
are as in `insert-file-contents'. Returns the new buffer."
    (unless (and (file-exists-p filename)
                 (file-readable-p filename))
      (error "File cannot be read: %s" filename))
    ;; TODO: instead of messing with the buffer name, it might be more
    ;; useful to keep track of the folding state in a variable and
    ;; display it somewhere else in the mode line
    (let ((uf-buffer (generate-new-buffer (concat (file-name-nondirectory filename)
                                                  "~UNFOLDED"))))
      (with-current-buffer uf-buffer
        (set-buffer-multibyte nil)
        (insert-file-contents-literally filename visit beg end t)
        (ical:unfold-undecoded-region (point-min) (point-max))
        (set-buffer-multibyte t)
        (decode-coding-inserted-region (point-min) (point-max) filename)
        ;; ensure we'll use CR-LF line endings on write, even if they weren't
        ;; in the source data. The standard also says UTF-8 is the default
        ;; encoding, so use 'prefer-utf-8-dos when last-coding-system-used
        ;; is nil. FIXME: for some reason, this doesn't seem to run at all!
        (setq buffer-file-coding-system
              (if last-coding-system-used
                  (coding-system-change-eol-conversion last-coding-system-used
                                                       'dos)
                'prefer-utf-8-dos))
        ;; restore buffer name after renaming by set-visited-file-name:
        (let ((bname (buffer-name)))
          (set-visited-file-name filename t)
          (rename-buffer bname)))
      uf-buffer))

(defun ical:fold-region (begin end &optional use-tabs)
  "Fold all content lines in the region longer than 75 octets.

\"Folding\" means inserting a line break and a single space
character at the beginning of the new line. If USE-TABS is
non-nil, insert a tab character instead of a single space.

RFC5545 specifies that lines longer than 75 *octets* (excluding
the line-ending CR-LF sequence) must be folded, and allows that
some implementations might fold lines in the middle of a
multibyte character. This function takes care not to do that in a
buffer where `enable-multibyte-characters' is non-nil, and only
folds between character boundaries. If the buffer is in unibyte
mode, however, and contains undecoded multibyte data, it may fold
lines in the middle of a multibyte character."
  ;; TODO: also make this a command so it can be run manually?
  (save-excursion
    (goto-char begin)
    (when (not (bolp))
      (let ((inhibit-field-text-motion t))
        (beginning-of-line)))
    (let ((bol (point))
          (eol (make-marker))
          (reg-end (make-marker))
          (line-fold
           (concat
            ;; if \n will be translated to \r\n on save (EOL type 1,
            ;; "DOS"), just insert \n, otherwise the full fold sequence:
            ;; FIXME: is buffer-file-coding-system the only relevant one here?
            ;; What if the buffer is not visiting a file, but has come from a
            ;; process, represents a mime part in an email, etc.?
            (if (eq 1 (coding-system-eol-type buffer-file-coding-system))
                "\n"
              "\r\n")
            ;; leading whitespace after line break:
            (if use-tabs "\t" " "))))
      (set-marker reg-end end)
      (while (< bol reg-end)
        (let ((inhibit-field-text-motion t))
          (end-of-line))
        (set-marker eol (point))
        (when (< 75 (- (position-bytes (marker-position eol))
                       (position-bytes bol)))
          (goto-char
           ;; the max of 75 excludes the two CR-LF
           ;; characters we're about to add:
           (byte-to-position (+ 75 (position-bytes bol))))
          (insert line-fold)
          (set-marker eol (point)))
        (setq bol (goto-char (1+ eol)))))))

(defun ical:contains-folded-lines-p ()
  "Determine whether the current buffer contains folded content
lines that should be unfolded for parsing and display purposes.
If it does, return the position at the end of the first fold."
  (save-excursion
    (goto-char (point-min))
    (re-search-forward (rx (seq line-start (or " " "\t")))
                       nil t)))

(defun ical:contains-unfolded-lines-p ()
  "Determine whether the current buffer contains long content lines
that should be folded before saving or transmitting. If it does,
return the position at the beginning of the first line that
requires folding."
  (save-excursion
    (goto-char (point-min))
    (let ((bol (point))
          (eol (make-marker)))
      (catch 'unfolded-line
        (while (< bol (point-max))
          (let ((inhibit-field-text-motion t))
            (end-of-line))
          (set-marker eol (point))
          ;; the max of 75 excludes the two CR-LF characters
          ;; after position eol:
          (when (< 75 (- (position-bytes (marker-position eol))
                         (position-bytes bol)))
            (throw 'unfolded-line bol))
          (setq bol (goto-char (1+ eol))))
        nil))))

\f
;; Parsing-related code starts here. All the parsing code assumes that
;; content lines have already been unfolded.

;;;; Error handling:

;; Errors at the parsing stage:
;; e.g. value does not match expected regex
(define-error 'ical:parse-error "Could not parse iCalendar data")

;; Errors at the printing stage:
;; e.g. default print function doesn't know how to print value
(define-error 'ical:print-error "Unable to print iCalendar data")

;;;; Some utilities:
(defun ical:parse-one-of (types limit)
  "Parse a value of one of the TYPES, which should be a list of type
symbols, from point up to LIMIT. For each type in TYPES, the
parser function associated with that type will be called at
point. The return value of the first successful parser function
is returned. If none of the parser functions are able to parse a
value, an `icalendar-parse-error' is signaled."
  (let* ((value nil)
         (start (point))
         (type (car types))
         (parser (get type 'ical:value-parser))
         (rest (cdr types)))
    (while (and parser (not value))
      (condition-case nil
          (setq value (funcall parser limit))
        (ical:parse-error
         ;; value of this type not found, so try again:
         (goto-char start)
         (setq type (car rest)
               rest (cdr rest)
               parser (get type 'ical:value-parser)))))
    (unless value
      (signal 'ical:parse-error
              (list (format "Unable to parse any of %s between %d and %d"
                            types start limit))))
    value))

(defun ical:read-list-with (reader string
                            &optional value-regex separators omit-nulls trim)
  "Read a list of values from STRING with READER.

READER should be a reader function that accepts a single string argument.
SEPARATORS, OMIT-NULLS, and TRIM are as in `split-string'.
SEPARATORS defaults to \"[^\\][,;]\". TRIM defaults to matching a
double quote character.

VALUE-REGEX should be a regular expression if READER assumes that
individual substrings in STRING have previously been matched
against this regex. In this case, each value in S is placed in a
temporary buffer and the match against VALUE-REGEX is performed
before READER is called."
  (let* ((wrapped-reader
           (if (not value-regex)
               ;; no need for temp buffer:
               reader
             ;; match the regex in a temp buffer before calling reader:
             (lambda (s)
               (with-temp-buffer
                 (insert s)
                 (goto-char (point-min))
                 (unless (looking-at value-regex)
                   (signal 'ical:parse-error
                           (list (format "Expected list of values matching '%s'"
                                         value-regex)
                                 s)))
                 (funcall reader (match-string 0))))))
         (seps (or separators "[^\\][,;]"))
         (trm (or trim "\""))
         (raw-values (split-string string seps omit-nulls trm)))

    (unless (functionp reader)
      (signal 'ical:parser-error
              (list (format "`%s' is not a reader function" reader))))

    (mapcar wrapped-reader raw-values)))

(defun ical:read-list-of (type string
                          &optional separators omit-nulls trim)
  "Read a list of values of type TYPE from STRING.

TYPE should be a value type symbol. The reader function
associated with that type will be called to read the successive
values in STRING, and the values will be returned as a list of
syntax nodes.

SEPARATORS, OMIT-NULLS, and TRIM are as in `split-string' and
will be passed on, if provided, to `icalendar-read-list-with'."
  (let* ((reader (lambda (s) (ical:read-value-node type s)))
         (val-regex (rx-to-string (get type 'ical:value-rx))))
    (ical:read-list-with reader string val-regex
                         separators omit-nulls trim)))

(defun ical:list-of-p (list type)
  "Returns non-nil if each value in LIST satisfies TYPE according to
`cl-typep'"
  (seq-every-p (lambda (val) (cl-typep val type)) list))

(defun ical:default-value-printer (val)
  "Default printer for a *single* property or parameter value.

If VAL is a string, just return it unchanged.

Otherwise, VAL should be a syntax node representing a value. In
that case, return the original string value if another was
substituted at parse time, or look up the printer function for
the node's type and call it on the value inside the node.

For properties and parameters that only allow a single value,
this function should be a sufficient value printer. It is not
sufficient for those that allow lists of values, or which have
other special requirements like quoting or escaping."
  (cond ((stringp val) val)
        ((and (ical:ast-node-p val)
              (get (ical:ast-node-type val) 'ical:value-printer))
         (or (ical:ast-node-meta-get val :original-value)
             (let* ((stored-value (ical:ast-node-value val))
                    (type (ical:ast-node-type val))
                    (printer (get type 'ical:value-printer)))
               (funcall printer stored-value))))
        ;; TODO: other cases to make things easy?
        ;; e.g. symbols print as their names?
        (t (signal 'ical:print-error
                   (list (format "Don't know how to print value: %s" val)
                         val)))))

\f
;;; Section 3.1: Content lines

;; Regexp constants for parsing:

;; In the following regexps and define-* declarations, because
;; Emacs does not have named groups, we observe the following
;; convention so that the regexps can be combined in sensible ways:
;;
;; - Groups 1 through 5 are reserved for the highest-level regexes
;;   created by define-param, define-property and define-component and
;;   used in the match-* functions. Group 1 always represents a 'key'
;;   (e.g. param or property name), group 2 always represents a
;;   correctly parsed value for that key, and group 3 (if matched) an
;;   invalid or unknown value.
;;
;;   Groups 4 and 5 are reserved for other information in these
;;   highest-level regexes, such as the parameter string between a
;;   property name and its value, or unrecognized values allowed by
;;   the standard and required to be treated like a default value.
;;
;; - Groups 6 through 10 are currently unused
;; - Groups 11 through 20 are reserved for significant sub-expressions
;;   of individual value expressions, e.g. the number of weeks in a
;;   duration value. The various read-* functions rely on these groups
;;   when converting iCalendar data to Elisp data structures.

(rx-define ical:iana-token
  (one-or-more (any alnum "-")))

(rx-define ical:x-name
  (seq "X-"
      (zero-or-one (>= 3 (any alnum)) "-") ; Vendor ID
      (one-or-more (any alnum "-")))) ; Name

(rx-define ical:name
  (or ical:iana-token ical:x-name))

(rx-define ical:crlf
  (seq #x12 #xa))

(rx-define ical:control
  ;; All the controls except HTAB
  (any (#x00 . #x08) (#x0A . #x1F) #x7F))

;; TODO: double check that "nonascii" class actually corresponds to
;; the range in the standard
(rx-define ical:safe-char
  ;; Any character except ical:control, ?\", ?\;, ?:, ?,
  (any #x09 #x20 #x21  (#x23 . #x2B) (#x2D . #x39) (#x3C . #x7E) nonascii))

(rx-define ical:qsafe-char
  ;; Any character except ical:control and ?"
  (any #x09 #x20 #x21 (#x23 . #x7E) nonascii))

(rx-define ical:quoted-string
  (seq ?\" (zero-or-more ical:qsafe-char) ?\"))

(rx-define ical:paramtext
  ;; RFC5545 allows *zero* characters here, but that would mean we could
  ;; have parameters like ;FOO=;BAR="somethingelse", and what would then
  ;; be the value of FOO? I see no reason to allow this and it breaks
  ;; parameter parsing so I have required at least one char here
  (one-or-more ical:safe-char))

(rx-define ical:param-name
  (or ical:iana-token ical:x-name))

(rx-define ical:param-value
  (or ical:paramtext ical:quoted-string))

(rx-define ical:value-char
  (any #x09 #x20 (#x21 . #x7E) nonascii))

(rx-define ical:value
  (zero-or-more ical:value-char))

;; some helpers for brevity, not defined in the standard:
(rx-define ical:comma-list (item-rx)
  (seq item-rx
       (zero-or-more (seq ?, item-rx))))

(rx-define ical:semicolon-list (item-rx)
  (seq item-rx
       (zero-or-more (seq ?\; item-rx))))

\f
;;; Section 3.3: Property Value Data Types

;; Note: These definitions are here (out of order with respect to the
;; standard) because a few of them are already required for property
;; parameter definitions (section 3.2) below.

(defconst ical:value-types nil ;; populated by define-type
  "Alist mapping value type strings in `icalendar-valuetypeparam'
parameters to type symbols defined with `icalendar-define-type'")

(defun ical:read-value-node (type s)
  "Read an iCalendar value of type TYPE from string S to a syntax node.
Returns a syntax node containing the value."
  (let ((reader (get type 'ical:value-reader)))
    (ical:make-ast-node type :value (funcall reader s))))

(defun ical:parse-value-node (type limit)
  "Parse an iCalendar value of type TYPE from point up to LIMIT.
Returns a syntax node containing the value."
  (let ((value-regex (rx-to-string (get type 'ical:value-rx))))

    (unless (re-search-forward value-regex limit t)
      (signal 'ical:parse-error
              (list (format "No %s value between %d and %s"
                            type (point) limit))))

    (let ((begin (match-beginning 0))
          (end (match-end 0))
          (node (ical:read-value-node type (match-string 0))))
      (ical:ast-node-meta-set node :buffer (current-buffer))
      (ical:ast-node-meta-set node :begin begin)
      (ical:ast-node-meta-set node :end end)

      node)))

(defun ical:print-value-node (node)
  "Serialize an iCalendar syntax node containing a value to a string."
  (let* ((type (ical:ast-node-type node))
         (value-printer (get type 'ical:value-printer)))
    (funcall value-printer (ical:ast-node-value node))))

(defun ical:printable-value-type-symbol-p (symbol)
  "Return non-nil if SYMBOL is a type symbol representing a printable
iCalendar value type, i.e., a type for a property or parameter
value defined by `icalendar-define-type' which has a print
name (mainly for use in `icalendar-valuetypeparam' parameters).

This means that SYMBOL must both satisfy
`icalendar-value-type-symbol-p' and be associated with a print
name in `icalendar-value-types'."
  (and (ical:value-type-symbol-p symbol)
       (rassq symbol ical:value-types)))

(defun ical:value-node-p (node)
  "Return non-nil if NODE is an iCalendar syntax node whose type
is a value type."
  (and (ical:ast-node-p node)
       (ical:value-type-symbol-p (ical:ast-node-type node))))

;;;; 3.3.1 Binary
;; from https://www.rfc-editor.org/rfc/rfc4648#section-4:
(rx-define ical:base64char
  (any (?A . ?Z) (?a . ?z) (?0 . ?9) ?+ ?/))

(ical:define-type ical:binary "BINARY"
   "Type for Binary values.

The parsed and printed representations are the same: a string of characters
representing base64-encoded data."
   '(and string (satisfies ical:match-binary-value))
   (seq (zero-or-more (= 4 ical:base64char))
        (zero-or-one (or (seq (= 2 ical:base64char) "==")
                         (seq (= 3 ical:base64char) "="))))
   :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.1")

;;;; 3.3.2 Boolean
(defun ical:read-boolean (s)
  "Read an `icalendar-boolean' value from a string S.
S should be a match against rx `icalendar-boolean'."
  (let ((upcased (upcase s)))
    (cond ((equal upcased "TRUE") t)
          ((equal upcased "FALSE") nil)
          (t (signal 'ical:parse-error
                     (list "Expected 'TRUE' or 'FALSE'" s))))))

(defun ical:print-boolean (b)
  "Serialize an `icalendar-boolean' value B to a string."
    (if b "TRUE" "FALSE"))

(ical:define-type ical:boolean "BOOLEAN"
   "Type for Boolean values.

When printed, either the string 'TRUE' or 'FALSE'.
When read, either t or nil."
   'boolean
   (or "TRUE" "FALSE")
   :reader ical:read-boolean
   :printer ical:print-boolean
   :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.2")

;;;; 3.3.3 Calendar User Address
;; Defined with URI, below

;; Dates and Times:

;;;; 3.3.4 Date
(cl-deftype ical:numeric-year () '(integer 0 9999))
(cl-deftype ical:numeric-month () '(integer 1 12))
(cl-deftype ical:numeric-monthday () '(integer 1 31))

(rx-define ical:year
  (= 4 digit))

(rx-define ical:month
  (= 2 digit))

(rx-define ical:mday
  (= 2 digit))

(defun ical:read-date (s)
  "Read an `icalendar-date' from a string S.
S should be a match against rx `icalendar-date'."
  (let ((year (string-to-number (substring s 0 4)))
        (month (string-to-number (substring s 4 6)))
        (day (string-to-number (substring s 6 8))))
    (list month day year)))

(defun ical:print-date (d)
  "Serialize an `icalendar-date' to a string."
  (format "%04d%02d%02d"
          (calendar-extract-year d)
          (calendar-extract-month d)
          (calendar-extract-day d)))

(ical:define-type ical:date "DATE"
   "Type for Date values.

When printed, a date is a string of digits in YYYYMMDD format.

When read, a date is a list (MONTH DAY YEAR), with the three
values being integers in the appropriate ranges; see `calendar.el'
for functions that work with this representation."
   '(and (satisfies calendar-date-is-valid-p))
   (seq ical:year ical:month ical:mday)
   :reader ical:read-date
   :printer ical:print-date
   :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.4")

;;;; 3.3.12 Time
;; (Defined here so that ical:time RX can be used in ical:date-time)
(cl-deftype ical:numeric-hour () '(integer 0 23))
(cl-deftype ical:numeric-minute () '(integer 0 59))
(cl-deftype ical:numeric-second () '(integer 0 60)) ; 60 represents a leap second

(defun ical:read-time (s)
  "Read an `icalendar-time' from a string S.
S should be a match against rx `icalendar-time'."
  (let ((hour (string-to-number (substring s 0 2)))
        (minute (string-to-number (substring s 2 4)))
        (second (string-to-number (substring s 4 6)))
        (utcoffset (if (and (length= s 7)
                            (equal "Z" (substring s 6 7)))
                       0
                     ;; unknown/'floating' time zone:
                     nil)))
    (make-decoded-time :second second
                       :minute minute
                       :hour hour
                       :zone utcoffset)))

(defun ical:print-time (time)
  "Serialize an `icalendar-time' to a string."
  (format "%02d%02d%02d%s"
          (decoded-time-hour time)
          (decoded-time-minute time)
          (decoded-time-second time)
          (if (eql 0 (decoded-time-zone time))
              "Z" "")))

(defun ical:-decoded-time-p (val)
  "Return non-nil if VAL is a valid decoded *time*.
This predicate does not check date-related values in VAL;
for that, see `icalendar--decoded-date-time-p'."
  ;; FIXME: this should probably be defined alongside the
  ;; other decoded-time-* functions!
  (and (listp val)
       (length= val 9)
       (cl-typep (decoded-time-second val) 'ical:numeric-second)
       (cl-typep (decoded-time-minute val) 'ical:numeric-minute)
       (cl-typep (decoded-time-hour val) 'ical:numeric-hour)
       (cl-typep (decoded-time-dst val) '(member t nil -1))
       (cl-typep (decoded-time-zone val) '(or integer null))))

(ical:define-type ical:time "TIME"
  "Type for Time values.

When printed, a time is a string of six digits HHMMSS, followed
by the letter 'Z' if it is in UTC.

When read, a time is a decoded time, i.e. a list in the format
(SEC MINUTE HOUR DAY MONTH YEAR DOW DST UTCOFF). See
`decode-time' for the specifics of the individual values. When
read, the DAY, MONTH, YEAR, and DOW fields are nil, and these
fields and DST are ignored when printed."
  '(satisfies ical:-decoded-time-p)
  (seq (= 6 digit) (zero-or-one ?Z))
  :reader ical:read-time
  :printer ical:print-time
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.12")

;;;; 3.3.5 Date-Time
(defun ical:-decoded-date-time-p (val)
  ;; FIXME: this should probably be defined alongside the
  ;; other decoded-time-* functions!
  (and (listp val)
       (length= val 9)
       (cl-typep (decoded-time-second val) 'ical:numeric-second)
       (cl-typep (decoded-time-minute val) 'ical:numeric-minute)
       (cl-typep (decoded-time-hour val) 'ical:numeric-hour)
       (cl-typep (decoded-time-day val) 'ical:numeric-monthday)
       (cl-typep (decoded-time-month val) 'ical:numeric-month)
       (cl-typep (decoded-time-year val) 'ical:numeric-year)
       (calendar-date-is-valid-p (list (decoded-time-month val)
                                       (decoded-time-day val)
                                       (decoded-time-year val)))
       ;; FIXME: the weekday slot value should be automatically
       ;; calculated from month, day, and year, like:
       ;;   (calendar-day-of-week (list month day year))
       ;; Although `ical:read-date-time' does this correctly,
       ;; `make-decoded-time' does not. Thus we can't use
       ;; `make-decoded-time' to construct valid `ical:date-time'
       ;; values unless this check is turned off,
       ;; which means it's annoying to write tests and anything
       ;; that uses cl-typecase to dispatch on values created by
       ;; `make-decoded-time':
       ;; (cl-typep (decoded-time-weekday val) '(integer 0 6))
       (cl-typep (decoded-time-dst val) '(member t nil -1))
       (cl-typep (decoded-time-zone val) '(or integer null))))

(defun ical:read-date-time (s)
  "Read an `icalendar-date-time' from a string S.
S should be a match against rx `icalendar-date-time'."
  (let ((year (string-to-number (substring s 0 4)))
        (month (string-to-number (substring s 4 6)))
        (day (string-to-number (substring s 6 8)))
        ;; "T" is index 8
        (hour (string-to-number (substring s 9 11)))
        (minute (string-to-number (substring s 11 13)))
        (second (string-to-number (substring s 13 15)))
        (utcoffset (if (and (length= s 16)
                            (equal "Z" (substring s 15 16)))
                       0
                     ;; unknown/'floating' time zone:
                     nil)))
    (list second minute hour day month year
          (calendar-day-of-week (list month day year))
          -1 ; DST information not available
          utcoffset)))

(defun ical:print-date-time (datetime)
  "Serialize an `icalendar-date-time' to a string."
  (format "%04d%02d%02dT%02d%02d%02d%s"
          (decoded-time-year datetime)
          (decoded-time-month datetime)
          (decoded-time-day datetime)
          (decoded-time-hour datetime)
          (decoded-time-minute datetime)
          (decoded-time-second datetime)
          (if (ical:date-time-is-utc-p datetime)
              "Z" "")))

(defun ical:date-time-is-utc-p (datetime)
  "Return non-nil if DATETIME is in UTC time"
  (let ((offset (decoded-time-zone datetime)))
    (and offset (= 0 offset))))

(ical:define-type ical:date-time "DATE-TIME"
   "Type for Date-Time values.

When printed, a date-time is a string of digits like:
  YYYYMMDDTHHMMSS
where the 'T' is literal, and separates the date string from the
time string.

When read, a date-time is a decoded time, i.e. a list in the format
(SEC MINUTE HOUR DAY MONTH YEAR DOW DST UTCOFF). See
`decode-time' for the specifics of the individual values."
   '(satisfies ical:-decoded-date-time-p)
  (seq ical:date ?T ical:time)
  :reader ical:read-date-time
  :printer ical:print-date-time
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.5")

;;;; 3.3.6 Duration
(rx-define ical:dur-second
  (seq (group-n 19 (one-or-more digit)) ?S))

(rx-define ical:dur-minute
  (seq (group-n 18 (one-or-more digit)) ?M (zero-or-one ical:dur-second)))

(rx-define ical:dur-hour
  (seq (group-n 17 (one-or-more digit)) ?H (zero-or-one ical:dur-minute)))

(rx-define ical:dur-day
  (seq (group-n 16 (one-or-more digit)) ?D))

(rx-define ical:dur-week
  (seq (group-n 15 (one-or-more digit)) ?W))

(rx-define ical:dur-time
  (seq ?T (or ical:dur-hour ical:dur-minute ical:dur-second)))

(rx-define ical:dur-date
  (seq ical:dur-day (zero-or-one ical:dur-time)))

(defun ical:read-dur-value (s)
  "Read an `icalendar-dur-value' from a string S.
S should be a match against rx `icalendar-dur-value'."
  ;; TODO: this smells like a design flaw. Silence the byte compiler for now.
  (ignore s)
  (let ((sign (if (equal (match-string 20) "-") -1 1)))
    (if (match-string 15)
        ;; dur-value specified in weeks, so just return an integer:
        (* sign (string-to-number (match-string 15)))
      ;; otherwise, make a time delta from the other units:
      (let* ((days (match-string 16))
             (ndays (* sign (if days (string-to-number days) 0)))
             (hours (match-string 17))
             (nhours (* sign (if hours (string-to-number hours) 0)))
             (minutes (match-string 18))
             (nminutes (* sign (if minutes (string-to-number minutes) 0)))
             (seconds (match-string 19))
             (nseconds (* sign (if seconds (string-to-number seconds) 0))))
        (make-decoded-time :second nseconds :minute nminutes :hour nhours
                           :day ndays)))))

(defun ical:print-dur-value (dur)
  "Serialize an `icalendar-dur-value' to a string"
  (if (integerp dur)
      ;; dur-value specified in weeks can only contain weeks:
      (format "%sP%dW" (if (< dur 0) "-" "") (abs dur))
    ;; otherwise, show all the time units present:
    (let* ((days+- (or (decoded-time-day dur) 0))
           (hours+- (or (decoded-time-hour dur) 0))
           (minutes+- (or (decoded-time-minute dur) 0))
           (seconds+- (or (decoded-time-second dur) 0))
           ;; deal with the possibility of mixed positive and negative values
           ;; in a time delta list:
           (sum (+ seconds+-
                   (* 60 minutes+-)
                   (* 60 60 hours+-)
                   (* 60 60 24 days+-)))
           (abssum (abs sum))
           (days (/ abssum (* 60 60 24)))
           (sumnodays (mod abssum (* 60 60 24)))
           (hours (/ sumnodays (* 60 60)))
           (sumnohours (mod sumnodays (* 60 60)))
           (minutes (/ sumnohours 60))
           (seconds (mod sumnohours 60))
           (sign (when (< sum 0) "-"))
           (time-sep (unless (and (zerop hours) (zerop minutes) (zerop seconds))
                       "T")))
      (concat sign
              "P"
              (unless (zerop days) (format "%dD" days))
              time-sep
              (unless (zerop hours) (format "%dH" hours))
              (unless (zerop minutes) (format "%dM" minutes))
              (unless (zerop seconds) (format "%dS" seconds))))))

(defun ical:-time-delta-p (val)
  (and (listp val)
       (length= val 9)
       (let ((seconds (decoded-time-second val))
             (minutes (decoded-time-minute val))
             (hours (decoded-time-hour val))
             (days (decoded-time-day val))) ; other values in list are ignored
         (and
          (cl-typep seconds 'integer)
          (cl-typep minutes 'integer)
          (cl-typep hours 'integer)
          (cl-typep days 'integer)
          (not (and (zerop seconds) (zerop minutes) (zerop hours)
                    (zerop days)))))))

(ical:define-type ical:dur-value "DUR-VALUE" ; avoid name clashes with DURATION
  "Type for Duration values.

When printed, a duration is a string containing:
  - possibly a +/- sign
  - the letter 'P'
  - one or more sequences of digits followed by a letter representing a unit
    of time: 'W' for weeks, 'D' for days, etc. Units smaller than a day are
    separated from days by the letter 'T'. If a duration is specified in weeks,
    other units of time are not allowed.

For example, a duration of 15 days, 5 hours, and 20 seconds would be printed:
   P15DT5H0M20S
and a duration of 7 weeks would be printed:
   P7W

When read, a duration is either an integer, in which case it
represents a number of weeks, or a decoded time, in which case it
must represent a time delta in the sense of `decoded-time-add'.
Note that, in the time delta representation, units of time longer
than a day are not supported and will be ignored if present.

This type is named `icalendar-dur-value' rather than
`icalendar-duration' for consistency with the text of RFC5545 and
so that its name does not collide with the symbol for the
'DURATION' property."
  '(or integer (satisfies ical:-time-delta-p))
  ;; Group 15: weeks
  ;; Group 16: days
  ;; Group 17: hours
  ;; Group 18: minutes
  ;; Group 19: seconds
  ;; Group 20: sign
  (seq
   (group-n 20 (zero-or-one (or ?+ ?-)))
   ?P
   (or ical:dur-date ical:dur-time ical:dur-week))
  :reader ical:read-dur-value
  :printer ical:print-dur-value
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.6")


;;;; 3.3.7 Float
(ical:define-type ical:float "FLOAT"
   "Type for Float values.

When printed, possibly a sign + or -, followed by a sequence of digits,
and possibly a decimal. When read, an Elisp float value."
   '(float * *)
   (seq
    (zero-or-one (or ?+ ?-))
    (one-or-more digit)
    (zero-or-one (seq ?. (one-or-more digit))))
   :reader string-to-number
   :printer number-to-string
   :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.7")

;;;; 3.3.8 Integer
(ical:define-type ical:integer "INTEGER"
   "Type for Integer values.

When printed, possibly a sign + or -, followed by a sequence of digits.
When read, an Elisp integer value between -2147483648 and 2147483647."
   '(integer -2147483648 2147483647)
   (seq
    (zero-or-one (or ?+ ?-))
    (one-or-more digit))
   :reader string-to-number
   :printer number-to-string
   :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.8")

;;;; 3.3.9 Period
(defsubst ical:period-start (period)
  "Return the `icalendar-date-time' which marks the start of PERIOD."
  (car period))

(defsubst ical:period-end (period)
  "Return the `icalendar-date-time' which marks the end of PERIOD, or nil."
  (cadr period))

(defsubst ical:period-dur-value (period)
  "Return the `icalendar-dur-value' which gives the length of PERIOD, or nil."
  (caddr period))

(defun ical:period-p (val)
  (and (listp val)
       (length= val 3)
       (cl-typep (ical:period-start val) 'ical:date-time)
       (cl-typep (ical:period-end val) '(or null ical:date-time))
       (cl-typep (ical:period-dur-value val) '(or null ical:dur-value))))

(defun ical:read-period (s)
  "Read an `icalendar-period' from a string S.
S should have been matched against rx `icalendar-period'."
  ;; TODO: this smells like a design flaw. Silence the byte compiler for now.
  (ignore s)
  (let ((start (ical:read-date-time (match-string 11)))
        (end (when (match-string 12) (ical:read-date-time (match-string 12))))
        (dur (when (match-string 13) (ical:read-dur-value (match-string 13)))))
    (list start end dur)))

(defun ical:print-period (per)
  "Serialize an `icalendar-period' to a string"
  (let ((start (ical:period-start per))
        (end (ical:period-end per))
        (dur (ical:period-dur-value per)))
    (concat (ical:print-date-time start)
            "/"
            (if dur
                (ical:print-dur-value dur)
              (ical:print-date-time end)))))

(ical:define-type ical:period "PERIOD"
   "Type for Period values.

A period of time is specified as a starting date-time together
with either an explicit date-time as its end, or a duration which
gives its length and implicitly marks its end.

When printed, the starting date-time is separated from the end or
duration by a / character.

When read, a period is represented as a list (START END DUR),
where START is an `icalendar-date-time', END is either an
`icalendar-date-time' or nil, and DUR is either an
`icalendar-dur-value' or nil. (This representation allows END to
be computed from DUR and cached, and also distinguishes DUR and
END, which might both be decoded times.)"
  '(satisfies ical:period-p)
  (seq (group-n 11 ical:date-time)
       "/"
       (or (group-n 12 ical:date-time)
           (group-n 13 ical:dur-value)))
  :reader ical:read-period
  :printer ical:print-period
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.9")

;;;; 3.3.10 Recurrence rules:
(rx-define ical:freq
   (or "SECONDLY" "MINUTELY" "HOURLY" "DAILY" "WEEKLY" "MONTHLY" "YEARLY"))

(rx-define ical:weekday
   (or "SU" "MO" "TU" "WE" "TH" "FR" "SA"))

(rx-define ical:ordwk
  (** 1 2 digit)) ; 1 to 53

(rx-define ical:weekdaynum
  ;; Group 19: Week num, if present
  ;; Group 20: week day abbreviation
   (seq (zero-or-one
         (group-n 19 (seq (zero-or-one (or ?+ ?-))
                          ical:ordwk)))
        (group-n 20 ical:weekday)))

(rx-define ical:weeknum
  (seq (zero-or-one (or ?+ ?-))
       ical:ordwk))

(rx-define ical:monthdaynum
  (seq (zero-or-one (or ?+ ?-))
       (** 1 2 digit))) ; 1 to 31

(rx-define ical:monthnum
  (seq (zero-or-one (or ?+ ?-))
       (** 1 2 digit))) ; 1 to 12

(rx-define ical:yeardaynum
  (seq (zero-or-one (or ?+ ?-))
       (** 1 3 digit))) ; 1 to 366

(defconst ical:weekday-numbers
  '(("SU" . 0)
    ("MO" . 1)
    ("TU" . 2)
    ("WE" . 3)
    ("TH" . 4)
    ("FR" . 5)
    ("SA" . 6))
  "Alist mapping two-letter weekday abbreviations to numbers 0 to 6.
Weekday abbreviations in recurrence rule parts are translated to
and from numbers for compatibility with calendar-* and
decoded-time-* functions.")

(defun ical:read-weekdaynum (s)
  "Read a weekday abbreviation to a number.
If the abbreviation is preceded by an offset, read a dotted
pair (WEEKDAY . OFFSET). Thus \"SU\" becomes 0, \"-1SU\"
becomes (0 . -1), etc. S should have been matched against
`icalendar-weekdaynum'."
  ;; TODO: this smells like a design flaw. Silence the byte compiler for now.
  (ignore s)
  (let ((dayno (cdr (assoc (match-string 20) ical:weekday-numbers)))
        (weekno (match-string 19)))
    (if weekno
        (cons dayno (string-to-number weekno))
      dayno)))

(defun ical:print-weekdaynum (val)
  "Serialize a number or dotted pair VAL to a string
(as part of a BYDAY recur rule part). See `icalendar-read-weekdaynum'
for the value format."
  (if (consp val)
      (let* ((dayno (car val))
             (day (car (rassq dayno ical:weekday-numbers)))
             (offset (cdr val)))
        (concat (number-to-string offset) day))
    ;; number alone just stands for a day:
    (car (rassq val ical:weekday-numbers))))

(defun ical:read-recur-rule-part (s)
  "Read an `icalendar-recur-rule-part' from string S.
S should have been matched against `icalendar-recur-rule-part'.
The return value is a list (KEYWORD VALUE), where VALUE may
itself be a list, depending on the values allowed by KEYWORD."
  ;; TODO: this smells like a design flaw. Silence the byte compiler for now.
  (ignore s)
  (let ((keyword (intern (upcase (match-string 11))))
        (values (match-string 12)))
    (list keyword
      (cl-case keyword
        (FREQ (intern (upcase values)))
        (UNTIL (if (length> values 8)
                   (ical:read-date-time values)
                 (ical:read-date values)))
        ((COUNT INTERVAL)
         (string-to-number values))
        ((BYSECOND BYMINUTE BYHOUR BYMONTHDAY BYYEARDAY BYWEEKNO BYMONTH BYSETPOS)
         (ical:read-list-with #'string-to-number values nil ","))
        (BYDAY
         (ical:read-list-with #'ical:read-weekdaynum values
                              (rx ical:weekdaynum) ","))
        (WKST (cdr (assoc values ical:weekday-numbers)))))))

(defun ical:print-recur-rule-part (part)
  "Serialize recur rule part PART to a string."
  (let ((keyword (car part))
        (values (cadr part))
        values-str)
    (cl-case keyword
      (FREQ (setq values-str (symbol-name values)))
      (UNTIL (setq values-str (cl-typecase values
                                (ical:date-time (ical:print-date-time values))
                                (ical:date (ical:print-date values)))))
      ((COUNT INTERVAL)
       (setq values-str (number-to-string values)))
      ((BYSECOND BYMINUTE BYHOUR BYMONTHDAY BYYEARDAY BYWEEKNO BYMONTH BYSETPOS)
       (setq values-str (string-join (mapcar #'number-to-string values)
                                     ",")))
      (BYDAY
       (setq values-str (string-join (mapcar #'ical:print-weekdaynum values)
                                     ",")))
      (WKST (setq values-str (car (rassq values ical:weekday-numbers)))))

    (concat (symbol-name keyword) "=" values-str)))

(rx-define ical:recur-rule-part
  ;; Group 11: keyword
  ;; Group 12: value(s)
  (or (seq (group-n 11 "FREQ") "=" (group-n 12 ical:freq))
      (seq (group-n 11 "UNTIL") "=" (group-n 12 (or ical:date-time ical:date)))
      (seq (group-n 11 "COUNT") "=" (group-n 12 (one-or-more digit)))
      (seq (group-n 11 "INTERVAL") "=" (group-n 12 (one-or-more digit)))
      (seq (group-n 11 "BYSECOND") "=" (group-n 12 ; 0 to 60
                                         (ical:comma-list (** 1 2 digit))))
      (seq (group-n 11 "BYMINUTE") "=" (group-n 12 ; 0 to 59
                                         (ical:comma-list (** 1 2 digit))))
      (seq (group-n 11 "BYHOUR") "=" (group-n 12 ; 0 to 23
                                       (ical:comma-list (** 1 2 digit)))) ; 0 to 23
      (seq (group-n 11 "BYDAY") "=" (group-n 12 ; weeknum? daynum, e.g. SU or 34SU
                                      (ical:comma-list ical:weekdaynum)))
      (seq (group-n 11 "BYMONTHDAY") "=" (group-n 12
                                           (ical:comma-list ical:monthdaynum)))
      (seq (group-n 11 "BYYEARDAY") "=" (group-n 12
                                          (ical:comma-list ical:yeardaynum)))
      (seq (group-n 11 "BYWEEKNO") "=" (group-n 12 (ical:comma-list ical:weeknum)))
      (seq (group-n 11 "BYMONTH") "=" (group-n 12 (ical:comma-list ical:monthnum)))
      (seq (group-n 11 "BYSETPOS") "=" (group-n 12
                                         (ical:comma-list ical:yeardaynum)))
      (seq (group-n 11 "WKST") "=" (group-n 12 ical:weekday))))

(defun ical:read-recur (s)
  "Read a recurrence rule value from string S.
S should be a match against rx `icalendar-recur'."
  (ical:read-list-with #'ical:read-recur-rule-part s (rx ical:recur-rule-part) ";"))

(defun ical:print-recur (val)
  "Serialize a recurrence rule value VAL to a string."
  ;; RFC5545 sec. 3.3.10: "to ensure backward compatibility with
  ;; applications that pre-date this revision of iCalendar the
  ;; FREQ rule part MUST be the first rule part specified in a
  ;; RECUR value."
  (string-join
   (cons
    (ical:print-recur-rule-part (assq 'FREQ val))
    (mapcar #'ical:print-recur-rule-part
            (seq-filter (lambda (part) (not (eq 'FREQ (car part))))
                        val)))
   ";"))

(defconst ical:-recur-value-types
  ;; Note: "list-of" is not a cl-type specifier, just a symbol here; it is
  ;; handled specially when checking types in ical:recur-value-p:
  '(FREQ (member YEARLY MONTHLY WEEKLY DAILY HOURLY MINUTELY SECONDLY)
    UNTIL (or ical:date-time ical:date)
    COUNT (integer 1 *)
    INTERVAL (integer 1 *)
    BYSECOND (list-of (integer 0 60))
    BYMINUTE (list-of (integer 0 59))
    BYHOUR (list-of (integer 0 23))
    BYDAY (list-of (or (integer 0 6) (satisfies ical:dayno-offset-p)))
    BYMONTHDAY (list-of (or (integer -31 -1) (integer 1 31)))
    BYYEARDAY (list-of (or (integer -366 -1) (integer 1 366)))
    BYWEEKNO (list-of (or (integer -53 -1) (integer 1 53)))
    BYMONTH (list-of (integer 1 12)) ; unlike the others, months cannot be negative
    BYSETPOS (list-of (or (integer -366 -1) (integer 1 366)))
    WKST (integer 0 6))
  "Plist mapping `icalendar-recur' keywords to type specifiers")

(defun ical:dayno-offset-p (val)
  "Return non-nil if VAL is a pair (DAYNO . OFFSET), part of a
recurrence rule BYDAY value"
  (and (consp val)
       (cl-typep (car val) '(integer 0 6))
       (cl-typep (cdr val) '(or (integer -53 -1) (integer 1 53)))))

(defun ical:recur-value-p (vals)
  "Return non-nil if VALS is an iCalendar recurrence rule value."
  (and (listp vals)
       ;; FREQ is always required:
       (assq 'FREQ vals)
       ;; COUNT and UNTIL are mutually exclusive if present:
       (not (and (assq 'COUNT vals) (assq 'UNTIL vals)))
       ;; If BYSETPOS is present, another BYXXX clause must be too:
       (or (not (assq 'BYSETPOS vals))
           (assq 'BYMONTH vals)
           (assq 'BYWEEKNO vals)
           (assq 'BYYEARDAY vals)
           (assq 'BYMONTHDAY vals)
           (assq 'BYDAY vals)
           (assq 'BYHOUR vals)
           (assq 'BYMINUTE vals)
           (assq 'BYSECOND vals))
       (let ((freq (ical:recur-freq vals))
             (byday (ical:recur-by* 'BYDAY vals))
             (byweekno (ical:recur-by* 'BYWEEKNO vals))
             (bymonthday (ical:recur-by* 'BYMONTHDAY vals))
             (byyearday (ical:recur-by* 'BYYEARDAY vals)))
         (and
          ;; "The BYDAY rule part MUST NOT be specified with a numeric
          ;; value when the FREQ rule part is not set to MONTHLY or
          ;; YEARLY."
          (or (not (consp (car byday)))
              (memq freq '(MONTHLY YEARLY)))
          ;; "The BYDAY rule part MUST NOT be specified with a numeric
          ;; value with the FREQ rule part set to YEARLY when the
          ;; BYWEEKNO rule part is specified." This also covers:
          ;; "[The BYWEEKNO] rule part MUST NOT be used when the FREQ
          ;; rule part is set to anything other than YEARLY."
          (or (not byweekno)
              (and (eq freq 'YEARLY)
                   (not (consp (car byday)))))
          ;; "The BYMONTHDAY rule part MUST NOT be specified when the
          ;; FREQ rule part is set to WEEKLY."
          (not (and bymonthday (eq freq 'WEEKLY)))
          ;; "The BYYEARDAY rule part MUST NOT be specified when the
          ;; FREQ rule part is set to DAILY, WEEKLY, or MONTHLY."
          (not (and byyearday (memq freq '(DAILY WEEKLY MONTHLY))))))
       ;; check types of all rule parts:
       (seq-every-p
        (lambda (kv)
          (when (consp kv)
            (let* ((keyword (car kv))
                   (val (cadr kv))
                   (type (plist-get ical:-recur-value-types keyword)))
              (and keyword val type
                   (if (and (consp type)
                            (eq (car type) 'list-of))
                       (ical:list-of-p val (cadr type))
                     (cl-typep val type))))))
         vals)))

(ical:define-type ical:recur "RECUR"
  "Type for Recurrence Rule values.

When printed, a recurrence rule value looks like
  KEY1=VAL1;KEY2=VAL2;...
where the VALs may themselves be lists or have other syntactic
structure; see RFC5545 sec. 3.3.10 for all the gory details.

The KEYs and their associated value types when read are as follows.
The first is required:
  '(FREQ (member YEARLY MONTHLY WEEKLY DAILY HOURLY MINUTELY SECONDLY)
These two are mutually exclusive; at most one may appear:
    UNTIL (or icalendar-date-time icalendar-date)
    COUNT (integer 1 *)
All others are optional:
    INTERVAL (integer 1 *)
    BYSECOND (list-of (integer 0 60))
    BYMINUTE (list-of (integer 0 59))
    BYHOUR (list-of (integer 0 23))
    BYDAY (list-of (or (integer 0 6) ; day of week
                       (pair (integer 0 6)  ; (day of week . offset)
                             (integer -53 53))) ; except 0
    BYMONTHDAY (list-of (integer -31 31))  ; except 0
    BYYEARDAY (list-of (integer -366 366)) ; except 0
    BYWEEKNO (list-of (integer -53 53))    ; except 0
    BYMONTH (list-of (integer 1 12))       ; months cannot be negative
    BYSETPOS (list-of (integer -366 366))  ; except 0
    WKST (integer 0 6))

When read, these KEYs and their associated VALs are gathered into
an alist.

In general, the VALs consist of integers or lists of integers.
Abbreviations for weekday names are translated into integers
0 (=Sunday) through 6 (=Saturday), for compatibility with
calendar.el and decoded-time-* functions.

Some examples:

1) Printed: FREQ=DAILY;COUNT=10;INTERVAL=2
   Meaning: 10 occurrences that occur every other day
   Read: ((FREQ DAILY)
          (COUNT 10)
          (INTERVAL 2))

2) Printed: FREQ=YEARLY;UNTIL=20000131T140000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA
   Meaning: Every day in January of every year until 2000/01/31 at 14:00 UTC
   Read: ((FREQ YEARLY)
          (UNTIL (0 0 14 31 1 2000 1 -1 0))
          (BYMONTH (1))
          (BYDAY (0 1 2 3 4 5 6)))

3) Printed: FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2
   Meaning: Every month on the second-to-last weekday of the month
   Read: ((FREQ MONTHLY)
          (BYDAY (1 2 3 4 5))
          (BYSETPOS (-2)))

Notice that singleton values are still wrapped in a list when the
KEY accepts a list of values, but not when the KEY always has a
single (e.g. integer) value."
  '(satisfies ical:recur-value-p)
  (ical:semicolon-list ical:recur-rule-part)
  :reader ical:read-recur
  :printer ical:print-recur
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.10")

(defun ical:recur-freq (recur-value)
  "Return the frequency in RECUR-VALUE"
  (car (alist-get 'FREQ recur-value)))

(defun ical:recur-interval-size (recur-value)
  "Return the interval size specified in RECUR-VALUE, or the default
of 1."
  (or (car (alist-get 'INTERVAL recur-value)) 1))

(defun ical:recur-until (recur-value)
  "Return the UNTIL date(-time) in RECUR-VALUE"
  (car (alist-get 'UNTIL recur-value)))

(defun ical:recur-count (recur-value)
  "Return the COUNT in RECUR-VALUE"
  (car (alist-get 'COUNT recur-value)))

(defun ical:recur-weekstart (recur-value)
  "Return the weekday which starts the work week specified in
RECUR-VALUE, or the default (1 = Monday)"
  (or (car (alist-get 'WKST recur-value)) 1))

(defun ical:recur-by* (byunit recur-value)
  "Return the values in the BYUNIT clause in RECUR-VALUE.
BYUNIT should be a symbol: \\='BYMONTH, \\='BYDAY, etc.
See `icalendar-recur' for all the possible BYUNIT values."
  (car (alist-get byunit recur-value)))

;;;; 3.3.11 Text
(rx-define ical:escaped-char
   (seq ?\\ (or ?\\ ?\; ?, ?N ?n)))

(rx-define ical:text-safe-char
  (not (or ?\" ?\; ?: ?\\ ?, ical:control))) ;; TODO: is this correct?

(defun ical:text-region-p (val)
  "Return t if VAL represents a region of text."
  (and (listp val)
       (markerp (car val))
       (not (null (marker-buffer (car val))))
       (markerp (cdr val))))

(defun ical:make-text-region (&optional buffer begin end)
  "Return an object that represents the region of text in BUFFER
between BEGIN and END. BUFFER defaults to the current buffer, and
BEGIN and END default to point and mark in BUFFER."
  (let ((buf (or buffer (current-buffer)))
        (b (make-marker))
        (e (make-marker)))
    (with-current-buffer buf
      (set-marker b (or begin (min (point) (mark))) buf)
      (set-marker e (or end (max (point) (mark))))
      (cons b e))))

(defsubst ical:text-region-begin (r)
  "Return the marker at the beginning of the text region R"
  (car r))

(defsubst ical:text-region-end (r)
  "Return the marker at the end of the text region R"
  (cdr r))

(defun ical:unescape-text-in-region (begin end)
 "Unescape the text between BEGIN and END, replacing
literal '\\n' and '\\N' with newline, and removing backslashes that escape
commas, semicolons, and backslashes."
 (with-restriction begin end
   (save-excursion
    (replace-string-in-region "\\N" "\n" (point-min) (point-max))
    (replace-string-in-region "\\n" "\n" (point-min) (point-max))
    (replace-string-in-region "\\," "," (point-min) (point-max))
    (replace-string-in-region "\\;" ";" (point-min) (point-max)))
    (replace-string-in-region (concat "\\" "\\") "\\" (point-min) (point-max))))

(defun ical:unescape-text-string (s)
 "Unescape the text in string S, replacing literal '\\n' and '\\N'
with newline, and removing backslashes that escape commas, semicolons
and backslashes."
  (with-temp-buffer
    (insert s)
    (ical:unescape-text-in-region (point-min) (point-max))
    (buffer-string)))

(defun ical:escape-text-in-region (begin end)
  "Escape the text between BEGIN and END, replacing newlines with
literal '\\n', and escaping commas, semicolons and backslashes with a
backslash."
 (with-restriction begin end
  (save-excursion
    ;; replace backslashes first, so the ones introduced when
    ;; escaping other characters don't end up double-escaped:
    (replace-string-in-region "\\" (concat "\\" "\\") (point-min) (point-max))
    (replace-string-in-region "\n" "\\n" (point-min) (point-max))
    (replace-string-in-region "," "\\," (point-min) (point-max))
    (replace-string-in-region ";" "\\;" (point-min) (point-max)))))

(defun ical:escape-text-string (s)
  "Escape the text in S, replacing newlines with '\\n', and escaping
commas, semicolons, and backslashes with a backslash."
  (with-temp-buffer
    (insert s)
    (ical:escape-text-in-region (point-min) (point-max))
    (buffer-string)))

(defun ical:read-text (s)
  "Read an `icalendar-text' value from a string S.
S should be a match against rx `icalendar-text'."
  (ical:unescape-text-string s))

(defun ical:print-text (val)
  "Serialize an iCalendar text value. VAL may be a string or a text
region (see `icalendar-make-text-region'). The text will be escaped before
printing. If VAL is a region, the text it contains will not be
modified; it is copied before escaping."
  (if (stringp val)
      (ical:escape-text-string val)
    ;; val is a region, so copy and escape its contents:
    (let* ((beg (ical:text-region-begin val))
           (buf (marker-buffer beg))
           (end (ical:text-region-end val)))
      (with-temp-buffer
        (insert-buffer-substring buf (marker-position beg) (marker-position end))
        (ical:escape-text-in-region (point-min) (point-max))
        (buffer-string)))))

(defun ical:text-to-string (node)
  "Return the value of an `icalendar-text' NODE as a string.
The returned string is *not* escaped. For that, see `icalendar-print-text'."
  (let ((val (ical:ast-node-value node)))
    (if (stringp val)
           val
      (let* ((beg (ical:text-region-begin val))
             (buf (marker-buffer beg))
             (end (ical:text-region-end val)))
        (with-current-buffer buf
          (buffer-substring (marker-position beg) (marker-position end)))))))

;; TODO: would it be useful to add a third representation, namely a
;; function or thunk? So that e.g. Org can pre-process its own syntax
;; and return a plain text string to use in the description?
(ical:define-type ical:text "TEXT"
   "Type for Text values.

Text values can be represented in Elisp in two ways: as strings,
or as buffer regions. For values which aren't expected to change,
such as property values in a text/calendar email attachment, use
strings. For values which are user-editable and might change
between parsing and serializing to iCalendar format, use a
region. In that case, a text value contains two markers BEGIN and
END which mark the bounds of the region. See
`icalendar-make-text-region' to create such values, and
`icalendar-text-region-begin' and `icalendar-text-region-end' to
access the markers.

Certain characters in text values are required to be escaped by
the iCalendar standard. These characters should NOT be
pre-escaped when inserting them into the parse tree. Instead,
`icalendar-print-text' takes care of escaping text values, and
`icalendar-read-text' takes care of unescaping them, when parsing and
printing iCalendar data."
  '(or string (satisfies ical:text-region-p))
  (zero-or-more (or ical:text-safe-char ?: ?\" ical:escaped-char))
  :reader ical:read-text
  :printer ical:print-text
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.11")

;; 3.3.12 Time - Defined above

;;;; 3.3.13 URI
;; see https://www.rfc-editor.org/rfc/rfc3986#section-3
(require 'icalendar-uri-schemes)
(rx-define ical:uri-with-scheme
  ;; Group 11: URI scheme; see icalendar-uri-schemes.el
  ;; Group 12: rest of URI after ":"
  ;; This regex mostly just scans for all characters allowed by
  ;; RFC3986. We make an effort to parse the scheme, even though this
  ;; is an open-ended list, because otherwise the regex is either too
  ;; permissive or too complicated to be useful. (ical:binary, in
  ;; particular, matches a subset of the characters allowed in a URI).
  ;; TODO: should we parse more structure here?
  (seq (group-n 11 ical:uri-scheme)
       ":"
       (group-n 12
         (one-or-more
          (any alnum ?- ?. ?_ ?~                   ; unreserved chars
               ?: ?/ ?? ?# ?\[ ?\] ?@              ; gen-delims
               ?! ?$ ?& ?' ?\( ?\) ?* ?+ ?, ?\; ?= ; sub-delims
               ?%)))))                             ; for %-encoding

(ical:define-type ical:uri "URI"
   "Type for URI values.

The parsed and printed representations are the same: a URI string."
   '(satisfies ical:match-uri-value)
   ical:uri-with-scheme
   :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.13")

;;;; 3.3.3 Calendar User Address
(ical:define-type ical:cal-address "CAL-ADDRESS"
   "Type for Calendar User Address values.

The parsed and printed representations are the same: a URI string.
Typically, this should be a mailto: URI.

RFC5545 says: '*When used to address an Internet email transport
  address* for a calendar user, the value MUST be a mailto URI,
  as defined by [RFC2368]'

Since it is unclear whether there are Calendar User Address values
which are not used to address email, this type does not enforce the use
of the mailto: scheme, but be prepared for problems if you create
values of this type with any other scheme."
   '(and string (satisfies ical:match-cal-address-value))
   ical:uri-with-scheme
   :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.3")

;;;; 3.3.14 UTC Offset
(defun ical:read-utc-offset (s)
  "Read a UTC offset from a string.
S should be a match against rx `icalendar-utc-offset'"
  (let ((sign (if (equal (substring s 0 1) "-") -1 1))
        (nhours (string-to-number (substring s 1 3)))
        (nminutes (string-to-number (substring s 3 5)))
        (nseconds (if (length= s 7)
                      (string-to-number (substring s 5 7))
                    0)))
    (* sign (+ nseconds (* 60 nminutes) (* 60 60 nhours)))))

(defun ical:print-utc-offset (utcoff)
  "Serialize a UTC offset to a string"
  (let* ((sign (if (< utcoff 0) "-" "+"))
         (absoff (abs utcoff))
         (nhours (/ absoff (* 60 60)))
         (no-hours (mod absoff (* 60 60)))
         (nminutes (/ no-hours 60))
         (nseconds (mod no-hours 60)))
    (if (zerop nseconds)
        (format "%s%02d%02d" sign nhours nminutes)
      (format "%s%02d%02d%02d" sign nhours nminutes nseconds))))

(ical:define-type ical:utc-offset "UTC-OFFSET"
  "Type for UTC Offset values.

When printed, a sign followed by a string of digits, like +HHMM
or -HHMMSS. When read, an integer representing the number of
seconds offset from UTC. This representation is for compatibility
with `decode-time' and related functions."
  '(integer -999999 999999)
  (seq (or ?+ ?-) ; + is not optional for positive values!
       (= 4 digit) ; HHMM
       (zero-or-one (= 2 digit))) ; SS
  :reader ical:read-utc-offset
  :printer ical:print-utc-offset
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.14")

\f
;;; Section 3.2: Property Parameters

(defconst ical:param-types nil ;; populated by ical:define-param
  "Alist mapping printed parameter names to type symbols")

(defun ical:maybe-quote-param-value (s &optional always)
  "Add quotes around param value string S if required. If ALWAYS is non-nil,
add quotes to S regardless of its contents"
  (if (or always
          (not (string-match (rx ical:paramtext) s))
          (< (match-end 0) (length s)))
      (concat "\"" s "\"")
    s))

(defun ical:read-param-value (type s)
  "Read a value for a parameter of type TYPE from a string S.
S should have already been matched against the regex for TYPE and
the match data should be available to this function. Returns a
syntax node of type TYPE containing the read value.

If TYPE accepts a list of values, S will be split on the list
separator for TYPE and read individually."
  (let* ((value-type (get type 'ical:value-type)) ; if nil, value is just a string
         (value-regex (when (get type 'ical:value-rx)
                         (rx-to-string (get type 'ical:value-rx))))
         (list-sep (get type 'ical:list-sep))
         (substitute-val (get type 'ical:substitute-value))
         (unrecognized-val (match-string 5)) ; see :unrecognized in define-param
         (raw-val (if unrecognized-val substitute-val s))
         (one-val-reader (if (ical:value-type-symbol-p value-type)
                             (lambda (s) (ical:read-value-node value-type s))
                           #'identity)) ; value is just a string
         ;; values may be quoted even if :quoted does not require it,
         ;; so they need to be stripped of quotes. read-list-of does
         ;; this by default; in the single value case, use string-trim
         (read-val (if list-sep
                       (ical:read-list-with one-val-reader raw-val
                                            value-regex list-sep)
                     (funcall one-val-reader
                              (string-trim raw-val "\"" "\"")))))
    (ical:make-ast-node type
                        :value read-val
                        :original-value unrecognized-val)))

(defun ical:parse-param-value (type limit)
  "Parse the value for a parameter of type TYPE from point up to LIMIT.
TYPE should be a type symbol for an iCalendar parameter type.
This function expects point to be at the start of the value
string, after the parameter name and the equals sign. Returns a
syntax node representing the parameter."
  (let ((full-value-regex (rx-to-string (get type 'ical:full-value-rx))))
    (unless (re-search-forward full-value-regex limit t)
      (signal 'ical:parse-error
              (list (format "Unable to parse `%s' value between %d and %d"
                            type (point) limit))))
    (when (match-string 3)
      (signal 'ical:parse-error
              (list (format "Invalid value for `%s' parameter" type)
                    (match-string 3))))

    (let ((value-begin (match-beginning 2))
          (value-end (match-end 2))
          (node (ical:read-param-value type (match-string 2))))
      (ical:ast-node-meta-set node :buffer (current-buffer))
      ;; :begin must be set by parse-params
      (ical:ast-node-meta-set node :value-begin value-begin)
      (ical:ast-node-meta-set node :value-end value-end)
      (ical:ast-node-meta-set node :end value-end)

      node)))

(defun ical:parse-params (limit)
  "Parse the parameter string of the current property, up to LIMIT.
Point should be at the \";\" at the start of the first parameter.
Returns a list of parameters, which may be nil if none are present.
After parsing, point is at the end of the parameter string and the
start of the property value string."
  (let ((params nil))
    (rx-let ((ical:param-start (seq ";" (group-n 1 ical:param-name) "=")))
      (while (re-search-forward (rx ical:param-start) limit t)
        (when-let* ((begin (match-beginning 1))
                    (param-name (match-string 1))
                    (param-type (or (alist-get (upcase param-name)
                                               ical:param-types
                                               nil nil #'equal)
                                    'ical:otherparam))
                    (param-node (ical:parse-param-value param-type limit)))
          (ical:ast-node-meta-set param-node :begin begin)
          ;; store the original param name if we didn't recognize it:
          (when (eq param-type 'ical:otherparam)
            (ical:ast-node-meta-set param-node :original-name param-name))
          (push param-node params))))
    (nreverse params)))

(defun ical:print-param-node (node)
  "Serialize a parameter syntax node NODE to a string.
NODE should be a syntax node whose type is an iCalendar
parameter type."
  (let* ((param-type (ical:ast-node-type node))
         (list-sep (get param-type 'ical:list-sep))

         (val/s (ical:ast-node-value node))
         (printed (if (and list-sep (listp val/s))
                      (mapcar #'ical:default-value-printer val/s)
                    (ical:default-value-printer val/s)))
         ;; add quotes to each value as needed, even if :quoted
         ;; does not require it:
         (must-quote (get param-type 'ical:is-quoted))
         (quoted (if (listp printed)
                     (mapcar
                      (lambda (v) (ical:maybe-quote-param-value v must-quote))
                      printed)
                   (ical:maybe-quote-param-value printed must-quote)))
         (val-str (or (ical:ast-node-meta-get node :original-value)
                      (if (and list-sep (listp quoted))
                          (string-join quoted list-sep)
                        quoted)))
         (param-name (car (rassq param-type ical:param-types)))
         (name-str (or param-name
                       ;; set by parse-params for unrecognized params:
                       (ical:ast-node-meta-get node :original-name))))
    (format ";%s=%s" name-str val-str)))

(defun ical:print-params (param-nodes)
  "Print the property parameter nodes in PARAM-NODES. Returns the
printed parameter list as a string."
  (apply #'concat
    (mapcar #'ical:print-param-node
            param-nodes)))

;; Parameter definitions in RFC5545:

(ical:define-param ical:altrepparam "ALTREP"
  "Alternate text representation (URI)"
  ical:uri
  :quoted t
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.1")

(ical:define-param ical:cnparam "CN"
  "Common Name"
  ical:param-value
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.2")

(ical:define-param ical:cutypeparam "CUTYPE"
  "Calendar User Type"
  (or "INDIVIDUAL"
      "GROUP"
      "RESOURCE"
      "ROOM"
      "UNKNOWN"
      (group-n 5
        (or ical:x-name ical:iana-token)))
  :default "INDIVIDUAL"
  ;; "Applications MUST treat x-name and iana-token values they
  ;; don't recognize the same way as they would the UNKNOWN
  ;; value":
  :unrecognized "UNKNOWN"
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.3")

(ical:define-param ical:delfromparam "DELEGATED-FROM"
  "Delegators.

This is a comma-separated list of quoted `icalendar-cal-address' URIs,
typically specified on the `icalendar-attendee' property. The users in
this list have delegated their participation to the user which is
the value of the property."
  ical:cal-address
  :quoted t
  :list-sep ","
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.4")

(ical:define-param ical:deltoparam "DELEGATED-TO"
  "Delegatees.

This is a comma-separated list of quoted `icalendar-cal-address' URIs,
typically specified on the `icalendar-attendee' property. The users in
this list have been delegated to participate by the user which is
the value of the property."
  ical:cal-address
  :quoted t
  :list-sep ","
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.5")

(ical:define-param ical:dirparam "DIR"
  "Directory Entry Reference.

This parameter may be specified on properties with a
`icalendar-cal-address' value type. It is a quoted URI which specifies
a reference to a directory entry associated with the calendar
user which is the value of the property."
   ical:uri
   :quoted t
   :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.6")

(ical:define-param ical:encodingparam "ENCODING"
  "Inline Encoding, either \"8BIT\" (text, default) or \"BASE64\" (binary).

If \"BASE64\", the property value is base64-encoded binary data.
This parameter must be specified if the `icalendar-valuetypeparam'
is \"BINARY\"."
  (or "8BIT" "BASE64")
  :default "8BIT"
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.7")

(rx-define ical:mimetype
  (seq ical:mimetype-regname "/" ical:mimetype-regname))

;; from https://www.rfc-editor.org/rfc/rfc4288#section-4.2:
(rx-define ical:mimetype-regname
  (** 1 127 (any alnum ?! ?# ?$ ?& ?. ?+ ?- ?^ ?_)))

(ical:define-param ical:fmttypeparam "FMTTYPE"
  "Format Type (Mimetype per RFC4288)

Specifies the media type of the object referenced in the property value,
for example \"text/plain\" or \"text/html\".
Valid media types are defined in RFC4288; see
URL `https://www.rfc-editor.org/rfc/rfc4288#section-4.2'"
  ical:mimetype
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.8")

(ical:define-param ical:fbtypeparam "FBTYPE"
  "Free/Busy Time Type. Default is \"BUSY\".

RFC5545 gives the following meanings to the values:

FREE: the time interval is free for scheduling.
BUSY: the time interval is busy because one or more events have
  been scheduled for that interval.
BUSY-UNAVAILABLE: the time interval is busy and that the interval
  can not be scheduled.
BUSY-TENTATIVE: the time interval is busy because one or more
  events have been tentatively scheduled for that interval.
Other values are treated like BUSY."
  (or "FREE"
      "BUSY-UNAVAILABLE"
      "BUSY-TENTATIVE"
      "BUSY"
      ical:x-name
      ical:iana-token)
  :default "BUSY"
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.9")

;; TODO: see https://www.rfc-editor.org/rfc/rfc5646#section-2.1
(rx-define ical:rfc5646-lang
  (one-or-more (any alnum ?-)))

(ical:define-param ical:languageparam "LANGUAGE"
  "Language tag (per RFC5646)

This parameter specifies the language of the property value as a
language tag, for example \"en-US\" for US English or \"no\" for
Norwegian. Valid language tags are defined in RFC5646; see
URL `https://www.rfc-editor.org/rfc/rfc5646'"
  ical:rfc5646-lang
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.10")

(ical:define-param ical:memberparam "MEMBER"
  "Group or List Membership.

This is a comma-separated list of quoted `icalendar-cal-address'
values. These are addresses of groups or lists of which the user
in the property value is a member."
  ical:cal-address
  :quoted t
  :list-sep ","
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.11")

(ical:define-param ical:partstatparam "PARTSTAT"
  "Participation status.

The value specifies the participation status of the calendar user
in the property value. They have different interpretations
depending on whether they occur in a VEVENT, VTODO or VJOURNAL
component. RFC5545 gives the values the following meanings:

NEEDS-ACTION (all): needs action by the user
ACCEPTED (all): accepted by the user
DECLINED (all): declined by the user
TENTATIVE (VEVENT, VTODO): tentatively accepted by the user
DELEGATED (VEVENT, VTODO): delegated by the user
COMPLETED (VTODO): completed at the `icalendar-date-time' in the
  VTODO's `icalendar-completed' property
IN-PROCESS (VTODO): in the process of being completed"
  (or "NEEDS-ACTION"
      "ACCEPTED"
      "DECLINED"
      "TENTATIVE"
      "DELEGATED"
      "COMPLETED"
      "IN-PROCESS"
      (group-n 5 (or ical:x-name
                     ical:iana-token)))
  ;; "Applications MUST treat x-name and iana-token values
  ;; they don't recognize the same way as they would the
  ;; NEEDS-ACTION value."
  :default "NEEDS-ACTION"
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.12")

(ical:define-param ical:rangeparam "RANGE"
  "Recurrence Identifier Range.

Specifies the effective range of recurrence instances of the property's value.
The value \"THISANDFUTURE\" is the only value compliant with RFC5545;
legacy applications might also produce \"THISANDPRIOR\"."
  "THISANDFUTURE"
  :default "THISANDFUTURE"
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.13")

(ical:define-param ical:trigrelparam "RELATED"
  "Alarm Trigger Relationship.

This parameter may be specified on properties whose values give
an alarm trigger as an `icalendar-duration'. If the parameter
value is \"START\" (the default), the alarm triggers relative to
the start of the component; similarly for \"END\"."
  (or "START" "END")
  :default "START"
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.14")

(ical:define-param ical:reltypeparam "RELTYPE"
  "Relationship type.

This parameter specifies a hierarchical relationship between the
calendar component referenced in a `icalendar-related-to'
property and the calendar component in which it occurs.
\"PARENT\" means the referenced component is superior to this
one, \"CHILD\" that the referenced component is subordinate to
this one, and \"SIBLING\" means they are peers."
  (or "PARENT"
      "CHILD"
      "SIBLING"
      (group-n 5 (or ical:x-name
                     ical:iana-token)))
  ;; "Applications MUST treat x-name and iana-token values they don't
  ;; recognize the same way as they would the PARENT value."
  :default "PARENT"
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.15")

(ical:define-param ical:roleparam "ROLE"
  "Participation role.

This parameter specifies the participation role of the calendar
user in the property value. RFC5545 gives the parameter values
the following meanings:
CHAIR: chair of the calendar entity
REQ-PARTICIPANT (default): user's participation is required
OPT-PARTICIPANT: user's participation is optional
NON-PARTICIPANT: user is copied for information purposes only"
  (or "CHAIR"
      "REQ-PARTICIPANT"
      "OPT-PARTICIPANT"
      "NON-PARTICIPANT"
      (group-n 5 (or ical:x-name
                     ical:iana-token)))
  ;; "Applications MUST treat x-name and iana-token values
  ;; they don't recognize the same way as they would the
  ;; REQ-PARTICIPANT value."
  :default "REQ-PARTICIPANT"
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.16")

(ical:define-param ical:rsvpparam "RSVP"
  "RSVP expectation.

This parameter is an `icalendar-boolean' which specifies whether
the calendar user in the property value is expected to reply to
the Organizer of a VEVENT or VTODO."
  ical:boolean
  :default "FALSE"
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.17")

(ical:define-param ical:sentbyparam "SENT-BY"
  "Sent by.

This parameter specifies a calendar user that is acting on behalf
of the user in the property value."
  ;; "The parameter value MUST be a mailto URI as defined in [RFC2368]"
  ;; Weirdly, this is the only place in the standard I've seen "mailto:"
  ;; be *required* for a cal-address. We ignore this requirement for
  ;; now, because coding around the exception is not worth it: it
  ;; requires some hackery to work around the fact that two different
  ;; types, the looser and the more stringent cal-address, would need to
  ;; have the same print name.
  ical:cal-address
  :quoted t
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.18")

(ical:define-param ical:tzidparam "TZID"
  "Time Zone identifier.

This parameter identifies the VTIMEZONE component in the calendar
which should be used to interpret the time value given in the
property. The value of this parameter must be equal to the value
of the TZID property in that VTIMEZONE component; there must be
exactly one such component for every unique value of this
parameter in the calendar."
  ;; TODO: "This parameter MUST be specified on the "DTSTART","DTEND",
  ;; "DUE", "EXDATE", and "RDATE" properties when either a DATE-TIME
  ;; or TIME value type is specified and when the value is neither a
  ;; UTC or a "floating" time."
  ;; TODO: "The "TZID" property parameter MUST NOT be applied to DATE
  ;; properties and DATE-TIME or TIME properties whose time values are
  ;; specified in UTC."
  (seq (zero-or-one "/") ical:paramtext)
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.19")

(defun ical:read-value-type (s)
  "Read a value type from string S.
S should contain the printed representation of a value type in a \"VALUE=...\"
property parameter. If S represents a known type in `icalendar-value-types',
it is read as the associated type symbol. Otherwise S is returned unchanged."
  (let ((type-assoc (assoc s ical:value-types)))
    (if type-assoc
        (cdr type-assoc)
      s)))

(defun ical:print-value-type (type)
  "Print a value type TYPE.
TYPE should be an iCalendar type symbol naming a known value type
defined with `icalendar-define-type', or a string naming an
unknown type. If it is a symbol, return the associated printed
representation for the type from `icalendar-value-types'.
Otherwise return TYPE."
  (if (symbolp type)
      (car (rassq type ical:value-types))
    type))

(ical:define-type ical:printed-value-type nil
  "Type to represent values of the `icalendar-valuetypeparam' parameter.

When read, if the type named by the parameter is a known value
type in `icalendar-value-types', it is represented as a type
symbol for that value type. If it is an unknown value type, it is
represented as a string. When printed, a string is returned
unchanged; a type symbol is printed as the associated name in
`icalendar-value-types'.

This is not a type defined by RFC5545; it is defined here to
facilitate parsing of the `icalendar-valuetypeparam' parameter."
  '(or string (satisfies ical:printable-value-type-symbol-p))
  (or "BINARY"
      "BOOLEAN"
      "CAL-ADDRESS"
      "DATE-TIME"
      "DATE"
      "DURATION"
      "FLOAT"
      "INTEGER"
      "PERIOD"
      "RECUR"
      "TEXT"
      "TIME"
      "URI"
      "UTC-OFFSET"
      ;; Note: "Applications MUST preserve the value data for x-name
      ;; and iana-token values that they don't recognize without
      ;; attempting to interpret or parse the value data." So in this
      ;; case we don't specify :default or :unrecognized in the
      ;; parameter definition, and we don't put the value in group 5;
      ;; the reader will just preserve whatever string matches here.
      ical:x-name
      ical:iana-token)
  :reader ical:read-value-type
  :printer ical:print-value-type)

(ical:define-param ical:valuetypeparam "VALUE"
  "Property value data type.

This parameter is used to specify the value type of the
containing property's value, if it is not of the default value
type."
  ical:printed-value-type
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.20")

(ical:define-param ical:otherparam nil ; don't add to ical:param-types
  "Parameter with an unknown name.

This is not a parameter type defined by RFC5545; it represents
parameters with an unknown name (matching rx `icalendar-param-name')
whose values must be parsed and preserved but not further
interpreted."
  ical:param-value)

(rx-define ical:other-param-safe
  ;; we use this rx to skip params when matching properties and
  ;; their values. Thus we *don't* capture the param names and param values
  ;; in numbered groups here, which would clobber the groups of the enclosing
  ;; expression.
  (seq ";"
       (or ical:iana-token ical:x-name)
       "="
       (ical:comma-list ical:param-value)))


;;; Properties:

(defconst ical:property-types nil ;; populated by ical:define-property
  "Alist mapping printed property names to type symbols")

(defun ical:read-property-value (type s &optional params)
    "Read a value for the property type TYPE from a string S.

TYPE should be a type symbol for an iCalendar property type
defined with `icalendar-define-property'. The property value is
assumed to be of TYPE's default value type, unless an
`icalendar-valuetypeparam' parameter appears in PARAMS, in which
case a value of that type will be read. S should have already
been matched against TYPE's value regex and the match data should
be available to this function. Returns a property syntax node of
type TYPE containing the read value and the list of PARAMS.

If TYPE accepts lists of values, they will be split from S on the
list separator and read separately."
  (let* ((value-type (or (ical:value-type-from-params params)
                         (get type 'ical:default-type)))
         (list-sep (get type 'ical:list-sep))
         (unrecognized-val (match-string 5))
         (raw-val (if unrecognized-val
                      (get type 'ical:substitute-value)
                    s))
         (value (if list-sep
                    (ical:read-list-of value-type raw-val list-sep)
                  (ical:read-value-node value-type raw-val))))
    (ical:make-ast-node type
                        :value value
                        :original-value unrecognized-val
                        :children params)))

(defun ical:parse-property-value (type limit &optional params)
  "Parse a value for the property type TYPE from point up to LIMIT.
This function expects point to be at the start of the value
expression, after \"PROPERTY-NAME[PARAM...]:\". Returns a syntax
node of type TYPE containing the parsed value and the list of
PARAMS."
  (let ((full-value-regex (rx-to-string (get type 'ical:full-value-rx))))

    (unless (re-search-forward full-value-regex limit t)
      (signal 'ical:parse-error
              (list (format "Unable to parse `%s' property value between %d and %d"
                            type (point) limit))))

    (when (match-string 3)
      (signal 'ical:parse-error
              (list (format "Invalid value for `%s' property" type)
                    (match-string 3))))

    (let* ((value-begin (match-beginning 2))
           (value-end (match-end 2))
           (end value-end)
           (node (ical:read-property-value type (match-string 2) params)))
      (ical:ast-node-meta-set node :buffer (current-buffer))
      ;; 'begin must be set by parse-property
      (ical:ast-node-meta-set node :value-begin value-begin)
      (ical:ast-node-meta-set node :value-end value-end)
      (ical:ast-node-meta-set node :end end)

      node)))

(defun ical:print-property-node (node)
  "Serialize a property syntax node NODE to a string."
  (ical:maybe-add-value-param node)
  (let* ((type (ical:ast-node-type node))
         (list-sep (get type 'ical:list-sep))
         (property-name (car (rassq type ical:property-types)))
         (params (ical:ast-node-children node))
         (value (ical:ast-node-value node))
         (value-str
          (or (ical:ast-node-meta-get node :original-value)
              (if list-sep
                  (string-join (mapcar #'ical:default-value-printer value)
                               list-sep)
                (ical:default-value-printer value))))
         (name-str (or property-name
                       (ical:ast-node-meta-get node :original-name))))

    (unless (and (stringp name-str)
                 (length> name-str 0))
      (signal 'ical:print-error
              (list (format "Unknown property name for type `%s'" type)
                    type node)))

    (concat name-str
            (ical:print-params params)
            ":"
            value-str
            ;; TODO: make line ending sensitive to coding system?
            "\r\n")))

(defun ical:maybe-add-value-param (property-node)
  "If the type of PROPERTY-NODE's value is not the same as its
default-type, check that its parameter list contains an
`icalendar-valuetypeparam' specifying that type as the type for
the value. If not, add such a parameter to PROPERTY-NODE's list
of parameters. Returns the possibly-modified PROPERTY-NODE.

If the parameter list already contains a value type parameter for
a type other than the property value's type, an
`icalendar-validation-error' is signaled.

If PROPERTY's value is a list, the type of the first element will
be assumed to be the type for all the values in the list. If the
list is empty, no change will be made to PROPERTY's parameters."
  (catch 'no-value-type
    (let* ((property-type (ical:ast-node-type property-node))
           (value/s (ical:ast-node-value property-node))
           (value (if (and (ical:expects-list-of-values-p property-type)
                           (listp value/s))
                      (car value/s)
                    value/s))
           (value-type (cond ((stringp value) 'ical:text)
                             ((ical:ast-node-p value)
                              (ical:ast-node-type value))
                             ;; if we can't determine a type from the value, bail:
                             (t (throw 'no-value-type property-node))))
           (params (ical:ast-node-children property-node))
           (expected-type (ical:value-type-from-params params)))

      (when (not (eq value-type (get property-type 'ical:default-type)))
        (if expected-type
            (when (not (eq value-type expected-type))
              (signal 'ical:validation-error
                      (list (format (concat "Mismatching VALUE parameter. "
                                            "VALUE specifies %s but "
                                            "property value has type %s")
                                    expected-type value-type))))
          ;; the value isn't of the default type, but we didn't find a
          ;; VALUE parameter, so add one now:
          (let* ((valuetype-param
                  (ical:make-ast-node 'ical:valuetypeparam
                                      :value (ical:make-ast-node
                                              'ical:printed-value-type
                                              :value value-type)))
                 (new-params (cons valuetype-param
                                   (ical:ast-node-children property-node))))
            (setf (nth 3 property-node) new-params))))

      ;; Return the modified property node:
      property-node)))

(defun ical:value-type-from-params (params)
  "If there is an `icalendar-valuetypeparam' in PARAMS, return the
type symbol associated with the value type it specifies."
  (catch 'found
    (dolist (param params)
      (when (ical:value-param-p param)
        (let ((type (ical:ast-node-value
                     (ical:ast-node-value param))))
          (throw 'found type))))))

(defun ical:parse-property (limit)
  "Parse the current property, up to LIMIT. Point should be at the
beginning of a property line; LIMIT should be the position at the
end of the line.

Returns a syntax node for the property. After parsing, point is
at the beginning of the next content line."
  (rx-let ((ical:property-start (seq line-start
                                     (group-n 1 ical:name))))
    (let ((line-begin nil)
          (line-end nil)
          (property-name nil)
          (params nil))

      ;; Property name
      (unless (re-search-forward (rx ical:property-start) limit t)
        (signal 'ical:parse-error
                (list (format (concat "Malformed property at line %d, position %d:"
                                      "could not match property name")
                              (line-number-at-pos (point))
                              (line-beginning-position)))))

      (setq property-name (match-string 1))
      (setq line-begin (line-beginning-position))
      (setq line-end (line-end-position))

      ;; Parameters
      (when (looking-at ";")
        (setq params (ical:parse-params line-end)))
      ;; TODO: param validation?

      (unless (looking-at ":")
        (signal 'ical:parse-error
                (list (format (concat "Malformed property at line %d, position %d:"
                                      "missing colon before value")
                              (line-number-at-pos (point))
                              (point)))))
      (forward-char)

      ;; Value
      (let* ((known-type (alist-get (upcase property-name)
                                    ical:property-types
                                    nil nil #'equal))
             (property-type (or known-type 'ical:other-property))
             (node (ical:parse-property-value property-type limit params)))

        ;; sanity check, since e.g. invalid base64 data might not
        ;; match all the way to the end of the line, as test
        ;; rfc5545-sec3.1.3/2 initially revealed
        (unless (eql (point) (line-end-position))
          (signal 'ical:parse-error
                  (list (format "Property value did not consume line %d: %s"
                                (line-number-at-pos (point))
                                (ical:default-value-printer
                                 (ical:ast-node-value node))))))

        ;; Set point up for the next property parser:
        (while (not (bolp))
          (forward-char))

        ;; value, children are set in ical:read-property-value,
        ;; value-begin, value-end, end in ical:parse-property-value.
        ;; begin and original-name are only available here:
        (ical:ast-node-meta-set node :begin line-begin)
        (when (eq property-type 'ical:other-property)
          (ical:ast-node-meta-set node :original-name property-name))

        ;; Return the syntax node
        node))))

\f
;;;; Section 3.7: Calendar Properties
(ical:define-property ical:calscale "CALSCALE"
  "Calendar scale.

This property specifies the time scale of an
`icalendar-vcalendar' object. The only scale defined by RFC5545
is \"GREGORIAN\", which is the default."
  ;; only allowed value:
  "GREGORIAN"
  :default "GREGORIAN"
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.7.1")

(ical:define-property ical:method "METHOD"
  "Method for a scheduling request.

When an `icalendar-vcalendar' is sent in a MIME message, this property
specifies the semantics of the request in the message: e.g. it is
a request to publish the calendar object, or a reply to an
invitation. This property and the MIME message's \"method\"
parameter value must be the same.

RFC5545 does not define any methods, but RFC5546 does; see
URL `https://www.rfc-editor.org/rfc/rfc5546.html#section-3.2'"
  ;; TODO: implement methods in RFC5546?
  ical:iana-token
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.7.2")

(ical:define-property ical:prodid "PRODID"
  "Product Identifier.

This property identifies the program that created an
`icalendar-vcalendar' object. It must be specified exactly once
in a calendar object. Its value should be a globally unique
identifier for the program, though RFC5545 does not specify any
particular way of creating such an identifier."
  ical:text
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.7.3")

(ical:define-property ical:version "VERSION"
  "Version (2.0 corresponds to RFC5545).

This property specifies the version number of the iCalendar
specification to which an `icalendar-vcalendar' object conforms,
and must be specified exactly once in a calendar object. It is
either the string \"2.0\" or a string like MIN;MAX specifying
minimum and maximum versions of future revisions of the
specification."
  (or "2.0"
      ;; minver ";" maxver
      (seq ical:iana-token ?\; ical:iana-token))
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.7.4")

\f
;;;; Section 3.8:
;;;;; Section 3.8.1: Descriptive Component Properties

(ical:define-property ical:attach "ATTACH"
  "Attachment.

This property specifies a file attached to an iCalendar
component, either via a URI, or as encoded binary data. In
`icalendar-valarm' components, it is used to specify the
notification sent by the alarm."
  ;; Groups 11, 12 are used in ical:uri
  (or (group-n 13 ical:uri)
      (group-n 14 ical:binary))
  :default-type ical:uri
  :other-types (ical:binary)
  :child-spec (:zero-or-one (ical:fmttypeparam
                             ical:valuetypeparam
                             ical:encodingparam)
               :zero-or-more (ical:otherparam))
  :other-validator ical:attach-validator
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.1")

(defun ical:attach-validator (node)
  "Additional validator for an `icalendar-attach' NODE.
Checks that NODE has a correct `icalendar-encodingparam' and
`icalendar-valuetypeparam' if its value is an `icalendar-binary'.

This function is called by `icalendar-ast-node-valid-p' for
ATTACH nodes; it is not normally necessary to call it directly."
  (let* ((value-node (ical:ast-node-value node))
         (value-type (ical:ast-node-type value-node))
         (valtypeparam (ical:ast-node-first-child-of 'ical:valuetypeparam node))
         (encodingparam (ical:ast-node-first-child-of 'ical:encodingparam node)))

    (when (eq value-type 'ical:binary)
      (unless (and (ical:ast-node-p valtypeparam)
                   (eq 'ical:binary
                       (ical:ast-node-value ; unwrap inner printed-value-type
                        (ical:ast-node-value valtypeparam))))
        (signal 'ical:validation-error
                (list (concat "`icalendar-binary' attachment requires "
                              "'VALUE=BINARY' parameter")
                      node)))
      (unless (and (ical:ast-node-p encodingparam)
                   (equal "BASE64" (ical:ast-node-value encodingparam)))
        (signal 'ical:validation-error
                (list (concat "`icalendar-binary' attachment requires "
                              "'ENCODING=BASE64' parameter")
                      node))))
    ;; success:
    node))

(ical:define-property ical:categories "CATEGORIES"
  "Categories.

This property lists categories or subtypes of an iCalendar
component for e.g. searching or filtering. The categories can be
any `icalendar-text' value."
  ical:text
  :list-sep ","
  :child-spec (:zero-or-one (ical:languageparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.2")

(ical:define-property ical:class "CLASS"
  "(Access) Classification.

This property specifies the scope of access that the calendar
owner intends for a given component, e.g. public or private."
  (or "PUBLIC"
      "PRIVATE"
      "CONFIDENTIAL"
      (group-n 5
        (or ical:iana-token
            ical:x-name)))
  ;; "If not specified in a component that allows this property, the
  ;; default value is PUBLIC. Applications MUST treat x-name and
  ;; iana-token values they don't recognize the same way as they would
  ;; the PRIVATE value."
  :default "PUBLIC"
  :unrecognized "PRIVATE"
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.3")

(ical:define-property ical:comment "COMMENT"
  "Comment to calendar user.

This property can be specified multiple times in calendar components,
and can contain any `icalendar-text' value."
  ical:text
  :child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.4")

(ical:define-property ical:description "DESCRIPTION"
  "Description.

This property should be a longer, more complete description of
the calendar component than is contained in the
`icalendar-summary' property. In a `icalendar-vjournal'
component, it is used to capture a journal entry, and may be
specified multiple times. Otherwise it may only be specified
once. In an `icalendar-valarm' component, it contains the
notification text for a DISPLAY or EMAIL alarm."
  ical:text
  :child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.5")

(defun ical:read-geo-coordinates (s)
  "Read an `icalendar-geo-coordinates' value from string S"
  (let ((vals (mapcar #'string-to-number (string-split s ";"))))
    (cons (car vals) (cadr vals))))

(defun ical:print-geo-coordinates (val)
  "Serialize an `icalendar-geo-coordinates' value to a string"
  (concat (number-to-string (car val)) ";" (number-to-string (cdr val))))

(defun ical:geo-coordinates-p (val)
  "Return non-nil if VAL is an `icalendar-geo-coordinates' value"
  (and (floatp (car val)) (floatp (cdr val))))

(ical:define-type ical:geo-coordinates nil ; don't add to ical:value-types
  "Type for global positions.

This is not a type defined by RFC5545; it is defined here to
facilitate parsing the `icalendar-geo' property. When printed, it
is represented as a pair of `icalendar-float' values separated by
a semicolon, like LATITUDE;LONGITUDE. When read, it is a dotted
pair of Elisp floats (LATITUDE . LONGITUDE)."
  '(satisfies ical:geo-coordinates-p)
  (seq ical:float ";" ical:float)
  :reader ical:read-geo-coordinates
  :printer ical:print-geo-coordinates)

(ical:define-property ical:geo "GEO"
  "Global position of a component as a pair LATITUDE;LONGITUDE.

Both values are floats representing a number of degrees. The
latitude value is north of the equator if positive, and south of
the equator if negative. The longitude value is east of the prime
meridian if positive, and west of it if negative."
  ical:geo-coordinates
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.6")

(ical:define-property ical:location "LOCATION"
  "Location.

This property describes the intended location or venue of a
component, e.g. a particular room or building, with an
`icalendar-text' value. RFC5545 suggests using the
`icalendar-altrep' parameter on this property to provide more
structured location information."
  ical:text
  :child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.7")

;; TODO: type for percentages?
(ical:define-property ical:percent-complete "PERCENT-COMPLETE"
  "Percent Complete.

This property describes progress toward the completion of an
`icalendar-vtodo' component. It can appear at most once in such a
component. If this TODO is assigned to multiple people, the value
represents the completion state for each person individually. The
value should be between 0 and 100 (though this is not currently
enforced here)."
  ical:integer
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.8")

;; TODO: type for priority values?
(ical:define-property ical:priority "PRIORITY"
  "Priority.

This property describes the priority of a component. 0 means an
undefined priority. Other values range from 1 (highest priority)
to 9 (lowest priority). See RFC5545 for suggestions on how to
represent other priority schemes with this property."
  ical:integer
  :default "0"
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.9")

(ical:define-property ical:resources "RESOURCES"
  "Resources for an activity.

This property is a list of `icalendar-text' values that describe
any resources required or foreseen for the activity represented
by a component, e.g. a projector and screen for a meeting."
  ical:text
  :list-sep ","
  :child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.10")

(ical:define-keyword-type ical:status-keyword nil
  "Keyword value of a STATUS property.

This is not a real type defined by RFC5545; it is defined here to
facilitate parsing that property."
  ;; Note that this type does NOT allow arbitrary text:
  (or "TENTATIVE"
      "CONFIRMED"
      "CANCELLED"
      "NEEDS-ACTION"
      "COMPLETED"
      "IN-PROCESS"
      "DRAFT"
      "FINAL"))

(ical:define-property ical:status "STATUS"
  "Overall status or confirmation.

This property is a keyword used by an Organizer to inform
Attendees about the status of a component, e.g. whether an
`icalendar-vevent' has been cancelled, whether an
`icalendar-vtodo' has been completed, or whether an
`icalendar-vjournal' is still in draft form. It can be specified
at most once on these components."
  ical:status-keyword
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.11")

(ical:define-property ical:summary "SUMMARY"
  "Short summary.

This property provides a short, one-line description of a
component for display purposes. In an EMAIL `icalendar-valarm',
it is used as the subject of the email. A longer description of
the component can be provided in the `icalendar-description'
property."
  ical:text
  :child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.12")

;;;;; Section 3.8.2: Date and Time Component Properties

(ical:define-property ical:completed "COMPLETED"
  "Time completed.

This property is a timestamp that records the date and time when
an `icalendar-vtodo' was actually completed. The value must be an
`icalendar-date-time' with a UTC time."
  ical:date-time
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.1")

(ical:define-property ical:dtend "DTEND"
  "End time of an event or free/busy block.

This property's value specifies when an `icalendar-vevent' or
`icalendar-freebusy' ends. Its value must be of the same type as
the value of the component's corresponding `icalendar-dtstart'
property. The value is a non-inclusive bound, i.e., the value of
this property must be the first time or date *after* the end of
the event or free/busy block."
  (or ical:date-time
      ical:date)
  :default-type ical:date-time
  :other-types (ical:date)
  :child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.2")

(ical:define-property ical:due "DUE"
  "Due date.

This property specifies the date (and possibly time) by which an
`icalendar-todo' item is expected to be completed, i.e., its
deadline. If the component also has an `icalendar-dtstart'
property, the two properties must have the same value type, and
the value of the DTSTART property must be earlier than the value
of this property."
  (or ical:date-time
      ical:date)
  :default-type ical:date-time
  :other-types (ical:date)
  :child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.3")

(ical:define-property ical:dtstart "DTSTART"
  "Start time of a component.

This property's value specifies when a component starts. In an
`icalendar-vevent', it specifies the start of the event. In an
`icalendar-vfreebusy', it specifies the start of the free/busy
block. In `icalendar-standard' and `icalendar-daylight'
sub-components, it defines the start time of a time zone
specification.

It is required in any component with an `icalendar-rrule'
property, and in any `icalendar-vevent' component contained in a
calendar that does not have a `icalendar-method' property.

Its value must be of the same type as the value of the
component's corresponding `icalendar-dtend' property. In an
`icalendar-vtodo' component, it must also be of the same type as
the value of an `icalendar-due' property (if present)."
  (or ical:date-time
      ical:date)
  :default-type ical:date-time
  :other-types (ical:date)
  :child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.4")

(ical:define-property ical:duration "DURATION"
  "Duration.

This property specifies a duration of time for a component.
In an `icalendar-vevent', it can be used to implicitly specify
the end of the event, instead of an explicit `icalendar-dtend'.
In an `icalendar-vtodo', it can likewise be used to implicitly specify
the due date, instead of an explicit `icalendar-due'.
In an `icalendar-valarm', it used to specify the delay period
before the alarm repeats.

If a related `icalendar-dtstart' property has an `icalendar-date'
value, then the duration must be given as a number of weeks or days."
  ical:dur-value
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.5")

(ical:define-property ical:freebusy "FREEBUSY"
  "Free/Busy Times.

This property specifies a list of periods of free or busy time in
an `icalendar-vfreebusy' component. Whether it specifies free or
busy times is determined by its `icalendar-fbtype' parameter. The
times in each period must be in UTC format."
  ical:period
  :list-sep ","
  :child-spec (:zero-or-one (ical:fbtypeparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.6")

(ical:define-property ical:transp "TRANSP"
  "Time Transparency for free/busy searches.

Note that this property only allows two values: \"TRANSPARENT\"
or \"OPAQUE\". An OPAQUE value means that the component consumes
time on a calendar. TRANSPARENT means it does not, and thus is
invisible to free/busy time searches."
  ;; Note that this does NOT allow arbitrary text:
  (or "TRANSPARENT"
      "OPAQUE")
  :default "OPAQUE"
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.7")

;;;;; Section 3.8.3: Time Zone Component Properties

(ical:define-property ical:tzid "TZID"
  "Time Zone Identifier.

This property specifies the unique identifier for a timezone in
an `icalendar-vtimezone' component, and is a required property of
that component. This is an identifier that `icalendar-tzidparam'
parameters in other components may then refer to."
  (seq (zero-or-one "/") ical:text)
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.1")

(ical:define-property ical:tzname "TZNAME"
  "Time Zone Name.

This property specifies a customary name for a time zone in
`icalendar-daylight' and `icalendar-standard' sub-components."
  ical:text
  :child-spec (:zero-or-one (ical:languageparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.2")

(ical:define-property ical:tzoffsetfrom "TZOFFSETFROM"
  "Time Zone Offset (prior to observance).

This property specifies the time zone offset that is in use
*prior to* this time zone observance. It is used to calculate the
absolute time at which the observance takes place. It is a
required property of an `icalendar-vtimezone' component. Positive
numbers indicate time east of the prime meridian (ahead of UTC).
Negative numbers indicate time west of the prime meridian (behind
UTC)."
  ical:utc-offset
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.3")

(ical:define-property ical:tzoffsetto "TZOFFSETTO"
  "Time Zone Offset (in this observance).

This property specifies the time zone offset that is in use *in*
this time zone observance. It is used to calculate the absolute
time at which a new observance takes place. It is a required
property of `icalendar-standard' and `icalendar-daylight'
components. Positive numbers indicate time east of the prime
meridian (ahead of UTC). Negative numbers indicate time west of
the prime meridian (behind UTC)."
  ical:utc-offset
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.4")

(ical:define-property ical:tzurl "TZURL"
  "Time Zone URL.

This property specifies a URL where updated versions of an
`icalendar-vtimezone' component are published."
  ical:uri
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.5")

;;;;; Section 3.8.4: Relationship Component Properties

(ical:define-property ical:attendee "ATTENDEE"
  "Attendee.

This property specfies a participant in a `icalendar-vevent',
`icalendar-vtodo', or `icalendar-valarm'. It is required when the
containing component represents event, task, or notification for
a *group* of people, but not for components that simply represent
these items in a single user's calendar (in that case, it should
not be specified). The property can be specified multiple times,
once for each participant in the event or task. In an
EMAIL-category VALARM component, this property specifies the
address of the user(s) who should receive the notification email.

The parameters `icalendar-roleparam', `icalendar-partstatparam',
`icalendar-rsvpparam', `icalendar-delfromparam', and
`icalendar-deltoparam' are especially relevant for further
specifying the roles of each participant in the containing
component."
  ical:cal-address
  :child-spec (:zero-or-one (ical:cutypeparam
                             ical:memberparam
                             ical:roleparam
                             ical:partstatparam
                             ical:rsvpparam
                             ical:deltoparam
                             ical:delfromparam
                             ical:sentbyparam
                             ical:cnparam
                             ical:dirparam
                             ical:languageparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.1")

(ical:define-property ical:contact "CONTACT"
  "Contact.

This property provides textual contact information relevant to an
`icalendar-vevent', `icalendar-vtodo', `icalendar-vjournal', or
`icalendar-vfreebusy'."
  ical:text
  :child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.2")

(ical:define-property ical:organizer "ORGANIZER"
  "Organizer.

This property specifies the organizer of a group-scheduled
`icalendar-vevent', `icalendar-vtodo', or `icalendar-vjournal'.
It is required in those components if they represent a calendar
entity with multiple participants. In an `icalendar-vfreebusy'
component, it used to specify the user requesting free or busy
time, or the user who published the calendar that the free/busy
information comes from."
  ical:cal-address
  :child-spec (:zero-or-one (ical:cnparam
                             ical:dirparam
                             ical:sentbyparam
                             ical:languageparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.3")

(ical:define-property ical:recurrence-id "RECURRENCE-ID"
  "Recurrence ID.

This property is used together with the `icalendar-uid' and
`icalendar-sequence' properties to identify a specific instance
of a recurring `icalendar-vevent', `icalendar-vtodo', or
`icalendar-vjournal' component. The property value is the
original value of the `icalendar-dtstart' property of the
recurrence instance. Its value must have the same type as that
property's value, and both must specify times in the same way
(either local or UTC)."
  (or ical:date-time
      ical:date)
  :default-type ical:date-time
  :other-types (ical:date)
  :child-spec (:zero-or-one (ical:valuetypeparam
                             ical:tzidparam
                             ical:rangeparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.4")

(ical:define-property ical:related-to "RELATED-TO"
  "Related To (component UID).

This property specifies the `icalendar-uid' value of a different,
related calendar component. It can be specified on an
`icalendar-vevent', `icalendar-vtodo', or `icalendar-vjournal'
component. An `icalendar-reltypeparam' can be used to specify the
relationship type."
  ical:text
  :child-spec (:zero-or-one (ical:reltypeparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.5")

(ical:define-property ical:url "URL"
  "Uniform Resource Locator.

This property specifies the URL associated with an
`icalendar-vevent', `icalendar-vtodo', `icalendar-vjournal', or
`icalendar-vfreebusy' component."
  ical:uri
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.6")

;; TODO: UID should probably be its own type
(ical:define-property ical:uid "UID"
  "Unique Identifier.

This property specifies a globally unique identifier for the
containing component, and is required in an `icalendar-vevent',
`icalendar-vtodo', `icalendar-vjournal', or `icalendar-vfreebusy'
component.

RFC5545 requires that the program generating the UID guarantee
that it be unique, and recommends generating it in a format which
includes a timestamp on the left hand side of an '@' character,
and the domain name or IP address of the host on the right-hand
side."
  ical:text
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.7")

;;;;; Section 3.8.5: Recurrence Component Properties

(ical:define-property ical:exdate "EXDATE"
  "Exception Date-Times.

This property defines a list of exceptions to a recurrence rule
in an `icalendar-vevent', `icalendar-todo', `icalendar-vjournal',
`icalendar-standard', or `icalendar-daylight' component. Together
with the `icalendar-dtstart', `icalendar-rrule', and
`icalendar-rdate' properties, it defines the recurrence set of
the component."
  (or ical:date-time
      ical:date)
  :list-sep ","
  :child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.5.1")

(ical:define-property ical:rdate "RDATE"
  "Recurrence Date-Times.

This property defines a list of date-times or dates on which an
`icalendar-vevent', `icalendar-todo', `icalendar-vjournal',
`icalendar-standard', or `icalendar-daylight' component recurs.
Together with the `icalendar-dtstart', `icalendar-rrule', and
`icalendar-exdate' properties, it defines the recurrence set of
the component."
  (or ical:period
      ical:date-time
      ical:date)
  :default-type ical:date-time
  :other-types (ical:date ical:period)
  :list-sep ","
  :child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.5.2")

(ical:define-property ical:rrule "RRULE"
  "Recurrence Rule.

This property defines a rule or repeating pattern for the dates
and times on which an `icalendar-vevent', `icalendar-todo',
`icalendar-vjournal', `icalendar-standard', or
`icalendar-daylight' component recurs. Together with the
`icalendar-dtstart', `icalendar-rdate', and `icalendar-exdate'
properties, it defines the recurrence set of the component."
  ical:recur
  ;; TODO: faces for subexpressions?
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.5.3")

;;;;; Section 3.8.6: Alarm Component Properties

(ical:define-property ical:action "ACTION"
  "Action (when alarm triggered).

This property defines the action to be taken when the containing
`icalendar-valarm' component is triggered. It is a required
property in an alarm component."
  (or "AUDIO"
      "DISPLAY"
      "EMAIL"
      (group-n 5
        (or ical:iana-token
            ical:x-name)))
  :default-type ical:text
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.6.1")

(ical:define-property ical:repeat "REPEAT"
  "Repeat Count (after initial trigger).

This property specifies the number of times an `icalendar-valarm'
should repeat after it is initially triggered. This property,
along with the `icalendar-duration' property, is required if the
alarm triggers more than once."
  ical:integer
  :default 0
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.6.2")

(ical:define-property ical:trigger "TRIGGER"
  "Trigger.

This property specifies when an `icalendar-valarm' should
trigger. If the value is an `icalendar-dur-value', it represents
a time of that duration relative to the start or end of a related
`icalendar-vevent' or `icalendar-vtodo'. Whether the trigger
applies to the start time or end time of the related component
can be specified with the `icalendar-trigrelparam' parameter. A
positive duration value triggers after the start or end of the
related component; a negative duration value triggers before.

If the value is an `icalendar-date-time', it must be in UTC
format, and it triggers at the specified time."
  (or ical:dur-value
      ical:date-time)
  :child-spec (:zero-or-one (ical:valuetypeparam ical:trigrelparam)
               :zero-or-more (ical:otherparam))
  :other-validator ical:trigger-validator
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.6.3")

(defun ical:trigger-validator (node)
  "Additional validator for an `icalendar-trigger' NODE.
Checks that NODE has valid parameters depending on the type of its value.

This function is called by `icalendar-ast-node-valid-p' for
TRIGGER nodes; it is not normally necessary to call it directly."
  (let* ((params (ical:ast-node-children node))
         (value-node (ical:ast-node-value node))
         (value-type (and value-node (ical:ast-node-type value-node))))
    (when (eq value-type 'ical:date-time)
      (let ((expl-type (ical:value-type-from-params params))
            (dt-value (ical:ast-node-value value-node)))
        (unless (eq expl-type 'ical:date-time)
          (signal 'ical:validation-error
                  (list (concat "Explicit `icalendar-valuetypeparam' required in "
                                "`icalendar-trigger' with non-duration value")
                        node)))
        (when (ical:ast-node-first-child-of 'ical:trigrelparam node)
          (signal 'ical:validation-error
                  (list (concat "`icalendar-trigrelparam' not allowed in "
                                "`icalendar-trigger' with non-duration value"))))
        (unless (ical:date-time-is-utc-p dt-value)
          (signal 'ical:validation-error
                  (list (concat "`icalendar-date-time' value of "
                                "`icalendar-trigger' must be in UTC time")
                        node)))))
    ;; success:
    node))

;;;;; Section 3.8.7: Change Management Component Properties

(ical:define-property ical:created "CREATED"
  "Date-Time Created.

This property specifies the date and time when the calendar user
initially created an `icalendar-vevent', `icalendar-vtodo', or
`icalendar-vjournal' in the calendar database. The value must be
in UTC time."
  ical:date-time
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.1")

(ical:define-property ical:dtstamp "DTSTAMP"
  "Timestamp (of last revision or instance creation).

In an `icalendar-vevent', `icalendar-vtodo',
`icalendar-vjournal', or `icalendar-vfreebusy', this property
specifies the date and time when the calendar user last revised
the component's data in the calendar database. (In this case, it
is equivalent to the `icalendar-last-modified' property.)

If this property is specified on an `icalendar-vcalendar' object
which contains an `icalendar-method' property, it specifies the
date and time when that instance of the calendar object was
created. In this case, it differs from the `icalendar-creation'
and `icalendar-last-modified' properties: whereas those specify
the time the underlying data was created and last modified in the
calendar database, this property specifies when the calendar
object *representing* that data was created.

The value must be in UTC time."
  ical:date-time
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.2")

(ical:define-property ical:last-modified "LAST-MODIFIED"
  "Last Modified timestamp.

This property specifies when the data in an `icalendar-vevent',
`icalendar-vtodo', `icalendar-vjournal', or `icalendar-vtimezone'
was last modified in the calendar database."
  ical:date-time
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.3")

(ical:define-property ical:sequence "SEQUENCE"
  "Revision Sequence Number.

This property specifies the number of the current revision in a
sequence of revisions in an `icalendar-vevent',
`icalendar-vtodo', or `icalendar-vjournal' component. It starts
at 0 and should be incremented monotonically every time the
Organizer makes a significant revision to the calendar data that
component represents."
  ical:integer
  :default 0
  :child-spec (:zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.4")

;;;;; Section 3.8.8: Miscellaneous Component Properties
;; IANA and X- properties should be parsed and printed but can be ignored:
(ical:define-property ical:other-property nil ; don't add to ical:property-types
  "IANA or X-name property.

This property type corresponds to the IANA Properties and
Non-Standard Properties defined in RFC5545; it represents
properties with an unknown name (matching rx
`icalendar-iana-token' or `icalendar-x-name') whose values must
be parsed and preserved but not further interpreted. Its value
may be set to any type with the `icalendar-valuetypeparam'
parameter."
  ical:value
  :default-type ical:text
  ;; "The default value type is TEXT. The value type can be set to any
  ;; value type." TODO: should we specify :other-types? Without it, a
  ;; VALUE param will be required to parse anything other than text,
  ;; but that seems reasonable.
  :child-spec (:allow-others t)
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.8")

(defun ical:read-req-status-info (s)
  "Read a request status value from S.
S should have been previously matched against `icalendar-request-status-info'."
  ;; TODO: this smells like a design flaw. Silence the byte compiler for now.
  (ignore s)
  (let ((code (match-string 11))
        (desc (match-string 12))
        (exdata (match-string 13)))
    (list code (ical:read-text desc) (when exdata (ical:read-text exdata)))))

(defun ical:print-req-status-info (rsi)
  "Serialize request status info value RSI to a string."
  (let ((code (car rsi))
        (desc (cadr rsi))
        (exdata (caddr rsi)))
    (if exdata
        (format "%s;%s;%s" code (ical:print-text desc) (ical:print-text exdata))
      (format "%s;%s" code (ical:print-text desc)))))

(defun ical:req-status-info-p (val)
  "Return non-nil if VAL is an `icalendar-request-status-info' value."
  (and (listp val)
       (length= val 3)
       (stringp (car val))
       (stringp (cadr val))
       (cl-typep (caddr val) '(or string null))))

(ical:define-type ical:req-status-info nil
  "Type for REQUEST-STATUS property values.

When read, a list (CODE DESCRIPTION EXCEPTION). CODE is a hierarchical
numerical code, represented as a string, with the following meanings:
  1.xx Preliminary success
  2.xx Successful
  3.xx Client Error
  4.xx Scheduling Error
DESCRIPTION is a longer description of the request status, also a string.
EXCEPTION (which may be nil) is textual data describing an error.

When printed, the three elements are separated by semicolons, like
  CODE;DESCRIPTION;EXCEPTION
or
  CODE;DESCRIPTION
if EXCEPTION is nil.

This is not a type defined by RFC5545; it is defined here to
facilitate parsing the `icalendar-request-status' property."
  '(satisfies ical:req-status-info-p)
  (seq
   ;; statcode: hierarchical status code
   (group-n 11
     (seq (one-or-more digit)
          (** 1 2 (seq ?. (one-or-more digit)))))
   ?\;
   ;; statdesc: status description
   (group-n 12 ical:text)
   ;; exdata: exception data
   (zero-or-one (seq ?\; (group-n 13 ical:text))))
  :reader ical:read-req-status-info
  :printer ical:print-req-status-info
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.8.3")

(ical:define-property ical:request-status "REQUEST-STATUS"
  "Request status"
  ical:req-status-info
  :child-spec (:zero-or-one (ical:languageparam)
               :zero-or-more (ical:otherparam))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.8.3")

\f
;;; Section 3.6: Calendar Components

(defconst ical:component-types nil ;; populated by ical:define-component
  "Alist mapping printed component names to type symbols")

(defun ical:parse-component (limit)
  "Parse an iCalendar component from point up to LIMIT.
Point should be at the start of the component, i.e., at the start
of a line that looks like \"BEGIN:[COMPONENT-NAME]\". After parsing,
point is at the beginning of the next line following the component
(or end of the buffer). Returns a syntax node representing the component."
  (let ((begin-pos nil)
        (body-begin-pos nil)
        (end-pos nil)
        (body-end-pos nil)
        (begin-regex (rx line-start "BEGIN:" (group-n 2 ical:name) line-end)))

    (unless (re-search-forward begin-regex limit t)
      (signal 'ical:parse-error
              (list (format "Not at start of a component at line %d, position %d"
                            (line-number-at-pos (point))
                            (point)))))

    (setq begin-pos (match-beginning 0)
          body-begin-pos (1+ (match-end 0))) ; start of next line

    (let* ((component-name (match-string 2))
           (known-type (alist-get (upcase component-name)
                                  ical:component-types
                                  nil nil #'equal))
           (component-type (or known-type 'ical:other-component))
           (children nil))

      ;; Find end of component:
      (save-excursion
        (if (re-search-forward
             (rx-to-string `(seq line-start "END:" ,component-name line-end))
             limit t)
            (setq end-pos (match-end 0)
                  body-end-pos (1- (match-beginning 0))) ; end of prev. line
          (signal 'ical:parse-error
                  (list (format (concat "Matching END: of component %s not found "
                                        "between %d and %d")
                                component-name begin-pos limit)))))

      (while (not (bolp)) (forward-char))

      ;; Parse the properties and subcomponents of this component:
      (while (<= (point) body-end-pos)
        (push (ical:parse-property-or-component end-pos)
              children))

      ;; Set point up for the next parser:
      (goto-char end-pos)
      (while (and (< (point) (point-max)) (not (bolp)))
        (forward-char))

      ;; Return the syntax node for the component:
      (ical:make-ast-node component-type
                          :children (nreverse children)
                          :original-name
                            (when (eq component-type 'ical:other-component)
                              component-name)
                          :buffer (current-buffer)
                          :begin begin-pos
                          :end end-pos
                          :value-begin body-begin-pos
                          :value-end body-end-pos))))

(defun ical:parse-property-or-component (limit)
  "Parse a component or a property at point.
Point should be at the beginning of a line which begins a
component or contains a property."
  (cond ((looking-at (rx line-start "BEGIN:" ical:name line-end))
         (icalendar-parse-component limit))
        ((looking-at (rx line-start ical:name))
         (icalendar-parse-property (line-end-position)))
        (t (signal 'ical:parse-error
                   (list (format (concat "Not at start of property or component "
                                         "at line %d, position %d")
                                 (line-number-at-pos (point))
                                 (point)))))))

(defun ical:print-component-node (node)
  "Serialize a component syntax node NODE to a string."
  (let* ((type (ical:ast-node-type node))
         (name (or (ical:ast-node-meta-get node :original-name)
                   (car (rassq type ical:component-types))))
         (children (ical:ast-node-children node)))

    (unless name
      (signal 'ical:print-error
              (list (format "Unknown component name for type `%s'" type)
                    type node)))

    (concat
     ;; TODO: should line ending be sensitive to buffer coding system?
     (format "BEGIN:%s\r\n" name)
     (apply #'concat
            (mapcar #'ical:print-property-or-component children))
     (format "END:%s\r\n" name))))

(defun ical:print-property-or-component (node)
  "Serialize a property or component node NODE to a string."
  (let ((type (ical:ast-node-type node)))
    (cond ((get type 'ical:is-property)
           (ical:print-property-node node))
          ((get type 'ical:is-component)
           (ical:print-component-node node))
          (t (signal 'ical:print-error
                     (list (format "Not a component or property node")
                           node))))))

(ical:define-component ical:vevent "VEVENT"
  "Represents an event.

This component contains properties which describe an event, such
as its start and end time (`icalendar-dtstart' and
`icalendar-dtend') and a summary (`icalendar-summary') and
description (`icalendar-description'). It may also contain
`icalendar-valarm' components as subcomponents which describe
reminder notifications related to the event. Event components can
only be direct children of an `icalendar-vcalendar'; they cannot
be subcomponents of any other component."
  :child-spec (:one (ical:dtstamp ical:uid)
               :zero-or-one (ical:dtstart
                             ;; TODO: dtstart required if METHOD not present
                             ;; in parent calendar
                             ical:class
                             ical:created
                             ical:description
                             ical:dtend
                             ical:duration
                             ical:geo
                             ical:last-modified
                             ical:location
                             ical:organizer
                             ical:priority
                             ical:sequence
                             ical:status
                             ical:summary
                             ical:transp
                             ical:url
                             ical:recurid
                             ical:rrule)
               :zero-or-more (ical:attach
                              ical:attendee
                              ical:categories
                              ical:comment
                              ical:contact
                              ical:exdate
                              ical:rstatus
                              ical:related-to
                              ical:resources
                              ical:rdate
                              ical:other-property
                              ical:valarm))
  :other-validator ical:vevent-validator
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.1")

(defun ical:rrule-validator (node)
  "When component NODE has an `icalendar-rrule', validate that its
`icalendar-dtstart', `icalendar-rdate', and `icalendar-exdate' properties
satisfy the requirements imposed by this rule."
  (let* ((rrule (ical:ast-node-first-child-of 'ical:rrule node))
         (recval (when rrule (ical:ast-node-value rrule)))
         (dtstart (ical:ast-node-first-child-of 'ical:dtstart node))
         (start (when dtstart (ical:ast-node-value dtstart)))
         (rdates (ical:ast-node-children-of 'ical:rdate node))
         (included (when rdates (mapcan #'ical:ast-node-value rdates)))
         (exdates (ical:ast-node-children-of 'ical:exdate node))
         (excluded (when exdates (mapcan #'ical:ast-node-value exdates))))
    (when rrule
      (unless dtstart
        (signal 'ical:validation-error
                (list (concat "An `icalendar-rrule' requires an "
                              "`icalendar-dtstart' property")
                      node)))
      (when included
        (unless (ical:list-of-p (ical:ast-node-type start) included)
          (signal 'ical:validation-error
                 (list (concat "`icalendar-rdate' values must agree with type "
                              "of `icalendar-dtstart' property")
                       node))))
      (when excluded
        (unless (ical:list-of-p (ical:ast-node-type start) excluded)
          (signal 'ical:validation-error
                 (list (concat "`icalendar-exdate' values must agree with type "
                              "of `icalendar-dtstart' property")
                       node))))
      (let* ((freq (car (alist-get 'FREQ recval)))
             (until (car (alist-get 'UNTIL recval))))
        (when (eq 'ical:date (ical:ast-node-type start))
          (when (or (memq freq '(HOURLY MINUTELY SECONDLY))
                    (assq 'BYSECOND recval)
                    (assq 'BYMINUTE recval)
                    (assq 'BYHOUR recval))
            (signal 'ical:validation-error
                    (list (concat "`icalendar-rrule' must not contain time-based "
                                  "rules when `icalendar-dtstart' is a plain date")
                          node))))
        (when until
          (unless (eq (ical:ast-node-type start)
                      (ical:ast-node-type until))
            (signal 'ical:validation-error
                    (list (concat "`icalendar-rrule' UNTIL clause must agree with "
                                  "type of `icalendar-dtstart' property")
                          node)))
          (when (eq 'ical:date-time (ical:ast-node-type until))
            (let ((until-zone
                   (decoded-time-zone (ical:ast-node-value until)))
                  (start-zone
                   (decoded-time-zone (ical:ast-node-value start))))
              ;; "If the "DTSTART" property is specified as a date
              ;; with local time, then the UNTIL rule part MUST also
              ;; be specified as a date with local time":
              (when (and (null start-zone) (not (null until-zone)))
                (signal 'ical:validation-error
                        (list (concat "`icalendar-rrule' UNTIL clause must be in "
                                      "local time if `icalendar-dtstart' is")
                              node)))
              ;; "If the "DTSTART" property is specified as a date
              ;; with UTC time or a date with local time and time zone
              ;; reference, then the UNTIL rule part MUST be specified
              ;; as a date with UTC time":
              (when (and (integerp start-zone)
                         (not (ical:date-time-is-utc-p until)))
                (signal 'ical:validation-error
                        (list (concat "`icalendar-rrule' UNTIL clause must be in "
                                      "UTC time if `icalendar-dtstart' has a "
                                      "defined time zone")
                              node)))))
          ;; "In the case of the "STANDARD" and "DAYLIGHT"
          ;; sub-components the UNTIL rule part MUST always be
          ;; specified as a date with UTC time":
          (when (memq (ical:ast-node-type node) '(ical:standard ical:daylight))
            (unless (ical:date-time-is-utc-p until)
              (signal 'ical:validation-error
                      (list (concat "`icalendar-rrule' UNTIL clause must be in "
                                      "UTC time in `icalendar-standard' and "
                                      "`icalendar-daylight' components")
                            node)))))))
    ;; Success:
    node))

(defun ical:vevent-validator (node)
  "Additional validator for an `icalendar-vevent' NODE.
Checks that NODE has conformant `icalendar-due',
`icalendar-duration', and `icalendar-dtstart' properties, and
calls `icalendar-rrule-validator'.

This function is called by `icalendar-ast-node-valid-p' for
VEVENT nodes; it is not normally necessary to call it directly."
  (let* ((duration (ical:ast-node-first-child-of 'ical:duration node))
         (dtend (ical:ast-node-first-child-of 'ical:dtend node)))
    (when (and dtend duration)
      (signal 'ical:validation-error
              (list (concat "`icalendar-dtend' and `icalendar-duration' "
                            "properties must not appear in the same "
                            "`icalendar-vevent'")
                    node))))
  (ical:rrule-validator node)
  ;; success:
  node)

(ical:define-component ical:vtodo "VTODO"
  "Represents a To-Do item or task.

This component contains properties which describe a to-do item or
task, such as its due date (`icalendar-due') and a summary
(`icalendar-summary') and description (`icalendar-description').
It may also contain `icalendar-valarm' components as
subcomponents which describe reminder notifications related to
the task. To-do components can only be direct children of an
`icalendar-vcalendar'; they cannot be subcomponents of any other
component."
  :child-spec (:one (ical:dtstamp ical:uid)
               :zero-or-one (ical:class
                             ical:completed
                             ical:created
                             ical:description
                             ical:dtstart
                             ical:due
                             ical:duration
                             ical:geo
                             ical:last-modified
                             ical:location
                             ical:organizer
                             ical:percent-complete
                             ical:priority
                             ical:recurrence-id
                             ical:sequence
                             ical:status
                             ical:summary
                             ical:url
                             ical:rrule)
               :zero-or-more (ical:attach
                              ical:attendee
                              ical:categories
                              ical:comment
                              ical:contact
                              ical:exdate
                              ical:request-status
                              ical:related-to
                              ical:resources
                              ical:rdate
                              ical:other-property
                              ical:valarm))
  :other-validator ical:vtodo-validator
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.2")

(defun ical:vtodo-validator (node)
  "Additional validator for an `icalendar-vtodo' NODE.
Checks that NODE has conformant `icalendar-due',
`icalendar-duration', and `icalendar-dtstart' properties, and calls
`icalendar-rrule-validator'.

This function is called by `icalendar-ast-node-valid-p' for
VTODO nodes; it is not normally necessary to call it directly."
  (let* ((due (ical:ast-node-first-child-of 'ical:due node))
         (duration (ical:ast-node-first-child-of 'ical:duration node))
         (dtstart (ical:ast-node-first-child-of 'ical:dtstart node)))
    (when (and due duration)
      (signal 'ical:validation-error
              (list (concat "`icalendar-due' and `icalendar-duration' properties "
                            "must not appear in the same `icalendar-vtodo'")
                    node)))
    (when (and duration (not dtstart))
      (signal 'ical:validation-error
              (list (concat "`icalendar-duration' requires `icalendar-dtstart' "
                            "property in the same `icalendar-vtodo'")
                    node))))
  (ical:rrule-validator node)
  ;; success:
  node)

(ical:define-component ical:vjournal "VJOURNAL"
  "Represents a journal entry.

This component contains properties which describe a journal
entry, which might be any longer-form data (e.g., meeting notes,
a diary entry, or information needed to complete a task). It can
be associated with an `icalendar-vevent' or `icalendar-vtodo' via
the `icalendar-related-to' property. A journal entry does not
take up time in a calendar, and plays no role in searches for
free or busy time. Journal components can only be direct children
of `icalendar-vcalendar'; they cannot be subcomponents of any
other component."
  :child-spec (:one (ical:dtstamp ical:uid)
               :zero-or-one (ical:class
                             ical:created
                             ical:dtstart
                             ical:last-modified
                             ical:organizer
                             ical:recurid
                             ical:sequence
                             ical:status
                             ical:summary
                             ical:url
                             ical:rrule)
               :zero-or-more (ical:attach
                              ical:attendee
                              ical:categories
                              ical:comment
                              ical:contact
                              ical:description
                              ical:exdate
                              ical:related-to
                              ical:rdate
                              ical:rstatus
                              ical:other-property)
               :other-validator ical:rrule-validator)
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.3")

(ical:define-component ical:vfreebusy "VFREEBUSY"
  "Represents a published set of free/busy time blocks, or a request
or response for such blocks.

The free/busy information is represented by the
`icalendar-freebusy' property (which may be given more than once)
and the related `icalendar-fbtype' parameter. Note that
recurrence properties (`icalendar-rrule', `icalendar-rdate', and
`icalendar-exdate') are NOT permitted in this component.

When used to publish blocks of free/busy time in a user's
schedule, the `icalendar-organizer' property specifies the user.

When used to request free/busy time in a user's schedule, or to
respond to such a request, the `icalendar-attendee' property
specifies the user whose time is being requested, and the
`icalendar-organizer' property specifies the user making the
request.

Free/busy components can only be direct children
of `icalendar-vcalendar'; they cannot be subcomponents of any
other component, and cannot contain subcomponents."
  :child-spec (:one (ical:dtstamp ical:uid)
               :zero-or-one (ical:contact
                             ical:dtstart
                             ical:dtend
                             ical:organizer
                             ical:url)
               :zero-or-more (ical:attendee
                              ical:comment
                              ical:freebusy
                              ical:rstatus
                              ical:other-property))
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.4")

(ical:define-component ical:vtimezone "VTIMEZONE"
  "Represents a time zone.

A time zone is identified by an `icalendar-tzid' property, which
is required in this component. Times in other calendar components
can be specified in local time in this time zone with the
`icalendar-tzidparam' parameter. An `icalendar-vcalendar' object
must contain exactly one `icalendar-vtimezone' component for each
unique timezone identifier used in the calendar.

Besides the time zone identifier, a time zone component must
contain at least one `icalendar-standard' or `icalendar-daylight'
subcomponent, which describe the observance of standard or
daylight time in the time zone, including the dates of the
observance and the relevant offsets from UTC time."
  :child-spec (:one (ical:tzid)
               :zero-or-one (ical:last-modified
                             ical:tzurl)
               :zero-or-more (ical:standard
                              ical:daylight
                              ical:other-property))
  :other-validator ical:vtimezone-validator
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.5")

(defun ical:vtimezone-validator (node)
  "Additional validator for an `icalendar-vtimezone' NODE.
Checks that NODE has at least one `icalendar-standard' or
`icalendar-daylight' child.

This function is called by `icalendar-ast-node-valid-p' for
VTIMEZONE nodes; it is not normally necessary to call it directly."
  (let ((child-counts (ical:count-children-by-type node)))
    (when (and (= 0 (alist-get 'ical:standard child-counts 0))
               (= 0 (alist-get 'ical:daylight child-counts 0)))
      (signal 'ical:validation-error
              (list (concat "`icalendar-timezone' must have at least one "
                            "`icalendar-standard' or `icalendar-daylight' child")
                    node))))
  ;; success:
  node)

(ical:define-component ical:standard "STANDARD"
  "Represents a Standard Time observance in a time zone.

The observance has a start time, specified by an
`icalendar-dtstart' property, which is required in this component
and must be in *local* time format. The observance may have a
recurring onset (e.g. each year on a particular day or date)
described by the `icalendar-rrule' and `icalendar-rdate'
properties. An end date for the observance, if there is one, must
be specified in the UNTIL clause of the `icalendar-rrule' in UTC
time.

The offset from UTC time when the observance begins is specified
in the `icalendar-tzoffsetfrom' property, which is required. The
offset from UTC time while the observance is in effect is
specified by the `icalendar-tzoffsetto' property, which is also
required. A common identifier for the time zone observance can be
specified in the `icalendar-tzname' property. Other explanatory
comments can be provided in `icalendar-comment'.

This component must be a direct child of an `icalendar-vtimezone'
component and cannot contain other subcomponents."
  :child-spec (:one (ical:dtstart
                     ical:tzoffsetto
                     ical:tzoffsetfrom)
               :zero-or-one (ical:rrule)
               :zero-or-more (ical:comment
                              ical:rdate
                              ical:tzname
                              ical:other-property)
               :other-validator ical:rrule-validator)
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.5")

(ical:define-component ical:daylight "DAYLIGHT"
  "Represents a Daylight Savings Time observance in a time zone.

The observance has a start time, specified by an
`icalendar-dtstart' property, which is required in this component
and must be in *local* time format. The observance may have a
recurring onset (e.g. each year on a particular day or date)
described by the `icalendar-rrule' and `icalendar-rdate'
properties. An end date for the observance, if there is one, must
be specified in the UNTIL clause of the `icalendar-rrule' in UTC
time.

The offset from UTC time when the observance begins is specified
in the `icalendar-tzoffsetfrom' property, which is required. The
offset from UTC time while the observance is in effect is
specified by the `icalendar-tzoffsetto' property, which is also
required. A common identifier for the time zone observance can be
specified in the `icalendar-tzname' property. Other
explanatory comments can be provided in `icalendar-comment'.

This component must be a direct child of an `icalendar-vtimezone'
component and cannot contain other subcomponents."
  :child-spec (:one (ical:dtstart
                     ical:tzoffsetto
                     ical:tzoffsetfrom)
               :zero-or-one (ical:rrule)
               :zero-or-more (ical:comment
                              ical:rdate
                              ical:tzname
                              ical:other-property)
               :other-validator ical:rrule-validator)
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.5")

(ical:define-component ical:valarm "VALARM"
  "Represents an alarm.

An alarm is a notification or reminder for an event or task. The
type of notification is determined by this component's
`icalendar-action' property: it may be an AUDIO, DISPLAY, or
EMAIL notification.
If it is an audio alarm, it can include an
`icalendar-attach' property specifying the audio to be rendered.
If it is a DISPLAY alarm, it must include an `icalendar-description'
property containing the text to be displayed.
If it is an EMAIL alarm, it must include both an
`icalendar-summary' and an `icalendar-description', which specify
the subject and body of the email, and one or more
`icalendar-attendee' properties, which specify the recipients.

The required `icalendar-trigger' property specifies when the
alarm triggers. If the alarm repeats, then `icalendar-duration'
and `icalendar-repeat' properties are also both required.

This component must occur as a direct child of an
`icalendar-vevent' or `icalendar-vtodo' component, and cannot
contain any subcomponents."
  :child-spec (:one (ical:action ical:trigger)
               :zero-or-one (ical:duration ical:repeat)
               :zero-or-more (ical:summary
                              ical:description
                              ical:attendee
                              ical:attach
                              ical:other-property))
  :other-validator ical:valarm-validator
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.6")

(defun ical:valarm-validator (node)
  "Additional validator function for `icalendar-valarm' components.
Checks that NODE has the right properties corresponding to its
`icalendar-action' type, e.g., that an EMAIL alarm has a
subject (`icalendar-summary') and recipients (`icalendar-attendee').

This function is called by `icalendar-ast-node-valid-p' for
VALARM nodes; it is not normally necessary to call it directly."
  (let* ((action (ical:ast-node-first-child-of 'ical:action node))
         (duration (ical:ast-node-first-child-of 'ical:duration node))
         (repeat (ical:ast-node-first-child-of 'ical:repeat node))
         (child-counts (ical:count-children-by-type node)))

    (when (and duration (not repeat))
      (signal 'ical:validation-error
              (list (concat "`icalendar-valarm' node with `icalendar-duration' "
                            "must also have `icalendar-repeat' property")
                    node)))

    (when (and repeat (not duration))
      (signal 'ical:validation-error
              (list (concat "`icalendar-valarm' node with `icalendar-repeat' "
                            "must also have `icalendar-duration' property")
                    node)))

    (let ((action-str (upcase (ical:text-to-string
                               (ical:ast-node-value action)))))
      (cond ((equal "AUDIO" action-str)
             (unless (<= (alist-get 'ical:attach child-counts 0) 1)
               (signal 'ical:validation-error
                       (list (concat "AUDIO `icalendar-valarm' may not have "
                                     "more than one `icalendar-attach'")
                             node)))
             node)

            ((equal "DISPLAY" action-str)
             (unless (= 1 (alist-get 'ical:description child-counts 0))
               (signal 'ical:validation-error
                       (list (concat "DISPLAY `icalendar-valarm' must have "
                                     "exactly one `icalendar-description'")
                             node)))
             node)

            ((equal "EMAIL" action-str)
             (unless (= 1 (alist-get 'ical:summary child-counts 0))
               (signal 'ical:validation-error
                       (list (concat "EMAIL `icalendar-valarm' must have "
                                     "exactly one `icalendar-summary'")
                             node)))
             (unless (= 1 (alist-get 'ical:description child-counts 0))
               (signal 'ical:validation-error
                       (list (concat "EMAIL `icalendar-valarm' must have "
                                     "exactly one `icalendar-description'")
                             node)))
             (unless (<= 1 (alist-get 'ical:attendee child-counts 0))
               (signal 'ical:validation-error
                       (list (concat "EMAIL `icalendar-valarm' must have "
                                     "at least one `icalendar-attendee'")
                             node)))
             node)

            (t
             ;; "Applications MUST ignore alarms with x-name and iana-token
             ;; values they don't recognize." So this is not a validation-error:
             (warn (format "Unknown ACTION value in VALARM: %s" action-str))
             node)))))

(ical:define-component ical:other-component nil
  "Component type for unrecognized component names.

This component type corresponds to the IANA and X-name components
allowed by RFC5545 sec. 3.6; it represents components with an
unknown name (matching rx `icalendar-iana-token' or
`icalendar-x-name') which must be parsed and preserved but not
further interpreted."
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6")

;; Technically VCALENDAR is not a "component", but for the
;; purposes of parsing and syntax highlighting, it looks just like
;; one, so we define it as such here.
;; TODO: if this becomes a problem, modify `ical:component-node-p'
;; to return nil for VCALENDAR components
(ical:define-component ical:vcalendar "VCALENDAR"
  "Calendar Object.

This is the top-level data structure defined by RFC5545. A
VCALENDAR must contain the calendar properties `icalendar-prodid'
and `icalendar-version', and may contain the calendar properties
`icalendar-method' and `icalendar-calscale'.

It must also contain at least one VEVENT, VTODO, VJOURNAL,
VFREEBUSY, or other component, and for every unique
`icalendar-tzidparam' value appearing in a property within these
components, the calendar object must contain an
`icalendar-vtimezone' defining a timezone with that TZID."

  :child-spec (:one (ical:prodid ical:version)
               :zero-or-one (ical:calscale ical:method)
               :zero-or-more (ical:other-property
                              ical:vevent
                              ical:vtodo
                              ical:vjournal
                              ical:vfreebusy
                              ical:vtimezone
                              ical:other-component))
  :other-validator ical:vcalendar-validator
  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.4")

(defun ical:all-tzidparams-in (node)
  "Recursively search NODE for `icalendar-tzidparam' nodes and
return a list of their values"
  (cond ((ical:tzid-param-p node)
         (list (ical:ast-node-value node)))
        ((ical:param-node-p node)
         nil)
        (t ;; TODO: could prune search here when properties don't allow tzidparam
         (seq-uniq (mapcan #'ical:all-tzidparams-in
                           (ical:ast-node-children node))))))

(defun ical:vcalendar-validator (node)
  "Additional validator for `icalendar-vcalendar' NODE. Checks that
NODE has at least one component child and that all of the
`ical-tzidparam' values appearing in subcomponents have a
corresponding `icalendar-vtimezone' definition.

This function is called by `icalendar-ast-node-valid-p' for
VCALENDAR nodes; it is not normally necessary to call it directly."
  (let* ((children (ical:ast-node-children node))
         (comp-children (seq-filter #'ical:component-node-p children))
         (tz-children (seq-filter #'ical:vtimezone-component-p children))
         (defined-tzs (mapcar
                       (lambda (tz)
                         ;; ensure timezone component has a TZID property and
                         ;; extract its string value:
                         (when (ical:ast-node-valid-p tz)
                           (ical:text-to-string
                            (ical:ast-node-value
                             (ical:ast-node-first-child-of 'ical:tzid tz)))))
                       tz-children))
         (appearing-tzids (ical:all-tzidparams-in node)))
    (unless comp-children
      (signal 'ical:validation-error
              (list (concat "`icalendar-vcalendar' must contain "
                            "at least one component")
                    node)))

    (let ((seen nil))
      (dolist (tzid appearing-tzids)
        (unless (member tzid seen)
          (unless (member tzid defined-tzs)
            (signal 'ical:validation-error
                    (list (format "No VTIMEZONE with TZID '%s' in calendar" tzid)
                          node))))
        (push tzid seen)))

    ;; success:
    node))

;; TODO: parse-calendar and print-calendar functions.  parse-component
;; is sufficient to parse all the syntax in a calendar, but a
;; calendar-level parsing function is needed to add support for
;; timezones. This function should ensure that every
;; `icalendar-tzidparam' in the calendar has a corresponding
;; `icalendar-vtimezone' component, and modify the zone information of
;; the parsed date-time according to the offset in that timezone (and
;; the print function should do the inverse). Calculating the offsets,
;; however, is dependent on an implementation of recurrence rules which
;; is still in the works.




;;; Documentation for all of the above via `describe-symbol':
(defun icalendar-documented-symbol-p (sym)
  "iCalendar symbol predicate for `describe-symbol-backends'"
  (or (get sym 'icalendar-type-documentation)
      ;; grammatical categories defined with rx-define, but with no
      ;; other special icalendar docs:
      (and (get sym 'rx-definition)
           (length> (symbol-name sym) 10)
           (equal "icalendar-" (substring (symbol-name sym) 0 10)))))

(defun icalendar-documentation (sym buf frame)
  "iCalendar documentation backend for `describe-symbol-backends'"
  (ignore buf frame) ; Silence the byte compiler
  (with-help-window (help-buffer)
    (with-current-buffer standard-output
      (let* ((type-doc (get sym 'icalendar-type-documentation))
             (link (get sym 'icalendar-link))
             (rx-def (get sym 'rx-definition))
             (rx-doc (when rx-def
                       (with-output-to-string
                         (pp rx-def))))
             (value-rx-def (get sym 'ical:value-rx))
             (value-rx-doc (when value-rx-def
                             (with-output-to-string
                               (pp value-rx-def))))
             (values-rx-def (get sym 'ical:values-rx))
             (values-rx-doc (when values-rx-def
                             (with-output-to-string
                               (pp values-rx-def))))

             (full-doc
              (concat
               (when type-doc
                 (format "`%s' is an iCalendar type:\n\n%s\n\n"
                         sym type-doc))
               (when link
                 (format "For further information see\nURL `%s'\n\n" link))
               ;; FIXME: this is probably better done in rx.el!
               ;; TODO: could also generalize this to recursively
               ;; search rx-def for any symbol that starts with "icalendar-"...
               (when rx-def
                 (format "`%s' is an iCalendar grammar category.
Its `rx' definition is:\n\n%s%s%s"
                         sym
                         rx-doc
                         (if value-rx-def
                             (format "\nIndividual values must match:\n%s"
                                      value-rx-doc)
                           "")
                         (if values-rx-def
                             (format "\nLists of values must match:\n%s"
                                      values-rx-doc)
                           "")))
               "\n")))

        (insert full-doc)
        full-doc))))


(defconst ical:describe-symbol-backend
  '(nil icalendar-documented-symbol-p icalendar-documentation)
  "Entry for icalendar documentation in `describe-symbol-backends'")

(push ical:describe-symbol-backend describe-symbol-backends)

;; Unloading:
(defun ical:parser-unload-function ()
  "Unload function for `icalendar-parser'."
  (mapatoms
   (lambda (sym)
     (when (string-match "^icalendar-" (symbol-name sym))
       (unintern sym obarray))))

  (setq describe-symbol-backends
        (remq ical:describe-symbol-backend describe-symbol-backends))
  ;; Proceed with normal unloading:
  nil)

(provide 'icalendar-parser)

;; Local Variables:
;; read-symbol-shorthands: (("ical:" . "icalendar-"))
;; End:
;;; icalendar-parser.el ends here

debug log:

solving 08cc4dbd0fb ...
found 08cc4dbd0fb in https://yhetil.org/emacs/87y10aku5u.fsf@recursewithless.net/
found bc9524ff389 in https://yhetil.org/emacs/871py2m90b.fsf@recursewithless.net/

applying [1/2] https://yhetil.org/emacs/871py2m90b.fsf@recursewithless.net/
diff --git a/lisp/calendar/icalendar-parser.el b/lisp/calendar/icalendar-parser.el
new file mode 100644
index 00000000000..bc9524ff389


applying [2/2] https://yhetil.org/emacs/87y10aku5u.fsf@recursewithless.net/
diff --git a/lisp/calendar/icalendar-parser.el b/lisp/calendar/icalendar-parser.el
index bc9524ff389..08cc4dbd0fb 100644

Checking patch lisp/calendar/icalendar-parser.el...
Applied patch lisp/calendar/icalendar-parser.el cleanly.
Checking patch lisp/calendar/icalendar-parser.el...
Applied patch lisp/calendar/icalendar-parser.el cleanly.

index at:
100644 08cc4dbd0fbeeffbe8ef2c5f7620acabbd136141	lisp/calendar/icalendar-parser.el

(*) Git path names are given by the tree(s) the blob belongs to.
    Blobs themselves have no identifier aside from the hash of its contents.^

Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.