@t8/docsgen 0.3.32 → 0.3.34

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": "@t8/docsgen",
3
- "version": "0.3.32",
3
+ "version": "0.3.34",
4
4
  "description": "",
5
5
  "main": "dist/bin.js",
6
6
  "type": "module",
@@ -0,0 +1,21 @@
1
+ import { access, mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type { Context } from "../../types/Context.ts";
4
+
5
+ export async function createDirs(ctx: Context) {
6
+ let { dir = "", contentDir = "" } = ctx;
7
+
8
+ let dirs = [contentDir];
9
+
10
+ await Promise.all(
11
+ dirs.map(async (path) => {
12
+ let dirPath = join(dir, path);
13
+
14
+ try {
15
+ await access(dirPath);
16
+ } catch {
17
+ await mkdir(dirPath);
18
+ }
19
+ }),
20
+ );
21
+ }
@@ -0,0 +1,50 @@
1
+ import { exec as defaultExec } from "node:child_process";
2
+ import { cp } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { promisify } from "node:util";
6
+ import { packageName } from "../../const/packageName.ts";
7
+ import type { Context } from "../../types/Context.ts";
8
+
9
+ const exec = promisify(defaultExec);
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+
14
+ let packageURL = "";
15
+
16
+ export async function getCSSRoot(ctx: Context, type: "index" | "content") {
17
+ let { dir = "", assetsDir } = ctx;
18
+
19
+ let cssRoot = {
20
+ index: "",
21
+ content: "",
22
+ };
23
+
24
+ if (assetsDir) {
25
+ cssRoot.index = assetsDir;
26
+ cssRoot.content = `../${assetsDir}`;
27
+
28
+ await cp(join(__dirname, "css"), join(dir, cssRoot.index), {
29
+ force: true,
30
+ recursive: true,
31
+ });
32
+ } else {
33
+ if (!packageURL) {
34
+ let packageVersion = (
35
+ await exec(`npm view ${packageName} version`)
36
+ ).stdout
37
+ .trim()
38
+ .split(".")
39
+ .slice(0, 2)
40
+ .join(".");
41
+
42
+ packageURL = `https://unpkg.com/${packageName}@${packageVersion}`;
43
+ }
44
+
45
+ cssRoot.index = `${packageURL}/dist/css`;
46
+ cssRoot.content = `${packageURL}/dist/css`;
47
+ }
48
+
49
+ return cssRoot[type];
50
+ }
@@ -1,4 +1,4 @@
1
- import type { Context } from "../types/Context.ts";
1
+ import type { Context } from "../../types/Context.ts";
2
2
 
3
3
  export function getCounterContent({ ymid }: Context) {
4
4
  if (!ymid) return "";
@@ -0,0 +1,9 @@
1
+ export function getDefaultCodeStyleContent(cssRoot: string) {
2
+ return `
3
+ <link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets@11.11.1/styles/stackoverflow-light.min.css" media="(prefers-color-scheme: light)">
4
+ <link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets@11.11.1/styles/base16/material.min.css" media="(prefers-color-scheme: dark)">
5
+ <link rel="stylesheet" href="${cssRoot}/code.css">
6
+ <script src="https://unpkg.com/@highlightjs/cdn-assets@11.11.1/highlight.min.js"></script>
7
+ <script>hljs.highlightAll()</script>
8
+ `.trim();
9
+ }
@@ -1,4 +1,4 @@
1
- import type { Context } from "../types/Context.ts";
1
+ import type { Context } from "../../types/Context.ts";
2
2
 
3
3
  const iconTypeMap: Record<string, string> = {
4
4
  ico: "image/x-icon",
@@ -0,0 +1,10 @@
1
+ import type { Context } from "../../types/Context.ts";
2
+ import { getIcon } from "./getIcon.ts";
3
+
4
+ export function getIconTag(ctx: Context) {
5
+ let { url, type } = getIcon(ctx);
6
+
7
+ return url
8
+ ? `<link rel="icon"${type ? ` type="${type}"` : ""} href="${url}">`
9
+ : "";
10
+ }
@@ -0,0 +1,119 @@
1
+ import type { Context } from "../../types/Context.ts";
2
+ import { escapeHTML } from "../../utils/escapeHTML.ts";
3
+ import { getRepoLink } from "../getRepoLink.ts";
4
+ import { getParsedContent } from "../parsing/getParsedContent.ts";
5
+ import { getCounterContent } from "./getCounterContent.ts";
6
+ import { getCSSRoot } from "./getCSSRoot.ts";
7
+ import { getDefaultCodeStyleContent } from "./getDefaultCodeStyleContent.ts";
8
+ import { getIconTag } from "./getIconTag.ts";
9
+ import { getInjectedContent } from "./getInjectedContent.ts";
10
+ import { getPlainTitle } from "./getPlainTitle.ts";
11
+ import { toFileContent } from "./toFileContent.ts";
12
+ import { tweakTypography } from "./tweakTypography.ts";
13
+
14
+ export async function getIndexContent(ctx: Context) {
15
+ let {
16
+ root,
17
+ contentDir = "",
18
+ name,
19
+ htmlTitle,
20
+ description: packageDescription,
21
+ isDevDep,
22
+ backstory,
23
+ } = ctx;
24
+
25
+ let counterContent = getCounterContent(ctx);
26
+ let escapedPackageDescription = escapeHTML(packageDescription);
27
+
28
+ let {
29
+ title: parsedTitle,
30
+ description,
31
+ intro,
32
+ features,
33
+ note,
34
+ installation,
35
+ nav,
36
+ } = await getParsedContent(ctx);
37
+
38
+ let plainTitle = await getPlainTitle(ctx);
39
+ let coverTitle = htmlTitle || parsedTitle || plainTitle;
40
+
41
+ let descriptionContent =
42
+ tweakTypography(description) ||
43
+ (escapedPackageDescription
44
+ ? `<p>${tweakTypography(escapedPackageDescription)}<p>`
45
+ : "");
46
+
47
+ if (!installation || isDevDep !== undefined)
48
+ installation = `npm i${isDevDep ? " -D" : ""} ${name}`;
49
+
50
+ let cssRoot = await getCSSRoot(ctx, "index");
51
+
52
+ return toFileContent(`
53
+ <!DOCTYPE html>
54
+ <html lang="en" data-layout="index">
55
+ <head>
56
+ ${getInjectedContent(ctx, "index", "head", "prepend")}
57
+ <meta charset="utf-8">
58
+ <meta name="viewport" content="width=device-width, initial-scale=1">
59
+ <meta name="description" content="${plainTitle}${escapedPackageDescription ? `: ${escapedPackageDescription}` : ""}">
60
+ <title>${plainTitle}${escapedPackageDescription ? ` | ${escapedPackageDescription}` : ""}</title>
61
+ <link rel="stylesheet" href="${cssRoot}/base.css">
62
+ <link rel="stylesheet" href="${cssRoot}/index.css">
63
+ ${getIconTag(ctx)}
64
+ <link rel="prefetch" href="${root}start">
65
+ ${nav[0] ? `<link rel="prefetch" href="${root}${contentDir}/${nav[0]?.id ?? ""}">` : ""}
66
+ ${getInjectedContent(ctx, "index", "head", "append")}
67
+ </head>
68
+ <body>
69
+ ${getInjectedContent(ctx, "index", "body", "prepend")}
70
+ <div class="layout">
71
+ <main>
72
+ <section class="aux intro-title">
73
+ <div class="section-content">
74
+ ${getInjectedContent(ctx, "index", "cover", "prepend")}
75
+ <h1>${coverTitle}</h1>
76
+ <div class="description">
77
+ ${descriptionContent}
78
+ </div>
79
+ <p class="actions">
80
+ <a href="${root}start" class="primary">Docs</a>
81
+ <span class="sep"> • </span>
82
+ ${getRepoLink(ctx)}
83
+ </p>
84
+ ${backstory ? `<p class="ref"><a href="${backstory}">Backstory</a></p>` : ""}
85
+ <p class="installation"><code>${escapeHTML(installation)}</code></p>
86
+ ${getInjectedContent(ctx, "index", "cover", "append")}
87
+ </div>
88
+ </section>
89
+ ${
90
+ intro || features || note
91
+ ? `
92
+ <section class="intro">
93
+ <div class="section-content">
94
+ ${intro ? `<div class="intro">${intro}</div>` : ""}
95
+ ${features ? `<div class="features">${features}</div>` : ""}
96
+ ${note ? `<div class="note">${note}</div>` : ""}
97
+ <p class="pagenav">
98
+ <span class="next"><a href="${root}start">To the docs</a> <span class="icon">→</span></span>
99
+ </p>
100
+ </div>
101
+ </section>
102
+ `
103
+ : ""
104
+ }
105
+ </main>
106
+ </div>
107
+
108
+ ${
109
+ [description, intro, features, note].some((s) => s.includes("<pre><code "))
110
+ ? getInjectedContent(ctx, "index", ":has-code", "append") ||
111
+ getDefaultCodeStyleContent(cssRoot)
112
+ : ""
113
+ }
114
+ ${counterContent}
115
+ ${getInjectedContent(ctx, "index", "body", "append")}
116
+ </body>
117
+ </html>
118
+ `);
119
+ }
@@ -1,6 +1,6 @@
1
- import type { ContentInjectionTarget } from "../types/ContentInjectionTarget.ts";
2
- import type { Context } from "../types/Context.ts";
3
- import type { Page } from "../types/Page.ts";
1
+ import type { ContentInjectionTarget } from "../../types/ContentInjectionTarget.ts";
2
+ import type { Context } from "../../types/Context.ts";
3
+ import type { Page } from "../../types/Page.ts";
4
4
 
5
5
  export function getInjectedContent(
6
6
  ctx: Context,
@@ -1,13 +1,50 @@
1
1
  import { JSDOM } from "jsdom";
2
- import type { Context } from "../types/Context.ts";
3
- import type { NavItem } from "../types/NavItem.ts";
4
- import { fetchContent } from "./fetchContent.ts";
5
- import { getNpmLink } from "./getNpmLink.ts";
6
- import { getRepoLink } from "./getRepoLink.ts";
2
+ import type { Context } from "../../types/Context.ts";
3
+ import type { NavItem } from "../../types/NavItem.ts";
4
+ import { fetchContent } from "../fetchContent.ts";
5
+ import { getNpmLink } from "../getNpmLink.ts";
6
+ import { getRepoLink } from "../getRepoLink.ts";
7
+
8
+ let cachedNavContent = new Map<string, string>();
9
+
10
+ async function getNavContent({ name, nav }: Context) {
11
+ if (!nav) return "";
12
+
13
+ let navContent = cachedNavContent.get(nav);
14
+
15
+ if (navContent !== undefined) return navContent;
16
+
17
+ navContent = await fetchContent(nav);
18
+
19
+ if (navContent) {
20
+ let navDom = new JSDOM(navContent).window.document.body;
21
+
22
+ for (let link of navDom.querySelectorAll("a")) {
23
+ if (link.dataset.name === name) {
24
+ let parent = link.parentElement;
25
+
26
+ link.remove();
27
+
28
+ while (parent && parent.innerHTML.trim() === "") {
29
+ let nextParent = parent.parentElement;
30
+
31
+ parent.remove();
32
+ parent = nextParent;
33
+ }
34
+ }
35
+ }
36
+
37
+ navContent = navDom.innerHTML;
38
+ }
39
+
40
+ cachedNavContent.set(nav, navContent);
41
+
42
+ return navContent;
43
+ }
7
44
 
8
45
  export async function getNav(ctx: Context, navItems: NavItem[]) {
9
- let { name, root, contentDir, backstory, nav } = ctx;
10
- let navContent = await fetchContent(nav);
46
+ let { name, root, contentDir, backstory } = ctx;
47
+ let navContent = await getNavContent(ctx);
11
48
  let s = "";
12
49
 
13
50
  if (navContent) {
@@ -0,0 +1,12 @@
1
+ import type { Context } from "../../types/Context.ts";
2
+ import { escapeHTML } from "../../utils/escapeHTML.ts";
3
+ import { getParsedContent } from "../parsing/getParsedContent.ts";
4
+ import { stripHTML } from "../stripHTML.ts";
5
+
6
+ export async function getPlainTitle(ctx: Context) {
7
+ let { name, title, htmlTitle } = ctx;
8
+
9
+ let { title: parsedTitle } = await getParsedContent(ctx);
10
+
11
+ return escapeHTML(title || stripHTML(htmlTitle || parsedTitle, true) || name);
12
+ }
@@ -0,0 +1,31 @@
1
+ import type { Context } from "../../types/Context.ts";
2
+ import { escapeHTML } from "../../utils/escapeHTML.ts";
3
+ import { getCounterContent } from "./getCounterContent.ts";
4
+ import { getIconTag } from "./getIconTag.ts";
5
+ import { getInjectedContent } from "./getInjectedContent.ts";
6
+ import { toFileContent } from "./toFileContent.ts";
7
+
8
+ export function getRedirectContent(ctx: Context) {
9
+ let { redirect } = ctx;
10
+ let escapedRedirect = escapeHTML(redirect);
11
+
12
+ return toFileContent(`
13
+ <!DOCTYPE html>
14
+ <html lang="en" data-layout="redirect" class="blank">
15
+ <head>
16
+ ${getInjectedContent(ctx, "redirect", "head", "prepend")}
17
+ <meta charset="utf-8">
18
+ <meta name="viewport" content="width=device-width">
19
+ <meta http-equiv="refresh" content="0; URL=${escapedRedirect}">
20
+ ${getIconTag(ctx)}
21
+ ${getInjectedContent(ctx, "redirect", "head", "append")}
22
+ </head>
23
+ <body>
24
+ ${getInjectedContent(ctx, "redirect", "body", "prepend")}
25
+ ${getCounterContent(ctx)}
26
+ ${getInjectedContent(ctx, "redirect", "body", "append")}
27
+ <script>window.location.replace("${escapedRedirect}");</script>
28
+ </body>
29
+ </html>
30
+ `);
31
+ }
@@ -0,0 +1,80 @@
1
+ import type { Context } from "../../types/Context.ts";
2
+ import { escapeHTML } from "../../utils/escapeHTML.ts";
3
+ import { escapeRegExp } from "../../utils/escapeRegExp.ts";
4
+ import { getRepoLink } from "../getRepoLink.ts";
5
+ import { getParsedContent } from "../parsing/getParsedContent.ts";
6
+ import { stripHTML } from "../stripHTML.ts";
7
+ import { getCounterContent } from "./getCounterContent.ts";
8
+ import { getCSSRoot } from "./getCSSRoot.ts";
9
+ import { getDefaultCodeStyleContent } from "./getDefaultCodeStyleContent.ts";
10
+ import { getIconTag } from "./getIconTag.ts";
11
+ import { getInjectedContent } from "./getInjectedContent.ts";
12
+ import { getNav } from "./getNav.ts";
13
+ import { getPlainTitle } from "./getPlainTitle.ts";
14
+ import { toFileContent } from "./toFileContent.ts";
15
+
16
+ export async function getSectionContent(ctx: Context, index: number) {
17
+ let { root, contentDir = "" } = ctx;
18
+
19
+ let cssRoot = await getCSSRoot(ctx, "content");
20
+ let { sections, nav } = await getParsedContent(ctx);
21
+
22
+ let content = sections[index];
23
+ let navContent = await getNav(ctx, nav);
24
+ let plainTitle = await getPlainTitle(ctx);
25
+
26
+ return toFileContent(`
27
+ <!DOCTYPE html>
28
+ <html lang="en" data-layout="section">
29
+ <head>
30
+ ${getInjectedContent(ctx, "section", "head", "prepend")}
31
+ <meta charset="utf-8">
32
+ <meta name="viewport" content="width=device-width, initial-scale=1">
33
+ <meta name="description" content="${plainTitle}: ${escapeHTML(stripHTML(nav[index]?.title, true))}">
34
+ <title>${escapeHTML(stripHTML(nav[index]?.title, true))} | ${plainTitle}</title>
35
+ <link rel="stylesheet" href="${cssRoot}/base.css">
36
+ <link rel="stylesheet" href="${cssRoot}/section.css">
37
+ ${getIconTag(ctx)}
38
+ ${nav[index + 1]?.id ? `<link rel="prefetch" href="${root}${contentDir}/${nav[index + 1]?.id}">` : ""}
39
+ ${nav[index - 1]?.id ? `<link rel="prefetch" href="${root}${contentDir}/${nav[index - 1]?.id}">` : ""}
40
+ ${getInjectedContent(ctx, "section", "head", "append")}
41
+ </head>
42
+ <body>
43
+ ${getInjectedContent(ctx, "section", "body", "prepend")}
44
+ <div class="layout">
45
+ <div class="${navContent ? "" : "no-nav "}body">
46
+ <main>
47
+ <h1><a href="${root}">${plainTitle}</a></h1>
48
+ ${content}
49
+
50
+ <p class="pagenav">
51
+ <span class="prev">
52
+ <span class="icon">←</span>
53
+ ${nav[index - 1]?.id ? `<a href="${root}${contentDir}/${nav[index - 1]?.id}">${nav[index - 1]?.title}</a>` : `<a href="${root}">Intro</a>`}
54
+ </span>
55
+ <span class="sep">|</span>
56
+ ${nav[index + 1]?.id ? `<span class="next"><a href="${root}${contentDir}/${nav[index + 1]?.id}">${nav[index + 1]?.title}</a> <span class="icon">→</span></span>` : `<span class="repo">${getRepoLink(ctx)}</span>`}
57
+ </p>
58
+ </main>
59
+ ${navContent ? "<hr>" : ""}
60
+ ${navContent.replace(
61
+ new RegExp(
62
+ `(<li data-id="${escapeRegExp(nav[index]?.id)}">)<a href="[^"]+">([^<]+)</a>`,
63
+ ),
64
+ "$1<strong>$2</strong>",
65
+ )}
66
+ </div>
67
+ </div>
68
+
69
+ ${
70
+ content.includes("<pre><code ")
71
+ ? getInjectedContent(ctx, "section", ":has-code", "append") ||
72
+ getDefaultCodeStyleContent(cssRoot)
73
+ : ""
74
+ }
75
+ ${getCounterContent(ctx)}
76
+ ${getInjectedContent(ctx, "section", "body", "append")}
77
+ </body>
78
+ </html>
79
+ `);
80
+ }
@@ -0,0 +1,40 @@
1
+ import type { Context } from "../../types/Context.ts";
2
+ import { getParsedContent } from "../parsing/getParsedContent.ts";
3
+ import { getCounterContent } from "./getCounterContent.ts";
4
+ import { getCSSRoot } from "./getCSSRoot.ts";
5
+ import { getIconTag } from "./getIconTag.ts";
6
+ import { getInjectedContent } from "./getInjectedContent.ts";
7
+ import { getPlainTitle } from "./getPlainTitle.ts";
8
+ import { toFileContent } from "./toFileContent.ts";
9
+
10
+ export async function getStartContent(ctx: Context) {
11
+ let { root, contentDir = "" } = ctx;
12
+ let { nav } = await getParsedContent(ctx);
13
+ let plainTitle = await getPlainTitle(ctx);
14
+
15
+ return toFileContent(`
16
+ <!DOCTYPE html>
17
+ <html lang="en" data-layout="start" class="blank">
18
+ <head>
19
+ ${getInjectedContent(ctx, "start", "head", "prepend")}
20
+ <meta charset="utf-8">
21
+ <meta name="viewport" content="width=device-width">
22
+ <meta http-equiv="refresh" content="0; URL=${root}${contentDir}/${nav[0]?.id}">
23
+ <title>${plainTitle}</title>
24
+ <link rel="stylesheet" href="${await getCSSRoot(ctx, "index")}/base.css">
25
+ ${getIconTag(ctx)}
26
+ <script>window.location.replace("${root}${contentDir}/${nav[0]?.id}");</script>
27
+ ${getInjectedContent(ctx, "start", "head", "append")}
28
+ </head>
29
+ <body>
30
+ ${getInjectedContent(ctx, "start", "body", "prepend")}
31
+ <div class="layout">
32
+ <h1>${plainTitle}</h1>
33
+ </div>
34
+
35
+ ${getCounterContent(ctx)}
36
+ ${getInjectedContent(ctx, "start", "body", "append")}
37
+ </body>
38
+ </html>
39
+ `);
40
+ }
@@ -0,0 +1,5 @@
1
+ export function tweakTypography(s = "") {
2
+ return s
3
+ .replace(/\b(for|in|on|at|to|with|a|an|the|its)\s+/gi, "$1\xa0")
4
+ .replace(/\b(React)\s+(apps?)\b/gi, "$1\xa0$2");
5
+ }
@@ -1,6 +1,7 @@
1
1
  import { JSDOM } from "jsdom";
2
2
  import Markdown from "markdown-it";
3
3
  import type { Context } from "../../types/Context.ts";
4
+ import type { NavItem } from "../../types/NavItem.ts";
4
5
  import { fetchContent } from "../fetchContent.ts";
5
6
  import { getLocation } from "../getLocation.ts";
6
7
  import { buildNav } from "./buildNav.ts";
@@ -10,15 +11,33 @@ import { isBadgeContainer } from "./isBadgeContainer.ts";
10
11
  import { joinLines } from "./joinLines.ts";
11
12
  import { preprocessContent } from "./preprocessContent.ts";
12
13
 
14
+ export type ParsedContent = {
15
+ badges: string;
16
+ title: string;
17
+ description: string;
18
+ intro: string;
19
+ features: string;
20
+ note: string;
21
+ installation: string;
22
+ sections: string[];
23
+ nav: NavItem[];
24
+ };
25
+
13
26
  const md = new Markdown({
14
27
  html: true,
15
28
  });
16
29
 
17
- export async function getParsedContent(ctx: Context) {
30
+ const parsedContentCache = new Map<string, ParsedContent>();
31
+
32
+ export async function getParsedContent(ctx: Context): Promise<ParsedContent> {
33
+ let contentLocation = getLocation(ctx, "README.md", ctx.source);
34
+ let parsedContent = parsedContentCache.get(contentLocation);
35
+
36
+ if (parsedContent) return parsedContent;
37
+
18
38
  let { singlePage, firstLineDescription, linkMap } = ctx;
19
- let rawContent = await fetchContent(
20
- getLocation(ctx, "README.md", ctx.source),
21
- );
39
+
40
+ let rawContent = await fetchContent(contentLocation);
22
41
  let content = md.render(preprocessContent(rawContent));
23
42
  let dom = new JSDOM(content);
24
43
 
@@ -123,7 +142,7 @@ export async function getParsedContent(ctx: Context) {
123
142
  }
124
143
  }
125
144
 
126
- return {
145
+ parsedContent = {
127
146
  badges, // postprocessBadges(joinLines(badges)),
128
147
  title,
129
148
  description: joinLines(description),
@@ -134,4 +153,8 @@ export async function getParsedContent(ctx: Context) {
134
153
  sections: sections.map(postprocess),
135
154
  nav,
136
155
  };
156
+
157
+ parsedContentCache.set(contentLocation, parsedContent);
158
+
159
+ return parsedContent;
137
160
  }