x.markdown: fix numeric reference and URI autolink boundaries
This commit is contained in:
1 parent
ac58c9b1d2
commit
7080058d97
3 files changed
+55
-38
No files matched your search
@@ -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(' ') == '<p>${rune(0x10ffff)} ${rune(0x10ffff)}</p>\n'
|
||||
assert to_html('A A A') == '<p>A A A</p>\n'
|
||||
}
|
||||
|
||||
fn test_numeric_entity_digit_limits_and_malformed_controls() {
|
||||
for source in ['A', '�', 'A', '�', '&#;', '&#x;', '&#-1;',
|
||||
'&#+65;', '&#xG;', 'z;', '&# 65;', 'A', 'A'] {
|
||||
assert to_html(source) == '<p>${source.replace('&', '&')}</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><${source}></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><ab:${control}payload></p>\n'
|
||||
}
|
||||
assert to_html('<ab:foo>') == '<p><a href="ab:foo">ab:foo</a></p>\n'
|
||||
}
|
||||
+12
-38
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user