@knowcode/doc-builder 1.9.31 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/core-builder.js +164 -174
- package/lib/emoji-mapper.js +27 -12
- package/package.json +1 -1
package/lib/core-builder.js
CHANGED
|
@@ -919,10 +919,10 @@ function buildNavigationStructure(files, currentFile, config = {}) {
|
|
|
919
919
|
return title;
|
|
920
920
|
};
|
|
921
921
|
|
|
922
|
-
// Helper function to render a section
|
|
922
|
+
// OPTIMIZATION: Helper function to render a section using array joins instead of string concatenation
|
|
923
923
|
const renderSection = (folderName, folderData, level = 0, parentPath = '') => {
|
|
924
924
|
const icons = {
|
|
925
|
-
'root': 'ph ph-caret-down',
|
|
925
|
+
'root': 'ph ph-caret-down',
|
|
926
926
|
'product-roadmap': 'ph ph-road-horizon',
|
|
927
927
|
'product-requirements': 'ph ph-list-checks',
|
|
928
928
|
'architecture': 'ph ph-tree-structure',
|
|
@@ -947,102 +947,95 @@ function buildNavigationStructure(files, currentFile, config = {}) {
|
|
|
947
947
|
'launch': 'ph ph-rocket-launch',
|
|
948
948
|
'prompts': 'ph ph-chat-circle-dots'
|
|
949
949
|
};
|
|
950
|
-
|
|
951
|
-
const displayName = folderName === 'root' ? 'Documentation' :
|
|
950
|
+
|
|
951
|
+
const displayName = folderName === 'root' ? 'Documentation' :
|
|
952
952
|
smartCapitalize(folderName);
|
|
953
953
|
const icon = icons[folderName] || 'ph ph-folder';
|
|
954
|
-
|
|
954
|
+
|
|
955
955
|
if (!folderData.files.length && !Object.keys(folderData.folders).length) {
|
|
956
956
|
return '';
|
|
957
957
|
}
|
|
958
|
-
|
|
958
|
+
|
|
959
959
|
// Include parent path in section ID to make it unique
|
|
960
960
|
const pathParts = parentPath ? [parentPath, folderName].join('-') : folderName;
|
|
961
961
|
const sectionId = `nav-${pathParts}-${level}`;
|
|
962
962
|
const isCollapsible = level > 0 || folderName !== 'root';
|
|
963
963
|
const collapseIcon = isCollapsible ? '<i class="ph ph-caret-right collapse-icon"></i>' : '';
|
|
964
|
-
|
|
964
|
+
|
|
965
965
|
// Check if this folder has a README.md file to link to
|
|
966
966
|
const readmeFile = folderData.files.find(f => f.displayName === 'README');
|
|
967
|
-
const folderLink = readmeFile ?
|
|
968
|
-
`href="${config.isStaticOutput ? readmeFile.urlPath : '/' + readmeFile.urlPath}"` :
|
|
967
|
+
const folderLink = readmeFile ?
|
|
968
|
+
`href="${config.isStaticOutput ? readmeFile.urlPath : '/' + readmeFile.urlPath}"` :
|
|
969
969
|
'href="#"';
|
|
970
|
-
|
|
970
|
+
|
|
971
971
|
// Get folder description for tooltip
|
|
972
972
|
const folderDescription = folderDescriptions[folderName] || '';
|
|
973
973
|
const tooltipAttr = folderDescription ? `data-tooltip="${escapeHtml(folderDescription)}"` : '';
|
|
974
|
-
|
|
974
|
+
|
|
975
975
|
// Check if this folder has active child or if we're on index page
|
|
976
976
|
const hasActiveChild = checkActiveChild(folderData, currentFile);
|
|
977
|
-
// Expand all folders by default on initial page load (index.html or root README)
|
|
978
977
|
const shouldExpand = hasActiveChild || currentFile === 'index.html' || (currentFile === 'README.html' && level === 1);
|
|
979
|
-
|
|
978
|
+
|
|
980
979
|
// Check if this is a private folder
|
|
981
980
|
const isPrivateFolder = folderName === 'private' || parentPath.includes('private');
|
|
982
981
|
const privateClass = isPrivateFolder ? ' private-nav' : '';
|
|
983
|
-
|
|
984
|
-
//
|
|
982
|
+
|
|
983
|
+
// OPTIMIZATION: Use array instead of string concatenation
|
|
984
|
+
const htmlParts = [];
|
|
985
985
|
const isRoot = folderName === 'root' && level === 0;
|
|
986
|
-
|
|
987
|
-
|
|
986
|
+
|
|
988
987
|
if (isRoot) {
|
|
989
|
-
|
|
990
|
-
html = `
|
|
988
|
+
htmlParts.push(`
|
|
991
989
|
<div class="nav-section${privateClass}" data-level="${level}">
|
|
992
990
|
<a class="nav-title toggle-all-nav expanded" href="#" id="nav-toggle-all" title="Collapse/Expand All">
|
|
993
991
|
<i class="ph ph-caret-down" id="toggle-all-icon"></i> ${displayName}
|
|
994
992
|
</a>
|
|
995
|
-
<div class="nav-content"
|
|
993
|
+
<div class="nav-content">`);
|
|
996
994
|
} else {
|
|
997
|
-
|
|
998
|
-
html = `
|
|
995
|
+
htmlParts.push(`
|
|
999
996
|
<div class="nav-section${privateClass}" data-level="${level}">
|
|
1000
997
|
<a class="nav-title${isCollapsible ? ' collapsible' : ''}${shouldExpand ? ' expanded' : ''}" ${folderLink} ${isCollapsible ? `data-target="${sectionId}"` : ''} ${tooltipAttr}>
|
|
1001
998
|
${collapseIcon}<i class="${icon}"></i> ${displayName}
|
|
1002
999
|
</a>
|
|
1003
|
-
<div class="nav-content${isCollapsible ? (shouldExpand ? '' : ' collapsed') : ''}" ${isCollapsible ? `id="${sectionId}"` : ''}
|
|
1000
|
+
<div class="nav-content${isCollapsible ? (shouldExpand ? '' : ' collapsed') : ''}" ${isCollapsible ? `id="${sectionId}"` : ''}>`);
|
|
1004
1001
|
}
|
|
1005
|
-
|
|
1002
|
+
|
|
1006
1003
|
// Sort and render files
|
|
1007
1004
|
const sortedFiles = [...folderData.files].sort((a, b) => {
|
|
1008
1005
|
if (a.displayName === 'README') return -1;
|
|
1009
1006
|
if (b.displayName === 'README') return 1;
|
|
1010
1007
|
return a.displayName.localeCompare(b.displayName);
|
|
1011
1008
|
});
|
|
1012
|
-
|
|
1009
|
+
|
|
1013
1010
|
sortedFiles.forEach(file => {
|
|
1014
1011
|
const title = generateFileTitle(file, displayName, level);
|
|
1015
|
-
|
|
1016
|
-
// Check if this file is active
|
|
1012
|
+
|
|
1017
1013
|
let isActive = '';
|
|
1018
1014
|
if (currentFile === file.urlPath) {
|
|
1019
1015
|
isActive = ' active';
|
|
1020
1016
|
} else if (currentFile === 'index.html' && file.displayName === 'README' && folderName === 'root') {
|
|
1021
|
-
// Mark root README as active when viewing index.html
|
|
1022
1017
|
isActive = ' active';
|
|
1023
1018
|
}
|
|
1024
|
-
|
|
1025
|
-
// Use relative paths for static output
|
|
1019
|
+
|
|
1026
1020
|
const linkPath = config.isStaticOutput ? file.urlPath : '/' + file.urlPath;
|
|
1027
1021
|
const tooltip = file.summary ? ` data-tooltip="${escapeHtml(file.summary)}"` : '';
|
|
1028
1022
|
const icon = getIconForStatus(file.status || 'default', false, config);
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
<a href="${linkPath}" class="nav-item${isActive}"${tooltip}>${icon} ${title}</a
|
|
1023
|
+
|
|
1024
|
+
htmlParts.push(`
|
|
1025
|
+
<a href="${linkPath}" class="nav-item${isActive}"${tooltip}>${icon} ${title}</a>`);
|
|
1032
1026
|
});
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1027
|
+
|
|
1028
|
+
htmlParts.push(`</div></div>`);
|
|
1029
|
+
|
|
1036
1030
|
// Render subfolders AFTER closing the parent section
|
|
1037
1031
|
Object.keys(folderData.folders)
|
|
1038
1032
|
.sort()
|
|
1039
1033
|
.forEach(subFolder => {
|
|
1040
|
-
// Build the path for the subfolder including current folder
|
|
1041
1034
|
const currentPath = parentPath ? `${parentPath}-${folderName}` : folderName;
|
|
1042
|
-
|
|
1035
|
+
htmlParts.push(renderSection(subFolder, folderData.folders[subFolder], level + 1, currentPath));
|
|
1043
1036
|
});
|
|
1044
|
-
|
|
1045
|
-
return
|
|
1037
|
+
|
|
1038
|
+
return htmlParts.join('');
|
|
1046
1039
|
};
|
|
1047
1040
|
|
|
1048
1041
|
// Check if this is a flat structure
|
|
@@ -1052,58 +1045,57 @@ function buildNavigationStructure(files, currentFile, config = {}) {
|
|
|
1052
1045
|
// Generate simple flat navigation for all files in root
|
|
1053
1046
|
return renderSection('root', { files: tree.files, folders: {} }, 0);
|
|
1054
1047
|
} else {
|
|
1055
|
-
//
|
|
1056
|
-
|
|
1057
|
-
|
|
1048
|
+
// OPTIMIZATION: Use array for hierarchical navigation building
|
|
1049
|
+
const navParts = [];
|
|
1050
|
+
|
|
1058
1051
|
// 1. First render the root Documentation section with Overview
|
|
1059
1052
|
const readmeFile = tree.files.find(f => f.displayName === 'README');
|
|
1060
1053
|
if (readmeFile || tree.files.length > 0) {
|
|
1061
1054
|
const rootFiles = readmeFile ? [readmeFile] : [];
|
|
1062
|
-
|
|
1055
|
+
navParts.push(renderSection('root', { files: rootFiles, folders: {} }, 0));
|
|
1063
1056
|
}
|
|
1064
|
-
|
|
1057
|
+
|
|
1065
1058
|
// 2. Then render all folders alphabetically
|
|
1066
1059
|
Object.keys(tree.folders)
|
|
1067
1060
|
.sort()
|
|
1068
1061
|
.forEach(folderName => {
|
|
1069
|
-
|
|
1062
|
+
navParts.push(renderSection(folderName, tree.folders[folderName], 1));
|
|
1070
1063
|
});
|
|
1071
|
-
|
|
1064
|
+
|
|
1072
1065
|
// 3. Finally, add remaining root files to the Documentation section
|
|
1073
1066
|
const otherRootFiles = tree.files.filter(f => f.displayName !== 'README');
|
|
1074
1067
|
if (otherRootFiles.length > 0) {
|
|
1068
|
+
const nav = navParts.join('');
|
|
1075
1069
|
// Find the closing </div></div> of the first nav-section (Documentation)
|
|
1076
1070
|
const navSections = nav.split('<div class="nav-section"');
|
|
1077
|
-
|
|
1071
|
+
|
|
1078
1072
|
if (navSections.length > 1) {
|
|
1079
|
-
// Find the end of the first section's content div
|
|
1080
1073
|
const firstSection = navSections[1];
|
|
1081
1074
|
const contentDivEnd = firstSection.indexOf('</div></div>');
|
|
1082
|
-
|
|
1075
|
+
|
|
1083
1076
|
if (contentDivEnd !== -1) {
|
|
1084
|
-
|
|
1077
|
+
const additionalFilesParts = [];
|
|
1085
1078
|
otherRootFiles.forEach(file => {
|
|
1086
1079
|
const title = smartCapitalize(file.displayName);
|
|
1087
1080
|
let isActive = '';
|
|
1088
1081
|
if (currentFile === file.urlPath) {
|
|
1089
1082
|
isActive = ' active';
|
|
1090
1083
|
}
|
|
1091
|
-
// Use relative paths for static output
|
|
1092
1084
|
const linkPath = config.isStaticOutput ? file.urlPath : '/' + file.urlPath;
|
|
1093
1085
|
const tooltip = file.summary ? ` data-tooltip="${escapeHtml(file.summary)}"` : '';
|
|
1094
1086
|
const icon = getIconForStatus(file.status || 'default', false, config);
|
|
1095
|
-
|
|
1096
|
-
<a href="${linkPath}" class="nav-item${isActive}"${tooltip}>${icon} ${title}</a
|
|
1087
|
+
additionalFilesParts.push(`
|
|
1088
|
+
<a href="${linkPath}" class="nav-item${isActive}"${tooltip}>${icon} ${title}</a>`);
|
|
1097
1089
|
});
|
|
1098
|
-
|
|
1090
|
+
|
|
1099
1091
|
// Reconstruct with additional files inserted
|
|
1100
|
-
navSections[1] = firstSection.slice(0, contentDivEnd) +
|
|
1101
|
-
|
|
1092
|
+
navSections[1] = firstSection.slice(0, contentDivEnd) + additionalFilesParts.join('') + firstSection.slice(contentDivEnd);
|
|
1093
|
+
return navSections.join('<div class="nav-section"');
|
|
1102
1094
|
}
|
|
1103
1095
|
}
|
|
1104
1096
|
}
|
|
1105
|
-
|
|
1106
|
-
return
|
|
1097
|
+
|
|
1098
|
+
return navParts.join('');
|
|
1107
1099
|
}
|
|
1108
1100
|
}
|
|
1109
1101
|
|
|
@@ -1119,20 +1111,32 @@ async function processMarkdownFile(filePath, outputPath, allFiles, config, useSt
|
|
|
1119
1111
|
.split('/')
|
|
1120
1112
|
.map(segment => encodeURIComponent(segment))
|
|
1121
1113
|
.join('/');
|
|
1122
|
-
|
|
1114
|
+
|
|
1123
1115
|
// Parse front matter
|
|
1124
1116
|
const { data: frontMatter, content } = matter(rawContent);
|
|
1125
|
-
|
|
1117
|
+
|
|
1126
1118
|
// Extract title - priority: front matter > H1 > filename
|
|
1127
1119
|
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
1128
1120
|
const h1Title = h1Match ? h1Match[1] : null;
|
|
1129
|
-
|
|
1121
|
+
|
|
1130
1122
|
// Normalize title if needed (e.g., convert all-caps to title case)
|
|
1131
1123
|
const rawTitle = frontMatter.title || h1Title || fileName;
|
|
1132
1124
|
const title = config.features?.normalizeTitle !== false ? normalizeTitle(rawTitle) : rawTitle;
|
|
1133
|
-
|
|
1125
|
+
|
|
1134
1126
|
// Extract summary for tooltip - priority: front matter > auto-extract
|
|
1135
1127
|
const summary = frontMatter.description || extractSummary(content);
|
|
1128
|
+
|
|
1129
|
+
// OPTIMIZATION: Update the file entry in allFiles with computed summary and status
|
|
1130
|
+
// This allows navigation tooltips to work even though we defer content loading
|
|
1131
|
+
const fileEntry = allFiles.find(f => f.path === filePath);
|
|
1132
|
+
if (fileEntry) {
|
|
1133
|
+
if (!fileEntry.summary) {
|
|
1134
|
+
fileEntry.summary = summary;
|
|
1135
|
+
}
|
|
1136
|
+
if (!fileEntry.status) {
|
|
1137
|
+
fileEntry.status = detectDocumentStatus(content, frontMatter);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1136
1140
|
|
|
1137
1141
|
// Process content
|
|
1138
1142
|
const htmlContent = processMarkdownContent(content, config);
|
|
@@ -1154,32 +1158,35 @@ async function processMarkdownFile(filePath, outputPath, allFiles, config, useSt
|
|
|
1154
1158
|
return { title, urlPath, summary, frontMatter };
|
|
1155
1159
|
}
|
|
1156
1160
|
|
|
1157
|
-
//
|
|
1158
|
-
async function
|
|
1159
|
-
const
|
|
1161
|
+
// OPTIMIZATION: Unified directory scanner - scans once for both markdown and attachments
|
|
1162
|
+
async function scanAllFiles(dir, baseDir = dir, options = {}) {
|
|
1163
|
+
const markdownFiles = [];
|
|
1164
|
+
const attachmentFiles = [];
|
|
1165
|
+
const attachmentTypes = options.attachmentTypes || [];
|
|
1160
1166
|
const items = await fs.readdir(dir);
|
|
1161
|
-
|
|
1167
|
+
|
|
1162
1168
|
for (const item of items) {
|
|
1163
1169
|
// Skip files with non-printable characters in their names
|
|
1164
1170
|
if (hasNonPrintableChars(item)) {
|
|
1165
1171
|
console.log(chalk.yellow(`⚠️ Skipping file with non-printable characters: ${sanitizeFilename(item)}`));
|
|
1166
1172
|
continue;
|
|
1167
1173
|
}
|
|
1168
|
-
|
|
1174
|
+
|
|
1169
1175
|
const fullPath = path.join(dir, item);
|
|
1170
1176
|
const stat = await fs.stat(fullPath);
|
|
1171
|
-
|
|
1177
|
+
|
|
1172
1178
|
// Skip private directories if excludePrivate is true
|
|
1173
1179
|
if (stat.isDirectory() && options.excludePrivate && item === 'private') {
|
|
1174
1180
|
continue;
|
|
1175
1181
|
}
|
|
1176
|
-
|
|
1182
|
+
|
|
1177
1183
|
if (stat.isDirectory() && !item.startsWith('.') && !item.startsWith('_')) {
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1184
|
+
const subResult = await scanAllFiles(fullPath, baseDir, options);
|
|
1185
|
+
markdownFiles.push(...subResult.markdownFiles);
|
|
1186
|
+
attachmentFiles.push(...subResult.attachmentFiles);
|
|
1180
1187
|
} else if (item.endsWith('.md') && !item.startsWith('_')) {
|
|
1188
|
+
// Process markdown files
|
|
1181
1189
|
const relativePath = path.relative(baseDir, fullPath);
|
|
1182
|
-
// Encode special characters in URL but keep slashes
|
|
1183
1190
|
const urlPath = relativePath
|
|
1184
1191
|
.replace(/\.md$/, '.html')
|
|
1185
1192
|
.replace(/\\/g, '/')
|
|
@@ -1187,25 +1194,25 @@ async function getAllMarkdownFiles(dir, baseDir = dir, options = {}) {
|
|
|
1187
1194
|
.map(segment => encodeURIComponent(segment))
|
|
1188
1195
|
.join('/');
|
|
1189
1196
|
const displayName = smartCapitalize(path.basename(item, '.md'));
|
|
1190
|
-
|
|
1191
|
-
//
|
|
1197
|
+
|
|
1198
|
+
// OPTIMIZATION: Only read frontmatter, not full content
|
|
1199
|
+
// Read file but only parse frontmatter, defer content processing
|
|
1192
1200
|
const rawContent = await fs.readFile(fullPath, 'utf-8');
|
|
1193
|
-
const { data: frontMatter
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
const isPrivate = relativePath.split(path.sep)[0] === 'private' ||
|
|
1200
|
-
relativePath.startsWith('private/') ||
|
|
1201
|
+
const { data: frontMatter } = matter(rawContent, { excerpt: false });
|
|
1202
|
+
|
|
1203
|
+
// Use frontmatter description if available, otherwise defer summary extraction
|
|
1204
|
+
const summary = frontMatter.description || '';
|
|
1205
|
+
const status = frontMatter.status || null;
|
|
1206
|
+
|
|
1207
|
+
const isPrivate = relativePath.split(path.sep)[0] === 'private' ||
|
|
1208
|
+
relativePath.startsWith('private/') ||
|
|
1201
1209
|
relativePath.startsWith('private\\');
|
|
1202
|
-
|
|
1203
|
-
// Skip private files if excludePrivate is true
|
|
1210
|
+
|
|
1204
1211
|
if (options.excludePrivate && isPrivate) {
|
|
1205
1212
|
continue;
|
|
1206
1213
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1214
|
+
|
|
1215
|
+
markdownFiles.push({
|
|
1209
1216
|
path: fullPath,
|
|
1210
1217
|
relativePath,
|
|
1211
1218
|
urlPath,
|
|
@@ -1214,39 +1221,21 @@ async function getAllMarkdownFiles(dir, baseDir = dir, options = {}) {
|
|
|
1214
1221
|
isPrivate,
|
|
1215
1222
|
status,
|
|
1216
1223
|
frontMatter,
|
|
1217
|
-
content
|
|
1218
1224
|
});
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
return files;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
// Get all attachment files
|
|
1226
|
-
async function getAllAttachmentFiles(dir, baseDir = dir, attachmentTypes) {
|
|
1227
|
-
const files = [];
|
|
1228
|
-
const items = await fs.readdir(dir);
|
|
1229
|
-
|
|
1230
|
-
for (const item of items) {
|
|
1231
|
-
// Skip files with non-printable characters in their names
|
|
1232
|
-
if (hasNonPrintableChars(item)) {
|
|
1233
|
-
console.log(chalk.yellow(`⚠️ Skipping attachment with non-printable characters: ${sanitizeFilename(item)}`));
|
|
1234
|
-
continue;
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
const fullPath = path.join(dir, item);
|
|
1238
|
-
const stat = await fs.stat(fullPath);
|
|
1239
|
-
|
|
1240
|
-
if (stat.isDirectory() && !item.startsWith('.')) {
|
|
1241
|
-
// Recursively scan subdirectories
|
|
1242
|
-
const subFiles = await getAllAttachmentFiles(fullPath, baseDir, attachmentTypes);
|
|
1243
|
-
files.push(...subFiles);
|
|
1244
|
-
} else {
|
|
1245
|
-
// Check if file is an attachment type
|
|
1225
|
+
} else if (attachmentTypes.length > 0) {
|
|
1226
|
+
// Process attachment files
|
|
1246
1227
|
const ext = path.extname(item).toLowerCase();
|
|
1247
1228
|
if (attachmentTypes.includes(ext) && !item.startsWith('.')) {
|
|
1248
1229
|
const relativePath = path.relative(baseDir, fullPath);
|
|
1249
|
-
|
|
1230
|
+
const isPrivate = relativePath.split(path.sep)[0] === 'private' ||
|
|
1231
|
+
relativePath.startsWith('private/') ||
|
|
1232
|
+
relativePath.startsWith('private\\');
|
|
1233
|
+
|
|
1234
|
+
if (options.excludePrivate && isPrivate) {
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
attachmentFiles.push({
|
|
1250
1239
|
path: fullPath,
|
|
1251
1240
|
relativePath,
|
|
1252
1241
|
size: stat.size
|
|
@@ -1254,8 +1243,20 @@ async function getAllAttachmentFiles(dir, baseDir = dir, attachmentTypes) {
|
|
|
1254
1243
|
}
|
|
1255
1244
|
}
|
|
1256
1245
|
}
|
|
1257
|
-
|
|
1258
|
-
return
|
|
1246
|
+
|
|
1247
|
+
return { markdownFiles, attachmentFiles };
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Get all markdown files (backward compatibility wrapper)
|
|
1251
|
+
async function getAllMarkdownFiles(dir, baseDir = dir, options = {}) {
|
|
1252
|
+
const result = await scanAllFiles(dir, baseDir, options);
|
|
1253
|
+
return result.markdownFiles;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Get all attachment files (backward compatibility wrapper)
|
|
1257
|
+
async function getAllAttachmentFiles(dir, baseDir = dir, attachmentTypes) {
|
|
1258
|
+
const result = await scanAllFiles(dir, baseDir, { attachmentTypes });
|
|
1259
|
+
return result.attachmentFiles;
|
|
1259
1260
|
}
|
|
1260
1261
|
|
|
1261
1262
|
// Copy attachment files to output directory
|
|
@@ -1300,10 +1301,26 @@ async function buildDocumentation(config) {
|
|
|
1300
1301
|
console.log(chalk.blue('📋 Checking documentation structure...'));
|
|
1301
1302
|
const readmeGenerated = await createPlaceholderReadme(docsDir, config);
|
|
1302
1303
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1304
|
+
// OPTIMIZATION: Single unified scan for both markdown and attachments
|
|
1305
|
+
console.log(chalk.blue('📄 Scanning documentation directory...'));
|
|
1306
|
+
const attachmentTypes = config.attachmentTypes || [
|
|
1307
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.csv', '.ppt', '.pptx', '.txt', '.rtf',
|
|
1308
|
+
'.html', '.htm',
|
|
1309
|
+
'.zip', '.tar', '.gz', '.7z', '.rar',
|
|
1310
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.bmp',
|
|
1311
|
+
'.json', '.xml', '.yaml', '.yml', '.toml',
|
|
1312
|
+
'.mp4', '.mp3', '.wav', '.avi', '.mov'
|
|
1313
|
+
];
|
|
1314
|
+
|
|
1315
|
+
const { markdownFiles: files, attachmentFiles } = await scanAllFiles(docsDir, docsDir, {
|
|
1316
|
+
attachmentTypes: config.features?.attachments !== false ? attachmentTypes : []
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1305
1319
|
console.log(chalk.green(`✅ Found ${files.length} markdown files${readmeGenerated ? ' (including auto-generated README)' : ''}`));
|
|
1306
|
-
|
|
1320
|
+
if (config.features?.attachments !== false && attachmentFiles.length > 0) {
|
|
1321
|
+
console.log(chalk.gray(` Found ${attachmentFiles.length} attachments`));
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1307
1324
|
// Log the files found
|
|
1308
1325
|
if (files.length > 0) {
|
|
1309
1326
|
console.log(chalk.gray(' Found files:'));
|
|
@@ -1482,52 +1499,39 @@ async function buildDocumentation(config) {
|
|
|
1482
1499
|
}
|
|
1483
1500
|
}
|
|
1484
1501
|
|
|
1485
|
-
//
|
|
1486
|
-
if (config.features?.attachments !== false) {
|
|
1502
|
+
// OPTIMIZATION: Use cached attachment files from initial scan
|
|
1503
|
+
if (config.features?.attachments !== false && attachmentFiles.length > 0) {
|
|
1487
1504
|
console.log(chalk.blue('\n📎 Processing attachments...'));
|
|
1488
|
-
|
|
1489
|
-
const attachmentTypes = config.attachmentTypes || [
|
|
1490
|
-
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.csv', '.ppt', '.pptx', '.txt', '.rtf',
|
|
1491
|
-
'.html', '.htm',
|
|
1492
|
-
'.zip', '.tar', '.gz', '.7z', '.rar',
|
|
1493
|
-
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.bmp',
|
|
1494
|
-
'.json', '.xml', '.yaml', '.yml', '.toml',
|
|
1495
|
-
'.mp4', '.mp3', '.wav', '.avi', '.mov'
|
|
1496
|
-
];
|
|
1497
|
-
|
|
1505
|
+
|
|
1498
1506
|
try {
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
};
|
|
1510
|
-
|
|
1511
|
-
console.log(chalk.green(`✅ Copied ${copiedCount} attachments (${formatSize(totalSize)} total)`));
|
|
1512
|
-
} else {
|
|
1513
|
-
console.log(chalk.gray(' No attachments found to copy'));
|
|
1514
|
-
}
|
|
1507
|
+
const { copiedCount, totalSize } = await copyAttachmentFiles(attachmentFiles, docsDir, outputDir);
|
|
1508
|
+
|
|
1509
|
+
// Format file size
|
|
1510
|
+
const formatSize = (bytes) => {
|
|
1511
|
+
if (bytes < 1024) return bytes + ' B';
|
|
1512
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
1513
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
console.log(chalk.green(`✅ Copied ${copiedCount} attachments (${formatSize(totalSize)} total)`));
|
|
1515
1517
|
} catch (error) {
|
|
1516
1518
|
console.warn(chalk.yellow(`Warning: Error processing attachments: ${error.message}`));
|
|
1517
1519
|
}
|
|
1520
|
+
} else if (config.features?.attachments !== false) {
|
|
1521
|
+
console.log(chalk.gray(' No attachments found to copy'));
|
|
1518
1522
|
}
|
|
1519
1523
|
|
|
1520
1524
|
// Generate static version if enabled
|
|
1521
1525
|
if (config.features?.staticOutput !== false) {
|
|
1522
1526
|
console.log(chalk.blue('\n🌐 Generating static version (no auth, no private content)...'));
|
|
1523
|
-
|
|
1527
|
+
|
|
1524
1528
|
const staticOutputDir = path.join(process.cwd(), config.staticOutputDir || 'html-static');
|
|
1525
|
-
|
|
1529
|
+
|
|
1526
1530
|
// Ensure static output directory exists
|
|
1527
1531
|
await fs.ensureDir(staticOutputDir);
|
|
1528
|
-
|
|
1529
|
-
//
|
|
1530
|
-
const staticFiles =
|
|
1532
|
+
|
|
1533
|
+
// OPTIMIZATION: Filter already-scanned files instead of re-scanning
|
|
1534
|
+
const staticFiles = files.filter(f => !f.isPrivate);
|
|
1531
1535
|
console.log(chalk.gray(` Found ${staticFiles.length} public files (private files excluded)`));
|
|
1532
1536
|
|
|
1533
1537
|
// Process files for static output
|
|
@@ -1568,27 +1572,13 @@ async function buildDocumentation(config) {
|
|
|
1568
1572
|
await fs.writeFile(staticIndexPath, defaultIndex);
|
|
1569
1573
|
}
|
|
1570
1574
|
|
|
1571
|
-
//
|
|
1572
|
-
if (config.features?.attachments !== false) {
|
|
1573
|
-
const attachmentTypes = config.attachmentTypes || [
|
|
1574
|
-
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.csv', '.ppt', '.pptx', '.txt', '.rtf',
|
|
1575
|
-
'.html', '.htm',
|
|
1576
|
-
'.zip', '.tar', '.gz', '.7z', '.rar',
|
|
1577
|
-
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.bmp',
|
|
1578
|
-
'.json', '.xml', '.yaml', '.yml', '.toml',
|
|
1579
|
-
'.mp4', '.mp3', '.wav', '.avi', '.mov'
|
|
1580
|
-
];
|
|
1581
|
-
|
|
1575
|
+
// OPTIMIZATION: Filter already-scanned attachments instead of re-scanning
|
|
1576
|
+
if (config.features?.attachments !== false && attachmentFiles.length > 0) {
|
|
1582
1577
|
try {
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
if (!file.relativePath.startsWith('private/') && !file.relativePath.startsWith('private\\')) {
|
|
1588
|
-
staticAttachmentFiles.push(file);
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1578
|
+
const staticAttachmentFiles = attachmentFiles.filter(file =>
|
|
1579
|
+
!file.relativePath.startsWith('private/') && !file.relativePath.startsWith('private\\')
|
|
1580
|
+
);
|
|
1581
|
+
|
|
1592
1582
|
if (staticAttachmentFiles.length > 0) {
|
|
1593
1583
|
await copyAttachmentFiles(staticAttachmentFiles, docsDir, staticOutputDir);
|
|
1594
1584
|
}
|
package/lib/emoji-mapper.js
CHANGED
|
@@ -280,6 +280,28 @@ const emojiToPhosphor = {
|
|
|
280
280
|
'🐌': '<i class="ph ph-spiral" aria-label="snail"></i>',
|
|
281
281
|
};
|
|
282
282
|
|
|
283
|
+
// OPTIMIZATION: Cache the compiled regex pattern
|
|
284
|
+
let cachedEmojiPattern = null;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get or build the cached emoji pattern
|
|
288
|
+
* @returns {RegExp} - Compiled emoji matching regex
|
|
289
|
+
*/
|
|
290
|
+
function getEmojiPattern() {
|
|
291
|
+
if (!cachedEmojiPattern) {
|
|
292
|
+
// Build regex pattern once with all emoji patterns
|
|
293
|
+
// Sort by length to match longer emojis first (e.g., ⚠️ before ⚠)
|
|
294
|
+
cachedEmojiPattern = new RegExp(
|
|
295
|
+
Object.keys(emojiToPhosphor)
|
|
296
|
+
.sort((a, b) => b.length - a.length)
|
|
297
|
+
.map(emoji => emoji.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
298
|
+
.join('|'),
|
|
299
|
+
'g'
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return cachedEmojiPattern;
|
|
303
|
+
}
|
|
304
|
+
|
|
283
305
|
/**
|
|
284
306
|
* Replace emojis with Phosphor icons in HTML
|
|
285
307
|
* @param {string} html - The HTML content to process
|
|
@@ -289,27 +311,20 @@ const emojiToPhosphor = {
|
|
|
289
311
|
function replaceEmojisWithIcons(html, config = {}) {
|
|
290
312
|
// Check if feature is enabled - default to true if not explicitly set to false
|
|
291
313
|
if (config.features?.phosphorIcons === false) return html;
|
|
292
|
-
|
|
314
|
+
|
|
293
315
|
// First, protect code blocks and inline code from emoji replacement
|
|
294
316
|
const codeBlocks = [];
|
|
295
317
|
const codePattern = /(<code[^>]*>[\s\S]*?<\/code>|<pre[^>]*>[\s\S]*?<\/pre>)/g;
|
|
296
|
-
|
|
318
|
+
|
|
297
319
|
// Replace code blocks with placeholders
|
|
298
320
|
html = html.replace(codePattern, (match, p1) => {
|
|
299
321
|
const placeholder = `CODE_BLOCK_${codeBlocks.length}`;
|
|
300
322
|
codeBlocks.push(match);
|
|
301
323
|
return placeholder;
|
|
302
324
|
});
|
|
303
|
-
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
const emojiPattern = new RegExp(
|
|
307
|
-
Object.keys(emojiToPhosphor)
|
|
308
|
-
.sort((a, b) => b.length - a.length)
|
|
309
|
-
.map(emoji => emoji.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
310
|
-
.join('|'),
|
|
311
|
-
'g'
|
|
312
|
-
);
|
|
325
|
+
|
|
326
|
+
// OPTIMIZATION: Use cached emoji pattern
|
|
327
|
+
const emojiPattern = getEmojiPattern();
|
|
313
328
|
|
|
314
329
|
// Get weight class if custom weight is specified
|
|
315
330
|
const weightClass = config.features?.phosphorWeight && config.features.phosphorWeight !== 'regular'
|