diff --git a/.stylelintrc b/.stylelintrc index 2fea87127..395ee8ba5 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -103,6 +103,7 @@ "dppx", "deg", "em", + "fr", "mm", "ms", "px", diff --git a/docs/schema/theme.json b/docs/schema/theme.json index 12400030a..ea9af64c0 100644 --- a/docs/schema/theme.json +++ b/docs/schema/theme.json @@ -834,20 +834,6 @@ } } ] - }, - "include_search_page": { - "title": "Only necessary when installing from git", - "markdownDescription": "Must be set to `false`", - "enum": [ - false - ] - }, - "search_index_only": { - "title": "Only necessary when installing from git", - "markdownDescription": "Must be set to `false`", - "enum": [ - true - ] } }, "additionalProperties": false, diff --git a/material/.overrides/assets/javascripts/custom.83b17dfb.min.js b/material/.overrides/assets/javascripts/custom.147554b9.min.js similarity index 72% rename from material/.overrides/assets/javascripts/custom.83b17dfb.min.js rename to material/.overrides/assets/javascripts/custom.147554b9.min.js index 9305e2121..b71806943 100644 --- a/material/.overrides/assets/javascripts/custom.83b17dfb.min.js +++ b/material/.overrides/assets/javascripts/custom.147554b9.min.js @@ -1,4 +1,4 @@ -"use strict";(()=>{var Zn=Object.create;var Fr=Object.defineProperty;var eo=Object.getOwnPropertyDescriptor;var ro=Object.getOwnPropertyNames,ot=Object.getOwnPropertySymbols,to=Object.getPrototypeOf,at=Object.prototype.hasOwnProperty,no=Object.prototype.propertyIsEnumerable;var it=(e,r,t)=>r in e?Fr(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t,Qe=(e,r)=>{for(var t in r||(r={}))at.call(r,t)&&it(e,t,r[t]);if(ot)for(var t of ot(r))no.call(r,t)&&it(e,t,r[t]);return e};var ve=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports);var oo=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of ro(r))!at.call(e,o)&&o!==t&&Fr(e,o,{get:()=>r[o],enumerable:!(n=eo(r,o))||n.enumerable});return e};var kr=(e,r,t)=>(t=e!=null?Zn(to(e)):{},oo(r||!e||!e.__esModule?Fr(t,"default",{value:e,enumerable:!0}):t,e));var At=ve((ui,Je)=>{/*! ***************************************************************************** +"use strict";(()=>{var Zn=Object.create;var kr=Object.defineProperty;var eo=Object.getOwnPropertyDescriptor;var ro=Object.getOwnPropertyNames,ot=Object.getOwnPropertySymbols,to=Object.getPrototypeOf,at=Object.prototype.hasOwnProperty,no=Object.prototype.propertyIsEnumerable;var it=(e,r,t)=>r in e?kr(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t,Qe=(e,r)=>{for(var t in r||(r={}))at.call(r,t)&&it(e,t,r[t]);if(ot)for(var t of ot(r))no.call(r,t)&&it(e,t,r[t]);return e};var ve=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports);var oo=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of ro(r))!at.call(e,o)&&o!==t&&kr(e,o,{get:()=>r[o],enumerable:!(n=eo(r,o))||n.enumerable});return e};var Fr=(e,r,t)=>(t=e!=null?Zn(to(e)):{},oo(r||!e||!e.__esModule?kr(t,"default",{value:e,enumerable:!0}):t,e));var At=ve((ui,Je)=>{/*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any @@ -11,8 +11,8 @@ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -***************************************************************************** */var st,ut,ft,ct,lt,pt,mt,ht,dt,Ke,Hr,vt,bt,yt,Oe,xt,gt,wt,_t,St,Et,Ot,Tt,Ye;(function(e){var r=typeof global=="object"?global:typeof self=="object"?self:typeof this=="object"?this:{};typeof define=="function"&&define.amd?define("tslib",["exports"],function(n){e(t(r,t(n)))}):typeof Je=="object"&&typeof Je.exports=="object"?e(t(r,t(Je.exports))):e(t(r));function t(n,o){return n!==r&&(typeof Object.create=="function"?Object.defineProperty(n,"__esModule",{value:!0}):n.__esModule=!0),function(i,a){return n[i]=o?o(i,a):a}}})(function(e){var r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(n,o){n.__proto__=o}||function(n,o){for(var i in o)Object.prototype.hasOwnProperty.call(o,i)&&(n[i]=o[i])};st=function(n,o){if(typeof o!="function"&&o!==null)throw new TypeError("Class extends value "+String(o)+" is not a constructor or null");r(n,o);function i(){this.constructor=n}n.prototype=o===null?Object.create(o):(i.prototype=o.prototype,new i)},ut=Object.assign||function(n){for(var o,i=1,a=arguments.length;i=0;c--)(f=n[c])&&(s=(u<3?f(s):u>3?f(o,i,s):f(o,i))||s);return u>3&&s&&Object.defineProperty(o,i,s),s},lt=function(n,o){return function(i,a){o(i,a,n)}},pt=function(n,o){if(typeof Reflect=="object"&&typeof Reflect.metadata=="function")return Reflect.metadata(n,o)},mt=function(n,o,i,a){function u(s){return s instanceof i?s:new i(function(f){f(s)})}return new(i||(i=Promise))(function(s,f){function c(h){try{l(a.next(h))}catch(d){f(d)}}function p(h){try{l(a.throw(h))}catch(d){f(d)}}function l(h){h.done?s(h.value):u(h.value).then(c,p)}l((a=a.apply(n,o||[])).next())})},ht=function(n,o){var i={label:0,sent:function(){if(s[0]&1)throw s[1];return s[1]},trys:[],ops:[]},a,u,s,f;return f={next:c(0),throw:c(1),return:c(2)},typeof Symbol=="function"&&(f[Symbol.iterator]=function(){return this}),f;function c(l){return function(h){return p([l,h])}}function p(l){if(a)throw new TypeError("Generator is already executing.");for(;i;)try{if(a=1,u&&(s=l[0]&2?u.return:l[0]?u.throw||((s=u.return)&&s.call(u),0):u.next)&&!(s=s.call(u,l[1])).done)return s;switch(u=0,s&&(l=[l[0]&2,s.value]),l[0]){case 0:case 1:s=l;break;case 4:return i.label++,{value:l[1],done:!1};case 5:i.label++,u=l[1],l=[0];continue;case 7:l=i.ops.pop(),i.trys.pop();continue;default:if(s=i.trys,!(s=s.length>0&&s[s.length-1])&&(l[0]===6||l[0]===2)){i=0;continue}if(l[0]===3&&(!s||l[1]>s[0]&&l[1]=n.length&&(n=void 0),{value:n&&n[a++],done:!n}}};throw new TypeError(o?"Object is not iterable.":"Symbol.iterator is not defined.")},Hr=function(n,o){var i=typeof Symbol=="function"&&n[Symbol.iterator];if(!i)return n;var a=i.call(n),u,s=[],f;try{for(;(o===void 0||o-- >0)&&!(u=a.next()).done;)s.push(u.value)}catch(c){f={error:c}}finally{try{u&&!u.done&&(i=a.return)&&i.call(a)}finally{if(f)throw f.error}}return s},vt=function(){for(var n=[],o=0;o1||c(m,b)})})}function c(m,b){try{p(a[m](b))}catch(w){d(s[0][3],w)}}function p(m){m.value instanceof Oe?Promise.resolve(m.value.v).then(l,h):d(s[0][2],m)}function l(m){c("next",m)}function h(m){c("throw",m)}function d(m,b){m(b),s.shift(),s.length&&c(s[0][0],s[0][1])}},gt=function(n){var o,i;return o={},a("next"),a("throw",function(u){throw u}),a("return"),o[Symbol.iterator]=function(){return this},o;function a(u,s){o[u]=n[u]?function(f){return(i=!i)?{value:Oe(n[u](f)),done:u==="return"}:s?s(f):f}:s}},wt=function(n){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var o=n[Symbol.asyncIterator],i;return o?o.call(n):(n=typeof Ke=="function"?Ke(n):n[Symbol.iterator](),i={},a("next"),a("throw"),a("return"),i[Symbol.asyncIterator]=function(){return this},i);function a(s){i[s]=n[s]&&function(f){return new Promise(function(c,p){f=n[s](f),u(c,p,f.done,f.value)})}}function u(s,f,c,p){Promise.resolve(p).then(function(l){s({value:l,done:c})},f)}},_t=function(n,o){return Object.defineProperty?Object.defineProperty(n,"raw",{value:o}):n.raw=o,n};var t=Object.create?function(n,o){Object.defineProperty(n,"default",{enumerable:!0,value:o})}:function(n,o){n.default=o};St=function(n){if(n&&n.__esModule)return n;var o={};if(n!=null)for(var i in n)i!=="default"&&Object.prototype.hasOwnProperty.call(n,i)&&Ye(o,n,i);return t(o,n),o},Et=function(n){return n&&n.__esModule?n:{default:n}},Ot=function(n,o,i,a){if(i==="a"&&!a)throw new TypeError("Private accessor was defined without a getter");if(typeof o=="function"?n!==o||!a:!o.has(n))throw new TypeError("Cannot read private member from an object whose class did not declare it");return i==="m"?a:i==="a"?a.call(n):a?a.value:o.get(n)},Tt=function(n,o,i,a,u){if(a==="m")throw new TypeError("Private method is not writable");if(a==="a"&&!u)throw new TypeError("Private accessor was defined without a setter");if(typeof o=="function"?n!==o||!u:!o.has(n))throw new TypeError("Cannot write private member to an object whose class did not declare it");return a==="a"?u.call(n,i):u?u.value=i:o.set(n,i),i},e("__extends",st),e("__assign",ut),e("__rest",ft),e("__decorate",ct),e("__param",lt),e("__metadata",pt),e("__awaiter",mt),e("__generator",ht),e("__exportStar",dt),e("__createBinding",Ye),e("__values",Ke),e("__read",Hr),e("__spread",vt),e("__spreadArrays",bt),e("__spreadArray",yt),e("__await",Oe),e("__asyncGenerator",xt),e("__asyncDelegator",gt),e("__asyncValues",wt),e("__makeTemplateObject",_t),e("__importStar",St),e("__importDefault",Et),e("__classPrivateFieldGet",Ot),e("__classPrivateFieldSet",Tt)})});var $e=ve(Y=>{(function(){var e,r,t,n,o,i,a,u,s,f,c,p,l,h,d,m,b,w,S,R;R=150,f=20,S=150,s=.75,Y.score=function(v,y,_){var O,x,g,T;return x=_.preparedQuery,O=_.allowErrors,O||o(v,x.core_lw,x.core_up)?(T=v.toLowerCase(),g=r(v,T,x),Math.ceil(g)):0},Y.isMatch=o=function(v,y,_){var O,x,g,T,P,z,C;if(g=v.length,T=y.length,!g||T>g)return!1;for(O=-1,x=-1;++x-1)return d(v,y,me,oe,se,D,q);for(Ee=new Array(D),P=new Array(D),Pr=w(D,q),j=Math.ceil(s*D)+5,ne=j,C=!0,I=-1;++Iue&&(ue=Ge),z=0,oe[I]===Lr)if(Rr=u(H,v,y),z=T>0?T:l(v,y,me,oe,H,I,Rr),g=Cr+p(H,I,Rr,x,z),g>ue)ue=g,ne=j;else{if(de&&--ne<=0)return Math.max(ue,Ee[D-1])*Pr;de=!1}Cr=Ge,T=P[I],P[I]=z,Ee[I]=ue}}return ue=Ee[D-1],ue*Pr},Y.isWordStart=u=function(v,y,_){var O,x;return v===0?!0:(O=y[v],x=y[v-1],i(x)||O!==_[v]&&x===_[v-1])},Y.isWordEnd=a=function(v,y,_,O){var x,g;return v===O-1?!0:(x=y[v],g=y[v+1],i(g)||x===_[v]&&g!==_[v+1])},i=function(v){return v===" "||v==="."||v==="-"||v==="_"||v==="/"||v==="\\"},b=function(v){var y;return vx?O:x)+10):g+R*x},Y.scoreConsecutives=l=function(v,y,_,O,x,g,T){var P,z,C,H,I,q,j;for(z=v.length,H=_.length,C=z-x,I=H-g,P=C-1&&(I=u(C,v,y),I&&(x=C))),z=-1,H=0;++z1&&C>1))return t;for(x=0,j=0,ne=0,I=0,T=-1,P=-1;++P-1){j++;continue}else break;for(;++T12*P)return!1;for(g=-1;++gO)return!1;return!0}}).call(Y)});var Mr=ve(Ne=>{(function(){var e,r,t,n,o,i,a,u,s,f;f=$e(),i=f.isMatch,e=f.computeScore,u=f.scoreSize,s=20,t=2.5,Ne.score=function(c,p,l){var h,d,m,b;return d=l.preparedQuery,h=l.allowErrors,h||i(c,d.core_lw,d.core_up)?(b=c.toLowerCase(),m=e(c,b,d),m=a(c,b,m,l),Math.ceil(m)):0},a=function(c,p,l,h){var d,m,b,w,S,R,v,y,_,O;if(l===0)return 0;for(_=h.preparedQuery,O=h.useExtensionBonus,y=h.pathSeparator,S=c.length-1;c[S]===y;)S--;if(b=c.lastIndexOf(y,S),v=S-b,R=1,O&&(R+=o(p,_.ext,b,S,2),l*=R),b===-1)return l;for(w=_.depth;b>-1&&w-- >0;)b=c.lastIndexOf(y,b-1);return m=b===-1?l:R*e(c.slice(b+1,S+1),p.slice(b+1,S+1),_),d=.5*s/(s+r(c,S+1,y)),d*m+(1-d)*l*u(0,t*v)},Ne.countDir=r=function(c,p,l){var h,d;if(p<1)return 0;for(h=0,d=-1;++dl)))return 0;for(w=p.length,m=h-S,m0?.9*o(c,p,l,S-2,d-1):b/m}}).call(Ne)});var rt=ve((Hn,Wn)=>{(function(){var e,r,t,n,o,i,a,u;u=Mr(),t=u.countDir,o=u.getExtension,Wn.exports=e=function(){function s(f,c){var p,l,h;if(h=c!=null?c:{},p=h.optCharRegEx,l=h.pathSeparator,!(f&&f.length))return null;this.query=f,this.query_lw=f.toLowerCase(),this.core=r(f,p),this.core_lw=this.core.toLowerCase(),this.core_up=a(this.core),this.depth=t(f,f.length,l),this.ext=o(this.query_lw),this.charCodes=n(this.query_lw)}return s}(),i=/[ _\-:\/\\]/g,r=function(s,f){return f==null&&(f=i),s.replace(f,"")},a=function(s){var f,c,p,l;for(c="",p=0,l=s.length;p{(function(){var e,r,t,n,o;n=$e(),r=Mr(),e=rt(),t=function(i){return i.candidate},o=function(i,a){return a.score-i.score},Un.exports=function(i,a,u){var s,f,c,p,l,h,d,m,b,w,S,R,v;for(m=[],c=u.key,l=u.maxResults,p=u.maxInners,S=u.usePathScoring,b=p!=null&&p>0?p:i.length+1,s=c!=null,d=S?r:n,R=0,v=i.length;R0&&(m.push({candidate:f,score:h}),!--b))));R++);return m.sort(o),i=m.map(t),l!=null&&(i=i.slice(0,l)),i}}).call(zn)});var Vn=ve(Ir=>{(function(){var e,r,t,n,o,i,a,u,s,f;f=$e(),t=f.isMatch,n=f.isWordStart,s=f.scoreConsecutives,u=f.scoreCharacter,a=f.scoreAcronyms,Ir.match=o=function(c,p,l){var h,d,m,b,w,S;return h=l.allowErrors,w=l.preparedQuery,b=l.pathSeparator,h||t(c,w.core_lw,w.core_up)?(S=c.toLowerCase(),m=r(c,S,w),m.length===0||c.indexOf(b)>-1&&(d=e(c,S,w,b),m=i(m,d)),m):[]},Ir.wrap=function(c,p,l){var h,d,m,b,w,S,R,v,y;if(l.wrap!=null&&(y=l.wrap,S=y.tagClass,v=y.tagOpen,R=y.tagClose),S==null&&(S="highlight"),v==null&&(v=''),R==null&&(R=""),c===p)return v+c+R;if(m=o(c,p,l),m.length===0)return c;for(b="",h=-1,w=0;++hw&&(b+=c.substring(w,d),w=d);++hw&&(b+=v,b+=c.substring(w,d),b+=R,w=d)}return w<=c.length-1&&(b+=c.substring(w)),b},e=function(c,p,l,h){var d,m,b;for(b=c.length-1;c[b]===h;)b--;if(d=c.lastIndexOf(h,b),d===-1)return[];for(m=l.depth;m-- >0;)if(d=c.lastIndexOf(h,d-1),d===-1)return[];return d++,b++,r(c.slice(d,b),p.slice(d,b),l,d)},i=function(c,p){var l,h,d,m,b,w,S;if(b=c.length,w=p.length,w===0)return c.slice();if(b===0)return p.slice();for(d=-1,m=0,h=p[m],S=[];++d0?y:s(c,p,I,q,x,g,oe),R=ne+u(x,g,oe,S,O)),se=D[g],y=_[g],j>se?z=m:(j=se,z=w),R>j?(j=R,z=d):O=0,D[g]=j,_[g]=O,de[++H]=j>0?z:b;for(x=T-1,g=C-1,H=x*C+g,v=!0,P=[];v&&x>=0&&g>=0;)switch(de[H]){case w:x--,H-=C;break;case m:g--,H--;break;case d:P.push(x+h),g--,x--,H-=C+1;break;default:v=!1}return P.reverse(),P}}).call(Ir)});var tt=ve((qn,Dn)=>{(function(){var e,r,t,n,o,i,a,u;t=jn(),n=Vn(),u=$e(),i=Mr(),e=rt(),a=null,r=(typeof process!="undefined"&&process!==null?process.platform:void 0)==="win32"?"\\":"/",Dn.exports={filter:function(s,f,c){return c==null&&(c={}),(f!=null?f.length:void 0)&&(s!=null?s.length:void 0)?(c=o(c,f),t(s,f,c)):[]},score:function(s,f,c){return c==null&&(c={}),(s!=null?s.length:void 0)&&(f!=null?f.length:void 0)?(c=o(c,f),c.usePathScoring?i.score(s,f,c):u.score(s,f,c)):0},match:function(s,f,c){var p,l,h;return c==null&&(c={}),s?f?s===f?function(){h=[];for(var d=0,m=s.length;0<=m?dm;0<=m?d++:d--)h.push(d);return h}.apply(this):(c=o(c,f),n.match(s,f,c)):[]:[]},wrap:function(s,f,c){return c==null&&(c={}),s?f?(c=o(c,f),n.wrap(s,f,c)):[]:[]},prepareQuery:function(s,f){return f==null&&(f={}),f=o(f,s),f.preparedQuery}},o=function(s,f){return s.allowErrors==null&&(s.allowErrors=!1),s.usePathScoring==null&&(s.usePathScoring=!0),s.useExtensionBonus==null&&(s.useExtensionBonus=!1),s.pathSeparator==null&&(s.pathSeparator=r),s.optCharRegEx==null&&(s.optCharRegEx=null),s.wrap==null&&(s.wrap=null),s.preparedQuery==null&&(s.preparedQuery=a&&a.query===f?a:a=new e(f,s)),s}}).call(qn)});var Mt=kr(At(),1),{__extends:V,__assign:fi,__rest:ci,__decorate:li,__param:pi,__metadata:mi,__awaiter:It,__generator:Be,__exportStar:hi,__createBinding:di,__values:re,__read:W,__spread:vi,__spreadArrays:bi,__spreadArray:U,__await:Xe,__asyncGenerator:Ct,__asyncDelegator:yi,__asyncValues:Lt,__makeTemplateObject:xi,__importStar:gi,__importDefault:wi,__classPrivateFieldGet:_i,__classPrivateFieldSet:Si}=Mt.default;function E(e){return typeof e=="function"}function Ze(e){var r=function(n){Error.call(n),n.stack=new Error().stack},t=e(r);return t.prototype=Object.create(Error.prototype),t.prototype.constructor=t,t}var er=Ze(function(e){return function(t){e(this),this.message=t?t.length+` errors occurred during unsubscription: +***************************************************************************** */var st,ut,ft,ct,lt,pt,mt,ht,dt,Ke,Hr,vt,bt,yt,Oe,xt,gt,wt,_t,Et,St,Ot,Tt,Ye;(function(e){var r=typeof global=="object"?global:typeof self=="object"?self:typeof this=="object"?this:{};typeof define=="function"&&define.amd?define("tslib",["exports"],function(n){e(t(r,t(n)))}):typeof Je=="object"&&typeof Je.exports=="object"?e(t(r,t(Je.exports))):e(t(r));function t(n,o){return n!==r&&(typeof Object.create=="function"?Object.defineProperty(n,"__esModule",{value:!0}):n.__esModule=!0),function(i,a){return n[i]=o?o(i,a):a}}})(function(e){var r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(n,o){n.__proto__=o}||function(n,o){for(var i in o)Object.prototype.hasOwnProperty.call(o,i)&&(n[i]=o[i])};st=function(n,o){if(typeof o!="function"&&o!==null)throw new TypeError("Class extends value "+String(o)+" is not a constructor or null");r(n,o);function i(){this.constructor=n}n.prototype=o===null?Object.create(o):(i.prototype=o.prototype,new i)},ut=Object.assign||function(n){for(var o,i=1,a=arguments.length;i=0;c--)(f=n[c])&&(s=(u<3?f(s):u>3?f(o,i,s):f(o,i))||s);return u>3&&s&&Object.defineProperty(o,i,s),s},lt=function(n,o){return function(i,a){o(i,a,n)}},pt=function(n,o){if(typeof Reflect=="object"&&typeof Reflect.metadata=="function")return Reflect.metadata(n,o)},mt=function(n,o,i,a){function u(s){return s instanceof i?s:new i(function(f){f(s)})}return new(i||(i=Promise))(function(s,f){function c(h){try{l(a.next(h))}catch(d){f(d)}}function p(h){try{l(a.throw(h))}catch(d){f(d)}}function l(h){h.done?s(h.value):u(h.value).then(c,p)}l((a=a.apply(n,o||[])).next())})},ht=function(n,o){var i={label:0,sent:function(){if(s[0]&1)throw s[1];return s[1]},trys:[],ops:[]},a,u,s,f;return f={next:c(0),throw:c(1),return:c(2)},typeof Symbol=="function"&&(f[Symbol.iterator]=function(){return this}),f;function c(l){return function(h){return p([l,h])}}function p(l){if(a)throw new TypeError("Generator is already executing.");for(;i;)try{if(a=1,u&&(s=l[0]&2?u.return:l[0]?u.throw||((s=u.return)&&s.call(u),0):u.next)&&!(s=s.call(u,l[1])).done)return s;switch(u=0,s&&(l=[l[0]&2,s.value]),l[0]){case 0:case 1:s=l;break;case 4:return i.label++,{value:l[1],done:!1};case 5:i.label++,u=l[1],l=[0];continue;case 7:l=i.ops.pop(),i.trys.pop();continue;default:if(s=i.trys,!(s=s.length>0&&s[s.length-1])&&(l[0]===6||l[0]===2)){i=0;continue}if(l[0]===3&&(!s||l[1]>s[0]&&l[1]=n.length&&(n=void 0),{value:n&&n[a++],done:!n}}};throw new TypeError(o?"Object is not iterable.":"Symbol.iterator is not defined.")},Hr=function(n,o){var i=typeof Symbol=="function"&&n[Symbol.iterator];if(!i)return n;var a=i.call(n),u,s=[],f;try{for(;(o===void 0||o-- >0)&&!(u=a.next()).done;)s.push(u.value)}catch(c){f={error:c}}finally{try{u&&!u.done&&(i=a.return)&&i.call(a)}finally{if(f)throw f.error}}return s},vt=function(){for(var n=[],o=0;o1||c(m,b)})})}function c(m,b){try{p(a[m](b))}catch(w){d(s[0][3],w)}}function p(m){m.value instanceof Oe?Promise.resolve(m.value.v).then(l,h):d(s[0][2],m)}function l(m){c("next",m)}function h(m){c("throw",m)}function d(m,b){m(b),s.shift(),s.length&&c(s[0][0],s[0][1])}},gt=function(n){var o,i;return o={},a("next"),a("throw",function(u){throw u}),a("return"),o[Symbol.iterator]=function(){return this},o;function a(u,s){o[u]=n[u]?function(f){return(i=!i)?{value:Oe(n[u](f)),done:u==="return"}:s?s(f):f}:s}},wt=function(n){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var o=n[Symbol.asyncIterator],i;return o?o.call(n):(n=typeof Ke=="function"?Ke(n):n[Symbol.iterator](),i={},a("next"),a("throw"),a("return"),i[Symbol.asyncIterator]=function(){return this},i);function a(s){i[s]=n[s]&&function(f){return new Promise(function(c,p){f=n[s](f),u(c,p,f.done,f.value)})}}function u(s,f,c,p){Promise.resolve(p).then(function(l){s({value:l,done:c})},f)}},_t=function(n,o){return Object.defineProperty?Object.defineProperty(n,"raw",{value:o}):n.raw=o,n};var t=Object.create?function(n,o){Object.defineProperty(n,"default",{enumerable:!0,value:o})}:function(n,o){n.default=o};Et=function(n){if(n&&n.__esModule)return n;var o={};if(n!=null)for(var i in n)i!=="default"&&Object.prototype.hasOwnProperty.call(n,i)&&Ye(o,n,i);return t(o,n),o},St=function(n){return n&&n.__esModule?n:{default:n}},Ot=function(n,o,i,a){if(i==="a"&&!a)throw new TypeError("Private accessor was defined without a getter");if(typeof o=="function"?n!==o||!a:!o.has(n))throw new TypeError("Cannot read private member from an object whose class did not declare it");return i==="m"?a:i==="a"?a.call(n):a?a.value:o.get(n)},Tt=function(n,o,i,a,u){if(a==="m")throw new TypeError("Private method is not writable");if(a==="a"&&!u)throw new TypeError("Private accessor was defined without a setter");if(typeof o=="function"?n!==o||!u:!o.has(n))throw new TypeError("Cannot write private member to an object whose class did not declare it");return a==="a"?u.call(n,i):u?u.value=i:o.set(n,i),i},e("__extends",st),e("__assign",ut),e("__rest",ft),e("__decorate",ct),e("__param",lt),e("__metadata",pt),e("__awaiter",mt),e("__generator",ht),e("__exportStar",dt),e("__createBinding",Ye),e("__values",Ke),e("__read",Hr),e("__spread",vt),e("__spreadArrays",bt),e("__spreadArray",yt),e("__await",Oe),e("__asyncGenerator",xt),e("__asyncDelegator",gt),e("__asyncValues",wt),e("__makeTemplateObject",_t),e("__importStar",Et),e("__importDefault",St),e("__classPrivateFieldGet",Ot),e("__classPrivateFieldSet",Tt)})});var De=ve(Y=>{(function(){var e,r,t,n,o,i,a,u,s,f,c,p,l,h,d,m,b,w,E,P;P=150,f=20,E=150,s=.75,Y.score=function(v,y,_){var O,x,g,T;return x=_.preparedQuery,O=_.allowErrors,O||o(v,x.core_lw,x.core_up)?(T=v.toLowerCase(),g=r(v,T,x),Math.ceil(g)):0},Y.isMatch=o=function(v,y,_){var O,x,g,T,R,z,C;if(g=v.length,T=y.length,!g||T>g)return!1;for(O=-1,x=-1;++x-1)return d(v,y,me,oe,se,q,$);for(Se=new Array(q),R=new Array(q),Rr=w(q,$),j=Math.ceil(s*q)+5,ne=j,C=!0,I=-1;++Iue&&(ue=Ge),z=0,oe[I]===Lr)if(Pr=u(H,v,y),z=T>0?T:l(v,y,me,oe,H,I,Pr),g=Cr+p(H,I,Pr,x,z),g>ue)ue=g,ne=j;else{if(de&&--ne<=0)return Math.max(ue,Se[q-1])*Rr;de=!1}Cr=Ge,T=R[I],R[I]=z,Se[I]=ue}}return ue=Se[q-1],ue*Rr},Y.isWordStart=u=function(v,y,_){var O,x;return v===0?!0:(O=y[v],x=y[v-1],i(x)||O!==_[v]&&x===_[v-1])},Y.isWordEnd=a=function(v,y,_,O){var x,g;return v===O-1?!0:(x=y[v],g=y[v+1],i(g)||x===_[v]&&g!==_[v+1])},i=function(v){return v===" "||v==="."||v==="-"||v==="_"||v==="/"||v==="\\"},b=function(v){var y;return vx?O:x)+10):g+P*x},Y.scoreConsecutives=l=function(v,y,_,O,x,g,T){var R,z,C,H,I,$,j;for(z=v.length,H=_.length,C=z-x,I=H-g,R=C-1&&(I=u(C,v,y),I&&(x=C))),z=-1,H=0;++z1&&C>1))return t;for(x=0,j=0,ne=0,I=0,T=-1,R=-1;++R-1){j++;continue}else break;for(;++T12*R)return!1;for(g=-1;++gO)return!1;return!0}}).call(Y)});var Mr=ve(Ne=>{(function(){var e,r,t,n,o,i,a,u,s,f;f=De(),i=f.isMatch,e=f.computeScore,u=f.scoreSize,s=20,t=2.5,Ne.score=function(c,p,l){var h,d,m,b;return d=l.preparedQuery,h=l.allowErrors,h||i(c,d.core_lw,d.core_up)?(b=c.toLowerCase(),m=e(c,b,d),m=a(c,b,m,l),Math.ceil(m)):0},a=function(c,p,l,h){var d,m,b,w,E,P,v,y,_,O;if(l===0)return 0;for(_=h.preparedQuery,O=h.useExtensionBonus,y=h.pathSeparator,E=c.length-1;c[E]===y;)E--;if(b=c.lastIndexOf(y,E),v=E-b,P=1,O&&(P+=o(p,_.ext,b,E,2),l*=P),b===-1)return l;for(w=_.depth;b>-1&&w-- >0;)b=c.lastIndexOf(y,b-1);return m=b===-1?l:P*e(c.slice(b+1,E+1),p.slice(b+1,E+1),_),d=.5*s/(s+r(c,E+1,y)),d*m+(1-d)*l*u(0,t*v)},Ne.countDir=r=function(c,p,l){var h,d;if(p<1)return 0;for(h=0,d=-1;++dl)))return 0;for(w=p.length,m=h-E,m0?.9*o(c,p,l,E-2,d-1):b/m}}).call(Ne)});var rt=ve((Hn,Wn)=>{(function(){var e,r,t,n,o,i,a,u;u=Mr(),t=u.countDir,o=u.getExtension,Wn.exports=e=function(){function s(f,c){var p,l,h;if(h=c!=null?c:{},p=h.optCharRegEx,l=h.pathSeparator,!(f&&f.length))return null;this.query=f,this.query_lw=f.toLowerCase(),this.core=r(f,p),this.core_lw=this.core.toLowerCase(),this.core_up=a(this.core),this.depth=t(f,f.length,l),this.ext=o(this.query_lw),this.charCodes=n(this.query_lw)}return s}(),i=/[ _\-:\/\\]/g,r=function(s,f){return f==null&&(f=i),s.replace(f,"")},a=function(s){var f,c,p,l;for(c="",p=0,l=s.length;p{(function(){var e,r,t,n,o;n=De(),r=Mr(),e=rt(),t=function(i){return i.candidate},o=function(i,a){return a.score-i.score},Un.exports=function(i,a,u){var s,f,c,p,l,h,d,m,b,w,E,P,v;for(m=[],c=u.key,l=u.maxResults,p=u.maxInners,E=u.usePathScoring,b=p!=null&&p>0?p:i.length+1,s=c!=null,d=E?r:n,P=0,v=i.length;P0&&(m.push({candidate:f,score:h}),!--b))));P++);return m.sort(o),i=m.map(t),l!=null&&(i=i.slice(0,l)),i}}).call(zn)});var Vn=ve(Ir=>{(function(){var e,r,t,n,o,i,a,u,s,f;f=De(),t=f.isMatch,n=f.isWordStart,s=f.scoreConsecutives,u=f.scoreCharacter,a=f.scoreAcronyms,Ir.match=o=function(c,p,l){var h,d,m,b,w,E;return h=l.allowErrors,w=l.preparedQuery,b=l.pathSeparator,h||t(c,w.core_lw,w.core_up)?(E=c.toLowerCase(),m=r(c,E,w),m.length===0||c.indexOf(b)>-1&&(d=e(c,E,w,b),m=i(m,d)),m):[]},Ir.wrap=function(c,p,l){var h,d,m,b,w,E,P,v,y;if(l.wrap!=null&&(y=l.wrap,E=y.tagClass,v=y.tagOpen,P=y.tagClose),E==null&&(E="highlight"),v==null&&(v=''),P==null&&(P=""),c===p)return v+c+P;if(m=o(c,p,l),m.length===0)return c;for(b="",h=-1,w=0;++hw&&(b+=c.substring(w,d),w=d);++hw&&(b+=v,b+=c.substring(w,d),b+=P,w=d)}return w<=c.length-1&&(b+=c.substring(w)),b},e=function(c,p,l,h){var d,m,b;for(b=c.length-1;c[b]===h;)b--;if(d=c.lastIndexOf(h,b),d===-1)return[];for(m=l.depth;m-- >0;)if(d=c.lastIndexOf(h,d-1),d===-1)return[];return d++,b++,r(c.slice(d,b),p.slice(d,b),l,d)},i=function(c,p){var l,h,d,m,b,w,E;if(b=c.length,w=p.length,w===0)return c.slice();if(b===0)return p.slice();for(d=-1,m=0,h=p[m],E=[];++d0?y:s(c,p,I,$,x,g,oe),P=ne+u(x,g,oe,E,O)),se=q[g],y=_[g],j>se?z=m:(j=se,z=w),P>j?(j=P,z=d):O=0,q[g]=j,_[g]=O,de[++H]=j>0?z:b;for(x=T-1,g=C-1,H=x*C+g,v=!0,R=[];v&&x>=0&&g>=0;)switch(de[H]){case w:x--,H-=C;break;case m:g--,H--;break;case d:R.push(x+h),g--,x--,H-=C+1;break;default:v=!1}return R.reverse(),R}}).call(Ir)});var tt=ve(($n,qn)=>{(function(){var e,r,t,n,o,i,a,u;t=jn(),n=Vn(),u=De(),i=Mr(),e=rt(),a=null,r=(typeof process!="undefined"&&process!==null?process.platform:void 0)==="win32"?"\\":"/",qn.exports={filter:function(s,f,c){return c==null&&(c={}),(f!=null?f.length:void 0)&&(s!=null?s.length:void 0)?(c=o(c,f),t(s,f,c)):[]},score:function(s,f,c){return c==null&&(c={}),(s!=null?s.length:void 0)&&(f!=null?f.length:void 0)?(c=o(c,f),c.usePathScoring?i.score(s,f,c):u.score(s,f,c)):0},match:function(s,f,c){var p,l,h;return c==null&&(c={}),s?f?s===f?function(){h=[];for(var d=0,m=s.length;0<=m?dm;0<=m?d++:d--)h.push(d);return h}.apply(this):(c=o(c,f),n.match(s,f,c)):[]:[]},wrap:function(s,f,c){return c==null&&(c={}),s?f?(c=o(c,f),n.wrap(s,f,c)):[]:[]},prepareQuery:function(s,f){return f==null&&(f={}),f=o(f,s),f.preparedQuery}},o=function(s,f){return s.allowErrors==null&&(s.allowErrors=!1),s.usePathScoring==null&&(s.usePathScoring=!0),s.useExtensionBonus==null&&(s.useExtensionBonus=!1),s.pathSeparator==null&&(s.pathSeparator=r),s.optCharRegEx==null&&(s.optCharRegEx=null),s.wrap==null&&(s.wrap=null),s.preparedQuery==null&&(s.preparedQuery=a&&a.query===f?a:a=new e(f,s)),s}}).call($n)});var Mt=Fr(At(),1),{__extends:V,__assign:fi,__rest:ci,__decorate:li,__param:pi,__metadata:mi,__awaiter:It,__generator:Be,__exportStar:hi,__createBinding:di,__values:re,__read:W,__spread:vi,__spreadArrays:bi,__spreadArray:U,__await:Xe,__asyncGenerator:Ct,__asyncDelegator:yi,__asyncValues:Lt,__makeTemplateObject:xi,__importStar:gi,__importDefault:wi,__classPrivateFieldGet:_i,__classPrivateFieldSet:Ei}=Mt.default;function S(e){return typeof e=="function"}function Ze(e){var r=function(n){Error.call(n),n.stack=new Error().stack},t=e(r);return t.prototype=Object.create(Error.prototype),t.prototype.constructor=t,t}var er=Ze(function(e){return function(t){e(this),this.message=t?t.length+` errors occurred during unsubscription: `+t.map(function(n,o){return o+1+") "+n.toString()}).join(` - `):"",this.name="UnsubscriptionError",this.errors=t}});function fe(e,r){if(e){var t=e.indexOf(r);0<=t&&e.splice(t,1)}}var ie=function(){function e(r){this.initialTeardown=r,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var r,t,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var u=re(a),s=u.next();!s.done;s=u.next()){var f=s.value;f.remove(this)}}catch(m){r={error:m}}finally{try{s&&!s.done&&(t=u.return)&&t.call(u)}finally{if(r)throw r.error}}else a.remove(this);var c=this.initialTeardown;if(E(c))try{c()}catch(m){i=m instanceof er?m.errors:[m]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var l=re(p),h=l.next();!h.done;h=l.next()){var d=h.value;try{Rt(d)}catch(m){i=i!=null?i:[],m instanceof er?i=U(U([],W(i)),W(m.errors)):i.push(m)}}}catch(m){n={error:m}}finally{try{h&&!h.done&&(o=l.return)&&o.call(l)}finally{if(n)throw n.error}}}if(i)throw new er(i)}},e.prototype.add=function(r){var t;if(r&&r!==this)if(this.closed)Rt(r);else{if(r instanceof e){if(r.closed||r._hasParent(this))return;r._addParent(this)}(this._finalizers=(t=this._finalizers)!==null&&t!==void 0?t:[]).push(r)}},e.prototype._hasParent=function(r){var t=this._parentage;return t===r||Array.isArray(t)&&t.includes(r)},e.prototype._addParent=function(r){var t=this._parentage;this._parentage=Array.isArray(t)?(t.push(r),t):t?[t,r]:r},e.prototype._removeParent=function(r){var t=this._parentage;t===r?this._parentage=null:Array.isArray(t)&&fe(t,r)},e.prototype.remove=function(r){var t=this._finalizers;t&&fe(t,r),r instanceof e&&r._removeParent(this)},e.EMPTY=function(){var r=new e;return r.closed=!0,r}(),e}();var Wr=ie.EMPTY;function rr(e){return e instanceof ie||e&&"closed"in e&&E(e.remove)&&E(e.add)&&E(e.unsubscribe)}function Rt(e){E(e)?e():e.unsubscribe()}var te={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Te={setTimeout:function(e,r){for(var t=[],n=2;n0},enumerable:!1,configurable:!0}),r.prototype._trySubscribe=function(t){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,t)},r.prototype._subscribe=function(t){return this._throwIfClosed(),this._checkFinalizedStatuses(t),this._innerSubscribe(t)},r.prototype._innerSubscribe=function(t){var n=this,o=this,i=o.hasError,a=o.isStopped,u=o.observers;return i||a?Wr:(this.currentObservers=null,u.push(t),new ie(function(){n.currentObservers=null,fe(u,t)}))},r.prototype._checkFinalizedStatuses=function(t){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?t.error(i):a&&t.complete()},r.prototype.asObservable=function(){var t=new L;return t.source=this,t},r.create=function(t,n){return new jt(t,n)},r}(L);var jt=function(e){V(r,e);function r(t,n){var o=e.call(this)||this;return o.destination=t,o.source=n,o}return r.prototype.next=function(t){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,t)},r.prototype.error=function(t){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,t)},r.prototype.complete=function(){var t,n;(n=(t=this.destination)===null||t===void 0?void 0:t.complete)===null||n===void 0||n.call(t)},r.prototype._subscribe=function(t){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(t))!==null&&o!==void 0?o:Wr},r}(J);var ze={now:function(){return(ze.delegate||Date).now()},delegate:void 0};var Vt=function(e){V(r,e);function r(t,n,o){t===void 0&&(t=1/0),n===void 0&&(n=1/0),o===void 0&&(o=ze);var i=e.call(this)||this;return i._bufferSize=t,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,t),i._windowTime=Math.max(1,n),i}return r.prototype.next=function(t){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,u=n._timestampProvider,s=n._windowTime;o||(i.push(t),!a&&i.push(u.now()+s)),this._trimBuffer(),e.prototype.next.call(this,t)},r.prototype._subscribe=function(t){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(t),o=this,i=o._infiniteTimeWindow,a=o._buffer,u=a.slice(),s=0;s0?e.prototype.requestAsyncId.call(this,t,n,o):(t.actions.push(this),t._scheduled||(t._scheduled=Ie.requestAnimationFrame(function(){return t.flush(void 0)})))},r.prototype.recycleAsyncId=function(t,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,t,n,o);var a=t.actions;n!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(Ie.cancelAnimationFrame(n),t._scheduled=void 0)},r}(or);var Nt=function(e){V(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.prototype.flush=function(t){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;t=t||o.shift();do if(i=t.execute(t.state,t.delay))break;while((t=o[0])&&t.id===n&&o.shift());if(this._active=!1,i){for(;(t=o[0])&&t.id===n&&o.shift();)t.unsubscribe();throw i}},r}(ir);var qr=new Nt($t);var le=new L(function(e){return e.complete()});function ar(e){return e&&E(e.schedule)}function Dr(e){return e[e.length-1]}function Ce(e){return E(Dr(e))?e.pop():void 0}function ae(e){return ar(Dr(e))?e.pop():void 0}function Gt(e,r){return typeof Dr(e)=="number"?e.pop():r}var Le=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function sr(e){return E(e==null?void 0:e.then)}function ur(e){return E(e[Me])}function fr(e){return Symbol.asyncIterator&&E(e==null?void 0:e[Symbol.asyncIterator])}function cr(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function mo(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var lr=mo();function pr(e){return E(e==null?void 0:e[lr])}function mr(e){return Ct(this,arguments,function(){var t,n,o,i;return Be(this,function(a){switch(a.label){case 0:t=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,Xe(t.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,Xe(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,Xe(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return t.releaseLock(),[7];case 10:return[2]}})})}function hr(e){return E(e==null?void 0:e.getReader)}function F(e){if(e instanceof L)return e;if(e!=null){if(ur(e))return ho(e);if(Le(e))return vo(e);if(sr(e))return bo(e);if(fr(e))return Qt(e);if(pr(e))return yo(e);if(hr(e))return xo(e)}throw cr(e)}function ho(e){return new L(function(r){var t=e[Me]();if(E(t.subscribe))return t.subscribe(r);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function vo(e){return new L(function(r){for(var t=0;t0&&(c=new ye({next:function(_){return y.next(_)},error:function(_){m=!0,b(),p=Br(w,o,_),y.error(_)},complete:function(){d=!0,b(),p=Br(w,a),y.complete()}}),F(R).subscribe(c))})(f)}}function Br(e,r){for(var t=[],n=2;n{let r=Sr();return typeof r!="undefined"?e.contains(r):!1}),he(e===Sr()),pe())}function yn(e){return{x:e.scrollLeft,y:e.scrollTop}}function xn(e){return $(Q(e,"scroll"),Q(window,"resize")).pipe(Nr(0,qr),k(()=>yn(e)),he(yn(e)))}var wn=function(){if(typeof Map!="undefined")return Map;function e(r,t){var n=-1;return r.some(function(o,i){return o[0]===t?(n=i,!0):!1}),n}return function(){function r(){this.__entries__=[]}return Object.defineProperty(r.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),r.prototype.get=function(t){var n=e(this.__entries__,t),o=this.__entries__[n];return o&&o[1]},r.prototype.set=function(t,n){var o=e(this.__entries__,t);~o?this.__entries__[o][1]=n:this.__entries__.push([t,n])},r.prototype.delete=function(t){var n=this.__entries__,o=e(n,t);~o&&n.splice(o,1)},r.prototype.has=function(t){return!!~e(this.__entries__,t)},r.prototype.clear=function(){this.__entries__.splice(0)},r.prototype.forEach=function(t,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!Zr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),qo?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!Zr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(r){var t=r.propertyName,n=t===void 0?"":t,o=Vo.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),_n=function(e,r){for(var t=0,n=Object.keys(r);t0},e}(),En=typeof WeakMap!="undefined"?new WeakMap:new wn,On=function(){function e(r){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var t=Do.getInstance(),n=new Zo(r,t,this);En.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){On.prototype[e]=function(){var r;return(r=En.get(this))[e].apply(r,arguments)}});var ei=function(){return typeof Er.ResizeObserver!="undefined"?Er.ResizeObserver:On}(),Tn=ei;var ri=new J,Hp=Ve(()=>X(new Tn(e=>{for(let r of e)ri.next(r)}))).pipe(N(e=>$(qe,X(e)).pipe(_e(()=>e.disconnect()))),Se(1));function An(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Mn(e){return{width:e.scrollWidth,height:e.scrollHeight}}var ti=new J,Np=Ve(()=>X(new IntersectionObserver(e=>{for(let r of e)ti.next(r)},{threshold:0}))).pipe(N(e=>$(qe,X(e)).pipe(_e(()=>e.disconnect()))),Se(1));function In(e,r=16){return xn(e).pipe(k(({y:t})=>{let n=An(e),o=Mn(e);return t>=o.height-n.height-r}),pe())}var em={drawer:Z("[data-md-toggle=drawer]"),search:Z("[data-md-toggle=search]")};function Cn(){return new URL(location.href)}function Ln(e,r){if(typeof r=="string"||typeof r=="number")e.innerHTML+=r.toString();else if(r instanceof Node)e.appendChild(r);else if(Array.isArray(r))for(let t of r)Ln(e,t)}function ee(e,r,...t){let n=document.createElement(e);if(r)for(let o of Object.keys(r))typeof r[o]!="undefined"&&(typeof r[o]!="boolean"?n.setAttribute(o,r[o]):n.setAttribute(o,""));for(let o of t)Ln(n,o);return n}function Rn(e){if(e>999){let r=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(r)}k`}else return e.toString()}function ni(e,r={credentials:"same-origin"}){return B(fetch(`${e}`,r)).pipe(wr(()=>le),N(t=>t.status!==200?$r(()=>new Error(t.statusText)):X(t)))}function Ar(e,r){return ni(e,r).pipe(N(t=>t.json()),Se(1))}function He(e,r=document){return Z(`[data-mdx-component=${e}]`,r)}function et(e,r=document){return dn(`[data-mdx-component=${e}]`,r)}var oi=Z("#__config"),De=JSON.parse(oi.textContent);De.base=`${new URL(De.base,Cn())}`;function Pn(){return De}function Fn(e,r){return typeof r!="undefined"?De.translations[e].replace("#",r.toString()):De.translations[e]}function kn(e){let r=bn(e),t=$(Q(e,"keyup"),Q(e,"focus").pipe(Jr(1))).pipe(k(()=>e.value),he(e.value),pe());return r.pipe(we(n=>!n),Fe(t)).subscribe(([,n])=>{let o=document.location.pathname;typeof ga=="function"&&n.length&&ga("send","pageview",`${o}?q=[icon]+${n}`)}),Re([t,r]).pipe(k(([n,o])=>({ref:e,value:n,focus:o})))}var nt=kr(tt());var Nn=kr(tt());function $n(e,r){return(0,Nn.wrap)(e.shortcode,r,{wrap:{tagOpen:"",tagClose:""}})}function Gn(e,r,t){return ee("li",{class:"mdx-iconsearch-result__item"},ee("span",{class:"twemoji"},ee("img",{src:e.url})),ee("button",{class:"md-clipboard--inline",title:Fn("clipboard.copy"),"data-clipboard-text":t?e.shortcode:`:${e.shortcode}:`},ee("code",null,t?$n(e,r):`:${$n(e,r)}:`)))}function Qn(e){let r=`@${e.name}`;return ee("a",{href:e.url,title:r,class:"mdx-sponsorship__item"},ee("img",{src:e.image}))}function Kn(e){return ee("a",{href:"https://github.com/sponsors/squidfunk",class:"mdx-sponsorship__item mdx-sponsorship__item--private"},"+",e)}function ii(e,{index$:r,query$:t}){switch(e.getAttribute("data-mdx-mode")){case"file":return Re([t.pipe(_r("value")),r.pipe(k(({icons:n})=>Object.values(n.data).map(o=>o.replace(/\.svg$/,""))))]).pipe(k(([{value:n},o])=>(0,nt.filter)(o,n)),N(n=>r.pipe(k(({icons:o})=>({data:n.map(i=>({shortcode:i,url:[o.base,i,".svg"].join("")}))})))));default:return Re([t.pipe(_r("value")),r.pipe(k(({icons:n,emojis:o})=>[...Object.keys(n.data),...Object.keys(o.data)]))]).pipe(k(([{value:n},o])=>(0,nt.filter)(o,n)),N(n=>r.pipe(k(({icons:o,emojis:i})=>({data:n.map(a=>{let u=a in o.data?o:i;return{shortcode:a,url:[u.base,u.data[a]].join("")}})})))))}}function Yn(e,{index$:r,query$:t}){let n=new J,o=In(e).pipe(we(Boolean)),i=Z(":scope > :first-child",e);n.pipe(Fe(t)).subscribe(([{data:s},{value:f}])=>{if(f)switch(s.length){case 0:i.textContent="No matches";break;case 1:i.textContent="1 match";break;default:i.textContent=`${Rn(s.length)} matches`}else i.textContent="Type to start searching"});let a=e.getAttribute("data-mdx-mode")==="file",u=Z(":scope > :last-child",e);return n.pipe(Pe(()=>u.innerHTML=""),N(({data:s})=>$(X(...s.slice(0,10)),X(...s.slice(10)).pipe(Gr(10),Xr(o),N(([f])=>f)))),Fe(t)).subscribe(([s,{value:f}])=>u.appendChild(Gn(s,f,a))),ii(e,{query$:t,index$:r}).pipe(Pe(s=>n.next(s)),_e(()=>n.complete()),k(s=>Qe({ref:e},s)))}function Jn(e){let r=Pn(),t=Ar(new URL("assets/javascripts/iconsearch_index.json",r.base)),n=He("iconsearch-query",e),o=He("iconsearch-result",e),i=kn(n),a=Yn(o,{index$:t,query$:i});return $(i,a)}function Bn(e){let r=Ar("https://3if8u9o552.execute-api.us-east-1.amazonaws.com/_/"),t=He("sponsorship-count"),n=He("sponsorship-total");return r.subscribe(o=>{e.removeAttribute("hidden");let i=Z(":scope > :first-child",e);for(let a of o.sponsors)a.type==="public"&&i.appendChild(Qn(a.user));i.appendChild(Kn(o.sponsors.filter(({type:a})=>a==="private").length)),t.innerText=`${o.sponsors.length}`,n.innerText=`$ ${o.total.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")} a month`}),r.pipe(k(o=>Qe({ref:e},o)))}function Xn(){let{origin:e}=new URL(location.href);Q(document.body,"click").subscribe(r=>{if(r.target instanceof HTMLElement){let t=r.target.closest("a");t&&t.origin!==e&&ga("send","event","outbound","click",t.href)}})}Xn();var ai=document$.pipe(N(()=>$(...et("iconsearch").map(e=>Jn(e)),...et("sponsorship").map(e=>Bn(e)))));ai.subscribe();})(); -//# sourceMappingURL=custom.83b17dfb.min.js.map + `):"",this.name="UnsubscriptionError",this.errors=t}});function fe(e,r){if(e){var t=e.indexOf(r);0<=t&&e.splice(t,1)}}var ie=function(){function e(r){this.initialTeardown=r,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var r,t,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var u=re(a),s=u.next();!s.done;s=u.next()){var f=s.value;f.remove(this)}}catch(m){r={error:m}}finally{try{s&&!s.done&&(t=u.return)&&t.call(u)}finally{if(r)throw r.error}}else a.remove(this);var c=this.initialTeardown;if(S(c))try{c()}catch(m){i=m instanceof er?m.errors:[m]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var l=re(p),h=l.next();!h.done;h=l.next()){var d=h.value;try{Pt(d)}catch(m){i=i!=null?i:[],m instanceof er?i=U(U([],W(i)),W(m.errors)):i.push(m)}}}catch(m){n={error:m}}finally{try{h&&!h.done&&(o=l.return)&&o.call(l)}finally{if(n)throw n.error}}}if(i)throw new er(i)}},e.prototype.add=function(r){var t;if(r&&r!==this)if(this.closed)Pt(r);else{if(r instanceof e){if(r.closed||r._hasParent(this))return;r._addParent(this)}(this._finalizers=(t=this._finalizers)!==null&&t!==void 0?t:[]).push(r)}},e.prototype._hasParent=function(r){var t=this._parentage;return t===r||Array.isArray(t)&&t.includes(r)},e.prototype._addParent=function(r){var t=this._parentage;this._parentage=Array.isArray(t)?(t.push(r),t):t?[t,r]:r},e.prototype._removeParent=function(r){var t=this._parentage;t===r?this._parentage=null:Array.isArray(t)&&fe(t,r)},e.prototype.remove=function(r){var t=this._finalizers;t&&fe(t,r),r instanceof e&&r._removeParent(this)},e.EMPTY=function(){var r=new e;return r.closed=!0,r}(),e}();var Wr=ie.EMPTY;function rr(e){return e instanceof ie||e&&"closed"in e&&S(e.remove)&&S(e.add)&&S(e.unsubscribe)}function Pt(e){S(e)?e():e.unsubscribe()}var te={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Te={setTimeout:function(e,r){for(var t=[],n=2;n0},enumerable:!1,configurable:!0}),r.prototype._trySubscribe=function(t){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,t)},r.prototype._subscribe=function(t){return this._throwIfClosed(),this._checkFinalizedStatuses(t),this._innerSubscribe(t)},r.prototype._innerSubscribe=function(t){var n=this,o=this,i=o.hasError,a=o.isStopped,u=o.observers;return i||a?Wr:(this.currentObservers=null,u.push(t),new ie(function(){n.currentObservers=null,fe(u,t)}))},r.prototype._checkFinalizedStatuses=function(t){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?t.error(i):a&&t.complete()},r.prototype.asObservable=function(){var t=new L;return t.source=this,t},r.create=function(t,n){return new jt(t,n)},r}(L);var jt=function(e){V(r,e);function r(t,n){var o=e.call(this)||this;return o.destination=t,o.source=n,o}return r.prototype.next=function(t){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,t)},r.prototype.error=function(t){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,t)},r.prototype.complete=function(){var t,n;(n=(t=this.destination)===null||t===void 0?void 0:t.complete)===null||n===void 0||n.call(t)},r.prototype._subscribe=function(t){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(t))!==null&&o!==void 0?o:Wr},r}(J);var ze={now:function(){return(ze.delegate||Date).now()},delegate:void 0};var Vt=function(e){V(r,e);function r(t,n,o){t===void 0&&(t=1/0),n===void 0&&(n=1/0),o===void 0&&(o=ze);var i=e.call(this)||this;return i._bufferSize=t,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,t),i._windowTime=Math.max(1,n),i}return r.prototype.next=function(t){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,u=n._timestampProvider,s=n._windowTime;o||(i.push(t),!a&&i.push(u.now()+s)),this._trimBuffer(),e.prototype.next.call(this,t)},r.prototype._subscribe=function(t){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(t),o=this,i=o._infiniteTimeWindow,a=o._buffer,u=a.slice(),s=0;s0?e.prototype.requestAsyncId.call(this,t,n,o):(t.actions.push(this),t._scheduled||(t._scheduled=Ie.requestAnimationFrame(function(){return t.flush(void 0)})))},r.prototype.recycleAsyncId=function(t,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,t,n,o);var a=t.actions;n!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(Ie.cancelAnimationFrame(n),t._scheduled=void 0)},r}(or);var Nt=function(e){V(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.prototype.flush=function(t){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;t=t||o.shift();do if(i=t.execute(t.state,t.delay))break;while((t=o[0])&&t.id===n&&o.shift());if(this._active=!1,i){for(;(t=o[0])&&t.id===n&&o.shift();)t.unsubscribe();throw i}},r}(ir);var $r=new Nt(Dt);var le=new L(function(e){return e.complete()});function ar(e){return e&&S(e.schedule)}function qr(e){return e[e.length-1]}function Ce(e){return S(qr(e))?e.pop():void 0}function ae(e){return ar(qr(e))?e.pop():void 0}function Gt(e,r){return typeof qr(e)=="number"?e.pop():r}var Le=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function sr(e){return S(e==null?void 0:e.then)}function ur(e){return S(e[Me])}function fr(e){return Symbol.asyncIterator&&S(e==null?void 0:e[Symbol.asyncIterator])}function cr(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function mo(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var lr=mo();function pr(e){return S(e==null?void 0:e[lr])}function mr(e){return Ct(this,arguments,function(){var t,n,o,i;return Be(this,function(a){switch(a.label){case 0:t=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,Xe(t.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,Xe(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,Xe(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return t.releaseLock(),[7];case 10:return[2]}})})}function hr(e){return S(e==null?void 0:e.getReader)}function k(e){if(e instanceof L)return e;if(e!=null){if(ur(e))return ho(e);if(Le(e))return vo(e);if(sr(e))return bo(e);if(fr(e))return Qt(e);if(pr(e))return yo(e);if(hr(e))return xo(e)}throw cr(e)}function ho(e){return new L(function(r){var t=e[Me]();if(S(t.subscribe))return t.subscribe(r);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function vo(e){return new L(function(r){for(var t=0;t0&&(c=new ye({next:function(_){return y.next(_)},error:function(_){m=!0,b(),p=Br(w,o,_),y.error(_)},complete:function(){d=!0,b(),p=Br(w,a),y.complete()}}),k(P).subscribe(c))})(f)}}function Br(e,r){for(var t=[],n=2;n{let r=Er();return typeof r!="undefined"?e.contains(r):!1}),he(e===Er()),pe())}function yn(e){return{x:e.scrollLeft,y:e.scrollTop}}function xn(e){return D(Q(e,"scroll"),Q(window,"resize")).pipe(Nr(0,$r),F(()=>yn(e)),he(yn(e)))}var wn=function(){if(typeof Map!="undefined")return Map;function e(r,t){var n=-1;return r.some(function(o,i){return o[0]===t?(n=i,!0):!1}),n}return function(){function r(){this.__entries__=[]}return Object.defineProperty(r.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),r.prototype.get=function(t){var n=e(this.__entries__,t),o=this.__entries__[n];return o&&o[1]},r.prototype.set=function(t,n){var o=e(this.__entries__,t);~o?this.__entries__[o][1]=n:this.__entries__.push([t,n])},r.prototype.delete=function(t){var n=this.__entries__,o=e(n,t);~o&&n.splice(o,1)},r.prototype.has=function(t){return!!~e(this.__entries__,t)},r.prototype.clear=function(){this.__entries__.splice(0)},r.prototype.forEach=function(t,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!Zr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),$o?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!Zr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(r){var t=r.propertyName,n=t===void 0?"":t,o=Vo.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),_n=function(e,r){for(var t=0,n=Object.keys(r);t0},e}(),Sn=typeof WeakMap!="undefined"?new WeakMap:new wn,On=function(){function e(r){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var t=qo.getInstance(),n=new Zo(r,t,this);Sn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){On.prototype[e]=function(){var r;return(r=Sn.get(this))[e].apply(r,arguments)}});var ei=function(){return typeof Sr.ResizeObserver!="undefined"?Sr.ResizeObserver:On}(),Tn=ei;var ri=new J,Hp=Ve(()=>X(new Tn(e=>{for(let r of e)ri.next(r)}))).pipe(N(e=>D($e,X(e)).pipe(_e(()=>e.disconnect()))),Ee(1));function An(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Mn(e){return{width:e.scrollWidth,height:e.scrollHeight}}var ti=new J,Np=Ve(()=>X(new IntersectionObserver(e=>{for(let r of e)ti.next(r)},{threshold:0}))).pipe(N(e=>D($e,X(e)).pipe(_e(()=>e.disconnect()))),Ee(1));function In(e,r=16){return xn(e).pipe(F(({y:t})=>{let n=An(e),o=Mn(e);return t>=o.height-n.height-r}),pe())}var em={drawer:Z("[data-md-toggle=drawer]"),search:Z("[data-md-toggle=search]")};function Cn(){return new URL(location.href)}function Ln(e,r){if(typeof r=="string"||typeof r=="number")e.innerHTML+=r.toString();else if(r instanceof Node)e.appendChild(r);else if(Array.isArray(r))for(let t of r)Ln(e,t)}function ee(e,r,...t){let n=document.createElement(e);if(r)for(let o of Object.keys(r))typeof r[o]!="undefined"&&(typeof r[o]!="boolean"?n.setAttribute(o,r[o]):n.setAttribute(o,""));for(let o of t)Ln(n,o);return n}function Pn(e){if(e>999){let r=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(r)}k`}else return e.toString()}function ni(e,r={credentials:"same-origin"}){return B(fetch(`${e}`,r)).pipe(wr(()=>le),N(t=>t.status!==200?Dr(()=>new Error(t.statusText)):X(t)))}function Ar(e,r){return ni(e,r).pipe(N(t=>t.json()),Ee(1))}(()=>{function e(o,i){parent.postMessage(o,i||"*")}function r(...o){return o.reduce((i,a)=>i.then(()=>new Promise(u=>{let s=document.createElement("script");s.src=a,s.addEventListener("load",()=>u()),document.body.appendChild(s)})),Promise.resolve())}function t(){let o=document.createElement("iframe");return o.width=o.height=o.frameBorder="0",o}var n=class{constructor(o,i){if(this.url=o,this.onerror=null,this.onmessage=null,this.onmessageerror=null,this.handleMessage=u=>{u.source===this.worker&&(u.stopImmediatePropagation(),this.dispatchEvent(new MessageEvent("message",{data:u.data})),this.onmessage&&this.onmessage(u))},this.handleError=(u,s,f,c,p)=>{if(s===this.url.toString()){let l=new ErrorEvent("error",{message:u,filename:s,lineno:f,colno:c,error:p});this.dispatchEvent(l),this.onerror&&this.onerror(l)}},typeof i!="undefined")throw new TypeError("Options are not supported for iframe workers");let a=new EventTarget;this.addEventListener=a.addEventListener.bind(a),this.removeEventListener=a.removeEventListener.bind(a),this.dispatchEvent=a.dispatchEvent.bind(a),document.body.appendChild(this.iframe=t()),this.worker.document.open(),this.worker.document.write(` + {% endblock %} diff --git a/material/assets/javascripts/bundle.43982a0d.min.js b/material/assets/javascripts/bundle.43982a0d.min.js new file mode 100644 index 000000000..f5ee16d97 --- /dev/null +++ b/material/assets/javascripts/bundle.43982a0d.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var ia=Object.create;var Or=Object.defineProperty;var aa=Object.getOwnPropertyDescriptor;var sa=Object.getOwnPropertyNames,kt=Object.getOwnPropertySymbols,ca=Object.getPrototypeOf,_r=Object.prototype.hasOwnProperty,un=Object.prototype.propertyIsEnumerable;var fn=(e,t,r)=>t in e?Or(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))_r.call(t,r)&&fn(e,r,t[r]);if(kt)for(var r of kt(t))un.call(t,r)&&fn(e,r,t[r]);return e};var pn=(e,t)=>{var r={};for(var n in e)_r.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&kt)for(var n of kt(e))t.indexOf(n)<0&&un.call(e,n)&&(r[n]=e[n]);return r};var yt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var fa=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of sa(t))!_r.call(e,o)&&o!==r&&Or(e,o,{get:()=>t[o],enumerable:!(n=aa(t,o))||n.enumerable});return e};var ot=(e,t,r)=>(r=e!=null?ia(ca(e)):{},fa(t||!e||!e.__esModule?Or(r,"default",{value:e,enumerable:!0}):r,e));var mn=yt((Tr,ln)=>{(function(e,t){typeof Tr=="object"&&typeof ln!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Tr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(O){return!!(O&&O!==document&&O.nodeName!=="HTML"&&O.nodeName!=="BODY"&&"classList"in O&&"contains"in O.classList)}function c(O){var Ne=O.type,Ue=O.tagName;return!!(Ue==="INPUT"&&a[Ne]&&!O.readOnly||Ue==="TEXTAREA"&&!O.readOnly||O.isContentEditable)}function f(O){O.classList.contains("focus-visible")||(O.classList.add("focus-visible"),O.setAttribute("data-focus-visible-added",""))}function u(O){!O.hasAttribute("data-focus-visible-added")||(O.classList.remove("focus-visible"),O.removeAttribute("data-focus-visible-added"))}function p(O){O.metaKey||O.altKey||O.ctrlKey||(s(r.activeElement)&&f(r.activeElement),n=!0)}function m(O){n=!1}function d(O){!s(O.target)||(n||c(O.target))&&f(O.target)}function h(O){!s(O.target)||(O.target.classList.contains("focus-visible")||O.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(O.target))}function b(O){document.visibilityState==="hidden"&&(o&&(n=!0),z())}function z(){document.addEventListener("mousemove",W),document.addEventListener("mousedown",W),document.addEventListener("mouseup",W),document.addEventListener("pointermove",W),document.addEventListener("pointerdown",W),document.addEventListener("pointerup",W),document.addEventListener("touchmove",W),document.addEventListener("touchstart",W),document.addEventListener("touchend",W)}function Z(){document.removeEventListener("mousemove",W),document.removeEventListener("mousedown",W),document.removeEventListener("mouseup",W),document.removeEventListener("pointermove",W),document.removeEventListener("pointerdown",W),document.removeEventListener("pointerup",W),document.removeEventListener("touchmove",W),document.removeEventListener("touchstart",W),document.removeEventListener("touchend",W)}function W(O){O.target.nodeName&&O.target.nodeName.toLowerCase()==="html"||(n=!1,Z())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",b,!0),z(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var dn=yt(Mr=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(f){return!1}},r=t(),n=function(f){var u={next:function(){var p=f.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(f){return encodeURIComponent(f).replace(/%20/g,"+")},i=function(f){return decodeURIComponent(String(f).replace(/\+/g," "))},a=function(){var f=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof f){var d=this;p.forEach(function(Z,W){d.append(W,Z)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),f._entries&&(f._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Mr);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(c,f){typeof c!="string"&&(c=String(c)),f&&typeof f!="string"&&(f=String(f));var u=document,p;if(f&&(e.location===void 0||f!==e.location.href)){f=f.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=f,u.head.appendChild(p);try{if(p.href.indexOf(f)!==0)throw new Error(p.href)}catch(O){throw new Error("URL unable to set base "+f+" due to "+O)}}var m=u.createElement("a");m.href=c,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=c,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!f)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),b=!0,z=!0,Z=this;["append","delete","set"].forEach(function(O){var Ne=h[O];h[O]=function(){Ne.apply(h,arguments),b&&(z=!1,Z.search=h.toString(),z=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var W=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==W&&(W=this.search,z&&(b=!1,this.searchParams._fromString(this.search),b=!0))}})},a=i.prototype,s=function(c){Object.defineProperty(a,c,{get:function(){return this._anchorElement[c]},set:function(f){this._anchorElement[c]=f},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(c){s(c)}),Object.defineProperty(a,"search",{get:function(){return this._anchorElement.search},set:function(c){this._anchorElement.search=c,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(a,{toString:{get:function(){var c=this;return function(){return c.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(c){this._anchorElement.href=c,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(c){this._anchorElement.pathname=c},enumerable:!0},origin:{get:function(){var c={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],f=this._anchorElement.port!=c&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(f?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(c){},enumerable:!0},username:{get:function(){return""},set:function(c){},enumerable:!0}}),i.createObjectURL=function(c){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(c){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Mr)});var $n=yt((Bs,$t)=>{/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */var hn,bn,vn,gn,yn,xn,wn,En,Sn,Ht,Lr,On,_n,Tn,it,Mn,Ln,An,Cn,Rn,kn,Hn,Pn,Pt;(function(e){var t=typeof global=="object"?global:typeof self=="object"?self:typeof this=="object"?this:{};typeof define=="function"&&define.amd?define("tslib",["exports"],function(n){e(r(t,r(n)))}):typeof $t=="object"&&typeof $t.exports=="object"?e(r(t,r($t.exports))):e(r(t));function r(n,o){return n!==t&&(typeof Object.create=="function"?Object.defineProperty(n,"__esModule",{value:!0}):n.__esModule=!0),function(i,a){return n[i]=o?o(i,a):a}}})(function(e){var t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(n,o){n.__proto__=o}||function(n,o){for(var i in o)Object.prototype.hasOwnProperty.call(o,i)&&(n[i]=o[i])};hn=function(n,o){if(typeof o!="function"&&o!==null)throw new TypeError("Class extends value "+String(o)+" is not a constructor or null");t(n,o);function i(){this.constructor=n}n.prototype=o===null?Object.create(o):(i.prototype=o.prototype,new i)},bn=Object.assign||function(n){for(var o,i=1,a=arguments.length;i=0;u--)(f=n[u])&&(c=(s<3?f(c):s>3?f(o,i,c):f(o,i))||c);return s>3&&c&&Object.defineProperty(o,i,c),c},yn=function(n,o){return function(i,a){o(i,a,n)}},xn=function(n,o){if(typeof Reflect=="object"&&typeof Reflect.metadata=="function")return Reflect.metadata(n,o)},wn=function(n,o,i,a){function s(c){return c instanceof i?c:new i(function(f){f(c)})}return new(i||(i=Promise))(function(c,f){function u(d){try{m(a.next(d))}catch(h){f(h)}}function p(d){try{m(a.throw(d))}catch(h){f(h)}}function m(d){d.done?c(d.value):s(d.value).then(u,p)}m((a=a.apply(n,o||[])).next())})},En=function(n,o){var i={label:0,sent:function(){if(c[0]&1)throw c[1];return c[1]},trys:[],ops:[]},a,s,c,f;return f={next:u(0),throw:u(1),return:u(2)},typeof Symbol=="function"&&(f[Symbol.iterator]=function(){return this}),f;function u(m){return function(d){return p([m,d])}}function p(m){if(a)throw new TypeError("Generator is already executing.");for(;i;)try{if(a=1,s&&(c=m[0]&2?s.return:m[0]?s.throw||((c=s.return)&&c.call(s),0):s.next)&&!(c=c.call(s,m[1])).done)return c;switch(s=0,c&&(m=[m[0]&2,c.value]),m[0]){case 0:case 1:c=m;break;case 4:return i.label++,{value:m[1],done:!1};case 5:i.label++,s=m[1],m=[0];continue;case 7:m=i.ops.pop(),i.trys.pop();continue;default:if(c=i.trys,!(c=c.length>0&&c[c.length-1])&&(m[0]===6||m[0]===2)){i=0;continue}if(m[0]===3&&(!c||m[1]>c[0]&&m[1]=n.length&&(n=void 0),{value:n&&n[a++],done:!n}}};throw new TypeError(o?"Object is not iterable.":"Symbol.iterator is not defined.")},Lr=function(n,o){var i=typeof Symbol=="function"&&n[Symbol.iterator];if(!i)return n;var a=i.call(n),s,c=[],f;try{for(;(o===void 0||o-- >0)&&!(s=a.next()).done;)c.push(s.value)}catch(u){f={error:u}}finally{try{s&&!s.done&&(i=a.return)&&i.call(a)}finally{if(f)throw f.error}}return c},On=function(){for(var n=[],o=0;o1||u(b,z)})})}function u(b,z){try{p(a[b](z))}catch(Z){h(c[0][3],Z)}}function p(b){b.value instanceof it?Promise.resolve(b.value.v).then(m,d):h(c[0][2],b)}function m(b){u("next",b)}function d(b){u("throw",b)}function h(b,z){b(z),c.shift(),c.length&&u(c[0][0],c[0][1])}},Ln=function(n){var o,i;return o={},a("next"),a("throw",function(s){throw s}),a("return"),o[Symbol.iterator]=function(){return this},o;function a(s,c){o[s]=n[s]?function(f){return(i=!i)?{value:it(n[s](f)),done:s==="return"}:c?c(f):f}:c}},An=function(n){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var o=n[Symbol.asyncIterator],i;return o?o.call(n):(n=typeof Ht=="function"?Ht(n):n[Symbol.iterator](),i={},a("next"),a("throw"),a("return"),i[Symbol.asyncIterator]=function(){return this},i);function a(c){i[c]=n[c]&&function(f){return new Promise(function(u,p){f=n[c](f),s(u,p,f.done,f.value)})}}function s(c,f,u,p){Promise.resolve(p).then(function(m){c({value:m,done:u})},f)}},Cn=function(n,o){return Object.defineProperty?Object.defineProperty(n,"raw",{value:o}):n.raw=o,n};var r=Object.create?function(n,o){Object.defineProperty(n,"default",{enumerable:!0,value:o})}:function(n,o){n.default=o};Rn=function(n){if(n&&n.__esModule)return n;var o={};if(n!=null)for(var i in n)i!=="default"&&Object.prototype.hasOwnProperty.call(n,i)&&Pt(o,n,i);return r(o,n),o},kn=function(n){return n&&n.__esModule?n:{default:n}},Hn=function(n,o,i,a){if(i==="a"&&!a)throw new TypeError("Private accessor was defined without a getter");if(typeof o=="function"?n!==o||!a:!o.has(n))throw new TypeError("Cannot read private member from an object whose class did not declare it");return i==="m"?a:i==="a"?a.call(n):a?a.value:o.get(n)},Pn=function(n,o,i,a,s){if(a==="m")throw new TypeError("Private method is not writable");if(a==="a"&&!s)throw new TypeError("Private accessor was defined without a setter");if(typeof o=="function"?n!==o||!s:!o.has(n))throw new TypeError("Cannot write private member to an object whose class did not declare it");return a==="a"?s.call(n,i):s?s.value=i:o.set(n,i),i},e("__extends",hn),e("__assign",bn),e("__rest",vn),e("__decorate",gn),e("__param",yn),e("__metadata",xn),e("__awaiter",wn),e("__generator",En),e("__exportStar",Sn),e("__createBinding",Pt),e("__values",Ht),e("__read",Lr),e("__spread",On),e("__spreadArrays",_n),e("__spreadArray",Tn),e("__await",it),e("__asyncGenerator",Mn),e("__asyncDelegator",Ln),e("__asyncValues",An),e("__makeTemplateObject",Cn),e("__importStar",Rn),e("__importDefault",kn),e("__classPrivateFieldGet",Hn),e("__classPrivateFieldSet",Pn)})});var Jr=yt((Lt,Gr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Lt=="object"&&typeof Gr=="object"?Gr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Lt=="object"?Lt.ClipboardJS=r():t.ClipboardJS=r()})(Lt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return oa}});var a=i(279),s=i.n(a),c=i(370),f=i.n(c),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(_){return!1}}var d=function(_){var E=p()(_);return m("cut"),E},h=d;function b(j){var _=document.documentElement.getAttribute("dir")==="rtl",E=document.createElement("textarea");E.style.fontSize="12pt",E.style.border="0",E.style.padding="0",E.style.margin="0",E.style.position="absolute",E.style[_?"right":"left"]="-9999px";var k=window.pageYOffset||document.documentElement.scrollTop;return E.style.top="".concat(k,"px"),E.setAttribute("readonly",""),E.value=j,E}var z=function(_,E){var k=b(_);E.container.appendChild(k);var I=p()(k);return m("copy"),k.remove(),I},Z=function(_){var E=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},k="";return typeof _=="string"?k=z(_,E):_ instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(_==null?void 0:_.type)?k=z(_.value,E):(k=p()(_),m("copy")),k},W=Z;function O(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?O=function(E){return typeof E}:O=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},O(j)}var Ne=function(){var _=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},E=_.action,k=E===void 0?"copy":E,I=_.container,q=_.target,Te=_.text;if(k!=="copy"&&k!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&O(q)==="object"&&q.nodeType===1){if(k==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(k==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Te)return W(Te,{container:I});if(q)return k==="cut"?h(q):W(q,{container:I})},Ue=Ne;function $e(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?$e=function(E){return typeof E}:$e=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},$e(j)}function Gi(j,_){if(!(j instanceof _))throw new TypeError("Cannot call a class as a function")}function cn(j,_){for(var E=0;E<_.length;E++){var k=_[E];k.enumerable=k.enumerable||!1,k.configurable=!0,"value"in k&&(k.writable=!0),Object.defineProperty(j,k.key,k)}}function Ji(j,_,E){return _&&cn(j.prototype,_),E&&cn(j,E),j}function Xi(j,_){if(typeof _!="function"&&_!==null)throw new TypeError("Super expression must either be null or a function");j.prototype=Object.create(_&&_.prototype,{constructor:{value:j,writable:!0,configurable:!0}}),_&&Er(j,_)}function Er(j,_){return Er=Object.setPrototypeOf||function(k,I){return k.__proto__=I,k},Er(j,_)}function Zi(j){var _=ra();return function(){var k=Ct(j),I;if(_){var q=Ct(this).constructor;I=Reflect.construct(k,arguments,q)}else I=k.apply(this,arguments);return ea(this,I)}}function ea(j,_){return _&&($e(_)==="object"||typeof _=="function")?_:ta(j)}function ta(j){if(j===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return j}function ra(){if(typeof Reflect=="undefined"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy=="function")return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(j){return!1}}function Ct(j){return Ct=Object.setPrototypeOf?Object.getPrototypeOf:function(E){return E.__proto__||Object.getPrototypeOf(E)},Ct(j)}function Sr(j,_){var E="data-clipboard-".concat(j);if(!!_.hasAttribute(E))return _.getAttribute(E)}var na=function(j){Xi(E,j);var _=Zi(E);function E(k,I){var q;return Gi(this,E),q=_.call(this),q.resolveOptions(I),q.listenClick(k),q}return Ji(E,[{key:"resolveOptions",value:function(){var I=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof I.action=="function"?I.action:this.defaultAction,this.target=typeof I.target=="function"?I.target:this.defaultTarget,this.text=typeof I.text=="function"?I.text:this.defaultText,this.container=$e(I.container)==="object"?I.container:document.body}},{key:"listenClick",value:function(I){var q=this;this.listener=f()(I,"click",function(Te){return q.onClick(Te)})}},{key:"onClick",value:function(I){var q=I.delegateTarget||I.currentTarget,Te=this.action(q)||"copy",Rt=Ue({action:Te,container:this.container,target:this.target(q),text:this.text(q)});this.emit(Rt?"success":"error",{action:Te,text:Rt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(I){return Sr("action",I)}},{key:"defaultTarget",value:function(I){var q=Sr("target",I);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(I){return Sr("text",I)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(I){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return W(I,q)}},{key:"cut",value:function(I){return h(I)}},{key:"isSupported",value:function(){var I=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof I=="string"?[I]:I,Te=!!document.queryCommandSupported;return q.forEach(function(Rt){Te=Te&&!!document.queryCommandSupported(Rt)}),Te}}]),E}(s()),oa=na},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}n.exports=a},438:function(n,o,i){var a=i(828);function s(u,p,m,d,h){var b=f.apply(this,arguments);return u.addEventListener(m,b,h),{destroy:function(){u.removeEventListener(m,b,h)}}}function c(u,p,m,d,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof m=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(b){return s(b,p,m,d,h)}))}function f(u,p,m,d){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=c},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(n,o,i){var a=i(879),s=i(438);function c(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!a.string(d))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(m))return f(m,d,h);if(a.nodeList(m))return u(m,d,h);if(a.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function f(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(b){b.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(b){b.removeEventListener(d,h)})}}}function p(m,d,h){return s(document.body,m,d,h)}n.exports=c},817:function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),f=document.createRange();f.selectNodeContents(i),c.removeAllRanges(),c.addRange(f),a=c.toString()}return a}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function f(){c.off(i,f),a.apply(s,arguments)}return f._=a,this.on(i,f,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,f=s.length;for(c;c{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var Rs=/["'&<>]/;Si.exports=ks;function ks(e){var t=""+e,r=Rs.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Ar:(this.currentObservers=null,s.push(r),new Ie(function(){n.currentObservers=null,We(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,n){return new Yn(r,n)},t}(F);var Yn=function(e){ne(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Ar},t}(w);var wt={now:function(){return(wt.delegate||Date).now()},delegate:void 0};var Et=function(e){ne(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=wt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,c=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),c=0;c0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Vt);var Jn=function(e){ne(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(zt);var xe=new Jn(Gn);var R=new F(function(e){return e.complete()});function Nt(e){return e&&A(e.schedule)}function Ir(e){return e[e.length-1]}function qe(e){return A(Ir(e))?e.pop():void 0}function Ee(e){return Nt(Ir(e))?e.pop():void 0}function qt(e,t){return typeof Ir(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Kt(e){return A(e==null?void 0:e.then)}function Qt(e){return A(e[ft])}function Yt(e){return Symbol.asyncIterator&&A(e==null?void 0:e[Symbol.asyncIterator])}function Bt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function ga(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Gt=ga();function Jt(e){return A(e==null?void 0:e[Gt])}function Xt(e){return Fn(this,arguments,function(){var r,n,o,i;return It(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,jt(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,jt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,jt(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Zt(e){return A(e==null?void 0:e.getReader)}function V(e){if(e instanceof F)return e;if(e!=null){if(Qt(e))return ya(e);if(pt(e))return xa(e);if(Kt(e))return wa(e);if(Yt(e))return Xn(e);if(Jt(e))return Ea(e);if(Zt(e))return Sa(e)}throw Bt(e)}function ya(e){return new F(function(t){var r=e[ft]();if(A(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function xa(e){return new F(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?T(function(o,i){return e(o,i,n)}):me,ge(1),r?He(t):bo(function(){return new tr}))}}function vo(){for(var e=[],t=0;t=2,!0))}function fe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new w}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(f){var u,p,m,d=0,h=!1,b=!1,z=function(){p==null||p.unsubscribe(),p=void 0},Z=function(){z(),u=m=void 0,h=b=!1},W=function(){var O=u;Z(),O==null||O.unsubscribe()};return g(function(O,Ne){d++,!b&&!h&&z();var Ue=m=m!=null?m:r();Ne.add(function(){d--,d===0&&!b&&!h&&(p=Vr(W,c))}),Ue.subscribe(Ne),!u&&d>0&&(u=new Ze({next:function($e){return Ue.next($e)},error:function($e){b=!0,z(),p=Vr(Z,o,$e),Ue.error($e)},complete:function(){h=!0,z(),p=Vr(Z,a),Ue.complete()}}),V(O).subscribe(u))})(f)}}function Vr(e,t){for(var r=[],n=2;ne.next(document)),e}function Q(e,t=document){return Array.from(t.querySelectorAll(e))}function K(e,t=document){let r=se(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function se(e,t=document){return t.querySelector(e)||void 0}function je(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function or(e){return M(v(document.body,"focusin"),v(document.body,"focusout")).pipe(ke(1),l(()=>{let t=je();return typeof t!="undefined"?e.contains(t):!1}),N(e===je()),B())}function Ye(e){return{x:e.offsetLeft,y:e.offsetTop}}function wo(e){return M(v(window,"load"),v(window,"resize")).pipe(Ce(0,xe),l(()=>Ye(e)),N(Ye(e)))}function ir(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return M(v(e,"scroll"),v(window,"resize")).pipe(Ce(0,xe),l(()=>ir(e)),N(ir(e)))}var So=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!Qr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),Ka?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!Qr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=qa.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Oo=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),To=typeof WeakMap!="undefined"?new WeakMap:new So,Mo=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=Qa.getInstance(),n=new ns(t,r,this);To.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){Mo.prototype[e]=function(){var t;return(t=To.get(this))[e].apply(t,arguments)}});var os=function(){return typeof ar.ResizeObserver!="undefined"?ar.ResizeObserver:Mo}(),Lo=os;var Ao=new w,is=$(()=>H(new Lo(e=>{for(let t of e)Ao.next(t)}))).pipe(x(e=>M(Se,H(e)).pipe(C(()=>e.disconnect()))),J(1));function de(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ye(e){return is.pipe(S(t=>t.observe(e)),x(t=>Ao.pipe(T(({target:r})=>r===e),C(()=>t.unobserve(e)),l(()=>de(e)))),N(de(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function fr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var Co=new w,as=$(()=>H(new IntersectionObserver(e=>{for(let t of e)Co.next(t)},{threshold:0}))).pipe(x(e=>M(Se,H(e)).pipe(C(()=>e.disconnect()))),J(1));function ur(e){return as.pipe(S(t=>t.observe(e)),x(t=>Co.pipe(T(({target:r})=>r===e),C(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function Ro(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=de(e),o=bt(e);return r>=o.height-n.height-t}),B())}var pr={drawer:K("[data-md-toggle=drawer]"),search:K("[data-md-toggle=search]")};function ko(e){return pr[e].checked}function Be(e,t){pr[e].checked!==t&&pr[e].click()}function Ge(e){let t=pr[e];return v(t,"change").pipe(l(()=>t.checked),N(t.checked))}function ss(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function cs(){return M(v(window,"compositionstart").pipe(l(()=>!0)),v(window,"compositionend").pipe(l(()=>!1))).pipe(N(!1))}function Ho(){let e=v(window,"keydown").pipe(T(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:ko("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),T(({mode:t,type:r})=>{if(t==="global"){let n=je();if(typeof n!="undefined")return!ss(n,r)}return!0}),fe());return cs().pipe(x(t=>t?R:e))}function Oe(){return new URL(location.href)}function lr(e){location.href=e.href}function Po(){return new w}function $o(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)$o(e,r)}function L(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)$o(n,o);return n}function mr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function Io(){return location.hash.substring(1)}function jo(e){let t=L("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function fs(){return v(window,"hashchange").pipe(l(Io),N(Io()),T(e=>e.length>0),J(1))}function Fo(){return fs().pipe(l(e=>se(`[id="${e}"]`)),T(e=>typeof e!="undefined"))}function Yr(e){let t=matchMedia(e);return rr(r=>t.addListener(()=>r(t.matches))).pipe(N(t.matches))}function Uo(){let e=matchMedia("print");return M(v(window,"beforeprint").pipe(l(()=>!0)),v(window,"afterprint").pipe(l(()=>!1))).pipe(N(e.matches))}function Br(e,t){return e.pipe(x(r=>r?t():R))}function dr(e,t={credentials:"same-origin"}){return ve(fetch(`${e}`,t)).pipe(ce(()=>R),x(r=>r.status!==200?Ot(()=>new Error(r.statusText)):H(r)))}function Fe(e,t){return dr(e,t).pipe(x(r=>r.json()),J(1))}function Wo(e,t){let r=new DOMParser;return dr(e,t).pipe(x(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),J(1))}function hr(e){let t=L("script",{src:e});return $(()=>(document.head.appendChild(t),M(v(t,"load"),v(t,"error").pipe(x(()=>Ot(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),C(()=>document.head.removeChild(t)),ge(1))))}function Do(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function Vo(){return M(v(window,"scroll",{passive:!0}),v(window,"resize",{passive:!0})).pipe(l(Do),N(Do()))}function zo(){return{width:innerWidth,height:innerHeight}}function No(){return v(window,"resize",{passive:!0}).pipe(l(zo),N(zo()))}function qo(){return Y([Vo(),No()]).pipe(l(([e,t])=>({offset:e,size:t})),J(1))}function br(e,{viewport$:t,header$:r}){let n=t.pipe(X("size")),o=Y([n,r]).pipe(l(()=>Ye(e)));return Y([r,t,o]).pipe(l(([{height:i},{offset:a,size:s},{x:c,y:f}])=>({offset:{x:a.x-c,y:a.y-f+i},size:s})))}(()=>{function e(o,i){parent.postMessage(o,i||"*")}function t(...o){return o.reduce((i,a)=>i.then(()=>new Promise(s=>{let c=document.createElement("script");c.src=a,c.addEventListener("load",()=>s()),document.body.appendChild(c)})),Promise.resolve())}function r(){let o=document.createElement("iframe");return o.width=o.height=o.frameBorder="0",o}var n=class{constructor(o,i){if(this.url=o,this.onerror=null,this.onmessage=null,this.onmessageerror=null,this.handleMessage=s=>{s.source===this.worker&&(s.stopImmediatePropagation(),this.dispatchEvent(new MessageEvent("message",{data:s.data})),this.onmessage&&this.onmessage(s))},this.handleError=(s,c,f,u,p)=>{if(c===this.url.toString()){let m=new ErrorEvent("error",{message:s,filename:c,lineno:f,colno:u,error:p});this.dispatchEvent(m),this.onerror&&this.onerror(m)}},typeof i!="undefined")throw new TypeError("Options are not supported for iframe workers");let a=new EventTarget;this.addEventListener=a.addEventListener.bind(a),this.removeEventListener=a.removeEventListener.bind(a),this.dispatchEvent=a.dispatchEvent.bind(a),document.body.appendChild(this.iframe=r()),this.worker.document.open(),this.worker.document.write(` {% endblock %} {% block scripts %} - + {% for path in config.extra_javascript %} {% endfor %} {% endblock %} {% if page.meta and page.meta.ᴴₒᴴₒᴴₒ %} - + {% endif %} diff --git a/material/partials/languages/el.html b/material/partials/languages/el.html index c9d7b4f5f..aad196561 100644 --- a/material/partials/languages/el.html +++ b/material/partials/languages/el.html @@ -14,7 +14,6 @@ "meta.source": "Πηγή", "nav": "Πλοήγηση", "search": "Αναζήτηση", - "search.config.pipeline": "stopWordFilter", "search.placeholder": "Αναζήτηση", "search.share": "Διαμοίραση", "search.reset": "Καθαρισμός", diff --git a/material/partials/languages/en.html b/material/partials/languages/en.html index f346227b3..a7150d667 100644 --- a/material/partials/languages/en.html +++ b/material/partials/languages/en.html @@ -20,7 +20,7 @@ "nav": "Navigation", "search": "Search", "search.config.lang": "en", - "search.config.pipeline": "trimmer, stopWordFilter", + "search.config.pipeline": "stopWordFilter", "search.config.separator": "[\\s\\-]+", "search.placeholder": "Search", "search.share": "Share", diff --git a/material/partials/languages/ja.html b/material/partials/languages/ja.html index 532a527b4..88cf531b3 100644 --- a/material/partials/languages/ja.html +++ b/material/partials/languages/ja.html @@ -14,7 +14,7 @@ "meta.source": "ソース", "nav": "ナビゲーション", "search.config.lang": "ja", - "search.config.pipeline": "trimmer, stemmer", + "search.config.pipeline": "stemmer", "search.config.separator": "[\\s\\- 、。,.]+", "search.placeholder": "検索", "search.reset": "クリア", diff --git a/material/partials/languages/zh-Hant.html b/material/partials/languages/zh-Hant.html index f2aa11d65..a9e90e189 100644 --- a/material/partials/languages/zh-Hant.html +++ b/material/partials/languages/zh-Hant.html @@ -11,7 +11,7 @@ "meta.comments": "評論", "meta.source": "來源", "search.config.lang": "ja", - "search.config.pipeline": "trimmer, stemmer", + "search.config.pipeline": "stemmer", "search.config.separator": "[\\s\\-,。]+", "search.placeholder": "搜尋", "search.result.initializer": "正在初始化搜尋引擎", diff --git a/material/partials/languages/zh-TW.html b/material/partials/languages/zh-TW.html index 4f1b12a87..179c35644 100644 --- a/material/partials/languages/zh-TW.html +++ b/material/partials/languages/zh-TW.html @@ -15,7 +15,7 @@ "meta.comments": "留言", "meta.source": "來源", "search.config.lang": "ja", - "search.config.pipeline": "trimmer, stemmer", + "search.config.pipeline": "stemmer", "search.config.separator": "[\\s\\- 、。,.?;]+", "search.placeholder": "搜尋", "search.result.initializer": "正在初始化搜尋引擎", diff --git a/material/partials/languages/zh.html b/material/partials/languages/zh.html index 4514368f5..4ee6adb1e 100644 --- a/material/partials/languages/zh.html +++ b/material/partials/languages/zh.html @@ -19,7 +19,7 @@ "nav": "导航栏", "search": "查找", "search.config.lang": "ja", - "search.config.pipeline": "trimmer, stemmer", + "search.config.pipeline": "stemmer", "search.config.separator": "[\\s\\-,。]+", "search.placeholder": "搜索", "search.share": "分享", diff --git a/material/plugins/offline/__init__.py b/material/plugins/offline/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/material/plugins/offline/plugin.py b/material/plugins/offline/plugin.py new file mode 100644 index 000000000..f3ca162d4 --- /dev/null +++ b/material/plugins/offline/plugin.py @@ -0,0 +1,69 @@ +# Copyright (c) 2016-2022 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +import os + +from mkdocs import utils +from mkdocs.config import config_options as opt +from mkdocs.config.base import Config +from mkdocs.plugins import BasePlugin, event_priority + +# ----------------------------------------------------------------------------- +# Class +# ----------------------------------------------------------------------------- + +# Offline plugin configuration scheme +class OfflinePluginConfig(Config): + enabled = opt.Type(bool, default = True) + +# ----------------------------------------------------------------------------- + +# Offline plugin +class OfflinePlugin(BasePlugin[OfflinePluginConfig]): + + # Initialize plugin + def on_config(self, config): + if not self.config.enabled: + return + + # Ensure correct resolution of links + config.use_directory_urls = False + + # Support offline search (run latest) + @event_priority(-100) + def on_post_build(self, *, config): + if not self.config.enabled: + return + + # Check for existence of search index + base = os.path.join(config.site_dir, "search") + path = os.path.join(base, "search_index.json") + if not os.path.exists(path): + return + + # Retrieve search index + with open(path, "r") as data: + index = data.read() + + # Inline search index into script + utils.write_file( + f"var __index = {index}".encode("utf-8"), + os.path.join(base, "search_index.js") + ) diff --git a/material/plugins/search/plugin.py b/material/plugins/search/plugin.py index 62683d4d2..bc2dea594 100644 --- a/material/plugins/search/plugin.py +++ b/material/plugins/search/plugin.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016-2021 Martin Donath +# Copyright (c) 2016-2022 Martin Donath # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to @@ -18,54 +18,445 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. +import json import logging +import os +import regex as re +from html import escape +from html.parser import HTMLParser +from mkdocs import utils from mkdocs.commands.build import DuplicateFilter -from mkdocs.contrib.search import SearchPlugin as BasePlugin -from mkdocs.contrib.search.search_index import SearchIndex as BaseIndex +from mkdocs.config import config_options as opt +from mkdocs.config.base import Config +from mkdocs.contrib.search import LangOption +from mkdocs.plugins import BasePlugin # ----------------------------------------------------------------------------- # Class # ----------------------------------------------------------------------------- -# Search plugin with custom search index -class SearchPlugin(BasePlugin): +# Search plugin configuration scheme +class SearchPluginConfig(Config): + lang = opt.Optional(LangOption()) + separator = opt.Optional(opt.Type(str)) + pipeline = opt.ListOfItems( + opt.Choice(("stemmer", "stopWordFilter", "trimmer")), + default = [] + ) - # Override to use a custom search index - def on_pre_build(self, config): - super().on_pre_build(config) + # Deprecated options + indexing = opt.Deprecated(message = "Unsupported option") + prebuild_index = opt.Deprecated(message = "Unsupported option") + min_search_length = opt.Deprecated(message = "Unsupported option") + +# ----------------------------------------------------------------------------- + +# Search plugin +class SearchPlugin(BasePlugin[SearchPluginConfig]): + + # Determine whether we're running under dirty reload + def on_startup(self, *, command, dirty): + self.is_dirtyreload = False + self.is_dirty = dirty + + # Initialize search index cache + self.search_index_prev = None + + # Initialize plugin + def on_config(self, config): + if not self.config.lang: + self.config.lang = [self._translate( + config, "search.config.lang" + )] + + # Retrieve default value for separator + if not self.config.separator: + self.config.separator = self._translate( + config, "search.config.separator" + ) + + # Retrieve default value for pipeline + if not self.config.pipeline: + self.config.pipeline = list(filter(len, re.split( + r"\s*,\s*", self._translate(config, "search.config.pipeline") + ))) + + # Initialize search index self.search_index = SearchIndex(**self.config) + # Add page to search index + def on_page_context(self, context, *, page, config, nav): + self.search_index.add_entry_from_context(page) + page.content = re.sub( + r"\s?data-search-\w+=\"[^\"]+\"", + "", + page.content + ) + + # Generate search index + def on_post_build(self, *, config): + base = os.path.join(config.site_dir, "search") + path = os.path.join(base, "search_index.json") + + # Generate and write search index to file + data = self.search_index.generate_search_index(self.search_index_prev) + utils.write_file(data.encode("utf-8"), path) + + # Persist search index for repeated invocation + if self.is_dirty: + self.search_index_prev = self.search_index + + # Determine whether we're running under dirty reload + def on_serve(self, server, *, config, builder): + self.is_dirtyreload = self.is_dirty + + # ------------------------------------------------------------------------- + + # Translate the given placeholder value + def _translate(self, config, value): + env = config.theme.get_env() + + # Load language template and return translation for placeholder + language = "partials/language.html" + template = env.get_template(language, None, { "config": config }) + return template.module.t(value) + # ----------------------------------------------------------------------------- # Search index with support for additional fields -class SearchIndex(BaseIndex): +class SearchIndex: - # Override to add additional fields for each page + # Initialize search index + def __init__(self, **config): + self.config = config + self.entries = [] + + # Add page to search index def add_entry_from_context(self, page): - index = len(self._entries) - super().add_entry_from_context(page) + search = page.meta.get("search", {}) + if search.get("exclude"): + return - # Add document tags, if any - if page.meta.get("tags"): - if type(page.meta["tags"]) is list: - entry = self._entries[index] - entry["tags"] = [ - str(tag) for tag in page.meta["tags"] - ] + # Divide page content into sections + parser = Parser() + parser.feed(page.content) + parser.close() + + # Add sections to index + for section in parser.data: + if not section.is_excluded(): + self.create_entry_for_section(section, page.toc, page.url, page) + + # Override: graceful indexing and additional fields + def create_entry_for_section(self, section, toc, url, page): + item = self._find_toc_by_id(toc, section.id) + if item: + url = url + item.url + elif section.id: + url = url + "#" + section.id + + # Set page title as section title if none was given, which happens when + # the first headline in a Markdown document is not a h1 headline. Also, + # if a page title was set via front matter, use that even though a h1 + # might be given or the page name was specified in nav in mkdocs.yml + if not section.title: + section.title = page.meta.get("title", page.title) + + # Compute title and text + title = "".join(section.title).strip() + text = "".join(section.text).strip() + + # Reset text, if only titles should be indexed + if self.config["indexing"] == "titles": + text = "" + + # Create entry for section + entry = { + "title": title, + "text": text, + "location": url + } + + # Set document tags + tags = page.meta.get("tags") + if isinstance(tags, list): + entry["tags"] = [] + for name in tags: + if name and isinstance(name, (str, int, float, bool)): + entry["tags"].append(name) + + # Set document boost + search = page.meta.get("search", {}) + if "boost" in search: + entry["boost"] = search["boost"] + + # Add entry to index + self.entries.append(entry) + + # Generate search index + def generate_search_index(self, prev): + config = { + key: self.config[key] + for key in ["lang", "separator", "pipeline"] + } + + # Hack: if we're running under dirty reload, the search index will only + # include the entries for the current page. However, MkDocs > 1.4 allows + # us to persist plugin state across rebuilds, which is exactly what we + # do by passing the previously built index to this method. Thus, we just + # remove the previous entries for the current page, and append the new + # entries to the end of the index, as order doesn't matter. + if prev and self.entries: + path = self.entries[0]["location"] + + # Since we're sure that we're running under dirty reload, the list + # of entries will only contain sections for a single page. Thus, we + # use the first entry to remove all entries from the previous run + # that belong to the current page. The rationale behind this is that + # authors might add or remove section headers, so we need to make + # sure that sections are synchronized correctly. + entries = [ + entry for entry in prev.entries + if not entry["location"].startswith(path) + ] + + # Merge previous with current entries + self.entries = entries + self.entries + + # Otherwise just set previous entries + if prev and not self.entries: + self.entries = prev.entries + + # Return search index as JSON + data = { "config": config, "docs": self.entries } + return json.dumps( + data, + separators = (",", ":"), + default = str + ) + + # ------------------------------------------------------------------------- + + # Retrieve item for anchor + def _find_toc_by_id(self, toc, id): + for toc_item in toc: + if toc_item.id == id: + return toc_item + + # Recurse into children of item + toc_item = self._find_toc_by_id(toc_item.children, id) + if toc_item is not None: + return toc_item + + # No item found + return None + +# ----------------------------------------------------------------------------- + +# HTML element +class Element: + """ + An element with attributes, essentially a small wrapper object for the + parser to access attributes in other callbacks than handle_starttag. + """ + + # Initialize HTML element + def __init__(self, tag, attrs = dict()): + self.tag = tag + self.attrs = attrs + + # Support comparison (compare by tag only) + def __eq__(self, other): + if other is Element: + return self.tag == other.tag + else: + return self.tag == other + + # Support set operations + def __hash__(self): + return hash(self.tag) + + # Check whether the element should be excluded + def is_excluded(self): + return "data-search-exclude" in self.attrs + +# ----------------------------------------------------------------------------- + +# HTML section +class Section: + """ + A block of text with markup, preceded by a title (with markup), i.e., a + headline with a certain level (h1-h6). Internally used by the parser. + """ + + # Initialize HTML section + def __init__(self, el): + self.el = el + self.text = [] + self.title = [] + self.id = None + + # Check whether the section should be excluded + def is_excluded(self): + return self.el.is_excluded() + +# ----------------------------------------------------------------------------- + +# HTML parser +class Parser(HTMLParser): + """ + This parser divides the given string of HTML into a list of sections, each + of which are preceded by a h1-h6 level heading. A white- and blacklist of + tags dictates which tags should be preserved as part of the index, and + which should be ignored in their entirety. + """ + + # Initialize HTML parser + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Tags to skip + self.skip = set([ + "object", # Objects + "script", # Scripts + "style" # Styles + ]) + + # Tags to keep + self.keep = set([ + "p", # Paragraphs + "code", "pre", # Code blocks + "li", "ol", "ul" # Lists + ]) + + # Current context and section + self.context = [] + self.section = None + + # All parsed sections + self.data = [] + + # Called at the start of every HTML tag + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + + # Ignore self-closing tags + el = Element(tag, attrs) + if not tag in void: + self.context.append(el) + else: + return + + # Handle headings + if tag in ([f"h{x}" for x in range(1, 7)]): + if "id" in attrs: + + # Ensure top-level section + if tag != "h1" and not self.data: + self.section = Section(Element("hx")) + self.data.append(self.section) + + # Set identifier, if not first section + self.section = Section(el) + if self.data: + self.section.id = attrs["id"] + + # Append section to list + self.data.append(self.section) + + # Handle preface - ensure top-level section + if not self.section: + self.section = Section(Element("hx")) + self.data.append(self.section) + + # Handle special cases to skip + for key, value in attrs.items(): + + # Skip block if explicitly excluded from search + if key == "data-search-exclude": + self.skip.add(el) + return + + # Skip line numbers - see https://bit.ly/3GvubZx + if key == "class" and value == "linenodiv": + self.skip.add(el) + return + + # Render opening tag if kept + if not self.skip.intersection(self.context): + if tag in self.keep: + data = self.section.text + if self.section.el in reversed(self.context): + data = self.section.title + + # Append to section title or text + data.append(f"<{tag}>") + + # Called at the end of every HTML tag + def handle_endtag(self, tag): + if not self.context or self.context[-1] != tag: + return + + # Remove element from skip list + el = self.context.pop() + if el in self.skip: + self.skip.remove(el) + return + + # Render closing tag if kept + if not self.skip.intersection(self.context): + if tag in self.keep: + data = self.section.text + if self.section.el in reversed(self.context): + data = self.section.title + + # Remove element if empty (or only whitespace) + prev, last = data[-2:] + if last == f"<{tag}>": + del data[len(data) - 1:] + elif last.isspace() and prev == f"<{tag}>": + del data[len(data) - 2:] + + # Append to section title or text + else: + data.append(f"") + + # Called for the text contents of each tag + def handle_data(self, data): + if self.skip.intersection(self.context): + return + + # Collapse whitespace in non-pre contexts + if not "pre" in self.context: + if not data.isspace(): + data = data.replace("\n", " ") else: - log.warning( - "Skipping 'tags' due to invalid syntax [%s]: %s", - page.file.src_uri, - page.meta["tags"] + data = " " + + # Handle preface - ensure top-level section + if not self.section: + self.section = Section(Element("hx")) + self.data.append(self.section) + + # Handle section headline + if self.section.el in reversed(self.context): + permalink = False + for el in self.context: + if el.tag == "a" and el.attrs.get("class") == "headerlink": + permalink = True + + # Ignore permalinks + if not permalink: + self.section.title.append( + escape(data, quote = False) ) - # Add document boost for search - if "search" in page.meta: - search = page.meta["search"] - if "boost" in search: - for entry in self._entries[index:]: - entry["boost"] = search["boost"] + # Handle everything else + else: + self.section.text.append( + escape(data, quote = False) + ) # ----------------------------------------------------------------------------- # Data @@ -74,3 +465,21 @@ class SearchIndex(BaseIndex): # Set up logging log = logging.getLogger("mkdocs") log.addFilter(DuplicateFilter()) + +# Tags that are self-closing +void = set([ + "area", # Image map areas + "base", # Document base + "br", # Line breaks + "col", # Table columns + "embed", # External content + "hr", # Horizontal rules + "img", # Images + "input", # Input fields + "link", # Links + "meta", # Metadata + "param", # External parameters + "source", # Image source sets + "track", # Text track + "wbr" # Line break opportunities +]) diff --git a/material/plugins/social/plugin.py b/material/plugins/social/plugin.py index f5727fc78..76bf1c7d6 100644 --- a/material/plugins/social/plugin.py +++ b/material/plugins/social/plugin.py @@ -80,7 +80,7 @@ class SocialPlugin(BasePlugin[SocialPluginConfig]): "Required dependencies of \"social\" plugin not found. " "Install with: pip install pillow cairosvg" ) - sys.exit() + sys.exit(1) # Check if site URL is defined if not config.site_url: diff --git a/material/plugins/tags/plugin.py b/material/plugins/tags/plugin.py index a040e68c6..e01818a63 100644 --- a/material/plugins/tags/plugin.py +++ b/material/plugins/tags/plugin.py @@ -92,7 +92,7 @@ class TagsPlugin(BasePlugin[TagsPluginConfig]): file = files.get_file_from_path(path) if not file: log.error(f"Tags file '{path}' does not exist.") - sys.exit() + sys.exit(1) # Add tags file to files files.append(file) diff --git a/mkdocs.yml b/mkdocs.yml index ae2ef5017..6b89d3542 100755 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -81,7 +81,8 @@ theme: # Plugins plugins: - - search + - search: + separator: '[\s\u200b,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' - redirects: redirect_maps: changelog/insiders.md: insiders/changelog.md diff --git a/package-lock.json b/package-lock.json index 163ed6bf2..1f7d48262 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "escape-html": "^1.0.3", "focus-visible": "^5.2.0", "fuzzaldrin-plus": "^0.6.0", + "iframe-worker": "^1.0.0", "lunr": "^2.3.9", "lunr-languages": "^1.10.0", "resize-observer-polyfill": "^1.5.1", @@ -5733,6 +5734,14 @@ "node": ">=0.10.0" } }, + "node_modules/iframe-worker": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/iframe-worker/-/iframe-worker-1.0.0.tgz", + "integrity": "sha512-kZcAynPvvsaMUh7nj89dCi6dmyjwgX6mlg3y28IUF1gdQpPX44+l0MP+4UFChfQmCdMy01EPkJ+joNuXOh0eWQ==", + "engines": { + "node": ">= 16" + } + }, "node_modules/ignore": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", @@ -17764,6 +17773,11 @@ "safer-buffer": ">= 2.1.2 < 3.0.0" } }, + "iframe-worker": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/iframe-worker/-/iframe-worker-1.0.0.tgz", + "integrity": "sha512-kZcAynPvvsaMUh7nj89dCi6dmyjwgX6mlg3y28IUF1gdQpPX44+l0MP+4UFChfQmCdMy01EPkJ+joNuXOh0eWQ==" + }, "ignore": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", diff --git a/package.json b/package.json index 3b9406b9e..846cd6c1f 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "escape-html": "^1.0.3", "focus-visible": "^5.2.0", "fuzzaldrin-plus": "^0.6.0", + "iframe-worker": "^1.0.0", "lunr": "^2.3.9", "lunr-languages": "^1.10.0", "resize-observer-polyfill": "^1.5.1", diff --git a/pyproject.toml b/pyproject.toml index 7767daf50..7cc386502 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ classifiers = [ ] [project.entry-points."mkdocs.plugins"] +"material/offline" = "material.plugins.search.plugin:OfflinePlugin" "material/search" = "material.plugins.search.plugin:SearchPlugin" "material/social" = "material.plugins.social.plugin:SocialPlugin" "material/tags" = "material.plugins.tags.plugin:TagsPlugin" diff --git a/requirements.txt b/requirements.txt index 992231f55..5f62572fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,8 @@ markdown>=3.2 mkdocs>=1.4.2 mkdocs-material-extensions>=1.1 pygments>=2.12 -pymdown-extensions>=9.4 +pymdown-extensions>=9.6 # Requirements for plugins +regex>=2022.4.24 requests>=2.26 diff --git a/src/assets/javascripts/_/index.ts b/src/assets/javascripts/_/index.ts index 201692db7..88eb35510 100644 --- a/src/assets/javascripts/_/index.ts +++ b/src/assets/javascripts/_/index.ts @@ -58,10 +58,6 @@ export type Flag = export type Translation = | "clipboard.copy" /* Copy to clipboard */ | "clipboard.copied" /* Copied to clipboard */ - | "search.config.lang" /* Search language */ - | "search.config.pipeline" /* Search pipeline */ - | "search.config.separator" /* Search separator */ - | "search.placeholder" /* Search */ | "search.result.placeholder" /* Type to start searching */ | "search.result.none" /* No matching documents */ | "search.result.one" /* 1 matching document */ @@ -74,7 +70,8 @@ export type Translation = /** * Translations */ -export type Translations = Record +export type Translations = + Record /* ------------------------------------------------------------------------- */ diff --git a/src/assets/javascripts/browser/element/size/content/index.ts b/src/assets/javascripts/browser/element/size/content/index.ts index fd44ae53a..ef1f3b4a2 100644 --- a/src/assets/javascripts/browser/element/size/content/index.ts +++ b/src/assets/javascripts/browser/element/size/content/index.ts @@ -55,7 +55,7 @@ export function getElementContainer( let parent = el.parentElement while (parent) if ( - el.scrollWidth <= parent.scrollWidth && + el.scrollWidth <= parent.scrollWidth && el.scrollHeight <= parent.scrollHeight ) parent = (el = parent).parentElement diff --git a/src/assets/javascripts/browser/keyboard/index.ts b/src/assets/javascripts/browser/keyboard/index.ts index 90760a317..ad9527936 100644 --- a/src/assets/javascripts/browser/keyboard/index.ts +++ b/src/assets/javascripts/browser/keyboard/index.ts @@ -21,11 +21,15 @@ */ import { + EMPTY, Observable, filter, fromEvent, map, - share + merge, + share, + startWith, + switchMap } from "rxjs" import { getActiveElement } from "../element" @@ -93,13 +97,28 @@ function isSusceptibleToKeyboard( * Functions * ------------------------------------------------------------------------- */ +/** + * Watch composition events + * + * @returns Composition observable + */ +export function watchComposition(): Observable { + return merge( + fromEvent(window, "compositionstart").pipe(map(() => true)), + fromEvent(window, "compositionend").pipe(map(() => false)) + ) + .pipe( + startWith(false) + ) +} + /** * Watch keyboard * * @returns Keyboard observable */ export function watchKeyboard(): Observable { - return fromEvent(window, "keydown") + const keyboard$ = fromEvent(window, "keydown") .pipe( filter(ev => !(ev.metaKey || ev.ctrlKey)), map(ev => ({ @@ -120,4 +139,10 @@ export function watchKeyboard(): Observable { }), share() ) + + /* Don't emit during composition events - see https://bit.ly/3te3Wl8 */ + return watchComposition() + .pipe( + switchMap(active => !active ? keyboard$ : EMPTY) + ) } diff --git a/src/assets/javascripts/browser/request/index.ts b/src/assets/javascripts/browser/request/index.ts index 882a5cb0e..ec91746e0 100644 --- a/src/assets/javascripts/browser/request/index.ts +++ b/src/assets/javascripts/browser/request/index.ts @@ -60,6 +60,8 @@ export function request( ) } +/* ------------------------------------------------------------------------- */ + /** * Fetch JSON from the given URL * diff --git a/src/assets/javascripts/browser/script/index.ts b/src/assets/javascripts/browser/script/index.ts index 16054defa..40d436550 100644 --- a/src/assets/javascripts/browser/script/index.ts +++ b/src/assets/javascripts/browser/script/index.ts @@ -42,7 +42,7 @@ import { h } from "~/utilities" * Create and load a `script` element * * This function returns an observable that will emit when the script was - * successfully loaded, or throw an error if it didn't. + * successfully loaded, or throw an error if it wasn't. * * @param src - Script URL * diff --git a/src/assets/javascripts/browser/worker/index.ts b/src/assets/javascripts/browser/worker/index.ts index 362f1f2ab..e42e4ef1a 100644 --- a/src/assets/javascripts/browser/worker/index.ts +++ b/src/assets/javascripts/browser/worker/index.ts @@ -20,15 +20,16 @@ * IN THE SOFTWARE. */ +import "iframe-worker/shim" import { Observable, Subject, + endWith, fromEvent, - map, + ignoreElements, + mergeWith, share, - switchMap, - tap, - throttle + takeUntil } from "rxjs" /* ---------------------------------------------------------------------------- @@ -43,29 +44,38 @@ export interface WorkerMessage { data?: unknown /* Message data */ } -/** - * Worker handler - * - * @template T - Message type - */ -export interface WorkerHandler< - T extends WorkerMessage -> { - tx$: Subject /* Message transmission subject */ - rx$: Observable /* Message receive observable */ -} - /* ---------------------------------------------------------------------------- - * Helper types + * Helper functions * ------------------------------------------------------------------------- */ /** - * Watch options + * Create an observable for receiving from a web worker * - * @template T - Worker message type + * @template T - Data type + * + * @param worker - Web worker + * + * @returns Message observable */ -interface WatchOptions { - tx$: Observable /* Message transmission observable */ +function recv(worker: Worker): Observable { + return fromEvent, T>(worker, "message", ev => ev.data) +} + +/** + * Create a subject for sending to a web worker + * + * @template T - Data type + * + * @param worker - Web worker + * + * @returns Message subject + */ +function send(worker: Worker): Subject { + const send$ = new Subject() + send$.subscribe(data => worker.postMessage(data)) + + /* Return message subject */ + return send$ } /* ---------------------------------------------------------------------------- @@ -73,34 +83,31 @@ interface WatchOptions { * ------------------------------------------------------------------------- */ /** - * Watch a web worker + * Create a bidirectional communication channel to a web worker * - * This function returns an observable that sends all values emitted by the - * message observable to the web worker. Web worker communication is expected - * to be bidirectional (request-response) and synchronous. Messages that are - * emitted during a pending request are throttled, the last one is emitted. + * @template T - Data type * - * @param worker - Web worker - * @param options - Options + * @param url - Worker URL + * @param worker - Worker * - * @returns Worker message observable + * @returns Worker subject */ export function watchWorker( - worker: Worker, { tx$ }: WatchOptions -): Observable { + url: string, worker = new Worker(url) +): Subject { + const recv$ = recv(worker) + const send$ = send(worker) - /* Intercept messages from worker-like objects */ - const rx$ = fromEvent(worker, "message") - .pipe( - map(({ data }) => data as T) - ) + /* Create worker subject and forward messages */ + const worker$ = new Subject() + worker$.subscribe(send$) - /* Send and receive messages, return hot observable */ - return tx$ + /* Return worker subject */ + const done$ = send$.pipe(ignoreElements(), endWith(true)) + return worker$ .pipe( - throttle(() => rx$, { leading: true, trailing: true }), - tap(message => worker.postMessage(message)), - switchMap(() => rx$), + ignoreElements(), + mergeWith(recv$.pipe(takeUntil(done$))), share() - ) + ) as Subject } diff --git a/src/assets/javascripts/bundle.ts b/src/assets/javascripts/bundle.ts index e0a38b531..9e3030c01 100644 --- a/src/assets/javascripts/bundle.ts +++ b/src/assets/javascripts/bundle.ts @@ -28,6 +28,7 @@ import "url-polyfill" import { EMPTY, NEVER, + Observable, Subject, defer, delay, @@ -51,6 +52,7 @@ import { watchLocationTarget, watchMedia, watchPrint, + watchScript, watchViewport } from "./browser" import { @@ -86,6 +88,32 @@ import { } from "./patches" import "./polyfills" +/* ---------------------------------------------------------------------------- + * Functions - @todo refactor + * ------------------------------------------------------------------------- */ + +/** + * Fetch search index + * + * @returns Search index observable + */ +function fetchSearchIndex(): Observable { + if (location.protocol === "file:") { + return watchScript( + `${new URL("search/search_index.js", config.base)}` + ) + .pipe( + // @ts-ignore - @todo fix typings + map(() => __index), + shareReplay(1) + ) + } else { + return requestJSON( + new URL("search/search_index.json", config.base) + ) + } +} + /* ---------------------------------------------------------------------------- * Application * ------------------------------------------------------------------------- */ @@ -109,9 +137,7 @@ const print$ = watchPrint() /* Retrieve search index, if search is enabled */ const config = configuration() const index$ = document.forms.namedItem("search") - ? __search?.index || requestJSON( - new URL("search/search_index.json", config.base) - ) + ? fetchSearchIndex() : NEVER /* Set up Clipboard.js integration */ diff --git a/src/assets/javascripts/components/content/annotation/_/index.ts b/src/assets/javascripts/components/content/annotation/_/index.ts index 79e66bfe9..57a6c61f5 100644 --- a/src/assets/javascripts/components/content/annotation/_/index.ts +++ b/src/assets/javascripts/components/content/annotation/_/index.ts @@ -29,14 +29,15 @@ import { debounceTime, defer, delay, + endWith, filter, finalize, fromEvent, + ignoreElements, map, merge, switchMap, take, - takeLast, takeUntil, tap, throttleTime, @@ -136,7 +137,7 @@ export function mountAnnotation( /* Mount component on subscription */ return defer(() => { const push$ = new Subject() - const done$ = push$.pipe(takeLast(1)) + const done$ = push$.pipe(ignoreElements(), endWith(true)) push$.subscribe({ /* Handle emission */ diff --git a/src/assets/javascripts/components/content/annotation/list/index.ts b/src/assets/javascripts/components/content/annotation/list/index.ts index a8e8bba6f..ccec7a671 100644 --- a/src/assets/javascripts/components/content/annotation/list/index.ts +++ b/src/assets/javascripts/components/content/annotation/list/index.ts @@ -25,10 +25,11 @@ import { Observable, Subject, defer, + endWith, finalize, + ignoreElements, merge, share, - takeLast, takeUntil } from "rxjs" @@ -167,7 +168,7 @@ export function mountAnnotationList( /* Handle print mode - see https://bit.ly/3rgPdpt */ print$ .pipe( - takeUntil(done$.pipe(takeLast(1))) + takeUntil(done$.pipe(ignoreElements(), endWith(true))) ) .subscribe(active => { el.hidden = !active diff --git a/src/assets/javascripts/components/content/tabs/index.ts b/src/assets/javascripts/components/content/tabs/index.ts index da139940f..865e9a918 100644 --- a/src/assets/javascripts/components/content/tabs/index.ts +++ b/src/assets/javascripts/components/content/tabs/index.ts @@ -28,8 +28,10 @@ import { auditTime, combineLatest, defer, + endWith, finalize, fromEvent, + ignoreElements, map, merge, skip, @@ -135,7 +137,7 @@ export function mountContentTabs( const container = getElement(".tabbed-labels", el) return defer(() => { const push$ = new Subject() - const done$ = push$.pipe(takeLast(1)) + const done$ = push$.pipe(ignoreElements(), endWith(true)) combineLatest([push$, watchElementSize(el)]) .pipe( auditTime(1, animationFrameScheduler), diff --git a/src/assets/javascripts/components/header/_/index.ts b/src/assets/javascripts/components/header/_/index.ts index 9b2bc061b..9e63a0f6a 100644 --- a/src/assets/javascripts/components/header/_/index.ts +++ b/src/assets/javascripts/components/header/_/index.ts @@ -29,13 +29,14 @@ import { defer, distinctUntilChanged, distinctUntilKeyChanged, + endWith, filter, + ignoreElements, map, of, shareReplay, startWith, switchMap, - takeLast, takeUntil } from "rxjs" @@ -175,7 +176,7 @@ export function mountHeader( ): Observable> { return defer(() => { const push$ = new Subject
() - const done$ = push$.pipe(takeLast(1)) + const done$ = push$.pipe(ignoreElements(), endWith(true)) push$ .pipe( distinctUntilKeyChanged("active"), diff --git a/src/assets/javascripts/components/search/_/index.ts b/src/assets/javascripts/components/search/_/index.ts index 2972d46b9..a21603362 100644 --- a/src/assets/javascripts/components/search/_/index.ts +++ b/src/assets/javascripts/components/search/_/index.ts @@ -26,9 +26,7 @@ import { ObservableInput, filter, merge, - mergeWith, - sample, - take + mergeWith } from "rxjs" import { configuration } from "~/_" @@ -41,8 +39,6 @@ import { import { SearchIndex, SearchResult, - isSearchQueryMessage, - isSearchReadyMessage, setupSearchWorker } from "~/integrations" @@ -110,23 +106,12 @@ export function mountSearch( ): Observable> { const config = configuration() try { - const url = __search?.worker || config.search - const worker = setupSearchWorker(url, index$) + const worker$ = setupSearchWorker(config.search, index$) /* Retrieve query and result components */ const query = getComponentElement("search-query", el) const result = getComponentElement("search-result", el) - /* Re-emit query when search is ready */ - const { tx$, rx$ } = worker - tx$ - .pipe( - filter(isSearchQueryMessage), - sample(rx$.pipe(filter(isSearchReadyMessage))), - take(1) - ) - .subscribe(tx$.next.bind(tx$)) - /* Set up search keyboard handlers */ keyboard$ .pipe( @@ -199,7 +184,7 @@ export function mountSearch( /* Set up global keyboard handlers */ keyboard$ .pipe( - filter(({ mode }) => mode === "global"), + filter(({ mode }) => mode === "global") ) .subscribe(key => { switch (key.type) { @@ -218,9 +203,11 @@ export function mountSearch( }) /* Create and return component */ - const query$ = mountSearchQuery(query, worker) - const result$ = mountSearchResult(result, worker, { query$ }) - return merge(query$, result$) + const query$ = mountSearchQuery(query, { worker$ }) + return merge( + query$, + mountSearchResult(result, { worker$, query$ }) + ) .pipe( mergeWith( @@ -230,7 +217,7 @@ export function mountSearch( /* Search suggestions */ ...getComponentElements("search-suggest", el) - .map(child => mountSearchSuggest(child, worker, { keyboard$ })) + .map(child => mountSearchSuggest(child, { worker$, keyboard$ })) ) ) diff --git a/src/assets/javascripts/components/search/highlight/index.ts b/src/assets/javascripts/components/search/highlight/index.ts index 775b9842c..d26e5f6a9 100644 --- a/src/assets/javascripts/components/search/highlight/index.ts +++ b/src/assets/javascripts/components/search/highlight/index.ts @@ -85,7 +85,7 @@ export function mountSearchHiglight( ) ]) .pipe( - map(([index, url]) => setupSearchHighlighter(index.config, true)( + map(([index, url]) => setupSearchHighlighter(index.config)( url.searchParams.get("h")! )), map(fn => { diff --git a/src/assets/javascripts/components/search/query/index.ts b/src/assets/javascripts/components/search/query/index.ts index 3ede78cd2..670e81b12 100644 --- a/src/assets/javascripts/components/search/query/index.ts +++ b/src/assets/javascripts/components/search/query/index.ts @@ -24,24 +24,20 @@ import { Observable, Subject, combineLatest, - delay, distinctUntilChanged, distinctUntilKeyChanged, - filter, + endWith, finalize, + first, fromEvent, + ignoreElements, map, merge, - share, shareReplay, - startWith, - take, - takeLast, takeUntil, tap } from "rxjs" -import { translation } from "~/_" import { getLocation, setToggle, @@ -49,10 +45,8 @@ import { watchToggle } from "~/browser" import { + SearchMessage, SearchMessageType, - SearchQueryMessage, - SearchWorker, - defaultTransform, isSearchReadyMessage } from "~/integrations" @@ -70,6 +64,24 @@ export interface SearchQuery { focus: boolean /* Query focus */ } +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + */ +interface WatchOptions { + worker$: Subject /* Search worker */ +} + +/** + * Mount options + */ +interface MountOptions { + worker$: Subject /* Search worker */ +} + /* ---------------------------------------------------------------------------- * Functions * ------------------------------------------------------------------------- */ @@ -81,59 +93,45 @@ export interface SearchQuery { * is delayed by `1ms` so the input's empty state is allowed to propagate. * * @param el - Search query element - * @param worker - Search worker + * @param options - Options * * @returns Search query observable */ export function watchSearchQuery( - el: HTMLInputElement, { rx$ }: SearchWorker + el: HTMLInputElement, { worker$ }: WatchOptions ): Observable { - const fn = __search?.transform || defaultTransform - /* Immediately show search dialog */ + /* Support search deep linking */ const { searchParams } = getLocation() - if (searchParams.has("q")) + if (searchParams.has("q")) { setToggle("search", true) - /* Intercept query parameter (deep link) */ - const param$ = rx$ - .pipe( - filter(isSearchReadyMessage), - take(1), - map(() => searchParams.get("q") || "") - ) + /* Set query from parameter */ + el.value = searchParams.get("q")! + el.focus() - /* Remove query parameter when search is closed */ - watchToggle("search") - .pipe( - filter(active => !active), - take(1) - ) - .subscribe(() => { - const url = new URL(location.href) - url.searchParams.delete("q") - history.replaceState({}, "", `${url}`) - }) - - /* Set query from parameter */ - param$.subscribe(value => { // TODO: not ideal - find a better way - if (value) { - el.value = value - el.focus() - } - }) + /* Remove query parameter on close */ + watchToggle("search") + .pipe( + first(active => !active) + ) + .subscribe(() => { + const url = new URL(location.href) + url.searchParams.delete("q") + history.replaceState({}, "", `${url}`) + }) + } /* Intercept focus and input events */ const focus$ = watchElementFocus(el) const value$ = merge( + worker$.pipe(first(isSearchReadyMessage)), fromEvent(el, "keyup"), - fromEvent(el, "focus").pipe(delay(1)), - param$ + focus$ ) .pipe( - map(() => fn(el.value)), - startWith(""), - distinctUntilChanged(), + map(() => el.value), + distinctUntilChanged() ) /* Combine into single observable */ @@ -148,39 +146,37 @@ export function watchSearchQuery( * Mount search query * * @param el - Search query element - * @param worker - Search worker + * @param options - Options * * @returns Search query component observable */ export function mountSearchQuery( - el: HTMLInputElement, { tx$, rx$ }: SearchWorker + el: HTMLInputElement, { worker$ }: MountOptions ): Observable> { const push$ = new Subject() - const done$ = push$.pipe(takeLast(1)) + const done$ = push$.pipe(ignoreElements(), endWith(true)) - /* Handle value changes */ - push$ + /* Handle value change */ + combineLatest([ + worker$.pipe(first(isSearchReadyMessage)), + push$ + ], (_, query) => query) .pipe( - distinctUntilKeyChanged("value"), - map(({ value }): SearchQueryMessage => ({ + distinctUntilKeyChanged("value") + ) + .subscribe(({ value }) => worker$.next({ type: SearchMessageType.QUERY, data: value })) - ) - .subscribe(tx$.next.bind(tx$)) - /* Handle focus changes */ + /* Handle focus change */ push$ .pipe( distinctUntilKeyChanged("focus") ) .subscribe(({ focus }) => { - if (focus) { + if (focus) setToggle("search", focus) - el.placeholder = "" - } else { - el.placeholder = translation("search.placeholder") - } }) /* Handle reset */ @@ -191,11 +187,11 @@ export function mountSearchQuery( .subscribe(() => el.focus()) /* Create and return component */ - return watchSearchQuery(el, { tx$, rx$ }) + return watchSearchQuery(el, { worker$ }) .pipe( tap(state => push$.next(state)), finalize(() => push$.complete()), map(state => ({ ref: el, ...state })), - share() + shareReplay(1) ) } diff --git a/src/assets/javascripts/components/search/result/index.ts b/src/assets/javascripts/components/search/result/index.ts index e6cc5ace2..c6f7fd7cc 100644 --- a/src/assets/javascripts/components/search/result/index.ts +++ b/src/assets/javascripts/components/search/result/index.ts @@ -21,17 +21,22 @@ */ import { + EMPTY, Observable, Subject, bufferCount, filter, finalize, + first, + fromEvent, map, merge, + mergeMap, of, + share, skipUntil, switchMap, - take, + takeUntil, tap, withLatestFrom, zipWith @@ -40,11 +45,12 @@ import { import { translation } from "~/_" import { getElement, + getOptionalElement, watchElementBoundary } from "~/browser" import { + SearchMessage, SearchResult, - SearchWorker, isSearchReadyMessage, isSearchResultMessage } from "~/integrations" @@ -63,6 +69,7 @@ import { SearchQuery } from "../query" */ interface MountOptions { query$: Observable /* Search query observable */ + worker$: Subject /* Search worker */ } /* ---------------------------------------------------------------------------- @@ -76,13 +83,12 @@ interface MountOptions { * the vertical offset of the search result container. * * @param el - Search result list element - * @param worker - Search worker * @param options - Options * * @returns Search result list component observable */ export function mountSearchResult( - el: HTMLElement, { rx$ }: SearchWorker, { query$ }: MountOptions + el: HTMLElement, { worker$, query$ }: MountOptions ): Observable> { const push$ = new Subject() const boundary$ = watchElementBoundary(el.parentElement!) @@ -90,51 +96,43 @@ export function mountSearchResult( filter(Boolean) ) + /* Retrieve container */ + const container = el.parentElement! + /* Retrieve nested components */ const meta = getElement(":scope > :first-child", el) const list = getElement(":scope > :last-child", el) - /* Wait until search is ready */ - const ready$ = rx$ - .pipe( - filter(isSearchReadyMessage), - take(1) - ) - /* Update search result metadata */ push$ .pipe( withLatestFrom(query$), - skipUntil(ready$) + skipUntil(worker$.pipe(first(isSearchReadyMessage))) ) .subscribe(([{ items }, { value }]) => { - if (value) { - switch (items.length) { + switch (items.length) { - /* No results */ - case 0: - meta.textContent = translation("search.result.none") - break + /* No results */ + case 0: + meta.textContent = value.length + ? translation("search.result.none") + : translation("search.result.placeholder") + break - /* One result */ - case 1: - meta.textContent = translation("search.result.one") - break + /* One result */ + case 1: + meta.textContent = translation("search.result.one") + break - /* Multiple result */ - default: - meta.textContent = translation( - "search.result.other", - round(items.length) - ) - } - } else { - meta.textContent = translation("search.result.placeholder") + /* Multiple result */ + default: + const count = round(items.length) + meta.textContent = translation("search.result.other", count) } }) - /* Update search result list */ - push$ + /* Render search result item */ + const render$ = push$ .pipe( tap(() => list.innerHTML = ""), switchMap(({ items }) => merge( @@ -145,14 +143,38 @@ export function mountSearchResult( zipWith(boundary$), switchMap(([chunk]) => chunk) ) - )) + )), + map(renderSearchResultItem), + share() ) - .subscribe(result => list.appendChild( - renderSearchResultItem(result) - )) + + /* Update search result list */ + render$.subscribe(item => list.appendChild(item)) + render$ + .pipe( + mergeMap(item => { + const details = getOptionalElement("details", item) + if (typeof details === "undefined") + return EMPTY + + /* Keep position of details element stable */ + return fromEvent(details, "toggle") + .pipe( + takeUntil(push$), + map(() => details) + ) + }) + ) + .subscribe(details => { + if ( + details.open === false && + details.offsetTop <= container.scrollTop + ) + container.scrollTo({ top: details.offsetTop }) + }) /* Filter search result message */ - const result$ = rx$ + const result$ = worker$ .pipe( filter(isSearchResultMessage), map(({ data }) => data) diff --git a/src/assets/javascripts/components/search/share/index.ts b/src/assets/javascripts/components/search/share/index.ts index 6e771b169..3569b9aa9 100644 --- a/src/assets/javascripts/components/search/share/index.ts +++ b/src/assets/javascripts/components/search/share/index.ts @@ -23,9 +23,12 @@ import { Observable, Subject, + endWith, finalize, fromEvent, + ignoreElements, map, + takeUntil, tap } from "rxjs" @@ -102,6 +105,7 @@ export function mountSearchShare( el: HTMLAnchorElement, options: MountOptions ): Observable> { const push$ = new Subject() + const done$ = push$.pipe(ignoreElements(), endWith(true)) push$.subscribe(({ url }) => { el.setAttribute("data-clipboard-text", el.href) el.href = `${url}` @@ -109,7 +113,10 @@ export function mountSearchShare( /* Prevent following of link */ fromEvent(el, "click") - .subscribe(ev => ev.preventDefault()) + .pipe( + takeUntil(done$) + ) + .subscribe(ev => ev.preventDefault()) /* Create and return component */ return watchSearchShare(el, options) diff --git a/src/assets/javascripts/components/search/suggest/index.ts b/src/assets/javascripts/components/search/suggest/index.ts index 95c78abc7..8ed470bd9 100644 --- a/src/assets/javascripts/components/search/suggest/index.ts +++ b/src/assets/javascripts/components/search/suggest/index.ts @@ -37,8 +37,8 @@ import { import { Keyboard } from "~/browser" import { + SearchMessage, SearchResult, - SearchWorker, isSearchResultMessage } from "~/integrations" @@ -62,6 +62,7 @@ export interface SearchSuggest {} */ interface MountOptions { keyboard$: Observable /* Keyboard observable */ + worker$: Subject /* Search worker */ } /* ---------------------------------------------------------------------------- @@ -75,13 +76,12 @@ interface MountOptions { * on the vertical offset of the search result container. * * @param el - Search result list element - * @param worker - Search worker * @param options - Options * * @returns Search result list component observable */ export function mountSearchSuggest( - el: HTMLElement, { rx$ }: SearchWorker, { keyboard$ }: MountOptions + el: HTMLElement, { worker$, keyboard$ }: MountOptions ): Observable> { const push$ = new Subject() @@ -101,10 +101,10 @@ export function mountSearchSuggest( push$ .pipe( combineLatestWith(query$), - map(([{ suggestions }, value]) => { + map(([{ suggest }, value]) => { const words = value.split(/([\s-]+)/) - if (suggestions?.length && words[words.length - 1]) { - const last = suggestions[suggestions.length - 1] + if (suggest?.length && words[words.length - 1]) { + const last = suggest[suggest.length - 1] if (last.startsWith(words[words.length - 1])) words[words.length - 1] = last } else { @@ -138,7 +138,7 @@ export function mountSearchSuggest( }) /* Filter search result message */ - const result$ = rx$ + const result$ = worker$ .pipe( filter(isSearchResultMessage), map(({ data }) => data) diff --git a/src/assets/javascripts/components/toc/index.ts b/src/assets/javascripts/components/toc/index.ts index c2bbd9a78..ef9da9fc6 100644 --- a/src/assets/javascripts/components/toc/index.ts +++ b/src/assets/javascripts/components/toc/index.ts @@ -29,8 +29,10 @@ import { defer, distinctUntilChanged, distinctUntilKeyChanged, + endWith, filter, finalize, + ignoreElements, map, merge, of, @@ -40,7 +42,6 @@ import { skip, startWith, switchMap, - takeLast, takeUntil, tap, withLatestFrom @@ -273,7 +274,7 @@ export function mountTableOfContents( ): Observable> { return defer(() => { const push$ = new Subject() - const done$ = push$.pipe(takeLast(1)) + const done$ = push$.pipe(ignoreElements(), endWith(true)) push$.subscribe(({ prev, next }) => { /* Look forward */ diff --git a/src/assets/javascripts/components/top/index.ts b/src/assets/javascripts/components/top/index.ts index 82ee8e3a7..11eb29613 100644 --- a/src/assets/javascripts/components/top/index.ts +++ b/src/assets/javascripts/components/top/index.ts @@ -29,10 +29,10 @@ import { distinctUntilKeyChanged, endWith, finalize, + ignoreElements, map, repeat, skip, - takeLast, takeUntil, tap } from "rxjs" @@ -134,7 +134,7 @@ export function mountBackToTop( el: HTMLElement, { viewport$, header$, main$, target$ }: MountOptions ): Observable> { const push$ = new Subject() - const done$ = push$.pipe(takeLast(1)) + const done$ = push$.pipe(ignoreElements(), endWith(true)) push$.subscribe({ /* Handle emission */ diff --git a/src/assets/javascripts/integrations/search/_/.eslintrc b/src/assets/javascripts/integrations/search/_/.eslintrc deleted file mode 100644 index fd92bace6..000000000 --- a/src/assets/javascripts/integrations/search/_/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "@typescript-eslint/no-explicit-any": "off", - "no-console": "off" - } -} diff --git a/src/assets/javascripts/integrations/search/_/index.ts b/src/assets/javascripts/integrations/search/_/index.ts index 0da514b4a..caef44329 100644 --- a/src/assets/javascripts/integrations/search/_/index.ts +++ b/src/assets/javascripts/integrations/search/_/index.ts @@ -22,18 +22,21 @@ import { SearchDocument, - SearchDocumentMap, + SearchIndex, + SearchOptions, setupSearchDocumentMap -} from "../document" +} from "../config" import { - SearchHighlightFactoryFn, - setupSearchHighlighter -} from "../highlighter" -import { SearchOptions } from "../options" + Position, + PositionTable, + highlighter, + tokenize +} from "../internal" import { SearchQueryTerms, getSearchQueryTerms, - parseSearchQuery + parseSearchQuery, + transformSearchQuery } from "../query" /* ---------------------------------------------------------------------------- @@ -41,74 +44,48 @@ import { * ------------------------------------------------------------------------- */ /** - * Search index configuration + * Search item */ -export interface SearchIndexConfig { - lang: string[] /* Search languages */ - separator: string /* Search separator */ -} - -/** - * Search index document - */ -export interface SearchIndexDocument { - location: string /* Document location */ - title: string /* Document title */ - text: string /* Document text */ - tags?: string[] /* Document tags */ - boost?: number /* Document boost */ -} - -/* ------------------------------------------------------------------------- */ - -/** - * Search index - * - * This interfaces describes the format of the `search_index.json` file which - * is automatically built by the MkDocs search plugin. - */ -export interface SearchIndex { - config: SearchIndexConfig /* Search index configuration */ - docs: SearchIndexDocument[] /* Search index documents */ - options: SearchOptions /* Search options */ -} - -/* ------------------------------------------------------------------------- */ - -/** - * Search metadata - */ -export interface SearchMetadata { +export interface SearchItem extends SearchDocument { score: number /* Score (relevance) */ terms: SearchQueryTerms /* Search query terms */ } -/* ------------------------------------------------------------------------- */ - -/** - * Search result document - */ -export type SearchResultDocument = SearchDocument & SearchMetadata - -/** - * Search result item - */ -export type SearchResultItem = SearchResultDocument[] - -/* ------------------------------------------------------------------------- */ - /** * Search result */ export interface SearchResult { - items: SearchResultItem[] /* Search result items */ - suggestions?: string[] /* Search suggestions */ + items: SearchItem[][] /* Search items */ + suggest?: string[] /* Search suggestions */ } /* ---------------------------------------------------------------------------- * Functions * ------------------------------------------------------------------------- */ +/** + * Create field extractor factory + * + * @param table - Position table map + * + * @returns Extractor factory + */ +function extractor(table: Map) { + return (name: keyof SearchDocument) => { + return (doc: SearchDocument) => { + if (typeof doc[name] === "undefined") + return undefined + + /* Compute identifier and initiable table */ + const id = [doc.location, name].join(":") + table.set(id, lunr.tokenizer.table = []) + + /* Return field value */ + return doc[name] + } + } +} + /** * Compute the difference of two lists of strings * @@ -134,85 +111,78 @@ function difference(a: string[], b: string[]): string[] { export class Search { /** - * Search document mapping - * - * A mapping of URLs (including hash fragments) to the actual articles and - * sections of the documentation. The search document mapping must be created - * regardless of whether the index was prebuilt or not, as Lunr.js itself - * only stores the actual index. + * Search document map */ - protected documents: SearchDocumentMap - - /** - * Search highlight factory function - */ - protected highlight: SearchHighlightFactoryFn - - /** - * The underlying Lunr.js search index - */ - protected index: lunr.Index + protected map: Map /** * Search options */ protected options: SearchOptions + /** + * The underlying Lunr.js search index + */ + protected index: lunr.Index + + /** + * Internal position table map + */ + protected table: Map + /** * Create the search integration * * @param data - Search index */ public constructor({ config, docs, options }: SearchIndex) { + const field = extractor(this.table = new Map()) + + /* Set up document map and options */ + this.map = setupSearchDocumentMap(docs) this.options = options - /* Set up document map and highlighter factory */ - this.documents = setupSearchDocumentMap(docs) - this.highlight = setupSearchHighlighter(config, false) - - /* Set separator for tokenizer */ - lunr.tokenizer.separator = new RegExp(config.separator) - - /* Create search index */ + /* Set up document index */ this.index = lunr(function () { + this.metadataWhitelist = ["position"] + this.b(0) - /* Set up multi-language support */ + /* Set up (multi-)language support */ if (config.lang.length === 1 && config.lang[0] !== "en") { - this.use((lunr as any)[config.lang[0]]) + // @ts-expect-error - namespace indexing not supported + this.use(lunr[config.lang[0]]) } else if (config.lang.length > 1) { - this.use((lunr as any).multiLanguage(...config.lang)) + this.use(lunr.multiLanguage(...config.lang)) } + /* Set up custom tokenizer (must be after language setup) */ + this.tokenizer = tokenize as typeof lunr.tokenizer + lunr.tokenizer.separator = new RegExp(config.separator) + /* Compute functions to be removed from the pipeline */ const fns = difference([ "trimmer", "stopWordFilter", "stemmer" - ], options.pipeline) + ], config.pipeline) /* Remove functions from the pipeline for registered languages */ for (const lang of config.lang.map(language => ( - language === "en" ? lunr : (lunr as any)[language] - ))) { + // @ts-expect-error - namespace indexing not supported + language === "en" ? lunr : lunr[language] + ))) for (const fn of fns) { this.pipeline.remove(lang[fn]) this.searchPipeline.remove(lang[fn]) } - } - /* Set up reference */ + /* Set up index reference */ this.ref("location") - /* Set up fields */ - this.field("title", { boost: 1e3 }) - this.field("text") - this.field("tags", { boost: 1e6, extractor: doc => { - const { tags = [] } = doc as SearchDocument - return tags.reduce((list, tag) => [ - ...list, - ...lunr.tokenizer(tag) - ], [] as lunr.Token[]) - } }) + /* Set up index fields */ + this.field("title", { boost: 1e3, extractor: field("title") }) + this.field("text", { boost: 1e0, extractor: field("text") }) + this.field("tags", { boost: 1e6, extractor: field("tags") }) - /* Index documents */ + /* Add documents to index */ for (const doc of docs) this.add(doc, { boost: doc.boost }) }) @@ -221,105 +191,129 @@ export class Search { /** * Search for matching documents * - * The search index which MkDocs provides is divided up into articles, which - * contain the whole content of the individual pages, and sections, which only - * contain the contents of the subsections obtained by breaking the individual - * pages up at `h1` ... `h6`. As there may be many sections on different pages - * with identical titles (for example within this very project, e.g. "Usage" - * or "Installation"), they need to be put into the context of the containing - * page. For this reason, section results are grouped within their respective - * articles which are the top-level results that are returned. + * @param query - Search query * - * @param query - Query value - * - * @returns Search results + * @returns Search result */ public search(query: string): SearchResult { - if (query) { - try { - const highlight = this.highlight(query) + query = transformSearchQuery(query) + if (!query) + return { items: [] } - /* Parse query to extract clauses for analysis */ - const clauses = parseSearchQuery(query) - .filter(clause => ( - clause.presence !== lunr.Query.presence.PROHIBITED - )) + /* Parse query to extract clauses for analysis */ + const clauses = parseSearchQuery(query) + .filter(clause => ( + clause.presence !== lunr.Query.presence.PROHIBITED + )) - /* Perform search and post-process results */ - const groups = this.index.search(`${query}*`) + /* Perform search and post-process results */ + const groups = this.index.search(query) - /* Apply post-query boosts based on title and search query terms */ - .reduce((item, { ref, score, matchData }) => { - const document = this.documents.get(ref) - if (typeof document !== "undefined") { - const { location, title, text, tags, parent } = document + /* Apply post-query boosts based on title and search query terms */ + .reduce((item, { ref, score, matchData }) => { + let doc = this.map.get(ref) + if (typeof doc !== "undefined") { + doc = { ...doc } + if (doc.tags) + doc.tags = [...doc.tags] - /* Compute and analyze search query terms */ - const terms = getSearchQueryTerms( - clauses, - Object.keys(matchData.metadata) + /* Compute and analyze search query terms */ + const terms = getSearchQueryTerms( + clauses, + Object.keys(matchData.metadata) + ) + + // we must collect all positions for each term! + // we now take the keys of the index + for (const field of this.index.fields) { + if (!(field in doc)) + continue + + /* Collect matches */ + const positions: Position[] = [] + for (const match of Object.values(matchData.metadata)) + if (field in match) + positions.push(...match[field].position) + + // @ts-expect-error - @todo fix typings + if (Array.isArray(doc[field])) { + // @ts-expect-error - @todo fix typings + for (let i = 0; i < doc[field].length; i++) { + // @ts-expect-error - @todo fix typings + doc[field][i] = highlighter(doc[field][i], + this.table.get([doc.location, field].join(":"))!, + positions + ) + } + } else { + // @ts-expect-error - @todo fix typings + doc[field] = highlighter(doc[field], + this.table.get([doc.location, field].join(":"))!, + positions ) - - /* Highlight title and text and apply post-query boosts */ - const boost = +!parent + +Object.values(terms).every(t => t) - item.push({ - location, - title: highlight(title), - text: highlight(text), - ...tags && { tags: tags.map(highlight) }, - score: score * (1 + boost), - terms - }) } - return item - }, []) + } - /* Sort search results again after applying boosts */ - .sort((a, b) => b.score - a.score) + /* Highlight title and text and apply post-query boosts */ + const boost = +!doc.parent + + Object.values(terms) + .filter(t => t).length / + Object.keys(terms).length - /* Group search results by page */ - .reduce((items, result) => { - const document = this.documents.get(result.location) - if (typeof document !== "undefined") { - const ref = "parent" in document - ? document.parent!.location - : document.location - items.set(ref, [...items.get(ref) || [], result]) - } - return items - }, new Map()) - - /* Generate search suggestions, if desired */ - let suggestions: string[] | undefined - if (this.options.suggestions) { - const titles = this.index.query(builder => { - for (const clause of clauses) - builder.term(clause.term, { - fields: ["title"], - presence: lunr.Query.presence.REQUIRED, - wildcard: lunr.Query.wildcard.TRAILING - }) + /* Append item */ + item.push({ + ...doc, + score: score * (1 + boost ** 2), + terms }) - - /* Retrieve suggestions for best match */ - suggestions = titles.length - ? Object.keys(titles[0].matchData.metadata) - : [] } + return item + }, []) - /* Return items and suggestions */ - return { - items: [...groups.values()], - ...typeof suggestions !== "undefined" && { suggestions } + /* Sort search results again after applying boosts */ + .sort((a, b) => b.score - a.score) + + /* Group search results by article */ + .reduce((items, result) => { + const doc = this.map.get(result.location) + if (typeof doc !== "undefined") { + const ref = doc.parent + ? doc.parent.location + : doc.location + items.set(ref, [...items.get(ref) || [], result]) } + return items + }, new Map()) - /* Log errors to console (for now) */ - } catch { - console.warn(`Invalid query: ${query} – see https://bit.ly/2s3ChXG`) + /* Ensure that every item set has an article */ + for (const [ref, items] of groups) + if (!items.find(item => item.location === ref)) { + const doc = this.map.get(ref)! + items.push({ ...doc, score: 0, terms: {} }) } + + /* Generate search suggestions, if desired */ + let suggest: string[] | undefined + if (this.options.suggest) { + const titles = this.index.query(builder => { + for (const clause of clauses) + builder.term(clause.term, { + fields: ["title"], + presence: lunr.Query.presence.REQUIRED, + wildcard: lunr.Query.wildcard.TRAILING + }) + }) + + /* Retrieve suggestions for best match */ + suggest = titles.length + ? Object.keys(titles[0].matchData.metadata) + : [] } - /* Return nothing in case of error or empty query */ - return { items: [] } + /* Return search result */ + return { + items: [...groups.values()], + ...typeof suggest !== "undefined" && { suggest } + } } } diff --git a/src/assets/javascripts/integrations/search/config/index.ts b/src/assets/javascripts/integrations/search/config/index.ts new file mode 100644 index 000000000..87f9e8441 --- /dev/null +++ b/src/assets/javascripts/integrations/search/config/index.ts @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016-2022 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search configuration + */ +export interface SearchConfig { + lang: string[] /* Search languages */ + separator: string /* Search separator */ + pipeline: SearchPipelineFn[] /* Search pipeline */ +} + +/** + * Search document + */ +export interface SearchDocument { + location: string /* Document location */ + title: string /* Document title */ + text: string /* Document text */ + tags?: string[] /* Document tags */ + boost?: number /* Document boost */ + parent?: SearchDocument /* Document parent */ +} + +/** + * Search options + */ +export interface SearchOptions { + suggest: boolean /* Search suggestions */ +} + +/* ------------------------------------------------------------------------- */ + +/** + * Search index + */ +export interface SearchIndex { + config: SearchConfig /* Search configuration */ + docs: SearchDocument[] /* Search documents */ + options: SearchOptions /* Search options */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Search pipeline function + */ +type SearchPipelineFn = + | "trimmer" /* Trimmer */ + | "stopWordFilter" /* Stop word filter */ + | "stemmer" /* Stemmer */ + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Create a search document map + * + * This function creates a mapping of URLs (including anchors) to the actual + * articles and sections. It relies on the invariant that the search index is + * ordered with the main article appearing before all sections with anchors. + * If this is not the case, the logic music be changed. + * + * @param docs - Search documents + * + * @returns Search document map + */ +export function setupSearchDocumentMap( + docs: SearchDocument[] +): Map { + const map = new Map() + for (const doc of docs) { + const [path] = doc.location.split("#") + + /* Add document article */ + const article = map.get(path) + if (typeof article === "undefined") { + map.set(path, doc) + + /* Add document section */ + } else { + map.set(doc.location, doc) + doc.parent = article + } + } + + /* Return search document map */ + return map +} diff --git a/src/assets/javascripts/integrations/search/document/index.ts b/src/assets/javascripts/integrations/search/document/index.ts deleted file mode 100644 index 2526d6c21..000000000 --- a/src/assets/javascripts/integrations/search/document/index.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2016-2022 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import escapeHTML from "escape-html" - -import { SearchIndexDocument } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search document - */ -export interface SearchDocument extends SearchIndexDocument { - parent?: SearchIndexDocument /* Parent article */ -} - -/* ------------------------------------------------------------------------- */ - -/** - * Search document mapping - */ -export type SearchDocumentMap = Map - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Create a search document mapping - * - * @param docs - Search index documents - * - * @returns Search document map - */ -export function setupSearchDocumentMap( - docs: SearchIndexDocument[] -): SearchDocumentMap { - const documents = new Map() - const parents = new Set() - for (const doc of docs) { - const [path, hash] = doc.location.split("#") - - /* Extract location, title and tags */ - const location = doc.location - const title = doc.title - const tags = doc.tags - - /* Escape and cleanup text */ - const text = escapeHTML(doc.text) - .replace(/\s+(?=[,.:;!?])/g, "") - .replace(/\s+/g, " ") - - /* Handle section */ - if (hash) { - const parent = documents.get(path)! - - /* Ignore first section, override article */ - if (!parents.has(parent)) { - parent.title = doc.title - parent.text = text - - /* Remember that we processed the article */ - parents.add(parent) - - /* Add subsequent section */ - } else { - documents.set(location, { - location, - title, - text, - parent - }) - } - - /* Add article */ - } else { - documents.set(location, { - location, - title, - text, - ...tags && { tags } - }) - } - } - return documents -} diff --git a/src/assets/javascripts/integrations/search/highlighter/index.ts b/src/assets/javascripts/integrations/search/highlighter/index.ts index b1d9c4ca0..990815e0a 100644 --- a/src/assets/javascripts/integrations/search/highlighter/index.ts +++ b/src/assets/javascripts/integrations/search/highlighter/index.ts @@ -22,7 +22,7 @@ import escapeHTML from "escape-html" -import { SearchIndexConfig } from "../_" +import { SearchConfig } from "../config" /* ---------------------------------------------------------------------------- * Types @@ -53,15 +53,21 @@ export type SearchHighlightFactoryFn = (query: string) => SearchHighlightFn /** * Create a search highlighter * - * @param config - Search index configuration - * @param escape - Whether to escape HTML + * @param config - Search configuration * * @returns Search highlight factory function */ export function setupSearchHighlighter( - config: SearchIndexConfig, escape: boolean + config: SearchConfig ): SearchHighlightFactoryFn { - const separator = new RegExp(config.separator, "img") + // Hack: temporarily remove pure lookaheads + const regex = config.separator.split("|").map(term => { + const temp = term.replace(/(\(\?[!=][^)]+\))/g, "") + return temp.length === 0 ? "�" : term + }) + .join("|") + + const separator = new RegExp(regex, "img") const highlight = (_: unknown, data: string, term: string) => { return `${data}${term}` } @@ -73,19 +79,15 @@ export function setupSearchHighlighter( .trim() /* Create search term match expression */ - const match = new RegExp(`(^|${config.separator})(${ + const match = new RegExp(`(^|${config.separator}|)(${ query .replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&") .replace(separator, "|") })`, "img") /* Highlight string value */ - return value => ( - escape - ? escapeHTML(value) - : value - ) - .replace(match, highlight) - .replace(/<\/mark>(\s+)]*>/img, "$1") + return value => escapeHTML(value) + .replace(match, highlight) + .replace(/<\/mark>(\s+)]*>/img, "$1") } } diff --git a/src/assets/javascripts/integrations/search/index.ts b/src/assets/javascripts/integrations/search/index.ts index 125ac6add..71adf1265 100644 --- a/src/assets/javascripts/integrations/search/index.ts +++ b/src/assets/javascripts/integrations/search/index.ts @@ -21,8 +21,7 @@ */ export * from "./_" -export * from "./document" +export * from "./config" export * from "./highlighter" -export * from "./options" export * from "./query" export * from "./worker" diff --git a/src/assets/javascripts/integrations/search/internal/.eslintrc b/src/assets/javascripts/integrations/search/internal/.eslintrc new file mode 100644 index 000000000..9368ceb63 --- /dev/null +++ b/src/assets/javascripts/integrations/search/internal/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "no-fallthrough": "off", + "no-underscore-dangle": "off" + } +} diff --git a/src/assets/javascripts/integrations/search/internal/_/index.ts b/src/assets/javascripts/integrations/search/internal/_/index.ts new file mode 100644 index 000000000..513b2e485 --- /dev/null +++ b/src/assets/javascripts/integrations/search/internal/_/index.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2016-2022 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Visitor function + * + * @param start - Start offset + * @param end - End offset + */ +type VisitorFn = ( + start: number, end: number +) => void + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Split a string using the given separator + * + * This function intentionally takes a visitor function contrary to collecting + * and returning all ranges, as it's significantly more memory efficient. + * + * @param value - String value + * @param separator - Separator + * @param fn - Visitor function + */ +export function split( + value: string, separator: RegExp, fn: VisitorFn +): void { + separator = new RegExp(separator, "g") + + /* Split string using separator */ + let match: RegExpExecArray | null + let index = 0 + do { + match = separator.exec(value) + + /* Emit non-empty range */ + const until = match?.index ?? value.length + if (index < until) + fn(index, until) + + /* Update last index */ + if (match) { + const [term] = match + index = match.index + term.length + + /* Support zero-length lookaheads */ + if (term.length === 0) + separator.lastIndex = match.index + 1 + } + } while (match) +} diff --git a/src/assets/javascripts/integrations/search/internal/extractor/index.ts b/src/assets/javascripts/integrations/search/internal/extractor/index.ts new file mode 100644 index 000000000..012f2a7bf --- /dev/null +++ b/src/assets/javascripts/integrations/search/internal/extractor/index.ts @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2016-2022 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Visitor function + * + * @param block - Block index + * @param operation - Operation index + * @param start - Start offset + * @param end - End offset + */ +type VisitorFn = ( + block: number, operation: number, start: number, end: number +) => void + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Extract all non-HTML parts of a string + * + * This function preprocesses the given string by isolating all non-HTML parts + * of a string, in order to ensure that HTML tags are removed before indexing. + * This function intentionally takes a visitor function contrary to collecting + * and returning all sections, as it's significantly more memory efficient. + * + * @param value - String value + * @param fn - Visitor function + */ +export function extract( + value: string, fn: VisitorFn +): void { + + let block = 0 /* Current block */ + let start = 0 /* Current start offset */ + let end = 0 /* Current end offset */ + + /* Split string into sections */ + for (let stack = 0; end < value.length; end++) { + + /* Tag start after non-empty section */ + if (value.charAt(end) === "<" && end > start) { + fn(block, 1, start, start = end) + + /* Tag end */ + } else if (value.charAt(end) === ">") { + if (value.charAt(start + 1) === "/") { + if (--stack === 0) + fn(block++, 2, start, end + 1) + + /* Tag is not self-closing */ + } else if (value.charAt(end - 1) !== "/") { + if (stack++ === 0) + fn(block, 0, start, end + 1) + } + + /* New section */ + start = end + 1 + } + } + + /* Add trailing section */ + if (end > start) + fn(block, 1, start, end) +} diff --git a/src/assets/javascripts/integrations/search/internal/highlighter/index.ts b/src/assets/javascripts/integrations/search/internal/highlighter/index.ts new file mode 100644 index 000000000..b9d349814 --- /dev/null +++ b/src/assets/javascripts/integrations/search/internal/highlighter/index.ts @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2016-2022 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { Position, PositionTable } from "../tokenizer" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Highlight all occurrences in a string + * + * @param value - String value + * @param table - Table for indexing + * @param positions - Occurrences + * + * @returns Highlighted string value + */ +export function highlighter( + value: string, table: PositionTable, positions: Position[] +): string { + const slices: string[] = [] + + /* Map matches to blocks */ + const blocks = new Map() + for (const i of positions.sort((a, b) => a - b)) { + const block = i >>> 20 + const index = i & 0xFFFFF + + /* Ensure presence of block group */ + let group = blocks.get(block) + if (typeof group === "undefined") + blocks.set(block, group = []) + + /* Add index to group */ + group.push(index) + } + + /* Compute slices */ + for (const [block, indexes] of blocks) { + const t = table[block] + + /* Extract start and end positions, and length */ + const start = t[0] >>> 12 + const end = t[t.length - 1] >>> 12 + const length = t[t.length - 1] >>> 2 & 0x3FF + + /* Extract and highlight slice/block */ + let slice = value.slice(start, end + length) + for (const i of indexes.sort((a, b) => b - a)) { + + /* Retrieve offset and length of match */ + const p = (t[i] >>> 12) - start + const q = (t[i] >>> 2 & 0x3FF) + p + + /* Wrap occurrence */ + slice = [ + slice.slice(0, p), + "", slice.slice(p, q), "", + slice.slice(q) + ].join("") + } + + /* Append slice and abort if we have two */ + if (slices.push(slice) === 2) + break + } + + /* Return highlighted string value */ + return slices.join("") +} diff --git a/src/assets/javascripts/integrations/search/options/index.ts b/src/assets/javascripts/integrations/search/internal/index.ts similarity index 60% rename from src/assets/javascripts/integrations/search/options/index.ts rename to src/assets/javascripts/integrations/search/internal/index.ts index b1eb06668..74e199c9c 100644 --- a/src/assets/javascripts/integrations/search/options/index.ts +++ b/src/assets/javascripts/integrations/search/internal/index.ts @@ -20,29 +20,7 @@ * IN THE SOFTWARE. */ -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search pipeline function - */ -export type SearchPipelineFn = - | "trimmer" /* Trimmer */ - | "stopWordFilter" /* Stop word filter */ - | "stemmer" /* Stemmer */ - -/** - * Search pipeline - */ -export type SearchPipeline = SearchPipelineFn[] - -/* ------------------------------------------------------------------------- */ - -/** - * Search options - */ -export interface SearchOptions { - pipeline: SearchPipeline /* Search pipeline */ - suggestions: boolean /* Search suggestions */ -} +export * from "./_" +export * from "./extractor" +export * from "./highlighter" +export * from "./tokenizer" diff --git a/src/assets/javascripts/integrations/search/internal/tokenizer/index.ts b/src/assets/javascripts/integrations/search/internal/tokenizer/index.ts new file mode 100644 index 000000000..153d4dbd6 --- /dev/null +++ b/src/assets/javascripts/integrations/search/internal/tokenizer/index.ts @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2016-2022 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { split } from "../_" +import { extract } from "../extractor" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Table for indexing + */ +export type PositionTable = number[][] + +/** + * Position + */ +export type Position = number + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Split a string into tokens + * + * This tokenizer supersedes the default tokenizer that is provided by Lunr.js, + * as it is aware of HTML tags and allows for multi-character splitting. + * + * @param input - String value or token + * + * @returns Tokens + */ +export function tokenize( + input?: string | string[] +): lunr.Token[] { + const tokens: lunr.Token[] = [] + + /** + * Initialize segmenter, if loaded + * + * Note that doing this here is not ideal, but it's okay as we just test it + * before bringing the new search implementation in its final shape. + */ + const segmenter = "TinySegmenter" in lunr + ? new lunr.TinySegmenter() + : undefined + + /* Tokenize an array of string values */ + if (Array.isArray(input)) { + // @todo: handle multi-valued fields (e.g. tags) + for (const value of input) + tokens.push(...tokenize(value)) + + /* Tokenize a string value */ + } else if (input) { + const table = lunr.tokenizer.table + + /* Split string into sections and tokenize content blocks */ + extract(input, (block, type, start, end) => { + if (type & 1) { + const section = input.slice(start, end) + split(section, lunr.tokenizer.separator, (index, until) => { + + /** + * Apply segmenter after tokenization. Note that the segmenter will + * also split words at word boundaries, which is not what we want, so + * we need to check if we can somehow mitigate this behavior. + */ + if (typeof segmenter !== "undefined") { + const subsection = section.slice(index, until) + if (/^[MHIK]$/.test(segmenter.ctype_(subsection))) { + const segments = segmenter.segment(subsection) + for (let i = 0, l = 0; i < segments.length; i++) { + + /* Add block to table */ + table[block] ||= [] + table[block].push( + start + index + l << 12 | + segments[i].length << 2 | + type + ) + + /* Add block as token */ + tokens.push(new lunr.Token( + segments[i].toLowerCase(), { + position: block << 20 | table[block].length - 1 + } + )) + + /* Keep track of length */ + l += segments[i].length + } + return // combine segmenter with other approach!? + } + } + + /* Add block to table */ + table[block] ||= [] + table[block].push( + start + index << 12 | + until - index << 2 | + type + ) + + /* Add block as token */ + tokens.push(new lunr.Token( + section.slice(index, until).toLowerCase(), { + position: block << 20 | table[block].length - 1 + } + )) + }) + + /* Add non-content block to table */ + } else { + table[block] ||= [] + table[block].push( + start << 12 | + end - start << 2 | + type + ) + } + }) + } + + /* Return tokens */ + return tokens +} diff --git a/src/assets/javascripts/integrations/search/query/_/.eslintrc b/src/assets/javascripts/integrations/search/query/.eslintrc similarity index 69% rename from src/assets/javascripts/integrations/search/query/_/.eslintrc rename to src/assets/javascripts/integrations/search/query/.eslintrc index 8b8e4250e..3031c7e33 100644 --- a/src/assets/javascripts/integrations/search/query/_/.eslintrc +++ b/src/assets/javascripts/integrations/search/query/.eslintrc @@ -1,5 +1,6 @@ { "rules": { + "no-control-regex": "off", "@typescript-eslint/no-explicit-any": "off" } } diff --git a/src/assets/javascripts/integrations/search/query/_/index.ts b/src/assets/javascripts/integrations/search/query/_/index.ts index 0f8e87ea4..78c95d8cc 100644 --- a/src/assets/javascripts/integrations/search/query/_/index.ts +++ b/src/assets/javascripts/integrations/search/query/_/index.ts @@ -20,6 +20,9 @@ * IN THE SOFTWARE. */ +import { split } from "../../internal" +import { transform } from "../transform" + /* ---------------------------------------------------------------------------- * Types * ------------------------------------------------------------------------- */ @@ -43,9 +46,54 @@ export type SearchQueryTerms = Record * Functions * ------------------------------------------------------------------------- */ +/** + * Transform search query + * + * This function lexes the given search query and applies the transformation + * function to each term, preserving markup like `+` and `-` modifiers. + * + * @param query - Search query + * + * @returns Search query + */ +export function transformSearchQuery( + query: string +): string { + + /* Split query terms with tokenizer */ + return transform(query, part => { + const terms: string[] = [] + + /* Initialize lexer and analyze part */ + const lexer = new lunr.QueryLexer(part) + lexer.run() + + /* Extract and tokenize term from lexeme */ + for (const { type, str: term, start, end } of lexer.lexemes) + if (type === "TERM") + split(term, lunr.tokenizer.separator, (...range) => { + terms.push([ + part.slice(0, start), + term.slice(...range), + part.slice(end) + ].join("")) + }) + + /* Return terms */ + return terms + }) +} + +/* ------------------------------------------------------------------------- */ + /** * Parse a search query for analysis * + * Lunr.js itself has a bug where it doesn't detect or remove wildcards for + * query clauses, so we must do this here. + * + * @see https://bit.ly/3DpTGtz - GitHub issue + * * @param value - Query value * * @returns Search query clauses @@ -53,11 +101,28 @@ export type SearchQueryTerms = Record export function parseSearchQuery( value: string ): SearchQueryClause[] { - const query = new (lunr as any).Query(["title", "text"]) - const parser = new (lunr as any).QueryParser(value, query) + const query = new lunr.Query(["title", "text", "tags"]) + const parser = new lunr.QueryParser(value, query) - /* Parse and return query clauses */ + /* Parse Search query */ parser.parse() + for (const clause of query.clauses) { + clause.usePipeline = true + + /* Handle leading wildcard */ + if (clause.term.startsWith("*")) { + clause.wildcard = lunr.Query.wildcard.LEADING + clause.term = clause.term.slice(1) + } + + /* Handle trailing wildcard */ + if (clause.term.endsWith("*")) { + clause.wildcard = lunr.Query.wildcard.TRAILING + clause.term = clause.term.slice(0, -1) + } + } + + /* Return query clauses */ return query.clauses } @@ -85,7 +150,7 @@ export function getSearchQueryTerms( /* Annotate unmatched non-stopword query clauses */ for (const clause of clauses) - if (lunr.stopWordFilter?.(clause.term as any)) + if (lunr.stopWordFilter?.(clause.term)) result[clause.term] = false /* Return query terms */ diff --git a/src/assets/javascripts/integrations/search/query/transform/.eslintrc b/src/assets/javascripts/integrations/search/query/transform/.eslintrc deleted file mode 100644 index 5645b172f..000000000 --- a/src/assets/javascripts/integrations/search/query/transform/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "no-control-regex": "off" - } -} diff --git a/src/assets/javascripts/integrations/search/query/transform/index.ts b/src/assets/javascripts/integrations/search/query/transform/index.ts index 5330489c4..73c8a7868 100644 --- a/src/assets/javascripts/integrations/search/query/transform/index.ts +++ b/src/assets/javascripts/integrations/search/query/transform/index.ts @@ -21,17 +21,19 @@ */ /* ---------------------------------------------------------------------------- - * Types + * Helper types * ------------------------------------------------------------------------- */ /** - * Search transformation function + * Visitor function * - * @param value - Query value + * @param value - String value * - * @returns Transformed query value + * @returns String term(s) */ -export type SearchTransformFn = (value: string) => string +type VisitorFn = ( + value: string +) => string | string[] /* ---------------------------------------------------------------------------- * Functions @@ -40,32 +42,55 @@ export type SearchTransformFn = (value: string) => string /** * Default transformation function * - * 1. Search for terms in quotation marks and prepend a `+` modifier to denote - * that the resulting document must contain all terms, converting the query - * to an `AND` query (as opposed to the default `OR` behavior). While users - * may expect terms enclosed in quotation marks to map to span queries, i.e. - * for which order is important, Lunr.js doesn't support them, so the best - * we can do is to convert the terms to an `AND` query. + * 1. Trim excess whitespace from left and right. * - * 2. Replace control characters which are not located at the beginning of the + * 2. Search for parts in quotation marks and prepend a `+` modifier to denote + * that the resulting document must contain all parts, converting the query + * to an `AND` query (as opposed to the default `OR` behavior). While users + * may expect parts enclosed in quotation marks to map to span queries, i.e. + * for which order is important, Lunr.js doesn't support them, so the best + * we can do is to convert the parts to an `AND` query. + * + * 3. Replace control characters which are not located at the beginning of the * query or preceded by white space, or are not followed by a non-whitespace * character or are at the end of the query string. Furthermore, filter * unmatched quotation marks. * - * 3. Trim excess whitespace from left and right. + * 4. Split the query string at whitespace, then pass each part to the visitor + * function for tokenization, and append a wildcard to every resulting term + * that is not explicitly marked with a `+`, `-`, `~` or `^` modifier, since + * it ensures consistent and stable ranking when multiple terms are entered. + * Also, if a fuzzy or boost modifier are given, but no numeric value has + * been entered, default to 1 to not induce a query error. * * @param query - Query value + * @param fn - Visitor function * * @returns Transformed query value */ -export function defaultTransform(query: string): string { +export function transform( + query: string, fn: VisitorFn = term => term +): string { return query - .split(/"([^"]+)"/g) /* => 1 */ - .map((terms, index) => index & 1 - ? terms.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g, " +") - : terms + + /* => 1 */ + .trim() + + /* => 2 */ + .split(/"([^"]+)"/g) + .map((parts, index) => index & 1 + ? parts.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g, " +") + : parts ) .join("") - .replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g, "") /* => 2 */ - .trim() /* => 3 */ + + /* => 3 */ + .replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g, "") + + /* => 4 */ + .split(/\s+/g) + .flatMap(fn) + .map(term => /([~^]$)/.test(term) ? `${term}1` : term) + .map(term => /(^[+-]|[~^]\d+$)/.test(term) ? term : `${term}*`) + .join(" ") } diff --git a/src/assets/javascripts/integrations/search/worker/_/index.ts b/src/assets/javascripts/integrations/search/worker/_/index.ts index 2d2a6410c..f49c07a95 100644 --- a/src/assets/javascripts/integrations/search/worker/_/index.ts +++ b/src/assets/javascripts/integrations/search/worker/_/index.ts @@ -23,73 +23,21 @@ import { ObservableInput, Subject, - from, - map, - share + first, + merge, + of, + switchMap } from "rxjs" -import { configuration, feature, translation } from "~/_" -import { WorkerHandler, watchWorker } from "~/browser" +import { feature } from "~/_" +import { watchToggle, watchWorker } from "~/browser" -import { SearchIndex } from "../../_" -import { - SearchOptions, - SearchPipeline -} from "../../options" +import { SearchIndex } from "../../config" import { SearchMessage, - SearchMessageType, - SearchSetupMessage, - isSearchResultMessage + SearchMessageType } from "../message" -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search worker - */ -export type SearchWorker = WorkerHandler - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Set up search index - * - * @param data - Search index - * - * @returns Search index - */ -function setupSearchIndex({ config, docs }: SearchIndex): SearchIndex { - - /* Override default language with value from translation */ - if (config.lang.length === 1 && config.lang[0] === "en") - config.lang = [ - translation("search.config.lang") - ] - - /* Override default separator with value from translation */ - if (config.separator === "[\\s\\-]+") - config.separator = translation("search.config.separator") - - /* Set pipeline from translation */ - const pipeline = translation("search.config.pipeline") - .split(/\s*,\s*/) - .filter(Boolean) as SearchPipeline - - /* Determine search options */ - const options: SearchOptions = { - pipeline, - suggestions: feature("search.suggest") - } - - /* Return search index after defaulting */ - return { config, docs, options } -} - /* ---------------------------------------------------------------------------- * Functions * ------------------------------------------------------------------------- */ @@ -97,46 +45,51 @@ function setupSearchIndex({ config, docs }: SearchIndex): SearchIndex { /** * Set up search worker * - * This function creates a web worker to set up and query the search index, - * which is done using Lunr.js. The index must be passed as an observable to - * enable hacks like _localsearch_ via search index embedding as JSON. + * This function creates and initializes a web worker that is used for search, + * so that the user interface doesn't freeze. In general, the application does + * not care how search is implemented, as long as the web worker conforms to + * the format expected by the application as defined in `SearchMessage`. This + * allows the author to implement custom search functionality, by providing a + * custom web worker via configuration. + * + * Material for MkDocs' built-in search implementation makes use of Lunr.js, an + * efficient and fast implementation for client-side search. Leveraging a tiny + * iframe-based web worker shim, search is even supported for the `file://` + * protocol, enabling search for local non-hosted builds. + * + * If the protocol is `file://`, search initialization is deferred to mitigate + * freezing, as it's now synchronous by design - see https://bit.ly/3C521EO + * + * @see https://bit.ly/3igvtQv - How to implement custom search * * @param url - Worker URL - * @param index - Search index observable input + * @param index$ - Search index observable input * * @returns Search worker */ export function setupSearchWorker( - url: string, index: ObservableInput -): SearchWorker { - const config = configuration() - const worker = new Worker(url) - - /* Create communication channels and resolve relative links */ - const tx$ = new Subject() - const rx$ = watchWorker(worker, { tx$ }) + url: string, index$: ObservableInput +): Subject { + const worker$ = watchWorker(url) + merge( + of(location.protocol !== "file:"), + watchToggle("search") + ) .pipe( - map(message => { - if (isSearchResultMessage(message)) { - for (const result of message.data.items) - for (const document of result) - document.location = `${new URL(document.location, config.base)}` - } - return message - }), - share() + first(active => active), + switchMap(() => index$) ) - - /* Set up search index */ - from(index) - .pipe( - map(data => ({ + .subscribe(({ config, docs }) => worker$.next({ type: SearchMessageType.SETUP, - data: setupSearchIndex(data) - } as SearchSetupMessage)) - ) - .subscribe(tx$.next.bind(tx$)) + data: { + config, + docs, + options: { + suggest: feature("search.suggest") + } + } + })) /* Return search worker */ - return { tx$, rx$ } + return worker$ } diff --git a/src/assets/javascripts/integrations/search/worker/main/.eslintrc b/src/assets/javascripts/integrations/search/worker/main/.eslintrc index 09c579193..3df9d5516 100644 --- a/src/assets/javascripts/integrations/search/worker/main/.eslintrc +++ b/src/assets/javascripts/integrations/search/worker/main/.eslintrc @@ -1,5 +1,6 @@ { "rules": { + "no-console": "off", "@typescript-eslint/no-misused-promises": "off" } } diff --git a/src/assets/javascripts/integrations/search/worker/main/index.ts b/src/assets/javascripts/integrations/search/worker/main/index.ts index c44884d1b..294a4e9a2 100644 --- a/src/assets/javascripts/integrations/search/worker/main/index.ts +++ b/src/assets/javascripts/integrations/search/worker/main/index.ts @@ -22,9 +22,11 @@ import lunr from "lunr" +import { getElement } from "~/browser/element/_" import "~/polyfills" -import { Search, SearchIndexConfig } from "../../_" +import { Search } from "../../_" +import { SearchConfig } from "../../config" import { SearchMessage, SearchMessageType @@ -35,14 +37,18 @@ import { * ------------------------------------------------------------------------- */ /** - * Add support for usage with `iframe-worker` polyfill + * Add support for `iframe-worker` shim * * While `importScripts` is synchronous when executed inside of a web worker, - * it's not possible to provide a synchronous polyfilled implementation. The - * cool thing is that awaiting a non-Promise is a noop, so extending the type - * definition to return a `Promise` shouldn't break anything. + * it's not possible to provide a synchronous shim implementation. The cool + * thing is that awaiting a non-Promise will convert it into a Promise, so + * extending the type definition to return a `Promise` shouldn't break anything. * * @see https://bit.ly/2PjDnXi - GitHub comment + * + * @param urls - Scripts to load + * + * @returns Promise resolving with no result */ declare global { function importScripts(...urls: string[]): Promise | void @@ -65,25 +71,25 @@ let index: Search * Fetch (= import) multi-language support through `lunr-languages` * * This function automatically imports the stemmers necessary to process the - * languages, which are defined through the search index configuration. + * languages which are defined as part of the search configuration. * * If the worker runs inside of an `iframe` (when using `iframe-worker` as * a shim), the base URL for the stemmers to be loaded must be determined by * searching for the first `script` element with a `src` attribute, which will * contain the contents of this script. * - * @param config - Search index configuration + * @param config - Search configuration * * @returns Promise resolving with no result */ async function setupSearchLanguages( - config: SearchIndexConfig + config: SearchConfig ): Promise { let base = "../lunr" /* Detect `iframe-worker` and fix base URL */ if (typeof parent !== "undefined" && "IFrameWorker" in parent) { - const worker = document.querySelector("script[src]")! + const worker = getElement("script[src]")! const [path] = worker.src.split("/worker") /* Prefix base with path */ @@ -150,9 +156,21 @@ export async function handler( /* Search query message */ case SearchMessageType.QUERY: - return { - type: SearchMessageType.RESULT, - data: index ? index.search(message.data) : { items: [] } + const query = message.data + try { + return { + type: SearchMessageType.RESULT, + data: index.search(query) + } + + /* Return empty result in case of error */ + } catch (err) { + console.warn(`Invalid query: ${query} – see https://bit.ly/2s3ChXG`) + console.warn(err) + return { + type: SearchMessageType.RESULT, + data: { items: [] } + } } /* All other messages */ @@ -165,7 +183,7 @@ export async function handler( * Worker * ------------------------------------------------------------------------- */ -/* @ts-expect-error - expose Lunr.js in global scope, or stemmers won't work */ +/* Expose Lunr.js in global scope, or stemmers won't work */ self.lunr = lunr /* Handle messages */ diff --git a/src/assets/javascripts/integrations/search/worker/message/index.ts b/src/assets/javascripts/integrations/search/worker/message/index.ts index f9a4ff015..8aece4b18 100644 --- a/src/assets/javascripts/integrations/search/worker/message/index.ts +++ b/src/assets/javascripts/integrations/search/worker/message/index.ts @@ -20,7 +20,8 @@ * IN THE SOFTWARE. */ -import { SearchIndex, SearchResult } from "../../_" +import { SearchResult } from "../../_" +import { SearchIndex } from "../../config" /* ---------------------------------------------------------------------------- * Types @@ -84,19 +85,6 @@ export type SearchMessage = * Functions * ------------------------------------------------------------------------- */ -/** - * Type guard for search setup messages - * - * @param message - Search worker message - * - * @returns Test result - */ -export function isSearchSetupMessage( - message: SearchMessage -): message is SearchSetupMessage { - return message.type === SearchMessageType.SETUP -} - /** * Type guard for search ready messages * @@ -110,19 +98,6 @@ export function isSearchReadyMessage( return message.type === SearchMessageType.READY } -/** - * Type guard for search query messages - * - * @param message - Search worker message - * - * @returns Test result - */ -export function isSearchQueryMessage( - message: SearchMessage -): message is SearchQueryMessage { - return message.type === SearchMessageType.QUERY -} - /** * Type guard for search result messages * diff --git a/src/assets/javascripts/templates/search/index.tsx b/src/assets/javascripts/templates/search/index.tsx index 75380c658..f089dd76f 100644 --- a/src/assets/javascripts/templates/search/index.tsx +++ b/src/assets/javascripts/templates/search/index.tsx @@ -23,12 +23,8 @@ import { ComponentChild } from "preact" import { configuration, feature, translation } from "~/_" -import { - SearchDocument, - SearchMetadata, - SearchResultItem -} from "~/integrations/search" -import { h, truncate } from "~/utilities" +import { SearchItem } from "~/integrations/search" +import { h } from "~/utilities" /* ---------------------------------------------------------------------------- * Helper types @@ -55,7 +51,7 @@ const enum Flag { * @returns Element */ function renderSearchDocument( - document: SearchDocument & SearchMetadata, flag: Flag + document: SearchItem, flag: Flag ): HTMLElement { const parent = flag & Flag.PARENT const teaser = flag & Flag.TEASER @@ -69,7 +65,8 @@ function renderSearchDocument( .slice(0, -1) /* Assemble query string for highlighting */ - const url = new URL(document.location) + const config = configuration() + const url = new URL(document.location, config.base) if (feature("search.highlight")) url.searchParams.set("h", Object.entries(document.terms) .filter(([, match]) => match) @@ -81,34 +78,25 @@ function renderSearchDocument( return (
{parent > 0 &&
} -

{document.title}

+ {parent > 0 &&

{document.title}

} + {parent <= 0 &&

{document.title}

} {teaser > 0 && document.text.length > 0 && -

- {truncate(document.text, 320)} -

+ document.text } - {document.tags && ( -
- {document.tags.map(tag => { - const id = tag.replace(/<[^>]+>/g, "") - const type = tags - ? id in tags - ? `md-tag-icon md-tag-icon--${tags[id]}` - : "md-tag-icon" - : "" - return ( - {tag} - ) - })} -
- )} + {document.tags && document.tags.map(tag => { + const type = tags + ? tag in tags + ? `md-tag-icon md-tag-icon--${tags[tag]}` + : "md-tag-icon" + : "" + return ( + {tag} + ) + })} {teaser > 0 && missing.length > 0 &&

{translation("search.result.term.missing")}: {...missing} @@ -131,13 +119,18 @@ function renderSearchDocument( * @returns Element */ export function renderSearchResultItem( - result: SearchResultItem + result: SearchItem[] ): HTMLElement { const threshold = result[0].score const docs = [...result] + const config = configuration() + /* Find and extract parent article */ - const parent = docs.findIndex(doc => !doc.location.includes("#")) + const parent = docs.findIndex(doc => { + const l = `${new URL(doc.location, config.base)}` // @todo hacky + return !l.includes("#") + }) const [article] = docs.splice(parent, 1) /* Determine last index above threshold */ @@ -156,10 +149,12 @@ export function renderSearchResultItem( ...more.length ? [

- {more.length > 0 && more.length === 1 - ? translation("search.result.more.one") - : translation("search.result.more.other", more.length) - } +
+ {more.length > 0 && more.length === 1 + ? translation("search.result.more.one") + : translation("search.result.more.other", more.length) + } +
{...more.map(section => renderSearchDocument(section, Flag.TEASER))}
diff --git a/src/assets/javascripts/templates/tooltip/index.tsx b/src/assets/javascripts/templates/tooltip/index.tsx index b383f8ecd..033fee408 100644 --- a/src/assets/javascripts/templates/tooltip/index.tsx +++ b/src/assets/javascripts/templates/tooltip/index.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 Martin Donath + * Copyright (c) 2016-2022 Martin Donath * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to diff --git a/src/assets/javascripts/utilities/h/index.ts b/src/assets/javascripts/utilities/h/index.ts index aaaf1d825..8c5367d99 100644 --- a/src/assets/javascripts/utilities/h/index.ts +++ b/src/assets/javascripts/utilities/h/index.ts @@ -38,6 +38,7 @@ type Attributes = * Child element */ type Child = + | ChildNode | HTMLElement | Text | string diff --git a/src/assets/javascripts/utilities/index.ts b/src/assets/javascripts/utilities/index.ts index e27c51bc0..8200671ac 100644 --- a/src/assets/javascripts/utilities/index.ts +++ b/src/assets/javascripts/utilities/index.ts @@ -21,4 +21,4 @@ */ export * from "./h" -export * from "./string" +export * from "./round" diff --git a/src/assets/javascripts/utilities/string/index.ts b/src/assets/javascripts/utilities/round/index.ts similarity index 74% rename from src/assets/javascripts/utilities/string/index.ts rename to src/assets/javascripts/utilities/round/index.ts index 121c74491..5a841f132 100644 --- a/src/assets/javascripts/utilities/string/index.ts +++ b/src/assets/javascripts/utilities/round/index.ts @@ -24,28 +24,6 @@ * Functions * ------------------------------------------------------------------------- */ -/** - * Truncate a string after the given number of characters - * - * This is not a very reasonable approach, since the summaries kind of suck. - * It would be better to create something more intelligent, highlighting the - * search occurrences and making a better summary out of it, but this note was - * written three years ago, so who knows if we'll ever fix it. - * - * @param value - Value to be truncated - * @param n - Number of characters - * - * @returns Truncated value - */ -export function truncate(value: string, n: number): string { - let i = n - if (value.length > i) { - while (value[i] !== " " && --i > 0) { /* keep eating */ } - return `${value.substring(0, i)}...` - } - return value -} - /** * Round a number for display with repository facts * diff --git a/src/assets/stylesheets/main.scss b/src/assets/stylesheets/main.scss index 56661781d..385300616 100644 --- a/src/assets/stylesheets/main.scss +++ b/src/assets/stylesheets/main.scss @@ -41,26 +41,26 @@ @import "main/icons"; @import "main/typeset"; -@import "main/layout/banner"; -@import "main/layout/base"; -@import "main/layout/clipboard"; -@import "main/layout/consent"; -@import "main/layout/content"; -@import "main/layout/dialog"; -@import "main/layout/feedback"; -@import "main/layout/footer"; -@import "main/layout/form"; -@import "main/layout/header"; -@import "main/layout/nav"; -@import "main/layout/search"; -@import "main/layout/select"; -@import "main/layout/sidebar"; -@import "main/layout/source"; -@import "main/layout/tabs"; -@import "main/layout/tag"; -@import "main/layout/tooltip"; -@import "main/layout/top"; -@import "main/layout/version"; +@import "main/components/banner"; +@import "main/components/base"; +@import "main/components/clipboard"; +@import "main/components/consent"; +@import "main/components/content"; +@import "main/components/dialog"; +@import "main/components/feedback"; +@import "main/components/footer"; +@import "main/components/form"; +@import "main/components/header"; +@import "main/components/nav"; +@import "main/components/search"; +@import "main/components/select"; +@import "main/components/sidebar"; +@import "main/components/source"; +@import "main/components/tabs"; +@import "main/components/tag"; +@import "main/components/tooltip"; +@import "main/components/top"; +@import "main/components/version"; @import "main/extensions/markdown/admonition"; @import "main/extensions/markdown/footnotes"; diff --git a/src/assets/stylesheets/main/_typeset.scss b/src/assets/stylesheets/main/_typeset.scss index 4533f8394..43f072593 100644 --- a/src/assets/stylesheets/main/_typeset.scss +++ b/src/assets/stylesheets/main/_typeset.scss @@ -42,7 +42,8 @@ body { // Define default fonts body, -input { +input, +aside { color: var(--md-typeset-color); font-feature-settings: "kern", "liga"; font-family: var(--md-text-font-family); @@ -52,7 +53,6 @@ input { code, pre, kbd { - color: var(--md-typeset-color); font-feature-settings: "kern"; font-family: var(--md-code-font-family); } diff --git a/src/assets/stylesheets/main/layout/_banner.scss b/src/assets/stylesheets/main/components/_banner.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_banner.scss rename to src/assets/stylesheets/main/components/_banner.scss diff --git a/src/assets/stylesheets/main/layout/_base.scss b/src/assets/stylesheets/main/components/_base.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_base.scss rename to src/assets/stylesheets/main/components/_base.scss diff --git a/src/assets/stylesheets/main/layout/_clipboard.scss b/src/assets/stylesheets/main/components/_clipboard.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_clipboard.scss rename to src/assets/stylesheets/main/components/_clipboard.scss diff --git a/src/assets/stylesheets/main/layout/_consent.scss b/src/assets/stylesheets/main/components/_consent.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_consent.scss rename to src/assets/stylesheets/main/components/_consent.scss diff --git a/src/assets/stylesheets/main/layout/_content.scss b/src/assets/stylesheets/main/components/_content.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_content.scss rename to src/assets/stylesheets/main/components/_content.scss diff --git a/src/assets/stylesheets/main/layout/_dialog.scss b/src/assets/stylesheets/main/components/_dialog.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_dialog.scss rename to src/assets/stylesheets/main/components/_dialog.scss diff --git a/src/assets/stylesheets/main/layout/_feedback.scss b/src/assets/stylesheets/main/components/_feedback.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_feedback.scss rename to src/assets/stylesheets/main/components/_feedback.scss diff --git a/src/assets/stylesheets/main/layout/_footer.scss b/src/assets/stylesheets/main/components/_footer.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_footer.scss rename to src/assets/stylesheets/main/components/_footer.scss diff --git a/src/assets/stylesheets/main/layout/_form.scss b/src/assets/stylesheets/main/components/_form.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_form.scss rename to src/assets/stylesheets/main/components/_form.scss diff --git a/src/assets/stylesheets/main/layout/_header.scss b/src/assets/stylesheets/main/components/_header.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_header.scss rename to src/assets/stylesheets/main/components/_header.scss diff --git a/src/assets/stylesheets/main/layout/_nav.scss b/src/assets/stylesheets/main/components/_nav.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_nav.scss rename to src/assets/stylesheets/main/components/_nav.scss diff --git a/src/assets/stylesheets/main/layout/_search.scss b/src/assets/stylesheets/main/components/_search.scss similarity index 90% rename from src/assets/stylesheets/main/layout/_search.scss rename to src/assets/stylesheets/main/components/_search.scss index 8aa381106..c554bca91 100644 --- a/src/assets/stylesheets/main/layout/_search.scss +++ b/src/assets/stylesheets/main/components/_search.scss @@ -277,10 +277,14 @@ text-overflow: clip; // Search icon and placeholder - + .md-search__icon, - &::placeholder { + + .md-search__icon { color: var(--md-default-fg-color--light); } + + // Search placeholder + &::placeholder { + color: transparent; + } } } } @@ -350,7 +354,7 @@ } // Search option buttons - > * { + > .md-icon { margin-inline-start: px2rem(4px); color: var(--md-default-fg-color--light); transform: scale(0.75); @@ -365,7 +369,7 @@ -webkit-tap-highlight-color: transparent; } - // Show reset button when search is active and input non-empty + // Show buttons when search is active and input non-empty [data-md-toggle="search"]:checked ~ .md-header .md-search__input:valid ~ & { transform: scale(1); @@ -556,31 +560,17 @@ } } - // Search result more link - &__more summary { + // Search result more container + &__more > summary { + position: sticky; + top: 0; + z-index: 1; display: block; - padding: px2em(12px) px2rem(16px); - color: var(--md-typeset-a-color); - font-size: px2rem(12.8px); outline: none; cursor: pointer; - transition: - color 250ms, - background-color 250ms; scroll-snap-align: start; - // [tablet landscape +]: Adjust spacing - @include break-from-device(tablet landscape) { - padding-inline-start: px2rem(44px); - } - - // Search result more link on focus/hover - &:is(:focus, :hover) { - color: var(--md-accent-fg-color); - background-color: var(--md-accent-fg-color--transparent); - } - - // Hide native details marker - modern + // Hide native details marker &::marker { display: none; } @@ -591,10 +581,32 @@ display: none; } - // Adjust transparency of less relevant results - ~ * > * { - opacity: 0.65; + // Search result more button + > div { + padding: px2em(12px) px2rem(16px); + color: var(--md-typeset-a-color); + font-size: px2rem(12.8px); + transition: + color 250ms, + background-color 250ms; + + // [tablet landscape +]: Adjust spacing + @include break-from-device(tablet landscape) { + padding-inline-start: px2rem(44px); + } } + + // Search result more link on focus/hover + &:is(:focus, :hover) > div { + color: var(--md-accent-fg-color); + background-color: var(--md-accent-fg-color--transparent); + } + } + + // Adjust background for more container in open state + &__more[open] > summary { + background-color: var(--md-default-bg-color); + // box-shadow: 0 px2rem(-1px) hsla(0, 0%, 0%, 0.07) inset; } // Search result article @@ -607,18 +619,6 @@ @include break-from-device(tablet landscape) { padding-inline-start: px2rem(44px); } - - // Search result article document - &--document { - - // Search result title - .md-search-result__title { - margin: px2rem(11px) 0; - font-weight: 400; - font-size: px2rem(16px); - line-height: 1.4; - } - } } // Search result icon @@ -654,49 +654,46 @@ } } - // Search result title - &__title { - margin: 0.5em 0; - font-weight: 700; - font-size: px2rem(12.8px); - line-height: 1.6; - } - - // Search result teaser - &__teaser { - display: -webkit-box; - max-height: px2rem(40px); - margin: 0.5em 0; - overflow: hidden; + // Typesetted content + .md-typeset { color: var(--md-default-fg-color--light); font-size: px2rem(12.8px); line-height: 1.6; - text-overflow: ellipsis; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - // [mobile -]: Adjust number of lines - @include break-to-device(mobile) { - max-height: px2rem(60px); - -webkit-line-clamp: 3; + // Search result article title + h1 { + margin: px2rem(11px) 0; + color: var(--md-default-fg-color); + font-weight: 400; + font-size: px2rem(16px); + line-height: 1.4; + + // Search term highlighting + mark { + text-decoration: none; + } } - // [tablet landscape]: Adjust number of lines - @include break-at-device(tablet landscape) { - max-height: px2rem(60px); - -webkit-line-clamp: 3; - } + // Search result section title + h2 { + margin: 0.5em 0; + color: var(--md-default-fg-color); + font-weight: 700; + font-size: px2rem(12.8px); + line-height: 1.6; - // Search term highlighting - mark { - text-decoration: underline; - background-color: transparent; + // Search term highlighting + mark { + text-decoration: none; + } } } // Search result terms &__terms { + display: block; margin: 0.5em 0; + color: var(--md-default-fg-color); font-size: px2rem(12.8px); font-style: italic; } @@ -704,6 +701,7 @@ // Search term highlighting mark { color: var(--md-accent-fg-color); + text-decoration: underline; background-color: transparent; } } diff --git a/src/assets/stylesheets/main/layout/_select.scss b/src/assets/stylesheets/main/components/_select.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_select.scss rename to src/assets/stylesheets/main/components/_select.scss diff --git a/src/assets/stylesheets/main/layout/_sidebar.scss b/src/assets/stylesheets/main/components/_sidebar.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_sidebar.scss rename to src/assets/stylesheets/main/components/_sidebar.scss diff --git a/src/assets/stylesheets/main/layout/_source.scss b/src/assets/stylesheets/main/components/_source.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_source.scss rename to src/assets/stylesheets/main/components/_source.scss diff --git a/src/assets/stylesheets/main/layout/_tabs.scss b/src/assets/stylesheets/main/components/_tabs.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_tabs.scss rename to src/assets/stylesheets/main/components/_tabs.scss diff --git a/src/assets/stylesheets/main/layout/_tag.scss b/src/assets/stylesheets/main/components/_tag.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_tag.scss rename to src/assets/stylesheets/main/components/_tag.scss diff --git a/src/assets/stylesheets/main/layout/_tooltip.scss b/src/assets/stylesheets/main/components/_tooltip.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_tooltip.scss rename to src/assets/stylesheets/main/components/_tooltip.scss diff --git a/src/assets/stylesheets/main/layout/_top.scss b/src/assets/stylesheets/main/components/_top.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_top.scss rename to src/assets/stylesheets/main/components/_top.scss diff --git a/src/assets/stylesheets/main/layout/_version.scss b/src/assets/stylesheets/main/components/_version.scss similarity index 100% rename from src/assets/stylesheets/main/layout/_version.scss rename to src/assets/stylesheets/main/components/_version.scss diff --git a/src/base.html b/src/base.html index 786eb7463..8eeb26215 100644 --- a/src/base.html +++ b/src/base.html @@ -393,10 +393,6 @@ {%- for key in [ "clipboard.copy", "clipboard.copied", - "search.config.lang", - "search.config.pipeline", - "search.config.separator", - "search.placeholder", "search.result.placeholder", "search.result.none", "search.result.one", diff --git a/src/partials/header.html b/src/partials/header.html index c76b2d041..a01559bfa 100644 --- a/src/partials/header.html +++ b/src/partials/header.html @@ -20,7 +20,7 @@ IN THE SOFTWARE. --> - + {% set class = "md-header" %} {% if "navigation.tabs.sticky" in features %} {% set class = class ~ " md-header--lifted" %} diff --git a/src/partials/languages/el.html b/src/partials/languages/el.html index 5ac29e17b..de534dee2 100644 --- a/src/partials/languages/el.html +++ b/src/partials/languages/el.html @@ -34,7 +34,6 @@ "meta.source": "Πηγή", "nav": "Πλοήγηση", "search": "Αναζήτηση", - "search.config.pipeline": "stopWordFilter", "search.placeholder": "Αναζήτηση", "search.share": "Διαμοίραση", "search.reset": "Καθαρισμός", diff --git a/src/partials/languages/en.html b/src/partials/languages/en.html index b741727a8..cd7d59ead 100644 --- a/src/partials/languages/en.html +++ b/src/partials/languages/en.html @@ -40,7 +40,7 @@ "nav": "Navigation", "search": "Search", "search.config.lang": "en", - "search.config.pipeline": "trimmer, stopWordFilter", + "search.config.pipeline": "stopWordFilter", "search.config.separator": "[\\s\\-]+", "search.placeholder": "Search", "search.share": "Share", diff --git a/src/partials/languages/ja.html b/src/partials/languages/ja.html index 22ae49f47..3ea464998 100644 --- a/src/partials/languages/ja.html +++ b/src/partials/languages/ja.html @@ -34,7 +34,7 @@ "meta.source": "ソース", "nav": "ナビゲーション", "search.config.lang": "ja", - "search.config.pipeline": "trimmer, stemmer", + "search.config.pipeline": "stemmer", "search.config.separator": "[\\s\\- 、。,.]+", "search.placeholder": "検索", "search.reset": "クリア", diff --git a/src/partials/languages/zh-Hant.html b/src/partials/languages/zh-Hant.html index 2eb1d16ac..fb97d9295 100644 --- a/src/partials/languages/zh-Hant.html +++ b/src/partials/languages/zh-Hant.html @@ -31,7 +31,7 @@ "meta.comments": "評論", "meta.source": "來源", "search.config.lang": "ja", - "search.config.pipeline": "trimmer, stemmer", + "search.config.pipeline": "stemmer", "search.config.separator": "[\\s\\-,。]+", "search.placeholder": "搜尋", "search.result.initializer": "正在初始化搜尋引擎", diff --git a/src/partials/languages/zh-TW.html b/src/partials/languages/zh-TW.html index 44bdd2498..7e689fded 100644 --- a/src/partials/languages/zh-TW.html +++ b/src/partials/languages/zh-TW.html @@ -35,7 +35,7 @@ "meta.comments": "留言", "meta.source": "來源", "search.config.lang": "ja", - "search.config.pipeline": "trimmer, stemmer", + "search.config.pipeline": "stemmer", "search.config.separator": "[\\s\\- 、。,.?;]+", "search.placeholder": "搜尋", "search.result.initializer": "正在初始化搜尋引擎", diff --git a/src/partials/languages/zh.html b/src/partials/languages/zh.html index 785de1498..e599bcc56 100644 --- a/src/partials/languages/zh.html +++ b/src/partials/languages/zh.html @@ -39,7 +39,7 @@ "nav": "导航栏", "search": "查找", "search.config.lang": "ja", - "search.config.pipeline": "trimmer, stemmer", + "search.config.pipeline": "stemmer", "search.config.separator": "[\\s\\-,。]+", "search.placeholder": "搜索", "search.share": "分享", diff --git a/src/plugins/offline/__init__.py b/src/plugins/offline/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugins/offline/plugin.py b/src/plugins/offline/plugin.py new file mode 100644 index 000000000..f3ca162d4 --- /dev/null +++ b/src/plugins/offline/plugin.py @@ -0,0 +1,69 @@ +# Copyright (c) 2016-2022 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +import os + +from mkdocs import utils +from mkdocs.config import config_options as opt +from mkdocs.config.base import Config +from mkdocs.plugins import BasePlugin, event_priority + +# ----------------------------------------------------------------------------- +# Class +# ----------------------------------------------------------------------------- + +# Offline plugin configuration scheme +class OfflinePluginConfig(Config): + enabled = opt.Type(bool, default = True) + +# ----------------------------------------------------------------------------- + +# Offline plugin +class OfflinePlugin(BasePlugin[OfflinePluginConfig]): + + # Initialize plugin + def on_config(self, config): + if not self.config.enabled: + return + + # Ensure correct resolution of links + config.use_directory_urls = False + + # Support offline search (run latest) + @event_priority(-100) + def on_post_build(self, *, config): + if not self.config.enabled: + return + + # Check for existence of search index + base = os.path.join(config.site_dir, "search") + path = os.path.join(base, "search_index.json") + if not os.path.exists(path): + return + + # Retrieve search index + with open(path, "r") as data: + index = data.read() + + # Inline search index into script + utils.write_file( + f"var __index = {index}".encode("utf-8"), + os.path.join(base, "search_index.js") + ) diff --git a/src/plugins/search/plugin.py b/src/plugins/search/plugin.py index 62683d4d2..bc2dea594 100644 --- a/src/plugins/search/plugin.py +++ b/src/plugins/search/plugin.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016-2021 Martin Donath +# Copyright (c) 2016-2022 Martin Donath # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to @@ -18,54 +18,445 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. +import json import logging +import os +import regex as re +from html import escape +from html.parser import HTMLParser +from mkdocs import utils from mkdocs.commands.build import DuplicateFilter -from mkdocs.contrib.search import SearchPlugin as BasePlugin -from mkdocs.contrib.search.search_index import SearchIndex as BaseIndex +from mkdocs.config import config_options as opt +from mkdocs.config.base import Config +from mkdocs.contrib.search import LangOption +from mkdocs.plugins import BasePlugin # ----------------------------------------------------------------------------- # Class # ----------------------------------------------------------------------------- -# Search plugin with custom search index -class SearchPlugin(BasePlugin): +# Search plugin configuration scheme +class SearchPluginConfig(Config): + lang = opt.Optional(LangOption()) + separator = opt.Optional(opt.Type(str)) + pipeline = opt.ListOfItems( + opt.Choice(("stemmer", "stopWordFilter", "trimmer")), + default = [] + ) - # Override to use a custom search index - def on_pre_build(self, config): - super().on_pre_build(config) + # Deprecated options + indexing = opt.Deprecated(message = "Unsupported option") + prebuild_index = opt.Deprecated(message = "Unsupported option") + min_search_length = opt.Deprecated(message = "Unsupported option") + +# ----------------------------------------------------------------------------- + +# Search plugin +class SearchPlugin(BasePlugin[SearchPluginConfig]): + + # Determine whether we're running under dirty reload + def on_startup(self, *, command, dirty): + self.is_dirtyreload = False + self.is_dirty = dirty + + # Initialize search index cache + self.search_index_prev = None + + # Initialize plugin + def on_config(self, config): + if not self.config.lang: + self.config.lang = [self._translate( + config, "search.config.lang" + )] + + # Retrieve default value for separator + if not self.config.separator: + self.config.separator = self._translate( + config, "search.config.separator" + ) + + # Retrieve default value for pipeline + if not self.config.pipeline: + self.config.pipeline = list(filter(len, re.split( + r"\s*,\s*", self._translate(config, "search.config.pipeline") + ))) + + # Initialize search index self.search_index = SearchIndex(**self.config) + # Add page to search index + def on_page_context(self, context, *, page, config, nav): + self.search_index.add_entry_from_context(page) + page.content = re.sub( + r"\s?data-search-\w+=\"[^\"]+\"", + "", + page.content + ) + + # Generate search index + def on_post_build(self, *, config): + base = os.path.join(config.site_dir, "search") + path = os.path.join(base, "search_index.json") + + # Generate and write search index to file + data = self.search_index.generate_search_index(self.search_index_prev) + utils.write_file(data.encode("utf-8"), path) + + # Persist search index for repeated invocation + if self.is_dirty: + self.search_index_prev = self.search_index + + # Determine whether we're running under dirty reload + def on_serve(self, server, *, config, builder): + self.is_dirtyreload = self.is_dirty + + # ------------------------------------------------------------------------- + + # Translate the given placeholder value + def _translate(self, config, value): + env = config.theme.get_env() + + # Load language template and return translation for placeholder + language = "partials/language.html" + template = env.get_template(language, None, { "config": config }) + return template.module.t(value) + # ----------------------------------------------------------------------------- # Search index with support for additional fields -class SearchIndex(BaseIndex): +class SearchIndex: - # Override to add additional fields for each page + # Initialize search index + def __init__(self, **config): + self.config = config + self.entries = [] + + # Add page to search index def add_entry_from_context(self, page): - index = len(self._entries) - super().add_entry_from_context(page) + search = page.meta.get("search", {}) + if search.get("exclude"): + return - # Add document tags, if any - if page.meta.get("tags"): - if type(page.meta["tags"]) is list: - entry = self._entries[index] - entry["tags"] = [ - str(tag) for tag in page.meta["tags"] - ] + # Divide page content into sections + parser = Parser() + parser.feed(page.content) + parser.close() + + # Add sections to index + for section in parser.data: + if not section.is_excluded(): + self.create_entry_for_section(section, page.toc, page.url, page) + + # Override: graceful indexing and additional fields + def create_entry_for_section(self, section, toc, url, page): + item = self._find_toc_by_id(toc, section.id) + if item: + url = url + item.url + elif section.id: + url = url + "#" + section.id + + # Set page title as section title if none was given, which happens when + # the first headline in a Markdown document is not a h1 headline. Also, + # if a page title was set via front matter, use that even though a h1 + # might be given or the page name was specified in nav in mkdocs.yml + if not section.title: + section.title = page.meta.get("title", page.title) + + # Compute title and text + title = "".join(section.title).strip() + text = "".join(section.text).strip() + + # Reset text, if only titles should be indexed + if self.config["indexing"] == "titles": + text = "" + + # Create entry for section + entry = { + "title": title, + "text": text, + "location": url + } + + # Set document tags + tags = page.meta.get("tags") + if isinstance(tags, list): + entry["tags"] = [] + for name in tags: + if name and isinstance(name, (str, int, float, bool)): + entry["tags"].append(name) + + # Set document boost + search = page.meta.get("search", {}) + if "boost" in search: + entry["boost"] = search["boost"] + + # Add entry to index + self.entries.append(entry) + + # Generate search index + def generate_search_index(self, prev): + config = { + key: self.config[key] + for key in ["lang", "separator", "pipeline"] + } + + # Hack: if we're running under dirty reload, the search index will only + # include the entries for the current page. However, MkDocs > 1.4 allows + # us to persist plugin state across rebuilds, which is exactly what we + # do by passing the previously built index to this method. Thus, we just + # remove the previous entries for the current page, and append the new + # entries to the end of the index, as order doesn't matter. + if prev and self.entries: + path = self.entries[0]["location"] + + # Since we're sure that we're running under dirty reload, the list + # of entries will only contain sections for a single page. Thus, we + # use the first entry to remove all entries from the previous run + # that belong to the current page. The rationale behind this is that + # authors might add or remove section headers, so we need to make + # sure that sections are synchronized correctly. + entries = [ + entry for entry in prev.entries + if not entry["location"].startswith(path) + ] + + # Merge previous with current entries + self.entries = entries + self.entries + + # Otherwise just set previous entries + if prev and not self.entries: + self.entries = prev.entries + + # Return search index as JSON + data = { "config": config, "docs": self.entries } + return json.dumps( + data, + separators = (",", ":"), + default = str + ) + + # ------------------------------------------------------------------------- + + # Retrieve item for anchor + def _find_toc_by_id(self, toc, id): + for toc_item in toc: + if toc_item.id == id: + return toc_item + + # Recurse into children of item + toc_item = self._find_toc_by_id(toc_item.children, id) + if toc_item is not None: + return toc_item + + # No item found + return None + +# ----------------------------------------------------------------------------- + +# HTML element +class Element: + """ + An element with attributes, essentially a small wrapper object for the + parser to access attributes in other callbacks than handle_starttag. + """ + + # Initialize HTML element + def __init__(self, tag, attrs = dict()): + self.tag = tag + self.attrs = attrs + + # Support comparison (compare by tag only) + def __eq__(self, other): + if other is Element: + return self.tag == other.tag + else: + return self.tag == other + + # Support set operations + def __hash__(self): + return hash(self.tag) + + # Check whether the element should be excluded + def is_excluded(self): + return "data-search-exclude" in self.attrs + +# ----------------------------------------------------------------------------- + +# HTML section +class Section: + """ + A block of text with markup, preceded by a title (with markup), i.e., a + headline with a certain level (h1-h6). Internally used by the parser. + """ + + # Initialize HTML section + def __init__(self, el): + self.el = el + self.text = [] + self.title = [] + self.id = None + + # Check whether the section should be excluded + def is_excluded(self): + return self.el.is_excluded() + +# ----------------------------------------------------------------------------- + +# HTML parser +class Parser(HTMLParser): + """ + This parser divides the given string of HTML into a list of sections, each + of which are preceded by a h1-h6 level heading. A white- and blacklist of + tags dictates which tags should be preserved as part of the index, and + which should be ignored in their entirety. + """ + + # Initialize HTML parser + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Tags to skip + self.skip = set([ + "object", # Objects + "script", # Scripts + "style" # Styles + ]) + + # Tags to keep + self.keep = set([ + "p", # Paragraphs + "code", "pre", # Code blocks + "li", "ol", "ul" # Lists + ]) + + # Current context and section + self.context = [] + self.section = None + + # All parsed sections + self.data = [] + + # Called at the start of every HTML tag + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + + # Ignore self-closing tags + el = Element(tag, attrs) + if not tag in void: + self.context.append(el) + else: + return + + # Handle headings + if tag in ([f"h{x}" for x in range(1, 7)]): + if "id" in attrs: + + # Ensure top-level section + if tag != "h1" and not self.data: + self.section = Section(Element("hx")) + self.data.append(self.section) + + # Set identifier, if not first section + self.section = Section(el) + if self.data: + self.section.id = attrs["id"] + + # Append section to list + self.data.append(self.section) + + # Handle preface - ensure top-level section + if not self.section: + self.section = Section(Element("hx")) + self.data.append(self.section) + + # Handle special cases to skip + for key, value in attrs.items(): + + # Skip block if explicitly excluded from search + if key == "data-search-exclude": + self.skip.add(el) + return + + # Skip line numbers - see https://bit.ly/3GvubZx + if key == "class" and value == "linenodiv": + self.skip.add(el) + return + + # Render opening tag if kept + if not self.skip.intersection(self.context): + if tag in self.keep: + data = self.section.text + if self.section.el in reversed(self.context): + data = self.section.title + + # Append to section title or text + data.append(f"<{tag}>") + + # Called at the end of every HTML tag + def handle_endtag(self, tag): + if not self.context or self.context[-1] != tag: + return + + # Remove element from skip list + el = self.context.pop() + if el in self.skip: + self.skip.remove(el) + return + + # Render closing tag if kept + if not self.skip.intersection(self.context): + if tag in self.keep: + data = self.section.text + if self.section.el in reversed(self.context): + data = self.section.title + + # Remove element if empty (or only whitespace) + prev, last = data[-2:] + if last == f"<{tag}>": + del data[len(data) - 1:] + elif last.isspace() and prev == f"<{tag}>": + del data[len(data) - 2:] + + # Append to section title or text + else: + data.append(f"") + + # Called for the text contents of each tag + def handle_data(self, data): + if self.skip.intersection(self.context): + return + + # Collapse whitespace in non-pre contexts + if not "pre" in self.context: + if not data.isspace(): + data = data.replace("\n", " ") else: - log.warning( - "Skipping 'tags' due to invalid syntax [%s]: %s", - page.file.src_uri, - page.meta["tags"] + data = " " + + # Handle preface - ensure top-level section + if not self.section: + self.section = Section(Element("hx")) + self.data.append(self.section) + + # Handle section headline + if self.section.el in reversed(self.context): + permalink = False + for el in self.context: + if el.tag == "a" and el.attrs.get("class") == "headerlink": + permalink = True + + # Ignore permalinks + if not permalink: + self.section.title.append( + escape(data, quote = False) ) - # Add document boost for search - if "search" in page.meta: - search = page.meta["search"] - if "boost" in search: - for entry in self._entries[index:]: - entry["boost"] = search["boost"] + # Handle everything else + else: + self.section.text.append( + escape(data, quote = False) + ) # ----------------------------------------------------------------------------- # Data @@ -74,3 +465,21 @@ class SearchIndex(BaseIndex): # Set up logging log = logging.getLogger("mkdocs") log.addFilter(DuplicateFilter()) + +# Tags that are self-closing +void = set([ + "area", # Image map areas + "base", # Document base + "br", # Line breaks + "col", # Table columns + "embed", # External content + "hr", # Horizontal rules + "img", # Images + "input", # Input fields + "link", # Links + "meta", # Metadata + "param", # External parameters + "source", # Image source sets + "track", # Text track + "wbr" # Line break opportunities +]) diff --git a/src/plugins/social/plugin.py b/src/plugins/social/plugin.py index f5727fc78..76bf1c7d6 100644 --- a/src/plugins/social/plugin.py +++ b/src/plugins/social/plugin.py @@ -80,7 +80,7 @@ class SocialPlugin(BasePlugin[SocialPluginConfig]): "Required dependencies of \"social\" plugin not found. " "Install with: pip install pillow cairosvg" ) - sys.exit() + sys.exit(1) # Check if site URL is defined if not config.site_url: diff --git a/src/plugins/tags/plugin.py b/src/plugins/tags/plugin.py index a040e68c6..e01818a63 100644 --- a/src/plugins/tags/plugin.py +++ b/src/plugins/tags/plugin.py @@ -92,7 +92,7 @@ class TagsPlugin(BasePlugin[TagsPluginConfig]): file = files.get_file_from_path(path) if not file: log.error(f"Tags file '{path}' does not exist.") - sys.exit() + sys.exit(1) # Add tags file to files files.append(file) diff --git a/tools/build/_/index.ts b/tools/build/_/index.ts index 3a424905c..5c4ba97e1 100644 --- a/tools/build/_/index.ts +++ b/tools/build/_/index.ts @@ -107,7 +107,7 @@ function now() { export function resolve( pattern: string, options?: ResolveOptions ): Observable { - return from(glob(pattern, { ...options, dot: true })) + return from(glob(pattern, { dot: true, ...options })) .pipe( catchError(() => EMPTY), concatAll(), diff --git a/typings/_/index.d.ts b/typings/_/index.d.ts index c16187ba3..2ebe12b96 100644 --- a/typings/_/index.d.ts +++ b/typings/_/index.d.ts @@ -24,33 +24,13 @@ import { Observable, Subject } from "rxjs" import { Keyboard, Viewport } from "~/browser" import { Component } from "~/components" -import { - SearchIndex, - SearchTransformFn -} from "~/integrations" /* ---------------------------------------------------------------------------- * Global types * ------------------------------------------------------------------------- */ -/** - * Global search configuration - */ -export interface GlobalSearchConfig { - transform?: SearchTransformFn /* Transformation function */ - index?: Promise /* Alternate index */ - worker?: string /* Alternate worker URL */ -} - -/* ------------------------------------------------------------------------- */ - declare global { - /** - * Global search configuration - */ - const __search: GlobalSearchConfig | undefined - /** * Compute hash from the given string * diff --git a/typings/lunr/index.d.ts b/typings/lunr/index.d.ts index a51d27daa..4b10e3783 100644 --- a/typings/lunr/index.d.ts +++ b/typings/lunr/index.d.ts @@ -27,5 +27,117 @@ import lunr from "lunr" * ------------------------------------------------------------------------- */ declare global { - const lunr: typeof lunr /* Global Lunr.js namespace */ + namespace lunr { + + /** + * Index - expose inverted index + */ + interface Index { + invertedIndex: Record + fields: string[] // @todo: make typing generic? + } + + interface Builder { + field( + fieldName: string, + attributes?: { + boost?: number | undefined, + extractor?: Function + }): void; + } + + /** + * Query parser + */ + class QueryParser { + constructor(value: string, query: Query) + public parse(): void + } + + /** + * Query clause - add missing field definitions + */ + namespace Query { + interface Clause { + presence: Query.presence + } + } + + /** + * Tokenizer + */ + namespace tokenizer { + let table: number[][] + } + + /** + * Lexeme type + */ + const enum LexemeType { + FIELD = "FIELD", + TERM = "TERM", + PRESENCE = "PRESENCE" + } + + /** + * Lexeme + */ + interface Lexeme { + type: LexemeType + str: string + start: number + end: number + } + + /** + * Query lexer - add missing class definitions + */ + class QueryLexer { + + /** + * Create query lexer + * + * @param query - Query + */ + constructor(query: string) + + /** + * Query lexemes + */ + public lexemes: Lexeme[] + + /** + * Lex query + */ + public run(): void + } + + /** + * Enable multi-language support + * + * @param lang - Languages + * + * @returns Plugin + */ + function multiLanguage(...lang: string[]): Builder.Plugin + + /** + * Stopword filter + * + * @template T - Token type + * + * @param token - Token or string + * + * @returns Token or nothing + */ + function stopWordFilter(token: T): T | undefined; + + /** + * Segmenter for Japanese + */ + class TinySegmenter { + public ctype_(value: string): string + public segment(value: string): string[] + } + } }