// 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 world
\n' } fn test_to_html_thematic_break() { assert to_html('---') == '')
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;') == '¬_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('- ')
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- item
\n
\n'
assert to_html('1)\titem') == '\n- item
\n
\n'
assert to_html('1.') == '\n\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 == '\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]') == '\n'
assert to_html(' [bar]: https://example.org\n\n[bar]') == '\n'
assert to_html(' [baz]: https://v-lang.io\n\n[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')
}