@limetech/lime-elements 39.2.1 → 39.3.0
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/CHANGELOG.md +14 -0
- package/dist/cjs/limel-file-viewer.cjs.entry.js +56 -58
- package/dist/cjs/limel-markdown.cjs.entry.js +79 -2
- package/dist/collection/components/email-viewer/sanitize-email-html.js +56 -58
- package/dist/collection/components/markdown/hydrate-custom-elements.js +75 -0
- package/dist/collection/components/markdown/markdown.js +5 -2
- package/dist/esm/limel-file-viewer.entry.js +57 -59
- package/dist/esm/limel-markdown.entry.js +79 -2
- package/dist/lime-elements/lime-elements.esm.js +1 -1
- package/dist/lime-elements/p-49204db4.entry.js +1 -0
- package/dist/lime-elements/{p-c509ec9a.entry.js → p-656b8f6e.entry.js} +1 -1
- package/dist/types/components/markdown/hydrate-custom-elements.d.ts +18 -0
- package/dist/types/components/markdown/markdown.d.ts +1 -0
- package/dist/types/components.d.ts +4 -0
- package/package.json +1 -1
- package/dist/lime-elements/p-d9a7a188.entry.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [39.3.0](https://github.com/Lundalogik/lime-elements/compare/v39.2.2...v39.3.0) (2026-02-23)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
* **markdown:** hydrate JSON attributes on whitelisted custom elements ([ef3a7a9](https://github.com/Lundalogik/lime-elements/commit/ef3a7a95d7340362dc7916fe16542e39278e74b0))
|
|
7
|
+
|
|
8
|
+
## [39.2.2](https://github.com/Lundalogik/lime-elements/compare/v39.2.1...v39.2.2) (2026-02-22)
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
* **email-viewer:** add explicit font and meta attributes to sanitization schema ([c3ea360](https://github.com/Lundalogik/lime-elements/commit/c3ea360fcc50fe7131caf366b695614d41c14e9d))
|
|
14
|
+
|
|
1
15
|
## [39.2.1](https://github.com/Lundalogik/lime-elements/compare/v39.2.0...v39.2.1) (2026-02-22)
|
|
2
16
|
|
|
3
17
|
### Bug Fixes
|
|
@@ -4569,6 +4569,7 @@ class PostalMime {
|
|
|
4569
4569
|
}
|
|
4570
4570
|
}
|
|
4571
4571
|
|
|
4572
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
4572
4573
|
const allowedMimeTypes = new Set([
|
|
4573
4574
|
'image/png',
|
|
4574
4575
|
'image/jpeg',
|
|
@@ -4594,7 +4595,7 @@ const allowedMimeTypes = new Set([
|
|
|
4594
4595
|
async function sanitizeEmailHTML(html) {
|
|
4595
4596
|
const file = await index.unified()
|
|
4596
4597
|
.use(index.rehypeParse)
|
|
4597
|
-
.use(index.rehypeSanitize,
|
|
4598
|
+
.use(index.rehypeSanitize, emailSanitizationSchema)
|
|
4598
4599
|
.use(() => {
|
|
4599
4600
|
return (tree) => {
|
|
4600
4601
|
index.visit(tree, 'element', (node) => {
|
|
@@ -4607,65 +4608,62 @@ async function sanitizeEmailHTML(html) {
|
|
|
4607
4608
|
.process(html);
|
|
4608
4609
|
return file.toString();
|
|
4609
4610
|
}
|
|
4611
|
+
// Base src protocols from defaultSchema, extended with 'data' below.
|
|
4612
|
+
const defaultSrcProtocols = (_b = (_a = index.defaultSchema.protocols) === null || _a === void 0 ? void 0 : _a.src) !== null && _b !== void 0 ? _b : [];
|
|
4610
4613
|
/**
|
|
4611
|
-
*
|
|
4612
|
-
*
|
|
4614
|
+
* Rehype-sanitize schema that allows all standard HTML elements and attributes
|
|
4615
|
+
* needed for rich email rendering, including `style`.
|
|
4616
|
+
*
|
|
4617
|
+
* Hoisted to module scope since the schema has no runtime dependencies and
|
|
4618
|
+
* doesn't need to be reconstructed on every sanitization call.
|
|
4613
4619
|
*/
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
//
|
|
4620
|
-
//
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
'colgroup',
|
|
4662
|
-
'col',
|
|
4663
|
-
'center', // Deprecated but widely used in email
|
|
4664
|
-
'font', // Deprecated but widely used in email
|
|
4665
|
-
]
|
|
4666
|
-
});
|
|
4667
|
-
return schema;
|
|
4668
|
-
}
|
|
4620
|
+
const emailSanitizationSchema = Object.assign(Object.assign({}, index.defaultSchema), {
|
|
4621
|
+
// Disable the 'user-content-' prefix that rehype-sanitize adds to
|
|
4622
|
+
// id and name attributes. Email HTML uses ids for internal anchor
|
|
4623
|
+
// links (href="#section") that must resolve without a prefix.
|
|
4624
|
+
clobberPrefix: '', protocols: Object.assign(Object.assign({}, index.defaultSchema.protocols), {
|
|
4625
|
+
// Email bodies often embed images as data URLs. We allow `data:` here,
|
|
4626
|
+
// but still validate the MIME type in `sanitizeDangerousUrls`.
|
|
4627
|
+
src: [...defaultSrcProtocols, 'data']
|
|
4628
|
+
}), attributes: Object.assign(Object.assign({}, index.defaultSchema.attributes), { table: [
|
|
4629
|
+
...((_c = index.defaultSchema.attributes.table) !== null && _c !== void 0 ? _c : []),
|
|
4630
|
+
// Email HTML often relies on these legacy attributes.
|
|
4631
|
+
// rehype-parse converts to camelCase HAST properties.
|
|
4632
|
+
'cellPadding',
|
|
4633
|
+
'cellSpacing',
|
|
4634
|
+
'border',
|
|
4635
|
+
'dir',
|
|
4636
|
+
'width',
|
|
4637
|
+
'height',
|
|
4638
|
+
], font: ['color', 'size', 'face'], meta: ['charset', 'content', 'name'], colgroup: [...((_d = index.defaultSchema.attributes.colgroup) !== null && _d !== void 0 ? _d : []), 'span'], col: [...((_e = index.defaultSchema.attributes.col) !== null && _e !== void 0 ? _e : []), 'width', 'span'], '*': [
|
|
4639
|
+
...((_f = index.defaultSchema.attributes['*']) !== null && _f !== void 0 ? _f : []),
|
|
4640
|
+
'style', // Allow inline styles on all elements
|
|
4641
|
+
// NOTE: rehype/parse maps `class` to the HAST property name
|
|
4642
|
+
// `className`, which is what rehype-sanitize checks.
|
|
4643
|
+
'className',
|
|
4644
|
+
'id', // Allow id for anchors/internal navigation
|
|
4645
|
+
// Used to store remote image URLs without loading them immediately.
|
|
4646
|
+
'dataRemoteSrc',
|
|
4647
|
+
] }),
|
|
4648
|
+
// Allow common email-specific tags
|
|
4649
|
+
tagNames: [
|
|
4650
|
+
...((_g = index.defaultSchema.tagNames) !== null && _g !== void 0 ? _g : []),
|
|
4651
|
+
// Allow full-document HTML emails. These tags won't render as text,
|
|
4652
|
+
// but keeping them avoids their contents being surfaced as plain text.
|
|
4653
|
+
'html',
|
|
4654
|
+
'head',
|
|
4655
|
+
'body',
|
|
4656
|
+
'title',
|
|
4657
|
+
'meta',
|
|
4658
|
+
// Preserve embedded email CSS.
|
|
4659
|
+
'style',
|
|
4660
|
+
// Preserve table column sizing when using <colgroup>/<col>.
|
|
4661
|
+
'colgroup',
|
|
4662
|
+
'col',
|
|
4663
|
+
'center', // Deprecated but widely used in email
|
|
4664
|
+
'font', // Deprecated but widely used in email
|
|
4665
|
+
]
|
|
4666
|
+
});
|
|
4669
4667
|
/**
|
|
4670
4668
|
* Validates and normalizes potentially dangerous URL attributes.
|
|
4671
4669
|
*
|
|
@@ -35,6 +35,82 @@ class ImageIntersectionObserver {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* After innerHTML is set on a container, custom elements receive all
|
|
40
|
+
* attribute values as strings. This function walks whitelisted custom
|
|
41
|
+
* elements and parses any attribute values that look like JSON objects
|
|
42
|
+
* or arrays, setting them as JS properties instead.
|
|
43
|
+
*
|
|
44
|
+
* This enables markdown content to include custom elements with complex
|
|
45
|
+
* props, e.g.:
|
|
46
|
+
* ```html
|
|
47
|
+
* <limel-chip text="GitHub" link='{"href":"https://github.com","target":"_blank"}'></limel-chip>
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @param container - The root element to search within.
|
|
51
|
+
* @param whitelist - The list of whitelisted custom element definitions.
|
|
52
|
+
*/
|
|
53
|
+
function hydrateCustomElements(container, whitelist) {
|
|
54
|
+
if (!container || !(whitelist === null || whitelist === void 0 ? void 0 : whitelist.length)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const definition of whitelist) {
|
|
58
|
+
const elements = container.querySelectorAll(definition.tagName);
|
|
59
|
+
for (const element of elements) {
|
|
60
|
+
hydrateElement(element, definition.attributes);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function hydrateElement(element, attributes) {
|
|
65
|
+
for (const attrName of attributes) {
|
|
66
|
+
const value = element.getAttribute(attrName);
|
|
67
|
+
if (!value) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const parsed = tryParseJson(value);
|
|
71
|
+
if (parsed !== undefined) {
|
|
72
|
+
// Set the JS property (camelCase) instead of the attribute
|
|
73
|
+
const propName = attributeToPropName(attrName);
|
|
74
|
+
element[propName] = parsed;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function tryParseJson(value) {
|
|
79
|
+
const trimmed = value.trim();
|
|
80
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
81
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(trimmed);
|
|
84
|
+
}
|
|
85
|
+
catch (_a) {
|
|
86
|
+
// The sanitizer may HTML-encode quotes inside attribute values.
|
|
87
|
+
// Try decoding common HTML entities before giving up.
|
|
88
|
+
try {
|
|
89
|
+
const decoded = trimmed
|
|
90
|
+
.replaceAll('"', '"')
|
|
91
|
+
.replaceAll('"', '"')
|
|
92
|
+
.replaceAll('"', '"')
|
|
93
|
+
.replaceAll(''', "'")
|
|
94
|
+
.replaceAll(''', "'")
|
|
95
|
+
.replaceAll(''', "'")
|
|
96
|
+
.replaceAll('&', '&');
|
|
97
|
+
return JSON.parse(decoded);
|
|
98
|
+
}
|
|
99
|
+
catch (_b) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Convert a kebab-case attribute name to a camelCase property name.
|
|
107
|
+
* e.g. "menu-items" → "menuItems"
|
|
108
|
+
* @param attrName
|
|
109
|
+
*/
|
|
110
|
+
function attributeToPropName(attrName) {
|
|
111
|
+
return attrName.replaceAll(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
112
|
+
}
|
|
113
|
+
|
|
38
114
|
const markdownCss = () => `@charset "UTF-8";code{font-family:ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace;font-size:var(--limel-theme-default-small-font-size);letter-spacing:-0.0125rem;color:rgb(var(--contrast-1300));-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;display:inline-block;border-radius:0.25rem;padding:0.03125rem 0.25rem;background-color:rgb(var(--contrast-600))}pre>code{display:block;margin:0.5rem 0;padding:0.5rem 0.75rem;overflow:auto;white-space:pre-wrap}h1{font-size:1.5rem}h2{font-size:1.25rem}h3{font-size:1.125rem}h4{font-size:1rem}h5{font-size:var(--limel-theme-default-font-size)}h6{font-size:0.75rem}h1,h2{margin-top:0.5rem;margin-bottom:0.5rem;letter-spacing:-0.03125rem;font-weight:500}h3,h4{margin-top:0.75rem;margin-bottom:0.25rem;font-weight:600}h5,h6{margin-top:0.5rem;margin-bottom:0.125rem;font-weight:600}h1,h2,h3,h4,h5,h6{word-break:break-word;hyphens:auto;-webkit-hyphens:auto}:not([contenteditable=true]) h1,:not([contenteditable=true]) h2,:not([contenteditable=true]) h3,:not([contenteditable=true]) h4,:not([contenteditable=true]) h5,:not([contenteditable=true]) h6{text-wrap:balance}[contenteditable=true] h1,[contenteditable=true] h2,[contenteditable=true] h3,[contenteditable=true] h4,[contenteditable=true] h5,[contenteditable=true] h6{text-wrap:initial}:host(limel-markdown.truncate-paragraphs) p{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}p,li{font-size:var(--limel-theme-default-font-size);word-break:break-word}a{word-break:break-all}p{margin-top:0;margin-bottom:0.5rem}p:only-child{margin-bottom:0}a{transition:color 0.2s ease;color:var(--markdown-hyperlink-color, rgb(var(--color-blue-dark)));text-decoration:none}a:hover{color:var(--markdown-hyperlink-color--hovered, rgb(var(--color-blue-default)))}hr{margin:1.75rem 0 2rem 0;border-width:0;border-top:1px solid rgb(var(--contrast-500))}ul{list-style:none}ul li{position:relative;margin-left:0.75rem}ul li:before{content:"";position:absolute;left:-0.5rem;top:0.5rem;width:0.25rem;height:0.25rem;border-radius:50%;background-color:rgb(var(--contrast-700));display:block}ol{margin-top:0.25rem;padding-left:1rem}ul{margin-top:0.25rem;padding-left:0}ul ul,ul ol,ol ol,ol ul{margin-left:0}li{margin-bottom:0.25rem}:host(limel-markdown:not(.no-table-styles)) table{table-layout:auto;min-width:100%;border-collapse:collapse;border-spacing:0;background:transparent;margin:0.75rem 0}:host(limel-markdown:not(.no-table-styles)) tbody{border:1px solid rgb(var(--contrast-400));border-radius:0.25rem}:host(limel-markdown:not(.no-table-styles)) th,:host(limel-markdown:not(.no-table-styles)) td{text-align:left;vertical-align:top;transition:background-color 0.2s ease;font-size:var(--limel-theme-default-font-size)}:host(limel-markdown:not(.no-table-styles)) td{padding:0.5rem 0.375rem 0.75rem 0.375rem}:host(limel-markdown:not(.no-table-styles)) tr th{background-color:rgb(var(--contrast-400));padding:0.25rem 0.375rem;font-weight:normal}:host(limel-markdown:not(.no-table-styles)) tr th:only-child{text-align:center}:host(limel-markdown:not(.no-table-styles)) tbody tr:nth-child(odd) td{background-color:rgb(var(--contrast-200))}:host(limel-markdown:not(.no-table-styles)) tbody tr:hover td{background-color:rgb(var(--contrast-300))}table{display:block;box-sizing:border-box;overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%}blockquote{position:relative;max-width:100%;margin:0.75rem 0;padding:0.5rem;border-left:0.25rem solid rgb(var(--contrast-500));background-color:rgb(var(--contrast-200))}blockquote:before,blockquote:after{position:absolute;line-height:0;font-size:2rem;opacity:0.4}blockquote:before{content:"“";left:-0.5rem;top:0.5rem}blockquote:after{content:"”";right:-0.25rem;bottom:-0.25rem}blockquote blockquote{padding-top:0;padding-right:0;padding-bottom:0;padding-left:0.25rem;border-color:rgb(var(--contrast-700));border-left-width:1px}blockquote blockquote:before,blockquote blockquote:after{display:none}blockquote:has(>blockquote){padding-left:0.25rem;padding-bottom:0}dl{display:grid;grid-template-columns:1fr 2fr;grid-template-rows:1fr;margin-bottom:2rem;border:1px solid rgb(var(--contrast-400));border-radius:0.375rem;background-color:rgb(var(--contrast-200))}dl dt,dl dd{padding:0.375rem 0.5rem;font-size:var(--limel-theme-default-font-size);margin:0}dl dt:nth-of-type(even),dl dd:nth-of-type(even){background-color:rgb(var(--contrast-300))}dl dt:first-child{border-top-left-radius:0.375rem}dl dt:last-child{border-bottom-left-radius:0.375rem}dl dd:first-child{border-top-right-radius:0.375rem}dl dd:last-child{border-bottom-right-radius:0.375rem}img{max-width:100%;border-radius:0.25rem}kbd{font-family:ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace;font-weight:600;color:rgb(var(--contrast-1100));background-color:rgb(var(--contrast-200));white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:normal;padding:0.125rem 0.5rem;margin:0 0.25rem;box-shadow:var(--button-shadow-normal), 0 0.03125rem 0.21875rem 0 rgba(var(--contrast-100), 0.5) inset;border-radius:0.125rem;border-style:solid;border-color:rgba(var(--contrast-600), 0.8);border-width:0 1px 0.125rem 1px}:host(limel-markdown.adjust-for-table-cell) img{max-height:1.25rem;vertical-align:middle}:host(limel-markdown.adjust-for-table-cell) p{display:inline}:host(limel-markdown.adjust-for-table-cell) h1,:host(limel-markdown.adjust-for-table-cell) h2,:host(limel-markdown.adjust-for-table-cell) h3,:host(limel-markdown.adjust-for-table-cell) h4,:host(limel-markdown.adjust-for-table-cell) h5,:host(limel-markdown.adjust-for-table-cell) h6{display:inline-block;vertical-align:bottom;font-size:var(--limel-theme-default-font-size);margin:0 0.25rem 0 0;letter-spacing:normal;font-weight:500}:host(limel-markdown.adjust-for-table-cell) h1:before,:host(limel-markdown.adjust-for-table-cell) h2:before,:host(limel-markdown.adjust-for-table-cell) h3:before,:host(limel-markdown.adjust-for-table-cell) h4:before,:host(limel-markdown.adjust-for-table-cell) h5:before,:host(limel-markdown.adjust-for-table-cell) h6:before{opacity:0.6;vertical-align:middle;font-size:0.5rem;border-radius:0.25rem 0 0 0.25rem;padding:0.25rem;padding-right:2rem;margin-right:-1.75rem;background:linear-gradient(to right, rgb(var(--contrast-800), 0.6), rgb(var(--contrast-800), 0))}:host(limel-markdown.adjust-for-table-cell) h1:before{content:"H1"}:host(limel-markdown.adjust-for-table-cell) h2:before{content:"H2"}:host(limel-markdown.adjust-for-table-cell) h3:before{content:"H3"}:host(limel-markdown.adjust-for-table-cell) h4:before{content:"H4"}:host(limel-markdown.adjust-for-table-cell) h5:before{content:"H5"}:host(limel-markdown.adjust-for-table-cell) h6:before{content:"H6"}:host(limel-markdown.adjust-for-table-cell) pre{margin:0}:host(limel-markdown.adjust-for-table-cell) pre>code{padding:0.125rem;margin:0}:host(limel-markdown.adjust-for-table-cell) dl{margin:0}:host(limel-markdown.adjust-for-table-cell) dl dt,:host(limel-markdown.adjust-for-table-cell) dl dd{padding:0.00625rem 0.125rem}*,*::before,*::after{box-sizing:border-box}* :where(:not(img,video,svg,canvas,iframe)),*::before :where(:not(img,video,svg,canvas,iframe)),*::after :where(:not(img,video,svg,canvas,iframe)){min-width:0;min-height:0}hr{border-top:1px solid rgb(var(--contrast-700))}.MsoNormal{margin:0}:host(limel-markdown.reset-img-height) #markdown img{height:auto}`;
|
|
39
115
|
|
|
40
116
|
const Markdown = class {
|
|
@@ -69,7 +145,7 @@ const Markdown = class {
|
|
|
69
145
|
this.imageIntersectionObserver = null;
|
|
70
146
|
}
|
|
71
147
|
async textChanged() {
|
|
72
|
-
var _a;
|
|
148
|
+
var _a, _b;
|
|
73
149
|
try {
|
|
74
150
|
this.cleanupImageIntersectionObserver();
|
|
75
151
|
const html = await markdownParser.markdownToHTML(this.value, {
|
|
@@ -79,6 +155,7 @@ const Markdown = class {
|
|
|
79
155
|
removeEmptyParagraphs: this.removeEmptyParagraphs,
|
|
80
156
|
});
|
|
81
157
|
this.rootElement.innerHTML = html;
|
|
158
|
+
hydrateCustomElements(this.rootElement, (_b = this.whitelist) !== null && _b !== void 0 ? _b : []);
|
|
82
159
|
this.setupImageIntersectionObserver();
|
|
83
160
|
}
|
|
84
161
|
catch (error) {
|
|
@@ -95,7 +172,7 @@ const Markdown = class {
|
|
|
95
172
|
this.cleanupImageIntersectionObserver();
|
|
96
173
|
}
|
|
97
174
|
render() {
|
|
98
|
-
return (index.h(index.Host, { key: '
|
|
175
|
+
return (index.h(index.Host, { key: 'd7e3122596fa19c63e05d69855fbc693cb8c4fe7' }, index.h("div", { key: '9a65798b0888a3d2d7a35c0d320408faa3d937b6', id: "markdown", ref: (el) => (this.rootElement = el) })));
|
|
99
176
|
}
|
|
100
177
|
setupImageIntersectionObserver() {
|
|
101
178
|
if (this.lazyLoadImages) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1
2
|
import { unified } from "unified";
|
|
2
3
|
import rehypeParse from "rehype-parse";
|
|
3
4
|
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
|
|
@@ -28,7 +29,7 @@ const allowedMimeTypes = new Set([
|
|
|
28
29
|
export async function sanitizeEmailHTML(html) {
|
|
29
30
|
const file = await unified()
|
|
30
31
|
.use(rehypeParse)
|
|
31
|
-
.use(rehypeSanitize,
|
|
32
|
+
.use(rehypeSanitize, emailSanitizationSchema)
|
|
32
33
|
.use(() => {
|
|
33
34
|
return (tree) => {
|
|
34
35
|
visit(tree, 'element', (node) => {
|
|
@@ -41,65 +42,62 @@ export async function sanitizeEmailHTML(html) {
|
|
|
41
42
|
.process(html);
|
|
42
43
|
return file.toString();
|
|
43
44
|
}
|
|
45
|
+
// Base src protocols from defaultSchema, extended with 'data' below.
|
|
46
|
+
const defaultSrcProtocols = (_b = (_a = defaultSchema.protocols) === null || _a === void 0 ? void 0 : _a.src) !== null && _b !== void 0 ? _b : [];
|
|
44
47
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
48
|
+
* Rehype-sanitize schema that allows all standard HTML elements and attributes
|
|
49
|
+
* needed for rich email rendering, including `style`.
|
|
50
|
+
*
|
|
51
|
+
* Hoisted to module scope since the schema has no runtime dependencies and
|
|
52
|
+
* doesn't need to be reconstructed on every sanitization call.
|
|
47
53
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
'colgroup',
|
|
96
|
-
'col',
|
|
97
|
-
'center', // Deprecated but widely used in email
|
|
98
|
-
'font', // Deprecated but widely used in email
|
|
99
|
-
]
|
|
100
|
-
});
|
|
101
|
-
return schema;
|
|
102
|
-
}
|
|
54
|
+
const emailSanitizationSchema = Object.assign(Object.assign({}, defaultSchema), {
|
|
55
|
+
// Disable the 'user-content-' prefix that rehype-sanitize adds to
|
|
56
|
+
// id and name attributes. Email HTML uses ids for internal anchor
|
|
57
|
+
// links (href="#section") that must resolve without a prefix.
|
|
58
|
+
clobberPrefix: '', protocols: Object.assign(Object.assign({}, defaultSchema.protocols), {
|
|
59
|
+
// Email bodies often embed images as data URLs. We allow `data:` here,
|
|
60
|
+
// but still validate the MIME type in `sanitizeDangerousUrls`.
|
|
61
|
+
src: [...defaultSrcProtocols, 'data']
|
|
62
|
+
}), attributes: Object.assign(Object.assign({}, defaultSchema.attributes), { table: [
|
|
63
|
+
...((_c = defaultSchema.attributes.table) !== null && _c !== void 0 ? _c : []),
|
|
64
|
+
// Email HTML often relies on these legacy attributes.
|
|
65
|
+
// rehype-parse converts to camelCase HAST properties.
|
|
66
|
+
'cellPadding',
|
|
67
|
+
'cellSpacing',
|
|
68
|
+
'border',
|
|
69
|
+
'dir',
|
|
70
|
+
'width',
|
|
71
|
+
'height',
|
|
72
|
+
], font: ['color', 'size', 'face'], meta: ['charset', 'content', 'name'], colgroup: [...((_d = defaultSchema.attributes.colgroup) !== null && _d !== void 0 ? _d : []), 'span'], col: [...((_e = defaultSchema.attributes.col) !== null && _e !== void 0 ? _e : []), 'width', 'span'], '*': [
|
|
73
|
+
...((_f = defaultSchema.attributes['*']) !== null && _f !== void 0 ? _f : []),
|
|
74
|
+
'style', // Allow inline styles on all elements
|
|
75
|
+
// NOTE: rehype/parse maps `class` to the HAST property name
|
|
76
|
+
// `className`, which is what rehype-sanitize checks.
|
|
77
|
+
'className',
|
|
78
|
+
'id', // Allow id for anchors/internal navigation
|
|
79
|
+
// Used to store remote image URLs without loading them immediately.
|
|
80
|
+
'dataRemoteSrc',
|
|
81
|
+
] }),
|
|
82
|
+
// Allow common email-specific tags
|
|
83
|
+
tagNames: [
|
|
84
|
+
...((_g = defaultSchema.tagNames) !== null && _g !== void 0 ? _g : []),
|
|
85
|
+
// Allow full-document HTML emails. These tags won't render as text,
|
|
86
|
+
// but keeping them avoids their contents being surfaced as plain text.
|
|
87
|
+
'html',
|
|
88
|
+
'head',
|
|
89
|
+
'body',
|
|
90
|
+
'title',
|
|
91
|
+
'meta',
|
|
92
|
+
// Preserve embedded email CSS.
|
|
93
|
+
'style',
|
|
94
|
+
// Preserve table column sizing when using <colgroup>/<col>.
|
|
95
|
+
'colgroup',
|
|
96
|
+
'col',
|
|
97
|
+
'center', // Deprecated but widely used in email
|
|
98
|
+
'font', // Deprecated but widely used in email
|
|
99
|
+
]
|
|
100
|
+
});
|
|
103
101
|
/**
|
|
104
102
|
* Validates and normalizes potentially dangerous URL attributes.
|
|
105
103
|
*
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* After innerHTML is set on a container, custom elements receive all
|
|
3
|
+
* attribute values as strings. This function walks whitelisted custom
|
|
4
|
+
* elements and parses any attribute values that look like JSON objects
|
|
5
|
+
* or arrays, setting them as JS properties instead.
|
|
6
|
+
*
|
|
7
|
+
* This enables markdown content to include custom elements with complex
|
|
8
|
+
* props, e.g.:
|
|
9
|
+
* ```html
|
|
10
|
+
* <limel-chip text="GitHub" link='{"href":"https://github.com","target":"_blank"}'></limel-chip>
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* @param container - The root element to search within.
|
|
14
|
+
* @param whitelist - The list of whitelisted custom element definitions.
|
|
15
|
+
*/
|
|
16
|
+
export function hydrateCustomElements(container, whitelist) {
|
|
17
|
+
if (!container || !(whitelist === null || whitelist === void 0 ? void 0 : whitelist.length)) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
for (const definition of whitelist) {
|
|
21
|
+
const elements = container.querySelectorAll(definition.tagName);
|
|
22
|
+
for (const element of elements) {
|
|
23
|
+
hydrateElement(element, definition.attributes);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function hydrateElement(element, attributes) {
|
|
28
|
+
for (const attrName of attributes) {
|
|
29
|
+
const value = element.getAttribute(attrName);
|
|
30
|
+
if (!value) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const parsed = tryParseJson(value);
|
|
34
|
+
if (parsed !== undefined) {
|
|
35
|
+
// Set the JS property (camelCase) instead of the attribute
|
|
36
|
+
const propName = attributeToPropName(attrName);
|
|
37
|
+
element[propName] = parsed;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function tryParseJson(value) {
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
44
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(trimmed);
|
|
47
|
+
}
|
|
48
|
+
catch (_a) {
|
|
49
|
+
// The sanitizer may HTML-encode quotes inside attribute values.
|
|
50
|
+
// Try decoding common HTML entities before giving up.
|
|
51
|
+
try {
|
|
52
|
+
const decoded = trimmed
|
|
53
|
+
.replaceAll('"', '"')
|
|
54
|
+
.replaceAll('"', '"')
|
|
55
|
+
.replaceAll('"', '"')
|
|
56
|
+
.replaceAll(''', "'")
|
|
57
|
+
.replaceAll(''', "'")
|
|
58
|
+
.replaceAll(''', "'")
|
|
59
|
+
.replaceAll('&', '&');
|
|
60
|
+
return JSON.parse(decoded);
|
|
61
|
+
}
|
|
62
|
+
catch (_b) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Convert a kebab-case attribute name to a camelCase property name.
|
|
70
|
+
* e.g. "menu-items" → "menuItems"
|
|
71
|
+
* @param attrName
|
|
72
|
+
*/
|
|
73
|
+
function attributeToPropName(attrName) {
|
|
74
|
+
return attrName.replaceAll(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
75
|
+
}
|
|
@@ -2,6 +2,7 @@ import { h, Host } from "@stencil/core";
|
|
|
2
2
|
import { markdownToHTML } from "./markdown-parser";
|
|
3
3
|
import { globalConfig } from "../../global/config";
|
|
4
4
|
import { ImageIntersectionObserver } from "./image-intersection-observer";
|
|
5
|
+
import { hydrateCustomElements } from "./hydrate-custom-elements";
|
|
5
6
|
/**
|
|
6
7
|
* The Markdown component receives markdown syntax
|
|
7
8
|
* and renders it as HTML.
|
|
@@ -19,6 +20,7 @@ import { ImageIntersectionObserver } from "./image-intersection-observer";
|
|
|
19
20
|
* @exampleComponent limel-example-markdown-blockquotes
|
|
20
21
|
* @exampleComponent limel-example-markdown-horizontal-rule
|
|
21
22
|
* @exampleComponent limel-example-markdown-custom-component
|
|
23
|
+
* @exampleComponent limel-example-markdown-custom-component-with-json-props
|
|
22
24
|
* @exampleComponent limel-example-markdown-remove-empty-paragraphs
|
|
23
25
|
* @exampleComponent limel-example-markdown-composite
|
|
24
26
|
*/
|
|
@@ -53,7 +55,7 @@ export class Markdown {
|
|
|
53
55
|
this.imageIntersectionObserver = null;
|
|
54
56
|
}
|
|
55
57
|
async textChanged() {
|
|
56
|
-
var _a;
|
|
58
|
+
var _a, _b;
|
|
57
59
|
try {
|
|
58
60
|
this.cleanupImageIntersectionObserver();
|
|
59
61
|
const html = await markdownToHTML(this.value, {
|
|
@@ -63,6 +65,7 @@ export class Markdown {
|
|
|
63
65
|
removeEmptyParagraphs: this.removeEmptyParagraphs,
|
|
64
66
|
});
|
|
65
67
|
this.rootElement.innerHTML = html;
|
|
68
|
+
hydrateCustomElements(this.rootElement, (_b = this.whitelist) !== null && _b !== void 0 ? _b : []);
|
|
66
69
|
this.setupImageIntersectionObserver();
|
|
67
70
|
}
|
|
68
71
|
catch (error) {
|
|
@@ -79,7 +82,7 @@ export class Markdown {
|
|
|
79
82
|
this.cleanupImageIntersectionObserver();
|
|
80
83
|
}
|
|
81
84
|
render() {
|
|
82
|
-
return (h(Host, { key: '
|
|
85
|
+
return (h(Host, { key: 'd7e3122596fa19c63e05d69855fbc693cb8c4fe7' }, h("div", { key: '9a65798b0888a3d2d7a35c0d320408faa3d937b6', id: "markdown", ref: (el) => (this.rootElement = el) })));
|
|
83
86
|
}
|
|
84
87
|
setupImageIntersectionObserver() {
|
|
85
88
|
if (this.lazyLoadImages) {
|