diff --git a/shaperglot-cli/src/check.rs b/shaperglot-cli/src/check.rs index 24efa22..73eef31 100644 --- a/shaperglot-cli/src/check.rs +++ b/shaperglot-cli/src/check.rs @@ -37,7 +37,7 @@ pub fn check_command(args: &CheckArgs, language_database: shaperglot::Languages) println!("{}", serde_json::to_string(&results).unwrap()); continue; } - println!("{}", results.to_summary_string(args.nearly, language)); + println!("{}", results.to_summary_string(language)); show_result(&results, args.verbose); if args.fix { for (category, fixes) in results.unique_fixes() { diff --git a/shaperglot-cli/src/report.rs b/shaperglot-cli/src/report.rs index 08292e9..3f16908 100644 --- a/shaperglot-cli/src/report.rs +++ b/shaperglot-cli/src/report.rs @@ -39,6 +39,6 @@ pub fn report_command(args: &ReportArgs, language_database: shaperglot::Language if results.is_unknown() { continue; } - println!("{}", results.to_summary_string(args.nearly, language)); + println!("{}", results.to_summary_string(language)); } } diff --git a/shaperglot-lib/src/checker.rs b/shaperglot-lib/src/checker.rs index e3af781..8686fb8 100644 --- a/shaperglot-lib/src/checker.rs +++ b/shaperglot-lib/src/checker.rs @@ -4,6 +4,7 @@ use crate::{ font::{feature_tags, glyph_names}, language::Language, reporter::Reporter, + ResultCode, }; use rustybuzz::Face; use skrifa::{raw::ReadError, FontRef, GlyphId, MetadataProvider}; @@ -52,7 +53,11 @@ impl<'a> Checker<'a> { // let toml = toml::to_string(&check_object).unwrap(); // println!("Running check:\n{}", toml); let checkresult = check_object.execute(self); + let status = checkresult.status; results.add(checkresult); + if status == ResultCode::StopNow { + break; + } } results } diff --git a/shaperglot-lib/src/checks/codepoint_coverage.rs b/shaperglot-lib/src/checks/codepoint_coverage.rs index 9aa7dac..83a77e9 100644 --- a/shaperglot-lib/src/checks/codepoint_coverage.rs +++ b/shaperglot-lib/src/checks/codepoint_coverage.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use std::collections::HashSet; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct CodepointCoverage { strings: HashSet, code: String, @@ -50,6 +50,9 @@ impl CheckImplementation for CodepointCoverage { missing_things.join(", ") ), ); + if missing_things.len() == self.strings.len() { + fail.terminal = true; + } fail.context = json!({"glyphs": missing_things}); fail.fixes.extend(missing_things.iter().map(|x| Fix { fix_type: "add_codepoint".to_string(), diff --git a/shaperglot-lib/src/checks/mod.rs b/shaperglot-lib/src/checks/mod.rs index 26620e5..cb74536 100644 --- a/shaperglot-lib/src/checks/mod.rs +++ b/shaperglot-lib/src/checks/mod.rs @@ -21,13 +21,13 @@ pub trait CheckImplementation { fn execute(&self, checker: &Checker) -> (Vec, usize); } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, PartialEq, Debug)] pub enum ScoringStrategy { Continuous, AllOrNothing, } -#[derive(Delegate, Serialize, Deserialize)] +#[derive(Delegate, Serialize, Deserialize, Debug)] #[delegate(CheckImplementation)] #[serde(tag = "type")] pub enum CheckType { @@ -36,7 +36,7 @@ pub enum CheckType { ShapingDiffers(ShapingDiffers), } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct Check { pub name: String, pub severity: ResultCode, @@ -103,6 +103,10 @@ impl Check { ResultCode::Skip } else if problems.is_empty() { ResultCode::Pass + } else if self.scoring_strategy == ScoringStrategy::AllOrNothing + && problems.iter().any(|p| p.terminal) + { + ResultCode::StopNow } else { self.severity }, diff --git a/shaperglot-lib/src/checks/no_orphaned_marks.rs b/shaperglot-lib/src/checks/no_orphaned_marks.rs index 721b054..c441b61 100644 --- a/shaperglot-lib/src/checks/no_orphaned_marks.rs +++ b/shaperglot-lib/src/checks/no_orphaned_marks.rs @@ -8,7 +8,7 @@ use itertools::Itertools; use serde::{Deserialize, Serialize}; use unicode_properties::{GeneralCategory, UnicodeGeneralCategory}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct NoOrphanedMarks { test_strings: Vec, has_orthography: bool, @@ -67,6 +67,7 @@ impl CheckImplementation for NoOrphanedMarks { check_name: self.name(), message: format!("Shaper produced a dotted circle when {}", string), code: "dotted-circle-produced".to_string(), + terminal: false, context: serde_json::json!({ "text": previous, "mark": codepoint.glyph_id, @@ -96,6 +97,7 @@ impl CheckImplementation for NoOrphanedMarks { .unwrap_or_else(|| format!("Glyph #{}", codepoint.glyph_id)); let fail = Problem { check_name: self.name(), + terminal: false, message: format!( "Shaper didn't attach {} to {} when {}", this_name, previous_name, string diff --git a/shaperglot-lib/src/checks/shaping_differs.rs b/shaperglot-lib/src/checks/shaping_differs.rs index 1212825..441ffd1 100644 --- a/shaperglot-lib/src/checks/shaping_differs.rs +++ b/shaperglot-lib/src/checks/shaping_differs.rs @@ -10,7 +10,7 @@ use itertools::Itertools; use rustybuzz::SerializeFlags; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct ShapingDiffers { pairs: Vec<(ShapingInput, ShapingInput)>, features_optional: bool, diff --git a/shaperglot-lib/src/lib.rs b/shaperglot-lib/src/lib.rs index d7fc534..d9fa574 100644 --- a/shaperglot-lib/src/lib.rs +++ b/shaperglot-lib/src/lib.rs @@ -10,5 +10,5 @@ pub use crate::{ checker::Checker, language::Languages, providers::Provider, - reporter::{Reporter, ResultCode}, + reporter::{Reporter, ResultCode, SupportLevel}, }; diff --git a/shaperglot-lib/src/providers/positional.rs b/shaperglot-lib/src/providers/positional.rs index 91717d6..75ccf4f 100644 --- a/shaperglot-lib/src/providers/positional.rs +++ b/shaperglot-lib/src/providers/positional.rs @@ -19,6 +19,9 @@ pub struct PositionalProvider; impl Provider for PositionalProvider { fn checks_for(&self, language: &Language) -> Vec { + if language.script() != "Arab" { + return vec![]; + } // let marks = language // .marks // .iter() diff --git a/shaperglot-lib/src/reporter.rs b/shaperglot-lib/src/reporter.rs index 6f17fea..6219f8e 100644 --- a/shaperglot-lib/src/reporter.rs +++ b/shaperglot-lib/src/reporter.rs @@ -18,6 +18,7 @@ pub enum ResultCode { Warn, Fail, Skip, + StopNow, } impl Display for ResultCode { @@ -28,6 +29,7 @@ impl Display for ResultCode { ResultCode::Warn => "WARN".yellow(), ResultCode::Fail => "FAIL".red(), ResultCode::Skip => "SKIP".blue(), + ResultCode::StopNow => "STOP".red(), }; #[cfg(not(feature = "colored"))] let to_string = match self { @@ -35,6 +37,7 @@ impl Display for ResultCode { ResultCode::Warn => "WARN", ResultCode::Fail => "FAIL", ResultCode::Skip => "SKIP", + ResultCode::StopNow => "STOP", }; write!(f, "{}", to_string) } @@ -51,6 +54,7 @@ pub struct Problem { pub check_name: String, pub message: String, pub code: String, + pub terminal: bool, #[serde(skip_serializing_if = "Value::is_null")] pub context: Value, #[serde(skip_serializing_if = "Vec::is_empty")] @@ -164,6 +168,25 @@ impl Reporter { total_score / f32::from(total_weight) * 100.0 } + pub fn support_level(&self) -> SupportLevel { + if self.0.iter().any(|r| r.status == ResultCode::StopNow) { + return SupportLevel::None; + } + if self.is_unknown() { + return SupportLevel::Indeterminate; + } + if self.is_success() { + return SupportLevel::Complete; + } + if self.0.iter().any(|r| r.status == ResultCode::Fail) { + return SupportLevel::Unsupported; + } + if self.0.iter().any(|r| r.status == ResultCode::Warn) { + return SupportLevel::Incomplete; + } + SupportLevel::Supported + } + pub fn is_success(&self) -> bool { self.0.iter().all(|r| r.problems.is_empty()) } @@ -171,31 +194,67 @@ impl Reporter { self.0.iter().map(|r| r.total_checks).sum::() == 0 } + pub fn fixes_required(&self) -> usize { + self.unique_fixes().values().map(|v| v.len()).sum::() + } + pub fn is_nearly_success(&self, nearly: usize) -> bool { - self.unique_fixes().values().map(|v| v.len()).sum::() <= nearly + self.fixes_required() <= nearly } - pub fn to_summary_string(&self, nearly: usize, language: &Language) -> String { - let score = if self.is_unknown() { - "" - } else { - &format!(": {:.0}%", self.score()) - }; - let status = if self.is_unknown() { - "Cannot determine whether font supports " - } else if self.is_success() { - "Font supports " - } else if self.is_nearly_success(nearly) { - "Font nearly supports " - } else { - "Font does not fully support " - }; - format!( - "{}language {} ({}){}", - status, - language.id(), - language.name(), - score - ) + pub fn to_summary_string(&self, language: &Language) -> String { + match self.support_level() { + SupportLevel::Complete => { + format!( + "Font has complete support for {} ({}): 100%", + language.id(), + language.name() + ) + } + SupportLevel::Supported => format!( + "Font fully supports {} ({}): {:.0}%", + language.id(), + language.name(), + self.score() + ), + SupportLevel::Incomplete => format!( + "Font partially supports {} ({}): {:.0}% ({} fixes required)", + language.id(), + language.name(), + self.score(), + self.fixes_required() + ), + SupportLevel::Unsupported => format!( + "Font does not support {} ({}): {:.0}% ({} fixes required)", + language.id(), + language.name(), + self.score(), + self.fixes_required() + ), + SupportLevel::None => { + format!( + "Font does not attempt to support {} ({})", + language.id(), + language.name() + ) + } + SupportLevel::Indeterminate => { + format!( + "Cannot determine whether font supports {} ({})", + language.id(), + language.name() + ) + } + } } } + +#[derive(Debug, Serialize, PartialEq)] +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 +} diff --git a/shaperglot-web/src/lib.rs b/shaperglot-web/src/lib.rs index a96d90a..5679d4a 100644 --- a/shaperglot-web/src/lib.rs +++ b/shaperglot-web/src/lib.rs @@ -47,13 +47,7 @@ pub fn check_font(font_data: &[u8]) -> Result { } results.push(( serde_json::to_value(&language.proto).map_err(|e| e.to_string())?, - if result.is_success() { - "supported" - } else if result.is_nearly_success(5) { - "nearly-supported" - } else { - "unsupported" - }, + serde_json::to_value(result.support_level()).map_err(|e| e.to_string())?, serde_json::to_value(&result).map_err(|e| e.to_string())?, )); } diff --git a/shaperglot-web/www/index.js b/shaperglot-web/www/index.js index f5bc9bf..a578dae 100644 --- a/shaperglot-web/www/index.js +++ b/shaperglot-web/www/index.js @@ -4,6 +4,14 @@ const fix_descriptions = { add_codepoint: "Add the following codepoints to the font", add_feature: "Add the following features to the font", }; +const STATUS_INT = { + "Complete": 5, + "Supported": 4, + "Incomplete": 3, + "Unsupported": 2, + "None": 1, + "Indeterminate": 0, +}; function commify(x) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); @@ -76,7 +84,7 @@ class Shaperglot { issues_by_script[language.script] = issues_by_script[language.script] || []; issues_by_script[language.script].push([language, result, problems]); - if (result === "supported") { + if (result === "Supported" || result === "Complete" || result === "Incomplete") { count_supported_by_script[language.script] = (count_supported_by_script[language.script] || 0) + 1; } @@ -121,7 +129,7 @@ class Shaperglot { } for (let [language, result, problems] of languages.sort( - (a, b) => a[2].score - b[2].score || a[0].name.localeCompare(b[0].name) + (a, b) => STATUS_INT[a[1]] - STATUS_INT[b[1]] || a[0].name.localeCompare(b[0].name) )) { var thispill = $(`