diff --git a/src/parser/org.rs b/src/parser/org.rs index 9019f58..d5ae880 100644 --- a/src/parser/org.rs +++ b/src/parser/org.rs @@ -167,7 +167,8 @@ struct OpenGreater { /// org-element-drawer-re NAME: `(any ?- ?_ word)` — hyphen, underscore, /// or Unicode word characters (letters and digits). `:END:` is the closer. /// org-element plain link at column 0: `file+emacs:` / `file+sys:` / -/// `file:` / `shell:` / `elisp:` / `http://` / `https://` / `mailto:` / +/// `file:` / `shell:` / `elisp:` / `help:` / `info:` / `http://` / +/// `https://` / `mailto:` / /// `news:` / `doi:` / `ftp://` / `attachment:` / `id:` plus the path. /// Leftover after the path is hung Prose. `file+emacs:` / `file+sys:` /// must be matched before `file:` or the `+…` is eaten as the path. @@ -184,6 +185,10 @@ pub(crate) fn org_plain_link_marker_len(line: &str) -> Option { "shell:" } else if t.starts_with("elisp:") { "elisp:" + } else if t.starts_with("help:") { + "help:" + } else if t.starts_with("info:") { + "info:" } else if t.starts_with("https://") { "https://" } else if t.starts_with("http://") { diff --git a/src/sentence/unicode.rs b/src/sentence/unicode.rs index f730033..2e7dfb7 100644 --- a/src/sentence/unicode.rs +++ b/src/sentence/unicode.rs @@ -3373,11 +3373,18 @@ fn split_double_bang_then_letter(segments: Vec) -> Vec { while idx + 1 < chars.len() { if chars[idx].1 == '!' && chars[idx + 1].1 == '!' { if let Some(&(off, ch)) = chars.get(idx + 2) { - if ch.is_alphabetic() && !wrap_closes_after_bang(&seg[start..], off - start) { - out.push(seg[start..off].to_string()); - start = off; - idx += 2; - continue; + if ch.is_alphabetic() { + let prev = idx.checked_sub(1).map(|i| chars[i].1); + let quote_before = matches!( + prev, + Some('"' | '\'' | '\u{201C}' | '\u{2018}' | '\u{00AB}') + ); + if !quote_before && !wrap_closes_after_bang(&seg[start..], off - start) { + out.push(seg[start..off].to_string()); + start = off; + idx += 2; + continue; + } } } } diff --git a/tests/org_file_token_punct.rs b/tests/org_file_token_punct.rs index d4ff3de..484f3d5 100644 --- a/tests/org_file_token_punct.rs +++ b/tests/org_file_token_punct.rs @@ -130,6 +130,36 @@ fn leftover_elisp_plain_link_after_path_hangs_and_splits() { assert_eq!(format_text(&out, &org_cfg()).unwrap(), out); } +#[test] +fn leftover_help_plain_link_after_path_hangs_and_splits() { + let input = concat!("help:org leftover. Next.\n", "After. Next.\n",); + let out = format_text(input, &org_cfg()).unwrap(); + assert!( + !out.contains("help:org leftover. Next."), + "leftover after help path must still split, got:\n{out}" + ); + assert!( + out.contains("After.\nNext."), + "following prose must still split, got:\n{out}" + ); + assert_eq!(format_text(&out, &org_cfg()).unwrap(), out); +} + +#[test] +fn leftover_info_plain_link_after_path_hangs_and_splits() { + let input = concat!("info:org leftover. Next.\n", "After. Next.\n",); + let out = format_text(input, &org_cfg()).unwrap(); + assert!( + !out.contains("info:org leftover. Next."), + "leftover after info path must still split, got:\n{out}" + ); + assert!( + out.contains("After.\nNext."), + "following prose must still split, got:\n{out}" + ); + assert_eq!(format_text(&out, &org_cfg()).unwrap(), out); +} + #[test] fn leftover_attachment_plain_link_after_path_hangs_and_splits() { let input = concat!("attachment:plot.png leftover. Next.\n", "After. Next.\n",); diff --git a/tests/sentence_delim_props.rs b/tests/sentence_delim_props.rs index 2e09b2c..e93b0ad 100644 --- a/tests/sentence_delim_props.rs +++ b/tests/sentence_delim_props.rs @@ -100,6 +100,26 @@ fn plaintext_period_then_latex_quotes_is_idempotent() { ); } +#[test] +fn plaintext_nested_quoted_bang_letter_is_span_safe() { + // ubuntu CI seed: `"!!a"` nested in a delimiter soup. + let input = "\"\"\"`A`]]\"`A`']']]\"!!a\"])]]'''}"; + let out = format_plain(input); + assert_eq!( + format_plain(&out), + out, + "idempotence\n in={input:?}\n out={out:?}" + ); + assert!( + newlines_respect_delimiter_spans(&out), + "span newline\n in={input:?}\n out={out:?}" + ); + assert!( + !out.contains("!!\n"), + "must not split !!a inside quotes\n in={input:?}\n out={out:?}" + ); +} + #[test] fn plaintext_quoted_bang_letter_is_span_safe() { let input = "\"!!a\"";