CETEIcean 1.8.0 → 1.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ {
2
+ "cSpell.words": [
3
+ "Ordinality",
4
+ "prefixdef"
5
+ ]
6
+ }
package/README.md CHANGED
@@ -12,10 +12,7 @@ documents to be displayed in a web browser without first transforming them to
12
12
  HTML. It uses the emerging [Web Components](http://webcomponents.org) standards,
13
13
  especially [Custom Elements](http://w3c.github.io/webcomponents/spec/custom/). It
14
14
  works by loading the TEI file dynamically, renaming the elements to follow the
15
- Custom Elements conventions, and registering them with the browser. Browsers
16
- that support Web Components will use them to add the appropriate display and
17
- behaviors to the TEI elements; other browsers will use fallback methods to
18
- achieve the same result.
15
+ Custom Elements conventions, and registering them with the browser.
19
16
 
20
17
  Because it preserves the full structure and information from your TEI data model,
21
18
  CETEIcean allows you to build rich web applications from your source documents
@@ -0,0 +1,86 @@
1
+ # TEI Custom Elements Specification and Contract
2
+
3
+ When CETEIcean processes a TEI document, the document structures will by default be converted as follows:
4
+
5
+ ## Elements
6
+
7
+ All tag names will be converted into [prefix]-[lowercased tagname]. The prefix can be defined in a behaviors object. There must be one prefix defined per XML namespace. There are three predefined prefixes:
8
+
9
+ ```js
10
+ "namespaces": {
11
+ "tei": "http://www.tei-c.org/ns/1.0",
12
+ "teieg": "http://www.tei-c.org/ns/Examples",
13
+ "rng": "http://relaxng.org/ns/structure/1.0"
14
+ }
15
+ ```
16
+
17
+ These prefixes can be remapped in the behaviors object passed to CETEIcean.
18
+
19
+ ### CETEIcean-created Elements
20
+
21
+ CETEIcean may add the following elements to markup it processes:
22
+
23
+ * `<cetei-content>`: holds any content returned from a behavior function call.
24
+ * `<cetei-original`: wraps any original content that has been hidden and replaced by the result of a behavior function call.
25
+
26
+ ## Attributes
27
+
28
+ CETEIcean adds a number of data attributes when it processes an XML element:
29
+
30
+ * `@data-origname`: contains the original tag name with whatever upper- or lowercasing it used. The HTML DOM normalizes tag names to uppercase.
31
+ * `@data-origatts`: contains a list of attribute names on the element before it was processed with whatever upper- or lowercasing they used. The HTML DOM normalizes attribute names to lowercase.
32
+ * `@data-processed`: a flag CETEIcean uses to determine whether elements have had behaviors applied. Normally, this will happen upon insertion into the browser DOM.
33
+ * `@data-empty`: a flag CETEIcean uses to mark empty elements (these may not remain empty after behaviors have been applied).
34
+
35
+ ### Automatically Converted Attributes
36
+
37
+ * `@xml:id` is preserved and the value copied to `@id`.
38
+ * `@xml:lang` is preserved and the value copied to `@lang`.
39
+ * `@xmlns` is removed and its value copied to `@data-xmlns`.
40
+ * `@rendition` is preserved and its value copied to `@class`.
41
+
42
+ ## Processing Instructions and Comments
43
+
44
+ PIs and comments are copied over into the result, so, for example, `<?xml-model?>` PIs pointing to a schema will be present before the root element of the converted document. The XML declaration, if present, is not a PI, and will not be copied over.
45
+
46
+ ## Document Type Declarations
47
+
48
+ DTDs are not copied over into the result.
49
+
50
+ ## Contract
51
+
52
+ If a source XML document has been processed into CETEIcean Custom Elements and is then re-serialized into XML using the `CETEI.utilities` instance method `resetAndSerialize()`, then the result will be logically equivalent to the source, provided that additional processing has not altered the document. Note that custom behaviors have direct access to the DOM, and thus are capable of directly manipulating the document in any way. No guarantees can be made as to the consistency of a serialized document if it has been rearranged, fo example.
53
+
54
+ This contract means that CETEIcean can be used in conjunction with in-browser editing or annotation tools to produce prospectively valid output XML.
55
+
56
+ For example, the built-in behavior for TEI `<ref>` is to create an HTML anchor tag with the content being the content of the `<ref>` and the `@href` attribute containing the pointer in the `@target` of the `<ref>` (or the first pointer if there are more than one). Given an element like
57
+
58
+ ```xml
59
+ <ref target="https://github.com/TEIC/TEI/blob/dev/P5/Source/guidelines-en.xml" mimeType="application/tei+xml">guidelines-en.xml</ref>
60
+ ```
61
+
62
+ The structure CETEIcean creates will look like this:
63
+
64
+ ```xml
65
+ <tei-ref target="https://github.com/TEIC/TEI/blob/dev/P5/Source/guidelines-en.xml" mimetype="application/tei+xml" data-origname="ref" data-origatts="target mimeType" data-processed="">
66
+ <cetei-original hidden="" data-original="">guidelines-en.xml</cetei-original>
67
+ <a href="https://github.com/TEIC/TEI/blob/dev/P5/Source/guidelines-en.xml">guidelines-en.xml</a>
68
+ </tei-ref>
69
+ ```
70
+
71
+ If this element is loaded into a variable and then passed to the `CETEI.utilities.copyAndReset()` function, the result will be:
72
+
73
+ ```xml
74
+ <tei-ref target="https://github.com/TEIC/TEI/blob/dev/P5/Source/guidelines-en.xml" mimetype="application/tei+xml" data-origname="ref" data-origatts="target mimeType">guidelines-en.xml</tei-ref>
75
+ ```
76
+
77
+ that is, the element as it was after CETEIcean has loaded it, but before it was inserted into the document DOM and had behaviors applied to it.
78
+
79
+ If this result is then passed to the `CETEI.utilities.serialize()` function, the output is
80
+
81
+ ```xml
82
+ <ref target="https://github.com/TEIC/TEI/blob/dev/P5/Source/guidelines-en.xml" mimeType="application/tei+xml">guidelines-en.xml</ref>
83
+ ```
84
+
85
+ CETEIcean's contract means that
86
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "CETEIcean",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
4
4
  "description": "JavaScript library to load a TEI XML document and register it as HTML5 custom elements.",
5
5
  "main": "src/CETEI.js",
6
6
  "type": "module",
package/src/CETEI.js CHANGED
@@ -61,7 +61,6 @@ class CETEI {
61
61
  window.removeEventListener("ceteiceanload", CETEI.restorePosition);
62
62
  }
63
63
  }
64
-
65
64
  }
66
65
 
67
66
  /*
@@ -69,36 +68,22 @@ class CETEI {
69
68
  provided in the first parameter and then calls the makeHTML5 method
70
69
  on the returned document.
71
70
  */
72
- getHTML5(XML_url, callback, perElementFn){
71
+ async getHTML5(XML_url, callback, perElementFn){
73
72
  if (window && window.location.href.startsWith(this.base) && (XML_url.indexOf("/") >= 0)) {
74
73
  this.base = XML_url.replace(/\/[^\/]*$/, "/");
75
74
  }
76
- // Get XML from XML_url and create a promise
77
- let promise = new Promise( function (resolve, reject) {
78
- let client = new XMLHttpRequest();
79
- client.open('GET', XML_url);
80
- client.send();
81
- client.onload = function () {
82
- if (this.status >= 200 && this.status < 300) {
83
- resolve(this.response);
84
- } else {
85
- reject(this.statusText);
86
- }
87
- };
88
- client.onerror = function () {
89
- reject(this.statusText);
90
- };
91
- })
92
- .catch( function(reason) {
93
- console.log("Could not get XML file.");
94
- if (this.debug) {
95
- console.log(reason);
96
- }
97
- });
98
-
99
- return promise.then((XML) => {
75
+ try {
76
+ const response = await fetch(XML_url);
77
+ if (response.ok) {
78
+ const XML = await response.text();
100
79
  return this.makeHTML5(XML, callback, perElementFn);
101
- });
80
+ } else {
81
+ console.log(`Could not get XML file ${XML_url}.\nServer returned ${response.status}: ${response.statusText}`);
82
+ }
83
+ }
84
+ catch (error) {
85
+ console.log(error);
86
+ }
102
87
  }
103
88
 
104
89
  /*
@@ -120,7 +105,7 @@ class CETEI {
120
105
  let newElement;
121
106
  if (this.namespaces.has(el.namespaceURI ? el.namespaceURI : "")) {
122
107
  let prefix = this.namespaces.get(el.namespaceURI ? el.namespaceURI : "");
123
- newElement = this.document.createElement(`${prefix}-${el.localName}`);
108
+ newElement = this.document.createElement(`${prefix}-${el.localName.toLowerCase()}`);
124
109
  } else {
125
110
  newElement = this.document.importNode(el, false);
126
111
  }
@@ -207,8 +192,23 @@ class CETEI {
207
192
  return newElement;
208
193
  }
209
194
 
210
- this.dom = convertEl(XML_dom.documentElement);
211
- this.utilities.dom = this.dom;
195
+ this.dom = this.document.createDocumentFragment();
196
+ for (let node of Array.from(XML_dom.childNodes)) {
197
+ // Node.ELEMENT_NODE
198
+ if (node.nodeType == 1) {
199
+ this.dom.appendChild(convertEl(node));
200
+ }
201
+ // Node.PROCESSING_INSTRUCTION_NODE
202
+ if (node.nodeType == 7) {
203
+ this.dom.appendChild(this.document.importNode(node, true));
204
+ }
205
+ // Node.COMMENT_NODE
206
+ if (node.nodeType == 8) {
207
+ this.dom.appendChild(this.document.importNode(node, true));
208
+ }
209
+ }
210
+ // DocumentFragments don't work in the same ways as other nodes, so use the root element.
211
+ this.utilities.dom = this.dom.firstElementChild;
212
212
 
213
213
  if (callback) {
214
214
  callback(this.dom, this);
@@ -255,6 +255,9 @@ class CETEI {
255
255
  processPage() {
256
256
  this.els = learnCustomElementNames(this.document);
257
257
  this.applyBehaviors();
258
+ if (window) {
259
+ window.dispatchEvent(ceteiceanLoad);
260
+ }
258
261
  }
259
262
 
260
263
  /*
@@ -282,19 +285,19 @@ class CETEI {
282
285
  by the provided function.
283
286
 
284
287
  Called by getHandler() and fallback()
285
- */
288
+ */
286
289
  append(fn, elt) {
287
290
  let self = this;
288
- if (elt) {
291
+ if (elt && !elt.hasAttribute('data-processed')) {
289
292
  let content = fn.call(self.utilities, elt);
290
- if (content && !self.childExists(elt.firstElementChild, content.nodeName)) {
293
+ if (content) {
291
294
  self.appendBasic(elt, content);
292
295
  }
293
296
  } else {
294
297
  return function() {
295
298
  if (!this.hasAttribute("data-processed")) {
296
299
  let content = fn.call(self.utilities, this);
297
- if (content && !self.childExists(this.firstElementChild, content.nodeName)) {
300
+ if (content) {
298
301
  self.appendBasic(this, content);
299
302
  }
300
303
  }
@@ -398,7 +401,7 @@ getHandler(behaviors, fn) {
398
401
  }
399
402
 
400
403
  insert(elt, strings) {
401
- let span = this.document.createElement("span");
404
+ let content = this.document.createElement("cetei-content");
402
405
  for (let node of Array.from(elt.childNodes)) {
403
406
  // nodeType 1 is Node.ELEMENT_NODE
404
407
  if (node.nodeType === 1 && !node.hasAttribute("data-processed")) {
@@ -408,19 +411,23 @@ insert(elt, strings) {
408
411
  // If we have before and after tags have them parsed by
409
412
  // .innerHTML and then add the content to the resulting child
410
413
  if (strings[0].match("<[^>]+>") && strings[1] && strings[1].match("<[^>]+>")) {
411
- span.innerHTML = strings[0] + elt.innerHTML + (strings[1]?strings[1]:"");
414
+ content.innerHTML = strings[0] + elt.innerHTML + (strings[1]?strings[1]:"");
412
415
  } else {
413
- span.innerHTML = strings[0];
414
- span.setAttribute("data-before", strings[0].replace(/<[^>]+>/g,"").length);
416
+ content.innerHTML = strings[0];
417
+ content.setAttribute("data-before", strings[0].replace(/<[^>]+>/g,"").length);
415
418
  for (let node of Array.from(elt.childNodes)) {
416
- span.appendChild(node.cloneNode(true));
419
+ content.appendChild(node.cloneNode(true));
417
420
  }
418
421
  if (strings.length > 1) {
419
- span.innerHTML += strings[1];
420
- span.setAttribute("data-after", strings[1].replace(/<[^>]+>/g,"").length);
422
+ content.innerHTML += strings[1];
423
+ content.setAttribute("data-after", strings[1].replace(/<[^>]+>/g,"").length);
421
424
  }
422
425
  }
423
- return span;
426
+ if (content.childNodes.length < 2) {
427
+ return content.firstChild;
428
+ } else {
429
+ return content;
430
+ }
424
431
  }
425
432
 
426
433
  // Runs behaviors recursively on the supplied element and children
@@ -482,7 +489,7 @@ define(names) {
482
489
  }
483
490
 
484
491
  /*
485
- Provides fallback functionality for browsers where Custom Elements
492
+ Provides fallback functionality for environments where Custom Elements
486
493
  are not supported.
487
494
 
488
495
  Like define(), this is called by makeHTML5(), but can be called
@@ -496,9 +503,10 @@ fallback(names) {
496
503
  this.dom && !this.done
497
504
  ? this.dom
498
505
  : this.document
499
- ).getElementsByTagName(utilities.tagName(name)))) {
506
+ ).querySelectorAll(utilities.tagName(name)))) {
500
507
  if (!elt.hasAttribute("data-processed")) {
501
508
  this.append(fn, elt);
509
+ elt.setAttribute("data-processed", "");
502
510
  }
503
511
  }
504
512
  }
@@ -518,6 +526,7 @@ fallback(names) {
518
526
  if (!window.location.hash) {
519
527
  let scroll;
520
528
  if (scroll = window.sessionStorage.getItem(window.location + "-scroll")) {
529
+ window.sessionStorage.removeItem(window.location + "-scroll");
521
530
  setTimeout(function() {
522
531
  window.scrollTo(0, scroll);
523
532
  }, 100);
package/src/behaviors.js CHANGED
@@ -21,7 +21,7 @@ export function addBehaviors(bhvs) {
21
21
  }
22
22
  if (bhvs["functions"]) {
23
23
  for (let fn of Object.keys(bhvs["functions"])) {
24
- this.utilities[fn] = bhvs["functions"][fn];
24
+ this.utilities[fn] = bhvs["functions"][fn].bind(this.utilities);
25
25
  }
26
26
  }
27
27
  if (bhvs["handlers"]) {
@@ -9,10 +9,12 @@ export default {
9
9
  // inserts a link inside <ptr> using the @target; the link in the
10
10
  // @href is piped through the rw (rewrite) function before insertion
11
11
  "ptr": ["<a href=\"$rw@target\">$@target</a>"],
12
- // wraps the content of the <ref> in an HTML link
12
+ // wraps the content of the <ref> in an HTML link with the @target in
13
+ // the @href. If there are multiple @targets, only the first is used.
13
14
  "ref": [
14
15
  ["[target]", ["<a href=\"$rw@target\">","</a>"]]
15
16
  ],
17
+ // creates an img tag with the @url as the src attribute
16
18
  "graphic": function(elt) {
17
19
  let content = new Image();
18
20
  content.src = this.rw(elt.getAttribute("url"));
@@ -71,15 +73,17 @@ export default {
71
73
  }
72
74
  let note = doc.createElement("li");
73
75
  note.id = id;
74
- note.innerHTML = elt.innerHTML
76
+ note.innerHTML = elt.innerHTML;
75
77
  notes.appendChild(note);
76
78
  return content;
77
79
  }],
78
80
  ["_", ["(",")"]]
79
81
  ],
82
+ // Hide the teiHeader by default
80
83
  "teiHeader": function(e) {
81
84
  this.hideContent(e, false);
82
85
  },
86
+ // Make the title element the HTML title
83
87
  "title": [
84
88
  ["tei-titlestmt>tei-title", function(elt) {
85
89
  const doc = elt.ownerDocument;
package/src/dom.js CHANGED
@@ -16,5 +16,5 @@ export function learnElementNames(XML_dom, namespaces) {
16
16
  }
17
17
 
18
18
  export function learnCustomElementNames(HTML_dom) {
19
- return Array.from(HTML_dom.querySelectorAll("*[data-origname]"), e => e.localName.replace(/(\w+)-.+/,"$1:") + e.getAttribute("data-origname"));
19
+ return new Set(Array.from(HTML_dom.querySelectorAll("*[data-origname]"), e => e.localName.replace(/(\w+)-.+/,"$1:") + e.getAttribute("data-origname")));
20
20
  }
package/src/utilities.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export function getOrdinality(elt, name) {
2
2
  let pos = 1;
3
3
  let e = elt;
4
- while (e && e.previousElementSibling !== null && (name?e.previousElementSibling.localName == name:true)) {
4
+ while (e && e.previousElementSibling !== null && (name ? e.previousElementSibling.localName == name : true)) {
5
5
  pos++;
6
6
  e = e.previousElementSibling;
7
7
  if (!e.previousElementSibling) {
@@ -17,11 +17,21 @@ export function getOrdinality(elt, name) {
17
17
  */
18
18
  export function copyAndReset(node) {
19
19
  const doc = node.ownerDocument;
20
- let clone = (n) => {
21
- // nodeType 1 is Node.ELEMENT_NODE
22
- let result = n.nodeType === 1
23
- ? doc.createElement(n.nodeName)
24
- : n.cloneNode(true);
20
+ let clone = (n) => {
21
+ let result;
22
+ switch (n.nodeType) {
23
+ case 1: // nodeType 1 is Node.ELEMENT_NODE
24
+ result = doc.createElement(n.nodeName);
25
+ break;
26
+ case 9: // nodeType 9 is Node.DOCUMENT_NODE
27
+ result = doc.implementation.createDocument();
28
+ break;
29
+ case 11: // nodeType 11 is Node.DOCUMENT_FRAGMENT_NODE
30
+ result = doc.createDocumentFragment();
31
+ break;
32
+ default:
33
+ result = n.cloneNode(true);
34
+ }
25
35
  if (n.attributes) {
26
36
  for (let att of Array.from(n.attributes)) {
27
37
  if (att.name !== "data-processed") {
@@ -32,23 +42,20 @@ export function copyAndReset(node) {
32
42
  for (let nd of Array.from(n.childNodes)){
33
43
  // nodeType 1 is Node.ELEMENT_NODE
34
44
  if (nd.nodeType == 1) {
35
- if (!n.hasAttribute("data-empty")) {
36
- if (nd.hasAttribute("data-original")) {
37
- for (let childNode of Array.from(nd.childNodes)) {
38
- let child = result.appendChild(clone(childNode));
39
- // nodeType 1 is Node.ELEMENT_NODE
40
- if (child.nodeType === 1 && child.hasAttribute("data-origid")) {
41
- child.setAttribute("id", child.getAttribute("data-origid"));
42
- child.removeAttribute("data-origid");
43
- }
45
+ if (nd.hasAttribute("data-original")) {
46
+ for (let childNode of Array.from(nd.childNodes)) {
47
+ let child = result.appendChild(clone(childNode));
48
+ // nodeType 1 is Node.ELEMENT_NODE
49
+ if (child.nodeType === 1 && child.hasAttribute("data-origid")) {
50
+ child.setAttribute("id", child.getAttribute("data-origid"));
51
+ child.removeAttribute("data-origid");
44
52
  }
45
- return result;
46
- } else {
47
- result.appendChild(clone(nd));
48
53
  }
54
+ return result;
55
+ } else if (nd.hasAttribute("data-origname")) {
56
+ result.appendChild(clone(nd));
49
57
  }
50
- }
51
- else {
58
+ } else {
52
59
  result.appendChild(nd.cloneNode());
53
60
  }
54
61
  }
@@ -66,13 +73,12 @@ export function first(urls) {
66
73
  }
67
74
 
68
75
  /*
69
- Wraps the content of the element parameter in a <span data-original>
70
- with display set to "none".
76
+ Wraps the content of the element parameter in a hidden <cetei-original data-original>
71
77
  */
72
78
  export function hideContent(elt, rewriteIds = true) {
73
79
  const doc = elt.ownerDocument;
74
80
  if (elt.childNodes.length > 0) {
75
- let hidden = doc.createElement("span");
81
+ let hidden = doc.createElement("cetei-original");
76
82
  elt.appendChild(hidden);
77
83
  hidden.setAttribute("hidden", "");
78
84
  hidden.setAttribute("data-original", "");
@@ -139,12 +145,20 @@ export function getPrefixDef(prefix) {
139
145
  */
140
146
  export function rw(url) {
141
147
  if (!url.match(/^(?:http|mailto|file|\/|#).*$/)) {
142
- return this.base + this.utilities.first(url);
148
+ return this.base + first(url);
143
149
  } else {
144
150
  return url;
145
151
  }
146
152
  }
147
153
 
154
+ /*
155
+ Combines the functionality of copyAndReset() and serialize() to return
156
+ a "clean" version of the XML markup.
157
+ */
158
+ export function resetAndSerialize(el, stripElt, ws) {
159
+ return serialize(copyAndReset(el), stripElt, ws);
160
+ }
161
+
148
162
  /*
149
163
  Takes an element and serializes it to an XML string or, if the stripElt
150
164
  parameter is set, serializes the element's content. The ws parameter, if
@@ -153,9 +167,12 @@ export function rw(url) {
153
167
  */
154
168
  export function serialize(el, stripElt, ws) {
155
169
  let str = "";
156
- let ignorable = (txt) => {
170
+ const ignorable = (txt) => {
157
171
  return !(/[^\t\n\r ]/.test(txt));
158
172
  }
173
+ if (el.nodeType === 9 || el.nodeType === 11) { // nodeType 9 is Node.DOCUMENT_NODE; nodeType 11 is Node.DOCUMENT_FRAGMENT_NODE
174
+ str += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
175
+ }
159
176
  // nodeType 1 is Node.ELEMENT_NODE
160
177
  if (!stripElt && el.nodeType == 1) {
161
178
  if ((typeof ws === "string") && ws !== "") {
@@ -180,7 +197,6 @@ export function serialize(el, stripElt, ws) {
180
197
  str += "/>";
181
198
  }
182
199
  }
183
- //TODO: Be smarter about skipping generated content with hidden original
184
200
  for (let node of Array.from(el.childNodes)) {
185
201
  // nodeType 1 is Node.ELEMENT_NODE
186
202
  // nodeType 7 is Node.PROCESSING_INSTRUCTION_NODE
@@ -194,10 +210,16 @@ export function serialize(el, stripElt, ws) {
194
210
  }
195
211
  break;
196
212
  case 7:
197
- str += "<?" + node.nodeValue + "?>";
213
+ str += `<?${node.nodeName} ${node.nodeValue}?>`;
214
+ if (el.nodeType === 9 || el.nodeType === 11) {
215
+ str += "\n";
216
+ }
198
217
  break;
199
218
  case 8:
200
- str += "<!--" + node.nodeValue + "-->";
219
+ str += `<!--${node.nodeValue}-->`;
220
+ if (el.nodeType === 9 || el.nodeType === 11) {
221
+ str += "\n";
222
+ }
201
223
  break;
202
224
  default:
203
225
  if (stripElt && ignorable(node.nodeValue)) {
@@ -209,7 +231,7 @@ export function serialize(el, stripElt, ws) {
209
231
  str += node.nodeValue;
210
232
  }
211
233
  }
212
- if (!stripElt && el.childNodes.length > 0) {
234
+ if (!stripElt && el.nodeType == 1 && el.childNodes.length > 0) {
213
235
  if (typeof ws === "string") {
214
236
  str += "\n" + ws + "</";
215
237
  } else {
@@ -217,6 +239,81 @@ export function serialize(el, stripElt, ws) {
217
239
  }
218
240
  str += el.getAttribute("data-origname") + ">";
219
241
  }
242
+ if (el.nodeType === 9 || el.nodeType === 11) {
243
+ str += "\n";
244
+ }
245
+ return str;
246
+ }
247
+
248
+ /*
249
+ Write out the HTML markup to a string, using HTML conventions.
250
+ */
251
+ export function serializeHTML(el, stripElt, ws) {
252
+ const EMPTY_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
253
+ let str = "";
254
+ const ignorable = (txt) => {
255
+ return !(/[^\t\n\r ]/.test(txt));
256
+ }
257
+ // nodeType 1 is Node.ELEMENT_NODE
258
+ if (!stripElt && el.nodeType == 1) {
259
+ if ((typeof ws === "string") && ws !== "") {
260
+ str += "\n" + ws + "<";
261
+ } else {
262
+ str += "<";
263
+ }
264
+ str += el.nodeName;
265
+ for (let attr of Array.from(el.attributes)) {
266
+ str += " " + attr.name + "=\"" + attr.value + "\"";
267
+ }
268
+ str += ">";
269
+ }
270
+ for (let node of Array.from(el.childNodes)) {
271
+ // nodeType 1 is Node.ELEMENT_NODE
272
+ // nodeType 7 is Node.PROCESSING_INSTRUCTION_NODE
273
+ // nodeType 8 is Node.COMMENT_NODE
274
+ switch (node.nodeType) {
275
+ case 1:
276
+ if (typeof ws === "string") {
277
+ str += serializeHTML(node, false, ws + " ");
278
+ } else {
279
+ str += serializeHTML(node, false, ws);
280
+ }
281
+ break;
282
+ case 7:
283
+ str += `<?${node.nodeName} ${node.nodeValue}?>`;
284
+ if (el.nodeType === 9 || el.nodeType === 11) {
285
+ str += "\n";
286
+ }
287
+ break;
288
+ case 8:
289
+ str += `<!--${node.nodeValue}-->`;
290
+ if (el.nodeType === 9 || el.nodeType === 11) {
291
+ str += "\n";
292
+ }
293
+ break;
294
+ default:
295
+ if (stripElt && ignorable(node.nodeValue)) {
296
+ str += node.nodeValue.replace(/^\s*\n/, "");
297
+ }
298
+ if ((typeof ws === "string") && ignorable(node.nodeValue)) {
299
+ break;
300
+ }
301
+ str += node.nodeValue;
302
+ }
303
+ }
304
+ if (!EMPTY_ELEMENTS.includes(el.nodeName)) {
305
+ if (!stripElt && el.nodeType == 1) {
306
+ if (typeof ws === "string") {
307
+ str += `\n${ws}</`;
308
+ } else {
309
+ str += "</";
310
+ }
311
+ str += `${el.nodeName}>`;
312
+ }
313
+ }
314
+ if (el.nodeType === 9 || el.nodeType === 11) {
315
+ str += "\n";
316
+ }
220
317
  return str;
221
318
  }
222
319
 
@@ -249,9 +346,9 @@ export function defineCustomElement(name, behavior = null, debug = false) {
249
346
  if (!this.matches(":defined")) { // "Upgraded" undefined elements can have attributes & children; new elements can't
250
347
  if (behavior) {
251
348
  behavior.call(this);
349
+ // We don't want to double-process elements, so add a flag
350
+ this.setAttribute("data-processed", "");
252
351
  }
253
- // We don't want to double-process elements, so add a flag
254
- this.setAttribute("data-processed", "");
255
352
  }
256
353
  }
257
354
  // Process new elements when they are connected to the browser DOM
@@ -259,8 +356,8 @@ export function defineCustomElement(name, behavior = null, debug = false) {
259
356
  if (!this.hasAttribute("data-processed")) {
260
357
  if (behavior) {
261
358
  behavior.call(this);
359
+ this.setAttribute("data-processed", "");
262
360
  }
263
- this.setAttribute("data-processed", "");
264
361
  }
265
362
  };
266
363
  });
@@ -212,6 +212,9 @@ tei-cell {
212
212
  border-bottom: thin solid black;
213
213
  padding: 2px;
214
214
  }
215
+ tei-cell[cols="2"] {
216
+ grid-column: span 2;
217
+ }
215
218
  /* for cell or row with @role = label */
216
219
  tei-cell[data-tei-role=label] {
217
220
  font-weight: bold;