diff --git a/src/library/text/lang.rs b/src/library/text/lang.rs new file mode 100644 index 000000000..343359d1e --- /dev/null +++ b/src/library/text/lang.rs @@ -0,0 +1,39 @@ +use crate::eval::Value; +use crate::geom::Dir; + +/// A natural language. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Lang([u8; 2]); + +impl Lang { + /// The code for the english language. + pub const ENGLISH: Self = Self(*b"en"); + + /// Construct a language from a two-byte ISO 639-1 code. + pub fn from_str(iso: &str) -> Option { + let mut bytes: [u8; 2] = iso.as_bytes().try_into().ok()?; + bytes.make_ascii_lowercase(); + Some(Self(bytes)) + } + + /// Return the language code as a string slice. + pub fn as_str(&self) -> &str { + std::str::from_utf8(&self.0).unwrap_or_default() + } + + /// The default direction for the language. + pub fn dir(&self) -> Dir { + match self.as_str() { + "ar" | "dv" | "fa" | "he" | "ks" | "pa" | "ps" | "sd" | "ug" | "ur" + | "yi" => Dir::RTL, + _ => Dir::LTR, + } + } +} + +castable! { + Lang, + Expected: "string", + Value::Str(string) => Self::from_str(&string) + .ok_or("expected two letter language code")?, +} diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs index a25b28275..636b878c8 100644 --- a/src/library/text/mod.rs +++ b/src/library/text/mod.rs @@ -1,6 +1,7 @@ //! Text handling and paragraph layout. mod deco; +mod lang; mod link; mod par; mod quotes; @@ -8,6 +9,7 @@ mod raw; mod shaping; pub use deco::*; +pub use lang::*; pub use link::*; pub use par::*; pub use quotes::*; @@ -64,8 +66,7 @@ impl TextNode { pub const BOTTOM_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::Baseline); /// An ISO 639-1 language code. - #[property(referenced)] - pub const LANG: Option = None; + pub const LANG: Lang = Lang::ENGLISH; /// The direction for text and inline objects. When `auto`, the direction is /// automatically inferred from the language. #[property(resolve)] @@ -257,32 +258,6 @@ castable! { }), } -/// A natural language. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Lang(EcoString); - -impl Lang { - /// The default direction for the language. - pub fn dir(&self) -> Dir { - match self.0.as_str() { - "ar" | "dv" | "fa" | "he" | "ks" | "pa" | "ps" | "sd" | "ug" | "ur" - | "yi" => Dir::RTL, - _ => Dir::LTR, - } - } - - /// Return the language code as a string slice. - pub fn as_str(&self) -> &str { - &self.0 - } -} - -castable! { - Lang, - Expected: "string", - Value::Str(string) => Self(string.to_lowercase()), -} - /// The direction of text and inline objects in their line. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct HorizontalDir(pub Dir); @@ -301,10 +276,7 @@ impl Resolve for Smart { fn resolve(self, styles: StyleChain) -> Self::Output { match self { - Smart::Auto => match styles.get(TextNode::LANG) { - Some(lang) => lang.dir(), - None => Dir::LTR, - }, + Smart::Auto => styles.get(TextNode::LANG).dir(), Smart::Custom(dir) => dir.0, } } diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 19ab10824..232a5d0f2 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -408,11 +408,7 @@ fn collect<'a>( if styles.get(TextNode::SMART_QUOTES) { // TODO: Also get region. let lang = styles.get(TextNode::LANG); - let quotes = lang - .as_ref() - .map(|lang| Quotes::from_lang(lang.as_str(), "")) - .unwrap_or_default(); - + let quotes = Quotes::from_lang(lang.as_str(), ""); let peeked = iter.peek().and_then(|(child, _)| match child { ParChild::Text(text) => text.chars().next(), ParChild::Quote(_) => Some('"'), @@ -750,7 +746,7 @@ fn breakpoints<'a>(p: &'a Preparation) -> Breakpoints<'a> { end: 0, mandatory: false, hyphenate: p.get_shared(TextNode::HYPHENATE), - lang: p.get_shared(TextNode::LANG).map(Option::as_ref), + lang: p.get_shared(TextNode::LANG), } } @@ -773,7 +769,7 @@ struct Breakpoints<'a> { /// Whether to hyphenate if it's the same for all children. hyphenate: Option, /// The text language if it's the same for all children. - lang: Option>, + lang: Option, } impl Iterator for Breakpoints<'_> { @@ -831,9 +827,9 @@ impl Breakpoints<'_> { /// The text language at the given offset. fn lang_at(&self, offset: usize) -> Option { - let lang = self.lang.unwrap_or_else(|| { + let lang = self.lang.or_else(|| { let shaped = self.p.find(offset)?.text()?; - shaped.styles.get(TextNode::LANG).as_ref() + Some(shaped.styles.get(TextNode::LANG)) })?; let bytes = lang.as_str().as_bytes().try_into().ok()?; diff --git a/tests/ref/text/linebreak.png b/tests/ref/text/linebreak.png index 43ac9c68a..4a691d72a 100644 Binary files a/tests/ref/text/linebreak.png and b/tests/ref/text/linebreak.png differ diff --git a/tests/typ/text/hyphenate.typ b/tests/typ/text/hyphenate.typ index 6bb87b13b..4dc5255f4 100644 --- a/tests/typ/text/hyphenate.typ +++ b/tests/typ/text/hyphenate.typ @@ -6,13 +6,13 @@ #set page(width: auto) #grid( columns: (70pt, 60pt), - text(lang: "en")[Warm welcomes to Typst.], + [Warm welcomes to Typst.], text(lang: "el")[διαμερίσματα. \ λατρευτός], ) --- // Test disabling hyphenation for short passages. -#set text(lang: "en", hyphenate: true) +#set text(hyphenate: true) Welcome to wonderful experiences. \ Welcome to `wonderful` experiences. \ @@ -20,14 +20,14 @@ Welcome to #text(hyphenate: false)[wonderful] experiences. \ Welcome to wonde#text(hyphenate: false)[rf]ul experiences. \ // Test enabling hyphenation for short passages. -#set text(lang: "en", hyphenate: false) +#set text(hyphenate: false) Welcome to wonderful experiences. \ Welcome to wo#text(hyphenate: true)[nd]erful experiences. \ --- // Hyphenate between shape runs. #set page(width: 80pt) -#set text(lang: "en", hyphenate: true) +#set text(hyphenate: true) It's a #emph[Tree]beard. --- @@ -46,5 +46,5 @@ It's a #emph[Tree]beard. // do that. The test passes if there's just one hyphenation between // "net" and "works". #set page(width: 70pt) -#set text(lang: "en", hyphenate: true) +#set text(hyphenate: true) #h(6pt) networks, the rest. diff --git a/tests/typ/text/justify.typ b/tests/typ/text/justify.typ index 3659f8efb..0cdef0000 100644 --- a/tests/typ/text/justify.typ +++ b/tests/typ/text/justify.typ @@ -1,7 +1,6 @@ --- #set page(width: 180pt) -#set text(lang: "en") #set par( justify: true, indent: 14pt, diff --git a/tests/typ/text/knuth.typ b/tests/typ/text/knuth.typ index 33249ef49..5adeee91a 100644 --- a/tests/typ/text/knuth.typ +++ b/tests/typ/text/knuth.typ @@ -1,6 +1,6 @@ #set page(width: auto, height: auto) #set par(leading: 4pt, justify: true) -#set text(lang: "en", family: "Latin Modern Roman") +#set text(family: "Latin Modern Roman") #let story = [ In olden times when wishing still helped one, there lived a king whose diff --git a/tests/typ/text/microtype.typ b/tests/typ/text/microtype.typ index d3b47f6dc..991e0d0dc 100644 --- a/tests/typ/text/microtype.typ +++ b/tests/typ/text/microtype.typ @@ -4,7 +4,7 @@ // Test hanging punctuation. #set page(width: 130pt, margins: 15pt) #set par(justify: true, linebreaks: "simple") -#set text(lang: "en", size: 9pt) +#set text(size: 9pt) #rect(fill: rgb(repr(teal) + "00"), width: 100%)[ This is a little bit of text that builds up to hang-ing hyphens and dash---es and then, you know, diff --git a/tests/typ/text/quotes.typ b/tests/typ/text/quotes.typ index 3f0649e8a..b447258fc 100644 --- a/tests/typ/text/quotes.typ +++ b/tests/typ/text/quotes.typ @@ -24,12 +24,10 @@ --- // Test single pair of quotes. -#set text(lang: "en") "" --- // Test sentences with numbers and apostrophes. -#set text(lang: "en") The 5'11" 'quick' brown fox jumps over the "lazy" dog's ear. He said "I'm a big fella." @@ -40,7 +38,6 @@ The 5\'11\" 'quick\' brown fox jumps over the \"lazy" dog\'s ear. --- // Test turning smart quotes off. -#set text(lang: "en") He's told some books contain questionable "example text". #set text(smart-quotes: false) @@ -48,7 +45,6 @@ He's told some books contain questionable "example text". --- // Test changing properties within text. -#set text(lang: "en") "She suddenly started speaking french: #text(lang: "fr")['Je suis une banane.']" Roman told me. Some people's thought on this would be #text(smart-quotes: false)["strange."]