0
0
mirror of https://github.com/honojs/hono.git synced 2024-11-21 18:18:57 +01:00

[WIP] feat: jsx middleware

This commit is contained in:
Yusuke Wada 2022-06-07 19:03:42 +09:00
parent 38a8c46568
commit 273106cfa9
10 changed files with 95 additions and 4 deletions

View File

@ -24,6 +24,7 @@
"./cors": "./dist/middleware/cors/index.js",
"./etag": "./dist/middleware/etag/index.js",
"./graphql-server": "./dist/middleware/graphql-server/index.js",
"./jsx": "./dist/middleware/jsx/index.js",
"./jwt": "./dist/middleware/jwt/index.js",
"./logger": "./dist/middleware/logger/index.js",
"./mustache": "./dist/middleware/mustache/index.js",
@ -60,6 +61,9 @@
"graphql-server": [
"./dist/middleware/graphql-server"
],
"jsx": [
"./dist/middleware/jsx"
],
"jwt": [
"./dist/middleware/jwt"
],

View File

@ -21,7 +21,7 @@ export class Context<RequestParamKeyType extends string = string, E = Env> {
private _res: Response | undefined
private notFoundHandler: NotFoundHandler
render: (template: string, params?: object, options?: object) => Promise<Response>
render: (content: string, params?: object, options?: object) => Response | Promise<Response>
constructor(
req: Request<RequestParamKeyType>,

View File

View File

@ -0,0 +1,17 @@
import { Hono } from '../../hono'
import { h, jsx } from '.'
describe('JSX middleware', () => {
const app = new Hono()
app.use('*', jsx())
it('Should render HTML strings', async () => {
app.get('/', (c) => {
return c.render(<h1>Hello</h1>)
})
const res = await app.request('http://localhost/')
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8')
expect(await res.text()).toBe('<!doctype html><h1>Hello</h1>')
})
})

View File

@ -0,0 +1,49 @@
import type { Context } from '../../context'
import type { Next } from '../../hono'
import { escape } from '../../utils/html'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace h.JSX {
interface IntrinsicElements {
[tagName: string]: Record<string, string>
}
}
}
export const jsx = () => {
return async (c: Context, next: Next) => {
c.render = (content: string) => {
const output = `<!doctype html>${content.toString()}`
return c.html(output)
}
await next()
}
}
type EscapedString = string & { isEscaped: true }
export const h = (
tag: string | Function,
props: Record<string, any>,
...children: (string | EscapedString)[]
): EscapedString => {
if (typeof tag === 'function') {
return tag.call(null, { ...props, children })
}
let attrs = ''
const propsKeys = Object.keys(props || {})
for (let i = 0, len = propsKeys.length; i < len; i++) {
attrs += ` ${propsKeys[i]}="${escape(props[propsKeys[i]])}"`
}
const res: any = new String(
`<${tag}${attrs}>${children
.flat()
.map((c) => ((c as any).isEscaped ? c : escape(c as string)))
.join('')}</${tag}>`
)
res.isEscaped = true
return res
}

View File

@ -0,0 +1,3 @@
export interface Context {
render(template: string, params?: object, options?: object): Promise<Response>
}

View File

@ -1,6 +1,6 @@
import Mustache from 'mustache'
import type { Context } from '../../context'
import type { Handler, Next } from '../../hono'
import type { Next } from '../../hono'
import { bufferToString } from '../../utils/buffer'
import type { KVAssetOptions } from '../../utils/cloudflare'
import { getContentFromKVAsset, getKVFilePath } from '../../utils/cloudflare'
@ -20,11 +20,11 @@ export type MustacheOptions = {
namespace?: KVNamespace
}
export const mustache = (init: MustacheOptions = { root: '' }): Handler => {
export const mustache = (init: MustacheOptions = { root: '' }) => {
const { root } = init
return async (c: Context, next: Next) => {
c.render = async (filename, params = {}, options?) => {
c.render = async (filename, params = {}, options?): Promise<Response> => {
const path = getKVFilePath({
filename: `${filename}${EXTENSION}`,
root: root,

8
src/utils/html.test.ts Normal file
View File

@ -0,0 +1,8 @@
import { escape } from './html'
describe('HTML escape', () => {
it('Should escape special characters', () => {
expect(escape('I <b>think</b> this is good.')).toBe('I &lt;b&gt;think&lt;/b&gt; this is good.')
expect(escape('John "Johnny" Smith')).toBe('John &quot;Johnny&quot; Smith')
})
})

8
src/utils/html.ts Normal file
View File

@ -0,0 +1,8 @@
export const escape = (str: string): string => {
return str
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
}

View File

@ -18,6 +18,8 @@
"node",
"@cloudflare/workers-types"
],
"jsx": "react",
"jsxFactory": "h",
},
"include": [
"src/**/*.ts",