mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(markdown): let a backslash escape a code fence (#2562)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use crate::ast::{AlertType, ListItem, Node, ParserFlags, TableAlignment};
|
use crate::ast::{AlertType, ListItem, Node, ParserFlags, TableAlignment};
|
||||||
use crate::constants::{MAX_AST_NODES, MAX_LINE_LENGTH};
|
use crate::constants::{CODE_FENCE_LENGTH, MAX_AST_NODES, MAX_LINE_LENGTH};
|
||||||
use crate::links::{has_open_inline_code, has_valid_code_fence_language};
|
use crate::links::{has_open_inline_code, has_valid_code_fence_language};
|
||||||
use crate::normalize::{normalize_nodes, replace_trailing_whitespace_with_newline};
|
use crate::normalize::{normalize_nodes, replace_trailing_whitespace_with_newline};
|
||||||
use crate::parser::{MarkdownParser, ParseError, RuntimeState};
|
use crate::parser::{MarkdownParser, ParseError, RuntimeState};
|
||||||
@@ -157,7 +157,7 @@ pub(crate) fn parse_block(state: &mut RuntimeState<'_>) -> Result<BlockParseResu
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ParserFlags::has(state.parser.flags(), ParserFlags::ALLOW_CODE_BLOCKS) {
|
if ParserFlags::has(state.parser.flags(), ParserFlags::ALLOW_CODE_BLOCKS) {
|
||||||
if let Some(fence_pos) = line.find("```") {
|
if let Some(fence_pos) = find_unescaped_fence(&line) {
|
||||||
let starts_with_fence =
|
let starts_with_fence =
|
||||||
starts_with(trimmed, "```") && fence_pos == line.len() - trimmed.len();
|
starts_with(trimmed, "```") && fence_pos == line.len() - trimmed.len();
|
||||||
if starts_with_fence {
|
if starts_with_fence {
|
||||||
@@ -459,7 +459,7 @@ fn parse_code_block(lines: &[Line], current: usize) -> Result<Option<CodeBlockRe
|
|||||||
while fence_length < trimmed_line.len() && byte_at(trimmed_line, fence_length) == b'`' {
|
while fence_length < trimmed_line.len() && byte_at(trimmed_line, fence_length) == b'`' {
|
||||||
fence_length += 1;
|
fence_length += 1;
|
||||||
}
|
}
|
||||||
if fence_length < 3 {
|
if fence_length < CODE_FENCE_LENGTH {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let language_part = &trimmed_line[fence_length..];
|
let language_part = &trimmed_line[fence_length..];
|
||||||
@@ -1197,7 +1197,7 @@ pub(crate) fn opens_code_block_midline(lines: &[Line], index: usize, flags: u32)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let line = &lines[index].text;
|
let line = &lines[index].text;
|
||||||
let Some(fence_pos) = line.find("```") else {
|
let Some(fence_pos) = find_unescaped_fence(line) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let trimmed = trim_start(line);
|
let trimmed = trim_start(line);
|
||||||
@@ -1267,6 +1267,28 @@ fn alert_type(label: &str) -> Option<AlertType> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_unescaped_fence(line: &str) -> Option<usize> {
|
||||||
|
let mut search = 0usize;
|
||||||
|
while let Some(offset) = line[search..].find("```") {
|
||||||
|
let fence_pos = search + offset;
|
||||||
|
if !is_escaped_fence(line, fence_pos) {
|
||||||
|
return Some(fence_pos);
|
||||||
|
}
|
||||||
|
search = fence_pos + 1;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_escaped_fence(line: &str, fence_pos: usize) -> bool {
|
||||||
|
let mut backslashes = 0usize;
|
||||||
|
let mut index = fence_pos;
|
||||||
|
while index > 0 && byte_at(line, index - 1) == b'\\' {
|
||||||
|
backslashes += 1;
|
||||||
|
index -= 1;
|
||||||
|
}
|
||||||
|
backslashes % 2 == 1
|
||||||
|
}
|
||||||
|
|
||||||
fn slice_lines_from_fence(lines: &[Line], current: usize, fence_pos: usize) -> Vec<Line> {
|
fn slice_lines_from_fence(lines: &[Line], current: usize, fence_pos: usize) -> Vec<Line> {
|
||||||
let mut out = Vec::with_capacity(lines.len() - current);
|
let mut out = Vec::with_capacity(lines.len() - current);
|
||||||
out.push(Line {
|
out.push(Line {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
pub const CODE_FENCE_LENGTH: usize = 3;
|
||||||
pub const MAX_AST_NODES: usize = 10_000;
|
pub const MAX_AST_NODES: usize = 10_000;
|
||||||
pub const MAX_INLINE_DEPTH: usize = 10;
|
pub const MAX_INLINE_DEPTH: usize = 10;
|
||||||
pub const MAX_LINES: usize = 10_000;
|
pub const MAX_LINES: usize = 10_000;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use crate::ast::{EmojiKind, Node, ParserFlags, ParserResult};
|
use crate::ast::{EmojiKind, Node, ParserFlags, ParserResult};
|
||||||
use crate::constants::{MAX_INLINE_DEPTH, MAX_LINE_LENGTH};
|
use crate::constants::{CODE_FENCE_LENGTH, MAX_INLINE_DEPTH, MAX_LINE_LENGTH};
|
||||||
use crate::links;
|
use crate::links;
|
||||||
use crate::normalize::{
|
use crate::normalize::{
|
||||||
combine_adjacent_text, compact_empty_text_nodes, flatten_top_level_formatting,
|
combine_adjacent_text, compact_empty_text_nodes, flatten_top_level_formatting,
|
||||||
@@ -125,6 +125,14 @@ fn parse_inline_with_context(
|
|||||||
position += 1 + emoji.len;
|
position += 1 + emoji.len;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if next == b'`' {
|
||||||
|
let run = backtick_run_length(text, position + 1);
|
||||||
|
if run >= CODE_FENCE_LENGTH {
|
||||||
|
accumulated.extend(std::iter::repeat_n(b'`', run));
|
||||||
|
position += 1 + run;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
if is_escapable_character(next)
|
if is_escapable_character(next)
|
||||||
|| is_ordered_list_marker_dot_escape(text, position)
|
|| is_ordered_list_marker_dot_escape(text, position)
|
||||||
|| is_word_dot_escape(text, position)
|
|| is_word_dot_escape(text, position)
|
||||||
@@ -349,6 +357,14 @@ fn regional_indicator_letter(ch: char) -> Option<char> {
|
|||||||
char::from_u32(u32::from(b'a') + codepoint - 0x1f1e6)
|
char::from_u32(u32::from(b'a') + codepoint - 0x1f1e6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn backtick_run_length(text: &str, start: usize) -> usize {
|
||||||
|
let mut run = 0usize;
|
||||||
|
while start + run < text.len() && byte_at(text, start + run) == b'`' {
|
||||||
|
run += 1;
|
||||||
|
}
|
||||||
|
run
|
||||||
|
}
|
||||||
|
|
||||||
fn is_ordered_list_marker_dot_escape(text: &str, backslash_position: usize) -> bool {
|
fn is_ordered_list_marker_dot_escape(text: &str, backslash_position: usize) -> bool {
|
||||||
if backslash_position + 2 >= text.len()
|
if backslash_position + 2 >= text.len()
|
||||||
|| byte_at(text, backslash_position + 1) != b'.'
|
|| byte_at(text, backslash_position + 1) != b'.'
|
||||||
|
|||||||
@@ -202,3 +202,46 @@ fn unterminated_midline_fence_after_a_line_stays_text() {
|
|||||||
json!([{"type": "Text", "content": "hello\nfoo```bar with no closing fence"}])
|
json!([{"type": "Text", "content": "hello\nfoo```bar with no closing fence"}])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escaped_fence_stays_text_on_one_line() {
|
||||||
|
assert_eq!(
|
||||||
|
parse("\\```hello```"),
|
||||||
|
json!([{"type": "Text", "content": "```hello```"}])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escaped_fence_stays_text_across_lines() {
|
||||||
|
assert_eq!(
|
||||||
|
parse("\\```rust\nfn main() {}\n```"),
|
||||||
|
json!([{"type": "Text", "content": "```rust\nfn main() {}\n```"}])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escaped_midline_fence_stays_text() {
|
||||||
|
assert_eq!(
|
||||||
|
parse("label\\```rust\nfn main() {}\n```"),
|
||||||
|
json!([{"type": "Text", "content": "label```rust\nfn main() {}\n```"}])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escaped_backslash_before_fence_still_opens_a_code_block() {
|
||||||
|
assert_eq!(
|
||||||
|
parse("\\\\```hello```"),
|
||||||
|
json!([
|
||||||
|
{"type": "Text", "content": "\\"},
|
||||||
|
{"type": "CodeBlock", "content": "hello"}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn longer_escaped_fence_stays_text() {
|
||||||
|
assert_eq!(
|
||||||
|
parse("\\````hello````"),
|
||||||
|
json!([{"type": "Text", "content": "````hello````"}])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user