docula 0.50.0 → 0.90.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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![tests](https://github.com/jaredwray/docula/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/docula/actions/workflows/tests.yaml)
6
6
  [![GitHub license](https://img.shields.io/github/license/jaredwray/docula)](https://github.com/jaredwray/docula/blob/master/LICENSE)
7
- [![codecov](https://codecov.io/gh/jaredwray/docula/graph/badge.svg?token=RS0GPY4V4M)](https://codecov.io/gh/jaredwray/docula)
7
+ [![codecov](https://codecov.io/gh/jaredwray/docula/branch/main/graph/badge.svg?token=RS0GPY4V4M)](https://codecov.io/gh/jaredwray/docula)
8
8
  [![npm](https://img.shields.io/npm/dm/docula)](https://npmjs.com/package/docula)
9
9
  [![npm](https://img.shields.io/npm/v/docula)](https://npmjs.com/package/docula)
10
10
 
@@ -12,9 +12,12 @@
12
12
  - [Features](#features)
13
13
  - [Open Source Examples](#open-source-examples)
14
14
  - [Getting Started](#getting-started)
15
+ - [Serve your site locally](#serve-your-site-locally)
15
16
  - [TypeScript Configuration](#typescript-configuration)
17
+ - [Theme Mode](#theme-mode)
16
18
  - [Using Your own Template](#using-your-own-template)
17
19
  - [Building Multiple Pages](#building-multiple-pages)
20
+ - [Including Assets in Markdown](#including-assets-in-markdown)
18
21
  - [Public Folder](#public-folder)
19
22
  - [API Reference](#api-reference)
20
23
  - [LLM Files](#llm-files)
@@ -76,6 +79,38 @@ Simply replace the logo, favicon, and css file with your own. The readme is your
76
79
 
77
80
  This will build your site and place it in the `dist` folder. You can then host it anywhere you like.
78
81
 
82
+ ## Serve your site locally
83
+
84
+ > npx docula serve
85
+
86
+ This will build and serve your site locally at `http://localhost:3000`. You can specify a custom port with the `-p` or `--port` flag:
87
+
88
+ > npx docula serve -p 8080
89
+
90
+ ### CLI Options for serve
91
+
92
+ | Flag | Description | Default |
93
+ |------|-------------|---------|
94
+ | `-p, --port` | Set the port number | `3000` |
95
+ | `-s, --site` | Set the path where site files are located | `./site` |
96
+ | `-o, --output` | Set the output directory | `./site/dist` |
97
+ | `-w, --watch` | Watch for changes and rebuild | `false` |
98
+ | `-c, --clean` | Clean the output directory before building | `false` |
99
+
100
+ ### Watch Mode
101
+
102
+ Use the `--watch` flag to automatically rebuild your site when files change:
103
+
104
+ > npx docula serve --watch
105
+
106
+ When watch mode is enabled:
107
+ 1. An initial build runs at startup
108
+ 2. The dev server starts and serves your site
109
+ 3. File changes in `sitePath` (e.g., `./site`) are detected and trigger an automatic rebuild
110
+ 4. Changes in the output directory are ignored to prevent rebuild loops
111
+
112
+ This is useful during development when you want to see changes reflected immediately without manually re-running the build.
113
+
79
114
  # TypeScript Configuration
80
115
 
81
116
  Docula supports TypeScript configuration files (`docula.config.ts`) in addition to JavaScript (`docula.config.mjs`). TypeScript configs provide type safety and better IDE support.
@@ -95,7 +130,7 @@ import type { DoculaOptions } from 'docula';
95
130
 
96
131
  export const options: Partial<DoculaOptions> = {
97
132
  templatePath: './template',
98
- outputPath: './dist',
133
+ output: './dist',
99
134
  sitePath: './site',
100
135
  githubPath: 'your-username/your-repo',
101
136
  siteTitle: 'My Project',
@@ -133,19 +168,34 @@ When both config files exist, Docula loads them in this order (first found wins)
133
168
  | Option | Type | Default | Description |
134
169
  |--------|------|---------|-------------|
135
170
  | `templatePath` | `string` | `'./template'` | Path to custom template directory |
136
- | `outputPath` | `string` | `'./dist'` | Output directory for built site |
171
+ | `output` | `string` | `'./dist'` | Output directory for built site |
137
172
  | `sitePath` | `string` | `'./site'` | Directory containing site content |
138
173
  | `githubPath` | `string` | - | GitHub repository path (e.g., `'user/repo'`) |
139
174
  | `siteTitle` | `string` | `'docula'` | Website title |
140
175
  | `siteDescription` | `string` | - | Website description |
141
176
  | `siteUrl` | `string` | - | Website URL |
142
177
  | `port` | `number` | `3000` | Port for local development server |
143
- | `singlePage` | `boolean` | `true` | Single page or multi-page site |
144
178
  | `homePage` | `boolean` | `true` | When `false`, Docula uses the first docs page as `/index.html` instead of rendering `home.hbs` |
145
179
  | `sections` | `DoculaSection[]` | - | Documentation sections |
146
180
  | `openApiUrl` | `string` | - | OpenAPI spec URL for API documentation (auto-detected if `api/swagger.json` exists) |
147
181
  | `enableReleaseChangelog` | `boolean` | `true` | Convert GitHub releases to changelog entries |
148
182
  | `enableLlmsTxt` | `boolean` | `true` | Generate `llms.txt` and `llms-full.txt` in the build output |
183
+ | `themeMode` | `'light'` \| `'dark'` | - | Override the default theme. By default the site follows the system preference. Set to `'light'` or `'dark'` to use that theme when no user preference is stored. |
184
+ | `allowedAssets` | `string[]` | *(see [Including Assets in Markdown](#including-images-and-assets-in-markdown))* | File extensions to copy from `docs/` and `changelog/` to output |
185
+
186
+ ## Theme Mode
187
+
188
+ By default, the site follows the user's system color scheme preference. The theme toggle cycles through three modes: **system** (follows OS preference), **light**, and **dark**. The user's choice is persisted in `localStorage`.
189
+
190
+ To set the initial theme mode, use `themeMode`:
191
+
192
+ ```js
193
+ export const options = {
194
+ themeMode: 'dark',
195
+ };
196
+ ```
197
+
198
+ When `themeMode` is set, new visitors see the specified theme instead of the system preference. Once a user clicks the theme toggle, their choice takes priority and is remembered across visits.
149
199
 
150
200
  # Using Your own Template
151
201
 
@@ -187,6 +237,70 @@ export const options = {
187
237
  };
188
238
  ```
189
239
 
240
+ # Including Assets in Markdown
241
+
242
+ Non-markdown files placed inside the `docs/` or `changelog/` directories are automatically copied to the build output, preserving their relative paths. This lets you keep images and other assets alongside the markdown that references them.
243
+
244
+ For `docs/`, only assets that are actually referenced in a document's markdown content are copied. If a file exists in the `docs/` directory but is not referenced by any document, it will not be included in the build output. For `changelog/`, all assets are copied regardless of whether they are referenced.
245
+
246
+ ```
247
+ site
248
+ ├───docs
249
+ │ ├───getting-started.md
250
+ │ ├───images
251
+ │ │ ├───architecture.png
252
+ │ │ └───screenshot.jpg
253
+ │ └───assets
254
+ │ └───example.pdf
255
+ ├───changelog
256
+ │ ├───2025-01-15-initial-release.md
257
+ │ └───images
258
+ │ └───release-banner.png
259
+ ```
260
+
261
+ After building, these files appear at the same relative paths under `dist/`:
262
+
263
+ ```
264
+ dist
265
+ ├───docs
266
+ │ ├───getting-started
267
+ │ │ └───index.html
268
+ │ ├───images
269
+ │ │ ├───architecture.png
270
+ │ │ └───screenshot.jpg
271
+ │ └───assets
272
+ │ └───example.pdf
273
+ ├───changelog
274
+ │ ├───initial-release
275
+ │ │ └───index.html
276
+ │ └───images
277
+ │ └───release-banner.png
278
+ ```
279
+
280
+ Reference assets from your markdown using relative paths:
281
+
282
+ ```md
283
+ ![Architecture](images/architecture.png)
284
+ [Download PDF](assets/example.pdf)
285
+ ```
286
+
287
+ ## Supported Extensions
288
+
289
+ By default the following file extensions are copied:
290
+
291
+ **Images:** `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.webp`, `.avif`, `.ico`
292
+ **Documents:** `.pdf`, `.zip`, `.tar`, `.gz`
293
+ **Media:** `.mp4`, `.webm`, `.ogg`, `.mp3`, `.wav`
294
+ **Data:** `.json`, `.xml`, `.csv`, `.txt`
295
+
296
+ Files with extensions not in this list are ignored. To customize the list, set `allowedAssets` in your config:
297
+
298
+ ```js
299
+ export const options = {
300
+ allowedAssets: ['.png', '.jpg', '.gif', '.svg', '.pdf', '.custom'],
301
+ };
302
+ ```
303
+
190
304
  # Public Folder
191
305
 
192
306
  If you have static assets like images, fonts, or other files that need to be copied directly to your built site, you can use a `public` folder. Any files placed in the `public` folder within your site directory will be automatically copied to the root of your `dist` output folder during the build process.
@@ -245,7 +359,7 @@ This is useful for:
245
359
 
246
360
  # API Reference
247
361
 
248
- Docula can generate an API Reference page from an OpenAPI (Swagger) specification. The page is rendered using [Docutopia](https://www.npmjs.com/package/@docutopia/react) and is available at `/api`.
362
+ Docula can generate an API Reference page from an OpenAPI (Swagger) specification. The spec is parsed at build time and rendered as a native, interactive API reference (inspired by [Scalar](https://github.com/scalar/scalar)) with grouped endpoints, method badges, schema tables, code examples, and search — all with no external dependencies. The page is available at `/api`.
249
363
 
250
364
  ## Auto-Detection
251
365
 
package/dist/docula.d.ts CHANGED
@@ -1,11 +1,73 @@
1
+ import fs from 'node:fs';
1
2
  import http from 'node:http';
2
3
  export { Writr } from 'writr';
3
4
 
4
- type DoculaSection = {
5
+ type ApiSchemaProperty = {
5
6
  name: string;
6
- order?: number;
7
+ type: string;
8
+ required: boolean;
9
+ description: string;
10
+ enumValues?: string[];
11
+ };
12
+ type ApiOperationParameter = {
13
+ name: string;
14
+ in: string;
15
+ required: boolean;
16
+ type: string;
17
+ description: string;
18
+ };
19
+ type ApiRequestBody = {
20
+ contentType: string;
21
+ schemaProperties: ApiSchemaProperty[];
22
+ example: string;
23
+ };
24
+ type ApiResponse = {
25
+ statusCode: string;
26
+ statusClass: string;
27
+ description: string;
28
+ contentType: string;
29
+ schemaProperties: ApiSchemaProperty[];
30
+ example: string;
31
+ };
32
+ type ApiCodeExamples = {
33
+ curl: string;
34
+ javascript: string;
35
+ python: string;
36
+ };
37
+ type ApiOperation = {
38
+ id: string;
39
+ method: string;
40
+ methodUpper: string;
7
41
  path: string;
8
- children?: DoculaSection[];
42
+ summary: string;
43
+ description: string;
44
+ parameters: ApiOperationParameter[];
45
+ requestBody?: ApiRequestBody;
46
+ responses: ApiResponse[];
47
+ codeExamples: ApiCodeExamples;
48
+ };
49
+ type ApiGroup = {
50
+ name: string;
51
+ description: string;
52
+ id: string;
53
+ operations: ApiOperation[];
54
+ };
55
+ type ApiSpecData = {
56
+ info: {
57
+ title: string;
58
+ description: string;
59
+ version: string;
60
+ };
61
+ servers: Array<{
62
+ url: string;
63
+ description: string;
64
+ }>;
65
+ groups: ApiGroup[];
66
+ };
67
+
68
+ type GithubData = {
69
+ releases: Record<string, unknown>;
70
+ contributors: Record<string, unknown>;
9
71
  };
10
72
 
11
73
  declare class DoculaOptions {
@@ -20,7 +82,7 @@ declare class DoculaOptions {
20
82
  /**
21
83
  * Path to the output directory
22
84
  */
23
- outputPath: string;
85
+ output: string;
24
86
  /**
25
87
  * Path to the site directory
26
88
  */
@@ -45,10 +107,6 @@ declare class DoculaOptions {
45
107
  * Port to run the server
46
108
  */
47
109
  port: number;
48
- /**
49
- * Single page website
50
- */
51
- singlePage: boolean;
52
110
  /**
53
111
  * Sections
54
112
  */
@@ -73,15 +131,150 @@ declare class DoculaOptions {
73
131
  * When true, generates llms.txt and llms-full.txt files for the built site.
74
132
  */
75
133
  enableLlmsTxt: boolean;
134
+ /**
135
+ * Override the default theme toggle. By default the site follows the system
136
+ * preference. Set to "light" or "dark" to use that theme when no user
137
+ * preference is stored in localStorage.
138
+ */
139
+ themeMode?: "light" | "dark";
140
+ /**
141
+ * When true, automatically adds generated paths (e.g., .cache) to the
142
+ * site directory's .gitignore file if not already present.
143
+ */
144
+ autoUpdateIgnores: boolean;
145
+ /**
146
+ * File extensions to copy as assets from docs/ and changelog/ directories.
147
+ * Override in docula.config to customize.
148
+ */
149
+ allowedAssets: string[];
76
150
  constructor(options?: Record<string, unknown>);
77
151
  parseOptions(options: Record<string, any>): void;
78
152
  }
79
153
 
154
+ type DoculaChangelogEntry = {
155
+ title: string;
156
+ date: string;
157
+ formattedDate: string;
158
+ tag?: string;
159
+ tagClass?: string;
160
+ slug: string;
161
+ content: string;
162
+ generatedHtml: string;
163
+ urlPath: string;
164
+ };
165
+ type DoculaData = {
166
+ siteUrl: string;
167
+ siteTitle: string;
168
+ siteDescription: string;
169
+ sitePath: string;
170
+ templatePath: string;
171
+ output: string;
172
+ githubPath?: string;
173
+ github?: GithubData;
174
+ templates?: DoculaTemplates;
175
+ hasDocuments?: boolean;
176
+ hasChangelog?: boolean;
177
+ sections?: DoculaSection[];
178
+ documents?: DoculaDocument[];
179
+ sidebarItems?: DoculaSection[];
180
+ announcement?: string;
181
+ openApiUrl?: string;
182
+ hasApi?: boolean;
183
+ apiSpec?: ApiSpecData;
184
+ changelogEntries?: DoculaChangelogEntry[];
185
+ homePage?: boolean;
186
+ themeMode?: string;
187
+ };
188
+ type DoculaTemplates = {
189
+ home: string;
190
+ docPage?: string;
191
+ api?: string;
192
+ changelog?: string;
193
+ changelogEntry?: string;
194
+ };
195
+ type DoculaSection = {
196
+ name: string;
197
+ order?: number;
198
+ path: string;
199
+ children?: DoculaSection[];
200
+ };
201
+ type DoculaDocument = {
202
+ title: string;
203
+ navTitle: string;
204
+ description: string;
205
+ order?: number;
206
+ section?: string;
207
+ keywords: string[];
208
+ content: string;
209
+ markdown: string;
210
+ generatedHtml: string;
211
+ documentPath: string;
212
+ urlPath: string;
213
+ isRoot: boolean;
214
+ };
215
+ declare class DoculaBuilder {
216
+ private readonly _options;
217
+ private readonly _ecto;
218
+ private readonly _console;
219
+ constructor(options?: DoculaOptions, engineOptions?: any);
220
+ get options(): DoculaOptions;
221
+ build(): Promise<void>;
222
+ validateOptions(options: DoculaOptions): void;
223
+ getGithubData(githubPath: string): Promise<GithubData>;
224
+ getTemplates(templatePath: string, hasDocuments: boolean, hasChangelog?: boolean): Promise<DoculaTemplates>;
225
+ getTemplateFile(path: string, name: string): Promise<string | undefined>;
226
+ buildRobotsPage(options: DoculaOptions): Promise<void>;
227
+ buildSiteMapPage(data: DoculaData): Promise<void>;
228
+ buildLlmsFiles(data: DoculaData): Promise<void>;
229
+ private generateLlmsIndexContent;
230
+ private generateLlmsFullContent;
231
+ private buildAbsoluteSiteUrl;
232
+ private normalizePathForUrl;
233
+ private isRemoteUrl;
234
+ private resolveOpenApiSpecUrl;
235
+ private resolveLocalOpenApiPath;
236
+ private getSafeSiteOverrideFileContent;
237
+ private getSafeLocalOpenApiSpec;
238
+ private isPathWithinBasePath;
239
+ private toPosixPath;
240
+ buildIndexPage(data: DoculaData): Promise<void>;
241
+ buildDocsHomePage(data: DoculaData): Promise<void>;
242
+ buildReadmeSection(data: DoculaData): Promise<string>;
243
+ buildAnnouncementSection(data: DoculaData): Promise<string | undefined>;
244
+ buildDocsPages(data: DoculaData): Promise<void>;
245
+ buildApiPage(data: DoculaData): Promise<void>;
246
+ getChangelogEntries(changelogPath: string): DoculaChangelogEntry[];
247
+ parseChangelogEntry(filePath: string): DoculaChangelogEntry;
248
+ convertReleaseToChangelogEntry(release: Record<string, any>): DoculaChangelogEntry;
249
+ getReleasesAsChangelogEntries(releases: any[]): DoculaChangelogEntry[];
250
+ buildChangelogPage(data: DoculaData): Promise<void>;
251
+ buildChangelogEntryPages(data: DoculaData): Promise<void>;
252
+ generateSidebarItems(data: DoculaData): DoculaSection[];
253
+ getDocuments(sitePath: string, doculaData: DoculaData): DoculaDocument[];
254
+ getDocumentInDirectory(sitePath: string): DoculaDocument[];
255
+ getSections(sitePath: string, doculaOptions: DoculaOptions): DoculaSection[];
256
+ mergeSectionWithOptions(section: DoculaSection, options: DoculaOptions): DoculaSection;
257
+ parseDocumentData(documentPath: string): DoculaDocument;
258
+ private hasTableOfContents;
259
+ private directoryContainsMarkdown;
260
+ private mergeTemplateOverrides;
261
+ private ensureCacheInGitignore;
262
+ private isCacheFresh;
263
+ private listFilesRecursive;
264
+ private copyDirectory;
265
+ private copyPublicFolder;
266
+ private copyPublicDirectory;
267
+ private copyDocumentSiblingAssets;
268
+ private listContentAssets;
269
+ private copyContentAssets;
270
+ }
271
+
80
272
  declare class Docula {
81
273
  private _options;
82
274
  private readonly _console;
83
275
  private _configFileModule;
84
276
  private _server;
277
+ private _watcher;
85
278
  /**
86
279
  * Initialize the Docula class
87
280
  * @param {DoculaOptions} options
@@ -104,11 +297,23 @@ declare class Docula {
104
297
  * @returns {http.Server | undefined}
105
298
  */
106
299
  get server(): http.Server | undefined;
300
+ /**
301
+ * The file watcher used in watch mode
302
+ * @returns {fs.FSWatcher | undefined}
303
+ */
304
+ get watcher(): fs.FSWatcher | undefined;
107
305
  /**
108
306
  * The config file module. This is the module that is loaded from the docula.config.ts or docula.config.mjs file
109
307
  * @returns {any}
110
308
  */
111
309
  get configFileModule(): any;
310
+ /**
311
+ * Remove the .cache directory inside the site path.
312
+ * Resolves the path and verifies it stays within sitePath to prevent
313
+ * path-traversal attacks.
314
+ * @param {string} sitePath
315
+ */
316
+ private cleanCache;
112
317
  /**
113
318
  * Check for updates
114
319
  * @returns {void}
@@ -120,12 +325,6 @@ declare class Docula {
120
325
  * @returns {Promise<void>}
121
326
  */
122
327
  execute(process: NodeJS.Process): Promise<void>;
123
- /**
124
- * Checks if the site is a single page website
125
- * @param {string} sitePath
126
- * @returns {boolean}
127
- */
128
- isSinglePageWebsite(sitePath: string): boolean;
129
328
  /**
130
329
  * Generate the init files
131
330
  * @param {string} sitePath
@@ -145,6 +344,13 @@ declare class Docula {
145
344
  * @returns {Promise<void>}
146
345
  */
147
346
  loadConfigFile(sitePath: string): Promise<void>;
347
+ /**
348
+ * Watch the site path for file changes and rebuild on change
349
+ * @param {DoculaOptions} options
350
+ * @param {DoculaBuilder} builder
351
+ * @returns {fs.FSWatcher}
352
+ */
353
+ watch(options: DoculaOptions, builder: DoculaBuilder): fs.FSWatcher;
148
354
  /**
149
355
  * Serve the site based on the options (port and output path)
150
356
  * @param {DoculaOptions} options
@@ -153,4 +359,4 @@ declare class Docula {
153
359
  serve(options: DoculaOptions): Promise<http.Server>;
154
360
  }
155
361
 
156
- export { Docula as default };
362
+ export { DoculaOptions, Docula as default };