amateras 0.4.2 → 0.6.0

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.
Files changed (97) hide show
  1. package/README.md +24 -25
  2. package/ext/html/node/$Anchor.ts +3 -3
  3. package/ext/html/node/$Canvas.ts +2 -2
  4. package/ext/html/node/$Dialog.ts +2 -2
  5. package/ext/html/node/$Form.ts +2 -2
  6. package/ext/html/node/$Image.ts +2 -2
  7. package/ext/html/node/$Input.ts +28 -4
  8. package/ext/html/node/$Label.ts +12 -3
  9. package/ext/html/node/$Media.ts +2 -2
  10. package/ext/html/node/$OptGroup.ts +2 -2
  11. package/ext/html/node/$Option.ts +2 -2
  12. package/ext/html/node/$Select.ts +2 -2
  13. package/ext/html/node/$TextArea.ts +2 -2
  14. package/ext/i18n/README.md +20 -0
  15. package/ext/i18n/src/index.ts +106 -12
  16. package/ext/i18n/src/structure/I18n.ts +12 -8
  17. package/ext/i18n/src/structure/I18nDictionary.ts +2 -2
  18. package/ext/i18n/src/structure/I18nTranslation.ts +35 -0
  19. package/ext/idb/README.md +127 -0
  20. package/ext/idb/package.json +13 -0
  21. package/ext/idb/src/core.ts +6 -0
  22. package/ext/idb/src/index.ts +17 -0
  23. package/ext/idb/src/lib/$IDBRequest.ts +8 -0
  24. package/ext/idb/src/structure/$IDB.ts +63 -0
  25. package/ext/idb/src/structure/$IDBCursor.ts +34 -0
  26. package/ext/idb/src/structure/$IDBIndex.ts +48 -0
  27. package/ext/idb/src/structure/$IDBStore.ts +103 -0
  28. package/ext/idb/src/structure/$IDBStoreBase.ts +30 -0
  29. package/ext/idb/src/structure/$IDBTransaction.ts +38 -0
  30. package/ext/idb/src/structure/builder/$IDBBuilder.ts +230 -0
  31. package/ext/idb/src/structure/builder/$IDBStoreBuilder.ts +100 -0
  32. package/ext/markdown/README.md +53 -0
  33. package/ext/markdown/package.json +15 -0
  34. package/ext/markdown/src/index.ts +3 -0
  35. package/ext/markdown/src/lib/type.ts +26 -0
  36. package/ext/markdown/src/lib/util.ts +21 -0
  37. package/ext/markdown/src/structure/Markdown.ts +54 -0
  38. package/ext/markdown/src/structure/MarkdownLexer.ts +111 -0
  39. package/ext/markdown/src/structure/MarkdownParser.ts +33 -0
  40. package/ext/markdown/src/syntax/alert.ts +46 -0
  41. package/ext/markdown/src/syntax/blockquote.ts +35 -0
  42. package/ext/markdown/src/syntax/bold.ts +11 -0
  43. package/ext/markdown/src/syntax/code.ts +11 -0
  44. package/ext/markdown/src/syntax/codeblock.ts +44 -0
  45. package/ext/markdown/src/syntax/heading.ts +14 -0
  46. package/ext/markdown/src/syntax/horizontalRule.ts +11 -0
  47. package/ext/markdown/src/syntax/image.ts +23 -0
  48. package/ext/markdown/src/syntax/italic.ts +11 -0
  49. package/ext/markdown/src/syntax/link.ts +46 -0
  50. package/ext/markdown/src/syntax/list.ts +121 -0
  51. package/ext/markdown/src/syntax/table.ts +67 -0
  52. package/ext/markdown/src/syntax/text.ts +19 -0
  53. package/ext/router/README.md +111 -17
  54. package/ext/router/package.json +10 -0
  55. package/ext/router/src/index.ts +69 -0
  56. package/ext/router/src/node/Page.ts +34 -0
  57. package/ext/router/src/node/Router.ts +191 -0
  58. package/ext/router/src/node/RouterAnchor.ts +24 -0
  59. package/ext/router/src/structure/PageBuilder.ts +24 -0
  60. package/ext/router/src/structure/Route.ts +105 -0
  61. package/ext/signal/README.md +93 -0
  62. package/ext/signal/package.json +9 -0
  63. package/ext/signal/src/index.ts +128 -0
  64. package/{src → ext/signal/src}/structure/Signal.ts +7 -11
  65. package/ext/ssr/index.ts +4 -4
  66. package/ext/ui/lib/VirtualScroll.ts +25 -0
  67. package/ext/ui/node/Accordian.ts +97 -0
  68. package/ext/ui/node/Form.ts +53 -0
  69. package/ext/ui/node/Grid.ts +0 -0
  70. package/ext/ui/node/Table.ts +43 -0
  71. package/ext/ui/node/Tabs.ts +114 -0
  72. package/ext/ui/node/Toast.ts +16 -0
  73. package/ext/ui/node/Waterfall.ts +72 -0
  74. package/ext/ui/package.json +11 -0
  75. package/package.json +9 -3
  76. package/src/core.ts +31 -59
  77. package/src/global.ts +12 -2
  78. package/src/index.ts +1 -2
  79. package/src/lib/assignProperties.ts +57 -0
  80. package/src/lib/native.ts +33 -11
  81. package/src/lib/sleep.ts +3 -1
  82. package/src/lib/toArray.ts +9 -0
  83. package/src/lib/trycatch.ts +17 -0
  84. package/src/lib/uppercase.ts +3 -0
  85. package/src/node/$Element.ts +7 -53
  86. package/src/node/$EventTarget.ts +45 -0
  87. package/src/node/$Node.ts +63 -55
  88. package/src/node/$Virtual.ts +65 -0
  89. package/src/node.ts +7 -6
  90. package/ext/i18n/src/node/I18nText.ts +0 -35
  91. package/ext/router/index.ts +0 -73
  92. package/ext/router/node/Page.ts +0 -27
  93. package/ext/router/node/Route.ts +0 -54
  94. package/ext/router/node/Router.ts +0 -149
  95. package/ext/router/node/RouterAnchor.ts +0 -8
  96. package/src/lib/assign.ts +0 -38
  97. package/src/lib/assignHelper.ts +0 -18
@@ -0,0 +1,69 @@
1
+ import type { Page } from "#node/Page";
2
+ import { Router } from "#node/Router";
3
+ import { PageBuilder } from "#structure/PageBuilder";
4
+ import { Route, type RouteBuilder, type RouteParams } from "#structure/Route";
5
+ import { _bind, _Object_assign, forEach } from "../../../src/lib/native";
6
+ import type { AnchorTarget } from "../../html/node/$Anchor";
7
+
8
+ declare module 'amateras/core' {
9
+ // export function $(nodeName: 'ra'): RouterAnchor;
10
+ export namespace $ {
11
+ export function route<Params extends RouteParams = []>(builder: (page: Page<Params>) => Page<Params>): PageBuilder<Params>;
12
+ export function open(url: string | URL | Nullish, target?: AnchorTarget): typeof Router;
13
+ export function replace(url: string | URL | Nullish): typeof Router;
14
+ export function back(): typeof Router;
15
+ export function forward(): typeof Router;
16
+ export interface $NodeMap {
17
+ 'router': typeof Router;
18
+ }
19
+ }
20
+ }
21
+
22
+ declare global {
23
+ interface GlobalEventHandlersEventMap {
24
+ 'routeopen': Event;
25
+ }
26
+ }
27
+
28
+ let prototype = {
29
+ route(this: { routes: Map<string, Route> }, path: string, builder: RouteBuilder, handle?: (route: Route) => Route) {
30
+ const route = new Route<any>(path, builder);
31
+ handle?.(route);
32
+ this.routes.set(path, route);
33
+ return this;
34
+ },
35
+
36
+ group(this: { routes: Map<string, Route> }, path: string, handle: (route: Route) => Route) {
37
+ this.routes.set(path, handle(new Route<any>(path)))
38
+ return this;
39
+ },
40
+
41
+ notFound(this: { routes: Map<string, Route> }, builder: RouteBuilder) {
42
+ this.routes.set('notfound', new Route('notfound', builder));
43
+ return this;
44
+ }
45
+ }
46
+
47
+ // assign methods
48
+ _Object_assign(Router.prototype, prototype)
49
+ _Object_assign(Route.prototype, prototype)
50
+ _Object_assign($, {
51
+ route: (builder: (page: Page) => Page) => new PageBuilder(builder),
52
+ open: _bind(Router.open, Router),
53
+ replace: _bind(Router.replace, Router),
54
+ back: _bind(Router.back, Router),
55
+ forward: _bind(Router.forward, Router)
56
+ })
57
+ // assign node
58
+ $.assign(['router', Router])
59
+ // use style
60
+ forEach([
61
+ `router{display:block}`,
62
+ `page{display:block}`
63
+ ], $.style);
64
+
65
+ export * from '#node/Page';
66
+ export * from '#node/Router';
67
+ export * from '#node/RouterAnchor';
68
+ export * from '#structure/PageBuilder';
69
+ export * from '#structure/Route';
@@ -0,0 +1,34 @@
1
+ import type { RouteParams } from "#structure/Route";
2
+ import { $HTMLElement } from "amateras/node/$HTMLElement";
3
+ import { _null } from "../../../../src/lib/native";
4
+ import type { Router } from "./Router";
5
+ import { chain } from "../../../../src/lib/chain";
6
+
7
+ export class Page<Params extends RouteParams = []> extends $HTMLElement {
8
+ params: PageParamsResolver<Params>;
9
+ router: null | Router = _null
10
+ #pageTitle: string | null = _null;
11
+ built = false;
12
+ constructor(params: PageParamsResolver<Params>) {
13
+ super('page');
14
+ this.params = params;
15
+ }
16
+
17
+ pageTitle(): string | null;
18
+ pageTitle(title: string | null): this;
19
+ pageTitle(title?: string | null) {
20
+ return chain(this, arguments, () => this.#pageTitle, title, title => this.#pageTitle = title)
21
+ }
22
+ }
23
+
24
+ export type PageParams = { [key: string]: string }
25
+ export type PageParamsResolver<Params extends string[]> =
26
+ Prettify<
27
+ Params extends [`${infer String}`, ...infer Rest]
28
+ ? Rest extends string[]
29
+ ? String extends `${infer Key}?`
30
+ ? { [key in Key]?: string } & PageParamsResolver<Rest>
31
+ : { [key in String]: string } & PageParamsResolver<Rest>
32
+ : never
33
+ : {}
34
+ >
@@ -0,0 +1,191 @@
1
+ import { $HTMLElement } from "amateras/node/$HTMLElement";
2
+ import { Route, type RouteBuilder, type RoutePath, type RouteParamsResolver, type RouteParams, type RouteParamsStrings, type AsyncPageBuilder } from "../structure/Route";
3
+ import { _document } from "amateras/lib/env";
4
+ import { _instanceof, startsWith, _JSON_parse, forEach, _Object_entries, _JSON_stringify, _Object_assign, isFunction, _null } from "../../../../src/lib/native";
5
+ import type { AnchorTarget } from "../../../html/node/$Anchor";
6
+ import { Page, type PageParams } from "./Page";
7
+ import type { PageBuilder, PageBuilderFunction } from "#structure/PageBuilder";
8
+ // history index
9
+ let index = 0;
10
+ const _addEventListener = addEventListener;
11
+ const _location = location;
12
+ const {origin} = _location;
13
+ const _history = history;
14
+ const _sessionStorage = sessionStorage;
15
+ const documentElement = _document.documentElement;
16
+ const [PUSH, REPLACE] = [1, 2] as const;
17
+ const [FORWARD, BACK] = ['forward', 'back'] as const;
18
+ const scrollStorageKey = '__scroll__';
19
+ /** convert path string to URL object */
20
+ const toURL = (path: string | URL) =>
21
+ _instanceof(path, URL) ? path : startsWith(path, 'http') ? new URL(path) : new URL(startsWith(path, origin) ? path : origin + path);
22
+
23
+ type ScrollData = {[key: number]: {x: number, y: number}};
24
+ const scrollRecord = (e?: Event) => {
25
+ const data = _JSON_parse(_sessionStorage.getItem(scrollStorageKey) ?? '{}') as ScrollData;
26
+ data[index] = { x: documentElement.scrollLeft, y: documentElement.scrollTop };
27
+ // e is Event when called from scroll or beforeload
28
+ if (!e) forEach(_Object_entries(data), ([i]) => +i > index && delete data[+i])
29
+ _sessionStorage.setItem(scrollStorageKey, _JSON_stringify(data));
30
+ }
31
+ /** handle history state with push and replace state. */
32
+ const historyHandler = async (path: string | URL | Nullish, mode: 1 | 2, target?: AnchorTarget) => {
33
+ if (!path) return;
34
+ const url = toURL(path);
35
+ if (url.href === _location.href) return;
36
+ if (target && target !== '_self') return open(url, target);
37
+ if (url.origin !== origin) return open(url, target);
38
+ scrollRecord();
39
+ if (mode === PUSH) index += 1;
40
+ Router.direction = FORWARD;
41
+ _history[mode === PUSH ? 'pushState' : 'replaceState']({index}, '' , url);
42
+ forEach(Router.routers, router => router.resolve(path))
43
+ }
44
+ // disable browser scroll restoration
45
+ _history.scrollRestoration = 'manual';
46
+
47
+ export class Router extends $HTMLElement {
48
+ static direction: 'back' | 'forward' = FORWARD;
49
+ static routers = new Set<Router>();
50
+ routes = new Map<RoutePath, Route>();
51
+ pages = new Map<string, Page>();
52
+ constructor(page?: Page) {
53
+ super('router');
54
+ if (page) page.router = this;
55
+ else Router.routers.add(this);
56
+ }
57
+
58
+ static open(path: string | URL | Nullish, target?: AnchorTarget) {
59
+ historyHandler(path, PUSH, target);
60
+ return this;
61
+ }
62
+
63
+ static back() {
64
+ _history.back();
65
+ return this;
66
+ }
67
+
68
+ static forward() {
69
+ _history.forward();
70
+ return this;
71
+ }
72
+
73
+ static replace(path: string | URL | Nullish) {
74
+ historyHandler(path, REPLACE);
75
+ return this;
76
+ }
77
+
78
+ static get scroll(): ScrollData[number] {
79
+ return _JSON_parse(_sessionStorage.getItem(scrollStorageKey) ?? '{}')[index] ?? {x: 0, y: 0}
80
+ }
81
+
82
+ listen() {
83
+ const resolve = () => {
84
+ const stateIndex = _history.state?.index ?? 0;
85
+ if (index > stateIndex) Router.direction = BACK;
86
+ if (index < stateIndex) Router.direction = FORWARD;
87
+ index = stateIndex;
88
+ this.resolve(_location.href);
89
+ }
90
+ _addEventListener('popstate', resolve);
91
+ _addEventListener('beforeunload', scrollRecord);
92
+ _addEventListener('scroll', scrollRecord, false);
93
+ resolve();
94
+ return this;
95
+ }
96
+
97
+ async resolve(path: string | URL): Promise<this> {
98
+ const {pathname, href} = toURL(path);
99
+ const split = (p: string) => p.replaceAll(/\/+/g, '/').replace(/^\//, '').split('/').map(path => `/${path}`);
100
+ type RouteData = { route: Route, params: PageParams, pathId: string }
101
+ const searchRoute = (routes: typeof this.routes, targetPath: string): RouteData[] => {
102
+ let targetPathSplit = split(targetPath);
103
+ if (!routes.size) return [];
104
+ // check each route
105
+ for (const [_, route] of routes) {
106
+ // check each path pass
107
+ routePathLoop: for (const [path, paramsHandle] of route.paths) {
108
+ let routePathSplit = split(path);
109
+ let targetPathNodePosition = 0;
110
+ let params: { [key: string]: string } = isFunction(paramsHandle) ? paramsHandle() : paramsHandle ?? {};
111
+ let pathId = '';
112
+ // check each path node
113
+ pathNodeLoop: for (let i = 0; i < routePathSplit.length; i++) {
114
+ // reset target path node position
115
+ targetPathNodePosition = i;
116
+ const routeNode = routePathSplit[i];
117
+ const targetNode = targetPathSplit[i];
118
+ // path node undefined, break path loop
119
+ if (!routeNode || !targetNode) continue routePathLoop;
120
+ // path node is params node
121
+ if (routeNode.includes(':')) {
122
+ // target not matched
123
+ if (targetNode === '/') continue routePathLoop;
124
+ const [prefix, paramName] = routeNode.split(':') as [string, string];
125
+ if (!startsWith(targetNode, prefix)) continue routePathLoop;
126
+ params[paramName] = targetNode.replace(`${prefix}`, '');
127
+ pathId += targetNode;
128
+ continue pathNodeLoop;
129
+ }
130
+ // path node not matched, next path
131
+ if (routeNode !== targetNode) continue routePathLoop;
132
+ pathId += targetNode;
133
+ }
134
+ // target path node longer than route, next route
135
+ if (targetPathSplit[targetPathNodePosition + 1] && !route.routes.size) continue routePathLoop;
136
+ // all path node passed, route found
137
+ return [{route, params, pathId}, ...searchRoute(route.routes, targetPathSplit.slice(targetPathNodePosition + 1).join('/'))]
138
+ }
139
+ }
140
+ // no route passed
141
+ const notfound = routes.get('notfound');
142
+ if (notfound) return [{route: notfound, params: {}, pathId: 'notfound'}]
143
+ return [];
144
+ }
145
+ const routes = searchRoute(this.routes, pathname);
146
+ let prevRouter: Router | null = this;
147
+ await forEach(routes, async ({route, params, pathId}) => {
148
+ // skip route group
149
+ const builderResolver = route.builder;
150
+ if (!builderResolver) return;
151
+ // get page from cache or create new page
152
+ const page = route.pages.get(pathId) ?? new Page(params);
153
+ // resolve builder
154
+ if (!page.built) await builderResolver.build(page);
155
+ page.built = true;
156
+ // set title
157
+ _document && (_document.title = page.pageTitle() ?? _document.title);
158
+ // check location is still same, page parent is not router before insert page
159
+ if (href === _location.href && page.parentNode !== prevRouter?.node) prevRouter?.content(page);
160
+ // set cache
161
+ route.pages.set(pathId, page);
162
+ prevRouter = page.router;
163
+ })
164
+ // handle scroll restoration
165
+ let { x, y } = Router.scroll ?? {x: 0, y: 0};
166
+ scrollTo(x, y);
167
+ // event
168
+ this.dispatchEvent(new Event('routeopen', {bubbles: true}));
169
+ return this;
170
+ }
171
+ }
172
+
173
+ export interface Router {
174
+ route<
175
+ P extends RoutePath,
176
+ B extends PageBuilder
177
+ >(path: P, builder: B, handle?: (route: Route<P, B['params']>) => Route<P>): Router
178
+ route<
179
+ K extends RoutePath,
180
+ P extends RouteParamsStrings<K>,
181
+ F extends PageBuilderFunction<P>
182
+ >(path: K, builder: F, handle?: (route: Route<K, P>) => Route<K, P>): this
183
+ route<
184
+ K extends RoutePath,
185
+ P extends RouteParamsStrings<K>,
186
+ F extends AsyncPageBuilder<P>
187
+ >(path: K, builder: F, handle?: (route: Route<K, P>) => Route<K, P>): this
188
+ route<P extends RoutePath>(path: P, builder: RouteBuilder<RouteParamsResolver<P>>, handle?: (route: Route<P>) => Route<P>): Router
189
+ group<P extends RoutePath>(path: P, handle: <R extends Route<P>>(route: R) => R): this;
190
+ notFound(builder: RouteBuilder<RouteParamsResolver<RoutePath>>): this;
191
+ }
@@ -0,0 +1,24 @@
1
+ import { $Anchor } from "../../../html/node/$Anchor";
2
+
3
+ export class RouterAnchor extends $Anchor {
4
+ constructor() {
5
+ super();
6
+ this.on('click', e => {
7
+ if (e.shiftKey || e.ctrlKey) return;
8
+ e.preventDefault();
9
+ this.target() === '_replace'
10
+ ? $.replace(this.href())
11
+ : $.open(this.href(), this.target())
12
+ })
13
+ }
14
+ }
15
+
16
+ declare module 'amateras/core' {
17
+ export namespace $ {
18
+ export interface $NodeMap {
19
+ 'ra': typeof RouterAnchor;
20
+ }
21
+ }
22
+ }
23
+
24
+ $.assign(['ra', RouterAnchor]);
@@ -0,0 +1,24 @@
1
+ import { Page, type PageParams } from "#node/Page";
2
+ import { _instanceof, _Promise, isFunction } from "../../../../src/lib/native";
3
+ import type { $NodeContentResolver } from "../../../../src/node/$Node";
4
+ import type { AsyncPageBuilder, RouteParams } from "./Route";
5
+
6
+ export class PageBuilder<Params extends RouteParams = any> {
7
+ params!: Params
8
+ #builder: PageBuilderFunction<Params> | AsyncPageBuilder<Params>;
9
+ constructor(builder: PageBuilderFunction<Params>) {
10
+ this.#builder = builder;
11
+ }
12
+
13
+ async build(page: Page<Params>): Promise<Page<Params>> {
14
+ const resolver = this.#builder(page)
15
+ const handle = async (result: any) => {
16
+ if (_instanceof(result, Page)) return result;
17
+ else if (result[Symbol.toStringTag] === 'Module') return await result.default.build(page);
18
+ else return page.content(result);
19
+ }
20
+ return handle(_instanceof(resolver, _Promise) ? await resolver : resolver);
21
+ }
22
+ }
23
+
24
+ export type PageBuilderFunction<Params extends RouteParams> = (page: Page<Params>) => OrPromise<Page<Params> | $NodeContentResolver<Page<Params>>>
@@ -0,0 +1,105 @@
1
+ import type { Page } from "#node/Page";
2
+ import { _instanceof, _null, isUndefined } from "../../../../src/lib/native";
3
+ import { PageBuilder, type PageBuilderFunction } from "./PageBuilder";
4
+
5
+ export class Route<Path extends RoutePath = RoutePath, Params extends RouteParams = []> {
6
+ readonly routes = new Map<RoutePath, Route>();
7
+ readonly path: Path;
8
+ readonly builder: PageBuilder<any> | undefined;
9
+ readonly paths = new Map<string, RouteAliasParams<RoutePath, Params> | null>()
10
+ readonly pages = new Map<string, Page>();
11
+ redirectURL: string | null = null
12
+ constructor(path: Path, builder?: RouteBuilder<Params>) {
13
+ this.path = path;
14
+ this.paths.set(path, _null);
15
+ this.builder = _instanceof(builder, PageBuilder<any>) || isUndefined(builder) ? builder : new PageBuilder(builder as any);
16
+ }
17
+
18
+
19
+ alias<K extends string, P extends RouteAliasParams<K, Params>>(path: K, ...params: RequireKeys<P> extends never ? [] : [Prettify<P>]): this
20
+ alias(path: string, params: RouteAliasParams<RoutePath, Params> | null = _null) {
21
+ this.paths.set(path, params);
22
+ return this;
23
+ }
24
+ }
25
+
26
+ export interface Route<Path extends RoutePath = RoutePath, Params extends RouteParams = []> {
27
+ route<
28
+ P extends RoutePath,
29
+ B extends PageBuilder
30
+ >(path: P, builder: B, handle?: (route: Route<`${Path}${P}`, B['params']>) => Route<`${Path}${P}`, B['params']>): this
31
+ route<
32
+ K extends RoutePath,
33
+ P extends [...Params, ...RouteParamsStrings<K>],
34
+ F extends PageBuilderFunction<P>
35
+ >(path: K, builder: F, handle?: (route: Route<`${Path}${K}`, P>) => Route<`${Path}${K}`, P>): this
36
+ route<
37
+ K extends RoutePath,
38
+ P extends [...Params, ...RouteParamsStrings<K>],
39
+ F extends AsyncPageBuilder<P>
40
+ >(path: K, builder: F, handle?: (route: Route<`${Path}${K}`, P>) => Route<`${Path}${K}`, P>): this
41
+ route<P extends RoutePath>(path: P, builder: RouteBuilder<RouteParamsResolver<`${Path}${P}`>>, handle?: <R extends Route<`${Path}${P}`>>(route: R) => R): this;
42
+ group<P extends RoutePath>(path: P, handle: <R extends Route<`${Path}${P}`>>(route: R) => R): this;
43
+ notFound(builder: RouteBuilder<RouteParamsResolver<`${Path}`>>): this;
44
+ }
45
+
46
+ export type RouteBuilder<Params extends RouteParams = []> = PageBuilder<Params | `${string}?`[]> | AsyncPageBuilder<Params | `${string}?`[]> | PageBuilderFunction<Params>;
47
+ export type RoutePath = string;
48
+ export type RouteParams = string[]
49
+ export type AsyncPageBuilder<Params extends RouteParams = any> = () => Promise<{default: PageBuilder<Params>}>
50
+
51
+ export type RouteParamsResolver<Path extends RoutePath> = RouteParamsOptional<RouteParamsStrings<Path>>
52
+
53
+ /** Convert route path to literals array */
54
+ export type RouteParamsStrings<Path extends string> =
55
+ Path extends `${infer Segment}/${infer Rest}`
56
+ ? Segment extends `${string}:${infer Param}`
57
+ ? [Param, ...RouteParamsStrings<Rest>]
58
+ : [...RouteParamsStrings<Rest>]
59
+ : Path extends `${string}:${infer Param}?${infer Query}`
60
+ ? [Param]
61
+ : Path extends `${string}:${infer Param}`
62
+ ? [Param]
63
+ : []
64
+ /** Convert route path to object structure */
65
+ export type RouteParamsConfig<Path extends string> =
66
+ Path extends `${infer Segment}/${infer Rest}`
67
+ ? Segment extends `${string}:${infer Param}`
68
+ ? { [key in Param]: string } & RouteParamsConfig<Rest>
69
+ : RouteParamsConfig<Rest>
70
+ : Path extends `${string}:${infer Param}?${infer Query}`
71
+ ? { [key in Param]: string }
72
+ : Path extends `${string}:${infer Param}`
73
+ ? { [key in Param]: string }
74
+ : {}
75
+ /** Convert literals array to optional literals arrays */
76
+ export type RouteParamsOptional<Params extends string[]> =
77
+ Params extends [infer A, ...infer Rest]
78
+ ? Rest extends string[]
79
+ ? A extends string
80
+ ? [A | `${A}?`, ...RouteParamsOptional<Rest>] | RouteParamsOptional<Rest>
81
+ : never
82
+ : never
83
+ : []
84
+
85
+ export type RouteAliasPath<Path extends string, RoutePath extends string> = RouteParamsStrings<RoutePath> extends RouteParamsStrings<Path> ? Route<RoutePath> : never
86
+
87
+ type RouteParamsConfigByArrayString<RouteParams extends string[]> =
88
+ RouteParams extends [`${infer A}`, ...infer Rest]
89
+ ? Rest extends string[]
90
+ ? A extends `${infer P}?`
91
+ ? { [key in P]?: string } & RouteParamsConfigByArrayString<Rest>
92
+ : { [key in A]: string } & RouteParamsConfigByArrayString<Rest>
93
+ : never
94
+ : {}
95
+
96
+
97
+ type RouteAliasParams<Path extends string, Params extends RouteParams> = Omit<RouteParamsConfigByArrayString<Params>, keyof RouteParamsConfig<Path>>
98
+
99
+ type a<T> = T[keyof T] extends (string | undefined) ? true : false
100
+
101
+ type b = RequireKeys<{t: string, a?: string, b?: string}> & {}
102
+
103
+ type RequireKeys<T> = {
104
+ [K in keyof T]: {} extends Pick<T, K> ? never : K
105
+ }[keyof T];
@@ -0,0 +1,93 @@
1
+ # amateras/signal
2
+
3
+ ## Usage
4
+
5
+ ```ts
6
+ import 'amateras';
7
+ import 'amateras/signal';
8
+
9
+ // define a signal with value 0
10
+ const count$ = $.signal(0);
11
+
12
+ // this variable will be auto recalculate when count$ changes
13
+ const doubleCount$ = $.compute(() => count$() * 2);
14
+
15
+ // the console message will fired when count$ changes
16
+ $.effect(() => console.log( count$() ))
17
+
18
+ $(document.body).content([
19
+ // Display Counts
20
+ $('p').content( $`Counts: ${count$}` ),
21
+
22
+ // Display Double Counts
23
+ $('p').content( $`Double Counts: ${doubleCount$}` ),
24
+
25
+ // Create a button that make counts plus 1 on click
26
+ $('button').content('Add Count').on('click', () => count$.set(value => value + 1))
27
+ ])
28
+ ```
29
+
30
+ ## Read and Write
31
+
32
+ ```ts
33
+ const number$ = $.signal(0);
34
+ const string$ = $.singal('');
35
+ const boolean$ = $.signal(false);
36
+ const object$ = $.signal({ number: 1 });
37
+
38
+ // write value
39
+ number$.set(42);
40
+ string$.set('New Content');
41
+ boolean$.set(true);
42
+ object$.set({ number: 42 });
43
+
44
+ // read value
45
+ number$(); // 42
46
+ string$(); // 'New Content'
47
+ boolean$(); // true
48
+ object$(); // { number: 42 }
49
+ ```
50
+
51
+ ## Use in attribute methods
52
+
53
+ ```ts
54
+ const src$ = $.signal('/image-1.png');
55
+
56
+ $(document.body).content([
57
+ // you can set signal variable in attribute
58
+ $('img').src( src$ ),
59
+
60
+ $('button').content('Change Image').on('click', () => src$.set('/image-2.png'))
61
+ ])
62
+ ```
63
+
64
+ ## Reactive object
65
+
66
+ ```ts
67
+ const user$ = $.signal({
68
+ name: 'Amateras',
69
+ age: 16,
70
+ avatar: {
71
+ url: '/amateras/avatar.png',
72
+ size: '350x350'
73
+ }
74
+ })
75
+
76
+ $(document.body).content([
77
+ // Display name and age
78
+ $('h1').content( $`${user$.name$} (${user$.age$})` ),
79
+ // Display avatar image
80
+ $('img').src( user$.avatar$.url$ ),
81
+ // Change the user$ when button is clicked
82
+ $('button')
83
+ .content('Change User')
84
+ .on('click', () => user$.set({
85
+ name: 'Tsukimi',
86
+ age: 10,
87
+ avatar: {
88
+ url: '/tsukimi/avatar.png',
89
+ size: '350x350'
90
+ }
91
+ }))
92
+ ])
93
+ ```
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@amateras/signal",
3
+ "peerDependencies": {
4
+ "amateras": "../../"
5
+ },
6
+ "imports": {
7
+ "#structure/*": "./src/structure/*.ts"
8
+ }
9
+ }
@@ -0,0 +1,128 @@
1
+ import { Signal } from "#structure/Signal";
2
+ import { _document } from "amateras/lib/env";
3
+ import { _instanceof, isObject, _JSON_stringify, _Object_assign, forEach, _null, _Object_defineProperty, _Object_entries, isNull, isFunction } from "amateras/lib/native";
4
+ import { $Node, $Text } from "amateras/node/$Node";
5
+
6
+ // handle $Node content process
7
+ $Node.processors.add((_, content) => {
8
+ const signal = (content as SignalFunction<any>)?.signal;
9
+ if (_instanceof(signal, Signal)) {
10
+ const resolver = (content as SignalFunction<any>)();
11
+ if (_instanceof(resolver, $Node)) {
12
+ // handler signal $Node result
13
+ let node = resolver;
14
+ const set = (value: any) => {
15
+ node.replace(value);
16
+ node = value;
17
+ }
18
+ signal.subscribe(set);
19
+ return [resolver];
20
+ } else {
21
+ // handler signal other type result
22
+ const $text = new $Text()
23
+ const set = (value: any) => $text.textContent(isObject(value) ? _JSON_stringify(value) : value);
24
+ signal.subscribe(set);
25
+ set(resolver);
26
+ return [$text];
27
+ }
28
+ }
29
+ })
30
+
31
+ // handle $Node native method setter
32
+ $Node.setters.add((value, set) => {
33
+ const signal = value?.signal
34
+ if (isFunction(value) && _instanceof(signal, Signal)) {
35
+ signal.subscribe(set);
36
+ return value();
37
+ }
38
+ })
39
+
40
+ declare module 'amateras/core' {
41
+ export namespace $ {
42
+ export function signal<T>(value: T): SignalFunction<T>;
43
+ export function compute<T>(process: () => T): ComputeFunction<T>;
44
+ export function effect(process: () => void): void;
45
+ export interface $NodeContentMap {
46
+ signalFn: SignalFunction<any>;
47
+ computeFn: ComputeFunction<any>;
48
+ }
49
+ export interface $NodeParameterMap<T> {
50
+ signalFn: SignalFunction<T>
51
+ }
52
+ }
53
+ }
54
+
55
+ type SignalObject<T> = T extends Array<any> ? {} : T extends object ? { [key in keyof T as `${string & key}$`]: SignalFunction<T[key]> } : {};
56
+ export type SignalFunction<T> = {
57
+ signal: Signal<T>,
58
+ set: (newValue: T | ((oldValue: T) => T)) => SignalFunction<T>,
59
+ value: () => T;
60
+ } & (() => T) & SignalObject<T>;
61
+ export type ComputeFunction<T> = ({(): T}) & { signal: Signal<T> };
62
+
63
+ const signalComputeListeners = new Set<(signal: Signal<any>) => void>();
64
+ const signalEffectListeners = new Set<(signal: Signal<any>) => void>();
65
+ const signalFnMap = new Map<any, SignalFunction<any> | ComputeFunction<any>>();
66
+
67
+ // experiment feature
68
+ const nestedComputeFn = (value: any, parentSignalFn: SignalFunction<any> | ComputeFunction<any>) => {
69
+ if (isObject(value) && !isNull(value)) {
70
+ forEach(_Object_entries(value), ([key, val]) => {
71
+ const cachedFn = signalFnMap.get(val);
72
+ const val$ = cachedFn ?? $.compute(() => parentSignalFn()[key]);
73
+ if (!cachedFn && isObject(val)) {
74
+ signalFnMap.set(val, val$);
75
+ nestedComputeFn(val, val$)
76
+ }
77
+ _Object_defineProperty(parentSignalFn, `${key}$`, {value: val$});
78
+ })
79
+ }
80
+ }
81
+
82
+ _Object_assign($, {
83
+ // signal function
84
+ signal<T>(value: T): SignalFunction<T> {
85
+ const signal = new Signal<T>(value);
86
+ const signalFn = function () {
87
+ forEach([...signalComputeListeners, ...signalEffectListeners], fn => fn(signal));
88
+ return signal.value();
89
+ } as SignalFunction<T>
90
+ nestedComputeFn(value, signalFn);
91
+ _Object_assign(signalFn, {
92
+ signal,
93
+ set: (newValue: T) => (signal.value(newValue), signalFn),
94
+ value: () => signal.value()
95
+ })
96
+ return signalFn
97
+ },
98
+
99
+ // compute function
100
+ compute<T>(process: () => T): ComputeFunction<T> {
101
+ let subscribed = false;
102
+ const signalFn: SignalFunction<any> = $.signal(_null);
103
+ const computeFn = () => {
104
+ if (!subscribed) return signalFn.set(subscribe()).value();
105
+ else return signalFn.set(process()).value();
106
+ }
107
+ const subscribe = () => {
108
+ const signalHandler = (signal: Signal<any>) =>
109
+ signal.subscribe(() => signalFn.set(process()))
110
+ signalComputeListeners.add(signalHandler);
111
+ const result = process();
112
+ signalComputeListeners.delete(signalHandler);
113
+ subscribed = true;
114
+ return result;
115
+ }
116
+ _Object_assign(computeFn, { signal: signalFn.signal });
117
+ return computeFn as ComputeFunction<T>
118
+ },
119
+
120
+ // effect
121
+ effect(process: () => void) {
122
+ const signalHandler = (signal: Signal<any>) =>
123
+ signal.subscribe(process);
124
+ signalEffectListeners.add(signalHandler);
125
+ process();
126
+ signalEffectListeners.delete(signalHandler);
127
+ }
128
+ })