@kenjura/ursa 0.35.0 → 0.40.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 CHANGED
@@ -1,3 +1,30 @@
1
+ # 0.40.0
2
+ 2025-12-13
3
+
4
+ - Added footer
5
+
6
+ # 0.39.0
7
+ 2025-12-13
8
+
9
+ - Updated to node 24.5 to satisfy npm Trusted Publishing
10
+ - Refactored Github Actions
11
+ - Added CONTRIBUTING.md
12
+
13
+ # 0.38.0
14
+ 2025-12-13
15
+
16
+ - Updated Github Actions workflow to use OIDC for authentication
17
+
18
+ # 0.37.0
19
+ 2025-12-13
20
+
21
+ - Added Github Actions workflow for CI/CD (npm publish)
22
+
23
+ # 0.36.0
24
+ 2025-12-13
25
+
26
+ - Links to a valid .md file in source will now render as a link to the corresponding .html file (and show as an active link)
27
+
1
28
  # 0.35.0
2
29
  2025-12-11
3
30
 
@@ -33,6 +33,9 @@
33
33
  <article id="main-content">
34
34
  ${body}
35
35
  </article>
36
+ <footer id="site-footer">
37
+ ${footer}
38
+ </footer>
36
39
  <div id="global-nav">
37
40
  </div>
38
41
 
package/meta/default.css CHANGED
@@ -432,6 +432,43 @@ article#main-content {
432
432
  }
433
433
  }
434
434
 
435
+ /* Footer styles */
436
+ footer#site-footer {
437
+ width: var(--article-width);
438
+ margin: 3rem auto 2rem auto;
439
+ padding-top: 2rem;
440
+ border-top: 1px solid rgba(128, 128, 128, 0.3);
441
+ text-align: center;
442
+
443
+ .footer-content {
444
+ margin-bottom: 1.5rem;
445
+ font-size: 0.95rem;
446
+ opacity: 0.85;
447
+ }
448
+
449
+ .footer-meta {
450
+ font-size: 0.8rem;
451
+ opacity: 0.6;
452
+ margin-bottom: 0.5rem;
453
+
454
+ a {
455
+ color: inherit;
456
+ }
457
+ }
458
+
459
+ .footer-copyright {
460
+ font-size: 0.8rem;
461
+ opacity: 0.6;
462
+ }
463
+ }
464
+
465
+ @media (max-width: 800px) {
466
+ footer#site-footer {
467
+ width: calc(100vw - 2rem);
468
+ margin: 3rem 1rem 2rem 1rem;
469
+ }
470
+ }
471
+
435
472
  @media (max-width: 800px) {
436
473
  nav#nav-global {
437
474
  display: flex;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@kenjura/ursa",
3
3
  "author": "Andrew London <andrew@kenjura.com>",
4
4
  "type": "module",
5
- "version": "0.35.0",
5
+ "version": "0.40.0",
6
6
  "description": "static site generator from MD/wikitext/YML",
7
7
  "main": "lib/index.js",
8
8
  "bin": {
@@ -129,7 +129,7 @@ function buildMenuData(tree, source, validPaths, parentPath = '') {
129
129
  const items = [];
130
130
 
131
131
  // Files to hide from menu by default
132
- const hiddenFiles = ['config.json', 'style.css'];
132
+ const hiddenFiles = ['config.json', 'style.css', 'footer.md'];
133
133
 
134
134
  for (const item of tree.children || []) {
135
135
  const ext = extname(item.path);
@@ -157,7 +157,24 @@ function resolveHref(href, validPaths, currentDocPath = null) {
157
157
  // Check if the href already has an extension
158
158
  const ext = extname(hrefWithoutHash);
159
159
  if (ext) {
160
- // Has extension but doesn't exist
160
+ // Special handling for .md links - convert to .html if valid
161
+ if (ext.toLowerCase() === '.md') {
162
+ // Remove .md and check if .html version exists
163
+ const pathWithoutMd = normalized.slice(0, -3); // Remove '.md'
164
+ const htmlPath = pathWithoutMd + '.html';
165
+ debugTries.push(`${normalized} (.md → .html) ${htmlPath} → ${validPaths.has(htmlPath) ? '✓' : '✗'}`);
166
+ if (validPaths.has(htmlPath)) {
167
+ // Convert .md to .html in the resolved href
168
+ const resolvedHref = absoluteHref.slice(0, -3) + '.html' + hash;
169
+ return { resolvedHref, inactive: false, debug: debugTries.join(' | ') };
170
+ }
171
+ // Also check without extension (in case valid paths don't have .html suffix)
172
+ if (validPaths.has(pathWithoutMd)) {
173
+ const resolvedHref = absoluteHref.slice(0, -3) + '.html' + hash;
174
+ return { resolvedHref, inactive: false, debug: debugTries.join(' | ') };
175
+ }
176
+ }
177
+ // Has extension but doesn't exist (or is not .md)
161
178
  debugTries.push(`${normalized} → ✗`);
162
179
  return { resolvedHref: absoluteHref + hash, inactive: true, debug: debugTries.join(' | ') };
163
180
  }
@@ -128,6 +128,9 @@ export async function generate({
128
128
 
129
129
  const menu = await getMenu(allSourceFilenames, source, validPaths);
130
130
 
131
+ // Generate footer content
132
+ const footer = await getFooter(source, _source);
133
+
131
134
  // Load content hash cache from .ursa folder in source directory
132
135
  let hashCache = new Map();
133
136
  if (!_clean) {
@@ -285,7 +288,8 @@ export async function generate({
285
288
  .replace("${transformedMetadata}", transformedMetadata)
286
289
  .replace("${body}", body)
287
290
  .replace("${embeddedStyle}", embeddedStyle)
288
- .replace("${searchIndex}", JSON.stringify(searchIndex));
291
+ .replace("${searchIndex}", JSON.stringify(searchIndex))
292
+ .replace("${footer}", footer);
289
293
 
290
294
  // Resolve links and mark broken internal links as inactive (debug mode on)
291
295
  // Pass docUrlPath so relative links can be resolved correctly
@@ -378,7 +382,8 @@ export async function generate({
378
382
  .replace("${title}", "Index")
379
383
  .replace("${meta}", "{}")
380
384
  .replace("${transformedMetadata}", "")
381
- .replace("${embeddedStyle}", "");
385
+ .replace("${embeddedStyle}", "")
386
+ .replace("${footer}", footer);
382
387
  console.log(`writing directory index to ${htmlOutputFilename}`);
383
388
  await outputFile(htmlOutputFilename, finalHtml);
384
389
  }
@@ -541,3 +546,70 @@ function addTrailingSlash(somePath) {
541
546
  if (somePath[somePath.length - 1] == "/") return somePath;
542
547
  return `${somePath}/`;
543
548
  }
549
+
550
+ /**
551
+ * Generate footer HTML from footer.md and package.json
552
+ * @param {string} source - resolved source path with trailing slash
553
+ * @param {string} _source - original source path
554
+ */
555
+ async function getFooter(source, _source) {
556
+ const footerParts = [];
557
+
558
+ // Try to read footer.md from source root
559
+ const footerPath = join(source, 'footer.md');
560
+ try {
561
+ if (existsSync(footerPath)) {
562
+ const footerMd = await readFile(footerPath, 'utf8');
563
+ const footerHtml = renderFile({ fileContents: footerMd, type: '.md' });
564
+ footerParts.push(`<div class="footer-content">${footerHtml}</div>`);
565
+ }
566
+ } catch (e) {
567
+ console.error(`Error reading footer.md: ${e.message}`);
568
+ }
569
+
570
+ // Try to read package.json from doc repo
571
+ let docPackage = null;
572
+ const packagePath = join(resolve(_source), 'package.json');
573
+ try {
574
+ if (existsSync(packagePath)) {
575
+ const packageJson = await readFile(packagePath, 'utf8');
576
+ docPackage = JSON.parse(packageJson);
577
+ }
578
+ } catch (e) {
579
+ console.error(`Error reading doc package.json: ${e.message}`);
580
+ }
581
+
582
+ // Get ursa version from ursa's own package.json
583
+ let ursaVersion = 'unknown';
584
+ try {
585
+ const ursaPackagePath = new URL('../../../package.json', import.meta.url).pathname;
586
+ const ursaPackageJson = await readFile(ursaPackagePath, 'utf8');
587
+ const ursaPackage = JSON.parse(ursaPackageJson);
588
+ ursaVersion = ursaPackage.version;
589
+ } catch (e) {
590
+ console.error(`Error reading ursa package.json: ${e.message}`);
591
+ }
592
+
593
+ // Build meta line: version, timestamp, "generated by ursa"
594
+ const metaParts = [];
595
+ if (docPackage?.version) {
596
+ metaParts.push(`v${docPackage.version}`);
597
+ }
598
+ metaParts.push(new Date().toISOString().split('T')[0]); // YYYY-MM-DD
599
+ metaParts.push(`Generated by <a href="https://www.npmjs.com/package/@kenjura/ursa">ursa</a> v${ursaVersion}`);
600
+
601
+ footerParts.push(`<div class="footer-meta">${metaParts.join(' • ')}</div>`);
602
+
603
+ // Copyright line from doc package.json
604
+ if (docPackage?.copyright) {
605
+ footerParts.push(`<div class="footer-copyright">${docPackage.copyright}</div>`);
606
+ } else if (docPackage?.author) {
607
+ const year = new Date().getFullYear();
608
+ const author = typeof docPackage.author === 'string' ? docPackage.author : docPackage.author.name;
609
+ if (author) {
610
+ footerParts.push(`<div class="footer-copyright">© ${year} ${author}</div>`);
611
+ }
612
+ }
613
+
614
+ return footerParts.join('\n');
615
+ }