diff --git a/css/smpte.css b/css/smpte.css
index 280b1eb..80056b3 100644
--- a/css/smpte.css
+++ b/css/smpte.css
@@ -207,9 +207,9 @@ blockquote {
font-style: italic;
}
-/* note and example */
+/* note, example, footnote */
-.note, .example {
+.note, .example, .footnote {
font-size: 0.9rem;
margin-left: 1rem;
}
diff --git a/doc/main.html b/doc/main.html
index 834d7d3..99a09bf 100644
--- a/doc/main.html
+++ b/doc/main.html
@@ -1133,10 +1133,78 @@
Tables
<a href="#tab-sample-tabledata"></a> is rendered as .
-
+
For more examples of various table layouts and optional cell/column text alignment, see
+ Tables may include footnotes. Footnotes shall appear in a tfoot element at the foot of the table,
+ each as a p element with class="footnote" and a unique id attribute. Footnotes are automatically assigned superscript lowercase letters (a, b, c, …) in the order they appear in the tfoot element.
+ References to footnotes within the table body are marked with an a element whose href
+ attribute references the footnote's id.
+
+ Footnotes are only permitted within tables. They shall not be used outside of a table element.
+
+
+
+<table id="tab-sample-footnote">
+ <caption>Sample Table with Footnotes</caption>
+ <thead>
+ <tr>
+ <th>Sample Number</th>
+ <th>Sample Name</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>0001</td>
+ <td>Name 01 <a href="#fn-doc-example"></a></td>
+ </tr>
+ <tr>
+ <td>0002</td>
+ <td>Name 02</td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr>
+ <td colspan="2">
+ <p class="footnote" id="fn-doc-example">Footnote content goes here.</p>
+ </td>
+ </tr>
+ </tfoot>
+</table>
+
+
+
is rendered as
+
+
+
+
+
@@ -1360,6 +1428,9 @@ Using class attributes
example
Applied to p or div elements to create a formatted and automatically numbered example. See .
+ footnote
+ Applied to a p element inside a tfoot element to create a table footnote. Footnotes are only permitted inside a tfoot element — use outside of tables is not allowed. See .
+
center-cell;
right-cell;
left-cell;
diff --git a/js/validate.mjs b/js/validate.mjs
index 039193e..323e3bc 100644
--- a/js/validate.mjs
+++ b/js/validate.mjs
@@ -65,10 +65,18 @@ function validateDisallowedStyleAttributes(root, logger) {
}
}
+function validateFootnoteLocation(root, logger) {
+ for (const fn of root.querySelectorAll('.footnote')) {
+ if (!fn.closest('tfoot'))
+ logger.error(`Footnotes are only permitted inside a table footer`, fn);
+ }
+}
+
export function smpteValidate(doc, logger) {
const docMetadata = smpte.validateHead(doc.head, logger);
validateDisallowedHeadLinks(doc.head, logger);
validateDisallowedStyleAttributes(doc.documentElement, logger);
+ validateFootnoteLocation(doc.documentElement, logger);
validateBody(doc.body, logger);
return docMetadata;
}
diff --git a/smpte.js b/smpte.js
index 408fe2a..5b334b9 100644
--- a/smpte.js
+++ b/smpte.js
@@ -1104,6 +1104,7 @@ function numberNotesToEntry(internalTermsSection) {
child.insertBefore(headingLabel, child.firstChild);
}
+
}
function numberSectionNotes(section) {
@@ -1111,7 +1112,7 @@ function numberSectionNotes(section) {
function _findNotes(e) {
for (const child of e.children) {
- if (child.localName === "section")
+ if (child.localName === "section" || child.localName === "table")
numberSectionNotes(child);
else if (child.classList.contains("note"))
notes.push(child);
@@ -1194,6 +1195,36 @@ function numberExamples() {
}
}
+function numberTableFootnotes() {
+ for (const table of document.querySelectorAll("table")) {
+ const footnotes = Array.from(table.querySelectorAll("tfoot p.footnote"));
+ if (footnotes.length === 0) continue;
+
+ let charCode = "a".charCodeAt(0);
+
+ for (const fn of footnotes) {
+ if (!fn.id) continue;
+ const letter = String.fromCharCode(charCode++);
+
+ const headingLabel = document.createElement("span");
+ headingLabel.className = "heading-label";
+
+ const sup = document.createElement("sup");
+ sup.textContent = letter;
+
+ headingLabel.appendChild(sup);
+ headingLabel.appendChild(document.createTextNode("\u00a0"));
+
+ fn.insertBefore(headingLabel, fn.firstChild);
+
+ /* fill all reference anchors pointing to this footnote */
+ for (const ref of table.querySelectorAll(`a[href="#${fn.id}"]`)) {
+ ref.textContent = letter;
+ }
+ }
+ }
+}
+
function numberTerms() {
const termsSection = document.getElementById("sec-terms-and-definitions");
const terms = document.getElementById("terms-int-defs");
@@ -1382,6 +1413,13 @@ function resolveLinks(docMetadata) {
anchor.innerText = t;
}
+ } else if (target.classList.contains("footnote")) {
+
+ /* footnote ref — letter already filled by numberTableFootnotes; wrap in */
+ const sup = document.createElement("sup");
+ anchor.replaceWith(sup);
+ sup.appendChild(anchor);
+
} else if (target.localName === "section") {
anchor.innerText = _getSectionReference(target);
@@ -1487,6 +1525,7 @@ async function render() {
numberFormulae();
numberNotes();
numberExamples();
+ numberTableFootnotes();
numberTerms();
resolveLinks(docMetadata);
insertTOC(docMetadata);
diff --git a/test/resources/html/validation/footnote-invalid.html b/test/resources/html/validation/footnote-invalid.html
new file mode 100644
index 0000000..a74f517
--- /dev/null
+++ b/test/resources/html/validation/footnote-invalid.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Title of the document
+
+
+
+ This is the scope of the document.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/resources/html/validation/footnote-valid.html b/test/resources/html/validation/footnote-valid.html
new file mode 100644
index 0000000..de7f324
--- /dev/null
+++ b/test/resources/html/validation/footnote-valid.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Title of the document
+
+
+
+ This is the scope of the document.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/resources/html/validation/st-valid.html b/test/resources/html/validation/st-valid.html
index 9f9f2ff..258d097 100644
--- a/test/resources/html/validation/st-valid.html
+++ b/test/resources/html/validation/st-valid.html
@@ -19,5 +19,8 @@
This is the scope of the document.
+
+
+