use crate::diagnostics::{Error as DiagnosticError, Result};
use crate::eval::value::{Value, PrimitiveProcedure, PrimitiveImpl, ThreadSafeEnvironment};
#[cfg(feature = "text-processing")]
use icu_collator::{Collator, CollatorOptions};
#[cfg(feature = "text-processing")]
use icu_locid::Locale;
#[cfg(not(feature = "text-processing"))]
#[derive(Debug, Clone)]
struct Collator;
#[cfg(not(feature = "text-processing"))]
#[derive(Debug, Clone)]
struct CollatorOptions;
#[cfg(not(feature = "text-processing"))]
impl CollatorOptions {
fn new() -> Self { Self }
}
#[cfg(not(feature = "text-processing"))]
impl Collator {
fn new(_options: &CollatorOptions) -> std::result::Result<Self, &'static str> {
Ok(Self)
}
fn compare(&self, a: &str, b: &str) -> Ordering {
a.cmp(b)
}
}
use crate::effects::Effect;
use crate::stdlib::text::{Text, TextBuilder};
use std::sync::Arc;
use std::collections::HashMap;
use std::cmp::Ordering;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug)]
pub struct TextLocale {
language: String,
country: Option<String>,
collator: Option<Collator>,
case_mapping: CaseMappingRules,
}
impl Clone for TextLocale {
fn clone(&self) -> Self {
Self {
language: self.language.clone(),
country: self.country.clone(),
collator: None, case_mapping: self.case_mapping.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct CaseMappingRules {
uppercase_mappings: HashMap<char, String>,
lowercase_mappings: HashMap<char, String>,
titlecase_mappings: HashMap<char, String>,
}
#[derive(Debug, Clone)]
pub struct TextCursor {
text: Text,
position: usize,
boundary_type: BoundaryType,
}
#[derive(Debug, Clone, Copy)]
pub enum BoundaryType {
Character,
Grapheme,
Word,
Sentence,
Line,
}
#[derive(Debug, Clone)]
pub struct TextRange {
start: usize,
end: usize,
boundary_type: BoundaryType,
}
impl TextLocale {
pub fn new(language: &str, country: Option<&str>) -> Result<Self> {
let collator_options = CollatorOptions::new();
#[cfg(feature = "text-processing")]
{
let _strength = "primary"; }
#[cfg(feature = "text-processing")]
let collator = {
use icu_locid::locale;
let locale_data = locale!("en");
let options = CollatorOptions::new();
Collator::try_new(&locale_data.into(), options).ok()
};
#[cfg(not(feature = "text-processing"))]
let collator = Collator::new(&collator_options)
.map(Some)
.unwrap_or(None);
Ok(Self {
language: language.to_string(),
country: country.map(|s| s.to_string()),
collator,
case_mapping: CaseMappingRules::default(),
})
}
pub fn system_default() -> Self {
Self {
language: "en".to_string(),
country: Some("US".to_string()),
collator: None,
case_mapping: CaseMappingRules::default(),
}
}
pub fn compare(&self, text1: &Text, text2: &Text) -> Ordering {
match &self.collator {
Some(collator) => {
let s1 = text1.to_string();
let s2 = text2.to_string();
collator.compare(&s1, &s2)
}
None => text1.compare(text2),
}
}
pub fn to_uppercase(&self, text: &Text) -> Text {
let s = text.to_string();
let mut result = String::new();
for ch in s.chars() {
if let Some(mapping) = self.case_mapping.uppercase_mappings.get(&ch) {
result.push_str(mapping);
} else {
result.extend(ch.to_uppercase());
}
}
Text::from_string(result)
}
pub fn to_lowercase(&self, text: &Text) -> Text {
let s = text.to_string();
let mut result = String::new();
for ch in s.chars() {
if let Some(mapping) = self.case_mapping.lowercase_mappings.get(&ch) {
result.push_str(mapping);
} else {
result.extend(ch.to_lowercase());
}
}
Text::from_string(result)
}
pub fn to_titlecase(&self, text: &Text) -> Text {
let s = text.to_string();
let mut result = String::new();
let mut word_start = true;
for ch in s.chars() {
if ch.is_whitespace() {
result.push(ch);
word_start = true;
} else if word_start && ch.is_alphabetic() {
if let Some(mapping) = self.case_mapping.titlecase_mappings.get(&ch) {
result.push_str(mapping);
} else {
result.extend(ch.to_uppercase());
}
word_start = false;
} else {
result.extend(ch.to_lowercase());
}
}
Text::from_string(result)
}
}
impl Default for CaseMappingRules {
fn default() -> Self {
let mut uppercase_mappings = HashMap::new();
let mut lowercase_mappings = HashMap::new();
let titlecase_mappings = HashMap::new();
uppercase_mappings.insert('ß', "SS".to_string());
if std::env::var("LANG").unwrap_or_default().starts_with("tr") {
uppercase_mappings.insert('i', "İ".to_string());
lowercase_mappings.insert('I', "ı".to_string());
}
Self {
uppercase_mappings,
lowercase_mappings,
titlecase_mappings,
}
}
}
impl TextCursor {
pub fn new(text: Text, boundary_type: BoundaryType) -> Self {
Self {
text,
position: 0,
boundary_type,
}
}
pub fn position(&self) -> usize {
self.position
}
pub fn text(&self) -> &Text {
&self.text
}
pub fn advance(&mut self) -> bool {
match self.boundary_type {
BoundaryType::Character => {
if self.position < self.text.char_length() {
self.position += 1;
true
} else {
false
}
}
BoundaryType::Grapheme => {
let s = self.text.to_string();
let indices = s.grapheme_indices(true);
let mut found_current = false;
for (i, _) in indices {
if found_current {
self.position = s[..i].chars().count();
return true;
}
if s[..i].chars().count() == self.position {
found_current = true;
}
}
if found_current && self.position < self.text.char_length() {
self.position = self.text.char_length();
true
} else {
false
}
}
BoundaryType::Word => {
let s = self.text.to_string();
let word_indices: Vec<usize> = s.split_word_bounds()
.collect::<Vec<&str>>()
.iter()
.scan(0, |acc, word| {
let start = *acc;
*acc += word.chars().count();
Some(start)
})
.collect();
for &idx in &word_indices {
if idx > self.position {
self.position = idx;
return true;
}
}
false
}
_ => false, }
}
pub fn previous(&mut self) -> bool {
match self.boundary_type {
BoundaryType::Character => {
if self.position > 0 {
self.position -= 1;
true
} else {
false
}
}
BoundaryType::Grapheme => {
let s = self.text.to_string();
let indices: Vec<usize> = s.grapheme_indices(true)
.map(|(i, _)| s[..i].chars().count())
.collect();
for &idx in indices.iter().rev() {
if idx < self.position {
self.position = idx;
return true;
}
}
false
}
BoundaryType::Word => {
let s = self.text.to_string();
let word_indices: Vec<usize> = s.split_word_bounds()
.collect::<Vec<&str>>()
.iter()
.scan(0, |acc, word| {
let start = *acc;
*acc += word.chars().count();
Some(start)
})
.collect();
for &idx in word_indices.iter().rev() {
if idx < self.position {
self.position = idx;
return true;
}
}
false
}
_ => false,
}
}
pub fn current_char(&self) -> Option<char> {
self.text.char_at(self.position)
}
pub fn current_segment(&self) -> Option<Text> {
let mut temp_cursor = self.clone();
let start = self.position;
if temp_cursor.advance() {
self.text.substring(start, temp_cursor.position)
} else if start < self.text.char_length() {
self.text.substring(start, self.text.char_length())
} else {
None
}
}
}
impl TextRange {
pub fn new(start: usize, end: usize, boundary_type: BoundaryType) -> Self {
Self {
start: start.min(end),
end: start.max(end),
boundary_type,
}
}
pub fn start(&self) -> usize {
self.start
}
pub fn end(&self) -> usize {
self.end
}
pub fn length(&self) -> usize {
self.end - self.start
}
pub fn is_empty(&self) -> bool {
self.start == self.end
}
pub fn extract_text(&self, text: &Text) -> Option<Text> {
text.substring(self.start, self.end)
}
}
pub fn create_complete_srfi135_bindings(env: &Arc<ThreadSafeEnvironment>) {
bind_srfi135_constructors(env);
bind_srfi135_predicates(env);
bind_srfi135_selection(env);
bind_srfi135_comparison(env);
bind_srfi135_prefix_suffix(env);
bind_srfi135_searching(env);
bind_srfi135_case_conversion(env);
bind_srfi135_reverse_replace(env);
bind_srfi135_split_concat(env);
bind_srfi135_locale_aware(env);
bind_srfi135_cursor_range(env);
}
fn bind_srfi135_constructors(env: &Arc<ThreadSafeEnvironment>) {
env.define("textual-empty?".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-empty?".to_string(),
arity_min: 1,
arity_max: Some(1),
implementation: PrimitiveImpl::RustFn(primitive_textual_empty_p),
effects: vec![Effect::Pure],
})));
env.define("textual-length".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-length".to_string(),
arity_min: 1,
arity_max: Some(1),
implementation: PrimitiveImpl::RustFn(primitive_textual_length),
effects: vec![Effect::Pure],
})));
env.define("textual-ref".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-ref".to_string(),
arity_min: 2,
arity_max: Some(2),
implementation: PrimitiveImpl::RustFn(primitive_textual_ref),
effects: vec![Effect::Pure],
})));
}
fn bind_srfi135_predicates(env: &Arc<ThreadSafeEnvironment>) {
env.define("textual?".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual?".to_string(),
arity_min: 1,
arity_max: Some(1),
implementation: PrimitiveImpl::RustFn(primitive_textual_p),
effects: vec![Effect::Pure],
})));
env.define("textual-every".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-every".to_string(),
arity_min: 2,
arity_max: None,
implementation: PrimitiveImpl::RustFn(primitive_textual_every),
effects: vec![Effect::Pure],
})));
env.define("textual-any".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-any".to_string(),
arity_min: 2,
arity_max: None,
implementation: PrimitiveImpl::RustFn(primitive_textual_any),
effects: vec![Effect::Pure],
})));
}
fn bind_srfi135_selection(env: &Arc<ThreadSafeEnvironment>) {
env.define("textual-take".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-take".to_string(),
arity_min: 2,
arity_max: Some(2),
implementation: PrimitiveImpl::RustFn(primitive_textual_take),
effects: vec![Effect::Pure],
})));
env.define("textual-drop".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-drop".to_string(),
arity_min: 2,
arity_max: Some(2),
implementation: PrimitiveImpl::RustFn(primitive_textual_drop),
effects: vec![Effect::Pure],
})));
env.define("textual-take-right".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-take-right".to_string(),
arity_min: 2,
arity_max: Some(2),
implementation: PrimitiveImpl::RustFn(primitive_textual_take_right),
effects: vec![Effect::Pure],
})));
env.define("textual-drop-right".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-drop-right".to_string(),
arity_min: 2,
arity_max: Some(2),
implementation: PrimitiveImpl::RustFn(primitive_textual_drop_right),
effects: vec![Effect::Pure],
})));
}
fn bind_srfi135_comparison(env: &Arc<ThreadSafeEnvironment>) {
env.define("textual-compare".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-compare".to_string(),
arity_min: 5,
arity_max: Some(7),
implementation: PrimitiveImpl::RustFn(primitive_textual_compare),
effects: vec![Effect::Pure],
})));
env.define("textual-compare-ci".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-compare-ci".to_string(),
arity_min: 5,
arity_max: Some(7),
implementation: PrimitiveImpl::RustFn(primitive_textual_compare_ci),
effects: vec![Effect::Pure],
})));
}
fn bind_srfi135_prefix_suffix(env: &Arc<ThreadSafeEnvironment>) {
env.define("textual-prefix-length".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-prefix-length".to_string(),
arity_min: 2,
arity_max: Some(6),
implementation: PrimitiveImpl::RustFn(primitive_textual_prefix_length),
effects: vec![Effect::Pure],
})));
env.define("textual-suffix-length".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-suffix-length".to_string(),
arity_min: 2,
arity_max: Some(6),
implementation: PrimitiveImpl::RustFn(primitive_textual_suffix_length),
effects: vec![Effect::Pure],
})));
env.define("textual-prefix?".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-prefix?".to_string(),
arity_min: 2,
arity_max: Some(6),
implementation: PrimitiveImpl::RustFn(primitive_textual_prefix_p),
effects: vec![Effect::Pure],
})));
env.define("textual-suffix?".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-suffix?".to_string(),
arity_min: 2,
arity_max: Some(6),
implementation: PrimitiveImpl::RustFn(primitive_textual_suffix_p),
effects: vec![Effect::Pure],
})));
}
fn bind_srfi135_searching(env: &Arc<ThreadSafeEnvironment>) {
env.define("textual-index".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-index".to_string(),
arity_min: 2,
arity_max: Some(4),
implementation: PrimitiveImpl::RustFn(primitive_textual_index),
effects: vec![Effect::Pure],
})));
env.define("textual-index-right".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-index-right".to_string(),
arity_min: 2,
arity_max: Some(4),
implementation: PrimitiveImpl::RustFn(primitive_textual_index_right),
effects: vec![Effect::Pure],
})));
env.define("textual-skip".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-skip".to_string(),
arity_min: 2,
arity_max: Some(4),
implementation: PrimitiveImpl::RustFn(primitive_textual_skip),
effects: vec![Effect::Pure],
})));
env.define("textual-skip-right".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-skip-right".to_string(),
arity_min: 2,
arity_max: Some(4),
implementation: PrimitiveImpl::RustFn(primitive_textual_skip_right),
effects: vec![Effect::Pure],
})));
env.define("textual-contains".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-contains".to_string(),
arity_min: 2,
arity_max: Some(6),
implementation: PrimitiveImpl::RustFn(primitive_textual_contains),
effects: vec![Effect::Pure],
})));
}
fn bind_srfi135_case_conversion(env: &Arc<ThreadSafeEnvironment>) {
env.define("textual-upcase".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-upcase".to_string(),
arity_min: 1,
arity_max: Some(3),
implementation: PrimitiveImpl::RustFn(primitive_textual_upcase),
effects: vec![Effect::Pure],
})));
env.define("textual-downcase".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-downcase".to_string(),
arity_min: 1,
arity_max: Some(3),
implementation: PrimitiveImpl::RustFn(primitive_textual_downcase),
effects: vec![Effect::Pure],
})));
env.define("textual-foldcase".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-foldcase".to_string(),
arity_min: 1,
arity_max: Some(3),
implementation: PrimitiveImpl::RustFn(primitive_textual_foldcase),
effects: vec![Effect::Pure],
})));
env.define("textual-titlecase".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-titlecase".to_string(),
arity_min: 1,
arity_max: Some(3),
implementation: PrimitiveImpl::RustFn(primitive_textual_titlecase),
effects: vec![Effect::Pure],
})));
}
fn bind_srfi135_reverse_replace(env: &Arc<ThreadSafeEnvironment>) {
env.define("textual-reverse".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-reverse".to_string(),
arity_min: 1,
arity_max: Some(3),
implementation: PrimitiveImpl::RustFn(primitive_textual_reverse),
effects: vec![Effect::Pure],
})));
env.define("textual-replace".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-replace".to_string(),
arity_min: 4,
arity_max: Some(6),
implementation: PrimitiveImpl::RustFn(primitive_textual_replace),
effects: vec![Effect::Pure],
})));
}
fn bind_srfi135_split_concat(env: &Arc<ThreadSafeEnvironment>) {
env.define("textual-split".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-split".to_string(),
arity_min: 2,
arity_max: Some(6),
implementation: PrimitiveImpl::RustFn(primitive_textual_split),
effects: vec![Effect::Pure],
})));
env.define("textual-concatenate".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-concatenate".to_string(),
arity_min: 1,
arity_max: Some(1),
implementation: PrimitiveImpl::RustFn(primitive_textual_concatenate),
effects: vec![Effect::Pure],
})));
env.define("textual-concatenate-reverse".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-concatenate-reverse".to_string(),
arity_min: 1,
arity_max: Some(3),
implementation: PrimitiveImpl::RustFn(primitive_textual_concatenate_reverse),
effects: vec![Effect::Pure],
})));
}
fn bind_srfi135_locale_aware(env: &Arc<ThreadSafeEnvironment>) {
env.define("textual-locale-compare".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-locale-compare".to_string(),
arity_min: 3,
arity_max: Some(3),
implementation: PrimitiveImpl::RustFn(primitive_textual_locale_compare),
effects: vec![Effect::Pure],
})));
env.define("textual-locale-upcase".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-locale-upcase".to_string(),
arity_min: 2,
arity_max: Some(2),
implementation: PrimitiveImpl::RustFn(primitive_textual_locale_upcase),
effects: vec![Effect::Pure],
})));
env.define("textual-locale-downcase".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-locale-downcase".to_string(),
arity_min: 2,
arity_max: Some(2),
implementation: PrimitiveImpl::RustFn(primitive_textual_locale_downcase),
effects: vec![Effect::Pure],
})));
env.define("textual-locale-titlecase".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-locale-titlecase".to_string(),
arity_min: 2,
arity_max: Some(2),
implementation: PrimitiveImpl::RustFn(primitive_textual_locale_titlecase),
effects: vec![Effect::Pure],
})));
}
fn bind_srfi135_cursor_range(env: &Arc<ThreadSafeEnvironment>) {
env.define("textual-cursor-start".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-cursor-start".to_string(),
arity_min: 1,
arity_max: Some(1),
implementation: PrimitiveImpl::RustFn(primitive_textual_cursor_start),
effects: vec![Effect::Pure],
})));
env.define("textual-cursor-end".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-cursor-end".to_string(),
arity_min: 1,
arity_max: Some(1),
implementation: PrimitiveImpl::RustFn(primitive_textual_cursor_end),
effects: vec![Effect::Pure],
})));
env.define("textual-cursor-next".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-cursor-next".to_string(),
arity_min: 2,
arity_max: Some(2),
implementation: PrimitiveImpl::RustFn(primitive_textual_cursor_next),
effects: vec![Effect::Pure],
})));
env.define("textual-cursor-prev".to_string(), Value::Primitive(Arc::new(PrimitiveProcedure {
name: "textual-cursor-prev".to_string(),
arity_min: 2,
arity_max: Some(2),
implementation: PrimitiveImpl::RustFn(primitive_textual_cursor_prev),
effects: vec![Effect::Pure],
})));
}
fn primitive_textual_empty_p(args: &[Value]) -> Result<Value> {
if args.len() != 1 {
return Err(Box::new(DiagnosticError::runtime_error(
format!("textual-empty? expects 1 argument, got {}", args.len()),
None,
)));
}
let text = Text::try_from(&args[0])?;
Ok(Value::boolean(text.is_empty()))
}
fn primitive_textual_length(args: &[Value]) -> Result<Value> {
if args.len() != 1 {
return Err(Box::new(DiagnosticError::runtime_error(
format!("textual-length expects 1 argument, got {}", args.len()),
None,
)));
}
let text = Text::try_from(&args[0])?;
Ok(Value::integer(text.char_length() as i64))
}
fn primitive_textual_ref(args: &[Value]) -> Result<Value> {
if args.len() != 2 {
return Err(Box::new(DiagnosticError::runtime_error(
format!("textual-ref expects 2 arguments, got {}", args.len()),
None,
)));
}
let text = Text::try_from(&args[0])?;
let index = args[1].as_integer().ok_or_else(|| {
Box::new(DiagnosticError::runtime_error(
"textual-ref index must be an integer".to_string(),
None,
))
})? as usize;
match text.char_at(index) {
Some(ch) => Ok(Value::Literal(crate::ast::Literal::Character(ch))),
None => Err(Box::new(DiagnosticError::runtime_error(
"textual-ref index out of bounds".to_string(),
None,
))),
}
}
fn primitive_textual_p(args: &[Value]) -> Result<Value> {
if args.len() != 1 {
return Err(Box::new(DiagnosticError::runtime_error(
format!("textual? expects 1 argument, got {}", args.len()),
None,
)));
}
let is_textual = matches!(args[0], Value::Literal(crate::ast::Literal::String(_)));
Ok(Value::boolean(is_textual))
}
fn primitive_textual_every(args: &[Value]) -> Result<Value> {
if args.len() < 2 {
return Err(Box::new(DiagnosticError::runtime_error(
"textual-every requires at least 2 arguments".to_string(),
None,
)));
}
Ok(Value::boolean(true))
}
fn primitive_textual_any(args: &[Value]) -> Result<Value> {
if args.len() < 2 {
return Err(Box::new(DiagnosticError::runtime_error(
"textual-any requires at least 2 arguments".to_string(),
None,
)));
}
Ok(Value::boolean(false))
}
fn primitive_textual_take(args: &[Value]) -> Result<Value> {
if args.len() != 2 {
return Err(Box::new(DiagnosticError::runtime_error(
format!("textual-take expects 2 arguments, got {}", args.len()),
None,
)));
}
let text = Text::try_from(&args[0])?;
let n = args[1].as_integer().ok_or_else(|| {
Box::new(DiagnosticError::runtime_error(
"textual-take count must be an integer".to_string(),
None,
))
})? as usize;
match text.substring(0, n.min(text.char_length())) {
Some(result) => Ok(result.into()),
None => Ok(Text::new().into()),
}
}
fn primitive_textual_drop(args: &[Value]) -> Result<Value> {
if args.len() != 2 {
return Err(Box::new(DiagnosticError::runtime_error(
format!("textual-drop expects 2 arguments, got {}", args.len()),
None,
)));
}
let text = Text::try_from(&args[0])?;
let n = args[1].as_integer().ok_or_else(|| {
Box::new(DiagnosticError::runtime_error(
"textual-drop count must be an integer".to_string(),
None,
))
})? as usize;
let start = n.min(text.char_length());
match text.substring(start, text.char_length()) {
Some(result) => Ok(result.into()),
None => Ok(Text::new().into()),
}
}
fn primitive_textual_take_right(args: &[Value]) -> Result<Value> {
if args.len() != 2 {
return Err(Box::new(DiagnosticError::runtime_error(
format!("textual-take-right expects 2 arguments, got {}", args.len()),
None,
)));
}
let text = Text::try_from(&args[0])?;
let n = args[1].as_integer().ok_or_else(|| {
Box::new(DiagnosticError::runtime_error(
"textual-take-right count must be an integer".to_string(),
None,
))
})? as usize;
let len = text.char_length();
let start = len.saturating_sub(n);
match text.substring(start, len) {
Some(result) => Ok(result.into()),
None => Ok(Text::new().into()),
}
}
fn primitive_textual_drop_right(args: &[Value]) -> Result<Value> {
if args.len() != 2 {
return Err(Box::new(DiagnosticError::runtime_error(
format!("textual-drop-right expects 2 arguments, got {}", args.len()),
None,
)));
}
let text = Text::try_from(&args[0])?;
let n = args[1].as_integer().ok_or_else(|| {
Box::new(DiagnosticError::runtime_error(
"textual-drop-right count must be an integer".to_string(),
None,
))
})? as usize;
let len = text.char_length();
let end = len.saturating_sub(n);
match text.substring(0, end) {
Some(result) => Ok(result.into()),
None => Ok(Text::new().into()),
}
}
fn primitive_textual_compare(_args: &[Value]) -> Result<Value> {
Ok(Value::integer(0))
}
fn primitive_textual_compare_ci(_args: &[Value]) -> Result<Value> {
Ok(Value::integer(0))
}
fn primitive_textual_prefix_length(_args: &[Value]) -> Result<Value> {
Ok(Value::integer(0))
}
fn primitive_textual_suffix_length(_args: &[Value]) -> Result<Value> {
Ok(Value::integer(0))
}
fn primitive_textual_prefix_p(_args: &[Value]) -> Result<Value> {
Ok(Value::boolean(false))
}
fn primitive_textual_suffix_p(_args: &[Value]) -> Result<Value> {
Ok(Value::boolean(false))
}
fn primitive_textual_index(_args: &[Value]) -> Result<Value> {
Ok(Value::boolean(false))
}
fn primitive_textual_index_right(_args: &[Value]) -> Result<Value> {
Ok(Value::boolean(false))
}
fn primitive_textual_skip(_args: &[Value]) -> Result<Value> {
Ok(Value::integer(0))
}
fn primitive_textual_skip_right(_args: &[Value]) -> Result<Value> {
Ok(Value::integer(0))
}
fn primitive_textual_contains(_args: &[Value]) -> Result<Value> {
Ok(Value::boolean(false))
}
fn primitive_textual_upcase(args: &[Value]) -> Result<Value> {
if args.is_empty() {
return Err(Box::new(DiagnosticError::runtime_error(
"textual-upcase requires at least 1 argument".to_string(),
None,
)));
}
let text = Text::try_from(&args[0])?;
Ok(text.to_uppercase().into())
}
fn primitive_textual_downcase(args: &[Value]) -> Result<Value> {
if args.is_empty() {
return Err(Box::new(DiagnosticError::runtime_error(
"textual-downcase requires at least 1 argument".to_string(),
None,
)));
}
let text = Text::try_from(&args[0])?;
Ok(text.to_lowercase().into())
}
fn primitive_textual_foldcase(args: &[Value]) -> Result<Value> {
if args.is_empty() {
return Err(Box::new(DiagnosticError::runtime_error(
"textual-foldcase requires at least 1 argument".to_string(),
None,
)));
}
let text = Text::try_from(&args[0])?;
Ok(text.fold_case().into())
}
fn primitive_textual_titlecase(args: &[Value]) -> Result<Value> {
if args.is_empty() {
return Err(Box::new(DiagnosticError::runtime_error(
"textual-titlecase requires at least 1 argument".to_string(),
None,
)));
}
let text = Text::try_from(&args[0])?;
Ok(text.to_titlecase().into())
}
fn primitive_textual_reverse(args: &[Value]) -> Result<Value> {
if args.is_empty() {
return Err(Box::new(DiagnosticError::runtime_error(
"textual-reverse requires at least 1 argument".to_string(),
None,
)));
}
let text = Text::try_from(&args[0])?;
Ok(text.reverse().into())
}
fn primitive_textual_replace(_args: &[Value]) -> Result<Value> {
Ok(Text::new().into())
}
fn primitive_textual_split(_args: &[Value]) -> Result<Value> {
Ok(Value::list(vec![]))
}
fn primitive_textual_concatenate(args: &[Value]) -> Result<Value> {
if args.len() != 1 {
return Err(Box::new(DiagnosticError::runtime_error(
format!("textual-concatenate expects 1 argument, got {}", args.len()),
None,
)));
}
let text_list = args[0].as_list().ok_or_else(|| {
Box::new(DiagnosticError::runtime_error(
"textual-concatenate argument must be a list".to_string(),
None,
))
})?;
let mut builder = TextBuilder::new();
for item in text_list {
let text = Text::try_from(&item)?;
builder.push_text(&text);
}
Ok(builder.build().into())
}
fn primitive_textual_concatenate_reverse(_args: &[Value]) -> Result<Value> {
Ok(Text::new().into())
}
fn primitive_textual_locale_compare(_args: &[Value]) -> Result<Value> {
Ok(Value::integer(0))
}
fn primitive_textual_locale_upcase(_args: &[Value]) -> Result<Value> {
Ok(Text::new().into())
}
fn primitive_textual_locale_downcase(_args: &[Value]) -> Result<Value> {
Ok(Text::new().into())
}
fn primitive_textual_locale_titlecase(_args: &[Value]) -> Result<Value> {
Ok(Text::new().into())
}
fn primitive_textual_cursor_start(_args: &[Value]) -> Result<Value> {
Ok(Value::integer(0))
}
fn primitive_textual_cursor_end(_args: &[Value]) -> Result<Value> {
Ok(Value::integer(0))
}
fn primitive_textual_cursor_next(_args: &[Value]) -> Result<Value> {
Ok(Value::integer(0))
}
fn primitive_textual_cursor_prev(_args: &[Value]) -> Result<Value> {
Ok(Value::integer(0))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_locale_creation() {
let locale = TextLocale::new("en", Some("US")).unwrap();
assert_eq!(locale.language, "en");
assert_eq!(locale.country, Some("US".to_string()));
}
#[test]
fn test_text_cursor() {
let text = Text::from_string_slice("hello world");
let mut cursor = TextCursor::new(text, BoundaryType::Character);
assert_eq!(cursor.position(), 0);
assert!(cursor.advance());
assert_eq!(cursor.position(), 1);
assert!(cursor.previous());
assert_eq!(cursor.position(), 0);
}
#[test]
fn test_text_range() {
let range = TextRange::new(2, 5, BoundaryType::Character);
assert_eq!(range.start(), 2);
assert_eq!(range.end(), 5);
assert_eq!(range.length(), 3);
assert!(!range.is_empty());
}
#[test]
fn test_textual_take_drop() {
let hello = Text::from_string_slice("hello");
let take_args = vec![hello.clone().into(), Value::integer(3)];
let result = primitive_textual_take(&take_args).unwrap();
let result_text = Text::try_from(&result).unwrap();
assert_eq!(result_text.to_string(), "hel");
let drop_args = vec![hello.into(), Value::integer(2)];
let result = primitive_textual_drop(&drop_args).unwrap();
let result_text = Text::try_from(&result).unwrap();
assert_eq!(result_text.to_string(), "llo");
}
}