x.markdown: fix numeric reference and URI autolink boundaries

This commit is contained in:
Alexander Medvednikov committed 2026-10-27 12:20:17 +03:00
1 parent ac58c9b1d2
commit 7080058d97
3 files changed
+55 -38

No files matched your search

+2
View File
@@ -23,6 +23,8 @@ extensions. Designed for feature parity with
HTML output follows CommonMark block boundaries, including a newline before a
nested list or code block inside a tight list. Image alt text flattens inline
formatting, nested links, and images to their text content.
Numeric character references follow CommonMark digit limits and replace invalid Unicode values
with U+FFFD. URI autolinks allow empty scheme payloads and reject ASCII control characters.
### GFM Extensions (via `.gfm()` helper or individual extensions)
- **Tables**: `| col | col |` with alignment (`:--`, `:--:`, `--:`)
@@ -0,0 +1,41 @@
// 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_numeric_entities_replace_invalid_unicode_values() {
for source in ['�', '�', '�', '�', '�', '�', '�',
'�'] {
assert to_html(source) == '<p>�</p>\n', source
}
assert to_html('&#1114111; &#x10FFFF;') == '<p>${rune(0x10ffff)} ${rune(0x10ffff)}</p>\n'
assert to_html('&#0000065; &#x000041; &#X000041;') == '<p>A A A</p>\n'
}
fn test_numeric_entity_digit_limits_and_malformed_controls() {
for source in ['&#00000065;', '&#11141110;', '&#x0000041;', '&#x1000000;', '&#;', '&#x;', '&#-1;',
'&#+65;', '&#xG;', '&#12z;', '&# 65;', '&#65', '&#x41'] {
assert to_html(source) == '<p>${source.replace('&', '&amp;')}</p>\n', source
}
}
fn test_uri_autolinks_allow_empty_payload_and_validate_schemes() {
for source in ['ab:', 'http:', 'a+.-:', 'abcdefghijklmnopqrstuvwxyzabcdef:'] {
assert to_html('<${source}>') == '<p><a href="${source}">${source}</a></p>\n', source
}
for source in ['a:', 'abcdefghijklmnopqrstuvwxyzabcdefg:', '1a:', 'a_:', ':'] {
assert to_html('<${source}>') == '<p>&lt;${source}&gt;</p>\n', source
}
}
fn test_uri_autolinks_reject_ascii_controls() {
for c in 0 .. 33 {
assert try_autolink('<ab:${u8(c).ascii_str()}payload>') == -1, c.str()
}
assert try_autolink('<ab:${u8(127).ascii_str()}payload>') == -1
for c in [u8(1), u8(11), u8(31), u8(127)] {
control := c.ascii_str()
assert to_html('<ab:${control}payload>') == '<p>&lt;ab:${control}payload&gt;</p>\n'
}
assert to_html('<ab:foo>') == '<p><a href="ab:foo">ab:foo</a></p>\n'
}
+12 -38
View File
@@ -984,11 +984,11 @@ fn try_autolink(s string) int {
}
for i := 0; i < inner.len; i++ {
c := inner[i]
if c == ` ` || c == `\t` || c == `\n` || c == `\r` || c == `<` {
if c <= 0x20 || c == 0x7f || c == `<` {
return -1
}
}
// Absolute URI: scheme ':' + at least one more character.
// Absolute URI: scheme ':' + zero or more non-control characters.
idx := inner.index(':') or { -1 }
if idx > 0 {
scheme := inner[..idx]
@@ -1001,7 +1001,7 @@ fn try_autolink(s string) int {
break
}
}
if ok && idx + 1 < inner.len {
if ok {
return end
}
}
@@ -1230,10 +1230,10 @@ fn (mut p InlineParser) try_entity() ?&Node {
return text_node(multi)
}
}
// Out-of-range numeric references (> U+10FFFF) are not entities at all
// (they stay literal); zero and surrogates decode to U+FFFD below.
// CommonMark limits the digit count, not the numeric value. Invalid
// Unicode values, zero, and surrogates decode to U+FFFD below.
if candidate.len > 3 && candidate[1] == `#` {
if numeric_entity_too_large(candidate[2..candidate.len - 1]) {
if numeric_entity_too_long(candidate[2..candidate.len - 1]) {
return none
}
}
@@ -1251,39 +1251,13 @@ const multi_rune_entities = {
'ngE': '≧̸'
}
// numeric_entity_too_large reports whether a numeric entity body (the part
// between `&#` and `;`, with optional `x` prefix) denotes a value above
// U+10FFFF. Malformed bodies return false (the decoder rejects them).
fn numeric_entity_too_large(body string) bool {
mut entity_digits := body
mut base := 10
if entity_digits.len > 0 && (entity_digits[0] == `x` || entity_digits[0] == `X`) {
base = 16
entity_digits = entity_digits[1..]
// numeric_entity_too_long checks CommonMark's decimal and hexadecimal digit limits.
// Malformed bodies within these limits are rejected by the entity decoder.
fn numeric_entity_too_long(body string) bool {
if body.len > 0 && (body[0] == `x` || body[0] == `X`) {
return body.len - 1 > 6
}
if entity_digits.len == 0 {
return false
}
limit := u64(0x10ffff)
mut v := u64(0)
for i := 0; i < entity_digits.len; i++ {
c := entity_digits[i]
mut d := u64(0)
if c >= `0` && c <= `9` {
d = u64(c - `0`)
} else if base == 16 && c >= `a` && c <= `f` {
d = u64(c - `a`) + 10
} else if base == 16 && c >= `A` && c <= `F` {
d = u64(c - `A`) + 10
} else {
return false
}
if v > (limit - d) / u64(base) {
return true
}
v = v * u64(base) + d
}
return v > limit
return body.len > 7
}
// parse_newline handles a newline character.