@kenjura/ursa 0.41.0 → 0.44.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 +17 -0
- package/package.json +1 -1
- package/src/helper/automenu.js +8 -1
- package/src/helper/sectionExtractor.js +76 -0
- package/src/helper/ursaConfig.js +62 -0
- package/src/jobs/generate.js +41 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
# 0.44.0
|
|
2
|
+
2025-12-16
|
|
3
|
+
|
|
4
|
+
- Added 'sections' metadata property with hierarchical section structure
|
|
5
|
+
|
|
6
|
+
# 0.43.0
|
|
7
|
+
2025-12-14
|
|
8
|
+
|
|
9
|
+
- Added buildId and .ursa.json
|
|
10
|
+
- Added full datetime to footer
|
|
11
|
+
- Added commit hash to footer as comment
|
|
12
|
+
|
|
13
|
+
# 0.42.0
|
|
14
|
+
2025-12-14
|
|
15
|
+
|
|
16
|
+
- Automenu will now remove dashes from file names
|
|
17
|
+
|
|
1
18
|
# 0.41.0
|
|
2
19
|
2025-12-13
|
|
3
20
|
|
package/package.json
CHANGED
package/src/helper/automenu.js
CHANGED
|
@@ -14,6 +14,13 @@ const HOME_ICON = '🏠';
|
|
|
14
14
|
// Index file extensions to check for folder links
|
|
15
15
|
const INDEX_EXTENSIONS = ['.md', '.txt', '.yml', '.yaml'];
|
|
16
16
|
|
|
17
|
+
// Convert filename to display name (e.g., "foo-bar" -> "Foo Bar")
|
|
18
|
+
function toDisplayName(filename) {
|
|
19
|
+
return filename
|
|
20
|
+
.replace(/[-_]/g, ' ') // Replace dashes and underscores with spaces
|
|
21
|
+
.replace(/\b\w/g, c => c.toUpperCase()); // Capitalize first letter of each word
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
function hasIndexFile(dirPath) {
|
|
18
25
|
for (const ext of INDEX_EXTENSIONS) {
|
|
19
26
|
const indexPath = join(dirPath, `index${ext}`);
|
|
@@ -151,7 +158,7 @@ function buildMenuData(tree, source, validPaths, parentPath = '') {
|
|
|
151
158
|
|
|
152
159
|
// Get folder config for custom label and icon
|
|
153
160
|
const folderConfig = hasChildren ? getFolderConfig(item.path) : null;
|
|
154
|
-
const label = folderConfig?.label || baseName;
|
|
161
|
+
const label = folderConfig?.label || toDisplayName(baseName);
|
|
155
162
|
|
|
156
163
|
let rawHref = null;
|
|
157
164
|
if (hasChildren) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract sections from markdown content based on headings
|
|
3
|
+
* Creates a hierarchical structure of sections
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract sections from markdown content
|
|
8
|
+
* @param {string} content - The markdown content
|
|
9
|
+
* @returns {Array} Array of section objects with name and optional children
|
|
10
|
+
*/
|
|
11
|
+
export function extractSections(content) {
|
|
12
|
+
if (!content) return [];
|
|
13
|
+
|
|
14
|
+
// Match all markdown headings (# to ######)
|
|
15
|
+
// Handles both "# Heading" and "#Heading" formats
|
|
16
|
+
const headingRegex = /^(#{1,6})\s*(.+?)$/gm;
|
|
17
|
+
|
|
18
|
+
const headings = [];
|
|
19
|
+
let match;
|
|
20
|
+
|
|
21
|
+
while ((match = headingRegex.exec(content)) !== null) {
|
|
22
|
+
const level = match[1].length; // Number of # characters
|
|
23
|
+
const name = match[2].trim();
|
|
24
|
+
headings.push({ level, name });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (headings.length === 0) return [];
|
|
28
|
+
|
|
29
|
+
// Build hierarchical structure
|
|
30
|
+
return buildSectionTree(headings);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build a hierarchical tree from flat heading list
|
|
35
|
+
* @param {Array} headings - Array of {level, name} objects
|
|
36
|
+
* @returns {Array} Hierarchical section tree
|
|
37
|
+
*/
|
|
38
|
+
function buildSectionTree(headings) {
|
|
39
|
+
const root = { level: 0, children: [] };
|
|
40
|
+
const stack = [root];
|
|
41
|
+
|
|
42
|
+
for (const heading of headings) {
|
|
43
|
+
const section = { name: heading.name, level: heading.level, children: [] };
|
|
44
|
+
|
|
45
|
+
// Pop stack until we find a parent with lower level
|
|
46
|
+
while (stack.length > 1 && stack[stack.length - 1].level >= heading.level) {
|
|
47
|
+
stack.pop();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add to parent's children
|
|
51
|
+
const parent = stack[stack.length - 1];
|
|
52
|
+
parent.children.push(section);
|
|
53
|
+
|
|
54
|
+
// Push this section onto stack (it might have children)
|
|
55
|
+
stack.push(section);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Clean up: remove level and empty children arrays
|
|
59
|
+
cleanupTree(root.children);
|
|
60
|
+
|
|
61
|
+
return root.children;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Remove level property and empty children arrays from the tree
|
|
66
|
+
*/
|
|
67
|
+
function cleanupTree(sections) {
|
|
68
|
+
for (const section of sections) {
|
|
69
|
+
delete section.level;
|
|
70
|
+
if (section.children && section.children.length > 0) {
|
|
71
|
+
cleanupTree(section.children);
|
|
72
|
+
} else {
|
|
73
|
+
delete section.children;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
const CONFIG_FILENAME = '.ursa.json';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the path to the .ursa.json config file in the source directory
|
|
8
|
+
* @param {string} sourceDir - The source directory path
|
|
9
|
+
* @returns {string} Path to the config file
|
|
10
|
+
*/
|
|
11
|
+
function getConfigPath(sourceDir) {
|
|
12
|
+
return join(sourceDir, CONFIG_FILENAME);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load the ursa config from .ursa.json
|
|
17
|
+
* @param {string} sourceDir - The source directory path
|
|
18
|
+
* @returns {object} The config object (empty object if file doesn't exist)
|
|
19
|
+
*/
|
|
20
|
+
export function loadUrsaConfig(sourceDir) {
|
|
21
|
+
const configPath = getConfigPath(sourceDir);
|
|
22
|
+
try {
|
|
23
|
+
if (existsSync(configPath)) {
|
|
24
|
+
const content = readFileSync(configPath, 'utf8');
|
|
25
|
+
return JSON.parse(content);
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.error(`Error reading ${CONFIG_FILENAME}: ${e.message}`);
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Save the ursa config to .ursa.json
|
|
35
|
+
* @param {string} sourceDir - The source directory path
|
|
36
|
+
* @param {object} config - The config object to save
|
|
37
|
+
*/
|
|
38
|
+
export function saveUrsaConfig(sourceDir, config) {
|
|
39
|
+
const configPath = getConfigPath(sourceDir);
|
|
40
|
+
try {
|
|
41
|
+
const content = JSON.stringify(config, null, 2);
|
|
42
|
+
writeFileSync(configPath, content, 'utf8');
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error(`Error writing ${CONFIG_FILENAME}: ${e.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the current build ID and increment it for the next build
|
|
50
|
+
* @param {string} sourceDir - The source directory path
|
|
51
|
+
* @returns {number} The current build ID (starting from 1)
|
|
52
|
+
*/
|
|
53
|
+
export function getAndIncrementBuildId(sourceDir) {
|
|
54
|
+
const config = loadUrsaConfig(sourceDir);
|
|
55
|
+
const currentBuildId = config.buildId || 0;
|
|
56
|
+
const newBuildId = currentBuildId + 1;
|
|
57
|
+
|
|
58
|
+
config.buildId = newBuildId;
|
|
59
|
+
saveUrsaConfig(sourceDir, config);
|
|
60
|
+
|
|
61
|
+
return newBuildId;
|
|
62
|
+
}
|
package/src/jobs/generate.js
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
buildValidPaths,
|
|
21
21
|
markInactiveLinks,
|
|
22
22
|
} from "../helper/linkValidator.js";
|
|
23
|
+
import { getAndIncrementBuildId } from "../helper/ursaConfig.js";
|
|
24
|
+
import { extractSections } from "../helper/sectionExtractor.js";
|
|
23
25
|
|
|
24
26
|
// Helper function to build search index from processed files
|
|
25
27
|
function buildSearchIndex(jsonCache, source, output) {
|
|
@@ -128,8 +130,12 @@ export async function generate({
|
|
|
128
130
|
|
|
129
131
|
const menu = await getMenu(allSourceFilenames, source, validPaths);
|
|
130
132
|
|
|
133
|
+
// Get and increment build ID from .ursa.json
|
|
134
|
+
const buildId = getAndIncrementBuildId(resolve(_source));
|
|
135
|
+
console.log(`Build #${buildId}`);
|
|
136
|
+
|
|
131
137
|
// Generate footer content
|
|
132
|
-
const footer = await getFooter(source, _source);
|
|
138
|
+
const footer = await getFooter(source, _source, buildId);
|
|
133
139
|
|
|
134
140
|
// Load content hash cache from .ursa folder in source directory
|
|
135
141
|
let hashCache = new Map();
|
|
@@ -225,12 +231,16 @@ export async function generate({
|
|
|
225
231
|
dirname: dir,
|
|
226
232
|
basename: base,
|
|
227
233
|
});
|
|
234
|
+
// Extract sections for markdown files
|
|
235
|
+
const sections = type === '.md' ? extractSections(rawBody) : [];
|
|
236
|
+
|
|
228
237
|
jsonCache.set(file, {
|
|
229
238
|
name: base,
|
|
230
239
|
url,
|
|
231
240
|
contents: rawBody,
|
|
232
241
|
bodyHtml: body,
|
|
233
242
|
metadata: meta,
|
|
243
|
+
sections,
|
|
234
244
|
transformedMetadata: '',
|
|
235
245
|
});
|
|
236
246
|
return; // Skip regenerating this file
|
|
@@ -302,6 +312,10 @@ export async function generate({
|
|
|
302
312
|
// json
|
|
303
313
|
|
|
304
314
|
const jsonOutputFilename = outputFilename.replace(".html", ".json");
|
|
315
|
+
|
|
316
|
+
// Extract sections for markdown files
|
|
317
|
+
const sections = type === '.md' ? extractSections(rawBody) : [];
|
|
318
|
+
|
|
305
319
|
const jsonObject = {
|
|
306
320
|
name: base,
|
|
307
321
|
url,
|
|
@@ -309,6 +323,7 @@ export async function generate({
|
|
|
309
323
|
// bodyLessMeta: bodyLessMeta,
|
|
310
324
|
bodyHtml: body,
|
|
311
325
|
metadata: meta,
|
|
326
|
+
sections,
|
|
312
327
|
transformedMetadata,
|
|
313
328
|
// html: finalHtml,
|
|
314
329
|
};
|
|
@@ -551,8 +566,9 @@ function addTrailingSlash(somePath) {
|
|
|
551
566
|
* Generate footer HTML from footer.md and package.json
|
|
552
567
|
* @param {string} source - resolved source path with trailing slash
|
|
553
568
|
* @param {string} _source - original source path
|
|
569
|
+
* @param {number} buildId - the current build ID
|
|
554
570
|
*/
|
|
555
|
-
async function getFooter(source, _source) {
|
|
571
|
+
async function getFooter(source, _source, buildId) {
|
|
556
572
|
const footerParts = [];
|
|
557
573
|
|
|
558
574
|
// Try to read footer.md from source root
|
|
@@ -607,12 +623,18 @@ async function getFooter(source, _source) {
|
|
|
607
623
|
console.error(`Error reading ursa package.json: ${e.message}`);
|
|
608
624
|
}
|
|
609
625
|
|
|
610
|
-
// Build meta line: version, timestamp, "generated by ursa"
|
|
626
|
+
// Build meta line: version, build id, timestamp, "generated by ursa"
|
|
611
627
|
const metaParts = [];
|
|
612
628
|
if (docPackage?.version) {
|
|
613
629
|
metaParts.push(`v${docPackage.version}`);
|
|
614
630
|
}
|
|
615
|
-
metaParts.push(
|
|
631
|
+
metaParts.push(`build ${buildId}`);
|
|
632
|
+
|
|
633
|
+
// Full date/time in a readable format
|
|
634
|
+
const now = new Date();
|
|
635
|
+
const timestamp = now.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
|
|
636
|
+
metaParts.push(timestamp);
|
|
637
|
+
|
|
616
638
|
metaParts.push(`Generated by <a href="https://www.npmjs.com/package/@kenjura/ursa">ursa</a> v${ursaVersion}`);
|
|
617
639
|
|
|
618
640
|
footerParts.push(`<div class="footer-meta">${metaParts.join(' • ')}</div>`);
|
|
@@ -628,5 +650,20 @@ async function getFooter(source, _source) {
|
|
|
628
650
|
}
|
|
629
651
|
}
|
|
630
652
|
|
|
653
|
+
// Try to get git short hash of doc repo (as HTML comment)
|
|
654
|
+
try {
|
|
655
|
+
const { execSync } = await import('child_process');
|
|
656
|
+
const gitHash = execSync('git rev-parse --short HEAD', {
|
|
657
|
+
cwd: resolve(_source),
|
|
658
|
+
encoding: 'utf8',
|
|
659
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
660
|
+
}).trim();
|
|
661
|
+
if (gitHash) {
|
|
662
|
+
footerParts.push(`<!-- git: ${gitHash} -->`);
|
|
663
|
+
}
|
|
664
|
+
} catch (e) {
|
|
665
|
+
// Not a git repo or git not available - silently skip
|
|
666
|
+
}
|
|
667
|
+
|
|
631
668
|
return footerParts.join('\n');
|
|
632
669
|
}
|