mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 02:17:24 -06:00
Merge main into craft-i18n-pilot
Catalog rows from #3480 and #3177 move to craft-i18n (storytold/craft-i18n sync/wordcraft-topup); the pin moves to 04600cb. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,8 @@ permissions:
|
|||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: sonarcloud-${{ github.ref }}
|
group: sonarcloud-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
# A push to main is the run that publishes coverage; superseded runs there would never finish.
|
||||||
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sonarcloud:
|
sonarcloud:
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
be1f0535afbae9bfa1554b9afd545690eb2de2a5
|
04600cbb1508f1b15927ec574a5e32e5f376fd8b
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use wordcraft_doc::resolve::ResolvedChar;
|
|||||||
use wordcraft_doc::{InlineObject, PartKind, Pos, RevisionKind, StoryRef};
|
use wordcraft_doc::{InlineObject, PartKind, Pos, RevisionKind, StoryRef};
|
||||||
|
|
||||||
use super::sel_result;
|
use super::sel_result;
|
||||||
|
use super::tools::{is_turkic, lower_char, upper_char};
|
||||||
use crate::{CmdError, CmdResult, CommandSpec, Session, p};
|
use crate::{CmdError, CmdResult, CommandSpec, Session, p};
|
||||||
|
|
||||||
/// Word's font size list.
|
/// Word's font size list.
|
||||||
@@ -453,7 +454,16 @@ fn change_case(s: &mut Session, v: &Value) -> CmdResult {
|
|||||||
let to = if path == b.path { b.off } else { p.len() };
|
let to = if path == b.path { b.off } else { p.len() };
|
||||||
let chars = live_chars(p, from, to);
|
let chars = live_chars(p, from, to);
|
||||||
let src: String = chars.iter().map(|(_, c)| c).collect();
|
let src: String = chars.iter().map(|(_, c)| c).collect();
|
||||||
case_groups(p, &chars, &convert_chars(&src, &mode, from == 0))
|
// Turkish, Azerbaijani and Crimean Tatar text follows the same case mapping as
|
||||||
|
// AutoCorrect (#1923); each run's own language decides (#3479).
|
||||||
|
let turkic: Vec<bool> = chars
|
||||||
|
.iter()
|
||||||
|
.map(|(off, _)| {
|
||||||
|
let lang = s.doc.styles.resolve_char(p.props.style.as_deref(), p.props_of_char(*off)).lang;
|
||||||
|
is_turkic(lang.as_deref().or(s.editing_lang.as_deref()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
case_groups(p, &chars, &convert_chars(&src, &mode, from == 0, &turkic))
|
||||||
};
|
};
|
||||||
if groups.is_empty() {
|
if groups.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
@@ -587,37 +597,47 @@ fn is_title(t: &str) -> bool {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn convert(src: &str, mode: &str, para_start: bool) -> String {
|
fn convert(src: &str, mode: &str, para_start: bool) -> String {
|
||||||
convert_chars(src, mode, para_start).concat()
|
let turkic = vec![false; src.chars().count()];
|
||||||
|
convert_chars(src, mode, para_start, &turkic).concat()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The case-converted form of each character of `src`, in order (one entry per char, so a
|
/// The case-converted form of each character of `src`, in order (one entry per char, so a
|
||||||
/// change of length such as ß → SS or İ → i̇ can be applied run by run).
|
/// change of length such as ß → SS or İ → i̇ can be applied run by run). `turkic` is one flag
|
||||||
fn convert_chars(src: &str, mode: &str, para_start: bool) -> Vec<String> {
|
/// per char: true when that character's run is Turkish, Azerbaijani or Crimean Tatar.
|
||||||
|
fn convert_chars(src: &str, mode: &str, para_start: bool, turkic: &[bool]) -> Vec<String> {
|
||||||
let chars: Vec<char> = src.chars().collect();
|
let chars: Vec<char> = src.chars().collect();
|
||||||
// Greek capital sigma lowers to the final form at the end of a word (as `str::to_lowercase`).
|
// Greek capital sigma lowers to the final form at the end of a word (as `str::to_lowercase`).
|
||||||
let lower = |i: usize, c: char| -> String {
|
let lower = |i: usize, c: char| -> String {
|
||||||
let prev = i.checked_sub(1).and_then(|j| chars.get(j)).is_some_and(|p| p.is_alphabetic());
|
let prev = i.checked_sub(1).and_then(|j| chars.get(j)).is_some_and(|p| p.is_alphabetic());
|
||||||
let next = chars.get(i + 1).is_some_and(|n| n.is_alphabetic());
|
let next = chars.get(i + 1).is_some_and(|n| n.is_alphabetic());
|
||||||
if c == 'Σ' && prev && !next { "ς".to_string() } else { c.to_lowercase().collect() }
|
if c == 'Σ' && prev && !next {
|
||||||
|
"ς".to_string()
|
||||||
|
} else if turkic.get(i).copied().unwrap_or(false) {
|
||||||
|
lower_char(c, true)
|
||||||
|
} else {
|
||||||
|
c.to_lowercase().collect()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Title case capitalises the first word of any selection; sentence case only at a paragraph start.
|
// Title case capitalises the first word of any selection; sentence case only at a paragraph start.
|
||||||
let mut up = mode == "title" || para_start;
|
let mut up = mode == "title" || para_start;
|
||||||
let mut out = Vec::with_capacity(chars.len());
|
let mut out = Vec::with_capacity(chars.len());
|
||||||
for (i, &c) in chars.iter().enumerate() {
|
for (i, &c) in chars.iter().enumerate() {
|
||||||
|
let turkic = turkic.get(i).copied().unwrap_or(false);
|
||||||
|
let upper = |c: char| upper_char(c, turkic);
|
||||||
let new = match mode {
|
let new = match mode {
|
||||||
"upper" => c.to_uppercase().collect(),
|
"upper" => upper(c),
|
||||||
"lower" => lower(i, c),
|
"lower" => lower(i, c),
|
||||||
"toggle" => {
|
"toggle" => {
|
||||||
if c.is_uppercase() {
|
if c.is_uppercase() {
|
||||||
lower(i, c)
|
lower(i, c)
|
||||||
} else {
|
} else {
|
||||||
c.to_uppercase().collect()
|
upper(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"title" => {
|
"title" => {
|
||||||
let n = if up && c.is_alphabetic() {
|
let n = if up && c.is_alphabetic() {
|
||||||
up = false;
|
up = false;
|
||||||
c.to_uppercase().collect()
|
upper(c)
|
||||||
} else {
|
} else {
|
||||||
lower(i, c)
|
lower(i, c)
|
||||||
};
|
};
|
||||||
@@ -630,7 +650,7 @@ fn convert_chars(src: &str, mode: &str, para_start: bool) -> Vec<String> {
|
|||||||
// Sentence case.
|
// Sentence case.
|
||||||
let n = if up && c.is_alphabetic() {
|
let n = if up && c.is_alphabetic() {
|
||||||
up = false;
|
up = false;
|
||||||
c.to_uppercase().collect()
|
upper(c)
|
||||||
} else {
|
} else {
|
||||||
lower(i, c)
|
lower(i, c)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1028,7 +1028,7 @@ fn prev_token(before: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Turkish, Azerbaijani (and Crimean Tatar) map `i`/`İ` and `ı`/`I` as pairs (#1923).
|
/// Turkish, Azerbaijani (and Crimean Tatar) map `i`/`İ` and `ı`/`I` as pairs (#1923).
|
||||||
fn is_turkic(lang: Option<&str>) -> bool {
|
pub(crate) fn is_turkic(lang: Option<&str>) -> bool {
|
||||||
if wordcraft_proof::ProofLang::from_tag(lang) == wordcraft_proof::ProofLang::Tr {
|
if wordcraft_proof::ProofLang::from_tag(lang) == wordcraft_proof::ProofLang::Tr {
|
||||||
return true; // tr, tur, any region, same normalisation as proofing
|
return true; // tr, tur, any region, same normalisation as proofing
|
||||||
}
|
}
|
||||||
@@ -1038,7 +1038,7 @@ fn is_turkic(lang: Option<&str>) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A letter in capitals: `i` → `İ` in Turkic languages, otherwise the Unicode default.
|
/// A letter in capitals: `i` → `İ` in Turkic languages, otherwise the Unicode default.
|
||||||
fn upper_char(c: char, turkic: bool) -> String {
|
pub(crate) fn upper_char(c: char, turkic: bool) -> String {
|
||||||
if turkic && c == 'i' {
|
if turkic && c == 'i' {
|
||||||
return "\u{130}".to_string();
|
return "\u{130}".to_string();
|
||||||
}
|
}
|
||||||
@@ -1047,7 +1047,7 @@ fn upper_char(c: char, turkic: bool) -> String {
|
|||||||
|
|
||||||
/// A letter in lowercase: `I` → `ı` in Turkic languages. `İ` is always the single letter `i`,
|
/// A letter in lowercase: `I` → `ı` in Turkic languages. `İ` is always the single letter `i`,
|
||||||
/// never `i` plus a combining dot.
|
/// never `i` plus a combining dot.
|
||||||
fn lower_char(c: char, turkic: bool) -> String {
|
pub(crate) fn lower_char(c: char, turkic: bool) -> String {
|
||||||
match c {
|
match c {
|
||||||
'I' if turkic => "\u{131}".to_string(),
|
'I' if turkic => "\u{131}".to_string(),
|
||||||
'\u{130}' => "i".to_string(),
|
'\u{130}' => "i".to_string(),
|
||||||
|
|||||||
@@ -2075,6 +2075,46 @@ fn autocorrect_capitalisation_follows_turkic_case_mapping() {
|
|||||||
assert_eq!(s.doc.plain_text(StoryRef::Body), "İngilizce ", "no combining dot in any language");
|
assert_eq!(s.doc.plain_text(StoryRef::Body), "İngilizce ", "no combining dot in any language");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #3479: Format > Change Case follows the same Turkic case mapping as AutoCorrect, using
|
||||||
|
/// each run's own language; other languages keep the Unicode default.
|
||||||
|
#[test]
|
||||||
|
fn change_case_follows_turkic_case_mapping() {
|
||||||
|
for tag in ["tr-TR", "az-AZ"] {
|
||||||
|
for (mode, input, want) in [
|
||||||
|
("upper", "istanbul", "İSTANBUL"),
|
||||||
|
("lower", "IŞIK", "ışık"),
|
||||||
|
("title", "istanbul güzel", "İstanbul Güzel"),
|
||||||
|
("sentence", "istanbul. izmir", "İstanbul. İzmir"),
|
||||||
|
("toggle", "iI", "İı"),
|
||||||
|
] {
|
||||||
|
let mut s = s();
|
||||||
|
run(&mut s, "review.language", json!({"lang": tag, "default": true}));
|
||||||
|
run(&mut s, "text.insert", json!({"text": input, "raw": true}));
|
||||||
|
run(&mut s, "select.all", json!({}));
|
||||||
|
run(&mut s, "format.changeCase", json!({"mode": mode}));
|
||||||
|
assert_eq!(s.doc.plain_text(StoryRef::Body), want, "{tag} {mode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Other languages keep the Unicode default.
|
||||||
|
let mut s = self::s();
|
||||||
|
run(&mut s, "text.insert", json!({"text": "istanbul", "raw": true}));
|
||||||
|
run(&mut s, "select.all", json!({}));
|
||||||
|
run(&mut s, "format.changeCase", json!({"mode": "title"}));
|
||||||
|
assert_eq!(s.doc.plain_text(StoryRef::Body), "Istanbul", "en-US");
|
||||||
|
// Mixed runs: each run's own language decides.
|
||||||
|
let mut s = self::s();
|
||||||
|
run(&mut s, "text.insert", json!({"text": "IŞIK IŞIK", "raw": true}));
|
||||||
|
run(&mut s, "caret.set", json!({"pos": Pos::body(0, 0)}));
|
||||||
|
run(&mut s, "caret.set", json!({"pos": Pos::body(0, 5), "extend": true}));
|
||||||
|
run(&mut s, "review.language", json!({"lang": "tr-TR"}));
|
||||||
|
run(&mut s, "caret.set", json!({"pos": Pos::body(0, 6)}));
|
||||||
|
run(&mut s, "caret.set", json!({"pos": Pos::body(0, 11), "extend": true}));
|
||||||
|
run(&mut s, "review.language", json!({"lang": "en-US"}));
|
||||||
|
run(&mut s, "select.all", json!({}));
|
||||||
|
run(&mut s, "format.changeCase", json!({"mode": "lower"}));
|
||||||
|
assert_eq!(s.doc.plain_text(StoryRef::Body), "ışık işik", "mixed runs");
|
||||||
|
}
|
||||||
|
|
||||||
// Spelling as you type (#1552): the word being typed gets no red squiggle until the caret leaves
|
// Spelling as you type (#1552): the word being typed gets no red squiggle until the caret leaves
|
||||||
// it; F7 and the Spelling pane still report it. See docs/typing-parity.md. Paragraphs start with
|
// it; F7 and the Spelling pane still report it. See docs/typing-parity.md. Paragraphs start with
|
||||||
// a correct word so AutoCorrect's sentence capital doesn't change the misspelled one.
|
// a correct word so AutoCorrect's sentence capital doesn't change the misspelled one.
|
||||||
|
|||||||
@@ -42,10 +42,11 @@ pub fn dictionary() -> Arc<AffixDict> {
|
|||||||
|
|
||||||
fn load_installed() -> AffixDict {
|
fn load_installed() -> AffixDict {
|
||||||
let Some((aff, dic)) = crate::hunspell::find(NAMES) else {
|
let Some((aff, dic)) = crate::hunspell::find(NAMES) else {
|
||||||
|
crate::hunspell::clear_load_error(NAMES[0]);
|
||||||
log::info!("Czech spelling: no Hunspell cs_CZ dictionary installed");
|
log::info!("Czech spelling: no Hunspell cs_CZ dictionary installed");
|
||||||
return AffixDict::default();
|
return AffixDict::default();
|
||||||
};
|
};
|
||||||
crate::hunspell::load(&aff, &dic).unwrap_or_else(|e| {
|
crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| {
|
||||||
log::warn!("Czech spelling: {e}");
|
log::warn!("Czech spelling: {e}");
|
||||||
AffixDict::default()
|
AffixDict::default()
|
||||||
})
|
})
|
||||||
|
|||||||
+44
-16
@@ -395,16 +395,13 @@ pub fn has_dictionary(lang: Option<&str>) -> bool {
|
|||||||
ProofLang::from_tag(lang).is_proofed()
|
ProofLang::from_tag(lang).is_proofed()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Hunspell dictionary name (`pl_PL`, `pt_BR`) a proofing language needs but doesn't have
|
/// The Hunspell dictionary name (`pl_PL`, `it_IT`) a proofing language needs but doesn't have
|
||||||
/// installed, for a status-bar hint (#1167). `None` when the language's dictionary is available,
|
/// installed, for a status-bar hint (#1167). `None` when the language's dictionary is available,
|
||||||
/// or when it needs none (English has a built-in word list; unproofed languages aren't offered
|
/// or when it needs none (English has a built-in word list; unproofed languages aren't offered
|
||||||
/// one). Reads the dictionary once (it is cached afterwards).
|
/// one). Reads the dictionary once (it is cached afterwards).
|
||||||
pub fn missing_dictionary(lang: Option<&str>) -> Option<&'static str> {
|
pub fn missing_dictionary(lang: Option<&str>) -> Option<&'static str> {
|
||||||
match ProofLang::from_tag(lang) {
|
let lang = ProofLang::from_tag(lang);
|
||||||
ProofLang::Pl => dictionary_needed(ProofLang::Pl, pl::has_dictionary()),
|
dictionary_needed(lang, dictionary_loaded(lang)?)
|
||||||
ProofLang::Pt => dictionary_needed(ProofLang::Pt, pt::has_dictionary()),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Hunspell name, `.dic` file and reason of a dictionary that is installed for this language
|
/// The Hunspell name, `.dic` file and reason of a dictionary that is installed for this language
|
||||||
@@ -412,24 +409,38 @@ pub fn missing_dictionary(lang: Option<&str>) -> Option<&'static str> {
|
|||||||
/// loaded, or when no file exists (then [`missing_dictionary`] names it). Reads the dictionary
|
/// loaded, or when no file exists (then [`missing_dictionary`] names it). Reads the dictionary
|
||||||
/// once (it is cached afterwards).
|
/// once (it is cached afterwards).
|
||||||
pub fn unreadable_dictionary(lang: Option<&str>) -> Option<(&'static str, std::path::PathBuf, String)> {
|
pub fn unreadable_dictionary(lang: Option<&str>) -> Option<(&'static str, std::path::PathBuf, String)> {
|
||||||
let (proof_lang, loaded) = match ProofLang::from_tag(lang) {
|
let lang = ProofLang::from_tag(lang);
|
||||||
ProofLang::Pl => (ProofLang::Pl, pl::has_dictionary()),
|
let name = dictionary_needed(lang, dictionary_loaded(lang)?)?;
|
||||||
ProofLang::Pt => (ProofLang::Pt, pt::has_dictionary()),
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
let name = dictionary_needed(proof_lang, false)?;
|
|
||||||
if loaded {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
hunspell::load_error(name).map(|(file, reason)| (name, file, reason))
|
hunspell::load_error(name).map(|(file, reason)| (name, file, reason))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Hunspell name `lang` needs when `installed` is false; only Polish and Portuguese use one.
|
/// Did the Hunspell dictionary of `lang` load? `None` for languages that use none (English).
|
||||||
|
fn dictionary_loaded(lang: ProofLang) -> Option<bool> {
|
||||||
|
Some(match lang {
|
||||||
|
ProofLang::Pl => pl::has_dictionary(),
|
||||||
|
ProofLang::Pt => pt::has_dictionary(),
|
||||||
|
ProofLang::It => it::has_dictionary(),
|
||||||
|
ProofLang::Hr => hr::has_dictionary(),
|
||||||
|
ProofLang::Tr => tr::has_dictionary(),
|
||||||
|
ProofLang::Ru => ru::has_dictionary(),
|
||||||
|
ProofLang::Cs => cs::has_dictionary(),
|
||||||
|
ProofLang::Id => id::has_dictionary(),
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Hunspell name `lang` needs when `installed` is false (the first name its module looks for).
|
||||||
fn dictionary_needed(lang: ProofLang, installed: bool) -> Option<&'static str> {
|
fn dictionary_needed(lang: ProofLang, installed: bool) -> Option<&'static str> {
|
||||||
match lang {
|
match lang {
|
||||||
_ if installed => None,
|
_ if installed => None,
|
||||||
ProofLang::Pl => Some("pl_PL"),
|
ProofLang::Pl => Some("pl_PL"),
|
||||||
ProofLang::Pt => Some("pt_BR"),
|
ProofLang::Pt => Some("pt_BR"),
|
||||||
|
ProofLang::It => Some("it_IT"),
|
||||||
|
ProofLang::Hr => Some("hr_HR"),
|
||||||
|
ProofLang::Tr => Some("tr_TR"),
|
||||||
|
ProofLang::Ru => Some("ru_RU"),
|
||||||
|
ProofLang::Cs => Some("cs_CZ"),
|
||||||
|
ProofLang::Id => Some("id_ID"),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -957,6 +968,23 @@ mod tests {
|
|||||||
assert_eq!(dictionary_needed(ProofLang::Pt, false), Some("pt_BR"));
|
assert_eq!(dictionary_needed(ProofLang::Pt, false), Some("pt_BR"));
|
||||||
assert_eq!(dictionary_needed(ProofLang::Pl, true), None);
|
assert_eq!(dictionary_needed(ProofLang::Pl, true), None);
|
||||||
assert_eq!(dictionary_needed(ProofLang::Pt, true), None);
|
assert_eq!(dictionary_needed(ProofLang::Pt, true), None);
|
||||||
|
// Every Hunspell language is named by the first name its module looks for (#3150).
|
||||||
|
for (l, name) in [
|
||||||
|
(ProofLang::It, "it_IT"),
|
||||||
|
(ProofLang::Hr, "hr_HR"),
|
||||||
|
(ProofLang::Tr, "tr_TR"),
|
||||||
|
(ProofLang::Ru, "ru_RU"),
|
||||||
|
(ProofLang::Cs, "cs_CZ"),
|
||||||
|
(ProofLang::Id, "id_ID"),
|
||||||
|
] {
|
||||||
|
assert_eq!(dictionary_needed(l, false), Some(name), "{l:?}");
|
||||||
|
assert_eq!(dictionary_needed(l, true), None, "{l:?}");
|
||||||
|
}
|
||||||
|
// Every proofed language other than English has a Hunspell dictionary to report.
|
||||||
|
for l in ProofLang::PROOFED {
|
||||||
|
let english = matches!(l, ProofLang::En | ProofLang::EnGb | ProofLang::EnCa);
|
||||||
|
assert_eq!(dictionary_needed(l, false).is_some(), !english, "{l:?}");
|
||||||
|
}
|
||||||
for l in [ProofLang::En, ProofLang::Bg, ProofLang::De, ProofLang::Other] {
|
for l in [ProofLang::En, ProofLang::Bg, ProofLang::De, ProofLang::Other] {
|
||||||
assert_eq!(dictionary_needed(l, false), None);
|
assert_eq!(dictionary_needed(l, false), None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,10 +63,11 @@ pub fn dictionary() -> Arc<AffixDict> {
|
|||||||
|
|
||||||
fn load_installed() -> AffixDict {
|
fn load_installed() -> AffixDict {
|
||||||
let Some((aff, dic)) = crate::hunspell::find(NAMES) else {
|
let Some((aff, dic)) = crate::hunspell::find(NAMES) else {
|
||||||
|
crate::hunspell::clear_load_error(NAMES[0]);
|
||||||
log::info!("Russian spelling: no Hunspell ru_RU dictionary installed");
|
log::info!("Russian spelling: no Hunspell ru_RU dictionary installed");
|
||||||
return AffixDict::default();
|
return AffixDict::default();
|
||||||
};
|
};
|
||||||
crate::hunspell::load(&aff, &dic).unwrap_or_else(|e| {
|
crate::hunspell::note_load(NAMES[0], &dic, crate::hunspell::load(&aff, &dic)).unwrap_or_else(|e| {
|
||||||
log::warn!("Russian spelling: {e}");
|
log::warn!("Russian spelling: {e}");
|
||||||
AffixDict::default()
|
AffixDict::default()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,23 @@ use wordcraft_doc::{PartKind, StoryRef};
|
|||||||
use crate::theme::{Tokens, TypeRung, fit_text, medium, paint_text, regular, semibold, text_width};
|
use crate::theme::{Tokens, TypeRung, fit_text, medium, paint_text, regular, semibold, text_width};
|
||||||
use crate::{WordApp, icons};
|
use crate::{WordApp, icons};
|
||||||
|
|
||||||
|
/// Why the caret's language isn't spell-checked (dictionary unreadable or missing), if it isn't.
|
||||||
|
pub(crate) fn dictionary_problem(lang: &str, unreadable: &Option<(std::path::PathBuf, String)>, missing: Option<&str>) -> Option<String> {
|
||||||
|
match (unreadable, missing) {
|
||||||
|
(Some((file, reason)), _) => Some(crate::i18n::fmt(
|
||||||
|
tl!("The spelling dictionary for {language} couldn't be read, so it isn't checked. File: {file}. Reason: {reason}"),
|
||||||
|
&[("language", lang), ("file", &file.to_string_lossy()), ("reason", reason)],
|
||||||
|
)),
|
||||||
|
(None, Some(name)) => Some(crate::i18n::fmt(
|
||||||
|
tl!(
|
||||||
|
"No spelling dictionary for {language} is installed, so it isn't checked. WordCraft looks for the Hunspell dictionary {name} in the WORDCRAFT_DICTIONARIES folder and in the usual system locations."
|
||||||
|
),
|
||||||
|
&[("language", lang), ("name", name)],
|
||||||
|
)),
|
||||||
|
(None, None) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The Quick Access Toolbar's button for command `id`.
|
/// The Quick Access Toolbar's button for command `id`.
|
||||||
pub(crate) fn qat_button_id(id: &str) -> egui::Id {
|
pub(crate) fn qat_button_id(id: &str) -> egui::Id {
|
||||||
egui::Id::new(("qat_button", id))
|
egui::Id::new(("qat_button", id))
|
||||||
@@ -590,17 +607,7 @@ pub fn status_bar(app: &mut WordApp, ui: &mut Ui) {
|
|||||||
} else {
|
} else {
|
||||||
lang.clone()
|
lang.clone()
|
||||||
};
|
};
|
||||||
let tip = match (&unreadable, missing) {
|
let tip = dictionary_problem(&lang, &unreadable, missing).unwrap_or_else(|| tl!("Language").to_string());
|
||||||
(Some((file, reason)), _) => crate::i18n::fmt(
|
|
||||||
tl!("The spelling dictionary for {language} couldn't be read, so it isn't checked. File: {file}. Reason: {reason}"),
|
|
||||||
&[("language", &lang), ("file", &file.to_string_lossy()), ("reason", reason)],
|
|
||||||
),
|
|
||||||
(None, Some(name)) => crate::i18n::fmt(
|
|
||||||
tl!("No spelling dictionary for {language} is installed, so it isn't checked. WordCraft looks for the Hunspell dictionary {name} in the WORDCRAFT_DICTIONARIES folder and in the usual system locations."),
|
|
||||||
&[("language", &lang), ("name", name)],
|
|
||||||
),
|
|
||||||
(None, None) => tl!("Language").to_string(),
|
|
||||||
};
|
|
||||||
let resp = st(ui, &shown).on_hover_text(tip);
|
let resp = st(ui, &shown).on_hover_text(tip);
|
||||||
egui::Popup::menu(&resp).show(|ui| {
|
egui::Popup::menu(&resp).show(|ui| {
|
||||||
ui.set_min_width(200.0);
|
ui.set_min_width(200.0);
|
||||||
|
|||||||
@@ -460,9 +460,17 @@ fn language(app: &mut WordApp, ui: &mut Ui) {
|
|||||||
// #414 (detect the document's language) goes here.
|
// #414 (detect the document's language) goes here.
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
section(ui, "Proofing language");
|
section(ui, "Proofing language");
|
||||||
// The document's default language (Review › Language › Set As Default changes it).
|
let lang = crate::ribbon::caret_language(app);
|
||||||
let tag = app.session.doc.styles.default_chr.lang.clone();
|
let text = if crate::ribbon::caret_no_proof(app) {
|
||||||
note(ui, &crate::ribbon::language_name(tag.as_deref()));
|
tl!("The text at the insertion point is marked Do not check spelling or grammar.").to_string()
|
||||||
|
} else if !crate::ribbon::caret_language_proofed(app) {
|
||||||
|
crate::i18n::fmt(tl!("{language}: WordCraft doesn't check spelling or grammar in this language yet."), &[("language", &lang)])
|
||||||
|
} else {
|
||||||
|
let unreadable = crate::ribbon::caret_unreadable_dictionary(app);
|
||||||
|
crate::chrome::dictionary_problem(&lang, &unreadable, crate::ribbon::caret_missing_dictionary(app))
|
||||||
|
.unwrap_or_else(|| crate::i18n::fmt(tl!("Spelling and grammar are checked in {language}."), &[("language", &lang)]))
|
||||||
|
};
|
||||||
|
ui.label(RichText::new(text).small().weak());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accessibility(app: &mut WordApp, ui: &mut Ui) {
|
fn accessibility(app: &mut WordApp, ui: &mut Ui) {
|
||||||
|
|||||||
@@ -908,11 +908,27 @@ pub(crate) fn caret_language(app: &WordApp) -> String {
|
|||||||
tl!(&language_name(caret_lang_tag(app).as_deref())).to_string()
|
tl!(&language_name(caret_lang_tag(app).as_deref())).to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The language tag at the caret, resolved through the styles and the document default.
|
/// The formatting at the caret, resolved through the styles and the document default.
|
||||||
fn caret_lang_tag(app: &WordApp) -> Option<String> {
|
fn caret_char(app: &WordApp) -> wordcraft_doc::resolve::ResolvedChar {
|
||||||
let s = &app.session;
|
let s = &app.session;
|
||||||
let style = s.doc.para_at(&s.sel.focus).and_then(|p| p.props.style.clone());
|
let style = s.doc.para_at(&s.sel.focus).and_then(|p| p.props.style.clone());
|
||||||
s.doc.styles.resolve_char(style.as_deref(), &s.typing_props()).lang
|
s.doc.styles.resolve_char(style.as_deref(), &s.typing_props())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The language tag at the caret, resolved through the styles and the document default.
|
||||||
|
fn caret_lang_tag(app: &WordApp) -> Option<String> {
|
||||||
|
caret_char(app).lang
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is the text at the caret itself marked Do not check spelling or grammar? (`format.state`
|
||||||
|
/// looks at the whole word around a caret, so a partly marked word would read as checked.)
|
||||||
|
pub(crate) fn caret_no_proof(app: &WordApp) -> bool {
|
||||||
|
caret_char(app).no_proof
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does WordCraft check spelling and grammar in the caret's language at all (Bulgarian: no)?
|
||||||
|
pub(crate) fn caret_language_proofed(app: &WordApp) -> bool {
|
||||||
|
wordcraft_engine::proof::has_dictionary(caret_lang_tag(app).as_deref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Hunspell dictionary the caret's language needs but doesn't have (`pl_PL`), if any (#1167).
|
/// The Hunspell dictionary the caret's language needs but doesn't have (`pl_PL`), if any (#1167).
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Installing spelling dictionaries
|
# Installing spelling dictionaries
|
||||||
|
|
||||||
> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-10-18 · **Change:** trivial (ISO 8859-1 dictionaries are read, #2824; previously major: first version)
|
> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-10-22 · **Change:** trivial (every Hunspell language shows (no dictionary), not only Polish and Portuguese, and Indonesian is listed, #3150; previously trivial: ISO 8859-1 dictionaries are read, #2824)
|
||||||
|
|
||||||
English spelling works out of the box. Polish, Russian, Portuguese, Czech, Italian, Croatian and
|
English spelling works out of the box. Polish, Russian, Portuguese, Czech, Italian, Croatian,
|
||||||
Turkish spelling use a standard **Hunspell** dictionary (an `.aff` and a `.dic` file) that
|
Turkish and Indonesian spelling use a standard **Hunspell** dictionary (an `.aff` and a `.dic` file) that
|
||||||
WordCraft reads at runtime. These dictionaries are not bundled: they are large, and several are
|
WordCraft reads at runtime. These dictionaries are not bundled: they are large, and several are
|
||||||
GPL or LGPL licensed. Without one, the language isn't checked. The status bar then shows the
|
GPL or LGPL licensed. Without one, the language isn't checked. The status bar then shows the
|
||||||
language with **(no dictionary)**, for example *Português (Brasil) (no dictionary)*.
|
language with **(no dictionary)**, for example *Português (Brasil) (no dictionary)*.
|
||||||
@@ -29,6 +29,7 @@ WordCraft looks for the first of these file names (each name means a `<name>.aff
|
|||||||
| Italian | `it_IT`, `it_CH`, `it-IT`, `it` | LibreOffice `dict-it` |
|
| Italian | `it_IT`, `it_CH`, `it-IT`, `it` | LibreOffice `dict-it` |
|
||||||
| Croatian | `hr_HR`, `hr-HR`, `hr` | LibreOffice `dict-hr` |
|
| Croatian | `hr_HR`, `hr-HR`, `hr` | LibreOffice `dict-hr` |
|
||||||
| Turkish | `tr_TR`, `tr-TR`, `tr` | LibreOffice `dict-tr` |
|
| Turkish | `tr_TR`, `tr-TR`, `tr` | LibreOffice `dict-tr` |
|
||||||
|
| Indonesian | `id_ID`, `id-ID`, `id`, `in_ID` | LibreOffice `dict-id` |
|
||||||
|
|
||||||
Every Portuguese region uses the same `pt_BR` dictionary, so text marked *Português (Portugal)*
|
Every Portuguese region uses the same `pt_BR` dictionary, so text marked *Português (Portugal)*
|
||||||
is checked with it too.
|
is checked with it too.
|
||||||
@@ -127,6 +128,7 @@ Never commit dictionary files to this repository: they stay on the user's system
|
|||||||
|
|
||||||
| Date | Change | Summary |
|
| Date | Change | Summary |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-10-22 | trivial | Italian, Croatian, Turkish, Russian, Czech and Indonesian without a dictionary now show **(no dictionary)** (or **(dictionary unreadable)**) too, as this page already said; before, only Polish and Portuguese did. Indonesian is listed with its file names (#3150) |
|
||||||
| 2026-10-18 | trivial | ISO 8859-1 (`SET ISO8859-1`) dictionaries are read, so the original VERO `pt_BR` works without converting it to UTF-8 (#2824) |
|
| 2026-10-18 | trivial | ISO 8859-1 (`SET ISO8859-1`) dictionaries are read, so the original VERO `pt_BR` works without converting it to UTF-8 (#2824) |
|
||||||
| 2026-10-18 | minor | An installed dictionary that can't be read shows **(dictionary unreadable)** in the status bar, with the file and reason in its tooltip and in File › Options › Proofing (#2830) |
|
| 2026-10-18 | minor | An installed dictionary that can't be read shows **(dictionary unreadable)** in the status bar, with the file and reason in its tooltip and in File › Options › Proofing (#2830) |
|
||||||
| 2026-10-16 | major | First version: the file names per language, the search order and folders on Windows, macOS and Linux, installing `pt_BR` on each, and how to check it works |
|
| 2026-10-16 | major | First version: the file names per language, the search order and folders on Windows, macOS and Linux, installing `pt_BR` on each, and how to check it works |
|
||||||
|
|||||||
@@ -187,8 +187,9 @@ Applied when a word is finished by a space, punctuation **or Enter** (`enter_fin
|
|||||||
`İngilizce`, `ılık` → `Ilık`; observed in Word for Mac with Turkish proofing). Lowercasing `İ`
|
`İngilizce`, `ılık` → `Ilık`; observed in Word for Mac with Turkish proofing). Lowercasing `İ`
|
||||||
gives the single letter `i` in every language, never `i` + combining dot. Other languages keep
|
gives the single letter `i` in every language, never `i` + combining dot. Other languages keep
|
||||||
the Unicode default (`istanbul` → `Istanbul` in English)
|
the Unicode default (`istanbul` → `Istanbul` in English)
|
||||||
(`autocorrect_capitalisation_follows_turkic_case_mapping`). Format > Change Case is not yet
|
(`autocorrect_capitalisation_follows_turkic_case_mapping`); Format > Change Case follows the
|
||||||
Turkish-aware.
|
same Turkic mapping from each run's language, while other languages keep the Unicode default
|
||||||
|
(`change_case_follows_turkic_case_mapping`, #3479).
|
||||||
- Names of days are capitalised (`monday` → `Monday`). AutoCorrect Options (#412,
|
- Names of days are capitalised (`monday` → `Monday`). AutoCorrect Options (#412,
|
||||||
`tools.autocorrect`, saved per user) switch each of these off: replacements, sentence and
|
`tools.autocorrect`, saved per user) switch each of these off: replacements, sentence and
|
||||||
table-cell capitals, days, smart quotes, fractions, ordinals, dashes, links, bulleted and
|
table-cell capitals, days, smart quotes, fractions, ordinals, dashes, links, bulleted and
|
||||||
|
|||||||
@@ -70,6 +70,6 @@ modules:
|
|||||||
# The interface translations at the commit in craft-i18n.rev (a test keeps the two equal).
|
# The interface translations at the commit in craft-i18n.rev (a test keeps the two equal).
|
||||||
- type: git
|
- type: git
|
||||||
url: https://github.com/storytold/craft-i18n
|
url: https://github.com/storytold/craft-i18n
|
||||||
commit: be1f0535afbae9bfa1554b9afd545690eb2de2a5
|
commit: 04600cbb1508f1b15927ec574a5e32e5f376fd8b
|
||||||
dest: craft-i18n
|
dest: craft-i18n
|
||||||
- cargo-sources.json
|
- cargo-sources.json
|
||||||
|
|||||||
@@ -99,6 +99,34 @@ fi
|
|||||||
|
|
||||||
# ---- AppImage -----------------------------------------------------------------------------------
|
# ---- AppImage -----------------------------------------------------------------------------------
|
||||||
if has appimage; then
|
if has appimage; then
|
||||||
|
APPIMAGETOOL_VERSION=1.9.1
|
||||||
|
# appimagetool would otherwise embed type2-runtime's `continuous` build, unchecked.
|
||||||
|
RUNTIME_VERSION=20251108
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64)
|
||||||
|
APPIMAGETOOL_SHA256=ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0
|
||||||
|
RUNTIME_SHA256=2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d
|
||||||
|
;;
|
||||||
|
aarch64)
|
||||||
|
APPIMAGETOOL_SHA256=f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158
|
||||||
|
RUNTIME_SHA256=00cbdfcf917cc6c0ff6d3347d59e0ca1f7f45a6df1a428a0d6d8a78664d87444
|
||||||
|
;;
|
||||||
|
*) APPIMAGETOOL_SHA256='' RUNTIME_SHA256='' ;;
|
||||||
|
esac
|
||||||
|
if [[ -z "$RUNTIME_SHA256" ]]; then
|
||||||
|
echo "no pinned AppImage runtime for $ARCH" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
# Downloads $1 to $3 once and checks it against SHA-256 $2, the cached copy again on every run.
|
||||||
|
fetch_pinned() {
|
||||||
|
if [[ ! -x "$3" ]]; then
|
||||||
|
curl --proto '=https' -fsSL -o "$3.download" "$1"
|
||||||
|
printf '%s %s\n' "$2" "$3.download" | sha256sum -c -
|
||||||
|
chmod 755 "$3.download"
|
||||||
|
mv "$3.download" "$3"
|
||||||
|
fi
|
||||||
|
printf '%s %s\n' "$2" "$3" | sha256sum -c -
|
||||||
|
}
|
||||||
APPDIR="$WORK/WordCraft.AppDir"
|
APPDIR="$WORK/WordCraft.AppDir"
|
||||||
cp -R "$STAGE" "$APPDIR"
|
cp -R "$STAGE" "$APPDIR"
|
||||||
mv "$APPDIR/usr/share/doc" "$WORK/doc-unused"
|
mv "$APPDIR/usr/share/doc" "$WORK/doc-unused"
|
||||||
@@ -109,17 +137,23 @@ if has appimage; then
|
|||||||
|
|
||||||
TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}"
|
TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}"
|
||||||
if [[ -z "$TOOL" ]]; then
|
if [[ -z "$TOOL" ]]; then
|
||||||
TOOL="$CARGO_TARGET_DIR/appimagetool-$ARCH.AppImage"
|
if [[ -z "$APPIMAGETOOL_SHA256" ]]; then
|
||||||
if [[ ! -x "$TOOL" ]]; then
|
echo "no pinned appimagetool for $ARCH; set APPIMAGETOOL or put appimagetool on PATH" >&2
|
||||||
curl --proto '=https' -fsSL -o "$TOOL" "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$ARCH.AppImage"
|
exit 2
|
||||||
chmod +x "$TOOL"
|
|
||||||
fi
|
fi
|
||||||
|
TOOL="$CARGO_TARGET_DIR/appimagetool-$APPIMAGETOOL_VERSION-$ARCH.AppImage"
|
||||||
|
fetch_pinned "https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage" \
|
||||||
|
"$APPIMAGETOOL_SHA256" "$TOOL"
|
||||||
fi
|
fi
|
||||||
|
RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$RUNTIME_VERSION-$ARCH"
|
||||||
|
fetch_pinned "https://github.com/AppImage/type2-runtime/releases/download/$RUNTIME_VERSION/runtime-$ARCH" \
|
||||||
|
"$RUNTIME_SHA256" "$RUNTIME"
|
||||||
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
|
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
|
||||||
# relative, e.g. target/agent-<name>).
|
# relative, e.g. target/agent-<name>).
|
||||||
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
|
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
|
||||||
APPDIR="$(cd "$APPDIR" && pwd)"
|
APPDIR="$(cd "$APPDIR" && pwd)"
|
||||||
TOOL="$(cd "$(dirname "$TOOL")" && pwd)/$(basename "$TOOL")"
|
TOOL="$(cd "$(dirname "$TOOL")" && pwd)/$(basename "$TOOL")"
|
||||||
|
RUNTIME="$(cd "$(dirname "$RUNTIME")" && pwd)/$(basename "$RUNTIME")"
|
||||||
# A .zsync left by an earlier run would hide a missing zsyncmake and describe another file.
|
# A .zsync left by an earlier run would hide a missing zsyncmake and describe another file.
|
||||||
rm -f "$OUT.zsync"
|
rm -f "$OUT.zsync"
|
||||||
# Update information: AppImageUpdate, AppImageLauncher and the like read it from the file and
|
# Update information: AppImageUpdate, AppImageLauncher and the like read it from the file and
|
||||||
@@ -128,10 +162,11 @@ if has appimage; then
|
|||||||
# pre-release. A fork's builds point at its own releases through GITHUB_REPOSITORY.
|
# pre-release. A fork's builds point at its own releases through GITHUB_REPOSITORY.
|
||||||
REPO="${GITHUB_REPOSITORY:-storytold/wordcraft}"
|
REPO="${GITHUB_REPOSITORY:-storytold/wordcraft}"
|
||||||
UPDATE_INFO="gh-releases-zsync|${REPO%%/*}|${REPO#*/}|latest|wordcraft-*-linux-$ARCH.AppImage.zsync"
|
UPDATE_INFO="gh-releases-zsync|${REPO%%/*}|${REPO#*/}|latest|wordcraft-*-linux-$ARCH.AppImage.zsync"
|
||||||
# Extract-and-run: works without FUSE (containers, CI). The output embeds the static runtime,
|
# Extract-and-run: works without FUSE (containers, CI). The output embeds the pinned static
|
||||||
# so users don't need libfuse2 either. With zsyncmake on the host (CI installs the zsync
|
# runtime, so users don't need libfuse2 either. With zsyncmake on the host (CI installs the zsync
|
||||||
# package) appimagetool also writes the .zsync, into its working directory, hence the cd.
|
# package) appimagetool also writes the .zsync, into its working directory, hence the cd.
|
||||||
(cd "$DIST" && ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --no-appstream -u "$UPDATE_INFO" "$APPDIR" "$OUT")
|
(cd "$DIST" && ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --no-appstream \
|
||||||
|
--runtime-file "$RUNTIME" -u "$UPDATE_INFO" "$APPDIR" "$OUT")
|
||||||
echo "wrote $OUT"
|
echo "wrote $OUT"
|
||||||
if [[ -s "$OUT.zsync" ]]; then
|
if [[ -s "$OUT.zsync" ]]; then
|
||||||
echo "wrote $OUT.zsync"
|
echo "wrote $OUT.zsync"
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ if (-not $haveCert -and -not $haveAzure) {
|
|||||||
|
|
||||||
$script:SignTool = Find-SignTool
|
$script:SignTool = Find-SignTool
|
||||||
$common = @('sign', '/v', '/fd', 'SHA256', '/td', 'SHA256', '/d', 'WordCraft', '/du', 'https://github.com/storytold/wordcraft')
|
$common = @('sign', '/v', '/fd', 'SHA256', '/td', 'SHA256', '/d', 'WordCraft', '/du', 'https://github.com/storytold/wordcraft')
|
||||||
$tmp = Join-Path ([IO.Path]::GetTempPath()) "wordcraft-sign-$PID"
|
$tmp = Join-Path ([IO.Path]::GetTempPath()) "wordcraft-sign-$([IO.Path]::GetRandomFileName())"
|
||||||
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
|
New-Item -ItemType Directory -Path $tmp | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($haveCert) {
|
if ($haveCert) {
|
||||||
@@ -73,8 +73,15 @@ try {
|
|||||||
Write-Output "Signing with Azure Trusted Signing ($env:AZURE_SIGNING_ACCOUNT / $env:AZURE_CERT_PROFILE): $($Files -join ', ')"
|
Write-Output "Signing with Azure Trusted Signing ($env:AZURE_SIGNING_ACCOUNT / $env:AZURE_CERT_PROFILE): $($Files -join ', ')"
|
||||||
# The dlib authenticates with DefaultAzureCredential, which reads AZURE_TENANT_ID,
|
# The dlib authenticates with DefaultAzureCredential, which reads AZURE_TENANT_ID,
|
||||||
# AZURE_CLIENT_ID and AZURE_CLIENT_SECRET from the environment.
|
# AZURE_CLIENT_ID and AZURE_CLIENT_SECRET from the environment.
|
||||||
|
$trustedSigningVersion = '1.0.95'
|
||||||
|
$trustedSigningSha256 = '3bfcf1e0a3cb42af1692f0a8ed45c15de070c2de86f28a59b2795d904d8a920f'
|
||||||
$pkg = Join-Path $tmp 'trusted-signing.zip'
|
$pkg = Join-Path $tmp 'trusted-signing.zip'
|
||||||
Invoke-WebRequest -Uri 'https://www.nuget.org/api/v2/package/Microsoft.Trusted.Signing.Client' -OutFile $pkg
|
$uri = "https://api.nuget.org/v3-flatcontainer/microsoft.trusted.signing.client/$trustedSigningVersion/microsoft.trusted.signing.client.$trustedSigningVersion.nupkg"
|
||||||
|
Invoke-WebRequest -Uri $uri -OutFile $pkg
|
||||||
|
$actualHash = (Get-FileHash -Path $pkg -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
if ($actualHash -ne $trustedSigningSha256) {
|
||||||
|
throw "Microsoft.Trusted.Signing.Client $trustedSigningVersion checksum mismatch"
|
||||||
|
}
|
||||||
Expand-Archive -Path $pkg -DestinationPath (Join-Path $tmp 'client') -Force
|
Expand-Archive -Path $pkg -DestinationPath (Join-Path $tmp 'client') -Force
|
||||||
$dlib = Get-ChildItem -Path (Join-Path $tmp 'client') -Recurse -Filter Azure.CodeSigning.Dlib.dll |
|
$dlib = Get-ChildItem -Path (Join-Path $tmp 'client') -Recurse -Filter Azure.CodeSigning.Dlib.dll |
|
||||||
Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1
|
Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1
|
||||||
|
|||||||
Reference in New Issue
Block a user