2016-10-03 21:19:13 +03:00
//- 💫 MAIN JAVASCRIPT
2017-10-03 15:18:49 +03:00
//- Note: Will be compiled using Babel before deployment.
2016-03-31 17:24:48 +03:00
2016-10-31 21:04:15 +03:00
'use strict'
2016-03-31 17:24:48 +03:00
2017-10-03 15:18:49 +03:00
const $ = document . querySelector . bind ( document ) ;
const $$ = document . querySelectorAll . bind ( document ) ;
2016-03-31 17:24:48 +03:00
2017-10-03 15:18:49 +03:00
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 = { } , accKeys = { } ) {
this . url = ` https://raw.githubusercontent.com/ ${ repo } /master ` ;
this . repo = ` https://github.com/ ${ repo } ` ;
this . modelIds = models ;
this . licenses = licenses ;
this . accKeys = accKeys ;
this . chartColor = '#09a3d5' ;
this . chartOptions = {
type : 'bar' ,
options : { responsive : true , scales : {
yAxes : [ { label : 'Accuracy' , ticks : { suggestedMin : 70 } } ] ,
xAxes : [ { barPercentage : 0.425 } ]
} }
}
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'" ;
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 } )
}
2017-10-25 17:15:37 +03:00
convertNumber ( num , separator = ',' ) {
return num . toString ( ) . replace ( /\B(?=(\d{3})+(?!\d))/g , separator ) ;
}
2017-10-03 15:18:49 +03:00
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' ) ) ;
}
2016-03-31 17:24:48 +03:00
2017-10-03 15:18:49 +03:00
showError ( modelId ) {
const template = new Templater ( modelId ) ;
template . get ( 'table' ) . removeAttribute ( 'data-loading' ) ;
template . get ( 'error' ) . style . display = 'block' ;
2017-10-25 17:15:37 +03:00
for ( let key of [ 'sources' , 'pipeline' , 'vectors' , 'author' , 'license' ] ) {
2017-10-03 15:18:49 +03:00
template . get ( key ) . parentElement . parentElement . style . display = 'none' ;
}
2016-03-31 17:24:48 +03:00
}
2017-10-03 15:18:49 +03:00
/ * *
* Update model details in tables . Currently quite hacky : (
* /
2017-10-25 17:15:37 +03:00
render ( { lang , name , version , sources , pipeline , vectors , url , author , license , accuracy , size , description , notes } ) {
2017-10-03 15:18:49 +03:00
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 ( ', ' ) ;
2017-10-25 17:15:37 +03:00
const getVectors = v => ` ${ this . convertNumber ( v . entries ) } ( ${ v . width } dimensions) ` ;
2017-10-03 15:18:49 +03:00
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' ;
2017-10-25 17:15:37 +03:00
if ( vectors ) template . fill ( 'vectors' , getVectors ( vectors ) ) ;
else template . get ( 'vectors' ) . parentElement . parentElement . style . display = 'none' ;
2017-10-03 15:18:49 +03:00
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 } ` ) ;
if ( accuracy ) this . renderAccuracy ( template , accuracy , modelId ) ;
this . renderCompat ( template , modelId ) ;
template . get ( 'table' ) . removeAttribute ( 'data-loading' ) ;
}
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' , '' ) ;
} ) ;
}
renderAccuracy ( template , accuracy , modelId , compare = false ) {
template . get ( 'accuracy-wrapper' ) . style . display = 'block' ;
const metaKeys = Object . keys ( this . accKeys ) . map ( k => accuracy [ k ] ? k : false ) . filter ( k => k ) ;
for ( let key of metaKeys ) {
template . fill ( key , accuracy [ key ] . toFixed ( 2 ) ) . parentElement . style . display = 'table-row' ;
}
this . chartOptions . options . legend = { display : compare }
new Chart ( ` chart_ ${ modelId } ` , Object . assign ( { } , this . chartOptions , { data : {
datasets : [ {
label : modelId ,
data : metaKeys . map ( key => accuracy [ key ] . toFixed ( 2 ) ) ,
backgroundColor : this . chartColor
} ] ,
labels : metaKeys . map ( key => this . accKeys [ key ] )
} } ) )
}
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" > < / c o d e > < p r e >
*
* @ 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 = ` \n Can't fetch code example from GitHub :( \n \n Please use the link below to view the example. If you've come across \n a broken link, we always appreciate a pull request to the repository, \n or 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' ) ;
}
2016-10-03 21:19:13 +03:00
}