ezal-theme-example 0.0.1
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/LICENSE +21 -0
- package/assets/scripts/404.ts +353 -0
- package/assets/scripts/_article.ts +290 -0
- package/assets/scripts/_base.ts +65 -0
- package/assets/scripts/_pagefind.d.ts +424 -0
- package/assets/scripts/_search.ts +88 -0
- package/assets/scripts/_utils.ts +74 -0
- package/assets/scripts/archive.ts +143 -0
- package/assets/scripts/article.ts +18 -0
- package/assets/scripts/category.ts +4 -0
- package/assets/scripts/home.ts +73 -0
- package/assets/scripts/links.ts +14 -0
- package/assets/scripts/main.ts +11 -0
- package/assets/scripts/page.ts +11 -0
- package/assets/scripts/tag.ts +4 -0
- package/assets/scripts/tsconfig.json +10 -0
- package/assets/styles/404.styl +31 -0
- package/assets/styles/_article/fold.styl +15 -0
- package/assets/styles/_article/footnote.styl +12 -0
- package/assets/styles/_article/heading.styl +29 -0
- package/assets/styles/_article/image.styl +30 -0
- package/assets/styles/_article/kbd.styl +10 -0
- package/assets/styles/_article/links.styl +31 -0
- package/assets/styles/_article/list.styl +19 -0
- package/assets/styles/_article/note.styl +18 -0
- package/assets/styles/_article/other.styl +44 -0
- package/assets/styles/_article/table.styl +29 -0
- package/assets/styles/_article/tabs.styl +25 -0
- package/assets/styles/_code.styl +83 -0
- package/assets/styles/_index/contact.styl +20 -0
- package/assets/styles/_index/footer.styl +5 -0
- package/assets/styles/_index/header.styl +40 -0
- package/assets/styles/_index/nav.styl +59 -0
- package/assets/styles/_index/search.styl +64 -0
- package/assets/styles/_index.styl +91 -0
- package/assets/styles/_var.styl +96 -0
- package/assets/styles/archive.styl +35 -0
- package/assets/styles/article.styl +138 -0
- package/assets/styles/category.styl +4 -0
- package/assets/styles/home.styl +124 -0
- package/assets/styles/links.styl +121 -0
- package/assets/styles/page.styl +12 -0
- package/assets/styles/tag.styl +4 -0
- package/dist/config.d.ts +128 -0
- package/dist/feed.d.ts +4 -0
- package/dist/image/asset.d.ts +22 -0
- package/dist/image/db.d.ts +18 -0
- package/dist/image/index.d.ts +10 -0
- package/dist/image/metadata.d.ts +2 -0
- package/dist/image/utils.d.ts +1 -0
- package/dist/index-now.d.ts +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2066 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +2 -0
- package/dist/markdown/codeblock/data.d.ts +1 -0
- package/dist/markdown/codeblock/index.d.ts +6 -0
- package/dist/markdown/codeblock/style.d.ts +2 -0
- package/dist/markdown/fold.d.ts +6 -0
- package/dist/markdown/footnote.d.ts +15 -0
- package/dist/markdown/image.d.ts +12 -0
- package/dist/markdown/index.d.ts +2 -0
- package/dist/markdown/kbd.d.ts +6 -0
- package/dist/markdown/link.d.ts +2 -0
- package/dist/markdown/links.d.ts +12 -0
- package/dist/markdown/note.d.ts +8 -0
- package/dist/markdown/table.d.ts +3 -0
- package/dist/markdown/tabs.d.ts +7 -0
- package/dist/markdown/tex.d.ts +9 -0
- package/dist/page/404.d.ts +1 -0
- package/dist/page/archive.d.ts +1 -0
- package/dist/page/category.d.ts +1 -0
- package/dist/page/home.d.ts +2 -0
- package/dist/page/tag.d.ts +1 -0
- package/dist/pagefind.d.ts +20 -0
- package/dist/sitemap.d.ts +2 -0
- package/dist/transform/script.d.ts +2 -0
- package/dist/transform/stylus.d.ts +2 -0
- package/dist/utils.d.ts +2 -0
- package/layouts/404.tsx +8 -0
- package/layouts/archive.tsx +81 -0
- package/layouts/article.tsx +145 -0
- package/layouts/base.tsx +20 -0
- package/layouts/category.tsx +18 -0
- package/layouts/components/ArchiveArticleList.tsx +14 -0
- package/layouts/components/Article.tsx +46 -0
- package/layouts/components/Contact.tsx +14 -0
- package/layouts/components/Footer.tsx +44 -0
- package/layouts/components/Head.tsx +119 -0
- package/layouts/components/Image.tsx +42 -0
- package/layouts/components/Nav.tsx +33 -0
- package/layouts/components/Search.tsx +20 -0
- package/layouts/components/Waline.tsx +22 -0
- package/layouts/context.d.ts +54 -0
- package/layouts/home.tsx +74 -0
- package/layouts/links.tsx +53 -0
- package/layouts/page.tsx +19 -0
- package/layouts/tag.tsx +18 -0
- package/layouts/tsconfig.json +11 -0
- package/package.json +47 -0
- package/readme.md +17 -0
- package/readme_zh.md +17 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Article, URL } from 'ezal';
|
|
2
|
+
import type { Context } from 'ezal-markdown';
|
|
3
|
+
import base from './base';
|
|
4
|
+
import Image from './components/Image';
|
|
5
|
+
|
|
6
|
+
const { theme } = context;
|
|
7
|
+
const page = context.page as Article;
|
|
8
|
+
const markdown = page.renderedData as Context;
|
|
9
|
+
|
|
10
|
+
const categories = [
|
|
11
|
+
...page.categories.values().map((cate) => (
|
|
12
|
+
<a
|
|
13
|
+
class="link"
|
|
14
|
+
href={URL.for(URL.encode(`/category/${cate.path.join('/')}/`))}
|
|
15
|
+
>
|
|
16
|
+
{cate.path.join('/')}
|
|
17
|
+
</a>
|
|
18
|
+
)),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const tags = [
|
|
22
|
+
...page.tags.keys().map((tag) => (
|
|
23
|
+
<a class="link tag" href={URL.for(URL.encode(`/tag/${tag}/`))}>
|
|
24
|
+
{tag}
|
|
25
|
+
</a>
|
|
26
|
+
)),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const date = page.date.toPlainDate().toString();
|
|
30
|
+
const dateTime = page.date.toString({ timeZoneName: 'never' });
|
|
31
|
+
const update = page.updated.toPlainDate().toString();
|
|
32
|
+
const updatedTime = page.updated.toString({ timeZoneName: 'never' });
|
|
33
|
+
|
|
34
|
+
const toc = <aside />;
|
|
35
|
+
let current = toc;
|
|
36
|
+
let currentLevel = 0;
|
|
37
|
+
for (const item of markdown.toc.values()) {
|
|
38
|
+
const el = (
|
|
39
|
+
<li>
|
|
40
|
+
<a href={`#${item.anchor}`}>{item.name}</a>
|
|
41
|
+
</li>
|
|
42
|
+
);
|
|
43
|
+
if (currentLevel < item.level) {
|
|
44
|
+
const next = <ol />;
|
|
45
|
+
next.append(el);
|
|
46
|
+
current.append(next);
|
|
47
|
+
if (current === toc) next.attr.class = 'toc';
|
|
48
|
+
} else if (currentLevel === item.level) {
|
|
49
|
+
current.after(el);
|
|
50
|
+
} else {
|
|
51
|
+
while (currentLevel > item.level) {
|
|
52
|
+
current = current.parent.parent;
|
|
53
|
+
currentLevel--;
|
|
54
|
+
}
|
|
55
|
+
current.after(el);
|
|
56
|
+
}
|
|
57
|
+
current = el;
|
|
58
|
+
currentLevel = item.level;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const articles = Article.getAll().sort(context.compareByDate);
|
|
62
|
+
const index = articles.indexOf(page);
|
|
63
|
+
const prev: Article | undefined = articles[index - 1];
|
|
64
|
+
const next: Article | undefined = articles[index + 1];
|
|
65
|
+
|
|
66
|
+
/* const relatedMap = new Map<Article, number>();
|
|
67
|
+
for (const tag of page.tags.values()) {
|
|
68
|
+
for (const article of tag.getArticles()) {
|
|
69
|
+
if (article === page) continue;
|
|
70
|
+
const count = relatedMap.get(article);
|
|
71
|
+
relatedMap.set(article, (count ?? 0) + 1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const related = relatedMap
|
|
75
|
+
.entries()
|
|
76
|
+
.toArray()
|
|
77
|
+
.sort(([_, a], [__, b]) => b - a)
|
|
78
|
+
.slice(0, 6)
|
|
79
|
+
.map(([a]) => a);
|
|
80
|
+
|
|
81
|
+
const relatedElements: JSX.Element[] = [];
|
|
82
|
+
if (related.length > 0) {
|
|
83
|
+
relatedElements.push(<h2 class="related-title wrap">相关推荐</h2>);
|
|
84
|
+
relatedElements.push(
|
|
85
|
+
<div class="related wrap">
|
|
86
|
+
{related.map((article) => (
|
|
87
|
+
<a href={article.url}>{article.title}</a>
|
|
88
|
+
))}
|
|
89
|
+
</div>,
|
|
90
|
+
);
|
|
91
|
+
} */
|
|
92
|
+
|
|
93
|
+
export default base(
|
|
94
|
+
<header>
|
|
95
|
+
{page.data.cover ? <Image url={page.data.cover} alt={page.title} /> : null}
|
|
96
|
+
<div class="wrap">
|
|
97
|
+
<div class="cates">
|
|
98
|
+
{categories}
|
|
99
|
+
{tags}
|
|
100
|
+
</div>
|
|
101
|
+
<h1>{page.title}</h1>
|
|
102
|
+
<div class="article-info">
|
|
103
|
+
<span class="icon-word-count" title="字数">
|
|
104
|
+
{markdown.counter.value}
|
|
105
|
+
</span>
|
|
106
|
+
<span class="icon-timer" title="预计阅读时长">
|
|
107
|
+
~{markdown.counter.minute2read().toFixed(0)} 分钟
|
|
108
|
+
</span>
|
|
109
|
+
<time class="icon-date" title="发布日期" datetime={dateTime}>
|
|
110
|
+
{date}
|
|
111
|
+
</time>
|
|
112
|
+
<time class="icon-updated" title="更新日期" datetime={updatedTime}>
|
|
113
|
+
{update}
|
|
114
|
+
</time>
|
|
115
|
+
{theme.waline?.pageview && page.data?.comment !== false ? (
|
|
116
|
+
<span class="icon-hot waline-pageview-count" title="阅读量">
|
|
117
|
+
...
|
|
118
|
+
</span>
|
|
119
|
+
) : null}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</header>,
|
|
123
|
+
<main>
|
|
124
|
+
{toc}
|
|
125
|
+
<article>
|
|
126
|
+
<RawHTML html={page.content} />
|
|
127
|
+
</article>
|
|
128
|
+
</main>,
|
|
129
|
+
// ...relatedElements,
|
|
130
|
+
<div class="pagination wrap rounded">
|
|
131
|
+
{prev ? (
|
|
132
|
+
<a href={URL.for(prev.url)} class="prev">
|
|
133
|
+
<i class="icon-left"></i>
|
|
134
|
+
{prev.title}
|
|
135
|
+
</a>
|
|
136
|
+
) : null}
|
|
137
|
+
<div class="flex"></div>
|
|
138
|
+
{next ? (
|
|
139
|
+
<a href={URL.for(next.url)} class="next">
|
|
140
|
+
{next.title}
|
|
141
|
+
<i class="icon-right"></i>
|
|
142
|
+
</a>
|
|
143
|
+
) : null}
|
|
144
|
+
</div>,
|
|
145
|
+
);
|
package/layouts/base.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Footer from './components/Footer';
|
|
2
|
+
import Head from './components/Head';
|
|
3
|
+
import Nav from './components/Nav';
|
|
4
|
+
import Search from './components/Search';
|
|
5
|
+
import Waline from './components/Waline';
|
|
6
|
+
|
|
7
|
+
export default (...elements: JSX.Element[]) => (
|
|
8
|
+
<Doc lang={context.site.language}>
|
|
9
|
+
<head>
|
|
10
|
+
<Head />
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<Nav />
|
|
14
|
+
{elements}
|
|
15
|
+
<Search />
|
|
16
|
+
<Waline />
|
|
17
|
+
<Footer />
|
|
18
|
+
</body>
|
|
19
|
+
</Doc>
|
|
20
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import base from './base';
|
|
2
|
+
import ArchiveArticleList from './components/ArchiveArticleList';
|
|
3
|
+
import type { CategoryPage } from './context';
|
|
4
|
+
|
|
5
|
+
const page = context.page as CategoryPage;
|
|
6
|
+
|
|
7
|
+
export default base(
|
|
8
|
+
<header>
|
|
9
|
+
<div class="wrap">
|
|
10
|
+
<h1>{page.title}</h1>
|
|
11
|
+
</div>
|
|
12
|
+
</header>,
|
|
13
|
+
<main>
|
|
14
|
+
<ArchiveArticleList
|
|
15
|
+
articles={page.data.category.getArticles().sort(context.compareByDate)}
|
|
16
|
+
/>
|
|
17
|
+
</main>,
|
|
18
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Article, URL } from 'ezal';
|
|
2
|
+
|
|
3
|
+
export default ({ articles }: { articles: Article[] }) => (
|
|
4
|
+
<ul class="archive">
|
|
5
|
+
{articles.map((article) => (
|
|
6
|
+
<li>
|
|
7
|
+
<time datetime={article.date.toString({ timeZoneName: 'never' })}>
|
|
8
|
+
{article.date.toPlainDate().toString()}
|
|
9
|
+
</time>
|
|
10
|
+
<a href={URL.for(article.url)}>{article.title}</a>
|
|
11
|
+
</li>
|
|
12
|
+
))}
|
|
13
|
+
</ul>
|
|
14
|
+
);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Article, URL } from 'ezal';
|
|
2
|
+
import Image from './Image';
|
|
3
|
+
|
|
4
|
+
export default ({ article }: { article: Article }): JSX.Element => (
|
|
5
|
+
<a class="article rounded" href={URL.for(article.url)}>
|
|
6
|
+
{article.data?.cover
|
|
7
|
+
? [
|
|
8
|
+
<Image
|
|
9
|
+
class="bloom"
|
|
10
|
+
url={URL.for(URL.resolve(article.url, article.data.cover))}
|
|
11
|
+
alt={article.title}
|
|
12
|
+
/>,
|
|
13
|
+
<Image
|
|
14
|
+
url={URL.for(URL.resolve(article.url, article.data.cover))}
|
|
15
|
+
alt={article.title}
|
|
16
|
+
/>,
|
|
17
|
+
]
|
|
18
|
+
: null}
|
|
19
|
+
<div class="article-info">
|
|
20
|
+
<object>
|
|
21
|
+
{...article.categories
|
|
22
|
+
.values()
|
|
23
|
+
.map((category) => (
|
|
24
|
+
<a class="link" href={URL.for('category', ...category.path)}>
|
|
25
|
+
{category.path.join('/')}
|
|
26
|
+
</a>
|
|
27
|
+
))
|
|
28
|
+
.toArray()}
|
|
29
|
+
</object>
|
|
30
|
+
<h2>{article.title}</h2>
|
|
31
|
+
<object>
|
|
32
|
+
{article.tags
|
|
33
|
+
.values()
|
|
34
|
+
.map((tag) => (
|
|
35
|
+
<a class="tag link" href={URL.for(`/tag/${tag.name}/`)}>
|
|
36
|
+
{tag.name}
|
|
37
|
+
</a>
|
|
38
|
+
))
|
|
39
|
+
.toArray()}
|
|
40
|
+
<time datetime={article.date.toString({ timeZoneName: 'never' })}>
|
|
41
|
+
{article.date.toPlainDate().toString()}
|
|
42
|
+
</time>
|
|
43
|
+
</object>
|
|
44
|
+
</div>
|
|
45
|
+
</a>
|
|
46
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { URL } from 'ezal';
|
|
2
|
+
|
|
3
|
+
export default (options?: { style?: JSX.IntrinsicAttributes['style'] }) => (
|
|
4
|
+
<div class="contact" style={options?.style}>
|
|
5
|
+
{context.theme.contact?.map(({ url, name, icon, color }) => (
|
|
6
|
+
<a
|
|
7
|
+
class={`icon-${icon}`}
|
|
8
|
+
href={URL.for(url)}
|
|
9
|
+
title={name}
|
|
10
|
+
style={{ $color: color }}
|
|
11
|
+
></a>
|
|
12
|
+
))}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Time } from 'ezal';
|
|
2
|
+
import Contact from './Contact';
|
|
3
|
+
|
|
4
|
+
const since = context.theme.since;
|
|
5
|
+
const now = Time.now();
|
|
6
|
+
const author = context.site.author;
|
|
7
|
+
|
|
8
|
+
export default () => (
|
|
9
|
+
<footer>
|
|
10
|
+
<div class="wrap">
|
|
11
|
+
<Contact />
|
|
12
|
+
<br />
|
|
13
|
+
{'小站运行了'}
|
|
14
|
+
<time id="since" datetime={since?.toString({ timeZoneName: 'never' })}>
|
|
15
|
+
好多好多
|
|
16
|
+
</time>
|
|
17
|
+
{'天'}
|
|
18
|
+
<br />
|
|
19
|
+
{'上次更新于 '}
|
|
20
|
+
<time
|
|
21
|
+
id="last"
|
|
22
|
+
datetime={now.toString({ timeZoneName: 'never' })}
|
|
23
|
+
>{`${now.year} 年 ${now.year} 月 ${now.month} 日`}</time>
|
|
24
|
+
<br />
|
|
25
|
+
{'基于 '}
|
|
26
|
+
<a href="https://github.com/JonnyJong/ezal">Ezal</a>
|
|
27
|
+
{' 博客框架 | '}
|
|
28
|
+
<a href="https://github.com/JonnyJong/ezal/tree/main/packages/ezal-theme-example">
|
|
29
|
+
ezal-theme-example
|
|
30
|
+
</a>
|
|
31
|
+
{' 主题'}
|
|
32
|
+
<br />
|
|
33
|
+
{`©${since ? `${since.year}~` : ''}`}
|
|
34
|
+
<time id="now"></time>
|
|
35
|
+
{` By ${author}`}
|
|
36
|
+
<br />
|
|
37
|
+
{'若无特别说明,所有文章均采用 '}
|
|
38
|
+
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">
|
|
39
|
+
CC-BY-NC-SA 4.0
|
|
40
|
+
</a>
|
|
41
|
+
{' 许可协议'}
|
|
42
|
+
</div>
|
|
43
|
+
</footer>
|
|
44
|
+
);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { URL } from 'ezal';
|
|
2
|
+
import * as mime from 'mime-types';
|
|
3
|
+
|
|
4
|
+
const { site, page, theme } = context;
|
|
5
|
+
|
|
6
|
+
function resolveFavicons(): [type: string, href: string][] {
|
|
7
|
+
if (!theme.favicon) return [];
|
|
8
|
+
let icons = theme.favicon;
|
|
9
|
+
if (!Array.isArray(icons)) icons = [icons];
|
|
10
|
+
return icons.map(String).map<[string, string]>((href) => {
|
|
11
|
+
const type = mime.lookup(href);
|
|
12
|
+
return [type ? type : '', href];
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const favicons = resolveFavicons();
|
|
17
|
+
const title = page.title ? `${page.title} - ${site.title}` : site.title;
|
|
18
|
+
const description = page.description ?? site.description;
|
|
19
|
+
const keywords = (page.keywords ?? site.keywords ?? []).join(',');
|
|
20
|
+
const canonicalUrl = URL.full(page.url);
|
|
21
|
+
const cover = page.data.cover ? URL.full(page.url, page.data.cover) : undefined;
|
|
22
|
+
|
|
23
|
+
const katex =
|
|
24
|
+
theme.cdn?.katex ?? 'https://unpkg.com/katex@0.16.21/dist/katex.min.css';
|
|
25
|
+
|
|
26
|
+
const enableComment =
|
|
27
|
+
theme.waline &&
|
|
28
|
+
(page.data?.comment || ('tags' in page && page.data?.comment !== false));
|
|
29
|
+
const waline =
|
|
30
|
+
theme.cdn?.walineCSS ?? 'https://unpkg.com/@waline/client@v3/dist/waline.css';
|
|
31
|
+
|
|
32
|
+
export default (slot?: any) => (
|
|
33
|
+
<head>
|
|
34
|
+
{/* 基本 */}
|
|
35
|
+
<meta charset="UTF-8" />
|
|
36
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
37
|
+
<link rel="canonical" href={canonicalUrl} />
|
|
38
|
+
{favicons.map(([type, href]) => (
|
|
39
|
+
<link rel="icon" type={type} href={URL.full(href)} />
|
|
40
|
+
))}
|
|
41
|
+
<title>{title}</title>
|
|
42
|
+
<link rel="sitemap" href={URL.full('sitemap.xml')} />
|
|
43
|
+
<link
|
|
44
|
+
rel="alternate"
|
|
45
|
+
type="application/atom+xml"
|
|
46
|
+
title={site.title}
|
|
47
|
+
href={URL.full('atom.xml')}
|
|
48
|
+
/>
|
|
49
|
+
<link
|
|
50
|
+
rel="alternate"
|
|
51
|
+
type="application/rss+xml"
|
|
52
|
+
title={site.title}
|
|
53
|
+
href={URL.full('rss.xml')}
|
|
54
|
+
/>
|
|
55
|
+
<link
|
|
56
|
+
rel="alternate"
|
|
57
|
+
type="application/feed+json"
|
|
58
|
+
title={site.title}
|
|
59
|
+
href={URL.full('feed.json')}
|
|
60
|
+
/>
|
|
61
|
+
<meta name="theme-color" content="dark light" />
|
|
62
|
+
<meta name="author" content={site.author} />
|
|
63
|
+
{description ? <meta name="description" content={description} /> : null}
|
|
64
|
+
{keywords ? <meta name="keywords" content={keywords} /> : null}
|
|
65
|
+
{/* Open Graph */}
|
|
66
|
+
<meta property="og:site_name" content={site.title} />
|
|
67
|
+
<meta property="og:title" content={title} />
|
|
68
|
+
{cover ? <meta property="og:image" content={cover} /> : null}
|
|
69
|
+
<meta property="og:url" content={canonicalUrl} />
|
|
70
|
+
<meta property="og:locale" content={site.language} />
|
|
71
|
+
{favicons[0] ? (
|
|
72
|
+
<meta property="og:image" content={URL.full(favicons[0][1])} />
|
|
73
|
+
) : null}
|
|
74
|
+
<meta
|
|
75
|
+
property="og:type"
|
|
76
|
+
content={page.layout === 'article' ? 'article' : 'website'}
|
|
77
|
+
/>
|
|
78
|
+
{/* Twitter */}
|
|
79
|
+
<meta name="twitter:card" content="card" />
|
|
80
|
+
<meta name="twitter:author" content={site.author} />
|
|
81
|
+
<meta name="twitter:title" content={title} />
|
|
82
|
+
<meta name="twitter:description" content={description} />
|
|
83
|
+
{cover ? <meta name="twitter:image" content={cover} /> : null}
|
|
84
|
+
{'tags' in page
|
|
85
|
+
? [
|
|
86
|
+
<meta name="twitter:tag" content={page.tags.keys().toArray().join(',')} />,
|
|
87
|
+
<meta name="twitter:published_time" content={page.date.toString()} />,
|
|
88
|
+
<meta name="twitter:modified_time" content={page.updated.toString()} />,
|
|
89
|
+
]
|
|
90
|
+
: null}
|
|
91
|
+
{/* 资源 */}
|
|
92
|
+
{'renderedData' in page && page.renderedData.shared.tex ? (
|
|
93
|
+
<link rel="stylesheet" href={URL.for(katex)} />
|
|
94
|
+
) : null}
|
|
95
|
+
{enableComment ? <link rel="stylesheet" href={URL.for(waline)} /> : null}
|
|
96
|
+
<link
|
|
97
|
+
rel="preload"
|
|
98
|
+
href="https://unpkg.com/@fontsource/maple-mono@5.2.4/files/maple-mono-latin-400-normal.woff2"
|
|
99
|
+
as="font"
|
|
100
|
+
type="font/woff2"
|
|
101
|
+
crossorigin
|
|
102
|
+
/>
|
|
103
|
+
<link
|
|
104
|
+
rel="preload"
|
|
105
|
+
href="https://unpkg.com/@fontsource/maple-mono@5.2.4/files/maple-mono-latin-400-normal.woff"
|
|
106
|
+
as="font"
|
|
107
|
+
type="font/woff"
|
|
108
|
+
crossorigin
|
|
109
|
+
/>
|
|
110
|
+
<link rel="stylesheet" href={URL.for(`styles/${page.layout}.css`)} />
|
|
111
|
+
<script src={URL.for(`scripts/${page.layout}.js`)} async defer></script>
|
|
112
|
+
{'renderedData' in page && page.renderedData.shared.codeblock ? (
|
|
113
|
+
<link rel="stylesheet" href={URL.for('styles/code.css')} />
|
|
114
|
+
) : null}
|
|
115
|
+
{/* 额外 */}
|
|
116
|
+
{slot}
|
|
117
|
+
{theme.inject ? <RawHTML html={theme.inject} /> : null}
|
|
118
|
+
</head>
|
|
119
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { URL } from 'ezal';
|
|
2
|
+
|
|
3
|
+
const { page, getImageInfo, mime } = context;
|
|
4
|
+
|
|
5
|
+
export default ({
|
|
6
|
+
url,
|
|
7
|
+
alt,
|
|
8
|
+
...extra
|
|
9
|
+
}: { url: string; alt: string } & JSX.IntrinsicAttributes) => {
|
|
10
|
+
const info = getImageInfo(URL.resolve(page.url, url));
|
|
11
|
+
if (!info.rule)
|
|
12
|
+
return (
|
|
13
|
+
<img
|
|
14
|
+
src={URL.for(url)}
|
|
15
|
+
alt={alt}
|
|
16
|
+
width={info.metadata?.width}
|
|
17
|
+
height={info.metadata?.height}
|
|
18
|
+
style={{ $imgColor: info.metadata?.color }}
|
|
19
|
+
loading="lazy"
|
|
20
|
+
{...extra}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
return (
|
|
24
|
+
<picture>
|
|
25
|
+
{info.rule.slice(0, -1).map((ext) => (
|
|
26
|
+
<source
|
|
27
|
+
srcset={URL.for(URL.encode(URL.extname(url, `.opt${ext}`)))}
|
|
28
|
+
type={mime.lookup(ext) as any}
|
|
29
|
+
/>
|
|
30
|
+
))}
|
|
31
|
+
<img
|
|
32
|
+
src={URL.for(URL.encode(URL.extname(url, info.rule.at(-1))))}
|
|
33
|
+
alt={alt}
|
|
34
|
+
width={info.metadata?.width}
|
|
35
|
+
height={info.metadata?.height}
|
|
36
|
+
style={{ $imgColor: info.metadata?.color }}
|
|
37
|
+
loading="lazy"
|
|
38
|
+
{...extra}
|
|
39
|
+
/>
|
|
40
|
+
</picture>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { URL } from 'ezal';
|
|
2
|
+
|
|
3
|
+
const { site, theme } = context;
|
|
4
|
+
|
|
5
|
+
export default () => (
|
|
6
|
+
<nav>
|
|
7
|
+
<a class="site link" href={URL.for('/')}>
|
|
8
|
+
{site.title}
|
|
9
|
+
</a>
|
|
10
|
+
<div class="flex"></div>
|
|
11
|
+
<ul class="nav">
|
|
12
|
+
{theme.nav?.map(({ name, link }) => (
|
|
13
|
+
<li>
|
|
14
|
+
<a class="link" href={URL.for(link)}>
|
|
15
|
+
{name}
|
|
16
|
+
</a>
|
|
17
|
+
</li>
|
|
18
|
+
))}
|
|
19
|
+
</ul>
|
|
20
|
+
<button
|
|
21
|
+
class="icon-search link"
|
|
22
|
+
id="search"
|
|
23
|
+
title="搜索"
|
|
24
|
+
type="button"
|
|
25
|
+
></button>
|
|
26
|
+
<button
|
|
27
|
+
class="icon-nav link"
|
|
28
|
+
id="nav"
|
|
29
|
+
title="导航菜单"
|
|
30
|
+
type="button"
|
|
31
|
+
></button>
|
|
32
|
+
</nav>
|
|
33
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export default () => (
|
|
2
|
+
<dialog class="rounded search">
|
|
3
|
+
<div class="search-bar">
|
|
4
|
+
<input
|
|
5
|
+
type="search"
|
|
6
|
+
id="search-input"
|
|
7
|
+
class="search-input"
|
|
8
|
+
placeholder="键入以搜索..."
|
|
9
|
+
/>
|
|
10
|
+
<button
|
|
11
|
+
class="icon-close link"
|
|
12
|
+
title="关闭"
|
|
13
|
+
type="button"
|
|
14
|
+
id="search-close"
|
|
15
|
+
></button>
|
|
16
|
+
</div>
|
|
17
|
+
<progress></progress>
|
|
18
|
+
<div class="search-result"></div>
|
|
19
|
+
</dialog>
|
|
20
|
+
);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Article, Page } from 'ezal';
|
|
2
|
+
|
|
3
|
+
export default () => {
|
|
4
|
+
const { page, theme } = context;
|
|
5
|
+
|
|
6
|
+
if (!theme.waline) return;
|
|
7
|
+
if (!(page instanceof Page)) return;
|
|
8
|
+
if (page instanceof Article) {
|
|
9
|
+
if (page.data?.comment === false) return;
|
|
10
|
+
} else if (!page.data?.comment) return;
|
|
11
|
+
|
|
12
|
+
const options = { ...theme.waline, el: '#waline', path: page.url };
|
|
13
|
+
|
|
14
|
+
const module =
|
|
15
|
+
theme.cdn?.walineJS ?? 'https://unpkg.com/@waline/client@v3/dist/waline.js';
|
|
16
|
+
const script = `import{init}from'${module}';init(${JSON.stringify(options)})`;
|
|
17
|
+
|
|
18
|
+
return <Container>
|
|
19
|
+
<div id="waline" class="wrap"></div>
|
|
20
|
+
<script type="module" defer><RawHTML html={script}/></script>
|
|
21
|
+
</Container>;
|
|
22
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Article,
|
|
3
|
+
Category,
|
|
4
|
+
Page,
|
|
5
|
+
SiteConfig,
|
|
6
|
+
Tag,
|
|
7
|
+
VirtualPage,
|
|
8
|
+
} from 'ezal';
|
|
9
|
+
import type { Context as MarkdownContext } from 'ezal-markdown';
|
|
10
|
+
import type mime from 'mime-types';
|
|
11
|
+
import type { ThemeConfig } from '../src/config';
|
|
12
|
+
import type { ImageInfo } from '../src/image';
|
|
13
|
+
|
|
14
|
+
export interface HomePageData {
|
|
15
|
+
index: number;
|
|
16
|
+
getPages(): HomePage[];
|
|
17
|
+
getArticles(index: number): Article[];
|
|
18
|
+
}
|
|
19
|
+
export type HomePage = VirtualPage & { data: HomePageData };
|
|
20
|
+
|
|
21
|
+
export interface ArchivePageData {
|
|
22
|
+
years: Map<number, number>;
|
|
23
|
+
getArticles(year: number): Article[];
|
|
24
|
+
}
|
|
25
|
+
export type ArchivePage = VirtualPage & { data: ArchivePageData };
|
|
26
|
+
|
|
27
|
+
export interface CategoryPageData {
|
|
28
|
+
category: Category;
|
|
29
|
+
}
|
|
30
|
+
export type CategoryPage = VirtualPage & { data: CategoryPageData };
|
|
31
|
+
|
|
32
|
+
export interface TagPageData {
|
|
33
|
+
tag: Tag;
|
|
34
|
+
}
|
|
35
|
+
export type TagPage = VirtualPage & { data: TagPageData };
|
|
36
|
+
|
|
37
|
+
export interface Context {
|
|
38
|
+
site: SiteConfig;
|
|
39
|
+
theme: ThemeConfig;
|
|
40
|
+
page?:
|
|
41
|
+
| (Page & { renderedData: MarkdownContext })
|
|
42
|
+
| (Article & { renderedData: MarkdownContext })
|
|
43
|
+
| HomePage
|
|
44
|
+
| ArchivePage
|
|
45
|
+
| CategoryPage
|
|
46
|
+
| TagPage;
|
|
47
|
+
getImageInfo(url: string): ImageInfo | null;
|
|
48
|
+
mime: typeof mime;
|
|
49
|
+
compareByDate(a: Article, b: Article): number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
declare global {
|
|
53
|
+
const context: Context;
|
|
54
|
+
}
|
package/layouts/home.tsx
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { URL } from 'ezal';
|
|
2
|
+
import base from './base';
|
|
3
|
+
import Article from './components/Article';
|
|
4
|
+
import Contact from './components/Contact';
|
|
5
|
+
import type { HomePage } from './context';
|
|
6
|
+
|
|
7
|
+
const { home } = context.theme;
|
|
8
|
+
const page = context.page as HomePage;
|
|
9
|
+
const pages = page.data.getPages();
|
|
10
|
+
const current = pages.indexOf(page);
|
|
11
|
+
|
|
12
|
+
const slogan = home?.slogan ? (
|
|
13
|
+
<RawHTML html={home.slogan} />
|
|
14
|
+
) : (
|
|
15
|
+
<Container>
|
|
16
|
+
{'Hi there!👋'}
|
|
17
|
+
<br />
|
|
18
|
+
{`I'm ${context.site.author}.`}
|
|
19
|
+
</Container>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const logo = home?.logo ? (
|
|
23
|
+
<svg
|
|
24
|
+
class="home-logo"
|
|
25
|
+
viewBox={home.logo.viewBox}
|
|
26
|
+
role="img"
|
|
27
|
+
aria-label="logo"
|
|
28
|
+
>
|
|
29
|
+
<g id="logo">
|
|
30
|
+
<RawHTML html={home.logo.g} />
|
|
31
|
+
</g>
|
|
32
|
+
</svg>
|
|
33
|
+
) : null;
|
|
34
|
+
|
|
35
|
+
let header: JSX.Element;
|
|
36
|
+
if (current) {
|
|
37
|
+
header = <header style={{ height: 70 }} />;
|
|
38
|
+
} else {
|
|
39
|
+
header = (
|
|
40
|
+
<header>
|
|
41
|
+
<div class="wrap home">
|
|
42
|
+
<div class="home-title">
|
|
43
|
+
{slogan}
|
|
44
|
+
<Contact style={{ paddingTop: 8 }} />
|
|
45
|
+
</div>
|
|
46
|
+
{logo}
|
|
47
|
+
</div>
|
|
48
|
+
<div class="home-indicator icon-down"></div>
|
|
49
|
+
</header>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const pagination = pages.map((p, i) => {
|
|
54
|
+
if (p === page) return <div class="page rounded">{i + 1}</div>;
|
|
55
|
+
return (
|
|
56
|
+
<a class="page rounded" href={`${URL.for(p.url)}#posts`}>
|
|
57
|
+
{i + 1}
|
|
58
|
+
</a>
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export default base(
|
|
63
|
+
header,
|
|
64
|
+
<main>
|
|
65
|
+
<div class="wrap">
|
|
66
|
+
<div class="article-list" id="posts">
|
|
67
|
+
{page.data.getArticles(page.data.index).map((article) => (
|
|
68
|
+
<Article article={article} />
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
{pagination.length > 1 ? <div class="pages">{pagination}</div> : null}
|
|
72
|
+
</div>
|
|
73
|
+
</main>,
|
|
74
|
+
);
|