mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
264 lines
6.6 KiB
Rust
264 lines
6.6 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use crate::ast::Node;
|
|
use crate::constants::{MAX_LINE_LENGTH, MAX_LINK_URL_LENGTH};
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct Line {
|
|
pub text: String,
|
|
pub offset: usize,
|
|
}
|
|
|
|
pub fn split_lines(input: &str) -> Vec<Line> {
|
|
if input.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
let mut out = Vec::new();
|
|
let mut start = 0;
|
|
for segment in input.split_inclusive('\n') {
|
|
if out.len() >= crate::constants::MAX_LINES {
|
|
break;
|
|
}
|
|
let text = segment.strip_suffix('\n').unwrap_or(segment);
|
|
out.push(Line {
|
|
text: bounded_line_text(text).to_owned(),
|
|
offset: start,
|
|
});
|
|
start += segment.len();
|
|
}
|
|
if start < input.len() && out.len() < crate::constants::MAX_LINES {
|
|
out.push(Line {
|
|
text: bounded_line_text(&input[start..]).to_owned(),
|
|
offset: start,
|
|
});
|
|
}
|
|
if out.len() == 1 && out[0].text.is_empty() {
|
|
out.clear();
|
|
}
|
|
out
|
|
}
|
|
|
|
pub fn trim_start(value: &str) -> &str {
|
|
value.trim_start_matches([' ', '\t', '\r', '\n'])
|
|
}
|
|
|
|
pub fn trim_right(value: &str) -> &str {
|
|
value.trim_end_matches([' ', '\t', '\r', '\n'])
|
|
}
|
|
|
|
pub fn trim(value: &str) -> &str {
|
|
value.trim_matches([' ', '\t', '\r', '\n'])
|
|
}
|
|
|
|
pub fn bounded_utf8_prefix_length(value: &str, max_len: usize) -> usize {
|
|
let mut end = value.len().min(max_len);
|
|
while end > 0 && end < value.len() && !value.is_char_boundary(end) {
|
|
end -= 1;
|
|
}
|
|
end
|
|
}
|
|
|
|
pub fn bounded_line_text(value: &str) -> &str {
|
|
&value[..bounded_utf8_prefix_length(value, MAX_LINE_LENGTH)]
|
|
}
|
|
|
|
pub fn concat(left: &str, right: &str) -> String {
|
|
if left.is_empty() {
|
|
return right.to_owned();
|
|
}
|
|
if right.is_empty() {
|
|
return left.to_owned();
|
|
}
|
|
let mut out = String::with_capacity(left.len() + right.len());
|
|
out.push_str(left);
|
|
out.push_str(right);
|
|
out
|
|
}
|
|
|
|
pub fn concat3(a: &str, b: &str, c: &str) -> String {
|
|
let mut out = String::with_capacity(a.len() + b.len() + c.len());
|
|
out.push_str(a);
|
|
out.push_str(b);
|
|
out.push_str(c);
|
|
out
|
|
}
|
|
|
|
pub fn append_text(nodes: &mut Vec<Node>, content: impl Into<String>) {
|
|
let content = content.into();
|
|
if content.is_empty() {
|
|
return;
|
|
}
|
|
if let Some(Node::Text { content: previous }) = nodes.last_mut() {
|
|
previous.push_str(&content);
|
|
return;
|
|
}
|
|
nodes.push(Node::Text { content });
|
|
}
|
|
|
|
pub fn line_as_text(lines: &[Line], current: usize) -> String {
|
|
if current + 1 == lines.len() {
|
|
lines[current].text.clone()
|
|
} else {
|
|
concat(&lines[current].text, "\n")
|
|
}
|
|
}
|
|
|
|
pub fn lines_to_text(lines: &[Line]) -> String {
|
|
let mut out = String::new();
|
|
for (index, line) in lines.iter().enumerate() {
|
|
if index > 0 {
|
|
out.push('\n');
|
|
}
|
|
out.push_str(&line.text);
|
|
}
|
|
out
|
|
}
|
|
|
|
pub fn trim_line_window(lines: &mut [Line]) -> Vec<Line> {
|
|
let mut start = 0;
|
|
let mut end = lines.len();
|
|
while start < end && trim(&lines[start].text).is_empty() {
|
|
start += 1;
|
|
}
|
|
while end > start && trim(&lines[end - 1].text).is_empty() {
|
|
end -= 1;
|
|
}
|
|
if start >= end {
|
|
return Vec::new();
|
|
}
|
|
let mut out = lines[start..end].to_vec();
|
|
let first_trimmed_len = trim_start(&out[0].text).len();
|
|
let leading = out[0].text.len() - first_trimmed_len;
|
|
out[0].offset += leading;
|
|
out[0].text = trim_start(&out[0].text).to_owned();
|
|
let last = out.len() - 1;
|
|
out[last].text = trim_right(&out[last].text).to_owned();
|
|
out
|
|
}
|
|
|
|
#[inline]
|
|
pub fn starts_with(value: &str, prefix: &str) -> bool {
|
|
value.as_bytes().starts_with(prefix.as_bytes())
|
|
}
|
|
|
|
#[inline]
|
|
pub fn ends_with(value: &[u8], suffix: &[u8]) -> bool {
|
|
value.ends_with(suffix)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn is_digit(byte: u8) -> bool {
|
|
byte.is_ascii_digit()
|
|
}
|
|
|
|
#[inline]
|
|
pub fn is_alpha_numeric(byte: u8) -> bool {
|
|
byte.is_ascii_alphanumeric()
|
|
}
|
|
|
|
pub fn is_digit_only(value: &str) -> bool {
|
|
!value.is_empty() && value.bytes().all(is_digit)
|
|
}
|
|
|
|
pub fn is_whitespace(byte: u8) -> bool {
|
|
matches!(byte, 0 | b'\t' | b'\n' | b'\r' | b' ')
|
|
}
|
|
|
|
pub fn has_visible_content(value: &str) -> bool {
|
|
value.chars().any(|char| {
|
|
!char.is_whitespace()
|
|
&& !matches!(
|
|
char,
|
|
'\0'
|
|
| '\u{00ad}'
|
|
| '\u{034f}'
|
|
| '\u{061c}'
|
|
| '\u{115f}'
|
|
| '\u{1160}'
|
|
| '\u{17b4}'
|
|
| '\u{17b5}'
|
|
| '\u{180e}'
|
|
| '\u{200b}'..='\u{200f}'
|
|
| '\u{202a}'..='\u{202e}'
|
|
| '\u{2060}'..='\u{2069}'
|
|
| '\u{2800}'
|
|
| '\u{3164}'
|
|
| '\u{fe00}'..='\u{fe0f}'
|
|
| '\u{feff}'
|
|
| '\u{ffa0}'
|
|
| '\u{e0100}'..='\u{e01ef}'
|
|
)
|
|
})
|
|
}
|
|
|
|
pub fn is_punctuation(byte: u8) -> bool {
|
|
(33..=47).contains(&byte)
|
|
|| (58..=64).contains(&byte)
|
|
|| (91..=96).contains(&byte)
|
|
|| (123..=126).contains(&byte)
|
|
}
|
|
|
|
pub fn is_escapable_character(byte: u8) -> bool {
|
|
matches!(
|
|
byte,
|
|
b'[' | b']'
|
|
| b'('
|
|
| b')'
|
|
| b'\\'
|
|
| b'*'
|
|
| b'_'
|
|
| b'~'
|
|
| b'`'
|
|
| b'@'
|
|
| b'#'
|
|
| b'-'
|
|
| b'|'
|
|
| b':'
|
|
| b'<'
|
|
| b'>'
|
|
)
|
|
}
|
|
|
|
pub fn byte_at(value: &str, index: usize) -> u8 {
|
|
value.as_bytes().get(index).copied().unwrap_or(0)
|
|
}
|
|
|
|
pub fn advance_one(value: &str, index: usize) -> usize {
|
|
if index >= value.len() {
|
|
return 0;
|
|
}
|
|
if value.is_char_boundary(index) {
|
|
value[index..].chars().next().map_or(1, char::len_utf8)
|
|
} else {
|
|
1
|
|
}
|
|
}
|
|
|
|
pub fn find_from(value: &str, start: usize, needle: u8) -> Option<usize> {
|
|
value.as_bytes()[start..]
|
|
.iter()
|
|
.position(|byte| *byte == needle)
|
|
.map(|pos| start + pos)
|
|
}
|
|
|
|
pub fn bounded_url_end(value: &str, end: usize, prefix_len: usize) -> usize {
|
|
bounded_utf8_prefix_length(&value[..end], prefix_len + MAX_LINK_URL_LENGTH)
|
|
}
|
|
|
|
pub fn remove_text_presentation(text: &str) -> String {
|
|
if !text.contains('\u{fe0f}') {
|
|
return text.to_owned();
|
|
}
|
|
text.replace("™️", "™")
|
|
.replace("©️", "©")
|
|
.replace("®️", "®")
|
|
}
|
|
|
|
pub fn trim_start_newline_whitespace(value: &str) -> &str {
|
|
value.trim_start_matches([' ', '\t', '\n', '\r'])
|
|
}
|
|
|
|
pub fn string_from_lossy(bytes: &[u8]) -> String {
|
|
String::from_utf8_lossy(bytes).into_owned()
|
|
}
|