Compare commits
15 Commits
ae28ce8278
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| baf88a1ca1 | |||
| d122f19ead | |||
| b570c9e621 | |||
| b4caa00827 | |||
| 1364698b81 | |||
| 6243898db3 | |||
| 7ae6fb91a4 | |||
| cbf35beeb8 | |||
| b7ebf3bef7 | |||
| 370b421eed | |||
| d97e6c66d6 | |||
| 5de5876bde | |||
| 6d39b0dec4 | |||
| 1c53b77312 | |||
| 79332344a8 |
Vendored
+18
-20
@@ -1,22 +1,20 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"quickfix.biome": "always",
|
||||
"source.organizeImports.biome": "always"
|
||||
},
|
||||
"frontMatter.dashboard.openOnStart": false
|
||||
"editor.formatOnSave": false,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit"
|
||||
},
|
||||
"frontMatter.dashboard.openOnStart": false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Project Information for AI Agents
|
||||
|
||||
## Build Commands
|
||||
- `pnpm dev` - Start development server
|
||||
- `pnpm build` - Build for production (includes pagefind search index)
|
||||
- `pnpm preview` - Preview production build
|
||||
|
||||
## Code Quality
|
||||
- `pnpm lint` - Run linter (biome)
|
||||
- `pnpm format` - Format code (biome)
|
||||
- `pnpm type-check` - Type check TypeScript
|
||||
|
||||
## Project Stack
|
||||
- Framework: Astro v5
|
||||
- Styling: Tailwind CSS
|
||||
- Package Manager: pnpm
|
||||
- Linting/Formatting: Biome
|
||||
|
||||
## Notes
|
||||
- This is a Fuwari blog template built with Astro
|
||||
- Search functionality powered by Pagefind
|
||||
@@ -1,3 +1,11 @@
|
||||
# Milkblogs
|
||||
|
||||
这是网站 [milkfunc.top](https://milkfunc.top) 的源码仓库。
|
||||
|
||||
本项目是 [saicaca/fuwari](https://github.com/saicaca/fuwari) 的一个 Fork。
|
||||
|
||||
---
|
||||
|
||||
# 🍥Fuwari
|
||||

|
||||

|
||||
@@ -69,24 +77,24 @@ lang: jp # Set only if the post's language differs from the site's language
|
||||
In addition to Astro's default support for [GitHub Flavored Markdown](https://github.github.com/gfm/), several extra Markdown features are included:
|
||||
|
||||
- Admonitions ([Preview and Usage](https://fuwari.vercel.app/posts/markdown-extended/#admonitions))
|
||||
- GitHub repository cards ([Preview and Usage](https://fuwari.vercel.app/posts/markdown-extended/#github-repository-cards))
|
||||
- GitHub and GitLab repository cards ([Preview and Usage](https://fuwari.vercel.app/posts/markdown-extended/#github-repository-cards))
|
||||
- Enhanced code blocks with Expressive Code ([Preview](https://fuwari.vercel.app/posts/expressive-code/) / [Docs](https://expressive-code.com/))
|
||||
|
||||
## ⚡ Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
|:---------------------------|:----------------------------------------------------|
|
||||
| `pnpm install` | Installs dependencies |
|
||||
| `pnpm dev` | Starts local dev server at `localhost:4321` |
|
||||
| `pnpm build` | Build your production site to `./dist/` |
|
||||
| `pnpm preview` | Preview your build locally, before deploying |
|
||||
| `pnpm check` | Run checks for errors in your code |
|
||||
| `pnpm format` | Format your code using Biome |
|
||||
| `pnpm new-post <filename>` | Create a new post |
|
||||
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `pnpm astro --help` | Get help using the Astro CLI |
|
||||
| Command | Action |
|
||||
| :------------------------- | :----------------------------------------------- |
|
||||
| `pnpm install` | Installs dependencies |
|
||||
| `pnpm dev` | Starts local dev server at `localhost:4321` |
|
||||
| `pnpm build` | Build your production site to `./dist/` |
|
||||
| `pnpm preview` | Preview your build locally, before deploying |
|
||||
| `pnpm check` | Run checks for errors in your code |
|
||||
| `pnpm format` | Format your code using Biome |
|
||||
| `pnpm new-post <filename>` | Create a new post |
|
||||
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `pnpm astro --help` | Get help using the Astro CLI |
|
||||
|
||||
## ✏️ Contributing
|
||||
|
||||
|
||||
+14
-11
@@ -4,9 +4,9 @@ import tailwind from "@astrojs/tailwind";
|
||||
import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections";
|
||||
import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
|
||||
import swup from "@swup/astro";
|
||||
import { defineConfig } from "astro/config";
|
||||
import expressiveCode from "astro-expressive-code";
|
||||
import icon from "astro-icon";
|
||||
import { defineConfig } from "astro/config";
|
||||
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||
import rehypeComponents from "rehype-components"; /* Render the custom directive content */
|
||||
import rehypeKatex from "rehype-katex";
|
||||
@@ -16,18 +16,19 @@ import remarkGithubAdmonitionsToDirectives from "remark-github-admonitions-to-di
|
||||
import remarkMath from "remark-math";
|
||||
import remarkSectionize from "remark-sectionize";
|
||||
import { expressiveCodeConfig } from "./src/config.ts";
|
||||
import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button.js";
|
||||
import { pluginLanguageBadge } from "./src/plugins/expressive-code/language-badge.ts";
|
||||
import { AdmonitionComponent } from "./src/plugins/rehype-component-admonition.mjs";
|
||||
import { GiteaCardComponent } from "./src/plugins/rehype-component-gitea-card.mjs";
|
||||
import { GithubCardComponent } from "./src/plugins/rehype-component-github-card.mjs";
|
||||
import { GitlabCardComponent } from "./src/plugins/rehype-component-gitlab-card.mjs";
|
||||
import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js";
|
||||
import { remarkExcerpt } from "./src/plugins/remark-excerpt.js";
|
||||
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs";
|
||||
import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button.js";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: "https://fuwari.vercel.app/",
|
||||
site: "https://milkfunc.top/",
|
||||
base: "/",
|
||||
trailingSlash: "always",
|
||||
integrations: [
|
||||
@@ -62,12 +63,12 @@ export default defineConfig({
|
||||
pluginCollapsibleSections(),
|
||||
pluginLineNumbers(),
|
||||
pluginLanguageBadge(),
|
||||
pluginCustomCopyButton()
|
||||
pluginCustomCopyButton(),
|
||||
],
|
||||
defaultProps: {
|
||||
wrap: true,
|
||||
overridesByLang: {
|
||||
'shellsession': {
|
||||
shellsession: {
|
||||
showLineNumbers: false,
|
||||
},
|
||||
},
|
||||
@@ -77,7 +78,8 @@ export default defineConfig({
|
||||
borderRadius: "0.75rem",
|
||||
borderColor: "none",
|
||||
codeFontSize: "0.875rem",
|
||||
codeFontFamily: "'JetBrains Mono Variable', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||
codeFontFamily:
|
||||
"'JetBrains Mono Variable', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||
codeLineHeight: "1.5rem",
|
||||
frames: {
|
||||
editorBackground: "var(--codeblock-bg)",
|
||||
@@ -88,19 +90,19 @@ export default defineConfig({
|
||||
editorActiveTabIndicatorBottomColor: "var(--primary)",
|
||||
editorActiveTabIndicatorTopColor: "none",
|
||||
editorTabBarBorderBottomColor: "var(--codeblock-topbar-bg)",
|
||||
terminalTitlebarBorderBottomColor: "none"
|
||||
terminalTitlebarBorderBottomColor: "none",
|
||||
},
|
||||
textMarkers: {
|
||||
delHue: 0,
|
||||
insHue: 180,
|
||||
markHue: 250
|
||||
}
|
||||
markHue: 250,
|
||||
},
|
||||
},
|
||||
frames: {
|
||||
showCopyToClipboardButton: false,
|
||||
}
|
||||
},
|
||||
}),
|
||||
svelte(),
|
||||
svelte(),
|
||||
sitemap(),
|
||||
],
|
||||
markdown: {
|
||||
@@ -122,6 +124,7 @@ export default defineConfig({
|
||||
components: {
|
||||
github: GithubCardComponent,
|
||||
gitlab: GitlabCardComponent,
|
||||
gitea: GiteaCardComponent,
|
||||
note: (x, y) => AdmonitionComponent(x, y, "note"),
|
||||
tip: (x, y) => AdmonitionComponent(x, y, "tip"),
|
||||
important: (x, y) => AdmonitionComponent(x, y, "important"),
|
||||
|
||||
+13
-13
@@ -16,28 +16,28 @@
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/rss": "^4.0.12",
|
||||
"@astrojs/check": "^0.9.6",
|
||||
"@astrojs/rss": "^4.0.14",
|
||||
"@astrojs/sitemap": "^3.6.0",
|
||||
"@astrojs/svelte": "7.2.0",
|
||||
"@astrojs/svelte": "7.2.3",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"@expressive-code/core": "^0.41.3",
|
||||
"@expressive-code/plugin-collapsible-sections": "^0.41.3",
|
||||
"@expressive-code/plugin-line-numbers": "^0.41.3",
|
||||
"@expressive-code/core": "^0.41.4",
|
||||
"@expressive-code/plugin-collapsible-sections": "^0.41.4",
|
||||
"@expressive-code/plugin-line-numbers": "^0.41.4",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@fontsource/roboto": "^5.2.8",
|
||||
"@fontsource/roboto": "^5.2.9",
|
||||
"@iconify-json/fa6-brands": "^1.2.6",
|
||||
"@iconify-json/fa6-regular": "^1.2.4",
|
||||
"@iconify-json/fa6-solid": "^1.2.4",
|
||||
"@iconify-json/material-symbols": "^1.2.40",
|
||||
"@iconify-json/material-symbols": "^1.2.50",
|
||||
"@iconify/svelte": "^4.2.0",
|
||||
"@swup/astro": "^1.7.0",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"astro": "5.13.10",
|
||||
"astro-expressive-code": "^0.41.3",
|
||||
"astro-expressive-code": "^0.41.4",
|
||||
"astro-icon": "^1.1.5",
|
||||
"hastscript": "^9.0.1",
|
||||
"katex": "^0.16.23",
|
||||
"katex": "^0.16.27",
|
||||
"markdown-it": "^14.1.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"overlayscrollbars": "^2.12.0",
|
||||
@@ -54,15 +54,15 @@
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-sectionize": "^2.1.0",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"sharp": "^0.34.4",
|
||||
"sharp": "^0.34.5",
|
||||
"stylus": "^0.64.0",
|
||||
"svelte": "^5.39.8",
|
||||
"tailwindcss": "^3.4.18",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5.9.3",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/ts-plugin": "^1.10.4",
|
||||
"@astrojs/ts-plugin": "^1.10.6",
|
||||
"@biomejs/biome": "2.2.5",
|
||||
"@rollup/plugin-yaml": "^4.1.2",
|
||||
"@types/hast": "^3.0.4",
|
||||
|
||||
Generated
+1149
-457
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
style="display:block"
|
||||
viewBox="0 0 716.8 716.8"
|
||||
width="350"
|
||||
height="350"
|
||||
id="svg33"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs33" />
|
||||
<path
|
||||
fill="#a6d4eb"
|
||||
d="m 230.10802,139.23103 c 0.173,-13.411 -0.96,-28.877 8.814,-39.464 7.389,-8.005 18.272,-9.119004 28.64,-9.535004 41.032,-1.648 82.724,-0.106 123.82,-0.127 l 51.813,0.005 c 9.99,0.009 20.354,-0.545 30.284,0.642 7.653,0.914004 14.525,1.979004 20.314,7.525004 11.444,10.963 10.268,26.372 10.306,40.893 20.284,22.728 80.22904,79.49803 98.66104,97.93003 18.432,18.432 25.14761,30.00191 31.43809,40.24191 7.26713,11.82983 9.52191,31.43809 9.52191,41.67809 v 38.91201 94.20805 65.536 c 10e-4,14.69 -0.23001,28.624 -1.59001,43.264 -0.782,8.42 -3.747,17.17 -7.872,24.55 -10.418,18.63 -27.289,27.15 -47.187,32.63 -35.013,9.63 -70.759,17.52 -106.085,25.98 -22.107,5.3 -45.268,12.64 -67.805,14.96 -23.727,2.43 -48.67,-1.44 -72.252,-4.03 l -76.089,-8.33 -78.493,-8.48 c 0,0 -74.79071,-12.3292 -78.44136,-14.048 -13.049651,-6.144 -18.812691,-11.6037 -27.385651,-20.48 -10.11256,-14.3288 -10.24,-59.392 -10.24,-59.392 v -137.4782 c 0,0 0,-77.56185 6.90598,-98.0259 3.25353,-9.64097 6.815,-19.442 12.834,-27.611 9.333,-12.667 21.849001,-23.489 32.985001,-34.589 l 68.867,-68.201 c 12.5,-12.35099 26.354,-24.48999 37.404,-38.11699 0.281,-0.346 0.554,-0.698 0.832,-1.047 z"
|
||||
id="path2"
|
||||
style="display:inline" />
|
||||
<path
|
||||
style="display:block;fill:#cccccc;fill-opacity:1;stroke:#6e6e6e;stroke-width:0.774048;stroke-opacity:0"
|
||||
d="m 643.78868,330.39021 c 0,0 -0.35178,-27.03287 -1.7741,-31.92725 -1.41983,-4.88581 -2.53508,-9.81373 -4.8689,-14.84037 -2.14477,-4.61948 -7.01648,17.29298 -9.90194,10.72496 -2.78722,-6.3444 8.79403,15.24875 11.35565,18.61884 1.69708,2.23269 5.25664,7.36569 5.18929,17.42382 z"
|
||||
id="path38" />
|
||||
<path
|
||||
fill="#1699e5"
|
||||
d="m 230.10802,139.23103 c 0.173,-13.411 -0.96,-28.877 8.814,-39.464 7.389,-8.005 18.272,-9.119004 28.64,-9.535004 41.032,-1.648 82.724,-0.106 123.82,-0.127 l 51.813,0.005 c 9.99,0.009 20.354,-0.545 30.284,0.642 7.653,0.914004 14.525,1.979004 20.314,7.525004 11.444,10.963 10.268,26.372 10.306,40.893 10.00576,11.21135 47.44216,46.71115 78.18104,77.45003 16.80034,16.80034 32.89095,31.77052 43.008,45.056 8.38232,11.00748 10.65371,18.36352 12.288,22.528 6.49877,15.35792 0.88259,22.55707 -16.72729,26.34557 -4.18558,0.90047 0.0934,0.33432 0.0934,0.33432 0,0 -202.05086,7.30405 -202.12128,7.62834 -8.051,-0.129 -20.41086,-4.64727 -27.04386,-8.89427 -1.578,-1.013 -3.142,-2.048 -4.692,-3.103 -28.013,-25.054 -54.115,-52.835 -80.861,-79.286 -10.732,-10.613 -21.971,-24.411 -34.612,-32.58 -3.714,-2.401 -8.647,-2.935 -12.982,-2.788 -3.381,0.115 -7.33,0.805 -10.208,2.695 -12.667,8.319 -32.616,30.853 -44.309,42.37 -21.983,21.653 -45.778,42.738 -66.251,65.739 -4.011,4.507 -10.029,11.238 -11.317,17.186 -3.39,15.649 -2.063,33.535 -2.07,49.515 l -0.022,65.934 v 85.5 c 0.001,15.02 -0.704,30.38 0.283,45.35 0.518,7.85 2.921,13.39 8.976,18.6 1.308,1.13 1.246,1.05 2.436,1.88 -1.319,0.21 -1.48,0.26 -3.026,0.31 -13.189,0.44 -35.741001,-0.5 -47.443001,-7.45 -8.61367,-5.112 -10.59682,-8.2601 -11.30298,-14.7099 -1.898,-17.38 -4.139,-33.66 -4.096,-51.2 v -106.7582 c -0.049,-23.95 2.18598,-76.5539 6.90598,-98.0259 2.184,-9.938 6.815,-19.442 12.834,-27.611 9.333,-12.667 21.849001,-23.489 32.985001,-34.589 l 68.867,-68.201 c 12.5,-12.35099 26.354,-24.48999 37.404,-38.11699 0.281,-0.346 0.554,-0.698 0.832,-1.047 z"
|
||||
id="path3"
|
||||
style="display:inline" />
|
||||
<path
|
||||
fill="#f3f2f2"
|
||||
d="m 136.14802,586.63002 c -1.19,-0.83 -1.128,-0.75 -2.436,-1.88 -6.055,-5.21 -8.458,-10.75 -8.976,-18.6 -0.987,-14.97 -0.282,-30.33 -0.283,-45.35 v -85.5 l 0.022,-65.934 c 0.007,-15.98 -1.32,-33.866 2.07,-49.515 1.288,-5.948 7.306,-12.679 11.317,-17.186 20.473,-23.001 44.268,-44.086 66.251,-65.739 11.693,-11.517 31.642,-34.051 44.309,-42.37 2.878,-1.89 6.827,-2.58 10.208,-2.695 4.335,-0.147 9.268,0.387 12.982,2.788 12.641,8.169 23.88,21.967 34.612,32.58 26.746,26.451 52.848,54.232 80.861,79.286 4.8,8.603 10.233,17.332 11.609,27.264 1.523,10.987 0.698,22.953 0.697,34.049 l -0.031,62.252 -0.072,101.49 c -0.003,18 0.671,36.25 -0.165,54.22 -0.253,5.43 -0.87,11.25 -2.449,16.46 -1.195,3.94 -3.813,6.36 -7.439,8.29 -3.468,1.85 -7.303,2.76 -11.215,2.97 -8.998,0.46 -37.971,-2.6 -46.568,-5.26 -4.434,-1.36 -8.121,-3.91 -10.245,-8.12 -5.068,-10.04 -2.472,-89.92 -2.485,-108.16 -0.009,-13.37 1.595,-28.51 -3.21,-41.18 -3.167,-8.35 -8.504,-13.62 -16.891,-16.65 -12.332,-4.45 -54.592,-8.54 -68.103,-7.35 -4.431,0.38 -8.929,1.52 -12.868,3.62 -7.818,4.17 -13,12.05 -15.438,20.4 -4.64,15.9 -2.087,80.66 -1.963,101.59 0.047,8.05 0.897,16.85 -0.424,24.78 -0.807,4.84 -2.773,10.51 -7.328,13.05 -11.859,6.63 -42.977,-1.63 -56.349,-3.6 z"
|
||||
id="path4"
|
||||
style="display:inline" />
|
||||
<rect
|
||||
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#6e6e6e;stroke-width:0.774048;stroke-opacity:0"
|
||||
id="rect36"
|
||||
width="186.88643"
|
||||
height="6.1440268"
|
||||
x="253.31686"
|
||||
y="134.70003"
|
||||
ry="2.976815"
|
||||
rx="0" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#6e6e6e;stroke-width:0.774048;stroke-opacity:0"
|
||||
id="rect37"
|
||||
width="36.879272"
|
||||
height="6.1439657"
|
||||
x="449.16003"
|
||||
y="134.70003"
|
||||
ry="3.0085199" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 267 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 424 KiB |
@@ -3,33 +3,29 @@ import { onMount } from "svelte";
|
||||
|
||||
import I18nKey from "../i18n/i18nKey";
|
||||
import { i18n } from "../i18n/translation";
|
||||
import type { PostForList } from "../utils/content-utils";
|
||||
import { getPostUrlBySlug } from "../utils/url-utils";
|
||||
|
||||
export let tags: string[];
|
||||
export let categories: string[];
|
||||
export let sortedPosts: Post[] = [];
|
||||
export let sortedPosts: PostForList[] = [];
|
||||
export let pinnedPosts: PostForList[] = [];
|
||||
export let categoryPinnedPosts: PostForList[] = [];
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
tags = params.has("tag") ? params.getAll("tag") : [];
|
||||
categories = params.has("category") ? params.getAll("category") : [];
|
||||
const uncategorized = params.get("uncategorized");
|
||||
|
||||
interface Post {
|
||||
slug: string;
|
||||
data: {
|
||||
title: string;
|
||||
tags: string[];
|
||||
category?: string;
|
||||
published: Date;
|
||||
};
|
||||
}
|
||||
const isCategoryMode = categories.length > 0;
|
||||
|
||||
interface Group {
|
||||
year: number;
|
||||
posts: Post[];
|
||||
posts: PostForList[];
|
||||
}
|
||||
|
||||
let groups: Group[] = [];
|
||||
let displayPinnedPosts: PostForList[] = [];
|
||||
|
||||
function formatDate(date: Date) {
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
@@ -42,7 +38,7 @@ function formatTag(tagList: string[]) {
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
let filteredPosts: Post[] = sortedPosts;
|
||||
let filteredPosts: PostForList[] = sortedPosts;
|
||||
|
||||
if (tags.length > 0) {
|
||||
filteredPosts = filteredPosts.filter(
|
||||
@@ -62,6 +58,39 @@ onMount(async () => {
|
||||
filteredPosts = filteredPosts.filter((post) => !post.data.category);
|
||||
}
|
||||
|
||||
if (isCategoryMode) {
|
||||
displayPinnedPosts = categoryPinnedPosts.filter(
|
||||
(post) => post.data.category && categories.includes(post.data.category),
|
||||
);
|
||||
if (tags.length > 0) {
|
||||
displayPinnedPosts = displayPinnedPosts.filter(
|
||||
(post) =>
|
||||
Array.isArray(post.data.tags) &&
|
||||
post.data.tags.some((tag) => tags.includes(tag)),
|
||||
);
|
||||
}
|
||||
filteredPosts = filteredPosts.filter(
|
||||
(post) => (post.data.category_pinned || 0) === 0,
|
||||
);
|
||||
} else {
|
||||
displayPinnedPosts = pinnedPosts;
|
||||
if (tags.length > 0) {
|
||||
displayPinnedPosts = displayPinnedPosts.filter(
|
||||
(post) =>
|
||||
Array.isArray(post.data.tags) &&
|
||||
post.data.tags.some((tag) => tags.includes(tag)),
|
||||
);
|
||||
}
|
||||
if (uncategorized) {
|
||||
displayPinnedPosts = displayPinnedPosts.filter(
|
||||
(post) => !post.data.category,
|
||||
);
|
||||
}
|
||||
filteredPosts = filteredPosts.filter(
|
||||
(post) => (post.data.pinned || 0) === 0,
|
||||
);
|
||||
}
|
||||
|
||||
const grouped = filteredPosts.reduce(
|
||||
(acc, post) => {
|
||||
const year = post.data.published.getFullYear();
|
||||
@@ -71,7 +100,7 @@ onMount(async () => {
|
||||
acc[year].push(post);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<number, Post[]>,
|
||||
{} as Record<number, PostForList[]>,
|
||||
);
|
||||
|
||||
const groupedPostsArray = Object.keys(grouped).map((yearStr) => ({
|
||||
@@ -86,66 +115,121 @@ onMount(async () => {
|
||||
</script>
|
||||
|
||||
<div class="card-base px-8 py-6">
|
||||
{#each groups as group}
|
||||
<div>
|
||||
<div class="flex flex-row w-full items-center h-[3.75rem]">
|
||||
<div class="w-[15%] md:w-[10%] transition text-2xl font-bold text-right text-75">
|
||||
{group.year}
|
||||
</div>
|
||||
<div class="w-[15%] md:w-[10%]">
|
||||
<div
|
||||
class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto
|
||||
-outline-offset-[2px] z-50 outline-3"
|
||||
></div>
|
||||
</div>
|
||||
<div class="w-[70%] md:w-[80%] transition text-left text-50">
|
||||
{group.posts.length} {i18n(group.posts.length === 1 ? I18nKey.postCount : I18nKey.postsCount)}
|
||||
</div>
|
||||
</div>
|
||||
{#if displayPinnedPosts.length > 0}
|
||||
<div>
|
||||
<div class="flex flex-row w-full items-center h-[3.75rem]">
|
||||
<div class="w-[15%] md:w-[10%] transition text-2xl font-bold text-right text-75">
|
||||
{i18n(I18nKey.pinned)}
|
||||
</div>
|
||||
<div class="w-[15%] md:w-[10%]">
|
||||
<div
|
||||
class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto
|
||||
-outline-offset-[2px] z-50 outline-3"
|
||||
></div>
|
||||
</div>
|
||||
<div class="w-[70%] md:w-[80%] transition text-left text-50">
|
||||
{displayPinnedPosts.length} {i18n(displayPinnedPosts.length === 1 ? I18nKey.postCount : I18nKey.postsCount)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#each group.posts as post}
|
||||
<a
|
||||
href={getPostUrlBySlug(post.slug)}
|
||||
aria-label={post.data.title}
|
||||
class="group btn-plain !block h-10 w-full rounded-lg hover:text-[initial]"
|
||||
>
|
||||
<div class="flex flex-row justify-start items-center h-full">
|
||||
<!-- date -->
|
||||
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
|
||||
{formatDate(post.data.published)}
|
||||
</div>
|
||||
{#each displayPinnedPosts as post}
|
||||
<a
|
||||
href={getPostUrlBySlug(post.slug)}
|
||||
aria-label={post.data.title}
|
||||
class="group btn-plain !block h-10 w-full rounded-lg hover:text-[initial]"
|
||||
>
|
||||
<div class="flex flex-row justify-start items-center h-full">
|
||||
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
|
||||
{formatDate(post.data.published)}
|
||||
</div>
|
||||
|
||||
<!-- dot and line -->
|
||||
<div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center">
|
||||
<div
|
||||
class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
|
||||
bg-[oklch(0.5_0.05_var(--hue))] group-hover:bg-[var(--primary)]
|
||||
outline outline-4 z-50
|
||||
outline-[var(--card-bg)]
|
||||
group-hover:outline-[var(--btn-plain-bg-hover)]
|
||||
group-active:outline-[var(--btn-plain-bg-active)]"
|
||||
></div>
|
||||
</div>
|
||||
<div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center">
|
||||
<div
|
||||
class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
|
||||
bg-[oklch(0.5_0.05_var(--hue))] group-hover:bg-[var(--primary)]
|
||||
outline outline-4 z-50
|
||||
outline-[var(--card-bg)]
|
||||
group-hover:outline-[var(--btn-plain-bg-hover)]
|
||||
group-active:outline-[var(--btn-plain-bg-active)]"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- post title -->
|
||||
<div
|
||||
class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold
|
||||
group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)]
|
||||
text-75 pr-8 whitespace-nowrap overflow-ellipsis overflow-hidden"
|
||||
>
|
||||
{post.data.title}
|
||||
</div>
|
||||
<div
|
||||
class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold
|
||||
group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)]
|
||||
text-75 pr-8 whitespace-nowrap overflow-ellipsis overflow-hidden"
|
||||
>
|
||||
{post.data.title}
|
||||
</div>
|
||||
|
||||
<!-- tag list -->
|
||||
<div
|
||||
class="hidden md:block md:w-[15%] text-left text-sm transition
|
||||
whitespace-nowrap overflow-ellipsis overflow-hidden text-30"
|
||||
>
|
||||
{formatTag(post.data.tags)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
<div
|
||||
class="hidden md:block md:w-[15%] text-left text-sm transition
|
||||
whitespace-nowrap overflow-ellipsis overflow-hidden text-30"
|
||||
>
|
||||
{formatTag(post.data.tags)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each groups as group}
|
||||
<div>
|
||||
<div class="flex flex-row w-full items-center h-[3.75rem]">
|
||||
<div class="w-[15%] md:w-[10%] transition text-2xl font-bold text-right text-75">
|
||||
{group.year}
|
||||
</div>
|
||||
<div class="w-[15%] md:w-[10%]">
|
||||
<div
|
||||
class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto
|
||||
-outline-offset-[2px] z-50 outline-3"
|
||||
></div>
|
||||
</div>
|
||||
<div class="w-[70%] md:w-[80%] transition text-left text-50">
|
||||
{group.posts.length} {i18n(group.posts.length === 1 ? I18nKey.postCount : I18nKey.postsCount)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#each group.posts as post}
|
||||
<a
|
||||
href={getPostUrlBySlug(post.slug)}
|
||||
aria-label={post.data.title}
|
||||
class="group btn-plain !block h-10 w-full rounded-lg hover:text-[initial]"
|
||||
>
|
||||
<div class="flex flex-row justify-start items-center h-full">
|
||||
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
|
||||
{formatDate(post.data.published)}
|
||||
</div>
|
||||
|
||||
<div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center">
|
||||
<div
|
||||
class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
|
||||
bg-[oklch(0.5_0.05_var(--hue))] group-hover:bg-[var(--primary)]
|
||||
outline outline-4 z-50
|
||||
outline-[var(--card-bg)]
|
||||
group-hover:outline-[var(--btn-plain-bg-hover)]
|
||||
group-active:outline-[var(--btn-plain-bg-active)]"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold
|
||||
group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)]
|
||||
text-75 pr-8 whitespace-nowrap overflow-ellipsis overflow-hidden"
|
||||
>
|
||||
{post.data.title}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="hidden md:block md:w-[15%] text-left text-sm transition
|
||||
whitespace-nowrap overflow-ellipsis overflow-hidden text-30"
|
||||
>
|
||||
{formatTag(post.data.tags)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { profileConfig } from "../config";
|
||||
import { url } from "../utils/url-utils";
|
||||
import Recorded from "./Recorded.astro";
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
---
|
||||
@@ -14,8 +15,6 @@ const currentYear = new Date().getFullYear();
|
||||
© <span id="copyright-year">{currentYear}</span> {profileConfig.name}. All Rights Reserved. /
|
||||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href={url('rss.xml')}>RSS</a> /
|
||||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href={url('sitemap-index.xml')}>Sitemap</a><br>
|
||||
Powered by
|
||||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://astro.build">Astro</a> &
|
||||
<a class="transition link text-[var(--primary)] font-medium" target="_blank" href="https://github.com/saicaca/fuwari">Fuwari</a>
|
||||
<Recorded />
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,6 +21,7 @@ interface Props {
|
||||
description: string;
|
||||
draft: boolean;
|
||||
style: string;
|
||||
pinned?: number;
|
||||
}
|
||||
const {
|
||||
entry,
|
||||
@@ -33,6 +34,7 @@ const {
|
||||
image,
|
||||
description,
|
||||
style,
|
||||
pinned = 0,
|
||||
} = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
|
||||
@@ -52,6 +54,7 @@ const { remarkPluginFrontmatter } = await entry.render();
|
||||
before:absolute before:top-[35px] before:left-[18px] before:hidden md:before:block
|
||||
">
|
||||
{title}
|
||||
{pinned > 0 && <Icon name="material-symbols:push-pin" class="inline text-[1.5rem] text-[var(--primary)] ml-2 translate-y-[-2px]" />}
|
||||
<Icon class="inline text-[2rem] text-[var(--primary)] md:hidden translate-y-0.5 absolute" name="material-symbols:chevron-right-rounded" ></Icon>
|
||||
<Icon class="text-[var(--primary)] text-[2rem] transition hidden md:inline absolute translate-y-0.5 opacity-0 group-hover:opacity-100 -translate-x-1 group-hover:translate-x-0" name="material-symbols:chevron-right-rounded"></Icon>
|
||||
</a>
|
||||
|
||||
@@ -21,6 +21,7 @@ const interval = 50;
|
||||
image={entry.data.image}
|
||||
description={entry.data.description}
|
||||
draft={entry.data.draft}
|
||||
pinned={entry.data.pinned}
|
||||
class:list="onload-animation"
|
||||
style={`animation-delay: calc(var(--content-delay) + ${delay++ * interval}ms);`}
|
||||
></PostCard>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<a
|
||||
class="whitespace-nowrap inline-flex items-center max-w-full overflow-hidden"
|
||||
target="_blank"
|
||||
href={"https://beian.miit.gov.cn/"}>鲁ICP备2025144068号-1</a
|
||||
><br />
|
||||
<a
|
||||
class="whitespace-nowrap inline-flex items-center max-w-full overflow-hidden"
|
||||
target="_blank"
|
||||
href={"https://beian.mps.gov.cn/#/query/webSearch?code=37011302000501"}
|
||||
>
|
||||
<img
|
||||
src="/recorded.png"
|
||||
class="w-4 h-4 mr-1.5"
|
||||
alt="备案图标"
|
||||
loading="lazy"
|
||||
/>
|
||||
鲁公网安备37011302000501号</a
|
||||
><br />
|
||||
@@ -1,22 +1,38 @@
|
||||
---
|
||||
import type { MarkdownHeading } from "astro";
|
||||
import { siteConfig } from "@/config";
|
||||
import Categories from "./Categories.astro";
|
||||
import Profile from "./Profile.astro";
|
||||
import Tag from "./Tags.astro";
|
||||
import TOC from "./TOC.astro";
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
headings?: MarkdownHeading[];
|
||||
}
|
||||
|
||||
const className = Astro.props.class;
|
||||
const { class: className, headings } = Astro.props;
|
||||
const currentPagePath = Astro.url.pathname;
|
||||
const isPostPage = currentPagePath.startsWith("/posts/");
|
||||
---
|
||||
|
||||
<div id="sidebar" class:list={[className, "w-full"]}>
|
||||
<div class="flex flex-col w-full gap-4 mb-4">
|
||||
<Profile></Profile>
|
||||
</div>
|
||||
{
|
||||
!isPostPage && (
|
||||
<div class="flex flex-col w-full gap-4 mb-4">
|
||||
<Profile />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div id="sidebar-sticky" class="transition-all duration-700 flex flex-col w-full gap-4 top-4 sticky top-4">
|
||||
<Categories class="onload-animation" style="animation-delay: 150ms"></Categories>
|
||||
<Tag class="onload-animation" style="animation-delay: 200ms"></Tag>
|
||||
{
|
||||
siteConfig.toc.enable && headings && headings.length > 0 && (
|
||||
<div id="toc-wrapper" class="onload-animation card-base" style="animation-delay: 250ms">
|
||||
<TOC headings={headings} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Categories class="onload-animation" style="animation-delay: 150ms" />
|
||||
<Tag class="onload-animation" style="animation-delay: 200ms" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+219
-205
@@ -1,7 +1,10 @@
|
||||
---
|
||||
import type { MarkdownHeading } from "astro";
|
||||
import { siteConfig } from "../../config";
|
||||
import I18nKey from "../../i18n/i18nKey";
|
||||
import { i18n } from "../../i18n/translation";
|
||||
import { url } from "../../utils/url-utils";
|
||||
import WidgetLayout from "./WidgetLayout.astro";
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
@@ -31,238 +34,249 @@ let heading1Count = 1;
|
||||
|
||||
const maxLevel = siteConfig.toc.depth;
|
||||
---
|
||||
{isPostsRoute &&
|
||||
<table-of-contents class:list={[className, "group"]}>
|
||||
{headings.filter((heading) => heading.depth < minDepth + maxLevel).map((heading) =>
|
||||
<a href={`#${heading.slug}`} class="px-2 flex gap-2 relative transition w-full min-h-9 rounded-xl
|
||||
hover:bg-[var(--toc-btn-hover)] active:bg-[var(--toc-btn-active)] py-2
|
||||
">
|
||||
<div class:list={["transition w-5 h-5 shrink-0 rounded-lg text-xs flex items-center justify-center font-bold",
|
||||
{
|
||||
"bg-[var(--toc-badge-bg)] text-[var(--btn-content)]": heading.depth == minDepth,
|
||||
"ml-4": heading.depth == minDepth + 1,
|
||||
"ml-8": heading.depth == minDepth + 2,
|
||||
}
|
||||
]}
|
||||
|
||||
<table-of-contents>
|
||||
<WidgetLayout name={i18n(I18nKey.toc)} id="toc" class={className}>
|
||||
<div class="flex flex-col pb-4">
|
||||
<div
|
||||
id="toc-wrapper"
|
||||
class="onload-animation card-base"
|
||||
style="animation-delay: 250ms"
|
||||
>
|
||||
<div
|
||||
id="toc-inner-wrapper"
|
||||
class="overflow-y-auto max-h-[50vh] hide-scrollbar pr-2"
|
||||
>
|
||||
{heading.depth == minDepth && heading1Count++}
|
||||
{heading.depth == minDepth + 1 && <div class="transition w-2 h-2 rounded-[0.1875rem] bg-[var(--toc-badge-bg)]"></div>}
|
||||
{heading.depth == minDepth + 2 && <div class="transition w-1.5 h-1.5 rounded-sm bg-black/5 dark:bg-white/10"></div>}
|
||||
{
|
||||
headings
|
||||
.filter(
|
||||
(heading) =>
|
||||
heading.depth < minDepth + maxLevel,
|
||||
)
|
||||
.map((heading) => (
|
||||
<a
|
||||
href={`#${heading.slug}`}
|
||||
class="px-2 flex gap-2 relative transition-colors duration-200 w-full min-h-9 rounded-xl hover:bg-[var(--toc-btn-hover)] active:bg-[var(--toc-btn-active)] py-2"
|
||||
>
|
||||
<div
|
||||
class:list={[
|
||||
"transition w-5 h-5 shrink-0 rounded-lg text-xs flex items-center justify-center font-bold",
|
||||
{
|
||||
"bg-[var(--toc-badge-bg)] text-[var(--btn-content)]":
|
||||
heading.depth == minDepth,
|
||||
"ml-4":
|
||||
heading.depth ==
|
||||
minDepth + 1,
|
||||
"ml-8":
|
||||
heading.depth ==
|
||||
minDepth + 2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{heading.depth == minDepth &&
|
||||
heading1Count++}
|
||||
{heading.depth == minDepth + 1 && (
|
||||
<div class="transition w-2 h-2 rounded-[0.1875rem] bg-[var(--toc-badge-bg)]" />
|
||||
)}
|
||||
{heading.depth == minDepth + 2 && (
|
||||
<div class="transition w-1.5 h-1.5 rounded-sm bg-black/5 dark:bg-white/10" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
class:list={[
|
||||
"transition text-sm",
|
||||
{
|
||||
"text-90":
|
||||
heading.depth == minDepth ||
|
||||
heading.depth ==
|
||||
minDepth + 1,
|
||||
"text-75":
|
||||
heading.depth ==
|
||||
minDepth + 2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{removeTailingHash(heading.text)}
|
||||
</div>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class:list={["transition text-sm", {
|
||||
"text-50": heading.depth == minDepth || heading.depth == minDepth + 1,
|
||||
"text-30": heading.depth == minDepth + 2,
|
||||
}]}>{removeTailingHash(heading.text)}</div>
|
||||
</a>
|
||||
)}
|
||||
<div id="active-indicator" style="opacity: 0" class:list={[{'hidden': headings.length == 0}, "-z-10 absolute bg-[var(--toc-btn-hover)] left-0 right-0 rounded-xl transition-all " +
|
||||
"group-hover:bg-transparent border-2 border-[var(--toc-btn-hover)] group-hover:border-[var(--toc-btn-active)] border-dashed"]}></div>
|
||||
</table-of-contents>}
|
||||
</div>
|
||||
</div>
|
||||
</WidgetLayout>
|
||||
</table-of-contents>
|
||||
|
||||
<style>
|
||||
table-of-contents a.visible {
|
||||
background-color: var(--toc-btn-active) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
class TableOfContents extends HTMLElement {
|
||||
tocEl: HTMLElement | null = null;
|
||||
visibleClass = "visible";
|
||||
observer: IntersectionObserver;
|
||||
anchorNavTarget: HTMLElement | null = null;
|
||||
headingIdxMap = new Map<string, number>();
|
||||
headings: HTMLElement[] = [];
|
||||
sections: HTMLElement[] = [];
|
||||
tocEntries: HTMLAnchorElement[] = [];
|
||||
active: boolean[] = [];
|
||||
activeIndicator: HTMLElement | null = null;
|
||||
class TableOfContents extends HTMLElement {
|
||||
tocEl: HTMLElement | null = null;
|
||||
visibleClass = "visible";
|
||||
observer: IntersectionObserver;
|
||||
anchorNavTarget: HTMLElement | null = null;
|
||||
headingIdxMap = new Map<string, number>();
|
||||
sections: HTMLElement[] = [];
|
||||
tocEntries: HTMLAnchorElement[] = [];
|
||||
// active 数组不再需要,我们将直接追踪 activeIndex
|
||||
activeIndex: number | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.observer = new IntersectionObserver(
|
||||
this.markVisibleSection, { threshold: 0 }
|
||||
);
|
||||
};
|
||||
|
||||
markVisibleSection = (entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach((entry) => {
|
||||
const id = entry.target.children[0]?.getAttribute("id");
|
||||
const idx = id ? this.headingIdxMap.get(id) : undefined;
|
||||
if (idx != undefined)
|
||||
this.active[idx] = entry.isIntersecting;
|
||||
|
||||
if (entry.isIntersecting && this.anchorNavTarget == entry.target.firstChild)
|
||||
this.anchorNavTarget = null;
|
||||
});
|
||||
|
||||
if (!this.active.includes(true))
|
||||
this.fallback();
|
||||
this.update();
|
||||
};
|
||||
|
||||
toggleActiveHeading = () => {
|
||||
let i = this.active.length - 1;
|
||||
let min = this.active.length - 1, max = -1;
|
||||
while (i >= 0 && !this.active[i]) {
|
||||
this.tocEntries[i].classList.remove(this.visibleClass);
|
||||
i--;
|
||||
constructor() {
|
||||
super();
|
||||
// 调整 IntersectionObserver 的 rootMargin,使其在章节顶部进入视口时就触发
|
||||
this.observer = new IntersectionObserver(
|
||||
this.handleIntersection,
|
||||
{ rootMargin: "0px 0px -80% 0px" }, // 关键调整
|
||||
);
|
||||
}
|
||||
while (i >= 0 && this.active[i]) {
|
||||
this.tocEntries[i].classList.add(this.visibleClass);
|
||||
min = Math.min(min, i);
|
||||
max = Math.max(max, i);
|
||||
i--;
|
||||
}
|
||||
while (i >= 0) {
|
||||
this.tocEntries[i].classList.remove(this.visibleClass);
|
||||
i--;
|
||||
}
|
||||
if (min > max) {
|
||||
this.activeIndicator?.setAttribute("style", `opacity: 0`);
|
||||
} else {
|
||||
let parentOffset = this.tocEl?.getBoundingClientRect().top || 0;
|
||||
let scrollOffset = this.tocEl?.scrollTop || 0;
|
||||
let top = this.tocEntries[min].getBoundingClientRect().top - parentOffset + scrollOffset;
|
||||
let bottom = this.tocEntries[max].getBoundingClientRect().bottom - parentOffset + scrollOffset;
|
||||
this.activeIndicator?.setAttribute("style", `top: ${top}px; height: ${bottom - top}px`);
|
||||
}
|
||||
};
|
||||
|
||||
scrollToActiveHeading = () => {
|
||||
// If the TOC widget can accommodate both the topmost
|
||||
// and bottommost items, scroll to the topmost item.
|
||||
// Otherwise, scroll to the bottommost one.
|
||||
// --- 核心修改点 1: 新的 IntersectionObserver 回调 ---
|
||||
handleIntersection = (entries: IntersectionObserverEntry[]) => {
|
||||
// 当用户点击链接导航时,忽略 IntersectionObserver 的结果,防止冲突
|
||||
if (this.anchorNavTarget) return;
|
||||
|
||||
if (this.anchorNavTarget || !this.tocEl) return;
|
||||
const activeHeading =
|
||||
document.querySelectorAll<HTMLDivElement>(`#toc .${this.visibleClass}`);
|
||||
if (!activeHeading.length) return;
|
||||
let latestVisibleEntry: IntersectionObserverEntry | undefined;
|
||||
|
||||
const topmost = activeHeading[0];
|
||||
const bottommost = activeHeading[activeHeading.length - 1];
|
||||
const tocHeight = this.tocEl.clientHeight;
|
||||
|
||||
let top;
|
||||
if (bottommost.getBoundingClientRect().bottom -
|
||||
topmost.getBoundingClientRect().top < 0.9 * tocHeight)
|
||||
top = topmost.offsetTop - 32;
|
||||
else
|
||||
top = bottommost.offsetTop - tocHeight * 0.8;
|
||||
|
||||
this.tocEl.scrollTo({
|
||||
top,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
update = () => {
|
||||
requestAnimationFrame(() => {
|
||||
this.toggleActiveHeading();
|
||||
// requestAnimationFrame(() => {
|
||||
this.scrollToActiveHeading();
|
||||
// });
|
||||
});
|
||||
};
|
||||
|
||||
fallback = () => {
|
||||
if (!this.sections.length) return;
|
||||
|
||||
for (let i = 0; i < this.sections.length; i++) {
|
||||
let offsetTop = this.sections[i].getBoundingClientRect().top;
|
||||
let offsetBottom = this.sections[i].getBoundingClientRect().bottom;
|
||||
|
||||
if (this.isInRange(offsetTop, 0, window.innerHeight)
|
||||
|| this.isInRange(offsetBottom, 0, window.innerHeight)
|
||||
|| (offsetTop < 0 && offsetBottom > window.innerHeight)) {
|
||||
this.markActiveHeading(i);
|
||||
// 找到进入视口的、最新的(在文档中位置最靠后)的条目
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
latestVisibleEntry = entry;
|
||||
}
|
||||
}
|
||||
else if (offsetTop > window.innerHeight) break;
|
||||
}
|
||||
};
|
||||
|
||||
markActiveHeading = (idx: number)=> {
|
||||
this.active[idx] = true;
|
||||
};
|
||||
if (latestVisibleEntry) {
|
||||
const id =
|
||||
latestVisibleEntry.target.children[0]?.getAttribute("id");
|
||||
const idx = id ? this.headingIdxMap.get(id) : undefined;
|
||||
if (idx !== undefined) {
|
||||
this.setActiveIndex(idx);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleAnchorClick = (event: Event) => {
|
||||
const anchor = event
|
||||
.composedPath()
|
||||
.find((element) => element instanceof HTMLAnchorElement);
|
||||
// --- 核心修改点 2: 统一设置活动索引并更新UI ---
|
||||
setActiveIndex = (index: number | null) => {
|
||||
// 如果索引没有变化,则不执行任何操作
|
||||
if (this.activeIndex === index) return;
|
||||
|
||||
this.activeIndex = index;
|
||||
|
||||
// 更新UI
|
||||
this.tocEntries.forEach((entry, i) => {
|
||||
if (i === this.activeIndex) {
|
||||
entry.classList.add(this.visibleClass);
|
||||
} else {
|
||||
entry.classList.remove(this.visibleClass);
|
||||
}
|
||||
});
|
||||
|
||||
// 只有在非用户点击导航时才自动滚动
|
||||
if (!this.anchorNavTarget) {
|
||||
this.scrollToActiveHeading();
|
||||
}
|
||||
};
|
||||
|
||||
scrollToActiveHeading = () => {
|
||||
if (this.activeIndex === null || !this.tocEl) return;
|
||||
|
||||
const activeEntry = this.tocEntries[this.activeIndex];
|
||||
if (!activeEntry) return;
|
||||
|
||||
const tocHeight = this.tocEl.clientHeight;
|
||||
const entryTop = activeEntry.offsetTop;
|
||||
const entryHeight = activeEntry.offsetHeight;
|
||||
|
||||
// 检查活动项是否在可视区域内
|
||||
if (
|
||||
entryTop < this.tocEl.scrollTop ||
|
||||
entryTop + entryHeight > this.tocEl.scrollTop + tocHeight
|
||||
) {
|
||||
this.tocEl.scrollTo({
|
||||
top: entryTop - tocHeight / 2 + entryHeight / 2, // 尝试滚动到中间位置
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// --- 核心修改点 3: 处理点击事件 ---
|
||||
handleAnchorClick = (event: Event) => {
|
||||
// 找到被点击的 <a> 标签
|
||||
const anchor = event.currentTarget as HTMLAnchorElement;
|
||||
|
||||
if (anchor) {
|
||||
const id = decodeURIComponent(anchor.hash?.substring(1));
|
||||
const idx = this.headingIdxMap.get(id);
|
||||
|
||||
if (idx !== undefined) {
|
||||
this.anchorNavTarget = this.headings[idx];
|
||||
} else {
|
||||
this.anchorNavTarget = null;
|
||||
// 标记正在进行锚点导航,以暂时禁用 IntersectionObserver 的更新
|
||||
const targetHeading = document.getElementById(id);
|
||||
if (targetHeading) {
|
||||
this.anchorNavTarget = targetHeading;
|
||||
// 在导航完成后重置标志
|
||||
setTimeout(() => {
|
||||
this.anchorNavTarget = null;
|
||||
}, 1000); // 1秒后自动清除,以防万一
|
||||
}
|
||||
// 立即更新高亮
|
||||
this.setActiveIndex(idx);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
isInRange(value: number, min: number, max: number) {
|
||||
return min < value && value < max;
|
||||
};
|
||||
|
||||
connectedCallback() {
|
||||
// wait for the onload animation to finish, which makes the `getBoundingClientRect` return correct values
|
||||
const element = document.querySelector('.prose');
|
||||
if (element) {
|
||||
element.addEventListener('animationend', () => {
|
||||
connectedCallback() {
|
||||
requestAnimationFrame(() => {
|
||||
this.init();
|
||||
}, { once: true });
|
||||
} else {
|
||||
console.debug('Animation element not found');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
init() {
|
||||
this.tocEl = document.getElementById(
|
||||
"toc-inner-wrapper"
|
||||
);
|
||||
init() {
|
||||
this.tocEl = document.getElementById("toc-inner-wrapper") || this;
|
||||
|
||||
if (!this.tocEl) return;
|
||||
this.tocEntries = Array.from(
|
||||
this.querySelectorAll<HTMLAnchorElement>("a[href^='#']"),
|
||||
);
|
||||
|
||||
this.tocEl.addEventListener("click", this.handleAnchorClick, {
|
||||
capture: true,
|
||||
});
|
||||
if (this.tocEntries.length === 0) return;
|
||||
|
||||
this.activeIndicator = document.getElementById("active-indicator");
|
||||
this.sections = new Array(this.tocEntries.length);
|
||||
for (let i = 0; i < this.tocEntries.length; i++) {
|
||||
const entry = this.tocEntries[i];
|
||||
const id = decodeURIComponent(entry.hash?.substring(1));
|
||||
const heading = document.getElementById(id);
|
||||
const section = heading?.parentElement;
|
||||
|
||||
this.tocEntries = Array.from(
|
||||
document.querySelectorAll<HTMLAnchorElement>("#toc a[href^='#']")
|
||||
);
|
||||
|
||||
if (this.tocEntries.length === 0) return;
|
||||
|
||||
this.sections = new Array(this.tocEntries.length);
|
||||
this.headings = new Array(this.tocEntries.length);
|
||||
for (let i = 0; i < this.tocEntries.length; i++) {
|
||||
const id = decodeURIComponent(this.tocEntries[i].hash?.substring(1));
|
||||
const heading = document.getElementById(id);
|
||||
const section = heading?.parentElement;
|
||||
if (heading instanceof HTMLElement && section instanceof HTMLElement) {
|
||||
this.headings[i] = heading;
|
||||
this.sections[i] = section;
|
||||
this.headingIdxMap.set(id, i);
|
||||
if (
|
||||
heading instanceof HTMLElement &&
|
||||
section instanceof HTMLElement
|
||||
) {
|
||||
this.sections[i] = section;
|
||||
this.headingIdxMap.set(id, i);
|
||||
// 为每个链接单独添加点击事件监听器
|
||||
entry.addEventListener("click", this.handleAnchorClick);
|
||||
}
|
||||
}
|
||||
|
||||
this.sections.forEach((section) => this.observer.observe(section));
|
||||
|
||||
// 页面加载时,设置第一个标题为活动状态
|
||||
this.setActiveIndex(0);
|
||||
}
|
||||
this.active = new Array(this.tocEntries.length).fill(false);
|
||||
|
||||
this.sections.forEach((section) =>
|
||||
this.observer.observe(section)
|
||||
);
|
||||
disconnectedCallback() {
|
||||
this.sections.forEach((section) =>
|
||||
this.observer.unobserve(section),
|
||||
);
|
||||
this.observer.disconnect();
|
||||
this.tocEntries.forEach((entry) => {
|
||||
entry.removeEventListener("click", this.handleAnchorClick);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.fallback();
|
||||
this.update();
|
||||
};
|
||||
|
||||
disconnectedCallback() {
|
||||
this.sections.forEach((section) =>
|
||||
this.observer.unobserve(section)
|
||||
);
|
||||
this.observer.disconnect();
|
||||
this.tocEl?.removeEventListener("click", this.handleAnchorClick);
|
||||
};
|
||||
}
|
||||
|
||||
if (!customElements.get("table-of-contents")) {
|
||||
customElements.define("table-of-contents", TableOfContents);
|
||||
}
|
||||
if (!customElements.get("table-of-contents")) {
|
||||
customElements.define("table-of-contents", TableOfContents);
|
||||
}
|
||||
</script>
|
||||
+27
-36
@@ -8,16 +8,16 @@ import type {
|
||||
import { LinkPreset } from "./types/config";
|
||||
|
||||
export const siteConfig: SiteConfig = {
|
||||
title: "Fuwari",
|
||||
subtitle: "Demo Site",
|
||||
lang: "en", // Language code, e.g. 'en', 'zh_CN', 'ja', etc.
|
||||
title: "MilkFunc",
|
||||
subtitle: "Blogs",
|
||||
lang: "zh_CN", // Language code, e.g. 'en', 'zh_CN', 'ja', etc.
|
||||
themeColor: {
|
||||
hue: 250, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345
|
||||
fixed: false, // Hide the theme color picker for visitors
|
||||
fixed: true, // Hide the theme color picker for visitors
|
||||
},
|
||||
banner: {
|
||||
enable: false,
|
||||
src: "assets/images/demo-banner.png", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
|
||||
enable: true,
|
||||
src: "assets/images/banner.webp", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
|
||||
position: "center", // Equivalent to object-position, only supports 'top', 'center', 'bottom'. 'center' by default
|
||||
credit: {
|
||||
enable: false, // Display the credit text of the banner image
|
||||
@@ -40,40 +40,31 @@ export const siteConfig: SiteConfig = {
|
||||
};
|
||||
|
||||
export const navBarConfig: NavBarConfig = {
|
||||
links: [
|
||||
LinkPreset.Home,
|
||||
LinkPreset.Archive,
|
||||
LinkPreset.About,
|
||||
{
|
||||
name: "GitHub",
|
||||
url: "https://github.com/saicaca/fuwari", // Internal links should not include the base path, as it is automatically added
|
||||
external: true, // Show an external link icon and will open in a new tab
|
||||
},
|
||||
],
|
||||
links: [LinkPreset.Home, LinkPreset.Archive, LinkPreset.About],
|
||||
};
|
||||
|
||||
export const profileConfig: ProfileConfig = {
|
||||
avatar: "assets/images/demo-avatar.png", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
|
||||
name: "Lorem Ipsum",
|
||||
bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
avatar: "assets/images/avatar.webp", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
|
||||
name: "CapaCake",
|
||||
bio: "😨为什么我的电容炸了?",
|
||||
links: [
|
||||
{
|
||||
name: "Twitter",
|
||||
icon: "fa6-brands:twitter", // Visit https://icones.js.org/ for icon codes
|
||||
// You will need to install the corresponding icon set if it's not already included
|
||||
// `pnpm add @iconify-json/<icon-set-name>`
|
||||
url: "https://twitter.com",
|
||||
},
|
||||
{
|
||||
name: "Steam",
|
||||
icon: "fa6-brands:steam",
|
||||
url: "https://store.steampowered.com",
|
||||
},
|
||||
{
|
||||
name: "GitHub",
|
||||
icon: "fa6-brands:github",
|
||||
url: "https://github.com/saicaca/fuwari",
|
||||
},
|
||||
// {
|
||||
// name: "Twitter",
|
||||
// icon: "fa6-brands:twitter", // Visit https://icones.js.org/ for icon codes
|
||||
// // You will need to install the corresponding icon set if it's not already included
|
||||
// // `pnpm add @iconify-json/<icon-set-name>`
|
||||
// url: "https://twitter.com",
|
||||
// },
|
||||
// {
|
||||
// name: "Steam",
|
||||
// icon: "fa6-brands:steam",
|
||||
// url: "https://store.steampowered.com",
|
||||
// },
|
||||
// {
|
||||
// name: "GitHub",
|
||||
// icon: "fa6-brands:github",
|
||||
// url: "https://github.com/saicaca/fuwari",
|
||||
// },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ const postsCollection = defineCollection({
|
||||
tags: z.array(z.string()).optional().default([]),
|
||||
category: z.string().optional().nullable().default(""),
|
||||
lang: z.string().optional().default(""),
|
||||
pinned: z.number().optional().default(0),
|
||||
category_pinned: z.number().optional().default(0),
|
||||
|
||||
/* For internal use */
|
||||
prevTitle: z.string().default(""),
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../../blog-post/published
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
title: Draft Example
|
||||
published: 2022-07-01
|
||||
tags: [Markdown, Blogging, Demo]
|
||||
category: Examples
|
||||
draft: true
|
||||
---
|
||||
|
||||
# This Article is a Draft
|
||||
|
||||
This article is currently in a draft state and is not published. Therefore, it will not be visible to the general audience. The content is still a work in progress and may require further editing and review.
|
||||
|
||||
When the article is ready for publication, you can update the "draft" field to "false" in the Frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Draft Example
|
||||
published: 2024-01-11T04:40:26.381Z
|
||||
tags: [Markdown, Blogging, Demo]
|
||||
category: Examples
|
||||
draft: false
|
||||
---
|
||||
@@ -1,311 +0,0 @@
|
||||
---
|
||||
title: Expressive Code Example
|
||||
published: 2024-04-10
|
||||
description: How code blocks look in Markdown using Expressive Code.
|
||||
tags: [Markdown, Blogging, Demo]
|
||||
category: Examples
|
||||
draft: false
|
||||
---
|
||||
|
||||
Here, we'll explore how code blocks look using [Expressive Code](https://expressive-code.com/). The provided examples are based on the official documentation, which you can refer to for further details.
|
||||
|
||||
## Expressive Code
|
||||
|
||||
### Syntax Highlighting
|
||||
|
||||
[Syntax Highlighting](https://expressive-code.com/key-features/syntax-highlighting/)
|
||||
|
||||
#### Regular syntax highlighting
|
||||
|
||||
```js
|
||||
console.log('This code is syntax highlighted!')
|
||||
```
|
||||
|
||||
#### Rendering ANSI escape sequences
|
||||
|
||||
```ansi
|
||||
ANSI colors:
|
||||
- Regular: [31mRed[0m [32mGreen[0m [33mYellow[0m [34mBlue[0m [35mMagenta[0m [36mCyan[0m
|
||||
- Bold: [1;31mRed[0m [1;32mGreen[0m [1;33mYellow[0m [1;34mBlue[0m [1;35mMagenta[0m [1;36mCyan[0m
|
||||
- Dimmed: [2;31mRed[0m [2;32mGreen[0m [2;33mYellow[0m [2;34mBlue[0m [2;35mMagenta[0m [2;36mCyan[0m
|
||||
|
||||
256 colors (showing colors 160-177):
|
||||
[38;5;160m160 [38;5;161m161 [38;5;162m162 [38;5;163m163 [38;5;164m164 [38;5;165m165[0m
|
||||
[38;5;166m166 [38;5;167m167 [38;5;168m168 [38;5;169m169 [38;5;170m170 [38;5;171m171[0m
|
||||
[38;5;172m172 [38;5;173m173 [38;5;174m174 [38;5;175m175 [38;5;176m176 [38;5;177m177[0m
|
||||
|
||||
Full RGB colors:
|
||||
[38;2;34;139;34mForestGreen - RGB(34, 139, 34)[0m
|
||||
|
||||
Text formatting: [1mBold[0m [2mDimmed[0m [3mItalic[0m [4mUnderline[0m
|
||||
```
|
||||
|
||||
### Editor & Terminal Frames
|
||||
|
||||
[Editor & Terminal Frames](https://expressive-code.com/key-features/frames/)
|
||||
|
||||
#### Code editor frames
|
||||
|
||||
```js title="my-test-file.js"
|
||||
console.log('Title attribute example')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```html
|
||||
<!-- src/content/index.html -->
|
||||
<div>File name comment example</div>
|
||||
```
|
||||
|
||||
#### Terminal frames
|
||||
|
||||
```bash
|
||||
echo "This terminal frame has no title"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```powershell title="PowerShell terminal example"
|
||||
Write-Output "This one has a title!"
|
||||
```
|
||||
|
||||
#### Overriding frame types
|
||||
|
||||
```sh frame="none"
|
||||
echo "Look ma, no frame!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```ps frame="code" title="PowerShell Profile.ps1"
|
||||
# Without overriding, this would be a terminal frame
|
||||
function Watch-Tail { Get-Content -Tail 20 -Wait $args }
|
||||
New-Alias tail Watch-Tail
|
||||
```
|
||||
|
||||
### Text & Line Markers
|
||||
|
||||
[Text & Line Markers](https://expressive-code.com/key-features/text-markers/)
|
||||
|
||||
#### Marking full lines & line ranges
|
||||
|
||||
```js {1, 4, 7-8}
|
||||
// Line 1 - targeted by line number
|
||||
// Line 2
|
||||
// Line 3
|
||||
// Line 4 - targeted by line number
|
||||
// Line 5
|
||||
// Line 6
|
||||
// Line 7 - targeted by range "7-8"
|
||||
// Line 8 - targeted by range "7-8"
|
||||
```
|
||||
|
||||
#### Selecting line marker types (mark, ins, del)
|
||||
|
||||
```js title="line-markers.js" del={2} ins={3-4} {6}
|
||||
function demo() {
|
||||
console.log('this line is marked as deleted')
|
||||
// This line and the next one are marked as inserted
|
||||
console.log('this is the second inserted line')
|
||||
|
||||
return 'this line uses the neutral default marker type'
|
||||
}
|
||||
```
|
||||
|
||||
#### Adding labels to line markers
|
||||
|
||||
```jsx {"1":5} del={"2":7-8} ins={"3":10-12}
|
||||
// labeled-line-markers.jsx
|
||||
<button
|
||||
role="button"
|
||||
{...props}
|
||||
value={value}
|
||||
className={buttonClassName}
|
||||
disabled={disabled}
|
||||
active={active}
|
||||
>
|
||||
{children &&
|
||||
!active &&
|
||||
(typeof children === 'string' ? <span>{children}</span> : children)}
|
||||
</button>
|
||||
```
|
||||
|
||||
#### Adding long labels on their own lines
|
||||
|
||||
```jsx {"1. Provide the value prop here:":5-6} del={"2. Remove the disabled and active states:":8-10} ins={"3. Add this to render the children inside the button:":12-15}
|
||||
// labeled-line-markers.jsx
|
||||
<button
|
||||
role="button"
|
||||
{...props}
|
||||
|
||||
value={value}
|
||||
className={buttonClassName}
|
||||
|
||||
disabled={disabled}
|
||||
active={active}
|
||||
>
|
||||
|
||||
{children &&
|
||||
!active &&
|
||||
(typeof children === 'string' ? <span>{children}</span> : children)}
|
||||
</button>
|
||||
```
|
||||
|
||||
#### Using diff-like syntax
|
||||
|
||||
```diff
|
||||
+this line will be marked as inserted
|
||||
-this line will be marked as deleted
|
||||
this is a regular line
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```diff
|
||||
--- a/README.md
|
||||
+++ b/README.md
|
||||
@@ -1,3 +1,4 @@
|
||||
+this is an actual diff file
|
||||
-all contents will remain unmodified
|
||||
no whitespace will be removed either
|
||||
```
|
||||
|
||||
#### Combining syntax highlighting with diff-like syntax
|
||||
|
||||
```diff lang="js"
|
||||
function thisIsJavaScript() {
|
||||
// This entire block gets highlighted as JavaScript,
|
||||
// and we can still add diff markers to it!
|
||||
- console.log('Old code to be removed')
|
||||
+ console.log('New and shiny code!')
|
||||
}
|
||||
```
|
||||
|
||||
#### Marking individual text inside lines
|
||||
|
||||
```js "given text"
|
||||
function demo() {
|
||||
// Mark any given text inside lines
|
||||
return 'Multiple matches of the given text are supported';
|
||||
}
|
||||
```
|
||||
|
||||
#### Regular expressions
|
||||
|
||||
```ts /ye[sp]/
|
||||
console.log('The words yes and yep will be marked.')
|
||||
```
|
||||
|
||||
#### Escaping forward slashes
|
||||
|
||||
```sh /\/ho.*\//
|
||||
echo "Test" > /home/test.txt
|
||||
```
|
||||
|
||||
#### Selecting inline marker types (mark, ins, del)
|
||||
|
||||
```js "return true;" ins="inserted" del="deleted"
|
||||
function demo() {
|
||||
console.log('These are inserted and deleted marker types');
|
||||
// The return statement uses the default marker type
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Word Wrap
|
||||
|
||||
[Word Wrap](https://expressive-code.com/key-features/word-wrap/)
|
||||
|
||||
#### Configuring word wrap per block
|
||||
|
||||
```js wrap
|
||||
// Example with wrap
|
||||
function getLongString() {
|
||||
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```js wrap=false
|
||||
// Example with wrap=false
|
||||
function getLongString() {
|
||||
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
|
||||
}
|
||||
```
|
||||
|
||||
#### Configuring indentation of wrapped lines
|
||||
|
||||
```js wrap preserveIndent
|
||||
// Example with preserveIndent (enabled by default)
|
||||
function getLongString() {
|
||||
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```js wrap preserveIndent=false
|
||||
// Example with preserveIndent=false
|
||||
function getLongString() {
|
||||
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
|
||||
}
|
||||
```
|
||||
|
||||
## Collapsible Sections
|
||||
|
||||
[Collapsible Sections](https://expressive-code.com/plugins/collapsible-sections/)
|
||||
|
||||
```js collapse={1-5, 12-14, 21-24}
|
||||
// All this boilerplate setup code will be collapsed
|
||||
import { someBoilerplateEngine } from '@example/some-boilerplate'
|
||||
import { evenMoreBoilerplate } from '@example/even-more-boilerplate'
|
||||
|
||||
const engine = someBoilerplateEngine(evenMoreBoilerplate())
|
||||
|
||||
// This part of the code will be visible by default
|
||||
engine.doSomething(1, 2, 3, calcFn)
|
||||
|
||||
function calcFn() {
|
||||
// You can have multiple collapsed sections
|
||||
const a = 1
|
||||
const b = 2
|
||||
const c = a + b
|
||||
|
||||
// This will remain visible
|
||||
console.log(`Calculation result: ${a} + ${b} = ${c}`)
|
||||
return c
|
||||
}
|
||||
|
||||
// All this code until the end of the block will be collapsed again
|
||||
engine.closeConnection()
|
||||
engine.freeMemory()
|
||||
engine.shutdown({ reason: 'End of example boilerplate code' })
|
||||
```
|
||||
|
||||
## Line Numbers
|
||||
|
||||
[Line Numbers](https://expressive-code.com/plugins/line-numbers/)
|
||||
|
||||
### Displaying line numbers per block
|
||||
|
||||
```js showLineNumbers
|
||||
// This code block will show line numbers
|
||||
console.log('Greetings from line 2!')
|
||||
console.log('I am on line 3')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```js showLineNumbers=false
|
||||
// Line numbers are disabled for this block
|
||||
console.log('Hello?')
|
||||
console.log('Sorry, do you know what line I am on?')
|
||||
```
|
||||
|
||||
### Changing the starting line number
|
||||
|
||||
```js showLineNumbers startLineNumber=5
|
||||
console.log('Greetings from line 5!')
|
||||
console.log('I am on line 6')
|
||||
```
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 218 KiB |
@@ -1,51 +0,0 @@
|
||||
---
|
||||
title: Simple Guides for Fuwari
|
||||
published: 2024-04-01
|
||||
description: "How to use this blog template."
|
||||
image: "./cover.jpeg"
|
||||
tags: ["Fuwari", "Blogging", "Customization"]
|
||||
category: Guides
|
||||
draft: false
|
||||
---
|
||||
|
||||
> Cover image source: [Source](https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/208fc754-890d-4adb-9753-2c963332675d/width=2048/01651-1456859105-(colour_1.5),girl,_Blue,yellow,green,cyan,purple,red,pink,_best,8k,UHD,masterpiece,male%20focus,%201boy,gloves,%20ponytail,%20long%20hair,.jpeg)
|
||||
|
||||
This blog template is built with [Astro](https://astro.build/). For the things that are not mentioned in this guide, you may find the answers in the [Astro Docs](https://docs.astro.build/).
|
||||
|
||||
## Front-matter of Posts
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: My First Blog Post
|
||||
published: 2023-09-09
|
||||
description: This is the first post of my new Astro blog.
|
||||
image: ./cover.jpg
|
||||
tags: [Foo, Bar]
|
||||
category: Front-end
|
||||
draft: false
|
||||
---
|
||||
```
|
||||
|
||||
| Attribute | Description |
|
||||
|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `title` | The title of the post. |
|
||||
| `published` | The date the post was published. |
|
||||
| `description` | A short description of the post. Displayed on index page. |
|
||||
| `image` | The cover image path of the post.<br/>1. Start with `http://` or `https://`: Use web image<br/>2. Start with `/`: For image in `public` dir<br/>3. With none of the prefixes: Relative to the markdown file |
|
||||
| `tags` | The tags of the post. |
|
||||
| `category` | The category of the post. |
|
||||
| `draft` | If this post is still a draft, which won't be displayed. |
|
||||
|
||||
## Where to Place the Post Files
|
||||
|
||||
|
||||
|
||||
Your post files should be placed in `src/content/posts/` directory. You can also create sub-directories to better organize your posts and assets.
|
||||
|
||||
```
|
||||
src/content/posts/
|
||||
├── post-1.md
|
||||
└── post-2/
|
||||
├── cover.png
|
||||
└── index.md
|
||||
```
|
||||
@@ -1,107 +0,0 @@
|
||||
---
|
||||
title: Markdown Extended Features
|
||||
published: 2024-05-01
|
||||
updated: 2024-11-29
|
||||
description: 'Read more about Markdown features in Fuwari'
|
||||
image: ''
|
||||
tags: [Demo, Example, Markdown, Fuwari]
|
||||
category: 'Examples'
|
||||
draft: false
|
||||
---
|
||||
|
||||
## GitHub Repository Cards
|
||||
You can add dynamic cards that link to GitHub repositories, on page load, the repository information is pulled from the GitHub API.
|
||||
|
||||
::github{repo="Fabrizz/MMM-OnSpotify"}
|
||||
|
||||
Create a GitHub repository card with the code `::github{repo="<owner>/<repo>"}`.
|
||||
|
||||
```markdown
|
||||
::github{repo="saicaca/fuwari"}
|
||||
```
|
||||
|
||||
## GitLab Repository Cards
|
||||
|
||||
You can also add dynamic cards that show a GitLab repository, on page load, the repository information is pulled from the GitLab API.
|
||||
|
||||
::gitlab{repo="gitlab-org/gitlab"}
|
||||
|
||||
Create a GitHub repository card with the code `::gitlab{repo="<owner>/<repo>"}`.
|
||||
|
||||
```mdx
|
||||
::gitlab{repo="gitlab-org/gitlab"}
|
||||
```
|
||||
|
||||
## Admonitions
|
||||
|
||||
Following types of admonitions are supported: `note` `tip` `important` `warning` `caution`
|
||||
|
||||
:::note
|
||||
Highlights information that users should take into account, even when skimming.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Optional information to help a user be more successful.
|
||||
:::
|
||||
|
||||
:::important
|
||||
Crucial information necessary for users to succeed.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Critical content demanding immediate user attention due to potential risks.
|
||||
:::
|
||||
|
||||
:::caution
|
||||
Negative potential consequences of an action.
|
||||
:::
|
||||
|
||||
### Basic Syntax
|
||||
|
||||
```markdown
|
||||
:::note
|
||||
Highlights information that users should take into account, even when skimming.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Optional information to help a user be more successful.
|
||||
:::
|
||||
```
|
||||
|
||||
### Custom Titles
|
||||
|
||||
The title of the admonition can be customized.
|
||||
|
||||
:::note[MY CUSTOM TITLE]
|
||||
This is a note with a custom title.
|
||||
:::
|
||||
|
||||
```markdown
|
||||
:::note[MY CUSTOM TITLE]
|
||||
This is a note with a custom title.
|
||||
:::
|
||||
```
|
||||
|
||||
### GitHub Syntax
|
||||
|
||||
> [!TIP]
|
||||
> [The GitHub syntax](https://github.com/orgs/community/discussions/16925) is also supported.
|
||||
|
||||
```
|
||||
> [!NOTE]
|
||||
> The GitHub syntax is also supported.
|
||||
|
||||
> [!TIP]
|
||||
> The GitHub syntax is also supported.
|
||||
```
|
||||
|
||||
### Spoiler
|
||||
|
||||
You can add spoilers to your text. The text also supports **Markdown** syntax.
|
||||
|
||||
The content :spoiler[is hidden **ayyy**]!
|
||||
|
||||
```markdown
|
||||
The content :spoiler[is hidden **ayyy**]!
|
||||
|
||||
```
|
||||
@@ -1,175 +0,0 @@
|
||||
---
|
||||
title: Markdown Example
|
||||
published: 2023-10-01
|
||||
description: A simple example of a Markdown blog post.
|
||||
tags: [Markdown, Blogging, Demo]
|
||||
category: Examples
|
||||
draft: false
|
||||
---
|
||||
|
||||
# An h1 header
|
||||
|
||||
Paragraphs are separated by a blank line.
|
||||
|
||||
2nd paragraph. _Italic_, **bold**, and `monospace`. Itemized lists
|
||||
look like:
|
||||
|
||||
- this one
|
||||
- that one
|
||||
- the other one
|
||||
|
||||
Note that --- not considering the asterisk --- the actual text
|
||||
content starts at 4-columns in.
|
||||
|
||||
> Block quotes are
|
||||
> written like so.
|
||||
>
|
||||
> They can span multiple paragraphs,
|
||||
> if you like.
|
||||
|
||||
Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all
|
||||
in chapters 12--14"). Three dots ... will be converted to an ellipsis.
|
||||
Unicode is supported. ☺
|
||||
|
||||
## An h2 header
|
||||
|
||||
Here's a numbered list:
|
||||
|
||||
1. first item
|
||||
2. second item
|
||||
3. third item
|
||||
|
||||
Note again how the actual text starts at 4 columns in (4 characters
|
||||
from the left side). Here's a code sample:
|
||||
|
||||
# Let me re-iterate ...
|
||||
for i in 1 .. 10 { do-something(i) }
|
||||
|
||||
As you probably guessed, indented 4 spaces. By the way, instead of
|
||||
indenting the block, you can use delimited blocks, if you like:
|
||||
|
||||
```
|
||||
define foobar() {
|
||||
print "Welcome to flavor country!";
|
||||
}
|
||||
```
|
||||
|
||||
(which makes copying & pasting easier). You can optionally mark the
|
||||
delimited block for Pandoc to syntax highlight it:
|
||||
|
||||
```python
|
||||
import time
|
||||
# Quick, count to ten!
|
||||
for i in range(10):
|
||||
# (but not *too* quick)
|
||||
time.sleep(0.5)
|
||||
print i
|
||||
```
|
||||
|
||||
### An h3 header
|
||||
|
||||
Now a nested list:
|
||||
|
||||
1. First, get these ingredients:
|
||||
|
||||
- carrots
|
||||
- celery
|
||||
- lentils
|
||||
|
||||
2. Boil some water.
|
||||
|
||||
3. Dump everything in the pot and follow
|
||||
this algorithm:
|
||||
|
||||
find wooden spoon
|
||||
uncover pot
|
||||
stir
|
||||
cover pot
|
||||
balance wooden spoon precariously on pot handle
|
||||
wait 10 minutes
|
||||
goto first step (or shut off burner when done)
|
||||
|
||||
Do not bump wooden spoon or it will fall.
|
||||
|
||||
Notice again how text always lines up on 4-space indents (including
|
||||
that last line which continues item 3 above).
|
||||
|
||||
Here's a link to [a website](http://foo.bar), to a [local
|
||||
doc](local-doc.html), and to a [section heading in the current
|
||||
doc](#an-h2-header). Here's a footnote [^1].
|
||||
|
||||
[^1]: Footnote text goes here.
|
||||
|
||||
Tables can look like this:
|
||||
|
||||
size material color
|
||||
|
||||
---
|
||||
|
||||
9 leather brown
|
||||
10 hemp canvas natural
|
||||
11 glass transparent
|
||||
|
||||
Table: Shoes, their sizes, and what they're made of
|
||||
|
||||
(The above is the caption for the table.) Pandoc also supports
|
||||
multi-line tables:
|
||||
|
||||
---
|
||||
|
||||
keyword text
|
||||
|
||||
---
|
||||
|
||||
red Sunsets, apples, and
|
||||
other red or reddish
|
||||
things.
|
||||
|
||||
green Leaves, grass, frogs
|
||||
and other things it's
|
||||
not easy being.
|
||||
|
||||
---
|
||||
|
||||
A horizontal rule follows.
|
||||
|
||||
---
|
||||
|
||||
Here's a definition list:
|
||||
|
||||
apples
|
||||
: Good for making applesauce.
|
||||
oranges
|
||||
: Citrus!
|
||||
tomatoes
|
||||
: There's no "e" in tomatoe.
|
||||
|
||||
Again, text is indented 4 spaces. (Put a blank line between each
|
||||
term/definition pair to spread things out more.)
|
||||
|
||||
Here's a "line block":
|
||||
|
||||
| Line one
|
||||
| Line too
|
||||
| Line tree
|
||||
|
||||
and images can be specified like so:
|
||||
|
||||
[//]: # ()
|
||||
|
||||
Inline math equations go in like so: $\omega = d\phi / dt$. Display
|
||||
math should get its own line and be put in in double-dollarsigns:
|
||||
|
||||
$$I = \int \rho R^{2} dV$$
|
||||
|
||||
$$
|
||||
\begin{equation*}
|
||||
\pi
|
||||
=3.1415926535
|
||||
\;8979323846\;2643383279\;5028841971\;6939937510\;5820974944
|
||||
\;5923078164\;0628620899\;8628034825\;3421170679\;\ldots
|
||||
\end{equation*}
|
||||
$$
|
||||
|
||||
And note that you can backslash-escape any punctuation characters
|
||||
which you wish to be displayed literally, ex.: \`foo\`, \*bar\*, etc.
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
title: Include Video in the Posts
|
||||
published: 2023-08-01
|
||||
description: This post demonstrates how to include embedded video in a blog post.
|
||||
tags: [Example, Video]
|
||||
category: Examples
|
||||
draft: false
|
||||
---
|
||||
|
||||
Just copy the embed code from YouTube or other platforms, and paste it in the markdown file.
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Include Video in the Post
|
||||
published: 2023-10-19
|
||||
// ...
|
||||
---
|
||||
|
||||
<iframe width="100%" height="468" src="https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_" title="YouTube video player" frameborder="0" allowfullscreen></iframe>
|
||||
```
|
||||
|
||||
## YouTube
|
||||
|
||||
<iframe width="100%" height="468" src="https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
## Bilibili
|
||||
|
||||
<iframe width="100%" height="468" src="//player.bilibili.com/player.html?bvid=BV1fK4y1s7Qf&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>
|
||||
@@ -1,9 +1,55 @@
|
||||
# About
|
||||
This is the demo site for [Fuwari](https://github.com/saicaca/fuwari).
|
||||
<div class="logo-container">
|
||||
<img src="/image.svg" alt="icon" class="logo-icon">
|
||||
<span class="logo-text">
|
||||
MilkBlogs<span class="logo-dot">.</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
::github{repo="saicaca/fuwari"}
|
||||
<style>
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
> ### Sources of images used in this site
|
||||
> - [Unsplash](https://unsplash.com/)
|
||||
> - [星と少女](https://www.pixiv.net/artworks/108916539) by [Stella](https://www.pixiv.net/users/93273965)
|
||||
> - [Rabbit - v1.4 Showcase](https://civitai.com/posts/586908) by [Rabbit_YourMajesty](https://civitai.com/user/Rabbit_YourMajesty)
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
height: clamp(3rem, 5vw, 8rem); /* 与文字大小保持一致的比例 */
|
||||
width: auto;
|
||||
flex-shrink: 0;
|
||||
margin-right: clamp(5px, 1vw, 20px); /* 间距也响应式 */
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: clamp(3rem, 5vw, 8rem);
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo-dot {
|
||||
color: #ff1e00ff
|
||||
}
|
||||
|
||||
</style>
|
||||
## 👋 Hi,欢迎来到MilkBlogs!
|
||||
|
||||
我是一名**电子信息专业**学生 ,立志成为一名优秀的**嵌入式系统工程师**。
|
||||
## 🏗技术栈
|
||||
* 📟 嵌入式 MCU 开发 (Bare-metal & RTOS)
|
||||
* 🐧 嵌入式 Linux
|
||||
* <...> C / C++ , Python ,Shell , Rust (探索中)
|
||||
## 🚀开源
|
||||
网站项目已开源
|
||||
::gitea{repo="CapaCake/milkblogs-fuwari" service="https://git.milkfunc.top"}
|
||||
## 📬建立联系
|
||||
如果你有什么问题,欢迎交流:
|
||||
* **GitHub**: [@CapaCake](https://github.com/CapaCake)
|
||||
* **Blog**: [milkfunc.top](https://milkfunc.top)
|
||||
* **Email**: [executor.cat@outlook.com](executor.cat@outlook.com)
|
||||
## 🥂鸣谢
|
||||
本站基于以下优秀的开源项目构建:
|
||||
* **Framework**: [Astro](https://astro.build/)
|
||||
* **BlogTemplate**: [Fuwari](https://github.com/saicaca/fuwari)
|
||||
@@ -32,6 +32,12 @@ enum I18nKey {
|
||||
author = "author",
|
||||
publishedAt = "publishedAt",
|
||||
license = "license",
|
||||
|
||||
notFound = "notFound",
|
||||
notFoundDesc = "notFoundDesc",
|
||||
backToHome = "backToHome",
|
||||
pinned = "pinned",
|
||||
toc = "toc",
|
||||
}
|
||||
|
||||
export default I18nKey;
|
||||
|
||||
@@ -35,4 +35,10 @@ export const en: Translation = {
|
||||
[Key.author]: "Author",
|
||||
[Key.publishedAt]: "Published at",
|
||||
[Key.license]: "License",
|
||||
|
||||
[Key.notFound]: "Page Not Found",
|
||||
[Key.notFoundDesc]: "The link is broken, the page has gone missing",
|
||||
[Key.backToHome]: "Back to Home",
|
||||
[Key.pinned]: "Pinned",
|
||||
[Key.toc]: "Toc",
|
||||
};
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const es: Translation = {
|
||||
[Key.home]: "Inicio",
|
||||
[Key.about]: "Sobre mí",
|
||||
[Key.archive]: "Archivo",
|
||||
[Key.search]: "Buscar",
|
||||
|
||||
[Key.tags]: "Etiquetas",
|
||||
[Key.categories]: "Categorías",
|
||||
[Key.recentPosts]: "Publicaciones recientes",
|
||||
|
||||
[Key.comments]: "Comentarios",
|
||||
|
||||
[Key.untitled]: "Sin título",
|
||||
[Key.uncategorized]: "Sin categoría",
|
||||
[Key.noTags]: "Sin etiquetas",
|
||||
|
||||
[Key.wordCount]: "palabra",
|
||||
[Key.wordsCount]: "palabras",
|
||||
[Key.minuteCount]: "minuto",
|
||||
[Key.minutesCount]: "minutos",
|
||||
[Key.postCount]: "publicación",
|
||||
[Key.postsCount]: "publicaciones",
|
||||
|
||||
[Key.themeColor]: "Color del tema",
|
||||
|
||||
[Key.lightMode]: "Claro",
|
||||
[Key.darkMode]: "Oscuro",
|
||||
[Key.systemMode]: "Sistema",
|
||||
|
||||
[Key.more]: "Más",
|
||||
|
||||
[Key.author]: "Autor",
|
||||
[Key.publishedAt]: "Publicado el",
|
||||
[Key.license]: "Licencia",
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const id: Translation = {
|
||||
[Key.home]: "Beranda",
|
||||
[Key.about]: "Tentang",
|
||||
[Key.archive]: "Arsip",
|
||||
[Key.search]: "Cari",
|
||||
|
||||
[Key.tags]: "Tag",
|
||||
[Key.categories]: "Kategori",
|
||||
[Key.recentPosts]: "Postingan Terbaru",
|
||||
|
||||
[Key.comments]: "Komentar",
|
||||
|
||||
[Key.untitled]: "Tanpa Judul",
|
||||
[Key.uncategorized]: "Tanpa Kategori",
|
||||
[Key.noTags]: "Tanpa Tag",
|
||||
|
||||
[Key.wordCount]: "kata",
|
||||
[Key.wordsCount]: "kata",
|
||||
[Key.minuteCount]: "menit",
|
||||
[Key.minutesCount]: "menit",
|
||||
[Key.postCount]: "postingan",
|
||||
[Key.postsCount]: "postingan",
|
||||
|
||||
[Key.themeColor]: "Warna Tema",
|
||||
|
||||
[Key.lightMode]: "Terang",
|
||||
[Key.darkMode]: "Gelap",
|
||||
[Key.systemMode]: "Sistem",
|
||||
|
||||
[Key.more]: "Lainnya",
|
||||
|
||||
[Key.author]: "Penulis",
|
||||
[Key.publishedAt]: "Diterbitkan pada",
|
||||
[Key.license]: "Lisensi",
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const ja: Translation = {
|
||||
[Key.home]: "Home",
|
||||
[Key.about]: "About",
|
||||
[Key.archive]: "Archive",
|
||||
[Key.search]: "検索",
|
||||
|
||||
[Key.tags]: "タグ",
|
||||
[Key.categories]: "カテゴリ",
|
||||
[Key.recentPosts]: "最近の投稿",
|
||||
|
||||
[Key.comments]: "コメント",
|
||||
|
||||
[Key.untitled]: "タイトルなし",
|
||||
[Key.uncategorized]: "カテゴリなし",
|
||||
[Key.noTags]: "タグなし",
|
||||
|
||||
[Key.wordCount]: "文字",
|
||||
[Key.wordsCount]: "文字",
|
||||
[Key.minuteCount]: "分",
|
||||
[Key.minutesCount]: "分",
|
||||
[Key.postCount]: "件の投稿",
|
||||
[Key.postsCount]: "件の投稿",
|
||||
|
||||
[Key.themeColor]: "テーマカラー",
|
||||
|
||||
[Key.lightMode]: "ライト",
|
||||
[Key.darkMode]: "ダーク",
|
||||
[Key.systemMode]: "システム",
|
||||
|
||||
[Key.more]: "もっと",
|
||||
|
||||
[Key.author]: "作者",
|
||||
[Key.publishedAt]: "公開日",
|
||||
[Key.license]: "ライセンス",
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const ko: Translation = {
|
||||
[Key.home]: "홈",
|
||||
[Key.about]: "소개",
|
||||
[Key.archive]: "아카이브",
|
||||
[Key.search]: "검색",
|
||||
|
||||
[Key.tags]: "태그",
|
||||
[Key.categories]: "카테고리",
|
||||
[Key.recentPosts]: "최근 게시물",
|
||||
|
||||
[Key.comments]: "댓글",
|
||||
|
||||
[Key.untitled]: "제목 없음",
|
||||
[Key.uncategorized]: "분류되지 않음",
|
||||
[Key.noTags]: "태그 없음",
|
||||
|
||||
[Key.wordCount]: "단어",
|
||||
[Key.wordsCount]: "단어",
|
||||
[Key.minuteCount]: "분",
|
||||
[Key.minutesCount]: "분",
|
||||
[Key.postCount]: "게시물",
|
||||
[Key.postsCount]: "게시물",
|
||||
|
||||
[Key.themeColor]: "테마 색상",
|
||||
|
||||
[Key.lightMode]: "밝은 모드",
|
||||
[Key.darkMode]: "어두운 모드",
|
||||
[Key.systemMode]: "시스템 모드",
|
||||
|
||||
[Key.more]: "더 보기",
|
||||
|
||||
[Key.author]: "저자",
|
||||
[Key.publishedAt]: "게시일",
|
||||
[Key.license]: "라이선스",
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const th: Translation = {
|
||||
[Key.home]: "หน้าแรก",
|
||||
[Key.about]: "เกี่ยวกับ",
|
||||
[Key.archive]: "คลัง",
|
||||
[Key.search]: "ค้นหา",
|
||||
|
||||
[Key.tags]: "ป้ายกำกับ",
|
||||
[Key.categories]: "หมวดหมู่",
|
||||
[Key.recentPosts]: "โพสต์ล่าสุด",
|
||||
|
||||
[Key.comments]: "ความคิดเห็น",
|
||||
|
||||
[Key.untitled]: "ไม่ได้ตั้งชื่อ",
|
||||
[Key.uncategorized]: "ไม่ได้จัดหมวดหมู่",
|
||||
[Key.noTags]: "ไม่มีป้ายกำกับ",
|
||||
|
||||
[Key.wordCount]: "คำ",
|
||||
[Key.wordsCount]: "คำ",
|
||||
[Key.minuteCount]: "นาที",
|
||||
[Key.minutesCount]: "นาที",
|
||||
[Key.postCount]: "โพสต์",
|
||||
[Key.postsCount]: "โพสต์",
|
||||
|
||||
[Key.themeColor]: "สีของธีม",
|
||||
|
||||
[Key.lightMode]: "สว่าง",
|
||||
[Key.darkMode]: "มืด",
|
||||
[Key.systemMode]: "ตามระบบ",
|
||||
|
||||
[Key.more]: "ดูเพิ่ม",
|
||||
|
||||
[Key.author]: "ผู้เขียน",
|
||||
[Key.publishedAt]: "เผยแพร่เมื่อ",
|
||||
[Key.license]: "สัญญาอนุญาต",
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const tr: Translation = {
|
||||
[Key.home]: "Anasayfa",
|
||||
[Key.about]: "Hakkında",
|
||||
[Key.archive]: "Arşiv",
|
||||
[Key.search]: "Ara",
|
||||
|
||||
[Key.tags]: "Taglar",
|
||||
[Key.categories]: "Katagoriler",
|
||||
[Key.recentPosts]: "Son Paylaşımlar",
|
||||
|
||||
[Key.comments]: "Yorumlar",
|
||||
|
||||
[Key.untitled]: "Başlıksız",
|
||||
[Key.uncategorized]: "Katagorisiz",
|
||||
[Key.noTags]: "Tag Bulunamadı",
|
||||
|
||||
[Key.wordCount]: "kelime",
|
||||
[Key.wordsCount]: "kelime",
|
||||
[Key.minuteCount]: "dakika",
|
||||
[Key.minutesCount]: "dakika",
|
||||
[Key.postCount]: "gönderi",
|
||||
[Key.postsCount]: "gönderiler",
|
||||
|
||||
[Key.themeColor]: "Tema Rengi",
|
||||
|
||||
[Key.lightMode]: "Aydınlık",
|
||||
[Key.darkMode]: "Koyu",
|
||||
[Key.systemMode]: "Sistem",
|
||||
|
||||
[Key.more]: "Daha Fazla",
|
||||
|
||||
[Key.author]: "Yazar",
|
||||
[Key.publishedAt]: "Yayınlanma:",
|
||||
[Key.license]: "Lisans",
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
|
||||
export const vi: Translation = {
|
||||
[Key.home]: "Trang chủ",
|
||||
[Key.about]: "Giới thiệu",
|
||||
[Key.archive]: "Kho bài",
|
||||
[Key.search]: "Tìm kiếm",
|
||||
|
||||
[Key.tags]: "Thẻ",
|
||||
[Key.categories]: "Danh mục",
|
||||
[Key.recentPosts]: "Bài viết mới nhất",
|
||||
|
||||
[Key.comments]: "Bình luận",
|
||||
|
||||
[Key.untitled]: "Không tiêu đề",
|
||||
[Key.uncategorized]: "Chưa phân loại",
|
||||
[Key.noTags]: "Chưa có thẻ",
|
||||
|
||||
[Key.wordCount]: "từ",
|
||||
[Key.wordsCount]: "từ",
|
||||
[Key.minuteCount]: "phút đọc",
|
||||
[Key.minutesCount]: "phút đọc",
|
||||
[Key.postCount]: "bài viết",
|
||||
[Key.postsCount]: "bài viết",
|
||||
|
||||
[Key.themeColor]: "Màu giao diện",
|
||||
|
||||
[Key.lightMode]: "Sáng",
|
||||
[Key.darkMode]: "Tối",
|
||||
[Key.systemMode]: "Hệ thống",
|
||||
|
||||
[Key.more]: "Thêm",
|
||||
|
||||
[Key.author]: "Tác giả",
|
||||
[Key.publishedAt]: "Đăng vào lúc",
|
||||
[Key.license]: "Giấy phép bản quyền",
|
||||
};
|
||||
@@ -35,4 +35,10 @@ export const zh_CN: Translation = {
|
||||
[Key.author]: "作者",
|
||||
[Key.publishedAt]: "发布于",
|
||||
[Key.license]: "许可协议",
|
||||
|
||||
[Key.notFound]: "页面未找到",
|
||||
[Key.notFoundDesc]: "你访问的链接已断开,页面走丢了",
|
||||
[Key.backToHome]: "返回首页",
|
||||
[Key.pinned]: "置顶",
|
||||
[Key.toc]: "目录",
|
||||
};
|
||||
|
||||
@@ -35,4 +35,10 @@ export const zh_TW: Translation = {
|
||||
[Key.author]: "作者",
|
||||
[Key.publishedAt]: "發佈於",
|
||||
[Key.license]: "許可協議",
|
||||
|
||||
[Key.notFound]: "頁面未找到",
|
||||
[Key.notFoundDesc]: "你訪問的連結已斷開,頁面走丟了",
|
||||
[Key.backToHome]: "返回首頁",
|
||||
[Key.pinned]: "置頂",
|
||||
[Key.toc]: "目錄",
|
||||
};
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { siteConfig } from "../config";
|
||||
import type I18nKey from "./i18nKey";
|
||||
import { en } from "./languages/en";
|
||||
import { es } from "./languages/es";
|
||||
import { id } from "./languages/id";
|
||||
import { ja } from "./languages/ja";
|
||||
import { ko } from "./languages/ko";
|
||||
import { th } from "./languages/th";
|
||||
import { tr } from "./languages/tr";
|
||||
import { vi } from "./languages/vi";
|
||||
import { zh_CN } from "./languages/zh_CN";
|
||||
import { zh_TW } from "./languages/zh_TW";
|
||||
|
||||
@@ -18,24 +11,12 @@ export type Translation = {
|
||||
const defaultTranslation = en;
|
||||
|
||||
const map: { [key: string]: Translation } = {
|
||||
es: es,
|
||||
en: en,
|
||||
en_us: en,
|
||||
en_gb: en,
|
||||
en_au: en,
|
||||
zh_cn: zh_CN,
|
||||
zh_tw: zh_TW,
|
||||
ja: ja,
|
||||
ja_jp: ja,
|
||||
ko: ko,
|
||||
ko_kr: ko,
|
||||
th: th,
|
||||
th_th: th,
|
||||
vi: vi,
|
||||
vi_vn: vi,
|
||||
id: id,
|
||||
tr: tr,
|
||||
tr_tr: tr,
|
||||
};
|
||||
|
||||
export function getTranslation(lang: string): Translation {
|
||||
|
||||
+13
-22
@@ -405,7 +405,19 @@ const setup = () => {
|
||||
}
|
||||
})
|
||||
window.swup.hooks.on('content:replace', initCustomScrollbar)
|
||||
window.swup.hooks.on('visit:start', (visit: {to: {url: string}}) => {
|
||||
window.swup.hooks.on('visit:start', (visit: {to: {url: string};from: { url: string };containers: string[]}) => {
|
||||
|
||||
const postUrlPattern = "/posts/";
|
||||
const isToPost = visit.to.url.includes(postUrlPattern);
|
||||
const isFromPost = visit.from.url.includes(postUrlPattern);
|
||||
|
||||
if (isToPost || isFromPost) {
|
||||
visit.containers = ["main", "#sidebar"]; // 刷新两者
|
||||
} else {
|
||||
// 否则(例如,从首页导航到关于页面),
|
||||
// 仅刷新主内容区域。
|
||||
visit.containers = ["main"];
|
||||
}
|
||||
// change banner height immediately when a link is clicked
|
||||
const bodyElement = document.querySelector('body')
|
||||
if (pathsEqual(visit.to.url, url('/'))) {
|
||||
@@ -419,12 +431,6 @@ const setup = () => {
|
||||
if (heightExtend) {
|
||||
heightExtend.classList.remove('hidden')
|
||||
}
|
||||
|
||||
// Hide the TOC while scrolling back to top
|
||||
let toc = document.getElementById('toc-wrapper');
|
||||
if (toc) {
|
||||
toc.classList.add('toc-not-ready')
|
||||
}
|
||||
});
|
||||
window.swup.hooks.on('page:view', () => {
|
||||
// hide the temp high element when the transition is done
|
||||
@@ -439,12 +445,6 @@ const setup = () => {
|
||||
if (heightExtend) {
|
||||
heightExtend.classList.add('hidden')
|
||||
}
|
||||
|
||||
// Just make the transition looks better
|
||||
const toc = document.getElementById('toc-wrapper');
|
||||
if (toc) {
|
||||
toc.classList.remove('toc-not-ready')
|
||||
}
|
||||
}, 200)
|
||||
});
|
||||
}
|
||||
@@ -455,7 +455,6 @@ if (window?.swup?.hooks) {
|
||||
}
|
||||
|
||||
let backToTopBtn = document.getElementById('back-to-top-btn');
|
||||
let toc = document.getElementById('toc-wrapper');
|
||||
let navbar = document.getElementById('navbar-wrapper')
|
||||
function scrollFunction() {
|
||||
let bannerHeight = window.innerHeight * (BANNER_HEIGHT / 100)
|
||||
@@ -468,14 +467,6 @@ function scrollFunction() {
|
||||
}
|
||||
}
|
||||
|
||||
if (bannerEnabled && toc) {
|
||||
if (document.body.scrollTop > bannerHeight || document.documentElement.scrollTop > bannerHeight) {
|
||||
toc.classList.remove('toc-hide')
|
||||
} else {
|
||||
toc.classList.add('toc-hide')
|
||||
}
|
||||
}
|
||||
|
||||
if (!bannerEnabled) return
|
||||
if (navbar) {
|
||||
const NAVBAR_HEIGHT = 72
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
---
|
||||
import BackToTop from "@components/control/BackToTop.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Navbar from "@components/Navbar.astro";
|
||||
import SideBar from "@components/widget/SideBar.astro";
|
||||
import type { MarkdownHeading } from "astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import ImageWrapper from "../components/misc/ImageWrapper.astro";
|
||||
import TOC from "../components/widget/TOC.astro";
|
||||
import { siteConfig } from "../config";
|
||||
import {
|
||||
BANNER_HEIGHT,
|
||||
@@ -98,28 +96,7 @@ const mainPanelTop = siteConfig.banner.enable
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BackToTop></BackToTop>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The things that should be under the banner, only the TOC for now -->
|
||||
<div class="absolute w-full z-0 hidden 2xl:block">
|
||||
<div class="relative max-w-[var(--page-width)] mx-auto">
|
||||
<!-- TOC component -->
|
||||
{siteConfig.toc.enable && <div id="toc-wrapper" class:list={["hidden lg:block transition absolute top-0 -right-[var(--toc-width)] w-[var(--toc-width)] items-center",
|
||||
{"toc-hide": siteConfig.banner.enable}]}
|
||||
>
|
||||
<div id="toc-inner-wrapper" class="fixed top-14 w-[var(--toc-width)] h-[calc(100vh_-_20rem)] overflow-y-scroll overflow-x-hidden hide-scrollbar">
|
||||
<div id="toc" class="w-full h-full transition-swup-fade ">
|
||||
<div class="h-8 w-full"></div>
|
||||
<TOC headings={headings}></TOC>
|
||||
<div class="h-8 w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<!-- #toc needs to exist for Swup to work normally -->
|
||||
{!siteConfig.toc.enable && <div id="toc"></div>}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import I18nKey from "../i18n/i18nKey";
|
||||
import { i18n } from "../i18n/translation";
|
||||
import MainGridLayout from "../layouts/MainGridLayout.astro";
|
||||
import { url } from "../utils/url-utils";
|
||||
---
|
||||
|
||||
<MainGridLayout title={i18n(I18nKey.notFound)}>
|
||||
<div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative min-h-[70vh]">
|
||||
<div class="card-base z-10 px-9 py-6 relative w-full flex flex-col items-center justify-center">
|
||||
<div class="onload-animation flex flex-col items-center justify-center text-center">
|
||||
<h2 class="text-2xl md:text-3xl font-bold mb-8 text-90">
|
||||
{i18n(I18nKey.notFound)}
|
||||
</h2>
|
||||
|
||||
<div class="flex items-center justify-center gap-4 md:gap-6 mb-6">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 blur-3xl opacity-20 bg-[var(--primary)] rounded-full"></div>
|
||||
<Icon
|
||||
name="fa6-solid:link-slash"
|
||||
class="relative text-[8rem] md:text-[10rem] text-[var(--primary)] opacity-80 dark:opacity-60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h1 class="text-7xl md:text-9xl font-bold text-[var(--primary)] opacity-90">
|
||||
404
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p class="text-lg text-75 mb-10 max-w-md">
|
||||
{i18n(I18nKey.notFoundDesc)}
|
||||
</p>
|
||||
|
||||
<a
|
||||
href={url("/")}
|
||||
class="btn-regular rounded-xl h-12 px-8 font-bold text-lg active:scale-95 transition
|
||||
flex items-center hover:gap-3"
|
||||
>
|
||||
{i18n(I18nKey.backToHome)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MainGridLayout>
|
||||
|
||||
<style>
|
||||
.btn-regular {
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
.btn-regular:hover {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -3,12 +3,18 @@ import ArchivePanel from "@components/ArchivePanel.svelte";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import MainGridLayout from "@layouts/MainGridLayout.astro";
|
||||
import { getSortedPostsList } from "../utils/content-utils";
|
||||
import {
|
||||
getCategoryPinnedPosts,
|
||||
getPinnedPosts,
|
||||
getSortedPostsList,
|
||||
} from "../utils/content-utils";
|
||||
|
||||
const sortedPostsList = await getSortedPostsList();
|
||||
const pinnedPosts = await getPinnedPosts();
|
||||
const categoryPinnedPosts = await getCategoryPinnedPosts();
|
||||
---
|
||||
|
||||
<MainGridLayout title={i18n(I18nKey.archive)}>
|
||||
<ArchivePanel sortedPosts={sortedPostsList} client:only="svelte"></ArchivePanel>
|
||||
<ArchivePanel sortedPosts={sortedPostsList} pinnedPosts={pinnedPosts} categoryPinnedPosts={categoryPinnedPosts} client:only="svelte"></ArchivePanel>
|
||||
</MainGridLayout>
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/// <reference types="mdast" />
|
||||
import { h } from "hastscript";
|
||||
|
||||
/**
|
||||
* Creates a Gitea Card component.
|
||||
*
|
||||
* @param {Object} properties - The properties of the component.
|
||||
* @param {string} properties.repo - The Gitea repository in the format "owner/repo".
|
||||
* @param {string} properties.service - (optional) Third-Party Gitea service provider.
|
||||
* @param {import('mdast').RootContent[]} children - The children elements of the component.
|
||||
* @returns {import('mdast').Parent} The created Gitea Card component.
|
||||
*/
|
||||
export function GiteaCardComponent(properties, children) {
|
||||
if (Array.isArray(children) && children.length !== 0)
|
||||
return h("div", { class: "hidden" }, [
|
||||
'Invalid directive. ("gitea" directive must be leaf type "::gitea{repo="owner/repo"}")',
|
||||
]);
|
||||
|
||||
if (!properties.repo || !properties.repo.includes("/"))
|
||||
return h(
|
||||
"div",
|
||||
{ class: "hidden" },
|
||||
'Invalid repository. ("repo" attributte must be in the format "owner/repo")',
|
||||
);
|
||||
|
||||
const repo = properties.repo;
|
||||
const service = properties?.service || "https://gitea.com";
|
||||
const cardUuid = `GC${Math.random().toString(36).slice(-6)}`;
|
||||
|
||||
const nAvatar = h(`div#${cardUuid}-avatar`, { class: "gc-avatar" });
|
||||
const nLanguage = h(
|
||||
`span#${cardUuid}-language`,
|
||||
{ class: "gc-language" },
|
||||
"Waiting...",
|
||||
);
|
||||
|
||||
const nTitle = h("div", { class: "gc-titlebar" }, [
|
||||
h("div", { class: "gc-titlebar-left" }, [
|
||||
h("div", { class: "gc-owner" }, [
|
||||
nAvatar,
|
||||
h("div", { class: "gc-user" }, repo.split("/")[0]),
|
||||
]),
|
||||
h("div", { class: "gc-divider" }, "/"),
|
||||
h("div", { class: "gc-repo" }, repo.split("/")[1]),
|
||||
]),
|
||||
h("div", { class: "gitea-logo" }),
|
||||
]);
|
||||
|
||||
const nDescription = h(
|
||||
`div#${cardUuid}-description`,
|
||||
{ class: "gc-description" },
|
||||
"Waiting for Gitea API...",
|
||||
);
|
||||
|
||||
const nStars = h(`div#${cardUuid}-stars`, { class: "gc-stars" }, "00K");
|
||||
const nForks = h(`div#${cardUuid}-forks`, { class: "gc-forks" }, "0K");
|
||||
|
||||
const nScript = h(
|
||||
`script#${cardUuid}-script`,
|
||||
{ type: "text/javascript", defer: true },
|
||||
`
|
||||
fetch('${service}/api/v1/repos/${repo}', { referrerPolicy: "no-referrer" }).then(response => response.json()).then(data => {
|
||||
document.getElementById('${cardUuid}-description').innerText = data.description?.replace(/:[a-zA-Z0-9_]+:/g, '') || "Description not set";
|
||||
document.getElementById('${cardUuid}-language').innerText = data.language;
|
||||
document.getElementById('${cardUuid}-forks').innerText = Intl.NumberFormat('en-us', { notation: "compact", maximumFractionDigits: 1 }).format(data.forks_count).replaceAll("\\u202f", '');
|
||||
document.getElementById('${cardUuid}-stars').innerText = Intl.NumberFormat('en-us', { notation: "compact", maximumFractionDigits: 1 }).format(data.stars_count).replaceAll("\\u202f", '');
|
||||
const avatarEl = document.getElementById('${cardUuid}-avatar');
|
||||
avatarEl.style.backgroundImage = 'url(' + data.owner.avatar_url + ')';
|
||||
avatarEl.style.backgroundColor = 'transparent';
|
||||
document.getElementById('${cardUuid}-card').classList.remove("fetch-waiting");
|
||||
console.log("[GITEA-CARD] Loaded card for ${repo} | ${cardUuid}.")
|
||||
}).catch(err => {
|
||||
const c = document.getElementById('${cardUuid}-card');
|
||||
c?.classList.add("fetch-error");
|
||||
console.warn("[GITEA-CARD] (Error) Loading card for ${repo} | ${cardUuid}.")
|
||||
})
|
||||
`,
|
||||
);
|
||||
|
||||
return h(
|
||||
`a#${cardUuid}-card`,
|
||||
{
|
||||
class: "card-github fetch-waiting no-styling",
|
||||
href: `${service}/${repo}`,
|
||||
target: "_blank",
|
||||
repo,
|
||||
},
|
||||
[
|
||||
nTitle,
|
||||
nDescription,
|
||||
h("div", { class: "gc-infobar" }, [nStars, nForks, nLanguage]),
|
||||
nScript,
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -6,8 +6,9 @@ import { h } from "hastscript";
|
||||
*
|
||||
* @param {Object} properties - The properties of the component.
|
||||
* @param {string} properties.repo - The Gitlab repository in the format "owner/repo".
|
||||
* @param {string} properties.service - (optional) Third-Party GitLab service provider.
|
||||
* @param {import('mdast').RootContent[]} children - The children elements of the component.
|
||||
* @returns {import('mdast').Parent} The created GitHub Card component.
|
||||
* @returns {import('mdast').Parent} The created GitLab Card component.
|
||||
*/
|
||||
export function GitlabCardComponent(properties, children) {
|
||||
if (Array.isArray(children) && children.length !== 0)
|
||||
@@ -24,6 +25,7 @@ export function GitlabCardComponent(properties, children) {
|
||||
|
||||
const repo = properties.repo;
|
||||
const repoE = repo.replace("/", "%2F"); // encoding by replace / to %2F
|
||||
const service = properties?.service || "https://gitlab.com";
|
||||
const cardUuid = `GC${Math.random().toString(36).slice(-6)}`; // Collisions are not important
|
||||
|
||||
const nAvatar = h(`div#${cardUuid}-avatar`, { class: "gc-avatar" });
|
||||
@@ -43,7 +45,7 @@ export function GitlabCardComponent(properties, children) {
|
||||
const nDescription = h(
|
||||
`div#${cardUuid}-description`,
|
||||
{ class: "gc-description" },
|
||||
"Waiting for gitlab.com api...",
|
||||
"Waiting for Gitlab Api...",
|
||||
);
|
||||
|
||||
const nStars = h(`div#${cardUuid}-stars`, { class: "gc-stars" }, "00K");
|
||||
@@ -53,7 +55,7 @@ export function GitlabCardComponent(properties, children) {
|
||||
`script#${cardUuid}-script`,
|
||||
{ type: "text/javascript", defer: true },
|
||||
`
|
||||
fetch('https://gitlab.com/api/v4/projects/${repoE}').then(response => response.json()).then(data => {
|
||||
fetch('${service}/api/v4/projects/${repoE}').then(response => response.json()).then(data => {
|
||||
document.getElementById('${cardUuid}-repo').innerText = data.name;
|
||||
document.getElementById('${cardUuid}-description').innerText = data.description?.replace(/:[a-zA-Z0-9_]+:/g, '') || "Description not set";
|
||||
document.getElementById('${cardUuid}-forks').innerText = Intl.NumberFormat('en-us', { notation: "compact", maximumFractionDigits: 1 }).format(data.forks_count).replaceAll("\u202f", '');
|
||||
@@ -61,7 +63,7 @@ export function GitlabCardComponent(properties, children) {
|
||||
|
||||
const avatar_url = data.namespace.avatar_url;
|
||||
if (avatar_url.startsWith('/')) {
|
||||
document.getElementById('${cardUuid}-avatar').style.backgroundImage = 'url(https://gitlab.com' + avatar_url + ')';
|
||||
document.getElementById('${cardUuid}-avatar').style.backgroundImage = 'url(${service}' + avatar_url + ')';
|
||||
} else {
|
||||
document.getElementById('${cardUuid}-avatar').style.backgroundImage = 'url(' + avatar_url + ')';
|
||||
}
|
||||
@@ -81,7 +83,7 @@ export function GitlabCardComponent(properties, children) {
|
||||
`a#${cardUuid}-card`,
|
||||
{
|
||||
class: "card-github fetch-waiting no-styling",
|
||||
href: `https://gitlab.com/${repo}`,
|
||||
href: `${service}/${repo}`,
|
||||
target: "_blank",
|
||||
repo,
|
||||
},
|
||||
|
||||
+2
-2
@@ -65,9 +65,9 @@
|
||||
@apply opacity-0 pointer-events-none
|
||||
}
|
||||
|
||||
#toc-inner-wrapper {
|
||||
/* #toc-inner-wrapper {
|
||||
mask-image: linear-gradient(to bottom, transparent 0%, black 2rem, black calc(100% - 2rem), transparent 100%);
|
||||
}
|
||||
} */
|
||||
|
||||
.hide-scrollbar {
|
||||
scrollbar-width: none;
|
||||
|
||||
@@ -163,7 +163,7 @@ a.card-github
|
||||
.gc-language
|
||||
display: none
|
||||
|
||||
.gc-stars, .gc-forks, .gc-license, .github-logo, .gitlab-logo
|
||||
.gc-stars, .gc-forks, .gc-license, .github-logo, .gitlab-logo, .gitea-logo
|
||||
font-weight: 500
|
||||
font-size: 0.875rem
|
||||
opacity: 0.9;
|
||||
@@ -214,6 +214,14 @@ a.card-github
|
||||
margin-right: 0
|
||||
mask-image: url("https://images.ctfassets.net/xz1dnu24egyd/4V92fFTJOIlTPHHzSdfxem/3fdc9f0d82f08ed4c355c6e4126b870c/gitlab-logo-600.svg")
|
||||
|
||||
.gitea-logo
|
||||
font-size: 1.25rem
|
||||
|
||||
&:before
|
||||
background-color: var(--tw-prose-headings)
|
||||
margin-right: 0
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 640 640%22%3E%3Cpath fill=%22%23609926%22 d=%22M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3 0.3-26.5 0.6-39.6 0.7c0 39.1 0 78.2 0 117.2c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29 0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-0.6-22.5-2.1-39 1.5c-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5c45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62c63 0 188.9-0.1 188.9-0.1s12 0.1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4s12.9-13.8 30.9-45.3c5.5-9.7 10.1-19.1 14.1-28c0 0 55.2-117.1 55.2-231.1C633.2 157.9 624.7 151.8 622.7 149.8z M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7S142.5 359.9 125.6 353.9z M425.9 461.5c0 0-6.1 14.5-19.6 15.4c-5.8 0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55c0 0-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273c0 0 4.8-9.7 12.2-13c0.6-0.3 2.3-1 4.5-1.5c8.1-2.1 18 2.8 18 2.8l110.7 53.7c0 0 12.6 5.7 15.3 16.2c1.9 7.4-0.5 14-1.8 17.2C474.6 363.8 425.9 461.5 425.9 461.5z%22/%3E%3Cpath fill=%22%23609926%22 d=%22M326.8 380.1c-8.2 0.1-15.4 5.8-17.3 13.8c-1.9 8 2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4c5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5 0.1 3.7 0.2 6.2-0.5c4.1-0.9 7.1-3.6 7.1-3.6c4.2 1.8 8.6 3.8 13.2 6.1c4.8 2.4 9.3 4.9 13.4 7.3c0.9 0.5 1.8 1.1 2.8 1.9c1.6 1.3 3.4 3.1 4.7 5.5c1.9 5.5-1.9 14.9-1.9 14.9c-2.3 7.6-18.4 40.6-18.4 40.6c-8.1-0.2-15.3 5-17.7 12.5c-2.6 8.1 1.1 17.3 8.9 21.3c7.8 4 17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6c1.9-3.7 3.7-7.4 5.6-11.3c5-10.4 13.5-30.4 13.5-30.4c0.9-1.7 5.7-10.3 2.7-21.3c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7 9.4-19.3 14.1-29c-4.1-2-8.1-4-12.2-6.1c-4.8 9.8-9.7 19.7-14.5 29.5c-6.7-0.1-12.9 3.5-16.1 9.4c-3.4 6.3-2.7 14.1 1.9 19.8C343.2 346.5 335 363.3 326.8 380.1z%22/%3E%3C/svg%3E")
|
||||
|
||||
a.card-github.fetch-waiting
|
||||
pointer-events: none
|
||||
opacity: 0.7
|
||||
@@ -247,7 +255,7 @@ a.card-github.fetch-error
|
||||
100%
|
||||
opacity: 0.15
|
||||
|
||||
.card-github, .gc-description, .gc-titlebar, .gc-stars, .gc-forks, .gc-license, .gc-avatar, .github-logo, .gitlab-logo
|
||||
.card-github, .gc-description, .gc-titlebar, .gc-stars, .gc-forks, .gc-license, .gc-avatar, .github-logo, .gitlab-logo, .gitea-logo
|
||||
transition-property: all
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1)
|
||||
transition-duration: 0.15s
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
&:hover, &:active {
|
||||
@apply decoration-transparent;
|
||||
background: var(--btn-plain-bg-hover);
|
||||
border-bottom: 1px dashed var(--link-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,35 @@ import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import { getCategoryUrl } from "@utils/url-utils.ts";
|
||||
|
||||
// // Retrieve posts and sort them by publication date
|
||||
async function getRawSortedPosts() {
|
||||
type Post = CollectionEntry<"posts">;
|
||||
|
||||
function sortPosts(posts: Post[], useCategoryPinned = false): Post[] {
|
||||
return posts.sort((a, b) => {
|
||||
const pinnedA = useCategoryPinned
|
||||
? a.data.category_pinned || 0
|
||||
: a.data.pinned || 0;
|
||||
const pinnedB = useCategoryPinned
|
||||
? b.data.category_pinned || 0
|
||||
: b.data.pinned || 0;
|
||||
|
||||
const pinnedDiff = pinnedB - pinnedA;
|
||||
if (pinnedDiff !== 0) return pinnedDiff;
|
||||
|
||||
const dateA = new Date(a.data.published);
|
||||
const dateB = new Date(b.data.published);
|
||||
return dateB.valueOf() - dateA.valueOf();
|
||||
});
|
||||
}
|
||||
|
||||
async function getRawSortedPosts(): Promise<Post[]> {
|
||||
const allBlogPosts = await getCollection("posts", ({ data }) => {
|
||||
return import.meta.env.PROD ? data.draft !== true : true;
|
||||
});
|
||||
|
||||
const sorted = allBlogPosts.sort((a, b) => {
|
||||
const dateA = new Date(a.data.published);
|
||||
const dateB = new Date(b.data.published);
|
||||
return dateA > dateB ? -1 : 1;
|
||||
});
|
||||
return sorted;
|
||||
return sortPosts(allBlogPosts);
|
||||
}
|
||||
|
||||
export async function getSortedPosts() {
|
||||
export async function getSortedPosts(): Promise<Post[]> {
|
||||
const sorted = await getRawSortedPosts();
|
||||
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
@@ -112,3 +126,37 @@ export async function getCategoryList(): Promise<Category[]> {
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function getPinnedPosts(): Promise<PostForList[]> {
|
||||
const allBlogPosts = await getCollection("posts", ({ data }) => {
|
||||
return import.meta.env.PROD ? data.draft !== true : true;
|
||||
});
|
||||
|
||||
const pinnedPosts = allBlogPosts.filter(
|
||||
(post) => (post.data.pinned || 0) > 0,
|
||||
);
|
||||
|
||||
const sorted = sortPosts(pinnedPosts);
|
||||
|
||||
return sorted.map((post) => ({
|
||||
slug: post.slug,
|
||||
data: post.data,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getCategoryPinnedPosts(): Promise<PostForList[]> {
|
||||
const allBlogPosts = await getCollection("posts", ({ data }) => {
|
||||
return import.meta.env.PROD ? data.draft !== true : true;
|
||||
});
|
||||
|
||||
const categoryPinnedPosts = allBlogPosts.filter(
|
||||
(post) => (post.data.category_pinned || 0) > 0,
|
||||
);
|
||||
|
||||
const sorted = sortPosts(categoryPinnedPosts, true);
|
||||
|
||||
return sorted.map((post) => ({
|
||||
slug: post.slug,
|
||||
data: post.data,
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user