1#![cfg_attr(docsrs, feature(doc_cfg))]
14#![doc(html_root_url = "https://docs.rs/ferogram-parsers/0.6.4")]
15#![deny(unsafe_code)]
85
86mod html;
87mod markdown;
88mod rich_common;
89mod rich_html;
90mod rich_markdown;
91
92#[allow(deprecated)]
93pub use markdown::{
94 generate_markdown, generate_markdown_v2, parse_markdown, parse_markdown_v1, parse_markdown_v2,
95};
96
97pub use html::{generate_html, parse_html};
98
99pub use rich_common::parse_rich_inline_md;
100pub use rich_markdown::parse_rich_markdown;
101
102pub use rich_html::{parse_rich_html, parse_rich_html_inline};
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use ferogram_tl_types as tl;
108
109 #[test]
112 fn markdown_bold() {
113 let (text, ents) = parse_markdown("Hello **world**!");
114 assert_eq!(text, "Hello world!");
115 if let tl::enums::MessageEntity::Bold(b) = &ents[0] {
116 assert_eq!(b.offset, 6);
117 assert_eq!(b.length, 5);
118 } else {
119 panic!("expected bold");
120 }
121 }
122
123 #[test]
124 fn markdown_bold_single_asterisk() {
125 let (text, ents) = parse_markdown("*bold*");
126 assert_eq!(text, "bold");
127 assert!(matches!(ents[0], tl::enums::MessageEntity::Bold(_)));
128 }
129
130 #[test]
131 fn markdown_italic_single_underscore() {
132 let (text, ents) = parse_markdown("_italic_");
133 assert_eq!(text, "italic");
134 assert!(matches!(ents[0], tl::enums::MessageEntity::Italic(_)));
135 }
136
137 #[test]
139 fn markdown_v2_underline_double_underscore() {
140 let (text, ents) = parse_markdown("__underline__");
141 assert_eq!(text, "underline");
142 assert!(
143 matches!(ents[0], tl::enums::MessageEntity::Underline(_)),
144 "expected Underline, got {:?}",
145 ents[0]
146 );
147 }
148
149 #[test]
151 fn markdown_v2_strike_single_tilde() {
152 let (text, ents) = parse_markdown("~strike~");
153 assert_eq!(text, "strike");
154 assert!(matches!(ents[0], tl::enums::MessageEntity::Strike(_)));
155 }
156
157 #[test]
158 fn markdown_spoiler() {
159 let (text, ents) = parse_markdown("||spoiler||");
160 assert_eq!(text, "spoiler");
161 assert!(matches!(ents[0], tl::enums::MessageEntity::Spoiler(_)));
162 }
163
164 #[test]
165 fn markdown_inline_code() {
166 let (text, ents) = parse_markdown("Use `foo()` to do it");
167 assert_eq!(text, "Use foo() to do it");
168 assert!(matches!(ents[0], tl::enums::MessageEntity::Code(_)));
169 }
170
171 #[test]
172 fn markdown_code_block_with_lang() {
173 let (text, ents) = parse_markdown("```rust\nfn main() {}\n```");
174 assert_eq!(text, "fn main() {}");
175 if let tl::enums::MessageEntity::Pre(p) = &ents[0] {
176 assert_eq!(p.language, "rust");
177 assert_eq!(p.offset, 0);
178 } else {
179 panic!("expected pre");
180 }
181 }
182
183 #[test]
184 fn markdown_code_block_no_lang() {
185 let (text, ents) = parse_markdown("```\nhello\n```");
186 assert_eq!(text, "hello");
187 if let tl::enums::MessageEntity::Pre(p) = &ents[0] {
188 assert_eq!(p.language, "");
189 } else {
190 panic!("expected pre");
191 }
192 }
193
194 #[test]
195 fn markdown_text_url() {
196 let (text, ents) = parse_markdown("[click](https://example.com)");
197 assert_eq!(text, "click");
198 if let tl::enums::MessageEntity::TextUrl(e) = &ents[0] {
199 assert_eq!(e.url, "https://example.com");
200 } else {
201 panic!("expected text url");
202 }
203 }
204
205 #[test]
206 fn markdown_mention() {
207 let (text, ents) = parse_markdown("[User](tg://user?id=42)");
208 assert_eq!(text, "User");
209 if let tl::enums::MessageEntity::MentionName(e) = &ents[0] {
210 assert_eq!(e.user_id, 42);
211 } else {
212 panic!("expected mention name");
213 }
214 }
215
216 #[test]
217 fn markdown_custom_emoji() {
218 let (text, ents) = parse_markdown("");
219 assert_eq!(text, "👍");
220 if let tl::enums::MessageEntity::CustomEmoji(e) = &ents[0] {
221 assert_eq!(e.document_id, 5368324170671202286);
222 } else {
223 panic!("expected custom emoji");
224 }
225 }
226
227 #[test]
229 fn markdown_v2_custom_emoji_empty_label() {
230 let (text, ents) = parse_markdown("");
231 assert_eq!(text, "");
232 if let tl::enums::MessageEntity::CustomEmoji(e) = &ents[0] {
233 assert_eq!(e.document_id, 12345);
234 } else {
235 panic!("expected custom emoji");
236 }
237 }
238
239 #[test]
240 fn markdown_backslash_escape() {
241 let (text, ents) = parse_markdown(r"\*not bold\*");
242 assert_eq!(text, "*not bold*");
243 assert!(ents.is_empty());
244 }
245
246 #[test]
247 fn markdown_v2_backslash_escape_extended() {
248 let (text, ents) = parse_markdown(r"\>\=\.");
249 assert_eq!(text, ">=.");
250 assert!(ents.is_empty());
251 }
252
253 #[test]
254 fn markdown_v2_nested_bold_italic() {
255 let (text, ents) = parse_markdown("**bold _italic_ end**");
256 assert_eq!(text, "bold italic end");
257 assert_eq!(ents.len(), 2);
258 assert!(
259 ents.iter()
260 .any(|e| matches!(e, tl::enums::MessageEntity::Bold(_)))
261 );
262 assert!(
263 ents.iter()
264 .any(|e| matches!(e, tl::enums::MessageEntity::Italic(_)))
265 );
266 }
267
268 #[test]
269 fn markdown_v2_blockquote_single_line() {
270 let (text, ents) = parse_markdown("> hello");
271 assert_eq!(text, "hello");
272 if let tl::enums::MessageEntity::Blockquote(e) = &ents[0] {
273 assert!(!e.collapsed);
274 } else {
275 panic!("expected blockquote, got {:?}", ents[0]);
276 }
277 }
278
279 #[test]
280 fn markdown_v2_blockquote_multi_line() {
281 let (text, ents) = parse_markdown("> line1\n> line2");
282 assert_eq!(text, "line1\nline2");
283 assert_eq!(ents.len(), 1);
284 assert!(matches!(ents[0], tl::enums::MessageEntity::Blockquote(_)));
285 }
286
287 #[test]
288 fn markdown_v2_expandable_blockquote() {
289 let (text, ents) = parse_markdown("**> secret");
290 assert_eq!(text, "secret");
291 if let tl::enums::MessageEntity::Blockquote(e) = &ents[0] {
292 assert!(
293 e.collapsed,
294 "expandable blockquote should have collapsed=true"
295 );
296 } else {
297 panic!("expected blockquote");
298 }
299 }
300
301 #[test]
305 fn markdown_v1_italic_double_underscore() {
306 #[allow(deprecated)]
307 let (text, ents) = parse_markdown_v1("__italic__");
308 assert_eq!(text, "italic");
309 assert!(
310 matches!(ents[0], tl::enums::MessageEntity::Italic(_)),
311 "V1 __ should be Italic"
312 );
313 }
314
315 #[test]
317 fn markdown_v1_strike_double_tilde() {
318 #[allow(deprecated)]
319 let (text, ents) = parse_markdown_v1("~~strike~~");
320 assert_eq!(text, "strike");
321 assert!(matches!(ents[0], tl::enums::MessageEntity::Strike(_)));
322 }
323
324 #[test]
327 fn generate_markdown_pre() {
328 let entities = vec![tl::enums::MessageEntity::Pre(tl::types::MessageEntityPre {
329 offset: 0,
330 length: 12,
331 language: "rust".into(),
332 })];
333 let md = generate_markdown("fn main() {}", &entities);
334 assert_eq!(md, "```rust\nfn main() {}\n```");
335 }
336
337 #[test]
338 fn generate_markdown_text_url() {
339 let entities = vec![tl::enums::MessageEntity::TextUrl(
340 tl::types::MessageEntityTextUrl {
341 offset: 0,
342 length: 5,
343 url: "https://example.com".into(),
344 },
345 )];
346 let md = generate_markdown("click", &entities);
347 assert_eq!(md, "[click](https://example.com)");
348 }
349
350 #[test]
351 fn generate_markdown_mention() {
352 let entities = vec![tl::enums::MessageEntity::MentionName(
353 tl::types::MessageEntityMentionName {
354 offset: 0,
355 length: 4,
356 user_id: 99,
357 },
358 )];
359 let md = generate_markdown("User", &entities);
360 assert_eq!(md, "[User](tg://user?id=99)");
361 }
362
363 #[test]
364 fn generate_markdown_custom_emoji() {
365 let entities = vec![tl::enums::MessageEntity::CustomEmoji(
366 tl::types::MessageEntityCustomEmoji {
367 offset: 0,
368 length: 2,
369 document_id: 123456,
370 },
371 )];
372 let md = generate_markdown("👍", &entities);
373 assert_eq!(md, "");
374 }
375
376 #[test]
377 fn generate_markdown_v2_escapes_special_chars() {
378 let (_, empty): (_, Vec<_>) = (String::new(), vec![]);
379 let md = generate_markdown("1 * 2 = 2", &empty);
380 assert_eq!(md, r"1 \* 2 \= 2");
381 }
382
383 #[test]
384 fn generate_markdown_v2_italic_and_underline() {
385 let entities = vec![
386 tl::enums::MessageEntity::Italic(tl::types::MessageEntityItalic {
387 offset: 0,
388 length: 6,
389 }),
390 tl::enums::MessageEntity::Underline(tl::types::MessageEntityUnderline {
391 offset: 7,
392 length: 9,
393 }),
394 ];
395 let md = generate_markdown("italic underline", &entities);
396 assert_eq!(md, "_italic_ __underline__");
397 }
398
399 #[test]
400 fn generate_markdown_v2_strike() {
401 let entities = vec![tl::enums::MessageEntity::Strike(
402 tl::types::MessageEntityStrike {
403 offset: 0,
404 length: 6,
405 },
406 )];
407 let md = generate_markdown("struck", &entities);
408 assert_eq!(md, "~struck~");
409 }
410
411 #[test]
412 fn generate_markdown_v2_blockquote() {
413 let entities = vec![tl::enums::MessageEntity::Blockquote(
414 tl::types::MessageEntityBlockquote {
415 collapsed: false,
416 offset: 0,
417 length: 5,
418 },
419 )];
420 let md = generate_markdown("hello", &entities);
421 assert!(md.starts_with("> "), "expected '> ' prefix, got: {md:?}");
422 assert!(md.contains("hello"));
423 }
424
425 #[test]
426 fn generate_markdown_v2_expandable_blockquote() {
427 let entities = vec![tl::enums::MessageEntity::Blockquote(
428 tl::types::MessageEntityBlockquote {
429 collapsed: true,
430 offset: 0,
431 length: 6,
432 },
433 )];
434 let md = generate_markdown("secret", &entities);
435 assert!(
436 md.starts_with("**> "),
437 "expected '**> ' prefix, got: {md:?}"
438 );
439 }
440
441 #[test]
442 fn markdown_roundtrip_url() {
443 let original = "click";
444 let entities = vec![tl::enums::MessageEntity::TextUrl(
445 tl::types::MessageEntityTextUrl {
446 offset: 0,
447 length: 5,
448 url: "https://example.com".into(),
449 },
450 )];
451 let md = generate_markdown(original, &entities);
452 let (back, ents2) = parse_markdown(&md);
453 assert_eq!(back, original);
454 if let tl::enums::MessageEntity::TextUrl(e) = &ents2[0] {
455 assert_eq!(e.url, "https://example.com");
456 } else {
457 panic!("roundtrip url failed");
458 }
459 }
460
461 #[test]
464 fn html_bold_italic() {
465 let (text, ents) = parse_html("<b>bold</b> and <i>italic</i>");
466 assert_eq!(text, "bold and italic");
467 assert_eq!(ents.len(), 2);
468 }
469
470 #[test]
471 fn html_strong_em_aliases() {
472 let (text, ents) = parse_html("<strong>bold</strong> <em>italic</em>");
473 assert_eq!(text, "bold italic");
474 assert!(
475 ents.iter()
476 .any(|e| matches!(e, tl::enums::MessageEntity::Bold(_)))
477 );
478 assert!(
479 ents.iter()
480 .any(|e| matches!(e, tl::enums::MessageEntity::Italic(_)))
481 );
482 }
483
484 #[test]
485 fn html_ins_alias_for_underline() {
486 let (text, ents) = parse_html("<ins>underline</ins>");
487 assert_eq!(text, "underline");
488 assert!(matches!(ents[0], tl::enums::MessageEntity::Underline(_)));
489 }
490
491 #[test]
492 fn html_span_tg_spoiler() {
493 let (text, ents) = parse_html("<span class=\"tg-spoiler\">hidden</span>");
494 assert_eq!(text, "hidden");
495 assert!(matches!(ents[0], tl::enums::MessageEntity::Spoiler(_)));
496 }
497
498 #[test]
499 fn html_blockquote() {
500 let (text, ents) = parse_html("<blockquote>quoted</blockquote>");
501 assert_eq!(text, "quoted");
502 if let tl::enums::MessageEntity::Blockquote(e) = &ents[0] {
503 assert!(!e.collapsed);
504 } else {
505 panic!("expected blockquote");
506 }
507 }
508
509 #[test]
510 fn html_blockquote_expandable() {
511 let (text, ents) = parse_html("<blockquote expandable>secret</blockquote>");
512 assert_eq!(text, "secret");
513 if let tl::enums::MessageEntity::Blockquote(e) = &ents[0] {
514 assert!(
515 e.collapsed,
516 "expandable blockquote should have collapsed=true"
517 );
518 } else {
519 panic!("expected blockquote");
520 }
521 }
522
523 #[test]
524 fn html_tg_time() {
525 let (text, ents) =
526 parse_html("<tg-time unix=\"1700000000\" format=\"Dt\">Nov 14</tg-time>");
527 assert_eq!(text, "Nov 14");
528 if let tl::enums::MessageEntity::FormattedDate(e) = &ents[0] {
529 assert_eq!(e.date, 1700000000);
530 assert!(e.long_date);
531 assert!(e.short_time);
532 } else {
533 panic!("expected FormattedDate, got {:?}", ents[0]);
534 }
535 }
536
537 #[test]
538 fn html_pre_with_language() {
539 let (text, ents) =
540 parse_html("<pre><code class=\"language-rust\">fn main() {}</code></pre>");
541 assert_eq!(text, "fn main() {}");
542 assert_eq!(ents.len(), 1, "should be exactly one Pre entity");
543 if let tl::enums::MessageEntity::Pre(p) = &ents[0] {
544 assert_eq!(p.language, "rust");
545 } else {
546 panic!("expected pre");
547 }
548 }
549
550 #[test]
551 fn html_link() {
552 let (text, ents) = parse_html("<a href=\"https://example.com\">click</a>");
553 assert_eq!(text, "click");
554 if let tl::enums::MessageEntity::TextUrl(e) = &ents[0] {
555 assert_eq!(e.url, "https://example.com");
556 } else {
557 panic!("expected text url");
558 }
559 }
560
561 #[cfg(not(feature = "html5ever"))]
562 #[test]
563 fn html_entities_decoded() {
564 let (text, _) = parse_html("A & B <3>");
565 assert_eq!(text, "A & B <3>");
566 }
567
568 #[test]
571 fn generate_html_roundtrip() {
572 let original = "Hello world";
573 let entities = vec![tl::enums::MessageEntity::Bold(
574 tl::types::MessageEntityBold {
575 offset: 0,
576 length: 5,
577 },
578 )];
579 let html = generate_html(original, &entities);
580 assert_eq!(html, "<b>Hello</b> world");
581 let (back, ents2) = parse_html(&html);
582 assert_eq!(back, original);
583 assert_eq!(ents2.len(), 1);
584 }
585
586 #[test]
587 fn generate_html_blockquote() {
588 let entities = vec![tl::enums::MessageEntity::Blockquote(
589 tl::types::MessageEntityBlockquote {
590 collapsed: false,
591 offset: 0,
592 length: 6,
593 },
594 )];
595 let html = generate_html("quoted", &entities);
596 assert_eq!(html, "<blockquote>quoted</blockquote>");
597 }
598
599 #[test]
600 fn generate_html_expandable_blockquote() {
601 let entities = vec![tl::enums::MessageEntity::Blockquote(
602 tl::types::MessageEntityBlockquote {
603 collapsed: true,
604 offset: 0,
605 length: 6,
606 },
607 )];
608 let html = generate_html("secret", &entities);
609 assert_eq!(html, "<blockquote expandable>secret</blockquote>");
610 }
611
612 #[test]
613 fn generate_html_formatted_date() {
614 let entities = vec![tl::enums::MessageEntity::FormattedDate(
615 tl::types::MessageEntityFormattedDate {
616 relative: false,
617 short_time: true,
618 long_time: false,
619 short_date: false,
620 long_date: true,
621 day_of_week: false,
622 offset: 0,
623 length: 6,
624 date: 1700000000,
625 },
626 )];
627 let html = generate_html("Nov 14", &entities);
628 assert!(html.contains("tg-time"), "expected tg-time in: {html}");
629 assert!(html.contains("1700000000"));
630 }
631
632 #[test]
633 fn html_pre_with_language_roundtrip() {
634 let original = "fn main() {}";
635 let entities = vec![tl::enums::MessageEntity::Pre(tl::types::MessageEntityPre {
636 offset: 0,
637 length: 12,
638 language: "rust".into(),
639 })];
640 let html = generate_html(original, &entities);
641 let (back, ents2) = parse_html(&html);
642 assert_eq!(back, original);
643 if let tl::enums::MessageEntity::Pre(p) = &ents2[0] {
644 assert_eq!(p.language, "rust");
645 } else {
646 panic!("roundtrip pre language failed");
647 }
648 }
649
650 #[test]
653 fn html_u_underline() {
654 let (text, ents) = parse_html("<u>under</u>");
655 assert_eq!(text, "under");
656 assert!(matches!(ents[0], tl::enums::MessageEntity::Underline(_)));
657 }
658
659 #[test]
660 fn html_s_strike() {
661 let (text, ents) = parse_html("<s>gone</s>");
662 assert_eq!(text, "gone");
663 assert!(matches!(ents[0], tl::enums::MessageEntity::Strike(_)));
664 }
665
666 #[test]
667 fn html_del_strike_alias() {
668 let (text, ents) = parse_html("<del>gone</del>");
669 assert_eq!(text, "gone");
670 assert!(matches!(ents[0], tl::enums::MessageEntity::Strike(_)));
671 }
672
673 #[test]
674 fn html_strike_tag_alias() {
675 let (text, ents) = parse_html("<strike>gone</strike>");
676 assert_eq!(text, "gone");
677 assert!(matches!(ents[0], tl::enums::MessageEntity::Strike(_)));
678 }
679
680 #[test]
681 fn html_tg_spoiler_tag() {
682 let (text, ents) = parse_html("<tg-spoiler>secret</tg-spoiler>");
683 assert_eq!(text, "secret");
684 assert!(matches!(ents[0], tl::enums::MessageEntity::Spoiler(_)));
685 }
686
687 #[test]
688 fn html_tg_emoji() {
689 let (text, ents) = parse_html("<tg-emoji emoji-id=\"9876\">X</tg-emoji>");
690 assert_eq!(text, "X");
691 if let tl::enums::MessageEntity::CustomEmoji(e) = &ents[0] {
692 assert_eq!(e.document_id, 9876);
693 assert_eq!(e.offset, 0);
694 assert_eq!(e.length, 1);
695 } else {
696 panic!("expected CustomEmoji, got {:?}", ents[0]);
697 }
698 }
699
700 #[test]
701 fn html_mention_name() {
702 let (text, ents) = parse_html("<a href=\"tg://user?id=777\">Alice</a>");
703 assert_eq!(text, "Alice");
704 if let tl::enums::MessageEntity::MentionName(e) = &ents[0] {
705 assert_eq!(e.user_id, 777);
706 } else {
707 panic!("expected MentionName, got {:?}", ents[0]);
708 }
709 }
710
711 #[test]
712 fn html_inline_code() {
713 let (text, ents) = parse_html("call <code>foo()</code> now");
714 assert_eq!(text, "call foo() now");
715 if let tl::enums::MessageEntity::Code(e) = &ents[0] {
716 assert_eq!(e.offset, 5);
717 assert_eq!(e.length, 5);
718 } else {
719 panic!("expected Code");
720 }
721 }
722
723 #[test]
726 fn html_offset_mid_string() {
727 let (text, ents) = parse_html("Hello <b>bold</b> end");
729 assert_eq!(text, "Hello bold end");
730 if let tl::enums::MessageEntity::Bold(e) = &ents[0] {
731 assert_eq!(e.offset, 6);
732 assert_eq!(e.length, 4);
733 } else {
734 panic!("expected Bold");
735 }
736 }
737
738 #[test]
739 fn html_nested_bold_italic() {
740 let (text, ents) = parse_html("<b>bold <i>both</i> bold</b>");
741 assert_eq!(text, "bold both bold");
742 assert_eq!(ents.len(), 2);
743 let bold = ents
744 .iter()
745 .find(|e| matches!(e, tl::enums::MessageEntity::Bold(_)))
746 .unwrap();
747 if let tl::enums::MessageEntity::Bold(e) = bold {
748 assert_eq!(e.offset, 0);
749 assert_eq!(e.length, 14); }
751 let italic = ents
752 .iter()
753 .find(|e| matches!(e, tl::enums::MessageEntity::Italic(_)))
754 .unwrap();
755 if let tl::enums::MessageEntity::Italic(e) = italic {
756 assert_eq!(e.offset, 5);
757 assert_eq!(e.length, 4); }
759 }
760
761 #[test]
764 fn generate_html_italic() {
765 let entities = vec![tl::enums::MessageEntity::Italic(
766 tl::types::MessageEntityItalic {
767 offset: 0,
768 length: 4,
769 },
770 )];
771 assert_eq!(generate_html("test", &entities), "<i>test</i>");
772 }
773
774 #[test]
775 fn generate_html_underline() {
776 let entities = vec![tl::enums::MessageEntity::Underline(
777 tl::types::MessageEntityUnderline {
778 offset: 0,
779 length: 4,
780 },
781 )];
782 assert_eq!(generate_html("test", &entities), "<u>test</u>");
783 }
784
785 #[test]
786 fn generate_html_strike() {
787 let entities = vec![tl::enums::MessageEntity::Strike(
788 tl::types::MessageEntityStrike {
789 offset: 0,
790 length: 4,
791 },
792 )];
793 assert_eq!(generate_html("test", &entities), "<s>test</s>");
794 }
795
796 #[test]
797 fn generate_html_spoiler() {
798 let entities = vec![tl::enums::MessageEntity::Spoiler(
799 tl::types::MessageEntitySpoiler {
800 offset: 0,
801 length: 6,
802 },
803 )];
804 assert_eq!(
805 generate_html("secret", &entities),
806 "<tg-spoiler>secret</tg-spoiler>"
807 );
808 }
809
810 #[test]
811 fn generate_html_inline_code() {
812 let entities = vec![tl::enums::MessageEntity::Code(
813 tl::types::MessageEntityCode {
814 offset: 0,
815 length: 3,
816 },
817 )];
818 assert_eq!(generate_html("foo", &entities), "<code>foo</code>");
819 }
820
821 #[test]
822 fn generate_html_pre_no_lang() {
823 let entities = vec![tl::enums::MessageEntity::Pre(tl::types::MessageEntityPre {
824 offset: 0,
825 length: 4,
826 language: String::new(),
827 })];
828 let html = generate_html("code", &entities);
829 assert_eq!(html, "<pre><code>code</code></pre>");
830 }
831
832 #[test]
833 fn generate_html_custom_emoji() {
834 let entities = vec![tl::enums::MessageEntity::CustomEmoji(
835 tl::types::MessageEntityCustomEmoji {
836 offset: 0,
837 length: 2,
838 document_id: 555,
839 },
840 )];
841 assert_eq!(
842 generate_html("ok", &entities),
843 "<tg-emoji emoji-id=\"555\">ok</tg-emoji>"
844 );
845 }
846
847 #[test]
848 fn generate_html_mention_name() {
849 let entities = vec![tl::enums::MessageEntity::MentionName(
850 tl::types::MessageEntityMentionName {
851 offset: 0,
852 length: 3,
853 user_id: 42,
854 },
855 )];
856 assert_eq!(
857 generate_html("Bob", &entities),
858 "<a href=\"tg://user?id=42\">Bob</a>"
859 );
860 }
861
862 #[test]
865 fn generate_html_escapes_special_chars() {
866 let (_, empty): (_, Vec<_>) = (String::new(), vec![]);
867 let html = generate_html("a & b < c > d \"e\"", &empty);
868 assert_eq!(html, "a & b < c > d "e"");
869 }
870
871 #[test]
874 fn utf16_offset_emoji_before_entity() {
875 let (text, ents) = parse_markdown("👍 **bold**");
877 assert_eq!(text, "👍 bold");
878 if let tl::enums::MessageEntity::Bold(e) = &ents[0] {
879 assert_eq!(e.offset, 3); assert_eq!(e.length, 4);
881 } else {
882 panic!("expected Bold");
883 }
884 }
885
886 #[test]
887 fn utf16_offset_emoji_bold_in_html() {
888 let (text, ents) = parse_html("👍 <b>bold</b>");
889 assert_eq!(text, "👍 bold");
890 if let tl::enums::MessageEntity::Bold(e) = &ents[0] {
891 assert_eq!(e.offset, 3);
892 assert_eq!(e.length, 4);
893 } else {
894 panic!("expected Bold");
895 }
896 }
897
898 #[test]
899 fn utf16_offset_cjk_char() {
900 let (text, ents) = parse_markdown("中**bold**");
902 assert_eq!(text, "中bold");
903 if let tl::enums::MessageEntity::Bold(e) = &ents[0] {
904 assert_eq!(e.offset, 1);
905 assert_eq!(e.length, 4);
906 } else {
907 panic!("expected Bold");
908 }
909 }
910
911 #[test]
912 fn utf16_offset_surrogate_pair_inside_entity() {
913 let (text, ents) = parse_markdown("**👍**");
915 assert_eq!(text, "👍");
916 if let tl::enums::MessageEntity::Bold(e) = &ents[0] {
917 assert_eq!(e.offset, 0);
918 assert_eq!(e.length, 2); } else {
920 panic!("expected Bold");
921 }
922 }
923
924 #[test]
927 fn generate_markdown_v2_spoiler() {
928 let entities = vec![tl::enums::MessageEntity::Spoiler(
929 tl::types::MessageEntitySpoiler {
930 offset: 0,
931 length: 6,
932 },
933 )];
934 assert_eq!(generate_markdown("secret", &entities), "||secret||");
935 }
936
937 #[test]
938 fn generate_markdown_v2_bold() {
939 let entities = vec![tl::enums::MessageEntity::Bold(
940 tl::types::MessageEntityBold {
941 offset: 0,
942 length: 4,
943 },
944 )];
945 assert_eq!(generate_markdown("bold", &entities), "*bold*");
946 }
947
948 fn roundtrip_md(text: &str, ent: tl::enums::MessageEntity) {
951 let md = generate_markdown(text, &[ent]);
952 let (back, ents2) = parse_markdown(&md);
953 assert_eq!(back, text, "roundtrip text mismatch for: {md:?}");
954 assert_eq!(ents2.len(), 1, "roundtrip entity count wrong for: {md:?}");
955 }
956
957 #[test]
958 fn markdown_roundtrip_bold() {
959 roundtrip_md(
960 "x",
961 tl::enums::MessageEntity::Bold(tl::types::MessageEntityBold {
962 offset: 0,
963 length: 1,
964 }),
965 );
966 }
967 #[test]
968 fn markdown_roundtrip_italic() {
969 roundtrip_md(
970 "x",
971 tl::enums::MessageEntity::Italic(tl::types::MessageEntityItalic {
972 offset: 0,
973 length: 1,
974 }),
975 );
976 }
977 #[test]
978 fn markdown_roundtrip_underline() {
979 roundtrip_md(
980 "x",
981 tl::enums::MessageEntity::Underline(tl::types::MessageEntityUnderline {
982 offset: 0,
983 length: 1,
984 }),
985 );
986 }
987 #[test]
988 fn markdown_roundtrip_strike() {
989 roundtrip_md(
990 "x",
991 tl::enums::MessageEntity::Strike(tl::types::MessageEntityStrike {
992 offset: 0,
993 length: 1,
994 }),
995 );
996 }
997 #[test]
998 fn markdown_roundtrip_spoiler() {
999 roundtrip_md(
1000 "x",
1001 tl::enums::MessageEntity::Spoiler(tl::types::MessageEntitySpoiler {
1002 offset: 0,
1003 length: 1,
1004 }),
1005 );
1006 }
1007 #[test]
1008 fn markdown_roundtrip_code() {
1009 roundtrip_md(
1010 "x",
1011 tl::enums::MessageEntity::Code(tl::types::MessageEntityCode {
1012 offset: 0,
1013 length: 1,
1014 }),
1015 );
1016 }
1017
1018 fn roundtrip_html(text: &str, ent: tl::enums::MessageEntity) {
1021 let html = generate_html(text, &[ent]);
1022 let (back, ents2) = parse_html(&html);
1023 assert_eq!(back, text, "roundtrip text mismatch for: {html:?}");
1024 assert_eq!(ents2.len(), 1, "roundtrip entity count wrong for: {html:?}");
1025 }
1026
1027 #[test]
1028 fn html_roundtrip_bold() {
1029 roundtrip_html(
1030 "x",
1031 tl::enums::MessageEntity::Bold(tl::types::MessageEntityBold {
1032 offset: 0,
1033 length: 1,
1034 }),
1035 );
1036 }
1037 #[test]
1038 fn html_roundtrip_italic() {
1039 roundtrip_html(
1040 "x",
1041 tl::enums::MessageEntity::Italic(tl::types::MessageEntityItalic {
1042 offset: 0,
1043 length: 1,
1044 }),
1045 );
1046 }
1047 #[test]
1048 fn html_roundtrip_underline() {
1049 roundtrip_html(
1050 "x",
1051 tl::enums::MessageEntity::Underline(tl::types::MessageEntityUnderline {
1052 offset: 0,
1053 length: 1,
1054 }),
1055 );
1056 }
1057 #[test]
1058 fn html_roundtrip_strike() {
1059 roundtrip_html(
1060 "x",
1061 tl::enums::MessageEntity::Strike(tl::types::MessageEntityStrike {
1062 offset: 0,
1063 length: 1,
1064 }),
1065 );
1066 }
1067 #[test]
1068 fn html_roundtrip_spoiler() {
1069 roundtrip_html(
1070 "x",
1071 tl::enums::MessageEntity::Spoiler(tl::types::MessageEntitySpoiler {
1072 offset: 0,
1073 length: 1,
1074 }),
1075 );
1076 }
1077 #[test]
1078 fn html_roundtrip_code() {
1079 roundtrip_html(
1080 "x",
1081 tl::enums::MessageEntity::Code(tl::types::MessageEntityCode {
1082 offset: 0,
1083 length: 1,
1084 }),
1085 );
1086 }
1087 #[test]
1088 fn html_roundtrip_blockquote() {
1089 roundtrip_html(
1090 "x",
1091 tl::enums::MessageEntity::Blockquote(tl::types::MessageEntityBlockquote {
1092 collapsed: false,
1093 offset: 0,
1094 length: 1,
1095 }),
1096 );
1097 }
1098 #[test]
1099 fn html_roundtrip_blockquote_expandable() {
1100 roundtrip_html(
1101 "x",
1102 tl::enums::MessageEntity::Blockquote(tl::types::MessageEntityBlockquote {
1103 collapsed: true,
1104 offset: 0,
1105 length: 1,
1106 }),
1107 );
1108 }
1109 #[test]
1110 fn html_roundtrip_custom_emoji() {
1111 let html = generate_html(
1112 "ok",
1113 &[tl::enums::MessageEntity::CustomEmoji(
1114 tl::types::MessageEntityCustomEmoji {
1115 offset: 0,
1116 length: 2,
1117 document_id: 999,
1118 },
1119 )],
1120 );
1121 let (back, ents2) = parse_html(&html);
1122 assert_eq!(back, "ok");
1123 if let tl::enums::MessageEntity::CustomEmoji(e) = &ents2[0] {
1124 assert_eq!(e.document_id, 999);
1125 } else {
1126 panic!("expected CustomEmoji");
1127 }
1128 }
1129 #[test]
1130 fn html_roundtrip_mention_name() {
1131 let html = generate_html(
1132 "Bob",
1133 &[tl::enums::MessageEntity::MentionName(
1134 tl::types::MessageEntityMentionName {
1135 offset: 0,
1136 length: 3,
1137 user_id: 42,
1138 },
1139 )],
1140 );
1141 let (back, ents2) = parse_html(&html);
1142 assert_eq!(back, "Bob");
1143 if let tl::enums::MessageEntity::MentionName(e) = &ents2[0] {
1144 assert_eq!(e.user_id, 42);
1145 } else {
1146 panic!("expected MentionName");
1147 }
1148 }
1149
1150 #[test]
1153 fn rich_md_heading1() {
1154 let blocks = parse_rich_markdown("# Hello");
1155 assert!(matches!(blocks[0], tl::enums::PageBlock::Heading1(_)));
1156 if let tl::enums::PageBlock::Heading1(h) = &blocks[0] {
1157 assert!(matches!(h.text, tl::enums::RichText::TextPlain(_)));
1158 }
1159 }
1160
1161 #[test]
1162 fn rich_md_heading6() {
1163 let blocks = parse_rich_markdown("###### Deep");
1164 assert!(matches!(blocks[0], tl::enums::PageBlock::Heading6(_)));
1165 }
1166
1167 #[test]
1168 fn rich_md_paragraph() {
1169 let blocks = parse_rich_markdown("Hello world");
1170 assert!(matches!(blocks[0], tl::enums::PageBlock::Paragraph(_)));
1171 }
1172
1173 #[test]
1174 fn rich_md_code_block() {
1175 let blocks = parse_rich_markdown("```python\nprint('hi')\n```");
1176 if let tl::enums::PageBlock::Preformatted(p) = &blocks[0] {
1177 assert_eq!(p.language, "python");
1178 assert!(matches!(p.text, tl::enums::RichText::TextPlain(_)));
1179 } else {
1180 panic!("expected Preformatted");
1181 }
1182 }
1183
1184 #[test]
1185 fn rich_md_math_block_backtick() {
1186 let blocks = parse_rich_markdown("```math\nE = mc^2\n```");
1187 assert!(matches!(blocks[0], tl::enums::PageBlock::Math(_)));
1188 if let tl::enums::PageBlock::Math(m) = &blocks[0] {
1189 assert_eq!(m.source, "E = mc^2");
1190 }
1191 }
1192
1193 #[test]
1194 fn rich_md_divider() {
1195 let blocks = parse_rich_markdown("---");
1196 assert!(matches!(blocks[0], tl::enums::PageBlock::Divider));
1197 }
1198
1199 #[test]
1200 fn rich_md_unordered_list() {
1201 let blocks = parse_rich_markdown("- item 1\n- item 2");
1202 assert!(matches!(blocks[0], tl::enums::PageBlock::List(_)));
1203 if let tl::enums::PageBlock::List(l) = &blocks[0] {
1204 assert_eq!(l.items.len(), 2);
1205 }
1206 }
1207
1208 #[test]
1209 fn rich_md_task_list() {
1210 let blocks = parse_rich_markdown("- [ ] todo\n- [x] done");
1211 if let tl::enums::PageBlock::List(l) = &blocks[0] {
1212 assert!(
1213 matches!(l.items[0], tl::enums::PageListItem::Text(ref t) if t.checkbox && !t.checked)
1214 );
1215 assert!(
1216 matches!(l.items[1], tl::enums::PageListItem::Text(ref t) if t.checkbox && t.checked)
1217 );
1218 } else {
1219 panic!("expected List");
1220 }
1221 }
1222
1223 #[test]
1224 fn rich_md_ordered_list() {
1225 let blocks = parse_rich_markdown("1. first\n2. second");
1226 assert!(matches!(blocks[0], tl::enums::PageBlock::OrderedList(_)));
1227 if let tl::enums::PageBlock::OrderedList(l) = &blocks[0] {
1228 assert_eq!(l.items.len(), 2);
1229 }
1230 }
1231
1232 #[test]
1233 fn rich_md_blockquote() {
1234 let blocks = parse_rich_markdown(">Hello\n>World");
1235 assert!(matches!(blocks[0], tl::enums::PageBlock::Blockquote(_)));
1236 }
1237
1238 #[test]
1239 fn rich_md_table() {
1240 let md = "| A | B |\n|---|---|\n| 1 | 2 |";
1241 let blocks = parse_rich_markdown(md);
1242 assert!(matches!(blocks[0], tl::enums::PageBlock::Table(_)));
1243 if let tl::enums::PageBlock::Table(t) = &blocks[0] {
1244 assert_eq!(t.rows.len(), 2); }
1246 }
1247
1248 #[test]
1249 fn rich_md_inline_bold() {
1250 let rt = parse_rich_inline_md("**bold**");
1251 assert!(matches!(rt, tl::enums::RichText::TextBold(_)));
1252 }
1253
1254 #[test]
1255 fn rich_md_inline_italic() {
1256 let rt = parse_rich_inline_md("*italic*");
1257 assert!(matches!(rt, tl::enums::RichText::TextItalic(_)));
1258 }
1259
1260 #[test]
1261 fn rich_md_inline_code() {
1262 let rt = parse_rich_inline_md("`code`");
1263 assert!(matches!(rt, tl::enums::RichText::TextFixed(_)));
1264 }
1265
1266 #[test]
1267 fn rich_md_inline_mark() {
1268 let rt = parse_rich_inline_md("==marked==");
1269 assert!(matches!(rt, tl::enums::RichText::TextMarked(_)));
1270 }
1271
1272 #[test]
1273 fn rich_md_inline_spoiler() {
1274 let rt = parse_rich_inline_md("||secret||");
1275 assert!(matches!(rt, tl::enums::RichText::TextSpoiler(_)));
1276 }
1277
1278 #[test]
1279 fn rich_md_inline_strike() {
1280 let rt = parse_rich_inline_md("~~strike~~");
1281 assert!(matches!(rt, tl::enums::RichText::TextStrike(_)));
1282 }
1283
1284 #[test]
1285 fn rich_md_inline_url() {
1286 let rt = parse_rich_inline_md("[click](https://t.me/)");
1287 assert!(matches!(rt, tl::enums::RichText::TextUrl(_)));
1288 }
1289
1290 #[test]
1291 fn rich_md_inline_mention() {
1292 let rt = parse_rich_inline_md("[User](tg://user?id=42)");
1293 assert!(matches!(rt, tl::enums::RichText::TextMentionName(_)));
1294 if let tl::enums::RichText::TextMentionName(m) = rt {
1295 assert_eq!(m.user_id, 42);
1296 }
1297 }
1298
1299 #[test]
1300 fn rich_md_inline_email_link() {
1301 let rt = parse_rich_inline_md("[mail](mailto:user@example.com)");
1302 assert!(matches!(rt, tl::enums::RichText::TextEmail(_)));
1303 }
1304
1305 #[test]
1306 fn rich_md_inline_phone_link() {
1307 let rt = parse_rich_inline_md("[call](tel:+123456789)");
1308 assert!(matches!(rt, tl::enums::RichText::TextPhone(_)));
1309 }
1310
1311 #[test]
1312 fn rich_md_inline_custom_emoji() {
1313 let rt = parse_rich_inline_md("");
1314 assert!(matches!(rt, tl::enums::RichText::TextCustomEmoji(_)));
1315 if let tl::enums::RichText::TextCustomEmoji(e) = rt {
1316 assert_eq!(e.document_id, 5368324170671202286);
1317 }
1318 }
1319
1320 #[test]
1321 fn rich_md_inline_math() {
1322 let rt = parse_rich_inline_md("$x^2 + y^2$");
1323 assert!(matches!(rt, tl::enums::RichText::TextMath(_)));
1324 if let tl::enums::RichText::TextMath(m) = rt {
1325 assert_eq!(m.source, "x^2 + y^2");
1326 }
1327 }
1328
1329 #[test]
1330 fn rich_md_inline_html_underline() {
1331 let rt = parse_rich_inline_md("<u>underlined</u>");
1332 assert!(matches!(rt, tl::enums::RichText::TextUnderline(_)));
1333 }
1334
1335 #[test]
1336 fn rich_md_inline_html_sub() {
1337 let rt = parse_rich_inline_md("<sub>sub</sub>");
1338 assert!(matches!(rt, tl::enums::RichText::TextSubscript(_)));
1339 }
1340
1341 #[test]
1342 fn rich_md_inline_html_sup() {
1343 let rt = parse_rich_inline_md("<sup>sup</sup>");
1344 assert!(matches!(rt, tl::enums::RichText::TextSuperscript(_)));
1345 }
1346
1347 #[test]
1348 fn rich_md_inline_tg_spoiler_html() {
1349 let rt = parse_rich_inline_md("<tg-spoiler>hidden</tg-spoiler>");
1350 assert!(matches!(rt, tl::enums::RichText::TextSpoiler(_)));
1351 }
1352
1353 #[test]
1354 fn rich_html_heading() {
1355 let blocks = parse_rich_html("<h2>World</h2>");
1356 assert!(matches!(blocks[0], tl::enums::PageBlock::Heading2(_)));
1357 }
1358
1359 #[test]
1360 fn rich_html_paragraph() {
1361 let blocks = parse_rich_html("<p>Hello</p>");
1362 assert!(matches!(blocks[0], tl::enums::PageBlock::Paragraph(_)));
1363 }
1364
1365 #[test]
1366 fn rich_html_preformatted() {
1367 let blocks = parse_rich_html("<pre><code class=\"language-rust\">fn main(){}</code></pre>");
1368 if let tl::enums::PageBlock::Preformatted(p) = &blocks[0] {
1369 assert_eq!(p.language, "rust");
1370 } else {
1371 panic!(
1372 "expected Preformatted, got {:?}",
1373 blocks.get(0).map(|_| "block")
1374 );
1375 }
1376 }
1377
1378 #[test]
1379 fn rich_html_blockquote() {
1380 let blocks = parse_rich_html("<blockquote>Quote<cite>Author</cite></blockquote>");
1381 assert!(matches!(blocks[0], tl::enums::PageBlock::Blockquote(_)));
1382 if let tl::enums::PageBlock::Blockquote(b) = &blocks[0] {
1383 assert!(!matches!(b.caption, tl::enums::RichText::TextEmpty));
1384 }
1385 }
1386
1387 #[test]
1388 fn rich_html_aside_pullquote() {
1389 let blocks = parse_rich_html("<aside>Pull quote<cite>The Author</cite></aside>");
1390 assert!(matches!(blocks[0], tl::enums::PageBlock::Pullquote(_)));
1391 }
1392
1393 #[test]
1394 fn rich_html_hr_divider() {
1395 let blocks = parse_rich_html("<hr/>");
1396 assert!(matches!(blocks[0], tl::enums::PageBlock::Divider));
1397 }
1398
1399 #[test]
1400 fn rich_html_unordered_list() {
1401 let blocks = parse_rich_html("<ul><li>a</li><li>b</li></ul>");
1402 assert!(matches!(blocks[0], tl::enums::PageBlock::List(_)));
1403 if let tl::enums::PageBlock::List(l) = &blocks[0] {
1404 assert_eq!(l.items.len(), 2);
1405 }
1406 }
1407
1408 #[test]
1409 fn rich_html_ordered_list() {
1410 let blocks = parse_rich_html("<ol><li>first</li><li>second</li></ol>");
1411 assert!(matches!(blocks[0], tl::enums::PageBlock::OrderedList(_)));
1412 }
1413
1414 #[test]
1415 fn rich_html_table() {
1416 let blocks = parse_rich_html(
1417 "<table><tr><th>H1</th><th>H2</th></tr><tr><td>v1</td><td>v2</td></tr></table>",
1418 );
1419 assert!(matches!(blocks[0], tl::enums::PageBlock::Table(_)));
1420 if let tl::enums::PageBlock::Table(t) = &blocks[0] {
1421 assert_eq!(t.rows.len(), 2);
1422 }
1423 }
1424
1425 #[test]
1426 fn rich_html_details() {
1427 let blocks = parse_rich_html("<details open><summary>Title</summary>Content</details>");
1428 assert!(matches!(blocks[0], tl::enums::PageBlock::Details(_)));
1429 if let tl::enums::PageBlock::Details(d) = &blocks[0] {
1430 assert!(d.open);
1431 }
1432 }
1433
1434 #[test]
1435 fn rich_html_map() {
1436 let blocks = parse_rich_html("<tg-map lat=\"41.9\" long=\"12.5\" zoom=\"14\"/>");
1437 assert!(matches!(blocks[0], tl::enums::PageBlock::Map(_)));
1438 if let tl::enums::PageBlock::Map(m) = &blocks[0] {
1439 assert_eq!(m.zoom, 14);
1440 }
1441 }
1442
1443 #[test]
1444 fn rich_html_math_block() {
1445 let blocks = parse_rich_html("<tg-math-block>E = mc^2</tg-math-block>");
1446 assert!(matches!(blocks[0], tl::enums::PageBlock::Math(_)));
1447 if let tl::enums::PageBlock::Math(m) = &blocks[0] {
1448 assert_eq!(m.source, "E = mc^2");
1449 }
1450 }
1451
1452 #[test]
1453 fn rich_html_inline_bold() {
1454 let rt = parse_rich_html_inline("<b>bold</b>");
1455 assert!(matches!(rt, tl::enums::RichText::TextBold(_)));
1456 }
1457
1458 #[test]
1459 fn rich_html_inline_spoiler() {
1460 let rt = parse_rich_html_inline("<tg-spoiler>secret</tg-spoiler>");
1461 assert!(matches!(rt, tl::enums::RichText::TextSpoiler(_)));
1462 }
1463
1464 #[test]
1465 fn rich_html_inline_custom_emoji() {
1466 let rt = parse_rich_html_inline("<tg-emoji emoji-id=\"999\">👍</tg-emoji>");
1467 assert!(matches!(rt, tl::enums::RichText::TextCustomEmoji(_)));
1468 if let tl::enums::RichText::TextCustomEmoji(e) = rt {
1469 assert_eq!(e.document_id, 999);
1470 }
1471 }
1472
1473 #[test]
1474 fn rich_html_inline_tg_time() {
1475 let rt = parse_rich_html_inline(
1476 "<tg-time unix=\"1647531900\" format=\"wDT\">22:45 tomorrow</tg-time>",
1477 );
1478 assert!(matches!(rt, tl::enums::RichText::TextDate(_)));
1479 if let tl::enums::RichText::TextDate(d) = rt {
1480 assert_eq!(d.date, 1647531900);
1481 assert!(d.day_of_week);
1482 }
1483 }
1484
1485 #[test]
1486 fn rich_html_photo_block() {
1487 let blocks = parse_rich_html("<img src=\"https://telegram.org/example/photo.jpg\"/>");
1488 assert!(matches!(blocks[0], tl::enums::PageBlock::Photo(_)));
1489 }
1490
1491 #[test]
1492 fn rich_html_video_block() {
1493 let blocks =
1494 parse_rich_html("<video src=\"https://telegram.org/example/video.mp4\"></video>");
1495 assert!(matches!(blocks[0], tl::enums::PageBlock::Video(_)));
1496 }
1497
1498 #[test]
1499 fn rich_html_audio_block() {
1500 let blocks =
1501 parse_rich_html("<audio src=\"https://telegram.org/example/audio.mp3\"></audio>");
1502 assert!(matches!(blocks[0], tl::enums::PageBlock::Audio(_)));
1503 }
1504
1505 #[test]
1506 fn rich_html_collage() {
1507 let blocks = parse_rich_html(
1508 "<tg-collage><img src=\"https://telegram.org/example/photo.jpg\"/><video src=\"https://telegram.org/example/video.mp4\"/></tg-collage>",
1509 );
1510 assert!(matches!(blocks[0], tl::enums::PageBlock::Collage(_)));
1511 if let tl::enums::PageBlock::Collage(c) = &blocks[0] {
1512 assert_eq!(c.items.len(), 2);
1513 }
1514 }
1515
1516 #[test]
1517 fn rich_html_slideshow() {
1518 let blocks = parse_rich_html(
1519 "<tg-slideshow><img src=\"https://telegram.org/example/photo.jpg\"/><video src=\"https://telegram.org/example/video.mp4\"/></tg-slideshow>",
1520 );
1521 assert!(matches!(blocks[0], tl::enums::PageBlock::Slideshow(_)));
1522 }
1523
1524 #[test]
1525 fn rich_html_footer() {
1526 let blocks = parse_rich_html("<footer>Footer text</footer>");
1527 assert!(matches!(blocks[0], tl::enums::PageBlock::Footer(_)));
1528 }
1529
1530 #[test]
1531 fn rich_html_anchor_block() {
1532 let blocks = parse_rich_html("<a name=\"chapter-1\"></a>");
1533 assert!(matches!(blocks[0], tl::enums::PageBlock::Anchor(_)));
1534 if let tl::enums::PageBlock::Anchor(a) = &blocks[0] {
1535 assert_eq!(a.name, "chapter-1");
1536 }
1537 }
1538}