summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Nate Moore <natemoo-re@users.noreply.github.com> 2022-03-14 18:19:53 -0500
committerGravatar GitHub <noreply@github.com> 2022-03-14 18:19:53 -0500
commit6b34840d3d082d6491515ff96976f603947316d3 (patch)
tree8bf9549f3b7459f4b8b0eb16570d0037f8d8c76a
parentde2b2462376d7750b9069e5473bfbf591e9929b5 (diff)
downloadastro-6b34840d3d082d6491515ff96976f603947316d3.tar.gz
astro-6b34840d3d082d6491515ff96976f603947316d3.tar.zst
astro-6b34840d3d082d6491515ff96976f603947316d3.zip
Fix `set:html` behavior with `null` (#2790)
* feat: improve set:html behavior for null/undefined * chore: add changeset * refactor: improve set:html and set:text documentation * test: improve set:html tests * refactor: better types for server API
-rw-r--r--.changeset/serious-guests-matter.md5
-rw-r--r--packages/astro/src/runtime/server/escape.ts16
-rw-r--r--packages/astro/src/runtime/server/index.ts4
-rw-r--r--packages/astro/test/astro-directives.test.js26
-rw-r--r--packages/astro/test/fixtures/astro-directives/src/pages/set-html.astro12
5 files changed, 57 insertions, 6 deletions
diff --git a/.changeset/serious-guests-matter.md b/.changeset/serious-guests-matter.md
new file mode 100644
index 000000000..bfdcba398
--- /dev/null
+++ b/.changeset/serious-guests-matter.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Improve `set:html` behavior for `null` and `undefined` values
diff --git a/packages/astro/src/runtime/server/escape.ts b/packages/astro/src/runtime/server/escape.ts
index 6c6eb4ff6..20ae3d3eb 100644
--- a/packages/astro/src/runtime/server/escape.ts
+++ b/packages/astro/src/runtime/server/escape.ts
@@ -1,6 +1,7 @@
const entities = { '"': 'quot', '&': 'amp', "'": 'apos', '<': 'lt', '>': 'gt' } as const;
-export const escapeHTML = (string: any) => string.replace(/["'&<>]/g, (char: keyof typeof entities) => '&' + entities[char] + ';');
+// This util is only ever run on expression values that we already know are of type `string`
+export const escapeHTML = (str: string) => str.replace(/["'&<>]/g, (char: string) => '&' + entities[char as keyof typeof entities] + ';');
/**
* RawString is a "blessed" version of String
@@ -11,7 +12,14 @@ export class UnescapedString extends String {}
/**
* unescapeHTML marks a string as raw, unescaped HTML.
* This should only be generated internally, not a public API.
- *
- * Need to cast the return value `as unknown as string` so TS doesn't yell at us.
*/
-export const unescapeHTML = (str: any) => new UnescapedString(str) as unknown as string;
+export const unescapeHTML = (value: any) => {
+ // Cast any `string` values to `UnescapedString` to mark them as ignored
+ // The `as unknown as string` is necessary for TypeScript to treat this as `string`
+ if (typeof value === 'string') {
+ return new UnescapedString(value) as unknown as string;
+ }
+ // Just return values that are `number`, `null`, `undefined` etc
+ // The compiler will recursively stringify these correctly
+ return value
+}
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index b81f88933..fe607e5b0 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -418,7 +418,7 @@ export async function renderEndpoint(mod: EndpointHandler, params: any) {
}
// Calls a component and renders it into a string of HTML
-export async function renderToString(result: SSRResult, componentFactory: AstroComponentFactory, props: any, children: any) {
+export async function renderToString(result: SSRResult, componentFactory: AstroComponentFactory, props: any, children: any): Promise<string> {
const Component = await componentFactory(result, props, children);
let template = await renderAstroComponent(Component);
@@ -439,7 +439,7 @@ const uniqueElements = (item: any, index: number, all: any[]) => {
// Renders a page to completion by first calling the factory callback, waiting for its result, and then appending
// styles and scripts into the head.
-export async function renderHead(result: SSRResult) {
+export async function renderHead(result: SSRResult): Promise<string> {
const styles = Array.from(result.styles)
.filter(uniqueElements)
.map((style) => {
diff --git a/packages/astro/test/astro-directives.test.js b/packages/astro/test/astro-directives.test.js
index 9fb2d5c6b..536ba3441 100644
--- a/packages/astro/test/astro-directives.test.js
+++ b/packages/astro/test/astro-directives.test.js
@@ -17,4 +17,30 @@ describe('Directives', async () => {
expect($('script#inline')).to.have.lengthOf(1);
expect($('script#inline').toString()).to.include('let foo = "bar"');
});
+
+ it('set:html', async () => {
+ const html = await fixture.readFile('/set-html/index.html');
+ const $ = cheerio.load(html);
+
+ expect($('#text')).to.have.lengthOf(1);
+ expect($('#text').text()).to.equal('a');
+
+ expect($('#zero')).to.have.lengthOf(1);
+ expect($('#zero').text()).to.equal('0');
+
+ expect($('#number')).to.have.lengthOf(1);
+ expect($('#number').text()).to.equal('1');
+
+ expect($('#undefined')).to.have.lengthOf(1);
+ expect($('#undefined').text()).to.equal('');
+
+ expect($('#null')).to.have.lengthOf(1);
+ expect($('#null').text()).to.equal('');
+
+ expect($('#false')).to.have.lengthOf(1);
+ expect($('#false').text()).to.equal('');
+
+ expect($('#true')).to.have.lengthOf(1);
+ expect($('#true').text()).to.equal('true');
+ });
});
diff --git a/packages/astro/test/fixtures/astro-directives/src/pages/set-html.astro b/packages/astro/test/fixtures/astro-directives/src/pages/set-html.astro
new file mode 100644
index 000000000..c51b2da3d
--- /dev/null
+++ b/packages/astro/test/fixtures/astro-directives/src/pages/set-html.astro
@@ -0,0 +1,12 @@
+<html>
+ <head></head>
+ <body>
+ <div id="text" set:html={"a"} />
+ <div id="zero" set:html={0} />
+ <div id="number" set:html={1} />
+ <div id="false" set:html={false} />
+ <div id="true" set:html={true} />
+ <div id="undefined" set:html={undefined} />
+ <div id="null" set:html={null} />
+ </body>
+</html>