use super::query::segment_input;
use super::ShuangpinProfile;
use crate::types::{ShuangpinHelpcodeModes, SyllableHelpcode};
pub const MID_SENTENCE_MARKER: u8 = b'`';
pub const END_HELPCODE_MARKER: u8 = b'|';
pub const DIRECT_HELPCODE_SLASH: u8 = b'/';
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct UppercaseTriggers {
pub mid: bool,
pub end: bool,
}
impl UppercaseTriggers {
pub fn new(mid: bool, end: bool) -> Self {
Self {
mid,
end: end && !mid,
}
}
}
pub fn uppercase_triggers(
modes: &ShuangpinHelpcodeModes,
shuangpin_helpcode_enabled: bool,
) -> UppercaseTriggers {
let mid = modes.mid_sentence && modes.mid_sentence_uppercase && !modes.direct_helpcode;
let end = shuangpin_helpcode_enabled
&& !modes.direct
&& modes.trigger_uppercase
&& !modes.direct_helpcode
&& !mid;
UppercaseTriggers::new(mid, end)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockKind {
Mid,
End,
}
fn is_ascii_upper(ch: u8) -> bool {
ch.is_ascii_uppercase()
}
fn block_end(text: &[u8], marker: usize) -> usize {
let mut end = marker + 1;
if end < text.len() && text[end].is_ascii_alphabetic() {
end += 1;
if end < text.len() && is_ascii_upper(text[end]) {
end += 1;
}
}
end
}
fn end_block_end(text: &[u8], first: usize, mut codes: usize) -> usize {
let mut end = first + 1;
while codes > 0 && end < text.len() && text[end].is_ascii_alphabetic() {
end += 1;
codes -= 1;
}
end
}
fn starts_uppercase_block(ch: u8, chunk_length: usize) -> bool {
is_ascii_upper(ch) && chunk_length >= 2 && chunk_length.is_multiple_of(2)
}
fn uppercase_block_end(text: &[u8], first: usize) -> usize {
let mut end = first + 1;
if end < text.len() && is_ascii_upper(text[end]) {
end += 1;
}
end
}
pub fn scan_blocks(
text: &[u8],
triggers: UppercaseTriggers,
mut on_block: impl FnMut(usize, usize, usize, BlockKind),
) -> usize {
let mut chunk_start = 0;
let mut index = 0;
while index < text.len() {
match text[index] {
b'\'' => {
index += 1;
chunk_start = index;
}
MID_SENTENCE_MARKER => {
let end = block_end(text, index);
on_block(index, end, index + 1, BlockKind::Mid);
chunk_start = end;
index = end;
}
END_HELPCODE_MARKER => {
let end = end_block_end(text, index, 2);
on_block(index, end, index + 1, BlockKind::End);
chunk_start = end;
index = end;
}
ch if triggers.mid && starts_uppercase_block(ch, index - chunk_start) => {
let end = uppercase_block_end(text, index);
on_block(index, end, index, BlockKind::Mid);
chunk_start = end;
index = end;
}
ch if triggers.end && starts_uppercase_block(ch, index - chunk_start) => {
let end = end_block_end(text, index, 1);
on_block(index, end, index, BlockKind::End);
chunk_start = end;
index = end;
}
_ => index += 1,
}
}
chunk_start
}
pub fn chunk_start(text: &[u8], triggers: UppercaseTriggers) -> usize {
scan_blocks(text, triggers, |_, _, _, _| {})
}
pub fn accepts_marker(text: &[u8], triggers: UppercaseTriggers) -> bool {
let chunk_length = text.len() - chunk_start(text, triggers);
chunk_length >= 2 && chunk_length.is_multiple_of(2)
}
pub fn accepts_marker_at(text: &[u8], caret: usize, triggers: UppercaseTriggers) -> bool {
caret <= text.len()
&& accepts_marker(&text[..caret], triggers)
&& (caret == text.len()
|| (text[caret] != MID_SENTENCE_MARKER
&& !((triggers.mid || triggers.end) && is_ascii_upper(text[caret]))))
}
pub fn accepts_end_marker_at(text: &[u8], caret: usize, triggers: UppercaseTriggers) -> bool {
caret == text.len() && accepts_marker(text, triggers)
}
#[allow(dead_code)]
pub fn accepts_second_code_at(text: &[u8], caret: usize, triggers: UppercaseTriggers) -> bool {
if caret > text.len() {
return false;
}
let mut open = false;
scan_blocks(&text[..caret], triggers, |_, end, codes_begin, kind| {
open = kind == BlockKind::Mid && end == caret && end - codes_begin == 1;
});
open && text[caret - 1].is_ascii_alphabetic()
}
pub fn accepts_semicolon_final_at(text: &[u8], caret: usize, triggers: UppercaseTriggers) -> bool {
if caret > text.len() {
return false;
}
let mut scan = triggers.mid || triggers.end;
for &byte in &text[..caret] {
if scan {
break;
}
scan = byte == END_HELPCODE_MARKER || byte == MID_SENTENCE_MARKER;
}
let start = if scan {
chunk_start(&text[..caret], triggers)
} else {
text[..caret]
.iter()
.rposition(|&byte| byte == b'\'')
.map_or(0, |at| at + 1)
};
(caret - start) % 2 == 1
}
fn count_segments(segmentation: &str) -> usize {
if segmentation.is_empty() {
0
} else {
segmentation.bytes().filter(|&byte| byte == b'\'').count() + 1
}
}
#[derive(Debug, Clone, Default)]
pub struct ParsedHelpcodes {
pub input: String,
pub source_index: Vec<usize>,
pub helpcodes: Vec<SyllableHelpcode>,
pub decorations: Vec<(usize, String)>,
pub end_helpcodes: String,
}
pub fn has_mid_sentence_helpcode(raw: &[u8], triggers: UppercaseTriggers) -> bool {
if raw.contains(&MID_SENTENCE_MARKER) || raw.contains(&END_HELPCODE_MARKER) {
return true;
}
(triggers.mid || triggers.end) && {
let mut found = false;
scan_blocks(raw, triggers, |_, _, _, _| found = true);
found
}
}
pub fn parse_mid_sentence_helpcodes(
raw_with_cases: &str,
profile: &ShuangpinProfile,
triggers: UppercaseTriggers,
) -> ParsedHelpcodes {
let text = raw_with_cases.as_bytes();
let mut parsed = ParsedHelpcodes {
input: String::with_capacity(text.len()),
source_index: Vec::with_capacity(text.len() + 1),
..ParsedHelpcodes::default()
};
let mut blocks = Vec::new();
scan_blocks(text, triggers, |begin, end, codes_begin, kind| {
blocks.push((begin, end, codes_begin, kind));
});
let mut next_block = blocks.iter().peekable();
let mut index = 0;
while index < text.len() {
let Some(&&(_begin, end, codes_begin, kind)) = next_block.peek().filter(|b| b.0 == index)
else {
parsed.input.push(text[index] as char);
parsed.source_index.push(index);
index += 1;
continue;
};
next_block.next();
let syllables = count_segments(&segment_input(&parsed.input.to_lowercase(), profile));
if kind == BlockKind::End {
if syllables > 0 {
parsed
.decorations
.push((syllables - 1, raw_with_cases[index..end].to_owned()));
}
if end == text.len() {
parsed.end_helpcodes.clear();
for &code in &text[codes_begin..end] {
parsed
.end_helpcodes
.push((code as char).to_ascii_lowercase());
}
index = end;
continue;
}
} else if syllables > 0 {
let syllable = syllables - 1;
parsed
.decorations
.push((syllable, raw_with_cases[index..end].to_owned()));
if end > codes_begin {
let helpcode = SyllableHelpcode {
syllable,
first: (text[codes_begin] as char).to_ascii_lowercase() as u8,
second: if end > codes_begin + 1 {
(text[codes_begin + 1] as char).to_ascii_lowercase() as u8
} else {
0
},
};
if let Some(existing) = parsed
.helpcodes
.iter_mut()
.find(|item| item.syllable == syllable)
{
*existing = helpcode;
} else {
parsed.helpcodes.push(helpcode);
}
}
}
if !parsed.input.is_empty() && !parsed.input.ends_with('\'') {
parsed.input.push('\'');
parsed.source_index.push(index);
}
index = end;
}
parsed.source_index.push(text.len());
parsed
}
pub fn syllable_helpcode_matches(helpcode: &SyllableHelpcode, code: &[u8]) -> bool {
!code.is_empty()
&& code[0] == helpcode.first
&& (helpcode.second == 0 || code.get(1) == Some(&helpcode.second))
}
pub fn syllable_helpcodes_signature(helpcodes: &[SyllableHelpcode]) -> String {
let mut signature = String::new();
for helpcode in helpcodes {
signature.push('`');
signature.push_str(&helpcode.syllable.to_string());
signature.push(helpcode.first as char);
if helpcode.second != 0 {
signature.push(helpcode.second as char);
}
}
signature
}
pub fn satisfies_syllable_helpcodes(
helpcodes: &[SyllableHelpcode],
word: &str,
keymap: &crate::helpcode::HelpcodeKeymap,
) -> bool {
let chars: Vec<char> = word.chars().collect();
helpcodes.iter().all(|helpcode| {
chars.get(helpcode.syllable).is_none_or(|ch| {
let mut buf = [0u8; 4];
keymap
.code(ch.encode_utf8(&mut buf))
.is_some_and(|code| syllable_helpcode_matches(helpcode, code.as_bytes()))
})
})
}
pub fn decorate_segmentation(segmentation: &str, decorations: &[(usize, String)]) -> String {
if decorations.is_empty() || segmentation.is_empty() {
return segmentation.to_owned();
}
let mut parts: Vec<String> = segmentation.split('\'').map(str::to_owned).collect();
for &(syllable, ref text) in decorations {
if let Some(part) = parts.get_mut(syllable) {
part.push_str(text);
}
}
parts.join("'")
}