1/** 2 * JATS XML to HTML renderer for the Streetcat theme. 3 * 4 * Converts JATS (Journal Article Tag Suite) XML into readable HTML, 5 * handling sections, paragraphs, figures, tables, footnotes, block quotes, 6 * lists, and inline formatting. 7 */ 8(function () { 9 'use strict'; 10 11 // ---- Inline element conversion ---- 12 13 function convertInline(node) { 14 var html = ''; 15 if (!node || !node.childNodes) return html; 16 17 for (var i = 0; i < node.childNodes.length; i++) { 18 var child = node.childNodes[i]; 19 20 if (child.nodeType === 3) { // text node 21 html += child.textContent; 22 continue; 23 } 24 25 if (child.nodeType !== 1) continue; // skip non-element nodes 26 27 var tag = child.nodeName.toLowerCase(); 28 29 switch (tag) { 30 case 'italic': 31 html += '<em>' + convertInline(child) + '</em>'; 32 break; 33 case 'bold': 34 html += '<strong>' + convertInline(child) + '</strong>'; 35 break; 36 case 'sup': 37 html += '<sup>' + convertInline(child) + '</sup>'; 38 break; 39 case 'sub': 40 html += '<sub>' + convertInline(child) + '</sub>'; 41 break; 42 case 'underline': 43 html += '<u>' + convertInline(child) + '</u>'; 44 break; 45 case 'monospace': 46 html += '<code>' + convertInline(child) + '</code>'; 47 break; 48 case 'sc': 49 html += '<span style="font-variant:small-caps">' + convertInline(child) + '</span>'; 50 break; 51 case 'break': 52 html += '<br>'; 53 break; 54 case 'xref': 55 var refType = child.getAttribute('ref-type'); 56 var rid = child.getAttribute('rid'); 57 if (refType === 'fn' && rid) { 58 html += '<a class="fn-ref" href="#' + rid + '" id="' + rid + '-ref"><sup>' + convertInline(child) + '</sup></a>'; 59 } else if (refType === 'bibr' && rid) { 60 html += '<a class="bibr-ref" href="#' + rid + '">' + convertInline(child) + '</a>'; 61 } else if (refType === 'fig' && rid) { 62 html += '<a class="fig-ref" href="#' + rid + '">' + convertInline(child) + '</a>'; 63 } else if (refType === 'table' && rid) { 64 html += '<a class="table-ref" href="#' + rid + '">' + convertInline(child) + '</a>'; 65 } else { 66 html += convertInline(child); 67 } 68 break; 69 case 'ext-link': 70 var href = child.getAttribute('xlink:href') || child.getAttribute('href') || '#'; 71 html += '<a href="' + href + '" target="_blank" rel="noopener">' + convertInline(child) + '</a>'; 72 break; 73 case 'uri': 74 var uri = child.getAttribute('xlink:href') || child.textContent; 75 html += '<a href="' + uri + '" target="_blank" rel="noopener">' + convertInline(child) + '</a>'; 76 break; 77 case 'named-content': 78 html += '<span class="named-content">' + convertInline(child) + '</span>'; 79 break; 80 case 'inline-formula': 81 case 'inline-graphic': 82 var src = child.getAttribute('xlink:href'); 83 if (src) { 84 html += '<img class="inline-graphic" src="' + src + '" alt="">'; 85 } else { 86 html += convertInline(child); 87 } 88 break; 89 case 'disp-formula': 90 html += '<div class="disp-formula">' + convertInline(child) + '</div>'; 91 break; 92 default: 93 html += convertInline(child); 94 break; 95 } 96 } 97 return html; 98 } 99 100 // ---- Block element conversion ---- 101 102 function convertFigure(fig) { 103 var id = fig.getAttribute('id') || ''; 104 var html = '<figure class="article-figure" id="' + id + '">'; 105 106 var graphic = fig.querySelector('graphic'); 107 if (graphic) { 108 var src = graphic.getAttribute('xlink:href') || ''; 109 html += '<img src="' + src + '" alt="">'; 110 } 111 112 var label = fig.querySelector(':scope > label'); 113 var caption = fig.querySelector('caption'); 114 if (label || caption) { 115 html += '<figcaption>'; 116 if (label) { 117 html += '<strong>' + label.textContent + '.</strong> '; 118 } 119 if (caption) { 120 var captionPs = caption.querySelectorAll('p');
121 captionPs.forEach(function (p) { 122 html += convertInline(p); 123 }); 124 if (captionPs.length === 0) { 125 var captionTitle = caption.querySelector('title'); 126 if (captionTitle && captionTitle.textContent.trim()) { 127 html += convertInline(captionTitle); 128 } 129 } 130 } 131 html += '</figcaption>'; 132 } 133 134 html += '</figure>'; 135 return html; 136 } 137 138 function convertTable(tableWrap) { 139 var id = tableWrap.getAttribute('id') || ''; 140 var html = '<div class="article-table" id="' + id + '">'; 141 142 var label = tableWrap.querySelector(':scope > label'); 143 var caption = tableWrap.querySelector(':scope > caption'); 144 if (label || caption) { 145 html += '<div class="table-caption">'; 146 if (label) html += '<strong>' + label.textContent + '.</strong> '; 147 if (caption) { 148 var ps = caption.querySelectorAll('p'); 149 ps.forEach(function (p) { html += convertInline(p); }); 150 if (ps.length === 0) html += convertInline(caption); 151 } 152 html += '</div>'; 153 } 154 155 var table = tableWrap.querySelector('table'); 156 if (table) { 157 html += '<div class="table-responsive"><table class="table">'; 158 // Serialize the table children, converting any JATS inline markup 159 var sections = table.querySelectorAll('thead, tbody, tfoot'); 160 if (sections.length > 0) { 161 sections.forEach(function (section) { 162 html += '<' + section.nodeName.toLowerCase() + '>'; 163 section.querySelectorAll('tr').forEach(function (tr) { 164 html += convertTableRow(tr); 165 }); 166 html += '</' + section.nodeName.toLowerCase() + '>'; 167 }); 168 } else { 169 table.querySelectorAll('tr').forEach(function (tr) { 170 html += convertTableRow(tr); 171 }); 172 } 173 html += '</table></div>'; 174 } 175 176 html += '</div>'; 177 return html; 178 } 179 180 function convertTableRow(tr) { 181 var html = '<tr>'; 182 for (var i = 0; i < tr.children.length; i++) { 183 var cell = tr.children[i]; 184 var cellTag = cell.nodeName.toLowerCase() === 'th' ? 'th' : 'td'; 185 var attrs = ''; 186 if (cell.getAttribute('colspan')) attrs += ' colspan="' + cell.getAttribute('colspan') + '"'; 187 if (cell.getAttribute('rowspan')) attrs += ' rowspan="' + cell.getAttribute('rowspan') + '"'; 188 html += '<' + cellTag + attrs + '>' + convertInline(cell) + '</' + cellTag + '>'; 189 } 190 html += '</tr>'; 191 return html; 192 } 193 194 function convertList(list) { 195 var listType = list.getAttribute('list-type'); 196 var tag = (listType === 'order') ? 'ol' : 'ul'; 197 var html = '<' + tag + '>'; 198 var items = list.querySelectorAll(':scope > list-item'); 199 items.forEach(function (item) { 200 html += '<li>' + convertBlockChildren(item) + '</li>'; 201 }); 202 html += '</' + tag + '>'; 203 return html; 204 } 205 206 function convertDispQuote(quote) { 207 var html = '<blockquote class="disp-quote">'; 208 html += convertBlockChildren(quote); 209 var attrib = quote.querySelector('attrib'); 210 if (attrib) { 211 html += '<footer>' + convertInline(attrib) + '</footer>'; 212 } 213 html += '</blockquote>'; 214 return html; 215 } 216 217 function convertSection(sec, depth) { 218 depth = depth || 1; 219 var id = sec.getAttribute('id') || ''; 220 var html = '<section id="' + id + '">'; 221 222 var title = sec.querySelector(':scope > title'); 223 if (title && title.textContent.trim()) { 224 var headingLevel = Math.min(depth + 1, 6); // h2 to h6 225 html += '<h' + headingLevel + '>' + convertInline(title) + '</h' + headingLevel + '>'; 226 } 227 228 html += convertBlockChildren(sec, depth + 1); 229 html += '</section>'; 230 return html; 231 } 232 233 // Process direct block-level children of a container 234 function convertBlockChildren(container, depth) { 235 depth = depth || 1; 236 var html = ''; 237 for (var i = 0; i < container.children.length; i++) { 238 var child = container.children[i]; 239 var tag = child.nodeName.toLowerCase(); 240 241 switch (tag) { 242 case 'title': // already handled by section 243 break; 244 case 'label': // already handled by parent 245 break; 246 case 'p': 247 html += '<p>' + convertInline(child) + '</p>'; 248 break; 249 case 'sec': 250 html += convertSection(child, depth); 251 break; 252 case 'fig': 253 html += convertFigure(child); 254 break; 255 case 'table-wrap': 256 html += convertTable(child); 257 break; 258 case 'list': 259 html += convertList(child); 260 break; 261 case 'disp-quote': 262 html += convertDispQuote(child); 263 break; 264 case 'verse-group': 265 html += '<div class="verse">'; 266 child.querySelectorAll('verse-line').forEach(function (line) { 267 html += convertInline(line) + '<br>'; 268 }); 269 html += '</div>'; 270 break; 271 case 'media': 272 var mediaSrc = child.getAttribute('xlink:href'); 273 if (mediaSrc) { 274 html += '<div class="article-media"><a href="' + mediaSrc + '" target="_blank">' + (child.textContent || 'Media') + '</a></div>'; 275 } 276 break; 277 case 'disp-formula': 278 html += '<div class="disp-formula">' + convertInline(child) + '</div>'; 279 break; 280 default:
281 // For unrecognized block elements, try inline conversion 282 var inner = convertInline(child); 283 if (inner.trim()) html += inner; 284 break; 285 } 286 } 287 return html; 288 } 289 290 // ---- Footnotes ---- 291 292 function convertFootnotes(xml) { 293 var fnGroup = xml.querySelector('fn-group'); 294 if (!fnGroup) { 295 // Also check inside <back> 296 var back = xml.querySelector('back'); 297 if (back) fnGroup = back.querySelector('fn-group'); 298 } 299 if (!fnGroup) return ''; 300 301 var fns = fnGroup.querySelectorAll('fn'); 302 if (fns.length === 0) return ''; 303 304 var html = '<section class="article-footnotes"><h2>Notes</h2><ol class="footnotes-list">'; 305 fns.forEach(function (fn) { 306 var id = fn.getAttribute('id') || ''; 307 var label = fn.querySelector(':scope > label'); 308 var content = ''; 309 fn.querySelectorAll('p').forEach(function (p) { 310 content += convertInline(p); 311 }); 312 html += '<li id="' + id + '">'; 313 if (id) { 314 html += '<a class="fn-back" href="#' + id + '-ref" aria-label="Back to text">↑</a> '; 315 } 316 html += content + '</li>'; 317 }); 318 html += '</ol></section>'; 319 return html; 320 } 321 322 // ---- References ---- 323 324 function convertReferences(xml) { 325 var refList = xml.querySelector('ref-list'); 326 if (!refList) { 327 var back = xml.querySelector('back'); 328 if (back) refList = back.querySelector('ref-list'); 329 } 330 if (!refList) return ''; 331 332 var refs = refList.querySelectorAll('ref'); 333 if (refs.length === 0) return ''; 334 335 var html = '<section class="article-references"><h2>References</h2><ol class="references-list">'; 336 refs.forEach(function (ref) { 337 var id = ref.getAttribute('id') || ''; 338 html += '<li id="' + id + '">' + convertInline(ref) + '</li>'; 339 }); 340 html += '</ol></section>'; 341 return html; 342 } 343 344 // ---- Main entry point ---- 345 346 function renderJatsXml(xml, container) { 347 var body = xml.querySelector('body'); 348 if (!body) return; 349 350 var html = ''; 351 352 // Convert body content 353 var topLevel = body.children; 354 for (var i = 0; i < topLevel.length; i++) { 355 var el = topLevel[i]; 356 var tag = el.nodeName.toLowerCase(); 357 358 switch (tag) { 359 case 'sec': 360 html += convertSection(el, 1); 361 break; 362 case 'p': 363 html += '<p>' + convertInline(el) + '</p>'; 364 break; 365 case 'fig': 366 html += convertFigure(el); 367 break; 368 case 'table-wrap': 369 html += convertTable(el); 370 break; 371 case 'list': 372 html += convertList(el); 373 break; 374 case 'disp-quote': 375 html += convertDispQuote(el); 376 break; 377 default: 378 html += convertBlockChildren(el, 1); 379 break; 380 } 381 } 382 383 // Add footnotes 384 html += convertFootnotes(xml); 385 386 // Add references (if present in XML, not all articles have them) 387 html += convertReferences(xml); 388 389 container.innerHTML = html; 390 } 391 392 // Expose globally 393 window.renderJatsXml = renderJatsXml; 394 395})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.