From de2cc52e15994732a5548a669349f503251d32a3 Mon Sep 17 00:00:00 2001 From: dribble-njr <72367140+dribble-njr@users.noreply.github.com> Date: Wed, 8 Jan 2025 22:01:44 +0800 Subject: [PATCH] feat: add reading time (#509) --- package-lock.json | 10 ++- package.json | 1 + .../CodemirrorEditor/EditorHeader/index.vue | 36 ++++++++- src/config/theme.ts | 2 + src/stores/index.ts | 29 ++++++- src/types/index.ts | 1 + src/utils/renderer.ts | 62 +++++++++++--- src/views/CodemirrorEditor.vue | 81 ++++++++----------- 8 files changed, 155 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index d5ed463..5781048 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "pinia": "^2.2.7", "qiniu-js": "^3.4.2", "radix-vue": "^1.9.10", + "reading-time": "^1.5.0", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "tiny-oss": "^0.5.1", @@ -12496,9 +12497,10 @@ }, "node_modules/mdast-util-to-string": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "dev": true, + "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0" }, @@ -15945,6 +15947,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/reading-time": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", + "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==", + "license": "MIT" + }, "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmmirror.com/rechoir/-/rechoir-0.6.2.tgz", diff --git a/package.json b/package.json index 9d6ec78..27ff993 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "pinia": "^2.2.7", "qiniu-js": "^3.4.2", "radix-vue": "^1.9.10", + "reading-time": "^1.5.0", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "tiny-oss": "^0.5.1", diff --git a/src/components/CodemirrorEditor/EditorHeader/index.vue b/src/components/CodemirrorEditor/EditorHeader/index.vue index 3555871..b7c523e 100644 --- a/src/components/CodemirrorEditor/EditorHeader/index.vue +++ b/src/components/CodemirrorEditor/EditorHeader/index.vue @@ -71,9 +71,9 @@ const formatItems = [ const store = useStore() const displayStore = useDisplayStore() -const { isDark, isCiteStatus, output, primaryColor } = storeToRefs(store) +const { isDark, isCiteStatus, isCountStatus, output, primaryColor } = storeToRefs(store) -const { toggleDark, editorRefresh, citeStatusChanged } = store +const { toggleDark, editorRefresh, citeStatusChanged, countStatusChanged } = store const copyMode = useStorage(addPrefix(`copyMode`), `txt`) const source = ref(``) @@ -173,6 +173,13 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`]) > 微信外链转底部引用 + + + 统计字数和阅读时间 + @@ -384,6 +391,31 @@ const formatOptions = ref([`rgb`, `hex`, `hsl`, `hsv`]) +
+

统计字数和阅读时间

+
+ + +
+

段落首行缩进

diff --git a/src/config/theme.ts b/src/config/theme.ts index 755d335..30106cf 100644 --- a/src/config/theme.ts +++ b/src/config/theme.ts @@ -84,6 +84,7 @@ const defaultTheme: Theme = { 'border-radius': `6px`, 'color': `rgba(0,0,0,0.5)`, 'background': `var(--blockquote-background)`, + 'margin-bottom': `1em`, }, // 引用内容 @@ -334,6 +335,7 @@ const graceTheme = toMerged(defaultTheme, { 'border-radius': `6px`, 'color': `rgba(0,0,0,0.6)`, 'box-shadow': `0 4px 6px rgba(0,0,0,0.05)`, + 'margin-bottom': `1em`, }, 'blockquote_p': { diff --git a/src/stores/index.ts b/src/stores/index.ts index 14351c5..195be76 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -1,9 +1,10 @@ +import type { ReadTimeResults } from 'reading-time' import DEFAULT_CONTENT from '@/assets/example/markdown.md?raw' import DEFAULT_CSS_CONTENT from '@/assets/example/theme-css.txt?raw' import { altKey, codeBlockThemeOptions, colorOptions, fontFamilyOptions, fontSizeOptions, legendOptions, shiftKey, themeMap, themeOptions } from '@/config' import { addPrefix, css2json, customCssWithTemplate, customizeTheme, downloadMD, exportHTML, formatDoc } from '@/utils' -import { initRenderer } from '@/utils/renderer' +import { initRenderer } from '@/utils/renderer' import CodeMirror from 'codemirror' import { marked } from 'marked' @@ -24,6 +25,10 @@ export const useStore = defineStore(`store`, () => { const isCiteStatus = useStorage(`isCiteStatus`, false) const toggleCiteStatus = useToggle(isCiteStatus) + // 是否统计字数和阅读时间 + const isCountStatus = useStorage(`isCountStatus`, false) + const toggleCountStatus = useToggle(isCountStatus) + // 是否开启段落首行缩进 const isUseIndent = useStorage(addPrefix(`use_indent`), false) const toggleUseIndent = useToggle(isUseIndent) @@ -174,14 +179,23 @@ export const useStore = defineStore(`store`, () => { isUseIndent: isUseIndent.value, }) + const readingTime = ref(null) + // 更新编辑器 const editorRefresh = () => { codeThemeChange() - renderer.reset({ citeStatus: isCiteStatus.value, legend: legend.value, isUseIndent: isUseIndent.value }) + renderer.reset({ citeStatus: isCiteStatus.value, legend: legend.value, isUseIndent: isUseIndent.value, countStatus: isCountStatus.value }) - const { markdownContent } = renderer.parseFrontMatterAndContent(editor.value!.getValue()) + const { markdownContent, readingTime: readingTimeResult } = renderer.parseFrontMatterAndContent(editor.value!.getValue()) + console.log(`Reading time result:`, readingTimeResult) + readingTime.value = readingTimeResult let outputTemp = marked.parse(markdownContent) as string + console.log(readingTime.value) + + // 阅读时间及字数统计 + outputTemp = renderer.buildReadingTime(readingTimeResult) + outputTemp + // 去除第一行的 margin-top outputTemp = outputTemp.replace(/(style=".*?)"/, `$1;margin-top: 0"`) // 引用脚注 @@ -275,6 +289,7 @@ export const useStore = defineStore(`store`, () => { const resetStyle = () => { isCiteStatus.value = false isMacCodeBlock.value = true + isCountStatus.value = false theme.value = themeOptions[0].value fontFamily.value = fontFamilyOptions[0].value @@ -366,6 +381,10 @@ export const useStore = defineStore(`store`, () => { toggleCiteStatus() }) + const countStatusChanged = withAfterRefresh(() => { + toggleCountStatus() + }) + const useIndentChanged = withAfterRefresh(() => { toggleUseIndent() }) @@ -427,6 +446,9 @@ export const useStore = defineStore(`store`, () => { isUseIndent, useIndentChanged, + isCountStatus, + countStatusChanged, + output, editor, cssEditor, @@ -436,6 +458,7 @@ export const useStore = defineStore(`store`, () => { primaryColor, codeBlockTheme, legend, + readingTime, editorRefresh, diff --git a/src/types/index.ts b/src/types/index.ts index d03c210..2ff12ff 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -26,6 +26,7 @@ export interface IOpts { isUseIndent: boolean legend?: string citeStatus?: boolean + countStatus?: boolean } export type ThemeStyles = Record diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts index 23c9622..0be2d3b 100644 --- a/src/utils/renderer.ts +++ b/src/utils/renderer.ts @@ -1,14 +1,18 @@ import type { ExtendedProperties, IOpts, ThemeStyles } from '@/types' import type { PropertiesHyphen } from 'csstype' import type { Renderer, RendererObject, Tokens } from 'marked' +import type { ReadTimeResults } from 'reading-time' import { cloneDeep, toMerged } from 'es-toolkit' import frontMatter from 'front-matter' import hljs from 'highlight.js' import { marked } from 'marked' import mermaid from 'mermaid' +import readingTime from 'reading-time' + import { getStyleString } from '.' import markedAlert from './MDAlert' + import { MDKatex } from './MDKatex' marked.setOptions({ @@ -109,6 +113,36 @@ const macCodeSvg = ` `.trim() +interface ParseResult { + yamlData: Record + markdownContent: string + readingTime: ReadTimeResults +} + +function parseFrontMatterAndContent(markdownText: string): ParseResult { + try { + const parsed = frontMatter(markdownText) + const yamlData = parsed.attributes + const markdownContent = parsed.body + + const readingTimeResult = readingTime(markdownContent) + + return { + yamlData: yamlData as Record, + markdownContent, + readingTime: readingTimeResult, + } + } + catch (error) { + console.error(`Error parsing front-matter:`, error) + return { + yamlData: {}, + markdownContent: markdownText, + readingTime: readingTime(markdownText), + } + } +} + export function initRenderer(opts: IOpts) { const footnotes: [number, string, string][] = [] let footnoteIndex: number = 0 @@ -121,19 +155,6 @@ export function initRenderer(opts: IOpts) { return getStyles(styleMapping, tag, addition) } - function parseFrontMatterAndContent(markdownText: string) { - try { - const parsed = frontMatter(markdownText) - const yamlData = parsed.attributes - const markdownContent = parsed.body - return { yamlData, markdownContent } - } - catch (error) { - console.error(`Error parsing front-matter:`, error) - return { yamlData: {}, markdownContent: markdownText } - } - } - function styledContent(styleLabel: string, content: string, tagName?: string): string { const tag = tagName ?? styleLabel return `<${tag} ${styles(styleLabel)}>${content}` @@ -156,6 +177,20 @@ export function initRenderer(opts: IOpts) { marked.use(markedAlert({ styles: styleMapping })) } + function buildReadingTime(readingTime: ReadTimeResults): string { + if (!opts.countStatus) { + return `` + } + if (!readingTime.words) { + return `` + } + return ` +
+

字数 ${readingTime?.words},阅读大约需 ${Math.ceil(readingTime?.minutes)} 分钟

+
+ ` + } + const buildFootnotes = () => { if (!footnotes.length) { return `` @@ -305,6 +340,7 @@ export function initRenderer(opts: IOpts) { setOptions, reset, parseFrontMatterAndContent, + buildReadingTime, createContainer(content: string) { return styledContent(`container`, content, `section`) }, diff --git a/src/views/CodemirrorEditor.vue b/src/views/CodemirrorEditor.vue index ac75dce..f6b9eba 100644 --- a/src/views/CodemirrorEditor.vue +++ b/src/views/CodemirrorEditor.vue @@ -12,7 +12,7 @@ import CodeMirror from 'codemirror' const store = useStore() const displayStore = useDisplayStore() -const { isDark, output, editor } = storeToRefs(store) +const { isDark, output, editor, readingTime } = storeToRefs(store) const { isShowCssEditor } = storeToRefs(displayStore) const { @@ -61,7 +61,7 @@ function leftAndRightScroll() { } const percentage - = source.scrollTop / (source.scrollHeight - source.offsetHeight) + = source.scrollTop / (source.scrollHeight - source.offsetHeight) const height = percentage * (target.scrollHeight - target.offsetHeight) target.scrollTo(0, height) @@ -274,7 +274,7 @@ function mdLocalToRemote() { let [, , matchStr] = item matchStr = matchStr.replace(/^.\//, ``) // 处理 ./img/ 为 img/ 统一相对路径风格 const { file } - = list.find(f => f.path === `${root}${matchStr}`) || {} + = list.find(f => f.path === `${root}${matchStr}`) || {} uploadImage(file!, (url) => { resolve({ matchStr, url }) }) @@ -363,29 +363,18 @@ onMounted(() => {