Skip to content

Commit

Permalink
feat: granular sanitization
Browse files Browse the repository at this point in the history
  • Loading branch information
Desdaemon committed Jul 23, 2023
1 parent caf516a commit 6603910
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 31 deletions.
31 changes: 30 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
/// <reference path="./jsx.d.ts" />

export {};
/**
* Configuration for the JSX runtime.
*/
export const jsxConfig: JsxConfig = {
jsonAttributes: ["hx-vals", "hx-headers", "data-hx-vals", "data-hx-headers"],
sanitize: false,
trusted: false,
};

export interface JsxConfig {
/**
* When these attributes' values are set to object literals, they will be stringified to JSON.
*/
jsonAttributes: string[];
/**
* The sanitizer to be used by the runtime.
* Accepts a function of the signature `(raw: string, originalType: string) => string`.
* @note {@link JsxConfig.trusted} must be false for elements to be sanitized.
* @see {@link Sanitizer}
*/
sanitize: Sanitizer;
/**
* If false, value interpolations inside of JSX will be sanitized.
* @note Sanitization will change the return type of JSX functions to an object that overrides `toString`.
* In most cases it will function as expected, but you might sometimes need to manually coerce the JSX tree to a string.
*/
trusted: boolean;
}

type Sanitizer = false | ((raw: string, originalType: string) => string);
4 changes: 2 additions & 2 deletions src/typed-html/jsx-dev-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// <reference path="../jsx.d.ts" />

export { Fragment } from "./jsx-runtime";
import { jsx, jsxs } from "./jsx-runtime";
import { jsx, jsxs, type Node } from "./jsx-runtime";

export function jsxDEV(
tag: any,
Expand All @@ -10,7 +10,7 @@ export function jsxDEV(
_isStatic: boolean,
source: unknown,
_self: unknown,
): JSX.Element {
): Node | JSX.Element {
try {
return Array.isArray(props.children) ? jsxs(tag, props) : jsx(tag, props);
} catch (error) {
Expand Down
54 changes: 26 additions & 28 deletions src/typed-html/jsx-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,33 @@
/// <reference path="../jsx.d.ts" />

import { createElement } from "typed-html";
import { jsxConfig } from "../index";

export function Fragment({ children }: { children?: unknown | unknown[] }): JSX.Element {
if (Array.isArray(children)) return children.map(sanitizer).join('\n');
return sanitizer(children)
type Element = JSX.Element | Node;

export class Node {
constructor(private children: Element | Element[]) {}

toString(): string {
if (Array.isArray(this.children)) return this.children.join("\n");
return this.children.toString();
}
}

/**
* Configuration for the JSX runtime.
*/
export const config = {
/**
* These attributes' record values be converted to JSON strings.
*/
jsonAttributes: ["hx-vals", "hx-headers", "data-hx-vals", "data-hx-headers"],
/**
* The sanitizer to be used by the runtime.
* Accepts a function of the signature `(raw: string, originalType: string) => string`.
*/
sanitize: false as Sanitizer,
};

function sanitizer(value: unknown): string {
const str = value || value === 0 ? value.toString() : '';
if (!config.sanitize) return str;
return config.sanitize(str, typeof value);
export function Fragment({ children }: { children?: unknown }): Element {
if (Array.isArray(children)) return new Node(children.map(sanitizer));
return sanitizer(children);
}

type Sanitizer = false | ((raw: string, originalType: string) => string);
function sanitizer(value: unknown): Element {
const str = value || value === 0 ? value.toString() : "";
if (!jsxConfig.sanitize || jsxConfig.trusted) return str;
if (value instanceof Node) return value;
return jsxConfig.sanitize(str, typeof value);
}

function expandLiterals(props: Record<string, unknown>) {
for (const attr of config.jsonAttributes) {
for (const attr of jsxConfig.jsonAttributes) {
if (!(attr in props)) continue;
const value = props[attr];
if (typeof value === "object") {
Expand All @@ -40,13 +36,15 @@ function expandLiterals(props: Record<string, unknown>) {
}
}

export function jsx(tag: any, { children, ...props }: { children?: unknown | unknown[] }): JSX.Element {
export function jsx(tag: any, { children, ...props }: { children?: unknown }): Element {
expandLiterals(props);
const contents = Array.isArray(children) ? children.map(sanitizer) : [sanitizer(children)];
return createElement(tag, props, ...contents);
const elt = createElement(tag, props, ...(contents as any[]));
return jsxConfig.trusted ? elt : new Node(elt);
}

export function jsxs(tag: any, { children, ...props }: { children: unknown[] }): JSX.Element {
export function jsxs(tag: any, { children, ...props }: { children: unknown[] }): Element {
expandLiterals(props);
return createElement(tag, props, ...children.map(sanitizer));
const elt = createElement(tag, props, ...(children.map(sanitizer) as any[]));
return jsxConfig.trusted ? elt : new Node(elt);
}

0 comments on commit 6603910

Please sign in to comment.