@litodocs/cli 1.1.0 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litodocs/cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Beautiful documentation sites from Markdown. Fast, simple, and open-source.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,6 +13,46 @@ const { copy, ensureDir, readFile, writeFile, pathExists, readJson } = pkg;
13
13
  import { join, relative, basename, extname } from 'path';
14
14
  import { readdir } from 'fs/promises';
15
15
 
16
+ /**
17
+ * Add is:inline to all <script> and <style> tags in HTML so Astro ships
18
+ * them as-is. Without this, Astro treats scripts as ES modules (scoping
19
+ * declarations, breaking onclick handlers) and scopes styles (breaking
20
+ * global CSS like :root variables, animations, etc.).
21
+ */
22
+ function inlineForAstro(html) {
23
+ // Add is:inline to <script> tags that don't already have it
24
+ html = html.replace(/<script(?![^>]*is:inline)([^>]*>)/gi, '<script is:inline$1');
25
+ // Add is:inline to <style> tags that don't already have is:inline or is:global
26
+ html = html.replace(/<style(?![^>]*is:(?:inline|global))([^>]*>)/gi, '<style is:inline$1');
27
+ return html;
28
+ }
29
+
30
+ /**
31
+ * Check if HTML is a full document (has <html> or <!doctype>).
32
+ * If so, extract head content, body content, and html/body attributes
33
+ * so we can merge them into the Astro template properly.
34
+ */
35
+ function parseFullHtmlDocument(html) {
36
+ const isFullDoc = /<!doctype\s+html|<html[\s>]/i.test(html);
37
+ if (!isFullDoc) return null;
38
+
39
+ // Extract <html> tag attributes
40
+ const htmlTagMatch = html.match(/<html([^>]*)>/i);
41
+ const htmlAttrs = htmlTagMatch ? htmlTagMatch[1].trim() : '';
42
+
43
+ // Extract <head> inner content
44
+ const headMatch = html.match(/<head[^>]*>([\s\S]*)<\/head>/i);
45
+ const headContent = headMatch ? headMatch[1].trim() : '';
46
+
47
+ // Extract <body> tag attributes and inner content
48
+ const bodyTagMatch = html.match(/<body([^>]*)>/i);
49
+ const bodyAttrs = bodyTagMatch ? bodyTagMatch[1].trim() : '';
50
+ const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
51
+ const bodyContent = bodyMatch ? bodyMatch[1].trim() : '';
52
+
53
+ return { htmlAttrs, headContent, bodyContent, bodyAttrs };
54
+ }
55
+
16
56
  /**
17
57
  * Landing page types
18
58
  */
@@ -98,11 +138,17 @@ export async function syncCustomLanding(sourcePath, projectDir, frameworkConfig,
98
138
  // Read all files from _landing/
99
139
  const files = await readdir(landingSource, { withFileTypes: true });
100
140
 
141
+ // Check if navbar/footer are explicitly hidden
142
+ const navbarHidden = landingConfig.navbar === false;
143
+ const footerHidden = landingConfig.footer === false;
144
+
101
145
  // Separate files by type
102
146
  const htmlFiles = [];
103
147
  const cssFiles = [];
104
148
  const jsFiles = [];
105
149
  const assetFiles = [];
150
+ let navbarHtml = null;
151
+ let footerHtml = null;
106
152
 
107
153
  for (const file of files) {
108
154
  if (file.isDirectory()) {
@@ -114,8 +160,17 @@ export async function syncCustomLanding(sourcePath, projectDir, frameworkConfig,
114
160
  }
115
161
 
116
162
  const ext = extname(file.name).toLowerCase();
163
+ const name = basename(file.name, ext).toLowerCase();
164
+
117
165
  if (ext === '.html' || ext === '.htm') {
118
- htmlFiles.push(file.name);
166
+ // Detect custom navbar/footer HTML files (skip if hidden)
167
+ if (!navbarHidden && (name === 'navbar' || name === 'nav' || name === 'header')) {
168
+ navbarHtml = file.name;
169
+ } else if (!footerHidden && name === 'footer') {
170
+ footerHtml = file.name;
171
+ } else if (!((name === 'navbar' || name === 'nav' || name === 'header') || name === 'footer')) {
172
+ htmlFiles.push(file.name);
173
+ }
119
174
  } else if (ext === '.css') {
120
175
  cssFiles.push(file.name);
121
176
  } else if (ext === '.js' || ext === '.mjs') {
@@ -123,6 +178,31 @@ export async function syncCustomLanding(sourcePath, projectDir, frameworkConfig,
123
178
  }
124
179
  }
125
180
 
181
+ // Read custom navbar/footer content if present
182
+ let navbarContent = navbarHidden ? '__hidden__' : null;
183
+ let footerContent = footerHidden ? '__hidden__' : null;
184
+
185
+ if (!navbarHidden && navbarHtml) {
186
+ navbarContent = await readFile(join(landingSource, navbarHtml), 'utf-8');
187
+ }
188
+ if (!footerHidden && footerHtml) {
189
+ footerContent = await readFile(join(landingSource, footerHtml), 'utf-8');
190
+ }
191
+
192
+ // Also check config for custom navbar/footer (skip if hidden)
193
+ if (!navbarHidden && !navbarContent && landingConfig.navbar?.html) {
194
+ const navPath = join(landingSource, '..', landingConfig.navbar.html);
195
+ if (await pathExists(navPath)) {
196
+ navbarContent = await readFile(navPath, 'utf-8');
197
+ }
198
+ }
199
+ if (!footerHidden && !footerContent && landingConfig.footer?.html) {
200
+ const footerPath = join(landingSource, '..', landingConfig.footer.html);
201
+ if (await pathExists(footerPath)) {
202
+ footerContent = await readFile(footerPath, 'utf-8');
203
+ }
204
+ }
205
+
126
206
  // Generate landing page based on framework
127
207
  await generateLandingForFramework(
128
208
  projectDir,
@@ -133,6 +213,8 @@ export async function syncCustomLanding(sourcePath, projectDir, frameworkConfig,
133
213
  cssFiles,
134
214
  jsFiles,
135
215
  assetFiles,
216
+ navbarContent,
217
+ footerContent,
136
218
  config: landingConfig,
137
219
  }
138
220
  );
@@ -170,12 +252,55 @@ export async function syncSectionsLanding(sourcePath, projectDir, frameworkConfi
170
252
  }
171
253
  }
172
254
 
255
+ // Check if navbar/footer are explicitly hidden
256
+ const navbarHidden = landingConfig.navbar === false;
257
+ const footerHidden = landingConfig.footer === false;
258
+
259
+ // Check for custom navbar/footer in _landing/ folder
260
+ const landingSource = join(sourcePath, landingConfig.source || '_landing');
261
+ let navbarContent = navbarHidden ? '__hidden__' : null;
262
+ let footerContent = footerHidden ? '__hidden__' : null;
263
+
264
+ if (!navbarHidden) {
265
+ const navbarNames = ['navbar.html', 'nav.html', 'header.html'];
266
+ for (const name of navbarNames) {
267
+ const navPath = join(landingSource, name);
268
+ if (await pathExists(navPath)) {
269
+ navbarContent = await readFile(navPath, 'utf-8');
270
+ break;
271
+ }
272
+ }
273
+ }
274
+
275
+ if (!footerHidden) {
276
+ const footerPath = join(landingSource, 'footer.html');
277
+ if (await pathExists(footerPath)) {
278
+ footerContent = await readFile(footerPath, 'utf-8');
279
+ }
280
+ }
281
+
282
+ // Also check config for custom navbar/footer (skip if hidden)
283
+ if (!navbarHidden && !navbarContent && landingConfig.navbar?.html) {
284
+ const navPath = join(sourcePath, landingConfig.navbar.html);
285
+ if (await pathExists(navPath)) {
286
+ navbarContent = await readFile(navPath, 'utf-8');
287
+ }
288
+ }
289
+ if (!footerHidden && !footerContent && landingConfig.footer?.html) {
290
+ const fPath = join(sourcePath, landingConfig.footer.html);
291
+ if (await pathExists(fPath)) {
292
+ footerContent = await readFile(fPath, 'utf-8');
293
+ }
294
+ }
295
+
173
296
  // Generate sections landing for framework
174
297
  await generateSectionsLandingForFramework(
175
298
  projectDir,
176
299
  frameworkConfig,
177
300
  {
178
301
  sections: processedSections,
302
+ navbarContent,
303
+ footerContent,
179
304
  config: landingConfig,
180
305
  }
181
306
  );
@@ -213,7 +338,7 @@ async function generateLandingForFramework(projectDir, frameworkConfig, landingD
213
338
  * Generate Astro landing page (standalone, no template imports)
214
339
  */
215
340
  async function generateAstroLanding(projectDir, landingData) {
216
- const { sourcePath, htmlFiles, cssFiles, jsFiles, config } = landingData;
341
+ const { sourcePath, htmlFiles, cssFiles, jsFiles, navbarContent, footerContent, config } = landingData;
217
342
 
218
343
  // Read main HTML file
219
344
  const mainHtml = htmlFiles.includes('index.html') ? 'index.html' : htmlFiles[0];
@@ -224,13 +349,20 @@ async function generateAstroLanding(projectDir, landingData) {
224
349
 
225
350
  let htmlContent = await readFile(join(sourcePath, mainHtml), 'utf-8');
226
351
 
227
- // Read CSS files
352
+ // Make all <script> tags in the user's HTML pass through Astro untouched
353
+ htmlContent = inlineForAstro(htmlContent);
354
+
355
+ // Read CSS files and write to a separate file
228
356
  let cssContent = '';
229
357
  for (const cssFile of cssFiles) {
230
358
  const css = await readFile(join(sourcePath, cssFile), 'utf-8');
231
359
  cssContent += `/* ${cssFile} */\n${css}\n\n`;
232
360
  }
233
361
 
362
+ // Write landing CSS to a separate file so Vite processes it through the full pipeline
363
+ const landingCssPath = join(projectDir, 'src', 'styles', 'landing.css');
364
+ await writeFile(landingCssPath, cssContent, 'utf-8');
365
+
234
366
  // Read JS files
235
367
  let jsContent = '';
236
368
  for (const jsFile of jsFiles) {
@@ -238,13 +370,110 @@ async function generateAstroLanding(projectDir, landingData) {
238
370
  jsContent += `// ${jsFile}\n${js}\n\n`;
239
371
  }
240
372
 
241
- // Generate standalone Astro component
242
- const astroContent = `---
373
+ // Check if the user's HTML is a full document (has <html>, <head>, <body>)
374
+ const parsed = parseFullHtmlDocument(htmlContent);
375
+
376
+ let astroContent;
377
+
378
+ if (parsed) {
379
+ // Full HTML document: merge the user's head/body into the Astro page
380
+ // instead of nesting an entire HTML document inside another one.
381
+ astroContent = generateAstroFromFullDoc(parsed, { cssFiles, jsContent, navbarContent, footerContent });
382
+ } else {
383
+ // HTML fragment: wrap it in a full Astro page
384
+ astroContent = generateAstroFromFragment(htmlContent, { jsContent, navbarContent, footerContent });
385
+ }
386
+
387
+ // Write to index.astro
388
+ const indexPath = join(projectDir, 'src', 'pages', 'index.astro');
389
+ await writeFile(indexPath, astroContent, 'utf-8');
390
+
391
+ // Copy assets if they exist
392
+ await copyLandingAssets(sourcePath, projectDir);
393
+ }
394
+
395
+ /**
396
+ * Generate Astro page from a full HTML document.
397
+ * Extracts <head> and <body> content, preserves the user's structure.
398
+ */
399
+ function generateAstroFromFullDoc(parsed, { cssFiles, jsContent, navbarContent, footerContent }) {
400
+ const { htmlAttrs, headContent, bodyContent, bodyAttrs } = parsed;
401
+
402
+ // Determine header/footer rendering
403
+ const navbarIsHidden = navbarContent === '__hidden__';
404
+ const footerIsHidden = footerContent === '__hidden__';
405
+ const hasCustomNavbar = !navbarIsHidden && !!navbarContent;
406
+ const hasCustomFooter = !footerIsHidden && !!footerContent;
407
+
408
+ const headerImport = navbarIsHidden || hasCustomNavbar ? '' : "import Header from '../components/Header.astro';";
409
+ const footerImport = footerIsHidden || hasCustomFooter ? '' : "import Footer from '../components/Footer.astro';";
410
+ const headerRender = navbarIsHidden
411
+ ? ''
412
+ : hasCustomNavbar
413
+ ? `<header class="landing-custom-navbar">\n ${inlineForAstro(navbarContent)}\n </header>`
414
+ : '<Header />';
415
+ const footerRender = footerIsHidden
416
+ ? ''
417
+ : hasCustomFooter
418
+ ? `<footer class="landing-custom-footer">\n ${inlineForAstro(footerContent)}\n </footer>`
419
+ : '<Footer />';
420
+
421
+ return `---
422
+ // Custom landing page - auto-generated by Lito CLI
423
+ // Source: _landing/ folder (full HTML document)
424
+ import '../styles/landing.css';
425
+ ${headerImport}
426
+ ${footerImport}
427
+ ---
428
+
429
+ <!doctype html>
430
+ <html ${htmlAttrs}>
431
+ <head>
432
+ ${headContent}
433
+ </head>
434
+ <body ${bodyAttrs}>
435
+ ${headerRender}
436
+
437
+ ${bodyContent}
438
+
439
+ ${footerRender}
440
+
441
+ ${jsContent ? `<script is:inline>\n${jsContent}\n</script>` : ''}
442
+ </body>
443
+ </html>
444
+ `;
445
+ }
446
+
447
+ /**
448
+ * Generate Astro page from an HTML fragment.
449
+ * Wraps it in a full Astro page with Lito's defaults.
450
+ */
451
+ function generateAstroFromFragment(htmlContent, { jsContent, navbarContent, footerContent }) {
452
+ const navbarIsHidden = navbarContent === '__hidden__';
453
+ const footerIsHidden = footerContent === '__hidden__';
454
+ const hasCustomNavbar = !navbarIsHidden && !!navbarContent;
455
+ const hasCustomFooter = !footerIsHidden && !!footerContent;
456
+
457
+ const headerImport = navbarIsHidden || hasCustomNavbar ? '' : "import Header from '../components/Header.astro';";
458
+ const footerImport = footerIsHidden || hasCustomFooter ? '' : "import Footer from '../components/Footer.astro';";
459
+ const headerRender = navbarIsHidden
460
+ ? ''
461
+ : hasCustomNavbar
462
+ ? `<header class="landing-custom-navbar">\n ${inlineForAstro(navbarContent)}\n </header>`
463
+ : '<Header />';
464
+ const footerRender = footerIsHidden
465
+ ? ''
466
+ : hasCustomFooter
467
+ ? `<footer class="landing-custom-footer">\n ${inlineForAstro(footerContent)}\n </footer>`
468
+ : '<Footer />';
469
+
470
+ return `---
243
471
  // Custom landing page - auto-generated by Lito CLI
244
472
  // Source: _landing/ folder
245
473
  import '../styles/global.css';
246
- import Header from '../components/Header.astro';
247
- import Footer from '../components/Footer.astro';
474
+ import '../styles/landing.css';
475
+ ${headerImport}
476
+ ${footerImport}
248
477
  import { getConfigFile } from '../utils/config';
249
478
 
250
479
  const config = await getConfigFile();
@@ -269,30 +498,20 @@ const config = await getConfigFile();
269
498
  document.documentElement.classList.add('light');
270
499
  }
271
500
  </script>
272
- <style>
273
- ${cssContent}
274
- </style>
275
501
  </head>
276
502
  <body class="bg-background text-foreground font-sans antialiased">
277
- <Header />
503
+ ${headerRender}
278
504
 
279
505
  <main class="landing-custom">
280
506
  ${htmlContent}
281
507
  </main>
282
508
 
283
- <Footer />
509
+ ${footerRender}
284
510
 
285
- ${jsContent ? `<script>\n${jsContent}\n</script>` : ''}
511
+ ${jsContent ? `<script is:inline>\n${jsContent}\n</script>` : ''}
286
512
  </body>
287
513
  </html>
288
514
  `;
289
-
290
- // Write to index.astro
291
- const indexPath = join(projectDir, 'src', 'pages', 'index.astro');
292
- await writeFile(indexPath, astroContent, 'utf-8');
293
-
294
- // Copy assets if they exist
295
- await copyLandingAssets(sourcePath, projectDir);
296
515
  }
297
516
 
298
517
  /**
@@ -571,7 +790,7 @@ async function generateSectionsLandingForFramework(projectDir, frameworkConfig,
571
790
  * Generate Astro sections landing page
572
791
  */
573
792
  async function generateAstroSectionsLanding(projectDir, landingData) {
574
- const { sections, config } = landingData;
793
+ const { sections, navbarContent, footerContent, config } = landingData;
575
794
 
576
795
  // Generate section renders
577
796
  const sectionRenders = sections.map((section, index) => {
@@ -591,11 +810,30 @@ async function generateAstroSectionsLanding(projectDir, landingData) {
591
810
  }
592
811
  }).join('\n');
593
812
 
813
+ // Determine header/footer: hidden ('__hidden__'), custom (string HTML), or default (null)
814
+ const navbarIsHidden = navbarContent === '__hidden__';
815
+ const footerIsHidden = footerContent === '__hidden__';
816
+ const hasCustomNavbar = !navbarIsHidden && !!navbarContent;
817
+ const hasCustomFooter = !footerIsHidden && !!footerContent;
818
+
819
+ const headerImport = navbarIsHidden || hasCustomNavbar ? '' : "import Header from '../components/Header.astro';";
820
+ const footerImport = footerIsHidden || hasCustomFooter ? '' : "import Footer from '../components/Footer.astro';";
821
+ const headerRender = navbarIsHidden
822
+ ? ''
823
+ : hasCustomNavbar
824
+ ? `<header class="landing-custom-navbar">\n ${inlineForAstro(navbarContent)}\n </header>`
825
+ : '<Header />';
826
+ const footerRender = footerIsHidden
827
+ ? ''
828
+ : hasCustomFooter
829
+ ? `<footer class="landing-custom-footer">\n ${inlineForAstro(footerContent)}\n </footer>`
830
+ : '<Footer />';
831
+
594
832
  const astroContent = `---
595
833
  // Sections-based landing page - auto-generated by Lito CLI
596
834
  import '../styles/global.css';
597
- import Header from '../components/Header.astro';
598
- import Footer from '../components/Footer.astro';
835
+ ${headerImport}
836
+ ${footerImport}
599
837
  import { getConfigFile } from '../utils/config';
600
838
 
601
839
  const config = await getConfigFile();
@@ -621,13 +859,13 @@ const config = await getConfigFile();
621
859
  </script>
622
860
  </head>
623
861
  <body class="bg-background text-foreground font-sans antialiased">
624
- <Header />
862
+ ${headerRender}
625
863
 
626
864
  <main class="landing-sections">
627
865
  ${sectionRenders}
628
866
  </main>
629
867
 
630
- <Footer />
868
+ ${footerRender}
631
869
  </body>
632
870
  </html>
633
871
  `;
package/src/core/sync.js CHANGED
@@ -13,7 +13,7 @@ const KNOWN_LOCALES = [
13
13
  ];
14
14
 
15
15
  // Special folders that are not content
16
- const SPECIAL_FOLDERS = ['_assets', '_css', '_images', '_static', '_landing', 'public'];
16
+ const SPECIAL_FOLDERS = ['_assets', '_css', '_images', '_static', '_landing', '_navbar', '_footer', 'public'];
17
17
 
18
18
  /**
19
19
  * Get i18n configuration from docs-config.json