diff --git a/eleventy.config.js b/eleventy.config.js index 5f4b2512..0da021ff 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -33,7 +33,7 @@ export default async function (eleventyConfig) { if (!entry.isSymbolicLink()) continue; const realPath = fs.realpathSync(path.join("plugins-local", entry.name)); eleventyConfig.addWatchTarget( - `${realPath}/{manifest.json,main.js,styles.css}`, + `${realPath}/{manifest.json,main.js,styles.css,README.md}`, ); } } @@ -68,6 +68,10 @@ export default async function (eleventyConfig) { if (fs.existsSync(stylesPath)) { fs.copyFileSync(stylesPath, path.join(destDir, "styles.css")); } + const readmePath = path.join(pluginPath, "README.md"); + if (fs.existsSync(readmePath)) { + fs.copyFileSync(readmePath, path.join(destDir, "README.md")); + } } fs.writeFileSync( "build/plugins-local/index.json", @@ -88,6 +92,13 @@ export default async function (eleventyConfig) { if (url.pathname.includes("reload-client.js")) { return null; } + if (path.extname(url.pathname)) { + return { + status: 404, + headers: { "Content-Type": "text/plain" }, + body: "Not Found", + }; + } return fs.readFileSync("build/index.html", "utf-8"); }, }, diff --git a/package.json b/package.json index 223655d0..660ea783 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.14.121", + "version": "0.14.122", "type": "module", "scripts": { "start": "rm -rf build && NODE_ENV=development eleventy --serve", diff --git a/src/css/style.css b/src/css/style.css index 0ca7bd69..4104057a 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -7495,6 +7495,195 @@ image-cropper { opacity: 0.5; } +.plugin-installed-badge { + font-size: 12px; + font-weight: 400; + background: light-dark( + color-mix(in srgb, var(--highlight-color) 12%, var(--white)), + color-mix(in srgb, var(--highlight-color) 16%, var(--mid-gray)) + ); + color: var(--highlight-color); + padding: 3px 8px; + border-radius: var(--tag-border-radius); + display: inline-block; + min-width: fit-content; + margin-left: 5px; + vertical-align: middle; + margin-bottom: 3px; +} + +/* Community plugin rows are whole-row navigation links */ +#settings-community-plugins-view .plugin-list-item { + padding: 0; +} + +.plugin-list-item-link { + display: flex; + flex: 1; + min-width: 0; + align-items: center; + gap: 24px; + padding: 16px; + text-decoration: none; + color: inherit; +} + +.plugin-list-item-link:hover { + background: var(--post-hover-color); +} + +.plugin-listing-header { + padding: 20px 16px; +} + +.plugin-listing-name { + margin: 0 0 16px; + font-size: 18px; + font-weight: 700; + line-height: 1.1; +} + +.plugin-listing-name .plugin-local-badge { + font-size: 14px; +} + +.plugin-listing-meta { + display: flex; + flex-direction: column; + gap: 2px; + font-size: 14px; + color: var(--text-color-secondary); +} + +.plugin-listing-meta a { + color: var(--primary-button-color); + text-decoration: none; + word-break: break-all; +} + +.plugin-listing-meta a:hover { + text-decoration: underline; +} + +.plugin-listing-description { + margin: 16px 0 0; + font-size: 16px; + line-height: 1.5; +} + +.plugin-listing-actions { + display: flex; + gap: 12px; + margin-top: 16px; +} + +.plugin-listing-readme { + padding: 0px 16px calc(var(--footer-height) + var(--safe-area-inset-bottom)); +} + +#settings-community-plugin-listing-view rendered-markdown { + display: block; + font-size: 15px; + line-height: 1.6; + color: var(--text-color); + word-break: break-word; +} + +#settings-community-plugin-listing-view rendered-markdown h1, +#settings-community-plugin-listing-view rendered-markdown h2, +#settings-community-plugin-listing-view rendered-markdown h3, +#settings-community-plugin-listing-view rendered-markdown h4 { + line-height: 1.25; + margin: 24px 0 12px; +} + +#settings-community-plugin-listing-view rendered-markdown h1 { + font-size: 24px; +} + +#settings-community-plugin-listing-view rendered-markdown h2 { + font-size: 20px; +} + +#settings-community-plugin-listing-view rendered-markdown h3 { + font-size: 17px; +} + +#settings-community-plugin-listing-view + rendered-markdown + :is(h1, h2, h3, h4):first-child { + margin-top: 0; +} + +#settings-community-plugin-listing-view rendered-markdown p, +#settings-community-plugin-listing-view rendered-markdown ul, +#settings-community-plugin-listing-view rendered-markdown ol { + margin: 12px 0; +} + +rendered-markdown ul, +rendered-markdown ol { + padding-left: 24px; +} + +rendered-markdown ul { + list-style: disc; +} + +rendered-markdown ol { + list-style: decimal; +} + +rendered-markdown li { + margin: 4px 0; +} + +rendered-markdown li > ul, +rendered-markdown li > ol { + margin: 4px 0; +} + +rendered-markdown ul:has(> li > input[type="checkbox"]) { + list-style: none; + padding-left: 0; +} + +#settings-community-plugin-listing-view rendered-markdown a { + color: var(--primary-button-color); +} + +#settings-community-plugin-listing-view rendered-markdown img { + max-width: 100%; + height: auto; +} + +#settings-community-plugin-listing-view rendered-markdown code { + font-family: var(--mono-font, monospace); + font-size: 0.9em; + background: var(--primary-button-muted-color); + padding: 2px 5px; + border-radius: var(--tag-border-radius); +} + +#settings-community-plugin-listing-view rendered-markdown pre { + background: var(--primary-button-muted-color); + padding: 12px; + border-radius: var(--callout-border-radius); + overflow-x: auto; +} + +#settings-community-plugin-listing-view rendered-markdown pre code { + background: none; + padding: 0; +} + +#settings-community-plugin-listing-view rendered-markdown blockquote { + margin: 12px 0; + padding-left: 12px; + border-left: 3px solid var(--post-border-color); + color: var(--text-color-secondary); +} + .plugin-settings-tab h1, .plugin-settings-tab h2, .plugin-settings-tab h3, diff --git a/src/index.html b/src/index.html index 6a3d4889..7dfa77a6 100644 --- a/src/index.html +++ b/src/index.html @@ -86,6 +86,7 @@ import settingsPluginsView from "/js/views/settings/plugins.view.js"; import settingsPluginDetailView from "/js/views/settings/pluginDetail.view.js"; import settingsCommunityPluginsView from "/js/views/settings/communityPlugins.view.js"; + import settingsCommunityPluginListingView from "/js/views/settings/communityPluginListing.view.js"; import feedDetailView from "/js/views/feedDetail.view.js"; import bookmarksView from "/js/views/bookmarks.view.js"; import { DataLayer } from "/js/dataLayer/dataLayer.js"; @@ -298,6 +299,10 @@ "/settings/plugins/community", () => settingsCommunityPluginsView, ); + router.addRoute( + "/settings/plugins/community/:pluginId", + () => settingsCommunityPluginListingView, + ); router.addRoute( "/settings/plugins/:pluginId", () => settingsPluginDetailView, diff --git a/src/js/components/rendered-markdown.js b/src/js/components/rendered-markdown.js new file mode 100644 index 00000000..0a46998d --- /dev/null +++ b/src/js/components/rendered-markdown.js @@ -0,0 +1,52 @@ +import { Component } from "/js/components/component.js"; +import { Signal, effect } from "/js/signals.js"; + +class RenderedMarkdown extends Component { + connectedCallback() { + if (this.initialized) return; + this.initialized = true; + this.$dependencies = new Signal.State(null); + this.$content = new Signal.State(this.getAttribute("content") || ""); + // Fetch dependencies + Promise.all([ + import("/js/lib/dompurify.js"), + import("/js/lib/marked.js"), + ]).then(([{ default: DOMPurify }, { marked }]) => { + this.$dependencies.set({ DOMPurify, marked }); + }); + this.dispose = effect(() => { + const dependencies = this.$dependencies.get(); + if (!dependencies) return; + const { DOMPurify, marked } = dependencies; + const content = this.$content.get(); + this.innerHTML = DOMPurify.sanitize(marked.parse(content)); + }); + // Treat links inside rendered markdown as external + this.onLinkClick = (event) => { + if (event.target.closest("a")) { + event.stopPropagation(); + } + }; + this.addEventListener("click", this.onLinkClick); + } + + static get observedAttributes() { + return ["content"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + if (!this.initialized || oldValue === newValue) return; + if (name === "content") { + this.$content.set(newValue); + } + } + + disconnectedCallback() { + if (!this.initialized) return; + this.dispose?.(); + this.dispose = null; + this.removeEventListener("click", this.onLinkClick); + } +} + +RenderedMarkdown.register(); diff --git a/src/js/lib/dompurify.js b/src/js/lib/dompurify.js new file mode 100644 index 00000000..0ab0e68a --- /dev/null +++ b/src/js/lib/dompurify.js @@ -0,0 +1,10 @@ +/** + * Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0. + * Original file: /npm/dompurify@3.4.7/dist/purify.es.mjs + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +/*! @license DOMPurify 3.4.7 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.7/LICENSE */ +function e(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=Array(t);n2?n-2:0),r=2;r1?t-1:0),o=1;o1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:b;if(o&&o(e,null),!T(t))return e;let i=t.length;for(;i--;){let o=t[i];if("string"==typeof o){const e=n(o);e!==o&&(r(t)||(t[i]=e),o=e)}e[o]=!0}return e}function z(e){for(let t=0;t/g),J=c(/\${[\w\W]*/g),Q=c(/^data-[\-\w.\u00B7-\uFFFF]+$/),ee=c(/^aria-[\-\w]+$/),te=c(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),ne=c(/^(?:\w+script|data):/i),oe=c(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),re=c(/^html$/i),ie=c(/^[a-z][.\w]*(-[.\w]+)+$/i),ae=1,le=3,ce=7,se=8,ue=9,me=11,pe=function(){return"undefined"==typeof window?null:window};var fe=function e(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:pe();const o=t=>e(t);if(o.version="3.4.7",o.removed=[],!t||!t.document||t.document.nodeType!==ue||!t.Element)return o.isSupported=!1,o;let r=t.document;const i=r,a=i.currentScript;t.DocumentFragment;const c=t.HTMLTemplateElement,u=t.Node,m=t.Element,p=t.NodeFilter,k=t.NamedNodeMap;void 0===k&&(t.NamedNodeMap||t.MozNamedAttrMap),t.HTMLFormElement;const L=t.DOMParser,z=t.trustedTypes,fe=m.prototype,de=U(fe,"cloneNode"),he=U(fe,"remove"),ge=U(fe,"nextSibling"),ye=U(fe,"childNodes"),Te=U(fe,"parentNode"),be=U(fe,"shadowRoot"),Ae=U(fe,"attributes"),Se=u&&u.prototype?U(u.prototype,"nodeType"):null,Ee=u&&u.prototype?U(u.prototype,"nodeName"):null;if("function"==typeof c){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let Ne,_e="";const Oe=r,De=Oe.implementation,Re=Oe.createNodeIterator,we=Oe.createDocumentFragment,Ie=Oe.getElementsByTagName,ve=i.importNode;let Ce={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};o.isSupported="function"==typeof n&&"function"==typeof Te&&De&&void 0!==De.createHTMLDocument;const xe=V,ke=Z,Le=J,Me=Q,ze=ee,Fe=ne,Ue=oe,Pe=ie;let He=te,Be=null;const Ge=M({},[...P,...H,...B,...j,...Y]);let je=null;const We=M({},[...X,...q,...$,...K]);let Ye=Object.seal(s(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Xe=null,qe=null;const $e=Object.seal(s(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let Ke=!0,Ve=!0,Ze=!1,Je=!0,Qe=!1,et=!0,tt=!1,nt=!1,ot=!1,rt=!1,it=!1,at=!1,lt=!0,ct=!1;const st="user-content-";let ut=!0,mt=!1,pt={},ft=null;const dt=M({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let ht=null;const gt=M({},["audio","video","img","source","image","track"]);let yt=null;const Tt=M({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),bt="http://www.w3.org/1998/Math/MathML",At="http://www.w3.org/2000/svg",St="http://www.w3.org/1999/xhtml";let Et=St,Nt=!1,_t=null;const Ot=M({},[bt,At,St],A);let Dt=M({},["mi","mo","mn","ms","mtext"]),Rt=M({},["annotation-xml"]);const wt=M({},["title","style","font","a","script"]);let It=null;const vt=["application/xhtml+xml","text/html"];let Ct=null,xt=null;const kt=r.createElement("form"),Lt=function(e){return e instanceof RegExp||e instanceof Function},Mt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(xt&&xt===e)return;e&&"object"==typeof e||(e={}),e=F(e),It=-1===vt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,Ct="application/xhtml+xml"===It?A:b,Be=I(e,"ALLOWED_TAGS")&&T(e.ALLOWED_TAGS)?M({},e.ALLOWED_TAGS,Ct):Ge,je=I(e,"ALLOWED_ATTR")&&T(e.ALLOWED_ATTR)?M({},e.ALLOWED_ATTR,Ct):We,_t=I(e,"ALLOWED_NAMESPACES")&&T(e.ALLOWED_NAMESPACES)?M({},e.ALLOWED_NAMESPACES,A):Ot,yt=I(e,"ADD_URI_SAFE_ATTR")&&T(e.ADD_URI_SAFE_ATTR)?M(F(Tt),e.ADD_URI_SAFE_ATTR,Ct):Tt,ht=I(e,"ADD_DATA_URI_TAGS")&&T(e.ADD_DATA_URI_TAGS)?M(F(gt),e.ADD_DATA_URI_TAGS,Ct):gt,ft=I(e,"FORBID_CONTENTS")&&T(e.FORBID_CONTENTS)?M({},e.FORBID_CONTENTS,Ct):dt,Xe=I(e,"FORBID_TAGS")&&T(e.FORBID_TAGS)?M({},e.FORBID_TAGS,Ct):F({}),qe=I(e,"FORBID_ATTR")&&T(e.FORBID_ATTR)?M({},e.FORBID_ATTR,Ct):F({}),pt=!!I(e,"USE_PROFILES")&&(e.USE_PROFILES&&"object"==typeof e.USE_PROFILES?F(e.USE_PROFILES):e.USE_PROFILES),Ke=!1!==e.ALLOW_ARIA_ATTR,Ve=!1!==e.ALLOW_DATA_ATTR,Ze=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Je=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,Qe=e.SAFE_FOR_TEMPLATES||!1,et=!1!==e.SAFE_FOR_XML,tt=e.WHOLE_DOCUMENT||!1,rt=e.RETURN_DOM||!1,it=e.RETURN_DOM_FRAGMENT||!1,at=e.RETURN_TRUSTED_TYPE||!1,ot=e.FORCE_BODY||!1,lt=!1!==e.SANITIZE_DOM,ct=e.SANITIZE_NAMED_PROPS||!1,ut=!1!==e.KEEP_CONTENT,mt=e.IN_PLACE||!1,He=function(e){try{return C(e,""),!0}catch(e){return!1}}(e.ALLOWED_URI_REGEXP)?e.ALLOWED_URI_REGEXP:te,Et="string"==typeof e.NAMESPACE?e.NAMESPACE:St,Dt=I(e,"MATHML_TEXT_INTEGRATION_POINTS")&&e.MATHML_TEXT_INTEGRATION_POINTS&&"object"==typeof e.MATHML_TEXT_INTEGRATION_POINTS?F(e.MATHML_TEXT_INTEGRATION_POINTS):M({},["mi","mo","mn","ms","mtext"]),Rt=I(e,"HTML_INTEGRATION_POINTS")&&e.HTML_INTEGRATION_POINTS&&"object"==typeof e.HTML_INTEGRATION_POINTS?F(e.HTML_INTEGRATION_POINTS):M({},["annotation-xml"]);const t=I(e,"CUSTOM_ELEMENT_HANDLING")&&e.CUSTOM_ELEMENT_HANDLING&&"object"==typeof e.CUSTOM_ELEMENT_HANDLING?F(e.CUSTOM_ELEMENT_HANDLING):s(null);if(Ye=s(null),I(t,"tagNameCheck")&&Lt(t.tagNameCheck)&&(Ye.tagNameCheck=t.tagNameCheck),I(t,"attributeNameCheck")&&Lt(t.attributeNameCheck)&&(Ye.attributeNameCheck=t.attributeNameCheck),I(t,"allowCustomizedBuiltInElements")&&"boolean"==typeof t.allowCustomizedBuiltInElements&&(Ye.allowCustomizedBuiltInElements=t.allowCustomizedBuiltInElements),Qe&&(Ve=!1),it&&(rt=!0),pt&&(Be=M({},Y),je=s(null),!0===pt.html&&(M(Be,P),M(je,X)),!0===pt.svg&&(M(Be,H),M(je,q),M(je,K)),!0===pt.svgFilters&&(M(Be,B),M(je,q),M(je,K)),!0===pt.mathMl&&(M(Be,j),M(je,$),M(je,K))),$e.tagCheck=null,$e.attributeCheck=null,I(e,"ADD_TAGS")&&("function"==typeof e.ADD_TAGS?$e.tagCheck=e.ADD_TAGS:T(e.ADD_TAGS)&&(Be===Ge&&(Be=F(Be)),M(Be,e.ADD_TAGS,Ct))),I(e,"ADD_ATTR")&&("function"==typeof e.ADD_ATTR?$e.attributeCheck=e.ADD_ATTR:T(e.ADD_ATTR)&&(je===We&&(je=F(je)),M(je,e.ADD_ATTR,Ct))),I(e,"ADD_URI_SAFE_ATTR")&&T(e.ADD_URI_SAFE_ATTR)&&M(yt,e.ADD_URI_SAFE_ATTR,Ct),I(e,"FORBID_CONTENTS")&&T(e.FORBID_CONTENTS)&&(ft===dt&&(ft=F(ft)),M(ft,e.FORBID_CONTENTS,Ct)),I(e,"ADD_FORBID_CONTENTS")&&T(e.ADD_FORBID_CONTENTS)&&(ft===dt&&(ft=F(ft)),M(ft,e.ADD_FORBID_CONTENTS,Ct)),ut&&(Be["#text"]=!0),tt&&M(Be,["html","head","body"]),Be.table&&(M(Be,["tbody"]),delete Xe.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw x('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw x('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');Ne=e.TRUSTED_TYPES_POLICY,_e=Ne.createHTML("")}else void 0===Ne&&(Ne=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(z,a)),null!==Ne&&"string"==typeof _e&&(_e=Ne.createHTML(""));(Ce.uponSanitizeElement.length>0||Ce.uponSanitizeAttribute.length>0)&&Be===Ge&&(Be=F(Be)),Ce.uponSanitizeAttribute.length>0&&je===We&&(je=F(je)),l&&l(e),xt=e},zt=M({},[...H,...B,...G]),Ft=M({},[...j,...W]),Ut=function(e){g(o.removed,{element:e});try{Te(e).removeChild(e)}catch(t){he(e)}},Pt=function(e,t){try{g(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){g(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(rt||it)try{Ut(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Ht=function(e){let t=null,n=null;if(ot)e=""+e;else{const t=S(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===It&&Et===St&&(e=''+e+"");const o=Ne?Ne.createHTML(e):e;if(Et===St)try{t=(new L).parseFromString(o,It)}catch(e){}if(!t||!t.documentElement){t=De.createDocument(Et,"template",null);try{t.documentElement.innerHTML=Nt?_e:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),Et===St?Ie.call(t,tt?"html":"body")[0]:tt?t.documentElement:i},Bt=function(e){return Re.call(e.ownerDocument||e,e,p.SHOW_ELEMENT|p.SHOW_COMMENT|p.SHOW_TEXT|p.SHOW_PROCESSING_INSTRUCTION|p.SHOW_CDATA_SECTION,null)},Gt=function(e){e.normalize();const t=Re.call(e.ownerDocument||e,e,p.SHOW_TEXT|p.SHOW_COMMENT|p.SHOW_CDATA_SECTION|p.SHOW_PROCESSING_INSTRUCTION,null);let n=t.nextNode();for(;n;){let e=n.data;f([xe,ke,Le],(t=>{e=E(e,t," ")})),n.data=e,n=t.nextNode()}},jt=function(e){const t=Ee?Ee(e):null;return"string"==typeof t&&("form"===Ct(t)&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||e.attributes!==Ae(e)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes||e.nodeType!==Se(e)||e.childNodes!==ye(e)))},Wt=function(e){if(!Se||"object"!=typeof e||null===e)return!1;try{return Se(e)===me}catch(e){return!1}},Yt=function(e){if(!Se||"object"!=typeof e||null===e)return!1;try{return"number"==typeof Se(e)}catch(e){return!1}};function Xt(e,t,n){f(e,(e=>{e.call(o,t,n,xt)}))}const qt=function(e){let t=null;if(Xt(Ce.beforeSanitizeElements,e,null),jt(e))return Ut(e),!0;const n=Ct(e.nodeName);if(Xt(Ce.uponSanitizeElement,e,{tagName:n,allowedTags:Be}),et&&e.hasChildNodes()&&!Yt(e.firstElementChild)&&C(/<[/\w!]/g,e.innerHTML)&&C(/<[/\w!]/g,e.textContent))return Ut(e),!0;if(et&&e.namespaceURI===St&&"style"===n&&Yt(e.firstElementChild))return Ut(e),!0;if(e.nodeType===ce)return Ut(e),!0;if(et&&e.nodeType===se&&C(/<[/\w]/g,e.data))return Ut(e),!0;if(Xe[n]||!($e.tagCheck instanceof Function&&$e.tagCheck(n))&&!Be[n]){if(!Xe[n]&&Vt(n)){if(Ye.tagNameCheck instanceof RegExp&&C(Ye.tagNameCheck,n))return!1;if(Ye.tagNameCheck instanceof Function&&Ye.tagNameCheck(n))return!1}if(ut&&!ft[n]){const t=Te(e),n=ye(e);if(n&&t){for(let o=n.length-1;o>=0;--o){const r=de(n[o],!0);t.insertBefore(r,ge(e))}}}return Ut(e),!0}return((Se?Se(e):e.nodeType)!==ae||function(e){let t=Te(e);t&&t.tagName||(t={namespaceURI:Et,tagName:"template"});const n=b(e.tagName),o=b(t.tagName);return!!_t[e.namespaceURI]&&(e.namespaceURI===At?t.namespaceURI===St?"svg"===n:t.namespaceURI===bt?"svg"===n&&("annotation-xml"===o||Dt[o]):Boolean(zt[n]):e.namespaceURI===bt?t.namespaceURI===St?"math"===n:t.namespaceURI===At?"math"===n&&Rt[o]:Boolean(Ft[n]):e.namespaceURI===St?!(t.namespaceURI===At&&!Rt[o])&&!(t.namespaceURI===bt&&!Dt[o])&&!Ft[n]&&(wt[n]||!zt[n]):!("application/xhtml+xml"!==It||!_t[e.namespaceURI]))}(e))&&("noscript"!==n&&"noembed"!==n&&"noframes"!==n||!C(/<\/no(script|embed|frames)/i,e.innerHTML))?(Qe&&e.nodeType===le&&(t=e.textContent,f([xe,ke,Le],(e=>{t=E(t,e," ")})),e.textContent!==t&&(g(o.removed,{element:e.cloneNode()}),e.textContent=t)),Xt(Ce.afterSanitizeElements,e,null),!1):(Ut(e),!0)},$t=function(e,t,n){if(qe[t])return!1;if(lt&&("id"===t||"name"===t)&&(n in r||n in kt))return!1;const o=je[t]||$e.attributeCheck instanceof Function&&$e.attributeCheck(t,e);if(Ve&&!qe[t]&&C(Me,t));else if(Ke&&C(ze,t));else if(!o||qe[t]){if(!(Vt(e)&&(Ye.tagNameCheck instanceof RegExp&&C(Ye.tagNameCheck,e)||Ye.tagNameCheck instanceof Function&&Ye.tagNameCheck(e))&&(Ye.attributeNameCheck instanceof RegExp&&C(Ye.attributeNameCheck,t)||Ye.attributeNameCheck instanceof Function&&Ye.attributeNameCheck(t,e))||"is"===t&&Ye.allowCustomizedBuiltInElements&&(Ye.tagNameCheck instanceof RegExp&&C(Ye.tagNameCheck,n)||Ye.tagNameCheck instanceof Function&&Ye.tagNameCheck(n))))return!1}else if(yt[t]);else if(C(He,E(n,Ue,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==N(n,"data:")||!ht[e]){if(Ze&&!C(Fe,E(n,Ue,"")));else if(n)return!1}else;return!0},Kt=M({},["annotation-xml","color-profile","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","missing-glyph"]),Vt=function(e){return!Kt[b(e)]&&C(Pe,e)},Zt=function(e){Xt(Ce.beforeSanitizeAttributes,e,null);const t=e.attributes;if(!t||jt(e))return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:je,forceKeepAttr:void 0};let r=t.length;for(;r--;){const i=t[r],a=i.name,l=i.namespaceURI,c=i.value,s=Ct(a),u=c;let m="value"===a?u:_(u);if(n.attrName=s,n.attrValue=m,n.keepAttr=!0,n.forceKeepAttr=void 0,Xt(Ce.uponSanitizeAttribute,e,n),m=n.attrValue,!ct||"id"!==s&&"name"!==s||0===N(m,st)||(Pt(a,e),m=st+m),et&&C(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,m)){Pt(a,e);continue}if("attributename"===s&&S(m,"href")){Pt(a,e);continue}if(n.forceKeepAttr)continue;if(!n.keepAttr){Pt(a,e);continue}if(!Je&&C(/\/>/i,m)){Pt(a,e);continue}Qe&&f([xe,ke,Le],(e=>{m=E(m,e," ")}));const p=Ct(e.nodeName);if($t(p,s,m)){if(Ne&&"object"==typeof z&&"function"==typeof z.getAttributeType)if(l);else switch(z.getAttributeType(p,s)){case"TrustedHTML":m=Ne.createHTML(m);break;case"TrustedScriptURL":m=Ne.createScriptURL(m)}if(m!==u)try{l?e.setAttributeNS(l,a,m):e.setAttribute(a,m),jt(e)?Ut(e):h(o.removed)}catch(t){Pt(a,e)}}else Pt(a,e)}Xt(Ce.afterSanitizeAttributes,e,null)},Jt=function(e){let t=null;const n=Bt(e);for(Xt(Ce.beforeSanitizeShadowDOM,e,null);t=n.nextNode();){Xt(Ce.uponSanitizeShadowNode,t,null),qt(t),Zt(t),Wt(t.content)&&Jt(t.content);if((Se?Se(t):t.nodeType)===ae){const e=be?be(t):t.shadowRoot;Wt(e)&&(Qt(e),Jt(e))}}Xt(Ce.afterSanitizeShadowDOM,e,null)},Qt=function(e){const t=Se?Se(e):e.nodeType;if(t===ae){const t=be?be(e):e.shadowRoot;Wt(t)&&(Qt(t),Jt(t))}const n=ye?ye(e):e.childNodes;if(!n)return;const o=[];f(n,(e=>{g(o,e)}));for(const e of o)Qt(e);if(t===ae){const t=Ee?Ee(e):null;if("string"==typeof t&&"template"===Ct(t)){const t=e.content;Wt(t)&&Qt(t)}}};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,a=null,l=null;if(Nt=!e,Nt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Yt(e)&&"string"!=typeof(e=function(e){switch(typeof e){case"string":return e;case"number":return O(e);case"boolean":return D(e);case"bigint":return R?R(e):"0";case"symbol":return w?w(e):"Symbol()";case"undefined":default:return v(e);case"function":case"object":{if(null===e)return v(e);const t=e,n=U(t,"toString");if("function"==typeof n){const e=n(t);return"string"==typeof e?e:v(e)}return v(e)}}}(e)))throw x("dirty is not a string, aborting");if(!o.isSupported)return e;if(nt||Mt(t),o.removed=[],"string"==typeof e&&(mt=!1),mt){const t=Ee?Ee(e):e.nodeName;if("string"==typeof t){const e=Ct(t);if(!Be[e]||Xe[e])throw x("root node is forbidden and cannot be sanitized in-place")}if(jt(e))throw x("root node is clobbered and cannot be sanitized in-place");Qt(e)}else if(Yt(e))n=Ht("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===ae&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r),Qt(r);else{if(!rt&&!Qe&&!tt&&-1===e.indexOf("<"))return Ne&&at?Ne.createHTML(e):e;if(n=Ht(e),!n)return rt?null:at?_e:""}n&&ot&&Ut(n.firstChild);const c=Bt(mt?e:n);for(;a=c.nextNode();)qt(a),Zt(a),Wt(a.content)&&Jt(a.content);if(mt)return Qe&&Gt(e),e;if(rt){if(Qe&&Gt(n),it)for(l=we.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(je.shadowroot||je.shadowrootmode)&&(l=ve.call(i,l,!0)),l}let s=tt?n.outerHTML:n.innerHTML;return tt&&Be["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&C(re,n.ownerDocument.doctype.name)&&(s="\n"+s),Qe&&f([xe,ke,Le],(e=>{s=E(s,e," ")})),Ne&&at?Ne.createHTML(s):s},o.setConfig=function(){Mt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),nt=!0},o.clearConfig=function(){xt=null,nt=!1},o.isValidAttribute=function(e,t,n){xt||Mt({});const o=Ct(e),r=Ct(t);return $t(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&g(Ce[e],t)},o.removeHook=function(e,t){if(void 0!==t){const n=d(Ce[e],t);return-1===n?void 0:y(Ce[e],n,1)[0]}return h(Ce[e])},o.removeHooks=function(e){Ce[e]=[]},o.removeAllHooks=function(){Ce={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},o}();export{fe as default}; +//# sourceMappingURL=/sm/255090b3bbd98fbff5770f240699c46c5bbaec65e5ed836d6ee43d0139fe7dab.map + diff --git a/src/js/lib/marked.js b/src/js/lib/marked.js new file mode 100644 index 00000000..4bf123f9 --- /dev/null +++ b/src/js/lib/marked.js @@ -0,0 +1,9 @@ +/** + * Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0. + * Original file: /npm/marked@18.0.4/lib/marked.esm.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +function e(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var t={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};function n(e){t=e}var r={exec:()=>null};function s(e){let t=[];return n=>{let r=Math.max(0,Math.min(3,n-1)),s=t[r];return s||(s=e(r),t[r]=s),s}}function l(e,t=""){let n="string"==typeof e?e:e.source,r={replace:(e,t)=>{let s="string"==typeof t?t:t.source;return s=s.replace(a.caret,"$1"),n=n.replace(e,s),r},getRegex:()=>new RegExp(n,t)};return r}var i=((e="")=>{try{return!!new RegExp("(?<=1)(?/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] +\S/,listReplaceTask:/^\[[ xX]\] +/,listTaskCheckbox:/\[[ xX]\]/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:e=>new RegExp(`^( {0,3}${e})((?:[\t ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:s((e=>new RegExp(`^ {0,${e}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`))),hrRegex:s((e=>new RegExp(`^ {0,${e}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`))),fencesBeginRegex:s((e=>new RegExp(`^ {0,${e}}(?:\`\`\`|~~~)`))),headingBeginRegex:s((e=>new RegExp(`^ {0,${e}}#`))),htmlBeginRegex:s((e=>new RegExp(`^ {0,${e}}<(?:[a-z].*>|!--)`,"i"))),blockquoteBeginRegex:s((e=>new RegExp(`^ {0,${e}}>`)))},o=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,c=/ {0,3}(?:[*+-]|\d{1,9}[.)])/,h=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,p=l(h).replace(/bull/g,c).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),u=l(h).replace(/bull/g,c).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),g=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,k=/(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/,d=l(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",k).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),f=l(/^(bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,c).getRegex(),x="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",b=/|$))/,w=l("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$))","i").replace("comment",b).replace("tag",x).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),m=l(g).replace("hr",o).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",x).getRegex(),y={blockquote:l(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",m).getRegex(),code:/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,def:d,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:o,html:w,lheading:p,list:f,newline:/^(?:[ \t]*(?:\n|$))+/,paragraph:m,table:r,text:/^[^\n]+/},$=l("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",o).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3}\t)[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",x).getRegex(),R={...y,lheading:u,table:$,paragraph:l(g).replace("hr",o).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",$).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",x).getRegex()},S={...y,html:l("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",b).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:r,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:l(g).replace("hr",o).replace("heading"," *#{1,6} *[^\n]").replace("lheading",p).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},T=/^( {2,}|\\)\n(?!\s*$)/,z=/[\p{P}\p{S}]/u,A=/[\s\p{P}\p{S}]/u,_=/[^\s\p{P}\p{S}]/u,P=l(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,A).getRegex(),L=/(?!~)[\p{P}\p{S}]/u,I=l(/link|precode-code|html/,"g").replace("link",/\[(?:[^\[\]`]|(?`+)[^`]+\k(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/).replace("precode-",i?"(?`+)[^`]+\k(?!`)/).replace("html",/<(?! )[^<>]*?>/).getRegex(),E=/^(?:\*+(?:((?!\*)punct)|([^\s*]))?)|^_+(?:((?!_)punct)|([^\s_]))?/,C=l(E,"u").replace(/punct/g,z).getRegex(),B=l(E,"u").replace(/punct/g,L).getRegex(),q="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",v=l(q,"gu").replace(/notPunctSpace/g,_).replace(/punctSpace/g,A).replace(/punct/g,z).getRegex(),Z=l(q,"gu").replace(/notPunctSpace/g,/(?:[^\s\p{P}\p{S}]|~)/u).replace(/punctSpace/g,/(?!~)[\s\p{P}\p{S}]/u).replace(/punct/g,L).getRegex(),D=l("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,_).replace(/punctSpace/g,A).replace(/punct/g,z).getRegex(),Q=l(/^~~?(?:((?!~)punct)|[^\s~])/,"u").replace(/punct/g,z).getRegex(),M=l("^[^~]+(?=[^~])|(?!~)punct(~~?)(?=[\\s]|$)|notPunctSpace(~~?)(?!~)(?=punctSpace|$)|(?!~)punctSpace(~~?)(?=notPunctSpace)|[\\s](~~?)(?!~)(?=punct)|(?!~)punct(~~?)(?!~)(?=punct)|notPunctSpace(~~?)(?=notPunctSpace)","gu").replace(/notPunctSpace/g,_).replace(/punctSpace/g,A).replace(/punct/g,z).getRegex(),H=l(/\\(punct)/,"gu").replace(/punct/g,z).getRegex(),O=l(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),j=l(b).replace("(?:--\x3e|$)","--\x3e").getRegex(),N=l("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",j).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),G=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+(?!`)[^`]*?`+(?!`)|``+(?=\])|[^\[\]\\`])*?/,X=l(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]+(?:\n[ \t]*)?|\n[ \t]*)(title))?\s*\)/).replace("label",G).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),W=l(/^!?\[(label)\]\[(ref)\]/).replace("label",G).replace("ref",k).getRegex(),F=l(/^!?\[(ref)\](?:\[\])?/).replace("ref",k).getRegex(),U=/[hH][tT][tT][pP][sS]?|[fF][tT][pP]/,J={_backpedal:r,anyPunctuation:H,autolink:O,blockSkip:I,br:T,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:r,delLDelim:r,delRDelim:r,emStrongLDelim:C,emStrongRDelimAst:v,emStrongRDelimUnd:D,escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,link:X,nolink:F,punctuation:P,reflink:W,reflinkSearch:l("reflink|nolink(?!\\()","g").replace("reflink",W).replace("nolink",F).getRegex(),tag:N,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},re=e=>ne[e];function se(e,t){if(t){if(a.escapeTest.test(e))return e.replace(a.escapeReplace,re)}else if(a.escapeTestNoEncode.test(e))return e.replace(a.escapeReplaceNoEncode,re);return e}function le(e){try{e=encodeURI(e).replace(a.percentDecode,"%")}catch{return null}return e}function ie(e,t){let n=e.replace(a.findPipe,((e,t,n)=>{let r=!1,s=t;for(;--s>=0&&"\\"===n[s];)r=!r;return r?"|":" |"})).split(a.splitPipe),r=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length=0&&a.blankLine.test(t[n]);)n--;return t.length-n<=2?e:t.slice(0,n+1).join("\n")}function ce(e,t=0){let n=t,r="";for(let t of e)if("\t"===t){let e=4-n%4;r+=" ".repeat(e),n+=e}else r+=t,n++;return r}function he(e,t,n,r,s){let l=t.href,i=t.title||null,a=e[1].replace(s.other.outputLinkReplace,"$1");r.state.inLink=!0;let o={type:"!"===e[0].charAt(0)?"image":"link",raw:n,href:l,title:i,text:a,tokens:r.inlineTokens(a)};return r.state.inLink=!1,o}var pe=class{options;rules;lexer;constructor(e){this.options=e||t}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let e=this.options.pedantic?t[0]:oe(t[0]),n=e.replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:e,codeBlockStyle:"indented",text:n}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let e=t[0],n=function(e,t,n){let r=e.match(n.other.indentCodeCompensation);if(null===r)return t;let s=r[1];return t.split("\n").map((e=>{let t=e.match(n.other.beginningSpace);if(null===t)return e;let[r]=t;return r.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"",this.rules);return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(this.rules.other.endingHash.test(e)){let t=ae(e,"#");(this.options.pedantic||!t||this.rules.other.endingSpaceChar.test(t))&&(e=t.trim())}return{type:"heading",raw:ae(t[0],"\n"),depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:ae(t[0],"\n")}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let e=ae(t[0],"\n").split("\n"),n="",r="",s=[];for(;e.length>0;){let t,l=!1,i=[];for(t=0;t1,s={type:"list",raw:"",ordered:r,start:r?+n.slice(0,-1):"",loose:!1,items:[]};n=r?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=r?n:"[*+-]");let l=this.rules.other.listItemRegex(n),i=!1;for(;e;){let n=!1,r="",a="";if(!(t=l.exec(e))||this.rules.block.hr.test(e))break;r=t[0],e=e.substring(r.length);let o=ce(t[2].split("\n",1)[0],t[1].length),c=e.split("\n",1)[0],h=!o.trim(),p=0;if(this.options.pedantic?(p=2,a=o.trimStart()):h?p=t[1].length+1:(p=o.search(this.rules.other.nonSpaceChar),p=p>4?1:p,a=o.slice(p),p+=t[1].length),h&&this.rules.other.blankLine.test(c)&&(r+=c+"\n",e=e.substring(c.length+1),n=!0),!n){let t=this.rules.other.nextBulletRegex(p),n=this.rules.other.hrRegex(p),s=this.rules.other.fencesBeginRegex(p),l=this.rules.other.headingBeginRegex(p),i=this.rules.other.htmlBeginRegex(p),u=this.rules.other.blockquoteBeginRegex(p);for(;e;){let g,k=e.split("\n",1)[0];if(c=k,this.options.pedantic?(c=c.replace(this.rules.other.listReplaceNesting," "),g=c):g=c.replace(this.rules.other.tabCharGlobal," "),s.test(c)||l.test(c)||i.test(c)||u.test(c)||t.test(c)||n.test(c))break;if(g.search(this.rules.other.nonSpaceChar)>=p||!c.trim())a+="\n"+g.slice(p);else{if(h||o.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||s.test(o)||l.test(o)||n.test(o))break;a+="\n"+c}h=!c.trim(),r+=k+"\n",e=e.substring(k.length+1),o=g.slice(p)}}s.loose||(i?s.loose=!0:this.rules.other.doubleBlankLine.test(r)&&(i=!0)),s.items.push({type:"list_item",raw:r,task:!!this.options.gfm&&this.rules.other.listIsTask.test(a),loose:!1,text:a,tokens:[]}),s.raw+=r}let a=s.items.at(-1);if(!a)return;a.raw=a.raw.trimEnd(),a.text=a.text.trimEnd(),s.raw=s.raw.trimEnd();for(let e of s.items){this.lexer.state.top=!1,e.tokens=this.lexer.blockTokens(e.text,[]);let t=e.tokens[0];if(!e.task||"text"!==t?.type&&"paragraph"!==t?.type)e.task&&(e.task=!1);else{e.text=e.text.replace(this.rules.other.listReplaceTask,""),t.raw=t.raw.replace(this.rules.other.listReplaceTask,""),t.text=t.text.replace(this.rules.other.listReplaceTask,"");for(let e=this.lexer.inlineQueue.length-1;e>=0;e--)if(this.rules.other.listIsTask.test(this.lexer.inlineQueue[e].src)){this.lexer.inlineQueue[e].src=this.lexer.inlineQueue[e].src.replace(this.rules.other.listReplaceTask,"");break}let n=this.rules.other.listTaskCheckbox.exec(e.raw);if(n){let t={type:"checkbox",raw:n[0]+" ",checked:"[ ]"!==n[0]};e.checked=t.checked,s.loose?e.tokens[0]&&["paragraph","text"].includes(e.tokens[0].type)&&"tokens"in e.tokens[0]&&e.tokens[0].tokens?(e.tokens[0].raw=t.raw+e.tokens[0].raw,e.tokens[0].text=t.raw+e.tokens[0].text,e.tokens[0].tokens.unshift(t)):e.tokens.unshift({type:"paragraph",raw:t.raw,text:t.raw,tokens:[t]}):e.tokens.unshift(t)}}if(!s.loose){let t=e.tokens.filter((e=>"space"===e.type)),n=t.length>0&&t.some((e=>this.rules.other.anyLine.test(e.raw)));s.loose=n}}if(s.loose)for(let e of s.items){e.loose=!0;for(let t of e.tokens)"text"===t.type&&(t.type="paragraph")}return s}}html(e){let t=this.rules.block.html.exec(e);if(t){let e=oe(t[0]);return{type:"html",block:!0,raw:e,pre:"pre"===t[1]||"script"===t[1]||"style"===t[1],text:e}}}def(e){let t=this.rules.block.def.exec(e);if(t){let e=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal," "),n=t[2]?t[2].replace(this.rules.other.hrefBrackets,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",r=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:ae(t[0],"\n"),href:n,title:r}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=ie(t[1]),r=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),s=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split("\n"):[],l={type:"table",raw:ae(t[0],"\n"),header:[],align:[],rows:[]};if(n.length===r.length){for(let e of r)this.rules.other.tableAlignRight.test(e)?l.align.push("right"):this.rules.other.tableAlignCenter.test(e)?l.align.push("center"):this.rules.other.tableAlignLeft.test(e)?l.align.push("left"):l.align.push(null);for(let e=0;e({text:e,tokens:this.lexer.inline(e),header:!1,align:l.align[t]}))));return l}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t){let e=t[1].trim();return{type:"heading",raw:ae(t[0],"\n"),depth:"="===t[2].charAt(0)?1:2,text:e,tokens:this.lexer.inline(e)}}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let e=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(e)){if(!this.rules.other.endAngleBracket.test(e))return;let t=ae(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{let e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let r=0;r0?-2:-1}(t[2],"()");if(-2===e)return;if(e>-1){let n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],r="";if(this.options.pedantic){let e=this.rules.other.pedanticHrefTitle.exec(n);e&&(n=e[1],r=e[3])}else r=t[3]?t[3].slice(1,-1):"";return n=n.trim(),this.rules.other.startAngleBracket.test(n)&&(n=this.options.pedantic&&!this.rules.other.endAngleBracket.test(e)?n.slice(1):n.slice(1,-1)),he(t,{href:n&&n.replace(this.rules.inline.anyPunctuation,"$1"),title:r&&r.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let e=t[(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," ").toLowerCase()];if(!e){let e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return he(n,e,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let r=this.rules.inline.emStrongLDelim.exec(e);if(r&&(r[1]||r[2]||r[3]||r[4])&&(!r[4]||!n.match(this.rules.other.unicodeAlphaNumeric))&&(!r[1]&&!r[3]||!n||this.rules.inline.punctuation.exec(n))){let n,s,l=[...r[0]].length-1,i=l,a=0,o="*"===r[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(o.lastIndex=0,t=t.slice(-1*e.length+l);null!==(r=o.exec(t));){if(n=r[1]||r[2]||r[3]||r[4]||r[5]||r[6],!n)continue;if(s=[...n].length,r[3]||r[4]){i+=s;continue}if((r[5]||r[6])&&l%3&&!((l+s)%3)){a+=s;continue}if(i-=s,i>0)continue;s=Math.min(s,s+i+a);let t=[...r[0]][0].length,o=e.slice(0,l+r.index+t+s);if(Math.min(l,s)%2){let e=o.slice(1,-1);return{type:"em",raw:o,text:e,tokens:this.lexer.inlineTokens(e)}}let c=o.slice(2,-2);return{type:"strong",raw:o,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(this.rules.other.newLineCharGlobal," "),n=this.rules.other.nonSpaceChar.test(e),r=this.rules.other.startingSpaceChar.test(e)&&this.rules.other.endingSpaceChar.test(e);return n&&r&&(e=e.substring(1,e.length-1)),{type:"codespan",raw:t[0],text:e}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e,t,n=""){let r=this.rules.inline.delLDelim.exec(e);if(r&&(!r[1]||!n||this.rules.inline.punctuation.exec(n))){let n,s,l=[...r[0]].length-1,i=l,a=this.rules.inline.delRDelim;for(a.lastIndex=0,t=t.slice(-1*e.length+l);null!==(r=a.exec(t));){if(n=r[1]||r[2]||r[3]||r[4]||r[5]||r[6],!n||(s=[...n].length,s!==l))continue;if(r[3]||r[4]){i+=s;continue}if(i-=s,i>0)continue;s=Math.min(s,s+i);let t=[...r[0]][0].length,a=e.slice(0,l+r.index+t+s),o=a.slice(l,-l);return{type:"del",raw:a,text:o,tokens:this.lexer.inlineTokens(o)}}}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=t[1],n="mailto:"+e):(e=t[1],n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=t[0],n="mailto:"+e;else{let r;do{r=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(r!==t[0]);e=t[0],n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let e=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:e}}}},ue=class e{tokens;options;state;inlineQueue;tokenizer;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||t,this.options.tokenizer=this.options.tokenizer||new pe,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let n={other:a,block:ee.normal,inline:te.normal};this.options.pedantic?(n.block=ee.pedantic,n.inline=te.pedantic):this.options.gfm&&(n.block=ee.gfm,this.options.breaks?n.inline=te.breaks:n.inline=te.gfm),this.tokenizer.rules=n}static get rules(){return{block:ee,inline:te}}static lex(t,n){return new e(n).lex(t)}static lexInline(t,n){return new e(n).inlineTokens(t)}lex(e){e=e.replace(a.carriageReturn,"\n"),this.blockTokens(e,this.tokens);for(let e=0;e!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0))))continue;if(s=this.tokenizer.space(e)){e=e.substring(s.raw.length);let n=t.at(-1);1===s.raw.length&&void 0!==n?n.raw+="\n":t.push(s);continue}if(s=this.tokenizer.code(e)){e=e.substring(s.raw.length);let n=t.at(-1);"paragraph"===n?.type||"text"===n?.type?(n.raw+=(n.raw.endsWith("\n")?"":"\n")+s.raw,n.text+="\n"+s.text,this.inlineQueue.at(-1).src=n.text):t.push(s);continue}if(s=this.tokenizer.fences(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.heading(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.hr(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.blockquote(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.list(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.html(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.def(e)){e=e.substring(s.raw.length);let n=t.at(-1);"paragraph"===n?.type||"text"===n?.type?(n.raw+=(n.raw.endsWith("\n")?"":"\n")+s.raw,n.text+="\n"+s.raw,this.inlineQueue.at(-1).src=n.text):this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title},t.push(s));continue}if(s=this.tokenizer.table(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.lheading(e)){e=e.substring(s.raw.length),t.push(s);continue}let l=e;if(this.options.extensions?.startBlock){let t,n=1/0,r=e.slice(1);this.options.extensions.startBlock.forEach((e=>{t=e.call({lexer:this},r),"number"==typeof t&&t>=0&&(n=Math.min(n,t))})),n<1/0&&n>=0&&(l=e.substring(0,n+1))}if(this.state.top&&(s=this.tokenizer.paragraph(l))){let r=t.at(-1);n&&"paragraph"===r?.type?(r.raw+=(r.raw.endsWith("\n")?"":"\n")+s.raw,r.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s),n=l.length!==e.length,e=e.substring(s.raw.length)}else if(s=this.tokenizer.text(e)){e=e.substring(s.raw.length);let n=t.at(-1);"text"===n?.type?(n.raw+=(n.raw.endsWith("\n")?"":"\n")+s.raw,n.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=n.text):t.push(s)}else if(e){this.infiniteLoopError(e.charCodeAt(0));break}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){this.tokenizer.lexer=this;let n,r=e,s=null;if(this.tokens.links){let e=Object.keys(this.tokens.links);if(e.length>0)for(;null!==(s=this.tokenizer.rules.inline.reflinkSearch.exec(r));)e.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(r=r.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+r.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!==(s=this.tokenizer.rules.inline.anyPunctuation.exec(r));)r=r.slice(0,s.index)+"++"+r.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;null!==(s=this.tokenizer.rules.inline.blockSkip.exec(r));)n=s[2]?s[2].length:0,r=r.slice(0,s.index+n)+"["+"a".repeat(s[0].length-n-2)+"]"+r.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);r=this.options.hooks?.emStrongMask?.call({lexer:this},r)??r;let l=!1,i="",a=1/0;for(;e;){if(!(e.length!!(n=r.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0))))continue;if(n=this.tokenizer.escape(e)){e=e.substring(n.raw.length),t.push(n);continue}if(n=this.tokenizer.tag(e)){e=e.substring(n.raw.length),t.push(n);continue}if(n=this.tokenizer.link(e)){e=e.substring(n.raw.length),t.push(n);continue}if(n=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(n.raw.length);let r=t.at(-1);"text"===n.type&&"text"===r?.type?(r.raw+=n.raw,r.text+=n.text):t.push(n);continue}if(n=this.tokenizer.emStrong(e,r,i)){e=e.substring(n.raw.length),t.push(n);continue}if(n=this.tokenizer.codespan(e)){e=e.substring(n.raw.length),t.push(n);continue}if(n=this.tokenizer.br(e)){e=e.substring(n.raw.length),t.push(n);continue}if(n=this.tokenizer.del(e,r,i)){e=e.substring(n.raw.length),t.push(n);continue}if(n=this.tokenizer.autolink(e)){e=e.substring(n.raw.length),t.push(n);continue}if(!this.state.inLink&&(n=this.tokenizer.url(e))){e=e.substring(n.raw.length),t.push(n);continue}let s=e;if(this.options.extensions?.startInline){let t,n=1/0,r=e.slice(1);this.options.extensions.startInline.forEach((e=>{t=e.call({lexer:this},r),"number"==typeof t&&t>=0&&(n=Math.min(n,t))})),n<1/0&&n>=0&&(s=e.substring(0,n+1))}if(n=this.tokenizer.inlineText(s)){e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(i=n.raw.slice(-1)),l=!0;let r=t.at(-1);"text"===r?.type?(r.raw+=n.raw,r.text+=n.text):t.push(n)}else if(e){this.infiniteLoopError(e.charCodeAt(0));break}}return t}infiniteLoopError(e){let t="Infinite loop on byte: "+e;if(!this.options.silent)throw new Error(t);console.error(t)}},ge=class{options;parser;constructor(e){this.options=e||t}space(e){return""}code({text:e,lang:t,escaped:n}){let r=(t||"").match(a.notSpaceStart)?.[0],s=e.replace(a.endingNewline,"")+"\n";return r?'
'+(n?s:se(s,!0))+"
\n":"
"+(n?s:se(s,!0))+"
\n"}blockquote({tokens:e}){return`
\n${this.parser.parse(e)}
\n`}html({text:e}){return e}def(e){return""}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)}\n`}hr(e){return"
\n"}list(e){let t=e.ordered,n=e.start,r="";for(let t=0;t\n"+r+"\n"}listitem(e){return`
  • ${this.parser.parse(e.tokens)}
  • \n`}checkbox({checked:e}){return" '}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    \n`}table(e){let t="",n="";for(let t=0;t${r}`),"\n\n"+t+"\n"+r+"
    \n"}tablerow({text:e}){return`\n${e}\n`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`\n`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${se(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){let r=this.parser.parseInline(n),s=le(e);if(null===s)return r;let l='
    ",l}image({href:e,title:t,text:n,tokens:r}){r&&(n=this.parser.parseInline(r,this.parser.textRenderer));let s=le(e);if(null===s)return se(n);let l=`${se(n)}{let s=e[r].flat(1/0);n=n.concat(this.walkTokens(s,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{let n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){let n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let r=e.renderer.apply(this,t);return!1===r&&(r=n.apply(this,t)),r}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");let n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){let t=this.defaults.renderer||new ge(this.defaults);for(let n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if(["options","parser"].includes(n))continue;let r=n,s=e.renderer[r],l=t[r];t[r]=(...e)=>{let n=s.apply(t,e);return!1===n&&(n=l.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){let t=this.defaults.tokenizer||new pe(this.defaults);for(let n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;let r=n,s=e.tokenizer[r],l=t[r];t[r]=(...e)=>{let n=s.apply(t,e);return!1===n&&(n=l.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){let t=this.defaults.hooks||new fe;for(let n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if(["options","block"].includes(n))continue;let r=n,s=e.hooks[r],l=t[r];fe.passThroughHooks.has(n)?t[r]=e=>{if(this.defaults.async&&fe.passThroughHooksRespectAsync.has(n))return(async()=>{let n=await s.call(t,e);return l.call(t,n)})();let r=s.call(t,e);return l.call(t,r)}:t[r]=(...e)=>{if(this.defaults.async)return(async()=>{let n=await s.apply(t,e);return!1===n&&(n=await l.apply(t,e)),n})();let n=s.apply(t,e);return!1===n&&(n=l.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){let t=this.defaults.walkTokens,r=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(r.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ue.lex(e,t??this.defaults)}parser(e,t){return de.parse(e,t??this.defaults)}parseMarkdown(e){return(t,n)=>{let r={...n},s={...this.defaults,...r},l=this.onError(!!s.silent,!!s.async);if(!0===this.defaults.async&&!1===r.async)return l(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof t>"u"||null===t)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof t)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));if(s.hooks&&(s.hooks.options=s,s.hooks.block=e),s.async)return(async()=>{let n=s.hooks?await s.hooks.preprocess(t):t,r=await(s.hooks?await s.hooks.provideLexer(e):e?ue.lex:ue.lexInline)(n,s),l=s.hooks?await s.hooks.processAllTokens(r):r;s.walkTokens&&await Promise.all(this.walkTokens(l,s.walkTokens));let i=await(s.hooks?await s.hooks.provideParser(e):e?de.parse:de.parseInline)(l,s);return s.hooks?await s.hooks.postprocess(i):i})().catch(l);try{s.hooks&&(t=s.hooks.preprocess(t));let n=(s.hooks?s.hooks.provideLexer(e):e?ue.lex:ue.lexInline)(t,s);s.hooks&&(n=s.hooks.processAllTokens(n)),s.walkTokens&&this.walkTokens(n,s.walkTokens);let r=(s.hooks?s.hooks.provideParser(e):e?de.parse:de.parseInline)(n,s);return s.hooks&&(r=s.hooks.postprocess(r)),r}catch(e){return l(e)}}}onError(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){let e="

    An error occurred:

    "+se(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}},be=new xe;function we(e,t){return be.parse(e,t)}we.options=we.setOptions=function(e){return be.setOptions(e),we.defaults=be.defaults,n(we.defaults),we},we.getDefaults=e,we.defaults=t,we.use=function(...e){return be.use(...e),we.defaults=be.defaults,n(we.defaults),we},we.walkTokens=function(e,t){return be.walkTokens(e,t)},we.parseInline=be.parseInline,we.Parser=de,we.parser=de.parse,we.Renderer=ge,we.TextRenderer=ke,we.Lexer=ue,we.lexer=ue.lex,we.Tokenizer=pe,we.Hooks=fe,we.parse=we;var me=we.options,ye=we.setOptions,$e=we.use,Re=we.walkTokens,Se=we.parseInline,Te=we,ze=de.parse,Ae=ue.lex;export{fe as Hooks,ue as Lexer,xe as Marked,de as Parser,ge as Renderer,ke as TextRenderer,pe as Tokenizer,t as defaults,e as getDefaults,Ae as lexer,we as marked,me as options,Te as parse,Se as parseInline,ze as parser,ye as setOptions,$e as use,Re as walkTokens};export default null; +//# sourceMappingURL=/sm/a0cc6b6fa5b76cfa534daebefa13d489b40cc184ee95e33f897de01e362c09e8.map + diff --git a/src/js/plugins/pluginService.js b/src/js/plugins/pluginService.js index d2066beb..b083148e 100644 --- a/src/js/plugins/pluginService.js +++ b/src/js/plugins/pluginService.js @@ -407,6 +407,14 @@ export class PluginService extends ReactiveStore { .catch(() => null); } + async getLiveManifest(pluginId, repo) { + return this.sourceProvider.getLiveManifest(pluginId, repo); + } + + async getReadme(pluginId, repo) { + return this.sourceProvider.getReadme(pluginId, repo); + } + async _reconcileCache(installed) { const urlLists = await Promise.all( installed.map((entry) => diff --git a/src/js/plugins/sourceProvider.js b/src/js/plugins/sourceProvider.js index 676e19c2..47c3e735 100644 --- a/src/js/plugins/sourceProvider.js +++ b/src/js/plugins/sourceProvider.js @@ -106,6 +106,24 @@ export class SourceProvider { } } + async getReadme(pluginId, repo) { + if (pluginId.endsWith("__LOCAL")) { + const response = await fetch(`/plugins-local/${pluginId}/README.md`); + if (response.status === 404) return null; + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return await response.text(); + } + if (!repo) { + throw new Error("Repo is required"); + } + // Fetch from main branch so we show the latest README + const url = remoteAssetUrl({ repo, file: "README.md" }); + const response = await fetch(url, { cache: "no-store" }); + if (response.status === 404) return null; + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return await response.text(); + } + // URLs that should be retained in the cache // Local plugins have no cached URLs async getCacheUrls(pluginId, version, repo) { diff --git a/src/js/views/settings/communityPluginListing.view.js b/src/js/views/settings/communityPluginListing.view.js new file mode 100644 index 00000000..02a19707 --- /dev/null +++ b/src/js/views/settings/communityPluginListing.view.js @@ -0,0 +1,269 @@ +import { View } from "/js/views/view.js"; +import { html, render } from "/js/lib/lit-html.js"; +import { pageEffect } from "/js/router.js"; +import { headerTemplate } from "/js/templates/header.template.js"; +import { mainLayoutTemplate } from "/js/templates/mainLayout.template.js"; +import { auth } from "/js/auth.js"; +import { showToast } from "/js/toasts.js"; +import { confirm } from "/js/modals.js"; +import { Signal } from "/js/signals.js"; +import { PermissionsDeclinedError } from "/js/plugins/pluginService.js"; +import "/js/components/rendered-markdown.js"; + +class SettingsCommunityPluginListingView extends View { + async render({ + root, + params, + context: { + dataLayer, + notificationService, + chatNotificationService, + postComposerService, + pluginService, + }, + }) { + await auth.requireAuth(); + + const { pluginId } = params; + const isLocal = pluginId.endsWith("__LOCAL"); + + const $loadError = new Signal.State(null); + const $version = new Signal.State(null); + const $readme = new Signal.State(null); + const $loading = new Signal.State(true); + const $pendingAction = new Signal.State(null); // ("install" | "uninstall") + + const $listing = new Signal.Computed(() => { + const listings = pluginService.$registryListings.get(); + if (!listings) return null; + return listings.find((listing) => listing.id === pluginId) ?? null; + }); + + async function loadListings() { + $loadError.set(null); + try { + await pluginService.loadRegistryListings(); + } catch (error) { + console.error(error); + $loadError.set(error.message ?? String(error)); + } + } + + async function loadDetails() { + const listing = $listing.get(); + if (!listing) { + $loading.set(false); + return; + } + const { id, repo } = listing; + await Promise.all([ + pluginService + .getLiveManifest(id, repo) + .then((manifest) => $version.set(manifest?.version ?? null)) + .catch((error) => + console.error("Failed to load plugin manifest", error), + ), + pluginService + .getReadme(id, repo) + .then((readme) => $readme.set(readme)) + .catch((error) => + console.error("Failed to load plugin README", error), + ), + ]); + $loading.set(false); + } + + async function toggleInstall(listing) { + const wasInstalled = listing.installed; + if (wasInstalled) { + const confirmed = await confirm( + `"${listing.name}" will be disabled and uninstalled.`, + { + title: "Uninstall plugin?", + confirmButtonStyle: "danger", + confirmButtonText: "Uninstall", + }, + ); + if (!confirmed) return; + } + $pendingAction.set(wasInstalled ? "uninstall" : "install"); + try { + if (wasInstalled) { + await pluginService.uninstallPlugin(listing.id); + } else { + await pluginService.installPlugin(listing.id); + } + showToast( + wasInstalled + ? `Uninstalled ${listing.name}` + : `Installed ${listing.name}`, + { style: wasInstalled ? "default" : "success" }, + ); + } catch (error) { + if (error instanceof PermissionsDeclinedError) { + // User declined the permission prompt; nothing to report. + } else { + console.error(error); + showToast( + wasInstalled + ? `Failed to uninstall ${listing.name}` + : `Failed to install ${listing.name}`, + { style: "error" }, + ); + } + } + $pendingAction.set(null); + } + + pageEffect(root, () => { + const currentUser = dataLayer.derived.$currentUser.get(); + const numNotifications = + notificationService?.$numNotifications.get() ?? null; + const numChatNotifications = + chatNotificationService?.$numNotifications.get() ?? null; + const listing = $listing.get(); + const loadError = $loadError.get(); + const version = $version.get(); + const readme = $readme.get(); + const loading = $loading.get(); + const pendingAction = $pendingAction.get(); + const installButtonClass = listing?.installed + ? "plugin-install-button rounded-button" + : "plugin-install-button rounded-button rounded-button-primary"; + render( + html`
    `, + root, + ); + }); + + root.addEventListener("page-enter", async () => { + dataLayer.declarative.ensureCurrentUser(); + await loadListings(); + await loadDetails(); + }); + + root.addEventListener("page-restore", () => { + // window.scrollTo(0, 0); + }); + } +} + +export default new SettingsCommunityPluginListingView(); diff --git a/src/js/views/settings/communityPlugins.view.js b/src/js/views/settings/communityPlugins.view.js index 59c9b050..f3b16268 100644 --- a/src/js/views/settings/communityPlugins.view.js +++ b/src/js/views/settings/communityPlugins.view.js @@ -4,10 +4,7 @@ import { pageEffect } from "/js/router.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { mainLayoutTemplate } from "/js/templates/mainLayout.template.js"; import { auth } from "/js/auth.js"; -import { showToast } from "/js/toasts.js"; -import { confirm } from "/js/modals.js"; -import { Signal, SignalSet } from "/js/signals.js"; -import { PermissionsDeclinedError } from "/js/plugins/pluginService.js"; +import { Signal } from "/js/signals.js"; class SettingsCommunityPluginsView extends View { async render({ @@ -23,7 +20,6 @@ class SettingsCommunityPluginsView extends View { await auth.requireAuth(); const $error = new Signal.State(null); - const $pendingIds = new SignalSet(); async function loadListings() { $error.set(null); @@ -35,48 +31,6 @@ class SettingsCommunityPluginsView extends View { } } - async function toggleInstall(listing) { - const wasInstalled = listing.installed; - if (wasInstalled) { - const confirmed = await confirm( - `"${listing.name}" will be disabled and uninstalled.`, - { - title: "Uninstall plugin?", - confirmButtonStyle: "danger", - confirmButtonText: "Uninstall", - }, - ); - if (!confirmed) return; - } - $pendingIds.add(listing.id); - try { - if (wasInstalled) { - await pluginService.uninstallPlugin(listing.id); - } else { - await pluginService.installPlugin(listing.id); - } - showToast( - wasInstalled - ? `Uninstalled ${listing.name}` - : `Installed ${listing.name}`, - { style: wasInstalled ? "default" : "success" }, - ); - } catch (e) { - if (e instanceof PermissionsDeclinedError) { - // User declined the permission prompt; nothing to report. - } else { - console.error(e); - showToast( - wasInstalled - ? `Failed to uninstall ${listing.name}` - : `Failed to install ${listing.name}`, - { style: "error" }, - ); - } - } - $pendingIds.delete(listing.id); - } - pageEffect(root, () => { const error = $error.get(); const currentUser = dataLayer.derived.$currentUser.get(); @@ -127,51 +81,40 @@ class SettingsCommunityPluginsView extends View { ` : html`
      ${listings.map((listing) => { - const pending = $pendingIds.has(listing.id); - const buttonClass = listing.installed - ? "plugin-install-button rounded-button" - : "plugin-install-button rounded-button rounded-button-primary"; return html`
    • -
    • `; })} diff --git a/tests/e2e/mockServer.js b/tests/e2e/mockServer.js index b3779c58..25bfb965 100644 --- a/tests/e2e/mockServer.js +++ b/tests/e2e/mockServer.js @@ -55,6 +55,9 @@ export class MockServer { this.localPluginSource = null; this.registryEntries = []; this.liveManifest = null; + // README markdown served for plugin repos; set to null to simulate a + // plugin with no README (404). + this.pluginReadme = "# Remote Themes\n\nA test readme for the plugin."; } addAuthorFeedPosts(did, filter, posts) { @@ -266,6 +269,24 @@ export class MockServer { }); }, ); + // Plugin README, served from the repo's main branch (remote) or the + // local plugin directory. + const fulfillReadme = (route) => { + if (this.pluginReadme === null) { + route.fulfill({ status: 404, body: "Not Found" }); + return; + } + route.fulfill({ + status: 200, + contentType: "text/markdown", + body: this.pluginReadme, + }); + }; + await page.route( + "**/raw.githubusercontent.com/*/*/main/README.md", + fulfillReadme, + ); + await page.route("**/plugins-local/*/README.md", fulfillReadme); await page.route("**/.well-known/atproto-did*", (route) => route.fulfill({ status: 404, body: "Not Found" }), diff --git a/tests/e2e/specs/flows/installRemotePlugin.test.js b/tests/e2e/specs/flows/installRemotePlugin.test.js index 95510c13..507d367d 100644 --- a/tests/e2e/specs/flows/installRemotePlugin.test.js +++ b/tests/e2e/specs/flows/installRemotePlugin.test.js @@ -20,17 +20,24 @@ test.describe("Remote plugin install flow", () => { await mockServer.setup(page); await login(page); - // Install from community view + // Open the plugin's detail page from the community view, then install. await page.goto("/settings/plugins/community"); const community = page.locator("#settings-community-plugins-view"); const installItem = community.locator(".plugin-list-item", { hasText: "Remote Themes", }); await expect(installItem).toBeVisible({ timeout: 10000 }); + await installItem.locator(".plugin-list-item-link").click(); + + const listing = page.locator("#settings-community-plugin-listing-view"); + const installButton = listing.locator( + '[data-testid="plugin-listing-install-button"]', + ); + await expect(installButton).toHaveText("Install", { timeout: 10000 }); const putPrefs = page.waitForResponse((res) => res.url().includes("app.bsky.actor.putPreferences"), ); - await installItem.locator(".plugin-install-button").click(); + await installButton.click(); await putPrefs; // Navigate back to the plugins list; the installed plugin should appear. diff --git a/tests/e2e/specs/views/settings/communityPluginListing.view.test.js b/tests/e2e/specs/views/settings/communityPluginListing.view.test.js new file mode 100644 index 00000000..0c7f6310 --- /dev/null +++ b/tests/e2e/specs/views/settings/communityPluginListing.view.test.js @@ -0,0 +1,167 @@ +import { test, expect } from "../../../base.js"; +import { login } from "../../../helpers.js"; +import { MockServer } from "../../../mockServer.js"; + +const REMOTE_ID = "remote-themes"; +const REGISTRY_ENTRY = { + id: REMOTE_ID, + name: "Remote Themes", + author: "alice", + repo: "alice/remote-themes", + description: "Adds extra themes", +}; + +function detailUrl(id = REMOTE_ID) { + return `/settings/plugins/community/${id}`; +} + +test.describe("Settings community plugin listing view", () => { + test("shows the plugin's header info and renders its README", async ({ + page, + }) => { + const mockServer = new MockServer(); + mockServer.registryEntries = [REGISTRY_ENTRY]; + mockServer.pluginReadme = "# Overview\n\nDetailed readme body text."; + await mockServer.setup(page); + await login(page); + + await page.goto(detailUrl()); + const view = page.locator("#settings-community-plugin-listing-view"); + await expect( + view.locator('[data-testid="plugin-listing-name"]'), + ).toHaveText("Remote Themes", { timeout: 10000 }); + + const header = view.locator('[data-testid="plugin-listing-header"]'); + await expect(header).toContainText("Version: 1.0.0"); + await expect(header).toContainText("By alice"); + await expect(header).toContainText("Adds extra themes"); + await expect(header.locator(".plugin-listing-repo a")).toHaveAttribute( + "href", + "https://github.com/alice/remote-themes", + ); + + // README markdown is rendered into the component. + const readme = view.locator('[data-testid="plugin-listing-readme"]'); + await expect(readme.locator("h1")).toHaveText("Overview", { + timeout: 10000, + }); + await expect(readme).toContainText("Detailed readme body text."); + }); + + test("omits the README section when the plugin has none", async ({ + page, + }) => { + const mockServer = new MockServer(); + mockServer.registryEntries = [REGISTRY_ENTRY]; + mockServer.pluginReadme = null; + await mockServer.setup(page); + await login(page); + + await page.goto(detailUrl()); + const view = page.locator("#settings-community-plugin-listing-view"); + await expect( + view.locator('[data-testid="plugin-listing-header"]'), + ).toBeVisible({ timeout: 10000 }); + + await expect(view.locator(".plugin-listing-readme")).toHaveCount(0); + await expect( + view.locator('[data-testid="plugin-listing-readme"]'), + ).toHaveCount(0); + }); + + test("installing flips the button to Uninstall", async ({ page }) => { + const mockServer = new MockServer(); + mockServer.registryEntries = [REGISTRY_ENTRY]; + await mockServer.setup(page); + await login(page); + + await page.goto(detailUrl()); + const view = page.locator("#settings-community-plugin-listing-view"); + const button = view.locator( + '[data-testid="plugin-listing-install-button"]', + ); + await expect(button).toHaveText("Install", { timeout: 10000 }); + + const putPrefs = page.waitForResponse((res) => + res.url().includes("app.bsky.actor.putPreferences"), + ); + await button.click(); + await putPrefs; + + await expect(button).toHaveText("Uninstall", { timeout: 10000 }); + await expect( + mockServer.installedPlugins.map((plugin) => plugin.id), + ).toEqual([REMOTE_ID]); + }); + + test("uninstalling flips the button back to Install", async ({ page }) => { + const mockServer = new MockServer(); + mockServer.registryEntries = [REGISTRY_ENTRY]; + mockServer.installedPlugins = [ + { id: REMOTE_ID, version: "1.0.0", enabled: true }, + ]; + await mockServer.setup(page); + await login(page); + + await page.goto(detailUrl()); + const view = page.locator("#settings-community-plugin-listing-view"); + const button = view.locator( + '[data-testid="plugin-listing-install-button"]', + ); + await expect(button).toHaveText("Uninstall", { timeout: 10000 }); + + await button.click(); + const confirmButton = page.locator("button.confirm-button"); + await expect(confirmButton).toBeVisible({ timeout: 5000 }); + const putPrefs = page.waitForResponse((res) => + res.url().includes("app.bsky.actor.putPreferences"), + ); + await confirmButton.click(); + await putPrefs; + + await expect(button).toHaveText("Install", { timeout: 10000 }); + await expect(mockServer.installedPlugins).toEqual([]); + }); + + test("cancelling the uninstall confirm leaves the plugin installed", async ({ + page, + }) => { + const mockServer = new MockServer(); + mockServer.registryEntries = [REGISTRY_ENTRY]; + mockServer.installedPlugins = [ + { id: REMOTE_ID, version: "1.0.0", enabled: true }, + ]; + await mockServer.setup(page); + await login(page); + + await page.goto(detailUrl()); + const view = page.locator("#settings-community-plugin-listing-view"); + const button = view.locator( + '[data-testid="plugin-listing-install-button"]', + ); + await expect(button).toHaveText("Uninstall", { timeout: 10000 }); + + await button.click(); + const cancelButton = page.locator("button.cancel-button"); + await expect(cancelButton).toBeVisible({ timeout: 5000 }); + await cancelButton.click(); + + await expect(button).toHaveText("Uninstall"); + await expect( + mockServer.installedPlugins.map((plugin) => plugin.id), + ).toEqual([REMOTE_ID]); + }); + + test("shows a not-found message for an unknown plugin", async ({ page }) => { + const mockServer = new MockServer(); + mockServer.registryEntries = [REGISTRY_ENTRY]; + await mockServer.setup(page); + await login(page); + + await page.goto(detailUrl("does-not-exist")); + const view = page.locator("#settings-community-plugin-listing-view"); + await expect( + view.locator('[data-testid="plugin-listing-not-found"]'), + ).toBeVisible({ timeout: 10000 }); + }); +}); diff --git a/tests/e2e/specs/views/settings/communityPlugins.view.test.js b/tests/e2e/specs/views/settings/communityPlugins.view.test.js index 0dcbfedb..cdb475f6 100644 --- a/tests/e2e/specs/views/settings/communityPlugins.view.test.js +++ b/tests/e2e/specs/views/settings/communityPlugins.view.test.js @@ -33,40 +33,12 @@ test.describe("Settings community plugins view", () => { await expect( view.locator(".plugin-list-item", { hasText: "Test Plugin" }), ).toBeVisible(); - }); - - test("installing a plugin flips the button to Uninstall", async ({ - page, - }) => { - const mockServer = new MockServer(); - mockServer.registryEntries = [REGISTRY_ENTRY]; - await mockServer.setup(page); - await login(page); - - await page.goto("/settings/plugins/community"); - const view = page.locator("#settings-community-plugins-view"); - const item = view.locator(".plugin-list-item", { - hasText: "Remote Themes", - }); - await expect(item).toBeVisible({ timeout: 10000 }); - await expect(item.locator(".plugin-install-button")).toHaveText("Install"); - - const putPrefs = page.waitForResponse((res) => - res.url().includes("app.bsky.actor.putPreferences"), - ); - await item.locator(".plugin-install-button").click(); - await putPrefs; - await expect(item.locator(".plugin-install-button")).toHaveText( - "Uninstall", - { timeout: 10000 }, - ); - await expect(mockServer.installedPlugins.map((p) => p.id)).toEqual([ - REMOTE_ID, - ]); + // Install/uninstall interactions now live on the detail page, not here. + await expect(view.locator(".plugin-install-button")).toHaveCount(0); }); - test("uninstalling flips the button back to Install", async ({ page }) => { + test("shows an installed badge for installed plugins", async ({ page }) => { const mockServer = new MockServer(); mockServer.registryEntries = [REGISTRY_ENTRY]; mockServer.installedPlugins = [ @@ -77,37 +49,25 @@ test.describe("Settings community plugins view", () => { await page.goto("/settings/plugins/community"); const view = page.locator("#settings-community-plugins-view"); - const item = view.locator(".plugin-list-item", { + const installedItem = view.locator(".plugin-list-item", { hasText: "Remote Themes", }); - await expect(item.locator(".plugin-install-button")).toHaveText( - "Uninstall", - { timeout: 10000 }, - ); - - await item.locator(".plugin-install-button").click(); - const confirmButton = page.locator("button.confirm-button"); - await expect(confirmButton).toBeVisible({ timeout: 5000 }); - const putPrefs = page.waitForResponse((res) => - res.url().includes("app.bsky.actor.putPreferences"), - ); - await confirmButton.click(); - await putPrefs; + await expect( + installedItem.locator('[data-testid="plugin-installed-badge"]'), + ).toBeVisible({ timeout: 10000 }); - await expect(item.locator(".plugin-install-button")).toHaveText("Install", { - timeout: 10000, + // The (not installed) local test plugin should have no badge. + const otherItem = view.locator(".plugin-list-item", { + hasText: "Test Plugin", }); - await expect(mockServer.installedPlugins).toEqual([]); + await expect( + otherItem.locator('[data-testid="plugin-installed-badge"]'), + ).toHaveCount(0); }); - test("cancelling the uninstall confirm leaves the plugin installed", async ({ - page, - }) => { + test("clicking a plugin opens its detail page", async ({ page }) => { const mockServer = new MockServer(); mockServer.registryEntries = [REGISTRY_ENTRY]; - mockServer.installedPlugins = [ - { id: REMOTE_ID, version: "1.0.0", enabled: true }, - ]; await mockServer.setup(page); await login(page); @@ -116,21 +76,16 @@ test.describe("Settings community plugins view", () => { const item = view.locator(".plugin-list-item", { hasText: "Remote Themes", }); - await expect(item.locator(".plugin-install-button")).toHaveText( - "Uninstall", - { timeout: 10000 }, - ); + await expect(item).toBeVisible({ timeout: 10000 }); - await item.locator(".plugin-install-button").click(); - const cancelButton = page.locator("button.cancel-button"); - await expect(cancelButton).toBeVisible({ timeout: 5000 }); - await cancelButton.click(); + await item.locator(".plugin-list-item-link").click(); - await expect(item.locator(".plugin-install-button")).toHaveText( - "Uninstall", + await expect(page).toHaveURL( + new RegExp(`/settings/plugins/community/${REMOTE_ID}$`), ); - await expect(mockServer.installedPlugins.map((p) => p.id)).toEqual([ - REMOTE_ID, - ]); + const detail = page.locator("#settings-community-plugin-listing-view"); + await expect( + detail.locator('[data-testid="plugin-listing-name"]'), + ).toHaveText("Remote Themes", { timeout: 10000 }); }); });