@schukai/monster 4.150.8 → 4.150.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","files":["source","test/cases","test/util","CHANGELOG.md"],"homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.150.8"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","files":["source","test/cases","test/util","CHANGELOG.md"],"homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.150.9"}
@@ -10,4 +10,49 @@
10
10
 
11
11
  [data-monster-role="viewer"] {
12
12
  height: inherit;
13
- }
13
+ }
14
+
15
+ [data-monster-role="external-link-confirmation"] {
16
+ position: fixed;
17
+ z-index: var(--monster-z-index-popover, 800);
18
+ display: grid;
19
+ gap: var(--monster-space-3, .4rem);
20
+ width: min(28rem, calc(100vw - 2rem));
21
+ max-height: calc(100vh - 2rem);
22
+ overflow: auto;
23
+ box-sizing: border-box;
24
+ padding: var(--monster-space-5, 1rem);
25
+ color: var(--monster-color-primary-1);
26
+ background: var(--monster-bg-color-primary-1);
27
+ border: var(--monster-border-width) var(--monster-border-style) var(--monster-bg-color-primary-4);
28
+ border-radius: var(--monster-border-radius);
29
+ box-shadow: var(--monster-box-shadow-2);
30
+
31
+ &[hidden] {
32
+ display: none;
33
+ }
34
+
35
+ & p {
36
+ margin: 0;
37
+ }
38
+
39
+ & code {
40
+ display: block;
41
+ max-width: 100%;
42
+ padding: var(--monster-space-3, .4rem);
43
+ overflow-wrap: anywhere;
44
+ user-select: all;
45
+ background: var(--monster-bg-color-primary-2);
46
+ }
47
+ }
48
+
49
+ [data-monster-role="external-link-actions"] {
50
+ display: flex;
51
+ flex-wrap: wrap;
52
+ gap: var(--monster-space-3, .4rem);
53
+
54
+ & > button {
55
+ width: auto;
56
+ flex: 1 1 auto;
57
+ }
58
+ }
@@ -85,7 +85,7 @@ try {
85
85
  --monster-button-background-color,var(--monster-color-tertiary-4)
86
86
  );border-color:var(
87
87
  --monster-button-border-color,var(--monster-bg-color-tertiary-4)
88
- )}button:hover{transition:background .8s,color .25s .0833333333s;box-shadow:var(--monster-box-shadow-2)}button:disabled{cursor:not-allowed}button:active{transition:background .8s,color .25s .0833333333s;box-shadow:var(--monster-box-shadow-2);z-index:var(--monster-z-index-outline)}.monster-button-bar,.monster-button-group{display:flex;justify-content:space-between;align-content:center;flex-wrap:nowrap;flex-direction:row;align-items:stretch}.monster-button-group{box-sizing:border-box;gap:0;margin:1rem 0}.monster-button-group>:not(:last-child){margin-right:calc(var(--monster-border-width)*-1)}.monster-button-group :hover{box-shadow:none}button:focus-visible{outline:var(--monster-focus-ring-width,2px) var(--monster-focus-ring-style,solid) var(--monster-focus-ring-color,currentColor);outline-offset:var(--monster-focus-ring-offset,2px);z-index:var(--monster-z-index-outline)}[data-monster-role=control]{outline:none;width:100%;box-sizing:border-box}[data-monster-role=control].flex{display:flex;flex-direction:row;align-items:center}:host{display:block;box-sizing:border-box}:after,:before,:root{--monster-font-family:-apple-system,BlinkMacSystemFont,\"Quicksand\",\"Segoe UI\",\"Roboto\",\"Oxygen\",\"Ubuntu\",\"Cantarell\",\"Fira Sans\",\"Droid Sans\",\"Helvetica Neue\",Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\";--monster-font-family-monospace:\"Consolas\",\"Courier New\",\"Roboto Mono\",\"Source Code Pro\",\"Fira Mono\",monospace;--monster-font-size-min:15px;--monster-font-size-base:16px;--monster-font-size-max:16px;--monster-font-scale:1;--monster-font-scale-hi-dpi:1.25;--monster-font-scale-xhi-dpi:1.25;--monster-font-size-fluid:clamp(var(--monster-font-size-min),calc(var(--monster-font-size-min) + 0.1vw),var(--monster-font-size-max))}@media (min-resolution:1.5dppx){:after,:before,:root{--monster-font-scale:var(--monster-font-scale-hi-dpi,1.1)}}@media (min-resolution:3dppx){:after,:before,:root{--monster-font-scale:var(--monster-font-scale-xhi-dpi,1.18)}}:after,:before,:root{--monster-color-primary-1:var(--monster-color-gray-6);--monster-color-primary-2:var(--monster-color-gray-6);--monster-color-primary-3:var(--monster-color-cinnamon-1);--monster-color-primary-4:var(--monster-color-cinnamon-1);--monster-bg-color-primary-1:var(--monster-color-gray-1);--monster-bg-color-primary-2:var(--monster-color-gray-2);--monster-bg-color-primary-3:var(--monster-color-gray-6);--monster-bg-color-primary-4:var(--monster-color-gray-4)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-primary-1:var(--monster-color-gray-1);--monster-color-primary-2:var(--monster-color-gray-1);--monster-color-primary-3:var(--monster-color-gray-6);--monster-color-primary-4:var(--monster-color-gray-6);--monster-bg-color-primary-1:var(--monster-color-gray-6);--monster-bg-color-primary-2:var(--monster-color-gray-3);--monster-bg-color-primary-3:var(--monster-color-gray-2);--monster-bg-color-primary-4:var(--monster-color-gray-1)}}:after,:before,:root{--monster-color-secondary-1:var(--monster-color-red-4);--monster-color-secondary-2:var(--monster-color-red-4);--monster-color-secondary-3:var(--monster-color-red-1);--monster-color-secondary-4:var(--monster-color-red-1);--monster-bg-color-secondary-1:var(--monster-color-gray-1);--monster-bg-color-secondary-2:var(--monster-color-red-2);--monster-bg-color-secondary-3:var(--monster-color-red-3);--monster-bg-color-secondary-4:var(--monster-color-red-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-secondary-1:var(--monster-color-red-1);--monster-color-secondary-2:var(--monster-color-red-1);--monster-color-secondary-3:var(--monster-color-red-6);--monster-color-secondary-4:var(--monster-color-red-4);--monster-bg-color-secondary-1:var(--monster-color-gray-6);--monster-bg-color-secondary-2:var(--monster-color-red-3);--monster-bg-color-secondary-3:var(--monster-color-red-2);--monster-bg-color-secondary-4:var(--monster-color-red-1)}}:after,:before,:root{--monster-color-tertiary-1:var(--monster-color-magenta-4);--monster-color-tertiary-2:var(--monster-color-magenta-4);--monster-color-tertiary-3:var(--monster-color-magenta-6);--monster-color-tertiary-4:var(--monster-color-magenta-1);--monster-bg-color-tertiary-1:var(--monster-color-gray-1);--monster-bg-color-tertiary-2:var(--monster-color-magenta-1);--monster-bg-color-tertiary-3:var(--monster-color-magenta-2);--monster-bg-color-tertiary-4:var(--monster-color-magenta-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-tertiary-1:var(--monster-color-magenta-1);--monster-color-tertiary-2:var(--monster-color-magenta-6);--monster-color-tertiary-3:var(--monster-color-magenta-4);--monster-color-tertiary-4:var(--monster-color-magenta-4);--monster-bg-color-tertiary-1:var(--monster-color-gray-6);--monster-bg-color-tertiary-2:var(--monster-color-magenta-2);--monster-bg-color-tertiary-3:var(--monster-color-magenta-1);--monster-bg-color-tertiary-4:var(--monster-color-magenta-1)}}:after,:before,:root{--monster-color-destructive-1:var(--monster-color-red-1);--monster-color-destructive-2:var(--monster-color-red-4);--monster-color-destructive-3:var(--monster-color-red-6);--monster-color-destructive-4:var(--monster-color-red-1);--monster-bg-color-destructive-1:var(--monster-color-red-4);--monster-bg-color-destructive-2:var(--monster-color-gray-1);--monster-bg-color-destructive-3:var(--monster-color-red-2);--monster-bg-color-destructive-4:var(--monster-color-red-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-destructive-1:var(--monster-color-red-1);--monster-color-destructive-2:var(--monster-color-red-3);--monster-color-destructive-3:var(--monster-color-red-4);--monster-color-destructive-4:var(--monster-color-red-1);--monster-bg-color-destructive-1:var(--monster-color-red-5);--monster-bg-color-destructive-2:var(--monster-color-gray-6);--monster-bg-color-destructive-3:var(--monster-color-red-1);--monster-bg-color-destructive-4:var(--monster-color-red-4)}}:after,:before,:root{--monster-color-danger-1:var(--monster-color-raspberry-1);--monster-color-danger-2:var(--monster-color-raspberry-4);--monster-color-danger-3:var(--monster-color-raspberry-6);--monster-color-danger-4:var(--monster-color-raspberry-1);--monster-bg-color-danger-1:var(--monster-color-raspberry-4);--monster-bg-color-danger-2:var(--monster-color-gray-1);--monster-bg-color-danger-3:var(--monster-color-raspberry-2);--monster-bg-color-danger-4:var(--monster-color-raspberry-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-danger-1:var(--monster-color-raspberry-1);--monster-color-danger-2:var(--monster-color-raspberry-3);--monster-color-danger-3:var(--monster-color-raspberry-4);--monster-color-danger-4:var(--monster-color-raspberry-1);--monster-bg-color-danger-1:var(--monster-color-raspberry-5);--monster-bg-color-danger-2:var(--monster-color-gray-6);--monster-bg-color-danger-3:var(--monster-color-raspberry-1);--monster-bg-color-danger-4:var(--monster-color-raspberry-4)}}:after,:before,:root{--monster-color-success-1:var(--monster-color-green-1);--monster-color-success-2:var(--monster-color-green-4);--monster-color-success-3:var(--monster-color-green-6);--monster-color-success-4:var(--monster-color-green-1);--monster-bg-color-success-1:var(--monster-color-green-3);--monster-bg-color-success-2:var(--monster-color-gray-1);--monster-bg-color-success-3:var(--monster-color-green-2);--monster-bg-color-success-4:var(--monster-color-green-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-success-1:var(--monster-color-green-1);--monster-color-success-2:var(--monster-color-green-2);--monster-color-success-3:var(--monster-color-green-4);--monster-color-success-4:var(--monster-color-green-1);--monster-bg-color-success-1:var(--monster-color-green-5);--monster-bg-color-success-2:var(--monster-color-gray-6);--monster-bg-color-success-3:var(--monster-color-green-1);--monster-bg-color-success-4:var(--monster-color-green-3)}}:after,:before,:root{--monster-color-warning-1:var(--monster-color-orange-1);--monster-color-warning-2:var(--monster-color-orange-4);--monster-color-warning-3:var(--monster-color-orange-6);--monster-color-warning-4:var(--monster-color-orange-1);--monster-bg-color-warning-1:var(--monster-color-orange-3);--monster-bg-color-warning-2:var(--monster-color-gray-1);--monster-bg-color-warning-3:var(--monster-color-orange-2);--monster-bg-color-warning-4:var(--monster-color-orange-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-warning-1:var(--monster-color-orange-1);--monster-color-warning-2:var(--monster-color-orange-3);--monster-color-warning-3:var(--monster-color-orange-4);--monster-color-warning-4:var(--monster-color-orange-1);--monster-bg-color-warning-1:var(--monster-color-orange-5);--monster-bg-color-warning-2:var(--monster-color-gray-6);--monster-bg-color-warning-3:var(--monster-color-orange-1);--monster-bg-color-warning-4:var(--monster-color-orange-3)}}:after,:before,:root{--monster-color-error-1:var(--monster-color-red-1);--monster-color-error-2:var(--monster-color-red-4);--monster-color-error-3:var(--monster-color-red-6);--monster-color-error-4:var(--monster-color-red-1);--monster-bg-color-error-1:var(--monster-color-red-4);--monster-bg-color-error-2:var(--monster-color-gray-1);--monster-bg-color-error-3:var(--monster-color-red-2);--monster-bg-color-error-4:var(--monster-color-red-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-error-1:var(--monster-color-red-1);--monster-color-error-2:var(--monster-color-red-3);--monster-color-error-3:var(--monster-color-red-4);--monster-color-error-4:var(--monster-color-red-1);--monster-bg-color-error-1:var(--monster-color-red-5);--monster-bg-color-error-2:var(--monster-color-gray-6);--monster-bg-color-error-3:var(--monster-color-red-1);--monster-bg-color-error-4:var(--monster-color-red-4)}}:after,:before,:root{--monster-color-selection-1:var(--monster-color-gray-6);--monster-color-selection-2:var(--monster-color-gray-6);--monster-color-selection-3:var(--monster-color-gray-6);--monster-color-selection-4:var(--monster-color-gray-1);--monster-bg-color-selection-1:var(--monster-color-yellow-2);--monster-bg-color-selection-2:var(--monster-color-yellow-1);--monster-bg-color-selection-3:var(--monster-color-yellow-2);--monster-bg-color-selection-4:var(--monster-color-yellow-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-selection-1:var(--monster-color-gray-6);--monster-color-selection-2:var(--monster-color-gray-6);--monster-color-selection-3:var(--monster-color-gray-6);--monster-color-selection-4:var(--monster-color-gray-1);--monster-bg-color-selection-1:var(--monster-color-yellow-2);--monster-bg-color-selection-2:var(--monster-color-yellow-1);--monster-bg-color-selection-3:var(--monster-color-yellow-2);--monster-bg-color-selection-4:var(--monster-color-yellow-6)}}:after,:before,:root{--monster-color-primary-disabled-1:var(--monster-color-gray-4);--monster-color-primary-disabled-2:var(--monster-color-gray-4);--monster-color-primary-disabled-3:var(--monster-color-gray-4);--monster-color-primary-disabled-4:var(--monster-color-gray-4);--monster-bg-color-primary-disabled-1:var(--monster-color-gray-1);--monster-bg-color-primary-disabled-2:var(--monster-color-gray-2);--monster-bg-color-primary-disabled-3:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-4:var(--monster-color-gray-6);--monster-color-gradient-1:#833ab4;--monster-color-gradient-2:#fd1d1d;--monster-color-gradient-3:#fcb045}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-primary-disabled-1:var(--monster-color-gray-4);--monster-color-primary-disabled-2:var(--monster-color-gray-4);--monster-color-primary-disabled-3:var(--monster-color-gray-3);--monster-color-primary-disabled-4:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-1:var(--monster-color-gray-6);--monster-bg-color-primary-disabled-2:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-3:var(--monster-color-gray-2);--monster-bg-color-primary-disabled-4:var(--monster-color-gray-1);--monster-color-gradient-1:#ffe0b2;--monster-color-gradient-2:#ad8275;--monster-color-gradient-3:#771ba3}}:after,:before,:root{--monster-box-shadow-1:none;--monster-box-shadow-2:-1px 1px 10px 1px hsla(0,0%,76%,.61);--monster-text-shadow:none;--monster-theme-control-bg-color:var(--monster-color-seashell-1);--monster-theme-control-color:var(--monster-color-seashell-6);--monster-theme-control-hover-color:var(--monster-color-seashell-6);--monster-theme-control-hover-bg-color:var(--monster-color-seashell-2);--monster-theme-control-border-width:2px;--monster-theme-control-border-style:solid;--monster-theme-control-border-radius:0;--monster-theme-control-border-color:var(--monster-color-primary-1)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-theme-control-bg-color:var(--monster-color-gray-5);--monster-theme-control-color:var(--monster-color-gray-1);--monster-theme-control-border-color:var(--monster-color-gray-3);--monster-theme-control-hover-color:var(--monster-color-gray-1);--monster-theme-control-hover-bg-color:var(--monster-color-gray-6)}}:after,:before,:root{--monster-theme-on-color:var(--monster-color-green-1);--monster-theme-on-bg-color:var(--monster-color-green-5);--monster-theme-off-color:var(--monster-color-gray-1);--monster-theme-off-bg-color:var(--monster-color-gray-4)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-theme-on-color:var(--monster-color-gray-6);--monster-theme-on-bg-color:var(--monster-color-gray-1);--monster-theme-off-color:var(--monster-color-gray-1);--monster-theme-off-bg-color:var(--monster-color-gray-5)}}:after,:before,:root{--monster-border-style:solid;--monster-border-width:3px;--monster-border-radius:0;--monster-outline-width:1px;--monster-focus-ring-width:2px;--monster-focus-ring-style:solid;--monster-focus-ring-color:var(--monster-color-blue-4);--monster-focus-ring-offset:2px}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-focus-ring-color:var(--monster-color-yellow-2)}}:after,:before,:root{--monster-popper-witharrrow-distance:-4px;--monster-z-index-default:0;--monster-z-index-outline:10;--monster-z-index-dropdown:200;--monster-z-index-dropdown-overlay:210;--monster-z-index-sticky:300;--monster-z-index-sticky-overlay:310;--monster-z-index-fixed:400;--monster-z-index-fixed-overlay:410;--monster-z-index-modal-backdrop:500;--monster-z-index-modal-backdrop-overlay:510;--monster-z-index-offcanvas:600;--monster-z-index-offcanvas-overlay:610;--monster-z-index-modal:700;--monster-z-index-modal-overlay:710;--monster-z-index-popover:800;--monster-z-index-popover-overlay:810;--monster-z-index-tooltip:800;--monster-z-index-tooltip-overlay:910;--monster-space-0:0;--monster-space-1:2px;--monster-space-2:4px;--monster-space-3:6px;--monster-space-4:10px;--monster-space-5:16px;--monster-space-6:26px;--monster-space-7:42px;--monster-breakpoint-0:480px;--monster-breakpoint-4:480px;--monster-breakpoint-7:768px;--monster-breakpoint-9:992px;--monster-breakpoint-12:1200px;--monster-dragger-width:2px;--monster-dragger-handle-width:4px;--monster-dragger-handle-height:50px}a,a:active,a:focus-visible,a:hover,a:link,a:visited{color:var(--monster-color-secondary-1);text-decoration:none;outline:none;transition:color .3s ease-in-out,text-decoration-color .3s ease-in-out}a:active,a:focus-visible,a:hover{color:var(--monster-color-primary-2);text-decoration:underline;text-decoration-color:var(--monster-color-secondary-1 );text-decoration-thickness:1px;text-underline-offset:2px}a:focus-visible{outline:var(--monster-focus-ring-width,2px) var(--monster-focus-ring-style,solid) var(--monster-focus-ring-color,currentColor);outline-offset:var(--monster-focus-ring-offset,2px)}[data-monster-role=viewer]{height:inherit}
88
+ )}button:hover{transition:background .8s,color .25s .0833333333s;box-shadow:var(--monster-box-shadow-2)}button:disabled{cursor:not-allowed}button:active{transition:background .8s,color .25s .0833333333s;box-shadow:var(--monster-box-shadow-2);z-index:var(--monster-z-index-outline)}.monster-button-bar,.monster-button-group{display:flex;justify-content:space-between;align-content:center;flex-wrap:nowrap;flex-direction:row;align-items:stretch}.monster-button-group{box-sizing:border-box;gap:0;margin:1rem 0}.monster-button-group>:not(:last-child){margin-right:calc(var(--monster-border-width)*-1)}.monster-button-group :hover{box-shadow:none}button:focus-visible{outline:var(--monster-focus-ring-width,2px) var(--monster-focus-ring-style,solid) var(--monster-focus-ring-color,currentColor);outline-offset:var(--monster-focus-ring-offset,2px);z-index:var(--monster-z-index-outline)}[data-monster-role=control]{outline:none;width:100%;box-sizing:border-box}[data-monster-role=control].flex{display:flex;flex-direction:row;align-items:center}:host{display:block;box-sizing:border-box}:after,:before,:root{--monster-font-family:-apple-system,BlinkMacSystemFont,\"Quicksand\",\"Segoe UI\",\"Roboto\",\"Oxygen\",\"Ubuntu\",\"Cantarell\",\"Fira Sans\",\"Droid Sans\",\"Helvetica Neue\",Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\";--monster-font-family-monospace:\"Consolas\",\"Courier New\",\"Roboto Mono\",\"Source Code Pro\",\"Fira Mono\",monospace;--monster-font-size-min:15px;--monster-font-size-base:16px;--monster-font-size-max:16px;--monster-font-scale:1;--monster-font-scale-hi-dpi:1.25;--monster-font-scale-xhi-dpi:1.25;--monster-font-size-fluid:clamp(var(--monster-font-size-min),calc(var(--monster-font-size-min) + 0.1vw),var(--monster-font-size-max))}@media (min-resolution:1.5dppx){:after,:before,:root{--monster-font-scale:var(--monster-font-scale-hi-dpi,1.1)}}@media (min-resolution:3dppx){:after,:before,:root{--monster-font-scale:var(--monster-font-scale-xhi-dpi,1.18)}}:after,:before,:root{--monster-color-primary-1:var(--monster-color-gray-6);--monster-color-primary-2:var(--monster-color-gray-6);--monster-color-primary-3:var(--monster-color-cinnamon-1);--monster-color-primary-4:var(--monster-color-cinnamon-1);--monster-bg-color-primary-1:var(--monster-color-gray-1);--monster-bg-color-primary-2:var(--monster-color-gray-2);--monster-bg-color-primary-3:var(--monster-color-gray-6);--monster-bg-color-primary-4:var(--monster-color-gray-4)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-primary-1:var(--monster-color-gray-1);--monster-color-primary-2:var(--monster-color-gray-1);--monster-color-primary-3:var(--monster-color-gray-6);--monster-color-primary-4:var(--monster-color-gray-6);--monster-bg-color-primary-1:var(--monster-color-gray-6);--monster-bg-color-primary-2:var(--monster-color-gray-3);--monster-bg-color-primary-3:var(--monster-color-gray-2);--monster-bg-color-primary-4:var(--monster-color-gray-1)}}:after,:before,:root{--monster-color-secondary-1:var(--monster-color-red-4);--monster-color-secondary-2:var(--monster-color-red-4);--monster-color-secondary-3:var(--monster-color-red-1);--monster-color-secondary-4:var(--monster-color-red-1);--monster-bg-color-secondary-1:var(--monster-color-gray-1);--monster-bg-color-secondary-2:var(--monster-color-red-2);--monster-bg-color-secondary-3:var(--monster-color-red-3);--monster-bg-color-secondary-4:var(--monster-color-red-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-secondary-1:var(--monster-color-red-1);--monster-color-secondary-2:var(--monster-color-red-1);--monster-color-secondary-3:var(--monster-color-red-6);--monster-color-secondary-4:var(--monster-color-red-4);--monster-bg-color-secondary-1:var(--monster-color-gray-6);--monster-bg-color-secondary-2:var(--monster-color-red-3);--monster-bg-color-secondary-3:var(--monster-color-red-2);--monster-bg-color-secondary-4:var(--monster-color-red-1)}}:after,:before,:root{--monster-color-tertiary-1:var(--monster-color-magenta-4);--monster-color-tertiary-2:var(--monster-color-magenta-4);--monster-color-tertiary-3:var(--monster-color-magenta-6);--monster-color-tertiary-4:var(--monster-color-magenta-1);--monster-bg-color-tertiary-1:var(--monster-color-gray-1);--monster-bg-color-tertiary-2:var(--monster-color-magenta-1);--monster-bg-color-tertiary-3:var(--monster-color-magenta-2);--monster-bg-color-tertiary-4:var(--monster-color-magenta-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-tertiary-1:var(--monster-color-magenta-1);--monster-color-tertiary-2:var(--monster-color-magenta-6);--monster-color-tertiary-3:var(--monster-color-magenta-4);--monster-color-tertiary-4:var(--monster-color-magenta-4);--monster-bg-color-tertiary-1:var(--monster-color-gray-6);--monster-bg-color-tertiary-2:var(--monster-color-magenta-2);--monster-bg-color-tertiary-3:var(--monster-color-magenta-1);--monster-bg-color-tertiary-4:var(--monster-color-magenta-1)}}:after,:before,:root{--monster-color-destructive-1:var(--monster-color-red-1);--monster-color-destructive-2:var(--monster-color-red-4);--monster-color-destructive-3:var(--monster-color-red-6);--monster-color-destructive-4:var(--monster-color-red-1);--monster-bg-color-destructive-1:var(--monster-color-red-4);--monster-bg-color-destructive-2:var(--monster-color-gray-1);--monster-bg-color-destructive-3:var(--monster-color-red-2);--monster-bg-color-destructive-4:var(--monster-color-red-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-destructive-1:var(--monster-color-red-1);--monster-color-destructive-2:var(--monster-color-red-3);--monster-color-destructive-3:var(--monster-color-red-4);--monster-color-destructive-4:var(--monster-color-red-1);--monster-bg-color-destructive-1:var(--monster-color-red-5);--monster-bg-color-destructive-2:var(--monster-color-gray-6);--monster-bg-color-destructive-3:var(--monster-color-red-1);--monster-bg-color-destructive-4:var(--monster-color-red-4)}}:after,:before,:root{--monster-color-danger-1:var(--monster-color-raspberry-1);--monster-color-danger-2:var(--monster-color-raspberry-4);--monster-color-danger-3:var(--monster-color-raspberry-6);--monster-color-danger-4:var(--monster-color-raspberry-1);--monster-bg-color-danger-1:var(--monster-color-raspberry-4);--monster-bg-color-danger-2:var(--monster-color-gray-1);--monster-bg-color-danger-3:var(--monster-color-raspberry-2);--monster-bg-color-danger-4:var(--monster-color-raspberry-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-danger-1:var(--monster-color-raspberry-1);--monster-color-danger-2:var(--monster-color-raspberry-3);--monster-color-danger-3:var(--monster-color-raspberry-4);--monster-color-danger-4:var(--monster-color-raspberry-1);--monster-bg-color-danger-1:var(--monster-color-raspberry-5);--monster-bg-color-danger-2:var(--monster-color-gray-6);--monster-bg-color-danger-3:var(--monster-color-raspberry-1);--monster-bg-color-danger-4:var(--monster-color-raspberry-4)}}:after,:before,:root{--monster-color-success-1:var(--monster-color-green-1);--monster-color-success-2:var(--monster-color-green-4);--monster-color-success-3:var(--monster-color-green-6);--monster-color-success-4:var(--monster-color-green-1);--monster-bg-color-success-1:var(--monster-color-green-3);--monster-bg-color-success-2:var(--monster-color-gray-1);--monster-bg-color-success-3:var(--monster-color-green-2);--monster-bg-color-success-4:var(--monster-color-green-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-success-1:var(--monster-color-green-1);--monster-color-success-2:var(--monster-color-green-2);--monster-color-success-3:var(--monster-color-green-4);--monster-color-success-4:var(--monster-color-green-1);--monster-bg-color-success-1:var(--monster-color-green-5);--monster-bg-color-success-2:var(--monster-color-gray-6);--monster-bg-color-success-3:var(--monster-color-green-1);--monster-bg-color-success-4:var(--monster-color-green-3)}}:after,:before,:root{--monster-color-warning-1:var(--monster-color-orange-1);--monster-color-warning-2:var(--monster-color-orange-4);--monster-color-warning-3:var(--monster-color-orange-6);--monster-color-warning-4:var(--monster-color-orange-1);--monster-bg-color-warning-1:var(--monster-color-orange-3);--monster-bg-color-warning-2:var(--monster-color-gray-1);--monster-bg-color-warning-3:var(--monster-color-orange-2);--monster-bg-color-warning-4:var(--monster-color-orange-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-warning-1:var(--monster-color-orange-1);--monster-color-warning-2:var(--monster-color-orange-3);--monster-color-warning-3:var(--monster-color-orange-4);--monster-color-warning-4:var(--monster-color-orange-1);--monster-bg-color-warning-1:var(--monster-color-orange-5);--monster-bg-color-warning-2:var(--monster-color-gray-6);--monster-bg-color-warning-3:var(--monster-color-orange-1);--monster-bg-color-warning-4:var(--monster-color-orange-3)}}:after,:before,:root{--monster-color-error-1:var(--monster-color-red-1);--monster-color-error-2:var(--monster-color-red-4);--monster-color-error-3:var(--monster-color-red-6);--monster-color-error-4:var(--monster-color-red-1);--monster-bg-color-error-1:var(--monster-color-red-4);--monster-bg-color-error-2:var(--monster-color-gray-1);--monster-bg-color-error-3:var(--monster-color-red-2);--monster-bg-color-error-4:var(--monster-color-red-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-error-1:var(--monster-color-red-1);--monster-color-error-2:var(--monster-color-red-3);--monster-color-error-3:var(--monster-color-red-4);--monster-color-error-4:var(--monster-color-red-1);--monster-bg-color-error-1:var(--monster-color-red-5);--monster-bg-color-error-2:var(--monster-color-gray-6);--monster-bg-color-error-3:var(--monster-color-red-1);--monster-bg-color-error-4:var(--monster-color-red-4)}}:after,:before,:root{--monster-color-selection-1:var(--monster-color-gray-6);--monster-color-selection-2:var(--monster-color-gray-6);--monster-color-selection-3:var(--monster-color-gray-6);--monster-color-selection-4:var(--monster-color-gray-1);--monster-bg-color-selection-1:var(--monster-color-yellow-2);--monster-bg-color-selection-2:var(--monster-color-yellow-1);--monster-bg-color-selection-3:var(--monster-color-yellow-2);--monster-bg-color-selection-4:var(--monster-color-yellow-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-selection-1:var(--monster-color-gray-6);--monster-color-selection-2:var(--monster-color-gray-6);--monster-color-selection-3:var(--monster-color-gray-6);--monster-color-selection-4:var(--monster-color-gray-1);--monster-bg-color-selection-1:var(--monster-color-yellow-2);--monster-bg-color-selection-2:var(--monster-color-yellow-1);--monster-bg-color-selection-3:var(--monster-color-yellow-2);--monster-bg-color-selection-4:var(--monster-color-yellow-6)}}:after,:before,:root{--monster-color-primary-disabled-1:var(--monster-color-gray-4);--monster-color-primary-disabled-2:var(--monster-color-gray-4);--monster-color-primary-disabled-3:var(--monster-color-gray-4);--monster-color-primary-disabled-4:var(--monster-color-gray-4);--monster-bg-color-primary-disabled-1:var(--monster-color-gray-1);--monster-bg-color-primary-disabled-2:var(--monster-color-gray-2);--monster-bg-color-primary-disabled-3:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-4:var(--monster-color-gray-6);--monster-color-gradient-1:#833ab4;--monster-color-gradient-2:#fd1d1d;--monster-color-gradient-3:#fcb045}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-primary-disabled-1:var(--monster-color-gray-4);--monster-color-primary-disabled-2:var(--monster-color-gray-4);--monster-color-primary-disabled-3:var(--monster-color-gray-3);--monster-color-primary-disabled-4:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-1:var(--monster-color-gray-6);--monster-bg-color-primary-disabled-2:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-3:var(--monster-color-gray-2);--monster-bg-color-primary-disabled-4:var(--monster-color-gray-1);--monster-color-gradient-1:#ffe0b2;--monster-color-gradient-2:#ad8275;--monster-color-gradient-3:#771ba3}}:after,:before,:root{--monster-box-shadow-1:none;--monster-box-shadow-2:-1px 1px 10px 1px hsla(0,0%,76%,.61);--monster-text-shadow:none;--monster-theme-control-bg-color:var(--monster-color-seashell-1);--monster-theme-control-color:var(--monster-color-seashell-6);--monster-theme-control-hover-color:var(--monster-color-seashell-6);--monster-theme-control-hover-bg-color:var(--monster-color-seashell-2);--monster-theme-control-border-width:2px;--monster-theme-control-border-style:solid;--monster-theme-control-border-radius:0;--monster-theme-control-border-color:var(--monster-color-primary-1)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-theme-control-bg-color:var(--monster-color-gray-5);--monster-theme-control-color:var(--monster-color-gray-1);--monster-theme-control-border-color:var(--monster-color-gray-3);--monster-theme-control-hover-color:var(--monster-color-gray-1);--monster-theme-control-hover-bg-color:var(--monster-color-gray-6)}}:after,:before,:root{--monster-theme-on-color:var(--monster-color-green-1);--monster-theme-on-bg-color:var(--monster-color-green-5);--monster-theme-off-color:var(--monster-color-gray-1);--monster-theme-off-bg-color:var(--monster-color-gray-4)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-theme-on-color:var(--monster-color-gray-6);--monster-theme-on-bg-color:var(--monster-color-gray-1);--monster-theme-off-color:var(--monster-color-gray-1);--monster-theme-off-bg-color:var(--monster-color-gray-5)}}:after,:before,:root{--monster-border-style:solid;--monster-border-width:3px;--monster-border-radius:0;--monster-outline-width:1px;--monster-focus-ring-width:2px;--monster-focus-ring-style:solid;--monster-focus-ring-color:var(--monster-color-blue-4);--monster-focus-ring-offset:2px}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-focus-ring-color:var(--monster-color-yellow-2)}}:after,:before,:root{--monster-popper-witharrrow-distance:-4px;--monster-z-index-default:0;--monster-z-index-outline:10;--monster-z-index-dropdown:200;--monster-z-index-dropdown-overlay:210;--monster-z-index-sticky:300;--monster-z-index-sticky-overlay:310;--monster-z-index-fixed:400;--monster-z-index-fixed-overlay:410;--monster-z-index-modal-backdrop:500;--monster-z-index-modal-backdrop-overlay:510;--monster-z-index-offcanvas:600;--monster-z-index-offcanvas-overlay:610;--monster-z-index-modal:700;--monster-z-index-modal-overlay:710;--monster-z-index-popover:800;--monster-z-index-popover-overlay:810;--monster-z-index-tooltip:800;--monster-z-index-tooltip-overlay:910;--monster-space-0:0;--monster-space-1:2px;--monster-space-2:4px;--monster-space-3:6px;--monster-space-4:10px;--monster-space-5:16px;--monster-space-6:26px;--monster-space-7:42px;--monster-breakpoint-0:480px;--monster-breakpoint-4:480px;--monster-breakpoint-7:768px;--monster-breakpoint-9:992px;--monster-breakpoint-12:1200px;--monster-dragger-width:2px;--monster-dragger-handle-width:4px;--monster-dragger-handle-height:50px}a,a:active,a:focus-visible,a:hover,a:link,a:visited{color:var(--monster-color-secondary-1);text-decoration:none;outline:none;transition:color .3s ease-in-out,text-decoration-color .3s ease-in-out}a:active,a:focus-visible,a:hover{color:var(--monster-color-primary-2);text-decoration:underline;text-decoration-color:var(--monster-color-secondary-1 );text-decoration-thickness:1px;text-underline-offset:2px}a:focus-visible{outline:var(--monster-focus-ring-width,2px) var(--monster-focus-ring-style,solid) var(--monster-focus-ring-color,currentColor);outline-offset:var(--monster-focus-ring-offset,2px)}[data-monster-role=viewer]{height:inherit}[data-monster-role=external-link-confirmation]{position:fixed;z-index:var(--monster-z-index-popover,800);display:grid;gap:var(--monster-space-3,.4rem);width:min(28rem,calc(100vw - 2rem));max-height:calc(100vh - 2rem);overflow:auto;box-sizing:border-box;padding:var(--monster-space-5,1rem);color:var(--monster-color-primary-1);background:var(--monster-bg-color-primary-1);border:var(--monster-border-width) var(--monster-border-style) var(--monster-bg-color-primary-4);border-radius:var(--monster-border-radius);box-shadow:var(--monster-box-shadow-2)}[data-monster-role=external-link-confirmation][hidden]{display:none}[data-monster-role=external-link-confirmation] p{margin:0}[data-monster-role=external-link-confirmation] code{display:block;max-width:100%;padding:var(--monster-space-3,.4rem);overflow-wrap:anywhere;-webkit-user-select:all;-moz-user-select:all;user-select:all;background:var(--monster-bg-color-primary-2)}[data-monster-role=external-link-actions]{display:flex;flex-wrap:wrap;gap:var(--monster-space-3,.4rem)}[data-monster-role=external-link-actions]>button{width:auto;flex:1 1 auto}
89
89
  }`,
90
90
  0,
91
91
  );
@@ -20,7 +20,7 @@ import {
20
20
  updaterTransformerMethodsSymbol,
21
21
  } from "../../../dom/customelement.mjs";
22
22
  import { HtmlStyleSheet } from "./stylesheet/html.mjs";
23
- import { sanitizeHtml } from "../../../dom/sanitize-html.mjs";
23
+ import { sanitizeViewerHtml } from "./sanitize-html.mjs";
24
24
 
25
25
  export { HtmlContent };
26
26
 
@@ -74,7 +74,7 @@ class HtmlContent extends CustomElement {
74
74
  },
75
75
 
76
76
  sanitize: {
77
- callback: sanitizeHtml.bind(this),
77
+ callback: sanitizeViewerHtml.bind(this),
78
78
  },
79
79
  });
80
80
  }
@@ -20,7 +20,7 @@ import {
20
20
  updaterTransformerMethodsSymbol,
21
21
  } from "../../../dom/customelement.mjs";
22
22
 
23
- import { sanitizeHtml } from "../../../dom/sanitize-html.mjs";
23
+ import { sanitizeViewerHtml } from "./sanitize-html.mjs";
24
24
  import { MessageStyleSheet } from "./stylesheet/message.mjs";
25
25
  import { isArray, isObject } from "../../../types/is.mjs";
26
26
  import { findTargetElementFromEvent } from "../../../dom/events.mjs";
@@ -131,7 +131,7 @@ class MessageContent extends CustomElement {
131
131
  },
132
132
 
133
133
  sanitize: {
134
- callback: sanitizeHtml.bind(this),
134
+ callback: sanitizeViewerHtml.bind(this),
135
135
  },
136
136
 
137
137
  labels: getTranslations(),
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
3
+ * Node module: @schukai/monster
4
+ *
5
+ * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
6
+ * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
7
+ *
8
+ * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
9
+ * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
10
+ * For more information about purchasing a commercial license, please contact Volker Schukai.
11
+ *
12
+ * SPDX-License-Identifier: AGPL-3.0
13
+ */
14
+
15
+ import { sanitizeHtml } from "../../../dom/sanitize-html.mjs";
16
+
17
+ export { sanitizeViewerHtml };
18
+
19
+ const BLOCKED_HTML_RESOURCE_DATA_URL =
20
+ "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
21
+
22
+ const BLOCKED_VIEWER_TAGS = [
23
+ "script",
24
+ "iframe",
25
+ "object",
26
+ "embed",
27
+ "link",
28
+ "meta",
29
+ "form",
30
+ "button",
31
+ "input",
32
+ "select",
33
+ "textarea",
34
+ "option",
35
+ "optgroup",
36
+ "datalist",
37
+ "fieldset",
38
+ "output",
39
+ "keygen",
40
+ "details",
41
+ "summary",
42
+ "dialog",
43
+ "map",
44
+ "area",
45
+ ];
46
+
47
+ const SAFE_VIEWER_LINK_PROTOCOLS = new Set([
48
+ "http:",
49
+ "https:",
50
+ "mailto:",
51
+ "tel:",
52
+ ]);
53
+
54
+ /**
55
+ * Sanitizes untrusted HTML specifically for rendering inside Monster viewers.
56
+ *
57
+ * @param {string} html Untrusted viewer HTML.
58
+ * @returns {string} Sanitized viewer HTML.
59
+ */
60
+ function sanitizeViewerHtml(html) {
61
+ const sanitizedHtml = sanitizeHtml(html, {
62
+ blockedTags: BLOCKED_VIEWER_TAGS,
63
+ });
64
+ const parser = new DOMParser();
65
+ const doc = parser.parseFromString(sanitizedHtml, "text/html");
66
+
67
+ doc.querySelectorAll("*").forEach((element) => {
68
+ if (element.localName.includes("-")) {
69
+ element.remove();
70
+ return;
71
+ }
72
+
73
+ removeUnsafeLink(element);
74
+ element.removeAttribute("xlink:href");
75
+ element.removeAttribute("action");
76
+ element.removeAttribute("formaction");
77
+ element.removeAttribute("ping");
78
+ element.removeAttribute("attributionsrc");
79
+
80
+ removeCrossOriginResourceAttribute(element, "src");
81
+ removeCrossOriginResourceAttribute(element, "srcset");
82
+ removeCrossOriginResourceAttribute(element, "poster");
83
+
84
+ element.removeAttribute("autofocus");
85
+ element.removeAttribute("contenteditable");
86
+
87
+ if (element.hasAttribute("style")) {
88
+ const safeStyle = sanitizeStyleAttribute(element.getAttribute("style"));
89
+ if (safeStyle) {
90
+ element.setAttribute("style", safeStyle);
91
+ } else {
92
+ element.removeAttribute("style");
93
+ }
94
+ }
95
+ });
96
+
97
+ doc.querySelectorAll("style").forEach((element) => {
98
+ element.textContent = sanitizeStyleElement(element.textContent);
99
+ });
100
+
101
+ return doc.body.innerHTML;
102
+ }
103
+
104
+ function removeUnsafeLink(element) {
105
+ const value = element.getAttribute("href");
106
+ if (!value) {
107
+ return;
108
+ }
109
+ if (element.localName !== "a") {
110
+ element.removeAttribute("href");
111
+ return;
112
+ }
113
+
114
+ const normalizedValue = Array.from(value)
115
+ .filter((character) => {
116
+ const codePoint = character.codePointAt(0);
117
+ return codePoint > 0x20 && (codePoint < 0x7f || codePoint > 0x9f);
118
+ })
119
+ .join("");
120
+ const scheme = /^([a-z][a-z0-9+.-]*):/iu.exec(normalizedValue)?.[1];
121
+ if (scheme && !SAFE_VIEWER_LINK_PROTOCOLS.has(`${scheme.toLowerCase()}:`)) {
122
+ element.removeAttribute("href");
123
+ }
124
+ }
125
+
126
+ function removeCrossOriginResourceAttribute(element, attributeName) {
127
+ const value = element.getAttribute(attributeName);
128
+ if (!value) {
129
+ return;
130
+ }
131
+
132
+ if (attributeName === "srcset") {
133
+ const sameOriginEntries = value
134
+ .split(",")
135
+ .map((entry) => entry.trim())
136
+ .filter(Boolean)
137
+ .filter((entry) => {
138
+ const [candidateUrl] = entry.split(/\s+/, 1);
139
+ return isSameOriginResourceUrl(candidateUrl);
140
+ });
141
+
142
+ if (sameOriginEntries.length > 0) {
143
+ element.setAttribute(attributeName, sameOriginEntries.join(", "));
144
+ return;
145
+ }
146
+ } else if (isSameOriginResourceUrl(value)) {
147
+ return;
148
+ }
149
+
150
+ if (
151
+ attributeName === "src" &&
152
+ element.tagName.toLowerCase() === "img" &&
153
+ !element.classList.contains("privacyImage")
154
+ ) {
155
+ element.setAttribute("src", BLOCKED_HTML_RESOURCE_DATA_URL);
156
+ element.classList.add("privacyImage");
157
+ return;
158
+ }
159
+
160
+ element.removeAttribute(attributeName);
161
+ }
162
+
163
+ function sanitizeStyleAttribute(styleValue) {
164
+ return styleValue.replace(/url\(([^)]+)\)/giu, (match, rawUrl) => {
165
+ const cleanedUrl = rawUrl.trim().replace(/^["']|["']$/g, "");
166
+ return isSameOriginResourceUrl(cleanedUrl) ? match : "";
167
+ });
168
+ }
169
+
170
+ function sanitizeStyleElement(styleValue) {
171
+ return sanitizeStyleAttribute(styleValue).replace(/@import\s+[^;]+;?/giu, "");
172
+ }
173
+
174
+ function isSameOriginResourceUrl(value) {
175
+ if (!value) {
176
+ return true;
177
+ }
178
+
179
+ const trimmedValue = value.trim();
180
+ if (!trimmedValue || trimmedValue.startsWith("#")) {
181
+ return true;
182
+ }
183
+
184
+ try {
185
+ const url = new URL(trimmedValue, document.location.href);
186
+ return (
187
+ url.origin === document.location.origin ||
188
+ url.protocol === "data:" ||
189
+ url.protocol === "blob:"
190
+ );
191
+ } catch {
192
+ return true;
193
+ }
194
+ }
@@ -29,12 +29,16 @@ import "./viewer/message.mjs";
29
29
  import { getLocaleOfDocument } from "../../dom/locale.mjs";
30
30
  import { Button } from "../form/button.mjs";
31
31
  import { findTargetElementFromEvent } from "../../dom/events.mjs";
32
- import { sanitizeHtml } from "../../dom/sanitize-html.mjs";
32
+ import { sanitizeViewerHtml } from "./viewer/sanitize-html.mjs";
33
33
 
34
34
  export { Viewer };
35
35
 
36
- const BLOCKED_HTML_RESOURCE_DATA_URL =
37
- "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
36
+ const SAFE_VIEWER_LINK_PROTOCOLS = new Set([
37
+ "http:",
38
+ "https:",
39
+ "mailto:",
40
+ "tel:",
41
+ ]);
38
42
 
39
43
  /**
40
44
  * @private
@@ -42,6 +46,24 @@ const BLOCKED_HTML_RESOURCE_DATA_URL =
42
46
  */
43
47
  const viewerElementSymbol = Symbol("viewerElement");
44
48
 
49
+ /** @private @type {symbol} */
50
+ const linkConfirmationElementSymbol = Symbol("linkConfirmationElement");
51
+
52
+ /** @private @type {symbol} */
53
+ const linkHostElementSymbol = Symbol("linkHostElement");
54
+
55
+ /** @private @type {symbol} */
56
+ const linkUrlElementSymbol = Symbol("linkUrlElement");
57
+
58
+ /** @private @type {symbol} */
59
+ const pendingLinkSymbol = Symbol("pendingLink");
60
+
61
+ /** @private @type {symbol} */
62
+ const linkClickHandlerSymbol = Symbol("linkClickHandler");
63
+
64
+ /** @private @type {symbol} */
65
+ const linkKeydownHandlerSymbol = Symbol("linkKeydownHandler");
66
+
45
67
  /**
46
68
  * @private
47
69
  * @type {symbol}
@@ -74,6 +96,16 @@ class Viewer extends CustomElement {
74
96
  return Symbol.for("@schukai/monster/components/content/viewer@@instance");
75
97
  }
76
98
 
99
+ connectedCallback() {
100
+ super.connectedCallback();
101
+ if (this[linkClickHandlerSymbol]) {
102
+ this.addEventListener("click", this[linkClickHandlerSymbol], true);
103
+ }
104
+ if (this[linkKeydownHandlerSymbol]) {
105
+ this.addEventListener("keydown", this[linkKeydownHandlerSymbol], true);
106
+ }
107
+ }
108
+
77
109
  /**
78
110
  * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
79
111
  * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
@@ -127,6 +159,14 @@ class Viewer extends CustomElement {
127
159
  */
128
160
  disconnectedCallback() {
129
161
  super.disconnectedCallback?.();
162
+ if (this[linkClickHandlerSymbol]) {
163
+ this.removeEventListener("click", this[linkClickHandlerSymbol], true);
164
+ this[linkClickHandlerSymbol] = null;
165
+ }
166
+ if (this[linkKeydownHandlerSymbol]) {
167
+ this.removeEventListener("keydown", this[linkKeydownHandlerSymbol], true);
168
+ this[linkKeydownHandlerSymbol] = null;
169
+ }
130
170
  if (this[downloadHandlerSymbol]) {
131
171
  this.removeEventListener("click", this[downloadHandlerSymbol]);
132
172
  this[downloadHandlerSymbol] = null;
@@ -736,7 +776,7 @@ class Viewer extends CustomElement {
736
776
  if (data instanceof Blob) {
737
777
  blobToText(data)
738
778
  .then((html) => {
739
- this.setOption("content", sanitizeViewerHTMLContent(html));
779
+ this.setOption("content", sanitizeViewerHtml(html));
740
780
  })
741
781
  .catch((error) => {
742
782
  this.dispatchEvent(
@@ -758,7 +798,7 @@ class Viewer extends CustomElement {
758
798
  return response.text();
759
799
  })
760
800
  .then((html) => {
761
- this.setOption("content", sanitizeViewerHTMLContent(html));
801
+ this.setOption("content", sanitizeViewerHtml(html));
762
802
  })
763
803
  .catch((error) => {
764
804
  this.dispatchEvent(
@@ -775,7 +815,7 @@ class Viewer extends CustomElement {
775
815
  throw new Error("HTMLElement or string expected");
776
816
  }
777
817
 
778
- this.setOption("content", sanitizeViewerHTMLContent(data));
818
+ this.setOption("content", sanitizeViewerHtml(data));
779
819
  }
780
820
 
781
821
  /**
@@ -904,217 +944,203 @@ function blobToText(blob) {
904
944
  });
905
945
  }
906
946
 
907
- function sanitizeViewerHTMLContent(html) {
908
- const sanitizedHTML = sanitizeHtml(html);
909
- const parser = new DOMParser();
910
- const doc = parser.parseFromString(sanitizedHTML, "text/html");
947
+ /**
948
+ * @private
949
+ * @return {Select}
950
+ * @throws {Error} no shadow-root is defined
951
+ */
952
+ function initControlReferences() {
953
+ if (!this.shadowRoot) {
954
+ throw new Error("no shadow-root is defined");
955
+ }
956
+
957
+ this[viewerElementSymbol] = this.shadowRoot.getElementById("viewer");
958
+ this[linkConfirmationElementSymbol] = this.shadowRoot.querySelector(
959
+ '[data-monster-role="external-link-confirmation"]',
960
+ );
961
+ this[linkHostElementSymbol] = this.shadowRoot.querySelector(
962
+ '[data-monster-role="external-link-host"]',
963
+ );
964
+ this[linkUrlElementSymbol] = this.shadowRoot.querySelector(
965
+ '[data-monster-role="external-link-url"]',
966
+ );
967
+ }
911
968
 
912
- doc.querySelectorAll("*").forEach((element) => {
913
- removeCrossOriginResourceAttribute(element, "src");
914
- removeCrossOriginResourceAttribute(element, "srcset");
915
- removeCrossOriginResourceAttribute(element, "poster");
969
+ /**
970
+ * @private
971
+ */
972
+ function initEventHandler() {
973
+ this[linkClickHandlerSymbol] = (event) => {
974
+ const path = event.composedPath?.() || [];
975
+ const actionElement = path.find(
976
+ (element) =>
977
+ element instanceof Element &&
978
+ element.hasAttribute?.("data-monster-link-action"),
979
+ );
916
980
 
917
- if (element.hasAttribute("style")) {
918
- const safeStyle = sanitizeStyleAttribute(element.getAttribute("style"));
919
- if (safeStyle) {
920
- element.setAttribute("style", safeStyle);
921
- } else {
922
- element.removeAttribute("style");
923
- }
981
+ if (actionElement) {
982
+ event.preventDefault();
983
+ event.stopPropagation();
984
+ handleLinkConfirmationAction.call(
985
+ this,
986
+ actionElement.getAttribute("data-monster-link-action"),
987
+ );
988
+ return;
924
989
  }
925
- });
926
990
 
927
- return doc.body.innerHTML;
928
- }
991
+ const link = path.find(
992
+ (element) => element instanceof Element && element.localName === "a",
993
+ );
994
+ if (!link) {
995
+ return;
996
+ }
929
997
 
930
- function removeCrossOriginResourceAttribute(element, attributeName) {
931
- const value = element.getAttribute(attributeName);
932
- if (!value) {
933
- return;
934
- }
998
+ const href = link.getAttribute("href")?.trim();
999
+ if (!href || href.startsWith("#")) {
1000
+ return;
1001
+ }
935
1002
 
936
- if (attributeName === "srcset") {
937
- const sameOriginEntries = value
938
- .split(",")
939
- .map((entry) => entry.trim())
940
- .filter(Boolean)
941
- .filter((entry) => {
942
- const [candidateUrl] = entry.split(/\s+/, 1);
943
- return isSameOriginResourceUrl(candidateUrl);
944
- });
945
-
946
- if (sameOriginEntries.length > 0) {
947
- element.setAttribute(attributeName, sameOriginEntries.join(", "));
1003
+ let url;
1004
+ try {
1005
+ url = new URL(href, document.baseURI);
1006
+ } catch {
1007
+ event.preventDefault();
948
1008
  return;
949
1009
  }
950
- } else if (isSameOriginResourceUrl(value)) {
951
- return;
952
- }
1010
+ if (!SAFE_VIEWER_LINK_PROTOCOLS.has(url.protocol.toLowerCase())) {
1011
+ event.preventDefault();
1012
+ return;
1013
+ }
1014
+
1015
+ event.preventDefault();
1016
+ event.stopPropagation();
1017
+ showLinkConfirmation.call(this, link, url);
1018
+ };
953
1019
 
954
- if (
955
- attributeName === "src" &&
956
- element.tagName.toLowerCase() === "img" &&
957
- !element.classList.contains("privacyImage")
958
- ) {
959
- element.setAttribute("src", BLOCKED_HTML_RESOURCE_DATA_URL);
960
- element.classList.add("privacyImage");
1020
+ this[linkKeydownHandlerSymbol] = (event) => {
1021
+ if (event.key !== "Escape" || this[linkConfirmationElementSymbol]?.hidden) {
1022
+ return;
1023
+ }
1024
+ event.preventDefault();
1025
+ closeLinkConfirmation.call(this, true);
1026
+ };
1027
+
1028
+ this.addEventListener("click", this[linkClickHandlerSymbol], true);
1029
+ this.addEventListener("keydown", this[linkKeydownHandlerSymbol], true);
1030
+ return this;
1031
+ }
1032
+
1033
+ function showLinkConfirmation(link, url) {
1034
+ const confirmation = this[linkConfirmationElementSymbol];
1035
+ if (!confirmation) {
961
1036
  return;
962
1037
  }
963
1038
 
964
- element.removeAttribute(attributeName);
965
- }
1039
+ this[pendingLinkSymbol] = { link, url };
1040
+ this[linkHostElementSymbol].textContent = url.hostname || url.protocol;
1041
+ this[linkUrlElementSymbol].textContent = url.href;
1042
+ confirmation.hidden = false;
966
1043
 
967
- function sanitizeStyleAttribute(styleValue) {
968
- return styleValue.replace(/url\(([^)]+)\)/gi, (match, rawUrl) => {
969
- const cleanedUrl = rawUrl.trim().replace(/^['"]|['"]$/g, "");
970
- return isSameOriginResourceUrl(cleanedUrl) ? match : "";
1044
+ queueMicrotask(() => {
1045
+ positionLinkConfirmation(confirmation, link);
1046
+ confirmation.querySelector('[data-monster-link-action="cancel"]')?.focus();
971
1047
  });
972
1048
  }
973
1049
 
974
- function isSameOriginResourceUrl(value) {
975
- if (!value) {
976
- return true;
977
- }
978
-
979
- const trimmedValue = value.trim();
980
- if (!trimmedValue || trimmedValue.startsWith("#")) {
981
- return true;
982
- }
1050
+ function positionLinkConfirmation(confirmation, link) {
1051
+ const linkRect = link.getBoundingClientRect();
1052
+ const width = confirmation.offsetWidth || 360;
1053
+ const height = confirmation.offsetHeight || 220;
1054
+ const viewportWidth =
1055
+ document.documentElement.clientWidth || window.innerWidth;
1056
+ const viewportHeight =
1057
+ document.documentElement.clientHeight || window.innerHeight;
1058
+ const edge = 16;
1059
+ const gap = 8;
1060
+ const left = Math.max(
1061
+ edge,
1062
+ Math.min(linkRect.left, viewportWidth - width - edge),
1063
+ );
1064
+ const below = linkRect.bottom + gap;
1065
+ const top =
1066
+ below + height <= viewportHeight - edge
1067
+ ? below
1068
+ : Math.max(edge, linkRect.top - height - gap);
1069
+
1070
+ confirmation.style.left = `${left}px`;
1071
+ confirmation.style.top = `${top}px`;
1072
+ }
983
1073
 
984
- try {
985
- const url = new URL(trimmedValue, document.location.href);
986
- return (
987
- url.origin === document.location.origin ||
988
- url.protocol === "data:" ||
989
- url.protocol === "blob:"
990
- );
991
- } catch {
992
- return true;
1074
+ function handleLinkConfirmationAction(action) {
1075
+ if (action === "cancel") {
1076
+ closeLinkConfirmation.call(this, true);
1077
+ return;
993
1078
  }
994
- }
995
1079
 
996
- /**
997
- * @private
998
- * @return {Select}
999
- * @throws {Error} no shadow-root is defined
1000
- */
1001
- function initControlReferences() {
1002
- if (!this.shadowRoot) {
1003
- throw new Error("no shadow-root is defined");
1080
+ const pendingLink = this[pendingLinkSymbol];
1081
+ if (!pendingLink || (action !== "open" && action !== "new-window")) {
1082
+ return;
1004
1083
  }
1005
1084
 
1006
- this[viewerElementSymbol] = this.shadowRoot.getElementById("viewer");
1085
+ const target = action === "new-window" ? "_blank" : "_self";
1086
+ window.open(pendingLink.url.href, target, "noopener,noreferrer");
1087
+ closeLinkConfirmation.call(this, false);
1007
1088
  }
1008
1089
 
1009
- /**
1010
- * @private
1011
- */
1012
- function initEventHandler() {
1013
- return this;
1090
+ function closeLinkConfirmation(restoreFocus) {
1091
+ const pendingLink = this[pendingLinkSymbol];
1092
+ this[linkConfirmationElementSymbol].hidden = true;
1093
+ delete this[pendingLinkSymbol];
1094
+ if (restoreFocus) {
1095
+ pendingLink?.link?.focus();
1096
+ }
1014
1097
  }
1015
1098
 
1016
1099
  function getLabels() {
1017
- switch (getLocaleOfDocument().language) {
1018
- case "de": // German
1019
- return {
1020
- download: "Herunterladen",
1021
- };
1022
-
1023
- case "es": // Spanish
1024
- return {
1025
- download: "Descargar",
1026
- };
1027
-
1028
- case "zh": // Mandarin
1029
- return {
1030
- download: "下载",
1031
- };
1032
-
1033
- case "hi": // Hindi
1034
- return {
1035
- download: "डाउनलोड करें",
1036
- };
1037
-
1038
- case "bn": // Bengali
1039
- return {
1040
- download: "ডাউনলোড",
1041
- };
1042
-
1043
- case "pt": // Portuguese
1044
- return {
1045
- download: "Baixar",
1046
- };
1047
-
1048
- case "ru": // Russian
1049
- return {
1050
- download: "Скачать",
1051
- };
1052
-
1053
- case "ja": // Japanese
1054
- return {
1055
- download: "ダウンロード",
1056
- };
1057
-
1058
- case "pa": // Western Punjabi
1059
- return {
1060
- download: "ਡਾਊਨਲੋਡ ਕਰੋ",
1061
- };
1062
-
1063
- case "mr": // Marathi
1064
- return {
1065
- download: "डाउनलोड करा",
1066
- };
1067
-
1068
- case "fr": // French
1069
- return {
1070
- download: "Télécharger",
1071
- };
1072
-
1073
- case "it": // Italian
1074
- return {
1075
- download: "Scarica",
1076
- };
1077
-
1078
- case "nl": // Dutch
1079
- return {
1080
- download: "Downloaden",
1081
- };
1082
-
1083
- case "sv": // Swedish
1084
- return {
1085
- download: "Ladda ner",
1086
- };
1087
-
1088
- case "pl": // Polish
1089
- return {
1090
- download: "Pobierz",
1091
- };
1092
-
1093
- case "da": // Danish
1094
- return {
1095
- download: "Hent",
1096
- };
1097
-
1098
- case "fi": // Finnish
1099
- return {
1100
- download: "Lataa",
1101
- };
1102
-
1103
- case "no": // Norwegian
1104
- return {
1105
- download: "Last ned",
1106
- };
1107
-
1108
- case "cs": // Czech
1109
- return {
1110
- download: "Stáhnout",
1111
- };
1112
-
1113
- default:
1114
- return {
1115
- download: "Download",
1116
- };
1117
- }
1100
+ const language = getLocaleOfDocument().language;
1101
+ const downloadLabels = {
1102
+ en: "Download",
1103
+ de: "Herunterladen",
1104
+ es: "Descargar",
1105
+ zh: "下载",
1106
+ hi: "डाउनलोड करें",
1107
+ bn: "ডাউনলোড",
1108
+ pt: "Baixar",
1109
+ ru: "Скачать",
1110
+ ja: "ダウンロード",
1111
+ pa: "ਡਾਊਨਲੋਡ ਕਰੋ",
1112
+ mr: "डाउनलोड करा",
1113
+ fr: "Télécharger",
1114
+ it: "Scarica",
1115
+ nl: "Downloaden",
1116
+ sv: "Ladda ner",
1117
+ pl: "Pobierz",
1118
+ da: "Hent",
1119
+ fi: "Lataa",
1120
+ no: "Last ned",
1121
+ cs: "Stáhnout",
1122
+ };
1123
+ const linkLabels =
1124
+ language === "de"
1125
+ ? {
1126
+ externalLinkTitle: "Externen Link öffnen?",
1127
+ externalLinkMessage: "Dieser Link führt zu:",
1128
+ externalLinkCancel: "Abbrechen",
1129
+ externalLinkOpen: "Öffnen",
1130
+ externalLinkNewWindow: "In neuem Fenster öffnen",
1131
+ }
1132
+ : {
1133
+ externalLinkTitle: "Open external link?",
1134
+ externalLinkMessage: "This link leads to:",
1135
+ externalLinkCancel: "Cancel",
1136
+ externalLinkOpen: "Open",
1137
+ externalLinkNewWindow: "Open in new window",
1138
+ };
1139
+
1140
+ return {
1141
+ download: downloadLabels[language] || "Download",
1142
+ ...linkLabels,
1143
+ };
1118
1144
  }
1119
1145
 
1120
1146
  /**
@@ -1127,7 +1153,26 @@ function getTemplate() {
1127
1153
  <div id="viewer" data-monster-role="viewer" part="viewer"
1128
1154
  data-monster-replace="path:content"
1129
1155
  data-monster-attributes="class path:classes.viewer">
1130
- </div>`;
1156
+ </div>
1157
+ <section data-monster-role="external-link-confirmation" part="external-link-confirmation"
1158
+ role="dialog" aria-labelledby="external-link-title"
1159
+ aria-describedby="external-link-message external-link-url" hidden>
1160
+ <strong id="external-link-title" data-monster-replace="path:labels.externalLinkTitle"></strong>
1161
+ <p id="external-link-message" data-monster-replace="path:labels.externalLinkMessage"></p>
1162
+ <strong data-monster-role="external-link-host"></strong>
1163
+ <code id="external-link-url" data-monster-role="external-link-url"></code>
1164
+ <div data-monster-role="external-link-actions">
1165
+ <button type="button" class="monster-button-outline-secondary"
1166
+ data-monster-link-action="cancel"
1167
+ data-monster-replace="path:labels.externalLinkCancel"></button>
1168
+ <button type="button" class="monster-button-primary"
1169
+ data-monster-link-action="open"
1170
+ data-monster-replace="path:labels.externalLinkOpen"></button>
1171
+ <button type="button" class="monster-button-outline-primary"
1172
+ data-monster-link-action="new-window"
1173
+ data-monster-replace="path:labels.externalLinkNewWindow"></button>
1174
+ </div>
1175
+ </section>`;
1131
1176
  }
1132
1177
 
1133
1178
  registerCustomElement(Viewer);
@@ -41,7 +41,9 @@ describe("Viewer", function () {
41
41
  expect(textNode.textContent).to.equal(
42
42
  "<strong>raw</strong>\n<script>alert(1)</script>",
43
43
  );
44
- expect(textNode.innerHTML).to.contain("&lt;strong&gt;raw&lt;/strong&gt;");
44
+ expect(textNode.innerHTML).to.contain(
45
+ "&lt;strong&gt;raw&lt;/strong&gt;",
46
+ );
45
47
  done();
46
48
  } catch (error) {
47
49
  done(error);
@@ -66,6 +68,10 @@ describe("Viewer", function () {
66
68
  `
67
69
  <link rel="stylesheet" href="https://evil.example/tracker.css">
68
70
  <script src="https://evil.example/tracker.js"></script>
71
+ <style id="tracking-style">
72
+ @import "https://evil.example/import.css";
73
+ .tracked { background-image: url("https://evil.example/background.png"); }
74
+ </style>
69
75
  <img id="external" src="https://evil.example/pixel.png">
70
76
  <img id="local" src="/image.png">
71
77
  `,
@@ -77,11 +83,16 @@ describe("Viewer", function () {
77
83
  const root = viewer.shadowRoot;
78
84
  expect(root.querySelector("link")).to.equal(null);
79
85
  expect(root.querySelector("script")).to.equal(null);
86
+ expect(root.querySelector("#tracking-style")).to.equal(null);
80
87
 
81
88
  const externalImage = root.querySelector("#external");
82
89
  expect(externalImage).to.not.equal(null);
83
- expect(externalImage.classList.contains("privacyImage")).to.equal(true);
84
- expect(externalImage.getAttribute("src")).to.match(/^data:image\/gif;base64,/);
90
+ expect(externalImage.classList.contains("privacyImage")).to.equal(
91
+ true,
92
+ );
93
+ expect(externalImage.getAttribute("src")).to.match(
94
+ /^data:image\/gif;base64,/,
95
+ );
85
96
 
86
97
  const localImage = root.querySelector("#local");
87
98
  expect(localImage).to.not.equal(null);
@@ -96,4 +107,240 @@ describe("Viewer", function () {
96
107
  }
97
108
  }, 0);
98
109
  });
110
+
111
+ it("removes unsafe links, forms, and interactive html", function (done) {
112
+ const mocks = document.getElementById("mocks");
113
+ mocks.innerHTML = `<monster-viewer id="viewer"></monster-viewer>`;
114
+
115
+ setTimeout(() => {
116
+ try {
117
+ const viewer = document.getElementById("viewer");
118
+ viewer.setContent(
119
+ `
120
+ <a id="script-link" href="jav&#x09;ascript:alert(1)" onclick="alert(2)">masked</a>
121
+ <a id="data-link" href=" DATA:text/html,evil">data</a>
122
+ <a id="custom-link" href="intent://evil">intent</a>
123
+ <a id="web-link" href="https://safe.example/path" ping="https://tracker.example" attributionsrc="https://tracker.example/register">web</a>
124
+ <a id="mail-link" href="mailto:user@example.com">mail</a>
125
+ <a id="phone-link" href="tel:+491234">phone</a>
126
+ <a id="fragment-link" href="#section">fragment</a>
127
+ <form id="form"><input name="secret"><button>Send</button></form>
128
+ <details id="details"><summary>Open</summary>Hidden</details>
129
+ <monster-button id="custom-element">Action</monster-button>
130
+ <svg><use id="external-use" href="https://evil.example/image.svg#pixel"></use></svg>
131
+ `,
132
+ "text/html",
133
+ );
134
+
135
+ setTimeout(() => {
136
+ try {
137
+ const root = viewer.shadowRoot;
138
+ expect(
139
+ root.querySelector("#script-link").hasAttribute("href"),
140
+ ).to.equal(false);
141
+ expect(
142
+ root.querySelector("#script-link").hasAttribute("onclick"),
143
+ ).to.equal(false);
144
+ expect(
145
+ root.querySelector("#data-link").hasAttribute("href"),
146
+ ).to.equal(false);
147
+ expect(
148
+ root.querySelector("#custom-link").hasAttribute("href"),
149
+ ).to.equal(false);
150
+ expect(
151
+ root.querySelector("#web-link").getAttribute("href"),
152
+ ).to.equal("https://safe.example/path");
153
+ expect(
154
+ root.querySelector("#web-link").hasAttribute("ping"),
155
+ ).to.equal(false);
156
+ expect(
157
+ root.querySelector("#web-link").hasAttribute("attributionsrc"),
158
+ ).to.equal(false);
159
+ expect(
160
+ root.querySelector("#mail-link").getAttribute("href"),
161
+ ).to.equal("mailto:user@example.com");
162
+ expect(
163
+ root.querySelector("#phone-link").getAttribute("href"),
164
+ ).to.equal("tel:+491234");
165
+ expect(
166
+ root.querySelector("#fragment-link").getAttribute("href"),
167
+ ).to.equal("#section");
168
+ expect(root.querySelector("form")).to.equal(null);
169
+ expect(root.querySelector("input")).to.equal(null);
170
+ expect(
171
+ root.querySelector("button:not([data-monster-link-action])"),
172
+ ).to.equal(null);
173
+ expect(root.querySelector("details")).to.equal(null);
174
+ expect(root.querySelector("monster-button")).to.equal(null);
175
+ expect(
176
+ root.querySelector("#external-use").hasAttribute("href"),
177
+ ).to.equal(false);
178
+ done();
179
+ } catch (error) {
180
+ done(error);
181
+ }
182
+ }, 0);
183
+ } catch (error) {
184
+ done(error);
185
+ }
186
+ }, 0);
187
+ });
188
+
189
+ it("confirms the canonical destination before opening a link", function (done) {
190
+ const mocks = document.getElementById("mocks");
191
+ mocks.innerHTML = `<monster-viewer id="viewer"></monster-viewer>`;
192
+
193
+ setTimeout(() => {
194
+ const viewer = document.getElementById("viewer");
195
+ const originalOpen = window.open;
196
+ const opened = [];
197
+ window.open = (...args) => {
198
+ opened.push(args);
199
+ return null;
200
+ };
201
+
202
+ try {
203
+ viewer.setContent(
204
+ '<a id="masked-link" href="https://xn--bcher-kva.example/account">Your bank</a>',
205
+ "text/html",
206
+ );
207
+
208
+ setTimeout(() => {
209
+ try {
210
+ const link = viewer.shadowRoot.querySelector("#masked-link");
211
+ link.click();
212
+
213
+ const confirmation = viewer.shadowRoot.querySelector(
214
+ '[data-monster-role="external-link-confirmation"]',
215
+ );
216
+ expect(confirmation.hidden).to.equal(false);
217
+ expect(
218
+ viewer.shadowRoot.querySelector(
219
+ '[data-monster-role="external-link-host"]',
220
+ ).textContent,
221
+ ).to.equal("xn--bcher-kva.example");
222
+ expect(
223
+ viewer.shadowRoot.querySelector(
224
+ '[data-monster-role="external-link-url"]',
225
+ ).textContent,
226
+ ).to.equal("https://xn--bcher-kva.example/account");
227
+
228
+ viewer.shadowRoot
229
+ .querySelector('[data-monster-link-action="cancel"]')
230
+ .click();
231
+ expect(confirmation.hidden).to.equal(true);
232
+ expect(opened).to.have.length(0);
233
+
234
+ link.click();
235
+ viewer.shadowRoot
236
+ .querySelector('[data-monster-link-action="new-window"]')
237
+ .click();
238
+
239
+ link.click();
240
+ viewer.shadowRoot
241
+ .querySelector('[data-monster-link-action="open"]')
242
+ .click();
243
+ expect(opened).to.deep.equal([
244
+ [
245
+ "https://xn--bcher-kva.example/account",
246
+ "_blank",
247
+ "noopener,noreferrer",
248
+ ],
249
+ [
250
+ "https://xn--bcher-kva.example/account",
251
+ "_self",
252
+ "noopener,noreferrer",
253
+ ],
254
+ ]);
255
+ done();
256
+ } catch (error) {
257
+ done(error);
258
+ } finally {
259
+ window.open = originalOpen;
260
+ }
261
+ }, 0);
262
+ } catch (error) {
263
+ window.open = originalOpen;
264
+ done(error);
265
+ }
266
+ }, 0);
267
+ });
268
+
269
+ it("keeps fragment links local without confirmation", function (done) {
270
+ const mocks = document.getElementById("mocks");
271
+ mocks.innerHTML = `<monster-viewer id="viewer"></monster-viewer>`;
272
+
273
+ setTimeout(() => {
274
+ try {
275
+ const viewer = document.getElementById("viewer");
276
+ viewer.setContent(
277
+ '<a id="fragment" href="#message-section">Section</a>',
278
+ "text/html",
279
+ );
280
+ setTimeout(() => {
281
+ try {
282
+ viewer.shadowRoot.querySelector("#fragment").click();
283
+ expect(
284
+ viewer.shadowRoot.querySelector(
285
+ '[data-monster-role="external-link-confirmation"]',
286
+ ).hidden,
287
+ ).to.equal(true);
288
+ done();
289
+ } catch (error) {
290
+ done(error);
291
+ }
292
+ }, 0);
293
+ } catch (error) {
294
+ done(error);
295
+ }
296
+ }, 0);
297
+ });
298
+
299
+ it("confirms links rendered inside an email message", function (done) {
300
+ const mocks = document.getElementById("mocks");
301
+ mocks.innerHTML = `<monster-viewer id="viewer"></monster-viewer>`;
302
+
303
+ setTimeout(() => {
304
+ try {
305
+ const viewer = document.getElementById("viewer");
306
+ viewer.setMessage({
307
+ parts: [
308
+ {
309
+ contentType: "text/html",
310
+ content:
311
+ '<a id="mail-link" href="https://evil.example/login">Trusted portal</a>',
312
+ },
313
+ ],
314
+ });
315
+
316
+ setTimeout(() => {
317
+ try {
318
+ const message = viewer.shadowRoot.querySelector(
319
+ "monster-message-content",
320
+ );
321
+ const html = message.shadowRoot.querySelector(
322
+ "monster-html-content",
323
+ );
324
+ const link = html.shadowRoot.querySelector("#mail-link");
325
+ link.click();
326
+
327
+ const confirmation = viewer.shadowRoot.querySelector(
328
+ '[data-monster-role="external-link-confirmation"]',
329
+ );
330
+ expect(confirmation.hidden).to.equal(false);
331
+ expect(
332
+ viewer.shadowRoot.querySelector(
333
+ '[data-monster-role="external-link-url"]',
334
+ ).textContent,
335
+ ).to.equal("https://evil.example/login");
336
+ done();
337
+ } catch (error) {
338
+ done(error);
339
+ }
340
+ }, 180);
341
+ } catch (error) {
342
+ done(error);
343
+ }
344
+ }, 0);
345
+ });
99
346
  });