diff --git a/bin/render b/bin/render index e9ab46c..1619718 100755 --- a/bin/render +++ b/bin/render @@ -4,7 +4,7 @@ const fs = require('fs'); const jsdom = require('jsdom').jsdom; const serialize = require('jsdom').serializeDocument; const program = require('commander'); -const transforms = require('../dist/transforms.js'); +const transforms = require('../dist/transforms.v2.js'); program .version('0.0.1') @@ -12,7 +12,7 @@ program .parse(process.argv); const htmlString = fs.readFileSync(program.input, 'utf8'); -const data = {}; +const data = new transforms.FrontMatter; const dom = jsdom(htmlString, {features: {ProcessExternalResources: false, FetchExternalResources: false, runScripts: 'dangerously'}}); transforms.render(dom, data); transforms.distillify(dom, data); diff --git a/examples/article.html b/examples/article.html index 146b11b..e2beedf 100644 --- a/examples/article.html +++ b/examples/article.html @@ -23,8 +23,8 @@

How to Use t-SNE Effectively

Although extremely useful for visualizing high-dimensional data, t-SNE plots can sometimes be mysterious or misleading.

-
+ diff --git a/package-lock.json b/package-lock.json index f6c919f..5a99ae7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "distill-template", - "version": "2.0.0", + "version": "2.0.0-alpha", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -71,6 +71,23 @@ } } }, + "acorn-object-spread": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/acorn-object-spread/-/acorn-object-spread-1.0.0.tgz", + "integrity": "sha1-SOrQ9KjrFplaF6Dbn/xqyq2kumg=", + "dev": true, + "requires": { + "acorn": "3.3.0" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, "ajv": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", @@ -852,6 +869,44 @@ "pako": "0.2.9" } }, + "buble": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/buble/-/buble-0.15.2.tgz", + "integrity": "sha1-VH/EdIP45egXbYKqXrzLGDsC1hM=", + "dev": true, + "requires": { + "acorn": "3.3.0", + "acorn-jsx": "3.0.1", + "acorn-object-spread": "1.0.0", + "chalk": "1.1.3", + "magic-string": "0.14.0", + "minimist": "1.2.0", + "os-homedir": "1.0.2" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + }, + "magic-string": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.14.0.tgz", + "integrity": "sha1-VyJK7xcByu7Sc7F6OalW5ysXJGI=", + "dev": true, + "requires": { + "vlq": "0.2.2" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "buffer": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", @@ -3181,6 +3236,11 @@ "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=", "dev": true }, + "intersection-observer": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.4.0.tgz", + "integrity": "sha512-hBECeRcNPMrQP02IlicMCDiL19KQweoWukv35juIVehB2lE+spel5UyfTux/YCkXaR8Zz0jq3xMZeWYZBAU2SA==" + }, "invariant": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", @@ -4699,13 +4759,10 @@ } }, "rollup": { - "version": "0.45.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.45.2.tgz", - "integrity": "sha512-2+bq5GQSrocdhr+M92mOQRmF1evtLRzv9NdmEC2wo7BILvTG8irHCtD0q+zg8ikNu63iJicdN5IzyxAXRTFKOQ==", - "dev": true, - "requires": { - "source-map-support": "0.4.15" - } + "version": "0.49.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.49.2.tgz", + "integrity": "sha512-9mySqItSwq5/dXYQyFGrrzqV282EZfz4kSCU2m4e6OjgqLmIsp9zK6qNQ6wbBWR4EhASEqQMBQ/IF45jaNPAtw==", + "dev": true }, "rollup-plugin-babili": { "version": "3.1.0", @@ -4718,6 +4775,34 @@ "babel-preset-babili": "0.1.4" } }, + "rollup-plugin-buble": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-buble/-/rollup-plugin-buble-0.15.0.tgz", + "integrity": "sha1-g8PonH/SJmx5GPQbo5gDE1Gcf9A=", + "dev": true, + "requires": { + "buble": "0.15.2", + "rollup-pluginutils": "1.5.2" + }, + "dependencies": { + "estree-walker": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.2.1.tgz", + "integrity": "sha1-va/oCVOD2EFNXcLs9MkXO225QS4=", + "dev": true + }, + "rollup-pluginutils": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz", + "integrity": "sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg=", + "dev": true, + "requires": { + "estree-walker": "0.2.1", + "minimatch": "3.0.4" + } + } + } + }, "rollup-plugin-commonjs": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-7.1.0.tgz", diff --git a/package.json b/package.json index c1306b5..0101cdd 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "scripts": { "start": "rollup -c rollup.config.dev.js -w", - "serve": "python3 -m http.server --bind 127.0.0.1", + "serve": "python3 -m http.server --bind 127.0.0.1 8888", "test": "mocha", "lint": "eslint", "build": "rollup -c rollup.config.js" @@ -32,6 +32,7 @@ "prismjs": "^1.6.0", "rollup": "latest", "rollup-plugin-babili": "^3.1.0", + "rollup-plugin-buble": "^0.15.0", "rollup-plugin-commonjs": "^7.0.0", "rollup-plugin-copy": "^0.2.3", "rollup-plugin-gzip": "^1.2.0", diff --git a/rollup.config.dev.js b/rollup.config.dev.js index c87b358..1dd57c3 100644 --- a/rollup.config.dev.js +++ b/rollup.config.dev.js @@ -1,19 +1,20 @@ import resolve from 'rollup-plugin-node-resolve'; import string from 'rollup-plugin-string'; import commonjs from 'rollup-plugin-commonjs'; +import buble from 'rollup-plugin-buble'; const componentsConfig = { - entry: 'src/components.js', - targets: [{format: 'umd', moduleName: 'dl', dest: 'dist/template.v2.js'}], + input: 'src/components.js', + output: [{format: 'umd', name: 'dl', file: 'dist/template.v2.js'}], }; const transformsConfig = { - entry: 'src/transforms.js', - targets: [{format: 'umd', moduleName: 'dl', dest: 'dist/transforms.v2.js'}], + input: 'src/transforms.js', + output: [{format: 'umd', name: 'dl', file: 'dist/transforms.v2.js'}], }; const defaultConfig = { - sourceMap: true, + sourcemap: true, plugins: [ resolve({ jsnext: true, @@ -29,6 +30,12 @@ const defaultConfig = { Object.assign(componentsConfig, defaultConfig); Object.assign(transformsConfig, defaultConfig); +// transpile transforms so the node render script works… +transformsConfig.plugins.push( + buble({ + target: { 'node': 6 } + })); + export default [ componentsConfig, transformsConfig, diff --git a/src/components.js b/src/components.js index 877e195..f835bcf 100644 --- a/src/components.js +++ b/src/components.js @@ -1,5 +1,6 @@ /* Static styles and other modules */ -import './styles/styles'; +import { makeStyleTag } from './styles/styles'; +makeStyleTag(document); /* Components */ import { Abstract } from './components/d-abstract'; diff --git a/src/components/d-abstract.js b/src/components/d-abstract.js index 553f33f..ff46ae5 100644 --- a/src/components/d-abstract.js +++ b/src/components/d-abstract.js @@ -7,7 +7,8 @@ const T = Template('d-abstract', ` display: block; font-size: 23px; line-height: 1.7em; - margin-bottom: 140px; + margin-top: 64px; + margin-bottom: 64px; } ${body('d-abstract')} diff --git a/src/components/d-acknowledgements.js b/src/components/d-acknowledgements.js index 842e652..1a70a95 100644 --- a/src/components/d-acknowledgements.js +++ b/src/components/d-acknowledgements.js @@ -2,6 +2,11 @@ import { Template } from '../mixins/template'; const T = Template('d-acknowledgements', ` diff --git a/src/components/d-appendix.js b/src/components/d-appendix.js index ae48e7e..8a54417 100644 --- a/src/components/d-appendix.js +++ b/src/components/d-appendix.js @@ -14,9 +14,10 @@ const T = Template('d-appendix', ` margin-bottom: 0; border-top: 1px solid rgba(0,0,0,0.1); color: rgba(0,0,0,0.5); - background: hsl(180, 5%, 98%); padding-top: 36px; padding-bottom: 48px; + + contain: content; } ${body('.content')} diff --git a/src/components/d-article.js b/src/components/d-article.js index 63e5715..ba9996f 100644 --- a/src/components/d-article.js +++ b/src/components/d-article.js @@ -2,7 +2,11 @@ import { Template } from '../mixins/template'; import { Controller } from '../controller'; const T = Template('d-article', ` - + `, false); // export function addInferableTags(dom, frontMatter) { diff --git a/src/components/d-bibliography.js b/src/components/d-bibliography.js index c59589c..c868f23 100644 --- a/src/components/d-bibliography.js +++ b/src/components/d-bibliography.js @@ -4,6 +4,9 @@ import { bibliography_cite } from '../helpers/citation'; export const templateString = ` +d-byline .byline::after { + content: ""; + display: block; + border-bottom: solid 1px #999; + width: 40px; + margin-top: 60px; +} + +d-byline a, +d-article d-byline a { + color: rgba(0, 0, 0, 0.8); + text-decoration: none; + border-bottom: none; +} + +d-article d-byline a:hover { + text-decoration: underline; + border-bottom: none; +} + +d-byline .authors { + text-align: left; +} + +d-byline .author { + margin-right: 12px; +} + +d-byline .author .name { + font-weight: 600; + display: inline; + text-transform: uppercase; + margin-right: 10px; +} + +d-byline .author .affiliation { + display: inline; +} + +d-byline .date { + display: inline; + text-align: left; + margin-right: 12px; +} + +d-byline .date .year, +d-byline .date .month { + display: inline; +} + +d-byline .citation { + display: inline; + text-align: left; +} + +d-byline .citation div { + display: inline; +} +`; -
-
-`, true); export function bylineTemplate(frontMatter) { return ` +
${frontMatter.authors.map( author => `
${author.personalURL ? @@ -126,14 +97,16 @@ export function bylineTemplate(frontMatter) {
${frontMatter.concatenatedAuthors}, ${frontMatter.publishedYear}
- `; +
+`; } -export class Byline extends T(HTMLElement) { +export class Byline extends HTMLElement { + + static get is() { return 'd-byline'; } set frontMatter(frontMatter) { - const container = this.root.querySelector('.byline'); - container.innerHTML = bylineTemplate(frontMatter); + this.innerHTML = bylineTemplate(frontMatter); } } diff --git a/src/components/d-figure.js b/src/components/d-figure.js index 571612a..2df9302 100644 --- a/src/components/d-figure.js +++ b/src/components/d-figure.js @@ -17,8 +17,39 @@ export class Figure extends HTMLElement { static get is() { return 'd-figure'; } + static get readyQueue() { + if (!Figure._readyQueue) { + Figure._readyQueue = []; + } + return Figure._readyQueue; + } + + static addToReadyQueue(figure) { + if (Figure.readyQueue.indexOf(figure) === -1) { + Figure.readyQueue.push(figure); + Figure.runReadyQueue(); + } + } + + static runReadyQueue() { + // console.log("Checking to run readyQueue, length: " + Figure.readyQueue.length + ", scrolling: " + Figure.isScrolling); + if (Figure.isScrolling) return; + + // console.log("Running ready Queue"); + const figure = Figure.readyQueue + .sort((a,b) => a._seenOnScreen - b._seenOnScreen ) + .filter((figure) => !figure._ready) + .pop(); + if (figure) { + figure.ready(); + requestAnimationFrame(Figure.runReadyQueue); + } + + } + constructor() { super(); + // debugger this._ready = false; this._onscreen = false; this._offscreen = true; @@ -54,7 +85,7 @@ export class Figure extends HTMLElement { for (const entry of entries) { const figure = entry.target; if (entry.isIntersecting && !figure._ready) { - figure.ready(); + Figure.addToReadyQueue(figure); } } } @@ -74,7 +105,8 @@ export class Figure extends HTMLElement { for (const entry of entries) { const figure = entry.target; if (entry.isIntersecting) { - if (!figure._ready) { figure.ready(); } + figure._seenOnScreen = new Date(); + // if (!figure._ready) { figure.ready(); } if (figure._offscreen) { figure.onscreen(); } } else { if (figure._onscreen) { figure.offscreen(); } @@ -87,19 +119,22 @@ export class Figure extends HTMLElement { addEventListener(eventName, callback) { super.addEventListener(eventName, callback); // if we had already dispatched something while presumingly no one was listening, we do so again - setTimeout(() => { - if (this._ready && eventName === 'ready') { - this.ready(); + // debugger + if (eventName === 'ready') { + if (Figure.readyQueue.indexOf(this) !== -1) { + this._ready = false; + Figure.runReadyQueue(); } - if (this._onscreen && eventName === 'onscreen') { - this.onscreen(); - } - }, 1); + } + if (eventName === 'onscreen') { + this.onscreen(); + } } // Custom Events ready() { + // debugger this._ready = true; Figure.marginObserver.unobserve(this); const event = new CustomEvent('ready'); @@ -121,3 +156,20 @@ export class Figure extends HTMLElement { } } + +if (typeof window !== 'undefined') { + + Figure.isScrolling = false; + let timeout; + const resetTimer = () => { + Figure.isScrolling = true; + clearTimeout(timeout); + timeout = setTimeout(() => { + Figure.isScrolling = false; + console.log('Stopped Scrolling') + Figure.runReadyQueue(); + }, 500); + }; + window.addEventListener('scroll', resetTimer, true); + +} diff --git a/src/components/d-footnote-list.js b/src/components/d-footnote-list.js index 0ca81be..cc2beca 100644 --- a/src/components/d-footnote-list.js +++ b/src/components/d-footnote-list.js @@ -2,6 +2,10 @@ import { Template } from '../mixins/template'; const T = Template('d-footnote-list', ` -`, false); + static get is() { return 'd-toc'; } -export class TOC extends T(HTMLElement) { + connectedCallback() { + if (!this.getAttribute('prerendered')) { + window.onload = () => { + const article = document.querySelector('d-article'); + const headings = article.querySelectorAll('h2, h3'); + renderTOC(this, headings); + }; + } + } } + +export function renderTOC(element, headings) { + + let ToC =` + + +

Table of contents

+ '; + element.innerHTML = ToC; +} diff --git a/src/controller.js b/src/controller.js index bd70002..831cb35 100644 --- a/src/controller.js +++ b/src/controller.js @@ -108,12 +108,12 @@ export const Controller = { frontMatter.mergeFromYMLFrontmatter(data); const appendix = document.querySelector('distill-appendix'); - if (appendix) { + if (appendix && !appendix.getAttribute('prerendered')) { appendix.frontMatter = frontMatter; } const byline = document.querySelector('d-byline'); - if (byline) { + if (byline && !byline.getAttribute('prerendered')) { byline.frontMatter = frontMatter; } }, diff --git a/src/distill-components/distill-appendix.js b/src/distill-components/distill-appendix.js index 791a982..94619df 100644 --- a/src/distill-components/distill-appendix.js +++ b/src/distill-components/distill-appendix.js @@ -2,6 +2,9 @@ import { serializeFrontmatterToBibtex } from '../helpers/bibtex'; const styles = ` - -`; - -const template = ` - -`; +import { bylineTemplate } from '../components/d-byline.js'; export default function(dom, data) { - let el = dom.querySelector('dt-byline'); - if (el) { - el.innerHTML = html + mustache.render(template, data); + const byline = dom.querySelector('d-byline'); + if (byline) { + byline.innerHTML = bylineTemplate(data); + byline.setAttribute('prerendered', true); } } diff --git a/src/transforms/helpers/bibtex.js b/src/transforms/helpers/bibtex.js deleted file mode 100644 index 786e83a..0000000 --- a/src/transforms/helpers/bibtex.js +++ /dev/null @@ -1,33 +0,0 @@ -import bibtexParse from 'bibtex-parse-js'; - -function normalizeTag(string) { - return string - .replace(/[\t\n ]+/g, ' ') - .replace(/{\\["^`.'acu~Hvs]( )?([a-zA-Z])}/g, (full, x, char) => char) - .replace(/{\\([a-zA-Z])}/g, (full, char) => char); -} - -export function parseBibtex(bibtex) { - const bibliography = new Map(); - const parsedEntries = bibtexParse.toJSON(bibtex); - for (const entry of parsedEntries) { - // normalize tags; note entryTags is an object, not Map - for (const [key, value] of Object.entries(entry.entryTags)) { - entry.entryTags[key] = normalizeTag(value); - } - entry.entryTags.type = entry.entryType; - // add to bibliography - bibliography.set(entry.citationKey, entry.entryTags); - } - return bibliography; -} - -export function serializeFrontmatterToBibtex(frontMatter) { - return `@article{${frontMatter.slug}, - author = {${frontMatter.bibtexAuthors}}, - title = {${frontMatter.title}}, - journal = {${frontMatter.journal.title}}, - year = {${frontMatter.publishedYear}}, - note = {${frontMatter.url}} -}`; -} diff --git a/src/transforms/helpers/citation.js b/src/transforms/helpers/citation.js deleted file mode 100644 index 8062b6f..0000000 --- a/src/transforms/helpers/citation.js +++ /dev/null @@ -1,165 +0,0 @@ -export function inline_cite_short(keys){ - function cite_string(key){ - if (key in data.bibliography){ - var n = data.citations.indexOf(key)+1; - return ''+n; - } else { - return '?'; - } - } - return '['+keys.map(cite_string).join(', ')+']'; -} - -export function inline_cite_long(keys){ - function cite_string(key){ - if (key in data.bibliography){ - var ent = data.bibliography[key]; - var names = ent.author.split(' and '); - names = names.map(name => name.split(',')[0].trim()); - var year = ent.year; - if (names.length == 1) return names[0] + ', ' + year; - if (names.length == 2) return names[0] + ' & ' + names[1] + ', ' + year; - if (names.length > 2) return names[0] + ', et al., ' + year; - } else { - return '?'; - } - } - return keys.map(cite_string).join(', '); -} - -function author_string(ent, template, sep, finalSep){ - var names = ent.author.split(' and '); - let name_strings = names.map(name => { - name = name.trim(); - if (name.indexOf(',') != -1){ - var last = name.split(',')[0].trim(); - var firsts = name.split(',')[1]; - } else { - var last = name.split(' ').slice(-1)[0].trim(); - var firsts = name.split(' ').slice(0,-1).join(' '); - } - var initials = ''; - if (firsts != undefined) { - initials = firsts.trim().split(' ').map(s => s.trim()[0]); - initials = initials.join('.')+'.'; - } - return template.replace('${F}', firsts) - .replace('${L}', last) - .replace('${I}', initials); - }); - if (names.length > 1) { - var str = name_strings.slice(0, names.length-1).join(sep); - str += (finalSep || sep) + name_strings[names.length-1]; - return str; - } else { - return name_strings[0]; - } -} - -function venue_string(ent) { - var cite = (ent.journal || ent.booktitle || ''); - if ('volume' in ent){ - var issue = ent.issue || ent.number; - issue = (issue != undefined)? '('+issue+')' : ''; - cite += ', Vol ' + ent.volume + issue; - } - if ('pages' in ent){ - cite += ', pp. ' + ent.pages; - } - if (cite != '') cite += '. '; - if ('publisher' in ent){ - cite += ent.publisher; - if (cite[cite.length-1] != '.') cite += '.'; - } - return cite; -} - -function link_string(ent){ - if ('url' in ent){ - var url = ent.url; - var arxiv_match = (/arxiv\.org\/abs\/([0-9\.]*)/).exec(url); - if (arxiv_match != null){ - url = `http://arxiv.org/pdf/${arxiv_match[1]}.pdf`; - } - - if (url.slice(-4) == '.pdf'){ - var label = 'PDF'; - } else if (url.slice(-5) == '.html') { - var label = 'HTML'; - } - return `  [${label||'link'}]`; - }/* else if ("doi" in ent){ - return `  [DOI]`; - }*/ else { - return ''; - } -} -function doi_string(ent, new_line){ - if ('doi' in ent) { - return `${new_line?'
':''} DOI: ${ent.doi}`; - } else { - return ''; - } -} - -export function bibliography_cite(ent, fancy){ - if (ent){ - var cite = '' + ent.title + ' '; - cite += link_string(ent) + '
'; - cite += author_string(ent, '${L}, ${I}', ', ', ' and '); - if (ent.year || ent.date){ - cite += ', ' + (ent.year || ent.date) + '. '; - } else { - cite += '. '; - } - cite += venue_string(ent); - cite += doi_string(ent); - return cite; - /*var cite = author_string(ent, "${L}, ${I}", ", ", " and "); - if (ent.year || ent.date){ - cite += ", " + (ent.year || ent.date) + ". " - } else { - cite += ". " - } - cite += "" + ent.title + ". "; - cite += venue_string(ent); - cite += doi_string(ent); - cite += link_string(ent); - return cite*/ - } else { - return '?'; - } -} - -export function hover_cite(ent){ - if (ent){ - var cite = ''; - cite += '' + ent.title + ''; - cite += link_string(ent); - cite += '
'; - - var a_str = author_string(ent, '${I} ${L}', ', ') + '.'; - var v_str = venue_string(ent).trim() + ' ' + ent.year + '. ' + doi_string(ent, true); - - if ((a_str+v_str).length < Math.min(40, ent.title.length)) { - cite += a_str + ' ' + v_str; - } else { - cite += a_str + '
' + v_str; - } - return cite; - } else { - return '?'; - } -} - - -//https://scholar.google.com/scholar?q=allintitle%3ADocument+author%3Aolah -function get_GS_URL(ent){ - if (ent){ - var names = ent.author.split(' and '); - names = names.map(name => name.split(',')[0].trim()); - var title = ent.title.split(' ');//.replace(/[,:]/, "") - var url = 'http://search.labs.crossref.org/dois?';//""https://scholar.google.com/scholar?" - url += uris({q: names.join(' ') + ' ' + title.join(' ')}); - } -} diff --git a/src/transforms/helpers/hover-box.js b/src/transforms/helpers/hover-box.js deleted file mode 100644 index be19f57..0000000 --- a/src/transforms/helpers/hover-box.js +++ /dev/null @@ -1,108 +0,0 @@ -function make_hover_css(pos) { - const pretty = window.innerWidth > 600; - const padding = pretty? 18 : 12; - const outer_padding = pretty ? 18 : 0; - const bbox = document.querySelector('body').getBoundingClientRect(); - let left = pos[0] - bbox.left, top = pos[1] - bbox.top; - let width = Math.min(window.innerWidth-2*outer_padding, 648); - left = Math.min(left, window.innerWidth-width-outer_padding); - width = width - 2 * padding; - return (`position: absolute; - background-color: #FFF; - opacity: 0.95; - max-width: ${width}px; - top: ${top}px; - left: ${left}px; - border: 1px solid rgba(0, 0, 0, 0.25); - padding: ${padding}px; - border-radius: ${pretty? 3 : 0}px; - box-shadow: 0px 2px 10px 2px rgba(0, 0, 0, 0.2); - z-index: ${1e6};`); -} - -export class HoverBox { - - constructor(contentHTML, triggerElement) { - this.visible = false; - // div hold teh contents of the box that will become visible - this.div = contentHTML; - this.bindDivEvents(this.div); - // triggerElement holds the element that needs to be hovered etc to show contents - this.triggerElement = triggerElement; - this.bindTriggerEvents(this.triggerElement); - this.hide(); - } - - bindDivEvents(node) { - // For mice, same behavior as hovering on links - this.div.addEventListener('mouseover', () => { - if (!this.visible) this.showAtNode(node); - this.stopTimeout(); - }); - this.div.addEventListener('mouseout', () => { - this.extendTimeout(250); - }); - // Don't trigger body touchstart event when touching within box - this.div.addEventListener('touchstart', (event) => { - event.stopPropagation(); - }, {passive: true}); - // Close box when touching outside box - document.body.addEventListener('touchstart', () => { - this.hide(); - }, {passive: true}); - } - - bindTriggerEvents(node) { - node.addEventListener('mouseover', () => { - if (!this.visible) { - this.showAtNode(node); - } - this.stopTimeout(); - }); - - node.addEventListener('mouseout', () => { - this.extendTimeout(250); - }); - - node.addEventListener('touchstart', (event) => { - if (this.visible) { - this.hide(); - } else { - this.showAtNode(node); - } - // Don't trigger body touchstart event when touching link - event.stopPropagation(); - }, {passive: true}); - } - - show(position) { - this.visible = true; - const css = make_hover_css(position); - this.div.setAttribute('style', css ); - } - - showAtNode(node) { - const bbox = node.getBoundingClientRect(); - this.show([bbox.right, bbox.bottom]); - } - - hide() { - this.visible = false; - this.div.setAttribute('style', 'display:none'); - this.stopTimeout(); - } - - stopTimeout() { - if (this.timeout) { - clearTimeout(this.timeout); - } - } - - extendTimeout(time) { - this.stopTimeout(); - this.timeout = setTimeout(() => { - this.hide(); - }, time); - } - -} diff --git a/src/transforms/helpers/layout.js b/src/transforms/helpers/layout.js deleted file mode 100644 index f2dc8be..0000000 --- a/src/transforms/helpers/layout.js +++ /dev/null @@ -1,33 +0,0 @@ -// const marginSmall = 16; -// const marginLarge = 3 * marginSmall; -// const margin = marginSmall + marginLarge; -// const gutter = marginSmall; -// const outsetAmount = margin / 2; -// const numCols = 4; -// const numGutters = numCols - 1; -// const columnWidth = (768 - 2 * marginLarge - numGutters * gutter) / numCols; -// -// const screenwidth = 768; -// const pageWidth = screenwidth - 2 * marginLarge; -// const bodyWidth = pageWidth - columnWidth - gutter; - -export function body(selector) { - return `${selector} { - grid-column: margin-left / body; - } - `; -} - -export function page(selector) { - return `${selector} { - grid-column: margin-left / page; - } - `; -} - -export function screen(selector) { - return `${selector} { - grid-column: start / end; - } - `; -} diff --git a/src/transforms/helpers0/bibtex.js b/src/transforms/helpers0/bibtex.js deleted file mode 100644 index 786e83a..0000000 --- a/src/transforms/helpers0/bibtex.js +++ /dev/null @@ -1,33 +0,0 @@ -import bibtexParse from 'bibtex-parse-js'; - -function normalizeTag(string) { - return string - .replace(/[\t\n ]+/g, ' ') - .replace(/{\\["^`.'acu~Hvs]( )?([a-zA-Z])}/g, (full, x, char) => char) - .replace(/{\\([a-zA-Z])}/g, (full, char) => char); -} - -export function parseBibtex(bibtex) { - const bibliography = new Map(); - const parsedEntries = bibtexParse.toJSON(bibtex); - for (const entry of parsedEntries) { - // normalize tags; note entryTags is an object, not Map - for (const [key, value] of Object.entries(entry.entryTags)) { - entry.entryTags[key] = normalizeTag(value); - } - entry.entryTags.type = entry.entryType; - // add to bibliography - bibliography.set(entry.citationKey, entry.entryTags); - } - return bibliography; -} - -export function serializeFrontmatterToBibtex(frontMatter) { - return `@article{${frontMatter.slug}, - author = {${frontMatter.bibtexAuthors}}, - title = {${frontMatter.title}}, - journal = {${frontMatter.journal.title}}, - year = {${frontMatter.publishedYear}}, - note = {${frontMatter.url}} -}`; -} diff --git a/src/transforms/helpers0/citation.js b/src/transforms/helpers0/citation.js deleted file mode 100644 index 8062b6f..0000000 --- a/src/transforms/helpers0/citation.js +++ /dev/null @@ -1,165 +0,0 @@ -export function inline_cite_short(keys){ - function cite_string(key){ - if (key in data.bibliography){ - var n = data.citations.indexOf(key)+1; - return ''+n; - } else { - return '?'; - } - } - return '['+keys.map(cite_string).join(', ')+']'; -} - -export function inline_cite_long(keys){ - function cite_string(key){ - if (key in data.bibliography){ - var ent = data.bibliography[key]; - var names = ent.author.split(' and '); - names = names.map(name => name.split(',')[0].trim()); - var year = ent.year; - if (names.length == 1) return names[0] + ', ' + year; - if (names.length == 2) return names[0] + ' & ' + names[1] + ', ' + year; - if (names.length > 2) return names[0] + ', et al., ' + year; - } else { - return '?'; - } - } - return keys.map(cite_string).join(', '); -} - -function author_string(ent, template, sep, finalSep){ - var names = ent.author.split(' and '); - let name_strings = names.map(name => { - name = name.trim(); - if (name.indexOf(',') != -1){ - var last = name.split(',')[0].trim(); - var firsts = name.split(',')[1]; - } else { - var last = name.split(' ').slice(-1)[0].trim(); - var firsts = name.split(' ').slice(0,-1).join(' '); - } - var initials = ''; - if (firsts != undefined) { - initials = firsts.trim().split(' ').map(s => s.trim()[0]); - initials = initials.join('.')+'.'; - } - return template.replace('${F}', firsts) - .replace('${L}', last) - .replace('${I}', initials); - }); - if (names.length > 1) { - var str = name_strings.slice(0, names.length-1).join(sep); - str += (finalSep || sep) + name_strings[names.length-1]; - return str; - } else { - return name_strings[0]; - } -} - -function venue_string(ent) { - var cite = (ent.journal || ent.booktitle || ''); - if ('volume' in ent){ - var issue = ent.issue || ent.number; - issue = (issue != undefined)? '('+issue+')' : ''; - cite += ', Vol ' + ent.volume + issue; - } - if ('pages' in ent){ - cite += ', pp. ' + ent.pages; - } - if (cite != '') cite += '. '; - if ('publisher' in ent){ - cite += ent.publisher; - if (cite[cite.length-1] != '.') cite += '.'; - } - return cite; -} - -function link_string(ent){ - if ('url' in ent){ - var url = ent.url; - var arxiv_match = (/arxiv\.org\/abs\/([0-9\.]*)/).exec(url); - if (arxiv_match != null){ - url = `http://arxiv.org/pdf/${arxiv_match[1]}.pdf`; - } - - if (url.slice(-4) == '.pdf'){ - var label = 'PDF'; - } else if (url.slice(-5) == '.html') { - var label = 'HTML'; - } - return `  [${label||'link'}]`; - }/* else if ("doi" in ent){ - return `  [DOI]`; - }*/ else { - return ''; - } -} -function doi_string(ent, new_line){ - if ('doi' in ent) { - return `${new_line?'
':''} DOI: ${ent.doi}`; - } else { - return ''; - } -} - -export function bibliography_cite(ent, fancy){ - if (ent){ - var cite = '' + ent.title + ' '; - cite += link_string(ent) + '
'; - cite += author_string(ent, '${L}, ${I}', ', ', ' and '); - if (ent.year || ent.date){ - cite += ', ' + (ent.year || ent.date) + '. '; - } else { - cite += '. '; - } - cite += venue_string(ent); - cite += doi_string(ent); - return cite; - /*var cite = author_string(ent, "${L}, ${I}", ", ", " and "); - if (ent.year || ent.date){ - cite += ", " + (ent.year || ent.date) + ". " - } else { - cite += ". " - } - cite += "" + ent.title + ". "; - cite += venue_string(ent); - cite += doi_string(ent); - cite += link_string(ent); - return cite*/ - } else { - return '?'; - } -} - -export function hover_cite(ent){ - if (ent){ - var cite = ''; - cite += '' + ent.title + ''; - cite += link_string(ent); - cite += '
'; - - var a_str = author_string(ent, '${I} ${L}', ', ') + '.'; - var v_str = venue_string(ent).trim() + ' ' + ent.year + '. ' + doi_string(ent, true); - - if ((a_str+v_str).length < Math.min(40, ent.title.length)) { - cite += a_str + ' ' + v_str; - } else { - cite += a_str + '
' + v_str; - } - return cite; - } else { - return '?'; - } -} - - -//https://scholar.google.com/scholar?q=allintitle%3ADocument+author%3Aolah -function get_GS_URL(ent){ - if (ent){ - var names = ent.author.split(' and '); - names = names.map(name => name.split(',')[0].trim()); - var title = ent.title.split(' ');//.replace(/[,:]/, "") - var url = 'http://search.labs.crossref.org/dois?';//""https://scholar.google.com/scholar?" - url += uris({q: names.join(' ') + ' ' + title.join(' ')}); - } -} diff --git a/src/transforms/helpers0/hover-box.js b/src/transforms/helpers0/hover-box.js deleted file mode 100644 index be19f57..0000000 --- a/src/transforms/helpers0/hover-box.js +++ /dev/null @@ -1,108 +0,0 @@ -function make_hover_css(pos) { - const pretty = window.innerWidth > 600; - const padding = pretty? 18 : 12; - const outer_padding = pretty ? 18 : 0; - const bbox = document.querySelector('body').getBoundingClientRect(); - let left = pos[0] - bbox.left, top = pos[1] - bbox.top; - let width = Math.min(window.innerWidth-2*outer_padding, 648); - left = Math.min(left, window.innerWidth-width-outer_padding); - width = width - 2 * padding; - return (`position: absolute; - background-color: #FFF; - opacity: 0.95; - max-width: ${width}px; - top: ${top}px; - left: ${left}px; - border: 1px solid rgba(0, 0, 0, 0.25); - padding: ${padding}px; - border-radius: ${pretty? 3 : 0}px; - box-shadow: 0px 2px 10px 2px rgba(0, 0, 0, 0.2); - z-index: ${1e6};`); -} - -export class HoverBox { - - constructor(contentHTML, triggerElement) { - this.visible = false; - // div hold teh contents of the box that will become visible - this.div = contentHTML; - this.bindDivEvents(this.div); - // triggerElement holds the element that needs to be hovered etc to show contents - this.triggerElement = triggerElement; - this.bindTriggerEvents(this.triggerElement); - this.hide(); - } - - bindDivEvents(node) { - // For mice, same behavior as hovering on links - this.div.addEventListener('mouseover', () => { - if (!this.visible) this.showAtNode(node); - this.stopTimeout(); - }); - this.div.addEventListener('mouseout', () => { - this.extendTimeout(250); - }); - // Don't trigger body touchstart event when touching within box - this.div.addEventListener('touchstart', (event) => { - event.stopPropagation(); - }, {passive: true}); - // Close box when touching outside box - document.body.addEventListener('touchstart', () => { - this.hide(); - }, {passive: true}); - } - - bindTriggerEvents(node) { - node.addEventListener('mouseover', () => { - if (!this.visible) { - this.showAtNode(node); - } - this.stopTimeout(); - }); - - node.addEventListener('mouseout', () => { - this.extendTimeout(250); - }); - - node.addEventListener('touchstart', (event) => { - if (this.visible) { - this.hide(); - } else { - this.showAtNode(node); - } - // Don't trigger body touchstart event when touching link - event.stopPropagation(); - }, {passive: true}); - } - - show(position) { - this.visible = true; - const css = make_hover_css(position); - this.div.setAttribute('style', css ); - } - - showAtNode(node) { - const bbox = node.getBoundingClientRect(); - this.show([bbox.right, bbox.bottom]); - } - - hide() { - this.visible = false; - this.div.setAttribute('style', 'display:none'); - this.stopTimeout(); - } - - stopTimeout() { - if (this.timeout) { - clearTimeout(this.timeout); - } - } - - extendTimeout(time) { - this.stopTimeout(); - this.timeout = setTimeout(() => { - this.hide(); - }, time); - } - -} diff --git a/src/transforms/helpers0/layout.js b/src/transforms/helpers0/layout.js deleted file mode 100644 index f2dc8be..0000000 --- a/src/transforms/helpers0/layout.js +++ /dev/null @@ -1,33 +0,0 @@ -// const marginSmall = 16; -// const marginLarge = 3 * marginSmall; -// const margin = marginSmall + marginLarge; -// const gutter = marginSmall; -// const outsetAmount = margin / 2; -// const numCols = 4; -// const numGutters = numCols - 1; -// const columnWidth = (768 - 2 * marginLarge - numGutters * gutter) / numCols; -// -// const screenwidth = 768; -// const pageWidth = screenwidth - 2 * marginLarge; -// const bodyWidth = pageWidth - columnWidth - gutter; - -export function body(selector) { - return `${selector} { - grid-column: margin-left / body; - } - `; -} - -export function page(selector) { - return `${selector} { - grid-column: margin-left / page; - } - `; -} - -export function screen(selector) { - return `${selector} { - grid-column: start / end; - } - `; -} diff --git a/src/transforms/helpers1/bibtex.js b/src/transforms/helpers1/bibtex.js deleted file mode 100644 index 786e83a..0000000 --- a/src/transforms/helpers1/bibtex.js +++ /dev/null @@ -1,33 +0,0 @@ -import bibtexParse from 'bibtex-parse-js'; - -function normalizeTag(string) { - return string - .replace(/[\t\n ]+/g, ' ') - .replace(/{\\["^`.'acu~Hvs]( )?([a-zA-Z])}/g, (full, x, char) => char) - .replace(/{\\([a-zA-Z])}/g, (full, char) => char); -} - -export function parseBibtex(bibtex) { - const bibliography = new Map(); - const parsedEntries = bibtexParse.toJSON(bibtex); - for (const entry of parsedEntries) { - // normalize tags; note entryTags is an object, not Map - for (const [key, value] of Object.entries(entry.entryTags)) { - entry.entryTags[key] = normalizeTag(value); - } - entry.entryTags.type = entry.entryType; - // add to bibliography - bibliography.set(entry.citationKey, entry.entryTags); - } - return bibliography; -} - -export function serializeFrontmatterToBibtex(frontMatter) { - return `@article{${frontMatter.slug}, - author = {${frontMatter.bibtexAuthors}}, - title = {${frontMatter.title}}, - journal = {${frontMatter.journal.title}}, - year = {${frontMatter.publishedYear}}, - note = {${frontMatter.url}} -}`; -} diff --git a/src/transforms/helpers1/citation.js b/src/transforms/helpers1/citation.js deleted file mode 100644 index 8062b6f..0000000 --- a/src/transforms/helpers1/citation.js +++ /dev/null @@ -1,165 +0,0 @@ -export function inline_cite_short(keys){ - function cite_string(key){ - if (key in data.bibliography){ - var n = data.citations.indexOf(key)+1; - return ''+n; - } else { - return '?'; - } - } - return '['+keys.map(cite_string).join(', ')+']'; -} - -export function inline_cite_long(keys){ - function cite_string(key){ - if (key in data.bibliography){ - var ent = data.bibliography[key]; - var names = ent.author.split(' and '); - names = names.map(name => name.split(',')[0].trim()); - var year = ent.year; - if (names.length == 1) return names[0] + ', ' + year; - if (names.length == 2) return names[0] + ' & ' + names[1] + ', ' + year; - if (names.length > 2) return names[0] + ', et al., ' + year; - } else { - return '?'; - } - } - return keys.map(cite_string).join(', '); -} - -function author_string(ent, template, sep, finalSep){ - var names = ent.author.split(' and '); - let name_strings = names.map(name => { - name = name.trim(); - if (name.indexOf(',') != -1){ - var last = name.split(',')[0].trim(); - var firsts = name.split(',')[1]; - } else { - var last = name.split(' ').slice(-1)[0].trim(); - var firsts = name.split(' ').slice(0,-1).join(' '); - } - var initials = ''; - if (firsts != undefined) { - initials = firsts.trim().split(' ').map(s => s.trim()[0]); - initials = initials.join('.')+'.'; - } - return template.replace('${F}', firsts) - .replace('${L}', last) - .replace('${I}', initials); - }); - if (names.length > 1) { - var str = name_strings.slice(0, names.length-1).join(sep); - str += (finalSep || sep) + name_strings[names.length-1]; - return str; - } else { - return name_strings[0]; - } -} - -function venue_string(ent) { - var cite = (ent.journal || ent.booktitle || ''); - if ('volume' in ent){ - var issue = ent.issue || ent.number; - issue = (issue != undefined)? '('+issue+')' : ''; - cite += ', Vol ' + ent.volume + issue; - } - if ('pages' in ent){ - cite += ', pp. ' + ent.pages; - } - if (cite != '') cite += '. '; - if ('publisher' in ent){ - cite += ent.publisher; - if (cite[cite.length-1] != '.') cite += '.'; - } - return cite; -} - -function link_string(ent){ - if ('url' in ent){ - var url = ent.url; - var arxiv_match = (/arxiv\.org\/abs\/([0-9\.]*)/).exec(url); - if (arxiv_match != null){ - url = `http://arxiv.org/pdf/${arxiv_match[1]}.pdf`; - } - - if (url.slice(-4) == '.pdf'){ - var label = 'PDF'; - } else if (url.slice(-5) == '.html') { - var label = 'HTML'; - } - return `  [${label||'link'}]`; - }/* else if ("doi" in ent){ - return `  [DOI]`; - }*/ else { - return ''; - } -} -function doi_string(ent, new_line){ - if ('doi' in ent) { - return `${new_line?'
':''} DOI: ${ent.doi}`; - } else { - return ''; - } -} - -export function bibliography_cite(ent, fancy){ - if (ent){ - var cite = '' + ent.title + ' '; - cite += link_string(ent) + '
'; - cite += author_string(ent, '${L}, ${I}', ', ', ' and '); - if (ent.year || ent.date){ - cite += ', ' + (ent.year || ent.date) + '. '; - } else { - cite += '. '; - } - cite += venue_string(ent); - cite += doi_string(ent); - return cite; - /*var cite = author_string(ent, "${L}, ${I}", ", ", " and "); - if (ent.year || ent.date){ - cite += ", " + (ent.year || ent.date) + ". " - } else { - cite += ". " - } - cite += "" + ent.title + ". "; - cite += venue_string(ent); - cite += doi_string(ent); - cite += link_string(ent); - return cite*/ - } else { - return '?'; - } -} - -export function hover_cite(ent){ - if (ent){ - var cite = ''; - cite += '' + ent.title + ''; - cite += link_string(ent); - cite += '
'; - - var a_str = author_string(ent, '${I} ${L}', ', ') + '.'; - var v_str = venue_string(ent).trim() + ' ' + ent.year + '. ' + doi_string(ent, true); - - if ((a_str+v_str).length < Math.min(40, ent.title.length)) { - cite += a_str + ' ' + v_str; - } else { - cite += a_str + '
' + v_str; - } - return cite; - } else { - return '?'; - } -} - - -//https://scholar.google.com/scholar?q=allintitle%3ADocument+author%3Aolah -function get_GS_URL(ent){ - if (ent){ - var names = ent.author.split(' and '); - names = names.map(name => name.split(',')[0].trim()); - var title = ent.title.split(' ');//.replace(/[,:]/, "") - var url = 'http://search.labs.crossref.org/dois?';//""https://scholar.google.com/scholar?" - url += uris({q: names.join(' ') + ' ' + title.join(' ')}); - } -} diff --git a/src/transforms/helpers1/hover-box.js b/src/transforms/helpers1/hover-box.js deleted file mode 100644 index be19f57..0000000 --- a/src/transforms/helpers1/hover-box.js +++ /dev/null @@ -1,108 +0,0 @@ -function make_hover_css(pos) { - const pretty = window.innerWidth > 600; - const padding = pretty? 18 : 12; - const outer_padding = pretty ? 18 : 0; - const bbox = document.querySelector('body').getBoundingClientRect(); - let left = pos[0] - bbox.left, top = pos[1] - bbox.top; - let width = Math.min(window.innerWidth-2*outer_padding, 648); - left = Math.min(left, window.innerWidth-width-outer_padding); - width = width - 2 * padding; - return (`position: absolute; - background-color: #FFF; - opacity: 0.95; - max-width: ${width}px; - top: ${top}px; - left: ${left}px; - border: 1px solid rgba(0, 0, 0, 0.25); - padding: ${padding}px; - border-radius: ${pretty? 3 : 0}px; - box-shadow: 0px 2px 10px 2px rgba(0, 0, 0, 0.2); - z-index: ${1e6};`); -} - -export class HoverBox { - - constructor(contentHTML, triggerElement) { - this.visible = false; - // div hold teh contents of the box that will become visible - this.div = contentHTML; - this.bindDivEvents(this.div); - // triggerElement holds the element that needs to be hovered etc to show contents - this.triggerElement = triggerElement; - this.bindTriggerEvents(this.triggerElement); - this.hide(); - } - - bindDivEvents(node) { - // For mice, same behavior as hovering on links - this.div.addEventListener('mouseover', () => { - if (!this.visible) this.showAtNode(node); - this.stopTimeout(); - }); - this.div.addEventListener('mouseout', () => { - this.extendTimeout(250); - }); - // Don't trigger body touchstart event when touching within box - this.div.addEventListener('touchstart', (event) => { - event.stopPropagation(); - }, {passive: true}); - // Close box when touching outside box - document.body.addEventListener('touchstart', () => { - this.hide(); - }, {passive: true}); - } - - bindTriggerEvents(node) { - node.addEventListener('mouseover', () => { - if (!this.visible) { - this.showAtNode(node); - } - this.stopTimeout(); - }); - - node.addEventListener('mouseout', () => { - this.extendTimeout(250); - }); - - node.addEventListener('touchstart', (event) => { - if (this.visible) { - this.hide(); - } else { - this.showAtNode(node); - } - // Don't trigger body touchstart event when touching link - event.stopPropagation(); - }, {passive: true}); - } - - show(position) { - this.visible = true; - const css = make_hover_css(position); - this.div.setAttribute('style', css ); - } - - showAtNode(node) { - const bbox = node.getBoundingClientRect(); - this.show([bbox.right, bbox.bottom]); - } - - hide() { - this.visible = false; - this.div.setAttribute('style', 'display:none'); - this.stopTimeout(); - } - - stopTimeout() { - if (this.timeout) { - clearTimeout(this.timeout); - } - } - - extendTimeout(time) { - this.stopTimeout(); - this.timeout = setTimeout(() => { - this.hide(); - }, time); - } - -} diff --git a/src/transforms/helpers1/layout.js b/src/transforms/helpers1/layout.js deleted file mode 100644 index f2dc8be..0000000 --- a/src/transforms/helpers1/layout.js +++ /dev/null @@ -1,33 +0,0 @@ -// const marginSmall = 16; -// const marginLarge = 3 * marginSmall; -// const margin = marginSmall + marginLarge; -// const gutter = marginSmall; -// const outsetAmount = margin / 2; -// const numCols = 4; -// const numGutters = numCols - 1; -// const columnWidth = (768 - 2 * marginLarge - numGutters * gutter) / numCols; -// -// const screenwidth = 768; -// const pageWidth = screenwidth - 2 * marginLarge; -// const bodyWidth = pageWidth - columnWidth - gutter; - -export function body(selector) { - return `${selector} { - grid-column: margin-left / body; - } - `; -} - -export function page(selector) { - return `${selector} { - grid-column: margin-left / page; - } - `; -} - -export function screen(selector) { - return `${selector} { - grid-column: start / end; - } - `; -} diff --git a/src/transforms/toc.js b/src/transforms/toc.js new file mode 100644 index 0000000..b7be965 --- /dev/null +++ b/src/transforms/toc.js @@ -0,0 +1,9 @@ +import { renderTOC } from '../components/d-toc'; + +export default function(dom) { + const article = dom.querySelector('d-article'); + const toc = dom.querySelector('d-toc'); + const headings = article.querySelectorAll('h2, h3'); + renderTOC(toc, headings); + toc.setAttribute('prerendered', 'true'); +}