//- 💫 MAIN JAVASCRIPT
//- Note: Will be compiled using Babel before deployment.
'use strict'
const $ = document.querySelector.bind(document);
const $$ = document.querySelectorAll.bind(document);
class ProgressBar {
/**
* Animated reading progress bar.
* @param {String} selector – CSS selector of progress bar element.
*/
constructor(selector) {
this.el = $(selector);
this.scrollY = 0;
this.sizes = this.updateSizes();
this.el.setAttribute('max', 100);
this.init();
}
init() {
window.addEventListener('scroll', () => {
this.scrollY = (window.pageYOffset || document.scrollTop) - (document.clientTop || 0);
requestAnimationFrame(this.update.bind(this));
}, false);
window.addEventListener('resize', () => {
this.sizes = this.updateSizes();
requestAnimationFrame(this.update.bind(this));
})
}
update() {
const offset = 100 - ((this.sizes.height - this.scrollY - this.sizes.vh) / this.sizes.height * 100);
this.el.setAttribute('value', (this.scrollY == 0) ? 0 : offset || 0);
}
updateSizes() {
const body = document.body;
const html = document.documentElement;
return {
height: Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight),
vh: Math.max(html.clientHeight, window.innerHeight || 0)
}
}
}
class SectionHighlighter {
/**
* Hightlight section in viewport in sidebar, using in-view library.
* @param {String} sectionAttr - Data attribute of sections.
* @param {String} navAttr - Data attribute of navigation items.
* @param {String} activeClass – Class name of active element.
*/
constructor(sectionAttr, navAttr, activeClass = 'is-active') {
this.sections = [...$$(`[${navAttr}]`)];
this.navAttr = navAttr;
this.sectionAttr = sectionAttr;
this.activeClass = activeClass;
inView(`[${sectionAttr}]`).on('enter', this.highlightSection.bind(this));
}
highlightSection(section) {
const id = section.getAttribute(this.sectionAttr);
const el = $(`[${this.navAttr}="${id}"]`);
if (el) {
this.sections.forEach(el => el.classList.remove(this.activeClass));
el.classList.add(this.activeClass);
}
}
}
class Templater {
/**
* Mini templating engine based on data attributes. Selects elements based
* on a data-tpl and data-tpl-key attribute and can set textContent
* and innterHtml.
*
* @param {String} templateId - Template section, e.g. value of data-tpl.
*/
constructor(templateId) {
this.templateId = templateId;
}
get(key) {
return $(`[data-tpl="${this.templateId}"][data-tpl-key="${key}"]`);
}
fill(key, value, html = false) {
const el = this.get(key);
if (html) el.innerHTML = value || '';
else el.textContent = value || '';
return el;
}
}
class ModelLoader {
/**
* Load model meta from GitHub and update model details on site. Uses the
* Templater mini template engine to update DOM.
*
* @param {String} repo - Path tp GitHub repository containing releases.
* @param {Array} models - List of model IDs, e.g. "en_core_web_sm".
* @param {Object} licenses - License IDs mapped to URLs.
* @param {Object} accKeys - Available accuracy keys mapped to display labels.
*/
constructor(repo, models = [], licenses = {}, benchmarkKeys = {}) {
this.url = `https://raw.githubusercontent.com/${repo}/master`;
this.repo = `https://github.com/${repo}`;
this.modelIds = models;
this.licenses = licenses;
this.benchKeys = benchmarkKeys;
this.init();
}
init() {
this.modelIds.forEach(modelId =>
new Templater(modelId).get('table').setAttribute('data-loading', ''));
fetch(`${this.url}/compatibility.json`)
.then(res => this.handleResponse(res))
.then(json => json.ok ? this.getModels(json['spacy']) : this.modelIds.forEach(modelId => this.showError(modelId)))
}
handleResponse(res) {
if (res.ok) return res.json().then(json => Object.assign({}, json, { ok: res.ok }))
else return ({ ok: res.ok })
}
convertNumber(num, separator = ',') {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator);
}
getModels(compat) {
this.compat = compat;
for (let modelId of this.modelIds) {
const version = this.getLatestVersion(modelId, compat);
if (!version) {
this.showError(modelId); return;
}
fetch(`${this.url}/meta/${modelId}-${version}.json`)
.then(res => this.handleResponse(res))
.then(json => json.ok ? this.render(json) : this.showError(modelId))
}
// make sure scroll positions for progress bar etc. are recalculated
window.dispatchEvent(new Event('resize'));
}
showError(modelId) {
const template = new Templater(modelId);
template.get('table').removeAttribute('data-loading');
template.get('error').style.display = 'block';
for (let key of ['sources', 'pipeline', 'vectors', 'author', 'license']) {
template.get(key).parentElement.parentElement.style.display = 'none';
}
}
/**
* Update model details in tables. Currently quite hacky :(
*/
render({ lang, name, version, sources, pipeline, vectors, url, author, license, accuracy, speed, size, description, notes }) {
const modelId = `${lang}_${name}`;
const model = `${modelId}-${version}`;
const template = new Templater(modelId);
const getSources = s => (s instanceof Array) ? s.join(', ') : s;
const getPipeline = p => p.map(comp => `${comp}
`).join(', ');
const getVectors = v => `${this.convertNumber(v.entries)} (${v.width} dimensions)`;
const getLink = (t, l) => `${t}`;
const keys = { version, size, description, notes }
Object.keys(keys).forEach(key => template.fill(key, keys[key]));
if (sources) template.fill('sources', getSources(sources));
if (pipeline && pipeline.length) template.fill('pipeline', getPipeline(pipeline), true);
else template.get('pipeline').parentElement.parentElement.style.display = 'none';
if (vectors) template.fill('vectors', getVectors(vectors));
else template.get('vectors').parentElement.parentElement.style.display = 'none';
if (author) template.fill('author', url ? getLink(author, url) : author, true);
if (license) template.fill('license', this.licenses[license] ? getLink(license, this.licenses[license]) : license, true);
template.get('download').setAttribute('href', `${this.repo}/releases/tag/${model}`);
this.renderBenchmarks(template, accuracy, speed);
this.renderCompat(template, modelId);
template.get('table').removeAttribute('data-loading');
}
renderBenchmarks(template, accuracy = {}, speed = {}) {
if (!accuracy && !speed) return;
template.get('benchmarks').style.display = 'block';
this.renderTable(template, 'parser', accuracy, val => val.toFixed(2));
this.renderTable(template, 'ner', accuracy, val => val.toFixed(2));
this.renderTable(template, 'speed', speed, Math.round);
}
renderTable(template, id, benchmarks, convertVal = val => val) {
if (!this.benchKeys[id]Â || !Object.keys(this.benchKeys[id]).some(key => benchmarks[key])) return;
const keys = Object.keys(this.benchKeys[id]).map(k => benchmarks[k] ? k : false).filter(k => k);
template.get(id).style.display = 'block';
for (let key of keys) {
template
.fill(key, this.convertNumber(convertVal(benchmarks[key])))
.parentElement.style.display = 'table-row';
}
}
renderCompat(template, modelId) {
template.get('compat-wrapper').style.display = 'table-row';
const options = Object.keys(this.compat).map(v => ``).join('');
template
.fill('compat', '' + options, true)
.addEventListener('change', ev => {
const result = this.compat[ev.target.value][modelId];
if (result) template.fill('compat-versions', `${modelId}-${result[0]}
`, true);
else template.fill('compat-versions', '');
});
}
getLatestVersion(model, compat = {}) {
for (let spacy_v of Object.keys(compat)) {
const models = compat[spacy_v];
if (models[model]) return models[model][0];
}
}
}
class Changelog {
/**
* Fetch and render changelog from GitHub. Clones a template node (table row)
* to avoid doubling templating markup in JavaScript.
*
* @param {String} user - GitHub username.
* @param {String} repo - Repository to fetch releases from.
*/
constructor(user, repo) {
this.url = `https://api.github.com/repos/${user}/${repo}/releases`;
this.template = new Templater('changelog');
fetch(this.url)
.then(res => this.handleResponse(res))
.then(json => json.ok ? this.render(json) : false)
}
/**
* Get template section from template row. Slightly hacky, but does make sense.
*/
$(item, id) {
return item.querySelector(`[data-changelog="${id}"]`);
}
handleResponse(res) {
if (res.ok) return res.json().then(json => Object.assign({}, json, { ok: res.ok }))
else return ({ ok: res.ok })
}
render(json) {
this.template.get('error').style.display = 'none';
this.template.get('table').style.display = 'block';
this.row = this.template.get('item');
this.releases = this.template.get('releases');
this.prereleases = this.template.get('prereleases');
Object.values(json)
.filter(release => release.name)
.forEach(release => this.renderRelease(release));
this.row.remove();
// make sure scroll positions for progress bar etc. are recalculated
window.dispatchEvent(new Event('resize'));
}
/**
* Clone the template row and populate with content from API response.
* https://developer.github.com/v3/repos/releases/#list-releases-for-a-repository
*
* @param {String} name - Release title.
* @param {String} tag (tag_name) - Release tag.
* @param {String} url (html_url) - URL to the release page on GitHub.
* @param {String} date (published_at) - Timestamp of release publication.
* @param {Boolean} pre (prerelease) - Whether the release is a prerelease.
*/
renderRelease({ name, tag_name: tag, html_url: url, published_at: date, prerelease: pre }) {
const container = pre ? this.prereleases : this.releases;
const row = this.row.cloneNode(true);
this.$(row, 'date').textContent = date.split('T')[0];
this.$(row, 'tag').innerHTML = `${tag}
`;
this.$(row, 'title').textContent = (name.split(': ').length == 2) ? name.split(': ')[1] : name;
container.appendChild(row);
}
}
class GitHubEmbed {
/**
* Embed code from GitHub repositories, similar to Gist embeds. Fetches the
* raw text and places it inside element.
* Usage:
* * @param {String} user - GitHub user or organization. * @param {String} attr - Data attribute used to select containers. Attribute * value should be path to file relative to user. */ constructor(user, attr) { this.url = `https://raw.githubusercontent.com/${user}`; this.attr = attr; this.error = `\nCan't fetch code example from GitHub :(\n\nPlease use the link below to view the example. If you've come across\na broken link, we always appreciate a pull request to the repository,\nor a report on the issue tracker. Thanks!`; [...$$(`[${this.attr}]`)].forEach(el => this.embed(el)); } embed(el) { el.parentElement.setAttribute('data-loading', ''); fetch(`${this.url}/${el.getAttribute(this.attr)}`) .then(res => res.text().then(text => ({ text, ok: res.ok }))) .then(({ text, ok }) => { el.textContent = ok ? text : this.error; if (ok && window.Prism) Prism.highlightElement(el); }) el.parentElement.removeAttribute('data-loading'); } }