use core::cmp::min;
use std::borrow::Cow;
const MASK_CHAR: char = '\u{E000}';
const AOZORA_TRIGGERS: &[char] = &['|', '《', '》', '[', ']', '※', '〔', '〕', '「', '」'];
#[must_use]
pub(crate) fn mask_code_block_triggers(source: &str) -> (Cow<'_, str>, Vec<char>) {
if source.contains(MASK_CHAR) || !source.contains(['`', '~']) {
return (Cow::Borrowed(source), Vec::new());
}
let mut out = String::with_capacity(source.len());
let mut originals: Vec<char> = Vec::new();
let mut phase = Phase::Outside;
let mut masked_anything = false;
for line in source.split_inclusive('\n') {
match phase {
Phase::Outside => {
out.push_str(line);
if let Some(fence) = parse_fence_open(line) {
phase = Phase::InFence(fence);
}
}
Phase::InFence(open) => {
if is_fence_close(line, open) {
out.push_str(line);
phase = Phase::Outside;
} else {
for ch in line.chars() {
if AOZORA_TRIGGERS.contains(&ch) {
originals.push(ch);
out.push(MASK_CHAR);
masked_anything = true;
} else {
out.push(ch);
}
}
}
}
}
}
if masked_anything {
(Cow::Owned(out), originals)
} else {
(Cow::Borrowed(source), Vec::new())
}
}
#[must_use]
pub(crate) fn unmask_html<'a>(html: &'a str, originals: &[char]) -> Cow<'a, str> {
if originals.is_empty() || !html.contains(MASK_CHAR) {
return Cow::Borrowed(html);
}
let mut out = String::with_capacity(html.len());
let mut idx = 0;
for ch in html.chars() {
if ch == MASK_CHAR && idx < originals.len() {
out.push(originals[idx]);
idx += 1;
} else {
out.push(ch);
}
}
Cow::Owned(out)
}
#[derive(Debug, Clone, Copy)]
enum Phase {
Outside,
InFence(FenceOpen),
}
#[derive(Debug, Clone, Copy)]
struct FenceOpen {
marker: u8,
width: usize,
}
fn parse_fence_open(line: &str) -> Option<FenceOpen> {
let stripped = trim_leading_indent(line, 3);
let bytes = stripped.as_bytes();
let &first = bytes.first()?;
if first != b'`' && first != b'~' {
return None;
}
let width = bytes.iter().take_while(|&&b| b == first).count();
(width >= 3).then_some(FenceOpen {
marker: first,
width,
})
}
fn is_fence_close(line: &str, open: FenceOpen) -> bool {
let stripped = trim_leading_indent(line, 3);
let bytes = stripped.as_bytes();
let run = bytes.iter().take_while(|&&b| b == open.marker).count();
if run < open.width {
return false;
}
bytes[run..]
.iter()
.all(|&b| matches!(b, b' ' | b'\t' | b'\n' | b'\r'))
}
fn trim_leading_indent(line: &str, max: usize) -> &str {
let bytes = line.as_bytes();
let cap = min(bytes.len(), max);
let consumed = bytes.iter().take(cap).take_while(|&&b| b == b' ').count();
&line[consumed..]
}
#[cfg(test)]
mod tests {
use super::*;
fn mask_owned(src: &str) -> (String, Vec<char>) {
let (cow, originals) = mask_code_block_triggers(src);
(cow.into_owned(), originals)
}
#[test]
fn no_code_block_no_mask() {
let (cow, originals) = mask_code_block_triggers("|青梅《おうめ》");
assert!(matches!(cow, Cow::Borrowed(_)));
assert_eq!(cow.as_ref(), "|青梅《おうめ》");
assert!(originals.is_empty());
}
#[test]
fn fenced_code_triggers_get_masked() {
let src = "before\n```\n|青梅《おうめ》\n```\nafter";
let (out, originals) = mask_owned(src);
assert!(!out.contains('|'), "trigger leaked: {out:?}");
assert!(!out.contains('《'), "trigger leaked: {out:?}");
assert!(!out.contains('》'), "trigger leaked: {out:?}");
assert!(out.starts_with("before\n```\n"));
assert!(out.ends_with("\n```\nafter"));
assert_eq!(originals, vec!['|', '《', '》']);
}
#[test]
fn tilde_fence_works_too() {
let src = "~~~\n[#改ページ]\n~~~";
let (out, originals) = mask_owned(src);
assert!(!out.contains('['));
assert_eq!(originals, vec!['[', ']']);
}
#[test]
fn close_fence_must_match_marker() {
let src = "```\n|inside\n~~~\n|still\n";
let (_, originals) = mask_owned(src);
assert_eq!(originals, vec!['|', '|']);
}
#[test]
fn close_fence_must_be_at_least_as_wide() {
let src = "````\n|inside\n```\n|still\n";
let (_, originals) = mask_owned(src);
assert_eq!(originals, vec!['|', '|']);
}
#[test]
fn outside_text_is_left_alone() {
let src = "|prose《outside》\n```\n|inside\n```\n|after《tail》";
let (out, originals) = mask_owned(src);
assert!(out.contains("|prose《outside》"), "out: {out}");
assert!(out.contains("|after《tail》"), "out: {out}");
assert_eq!(originals, vec!['|']);
}
#[test]
fn pre_existing_mask_char_disables_masking() {
let src = "\u{E000}\n```\n|trigger\n```";
let (cow, originals) = mask_code_block_triggers(src);
assert!(matches!(cow, Cow::Borrowed(_)));
assert_eq!(cow.as_ref(), src);
assert!(originals.is_empty());
}
#[test]
fn unmask_round_trips_fenced_triggers() {
let src = "```\n|青梅《おうめ》\n```";
let (masked, originals) = mask_owned(src);
let pseudo_html = format!(
"<pre><code>{}\n</code></pre>\n",
&masked[4..masked.len() - 4]
);
let restored = unmask_html(&pseudo_html, &originals);
assert!(restored.contains('|'), "got: {restored}");
assert!(restored.contains('《'));
assert!(restored.contains('》'));
}
#[test]
fn unmask_with_empty_originals_is_a_noop() {
assert_eq!(unmask_html("hello", &[]).as_ref(), "hello");
}
#[test]
fn unmask_handles_more_mask_chars_than_originals_gracefully() {
let originals = vec!['|'];
let masked = format!("{MASK_CHAR}{MASK_CHAR}");
let restored = unmask_html(&masked, &originals);
assert_eq!(restored.chars().filter(|&c| c == '|').count(), 1);
assert_eq!(restored.chars().filter(|&c| c == MASK_CHAR).count(), 1);
}
#[test]
fn indent_up_to_three_spaces_does_not_break_fence_detection() {
let src = " ```\n|inside\n ```\nafter";
let (_, originals) = mask_owned(src);
assert_eq!(originals, vec!['|']);
}
#[test]
fn indent_of_four_spaces_disables_the_fence() {
let src = " ```\n|prose\n ```";
let (out, originals) = mask_owned(src);
assert!(out.contains('|'), "out: {out}");
assert!(originals.is_empty());
}
#[test]
fn crlf_line_endings_are_preserved_through_the_fence() {
let src = "```\r\n|inside\r\n```\r\nafter";
let (out, originals) = mask_owned(src);
assert!(!out.contains('|'), "trigger leaked: {out:?}");
assert_eq!(originals, vec!['|']);
assert!(out.contains("\r\nafter"));
}
}
#[cfg(test)]
mod proptests {
use super::*;
use aozora::proptest::config::default_config;
use aozora::proptest::generators::{aozora_fragment, commonmark_adversarial};
use proptest::prelude::*;
fn aozora_or_commonmark() -> impl Strategy<Value = String> {
prop_oneof![aozora_fragment(40), commonmark_adversarial()]
}
fn outside_fences(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut phase = Phase::Outside;
for line in s.split_inclusive('\n') {
match phase {
Phase::Outside => {
out.push_str(line);
if let Some(fence) = parse_fence_open(line) {
phase = Phase::InFence(fence);
}
}
Phase::InFence(open) => {
if is_fence_close(line, open) {
phase = Phase::Outside;
}
}
}
}
out
}
fn count_triggers(s: &str) -> usize {
s.chars().filter(|c| AOZORA_TRIGGERS.contains(c)).count()
}
proptest! {
#![proptest_config(default_config())]
#[test]
fn no_fence_input_is_borrowed_with_no_originals(s in aozora_fragment(40)) {
let scrubbed: String = s.chars().filter(|c| *c != '`' && *c != '~').collect();
let (masked, originals) = mask_code_block_triggers(&scrubbed);
prop_assert!(matches!(masked, Cow::Borrowed(_)));
prop_assert!(originals.is_empty());
prop_assert_eq!(&*masked, &scrubbed);
}
#[test]
fn pre_existing_mask_char_short_circuits(s in aozora_fragment(40)) {
let mut with_mask = String::with_capacity(s.len() + 1);
with_mask.push(MASK_CHAR);
with_mask.push_str(&s);
let (masked, originals) = mask_code_block_triggers(&with_mask);
prop_assert!(matches!(masked, Cow::Borrowed(_)));
prop_assert!(originals.is_empty());
prop_assert_eq!(&*masked, &with_mask);
}
#[test]
fn mask_then_unmask_is_identity(src in aozora_or_commonmark()) {
let (masked, originals) = mask_code_block_triggers(&src);
let restored = unmask_html(&masked, &originals);
prop_assert_eq!(&*restored, &src);
}
#[test]
fn outside_fence_triggers_are_preserved(src in aozora_or_commonmark()) {
let outside_count = count_triggers(&outside_fences(&src));
let (masked, _) = mask_code_block_triggers(&src);
let masked_count = count_triggers(&masked);
prop_assert!(
masked_count >= outside_count,
"outside-fence triggers were not preserved: outside={outside_count} masked={masked_count}\n\
source: {src:?}\nmasked: {masked:?}"
);
}
}
}