commit ec4241ea88c66dc7d1bfaa275f6519137a8def9c Author: mehbark Date: Sun Jul 9 02:34:35 2023 -0400 jsx working diff --git a/DialogueTree.hs b/DialogueTree.hs new file mode 100644 index 0000000..2f12b33 --- /dev/null +++ b/DialogueTree.hs @@ -0,0 +1,29 @@ +module DialogueTree where + +import Text.Blaze.Html.Renderer.String +import Text.Blaze.Html5 + +data DTree + = T Html + | O Html [(Html, DTree)] + +ht :: (ToMarkup a) => a -> Html +ht = preEscapedToHtml + +normalizeOption :: (Html, DTree) -> Html +normalizeOption (h, t) = + details . ht $ + [summary h, normalize t] + +normalize :: DTree -> Html +normalize (T s) = s +normalize (O response things) = + details . ht $ + summary response + : (normalizeOption <$> things) + +render :: DTree -> String +render = renderHtml . normalize + +put :: DTree -> IO () +put = putStrLn . render diff --git a/html/deno.json b/html/deno.json new file mode 100644 index 0000000..ed711e5 --- /dev/null +++ b/html/deno.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "jsxFactory": "create_element", + "jsxFragmentFactory": "create_fragment", + "jsxImportSource": "./jsx" + }, + "imports": { + "html": "./html.ts", + "./jsx/jsx-runtime": "./jsx/jsx-runtime.ts" + } +} diff --git a/html/html.ts b/html/html.ts new file mode 100644 index 0000000..13069e4 --- /dev/null +++ b/html/html.ts @@ -0,0 +1,146 @@ +export type Prop = string | number | boolean; +export type Attributes = Record; +export type Html = string | NonText; +export type NonText = { + tag: string; + attributes: Attributes; + children: Html[]; +}; + +export const Fragment = "Fragment"; + +function is_string(elem: Html): elem is string { + return typeof elem == "string"; +} + +export function fr(...children: Html[]): Html { + return { tag: Fragment, attributes: {}, children }; +} + +function div(...children: Html[]): Html { + return { tag: "div", attributes: {}, children }; +} + +function expand_fragments_in_list(children: Html[]): Html[] { + let out = []; + + for (const child of children) { + if (is_string(child)) { + out.push(child); + } else if (child.tag == Fragment) { + out.push(...child.children.map(expand_fragments)); + } else { + out.push(child); + } + } + + return out; +} + +// NOTE: if you pass a fragment to this, it won't be expanded (which makes sense if you think about it) +function expand_fragments(elem: Html): Html { + if (is_string(elem)) { + return elem; + } else { + const children = expand_fragments_in_list(elem.children); + return { ...elem, children }; + } +} + +const attr = (attrs: Attributes) => (elem: Html): Html => { + if (is_string(elem)) { + return elem; + } else { + const { tag, attributes, children } = elem; + return { tag, attributes: { ...attributes, ...attrs }, children }; + } +}; + +const escape = (unsafe: string): string => { + return unsafe + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +}; + +function render_attributes(attrs: Attributes): string { + return ( + (Object.keys(attrs).length > 0 ? " " : "") + + Object.entries(attrs) + .filter(([_attr, val]) => typeof val != "undefined") + .map(([attr, val]) => `${attr}="${escape((val ?? "").toString())}"`) + .join(" ") + ); +} + +function indent(str: string, amount = 4, char = " "): string { + const ind = char.repeat(4); + return str + .split("\n") + .map((l) => ind + l) + .join("\n"); +} + +function render_elem( + { tag, attributes, children }: NonText, + mini = false, +): string { + if (children.length == 0) { + return ( + `<${tag}${render_attributes(attributes)}` + (mini ? "/>" : " />") + ); + } else { + let inner = ""; + let string_last = false; + for (let i = 0; i < children.length; i++) { + let child = children[i]; + const rendered = render(child, mini); + + if (is_string(child)) { + mini && string_last && (inner += " "); + inner += rendered; + + string_last = true; + } else { + inner += rendered; + string_last = false; + } + + if (!mini && i + 1 < children.length) inner += "\n"; + } + + return [ + `<${tag}${render_attributes(attributes)}>`, + mini ? inner : indent(inner), + ``, + ].join(mini ? "" : "\n"); + } +} +// old: +// joining with " " is inefficient, but necessary for correct string behavior +// i've decided that joining with "" is worth the size savings +// THIS MEANS MINIFICATION IS SEMANTICALLY DIFFERENT +// JK I DID IT THE HARD WAY :] + +export function render(elem: Html, mini = true): string { + if (is_string(elem)) { + return escape(elem); + } else if (elem.tag == Fragment) { + // mimics react's behavior with fragments, ehhh nvm different aims + return elem.children.map((elem) => render(elem, mini)).join("\n"); + } else { + const { tag, attributes } = elem; + const expanded = expand_fragments(elem); + + if (is_string(expanded)) { + throw "impossible"; + } + + return render_elem( + { tag, attributes, children: expanded.children }, + mini, + ); + } +} diff --git a/html/jsx/index.d.ts b/html/jsx/index.d.ts new file mode 100644 index 0000000..c9af03e --- /dev/null +++ b/html/jsx/index.d.ts @@ -0,0 +1,7 @@ +declare namespace JSX { + export interface IntrinsicElements { + [elemName: string]: any; + } +} + +declare var React: never; diff --git a/html/jsx/jsx-runtime.ts b/html/jsx/jsx-runtime.ts new file mode 100644 index 0000000..38c04f6 --- /dev/null +++ b/html/jsx/jsx-runtime.ts @@ -0,0 +1,47 @@ +import { Attributes, fr, Html, Prop } from "../html.ts"; + +type SadProp = Prop | undefined | Html | Html[]; +export type Component = ( + attributes: Attributes, + children: Html[], +) => Html; + +interface Props { + [key: string]: typeof key extends "children" ? never : SadProp; + children: undefined | Html | Html[]; +} + +export function create_element( + type: Component | string, + props_?: Props, +): Html { + console.log(arguments); + const props: Props = props_ ?? { children: [] }; + const children: Html[] = [props.children ?? []].flat(); + const attributes: Attributes = {}; + + for (const [key, val] of Object.entries(props)) { + if ( + key == "children" || typeof val == "undefined" || typeof val == "object" + ) continue; + attributes[key] = val; + } + + console.log(attributes, children); + + if (typeof type == "string") { + return { tag: type, attributes, children }; + } else { + return type(attributes, children); + } +} + +export function Fragment( + _: Record, + children: Html[], +) { + return fr(...children); +} + +export const jsx = create_element; +export const jsxs = create_element; diff --git a/html/main.tsx b/html/main.tsx new file mode 100644 index 0000000..47ae8d6 --- /dev/null +++ b/html/main.tsx @@ -0,0 +1,32 @@ +import { Html, render } from "./html.ts"; +import { Component } from "./jsx/jsx-runtime.ts"; + +const Homestuck: Component = (attrs, children) => ( +
+ {...children} +
+); + +const bla: Html = ( + + + text +
+ <> + homestuck + is + cool + + <> + vast error is + also + cool + +
+
+); +console.log(bla); +console.log(render(bla, false)); +console.log(render(bla, true)); + +export {};