amateras 0.4.2 → 0.5.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.
- package/README.md +7 -5
- package/ext/html/node/$Anchor.ts +1 -1
- package/ext/html/node/$Input.ts +26 -2
- package/ext/html/node/$Label.ts +10 -1
- package/ext/i18n/src/structure/I18nDictionary.ts +2 -2
- package/ext/idb/README.md +127 -0
- package/ext/idb/package.json +13 -0
- package/ext/idb/src/core.ts +6 -0
- package/ext/idb/src/index.ts +17 -0
- package/ext/idb/src/lib/$IDBRequest.ts +8 -0
- package/ext/idb/src/structure/$IDB.ts +63 -0
- package/ext/idb/src/structure/$IDBCursor.ts +34 -0
- package/ext/idb/src/structure/$IDBIndex.ts +48 -0
- package/ext/idb/src/structure/$IDBStore.ts +103 -0
- package/ext/idb/src/structure/$IDBStoreBase.ts +30 -0
- package/ext/idb/src/structure/$IDBTransaction.ts +38 -0
- package/ext/idb/src/structure/builder/$IDBBuilder.ts +230 -0
- package/ext/idb/src/structure/builder/$IDBStoreBuilder.ts +100 -0
- package/ext/markdown/index.ts +121 -0
- package/ext/markdown/package.json +8 -0
- package/ext/router/index.ts +1 -1
- package/ext/router/node/Route.ts +2 -2
- package/ext/router/node/RouterAnchor.ts +6 -1
- package/package.json +5 -2
- package/src/core.ts +5 -3
- package/src/global.ts +3 -0
- package/src/lib/native.ts +9 -4
- package/src/lib/sleep.ts +3 -1
- package/src/lib/toArray.ts +9 -0
- package/src/lib/trycatch.ts +17 -0
- package/src/node/$Element.ts +2 -14
- package/src/node/$Node.ts +19 -6
- package/src/structure/Signal.ts +2 -2
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { $IDB, type $IDBConfig } from "#structure/$IDB";
|
|
2
|
+
import { _Array_from, _instanceof, _JSON_stringify, _null, _Object_assign, _Object_fromEntries, _Promise, forEach, isFunction } from "amateras/lib/native";
|
|
3
|
+
import { $IDBStoreBuilder } from "./$IDBStoreBuilder";
|
|
4
|
+
import type { $IDBIndexConfig } from "#structure/$IDBIndex";
|
|
5
|
+
import type { $IDBStoreConfig } from "#structure/$IDBStore";
|
|
6
|
+
import { trycatch } from "amateras/lib/trycatch";
|
|
7
|
+
// optimizer variables
|
|
8
|
+
const objectStoreNames = 'objectStoreNames';
|
|
9
|
+
const deleteObjectStore = 'deleteObjectStore';
|
|
10
|
+
const createObjectStore = 'createObjectStore';
|
|
11
|
+
const _indexedDB = indexedDB;
|
|
12
|
+
const onupgradeneeded = 'onupgradeneeded';
|
|
13
|
+
const onsuccess = 'onsuccess';
|
|
14
|
+
|
|
15
|
+
export interface $IDBBuilder {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly version: number;
|
|
18
|
+
}
|
|
19
|
+
export class $IDBBuilder<Config extends $IDBConfig = { name: string, stores: {}, version: number }> {
|
|
20
|
+
#deleteUnused = false;
|
|
21
|
+
storeMap = new Map<string, $IDBStoreBuilder>();
|
|
22
|
+
#devMode: boolean = false;
|
|
23
|
+
constructor(config: Config) {
|
|
24
|
+
_Object_assign(this, config);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* This option helping developer to debug when initializing.
|
|
29
|
+
* @param dev - Enable dev mode
|
|
30
|
+
*/
|
|
31
|
+
devMode(dev: boolean) {
|
|
32
|
+
this.#devMode = dev;
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* If set to true, unused store will be deleted when initialize.
|
|
38
|
+
* @param enable - Enable delete unused stores
|
|
39
|
+
*/
|
|
40
|
+
deleteUnused(enable: boolean) {
|
|
41
|
+
this.#deleteUnused = enable;
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Add new store to builder.
|
|
47
|
+
* @param name - Store name
|
|
48
|
+
* @param builder - Store builder or builder function
|
|
49
|
+
*/
|
|
50
|
+
store<N extends string, B extends $IDBStoreBuilderFunction>(name: N, builder: B): $IDBBuilder<Prettify<Config & { stores: Config['stores'] & Prettify<Record<N, ReturnType<B>['config']>> }>>
|
|
51
|
+
store<N extends string, B extends $IDBStoreBuilder<any>>(name: N, builder: B): $IDBBuilder<Prettify<Config & { stores: Config['stores'] & Prettify<Record<N, B['config']>> }>>
|
|
52
|
+
store(name: string, builder: $IDBStoreBuilderFunction | $IDBStoreBuilder)
|
|
53
|
+
{
|
|
54
|
+
this.storeMap.set(name, isFunction(builder) ? builder(new $IDBStoreBuilder({autoIncrement: false, keyPath: null, indexes: {}, name, schema: null})) : builder);
|
|
55
|
+
return this as any;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Open IDB and initialize, create new IDB if the name of IDB is not exists, or perform the upgrade if version number change.
|
|
60
|
+
*/
|
|
61
|
+
async open(): Promise<$IDB<Config>> {
|
|
62
|
+
return new _Promise<$IDB>((resolve, reject) => {
|
|
63
|
+
const {version: dbVersion, name: dbName, storeMap} = this;
|
|
64
|
+
const initDBRequest = _indexedDB.open(dbName);
|
|
65
|
+
const createStoresMap = new Map<string, $IDBStoreBuilder<$IDBStoreConfig>>();
|
|
66
|
+
const createIndexMap = new Map<$IDBStoreBuilder, Map<string, $IDBIndexConfig>>();
|
|
67
|
+
const upgradeStoreMap = new Map<string, $IDBStoreBuilder<$IDBStoreConfig>>();
|
|
68
|
+
const cachedObjectMap = new Map<string, {key: any, value: any}[]>();
|
|
69
|
+
const unusedStoreNameList: string[] = [];
|
|
70
|
+
const debug = (message: string) => this.#devMode && console.debug(`[$IDBBuilder (${dbName})]`, message);
|
|
71
|
+
const storesObject: $IDBConfig['stores'] = _Object_fromEntries(_Array_from(storeMap).map(([name, {config: {keyPath, autoIncrement}, indexes}]) => [
|
|
72
|
+
name,
|
|
73
|
+
{
|
|
74
|
+
autoIncrement, keyPath, name,
|
|
75
|
+
schema: _null,
|
|
76
|
+
indexes: _Object_fromEntries(_Array_from(indexes).map(([name, {keyPath, multiEntry, unique}]) => [
|
|
77
|
+
name,
|
|
78
|
+
{ keyPath, multiEntry, unique } as $IDBIndexConfig
|
|
79
|
+
]))
|
|
80
|
+
} as $IDBStoreConfig
|
|
81
|
+
]))
|
|
82
|
+
const idbConfig = { version: dbVersion, name: dbName, stores: storesObject };
|
|
83
|
+
/** IndexedDB initial create function */
|
|
84
|
+
const initialCreateDB = () => {
|
|
85
|
+
debug(`No IDB detected, create IDB`);
|
|
86
|
+
const {transaction, result: idb} = initDBRequest;
|
|
87
|
+
forEach(storeMap, (storeBuilder, name) => {
|
|
88
|
+
createStoresMap.set(name, storeBuilder);
|
|
89
|
+
createIndexMap.set(storeBuilder, new Map(storeBuilder.indexes))
|
|
90
|
+
})
|
|
91
|
+
if (idb.version === dbVersion) upgradeStore(initDBRequest);
|
|
92
|
+
else transaction!.oncomplete = _ => {
|
|
93
|
+
const upgradeDBRequest = indexedDB.open(dbName, dbVersion);
|
|
94
|
+
upgradeDBRequest.onupgradeneeded = _ => upgradeStore(upgradeDBRequest);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** IndexedDB initial open function */
|
|
98
|
+
const initialOpenDB = async () => {
|
|
99
|
+
debug(`IDB Detected`);
|
|
100
|
+
const idb = initDBRequest.result;
|
|
101
|
+
const $idb = new $IDB(idb, idbConfig);
|
|
102
|
+
const transaction = idb[objectStoreNames].length ? idb.transaction(_Array_from(idb[objectStoreNames]), 'readonly') : null;
|
|
103
|
+
const noUpgrade = () => {
|
|
104
|
+
debug(`No Upgrade`);
|
|
105
|
+
resolve($idb);
|
|
106
|
+
}
|
|
107
|
+
if (idb.version === dbVersion) return noUpgrade();
|
|
108
|
+
// get unused stores
|
|
109
|
+
transaction && forEach(_Array_from(transaction[objectStoreNames]), name => storeMap.has(name) && unusedStoreNameList.push(name))
|
|
110
|
+
// check store config matches
|
|
111
|
+
forEach(storeMap, (storeBuilder, storeName) => {
|
|
112
|
+
const {keyPath, autoIncrement} = storeBuilder.config;
|
|
113
|
+
const indexMap = new Map();
|
|
114
|
+
const checkIndexes = () =>
|
|
115
|
+
forEach(storeBuilder.indexes, (indexBuilder, indexName) => {
|
|
116
|
+
const [index] = trycatch(() => store?.index(indexName));
|
|
117
|
+
const CONFIG_CHANGED = _JSON_stringify(indexBuilder.keyPath) !== _JSON_stringify(index?.keyPath)
|
|
118
|
+
|| !!indexBuilder.multiEntry !== index?.multiEntry
|
|
119
|
+
|| !!indexBuilder.unique !== index?.unique;
|
|
120
|
+
if (!index || CONFIG_CHANGED) {
|
|
121
|
+
indexMap.set(indexName, indexBuilder);
|
|
122
|
+
createIndexMap.set(storeBuilder, indexMap);
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
// get store from idb
|
|
126
|
+
const [store] = trycatch(() => transaction?.objectStore(storeName));
|
|
127
|
+
// create store and break if idb have no store exist
|
|
128
|
+
if (!store) return createStoresMap.set(storeName, storeBuilder), checkIndexes();
|
|
129
|
+
// define matches variables
|
|
130
|
+
const OBJECT_UPGRADE = _Array_from(storeBuilder.upgrades).find(([upgradeVersion],) =>
|
|
131
|
+
dbVersion >= upgradeVersion && idb.version < upgradeVersion
|
|
132
|
+
)
|
|
133
|
+
const CONFIG_CHANGED =
|
|
134
|
+
_JSON_stringify(keyPath) !== _JSON_stringify(store.keyPath)
|
|
135
|
+
|| autoIncrement !== store?.autoIncrement
|
|
136
|
+
const UPGRADE_NEEDED = OBJECT_UPGRADE || CONFIG_CHANGED;
|
|
137
|
+
// add indexes
|
|
138
|
+
checkIndexes();
|
|
139
|
+
// store existed and not need upgrade
|
|
140
|
+
if (store && !UPGRADE_NEEDED) return;
|
|
141
|
+
// add upgrade store queue
|
|
142
|
+
upgradeStoreMap.set(storeName, storeBuilder)
|
|
143
|
+
|
|
144
|
+
})
|
|
145
|
+
// resolve if no need upgrade
|
|
146
|
+
if (dbVersion === idb.version && !createStoresMap.size && !upgradeStoreMap.size && !unusedStoreNameList.length && !createIndexMap.size)
|
|
147
|
+
return noUpgrade();
|
|
148
|
+
// cache objects
|
|
149
|
+
for (const [storeName, storeBuilder] of upgradeStoreMap) {
|
|
150
|
+
const cache: {key: any, value: any}[] = [];
|
|
151
|
+
// filter version lower than current idb
|
|
152
|
+
const upgradeHandleList = _Array_from(storeBuilder.upgrades)
|
|
153
|
+
.filter(([upgradeVersion]) => dbVersion >= upgradeVersion && idb.version < upgradeVersion )
|
|
154
|
+
.sort((a, b) => a[0] - b[0])
|
|
155
|
+
.map(config => config[1]);
|
|
156
|
+
// cache objects from store
|
|
157
|
+
await $idb.transaction(storeName, false, async $tx => {
|
|
158
|
+
cachedObjectMap.set(storeName, cache);
|
|
159
|
+
await $tx.store(storeName).cursor(async cursor => {
|
|
160
|
+
cache.push({key: cursor.key, value: cursor.value});
|
|
161
|
+
cursor.continue();
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
// upgrade objects
|
|
165
|
+
for (const upgradeHandle of upgradeHandleList)
|
|
166
|
+
cachedObjectMap.set(storeName, await upgradeHandle(cache, $idb));
|
|
167
|
+
}
|
|
168
|
+
// upgrade db from lower version
|
|
169
|
+
idb.close();
|
|
170
|
+
const upgradeDBRequest = _indexedDB.open(dbName, dbVersion);
|
|
171
|
+
upgradeDBRequest[onupgradeneeded] = _ => upgradeStore(upgradeDBRequest);
|
|
172
|
+
upgradeDBRequest[onsuccess] = _ => {
|
|
173
|
+
debug('IDB Upgrade Completed');
|
|
174
|
+
resolve(new $IDB(upgradeDBRequest.result, idbConfig));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** IndexedDB upgrade version */
|
|
179
|
+
const upgradeStore = (req: IDBOpenDBRequest) => {
|
|
180
|
+
debug('Upgrade DB')
|
|
181
|
+
const idb = req.result;
|
|
182
|
+
/** 'versionchange' type transaction */
|
|
183
|
+
const transaction = req.transaction as IDBTransaction;
|
|
184
|
+
// create stores
|
|
185
|
+
forEach(createStoresMap, ({config}, name) => {
|
|
186
|
+
idb[createObjectStore](name, config);
|
|
187
|
+
debug(`Store Created: ${name}`);
|
|
188
|
+
});
|
|
189
|
+
// upgrade stores
|
|
190
|
+
forEach(upgradeStoreMap, ({config}, name) => {
|
|
191
|
+
idb[deleteObjectStore](name);
|
|
192
|
+
idb[createObjectStore](name, config);
|
|
193
|
+
debug(`Store Upgraded: ${name}`);
|
|
194
|
+
})
|
|
195
|
+
// create indexes
|
|
196
|
+
forEach(createIndexMap, (indexMap, {config: {name}}) => {
|
|
197
|
+
const store = transaction.objectStore(name);
|
|
198
|
+
forEach(indexMap, ({keyPath, ...config}, indexName) => {
|
|
199
|
+
// if indexes existed, delete and create again
|
|
200
|
+
if (store.indexNames.contains(indexName)) store.deleteIndex(indexName);
|
|
201
|
+
store.createIndex(indexName, keyPath, config);
|
|
202
|
+
debug(`Store '${name}' Index Created: ${indexName}`);
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
// delete unused stores
|
|
206
|
+
if (this.#deleteUnused) forEach(unusedStoreNameList, name => {
|
|
207
|
+
idb[deleteObjectStore](name);
|
|
208
|
+
debug(`Unused Store Deleted: ${name}`);
|
|
209
|
+
});
|
|
210
|
+
// open db again for insert objects
|
|
211
|
+
forEach(cachedObjectMap, (objectList, storeName) => {
|
|
212
|
+
const store = transaction.objectStore(storeName);
|
|
213
|
+
forEach(objectList, ({key, value}) => {
|
|
214
|
+
if (store.autoIncrement || store.keyPath) store.add(value);
|
|
215
|
+
else store.add(value, key)
|
|
216
|
+
})
|
|
217
|
+
debug(`Recovered Store Objects: ${objectList.length} objects of store '${storeName}'`);
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// If db not exist, create db will trigger upgraedneeded event
|
|
222
|
+
initDBRequest[onupgradeneeded] = initialCreateDB;
|
|
223
|
+
// If db exist, trigger success event
|
|
224
|
+
initDBRequest[onsuccess] = initialOpenDB;
|
|
225
|
+
initDBRequest.onerror = _ => reject(initDBRequest.error);
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export type $IDBStoreBuilderFunction = (store: $IDBStoreBuilder) => $IDBStoreBuilder<any>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { $IDB } from "#structure/$IDB";
|
|
2
|
+
import { _null } from "amateras/lib/native";
|
|
3
|
+
import type { $IDBStoreConfig } from "#structure/$IDBStore";
|
|
4
|
+
import type { $IDBIndexConfig } from "#structure/$IDBIndex";
|
|
5
|
+
|
|
6
|
+
export class $IDBStoreBuilder<Config extends $IDBStoreConfig = { name: string, keyPath: null, autoIncrement: false, schema: any, indexes: {} }> {
|
|
7
|
+
readonly config: Config
|
|
8
|
+
upgrades = new Map<number, $IDBStoreUpgradeFunction>();
|
|
9
|
+
indexes = new Map<string, $IDBIndexConfig>();
|
|
10
|
+
constructor(config: Config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Define the `keyPath` option of store.
|
|
16
|
+
*
|
|
17
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB#structuring_the_database).
|
|
18
|
+
*/
|
|
19
|
+
keyPath<K extends string[]>(keyPath: K): $IDBStoreBuilder<Prettify<Omit<Config, 'keyPath'> & { keyPath: K }>>;
|
|
20
|
+
keyPath<K extends string>(keyPath: K): $IDBStoreBuilder<Prettify<Omit<Config, 'keyPath'> & { keyPath: K }>>;
|
|
21
|
+
keyPath(keyPath: string | string[]) {
|
|
22
|
+
this.config.keyPath = keyPath;
|
|
23
|
+
return this as any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Define the `autoIncrement` option of store.
|
|
28
|
+
*
|
|
29
|
+
* [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB#structuring_the_database).
|
|
30
|
+
*/
|
|
31
|
+
autoIncrement<K extends boolean>(enable: K): $IDBStoreBuilder<Prettify<Omit<Config, 'autoIncrement'> & { autoIncrement: K }>>;
|
|
32
|
+
autoIncrement(enable: boolean) {
|
|
33
|
+
this.config.autoIncrement = enable;
|
|
34
|
+
return this as any;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Use the generic type to define store object type.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* store.schema<{
|
|
42
|
+
* id: number,
|
|
43
|
+
* name: string,
|
|
44
|
+
* created: Date,
|
|
45
|
+
* updated: Date
|
|
46
|
+
* }>()
|
|
47
|
+
*/
|
|
48
|
+
schema<T extends $IDBStoreBuilderSchema<Config>>(): $IDBStoreBuilder<Prettify<Omit<Config, 'schema'> & { schema: T }>>;
|
|
49
|
+
schema() { return this as any }
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Add new index to store builder.
|
|
53
|
+
* @param name - Index name
|
|
54
|
+
* @param config - {@link $IDBIndexOptionalConfig}
|
|
55
|
+
*/
|
|
56
|
+
index<N extends string, C extends $IDBIndexOptionalConfig<Config>>(name: N, config: C): $IDBStoreBuilder<Prettify<Config & { indexes: Config['indexes'] & Prettify<Record<N, Prettify<$IDBIndexOptionalHandle<N, C>>>> }>>
|
|
57
|
+
index<C extends $IDBIndexConfig>(name: string, config: C) {
|
|
58
|
+
this.indexes.set(name, {
|
|
59
|
+
...config, name,
|
|
60
|
+
multiEntry: config.multiEntry ?? false,
|
|
61
|
+
unique: config.unique ?? false
|
|
62
|
+
});
|
|
63
|
+
return this as any;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Add store upgrade function to store builder. The store upgrade function is used for change object structure when the store is upgrading.
|
|
68
|
+
* @param version - Target version of database
|
|
69
|
+
* @param handle - Upgrade handle function
|
|
70
|
+
*/
|
|
71
|
+
upgrade(version: number, handle: $IDBStoreUpgradeFunction) {
|
|
72
|
+
this.upgrades.set(version, handle);
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type $IDBStoreUpgradeFunction = (objects: {key: IDBValidKey, value: any}[], idb: $IDB<any>) => OrPromise<{key: IDBValidKey, value: any}[]>;
|
|
79
|
+
|
|
80
|
+
export type $IDBIndexOptionalConfig<StoreConfig extends $IDBStoreConfig = any> = {
|
|
81
|
+
keyPath: OrArray<keyof StoreConfig['schema']>,
|
|
82
|
+
unique?: boolean,
|
|
83
|
+
multiEntry?: boolean
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type $IDBIndexOptionalHandle<N extends string, Config extends $IDBIndexOptionalConfig> = {
|
|
87
|
+
name: N;
|
|
88
|
+
keyPath: Config['keyPath'];
|
|
89
|
+
multiEntry: Config['multiEntry'] extends boolean ? Config['multiEntry'] : false;
|
|
90
|
+
unique: Config['unique'] extends boolean ? Config['unique'] : false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type $IDBStoreBuilderSchema<Config extends $IDBStoreConfig> =
|
|
94
|
+
Config['keyPath'] extends string
|
|
95
|
+
? Config['autoIncrement'] extends true
|
|
96
|
+
? { [key in Config['keyPath']]: number }
|
|
97
|
+
: { [key in Config['keyPath']]: IDBValidKey }
|
|
98
|
+
: Config['keyPath'] extends string[]
|
|
99
|
+
? { [key in Config['keyPath'][number]]: IDBValidKey }
|
|
100
|
+
: any;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { _Array_from, forEach } from "amateras/lib/native";
|
|
2
|
+
|
|
3
|
+
const blockProcesses = new Set<MarkdownBlockProcessOptions>();
|
|
4
|
+
const inlineProcesses = new Set<MarkdownProcessFunction>();
|
|
5
|
+
|
|
6
|
+
export class Markdown {
|
|
7
|
+
blockProcessSet = new Set(blockProcesses);
|
|
8
|
+
inlineProcessSet = new Set(inlineProcesses);
|
|
9
|
+
constructor() {}
|
|
10
|
+
|
|
11
|
+
blockProcess(options: MarkdownBlockProcessOptions) {
|
|
12
|
+
this.blockProcessSet.add(options);
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
inlineProcess(handle: MarkdownProcessFunction) {
|
|
17
|
+
this.inlineProcessSet.add(handle);
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toHTML(text: string) {
|
|
22
|
+
const blocks = _Array_from(text.matchAll(/(?:.+?\n?)+/gm));
|
|
23
|
+
return blocks.map(block => {
|
|
24
|
+
let matched, blockText = block[0]
|
|
25
|
+
for (const blockProcess of blockProcesses) {
|
|
26
|
+
matched = blockText.match(blockProcess.regexp);
|
|
27
|
+
if (!matched) continue;
|
|
28
|
+
blockText = blockProcess.handle(blockText);
|
|
29
|
+
const removeHTML = blockText.replaceAll(/<.+>[^<]+?<\/.+>/gm, '');
|
|
30
|
+
if (!removeHTML) break;
|
|
31
|
+
}
|
|
32
|
+
if (!matched) blockText = paragraph(blockText);
|
|
33
|
+
inlineProcesses.forEach(fn => blockText = fn(blockText))
|
|
34
|
+
return blockText;
|
|
35
|
+
}).join('')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
toDOM(text: string) {
|
|
39
|
+
return $('article').innerHTML(this.toHTML(text))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type MarkdownProcessFunction = (text: string) => string;
|
|
44
|
+
export interface MarkdownBlockProcessOptions {
|
|
45
|
+
regexp: RegExp,
|
|
46
|
+
handle: (text: string) => string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const blockProcess = (options: MarkdownBlockProcessOptions) => blockProcesses.add(options);
|
|
50
|
+
const inlineProcess = (handle: MarkdownProcessFunction) => inlineProcesses.add(handle);
|
|
51
|
+
const replaceAll = (str: string, searchValue: string | RegExp, replacer: ((substring: string, ...args: any[]) => string) | string): string => str.replaceAll(searchValue, replacer as any);
|
|
52
|
+
const trim = (str: string) => str.trim();
|
|
53
|
+
const paragraph = (str: string) => replaceAll(str, /(?:.+?\n?)+/gm, $0 => `<p>${trim($0)}</p>`);
|
|
54
|
+
// Headings
|
|
55
|
+
blockProcess({
|
|
56
|
+
regexp: /^(#+) (.+)/gm,
|
|
57
|
+
handle: text => replaceAll(text, /^(#+) (.+)/gm, (_, $1: string, $2) => `<h${$1.length}>${$2}</h${$1.length}>`)
|
|
58
|
+
});
|
|
59
|
+
blockProcess({
|
|
60
|
+
regexp: /^(.+)\n==+$/gm,
|
|
61
|
+
handle: text => replaceAll(text, /^(.+)\n==+$/gm, (_, $1) => `<h1>${$1}</h1>`)
|
|
62
|
+
});
|
|
63
|
+
blockProcess({
|
|
64
|
+
regexp: /^(.+)\n--+$/gm,
|
|
65
|
+
handle: text => replaceAll(text, /^(.+)\n--+$/gm, (_, $1) => `<h2>${$1}</h2>`)
|
|
66
|
+
});
|
|
67
|
+
// Blockquote
|
|
68
|
+
blockProcess({
|
|
69
|
+
regexp: /(?:^> ?.*(?:\n|$))+/gm,
|
|
70
|
+
handle: text => {
|
|
71
|
+
const fn = (str: string) => {
|
|
72
|
+
const blocks = _Array_from(str.matchAll(/(?:^> ?.*(?:\n|$))+/gm));
|
|
73
|
+
forEach(blocks, block => {
|
|
74
|
+
const blocked = fn(replaceAll(block[0], /^> ?/gm, ''));
|
|
75
|
+
str = str.replace(block[0], `<blockquote>\n${paragraph(blocked)}\n</blockquote>`);
|
|
76
|
+
})
|
|
77
|
+
return str;
|
|
78
|
+
}
|
|
79
|
+
return fn(text);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// List
|
|
83
|
+
blockProcess({
|
|
84
|
+
regexp: /(?:^(?:\t|(?: )+)?(?:-|[0-9]+\.) (?:.+\n?))+/gm,
|
|
85
|
+
handle: text => {
|
|
86
|
+
const fn = (str: string) => {
|
|
87
|
+
const blocks = _Array_from(str.matchAll(/(?:^(?:\t|(?: )+)?(?:-|[0-9]+\.) (?:.+\n?))+/gm));
|
|
88
|
+
forEach(blocks, block => {
|
|
89
|
+
let haveList = false // check this loop have list
|
|
90
|
+
const type = block[0].match(/^(-|[0-9]+\.) /)?.[1] === '-' ? 'ul' : 'ol';
|
|
91
|
+
const listed = replaceAll(block[0], /^(?:-|[0-9]+\.) (.+)/gm, (_, $1: string) => (haveList = true, `<li>\n${trim($1)}\n</li>`));
|
|
92
|
+
const clearTabbed = replaceAll(listed, /^(?:\t|(?: ))/gm, '');
|
|
93
|
+
const convertedList = fn(clearTabbed);
|
|
94
|
+
str = str.replace(block[0], haveList ? `<${type}>\n${trim(convertedList)}\n</${type}>` : convertedList);
|
|
95
|
+
})
|
|
96
|
+
return str;
|
|
97
|
+
}
|
|
98
|
+
return fn(text);
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
// Codeblock
|
|
102
|
+
blockProcess({
|
|
103
|
+
regexp: /^```([^`\n]+)\n([^`]+)?```/gm,
|
|
104
|
+
handle: text => replaceAll(text, /^```([^`\n]+)\n([^`]+)?```/gm, (_, $1, $2: string) => `<pre><code>\n${trim($2)}\n</code></pre>`)
|
|
105
|
+
})
|
|
106
|
+
// Horizontal Rule
|
|
107
|
+
blockProcess({
|
|
108
|
+
regexp: /^(?:---|\*\*\*|___)(\s+)?$/gm,
|
|
109
|
+
handle: text => replaceAll(text, /^(?:---|\*\*\*|___)(\s+)?$/gm, _ => `<hr>`)
|
|
110
|
+
})
|
|
111
|
+
// Bold
|
|
112
|
+
inlineProcess(text => replaceAll(text, /\*\*([^*]+?)\*\*/g, (_, $1) => `<b>${$1}</b>`));
|
|
113
|
+
// Italic
|
|
114
|
+
inlineProcess(text => replaceAll(text, /\*([^*]+?)\*/g, (_, $1) => `<i>${$1}</i>`));
|
|
115
|
+
// Image
|
|
116
|
+
inlineProcess(text => replaceAll(text, /!\[(.+?)\]\((.+?)(?: "(.+?)?")?\)/g, (_, alt, src, title) => `<img src="${src}" alt="${alt}"${title ? ` title="${title}"` : ''}>`));
|
|
117
|
+
// Link
|
|
118
|
+
inlineProcess(text => replaceAll(text, /\[(.+?)\]\((?:(\w\w+?:[^\s]+?)(?: "(.+?)?")?)\)/g, (_, content, href, title) => `<a href="${href}"${title ? ` title="${title}"` : ''}>${content}</a>`));
|
|
119
|
+
inlineProcess(text => replaceAll(text, /\[(.+?)\]\((?:(\w+?@(?:\w|\.\w)+?)(?: "(.+)?")?)\)/g, (_, content, mail, title) => `<a href="mailto:${mail}"${title ? ` title="${title}"` : ''}>${content}</a>`));
|
|
120
|
+
inlineProcess(text => replaceAll(text, /<(\w\w+?:[^\s]+?)>/g, (_, href) => `<a href="${href}">${href}</a>`));
|
|
121
|
+
inlineProcess(text => replaceAll(text, /<(\w+?@(?:\w|\.\w)+?)>/g, (_, mail) => `<a href="mailto:${mail}">${mail}</a>`));
|
package/ext/router/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ declare module 'amateras/core' {
|
|
|
15
15
|
export function $(nodeName: 'router', page?: Page<any>): Router;
|
|
16
16
|
export function $(nodeName: 'ra'): RouterAnchor;
|
|
17
17
|
export namespace $ {
|
|
18
|
-
export function open(url: string | URL | Nullish, target
|
|
18
|
+
export function open(url: string | URL | Nullish, target?: AnchorTarget): typeof Router;
|
|
19
19
|
export function replace(url: string | URL | Nullish): typeof Router;
|
|
20
20
|
export function back(): typeof Router;
|
|
21
21
|
export function forward(): typeof Router;
|
package/ext/router/node/Route.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _instanceof, _Object_fromEntries, _Array_from } from "#lib/native";
|
|
1
|
+
import { _instanceof, _Object_fromEntries, _Array_from, _Promise } from "#lib/native";
|
|
2
2
|
import { $Element } from "#node/$Element";
|
|
3
3
|
import type { AsyncRoute, RouteBuilder, RouteDataResolver } from "..";
|
|
4
4
|
import { Page } from "./Page";
|
|
@@ -39,7 +39,7 @@ export class Route<Path extends string = string> extends BaseRouteNode<Path> {
|
|
|
39
39
|
page.params = data.params;
|
|
40
40
|
page.initial = true;
|
|
41
41
|
let resolver: any = this.builder(page);
|
|
42
|
-
if (_instanceof(resolver,
|
|
42
|
+
if (_instanceof(resolver, _Promise)) {
|
|
43
43
|
const result = await resolver as any;
|
|
44
44
|
// Module import
|
|
45
45
|
if (result[Symbol.toStringTag] === 'Module') {
|
|
@@ -3,6 +3,11 @@ import { $Anchor } from "#html/$Anchor";
|
|
|
3
3
|
export class RouterAnchor extends $Anchor {
|
|
4
4
|
constructor() {
|
|
5
5
|
super();
|
|
6
|
-
this.on('click', e => {
|
|
6
|
+
this.on('click', e => {
|
|
7
|
+
e.preventDefault();
|
|
8
|
+
this.target() === '_replace'
|
|
9
|
+
? $.replace(this.href())
|
|
10
|
+
: $.open(this.href(), this.target())
|
|
11
|
+
})
|
|
7
12
|
}
|
|
8
13
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amateras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Amateras is a DOM Utility library.",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,10 @@
|
|
|
35
35
|
"./css/color/*": "./ext/css/src/lib/colors/*.ts",
|
|
36
36
|
"./router": "./ext/router/index.ts",
|
|
37
37
|
"./ssr": "./ext/ssr/index.ts",
|
|
38
|
-
"./i18n": "./ext/i18n/src/index.ts"
|
|
38
|
+
"./i18n": "./ext/i18n/src/index.ts",
|
|
39
|
+
"./idb": "./ext/idb/src/index.ts",
|
|
40
|
+
"./idb/core": "./ext/idb/src/core.ts",
|
|
41
|
+
"./markdown": "./ext/markdown/index.ts"
|
|
39
42
|
},
|
|
40
43
|
"workspaces": [
|
|
41
44
|
"./ext/*"
|
package/src/core.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { $Node, type $NodeContentResolver, type $NodeContentTypes } from '#node/
|
|
|
6
6
|
import { _instanceof, isString, isFunction, _Object_assign, isObject, isNull, _Object_entries, _Object_defineProperty, forEach, isNumber, _Array_from, isUndefined, _bind, _null } from '#lib/native';
|
|
7
7
|
import { $HTMLElement } from '#node/$HTMLElement';
|
|
8
8
|
import { _document } from '#lib/env';
|
|
9
|
+
import { toArray } from '#lib/toArray';
|
|
9
10
|
|
|
10
11
|
const nodeNameMap: {[key: string]: Constructor<$Node>} = {}
|
|
11
12
|
const _stylesheet = new CSSStyleSheet();
|
|
@@ -45,6 +46,7 @@ export function $(resolver: string | number | null | undefined | Element | HTMLE
|
|
|
45
46
|
if (isNumber(resolver)) return _Array_from({length: resolver}).map(_ => $(args[0], ...args.slice(1)));
|
|
46
47
|
if (_instanceof(resolver, HTMLElement)) return new $HTMLElement(resolver);
|
|
47
48
|
if (_instanceof(resolver, Element)) return new $Element(resolver);
|
|
49
|
+
if (_instanceof(resolver, Node)) return new $Node(resolver as any);
|
|
48
50
|
if (_instanceof(resolver, NodeList)) return _Array_from(resolver).map($)
|
|
49
51
|
return new $HTMLElement(resolver);
|
|
50
52
|
}
|
|
@@ -104,9 +106,9 @@ export namespace $ {
|
|
|
104
106
|
return $;
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
export const toArray = <T>(item: OrArray<T>): T[] => _instanceof(item, Array) ? item : [item];
|
|
108
|
-
|
|
109
109
|
export const span = (content: string) => $('span').content(content);
|
|
110
110
|
}
|
|
111
|
+
|
|
111
112
|
export type $ = typeof $;
|
|
112
|
-
globalThis.$ = $;
|
|
113
|
+
globalThis.$ = $;
|
|
114
|
+
_Object_assign($, {toArray})
|
package/src/global.ts
CHANGED
|
@@ -12,6 +12,9 @@ declare global {
|
|
|
12
12
|
type OrNullish<T> = T | Nullish;
|
|
13
13
|
type Constructor<T> = { new (...args: any[]): T }
|
|
14
14
|
type $Parameter<T> = T | undefined | Signal<T> | Signal<T | undefined>
|
|
15
|
+
type Ok<D> = [data: D, err: null];
|
|
16
|
+
type Err<E> = [data: null, err: E]
|
|
17
|
+
type Result<D, E> = Ok<D> | Err<E>
|
|
15
18
|
type Repeat<T, N extends number, Acc extends T[] = []> =
|
|
16
19
|
Acc['length'] extends N
|
|
17
20
|
? Acc
|
package/src/lib/native.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Value
|
|
2
2
|
export const _null = null;
|
|
3
|
+
export const _undefined = undefined;
|
|
3
4
|
// Object
|
|
4
5
|
export const _Object_fromEntries = Object.fromEntries;
|
|
5
6
|
export const _Object_entries = Object.entries;
|
|
@@ -14,11 +15,13 @@ export const forEach: forEach = <T>(arr: any, fn: any, thisArgs?: any) => arr.fo
|
|
|
14
15
|
export const _typeof = (target: any, type: 'string' | 'number' | 'object' | 'boolean' | 'function' | 'bigint' | 'symbol' | 'undefined') => typeof target === type;
|
|
15
16
|
export const equal = <T, V extends T>(target: T, ...args: V[]): target is V => !!args.find(a => a === target);
|
|
16
17
|
export const isString = (target: any): target is string => _typeof(target, 'string');
|
|
17
|
-
export const
|
|
18
|
-
export const
|
|
19
|
-
export const
|
|
18
|
+
export const isBoolean = (target: any): target is boolean => _typeof(target, 'boolean');
|
|
19
|
+
export const isNumber = (target: any): target is number => _typeof(target, 'number');
|
|
20
|
+
export const isObject = (target: any): target is object => _typeof(target, 'object');
|
|
21
|
+
export const isFunction = (target: any): target is Function => _typeof(target, 'function');
|
|
20
22
|
export const isUndefined = (target: any): target is undefined => target === undefined;
|
|
21
23
|
export const isNull = (target: any): target is null => target === _null;
|
|
24
|
+
export const isArray = Array.isArray;
|
|
22
25
|
export const _instanceof = <T extends (abstract new (...args: any[]) => any)[]>(target: any, ...instance: T): target is InstanceType<T[number]> => !!instance.find(i => target instanceof i);
|
|
23
26
|
// JSON
|
|
24
27
|
export const _JSON_stringify = JSON.stringify;
|
|
@@ -32,4 +35,6 @@ interface forEach {
|
|
|
32
35
|
<T>(set: Set<T>, fn: (value: T, index: number, set: Set<T>) => any, thisArgs?: any): void;
|
|
33
36
|
<K, V>(set: Map<K, V>, fn: (value: V, key: K, index: number, map: Map<K, V>) => any, thisArgs?: any): void;
|
|
34
37
|
<N extends Node>(set: NodeListOf<N>, fn: (value: N, index: number, parent: NodeListOf<N>) => any, thisArgs?: any): void;
|
|
35
|
-
}
|
|
38
|
+
}
|
|
39
|
+
// Promise
|
|
40
|
+
export const _Promise = Promise;
|
package/src/lib/sleep.ts
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { _instanceof, _Object_assign } from "./native";
|
|
2
|
+
|
|
3
|
+
export const toArray = <T>(item: OrArray<T>): T[] => _instanceof(item, Array) ? item : [item];
|
|
4
|
+
|
|
5
|
+
declare module '#core' {
|
|
6
|
+
export namespace $ {
|
|
7
|
+
export function toArray<T>(item: OrArray<T>): T[];
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { _instanceof, _JSON_stringify, _null, _Object_assign } from "./native";
|
|
2
|
+
|
|
3
|
+
export const trycatch = <D>(callback: () => D): Result<D, Error> => {
|
|
4
|
+
try {
|
|
5
|
+
return [callback(), _null];
|
|
6
|
+
} catch (err) {
|
|
7
|
+
return [_null, _instanceof(err, Error) ? err : new Error(_JSON_stringify(err))];
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
_Object_assign($, {trycatch})
|
|
12
|
+
|
|
13
|
+
declare module '#core' {
|
|
14
|
+
export namespace $ {
|
|
15
|
+
export function trycatch <D>(callback: () => D): Result<D, Error>
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/node/$Element.ts
CHANGED
|
@@ -13,8 +13,8 @@ export class $Element<Ele extends Element = Element, EvMap = ElementEventMap> ex
|
|
|
13
13
|
|
|
14
14
|
attr(): {[key: string]: string};
|
|
15
15
|
attr(key: string): string | null;
|
|
16
|
-
attr(obj: {[key: string]: string | number | boolean | Signal<any> | null}): this;
|
|
17
|
-
attr(resolver?: {[key: string]: string | number | boolean | Signal<any> | null} | string) {
|
|
16
|
+
attr(obj: {[key: string]: string | number | boolean | Signal<any> | null | undefined}): this;
|
|
17
|
+
attr(resolver?: {[key: string]: string | number | boolean | Signal<any> | null | undefined} | string) {
|
|
18
18
|
if (!arguments.length) return _Object_fromEntries(_Array_from(this.attributes).map(attr => [attr.name, attr.value]));
|
|
19
19
|
if (isString(resolver)) return this.getAttribute(resolver);
|
|
20
20
|
if (resolver) for (let [key, value] of _Object_entries(resolver)) {
|
|
@@ -39,18 +39,6 @@ export class $Element<Ele extends Element = Element, EvMap = ElementEventMap> ex
|
|
|
39
39
|
return this;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
on<K extends keyof EvMap, Ev extends EvMap[K]>(type: K, listener: $EventListener<this, Ev> | $EventListenerObject<this, Ev>, options?: boolean | AddEventListenerOptions) {
|
|
43
|
-
return this.addEventListener(type as string, listener as any, options);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
off<K extends keyof EvMap, Ev extends EvMap[K]>(type: K, listener: $EventListener<this, Ev> | $EventListenerObject<this, Ev>, options?: boolean | EventListenerOptions) {
|
|
47
|
-
return this.removeEventListener(type as string, listener as any, options);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
once<K extends keyof EvMap, Ev extends EvMap[K]>(type: K, listener: $EventListener<this, Ev> | $EventListenerObject<this, Ev>, options?: boolean | AddEventListenerOptions) {
|
|
51
|
-
return this.on(type, listener, {once: true})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
42
|
toString() {
|
|
55
43
|
return this.outerHTML();
|
|
56
44
|
}
|