From 303157f53d289bb8e67e372897274d835fa9a8b3 Mon Sep 17 00:00:00 2001 From: Blake McAnally Date: Thu, 5 Nov 2026 17:28:19 -0600 Subject: [PATCH] feat(cli): send a thread message to the channel (#8778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > Replaces #8769, which was opened from a fork. ## Summary This change adds `buzz messages send --from-thread `. The flag is the CLI form of desktop "Send to channel". It publishes a new top-level message in the channel. The message carries the tag `["buzz:sent-from-thread", , ]`. The message has no NIP-10 reply tags. ## Design Desktop puts the thread root id in the tag. It does not use the selected message id. Desktop publishes kind 9. The CLI does the same. The CLI checks the root event before it publishes anything. The root must exist. The root must have the same `h` tag as `--channel`. The root must be a thread root. If the id belongs to a reply, the CLI fails and prints the real root id. A missing root exits with a not-found error. Other failures exit with a usage error. No event is published after a failed check. The excerpt comes from the root content. It is a Rust port of `summarizeThreadRoot` in `sentFromThread.ts`. The port clips by code point, as desktop does. It breaks words by UTF-16 unit, as desktop does. `--from-thread` conflicts with `--reply-to` and `--broadcast`. It works only with kind 9. The relay does not validate this tag. Desktop's Tauri code validates it (`message_tags.rs`). The CLI output follows those rules: 64 hex characters, an excerpt of at most 64 characters, and no control characters. The change adds no dependency and no SDK change. The CLI appends the tag to the SDK `build_message` builder. The README describes the flag. This PR does not change `desktop/` (deprecated; see #8840). ## Testing What ran: - `cargo test -p buzz-cli`: 508 passed. - `cargo clippy -p buzz-cli --all-targets -- -D warnings`: clean. - `cargo fmt --all --check`: clean. - `just file-size-check`: passed. Test coverage: - One test calls `cmd_send_message` against a fake relay. It covers success, a reply id, another channel, a missing root, an invalid id, and a non-kind-9 send. The failure cases publish nothing. The success case publishes one event with the tag and without `e` or `broadcast` tags. - One test checks the clap conflicts. - Unit tests cover the excerpt boundaries (63, 64, 65 characters, the word-break threshold, and multi-byte and emoji text). - One test compares the Rust excerpt with 639 vectors. Desktop's own `summarizeThreadRoot` generated the vectors. The first run found an ordering bug (desktop replaces each pattern over the whole text in turn). The code now does the same. Interop evidence: - A node script runs the real `buzz` binary against a local fake relay. It is not a live relay. - The script passes the published tags to desktop's `getSentFromThreadReference` and `getSentFromThreadRootId`. - Desktop accepts the tag, returns the same root id, and returns the same excerpt as `summarizeThreadRoot`. - Output: `["buzz:sent-from-thread","abab…ab","Launch plan for the doc 😀 word word word word word word word…"]` then `INTEROP OK`. Mutation checks. Each guard was broken once. A test failed each time. Then the guard was restored. - Channel check removed: success test fails. - Reply-root check removed: success test fails. - Kind check removed: success test fails. - `--broadcast` conflict removed: clap test fails. - Word-break threshold `>` changed to `>=`: three excerpt tests fail. - UTF-16 break changed to code-point break: two excerpt tests fail. - Tag not attached: success test fails. ## Manual test steps Use a test channel. You need `BUZZ_RELAY_URL` and `BUZZ_PRIVATE_KEY` set. Build with `cargo build -p buzz-cli`. 1. Post a root message. Copy its event id as `ROOT`. `buzz messages send --channel --content "Launch plan: [doc](https://example.com)"` 2. Send it to the channel. `buzz messages send --channel --content "Decision: ship it" --from-thread $ROOT` Success: the output has `"accepted":true`. A new top-level message appears in the channel. 3. Read it back. `buzz --format json messages get --channel --limit 1` Success: the tags include `["buzz:sent-from-thread","","Launch plan: doc"]`. There are no `e` tags. 4. Open the channel in desktop. Success: the message shows the "sent from thread" line for that root. 5. Try a reply id. `buzz messages send --channel --content x --from-thread ` Success: exit code 1. The error names the root id. Nothing is published. 6. Try a root from another channel, and try `--reply-to` together with `--from-thread`. Success: both fail and publish nothing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: bmcanally Co-authored-by: Claude Opus 5.5 --- crates/buzz-cli/README.md | 1 + crates/buzz-cli/src/commands/messages.rs | 234 +- crates/buzz-cli/src/commands/mod.rs | 1 + .../buzz-cli/src/commands/sent_from_thread.rs | 255 ++ .../commands/sent_from_thread_vectors.json | 2558 +++++++++++++++++ crates/buzz-cli/src/lib.rs | 28 +- 6 files changed, 3072 insertions(+), 5 deletions(-) create mode 100644 crates/buzz-cli/src/commands/sent_from_thread.rs create mode 100644 crates/buzz-cli/src/commands/sent_from_thread_vectors.json diff --git a/crates/buzz-cli/README.md b/crates/buzz-cli/README.md index c279008a4e..104e566c98 100644 --- a/crates/buzz-cli/README.md +++ b/crates/buzz-cli/README.md @@ -32,6 +32,7 @@ export BUZZ_RELAY_URL="https://relay.example.com" buzz messages send --channel --content "Hello" buzz messages send --channel --content "Reply" --reply-to --broadcast buzz messages send --channel --content - < message.md # read body from stdin +buzz messages send --channel --content "Decision: ship" --from-thread # desktop "Send to channel" buzz messages get --channel --limit 20 buzz messages thread --channel --event buzz messages thread --link 'buzz://message?channel=&id=&thread=' diff --git a/crates/buzz-cli/src/commands/messages.rs b/crates/buzz-cli/src/commands/messages.rs index a453d57c25..6babbcc61f 100644 --- a/crates/buzz-cli/src/commands/messages.rs +++ b/crates/buzz-cli/src/commands/messages.rs @@ -4,6 +4,7 @@ use uuid::Uuid; use crate::client::{normalize_events, normalize_write_response, BuzzClient}; use crate::commands::event_output::{format_events, thread_markers}; +use crate::commands::sent_from_thread; use crate::error::CliError; use crate::validate::{ infer_language, parse_event_id, parse_uuid, read_or_stdin, truncate_diff, @@ -579,6 +580,46 @@ pub struct SendMessageParams { pub broadcast: bool, pub files: Vec, pub mentions: Vec, + pub from_thread: Option, +} + +/// Verify the thread root exists in `channel` and build the provenance tag +/// desktop's "Send to channel" attaches. +async fn resolve_sent_from_thread_tag( + client: &BuzzClient, + channel: Uuid, + root_event_id: &str, +) -> Result { + let root_hex = parse_event_id(root_event_id)?.to_hex(); + let root = fetch_event(client, &root_hex).await?; + sent_from_thread_tag_for_root(channel, &root_hex, &root) +} + +fn sent_from_thread_tag_for_root( + channel: Uuid, + root_hex: &str, + root: &serde_json::Value, +) -> Result { + if channel_id_from_event(root_hex, root)? != channel { + return Err(CliError::Usage(format!( + "--from-thread event {root_hex} is not in channel {channel}" + ))); + } + if root.get("kind").and_then(|k| k.as_u64()) != Some(9) { + return Err(CliError::Usage(format!( + "--from-thread event {root_hex} is not an ordinary channel message (kind 9)" + ))); + } + if let Some(real_root) = root.get("tags").and_then(find_root_from_tags) { + return Err(CliError::Usage(format!( + "--from-thread takes a thread root, but {root_hex} is a reply; use {real_root}" + ))); + } + let content = root.get("content").and_then(|c| c.as_str()).unwrap_or(""); + Ok(sent_from_thread::build_tag( + root_hex, + sent_from_thread::summarize_thread_root(content).as_deref(), + )) } pub async fn cmd_send_message( @@ -595,6 +636,15 @@ pub async fn cmd_send_message( validate_hex64(r)?; } let channel_uuid = parse_uuid(&p.channel_id)?; + if p.from_thread.is_some() && !matches!(p.kind, None | Some(9)) { + return Err(CliError::Usage( + "--from-thread only applies to kind 9 messages".into(), + )); + } + let sent_from_thread_tag = match p.from_thread.as_deref() { + Some(root) => Some(resolve_sent_from_thread_tag(client, channel_uuid, root).await?), + None => None, + }; let explicit_mentions = normalize_explicit_mentions(&p.mentions)?; let stripped = strip_code_regions(&p.content); @@ -697,7 +747,7 @@ pub async fn cmd_send_message( } else { Vec::new() }; - buzz_sdk::build_message( + let builder = buzz_sdk::build_message( channel_uuid, &final_content, thread_ref.as_ref(), @@ -706,7 +756,11 @@ pub async fn cmd_send_message( &media_tags, &emoji_tags, ) - .map_err(|e| CliError::Other(format!("build_message failed: {e}")))? + .map_err(|e| CliError::Other(format!("build_message failed: {e}")))?; + match sent_from_thread_tag { + Some(tag) => builder.tag(tag), + None => builder, + } } Some(k) => { return Err(CliError::Usage(format!( @@ -918,6 +972,7 @@ pub async fn dispatch( broadcast, files, mentions, + from_thread, } => { cmd_send_message( client, @@ -929,6 +984,7 @@ pub async fn dispatch( broadcast, files, mentions, + from_thread, }, ) .await @@ -1079,8 +1135,8 @@ mod tests { channel_id_from_event, cmd_get_thread, cmd_send_message, event_mention_pubkeys, find_root_from_tags, match_profiles_by_name, merge_message_mentions, missing_members, normalize_explicit_mentions, parse_member_pubkeys, resolve_names_to_pubkeys, - resolve_thread_target, thread_ref_from_event, thread_ref_from_parent_tags, BuzzClient, - CliError, Uuid, + resolve_thread_target, sent_from_thread_tag_for_root, thread_ref_from_event, + thread_ref_from_parent_tags, BuzzClient, CliError, Uuid, }; use buzz_sdk::mentions::{ extract_at_mentions_with_known, extract_at_names, match_names_to_profiles, MentionProfile, @@ -1142,6 +1198,32 @@ mod tests { ); } + #[test] + fn from_thread_accepts_only_top_level_kind_9_roots_in_the_channel() { + let channel = "123e4567-e89b-12d3-a456-426614174000"; + let other = "123e4567-e89b-12d3-a456-426614174001"; + let channel_uuid = Uuid::parse_str(channel).unwrap(); + let root = |kind: u64, h: &str| json!({"kind": kind, "content": "hi", "tags": [["h", h]]}); + + let tag = sent_from_thread_tag_for_root(channel_uuid, ID_A, &root(9, channel)).unwrap(); + assert_eq!(tag.as_slice()[1], ID_A); + + for kind in [40002, 40008, 45001, 45003] { + let err = sent_from_thread_tag_for_root(channel_uuid, ID_A, &root(kind, channel)) + .unwrap_err(); + assert!(matches!(err, CliError::Usage(_)), "kind {kind}"); + assert!(err.to_string().contains("kind 9"), "kind {kind}"); + } + let no_kind = json!({"content": "hi", "tags": [["h", channel]]}); + assert!(sent_from_thread_tag_for_root(channel_uuid, ID_A, &no_kind).is_err()); + assert!(sent_from_thread_tag_for_root(channel_uuid, ID_A, &root(9, other)).is_err()); + let reply = json!({"kind": 9, "tags": [["h", channel], ["e", ID_B, "", "reply"]]}); + assert!(sent_from_thread_tag_for_root(channel_uuid, ID_A, &reply) + .unwrap_err() + .to_string() + .contains(ID_B)); + } + #[test] fn selected_event_requires_a_valid_channel_tag() { let missing = json!({"tags": []}); @@ -1682,6 +1764,7 @@ mod tests { broadcast: false, files: vec![], mentions: vec![], + from_thread: None, } } @@ -1853,4 +1936,147 @@ mod tests { "palette-error fallback must produce no emoji tags, got: {emoji_tags:?}" ); } + + /// Fake relay that answers `/query` by `ids` filter from `stored` and + /// records every `/events` body. + async fn fake_thread_relay( + stored: Vec, + ) -> (String, StdArc>>) { + type S = ( + Vec, + StdArc>>, + ); + let published = StdArc::new(std::sync::Mutex::new(Vec::new())); + let app = AxumRouter::new() + .route( + "/query", + axum_post( + |AxumState((stored, _)): AxumState, body: AxumBytes| async move { + let filter: serde_json::Value = serde_json::from_slice(&body).unwrap(); + let filter = filter.get(0).unwrap_or(&filter); + let found: Vec<_> = stored + .iter() + .filter(|e| { + filter["ids"] + .as_array() + .is_some_and(|ids| ids.contains(&e["id"])) + }) + .collect(); + ( + AxumStatusCode::OK, + [("content-type", "application/json")], + serde_json::to_string(&found).unwrap(), + ) + }, + ), + ) + .route( + "/events", + axum_post( + |AxumState((_, published)): AxumState, body: AxumBytes| async move { + published + .lock() + .unwrap() + .push(String::from_utf8_lossy(&body).to_string()); + ( + AxumStatusCode::OK, + [("content-type", "application/json")], + r#"{"event_id":"fake0000","accepted":true}"#, + ) + }, + ), + ) + .with_state((stored, published.clone())); + let listener = TokioTcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr: StdSocketAddr = listener.local_addr().unwrap(); + tokio::spawn(async move { axum::serve(listener, app).await.unwrap() }); + (format!("http://{addr}"), published) + } + + fn stored_message( + id: &str, + channel: &str, + content: &str, + extra: &[serde_json::Value], + ) -> serde_json::Value { + let mut tags = vec![serde_json::json!(["h", channel])]; + tags.extend(extra.iter().cloned()); + serde_json::json!({ "id": id, "kind": 9, "content": content, "tags": tags }) + } + + #[tokio::test] + async fn send_from_thread_publishes_a_top_level_tagged_message() { + let root = "a".repeat(64); + let reply = "b".repeat(64); + let other_channel = "223e4567-e89b-12d3-a456-426614174000"; + let (url, published) = fake_thread_relay(vec![ + stored_message( + &root, + SEND_TEST_CHANNEL, + "**Plan**: see [doc](https://x.io) 😀", + &[], + ), + stored_message( + &reply, + SEND_TEST_CHANNEL, + "reply", + &[serde_json::json!(["e", root, "", "reply"])], + ), + stored_message(&"c".repeat(64), other_channel, "elsewhere", &[]), + ]) + .await; + let client = BuzzClient::new(url, Keys::generate(), None, None).unwrap(); + + let send = |from_thread: &str, kind: Option| { + let mut params = send_params("decision: ship it"); + params.from_thread = Some(from_thread.to_string()); + params.kind = kind; + cmd_send_message(&client, params) + }; + + for (target, kind) in [ + (reply.as_str(), None), + ("c".repeat(64).as_str(), None), + ("d".repeat(64).as_str(), None), + ("not-hex", None), + (root.as_str(), Some(45001)), + ] { + let err = send(target, kind).await.unwrap_err(); + let expected = match target { + t if t == "d".repeat(64) => matches!(err, CliError::NotFound(_)), + _ => matches!(err, CliError::Usage(_)), + }; + assert!(expected, "{target} {kind:?}: {err}"); + if target == reply { + assert!(err.to_string().contains(&root), "{err}"); + } + } + assert!( + published.lock().unwrap().is_empty(), + "failed checks must not publish" + ); + + send(&root.to_uppercase(), None).await.unwrap(); + let bodies = published.lock().unwrap(); + assert_eq!(bodies.len(), 1, "one send is one publish"); + let event: serde_json::Value = serde_json::from_str(&bodies[0]).unwrap(); + assert_eq!(event["kind"], 9); + assert_eq!(event["content"], "decision: ship it"); + let tags = event["tags"].as_array().unwrap(); + assert!(tags.contains(&serde_json::json!(["h", SEND_TEST_CHANNEL]))); + assert!( + tags.contains(&serde_json::json!([ + "buzz:sent-from-thread", + root, + "Plan : see doc 😀" + ])), + "{tags:?}" + ); + assert!( + !tags + .iter() + .any(|t| matches!(t[0].as_str(), Some("e" | "broadcast"))), + "must be top-level: {tags:?}" + ); + } } diff --git a/crates/buzz-cli/src/commands/mod.rs b/crates/buzz-cli/src/commands/mod.rs index cd9f94a21c..3634f12ea9 100644 --- a/crates/buzz-cli/src/commands/mod.rs +++ b/crates/buzz-cli/src/commands/mod.rs @@ -24,6 +24,7 @@ pub mod reactions; pub(crate) mod read_state; mod repo_default_branch; pub mod repos; +pub(crate) mod sent_from_thread; pub mod social; pub mod upload; pub mod users; diff --git a/crates/buzz-cli/src/commands/sent_from_thread.rs b/crates/buzz-cli/src/commands/sent_from_thread.rs new file mode 100644 index 0000000000..e30928be88 --- /dev/null +++ b/crates/buzz-cli/src/commands/sent_from_thread.rs @@ -0,0 +1,255 @@ +//! `buzz:sent-from-thread` provenance tag, mirroring desktop's +//! `features/messages/lib/sentFromThread.ts` and the validation in +//! `desktop/src-tauri/src/events/message_tags.rs`. + +use nostr::{Tag, TagKind}; + +const TAG_NAME: &str = "buzz:sent-from-thread"; +const EXCERPT_MAX_CHARS: usize = 64; +const MIN_WORD_BREAK_UTF16: usize = 32; + +/// JavaScript `\s` / `String.prototype.trim` whitespace. +fn is_js_space(c: char) -> bool { + c.is_whitespace() || c == '\u{feff}' +} + +fn starts_with_at(chars: &[char], at: usize, pat: &str) -> bool { + pat.chars() + .enumerate() + .all(|(i, p)| chars.get(at + i) == Some(&p)) +} + +fn find_from(chars: &[char], from: usize, pat: &str) -> Option { + (from..chars.len()).find(|&i| starts_with_at(chars, i, pat)) +} + +/// Match `||…||` (no `||` inside) at `at`; returns the end index. +fn spoiler_end(chars: &[char], at: usize) -> Option { + if !starts_with_at(chars, at, "||") { + return None; + } + find_from(chars, at + 2, "||").map(|close| close + 2) +} + +/// Match `[label](target)` at `at`; returns `(end, label_end)` where the label +/// is `at + 1..label_end`. `min_label` is the minimum label length. +fn link_end(chars: &[char], at: usize, min_label: usize) -> Option<(usize, usize)> { + if chars.get(at) != Some(&'[') { + return None; + } + let close = (at + 1..chars.len()).find(|&i| chars[i] == ']')?; + if close - (at + 1) < min_label || chars.get(close + 1) != Some(&'(') { + return None; + } + let paren = (close + 2..chars.len()).find(|&i| chars[i] == ')')?; + Some((paren + 1, close)) +} + +/// Match `?` at `at`; returns the end index. +fn url_end(chars: &[char], at: usize) -> Option { + let start = if chars.get(at) == Some(&'<') { + at + 1 + } else { + at + }; + let after_scheme = if starts_with_at(chars, start, "https://") { + start + 8 + } else if starts_with_at(chars, start, "http://") { + start + 7 + } else { + return None; + }; + let end = (after_scheme..chars.len()) + .find(|&i| is_js_space(chars[i])) + .unwrap_or(chars.len()); + (end > after_scheme).then_some(end) +} + +/// Left-to-right `String.prototype.replace` with a global pattern: `matcher` +/// returns the match end and replacement for a match starting at the index. +fn replace_all( + chars: &[char], + matcher: impl Fn(&[char], usize) -> Option<(usize, Vec)>, +) -> Vec { + let mut out = Vec::with_capacity(chars.len()); + let mut i = 0; + while i < chars.len() { + match matcher(chars, i) { + Some((end, replacement)) => { + out.extend(replacement); + i = end; + } + None => { + out.push(chars[i]); + i += 1; + } + } + } + out +} + +fn strip_markdown(content: &str) -> String { + let controls_removed: Vec = content + .chars() + .map(|c| { + let cp = c as u32; + if cp <= 0x1f || (0x7f..=0x9f).contains(&cp) { + ' ' + } else { + c + } + }) + .collect(); + let spoilers = replace_all(&controls_removed, |chars, at| { + spoiler_end(chars, at).map(|end| (end, vec![' '])) + }); + let images = replace_all(&spoilers, |chars, at| { + if chars[at] != '!' { + return None; + } + link_end(chars, at + 1, 0).map(|(end, _)| (end, vec![' '])) + }); + let links = replace_all(&images, |chars, at| { + link_end(chars, at, 1).map(|(end, label_end)| (end, chars[at + 1..label_end].to_vec())) + }); + let urls = replace_all(&links, |chars, at| { + url_end(chars, at).map(|end| (end, vec![' '])) + }); + urls.into_iter() + .map(|c| if "`*_~>#|".contains(c) { ' ' } else { c }) + .collect() +} + +fn collapse_whitespace(text: &str) -> String { + text.split(is_js_space) + .filter(|part| !part.is_empty()) + .collect::>() + .join(" ") +} + +/// Summarize a thread root for the tag excerpt exactly as desktop's +/// `summarizeThreadRoot` does. Returns `None` when nothing readable remains. +pub(crate) fn summarize_thread_root(content: &str) -> Option { + let normalized = collapse_whitespace(&strip_markdown(content)); + if normalized.is_empty() { + return None; + } + let chars: Vec = normalized.chars().collect(); + if chars.len() <= EXCERPT_MAX_CHARS { + return Some(normalized); + } + let clipped: String = chars[..EXCERPT_MAX_CHARS - 1].iter().collect(); + let units: Vec = clipped.encode_utf16().collect(); + let excerpt = match units.iter().rposition(|&u| u == u16::from(b' ')) { + Some(space) if space > MIN_WORD_BREAK_UTF16 => String::from_utf16_lossy(&units[..space]), + _ => clipped, + }; + Some(format!("{}…", excerpt.trim_end_matches(is_js_space))) +} + +/// Build the `["buzz:sent-from-thread", , ]` tag. +pub(crate) fn build_tag(root_event_id: &str, excerpt: Option<&str>) -> Tag { + let mut parts = vec![root_event_id.to_string()]; + parts.extend(excerpt.map(str::to_string)); + Tag::custom(TagKind::custom(TAG_NAME), parts) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn s(content: &str) -> Option { + summarize_thread_root(content) + } + + #[test] + fn strips_markdown_like_desktop() { + assert_eq!(s(" hello\n\tworld ").as_deref(), Some("hello world")); + assert_eq!( + s("**bold** _it_ `code` ~~x~~ > q # h").as_deref(), + Some("bold it code x q h") + ); + assert_eq!( + s("see [the doc](https://x.io/a) now").as_deref(), + Some("see the doc now") + ); + assert_eq!(s("a ![alt](https://x.io/i.png) b").as_deref(), Some("a b")); + assert_eq!(s("a ||secret | x|| b").as_deref(), Some("a b")); + assert_eq!(s("a ||unclosed b").as_deref(), Some("a unclosed b")); + assert_eq!( + s("go and http://y.io z").as_deref(), + Some("go and z") + ); + assert_eq!(s("[](x) [a]b").as_deref(), Some("[](x) [a]b")); + assert_eq!(s("a\u{0}b\u{7f}c\u{85}d").as_deref(), Some("a b c d")); + assert_eq!(s("a\u{feff}b\u{a0}c").as_deref(), Some("a b c")); + assert_eq!(s("*** ||x|| ![i](u)"), None); + assert_eq!(s(""), None); + } + + #[test] + fn clips_at_the_desktop_boundaries() { + let exact = "a".repeat(64); + assert_eq!(s(&exact).as_deref(), Some(exact.as_str())); + + let unbroken = "a".repeat(65); + assert_eq!(s(&unbroken), Some(format!("{}…", "a".repeat(63)))); + + let words = format!("{} {}", "a".repeat(40), "b".repeat(40)); + assert_eq!(s(&words), Some(format!("{}…", "a".repeat(40)))); + + let early_space = format!("{} {}", "a".repeat(32), "b".repeat(40)); + assert_eq!( + s(&early_space), + Some(format!("{} {}…", "a".repeat(32), "b".repeat(30))) + ); + + let late_space = format!("{} {}", "a".repeat(33), "b".repeat(40)); + assert_eq!(s(&late_space), Some(format!("{}…", "a".repeat(33)))); + } + + #[test] + fn clips_by_code_point_and_breaks_by_utf16_unit() { + let wide = "é".repeat(65); + assert_eq!(s(&wide), Some(format!("{}…", "é".repeat(63)))); + let emoji = "😀".repeat(65); + assert_eq!(s(&emoji), Some(format!("{}…", "😀".repeat(63)))); + let kept = "😀".repeat(64); + assert_eq!(s(&kept).as_deref(), Some(kept.as_str())); + + // 16 emoji take 32 UTF-16 units, so the space sits at unit 32: kept whole. + let at_32 = format!("{} {}", "😀".repeat(16), "b".repeat(60)); + assert_eq!( + s(&at_32), + Some(format!("{} {}…", "😀".repeat(16), "b".repeat(46))) + ); + // 17 emoji put the space at unit 34 (> 32), though only 17 code points in: break there. + let at_34 = format!("{} {}", "😀".repeat(17), "b".repeat(60)); + assert_eq!(s(&at_34), Some(format!("{}…", "😀".repeat(17)))); + } + + #[test] + fn matches_desktop_summarize_thread_root_on_generated_vectors() { + let vectors: Vec = + serde_json::from_str(include_str!("sent_from_thread_vectors.json")).unwrap(); + assert!(vectors.len() > 600); + for vector in vectors { + let input = vector["input"].as_str().unwrap(); + assert_eq!( + s(input).as_deref(), + vector["expected"].as_str(), + "input: {input:?}" + ); + } + } + + #[test] + fn tag_has_desktop_shape() { + let id = "a".repeat(64); + assert_eq!(build_tag(&id, None).as_slice(), [TAG_NAME, id.as_str()]); + assert_eq!( + build_tag(&id, Some("hi")).as_slice(), + [TAG_NAME, id.as_str(), "hi"] + ); + } +} diff --git a/crates/buzz-cli/src/commands/sent_from_thread_vectors.json b/crates/buzz-cli/src/commands/sent_from_thread_vectors.json new file mode 100644 index 0000000000..b72c329fef --- /dev/null +++ b/crates/buzz-cli/src/commands/sent_from_thread_vectors.json @@ -0,0 +1,2558 @@ +[ + { + "input": "", + "expected": null + }, + { + "input": " ", + "expected": null + }, + { + "input": "hello", + "expected": "hello" + }, + { + "input": " hello\n\tworld ", + "expected": "hello world" + }, + { + "input": "**bold** _it_ `code` ~~x~~ > q # h", + "expected": "bold it code x q h" + }, + { + "input": "see [the doc](https://x.io/a) now", + "expected": "see the doc now" + }, + { + "input": "a ![alt](https://x.io/i.png) b", + "expected": "a b" + }, + { + "input": "a ||secret | x|| b", + "expected": "a b" + }, + { + "input": "a ||unclosed b", + "expected": "a unclosed b" + }, + { + "input": "||a|||b||", + "expected": "b" + }, + { + "input": "||||", + "expected": null + }, + { + "input": "|||", + "expected": null + }, + { + "input": "go and http://y.io z", + "expected": "go and z" + }, + { + "input": "http:// x", + "expected": "http:// x" + }, + { + "input": "https://", + "expected": "https://" + }, + { + "input": "[](x) [a]b", + "expected": "[](x) [a]b" + }, + { + "input": "[a[b](c)", + "expected": "a[b" + }, + { + "input": "![a](b", + "expected": "![a](b" + }, + { + "input": "![](x)", + "expected": null + }, + { + "input": "[a](b(c)d)", + "expected": "ad)" + }, + { + "input": "a\u0000bc…d", + "expected": "a b c d" + }, + { + "input": "ab c d", + "expected": "a b c d" + }, + { + "input": "*** ||x|| ![i](u)", + "expected": null + }, + { + "input": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + { + "input": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "expected": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…" + }, + { + "input": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "expected": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…" + }, + { + "input": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "expected": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb…" + }, + { + "input": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "expected": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…" + }, + { + "input": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "expected": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…" + }, + { + "input": "ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", + "expected": "ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé…" + }, + { + "input": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀", + "expected": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + }, + { + "input": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀", + "expected": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀…" + }, + { + "input": "漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢", + "expected": "漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢漢…" + }, + { + "input": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "expected": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb…" + }, + { + "input": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "expected": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀…" + }, + { + "input": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "expected": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb…" + }, + { + "input": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀a bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "expected": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀a bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb…" + }, + { + "input": "x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x ", + "expected": "x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x…" + }, + { + "input": "ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab  z", + "expected": "ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab…" + }, + { + "input": "](漢!__\n`#~)>\né||**>  http://x.io`)![[\u0000]]#", + "expected": "](漢! ) é ]]" + }, + { + "input": " ]([https://y.io/p!http://x.io~**", + "expected": "]([" + }, + { + "input": "#](😀 ` éword_[_||[]]…]a||", + "expected": "](😀 éword [" + }, + { + "input": "_a![![]**", + "expected": "a![![]" + }, + { + "input": "http://x.io😀 漢_a😀!http://x.io\né_ é_word…word||", + "expected": "漢 a😀 漢a" + }, + { + "input": "#]( ||#]]a](`#>!é ](a||https://y.io/p|_", + "expected": "](" + }, + { + "input": "http://x.io|\u0000_ é**_<](_)[||]( 😀!`https://y.io/p##](http://x.iohttp://x.io )a漢[", + "expected": "é <]( ) a漢[" + }, + { + "input": " >`", + "expected": null + }, + { + "input": ")**http://x.io\u0000](word)!**||é!||`](#![https://y.io/p`https://y.io/p__![ >a http://x.io<)_", + "expected": ") ](word )! ]( ![ a" + }, + { + "input": "é)<<é](\u0000\u0000漢]![b][…!a][\n![…`!>http://x.io_", + "expected": "é)<<é]( 漢] ![b][ !a][ ![ !" + }, + { + "input": "](>é漢😀word\u0000~a|![#`…https://y.io/p** **\u0000>![]( >_`\n##~** word||)http://x.iohttp://x.iob](漢", + "expected": "]( é漢😀word a" + }, + { + "input": "\nb||>http://x.io|é #<|漢]( é😀>]b!>>||\n_\u0000]http://x.io!_http://x.io_https://y.io/phttps://y.io/p``![||b  ", + "expected": "b ]" + }, + { + "input": "](a !#é!>`#!|_ >)_http://x.io\u0000>[**`ba ] ||| wordéé|word![||word\n ![  http://x.io\u0000b`_]a \n_#|**http://x.io**>a![](", + "expected": "[ ba ] word ![ b ]a" + }, + { + "input": "]]wordbwordhttps://y.io/p…#\nb**\u0000>)…](word", + "expected": "]]wordb word b ) ](word" + }, + { + "input": "|http://x.io#<~éb… a[[ [http://x.ioé>😀a aé", + "expected": "]( wordé <<](>\u0000](漢\n漢http://x.io😀a](]( **\né# 漢  # |> é]漢** ![\u0000", + "expected": "] word <<]( ](漢 漢 é 漢 é] 漢 ![" + }, + { + "input": " ~a!~![ \n))`**![漢é]<]![||…`]( _#http://x.io", + "expected": "a ! ![ ) ) ![漢é]<]![ ](" + }, + { + "input": "a](http://x.io!#\n_http://x.iohttp://x.io!…\u0000a… |>|)![…|``**)`…漢[]", + "expected": "a]( a )![ ) 漢[]" + }, + { + "input": "~https://y.io/p !|~b![é漢]wordhttps://y.io/p<)**https://y.io/p\u0000b_  |…~>]()[)] ", + "expected": "! b![é漢]word b ]()[)]" + }, + { + "input": "\u0000||~😀>", + "expected": "😀" + }, + { + "input": "…[||wordword >![`[|![~é…**|http://x.io**a!`>]", + "expected": "[ wordword ![ [ ![ é" + }, + { + "input": " `https://y.io/p_~aahttp://x.iohttp://x.iob😀** >![  https://y.io/p[ [`a**http://x.io||https://y.io/p_😀\n||ééhttp://x.io](![![é", + "expected": "aa ![ [ a éé" + }, + { + "input": "é…😀😀 https://y.io/pb\u0000", + "expected": "é 😀😀" + }, + { + "input": "![ba", + "expected": "![b a" + }, + { + "input": "_bé>\n_ http://x.ioéaéhttps://y.io/p `", + "expected": "bé" + }, + { + "input": "b) \né`_", + "expected": "b) é" + }, + { + "input": "~https://y.io/pa😀 a http://x.io**`aabwordé]~ http://x.io_||word~\n]( >\u0000![", + "expected": "a ]( ![" + }, + { + "input": "a![http://x.io", + "expected": "a![" + }, + { + "input": "]| ]…[😀![ ", + "expected": "] ] [😀![" + }, + { + "input": "http://x.iob_||😀![![~**é[|\n| **\nb", + "expected": "😀![![ é[ b" + }, + { + "input": "](…>\u0000](\n >… |**|<~_aé#é]", + "expected": "]( ]( < aé é]" + }, + { + "input": "![]( b> ~|\u0000漢[a \n\n https://y.io/p])~![|é\nword![https://y.io/pword** ab|]||!>!…é", + "expected": "![ é word![ ab ] ! ! é" + }, + { + "input": "~>>bhttps://y.io/p![漢wordé![… é漢`…](<[漢é…#]\n[|b~**!…漢word#…word😀|é", + "expected": "b é漢 ](<[漢é ] [ b ! 漢word word😀 é" + }, + { + "input": "😀é](https://y.io/p#漢><é>||![ [", + "expected": ") é [ 😀é]( [" + }, + { + "input": "https://y.io/p**a\u0000](a |#\u0000< 😀\u0000é`]\n <漢_|| word|| [ b\u0000<[ahttps://y.io/pword[word é…", + "expected": "](a < 😀 é ] <漢 [ b <[a é" + }, + { + "input": "![_word😀 ![>_wordhttp://x.io**||**)漢…![!…]![~<😀https://y.io/p>)\n <`", + "expected": "![ word😀 ![ word ![! ]![ <😀 <" + }, + { + "input": "`b漢<#😀word]\n<|…", + "expected": "b漢< 😀word] <" + }, + { + "input": "[](http://x.io[…>]<\n<  \u0000word😀\u0000![\u0000#_](word![…**word", + "expected": "[]( ]< < word😀 ![ ](word![ word" + }, + { + "input": " ](\u0000https://y.io/p#)ahttps://y.io/p||……~😀wordbbword ]> _a漢", + "expected": "]( 😀wordbbword ] a漢" + }, + { + "input": "#漢a||]…_~![](b😀é![\u0000\n||http://x.io<_ éhttps://y.io/p|word\u0000#](漢ahttps://y.io/p||a漢!||]漢b\u0000", + "expected": "漢a é ](漢a ]漢b" + }, + { + "input": "`|#…\n]https://y.io/p`](>漢~", + "expected": "]" + }, + { + "input": "**#[>\nbbb> ", + "expected": "b> a|a|)b…bhttp://x.io\na…_", + "expected": "ab b a a )b b a" + }, + { + "input": "]<]éa\nhttp://x.io) **[http://x.io\nhttp://x.io漢  b !||#![>", + "expected": "]<]éa [ b ! ![" + }, + { + "input": "a~\u0000😀 ]", + "expected": "a 😀 ]" + }, + { + "input": " … `word![![!", + "expected": "word![![!" + }, + { + "input": "~!~`](](… 😀 <_ )http://x.ioa`||>b`| [http://x.ioé#`~[http://x.io", + "expected": "[] ! ) [ [" + }, + { + "input": "a _~漢_|https://y.io/p<)\u0000>>>![>~#<||**\u0000\n!é~a|#[ word", + "expected": "a 漢 ![ < !é a [ word" + }, + { + "input": "…`)~http://x.io||a](word<||||`#", + "expected": ")" + }, + { + "input": "> https://y.io/p!](>`)aa", + "expected": "! 😀]😀<漢 😀 é]![ [ )aa" + }, + { + "input": "![![_]( ~漢http://x.io||", + "expected": "![![ ]( 漢" + }, + { + "input": "\u0000![漢😀](>[!a~)|word漢[||**", + "expected": "word漢[" + }, + { + "input": "~![😀…)…)[", + "expected": "![😀 ) )[" + }, + { + "input": "**#< [ **<]( a\u0000](!](# ~<[](~ ||b ", + "expected": "< [ < ]( a ](! ]( <[]( b" + }, + { + "input": "~]) [ _ ](||~)b~bé||😀[\u0000<éa<…word\n ![!_b", + "expected": "]) [ ]( 😀[ <éa< word ![! b" + }, + { + "input": "_|~a#!]", + "expected": "a !]" + }, + { + "input": "`…| ![![<\n!漢<](<#||__ || #!~…)<](a…>b_\n", + "expected": "<](a b" + }, + { + "input": "…\u0000http://x.io # |b )<…> `😀 ](`b<  漢a`…😀_[b…http://x.io) ||_😀😀 b!~", + "expected": "漢 😀😀 b!" + }, + { + "input": "[b", + "expected": "[b" + }, + { + "input": "`漢 https://y.io/p)word http://x.io|||![` <😀😀](\nwordhttps://y.io/p**", + "expected": "漢 ![ <😀😀]( word" + }, + { + "input": "wordhttp://x.ioaéé]a![http://x.io![](>\u0000…\n](", + "expected": "word ](" + }, + { + "input": "**[…é`!>漢||\u0000 aword😀 é< ![] ![", + "expected": "]( < ![] ![" + }, + { + "input": "\u0000#ahttps://y.io/p](word>)https://y.io/p!~>#https://y.io/pba](http://x.io😀a`[http://x.io**_https://y.io/p[https://y.io/p ~http://x.iohttps://y.io/p]https://y.io/p]( `", + "expected": "a word ) ](" + }, + { + "input": "|]|| ", + "expected": "]" + }, + { + "input": " #!![!**é\n#…a ]`漢|]_😀http://x.io>)`é`", + "expected": "<)漢 a ] 漢 ] 😀" + }, + { + "input": "#\n😀😀|[漢 é", + "expected": "😀😀 [漢 é" + }, + { + "input": "漢", + "expected": "漢" + }, + { + "input": "]~)![word😀\nhttps://y.io/p**||word\u0000 \n ||漢 😀#https://y.io/p ||||word><~b 漢[http://x.io", + "expected": "] )![word😀 漢 😀 word < b 漢[" + }, + { + "input": ">! http://x.io)~word <#word )](…word", + "expected": "! < word )]( word" + }, + { + "input": "é>a!>~  `~ 😀!|`<😀![|| |[>\nword[~bhttps://y.io/p||[https://y.io/p!https://y.io/pab)漢\n", + "expected": "é a! 😀! <😀![ [" + }, + { + "input": "…[_a<]|>\u0000é漢word", + "expected": "[ a<] é漢word" + }, + { + "input": "[  😀~!~~`~](_b😀…**http://x.io||…_>!>ba**😀https://y.io/p", + "expected": "[ 😀 ! ]( b😀 ! ba 😀" + }, + { + "input": "é| ****![~\u0000éaword](<…<[word… ]#\nab`!word_😀**](||#  \u0000) || 漢#  `", + "expected": "é ![ éaword](< <[word ] ab !word 😀 ]( 漢" + }, + { + "input": "_!word**<…\u0000…http://x.io漢](漢 `…#😀", + "expected": "!word < 😀" + }, + { + "input": "word|b~) a`>\u0000a>~", + "expected": "word b ) a a" + }, + { + "input": "<…](**… |**😀\n\u0000😀![漢[]\u0000](**", + "expected": "< ]( 😀 😀 [] ](" + }, + { + "input": "![https://y.io/p)a[b <#wordhttp://x.io|[>>>~#b! >", + "expected": "![ < word" + }, + { + "input": "a` ", + "expected": "a" + }, + { + "input": "word![_>bé …<#word", + "expected": "word![ bé < word" + }, + { + "input": "…_|<[ ) !wordb漢`#~ ])__漢…~[!` https://y.io/phttp://x.io||)漢b\u0000é😀https://y.io/pword>]>![", + "expected": "<[ ) !wordb漢 ]) 漢 [! é😀" + }, + { + "input": "…é)>!", + "expected": "é) !" + }, + { + "input": "](", + "expected": "](" + }, + { + "input": " b… word …!a", + "expected": "b word !a" + }, + { + "input": " ](||#||…`]( ]…https://y.io/p` !https://y.io/p>😀wordword😀http://x.io||#>`>~)😀", + "expected": "]( ]( ] !" + }, + { + "input": "**`[…\n )![ !http://x.io[ [https://y.io/p! https://y.io/p漢_word`😀# … `_!https://y.io/p>](https://y.io/p )漢", + "expected": "word[é ! 漢" + }, + { + "input": "a ]é_…![![# word~~ [漢< \u0000 ", + "expected": "a ]é ![![ word [漢<" + }, + { + "input": "\n😀![!😀[>>é b](]( word|||)**](||word_…\n漢word`漢  #\u0000|<😀😀< ", + "expected": "😀![!😀[ é b](]( word word 漢word 漢 <😀😀<" + }, + { + "input": "|a#http://x.io[https://y.io/p[ <|word# |漢https://y.io/p||\u0000b[)) 漢[ #|!", + "expected": "a < word 漢 ]😀 !" + }, + { + "input": "b\u0000#http://x.io[\n||> \u0000 https://y.io/pword`…!  é http://x.io](é||~…\u0000 漢||😀word ", + "expected": "b 漢 😀word" + }, + { + "input": "_)![`ahttp://x.io\u0000word>!b](…`b_ )~é\n[)]é\n~]([", + "expected": "é < a word é [)]é ]([" + }, + { + "input": "`http://x.iohttp://x.io\néa![![😀\n… \u0000)b~漢![ \u0000 ", + "expected": "éa![![ 😀 )b 漢![" + }, + { + "input": " \u0000\u0000!a**|éa漢…", + "expected": "!a é a漢" + }, + { + "input": "… b||😀 _ ]…>~……|) https://y.io/phttps://y.io/p||a! … 😀||**https://y.io/p", + "expected": "b a < _!😀|**|漢__\u0000éhttp://x.io漢_é|| ~])😀漢漢", + "expected": "b < !😀 漢 é ] )😀漢漢" + }, + { + "input": "http://x.io\n<\né>>é|aword!\n]http://x.io![worda]", + "expected": "< é é aword! ]" + }, + { + "input": "[…aé]…![\u0000a<||<漢", + "expected": "[ aé] ![ a< <漢" + }, + { + "input": "b~a", + "expected": "b a" + }, + { + "input": "`[é https://y.io/p](http://x.ioword~b`||]éahttps://y.io/p😀http://x.io ahttps://y.io/p[ !||é!\n[", + "expected": "[é é! [" + }, + { + "input": "]_#wordé", + "expected": "] wordé" + }, + { + "input": "|http://x.io|!||[…`\u0000)[https://y.io/phttp://x.io![ [http://x.io\n!https://y.io/p_https://y.io/p~word_\n|_https://y.io/p>漢)word]>", + "expected": ")[ [ !" + }, + { + "input": "word  ) ~https://y.io/pb** _b!<\u0000[\n _![[>https://y.io/phttps://y.io/p](\n", + "expected": "word ) b!< [ ![[" + }, + { + "input": "~éa#word![>https://y.io/p\u0000😀…😀ahttp://x.io]|", + "expected": "éa word![ 😀 😀a" + }, + { + "input": "<`é!~]!)#https://y.io/p<~](~…http://x.io>", + "expected": "< é! ]!)" + }, + { + "input": "~\u0000https://y.io/pa<](![****\nhttp://x.io||![_\u0000", + "expected": null + }, + { + "input": "漢éhttp://x.ioword _😀`[|_ a\u0000 >|| … |http://x.io||~\u0000**||~b word…b><…a|a b", + "expected": "漢é 😀 [ a b word b < a a b" + }, + { + "input": "😀[\u0000http://x.io![\u0000", + "expected": "😀[" + }, + { + "input": "![\n", + "expected": "![" + }, + { + "input": " ](<|\nb\u0000b ](ba>  !\n||`![]**||##[ ![!é\n_😀!\nhttps://y.io/p", + "expected": "](< b b ](ba ! [ ![!é 😀!" + }, + { + "input": ">\n…>https://y.io/pword)word é ![](https://y.io/p#\u0000**#|\u0000~||``** ![…", + "expected": "é ![]( ![" + }, + { + "input": "b 😀é !]( \nhttps://y.io/phttps://y.io/p\n# …[]( `)b漢", + "expected": "b 😀é !]( b漢" + }, + { + "input": "# ||![漢 )||> é …http://x.io[😀 http://x.io#\n …😀 ![**[", + "expected": "é 😀 ![ [" + }, + { + "input": "[b…漢a**\n", + "expected": "[b 漢a" + }, + { + "input": "http://x.ioé)http://x.iohttp://x.iob)#[b_…]", + "expected": ") ]" + }, + { + "input": " [https://y.io/p`\n…||[~[", + "expected": "[ [ [" + }, + { + "input": "> |||]<<||b漢](||~#…[word )é漢é漢\n…_**word!\n", + "expected": "b漢]( [word )é漢é漢 word !" + }, + { + "input": ")漢<](\u0000 …https://y.io/p)) ", + "expected": "] word)a <](" + }, + { + "input": "]](b]]wordé\u0000\u0000", + "expected": "]](b]]wordé" + }, + { + "input": "!_http://x.io…#http://x.io![a__ b](https://y.io/p_>http://x.io😀é ![\u0000漢", + "expected": "! b]( ![ 漢" + }, + { + "input": "wordhttps://y.io/p](word#![) ) ~漢…](\u0000 || ~||\n`_bhttp://x.io![~)_![😀", + "expected": "word ![😀" + }, + { + "input": ">_", + "expected": null + }, + { + "input": "\u0000 bwordhttp://x.io\u0000…![#漢`word<", + "expected": "bword ![ 漢 word<" + }, + { + "input": " http://x.io…<é ", + "expected": "<é" + }, + { + "input": "![\n>**![]\né!…|😀|~# ||漢", + "expected": "![ ![] é! 😀 漢" + }, + { + "input": "\n](>http://x.io… <", + "expected": "]( <" + }, + { + "input": "``é\né!…_é ", + "expected": "é é! é" + }, + { + "input": "!word!éé😀b )!#](word#!http://x.io😀![ < `a![ ||**`a![", + "expected": "!word!éé😀b )! ](word ! < a![ a![" + }, + { + "input": "![]!b!é##漢…https://y.io/p`!## é漢漢 ]( # \u0000[**https://y.io/p#漢a\u0000!#`http://x.io_](😀[**_https://y.io/p||…#", + "expected": "b ![]( bbb😀] 漢 ]( [ !" + }, + { + "input": "~\nword](", + "expected": "word](" + }, + { + "input": "#word](\u0000#!**||😀https://y.io/pbhttps://y.io/phttp://x.io!https://y.io/p<\n\u0000>😀a![![!![_[é", + "expected": "word]( ! 😀 😀a![![!![ [é" + }, + { + "input": "![[~éhttp://x.io`_😀`漢|http://x.io` **\n|a](漢]https://y.io/p<**  ]", + "expected": "![[ é a](漢] ]" + }, + { + "input": "]!b|\u0000漢http://x.io||https://y.io/p! …) \u0000  é**|~http://x.io#[😀](~…b", + "expected": "]!b 漢 ) é b" + }, + { + "input": "||漢]<  ~\n…] \u0000bword](#aé\nb]<\u0000bhttps://y.io/p", + "expected": "漢]< ] bword]( aé b]< b" + }, + { + "input": ")\u0000[…**\n**#word", + "expected": ") [ word" + }, + { + "input": "]()😀漢_||](_b]https://y.io/p< …\u0000bword \nhttps://y.io/p\u0000~]\né !…![__**😀b )[ [](||[漢", + "expected": "]()😀漢 [漢" + }, + { + "input": "|[~_ ![ 漢a…[word漢 https://y.io/p![ ]()|b ~é|\n~_#éé #😀 https://y.io/p[#>_[", + "expected": "[ b é éé 😀" + }, + { + "input": "**]**]a)|| bhttp://x.io__**~****… #[|| \n", + "expected": "] ]a)" + }, + { + "input": "]word]b><]", + "expected": "]word]b <]" + }, + { + "input": "word)\né😀http://x.io https://y.io/p ! [#é|é\u0000 ****http://x.io\u0000`https://y.io/p|| **b é**word`|![\n||\nhttp://x.io", + "expected": "word) é😀 ! [ é é" + }, + { + "input": "a||! a\n)_[a**#<", + "expected": "a ! a ) [a <" + }, + { + "input": "\u0000![漢!\u0000#!\n_** ](漢)!https://y.io/p~>](~ab]b…|| a漢…##", + "expected": "! a漢" + }, + { + "input": "http://x.io", + "expected": null + }, + { + "input": "ba !](_>]**a]_>漢漢éword|😀é", + "expected": "ba !]( ] ![ b é ) é a] 漢漢éword 😀é" + }, + { + "input": "](#漢|>![漢**)| **https://y.io/p_>|\u0000>\u0000ba|||![](`)`>…漢\u0000…", + "expected": "]( 漢 漢" + }, + { + "input": " ]é😀漢](a😀\n_||)a|word~]]!](\n_", + "expected": "]é😀漢](a😀 )a word ]]!](" + }, + { + "input": "word\n>![b`", + "expected": "word ![b" + }, + { + "input": "…](****<a![](**…https://y.io/phttp://x.io ](**b||a||] \u0000b](_", + "expected": "a ] b](" + }, + { + "input": "<_", + "expected": "<" + }, + { + "input": "…)`!word|| ![😀](|_)||] _**http://x.ioword **\n… ", + "expected": ") !word ]" + }, + { + "input": "\n>http://x.iohttps://y.io/p!)… ~ 😀`😀\n_)`a**", + "expected": "😀 😀 ) a" + }, + { + "input": "`b< ]…http://x.io~ ~😀", + "expected": "b< ] 😀" + }, + { + "input": "||é)** 😀`!…# 漢<\n[||漢\na|)~#`> #\n", + "expected": "漢 a )" + }, + { + "input": "漢\n https://y.io/p||<…b word\u0000`a_[b", + "expected": "b[[ a ! word漢 b" + }, + { + "input": "https://y.io/p", + "expected": null + }, + { + "input": "https://y.io/phttps://y.io/p![https://y.io/p)) ]", + "expected": "a![)a ]" + }, + { + "input": "a `http://x.iob<# https://y.io/pwordé]…~||", + "expected": "a" + }, + { + "input": "#é](![``http://x.io`]` #**aahttp://x.io] ", + "expected": "é](![ aa" + }, + { + "input": "![](…\n\u0000![ >漢b~>~**||\n\n<漢a word漢_)word#漢!😀wordhttp://x.iobé**http://x.io<word~\n)[<😀|![~", + "expected": "word[ << [<😀 ![" + }, + { + "input": "#|…||https://y.io/p_https://y.io/p!éb[)\u0000](é>#]~…", + "expected": "](é ]" + }, + { + "input": "](aword |a] 漢_]  #_|](漢 **漢éword", + "expected": "](aword a ] 漢 ] ](漢 漢éword" + }, + { + "input": "!http://x.io]漢", + "expected": "!" + }, + { + "input": "a b![é\u0000https://y.io/p|| b<_…**", + "expected": "a é b![é b<" + }, + { + "input": "\u0000 `> >é\u0000[_]<>||_)b``_](#_ https://y.io/pb![`\nb", + "expected": "é [ ]< )b ]( b" + }, + { + "input": "](word~word\u0000word](]漢\u0000\na \n\u0000漢 ]||` http://x.ioaa…https://y.io/p b>](![>b]…\u0000b#[word", + "expected": "](word word word](]漢 a 漢 ] b ](![ b] b [word" + }, + { + "input": "…``ba<\u0000<é)[_` `<\u0000]( 漢>😀! !b) <", + "expected": "a< <é) < <" + }, + { + "input": "漢|a||word… > )", + "expected": "漢 a word )" + }, + { + "input": "|| ``é_ #](a", + "expected": "é ](a" + }, + { + "input": " <…#_漢http://x.io!![#…a![ \n**)", + "expected": "< 漢 ![ a![ )" + }, + { + "input": "!é>![", + "expected": "!é ![" + }, + { + "input": "\nhttps://y.io/phttps://y.io/p](", + "expected": null + }, + { + "input": "a!…`[https://y.io/p ](https://y.io/p`…![||)…  [** \n\n#http://x.io]😀\u0000`bhttps://y.io/p[~😀", + "expected": ")< !< word ![ ) [ b" + }, + { + "input": "…\u0000é😀<漢…b~😀~#](漢]…[word\nhttps://y.io/p\u0000\u0000>", + "expected": "é😀<漢 b 😀 ](漢] [word" + }, + { + "input": "a`]漢[\u0000ba…😀](b!||漢[ …~b>…#|https://y.io/p ~bhttps://y.io/phttps://y.io/p 😀b😀_](~", + "expected": "a ]漢[ ba 😀]( b! 漢[ b b 😀b😀 ](" + }, + { + "input": "**😀 )]![é…", + "expected": "😀 )]![é" + }, + { + "input": ">漢[wordé…[`| ~http://x.io a[](] ~` ||a![[**b)_<…a**", + "expected": "漢wordé [ a[ < a" + }, + { + "input": "ééé#||!![ ` ](é`]**_https://y.io/p]||[!漢\né http://x.io", + "expected": "ééé [!漢 é" + }, + { + "input": "漢https://y.io/p**~> b😀…**… https://y.io/p`)https://y.io/p|", + "expected": "漢 b😀" + }, + { + "input": "https://y.io/p`<>漢 )|>**bhttp://x.io", + "expected": "< 漢 ) b" + }, + { + "input": "](bhttps://y.io/p!https://y.io/p b_\u0000](漢a!word|b", + "expected": "](b b ]( 漢a!word b" + }, + { + "input": "`[\u0000  ||a word ||~~](>\n<~`>]![http://x.io\u0000aé![é![]( http://x.io\u0000||~|]([>word]bhttps://y.io/p", + "expected": "[ ]( < ]![ aé![é![]( ]([ word]b" + }, + { + "input": "![||bhttp://x.io!_||_]||", + "expected": "![ ]" + }, + { + "input": "https://y.io/p](", + "expected": null + }, + { + "input": ">😀` é>漢` <_ http://x.io_", + "expected": "😀 é 漢 <" + }, + { + "input": "~)~#_\n\n漢!a) \u0000[|_~…]( https://y.io/p", + "expected": "b) [https://y.io/p**||>ahttps://y.io/pa…😀word |**||<", + "expected": "b) [ <" + }, + { + "input": "#a\u0000b\n|[<<__\u0000漢\u0000](||é`漢word~ \n 漢**é[|wordé 漢**https://y.io/p**", + "expected": "a b [<< 漢 ]( é 漢word 漢 é[ wordé 漢" + }, + { + "input": "~|…漢b \u0000", + "expected": "漢b" + }, + { + "input": "\u0000漢bé||…<~", + "expected": "漢bé <" + }, + { + "input": "[_é|](wordhttp://x.io\u0000 …|", + "expected": "[ é ](word" + }, + { + "input": ">_ https://y.io/p])#>|** 漢 >)`", + "expected": "漢 )" + }, + { + "input": "漢", + "expected": "漢" + }, + { + "input": "#!#b||_\u0000…漢**|\n\u0000b`awordword`>)||wordhttps://y.io/p< \n\na[)](漢\u0000![😀!b![😀", + "expected": "! b word a[)](漢 ![😀!b![😀" + }, + { + "input": "|| ! …[😀  _![ <", + "expected": "! [😀 ![ <" + }, + { + "input": "_]|  ba漢\n[ b||`", + "expected": "] ba漢 [ b" + }, + { + "input": " ![_!)!", + "expected": "![ !)!" + }, + { + "input": " \u0000|| |!b_|![![😀<…b<||", + "expected": " ](worda\u0000| ||| )~…](~~)]", + "expected": "!é ]" + }, + { + "input": "漢||)\n)_", + "expected": "漢 ) )" + }, + { + "input": "#**>漢b<\u0000)![`[http://x.iohttp://x.iohttps://y.io/p]()!", + "expected": "](" + }, + { + "input": "b _ \n||wordhttps://y.io/p)]~…é…||\n[_a…https://y.io/p||![b😀http://x.ioa](! !`|https://y.io/pword😀\n> ", + "expected": "b [ a !" + }, + { + "input": " \nb漢a |b) \nword…]>😀http://x.iob) )…b>)[", + "expected": "b漢a b) word ] 😀 ) b )[" + }, + { + "input": "` _<\u0000http://x.io\u0000||)éword #!漢`😀||~>#||](](!http://x.io\n)", + "expected": "< ](](! )" + }, + { + "input": "http://x.iohttp://x.iohttp://x.ioé<é~](é…[\u0000**b_漢`é`a||\n漢é`é! **[a](**https://y.io/p😀~http://x.io😀…", + "expected": "[ b 漢 é a 漢é é! [a ](" + }, + { + "input": "a](](😀`word![😀|](\na]\n)a<)word", + "expected": "a](](😀 word a<)word" + }, + { + "input": "~])#b漢 #b", + "expected": "]) b漢 b" + }, + { + "input": "é**\u0000_](#](<|||< ", + "expected": "é ]( ](< <" + }, + { + "input": "#é~https://y.io/phttps://y.io/p)http://x.io !\u0000word[  …\u0000 [é>http://x.io", + "expected": "😀] 😀! ] [ a <" + }, + { + "input": "\u0000|**>😀https://y.io/p |…**![\n\u0000_\u0000[漢😀`**]~`http://x.io)", + "expected": "😀 ![ [漢😀 ]" + }, + { + "input": "…**[](a~|~<é![!!\u0000#…#](http://x.io [**word~[**http://x.io\n", + "expected": "[](a <é![!! ]( [ word [" + }, + { + "input": ">http://x.io~https://y.io/p\u0000|]word**![)>**https://y.io/p))!", + "expected": "]word ![)" + }, + { + "input": " \u0000)||``![", + "expected": ") ![" + }, + { + "input": " word…![ _…漢b|~", + "expected": "word ![ 漢b" + }, + { + "input": ">!bbhttps://y.io/p> …wordhttp://x.ioé~😀a\u0000#<\u0000\u0000b… !https://y.io/p…||)[http://x.io||> 漢**)", + "expected": "漢 b ! 漢 )" + }, + { + "input": "\n![ …<] #`https://y.io/pword#]ahttps://y.io/p](", + "expected": "![ <]" + }, + { + "input": ">~ **||éb\u0000https://y.io/pé a~_ **https://y.io/p~_)a\u0000||)**https://y.io/p~", + "expected": ")" + }, + { + "input": "|\n…a||é 😀`[ _", + "expected": "a é 😀 [" + }, + { + "input": "…<**a ](漢   ]…", + "expected": "< a ](漢 ]" + }, + { + "input": "](||)~||_[\u0000", + "expected": "]( [" + }, + { + "input": "<||!é\u0000 … word😀](<> !#  ** \u0000_[b]` !", + "expected": "< !é word😀](< ! [b] !" + }, + { + "input": "http://x.io||\u0000_\u0000_>a\u0000a#`< 😀…_", + "expected": "a a < 😀" + }, + { + "input": "é](`", + "expected": "é](" + }, + { + "input": " <é![](b] >_)!漢 word`http://x.io|)#é< …)#é\u0000http://x.io!>https://y.io/p", + "expected": "<é https://y.io/p](https://y.io/p>](", + "expected": "< b ! 😀b ! 😀" + }, + { + "input": "#)… **[||`word![\n word ](]||_)# ](éaaé||…[word<)||||`😀", + "expected": ") [ ) ](éaaé 😀" + }, + { + "input": "\u0000[>#word\n|https://y.io/p<)|😀https://y.io/p[||<]漢#!>", + "expected": "[ word <]漢 !" + }, + { + "input": "<**)…||\u0000![]<~![b\u0000**ahttps://y.io/p||", + "expected": "< )" + }, + { + "input": "ba`word~~](_漢#|)||…![** http://x.io#\nbword#word |…_|\u0000!https://y.io/p~é", + "expected": "b a word ]( 漢 ) ![ bword word !" + }, + { + "input": "[](😀`<[||https://y.io/p)http://x.io😀#![| ] <\n**é>word漢 ", + "expected": "[](😀 <[ ] < é word漢" + }, + { + "input": ")||#😀`#word#||é", + "expected": ") é" + }, + { + "input": "~http://x.io|| !~[~ ` _漢|b", + "expected": "! [ 漢 b" + }, + { + "input": " \n!`![` _a |||| éa]😀`)<\u0000![` ", + "expected": "! ![ a éa]😀 )< ![" + }, + { + "input": " _😀http://x.io~é\nhttp://x.io[)!!漢b😀a ~word>](](  `word!😀](~😀\u0000** ](||word](", + "expected": "😀 word ](]( word!😀]( 😀 ]( word](" + }, + { + "input": " 😀](<", + "expected": "😀](<" + }, + { + "input": "😀\n |_)\u0000|~)\n\u0000_https://y.io/péa \u0000é… |漢 <||<", + "expected": ") [] é 漢 < <" + }, + { + "input": "b\u0000…! word#a ", + "expected": "b ! word a" + }, + { + "input": "é)]`| #\u0000 … |`\n", + "expected": "é)]" + }, + { + "input": "_word_", + "expected": "word" + }, + { + "input": "ahttp://x.io~)\u0000\u0000word漢https://y.io/pa#)**https://y.io/p>_!_http://x.io>~||](_\nahttp://x.io word![", + "expected": "a word漢 a word![" + }, + { + "input": "bhttps://y.io/p**https://y.io/p||\nb[_` ![https://y.io/pb", + "expected": "b b[ ![" + }, + { + "input": "😀\u0000[]https://y.io/p>\u0000<é…b", + "expected": "😀 [] <é b" + }, + { + "input": "word **`[\n)éhttp://x.io **](http://x.io| **`http://x.io\n [||]!b_ ]()![éword](#é`**||||https://y.io/p#)", + "expected": "word )é" + }, + { + "input": "](worda\n😀]> [) aa>|> 😀word\n!http://x.io![| ![| 漢[#…[||https://y.io/p", + "expected": "](worda 😀] [) aa 😀word ! ![ 漢[ [" + }, + { + "input": "word", + "expected": "word" + }, + { + "input": "http://x.io ])`_ _![](`||![~  ||", + "expected": "]) ![](" + }, + { + "input": "😀[** [ ****é|>>)bb漢[](**)![![`[\u0000word](![]é||http://x.io[ ", + "expected": "😀 [ é )bb漢[![![ [ word](![]é" + }, + { + "input": "](#…]|漢`](#é~漢 ~https://y.io/pb😀|]", + "expected": "]( ] 漢 ]( é 漢 ]" + }, + { + "input": "b#!! |>< https://y.io/phttps://y.io/p<](http://x.io  \u0000… 漢b…漢 ]éword 😀**……**>~", + "expected": "b !! < 漢b 漢 ]éword 😀" + }, + { + "input": "_]<]![[]**<||](漢b|]b<> 😀\n**_||a#)![\u0000![b漢", + "expected": "]<]![[] < a )![ ![b漢" + }, + { + "input": "b~…a<<_http://x.ioa<😀>#a😀https://y.io/p<>![", + "expected": "b a<<" + }, + { + "input": ") **https://y.io/pword<😀_\u0000` 😀 b 😀#)](……a!<) \u0000漢![word", + "expected": ") 😀 b 😀 )]( a!<) 漢![word" + }, + { + "input": "!>]||](>![ <#é ||😀a** 漢>< )http://x.io!", + "expected": "! ] ]![b!< 😀a 漢 < )" + }, + { + "input": " ||[**||https://y.io/pword]word…", + "expected": null + }, + { + "input": ")http://x.iohttps://y.io/p  …😀 https://y.io/p](https://y.io/p😀]](http://x.io>https://y.io/p\n](**😀**\u0000]( ||a_aawordhttp://x.io![😀)][\n>漢", + "expected": ") 😀 ]( 😀 ]( a aaword ][ 漢" + }, + { + "input": "]![`https://y.io/pword`)\n!\né||~éword| )http://x.io漢 #~…é\n_\n <**]\u0000\néé||<)b wordb", + "expected": "]![ ! é <)b wordb" + }, + { + "input": "_#éé)_…_ |||ahttp://x.io", + "expected": "éé) a" + }, + { + "input": " ", + "expected": null + }, + { + "input": "https://y.io/phttps://y.io/p**_\u0000#](é\n<>![ ]漢]https://y.io/pb#b[![", + "expected": "](é < ![ ]漢]" + }, + { + "input": "b>#**~_é![!é😀>\n<éa)~…![![![ é !||](#|>word", + "expected": "b é![! é😀 <éa) ![![![ é ! ]( word" + }, + { + "input": "]](] é**é\u0000_******#!**#]#**]](\n", + "expected": "]](] é é ! ] ]](" + }, + { + "input": "\u0000\n  _|http://x.io>_)]…))>])a~_é|**< 漢]#< >b~ <`\u0000#b**<", + "expected": ")) ])a é < 漢] < b < b <" + }, + { + "input": "||`))…**a…|https://y.io/p>[>[_", + "expected": ")) a" + }, + { + "input": " é [ ](https://y.io/pé|`<漢<…]https://y.io/p", + "expected": "é [ ]( <漢< ]" + }, + { + "input": "a漢]|<>😀 \nhttp://x.io~**||~|http://x.io**#<<", + "expected": "a漢] < 😀" + }, + { + "input": "![https://y.io/p[#|>b||>_|😀漢||…é\na…`https://y.io/p😀!😀 ](", + "expected": "word](é 漢 ]( []é 漢 😀!😀 ](" + }, + { + "input": "`! 漢<** ` !https://y.io/pa\u0000… `)bab é<~…**https://y.io/p](!<\n ", + "expected": "! 漢< ! )bab é<" + }, + { + "input": "#…😀é![]>漢_b]é\n\u0000**![é ~**https://y.io/p\u0000é||word__||||![_", + "expected": "😀é![] 漢 b]é ![é é ![" + }, + { + "input": "<", + "expected": "<" + }, + { + "input": ">**~|](![] http://x.io~|😀!a#~https://y.io/pa[b >](word~[😀>>|`]a~ \n_http://x.io>éhttp://x.io\n!", + "expected": "](![] ](word [😀 ]a !" + }, + { + "input": "!](!http://x.io<\n_~]#…||…`https://y.io/p`](<](**https://y.io/p|é\n~ #>![<)~![#![#!", + "expected": "!](! ] ![<) ![ ![ !" + }, + { + "input": "\n\n#>||||b)http://x.iobwordé<", + "expected": "b)" + }, + { + "input": ">||…a![|!http://x.io word) )……)https://y.io/p_ http://x.io****` \u0000![]|a|](漢#**#** [](**](", + "expected": "a![ ! word) ) ) ![] a ](漢 []( ](" + }, + { + "input": ">😀`!![>é漢b漢](http://x.io~||😀\u0000https://y.io/p\u0000b é[![\n #![!>[b](\n😀[b_\u0000_>`http://x.io\u0000**](😀\n`](|", + "expected": "漢 b é[![ ![! [b]( 😀[b ](😀 ](" + }, + { + "input": ">`**||****😀\u0000é](漢http://x.iob", + "expected": "! b![ a[😀 word 漢" + }, + { + "input": "~word![é\nhttp://x.io**https://y.io/p!http://x.io[](\n](!éaa!`é][**http://x.io](word~bé) ", + "expected": "word" + }, + { + "input": "b~a![~😀é😀||", + "expected": "![] ![a漢 b é b[ !word![ é😀" + }, + { + "input": "|a😀]http://x.io\u0000 !!漢 漢https://y.io/p\nhttp://x.io \n 漢", + "expected": "a😀] ! !漢 漢 漢" + }, + { + "input": "!é", + "expected": "!é" + }, + { + "input": "|a]|bhttps://y.io/p", + "expected": "a] b" + }, + { + "input": "~~|…é# é`#<[\n<#]b]https://y.io/p \n!ahttps://y.io/p😀~) bb", + "expected": "é é < [ < ] b] !a bb" + }, + { + "input": "![!**_<|]) ||[>)[ 漢#_b ", + "expected": "![! < ]) [ )[ 漢 b" + }, + { + "input": "https://y.io/p](éhttp://x.io\u0000||\u0000http://x.io~**😀", + "expected": "](" + }, + { + "input": "`b é |\na é!", + "expected": "b é a é!" + }, + { + "input": "漢`](https://y.io/p>]()\n! ||~ ", + "expected": "漢 ]( !" + }, + { + "input": "](~ >~#\nword", + "expected": "]( b ~a<||>a~|[~😀é||…aa` …", + "expected": ")漢 [ b a< aa" + }, + { + "input": "a)[", + "expected": "a )[" + }, + { + "input": "#https://y.io/p 😀 word#||!#<[[ _…`!!é`** http://x.iohttp://x.io<\u0000!_[漢a )`**`!", + "expected": "😀 word ! <[[ !!é ! [漢a ) !" + }, + { + "input": "!!http://x.io", + "expected": "!!" + }, + { + "input": "\u0000|||😀a\nhttp://x.io`… 😀_b!** é漢aé\u0000]a aa![", + "expected": "😀 a 😀 b! é漢aé ]a aa![" + }, + { + "input": "|| aword`! ](~#`b…||](****  #http://x.io](a`_]([ 😀word \u0000", + "expected": "]( 😀word" + }, + { + "input": "\nword  [http://x.io)])][http://x.iohttp://x.io](#]( <||aé\u0000||!>]||b \u0000\u0000😀é]", + "expected": "word [ < ! ] b 😀é]" + }, + { + "input": "http://x.io]([_]#漢https://y.io/p😀|\u0000#!b] ||||>)  !", + "expected": "漢 !b] ) !" + }, + { + "input": "http://x.io**wordhttps://y.io/pa![ ] a!**[** …](<_\néa😀b](>https://y.io/p…>…", + "expected": "] a! [ ](< éa😀b](" + }, + { + "input": " a _))**word||\n>\nbb`\n|| \u0000|| \u0000>]", + "expected": "b b ]" + }, + { + "input": " ![", + "expected": "![" + }, + { + "input": "`\n](|| 漢![#]](](~_漢![ 😀** [b\u0000b", + "expected": "]( 漢![ ]](]( 漢![ 😀 [b b" + }, + { + "input": "a<#a#http://x.io|| 😀a![]", + "expected": "a< a a漢! ! ] ] 😀a![]" + }, + { + "input": " 漢a`[|é ` ||http://x.io😀", + "expected": "漢a [ é" + }, + { + "input": "||)\n", + "expected": ")" + }, + { + "input": "<|", + "expected": "<" + }, + { + "input": "<[!_\u0000漢|]\u0000 \u0000!]() **漢[https://y.io/p)…#>)||", + "expected": "](![)!é!" + }, + { + "input": "![`…https://y.io/p`]( \n😀~||)!**`]**>~**wordhttp://x.io` [\nword)]!](|< **\n<\n<漢", + "expected": "! ] word [ word)]!]( < < < 漢" + }, + { + "input": "![https://y.io/p|||>](|\n\u0000", + "expected": "![" + }, + { + "input": " <~**https://y.io/p )|**`😀|**a😀# ![)~# ]([) <]]](http://x.io _\u0000#<\u0000**b", + "expected": "<] < b" + }, + { + "input": "aéhttps://y.io/p#~[||é#[`ahttps://y.io/p \n<>aword😀<>https://y.io/p](|a~!漢](…!b]\n…|| !", + "expected": "aé !" + }, + { + "input": "||", + "expected": null + }, + { + "input": "! \u0000||http://x.io<`b_ \u0000\n 漢http://x.io|| a!😀]https://y.io/p![||~>>aé||a~>", + "expected": "! a!😀] a" + }, + { + "input": "a||`http://x.io`\u0000", + "expected": "a" + }, + { + "input": "\u0000\n漢![![😀]\n😀\u0000](wordéé~||http://x.io!![", + "expected": "漢![![😀 ] 😀 ](wordéé" + }, + { + "input": "b**😀![!#]![)>! [\n\n>\n|a#… ](wordéhttp://x.io[**b](_`é\n\n", + "expected": "b 😀![! ] ![) ! [ a ](wordé" + }, + { + "input": " ||_][\n](漢_||…!>…é )é >word~https://y.io/p!a![ é漢\u0000b |#é!](", + "expected": "! é )é word é漢 b é!](" + }, + { + "input": "](~a_\u0000word`)\n😀<)😀`#", + "expected": "]( a word ) 😀<)😀" + }, + { + "input": "…**>![`https://y.io/p #!http://x.iohttps://y.io/p_ word`>_wordhttp://x.io😀**_#éa#a_", + "expected": "![ ! word word" + }, + { + "input": "b|[word_ […a](**é#)http://x.io#😀!|word![…\u0000b\n>https://y.io/pa漢**]( ![](]||wordhttps://y.io/p!|é", + "expected": "b word [ a b 漢 ]( ![](] word" + }, + { + "input": "![漢\nb\u0000|_]a[**http://x.io)b]||>~漢)漢😀 **…|\u0000`|| http://x.io](é\u0000http://x.io ~é", + "expected": "![漢 b ]a[ é" + }, + { + "input": "😀~https://y.io/p[ http://x.io!_><漢>a`[ #wordword  a\nword\n<a)", + "expected": "![![漢 wordword a word < a)" + }, + { + "input": "![) >)_ http://x.io😀|\naa`>##a", + "expected": "![) ) aa a" + }, + { + "input": "`~_]http://x.io]([||)漢wordééa _a http://x.io#!#|word**…a>#~", + "expected": "] wordééa a a" + }, + { + "input": "**\nhttps://y.io/p", + "expected": null + }, + { + "input": "…a`…b**", + "expected": "! ! )![😀!a b" + }, + { + "input": "_https://y.io/pa\u0000😀 …~  >[\u0000|| |>漢|é~é<#`漢 ||http://x.io >漢![漢\n)", + "expected": "😀 [ 漢![漢 )" + }, + { + "input": " >_]<**\u0000\u0000`😀b![)**https://y.io/p**漢b", + "expected": "]< 😀b![) 漢b" + }, + { + "input": "[é~](_)]([[\nhttps://y.io/p\u0000 `\n|!_\u0000|https://y.io/p #", + "expected": "é ]([[ !" + }, + { + "input": "word😀 a\u0000 https://y.io/pb\u0000漢漢#`](é [http://x.io\u0000_~!漢~]]word\u0000", + "expected": "word😀 a 漢漢 ](é [ !漢 ]]word" + }, + { + "input": "\u0000[http://x.io~**😀>![\n||)](https://y.io/p**https://y.io/p漢)word", + "expected": "[ word" + }, + { + "input": "漢 wordé||](] |😀 http://x.ioa[\nword[b]!😀||\u0000\u0000 a\u0000!\na\u0000\n]", + "expected": "漢 wordé a ! a ]" + }, + { + "input": "…😀#漢|é)a) word)https://y.io/p# < a_ \u0000|漢éhttps://y.io/phttps://y.io/pé_a|||]|| a](]word", + "expected": "😀 漢 é)a) word) < a 漢é a](]word" + }, + { + "input": "\u0000😀http://x.io!![a**~http://x.io|<**[bb)…a…#😀[](~\n![![<éa\u0000<[ [**# <>…é", + "expected": "😀 ![a a 😀[]( ![![< éa <[ [ < é" + }, + { + "input": "~漢\u0000![![||éhttp://x.io<](http://x.io!é ! **)é![", + "expected": "漢 é![" + }, + { + "input": "\n_>", + "expected": null + }, + { + "input": "_>]b…[\u0000<**]([<`", + "expected": "]b [ < ]([<" + }, + { + "input": " |\u0000", + "expected": "😀![ )b 漢漢 漢 ]( ![  http://x.io😀#漢\n_> 漢\u0000 **> )](a|**#><)|**<", + "expected": "é](wordé 漢 )](a <) <" + }, + { + "input": "`http://x.io😀   #b…**", + "expected": "b" + }, + { + "input": " https://y.io/phttp://x.io>]é😀\u0000<ééé~#~_#[] ! ||é\nwordword`#", + "expected": "![  ]", + "expected": "😀 漢]( !] é ![ ]" + }, + { + "input": "**||\u0000#>é[!](_漢\n)[**\n_#>\n…]||é)…  ![]a>http://x.iohttp://x.io|", + "expected": "é) ![]a" + }, + { + "input": "_b](… <<\n…😀`]😀~éhttp://x.iob漢#[\néword[**a…**![\u0000~", + "expected": "b]( << 😀 ]😀 é éword[ a ![" + }, + { + "input": "![ b|[_", + "expected": "![ b [" + }, + { + "input": "\n https://y.io/p ~**_||])[word", + "expected": "])[word" + }, + { + "input": "[http://x.io )<`](😀 ~](`漢)!", + "expected": ")< !" + }, + { + "input": " http://x.ioword! `~](\u0000 ]( http://x.io]", + "expected": "]( ](" + }, + { + "input": "#]", + "expected": "]" + }, + { + "input": "[http://x.iohttps://y.io/p \u0000|~[|http://x.io![http://x.io é\n![ < )\néb\n![| <**\u0000😀http://x.io ~_😀||![>", + "expected": "b ) 😀 ![" + }, + { + "input": "**!漢)漢 ~…https://y.io/p|# ||!_éhttp://x.iohttp://x.io", + "expected": "!漢)漢 ! é" + }, + { + "input": "|||])", + "expected": "])" + }, + { + "input": "bword!![漢[ <|||", + "expected": "bword!![漢[ <" + }, + { + "input": "https://y.io/p>_漢\n~~…[|_![https://y.io/p<…![漢)a**b**)…[[wordb\u0000b](#a😀", + "expected": "[ ![ ![漢 )a b ) [[wordb b]( a😀" + }, + { + "input": " ||<#\u0000b>#\n", + "expected": "< b" + }, + { + "input": "aawordhttp://x.ioé\u0000|", + "expected": "aaword é" + }, + { + "input": "<_![|**\u0000漢😀 [ba😀`https://y.io/p<](wordhttp://x.io![ ](] #é_", + "expected": "< ![ 漢😀 [ba😀 ](] é" + }, + { + "input": "😀]()\u0000\u0000word#~http://x.io ~#😀**[a)a>é )http://x.io)…)]https://y.io/p!~)**` #||……](]", + "expected": "é< ] ](]" + }, + { + "input": "http://x.ioaword😀](\u0000…\n)\n]\u0000\u0000[\n漢é…é)\n\u0000a)", + "expected": "]( ) ] [ 漢é é) a)" + }, + { + "input": "**![!\nhttps://y.io/pb…![~a\n漢…` ](漢", + "expected": "![! b ![ a 漢 ](漢" + }, + { + "input": "_|a!a# ](|< é! [ |…>\u0000…](a~_https://y.io/p", + "expected": "a!a ]( < é! [ ](a" + }, + { + "input": "http://x.io…#!漢http://x.io", + "expected": "!漢" + }, + { + "input": "_😀éhttp://x.io ![[!_ http://x.iob_!😀](b漢a)a# ~b", + "expected": "😀é a b" + }, + { + "input": "||b]**!https://y.io/p#>", + "expected": "😀 ]) ]( ]( a !" + }, + { + "input": "#||~**é!\n~![](]( éhttp://x.io ](http://x.io<é![|bhttps://y.io/p~\u0000 )|![**", + "expected": "é! ![" + }, + { + "input": " [😀 ||é#漢ééb\n[漢~>~](b… #||`", + "expected": "[😀" + }, + { + "input": "!>漢<…word|||~#\n<](\n…漢> `a…>é||]![\u0000 😀漢\u0000_<_ https://y.io/p", + "expected": "! 漢< word ]![ 😀漢 <" + }, + { + "input": "<||a_b😀[**_**b|![ a", + "expected": "<) ]( ![![]( a" + }, + { + "input": "~![# >_ [é_<)😀b)![]|#**||…](…http://x.io**\nword", + "expected": "![ [é <)😀b)![] ]( word" + }, + { + "input": "漢)\n![http://x.io \n", + "expected": "漢) ![" + }, + { + "input": "…a漢…!a)**word![ #  ", + "expected": "a漢 !a) word![" + }, + { + "input": "**", + "expected": null + }, + { + "input": "漢|_éhttps://y.io/phttps://y.io/phttps://y.io/paé)\u0000https://y.io/p# >http://x.ioé##![漢)](…>||…>](]\u0000]é😀 ahttps://y.io/p||) ", + "expected": "漢 é" + }, + { + "input": "!b| ||éword…](~`** ](\u0000~**[a)éword", + "expected": "!b éword ]( ]( [a)éword" + }, + { + "input": "https://y.io/pa**…[`!…  >||[` ] <]#!!#a](![😀a)_>", + "expected": "[ ! [ ] <] !! a](![😀a)" + }, + { + "input": "[~||~ ", + "expected": "[" + }, + { + "input": "é||\n 😀http://x.io#漢 **||!<", + "expected": "é !<" + }, + { + "input": "\u0000 !)])\u0000\n…é~http://x.iowordhttp://x.io ~word !||a_  |é![", + "expected": "!)]) é word ! a é![" + }, + { + "input": "http://x.io[! ||)😀………http://x.iohttp://x.io😀\n\n**)](a) ]…b…**漢wordéword!][ wordhttp://x.iob ", + "expected": ")😀 ) ] b 漢wordéword!][ word" + }, + { + "input": "||! ![http://x.io)! ||éhttps://y.io/p", + "expected": "é" + }, + { + "input": "_~https://y.io/p <漢\n>http://x.io_b漢**![|https://y.io/p**é`http://x.iohttp://x.io!< `…![https://y.io/p~ **||\u0000\n ]!![\u0000a", + "expected": "<漢 ![ ]!![ a" + }, + { + "input": "#😀#|****é`**\u0000!**a]http://x.io[](\u0000_<)]https://y.io/p… ![https://y.io/pb http://x.io", + "expected": "😀 é ! a] <)] ![" + }, + { + "input": "https://y.io/pbb`> a#ab__#|\n#😀](~] [漢\n", + "expected": "a ab 😀]( ] [漢" + }, + { + "input": " é)_**word[>😀!>#a", + "expected": "é) word[ 😀! a" + }, + { + "input": " \nhttp://x.io😀http://x.io", + "expected": null + }, + { + "input": "…#]漢", + "expected": "]漢" + }, + { + "input": "b!…![~…", + "expected": "b! ![" + }, + { + "input": "# _ ", + "expected": null + }, + { + "input": "](😀https://y.io/p é![>#é**< 漢 ", + "expected": "](😀 é![ é < 漢" + }, + { + "input": "word😀\n\u0000word[||~[ \u0000![ ||<", + "expected": "word😀 word[ <" + }, + { + "input": "[](!)<|漢😀 _]é[… |**]http://x.iob😀", + "expected": "[](!)< 漢😀 ]é[ ]" + }, + { + "input": "**…`_b\nhttp://x.io !é\n[<_ |#<_ |]b", + "expected": "b !é [< < ]b" + }, + { + "input": "**<))||http://x.io#**`|\n )…~>** >a😀 \u0000_))漢", + "expected": "<)) ) >>>b~é ![`# http://x.iohttp://x.io漢http://x.io ~**…> a漢![||https://y.io/p_a", + "expected": "a]()漢" + }, + { + "input": " a`ab>`https://y.io/p>é", + "expected": "a ab" + }, + { + "input": "**…http://x.io\n\n…\n< \u0000)é]**a](! )](é)[é](**!wordword", + "expected": "< )é] a](! )]( é)[é]( ! wordword" + }, + { + "input": "~]ba…[]#\n![`\n", + "expected": "]ba [] ![" + }, + { + "input": "😀`||…é_a~[\n**>", + "expected": "😀 é a [" + }, + { + "input": "a||[漢\n\n漢 ~ éword]![]![a\u0000", + "expected": "a [漢 漢 éword]![]![a" + }, + { + "input": "`http://x.io ![](_[…>b||https://y.io/p[ `é||](https://y.io/pa![` ](b |)_word ~_\u0000] ]\n#`", + "expected": "word ] ]" + }, + { + "input": "_\n** ](a`😀)`~\n http://x.io~ ##)>漢 ||![~]]", + "expected": "](a 😀) ) 漢 ![ ] ]" + }, + { + "input": "|||http://x.io||é_", + "expected": "é" + }, + { + "input": "|| aéhttps://y.io/phttps://y.io/p](awordhttp://x.ioa😀éword https://y.io/p_!http://x.io", + "expected": "aé" + }, + { + "input": ">|| 漢\u0000…b~bb _word__\n… _ [![]é", + "expected": "漢 b bb word [![]é" + }, + { + "input": "`漢http://x.io\n]([`![]~\u0000_b", + "expected": "漢 ]([ ![] b" + }, + { + "input": "_b_#](😀wordhttps://y.io/p||word]😀 a>)# ~`>http://x.io!b", + "expected": null + }, + { + "input": " _é`!`😀!_](_ [http://x.io", + "expected": "é ! 😀! ]( [" + }, + { + "input": "b\n|漢…b  漢 ~|word_>https://y.io/p 漢b](~< [ https://y.io/p#[…`< \n\u0000é!)http://x.io| ", + "expected": "b 漢 b 漢 word 漢b]( < [ < é!)" + }, + { + "input": ">#http://x.ioword**](😀b`\nhttps://y.io/p[\n~|]()[|** 漢~|`bé`)| a[>b[|![http://x.io", + "expected": "漢漢[ ]( b[ ![" + }, + { + "input": " éhttps://y.io/p> a ![<漢<##😀>漢 _ 😀\n**http://x.io](\u0000)[>!<…\u0000#_ ](||)", + "expected": "é a !<" + }, + { + "input": " 😀https://y.io/p<**漢||éhttp://x.io\u0000😀😀!`b`…!<漢**\nhttp://x.ioa !](…http://x.io\n\n__)http://x.io](http://x.io", + "expected": "😀 😀😀! b !<漢 !]( )" + }, + { + "input": " é]😀!\u0000[\u0000)](`https://y.io/p~http://x.io>\u0000 ](![)](<é…https://y.io/p\u0000` ", + "expected": "é]😀! )](<é" + }, + { + "input": " )b …", + "expected": ")b" + }, + { + "input": "![http://x.io > ~**漢https://y.io/p<😀é [bb😀 |", + "expected": "![ 漢 [b b😀" + }, + { + "input": "||😀<****https://y.io/p_||](wordhttp://x.io]word!<**>http://x.iohttps://y.io/p!<|| ![a漢**|![](![[b**!![[!word", + "expected": "](word ![a漢 ![](![[b !![[!word" + }, + { + "input": "|", + "expected": null + }, + { + "input": "😀|bé漢https://y.io/p…||**<\nb<…http://x.io_|~~ ||https://y.io/p|> [`漢 漢||漢>]漢word)[ ", + "expected": "😀 bé漢 [ 漢 漢 漢 ]漢word)[" + }, + { + "input": "`~|||éba <_……![漢", + "expected": "a! ![ < ]😀 word漢 ![ 漢 ](![] < ![漢" + }, + { + "input": "\u0000]http://x.io](\u0000](![~é]>](](😀…<[)]http://x.ioé[https://y.io/p)éhttps://y.io/phttp://x.io\u0000word ~)http://x.io![ 😀_>", + "expected": "] ](![ é] ](](😀 <[)] word ) 😀" + }, + { + "input": "a漢\n漢](http://x.io|>\u0000[😀~!a_|` <**|**é😀\u0000 ![  ||😀http://x.io\u0000#http://x.ioab]]([word…\n ", + "expected": "b b ] a 😀 [ word) 😀" + }, + { + "input": "http://x.io😀 a漢|https://y.io/pwordhttps://y.io/p~<` \u0000", + "expected": "a漢" + }, + { + "input": "word !)\u0000||a~\u0000a )é[!]\n[[<😀https://y.io/pé![_\u0000#> >漢!** …https://y.io/pb!!)", + "expected": "!) a a )é[!] [[<😀 漢! !)" + }, + { + "input": "word😀😀\u0000[漢", + "expected": "word😀😀 [漢" + }, + { + "input": "[漢酅a wordb]", + "expected": "[漢 é a wordb]" + }, + { + "input": ">_![|word]||![b~]`word]https://y.io/p|!…_https://y.io/p|a![http://x.io<é`![ 漢https://y.io/p a ** ](!ahttp://x.io…|", + "expected": "![ word] ![b ] word] 漢 a ](!a" + }, + { + "input": "<[!…~![ 😀\u0000**a![** ! b**…https://y.io/p_\u0000`~http://x.io漢漢#漢**_😀|| !~[!😀http://x.ioword", + "expected": "<[! ![ 😀 a![ ! b ! [!😀" + }, + { + "input": "||word…https://y.io/phttps://y.io/p![ word~#)https://y.io/péb #>]!\nhttp://x.ioé[](#_http://x.io_]**[**", + "expected": "word word ) ]!" + }, + { + "input": "漢😀é> _[b]( http://x.io**<](|[`worda>| ||~!word ](![![[![\u0000b漢`", + "expected": "漢😀é [b]( !word ](![ ![[![ b漢" + }, + { + "input": "word!_\u0000_~#><<||😀 !\n**a](\nhttp://x.io** _)!_😀[", + "expected": "😀 ](< ! 😀[" + }, + { + "input": "![](_||\n||\n_word[😀)", + "expected": null + }, + { + "input": "#|](😀é<\n![](||![漢\n||_# )https://y.io/p!\u0000`\n||>b…[…]… word!>😀", + "expected": "](😀é< b [ ] word! 😀" + }, + { + "input": "#~a] **` **`|!]http://x.io**[[] ]http://x.io#_\u0000~!https://y.io/p … _||…", + "expected": "a] !] ] ~](http://x.ioé`bab|b#https://y.io/p", + "expected": "b ](" + }, + { + "input": ")](aa漢漢**a#… ab ![\u0000\u0000|😀![ <éé`_ >`~", + "expected": ")](aa 漢漢 a ab ![ 😀![ <éé" + }, + { + "input": "](https://y.io/p~ ![]([<|](b…#|", + "expected": "]( ![]([< ](é😀…\u0000漢>é]é`_b`\u0000\n <<]![]…>< é[<  _||||<", + "expected": "é😀 漢 é]é b <<]![] < é[< <" + }, + { + "input": "<>`\u0000<…> http://x.ioa😀#**\u0000](é`https://y.io/p)😀_!😀>😀漢", + "expected": "< < ](é" + }, + { + "input": "||||`a [**!b éhttp://x.iohttps://y.io/pé😀é `ahttp://x.io", + "expected": "a [ !b é a" + }, + { + "input": "\u0000#http://x.io", + "expected": null + }, + { + "input": "](word<|`||wordé`b éa**", + "expected": "](word< wordé b éa" + }, + { + "input": "https://y.io/p漢漢worda ![ `http://x.io![**b wordb_baé", + "expected": "![ )](\u0000![)`>word", + "expected": ") < word" + }, + { + "input": "` !](<|b ~\u0000漢😀https://y.io/p~a>é> http://x.io ]( ~|||\n[漢 [||https://y.io/p`_<_|_]~https://y.io/p)||", + "expected": "!](< b 漢😀 ](" + }, + { + "input": "…a漢ab#`worda||https://y.io/p]( ]![_!**`<#[ 😀a#|](aba漢 > ># word…é é[", + "expected": "a漢ab worda ]![ ! < [ 😀a ](aba漢 word é é[" + }, + { + "input": "\u0000 **<\u0000word#https://y.io/p#>…!#", + "expected": "< word !" + }, + { + "input": "…a[…](`word[😀[|<\u0000`http://x.io!http://x.iohttp://x.ioa]\u0000![**||||http://x.io)|\u0000![ ~\n#https://y.io/p](https://y.io/p", + "expected": "a ![" + }, + { + "input": "])aéaba", + "expected": "])aéaba" + }, + { + "input": "😀**", + "expected": "😀" + }, + { + "input": "😀\u0000>word|| word\nhttp://x.io]**`_é#~漢#😀**_!\n_**\u0000**…#b)b!http://x.io # \u0000b ", + "expected": "😀 word word b)b! b" + }, + { + "input": "|é# )\u0000", + "expected": "é )" + }, + { + "input": " !![![word|<😀#[word#a**\n``<é)word\u0000 [\u0000\u0000http://x.io…<é>[)\u0000\u0000![#é word\nhttps://y.io/p ", + "expected": "!![![word <😀 [word a <é)word [ <é [) ![ é word" + }, + { + "input": "] …~||<[漢>` < ]b", + "expected": "] <[漢 < ]b" + }, + { + "input": "aaéé>||_[\n~#<😀a!\n é…](\u0000||<é漢[`http://x.io_<\u0000](…![a~<漢", + "expected": "aaéé <é漢[ ]( ![ http://x.io", + "expected": "😀" + }, + { + "input": "😀\u0000b**](é![ba**éword \n![ \u0000", + "expected": "😀 b ](é![ba éword ![" + }, + { + "input": "[漢 **|<é![ [>", + "expected": "[漢 <é![ [" + }, + { + "input": "http://x.io](é漢)\u0000…b\u0000>😀a## [||é![|https://y.io/p)!bhttps://y.io/p\n#`]", + "expected": "b 😀a [ é![ ]" + }, + { + "input": "b漢<](># ……_http://x.io||~|漢word||#a!b漢…!|ahttp://x.ioword)b||](](\n**", + "expected": "漢word ](](" + }, + { + "input": "`](`|| < _\n😀<\u0000… **` _😀https://y.io/p#é| #|`http://x.io # word>", + "expected": "]( < 😀< 😀 word" + }, + { + "input": "![ http://x.io#", + "expected": "![" + }, + { + "input": "!]()~a||[||** 😀_a)![_<]( ]( ", + "expected": "!]() a 😀 a)![ _  >b `https://y.io/p 漢#`\u0000a>", + "expected": "![) ]aa é b 漢 a" + }, + { + "input": "!\n`](~éé]([wordb\u0000`)é#|ahttp://x.iohttps://y.io/p_|#\u0000**", + "expected": "! ]( éé]([wordb )é a" + }, + { + "input": " é | https://y.io/p||😀  ! #||]…#word`!||https://y.io/p~😀http://x.iohttps://y.io/p\n_**>[**\n…<", + "expected": "![](]😀 worda漢 [ <" + }, + { + "input": "_ ![||~||word] ]<`]** !| \n|~\n> | [||>)!\u0000a~😀||é|", + "expected": "![ word] ]< ] ! [ é" + }, + { + "input": " 😀https://y.io/p![_**漢> bé", + "expected": "😀 bé" + }, + { + "input": "|aword~](!a!", + "expected": "aword ](! a!" + }, + { + "input": "**]()_~~http://x.iob[a![漢 _https://y.io/pa[~|word**![", + "expected": "]()" + }, + { + "input": ">a>|https://y.io/pa😀|😀http://x.io> ****word\n)éhttp://x.io word😀b é", + "expected": "aé[a word )é word😀b é" + }, + { + "input": "![** http://x.iohttp://x.io**`](b…\u0000 ![]]( _ !\nword[\u0000…]| b😀 _b", + "expected": "![ ![ ] ]( ! word[ ] b😀 b" + }, + { + "input": "a][word\u0000b", + "expected": "a][word b" + }, + { + "input": " ](`[<[word]` ![|word ~漢<[http://x.iohttp://x.io![](**[…`[_😀[~** >漢#é>##http://x.io ||", + "expected": "]( [<[word] ![ word 漢<[ [ 😀[ 漢 é" + }, + { + "input": "****#**[_b[](😀 word#!… é word)word😀http://x.io||`😀](**", + "expected": "b[word😀" + }, + { + "input": "||\n#\u0000>![https://y.io/p || \u0000 http://x.io\u0000>](word<[a)a~_ ~ >)béb!http://x.iohttps://y.io/p >\u0000! ", + "expected": "]( word<[a)a ) béb! !" + }, + { + "input": "_\n**a)word)~![ )éa ](https://y.io/p…word~`\u0000]( …!![#漢~!](\u0000!~ )", + "expected": "a)word)" + }, + { + "input": "é[)\u0000 b]< **a)\n`![]…# ]]!word[😀", + "expected": "![ b < a) ~#![<…a#< https://y.io/p _é**>é](\n**漢https://y.io/phttps://y.io/p[_< **\n", + "expected": "< ![< a < é é]( 漢" + }, + { + "input": "https://y.io/p <…![` ~<<_>aéhttps://y.io/p!word **漢|]( \n ~word漢😀>![]( _https://y.io/p ])](漢||😀http://x.io", + "expected": "< ](漢 😀" + } +] diff --git a/crates/buzz-cli/src/lib.rs b/crates/buzz-cli/src/lib.rs index 9c343a61fa..663bc5cea3 100644 --- a/crates/buzz-cli/src/lib.rs +++ b/crates/buzz-cli/src/lib.rs @@ -463,7 +463,7 @@ buzz agents archived" pub enum MessagesCmd { /// Send a message to a channel #[command( - after_help = "Examples:\n buzz messages send --channel --content \"hello\"\n buzz messages send --channel --content \"@alice check this\"\n echo \"hello from stdin\" | buzz messages send --channel --content -" + after_help = "Examples:\n buzz messages send --channel --content \"hello\"\n buzz messages send --channel --content \"@alice check this\"\n echo \"hello from stdin\" | buzz messages send --channel --content -\n buzz messages send --channel --content \"decision: ship it\" --from-thread " )] Send { /// Channel UUID (from 'buzz channels list') @@ -478,6 +478,9 @@ pub enum MessagesCmd { /// Event ID to reply to (creates a thread) #[arg(long)] reply_to: Option, + /// Thread root event ID: send this as a new top-level message tagged as sent from that thread (like desktop's "Send to channel") + #[arg(long, conflicts_with_all = ["reply_to", "broadcast"])] + from_thread: Option, /// Also publish to the Nostr network #[arg(long, default_value_t = false)] broadcast: bool, @@ -2433,6 +2436,29 @@ mod tests { Cli::command().debug_assert(); } + #[test] + fn messages_send_from_thread_conflicts_with_reply_and_broadcast() { + let base = [ + "buzz", + "messages", + "send", + "--channel", + "123e4567-e89b-12d3-a456-426614174000", + "--content", + "x", + "--from-thread", + ]; + let with = |extra: &[&'static str]| { + let mut args = base.to_vec(); + args.push("a"); + args.extend(extra); + Cli::try_parse_from(args) + }; + assert!(with(&[]).is_ok()); + assert!(with(&["--reply-to", "b"]).is_err()); + assert!(with(&["--broadcast"]).is_err()); + } + #[test] fn messages_thread_accepts_link_or_explicit_identifiers() { let channel = "123e4567-e89b-12d3-a456-426614174000";