@wdprlib/render 1.1.0 → 1.2.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/dist/index.cjs +22 -11
- package/dist/index.d.cts +86 -26
- package/dist/index.d.ts +86 -26
- package/dist/index.js +22 -11
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -324,11 +324,17 @@ function isValidCssColor(color) {
|
|
|
324
324
|
if (/^#[0-9a-f]{3}([0-9a-f])?$/.test(trimmed) || /^#[0-9a-f]{6}([0-9a-f]{2})?$/.test(trimmed)) {
|
|
325
325
|
return true;
|
|
326
326
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
327
|
+
const fnMatch = trimmed.match(/^(rgba?|hsla?)\(([^)]*)\)$/);
|
|
328
|
+
if (fnMatch) {
|
|
329
|
+
const fn = fnMatch[1];
|
|
330
|
+
const args = fnMatch[2].split(",").map((s) => s.trim()).join(",");
|
|
331
|
+
if (fn.startsWith("rgb")) {
|
|
332
|
+
if (/^\d{1,3},\d{1,3},\d{1,3}(,(0|1|0?\.\d+))?$/.test(args))
|
|
333
|
+
return true;
|
|
334
|
+
} else {
|
|
335
|
+
if (/^\d{1,3},\d{1,3}%,\d{1,3}%(,(0|1|0?\.\d+))?$/.test(args))
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
332
338
|
}
|
|
333
339
|
return false;
|
|
334
340
|
}
|
|
@@ -4117,7 +4123,7 @@ function renderJoin(ctx, data) {
|
|
|
4117
4123
|
const buttonText = data["button-text"] ?? "Join";
|
|
4118
4124
|
const attrs = data.attributes ?? {};
|
|
4119
4125
|
const className = attrs.class ?? "join-box";
|
|
4120
|
-
ctx.push(`<div class="${
|
|
4126
|
+
ctx.push(`<div class="${escapeAttr(className)}">`);
|
|
4121
4127
|
ctx.push(`<a href="javascript:;">${escapeHtml(buttonText)}</a>`);
|
|
4122
4128
|
ctx.push("</div>");
|
|
4123
4129
|
}
|
|
@@ -4294,7 +4300,7 @@ var SANITIZE_CONFIG = {
|
|
|
4294
4300
|
"width"
|
|
4295
4301
|
]
|
|
4296
4302
|
},
|
|
4297
|
-
allowedSchemes: ["https"]
|
|
4303
|
+
allowedSchemes: ["https", "http"]
|
|
4298
4304
|
};
|
|
4299
4305
|
function findIframes(html) {
|
|
4300
4306
|
const doc = import_htmlparser2.parseDocument(html);
|
|
@@ -4342,7 +4348,7 @@ function matchesAllowlistEntry(url, entry) {
|
|
|
4342
4348
|
}
|
|
4343
4349
|
return true;
|
|
4344
4350
|
}
|
|
4345
|
-
function validateAndSanitizeEmbed(content, allowlist) {
|
|
4351
|
+
function validateAndSanitizeEmbed(content, allowlist, baseUrl) {
|
|
4346
4352
|
const sanitized = import_sanitize_html.default(content.trim(), SANITIZE_CONFIG);
|
|
4347
4353
|
if (!sanitized.trim()) {
|
|
4348
4354
|
return null;
|
|
@@ -4358,11 +4364,16 @@ function validateAndSanitizeEmbed(content, allowlist) {
|
|
|
4358
4364
|
}
|
|
4359
4365
|
let url;
|
|
4360
4366
|
try {
|
|
4361
|
-
|
|
4367
|
+
if (src.startsWith("//")) {
|
|
4368
|
+
const base = baseUrl ?? "https://localhost";
|
|
4369
|
+
url = new URL(src, base);
|
|
4370
|
+
} else {
|
|
4371
|
+
url = new URL(src);
|
|
4372
|
+
}
|
|
4362
4373
|
} catch {
|
|
4363
4374
|
return null;
|
|
4364
4375
|
}
|
|
4365
|
-
if (url.protocol !== "https:") {
|
|
4376
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
4366
4377
|
return null;
|
|
4367
4378
|
}
|
|
4368
4379
|
if (allowlist !== null) {
|
|
@@ -4385,7 +4396,7 @@ function normalizeBooleanAttributes(html) {
|
|
|
4385
4396
|
}
|
|
4386
4397
|
function renderEmbedBlock(ctx, data) {
|
|
4387
4398
|
const allowlist = ctx.options.embedAllowlist !== undefined ? ctx.options.embedAllowlist : DEFAULT_EMBED_ALLOWLIST;
|
|
4388
|
-
const sanitized = validateAndSanitizeEmbed(data.contents, allowlist);
|
|
4399
|
+
const sanitized = validateAndSanitizeEmbed(data.contents, allowlist, ctx.options.baseUrl);
|
|
4389
4400
|
if (sanitized === null) {
|
|
4390
4401
|
ctx.push('<div class="error-block">Sorry, no match for the embedded content.</div>');
|
|
4391
4402
|
return;
|
package/dist/index.d.cts
CHANGED
|
@@ -19,64 +19,113 @@ interface EmbedAllowlistEntry {
|
|
|
19
19
|
*/
|
|
20
20
|
declare const DEFAULT_EMBED_ALLOWLIST: EmbedAllowlistEntry[] | null;
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Contextual information about the wiki page being rendered.
|
|
23
|
+
*
|
|
24
|
+
* The renderer uses this to resolve relative links, local file paths,
|
|
25
|
+
* page-existence checks (adding a `"newpage"` CSS class to links
|
|
26
|
+
* targeting non-existent pages), and `[[iftags]]` evaluation.
|
|
27
|
+
*
|
|
28
|
+
* @group Render Options
|
|
23
29
|
*/
|
|
24
30
|
interface PageContext {
|
|
25
|
-
/**
|
|
31
|
+
/** Full page name including category prefix (e.g. `"secret:test2"`) */
|
|
26
32
|
pageName: string;
|
|
27
|
-
/** Site slug (e.g
|
|
33
|
+
/** Site slug used to build inter-site URLs (e.g. `"scp-wiki"`) */
|
|
28
34
|
site?: string;
|
|
29
|
-
/** Site domain (e.g
|
|
35
|
+
/** Site domain used for absolute URL generation (e.g. `"scp-wiki.wikidot.com"`) */
|
|
30
36
|
domain?: string;
|
|
31
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Returns whether a page exists on the site.
|
|
39
|
+
* When a target page does not exist, the renderer adds `class="newpage"`
|
|
40
|
+
* to the link element — the standard Wikidot convention for red-links.
|
|
41
|
+
*/
|
|
32
42
|
pageExists?: (page: string) => boolean;
|
|
33
|
-
/** Page tags for [[iftags]]
|
|
43
|
+
/** Page tags used for client-side `[[iftags]]` evaluation during rendering */
|
|
34
44
|
tags?: string[];
|
|
35
45
|
}
|
|
36
46
|
/**
|
|
37
|
-
*
|
|
47
|
+
* User profile data returned by a user-resolver callback.
|
|
48
|
+
*
|
|
49
|
+
* Passed to the renderer to produce the Wikidot user-info markup
|
|
50
|
+
* (`[[user username]]`). When a field is omitted the corresponding
|
|
51
|
+
* UI element is simply not emitted.
|
|
52
|
+
*
|
|
53
|
+
* @group Render Options
|
|
38
54
|
*/
|
|
39
55
|
interface ResolvedUser {
|
|
40
|
-
/**
|
|
56
|
+
/** Display name shown in the rendered output (falls back to the raw username) */
|
|
41
57
|
name?: string;
|
|
42
|
-
/**
|
|
58
|
+
/** Profile URL. When omitted the username is rendered as a non-navigable `<span>` */
|
|
43
59
|
url?: string;
|
|
44
|
-
/** Avatar image URL.
|
|
60
|
+
/** Avatar image URL. When omitted no avatar `<img>` is rendered */
|
|
45
61
|
avatarUrl?: string;
|
|
46
|
-
/** Karma image URL
|
|
62
|
+
/** Karma-badge image URL shown behind the avatar (Wikidot-specific feature) */
|
|
47
63
|
karmaUrl?: string;
|
|
48
64
|
}
|
|
49
65
|
/**
|
|
50
|
-
*
|
|
66
|
+
* Async/sync resolver callbacks for content that depends on external data.
|
|
67
|
+
*
|
|
68
|
+
* Unlike the parser's `DataProvider` (which fetches bulk data for
|
|
69
|
+
* module expansion), these resolvers are called per-element during the
|
|
70
|
+
* rendering pass.
|
|
71
|
+
*
|
|
72
|
+
* @group Render Options
|
|
51
73
|
*/
|
|
52
74
|
interface RenderResolvers {
|
|
53
75
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
76
|
+
* Look up a user profile by username.
|
|
77
|
+
*
|
|
78
|
+
* @param username - The raw username from `[[user username]]`
|
|
79
|
+
* @returns Profile data for rendering, or `null` if the user is unknown.
|
|
80
|
+
* When `null` or when the resolver is omitted, the username is
|
|
81
|
+
* rendered as plain text.
|
|
57
82
|
*/
|
|
58
83
|
user?: (username: string) => ResolvedUser | null;
|
|
59
84
|
/**
|
|
60
|
-
*
|
|
61
|
-
* Called with the index of the htmlBlock (0-based, matching tree["html-blocks"] order).
|
|
62
|
-
* If not provided or returns empty string, uses default pattern: /{pageName}/html/{hash}-{nonce}
|
|
85
|
+
* Build an iframe `src` URL for an `[[html]]` block.
|
|
63
86
|
*
|
|
64
|
-
*
|
|
65
|
-
* The
|
|
87
|
+
* @param index - Zero-based index matching `SyntaxTree["html-blocks"]`
|
|
88
|
+
* @returns The URL string. When empty or when the resolver is omitted,
|
|
89
|
+
* the default pattern `/{pageName}/html/{hash}-{nonce}` is used.
|
|
90
|
+
*
|
|
91
|
+
* @security The returned URL is injected directly into the iframe `src`
|
|
92
|
+
* attribute. The caller must validate the scheme to reject `javascript:`,
|
|
93
|
+
* `data:`, and other dangerous protocols.
|
|
66
94
|
*/
|
|
67
95
|
htmlBlockUrl?: (index: number) => string;
|
|
68
96
|
}
|
|
69
97
|
/**
|
|
70
|
-
*
|
|
98
|
+
* Full configuration for `renderToHtml()`.
|
|
99
|
+
*
|
|
100
|
+
* Every field is optional; defaults produce safe, standalone HTML output
|
|
101
|
+
* suitable for a full wiki page.
|
|
102
|
+
*
|
|
103
|
+
* @group Render Options
|
|
71
104
|
*/
|
|
72
105
|
interface RenderOptions {
|
|
73
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Base URL for resolving protocol-relative URLs (e.g. `"//example.com/path"`).
|
|
108
|
+
*
|
|
109
|
+
* The scheme of this URL (`http:` or `https:`) is prepended to
|
|
110
|
+
* protocol-relative references. When omitted, HTTPS is assumed.
|
|
111
|
+
*
|
|
112
|
+
* @example "https://scp-wiki.wikidot.com"
|
|
113
|
+
*/
|
|
114
|
+
baseUrl?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Context-dependent feature flags.
|
|
117
|
+
* Defaults to page-mode settings when omitted.
|
|
118
|
+
*/
|
|
74
119
|
settings?: WikitextSettings;
|
|
75
|
-
/** Page context for resolving file paths,
|
|
120
|
+
/** Page context for resolving relative links, local file paths, etc. */
|
|
76
121
|
page?: PageContext;
|
|
77
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Pre-collected footnote element arrays from `SyntaxTree.footnotes`.
|
|
124
|
+
* Passed through so the renderer can emit footnote bodies in the
|
|
125
|
+
* `[[footnoteblock]]` section.
|
|
126
|
+
*/
|
|
78
127
|
footnotes?: Element[][];
|
|
79
|
-
/**
|
|
128
|
+
/** Callbacks for resolving users, HTML-block URLs, etc. */
|
|
80
129
|
resolvers?: RenderResolvers;
|
|
81
130
|
/**
|
|
82
131
|
* Sandbox attribute value for htmlBlock iframes.
|
|
@@ -99,7 +148,18 @@ interface RenderOptions {
|
|
|
99
148
|
embedAllowlist?: EmbedAllowlistEntry[] | null;
|
|
100
149
|
}
|
|
101
150
|
/**
|
|
102
|
-
* Render a SyntaxTree to HTML string
|
|
151
|
+
* Render a {@link SyntaxTree} to an HTML string.
|
|
152
|
+
*
|
|
153
|
+
* This is the main entry point of `@wdprlib/render`. It walks the AST
|
|
154
|
+
* produced by `@wdprlib/parser`, serialises each element to HTML, and
|
|
155
|
+
* appends any collected `[[module CSS]]` styles at the end (when
|
|
156
|
+
* `WikitextSettings.allowStyleElements` is `true`).
|
|
157
|
+
*
|
|
158
|
+
* @param tree - Parsed AST (from `parse()` or `resolveModules()`)
|
|
159
|
+
* @param options - Rendering configuration
|
|
160
|
+
* @returns Complete HTML string
|
|
161
|
+
*
|
|
162
|
+
* @group Render
|
|
103
163
|
*/
|
|
104
164
|
declare function renderToHtml(tree: SyntaxTree2, options?: RenderOptions): string;
|
|
105
165
|
import { WikitextMode, WikitextSettings as WikitextSettings3 } from "@wdprlib/ast";
|
package/dist/index.d.ts
CHANGED
|
@@ -19,64 +19,113 @@ interface EmbedAllowlistEntry {
|
|
|
19
19
|
*/
|
|
20
20
|
declare const DEFAULT_EMBED_ALLOWLIST: EmbedAllowlistEntry[] | null;
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Contextual information about the wiki page being rendered.
|
|
23
|
+
*
|
|
24
|
+
* The renderer uses this to resolve relative links, local file paths,
|
|
25
|
+
* page-existence checks (adding a `"newpage"` CSS class to links
|
|
26
|
+
* targeting non-existent pages), and `[[iftags]]` evaluation.
|
|
27
|
+
*
|
|
28
|
+
* @group Render Options
|
|
23
29
|
*/
|
|
24
30
|
interface PageContext {
|
|
25
|
-
/**
|
|
31
|
+
/** Full page name including category prefix (e.g. `"secret:test2"`) */
|
|
26
32
|
pageName: string;
|
|
27
|
-
/** Site slug (e.g
|
|
33
|
+
/** Site slug used to build inter-site URLs (e.g. `"scp-wiki"`) */
|
|
28
34
|
site?: string;
|
|
29
|
-
/** Site domain (e.g
|
|
35
|
+
/** Site domain used for absolute URL generation (e.g. `"scp-wiki.wikidot.com"`) */
|
|
30
36
|
domain?: string;
|
|
31
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Returns whether a page exists on the site.
|
|
39
|
+
* When a target page does not exist, the renderer adds `class="newpage"`
|
|
40
|
+
* to the link element — the standard Wikidot convention for red-links.
|
|
41
|
+
*/
|
|
32
42
|
pageExists?: (page: string) => boolean;
|
|
33
|
-
/** Page tags for [[iftags]]
|
|
43
|
+
/** Page tags used for client-side `[[iftags]]` evaluation during rendering */
|
|
34
44
|
tags?: string[];
|
|
35
45
|
}
|
|
36
46
|
/**
|
|
37
|
-
*
|
|
47
|
+
* User profile data returned by a user-resolver callback.
|
|
48
|
+
*
|
|
49
|
+
* Passed to the renderer to produce the Wikidot user-info markup
|
|
50
|
+
* (`[[user username]]`). When a field is omitted the corresponding
|
|
51
|
+
* UI element is simply not emitted.
|
|
52
|
+
*
|
|
53
|
+
* @group Render Options
|
|
38
54
|
*/
|
|
39
55
|
interface ResolvedUser {
|
|
40
|
-
/**
|
|
56
|
+
/** Display name shown in the rendered output (falls back to the raw username) */
|
|
41
57
|
name?: string;
|
|
42
|
-
/**
|
|
58
|
+
/** Profile URL. When omitted the username is rendered as a non-navigable `<span>` */
|
|
43
59
|
url?: string;
|
|
44
|
-
/** Avatar image URL.
|
|
60
|
+
/** Avatar image URL. When omitted no avatar `<img>` is rendered */
|
|
45
61
|
avatarUrl?: string;
|
|
46
|
-
/** Karma image URL
|
|
62
|
+
/** Karma-badge image URL shown behind the avatar (Wikidot-specific feature) */
|
|
47
63
|
karmaUrl?: string;
|
|
48
64
|
}
|
|
49
65
|
/**
|
|
50
|
-
*
|
|
66
|
+
* Async/sync resolver callbacks for content that depends on external data.
|
|
67
|
+
*
|
|
68
|
+
* Unlike the parser's `DataProvider` (which fetches bulk data for
|
|
69
|
+
* module expansion), these resolvers are called per-element during the
|
|
70
|
+
* rendering pass.
|
|
71
|
+
*
|
|
72
|
+
* @group Render Options
|
|
51
73
|
*/
|
|
52
74
|
interface RenderResolvers {
|
|
53
75
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
76
|
+
* Look up a user profile by username.
|
|
77
|
+
*
|
|
78
|
+
* @param username - The raw username from `[[user username]]`
|
|
79
|
+
* @returns Profile data for rendering, or `null` if the user is unknown.
|
|
80
|
+
* When `null` or when the resolver is omitted, the username is
|
|
81
|
+
* rendered as plain text.
|
|
57
82
|
*/
|
|
58
83
|
user?: (username: string) => ResolvedUser | null;
|
|
59
84
|
/**
|
|
60
|
-
*
|
|
61
|
-
* Called with the index of the htmlBlock (0-based, matching tree["html-blocks"] order).
|
|
62
|
-
* If not provided or returns empty string, uses default pattern: /{pageName}/html/{hash}-{nonce}
|
|
85
|
+
* Build an iframe `src` URL for an `[[html]]` block.
|
|
63
86
|
*
|
|
64
|
-
*
|
|
65
|
-
* The
|
|
87
|
+
* @param index - Zero-based index matching `SyntaxTree["html-blocks"]`
|
|
88
|
+
* @returns The URL string. When empty or when the resolver is omitted,
|
|
89
|
+
* the default pattern `/{pageName}/html/{hash}-{nonce}` is used.
|
|
90
|
+
*
|
|
91
|
+
* @security The returned URL is injected directly into the iframe `src`
|
|
92
|
+
* attribute. The caller must validate the scheme to reject `javascript:`,
|
|
93
|
+
* `data:`, and other dangerous protocols.
|
|
66
94
|
*/
|
|
67
95
|
htmlBlockUrl?: (index: number) => string;
|
|
68
96
|
}
|
|
69
97
|
/**
|
|
70
|
-
*
|
|
98
|
+
* Full configuration for `renderToHtml()`.
|
|
99
|
+
*
|
|
100
|
+
* Every field is optional; defaults produce safe, standalone HTML output
|
|
101
|
+
* suitable for a full wiki page.
|
|
102
|
+
*
|
|
103
|
+
* @group Render Options
|
|
71
104
|
*/
|
|
72
105
|
interface RenderOptions {
|
|
73
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Base URL for resolving protocol-relative URLs (e.g. `"//example.com/path"`).
|
|
108
|
+
*
|
|
109
|
+
* The scheme of this URL (`http:` or `https:`) is prepended to
|
|
110
|
+
* protocol-relative references. When omitted, HTTPS is assumed.
|
|
111
|
+
*
|
|
112
|
+
* @example "https://scp-wiki.wikidot.com"
|
|
113
|
+
*/
|
|
114
|
+
baseUrl?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Context-dependent feature flags.
|
|
117
|
+
* Defaults to page-mode settings when omitted.
|
|
118
|
+
*/
|
|
74
119
|
settings?: WikitextSettings;
|
|
75
|
-
/** Page context for resolving file paths,
|
|
120
|
+
/** Page context for resolving relative links, local file paths, etc. */
|
|
76
121
|
page?: PageContext;
|
|
77
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Pre-collected footnote element arrays from `SyntaxTree.footnotes`.
|
|
124
|
+
* Passed through so the renderer can emit footnote bodies in the
|
|
125
|
+
* `[[footnoteblock]]` section.
|
|
126
|
+
*/
|
|
78
127
|
footnotes?: Element[][];
|
|
79
|
-
/**
|
|
128
|
+
/** Callbacks for resolving users, HTML-block URLs, etc. */
|
|
80
129
|
resolvers?: RenderResolvers;
|
|
81
130
|
/**
|
|
82
131
|
* Sandbox attribute value for htmlBlock iframes.
|
|
@@ -99,7 +148,18 @@ interface RenderOptions {
|
|
|
99
148
|
embedAllowlist?: EmbedAllowlistEntry[] | null;
|
|
100
149
|
}
|
|
101
150
|
/**
|
|
102
|
-
* Render a SyntaxTree to HTML string
|
|
151
|
+
* Render a {@link SyntaxTree} to an HTML string.
|
|
152
|
+
*
|
|
153
|
+
* This is the main entry point of `@wdprlib/render`. It walks the AST
|
|
154
|
+
* produced by `@wdprlib/parser`, serialises each element to HTML, and
|
|
155
|
+
* appends any collected `[[module CSS]]` styles at the end (when
|
|
156
|
+
* `WikitextSettings.allowStyleElements` is `true`).
|
|
157
|
+
*
|
|
158
|
+
* @param tree - Parsed AST (from `parse()` or `resolveModules()`)
|
|
159
|
+
* @param options - Rendering configuration
|
|
160
|
+
* @returns Complete HTML string
|
|
161
|
+
*
|
|
162
|
+
* @group Render
|
|
103
163
|
*/
|
|
104
164
|
declare function renderToHtml(tree: SyntaxTree2, options?: RenderOptions): string;
|
|
105
165
|
import { WikitextMode, WikitextSettings as WikitextSettings3 } from "@wdprlib/ast";
|
package/dist/index.js
CHANGED
|
@@ -272,11 +272,17 @@ function isValidCssColor(color) {
|
|
|
272
272
|
if (/^#[0-9a-f]{3}([0-9a-f])?$/.test(trimmed) || /^#[0-9a-f]{6}([0-9a-f]{2})?$/.test(trimmed)) {
|
|
273
273
|
return true;
|
|
274
274
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
275
|
+
const fnMatch = trimmed.match(/^(rgba?|hsla?)\(([^)]*)\)$/);
|
|
276
|
+
if (fnMatch) {
|
|
277
|
+
const fn = fnMatch[1];
|
|
278
|
+
const args = fnMatch[2].split(",").map((s) => s.trim()).join(",");
|
|
279
|
+
if (fn.startsWith("rgb")) {
|
|
280
|
+
if (/^\d{1,3},\d{1,3},\d{1,3}(,(0|1|0?\.\d+))?$/.test(args))
|
|
281
|
+
return true;
|
|
282
|
+
} else {
|
|
283
|
+
if (/^\d{1,3},\d{1,3}%,\d{1,3}%(,(0|1|0?\.\d+))?$/.test(args))
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
280
286
|
}
|
|
281
287
|
return false;
|
|
282
288
|
}
|
|
@@ -4065,7 +4071,7 @@ function renderJoin(ctx, data) {
|
|
|
4065
4071
|
const buttonText = data["button-text"] ?? "Join";
|
|
4066
4072
|
const attrs = data.attributes ?? {};
|
|
4067
4073
|
const className = attrs.class ?? "join-box";
|
|
4068
|
-
ctx.push(`<div class="${
|
|
4074
|
+
ctx.push(`<div class="${escapeAttr(className)}">`);
|
|
4069
4075
|
ctx.push(`<a href="javascript:;">${escapeHtml(buttonText)}</a>`);
|
|
4070
4076
|
ctx.push("</div>");
|
|
4071
4077
|
}
|
|
@@ -4242,7 +4248,7 @@ var SANITIZE_CONFIG = {
|
|
|
4242
4248
|
"width"
|
|
4243
4249
|
]
|
|
4244
4250
|
},
|
|
4245
|
-
allowedSchemes: ["https"]
|
|
4251
|
+
allowedSchemes: ["https", "http"]
|
|
4246
4252
|
};
|
|
4247
4253
|
function findIframes(html) {
|
|
4248
4254
|
const doc = parseDocument(html);
|
|
@@ -4290,7 +4296,7 @@ function matchesAllowlistEntry(url, entry) {
|
|
|
4290
4296
|
}
|
|
4291
4297
|
return true;
|
|
4292
4298
|
}
|
|
4293
|
-
function validateAndSanitizeEmbed(content, allowlist) {
|
|
4299
|
+
function validateAndSanitizeEmbed(content, allowlist, baseUrl) {
|
|
4294
4300
|
const sanitized = sanitizeHtml(content.trim(), SANITIZE_CONFIG);
|
|
4295
4301
|
if (!sanitized.trim()) {
|
|
4296
4302
|
return null;
|
|
@@ -4306,11 +4312,16 @@ function validateAndSanitizeEmbed(content, allowlist) {
|
|
|
4306
4312
|
}
|
|
4307
4313
|
let url;
|
|
4308
4314
|
try {
|
|
4309
|
-
|
|
4315
|
+
if (src.startsWith("//")) {
|
|
4316
|
+
const base = baseUrl ?? "https://localhost";
|
|
4317
|
+
url = new URL(src, base);
|
|
4318
|
+
} else {
|
|
4319
|
+
url = new URL(src);
|
|
4320
|
+
}
|
|
4310
4321
|
} catch {
|
|
4311
4322
|
return null;
|
|
4312
4323
|
}
|
|
4313
|
-
if (url.protocol !== "https:") {
|
|
4324
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
4314
4325
|
return null;
|
|
4315
4326
|
}
|
|
4316
4327
|
if (allowlist !== null) {
|
|
@@ -4333,7 +4344,7 @@ function normalizeBooleanAttributes(html) {
|
|
|
4333
4344
|
}
|
|
4334
4345
|
function renderEmbedBlock(ctx, data) {
|
|
4335
4346
|
const allowlist = ctx.options.embedAllowlist !== undefined ? ctx.options.embedAllowlist : DEFAULT_EMBED_ALLOWLIST;
|
|
4336
|
-
const sanitized = validateAndSanitizeEmbed(data.contents, allowlist);
|
|
4347
|
+
const sanitized = validateAndSanitizeEmbed(data.contents, allowlist, ctx.options.baseUrl);
|
|
4337
4348
|
if (sanitized === null) {
|
|
4338
4349
|
ctx.push('<div class="error-block">Sorry, no match for the embedded content.</div>');
|
|
4339
4350
|
return;
|