Skip to content

Commit

Permalink
partial text
Browse files Browse the repository at this point in the history
Co-authored-by: Yjn024 <[email protected]>
  • Loading branch information
0123456789-jpg and JieningYu committed Nov 16, 2023
1 parent 08d188a commit fb1986f
Show file tree
Hide file tree
Showing 5 changed files with 537 additions and 73 deletions.
255 changes: 217 additions & 38 deletions core/src/text/content.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
use parking_lot::RwLock;
use lazy_regex::Captures;
use serde::Serialize;

use std::{
any::Any,
borrow::Cow,
sync::{Arc, Weak},
};
use std::{borrow::Cow, fmt::Debug, sync::Arc};

use crate::lang::{DebugLang, LangDepended, LangExt, UpdateLang};

Expand All @@ -14,21 +9,19 @@ use super::{
Style, Text,
};

#[deprecated]
pub trait ContentDeprecated: Visit<()> + StyledVisit<Style> {}

#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Default)]
pub enum Content {
#[default]
Empty,
Literal(Cow<'static, str>),
Translatable(()),
Translatable(Translatable),
}

impl<T> Visit<T> for Content {
fn visit<V: super::visit::Visitor<T> + ?Sized>(&self, visitor: &mut V) -> Option<T> {
match self {
Content::Literal(value) => visitor.accept(value),
Content::Translatable(_) => todo!(),
Content::Translatable(value) => value.visit(visitor),
_ => None,
}
}
Expand All @@ -42,7 +35,7 @@ impl<T> StyledVisit<T> for Content {
) -> Option<T> {
match self {
Content::Literal(value) => visitor.accept(style, value),
Content::Translatable(_) => todo!(),
Content::Translatable(value) => value.styled_visit(visitor, style),
_ => None,
}
}
Expand All @@ -54,20 +47,100 @@ pub enum TranslationError {
Parse(Cow<'static, str>, TranslateParseError),
#[error("invalid index {1} requested for {0}")]
Index(Cow<'static, str>, usize),
#[error("illegal argument: {0}")]
IllegalArg(String),
}

#[derive(Debug, thiserror::Error)]
pub enum TranslateParseError {
#[error("unsupported format: {0}")]
UnsupportedFormat(String),
#[error("error parsing integer: {0}")]
ParseInt(std::num::ParseIntError),
}

#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Translatable {
inner: LangDepended<TranslatableInner>,
cx: TranslatableContext,
parts: LangDepended<TranslatableParts>,
}

impl Translatable {
pub fn new(
key: Cow<'static, str>,
fallback: Option<Cow<'static, str>>,
args: Vec<TranslatableArg>,
) -> Self {
Self {
cx: TranslatableContext {
key,
fallback,
args,
},
parts: LangDepended::new(TranslatableParts { parts: vec![] }),
}
}

#[inline]
pub fn fallback(&self) -> Option<&str> {
self.cx.fallback.as_deref()
}

#[inline]
pub fn key(&self) -> &str {
&self.cx.key
}

#[inline]
pub fn args(&self) -> &[TranslatableArg] {
&self.cx.args
}
}

#[derive(Debug, Clone)]
struct TranslatableContext {
key: Cow<'static, str>,
fallback: Option<Cow<'static, str>>,
args: Vec<TranslatableArg>,
}

impl TranslatableContext {
fn arg(&self, index: usize) -> Option<VisitAbst> {
self.args.get(index).map(|e| match e {
TranslatableArg::Text(text) => VisitAbst::Text(text.clone()),
TranslatableArg::Display(disp) => {
VisitAbst::Plain(visit::plain(Cow::Owned(disp.to_string())))
}
TranslatableArg::Debug(deb) => {
VisitAbst::Plain(visit::plain(Cow::Owned(format!("{deb:?}"))))
}
})
}
}

impl<T> visit::Visit<T> for Translatable {
fn visit<V: visit::Visitor<T> + ?Sized>(&self, visitor: &mut V) -> Option<T> {
self.parts
.get(&self.cx)
.parts
.iter()
.find_map(|val| visit::Visit::visit(val, visitor))
}
}

impl Translatable {}
impl<T> visit::StyledVisit<T> for Translatable {
fn styled_visit<V: visit::StyleVisitor<T> + ?Sized>(
&self,
visitor: &mut V,
style: &Style,
) -> Option<T> {
self.parts
.get(&self.cx)
.parts
.iter()
.find_map(|val| visit::StyledVisit::styled_visit(val, visitor, style))
}
}

#[derive(Debug, Clone)]
enum VisitAbst {
Expand Down Expand Up @@ -100,43 +173,149 @@ impl<T> visit::StyledVisit<T> for VisitAbst {
}
}

#[derive(Clone)]
pub enum TranslatableArg {
Text(Text),
Display(Arc<dyn std::fmt::Display + Send + Sync>),
Debug(Arc<dyn std::fmt::Debug + Send + Sync>),
}

impl Debug for TranslatableArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Text(arg0) => f.debug_tuple("Text").field(arg0).finish(),
Self::Display(arg0) => f.debug_tuple("Display").field(&arg0.to_string()).finish(),
Self::Debug(arg0) => f.debug_tuple("Debug").field(arg0).finish(),
}
}
}

impl From<Text> for TranslatableArg {
#[inline]
fn from(text: Text) -> Self {
if text.style.is_empty() && text.sibs.is_empty() {
if let Content::Literal(ref lit) = text.content {
return TranslatableArg::Display(Arc::new(lit.clone()));
}
}
TranslatableArg::Text(text)
}
}

impl Serialize for TranslatableArg {
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Text(arg0) => arg0.serialize(serializer),
Self::Display(arg0) => serializer.serialize_str(&arg0.to_string()),
Self::Debug(arg0) => serializer.serialize_str(&format!("{:?}", arg0)),
}
}
}

#[derive(Debug, Clone)]
struct TranslatableInner {
struct TranslatableParts {
parts: Vec<VisitAbst>,
key: Cow<'static, String>,
fallback: Option<Cow<'static, str>>,

args: Vec<Arc<dyn Any + Sync + Send>>,
}

static T_ARG_FMT: lazy_regex::Lazy<lazy_regex::Regex> =
lazy_regex::lazy_regex!("%(?:(\\d+)\\$)?([A-Za-z%]|$)");

impl TranslatableInner {
fn update_parts(&mut self, translation: &str) -> Result<(), TranslationError> {
impl TranslatableParts {
fn update_parts(
&mut self,
translation: &str,
cx: &TranslatableContext,
) -> Result<(), TranslationError> {
macro_rules! validate_arg {
($a:ident) => {
if $a.find('%').is_some() {
return Err(TranslationError::IllegalArg($a.to_string()));
}
};
}

let mut last_end = 0;

for (g,m) in T_ARG_FMT.captures_iter(translation).map(|e|e.extract()) {
let mut i = 0;

for (g, m) in T_ARG_FMT
.captures_iter(translation)
.zip(T_ARG_FMT.find_iter(translation))
{
if last_end != m.start() {
self.parts.push(VisitAbst::Plain(visit::plain(Cow::Owned(
translation[last_end..m.start()].to_owned(),
))));
let str = &translation[last_end..m.start()];
validate_arg!(str);
self.parts
.push(VisitAbst::Plain(visit::plain(Cow::Owned(str.to_owned()))));
}
self.parts.push(VisitAbst::Plain(visit::plain(Cow::Owned(
m.as_str().to_owned(),
))));

match (g.get(2).map(|e| e.as_str()).unwrap_or_default(), m.as_str()) {
("%", "%%") => {
const LIT_PERC_SIGN: visit::Plain<'static> = visit::plain(Cow::Borrowed("%"));
self.parts.push(VisitAbst::Plain(LIT_PERC_SIGN));
}
("s", _) => {
if let Some(arg) = cx.arg(
g.get(1)
.map_or_else(
|| {
let ii = i;
i += 1;
Ok(ii)
},
|e| e.as_str().parse().map(|ee: usize| ee - 1),
)
.map_err(|err| {
TranslationError::Parse(
cx.key.clone(),
TranslateParseError::ParseInt(err),
)
})?,
) {
self.parts.push(arg);
}
}
(_, fmt) => {
return Err(TranslationError::Parse(
cx.key.clone(),
TranslateParseError::UnsupportedFormat(fmt.to_owned()),
))
}
}

if last_end < translation.len() {
let str = &translation[last_end..];
validate_arg!(str);
self.parts
.push(VisitAbst::Plain(visit::plain(Cow::Owned(str.to_owned()))));
}

last_end = m.end();
}
Ok(())
}
}

impl UpdateLang for TranslatableInner {
fn update_from_lang(&mut self, lang: &dyn DebugLang) {
let translation = self.fallback.as_ref().map_or_else(
|| lang.translation_or_key(&self.key),
|fallback| lang.translation(&self.key).unwrap_or(&fallback),
);
impl UpdateLang for TranslatableParts {
type Context = TranslatableContext;

fn update_from_lang(&mut self, lang: &dyn DebugLang, cx: &Self::Context) {
let translation = cx
.fallback
.as_ref()
.map_or_else(
|| lang.translation_or_key(&cx.key),
|fallback| lang.translation(&cx.key).unwrap_or(&fallback),
)
.to_owned();
self.parts.clear();

if self.update_parts(&translation, cx).is_err() {
self.parts
.push(VisitAbst::Plain(visit::plain(Cow::Owned(translation))));
}
}
}
Loading

0 comments on commit fb1986f

Please sign in to comment.