Refactor JS, split into modules and add nomodule option

rollup.js will be compiled by the rollup package and Babel on build, and will be loaded if a browser doesn't yet support JS modules
This commit is contained in:
ines 2017-10-30 14:06:25 +01:00
parent 5453821a9f
commit 8db3da3c3d
14 changed files with 493 additions and 347 deletions

View File

@ -84,8 +84,8 @@
],
"ALPHA": true,
"V_CSS": "2.0a1",
"V_JS": "2.0a0",
"V_CSS": "2.0a2",
"V_JS": "2.0a1",
"DEFAULT_SYNTAX": "python",
"ANALYTICS": "UA-58931649-1",
"MAILCHIMP": {

View File

@ -1,43 +1,80 @@
//- 💫 INCLUDES > SCRIPTS
if quickstart
script(src="/assets/js/quickstart.min.js")
script(src="/assets/js/vendor/quickstart.min.js")
if IS_PAGE
script(src="/assets/js/in-view.min.js")
script(src="/assets/js/vendor/in-view.min.js")
if environment == "deploy"
script(async src="https://www.google-analytics.com/analytics.js")
script(src="/assets/js/prism.min.js")
script(src="/assets/js/main.js?v#{V_JS}")
script(src="/assets/js/vendor/prism.min.js")
if SECTION == "models"
script(src="/assets/js/vendor/chart.min.js")
script(src="/assets/js/models.js?v#{V_JS}" type="module")
script
| new ProgressBar('.js-progress');
if changelog
| new Changelog('!{SOCIAL.github}', 'spacy');
if quickstart
| new Quickstart("#qs");
if IS_PAGE
| new SectionHighlighter('data-section', 'data-nav');
| new GitHubEmbed('!{SOCIAL.github}', 'data-gh-embed');
| ((window.gitter = {}).chat = {}).options = {
| useStyles: false,
| activationElement: '.js-gitter-button',
| targetElement: '.js-gitter',
| room: '!{SOCIAL.gitter}'
| };
if HAS_MODELS
| new ModelLoader('!{MODELS_REPO}', !{JSON.stringify(CURRENT_MODELS)}, !{JSON.stringify(MODEL_LICENSES)}, !{JSON.stringify(MODEL_BENCHMARKS)});
if environment == "deploy"
| window.ga=window.ga||function(){
| (ga.q=ga.q||[]).push(arguments)}; ga.l=+new Date;
| ga('create', '#{ANALYTICS}', 'auto'); ga('send', 'pageview');
if IS_PAGE
script
| ((window.gitter = {}).chat = {}).options = {
| useStyles: false,
| activationElement: '.js-gitter-button',
| targetElement: '.js-gitter',
| room: '!{SOCIAL.gitter}'
| };
script(src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer)
//- JS modules slightly hacky, but necessary to dynamically instantiate the
classes with data from the Harp JSON files, while still being able to
support older browsers that can't handle JS modules. More details:
https://medium.com/dev-channel/es6-modules-in-chrome-canary-m60-ba588dfb8ab7
- ProgressBar = "new ProgressBar('.js-progress');"
- Changelog = "new Changelog('" + SOCIAL.github + "', 'spacy');"
- NavHighlighter = "new NavHighlighter('data-section', 'data-nav');"
- GitHubEmbed = "new GitHubEmbed('" + SOCIAL.github + "', 'data-gh-embed');"
- ModelLoader = "new ModelLoader('" + MODELS_REPO + "'," + JSON.stringify(CURRENT_MODELS) + "," + JSON.stringify(MODEL_LICENSES) + "," + JSON.stringify(MODEL_BENCHMARKS) + ");"
//- Browsers with JS module support.
Will be ignored otherwise.
script(type="module")
| import ProgressBar from '/assets/js/progress.js';
!=ProgressBar
if changelog
| import Changelog from '/assets/js/changelog.js';
!=Changelog
if IS_PAGE
| import NavHighlighter from '/assets/js/nav-highlighter.js';
!=NavHighlighter
| import GitHubEmbed from '/assets/js/github-embed.js';
!=GitHubEmbed
if HAS_MODELS
| import { ModelLoader } from '/assets/js/models.js';
!=ModelLoader
//- Browsers with no JS module support.
Won't be fetched or interpreted otherwise.
script(nomodule src="/assets/js/rollup.js")
script(nomodule)
!=ProgressBar
if changelog
!=Changelog
if IS_PAGE
!=NavHighlighter
!=GitHubEmbed
if HAS_MODELS
!=ModeLoader

View File

@ -0,0 +1,72 @@
'use strict';
import { Templater, handleResponse } from './util.js';
export default 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');
this.fetchChangelog()
.then(json => this.render(json))
.catch(this.showError.bind(this));
// make sure scroll positions for progress bar etc. are recalculated
window.dispatchEvent(new Event('resize'));
}
fetchChangelog() {
return new Promise((resolve, reject) =>
fetch(this.url)
.then(res => handleResponse(res))
.then(json => json.ok ? resolve(json) : reject()))
}
showError() {
this.template.get('error').style.display = 'block';
}
/**
* Get template section from template row. Hacky, but does make sense.
* @param {node} item - Parent element.
* @param {string} id - ID of child element, set via data-changelog.
*/
getField(item, id) {
return item.querySelector(`[data-changelog="${id}"]`);
}
render(json) {
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();
}
/**
* 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} prerelease - Whether the release is a prerelease.
*/
renderRelease({ name, tag_name: tag, html_url: url, published_at: date, prerelease }) {
const container = prerelease ? this.prereleases : this.releases;
const tagLink = `<a href="${url}" target="_blank"><code>${tag}</code></a>`;
const title = (name.split(': ').length == 2) ? name.split(': ')[1] : name;
const row = this.row.cloneNode(true);
this.getField(row, 'date').textContent = date.split('T')[0];
this.getField(row, 'tag').innerHTML = tagLink;
this.getField(row, 'title').textContent = title;
container.appendChild(row);
}
}

View File

@ -0,0 +1,36 @@
'use strict';
import { $$ } from './util.js';
export default class GitHubEmbed {
/**
* Embed code from GitHub repositories, similar to Gist embeds. Fetches the
* raw text and places it inside element.
* Usage: <pre><code data-gh-embed="spacy/master/examples/x.py"></code><pre>
* @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));
}
/**
* Fetch code from GitHub and insert it as element content. File path is
* read off the container's data attribute.
* @param {node} el - The element.
*/
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');
}
}

View File

@ -1,323 +0,0 @@
//- 💫 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 => `<code>${comp}</code>`).join(', ');
const getVectors = v => `${this.convertNumber(v.entries)} (${v.width} dimensions)`;
const getLink = (t, l) => `<a href="${l}" target="_blank">${t}</a>`;
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 => `<option value="${v}">v${v}</option>`).join('');
template
.fill('compat', '<option selected disabled>spaCy version</option>' + options, true)
.addEventListener('change', ev => {
const result = this.compat[ev.target.value][modelId];
if (result) template.fill('compat-versions', `<code>${modelId}-${result[0]}</code>`, 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 = `<a href="${url}" target="_blank"><code>${tag}</code></a>`;
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: <pre><code data-gh-embed="spacy/master/examples/x.py"></code><pre>
*
* @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');
}
}

160
website/assets/js/models.js Normal file
View File

@ -0,0 +1,160 @@
'use strict';
import { Templater, handleResponse, convertNumber } from './util.js';
/**
* Chart.js defaults
*/
Chart.defaults.global.legend.position = 'bottom';
Chart.defaults.global.defaultFontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'";
const CHART_COLORS = { model1: '#09a3d5', model2: '#066B8C' };
/**
* Formatters for model details.
* @property {function} author Format model author with optional link.
* @property {function} license - Format model license with optional link.
* @property {function} sources - Format training data sources (list or string).
* @property {function} pipeline - Format list of pipeline components.
* @property {function} vectors - Format vector data (entries and dimensions).
* @property {function} version - Format model version number.
*/
export const formats = {
author: (author, url) => url ? `<a href="${url}" target="_blank">${author}</a>` : author,
license: (license, url) => url ? `<a href="${url}" target="_blank">${license}</a>` : license,
sources: sources => (sources instanceof Array) ? sources.join(', ') : sources,
pipeline: pipes => (pipes && pipes.length) ? pipes.map(p => `<code>${p}</code>`).join(', ') : '-',
vectors: vec => vec ? `${convertNumber(vec.entries)} (${vec.width} dimensions)` : 'n/a',
version: version => `<code>v${version}</code>`
};
/**
* Find the latest version of a model in a compatibility table.
* @param {string} model - The model name.
* @param {Object} compat - Compatibility table, keyed by spaCy version.
*/
export const getLatestVersion = (model, compat = {}) => {
for (let [spacy_v, models] of Object.entries(compat)) {
if (models[model]) return models[model][0];
}
};
export 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} benchmarkKeys - Objects of available keys by type, e.g.
* 'parser', 'ner', 'speed', mapped to 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', ''));
this.fetch(`${this.url}/compatibility.json`)
.then(json => this.getModels(json.spacy))
.catch(_ => this.modelIds.forEach(modelId => this.showError(modelId)));
// make sure scroll positions for progress bar etc. are recalculated
window.dispatchEvent(new Event('resize'));
}
fetch(url) {
return new Promise((resolve, reject) =>
fetch(url).then(res => handleResponse(res))
.then(json => json.ok ? resolve(json) : reject()))
}
getModels(compat) {
this.compat = compat;
for (let modelId of this.modelIds) {
const version = getLatestVersion(modelId, compat);
if (version) this.fetch(`${this.url}/meta/${modelId}-${version}.json`)
.then(json => this.render(json))
.catch(_ => this.showError(modelId))
else this.showError(modelId);
}
}
showError(modelId) {
const tpl = new Templater(modelId);
tpl.get('table').removeAttribute('data-loading');
tpl.get('error').style.display = 'block';
for (let key of ['sources', 'pipeline', 'vectors', 'author', 'license']) {
tpl.get(key).parentElement.parentElement.style.display = 'none';
}
}
/**
* Update model details in tables. Currently quite hacky :(
*/
render(data) {
const modelId = `${data.lang}_${data.name}`;
const model = `${modelId}-${data.version}`;
const tpl = new Templater(modelId);
this.renderDetails(tpl, data)
this.renderBenchmarks(tpl, data.accuracy, data.speed);
this.renderCompat(tpl, modelId);
tpl.get('download').setAttribute('href', `${this.repo}/releases/tag/${model}`);
tpl.get('table').removeAttribute('data-loading');
}
renderDetails(tpl, { version, size, description, notes, author, url,
license, sources, vectors, pipeline }) {
const basics = { version, size, description, notes }
for (let [key, value] of Object.entries(basics)) {
if (value) tpl.fill(key, value);
}
if (author) tpl.fill('author', formats.author(author, url), true);
if (license) tpl.fill('license', formats.license(license, this.licenses[license]), true);
if (sources) tpl.fill('sources', formats.sources(sources));
if (vectors) tpl.fill('vectors', formats.vectors(vectors));
else tpl.get('vectors').parentElement.parentElement.style.display = 'none';
if (pipeline && pipeline.length) tpl.fill('pipeline', formats.pipeline(pipeline), true);
else tpl.get('pipeline').parentElement.parentElement.style.display = 'none';
}
renderBenchmarks(tpl, accuracy = {}, speed = {}) {
if (!accuracy && !speed) return;
this.renderTable(tpl, 'parser', accuracy, val => val.toFixed(2));
this.renderTable(tpl, 'ner', accuracy, val => val.toFixed(2));
this.renderTable(tpl, 'speed', speed, Math.round);
tpl.get('benchmarks').style.display = 'block';
}
renderTable(tpl, id, benchmarks, converter = val => val) {
if (!this.benchKeys[id] || !Object.keys(this.benchKeys[id]).some(key => benchmarks[key])) return;
for (let key of Object.keys(this.benchKeys[id])) {
if (benchmarks[key]) tpl
.fill(key, convertNumber(converter(benchmarks[key])))
.parentElement.style.display = 'table-row';
}
tpl.get(id).style.display = 'block';
}
renderCompat(tpl, modelId) {
tpl.get('compat-wrapper').style.display = 'table-row';
const header = '<option selected disabled>spaCy version</option>';
const options = Object.keys(this.compat)
.map(v => `<option value="${v}">v${v}</option>`)
.join('');
tpl
.fill('compat', header + options, true)
.addEventListener('change', ({ target: { value }}) =>
tpl.fill('compat-versions', this.getCompat(value, modelId), true))
}
getCompat(version, model) {
const res = this.compat[version][model];
return res ? `<code>${model}-${res[0]}</code>` : '<em>not compatible</em>';
}
}

View File

@ -0,0 +1,33 @@
'use strict';
import { $, $$ } from './util.js';
export default class NavHighlighter {
/**
* 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;
if (window.inView) inView(`[${sectionAttr}]`)
.on('enter', this.highlightSection.bind(this));
}
/**
* Check if section in view exists in sidebar and mark as active.
* @param {node} section - The section in view.
*/
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);
}
}
}

View File

@ -0,0 +1,52 @@
'use strict';
import { $ } from './util.js';
export default class ProgressBar {
/**
* Animated reading progress bar.
* @param {string} selector CSS selector of progress bar element.
*/
constructor(selector) {
this.scrollY = 0;
this.sizes = this.updateSizes();
this.el = $(selector);
this.el.setAttribute('max', 100);
window.addEventListener('scroll', this.onScroll.bind(this));
window.addEventListener('resize', this.onResize.bind(this));
}
onScroll(ev) {
this.scrollY = (window.pageYOffset || document.scrollTop) - (document.clientTop || 0);
requestAnimationFrame(this.update.bind(this));
}
onResize(ev) {
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);
}
/**
* Update scroll and viewport height. Called on load and window resize.
*/
updateSizes() {
return {
height: Math.max(
document.body.scrollHeight,
document.body.offsetHeight,
document.documentElement.clientHeight,
document.documentElement.scrollHeight,
document.documentElement.offsetHeight
),
vh: Math.max(
document.documentElement.clientHeight,
window.innerHeight || 0
)
}
}
}

View File

@ -0,0 +1,23 @@
/**
* This file is bundled by Rollup, compiled with Babel and included as
* <script nomodule> for older browsers that don't yet support JavaScript
* modules. Browsers that do will ignore this bundle and won't even fetch it
* from the server. Details:
* https://github.com/rollup/rollup
* https://medium.com/dev-channel/es6-modules-in-chrome-canary-m60-ba588dfb8ab7
*/
// Import all modules that are instantiated directly in _includes/_scripts.jade
import ProgressBar from './progress.js';
import NavHighlighter from './nav-highlighter.js';
import Changelog from './changelog.js';
import GitHubEmbed from './github-embed.js';
import { ModelLoader, ModelComparer } from './models.js';
// Assign to window so they are bundled by rollup
window.ProgressBar = ProgressBar;
window.NavHighlighter = NavHighlighter;
window.Changelog = Changelog;
window.GitHubEmbed = GitHubEmbed;
window.ModelLoader = ModelLoader;
window.ModelComparer = ModelComparer;

56
website/assets/js/util.js Normal file
View File

@ -0,0 +1,56 @@
'use strict';
export const $ = document.querySelector.bind(document);
export const $$ = document.querySelectorAll.bind(document);
export 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 an element from the template and return it.
* @param {string} key - Name of the key within the current template.
*/
get(key) {
return $(`[data-tpl="${this.templateId}"][data-tpl-key="${key}"]`);
}
/**
* Fill the content of a template element with a value.
* @param {string} key - Name of the key within the current template.
* @param {string} value - Content to insert into template element.
* @param {boolean} html - Insert content as HTML. Defaults to false.
*/
fill(key, value, html = false) {
const el = this.get(key);
if (html) el.innerHTML = value || '';
else el.textContent = value || '';
return el;
}
}
/**
* Handle API response and assign status to returned JSON.
* @param {Response} res The response.
*/
export const handleResponse = res => {
if (res.ok) return res.json()
.then(json => Object.assign({}, json, { ok: res.ok }))
else return ({ ok: res.ok })
};
/**
* Convert a number to a string and add thousand separator.
* @param {number|string} num - The number to convert.
* @param {string} separator Thousand separator.
*/
export const convertNumber = (num, separator = ',') =>
num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator);