diff --git a/runtime-tests/deno-jsx/jsx.test.tsx b/runtime-tests/deno-jsx/jsx.test.tsx index 65833a68..4b58a34a 100644 --- a/runtime-tests/deno-jsx/jsx.test.tsx +++ b/runtime-tests/deno-jsx/jsx.test.tsx @@ -110,3 +110,57 @@ Deno.test('JSX: css', async () => { '
' ) }) + +Deno.test('JSX: normalize key', async () => { + const className =
+ const htmlFor =
+ const crossOrigin =
+ const httpEquiv =
+ const itemProp =
+ const fetchPriority =
+ const noModule =
+ const formAction =
+ + assertEquals(className.toString(), '
') + assertEquals(htmlFor.toString(), '
') + assertEquals(crossOrigin.toString(), '
') + assertEquals(httpEquiv.toString(), '
') + assertEquals(itemProp.toString(), '
') + assertEquals(fetchPriority.toString(), '
') + assertEquals(noModule.toString(), '
') + assertEquals(formAction.toString(), '
') +}) + +Deno.test('JSX: null or undefined', async () => { + const nullHtml =
+ const undefinedHtml =
+ + // react-jsx :
+ // precompile :
// Extra whitespace is allowed because it is a specification. + + assertEquals(nullHtml.toString().replace(/\s+/g, ''), '
') + assertEquals(undefinedHtml.toString().replace(/\s+/g, ''), '
') +}) + +Deno.test('JSX: boolean attributes', async () => { + const trueHtml =
+ const falseHtml =
+ + // output is different, but semantics as HTML is the same, so both are OK + // react-jsx :
+ // precompile :
+ + assertEquals(trueHtml.toString().replace('=""', ''), '
') + assertEquals(falseHtml.toString(), '
') +}) + +Deno.test('JSX: number', async () => { + const html =
+ + assertEquals(html.toString(), '
') +}) + +Deno.test('JSX: style', async () => { + const html =
+ assertEquals(html.toString(), '
') +}) diff --git a/src/jsx/base.ts b/src/jsx/base.ts index 5523c0a8..7b184373 100644 --- a/src/jsx/base.ts +++ b/src/jsx/base.ts @@ -62,7 +62,7 @@ const emptyTags = [ 'track', 'wbr', ] -const booleanAttributes = [ +export const booleanAttributes = [ 'allowfullscreen', 'async', 'autofocus', diff --git a/src/jsx/jsx-runtime.ts b/src/jsx/jsx-runtime.ts index af7444f2..77da8f61 100644 --- a/src/jsx/jsx-runtime.ts +++ b/src/jsx/jsx-runtime.ts @@ -6,13 +6,43 @@ export { jsxDEV as jsx, Fragment } from './jsx-dev-runtime' export { jsxDEV as jsxs } from './jsx-dev-runtime' export type { JSX } from './jsx-dev-runtime' - import { html, raw } from '../helper/html' -import type { HtmlEscapedString } from '../utils/html' +import type { HtmlEscapedString, StringBuffer, HtmlEscaped } from '../utils/html' +import { escapeToBuffer, stringBufferToString } from '../utils/html' +import { styleObjectForEach } from './utils' + export { html as jsxTemplate } + export const jsxAttr = ( - name: string, - value: string | Promise -): HtmlEscapedString | Promise => - typeof value === 'string' ? raw(name + '="' + html`${value}` + '"') : html`${name}="${value}"` + key: string, + v: string | Promise | Record +): HtmlEscapedString | Promise => { + const buffer: StringBuffer = [`${key}="`] as StringBuffer + if (key === 'style' && typeof v === 'object') { + // object to style strings + let styleStr = '' + styleObjectForEach(v as Record, (property, value) => { + if (value != null) { + styleStr += `${styleStr ? ';' : ''}${property}:${value}` + } + }) + escapeToBuffer(styleStr, buffer) + buffer[0] += '"' + } else if (typeof v === 'string') { + escapeToBuffer(v, buffer) + buffer[0] += '"' + } else if (v === null || v === undefined) { + return raw('') + } else if (typeof v === 'number' || (v as unknown as HtmlEscaped).isEscaped) { + buffer[0] += `${v}"` + } else if (v instanceof Promise) { + buffer.unshift('"', v) + } else { + escapeToBuffer(v.toString(), buffer) + buffer[0] += '"' + } + + return buffer.length === 1 ? raw(buffer[0]) : stringBufferToString(buffer, undefined) +} + export const jsxEscape = (value: string) => value