chore: setup and run prettier

This commit is contained in:
CoolElectronics 2024-03-13 20:16:11 -04:00
parent ffe095557e
commit 7a131a3e2e
No known key found for this signature in database
GPG key ID: F63593D168636C50
17 changed files with 1418 additions and 1512 deletions

123
DreamlandJS.d.ts vendored
View file

@ -1,84 +1,97 @@
declare namespace JSX {
export type IntrinsicElements = {
[index: string]: any
};
type ElementType = Fragment | string | Component<any, any>;
type Element = DLElement<any>;
export type IntrinsicElements = {
[index: string]: any
}
type ElementType = Fragment | string | Component<any, any>
type Element = DLElement<any>
interface ElementAttributesProperty {
props: {};
}
interface ElementChildrenAttribute {
children: {};
}
interface ElementAttributesProperty {
props: {}
}
interface ElementChildrenAttribute {
children: {}
}
}
declare function h(
type: string,
props?: { [index: string]: any } | null,
...children: (HTMLElement | string)[]
): Node;
declare function $if(condition: DLPointer<any> | any, then?: Element, otherwise?: Element): HTMLElement;
type: string,
props?: { [index: string]: any } | null,
...children: (HTMLElement | string)[]
): Node
declare function $if(
condition: DLPointer<any> | any,
then?: Element,
otherwise?: Element
): HTMLElement
type DLPointer<T> = { readonly __symbol: unique symbol, readonly __signature: T };
type DLPointer<T> = {
readonly __symbol: unique symbol
readonly __signature: T
}
declare function use<T>(sink: T, mapping?: (arg: T) => any): DLPointer<T>;
declare function useValue<T>(trap: DLPointer<T>): T;
declare function use<T>(sink: T, mapping?: (arg: T) => any): DLPointer<T>
declare function useValue<T>(trap: DLPointer<T>): T
type Stateful<T> = T & { readonly symbol: unique symbol };
type Stateful<T> = T & { readonly symbol: unique symbol }
declare function stateful<T>(target: T): Stateful<T>
declare function $state<T>(target: T): Stateful<T>
declare function $store<T>(
target: T,
ident: string,
backing: 'localstorage'
): Stateful<T>
declare function stateful<T>(target: T): Stateful<T>;
declare function $state<T>(target: T): Stateful<T>;
declare function $store<T>(target: T, ident: string, backing: "localstorage"): Stateful<T>;
declare function handle<T>(
references: DLPointer<T>,
callback: (value: T) => void
): void
declare function handle<T>(references: DLPointer<T>, callback: (value: T) => void): void;
declare function css(strings: TemplateStringsArray, ...values: any): string
declare function rule(strings: TemplateStringsArray, ...values: any): string
declare var styled: { new: typeof css; rule: typeof rule }
declare function css(strings: TemplateStringsArray, ...values: any): string;
declare function rule(strings: TemplateStringsArray, ...values: any): string;
declare var styled: { new: typeof css, rule: typeof rule };
type DLCSS = string
type DLCSS = string;
declare var $el: HTMLElement
declare var $el: HTMLElement;
type Fragment = { readonly fragment: unique symbol };
declare var Fragment: Fragment;
type Fragment = { readonly fragment: unique symbol }
declare var Fragment: Fragment
interface Element {
$: OuterComponentTypes & { [index: string | symbol]: any }
$: OuterComponentTypes & { [index: string | symbol]: any }
}
interface DLElement<T> extends HTMLElement {
$: T & OuterComponentTypes
$: T & OuterComponentTypes
}
type ComponentElement<T extends (...args: any) => any> = ReturnType<T>;
type ComponentElement<T extends (...args: any) => any> = ReturnType<T>
type OuterComponentTypes = {
root: Element,
children: Element[],
root: Element
children: Element[]
}
type InnerComponentTypes = {
css: DLCSS,
mount?: () => void,
css: DLCSS
mount?: () => void
}
type ComponentTypes = OuterComponentTypes & InnerComponentTypes;
type ComponentTypes = OuterComponentTypes & InnerComponentTypes
type ArrayOrSingular<T extends []> = T | T[keyof T];
type ArrayOrSingular<T extends []> = T | T[keyof T]
type Component<Public, Private, Constructed extends string | symbol | number = never> =
(
(
this: Public & Private & ComponentTypes,
props: (
[Constructed] extends [never] ? Public : Omit<Public, Constructed>
) &
{
children?: ArrayOrSingular<Private extends { children: any } ? Private["children"] : never>
type Component<
Public,
Private,
Constructed extends string | symbol | number = never,
> = (
this: Public & Private & ComponentTypes,
props: ([Constructed] extends [never]
? Public
: Omit<Public, Constructed>) & {
children?: ArrayOrSingular<
Private extends { children: any } ? Private['children'] : never
>
[index: `${'bind:'}${string}`]: any
},
) => DLElement<Public>
)
}
) => DLElement<Public>

127
README.md
View file

@ -1,83 +1,89 @@
## What is Dreamland?
dreamland.js is a reactive JSX-inspired rendering library with **no virtual dom** and **no build step**
## Why Dreamland?
We've found frameworks such as React to be cumbersome, with more than just a few footguns. Dreamland can get you fast results with brutal simplicity. See the [documentation](https://dreamland.js.org) for more information.
## What does it look like?
Here's a simple counter app
```jsx
function App() {
this.counter = 0;
return (
<div>
<button on:click={() => this.counter++}>Click me!</button>
<p>
{use(this.counter)}
</p>
</div>
);
this.counter = 0
return (
<div>
<button on:click={() => this.counter++}>Click me!</button>
<p>{use(this.counter)}</p>
</div>
)
}
window.addEventListener("load", () => {
document.body.appendChild(<App/>);
});
window.addEventListener('load', () => {
document.body.appendChild(<App />)
})
```
Compare that to the equivalent code in react:
```jsx
import { React, useState } from 'react'
function App() {
const [counter, setCounter] = useState(0);
const [counter, setCounter] = useState(0)
const increase = () => {
setCounter(count => count + 1);
};
const increase = () => {
setCounter((count) => count + 1)
}
return (
<div>
<button onClick={increase}>Click me!</button>
<p>
Value: {counter}
</p>
</div>
);
return (
<div>
<button onClick={increase}>Click me!</button>
<p>Value: {counter}</p>
</div>
)
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
```
The idea of dreamland is to get some of the convience of big framworks at a ridiculously tiny size (~3kb, smaller than preact) with less hurdles.
# Getting Started
Dreamland can be integrated into plain-javascript applications gradually and seamlessly. See the [Wiki](https://github.com/MercuryWorkshop/dreamlandjs/wiki) for learning the concepts that dreamland uses.
## Plain JS
In your HTML file, add `<script src="https://unpkg.com/dreamland"></script>` somewhere. This unlocks the html builder allowing you to start writing dreamland code, such as the example shown below
```javascript
function App() {
this.counter = 0;
return html`
<div>
<button on:click=${() => this.counter++}>Click me!</button>
<p>
${use(this.counter)}
</p>
</div>
`;
this.counter = 0
return html`
<div>
<button on:click=${() => this.counter++}>Click me!</button>
<p>${use(this.counter)}</p>
</div>
`
}
window.addEventListener("load", () => {
document.body.appendChild(h(App));
});
window.addEventListener('load', () => {
document.body.appendChild(h(App))
})
```
## Typescript + Bundler (vite, rollup, webpack, esbuild, etc)
First install dreamland (`npm install dreamland`), then add this to the compileroptions of your `tsconfig.json` to setup JSX.
```json
"jsx":"react",
"jsxFactory":"h",
@ -89,27 +95,28 @@ In the entry point of the app, add the line `import "dreamland/dev"` into at lea
```tsx
// typescript syntax for defining components
const App: Component<{
// component properties. if you had a component that took a property like `<Button text="..." /> you would use a type like the one in the following line
// text: string
},{
// types for internal state
counter: number
}> = function() {
this.counter = 0;
return (
<div>
<button on:click={() => this.counter++}>Click me!</button>
<p>
{use(this.counter)}
</p>
</div>
);
const App: Component<
{
// component properties. if you had a component that took a property like `<Button text="..." /> you would use a type like the one in the following line
// text: string
},
{
// types for internal state
counter: number
}
> = function () {
this.counter = 0
return (
<div>
<button on:click={() => this.counter++}>Click me!</button>
<p>{use(this.counter)}</p>
</div>
)
}
window.addEventListener("load", () => {
document.body.appendChild(<App/>);
});
window.addEventListener('load', () => {
document.body.appendChild(<App />)
})
```
See the [documentation](https://dreamland.js.org) for more information.

View file

@ -1,18 +1,14 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<title>Dreamland examples</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="index.css" />
<script src="../index.js"></script>
<head>
<title>Dreamland examples</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="index.css" />
<script src="../index.js"></script>
<script src="lib/index.js"></script>
</head>
<body>
</body>
<script src="lib/index.js"></script>
</head>
<body></body>
</html>

View file

@ -1,20 +1,21 @@
html {
padding: 0;
margin: 0;
width: 100vw;
height: 100vh;
padding: 0;
margin: 0;
width: 100vw;
height: 100vh;
}
body {
background-color: #191724;
background-color: #191724;
}
.box {
background-color: #1f1d2e;
border-radius: 25px;
padding:2em;
background-color: #1f1d2e;
border-radius: 25px;
padding: 2em;
}
p,h1 {
font-family: "serif";
color: #e0def4;
p,
h1 {
font-family: 'serif';
color: #e0def4;
}

View file

@ -1,40 +1,39 @@
function Counter() {
this.css = css`
self {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
this.css = css`
self {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
button {
border-radius: 5px;
border: none;
outline: none;
width: 10em;
height:5em;
background-color: #f6c177;
}
button {
border-radius: 5px;
border: none;
outline: none;
width: 10em;
height: 5em;
background-color: #f6c177;
}
p {
font-size: 20px;
}
`;
p {
font-size: 20px;
}
`
this.counter ??= 0;
this.counter ??= 0
return (
<div class="box">
<h1>Counter</h1>
<p>
Value: {use(this.counter)}
</p>
<button on:click={() => this.counter++} >Click me!</button>
<p>
is {use(this.counter)} odd? {use(this.counter, p => p % 2 == 1)}
</p>
</div>
)
return (
<div class="box">
<h1>Counter</h1>
<p>Value: {use(this.counter)}</p>
<button on:click={() => this.counter++}>Click me!</button>
<p>
is {use(this.counter)} odd?{' '}
{use(this.counter, (p) => p % 2 == 1)}
</p>
</div>
)
}
//
// function ToDoList() {
@ -120,14 +119,20 @@ function Counter() {
// );
// }
window.addEventListener("load", () => {
document.body.appendChild(<Counter />);
});
window.addEventListener('load', () => {
document.body.appendChild(<Counter />)
})
let a = stateful({ b: stateful({ c: stateful({ d: 0 }) }), array: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] }) as any;
let r = use(a.array[a.b.c.d][a.b.c.d]);
let a = stateful({
b: stateful({ c: stateful({ d: 0 }) }),
array: [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
],
}) as any
let r = use(a.array[a.b.c.d][a.b.c.d])
handle(r, v => {
console.log(v);
});
handle(r, (v) => {
console.log(v)
})

View file

@ -1,55 +1,56 @@
{
"name": "dreamland",
"version": "0.0.3",
"description": "A utilitarian HTML rendering library",
"scripts": {
"build": "rollup -c",
"watch": "rollup -cw"
},
"keywords": [
"html",
"jsx",
"framework",
"dreamlandjs",
"dreamland"
],
"author": "MercuryWorkshop",
"repository": "https://github.com/MercuryWorkshop/dreamlandjs",
"license": "MIT",
"browser": "./dist/dev/index.js",
"types": "./DreamlandJS.d.ts",
"node": "./dist/dev/index.js",
"exports": {
"./dev": {
"default": "./dist/dev/index.js",
"types": "./DreamlandJS.d.ts"
"name": "dreamland",
"version": "0.0.4",
"description": "A utilitarian HTML rendering library",
"scripts": {
"build": "rollup -c && prettier . --check",
"watch": "rollup -cw",
"publish": "npm publish --access public"
},
"./js": {
"default": "./dist/js.js",
"types": "./DreamlandJS.d.ts"
"keywords": [
"html",
"jsx",
"framework",
"dreamlandjs",
"dreamland"
],
"author": "MercuryWorkshop",
"repository": "https://github.com/MercuryWorkshop/dreamlandjs",
"license": "MIT",
"browser": "./dist/dev/index.js",
"types": "./DreamlandJS.d.ts",
"node": "./dist/dev/index.js",
"exports": {
"./dev": {
"default": "./dist/dev/index.js",
"types": "./DreamlandJS.d.ts"
},
"./js": {
"default": "./dist/js.js",
"types": "./DreamlandJS.d.ts"
},
"./css": {
"default": "./dist/css.js",
"types": "./DreamlandJS.d.ts"
},
"./html": {
"default": "./dist/html.js",
"types": "./DreamlandJS.d.ts"
},
"./store": {
"default": "./dist/store.js",
"types": "./DreamlandJS.d.ts"
}
},
"./css": {
"default": "./dist/css.js",
"types": "./DreamlandJS.d.ts"
},
"./html": {
"default": "./dist/html.js",
"types": "./DreamlandJS.d.ts"
},
"./store": {
"default": "./dist/store.js",
"types": "./DreamlandJS.d.ts"
"files": [
"dist",
"DreamlandJS.d.ts"
],
"devDependencies": {
"@rollup/plugin-strip": "^3.0.4",
"prettier": "^3.2.5",
"rollup": "^4.12.1",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^5.3.3"
}
},
"files": [
"dist",
"DreamlandJS.d.ts"
],
"devDependencies": {
"rollup": "^4.12.1",
"@rollup/plugin-strip": "^3.0.4",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^5.3.3"
}
}

1283
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

11
prettier.config.js Normal file
View file

@ -0,0 +1,11 @@
// prettier.config.js, .prettierrc.js, prettier.config.cjs, or .prettierrc.cjs
/** @type {import("prettier").Config} */
const config = {
trailingComma: 'es5',
tabWidth: 4,
semi: false,
singleQuote: true,
}
module.exports = config

View file

@ -1,74 +1,74 @@
import strip from "@rollup/plugin-strip";
import { terser } from "rollup-plugin-terser";
import strip from '@rollup/plugin-strip'
import { terser } from 'rollup-plugin-terser'
const prodPlugins = [
strip({
functions: ["console.log", "assert.*", "panic", "log"],
labels: ["dev"]
}),
terser({
mangle: {
toplevel: true
},
compress: {
unused: true,
collapse_vars: true,
toplevel: true
},
output: {
comments: false
},
})
strip({
functions: ['console.log', 'assert.*', 'panic', 'log'],
labels: ['dev'],
}),
terser({
mangle: {
toplevel: true,
},
compress: {
unused: true,
collapse_vars: true,
toplevel: true,
},
output: {
comments: false,
},
}),
]
const devPlugins = [];
const devPlugins = []
export default [
{
input: "src/dev.js",
output: {
file: "dist/dev/index.js",
format: 'cjs',
sourcemap: true,
{
input: 'src/dev.js',
output: {
file: 'dist/dev/index.js',
format: 'cjs',
sourcemap: true,
},
plugins: devPlugins,
},
plugins: devPlugins
},
{
input: "src/js.js",
output: {
file: "dist/js.js",
strict: false,
format: 'cjs',
sourcemap: true,
{
input: 'src/js.js',
output: {
file: 'dist/js.js',
strict: false,
format: 'cjs',
sourcemap: true,
},
plugins: prodPlugins,
},
plugins: prodPlugins
},
{
input: "src/css.js",
output: {
file: "dist/css.js",
strict: false,
format: 'cjs',
sourcemap: true,
{
input: 'src/css.js',
output: {
file: 'dist/css.js',
strict: false,
format: 'cjs',
sourcemap: true,
},
plugins: prodPlugins,
},
plugins: prodPlugins
},
{
input: "src/html.js",
output: {
file: "dist/html.js",
strict: false,
format: 'cjs',
sourcemap: true,
{
input: 'src/html.js',
output: {
file: 'dist/html.js',
strict: false,
format: 'cjs',
sourcemap: true,
},
plugins: prodPlugins,
},
plugins: prodPlugins
},
{
input: "src/store.js",
output: {
file: "dist/store.js",
strict: false,
format: 'cjs',
sourcemap: true,
{
input: 'src/store.js',
output: {
file: 'dist/store.js',
strict: false,
format: 'cjs',
sourcemap: true,
},
plugins: prodPlugins,
},
plugins: prodPlugins
}
];
]

View file

@ -1,27 +1,26 @@
class DreamlandError extends Error {
constructor(message) {
super("[dreamland-js/dev] " + message);
this.name = "DreamlandDevError";
}
constructor(message) {
super('[dreamland-js/dev] ' + message)
this.name = 'DreamlandDevError'
}
}
export function log(message) {
console.log("[dreamland-js/dev] " + message);
console.log('[dreamland-js/dev] ' + message)
}
export function panic(message) {
throw new DreamlandError("fatal: " + message);
throw new DreamlandError('fatal: ' + message)
}
export function assert(condition, message) {
if (!condition) {
panic(message);
}
if (!condition) {
panic(message)
}
}
dev: assert.eq = (a, b) => {
if (a != b) panic("Assertion failed: " + a + " != " + b);
};
if (a != b) panic('Assertion failed: ' + a + ' != ' + b)
}
dev: assert.neq = (a, b) => {
if (a == b) panic("Assertion failed: " + a + " == " + b);
};
if (a == b) panic('Assertion failed: ' + a + ' == ' + b)
}

View file

@ -1 +1 @@
export const VERSION = "0.0.2-beta";
export const VERSION = '0.0.2-beta'

View file

@ -1,84 +1,76 @@
Object.assign(window, { css, rule, styled: { new: css, rule: rule } });
const cssmap = {};
Object.assign(window, { css, rule, styled: { new: css, rule: rule } })
const cssmap = {}
function scopify_css(uid, css) {
const virtualDoc = document.implementation.createHTMLDocument("");
const virtualStyleElement = document.createElement("style");
virtualDoc.body.appendChild(virtualStyleElement);
const virtualDoc = document.implementation.createHTMLDocument('')
const virtualStyleElement = document.createElement('style')
virtualDoc.body.appendChild(virtualStyleElement)
let cssParsed = "";
let cssParsed = ''
virtualStyleElement.textContent = css;
virtualStyleElement.textContent = css
//@ts-ignore
for (const rule of virtualStyleElement.sheet.cssRules) {
rule.selectorText = rule.selectorText.includes("self")
? `.${uid}.self${rule.selectorText.replace("self", "")}`
: `.${uid} ${rule.selectorText}`;
cssParsed += `${rule.cssText}\n`;
}
//@ts-ignore
for (const rule of virtualStyleElement.sheet.cssRules) {
rule.selectorText = rule.selectorText.includes('self')
? `.${uid}.self${rule.selectorText.replace('self', '')}`
: `.${uid} ${rule.selectorText}`
cssParsed += `${rule.cssText}\n`
}
return cssParsed;
return cssParsed
}
function tagcss(strings, values, isblock) {
let cached = cssmap[strings[0]];
let cachable = strings.length == 1;
if (cachable && cached) return cached;
const uid = `dl${Array(5)
.fill(0)
.map(() => {
return Math.floor(Math.random() * 36).toString(36);
})
.join("")}`;
let cached = cssmap[strings[0]]
let cachable = strings.length == 1
if (cachable && cached) return cached
const uid = `dl${Array(5)
.fill(0)
.map(() => {
return Math.floor(Math.random() * 36).toString(36)
})
.join('')}`
const styleElement = document.createElement("style");
const styleElement = document.createElement('style')
document.head.appendChild(styleElement);
document.head.appendChild(styleElement)
const flattened_template = [];
for (const i in strings) {
flattened_template.push(strings[i]);
if (values[i]) {
const prop = values[i];
const flattened_template = []
for (const i in strings) {
flattened_template.push(strings[i])
if (values[i]) {
const prop = values[i]
if (isDLPtr(prop)) {
const current_i = flattened_template.length;
let oldparsed;
handle(prop, (val) => {
flattened_template[current_i] = String(val);
let parsed = flattened_template.join("");
if (parsed != oldparsed)
if (isblock)
styleElement.textContent = scopify_css(
uid,
parsed,
);
else
styleElement.textContent = `.${uid} { ${parsed}; }`
oldparsed = parsed;
});
} else {
flattened_template.push(String(prop));
}
if (isDLPtr(prop)) {
const current_i = flattened_template.length
let oldparsed
handle(prop, (val) => {
flattened_template[current_i] = String(val)
let parsed = flattened_template.join('')
if (parsed != oldparsed)
if (isblock)
styleElement.textContent = scopify_css(uid, parsed)
else styleElement.textContent = `.${uid} { ${parsed}; }`
oldparsed = parsed
})
} else {
flattened_template.push(String(prop))
}
}
}
}
if (isblock) {
styleElement.textContent = scopify_css(
uid,
flattened_template.join(""),
);
} else {
styleElement.textContent = `.${uid} { ${flattened_template.join("")}; }`
}
if (isblock) {
styleElement.textContent = scopify_css(uid, flattened_template.join(''))
} else {
styleElement.textContent = `.${uid} { ${flattened_template.join('')}; }`
}
if (cachable) cssmap[strings[0]] = uid;
if (cachable) cssmap[strings[0]] = uid
return uid;
return uid
}
function rule(strings, ...values) {
return tagcss(strings, values, false)
return tagcss(strings, values, false)
}
function css(strings, ...values) {
return tagcss(strings, values, true);
return tagcss(strings, values, true)
}

View file

@ -1,10 +1,12 @@
import { log } from "./asserts"
import { VERSION } from "./consts"
import { log } from './asserts'
import { VERSION } from './consts'
import "./js"
import "./css"
import "./html"
import "./store"
import './js'
import './css'
import './html'
import './store'
log("Version: " + VERSION)
console.warn("This is a DEVELOPER build of dreamland.js. It is not suitable for production use.")
log('Version: ' + VERSION)
console.warn(
'This is a DEVELOPER build of dreamland.js. It is not suitable for production use.'
)

View file

@ -1,65 +1,66 @@
Object.assign(window, { html });
Object.assign(window, { html })
function html(strings, ...values) {
let flattened = "";
let markers = {};
for (const i in strings) {
let string = strings[i];
let value = values[i];
let flattened = ''
let markers = {}
for (const i in strings) {
let string = strings[i]
let value = values[i]
flattened += string;
if (i < values.length) {
let dupe = Object.values(markers).findIndex(v => v == value);
if (dupe !== -1) {
flattened += Object.keys(markers)[dupe];
} else {
let marker = "m" + Array(16).fill(0).map(() => Math.floor(Math.random() * 16).toString(16)).join("");
markers[marker] = value;
flattened += marker;
}
flattened += string
if (i < values.length) {
let dupe = Object.values(markers).findIndex((v) => v == value)
if (dupe !== -1) {
flattened += Object.keys(markers)[dupe]
} else {
let marker =
'm' +
Array(16)
.fill(0)
.map(() => Math.floor(Math.random() * 16).toString(16))
.join('')
markers[marker] = value
flattened += marker
}
}
}
}
let dom = new DOMParser().parseFromString(flattened, "text/html");
if (dom.body.children.length !== 1)
throw "html builder needs exactly one child";
let dom = new DOMParser().parseFromString(flattened, 'text/html')
if (dom.body.children.length !== 1)
throw 'html builder needs exactly one child'
function wraph(elm) {
let nodename = elm.nodeName.toLowerCase();
if (nodename === "#text")
return elm.textContent;
if (nodename in markers)
nodename = markers[nodename];
function wraph(elm) {
let nodename = elm.nodeName.toLowerCase()
if (nodename === '#text') return elm.textContent
if (nodename in markers) nodename = markers[nodename]
let children = [...elm.childNodes].map(wraph);
for (let i = 0; i < children.length; i++) {
let text = children[i];
if (typeof text !== "string") continue;
for (const [marker, value] of Object.entries(markers)) {
if (!text) break;
if (!text.includes(marker)) continue;
let before;
[before, text] = text.split(marker);
children = [
...children.slice(0, i),
before,
value,
text,
...children.slice(i + 1)
];
i += 2;
}
let children = [...elm.childNodes].map(wraph)
for (let i = 0; i < children.length; i++) {
let text = children[i]
if (typeof text !== 'string') continue
for (const [marker, value] of Object.entries(markers)) {
if (!text) break
if (!text.includes(marker)) continue
let before
;[before, text] = text.split(marker)
children = [
...children.slice(0, i),
before,
value,
text,
...children.slice(i + 1),
]
i += 2
}
}
let attributes = {}
for (const attr of [...elm.attributes]) {
let val = attr.nodeValue
if (val in markers) val = markers[val]
attributes[attr.name] = val
}
return h(nodename, attributes, children)
}
let attributes = {};
for (const attr of [...elm.attributes]) {
let val = attr.nodeValue;
if (val in markers)
val = markers[val];
attributes[attr.name] = val;
}
return h(nodename, attributes, children);
}
return wraph(dom.body.children[0]);
return wraph(dom.body.children[0])
}

675
src/js.js
View file

@ -1,17 +1,17 @@
import { assert } from "./asserts";
import { assert } from './asserts'
// enables a small terser optimization
let document = self.document;
let document = self.document
let Fragment = Symbol();
let Fragment = Symbol()
// We add some extra properties into various objects throughout, better to use symbols and not interfere. this is just a tiny optimization
let [USE_MAPFN, TARGET, PROXY, STEPS, LISTENERS, IF] = [, , , , , ,].fill().map(Symbol);
let [USE_MAPFN, TARGET, PROXY, STEPS, LISTENERS, IF] = [, , , , , ,]
.fill()
.map(Symbol)
// whether to return the true value from a stateful object or a "trap" containing the pointer
let __use_trap = false;
let __use_trap = false
// Say you have some code like
//// let state = stateful({
@ -29,399 +29,418 @@ let __use_trap = false;
// - the JSX factory h() is now passed the trap, which essentially contains a set of pointers pointing to the theoretical value of b
// - with the setter on the stateful proxy, we can listen to any change in any of the nested layers and call whatever listeners registered
// - the result is full intuitive reactivity with minimal overhead
Object.defineProperty(window, "use", {
get: () => {
__use_trap = true;
return (ptr, mapping, ...rest) => {
if (ptr instanceof Array) return usestr(ptr, mapping, ...rest);
assert(isDLPtr(ptr), "a value was passed into use() that was not part of a stateful context");
__use_trap = false;
if (mapping) ptr[USE_MAPFN] = mapping;
return ptr;
};
}
});
Object.defineProperty(window, 'use', {
get: () => {
__use_trap = true
return (ptr, mapping, ...rest) => {
if (ptr instanceof Array) return usestr(ptr, mapping, ...rest)
assert(
isDLPtr(ptr),
'a value was passed into use() that was not part of a stateful context'
)
__use_trap = false
if (mapping) ptr[USE_MAPFN] = mapping
return ptr
}
},
})
const usestr = (strings, ...values) => {
__use_trap = false;
__use_trap = false
let state = stateful({});
const flattened_template = [];
for (const i in strings) {
flattened_template.push(strings[i]);
if (values[i]) {
const prop = values[i];
let state = stateful({})
const flattened_template = []
for (const i in strings) {
flattened_template.push(strings[i])
if (values[i]) {
const prop = values[i]
if (isDLPtr(prop)) {
const current_i = flattened_template.length;
let oldparsed;
handle(prop, (val) => {
flattened_template[current_i] = String(val);
let parsed = flattened_template.join("");
if (parsed != oldparsed)
state.string = parsed
oldparsed = parsed;
});
} else {
flattened_template.push(String(prop));
}
if (isDLPtr(prop)) {
const current_i = flattened_template.length
let oldparsed
handle(prop, (val) => {
flattened_template[current_i] = String(val)
let parsed = flattened_template.join('')
if (parsed != oldparsed) state.string = parsed
oldparsed = parsed
})
} else {
flattened_template.push(String(prop))
}
}
}
}
state.string = flattened_template.join("");
state.string = flattened_template.join('')
return use(state.string);
};
Object.assign(window, { isDLPtr, h, stateful, handle, $if, Fragment });
return use(state.string)
}
Object.assign(window, { isDLPtr, h, stateful, handle, $if, Fragment })
let TRAPS = new Map;
let TRAPS = new Map()
// This wraps the target in a proxy, doing 2 things:
// - whenever a property is accessed, return a "trap" that catches and records accessors
// - whenever a property is set, notify the subscribed listeners
// This is what makes our "pass-by-reference" magic work
function stateful(target, hook) {
assert(isobj(target), "stateful() requires an object");
target[LISTENERS] = [];
target[TARGET] = target;
let TOPRIMITIVE = Symbol.toPrimitive;
assert(isobj(target), 'stateful() requires an object')
target[LISTENERS] = []
target[TARGET] = target
let TOPRIMITIVE = Symbol.toPrimitive
let proxy = new Proxy(target, {
get(target, property, proxy) {
if (__use_trap) {
let sym = Symbol();
let trap = new Proxy({
[TARGET]: target,
[PROXY]: proxy,
[STEPS]: [property],
[TOPRIMITIVE]: _ => sym,
}, {
get(target, property) {
if ([TARGET, PROXY, STEPS, USE_MAPFN, TOPRIMITIVE].includes(property)) return target[property];
property = TRAPS.get(property) || property;
target[STEPS].push(property);
return trap;
}
});
TRAPS.set(sym, trap);
let proxy = new Proxy(target, {
get(target, property, proxy) {
if (__use_trap) {
let sym = Symbol()
let trap = new Proxy(
{
[TARGET]: target,
[PROXY]: proxy,
[STEPS]: [property],
[TOPRIMITIVE]: (_) => sym,
},
{
get(target, property) {
if (
[
TARGET,
PROXY,
STEPS,
USE_MAPFN,
TOPRIMITIVE,
].includes(property)
)
return target[property]
property = TRAPS.get(property) || property
target[STEPS].push(property)
return trap
},
}
)
TRAPS.set(sym, trap)
return trap;
}
return Reflect.get(target, property, proxy);
},
set(target, property, val) {
if (hook) hook(target, property, val);
let trap = Reflect.set(target, property, val);
for (let listener of target[LISTENERS]) {
listener(target, property, val);
}
return trap;
},
});
return trap
}
return Reflect.get(target, property, proxy)
},
set(target, property, val) {
if (hook) hook(target, property, val)
let trap = Reflect.set(target, property, val)
for (let listener of target[LISTENERS]) {
listener(target, property, val)
}
return trap
},
})
return proxy;
return proxy
}
let isobj = (o) => o instanceof Object;
let isfn = (o) => typeof o === "function";
let isobj = (o) => o instanceof Object
let isfn = (o) => typeof o === 'function'
function isDLPtr(arr) {
return isobj(arr) && TARGET in arr
return isobj(arr) && TARGET in arr
}
function $if(condition, then, otherwise) {
otherwise ??= document.createTextNode("");
if (!isDLPtr(condition)) return condition ? then : otherwise;
otherwise ??= document.createTextNode('')
if (!isDLPtr(condition)) return condition ? then : otherwise
return { [IF]: condition, then, otherwise };
return { [IF]: condition, then, otherwise }
}
// This lets you subscribe to a stateful object
function handle(ptr, callback) {
assert(isDLPtr(ptr), "handle() requires a stateful object");
assert(isfn(callback), "handle() requires a callback function");
let step, resolvedSteps = [];
assert(isDLPtr(ptr), 'handle() requires a stateful object')
assert(isfn(callback), 'handle() requires a callback function')
let step,
resolvedSteps = []
function update() {
let val = ptr[TARGET];
for (step of resolvedSteps) {
val = val[step];
if (!isobj(val)) break;
}
let mapfn = ptr[USE_MAPFN];
if (mapfn) val = mapfn(val);
callback(val);
}
// inject ourselves into nested objects
let curry = (target, i) => function subscription(tgt, prop, val) {
if (prop === resolvedSteps[i] && target === tgt) {
update();
if (isobj(val)) {
let v = val[LISTENERS];
if (v && !v.includes(subscription)) {
v.push(curry(val[TARGET], i + 1));
function update() {
let val = ptr[TARGET]
for (step of resolvedSteps) {
val = val[step]
if (!isobj(val)) break
}
}
let mapfn = ptr[USE_MAPFN]
if (mapfn) val = mapfn(val)
callback(val)
}
};
// inject ourselves into nested objects
let curry = (target, i) =>
function subscription(tgt, prop, val) {
if (prop === resolvedSteps[i] && target === tgt) {
update()
// imagine we have a `use(state.a[state.b])`
// simply recursively resolve any of the intermediate steps until we get to the final value
// this will "misfire" occassionaly with a scenario like state.a[state.b][state.c] and call the listener more than needed
// it is up to the caller to not implode
for (let i in ptr[STEPS]) {
let step = ptr[STEPS][i];
if (isobj(step) && step[TARGET]) {
handle(step, val => {
resolvedSteps[i] = val;
update();
});
continue;
if (isobj(val)) {
let v = val[LISTENERS]
if (v && !v.includes(subscription)) {
v.push(curry(val[TARGET], i + 1))
}
}
}
}
// imagine we have a `use(state.a[state.b])`
// simply recursively resolve any of the intermediate steps until we get to the final value
// this will "misfire" occassionaly with a scenario like state.a[state.b][state.c] and call the listener more than needed
// it is up to the caller to not implode
for (let i in ptr[STEPS]) {
let step = ptr[STEPS][i]
if (isobj(step) && step[TARGET]) {
handle(step, (val) => {
resolvedSteps[i] = val
update()
})
continue
}
resolvedSteps[i] = step
}
resolvedSteps[i] = step;
}
let sub = curry(ptr[TARGET], 0);
ptr[TARGET][LISTENERS].push(sub);
let sub = curry(ptr[TARGET], 0)
ptr[TARGET][LISTENERS].push(sub)
sub(ptr[TARGET], resolvedSteps[0], ptr[TARGET][resolvedSteps[0]]);
sub(ptr[TARGET], resolvedSteps[0], ptr[TARGET][resolvedSteps[0]])
}
function JSXAddFixedWrapper(ptr, cb, $if) {
let before, appended, first, flag;
handle(ptr, val => {
first = appended?.[0];
if (first)
before = first.previousSibling || (flag = first.parentNode);
if (appended)
appended.forEach(a => a.remove());
let before, appended, first, flag
handle(ptr, (val) => {
first = appended?.[0]
if (first) before = first.previousSibling || (flag = first.parentNode)
if (appended) appended.forEach((a) => a.remove())
appended = JSXAddChild($if ? (val ? $if.then : $if.otherwise) : val, el => {
if (before) {
if (flag) {
before.prepend(el)
flag = null;
}
else before.after(el);
before = el;
}
else cb(el)
appended = JSXAddChild(
$if ? (val ? $if.then : $if.otherwise) : val,
(el) => {
if (before) {
if (flag) {
before.prepend(el)
flag = null
} else before.after(el)
before = el
} else cb(el)
}
)
})
})
}
// returns a function that sets a reference
// the currying is a small optimization
let curryset = ptr => val => {
let next = ptr[PROXY];
let steps = ptr[STEPS];
let i = 0;
for (; i < steps.length - 1; i++) {
next = next[steps[i]];
if (!isobj(next)) return;
}
next[steps[i]] = val;
let curryset = (ptr) => (val) => {
let next = ptr[PROXY]
let steps = ptr[STEPS]
let i = 0
for (; i < steps.length - 1; i++) {
next = next[steps[i]]
if (!isobj(next)) return
}
next[steps[i]] = val
}
// Actual JSX factory. Responsible for creating the HTML elements and all of the *reactive* syntactic sugar
function h(type, props, ...children) {
if (type == Fragment) return children;
if (typeof type == "function") {
// functional components. create the stateful object
let newthis = stateful(Object.create(type.prototype));
if (type == Fragment) return children
if (typeof type == 'function') {
// functional components. create the stateful object
let newthis = stateful(Object.create(type.prototype))
for (let name in props) {
let ptr = props[name]
if (name.startsWith('bind:')) {
assert(
isDLPtr(ptr),
'bind: requires a reference pointer from use'
)
let set = curryset(ptr)
let propname = name.substring(5)
if (propname == 'this') {
set(newthis)
} else {
// component two way data binding!! (exact same behavior as svelte:bind)
let isRecursive = false
handle(ptr, (value) => {
if (isRecursive) {
isRecursive = false
return
}
isRecursive = true
newthis[propname] = value
})
handle(use(newthis[propname]), (value) => {
if (isRecursive) {
isRecursive = false
return
}
isRecursive = true
set(value)
})
}
delete props[name]
}
}
Object.assign(newthis, props)
newthis.children = []
for (let child of children) {
JSXAddChild(child, newthis.children.push.bind(newthis.children))
}
let elm = type.apply(newthis)
elm.$ = newthis
newthis.root = elm
if (newthis.css) {
let cl = elm.classList
cl.add(newthis.css)
cl.add('self')
}
elm.setAttribute('data-component', type.name)
if (typeof newthis.mount === 'function') newthis.mount()
return elm
}
let xmlns = props?.xmlns
let elm = xmlns
? document.createElementNS(xmlns, type)
: document.createElement(type)
for (let child of children) {
let cond = child && !isDLPtr(child) && child[IF]
let bappend = elm.append.bind(elm)
if (cond) {
JSXAddFixedWrapper(cond, bappend, child)
} else JSXAddChild(child, bappend)
}
if (!props) return elm
let useProp = (name, callback) => {
if (!(name in props)) return
let prop = props[name]
callback(prop)
delete props[name]
}
for (let name in props) {
let ptr = props[name];
if (name.startsWith("bind:")) {
assert(isDLPtr(ptr), "bind: requires a reference pointer from use");
let ptr = props[name]
if (name.startsWith('bind:')) {
assert(isDLPtr(ptr), 'bind: requires a reference pointer from use')
let propname = name.substring(5)
let set = curryset(ptr);
let propname = name.substring(5);
if (propname == "this") {
set(newthis);
} else {
// component two way data binding!! (exact same behavior as svelte:bind)
let isRecursive = false;
handle(ptr, value => {
if (isRecursive) {
isRecursive = false;
return;
// create the function to set the value of the pointer
let set = curryset(ptr)
if (propname == 'this') {
set(elm)
} else if (propname == 'value') {
handle(ptr, (value) => (elm.value = value))
elm.addEventListener('change', () => set(elm.value))
} else if (propname == 'checked') {
handle(ptr, (value) => (elm.checked = value))
elm.addEventListener('click', () => set(elm.checked))
}
isRecursive = true;
newthis[propname] = value
});
handle(use(newthis[propname]), value => {
if (isRecursive) {
isRecursive = false;
return;
}
isRecursive = true;
set(value);
});
delete props[name]
}
if (name == 'style' && isobj(ptr)) {
for (let key in ptr) {
let prop = ptr[key]
if (isDLPtr(prop)) {
handle(prop, (value) => (elm.style[key] = value))
} else {
elm.style[key] = prop
}
}
delete props[name]
}
delete props[name];
}
}
Object.assign(newthis, props);
newthis.children = [];
for (let child of children) {
JSXAddChild(child, newthis.children.push.bind(newthis.children));
}
let elm = type.apply(newthis);
elm.$ = newthis;
newthis.root = elm;
if (newthis.css) {
let cl = elm.classList
cl.add(newthis.css);
cl.add("self");
}
elm.setAttribute("data-component", type.name);
if (typeof newthis.mount === "function")
newthis.mount();
return elm;
}
useProp('class', (classlist) => {
assert(
typeof classlist === 'string' || classlist instanceof Array,
'class must be a string or array'
)
if (typeof classlist === 'string') {
elm.setAttribute('class', classlist)
return
}
if (isDLPtr(classlist)) {
handle(classlist, (classname) =>
elm.setAttribute('class', classname)
)
return
}
let xmlns = props?.xmlns;
let elm = xmlns ? document.createElementNS(xmlns, type) : document.createElement(type);
for (let name of classlist) {
if (isDLPtr(name)) {
let oldvalue = null
handle(name, (value) => {
if (typeof oldvalue === 'string') {
elm.classList.remove(oldvalue)
}
elm.classList.add(value)
oldvalue = value
})
} else {
elm.classList.add(name)
}
}
})
for (let child of children) {
let cond = child && !isDLPtr(child) && child[IF];
let bappend = elm.append.bind(elm);
if (cond) {
JSXAddFixedWrapper(cond, bappend, child);
} else
JSXAddChild(child, bappend);
}
if (!props) return elm;
let useProp = (name, callback) => {
if (!(name in props)) return;
let prop = props[name];
callback(prop);
delete props[name];
}
for (let name in props) {
let ptr = props[name];
if (name.startsWith("bind:")) {
assert(isDLPtr(ptr), "bind: requires a reference pointer from use");
let propname = name.substring(5);
// create the function to set the value of the pointer
let set = curryset(ptr);
if (propname == "this") {
set(elm);
} else if (propname == "value") {
handle(ptr, value => elm.value = value);
elm.addEventListener("change", () => set(elm.value))
} else if (propname == "checked") {
handle(ptr, value => elm.checked = value);
elm.addEventListener("click", () => set(elm.checked))
}
delete props[name];
}
if (name == "style" && isobj(ptr)) {
for (let key in ptr) {
let prop = ptr[key];
// apply the non-reactive properties
for (let name in props) {
let prop = props[name]
if (isDLPtr(prop)) {
handle(prop, value => elm.style[key] = value);
handle(prop, (val) => {
JSXAddAttributes(elm, name, val)
})
} else {
elm.style[key] = prop;
JSXAddAttributes(elm, name, prop)
}
}
delete props[name];
}
}
useProp("class", classlist => {
assert(typeof classlist === "string" || classlist instanceof Array, "class must be a string or array");
if (typeof classlist === "string") {
elm.setAttribute("class", classlist);
return;
}
if (isDLPtr(classlist)) {
handle(classlist, classname => elm.setAttribute("class", classname));
return;
}
// hack to fix svgs
if (xmlns) elm.innerHTML = elm.innerHTML
for (let name of classlist) {
if (isDLPtr(name)) {
let oldvalue = null;
handle(name, value => {
if (typeof oldvalue === "string") {
elm.classList.remove(oldvalue);
}
elm.classList.add(value);
oldvalue = value;
});
} else {
elm.classList.add(name);
}
}
});
// apply the non-reactive properties
for (let name in props) {
let prop = props[name];
if (isDLPtr(prop)) {
handle(prop, (val) => {
JSXAddAttributes(elm, name, val);
});
} else {
JSXAddAttributes(elm, name, prop);
}
}
// hack to fix svgs
if (xmlns)
elm.innerHTML = elm.innerHTML
return elm;
return elm
}
// glue for nested children
function JSXAddChild(child, cb) {
let childchild, elms, node;
if (isDLPtr(child)) {
JSXAddFixedWrapper(child, cb);
} else if (child instanceof Node) {
cb(child);
return [child];
} else if (child instanceof Array) {
elms = [];
for (childchild of child) {
elms = elms.concat(JSXAddChild(childchild, cb));
let childchild, elms, node
if (isDLPtr(child)) {
JSXAddFixedWrapper(child, cb)
} else if (child instanceof Node) {
cb(child)
return [child]
} else if (child instanceof Array) {
elms = []
for (childchild of child) {
elms = elms.concat(JSXAddChild(childchild, cb))
}
if (!elms[0]) elms = JSXAddChild('', cb)
return elms
} else {
node = document.createTextNode(child)
cb(node)
return [node]
}
if (!elms[0]) elms = JSXAddChild("", cb);
return elms;
} else {
node = document.createTextNode(child);
cb(node);
return [node];
}
}
// Where properties are assigned to elements, and where the *non-reactive* syntax sugar goes
function JSXAddAttributes(elm, name, prop) {
if (name.startsWith("on:")) {
assert(typeof prop === "function", "on: requires a function");
let names = name.substring(3);
for (let name of names.split("$")) {
elm.addEventListener(name, (...args) => {
self.$el = elm;
prop(...args);
});
if (name.startsWith('on:')) {
assert(typeof prop === 'function', 'on: requires a function')
let names = name.substring(3)
for (let name of names.split('$')) {
elm.addEventListener(name, (...args) => {
self.$el = elm
prop(...args)
})
}
return
}
return;
}
elm.setAttribute(name, prop);
elm.setAttribute(name, prop)
}

View file

@ -1,12 +1,12 @@
Object.assign(window, { $store });
Object.assign(window, { $store })
function $store(target, ident, type) {
let stored = localStorage.getItem(ident);
target = JSON.parse(stored) ?? target;
let stored = localStorage.getItem(ident)
target = JSON.parse(stored) ?? target
addEventListener("beforeunload", () => {
console.info("[dreamland.js]: saving " + ident);
localStorage.setItem(ident, JSON.stringify(target));
});
addEventListener('beforeunload', () => {
console.info('[dreamland.js]: saving ' + ident)
localStorage.setItem(ident, JSON.stringify(target))
})
return stateful(target);
return stateful(target)
}

View file

@ -1,29 +1,23 @@
{
"compilerOptions": {
"compilerOptions": {
"lib": ["ESNext", "dom"],
"outDir": "./examples/lib",
"removeComments": true,
"target": "ES6",
"lib": [
"ESNext",
"dom"
],
"outDir": "./examples/lib",
"removeComments": true,
"target": "ES6",
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"jsx":"react",
"jsxFactory":"h",
"jsxFragmentFactory":"Fragment",
"sourceMap": true,
"sourceRoot": "",
"sourceMap": true,
"sourceRoot": "",
"alwaysStrict": false,
"noImplicitAny": false,
"strictNullChecks": false,
"alwaysStrict": false,
"noImplicitAny": false,
"strictNullChecks":false,
"noUncheckedIndexedAccess": true
},
"include": ["./examples/*.ts","./examples/*.tsx", "./DreamlandJS.d.ts"],
"exclude": [
"node_modules/**/*"
]
"noUncheckedIndexedAccess": true
},
"include": ["./examples/*.ts", "./examples/*.tsx", "./DreamlandJS.d.ts"],
"exclude": ["node_modules/**/*"]
}