feat: 所有页面支持1到4层级的嵌套结构
This commit is contained in:
@@ -103,11 +103,10 @@ function getLatestBookmarkFile() {
|
||||
}
|
||||
}
|
||||
|
||||
// 解析书签HTML内容
|
||||
// 解析书签HTML内容,支持2-4层级嵌套结构
|
||||
function parseBookmarks(htmlContent) {
|
||||
// 简单的正则表达式匹配方法解析书签文件
|
||||
// 注意:这是一个简化实现,可能不适用于所有浏览器的书签格式
|
||||
const folderRegex = /<DT><H3[^>]*>(.*?)<\/H3>/g;
|
||||
// 正则表达式匹配文件夹和书签
|
||||
const folderRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/g;
|
||||
const bookmarkRegex = /<DT><A HREF="([^"]+)"[^>]*>(.*?)<\/A>/g;
|
||||
|
||||
// 储存解析结果
|
||||
@@ -115,31 +114,89 @@ function parseBookmarks(htmlContent) {
|
||||
categories: []
|
||||
};
|
||||
|
||||
// 提取文件夹
|
||||
let match;
|
||||
let folderMatches = [];
|
||||
while ((match = folderRegex.exec(htmlContent)) !== null) {
|
||||
folderMatches.push({
|
||||
index: match.index,
|
||||
name: match[1].trim(),
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
// 递归解析嵌套文件夹
|
||||
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;
|
||||
}
|
||||
|
||||
// 对每个文件夹,提取其中的书签
|
||||
for (let i = 0; i < folderMatches.length; i++) {
|
||||
const folder = folderMatches[i];
|
||||
const nextFolder = folderMatches[i + 1];
|
||||
|
||||
// 确定当前文件夹的内容范围
|
||||
const folderContent = nextFolder
|
||||
? htmlContent.substring(folder.end, nextFolder.index)
|
||||
: htmlContent.substring(folder.end);
|
||||
|
||||
// 从文件夹内容中提取书签
|
||||
// 解析文件夹中的书签
|
||||
function parseSitesInFolder(folderContent) {
|
||||
const sites = [];
|
||||
let bookmarkMatch;
|
||||
bookmarkRegex.lastIndex = 0; // 重置regex索引
|
||||
bookmarkRegex.lastIndex = 0;
|
||||
|
||||
while ((bookmarkMatch = bookmarkRegex.exec(folderContent)) !== null) {
|
||||
const url = bookmarkMatch[1];
|
||||
@@ -162,16 +219,12 @@ function parseBookmarks(htmlContent) {
|
||||
});
|
||||
}
|
||||
|
||||
// 只添加包含书签的文件夹
|
||||
if (sites.length > 0) {
|
||||
bookmarks.categories.push({
|
||||
name: folder.name,
|
||||
icon: 'fas fa-folder', // 默认使用文件夹图标
|
||||
sites: sites
|
||||
});
|
||||
}
|
||||
return sites;
|
||||
}
|
||||
|
||||
// 开始解析
|
||||
bookmarks.categories = parseNestedFolder(htmlContent);
|
||||
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
|
||||
@@ -196,7 +196,21 @@ function safeLoadYamlConfig(filePath) {
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
return yaml.load(fileContent);
|
||||
// 使用 loadAll 而不是 load 来支持多文档 YAML 文件
|
||||
const docs = yaml.loadAll(fileContent);
|
||||
|
||||
// 如果只有一个文档,直接返回
|
||||
if (docs.length === 1) {
|
||||
return docs[0];
|
||||
}
|
||||
|
||||
// 如果有多个文档,返回第一个文档(忽略后面的文档)
|
||||
if (docs.length > 1) {
|
||||
console.warn(`Warning: Multiple documents found in ${filePath}. Using the first document only.`);
|
||||
return docs[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
handleConfigLoadError(filePath, error);
|
||||
return null;
|
||||
|
||||
@@ -181,6 +181,19 @@ function encodeURIComponentHelper(text) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数学加法运算助手函数
|
||||
* @param {number} a 第一个数
|
||||
* @param {number} b 第二个数
|
||||
* @returns {number} 两数之和
|
||||
* @example {{add level 1}}
|
||||
*/
|
||||
function add(a, b) {
|
||||
const numA = parseInt(a, 10) || 0;
|
||||
const numB = parseInt(b, 10) || 0;
|
||||
return numA + numB;
|
||||
}
|
||||
|
||||
// 导出所有工具类助手函数
|
||||
module.exports = {
|
||||
slice,
|
||||
@@ -191,5 +204,6 @@ module.exports = {
|
||||
range,
|
||||
pick,
|
||||
keys,
|
||||
encodeURIComponent: encodeURIComponentHelper
|
||||
encodeURIComponent: encodeURIComponentHelper,
|
||||
add
|
||||
};
|
||||
|
||||
134
src/script.js
134
src/script.js
@@ -342,6 +342,137 @@ window.MeNav = {
|
||||
}
|
||||
};
|
||||
|
||||
// 多层级嵌套书签功能
|
||||
window.MeNav.expandAll = function() {
|
||||
document.querySelectorAll('.category.collapsed, .group.collapsed').forEach(element => {
|
||||
element.classList.remove('collapsed');
|
||||
saveToggleState(element, 'expanded');
|
||||
});
|
||||
};
|
||||
|
||||
window.MeNav.collapseAll = function() {
|
||||
document.querySelectorAll('.category:not(.collapsed), .group:not(.collapsed)').forEach(element => {
|
||||
element.classList.add('collapsed');
|
||||
saveToggleState(element, 'collapsed');
|
||||
});
|
||||
};
|
||||
|
||||
window.MeNav.toggleCategory = function(categoryName, subcategoryName = null, groupName = null) {
|
||||
const selector = groupName
|
||||
? `[data-name="${categoryName}"] [data-name="${subcategoryName}"] [data-name="${groupName}"]`
|
||||
: subcategoryName
|
||||
? `[data-name="${categoryName}"] [data-name="${subcategoryName}"]`
|
||||
: `[data-name="${categoryName}"]`;
|
||||
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
toggleNestedElement(element);
|
||||
}
|
||||
};
|
||||
|
||||
window.MeNav.getNestedStructure = function() {
|
||||
// 返回完整的嵌套结构数据
|
||||
const categories = [];
|
||||
document.querySelectorAll('.category-level-1').forEach(cat => {
|
||||
categories.push(extractNestedData(cat));
|
||||
});
|
||||
return categories;
|
||||
};
|
||||
|
||||
// 切换嵌套元素
|
||||
function toggleNestedElement(container) {
|
||||
const isCollapsed = container.classList.contains('collapsed');
|
||||
|
||||
if (isCollapsed) {
|
||||
container.classList.remove('collapsed');
|
||||
saveToggleState(container, 'expanded');
|
||||
} else {
|
||||
container.classList.add('collapsed');
|
||||
saveToggleState(container, 'collapsed');
|
||||
}
|
||||
|
||||
// 触发自定义事件
|
||||
const event = new CustomEvent('nestedToggle', {
|
||||
detail: {
|
||||
element: container,
|
||||
type: container.dataset.type,
|
||||
name: container.dataset.name,
|
||||
isCollapsed: !isCollapsed
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
// 保存切换状态
|
||||
function saveToggleState(element, state) {
|
||||
const type = element.dataset.type;
|
||||
const name = element.dataset.name;
|
||||
const level = element.dataset.level || '1';
|
||||
const key = `menav-toggle-${type}-${level}-${name}`;
|
||||
localStorage.setItem(key, state);
|
||||
}
|
||||
|
||||
// 恢复切换状态
|
||||
function restoreToggleState(element) {
|
||||
const type = element.dataset.type;
|
||||
const name = element.dataset.name;
|
||||
const level = element.dataset.level || '1';
|
||||
const key = `menav-toggle-${type}-${level}-${name}`;
|
||||
const savedState = localStorage.getItem(key);
|
||||
|
||||
if (savedState === 'collapsed') {
|
||||
element.classList.add('collapsed');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化嵌套分类
|
||||
function initializeNestedCategories() {
|
||||
// 为所有可折叠元素添加切换功能
|
||||
document.querySelectorAll('[data-toggle="category"], [data-toggle="group"]').forEach(header => {
|
||||
header.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const container = this.parentElement;
|
||||
toggleNestedElement(container);
|
||||
});
|
||||
|
||||
// 恢复保存的状态
|
||||
restoreToggleState(header.parentElement);
|
||||
});
|
||||
}
|
||||
|
||||
// 提取嵌套数据
|
||||
function extractNestedData(element) {
|
||||
const data = {
|
||||
name: element.dataset.name,
|
||||
type: element.dataset.type,
|
||||
level: element.dataset.level,
|
||||
isCollapsed: element.classList.contains('collapsed')
|
||||
};
|
||||
|
||||
// 提取子元素数据
|
||||
const subcategories = element.querySelectorAll(':scope > .category-content > .subcategories-container > .category');
|
||||
if (subcategories.length > 0) {
|
||||
data.subcategories = Array.from(subcategories).map(sub => extractNestedData(sub));
|
||||
}
|
||||
|
||||
const groups = element.querySelectorAll(':scope > .category-content > .groups-container > .group');
|
||||
if (groups.length > 0) {
|
||||
data.groups = Array.from(groups).map(group => extractNestedData(group));
|
||||
}
|
||||
|
||||
const sites = element.querySelectorAll(':scope > .category-content > .sites-grid > .site-card, :scope > .group-content > .sites-grid > .site-card');
|
||||
if (sites.length > 0) {
|
||||
data.sites = Array.from(sites).map(site => ({
|
||||
name: site.dataset.name,
|
||||
url: site.dataset.url,
|
||||
icon: site.dataset.icon,
|
||||
description: site.dataset.description
|
||||
}));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 先声明所有状态变量
|
||||
let isSearchActive = false;
|
||||
@@ -1312,6 +1443,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// 初始化嵌套分类功能
|
||||
initializeNestedCategories();
|
||||
|
||||
// 初始化搜索索引(使用requestIdleCallback或setTimeout延迟初始化,避免影响页面加载)
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => initSearchIndex());
|
||||
|
||||
Reference in New Issue
Block a user