!i.includes(e))).join(" ").length/n.join(" ").length:0},_checkByline:function(e,t){if(this._articleByline)return!1;if(void 0!==e.getAttribute)var i=e.getAttribute("rel"),n=e.getAttribute("itemprop");return!(!("author"===i||n&&-1!==n.indexOf("author")||this.REGEXPS.byline.test(t))||!this._isValidByline(e.textContent)||(this._articleByline=e.textContent.trim(),0))},_getNodeAncestors:function(e,t){t=t||0;for(var i=0,n=[];e.parentNode&&(n.push(e.parentNode),!t||++i!==t);)e=e.parentNode;return n},_grabArticle:function(e){this.log("**** grabArticle ****");var t=this._doc,i=null!==e;if(!(e=e||this._doc.body))return this.log("No body found in document. Abort."),null;for(var n=e.innerHTML;;){this.log("Starting grabArticle loop");var r=this._flagIsActive(this.FLAG_STRIP_UNLIKELYS),a=[],o=this._doc.documentElement;let X=!0;for(;o;){"HTML"===o.tagName&&(this._articleLang=o.getAttribute("lang"));var s=o.className+" "+o.id;if(this._isProbablyVisible(o))if("true"!=o.getAttribute("aria-modal")||"dialog"!=o.getAttribute("role"))if(this._checkByline(o,s))o=this._removeAndGetNext(o);else if(X&&this._headerDuplicatesTitle(o))this.log("Removing header: ",o.textContent.trim(),this._articleTitle.trim()),X=!1,o=this._removeAndGetNext(o);else{if(r){if(this.REGEXPS.unlikelyCandidates.test(s)&&!this.REGEXPS.okMaybeItsACandidate.test(s)&&!this._hasAncestorTag(o,"table")&&!this._hasAncestorTag(o,"code")&&"BODY"!==o.tagName&&"A"!==o.tagName){this.log("Removing unlikely candidate - "+s),o=this._removeAndGetNext(o);continue}if(this.UNLIKELY_ROLES.includes(o.getAttribute("role"))){this.log("Removing content with role "+o.getAttribute("role")+" - "+s),o=this._removeAndGetNext(o);continue}}if("DIV"!==o.tagName&&"SECTION"!==o.tagName&&"HEADER"!==o.tagName&&"H1"!==o.tagName&&"H2"!==o.tagName&&"H3"!==o.tagName&&"H4"!==o.tagName&&"H5"!==o.tagName&&"H6"!==o.tagName||!this._isElementWithoutContent(o)){if(-1!==this.DEFAULT_TAGS_TO_SCORE.indexOf(o.tagName)&&a.push(o),"DIV"===o.tagName){for(var l=null,c=o.firstChild;c;){var h=c.nextSibling;if(this._isPhrasingContent(c))null!==l?l.appendChild(c):this._isWhitespace(c)||(l=t.createElement("p"),o.replaceChild(l,c),l.appendChild(c));else if(null!==l){for(;l.lastChild&&this._isWhitespace(l.lastChild);)l.removeChild(l.lastChild);l=null}c=h}if(this._hasSingleTagInsideElement(o,"P")&&this._getLinkDensity(o)<.25){var d=o.children[0];o.parentNode.replaceChild(d,o),o=d,a.push(o)}else this._hasChildBlockElement(o)||(o=this._setNodeTag(o,"P"),a.push(o))}o=this._getNextNode(o)}else o=this._removeAndGetNext(o)}else o=this._removeAndGetNext(o);else this.log("Removing hidden node - "+s),o=this._removeAndGetNext(o)}var u=[];this._forEachNode(a,(function(e){if(e.parentNode&&void 0!==e.parentNode.tagName){var t=this._getInnerText(e);if(!(t.length<25)){var i=this._getNodeAncestors(e,5);if(0!==i.length){var n=0;n+=1,n+=t.split(this.REGEXPS.commas).length,n+=Math.min(Math.floor(t.length/100),3),this._forEachNode(i,(function(e,t){if(e.tagName&&e.parentNode&&void 0!==e.parentNode.tagName){if(void 0===e.readability&&(this._initializeNode(e),u.push(e)),0===t)var i=1;else i=1===t?2:3*t;e.readability.contentScore+=n/i}}))}}}}));for(var m=[],g=0,p=u.length;gE.readability.contentScore){m.splice(N,0,f),m.length>this._nbTopCandidates&&m.pop();break}}}var T,b=m[0]||null,y=!1;if(null===b||"BODY"===b.tagName){for(b=t.createElement("DIV"),y=!0;e.firstChild;)this.log("Moving child out:",e.firstChild),b.appendChild(e.firstChild);e.appendChild(b),this._initializeNode(b)}else if(b){for(var A=[],v=1;v=.75&&A.push(this._getNodeAncestors(m[v]));if(A.length>=3)for(T=b.parentNode;"BODY"!==T.tagName;){for(var S=0,C=0;C=3){b=T;break}T=T.parentNode}b.readability||this._initializeNode(b),T=b.parentNode;for(var L=b.readability.contentScore,x=L/3;"BODY"!==T.tagName;)if(T.readability){var R=T.readability.contentScore;if(RL){b=T;break}L=T.readability.contentScore,T=T.parentNode}else T=T.parentNode;for(T=b.parentNode;"BODY"!=T.tagName&&1==T.children.length;)T=(b=T).parentNode;b.readability||this._initializeNode(b)}var D=t.createElement("DIV");i&&(D.id="readability-content");for(var I=Math.max(10,.2*b.readability.contentScore),O=(T=b.parentNode).children,w=0,M=O.length;w=I)k=!0;else if("P"===P.nodeName){var B=this._getLinkDensity(P),H=this._getInnerText(P),G=H.length;(G>80&&B<.25||G<80&&G>0&&0===B&&-1!==H.search(/\.( |$)/))&&(k=!0)}}k&&(this.log("Appending node:",P),-1===this.ALTER_TO_DIV_EXCEPTIONS.indexOf(P.nodeName)&&(this.log("Altering sibling:",P,"to div."),P=this._setNodeTag(P,"DIV")),D.appendChild(P),O=T.children,w-=1,M-=1)}if(this._debug&&this.log("Article content pre-prep: "+D.innerHTML),this._prepArticle(D),this._debug&&this.log("Article content post-prep: "+D.innerHTML),y)b.id="readability-page-1",b.className="page";else{var F=t.createElement("DIV");for(F.id="readability-page-1",F.className="page";D.firstChild;)F.appendChild(D.firstChild);D.appendChild(F)}this._debug&&this.log("Article content after paging: "+D.innerHTML);var z=!0,W=this._getInnerText(D,!0).length;if(W0&&e.length<100},_unescapeHtmlEntities:function(e){if(!e)return e;var t=this.HTML_ESCAPE_MAP;return e.replace(/&(quot|amp|apos|lt|gt);/g,(function(e,i){return t[i]})).replace(/(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi,(function(e,t,i){var n=parseInt(t||i,t?16:10);return String.fromCharCode(n)}))},_getJSONLD:function(e){var t,i=this._getAllNodesWithTag(e,["script"]);return this._forEachNode(i,(function(e){if(!t&&"application/ld+json"===e.getAttribute("type"))try{var i=e.textContent.replace(/^\s*\s*$/g,""),n=JSON.parse(i);if(!n["@context"]||!n["@context"].match(/^https?\:\/\/schema\.org$/))return;if(!n["@type"]&&Array.isArray(n["@graph"])&&(n=n["@graph"].find((function(e){return(e["@type"]||"").match(this.REGEXPS.jsonLdArticleTypes)}))),!n||!n["@type"]||!n["@type"].match(this.REGEXPS.jsonLdArticleTypes))return;if(t={},"string"==typeof n.name&&"string"==typeof n.headline&&n.name!==n.headline){var r=this._getArticleTitle(),a=this._textSimilarity(n.name,r)>.75,o=this._textSimilarity(n.headline,r)>.75;t.title=o&&!a?n.headline:n.name}else"string"==typeof n.name?t.title=n.name.trim():"string"==typeof n.headline&&(t.title=n.headline.trim());return n.author&&("string"==typeof n.author.name?t.byline=n.author.name.trim():Array.isArray(n.author)&&n.author[0]&&"string"==typeof n.author[0].name&&(t.byline=n.author.filter((function(e){return e&&"string"==typeof e.name})).map((function(e){return e.name.trim()})).join(", "))),"string"==typeof n.description&&(t.excerpt=n.description.trim()),n.publisher&&"string"==typeof n.publisher.name&&(t.siteName=n.publisher.name.trim()),void("string"==typeof n.datePublished&&(t.datePublished=n.datePublished.trim()))}catch(e){this.log(e.message)}})),t||{}},_getArticleMetadata:function(e){var t={},i={},n=this._doc.getElementsByTagName("meta"),r=/\s*(article|dc|dcterm|og|twitter)\s*:\s*(author|creator|description|published_time|title|site_name)\s*/gi,a=/^\s*(?:(dc|dcterm|og|twitter|weibo:(article|webpage))\s*[\.:]\s*)?(author|creator|description|title|site_name)\s*$/i;return this._forEachNode(n,(function(e){var t=e.getAttribute("name"),n=e.getAttribute("property"),o=e.getAttribute("content");if(o){var s=null,l=null;n&&(s=n.match(r))&&(l=s[0].toLowerCase().replace(/\s/g,""),i[l]=o.trim()),!s&&t&&a.test(t)&&(l=t,o&&(l=l.toLowerCase().replace(/\s/g,"").replace(/\./g,":"),i[l]=o.trim()))}})),t.title=e.title||i["dc:title"]||i["dcterm:title"]||i["og:title"]||i["weibo:article:title"]||i["weibo:webpage:title"]||i.title||i["twitter:title"],t.title||(t.title=this._getArticleTitle()),t.byline=e.byline||i["dc:creator"]||i["dcterm:creator"]||i.author,t.excerpt=e.excerpt||i["dc:description"]||i["dcterm:description"]||i["og:description"]||i["weibo:article:description"]||i["weibo:webpage:description"]||i.description||i["twitter:description"],t.siteName=e.siteName||i["og:site_name"],t.publishedTime=e.datePublished||i["article:published_time"]||null,t.title=this._unescapeHtmlEntities(t.title),t.byline=this._unescapeHtmlEntities(t.byline),t.excerpt=this._unescapeHtmlEntities(t.excerpt),t.siteName=this._unescapeHtmlEntities(t.siteName),t.publishedTime=this._unescapeHtmlEntities(t.publishedTime),t},_isSingleImage:function(e){return"IMG"===e.tagName||1===e.children.length&&""===e.textContent.trim()&&this._isSingleImage(e.children[0])},_unwrapNoscriptImages:function(e){var t=Array.from(e.getElementsByTagName("img"));this._forEachNode(t,(function(e){for(var t=0;t0&&r>i)return!1;if(e.parentNode.tagName===t&&(!n||n(e.parentNode)))return!0;e=e.parentNode,r++}return!1},_getRowAndColumnCount:function(e){for(var t=0,i=0,n=e.getElementsByTagName("tr"),r=0;r0)n._readabilityDataTable=!0;else if(["col","colgroup","tfoot","thead","th"].some((function(e){return!!n.getElementsByTagName(e)[0]})))this.log("Data table because found data-y descendant"),n._readabilityDataTable=!0;else if(n.getElementsByTagName("table")[0])n._readabilityDataTable=!1;else{var a=this._getRowAndColumnCount(n);a.rows>=10||a.columns>4?n._readabilityDataTable=!0:n._readabilityDataTable=a.rows*a.columns>10}}else n._readabilityDataTable=!1;else n._readabilityDataTable=!1}},_fixLazyImages:function(e){this._forEachNode(this._getAllNodesWithTag(e,["img","picture","figure"]),(function(e){if(e.src&&this.REGEXPS.b64DataUrl.test(e.src)){if("image/svg+xml"===this.REGEXPS.b64DataUrl.exec(e.src)[1])return;for(var t=!1,i=0;in+=this._getInnerText(e,!0).length)),n/i},_cleanConditionally:function(e,t){this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)&&this._removeNodes(this._getAllNodesWithTag(e,[t]),(function(e){var i=function(e){return e._readabilityDataTable},n="ul"===t||"ol"===t;if(!n){var r=0,a=this._getAllNodesWithTag(e,["ul","ol"]);this._forEachNode(a,(e=>r+=this._getInnerText(e).length)),n=r/this._getInnerText(e).length>.9}if("table"===t&&i(e))return!1;if(this._hasAncestorTag(e,"table",-1,i))return!1;if(this._hasAncestorTag(e,"code"))return!1;var o=this._getClassWeight(e);if(this.log("Cleaning Conditionally",e),o+0<0)return!0;if(this._getCharCount(e,",")<10){for(var s=e.getElementsByTagName("p").length,l=e.getElementsByTagName("img").length,c=e.getElementsByTagName("li").length-100,h=e.getElementsByTagName("input").length,d=this._getTextDensity(e,["h1","h2","h3","h4","h5","h6"]),u=0,m=this._getAllNodesWithTag(e,["object","embed","iframe"]),g=0;g1&&s/l<.5&&!this._hasAncestorTag(e,"figure")||!n&&c>s||h>Math.floor(s/3)||!n&&d<.9&&_<25&&(0===l||l>2)&&!this._hasAncestorTag(e,"figure")||!n&&o<25&&f>.2||o>=25&&f>.5||1===u&&_<75||u>1;if(n&&N){for(var E=0;E1)return N;if(l==e.getElementsByTagName("li").length)return!1}return N}return!1}))},_cleanMatchedNodes:function(e,t){for(var i=this._getNextNode(e,!0),n=this._getNextNode(e);n&&n!=i;)n=t.call(this,n,n.className+" "+n.id)?this._removeAndGetNext(n):this._getNextNode(n)},_cleanHeaders:function(e){let t=this._getAllNodesWithTag(e,["h1","h2"]);this._removeNodes(t,(function(e){let t=this._getClassWeight(e)<0;return t&&this.log("Removing header with low class weight:",e),t}))},_headerDuplicatesTitle:function(e){if("H1"!=e.tagName&&"H2"!=e.tagName)return!1;var t=this._getInnerText(e,!1);return this.log("Evaluating similarity of header:",t,this._articleTitle),this._textSimilarity(this._articleTitle,t)>.75},_flagIsActive:function(e){return(this._flags&e)>0},_removeFlag:function(e){this._flags=this._flags&~e},_isProbablyVisible:function(e){return(!e.style||"none"!=e.style.display)&&(!e.style||"hidden"!=e.style.visibility)&&!e.hasAttribute("hidden")&&(!e.hasAttribute("aria-hidden")||"true"!=e.getAttribute("aria-hidden")||e.className&&e.className.indexOf&&-1!==e.className.indexOf("fallback-image"))},parse:function(){if(this._maxElemsToParse>0){var e=this._doc.getElementsByTagName("*").length;if(e>this._maxElemsToParse)throw new Error("Aborting parsing document; "+e+" elements found")}this._unwrapNoscriptImages(this._doc);var t=this._disableJSONLD?{}:this._getJSONLD(this._doc);this._removeScripts(this._doc),this._prepDocument();var i=this._getArticleMetadata(t);this._articleTitle=i.title;var n=this._grabArticle();if(!n)return null;if(this.log("Grabbed: "+n.innerHTML),this._postProcessContent(n),!i.excerpt){var r=n.getElementsByTagName("p");r.length>0&&(i.excerpt=r[0].textContent.trim())}var a=n.textContent;return{title:this._articleTitle,byline:i.byline||this._articleByline,dir:this._articleDir,lang:this._articleLang,content:this._serializer(n),textContent:a,length:a.length,excerpt:i.excerpt,siteName:i.siteName||this._articleSiteName,publishedTime:i.publishedTime}}},e.exports=t},396:(e,t,i)=>{var n=i(238),r=i(804);e.exports={Readability:n,isProbablyReaderable:r}},454:e=>{"use strict";const{entries:t,setPrototypeOf:i,isFrozen:n,getPrototypeOf:r,getOwnPropertyDescriptor:a}=Object;let{freeze:o,seal:s,create:l}=Object,{apply:c,construct:h}="undefined"!=typeof Reflect&&Reflect;o||(o=function(e){return e}),s||(s=function(e){return e}),c||(c=function(e,t,i){return e.apply(t,i)}),h||(h=function(e,t){return new e(...t)});const d=v(Array.prototype.forEach),u=v(Array.prototype.pop),m=v(Array.prototype.push),g=v(String.prototype.toLowerCase),p=v(String.prototype.toString),f=v(String.prototype.match),_=v(String.prototype.replace),N=v(String.prototype.indexOf),E=v(String.prototype.trim),T=v(Object.prototype.hasOwnProperty),b=v(RegExp.prototype.test),y=(A=TypeError,function(){for(var e=arguments.length,t=new Array(e),i=0;i1?i-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:g;i&&i(e,null);let a=t.length;for(;a--;){let i=t[a];if("string"==typeof i){const e=r(i);e!==i&&(n(t)||(t[a]=e),i=e)}e[i]=!0}return e}function C(e){for(let t=0;t/gm),z=s(/\$\{[\w\W]*}/gm),W=s(/^data-[\-\w.\u00B7-\uFFFF]+$/),j=s(/^aria-[\-\w]+$/),X=s(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Y=s(/^(?:\w+script|data):/i),V=s(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),$=s(/^html$/i),q=s(/^[a-z][.\w]*(-[.\w]+)+$/i);var K=Object.freeze({__proto__:null,ARIA_ATTR:j,ATTR_WHITESPACE:V,CUSTOM_ELEMENT:q,DATA_ATTR:W,DOCTYPE_NAME:$,ERB_EXPR:F,IS_ALLOWED_URI:X,IS_SCRIPT_OR_DATA:Y,MUSTACHE_EXPR:G,TMPLIT_EXPR:z});const J=function(){return"undefined"==typeof window?null:window};var Z=function e(){let i=arguments.length>0&&void 0!==arguments[0]?arguments[0]:J();const n=t=>e(t);if(n.version="3.2.3",n.removed=[],!i||!i.document||9!==i.document.nodeType)return n.isSupported=!1,n;let{document:r}=i;const a=r,s=a.currentScript,{DocumentFragment:c,HTMLTemplateElement:h,Node:A,Element:v,NodeFilter:C,NamedNodeMap:G=i.NamedNodeMap||i.MozNamedAttrMap,HTMLFormElement:F,DOMParser:z,trustedTypes:W}=i,j=v.prototype,Y=x(j,"cloneNode"),V=x(j,"remove"),q=x(j,"nextSibling"),Z=x(j,"childNodes"),Q=x(j,"parentNode");if("function"==typeof h){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ee,te="";const{implementation:ie,createNodeIterator:ne,createDocumentFragment:re,getElementsByTagName:ae}=r,{importNode:oe}=a;let se={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};n.isSupported="function"==typeof t&&"function"==typeof Q&&ie&&void 0!==ie.createHTMLDocument;const{MUSTACHE_EXPR:le,ERB_EXPR:ce,TMPLIT_EXPR:he,DATA_ATTR:de,ARIA_ATTR:ue,IS_SCRIPT_OR_DATA:me,ATTR_WHITESPACE:ge,CUSTOM_ELEMENT:pe}=K;let{IS_ALLOWED_URI:fe}=K,_e=null;const Ne=S({},[...R,...D,...I,...w,...P]);let Ee=null;const Te=S({},[...k,...U,...B,...H]);let be=Object.seal(l(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}})),ye=null,Ae=null,ve=!0,Se=!0,Ce=!1,Le=!0,xe=!1,Re=!0,De=!1,Ie=!1,Oe=!1,we=!1,Me=!1,Pe=!1,ke=!0,Ue=!1,Be=!0,He=!1,Ge={},Fe=null;const ze=S({},["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 We=null;const je=S({},["audio","video","img","source","image","track"]);let Xe=null;const Ye=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ve="http://www.w3.org/1998/Math/MathML",$e="http://www.w3.org/2000/svg",qe="http://www.w3.org/1999/xhtml";let Ke=qe,Je=!1,Ze=null;const Qe=S({},[Ve,$e,qe],p);let et=S({},["mi","mo","mn","ms","mtext"]),tt=S({},["annotation-xml"]);const it=S({},["title","style","font","a","script"]);let nt=null;const rt=["application/xhtml+xml","text/html"];let at=null,ot=null;const st=r.createElement("form"),lt=function(e){return e instanceof RegExp||e instanceof Function},ct=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ot||ot!==e){if(e&&"object"==typeof e||(e={}),e=L(e),nt=-1===rt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,at="application/xhtml+xml"===nt?p:g,_e=T(e,"ALLOWED_TAGS")?S({},e.ALLOWED_TAGS,at):Ne,Ee=T(e,"ALLOWED_ATTR")?S({},e.ALLOWED_ATTR,at):Te,Ze=T(e,"ALLOWED_NAMESPACES")?S({},e.ALLOWED_NAMESPACES,p):Qe,Xe=T(e,"ADD_URI_SAFE_ATTR")?S(L(Ye),e.ADD_URI_SAFE_ATTR,at):Ye,We=T(e,"ADD_DATA_URI_TAGS")?S(L(je),e.ADD_DATA_URI_TAGS,at):je,Fe=T(e,"FORBID_CONTENTS")?S({},e.FORBID_CONTENTS,at):ze,ye=T(e,"FORBID_TAGS")?S({},e.FORBID_TAGS,at):{},Ae=T(e,"FORBID_ATTR")?S({},e.FORBID_ATTR,at):{},Ge=!!T(e,"USE_PROFILES")&&e.USE_PROFILES,ve=!1!==e.ALLOW_ARIA_ATTR,Se=!1!==e.ALLOW_DATA_ATTR,Ce=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Le=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,xe=e.SAFE_FOR_TEMPLATES||!1,Re=!1!==e.SAFE_FOR_XML,De=e.WHOLE_DOCUMENT||!1,we=e.RETURN_DOM||!1,Me=e.RETURN_DOM_FRAGMENT||!1,Pe=e.RETURN_TRUSTED_TYPE||!1,Oe=e.FORCE_BODY||!1,ke=!1!==e.SANITIZE_DOM,Ue=e.SANITIZE_NAMED_PROPS||!1,Be=!1!==e.KEEP_CONTENT,He=e.IN_PLACE||!1,fe=e.ALLOWED_URI_REGEXP||X,Ke=e.NAMESPACE||qe,et=e.MATHML_TEXT_INTEGRATION_POINTS||et,tt=e.HTML_INTEGRATION_POINTS||tt,be=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&<(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(be.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&<(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(be.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(be.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),xe&&(Se=!1),Me&&(we=!0),Ge&&(_e=S({},P),Ee=[],!0===Ge.html&&(S(_e,R),S(Ee,k)),!0===Ge.svg&&(S(_e,D),S(Ee,U),S(Ee,H)),!0===Ge.svgFilters&&(S(_e,I),S(Ee,U),S(Ee,H)),!0===Ge.mathMl&&(S(_e,w),S(Ee,B),S(Ee,H))),e.ADD_TAGS&&(_e===Ne&&(_e=L(_e)),S(_e,e.ADD_TAGS,at)),e.ADD_ATTR&&(Ee===Te&&(Ee=L(Ee)),S(Ee,e.ADD_ATTR,at)),e.ADD_URI_SAFE_ATTR&&S(Xe,e.ADD_URI_SAFE_ATTR,at),e.FORBID_CONTENTS&&(Fe===ze&&(Fe=L(Fe)),S(Fe,e.FORBID_CONTENTS,at)),Be&&(_e["#text"]=!0),De&&S(_e,["html","head","body"]),_e.table&&(S(_e,["tbody"]),delete ye.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw y('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw y('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ee=e.TRUSTED_TYPES_POLICY,te=ee.createHTML("")}else void 0===ee&&(ee=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let i=null;const n="data-tt-policy-suffix";t&&t.hasAttribute(n)&&(i=t.getAttribute(n));const r="dompurify"+(i?"#"+i:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(W,s)),null!==ee&&"string"==typeof te&&(te=ee.createHTML(""));o&&o(e),ot=e}},ht=S({},[...D,...I,...O]),dt=S({},[...w,...M]),ut=function(e){m(n.removed,{element:e});try{Q(e).removeChild(e)}catch(t){V(e)}},mt=function(e,t){try{m(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){m(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(we||Me)try{ut(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},gt=function(e){let t=null,i=null;if(Oe)e=""+e;else{const t=f(e,/^[\r\n\t ]+/);i=t&&t[0]}"application/xhtml+xml"===nt&&Ke===qe&&(e=''+e+"");const n=ee?ee.createHTML(e):e;if(Ke===qe)try{t=(new z).parseFromString(n,nt)}catch(e){}if(!t||!t.documentElement){t=ie.createDocument(Ke,"template",null);try{t.documentElement.innerHTML=Je?te:n}catch(e){}}const a=t.body||t.documentElement;return e&&i&&a.insertBefore(r.createTextNode(i),a.childNodes[0]||null),Ke===qe?ae.call(t,De?"html":"body")[0]:De?t.documentElement:a},pt=function(e){return ne.call(e.ownerDocument||e,e,C.SHOW_ELEMENT|C.SHOW_COMMENT|C.SHOW_TEXT|C.SHOW_PROCESSING_INSTRUCTION|C.SHOW_CDATA_SECTION,null)},ft=function(e){return e instanceof F&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof G)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},_t=function(e){return"function"==typeof A&&e instanceof A};function Nt(e,t,i){d(e,(e=>{e.call(n,t,i,ot)}))}const Et=function(e){let t=null;if(Nt(se.beforeSanitizeElements,e,null),ft(e))return ut(e),!0;const i=at(e.nodeName);if(Nt(se.uponSanitizeElement,e,{tagName:i,allowedTags:_e}),e.hasChildNodes()&&!_t(e.firstElementChild)&&b(/<[/\w]/g,e.innerHTML)&&b(/<[/\w]/g,e.textContent))return ut(e),!0;if(7===e.nodeType)return ut(e),!0;if(Re&&8===e.nodeType&&b(/<[/\w]/g,e.data))return ut(e),!0;if(!_e[i]||ye[i]){if(!ye[i]&&bt(i)){if(be.tagNameCheck instanceof RegExp&&b(be.tagNameCheck,i))return!1;if(be.tagNameCheck instanceof Function&&be.tagNameCheck(i))return!1}if(Be&&!Fe[i]){const t=Q(e)||e.parentNode,i=Z(e)||e.childNodes;if(i&&t)for(let n=i.length-1;n>=0;--n){const r=Y(i[n],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,q(e))}}return ut(e),!0}return e instanceof v&&!function(e){let t=Q(e);t&&t.tagName||(t={namespaceURI:Ke,tagName:"template"});const i=g(e.tagName),n=g(t.tagName);return!!Ze[e.namespaceURI]&&(e.namespaceURI===$e?t.namespaceURI===qe?"svg"===i:t.namespaceURI===Ve?"svg"===i&&("annotation-xml"===n||et[n]):Boolean(ht[i]):e.namespaceURI===Ve?t.namespaceURI===qe?"math"===i:t.namespaceURI===$e?"math"===i&&tt[n]:Boolean(dt[i]):e.namespaceURI===qe?!(t.namespaceURI===$e&&!tt[n])&&!(t.namespaceURI===Ve&&!et[n])&&!dt[i]&&(it[i]||!ht[i]):!("application/xhtml+xml"!==nt||!Ze[e.namespaceURI]))}(e)?(ut(e),!0):"noscript"!==i&&"noembed"!==i&&"noframes"!==i||!b(/<\/no(script|embed|frames)/i,e.innerHTML)?(xe&&3===e.nodeType&&(t=e.textContent,d([le,ce,he],(e=>{t=_(t,e," ")})),e.textContent!==t&&(m(n.removed,{element:e.cloneNode()}),e.textContent=t)),Nt(se.afterSanitizeElements,e,null),!1):(ut(e),!0)},Tt=function(e,t,i){if(ke&&("id"===t||"name"===t)&&(i in r||i in st))return!1;if(Se&&!Ae[t]&&b(de,t));else if(ve&&b(ue,t));else if(!Ee[t]||Ae[t]){if(!(bt(e)&&(be.tagNameCheck instanceof RegExp&&b(be.tagNameCheck,e)||be.tagNameCheck instanceof Function&&be.tagNameCheck(e))&&(be.attributeNameCheck instanceof RegExp&&b(be.attributeNameCheck,t)||be.attributeNameCheck instanceof Function&&be.attributeNameCheck(t))||"is"===t&&be.allowCustomizedBuiltInElements&&(be.tagNameCheck instanceof RegExp&&b(be.tagNameCheck,i)||be.tagNameCheck instanceof Function&&be.tagNameCheck(i))))return!1}else if(Xe[t]);else if(b(fe,_(i,ge,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==N(i,"data:")||!We[e])if(Ce&&!b(me,_(i,ge,"")));else if(i)return!1;return!0},bt=function(e){return"annotation-xml"!==e&&f(e,pe)},yt=function(e){Nt(se.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||ft(e))return;const i={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Ee,forceKeepAttr:void 0};let r=t.length;for(;r--;){const a=t[r],{name:o,namespaceURI:s,value:l}=a,c=at(o);let h="value"===o?l:E(l);if(i.attrName=c,i.attrValue=h,i.keepAttr=!0,i.forceKeepAttr=void 0,Nt(se.uponSanitizeAttribute,e,i),h=i.attrValue,!Ue||"id"!==c&&"name"!==c||(mt(o,e),h="user-content-"+h),Re&&b(/((--!?|])>)|<\/(style|title)/i,h)){mt(o,e);continue}if(i.forceKeepAttr)continue;if(mt(o,e),!i.keepAttr)continue;if(!Le&&b(/\/>/i,h)){mt(o,e);continue}xe&&d([le,ce,he],(e=>{h=_(h,e," ")}));const m=at(e.nodeName);if(Tt(m,c,h)){if(ee&&"object"==typeof W&&"function"==typeof W.getAttributeType)if(s);else switch(W.getAttributeType(m,c)){case"TrustedHTML":h=ee.createHTML(h);break;case"TrustedScriptURL":h=ee.createScriptURL(h)}try{s?e.setAttributeNS(s,o,h):e.setAttribute(o,h),ft(e)?ut(e):u(n.removed)}catch(e){}}}Nt(se.afterSanitizeAttributes,e,null)},At=function e(t){let i=null;const n=pt(t);for(Nt(se.beforeSanitizeShadowDOM,t,null);i=n.nextNode();)Nt(se.uponSanitizeShadowNode,i,null),Et(i),yt(i),i.content instanceof c&&e(i.content);Nt(se.afterSanitizeShadowDOM,t,null)};return n.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=null,r=null,o=null,s=null;if(Je=!e,Je&&(e="\x3c!--\x3e"),"string"!=typeof e&&!_t(e)){if("function"!=typeof e.toString)throw y("toString is not a function");if("string"!=typeof(e=e.toString()))throw y("dirty is not a string, aborting")}if(!n.isSupported)return e;if(Ie||ct(t),n.removed=[],"string"==typeof e&&(He=!1),He){if(e.nodeName){const t=at(e.nodeName);if(!_e[t]||ye[t])throw y("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof A)i=gt("\x3c!----\x3e"),r=i.ownerDocument.importNode(e,!0),1===r.nodeType&&"BODY"===r.nodeName||"HTML"===r.nodeName?i=r:i.appendChild(r);else{if(!we&&!xe&&!De&&-1===e.indexOf("<"))return ee&&Pe?ee.createHTML(e):e;if(i=gt(e),!i)return we?null:Pe?te:""}i&&Oe&&ut(i.firstChild);const l=pt(He?e:i);for(;o=l.nextNode();)Et(o),yt(o),o.content instanceof c&&At(o.content);if(He)return e;if(we){if(Me)for(s=re.call(i.ownerDocument);i.firstChild;)s.appendChild(i.firstChild);else s=i;return(Ee.shadowroot||Ee.shadowrootmode)&&(s=oe.call(a,s,!0)),s}let h=De?i.outerHTML:i.innerHTML;return De&&_e["!doctype"]&&i.ownerDocument&&i.ownerDocument.doctype&&i.ownerDocument.doctype.name&&b($,i.ownerDocument.doctype.name)&&(h="\n"+h),xe&&d([le,ce,he],(e=>{h=_(h,e," ")})),ee&&Pe?ee.createHTML(h):h},n.setConfig=function(){ct(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Ie=!0},n.clearConfig=function(){ot=null,Ie=!1},n.isValidAttribute=function(e,t,i){ot||ct({});const n=at(e),r=at(t);return Tt(n,r,i)},n.addHook=function(e,t){"function"==typeof t&&m(se[e],t)},n.removeHook=function(e){return u(se[e])},n.removeHooks=function(e){se[e]=[]},n.removeAllHooks=function(){se={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},n}();e.exports=Z}},t={};function i(n){var r=t[n];if(void 0!==r)return r.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,i),a.exports}i.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return i.d(t,{a:t}),t},i.d=(e,t)=>{for(var n in t)i.o(t,n)&&!i.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";var e=i(396);function t(e){webkit.messageHandlers.readabilityMessageHandler.postMessage({Type:"StateChange",Value:e})}(0,e.isProbablyReaderable)(document)?t("Available"):t("Unavailable");var n=(new XMLSerializer).serializeToString(document);const r=i(454).sanitize(n,{WHOLE_DOCUMENT:!0});var a=(new DOMParser).parseFromString(r,"text/html");const o=new e.Readability(a,__READABILITY_OPTION__).parse();webkit.messageHandlers.readabilityMessageHandler.postMessage({Type:"ContentParsed",Value:JSON.stringify(o)})})()})();
--------------------------------------------------------------------------------
/Sources/Readability/Resources/ReadabilitySanitized.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*! @license DOMPurify 3.2.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.3/LICENSE */
2 |
--------------------------------------------------------------------------------
/Sources/Readability/exported.swift:
--------------------------------------------------------------------------------
1 | @_exported import ReadabilityCore
2 |
--------------------------------------------------------------------------------
/Sources/ReadabilityCore/ReadabilityMessageHandler.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import WebKit
3 |
4 | /// A message handler for receiving messages from injected JavaScript in the WKWebView.
5 | @MainActor
6 | package final class ReadabilityMessageHandler: NSObject, WKScriptMessageHandler {
7 | /// Modes that determine how the message handler processes content.
8 | package enum Mode {
9 | /// Generates reader HTML using the provided initial style.
10 | case generateReaderHTML(initialStyle: ReaderStyle)
11 | /// Returns the raw readability result.
12 | case generateReadabilityResult
13 | }
14 |
15 | /// Events emitted by the message handler.
16 | package enum Event {
17 | /// The readability content was parsed and reader HTML was generated.
18 | case contentParsedAndGeneratedHTML(html: String)
19 | /// The readability content was parsed.
20 | case contentParsed(readabilityResult: ReadabilityResult)
21 | /// The availability status of the reader changed.
22 | case availabilityChanged(availability: ReaderAvailability)
23 | }
24 |
25 | // The generator used to produce reader HTML from the readability result.
26 | private let readerContentGenerator: Generator
27 | private let mode: Mode
28 |
29 | /// A closure that is called when an event is received.
30 | package var eventHandler: (@MainActor (Event) -> Void)?
31 |
32 | package init(mode: Mode, readerContentGenerator: Generator) {
33 | self.mode = mode
34 | self.readerContentGenerator = readerContentGenerator
35 | }
36 |
37 | package func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
38 | guard let message = message.body as? [String: Any],
39 | let typeString = message["Type"] as? String,
40 | let type = ReadabilityMessageType(rawValue: typeString),
41 | let value = message["Value"]
42 | else {
43 | return
44 | }
45 |
46 | switch type {
47 | case .stateChange:
48 | if let availability = ReaderAvailability(rawValue: value as? String ?? "") {
49 | eventHandler?(.availabilityChanged(availability: availability))
50 | }
51 | case .contentParsed:
52 | Task.detached { [weak self, mode] in
53 | if let jsonString = value as? String,
54 | let jsonData = jsonString.data(using: .utf8),
55 | let result = try? JSONDecoder().decode(ReadabilityResult.self, from: jsonData)
56 | {
57 | switch mode {
58 | case let .generateReaderHTML(initialStyle):
59 | if let html = await self?.readerContentGenerator.generate(result, initialStyle: initialStyle) {
60 | await self?.eventHandler?(.contentParsedAndGeneratedHTML(html: html))
61 | }
62 | case .generateReadabilityResult:
63 | await self?.eventHandler?(.contentParsed(readabilityResult: result))
64 | }
65 | }
66 | }
67 | }
68 | }
69 |
70 | /// Subscribes to events emitted by the message handler.
71 | ///
72 | /// - Parameter operation: A closure to be invoked when an event occurs, or `nil` to unsubscribe.
73 | package func subscribeEvent(_ operation: (@MainActor (Event) -> Void)?) {
74 | eventHandler = operation
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/ReadabilityCore/ReadabilityMessageType.swift:
--------------------------------------------------------------------------------
1 | /// Represents the type of messages exchanged between the JavaScript and the native code.
2 | enum ReadabilityMessageType: String {
3 | /// Indicates a change in the reader state.
4 | case stateChange = "StateChange"
5 | /// Indicates that the content has been parsed.
6 | case contentParsed = "ContentParsed"
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/ReadabilityCore/ReadabilityResult.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A structure representing the result of parsing a web page using Readability.
4 | /// It contains metadata and content extracted from the web page.
5 | public struct ReadabilityResult: Decodable, Sendable {
6 | /// The title of the article.
7 | public let title: String
8 | /// The byline of the article, if available.
9 | public let byline: String?
10 | /// The main HTML content of the article.
11 | public let content: String
12 | /// The plain text content of the article.
13 | public let textContent: String
14 | /// The length of the article content.
15 | public let length: Int
16 | /// An excerpt from the article.
17 | public let excerpt: String
18 | /// The name of the site where the article originated.
19 | public let siteName: String?
20 | /// The language of the article.
21 | public let language: String
22 | /// The text direction (e.g., "ltr", "rtl") of the article, if available.
23 | public let direction: String?
24 | /// The published time of the article, if available.
25 | public let publishedTime: String?
26 |
27 | public enum CodingKeys: String, CodingKey, Sendable {
28 | case title
29 | case byline
30 | case content
31 | case textContent
32 | case length
33 | case excerpt
34 | case siteName
35 | case language = "lang"
36 | case direction = "dir"
37 | case publishedTime
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/ReadabilityCore/ReaderAvailability.swift:
--------------------------------------------------------------------------------
1 | /// An enumeration representing the availability status of the reader mode.
2 | public enum ReaderAvailability: String, Sendable {
3 | /// The reader mode is available.
4 | case available = "Available"
5 | /// The reader mode is unavailable.
6 | case unavailable = "Unavailable"
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/ReadabilityCore/ReaderContentGeneratable.swift:
--------------------------------------------------------------------------------
1 | /// A protocol that defines the ability to generate reader content (HTML) from a `ReadabilityResult` and an initial style.
2 | package protocol ReaderContentGeneratable: Sendable {
3 | /// Generates reader HTML content based on the provided readability result and initial style.
4 | ///
5 | /// - Parameters:
6 | /// - readabilityResult: The result of the readability parsing.
7 | /// - initialStyle: The initial style to apply to the reader content.
8 | /// - Returns: An optional `String` containing the generated HTML content.
9 | func generate(
10 | _ readabilityResult: ReadabilityResult,
11 | initialStyle: ReaderStyle
12 | ) async -> String?
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/ReadabilityCore/ReaderStyle.swift:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/
4 |
5 | #if canImport(UIKit)
6 | import UIKit
7 | #elseif canImport(AppKit)
8 | import AppKit
9 | #endif
10 |
11 | /// A structure representing the style settings for the reader mode.
12 | public struct ReaderStyle: Sendable, Codable, Hashable {
13 | /// The theme to be applied in reader mode.
14 | public var theme: Theme
15 | /// The font size to be applied in reader mode.
16 | public var fontSize: FontSize
17 |
18 | /// Initializes a new `ReaderStyle` with the specified theme and font size.
19 | ///
20 | /// - Parameters:
21 | /// - theme: The theme to use (e.g., light, dark, sepia).
22 | /// - fontSize: The font size setting.
23 | public init(theme: Theme, fontSize: FontSize) {
24 | self.theme = theme
25 | self.fontSize = fontSize
26 | }
27 |
28 | /// An enumeration representing the available themes for reader mode.
29 | public enum Theme: String, Sendable, Codable, Hashable, CaseIterable {
30 | /// A light theme.
31 | case light
32 | /// A dark theme.
33 | case dark
34 | /// A sepia theme.
35 | case sepia
36 | }
37 |
38 | /// An enumeration representing the available font sizes for reader mode.
39 | public enum FontSize: Int, Sendable, Codable, Hashable, CaseIterable {
40 | /// The smallest font size.
41 | case size1 = 1
42 | case size2 = 2
43 | case size3 = 3
44 | case size4 = 4
45 | case size5 = 5
46 | case size6 = 6
47 | case size7 = 7
48 | case size8 = 8
49 | case size9 = 9
50 | case size10 = 10
51 | case size11 = 11
52 | case size12 = 12
53 | /// The largest font size.
54 | case size13 = 13
55 |
56 | /// Checks if the current font size is the smallest.
57 | public var isSmallest: Bool {
58 | self == FontSize.size1
59 | }
60 |
61 | /// Checks if the current font size is the largest.
62 | public var isLargest: Bool {
63 | self == FontSize.size13
64 | }
65 |
66 | /// Returns a smaller font size if available.
67 | ///
68 | /// - Returns: The next smaller font size, or the current size if already smallest.
69 | public func smaller() -> FontSize {
70 | if isSmallest {
71 | return self
72 | } else {
73 | return FontSize(rawValue: rawValue - 1)!
74 | }
75 | }
76 |
77 | /// Returns a larger font size if available.
78 | public func bigger() -> FontSize {
79 | if isLargest {
80 | return self
81 | } else {
82 | return FontSize(rawValue: rawValue + 1)!
83 | }
84 | }
85 |
86 | /// The default font size based on the user's preferred content size category.
87 | #if canImport(UIKit)
88 | @MainActor
89 | static var defaultSize: FontSize {
90 | switch UIApplication.shared.preferredContentSizeCategory {
91 | case .extraSmall:
92 | .size1
93 | case .small:
94 | .size2
95 | case .medium:
96 | .size3
97 | case .large:
98 | .size5
99 | case .extraLarge:
100 | .size7
101 | case .extraExtraLarge:
102 | .size9
103 | case .extraExtraExtraLarge:
104 | .size12
105 | default:
106 | .size5
107 | }
108 | }
109 | #endif
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Sources/ReadabilityCore/ScriptLoader.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// An actor responsible for loading JavaScript and HTML resources from the bundle.
4 | package actor ScriptLoader {
5 | /// Resources available to be loaded by the ScriptLoader.
6 | package enum Resource {
7 | /// Script to be injected at document start.
8 | case atDocumentStart
9 | /// Script to be injected at document end.
10 | case atDocumentEnd
11 | /// HTML template for generating reader content.
12 | case readerHTML
13 | /// Basic Readability parsing script.
14 | case readabilityBasic
15 | /// Sanitized Readability parsing script.
16 | case readabilitySanitized
17 |
18 | /// The name of the resource file (without extension).
19 | var name: String {
20 | switch self {
21 | case .atDocumentStart:
22 | return "AtDocumentStart"
23 | case .atDocumentEnd:
24 | return "AtDocumentEnd"
25 | case .readerHTML:
26 | return "Reader"
27 | case .readabilityBasic:
28 | return "ReadabilityBasic"
29 | case .readabilitySanitized:
30 | return "ReadabilitySanitized"
31 | }
32 | }
33 |
34 | /// The file extension of the resource.
35 | var ext: String {
36 | switch self {
37 | case .atDocumentStart, .atDocumentEnd, .readabilityBasic, .readabilitySanitized:
38 | return "js"
39 | case .readerHTML:
40 | return "html"
41 | }
42 | }
43 | }
44 |
45 | // The bundle from which resources are loaded.
46 | private let bundle: Bundle
47 |
48 | /// Initializes a new `ScriptLoader` with the specified bundle.
49 | ///
50 | /// - Parameter bundle: The bundle containing the script resources.
51 | package init(bundle: Bundle) {
52 | self.bundle = bundle
53 | }
54 |
55 | /// Loads the content of the specified resource.
56 | ///
57 | /// - Parameter resource: The resource to load.
58 | /// - Returns: A `String` containing the contents of the resource.
59 | /// - Throws: An error if the resource cannot be found or read.
60 | package func load(_ resource: Resource) throws -> String {
61 | try load(forResource: resource.name, withExtension: resource.ext)
62 | }
63 |
64 | /// Loads the content for a given resource name and file extension.
65 | ///
66 | /// - Parameters:
67 | /// - name: The name of the resource.
68 | /// - ext: The file extension of the resource.
69 | /// - Returns: A `String` containing the resource's contents.
70 | /// - Throws: An error if the resource cannot be located or read.
71 | private func load(forResource name: String, withExtension ext: String) throws -> String {
72 | guard let url = bundle.url(forResource: name, withExtension: ext) else {
73 | throw Error.failedToCopyReadabilityScriptFromNodeModules
74 | }
75 |
76 | let readabilityScript = try String(contentsOf: url, encoding: .utf8)
77 |
78 | return readabilityScript
79 | }
80 | }
81 |
82 | extension ScriptLoader {
83 | /// Errors that can occur while loading script resources.
84 | enum Error: Swift.Error {
85 | /// Indicates failure to locate or read the Readability script from the bundle.
86 | case failedToCopyReadabilityScriptFromNodeModules
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Sources/ReadabilityUI/Internal/ReaderContentGenerator.swift:
--------------------------------------------------------------------------------
1 | // This Source Code Form is subject to the terms of the Mozilla Public
2 | // License, v. 2.0. If a copy of the MPL was not distributed with this
3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/
4 |
5 | import Foundation
6 | import ReadabilityCore
7 |
8 | /// A content generator that creates reader HTML using a template and a readability result.
9 | /// Conforms to the `ReaderContentGeneratable` protocol.
10 | struct ReaderContentGenerator: ReaderContentGeneratable {
11 | private let encoder = {
12 | let encoder = JSONEncoder()
13 | return encoder
14 | }()
15 |
16 | private let scriptLoader = ScriptLoader(bundle: .module)
17 |
18 | /// Generates reader HTML content based on the provided `ReadabilityResult` and `ReaderStyle`.
19 | ///
20 | /// - Parameters:
21 | /// - readabilityResult: The result of the readability parsing.
22 | /// - initialStyle: The initial style settings to apply.
23 | /// - Returns: An optional `String` containing the generated reader HTML, or `nil` if generation fails.
24 | func generate(
25 | _ readabilityResult: ReadabilityResult,
26 | initialStyle: ReaderStyle
27 | ) async -> String? {
28 | // Load the HTML template and encode the reader style into JSON.
29 | guard let template = try? await scriptLoader.load(.readerHTML),
30 | let styleData = try? encoder.encode(initialStyle),
31 | let styleString = String(data: styleData, encoding: .utf8)
32 | else { return nil }
33 |
34 | // Replace placeholders in the template with actual content.
35 | return template.replacingOccurrences(of: "%READER-STYLE%", with: styleString)
36 | .replacingOccurrences(of: "%READER-TITLE%", with: readabilityResult.title)
37 | .replacingOccurrences(of: "%READER-BYLINE%", with: readabilityResult.byline ?? "")
38 | .replacingOccurrences(of: "%READER-CONTENT%", with: readabilityResult.content)
39 | .replacingOccurrences(of: "%READER-LANGUAGE%", with: readabilityResult.language)
40 | .replacingOccurrences(of: "%READER-DIRECTION%", with: readabilityResult.direction ?? "auto")
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/ReadabilityUI/ReadabilityWebCoordinator.swift:
--------------------------------------------------------------------------------
1 | import ReadabilityCore
2 | import SwiftUI
3 | import WebKit
4 |
5 | /// A coordinator that manages a WKWebView configured for reader mode.
6 | /// It sets up the necessary scripts and message handlers to parse content and manage reader mode availability.
7 | @MainActor
8 | public final class ReadabilityWebCoordinator: ObservableObject {
9 | // A weak reference to the message handler that processes JavaScript messages.
10 | private weak var messageHandler: ReadabilityMessageHandler?
11 | // A weak reference to the WKWebView configuration.
12 | private weak var configuration: WKWebViewConfiguration?
13 |
14 | private let scriptLoader = ScriptLoader(bundle: .module)
15 | private let messageHandlerName = "readabilityMessageHandler"
16 |
17 | private var (_contentParsed, contentParsedContinuation) = AsyncStream.makeStream(of: String.self)
18 | private var (_availabilityChanged, availabilityChangedContinuation) = AsyncStream.makeStream(of: ReaderAvailability.self)
19 |
20 | /// An asynchronous stream that emits the generated reader HTML when the content is parsed.
21 | public var contentParsed: AsyncStream {
22 | _contentParsed
23 | }
24 |
25 | /// An asynchronous stream that emits updates to the reader mode availability status.
26 | public var availabilityChanged: AsyncStream {
27 | _availabilityChanged
28 | }
29 |
30 | /// The initial style to apply to the reader content.
31 | public let initialStyle: ReaderStyle
32 |
33 | /// Initializes a new `ReadabilityWebCoordinator` with the specified initial style.
34 | ///
35 | /// - Parameter initialStyle: The initial `ReaderStyle` to use.
36 | public init(initialStyle: ReaderStyle) {
37 | self.initialStyle = initialStyle
38 | }
39 |
40 | /// Creates and configures a `WKWebViewConfiguration` for reader mode.
41 | ///
42 | /// - Returns: A configured `WKWebViewConfiguration` with injected scripts and message handlers.
43 | /// - Throws: An error if script loading fails.
44 | public func createReadableWebViewConfiguration() async throws -> WKWebViewConfiguration {
45 | async let documentStartStringTask = scriptLoader.load(.atDocumentStart)
46 | async let documentEndStringTask = scriptLoader.load(.atDocumentEnd)
47 |
48 | let (documentStartString, documentEndString) = try await (documentStartStringTask, documentEndStringTask)
49 |
50 | let documentStartScript = WKUserScript(
51 | source: documentStartString,
52 | injectionTime: .atDocumentStart,
53 | forMainFrameOnly: true
54 | )
55 |
56 | let documentEndScript = WKUserScript(
57 | source: documentEndString,
58 | injectionTime: .atDocumentEnd,
59 | forMainFrameOnly: true
60 | )
61 |
62 | let configuration = WKWebViewConfiguration()
63 | let messageHandler = ReadabilityMessageHandler(
64 | mode: .generateReaderHTML(initialStyle: initialStyle),
65 | readerContentGenerator: ReaderContentGenerator()
66 | )
67 |
68 | self.configuration = configuration
69 | self.messageHandler = messageHandler
70 |
71 | configuration.userContentController.addUserScript(documentStartScript)
72 | configuration.userContentController.addUserScript(documentEndScript)
73 | configuration.userContentController.add(messageHandler, name: messageHandlerName)
74 |
75 | messageHandler.subscribeEvent { [weak self] event in
76 | switch event {
77 | case let .availabilityChanged(availability):
78 | self?.availabilityChangedContinuation.yield(availability)
79 | case let .contentParsedAndGeneratedHTML(html: html):
80 | self?.contentParsedContinuation.yield(html)
81 | case .contentParsed:
82 | break
83 | }
84 | }
85 |
86 | return configuration
87 | }
88 |
89 | /// Invalidates the current configuration by removing all script message handlers and finishing the asynchronous streams.
90 | public func invalidate() {
91 | configuration?.userContentController.removeScriptMessageHandler(forName: messageHandlerName)
92 | configuration?.userContentController.removeAllUserScripts()
93 | contentParsedContinuation.finish()
94 | availabilityChangedContinuation.finish()
95 | }
96 |
97 | deinit {
98 | MainActor.assumeIsolated {
99 | invalidate()
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/Sources/ReadabilityUI/ReaderControllable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import ReadabilityCore
3 | import WebKit
4 |
5 | /// A protocol defining an interface for controlling a reader mode web view.
6 | /// Provides methods to evaluate JavaScript and manipulate the reader overlay.
7 | @MainActor
8 | public protocol ReaderControllable {
9 | /// Evaluates a JavaScript string in the context of the web view.
10 | ///
11 | /// - Parameter javascriptString: The JavaScript code to evaluate.
12 | /// - Returns: The result of the JavaScript evaluation.
13 | /// - Throws: An error if the evaluation fails.
14 | func evaluateJavaScript(_ javascriptString: String) async throws -> Any
15 | }
16 |
17 | public extension ReaderControllable {
18 | /// The JavaScript namespace for the Readability functions.
19 | private var namespace: String {
20 | "window.__swift_readability__"
21 | }
22 |
23 | /// Sets the reader style of the web view.
24 | ///
25 | /// - Parameter style: The `ReaderStyle` to apply.
26 | /// - Throws: An error if the JavaScript evaluation fails.
27 | func set(style: ReaderStyle) async throws {
28 | guard try await isReaderMode() else {
29 | throw ReaderControllableError.readerStyleChangeOnlyAllowedInReaderMode
30 | }
31 | let jsonData = try JSONEncoder().encode(style)
32 | let jsonString = String(data: jsonData, encoding: .utf8)!
33 |
34 | _ = try await evaluateJavaScript(
35 | "\(namespace).setStyle(\(jsonString));0"
36 | )
37 | }
38 |
39 | /// Sets the reader theme of the web view.
40 | ///
41 | /// - Parameter theme: The `ReaderStyle.Theme` to apply.
42 | /// - Throws: An error if the JavaScript evaluation fails.
43 | func set(theme: ReaderStyle.Theme) async throws {
44 | guard try await isReaderMode() else {
45 | throw ReaderControllableError.readerStyleChangeOnlyAllowedInReaderMode
46 | }
47 | let jsonData = try JSONEncoder().encode(theme)
48 | let jsonString = String(data: jsonData, encoding: .utf8)!
49 |
50 | _ = try await evaluateJavaScript("\(namespace).setTheme(\(jsonString));0")
51 | }
52 |
53 | /// Sets the font size of the reader content.
54 | ///
55 | /// - Parameter fontSize: The `ReaderStyle.FontSize` to apply.
56 | /// - Throws: An error if the JavaScript evaluation fails.
57 | func set(fontSize: ReaderStyle.FontSize) async throws {
58 | guard try await isReaderMode() else {
59 | throw ReaderControllableError.readerStyleChangeOnlyAllowedInReaderMode
60 | }
61 | let jsonData = try JSONEncoder().encode(fontSize)
62 | let jsonString = String(data: jsonData, encoding: .utf8)!
63 |
64 | _ = try await evaluateJavaScript("\(namespace).setFontSize(\(jsonString));0")
65 | }
66 |
67 | /// Displays the reader content overlay with the specified HTML.
68 | ///
69 | /// - Parameter html: The HTML content to display.
70 | /// - Throws: An error if the JavaScript evaluation fails.
71 | func showReaderContent(with html: String) async throws {
72 | let escapedHTML = html.jsonEscaped
73 | _ = try await evaluateJavaScript("\(namespace).showReaderOverlay(\(escapedHTML));0")
74 | }
75 |
76 | /// Hides the reader content overlay.
77 | ///
78 | /// - Throws: An error if the JavaScript evaluation fails.
79 | func hideReaderContent() async throws {
80 | _ = try await evaluateJavaScript("\(namespace).hideReaderOverlay();0")
81 | }
82 |
83 | /// Checks whether the web view is currently in reader mode.
84 | ///
85 | /// - Returns: `true` if the web view is in reader mode, otherwise `false`.
86 | /// - Throws: An error if the JavaScript evaluation fails.
87 | func isReaderMode() async throws -> Bool {
88 | let isReaderMode = try await evaluateJavaScript("\(namespace).isReaderMode() ? 1 : 0") as? Int
89 | return isReaderMode == 1 ? true : false
90 | }
91 | }
92 |
93 | private extension String {
94 | var jsonEscaped: String {
95 | let data = try? JSONSerialization.data(withJSONObject: [self], options: [])
96 | if let data = data,
97 | let json = String(data: data, encoding: .utf8),
98 | json.first == "[", json.last == "]"
99 | {
100 | return String(json.dropFirst().dropLast())
101 | }
102 | return self
103 | }
104 | }
105 |
106 | public enum ReaderControllableError: LocalizedError {
107 | case readerStyleChangeOnlyAllowedInReaderMode
108 |
109 | public var errorDescription: String? {
110 | switch self {
111 | case .readerStyleChangeOnlyAllowedInReaderMode:
112 | "ReaderStyle changes are only available when in Reader Mode."
113 | }
114 | }
115 | }
116 |
117 | extension WKWebView: ReaderControllable {}
118 |
--------------------------------------------------------------------------------
/Sources/ReadabilityUI/Resources/AtDocumentEnd.js:
--------------------------------------------------------------------------------
1 | window.__swift_readability__.checkReadability();
2 | window.__swift_readability__.configureReader();
3 |
--------------------------------------------------------------------------------
/Sources/ReadabilityUI/Resources/AtDocumentStart.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*! @license DOMPurify 3.2.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.3/LICENSE */
2 |
--------------------------------------------------------------------------------
/Sources/ReadabilityUI/Resources/Reader.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | %READER-TITLE%
12 |
747 |
748 |
749 |
750 |
754 |
755 |
756 | %READER-CONTENT%
757 |
758 |
759 |
760 |
761 |
762 |
--------------------------------------------------------------------------------
/Sources/ReadabilityUI/exported.swift:
--------------------------------------------------------------------------------
1 | @_exported import ReadabilityCore
2 |
--------------------------------------------------------------------------------
/Tests/ReadabilityTests/ReadabilityTests.swift:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ryu0118/swift-readability/5baf9d1fddc66fae9be962462532a27269882f93/Tests/ReadabilityTests/ReadabilityTests.swift
--------------------------------------------------------------------------------
/bootstrap.sh:
--------------------------------------------------------------------------------
1 | if ! command -v nest >/dev/null 2>&1; then
2 | curl -s https://raw.githubusercontent.com/mtj0928/nest/main/Scripts/install.sh | bash
3 | fi
4 |
5 | ~/.nest/bin/nest bootstrap nestfile.yaml
6 |
7 | npm install
8 | npm run build
9 |
--------------------------------------------------------------------------------
/nestfile.yaml:
--------------------------------------------------------------------------------
1 | nestPath: ./.nest
2 | targets:
3 | - reference: nicklockwood/SwiftFormat
4 | version: 0.55.5
5 | assetName: swiftformat.artifactbundle.zip
6 | checksum: 2c6e8903b88ca94f621586a91617c89337f53460bb3db00e3de655f96895a1a8
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@mozilla/readability": "^0.5.0",
4 | "dompurify": "^3.2.3"
5 | },
6 | "devDependencies": {
7 | "html-webpack-plugin": "^5.6.3",
8 | "raw-loader": "^4.0.2",
9 | "webpack": "^5.97.1",
10 | "webpack-cli": "^6.0.1"
11 | },
12 | "scripts": {
13 | "build": "./node_modules/.bin/webpack -c webpack.config.js"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/webpack-resources/AtDocumentStart.js:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 | * You can obtain one at http://mozilla.org/MPL/2.0/.
4 | */
5 |
6 | "use strict";
7 | import { isProbablyReaderable, Readability } from "@mozilla/readability";
8 |
9 | // Debug flag to control logging.
10 | const DEBUG = false;
11 |
12 | // Variables to hold the readability result, current style, and original body style.
13 | let readabilityResult = null;
14 | let currentStyle = null;
15 | let originalBodyStyle = null;
16 |
17 | const themeColors = {
18 | light: { background: "#ffffff", color: "#15141a" },
19 | dark: { background: "#333333", color: "#fbfbfe" },
20 | sepia: { background: "#fff4de", color: "#15141a" }
21 | };
22 |
23 | // Selector for block-level images in the content.
24 | const BLOCK_IMAGES_SELECTOR =
25 | ".content p > img:only-child, " +
26 | ".content p > a:only-child > img:only-child, " +
27 | ".content .wp-caption img, " +
28 | ".content figure img";
29 |
30 | /**
31 | * Logs debug information if DEBUG is enabled.
32 | * @param {*} s - The message or object to log.
33 | */
34 | function debug(s) {
35 | if (!DEBUG) {
36 | return;
37 | }
38 | console.log(s);
39 | }
40 |
41 | /**
42 | * Checks if the current document is readerable and initiates parsing if so.
43 | */
44 | function checkReadability() {
45 | setTimeout(function() {
46 | if (!isProbablyReaderable(document)) {
47 | postStateChangedToUnavailable();
48 | return;
49 | }
50 |
51 | if ((document.location.protocol === "http:" || document.location.protocol === "https:") &&
52 | document.location.pathname !== "/") {
53 | // If a previous readability result exists, reuse it.
54 | if (readabilityResult && readabilityResult.content) {
55 | postStateChangedToAvailable();
56 | postContentParsed(readabilityResult);
57 | return;
58 | }
59 |
60 | const uri = {
61 | spec: document.location.href,
62 | host: document.location.host,
63 | prePath: document.location.protocol + "//" + document.location.host,
64 | scheme: document.location.protocol.substr(0, document.location.protocol.indexOf(":")),
65 | pathBase: document.location.protocol + "//" + document.location.host + location.pathname.substr(0, location.pathname.lastIndexOf("/") + 1)
66 | };
67 |
68 | const docStr = new XMLSerializer().serializeToString(document);
69 | if (docStr.indexOf("