diff --git a/_assets/css/bundle.css b/_assets/css/bundle.css index f04478dc..364b0551 100644 --- a/_assets/css/bundle.css +++ b/_assets/css/bundle.css @@ -2,3 +2,4 @@ //= require highlight //= require font-awesome.min //= require style +//= require highlight-custom diff --git a/_assets/css/highlight-custom.css b/_assets/css/highlight-custom.css new file mode 100644 index 00000000..bd8f8e85 --- /dev/null +++ b/_assets/css/highlight-custom.css @@ -0,0 +1,3 @@ +code.hljs { + background: none; +} diff --git a/_assets/css/highlight.css b/_assets/css/highlight.css index a37f938e..fb695c14 100644 --- a/_assets/css/highlight.css +++ b/_assets/css/highlight.css @@ -1,152 +1 @@ -/* - -Original style from softwaremaniacs.org (c) Ivan Sagalaev - -*/ - -.hljs { - display: block; - overflow-x: auto; - padding: 0.5em; - background: #f0f0f0; - -webkit-text-size-adjust: none; -} - -.hljs, -.hljs-subst, -.hljs-tag .hljs-title, -.nginx .hljs-title { - color: black; -} - -.hljs-string, -.hljs-title, -.hljs-constant, -.hljs-parent, -.hljs-tag .hljs-value, -.hljs-rules .hljs-value, -.hljs-preprocessor, -.hljs-pragma, -.haml .hljs-symbol, -.ruby .hljs-symbol, -.ruby .hljs-symbol .hljs-string, -.hljs-template_tag, -.django .hljs-variable, -.smalltalk .hljs-class, -.hljs-addition, -.hljs-flow, -.hljs-stream, -.bash .hljs-variable, -.apache .hljs-tag, -.apache .hljs-cbracket, -.tex .hljs-command, -.tex .hljs-special, -.erlang_repl .hljs-function_or_atom, -.asciidoc .hljs-header, -.markdown .hljs-header, -.coffeescript .hljs-attribute { - color: #800; -} - -.smartquote, -.hljs-comment, -.hljs-annotation, -.diff .hljs-header, -.hljs-chunk, -.asciidoc .hljs-blockquote, -.markdown .hljs-blockquote { - color: #888; -} - -.hljs-number, -.hljs-date, -.hljs-regexp, -.hljs-literal, -.hljs-hexcolor, -.smalltalk .hljs-symbol, -.smalltalk .hljs-char, -.go .hljs-constant, -.hljs-change, -.lasso .hljs-variable, -.makefile .hljs-variable, -.asciidoc .hljs-bullet, -.markdown .hljs-bullet, -.asciidoc .hljs-link_url, -.markdown .hljs-link_url { - color: #080; -} - -.hljs-label, -.hljs-javadoc, -.ruby .hljs-string, -.hljs-decorator, -.hljs-filter .hljs-argument, -.hljs-localvars, -.hljs-array, -.hljs-attr_selector, -.hljs-important, -.hljs-pseudo, -.hljs-pi, -.haml .hljs-bullet, -.hljs-doctype, -.hljs-deletion, -.hljs-envvar, -.hljs-shebang, -.apache .hljs-sqbracket, -.nginx .hljs-built_in, -.tex .hljs-formula, -.erlang_repl .hljs-reserved, -.hljs-prompt, -.asciidoc .hljs-link_label, -.markdown .hljs-link_label, -.vhdl .hljs-attribute, -.clojure .hljs-attribute, -.asciidoc .hljs-attribute, -.lasso .hljs-attribute, -.coffeescript .hljs-property, -.hljs-phony { - color: #88f; -} - -.hljs-keyword, -.hljs-id, -.hljs-title, -.hljs-built_in, -.css .hljs-tag, -.hljs-javadoctag, -.hljs-phpdoc, -.hljs-dartdoc, -.hljs-yardoctag, -.smalltalk .hljs-class, -.hljs-winutils, -.bash .hljs-variable, -.apache .hljs-tag, -.hljs-type, -.hljs-typename, -.tex .hljs-command, -.asciidoc .hljs-strong, -.markdown .hljs-strong, -.hljs-request, -.hljs-status { - font-weight: bold; -} - -.asciidoc .hljs-emphasis, -.markdown .hljs-emphasis { - font-style: italic; -} - -.nginx .hljs-built_in { - font-weight: normal; -} - -.coffeescript .javascript, -.javascript .xml, -.lasso .markup, -.tex .hljs-formula, -.xml .javascript, -.xml .vbscript, -.xml .css, -.xml .hljs-cdata { - opacity: 0.5; -} +pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#fff;color:#000}.hljs-comment,.hljs-quote,.hljs-variable{color:green}.hljs-built_in,.hljs-keyword,.hljs-name,.hljs-selector-tag,.hljs-tag{color:#00f}.hljs-addition,.hljs-attribute,.hljs-literal,.hljs-section,.hljs-string,.hljs-template-tag,.hljs-template-variable,.hljs-title,.hljs-type{color:#a31515}.hljs-deletion,.hljs-meta,.hljs-selector-attr,.hljs-selector-pseudo{color:#2b91af}.hljs-doctag{color:grey}.hljs-attr{color:red}.hljs-bullet,.hljs-link,.hljs-symbol{color:#00b0e8}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} \ No newline at end of file diff --git a/_assets/js/bundle.js b/_assets/js/bundle.js index 288ca5a8..8a46603c 100644 --- a/_assets/js/bundle.js +++ b/_assets/js/bundle.js @@ -1,7 +1,7 @@ //= require clipboard.min //= require jquery.min //= require bootstrap.min -//= require highlight.pack +//= require highlight.min //= require lunr //= require highcharts //= require custom diff --git a/_assets/js/highlight.min.js b/_assets/js/highlight.min.js new file mode 100644 index 00000000..42fd19d1 --- /dev/null +++ b/_assets/js/highlight.min.js @@ -0,0 +1,672 @@ +/*! + Highlight.js v11.7.0 (git: 82688fad18) + (c) 2006-2022 undefined and other contributors + License: BSD-3-Clause + */ +var hljs=function(){"use strict";var e={exports:{}};function t(e){ +return e instanceof Map?e.clear=e.delete=e.set=()=>{ +throw Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=()=>{ +throw Error("set is read-only") +}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach((n=>{var i=e[n] +;"object"!=typeof i||Object.isFrozen(i)||t(i)})),e} +e.exports=t,e.exports.default=t;class n{constructor(e){ +void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} +ignoreMatch(){this.isMatchIgnored=!0}}function i(e){ +return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") +}function r(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t] +;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n} +const s=e=>!!e.scope||e.sublanguage&&e.language;class o{constructor(e,t){ +this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){ +this.buffer+=i(e)}openNode(e){if(!s(e))return;let t="" +;t=e.sublanguage?"language-"+e.language:((e,{prefix:t})=>{if(e.includes(".")){ +const n=e.split(".") +;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ") +}return`${t}${e}`})(e.scope,{prefix:this.classPrefix}),this.span(t)} +closeNode(e){s(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ +this.buffer+=``}}const a=(e={})=>{const t={children:[]} +;return Object.assign(t,e),t};class c{constructor(){ +this.rootNode=a(),this.stack=[this.rootNode]}get top(){ +return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ +this.top.children.push(e)}openNode(e){const t=a({scope:e}) +;this.add(t),this.stack.push(t)}closeNode(){ +if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ +for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} +walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){ +return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t), +t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){ +"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ +c._collapse(e)})))}}class l extends c{constructor(e){super(),this.options=e} +addKeyword(e,t){""!==e&&(this.openNode(t),this.addText(e),this.closeNode())} +addText(e){""!==e&&this.add(e)}addSublanguage(e,t){const n=e.root +;n.sublanguage=!0,n.language=t,this.add(n)}toHTML(){ +return new o(this,this.options).value()}finalize(){return!0}}function g(e){ +return e?"string"==typeof e?e:e.source:null}function d(e){return p("(?=",e,")")} +function u(e){return p("(?:",e,")*")}function h(e){return p("(?:",e,")?")} +function p(...e){return e.map((e=>g(e))).join("")}function f(...e){const t=(e=>{ +const t=e[e.length-1] +;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{} +})(e);return"("+(t.capture?"":"?:")+e.map((e=>g(e))).join("|")+")"} +function b(e){return RegExp(e.toString()+"|").exec("").length-1} +const m=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ +;function E(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n +;let i=g(e),r="";for(;i.length>0;){const e=m.exec(i);if(!e){r+=i;break} +r+=i.substring(0,e.index), +i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?r+="\\"+(Number(e[1])+t):(r+=e[0], +"("===e[0]&&n++)}return r})).map((e=>`(${e})`)).join(t)} +const x="[a-zA-Z]\\w*",w="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",_="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",O="\\b(0b[01]+)",v={ +begin:"\\\\[\\s\\S]",relevance:0},N={scope:"string",begin:"'",end:"'", +illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", +contains:[v]},M=(e,t,n={})=>{const i=r({scope:"comment",begin:e,end:t, +contains:[]},n);i.contains.push({scope:"doctag", +begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", +end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) +;const s=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) +;return i.contains.push({begin:p(/[ ]+/,"(",s,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i +},S=M("//","$"),R=M("/\\*","\\*/"),j=M("#","$");var A=Object.freeze({ +__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:x,UNDERSCORE_IDENT_RE:w, +NUMBER_RE:y,C_NUMBER_RE:_,BINARY_NUMBER_RE:O, +RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", +SHEBANG:(e={})=>{const t=/^#![ ]*\// +;return e.binary&&(e.begin=p(t,/.*\b/,e.binary,/\b.*/)),r({scope:"meta",begin:t, +end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)}, +BACKSLASH_ESCAPE:v,APOS_STRING_MODE:N,QUOTE_STRING_MODE:k,PHRASAL_WORDS_MODE:{ +begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ +},COMMENT:M,C_LINE_COMMENT_MODE:S,C_BLOCK_COMMENT_MODE:R,HASH_COMMENT_MODE:j, +NUMBER_MODE:{scope:"number",begin:y,relevance:0},C_NUMBER_MODE:{scope:"number", +begin:_,relevance:0},BINARY_NUMBER_MODE:{scope:"number",begin:O,relevance:0}, +REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{scope:"regexp",begin:/\//, +end:/\/[gimuy]*/,illegal:/\n/,contains:[v,{begin:/\[/,end:/\]/,relevance:0, +contains:[v]}]}]},TITLE_MODE:{scope:"title",begin:x,relevance:0}, +UNDERSCORE_TITLE_MODE:{scope:"title",begin:w,relevance:0},METHOD_GUARD:{ +begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:e=>Object.assign(e,{ +"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{ +t.data._beginMatch!==e[1]&&t.ignoreMatch()}})});function I(e,t){ +"."===e.input[e.index-1]&&t.ignoreMatch()}function T(e,t){ +void 0!==e.className&&(e.scope=e.className,delete e.className)}function L(e,t){ +t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", +e.__beforeBegin=I,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, +void 0===e.relevance&&(e.relevance=0))}function B(e,t){ +Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function D(e,t){ +if(e.match){ +if(e.begin||e.end)throw Error("begin & end are not supported with match") +;e.begin=e.match,delete e.match}}function H(e,t){ +void 0===e.relevance&&(e.relevance=1)}const P=(e,t)=>{if(!e.beforeMatch)return +;if(e.starts)throw Error("beforeMatch cannot be used with starts") +;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t] +})),e.keywords=n.keywords,e.begin=p(n.beforeMatch,d(n.begin)),e.starts={ +relevance:0,contains:[Object.assign(n,{endsParent:!0})] +},e.relevance=0,delete n.beforeMatch +},C=["of","and","for","in","not","or","if","then","parent","list","value"] +;function $(e,t,n="keyword"){const i=Object.create(null) +;return"string"==typeof e?r(n,e.split(" ")):Array.isArray(e)?r(n,e):Object.keys(e).forEach((n=>{ +Object.assign(i,$(e[n],t,n))})),i;function r(e,n){ +t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|") +;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){ +return t?Number(t):(e=>C.includes(e.toLowerCase()))(e)?0:1}const z={},K=e=>{ +console.error(e)},W=(e,...t)=>{console.log("WARN: "+e,...t)},X=(e,t)=>{ +z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0) +},G=Error();function Z(e,t,{key:n}){let i=0;const r=e[n],s={},o={} +;for(let e=1;e<=t.length;e++)o[e+i]=r[e],s[e+i]=!0,i+=b(t[e-1]) +;e[n]=o,e[n]._emit=s,e[n]._multi=!0}function F(e){(e=>{ +e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, +delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ +_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope +}),(e=>{if(Array.isArray(e.begin)){ +if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), +G +;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"), +G;Z(e,e.begin,{key:"beginScope"}),e.begin=E(e.begin,{joinWith:""})}})(e),(e=>{ +if(Array.isArray(e.end)){ +if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"), +G +;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"), +G;Z(e,e.end,{key:"endScope"}),e.end=E(e.end,{joinWith:""})}})(e)}function V(e){ +function t(t,n){ +return RegExp(g(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":"")) +}class n{constructor(){ +this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} +addRule(e,t){ +t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]), +this.matchAt+=b(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) +;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(E(e,{joinWith:"|" +}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex +;const t=this.matcherRe.exec(e);if(!t)return null +;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n] +;return t.splice(0,n),Object.assign(t,i)}}class i{constructor(){ +this.rules=[],this.multiRegexes=[], +this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ +if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n +;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))), +t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){ +return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){ +this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){ +const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex +;let n=t.exec(e) +;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{ +const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)} +return n&&(this.regexIndex+=n.position+1, +this.regexIndex===this.count&&this.considerAll()),n}} +if(e.compilerExtensions||(e.compilerExtensions=[]), +e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") +;return e.classNameAliases=r(e.classNameAliases||{}),function n(s,o){const a=s +;if(s.isCompiled)return a +;[T,D,F,P].forEach((e=>e(s,o))),e.compilerExtensions.forEach((e=>e(s,o))), +s.__beforeBegin=null,[L,B,H].forEach((e=>e(s,o))),s.isCompiled=!0;let c=null +;return"object"==typeof s.keywords&&s.keywords.$pattern&&(s.keywords=Object.assign({},s.keywords), +c=s.keywords.$pattern, +delete s.keywords.$pattern),c=c||/\w+/,s.keywords&&(s.keywords=$(s.keywords,e.case_insensitive)), +a.keywordPatternRe=t(c,!0), +o&&(s.begin||(s.begin=/\B|\b/),a.beginRe=t(a.begin),s.end||s.endsWithParent||(s.end=/\B|\b/), +s.end&&(a.endRe=t(a.end)), +a.terminatorEnd=g(a.end)||"",s.endsWithParent&&o.terminatorEnd&&(a.terminatorEnd+=(s.end?"|":"")+o.terminatorEnd)), +s.illegal&&(a.illegalRe=t(s.illegal)), +s.contains||(s.contains=[]),s.contains=[].concat(...s.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>r(e,{ +variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?r(e,{ +starts:e.starts?r(e.starts):null +}):Object.isFrozen(e)?r(e):e))("self"===e?s:e)))),s.contains.forEach((e=>{n(e,a) +})),s.starts&&n(s.starts,o),a.matcher=(e=>{const t=new i +;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin" +}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end" +}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){ +return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{ +constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}} +const Y=i,Q=r,ee=Symbol("nomatch");var te=(t=>{ +const i=Object.create(null),r=Object.create(null),s=[];let o=!0 +;const a="Could not find the language '{}', did you forget to load/include a language module?",c={ +disableAutodetect:!0,name:"Plain text",contains:[]};let g={ +ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, +languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", +cssSelector:"pre code",languages:null,__emitter:l};function b(e){ +return g.noHighlightRe.test(e)}function m(e,t,n){let i="",r="" +;"object"==typeof t?(i=e, +n=t.ignoreIllegals,r=t.language):(X("10.7.0","highlight(lang, code, ...args) has been deprecated."), +X("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), +r=e,i=t),void 0===n&&(n=!0);const s={code:i,language:r};k("before:highlight",s) +;const o=s.result?s.result:E(s.language,s.code,n) +;return o.code=s.code,k("after:highlight",o),o}function E(e,t,r,s){ +const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(S) +;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(S),n="" +;for(;t;){n+=S.substring(e,t.index) +;const r=y.case_insensitive?t[0].toLowerCase():t[0],s=(i=r,N.keywords[i]);if(s){ +const[e,i]=s +;if(M.addText(n),n="",c[r]=(c[r]||0)+1,c[r]<=7&&(R+=i),e.startsWith("_"))n+=t[0];else{ +const n=y.classNameAliases[e]||e;M.addKeyword(t[0],n)}}else n+=t[0] +;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(S)}var i +;n+=S.substring(e),M.addText(n)}function d(){null!=N.subLanguage?(()=>{ +if(""===S)return;let e=null;if("string"==typeof N.subLanguage){ +if(!i[N.subLanguage])return void M.addText(S) +;e=E(N.subLanguage,S,!0,k[N.subLanguage]),k[N.subLanguage]=e._top +}else e=x(S,N.subLanguage.length?N.subLanguage:null) +;N.relevance>0&&(R+=e.relevance),M.addSublanguage(e._emitter,e.language) +})():l(),S=""}function u(e,t){let n=1;const i=t.length-1;for(;n<=i;){ +if(!e._emit[n]){n++;continue}const i=y.classNameAliases[e[n]]||e[n],r=t[n] +;i?M.addKeyword(r,i):(S=r,l(),S=""),n++}}function h(e,t){ +return e.scope&&"string"==typeof e.scope&&M.openNode(y.classNameAliases[e.scope]||e.scope), +e.beginScope&&(e.beginScope._wrap?(M.addKeyword(S,y.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), +S=""):e.beginScope._multi&&(u(e.beginScope,t),S="")),N=Object.create(e,{parent:{ +value:N}}),N}function p(e,t,i){let r=((e,t)=>{const n=e&&e.exec(t) +;return n&&0===n.index})(e.endRe,i);if(r){if(e["on:end"]){const i=new n(e) +;e["on:end"](t,i),i.isMatchIgnored&&(r=!1)}if(r){ +for(;e.endsParent&&e.parent;)e=e.parent;return e}} +if(e.endsWithParent)return p(e.parent,t,i)}function f(e){ +return 0===N.matcher.regexIndex?(S+=e[0],1):(I=!0,0)}function b(e){ +const n=e[0],i=t.substring(e.index),r=p(N,e,i);if(!r)return ee;const s=N +;N.endScope&&N.endScope._wrap?(d(), +M.addKeyword(n,N.endScope._wrap)):N.endScope&&N.endScope._multi?(d(), +u(N.endScope,e)):s.skip?S+=n:(s.returnEnd||s.excludeEnd||(S+=n), +d(),s.excludeEnd&&(S=n));do{ +N.scope&&M.closeNode(),N.skip||N.subLanguage||(R+=N.relevance),N=N.parent +}while(N!==r.parent);return r.starts&&h(r.starts,e),s.returnEnd?0:n.length} +let m={};function w(i,s){const a=s&&s[0];if(S+=i,null==a)return d(),0 +;if("begin"===m.type&&"end"===s.type&&m.index===s.index&&""===a){ +if(S+=t.slice(s.index,s.index+1),!o){const t=Error(`0 width match regex (${e})`) +;throw t.languageName=e,t.badRule=m.rule,t}return 1} +if(m=s,"begin"===s.type)return(e=>{ +const t=e[0],i=e.rule,r=new n(i),s=[i.__beforeBegin,i["on:begin"]] +;for(const n of s)if(n&&(n(e,r),r.isMatchIgnored))return f(t) +;return i.skip?S+=t:(i.excludeBegin&&(S+=t), +d(),i.returnBegin||i.excludeBegin||(S=t)),h(i,e),i.returnBegin?0:t.length})(s) +;if("illegal"===s.type&&!r){ +const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"")+'"') +;throw e.mode=N,e}if("end"===s.type){const e=b(s);if(e!==ee)return e} +if("illegal"===s.type&&""===a)return 1 +;if(A>1e5&&A>3*s.index)throw Error("potential infinite loop, way more iterations than matches") +;return S+=a,a.length}const y=O(e) +;if(!y)throw K(a.replace("{}",e)),Error('Unknown language: "'+e+'"') +;const _=V(y);let v="",N=s||_;const k={},M=new g.__emitter(g);(()=>{const e=[] +;for(let t=N;t!==y;t=t.parent)t.scope&&e.unshift(t.scope) +;e.forEach((e=>M.openNode(e)))})();let S="",R=0,j=0,A=0,I=!1;try{ +for(N.matcher.considerAll();;){ +A++,I?I=!1:N.matcher.considerAll(),N.matcher.lastIndex=j +;const e=N.matcher.exec(t);if(!e)break;const n=w(t.substring(j,e.index),e) +;j=e.index+n} +return w(t.substring(j)),M.closeAllNodes(),M.finalize(),v=M.toHTML(),{ +language:e,value:v,relevance:R,illegal:!1,_emitter:M,_top:N}}catch(n){ +if(n.message&&n.message.includes("Illegal"))return{language:e,value:Y(t), +illegal:!0,relevance:0,_illegalBy:{message:n.message,index:j, +context:t.slice(j-100,j+100),mode:n.mode,resultSoFar:v},_emitter:M};if(o)return{ +language:e,value:Y(t),illegal:!1,relevance:0,errorRaised:n,_emitter:M,_top:N} +;throw n}}function x(e,t){t=t||g.languages||Object.keys(i);const n=(e=>{ +const t={value:Y(e),illegal:!1,relevance:0,_top:c,_emitter:new g.__emitter(g)} +;return t._emitter.addText(e),t})(e),r=t.filter(O).filter(N).map((t=>E(t,e,!1))) +;r.unshift(n);const s=r.sort(((e,t)=>{ +if(e.relevance!==t.relevance)return t.relevance-e.relevance +;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1 +;if(O(t.language).supersetOf===e.language)return-1}return 0})),[o,a]=s,l=o +;return l.secondBest=a,l}function w(e){let t=null;const n=(e=>{ +let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"" +;const n=g.languageDetectRe.exec(t);if(n){const t=O(n[1]) +;return t||(W(a.replace("{}",n[1])), +W("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"} +return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return +;if(k("before:highlightElement",{el:e,language:n +}),e.children.length>0&&(g.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), +console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), +console.warn("The element with unescaped HTML:"), +console.warn(e)),g.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML) +;t=e;const i=t.textContent,s=n?m(i,{language:n,ignoreIllegals:!0}):x(i) +;e.innerHTML=s.value,((e,t,n)=>{const i=t&&r[t]||n +;e.classList.add("hljs"),e.classList.add("language-"+i) +})(e,n,s.language),e.result={language:s.language,re:s.relevance, +relevance:s.relevance},s.secondBest&&(e.secondBest={ +language:s.secondBest.language,relevance:s.secondBest.relevance +}),k("after:highlightElement",{el:e,result:s,text:i})}let y=!1;function _(){ +"loading"!==document.readyState?document.querySelectorAll(g.cssSelector).forEach(w):y=!0 +}function O(e){return e=(e||"").toLowerCase(),i[e]||i[r[e]]} +function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ +r[e.toLowerCase()]=t}))}function N(e){const t=O(e) +;return t&&!t.disableAutodetect}function k(e,t){const n=e;s.forEach((e=>{ +e[n]&&e[n](t)}))} +"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ +y&&_()}),!1),Object.assign(t,{highlight:m,highlightAuto:x,highlightAll:_, +highlightElement:w, +highlightBlock:e=>(X("10.7.0","highlightBlock will be removed entirely in v12.0"), +X("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{g=Q(g,e)}, +initHighlighting:()=>{ +_(),X("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, +initHighlightingOnLoad:()=>{ +_(),X("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") +},registerLanguage:(e,n)=>{let r=null;try{r=n(t)}catch(t){ +if(K("Language definition for '{}' could not be registered.".replace("{}",e)), +!o)throw t;K(t),r=c} +r.name||(r.name=e),i[e]=r,r.rawDefinition=n.bind(null,t),r.aliases&&v(r.aliases,{ +languageName:e})},unregisterLanguage:e=>{delete i[e] +;for(const t of Object.keys(r))r[t]===e&&delete r[t]}, +listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:v, +autoDetection:N,inherit:Q,addPlugin:e=>{(e=>{ +e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{ +e["before:highlightBlock"](Object.assign({block:t.el},t)) +}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{ +e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),s.push(e)} +}),t.debugMode=()=>{o=!1},t.safeMode=()=>{o=!0 +},t.versionString="11.7.0",t.regex={concat:p,lookahead:d,either:f,optional:h, +anyNumberOfTimes:u};for(const t in A)"object"==typeof A[t]&&e.exports(A[t]) +;return Object.assign(t,A),t})({});return te}() +;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `xml` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const a=e.regex,n=a.concat(/[\p{L}_]/u,a.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),s={ +className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},t={begin:/\s/, +contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] +},i=e.inherit(t,{begin:/\(/,end:/\)/}),c=e.inherit(e.APOS_STRING_MODE,{ +className:"string"}),l=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),r={ +endsWithParent:!0,illegal:/`]+/}]}]}]};return{ +name:"HTML, XML", +aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], +case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin://,relevance:10,contains:[t,l,c,i,{begin:/\[/,end:/\]/,contains:[{ +className:"meta",begin://,contains:[t,i,l,c]}]}] +},e.COMMENT(//,{relevance:10}),{begin://, +relevance:10},s,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/, +relevance:10,contains:[l]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"style"},contains:[r],starts:{ +end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"script"},contains:[r],starts:{ +end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ +className:"tag",begin:/<>|<\/>/},{className:"tag", +begin:a.concat(//,/>/,/\s/)))), +end:/\/?>/,contains:[{className:"name",begin:n,relevance:0,starts:r}]},{ +className:"tag",begin:a.concat(/<\//,a.lookahead(a.concat(n,/>/))),contains:[{ +className:"name",begin:n,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}} +})();hljs.registerLanguage("xml",e)})();/*! `plaintext` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var t=(()=>{"use strict";return t=>({name:"Plain text", +aliases:["text","txt"],disableAutodetect:!0})})() +;hljs.registerLanguage("plaintext",t)})();/*! `typescript` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],t=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],s=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],c=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],r=["arguments","this","super","console","window","document","localStorage","module","global"],i=[].concat(c,t,s) +;function o(o){const l=o.regex,d=e,b={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const a=e[0].length+e.index,t=e.input[a] +;if("<"===t||","===t)return void n.ignoreMatch();let s +;">"===t&&(((e,{after:n})=>{const a="",M={ +match:[/const|var|let/,/\s+/,d,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(T)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[S]} +;return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:g,exports:{ +PARAMS_CONTAINS:v,CLASS_REFERENCE:R},illegal:/#(?![$_A-z])/, +contains:[o.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,A,p,_,N,{match:/\$\d+/},E,R,{ +className:"attr",begin:d+l.lookahead(":"),relevance:0},M,{ +begin:"("+o.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[N,o.REGEXP_MODE,{ +className:"function",begin:T,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, +excludeEnd:!0,keywords:g,contains:v}]}]},{begin:/,/,relevance:0},{match:/\s+/, +relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:b.begin, +"on:begin":b.isTrulyOpeningTag,end:b.end}],subLanguage:"xml",contains:[{ +begin:b.begin,end:b.end,skip:!0,contains:["self"]}]}]},x,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[S,o.inherit(o.TITLE_MODE,{begin:d, +className:"title.function"})]},{match:/\.\.\./,relevance:0},I,{match:"\\$"+d, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[S]},k,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},w,C,{match:/\$[(.]/}]}}return t=>{ +const s=o(t),c=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],l={ +beginKeywords:"namespace",end:/\{/,excludeEnd:!0, +contains:[s.exports.CLASS_REFERENCE]},d={beginKeywords:"interface",end:/\{/, +excludeEnd:!0,keywords:{keyword:"interface extends",built_in:c}, +contains:[s.exports.CLASS_REFERENCE]},b={$pattern:e, +keyword:n.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]), +literal:a,built_in:i.concat(c),"variable.language":r},g={className:"meta", +begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},u=(e,n,a)=>{ +const t=e.contains.findIndex((e=>e.label===n)) +;if(-1===t)throw Error("can not find mode to replace");e.contains.splice(t,1,a)} +;return Object.assign(s.keywords,b), +s.exports.PARAMS_CONTAINS.push(g),s.contains=s.contains.concat([g,l,d]), +u(s,"shebang",t.SHEBANG()),u(s,"use_strict",{className:"meta",relevance:10, +begin:/^\s*['"]use strict['"]/ +}),s.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(s,{ +name:"TypeScript",aliases:["ts","tsx"]}),s}})() +;hljs.registerLanguage("typescript",e)})();/*! `less` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],t=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],r=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],i=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],o=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse(),n=r.concat(i) +;return a=>{const l=(e=>({IMPORTANT:{scope:"meta",begin:"!important"}, +BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number", +begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{ +className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{ +scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z][A-Za-z0-9_-]*/} +}))(a),s=n,d="([\\w-]+|@\\{[\\w-]+\\})",c=[],g=[],b=e=>({className:"string", +begin:"~?"+e+".*?"+e}),m=(e,t,r)=>({className:e,begin:t,relevance:r}),p={ +$pattern:/[a-z-]+/,keyword:"and or not only",attribute:t.join(" ")},u={ +begin:"\\(",end:"\\)",contains:g,keywords:p,relevance:0} +;g.push(a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b("'"),b('"'),l.CSS_NUMBER_MODE,{ +begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]", +excludeEnd:!0} +},l.HEXCOLOR,u,m("variable","@@?[\\w-]+",10),m("variable","@\\{[\\w-]+\\}"),m("built_in","~?`[^`]*?`"),{ +className:"attribute",begin:"[\\w-]+\\s*:",end:":",returnBegin:!0,excludeEnd:!0 +},l.IMPORTANT,{beginKeywords:"and not"},l.FUNCTION_DISPATCH);const h=g.concat({ +begin:/\{/,end:/\}/,contains:c}),f={beginKeywords:"when",endsWithParent:!0, +contains:[{beginKeywords:"and not"}].concat(g)},k={begin:d+"\\s*:", +returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/ +},l.CSS_VARIABLE,{className:"attribute",begin:"\\b("+o.join("|")+")\\b", +end:/(?=:)/,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:g}}] +},w={className:"keyword", +begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b", +starts:{end:"[;{}]",keywords:p,returnEnd:!0,contains:g,relevance:0}},v={ +className:"variable",variants:[{begin:"@[\\w-]+\\s*:",relevance:15},{ +begin:"@[\\w-]+"}],starts:{end:"[;}]",returnEnd:!0,contains:h}},y={variants:[{ +begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:d,end:/\{/}],returnBegin:!0, +returnEnd:!0,illegal:"[<='$\"]",relevance:0, +contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,f,m("keyword","all\\b"),m("variable","@\\{[\\w-]+\\}"),{ +begin:"\\b("+e.join("|")+")\\b",className:"selector-tag" +},l.CSS_NUMBER_MODE,m("selector-tag",d,0),m("selector-id","#"+d),m("selector-class","\\."+d,0),m("selector-tag","&",0),l.ATTRIBUTE_SELECTOR_MODE,{ +className:"selector-pseudo",begin:":("+r.join("|")+")"},{ +className:"selector-pseudo",begin:":(:)?("+i.join("|")+")"},{begin:/\(/, +end:/\)/,relevance:0,contains:h},{begin:"!important"},l.FUNCTION_DISPATCH]},x={ +begin:`[\\w-]+:(:)?(${s.join("|")})`,returnBegin:!0,contains:[y]} +;return c.push(a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,w,v,x,k,y,f,l.FUNCTION_DISPATCH), +{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:c}}})() +;hljs.registerLanguage("less",e)})();/*! `javascript` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],t=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],s=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],r=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],c=["arguments","this","super","console","window","document","localStorage","module","global"],i=[].concat(r,t,s) +;return o=>{const l=o.regex,b=e,d={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const a=e[0].length+e.index,t=e.input[a] +;if("<"===t||","===t)return void n.ignoreMatch();let s +;">"===t&&(((e,{after:n})=>{const a="",M={ +match:[/const|var|let/,/\s+/,b,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(C)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[S]} +;return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:g,exports:{ +PARAMS_CONTAINS:p,CLASS_REFERENCE:R},illegal:/#(?![$_A-z])/, +contains:[o.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,y,N,_,h,{match:/\$\d+/},E,R,{ +className:"attr",begin:b+l.lookahead(":"),relevance:0},M,{ +begin:"("+o.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[h,o.REGEXP_MODE,{ +className:"function",begin:C,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, +excludeEnd:!0,keywords:g,contains:p}]}]},{begin:/,/,relevance:0},{match:/\s+/, +relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:d.begin, +"on:begin":d.isTrulyOpeningTag,end:d.end}],subLanguage:"xml",contains:[{ +begin:d.begin,end:d.end,skip:!0,contains:["self"]}]}]},O,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[S,o.inherit(o.TITLE_MODE,{begin:b, +className:"title.function"})]},{match:/\.\.\./,relevance:0},x,{match:"\\$"+b, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[S]},k,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},w,T,{match:/\$[(.]/}]}}})() +;hljs.registerLanguage("javascript",e)})();/*! `css` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],i=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],r=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],t=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],o=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse() +;return n=>{const a=n.regex,l=(e=>({IMPORTANT:{scope:"meta",begin:"!important"}, +BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number", +begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{ +className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{ +scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z][A-Za-z0-9_-]*/} +}))(n),s=[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE];return{name:"CSS", +case_insensitive:!0,illegal:/[=|'\$]/,keywords:{keyframePosition:"from to"}, +classNameAliases:{keyframePosition:"selector-tag"},contains:[l.BLOCK_COMMENT,{ +begin:/-(webkit|moz|ms|o)-(?=[a-z])/},l.CSS_NUMBER_MODE,{ +className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0},{ +className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 +},l.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ +begin:":("+r.join("|")+")"},{begin:":(:)?("+t.join("|")+")"}]},l.CSS_VARIABLE,{ +className:"attribute",begin:"\\b("+o.join("|")+")\\b"},{begin:/:/,end:/[;}{]/, +contains:[l.BLOCK_COMMENT,l.HEXCOLOR,l.IMPORTANT,l.CSS_NUMBER_MODE,...s,{ +begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" +},contains:[...s,{className:"string",begin:/[^)]/,endsWithParent:!0, +excludeEnd:!0}]},l.FUNCTION_DISPATCH]},{begin:a.lookahead(/@/),end:"[{;]", +relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ +},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ +$pattern:/[a-z-]+/,keyword:"and or not only",attribute:i.join(" ")},contains:[{ +begin:/[a-z-]+(?=:)/,className:"attribute"},...s,l.CSS_NUMBER_MODE]}]},{ +className:"selector-tag",begin:"\\b("+e.join("|")+")\\b"}]}}})() +;hljs.registerLanguage("css",e)})();/*! `json` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const a=["true","false","null"],n={ +scope:"literal",beginKeywords:a.join(" ")};return{name:"JSON",keywords:{ +literal:a},contains:[{className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/, +relevance:1.01},{match:/[{}[\],:]/,className:"punctuation",relevance:0 +},e.QUOTE_STRING_MODE,n,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], +illegal:"\\S"}}})();hljs.registerLanguage("json",e)})();/*! `scala` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const n=e.regex,a={className:"subst", +variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:/\$\{/,end:/\}/}]},s={ +className:"string",variants:[{begin:'"""',end:'"""'},{begin:'"',end:'"', +illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:'[a-z]+"',end:'"', +illegal:"\\n",contains:[e.BACKSLASH_ESCAPE,a]},{className:"string", +begin:'[a-z]+"""',end:'"""',contains:[a],relevance:10}]},i={className:"type", +begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},t={className:"title", +begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/, +relevance:0},l={className:"class",beginKeywords:"class object trait type", +end:/[:={\[\n;]/,excludeEnd:!0, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{ +beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0, +excludeEnd:!0,relevance:0,contains:[i]},{className:"params",begin:/\(/,end:/\)/, +excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[i]},t]},r={ +className:"function",beginKeywords:"def",end:n.lookahead(/[:={\[(\n;]/), +contains:[t]};return{name:"Scala",keywords:{literal:"true false null", +keyword:"type yield lazy override def with val var sealed abstract private trait object if then forSome for while do throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit export enum given transparent" +}, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,i,r,l,e.C_NUMBER_MODE,{ +begin:[/^\s*/,"extension",/\s+(?=[[(])/],beginScope:{2:"keyword"}},{ +begin:[/^\s*/,/end/,/\s+/,/(extension\b)?/],beginScope:{2:"keyword",4:"keyword"} +},{match:/\.inline\b/},{begin:/\binline(?=\s)/,keywords:"inline"},{ +begin:[/\(\s*/,/using/,/\s+(?!\))/],beginScope:{2:"keyword"}},{className:"meta", +begin:"@[A-Za-z]+"}]}}})();hljs.registerLanguage("scala",e)})();/*! `diff` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const a=e.regex;return{name:"Diff", +aliases:["patch"],contains:[{className:"meta",relevance:10, +match:a.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/) +},{className:"comment",variants:[{ +begin:a.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/), +end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{ +className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/, +end:/$/}]}}})();hljs.registerLanguage("diff",e)})();/*! `scss` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],r=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],i=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],t=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],o=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse() +;return n=>{const a=(e=>({IMPORTANT:{scope:"meta",begin:"!important"}, +BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number", +begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{ +className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{ +scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z][A-Za-z0-9_-]*/} +}))(n),l=t,s=i,d="@[a-z-]+",c={className:"variable", +begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS", +case_insensitive:!0,illegal:"[=/|']", +contains:[n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,a.CSS_NUMBER_MODE,{ +className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{ +className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0 +},a.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag", +begin:"\\b("+e.join("|")+")\\b",relevance:0},{className:"selector-pseudo", +begin:":("+s.join("|")+")"},{className:"selector-pseudo", +begin:":(:)?("+l.join("|")+")"},c,{begin:/\(/,end:/\)/, +contains:[a.CSS_NUMBER_MODE]},a.CSS_VARIABLE,{className:"attribute", +begin:"\\b("+o.join("|")+")\\b"},{ +begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" +},{begin:/:/,end:/[;}{]/,relevance:0, +contains:[a.BLOCK_COMMENT,c,a.HEXCOLOR,a.CSS_NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,a.IMPORTANT,a.FUNCTION_DISPATCH] +},{begin:"@(page|font-face)",keywords:{$pattern:d,keyword:"@page @font-face"}},{ +begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/, +keyword:"and or not only",attribute:r.join(" ")},contains:[{begin:d, +className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute" +},c,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,a.HEXCOLOR,a.CSS_NUMBER_MODE] +},a.FUNCTION_DISPATCH]}}})();hljs.registerLanguage("scss",e)})();/*! `bash` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const s=e.regex,t={},n={begin:/\$\{/, +end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]};Object.assign(t,{ +className:"variable",variants:[{ +begin:s.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},n]});const a={ +className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},i={ +begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/, +end:/(\w+)/,className:"string"})]}},c={className:"string",begin:/"/,end:/"/, +contains:[e.BACKSLASH_ESCAPE,t,a]};a.contains.push(c);const o={begin:/\$?\(\(/, +end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t] +},r=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10 +}),l={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0, +contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{ +name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/, +keyword:["if","then","else","elif","fi","for","while","in","do","done","case","esac","function"], +literal:["true","false"], +built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"] +},contains:[r,e.SHEBANG(),l,o,e.HASH_COMMENT_MODE,i,{match:/(\/[a-z._-]+)+/},c,{ +className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},t]}}})() +;hljs.registerLanguage("bash",e)})();/*! `shell` grammar compiled for Highlight.js 11.7.0 */ +(()=>{var s=(()=>{"use strict";return s=>({name:"Shell Session", +aliases:["console","shellsession"],contains:[{className:"meta.prompt", +begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/, +subLanguage:"bash"}}]})})();hljs.registerLanguage("shell",s)})(); \ No newline at end of file diff --git a/_assets/js/highlight.pack.js b/_assets/js/highlight.pack.js deleted file mode 100644 index 05ca0394..00000000 --- a/_assets/js/highlight.pack.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! highlight.js v8.9.1 | BSD3 License | git.io/hljslicense */ -!function(e){if("undefined"!=typeof exports)e(exports);else if("undefined"!=typeof window)window.hljs=e({}),"function"==typeof define&&define.amd&&define("hljs",[],function(){return window.hljs});else{if("undefined"==typeof self)throw new Error("No global object found to bind hljs variable to.");self.hljs=e({})}}(function(e){function t(e){return e.replace(/&/gm,"&").replace(//gm,">")}function r(e){return e.nodeName.toLowerCase()}function n(e,t){var r=e&&e.exec(t);return r&&0==r.index}function a(e){return/^(no-?highlight|plain|text)$/i.test(e)}function i(e){var t,r,n,i=e.className+" ";if(i+=e.parentNode?e.parentNode.className:"",r=/\blang(?:uage)?-([\w-]+)\b/i.exec(i))return w(r[1])?r[1]:"no-highlight";for(i=i.split(/\s+/),t=0,n=i.length;n>t;t++)if(w(i[t])||a(i[t]))return i[t]}function o(e,t){var r,n={};for(r in e)n[r]=e[r];if(t)for(r in t)n[r]=t[r];return n}function c(e){var t=[];return function n(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3==i.nodeType?a+=i.nodeValue.length:1==i.nodeType&&(t.push({event:"start",offset:a,node:i}),a=n(i,a),r(i).match(/br|hr|img|input/)||t.push({event:"stop",offset:a,node:i}));return a}(e,0),t}function s(e,n,a){function i(){return e.length&&n.length?e[0].offset!=n[0].offset?e[0].offset"}function c(e){u+=""}function s(e){("start"==e.event?o:c)(e.node)}for(var l=0,u="",f=[];e.length||n.length;){var d=i();if(u+=t(a.substr(l,d[0].offset-l)),l=d[0].offset,d==e){f.reverse().forEach(c);do s(d.splice(0,1)[0]),d=i();while(d==e&&d.length&&d[0].offset==l);f.reverse().forEach(o)}else"start"==d[0].event?f.push(d[0].node):f.pop(),s(d.splice(0,1)[0])}return u+t(a.substr(l))}function l(e){function t(e){return e&&e.source||e}function r(r,n){return new RegExp(t(r),"m"+(e.cI?"i":"")+(n?"g":""))}function n(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var c={},s=function(t,r){e.cI&&(r=r.toLowerCase()),r.split(" ").forEach(function(e){var r=e.split("|");c[r[0]]=[t,r[1]?Number(r[1]):1]})};"string"==typeof a.k?s("keyword",a.k):Object.keys(a.k).forEach(function(e){s(e,a.k[e])}),a.k=c}a.lR=r(a.l||/\b\w+\b/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=r(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=r(a.e)),a.tE=t(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=r(a.i)),void 0===a.r&&(a.r=1),a.c||(a.c=[]);var l=[];a.c.forEach(function(e){e.v?e.v.forEach(function(t){l.push(o(e,t))}):l.push("self"==e?a:e)}),a.c=l,a.c.forEach(function(e){n(e,a)}),a.starts&&n(a.starts,i);var u=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(t).filter(Boolean);a.t=u.length?r(u.join("|"),!0):{exec:function(){return null}}}}n(e)}function u(e,r,a,i){function o(e,t){for(var r=0;r";return i+=e+'">',i+t+o}function b(){if(!C.k)return t(R);var e="",r=0;C.lR.lastIndex=0;for(var n=C.lR.exec(R);n;){e+=t(R.substr(r,n.index-r));var a=d(C,n);a?(B+=a[1],e+=p(a[0],t(n[0]))):e+=t(n[0]),r=C.lR.lastIndex,n=C.lR.exec(R)}return e+t(R.substr(r))}function h(){var e="string"==typeof C.sL;if(e&&!k[C.sL])return t(R);var r=e?u(C.sL,R,!0,x[C.sL]):f(R,C.sL.length?C.sL:void 0);return C.r>0&&(B+=r.r),e&&(x[C.sL]=r.top),p(r.language,r.value,!1,!0)}function m(){return void 0!==C.sL?h():b()}function g(e,r){var n=e.cN?p(e.cN,"",!0):"";e.rB?(M+=n,R=""):e.eB?(M+=t(r)+n,R=""):(M+=n,R=r),C=Object.create(e,{parent:{value:C}})}function y(e,r){if(R+=e,void 0===r)return M+=m(),0;var n=o(r,C);if(n)return M+=m(),g(n,r),n.rB?0:r.length;var a=c(C,r);if(a){var i=C;i.rE||i.eE||(R+=r),M+=m();do C.cN&&(M+=""),B+=C.r,C=C.parent;while(C!=a.parent);return i.eE&&(M+=t(r)),R="",a.starts&&g(a.starts,""),i.rE?0:r.length}if(s(r,C))throw new Error('Illegal lexeme "'+r+'" for mode "'+(C.cN||"")+'"');return R+=r,r.length||1}var v=w(e);if(!v)throw new Error('Unknown language: "'+e+'"');l(v);var E,C=i||v,x={},M="";for(E=C;E!=v;E=E.parent)E.cN&&(M=p(E.cN,"",!0)+M);var R="",B=0;try{for(var A,I,L=0;;){if(C.t.lastIndex=L,A=C.t.exec(r),!A)break;I=y(r.substr(L,A.index-L),A[0]),L=A.index+I}for(y(r.substr(L)),E=C;E.parent;E=E.parent)E.cN&&(M+="");return{r:B,value:M,language:e,top:C}}catch(z){if(-1!=z.message.indexOf("Illegal"))return{r:0,value:t(r)};throw z}}function f(e,r){r=r||N.languages||Object.keys(k);var n={r:0,value:t(e)},a=n;return r.forEach(function(t){if(w(t)){var r=u(t,e,!1);r.language=t,r.r>a.r&&(a=r),r.r>n.r&&(a=n,n=r)}}),a.language&&(n.second_best=a),n}function d(e){return N.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,t){return t.replace(/\t/g,N.tabReplace)})),N.useBR&&(e=e.replace(/\n/g,"
")),e}function p(e,t,r){var n=t?E[t]:r,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(n)&&a.push(n),a.join(" ").trim()}function b(e){var t=i(e);if(!a(t)){var r;N.useBR?(r=document.createElementNS("http://www.w3.org/1999/xhtml","div"),r.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):r=e;var n=r.textContent,o=t?u(t,n,!0):f(n),l=c(r);if(l.length){var b=document.createElementNS("http://www.w3.org/1999/xhtml","div");b.innerHTML=o.value,o.value=s(l,c(b),n)}o.value=d(o.value),e.innerHTML=o.value,e.className=p(e.className,t,o.language),e.result={language:o.language,re:o.r},o.second_best&&(e.second_best={language:o.second_best.language,re:o.second_best.r})}}function h(e){N=o(N,e)}function m(){if(!m.called){m.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,b)}}function g(){addEventListener("DOMContentLoaded",m,!1),addEventListener("load",m,!1)}function y(t,r){var n=k[t]=r(e);n.aliases&&n.aliases.forEach(function(e){E[e]=t})}function v(){return Object.keys(k)}function w(e){return e=(e||"").toLowerCase(),k[e]||k[E[e]]}var N={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},k={},E={};return e.highlight=u,e.highlightAuto=f,e.fixMarkup=d,e.highlightBlock=b,e.configure=h,e.initHighlighting=m,e.initHighlightingOnLoad=g,e.registerLanguage=y,e.listLanguages=v,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(t,r,n){var a=e.inherit({cN:"comment",b:t,e:r,c:[]},n||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},r={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]},n={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/-?[a-z\.]+/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,e.NM,r,n,t]}}),e.registerLanguage("clojure",function(e){var t={built_in:"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},r="a-zA-Z_\\-!.?+*=<>&#'",n="["+r+"]["+r+"0-9/;:]*",a="[-+]?\\d+(\\.\\d+)?",i={b:n,r:0},o={cN:"number",b:a,r:0},c=e.inherit(e.QSM,{i:null}),s=e.C(";","$",{r:0}),l={cN:"literal",b:/\b(true|false|nil)\b/},u={b:"[\\[\\{]",e:"[\\]\\}]"},f={cN:"comment",b:"\\^"+n},d=e.C("\\^\\{","\\}"),p={cN:"symbol",b:"[:]"+n},b={b:"\\(",e:"\\)"},h={eW:!0,r:0},m={k:t,l:n,cN:"name",b:n,starts:h},g=[b,c,f,d,s,p,u,o,l,i];return b.c=[e.C("comment",""),m,h],h.c=g,u.c=g,{aliases:["clj"],i:/\S/,c:[b,c,f,d,s,p,u,o,l]}}),e.registerLanguage("coffeescript",function(e){var t={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},r="[A-Za-z$_][0-9A-Za-z$_]*",n={cN:"subst",b:/#\{/,e:/}/,k:t},a=[e.BNM,e.inherit(e.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/"""/,e:/"""/,c:[e.BE,n]},{b:/"/,e:/"/,c:[e.BE,n]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[n,e.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{b:"@"+r},{b:"`",e:"`",eB:!0,eE:!0,sL:"javascript"}];n.c=a;var i=e.inherit(e.TM,{b:r}),o="(\\(.*\\))?\\s*\\B[-=]>",c={cN:"params",b:"\\([^\\(]",rB:!0,c:[{b:/\(/,e:/\)/,k:t,c:["self"].concat(a)}]};return{aliases:["coffee","cson","iced"],k:t,i:/\/\*/,c:a.concat([e.C("###","###"),e.HCM,{cN:"function",b:"^\\s*"+r+"\\s*=\\s*"+o,e:"[-=]>",rB:!0,c:[i,c]},{b:/[:\(,=]\s*/,r:0,c:[{cN:"function",b:o,e:"[-=]>",rB:!0,c:[c]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:!0,i:/[:="\[\]]/,c:[i]},i]},{b:r+":",e:":",rB:!0,rE:!0,r:0}])}}),e.registerLanguage("elm",function(e){var t={v:[e.C("--","$"),e.C("{-","-}",{c:["self"]})]},r={cN:"type",b:"\\b[A-Z][\\w']*",r:0},n={b:"\\(",e:"\\)",i:'"',c:[{cN:"type",b:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},t]},a={b:"{",e:"}",c:n.c};return{k:"let in if then else case of where module import exposing type alias as infix infixl infixr port",c:[{bK:"module",e:"where",k:"module where",c:[n,t],i:"\\W\\.|;"},{b:"import",e:"$",k:"import as exposing",c:[n,t],i:"\\W\\.|;"},{b:"type",e:"$",k:"type alias",c:[r,n,a,t]},{bK:"infix infixl infixr",e:"$",c:[e.CNM,t]},{b:"port",e:"$",k:"port",c:[t]},e.QSM,e.CNM,r,e.inherit(e.TM,{b:"^[_a-z][\\w']*"}),t,{b:"->|<-"}]}}),e.registerLanguage("java",function(e){var t=e.UIR+"(<"+e.UIR+">)?",r="false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private",n="\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",a={cN:"number",b:n,r:0};return{aliases:["jsp"],k:r,i:/<\/|#/,c:[e.C("/\\*\\*","\\*/",{r:0,c:[{b:/\w+@/,r:0},{cN:"doctag",b:"@[A-Za-z]+"}]}),e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:!0,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},e.UTM]},{bK:"new throw return else",r:0},{cN:"function",b:"("+t+"\\s+)+"+e.UIR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:r,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"params",b:/\(/,e:/\)/,k:r,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},a,{cN:"meta",b:"@[A-Za-z]+"}]}}),e.registerLanguage("javascript",function(e){return{aliases:["js"],k:{keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},e.ASM,e.QSM,{cN:"string",b:"`",e:"`",c:[e.BE,{cN:"subst",b:"\\$\\{",e:"\\}"}]},e.CLCM,e.CBCM,{cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{b:/\s*[);\]]/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:[e.CLCM,e.CBCM]}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+e.IR,r:0},{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#/}}),e.registerLanguage("json",function(e){var t={literal:"true false null"},r=[e.QSM,e.CNM],n={e:",",eW:!0,eE:!0,c:r,k:t},a={b:"{",e:"}",c:[{cN:"attr",b:'\\s*"',e:'"\\s*:\\s*',eB:!0,eE:!0,c:[e.BE],i:"\\n",starts:n}],i:"\\S"},i={b:"\\[",e:"\\]",c:[e.inherit(n)],i:"\\S"};return r.splice(r.length,0,a,i),{c:r,k:t,i:"\\S"}}),e.registerLanguage("scala",function(e){var t={cN:"meta",b:"@[A-Za-z]+"},r={cN:"subst",v:[{b:"\\$[A-Za-z0-9_]+"},{b:"\\${",e:"}"}]},n={cN:"string",v:[{b:'"',e:'"',i:"\\n",c:[e.BE]},{b:'"""',e:'"""',r:10},{b:'[a-z]+"',e:'"',i:"\\n",c:[e.BE,r]},{cN:"string",b:'[a-z]+"""',e:'"""',c:[r],r:10}]},a={cN:"symbol",b:"'\\w[\\w\\d_]*(?!')"},i={cN:"type",b:"\\b[A-Z][A-Za-z0-9_]*",r:0},o={cN:"title",b:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,r:0},c={cN:"class",bK:"class object trait type",e:/[:={\[\n;]/,eE:!0,c:[{bK:"extends with",r:10},{cN:"params",b:/\(/,e:/\)/,r:0},o]},s={cN:"function",bK:"def",e:/[:={\[(\n;]/,c:[o]};return{k:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},c:[e.CLCM,e.CBCM,n,a,i,s,c,e.CNM,t]}}),e.registerLanguage("typescript",function(e){var t={keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void"};return{aliases:["ts"],k:t,c:[{cN:"meta",b:/^\s*['"]use strict['"]/},e.ASM,e.QSM,{cN:"string",b:"`",e:"`",c:[e.BE,{cN:"subst",b:"\\$\\{",e:"\\}"}]},e.CLCM,e.CBCM,{cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM],r:0},{cN:"function",b:"function",e:/[\{;]/,eE:!0,k:t,c:["self",e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:[e.CLCM,e.CBCM],i:/["'\(]/}],i:/\[|%/,r:0},{bK:"constructor",e:/\{/,eE:!0},{bK:"module",e:/\{/,eE:!0},{bK:"interface",e:/\{/,eE:!0,k:"interface extends"},{b:/\$[(.]/},{b:"\\."+e.IR,r:0}]}}),e}); \ No newline at end of file diff --git a/_config.yml b/_config.yml index 7d55b233..3804c666 100644 --- a/_config.yml +++ b/_config.yml @@ -68,4 +68,4 @@ versions: scalaJSBinary: 1 scalaJS06x: 0.6.33 scalaJS06xBinary: 0.6 - scalaJSDOM: 2.1.0 + scalaJSDOM: 2.4.0 diff --git a/_data/doc.yml b/_data/doc.yml index 625fc3ae..06066d96 100644 --- a/_data/doc.yml +++ b/_data/doc.yml @@ -1,12 +1,14 @@ -- text: Introduction +- text: Documentation url: /doc/ -#- text: Quick Start -# url: /doc/quick-start.html - text: Tutorials url: /doc/tutorial/ subitems: - - text: Basic tutorial - url: /doc/tutorial/basic/ + - text: Getting Started with Scala.js and Vite + url: /doc/tutorial/scalajs-vite.html + - text: Build UIs with Laminar + url: /doc/tutorial/laminar.html + - text: Integrate JavaScript libraries with ScalablyTyped + url: /doc/tutorial/scalablytyped.html - text: Scala.js for JavaScript developers url: /doc/sjs-for-js/ subitems: diff --git a/doc/index.md b/doc/index.md index 4eafdae4..dfc87318 100644 --- a/doc/index.md +++ b/doc/index.md @@ -1,29 +1,24 @@ --- layout: doc -title: Introduction +title: Documentation --- -The bullets on the right link to various facets of the documentation. +# Getting Started -If you are coming from a JavaScript background, we've got -[a special tour for you](sjs-for-js/), which will teach you the basics of -Scala.js with respect to JavaScript. +Follow our [Getting Started Tutorials](tutorial/). -# Installation +# Installation / Download -All you need to get started is +To install Scala and Node.js, follow the [Prerequisites](tutorial/#prerequisites) section of the Getting Started Tutorials. -* A recent version of Java JDK [(download)](https://www.oracle.com/java/technologies/downloads/) -* SBT [(download)](https://www.scala-sbt.org/1.x/docs/Setup.html) +Scala.js itself is managed by your Scala build tool of choice. +You can follow [the Getting Started instructions](tutorial/scalajs-vite.html#introducing-scalajs) to enable it. -See the [Basic tutorial](tutorial/basic/) for details about installation and setup. +# Other resources -# Tutorials - -Check out one of the [tutorials](tutorial/) to continue your journey! - -You may also find the [presentation videos](../community/presentations.html) -a nice way to get started with Scala.js! +* [Presentation videos](../community/presentations.html) +* [Tour of Scala.js for JavaScript developers](sjs-for-js/) +* More in the column on the right of this page # Get help diff --git a/doc/tutorial/index.md b/doc/tutorial/index.md index 0483eb3d..0bb34d13 100644 --- a/doc/tutorial/index.md +++ b/doc/tutorial/index.md @@ -3,10 +3,52 @@ layout: doc title: Tutorials --- -Arguably, the most efficient way to learn new technology is to try it out yourself. Here, we have collected a set -of tutorials from simple to complete web app, to get you started right away! +This series of tutorials teaches you how to use Scala.js together with modern development tools. +They can be followed independently of each other. -* [Basic tutorial](./basic/) to get you started +If you have time, reading and applying them in order will give you more in-depth knowledge about the development environment. + +If you are in a hurry, you can skip the ones you are not interested in. +Each tutorial starts with a link to a repo that you can clone to get off the ground. + +In any case, make sure that you have the prerequisites listed below covered. + +1. [Scala.js and Vite](./scalajs-vite.html): + * Set up a hello world project ready for live reloading in the browser. + * Generate minimized production assets. +2. [Laminar](./laminar.html): + * Build UIs with Laminar using Functional Reactive Programming (FRP), a hybrid model between imperative and functional programming particularly well suited for UI development in Scala. +3. [ScalablyTyped](./scalablytyped.html): + * Integrate JavaScript libraries using ScalablyTyped. + +## Prerequisites + +In any case, make sure that you have the following tools installed first: + +* [Scala and its development tools](https://www.scala-lang.org/download/) +* [Node.js](https://nodejs.org/en/download/) + +If in doubt, try the following commands in a terminal. +They should all succeed, though the reported version numbers may differ. + +{% highlight shell %} +$ node -v +v16.13.0 +$ npm -v +8.1.0 +$ sbt -version +sbt version in this project: 1.7.3 +sbt script version: 1.7.3 +{% endhighlight %} + +We also recommend that you use an IDE for Scala. +If you do not know what to pick, we recommend [VS Code](https://code.visualstudio.com/download/) with [the Metals extension](https://scalameta.org/metals/docs/editors/vscode/). + +## Older tutorials + +Here are some older tutorials, which may still provide value: + +* The old [basic tutorial](./basic/) * [Hands-on Scala.js](https://lihaoyi.github.io/hands-on-scala-js), an extensive tutorial in eBook format * [SPA tutorial](https://github.com/ochrons/scalajs-spa-tutorial) for writing a Single-Page-Application using React diff --git a/doc/tutorial/laminar.md b/doc/tutorial/laminar.md new file mode 100644 index 00000000..ca5b819b --- /dev/null +++ b/doc/tutorial/laminar.md @@ -0,0 +1,782 @@ +--- +layout: doc +title: Build UIs with Laminar +--- + +In this second tutorial, we learn how to develop UIs in Scala.js with [Laminar](https://laminar.dev/). + +We start here with the project setup developed in the previous tutorial about [Setting up Scala.js with Vite](./scalajs-vite.html). +To follow along this tutorial, either use the result of the previous tutorial, or checkout [the scalajs-vite-end-state branch](https://github.com/sjrd/scalajs-sbt-vite-laminar-chartjs-example/tree/scalajs-vite-end-state) of the accompanying repo. + +If you prefer to look at the end result for this tutorial directly, checkout [the laminar-end-state branch](https://github.com/sjrd/scalajs-sbt-vite-laminar-chartjs-example/tree/laminar-end-state) instead. + +[See the final result in action and fiddle with the code in Scribble](https://scribble.ninja/u/sjrd/ddueiaxghmbmbnbpzggmkwwmpigc) + +## Prerequisites + +Make sure to install [the prerequisites](./index.html#prerequisites) before continuing further. + +## Introducing Laminar + +[Laminar](https://laminar.dev/) is a Scala.js library to build UIs using Functional Reactive Programming (FRP). +FRP is a hybrid model between imperative and functional programming. +It is particularly well suited to developing UIs in Scala, as we can reason about relationships between immutable values while dealing with the changing nature of the UI. +We will elaborate on this point later. + +To start off, we add a dependency on Laminar in our `build.sbt`: + +{% highlight diff %} + libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.4.0", ++ ++ // Depend on Laminar ++ libraryDependencies += "com.raquo" %%% "laminar" % "15.0.1", + ) +{% endhighlight %} + +Once that is done, start the incremental compiler: + +{% highlight shell %} +$ sbt +[...] +sbt:livechart> ~fastLinkJS +[...] +{% endhighlight %} + +If sbt was already running, run `reload` for the changes in `build.sbt` to take effect, then start the incremental compiler with `~fastLinkJS`. + +Additionally, start Vite's development server if it wasn't already running: + +{% highlight shell %} +$ npm run dev +[...] +{% endhighlight %} + +We can now change the contents of `src/main/scala/livechart/LiveChart.scala` to use Laminar instead of vanilla DOM APIs. + +At the top, we use the following import: + +{% highlight scala %} +import com.raquo.laminar.api.L.{*, given} +{% endhighlight %} + +That import brings all the Laminar features we want into scope. + +We replace the contents of the main `def LiveChart` method with a call to Laminar's `renderOnDomContentLoaded` method. +This method bootstraps Laminar by installing a Laminar `Element` in an existing DOM element: + +{% highlight scala %} +@main +def LiveChart(): Unit = + renderOnDomContentLoaded( + dom.document.getElementById("app"), + Main.appElement() + ) +{% endhighlight %} + +and we then define `Main.appElement` as follows: + +{% highlight scala %} +object Main: + def appElement(): Element = + div( + a(href := "https://vitejs.dev", target := "_blank", + img(src := "/vite.svg", className := "logo", alt := "Vite logo"), + ), + a(href := "https://developer.mozilla.org/en-US/docs/Web/JavaScript", target := "_blank", + img(src := javascriptLogo, className := "logo vanilla", alt := "JavaScript logo"), + ), + h1("Hello Laminar!"), + div(className := "card", + button(tpe := "button"), + ), + p(className := "read-the-docs", + "Click on the Vite logo to learn more", + ), + ) + end appElement +end Main +{% endhighlight %} + +Instead of using HTML tags in a big string, we use Laminar functions corresponding to every kind of DOM element. +In the parameters, we can specify both attributes, using the `:=` operator, and child elements. + +Every reference is type-checked. +If we misspell `button` as `buton`, or if we try to put an integer in `target := 0`, the compiler will flag it as an error. + +### Code of the button + +Now, we have not yet replicated the behavior of the button. +Previously the code using the DOM API was the following: + +{% highlight scala %} +def setupCounter(element: dom.Element): Unit = + var counter = 0 + + def setCounter(count: Int): Unit = + counter = count + element.innerHTML = s"count is $counter" + + element.addEventListener("click", e => setCounter(counter + 1)) + setCounter(0) +end setupCounter +{% endhighlight %} + +In Laminar, we do not use event listeners to directly mutate the attributes of DOM elements. +Instead, we use `Var`s and `Signal`s to model *time-varying* values. + +Let us isolate the button definition in a separate method, and illustrate how to write its behavior using Laminar: + +{% highlight scala %} + def counterButton(): Element = + val counter = Var(0) + button( + tpe := "button", + "count is ", + child.text <-- counter, + onClick --> { event => counter.update(c => c + 1) }, + ) + end counterButton +{% endhighlight %} + +We declare `counter` as a `Var[Int]` initialized with `0`. +We then use it in two *bindings*: + +* In `child.text <-- counter`, we declare a text child of the button whose content will always reflect the value of `counter`. + Together with the first (immutable) text child `"count is "`, it initially forms the text `"count is 0"`. + As the value of `counter` changes over time, so does the text in the button. +* In `counter.update(c => c + 1)`, we schedule an update of the value of `counter`, to be increased by 1. + We schedule that as a result of the `onClick -->` event of the button. + +We do not need to explicitly set the `innerText` attribute of the button. +That is taken care of by the `<--` binding. + +Unlike frameworks based on a virtual DOM, Laminar bindings directly target the DOM element to update. +With a virtual DOM, when the value of `counter` changes, we would build an entirely new VDOM representation for the button (and perhaps its parents), and the framework would later diff it and identify which DOM `HTMLButtonElement` to update. +In Laminar, however, the `<--` binding remembers the precise instance of `HTMLButtonElement` to update, and directly modifies its text. +This is more efficient than going through the VDOM indirection. + +Beside `:=`, the two binding arrows `<--` and `-->` are the only symbolic operators that Laminar defines. +`:=` is a static binding. +It can be seen as a `<--` with a time-immutable value on the right. +The left arrow `<--` makes data flow from the right to the left; it is usually used with DOM attributes on the left and *signals* on the right. +The right arrow `-->` makes data flow from the left to the right; it is usually used with DOM *events* on the left and *observers* on the right. +It helps to visualize the UI as being "on the left" and the application data model as being "on the right". + +## `Var`s, `Signal`s, and Functional Reactive Programming + +Before going further, let us look more closely at what *are* `Var`s and `Signal`s. + +Consider the following three definitions: + +{% highlight scala %} +val intVar: Var[Int] = Var(1) +val intSignal: Signal[Int] = intVar.signal +val times2Signal: Signal[Int] = intSignal.map(_ * 2) +{% endhighlight %} + +The first definition is a `Var` containing an `Int`, like the `counter` we used before. +It is initialized with the value `1`, but its value can evolve over time. + +A `Var` is a read-write container. +Often, we want to give access to its value in a read-only way, which is what `intVar.signal` does. + +A `Signal` is a read-only view of some time-varying value. +`Signal`s are similar to Scala immutable collections. +Whereas collections give values as functions of *indices*, signals give them as functions of *time*. +As time progresses, the value in a `Signal` can change. + +Like collections, we can manipulate signals with the typical higher-order functions. +For example, we use `map` here to get another `times2Signal: Signal` whose time-varying value is always twice that of `intSignal`. + +We can visualize those relationships in a diagram. + +![Diagram of Vars and Signals](./vars-and-signals.svg) + +We can schedule updates to `intVar` by using its `update` method. +When we schedule `intVar.update(_ + 2)` (equivalently, `.update(x => x + 2)`), we increase its time-varying value by 2. +Automatically, the time-varying values in `intSignal` and `times2Signal` change to maintain the relationships. + +`Var`s and `Signal`s are the core concepts of **Functional Reactive Programming** (FRP). +This paradigm is a hybrid between functional programming, in which we manipulate immutable values, and imperative programming, where time plays a role. +Unlike imperative programming, when we change the value behind a `Var` that was used to compute derived `Signal`s, the latter are automatically updated. +This ensures that *relationships* between `Var`s and `Signal`s are maintained at all times. +Thanks to these properties, we can reason about our program in a very similar way that we do when using only immutable values, as is the case in functional programming. + +## Data Model + +We can now start developing our live chart application in earnest. +The first thing we need is a *model* for the data that we want to edit and render. +We will focus on "shopping list" items with string labels, decimal prices and integer counts. +Each item also offers a `fullPrice` accessor, for convenience. +Therefore, an immutable model of our data can look like the following: + +{% highlight scala %} +import scala.util.Random + +final class DataItemID + +case class DataItem(id: DataItemID, label: String, price: Double, count: Int): + def fullPrice: Double = price * count + +object DataItem: + def apply(): DataItem = + DataItem(DataItemID(), "?", Random.nextDouble(), Random.nextInt(5) + 1) +end DataItem + +type DataList = List[DataItem] +{% endhighlight %} + +In addition to the expected fields `label`, `price` and `count`, we include an `id: DataItemID` in our `DataItem` class. +The ID will serve to uniquely identify rows of our table even if they happen to have the same content. +Think about a delete button on each row: if we click it, we would like the corresponding row to be removed, not another one with the same content. + +Since we want our chart to be editable, we will need to change the table data over time. +For that purpose, we put the entire `DataList` in a `Var`, which we encapsulate in a `Model` class, as follows: + +{% highlight scala %} +final class Model: + val dataVar: Var[DataList] = Var(List(DataItem(DataItemID(), "one", 1.0, 1))) + val dataSignal = dataVar.signal +end Model +{% endhighlight %} + +We also define two functions that will add a new random item, and remove a specific item (given its ID): + +{% highlight scala %} +final class Model: + ... + + def addDataItem(item: DataItem): Unit = + dataVar.update(data => data :+ item) + + def removeDataItem(id: DataItemID): Unit = + dataVar.update(data => data.filter(_.id != id)) +end Model +{% endhighlight %} + +## Testing + +This is a good time to introduce some unit tests to our application. +We want to make sure that some of the model operations, like `DataItem.fullPrice` or `Model.addDataItem`, work as expected. + +We first add the following dependency on [MUnit](https://scalameta.org/munit/), a Scala testing framework, in our `build.sbt`: + +{% highlight diff %} + // Depend on Laminar + libraryDependencies += "com.raquo" %%% "laminar" % "15.0.1", ++ ++ // Testing framework ++ libraryDependencies += "org.scalameta" %%% "munit" % "0.7.29" % Test, + ) +{% endhighlight %} + +After re-importing the project in the IDE, we create a new file `src/test/scala/livechart/ModelTest.scala`. +We write an elementary test for `DataItem.fullPrice` as follows: + +{% highlight scala %} +package livechart + +class ModelTest extends munit.FunSuite: + test("fullPrice") { + val item = DataItem(DataItemID(), "test", 0.5, 5) + assert(item.fullPrice == 2.5) + } +end ModelTest +{% endhighlight %} + +We can run our test from the `sbt` prompt with the `test` command: + +{% highlight none %} +sbt:livechart> test +livechart.ModelTest: + + fullPrice 0.00s +[info] Passed: Total 1, Failed 0, Errors 0, Passed 1 +[success] Total time: 0 s, completed +{% endhighlight %} + +In order to test `addDataItem` and `removeDataItem`, we need to read the current value of `dataSignal`. +For testing purposes, the most straightforward way to do so is to use the `now()` method. +In the application code, we would prefer using `map()` and other combinators, as we will see later, but `now()` is good for tests. + +{% highlight scala %} + test("addDataItem") { + val model = new Model + + val item = DataItem(DataItemID(), "test", 0.5, 2) + model.addDataItem(item) + + val afterItems = model.dataSignal.now() + assert(afterItems.size == 2) + assert(afterItems.last == item) + } + + test("removeDataItem") { + val model = new Model + + model.addDataItem(DataItem(DataItemID(), "test", 0.5, 2)) + + val beforeItems = model.dataSignal.now() + assert(beforeItems.size == 2) + + model.removeDataItem(beforeItems.head.id) + + val afterItems = model.dataSignal.now() + assert(afterItems.size == 1) + assert(afterItems == beforeItems.tail) + } +{% endhighlight %} + +Running the tests now yields + +{% highlight none %} +sbt:livechart> test +livechart.ModelTest: + + fullPrice 0.00s + + addDataItem 0.00s + + removeDataItem 0.00s +[info] Passed: Total 3, Failed 0, Errors 0, Passed 3 +[success] Total time: 0 s, completed +{% endhighlight %} + +## Rendering as a table + +For this article in the series, we focus on Laminar itself, and therefore on rendering the *table* view of our data. + +{% highlight scala %} +object Main: + val model = new Model + import model.* + + def appElement(): Element = + div( + h1("Live Chart"), + renderDataTable(), + ) + end appElement + + def renderDataTable(): Element = + table( + thead(tr(th("Label"), th("Price"), th("Count"), th("Full price"), th("Action"))), + tbody( + children <-- dataSignal.map(data => data.map { item => + renderDataItem(item.id, item) + }), + ), + tfoot(tr( + td(button("➕", onClick --> (_ => addDataItem(DataItem())))), + td(), + td(), + td(child.text <-- dataSignal.map(data => "%.2f".format(data.map(_.fullPrice).sum))), + )), + ) + end renderDataTable + + def renderDataItem(id: DataItemID, item: DataItem): Element = + tr( + td(item.label), + td(item.price), + td(item.count), + td("%.2f".format(item.fullPrice)), + td(button("🗑️", onClick --> (_ => removeDataItem(id)))), + ) + end renderDataItem +end Main +{% endhighlight %} + +Let us pick apart the above. +First, the bottommost function, `renderDataItem`, takes one `DataItem` and renders a table row (`tr`) for that item. +The row contains text cells for the `label`, `price`, `count` and `fullPrice`. +It also contains a Remove `button`. +Recall from our initial example with a `counter` that `onClick --> (event => action)` performs the given `action` on every click event. +Since we do not care about the properties of the event, we use `_` instead. +The action is to call `removeDataItem(id)`, which will schedule an update to the root `dataVar`, filtering out the data item with the given `id`. + +Similarly, in `renderDataTable`, the footer of the table contains a ➕ `button` whose action is to add a new random data item to the chart data. + +In the fourth column of the footer, we insert the total price for the shopping list. +We use the `dataSignal.map` method to derive a time-varying string out of the data model. +It maps a `Signal[List[DataItem]]` into a `Signal[String]`. +Given `data: List[DataItem]`, we compute the total price as the `sum` of the `fullPrice` of each item: `data.map(_.fullPrice).sum`. +We format it with 2 decimal digits using `"%.2f".format(...)`. + +In the `tbody` element, we have to render a row for every item in the chart data list. +The list `dataSignal` is a time-varying `Signal[List[DataItem]]`. +From that, we have to derive a `Signal[List[Element]]`. +We do so with two nested `map`s: the outer one is `Signal.map`, while the inner one is `List.map`: + +{% highlight scala %} +dataSignal.map(data => data.map { item => + renderDataItem(item.id, item) +}) +{% endhighlight %} + +`data` is of type `List[DataItem]`, and `item` is of type `DataItem`. + +Finally, to render the `Signal[List[Element]]` as children of the `tbody` node, we use Laminar's `children <--` binder: + +{% highlight scala %} +tbody( + children <-- dataSignal.map(data => data.map { item => + renderDataItem(item.id, item) + }), +), +{% endhighlight %} + +As the value in the `Signal[List[Element]]` changes over time, so will the list of actual DOM children of the `tbody` node. + +[See it in action and fiddle with the code in Scribble](https://scribble.ninja/u/sjrd/dbniuhlnktuvkdchyfusgqtdprgz) + +## Splitting + +When you play with the Scribble above, you may notice a suboptimal behavior: + +1. Select something within one row of the table. +1. Click the Add button, or the Remove button of some other row. +1. The selection gets lost. + +This is a symptom of a deeper issue with our existing code. +Every time one item changes, we recreate an entirely new `List[Element]`. +Even for items that stay the same, we discard the old `tr` element and create a new one. + +As long as all we have is text within the rows, the only issues we may encounter are the selection issue and suboptimal performance. +However, if we put form fields in the rows, the experience will significantly degrade. + +We would like to *reuse* the `tr` elements for the `DataItem`s that were already rendered before. +Ideally, for any given `id: DataItemID`, we would like the same rendered `tr` to be reused. + +This is exactly what Laminar's `split` method provides. +Let us amend the binding for `children` as follows: + +{% highlight scala %} +children <-- dataSignal.split(_.id) { (id, initial, itemSignal) => + renderDataItem(id, itemSignal) +}, +{% endhighlight %} + +`split` can be applied on a `Signal[List[T]]` and returns a `Signal[List[U]]`. +Whereas `map` would systematically create new output values, `split` reuses the output values for the inputs whose `_.id` is the same as before. +In other words, `_.id` is the *key* used by `split` to find previously rendered elements. + +For input elements whose key (or ID) existed before, it reuses a previously computed element. +For new input elements, with an unknown key, it calls the 3-parameter function with the following arguments: + +1. `id: DataItemID`: the key of the newly found input element. +1. `initial: DataItem`: the input element with that ID, as found the first time. +1. `itemSignal: Signal[DataItem]`: a signal of all the `DataItem`s that will be found with the same `id` in the future, starting with `initial` itself. + +The `id` is the only field taken into account to reuse an output element. +Therefore, if a future input contains a different `DataItem` with the same `id`, it will return the same output element. +That `DataItem` will then become the new value of the time-varying `itemSignal`. +This is why we now pass `itemSignal` to `renderDataItem`, which we amend to accept a `Signal[DataItem]`: + +{% highlight diff %} +- def renderDataItem(id: DataItemID, item: DataItem): Element = ++ def renderDataItem(id: DataItemID, itemSignal: Signal[DataItem]): Element = + tr( +- td(item.label), +- td(item.price), +- td(item.count), +- td("%.2f".format(item.fullPrice)), ++ td(child.text <-- itemSignal.map(_.label)), ++ td(child.text <-- itemSignal.map(_.price)), ++ td(child.text <-- itemSignal.map(_.count)), ++ td( ++ child.text <-- itemSignal.map(item => "%.2f".format(item.fullPrice)) ++ ), + td(button("🗑️", onClick --> (_ => removeDataItem(id)))), + ) + end renderDataItem +{% endhighlight %} + +We cannot directly read `item.label` or `item.fullPrice` anymore. +Instead, we use `map` to get time-varying views of the properties, which we bind to the text of the `td` elements using `child.text <--`. +You may recall that we had done something very similar in our initial `counter` example: + +{% highlight scala %} + button( + ... + "count is ", + child.text <-- counter, + ... + ) +{% endhighlight %} + +We now have optimal reuse of `tr` elements based on the `id` of the input `DataItem`s. + +[See it in action and fiddle with the code in Scribble](https://scribble.ninja/u/sjrd/ddxiuvwasiuitxshbdmmlldzhfn) + +## Editing labels + +So far, we do not have any way to actually edit individual items. +Let us start by editing labels. + +In order to be able to actually notice any change to our model, let us first add an additional *list* view summarizing the shopping list: + +```diff + def appElement(): Element = + div( + h1("Live Chart"), + renderDataTable(), ++ renderDataList(), + ) + end appElement ++ ++ def renderDataList(): Element = ++ ul( ++ children <-- dataSignal.split(_.id) { (id, initial, itemSignal) => ++ li(child.text <-- itemSignal.map(item => s"${item.count} ${item.label}")) ++ } ++ ) ++ end renderDataList +``` + +Recall our earlier function generating a table row for a `Signal[DataItem]`: + +{% highlight scala %} + def renderDataItem(id: DataItemID, itemSignal: Signal[DataItem]): Element = + tr( + td(child.text <-- itemSignal.map(_.label)), + td(child.text <-- itemSignal.map(_.price)), + td(child.text <-- itemSignal.map(_.count)), + td( + child.text <-- itemSignal.map(item => "%.2f".format(item.fullPrice)) + ), + td(button("🗑️", onClick --> (_ => removeDataItem(id)))), + ) + end renderDataItem +{% endhighlight %} + +Instead of using a `child.text` for the label, we now use an `` element. +Its value should at all times reflect the time-varying value within `itemSignal.map(_.label)`. +Conversely, if we change the value from the UI, the data model should be updated accordingly. + +{% highlight scala %} + td( + input( + typ := "text", + value <-- itemSignal.map(_.label), + onInput.mapToValue --> { (newLabel: String) => + dataVar.update { data => + data.map { item => + if item.id == id then item.copy(label = newLabel) else item + } + } + }, + ) + ), +{% endhighlight %} + +Similarly to `child.text <-- itemSignal.map(_.label)`, we now use `value <--`. +When the contents of the `Signal` changes over time, the `value` is automatically updated. +In the other direction, we use `onInput.mapToValue -->` to execute a callback every time the user updates the value from the UI. +Within the callback, we schedule an update to our `dataVar`. + +With these changes, we can already edit the labels of our data items. +As the list view of `renderDataList()` directly feeds from the `dataVar`, any changes there are automatically reflected. +We do not have to touch the list rendering method. + +We can improve our code a bit through a few refactorings. + +A callback directly feeding into `someVar.update` is a common pattern. +Laminar provides a dedicated method on `Var` for that: + +{% highlight diff %} + input( + typ := "text", + value <-- itemSignal.map(_.label), +- onInput.mapToValue --> { (newLabel: String) => +- dataVar.update { data => ++ onInput.mapToValue --> dataVar.updater[String] { (data, newLabel) => + data.map { item => + if item.id == id then item.copy(label = newLabel) else item + } +- } + }, + ) +{% endhighlight %} + +Now, we can see another pattern emerging, not related to Laminar, but to our own model. +We change a single `DataItem` in our model by `map`ping over the `data` list and transforming a single element based on its `id`. +We can refactor this pattern as a separate method. +We make that method generic in the type of value, as we will reuse it later for the `Double` prices of data items. + +{% highlight scala %} + def renderDataItem(id: DataItemID, itemSignal: Signal[DataItem]): Element = + ... + onInput.mapToValue --> makeDataItemUpdater[String](id, { + (item, newLabel) => + item.copy(label = newLabel) + }), + ... + end renderDataItem + + def makeDataItemUpdater[A](id: DataItemID, + f: (DataItem, A) => DataItem): Observer[A] = + dataVar.updater { (data, newValue) => + data.map { item => + if item.id == id then f(item, newValue) else item + } + } + end makeDataItemUpdater +{% endhighlight %} + +As a final refactoring step, we notice the following pattern: + +{% highlight scala %} + input( + typ := "text", + value <-- someSignalOfString, + onInput.mapToValue --> someObserverOfString, + ) +{% endhighlight %} + +It represents an `input` text whose `value` is linked to `Signal[String]`, which can be updated through some `Observer[String]`. +We can define a separate method for this pattern as follows: + +{% highlight scala %} + def renderDataItem(id: DataItemID, itemSignal: Signal[DataItem]): Element = + ... + td( + inputForString( + itemSignal.map(_.label), + makeDataItemUpdater(id, { (item, newLabel) => + item.copy(label = newLabel) + }) + ) + ), + ... + end renderDataItem + + def inputForString(valueSignal: Signal[String], + valueUpdater: Observer[String]): Input = + input( + typ := "text", + value <-- valueSignal, + onInput.mapToValue --> valueUpdater, + ) + end inputForString +{% endhighlight %} + +Consider what this method represents. +It takes data model values as arguments, and returns a Laminar element manipulating those values. +This is what many UI frameworks call a *component*. +In Laminar, components are nothing but methods manipulating time-varying data and returning Laminar elements. + +## Editing prices + +To finish our application, we should also be able to edit *prices* and *counts*. + +For the prices, we start with a "component" method building an `Input` that manipulates `Double` values. + +{% highlight scala %} + def inputForDouble(valueSignal: Signal[Double], + valueUpdater: Observer[Double]): Input = + val strValue = Var[String]("") + input( + typ := "text", + value <-- strValue.signal, + onInput.mapToValue --> strValue, + valueSignal --> strValue.updater[Double] { (prevStr, newValue) => + if prevStr.toDoubleOption.contains(newValue) then prevStr + else newValue.toString + }, + strValue.signal --> { valueStr => + valueStr.toDoubleOption.foreach(valueUpdater.onNext) + }, + ) + end inputForDouble +{% endhighlight %} + +It is more complicated than the one for `String`s because of the need for parsing and formatting. +This complexity perhaps better highlights the benefit of encapsulating it in a dedicated method (or component). + +We leave it to the reader to understand the details of the transformations. +We point out that we use an intermediate, local `Var[String]` to hold the actual text of the `input` element. +We then write separate transformations to link that `Var[String]` to the string representation of the `Double` signal and updater. + +Note that we put the `<--` and `-->` binders connecting `strValue` with `valueSignal` and `valueUpdater` as arguments to the Laminar `input` element. +This may seem suspicious, as none of them nor their callbacks have any direct relationship to the DOM `input` element. +We do this to tie the lifetime of the binders to the lifetime of the `input` element. +When the latter gets unmounted, we release the binder connections, possibly allowing resources to be reclaimed. + +In general, every binder must be *owned* by a Laminar element. +It only gets *activated* when that element is mounted. +This prevents memory leaks. + +## Editing counts + +For the counts, we want a component that manipulates `Int` values. +For those, we would like a more *controlled* input. +Instead of letting the user enter any string in the input, and only remember the last valid `Double` value, we now want to only allow valid `Int` values in the first place. +This is the role of the `controlled` method of Laminar. +As an approximation of its behavior, itt wraps a UI-from-data binder `<--` and a UI-to-data binder `-->` in a way that forces the UI to adhere to some filters. + +{% highlight scala %} + def inputForInt(valueSignal: Signal[Int], + valueUpdater: Observer[Int]): Input = + input( + typ := "text", + controlled( + value <-- valueSignal.map(_.toString), + onInput.mapToValue.map(_.toIntOption).collect { + case Some(newCount) => newCount + } --> valueUpdater, + ), + ) + end inputForInt +{% endhighlight %} + +We use `collect` in the `-->` binder to filter out strings that do not correctly parse as `Int` values. +`controlled` then takes care of enforcing that those values are rejected at the UI level as well. +More details about `controlled` can be found [in the Laminar documentation](https://laminar.dev/documentation#controlled-inputs). + +Finally, we update our `renderDataItem` method to use our new double and int inputs: + +{% highlight scala %} + def renderDataItem(id: DataItemID, itemSignal: Signal[DataItem]): Element = + tr( + td( + inputForString( + itemSignal.map(_.label), + makeDataItemUpdater(id, { (item, newLabel) => + item.copy(label = newLabel) + }) + ) + ), + td( + inputForDouble( + itemSignal.map(_.price), + makeDataItemUpdater(id, { (item, newPrice) => + item.copy(price = newPrice) + }) + ) + ), + td( + inputForInt( + itemSignal.map(_.count), + makeDataItemUpdater(id, { (item, newCount) => + item.copy(count = newCount) + }) + ) + ), + td( + child.text <-- itemSignal.map(item => "%.2f".format(item.fullPrice)) + ), + td(button("🗑️", onClick --> (_ => removeDataItem(id)))), + ) + end renderDataItem +{% endhighlight %} + +[See it in action and fiddle with the code in Scribble](https://scribble.ninja/u/sjrd/ddueiaxghmbmbnbpzggmkwwmpigc) + +## Conclusion + +That concludes our tutorial on Laminar. + +We saw how to use Laminar for UI development in Scala.js. +We discovered the Functional Reactive Programming (FRP) model used by Laminar. +This model is particularly suited to UI development in Scala. +With its time-varying values, it offers a balance between the changing world of UIs and the reasoning about immutable data that we favor in Scala. +We mentioned how to define "components" in Laminar as mere methods manipulating `Signal`s and `Observer`s. + +In our [next tutorial about ScalablyTyped](./scalablytyped.html), we will learn how to integrate third-party JavaScript libraries. diff --git a/doc/tutorial/scalablytyped.md b/doc/tutorial/scalablytyped.md new file mode 100644 index 00000000..c5d0d174 --- /dev/null +++ b/doc/tutorial/scalablytyped.md @@ -0,0 +1,259 @@ +--- +layout: doc +title: Integrate JavaScript libraries with ScalablyTyped +--- + +In this third tutorial, we learn how to integrate JavaScript libraries with [ScalablyTyped](https://scalablytyped.org/). + +We start here with the project developed in the previous tutorial about [UI development with Laminar](./laminar.html). +To follow along this tutorial, either use the result of the previous tutorial, or checkout [the laminar-end-state branch](https://github.com/sjrd/scalajs-sbt-vite-laminar-chartjs-example/tree/laminar-end-state) of the accompanying repo. + +If you prefer to look at the end result for this tutorial directly, checkout [the scalablytyped-end-state branch](https://github.com/sjrd/scalajs-sbt-vite-laminar-chartjs-example/tree/scalablytyped-end-state) instead. + +There are no playgrounds in [Scribble](https://scribble.ninja/) for this tutorial because it does not support ScalablyTyped. +You will need a local project to follow along. + +## Prerequisites + +Make sure to install [the prerequisites](./index.html#prerequisites) before continuing further. + +## Set up ScalablyTyped with Chart.js + +We will use [Chart.js](https://www.chartjs.org/), a JavaScript library, to draw a bar chart out of the shopping list data. +In order to get static types and bindings for Chart.js, we use [ScalablyTyped](https://scalablytyped.org/). +ScalablyTyped can read TypeScript type definition files and produce corresponding [Scala.js facade types](/doc/interoperability/facade-types.html). + +We set up our new dependencies as follows. + +First, we install some npm packages: Chart.js as a regular dependency (with `-S`), and its TypeScript type definitions along with the TypeScript compiler---required by ScalablyTyped---as development dependencies (with `-D`): + +{% highlight shell %} +$ npm install -S chart.js@2.9.4 +... +$ npm install -D @types/chart.js@2.9.29 typescript@4.9.5 +... +{% endhighlight %} + +In `project/plugins.sbt`, we add a dependency on ScalablyTyped: + +{% highlight scala %} +addSbtPlugin("org.scalablytyped.converter" % "sbt-converter" % "1.0.0-beta41") +{% endhighlight %} + +Finally, in `build.sbt`, we configure ScalablyTyped on our project: + +{% highlight diff %} + lazy val livechart = project.in(file(".")) + .enablePlugins(ScalaJSPlugin) // Enable the Scala.js plugin in this project ++ .enablePlugins(ScalablyTypedConverterExternalNpmPlugin) + .settings( + scalaVersion := "3.2.2", + [...] + // Testing framework + libraryDependencies += "org.scalameta" %%% "munit" % "0.7.29" % Test, ++ ++ // Tell ScalablyTyped that we manage `npm install` ourselves ++ externalNpm := baseDirectory.value, + ) +{% endhighlight %} + +For these changes to take effect, we have to perform the following steps: + +1. Restart sbt and the `~fastLinkJS` task (this will take a while the first time, as ScalablyTyped performs its magic) +1. Restart `npm run dev` if it was running +1. Possibly re-import the project in your IDE of choice + +## Chart configuration + +We can now enjoy Chart.js with static types in our Scala.js code. + +First, we define the Chart.js configuration that we will use: + +{% highlight scala %} + val chartConfig = + import typings.chartJs.mod.* + new ChartConfiguration { + `type` = ChartType.bar + data = new ChartData { + datasets = js.Array( + new ChartDataSets { + label = "Price" + borderWidth = 1 + backgroundColor = "green" + }, + new ChartDataSets { + label = "Full price" + borderWidth = 1 + backgroundColor = "blue" + } + ) + } + options = new ChartOptions { + scales = new ChartScales { + yAxes = js.Array(new CommonAxe { + ticks = new TickOptions { + beginAtZero = true + } + }) + } + } + } + end chartConfig +{% endhighlight %} + +At the top, we import the facade types for Chart.js generated by ScalablyTyped: + +{% highlight scala %} +import typings.chartJs.mod.* +{% endhighlight %} + +This gives us access to types like `ChartConfiguration` and `ChartType`. + +Inside the `ChartConfiguration`, we provide a number of Chart.js-related options to make our chart look the way we want: + +* the type of chart as a bar chart: `type = ChartType.bar`, +* two datasets with the labels `"Price"` and `"Full price"`, respectively, and +* the `y` axis' start value as `0`. + +All of these configuration options are type-checked, using the static types provided by ScalablyTyped. +If written in JavaScript, the above configuration would read as: + +{% highlight javascript %} +{ + type: "bar", + data: { + datasets: [ + { + label: "Price", + borderWidth: 1, + backgroundColor: "green" + }, + { + label: "Full price", + borderWidth: 1, + backgroundColor: "blue" + } + ] + }, + options: { + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } +} +{% endhighlight %} + +In a sense, that is all there is to know about ScalablyTyped. +What follows is more about the integration of a third-party "component" into Laminar than anything else. + +## Rendering the chart + +We now amend our `appElement()` method to also call a new `renderDataChart()` function: + +{% highlight diff %} + def appElement(): Element = + div( + h1("Live Chart"), + renderDataTable(), ++ renderDataChart(), + renderDataList(), + ) + end appElement +{% endhighlight %} + +The implementation of `renderDataChart()` is rather large. +We show it in its entirety first, then we will pick it apart. + +{% highlight scala %} + def renderDataChart(): Element = + import scala.scalajs.js.JSConverters.* + import typings.chartJs.mod.* + + var optChart: Option[Chart] = None + + canvasTag( + // Regular properties of the canvas + width := "100%", + height := "200px", + + // onMountUnmount callback to bridge the Laminar world and the Chart.js world + onMountUnmountCallback( + // on mount, create the `Chart` instance and store it in optChart + mount = { nodeCtx => + val domCanvas: dom.HTMLCanvasElement = nodeCtx.thisNode.ref + val chart = Chart.apply.newInstance2(domCanvas, chartConfig) + optChart = Some(chart) + }, + // on unmount, destroy the `Chart` instance + unmount = { thisNode => + for (chart <- optChart) + chart.destroy() + optChart = None + } + ), + + // Bridge the FRP world of dataSignal to the imperative world of the `chart.data` + dataSignal --> { data => + for (chart <- optChart) { + chart.data.labels = data.map(_.label).toJSArray + chart.data.datasets.get(0).data = data.map(_.price).toJSArray + chart.data.datasets.get(1).data = data.map(_.fullPrice).toJSArray + chart.update() + } + }, + ) + end renderDataChart +{% endhighlight %} + +We create a Laminar `canvasTag()` element. +We give it a `width` and `height` using Laminar's `:=`, as we did before. + +For its actual content, we want Chart.js to take over. +For that, we have to create an instance of `Chart` referencing the DOM `HTMLCanvasElement`. +In order to bridge the world of Laminar `Element`s and Chart.js, we use `onMountUnmountCallback`. + +That function takes one callback executed when the element is attached to a DOM tree, and one when it is removed. +When the element is mounted, we want to create the instance of `Chart`. +When it is unmounted, we want to call the `destroy()` method of Chart.js to release its resources. + +The `mount` callback receives a `nodeCtx`, which, among other things, gives us a handle to the underlying `HTMLCanvasElement`. +We name it `domCanvas`, and use it together with the `chartConfig` defined above to create an instance of Chart.js' `Chart` class: + +{% highlight scala %} +val domCanvas: dom.HTMLCanvasElement = nodeCtx.thisNode.ref +val chart = Chart.apply.newInstance2(domCanvas, chartConfig) +{% endhighlight %} + +We store the resulting `chart` instance in a local `var optChart: Option[Chart]`. +We will use it later to update the `chart`'s imperative data model when our FRP `dataSignal` changes. + +In order to achieve that, we use a `dataSignal -->` binder. +We give it as an argument to the Laminar `canvasTag` element to tie the binder to the canvas lifetime, as you may [recall from the Laminar tutorial](laminar.html#editing-prices). +Once the canvas gets mounted, every time the value of `dataSignal` changes, the callback is executed. + +{% highlight scala %} +dataSignal --> { data => + for (chart <- optChart) { + chart.data.labels = data.map(_.label).toJSArray + chart.data.datasets.get(0).data = data.map(_.price).toJSArray + chart.data.datasets.get(1).data = data.map(_.fullPrice).toJSArray + chart.update() + } +}, +{% endhighlight %} + +In the callback, we get access to the `chart: Chart` instance and update its data model. +This `-->` binder allows to bridge the FRP world of `dataSignal` with the imperative world of Chart.js. + +Our application now properly renders the data model as a chart. +When we add or remove data items, the chart is automatically updated, thanks to the connection established by the `dataSignal -->` binder. + +## Conclusion + +That concludes our tutorial on ScalablyTyped. + +We saw how to configure ScalablyTyped to get static types for external JavaScript libraries, and how to integrate a third-party component into a Laminar model. diff --git a/doc/tutorial/scalajs-vite.md b/doc/tutorial/scalajs-vite.md new file mode 100644 index 00000000..daa91513 --- /dev/null +++ b/doc/tutorial/scalajs-vite.md @@ -0,0 +1,359 @@ +--- +layout: doc +title: Getting Started with Scala.js and Vite +--- + +In this first tutorial, we learn how to get started with Scala.js and [Vite](https://vitejs.dev/). +We use Vite to provide live-reloading of the Scala.js application in the browser for development. +We also configure it to build a minimal bundle for production. + +Going through this tutorial will make sure you understand the basic building blocks. +If you prefer to skip this step and directly write Scala.js code, you may jump to [Getting Started with Scala.js and Laminar](./laminar-scalablytyped.html). + +If you prefer to look at the end result for this tutorial directly, checkout [the scalajs-vite-end-state branch](https://github.com/sjrd/scalajs-sbt-vite-laminar-chartjs-example/tree/scalajs-vite-end-state) instead of creating everything from scratch. + +## Prerequisites + +Make sure to install [the prerequisites](./index.html#prerequisites) before continuing further. + +## Vite template + +We bootstrap our setup using the vanilla Vite template. +Navigate to a directory where you store projects, and run the command + +{% highlight shell %} +$ npm create vite@4.1.0 +{% endhighlight %} + +Choose a project name (we choose `livechart`). +Select the "Vanilla" framework and the "JavaScript" variant. +Our output gives: + +{% highlight shell %} +$ npm create vite@4.1.0 +Need to install the following packages: + create-vite@4.1.0 +Ok to proceed? (y) +✔ Project name: … livechart +✔ Select a framework: › Vanilla +✔ Select a variant: › JavaScript + +Scaffolding project in .../livechart... + +Done. Now run: + + cd livechart + npm install + npm run dev +{% endhighlight %} + +As instructed, we follow up with + +{% highlight shell %} +$ cd livechart +$ npm install +[...] +$ npm run dev + + VITE v4.1.4 ready in 156 ms + + ➜ Local: http://localhost:5173/ + ➜ Network: use --host to expose + ➜ press h to show help +{% endhighlight %} + +Open the provided URL to see the running JavaScript-based hello world. + +### Exploring the template + +In the generated folder, we find the following relevant files: + +* `index.html`: the main web page; it contains a `