mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
M12: a date typed by another of an XFA field's edit patterns is kept in its first
This commit is contained in:
@@ -2428,10 +2428,20 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) ->
|
||||
Edit::SetAnnotationInfo { page, index, author, subject, icon } => {
|
||||
pdfcraft_annot::set_info(doc, *page, *index, author.as_deref(), subject.as_deref(), *icon, &cx.meta())?;
|
||||
}
|
||||
Edit::SetFieldValue { name, value } => match cx.js.as_mut() {
|
||||
Some(js) => pdfcraft_forms::set_value_with(doc, name, value, js)?,
|
||||
None => pdfcraft_forms::set_value(doc, name, value)?,
|
||||
},
|
||||
Edit::SetFieldValue { name, value } => {
|
||||
let mut set = |doc: &mut pdfcraft_cos::Document, value: &FieldValue| match cx.js.as_mut() {
|
||||
Some(js) => pdfcraft_forms::set_value_with(doc, name, value, js),
|
||||
None => pdfcraft_forms::set_value(doc, name, value),
|
||||
};
|
||||
if let Err(refused) = set(doc, value) {
|
||||
// A date the field's pattern doesn't read may be typed by another of an XFA date
|
||||
// field's patterns: it is kept in the first.
|
||||
match xfa::typed_date(doc, cx.xfa.as_deref(), name, value) {
|
||||
Some(typed) => set(doc, &typed)?,
|
||||
None => return Err(refused.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Edit::ApplyScriptChanges { changes } => match cx.js.as_mut() {
|
||||
Some(js) => pdfcraft_forms::apply_script_changes(doc, changes, js)?,
|
||||
None => pdfcraft_forms::apply_script_changes(doc, changes, &mut pdfcraft_forms::NoScripts)?,
|
||||
|
||||
@@ -6153,3 +6153,23 @@ fn undo_never_makes_an_unsaved_document_clean() {
|
||||
s.undo(new).unwrap();
|
||||
assert!(!s.get(new).unwrap().dirty, "once saved, undo back to the saved state is clean");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_date_typed_by_another_of_an_xfa_fields_edit_patterns_is_kept_in_its_first() {
|
||||
let mut s = Session::new();
|
||||
let id = s
|
||||
.open("dates.pdf", None, Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::date_alternatives_template())), None)
|
||||
.expect("opens");
|
||||
let value = |s: &Session| s.get(id).unwrap().form.iter().find(|f| f.name == "leave").unwrap().value.clone();
|
||||
s.apply(id, Edit::SetFieldValue { name: "leave".into(), value: FieldValue::Text("261018".into()) }).expect("yymmdd is one of its patterns");
|
||||
assert_eq!(value(&s), ["20261018"]);
|
||||
s.apply(id, Edit::SetFieldValue { name: "leave".into(), value: FieldValue::Text("2026118".into()) }).expect("so is yyyymd");
|
||||
assert_eq!(value(&s), ["20261108"]);
|
||||
let refused = s.apply(id, Edit::SetFieldValue { name: "leave".into(), value: FieldValue::Text("2618".into()) });
|
||||
assert!(refused.is_err_and(|e| e.to_string().contains("yyyymmdd")), "none of its patterns reads it");
|
||||
assert_eq!(value(&s), ["20261108"]);
|
||||
// The data holds the date.
|
||||
let cos = pdfcraft_cos::Document::open(s.save_bytes(id).unwrap()).unwrap();
|
||||
let data = pdfcraft_xfa::parse_datasets(&pdfcraft_xfa::read_packets(&cos).unwrap().unwrap().xdp).expect("a datasets packet");
|
||||
assert_eq!(data.text_at(&pdfcraft_xfa::som_to_path("form[0].leave[0]")), Some("2026-11-08"));
|
||||
}
|
||||
|
||||
@@ -810,6 +810,28 @@ pub(crate) fn on_data_replaced(doc: &mut pdfcraft_cos::Document, tpl: &Template,
|
||||
r.finish().map(|_| ())
|
||||
}
|
||||
|
||||
/// A text value typed into the laid-out XFA date field `name` by another of its edit picture's
|
||||
/// patterns, written in the first ([`pdfcraft_xfa::typed_date`]); `None` for anything else.
|
||||
/// `tpl` is the template the scripts use, when they run; otherwise it is read from `doc`.
|
||||
pub(crate) fn typed_date(
|
||||
doc: &pdfcraft_cos::Document,
|
||||
tpl: Option<&Template>,
|
||||
name: &str,
|
||||
value: &pdfcraft_forms::FieldValue,
|
||||
) -> Option<pdfcraft_forms::FieldValue> {
|
||||
let pdfcraft_forms::FieldValue::Text(text) = value else { return None };
|
||||
let som = pdfcraft_xfa::fields_by_som(doc).into_iter().find_map(|(som, n)| (n == name).then_some(som))?;
|
||||
let read;
|
||||
let tpl = match tpl {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
read = pdfcraft_xfa::template_of(doc).ok()?;
|
||||
&read
|
||||
}
|
||||
};
|
||||
pdfcraft_xfa::typed_date(tpl, &som, text).map(pdfcraft_forms::FieldValue::Text)
|
||||
}
|
||||
|
||||
/// A button's click script (or any event, by SOM path and activity), then calculations.
|
||||
pub(crate) fn on_event(doc: &mut pdfcraft_cos::Document, tpl: &Template, som: &str, activity: &str, run: &mut XfaRun) -> Result<(), String> {
|
||||
let mut r = Runner::new(doc, tpl, run, EVENT_BUDGET_MS);
|
||||
|
||||
@@ -375,6 +375,24 @@ pub fn fields_template(data: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// Date fields whose edit pictures list alternatives, as forms let a date be typed several ways
|
||||
/// (`date{YYYYMMDD}|date{YYMMDD}|…`); one with a single pattern; a text field.
|
||||
pub fn date_alternatives_template() -> String {
|
||||
r##"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
|
||||
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
|
||||
<subform name="form" layout="tb">
|
||||
<pageSet><pageArea name="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||||
<field name="leave" w="2in" h="0.3in"><ui><dateTimeEdit/><picture>date{YYYYMMDD}|date{DDMMMYY}|date{YYMMDD}|date{YYYYMMD}|date{YY MM DD}</picture></ui><value><date/></value></field>
|
||||
<field name="born" w="2in" h="0.3in"><ui><dateTimeEdit/><picture>date{YYYY-MM-DD}</picture></ui><value><date/></value></field>
|
||||
<field name="name" w="2in" h="0.3in"><ui><textEdit/></ui></field>
|
||||
</subform>
|
||||
</template>
|
||||
</xdp:xdp>
|
||||
"##
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// A form of `sections` sections of `rows` rows, each row with a calculated total (half in
|
||||
/// JavaScript, half in FormCalc) and a section sum over its rows: 2 × sections × rows + sections
|
||||
/// calculate scripts, every one reading values the earlier ones settled.
|
||||
|
||||
@@ -395,7 +395,7 @@ fn is_row(node: &Node) -> bool {
|
||||
}
|
||||
|
||||
/// The Acrobat date pattern for an XFA picture clause such as `date{YYYY-MM-DD}`.
|
||||
fn date_pattern(picture: Option<&str>) -> String {
|
||||
pub(crate) fn date_pattern(picture: Option<&str>) -> String {
|
||||
picture.and_then(date_letters).unwrap_or_else(|| "yyyy-mm-dd".into())
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ pub mod model;
|
||||
pub mod packets;
|
||||
pub mod parse;
|
||||
pub mod pdf;
|
||||
pub mod picture;
|
||||
pub mod script;
|
||||
pub mod text;
|
||||
|
||||
@@ -99,6 +100,7 @@ pub use layout::{Action, BorderShape, Form, Item, MAX_PAGES, Page, Widget, Widge
|
||||
pub use packets::{Encoding, Packets, decode as decode_packet, encode as encode_packet, read_packets};
|
||||
pub use parse::{measure, parse};
|
||||
pub use pdf::{CLICK_KEY, LAYOUT_KEY, SOM_KEY, Stripped, XfaShape, existing_layout, strip, xfa_shape};
|
||||
pub use picture::typed_date;
|
||||
pub use script::{
|
||||
FormNode, LiveForm, MAX_FORM_NODES, NodeKind, OVERRIDES_KEY, Overrides, ScriptEvent, apply_overrides, data_of, fields_by_som, form_tree,
|
||||
has_scripts, overrides, refresh_value, rerender, set_overrides, template_of,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//! Picture clauses that list alternatives (`date{YYYYMMDD}|date{YYMMDD}`): a date may be typed
|
||||
//! by any of them, and is shown, and kept in the field, by the first.
|
||||
|
||||
use crate::data::{date_letters, iso_to_pattern, pattern_to_iso, template_field};
|
||||
use crate::layout::date_pattern;
|
||||
use crate::model::{Template, Ui};
|
||||
|
||||
/// The alternatives of a picture clause: its parts between `|`s that aren't inside braces or
|
||||
/// quotes.
|
||||
pub(crate) fn alternatives(picture: &str) -> Vec<&str> {
|
||||
let mut out = Vec::new();
|
||||
let (mut depth, mut quoted, mut start) = (0usize, false, 0);
|
||||
for (i, c) in picture.char_indices() {
|
||||
match c {
|
||||
'\'' => quoted = !quoted,
|
||||
'{' if !quoted => depth = depth.saturating_add(1),
|
||||
'}' if !quoted => depth = depth.saturating_sub(1),
|
||||
'|' if !quoted && depth == 0 => {
|
||||
out.extend(picture.get(start..i));
|
||||
start = i + 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out.extend(picture.get(start..));
|
||||
out
|
||||
}
|
||||
|
||||
/// `text` typed into the date field at `som` by one of its edit picture's alternatives after the
|
||||
/// first, written in the first, which the field shows and checks typed dates against (`261018`
|
||||
/// becomes `20261018` for `date{YYYYMMDD}|date{YYMMDD}`). A pattern reads the text only when the
|
||||
/// date it gives, written in that pattern, is the text again. `None` when the field isn't a date
|
||||
/// field with alternatives, the first pattern reads the text, or no other does.
|
||||
pub fn typed_date(tpl: &Template, som: &str, text: &str) -> Option<String> {
|
||||
let f = template_field(tpl, som)?;
|
||||
if f.ui != Ui::DateTimeEdit {
|
||||
return None;
|
||||
}
|
||||
let picture = f.picture.as_deref()?;
|
||||
let reads = |pattern: &str| pattern_to_iso(text, pattern).filter(|iso| iso_to_pattern(iso, pattern).eq_ignore_ascii_case(text.trim()));
|
||||
let first = date_pattern(Some(picture));
|
||||
if reads(&first).is_some() {
|
||||
return None;
|
||||
}
|
||||
let iso = alternatives(picture).into_iter().skip(1).filter_map(date_letters).find_map(|p| reads(&p))?;
|
||||
Some(iso_to_pattern(&iso, &first))
|
||||
}
|
||||
@@ -2141,3 +2141,18 @@ fn som_indices_count_earlier_same_named_siblings() {
|
||||
let sibs = SomIndices::of(&children);
|
||||
assert_eq!((0..=children.len()).map(|i| sibs.at(i)).collect::<Vec<_>>(), [0, 0, 1, 0, 2, 1, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_date_typed_by_another_of_its_edit_pictures_patterns_is_written_in_the_first() {
|
||||
let (tpl, _) = crate::parse(&crate::fixtures::date_alternatives_template()).unwrap();
|
||||
let typed = |som: &str, text: &str| crate::typed_date(&tpl, som, text);
|
||||
let leave = "form[0].leave[0]";
|
||||
assert_eq!(typed(leave, "261018").as_deref(), Some("20261018"), "by yymmdd");
|
||||
assert_eq!(typed(leave, "2026118").as_deref(), Some("20261108"), "by yyyymd");
|
||||
assert_eq!(typed(leave, "26 10 18").as_deref(), Some("20261018"), "by yy mm dd");
|
||||
assert_eq!(typed(leave, "20261018"), None, "the first pattern reads it: it stays as typed");
|
||||
assert_eq!(typed(leave, "not a date"), None);
|
||||
assert_eq!(typed(leave, "2618"), None, "no pattern has room for four digits");
|
||||
assert_eq!(typed("form[0].born[0]", "181026"), None, "one pattern: nothing else to try");
|
||||
assert_eq!(typed("form[0].name[0]", "261018"), None, "not a date field");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user