diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index 24fdf99..0000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-image: node:latest
-
-stages:
- - deploy
-
-variables:
- REGISTRY_URL: "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/"
-
-deploy:
- stage: deploy
- script:
- - echo "@scope:registry=https:${REGISTRY_URL}" > .npmrc
- - echo "${REGISTRY_URL}:_authToken=${CI_JOB_TOKEN}" >> .npmrc
- - npm publish
- only:
- - master
- environment: production
diff --git a/CHANGELOG b/CHANGELOG
index a77726f..50d9bb8 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,9 @@
+v4.4.1 - 04/04/2024
+- Fix render emojos in warning/spoiler text
+- Sanitize post content before rendering
+- Add custom title for play video button
+- Update Rollup devDependency version
+
v4.3.12 - 26/03/2024
- Add button to hide sensitive/spoiler media
- Fix Refresh button bug when empty text
diff --git a/README.md b/README.md
index b498bd0..ca5bb1e 100644
--- a/README.md
+++ b/README.md
@@ -65,11 +65,11 @@ This option allows you to start without the need to upload any files on your ser
Copy the following CSS and JS links to include them in your project:
```html
-
+
```
```html
-
+
```
### Package manager
@@ -273,13 +273,16 @@ Here you have all the options available to quickly setup and customize your time
// Default: false (don't hide)
hideEmojos: false,
- // Customize the text of the button used for showing sensitive/spoiler media content
+ // Customize the text of the button used for showing a sensitive/spoiler media content
btnShowContent: "SHOW CONTENT",
- // Hide video image preview and load video player instead
+ // Hide video image preview and load the video player instead
// Default: false (don't hide)
hideVideoPreview: false,
+ // Customize the text of the button used for the image preview to play the video
+ btnPlayVideoTxt: "Load and play video",
+
// Hide preview card if post contains a link, photo or video from a Url
// Default: false (don't hide)
hidePreviewLink: false,
diff --git a/dist/mastodon-timeline.esm.js b/dist/mastodon-timeline.esm.js
index a79af25..eec777d 100644
--- a/dist/mastodon-timeline.esm.js
+++ b/dist/mastodon-timeline.esm.js
@@ -1,8 +1,8 @@
/**
* Mastodon embed timeline
* @author idotj
- * @version 4.3.12
+ * @version 4.4.1
* @url https://gitlab.com/idotj/mastodon-embed-timeline
* @license GNU AGPLv3
*/
-class t{constructor(t={}){this.defaultSettings={mtContainerId:"mt-container",instanceUrl:"https://mastodon.social",timelineType:"local",userId:"",profileName:"",hashtagName:"",spinnerClass:"mt-loading-spinner",defaultTheme:"auto",maxNbPostFetch:"20",maxNbPostShow:"20",dateLocale:"en-GB",dateOptions:{day:"2-digit",month:"short",year:"numeric"},hideUnlisted:!1,hideReblog:!1,hideReplies:!1,hidePinnedPosts:!1,hideUserAccount:!1,txtMaxLines:"",btnShowMore:"SHOW MORE",btnShowLess:"SHOW LESS",markdownBlockquote:!1,hideEmojos:!1,btnShowContent:"SHOW CONTENT",hideVideoPreview:!1,hidePreviewLink:!1,previewMaxLines:"",hideCounterBar:!1,disableCarousel:!1,carouselCloseTxt:"Close carousel",carouselPrevTxt:"Previous media item",carouselNextTxt:"Next media item",btnSeeMore:"See more posts at Mastodon",btnReload:"Refresh",insistSearchContainer:!1,insistSearchContainerTime:"3000"},this.mtSettings={...this.defaultSettings,...t},this.#t(),this.linkHeader={},this.mtContainerNode="",this.mtBodyNode="",this.fetchedData={},this.#e((()=>{this.#i()}))}#t(){Number(this.mtSettings.maxNbPostShow)>Number(this.mtSettings.maxNbPostFetch)&&(console.error(`Please check your settings! The maximum number of posts to show is bigger than the maximum number of posts to fetch. Changing the value of "maxNbPostFetch" to: ${this.mtSettings.maxNbPostShow}`),this.mtSettings.maxNbPostFetch=this.mtSettings.maxNbPostShow)}#e(t){"undefined"!=typeof document&&"complete"===document.readyState?t():"undefined"!=typeof document&&"complete"!==document.readyState&&document.addEventListener("DOMContentLoaded",t())}#i(){const t=()=>{this.mtContainerNode=document.getElementById(this.mtSettings.mtContainerId),this.mtBodyNode=this.mtContainerNode.getElementsByClassName("mt-body")[0],this.#s(),this.#a("newTimeline")};if(this.mtSettings.insistSearchContainer){const e=performance.now(),i=()=>{if(document.getElementById(this.mtSettings.mtContainerId))t();else{performance.now()-e container with id: "${this.mtSettings.mtContainerId}" after several attempts for ${this.mtSettings.insistSearchContainerTime/1e3} seconds`)}};i()}else document.getElementById(this.mtSettings.mtContainerId)?t():console.error(`Impossible to find the
container with id: "${this.mtSettings.mtContainerId}". Please try to add the option 'insistSearchContainer: true' when initializing the script`)}mtUpdate(){this.#e((()=>{this.mtBodyNode.replaceChildren(),this.mtBodyNode.insertAdjacentHTML("afterbegin",''),this.#a("updateTimeline")}))}mtColorTheme(t){this.#e((()=>{this.mtContainerNode.setAttribute("data-theme",t)}))}#s(){if("auto"===this.mtSettings.defaultTheme){let t=window.matchMedia("(prefers-color-scheme: dark)");t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light"),t.addEventListener("change",(t=>{t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light")}))}else this.mtColorTheme(this.mtSettings.defaultTheme)}#o(){return new Promise(((t,e)=>{const i=this.mtSettings.instanceUrl?`${this.mtSettings.instanceUrl}/api/v1/`:this.#n("Please check your instanceUrl value","⚠️"),s=this.#r(i),a=Object.entries(s).map((([t,i])=>{const s="timeline"===t;return this.#l(i,s).then((e=>({[t]:e}))).catch((s=>(e(new Error(`Something went wrong fetching data from: ${i}`)),this.#n(s.message),{[t]:[]})))}));Promise.all(a).then((async e=>{if(this.fetchedData=e.reduce(((t,e)=>({...t,...e})),{}),!this.mtSettings.hidePinnedPosts&&void 0!==this.fetchedData.pinned?.length&&0!==this.fetchedData.pinned.length){const t=this.fetchedData.pinned.map((t=>({...t,pinned:!0})));this.fetchedData.timeline=[...t,...this.fetchedData.timeline]}if(this.#d())t();else{do{await this.#m()}while(!this.#d()&&this.linkHeader.next);t()}}))}))}#r(t){let e={};return"profile"===this.mtSettings.timelineType?this.mtSettings.userId?(e.timeline=`${t}accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`,this.mtSettings.hidePinnedPosts||(e.pinned=`${t}accounts/${this.mtSettings.userId}/statuses?pinned=true`)):this.#n("Please check your userId value","⚠️"):"hashtag"===this.mtSettings.timelineType?this.mtSettings.hashtagName?e.timeline=`${t}timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`:this.#n("Please check your hashtagName value","⚠️"):"local"===this.mtSettings.timelineType?e.timeline=`${t}timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`:this.#n("Please check your timelineType value","⚠️"),this.mtSettings.hideEmojos||(e.emojos=`${t}custom_emojis`),e}async#l(t,e=!1){const i=await fetch(t);if(!i.ok)throw new Error(`\n Failed to fetch the following Url: ${t}Error status: ${i.status}Error message: ${i.statusText}\n `);const s=await i.json();return e&&i.headers.get("Link")&&(this.linkHeader=this.#h(i.headers.get("Link"))),s}#d(){return this.fetchedData.timeline.length>=Number(this.mtSettings.maxNbPostFetch)}#m(){return new Promise((t=>{this.linkHeader.next?this.#l(this.linkHeader.next,!0).then((e=>{this.fetchedData.timeline=[...this.fetchedData.timeline,...e],t()})):t()}))}#h(t){const e=t.split(", ").map((t=>t.split("; "))).map((t=>[t[1].replace(/"/g,"").replace("rel=",""),t[0].slice(1,-1)]));return Object.fromEntries(e)}async#a(t){await this.#o();const e=this.fetchedData.timeline;let i=0;if(this.mtBodyNode.replaceChildren(),e.forEach((t=>{const e="public"===t.visibility||!this.mtSettings.hideUnlisted&&"unlisted"===t.visibility,s=this.mtSettings.hideReblog&&t.reblog,a=this.mtSettings.hideReplies&&t.in_reply_to_id;!e||s||a||i${e?.length||0} posts have been fetched from the serverThis may be due to an incorrect configuration with the parameters or with the filters applied (to hide certains type of posts)`;this.#n(t,"📭")}}#g(){"0"!==this.mtSettings.txtMaxLines&&0!==this.mtSettings.txtMaxLines.length&&this.mtBodyNode.parentNode.style.setProperty("--mt-txt-max-lines",this.mtSettings.txtMaxLines),"0"!==this.mtSettings.previewMaxLines&&0!==this.mtSettings.previewMaxLines.length&&this.mtBodyNode.parentNode.style.setProperty("--mt-preview-max-lines",this.mtSettings.previewMaxLines)}#u(t){const e=this.mtBodyNode.getElementsByTagName("article");for(let i=0;i
"}let g=[];if(t.media_attachments.length>0)for(let e in t.media_attachments)g.push(this.#L(t.media_attachments[e],t.sensitive));if(t.reblog&&t.reblog.media_attachments.length>0)for(let e in t.reblog.media_attachments)g.push(this.#L(t.reblog.media_attachments[e],t.reblog.sensitive));g=`
${g.join("")}
`;let u="";!this.mtSettings.hidePreviewLink&&t.card&&(u=this.#N(t.card));let v="";if(t.poll){let e="";for(let i in t.poll.options)e+="
`,this.mtBodyNode.setAttribute("role","none"),new Error("Stopping the script due to an error building the timeline.")}}export{t as Init};
+class t{constructor(t={}){this.defaultSettings={mtContainerId:"mt-container",instanceUrl:"https://mastodon.social",timelineType:"local",userId:"",profileName:"",hashtagName:"",spinnerClass:"mt-loading-spinner",defaultTheme:"auto",maxNbPostFetch:"20",maxNbPostShow:"20",dateLocale:"en-GB",dateOptions:{day:"2-digit",month:"short",year:"numeric"},hideUnlisted:!1,hideReblog:!1,hideReplies:!1,hidePinnedPosts:!1,hideUserAccount:!1,txtMaxLines:"",btnShowMore:"SHOW MORE",btnShowLess:"SHOW LESS",markdownBlockquote:!1,hideEmojos:!1,btnShowContent:"SHOW CONTENT",hideVideoPreview:!1,btnPlayVideoTxt:"Load and play video",hidePreviewLink:!1,previewMaxLines:"",hideCounterBar:!1,disableCarousel:!1,carouselCloseTxt:"Close carousel",carouselPrevTxt:"Previous media item",carouselNextTxt:"Next media item",btnSeeMore:"See more posts at Mastodon",btnReload:"Refresh",insistSearchContainer:!1,insistSearchContainerTime:"3000"},this.mtSettings={...this.defaultSettings,...t},this.#t(),this.linkHeader={},this.mtContainerNode="",this.mtBodyNode="",this.fetchedData={},this.#e((()=>{this.#i()}))}#t(){Number(this.mtSettings.maxNbPostShow)>Number(this.mtSettings.maxNbPostFetch)&&(console.error(`Please check your settings! The maximum number of posts to show is bigger than the maximum number of posts to fetch. Changing the value of "maxNbPostFetch" to: ${this.mtSettings.maxNbPostShow}`),this.mtSettings.maxNbPostFetch=this.mtSettings.maxNbPostShow)}#e(t){"undefined"!=typeof document&&"complete"===document.readyState?t():"undefined"!=typeof document&&"complete"!==document.readyState&&document.addEventListener("DOMContentLoaded",t())}#i(){const t=()=>{this.mtContainerNode=document.getElementById(this.mtSettings.mtContainerId),this.mtBodyNode=this.mtContainerNode.getElementsByClassName("mt-body")[0],this.#s(),this.#a("newTimeline")};if(this.mtSettings.insistSearchContainer){const e=performance.now(),i=()=>{if(document.getElementById(this.mtSettings.mtContainerId))t();else{performance.now()-e container with id: "${this.mtSettings.mtContainerId}" after several attempts for ${this.mtSettings.insistSearchContainerTime/1e3} seconds`)}};i()}else document.getElementById(this.mtSettings.mtContainerId)?t():console.error(`Impossible to find the
container with id: "${this.mtSettings.mtContainerId}". Please try to add the option 'insistSearchContainer: true' when initializing the script`)}mtUpdate(){this.#e((()=>{this.mtBodyNode.replaceChildren(),this.mtBodyNode.insertAdjacentHTML("afterbegin",''),this.#a("updateTimeline")}))}mtColorTheme(t){this.#e((()=>{this.mtContainerNode.setAttribute("data-theme",t)}))}#s(){if("auto"===this.mtSettings.defaultTheme){let t=window.matchMedia("(prefers-color-scheme: dark)");t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light"),t.addEventListener("change",(t=>{t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light")}))}else this.mtColorTheme(this.mtSettings.defaultTheme)}#o(){return new Promise(((t,e)=>{const i=this.mtSettings.instanceUrl?`${this.mtSettings.instanceUrl}/api/v1/`:this.#n("Please check your instanceUrl value","⚠️"),s=this.#r(i),a=Object.entries(s).map((([t,i])=>{const s="timeline"===t;return this.#l(i,s).then((e=>({[t]:e}))).catch((s=>(e(new Error(`Something went wrong fetching data from: ${i}`)),this.#n(s.message),{[t]:[]})))}));Promise.all(a).then((async e=>{if(this.fetchedData=e.reduce(((t,e)=>({...t,...e})),{}),!this.mtSettings.hidePinnedPosts&&void 0!==this.fetchedData.pinned?.length&&0!==this.fetchedData.pinned.length){const t=this.fetchedData.pinned.map((t=>({...t,pinned:!0})));this.fetchedData.timeline=[...t,...this.fetchedData.timeline]}if(this.#d())t();else{do{await this.#m()}while(!this.#d()&&this.linkHeader.next);t()}}))}))}#r(t){let e={};return"profile"===this.mtSettings.timelineType?this.mtSettings.userId?(e.timeline=`${t}accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`,this.mtSettings.hidePinnedPosts||(e.pinned=`${t}accounts/${this.mtSettings.userId}/statuses?pinned=true`)):this.#n("Please check your userId value","⚠️"):"hashtag"===this.mtSettings.timelineType?this.mtSettings.hashtagName?e.timeline=`${t}timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`:this.#n("Please check your hashtagName value","⚠️"):"local"===this.mtSettings.timelineType?e.timeline=`${t}timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`:this.#n("Please check your timelineType value","⚠️"),this.mtSettings.hideEmojos||(e.emojos=`${t}custom_emojis`),e}async#l(t,e=!1){const i=await fetch(t);if(!i.ok)throw new Error(`\n Failed to fetch the following Url: ${t}Error status: ${i.status}Error message: ${i.statusText}\n `);const s=await i.json();return e&&i.headers.get("Link")&&(this.linkHeader=this.#h(i.headers.get("Link"))),s}#d(){return this.fetchedData.timeline.length>=Number(this.mtSettings.maxNbPostFetch)}#m(){return new Promise((t=>{this.linkHeader.next?this.#l(this.linkHeader.next,!0).then((e=>{this.fetchedData.timeline=[...this.fetchedData.timeline,...e],t()})):t()}))}#h(t){const e=t.split(", ").map((t=>t.split("; "))).map((t=>[t[1].replace(/"/g,"").replace("rel=",""),t[0].slice(1,-1)]));return Object.fromEntries(e)}async#a(t){await this.#o();const e=this.fetchedData.timeline;let i=0;if(this.mtBodyNode.replaceChildren(),e.forEach((t=>{const e="public"===t.visibility||!this.mtSettings.hideUnlisted&&"unlisted"===t.visibility,s=this.mtSettings.hideReblog&&t.reblog,a=this.mtSettings.hideReplies&&t.in_reply_to_id;!e||s||a||i${e?.length||0} posts have been fetched from the serverThis may be due to an incorrect configuration with the parameters or with the filters applied (to hide certains type of posts)`;this.#n(t,"📭")}}#g(){"0"!==this.mtSettings.txtMaxLines&&0!==this.mtSettings.txtMaxLines.length&&this.mtBodyNode.parentNode.style.setProperty("--mt-txt-max-lines",this.mtSettings.txtMaxLines),"0"!==this.mtSettings.previewMaxLines&&0!==this.mtSettings.previewMaxLines.length&&this.mtBodyNode.parentNode.style.setProperty("--mt-preview-max-lines",this.mtSettings.previewMaxLines)}#u(t){const e=this.mtBodyNode.getElementsByTagName("article");for(let i=0;i
"}let g=[];if(t.media_attachments.length>0)for(let e in t.media_attachments)g.push(this.#L(t.media_attachments[e],t.sensitive));if(t.reblog&&t.reblog.media_attachments.length>0)for(let e in t.reblog.media_attachments)g.push(this.#L(t.reblog.media_attachments[e],t.reblog.sensitive));g=`
${g.join("")}
`;let u="";!this.mtSettings.hidePreviewLink&&t.card&&(u=this.#T(t.card));let v="";if(t.poll){let e="";for(let i in t.poll.options)e+="
`,this.mtBodyNode.setAttribute("role","none"),new Error("Stopping the script due to an error building the timeline.")}}export{t as Init};
diff --git a/dist/mastodon-timeline.umd.js b/dist/mastodon-timeline.umd.js
index fe355e7..6046f1b 100644
--- a/dist/mastodon-timeline.umd.js
+++ b/dist/mastodon-timeline.umd.js
@@ -2,7 +2,7 @@
/**
* Mastodon embed timeline
* @author idotj
- * @version 4.3.12
+ * @version 4.4.1
* @url https://gitlab.com/idotj/mastodon-embed-timeline
* @license GNU AGPLv3
- */t.Init=class{constructor(t={}){this.defaultSettings={mtContainerId:"mt-container",instanceUrl:"https://mastodon.social",timelineType:"local",userId:"",profileName:"",hashtagName:"",spinnerClass:"mt-loading-spinner",defaultTheme:"auto",maxNbPostFetch:"20",maxNbPostShow:"20",dateLocale:"en-GB",dateOptions:{day:"2-digit",month:"short",year:"numeric"},hideUnlisted:!1,hideReblog:!1,hideReplies:!1,hidePinnedPosts:!1,hideUserAccount:!1,txtMaxLines:"",btnShowMore:"SHOW MORE",btnShowLess:"SHOW LESS",markdownBlockquote:!1,hideEmojos:!1,btnShowContent:"SHOW CONTENT",hideVideoPreview:!1,hidePreviewLink:!1,previewMaxLines:"",hideCounterBar:!1,disableCarousel:!1,carouselCloseTxt:"Close carousel",carouselPrevTxt:"Previous media item",carouselNextTxt:"Next media item",btnSeeMore:"See more posts at Mastodon",btnReload:"Refresh",insistSearchContainer:!1,insistSearchContainerTime:"3000"},this.mtSettings={...this.defaultSettings,...t},this.#t(),this.linkHeader={},this.mtContainerNode="",this.mtBodyNode="",this.fetchedData={},this.#e((()=>{this.#i()}))}#t(){Number(this.mtSettings.maxNbPostShow)>Number(this.mtSettings.maxNbPostFetch)&&(console.error(`Please check your settings! The maximum number of posts to show is bigger than the maximum number of posts to fetch. Changing the value of "maxNbPostFetch" to: ${this.mtSettings.maxNbPostShow}`),this.mtSettings.maxNbPostFetch=this.mtSettings.maxNbPostShow)}#e(t){"undefined"!=typeof document&&"complete"===document.readyState?t():"undefined"!=typeof document&&"complete"!==document.readyState&&document.addEventListener("DOMContentLoaded",t())}#i(){const t=()=>{this.mtContainerNode=document.getElementById(this.mtSettings.mtContainerId),this.mtBodyNode=this.mtContainerNode.getElementsByClassName("mt-body")[0],this.#s(),this.#a("newTimeline")};if(this.mtSettings.insistSearchContainer){const e=performance.now(),i=()=>{if(document.getElementById(this.mtSettings.mtContainerId))t();else{performance.now()-e container with id: "${this.mtSettings.mtContainerId}" after several attempts for ${this.mtSettings.insistSearchContainerTime/1e3} seconds`)}};i()}else document.getElementById(this.mtSettings.mtContainerId)?t():console.error(`Impossible to find the
container with id: "${this.mtSettings.mtContainerId}". Please try to add the option 'insistSearchContainer: true' when initializing the script`)}mtUpdate(){this.#e((()=>{this.mtBodyNode.replaceChildren(),this.mtBodyNode.insertAdjacentHTML("afterbegin",''),this.#a("updateTimeline")}))}mtColorTheme(t){this.#e((()=>{this.mtContainerNode.setAttribute("data-theme",t)}))}#s(){if("auto"===this.mtSettings.defaultTheme){let t=window.matchMedia("(prefers-color-scheme: dark)");t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light"),t.addEventListener("change",(t=>{t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light")}))}else this.mtColorTheme(this.mtSettings.defaultTheme)}#o(){return new Promise(((t,e)=>{const i=this.mtSettings.instanceUrl?`${this.mtSettings.instanceUrl}/api/v1/`:this.#n("Please check your instanceUrl value","⚠️"),s=this.#r(i),a=Object.entries(s).map((([t,i])=>{const s="timeline"===t;return this.#l(i,s).then((e=>({[t]:e}))).catch((s=>(e(new Error(`Something went wrong fetching data from: ${i}`)),this.#n(s.message),{[t]:[]})))}));Promise.all(a).then((async e=>{if(this.fetchedData=e.reduce(((t,e)=>({...t,...e})),{}),!this.mtSettings.hidePinnedPosts&&void 0!==this.fetchedData.pinned?.length&&0!==this.fetchedData.pinned.length){const t=this.fetchedData.pinned.map((t=>({...t,pinned:!0})));this.fetchedData.timeline=[...t,...this.fetchedData.timeline]}if(this.#d())t();else{do{await this.#m()}while(!this.#d()&&this.linkHeader.next);t()}}))}))}#r(t){let e={};return"profile"===this.mtSettings.timelineType?this.mtSettings.userId?(e.timeline=`${t}accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`,this.mtSettings.hidePinnedPosts||(e.pinned=`${t}accounts/${this.mtSettings.userId}/statuses?pinned=true`)):this.#n("Please check your userId value","⚠️"):"hashtag"===this.mtSettings.timelineType?this.mtSettings.hashtagName?e.timeline=`${t}timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`:this.#n("Please check your hashtagName value","⚠️"):"local"===this.mtSettings.timelineType?e.timeline=`${t}timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`:this.#n("Please check your timelineType value","⚠️"),this.mtSettings.hideEmojos||(e.emojos=`${t}custom_emojis`),e}async#l(t,e=!1){const i=await fetch(t);if(!i.ok)throw new Error(`\n Failed to fetch the following Url: ${t}Error status: ${i.status}Error message: ${i.statusText}\n `);const s=await i.json();return e&&i.headers.get("Link")&&(this.linkHeader=this.#h(i.headers.get("Link"))),s}#d(){return this.fetchedData.timeline.length>=Number(this.mtSettings.maxNbPostFetch)}#m(){return new Promise((t=>{this.linkHeader.next?this.#l(this.linkHeader.next,!0).then((e=>{this.fetchedData.timeline=[...this.fetchedData.timeline,...e],t()})):t()}))}#h(t){const e=t.split(", ").map((t=>t.split("; "))).map((t=>[t[1].replace(/"/g,"").replace("rel=",""),t[0].slice(1,-1)]));return Object.fromEntries(e)}async#a(t){await this.#o();const e=this.fetchedData.timeline;let i=0;if(this.mtBodyNode.replaceChildren(),e.forEach((t=>{const e="public"===t.visibility||!this.mtSettings.hideUnlisted&&"unlisted"===t.visibility,s=this.mtSettings.hideReblog&&t.reblog,a=this.mtSettings.hideReplies&&t.in_reply_to_id;!e||s||a||i${e?.length||0} posts have been fetched from the serverThis may be due to an incorrect configuration with the parameters or with the filters applied (to hide certains type of posts)`;this.#n(t,"📭")}}#g(){"0"!==this.mtSettings.txtMaxLines&&0!==this.mtSettings.txtMaxLines.length&&this.mtBodyNode.parentNode.style.setProperty("--mt-txt-max-lines",this.mtSettings.txtMaxLines),"0"!==this.mtSettings.previewMaxLines&&0!==this.mtSettings.previewMaxLines.length&&this.mtBodyNode.parentNode.style.setProperty("--mt-preview-max-lines",this.mtSettings.previewMaxLines)}#u(t){const e=this.mtBodyNode.getElementsByTagName("article");for(let i=0;i
"}let g=[];if(t.media_attachments.length>0)for(let e in t.media_attachments)g.push(this.#L(t.media_attachments[e],t.sensitive));if(t.reblog&&t.reblog.media_attachments.length>0)for(let e in t.reblog.media_attachments)g.push(this.#L(t.reblog.media_attachments[e],t.reblog.sensitive));g=`
${g.join("")}
`;let u="";!this.mtSettings.hidePreviewLink&&t.card&&(u=this.#N(t.card));let v="";if(t.poll){let e="";for(let i in t.poll.options)e+="
`,this.mtBodyNode.setAttribute("role","none"),new Error("Stopping the script due to an error building the timeline.")}}}));
+ */t.Init=class{constructor(t={}){this.defaultSettings={mtContainerId:"mt-container",instanceUrl:"https://mastodon.social",timelineType:"local",userId:"",profileName:"",hashtagName:"",spinnerClass:"mt-loading-spinner",defaultTheme:"auto",maxNbPostFetch:"20",maxNbPostShow:"20",dateLocale:"en-GB",dateOptions:{day:"2-digit",month:"short",year:"numeric"},hideUnlisted:!1,hideReblog:!1,hideReplies:!1,hidePinnedPosts:!1,hideUserAccount:!1,txtMaxLines:"",btnShowMore:"SHOW MORE",btnShowLess:"SHOW LESS",markdownBlockquote:!1,hideEmojos:!1,btnShowContent:"SHOW CONTENT",hideVideoPreview:!1,btnPlayVideoTxt:"Load and play video",hidePreviewLink:!1,previewMaxLines:"",hideCounterBar:!1,disableCarousel:!1,carouselCloseTxt:"Close carousel",carouselPrevTxt:"Previous media item",carouselNextTxt:"Next media item",btnSeeMore:"See more posts at Mastodon",btnReload:"Refresh",insistSearchContainer:!1,insistSearchContainerTime:"3000"},this.mtSettings={...this.defaultSettings,...t},this.#t(),this.linkHeader={},this.mtContainerNode="",this.mtBodyNode="",this.fetchedData={},this.#e((()=>{this.#i()}))}#t(){Number(this.mtSettings.maxNbPostShow)>Number(this.mtSettings.maxNbPostFetch)&&(console.error(`Please check your settings! The maximum number of posts to show is bigger than the maximum number of posts to fetch. Changing the value of "maxNbPostFetch" to: ${this.mtSettings.maxNbPostShow}`),this.mtSettings.maxNbPostFetch=this.mtSettings.maxNbPostShow)}#e(t){"undefined"!=typeof document&&"complete"===document.readyState?t():"undefined"!=typeof document&&"complete"!==document.readyState&&document.addEventListener("DOMContentLoaded",t())}#i(){const t=()=>{this.mtContainerNode=document.getElementById(this.mtSettings.mtContainerId),this.mtBodyNode=this.mtContainerNode.getElementsByClassName("mt-body")[0],this.#s(),this.#a("newTimeline")};if(this.mtSettings.insistSearchContainer){const e=performance.now(),i=()=>{if(document.getElementById(this.mtSettings.mtContainerId))t();else{performance.now()-e container with id: "${this.mtSettings.mtContainerId}" after several attempts for ${this.mtSettings.insistSearchContainerTime/1e3} seconds`)}};i()}else document.getElementById(this.mtSettings.mtContainerId)?t():console.error(`Impossible to find the
container with id: "${this.mtSettings.mtContainerId}". Please try to add the option 'insistSearchContainer: true' when initializing the script`)}mtUpdate(){this.#e((()=>{this.mtBodyNode.replaceChildren(),this.mtBodyNode.insertAdjacentHTML("afterbegin",''),this.#a("updateTimeline")}))}mtColorTheme(t){this.#e((()=>{this.mtContainerNode.setAttribute("data-theme",t)}))}#s(){if("auto"===this.mtSettings.defaultTheme){let t=window.matchMedia("(prefers-color-scheme: dark)");t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light"),t.addEventListener("change",(t=>{t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light")}))}else this.mtColorTheme(this.mtSettings.defaultTheme)}#o(){return new Promise(((t,e)=>{const i=this.mtSettings.instanceUrl?`${this.mtSettings.instanceUrl}/api/v1/`:this.#n("Please check your instanceUrl value","⚠️"),s=this.#r(i),a=Object.entries(s).map((([t,i])=>{const s="timeline"===t;return this.#l(i,s).then((e=>({[t]:e}))).catch((s=>(e(new Error(`Something went wrong fetching data from: ${i}`)),this.#n(s.message),{[t]:[]})))}));Promise.all(a).then((async e=>{if(this.fetchedData=e.reduce(((t,e)=>({...t,...e})),{}),!this.mtSettings.hidePinnedPosts&&void 0!==this.fetchedData.pinned?.length&&0!==this.fetchedData.pinned.length){const t=this.fetchedData.pinned.map((t=>({...t,pinned:!0})));this.fetchedData.timeline=[...t,...this.fetchedData.timeline]}if(this.#d())t();else{do{await this.#m()}while(!this.#d()&&this.linkHeader.next);t()}}))}))}#r(t){let e={};return"profile"===this.mtSettings.timelineType?this.mtSettings.userId?(e.timeline=`${t}accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`,this.mtSettings.hidePinnedPosts||(e.pinned=`${t}accounts/${this.mtSettings.userId}/statuses?pinned=true`)):this.#n("Please check your userId value","⚠️"):"hashtag"===this.mtSettings.timelineType?this.mtSettings.hashtagName?e.timeline=`${t}timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`:this.#n("Please check your hashtagName value","⚠️"):"local"===this.mtSettings.timelineType?e.timeline=`${t}timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`:this.#n("Please check your timelineType value","⚠️"),this.mtSettings.hideEmojos||(e.emojos=`${t}custom_emojis`),e}async#l(t,e=!1){const i=await fetch(t);if(!i.ok)throw new Error(`\n Failed to fetch the following Url: ${t}Error status: ${i.status}Error message: ${i.statusText}\n `);const s=await i.json();return e&&i.headers.get("Link")&&(this.linkHeader=this.#h(i.headers.get("Link"))),s}#d(){return this.fetchedData.timeline.length>=Number(this.mtSettings.maxNbPostFetch)}#m(){return new Promise((t=>{this.linkHeader.next?this.#l(this.linkHeader.next,!0).then((e=>{this.fetchedData.timeline=[...this.fetchedData.timeline,...e],t()})):t()}))}#h(t){const e=t.split(", ").map((t=>t.split("; "))).map((t=>[t[1].replace(/"/g,"").replace("rel=",""),t[0].slice(1,-1)]));return Object.fromEntries(e)}async#a(t){await this.#o();const e=this.fetchedData.timeline;let i=0;if(this.mtBodyNode.replaceChildren(),e.forEach((t=>{const e="public"===t.visibility||!this.mtSettings.hideUnlisted&&"unlisted"===t.visibility,s=this.mtSettings.hideReblog&&t.reblog,a=this.mtSettings.hideReplies&&t.in_reply_to_id;!e||s||a||i${e?.length||0} posts have been fetched from the serverThis may be due to an incorrect configuration with the parameters or with the filters applied (to hide certains type of posts)`;this.#n(t,"📭")}}#g(){"0"!==this.mtSettings.txtMaxLines&&0!==this.mtSettings.txtMaxLines.length&&this.mtBodyNode.parentNode.style.setProperty("--mt-txt-max-lines",this.mtSettings.txtMaxLines),"0"!==this.mtSettings.previewMaxLines&&0!==this.mtSettings.previewMaxLines.length&&this.mtBodyNode.parentNode.style.setProperty("--mt-preview-max-lines",this.mtSettings.previewMaxLines)}#u(t){const e=this.mtBodyNode.getElementsByTagName("article");for(let i=0;i
"}let g=[];if(t.media_attachments.length>0)for(let e in t.media_attachments)g.push(this.#L(t.media_attachments[e],t.sensitive));if(t.reblog&&t.reblog.media_attachments.length>0)for(let e in t.reblog.media_attachments)g.push(this.#L(t.reblog.media_attachments[e],t.reblog.sensitive));g=`
${g.join("")}
`;let u="";!this.mtSettings.hidePreviewLink&&t.card&&(u=this.#T(t.card));let v="";if(t.poll){let e="";for(let i in t.poll.options)e+="
";
}
- // Add all to main post container
+ // Put all elements together in the post container
const post =
' elements
+ * @param {Node} html The HTML
+ */
+ function removeScripts(html) {
+ let scripts = html.querySelectorAll("script");
+ for (let script of scripts) {
+ script.remove();
+ }
+ }
+
+ /**
+ * Check if the attribute is potentially dangerous
+ * @param {String} name The attribute name
+ * @param {String} value The attribute value
+ * @return {Boolean} If true, the attribute is potentially dangerous
+ */
+ function isPossiblyDangerous(name, value) {
+ let val = value.replace(/\s+/g, "").toLowerCase();
+ if (["src", "href", "xlink:href"].includes(name)) {
+ if (val.includes("javascript:") || val.includes("data:")) return true;
+ }
+ if (name.startsWith("on")) return true;
+ }
+
+ /**
+ * Remove potentially dangerous attributes from an element
+ * @param {Node} elem The element
+ */
+ function removeAttributes(elem) {
+ // Loop through each attribute
+ // If it's dangerous, remove it
+ let atts = elem.attributes;
+ for (let { name, value } of atts) {
+ if (!isPossiblyDangerous(name, value)) continue;
+ elem.removeAttribute(name);
+ }
+ }
+
+ /**
+ * Remove dangerous stuff from the HTML document's nodes
+ * @param {Node} html The HTML document
+ */
+ function clean(html) {
+ let nodes = html.children;
+ for (let node of nodes) {
+ removeAttributes(node);
+ clean(node);
+ }
+ }
+
+ // Convert the string to HTML
+ let html = stringToHTML();
+
+ // Sanitize it
+ removeScripts(html);
+ clean(html);
+
+ // If the user wants HTML nodes back, return them
+ // Otherwise, pass a sanitized string back
+ return n ? html.childNodes : html.innerHTML;
+ }
+
/**
* Handle text changes made to posts
- * @param {string} c Text content
- * @returns {string} Text content modified
+ * @param {String} c Text content
+ * @returns {String} Text content modified
*/
#formatPostText(c) {
let content = c;
+ // Sanitize string
+ content = this.#cleanHTML(content, false);
+
// Format hashtags and mentions
content = this.#addTarget2hashtagMention(content);
@@ -811,8 +896,8 @@ export class Init {
/**
* Add target="_blank" to all #hashtags and @mentions in the post
- * @param {string} c Text content
- * @returns {string} Text content modified
+ * @param {String} c Text content
+ * @returns {String} Text content modified
*/
#addTarget2hashtagMention(c) {
let content = c.replaceAll('rel="tag"', 'rel="tag" target="_blank"');
@@ -826,12 +911,12 @@ export class Init {
/**
* Find all start/end and replace them by another start/end
- * @param {string} c Text content
- * @param {string} initialTagOpen Start HTML tag to replace
- * @param {string} initialTagClose End HTML tag to replace
- * @param {string} replacedTagOpen New start HTML tag
- * @param {string} replacedTagClose New end HTML tag
- * @returns {string} Text in HTML format
+ * @param {String} c Text content
+ * @param {String} initialTagOpen Start HTML tag to replace
+ * @param {String} initialTagClose End HTML tag to replace
+ * @param {String} replacedTagOpen New start HTML tag
+ * @param {String} replacedTagClose New end HTML tag
+ * @returns {String} Text in HTML format
*/
#replaceHTMLtag(
c,
@@ -855,8 +940,8 @@ export class Init {
/**
* Escape quotes and other special characters, to make them safe to add
* to HTML content and attributes as plain text
- * @param {string} s String
- * @returns {string} String
+ * @param {String} s String
+ * @returns {String} String
*/
#escapeHTML(s) {
return (s ?? "")
@@ -869,9 +954,9 @@ export class Init {
/**
* Find all custom emojis shortcode and replace by image
- * @param {string} c Text content
- * @param {array} e List with all custom emojis
- * @returns {string} Text content modified
+ * @param {String} c Text content
+ * @param {Array} e List with all custom emojis
+ * @returns {String} Text content modified
*/
#shortcode2Emojos(c, e) {
if (c.includes(":")) {
@@ -891,8 +976,8 @@ export class Init {
/**
* Format date
- * @param {string} d Date in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ)
- * @returns {string} Date formated
+ * @param {String} d Date in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ)
+ * @returns {String} Date formated
*/
#formatDate(d) {
const originalDate = new Date(d);
@@ -907,9 +992,9 @@ export class Init {
/**
* Create media element
- * @param {object} m Media content
- * @param {boolean} s Sensitive/spoiler status
- * @returns {string} Media in HTML format
+ * @param {Object} m Media content
+ * @param {Boolean} s Sensitive/spoiler status
+ * @returns {String} Media in HTML format
*/
#createMedia(m, s = false) {
const type = m.type;
@@ -1019,7 +1104,9 @@ export class Init {
'" alt="' +
(m.description ? this.#escapeHTML(m.description) : "") +
'" loading="lazy" />' +
- '' +
+ '' +
"
";
} else {
media =
@@ -1051,8 +1138,8 @@ export class Init {
/**
* Open a dialog/modal with the styles of Mastodon timeline
- * @param {string} i Dialog Id name
- * @param {string} c Dialog HTML content
+ * @param {String} i Dialog Id name
+ * @param {String} c Dialog HTML content
*/
#openDialog(i, c) {
let dialog = document.createElement("dialog");
@@ -1069,7 +1156,7 @@ export class Init {
/**
* Build a carousel/lightbox with the media content in the post clicked
- * @param {event} e User interaction trigger
+ * @param {Event} e User interaction trigger
*/
#showCarousel(e) {
// List all medias in the post and remove sensitive/spoiler medias
@@ -1181,8 +1268,8 @@ export class Init {
/**
* Add interactions for the carousel
- * @param {number} t Total number of medias loaded
- * @param {number} m Index position of media clicked by user
+ * @param {Number} t Total number of medias loaded
+ * @param {Number} m Index position of media clicked by user
*/
#setCarouselInteractions(t, m) {
let currentMediaIndex = m;
@@ -1270,7 +1357,7 @@ export class Init {
/**
* Replace the video preview image by the video player
- * @param {event} e User interaction trigger
+ * @param {Event} e User interaction trigger
*/
#loadPostVideo(e) {
const parentNode = e.target.closest("[data-media-type]");
@@ -1281,7 +1368,7 @@ export class Init {
/**
* Spoiler toggle for text
- * @param {event} e User interaction trigger
+ * @param {Event} e User interaction trigger
*/
#toogleTxtSpoiler(e) {
const target = e.target;
@@ -1302,7 +1389,7 @@ export class Init {
/**
* Spoiler toggle for image/video
- * @param {event} e User interaction trigger
+ * @param {Event} e User interaction trigger
*/
#toogleMediaSpoiler(e) {
const target = e.target;
@@ -1315,8 +1402,8 @@ export class Init {
/**
* Create preview link
- * @param {object} c Preview link content
- * @returns {string} Preview link in HTML format
+ * @param {Object} c Preview link content
+ * @returns {String} Preview link in HTML format
*/
#createPreviewLink(c) {
let previewDescription = "";
@@ -1369,8 +1456,8 @@ export class Init {
/**
* Parse HTML string
- * @param {string} s HTML string
- * @returns {string} Plain text
+ * @param {String} s HTML string
+ * @returns {String} Plain text
*/
#parseHTMLstring(s) {
const parser = new DOMParser();
@@ -1443,6 +1530,7 @@ export class Init {
*/
#addPostListener() {
this.mtBodyNode.addEventListener("click", (e) => {
+ console.log("click on: ", e);
const target = e.target;
const localName = target.localName;
const parentNode = target.parentNode;
@@ -1487,6 +1575,7 @@ export class Init {
(parentNode.getAttribute("data-media-type") === "video" ||
parentNode.getAttribute("data-media-type") === "gifv"))
) {
+ console.log("loadPostVideo");
this.#loadPostVideo(e);
}
});
@@ -1501,7 +1590,7 @@ export class Init {
/**
* Open post in a new tab/page avoiding any other natural link
- * @param {event} e User interaction trigger
+ * @param {Event} e User interaction trigger
*/
#openPostUrl(e) {
const urlPost = e.target.closest(".mt-post").dataset.location;
@@ -1545,8 +1634,8 @@ export class Init {
/**
* Show an error on the timeline
- * @param {string} e Error message
- * @param {string} i Icon
+ * @param {String} e Error message
+ * @param {String} i Icon
*/
#showError(t, i) {
const icon = i || "❌";