Hello everyone,
the attached patches implement the feature I proposed in this thread
partially: For `org-html-doctype' "html5" and `org-html-html5-fancy',
use the semantic <time> element to wrap timestamps and populate its
datetime attribute with a valid date/date and time string according to
the HTML Standard. The same should also be done for the duration of
clock elements, but I think this is good point to get some feedback on
my first change.
The first patches add tests for timestamp rendering (which was untested
before). Then I move rendering of the <span class="timestamp"> elements
into a single function and finally add support for <time>.
Just make the timestamp format customizeable.
As for different timezone settings - Org implicitly uses current
timezone, so it is not an error to include it.
If you make the format customizeable, do not put timezone it in by
default, and leave the version with timezone as one of the available
custom options, it should be good enough.
I ended up doing just that via
`org-html-datetime-attribute-date-timestamp-format' and
`org-html-datetime-attribute-full-timestamp-format'. I haven't put much
time into naming, so they are super clunky and I would love some
suggestions here. Also I wasn't sure what Emacs version would be correct
to put in :version, so that's still missing.
~lukas
PS: My FSF Copyright Assignment paperwork has been completed.
From f825acf3f6687ac64311faec0983bcb12c245e94 Mon Sep 17 00:00:00 2001
From: Lukas Epple <em...@lukasepple.de>
Date: Wed, 25 Jun 2025 15:45:43 +0200
Subject: [PATCH 5/5] lisp/ox-html.el: use time element for timestamps in fancy
html5
* ox-html.el (org-html--format-timestamp): use time html element and its
datetime attribute when org-html--html5-fancy-p.
(org-html-datetime-attribute-date-timestamp-format,
org-html-datetime-attribute-full-timestamp-format): customizable format
strings for use by org-html--format-timestamp to render the value of the
datetime attribute.
* testing/lisp/test-ox-html.el (ox-html/html5-fancy-timestamps): add
simple test for the changed behavior (which only covers the behavior of
org-html-timestamp).
* ORG-NEWS: describe changed behavior of timestamp formatting with fancy
HTML5 rendering.
---
etc/ORG-NEWS | 10 +++++++++
lisp/ox-html.el | 42 ++++++++++++++++++++++++++++++------
testing/lisp/test-ox-html.el | 32 +++++++++++++++++++++------
3 files changed, 71 insertions(+), 13 deletions(-)
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 62502a678..08d5a1318 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -640,6 +640,16 @@ will be defined as empty and not produce any metadata if their
corresponding ~org-latex-with-author~, ~org-latex-with-title~, or
~org-latex-with-creator~ option is set to ~nil~.
+*** Fancy HTML5 export uses ~<time>~ element for timestamps
+Previously, timestamps would always be rendered inside a ~<span
+class="timestamp">~. If both ~org-html-doctype~ is ~html5~ and
+~org-html-html5-fancy~ is enabled, org will now use the semantically
+appropriate ~<time>~ element. It also will use the ~timestamp~ class,
+but additionally set the ~datetime~ attribute with a machine readable
+variant of the timestamp. The format used for the attribute can be
+customized using ~org-html-datetime-attribute-date-timestamp-format~
+and ~org-html-datetime-attribute-full-timestamp-format~.
+
* Version 9.7
** Important announcements and breaking changes
diff --git a/lisp/ox-html.el b/lisp/ox-html.el
index 8fa43bab2..819124d82 100644
--- a/lisp/ox-html.el
+++ b/lisp/ox-html.el
@@ -1168,6 +1168,24 @@ See `format-time-string' for more information on its components."
:package-version '(Org . "8.0")
:type 'string)
+(defcustom org-html-datetime-attribute-date-timestamp-format "%F"
+ "Format used for date (only) metadata attached to the time HTML element.
+Note that the format must be a valid date string according to the HTML
+Standard. See `format-time-string' for more information on its
+components."
+ :group 'org-export-html
+ :type 'string
+ :package-version '(Org . "9.8"))
+
+(defcustom org-html-datetime-attribute-full-timestamp-format "%FT%T"
+ "Format used for date and time metadata attached to the HTML time element.
+Note that the format must be a valid local date and time or valid global
+date and time string according to the HTML Standard. See `format-time-string'
+for more information on its components."
+ :group 'org-export-html
+ :type 'string
+ :package-version '(Org . "9.8"))
+
;;;; Template :: Mathjax
(defcustom org-html-mathjax-options
@@ -1811,12 +1829,24 @@ a value to `org-html-standalone-image-predicate'."
"Format given TIMESTAMP for inclusion in an HTML document.
INFO is a plist used as a communication channel. Formatted timestamp
will be wrapped in an element with class timestamp."
- (replace-regexp-in-string
- "--"
- "–" ; EN DASH
- (format "<span class=\"timestamp\">%s</span>"
- (org-html-plain-text (org-timestamp-translate timestamp)
- info))))
+ (let ((html-tag (if (org-html--html5-fancy-p info) "time" "span"))
+ (html-attrs (concat "class=\"timestamp\""
+ (when (org-html--html5-fancy-p info)
+ (format " datetime=\"%s\""
+ (org-format-timestamp
+ timestamp
+ (if (org-timestamp-has-time-p timestamp)
+ org-html-datetime-attribute-full-timestamp-format
+ org-html-datetime-attribute-date-timestamp-format)))))))
+ (replace-regexp-in-string
+ "--"
+ "–" ; EN DASH
+ (format "<%s %s>%s</%s>"
+ html-tag
+ html-attrs
+ (org-html-plain-text (org-timestamp-translate timestamp)
+ info)
+ html-tag))))
;;;; Table
diff --git a/testing/lisp/test-ox-html.el b/testing/lisp/test-ox-html.el
index 458747c95..50d751f6e 100644
--- a/testing/lisp/test-ox-html.el
+++ b/testing/lisp/test-ox-html.el
@@ -949,19 +949,37 @@ SCHEDULED: <2025-03-26 Wed> DEADLINE: <2025-03-27 Thu 13:00> CLOSED: [2025-03-25
"<span class=\"timestamp-kwd\">DEADLINE:</span> <span class=\"timestamp\"><2025-03-27 Thu 13:00> </span>"
"<span class=\"timestamp-kwd\">SCHEDULED:</span> <span class=\"timestamp\"><2025-03-26 Wed> </span>"))))))
+(ert-deftest ox-html/html5-fancy-timestamps ()
+ "Test rendering of timestamps with fancy HTML5 enabled"
+ (org-test-with-temp-text "
+[2025-06-25 Wed]
+<2025-06-25 Wed 19:10>
+"
+ (let ((export-buffer "*Test HTML Export")
+ (org-export-show-temporary-buffer nil)
+ (org-html-doctype "html5")
+ (org-html-html5-fancy t))
+ (org-export-to-buffer 'html export-buffer
+ nil nil nil t)
+ (with-current-buffer export-buffer
+ (mapc (lambda (s)
+ (should (= 1 (how-many (rx-to-string s)))))
+ '("<span class=\"timestamp-wrapper\"><time class=\"timestamp\" datetime=\"2025-06-25\">[2025-06-25 Wed]</time></span>"
+ "<span class=\"timestamp-wrapper\"><time class=\"timestamp\" datetime=\"2025-06-25T19:10:00\"><2025-06-25 Wed 19:10></time></span>"))))))
+
;;; Postamble Format
(ert-deftest ox-html/postamble-default ()
"Test default postamble"
(org-test-with-temp-text "Test, hi"
- (let ((export-buffer "*Test HTML Export*")
- (org-export-show-temporary-export-buffer nil))
- (org-export-to-buffer 'html export-buffer
- nil nil nil nil nil)
- (with-current-buffer export-buffer
- (should (= 1 (how-many "Validate")))
- (should (= 1 (how-many "Created: ")))))))
+ (let ((export-buffer "*Test HTML Export*")
+ (org-export-show-temporary-export-buffer nil))
+ (org-export-to-buffer 'html export-buffer
+ nil nil nil nil nil)
+ (with-current-buffer export-buffer
+ (should (= 1 (how-many "Validate")))
+ (should (= 1 (how-many "Created: ")))))))
(ert-deftest ox-html/postamble-custom ()
--
2.49.0
From d55f98880792683c32aad5a902ffac86cf59f933 Mon Sep 17 00:00:00 2001
From: Lukas Epple <em...@lukasepple.de>
Date: Fri, 21 Feb 2025 17:12:39 +0100
Subject: [PATCH 4/5] lisp/ox-html.el: unify timestamp formatting
* ox-html.el (org-html--format-timestamp): add new function for handling
the creation of <span class="timestamp"> elements.
(org-html-clock): use org-html--format-timestamp, except for the duration
string which can't use the same code.
(org-html-planning, org-html-timestamp): use org-html--format-timestamp
* test-ox-html.el (ox-html/clock): update expected output since
org-html--format-timestamp replaces -- with the EN DASH.
---
lisp/ox-html.el | 29 +++++++++++++++++++----------
testing/lisp/test-ox-html.el | 2 +-
2 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/lisp/ox-html.el b/lisp/ox-html.el
index 1fee9f05d..8fa43bab2 100644
--- a/lisp/ox-html.el
+++ b/lisp/ox-html.el
@@ -1807,6 +1807,17 @@ is meant to be used as a predicate for `org-export-get-ordinal' or
a value to `org-html-standalone-image-predicate'."
(org-element-property :caption element))
+(defun org-html--format-timestamp (timestamp info)
+ "Format given TIMESTAMP for inclusion in an HTML document.
+INFO is a plist used as a communication channel. Formatted timestamp
+will be wrapped in an element with class timestamp."
+ (replace-regexp-in-string
+ "--"
+ "–" ; EN DASH
+ (format "<span class=\"timestamp\">%s</span>"
+ (org-html-plain-text (org-timestamp-translate timestamp)
+ info))))
+
;;;; Table
(defun org-html-htmlize-region-for-paste (beg end)
@@ -2651,17 +2662,17 @@ holding contextual information."
;;;; Clock
-(defun org-html-clock (clock _contents _info)
+(defun org-html-clock (clock _contents info)
"Transcode a CLOCK element from Org to HTML.
CONTENTS is nil. INFO is a plist used as a communication
channel."
(format "<p>
<span class=\"timestamp-wrapper\">
-<span class=\"timestamp-kwd\">%s</span> <span class=\"timestamp\">%s</span>%s
+<span class=\"timestamp-kwd\">%s</span> %s%s
</span>
</p>"
org-clock-string
- (org-timestamp-translate (org-element-property :value clock))
+ (org-html--format-timestamp (org-element-property :value clock) info)
(let ((time (org-element-property :duration clock)))
(and time (format " <span class=\"timestamp\">(%s)</span>" time)))))
@@ -3577,10 +3588,9 @@ channel."
(when timestamp
(let ((string (car pair)))
(format "<span class=\"timestamp-kwd\">%s</span> \
-<span class=\"timestamp\">%s</span> "
+%s "
string
- (org-html-plain-text (org-timestamp-translate timestamp)
- info))))))
+ (org-html--format-timestamp timestamp info))))))
`((,org-closed-string . ,(org-element-property :closed planning))
(,org-deadline-string . ,(org-element-property :deadline planning))
(,org-scheduled-string . ,(org-element-property :scheduled planning)))
@@ -3939,10 +3949,9 @@ information."
(timestamp-no-blank
(org-element-put-property
(org-element-copy timestamp t)
- :post-blank 0))
- (value (org-html-plain-text (org-timestamp-translate timestamp-no-blank) info)))
- (format "<span class=\"timestamp-wrapper\"><span class=\"timestamp\">%s</span></span>"
- (replace-regexp-in-string "--" "–" value))))
+ :post-blank 0)))
+ (format "<span class=\"timestamp-wrapper\">%s</span>"
+ (org-html--format-timestamp timestamp-no-blank info))))
;;;; Underline
diff --git a/testing/lisp/test-ox-html.el b/testing/lisp/test-ox-html.el
index db815b62a..458747c95 100644
--- a/testing/lisp/test-ox-html.el
+++ b/testing/lisp/test-ox-html.el
@@ -929,7 +929,7 @@ CLOCK: [2025-02-21 Fri 17:43]--[2025-02-21 Fri 17:48] => 0:05
(should (= 1
(how-many
(rx-to-string
- "<span class=\"timestamp-kwd\">CLOCK:</span> <span class=\"timestamp\">[2025-02-21 Fri 17:43]--[2025-02-21 Fri 17:48] </span> <span class=\"timestamp\">(0:05)</span>"))))))))
+ "<span class=\"timestamp-kwd\">CLOCK:</span> <span class=\"timestamp\">[2025-02-21 Fri 17:43]–[2025-02-21 Fri 17:48] </span> <span class=\"timestamp\">(0:05)</span>"))))))))
(ert-deftest ox-html/planning ()
"Test rendering of timestamps in planning elements"
--
2.49.0
From c87bbb4050ff039691bad73f8fe00aa5a4f3af76 Mon Sep 17 00:00:00 2001
From: Lukas Epple <em...@lukasepple.de>
Date: Tue, 25 Mar 2025 19:30:34 +0100
Subject: [PATCH 3/5] testing/lisp/test-ox-html.el: add test for planning
rendering
---
testing/lisp/test-ox-html.el | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/testing/lisp/test-ox-html.el b/testing/lisp/test-ox-html.el
index 51b33fffa..db815b62a 100644
--- a/testing/lisp/test-ox-html.el
+++ b/testing/lisp/test-ox-html.el
@@ -931,6 +931,24 @@ CLOCK: [2025-02-21 Fri 17:43]--[2025-02-21 Fri 17:48] => 0:05
(rx-to-string
"<span class=\"timestamp-kwd\">CLOCK:</span> <span class=\"timestamp\">[2025-02-21 Fri 17:43]--[2025-02-21 Fri 17:48] </span> <span class=\"timestamp\">(0:05)</span>"))))))))
+(ert-deftest ox-html/planning ()
+ "Test rendering of timestamps in planning elements"
+ (org-test-with-temp-text "
+* Some Item
+SCHEDULED: <2025-03-26 Wed> DEADLINE: <2025-03-27 Thu 13:00> CLOSED: [2025-03-25 Tue 19:09]
+"
+ (let ((export-buffer "*Test HTML Export")
+ (org-export-show-temporary-buffer nil)
+ (org-export-with-planning t))
+ (org-export-to-buffer 'html export-buffer
+ nil nil nil t)
+ (with-current-buffer export-buffer
+ (mapc (lambda (s)
+ (should (= 1 (how-many (rx-to-string s)))))
+ '("<span class=\"timestamp-kwd\">CLOSED:</span> <span class=\"timestamp\">[2025-03-25 Tue 19:09]</span>"
+ "<span class=\"timestamp-kwd\">DEADLINE:</span> <span class=\"timestamp\"><2025-03-27 Thu 13:00> </span>"
+ "<span class=\"timestamp-kwd\">SCHEDULED:</span> <span class=\"timestamp\"><2025-03-26 Wed> </span>"))))))
+
;;; Postamble Format
--
2.49.0
From de22455b790b9b95b1c50a860e1446bd5176b6e1 Mon Sep 17 00:00:00 2001
From: Lukas Epple <em...@lukasepple.de>
Date: Tue, 25 Mar 2025 19:06:51 +0100
Subject: [PATCH 2/5] testing/lisp/test-ox-html.el: add test for clock
rendering
---
testing/lisp/test-ox-html.el | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/testing/lisp/test-ox-html.el b/testing/lisp/test-ox-html.el
index 1190b541f..51b33fffa 100644
--- a/testing/lisp/test-ox-html.el
+++ b/testing/lisp/test-ox-html.el
@@ -911,6 +911,26 @@ $x$"
"<span class=\"timestamp\"><2025-02-18 Tue 23:59></span>"
"<span class=\"timestamp\">[2025-02-17 Mon 17:00]–[2025-02-17 Mon 19:00]</span>"))))))
+(ert-deftest ox-html/clock ()
+ "Test rendering of clock elements"
+ (org-test-with-temp-text "
+* Test
+:LOGBOOK:
+CLOCK: [2025-02-21 Fri 17:43]--[2025-02-21 Fri 17:48] => 0:05
+:END:
+"
+ (let ((export-buffer "*Test HTML Export")
+ (org-export-show-temporary-buffer nil)
+ (org-export-with-drawers t)
+ (org-export-with-clocks t))
+ (org-export-to-buffer 'html export-buffer
+ nil nil nil t)
+ (with-current-buffer export-buffer
+ (should (= 1
+ (how-many
+ (rx-to-string
+ "<span class=\"timestamp-kwd\">CLOCK:</span> <span class=\"timestamp\">[2025-02-21 Fri 17:43]--[2025-02-21 Fri 17:48] </span> <span class=\"timestamp\">(0:05)</span>"))))))))
+
;;; Postamble Format
--
2.49.0
From 4beadd26ec278908c544a625c7dfa5ff8f048cf6 Mon Sep 17 00:00:00 2001
From: Lukas Epple <em...@lukasepple.de>
Date: Fri, 21 Feb 2025 19:20:09 +0100
Subject: [PATCH 1/5] testing/lisp/test-ox-html.el: add test for timestamp
rendering
---
testing/lisp/test-ox-html.el | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/testing/lisp/test-ox-html.el b/testing/lisp/test-ox-html.el
index c02d47fea..1190b541f 100644
--- a/testing/lisp/test-ox-html.el
+++ b/testing/lisp/test-ox-html.el
@@ -886,6 +886,30 @@ $x$"
(with-current-buffer export-buffer
(libxml-parse-xml-region (point-min) (point-max))))))))
+
+;;; Rendering Timestamps
+
+(ert-deftest ox-html/plain-timestamps ()
+ "Test rendering of timestamps (outside of clock/planning)"
+ (org-test-with-temp-text "
+- [2025-01-31 Fri]
+- [2025-01-31 Fri 14:00]
+- <2025-02-18 Tue>
+- <2025-02-18 Tue 23:59>
+- [2025-02-17 Tue 17:00]--[2025-02-17 Fri 19:00]
+"
+ (let ((export-buffer "*Test HTML Export")
+ (org-export-show-temporary-buffer nil))
+ (org-export-to-buffer 'html export-buffer
+ nil nil nil t)
+ (with-current-buffer export-buffer
+ (mapc (lambda (s)
+ (should (= 1 (how-many (rx-to-string s)))))
+ '("<span class=\"timestamp\">[2025-01-31 Fri]</span>"
+ "<span class=\"timestamp\">[2025-01-31 Fri 14:00]</span>"
+ "<span class=\"timestamp\"><2025-02-18 Tue></span>"
+ "<span class=\"timestamp\"><2025-02-18 Tue 23:59></span>"
+ "<span class=\"timestamp\">[2025-02-17 Mon 17:00]–[2025-02-17 Mon 19:00]</span>"))))))
;;; Postamble Format
--
2.49.0