mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 08:51:27 +00:00
* AcroForm: integrate persistent timesheet script workflows * AcroForm: address review caching, worker stacks and live metadata * M6: address second review of AcroForm script workflows * M6: handle top-level ASI boundaries and verify cyclic calculation results * M6: bound script workers and isolate pending completions across edits * M6: make form resets visible before subsequent script reads * M6: include calculated dependents in reset comparison baselines * M6: bound reset writeback work before native completion * M6: charge expanded reset selections and field name bytes * M6: bound string conversion bytes and charge reset widgets * M6: preserve unread defaults and bound prompt text conversion * M6: bound reset calls early and preserve calculated field identity * AcroForm: address grouped calculations, field identity and widget ownership * AcroForm: retire field instances and address native review edge cases * AcroForm: route comparison Save As and Print to the source document * M6: dispatch text widget mouse-up scripts * M6: honor fit bounds for form button icons * AcroForm: bound calculation work and preserve reset and save integrity * Harden AcroForm script requests and worker recovery * Keep form callbacks and focus consistent across edits * AcroForm: preserve reset ordering and bound edit callbacks * Fix reset appearance ordering and XFA focus after row removal * Keep XFA focus indexes and stopped reset appearances consistent * Document form script execution and reset helpers * Keep script workers bounded and reject partial callback edits * Keep form events available after a document script reaches VM limits * Keep main's first-value Simple calculation and restore the calculateNow helper test AFSimple_Calculate (JavaScript) and the native Simple calculation count each field's first value again, as #5304 landed them; summing every selection of a multiple-selection list is a separate behaviour change. Restores the document-helper calculateNow test and the AFSimple_Calculate doc comment, and adds native and JavaScript multiple-selection cases. * Patch the callback field snapshot in place instead of rebuilding it Each field callback rebuilt all field states (3,129 on the benchmark form), and the retained realm held the previous snapshot, so stamping instance tokens copied it again. The runner now keeps its last snapshot and patches only the values and flags that changed; ScriptSession::stamp_fields stamps it before dispatch, the worker copies only when a token changes, and the realm drops the job's snapshot when the job ends (field identity is checked against the cached keys instead). * An oversized job script is that script's error, not a refused edit Realm::run refused a script too long or too deep to run as an execution refusal, which made the engine reject the whole edit (setting a 300 KiB calculation script failed). As on main it is now a plain script error that keeps the value; the realm is not touched. * Read a leading #! line as a comment in the chain scan; follow main's refusal and conditional limits in tests The nesting guard from #5079 now checks brackets, prefixes and conditionals; the document worker's preflight still bounds arrow-function and right-associative chains and skips a leading #! line as the guard does. A refused script is an error, not a refused edit, so the preflight test now checks the edit goes through with the error reported. --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
454 lines
21 KiB
Rust
454 lines
21 KiB
Rust
//! Form date fields name months in the app's date language (#5303). Its own test binary: the
|
||
//! names are process-wide, so other tests must not see them change, and its tests take turns.
|
||
|
||
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
|
||
|
||
use pdfcraft_cos::{Document, Object};
|
||
use pdfcraft_engine::dates::use_form_date_language;
|
||
use pdfcraft_engine::js::JsRunner;
|
||
use pdfcraft_forms::FieldValue;
|
||
use pdfcraft_forms::af::{DateTime, Format, format_date, format_value, keystroke, parse_date};
|
||
use pdfcraft_js::{DocInfo, Event, Limits, run};
|
||
|
||
/// Held by each test while it changes the process-wide date names, which start as English (a
|
||
/// failed test may have left others).
|
||
fn names_lock() -> MutexGuard<'static, ()> {
|
||
static LOCK: Mutex<()> = Mutex::new(());
|
||
let lock = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
|
||
use_form_date_language("en");
|
||
lock
|
||
}
|
||
|
||
/// `util.printd(fmt)` of 18 October 2026, then of what `util.scand(fmt, typed)` reads.
|
||
fn printd(fmt: &str, typed: &str) -> String {
|
||
let doc = DocInfo { file_name: "form.pdf".into(), num_pages: 1, page: 0, ..Default::default() };
|
||
let script = format!("event.value = util.printd('{fmt}', new Date(2026, 9, 18)) + '|' + util.printd('{fmt}', util.scand('{fmt}', '{typed}'));");
|
||
let o = run(&script, &Event::field("Format", "date", ""), &doc, &[], &[], Limits::default());
|
||
assert_eq!(o.error, None);
|
||
o.value
|
||
}
|
||
|
||
#[test]
|
||
fn form_dates_follow_the_date_language() {
|
||
let _names = names_lock();
|
||
let oct18 = DateTime { y: 2026, m: 10, d: 18, hh: 0, mm: 0, ss: 0 };
|
||
let dd_mmm_yy = Format::Date("dd-mmm-yy".into());
|
||
// English until the app says otherwise.
|
||
assert_eq!(format_date(oct18, "dd-mmm-yy dddd ddd mmmm"), "18-Oct-26 Sunday Sun October");
|
||
use_form_date_language("it");
|
||
// AFDate_FormatEx("dd-mmm-yy"), what the issue's form used.
|
||
assert_eq!(format_value(&dd_mmm_yy, "18-10-26"), "18-ott-26");
|
||
assert_eq!(format_date(oct18, "dddd d mmmm yyyy, ddd"), "domenica 18 ottobre 2026, dom");
|
||
// What it writes reads back, and so does English typing.
|
||
assert_eq!(parse_date("18-ott-26", "dd-mmm-yy"), Some(oct18));
|
||
assert_eq!(parse_date("18 ottobre 2026", "dd mmmm yyyy"), Some(oct18));
|
||
assert_eq!(parse_date("18-Oct-26", "dd-mmm-yy"), Some(oct18));
|
||
assert!(keystroke(&dd_mmm_yy, "date", "18-ott-26").is_ok());
|
||
// util.printd writes Italian names; util.scand reads them.
|
||
assert_eq!(printd("dd-mmm-yy", "18-ott-26"), "18-ott-26|18-ott-26");
|
||
assert_eq!(printd("dd-mmm-yy", "18-Oct-26"), "18-ott-26|18-ott-26", "English typing reads too");
|
||
assert_eq!(printd("dddd d mmmm yyyy", "domenica 18 ottobre 2026"), "domenica 18 ottobre 2026|domenica 18 ottobre 2026");
|
||
// German short names end in a dot; they are written and read whole.
|
||
use_form_date_language("de");
|
||
assert_eq!(format_value(&dd_mmm_yy, "18-10-26"), "18-Okt.-26");
|
||
assert_eq!(parse_date("18-Okt.-26", "dd-mmm-yy"), Some(oct18));
|
||
// An unknown language is English again.
|
||
use_form_date_language("xx");
|
||
assert_eq!(format_value(&dd_mmm_yy, "18-10-26"), "18-Oct-26");
|
||
assert_eq!(printd("dd-mmm-yy", "18-Oct-26"), "18-Oct-26|18-Oct-26");
|
||
every_date_language_reads_back_what_it_writes();
|
||
// Hostile typing never panics and stays quick: long, odd and mixed-script text.
|
||
use_form_date_language("th");
|
||
let junk = ["", " ", "ม.", "ม.ค.ม.ค.ม.ค.", "\u{0}", "1月1月1月", "tháng tháng", "Ottobre", "🙂ott🙂", "İstanbul ott 26"];
|
||
for t in junk.iter().map(|s| s.to_string()).chain(["ม.ค.1".repeat(5000), "9".repeat(100_000)]) {
|
||
let started = std::time::Instant::now();
|
||
let _ = parse_date(&t, "dd-mmm-yy");
|
||
let _ = keystroke(&dd_mmm_yy, "date", &t);
|
||
assert!(started.elapsed() < std::time::Duration::from_secs(2), "{} chars took {:?}", t.len(), started.elapsed());
|
||
}
|
||
use_form_date_language("en");
|
||
}
|
||
|
||
/// An English weekday typed before the month fills the format's weekday, so a month name that
|
||
/// the date language shares with a weekday (Italian, Spanish, Romanian and Albanian "mar") is
|
||
/// still read as the month: "Tue 17 Mar 2026" read as "Tue 17 Tuesday 2026" (#5303).
|
||
#[test]
|
||
fn an_english_weekday_before_a_shared_month_name_reads_as_english() {
|
||
let _names = names_lock();
|
||
let doc = DocInfo { file_name: "form.pdf".into(), num_pages: 1, page: 0, ..Default::default() };
|
||
let mar17 = DateTime { y: 2026, m: 3, d: 17, hh: 0, mm: 0, ss: 0 };
|
||
let mut bad = Vec::new();
|
||
for language in ["it", "es", "ro", "sq"] {
|
||
use_form_date_language(language);
|
||
let ddd_d_mmm = "ddd d mmm yyyy";
|
||
let typed = [
|
||
// What the language writes, then English, then English and localized mixed.
|
||
(ddd_d_mmm, format_date(mar17, ddd_d_mmm)),
|
||
(ddd_d_mmm, "Tue 17 Mar 2026".into()),
|
||
(ddd_d_mmm, "Tuesday 17 Mar 2026".into()),
|
||
(ddd_d_mmm, "tue 17 mar 2026".into()),
|
||
(ddd_d_mmm, format!("Tue 17 {}", format_date(mar17, "mmm yyyy"))),
|
||
(ddd_d_mmm, format!("{} 17 Mar 2026", format_date(mar17, "ddd"))),
|
||
("mmm d yyyy ddd", "Mar 17 2026 Tue".into()),
|
||
("dddd d mmmm yyyy", "Tuesday 17 March 2026".into()),
|
||
];
|
||
for (fmt, text) in typed {
|
||
if parse_date(&text, fmt) != Some(mar17) {
|
||
bad.push(format!("{language} forms {fmt}: {text:?} -> {:?}", parse_date(&text, fmt)));
|
||
}
|
||
let script = format!("var d = util.scand('{fmt}', {text:?}); event.value = d ? util.printd('yyyy-mm-dd', d) : 'null';");
|
||
let o = run(&script, &Event::field("Format", "date", ""), &doc, &[], &[], Limits::default());
|
||
if o.value != "2026-03-17" {
|
||
bad.push(format!("{language} scand {fmt}: {text:?} -> {:?} {:?}", o.value, o.error));
|
||
}
|
||
}
|
||
}
|
||
use_form_date_language("en");
|
||
assert!(bad.is_empty(), "{}", bad.join("; "));
|
||
}
|
||
|
||
/// Every date language: what a date field writes reads back as the same date, for each date
|
||
/// letter that writes a name, through the form path and through `util.scand`. Called from the
|
||
/// one test, which owns the process-wide names.
|
||
fn every_date_language_reads_back_what_it_writes() {
|
||
let doc = DocInfo { file_name: "form.pdf".into(), num_pages: 1, page: 0, ..Default::default() };
|
||
let mut bad = Vec::new();
|
||
for l in pdfcraft_engine::dates::DATE_LANGUAGES.iter() {
|
||
use_form_date_language(l.code);
|
||
for m in 1..=12u32 {
|
||
let dt = DateTime { y: 2026, m, d: 18, hh: 0, mm: 0, ss: 0 };
|
||
for fmt in ["dd-mmm-yy", "d mmmm yyyy", "dddd, mmmm d, yyyy", "ddd d mmm yyyy", "mmm d, yyyy ddd", "yyyymmmdd"] {
|
||
let text = format_date(dt, fmt);
|
||
// A short month ending in a number ("Thg 1") glued to the day is ambiguous
|
||
// ("Thg 118": January 18 or November 8): nothing can read it back.
|
||
if fmt == "yyyymmmdd" && l.months_short.iter().any(|n| n.ends_with(|c: char| c.is_ascii_digit())) {
|
||
continue;
|
||
}
|
||
if parse_date(&text, fmt).map(|p| (p.y, p.m, p.d)) != Some((2026, m, 18)) {
|
||
bad.push(format!("{} forms {fmt}: {text:?}", l.code));
|
||
}
|
||
let script = format!("var d = util.scand('{fmt}', {text:?}); event.value = d ? util.printd('yyyy-mm-dd', d) : 'null';");
|
||
let o = run(&script, &Event::field("Format", "date", ""), &doc, &[], &[], Limits::default());
|
||
if o.value != format!("2026-{m:02}-18") {
|
||
bad.push(format!("{} scand {fmt}: {text:?} -> {:?} {:?}", l.code, o.value, o.error));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
use_form_date_language("en");
|
||
assert!(bad.is_empty(), "{} failures:\n{}", bad.len(), bad.iter().take(40).cloned().collect::<Vec<_>>().join("\n"));
|
||
}
|
||
|
||
/// A form whose text fields are each `(name, font, format script)`: `/Helv` is Helvetica with
|
||
/// WinAnsi, `/Song` a Chinese font with a Unicode CMap, which also holds Cyrillic.
|
||
fn form(fields: &[(&str, &str, &str)]) -> Document {
|
||
let n = fields.len();
|
||
let first = 6;
|
||
let refs = |from: usize| (from..from + n).map(|i| format!("{i} 0 R")).collect::<Vec<_>>().join(" ");
|
||
let mut objs: Vec<String> = vec![
|
||
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(),
|
||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(),
|
||
format!("<< /Type /Page /Parent 2 0 R /Annots [{}] >>", refs(first)),
|
||
format!("<< /Fields [{}] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 5 0 R /Song {} 0 R >> >> >>", refs(first), first + n),
|
||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(),
|
||
];
|
||
for (i, (name, font, js)) in fields.iter().enumerate() {
|
||
let js = js.replace('\\', "\\\\").replace('(', "\\(").replace(')', "\\)");
|
||
let y = 700 - 30 * i;
|
||
objs.push(format!(
|
||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T ({name}) /DA (/{font} 10 Tf 0 g) /Rect [50 {y} 300 {}] /P 3 0 R /AA << /F << /S /JavaScript /JS ({js}) >> >> >>",
|
||
y + 20
|
||
));
|
||
}
|
||
objs.push("<< /Type /Font /Subtype /Type0 /BaseFont /STSong-Light /Encoding /UniGB-UCS2-H /DescendantFonts [<< /Type /Font /Subtype /CIDFontType0 /BaseFont /STSong-Light /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 4 >> >>] >>".into());
|
||
let mut out = b"%PDF-1.7
|
||
"
|
||
.to_vec();
|
||
let mut offsets = Vec::new();
|
||
for (i, o) in objs.iter().enumerate() {
|
||
offsets.push(out.len());
|
||
out.extend_from_slice(
|
||
format!(
|
||
"{} 0 obj
|
||
{o}
|
||
endobj
|
||
",
|
||
i + 1
|
||
)
|
||
.as_bytes(),
|
||
);
|
||
}
|
||
let xref = out.len();
|
||
out.extend_from_slice(
|
||
format!(
|
||
"xref
|
||
0 {}
|
||
0000000000 65535 f
|
||
",
|
||
objs.len() + 1
|
||
)
|
||
.as_bytes(),
|
||
);
|
||
for o in offsets {
|
||
out.extend_from_slice(
|
||
format!(
|
||
"{o:010} 00000 n
|
||
"
|
||
)
|
||
.as_bytes(),
|
||
);
|
||
}
|
||
out.extend_from_slice(
|
||
format!(
|
||
"trailer
|
||
<< /Size {} /Root 1 0 R >>
|
||
startxref
|
||
{xref}
|
||
%%EOF
|
||
",
|
||
objs.len() + 1
|
||
)
|
||
.as_bytes(),
|
||
);
|
||
Document::open(Arc::new(out)).expect("opens")
|
||
}
|
||
|
||
/// The content of field `name`'s normal appearance.
|
||
fn appearance(doc: &Document, name: &str) -> Vec<u8> {
|
||
let all = pdfcraft_forms::fields(doc);
|
||
let f = all.iter().find(|f| f.name == name).expect("field");
|
||
let widget = doc.get(f.widgets[0].obj);
|
||
let normal = widget.as_dict().and_then(|d| d.get(b"AP")).and_then(|a| doc.resolve(a).as_dict().and_then(|a| a.reference(b"N"))).expect("/AP /N");
|
||
match &*doc.get(normal) {
|
||
Object::Stream(s) => s.decoded().expect("decodes"),
|
||
other => panic!("not a stream: {other:?}"),
|
||
}
|
||
}
|
||
|
||
/// What a Unicode-CMap (UCS-2) font is given for `text`.
|
||
fn ucs2(text: &str) -> Vec<u8> {
|
||
text.encode_utf16().flat_map(u16::to_be_bytes).collect()
|
||
}
|
||
|
||
fn contains(hay: &[u8], needle: &[u8]) -> bool {
|
||
hay.windows(needle.len()).any(|w| w == needle)
|
||
}
|
||
|
||
/// Fills `form`'s field `name` with 18 October 2026 through the JavaScript runner.
|
||
fn fill(doc: &mut Document, name: &str) {
|
||
fill_with(doc, name, "18-10-26");
|
||
}
|
||
|
||
/// Fills `form`'s field `name` with `value` through the JavaScript runner.
|
||
fn fill_with(doc: &mut Document, name: &str, value: &str) {
|
||
let mut js = JsRunner::new(doc, "form.pdf", false);
|
||
pdfcraft_forms::set_value_with(doc, name, &FieldValue::Text(value.into()), &mut js).expect("fills");
|
||
}
|
||
|
||
/// Month names in the date language that no font of the field can draw are drawn in English,
|
||
/// never as `?` ("18-????.-26" in Russian); a font that holds them keeps them (#5303).
|
||
#[test]
|
||
fn date_names_no_field_font_can_draw_are_drawn_in_english() {
|
||
let _names = names_lock();
|
||
let date = "AFDate_FormatEx(\"dd-mmm-yy\");";
|
||
let fields = [("helv", "Helv", date), ("song", "Song", date)];
|
||
for (language, local) in [("ru", "окт."), ("cs", "říj"), ("ja", "10月"), ("el", "Οκτ")] {
|
||
use_form_date_language(language);
|
||
let mut doc = form(&fields);
|
||
fill(&mut doc, "helv");
|
||
fill(&mut doc, "song");
|
||
let helv = appearance(&doc, "helv");
|
||
assert!(contains(&helv, b"(18-Oct-26) Tj"), "{language}: {}", String::from_utf8_lossy(&helv));
|
||
assert!(!helv.contains(&b'?'), "{language}: {}", String::from_utf8_lossy(&helv));
|
||
assert!(pdfcraft_forms::undrawn_fields(&doc).is_empty(), "{language}");
|
||
let song = appearance(&doc, "song");
|
||
assert!(contains(&song, &ucs2(&format!("18-{local}-26"))), "{language} keeps its name: {song:?}");
|
||
}
|
||
// Names Helvetica holds are kept: Italian "ott".
|
||
use_form_date_language("it");
|
||
let mut doc = form(&fields);
|
||
fill(&mut doc, "helv");
|
||
assert!(contains(&appearance(&doc, "helv"), b"(18-ott-26) Tj"));
|
||
use_form_date_language("en");
|
||
}
|
||
|
||
/// A Format script's `util.printd` is run again with English names for a field whose fonts
|
||
/// can't draw the date language's, and kept where they can (#5303).
|
||
#[test]
|
||
fn format_scripts_print_dates_in_english_where_the_field_cannot_draw_them() {
|
||
let _names = names_lock();
|
||
let script = "event.value = event.value ? util.printd(\"dd-mmm-yy\", util.scand(\"dd-mm-yy\", event.value)) + \" !\" : \"\";";
|
||
let fields = [("helv", "Helv", script), ("song", "Song", script)];
|
||
use_form_date_language("ru");
|
||
let mut doc = form(&fields);
|
||
fill(&mut doc, "helv");
|
||
fill(&mut doc, "song");
|
||
let helv = appearance(&doc, "helv");
|
||
assert!(contains(&helv, b"(18-Oct-26 !) Tj"), "{}", String::from_utf8_lossy(&helv));
|
||
assert!(!helv.contains(&b'?'), "{}", String::from_utf8_lossy(&helv));
|
||
assert!(pdfcraft_forms::undrawn_fields(&doc).is_empty());
|
||
assert!(contains(&appearance(&doc, "song"), &ucs2("18-окт.-26 !")));
|
||
// A script whose text has no English either keeps it, reported as undrawn.
|
||
let greeting = [("hi", "Helv", "event.value = \"Привет \" + util.printd(\"mmm\", new Date(2026, 9, 18));")];
|
||
let mut doc = form(&greeting);
|
||
fill(&mut doc, "hi");
|
||
assert_eq!(pdfcraft_forms::undrawn_fields(&doc), [("hi".to_string(), 'П')]);
|
||
// The English run is the appearance's alone: scripts go on printing the date language.
|
||
assert_eq!(printd("dd-mmm-yy", "18-окт.-26"), "18-окт.-26|18-окт.-26");
|
||
use_form_date_language("en");
|
||
}
|
||
|
||
/// A Format script's English run is kept when it ran, even where it prints the stored value
|
||
/// again: a Helvetica field storing "18-Oct-26", printed again as `dd-mmm-yy`, showed
|
||
/// "18-????.-26" in Russian. A script that fails keeps what it showed, reported undrawn (#5303).
|
||
#[test]
|
||
fn an_english_run_that_prints_the_stored_value_is_drawn() {
|
||
let _names = names_lock();
|
||
let script = "event.value = util.printd(\"dd-mmm-yy\", util.scand(\"dd-mmm-yy\", event.value));";
|
||
use_form_date_language("ru");
|
||
let mut doc = form(&[("helv", "Helv", script), ("fails", "Helv", "throw new Error(\"no\");")]);
|
||
fill_with(&mut doc, "helv", "18-Oct-26");
|
||
let helv = appearance(&doc, "helv");
|
||
assert!(contains(&helv, b"(18-Oct-26) Tj"), "{}", String::from_utf8_lossy(&helv));
|
||
assert!(!helv.contains(&b'?'), "{}", String::from_utf8_lossy(&helv));
|
||
assert!(pdfcraft_forms::undrawn_fields(&doc).is_empty());
|
||
fill_with(&mut doc, "fails", "18-окт.-26");
|
||
assert_eq!(pdfcraft_forms::undrawn_fields(&doc), [("fails".to_string(), 'о')]);
|
||
use_form_date_language("en");
|
||
}
|
||
|
||
/// What `util.scand(fmt, text)` reads, as `yyyy-mm-dd`, or `null`.
|
||
fn scanned(fmt: &str, text: &str) -> String {
|
||
let doc = DocInfo { file_name: "form.pdf".into(), num_pages: 1, page: 0, ..Default::default() };
|
||
let script = format!("var d = util.scand('{fmt}', {text:?}); event.value = d ? util.printd('yyyy-mm-dd', d) : 'null';");
|
||
let o = run(&script, &Event::field("Format", "date", ""), &doc, &[], &[], Limits::default());
|
||
assert_eq!(o.error, None);
|
||
o.value
|
||
}
|
||
|
||
/// A short weekday typed without its dot ("mar" for French "mar.", Tuesday) is the weekday where
|
||
/// the format has one, not the English "Mar" (March) that is the only other name of its length:
|
||
/// "mar 6 janv 2026" read as no date in French and Romanian (#5303).
|
||
#[test]
|
||
fn a_short_weekday_typed_without_its_dot_is_the_weekday() {
|
||
let _names = names_lock();
|
||
let mut bad = Vec::new();
|
||
let cases = [
|
||
("fr", "ddd d mmm yyyy", "Mar 6 janv 2026", "2026-01-06"),
|
||
("fr", "ddd d mmm yyyy", "mar 17 mars 2026", "2026-03-17"),
|
||
("fr", "mmm d yyyy ddd", "janv 6 2026 mar", "2026-01-06"),
|
||
("fr", "ddd dd/mm/yyyy", "mar 06/01/2026", "2026-01-06"),
|
||
("ro", "ddd d mmm yyyy", "mar 6 ian 2026", "2026-01-06"),
|
||
("ro", "ddd dd/mm/yyyy", "mar 06/01/2026", "2026-01-06"),
|
||
];
|
||
for (language, fmt, text, want) in cases {
|
||
use_form_date_language(language);
|
||
let got = scanned(fmt, text);
|
||
if got != want {
|
||
bad.push(format!("{language} scand {fmt}: {text:?} -> {got}"));
|
||
}
|
||
let read = parse_date(text, fmt).map(|d| format!("{:04}-{:02}-{:02}", d.y, d.m, d.d));
|
||
if read.as_deref() != Some(want) {
|
||
bad.push(format!("{language} forms {fmt}: {text:?} -> {read:?}"));
|
||
}
|
||
}
|
||
use_form_date_language("en");
|
||
assert!(bad.is_empty(), "{}", bad.join("; "));
|
||
}
|
||
|
||
/// A weekday left out of a format with one ("17 mar 2026" for "ddd d mmm yyyy") lets a name the
|
||
/// date language shares between a weekday and a month be the month: Italian, Spanish and
|
||
/// Albanian "mar" (Tuesday, March) and Albanian "sht" (Saturday, September) were read as the
|
||
/// weekday, leaving no month (#5303). Typed weekdays still go first.
|
||
#[test]
|
||
fn a_shared_name_is_the_month_when_the_weekday_is_left_out() {
|
||
let _names = names_lock();
|
||
let mut bad = Vec::new();
|
||
let cases = [
|
||
("it", "ddd d mmm yyyy", "17 Mar 2026", (2026, 3, 17)),
|
||
("es", "ddd d mmm yyyy", "17 Mar 2026", (2026, 3, 17)),
|
||
("sq", "ddd d mmm yyyy", "17 Mar 2026", (2026, 3, 17)),
|
||
("it", "ddd d mmm yyyy", "17 mar 2026", (2026, 3, 17)),
|
||
("it", "dddd, mmmm d, yyyy", "mar 17, 2026", (2026, 3, 17)),
|
||
("it", "dddd, mmm d, yyyy", "mar 17 2026", (2026, 3, 17)),
|
||
("sq", "ddd d mmm yyyy", "5 sht 2026", (2026, 9, 5)),
|
||
("it", "ddd d mmm yyyy", "mar 17 Mar 2026", (2026, 3, 17)),
|
||
("it", "ddd d mmm yyyy", "mar 17 mar 2026", (2026, 3, 17)),
|
||
];
|
||
for (language, fmt, text, want) in cases {
|
||
use_form_date_language(language);
|
||
let read = parse_date(text, fmt).map(|d| (d.y, d.m, d.d));
|
||
if read != Some(want) {
|
||
bad.push(format!("{language} forms {fmt}: {text:?} -> {read:?}"));
|
||
}
|
||
if keystroke(&Format::Date(fmt.into()), "date", text).is_err() {
|
||
bad.push(format!("{language} keystroke {fmt}: {text:?} refused"));
|
||
}
|
||
}
|
||
use_form_date_language("en");
|
||
assert!(bad.is_empty(), "{}", bad.join("; "));
|
||
}
|
||
|
||
/// Turkish names typed in capitals are read: lower-casing letter by letter made "MAYIS" "mayis"
|
||
/// (not "mayıs"), "SALI" "sali" and "EKİM" "eki̇m" (an i with a dot above), so no name matched.
|
||
/// Dotted, dotless and capital i are one letter when names are read (#5303).
|
||
#[test]
|
||
fn turkish_names_typed_in_capitals_are_read() {
|
||
let _names = names_lock();
|
||
use_form_date_language("tr");
|
||
let mut bad = Vec::new();
|
||
for (fmt, text, want) in [
|
||
("d mmmm yyyy", "1 MAYIS 2026", (2026, 5, 1)),
|
||
("d mmmm yyyy", "1 EKİM 2026", (2026, 10, 1)),
|
||
("d mmmm yyyy", "1 Ekim 2026", (2026, 10, 1)),
|
||
("d mmm yyyy", "1 EKİ 2026", (2026, 10, 1)),
|
||
("dddd d mmmm yyyy", "SALI 6 OCAK 2026", (2026, 1, 6)),
|
||
("dddd d mmmm yyyy", "Salı 6 Ocak 2026", (2026, 1, 6)),
|
||
] {
|
||
let read = parse_date(text, fmt).map(|d| (d.y, d.m, d.d));
|
||
if read != Some(want) {
|
||
bad.push(format!("forms {fmt}: {text:?} -> {read:?}"));
|
||
}
|
||
let (y, m, d) = want;
|
||
let got = scanned(fmt, text);
|
||
if got != format!("{y:04}-{m:02}-{d:02}") {
|
||
bad.push(format!("scand {fmt}: {text:?} -> {got}"));
|
||
}
|
||
}
|
||
// English is read as before, capitals and all.
|
||
use_form_date_language("en");
|
||
for (fmt, text) in [("d mmmm yyyy", "1 APRIL 2026"), ("dddd d mmmm yyyy", "FRIDAY 1 MAY 2026"), ("d mmm yyyy", "1 MAY 2026")] {
|
||
if parse_date(text, fmt).is_none() || scanned(fmt, text) == "null" {
|
||
bad.push(format!("en {fmt}: {text:?}"));
|
||
}
|
||
}
|
||
assert!(bad.is_empty(), "{}", bad.join("; "));
|
||
}
|
||
|
||
/// #5464 review: Greek names typed in capitals end in a capital Σ, which is the final ς of the
|
||
/// name ("ΙΑΝΟΥΆΡΙΟΣ" for "Ιανουάριος"); they read as they did before names were folded.
|
||
#[test]
|
||
fn greek_names_typed_in_capitals_are_read() {
|
||
let _names = names_lock();
|
||
use_form_date_language("el");
|
||
let mut bad = Vec::new();
|
||
for (fmt, text, want) in [
|
||
("d mmmm yyyy", "1 ΙΑΝΟΥΆΡΙΟΣ 2026", (2026, 1, 1)),
|
||
("d mmmm yyyy", "1 Ιανουάριος 2026", (2026, 1, 1)),
|
||
("d mmmm yyyy", "1 ιανουάριοσ 2026", (2026, 1, 1)),
|
||
("mmmm d, yyyy", "ΜΆΙΟΣ 5, 2026", (2026, 5, 5)),
|
||
("dddd, mmmm d, yyyy", "FRIDAY, ΙΑΝΟΥΆΡΙΟΣ 2, 2026", (2026, 1, 2)),
|
||
] {
|
||
let read = parse_date(text, fmt).map(|d| (d.y, d.m, d.d));
|
||
if read != Some(want) {
|
||
bad.push(format!("forms {fmt}: {text:?} -> {read:?}"));
|
||
}
|
||
let (y, m, d) = want;
|
||
let got = scanned(fmt, text);
|
||
if got != format!("{y:04}-{m:02}-{d:02}") {
|
||
bad.push(format!("scand {fmt}: {text:?} -> {got}"));
|
||
}
|
||
}
|
||
assert!(bad.is_empty(), "{}", bad.join("; "));
|
||
}
|