@kenjura/ursa 0.53.0 → 0.55.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/CHANGELOG.md +13 -1
- package/meta/default.css +44 -9
- package/meta/menu.js +237 -2
- package/meta/search.js +41 -0
- package/package.json +1 -1
- package/src/helper/build/autoIndex.js +197 -0
- package/src/helper/build/batch.js +19 -0
- package/src/helper/build/cacheBust.js +62 -0
- package/src/helper/build/excludeFilter.js +67 -0
- package/src/helper/build/footer.js +113 -0
- package/src/helper/build/index.js +13 -0
- package/src/helper/build/menu.js +103 -0
- package/src/helper/build/metadata.js +30 -0
- package/src/helper/build/pathUtils.js +13 -0
- package/src/helper/build/progress.js +35 -0
- package/src/helper/build/templates.js +30 -0
- package/src/helper/build/titleCase.js +7 -0
- package/src/helper/build/watchCache.js +26 -0
- package/src/helper/customMenu.js +303 -0
- package/src/jobs/generate.js +97 -561
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// Custom menu support - allows defining custom menus in menu.md, menu.txt, _menu.md, or _menu.txt
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { join, dirname, relative, resolve, basename } from "path";
|
|
4
|
+
|
|
5
|
+
// Menu file names to look for (in order of priority)
|
|
6
|
+
const MENU_FILE_NAMES = ['menu.md', 'menu.txt', '_menu.md', '_menu.txt'];
|
|
7
|
+
|
|
8
|
+
// Source file extensions to check
|
|
9
|
+
const SOURCE_EXTENSIONS = ['.md', '.txt'];
|
|
10
|
+
|
|
11
|
+
// Default icons
|
|
12
|
+
const FOLDER_ICON = '📁';
|
|
13
|
+
const DOCUMENT_ICON = '📄';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if a source file exists for a given path
|
|
17
|
+
* Checks for: ./Foo.md, ./Foo.txt, ./Foo/index.md, ./Foo/index.txt, ./Foo/home.md, ./Foo/home.txt, ./Foo/Foo.md, ./Foo/Foo.txt
|
|
18
|
+
* @param {string} basePath - The base path without extension (absolute path in source)
|
|
19
|
+
* @returns {boolean} - True if a source file exists
|
|
20
|
+
*/
|
|
21
|
+
function sourceFileExists(basePath) {
|
|
22
|
+
const name = basename(basePath);
|
|
23
|
+
|
|
24
|
+
// Check direct file (./Foo.md, ./Foo.txt)
|
|
25
|
+
for (const ext of SOURCE_EXTENSIONS) {
|
|
26
|
+
if (existsSync(basePath + ext)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check as folder with index files (./Foo/index.md, ./Foo/index.txt, ./Foo/home.md, ./Foo/home.txt, ./Foo/Foo.md, ./Foo/Foo.txt)
|
|
32
|
+
const indexNames = ['index', 'home', name];
|
|
33
|
+
for (const indexName of indexNames) {
|
|
34
|
+
for (const ext of SOURCE_EXTENSIONS) {
|
|
35
|
+
if (existsSync(join(basePath, indexName + ext))) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find a custom menu file in the given directory or any parent directory
|
|
46
|
+
* @param {string} dirPath - The directory to start searching from
|
|
47
|
+
* @param {string} sourceRoot - The root source directory (stop searching here)
|
|
48
|
+
* @returns {{path: string, content: string, menuDir: string} | null} - Menu file info or null if not found
|
|
49
|
+
*/
|
|
50
|
+
export function findCustomMenu(dirPath, sourceRoot) {
|
|
51
|
+
// Normalize paths
|
|
52
|
+
const normalizedDir = resolve(dirPath);
|
|
53
|
+
const normalizedRoot = resolve(sourceRoot);
|
|
54
|
+
|
|
55
|
+
let currentDir = normalizedDir;
|
|
56
|
+
|
|
57
|
+
// Walk up the directory tree until we reach or pass the source root
|
|
58
|
+
while (currentDir.startsWith(normalizedRoot)) {
|
|
59
|
+
for (const menuFileName of MENU_FILE_NAMES) {
|
|
60
|
+
const menuPath = join(currentDir, menuFileName);
|
|
61
|
+
if (existsSync(menuPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const content = readFileSync(menuPath, 'utf8');
|
|
64
|
+
return {
|
|
65
|
+
path: menuPath,
|
|
66
|
+
content,
|
|
67
|
+
menuDir: currentDir, // The directory where the menu was found
|
|
68
|
+
};
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error(`Error reading menu file ${menuPath}:`, e);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Move up one directory
|
|
76
|
+
const parentDir = dirname(currentDir);
|
|
77
|
+
if (parentDir === currentDir) {
|
|
78
|
+
// Reached filesystem root
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
currentDir = parentDir;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parse a custom menu file and return menu data structure
|
|
89
|
+
* Supports two formats:
|
|
90
|
+
*
|
|
91
|
+
* Markdown format:
|
|
92
|
+
* - [Label](./relative/path)
|
|
93
|
+
* - [Child Label](./relative/child/path)
|
|
94
|
+
*
|
|
95
|
+
* Wikitext format:
|
|
96
|
+
* * [[path|Label]]
|
|
97
|
+
* ** [[child/path|Child Label]]
|
|
98
|
+
* or
|
|
99
|
+
* * [[Label]] (path derived from label)
|
|
100
|
+
*
|
|
101
|
+
* @param {string} content - The menu file content
|
|
102
|
+
* @param {string} menuDir - The directory where the menu file was found
|
|
103
|
+
* @param {string} sourceRoot - The root source directory
|
|
104
|
+
* @returns {Array} - Menu data array compatible with the existing menu system
|
|
105
|
+
*/
|
|
106
|
+
export function parseCustomMenu(content, menuDir, sourceRoot) {
|
|
107
|
+
const lines = content.split('\n');
|
|
108
|
+
const menuItems = [];
|
|
109
|
+
const stack = [{ children: menuItems, indent: -1 }]; // Stack for tracking nesting
|
|
110
|
+
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
// Skip empty lines
|
|
113
|
+
const trimmedLine = line.trim();
|
|
114
|
+
if (!trimmedLine) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let label = null;
|
|
119
|
+
let href = null;
|
|
120
|
+
let indent = 0;
|
|
121
|
+
|
|
122
|
+
// Try wikitext format first: * [[path|Label]] or * [[Label]]
|
|
123
|
+
if (trimmedLine.match(/^\*+\s*\[\[/)) {
|
|
124
|
+
// Count asterisks for indent level
|
|
125
|
+
const asteriskMatch = trimmedLine.match(/^(\*+)/);
|
|
126
|
+
indent = asteriskMatch ? (asteriskMatch[1].length - 1) * 2 : 0; // Convert to space-equivalent
|
|
127
|
+
|
|
128
|
+
// Parse wikitext link: [[path|label]] or [[label]]
|
|
129
|
+
const wikiMatch = trimmedLine.match(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/);
|
|
130
|
+
if (wikiMatch) {
|
|
131
|
+
if (wikiMatch[2]) {
|
|
132
|
+
// [[path|label]] format - first part is path, second is label
|
|
133
|
+
// Special case: _home means index
|
|
134
|
+
const pathPart = wikiMatch[1] === '_home' ? 'index' : wikiMatch[1];
|
|
135
|
+
label = wikiMatch[2];
|
|
136
|
+
href = './' + pathPart;
|
|
137
|
+
} else {
|
|
138
|
+
// [[label]] format - path derived from label
|
|
139
|
+
label = wikiMatch[1];
|
|
140
|
+
// Special case: _home means index
|
|
141
|
+
href = './' + (wikiMatch[1] === '_home' ? 'index' : wikiMatch[1]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Try markdown format: - [Label](path)
|
|
146
|
+
else if (trimmedLine.startsWith('-')) {
|
|
147
|
+
// Calculate indentation level (count leading spaces/tabs before the dash)
|
|
148
|
+
const leadingWhitespace = line.match(/^(\s*)/)[1];
|
|
149
|
+
indent = leadingWhitespace.length;
|
|
150
|
+
|
|
151
|
+
// Parse the markdown link: - [Label](path)
|
|
152
|
+
const linkMatch = trimmedLine.match(/^-\s*\[([^\]]+)\]\(([^)]+)\)/);
|
|
153
|
+
if (linkMatch) {
|
|
154
|
+
label = linkMatch[1];
|
|
155
|
+
href = linkMatch[2];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Skip if we couldn't parse
|
|
160
|
+
if (!label || !href) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Resolve relative paths based on where the menu file was found
|
|
165
|
+
// Resolve relative paths and check if source file exists
|
|
166
|
+
let absoluteSourcePath = null;
|
|
167
|
+
if (href.startsWith('./') || href.startsWith('../') || !href.startsWith('/')) {
|
|
168
|
+
// It's a relative path - resolve it relative to the menu directory
|
|
169
|
+
absoluteSourcePath = resolve(menuDir, href);
|
|
170
|
+
|
|
171
|
+
// Check if the source file exists
|
|
172
|
+
const fileExists = sourceFileExists(absoluteSourcePath);
|
|
173
|
+
|
|
174
|
+
if (fileExists) {
|
|
175
|
+
// Convert to web-accessible path (relative to source root)
|
|
176
|
+
href = '/' + relative(sourceRoot, absoluteSourcePath);
|
|
177
|
+
// Normalize path separators for web
|
|
178
|
+
href = href.replace(/\\/g, '/');
|
|
179
|
+
// Ensure it ends with .html if it doesn't have an extension
|
|
180
|
+
if (!href.match(/\.[a-z]+$/i)) {
|
|
181
|
+
// Check if it's likely a folder (ends with /) or file
|
|
182
|
+
if (href.endsWith('/')) {
|
|
183
|
+
href = href + 'index.html';
|
|
184
|
+
} else {
|
|
185
|
+
// Assume it's a file - add .html
|
|
186
|
+
href = href + '.html';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
// Source file doesn't exist - this is a non-navigable menu item
|
|
191
|
+
href = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const menuItem = {
|
|
196
|
+
label,
|
|
197
|
+
path: label.toLowerCase().replace(/\s+/g, '-'), // Generate path from label
|
|
198
|
+
href,
|
|
199
|
+
hasChildren: false, // Will be updated if children are added
|
|
200
|
+
icon: `<span class="menu-icon">${href ? DOCUMENT_ICON : FOLDER_ICON}</span>`,
|
|
201
|
+
children: [],
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Find the correct parent based on indentation
|
|
205
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
206
|
+
stack.pop();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Add this item to the current parent
|
|
210
|
+
const parent = stack[stack.length - 1];
|
|
211
|
+
parent.children.push(menuItem);
|
|
212
|
+
|
|
213
|
+
// If this is the first child, mark parent as having children
|
|
214
|
+
if (parent.menuItem) {
|
|
215
|
+
parent.menuItem.hasChildren = true;
|
|
216
|
+
parent.menuItem.icon = `<span class="menu-icon">${FOLDER_ICON}</span>`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Push this item onto the stack as a potential parent
|
|
220
|
+
stack.push({ children: menuItem.children, indent, menuItem });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return menuItems;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get custom menu data for a given file path
|
|
228
|
+
* @param {string} filePath - The source file path
|
|
229
|
+
* @param {string} sourceRoot - The root source directory
|
|
230
|
+
* @returns {{menuData: Array, menuPath: string} | null} - Menu data and path, or null if no custom menu
|
|
231
|
+
*/
|
|
232
|
+
export function getCustomMenuForFile(filePath, sourceRoot) {
|
|
233
|
+
const fileDir = dirname(filePath);
|
|
234
|
+
const customMenuInfo = findCustomMenu(fileDir, sourceRoot);
|
|
235
|
+
|
|
236
|
+
if (!customMenuInfo) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const menuData = parseCustomMenu(customMenuInfo.content, customMenuInfo.menuDir, sourceRoot);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
menuData,
|
|
244
|
+
menuPath: customMenuInfo.path,
|
|
245
|
+
menuDir: customMenuInfo.menuDir,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Build menu HTML structure from custom menu data
|
|
251
|
+
* This matches the format expected by the existing menu.js client-side code
|
|
252
|
+
* @param {Array} menuData - The parsed menu data
|
|
253
|
+
* @returns {string} - HTML string for the menu
|
|
254
|
+
*/
|
|
255
|
+
export function buildCustomMenuHtml(menuData) {
|
|
256
|
+
const menuConfigScript = `<script type="application/json" id="menu-config">${JSON.stringify({ openMenuItems: [], customMenu: true })}</script>`;
|
|
257
|
+
|
|
258
|
+
const breadcrumbHtml = `
|
|
259
|
+
<div class="menu-breadcrumb" style="display: none;">
|
|
260
|
+
<button class="menu-back" title="Go back">←</button>
|
|
261
|
+
<button class="menu-home" title="Go to root">🏠</button>
|
|
262
|
+
<span class="menu-current-path"></span>
|
|
263
|
+
</div>`;
|
|
264
|
+
|
|
265
|
+
const menuHtml = renderCustomMenuLevel(menuData);
|
|
266
|
+
|
|
267
|
+
return `${menuConfigScript}${breadcrumbHtml}<ul class="menu-level" data-level="0">${menuHtml}</ul>`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Render a level of the custom menu
|
|
272
|
+
* @param {Array} items - Menu items at this level
|
|
273
|
+
* @returns {string} - HTML string
|
|
274
|
+
*/
|
|
275
|
+
function renderCustomMenuLevel(items) {
|
|
276
|
+
return items.map(item => {
|
|
277
|
+
const hasChildrenClass = item.hasChildren ? ' has-children' : '';
|
|
278
|
+
const hasChildrenIndicator = item.hasChildren ? '<span class="menu-more">⋯</span>' : '';
|
|
279
|
+
|
|
280
|
+
const labelHtml = item.href
|
|
281
|
+
? `<a href="${item.href}" class="menu-label">${item.label}</a>`
|
|
282
|
+
: `<span class="menu-label">${item.label}</span>`;
|
|
283
|
+
|
|
284
|
+
return `
|
|
285
|
+
<li class="menu-item${hasChildrenClass}" data-path="${item.path}">
|
|
286
|
+
<div class="menu-item-row">
|
|
287
|
+
${item.icon}
|
|
288
|
+
${labelHtml}
|
|
289
|
+
${hasChildrenIndicator}
|
|
290
|
+
</div>
|
|
291
|
+
</li>`;
|
|
292
|
+
}).join('');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check if a directory (or any parent) has a custom menu
|
|
297
|
+
* @param {string} dirPath - The directory to check
|
|
298
|
+
* @param {string} sourceRoot - The root source directory
|
|
299
|
+
* @returns {boolean} - True if a custom menu exists
|
|
300
|
+
*/
|
|
301
|
+
export function hasCustomMenu(dirPath, sourceRoot) {
|
|
302
|
+
return findCustomMenu(dirPath, sourceRoot) !== null;
|
|
303
|
+
}
|