From 3ae40b23d5d928fa9c973f9b8ec06613bac9865f Mon Sep 17 00:00:00 2001 From: rbetree Date: Fri, 31 Oct 2025 18:34:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=B9=A6=E7=AD=BE?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E9=80=BB=E8=BE=91=E5=92=8C=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E5=B5=8C=E5=A5=97=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/style.css | 40 ++- src/bookmark-processor.js | 510 ++++++++++++++++++++++------- src/helpers/formatters.js | 30 +- templates/components/category.hbs | 18 +- templates/components/site-card.hbs | 4 +- 5 files changed, 474 insertions(+), 128 deletions(-) diff --git a/assets/style.css b/assets/style.css index ff00cda..caf38e5 100644 --- a/assets/style.css +++ b/assets/style.css @@ -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; } } diff --git a/src/bookmark-processor.js b/src/bookmark-processor.js index 944d2c8..ba1b368 100644 --- a/src/bookmark-processor.js +++ b/src/bookmark-processor.js @@ -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 = /
]*)>(.*?)<\/H3>/g; @@ -132,79 +120,229 @@ function parseBookmarks(htmlContent) { categories: [] }; + // 提取根路径书签(书签栏容器内但不在任何子文件夹内的书签) + function extractRootBookmarks(htmlContent) { + // 找到书签栏文件夹标签 + const bookmarkBarMatch = htmlContent.match(/
]*PERSONAL_TOOLBAR_FOLDER[^>]*>([^<]+)<\/H3>/i); + if (!bookmarkBarMatch) { + return []; + } + const bookmarkBarStart = bookmarkBarMatch.index + bookmarkBarMatch[0].length; + + // 找到书签栏后面的

标签 + const remainingAfterBar = htmlContent.substring(bookmarkBarStart); + const dlMatch = remainingAfterBar.match(/

/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(/

/i); + const dlEndIndex = remaining.search(/<\/DL>

/i); + + if (dlStartIndex !== -1 && (dlEndIndex === -1 || dlStartIndex < dlEndIndex)) { + depth++; + pos += dlStartIndex + '

'.length; + } else if (dlEndIndex !== -1) { + depth--; + pos += dlEndIndex; + if (depth === 0) { + bookmarkBarContentEnd = pos; + } + pos += '

'.length; + } else { + break; + } + } + + const bookmarkBarContent = htmlContent.substring(bookmarkBarContentStart, bookmarkBarContentEnd); + + // 现在提取书签栏内所有子文件夹的范围 + const subfolderRanges = []; + const folderRegex = /

]*>([^<]+)<\/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; + + // 跳过空白直到找到

+ const afterFolder = bookmarkBarContent.substring(folderPos); + const folderDLMatch = afterFolder.match(/

/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(/

/i); + const dlEndIdx = remaining.search(/<\/DL>

/i); + + if (dlStartIdx !== -1 && (dlEndIdx === -1 || dlStartIdx < dlEndIdx)) { + folderDepth++; + folderPos += dlStartIdx + '

'.length; + } else if (dlEndIdx !== -1) { + folderDepth--; + folderPos += dlEndIdx; + if (folderDepth === 0) { + folderContentEnd = folderPos + '

'.length; + } + folderPos += '

'.length; + } else { + break; + } + } + + subfolderRanges.push({ + name: folderName, + start: folderMatch.index, + end: folderContentEnd + }); + } + } + + // 提取不在任何子文件夹范围内的书签 + const rootSites = []; + const bookmarkRegex = /

]*>(.*?)<\/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 = /
]*)>(.*?)<\/H3>/g; + // 第一步:扫描所有文件夹,记录它们的完整范围 + const folderRanges = []; + const scanRegex = /
]*)>(.*?)<\/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; - } + // 跳过空白直到找到

+ const afterFolder = htmlContent.substring(pos); + const folderDLMatch = afterFolder.match(/

/i); + if (folderDLMatch) { + depth = 1; + pos += folderDLMatch.index + folderDLMatch[0].length; - // 修复:使用 search() 而不是 match(),因为 match() 返回数组没有 index 属性 - const remainingContent = htmlContent.substring(pos); - const dlStartIndex = remainingContent.search(/

/i); - const dlEndIndex = remainingContent.search(/<\/DL>

/i); + while (pos < htmlContent.length && depth > 0) { + const remaining = htmlContent.substring(pos); + const dlStartIdx = remaining.search(/

/i); + const dlEndIdx = remaining.search(/<\/DL>

/i); + + if (dlStartIdx !== -1 && (dlEndIdx === -1 || dlStartIdx < dlEndIdx)) { + depth++; + pos += dlStartIdx + '

'.length; + } else if (dlEndIdx !== -1) { + depth--; + pos += dlEndIdx; + if (depth === 0) { + const folderEnd = pos + '

'.length; + folderRanges.push({ + name: folderName, + start: folderStart, + headerEnd: folderHeaderEnd, + end: folderEnd + }); + } + pos += '

'.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 + '

'.length; - console.log(`[DEBUG] 找到

在位置 ${pos}, depth增加到 ${depth}`); - } - // 找到结束标签 - else if (dlEndIndex !== -1) { - depth--; - pos += dlEndIndex + '

'.length; - console.log(`[DEBUG] 找到

在位置 ${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之间包含完整的

...

结构 + const folderContent = htmlContent.substring(folderHeaderEnd, folderEnd); + + // 验证是否有有效的容器结构 + if (!/

/i.test(folderContent)) { + continue; + } // 解析文件夹内容 const folder = { @@ -217,85 +355,240 @@ function parseBookmarks(htmlContent) { const testFolderRegex = /

]*)>(.*?)<\/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 = /
]*>([^<]+)<\/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; + + // 跳过空白直到找到

+ const afterFolder = folderContent.substring(folderPos); + const folderDLMatch = afterFolder.match(/

/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(/

/i); + const dlEndIdx = remaining.search(/<\/DL>

/i); + + if (dlStartIdx !== -1 && (dlEndIdx === -1 || dlStartIdx < dlEndIdx)) { + folderDepth++; + folderPos += dlStartIdx + '

'.length; + } else if (dlEndIdx !== -1) { + folderDepth--; + folderPos += dlEndIdx; + if (folderDepth === 0) { + folderContentEnd = folderPos + '

'.length; + } + folderPos += '

'.length; + } else { + break; + } + } + + subfolderRanges.push({ + name: folderName, + start: folderStart, + end: folderContentEnd + }); + } + } + + // 现在提取不在任何子文件夹范围内的书签 + const bookmarkRegex = /

]*>(.*?)<\/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(/
]*PERSONAL_TOOLBAR_FOLDER[^>]*>([^<]+)<\/H3>/i); + if (!bookmarkBarMatch) { + console.log('[WARN] 未找到书签栏文件夹(PERSONAL_TOOLBAR_FOLDER),使用备用方案'); + // 备用方案:使用第一个

标签 + const firstDLMatch = htmlContent.match(/

/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(/

/i); + const dlEndIndex = remainingContent.search(/<\/DL>

/i); + + if (dlStartIndex !== -1 && (dlEndIndex === -1 || dlStartIndex < dlEndIndex)) { + depth++; + pos += dlStartIndex + '

'.length; + } else if (dlEndIndex !== -1) { + depth--; + pos += dlEndIndex + '

'.length; + } else { + break; + } + } + + dlEnd = pos - '

'.length; + const bookmarksBarContent = htmlContent.substring(dlStart, dlEnd); + bookmarks.categories = parseNestedFolder(bookmarksBarContent); + } + } else { + const bookmarkBarStart = bookmarkBarMatch.index + bookmarkBarMatch[0].length; + + // 找到书签栏后面的

标签 + const remainingAfterBar = htmlContent.substring(bookmarkBarStart); + const dlMatch = remainingAfterBar.match(/

/i); + if (!dlMatch) { + console.log('[ERROR] 未找到书签栏的内容容器

'); + 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(/

/i); + const dlEndIndex = remaining.search(/<\/DL>

/i); + + if (dlStartIndex !== -1 && (dlEndIndex === -1 || dlStartIndex < dlEndIndex)) { + depth++; + pos += dlStartIndex + '

'.length; + } else if (dlEndIndex !== -1) { + depth--; + pos += dlEndIndex; + if (depth === 0) { + bookmarkBarContentEnd = pos; + } + pos += '

'.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) { diff --git a/src/helpers/formatters.js b/src/helpers/formatters.js index a6ed097..9a760e7 100644 --- a/src/helpers/formatters.js +++ b/src/helpers/formatters.js @@ -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 -}; \ No newline at end of file + json, + extractDomain +}; \ No newline at end of file diff --git a/templates/components/category.hbs b/templates/components/category.hbs index 21bb6b9..36ff9ba 100644 --- a/templates/components/category.hbs +++ b/templates/components/category.hbs @@ -23,13 +23,17 @@ {{> category level=2}} {{/each}} - {{else if groups}} + {{/if}} + + {{#if groups}}

{{#each groups}} {{> group}} {{/each}}
- {{else if sites}} + {{/if}} + + {{#if sites}}
{{#if sites.length}} {{#each sites}} @@ -39,8 +43,14 @@

暂无网站

{{/if}}
- {{else}} -

暂无内容

{{/if}} + + {{#unless subcategories}} + {{#unless groups}} + {{#unless sites}} +

暂无内容

+ {{/unless}} + {{/unless}} + {{/unless}} diff --git a/templates/components/site-card.hbs b/templates/components/site-card.hbs index 74f760b..ab9fa91 100644 --- a/templates/components/site-card.hbs +++ b/templates/components/site-card.hbs @@ -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}}