feat: add initial OpenWiki structure with custom theme and scripts
This commit is contained in:
15
openwiki/config.yaml
Normal file
15
openwiki/config.yaml
Normal 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
19
openwiki/index.md
Normal 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
1
openwiki/log.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# 变更日志
|
||||||
1
openwiki/nodes/index.md
Normal file
1
openwiki/nodes/index.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# 知识点
|
||||||
1
openwiki/overview.md
Normal file
1
openwiki/overview.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# 概览
|
||||||
254
openwiki/scripts/check-md.js
Normal file
254
openwiki/scripts/check-md.js
Normal 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);
|
||||||
|
});
|
||||||
162
openwiki/scripts/check-tag.js
Normal file
162
openwiki/scripts/check-tag.js
Normal 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();
|
||||||
35
openwiki/scripts/deploy.sh
Normal file
35
openwiki/scripts/deploy.sh
Normal 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
|
||||||
1
openwiki/sources/index.md
Normal file
1
openwiki/sources/index.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# 资料库
|
||||||
Reference in New Issue
Block a user