amateras 0.3.0 → 0.4.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 +6 -4
- package/ext/css/README.md +19 -0
- package/ext/css/src/index.ts +347 -331
- package/ext/css/src/lib/colorAssign.ts +1 -1
- package/ext/css/src/structure/$CSSContainerRule.ts +13 -0
- package/ext/css/src/structure/$CSSRule.ts +1 -1
- package/ext/css/src/structure/$CSSStyleRule.ts +0 -7
- package/ext/css/src/structure/$CSSVariable.ts +3 -3
- package/ext/html/html.ts +1 -13
- package/ext/i18n/README.md +53 -0
- package/ext/i18n/package.json +10 -0
- package/ext/i18n/src/index.ts +54 -0
- package/ext/i18n/src/node/I18nText.ts +35 -0
- package/ext/i18n/src/structure/I18n.ts +40 -0
- package/ext/i18n/src/structure/I18nDictionary.ts +31 -0
- package/ext/markdown/index.ts +123 -0
- package/ext/router/index.ts +8 -1
- package/ext/router/node/Page.ts +1 -0
- package/ext/router/node/Route.ts +2 -1
- package/ext/router/node/Router.ts +33 -22
- package/ext/ssr/index.ts +4 -2
- package/ext/ui/lib/VirtualScroll.ts +24 -0
- package/ext/ui/node/Accordian.ts +97 -0
- package/ext/ui/node/Tabs.ts +114 -0
- package/ext/ui/node/Toast.ts +16 -0
- package/ext/ui/node/Waterfall.ts +73 -0
- package/ext/ui/package.json +11 -0
- package/package.json +4 -7
- package/src/core.ts +21 -8
- package/src/lib/assign.ts +8 -9
- package/src/lib/assignHelper.ts +1 -1
- package/src/lib/chain.ts +3 -0
- package/src/lib/debounce.ts +7 -0
- package/src/lib/env.ts +2 -0
- package/src/lib/native.ts +22 -35
- package/src/lib/randomId.ts +1 -1
- package/src/lib/sleep.ts +1 -1
- package/src/node/$Element.ts +182 -20
- package/src/node/$HTMLElement.ts +24 -0
- package/src/node/$Node.ts +75 -52
- package/src/node/$Virtual.ts +58 -0
- package/src/{node/node.ts → node.ts} +2 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { _Object_assign } from "../../../../src/lib/native";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export const colorAssign = (key: string, colors: {[key: number]: string}) => {
|
|
4
4
|
if (!$.color) _Object_assign($, {color: {}});
|
|
5
5
|
_Object_assign($.color, {[key]: colors})
|
|
6
6
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { _Array_from, _instanceof } from "amateras/lib/native";
|
|
2
|
+
import { $CSSRule } from "#structure/$CSSRule";
|
|
3
|
+
|
|
4
|
+
export class $CSSContainerRule extends $CSSRule {
|
|
5
|
+
condition: string;
|
|
6
|
+
name: string;
|
|
7
|
+
constructor(selector: string) {
|
|
8
|
+
super(selector);
|
|
9
|
+
const [_, name, condition] = selector.match(/@container (.+?) (.+)/) as [string, string, string]
|
|
10
|
+
this.name = name;
|
|
11
|
+
this.condition = condition;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -8,6 +8,6 @@ export abstract class $CSSRule {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
get options(): {[key: string]: any} {
|
|
11
|
-
return _Object_fromEntries(_Array_from(this.rules).map(rule => [rule.selector, rule]))
|
|
11
|
+
return _Object_fromEntries(_Array_from(this.rules).map(rule => [rule.selector, rule.options]))
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -8,13 +8,6 @@ export class $CSSStyleRule extends $CSSRule {
|
|
|
8
8
|
super(selector);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
clone(selector: string) {
|
|
12
|
-
const rule = new $CSSStyleRule(selector)
|
|
13
|
-
rule.declarations = this.declarations;
|
|
14
|
-
rule.rules = this.rules;
|
|
15
|
-
return rule
|
|
16
|
-
}
|
|
17
|
-
|
|
18
11
|
get options(): {[key: string]: any} {
|
|
19
12
|
return {..._Object_fromEntries(_Array_from(this.declarations).map(([_, dec]) => [dec.key, dec])), ...super.options}
|
|
20
13
|
}
|
package/ext/html/html.ts
CHANGED
|
@@ -10,16 +10,4 @@ import './node/$Media';
|
|
|
10
10
|
import './node/$OptGroup';
|
|
11
11
|
import './node/$Option';
|
|
12
12
|
import './node/$Select';
|
|
13
|
-
import './node/$TextArea';
|
|
14
|
-
export * from './node/$Anchor';
|
|
15
|
-
export * from './node/$Canvas';
|
|
16
|
-
export * from './node/$Dialog';
|
|
17
|
-
export * from './node/$Form';
|
|
18
|
-
export * from './node/$Image';
|
|
19
|
-
export * from './node/$Input';
|
|
20
|
-
export * from './node/$Label';
|
|
21
|
-
export * from './node/$Media';
|
|
22
|
-
export * from './node/$OptGroup';
|
|
23
|
-
export * from './node/$Option';
|
|
24
|
-
export * from './node/$Select';
|
|
25
|
-
export * from './node/$TextArea';
|
|
13
|
+
import './node/$TextArea';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# amateras/i18n
|
|
2
|
+
|
|
3
|
+
## Usage
|
|
4
|
+
```ts
|
|
5
|
+
import 'amateras';
|
|
6
|
+
import 'amateras/i18n';
|
|
7
|
+
|
|
8
|
+
const $t = $.i18n()
|
|
9
|
+
// add 'en' locale dictionary context
|
|
10
|
+
.add('en', {
|
|
11
|
+
homepage: {
|
|
12
|
+
_: 'Home',
|
|
13
|
+
hello: 'Hello, $name$!',
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
// set 'en' as locale language
|
|
17
|
+
.locale('en')
|
|
18
|
+
|
|
19
|
+
$(document.body).content([
|
|
20
|
+
$('h1').content( $t('homepage') )
|
|
21
|
+
// <h1><text>Home</text></h1>
|
|
22
|
+
$t('homepage.hello', {name: 'Amateras'})
|
|
23
|
+
// <text>Hello, Amateras!</text>
|
|
24
|
+
])
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Change Language
|
|
28
|
+
```ts
|
|
29
|
+
$t.locale('zh')
|
|
30
|
+
// all translation text will be updated
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Import Dictionary Context
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// ./i18n/en.ts
|
|
37
|
+
export default {
|
|
38
|
+
hello: 'Hello, $name$!'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ./i18n/zh.ts
|
|
42
|
+
export default {
|
|
43
|
+
hello: '您好,$name$!'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ./entry_file.ts
|
|
47
|
+
const $t = $.i18n()
|
|
48
|
+
.add('en', () => import('./i18n/en.ts'))
|
|
49
|
+
.add('zh', () => import('./i18n/zh.ts'))
|
|
50
|
+
// set 'zh' as locale language
|
|
51
|
+
// and fetch file automatically from path
|
|
52
|
+
.locale('zh');
|
|
53
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { _Array_from, _instanceof, _Object_assign } from "amateras/lib/native"
|
|
2
|
+
import { $ } from "amateras/core"
|
|
3
|
+
import { I18n } from "#structure/I18n"
|
|
4
|
+
import type { I18nText as _I18nText, I18nTextOptions } from "#node/I18nText";
|
|
5
|
+
import { I18nDictionary, type I18nDictionaryContext, type I18nDictionaryContextImporter } from "#structure/I18nDictionary";
|
|
6
|
+
|
|
7
|
+
_Object_assign($, {
|
|
8
|
+
i18n(defaultLocale: string) {
|
|
9
|
+
const i18n = new I18n(defaultLocale);
|
|
10
|
+
const i18nFn = (key: string, options?: I18nTextOptions) => i18n.translate(key, options);
|
|
11
|
+
_Object_assign(i18nFn, {
|
|
12
|
+
i18n,
|
|
13
|
+
locale(locale: string) {
|
|
14
|
+
if (!arguments.length) return i18n.locale();
|
|
15
|
+
i18n.locale(locale);
|
|
16
|
+
return this;
|
|
17
|
+
},
|
|
18
|
+
add(lang: string, context: I18nDictionaryContext | I18nDictionaryContextImporter) {
|
|
19
|
+
i18n.map.set(lang, new I18nDictionary(context));
|
|
20
|
+
return this;
|
|
21
|
+
},
|
|
22
|
+
delete(lang: string) {
|
|
23
|
+
i18n.map.delete(lang);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
return i18nFn
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
type ResolvedAsyncDictionary<F extends I18nDictionaryContextImporter> = Awaited<ReturnType<F>>['default'];
|
|
32
|
+
|
|
33
|
+
type DeepKeys<T> = T extends I18nDictionaryContext
|
|
34
|
+
? {
|
|
35
|
+
[K in keyof T]: K extends string
|
|
36
|
+
? K extends '_' ? never : `${K}` | `${K}.${DeepKeys<T[K]>}`
|
|
37
|
+
: never;
|
|
38
|
+
}[keyof T]
|
|
39
|
+
: never;
|
|
40
|
+
|
|
41
|
+
declare module "amateras/core" {
|
|
42
|
+
export namespace $ {
|
|
43
|
+
export interface I18nFunction<D extends I18nDictionaryContext = {}> {
|
|
44
|
+
(path: DeepKeys<D>, ...args: any[]): I18nText;
|
|
45
|
+
i18n: I18n;
|
|
46
|
+
locale(): string;
|
|
47
|
+
locale(lang?: $Parameter<string>): this;
|
|
48
|
+
add<F extends I18nDictionaryContext | I18nDictionaryContextImporter>(lang: string, dictionary: F): I18nFunction<D | (F extends I18nDictionaryContextImporter ? ResolvedAsyncDictionary<F> : F)>;
|
|
49
|
+
delete(lang: string): this;
|
|
50
|
+
}
|
|
51
|
+
export function i18n(defaultLocale: string): I18nFunction;
|
|
52
|
+
export type I18nText = _I18nText;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { _Array_from, isUndefined } from "amateras/lib/native";
|
|
2
|
+
import { $HTMLElement } from "amateras/node/$HTMLElement";
|
|
3
|
+
import type { I18n } from "#structure/I18n";
|
|
4
|
+
|
|
5
|
+
export class I18nText extends $HTMLElement<HTMLElement, { i18nupdate: Event }> {
|
|
6
|
+
i18n: I18n;
|
|
7
|
+
key: string;
|
|
8
|
+
options: I18nTextOptions | undefined;
|
|
9
|
+
constructor(i18n: I18n, key: string, options?: I18nTextOptions) {
|
|
10
|
+
super('text');
|
|
11
|
+
this.i18n = i18n;
|
|
12
|
+
this.key = key;
|
|
13
|
+
this.options = options;
|
|
14
|
+
i18n.locale$.signal.subscribe(() => this.update())
|
|
15
|
+
this.update();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async update() {
|
|
19
|
+
update: {
|
|
20
|
+
const {key, i18n} = this;
|
|
21
|
+
const dictionary = i18n.dictionary();
|
|
22
|
+
if (!dictionary) {this.content(key); break update}
|
|
23
|
+
const target = await dictionary.find(key);
|
|
24
|
+
if (isUndefined(target)) break update;
|
|
25
|
+
const snippets = target.split(/\$[a-zA-Z0-9_]+\$/);
|
|
26
|
+
if (snippets.length === 1 || !this.options) {this.content(target); break update}
|
|
27
|
+
const matches = target.matchAll(/(\$([a-zA-Z0-9_]+)\$)/g);
|
|
28
|
+
this.content(snippets.map(text => [text, this.options?.[matches.next().value?.at(2)!] ?? null]));
|
|
29
|
+
}
|
|
30
|
+
this.dispatchEvent(new Event('i18nupdate'));
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type I18nTextOptions = {[key: string]: any}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { _instanceof } from "amateras/lib/native";
|
|
2
|
+
import { I18nText, type I18nTextOptions } from "#node/I18nText";
|
|
3
|
+
import { I18nDictionary } from "#structure/I18nDictionary";
|
|
4
|
+
|
|
5
|
+
export class I18n {
|
|
6
|
+
locale$ = $.signal<string>('');
|
|
7
|
+
map = new Map<string, I18nDictionary>();
|
|
8
|
+
#defaultLocale: string;
|
|
9
|
+
constructor(defaultLocale: string) {
|
|
10
|
+
this.#defaultLocale = defaultLocale;
|
|
11
|
+
this.locale$.set(defaultLocale);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
defaultLocale(): string;
|
|
15
|
+
defaultLocale(locale: string): this;
|
|
16
|
+
defaultLocale(locale?: string) {
|
|
17
|
+
if (!arguments.length) return this.#defaultLocale;
|
|
18
|
+
if (locale) this.locale$.set(locale);
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
locale(): string;
|
|
23
|
+
locale(locale: string): this;
|
|
24
|
+
locale(locale?: string) {
|
|
25
|
+
if (!arguments.length) return this.locale$();
|
|
26
|
+
if (locale) this.locale$.set(locale)
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
dictionary(locale = this.locale$()) {
|
|
31
|
+
if (!locale) return null;
|
|
32
|
+
const dictionary = this.map.get(locale);
|
|
33
|
+
return dictionary;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
translate(key: string, options?: I18nTextOptions) {
|
|
37
|
+
return new I18nText(this, key, options);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { _instanceof, isObject } from "amateras/lib/native";
|
|
2
|
+
|
|
3
|
+
export class I18nDictionary {
|
|
4
|
+
#context: I18nDictionaryContext | Promise<I18nDictionaryContext> | null = null;
|
|
5
|
+
#fetch: I18nDictionaryContextImporter | null = null;
|
|
6
|
+
constructor(resolver: I18nDictionaryContext | I18nDictionaryContextImporter) {
|
|
7
|
+
if (_instanceof(resolver, Function)) this.#fetch = resolver;
|
|
8
|
+
else this.#context = resolver;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async context(): Promise<I18nDictionaryContext> {
|
|
12
|
+
if (this.#context) return await this.#context;
|
|
13
|
+
if (!this.#fetch) throw 'I18n Context Fetch Error';
|
|
14
|
+
return this.#context = this.#fetch().then((module) => module.default);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async find(path: string, context?: I18nDictionaryContext): Promise<string | undefined> {
|
|
18
|
+
if (!context) context = await this.context();
|
|
19
|
+
const [snippet, ...rest] = path.split('.') as [string, ...string[]];
|
|
20
|
+
const target = context[snippet];
|
|
21
|
+
if (isObject(target)) {
|
|
22
|
+
if (rest.length) return this.find(rest.join('.'), target);
|
|
23
|
+
else return target['_'] as string;
|
|
24
|
+
}
|
|
25
|
+
if (rest.length) return path;
|
|
26
|
+
else return target;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type I18nDictionaryContext = {[key: string]: string | I18nDictionaryContext}
|
|
31
|
+
export type I18nDictionaryContextImporter = () => Promise<{default: I18nDictionaryContext}>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { _Array_from, forEach } from "#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) => {
|
|
54
|
+
return replaceAll(str, /(?:.+?\n?)+/gm, $0 => `<p>${trim($0)}</p>`)
|
|
55
|
+
}
|
|
56
|
+
// Headings
|
|
57
|
+
blockProcess({
|
|
58
|
+
regexp: /^(#+) (.+)/gm,
|
|
59
|
+
handle: text => replaceAll(text, /^(#+) (.+)/gm, (_, $1: string, $2) => `<h${$1.length}>${$2}</h${$1.length}>`)
|
|
60
|
+
});
|
|
61
|
+
blockProcess({
|
|
62
|
+
regexp: /^(.+)\n==+$/gm,
|
|
63
|
+
handle: text => replaceAll(text, /^(.+)\n==+$/gm, (_, $1) => `<h1>${$1}</h1>`)
|
|
64
|
+
});
|
|
65
|
+
blockProcess({
|
|
66
|
+
regexp: /^(.+)\n--+$/gm,
|
|
67
|
+
handle: text => replaceAll(text, /^(.+)\n--+$/gm, (_, $1) => `<h2>${$1}</h2>`)
|
|
68
|
+
});
|
|
69
|
+
// Blockquote
|
|
70
|
+
blockProcess({
|
|
71
|
+
regexp: /(?:^> ?.*(?:\n|$))+/gm,
|
|
72
|
+
handle: text => {
|
|
73
|
+
const fn = (str: string) => {
|
|
74
|
+
const blocks = _Array_from(str.matchAll(/(?:^> ?.*(?:\n|$))+/gm));
|
|
75
|
+
forEach(blocks, block => {
|
|
76
|
+
const blocked = fn(replaceAll(block[0], /^> ?/gm, ''));
|
|
77
|
+
str = str.replace(block[0], `<blockquote>\n${paragraph(blocked)}\n</blockquote>`);
|
|
78
|
+
})
|
|
79
|
+
return str;
|
|
80
|
+
}
|
|
81
|
+
return fn(text);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// List
|
|
85
|
+
blockProcess({
|
|
86
|
+
regexp: /(?:^(?:\t|(?: )+)?(?:-|[0-9]+\.) (?:.+\n?))+/gm,
|
|
87
|
+
handle: text => {
|
|
88
|
+
const fn = (str: string) => {
|
|
89
|
+
const blocks = _Array_from(str.matchAll(/(?:^(?:\t|(?: )+)?(?:-|[0-9]+\.) (?:.+\n?))+/gm));
|
|
90
|
+
forEach(blocks, block => {
|
|
91
|
+
let haveList = false // check this loop have list
|
|
92
|
+
const type = block[0].match(/^(-|[0-9]+\.) /)?.[1] === '-' ? 'ul' : 'ol';
|
|
93
|
+
const listed = replaceAll(block[0], /^(?:-|[0-9]+\.) (.+)/gm, (_, $1: string) => (haveList = true, `<li>\n${trim($1)}\n</li>`));
|
|
94
|
+
const clearTabbed = replaceAll(listed, /^(?:\t|(?: ))/gm, '');
|
|
95
|
+
const convertedList = fn(clearTabbed);
|
|
96
|
+
str = str.replace(block[0], haveList ? `<${type}>\n${trim(convertedList)}\n</${type}>` : convertedList);
|
|
97
|
+
})
|
|
98
|
+
return str;
|
|
99
|
+
}
|
|
100
|
+
return fn(text);
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
// Codeblock
|
|
104
|
+
blockProcess({
|
|
105
|
+
regexp: /^```([^`\n]+)\n([^`]+)?```/gm,
|
|
106
|
+
handle: text => replaceAll(text, /^```([^`\n]+)\n([^`]+)?```/gm, (_, $1, $2: string) => `<pre><code>\n${trim($2)}\n</code></pre>`)
|
|
107
|
+
})
|
|
108
|
+
// Horizontal Rule
|
|
109
|
+
blockProcess({
|
|
110
|
+
regexp: /^(?:---|\*\*\*|___)(\s+)?$/gm,
|
|
111
|
+
handle: text => replaceAll(text, /^(?:---|\*\*\*|___)(\s+)?$/gm, _ => `<hr>`)
|
|
112
|
+
})
|
|
113
|
+
// Bold
|
|
114
|
+
inlineProcess(text => replaceAll(text, /\*\*([^*]+?)\*\*/g, (_, $1) => `<b>${$1}</b>`));
|
|
115
|
+
// Italic
|
|
116
|
+
inlineProcess(text => replaceAll(text, /\*([^*]+?)\*/g, (_, $1) => `<i>${$1}</i>`));
|
|
117
|
+
// Image
|
|
118
|
+
inlineProcess(text => replaceAll(text, /!\[(.+?)\]\((.+?)(?: "(.+?)?")?\)/g, (_, alt, src, title) => `<img src="${src}" alt="${alt}"${title ? ` title="${title}"` : ''}>`));
|
|
119
|
+
// Link
|
|
120
|
+
inlineProcess(text => replaceAll(text, /\[(.+?)\]\((?:(\w\w+?:[^\s]+?)(?: "(.+?)?")?)\)/g, (_, content, href, title) => `<a href="${href}"${title ? ` title="${title}"` : ''}>${content}</a>`));
|
|
121
|
+
inlineProcess(text => replaceAll(text, /\[(.+?)\]\((?:(\w+?@(?:\w|\.\w)+?)(?: "(.+)?")?)\)/g, (_, content, mail, title) => `<a href="mailto:${mail}"${title ? ` title="${title}"` : ''}>${content}</a>`));
|
|
122
|
+
inlineProcess(text => replaceAll(text, /<(\w\w+?:[^\s]+?)>/g, (_, href) => `<a href="${href}">${href}</a>`));
|
|
123
|
+
inlineProcess(text => replaceAll(text, /<(\w+?@(?:\w|\.\w)+?)>/g, (_, mail) => `<a href="mailto:${mail}">${mail}</a>`));
|
package/ext/router/index.ts
CHANGED
|
@@ -21,6 +21,13 @@ declare module 'amateras/core' {
|
|
|
21
21
|
export function forward(): typeof Router;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
declare global {
|
|
26
|
+
interface GlobalEventHandlersEventMap {
|
|
27
|
+
'routeopen': Event;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
// assign methods
|
|
25
32
|
_Object_assign($, {
|
|
26
33
|
open: Router.open.bind(Router),
|
|
@@ -32,7 +39,7 @@ _Object_assign($, {
|
|
|
32
39
|
forEach([
|
|
33
40
|
`router{display:block}`,
|
|
34
41
|
`page{display:block}`
|
|
35
|
-
],
|
|
42
|
+
], $.style);
|
|
36
43
|
// assign nodes
|
|
37
44
|
$.assign([
|
|
38
45
|
['router', Router],
|
package/ext/router/node/Page.ts
CHANGED
|
@@ -9,6 +9,7 @@ export class Page<R extends Route<any> = any, Data extends RouteData = any> exte
|
|
|
9
9
|
params: Data['params'];
|
|
10
10
|
query: Data['query'];
|
|
11
11
|
#pageTitle: null | string = null;
|
|
12
|
+
initial = false;
|
|
12
13
|
constructor(route: R, data?: {params: any, query: any}) {
|
|
13
14
|
super('page');
|
|
14
15
|
this.route = route;
|
package/ext/router/node/Route.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _instanceof, _Object_fromEntries, _Array_from } from "#lib/native";
|
|
2
|
-
import { $Element } from "#node
|
|
2
|
+
import { $Element } from "#node/$Element";
|
|
3
3
|
import type { AsyncRoute, RouteBuilder, RouteDataResolver } from "..";
|
|
4
4
|
import { Page } from "./Page";
|
|
5
5
|
|
|
@@ -37,6 +37,7 @@ export class Route<Path extends string = string> extends BaseRouteNode<Path> {
|
|
|
37
37
|
async build(data: {params: any, query: any} = {params: {}, query: {}}, page?: Page) {
|
|
38
38
|
page = page ?? new Page(this, data);
|
|
39
39
|
page.params = data.params;
|
|
40
|
+
page.initial = true;
|
|
40
41
|
let resolver: any = this.builder(page);
|
|
41
42
|
if (_instanceof(resolver, Promise)) {
|
|
42
43
|
const result = await resolver as any;
|
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
import type { AnchorTarget } from "#html/$Anchor";
|
|
2
|
-
import {
|
|
2
|
+
import { _document } from "#lib/env";
|
|
3
|
+
import { _Array_from, _instanceof, _JSON_parse, _JSON_stringify, _Object_entries, _Object_fromEntries, forEach, startsWith } from "#lib/native";
|
|
3
4
|
import { Page } from "./Page";
|
|
4
5
|
import { BaseRouteNode, Route } from "./Route";
|
|
5
6
|
|
|
6
7
|
// history index
|
|
7
8
|
let index = 0;
|
|
9
|
+
const _addEventListener = addEventListener;
|
|
8
10
|
const _location = location;
|
|
9
11
|
const {origin} = _location;
|
|
10
12
|
const _history = history;
|
|
13
|
+
const _sessionStorage = sessionStorage;
|
|
11
14
|
const documentElement = _document.documentElement;
|
|
12
15
|
const [PUSH, REPLACE] = [1, 2] as const;
|
|
13
16
|
const [FORWARD, BACK] = ['forward', 'back'] as const;
|
|
14
|
-
|
|
15
|
-
// disable browser scroll restoration
|
|
16
|
-
_history.scrollRestoration = 'manual';
|
|
17
|
-
|
|
17
|
+
const scrollStorageKey = '__scroll__';
|
|
18
18
|
/** convert path string to URL object */
|
|
19
19
|
const toURL = (path: string | URL) =>
|
|
20
|
-
_instanceof(path, URL) ? path :
|
|
20
|
+
_instanceof(path, URL) ? path : startsWith(path, 'http') ? new URL(path) : new URL(startsWith(path, origin) ? path : origin + path);
|
|
21
21
|
|
|
22
|
+
type ScrollData = {[key: number]: {x: number, y: number}};
|
|
23
|
+
const scrollRecord = (e?: Event) => {
|
|
24
|
+
const data = _JSON_parse(_sessionStorage.getItem(scrollStorageKey) ?? '{}') as ScrollData;
|
|
25
|
+
data[index] = { x: documentElement.scrollLeft, y: documentElement.scrollTop };
|
|
26
|
+
// e is Event when called from scroll or beforeload
|
|
27
|
+
if (!e) forEach(_Object_entries(data), ([i]) => +i > index && delete data[+i])
|
|
28
|
+
_sessionStorage.setItem(scrollStorageKey, _JSON_stringify(data));
|
|
29
|
+
}
|
|
22
30
|
/** handle history state with push and replace state. */
|
|
23
31
|
const historyHandler = async (path: string | URL | Nullish, mode: 1 | 2, target?: AnchorTarget) => {
|
|
24
32
|
if (!path) return;
|
|
@@ -26,16 +34,14 @@ const historyHandler = async (path: string | URL | Nullish, mode: 1 | 2, target?
|
|
|
26
34
|
if (url.href === _location.href) return;
|
|
27
35
|
if (target && target !== '_self') return open(url, target);
|
|
28
36
|
if (url.origin !== origin) return open(url, target);
|
|
29
|
-
|
|
30
|
-
index: index,
|
|
31
|
-
x: documentElement.scrollLeft,
|
|
32
|
-
y: documentElement.scrollTop
|
|
33
|
-
}, '', _location.href);
|
|
37
|
+
scrollRecord();
|
|
34
38
|
if (mode === PUSH) index += 1;
|
|
35
39
|
Router.direction = FORWARD;
|
|
36
|
-
|
|
37
|
-
for (let router of Router.routers) router.routes.size && await router.resolve(path)
|
|
40
|
+
_history[mode === PUSH ? 'pushState' : 'replaceState']({index}, '' , url);
|
|
41
|
+
for (let router of Router.routers) router.routes.size && await router.resolve(path);
|
|
38
42
|
}
|
|
43
|
+
// disable browser scroll restoration
|
|
44
|
+
_history.scrollRestoration = 'manual';
|
|
39
45
|
|
|
40
46
|
export class Router extends BaseRouteNode<''> {
|
|
41
47
|
static pageRouters = new Map<Page, Router>();
|
|
@@ -88,7 +94,7 @@ export class Router extends BaseRouteNode<''> {
|
|
|
88
94
|
if (routeSnippet.includes(':')) {
|
|
89
95
|
if (targetSnippet === '/') continue routeLoop;
|
|
90
96
|
const [prefix, paramName] = routeSnippet.split(':') as [string, string];
|
|
91
|
-
if (!
|
|
97
|
+
if (!startsWith(targetSnippet, prefix)) continue routeLoop;
|
|
92
98
|
routeData.params[paramName] = targetSnippet.replace(`${prefix}`, '');
|
|
93
99
|
pass();
|
|
94
100
|
continue splitLoop;
|
|
@@ -105,21 +111,20 @@ export class Router extends BaseRouteNode<''> {
|
|
|
105
111
|
const targetRoutes = determineRoute(this, pathname + '/', hash);
|
|
106
112
|
// build pages
|
|
107
113
|
let prevPage: null | Page = null, prevRoute: BaseRouteNode<any> = this;
|
|
114
|
+
const appendPage = (prevRouter: Router | undefined, page: Page) => page.parentNode !== prevRouter?.node && prevRouter?.content(page);
|
|
115
|
+
|
|
108
116
|
for (const [route, pathId] of targetRoutes) {
|
|
109
117
|
const page = this.pageMap.get(pathId) ?? new Page(route ?? prevRoute.routes.get('404') ?? new Route('404', () => null), routeData);
|
|
110
|
-
await route?.build(routeData, page);
|
|
118
|
+
if (!page.initial) await route?.build(routeData, page);
|
|
111
119
|
_document && (_document.title = page.pageTitle() ?? _document.title);
|
|
112
120
|
this.pageMap.set(pathId, page);
|
|
113
|
-
|
|
114
|
-
if (href === _location.href) {
|
|
115
|
-
if (prevPage) Router.pageRouters.get(prevPage)?.content(page);
|
|
116
|
-
else this.content(page);
|
|
117
|
-
}
|
|
121
|
+
if (href === _location.href) appendPage(prevPage ? Router.pageRouters.get(prevPage) : this, page);
|
|
118
122
|
prevPage = page;
|
|
119
123
|
if (route) prevRoute = route;
|
|
120
124
|
}
|
|
121
|
-
let { x, y } =
|
|
125
|
+
let { x, y } = Router.scroll ?? {x: 0, y: 0};
|
|
122
126
|
scrollTo(x, y);
|
|
127
|
+
this.dispatchEvent(new Event('routeopen', {bubbles: true}));
|
|
123
128
|
return this;
|
|
124
129
|
}
|
|
125
130
|
|
|
@@ -131,8 +136,14 @@ export class Router extends BaseRouteNode<''> {
|
|
|
131
136
|
index = stateIndex;
|
|
132
137
|
this.resolve(_location.href);
|
|
133
138
|
}
|
|
134
|
-
|
|
139
|
+
_addEventListener('popstate', resolve);
|
|
140
|
+
_addEventListener('beforeunload', scrollRecord);
|
|
141
|
+
_addEventListener('scroll', scrollRecord, false);
|
|
135
142
|
resolve();
|
|
136
143
|
return this;
|
|
137
144
|
}
|
|
145
|
+
|
|
146
|
+
static get scroll(): ScrollData[number] {
|
|
147
|
+
return _JSON_parse(_sessionStorage.getItem(scrollStorageKey) ?? '{}')[index] ?? {x: 0, y: 0}
|
|
148
|
+
}
|
|
138
149
|
}
|
package/ext/ssr/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import './env';
|
|
2
2
|
import 'amateras';
|
|
3
|
-
import { _Array_from,
|
|
4
|
-
import { $Element
|
|
3
|
+
import { _Array_from, _instanceof, _Object_assign, _Object_defineProperty, forEach } from "amateras/lib/native";
|
|
4
|
+
import { $Element } from 'amateras/node/$Element';
|
|
5
5
|
import { BROWSER, NODE } from 'esm-env';
|
|
6
|
+
import { $Node, $Text } from 'amateras/node/$Node';
|
|
7
|
+
import { _document } from '../../src/lib/env';
|
|
6
8
|
|
|
7
9
|
declare module 'amateras/core' {
|
|
8
10
|
export namespace $ {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { _Array_from, _instanceof, forEach } from "amateras/lib/native";
|
|
2
|
+
import type { $Virtual } from "amateras/node/$Virtual";
|
|
3
|
+
import { $HTMLElement } from "amateras/node/$HTMLElement";
|
|
4
|
+
import { _document } from "../../../src/lib/env";
|
|
5
|
+
|
|
6
|
+
export function VirtualScroll($parent: $Virtual, scroller: Node = _document) {
|
|
7
|
+
scroller.addEventListener('scroll', render, true);
|
|
8
|
+
$parent.on('layout', render);
|
|
9
|
+
function render() {
|
|
10
|
+
const getRect = ($node: $HTMLElement) => $node.getBoundingClientRect();
|
|
11
|
+
const number = (str: string) => parseInt(str);
|
|
12
|
+
const parentRect = getRect($parent);
|
|
13
|
+
const children = _Array_from($parent.nodes);
|
|
14
|
+
forEach(children, $child => {
|
|
15
|
+
if (!_instanceof($child, $HTMLElement)) return;
|
|
16
|
+
const { top, height } = $child.style();
|
|
17
|
+
const topPos = parentRect.top + number(top);
|
|
18
|
+
const bottomPos = topPos + number(height);
|
|
19
|
+
if (bottomPos < 0 || topPos > innerHeight) $parent.hide($child);
|
|
20
|
+
else $parent.show($child);
|
|
21
|
+
})
|
|
22
|
+
$parent.render();
|
|
23
|
+
}
|
|
24
|
+
}
|