Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add read time and word count #141

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion src/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,43 @@
import genConfig from "@project-trans/vitepress-theme-project-trans/config";
import type { ThemeContext } from "@project-trans/vitepress-theme-project-trans/utils";
import { withThemeContext } from "@project-trans/vitepress-theme-project-trans/utils";
import path from "path";
import type { DefaultTheme } from "vitepress";
import fs from "fs";

type NavConfig = DefaultTheme.Config["nav"];
function countWords(content: string): number {
const cleanedContent = content
.replace(/```[\s\S]*?```/g, "") // 移除代码块
.replace(/!\[.*?\]\(.*?\)/g, "") // 移除图片链接
.replace(/\[.*?\]\(.*?\)/g, "") // 移除普通链接
.replace(/<[^>]+(>|$)/g, "") // 移除 HTML 标签
.replace(/[.,/#!$%^&*;:{}=\-_`~()]/g, "") // 移除标点符号
.replace(/\s+/g, " ") // 将多余的空格归为一个空格
.trim(); // 去除首尾空格

const chineseCharacters =
cleanedContent.match(/[\u4E00-\u9FFF\uFF01-\uFFE5]/g) || [];
const words = cleanedContent.split(/\s+/).filter(Boolean);

return chineseCharacters.length + words.length;
}

function readMarkdownFileContent(filePath: string): string {
if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath, "utf-8");
}
return "";
}

function searchForFirstTitle(filePath: string): string {
const content = readMarkdownFileContent(filePath);
const title = content.match(/# (.*)/);
if (title) {
return title[1];
}
return "";
}

const nav = [
{ text: "正文", link: "/foreword" },
Expand Down Expand Up @@ -41,6 +75,7 @@ const sidebarOptions = [

const themeConfig: ThemeContext = {
siteTitle: "药娘的天空",
SiteTitle: "药娘的天空",
siteDescription: "一个 2000 年代的跨性别者的故事。",
siteLogo: "/progynova.png",
/** Repo */
Expand All @@ -58,4 +93,29 @@ const themeConfig: ThemeContext = {
};

// https://vitepress.dev/reference/site-config
export default withThemeContext(themeConfig, genConfig);
export default withThemeContext(themeConfig, () => {
const config = genConfig();
return {
...config,
transformPageData(pageData) {
// 构建 Markdown 文件路径
const markdownFile = `${pageData.relativePath}`;
const filePath = path.join(process.cwd(), "src", markdownFile);

// 从文件系统读取文件内容
const content = readMarkdownFileContent(filePath);
const title = searchForFirstTitle(filePath);

// 统计字数并插入到 Frontmatter
const wordCount = countWords(content);

return {
frontmatter: {
...pageData.frontmatter,
wordCount, // 将字数写入 Frontmatter
autoTitle: title,
},
};
},
};
});
16 changes: 8 additions & 8 deletions src/.vitepress/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
declare module 'markdown-it-pangu' {
import type { PluginSimple } from 'markdown-it'
declare module "markdown-it-pangu" {
import type { PluginSimple } from "markdown-it";

const pangu: PluginSimple
export default pangu
const pangu: PluginSimple;
export default pangu;
}

declare module 'markdown-it-katex' {
import type { PluginSimple } from 'markdown-it'
declare module "markdown-it-katex" {
import type { PluginSimple } from "markdown-it";

const katex: PluginSimple
export default katex
const katex: PluginSimple;
export default katex;
}
15 changes: 12 additions & 3 deletions src/.vitepress/theme/Layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "@project-trans/vitepress-theme-project-trans/components";

import CopyrightInfo from "./CopyrightInfo.vue";
import PageInfo from "./PageInfo.vue";

const { Layout } = DefaultTheme;
</script>
Expand All @@ -22,16 +23,19 @@ const { Layout } = DefaultTheme;
<Layout>
<template #doc-before>
<NolebaseHighlightTargetedHeading />
<PageInfo />
</template>
<template #doc-after>
<AppFooter />
<br />
<CopyrightInfo />
<p style="text-align: center; margin-top: 40px">
本站不提供评论服务,如有评论需求,请移步至
<a style="color: var(--vp-c-brand-1)"
href="https://github.com/transky-book/transky/discussions/categories/%E8%AF%84%E8%AE%BA%E5%8C%BA">GitHub
Discussions</a>。
<a
style="color: var(--vp-c-brand-1)"
href="https://github.com/transky-book/transky/discussions/categories/%E8%AF%84%E8%AE%BA%E5%8C%BA"
>GitHub Discussions</a
>。
</p>
</template>
<template #nav-bar-content-after>
Expand All @@ -49,4 +53,9 @@ const { Layout } = DefaultTheme;
--vp-font-family-base: sans-serif;
--vp-font-family-mono: monospace;
}

/* hide the first h1 under vp-doc */
.vp-doc div h1:first-of-type {
display: none;
}
</style>
78 changes: 78 additions & 0 deletions src/.vitepress/theme/PageInfo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<script setup lang="ts">
import { useData } from "vitepress";
import { computed, onMounted, ref, watchEffect } from "vue";
import ReadingTime from "./ReadingTime.vue"; // 导入 ReadingTime 组件

// 从 VitePress 获取页面数据
const { frontmatter, page, theme, lang, site } = useData();

// 计算页面的最后更新时间
const date = computed(
() => new Date(frontmatter.value.lastUpdated ?? page.value.lastUpdated)
);

// 计算 ISO 格式的日期时间字符串
const isoDatetime = computed(() => date.value.toISOString());

// 定义一个响应式变量来存储格式化后的日期时间字符串
const datetime = ref("");

// 避免 hydration 错误,在组件挂载后执行
onMounted(() => {
watchEffect(() => {
// 使用国际化 API 格式化日期时间
datetime.value = new Intl.DateTimeFormat(
theme.value.lastUpdated?.formatOptions?.forceLocale
? lang.value
: undefined,
theme.value.lastUpdated?.formatOptions ?? {
dateStyle: "short",
timeStyle: "short",
}
).format(date.value);
});
});

// 计算页面的作者信息
const authors = computed(() => {
let author = (frontmatter.value?.author ?? []) as string[];
if (!Array.isArray(author)) author = [author];
return author;
});

// 计算显示的作者信息
const displayAuthors = computed(() => {
if (authors.value.length === 0) {
return "匿名";
} else {
return `${authors.value.join(", ")} 等`;
}
});
</script>

<template>
<div>
<div class="vp-doc">
<h1>
{{ frontmatter.autoTitle }}
</h1>
</div>
<div class="mb-10 mt-4 flex flex-wrap gap-4">
<div v-if="!theme.HideAuthors" class="inline-flex items-center gap-1">
<span class="i-octicon:person" />
<span>作者:</span>
<span>{{ displayAuthors }}</span>
</div>

<div v-if="!theme.HideLastUpdated" class="inline-flex items-center gap-1">
<span class="i-octicon:calendar-16" />
<span>{{ theme.lastUpdated?.text || "Last updated" }}:</span>
<time :datetime="isoDatetime">{{ datetime }}</time>
</div>
<ClientOnly>
<ReadingTime v-if="!theme.HideReadingTime" />
<!-- 添加 ReadingTime 组件 -->
</ClientOnly>
</div>
</div>
</template>
43 changes: 43 additions & 0 deletions src/.vitepress/theme/ReadingTime.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { useData } from "vitepress";
import { ref, watch } from "vue";

// 获取页面数据
const { frontmatter } = useData();

// 计算阅读时间的函数
function calculateReadingTime(wordCount: number) {
const wordsPerMinute = 500; // 假设中文阅读速度为每分钟500字
return Math.ceil(wordCount / wordsPerMinute); // 计算预计阅读时间
}

// 使用 ref 创建响应式变量
const wordCount = ref(frontmatter.value.wordCount || 0);
const readingTime = ref(calculateReadingTime(wordCount.value));

// 监听 frontmatter 的变化
watch(
() => frontmatter.value,
(newFrontmatter) => {
wordCount.value = newFrontmatter.wordCount || 0;
readingTime.value = calculateReadingTime(wordCount.value);
}
);
</script>

<template>
<div class="inline-flex items-center gap-4">
<div class="inline-flex items-center gap-1">
<span class="i-octicon:pencil-16" />
<span>字数: {{ wordCount }} </span>
</div>
<div class="inline-flex items-center gap-1">
<span class="i-octicon:book-16" />
<span>预计阅读时间: {{ readingTime }} 分钟</span>
</div>
</div>
</template>

<style scoped>
/* 这里可以添加样式 */
</style>
Loading