├── .eslintignore
├── .editorconfig
├── .gitignore
├── .npmignore
├── LICENSE
├── rollup.esbuild.config.mjs
├── package.json
├── README.md
├── .eslintrc
└── src
└── main.mjs
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | config/*.js
3 | test/*.js
4 | **/*.json
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = tab
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [package.json]
12 | indent_style = space
13 |
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated files
2 | bin/
3 | gen/
4 |
5 | # Log Files
6 | *.log
7 | *.log.*
8 |
9 | # Temp Files
10 | *~
11 | *.*~
12 | .fuse_*
13 | yarn.lock
14 | cache/
15 | temp/
16 |
17 | # Project files
18 | dist/
19 | dev/
20 |
21 | # node modules
22 | node_modules/
23 |
24 | .DS_Store
25 | /dist.zip
26 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Generated files
2 | bin/
3 | gen/
4 |
5 | # Log Files
6 | *.log
7 | *.log.*
8 |
9 | # Temp Files
10 | *~
11 | *.*~
12 | .fuse_*
13 | yarn.lock
14 | cache/
15 | temp/
16 |
17 | # Project files
18 | build/
19 | config/
20 | test/
21 |
22 | # Hidden files
23 | .*
24 |
25 | # node modules
26 | node_modules/
27 |
28 | # local tools
29 | publish
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Yukino Song
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/rollup.esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild, {minify} from 'rollup-plugin-esbuild'
2 |
3 | const exportName = 'singui'
4 |
5 | const minifyPlugin = minify()
6 |
7 | export default {
8 | input: 'src/main.mjs',
9 | output: [{
10 | file: 'dist/main.umd.js',
11 | name: exportName,
12 | format: 'umd',
13 | sourcemap: true
14 | }, {
15 | file: 'dist/main.umd.min.js',
16 | name: exportName,
17 | format: 'umd',
18 | plugins: [minifyPlugin]
19 | }, {
20 | file: 'dist/main.iife.js',
21 | name: exportName,
22 | format: 'iife',
23 | sourcemap: true
24 | }, {
25 | file: 'dist/main.iife.min.js',
26 | name: exportName,
27 | format: 'iife',
28 | plugins: [minifyPlugin]
29 | }, {
30 | file: 'dist/main.cjs',
31 | name: exportName,
32 | format: 'cjs',
33 | sourcemap: true
34 | }, {
35 | file: 'dist/main.min.cjs',
36 | name: exportName,
37 | format: 'cjs',
38 | plugins: [minifyPlugin]
39 | }, {
40 | file: 'dist/main.js',
41 | name: exportName,
42 | format: 'esm',
43 | sourcemap: true
44 | }, {
45 | file: 'dist/main.min.js',
46 | name: exportName,
47 | format: 'esm',
48 | plugins: [minifyPlugin]
49 | }],
50 | plugins: [
51 | esbuild()
52 | ]
53 | }
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "singui",
3 | "version": "0.3.7",
4 | "description": "The next-gen, no compile/transpile needed, self-contained JS UI library",
5 | "main": "dist/main.min.js",
6 | "module": "src/main.mjs",
7 | "unpkg": "dist/main.umd.min.js",
8 | "scripts": {
9 | "build": "rollup -c ./rollup.esbuild.config.mjs"
10 | },
11 | "exports": {
12 | "./package.json": "./package.json",
13 | ".": {
14 | "module": "src/main.mjs",
15 | "script": "dist/main.umd.min.js",
16 | "require": "dist/main.min.cjs",
17 | "node": "src/main.mjs",
18 | "default": "dist/main.min.js"
19 | }
20 | },
21 | "files": [
22 | "src/**/*",
23 | "dist/**/*"
24 | ],
25 | "repository": {
26 | "type": "git",
27 | "url": "git+https://github.com/ClassicOldSong/SingUI.git"
28 | },
29 | "keywords": [
30 | "singui"
31 | ],
32 | "author": "Yukino Song",
33 | "license": "MIT",
34 | "bugs": {
35 | "url": "https://github.com/ClassicOldSong/SingUI/issues"
36 | },
37 | "homepage": "https://github.com/ClassicOldSong/SingUI",
38 | "devDependencies": {
39 | "esbuild": "^0.15.16",
40 | "rollup": "^3.5.0",
41 | "rollup-plugin-esbuild": "^5.0.0"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SINGUI
2 |
3 | The next-gen, no compile/transpile needed, self-contained JS UI library
4 |
5 | [Try it out](https://stackblitz.com/edit/singui-demo?file=index.js)
6 |
7 | ## Usage
8 |
9 | ### Browser
10 |
11 | ```html
12 |
13 |
16 | ```
17 |
18 | or
19 |
20 | ```javascript
21 | import {browser, tags, text, attr, prop, setGlobalCtx} from 'singui'
22 | ```
23 |
24 | or
25 |
26 | ```javascript
27 | const {browser, tags, text, attr, prop, setGlobalCtx} = require('singui')
28 | ```
29 |
30 | then
31 |
32 | ```javascript
33 | setGlobalCtx(browser())
34 |
35 | const app = (target) => build(({attach}) => {
36 | const {h1, center, p} = tags
37 |
38 | center(() => {
39 | h1(() => {
40 | attr.style = 'font-weight: 300'
41 | text('Hello World!')
42 | })
43 | })
44 |
45 | p(() => {
46 | const style = prop.style
47 | style.color = 'green'
48 | style.textAlign = 'center'
49 | text('Welcome to SingUI')
50 | })
51 |
52 | attach(target)
53 | })
54 |
55 | app(document.body)
56 | ```
57 | More details please see [Try it out](https://stackblitz.com/edit/singui-demo?file=index.js)
58 |
59 | ## License
60 |
61 | MIT
62 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es6": true
6 | },
7 | "globals": {
8 | "ENV": true,
9 | "process": true,
10 | "__dirname": true,
11 | "__filename": true
12 | },
13 | "extends": [
14 | "eslint:recommended"
15 | ],
16 | "parserOptions": {
17 | "ecmaVersion": 8,
18 | "sourceType": "module"
19 | },
20 | "settings": {},
21 | "rules": {
22 | "accessor-pairs": "error",
23 | "array-bracket-spacing": [
24 | "error",
25 | "never"
26 | ],
27 | "array-callback-return": "error",
28 | "arrow-body-style": "error",
29 | "arrow-parens": [
30 | "error",
31 | "as-needed",
32 | {
33 | "requireForBlockBody": true
34 | }
35 | ],
36 | "arrow-spacing": [
37 | "error",
38 | {
39 | "after": true,
40 | "before": true
41 | }
42 | ],
43 | "block-scoped-var": "error",
44 | "block-spacing": "error",
45 | "brace-style": [
46 | "error",
47 | "1tbs"
48 | ],
49 | "callback-return": "error",
50 | "camelcase": "warn",
51 | "class-methods-use-this": "error",
52 | "comma-dangle": [
53 | "error",
54 | "only-multiline"
55 | ],
56 | "comma-spacing": "off",
57 | "comma-style": [
58 | "error",
59 | "last"
60 | ],
61 | "complexity": ["warn", "max": 25],
62 | "computed-property-spacing": [
63 | "error",
64 | "never"
65 | ],
66 | "consistent-return": "off",
67 | "consistent-this": "error",
68 | "curly": "off",
69 | "default-case": "error",
70 | "dot-location": "off",
71 | "dot-notation": "error",
72 | "eol-last": "off",
73 | "eqeqeq": "error",
74 | "func-call-spacing": "error",
75 | "func-names": [
76 | "error",
77 | "never"
78 | ],
79 | "func-style": [
80 | "off",
81 | "expression"
82 | ],
83 | "generator-star-spacing": "error",
84 | "global-require": "error",
85 | "guard-for-in": "off",
86 | "handle-callback-err": "error",
87 | "id-blacklist": "error",
88 | "id-length": "off",
89 | "id-match": "error",
90 | "indent": "off",
91 | "init-declarations": "error",
92 | "jsx-quotes": "off",
93 | "key-spacing": "error",
94 | "keyword-spacing": [
95 | "error",
96 | {
97 | "after": true,
98 | "before": true
99 | }
100 | ],
101 | "line-comment-position": "off",
102 | "linebreak-style": [
103 | "off"
104 | ],
105 | "lines-around-comment": "error",
106 | "lines-around-directive": "off",
107 | "max-depth": "off",
108 | "max-len": "off",
109 | "max-lines": "off",
110 | "max-nested-callbacks": "error",
111 | "max-params": "error",
112 | "max-statements": "off",
113 | "max-statements-per-line": "error",
114 | "multiline-ternary": "off",
115 | "new-parens": "error",
116 | "newline-after-var": "off",
117 | "newline-before-return": "off",
118 | "newline-per-chained-call": "error",
119 | "no-alert": "error",
120 | "no-array-constructor": "error",
121 | "no-bitwise": "error",
122 | "no-caller": "error",
123 | "no-catch-shadow": "error",
124 | "no-confusing-arrow": "error",
125 | "no-console": "off",
126 | "no-continue": "error",
127 | "no-div-regex": "error",
128 | "no-duplicate-imports": "error",
129 | "no-else-return": "off",
130 | "no-empty-function": "error",
131 | "no-eq-null": "error",
132 | "no-eval": "error",
133 | "no-extend-native": "error",
134 | "no-extra-bind": "error",
135 | "no-extra-label": "error",
136 | "no-extra-parens": "off",
137 | "no-floating-decimal": "error",
138 | "no-global-assign": "error",
139 | "no-implicit-globals": "error",
140 | "no-implied-eval": "error",
141 | "no-inline-comments": "off",
142 | "no-invalid-this": "off",
143 | "no-iterator": "error",
144 | "no-label-var": "error",
145 | "no-labels": "error",
146 | "no-lone-blocks": "error",
147 | "no-lonely-if": "error",
148 | "no-loop-func": "error",
149 | "no-magic-numbers": "off",
150 | "no-mixed-operators": "off",
151 | "no-mixed-requires": "error",
152 | "no-multi-spaces": "error",
153 | "no-multi-str": "error",
154 | "no-multiple-empty-lines": "error",
155 | "no-negated-condition": "error",
156 | "no-nested-ternary": "error",
157 | "no-new": "error",
158 | "no-new-func": "error",
159 | "no-new-object": "error",
160 | "no-new-require": "error",
161 | "no-new-wrappers": "error",
162 | "no-octal-escape": "error",
163 | "no-param-reassign": "off",
164 | "no-path-concat": "error",
165 | "no-plusplus": [
166 | "error",
167 | {
168 | "allowForLoopAfterthoughts": true
169 | }
170 | ],
171 | "no-process-env": "off",
172 | "no-process-exit": "error",
173 | "no-proto": "error",
174 | "no-prototype-builtins": "error",
175 | "no-restricted-globals": "error",
176 | "no-restricted-imports": "error",
177 | "no-restricted-modules": "error",
178 | "no-restricted-properties": "error",
179 | "no-restricted-syntax": "error",
180 | "no-return-assign": "error",
181 | "no-script-url": "error",
182 | "no-self-compare": "error",
183 | "no-sequences": "error",
184 | "no-shadow": "off",
185 | "no-shadow-restricted-names": "error",
186 | "no-spaced-func": "error",
187 | "no-sync": "error",
188 | "no-tabs": "off",
189 | "no-template-curly-in-string": "error",
190 | "no-ternary": "off",
191 | "no-throw-literal": "error",
192 | "no-trailing-spaces": "error",
193 | "no-undef-init": "error",
194 | "no-undefined": "error",
195 | "no-underscore-dangle": "off",
196 | "no-unmodified-loop-condition": "error",
197 | "no-unneeded-ternary": "error",
198 | "no-unsafe-negation": "error",
199 | "no-unused-expressions": "error",
200 | "no-use-before-define": "error",
201 | "no-useless-call": "error",
202 | "no-useless-computed-key": "error",
203 | "no-useless-concat": "error",
204 | "no-useless-constructor": "error",
205 | "no-useless-escape": "error",
206 | "no-useless-rename": "error",
207 | "no-var": "error",
208 | "no-void": "error",
209 | "no-warning-comments": "error",
210 | "no-whitespace-before-property": "error",
211 | "no-with": "error",
212 | "object-curly-newline": "off",
213 | "object-curly-spacing": [
214 | "off",
215 | "never"
216 | ],
217 | "object-property-newline": "off",
218 | "object-shorthand": "off",
219 | "one-var": "off",
220 | "one-var-declaration-per-line": "error",
221 | "operator-assignment": "error",
222 | "operator-linebreak": "error",
223 | "padded-blocks": "off",
224 | "prefer-arrow-callback": "error",
225 | "prefer-const": "off",
226 | "prefer-numeric-literals": "error",
227 | "prefer-reflect": "off",
228 | "prefer-rest-params": "error",
229 | "prefer-spread": "error",
230 | "prefer-template": "error",
231 | "quote-props": "off",
232 | "quotes": "off",
233 | "radix": "error",
234 | "require-jsdoc": "off",
235 | "rest-spread-spacing": [
236 | "error",
237 | "never"
238 | ],
239 | "semi": [
240 | "warn",
241 | "never"
242 | ],
243 | "semi-spacing": [
244 | "error",
245 | {
246 | "after": true,
247 | "before": false
248 | }
249 | ],
250 | "sort-imports": "off",
251 | "sort-keys": "off",
252 | "sort-vars": "off",
253 | "space-before-blocks": "error",
254 | "space-before-function-paren": "off",
255 | "space-in-parens": [
256 | "error",
257 | "never"
258 | ],
259 | "space-infix-ops": "error",
260 | "space-unary-ops": [
261 | "error",
262 | {
263 | "nonwords": false,
264 | "words": false
265 | }
266 | ],
267 | "spaced-comment": [
268 | "error",
269 | "always"
270 | ],
271 | "strict": "off",
272 | "symbol-description": "error",
273 | "template-curly-spacing": [
274 | "error",
275 | "never"
276 | ],
277 | "unicode-bom": [
278 | "error",
279 | "never"
280 | ],
281 | "valid-jsdoc": "error",
282 | "vars-on-top": "error",
283 | "wrap-iife": "error",
284 | "wrap-regex": "error",
285 | "yield-star-spacing": "error",
286 | "yoda": [
287 | "error",
288 | "never"
289 | ],
290 | }
291 | }
292 |
--------------------------------------------------------------------------------
/src/main.mjs:
--------------------------------------------------------------------------------
1 | const TARGET_SYMBOL = Symbol('TARGET')
2 | const emptyObj = Object.create(null)
3 | const proxyMap = new WeakMap()
4 | const R = Reflect
5 |
6 | const prepareHooks = () => {
7 | let hooks = new Set()
8 | const runHooks = (...args) => hooks.forEach(hook => hook(...args))
9 |
10 | const addHooks = (...newHooks) => {
11 | for (let i of newHooks) hooks.add(i)
12 |
13 | let disconnected = false
14 |
15 | return () => {
16 | if (disconnected) return
17 | for (let i of newHooks) hooks.delete(i)
18 | disconnected = true
19 | }
20 | }
21 |
22 | return [runHooks, addHooks]
23 | }
24 |
25 | const useSignal = (initVal) => {
26 | let val = initVal
27 |
28 | const [runHooks, addHooks] = prepareHooks()
29 |
30 | const connect = (...handlers) => {
31 | if (handlers.length === 0) return val
32 | for (let i of handlers) i(val)
33 | return addHooks(...handlers)
34 | }
35 |
36 | const setVal = (newVal) => {
37 | if (val === newVal) return
38 | if (typeof newVal === 'function') newVal = newVal(val)
39 | const oldVal = val
40 | val = newVal
41 | runHooks(newVal, oldVal)
42 | }
43 |
44 | const signal = (newVal) => {
45 | // eslint-disable-next-line no-undefined
46 | if (newVal === undefined) return val
47 | return setVal(newVal)
48 | }
49 |
50 | signal.connect = connect
51 |
52 | return signal
53 | }
54 |
55 | const mux = (...args) => {
56 | const staticStrs = args.shift()
57 | const valList = new Array(staticStrs.length + args.length)
58 |
59 | let batchDepth = 0
60 | let handlerCount = 0
61 | let disconnectList = null
62 | let evalList = []
63 |
64 | for (let i in staticStrs) {
65 | valList[i * 2] = staticStrs[i]
66 | }
67 |
68 | const strMux = useSignal()
69 |
70 | const flush = () => {
71 | if (batchDepth <= 0) {
72 | for (let i of evalList) i()
73 | strMux(''.concat(...valList))
74 | batchDepth = 0
75 | }
76 | }
77 |
78 | const pause = () => {
79 | batchDepth += 1
80 | }
81 |
82 | const resume = () => {
83 | batchDepth -= 1
84 | if (batchDepth <= 0) flush()
85 | }
86 |
87 | const batch = (handler) => {
88 | pause()
89 | handler()
90 | resume()
91 | }
92 |
93 | const init = () => {
94 | if (disconnectList) return
95 | pause()
96 | disconnectList = args.map((signal, index) => {
97 | index = index * 2 + 1
98 | if (typeof signal === 'function') {
99 | evalList.push(() => {
100 | valList[index] = signal()
101 | })
102 | if (typeof signal.connect === 'function') return signal.connect(flush)
103 | return null
104 | }
105 |
106 | valList[index] = signal
107 | return null
108 | })
109 | resume()
110 | }
111 |
112 | const destroy = () => {
113 | if (!disconnectList) return
114 | for (let i of disconnectList) {
115 | if (i) i()
116 | }
117 | disconnectList = null
118 | evalList.length = 0
119 | handlerCount = 0
120 | }
121 |
122 | const cleanup = () => {
123 | handlerCount -= 1
124 | if (handlerCount <= 0) destroy()
125 | }
126 |
127 | const connect = (handler) => {
128 | if (!handler) return strMux()
129 |
130 | if (!disconnectList) init()
131 |
132 | handlerCount += 1
133 |
134 | const disconnectHandler = strMux.connect(handler)
135 |
136 | return () => {
137 | if (disconnectHandler()) {
138 | cleanup()
139 | return true
140 | }
141 |
142 | return false
143 | }
144 | }
145 |
146 | const disconnect = (handler) => {
147 | if (strMux.disconnect(handler)) cleanup()
148 | }
149 |
150 | let muxedSignal = null
151 |
152 | const watch = (...signals) => {
153 | if (!disconnectList) init()
154 |
155 | for (let i of signals) {
156 | disconnectList.push(i.connect(flush))
157 | }
158 |
159 | return muxedSignal
160 | }
161 |
162 | muxedSignal = (...args) => {
163 | if (!args.length) return strMux()
164 | return watch(...args)
165 | }
166 |
167 | muxedSignal.connect = connect
168 | muxedSignal.disconnect = disconnect
169 | muxedSignal.pause = pause
170 | muxedSignal.resume = resume
171 | muxedSignal.batch = batch
172 | muxedSignal.flush = flush
173 | muxedSignal.watch = watch
174 |
175 | return muxedSignal
176 | }
177 |
178 | const getCachedProxy = (target, lifeCycle) => {
179 | if (proxyMap.has(target)) {
180 | const lifeCycleMap = proxyMap.get(target)
181 | if (lifeCycleMap.has(lifeCycle)) return lifeCycleMap.get(lifeCycle)
182 | }
183 |
184 | return null
185 | }
186 |
187 | const proxify = (handler, target, lifeCycle) => {
188 | if (target) {
189 | let lifeCycleMap = null
190 | if (proxyMap.has(target)) {
191 | lifeCycleMap = proxyMap.get(target)
192 | } else {
193 | lifeCycleMap = new WeakMap()
194 | proxyMap.set(target, lifeCycleMap)
195 | }
196 |
197 | const proxied = new Proxy(target, handler)
198 | lifeCycleMap.set(lifeCycle, proxied)
199 |
200 | return proxied
201 | }
202 |
203 | return new Proxy(emptyObj, handler)
204 | }
205 |
206 | const unwrap = proxiedObj => R.get(proxiedObj, TARGET_SYMBOL) || proxiedObj
207 |
208 | const wrapObj = (target, lifeCycle) => {
209 | const cachedProxy = getCachedProxy(target, lifeCycle)
210 | if (cachedProxy) return cachedProxy
211 |
212 | const targetObj = Object(target)
213 | if (targetObj !== target) return target
214 |
215 | const signalMap = {}
216 |
217 | const propProxy = proxify({
218 | get(_, propName) {
219 | if (propName === TARGET_SYMBOL) return target
220 | if (propName[0] === '$') {
221 | const realPropName = propName.substring(1)
222 | return (handler) => {
223 | if (handler) return val => R.set(propProxy, realPropName, handler(val, R.get(targetObj, realPropName), realPropName))
224 | return val => R.set(propProxy, realPropName, val)
225 | }
226 | }
227 |
228 | if (propName[0] === '_') {
229 | const realPropName = propName.substring(1)
230 | return (handler) => {
231 | if (handler) return () => handler(realPropName, propProxy)
232 | return () => wrapObj(R.get(target, realPropName), lifeCycle)
233 | }
234 | }
235 |
236 | return wrapObj(R.get(target, propName), lifeCycle)
237 | },
238 | set(_, propName, val) {
239 | if (propName[0] === '$' || propName[0] === '_') propName = propName.substring(1)
240 |
241 | if (signalMap[propName]) {
242 | if (val === signalMap[propName].signal) return true
243 |
244 | signalMap[propName].disconnect()
245 | delete signalMap[propName]
246 | }
247 |
248 | if (typeof val === 'function' && typeof val.connect === 'function') {
249 |
250 | let settedUp = false
251 |
252 | const setup = () => {
253 | if (settedUp) return
254 |
255 | const disconnect = val.connect(newVal => R.set(target, propName, newVal))
256 | const disconnectSelf = lifeCycle.onAfterDetatch(() => {
257 | disconnect()
258 | disconnectSelf()
259 | settedUp = false
260 | })
261 |
262 | settedUp = true
263 | }
264 |
265 | const disconnect = lifeCycle.onBeforeAttach(setup)
266 |
267 | signalMap[propName] = {
268 | signal: val,
269 | setup,
270 | disconnect
271 | }
272 |
273 | setTimeout(setup, 0)
274 |
275 | return true
276 | }
277 |
278 | return R.set(target, propName, val)
279 | },
280 | apply(_, thisArg, argList) {
281 | R.apply(target, unwrap(thisArg), argList)
282 | }
283 | }, targetObj, lifeCycle)
284 |
285 | return propProxy
286 | }
287 |
288 | const camelToKebab = str => [...str].map((i) => {
289 | const lowerCaseLetter = i.toLowerCase()
290 | if (i === lowerCaseLetter) return i
291 | return `-${lowerCaseLetter}`
292 | }).join('')
293 |
294 | const env = ({
295 | createElement,
296 | createTextNode,
297 | createComment,
298 | createDocumentFragment,
299 | cloneElement,
300 | appendChild,
301 | appendBefore,
302 | appendAfter,
303 | getNextSibling,
304 | getAttr,
305 | setAttr,
306 | removeAttr,
307 | addEventListener,
308 | removeEventListener
309 | }, {
310 | tags = null,
311 | build = null,
312 | currentNode = null,
313 | currentNamespace = null,
314 | // hydrating = null,
315 | lifeCycleHooks = new WeakMap()
316 | } = {}) => {
317 |
318 | const prevNodes = []
319 | const pushCurrentNode = (node) => {
320 | prevNodes.push(currentNode)
321 | currentNode = node
322 | }
323 | const popCurrentNode = () => {
324 | currentNode = prevNodes.pop()
325 | }
326 |
327 | const prevNamespaces = []
328 | const pushCurrentNamespace = (namespace) => {
329 | prevNamespaces.push(namespace)
330 | currentNamespace = namespace
331 | }
332 | const popCurrentNamespace = () => {
333 | currentNamespace = prevNamespaces.pop()
334 | }
335 |
336 | const scoped = (builder, node = currentNode) => {
337 | if (node === null) return builder
338 | return (...args) => {
339 | pushCurrentNode(node)
340 | const ret = builder(...args)
341 | popCurrentNode()
342 | return ret
343 | }
344 | }
345 |
346 | const namespaced = (builder, namespace = currentNamespace) => {
347 | if (namespace === null) return builder
348 | return (...args) => {
349 | pushCurrentNamespace(namespace)
350 | const ret = builder(...args)
351 | popCurrentNamespace()
352 | return ret
353 | }
354 | }
355 |
356 | const clearScope = builder => (...args) => {
357 | pushCurrentNode()
358 | const ret = builder(...args)
359 | popCurrentNode()
360 | return ret
361 | }
362 |
363 | const clearNamespace = builder => (...args) => {
364 | pushCurrentNamespace()
365 | const ret = builder(...args)
366 | popCurrentNamespace()
367 | return ret
368 | }
369 |
370 | const on = (...args) => addEventListener(currentNode, ...args)
371 | const off = (...args) => removeEventListener(currentNode, ...args)
372 |
373 | const useElement = () => currentNode
374 | const useTags = (toKebab = true, namespace = null) => {
375 | const getTag = namespaced(tagName => R.get(tags, tagName), namespace)
376 | return proxify({
377 | get(_, tagName) {
378 | if (toKebab) tagName = camelToKebab(tagName)
379 | return getTag(tagName)
380 | }
381 | })
382 | }
383 |
384 | let currentLifeCycleNode = null
385 |
386 | const useLifeCycle = (target) => {
387 | if (!target && currentLifeCycleNode) return useLifeCycle(currentLifeCycleNode)
388 |
389 | target = unwrap(target || currentNode)
390 | let hooks = lifeCycleHooks.get(target)
391 | if (hooks) return hooks
392 |
393 | const [beforeAttach, onBeforeAttach] = prepareHooks()
394 | const [afterAttach, onAfterAttach] = prepareHooks()
395 | const [beforeDetatch, onBeforeDetatch] = prepareHooks()
396 | const [afterDetatch, onAfterDetatch] = prepareHooks()
397 |
398 | hooks = {
399 | beforeAttach,
400 | afterAttach,
401 | beforeDetatch,
402 | afterDetatch,
403 | onBeforeAttach,
404 | onAfterAttach,
405 | onBeforeDetatch,
406 | onAfterDetatch
407 | }
408 |
409 | lifeCycleHooks.set(target, hooks)
410 |
411 | return hooks
412 | }
413 |
414 | const withLifeCycle = (handler, target = currentNode) => {
415 | const prevNode = currentLifeCycleNode
416 | currentLifeCycleNode = target
417 |
418 | const ret = handler()
419 |
420 | currentLifeCycleNode = prevNode
421 | return ret
422 | }
423 |
424 | const wrap = (target, lifeCycle = useLifeCycle()) => wrapObj(target, lifeCycle)
425 |
426 | const attrProxyMap = new WeakMap()
427 |
428 | const toAttr = (target) => {
429 | if (attrProxyMap.has(target)) return attrProxyMap.get(target)
430 |
431 | const attrProxy = new Proxy(target, {
432 | get(_, attrName) {
433 | return getAttr(target, attrName, currentNamespace)
434 | },
435 | set(_, attrName, val) {
436 | if (val === null) removeAttr(target, attrName, currentNamespace)
437 | setAttr(target, attrName, val, currentNamespace)
438 | return true
439 | }
440 | })
441 |
442 | attrProxyMap.set(target, attrProxy)
443 |
444 | return attrProxy
445 | }
446 |
447 | const attr = proxify({
448 | get(_, attrName) {
449 | return R.get(wrap(toAttr(currentNode)), attrName)
450 | },
451 | set(_, attrName, val) {
452 | return R.set(wrap(toAttr(currentNode)), attrName, val)
453 | }
454 | })
455 | const useAttr = (capture = true, toKebab = true, namespace = null) => {
456 | const scope = capture && currentNode || null
457 | const getAttribute = scoped(namespaced(attrName => R.get(attr, attrName), namespace), scope)
458 | const setAttribute = scoped(namespaced((attrName, val) => R.set(attr, attrName, val), namespace), scope)
459 |
460 | return proxify({
461 | get(_, attrName) {
462 | if (toKebab) attrName = camelToKebab(attrName)
463 | return getAttribute(attrName)
464 | },
465 | set(_, attrName, val) {
466 | if (toKebab) attrName = camelToKebab(attrName)
467 | return setAttribute(attrName, val)
468 | }
469 | })
470 | }
471 |
472 | const prop = proxify({
473 | get(_, propName) {
474 | return R.get(wrap(currentNode), propName)
475 | },
476 | set(_, propName, val) {
477 | return R.set(wrap(currentNode), propName, val)
478 | }
479 | })
480 | const useProp = () => {
481 | const getProp = scoped(propName => R.get(prop, propName))
482 | const setProp = scoped((propName, val) => R.set(prop, propName, val))
483 |
484 | return proxify({
485 | get(_, propName) {
486 | return getProp(propName)
487 | },
488 | set(_, propName, val) {
489 | return setProp(propName, val)
490 | }
491 | })
492 | }
493 |
494 | const text = (initVal) => {
495 | const textNode = createTextNode('')
496 | pushCurrentNode(textNode)
497 | const wrappedNode = wrap(textNode)
498 | if (initVal) wrappedNode.textContent = initVal
499 | popCurrentNode()
500 | if (currentNode) appendChild(currentNode, textNode)
501 | return wrappedNode
502 | }
503 |
504 | const comment = (initVal) => {
505 | const commentNode = createComment('')
506 | pushCurrentNode(commentNode)
507 | const wrappedNode = wrap(commentNode)
508 | if (initVal) wrappedNode.textContent = initVal
509 | popCurrentNode()
510 | if (currentNode) appendChild(currentNode, commentNode)
511 | return wrappedNode
512 | }
513 |
514 | const fragment = (builder, append = true) => {
515 | const ret = {}
516 |
517 | build(({attach, detatch, before, after, startAnchor, endAnchor}) => {
518 | ret.attach = attach
519 | ret.detatch = detatch
520 | ret.before = before
521 | ret.after = after
522 | ret.empty = () => {
523 | const tempStore = createDocumentFragment()
524 |
525 | let currentElement = getNextSibling(startAnchor)
526 | while (currentElement !== endAnchor) {
527 | const nextElement = getNextSibling(currentElement)
528 | appendChild(tempStore, currentElement)
529 | currentElement = nextElement
530 | }
531 | }
532 | ret.append = (builder) => {
533 | const tempStore = createDocumentFragment()
534 | const ret = scoped(build, tempStore)(builder)
535 | appendBefore(endAnchor, tempStore)
536 | return ret
537 | }
538 | ret.set = (builder) => {
539 | ret.empty()
540 | return ret.append(builder)
541 | }
542 | }, append)
543 |
544 | if (builder) ret.append(builder)
545 |
546 | return ret
547 | }
548 |
549 | const adopt = (rawElement, clone) => (builder, append = true) => {
550 | if (!rawElement) return
551 |
552 | const element = clone ? cloneElement(rawElement) : rawElement
553 | const elementStore = createDocumentFragment()
554 |
555 | const {beforeAttach, afterAttach, beforeDetatch, afterDetatch} = useLifeCycle(element)
556 |
557 | const attach = (target) => {
558 | if (!target) target = currentNode
559 | if (!target) return
560 | beforeAttach(target)
561 | appendChild(target, element)
562 | afterAttach(target)
563 | }
564 | const detatch = () => {
565 | beforeDetatch()
566 | appendChild(elementStore, element)
567 | afterDetatch()
568 | }
569 | const before = (builder) => {
570 | const tempStore = createDocumentFragment()
571 | const ret = scoped(build, tempStore)(builder)
572 | appendBefore(element, tempStore)
573 | return ret
574 | }
575 | const after = (builder) => {
576 | const tempStore = createDocumentFragment()
577 | const ret = scoped(build, tempStore)(builder)
578 | appendAfter(element, tempStore)
579 | return ret
580 | }
581 |
582 | // eslint-disable-next-line init-declarations
583 | let ret
584 |
585 | if (builder) {
586 | pushCurrentNode(element)
587 | ret = clearNamespace(builder)({
588 | build,
589 | adopt,
590 | text,
591 | comment,
592 | fragment,
593 | scoped,
594 | namespaced,
595 | clearScope,
596 | clearNamespace,
597 | element,
598 | on,
599 | off,
600 | mux,
601 | useSignal,
602 | useTags,
603 | useElement,
604 | useAttr,
605 | useProp,
606 | useLifeCycle,
607 | withLifeCycle,
608 | tags,
609 | attr,
610 | prop,
611 | attach,
612 | detatch,
613 | before,
614 | after
615 | })
616 | popCurrentNode()
617 | }
618 |
619 | if (append && currentNode) attach(currentNode)
620 | else attach(elementStore)
621 |
622 | if (!clone) rawElement = null
623 |
624 | return {element, ret, attach, detatch, before, after}
625 | }
626 |
627 | if (!tags) {
628 | tags = proxify({
629 | get(_, tagName) {
630 | const namespace = currentNamespace
631 | return (builder, append) => {
632 | const element = createElement(tagName, namespace)
633 | return adopt(element, false)(builder, append)
634 | }
635 | }
636 | })
637 | }
638 |
639 | if (!build) {
640 | build = (builder, append = true) => {
641 | const elementStore = createDocumentFragment()
642 | const startAnchor = createTextNode('')
643 | const endAnchor = createTextNode('')
644 |
645 | builder = clearNamespace(builder)
646 |
647 | appendChild(elementStore, startAnchor)
648 | appendChild(elementStore, endAnchor)
649 |
650 | pushCurrentNode(elementStore)
651 |
652 | const {beforeAttach, afterAttach, beforeDetatch, afterDetatch} = useLifeCycle(elementStore)
653 |
654 | const detatch = () => {
655 | beforeDetatch()
656 |
657 | let currentElement = startAnchor
658 | while (currentElement !== endAnchor) {
659 | const nextElement = getNextSibling(currentElement)
660 | appendChild(elementStore, currentElement)
661 | currentElement = nextElement
662 | }
663 | appendChild(elementStore, endAnchor)
664 |
665 | afterDetatch()
666 | }
667 | const attach = (target) => {
668 | if (!target) target = currentNode
669 | if (!target) return
670 |
671 | detatch()
672 |
673 | beforeAttach(target)
674 |
675 | appendChild(target, startAnchor)
676 | appendChild(target, elementStore)
677 | appendChild(target, endAnchor)
678 |
679 | afterAttach(target)
680 | }
681 | const before = (builder) => {
682 | const tempStore = createDocumentFragment()
683 | const ret = scoped(build, tempStore)(builder)
684 | appendBefore(startAnchor, tempStore)
685 | return ret
686 | }
687 | const after = (builder) => {
688 | const tempStore = createDocumentFragment()
689 | const ret = scoped(build, tempStore)(builder)
690 | appendAfter(endAnchor, tempStore)
691 | return ret
692 | }
693 |
694 | const ret = builder({
695 | build,
696 | adopt,
697 | text,
698 | comment,
699 | fragment,
700 | scoped,
701 | namespaced,
702 | clearScope,
703 | clearNamespace,
704 | on,
705 | off,
706 | mux,
707 | useSignal,
708 | useTags,
709 | useElement,
710 | useAttr,
711 | useProp,
712 | useLifeCycle,
713 | withLifeCycle,
714 | tags,
715 | attr,
716 | prop,
717 | attach,
718 | detatch,
719 | before,
720 | after,
721 | startAnchor,
722 | endAnchor
723 | })
724 |
725 | popCurrentNode()
726 | if (currentNode && append) attach(currentNode)
727 |
728 | return ret
729 | }
730 | }
731 |
732 | return {
733 | wrap,
734 | build,
735 | adopt,
736 | text,
737 | comment,
738 | fragment,
739 | scoped,
740 | namespaced,
741 | clearScope,
742 | clearNamespace,
743 | on,
744 | off,
745 | mux,
746 | useSignal,
747 | useTags,
748 | useElement,
749 | useAttr,
750 | useProp,
751 | useLifeCycle,
752 | withLifeCycle,
753 | tags: useTags(),
754 | attr: useAttr(false),
755 | prop
756 | }
757 | }
758 |
759 | const browser = (doc = document, userNamespaceMap = {}) => {
760 | const namespaceURIMap = Object.assign({
761 | xml: 'http://www.w3.org/XML/1998/namespace',
762 | html: 'http://www.w3.org/1999/xhtml',
763 | svg: 'http://www.w3.org/2000/svg',
764 | math: 'http://www.w3.org/1998/Math/MathML',
765 | xlink: 'http://www.w3.org/1999/xlink'
766 | }, userNamespaceMap)
767 |
768 | return env({
769 | createElement(tag, namespace) {
770 | if (namespace) {
771 | const namespaceURI = Reflect.get(namespaceURIMap, namespace) || namespace
772 | return doc.createElementNS(namespaceURI, tag)
773 | }
774 | return doc.createElement(tag)
775 | },
776 | createTextNode(text) {
777 | return doc.createTextNode(text)
778 | },
779 | createComment(text) {
780 | return doc.createComment(text)
781 | },
782 | createDocumentFragment() {
783 | return doc.createDocumentFragment()
784 | },
785 | cloneElement(element) {
786 | return element.cloneNode(true)
787 | },
788 | appendChild(parent, child) {
789 | return parent.appendChild(child)
790 | },
791 | appendBefore(node, element) {
792 | return node.parentNode.insertBefore(element, node)
793 | },
794 | appendAfter(node, element) {
795 | return node.parentNode.insertBefore(element, node.nextSibling)
796 | },
797 | getNextSibling(node) {
798 | return node.nextSibling
799 | },
800 | getAttr(node, attrName, namespace) {
801 | if (namespace) {
802 | const namespaceURI = Reflect.get(namespaceURIMap, namespace) || namespace
803 | return node.getAttributeNS(namespaceURI, attrName)
804 | }
805 | return node.getAttribute(attrName)
806 | },
807 | // eslint-disable-next-line max-params
808 | setAttr(node, attrName, val, namespace) {
809 | if (namespace) {
810 | const namespaceURI = Reflect.get(namespaceURIMap, namespace) || namespace
811 | return node.setAttributeNS(namespaceURI, attrName, val)
812 | }
813 | return node.setAttribute(attrName, val)
814 | },
815 | removeAttr(node, attrName, namespace) {
816 | if (namespace) {
817 | const namespaceURI = Reflect.get(namespaceURIMap, namespace) || namespace
818 | return node.removeAttributeNS(namespaceURI, attrName)
819 | }
820 | return node.removeAttribute(attrName)
821 | },
822 | addEventListener(node, ...args) {
823 | return node.addEventListener(...args)
824 | },
825 | removeEventListener(node, ...args) {
826 | return node.removeEventListener(...args)
827 | }
828 | })
829 | }
830 |
831 | let globalCtx = null
832 |
833 | const wrap = (...args) => globalCtx.wrap(...args)
834 | const build = (...args) => globalCtx.build(...args)
835 | const adopt = (...args) => globalCtx.adopt(...args)
836 | const text = (...args) => globalCtx.text(...args)
837 | const comment = (...args) => globalCtx.comment(...args)
838 | const fragment = (...args) => globalCtx.fragment(...args)
839 | const scoped = (...args) => globalCtx.scoped(...args)
840 | const namespaced = (...args) => globalCtx.namespaced(...args)
841 | const clearScope = (...args) => globalCtx.clearScope(...args)
842 | const clearNamespace = (...args) => globalCtx.clearNamespace(...args)
843 | const on = (...args) => globalCtx.on(...args)
844 | const off = (...args) => globalCtx.off(...args)
845 | const useTags = (...args) => globalCtx.useTags(...args)
846 | const useElement = (...args) => globalCtx.useElement(...args)
847 | const useAttr = (...args) => globalCtx.useAttr(...args)
848 | const useProp = (...args) => globalCtx.useProp(...args)
849 | const useLifeCycle = (...args) => globalCtx.useLifeCycle(...args)
850 | const withLifeCycle = (...args) => globalCtx.withLifeCycle(...args)
851 | const tags = proxify({
852 | get(_, tagName) {
853 | return (...args) => R.get(globalCtx.tags, tagName)(...args)
854 | }
855 | })
856 | const attr = proxify({
857 | get(_, attrName) {
858 | return R.get(globalCtx.attr, attrName)
859 | },
860 | set(_, attrName, val) {
861 | return R.set(globalCtx.attr, attrName, val)
862 | }
863 | })
864 | const prop = proxify({
865 | get(_, propName) {
866 | return R.get(globalCtx.prop, propName)
867 | },
868 | set(_, propName, val) {
869 | return R.set(globalCtx.prop, propName, val)
870 | }
871 | })
872 |
873 | const setGlobalCtx = (ctx) => {
874 | globalCtx = ctx
875 | }
876 |
877 | const getGlobalCtx = () => globalCtx
878 |
879 | export {
880 | env,
881 | browser,
882 | wrap,
883 | unwrap,
884 | build,
885 | adopt,
886 | text,
887 | comment,
888 | fragment,
889 | scoped,
890 | namespaced,
891 | clearScope,
892 | clearNamespace,
893 | on,
894 | off,
895 | mux,
896 | useSignal,
897 | useElement,
898 | useTags,
899 | useAttr,
900 | useProp,
901 | useLifeCycle,
902 | withLifeCycle,
903 | tags,
904 | attr,
905 | prop,
906 | setGlobalCtx,
907 | getGlobalCtx
908 | }
909 |
--------------------------------------------------------------------------------