aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRalph Amissah <ralph.amissah@gmail.com>2026-09-21 00:25:19 -0400
committerRalph Amissah <ralph.amissah@gmail.com>2026-09-22 13:09:35 -0400
commit515c44399843c62336a0102e933f9fbd7289743d (patch)
tree97cb7a699872c7d235a5833933c415458aff074e
parentmetadata: 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.org15
-rw-r--r--org/out_xmls.org13
-rw-r--r--org/spine.org72
-rw-r--r--src/sisudoc/ocda/meta/doc_matters.d72
-rw-r--r--src/sisudoc/outputs/io_out/epub3.d73
-rw-r--r--src/sisudoc/outputs/io_out/odt.d15
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;
}