feat: 优化书签转换逻辑和分类嵌套结构
This commit is contained in:
@@ -1189,6 +1189,26 @@ body .content.expanded {
|
|||||||
width: 100%;
|
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,
|
.category-level-2 .sites-grid,
|
||||||
.group-level-3 .sites-grid,
|
.group-level-3 .sites-grid,
|
||||||
@@ -1412,12 +1432,8 @@ body .content.expanded {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
transition: color 0.3s ease;
|
transition: color 0.3s ease;
|
||||||
display: -webkit-box;
|
white-space: nowrap;
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 2.8em;
|
|
||||||
word-break: break-word;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -1812,7 +1828,9 @@ body .content.expanded {
|
|||||||
.site-card p {
|
.site-card p {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
line-height: 1.3;
|
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 {
|
.site-card p {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
max-height: 2.4em;
|
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1926,9 +1945,10 @@ body .content.expanded {
|
|||||||
|
|
||||||
.site-card p {
|
.site-card p {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
max-height: 2.5em;
|
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,12 +72,8 @@ const ICON_MAPPING = {
|
|||||||
// 获取最新的书签文件
|
// 获取最新的书签文件
|
||||||
function getLatestBookmarkFile() {
|
function getLatestBookmarkFile() {
|
||||||
try {
|
try {
|
||||||
console.log('[DEBUG] 开始查找书签文件...');
|
|
||||||
console.log('[DEBUG] 书签目录:', BOOKMARKS_DIR);
|
|
||||||
|
|
||||||
// 确保书签目录存在
|
// 确保书签目录存在
|
||||||
if (!fs.existsSync(BOOKMARKS_DIR)) {
|
if (!fs.existsSync(BOOKMARKS_DIR)) {
|
||||||
console.log('[DEBUG] 书签目录不存在,创建目录...');
|
|
||||||
fs.mkdirSync(BOOKMARKS_DIR, { recursive: true });
|
fs.mkdirSync(BOOKMARKS_DIR, { recursive: true });
|
||||||
console.log('[WARN] 书签目录为空,未找到HTML文件');
|
console.log('[WARN] 书签目录为空,未找到HTML文件');
|
||||||
return null;
|
return null;
|
||||||
@@ -87,11 +83,6 @@ function getLatestBookmarkFile() {
|
|||||||
const files = fs.readdirSync(BOOKMARKS_DIR)
|
const files = fs.readdirSync(BOOKMARKS_DIR)
|
||||||
.filter(file => file.toLowerCase().endsWith('.html'));
|
.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) {
|
if (files.length === 0) {
|
||||||
console.log('[WARN] 未找到任何HTML书签文件');
|
console.log('[WARN] 未找到任何HTML书签文件');
|
||||||
return null;
|
return null;
|
||||||
@@ -109,7 +100,6 @@ function getLatestBookmarkFile() {
|
|||||||
const latestFilePath = path.join(BOOKMARKS_DIR, latestFile);
|
const latestFilePath = path.join(BOOKMARKS_DIR, latestFile);
|
||||||
|
|
||||||
console.log('[INFO] 选择最新的书签文件:', latestFile);
|
console.log('[INFO] 选择最新的书签文件:', latestFile);
|
||||||
console.log('[DEBUG] 完整路径:', latestFilePath);
|
|
||||||
|
|
||||||
return latestFilePath;
|
return latestFilePath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -120,8 +110,6 @@ function getLatestBookmarkFile() {
|
|||||||
|
|
||||||
// 解析书签HTML内容,支持2-4层级嵌套结构
|
// 解析书签HTML内容,支持2-4层级嵌套结构
|
||||||
function parseBookmarks(htmlContent) {
|
function parseBookmarks(htmlContent) {
|
||||||
console.log('[DEBUG] 开始解析书签HTML内容...');
|
|
||||||
console.log('[DEBUG] HTML内容长度:', htmlContent.length, '字符');
|
|
||||||
|
|
||||||
// 正则表达式匹配文件夹和书签
|
// 正则表达式匹配文件夹和书签
|
||||||
const folderRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/g;
|
const folderRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/g;
|
||||||
@@ -132,79 +120,229 @@ function parseBookmarks(htmlContent) {
|
|||||||
categories: []
|
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) {
|
function parseNestedFolder(htmlContent, parentPath = [], level = 1) {
|
||||||
console.log(`[DEBUG] parseNestedFolder 被调用 - 层级:${level}, 路径:${parentPath.join('/')}, 内容长度:${htmlContent.length}`);
|
|
||||||
|
|
||||||
const folders = [];
|
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) {
|
while ((scanMatch = scanRegex.exec(htmlContent)) !== null) {
|
||||||
matchCount++;
|
const folderName = scanMatch[2].trim();
|
||||||
const folderName = match[2].trim();
|
const folderStart = scanMatch.index;
|
||||||
const folderStart = match.index;
|
const folderHeaderEnd = scanMatch.index + scanMatch[0].length;
|
||||||
const folderEnd = match.index + match[0].length;
|
|
||||||
|
|
||||||
console.log(`[DEBUG] 找到文件夹 #${matchCount}: "${folderName}" (层级${level}) 在位置 ${folderStart}`);
|
// 找到文件夹内容的结束位置
|
||||||
|
let depth = 0;
|
||||||
|
let pos = folderHeaderEnd;
|
||||||
|
|
||||||
// 查找文件夹的结束位置
|
// 跳过空白直到找到 <DL><p>
|
||||||
let folderContentEnd = htmlContent.length;
|
const afterFolder = htmlContent.substring(pos);
|
||||||
let depth = 1;
|
const folderDLMatch = afterFolder.match(/<DL><p>/i);
|
||||||
let pos = folderEnd;
|
if (folderDLMatch) {
|
||||||
let loopCount = 0;
|
depth = 1;
|
||||||
const maxLoops = 10000; // 防止无限循环
|
pos += folderDLMatch.index + folderDLMatch[0].length;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修复:使用 search() 而不是 match(),因为 match() 返回数组没有 index 属性
|
while (pos < htmlContent.length && depth > 0) {
|
||||||
const remainingContent = htmlContent.substring(pos);
|
const remaining = htmlContent.substring(pos);
|
||||||
const dlStartIndex = remainingContent.search(/<DL><p>/i);
|
const dlStartIdx = remaining.search(/<DL><p>/i);
|
||||||
const dlEndIndex = remainingContent.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) {
|
const otherFolder = folderRanges[j];
|
||||||
console.log(`[DEBUG] 循环 ${loopCount}: pos=${pos}, depth=${depth}, dlStart=${dlStartIndex}, dlEnd=${dlEndIndex}`);
|
// 如果当前文件夹的起始位置在另一个文件夹的范围内,说明它是嵌套的
|
||||||
}
|
if (currentFolder.start > otherFolder.start && currentFolder.end <= otherFolder.end) {
|
||||||
|
isNested = true;
|
||||||
// 找到开始标签且在结束标签之前(或没有结束标签)
|
|
||||||
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] 未找到更多标签,退出循环`);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loopCount > 100) {
|
if (isNested) {
|
||||||
console.log(`[DEBUG] 文件夹"${folderName}"边界查找循环${loopCount}次`);
|
continue; // 跳过嵌套的文件夹,它们会被递归调用处理
|
||||||
}
|
}
|
||||||
|
|
||||||
folderContentEnd = pos;
|
const folderName = currentFolder.name;
|
||||||
const folderContent = htmlContent.substring(folderEnd, folderContentEnd);
|
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 = {
|
const folder = {
|
||||||
@@ -217,85 +355,240 @@ function parseBookmarks(htmlContent) {
|
|||||||
const testFolderRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/;
|
const testFolderRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/;
|
||||||
const hasSubfolders = testFolderRegex.test(folderContent);
|
const hasSubfolders = testFolderRegex.test(folderContent);
|
||||||
|
|
||||||
console.log(`[DEBUG] 文件夹"${folderName}"包含子文件夹: ${hasSubfolders}`);
|
// 先解析当前层级的书签
|
||||||
|
const currentLevelSites = parseSitesInFolder(folderContent, folderName);
|
||||||
|
|
||||||
if (hasSubfolders && level < 4) {
|
if (hasSubfolders && level < 4) {
|
||||||
console.log(`[DEBUG] 递归解析文件夹"${folderName}"的子文件夹...`);
|
|
||||||
// 递归解析子文件夹
|
// 递归解析子文件夹
|
||||||
const subfolders = parseNestedFolder(folderContent, folder.path, level + 1);
|
const subfolders = parseNestedFolder(folderContent, folder.path, level + 1);
|
||||||
|
|
||||||
console.log(`[DEBUG] 文件夹"${folderName}"解析到 ${subfolders.length} 个子项`);
|
|
||||||
|
|
||||||
// 根据层级深度决定数据结构
|
// 根据层级深度决定数据结构
|
||||||
if (level === 1) {
|
if (level === 1) {
|
||||||
folder.subcategories = subfolders;
|
folder.subcategories = subfolders;
|
||||||
} else if (level === 2) {
|
} else if (level === 2) {
|
||||||
folder.groups = subfolders;
|
folder.groups = subfolders;
|
||||||
} else if (level === 3) {
|
} else if (level === 3) {
|
||||||
// 层级3直接解析书签
|
folder.subgroups = subfolders;
|
||||||
folder.sites = parseSitesInFolder(folderContent);
|
}
|
||||||
|
|
||||||
|
// 添加当前层级的书签(如果有)
|
||||||
|
if (currentLevelSites.length > 0) {
|
||||||
|
folder.sites = currentLevelSites;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`[DEBUG] 解析文件夹"${folderName}"中的书签...`);
|
|
||||||
// 解析书签
|
// 解析书签
|
||||||
folder.sites = parseSitesInFolder(folderContent);
|
folder.sites = currentLevelSites;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只添加包含内容的文件夹
|
// 只添加包含内容的文件夹
|
||||||
const hasContent = folder.sites && folder.sites.length > 0 ||
|
const hasContent = folder.sites && folder.sites.length > 0 ||
|
||||||
folder.subcategories && folder.subcategories.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) {
|
if (hasContent) {
|
||||||
console.log(`[DEBUG] 添加文件夹"${folderName}" (包含内容)`);
|
|
||||||
folders.push(folder);
|
folders.push(folder);
|
||||||
} else {
|
|
||||||
console.log(`[DEBUG] 跳过空文件夹"${folderName}"`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DEBUG] parseNestedFolder 完成 - 层级:${level}, 返回 ${folders.length} 个文件夹`);
|
|
||||||
return folders;
|
return folders;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析文件夹中的书签
|
// 解析文件夹中的书签(仅当前层级,排除子文件夹内的书签)
|
||||||
function parseSitesInFolder(folderContent) {
|
function parseSitesInFolder(folderContent) {
|
||||||
const sites = [];
|
const sites = [];
|
||||||
let bookmarkMatch;
|
|
||||||
let siteCount = 0;
|
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) {
|
while ((bookmarkMatch = bookmarkRegex.exec(folderContent)) !== null) {
|
||||||
siteCount++;
|
const bookmarkPos = bookmarkMatch.index;
|
||||||
const url = bookmarkMatch[1];
|
const url = bookmarkMatch[1];
|
||||||
const name = bookmarkMatch[2].trim();
|
const name = bookmarkMatch[2].trim();
|
||||||
|
|
||||||
// 基于URL选择适当的图标
|
// 检查这个书签是否在任何子文件夹范围内
|
||||||
let icon = 'fas fa-link'; // 默认图标
|
let inSubfolder = false;
|
||||||
for (const [keyword, iconClass] of Object.entries(ICON_MAPPING)) {
|
for (const folder of subfolderRanges) {
|
||||||
if (url.includes(keyword)) {
|
if (bookmarkPos >= folder.start && bookmarkPos < folder.end) {
|
||||||
icon = iconClass;
|
inSubfolder = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sites.push({
|
if (!inSubfolder) {
|
||||||
name: name,
|
|
||||||
url: url,
|
// 基于URL选择适当的图标
|
||||||
icon: icon,
|
let icon = 'fas fa-link'; // 默认图标
|
||||||
description: ''
|
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;
|
return sites;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始解析
|
// 开始解析
|
||||||
console.log('[DEBUG] 开始递归解析顶层分类...');
|
const rootSites = extractRootBookmarks(htmlContent);
|
||||||
bookmarks.categories = parseNestedFolder(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} 个顶层分类`);
|
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;
|
return bookmarks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,13 +719,13 @@ async function main() {
|
|||||||
console.log('[ERROR] 未找到书签文件,处理终止');
|
console.log('[ERROR] 未找到书签文件,处理终止');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('[SUCCESS] ✓ 找到书签文件\n');
|
console.log('[SUCCESS] 找到书签文件\n');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 读取文件内容
|
// 读取文件内容
|
||||||
console.log('[步骤 2/5] 读取书签文件...');
|
console.log('[步骤 2/5] 读取书签文件...');
|
||||||
const htmlContent = fs.readFileSync(bookmarkFile, 'utf8');
|
const htmlContent = fs.readFileSync(bookmarkFile, 'utf8');
|
||||||
console.log('[SUCCESS] ✓ 文件读取成功,大小:', htmlContent.length, '字符\n');
|
console.log('[SUCCESS] 文件读取成功,大小:', htmlContent.length, '字符\n');
|
||||||
|
|
||||||
// 解析书签
|
// 解析书签
|
||||||
console.log('[步骤 3/5] 解析书签结构...');
|
console.log('[步骤 3/5] 解析书签结构...');
|
||||||
@@ -441,7 +734,7 @@ async function main() {
|
|||||||
console.error('[ERROR] HTML文件中未找到书签分类,处理终止');
|
console.error('[ERROR] HTML文件中未找到书签分类,处理终止');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('[SUCCESS] ✓ 解析完成\n');
|
console.log('[SUCCESS] 解析完成\n');
|
||||||
|
|
||||||
// 生成YAML
|
// 生成YAML
|
||||||
console.log('[步骤 4/5] 生成YAML配置...');
|
console.log('[步骤 4/5] 生成YAML配置...');
|
||||||
@@ -450,20 +743,17 @@ async function main() {
|
|||||||
console.error('[ERROR] YAML生成失败,处理终止');
|
console.error('[ERROR] YAML生成失败,处理终止');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('[DEBUG] YAML内容长度:', yamlContent.length, '字符');
|
console.log('[SUCCESS] YAML生成成功\n');
|
||||||
console.log('[SUCCESS] ✓ YAML生成成功\n');
|
|
||||||
|
|
||||||
// 保存文件
|
// 保存文件
|
||||||
console.log('[步骤 5/5] 保存配置文件...');
|
console.log('[步骤 5/5] 保存配置文件...');
|
||||||
try {
|
try {
|
||||||
// 确保目标目录存在
|
// 确保目标目录存在
|
||||||
if (!fs.existsSync(CONFIG_USER_PAGES_DIR)) {
|
if (!fs.existsSync(CONFIG_USER_PAGES_DIR)) {
|
||||||
console.log('[DEBUG] 创建目录:', CONFIG_USER_PAGES_DIR);
|
|
||||||
fs.mkdirSync(CONFIG_USER_PAGES_DIR, { recursive: true });
|
fs.mkdirSync(CONFIG_USER_PAGES_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存YAML到模块化位置
|
// 保存YAML到模块化位置
|
||||||
console.log('[DEBUG] 写入文件:', MODULAR_OUTPUT_FILE);
|
|
||||||
fs.writeFileSync(MODULAR_OUTPUT_FILE, yamlContent, 'utf8');
|
fs.writeFileSync(MODULAR_OUTPUT_FILE, yamlContent, 'utf8');
|
||||||
|
|
||||||
// 验证文件是否确实被创建
|
// 验证文件是否确实被创建
|
||||||
@@ -472,13 +762,13 @@ async function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[SUCCESS] ✓ 文件保存成功');
|
console.log('[SUCCESS] 文件保存成功');
|
||||||
console.log('[INFO] 输出文件:', MODULAR_OUTPUT_FILE, '\n');
|
console.log('[INFO] 输出文件:', MODULAR_OUTPUT_FILE, '\n');
|
||||||
|
|
||||||
// 更新导航
|
// 更新导航
|
||||||
console.log('[附加步骤] 更新导航配置...');
|
console.log('[附加步骤] 更新导航配置...');
|
||||||
updateNavigationWithBookmarks();
|
updateNavigationWithBookmarks();
|
||||||
console.log('[SUCCESS] ✓ 导航配置已更新\n');
|
console.log('[SUCCESS] 导航配置已更新\n');
|
||||||
|
|
||||||
} catch (writeError) {
|
} catch (writeError) {
|
||||||
console.error(`[ERROR] 写入文件时出错:`, writeError);
|
console.error(`[ERROR] 写入文件时出错:`, writeError);
|
||||||
@@ -487,7 +777,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
console.log('[SUCCESS] ✓✓✓ 书签处理完成!✓✓✓');
|
console.log('[SUCCESS] 书签处理完成!');
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -91,11 +91,37 @@ function json(obj) {
|
|||||||
return JSON.stringify(obj, null, 2);
|
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 = {
|
module.exports = {
|
||||||
formatDate,
|
formatDate,
|
||||||
limit,
|
limit,
|
||||||
toLowerCase,
|
toLowerCase,
|
||||||
toUpperCase,
|
toUpperCase,
|
||||||
json
|
json,
|
||||||
};
|
extractDomain
|
||||||
|
};
|
||||||
@@ -23,13 +23,17 @@
|
|||||||
{{> category level=2}}
|
{{> category level=2}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
{{else if groups}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if groups}}
|
||||||
<div class="groups-container" data-container="groups">
|
<div class="groups-container" data-container="groups">
|
||||||
{{#each groups}}
|
{{#each groups}}
|
||||||
{{> group}}
|
{{> group}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
{{else if sites}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if sites}}
|
||||||
<div class="sites-grid" data-container="sites">
|
<div class="sites-grid" data-container="sites">
|
||||||
{{#if sites.length}}
|
{{#if sites.length}}
|
||||||
{{#each sites}}
|
{{#each sites}}
|
||||||
@@ -39,8 +43,14 @@
|
|||||||
<p class="empty-sites">暂无网站</p>
|
<p class="empty-sites">暂无网站</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
|
||||||
<p class="empty-content">暂无内容</p>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#unless subcategories}}
|
||||||
|
{{#unless groups}}
|
||||||
|
{{#unless sites}}
|
||||||
|
<p class="empty-content">暂无内容</p>
|
||||||
|
{{/unless}}
|
||||||
|
{{/unless}}
|
||||||
|
{{/unless}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
data-name="{{name}}"
|
data-name="{{name}}"
|
||||||
data-url="{{url}}"
|
data-url="{{url}}"
|
||||||
data-icon="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"
|
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"}}
|
{{#ifEquals @root.icons.mode "favicon"}}
|
||||||
{{#ifHttpUrl url}}
|
{{#ifHttpUrl url}}
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
@@ -27,6 +27,6 @@
|
|||||||
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"></i>
|
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"></i>
|
||||||
{{/ifEquals}}
|
{{/ifEquals}}
|
||||||
<h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3>
|
<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>
|
</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
Reference in New Issue
Block a user