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,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, ([name, storeBuilder]) => {
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, ([storeName, storeBuilder]) => {
112
+ const {keyPath, autoIncrement} = storeBuilder.config;
113
+ const indexMap = new Map();
114
+ const checkIndexes = () =>
115
+ forEach(storeBuilder.indexes, ([indexName, indexBuilder]) => {
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, ([name, {config}]) => {
186
+ idb[createObjectStore](name, config);
187
+ debug(`Store Created: ${name}`);
188
+ });
189
+ // upgrade stores
190
+ forEach(upgradeStoreMap, ([name, {config}]) => {
191
+ idb[deleteObjectStore](name);
192
+ idb[createObjectStore](name, config);
193
+ debug(`Store Upgraded: ${name}`);
194
+ })
195
+ // create indexes
196
+ forEach(createIndexMap, ([{config: {name}}, indexMap]) => {
197
+ const store = transaction.objectStore(name);
198
+ forEach(indexMap, ([indexName, {keyPath, ...config}]) => {
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, ([storeName, objectList]) => {
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,53 @@
1
+ # amateras/markdown
2
+
3
+ ## Usage
4
+ ```ts
5
+ import { Markdown } from 'amateras/markdown';
6
+
7
+ const markdown = new Markdown();
8
+
9
+ markdown.parseHTML('# Title'); // => <h1>Title</h1>
10
+ ```
11
+
12
+ ## Add Custom Markdown Rules
13
+ ```ts
14
+ markdown.lexer.blockTokenizers.set('CUSTOM_TYPE', {
15
+ regex: /#! (.+)/,
16
+ handle: matches => {
17
+ content: lexer.inlineTokenize(matches[1]!)
18
+ }
19
+ })
20
+
21
+ markdown.parser.processors.set('CUSTOM_TYPE', token => {
22
+ return `<custom>${markdown.parser.parse(token.content!)}</custom>`
23
+ })
24
+ ```
25
+
26
+ ## Import Syntax
27
+ ```ts
28
+ import { MarkdownParser, MarkdownLexer } from 'amateras/markdown';
29
+ import { blockquoteProcessor, blockquoteTokenizer } from 'amateras/markdown/syntax/blockquote';
30
+ import { headingProcessor, headingTokenizer } from 'amateras/markdown/syntax/heading';
31
+
32
+ const lexer = new MarkdownLexer();
33
+ const parser = new MarkdownParser();
34
+
35
+ lexer.use(
36
+ blockquoteTokenizer,
37
+ headingTokenizer
38
+ )
39
+
40
+ parser.use(
41
+ blockquoteProcessor,
42
+ headingProcessor
43
+ )
44
+
45
+ function parseHTML(str: string) {
46
+ const tokens = lexer.blockTokenize(str);
47
+ return parser.parse(tokens);
48
+ }
49
+
50
+ parseHTML('# Title') // => <h1>Title</h1>
51
+ parseHTML('> This is Blockquote') // => <blockquote>This is Blockquote</blockquote>
52
+ parseHTML('- List') // => - List
53
+ ```
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@amateras/markdown",
3
+ "peerDependencies": {
4
+ "amateras": "../../"
5
+ },
6
+ "imports": {
7
+ "#structure/*": "./src/structure/*.ts",
8
+ "#lib/*": "./src/lib/*.ts"
9
+ },
10
+ "exports": {
11
+ "./structure/*": "./src/structure/*.ts",
12
+ "./lib/*": "./src/lib/*.ts",
13
+ "./syntax/*": "./src/syntax/*.ts"
14
+ }
15
+ }
@@ -0,0 +1,3 @@
1
+ export * from '#structure/MarkdownLexer';
2
+ export * from '#structure/MarkdownParser';
3
+ export * from "#structure/Markdown";
@@ -0,0 +1,26 @@
1
+ export const INLINE = 'INLINE';
2
+ export const BLOCK = 'BLOCK';
3
+ export const TEXT = 'TEXT';
4
+ export const IMAGE = 'IMAGE';
5
+ export const LINK = 'LINK';
6
+ export const QUICK_LINK = 'QUICK_LINK';
7
+ export const CODE = 'CODE';
8
+ export const ITALIC = 'ITALIC';
9
+ export const BOLD = 'BOLD';
10
+ export const TEXT_LINE = 'TEXT_LINE';
11
+ export const HEADING = 'HEADING';
12
+ export const CODE_START = 'CODE_START';
13
+ export const CODE_LINE = 'CODE_LINE';
14
+ export const CODE_END = 'CODE_END';
15
+ export const UNORDERED_LIST_ITEM = 'UNORDERED_LIST_ITEM';
16
+ export const ORDERED_LIST_ITEM = 'ORDERED_LIST_ITEM';
17
+ export const BLOCKQUOTE = 'BLOCKQUOTE';
18
+ export const ALERT = 'ALERT';
19
+ export const ALERT_LINE = 'ALERT_LINE';
20
+ export const HORIZONTAL_RULE = 'HORIZONTAL_RULE';
21
+ export const TABLE = 'TABLE';
22
+ export const TABLE_ROW = 'TABLE_ROW';
23
+ export const TABLE_COLUMN = 'TABLE_COLUMN';
24
+ export const EMPTY_LINE = 'EMPTY_LINE'
25
+ export const INLINE_TEXT = "INLINE_TEXT";
26
+ export const INLINE_CONTENT = "INLINE_CONTENT"
@@ -0,0 +1,21 @@
1
+ import type { BlockTokenizer, InlineTokenizer, MarkdownLexer } from "#structure/MarkdownLexer";
2
+ import type { MarkdownParseProcessor, MarkdownParser } from "#structure/MarkdownParser";
3
+ import { forEach } from "amateras/lib/native";
4
+
5
+ export const setBlockTokenizer = (lexer: MarkdownLexer, type: string, tokenizer: BlockTokenizer) => lexer.blockTokenizers.set(type, tokenizer);
6
+ export const setInlineTokenizer = (lexer: MarkdownLexer, type: string, tokenizer: InlineTokenizer) => lexer.inlineTokenizers.set(type, tokenizer);
7
+
8
+ export const setProcessor = (parser: MarkdownParser, type: string, processor: MarkdownParseProcessor) => parser.processors.set(type, processor);
9
+
10
+ export const htmltag = (tagname: string, content: string) => `<${tagname}>${content}</${tagname}>`
11
+
12
+ export const htmlEscapeChar = (str: string) => {
13
+ forEach([
14
+ ['&', '&amp;'],
15
+ ['<', '&lt;'],
16
+ ['>', '&gt;'],
17
+ ['"', '&quot;'],
18
+ ["'", '&#39;']
19
+ ] as [string, string][], group => str = str.replaceAll(...group))
20
+ return str;
21
+ }
@@ -0,0 +1,54 @@
1
+ import { alertProcessor, alertTokenizer } from "../syntax/alert";
2
+ import { blockquoteProcessor, blockquoteTokenizer } from "../syntax/blockquote";
3
+ import { boldProcessor, boldTokenizer } from "../syntax/bold";
4
+ import { codeProcessor, codeTokenizer } from "../syntax/code";
5
+ import { codeblockProcessor, codeblockTokenizer } from "../syntax/codeblock";
6
+ import { headingProcessor, headingTokenizer } from "../syntax/heading";
7
+ import { horizontalRuleProcessor, horizontalRuleTokenizer } from "../syntax/horizontalRule";
8
+ import { imageProcessor, imageTokenizer } from "../syntax/image";
9
+ import { italicProcessor, italicTokenizer } from "../syntax/italic";
10
+ import { linkProcessor, linkTokenizer } from "../syntax/link";
11
+ import { listProcessor, listTokenizer } from "../syntax/list";
12
+ import { textLineProcessor, textProcessor } from "../syntax/text";
13
+ import { MarkdownLexer } from "./MarkdownLexer";
14
+ import { MarkdownParser } from "./MarkdownParser";
15
+
16
+ export class Markdown {
17
+ lexer = new MarkdownLexer();
18
+ parser = new MarkdownParser();
19
+ constructor() {
20
+ this.lexer.use(
21
+ headingTokenizer,
22
+ codeblockTokenizer,
23
+ listTokenizer,
24
+ alertTokenizer,
25
+ blockquoteTokenizer,
26
+ horizontalRuleTokenizer,
27
+ imageTokenizer, // image tokenizer must before link
28
+ linkTokenizer, // link tokenizer must before bold and italic and code
29
+ codeTokenizer,
30
+ boldTokenizer,
31
+ italicTokenizer,
32
+ )
33
+
34
+ this.parser.use(
35
+ textProcessor,
36
+ imageProcessor,
37
+ linkProcessor,
38
+ codeProcessor,
39
+ italicProcessor,
40
+ boldProcessor,
41
+ textLineProcessor,
42
+ headingProcessor,
43
+ codeblockProcessor,
44
+ listProcessor,
45
+ alertProcessor,
46
+ blockquoteProcessor,
47
+ horizontalRuleProcessor
48
+ )
49
+ }
50
+
51
+ parseHTML(str: string) {
52
+ return this.parser.parse(this.lexer.blockTokenize(str));
53
+ }
54
+ }
@@ -0,0 +1,111 @@
1
+ import { BLOCK, EMPTY_LINE, INLINE_CONTENT, INLINE_TEXT, TEXT_LINE } from "#lib/type";
2
+ import { forEach, isString } from "amateras/lib/native";
3
+
4
+ export class MarkdownLexer {
5
+ blockTokenizers = new Map<string, BlockTokenizer>();
6
+ inlineTokenizers = new Map<string, InlineTokenizer>();
7
+
8
+ blockTokenize(str: string) {
9
+ const lines = str.split(/\r?\n/);
10
+ const tokens: BlockToken[] = [];
11
+ let lineIndex = 0;
12
+ lineLoop: while (lineIndex < lines.length) {
13
+ let line = lines[lineIndex];
14
+ if (line === undefined) throw 'LINE ERROR';
15
+ let token: BlockToken | undefined;
16
+ for (const [type, tokenizer] of this.blockTokenizers) {
17
+ const matched = line.match(tokenizer.regex);
18
+ if (matched) {
19
+ const {content, multiLine, data} = tokenizer.handle(matched, lineIndex, lines);
20
+ token = { layout: BLOCK, type, content, data }
21
+ if (multiLine) {
22
+ tokens.push(token);
23
+ tokens.push(...multiLine.tokens)
24
+ lineIndex = multiLine.skip;
25
+ continue lineLoop;
26
+ }
27
+ break;
28
+ }
29
+ }
30
+ if (!token) token = {
31
+ layout: BLOCK,
32
+ ...(
33
+ line.length
34
+ ? { type: TEXT_LINE, content: this.inlineTokenize(line) }
35
+ : { type: EMPTY_LINE, content: [] }
36
+ )
37
+ };
38
+ tokens.push(token);
39
+ lineIndex++;
40
+ }
41
+ return tokens;
42
+ }
43
+
44
+ inlineTokenize(str: string): InlineToken[] {
45
+ const tokens: InlineToken[] = [];
46
+ let remainStr = str;
47
+ while (remainStr.length) {
48
+ let token: InlineToken | undefined;
49
+ for (const [type, tokenizer] of this.inlineTokenizers) {
50
+ const matched = remainStr.match(tokenizer.regex);
51
+ if (matched) {
52
+ const {index, 0: matchStr} = matched;
53
+ // handle before matched string
54
+ if (index != 0) tokens.push(...this.inlineTokenize(remainStr.substring(0, index)));
55
+ // handle matched string
56
+ const {content, data} = tokenizer.handle(matched);
57
+ token = { type, ...(isString(content) ? { layout: INLINE_TEXT, text: content } : { layout: INLINE_CONTENT, content })};
58
+ if (data) token.data = data;
59
+ remainStr = remainStr.substring(index! + matchStr.length);
60
+ break;
61
+ }
62
+ }
63
+ if (!token) {
64
+ token = { type: 'TEXT', layout: INLINE_TEXT, text: remainStr };
65
+ remainStr = '';
66
+ }
67
+ tokens.push(token);
68
+ }
69
+ return tokens;
70
+ }
71
+
72
+ use(...handle: ((parser: this) => void)[]) {
73
+ forEach(handle, fn => fn(this));
74
+ return this;
75
+ }
76
+ }
77
+
78
+ export type BlockTokenizer = {
79
+ regex: RegExp;
80
+ handle: (matches: RegExpMatchArray, position: number, lines: string[]) => { content: (InlineToken | BlockToken)[], multiLine?: BlockTokenizerMultiLine, data?: {[key: string]: any} };
81
+ }
82
+ export type BlockTokenizerMultiLine = {
83
+ skip: number;
84
+ tokens: BlockToken[];
85
+ }
86
+ export type InlineTokenizer = {
87
+ regex: RegExp;
88
+ handle: (matches: RegExpMatchArray) => { content: InlineToken[] | string, data?: {[key: string]: any} }
89
+ }
90
+
91
+ export interface TokenBase {
92
+ type: string;
93
+ layout: 'BLOCK' | 'INLINE_CONTENT' | 'INLINE_TEXT';
94
+ content?: Token[];
95
+ text?: string;
96
+ data?: {[key: string]: any};
97
+ }
98
+ export interface BlockToken extends TokenBase {
99
+ layout: 'BLOCK'
100
+ content: Token[];
101
+ }
102
+ export interface InlineTextToken extends TokenBase {
103
+ layout: 'INLINE_TEXT'
104
+ text: string;
105
+ }
106
+ export interface InlineContentToken extends TokenBase {
107
+ layout: 'INLINE_CONTENT'
108
+ content: (InlineToken)[];
109
+ }
110
+ export type InlineToken = InlineTextToken | InlineContentToken;
111
+ export type Token = BlockToken | InlineToken;
@@ -0,0 +1,33 @@
1
+ import { forEach, isString } from "../../../../src/lib/native";
2
+ import { type Token } from "./MarkdownLexer";
3
+
4
+ export class MarkdownParser {
5
+ processors = new Map<string, MarkdownParseProcessor>();
6
+
7
+ parse(tokens: (Token)[]) {
8
+ let html = '';
9
+ let i = 0;
10
+ while (i < tokens.length) {
11
+ const token = tokens[i]!;
12
+ const processor = this.processors.get(token.type);
13
+ if (processor) {
14
+ const result = processor(token, tokens.slice(i));
15
+ if (isString(result)) {
16
+ html += result;
17
+ } else {
18
+ html += result.html;
19
+ i += result.skipTokens;
20
+ }
21
+ }
22
+ i++;
23
+ }
24
+ return html;
25
+ }
26
+
27
+ use(...handle: ((parser: this) => void)[]) {
28
+ forEach(handle, fn => fn(this));
29
+ return this;
30
+ }
31
+ }
32
+
33
+ export type MarkdownParseProcessor = (token: Token, tokens: Token[]) => (string | { html: string, skipTokens: number })