@kenjura/ursa 0.72.0 → 0.75.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.
@@ -39,6 +39,7 @@ import { createWhitelistFilter } from "../helper/whitelistFilter.js";
39
39
  import { processAllImages, transformImageTags, clearImageCache, copyAllImagesFast } from "../helper/imageProcessor.js";
40
40
  import { extractImageReferences } from "../helper/imageExtractor.js";
41
41
  import { checkFileSize, readFileStreaming, formatFileSize } from "../helper/streamingReader.js";
42
+ import { generateBreadcrumbs } from "../helper/breadcrumbs.js";
42
43
  import {
43
44
  loadNavCache,
44
45
  saveNavCache,
@@ -462,7 +463,9 @@ export async function generate({
462
463
  const searchUrl = relativePath.startsWith('/') ? relativePath : '/' + relativePath;
463
464
 
464
465
  // Generate title from filename (in title case)
465
- const title = toTitleCase(base);
466
+ // For index/home files, use parent folder name instead
467
+ const titleBase = (base === 'index' || base === 'home') ? basename(dirname(file)) : base;
468
+ const title = toTitleCase(titleBase || base);
466
469
 
467
470
  // Always add to search index (lightweight: title + path only, content added lazily)
468
471
  searchIndex.push({
@@ -543,6 +546,18 @@ export async function generate({
543
546
  useWorker: true
544
547
  });
545
548
 
549
+ // Inject default H1 if body doesn't start with one
550
+ if (!body || !body.trimStart().startsWith('<h1')) {
551
+ const h1Title = fileMeta?.title || title;
552
+ body = `<h1>${h1Title}</h1>\n` + (body || '');
553
+ }
554
+
555
+ // Inject breadcrumbs before the H1
556
+ const breadcrumbs = generateBreadcrumbs(dir, base, fileMeta);
557
+ if (breadcrumbs) {
558
+ body = breadcrumbs + body;
559
+ }
560
+
546
561
  // Inject frontmatter table after first H1 (for markdown files with metadata)
547
562
  if ((type === '.md' || type === '.mdx') && fileMeta) {
548
563
  body = injectFrontmatterTable(body, fileMeta);
@@ -640,7 +655,7 @@ export async function generate({
640
655
  // Build final HTML with all replacements in a single regex pass
641
656
  // This avoids creating 8 intermediate strings
642
657
  const replacements = {
643
- "${title}": title,
658
+ "${title}": fileMeta?.title || title,
644
659
  "${menu}": menu,
645
660
  "${meta}": JSON.stringify(fileMeta),
646
661
  "${transformedMetadata}": lazyTransformedMeta,
@@ -654,13 +669,19 @@ export async function generate({
654
669
  const pattern = /\$\{(title|menu|meta|transformedMetadata|body|styleLink|customScript|searchIndex|footer)\}/g;
655
670
  let finalHtml = template.replace(pattern, (match) => replacements[match] ?? match);
656
671
 
657
- // If this page has a custom menu, add data attributes to body
672
+ // Add menu data attributes to body
658
673
  if (customMenuInfo) {
659
- const menuPosition = customMenuInfo.menuPosition || 'side';
674
+ const menuPosition = customMenuInfo.menuPosition || 'top';
660
675
  finalHtml = finalHtml.replace(
661
676
  /<body([^>]*)>/,
662
677
  `<body$1 data-custom-menu="${customMenuInfo.menuJsonPath}" data-menu-position="${menuPosition}">`
663
678
  );
679
+ } else {
680
+ // No custom menu — default to top menu
681
+ finalHtml = finalHtml.replace(
682
+ /<body([^>]*)>/,
683
+ `<body$1 data-menu-position="top">`
684
+ );
664
685
  }
665
686
 
666
687
  // Resolve relative URLs in raw HTML elements (img src, etc.)
@@ -802,7 +823,7 @@ export async function generate({
802
823
  // Include menuPosition in the JSON so client knows how to render
803
824
  const customMenuJson = JSON.stringify({
804
825
  menuData: menuInfo.menuData,
805
- menuPosition: menuInfo.menuPosition || 'side',
826
+ menuPosition: menuInfo.menuPosition || 'top',
806
827
  });
807
828
  progress.log(`Writing custom menu: ${menuInfo.menuJsonPath}`);
808
829
  await outputFile(customMenuPath, customMenuJson);
@@ -1027,6 +1048,9 @@ export async function generate({
1027
1048
  // Print profiler report
1028
1049
  progress.log(profiler.report());
1029
1050
 
1051
+ // Terminate worker pool so threads don't keep the process alive
1052
+ await terminateParserPool();
1053
+
1030
1054
  // Return deferred processing promises if in deferred mode
1031
1055
  // Caller can await these to know when background processing is complete
1032
1056
  return {
@@ -1084,8 +1108,9 @@ export async function regenerateSingleFile(changedFile, {
1084
1108
  .replace(parse(changedFile).ext, ".html");
1085
1109
  const url = '/' + outputFilename.replace(output, '');
1086
1110
 
1087
- // Title from filename
1088
- const title = toTitleCase(base);
1111
+ // Title from filename (for index/home, use parent folder name)
1112
+ const titleBase = (base === 'index' || base === 'home') ? basename(dirname(changedFile)) : base;
1113
+ const title = toTitleCase(titleBase || base);
1089
1114
 
1090
1115
  // Extract metadata
1091
1116
  const fileMeta = extractMetadata(rawBody);
@@ -1117,6 +1142,18 @@ export async function regenerateSingleFile(changedFile, {
1117
1142
  basename: base,
1118
1143
  });
1119
1144
  }
1145
+
1146
+ // Inject default H1 if body doesn't start with one
1147
+ if (!body || !body.trimStart().startsWith('<h1')) {
1148
+ const h1Title = fileMeta?.title || title;
1149
+ body = `<h1>${h1Title}</h1>\n` + (body || '');
1150
+ }
1151
+
1152
+ // Inject breadcrumbs before the H1
1153
+ const breadcrumbs = generateBreadcrumbs(dir, base, fileMeta);
1154
+ if (breadcrumbs) {
1155
+ body = breadcrumbs + body;
1156
+ }
1120
1157
 
1121
1158
  // Inject frontmatter table for markdown/mdx files
1122
1159
  if ((type === '.md' || type === '.mdx') && fileMeta) {
@@ -1191,7 +1228,7 @@ export async function regenerateSingleFile(changedFile, {
1191
1228
  // Build final HTML
1192
1229
  let finalHtml = template;
1193
1230
  const replacements = {
1194
- "${title}": title,
1231
+ "${title}": fileMeta?.title || title,
1195
1232
  "${menu}": menu,
1196
1233
  "${meta}": JSON.stringify(fileMeta),
1197
1234
  "${transformedMetadata}": transformedMetadata,
@@ -1207,11 +1244,17 @@ export async function regenerateSingleFile(changedFile, {
1207
1244
 
1208
1245
  // If this page has a custom menu, add data attributes to body
1209
1246
  if (customMenuInfo) {
1210
- const menuPosition = customMenuInfo.menuPosition || 'side';
1247
+ const menuPosition = customMenuInfo.menuPosition || 'top';
1211
1248
  finalHtml = finalHtml.replace(
1212
1249
  /<body([^>]*)>/,
1213
1250
  `<body$1 data-custom-menu="${customMenuInfo.menuJsonPath}" data-menu-position="${menuPosition}">`
1214
1251
  );
1252
+ } else {
1253
+ // No custom menu — default to top menu
1254
+ finalHtml = finalHtml.replace(
1255
+ /<body([^>]*)>/,
1256
+ `<body$1 data-menu-position="top">`
1257
+ );
1215
1258
  }
1216
1259
 
1217
1260
  // Resolve relative URLs in raw HTML elements (img src, etc.)