mirror of
https://github.com/withastro/astro.git
synced 2024-12-30 22:03:56 -05:00
update HTML escape logic (#2793)
This commit is contained in:
parent
2b76ee8d75
commit
6eb494796e
7 changed files with 75 additions and 39 deletions
5
.changeset/pink-stingrays-swim.md
Normal file
5
.changeset/pink-stingrays-swim.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix HTML double-escaping issue
|
|
@ -85,6 +85,7 @@
|
||||||
"fast-glob": "^3.2.11",
|
"fast-glob": "^3.2.11",
|
||||||
"fast-xml-parser": "^4.0.6",
|
"fast-xml-parser": "^4.0.6",
|
||||||
"html-entities": "^2.3.2",
|
"html-entities": "^2.3.2",
|
||||||
|
"html-escaper": "^3.0.3",
|
||||||
"htmlparser2": "^7.2.0",
|
"htmlparser2": "^7.2.0",
|
||||||
"kleur": "^4.1.4",
|
"kleur": "^4.1.4",
|
||||||
"magic-string": "^0.25.9",
|
"magic-string": "^0.25.9",
|
||||||
|
@ -123,6 +124,7 @@
|
||||||
"@types/connect": "^3.4.35",
|
"@types/connect": "^3.4.35",
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
"@types/estree": "^0.0.51",
|
"@types/estree": "^0.0.51",
|
||||||
|
"@types/html-escaper": "^3.0.0",
|
||||||
"@types/mime": "^2.0.3",
|
"@types/mime": "^2.0.3",
|
||||||
"@types/mocha": "^9.1.0",
|
"@types/mocha": "^9.1.0",
|
||||||
"@types/parse5": "^6.0.3",
|
"@types/parse5": "^6.0.3",
|
||||||
|
|
|
@ -1,25 +1,31 @@
|
||||||
const entities = { '"': 'quot', '&': 'amp', "'": 'apos', '<': 'lt', '>': 'gt' } as const;
|
import { escape } from 'html-escaper';
|
||||||
|
|
||||||
// This util is only ever run on expression values that we already know are of type `string`
|
// Leverage the battle-tested `html-escaper` npm package.
|
||||||
export const escapeHTML = (str: string) => str.replace(/["'&<>]/g, (char: string) => '&' + entities[char as keyof typeof entities] + ';');
|
export const escapeHTML = escape;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RawString is a "blessed" version of String
|
* A "blessed" extension of String that tells Astro that the string
|
||||||
* that is not subject to escaping.
|
* has already been escaped. This helps prevent double-escaping of HTML.
|
||||||
*/
|
*/
|
||||||
export class UnescapedString extends String {}
|
export class HTMLString extends String {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* unescapeHTML marks a string as raw, unescaped HTML.
|
* markHTMLString marks a string as raw or "already escaped" by returning
|
||||||
* This should only be generated internally, not a public API.
|
* a `HTMLString` instance. This is meant for internal use, and should not
|
||||||
|
* be returned through any public JS API.
|
||||||
*/
|
*/
|
||||||
export const unescapeHTML = (value: any) => {
|
export const markHTMLString = (value: any) => {
|
||||||
// Cast any `string` values to `UnescapedString` to mark them as ignored
|
// If value is already marked as an HTML string, there is nothing to do.
|
||||||
// The `as unknown as string` is necessary for TypeScript to treat this as `string`
|
if (value instanceof HTMLString) {
|
||||||
if (typeof value === 'string') {
|
return value;
|
||||||
return new UnescapedString(value) as unknown as string;
|
|
||||||
}
|
}
|
||||||
// Just return values that are `number`, `null`, `undefined` etc
|
// Cast to `HTMLString` to mark the string as valid HTML. Any HTML escaping
|
||||||
// The compiler will recursively stringify these correctly
|
// and sanitization should have already happened to the `value` argument.
|
||||||
|
// NOTE: `unknown as string` is necessary for TypeScript to treat this as `string`
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return new HTMLString(value) as unknown as string;
|
||||||
|
}
|
||||||
|
// Return all other values (`number`, `null`, `undefined`) as-is.
|
||||||
|
// The compiler will recursively stringify these correctly at a later stage.
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,11 +4,14 @@ import type { AstroGlobalPartial, SSRResult, SSRElement } from '../../@types/ast
|
||||||
import shorthash from 'shorthash';
|
import shorthash from 'shorthash';
|
||||||
import { extractDirectives, generateHydrateScript } from './hydration.js';
|
import { extractDirectives, generateHydrateScript } from './hydration.js';
|
||||||
import { serializeListValue } from './util.js';
|
import { serializeListValue } from './util.js';
|
||||||
import { escapeHTML, UnescapedString, unescapeHTML } from './escape.js';
|
import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
|
||||||
|
|
||||||
export type { Metadata } from './metadata';
|
export type { Metadata } from './metadata';
|
||||||
export { createMetadata } from './metadata.js';
|
export { createMetadata } from './metadata.js';
|
||||||
export { unescapeHTML } from './escape.js';
|
|
||||||
|
export { markHTMLString } from './escape.js';
|
||||||
|
// TODO(deprecated): This name has been updated in Astro runtime but not yet in the Astro compiler.
|
||||||
|
export { markHTMLString as unescapeHTML } from './escape.js';
|
||||||
|
|
||||||
const voidElementNames = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
|
const voidElementNames = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
|
||||||
const htmlBooleanAttributes =
|
const htmlBooleanAttributes =
|
||||||
|
@ -26,24 +29,24 @@ const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|pre
|
||||||
// Or maybe type UserValue = any; ?
|
// Or maybe type UserValue = any; ?
|
||||||
async function _render(child: any): Promise<any> {
|
async function _render(child: any): Promise<any> {
|
||||||
child = await child;
|
child = await child;
|
||||||
if (child instanceof UnescapedString) {
|
if (child instanceof HTMLString) {
|
||||||
return child;
|
return child;
|
||||||
} else if (Array.isArray(child)) {
|
} else if (Array.isArray(child)) {
|
||||||
return unescapeHTML((await Promise.all(child.map((value) => _render(value)))).join(''));
|
return markHTMLString((await Promise.all(child.map((value) => _render(value)))).join(''));
|
||||||
} else if (typeof child === 'function') {
|
} else if (typeof child === 'function') {
|
||||||
// Special: If a child is a function, call it automatically.
|
// Special: If a child is a function, call it automatically.
|
||||||
// This lets you do {() => ...} without the extra boilerplate
|
// This lets you do {() => ...} without the extra boilerplate
|
||||||
// of wrapping it in a function and calling it.
|
// of wrapping it in a function and calling it.
|
||||||
return _render(child());
|
return _render(child());
|
||||||
} else if (typeof child === 'string') {
|
} else if (typeof child === 'string') {
|
||||||
return escapeHTML(child);
|
return markHTMLString(escapeHTML(child));
|
||||||
} else if (!child && child !== 0) {
|
} else if (!child && child !== 0) {
|
||||||
// do nothing, safe to ignore falsey values.
|
// do nothing, safe to ignore falsey values.
|
||||||
}
|
}
|
||||||
// Add a comment explaining why each of these are needed.
|
// Add a comment explaining why each of these are needed.
|
||||||
// Maybe create clearly named function for what this is doing.
|
// Maybe create clearly named function for what this is doing.
|
||||||
else if (child instanceof AstroComponent || Object.prototype.toString.call(child) === '[object AstroComponent]') {
|
else if (child instanceof AstroComponent || Object.prototype.toString.call(child) === '[object AstroComponent]') {
|
||||||
return unescapeHTML(await renderAstroComponent(child));
|
return markHTMLString(await renderAstroComponent(child));
|
||||||
} else {
|
} else {
|
||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
|
@ -71,7 +74,7 @@ export class AstroComponent {
|
||||||
const html = htmlParts[i];
|
const html = htmlParts[i];
|
||||||
const expression = expressions[i];
|
const expression = expressions[i];
|
||||||
|
|
||||||
yield unescapeHTML(html);
|
yield markHTMLString(html);
|
||||||
yield _render(expression);
|
yield _render(expression);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,12 +137,12 @@ export async function renderComponent(result: SSRResult, displayName: string, Co
|
||||||
if (children == null) {
|
if (children == null) {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
return unescapeHTML(children);
|
return markHTMLString(children);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Component && (Component as any).isAstroComponentFactory) {
|
if (Component && (Component as any).isAstroComponentFactory) {
|
||||||
const output = await renderToString(result, Component as any, _props, slots);
|
const output = await renderToString(result, Component as any, _props, slots);
|
||||||
return unescapeHTML(output);
|
return markHTMLString(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Component === null && !_props['client:only']) {
|
if (Component === null && !_props['client:only']) {
|
||||||
|
@ -250,7 +253,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
// as a string and the user is responsible for adding a script tag for the component definition.
|
// as a string and the user is responsible for adding a script tag for the component definition.
|
||||||
if (!html && typeof Component === 'string') {
|
if (!html && typeof Component === 'string') {
|
||||||
html = await renderAstroComponent(
|
html = await renderAstroComponent(
|
||||||
await render`<${Component}${spreadAttributes(props)}${unescapeHTML(
|
await render`<${Component}${spreadAttributes(props)}${markHTMLString(
|
||||||
(children == null || children == '') && voidElementNames.test(Component) ? `/>` : `>${children}</${Component}>`
|
(children == null || children == '') && voidElementNames.test(Component) ? `/>` : `>${children}</${Component}>`
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
|
@ -267,7 +270,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hydration) {
|
if (!hydration) {
|
||||||
return unescapeHTML(html.replace(/\<\/?astro-fragment\>/g, ''));
|
return markHTMLString(html.replace(/\<\/?astro-fragment\>/g, ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include componentExport name and componentUrl in hash to dedupe identical islands
|
// Include componentExport name and componentUrl in hash to dedupe identical islands
|
||||||
|
@ -280,7 +283,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
// Render a template if no fragment is provided.
|
// Render a template if no fragment is provided.
|
||||||
const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html);
|
const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html);
|
||||||
const template = needsAstroTemplate ? `<template data-astro-template>${children}</template>` : '';
|
const template = needsAstroTemplate ? `<template data-astro-template>${children}</template>` : '';
|
||||||
return unescapeHTML(`<astro-root uid="${astroId}"${needsAstroTemplate ? ' tmpl' : ''}>${html ?? ''}${template}</astro-root>`);
|
return markHTMLString(`<astro-root uid="${astroId}"${needsAstroTemplate ? ' tmpl' : ''}>${html ?? ''}${template}</astro-root>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create the Astro.fetchContent() runtime function. */
|
/** Create the Astro.fetchContent() runtime function. */
|
||||||
|
@ -349,7 +352,7 @@ export function addAttribute(value: any, key: string, shouldEscape = true) {
|
||||||
|
|
||||||
if (value === false) {
|
if (value === false) {
|
||||||
if (htmlEnumAttributes.test(key) || svgEnumAttributes.test(key)) {
|
if (htmlEnumAttributes.test(key) || svgEnumAttributes.test(key)) {
|
||||||
return unescapeHTML(` ${key}="false"`);
|
return markHTMLString(` ${key}="false"`);
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
@ -365,14 +368,14 @@ Make sure to use the static attribute syntax (\`${key}={value}\`) instead of the
|
||||||
|
|
||||||
// support "class" from an expression passed into an element (#782)
|
// support "class" from an expression passed into an element (#782)
|
||||||
if (key === 'class:list') {
|
if (key === 'class:list') {
|
||||||
return unescapeHTML(` ${key.slice(0, -5)}="${toAttributeString(serializeListValue(value))}"`);
|
return markHTMLString(` ${key.slice(0, -5)}="${toAttributeString(serializeListValue(value))}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boolean values only need the key
|
// Boolean values only need the key
|
||||||
if (value === true && (key.startsWith('data-') || htmlBooleanAttributes.test(key))) {
|
if (value === true && (key.startsWith('data-') || htmlBooleanAttributes.test(key))) {
|
||||||
return unescapeHTML(` ${key}`);
|
return markHTMLString(` ${key}`);
|
||||||
} else {
|
} else {
|
||||||
return unescapeHTML(` ${key}="${toAttributeString(value, shouldEscape)}"`);
|
return markHTMLString(` ${key}="${toAttributeString(value, shouldEscape)}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,7 +385,7 @@ export function spreadAttributes(values: Record<any, any>, shouldEscape = true)
|
||||||
for (const [key, value] of Object.entries(values)) {
|
for (const [key, value] of Object.entries(values)) {
|
||||||
output += addAttribute(value, key, shouldEscape);
|
output += addAttribute(value, key, shouldEscape);
|
||||||
}
|
}
|
||||||
return unescapeHTML(output);
|
return markHTMLString(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds CSS variables to an inline style tag
|
// Adds CSS variables to an inline style tag
|
||||||
|
@ -391,7 +394,7 @@ export function defineStyleVars(selector: string, vars: Record<any, any>) {
|
||||||
for (const [key, value] of Object.entries(vars)) {
|
for (const [key, value] of Object.entries(vars)) {
|
||||||
output += ` --${key}: ${value};\n`;
|
output += ` --${key}: ${value};\n`;
|
||||||
}
|
}
|
||||||
return unescapeHTML(`${selector} {${output}}`);
|
return markHTMLString(`${selector} {${output}}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds variables to an inline script.
|
// Adds variables to an inline script.
|
||||||
|
@ -400,7 +403,7 @@ export function defineScriptVars(vars: Record<any, any>) {
|
||||||
for (const [key, value] of Object.entries(vars)) {
|
for (const [key, value] of Object.entries(vars)) {
|
||||||
output += `let ${key} = ${JSON.stringify(value)};\n`;
|
output += `let ${key} = ${JSON.stringify(value)};\n`;
|
||||||
}
|
}
|
||||||
return unescapeHTML(output);
|
return markHTMLString(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders an endpoint request to completion, returning the body.
|
// Renders an endpoint request to completion, returning the body.
|
||||||
|
@ -467,7 +470,7 @@ export async function renderHead(result: SSRResult): Promise<string> {
|
||||||
const links = Array.from(result.links)
|
const links = Array.from(result.links)
|
||||||
.filter(uniqueElements)
|
.filter(uniqueElements)
|
||||||
.map((link) => renderElement('link', link, false));
|
.map((link) => renderElement('link', link, false));
|
||||||
return unescapeHTML(links.join('\n') + styles.join('\n') + scripts.join('\n') + '\n' + '<!--astro:head:injected-->');
|
return markHTMLString(links.join('\n') + styles.join('\n') + scripts.join('\n') + '\n' + '<!--astro:head:injected-->');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {
|
export async function renderAstroComponent(component: InstanceType<typeof AstroComponent>) {
|
||||||
|
@ -479,7 +482,7 @@ export async function renderAstroComponent(component: InstanceType<typeof AstroC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return unescapeHTML(await _render(template));
|
return markHTMLString(await _render(template));
|
||||||
}
|
}
|
||||||
|
|
||||||
function componentIsHTMLElement(Component: unknown) {
|
function componentIsHTMLElement(Component: unknown) {
|
||||||
|
@ -495,7 +498,7 @@ export async function renderHTMLElement(result: SSRResult, constructor: typeof H
|
||||||
attrHTML += ` ${attr}="${toAttributeString(await props[attr])}"`;
|
attrHTML += ` ${attr}="${toAttributeString(await props[attr])}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return unescapeHTML(`<${name}${attrHTML}>${await renderSlot(result, slots?.default)}</${name}>`);
|
return markHTMLString(`<${name}${attrHTML}>${await renderSlot(result, slots?.default)}</${name}>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHTMLElementName(constructor: typeof HTMLElement) {
|
function getHTMLElementName(constructor: typeof HTMLElement) {
|
||||||
|
|
|
@ -104,8 +104,15 @@ describe('Expressions', () => {
|
||||||
const html = await fixture.readFile('/escape/index.html');
|
const html = await fixture.readFile('/escape/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
expect($('body').children()).to.have.lengthOf(1);
|
expect($('body').children()).to.have.lengthOf(2);
|
||||||
expect($('body').text()).to.include('<script>console.log("pwnd")</script>');
|
expect($('body').html()).to.include('<script>console.log("pwnd")</script>');
|
||||||
expect($('#trusted')).to.have.lengthOf(1);
|
expect($('#trusted')).to.have.lengthOf(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Does not double-escape HTML', async () => {
|
||||||
|
const html = await fixture.readFile('/escape/index.html');
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
|
expect($('#single-escape').html()).to.equal('Astro & Vite');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<title>My site</title>
|
<title>My site</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<span id="single-escape">{'Astro & Vite'}</span>
|
||||||
{'<script>console.log("pwnd")</script>'}
|
{'<script>console.log("pwnd")</script>'}
|
||||||
<Fragment set:html={'<script id="trusted">console.log("yay!")</script>'} />
|
<Fragment set:html={'<script id="trusted">console.log("yay!")</script>'} />
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -352,6 +352,7 @@ importers:
|
||||||
'@types/connect': ^3.4.35
|
'@types/connect': ^3.4.35
|
||||||
'@types/debug': ^4.1.7
|
'@types/debug': ^4.1.7
|
||||||
'@types/estree': ^0.0.51
|
'@types/estree': ^0.0.51
|
||||||
|
'@types/html-escaper': ^3.0.0
|
||||||
'@types/mime': ^2.0.3
|
'@types/mime': ^2.0.3
|
||||||
'@types/mocha': ^9.1.0
|
'@types/mocha': ^9.1.0
|
||||||
'@types/parse5': ^6.0.3
|
'@types/parse5': ^6.0.3
|
||||||
|
@ -374,6 +375,7 @@ importers:
|
||||||
fast-glob: ^3.2.11
|
fast-glob: ^3.2.11
|
||||||
fast-xml-parser: ^4.0.6
|
fast-xml-parser: ^4.0.6
|
||||||
html-entities: ^2.3.2
|
html-entities: ^2.3.2
|
||||||
|
html-escaper: ^3.0.3
|
||||||
htmlparser2: ^7.2.0
|
htmlparser2: ^7.2.0
|
||||||
kleur: ^4.1.4
|
kleur: ^4.1.4
|
||||||
magic-string: ^0.25.9
|
magic-string: ^0.25.9
|
||||||
|
@ -428,6 +430,7 @@ importers:
|
||||||
fast-glob: 3.2.11
|
fast-glob: 3.2.11
|
||||||
fast-xml-parser: 4.0.6
|
fast-xml-parser: 4.0.6
|
||||||
html-entities: 2.3.2
|
html-entities: 2.3.2
|
||||||
|
html-escaper: 3.0.3
|
||||||
htmlparser2: 7.2.0
|
htmlparser2: 7.2.0
|
||||||
kleur: 4.1.4
|
kleur: 4.1.4
|
||||||
magic-string: 0.25.9
|
magic-string: 0.25.9
|
||||||
|
@ -465,6 +468,7 @@ importers:
|
||||||
'@types/connect': 3.4.35
|
'@types/connect': 3.4.35
|
||||||
'@types/debug': 4.1.7
|
'@types/debug': 4.1.7
|
||||||
'@types/estree': 0.0.51
|
'@types/estree': 0.0.51
|
||||||
|
'@types/html-escaper': 3.0.0
|
||||||
'@types/mime': 2.0.3
|
'@types/mime': 2.0.3
|
||||||
'@types/mocha': 9.1.0
|
'@types/mocha': 9.1.0
|
||||||
'@types/parse5': 6.0.3
|
'@types/parse5': 6.0.3
|
||||||
|
@ -3490,6 +3494,10 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 2.0.6
|
'@types/unist': 2.0.6
|
||||||
|
|
||||||
|
/@types/html-escaper/3.0.0:
|
||||||
|
resolution: {integrity: sha512-OcJcvP3Yk8mjYwf/IdXZtTE1tb/u0WF0qa29ER07ZHCYUBZXSN29Z1mBS+/96+kNMGTFUAbSz9X+pHmHpZrTCw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/is-ci/3.0.0:
|
/@types/is-ci/3.0.0:
|
||||||
resolution: {integrity: sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ==}
|
resolution: {integrity: sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6283,6 +6291,10 @@ packages:
|
||||||
resolution: {integrity: sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==}
|
resolution: {integrity: sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/html-escaper/3.0.3:
|
||||||
|
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/html-void-elements/2.0.1:
|
/html-void-elements/2.0.1:
|
||||||
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
|
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
Loading…
Reference in a new issue