mirror of
https://github.com/honojs/hono.git
synced 2024-11-21 18:18:57 +01:00
feat(jsx/precompile): Normalization and stringification of attribute values as renderToString
(#3432)
* feat(jsx/precompile): Normalization and stringification of attribute values as `renderToString` * fix(jsx/precompile): boolean attributes are processed by compiler * test: fix test data for jsx (precompile and react-jsx) * fix(jsx/precompile): remove unnecessary import statement * refactor(jsx/precompile): Normalization of keys from JSX to HTML is done by runtime, not by hono
This commit is contained in:
parent
27961dcf31
commit
7779fca9a5
@ -110,3 +110,57 @@ Deno.test('JSX: css', async () => {
|
||||
'<html><head><style id="hono-css">.css-3142110215{color:red}</style></head><body><div class="css-3142110215"></div></body></html>'
|
||||
)
|
||||
})
|
||||
|
||||
Deno.test('JSX: normalize key', async () => {
|
||||
const className = <div className='foo'></div>
|
||||
const htmlFor = <div htmlFor='foo'></div>
|
||||
const crossOrigin = <div crossOrigin='foo'></div>
|
||||
const httpEquiv = <div httpEquiv='foo'></div>
|
||||
const itemProp = <div itemProp='foo'></div>
|
||||
const fetchPriority = <div fetchPriority='foo'></div>
|
||||
const noModule = <div noModule='foo'></div>
|
||||
const formAction = <div formAction='foo'></div>
|
||||
|
||||
assertEquals(className.toString(), '<div class="foo"></div>')
|
||||
assertEquals(htmlFor.toString(), '<div for="foo"></div>')
|
||||
assertEquals(crossOrigin.toString(), '<div crossorigin="foo"></div>')
|
||||
assertEquals(httpEquiv.toString(), '<div http-equiv="foo"></div>')
|
||||
assertEquals(itemProp.toString(), '<div itemprop="foo"></div>')
|
||||
assertEquals(fetchPriority.toString(), '<div fetchpriority="foo"></div>')
|
||||
assertEquals(noModule.toString(), '<div nomodule="foo"></div>')
|
||||
assertEquals(formAction.toString(), '<div formaction="foo"></div>')
|
||||
})
|
||||
|
||||
Deno.test('JSX: null or undefined', async () => {
|
||||
const nullHtml = <div className={null}></div>
|
||||
const undefinedHtml = <div className={undefined}></div>
|
||||
|
||||
// react-jsx : <div>
|
||||
// precompile : <div > // Extra whitespace is allowed because it is a specification.
|
||||
|
||||
assertEquals(nullHtml.toString().replace(/\s+/g, ''), '<div></div>')
|
||||
assertEquals(undefinedHtml.toString().replace(/\s+/g, ''), '<div></div>')
|
||||
})
|
||||
|
||||
Deno.test('JSX: boolean attributes', async () => {
|
||||
const trueHtml = <div disabled={true}></div>
|
||||
const falseHtml = <div disabled={false}></div>
|
||||
|
||||
// output is different, but semantics as HTML is the same, so both are OK
|
||||
// react-jsx : <div disabled="">
|
||||
// precompile : <div disabled>
|
||||
|
||||
assertEquals(trueHtml.toString().replace('=""', ''), '<div disabled></div>')
|
||||
assertEquals(falseHtml.toString(), '<div></div>')
|
||||
})
|
||||
|
||||
Deno.test('JSX: number', async () => {
|
||||
const html = <div tabindex={1}></div>
|
||||
|
||||
assertEquals(html.toString(), '<div tabindex="1"></div>')
|
||||
})
|
||||
|
||||
Deno.test('JSX: style', async () => {
|
||||
const html = <div style={{ fontSize: '12px', color: null }}></div>
|
||||
assertEquals(html.toString(), '<div style="font-size:12px"></div>')
|
||||
})
|
||||
|
@ -62,7 +62,7 @@ const emptyTags = [
|
||||
'track',
|
||||
'wbr',
|
||||
]
|
||||
const booleanAttributes = [
|
||||
export const booleanAttributes = [
|
||||
'allowfullscreen',
|
||||
'async',
|
||||
'autofocus',
|
||||
|
@ -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<string>
|
||||
): HtmlEscapedString | Promise<HtmlEscapedString> =>
|
||||
typeof value === 'string' ? raw(name + '="' + html`${value}` + '"') : html`${name}="${value}"`
|
||||
key: string,
|
||||
v: string | Promise<string> | Record<string, string | number | null | undefined | boolean>
|
||||
): HtmlEscapedString | Promise<HtmlEscapedString> => {
|
||||
const buffer: StringBuffer = [`${key}="`] as StringBuffer
|
||||
if (key === 'style' && typeof v === 'object') {
|
||||
// object to style strings
|
||||
let styleStr = ''
|
||||
styleObjectForEach(v as Record<string, string | number>, (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
|
||||
|
Loading…
Reference in New Issue
Block a user