diff --git a/openwiki/config.mts b/openwiki/.vitepress/config.mts similarity index 100% rename from openwiki/config.mts rename to openwiki/.vitepress/config.mts diff --git a/openwiki/theme/Layout.vue b/openwiki/.vitepress/theme/Layout.vue similarity index 100% rename from openwiki/theme/Layout.vue rename to openwiki/.vitepress/theme/Layout.vue diff --git a/openwiki/theme/components/DocMetaEx.vue b/openwiki/.vitepress/theme/components/DocMetaEx.vue similarity index 100% rename from openwiki/theme/components/DocMetaEx.vue rename to openwiki/.vitepress/theme/components/DocMetaEx.vue diff --git a/openwiki/theme/components/DocTags.vue b/openwiki/.vitepress/theme/components/DocTags.vue similarity index 100% rename from openwiki/theme/components/DocTags.vue rename to openwiki/.vitepress/theme/components/DocTags.vue diff --git a/openwiki/theme/components/Viewer.vue b/openwiki/.vitepress/theme/components/Viewer.vue similarity index 100% rename from openwiki/theme/components/Viewer.vue rename to openwiki/.vitepress/theme/components/Viewer.vue diff --git a/openwiki/theme/fonts.css b/openwiki/.vitepress/theme/fonts.css similarity index 100% rename from openwiki/theme/fonts.css rename to openwiki/.vitepress/theme/fonts.css diff --git a/openwiki/theme/index.js b/openwiki/.vitepress/theme/index.js similarity index 100% rename from openwiki/theme/index.js rename to openwiki/.vitepress/theme/index.js diff --git a/openwiki/config.yaml b/openwiki/config.yaml new file mode 100644 index 0000000..331e0cc --- /dev/null +++ b/openwiki/config.yaml @@ -0,0 +1,15 @@ +title: "OpenWiki" +description: "知识库" +logo: "/logo.svg" +siteTitle: false +appearance: false +nav: + - text: "知识库" + link: "/overview" + - text: "资料库" + link: "/sources/" + - text: "文档" + link: "/docs/" +footer: + message: "Powered by OpenWiki" + copyright: "© 2024" \ No newline at end of file diff --git a/openwiki/index.md b/openwiki/index.md new file mode 100644 index 0000000..74d3bc7 --- /dev/null +++ b/openwiki/index.md @@ -0,0 +1,19 @@ +--- +layout: home + +hero: + name: {{项目名称}} + text: 知识库 + tagline: {{一句话说明}} + image: + src: /logo.png + alt: VitePress + + actions: + - theme: brand + text: 概览 + link: /overview + - theme: alt + text: 变更日志 + link: /log +--- diff --git a/openwiki/log.md b/openwiki/log.md new file mode 100644 index 0000000..b80cbda --- /dev/null +++ b/openwiki/log.md @@ -0,0 +1 @@ +# 变更日志 \ No newline at end of file diff --git a/openwiki/nodes/index.md b/openwiki/nodes/index.md new file mode 100644 index 0000000..4e6470f --- /dev/null +++ b/openwiki/nodes/index.md @@ -0,0 +1 @@ +# 知识点 \ No newline at end of file diff --git a/openwiki/overview.md b/openwiki/overview.md new file mode 100644 index 0000000..3bf1291 --- /dev/null +++ b/openwiki/overview.md @@ -0,0 +1 @@ +# 概览 \ No newline at end of file diff --git a/openwiki/scripts/check-md.js b/openwiki/scripts/check-md.js new file mode 100644 index 0000000..9744fab --- /dev/null +++ b/openwiki/scripts/check-md.js @@ -0,0 +1,254 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const http = require('http'); +const https = require('https'); + +const ILLEGAL_CHARS = /[\s!@#$%^&*()+={}\[\]|\\:;"'<>~`]/; +const EXTERNAL_LINK_REGEX = /\[([^\]]*)\]\((https?:\/\/[^\)]+)\)/g; +const INTERNAL_LINK_REGEX = /\[([^\]]*)\]\((?!https?:\/\/)([^)]+)\)/g; +const WIKILINK_REGEX = /\[\[([^\]]+)\]\]/g; + +function showHelp() { + console.log(`Usage: check-md.js [OPTIONS] PATHS... + +Check Markdown files for broken links and invalid filenames. + +PATHS... One or more directories or files to check + (directories are scanned recursively for .md files) + +OPTIONS: + -h, --help Show this help message + -v, --verbose Show detailed progress and statistics + +EXAMPLES: + check-md.js openwiki/ + check-md.js openwiki/ references/ + check-md.js openwiki/notes/file.md + check-md.js folder-1/ file.md folder-2/`); +} + +function decodeUrlSpaces(url) { + return url.replace(/%20/g, ' '); +} + +function validateFilename(filename, issues) { + const name = path.basename(filename); + + if (ILLEGAL_CHARS.test(name)) { + const found = name.split('').filter(c => ILLEGAL_CHARS.test(c)).join(' '); + issues.push(`filename has illegal characters: ${found}`); + } + + if (!filename.endsWith('.md') && name.includes('.')) { + issues.push('filename has incorrect extension'); + } +} + +function extractLinks(content) { + const externalLinks = []; + const internalLinks = []; + const wikilinks = []; + + let match; + + EXTERNAL_LINK_REGEX.lastIndex = 0; + while ((match = EXTERNAL_LINK_REGEX.exec(content)) !== null) { + externalLinks.push({ text: match[1], url: match[2], line: getLineNumber(content, match.index) }); + } + + INTERNAL_LINK_REGEX.lastIndex = 0; + while ((match = INTERNAL_LINK_REGEX.exec(content)) !== null) { + internalLinks.push({ text: match[1], path: match[2], line: getLineNumber(content, match.index) }); + } + + WIKILINK_REGEX.lastIndex = 0; + while ((match = WIKILINK_REGEX.exec(content)) !== null) { + wikilinks.push({ text: match[1], line: getLineNumber(content, match.index) }); + } + + return { externalLinks, internalLinks, wikilinks }; +} + +function getLineNumber(content, index) { + return content.substring(0, index).split('\n').length; +} + +function checkExternalLink(url) { + return new Promise((resolve) => { + const protocol = url.startsWith('https') ? https : http; + const parsed = new URL(url); + + const req = protocol.request({ + hostname: parsed.hostname, + port: parsed.port, + path: parsed.pathname + parsed.search, + method: 'HEAD', + timeout: 5000 + }, (res) => { + resolve({ status: res.statusCode, valid: res.statusCode >= 200 && res.statusCode < 400 }); + }); + + req.on('error', () => resolve({ status: 0, valid: false })); + req.on('timeout', () => { + req.destroy(); + resolve({ status: 0, valid: false, timeout: true }); + }); + + req.end(); + }); +} + +async function checkFile(filepath, options) { + const issues = []; + const filenameIssues = []; + + validateFilename(filepath, filenameIssues); + if (filenameIssues.length > 0) { + for (const issue of filenameIssues) { + issues.push({ type: 'filename', message: issue }); + } + } + + const content = fs.readFileSync(filepath, 'utf-8'); + const { externalLinks, internalLinks, wikilinks } = extractLinks(content); + + for (const wikilink of wikilinks) { + issues.push({ + type: 'wikilink', + message: `illegal wikilink format: [[${wikilink.text}]]`, + line: wikilink.line + }); + } + + const fileDir = path.dirname(filepath); + + for (const link of internalLinks) { + const decodedPath = decodeUrlSpaces(link.path); + const targetPath = path.isAbsolute(decodedPath) + ? decodedPath + : path.resolve(fileDir, decodedPath); + + if (!fs.existsSync(targetPath)) { + issues.push({ + type: 'broken-link', + message: `broken internal link: ${link.path}`, + line: link.line + }); + } + } + + for (const link of externalLinks) { + const result = await checkExternalLink(link.url); + if (!result.valid) { + const msg = result.timeout ? 'timeout' : `HTTP ${result.status}`; + issues.push({ + type: 'broken-external', + message: `broken external link: ${link.url} (${msg})`, + line: link.line + }); + } + } + + return { filepath, issues }; +} + +async function walkDir(dir, callback) { + if (!fs.existsSync(dir)) return; + + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + await walkDir(fullPath, callback); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + await callback(fullPath); + } + } +} + +async function main() { + const args = process.argv.slice(2); + const options = { verbose: false, directories: [] }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === '-h' || arg === '--help') { + showHelp(); + process.exit(0); + } else if (arg === '-v' || arg === '--verbose') { + options.verbose = true; + } else if (!arg.startsWith('-')) { + options.directories.push(arg); + } + } + + if (options.directories.length === 0) { + console.error('Error: No directories provided. Use -h for help.'); + process.exit(1); + } + + const allResults = []; + const fileQueue = []; + + for (const target of options.directories) { + if (fs.existsSync(target) && fs.statSync(target).isFile()) { + if (target.endsWith('.md')) { + fileQueue.push(target); + } + } else { + await walkDir(target, async (filepath) => { + fileQueue.push(filepath); + }); + } + } + + let checkedCount = 0; + for (const filepath of fileQueue) { + const result = await checkFile(filepath, options); + allResults.push(result); + + if (result.issues.length > 0) { + for (const issue of result.issues) { + const lineInfo = issue.line ? `:${issue.line}` : ''; + console.log(`${filepath}${lineInfo}: ${issue.message}`); + } + } else { + console.log(`[OK] ${filepath}`); + } + + checkedCount++; + if (options.verbose) { + process.stdout.write(`\rChecked: ${checkedCount}/${fileQueue.length} files...`); + } + } + + if (options.verbose) { + console.log('\n'); + } + + const totalIssues = allResults.reduce((sum, r) => sum + r.issues.length, 0); + console.log(`\n--- Summary ---`); + console.log(`Total files checked: ${fileQueue.length}`); + console.log(`Total issues found: ${totalIssues}`); + if (totalIssues > 0) { + console.log(`\n--- Issues ---`); + for (const result of allResults) { + if (result.issues.length > 0) { + for (const issue of result.issues) { + const lineInfo = issue.line ? `:${issue.line}` : ''; + console.log(`${result.filepath}${lineInfo}: ${issue.message}`); + } + } + } + process.exit(1); + } else { + console.log(`All files passed!`); + } +} + +main().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/openwiki/scripts/check-tag.js b/openwiki/scripts/check-tag.js new file mode 100644 index 0000000..271af3e --- /dev/null +++ b/openwiki/scripts/check-tag.js @@ -0,0 +1,162 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +function showHelp() { + console.log(`Usage: check-tag.js [OPTIONS] PATHS... + +Extract and count all tags from markdown files. + +PATHS... One or more directories or files to check + (directories are scanned recursively for .md files) + +OPTIONS: + -h, --help Show this help message + -d, --detail Show which files each tag appears in + +EXAMPLES: + check-tag.js openwiki/concepts/ + check-tag.js openwiki/ references/ + check-tag.js openwiki/notes/file.md + check-tag.js -d openwiki/concepts/`); +} + +function extractTags(content) { + const tags = []; + + let inFrontmatter = false; + let frontmatterEnd = -1; + + if (content.startsWith('---')) { + const endMatch = content.indexOf('---', 3); + if (endMatch !== -1) { + inFrontmatter = true; + frontmatterEnd = endMatch; + } + } + + if (inFrontmatter) { + const frontmatter = content.substring(0, frontmatterEnd); + const tagsMatch = frontmatter.match(/^tags:\s*$/m); + if (tagsMatch) { + const afterTags = frontmatter.substring(tagsMatch.index + tagsMatch[0].length); + const blockMatch = afterTags.match(/^(\s*-\s*.*(?:\n\s*-\s*.*)*)/m); + if (blockMatch) { + const tagLines = blockMatch[0].split('\n'); + for (const line of tagLines) { + const tag = line.replace(/^\s*-\s*/, '').trim(); + if (tag) { + tags.push(tag); + } + } + } + } + + const arrayMatch = frontmatter.match(/tags:\s*\[([^\]]*)\]/); + if (arrayMatch) { + const tagList = arrayMatch[1].split(',').map(t => t.trim()).filter(t => t); + for (const tag of tagList) { + tags.push(tag); + } + } + } + + const inlineRegex = /#([a-zA-Z0-9_-]+)/g; + let match; + while ((match = inlineRegex.exec(content)) !== null) { + tags.push(match[1]); + } + + return tags; +} + +function collectFiles(targets) { + const files = []; + + for (const target of targets) { + if (fs.existsSync(target)) { + const stat = fs.statSync(target); + if (stat.isFile() && target.endsWith('.md')) { + files.push(target); + } else if (stat.isDirectory()) { + walkDir(target, files); + } + } + } + + return files; +} + +function walkDir(dir, files) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walkDir(fullPath, files); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + files.push(fullPath); + } + } +} + +function main() { + const args = process.argv.slice(2); + const targets = []; + let showDetail = false; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === '-h' || arg === '--help') { + showHelp(); + process.exit(0); + } else if (arg === '-d' || arg === '--detail') { + showDetail = true; + } else if (!arg.startsWith('-')) { + targets.push(arg); + } + } + + if (targets.length === 0) { + console.error('Error: No paths provided. Use -h for help.'); + process.exit(1); + } + + const tagCount = new Map(); + const tagFiles = new Map(); + const files = collectFiles(targets); + + for (const file of files) { + try { + const content = fs.readFileSync(file, 'utf-8'); + const tags = extractTags(content); + const uniqueTags = [...new Set(tags)]; + for (const tag of uniqueTags) { + tagCount.set(tag, (tagCount.get(tag) || 0) + 1); + if (!tagFiles.has(tag)) { + tagFiles.set(tag, []); + } + tagFiles.get(tag).push(file); + } + } catch (err) { + // skip files that can't be read + } + } + + const sorted = Array.from(tagCount.entries()).sort((a, b) => b[1] - a[1]); + + if (showDetail) { + for (const [tag, count] of sorted) { + console.log(`${tag} ${count}`); + for (const file of tagFiles.get(tag)) { + console.log(` - ${file}`); + } + } + } else { + for (const [tag, count] of sorted) { + console.log(`${tag} ${count}`); + } + } +} + +main(); \ No newline at end of file diff --git a/openwiki/scripts/deploy.sh b/openwiki/scripts/deploy.sh new file mode 100644 index 0000000..3f0b884 --- /dev/null +++ b/openwiki/scripts/deploy.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# 检查 wiki 目录是否有变更 +CHANGED_FILES=$(git diff --name-only . && git ls-files --others --exclude-standard .) + +if [ -n "$CHANGED_FILES" ]; then + echo "检测到 wiki 目录有变更,正在提交..." + git add . + git commit -m "Update wiki" +else + echo "wiki 目录没有变更,跳过提交" +fi + +# 自动获取当前 Git 分支名 +BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) + +# 获取仓库名称 +REMOTE_URL=$(git remote get-url origin) +REPO_NAME=$(basename -s .git "$REMOTE_URL") + +# 拼接 Tag 名称:wiki/分支名 +TAG_NAME="wiki/$BRANCH_NAME" + +echo "当前分支:$BRANCH_NAME" + +# 创建本地 Tag +git tag -f "$TAG_NAME" + +# 推送到远程仓库 +if git push origin -f "$TAG_NAME"; then + echo -e "\n✅ $BRANCH_NAME 创建并推送成功,等待部署完毕" +else + echo -e "\n❌ $BRANCH_NAME 推送失败!" + exit 1 +fi \ No newline at end of file diff --git a/openwiki/sources/index.md b/openwiki/sources/index.md new file mode 100644 index 0000000..c0cfc50 --- /dev/null +++ b/openwiki/sources/index.md @@ -0,0 +1 @@ +# 资料库 \ No newline at end of file