diff --git a/website/_harp.json b/website/_harp.json
index 7c69beef0..bc1a0b5e5 100644
--- a/website/_harp.json
+++ b/website/_harp.json
@@ -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": {
diff --git a/website/_includes/_mixins.jade b/website/_includes/_mixins.jade
index 94d84b4fe..615160023 100644
--- a/website/_includes/_mixins.jade
+++ b/website/_includes/_mixins.jade
@@ -281,7 +281,12 @@ mixin github(repo, file, height, alt_file, language)
figure.o-block
pre.c-code-block.o-block-small(class="lang-#{(language || DEFAULT_SYNTAX)}" style="height: #{height}px; min-height: #{height}px")
- code.c-code-block__content(data-gh-embed="#{repo}/#{branch}/#{file}")
+ code.c-code-block__content(data-gh-embed="#{repo}/#{branch}/#{file}").
+ Can't fetch code example from GitHub :(
+
+ Please use the link below to view the example. If you've come across
+ a broken link, we always appreciate a pull request to the repository,
+ or a report on the issue tracker. Thanks!
footer.o-grid.u-text
.o-block-small.u-flex-full.u-padding-small #[+icon("github")] #[code.u-break.u-break--all=repo + '/' + (alt_file || file)]
diff --git a/website/_includes/_page_models.jade b/website/_includes/_page_models.jade
index 10e7e1746..1cab930fb 100644
--- a/website/_includes/_page_models.jade
+++ b/website/_includes/_page_models.jade
@@ -20,7 +20,7 @@ for id in CURRENT_MODELS
p(data-tpl=id data-tpl-key="description")
- div(data-tpl=id data-tpl-key="error" style="display: none")
+ div(data-tpl=id data-tpl-key="error")
+infobox
| Unable to load model details from GitHub. To find out more
| about this model, see the overview of the
@@ -54,7 +54,7 @@ for id in CURRENT_MODELS
+cell
.o-field.u-float-left
select.o-field__select.u-text-small(data-tpl=id data-tpl-key="compat")
- .o-empty(data-tpl=id data-tpl-key="compat-versions")
+ div(data-tpl=id data-tpl-key="compat-versions")
section(data-tpl=id data-tpl-key="benchmarks" style="display: none")
+grid.o-block-small
diff --git a/website/_includes/_scripts.jade b/website/_includes/_scripts.jade
index 5ecdd0711..05a468076 100644
--- a/website/_includes/_scripts.jade
+++ b/website/_includes/_scripts.jade
@@ -1,43 +1,86 @@
//- 💫 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) + ");"
+- ModelComparer = "new ModelComparer('" + MODELS_REPO + "'," + JSON.stringify(MODEL_LICENSES) + "," + JSON.stringify(MODEL_BENCHMARKS) + "," + JSON.stringify(LANGUAGES) + "," + JSON.stringify(MODEL_META) + "," + JSON.stringify(default_models || false) + ");"
+
+//- 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
+ if compare_models
+ | import { ModelComparer } from '/assets/js/models.js';
+ !=ModelComparer
+
+//- 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
+ if compare_models
+ !=ModelComparer
diff --git a/website/_includes/_sidebar.jade b/website/_includes/_sidebar.jade
index 1bca2cb80..9b9cd00a3 100644
--- a/website/_includes/_sidebar.jade
+++ b/website/_includes/_sidebar.jade
@@ -19,5 +19,5 @@ menu.c-sidebar.js-sidebar.u-text
- var counter = 0
for id, title in menu
- counter++
- li.c-sidebar__crumb__item(data-nav=id class=(counter == 1) ? "is-active" : null)
+ li.c-sidebar__crumb__item(data-nav=id)
+a("#section-" + id)=title
diff --git a/website/assets/css/_base/_objects.sass b/website/assets/css/_base/_objects.sass
index 23dc14744..4e63a4346 100644
--- a/website/assets/css/_base/_objects.sass
+++ b/website/assets/css/_base/_objects.sass
@@ -163,11 +163,4 @@
height: 1.4em
border: none
text-align-last: center
-
-.o-empty:empty:before
- @include size(1em)
- border-radius: 50%
- content: ""
- display: inline-block
- background: $color-red
- vertical-align: middle
+ width: 100%
diff --git a/website/assets/js/changelog.js b/website/assets/js/changelog.js
new file mode 100644
index 000000000..94f2149ad
--- /dev/null
+++ b/website/assets/js/changelog.js
@@ -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 = `${tag}
`;
+ 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);
+ }
+}
diff --git a/website/assets/js/github-embed.js b/website/assets/js/github-embed.js
new file mode 100644
index 000000000..ec72fd713
--- /dev/null
+++ b/website/assets/js/github-embed.js
@@ -0,0 +1,42 @@
+'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:
+ * @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.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 }) => ok ? this.render(el, text) : false) + el.parentElement.removeAttribute('data-loading'); + } + + /** + * Add text to container and apply syntax highlighting via Prism, if available. + * @param {node} el - The element. + * @param {string} text - The raw code, fetched from GitHub. + */ + render(el, text) { + el.textContent = text; + if (window.Prism) Prism.highlightElement(el); + } +} diff --git a/website/assets/js/main.js b/website/assets/js/main.js deleted file mode 100644 index d9465bb67..000000000 --- a/website/assets/js/main.js +++ /dev/null @@ -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 => `${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'); - } -} diff --git a/website/assets/js/models.js b/website/assets/js/models.js new file mode 100644 index 000000000..e79073edd --- /dev/null +++ b/website/assets/js/models.js @@ -0,0 +1,316 @@ +'use strict'; + +import { Templater, handleResponse, convertNumber } from './util.js'; + +/** + * Chart.js defaults + */ +const CHART_COLORS = { model1: '#09a3d5', model2: '#066B8C' }; +const CHART_FONTS = { + legend: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + ticks: 'Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' +}; + +/** + * 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 ? `${author}` : author, + license: (license, url) => url ? `${license}` : license, + sources: sources => (sources instanceof Array) ? sources.join(', ') : sources, + pipeline: pipes => (pipes && pipes.length) ? pipes.map(p => `${p}
`).join(', ') : '-', + vectors: vec => vec ? `${convertNumber(vec.entries)} (${vec.width} dimensions)` : 'n/a', + version: version => `v${version}
` +}; + +/** + * 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); + tpl.get('error').style.display = 'none'; + 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 = ''; + const options = Object.keys(this.compat) + .map(v => ``) + .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 ? `${model}-${res[0]}
` : 'not compatible'; + } +} + +export class ModelComparer { + /** + * Compare to model meta files and render chart and comparison table. + * @param {string} repo - Path tp GitHub repository containing releases. + * @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. + * @param {Object} languages - Available languages, ID mapped to name. + * @param {Object} defaultModels - Models to compare on load, 'model1' and + * 'model2' mapped to model names. + */ + constructor(repo, licenses = {}, benchmarkKeys = {}, languages = {}, labels = {}, defaultModels) { + this.url = `https://raw.githubusercontent.com/${repo}/master`; + this.repo = `https://github.com/${repo}`; + this.tpl = new Templater('compare'); + this.benchKeys = benchmarkKeys; + this.licenses = licenses; + this.languages = languages; + this.labels = labels; + this.models = {}; + this.colors = CHART_COLORS; + this.fonts = CHART_FONTS; + this.defaultModels = defaultModels; + this.tpl.get('result').style.display = 'block'; + this.fetchCompat() + .then(compat => this.init(compat)) + .catch(this.showError.bind(this)) + } + + init(compat) { + this.compat = compat; + const selectA = this.tpl.get('model1'); + const selectB = this.tpl.get('model2'); + selectA.addEventListener('change', this.onSelect.bind(this)); + selectB.addEventListener('change', this.onSelect.bind(this)); + this.chart = new Chart('chart_compare_accuracy', { type: 'bar', options: { + responsive: true, + legend: { position: 'bottom', labels: { fontFamily: this.fonts.legend, fontSize: 13 }}, + scales: { + yAxes: [{ label: 'Accuracy', ticks: { min: 70, fontFamily: this.fonts.ticks }}], + xAxes: [{ barPercentage: 0.75, ticks: { fontFamily: this.fonts.ticks }}] + } + }}); + if (this.defaultModels) { + selectA.value = this.defaultModels.model1; + selectB.value = this.defaultModels.model2; + this.getModels(this.defaultModels); + } + } + + fetchCompat() { + return new Promise((resolve, reject) => + fetch(`${this.url}/compatibility.json`) + .then(res => handleResponse(res)) + .then(json => json.ok ? resolve(json.spacy) : reject())) + } + + fetchModel(name) { + const version = getLatestVersion(name, this.compat); + const modelName = `${name}-${version}`; + return new Promise((resolve, reject) => { + // resolve immediately if model already loaded, e.g. in this.models + if (this.models[name]) resolve(this.models[name]); + else fetch(`${this.url}/meta/${modelName}.json`) + .then(res => handleResponse(res)) + .then(json => json.ok ? resolve(this.saveModel(name, json)) : reject()) + }) + } + + /** + * "Save" meta to this.models so it only has to be fetched from GitHub once. + * @param {string} name - The model name. + * @param {Object} data - The model meta data. + */ + saveModel(name, data) { + this.models[name] = data; + return data; + } + + showError() { + this.tpl.get('result').style.display = 'none'; + this.tpl.get('error').style.display = 'block'; + } + + onSelect(ev) { + const modelId = ev.target.value; + const otherId = (ev.target.id == 'model1') ? 'model2' : 'model1'; + const otherVal = this.tpl.get(otherId); + const otherModel = otherVal.options[otherVal.selectedIndex].value; + if (otherModel != '') this.getModels({ + [ev.target.id]: modelId, + [otherId]: otherModel + }) + } + + getModels({ model1, model2 }) { + this.tpl.get('result').setAttribute('data-loading', ''); + this.fetchModel(model1) + .then(data1 => this.fetchModel(model2) + .then(data2 => this.render({ model1: data1, model2: data2 }))) + .catch(this.showError.bind(this)) + } + + /** + * Render two models, and populate the chart and table. Currently quite hacky :( + * @param {Object} models - The models to render. + * @param {Object} models.model1 - The first model (via first