fix(markdown): only open code fences at line start (#2081)

This commit is contained in:
Hampus
2026-08-29 15:59:04 +02:00
committed by GitHub
parent 82941c08c9
commit 34eb10cd88
3 changed files with 25 additions and 61 deletions
+1 -36
View File
@@ -2,7 +2,7 @@
use crate::ast::{AlertType, ListItem, Node, ParserFlags, TableAlignment};
use crate::constants::{MAX_AST_NODES, MAX_LINE_LENGTH};
use crate::links::{has_open_inline_code, has_valid_code_fence_language};
use crate::links::has_valid_code_fence_language;
use crate::normalize::{normalize_nodes, replace_trailing_whitespace_with_newline};
use crate::parser::{MarkdownParser, ParseError, RuntimeState};
use crate::text::{
@@ -175,31 +175,6 @@ pub(crate) fn parse_block(state: &mut RuntimeState<'_>) -> Result<BlockParseResu
}
return Ok(no_block(current, state.parser.node_count));
}
let prefix = &line[..fence_pos];
if has_open_inline_code(prefix) {
return Ok(no_block(current, state.parser.node_count));
}
let inline_nodes = crate::inline::parse_inline(state.parser, prefix, line_offset)?;
let code_lines = slice_lines_from_fence(&state.lines, current, fence_pos);
if let Some(code_result) = parse_code_block(state.parser, &code_lines, 0)? {
let new_line_index = current + code_result.new_line_index;
let mut extra_nodes = Vec::new();
if inline_nodes.len() > 1 {
extra_nodes.extend(inline_nodes[1..].iter().cloned());
}
extra_nodes.push(code_result.node.clone());
if let Some(extra) = &code_result.extra_content {
state.lines[new_line_index].text = extra.content.clone();
state.lines[new_line_index].offset = extra.offset;
}
let first_node = inline_nodes.first().cloned().unwrap_or(code_result.node);
return Ok(BlockParseResult {
node: Some(first_node),
extra_nodes: Some(extra_nodes),
new_line_index,
new_node_count: state.parser.node_count + inline_nodes.len() + 1,
});
}
}
} else if starts_with(trimmed, "```") {
let mut content = line;
@@ -1251,16 +1226,6 @@ fn alert_type(label: &str) -> Option<AlertType> {
}
}
fn slice_lines_from_fence(lines: &[Line], current: usize, fence_pos: usize) -> Vec<Line> {
let mut out = Vec::with_capacity(lines.len() - current);
out.push(Line {
text: lines[current].text[fence_pos..].to_owned(),
offset: lines[current].offset + fence_pos,
});
out.extend(lines[current + 1..].iter().cloned());
out
}
fn normalise_ordinal(items: &[ListItem], ordinal: Option<usize>, ordered: bool) -> Option<usize> {
if !ordered {
return None;
@@ -1078,31 +1078,6 @@ fn has_terminal_tld(text: &str) -> bool {
letter_count >= 2 && i > 0 && byte_at(text, i - 1) == b'.'
}
pub fn has_open_inline_code(text: &str) -> bool {
if !text.contains('`') {
return false;
}
let mut open_len: Option<usize> = None;
let mut index = 0;
while index < text.len() {
if byte_at(text, index) != b'`' {
index += 1;
continue;
}
let mut run = 0usize;
while index + run < text.len() && byte_at(text, index + run) == b'`' {
run += 1;
}
if open_len.is_none() {
open_len = Some(run);
} else if open_len == Some(run) {
open_len = None;
}
index += run;
}
open_len.is_some()
}
pub fn has_valid_code_fence_language(language: &str) -> bool {
if is_whitespace(byte_at(language, 0)) {
return false;
@@ -172,3 +172,27 @@ fn single_line_fence_without_space_is_content() {
json!([{"type":"CodeBlock","content":"hello"}])
);
}
#[test]
fn mid_line_fence_is_literal_text() {
assert_eq!(
parse("note text```rust\nfn main() {}\n```"),
json!([{"type":"Text","content":"note text```rust\nfn main() {}\n```"}])
);
}
#[test]
fn mid_line_fence_spanning_multiple_lines_stays_text() {
assert_eq!(
parse("intro line ```js\nbody one\nbody two\n```"),
json!([{"type":"Text","content":"intro line ```js\nbody one\nbody two\n```"}])
);
}
#[test]
fn line_start_fence_still_opens_code_block() {
assert_eq!(
parse("```rust\nfn main() {}\n```"),
json!([{"type":"CodeBlock","language":"rust","content":"fn main() {}\n"}])
);
}