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:
parent
38a8c46568
commit
273106cfa9
@ -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"
|
||||
],
|
||||
|
@ -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>,
|
||||
|
0
src/middleware/jsx/README.md
Normal file
0
src/middleware/jsx/README.md
Normal file
17
src/middleware/jsx/index.test.tsx
Normal file
17
src/middleware/jsx/index.test.tsx
Normal 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>')
|
||||
})
|
||||
})
|
49
src/middleware/jsx/index.ts
Normal file
49
src/middleware/jsx/index.ts
Normal 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
|
||||
}
|
3
src/middleware/mustache/Context.ts
Normal file
3
src/middleware/mustache/Context.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface Context {
|
||||
render(template: string, params?: object, options?: object): Promise<Response>
|
||||
}
|
@ -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
8
src/utils/html.test.ts
Normal 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 <b>think</b> this is good.')
|
||||
expect(escape('John "Johnny" Smith')).toBe('John "Johnny" Smith')
|
||||
})
|
||||
})
|
8
src/utils/html.ts
Normal file
8
src/utils/html.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const escape = (str: string): string => {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
}
|
@ -18,6 +18,8 @@
|
||||
"node",
|
||||
"@cloudflare/workers-types"
|
||||
],
|
||||
"jsx": "react",
|
||||
"jsxFactory": "h",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
Loading…
Reference in New Issue
Block a user