diff --git a/shaperglot-lib/src/checker.rs b/shaperglot-lib/src/checker.rs index 8686fb8..ff68710 100644 --- a/shaperglot-lib/src/checker.rs +++ b/shaperglot-lib/src/checker.rs @@ -9,17 +9,25 @@ use crate::{ use rustybuzz::Face; use skrifa::{raw::ReadError, FontRef, GlyphId, MetadataProvider}; +/// The context for running font language support checks pub struct Checker<'a> { + /// The font to check, as a [read_fonts::FontRef] pub font: FontRef<'a>, + /// The face to use for shaping pub face: Face<'a>, + /// The glyph names in the font pub glyph_names: Vec, + /// The OpenType features present in the font pub features: HashSet, + /// The character map of the font pub cmap: BTreeMap, + /// The reversed character map of the font reversed_cmap: BTreeMap, // full_reversed_cmap: Arc>>>, } impl<'a> Checker<'a> { + /// Create a new font checker pub fn new(font_binary: &'a [u8]) -> Result { let face = Face::from_slice(font_binary, 0).expect("Couldn't load font"); let font = FontRef::new(font_binary)?; @@ -38,6 +46,7 @@ impl<'a> Checker<'a> { }) } + /// Get the codepoint for a given glyph ID pub fn codepoint_for(&self, gid: GlyphId) -> Option { // self.reversed_cmap.get(&gid).copied().or_else(|| // if !self.full_reversed_cmap.is_some() { @@ -47,6 +56,7 @@ impl<'a> Checker<'a> { self.reversed_cmap.get(&gid).copied() } + /// Check a font for language support pub fn check(&self, language: &Language) -> Reporter { let mut results = Reporter::default(); for check_object in language.checks.iter() { diff --git a/shaperglot-lib/src/checks/codepoint_coverage.rs b/shaperglot-lib/src/checks/codepoint_coverage.rs index bd83434..8f0f733 100644 --- a/shaperglot-lib/src/checks/codepoint_coverage.rs +++ b/shaperglot-lib/src/checks/codepoint_coverage.rs @@ -10,8 +10,11 @@ use serde_json::json; use std::collections::HashSet; #[derive(Serialize, Deserialize, Debug, Clone)] +/// A check implementation which ensures codepoints are present in a font pub struct CodepointCoverage { + /// The codepoints to check for strings: HashSet, + /// The unique code to return on failure (e.g. "marks-missing") code: String, } @@ -72,6 +75,7 @@ impl CheckImplementation for CodepointCoverage { } impl CodepointCoverage { + /// Create a new `CodepointCoverage` check implementation pub fn new(test_strings: Vec, code: String) -> Self { Self { strings: test_strings.into_iter().collect(), diff --git a/shaperglot-lib/src/checks/mod.rs b/shaperglot-lib/src/checks/mod.rs index f83384f..b8857e0 100644 --- a/shaperglot-lib/src/checks/mod.rs +++ b/shaperglot-lib/src/checks/mod.rs @@ -1,5 +1,8 @@ +/// A check implementation which ensures codepoints are present in a font mod codepoint_coverage; +/// A check implementation which ensures marks are anchors to their respective base characters mod no_orphaned_marks; +/// A check implementation which ensures that two shaping inputs produce different outputs mod shaping_differs; use crate::{ @@ -14,39 +17,63 @@ use serde::{Deserialize, Serialize}; pub use shaping_differs::ShapingDiffers; #[delegatable_trait] +/// A check implementation +/// +/// This is a sub-unit of a [Check]; a Check is made up of multiple +/// `CheckImplementations`. For example, an orthography check will +/// first check bases, then marks, then auxiliary codepoints. pub trait CheckImplementation { + /// The name of the check implementation fn name(&self) -> String; + /// A description of the check implementation fn describe(&self) -> String; + /// Whether the subcheck should be skipped for this font fn should_skip(&self, checker: &Checker) -> Option; + /// Execute the check implementation and return problems found fn execute(&self, checker: &Checker) -> (Vec, usize); } #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +/// The scoring strategy for a check pub enum ScoringStrategy { + /// A continuous score; the score is the proportion of checks that pass Continuous, + /// An all-or-nothing score; the score is 1 if all checks pass, 0 otherwise AllOrNothing, } #[derive(Delegate, Serialize, Deserialize, Debug, Clone)] #[delegate(CheckImplementation)] #[serde(tag = "type")] +/// Check implementations available to higher-level checks pub enum CheckType { + /// A check implementation which ensures codepoints are present in a font CodepointCoverage(CodepointCoverage), + /// A check implementation which ensures marks are anchors to their respective base characters NoOrphanedMarks(NoOrphanedMarks), + /// A check implementation which ensures that two shaping inputs produce different outputs ShapingDiffers(ShapingDiffers), } #[derive(Serialize, Deserialize, Debug, Clone)] +/// A check to be executed pub struct Check { + /// The name of the check pub name: String, + /// The severity of the check in terms of how it affects language support pub severity: ResultCode, + /// A description of the check pub description: String, + /// The scoring strategy for the check pub scoring_strategy: ScoringStrategy, + /// The weight of the check pub weight: u8, + /// Individual implementations to be run pub implementations: Vec, } impl Check { + /// Execute the check and return the results pub fn execute(&self, checker: &Checker) -> CheckResult { let mut problems = Vec::new(); let mut total_checks = 0; diff --git a/shaperglot-lib/src/checks/no_orphaned_marks.rs b/shaperglot-lib/src/checks/no_orphaned_marks.rs index 4b3a853..48906da 100644 --- a/shaperglot-lib/src/checks/no_orphaned_marks.rs +++ b/shaperglot-lib/src/checks/no_orphaned_marks.rs @@ -9,8 +9,14 @@ use serde::{Deserialize, Serialize}; use unicode_properties::{GeneralCategory, UnicodeGeneralCategory}; #[derive(Serialize, Deserialize, Debug, Clone)] +/// A check implementation which ensures marks are anchors to their respective base characters pub struct NoOrphanedMarks { + /// The strings to shape and check test_strings: Vec, + /// Whether the language has orthography data + /// + /// If this is true, we will not report notdefs, as the orthography check will + /// catch them. has_orthography: bool, } @@ -130,6 +136,7 @@ impl CheckImplementation for NoOrphanedMarks { } } +/// Check if a codepoint is a nonspacing mark fn simple_mark_check(c: u32) -> bool { char::from_u32(c) .map(|c| matches!(c.general_category(), GeneralCategory::NonspacingMark)) @@ -137,6 +144,7 @@ fn simple_mark_check(c: u32) -> bool { } impl NoOrphanedMarks { + /// Create a new `NoOrphanedMarks` check implementation pub fn new(test_strings: Vec, has_orthography: bool) -> Self { Self { test_strings, diff --git a/shaperglot-lib/src/checks/shaping_differs.rs b/shaperglot-lib/src/checks/shaping_differs.rs index cb427ce..a72fa9d 100644 --- a/shaperglot-lib/src/checks/shaping_differs.rs +++ b/shaperglot-lib/src/checks/shaping_differs.rs @@ -11,8 +11,14 @@ use rustybuzz::SerializeFlags; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone)] +/// A check implementation which ensures that two shaping inputs produce different outputs pub struct ShapingDiffers { + /// The pairs of strings to shape and compare pairs: Vec<(ShapingInput, ShapingInput)>, + /// Whether the features are optional + /// + /// If this is true, the check will only run if the font contains the requested feature; + /// otherwise it will be skiped. If it is false, the check will always run. features_optional: bool, } @@ -94,6 +100,7 @@ impl CheckImplementation for ShapingDiffers { } impl ShapingDiffers { + /// Create a new `ShapingDiffers` check implementation pub fn new(pairs: Vec<(ShapingInput, ShapingInput)>, features_optional: bool) -> Self { Self { pairs, diff --git a/shaperglot-lib/src/font.rs b/shaperglot-lib/src/font.rs index 0a8882b..a2d5473 100644 --- a/shaperglot-lib/src/font.rs +++ b/shaperglot-lib/src/font.rs @@ -9,6 +9,7 @@ use skrifa::{ FontRef, }; +/// Get a list of glyph names for a font pub(crate) fn glyph_names(font: &FontRef) -> Result, ReadError> { #[allow(clippy::unwrap_used)] // Heck, Skrifa does the same let glyph_count = font.maxp().unwrap().num_glyphs().into(); @@ -54,6 +55,7 @@ pub(crate) fn glyph_names(font: &FontRef) -> Result, ReadError> { Ok(names) } +/// Get a list of feature tags present in a font pub(crate) fn feature_tags(font: &FontRef) -> Result, ReadError> { let mut tags = HashSet::new(); if let Some(gsub_featurelist) = font.gsub().ok().and_then(|gsub| gsub.feature_list().ok()) { diff --git a/shaperglot-lib/src/language.rs b/shaperglot-lib/src/language.rs index 05bc2f2..543fdca 100644 --- a/shaperglot-lib/src/language.rs +++ b/shaperglot-lib/src/language.rs @@ -1,49 +1,49 @@ -use google_fonts_languages::{LanguageProto, LANGUAGES, SCRIPTS}; +use google_fonts_languages::{LanguageProto, LANGUAGES}; use unicode_normalization::UnicodeNormalization; use crate::{ checks::Check, providers::{BaseCheckProvider, Provider}, }; + +/// A language definition, including checks and exemplar characters pub struct Language { + /// The underlying language definition from the google-fonts-languages database pub proto: Box, + /// The checks that apply to this language pub checks: Vec, + /// Mandatory base characters for the language pub bases: Vec, + /// Optional auxiliary characters for the language pub auxiliaries: Vec, + /// Mandatory mark characters for the language pub marks: Vec, } impl Language { + /// The language's ISO 639-3 code pub fn id(&self) -> &str { self.proto.id() } + /// The language's name pub fn name(&self) -> &str { self.proto.name() } - pub fn full_name(&self) -> String { - format!( - "{} in the {} script", - self.proto.name(), - SCRIPTS - .get(self.proto.script()) - .map(|s| s.name()) - .unwrap_or("Unknown") - ) - } - + /// The language's ISO15924 script code pub fn script(&self) -> &str { self.proto.script() } - pub fn language(&self) -> &str { - self.proto.language() - } } +/// The language database pub struct Languages(Vec); impl Languages { + /// Instantiate a new language database + /// + /// This loads the database and fills it with checks. pub fn new() -> Self { let mut languages = Vec::new(); for (_id, proto) in LANGUAGES.iter() { @@ -85,10 +85,12 @@ impl Languages { Languages(languages) } + /// Get an iterator over the languages pub fn iter(&self) -> std::slice::Iter { self.0.iter() } + /// Get a single language by ID or name pub fn get_language(&self, id: &str) -> Option<&Language> { self.0 .iter() @@ -103,6 +105,7 @@ impl Default for Languages { } } +/// Split up an exemplars string into individual characters fn parse_chars(chars: &str) -> Vec { chars .split_whitespace() diff --git a/shaperglot-lib/src/lib.rs b/shaperglot-lib/src/lib.rs index 16ad237..493a7e3 100644 --- a/shaperglot-lib/src/lib.rs +++ b/shaperglot-lib/src/lib.rs @@ -1,11 +1,27 @@ -// #![deny(missing_docs)] -// #![deny(clippy::missing_docs_in_private_items)] +#![deny(missing_docs)] +#![deny(clippy::missing_docs_in_private_items)] +//! Shaperglot is a library for checking a font's language support. +//! +//! Unlike other language coverage tools, shaperglot is based on the idea +//! that the font must not simply cover Unicode codepoints to support a +//! language but must also behave in certain ways. Shaperglot does not +//! dictate particular implementations of language support, in terms of +//! what glyphs or rules are present in the font or how glyphs should be named, +//! but tests a font for its behaviour. + +/// The checker object, representing the context of a check mod checker; +/// Low-level checks and their implementations mod checks; +/// Utility functions to extract information from a font mod font; +/// Structures and routines relating to the language database mod language; +/// Providers turn a language definition into a set of checks mod providers; +/// The reporter object, representing the results of a language test mod reporter; +/// Utility functions for text shaping mod shaping; pub use crate::{ diff --git a/shaperglot-lib/src/providers/mod.rs b/shaperglot-lib/src/providers/mod.rs index 275fbfe..8053811 100644 --- a/shaperglot-lib/src/providers/mod.rs +++ b/shaperglot-lib/src/providers/mod.rs @@ -1,8 +1,12 @@ use crate::{checks::Check, language::Language}; +/// Orthographic checks provider mod orthographies; +/// Arabic positional forms checks provider mod positional; +/// Latin small caps checks provider mod small_caps; +/// Manually-coded checks provider mod toml; use orthographies::OrthographiesProvider; @@ -10,11 +14,15 @@ use positional::PositionalProvider; use small_caps::SmallCapsProvider; use toml::TomlProvider; +/// A provider of checks for a language pub trait Provider { + /// Given a language, return a list of checks that apply to it fn checks_for(&self, language: &Language) -> Vec; } /// The base check provider provides all checks for a language +/// +/// It calls all other known providers to get their checks. pub struct BaseCheckProvider; impl Provider for BaseCheckProvider { diff --git a/shaperglot-lib/src/providers/orthographies.rs b/shaperglot-lib/src/providers/orthographies.rs index d2edca0..0f4b8b0 100644 --- a/shaperglot-lib/src/providers/orthographies.rs +++ b/shaperglot-lib/src/providers/orthographies.rs @@ -7,11 +7,16 @@ use crate::{ use itertools::Itertools; use unicode_properties::{GeneralCategoryGroup, UnicodeGeneralCategory}; +/// Check if a base character (in NFC) contains a mark fn has_complex_decomposed_base(base: &str) -> bool { base.chars() .any(|c| c.general_category_group() == GeneralCategoryGroup::Mark) } +/// Check that the font covers the basic codepoints for the language's orthography +/// +/// This check is mandatory for all languages. Base and mark codepoints are required, +/// and auxiliary codepoints are optional. pub struct OrthographiesProvider; impl Provider for OrthographiesProvider { @@ -29,7 +34,7 @@ impl Provider for OrthographiesProvider { } } -// Orthography check. We MUST have all bases and marks. +/// Orthography check. We MUST have all bases and marks. fn mandatory_orthography(language: &Language) -> Check { let mut mandatory_orthography = Check { name: "Mandatory orthography codepoints".to_string(), @@ -83,7 +88,7 @@ fn mandatory_orthography(language: &Language) -> Check { mandatory_orthography } -// We SHOULD have auxiliaries +/// We SHOULD have auxiliaries fn auxiliaries_check(language: &Language) -> Option { if language.auxiliaries.is_empty() { return None; diff --git a/shaperglot-lib/src/providers/positional.rs b/shaperglot-lib/src/providers/positional.rs index 75ccf4f..f32e3ed 100644 --- a/shaperglot-lib/src/providers/positional.rs +++ b/shaperglot-lib/src/providers/positional.rs @@ -8,6 +8,7 @@ use crate::{ Provider, ResultCode, }; +/// Zero Width Joiner const ZWJ: &str = "\u{200D}"; // const MARKS_FOR_LANG: [(&str, &str); 1] = [( @@ -15,6 +16,11 @@ const ZWJ: &str = "\u{200D}"; // "\u{064E}\u{0651} \u{064B}\u{0651} \u{0650}\u{0651} \u{064D}\u{0651} \u{064F}\u{0651} \u{064C}\u{0651}", // )]; +/// A provider that checks for positional forms in Arabic +/// +/// A font which supports Arabic should not only cover the Arabic codepoints, +/// but contain OpenType shaping rules for the `init`, `medi`, and `fina` features. +/// This provider checks that Arabic letters form positional forms when the `init`, `medi`, and `fina` features are enabled. pub struct PositionalProvider; impl Provider for PositionalProvider { @@ -68,6 +74,7 @@ impl Provider for PositionalProvider { } } +/// Create a pair of ShapingInputs for a positional form check fn positional_check( pre: &str, character: &str, diff --git a/shaperglot-lib/src/providers/small_caps.rs b/shaperglot-lib/src/providers/small_caps.rs index afdd26d..4be04ba 100644 --- a/shaperglot-lib/src/providers/small_caps.rs +++ b/shaperglot-lib/src/providers/small_caps.rs @@ -6,6 +6,10 @@ use crate::{ }; use unicode_properties::{GeneralCategory, UnicodeGeneralCategory}; +/// A provider that checks for small caps support in Latin-based languages +/// +/// This provider checks that Latin letters form small caps when the `smcp` feature is enabled. +/// If the `smcp` feature is not present in the font, the check will be skipped. pub struct SmallCapsProvider; impl Provider for SmallCapsProvider { diff --git a/shaperglot-lib/src/providers/toml.rs b/shaperglot-lib/src/providers/toml.rs index db105e3..3f07249 100644 --- a/shaperglot-lib/src/providers/toml.rs +++ b/shaperglot-lib/src/providers/toml.rs @@ -2,13 +2,16 @@ use std::collections::HashMap; use crate::{checks::Check, language::Language, Provider}; +/// The manual checks profile, a TOML file const TOML_PROFILE: &str = include_str!("../../manual_checks.toml"); use std::sync::LazyLock; +/// The manual checks, loaded from the TOML profile static MANUAL_CHECKS: LazyLock>> = LazyLock::new(|| toml::from_str(TOML_PROFILE).expect("Could not parse manual checks file: ")); +/// Provide additional language-specific checks via a static TOML file pub struct TomlProvider; impl Provider for TomlProvider { diff --git a/shaperglot-lib/src/reporter.rs b/shaperglot-lib/src/reporter.rs index 6219f8e..179f038 100644 --- a/shaperglot-lib/src/reporter.rs +++ b/shaperglot-lib/src/reporter.rs @@ -11,13 +11,19 @@ use serde_json::Value; use crate::language::Language; +/// A code representing the overall status of an individual check #[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] pub enum ResultCode { + /// The check passed successfully #[default] Pass, + /// There was a problem which does not prevent the font from being used Warn, + /// There was a problem which does prevent the font from being used Fail, + /// The check was skipped because some condition was not met Skip, + /// The font doesn't support something fundamental, no need to test further StopNow, } @@ -44,24 +50,35 @@ impl Display for ResultCode { } #[derive(Debug, Default, Serialize, Deserialize)] +/// Suggestions for how to fix the problem pub struct Fix { + /// The broad category of fix pub fix_type: String, + /// What the designer needs to do pub fix_thing: String, } #[derive(Debug, Default, Serialize, Deserialize)] +/// A problem found during a sub-test of a check pub struct Problem { + /// The name of the check that found the problem pub check_name: String, + /// The message describing the problem pub message: String, + /// A unique code for the problem pub code: String, + /// Whether the problem is terminal (i.e. the font is unusable) pub terminal: bool, + /// Additional context for the problem #[serde(skip_serializing_if = "Value::is_null")] pub context: Value, + /// Suggestions for how to fix the problem #[serde(skip_serializing_if = "Vec::is_empty")] pub fixes: Vec, } impl Problem { + /// Create a new problem pub fn new(check_name: &str, code: &str, message: String) -> Self { Self { check_name: check_name.to_string(), @@ -93,13 +110,21 @@ impl Display for Problem { impl Eq for Problem {} #[derive(Debug, Default, Serialize, Deserialize)] +/// The result of an individual check pub struct CheckResult { + /// The name of the check pub check_name: String, + /// A description of what the check does and why pub check_description: String, + /// The score for the check from 0.0 to 1.0 pub score: f32, + /// The weight of the check in the overall score for language support pub weight: u8, + /// The problems found during the check pub problems: Vec, + /// The total number of sub-tests run pub total_checks: usize, + /// The overall status of the check pub status: ResultCode, } @@ -113,6 +138,7 @@ impl Display for CheckResult { } } impl CheckResult { + /// Describe the result in a sentence pub fn summary_result(&self) -> String { if self.problems.is_empty() { return format!("{}: no problems found", self.check_name); @@ -122,25 +148,34 @@ impl CheckResult { } #[derive(Debug, Default, Serialize)] +/// A collection of check results pub struct Reporter(Vec); impl Reporter { + /// Create a new, empty reporter pub fn new() -> Self { Self(vec![]) } + /// Add a check result to the reporter pub fn add(&mut self, checkresult: CheckResult) { self.0.push(checkresult); } + /// Iterate over check results pub fn iter(&self) -> impl Iterator { self.0.iter() } + /// Iterate over individual problems found while checking pub fn iter_problems(&self) -> impl Iterator { self.0.iter().flat_map(|r| r.problems.iter()) } + /// A unique set of fixes required, organised by category + /// + /// Some checks may have multiple problems with the same fix, + /// so this method gathers the problems by category and fix required. pub fn unique_fixes(&self) -> HashMap> { // Arrange by fix type let mut fixes: HashMap> = HashMap::new(); @@ -155,19 +190,17 @@ impl Reporter { fixes } - pub fn count_fixes(&self) -> usize { - let unique_fixes = self.unique_fixes(); - unique_fixes.values().map(|v| v.len()).sum() - } - + /// Language support as a numerical score + /// + /// This is a weighted sum of all scores of the checks run, out of 100% pub fn score(&self) -> f32 { - // Weighted sum of all scores, out of 100% let total_weight: u8 = self.0.iter().map(|r| r.weight).sum(); let weighted_scores = self.0.iter().map(|r| r.score * f32::from(r.weight)); let total_score: f32 = weighted_scores.sum(); total_score / f32::from(total_weight) * 100.0 } + /// The overall level of support for a language pub fn support_level(&self) -> SupportLevel { if self.0.iter().any(|r| r.status == ResultCode::StopNow) { return SupportLevel::None; @@ -187,21 +220,34 @@ impl Reporter { SupportLevel::Supported } + /// Whether the font supports the language pub fn is_success(&self) -> bool { self.0.iter().all(|r| r.problems.is_empty()) } + + /// Whether the support level is unknown + /// + /// This normally occurs when the language definition is not complete + /// enough to run any checks. pub fn is_unknown(&self) -> bool { self.0.iter().map(|r| r.total_checks).sum::() == 0 } + /// The total number of unique fixes required to provide language support pub fn fixes_required(&self) -> usize { self.unique_fixes().values().map(|v| v.len()).sum::() } + /// Whether the font is nearly successful in supporting the language + /// + /// This is a designer-focused measure in that it counts the number of + /// fixes required and compares it to a threshold. The threshold is + /// set by the caller. pub fn is_nearly_success(&self, nearly: usize) -> bool { self.fixes_required() <= nearly } + /// A summary of the language support in one sentence pub fn to_summary_string(&self, language: &Language) -> String { match self.support_level() { SupportLevel::Complete => { @@ -250,11 +296,18 @@ impl Reporter { } #[derive(Debug, Serialize, PartialEq)] +/// Represents different levels of support for the language pub enum SupportLevel { - Complete, // Nothing can be improved. - Supported, // No FAILs or WARNS, but some optional SKIPs - Incomplete, // No FAILs - Unsupported, // There were FAILs - None, // Didn't even try - Indeterminate, // No checks + /// The support is complete; i.e. nothing can be improved + Complete, + /// There were no FAILs or WARNS, but some optional SKIPs which suggest possible improvements + Supported, + /// The support is incomplete, but usable; ie. there were WARNs, but no FAILs + Incomplete, + /// The language is not usable; ie. there were FAILs + Unsupported, + /// The font failed basic checks and is not usable at all for this language + None, + /// Language support could not be determined + Indeterminate, } diff --git a/shaperglot-lib/src/shaping.rs b/shaperglot-lib/src/shaping.rs index 323b115..39cc2af 100644 --- a/shaperglot-lib/src/shaping.rs +++ b/shaperglot-lib/src/shaping.rs @@ -9,15 +9,20 @@ use serde::{Deserialize, Serialize}; use crate::Checker; #[derive(Serialize, Deserialize, Debug, Clone)] +/// A struct representing the input to the shaping process. pub struct ShapingInput { + /// The text to shape. pub text: String, + /// The OpenType features to apply. #[serde(skip_serializing_if = "Vec::is_empty")] pub features: Vec, + /// The language to shape the text in. #[serde(skip_serializing_if = "Option::is_none")] pub language: Option, } impl ShapingInput { + /// Create a new `ShapingInput` with the given text, no features and language supplied pub fn new_simple(text: String) -> Self { Self { text, @@ -26,6 +31,7 @@ impl ShapingInput { } } + /// Create a new `ShapingInput` with the given text and a single OpenType feature pub fn new_with_feature(text: String, feature: impl AsRef) -> Self { Self { text, @@ -33,6 +39,8 @@ impl ShapingInput { language: None, } } + + /// Shape the text using the given checker context pub fn shape(&self, checker: &Checker) -> Result { let mut buffer = rustybuzz::UnicodeBuffer::new(); buffer.push_str(&self.text); @@ -47,6 +55,7 @@ impl ShapingInput { Ok(glyph_buffer) } + /// Describe the shaping input pub fn describe(&self) -> String { let mut description = format!("shaping the text '{}'", self.text); if let Some(language) = &self.language { @@ -59,6 +68,7 @@ impl ShapingInput { description } + /// Get the character at the given position in the text pub fn char_at(&self, pos: usize) -> Option { self.text.chars().nth(pos) }