docula 1.9.1 → 1.10.1

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/dist/docula.d.ts CHANGED
@@ -139,6 +139,7 @@ type DoculaChangelogEntry = {
139
139
  content: string;
140
140
  generatedHtml: string;
141
141
  preview: string;
142
+ draft?: boolean;
142
143
  previewImage?: string;
143
144
  urlPath: string;
144
145
  lastModified: string;
@@ -180,6 +181,7 @@ type DoculaData = {
180
181
  enableLlmsTxt?: boolean;
181
182
  hasFeed?: boolean;
182
183
  lastModified?: string;
184
+ homeUrl?: string;
183
185
  baseUrl: string;
184
186
  docsPath: string;
185
187
  apiPath: string;
@@ -334,6 +336,11 @@ declare class DoculaOptions {
334
336
  * When true, suppresses all non-error console output during the build.
335
337
  */
336
338
  quiet: boolean;
339
+ /**
340
+ * URL for the logo/home link in the header. Defaults to baseUrl or "/".
341
+ * Useful when hosting docs under a subpath but the logo should link to the parent site.
342
+ */
343
+ homeUrl?: string;
337
344
  /**
338
345
  * Base URL path prefix for all generated paths (e.g., "/docs").
339
346
  * When set, all asset and navigation URLs are prefixed with this path.
package/dist/docula.js CHANGED
@@ -1307,7 +1307,10 @@ import path5 from "path";
1307
1307
  import { Writr as Writr4 } from "writr";
1308
1308
  var writrOptions4 = {
1309
1309
  throwOnEmitError: false,
1310
- throwOnEmptyListeners: false
1310
+ throwOnEmptyListeners: false,
1311
+ renderOptions: {
1312
+ rawHtml: true
1313
+ }
1311
1314
  };
1312
1315
  function getChangelogEntries(changelogPath, options, hash, cachedEntries, previousHashes, currentHashes) {
1313
1316
  const entries = [];
@@ -1330,6 +1333,9 @@ function getChangelogEntries(changelogPath, options, hash, cachedEntries, previo
1330
1333
  }
1331
1334
  }
1332
1335
  const entry = parseChangelogEntry(filePath, options);
1336
+ if (entry.draft) {
1337
+ continue;
1338
+ }
1333
1339
  entries.push(entry);
1334
1340
  }
1335
1341
  }
@@ -1375,6 +1381,7 @@ function parseChangelogEntry(filePath, options) {
1375
1381
  });
1376
1382
  }
1377
1383
  const previewImage = matterData.previewImage;
1384
+ const draft = matterData.draft === true;
1378
1385
  return {
1379
1386
  title: matterData.title ?? fileName,
1380
1387
  date: dateString,
@@ -1387,6 +1394,7 @@ function parseChangelogEntry(filePath, options) {
1387
1394
  mdx: isMdx
1388
1395
  }),
1389
1396
  preview: generateChangelogPreview(markdownContent, 500, isMdx),
1397
+ draft,
1390
1398
  previewImage,
1391
1399
  urlPath: `${buildUrlPath(options.baseUrl, options.changelogPath, slug)}/index.html`,
1392
1400
  lastModified: fs5.statSync(filePath).mtime.toISOString().split("T")[0]
@@ -1402,7 +1410,53 @@ function generateChangelogPreview(markdown, maxLength = 500, mdx = false) {
1402
1410
  if (cleaned.length <= minLength) {
1403
1411
  return new Writr4(cleaned, writrOptions4).renderSync({ mdx });
1404
1412
  }
1405
- const searchArea = cleaned.slice(0, maxLength);
1413
+ const htmlBlocks = [];
1414
+ const tagPattern = /<\/?(\w+)\b[^>]*>/g;
1415
+ const blockStarts = [];
1416
+ for (const tagMatch of cleaned.matchAll(tagPattern)) {
1417
+ const fullMatch = tagMatch[0];
1418
+ const tagName = tagMatch[1];
1419
+ const isClosing = fullMatch.startsWith("</");
1420
+ if (isClosing) {
1421
+ for (let i = blockStarts.length - 1; i >= 0; i--) {
1422
+ if (blockStarts[i].tag === tagName) {
1423
+ if (blockStarts[i].depth === 0) {
1424
+ htmlBlocks.push({
1425
+ start: blockStarts[i].index,
1426
+ end: tagMatch.index + fullMatch.length
1427
+ });
1428
+ blockStarts.splice(i, 1);
1429
+ } else {
1430
+ blockStarts[i].depth--;
1431
+ }
1432
+ break;
1433
+ }
1434
+ }
1435
+ } else if (!fullMatch.endsWith("/>")) {
1436
+ const existing = blockStarts.find((s) => s.tag === tagName);
1437
+ if (existing) {
1438
+ existing.depth++;
1439
+ } else {
1440
+ blockStarts.push({ tag: tagName, index: tagMatch.index, depth: 0 });
1441
+ }
1442
+ }
1443
+ }
1444
+ let effectiveMax = maxLength;
1445
+ let extended = true;
1446
+ while (extended) {
1447
+ extended = false;
1448
+ for (const block of htmlBlocks) {
1449
+ if (effectiveMax > block.start && effectiveMax < block.end) {
1450
+ let end = block.end;
1451
+ while (end < cleaned.length && cleaned[end] === "\n") {
1452
+ end++;
1453
+ }
1454
+ effectiveMax = end;
1455
+ extended = true;
1456
+ }
1457
+ }
1458
+ }
1459
+ const searchArea = cleaned.slice(0, effectiveMax);
1406
1460
  let splitIndex = -1;
1407
1461
  let pos = searchArea.lastIndexOf("\n\n");
1408
1462
  while (pos >= 0) {
@@ -1429,7 +1483,7 @@ function generateChangelogPreview(markdown, maxLength = 500, mdx = false) {
1429
1483
  let lastItemEnd = -1;
1430
1484
  for (const line of lines) {
1431
1485
  const lineEnd = charCount + line.length;
1432
- if (lineEnd <= maxLength && (/^[-*]\s/.test(line) || /^\d+\.\s/.test(line))) {
1486
+ if (lineEnd <= effectiveMax && (/^[-*]\s/.test(line) || /^\d+\.\s/.test(line))) {
1433
1487
  if (charCount > 0 && charCount >= minLength) {
1434
1488
  lastItemEnd = charCount - 1;
1435
1489
  }
@@ -1445,7 +1499,7 @@ function generateChangelogPreview(markdown, maxLength = 500, mdx = false) {
1445
1499
  const truncated2 = cleaned.slice(0, splitIndex).trim();
1446
1500
  return new Writr4(truncated2, writrOptions4).renderSync({ mdx });
1447
1501
  }
1448
- let truncated = cleaned.slice(0, maxLength);
1502
+ let truncated = cleaned.slice(0, effectiveMax);
1449
1503
  const lastSpace = truncated.lastIndexOf(" ");
1450
1504
  if (lastSpace > 0) {
1451
1505
  truncated = truncated.slice(0, lastSpace);
@@ -2918,6 +2972,11 @@ var DoculaOptions = class {
2918
2972
  * When true, suppresses all non-error console output during the build.
2919
2973
  */
2920
2974
  quiet = false;
2975
+ /**
2976
+ * URL for the logo/home link in the header. Defaults to baseUrl or "/".
2977
+ * Useful when hosting docs under a subpath but the logo should link to the parent site.
2978
+ */
2979
+ homeUrl;
2921
2980
  /**
2922
2981
  * Base URL path prefix for all generated paths (e.g., "/docs").
2923
2982
  * When set, all asset and navigation URLs are prefixed with this path.
@@ -3074,6 +3133,9 @@ var DoculaOptions = class {
3074
3133
  if (options.cache && typeof options.cache === "object" && options.cache.github !== null && typeof options.cache.github === "object" && typeof options.cache.github.ttl === "number") {
3075
3134
  this.cache = options.cache;
3076
3135
  }
3136
+ if (options.homeUrl !== void 0 && typeof options.homeUrl === "string") {
3137
+ this.homeUrl = options.homeUrl === "/" ? "/" : trimTrailingSlashes(options.homeUrl);
3138
+ }
3077
3139
  if (options.baseUrl !== void 0 && typeof options.baseUrl === "string") {
3078
3140
  this.baseUrl = trimTrailingSlashes(options.baseUrl);
3079
3141
  }
@@ -3229,6 +3291,7 @@ var DoculaBuilder = class {
3229
3291
  cookieAuth: this.options.cookieAuth,
3230
3292
  headerLinks: this.options.headerLinks,
3231
3293
  enableLlmsTxt: this.options.enableLlmsTxt,
3294
+ homeUrl: this.options.homeUrl,
3232
3295
  baseUrl: this.options.baseUrl,
3233
3296
  docsPath: this.options.docsPath,
3234
3297
  apiPath: this.options.apiPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docula",
3
- "version": "1.9.1",
3
+ "version": "1.10.1",
4
4
  "description": "Beautiful Website for Your Projects",
5
5
  "type": "module",
6
6
  "main": "./dist/docula.js",
@@ -48,14 +48,14 @@
48
48
  "colorette": "^2.0.20",
49
49
  "ecto": "^4.8.3",
50
50
  "feed": "^5.2.0",
51
- "hashery": "^1.5.0",
51
+ "hashery": "^1.5.1",
52
52
  "jiti": "^2.6.1",
53
53
  "serve-handler": "^6.1.7",
54
54
  "update-notifier": "^7.3.1",
55
- "writr": "^6.0.1"
55
+ "writr": "^6.1.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@biomejs/biome": "^2.4.7",
58
+ "@biomejs/biome": "^2.4.8",
59
59
  "@playwright/test": "^1.58.2",
60
60
  "@types/express": "^5.0.6",
61
61
  "@types/js-yaml": "^4.0.9",
@@ -68,10 +68,12 @@ body {
68
68
  gap: 12px;
69
69
  }
70
70
 
71
+ .logo-link { display: flex; align-items: center; gap: 8px; text-decoration: none; color: var(--fg); }
71
72
  .logo__img {
72
73
  height: 75px;
73
74
  width: auto;
74
75
  }
76
+ .logo__text { font-size: 18px; font-weight: 600; }
75
77
 
76
78
  .theme-button {
77
79
  border: 1px solid transparent;
@@ -354,8 +356,17 @@ body {
354
356
  .article__main li, .release-body li, .changelog-entry-body li, .home-content li { margin-bottom: 6px; list-style: disc; }
355
357
  .article__main ol li, .release-body ol li, .changelog-entry-body ol li, .home-content ol li { list-style: decimal; }
356
358
  .article__main a, .release-body a, .changelog-entry-body a, .home-content a { color: var(--link); text-decoration: underline; }
357
- .article__main pre, .release-body pre, .changelog-entry-body pre, .home-content pre { background: var(--pre-bg); border-radius: 6px; padding: 12px 16px; margin-bottom: 20px; overflow-x: auto; }
359
+ .article__main pre, .release-body pre, .changelog-entry-body pre, .home-content pre { background: var(--pre-bg); border-radius: 6px; padding: 12px 16px; margin-bottom: 20px; overflow-x: auto; position: relative; }
358
360
  .article__main pre code, .release-body pre code, .changelog-entry-body pre code, .home-content pre code { background: none; padding: 0; font-size: 13.5px; line-height: 1.5; }
361
+ .copy-code-btn { position: absolute; top: 8px; right: 8px; padding: 4px; line-height: 0; border-radius: 4px; background: transparent; color: var(--muted); border: none; cursor: pointer; opacity: 0; transition: opacity 0.15s; }
362
+ pre:hover .copy-code-btn { opacity: 1; }
363
+ .copy-code-btn:hover { color: var(--fg); }
364
+ .article__main img, .changelog-entry-body img { cursor: zoom-in; }
365
+ .lightbox-overlay { display: none; position: fixed; inset: 0; z-index: 200; background: rgba(0, 0, 0, 0.8); justify-content: center; align-items: center; cursor: pointer; }
366
+ .lightbox-overlay--visible { display: flex !important; }
367
+ .lightbox-overlay img { max-width: 90vw; max-height: 90vh; border-radius: 8px; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4); cursor: default; }
368
+ .lightbox-close { position: absolute; top: 16px; right: 16px; background: none; border: none; color: #fff; cursor: pointer; padding: 4px; line-height: 0; }
369
+ .lightbox-close:hover { opacity: 0.7; }
359
370
  .article__main blockquote, .release-body blockquote, .changelog-entry-body blockquote, .home-content blockquote { border-left: 3px solid var(--border-strong); padding: 10px 16px; margin-bottom: 15px; color: var(--muted); }
360
371
  .article__main img, .release-body img, .changelog-entry-body img, .home-content img { max-width: 100%; border-radius: 6px; }
361
372
  .article__main table, .release-body table, .changelog-entry-body table, .home-content table { width: 100%; border-collapse: collapse; margin-bottom: 15px; }
@@ -799,6 +810,11 @@ body {
799
810
  color: var(--muted);
800
811
  }
801
812
 
813
+ .changelog-entry-preview img {
814
+ max-width: 100%;
815
+ border-radius: 6px;
816
+ }
817
+
802
818
  .changelog-read-more {
803
819
  display: inline-block;
804
820
  margin-top: 8px;
@@ -4,10 +4,17 @@
4
4
  <button class="mobile-menu-toggle" id="mobile-menu-toggle" aria-label="Toggle navigation menu">
5
5
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
6
6
  </button>
7
- <a href="{{baseUrl}}/">
7
+ <a href="{{baseUrl}}/" class="logo-link">
8
8
  <img alt="{{siteTitle}}" class="logo__img" src="{{baseUrl}}/logo.svg">
9
+ <span class="logo__text">{{siteTitle}}</span>
9
10
  </a>
10
11
  <nav class="header-bottom__nav">
12
+ {{#if homeUrl}}
13
+ <a class="header-bottom__item header-bottom__item--home" href="{{homeUrl}}">
14
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
15
+ <span>Back to Home</span>
16
+ </a>
17
+ {{/if}}
11
18
  {{#if hasDocuments}}
12
19
  <a class="header-bottom__item" href="{{docsUrl}}/" id="nav-docs">
13
20
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
@@ -67,6 +74,12 @@
67
74
  </header>
68
75
  <aside class="mobile-sidebar" id="mobile-sidebar">
69
76
  <nav class="mobile-nav">
77
+ {{#if homeUrl}}
78
+ <a class="mobile-nav__item" href="{{homeUrl}}">
79
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
80
+ <span>Back to Home</span>
81
+ </a>
82
+ {{/if}}
70
83
  {{#if hasDocuments}}
71
84
  <a class="mobile-nav__item" href="{{docsUrl}}/">
72
85
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
@@ -5,6 +5,25 @@
5
5
  hljs.highlightAll();
6
6
  var kbd = document.querySelector('.search-button__shortcut');
7
7
  if (kbd) kbd.textContent = navigator.platform.indexOf('Mac') > -1 ? '⌘K' : 'Ctrl K';
8
+
9
+ // Copy code buttons
10
+ const copyIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
11
+ const checkIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
12
+ document.querySelectorAll('pre').forEach(function(pre) {
13
+ const btn = document.createElement('button');
14
+ btn.className = 'copy-code-btn';
15
+ btn.innerHTML = copyIcon;
16
+ btn.setAttribute('aria-label', 'Copy code');
17
+ btn.addEventListener('click', function() {
18
+ const code = pre.querySelector('code');
19
+ const text = code ? code.textContent : pre.textContent;
20
+ navigator.clipboard.writeText(text || '').then(function() {
21
+ btn.innerHTML = checkIcon;
22
+ setTimeout(function() { btn.innerHTML = copyIcon; }, 2000);
23
+ });
24
+ });
25
+ pre.appendChild(btn);
26
+ });
8
27
  });
9
28
  </script>
10
29
  <script>
@@ -115,6 +134,7 @@
115
134
  function setAuthUI(loggedIn, displayName) {
116
135
  window.__doculaAuth = { loggedIn: loggedIn, displayName: displayName };
117
136
  try { localStorage.setItem('docula-auth-state', JSON.stringify(window.__doculaAuth)); } catch(e) {}
137
+ document.documentElement.classList.toggle('docula-auth-logged-in', loggedIn);
118
138
  document.dispatchEvent(new CustomEvent('docula-auth-change'));
119
139
  var els = [
120
140
  { login: document.getElementById('cookie-auth-login'), logout: document.getElementById('cookie-auth-logout'), user: document.getElementById('cookie-auth-user') },
@@ -243,5 +263,29 @@
243
263
  const aside = tocSidebar.closest('.content-aside');
244
264
  if (aside) { aside.style.display = 'none'; }
245
265
  }
266
+
267
+ // Image lightbox
268
+ const lightboxOverlay = document.createElement('div');
269
+ lightboxOverlay.className = 'lightbox-overlay';
270
+ lightboxOverlay.addEventListener('click', function(e) {
271
+ if (e.target !== lightboxImg) lightboxOverlay.classList.remove('lightbox-overlay--visible');
272
+ });
273
+ const lightboxClose = document.createElement('button');
274
+ lightboxClose.className = 'lightbox-close';
275
+ lightboxClose.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
276
+ lightboxClose.addEventListener('click', function() {
277
+ lightboxOverlay.classList.remove('lightbox-overlay--visible');
278
+ });
279
+ const lightboxImg = document.createElement('img');
280
+ lightboxOverlay.appendChild(lightboxClose);
281
+ lightboxOverlay.appendChild(lightboxImg);
282
+ document.body.appendChild(lightboxOverlay);
283
+
284
+ document.querySelectorAll('.article__main img, .changelog-entry-body img').forEach(function(img) {
285
+ img.addEventListener('click', function() {
286
+ lightboxImg.src = img.src;
287
+ lightboxOverlay.classList.add('lightbox-overlay--visible');
288
+ });
289
+ });
246
290
  });
247
291
  </script>