@bitstillery/mithril 3.0.0-AA
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/api/mount-redraw.ts +173 -0
- package/api/router-server.ts +88 -0
- package/api/router.ts +670 -0
- package/browser.ts +10 -0
- package/htm.ts +7 -0
- package/hyperscript.ts +8 -0
- package/index.ts +134 -0
- package/jsx.d.ts +16 -0
- package/mount.ts +10 -0
- package/package.json +66 -0
- package/pathname/build.ts +40 -0
- package/pathname/compileTemplate.ts +49 -0
- package/pathname/parse.ts +19 -0
- package/querystring/build.ts +22 -0
- package/querystring/parse.ts +50 -0
- package/redraw.ts +10 -0
- package/render/cachedAttrsIsStaticMap.ts +10 -0
- package/render/delayedRemoval.ts +1 -0
- package/render/domFor.ts +23 -0
- package/render/emptyAttrs.ts +2 -0
- package/render/fragment.ts +11 -0
- package/render/hyperscript.ts +125 -0
- package/render/hyperscriptVnode.ts +21 -0
- package/render/render.ts +1307 -0
- package/render/renderToString.ts +494 -0
- package/render/ssrState.ts +366 -0
- package/render/trust.ts +6 -0
- package/render/vnode.ts +131 -0
- package/route.ts +11 -0
- package/server/logger.ts +223 -0
- package/server/session.ts +87 -0
- package/server/ssr.ts +205 -0
- package/server/ssrLogger.ts +13 -0
- package/server.ts +101 -0
- package/signal.ts +320 -0
- package/ssrContext.ts +87 -0
- package/state.ts +1026 -0
- package/store.ts +457 -0
- package/util/censor.ts +46 -0
- package/util/decodeURIComponentSafe.ts +34 -0
- package/util/hasOwn.ts +2 -0
- package/util/next_tick.ts +45 -0
- package/util/ssr.ts +385 -0
- package/util/uri.ts +126 -0
- package/version.ts +3 -0
package/htm.ts
ADDED
package/hyperscript.ts
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import hyperscript from './render/hyperscript'
|
|
2
|
+
import mountRedrawFactory from './api/mount-redraw'
|
|
3
|
+
import routerFactory from './api/router'
|
|
4
|
+
import renderFactory from './render/render'
|
|
5
|
+
import parseQueryString from './querystring/parse'
|
|
6
|
+
import buildQueryString from './querystring/build'
|
|
7
|
+
import parsePathname from './pathname/parse'
|
|
8
|
+
import buildPathname from './pathname/build'
|
|
9
|
+
import VnodeFactory, {MithrilComponent} from './render/vnode'
|
|
10
|
+
import censor from './util/censor'
|
|
11
|
+
import next_tick from './util/next_tick'
|
|
12
|
+
import domFor from './render/domFor'
|
|
13
|
+
import {signal, computed, effect, Signal, ComputedSignal, setSignalRedrawCallback, getSignalComponents} from './signal'
|
|
14
|
+
import {state, watch, registerState, getRegisteredStates, copyGlobalStatesToContext} from './state'
|
|
15
|
+
|
|
16
|
+
import type {Vnode, Children, ComponentType} from './render/vnode'
|
|
17
|
+
import type {Hyperscript} from './render/hyperscript'
|
|
18
|
+
import type {Route, RouteResolver, RedirectObject} from './api/router'
|
|
19
|
+
import type {Render, Redraw, Mount} from './api/mount-redraw'
|
|
20
|
+
|
|
21
|
+
export interface MithrilStatic {
|
|
22
|
+
m: Hyperscript
|
|
23
|
+
trust: (html: string) => Vnode
|
|
24
|
+
fragment: (attrs: Record<string, any> | null, ...children: Children[]) => Vnode
|
|
25
|
+
Fragment: string
|
|
26
|
+
mount: Mount
|
|
27
|
+
route: Route &
|
|
28
|
+
((root: Element, defaultRoute: string, routes: Record<string, ComponentType | RouteResolver>) => void) & {
|
|
29
|
+
redirect: (path: string) => RedirectObject
|
|
30
|
+
}
|
|
31
|
+
render: Render
|
|
32
|
+
redraw: Redraw
|
|
33
|
+
parseQueryString: (queryString: string) => Record<string, any>
|
|
34
|
+
buildQueryString: (values: Record<string, any>) => string
|
|
35
|
+
parsePathname: (pathname: string) => {path: string; params: Record<string, any>}
|
|
36
|
+
buildPathname: (template: string, params: Record<string, any>) => string
|
|
37
|
+
vnode: typeof VnodeFactory
|
|
38
|
+
censor: (attrs: Record<string, any>, extras?: string[]) => Record<string, any>
|
|
39
|
+
next_tick: () => Promise<void>
|
|
40
|
+
domFor: (vnode: Vnode) => Generator<Node, void, unknown>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const mountRedrawInstance = mountRedrawFactory(
|
|
44
|
+
renderFactory(),
|
|
45
|
+
typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame.bind(window) : setTimeout,
|
|
46
|
+
console,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const router = routerFactory(typeof window !== 'undefined' ? window : null, mountRedrawInstance)
|
|
50
|
+
|
|
51
|
+
const m: MithrilStatic & Hyperscript = function m(this: any) {
|
|
52
|
+
return hyperscript.apply(this, arguments as any)
|
|
53
|
+
} as unknown as MithrilStatic & Hyperscript
|
|
54
|
+
|
|
55
|
+
m.m = hyperscript as Hyperscript
|
|
56
|
+
m.trust = hyperscript.trust
|
|
57
|
+
m.fragment = hyperscript.fragment
|
|
58
|
+
m.Fragment = '['
|
|
59
|
+
m.mount = mountRedrawInstance.mount
|
|
60
|
+
m.route = router as Route & typeof router & {redirect: (path: string) => RedirectObject}
|
|
61
|
+
m.render = renderFactory()
|
|
62
|
+
m.redraw = mountRedrawInstance.redraw
|
|
63
|
+
m.parseQueryString = parseQueryString
|
|
64
|
+
m.buildQueryString = buildQueryString
|
|
65
|
+
m.parsePathname = parsePathname
|
|
66
|
+
m.buildPathname = buildPathname
|
|
67
|
+
m.vnode = VnodeFactory
|
|
68
|
+
m.censor = censor
|
|
69
|
+
m.next_tick = next_tick
|
|
70
|
+
m.domFor = domFor
|
|
71
|
+
|
|
72
|
+
// Set up signal-to-component redraw integration
|
|
73
|
+
setSignalRedrawCallback((sig: Signal<any>) => {
|
|
74
|
+
const components = getSignalComponents(sig)
|
|
75
|
+
if (components) {
|
|
76
|
+
components.forEach((component) => {
|
|
77
|
+
// Use the component-level redraw
|
|
78
|
+
m.redraw(component as any)
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Export signals API
|
|
84
|
+
export {signal, computed, effect, Signal, ComputedSignal, state, watch, registerState, getRegisteredStates}
|
|
85
|
+
export type {State, StateOptions} from './state'
|
|
86
|
+
|
|
87
|
+
// Export Store class
|
|
88
|
+
export {Store} from './store'
|
|
89
|
+
|
|
90
|
+
// Export SSR utilities
|
|
91
|
+
export {serializeStore, deserializeStore, serializeAllStates, deserializeAllStates} from './render/ssrState'
|
|
92
|
+
|
|
93
|
+
// Export SSR request context (for per-request store and state registry)
|
|
94
|
+
export {getSSRContext, runWithContext, runWithContextAsync, cleanupWatchers} from './ssrContext'
|
|
95
|
+
export type {SSRAccessContext} from './ssrContext'
|
|
96
|
+
|
|
97
|
+
// Export isomorphic logger
|
|
98
|
+
export {logger, Logger} from './server/logger'
|
|
99
|
+
export type {LogContext} from './server/logger'
|
|
100
|
+
|
|
101
|
+
// Export next_tick utility
|
|
102
|
+
export {next_tick} from './util/next_tick'
|
|
103
|
+
|
|
104
|
+
// Export URI utilities
|
|
105
|
+
export {getCurrentUrl, getPathname, getSearch, getHash, getLocation} from './util/uri'
|
|
106
|
+
export type {IsomorphicLocation} from './util/uri'
|
|
107
|
+
|
|
108
|
+
// Export component and vnode types
|
|
109
|
+
export type {
|
|
110
|
+
Vnode,
|
|
111
|
+
ComponentVnode,
|
|
112
|
+
Children,
|
|
113
|
+
Child,
|
|
114
|
+
VnodeDOM,
|
|
115
|
+
Component,
|
|
116
|
+
ComponentFactory,
|
|
117
|
+
ComponentType,
|
|
118
|
+
VnodeOf,
|
|
119
|
+
} from './render/vnode'
|
|
120
|
+
export {MithrilComponent}
|
|
121
|
+
export type {Hyperscript} from './render/hyperscript'
|
|
122
|
+
export type {Route, RouteResolver, RedirectObject} from './api/router'
|
|
123
|
+
export type {Render, Redraw, Mount} from './api/mount-redraw'
|
|
124
|
+
|
|
125
|
+
// Namespace merge: enables m.Vnode<Attrs> and m.Children when using import m from '@bitstillery/mithril'
|
|
126
|
+
// m.Vnode uses ComponentVnode so vnode.attrs is always defined in component lifecycle methods
|
|
127
|
+
declare namespace m {
|
|
128
|
+
type Vnode<Attrs = Record<string, any>, State = any> = import('./render/vnode').ComponentVnode<Attrs, State>
|
|
129
|
+
type VnodeDOM<Attrs = Record<string, any>, State = any> = import('./render/vnode').VnodeDOM<Attrs, State>
|
|
130
|
+
type Children = import('./render/vnode').Children
|
|
131
|
+
type ChildArray = import('./render/vnode').Child[]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default m
|
package/jsx.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type {Vnode} from './render/vnode'
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
namespace JSX {
|
|
5
|
+
/** JSX elements from hyperscript always have attrs set (at least {}). */
|
|
6
|
+
interface Element extends Omit<Vnode, 'attrs'> {
|
|
7
|
+
attrs: Record<string, any>
|
|
8
|
+
}
|
|
9
|
+
interface IntrinsicElements {
|
|
10
|
+
[elemName: string]: any
|
|
11
|
+
}
|
|
12
|
+
interface ElementAttributesProperty {
|
|
13
|
+
__tsx_attrs: any
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
package/mount.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import mountRedrawFactory from './api/mount-redraw'
|
|
2
|
+
import renderFactory from './render/render'
|
|
3
|
+
|
|
4
|
+
const mountRedraw = mountRedrawFactory(
|
|
5
|
+
renderFactory(),
|
|
6
|
+
typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame.bind(window) : setTimeout,
|
|
7
|
+
console,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
export default mountRedraw.mount
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bitstillery/mithril",
|
|
3
|
+
"version": "3.0.0-AA",
|
|
4
|
+
"description": "Mithril AA (AI Augmented) - Mithril + Signals, Store and SSR",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Bitstillery",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/bitstillery/mithril.git"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"*.ts",
|
|
13
|
+
"*.d.ts",
|
|
14
|
+
"ssrContext.ts",
|
|
15
|
+
"signal.ts",
|
|
16
|
+
"store.ts",
|
|
17
|
+
"api/*.ts",
|
|
18
|
+
"pathname/*.ts",
|
|
19
|
+
"querystring/*.ts",
|
|
20
|
+
"render/*.ts",
|
|
21
|
+
"server/*.ts",
|
|
22
|
+
"util/*.ts",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./index.ts",
|
|
28
|
+
"module": "./index.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": "./index.ts",
|
|
32
|
+
"default": "./index.ts"
|
|
33
|
+
},
|
|
34
|
+
"./browser": {
|
|
35
|
+
"import": "./browser.ts",
|
|
36
|
+
"default": "./browser.ts"
|
|
37
|
+
},
|
|
38
|
+
"./server": {
|
|
39
|
+
"import": "./server.ts",
|
|
40
|
+
"default": "./server.ts"
|
|
41
|
+
},
|
|
42
|
+
"./htm": {
|
|
43
|
+
"import": "./htm.ts",
|
|
44
|
+
"default": "./htm.ts"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"lint": "bun run lint:ts",
|
|
49
|
+
"lint:ts": "bun run lint:ts-format && bun run lint:ts-syntax && bun run lint:ts-types",
|
|
50
|
+
"lint:ts-format": "oxfmt",
|
|
51
|
+
"lint:ts-syntax": "oxlint -c .oxlintrc.json",
|
|
52
|
+
"lint:ts-types": "tsgo --noEmit",
|
|
53
|
+
"test": "bun test",
|
|
54
|
+
"prepublishOnly": "bun run lint:ts",
|
|
55
|
+
"pack:dry-run": "npm pack --dry-run"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"htm": "^3.1.1"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/bun": "^1.3.9",
|
|
62
|
+
"@typescript/native-preview": "^7.0.0-dev.20260120.1",
|
|
63
|
+
"oxfmt": "^0.32.0",
|
|
64
|
+
"oxlint": "^1.30.0"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import buildQueryString from '../querystring/build'
|
|
2
|
+
|
|
3
|
+
// Returns `path` from `template` + `params`
|
|
4
|
+
export default function buildPathname(template: string, params: Record<string, any>): string {
|
|
5
|
+
if (/:([^/.-]+)(\.{3})?:/.test(template)) {
|
|
6
|
+
throw new SyntaxError("Template parameter names must be separated by either a '/', '-', or '.'.")
|
|
7
|
+
}
|
|
8
|
+
if (params == null) return template
|
|
9
|
+
const queryIndex = template.indexOf('?')
|
|
10
|
+
const hashIndex = template.indexOf('#')
|
|
11
|
+
const queryEnd = hashIndex < 0 ? template.length : hashIndex
|
|
12
|
+
const pathEnd = queryIndex < 0 ? queryEnd : queryIndex
|
|
13
|
+
const path = template.slice(0, pathEnd)
|
|
14
|
+
const query: Record<string, any> = {}
|
|
15
|
+
|
|
16
|
+
Object.assign(query, params)
|
|
17
|
+
|
|
18
|
+
const resolved = path.replace(/:([^/.-]+)(\.{3})?/g, function (m, key, variadic) {
|
|
19
|
+
delete query[key]
|
|
20
|
+
// If no such parameter exists, don't interpolate it.
|
|
21
|
+
if (params[key] == null) return m
|
|
22
|
+
// Escape normal parameters, but not variadic ones.
|
|
23
|
+
return variadic ? params[key] : encodeURIComponent(String(params[key]))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// In case the template substitution adds new query/hash parameters.
|
|
27
|
+
const newQueryIndex = resolved.indexOf('?')
|
|
28
|
+
const newHashIndex = resolved.indexOf('#')
|
|
29
|
+
const newQueryEnd = newHashIndex < 0 ? resolved.length : newHashIndex
|
|
30
|
+
const newPathEnd = newQueryIndex < 0 ? newQueryEnd : newQueryIndex
|
|
31
|
+
let result = resolved.slice(0, newPathEnd)
|
|
32
|
+
|
|
33
|
+
if (queryIndex >= 0) result += template.slice(queryIndex, queryEnd)
|
|
34
|
+
if (newQueryIndex >= 0) result += (queryIndex < 0 ? '?' : '&') + resolved.slice(newQueryIndex, newQueryEnd)
|
|
35
|
+
const querystring = buildQueryString(query)
|
|
36
|
+
if (querystring) result += (queryIndex < 0 && newQueryIndex < 0 ? '?' : '&') + querystring
|
|
37
|
+
if (hashIndex >= 0) result += template.slice(hashIndex)
|
|
38
|
+
if (newHashIndex >= 0) result += (hashIndex < 0 ? '' : '&') + resolved.slice(newHashIndex)
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import parsePathname from './parse'
|
|
2
|
+
|
|
3
|
+
interface CompiledTemplate {
|
|
4
|
+
(data: {path: string; params: Record<string, any>}): boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Compiles a template into a function that takes a resolved path (without query
|
|
8
|
+
// strings) and returns an object containing the template parameters with their
|
|
9
|
+
// parsed values. This expects the input of the compiled template to be the
|
|
10
|
+
// output of `parsePathname`. Note that it does *not* remove query parameters
|
|
11
|
+
// specified in the template.
|
|
12
|
+
export default function compileTemplate(template: string): CompiledTemplate {
|
|
13
|
+
const templateData = parsePathname(template)
|
|
14
|
+
const templateKeys = Object.keys(templateData.params)
|
|
15
|
+
const keys: Array<{k: string; r: boolean}> = []
|
|
16
|
+
const regexp = new RegExp(
|
|
17
|
+
'^' +
|
|
18
|
+
templateData.path.replace(
|
|
19
|
+
// I escape literal text so people can use things like `:file.:ext` or
|
|
20
|
+
// `:lang-:locale` in routes. This is all merged into one pass so I
|
|
21
|
+
// don't also accidentally escape `-` and make it harder to detect it to
|
|
22
|
+
// ban it from template parameters.
|
|
23
|
+
/:([^/.-]+)(\.{3}|\.(?!\.)|-)?|[\\^$*+.()|[\]{}]/g,
|
|
24
|
+
function (m, key, extra) {
|
|
25
|
+
if (key == null) return '\\' + m
|
|
26
|
+
keys.push({k: key, r: extra === '...'})
|
|
27
|
+
if (extra === '...') return '(.*)'
|
|
28
|
+
if (extra === '.') return '([^/]+)\\.'
|
|
29
|
+
return '([^/]+)' + (extra || '')
|
|
30
|
+
},
|
|
31
|
+
) +
|
|
32
|
+
'\\/?$',
|
|
33
|
+
)
|
|
34
|
+
return function (data: {path: string; params: Record<string, any>}): boolean {
|
|
35
|
+
// First, check the params. Usually, there isn't any, and it's just
|
|
36
|
+
// checking a static set.
|
|
37
|
+
for (let i = 0; i < templateKeys.length; i++) {
|
|
38
|
+
if (templateData.params[templateKeys[i]] !== data.params[templateKeys[i]]) return false
|
|
39
|
+
}
|
|
40
|
+
// If no interpolations exist, let's skip all the ceremony
|
|
41
|
+
if (!keys.length) return regexp.test(data.path)
|
|
42
|
+
const values = regexp.exec(data.path)
|
|
43
|
+
if (values == null) return false
|
|
44
|
+
for (let i = 0; i < keys.length; i++) {
|
|
45
|
+
data.params[keys[i].k] = keys[i].r ? values[i + 1] : decodeURIComponent(values[i + 1])
|
|
46
|
+
}
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import parseQueryString from '../querystring/parse'
|
|
2
|
+
|
|
3
|
+
// Returns `{path, params}` from `url`
|
|
4
|
+
export default function parsePathname(url: string): {path: string; params: Record<string, any>} {
|
|
5
|
+
const queryIndex = url.indexOf('?')
|
|
6
|
+
const hashIndex = url.indexOf('#')
|
|
7
|
+
const queryEnd = hashIndex < 0 ? url.length : hashIndex
|
|
8
|
+
const pathEnd = queryIndex < 0 ? queryEnd : queryIndex
|
|
9
|
+
let path = url.slice(0, pathEnd).replace(/\/{2,}/g, '/')
|
|
10
|
+
|
|
11
|
+
if (!path) path = '/'
|
|
12
|
+
else {
|
|
13
|
+
if (path[0] !== '/') path = '/' + path
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
path: path,
|
|
17
|
+
params: queryIndex < 0 ? {} : parseQueryString(url.slice(queryIndex + 1, queryEnd)),
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export default function buildQueryString(object: Record<string, any>): string {
|
|
2
|
+
if (Object.prototype.toString.call(object) !== '[object Object]') return ''
|
|
3
|
+
|
|
4
|
+
const args: string[] = []
|
|
5
|
+
function destructure(key: string, value: any) {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
for (let i = 0; i < value.length; i++) {
|
|
8
|
+
destructure(key + '[' + i + ']', value[i])
|
|
9
|
+
}
|
|
10
|
+
} else if (Object.prototype.toString.call(value) === '[object Object]') {
|
|
11
|
+
for (const i in value) {
|
|
12
|
+
destructure(key + '[' + i + ']', value[i])
|
|
13
|
+
}
|
|
14
|
+
} else args.push(encodeURIComponent(key) + (value != null && value !== '' ? '=' + encodeURIComponent(value) : ''))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const key in object) {
|
|
18
|
+
destructure(key, object[key])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return args.join('&')
|
|
22
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import decodeURIComponentSafe from '../util/decodeURIComponentSafe'
|
|
2
|
+
|
|
3
|
+
export default function parseQueryString(string: string | null | undefined): Record<string, any> {
|
|
4
|
+
if (string === '' || string == null) return {}
|
|
5
|
+
if (string.charAt(0) === '?') string = string.slice(1)
|
|
6
|
+
|
|
7
|
+
const entries = string.split('&')
|
|
8
|
+
const counters: Record<string, number> = {}
|
|
9
|
+
const data: Record<string, any> = {}
|
|
10
|
+
for (let i = 0; i < entries.length; i++) {
|
|
11
|
+
const entry = entries[i].split('=')
|
|
12
|
+
const key = decodeURIComponentSafe(entry[0])
|
|
13
|
+
let value: any = entry.length === 2 ? decodeURIComponentSafe(entry[1]) : ''
|
|
14
|
+
|
|
15
|
+
if (value === 'true') value = true
|
|
16
|
+
else if (value === 'false') value = false
|
|
17
|
+
|
|
18
|
+
const levels = key.split(/\]\[?|\[/)
|
|
19
|
+
let cursor: any = data
|
|
20
|
+
if (key.indexOf('[') > -1) levels.pop()
|
|
21
|
+
for (let j = 0; j < levels.length; j++) {
|
|
22
|
+
const level = levels[j]
|
|
23
|
+
const nextLevel = levels[j + 1]
|
|
24
|
+
const isNumber = nextLevel == '' || !isNaN(parseInt(nextLevel, 10))
|
|
25
|
+
let finalLevel: string | number
|
|
26
|
+
if (level === '') {
|
|
27
|
+
const key = levels.slice(0, j).join()
|
|
28
|
+
if (counters[key] == null) {
|
|
29
|
+
counters[key] = Array.isArray(cursor) ? cursor.length : 0
|
|
30
|
+
}
|
|
31
|
+
finalLevel = counters[key]++
|
|
32
|
+
}
|
|
33
|
+
// Disallow direct prototype pollution
|
|
34
|
+
else if (level === '__proto__') break
|
|
35
|
+
else {
|
|
36
|
+
finalLevel = level
|
|
37
|
+
}
|
|
38
|
+
if (j === levels.length - 1) cursor[finalLevel] = value
|
|
39
|
+
else {
|
|
40
|
+
// Read own properties exclusively to disallow indirect
|
|
41
|
+
// prototype pollution
|
|
42
|
+
const desc = Object.getOwnPropertyDescriptor(cursor, finalLevel)
|
|
43
|
+
let descValue = desc != null ? desc.value : undefined
|
|
44
|
+
if (descValue == null) cursor[finalLevel] = descValue = isNumber ? [] : {}
|
|
45
|
+
cursor = descValue
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return data
|
|
50
|
+
}
|
package/redraw.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import mountRedrawFactory from './api/mount-redraw'
|
|
2
|
+
import renderFactory from './render/render'
|
|
3
|
+
|
|
4
|
+
const mountRedraw = mountRedrawFactory(
|
|
5
|
+
renderFactory(),
|
|
6
|
+
typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame.bind(window) : setTimeout,
|
|
7
|
+
console,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
export default mountRedraw.redraw
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import emptyAttrs from './emptyAttrs'
|
|
2
|
+
|
|
3
|
+
// This Map manages the following:
|
|
4
|
+
// - Whether an attrs is cached attrs generated by compileSelector().
|
|
5
|
+
// - Whether the cached attrs is "static", i.e., does not contain any form attributes.
|
|
6
|
+
// These information will be useful to skip updating attrs in render().
|
|
7
|
+
//
|
|
8
|
+
// Since the attrs used as keys in this map are not released from the selectorCache object,
|
|
9
|
+
// there is no risk of memory leaks. Therefore, Map is used here instead of WeakMap.
|
|
10
|
+
export default new Map([[emptyAttrs, true]])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default new WeakMap<Node, number>()
|
package/render/domFor.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import delayedRemoval from './delayedRemoval'
|
|
2
|
+
|
|
3
|
+
import type {Vnode} from './vnode'
|
|
4
|
+
|
|
5
|
+
function* domFor(vnode: Vnode): Generator<Node, void, unknown> {
|
|
6
|
+
// To avoid unintended mangling of the internal bundler,
|
|
7
|
+
// parameter destructuring is not used here.
|
|
8
|
+
let dom = vnode.dom
|
|
9
|
+
let domSize = vnode.domSize
|
|
10
|
+
const generation = delayedRemoval.get(dom!)
|
|
11
|
+
do {
|
|
12
|
+
const nextSibling = dom!.nextSibling
|
|
13
|
+
|
|
14
|
+
if (delayedRemoval.get(dom!) === generation) {
|
|
15
|
+
yield dom!
|
|
16
|
+
domSize!--
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
dom = nextSibling as Node | null
|
|
20
|
+
} while (domSize)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default domFor
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Vnode from './vnode'
|
|
2
|
+
import hyperscriptVnode from './hyperscriptVnode'
|
|
3
|
+
|
|
4
|
+
export default function fragment(attrs: any, ...children: any[]): any {
|
|
5
|
+
const vnode = hyperscriptVnode(attrs, children)
|
|
6
|
+
|
|
7
|
+
if (vnode.attrs == null) vnode.attrs = {}
|
|
8
|
+
vnode.tag = '['
|
|
9
|
+
vnode.children = Vnode.normalizeChildren(vnode.children)
|
|
10
|
+
return vnode
|
|
11
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import hasOwn from '../util/hasOwn'
|
|
2
|
+
|
|
3
|
+
import Vnode from './vnode'
|
|
4
|
+
import hyperscriptVnode from './hyperscriptVnode'
|
|
5
|
+
import emptyAttrs from './emptyAttrs'
|
|
6
|
+
import cachedAttrsIsStaticMap from './cachedAttrsIsStaticMap'
|
|
7
|
+
import trust from './trust'
|
|
8
|
+
import fragment from './fragment'
|
|
9
|
+
|
|
10
|
+
import type {ComponentType, Children, Vnode as VnodeType} from './vnode'
|
|
11
|
+
|
|
12
|
+
export interface Hyperscript {
|
|
13
|
+
(selector: string, ...children: Children[]): VnodeType
|
|
14
|
+
(selector: string, attrs: Record<string, any>, ...children: Children[]): VnodeType
|
|
15
|
+
<Attrs, State>(component: ComponentType<Attrs, State>, ...children: Children[]): VnodeType<Attrs, State>
|
|
16
|
+
<Attrs, State>(component: ComponentType<Attrs, State>, attrs: Attrs, ...children: Children[]): VnodeType<Attrs, State>
|
|
17
|
+
trust(html: string): VnodeType
|
|
18
|
+
fragment(attrs: Record<string, any> | null, ...children: Children[]): VnodeType
|
|
19
|
+
Fragment: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const selectorParser = /(?:(^|#|\.)([^#.[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g
|
|
23
|
+
const selectorCache: Record<string, {tag: string; attrs: Record<string, any>; is?: string}> = Object.create(null)
|
|
24
|
+
|
|
25
|
+
function isEmpty(object: Record<string, any>): boolean {
|
|
26
|
+
for (const key in object) if (hasOwn.call(object, key)) return false
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isFormAttributeKey(key: string): boolean {
|
|
31
|
+
return key === 'value' || key === 'checked' || key === 'selectedIndex' || key === 'selected'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function compileSelector(selector: string): {tag: string; attrs: Record<string, any>; is?: string} {
|
|
35
|
+
let match: RegExpExecArray | null
|
|
36
|
+
let tag = 'div'
|
|
37
|
+
const classes: string[] = []
|
|
38
|
+
let attrs: Record<string, any> = {}
|
|
39
|
+
let isStatic = true
|
|
40
|
+
while ((match = selectorParser.exec(selector)) !== null) {
|
|
41
|
+
const type = match[1]
|
|
42
|
+
const value = match[2]
|
|
43
|
+
if (type === '' && value !== '') tag = value
|
|
44
|
+
else if (type === '#') attrs.id = value
|
|
45
|
+
else if (type === '.') classes.push(value)
|
|
46
|
+
else if (match[3][0] === '[') {
|
|
47
|
+
let attrValue = match[6]
|
|
48
|
+
if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, '$1').replace(/\\\\/g, '\\')
|
|
49
|
+
if (match[4] === 'class') classes.push(attrValue)
|
|
50
|
+
else {
|
|
51
|
+
attrs[match[4]] = attrValue === '' ? attrValue : attrValue || true
|
|
52
|
+
if (isFormAttributeKey(match[4])) isStatic = false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (classes.length > 0) attrs.className = classes.join(' ')
|
|
57
|
+
if (isEmpty(attrs)) attrs = emptyAttrs
|
|
58
|
+
else cachedAttrsIsStaticMap.set(attrs, isStatic)
|
|
59
|
+
return (selectorCache[selector] = {tag: tag, attrs: attrs, is: attrs.is})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function execSelector(state: {tag: string; attrs: Record<string, any>; is?: string}, vnode: any): any {
|
|
63
|
+
vnode.tag = state.tag
|
|
64
|
+
|
|
65
|
+
let attrs = vnode.attrs
|
|
66
|
+
if (attrs == null) {
|
|
67
|
+
vnode.attrs = state.attrs
|
|
68
|
+
vnode.is = state.is
|
|
69
|
+
return vnode
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (hasOwn.call(attrs, 'class')) {
|
|
73
|
+
if (attrs.class != null) attrs.className = attrs.class
|
|
74
|
+
attrs.class = null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (state.attrs !== emptyAttrs) {
|
|
78
|
+
const className = attrs.className
|
|
79
|
+
attrs = Object.assign({}, state.attrs, attrs)
|
|
80
|
+
|
|
81
|
+
if (state.attrs.className != null)
|
|
82
|
+
attrs.className = className != null ? String(state.attrs.className) + ' ' + String(className) : state.attrs.className
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// workaround for #2622 (reorder keys in attrs to set "type" first)
|
|
86
|
+
// The DOM does things to inputs based on the "type", so it needs set first.
|
|
87
|
+
// See: https://github.com/MithrilJS/mithril.js/issues/2622
|
|
88
|
+
if (state.tag === 'input' && hasOwn.call(attrs, 'type')) {
|
|
89
|
+
attrs = Object.assign({type: attrs.type}, attrs)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// This reduces the complexity of the evaluation of "is" within the render function.
|
|
93
|
+
vnode.is = attrs.is
|
|
94
|
+
|
|
95
|
+
vnode.attrs = attrs
|
|
96
|
+
|
|
97
|
+
return vnode
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function hyperscript(selector: string | ComponentType, attrs?: Record<string, any> | null, ...children: Children[]): any {
|
|
101
|
+
if (
|
|
102
|
+
selector == null ||
|
|
103
|
+
(typeof selector !== 'string' && typeof selector !== 'function' && typeof (selector as any).view !== 'function')
|
|
104
|
+
) {
|
|
105
|
+
throw Error('The selector must be either a string or a component.')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const vnode = hyperscriptVnode(attrs, children)
|
|
109
|
+
|
|
110
|
+
if (typeof selector === 'string') {
|
|
111
|
+
vnode.children = Vnode.normalizeChildren(vnode.children)
|
|
112
|
+
if (selector !== '[') return execSelector(selectorCache[selector] || compileSelector(selector), vnode)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (vnode.attrs == null) vnode.attrs = {}
|
|
116
|
+
vnode.tag = selector
|
|
117
|
+
return vnode
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
hyperscript.trust = trust
|
|
121
|
+
|
|
122
|
+
hyperscript.fragment = fragment
|
|
123
|
+
hyperscript.Fragment = '['
|
|
124
|
+
|
|
125
|
+
export default hyperscript
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Vnode from './vnode'
|
|
2
|
+
|
|
3
|
+
// Note: the processing of variadic parameters is perf-sensitive.
|
|
4
|
+
//
|
|
5
|
+
// In native ES6, it might be preferable to define hyperscript and fragment
|
|
6
|
+
// factories with a final ...args parameter and call hyperscriptVnode(...args),
|
|
7
|
+
// since modern engines can optimize spread calls.
|
|
8
|
+
//
|
|
9
|
+
// However, benchmarks showed this was not faster. As a result, spread is used
|
|
10
|
+
// only in the parameter lists of hyperscript and fragment, while an array is
|
|
11
|
+
// passed to hyperscriptVnode.
|
|
12
|
+
export default function hyperscriptVnode(attrs: any, children: any[]): any {
|
|
13
|
+
if (attrs == null || (typeof attrs === 'object' && attrs.tag == null && !Array.isArray(attrs))) {
|
|
14
|
+
if (children.length === 1 && Array.isArray(children[0])) children = children[0]
|
|
15
|
+
} else {
|
|
16
|
+
children = children.length === 0 && Array.isArray(attrs) ? attrs : [attrs, ...children]
|
|
17
|
+
attrs = undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return Vnode('', attrs && attrs.key, attrs, children, null, null)
|
|
21
|
+
}
|