// Copyright 2026 The V Language. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. module markdown fn test_to_html_heading() { assert to_html('# Hello') == '

Hello

\n' assert to_html('## World') == '

World

\n' } fn test_to_html_paragraph() { assert to_html('Hello world') == '

Hello world

\n' } fn test_to_html_thematic_break() { assert to_html('---') == '
\n' } fn test_to_html_emphasis() { html := to_html('*em*') assert html.contains('') } fn test_to_html_strong() { html := to_html('**bold**') assert html.contains('') } fn test_to_html_code_span() { html := to_html('`code`') assert html.contains('') assert html.contains('code') } fn test_to_html_link() { html := to_html('[link](https://example.com)') assert html.contains('') assert html.contains('link') } fn test_html_escape_in_text() { html := to_html('A < B') assert html.contains('<') } fn test_named_entities_are_decoded_before_render() { assert to_html('©') == '

©

\n' assert to_html('&') == '

&

\n' } fn test_unknown_named_entity_is_left_as_literal_text() { assert to_html('¬_a_real_entity;') == '

&not_a_real_entity;

\n' } fn test_numeric_entities_are_decoded() { assert to_html('© ©') == '

© ©

\n' } fn test_empty_input() { assert to_html('') == '' } fn test_multiline_paragraph() { html := to_html('line one\nline two') assert html.contains('

') assert html.contains('line one') } fn test_fenced_code() { html := to_html('```go\nfn main() {}\n```') assert html.contains('') assert html.contains('

  • ') assert html.contains('item') } fn test_ordered_list() { html := to_html('1. first') assert html.contains('
      ') assert html.contains('
    1. ') assert html.contains('first') } fn test_ordered_list_marker_requires_whitespace_or_eol() { assert to_html('1.test') == '

      1.test

      \n' assert to_html('1)test') == '

      1)test

      \n' } fn test_ordered_list_marker_allows_space_tab_or_eol() { assert to_html('1. item') == '
        \n
      1. item
      2. \n
      \n' assert to_html('1)\titem') == '
        \n
      1. item
      2. \n
      \n' assert to_html('1.') == '
        \n
      1. \n
      \n' } fn test_blockquote() { html := to_html('> quote') assert html.contains('
      ') assert html.contains('quote') } fn test_list_multiple_items() { html := to_html('- item 1\n- item 2') assert html.contains('
        ') assert html.contains('item 1') assert html.contains('item 2') } fn test_invalid_link_ref_def_does_not_create_reference() { src := '[bad]: ok') } fn test_full_reference_does_not_fallback_to_shortcut_when_label_is_undefined() { src := '[text]: https://example.com/text\n\n[text][missing]' html := to_html(src) assert html == '

        [text][missing]

        \n' } fn test_shortcut_reference_still_resolves_normally() { src := '[text]: https://example.com/text\n\n[text]' html := to_html(src) assert html == '

        text

        \n' } fn test_gfm_table_header_uses_th_cells() { src := '| a | b |\n| --- | --- |\n| 1 | 2 |' html := to_html(src, extensions: gfm()) assert html.contains('') assert html.contains('a') assert html.contains('b') } fn test_emphasis_underscore_intraword_does_not_emphasize() { assert to_html('foo_bar_baz') == '

        foo_bar_baz

        \n' assert to_html('foo_bar_') == '

        foo_bar_

        \n' assert to_html('_foo_bar') == '

        _foo_bar

        \n' } fn test_emphasis_star_delimiters_still_emphasize() { assert to_html('a*b*c') == '

        abc

        \n' } fn test_emphasis_triple_delimiters() { assert to_html('***foo***') == '

        foo

        \n' assert to_html('___foo___') == '

        foo

        \n' assert to_html('foo***bar***baz') == '

        foobarbaz

        \n' } fn test_emphasis_nested_mixed_runs() { assert to_html('**foo *bar***') == '

        foo bar

        \n' assert to_html('*foo **bar***') == '

        foo bar

        \n' assert to_html('*foo**bar**baz*') == '

        foobarbaz

        \n' assert to_html('*foo **bar** baz*') == '

        foo bar baz

        \n' assert to_html('**foo *bar* baz**') == '

        foo bar baz

        \n' } fn test_emphasis_multiple_of_three_resolution() { assert to_html('***foo** bar*') == '

        foo bar

        \n' assert to_html('***foo* bar**') == '

        foo bar

        \n' assert to_html('***foo**bar*') == '

        foobar

        \n' } fn test_emphasis_underscore_punctuation_flanking() { assert to_html('foo-_(bar)_') == '

        foo-(bar)

        \n' assert to_html('foo__bar__baz') == '

        foo__bar__baz

        \n' assert to_html('foo__bar__') == '

        foo__bar__

        \n' assert to_html('__foo__bar') == '

        __foo__bar

        \n' } fn test_setext_heading_leading_spaces() { // CommonMark allows 0-3 leading spaces on the setext underline. assert to_html('Foo\n ===') == '

        Foo

        \n' assert to_html('Foo\n ---') == '

        Foo

        \n' assert to_html('Foo\n ===') == '

        Foo

        \n' } fn test_emphasis_leftover_delimiters_are_literal() { // Unmatched delimiters become literal text. assert to_html('*a**b**') == '

        *ab

        \n' assert to_html('**a**b*') == '

        ab*

        \n' assert to_html('*foo bar') == '

        *foo bar

        \n' } fn test_emphasis_mixed_star_underscore() { // * and _ delimiters do not pair with each other. assert to_html('*foo _bar_ baz*') == '

        foo bar baz

        \n' assert to_html('__foo *bar* baz__') == '

        foo bar baz

        \n' } fn test_link_ref_def_with_leading_spaces() { // CommonMark allows 0-3 leading spaces before a link ref def. assert to_html(' [foo]: https://example.com\n\n[foo]') == '

        foo

        \n' assert to_html(' [bar]: https://example.org\n\n[bar]') == '

        bar

        \n' assert to_html(' [baz]: https://v-lang.io\n\n[baz]') == '

        baz

        \n' } fn test_link_ref_def_with_four_leading_spaces_is_not_a_ref() { // Four leading spaces start an indented code block, not a reference definition. src := ' [foo]: https://example.com\n\n[foo]' html := to_html(src) assert !html.contains('Foo\nbar\n' } fn test_task_list() { src := '- [ ] unchecked\n- [x] checked\n- [X] also checked' html := to_html(src, task_list: true) assert html.contains('') assert html.contains('') assert html.contains('unchecked') assert html.contains('checked') } fn test_task_list_not_applied_without_extension() { // Without the extension, task markers are rendered as plain text. html := to_html('- [ ] item') assert !html.contains('') } fn test_footnote_definition_inside_list_item_is_preserved() { src := '- item[^note]\n\n [^note]: footnote in list\n\noutside[^note]' html := to_html(src, footnotes: true) assert html.contains('item1') assert html.contains('outside1') assert html.contains('
      • footnote in list') assert html.contains('
      • ') } fn test_footnote_definition_inside_blockquote_is_preserved() { src := '> quote[^q]\n>\n> [^q]: footnote in quote' html := to_html(src, footnotes: true) assert html.contains('quote1') assert html.contains('
      • footnote in quote') assert html.contains('
      • ') } fn test_link_ref_def_multiline_title() { // CommonMark allows the title on the next line when the destination is alone. src := '[foo]: /url\n"a title"\n\n[foo]' html := to_html(src) assert html.contains('foo') } fn test_link_ref_def_multiline_title_single_quotes() { src := "[bar]: /path\n'my title'\n\n[bar]" html := to_html(src) assert html.contains('baz') assert html.contains('some text') } fn test_gfm_helper_sets_core_extension_flags() { md := Markdown.new(extensions: gfm()) assert md.opts.tables assert md.opts.strikethrough assert md.opts.linkify assert md.opts.task_list } fn test_individual_extension_helpers_set_flags() { md_footnote := Markdown.new(extensions: [Extension(footnote())]) assert md_footnote.opts.footnotes md_typographer := Markdown.new(extensions: [Extension(typographer())]) assert md_typographer.opts.typographer md_definition_list := Markdown.new(extensions: [Extension(definition_list())]) assert md_definition_list.opts.definition_list } fn test_emphasis_goldmark_parity_edge_cases() { assert to_html('_a* __*_* b b') == '

        a* __** b b

        \n' assert to_html('* bb _ *__*a* a_') == '
          \n
        • bb _ *__a a_
        • \n
        \n' assert to_html('baa _ a*aba**_ba') == '

        baa _ a*aba**_ba

        \n' assert to_html('_a_*_b**_aba*') == '

        a_b**_aba

        \n' assert to_html('x_ ***b*ab*bb_a*a a') == '

        x_ babbb_aa a

        \n' } fn test_to_plaintext_basic_blocks_and_inlines() { text := to_plaintext('# Héllo\n\nA *b* [site](https://example.com)') assert text.contains('# Héllo') assert text.contains('A *b* site (https://example.com)') } fn test_to_plaintext_task_list() { text := to_plaintext('- [ ] todo\n- [x] done', task_list: true) assert text.contains('☐') assert text.contains('☑') assert text.contains('todo') assert text.contains('done') } fn test_to_plaintext_footnotes() { text := to_plaintext('Text[^n]\n\n[^n]: note body', footnotes: true) assert text.contains('Text[1]') assert text.contains('Footnotes:') assert text.contains('[1] note body') } fn test_to_plaintext_table_rows_are_separated() { text := to_plaintext('| a | b |\n|---|---|\n| 1 | 2 |', extensions: gfm()) assert text.contains('a | b |') assert text.contains('1 | 2 |') assert text.contains('a | b | \n1 | 2 |') assert !text.contains('a | b | 1 | 2 |') } fn test_to_plaintext_blockquote_footnotes_share_global_order() { text := to_plaintext('> quote[^q]\n\n[^q]: note body', footnotes: true) assert text.contains('> quote[1]') assert text.contains('Footnotes:') assert text.contains('[1] note body') }