diff options
| author | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-21 00:25:19 -0400 |
|---|---|---|
| committer | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-22 13:09:35 -0400 |
| commit | 515c44399843c62336a0102e933f9fbd7289743d (patch) | |
| tree | 97cb7a699872c7d235a5833933c415458aff074e | |
| parent | metadata: use document's language (diff) | |
odt: office:meta from the document, not the run
Three faults in four lines of the odt metadata.
meta:creation-date and dc:date were taken from generated_time, the run's
own clock, so two runs over unchanged markup produced different odt
bytes. They now carry the document's own instant, so an odt is a
function of its source. That was the last clock stamp in any writer:
every output format is now reproducible.
generated_time is a human-readable run stamp, of the form
"2026-9-17 [38/3] 18:39:35", with unpadded numbers and an iso week in
brackets. It is not a valid xsd:dateTime, which is what ODF asks for in
both of those fields, so what was written there was malformed as well as
unstable.
dc:language now carries the document's own language tag.
The instant is doc_matters.modified_utc, moved there from epub3.d
in this commit: epub3 needs the same value for its dcterms:modified,
and one definition cannot drift from the other. generated_time now
has no caller and is left in place.
(assisted by Claude-Code)
| -rw-r--r-- | org/out_odt.org | 15 | ||||
| -rw-r--r-- | org/out_xmls.org | 13 | ||||
| -rw-r--r-- | org/spine.org | 72 | ||||
| -rw-r--r-- | src/sisudoc/ocda/meta/doc_matters.d | 72 | ||||
| -rw-r--r-- | src/sisudoc/outputs/io_out/epub3.d | 73 | ||||
| -rw-r--r-- | src/sisudoc/outputs/io_out/odt.d | 15 |
6 files changed, 183 insertions, 77 deletions
diff --git a/org/out_odt.org b/org/out_odt.org index 4dbb549..b28e8d1 100644 --- a/org/out_odt.org +++ b/org/out_odt.org @@ -2544,7 +2544,13 @@ _images.join("\n"), string meta_xml(M)( auto ref M doc_matters, ) { - /+ (meta_xml includes output time-stamp) +/ + /+ ↓ office:meta carries the document's own date and language, not the run's + time-stamp. Both dates were taken from doc_matters.generated_time, which + is a human-readable run stamp ("2026-9-17 [38/3] 18:39:35") and not a + valid xsd:dateTime, which is what ODF asks for here; so the field was + malformed and different on every run. dc:language was a hard-coded + "en-US". + +/ string _meta_xml = format(q"┃<?xml version="1.0" encoding="UTF-8"?> #+END_SRC @@ -2555,7 +2561,7 @@ string meta_xml(M)( <meta:generator>%s</meta:generator> <meta:creation-date>%s</meta:creation-date> <dc:date>%s</dc:date> - <dc:language>en-US</dc:language> + <dc:language>%s</dc:language> </office:meta> </office:document-meta> #+END_SRC @@ -2564,8 +2570,9 @@ string meta_xml(M)( #+BEGIN_SRC d ┃", doc_matters.generator_program.name_and_version, -doc_matters.generated_time, -doc_matters.generated_time, +doc_matters.modified_utc, +doc_matters.modified_utc, +doc_matters.src.language_tag, ); return _meta_xml; } diff --git a/org/out_xmls.org b/org/out_xmls.org index db031e2..37daadf 100644 --- a/org/out_xmls.org +++ b/org/out_xmls.org @@ -2851,6 +2851,17 @@ string _epub_publication_uuid(string _doc_uid) { #+NAME: output_epub3_last_modified_time #+BEGIN_SRC d +/+ ↓ the last modified time EPUB3 requires, exactly one dcterms:modified on + the package in UTC to the second, comes from doc_matters.modified_utc. + Set in spine rather than here because an odt's office:meta needs the + same instant, and a single definition cannot drift apart. ++/ +#+END_SRC + +**** time unused + +#+NAME: output_epub3_last_modified_time_UNUSED +#+BEGIN_SRC d /+ ↓ the last modified time, as EPUB3 requires it: exactly one dcterms:modified on the package, in UTC to the second, CCYY-MM-DDThh:mm:ssZ . @@ -3013,7 +3024,7 @@ string epub3_oebps_content(D,P)(D doc, P parts) { ? "" : xhtml_format.special_characters_plain_date(doc.matters.conf_make_meta.meta.date_published), (doc.matters.conf_make_meta.meta.rights_copyright.empty) ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.rights_copyright), - _epub_modified_utc(doc.matters.conf_make_meta.meta), + doc.matters.modified_utc, _epub_a11y_metadata(doc), (pth_epub3.fn_oebps_css).chompPrefix("OEBPS/"), ); diff --git a/org/spine.org b/org/spine.org index c93e334..aa8b6df 100644 --- a/org/spine.org +++ b/org/spine.org @@ -2056,6 +2056,78 @@ template spineDocMatters() { } return Prog_(); } + /+ ↓ when the document is from, as a UTC instant, for the output formats + that must carry a timestamp: epub3 requires exactly one + dcterms:modified on the package, and an odt carries + meta:creation-date and dc:date. + . + Taken from the document rather than from the clock, so that those + artefacts are a function of their source. Stamped from the clock, + two runs over unchanged markup produced different bytes, and no + comparison of that output could tell a real change from the + seconds between two builds. + . + Read in order: SOURCE_DATE_EPOCH where the environment sets it, + which is the reproducible-builds convention and lets a build pin + every artefact of one run; then the document's own date.modified; + then date.published. The clock is the last resort, for a document + that says nothing about when it is from. + . + A document date is loose markup: "1991", "2006-03" and + "2021-12-00" all occur in the sample set, and a month or day of 00 + is not a date. A missing or zero part becomes 01, and a day out of + range for its month falls back to the first, so what is written is + always a valid CCYY-MM-DDThh:mm:ssZ. + +/ + string modified_utc() { + import std.datetime : Date, SysTime, unixTimeToStdTime; + import std.format : format; + import std.process : environment; + import std.regex : ctRegex, matchFirst; + /+ ↓ a build may pin every artefact of the run +/ + string _sde = environment.get("SOURCE_DATE_EPOCH", ""); + if (_sde.length > 0) { + try { + auto _t = SysTime(unixTimeToStdTime(_sde.to!long), UTC()); + return format("%04d-%02d-%02dT%02d:%02d:%02dZ", + _t.year, _t.month.to!int, _t.day, _t.hour, _t.minute, _t.second); + } catch (Exception) { + } + } + /+ ↓ what the document itself says +/ + auto _rgx_date + = ctRegex!(`^\s*(?P<y>\d{4})(?:[-/](?P<m>\d{1,2}))?(?:[-/](?P<d>\d{1,2}))?`); + foreach (_said; [ + conf_make_meta.meta.date_modified, + conf_make_meta.meta.date_published, + ]) { + if (_said.length == 0) { + continue; + } + if (auto _m = _said.matchFirst(_rgx_date)) { + int _y = _m["y"].to!int; + int _mo = (_m["m"].length > 0) ? _m["m"].to!int : 1; + int _d = (_m["d"].length > 0) ? _m["d"].to!int : 1; + if (_mo < 1 || _mo > 12) { + _mo = 1; + } + if (_d < 1) { + _d = 1; + } + try { + cast(void) Date(_y, _mo, _d); + } catch (Exception) { + _d = 1; + } + return format("%04d-%02d-%02dT00:00:00Z", _y, _mo, _d); + } + } + /+ ↓ the document says nothing: the run's own time +/ + auto _now = Clock.currTime(UTC()); + return format("%04d-%02d-%02dT%02d:%02d:%02dZ", + _now.year, _now.month.to!int, _now.day, + _now.hour, _now.minute, _now.second); + } auto generated_time() { auto _st = Clock.currTime(UTC()); auto _time = _st.year.to!string diff --git a/src/sisudoc/ocda/meta/doc_matters.d b/src/sisudoc/ocda/meta/doc_matters.d index 19e3d0f..ff528d5 100644 --- a/src/sisudoc/ocda/meta/doc_matters.d +++ b/src/sisudoc/ocda/meta/doc_matters.d @@ -116,6 +116,78 @@ template spineDocMatters() { } return Prog_(); } + /+ ↓ when the document is from, as a UTC instant, for the output formats + that must carry a timestamp: epub3 requires exactly one + dcterms:modified on the package, and an odt carries + meta:creation-date and dc:date. + . + Taken from the document rather than from the clock, so that those + artefacts are a function of their source. Stamped from the clock, + two runs over unchanged markup produced different bytes, and no + comparison of that output could tell a real change from the + seconds between two builds. + . + Read in order: SOURCE_DATE_EPOCH where the environment sets it, + which is the reproducible-builds convention and lets a build pin + every artefact of one run; then the document's own date.modified; + then date.published. The clock is the last resort, for a document + that says nothing about when it is from. + . + A document date is loose markup: "1991", "2006-03" and + "2021-12-00" all occur in the sample set, and a month or day of 00 + is not a date. A missing or zero part becomes 01, and a day out of + range for its month falls back to the first, so what is written is + always a valid CCYY-MM-DDThh:mm:ssZ. + +/ + string modified_utc() { + import std.datetime : Date, SysTime, unixTimeToStdTime; + import std.format : format; + import std.process : environment; + import std.regex : ctRegex, matchFirst; + /+ ↓ a build may pin every artefact of the run +/ + string _sde = environment.get("SOURCE_DATE_EPOCH", ""); + if (_sde.length > 0) { + try { + auto _t = SysTime(unixTimeToStdTime(_sde.to!long), UTC()); + return format("%04d-%02d-%02dT%02d:%02d:%02dZ", + _t.year, _t.month.to!int, _t.day, _t.hour, _t.minute, _t.second); + } catch (Exception) { + } + } + /+ ↓ what the document itself says +/ + auto _rgx_date + = ctRegex!(`^\s*(?P<y>\d{4})(?:[-/](?P<m>\d{1,2}))?(?:[-/](?P<d>\d{1,2}))?`); + foreach (_said; [ + conf_make_meta.meta.date_modified, + conf_make_meta.meta.date_published, + ]) { + if (_said.length == 0) { + continue; + } + if (auto _m = _said.matchFirst(_rgx_date)) { + int _y = _m["y"].to!int; + int _mo = (_m["m"].length > 0) ? _m["m"].to!int : 1; + int _d = (_m["d"].length > 0) ? _m["d"].to!int : 1; + if (_mo < 1 || _mo > 12) { + _mo = 1; + } + if (_d < 1) { + _d = 1; + } + try { + cast(void) Date(_y, _mo, _d); + } catch (Exception) { + _d = 1; + } + return format("%04d-%02d-%02dT00:00:00Z", _y, _mo, _d); + } + } + /+ ↓ the document says nothing: the run's own time +/ + auto _now = Clock.currTime(UTC()); + return format("%04d-%02d-%02dT%02d:%02d:%02dZ", + _now.year, _now.month.to!int, _now.day, + _now.hour, _now.minute, _now.second); + } auto generated_time() { auto _st = Clock.currTime(UTC()); auto _time = _st.year.to!string diff --git a/src/sisudoc/outputs/io_out/epub3.d b/src/sisudoc/outputs/io_out/epub3.d index 7ffe978..211176e 100644 --- a/src/sisudoc/outputs/io_out/epub3.d +++ b/src/sisudoc/outputs/io_out/epub3.d @@ -250,74 +250,11 @@ template outputEPub3() { auto _ns = sha1UUID("sisudoc.org", dnsNamespace); return sha1UUID(_doc_uid, _ns).toString; } - /+ ↓ the last modified time, as EPUB3 requires it: exactly one dcterms:modified - on the package, in UTC to the second, CCYY-MM-DDThh:mm:ssZ - . - Taken from the document rather than from the clock, so that an epub is - a function of its source and not of the moment it was built. Read in - order: SOURCE_DATE_EPOCH where the environment sets it, which is the - reproducible-builds convention and lets a packager pin every artefact - of one run; then the document's own date.modified; then - date.published. The clock is the last resort, for a document that says - nothing about when it is from. - . - Before this, two runs over unchanged markup produced different epub - bytes, so no comparison of epub output could tell a real change from - the seconds between two builds. - . - A document date is loose markup: "1991", "2006-03" and "2021-12-00" - all occur in the sample set, and a month or day of 00 is not a date. - A missing or zero part becomes 01, and a day out of range for its - month falls back to the first, so what is written is always a valid - timestamp. + /+ ↓ the last modified time EPUB3 requires, exactly one dcterms:modified on + the package in UTC to the second, comes from doc_matters.modified_utc. + Set in spine rather than here because an odt's office:meta needs the + same instant, and a single definition cannot drift apart. +/ - string _epub_modified_utc(M)(M _meta) { - import std.datetime : Clock, UTC, SysTime, Date, unixTimeToStdTime; - import std.process : environment; - import std.regex : ctRegex, matchFirst; - string _stamp(int _y, int _mo, int _d) { - return format("%04d-%02d-%02dT00:00:00Z", _y, _mo, _d); - } - /+ ↓ a build may pin every artefact of the run +/ - string _sde = environment.get("SOURCE_DATE_EPOCH", ""); - if (_sde.length > 0) { - try { - auto _t = SysTime(unixTimeToStdTime(_sde.to!long), UTC()); - return format("%04d-%02d-%02dT%02d:%02d:%02dZ", - _t.year, _t.month.to!int, _t.day, _t.hour, _t.minute, _t.second); - } catch (Exception) { - } - } - /+ ↓ what the document itself says +/ - static auto _rgx_date - = ctRegex!(`^\s*(?P<y>\d{4})(?:[-/](?P<m>\d{1,2}))?(?:[-/](?P<d>\d{1,2}))?`); - foreach (_said; [_meta.date_modified, _meta.date_published]) { - if (_said.length == 0) { - continue; - } - if (auto _m = _said.matchFirst(_rgx_date)) { - int _y = _m["y"].to!int; - int _mo = (_m["m"].length > 0) ? _m["m"].to!int : 1; - int _d = (_m["d"].length > 0) ? _m["d"].to!int : 1; - if (_mo < 1 || _mo > 12) { - _mo = 1; - } - if (_d < 1) { - _d = 1; - } - try { - cast(void) Date(_y, _mo, _d); - } catch (Exception) { - _d = 1; - } - return _stamp(_y, _mo, _d); - } - } - /+ ↓ the document says nothing: the run's own time, as before +/ - auto _t = Clock.currTime(UTC()); - return format("%04d-%02d-%02dT%02d:%02d:%02dZ", - _t.year, _t.month.to!int, _t.day, _t.hour, _t.minute, _t.second); - } string epub3_mimetypes() { string o; o = format(q"┃application/epub+zip┃") ~ "\n"; @@ -373,7 +310,7 @@ template outputEPub3() { ? "" : xhtml_format.special_characters_plain_date(doc.matters.conf_make_meta.meta.date_published), (doc.matters.conf_make_meta.meta.rights_copyright.empty) ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.rights_copyright), - _epub_modified_utc(doc.matters.conf_make_meta.meta), + doc.matters.modified_utc, _epub_a11y_metadata(doc), (pth_epub3.fn_oebps_css).chompPrefix("OEBPS/"), ); diff --git a/src/sisudoc/outputs/io_out/odt.d b/src/sisudoc/outputs/io_out/odt.d index 711be50..40d760d 100644 --- a/src/sisudoc/outputs/io_out/odt.d +++ b/src/sisudoc/outputs/io_out/odt.d @@ -959,20 +959,27 @@ template outputODT() { string meta_xml(M)( auto ref M doc_matters, ) { - /+ (meta_xml includes output time-stamp) +/ + /+ ↓ office:meta carries the document's own date and language, not the run's + time-stamp. Both dates were taken from doc_matters.generated_time, which + is a human-readable run stamp ("2026-9-17 [38/3] 18:39:35") and not a + valid xsd:dateTime, which is what ODF asks for here; so the field was + malformed and different on every run. dc:language was a hard-coded + "en-US". + +/ string _meta_xml = format(q"┃<?xml version="1.0" encoding="UTF-8"?> <office:document-meta xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:xl="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:grddl="http://www.w3.org/2003/g/data-view#" office:version="1.2"> <office:meta> <meta:generator>%s</meta:generator> <meta:creation-date>%s</meta:creation-date> <dc:date>%s</dc:date> - <dc:language>en-US</dc:language> + <dc:language>%s</dc:language> </office:meta> </office:document-meta> ┃", doc_matters.generator_program.name_and_version, - doc_matters.generated_time, - doc_matters.generated_time, + doc_matters.modified_utc, + doc_matters.modified_utc, + doc_matters.src.language_tag, ); return _meta_xml; } |
