Files
menav/src/bookmark-processor.js

403 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
// 书签文件夹路径 - 使用相对路径
const BOOKMARKS_DIR = 'bookmarks';
// 模块化配置目录
const CONFIG_USER_DIR = 'config/user';
// 模块化页面配置目录
const CONFIG_USER_PAGES_DIR = path.join(CONFIG_USER_DIR, 'pages');
// 模块化输出配置文件路径
const MODULAR_OUTPUT_FILE = path.join(CONFIG_USER_PAGES_DIR, 'bookmarks.yml');
// 模块化默认书签配置文件路径
const MODULAR_DEFAULT_BOOKMARKS_FILE = 'config/_default/pages/bookmarks.yml';
// 图标映射根据URL关键字匹配合适的图标
const ICON_MAPPING = {
'github.com': 'fab fa-github',
'stackoverflow.com': 'fab fa-stack-overflow',
'youtube.com': 'fab fa-youtube',
'twitter.com': 'fab fa-twitter',
'facebook.com': 'fab fa-facebook',
'instagram.com': 'fab fa-instagram',
'linkedin.com': 'fab fa-linkedin',
'reddit.com': 'fab fa-reddit',
'amazon.com': 'fab fa-amazon',
'google.com': 'fab fa-google',
'gmail.com': 'fas fa-envelope',
'drive.google.com': 'fab fa-google-drive',
'docs.google.com': 'fas fa-file-alt',
'medium.com': 'fab fa-medium',
'dev.to': 'fab fa-dev',
'gitlab.com': 'fab fa-gitlab',
'bitbucket.org': 'fab fa-bitbucket',
'wikipedia.org': 'fab fa-wikipedia-w',
'discord.com': 'fab fa-discord',
'slack.com': 'fab fa-slack',
'apple.com': 'fab fa-apple',
'microsoft.com': 'fab fa-microsoft',
'android.com': 'fab fa-android',
'twitch.tv': 'fab fa-twitch',
'spotify.com': 'fab fa-spotify',
'pinterest.com': 'fab fa-pinterest',
'telegram.org': 'fab fa-telegram',
'whatsapp.com': 'fab fa-whatsapp',
'netflix.com': 'fas fa-film',
'trello.com': 'fab fa-trello',
'wordpress.com': 'fab fa-wordpress',
'jira': 'fab fa-jira',
'atlassian.com': 'fab fa-atlassian',
'dropbox.com': 'fab fa-dropbox',
'npm': 'fab fa-npm',
'docker.com': 'fab fa-docker',
'python.org': 'fab fa-python',
'javascript': 'fab fa-js',
'php.net': 'fab fa-php',
'java': 'fab fa-java',
'codepen.io': 'fab fa-codepen',
'behance.net': 'fab fa-behance',
'dribbble.com': 'fab fa-dribbble',
'tumblr.com': 'fab fa-tumblr',
'vimeo.com': 'fab fa-vimeo',
'flickr.com': 'fab fa-flickr',
'github.io': 'fab fa-github',
'airbnb.com': 'fab fa-airbnb',
'bitcoin': 'fab fa-bitcoin',
'paypal.com': 'fab fa-paypal',
'ethereum': 'fab fa-ethereum',
'steam': 'fab fa-steam',
};
// 获取最新的书签文件
function getLatestBookmarkFile() {
try {
// 确保书签目录存在
if (!fs.existsSync(BOOKMARKS_DIR)) {
fs.mkdirSync(BOOKMARKS_DIR, { recursive: true });
return null;
}
// 获取目录中的所有HTML文件
const files = fs.readdirSync(BOOKMARKS_DIR)
.filter(file => file.toLowerCase().endsWith('.html'));
if (files.length === 0) {
return null;
}
// 获取文件状态,按最后修改时间排序
const fileStats = files.map(file => ({
file,
mtime: fs.statSync(path.join(BOOKMARKS_DIR, file)).mtime
}));
// 找出最新的文件
fileStats.sort((a, b) => b.mtime - a.mtime);
const latestFile = fileStats[0].file;
return path.join(BOOKMARKS_DIR, latestFile);
} catch (error) {
console.error('Error finding bookmark file:', error);
return null;
}
}
// 解析书签HTML内容支持2-4层级嵌套结构
function parseBookmarks(htmlContent) {
// 正则表达式匹配文件夹和书签
const folderRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/g;
const bookmarkRegex = /<DT><A HREF="([^"]+)"[^>]*>(.*?)<\/A>/g;
// 储存解析结果
const bookmarks = {
categories: []
};
// 递归解析嵌套文件夹
function parseNestedFolder(htmlContent, parentPath = [], level = 1) {
const folders = [];
let match;
// 使用正则表达式匹配文件夹
folderRegex.lastIndex = 0;
while ((match = folderRegex.exec(htmlContent)) !== null) {
const folderName = match[2].trim();
const folderStart = match.index;
const folderEnd = match.index + match[0].length;
// 查找文件夹的结束位置
let folderContentEnd = htmlContent.length;
let depth = 1;
let pos = folderEnd;
while (pos < htmlContent.length && depth > 0) {
const dlStart = htmlContent.substring(pos).match(/<DL><p>/gi);
const dlEnd = htmlContent.substring(pos).match(/<\/DL><p>/gi);
if (dlStart && dlStart.index < (dlEnd ? dlEnd.index : htmlContent.length)) {
depth++;
pos += dlStart.index + dlStart[0].length;
} else if (dlEnd) {
depth--;
pos += dlEnd.index + dlEnd[0].length;
} else {
break;
}
}
folderContentEnd = pos;
const folderContent = htmlContent.substring(folderEnd, folderContentEnd);
// 解析文件夹内容
const folder = {
name: folderName,
icon: 'fas fa-folder',
path: [...parentPath, folderName]
};
// 检查是否包含子文件夹
const hasSubfolders = folderRegex.test(folderContent);
folderRegex.lastIndex = 0;
if (hasSubfolders && level < 4) {
// 递归解析子文件夹
const subfolders = parseNestedFolder(folderContent, folder.path, level + 1);
// 根据层级深度决定数据结构
if (level === 1) {
folder.subcategories = subfolders;
} else if (level === 2) {
folder.groups = subfolders;
} else if (level === 3) {
// 层级3直接解析书签
folder.sites = parseSitesInFolder(folderContent);
}
} else {
// 解析书签
folder.sites = parseSitesInFolder(folderContent);
}
// 只添加包含内容的文件夹
const hasContent = folder.sites && folder.sites.length > 0 ||
folder.subcategories && folder.subcategories.length > 0 ||
folder.groups && folder.groups.length > 0;
if (hasContent) {
folders.push(folder);
}
}
return folders;
}
// 解析文件夹中的书签
function parseSitesInFolder(folderContent) {
const sites = [];
let bookmarkMatch;
bookmarkRegex.lastIndex = 0;
while ((bookmarkMatch = bookmarkRegex.exec(folderContent)) !== null) {
const url = bookmarkMatch[1];
const name = bookmarkMatch[2].trim();
// 基于URL选择适当的图标
let icon = 'fas fa-link'; // 默认图标
for (const [keyword, iconClass] of Object.entries(ICON_MAPPING)) {
if (url.includes(keyword)) {
icon = iconClass;
break;
}
}
sites.push({
name: name,
url: url,
icon: icon,
description: ''
});
}
return sites;
}
// 开始解析
bookmarks.categories = parseNestedFolder(htmlContent);
return bookmarks;
}
// 生成YAML配置
function generateBookmarksYaml(bookmarks) {
try {
// 创建书签页面配置
const bookmarksPage = {
title: '我的书签',
subtitle: '从浏览器导入的书签收藏',
categories: bookmarks.categories
};
// 转换为YAML
const yamlString = yaml.dump(bookmarksPage, {
indent: 2,
lineWidth: -1,
quotingType: '"'
});
// 添加注释
const yamlWithComment =
`# 自动生成的书签配置文件
# 由bookmark-processor.js生成于 ${new Date().toISOString()}
# 若要更新请将新的书签HTML文件放入bookmarks/目录
# 此文件使用模块化配置格式位于config/user/pages/目录下
${yamlString}`;
return yamlWithComment;
} catch (error) {
console.error('Error generating YAML:', error);
return null;
}
}
// 更新导航以包含书签页面
function updateNavigationWithBookmarks() {
// 模块化配置文件
const modularUserNavFile = path.join(CONFIG_USER_DIR, 'navigation.yml');
const modularDefaultNavFile = 'config/_default/navigation.yml';
let navigationUpdated = false;
// 按优先级顺序尝试更新导航配置
// 1. 首选: 模块化用户导航配置
if (fs.existsSync(modularUserNavFile)) {
navigationUpdated = updateNavigationFile(modularUserNavFile);
}
// 2. 其次: 模块化默认导航配置
else if (fs.existsSync(modularDefaultNavFile)) {
// 如果用户导航文件不存在,我们需要先创建它,然后基于默认文件更新
try {
// 读取默认导航文件
const defaultNavContent = fs.readFileSync(modularDefaultNavFile, 'utf8');
const defaultNav = yaml.load(defaultNavContent);
// 确保目录存在
if (!fs.existsSync(CONFIG_USER_DIR)) {
fs.mkdirSync(CONFIG_USER_DIR, { recursive: true });
}
// 写入用户导航文件
fs.writeFileSync(modularUserNavFile, defaultNavContent, 'utf8');
// 更新新创建的文件
navigationUpdated = updateNavigationFile(modularUserNavFile);
} catch (error) {
console.error(`Error creating user navigation file:`, error);
}
}
}
// 更新单个导航配置文件
function updateNavigationFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const navConfig = yaml.load(content);
// 检查是否已有书签页面
const hasBookmarksNav = Array.isArray(navConfig) &&
navConfig.some(nav => nav.id === 'bookmarks');
if (!hasBookmarksNav) {
// 添加书签导航项
if (!Array.isArray(navConfig)) {
console.log(`Warning: Navigation config in ${filePath} is not an array, cannot update`);
return false;
}
navConfig.push({
name: '书签',
icon: 'fas fa-bookmark',
id: 'bookmarks',
active: false
});
// 更新文件
const updatedYaml = yaml.dump(navConfig, {
indent: 2,
lineWidth: -1,
quotingType: '"'
});
fs.writeFileSync(filePath, updatedYaml, 'utf8');
return true;
}
return false; // 无需更新
} catch (error) {
console.error(`Error updating navigation file ${filePath}:`, error);
return false;
}
}
// 主函数
async function main() {
// 获取最新的书签文件
const bookmarkFile = getLatestBookmarkFile();
if (!bookmarkFile) {
return;
}
try {
// 读取文件内容
const htmlContent = fs.readFileSync(bookmarkFile, 'utf8');
// 解析书签
const bookmarks = parseBookmarks(htmlContent);
if (bookmarks.categories.length === 0) {
console.error('ERROR: No bookmark categories found in the HTML file. Processing aborted.');
return;
}
// 生成YAML
const yaml = generateBookmarksYaml(bookmarks);
if (!yaml) {
console.error('ERROR: Failed to generate YAML from bookmarks. Processing aborted.');
return;
}
try {
// 确保目标目录存在
if (!fs.existsSync(CONFIG_USER_PAGES_DIR)) {
fs.mkdirSync(CONFIG_USER_PAGES_DIR, { recursive: true });
}
// 保存YAML到模块化位置
fs.writeFileSync(MODULAR_OUTPUT_FILE, yaml, 'utf8');
// 验证文件是否确实被创建
if (!fs.existsSync(MODULAR_OUTPUT_FILE)) {
console.error(`ERROR: File was not created: ${MODULAR_OUTPUT_FILE}`);
process.exit(1);
}
// 更新导航
updateNavigationWithBookmarks();
} catch (writeError) {
console.error(`ERROR writing file:`, writeError);
process.exit(1);
}
} catch (error) {
console.error('Error processing bookmark file:', error);
process.exit(1);
}
}
// 启动处理
if (require.main === module) {
main().catch(err => {
console.error('Unhandled error in bookmark processing:', err);
process.exit(1);
});
}