feat: 优化书签转换逻辑和分类嵌套结构
This commit is contained in:
@@ -1189,6 +1189,26 @@ body .content.expanded {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 当分类同时包含子分类和站点时的样式优化 */
|
||||
.category-content .subcategories-container + .sites-grid {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* 当分类同时包含分组和站点时的样式优化 */
|
||||
.category-content .groups-container + .sites-grid {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* 子分类容器底部间距调整 */
|
||||
.category-content .subcategories-container:not(:last-child),
|
||||
.category-content .groups-container:not(:last-child) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 确保嵌套的网站网格正确显示 */
|
||||
.category-level-2 .sites-grid,
|
||||
.group-level-3 .sites-grid,
|
||||
@@ -1412,12 +1432,8 @@ body .content.expanded {
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
transition: color 0.3s ease;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-height: 2.8em;
|
||||
word-break: break-word;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1812,7 +1828,9 @@ body .content.expanded {
|
||||
.site-card p {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.3;
|
||||
max-height: 2.6em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 在移动端的主题切换按钮 */
|
||||
@@ -1888,9 +1906,10 @@ body .content.expanded {
|
||||
|
||||
.site-card p {
|
||||
font-size: 0.8rem;
|
||||
-webkit-line-clamp: 2;
|
||||
max-height: 2.4em;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1926,9 +1945,10 @@ body .content.expanded {
|
||||
|
||||
.site-card p {
|
||||
font-size: 0.75rem;
|
||||
-webkit-line-clamp: 2;
|
||||
max-height: 2.5em;
|
||||
line-height: 1.15;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,12 +72,8 @@ const ICON_MAPPING = {
|
||||
// 获取最新的书签文件
|
||||
function getLatestBookmarkFile() {
|
||||
try {
|
||||
console.log('[DEBUG] 开始查找书签文件...');
|
||||
console.log('[DEBUG] 书签目录:', BOOKMARKS_DIR);
|
||||
|
||||
// 确保书签目录存在
|
||||
if (!fs.existsSync(BOOKMARKS_DIR)) {
|
||||
console.log('[DEBUG] 书签目录不存在,创建目录...');
|
||||
fs.mkdirSync(BOOKMARKS_DIR, { recursive: true });
|
||||
console.log('[WARN] 书签目录为空,未找到HTML文件');
|
||||
return null;
|
||||
@@ -87,11 +83,6 @@ function getLatestBookmarkFile() {
|
||||
const files = fs.readdirSync(BOOKMARKS_DIR)
|
||||
.filter(file => file.toLowerCase().endsWith('.html'));
|
||||
|
||||
console.log('[DEBUG] 找到的HTML文件数量:', files.length);
|
||||
if (files.length > 0) {
|
||||
console.log('[DEBUG] HTML文件列表:', files);
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('[WARN] 未找到任何HTML书签文件');
|
||||
return null;
|
||||
@@ -109,7 +100,6 @@ function getLatestBookmarkFile() {
|
||||
const latestFilePath = path.join(BOOKMARKS_DIR, latestFile);
|
||||
|
||||
console.log('[INFO] 选择最新的书签文件:', latestFile);
|
||||
console.log('[DEBUG] 完整路径:', latestFilePath);
|
||||
|
||||
return latestFilePath;
|
||||
} catch (error) {
|
||||
@@ -120,8 +110,6 @@ function getLatestBookmarkFile() {
|
||||
|
||||
// 解析书签HTML内容,支持2-4层级嵌套结构
|
||||
function parseBookmarks(htmlContent) {
|
||||
console.log('[DEBUG] 开始解析书签HTML内容...');
|
||||
console.log('[DEBUG] HTML内容长度:', htmlContent.length, '字符');
|
||||
|
||||
// 正则表达式匹配文件夹和书签
|
||||
const folderRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/g;
|
||||
@@ -132,79 +120,229 @@ function parseBookmarks(htmlContent) {
|
||||
categories: []
|
||||
};
|
||||
|
||||
// 提取根路径书签(书签栏容器内但不在任何子文件夹内的书签)
|
||||
function extractRootBookmarks(htmlContent) {
|
||||
// 找到书签栏文件夹标签
|
||||
const bookmarkBarMatch = htmlContent.match(/<DT><H3[^>]*PERSONAL_TOOLBAR_FOLDER[^>]*>([^<]+)<\/H3>/i);
|
||||
if (!bookmarkBarMatch) {
|
||||
return [];
|
||||
}
|
||||
const bookmarkBarStart = bookmarkBarMatch.index + bookmarkBarMatch[0].length;
|
||||
|
||||
// 找到书签栏后面的 <DL><p> 标签
|
||||
const remainingAfterBar = htmlContent.substring(bookmarkBarStart);
|
||||
const dlMatch = remainingAfterBar.match(/<DL><p>/i);
|
||||
if (!dlMatch) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const bookmarkBarContentStart = bookmarkBarStart + dlMatch.index + dlMatch[0].length;
|
||||
|
||||
// 找到书签栏内容的结束位置
|
||||
let depth = 1;
|
||||
let pos = bookmarkBarContentStart;
|
||||
let bookmarkBarContentEnd = htmlContent.length;
|
||||
|
||||
while (pos < htmlContent.length && depth > 0) {
|
||||
const remaining = htmlContent.substring(pos);
|
||||
const dlStartIndex = remaining.search(/<DL><p>/i);
|
||||
const dlEndIndex = remaining.search(/<\/DL><p>/i);
|
||||
|
||||
if (dlStartIndex !== -1 && (dlEndIndex === -1 || dlStartIndex < dlEndIndex)) {
|
||||
depth++;
|
||||
pos += dlStartIndex + '<DL><p>'.length;
|
||||
} else if (dlEndIndex !== -1) {
|
||||
depth--;
|
||||
pos += dlEndIndex;
|
||||
if (depth === 0) {
|
||||
bookmarkBarContentEnd = pos;
|
||||
}
|
||||
pos += '</DL><p>'.length;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const bookmarkBarContent = htmlContent.substring(bookmarkBarContentStart, bookmarkBarContentEnd);
|
||||
|
||||
// 现在提取书签栏内所有子文件夹的范围
|
||||
const subfolderRanges = [];
|
||||
const folderRegex = /<DT><H3[^>]*>([^<]+)<\/H3>/g;
|
||||
let folderMatch;
|
||||
|
||||
while ((folderMatch = folderRegex.exec(bookmarkBarContent)) !== null) {
|
||||
const folderName = folderMatch[1].trim();
|
||||
const folderStart = folderMatch.index + folderMatch[0].length;
|
||||
|
||||
// 找到这个文件夹内容的结束位置
|
||||
let folderDepth = 0;
|
||||
let folderPos = folderStart;
|
||||
let folderContentEnd = bookmarkBarContent.length;
|
||||
|
||||
// 跳过空白直到找到 <DL><p>
|
||||
const afterFolder = bookmarkBarContent.substring(folderPos);
|
||||
const folderDLMatch = afterFolder.match(/<DL><p>/i);
|
||||
if (folderDLMatch) {
|
||||
folderDepth = 1;
|
||||
folderPos += folderDLMatch.index + folderDLMatch[0].length;
|
||||
|
||||
while (folderPos < bookmarkBarContent.length && folderDepth > 0) {
|
||||
const remaining = bookmarkBarContent.substring(folderPos);
|
||||
const dlStartIdx = remaining.search(/<DL><p>/i);
|
||||
const dlEndIdx = remaining.search(/<\/DL><p>/i);
|
||||
|
||||
if (dlStartIdx !== -1 && (dlEndIdx === -1 || dlStartIdx < dlEndIdx)) {
|
||||
folderDepth++;
|
||||
folderPos += dlStartIdx + '<DL><p>'.length;
|
||||
} else if (dlEndIdx !== -1) {
|
||||
folderDepth--;
|
||||
folderPos += dlEndIdx;
|
||||
if (folderDepth === 0) {
|
||||
folderContentEnd = folderPos + '</DL><p>'.length;
|
||||
}
|
||||
folderPos += '</DL><p>'.length;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
subfolderRanges.push({
|
||||
name: folderName,
|
||||
start: folderMatch.index,
|
||||
end: folderContentEnd
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 提取不在任何子文件夹范围内的书签
|
||||
const rootSites = [];
|
||||
const bookmarkRegex = /<DT><A HREF="([^"]+)"[^>]*>(.*?)<\/A>/g;
|
||||
let bookmarkMatch;
|
||||
|
||||
while ((bookmarkMatch = bookmarkRegex.exec(bookmarkBarContent)) !== null) {
|
||||
const bookmarkPos = bookmarkMatch.index;
|
||||
const url = bookmarkMatch[1];
|
||||
const name = bookmarkMatch[2].trim();
|
||||
|
||||
// 检查这个书签是否在任何子文件夹范围内
|
||||
let inFolder = false;
|
||||
for (const folder of subfolderRanges) {
|
||||
if (bookmarkPos >= folder.start && bookmarkPos < folder.end) {
|
||||
inFolder = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!inFolder) {
|
||||
|
||||
// 基于URL选择适当的图标
|
||||
let icon = 'fas fa-link';
|
||||
for (const [keyword, iconClass] of Object.entries(ICON_MAPPING)) {
|
||||
if (url.includes(keyword)) {
|
||||
icon = iconClass;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
rootSites.push({
|
||||
name: name,
|
||||
url: url,
|
||||
icon: icon,
|
||||
description: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return rootSites;
|
||||
}
|
||||
|
||||
// 递归解析嵌套文件夹
|
||||
function parseNestedFolder(htmlContent, parentPath = [], level = 1) {
|
||||
console.log(`[DEBUG] parseNestedFolder 被调用 - 层级:${level}, 路径:${parentPath.join('/')}, 内容长度:${htmlContent.length}`);
|
||||
|
||||
const folders = [];
|
||||
let match;
|
||||
let matchCount = 0;
|
||||
|
||||
// 创建新的正则表达式实例,避免全局正则的 lastIndex 问题
|
||||
const localFolderRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/g;
|
||||
// 第一步:扫描所有文件夹,记录它们的完整范围
|
||||
const folderRanges = [];
|
||||
const scanRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/g;
|
||||
let scanMatch;
|
||||
|
||||
while ((match = localFolderRegex.exec(htmlContent)) !== null) {
|
||||
matchCount++;
|
||||
const folderName = match[2].trim();
|
||||
const folderStart = match.index;
|
||||
const folderEnd = match.index + match[0].length;
|
||||
while ((scanMatch = scanRegex.exec(htmlContent)) !== null) {
|
||||
const folderName = scanMatch[2].trim();
|
||||
const folderStart = scanMatch.index;
|
||||
const folderHeaderEnd = scanMatch.index + scanMatch[0].length;
|
||||
|
||||
console.log(`[DEBUG] 找到文件夹 #${matchCount}: "${folderName}" (层级${level}) 在位置 ${folderStart}`);
|
||||
// 找到文件夹内容的结束位置
|
||||
let depth = 0;
|
||||
let pos = folderHeaderEnd;
|
||||
|
||||
// 查找文件夹的结束位置
|
||||
let folderContentEnd = htmlContent.length;
|
||||
let depth = 1;
|
||||
let pos = folderEnd;
|
||||
let loopCount = 0;
|
||||
const maxLoops = 10000; // 防止无限循环
|
||||
|
||||
console.log(`[DEBUG] 开始查找文件夹"${folderName}"的边界,起始位置:${pos}`);
|
||||
|
||||
while (pos < htmlContent.length && depth > 0) {
|
||||
loopCount++;
|
||||
if (loopCount > maxLoops) {
|
||||
console.error(`[ERROR] 检测到可能的无限循环! 文件夹:"${folderName}", 层级:${level}, 循环次数:${loopCount}`);
|
||||
console.error(`[ERROR] 当前位置:${pos}, 深度:${depth}`);
|
||||
console.error(`[ERROR] 周围内容:`, htmlContent.substring(pos, pos + 100));
|
||||
break;
|
||||
}
|
||||
// 跳过空白直到找到 <DL><p>
|
||||
const afterFolder = htmlContent.substring(pos);
|
||||
const folderDLMatch = afterFolder.match(/<DL><p>/i);
|
||||
if (folderDLMatch) {
|
||||
depth = 1;
|
||||
pos += folderDLMatch.index + folderDLMatch[0].length;
|
||||
|
||||
// 修复:使用 search() 而不是 match(),因为 match() 返回数组没有 index 属性
|
||||
const remainingContent = htmlContent.substring(pos);
|
||||
const dlStartIndex = remainingContent.search(/<DL><p>/i);
|
||||
const dlEndIndex = remainingContent.search(/<\/DL><p>/i);
|
||||
while (pos < htmlContent.length && depth > 0) {
|
||||
const remaining = htmlContent.substring(pos);
|
||||
const dlStartIdx = remaining.search(/<DL><p>/i);
|
||||
const dlEndIdx = remaining.search(/<\/DL><p>/i);
|
||||
|
||||
if (dlStartIdx !== -1 && (dlEndIdx === -1 || dlStartIdx < dlEndIdx)) {
|
||||
depth++;
|
||||
pos += dlStartIdx + '<DL><p>'.length;
|
||||
} else if (dlEndIdx !== -1) {
|
||||
depth--;
|
||||
pos += dlEndIdx;
|
||||
if (depth === 0) {
|
||||
const folderEnd = pos + '</DL><p>'.length;
|
||||
folderRanges.push({
|
||||
name: folderName,
|
||||
start: folderStart,
|
||||
headerEnd: folderHeaderEnd,
|
||||
end: folderEnd
|
||||
});
|
||||
}
|
||||
pos += '</DL><p>'.length;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步:只处理当前层级的文件夹(不在其他文件夹内部的)
|
||||
for (let i = 0; i < folderRanges.length; i++) {
|
||||
const currentFolder = folderRanges[i];
|
||||
|
||||
// 检查这个文件夹是否在其他文件夹内部
|
||||
let isNested = false;
|
||||
for (let j = 0; j < folderRanges.length; j++) {
|
||||
if (i === j) continue; // 跳过自己
|
||||
|
||||
if (loopCount % 100 === 0) {
|
||||
console.log(`[DEBUG] 循环 ${loopCount}: pos=${pos}, depth=${depth}, dlStart=${dlStartIndex}, dlEnd=${dlEndIndex}`);
|
||||
}
|
||||
|
||||
// 找到开始标签且在结束标签之前(或没有结束标签)
|
||||
if (dlStartIndex !== -1 && (dlEndIndex === -1 || dlStartIndex < dlEndIndex)) {
|
||||
depth++;
|
||||
pos += dlStartIndex + '<DL><p>'.length;
|
||||
console.log(`[DEBUG] 找到 <DL><p> 在位置 ${pos}, depth增加到 ${depth}`);
|
||||
}
|
||||
// 找到结束标签
|
||||
else if (dlEndIndex !== -1) {
|
||||
depth--;
|
||||
pos += dlEndIndex + '</DL><p>'.length;
|
||||
console.log(`[DEBUG] 找到 </DL><p> 在位置 ${pos}, depth减少到 ${depth}`);
|
||||
}
|
||||
// 都没找到,退出循环
|
||||
else {
|
||||
console.log(`[DEBUG] 未找到更多标签,退出循环`);
|
||||
const otherFolder = folderRanges[j];
|
||||
// 如果当前文件夹的起始位置在另一个文件夹的范围内,说明它是嵌套的
|
||||
if (currentFolder.start > otherFolder.start && currentFolder.end <= otherFolder.end) {
|
||||
isNested = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (loopCount > 100) {
|
||||
console.log(`[DEBUG] 文件夹"${folderName}"边界查找循环${loopCount}次`);
|
||||
if (isNested) {
|
||||
continue; // 跳过嵌套的文件夹,它们会被递归调用处理
|
||||
}
|
||||
|
||||
folderContentEnd = pos;
|
||||
const folderContent = htmlContent.substring(folderEnd, folderContentEnd);
|
||||
const folderName = currentFolder.name;
|
||||
const folderStart = currentFolder.start;
|
||||
const folderHeaderEnd = currentFolder.headerEnd;
|
||||
const folderEnd = currentFolder.end;
|
||||
|
||||
console.log(`[DEBUG] 文件夹"${folderName}"内容长度: ${folderContent.length}`);
|
||||
// 提取文件夹内容(保留完整的HTML结构供递归使用)
|
||||
// 从headerEnd到end之间包含完整的<DL><p>...</DL><p>结构
|
||||
const folderContent = htmlContent.substring(folderHeaderEnd, folderEnd);
|
||||
|
||||
// 验证是否有有效的容器结构
|
||||
if (!/<DL><p>/i.test(folderContent)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析文件夹内容
|
||||
const folder = {
|
||||
@@ -217,85 +355,240 @@ function parseBookmarks(htmlContent) {
|
||||
const testFolderRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/;
|
||||
const hasSubfolders = testFolderRegex.test(folderContent);
|
||||
|
||||
console.log(`[DEBUG] 文件夹"${folderName}"包含子文件夹: ${hasSubfolders}`);
|
||||
// 先解析当前层级的书签
|
||||
const currentLevelSites = parseSitesInFolder(folderContent, folderName);
|
||||
|
||||
if (hasSubfolders && level < 4) {
|
||||
console.log(`[DEBUG] 递归解析文件夹"${folderName}"的子文件夹...`);
|
||||
// 递归解析子文件夹
|
||||
const subfolders = parseNestedFolder(folderContent, folder.path, level + 1);
|
||||
|
||||
console.log(`[DEBUG] 文件夹"${folderName}"解析到 ${subfolders.length} 个子项`);
|
||||
|
||||
// 根据层级深度决定数据结构
|
||||
if (level === 1) {
|
||||
folder.subcategories = subfolders;
|
||||
} else if (level === 2) {
|
||||
folder.groups = subfolders;
|
||||
} else if (level === 3) {
|
||||
// 层级3直接解析书签
|
||||
folder.sites = parseSitesInFolder(folderContent);
|
||||
folder.subgroups = subfolders;
|
||||
}
|
||||
|
||||
// 添加当前层级的书签(如果有)
|
||||
if (currentLevelSites.length > 0) {
|
||||
folder.sites = currentLevelSites;
|
||||
}
|
||||
} else {
|
||||
console.log(`[DEBUG] 解析文件夹"${folderName}"中的书签...`);
|
||||
// 解析书签
|
||||
folder.sites = parseSitesInFolder(folderContent);
|
||||
folder.sites = currentLevelSites;
|
||||
}
|
||||
|
||||
// 只添加包含内容的文件夹
|
||||
const hasContent = folder.sites && folder.sites.length > 0 ||
|
||||
folder.subcategories && folder.subcategories.length > 0 ||
|
||||
folder.groups && folder.groups.length > 0;
|
||||
folder.groups && folder.groups.length > 0 ||
|
||||
folder.subgroups && folder.subgroups.length > 0;
|
||||
|
||||
if (hasContent) {
|
||||
console.log(`[DEBUG] 添加文件夹"${folderName}" (包含内容)`);
|
||||
folders.push(folder);
|
||||
} else {
|
||||
console.log(`[DEBUG] 跳过空文件夹"${folderName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[DEBUG] parseNestedFolder 完成 - 层级:${level}, 返回 ${folders.length} 个文件夹`);
|
||||
return folders;
|
||||
}
|
||||
|
||||
// 解析文件夹中的书签
|
||||
// 解析文件夹中的书签(仅当前层级,排除子文件夹内的书签)
|
||||
function parseSitesInFolder(folderContent) {
|
||||
const sites = [];
|
||||
let bookmarkMatch;
|
||||
let siteCount = 0;
|
||||
bookmarkRegex.lastIndex = 0;
|
||||
|
||||
// 首先找到所有子文件夹的范围
|
||||
const subfolderRanges = [];
|
||||
const folderRegex = /<DT><H3[^>]*>([^<]+)<\/H3>/g;
|
||||
let folderMatch;
|
||||
|
||||
while ((folderMatch = folderRegex.exec(folderContent)) !== null) {
|
||||
const folderName = folderMatch[1].trim();
|
||||
const folderStart = folderMatch.index;
|
||||
const folderHeaderEnd = folderMatch.index + folderMatch[0].length;
|
||||
|
||||
// 找到这个文件夹内容的结束位置
|
||||
let folderDepth = 0;
|
||||
let folderPos = folderHeaderEnd;
|
||||
let folderContentEnd = folderContent.length;
|
||||
|
||||
// 跳过空白直到找到 <DL><p>
|
||||
const afterFolder = folderContent.substring(folderPos);
|
||||
const folderDLMatch = afterFolder.match(/<DL><p>/i);
|
||||
if (folderDLMatch) {
|
||||
folderDepth = 1;
|
||||
folderPos += folderDLMatch.index + folderDLMatch[0].length;
|
||||
|
||||
while (folderPos < folderContent.length && folderDepth > 0) {
|
||||
const remaining = folderContent.substring(folderPos);
|
||||
const dlStartIdx = remaining.search(/<DL><p>/i);
|
||||
const dlEndIdx = remaining.search(/<\/DL><p>/i);
|
||||
|
||||
if (dlStartIdx !== -1 && (dlEndIdx === -1 || dlStartIdx < dlEndIdx)) {
|
||||
folderDepth++;
|
||||
folderPos += dlStartIdx + '<DL><p>'.length;
|
||||
} else if (dlEndIdx !== -1) {
|
||||
folderDepth--;
|
||||
folderPos += dlEndIdx;
|
||||
if (folderDepth === 0) {
|
||||
folderContentEnd = folderPos + '</DL><p>'.length;
|
||||
}
|
||||
folderPos += '</DL><p>'.length;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
subfolderRanges.push({
|
||||
name: folderName,
|
||||
start: folderStart,
|
||||
end: folderContentEnd
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 现在提取不在任何子文件夹范围内的书签
|
||||
const bookmarkRegex = /<DT><A HREF="([^"]+)"[^>]*>(.*?)<\/A>/g;
|
||||
let bookmarkMatch;
|
||||
|
||||
while ((bookmarkMatch = bookmarkRegex.exec(folderContent)) !== null) {
|
||||
siteCount++;
|
||||
const bookmarkPos = bookmarkMatch.index;
|
||||
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;
|
||||
// 检查这个书签是否在任何子文件夹范围内
|
||||
let inSubfolder = false;
|
||||
for (const folder of subfolderRanges) {
|
||||
if (bookmarkPos >= folder.start && bookmarkPos < folder.end) {
|
||||
inSubfolder = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sites.push({
|
||||
name: name,
|
||||
url: url,
|
||||
icon: icon,
|
||||
description: ''
|
||||
});
|
||||
if (!inSubfolder) {
|
||||
|
||||
// 基于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: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[DEBUG] parseSitesInFolder 完成 - 找到 ${siteCount} 个书签`);
|
||||
return sites;
|
||||
}
|
||||
|
||||
// 开始解析
|
||||
console.log('[DEBUG] 开始递归解析顶层分类...');
|
||||
bookmarks.categories = parseNestedFolder(htmlContent);
|
||||
const rootSites = extractRootBookmarks(htmlContent);
|
||||
|
||||
// 找到书签栏文件夹(PERSONAL_TOOLBAR_FOLDER)
|
||||
const bookmarkBarMatch = htmlContent.match(/<DT><H3[^>]*PERSONAL_TOOLBAR_FOLDER[^>]*>([^<]+)<\/H3>/i);
|
||||
if (!bookmarkBarMatch) {
|
||||
console.log('[WARN] 未找到书签栏文件夹(PERSONAL_TOOLBAR_FOLDER),使用备用方案');
|
||||
// 备用方案:使用第一个 <DL><p> 标签
|
||||
const firstDLMatch = htmlContent.match(/<DL><p>/i);
|
||||
if (!firstDLMatch) {
|
||||
console.log('[ERROR] 未找到任何书签容器');
|
||||
bookmarks.categories = [];
|
||||
} else {
|
||||
const dlStart = firstDLMatch.index + firstDLMatch[0].length;
|
||||
let dlEnd = htmlContent.length;
|
||||
let depth = 1;
|
||||
let pos = dlStart;
|
||||
|
||||
while (pos < htmlContent.length && depth > 0) {
|
||||
const remainingContent = htmlContent.substring(pos);
|
||||
const dlStartIndex = remainingContent.search(/<DL><p>/i);
|
||||
const dlEndIndex = remainingContent.search(/<\/DL><p>/i);
|
||||
|
||||
if (dlStartIndex !== -1 && (dlEndIndex === -1 || dlStartIndex < dlEndIndex)) {
|
||||
depth++;
|
||||
pos += dlStartIndex + '<DL><p>'.length;
|
||||
} else if (dlEndIndex !== -1) {
|
||||
depth--;
|
||||
pos += dlEndIndex + '</DL><p>'.length;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dlEnd = pos - '</DL><p>'.length;
|
||||
const bookmarksBarContent = htmlContent.substring(dlStart, dlEnd);
|
||||
bookmarks.categories = parseNestedFolder(bookmarksBarContent);
|
||||
}
|
||||
} else {
|
||||
const bookmarkBarStart = bookmarkBarMatch.index + bookmarkBarMatch[0].length;
|
||||
|
||||
// 找到书签栏后面的 <DL><p> 标签
|
||||
const remainingAfterBar = htmlContent.substring(bookmarkBarStart);
|
||||
const dlMatch = remainingAfterBar.match(/<DL><p>/i);
|
||||
if (!dlMatch) {
|
||||
console.log('[ERROR] 未找到书签栏的内容容器 <DL><p>');
|
||||
bookmarks.categories = [];
|
||||
} else {
|
||||
const bookmarkBarContentStart = bookmarkBarStart + dlMatch.index + dlMatch[0].length;
|
||||
|
||||
// 找到书签栏内容的结束位置
|
||||
let depth = 1;
|
||||
let pos = bookmarkBarContentStart;
|
||||
let bookmarkBarContentEnd = htmlContent.length;
|
||||
|
||||
while (pos < htmlContent.length && depth > 0) {
|
||||
const remaining = htmlContent.substring(pos);
|
||||
const dlStartIndex = remaining.search(/<DL><p>/i);
|
||||
const dlEndIndex = remaining.search(/<\/DL><p>/i);
|
||||
|
||||
if (dlStartIndex !== -1 && (dlEndIndex === -1 || dlStartIndex < dlEndIndex)) {
|
||||
depth++;
|
||||
pos += dlStartIndex + '<DL><p>'.length;
|
||||
} else if (dlEndIndex !== -1) {
|
||||
depth--;
|
||||
pos += dlEndIndex;
|
||||
if (depth === 0) {
|
||||
bookmarkBarContentEnd = pos;
|
||||
}
|
||||
pos += '</DL><p>'.length;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const bookmarkBarContent = htmlContent.substring(bookmarkBarContentStart, bookmarkBarContentEnd);
|
||||
|
||||
// 解析书签栏内的子文件夹作为顶层分类(跳过书签栏本身)
|
||||
bookmarks.categories = parseNestedFolder(bookmarkBarContent);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[INFO] 解析完成 - 共找到 ${bookmarks.categories.length} 个顶层分类`);
|
||||
|
||||
// 如果存在根路径书签,创建"根目录书签"特殊分类并插入到首位
|
||||
if (rootSites.length > 0) {
|
||||
console.log(`[INFO] 创建"根目录书签"特殊分类,包含 ${rootSites.length} 个书签`);
|
||||
const rootCategory = {
|
||||
name: '根目录书签',
|
||||
icon: 'fas fa-star',
|
||||
path: ['根目录书签'],
|
||||
sites: rootSites
|
||||
};
|
||||
|
||||
// 插入到数组首位
|
||||
bookmarks.categories.unshift(rootCategory);
|
||||
console.log(`[INFO] "根目录书签"已插入到分类列表首位`);
|
||||
}
|
||||
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
@@ -426,13 +719,13 @@ async function main() {
|
||||
console.log('[ERROR] 未找到书签文件,处理终止');
|
||||
return;
|
||||
}
|
||||
console.log('[SUCCESS] ✓ 找到书签文件\n');
|
||||
console.log('[SUCCESS] 找到书签文件\n');
|
||||
|
||||
try {
|
||||
// 读取文件内容
|
||||
console.log('[步骤 2/5] 读取书签文件...');
|
||||
const htmlContent = fs.readFileSync(bookmarkFile, 'utf8');
|
||||
console.log('[SUCCESS] ✓ 文件读取成功,大小:', htmlContent.length, '字符\n');
|
||||
console.log('[SUCCESS] 文件读取成功,大小:', htmlContent.length, '字符\n');
|
||||
|
||||
// 解析书签
|
||||
console.log('[步骤 3/5] 解析书签结构...');
|
||||
@@ -441,7 +734,7 @@ async function main() {
|
||||
console.error('[ERROR] HTML文件中未找到书签分类,处理终止');
|
||||
return;
|
||||
}
|
||||
console.log('[SUCCESS] ✓ 解析完成\n');
|
||||
console.log('[SUCCESS] 解析完成\n');
|
||||
|
||||
// 生成YAML
|
||||
console.log('[步骤 4/5] 生成YAML配置...');
|
||||
@@ -450,20 +743,17 @@ async function main() {
|
||||
console.error('[ERROR] YAML生成失败,处理终止');
|
||||
return;
|
||||
}
|
||||
console.log('[DEBUG] YAML内容长度:', yamlContent.length, '字符');
|
||||
console.log('[SUCCESS] ✓ YAML生成成功\n');
|
||||
console.log('[SUCCESS] YAML生成成功\n');
|
||||
|
||||
// 保存文件
|
||||
console.log('[步骤 5/5] 保存配置文件...');
|
||||
try {
|
||||
// 确保目标目录存在
|
||||
if (!fs.existsSync(CONFIG_USER_PAGES_DIR)) {
|
||||
console.log('[DEBUG] 创建目录:', CONFIG_USER_PAGES_DIR);
|
||||
fs.mkdirSync(CONFIG_USER_PAGES_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// 保存YAML到模块化位置
|
||||
console.log('[DEBUG] 写入文件:', MODULAR_OUTPUT_FILE);
|
||||
fs.writeFileSync(MODULAR_OUTPUT_FILE, yamlContent, 'utf8');
|
||||
|
||||
// 验证文件是否确实被创建
|
||||
@@ -472,13 +762,13 @@ async function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('[SUCCESS] ✓ 文件保存成功');
|
||||
console.log('[SUCCESS] 文件保存成功');
|
||||
console.log('[INFO] 输出文件:', MODULAR_OUTPUT_FILE, '\n');
|
||||
|
||||
// 更新导航
|
||||
console.log('[附加步骤] 更新导航配置...');
|
||||
updateNavigationWithBookmarks();
|
||||
console.log('[SUCCESS] ✓ 导航配置已更新\n');
|
||||
console.log('[SUCCESS] 导航配置已更新\n');
|
||||
|
||||
} catch (writeError) {
|
||||
console.error(`[ERROR] 写入文件时出错:`, writeError);
|
||||
@@ -487,7 +777,7 @@ async function main() {
|
||||
}
|
||||
|
||||
console.log('========================================');
|
||||
console.log('[SUCCESS] ✓✓✓ 书签处理完成!✓✓✓');
|
||||
console.log('[SUCCESS] 书签处理完成!');
|
||||
console.log('========================================');
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -91,11 +91,37 @@ function json(obj) {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL中提取干净的域名
|
||||
* @param {string} url 完整URL
|
||||
* @returns {string} 提取的域名(不包含协议和尾部斜杠)
|
||||
* @example {{extractDomain "https://app.follow.is/"}} => "app.follow.is"
|
||||
*/
|
||||
function extractDomain(url) {
|
||||
if (!url) return '';
|
||||
|
||||
try {
|
||||
// 移除协议部分 (http://, https://, etc.)
|
||||
let domain = String(url).replace(/^[a-zA-Z]+:\/\//, '');
|
||||
|
||||
// 移除路径、查询参数和锚点
|
||||
domain = domain.split('/')[0].split('?')[0].split('#')[0];
|
||||
|
||||
// 移除端口号(如果有)
|
||||
domain = domain.split(':')[0];
|
||||
|
||||
return domain;
|
||||
} catch (e) {
|
||||
return String(url);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出所有格式化助手函数
|
||||
module.exports = {
|
||||
formatDate,
|
||||
limit,
|
||||
toLowerCase,
|
||||
toUpperCase,
|
||||
json
|
||||
};
|
||||
json,
|
||||
extractDomain
|
||||
};
|
||||
@@ -23,13 +23,17 @@
|
||||
{{> category level=2}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else if groups}}
|
||||
{{/if}}
|
||||
|
||||
{{#if groups}}
|
||||
<div class="groups-container" data-container="groups">
|
||||
{{#each groups}}
|
||||
{{> group}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else if sites}}
|
||||
{{/if}}
|
||||
|
||||
{{#if sites}}
|
||||
<div class="sites-grid" data-container="sites">
|
||||
{{#if sites.length}}
|
||||
{{#each sites}}
|
||||
@@ -39,8 +43,14 @@
|
||||
<p class="empty-sites">暂无网站</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="empty-content">暂无内容</p>
|
||||
{{/if}}
|
||||
|
||||
{{#unless subcategories}}
|
||||
{{#unless groups}}
|
||||
{{#unless sites}}
|
||||
<p class="empty-content">暂无内容</p>
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
data-name="{{name}}"
|
||||
data-url="{{url}}"
|
||||
data-icon="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"
|
||||
data-description="{{#if description}}{{description}}{{else}}{{url}}{{/if}}">
|
||||
data-description="{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}">
|
||||
{{#ifEquals @root.icons.mode "favicon"}}
|
||||
{{#ifHttpUrl url}}
|
||||
<div class="icon-container">
|
||||
@@ -27,6 +27,6 @@
|
||||
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"></i>
|
||||
{{/ifEquals}}
|
||||
<h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3>
|
||||
<p>{{#if description}}{{description}}{{else}}{{url}}{{/if}}</p>
|
||||
<p>{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}</p>
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
Reference in New Issue
Block a user