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 = /
标签 + 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 = /
+ 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 = /
+ 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 = /
+ 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 = /
标签 + 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}}
暂无网站
{{/if}}暂无内容
{{/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}}{{#if description}}{{description}}{{else}}{{url}}{{/if}}
+{{#if description}}{{description}}{{else}}{{extractDomain url}}{{/if}}
{{/if}}