Revert "Optimize string replacement"

This reverts commit 3f600b6e76.
This commit is contained in:
Niklas Mohrin 2026-02-01 21:55:27 +01:00
commit 4e8f6661d3
No known key found for this signature in database
GPG key ID: 0ACD89A5C1DEB3EB
2 changed files with 22 additions and 78 deletions

View file

@ -31,53 +31,3 @@ impl FindFrom for str {
.map(|i| i + start)
}
}
pub(crate) trait ReplaceInplace {
fn replace_inplace(&mut self, pattern: &Self, replacement: &Self) -> &mut Self;
}
impl<T: PartialEq + Copy> ReplaceInplace for [T] {
fn replace_inplace(&mut self, pattern: &Self, replacement: &Self) -> &mut Self {
assert!(replacement.len() <= pattern.len());
let mut read_index = 0;
let mut write_index = 0;
loop {
if let Some(match_dist) = self[read_index..]
.windows(pattern.len())
.position(|win| win == pattern)
{
self.copy_within(read_index..read_index + match_dist, write_index);
read_index += match_dist + pattern.len();
write_index += match_dist;
self[write_index..write_index + replacement.len()].copy_from_slice(replacement);
write_index += replacement.len();
} else {
self.copy_within(read_index.., write_index);
write_index += self.len() - read_index;
return &mut self[..write_index];
}
}
}
}
impl ReplaceInplace for str {
fn replace_inplace(&mut self, pattern: &Self, replacement: &Self) -> &mut Self {
let end = {
// SAFETY: At the end of the lifetime of `self_bytes`, we have written to all bytes.
// The bytes until `end` are valid UTF-8, because UTF-8 substrings matching `pattern`
// are replaced by the UTF-8 string `replacement`. From `end` ongoing, we overwrite
// everything with ascii letters.
let self_bytes = unsafe { self.as_bytes_mut() };
let end = self_bytes
.replace_inplace(pattern.as_bytes(), replacement.as_bytes())
.len();
// Note that if we wouldn't do this, we could have the end of a multi-byte sequence
// left at the end of `self_bytes` which doesn't have the, say, first byte anymore.
self_bytes[end..].fill(b'a');
end
};
&mut self[..end]
}
}

View file

@ -2,10 +2,7 @@
use log::debug;
use crate::{
extensions::{FindFrom, ReplaceInplace},
types::LineType,
};
use crate::{extensions::FindFrom, types::LineType};
#[derive(Debug, Clone, Copy, Eq)]
/// Represents a snippet from a page of a specific highlighting class.
@ -98,9 +95,9 @@ where
}
LineType::Description(text) => process_snippet(PageSnippet::Description(&text))?,
LineType::ExampleText(text) => process_snippet(PageSnippet::Text(&text))?,
LineType::ExampleCode(mut text) => {
LineType::ExampleCode(text) => {
process_snippet(PageSnippet::NormalCode(" "))?;
highlight_code(&command, &mut text, process_snippet)?;
highlight_code(&command, text, process_snippet)?;
process_snippet(PageSnippet::Linebreak)?;
}
@ -114,7 +111,7 @@ where
/// Highlight code examples including user variables in {{ curly braces }}.
fn highlight_code<E>(
command: &str,
mut text: &mut str,
mut text: String,
process_snippet: &mut impl FnMut(PageSnippet<&str>) -> Result<(), E>,
) -> Result<(), E> {
fn find_marker(s: &str, marker: &str, forbidden_prefix: &str) -> Option<usize> {
@ -142,39 +139,36 @@ fn highlight_code<E>(
}
}
fn replace_escaped(s: &mut str) -> &str {
s.replace_inplace(r"\{\{", "{{")
.replace_inplace(r"\}\}", "}}")
}
// NOTE: This is not optimal, as it allocates one String for each `replace`
let replace_escaped = |s: &str| s.replace(r"\{\{", "{{").replace(r"\}\}", "}}");
while !text.is_empty() {
let Some(placeholder_start) = find_marker(&text, "{{", r"\{\{") else {
return highlight_code_segment(command, replace_escaped(text), process_snippet);
return highlight_code_segment(command, &replace_escaped(&text), process_snippet);
};
let Some(mut placeholder_len) = find_marker(&text[placeholder_start..], "}}", r"\}\}")
let Some(mut placeholder_end) = find_marker(&text[placeholder_start + 2..], "}}", r"\}\}")
else {
return highlight_code_segment(command, replace_escaped(text), process_snippet);
return highlight_code_segment(command, &replace_escaped(&text), process_snippet);
};
placeholder_len += 2;
placeholder_end += placeholder_start + 2;
// Greedily extend matched range
while text.as_bytes().get(placeholder_start + placeholder_len) == Some(&b'}') {
placeholder_len += 1;
while placeholder_end + 2 < text.len() && text.as_bytes()[placeholder_end + 2] == b'}' {
placeholder_end += 1;
}
let (segment, placeholder_and_rest) = text.split_at_mut(placeholder_start);
let (placeholder, rest) = placeholder_and_rest.split_at_mut(placeholder_len);
let placeholder_without_markers = &mut placeholder[2..placeholder_len - 2];
let placeholder_content = &text[placeholder_start + 2..placeholder_end];
if !segment.is_empty() {
highlight_code_segment(command, replace_escaped(segment), process_snippet)?;
if placeholder_start > 0 {
highlight_code_segment(
command,
&replace_escaped(&text[..placeholder_start]),
process_snippet,
)?;
}
process_snippet(PageSnippet::Variable(&replace_escaped(placeholder_content)))?;
process_snippet(PageSnippet::Variable(replace_escaped(
placeholder_without_markers,
)))?;
text = rest;
text.replace_range(..placeholder_end + 2, "");
}
Ok(())
@ -264,7 +258,7 @@ mod tests {
Ok::<(), ()>(())
};
highlight_code(cmd, &mut segment.to_string(), &mut process_snippet)
highlight_code(cmd, segment.to_string(), &mut process_snippet)
.expect("highlight code segment failed");
yielded
}