@schukai/monster 4.135.0 → 4.136.1

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.","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.135.0"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","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.136.1"}
@@ -29,6 +29,35 @@ try {
29
29
  }`,
30
30
  0,
31
31
  );
32
+ ViewerStyleSheet.insertRule(
33
+ `
34
+ @layer viewer {
35
+ :host {
36
+ max-width: 100%;
37
+ min-width: 0;
38
+ }
39
+
40
+ [data-monster-role="viewer"] {
41
+ max-width: 100%;
42
+ min-height: 0;
43
+ min-width: 0;
44
+ overflow: auto;
45
+ }
46
+
47
+ [data-monster-role="viewer"] :is(img, video, audio, object, embed, table, pre) {
48
+ max-width: 100%;
49
+ }
50
+
51
+ [data-monster-role="viewer"] [part="text"] {
52
+ overflow: hidden;
53
+ overflow-wrap: anywhere;
54
+ white-space: pre-wrap;
55
+ width: 100%;
56
+ }
57
+ }
58
+ `,
59
+ 1,
60
+ );
32
61
  } catch (e) {
33
62
  addAttributeToken(
34
63
  document.getRootNode().querySelector("html"),
@@ -12,6 +12,8 @@ div {
12
12
  div[data-monster-role="content-container"] {
13
13
  width: 100%;
14
14
  height: 100%;
15
+ max-width: 100%;
16
+ min-width: 0;
15
17
  overflow: auto;
16
18
  position: relative;
17
19
  display: flex;
@@ -25,7 +25,7 @@ try {
25
25
  HtmlStyleSheet.insertRule(
26
26
  `
27
27
  @layer html {
28
- div,img{transition:opacity .9s ease,transform .3s ease,filter .3s ease,box-shadow .3s ease,background-color .3s ease}div[data-monster-role=content-container]{display:flex;flex-direction:column;height:100%;overflow:auto;position:relative;width:100%}.privacyImage{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56'/%3E%3Cpath d='M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56'/%3E%3Cpath d='M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0z'/%3E%3C/svg%3E\")}.notFoundImage,.privacyImage{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--monster-bg-color-primary-4);-webkit-mask-position:center center;mask-position:center center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:50%;mask-size:50%;-o-object-fit:contain;object-fit:contain}.notFoundImage{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M3.112 5.112a3 3 0 0 0-.17.613C1.266 6.095 0 7.555 0 9.318 0 11.366 1.708 13 3.781 13H11l-1-1H3.781C2.231 12 1 10.785 1 9.318c0-1.365 1.064-2.513 2.46-2.666l.446-.05v-.447q0-.113.018-.231zm2.55-1.45-.725-.725A5.5 5.5 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773a3.2 3.2 0 0 1-1.516 2.711l-.733-.733C14.498 11.378 15 10.626 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3c-.875 0-1.678.26-2.339.661z'/%3E%3Cpath d='m13.646 14.354-12-12 .708-.708 12 12z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M3.112 5.112a3 3 0 0 0-.17.613C1.266 6.095 0 7.555 0 9.318 0 11.366 1.708 13 3.781 13H11l-1-1H3.781C2.231 12 1 10.785 1 9.318c0-1.365 1.064-2.513 2.46-2.666l.446-.05v-.447q0-.113.018-.231zm2.55-1.45-.725-.725A5.5 5.5 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773a3.2 3.2 0 0 1-1.516 2.711l-.733-.733C14.498 11.378 15 10.626 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3c-.875 0-1.678.26-2.339.661z'/%3E%3Cpath d='m13.646 14.354-12-12 .708-.708 12 12z'/%3E%3C/svg%3E\")}
28
+ div,img{transition:opacity .9s ease,transform .3s ease,filter .3s ease,box-shadow .3s ease,background-color .3s ease}div[data-monster-role=content-container]{display:flex;flex-direction:column;height:100%;max-width:100%;min-width:0;overflow:auto;position:relative;width:100%}.privacyImage{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56'/%3E%3Cpath d='M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56'/%3E%3Cpath d='M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0z'/%3E%3C/svg%3E\")}.notFoundImage,.privacyImage{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--monster-bg-color-primary-4);-webkit-mask-position:center center;mask-position:center center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:50%;mask-size:50%;-o-object-fit:contain;object-fit:contain}.notFoundImage{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M3.112 5.112a3 3 0 0 0-.17.613C1.266 6.095 0 7.555 0 9.318 0 11.366 1.708 13 3.781 13H11l-1-1H3.781C2.231 12 1 10.785 1 9.318c0-1.365 1.064-2.513 2.46-2.666l.446-.05v-.447q0-.113.018-.231zm2.55-1.45-.725-.725A5.5 5.5 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773a3.2 3.2 0 0 1-1.516 2.711l-.733-.733C14.498 11.378 15 10.626 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3c-.875 0-1.678.26-2.339.661z'/%3E%3Cpath d='m13.646 14.354-12-12 .708-.708 12 12z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M3.112 5.112a3 3 0 0 0-.17.613C1.266 6.095 0 7.555 0 9.318 0 11.366 1.708 13 3.781 13H11l-1-1H3.781C2.231 12 1 10.785 1 9.318c0-1.365 1.064-2.513 2.46-2.666l.446-.05v-.447q0-.113.018-.231zm2.55-1.45-.725-.725A5.5 5.5 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773a3.2 3.2 0 0 1-1.516 2.711l-.733-.733C14.498 11.378 15 10.626 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3c-.875 0-1.678.26-2.339.661z'/%3E%3Cpath d='m13.646 14.354-12-12 .708-.708 12 12z'/%3E%3C/svg%3E\")}
29
29
  }`,
30
30
  0,
31
31
  );
@@ -29,9 +29,13 @@ 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
33
 
33
34
  export { Viewer };
34
35
 
36
+ const BLOCKED_HTML_RESOURCE_DATA_URL =
37
+ "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
38
+
35
39
  /**
36
40
  * @private
37
41
  * @type {symbol}
@@ -732,7 +736,7 @@ class Viewer extends CustomElement {
732
736
  if (data instanceof Blob) {
733
737
  blobToText(data)
734
738
  .then((html) => {
735
- this.setOption("content", html);
739
+ this.setOption("content", sanitizeViewerHTMLContent(html));
736
740
  })
737
741
  .catch((error) => {
738
742
  this.dispatchEvent(
@@ -754,7 +758,7 @@ class Viewer extends CustomElement {
754
758
  return response.text();
755
759
  })
756
760
  .then((html) => {
757
- this.setOption("content", html);
761
+ this.setOption("content", sanitizeViewerHTMLContent(html));
758
762
  })
759
763
  .catch((error) => {
760
764
  this.dispatchEvent(
@@ -771,7 +775,7 @@ class Viewer extends CustomElement {
771
775
  throw new Error("HTMLElement or string expected");
772
776
  }
773
777
 
774
- this.setOption("content", data);
778
+ this.setOption("content", sanitizeViewerHTMLContent(data));
775
779
  }
776
780
 
777
781
  /**
@@ -787,7 +791,7 @@ class Viewer extends CustomElement {
787
791
  setPlainText(data) {
788
792
  const mkPreSpan = (text) => {
789
793
  const pre = document.createElement("pre");
790
- pre.innerText = text;
794
+ pre.textContent = text;
791
795
  pre.setAttribute("part", "text");
792
796
  return pre.outerHTML;
793
797
  };
@@ -795,10 +799,6 @@ class Viewer extends CustomElement {
795
799
  if (data instanceof Blob) {
796
800
  blobToText(data)
797
801
  .then((text) => {
798
- const div = document.createElement("div");
799
- div.innerHTML = text;
800
- text = div.innerText;
801
-
802
802
  this.setOption("content", mkPreSpan(text));
803
803
  })
804
804
  .catch((error) => {
@@ -812,9 +812,7 @@ class Viewer extends CustomElement {
812
812
  } else if (data instanceof HTMLElement) {
813
813
  data = data.outerText;
814
814
  } else if (isString(data)) {
815
- const div = document.createElement("div");
816
- div.innerHTML = data;
817
- data = div.innerText;
815
+ // text/plain should never be interpreted as HTML
818
816
  } else if (isURL(data)) {
819
817
  getGlobal()
820
818
  .fetch(data)
@@ -822,10 +820,6 @@ class Viewer extends CustomElement {
822
820
  return response.text();
823
821
  })
824
822
  .then((text) => {
825
- const div = document.createElement("div");
826
- div.innerHTML = text;
827
- text = div.innerText;
828
-
829
823
  this.setOption("content", mkPreSpan(text));
830
824
  })
831
825
  .catch((error) => {
@@ -910,6 +904,95 @@ function blobToText(blob) {
910
904
  });
911
905
  }
912
906
 
907
+ function sanitizeViewerHTMLContent(html) {
908
+ const sanitizedHTML = sanitizeHtml(html);
909
+ const parser = new DOMParser();
910
+ const doc = parser.parseFromString(sanitizedHTML, "text/html");
911
+
912
+ doc.querySelectorAll("*").forEach((element) => {
913
+ removeCrossOriginResourceAttribute(element, "src");
914
+ removeCrossOriginResourceAttribute(element, "srcset");
915
+ removeCrossOriginResourceAttribute(element, "poster");
916
+
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
+ }
924
+ }
925
+ });
926
+
927
+ return doc.body.innerHTML;
928
+ }
929
+
930
+ function removeCrossOriginResourceAttribute(element, attributeName) {
931
+ const value = element.getAttribute(attributeName);
932
+ if (!value) {
933
+ return;
934
+ }
935
+
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(", "));
948
+ return;
949
+ }
950
+ } else if (isSameOriginResourceUrl(value)) {
951
+ return;
952
+ }
953
+
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");
961
+ return;
962
+ }
963
+
964
+ element.removeAttribute(attributeName);
965
+ }
966
+
967
+ function sanitizeStyleAttribute(styleValue) {
968
+ return styleValue.replace(/url\(([^)]+)\)/gi, (match, rawUrl) => {
969
+ const cleanedUrl = rawUrl.trim().replace(/^['"]|['"]$/g, "");
970
+ return isSameOriginResourceUrl(cleanedUrl) ? match : "";
971
+ });
972
+ }
973
+
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
+ }
983
+
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;
993
+ }
994
+ }
995
+
913
996
  /**
914
997
  * @private
915
998
  * @return {Select}
@@ -39,6 +39,9 @@ const rawDataSymbol = Symbol.for(
39
39
  *
40
40
  * @externalExample ../../../../example/data/datasource/server/restapi.mjs
41
41
  * @example /examples/libraries/data/datasource/server/restapi/simple/ Configure read and write endpoints
42
+ * @example /examples/libraries/data/datasource/server/restapi/response-callback/ Replace the default read callback with a custom assignment flow
43
+ * @example /examples/libraries/data/datasource/server/restapi/partial-write/ Patch only changed fields before sending a write request
44
+ * @example /examples/libraries/data/datasource/server/restapi/report-path/ Read validation reports from a nested response path
42
45
  * @license AGPLv3
43
46
  * @since 1.22.0
44
47
  * @copyright Volker Schukai
@@ -35,6 +35,9 @@ const webConnectSymbol = Symbol("connection");
35
35
  *
36
36
  * @externalExample ../../../../example/data/datasource/server/webconnect.mjs
37
37
  * @example /examples/libraries/data/datasource/server/webconnect/simple/ Configure a realtime datasource bridge
38
+ * @example /examples/libraries/data/datasource/server/webconnect/message-queue/ Read queued realtime messages through the datasource API
39
+ * @example /examples/libraries/data/datasource/server/webconnect/transformed-read/ Apply read mapping to incoming socket payloads
40
+ * @example /examples/libraries/data/datasource/server/webconnect/write-envelope/ Prepare outgoing socket writes with sheathing options
38
41
  * @license AGPLv3
39
42
  * @since 3.1.0
40
43
  * @copyright Volker Schukai
@@ -33,6 +33,9 @@ const serverVersionSymbol = Symbol("serverVersion");
33
33
  * @fragments /fragments/libraries/data/datasource/server/
34
34
  *
35
35
  * @example /examples/libraries/data/datasource/server/simple/ Transform and prepare server payloads
36
+ * @example /examples/libraries/data/datasource/server/transformer-callbacks/ Combine transformer expressions with mapping callbacks
37
+ * @example /examples/libraries/data/datasource/server/partial-diff/ Reduce write payloads through the partial diff callback
38
+ * @example /examples/libraries/data/datasource/server/sheathing-object/ Wrap outgoing payloads into a server-side envelope
36
39
  * @license AGPLv3
37
40
  * @since 3.4.0
38
41
  * @copyright Volker Schukai
@@ -32,6 +32,9 @@ const storageObjectSymbol = Symbol.for(
32
32
  * @fragments /fragments/libraries/data/datasource/storage/
33
33
  *
34
34
  * @example /examples/libraries/data/datasource/storage/simple/ Read and write JSON through Web Storage
35
+ * @example /examples/libraries/data/datasource/storage/session-draft/ Persist draft state in session storage
36
+ * @example /examples/libraries/data/datasource/storage/local-preferences/ Persist user preferences in local storage
37
+ * @example /examples/libraries/data/datasource/storage/remove-on-undefined/ Remove the storage entry when the datasource becomes undefined
35
38
  * @license AGPLv3
36
39
  * @since 1.22.0
37
40
  * @copyright Volker Schukai
@@ -31,6 +31,9 @@ const internalTranslationSymbol = Symbol("internalTranslation");
31
31
  * The Formatter extends the Text.Formatter with the possibility to replace the key by a translation.
32
32
  *
33
33
  * @fragments /fragments/libraries/i18n/formatter/
34
+ * @example /examples/libraries/i18n/formatter/basic-translation/ Resolve one translation key through the i18n marker
35
+ * @example /examples/libraries/i18n/formatter/parameterized-text/ Combine translated text with formatter parameters
36
+ * @example /examples/libraries/i18n/formatter/custom-markers/ Configure custom formatter markers for translated strings
34
37
  *
35
38
  * @license AGPLv3
36
39
  * @since 1.26.0
@@ -41,6 +41,9 @@ const translationsLinkSymbol = Symbol.for(
41
41
  * @fragments /fragments/libraries/i18n/provider/
42
42
  *
43
43
  * @example /examples/libraries/i18n/provider/simple/ Assign translations to a document
44
+ * @example /examples/libraries/i18n/provider/merge-existing/ Merge a second translation payload into an already linked element
45
+ * @example /examples/libraries/i18n/provider/subtree-assignment/ Attach translations to a dedicated subtree instead of the whole document
46
+ * @example /examples/libraries/i18n/provider/locale-switch/ Resolve different translation objects for different locales
44
47
  * @license AGPLv3
45
48
  * @since 1.13.0
46
49
  * @copyright Volker Schukai
@@ -25,6 +25,9 @@ export { Handler };
25
25
  * @fragments /fragments/libraries/logging/handler/
26
26
  *
27
27
  * @example /examples/libraries/logging/handler/simple/ Filter log entries before forwarding them
28
+ * @example /examples/libraries/logging/handler/level-switch/ Switch handler thresholds with the convenience methods
29
+ * @example /examples/libraries/logging/handler/structured-output/ Forward structured log entry data into a custom buffer
30
+ * @example /examples/libraries/logging/handler/off-state/ Disable a handler completely with the off level
28
31
  * @license AGPLv3
29
32
  * @since 1.5.0
30
33
  * @copyright Volker Schukai
@@ -71,6 +71,9 @@ const OFF = 0;
71
71
  * @fragments /fragments/libraries/logging/logger/
72
72
  *
73
73
  * @example /examples/libraries/logging/logger/simple/ Write entries through a console handler
74
+ * @example /examples/libraries/logging/logger/multiple-handlers/ Route the same log entry through multiple handlers
75
+ * @example /examples/libraries/logging/logger/level-filtering/ Filter log output through handler log levels
76
+ * @example /examples/libraries/logging/logger/remove-handler/ Remove a handler and verify that it no longer receives entries
74
77
  * @license AGPLv3
75
78
  * @since 1.5.0
76
79
  * @copyright Volker Schukai
@@ -25,6 +25,9 @@ const dataSymbol = Symbol("@@data");
25
25
  * @fragments /fragments/libraries/net/webconnect/message/
26
26
  *
27
27
  * @example /examples/libraries/net/webconnect/message/simple/ Serialize and restore a structured message
28
+ * @example /examples/libraries/net/webconnect/message/nested-payload/ Keep nested payload data intact through serialization
29
+ * @example /examples/libraries/net/webconnect/message/queue-ready-shape/ Use a message shape that is ready for queue inspection and logging
30
+ * @example /examples/libraries/net/webconnect/message/object-access/ Access raw message data through getData and toJSON
28
31
  * @license AGPLv3
29
32
  * @since 3.4.0
30
33
  * @copyright Volker Schukai
@@ -213,6 +213,9 @@ function connectServer(resolve, reject) {
213
213
  *
214
214
  * @externalExample ../../example/net/webconnect.mjs
215
215
  * @example /examples/libraries/net/webconnect/simple/ Connect and consume structured messages
216
+ * @example /examples/libraries/net/webconnect/send-and-close/ Send structured messages and close the connection manually
217
+ * @example /examples/libraries/net/webconnect/observer-queue/ Observe incoming queue updates through the receive queue observer API
218
+ * @example /examples/libraries/net/webconnect/reconnect-options/ Configure reconnect and timeout options for resilient connections
216
219
  * @license AGPLv3
217
220
  * @since 3.1.0
218
221
  * @copyright Volker Schukai
@@ -0,0 +1,99 @@
1
+ import * as chai from "chai";
2
+ import { chaiDom } from "../../../util/chai-dom.mjs";
3
+ import { initJSDOM } from "../../../util/jsdom.mjs";
4
+
5
+ chai.use(chaiDom);
6
+ const expect = chai.expect;
7
+
8
+ let Viewer;
9
+
10
+ describe("Viewer", function () {
11
+ before(function (done) {
12
+ initJSDOM()
13
+ .then(() => import("../../../../source/components/content/viewer.mjs"))
14
+ .then((module) => {
15
+ Viewer = module.Viewer;
16
+ done();
17
+ })
18
+ .catch((error) => done(error));
19
+ });
20
+
21
+ afterEach(() => {
22
+ const mocks = document.getElementById("mocks");
23
+ mocks.innerHTML = "";
24
+ });
25
+
26
+ it("renders text/plain without interpreting html", function (done) {
27
+ const mocks = document.getElementById("mocks");
28
+ mocks.innerHTML = `<monster-viewer id="viewer"></monster-viewer>`;
29
+
30
+ setTimeout(() => {
31
+ try {
32
+ const viewer = document.getElementById("viewer");
33
+ expect(viewer).instanceof(Viewer);
34
+
35
+ viewer.setContent("<strong>raw</strong>\n<script>alert(1)</script>");
36
+
37
+ setTimeout(() => {
38
+ try {
39
+ const textNode = viewer.shadowRoot.querySelector('[part="text"]');
40
+ expect(textNode).to.not.equal(null);
41
+ expect(textNode.textContent).to.equal(
42
+ "<strong>raw</strong>\n<script>alert(1)</script>",
43
+ );
44
+ expect(textNode.innerHTML).to.contain("&lt;strong&gt;raw&lt;/strong&gt;");
45
+ done();
46
+ } catch (error) {
47
+ done(error);
48
+ }
49
+ }, 0);
50
+ } catch (error) {
51
+ done(error);
52
+ }
53
+ }, 0);
54
+ });
55
+
56
+ it("blocks cross-origin resources in html view", function (done) {
57
+ const mocks = document.getElementById("mocks");
58
+ mocks.innerHTML = `<monster-viewer id="viewer"></monster-viewer>`;
59
+
60
+ setTimeout(() => {
61
+ try {
62
+ const viewer = document.getElementById("viewer");
63
+ expect(viewer).instanceof(Viewer);
64
+
65
+ viewer.setContent(
66
+ `
67
+ <link rel="stylesheet" href="https://evil.example/tracker.css">
68
+ <script src="https://evil.example/tracker.js"></script>
69
+ <img id="external" src="https://evil.example/pixel.png">
70
+ <img id="local" src="/image.png">
71
+ `,
72
+ "text/html",
73
+ );
74
+
75
+ setTimeout(() => {
76
+ try {
77
+ const root = viewer.shadowRoot;
78
+ expect(root.querySelector("link")).to.equal(null);
79
+ expect(root.querySelector("script")).to.equal(null);
80
+
81
+ const externalImage = root.querySelector("#external");
82
+ 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,/);
85
+
86
+ const localImage = root.querySelector("#local");
87
+ expect(localImage).to.not.equal(null);
88
+ expect(localImage.getAttribute("src")).to.equal("/image.png");
89
+ done();
90
+ } catch (error) {
91
+ done(error);
92
+ }
93
+ }, 0);
94
+ } catch (error) {
95
+ done(error);
96
+ }
97
+ }, 0);
98
+ });
99
+ });
@@ -3,6 +3,7 @@ import "./prepare.js";
3
3
  import "../cases/components/layout/tabs.mjs";
4
4
  import "../cases/components/layout/slit-panel.mjs";
5
5
  import "../cases/components/layout/panel.mjs";
6
+ import "../cases/components/content/viewer.mjs";
6
7
  import "../cases/components/content/image-editor.mjs";
7
8
  import "../cases/components/form/buy-box.mjs";
8
9
  import "../cases/components/form/message-state-button.mjs";