feat: 优化书签转换逻辑和分类嵌套结构

This commit is contained in:
rbetree
2025-10-31 18:34:44 +08:00
parent dd6e688005
commit 3ae40b23d5
5 changed files with 474 additions and 128 deletions

View File

@@ -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;
}
}

View File

@@ -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 parseNestedFolder(htmlContent, parentPath = [], level = 1) {
console.log(`[DEBUG] parseNestedFolder 被调用 - 层级:${level}, 路径:${parentPath.join('/')}, 内容长度:${htmlContent.length}`);
// 提取根路径书签(书签栏容器内但不在任何子文件夹内的书签)
function extractRootBookmarks(htmlContent) {
// 找到书签栏文件夹标签
const bookmarkBarMatch = htmlContent.match(/<DT><H3[^>]*PERSONAL_TOOLBAR_FOLDER[^>]*>([^<]+)<\/H3>/i);
if (!bookmarkBarMatch) {
return [];
}
const bookmarkBarStart = bookmarkBarMatch.index + bookmarkBarMatch[0].length;
const folders = [];
let match;
let matchCount = 0;
// 找到书签栏后面的 <DL><p> 标签
const remainingAfterBar = htmlContent.substring(bookmarkBarStart);
const dlMatch = remainingAfterBar.match(/<DL><p>/i);
if (!dlMatch) {
return [];
}
// 创建新的正则表达式实例,避免全局正则的 lastIndex 问题
const localFolderRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/g;
const bookmarkBarContentStart = bookmarkBarStart + dlMatch.index + dlMatch[0].length;
while ((match = localFolderRegex.exec(htmlContent)) !== null) {
matchCount++;
const folderName = match[2].trim();
const folderStart = match.index;
const folderEnd = match.index + match[0].length;
console.log(`[DEBUG] 找到文件夹 #${matchCount}: "${folderName}" (层级${level}) 在位置 ${folderStart}`);
// 查找文件夹的结束位置
let folderContentEnd = htmlContent.length;
// 找到书签栏内容的结束位置
let depth = 1;
let pos = folderEnd;
let loopCount = 0;
const maxLoops = 10000; // 防止无限循环
console.log(`[DEBUG] 开始查找文件夹"${folderName}"的边界,起始位置:${pos}`);
let pos = bookmarkBarContentStart;
let bookmarkBarContentEnd = htmlContent.length;
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 remaining = htmlContent.substring(pos);
const dlStartIndex = remaining.search(/<DL><p>/i);
const dlEndIndex = remaining.search(/<\/DL><p>/i);
// 修复:使用 search() 而不是 match(),因为 match() 返回数组没有 index 属性
const remainingContent = htmlContent.substring(pos);
const dlStartIndex = remainingContent.search(/<DL><p>/i);
const dlEndIndex = remainingContent.search(/<\/DL><p>/i);
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) {
} else if (dlEndIndex !== -1) {
depth--;
pos += dlEndIndex + '</DL><p>'.length;
console.log(`[DEBUG] 找到 </DL><p> 在位置 ${pos}, depth减少到 ${depth}`);
pos += dlEndIndex;
if (depth === 0) {
bookmarkBarContentEnd = pos;
}
// 都没找到,退出循环
else {
console.log(`[DEBUG] 未找到更多标签,退出循环`);
pos += '</DL><p>'.length;
} else {
break;
}
}
if (loopCount > 100) {
console.log(`[DEBUG] 文件夹"${folderName}"边界查找循环${loopCount}`);
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;
}
}
folderContentEnd = pos;
const folderContent = htmlContent.substring(folderEnd, folderContentEnd);
subfolderRanges.push({
name: folderName,
start: folderMatch.index,
end: folderContentEnd
});
}
}
console.log(`[DEBUG] 文件夹"${folderName}"内容长度: ${folderContent.length}`);
// 提取不在任何子文件夹范围内的书签
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) {
const folders = [];
// 第一步:扫描所有文件夹,记录它们的完整范围
const folderRanges = [];
const scanRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/g;
let scanMatch;
while ((scanMatch = scanRegex.exec(htmlContent)) !== null) {
const folderName = scanMatch[2].trim();
const folderStart = scanMatch.index;
const folderHeaderEnd = scanMatch.index + scanMatch[0].length;
// 找到文件夹内容的结束位置
let depth = 0;
let pos = folderHeaderEnd;
// 跳过空白直到找到 <DL><p>
const afterFolder = htmlContent.substring(pos);
const folderDLMatch = afterFolder.match(/<DL><p>/i);
if (folderDLMatch) {
depth = 1;
pos += folderDLMatch.index + folderDLMatch[0].length;
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; // 跳过自己
const otherFolder = folderRanges[j];
// 如果当前文件夹的起始位置在另一个文件夹的范围内,说明它是嵌套的
if (currentFolder.start > otherFolder.start && currentFolder.end <= otherFolder.end) {
isNested = true;
break;
}
}
if (isNested) {
continue; // 跳过嵌套的文件夹,它们会被递归调用处理
}
const folderName = currentFolder.name;
const folderStart = currentFolder.start;
const folderHeaderEnd = currentFolder.headerEnd;
const folderEnd = currentFolder.end;
// 提取文件夹内容保留完整的HTML结构供递归使用
// 从headerEnd到end之间包含完整的<DL><p>...</DL><p>结构
const folderContent = htmlContent.substring(folderHeaderEnd, folderEnd);
// 验证是否有有效的容器结构
if (!/<DL><p>/i.test(folderContent)) {
continue;
}
// 解析文件夹内容
const folder = {
@@ -217,59 +355,120 @@ 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();
// 检查这个书签是否在任何子文件夹范围内
let inSubfolder = false;
for (const folder of subfolderRanges) {
if (bookmarkPos >= folder.start && bookmarkPos < folder.end) {
inSubfolder = true;
break;
}
}
if (!inSubfolder) {
// 基于URL选择适当的图标
let icon = 'fas fa-link'; // 默认图标
for (const [keyword, iconClass] of Object.entries(ICON_MAPPING)) {
@@ -286,16 +485,110 @@ function parseBookmarks(htmlContent) {
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) {

View File

@@ -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
};

View File

@@ -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>

View File

@@ -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}}