v3 : liens d'équipe, publication sur git.open.us.org, huitième relecture
Liens d'équipe (Yvv, 2026-10-21) : un lien réutilisable jusqu'à une date
et pour un nombre de personnes, révocable. Chaque ouverture demande un nom
et crée sa propre identité d'invitation, jamais administratrice ; un nom
déjà pris est refusé ; une personne déjà connectée reçoit les groupes.
trustinWallet montre par quel lien chaque identité est venue, et donne un
nouveau lien de connexion à une personne invitée (autre appareil).
Migration additive 0002 (table team_links, colonne identities.team_link).
Publication : yvv/libredecision-textes sur git.open.us.org, pour
l'instant (un dépôt pour l'ouverture, puis le dépôt officiel, viendront).
Huitième relecture (c3d6bf8 : ok), points corrigés :
- une négation n'est jamais coupée ni formée par collage (« ja mais ») ;
- caractères invisibles : tout l'ensemble Unicode des ignorables et les
contrôles autres que saut de ligne et tabulation ;
- sauts de ligne et indentation font partie de la forme (listes) ;
- un titre tient sur une ligne.
Boucle : 65 tests Rust, svelte-check, 9 vitest, e2e 13/13 (C12 ajouté).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -89,17 +89,41 @@ fn is_latin_letter(c: char) -> bool {
|
||||
c.is_alphabetic() && matches!(c as u32, 0x41..=0x5A | 0x61..=0x7A | 0xC0..=0x17F)
|
||||
}
|
||||
|
||||
/// The only spaces free to change; any other (carriage return, tab, hair,
|
||||
/// thin, ogham…) is kept as it is.
|
||||
const FREE_SPACES: &[char] = &[' ', '\u{a0}', '\u{202f}', '\n'];
|
||||
/// The only spaces free to change between words; any other (carriage return,
|
||||
/// tab, hair, thin, ogham…) is kept as it is, and so are line breaks and the
|
||||
/// indentation that opens a line: in Markdown they make lists and paragraphs.
|
||||
const FREE_SPACES: &[char] = &[' ', '\u{a0}', '\u{202f}'];
|
||||
|
||||
/// Invisible format characters (zero-width, bidirectional overrides and
|
||||
/// isolates, byte order mark): they can make "12" display as "21" ("Trojan
|
||||
/// Source"). A text of reference never needs them; they are refused.
|
||||
/// Invisible characters: the Unicode default-ignorable set (zero-width,
|
||||
/// bidirectional overrides and isolates, soft hyphen, variation selectors,
|
||||
/// tags, byte order mark…) and control characters other than line breaks and
|
||||
/// tabs (an escape sequence rewrites a terminal). They can make "12" display as
|
||||
/// "21" ("Trojan Source") or hide text in the published files. A text of
|
||||
/// reference never needs them; they are refused.
|
||||
pub fn has_hidden_controls(s: &str) -> bool {
|
||||
s.chars().any(
|
||||
|c| matches!(c as u32, 0x200B..=0x200F | 0x202A..=0x202E | 0x2060..=0x2064 | 0x2066..=0x2069 | 0x061C | 0xFEFF),
|
||||
)
|
||||
s.chars().any(|c| {
|
||||
(c.is_control() && !matches!(c, '\n' | '\r' | '\t'))
|
||||
|| matches!(
|
||||
c as u32,
|
||||
0x00AD
|
||||
| 0x034F
|
||||
| 0x061C
|
||||
| 0x115F..=0x1160
|
||||
| 0x17B4..=0x17B5
|
||||
| 0x180B..=0x180F
|
||||
| 0x200B..=0x200F
|
||||
| 0x202A..=0x202E
|
||||
| 0x2060..=0x206F
|
||||
| 0x3164
|
||||
| 0xFE00..=0xFE0F
|
||||
| 0xFEFF
|
||||
| 0xFFA0
|
||||
| 0xFFF0..=0xFFF8
|
||||
| 0x1BCA0..=0x1BCA3
|
||||
| 0x1D173..=0x1D17A
|
||||
| 0xE0000..=0xE0FFF
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Apostrophes and quotation marks of every style are one and the same sign:
|
||||
@@ -123,7 +147,16 @@ fn shape(s: &str) -> String {
|
||||
let is_figure = |i: Option<usize>| i.and_then(|i| cs.get(i)).is_some_and(|c| c.is_numeric());
|
||||
let mut out = String::new();
|
||||
let mut in_word = false;
|
||||
let mut line_start = true;
|
||||
for (i, &c) in cs.iter().enumerate() {
|
||||
if c == '\n' || (line_start && c.is_whitespace()) {
|
||||
// Line breaks and indentation are structure: « - a\n- b » is a list.
|
||||
out.push(c);
|
||||
in_word = false;
|
||||
line_start = true;
|
||||
continue;
|
||||
}
|
||||
line_start = false;
|
||||
if is_latin_letter(c) {
|
||||
if !in_word {
|
||||
out.push('a');
|
||||
@@ -203,16 +236,17 @@ fn words(s: &str) -> Vec<String> {
|
||||
/// « latransmission » → « la transmission ». A boundary moved is not a typo
|
||||
/// (« les avoir » → « le savoir »), nor « Si non » → « Sinon ».
|
||||
fn only_glued_or_split(a: &[String], b: &[String]) -> bool {
|
||||
let joined = |x: &String, y: &String| !NEGATIONS.contains(&x.as_str()) && !NEGATIONS.contains(&y.as_str());
|
||||
// Neither the word glued or split nor its pieces may be a negation (« ja mais »).
|
||||
let joined = |w: &str, x: &str, y: &str| ![w, x, y].iter().any(|t| NEGATIONS.contains(t));
|
||||
let (mut i, mut j, mut moves) = (0, 0, 0);
|
||||
while i < a.len() && j < b.len() {
|
||||
if a[i] == b[j] {
|
||||
(i, j) = (i + 1, j + 1);
|
||||
continue;
|
||||
}
|
||||
if j + 1 < b.len() && a[i] == format!("{}{}", b[j], b[j + 1]) && joined(&b[j], &b[j + 1]) {
|
||||
if j + 1 < b.len() && a[i] == format!("{}{}", b[j], b[j + 1]) && joined(&a[i], &b[j], &b[j + 1]) {
|
||||
(i, j) = (i + 1, j + 2);
|
||||
} else if i + 1 < a.len() && b[j] == format!("{}{}", a[i], a[i + 1]) && joined(&a[i], &a[i + 1]) {
|
||||
} else if i + 1 < a.len() && b[j] == format!("{}{}", a[i], a[i + 1]) && joined(&b[j], &a[i], &a[i + 1]) {
|
||||
(i, j) = (i + 2, j + 1);
|
||||
} else {
|
||||
return false;
|
||||
@@ -389,6 +423,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_negation_is_never_cut_nor_formed() {
|
||||
// Eighth independent review, 2026-10-21: the pieces were checked, not the word.
|
||||
for (old, new) in [
|
||||
("n'avoir jamais qu'un seul compte", "n'avoir ja mais qu'un seul compte"),
|
||||
("aucun compte", "au cun compte"),
|
||||
("sans retour", "san s retour"),
|
||||
("rien de plus", "ri en de plus"),
|
||||
("nowhere else", "now here else"),
|
||||
] {
|
||||
assert_eq!(classify(old, new), EditKind::Substantive, "{old} → {new}");
|
||||
assert_eq!(classify(new, old), EditKind::Substantive, "{new} → {old}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_breaks_and_indentation_are_structure() {
|
||||
for (old, new) in [
|
||||
("- une garantie\n- une autre", "- une garantie - une autre"),
|
||||
("- une garantie\n - une précision", "- une garantie\n- une précision"),
|
||||
("la\ntransmission", "la transmission"),
|
||||
] {
|
||||
assert_eq!(classify(old, new), EditKind::Substantive, "{old:?} → {new:?}");
|
||||
}
|
||||
assert_eq!(
|
||||
classify("- une garantie\n- latransmission", "- une garantie\n- la transmission"),
|
||||
EditKind::Typographic
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_spaces_and_look_alike_letters_are_kept() {
|
||||
assert_eq!(classify("un texte\tde référence", "un texte de référence"), EditKind::Substantive);
|
||||
@@ -518,6 +582,11 @@ mod tests {
|
||||
assert!(has_hidden_controls("**\u{202e}12\u{202c} mois**"));
|
||||
assert!(has_hidden_controls("5\u{200b}0"));
|
||||
assert!(!has_hidden_controls("Je m’engage — « à 50 % »."));
|
||||
assert!(!has_hidden_controls("une ligne\nune autre\tavec tabulation"));
|
||||
// The whole default-ignorable set and the control characters (eighth review).
|
||||
for hidden in ["\u{ad}", "\u{180e}", "\u{fe0f}", "\u{e0041}", "\u{206e}", "\u{1b}[2J", "\u{7f}", "\u{85}"] {
|
||||
assert!(has_hidden_controls(&format!("cer{hidden}tification")), "{hidden:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user