feat: add initial OpenWiki structure with custom theme and scripts

This commit is contained in:
2026-06-02 13:20:51 +08:00
parent b9bbcf131e
commit 0b4b79a1e6
16 changed files with 489 additions and 0 deletions

15
openwiki/config.yaml Normal file
View File

@@ -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"

19
openwiki/index.md Normal file
View File

@@ -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
---

1
openwiki/log.md Normal file
View File

@@ -0,0 +1 @@
# 变更日志

1
openwiki/nodes/index.md Normal file
View File

@@ -0,0 +1 @@
# 知识点

1
openwiki/overview.md Normal file
View File

@@ -0,0 +1 @@
# 概览

View File

@@ -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);
});

View File

@@ -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();

View File

@@ -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

View File

@@ -0,0 +1 @@
# 资料库