Last active
August 7, 2023 05:13
-
-
Save nakasyou/4e186846c2ff4ddb0696f067c87d9f7c to your computer and use it in GitHub Desktop.
`luxt/jsx`のアイデア
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type IntrinsicElementProps = Record<string, any> & { | |
children?: JSX.Element[], | |
dangerouslySetInnerHTML?: { | |
__html: string, | |
} | |
} | |
/** | |
* LuxtのJSXの型。 | |
* JSXのElement | |
*/ | |
export interface Node { | |
type: Component | string | |
props: Record<string, any> & { | |
children?: any[] | |
} | |
} | |
declare global { | |
namespace JSX { | |
type Element = NodeLike | |
interface IntrinsicElements { | |
[elemName: string]: IntrinsicElementProps | |
} | |
interface ElementChildrenAttribute { | |
children: any | |
} | |
} | |
} | |
export type Component = (props: any) => JSX.Element | Promise<JSX.Element> | |
/** | |
* Create element | |
*/ | |
export const h = ( | |
type: Component | string, | |
props: Record<string, string>, | |
...children: NodeSet | |
): JSX.Element => { | |
return { | |
type, | |
props: { | |
...props, | |
children, | |
}, | |
} | |
} | |
type NodeLike = Node | string | number | boolean | null | void | |
interface FragmentProps { | |
children: NodeSet | |
} | |
/** | |
* JSX Fragmant | |
*/ | |
export const Fragment = (props: FragmentProps): JSX.Element => { | |
return { | |
type: "_luxt_fragment", | |
props, | |
} | |
} | |
export interface LuxtJSXLuxtData { | |
headStrings: string[] | |
} | |
const escapeChars: Record<string,string> = { | |
"<": "<", | |
">": ">", | |
"&": "&", | |
"'": "'", | |
'"': """, | |
} | |
export const escapeHTML = (text: string): string => { | |
return text.replaceAll(/[&<>"']/g, char => escapeChars[char]); | |
} | |
const emptyTags = [ | |
'area', | |
'base', | |
'br', | |
'col', | |
'embed', | |
'hr', | |
'img', | |
'input', | |
'keygen', | |
'link', | |
'meta', | |
'param', | |
'source', | |
'track', | |
'wbr', | |
] | |
export const renderToString = async (element: JSX.Element | JSX.Element[], luxtData: LuxtJSXLuxtData): string => { | |
if (element instanceof Array) { | |
// これがNodeの集合である | |
return (await Promise.all(element.map(async (node: NodeLike) => { | |
return await renderToString(node, luxtData) | |
}))).join("") | |
} else { | |
// これは一つのNodeである | |
if (!element.type) { | |
// これはNodeではない | |
return escapeHTML(String(element)) | |
} else if (typeof element.type === "function") { | |
// これはcomponentである | |
const resultNode: JSX.Element = await element.type(element.props) | |
return await renderToString(resultNode, luxtData) | |
} else if (element.type[0]+element.type[1]+element.type[2]+element.type[3]+element.type[4]+element.type[5] === "_luxt_") { | |
// これはLuxtのための予約タグである | |
switch (element.type) { | |
case "_luxt_fragment": | |
return (await Promise.all(element.props.children.map(async (node: NodeLike) => { | |
return await renderToString(node, luxtData) | |
}))).join("") | |
case "_luxt_pushHead": { | |
luxtData.headStrings.push(element.props.string) | |
return "" | |
} | |
} | |
} else { | |
const excludeProps = ["dangerouslySetInnerHTML", "children"] | |
// これはタグである | |
const propsString = Object.entries(element.props || {}).filter(([key,_value]) => !excludeProps.includes(key)).map(([key, value]) => { | |
return `${key}="${value}"` | |
}).join(" ") | |
if (emptyTags.includes(element.type)) { | |
return `<${element.type}${propsString ? " "+propsString : ""} />` | |
} | |
const childText = element.props.dangerouslySetInnerHTML?.__html || | |
(element.props?.children ? await renderToString(element.props.children, luxtData) : "") | |
return `<${element.type}${propsString ? " "+propsString : ""}>${childText}</${element.type}>` | |
} | |
} | |
} | |
export const PushHead = (props: {string: string}): JSX.Element => { | |
return { | |
type: "_luxt_pushHead", | |
props, | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** @jsxFrag Fragment */ | |
/** @jsx h */ | |
import { | |
h, | |
Fragment, | |
renderToString, | |
PushHead | |
} from "./mod.ts" | |
const Link = (props: {link: string}) => <a href={props.link} class="link">Link</a> | |
const Island = (props) => { | |
return <PushHead string={`<script src="/main.js" />`} /> | |
} | |
const App = () => { | |
return <> | |
<div> | |
{`<img src="" onerror="alert("xss")>`} | |
</div> | |
<Link link="/" /> | |
<Island /> | |
<div dangerouslySetInnerHTML={{ | |
__html: "<div>This is innerHTML</div>" | |
}} /> | |
<br /> | |
</> | |
} | |
const luxtData = { | |
headStrings: [] | |
} | |
console.log(await renderToString(<App />, luxtData)) | |
console.log(luxtData) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment