docula 0.9.0 → 0.9.2

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.js CHANGED
@@ -1,148 +1,841 @@
1
- import path from 'node:path';
2
- import process from 'node:process';
3
- import fs from 'node:fs';
4
- import updateNotifier from 'update-notifier';
5
- import express from 'express';
6
- import { DoculaOptions } from './options.js';
7
- import { DoculaConsole } from './console.js';
8
- import { DoculaBuilder } from './builder.js';
9
- export default class Docula {
10
- _options = new DoculaOptions();
11
- _console = new DoculaConsole();
12
- _configFileModule = {};
13
- _server;
14
- constructor(options) {
15
- if (options) {
16
- this._options = options;
17
- }
1
+ // src/docula.ts
2
+ import path3 from "node:path";
3
+ import process3 from "node:process";
4
+ import fs3 from "node:fs";
5
+ import updateNotifier from "update-notifier";
6
+ import express from "express";
7
+
8
+ // src/options.ts
9
+ import path from "node:path";
10
+ import process from "node:process";
11
+ var DoculaOptions = class {
12
+ templatePath = path.join(import.meta.url, "../../template").replace("file:", "");
13
+ outputPath = path.join(process.cwd(), "./dist");
14
+ sitePath = path.join(process.cwd(), "./site");
15
+ githubPath = "jaredwray/docula";
16
+ siteTitle = "docula";
17
+ siteDescription = "Beautiful Website for Your Projects";
18
+ siteUrl = "https://docula.org";
19
+ port = 3e3;
20
+ singlePage = true;
21
+ sections;
22
+ constructor(options) {
23
+ if (options) {
24
+ this.parseOptions(options);
18
25
  }
19
- get options() {
20
- return this._options;
26
+ }
27
+ parseOptions(options) {
28
+ if (options.templatePath) {
29
+ this.templatePath = options.templatePath;
30
+ this.templatePath = path.join(process.cwd(), this.templatePath);
21
31
  }
22
- set options(value) {
23
- this._options = value;
32
+ if (options.outputPath) {
33
+ this.outputPath = options.outputPath;
34
+ this.githubPath = path.join(process.cwd(), this.outputPath);
24
35
  }
25
- get server() {
26
- return this._server;
36
+ if (options.sitePath) {
37
+ this.sitePath = options.sitePath;
38
+ this.sitePath = path.join(process.cwd(), this.sitePath);
27
39
  }
28
- get configFileModule() {
29
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
30
- return this._configFileModule;
40
+ if (options.githubPath) {
41
+ this.githubPath = options.githubPath;
31
42
  }
32
- checkForUpdates() {
33
- const packageJsonPath = path.join(process.cwd(), 'package.json');
34
- if (fs.existsSync(packageJsonPath)) {
35
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
36
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
37
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
38
- updateNotifier({ pkg: packageJson }).notify();
39
- }
43
+ if (options.siteTitle) {
44
+ this.siteTitle = options.siteTitle;
45
+ }
46
+ if (options.siteDescription) {
47
+ this.siteDescription = options.siteDescription;
48
+ }
49
+ if (options.siteUrl) {
50
+ this.siteUrl = options.siteUrl;
40
51
  }
41
- async execute(process) {
42
- // Check for updates
43
- this.checkForUpdates();
44
- const consoleProcess = this._console.parseProcessArgv(process.argv);
45
- // Automatic detect singlePage option
46
- this.options.singlePage = this.isSinglePageWebsite(this.options.sitePath);
47
- // Update options
48
- if (consoleProcess.args.sitePath) {
49
- this.options.sitePath = consoleProcess.args.sitePath;
52
+ if (options.sections) {
53
+ this.sections = options.sections;
54
+ }
55
+ if (options.port) {
56
+ this.port = options.port;
57
+ }
58
+ if (options.singlePage !== void 0 && typeof options.singlePage === "boolean") {
59
+ this.singlePage = options.singlePage;
60
+ }
61
+ }
62
+ };
63
+
64
+ // src/console.ts
65
+ import path2 from "node:path";
66
+ import process2 from "node:process";
67
+ var DoculaConsole = class {
68
+ log(message) {
69
+ console.log(message);
70
+ }
71
+ error(message) {
72
+ console.error(message);
73
+ }
74
+ warn(message) {
75
+ console.warn(message);
76
+ }
77
+ printHelp() {
78
+ console.log(" Usage: docula [command] [arguments]");
79
+ console.log();
80
+ console.log(" Commands:");
81
+ console.log(" init Initialize a new project");
82
+ console.log(" build Build the project. By default just npx docula will build the project if it finds a ./site folder");
83
+ console.log(" serve Serve the project as a local website");
84
+ console.log(" help Print this help");
85
+ console.log(" version Print the version");
86
+ console.log();
87
+ console.log(" Arguments Build:");
88
+ console.log(" -w, --watch watch for changes and rebuild");
89
+ console.log(" -s, --site Set the path where site files are located");
90
+ console.log(" -o, --outputPath Set the output directory. Default is ./site/dist");
91
+ console.log(" -t, --templatePath Set the custom template to use");
92
+ console.log();
93
+ console.log(" Arguments serve:");
94
+ console.log(" -p, --port Set the port number used with serve");
95
+ console.log(" -w, --watch watch for changes and rebuild");
96
+ console.log(" -s, --site Set the path where site files are located");
97
+ }
98
+ parseProcessArgv(argv) {
99
+ const command = this.getCommand(argv);
100
+ const arguments_ = this.getArguments(argv);
101
+ return {
102
+ argv,
103
+ command,
104
+ args: arguments_
105
+ };
106
+ }
107
+ getCommand(argv) {
108
+ let result;
109
+ for (const argument of argv) {
110
+ switch (argument) {
111
+ case "init": {
112
+ result = "init";
113
+ break;
50
114
  }
51
- // Load the Config File
52
- await this.loadConfigFile(this.options.sitePath);
53
- // Parse the config file
54
- if (this._configFileModule.options) {
55
- this.options.parseOptions(this._configFileModule.options);
115
+ case "build": {
116
+ result = "build";
117
+ break;
56
118
  }
57
- // Run the onPrepare function
58
- if (this._configFileModule.onPrepare) {
59
- try {
60
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
61
- await this._configFileModule.onPrepare(this.options);
62
- }
63
- catch (error) {
64
- this._console.error(error.message);
65
- }
119
+ case "serve": {
120
+ result = "serve";
121
+ break;
66
122
  }
67
- if (consoleProcess.args.output) {
68
- this.options.outputPath = consoleProcess.args.output;
123
+ case "help": {
124
+ result = "help";
125
+ break;
69
126
  }
70
- switch (consoleProcess.command) {
71
- case 'init': {
72
- this.generateInit(this.options.sitePath);
73
- break;
74
- }
75
- case 'help': {
76
- this._console.printHelp();
77
- break;
78
- }
79
- case 'version': {
80
- this._console.log(this.getVersion());
81
- break;
82
- }
83
- case 'serve': {
84
- const builder = new DoculaBuilder(this.options);
85
- await builder.build();
86
- await this.serve(this.options);
87
- break;
88
- }
89
- default: {
90
- const builder = new DoculaBuilder(this.options);
91
- await builder.build();
92
- break;
93
- }
127
+ case "version": {
128
+ result = argument;
129
+ break;
94
130
  }
131
+ }
95
132
  }
96
- isSinglePageWebsite(sitePath) {
97
- const documentationPath = `${sitePath}/docs`;
98
- if (!fs.existsSync(documentationPath)) {
99
- return true;
133
+ return result;
134
+ }
135
+ getArguments(argv) {
136
+ const arguments_ = {
137
+ sitePath: "",
138
+ templatePath: "",
139
+ output: "",
140
+ watch: false,
141
+ port: 3e3
142
+ };
143
+ for (let i = 0; i < argv.length; i++) {
144
+ const argument = argv[i];
145
+ switch (argument) {
146
+ case "-p":
147
+ case "--port": {
148
+ const portString = argv[i + 1];
149
+ if (portString !== void 0) {
150
+ arguments_.port = Number.parseInt(portString, 10);
151
+ }
152
+ break;
100
153
  }
101
- const files = fs.readdirSync(documentationPath);
102
- return files.length === 0;
103
- }
104
- generateInit(sitePath) {
105
- // Check if the site path exists
106
- if (!fs.existsSync(sitePath)) {
107
- fs.mkdirSync(sitePath);
154
+ case "-o":
155
+ case "--output": {
156
+ arguments_.output = argv[i + 1];
157
+ arguments_.output = path2.join(process2.cwd(), arguments_.output);
158
+ break;
108
159
  }
109
- // Add the docula.config file based on js or ts
110
- const doculaConfigFile = './init/docula.config.cjs';
111
- fs.copyFileSync(doculaConfigFile, `${sitePath}/docula.config.cjs`);
112
- // Add in the image and favicon
113
- fs.copyFileSync('./init/logo.png', `${sitePath}/logo.png`);
114
- fs.copyFileSync('./init/favicon.ico', `${sitePath}/favicon.ico`);
115
- // Add in the variables file
116
- fs.copyFileSync('./init/variables.css', `${sitePath}/variables.css`);
117
- // Output the instructions
118
- this._console.log(`docula initialized. Please update the ${doculaConfigFile} file with your site information. In addition, you can replace the image, favicon, and stype the site with site.css file.`);
119
- }
120
- getVersion() {
121
- const packageJson = fs.readFileSync('./package.json', 'utf8');
122
- const packageObject = JSON.parse(packageJson);
123
- return packageObject.version;
124
- }
125
- async loadConfigFile(sitePath) {
126
- if (fs.existsSync(sitePath)) {
127
- const configFile = `${sitePath}/docula.config.cjs`;
128
- if (fs.existsSync(configFile)) {
129
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
130
- this._configFileModule = await import(configFile);
131
- }
160
+ case "-w":
161
+ case "--watch": {
162
+ arguments_.watch = true;
163
+ break;
132
164
  }
165
+ case "-s":
166
+ case "--site": {
167
+ arguments_.sitePath = argv[i + 1];
168
+ arguments_.sitePath = path2.join(process2.cwd(), arguments_.sitePath);
169
+ break;
170
+ }
171
+ case "-t":
172
+ case "--templatePath": {
173
+ arguments_.templatePath = argv[i + 1];
174
+ arguments_.templatePath = path2.join(process2.cwd(), arguments_.templatePath);
175
+ break;
176
+ }
177
+ }
178
+ }
179
+ return arguments_;
180
+ }
181
+ };
182
+
183
+ // src/builder.ts
184
+ import fs from "node:fs";
185
+ import { Ecto } from "ecto";
186
+ import * as matter from "gray-matter";
187
+ import he from "he";
188
+ import * as cheerio from "cheerio";
189
+
190
+ // src/github.ts
191
+ import axios from "axios";
192
+ var Github = class {
193
+ options = {
194
+ api: "https://api.github.com",
195
+ author: "",
196
+ repo: ""
197
+ };
198
+ constructor(options) {
199
+ this.parseOptions(options);
200
+ }
201
+ async getData() {
202
+ const data = {
203
+ releases: {},
204
+ contributors: {}
205
+ };
206
+ data.releases = await this.getReleases();
207
+ data.contributors = await this.getContributors();
208
+ return data;
209
+ }
210
+ async getReleases() {
211
+ const url = `${this.options.api}/repos/${this.options.author}/${this.options.repo}/releases`;
212
+ try {
213
+ const result = await axios.get(url);
214
+ if (result && result.data.length > 0) {
215
+ return this.addAnchorLink(result.data);
216
+ }
217
+ return [];
218
+ } catch (error) {
219
+ const typedError = error;
220
+ if (typedError.response?.status === 404) {
221
+ throw new Error(`Repository ${this.options.author}/${this.options.repo} not found.`);
222
+ }
223
+ throw error;
224
+ }
225
+ }
226
+ async getContributors() {
227
+ const url = `${this.options.api}/repos/${this.options.author}/${this.options.repo}/contributors`;
228
+ try {
229
+ const result = await axios.get(url);
230
+ if (result && result.data.length > 0) {
231
+ return result.data;
232
+ }
233
+ } catch (error) {
234
+ const typedError = error;
235
+ if (typedError.response?.status === 404) {
236
+ throw new Error(`Repository ${this.options.author}/${this.options.repo} not found.`);
237
+ }
238
+ throw error;
239
+ }
240
+ }
241
+ parseOptions(options) {
242
+ if (options.api) {
243
+ this.options.api = options.api;
244
+ }
245
+ this.options.author = options.author;
246
+ this.options.repo = options.repo;
247
+ }
248
+ addAnchorLink(data) {
249
+ return data.map((release) => {
250
+ const regex = /(?<!]\()(https:\/\/[\w./]+)(?!\))/g;
251
+ release.body = release.body.replaceAll(regex, "[$1]($1)");
252
+ return release;
253
+ });
254
+ }
255
+ };
256
+
257
+ // src/builder.ts
258
+ var DoculaBuilder = class {
259
+ _options = new DoculaOptions();
260
+ _ecto;
261
+ _console = new DoculaConsole();
262
+ constructor(options, engineOptions) {
263
+ if (options) {
264
+ this._options = options;
265
+ }
266
+ this._ecto = new Ecto(engineOptions);
267
+ }
268
+ get options() {
269
+ return this._options;
270
+ }
271
+ async build() {
272
+ const startTime = Date.now();
273
+ this.validateOptions(this.options);
274
+ const doculaData = {
275
+ siteUrl: this.options.siteUrl,
276
+ siteTitle: this.options.siteTitle,
277
+ siteDescription: this.options.siteDescription,
278
+ sitePath: this.options.sitePath,
279
+ templatePath: this.options.templatePath,
280
+ outputPath: this.options.outputPath,
281
+ githubPath: this.options.githubPath,
282
+ sections: this.options.sections
283
+ };
284
+ doculaData.github = await this.getGithubData(this.options.githubPath);
285
+ doculaData.documents = this.getDocuments(`${doculaData.sitePath}/docs`, doculaData);
286
+ doculaData.sections = this.getSections(`${doculaData.sitePath}/docs`, this.options);
287
+ doculaData.hasDocuments = doculaData.documents?.length > 0;
288
+ doculaData.templates = await this.getTemplates(this.options, doculaData.hasDocuments);
289
+ await this.buildIndexPage(doculaData);
290
+ await this.buildReleasePage(doculaData);
291
+ await this.buildSiteMapPage(doculaData);
292
+ await this.buildRobotsPage(this.options);
293
+ if (doculaData.hasDocuments) {
294
+ await this.buildDocsPages(doculaData);
295
+ }
296
+ const siteRelativePath = this.options.sitePath;
297
+ if (fs.existsSync(`${siteRelativePath}/favicon.ico`)) {
298
+ await fs.promises.copyFile(
299
+ `${siteRelativePath}/favicon.ico`,
300
+ `${this.options.outputPath}/favicon.ico`
301
+ );
302
+ }
303
+ if (fs.existsSync(`${siteRelativePath}/logo.svg`)) {
304
+ await fs.promises.copyFile(
305
+ `${siteRelativePath}/logo.svg`,
306
+ `${this.options.outputPath}/logo.svg`
307
+ );
308
+ }
309
+ if (fs.existsSync(`${siteRelativePath}/logo_horizontal.png`)) {
310
+ await fs.promises.copyFile(
311
+ `${siteRelativePath}/logo_horizontal.png`,
312
+ `${this.options.outputPath}/logo_horizontal.png`
313
+ );
314
+ }
315
+ if (fs.existsSync(`${this.options.templatePath}/css`)) {
316
+ this.copyDirectory(
317
+ `${this.options.templatePath}/css`,
318
+ `${this.options.outputPath}/css`
319
+ );
320
+ }
321
+ if (fs.existsSync(`${siteRelativePath}/variables.css`)) {
322
+ await fs.promises.copyFile(
323
+ `${siteRelativePath}/variables.css`,
324
+ `${this.options.outputPath}/css/variables.css`
325
+ );
326
+ }
327
+ const endTime = Date.now();
328
+ const executionTime = endTime - startTime;
329
+ this._console.log(`Build completed in ${executionTime}ms`);
330
+ }
331
+ validateOptions(options) {
332
+ if (options.githubPath.length < 3) {
333
+ throw new Error("No github options provided");
334
+ }
335
+ if (options.siteDescription.length < 3) {
336
+ throw new Error("No site description options provided");
337
+ }
338
+ if (!options.siteTitle) {
339
+ throw new Error("No site title options provided");
340
+ }
341
+ if (!options.siteUrl) {
342
+ throw new Error("No site url options provided");
343
+ }
344
+ }
345
+ async getGithubData(githubPath) {
346
+ const paths = githubPath.split("/");
347
+ const options = {
348
+ author: paths[0],
349
+ repo: paths[1]
350
+ };
351
+ const github = new Github(options);
352
+ return github.getData();
353
+ }
354
+ async getTemplates(options, hasDocuments) {
355
+ const templates = {
356
+ index: "",
357
+ releases: ""
358
+ };
359
+ if (fs.existsSync(options.templatePath)) {
360
+ const index = await this.getTemplateFile(options.templatePath, "index");
361
+ if (index) {
362
+ templates.index = index;
363
+ }
364
+ const releases = await this.getTemplateFile(
365
+ options.templatePath,
366
+ "releases"
367
+ );
368
+ if (releases) {
369
+ templates.releases = releases;
370
+ }
371
+ const documentPage = hasDocuments ? await this.getTemplateFile(
372
+ options.templatePath,
373
+ "docs"
374
+ ) : void 0;
375
+ if (documentPage) {
376
+ templates.docPage = documentPage;
377
+ }
378
+ } else {
379
+ throw new Error(`No template path found at ${options.templatePath}`);
380
+ }
381
+ return templates;
382
+ }
383
+ async getTemplateFile(path4, name) {
384
+ let result;
385
+ const files = await fs.promises.readdir(path4);
386
+ for (const file of files) {
387
+ const fileName = file.split(".");
388
+ if (fileName[0].toString().toLowerCase() === name.toLowerCase()) {
389
+ result = file.toString();
390
+ break;
391
+ }
392
+ }
393
+ return result;
394
+ }
395
+ async buildRobotsPage(options) {
396
+ const { sitePath } = options;
397
+ const { outputPath } = options;
398
+ const robotsPath = `${outputPath}/robots.txt`;
399
+ await fs.promises.mkdir(outputPath, { recursive: true });
400
+ await (fs.existsSync(`${sitePath}/robots.txt`) ? fs.promises.copyFile(`${sitePath}/robots.txt`, robotsPath) : fs.promises.writeFile(robotsPath, "User-agent: *\nDisallow:"));
401
+ }
402
+ async buildSiteMapPage(data) {
403
+ const sitemapPath = `${data.outputPath}/sitemap.xml`;
404
+ const urls = [{ url: data.siteUrl }, { url: `${data.siteUrl}/releases` }];
405
+ for (const document of data.documents ?? []) {
406
+ let { urlPath } = document;
407
+ if (urlPath.endsWith("index.html")) {
408
+ urlPath = urlPath.slice(0, -10);
409
+ }
410
+ urls.push({ url: `${data.siteUrl}${urlPath}` });
411
+ }
412
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>';
413
+ xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
414
+ for (const { url } of urls) {
415
+ xml += "<url>";
416
+ xml += `<loc>${url}</loc>`;
417
+ xml += "</url>";
418
+ }
419
+ xml += "</urlset>";
420
+ await fs.promises.mkdir(data.outputPath, { recursive: true });
421
+ await fs.promises.writeFile(sitemapPath, xml, "utf8");
422
+ }
423
+ async buildIndexPage(data) {
424
+ if (data.templates) {
425
+ const indexPath = `${data.outputPath}/index.html`;
426
+ await fs.promises.mkdir(data.outputPath, { recursive: true });
427
+ const indexTemplate = `${data.templatePath}/${data.templates.index}`;
428
+ let content;
429
+ if (!data.hasDocuments) {
430
+ content = await this.buildReadmeSection(data);
431
+ }
432
+ const indexContent = await this._ecto.renderFromFile(
433
+ indexTemplate,
434
+ { ...data, content },
435
+ data.templatePath
436
+ );
437
+ await fs.promises.writeFile(indexPath, indexContent, "utf8");
438
+ } else {
439
+ throw new Error("No templates found");
133
440
  }
134
- async serve(options) {
135
- if (this._server) {
136
- this._server.close();
441
+ }
442
+ async buildReleasePage(data) {
443
+ if (data.github && data.templates) {
444
+ const releasesPath = `${data.outputPath}/releases/index.html`;
445
+ const releaseOutputPath = `${data.outputPath}/releases`;
446
+ await fs.promises.mkdir(releaseOutputPath, { recursive: true });
447
+ const releasesTemplate = `${data.templatePath}/${data.templates.releases}`;
448
+ const releasesContent = await this._ecto.renderFromFile(
449
+ releasesTemplate,
450
+ data,
451
+ data.templatePath
452
+ );
453
+ await fs.promises.writeFile(releasesPath, releasesContent, "utf8");
454
+ } else {
455
+ throw new Error("No github data found");
456
+ }
457
+ }
458
+ async buildReadmeSection(data) {
459
+ let htmlReadme = "";
460
+ if (fs.existsSync(`${data.sitePath}/README.md`)) {
461
+ const readmeContent = fs.readFileSync(
462
+ `${data.sitePath}/README.md`,
463
+ "utf8"
464
+ );
465
+ htmlReadme = await this._ecto.render(readmeContent, void 0, "markdown");
466
+ }
467
+ return htmlReadme;
468
+ }
469
+ async buildDocsPages(data) {
470
+ if (data.templates && data.documents?.length) {
471
+ const documentsTemplate = `${data.templatePath}/${data.templates.docPage}`;
472
+ await fs.promises.mkdir(`${data.outputPath}/docs`, { recursive: true });
473
+ data.sidebarItems = this.generateSidebarItems(data);
474
+ const promises = data.documents.map(async (document) => {
475
+ const folder = document.urlPath.split("/").slice(0, -1).join("/");
476
+ await fs.promises.mkdir(`${data.outputPath}/${folder}`, { recursive: true });
477
+ const slug = `${data.outputPath}${document.urlPath}`;
478
+ let documentContent = await this._ecto.renderFromFile(
479
+ documentsTemplate,
480
+ { ...data, ...document },
481
+ data.templatePath
482
+ );
483
+ documentContent = he.decode(documentContent);
484
+ return fs.promises.writeFile(slug, documentContent, "utf8");
485
+ });
486
+ await Promise.all(promises);
487
+ } else {
488
+ throw new Error("No templates found");
489
+ }
490
+ }
491
+ generateSidebarItems(data) {
492
+ let sidebarItems = [...data.sections ?? []];
493
+ for (const document of data.documents ?? []) {
494
+ if (document.isRoot) {
495
+ sidebarItems.unshift({
496
+ path: document.urlPath.replace("index.html", ""),
497
+ name: document.navTitle,
498
+ order: document.order
499
+ });
500
+ } else {
501
+ const relativeFilePath = document.documentPath.replace(`${data.sitePath}/docs/`, "");
502
+ const sectionPath = relativeFilePath.slice(0, Math.max(0, relativeFilePath.lastIndexOf("/")));
503
+ const documentSection = document.section ?? sectionPath;
504
+ const sectionIndex = sidebarItems.findIndex((section) => section.path === documentSection);
505
+ if (sectionIndex === -1) {
506
+ continue;
137
507
  }
138
- const app = express();
139
- const { port } = options;
140
- const { outputPath } = options;
141
- app.use(express.static(outputPath));
142
- this._server = app.listen(port, () => {
143
- this._console.log(`Docula 🦇 at http://localhost:${port}`);
508
+ sidebarItems[sectionIndex].children ??= [];
509
+ sidebarItems[sectionIndex].children.push({
510
+ path: document.urlPath.replace("index.html", ""),
511
+ name: document.navTitle,
512
+ order: document.order
144
513
  });
514
+ }
515
+ }
516
+ sidebarItems = sidebarItems.map((section) => {
517
+ if (section.children) {
518
+ section.children.sort((a, b) => (a.order ?? section.children.length) - (b.order ?? section.children.length));
519
+ }
520
+ return section;
521
+ });
522
+ sidebarItems.sort((a, b) => (a.order ?? sidebarItems.length) - (b.order ?? sidebarItems.length));
523
+ return sidebarItems;
524
+ }
525
+ getDocuments(sitePath, doculaData) {
526
+ let documents = new Array();
527
+ if (fs.existsSync(sitePath)) {
528
+ documents = this.getDocumentInDirectory(sitePath);
529
+ doculaData.sections = this.getSections(sitePath, this.options);
530
+ for (const section of doculaData.sections) {
531
+ const sectionPath = `${sitePath}/${section.path}`;
532
+ const sectionDocuments = this.getDocumentInDirectory(sectionPath);
533
+ documents = [...documents, ...sectionDocuments];
534
+ }
535
+ }
536
+ return documents;
537
+ }
538
+ getDocumentInDirectory(sitePath) {
539
+ const documents = new Array();
540
+ const documentList = fs.readdirSync(sitePath);
541
+ if (documentList.length > 0) {
542
+ for (const document of documentList) {
543
+ const documentPath = `${sitePath}/${document}`;
544
+ const stats = fs.statSync(documentPath);
545
+ if (stats.isFile()) {
546
+ const documentData = this.parseDocumentData(documentPath);
547
+ documents.push(documentData);
548
+ }
549
+ }
550
+ }
551
+ documents.sort((a, b) => (a.order ?? documents.length) - (b.order ?? documents.length));
552
+ return documents;
553
+ }
554
+ getSections(sitePath, doculaOptions) {
555
+ const sections = new Array();
556
+ if (fs.existsSync(sitePath)) {
557
+ const documentList = fs.readdirSync(sitePath);
558
+ if (documentList.length > 0) {
559
+ for (const document of documentList) {
560
+ const documentPath = `${sitePath}/${document}`;
561
+ const stats = fs.statSync(documentPath);
562
+ if (stats.isDirectory()) {
563
+ const section = {
564
+ name: document.replaceAll("-", " ").replaceAll(/\b\w/g, (l) => l.toUpperCase()),
565
+ path: document
566
+ };
567
+ this.mergeSectionWithOptions(section, doculaOptions);
568
+ sections.push(section);
569
+ }
570
+ }
571
+ }
572
+ sections.sort((a, b) => (a.order ?? sections.length) - (b.order ?? sections.length));
573
+ }
574
+ return sections;
575
+ }
576
+ mergeSectionWithOptions(section, options) {
577
+ if (options.sections) {
578
+ const sectionOptions = options.sections.find(
579
+ (sectionOption) => sectionOption.path === section.path
580
+ );
581
+ if (sectionOptions) {
582
+ section.name = sectionOptions.name;
583
+ section.order = sectionOptions.order;
584
+ section.path = sectionOptions.path;
585
+ }
586
+ }
587
+ return section;
588
+ }
589
+ parseDocumentData(documentPath) {
590
+ const documentContent = fs.readFileSync(documentPath, "utf8");
591
+ const matterData = matter.default(documentContent);
592
+ let markdownContent = this.removeFrontmatter(documentContent);
593
+ markdownContent = markdownContent.replace(/^# .*\n/, "");
594
+ const documentsFolderIndex = documentPath.lastIndexOf("/docs/");
595
+ let urlPath = documentPath.slice(documentsFolderIndex).replace(".md", "/index.html");
596
+ let isRoot = urlPath.split("/").length === 3;
597
+ if (!documentPath.slice(documentsFolderIndex + 6).includes("/")) {
598
+ isRoot = true;
599
+ const filePath = documentPath.slice(documentsFolderIndex + 6);
600
+ if (filePath === "index.md") {
601
+ urlPath = documentPath.slice(documentsFolderIndex).replace(".md", ".html");
602
+ }
603
+ }
604
+ return {
605
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
606
+ title: matterData.data.title,
607
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
608
+ navTitle: matterData.data.navTitle || matterData.data.title,
609
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
610
+ description: matterData.data.description || "",
611
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
612
+ order: matterData.data.order || void 0,
613
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
614
+ section: matterData.data.section || void 0,
615
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
616
+ keywords: matterData.data.keywords || [],
617
+ content: documentContent,
618
+ markdown: markdownContent,
619
+ generatedHtml: this._ecto.renderSync(markdownContent, void 0, "markdown"),
620
+ tableOfContents: this.getTableOfContents(markdownContent),
621
+ documentPath,
622
+ urlPath,
623
+ isRoot
624
+ };
625
+ }
626
+ removeFrontmatter(markdown) {
627
+ return markdown.replace(/^-{3}[\s\S]*?-{3}\s*/, "");
628
+ }
629
+ getTableOfContents(markdown) {
630
+ markdown = `## Table of Contents
631
+
632
+ ${markdown}`;
633
+ const html = this._ecto.renderSync(markdown, void 0, "markdown");
634
+ const $ = cheerio.load(html);
635
+ const tocTitle = $("h2").first();
636
+ const tocContent = tocTitle.next("ul").toString();
637
+ if (tocContent) {
638
+ return tocTitle.toString() + tocContent;
639
+ }
640
+ return void 0;
641
+ }
642
+ copyDirectory(source, target) {
643
+ const files = fs.readdirSync(source);
644
+ for (const file of files) {
645
+ if (file.startsWith(".")) {
646
+ continue;
647
+ }
648
+ const sourcePath = `${source}/${file}`;
649
+ const targetPath = `${target}/${file}`;
650
+ const stat = fs.lstatSync(sourcePath);
651
+ if (stat.isDirectory()) {
652
+ fs.mkdirSync(targetPath, { recursive: true });
653
+ this.copyDirectory(sourcePath, targetPath);
654
+ } else {
655
+ fs.mkdirSync(target, { recursive: true });
656
+ fs.copyFileSync(sourcePath, targetPath);
657
+ }
658
+ }
659
+ }
660
+ };
661
+
662
+ // src/helpers.ts
663
+ import fs2 from "node:fs";
664
+ import { load as load2, dump } from "js-yaml";
665
+ var DoculaHelpers = class {
666
+ createDoc(path4, destination, frontMatter, contentFunction) {
667
+ const content = fs2.readFileSync(path4, "utf8");
668
+ let newContent = this.setFrontMatterInContent(content, frontMatter);
669
+ if (contentFunction) {
670
+ newContent = contentFunction(newContent);
671
+ }
672
+ fs2.writeFileSync(destination, newContent, "utf8");
673
+ }
674
+ getFrontMatterFromFile(path4) {
675
+ const content = fs2.readFileSync(path4, "utf8");
676
+ return this.getFrontMatter(content);
677
+ }
678
+ getFrontMatter(content) {
679
+ const match = /^---\r?\n([\s\S]+?)\r?\n---/.exec(content);
680
+ if (match) {
681
+ const frontMatter = load2(match[1]);
682
+ return frontMatter;
683
+ }
684
+ return {};
685
+ }
686
+ setFrontMatterToFile(path4, frontMatter) {
687
+ const content = fs2.readFileSync(path4, "utf8");
688
+ const newContent = this.setFrontMatterInContent(content, frontMatter);
689
+ fs2.writeFileSync(path4, newContent, "utf8");
690
+ }
691
+ setFrontMatterInContent(content, frontMatter) {
692
+ if (!frontMatter) {
693
+ return content;
694
+ }
695
+ const match = /^---\r?\n([\s\S]+?)\r?\n---\r?\n([\s\S]*)/.exec(content);
696
+ if (match) {
697
+ let oldFrontMatter = load2(match[1]);
698
+ oldFrontMatter = {
699
+ ...oldFrontMatter,
700
+ ...frontMatter
701
+ };
702
+ const newYaml2 = dump(oldFrontMatter);
703
+ const newContent2 = `---
704
+ ${newYaml2}---
705
+ ${match[2]}`;
706
+ return newContent2;
707
+ }
708
+ const newYaml = dump(frontMatter);
709
+ const newContent = `---
710
+ ${newYaml}---
711
+ ${content}`;
712
+ return newContent;
713
+ }
714
+ };
715
+
716
+ // src/docula.ts
717
+ var Docula = class {
718
+ _options = new DoculaOptions();
719
+ _console = new DoculaConsole();
720
+ _configFileModule = {};
721
+ _server;
722
+ constructor(options) {
723
+ if (options) {
724
+ this._options = options;
725
+ }
726
+ }
727
+ get options() {
728
+ return this._options;
729
+ }
730
+ set options(value) {
731
+ this._options = value;
732
+ }
733
+ get server() {
734
+ return this._server;
735
+ }
736
+ get configFileModule() {
737
+ return this._configFileModule;
738
+ }
739
+ checkForUpdates() {
740
+ const packageJsonPath = path3.join(process3.cwd(), "package.json");
741
+ if (fs3.existsSync(packageJsonPath)) {
742
+ const packageJson = JSON.parse(fs3.readFileSync(packageJsonPath, "utf8"));
743
+ updateNotifier({ pkg: packageJson }).notify();
744
+ }
745
+ }
746
+ async execute(process4) {
747
+ this.checkForUpdates();
748
+ const consoleProcess = this._console.parseProcessArgv(process4.argv);
749
+ this.options.singlePage = this.isSinglePageWebsite(this.options.sitePath);
750
+ if (consoleProcess.args.sitePath) {
751
+ this.options.sitePath = consoleProcess.args.sitePath;
752
+ }
753
+ await this.loadConfigFile(this.options.sitePath);
754
+ if (this._configFileModule.options) {
755
+ this.options.parseOptions(this._configFileModule.options);
756
+ }
757
+ if (this._configFileModule.onPrepare) {
758
+ try {
759
+ await this._configFileModule.onPrepare(this.options);
760
+ } catch (error) {
761
+ this._console.error(error.message);
762
+ }
763
+ }
764
+ if (consoleProcess.args.output) {
765
+ this.options.outputPath = consoleProcess.args.output;
766
+ }
767
+ switch (consoleProcess.command) {
768
+ case "init": {
769
+ this.generateInit(this.options.sitePath);
770
+ break;
771
+ }
772
+ case "help": {
773
+ this._console.printHelp();
774
+ break;
775
+ }
776
+ case "version": {
777
+ this._console.log(this.getVersion());
778
+ break;
779
+ }
780
+ case "serve": {
781
+ const builder = new DoculaBuilder(this.options);
782
+ await builder.build();
783
+ await this.serve(this.options);
784
+ break;
785
+ }
786
+ default: {
787
+ const builder = new DoculaBuilder(this.options);
788
+ await builder.build();
789
+ break;
790
+ }
791
+ }
792
+ }
793
+ isSinglePageWebsite(sitePath) {
794
+ const documentationPath = `${sitePath}/docs`;
795
+ if (!fs3.existsSync(documentationPath)) {
796
+ return true;
797
+ }
798
+ const files = fs3.readdirSync(documentationPath);
799
+ return files.length === 0;
800
+ }
801
+ generateInit(sitePath) {
802
+ if (!fs3.existsSync(sitePath)) {
803
+ fs3.mkdirSync(sitePath);
804
+ }
805
+ const doculaConfigFile = "./init/docula.config.mjs";
806
+ fs3.copyFileSync(doculaConfigFile, `${sitePath}/docula.config.mjs`);
807
+ fs3.copyFileSync("./init/logo.png", `${sitePath}/logo.png`);
808
+ fs3.copyFileSync("./init/favicon.ico", `${sitePath}/favicon.ico`);
809
+ fs3.copyFileSync("./init/variables.css", `${sitePath}/variables.css`);
810
+ this._console.log(`docula initialized. Please update the ${doculaConfigFile} file with your site information. In addition, you can replace the image, favicon, and stype the site with site.css file.`);
811
+ }
812
+ getVersion() {
813
+ const packageJson = fs3.readFileSync("./package.json", "utf8");
814
+ const packageObject = JSON.parse(packageJson);
815
+ return packageObject.version;
816
+ }
817
+ async loadConfigFile(sitePath) {
818
+ if (fs3.existsSync(sitePath)) {
819
+ const configFile = `${sitePath}/docula.config.mjs`;
820
+ if (fs3.existsSync(configFile)) {
821
+ this._configFileModule = await import(configFile);
822
+ }
823
+ }
824
+ }
825
+ async serve(options) {
826
+ if (this._server) {
827
+ this._server.close();
145
828
  }
146
- }
147
- export { DoculaHelpers } from './helpers.js';
148
- //# sourceMappingURL=docula.js.map
829
+ const app = express();
830
+ const { port } = options;
831
+ const { outputPath } = options;
832
+ app.use(express.static(outputPath));
833
+ this._server = app.listen(port, () => {
834
+ this._console.log(`Docula \u{1F987} at http://localhost:${port}`);
835
+ });
836
+ }
837
+ };
838
+ export {
839
+ DoculaHelpers,
840
+ Docula as default
841
+ };