idcmd 0.0.4 → 0.0.6

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.
Files changed (32) hide show
  1. package/README.md +23 -4
  2. package/package.json +2 -2
  3. package/src/build.ts +4 -3
  4. package/src/cli/commands/build.ts +7 -0
  5. package/src/cli/commands/client.ts +317 -0
  6. package/src/cli/commands/dev.ts +89 -24
  7. package/src/cli/commands/init.ts +93 -2
  8. package/src/cli/main.ts +12 -0
  9. package/src/cli/runtime-assets.ts +92 -0
  10. package/src/client/index.ts +7 -1
  11. package/src/render/layout-loader.ts +6 -3
  12. package/src/render/layout.tsx +10 -2
  13. package/src/render/page-renderer.ts +12 -2
  14. package/src/render/right-rail-loader.ts +49 -0
  15. package/src/render/right-rail.tsx +10 -6
  16. package/src/search/page.tsx +4 -2
  17. package/src/search/search-page-loader.ts +51 -0
  18. package/src/search/server-page.ts +52 -18
  19. package/templates/default/.github/workflows/ci.yml +24 -0
  20. package/templates/default/README.md +23 -0
  21. package/templates/default/package.json +2 -1
  22. package/templates/default/scripts/check-internal.ts +56 -0
  23. package/templates/default/scripts/check.ts +318 -0
  24. package/templates/default/scripts/smoke.ts +193 -0
  25. package/templates/default/site/client/layout.tsx +237 -2
  26. package/templates/default/site/client/right-rail.tsx +246 -1
  27. package/templates/default/site/{public/_idcmd/llm-menu.js → client/runtime/llm-menu.ts} +27 -18
  28. package/templates/default/site/{public/_idcmd/nav-prefetch.js → client/runtime/nav-prefetch.ts} +3 -3
  29. package/templates/default/site/{public/_idcmd/right-rail-scrollspy.js → client/runtime/right-rail-scrollspy.ts} +73 -32
  30. package/templates/default/site/client/search-page.tsx +87 -1
  31. package/templates/default/tsconfig.json +1 -1
  32. /package/templates/default/site/{public/_idcmd/live-reload.js → client/runtime/live-reload.ts} +0 -0
@@ -1,2 +1,237 @@
1
- export { renderLayout } from "idcmd/client";
2
- export type { LayoutProps } from "idcmd/client";
1
+ import type { LayoutProps } from "idcmd/client";
2
+ /* eslint-disable react/no-danger */
3
+ import type { JSX } from "preact";
4
+
5
+ import { render } from "preact-render-to-string";
6
+
7
+ import { RightRail } from "./right-rail";
8
+
9
+ type NavItem = LayoutProps["navigation"][number]["items"][number];
10
+
11
+ const Icon = ({ svg }: { svg: string }): JSX.Element => (
12
+ <span
13
+ class="inline-flex h-[18px] w-[18px]"
14
+ dangerouslySetInnerHTML={{ __html: svg }}
15
+ />
16
+ );
17
+
18
+ const isActiveLink = (item: NavItem, currentPath: string): boolean =>
19
+ currentPath === item.href ||
20
+ (item.href !== "/" && currentPath.startsWith(item.href));
21
+
22
+ const Sidebar = ({
23
+ siteName,
24
+ navigation,
25
+ currentPath,
26
+ }: {
27
+ siteName: LayoutProps["siteName"];
28
+ navigation: LayoutProps["navigation"];
29
+ currentPath: LayoutProps["currentPath"];
30
+ }): JSX.Element => (
31
+ <aside class="sidebar">
32
+ <div class="sidebar-header">
33
+ <a
34
+ href="/"
35
+ class="text-sm font-medium tracking-tight"
36
+ data-prefetch="hover"
37
+ >
38
+ <span class="text-muted-foreground">~/</span>
39
+ {siteName}
40
+ </a>
41
+ </div>
42
+ <div class="sidebar-content">
43
+ {navigation.map((group) => (
44
+ <div key={group.id} class="py-2">
45
+ <p class="px-3 py-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
46
+ {group.label}
47
+ </p>
48
+ <nav class="space-y-1">
49
+ {group.items.map((item) => (
50
+ <a
51
+ key={item.href}
52
+ href={item.href}
53
+ data-prefetch="hover"
54
+ class={`flex items-center gap-3 px-3 py-1.5 text-sm transition-colors hover:text-sidebar-foreground ${
55
+ isActiveLink(item, currentPath)
56
+ ? "border-l-2 border-sidebar-primary font-medium text-sidebar-foreground"
57
+ : "border-l-2 border-transparent"
58
+ }`}
59
+ >
60
+ <Icon svg={item.iconSvg} />
61
+ <span>{item.title}</span>
62
+ </a>
63
+ ))}
64
+ </nav>
65
+ </div>
66
+ ))}
67
+ </div>
68
+ </aside>
69
+ );
70
+
71
+ const SearchForm = ({ query }: { query?: string }): JSX.Element => (
72
+ <form
73
+ method="get"
74
+ action="/search/"
75
+ class="flex w-full items-center"
76
+ role="search"
77
+ noValidate
78
+ >
79
+ <label htmlFor="site-search" class="sr-only">
80
+ Search pages
81
+ </label>
82
+ <input
83
+ id="site-search"
84
+ name="q"
85
+ type="search"
86
+ autoComplete="off"
87
+ spellcheck={false}
88
+ placeholder="Search..."
89
+ defaultValue={query ?? ""}
90
+ class="w-full border-b border-input bg-transparent px-1 py-1.5 text-sm placeholder:text-muted-foreground focus:border-foreground focus:outline-none transition-colors"
91
+ />
92
+ </form>
93
+ );
94
+
95
+ const TopNavbar = ({
96
+ query,
97
+ siteName,
98
+ }: {
99
+ query?: LayoutProps["searchQuery"];
100
+ siteName: LayoutProps["siteName"];
101
+ }): JSX.Element => (
102
+ <header class="sticky top-0 z-30 border-b border-border bg-background/80 backdrop-blur-sm">
103
+ <div class="mx-auto max-w-6xl px-8 py-3">
104
+ <div class="flex items-center gap-4">
105
+ <a
106
+ href="/"
107
+ class="text-sm font-mono font-medium tracking-tight md:hidden"
108
+ data-prefetch="hover"
109
+ >
110
+ <span class="text-muted-foreground">~/</span>
111
+ {siteName}
112
+ </a>
113
+ <div class="not-prose ml-auto w-full max-w-xs">
114
+ <SearchForm query={query} />
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </header>
119
+ );
120
+
121
+ const buildHtmlClass = (
122
+ smoothScroll: LayoutProps["rightRail"]["smoothScroll"]
123
+ ): string => (smoothScroll ? "dark smooth-scroll" : "dark");
124
+
125
+ const buildScrollSpyDataset = (props: {
126
+ isScrollSpyEnabled: boolean;
127
+ rightRail: LayoutProps["rightRail"];
128
+ }): {
129
+ scrollspy?: string;
130
+ scrollspyCenter?: string;
131
+ scrollspyUpdateHash?: string;
132
+ } =>
133
+ props.isScrollSpyEnabled
134
+ ? {
135
+ scrollspy: "1",
136
+ scrollspyCenter: props.rightRail.scrollSpy.centerActiveItem
137
+ ? "1"
138
+ : undefined,
139
+ scrollspyUpdateHash: props.rightRail.scrollSpy.updateHash,
140
+ }
141
+ : {};
142
+
143
+ const Layout = ({
144
+ title,
145
+ siteName,
146
+ description,
147
+ canonicalUrl,
148
+ content,
149
+ cssPath,
150
+ inlineCss,
151
+ currentPath,
152
+ navigation,
153
+ scriptPaths = [],
154
+ searchQuery,
155
+ showRightRail = true,
156
+ rightRail,
157
+ tocItems,
158
+ }: LayoutProps): JSX.Element => {
159
+ const resolvedCssPath = inlineCss ? undefined : (cssPath ?? "/styles.css");
160
+ const shouldShowRightRail = showRightRail && rightRail.enabled;
161
+ const isScrollSpyEnabled =
162
+ shouldShowRightRail && rightRail.scrollSpy.enabled && tocItems.length > 0;
163
+ const scrollSpyDataset = buildScrollSpyDataset({
164
+ isScrollSpyEnabled,
165
+ rightRail,
166
+ });
167
+
168
+ return (
169
+ <html lang="en" class={buildHtmlClass(rightRail.smoothScroll)}>
170
+ <head>
171
+ <meta charset="utf-8" />
172
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
173
+ <title>{title}</title>
174
+ {description ? <meta name="description" content={description} /> : null}
175
+ {canonicalUrl ? <link rel="canonical" href={canonicalUrl} /> : null}
176
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
177
+ <link
178
+ rel="preconnect"
179
+ href="https://fonts.gstatic.com"
180
+ crossOrigin="anonymous"
181
+ />
182
+ <link
183
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
184
+ rel="stylesheet"
185
+ />
186
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
187
+ {inlineCss ? <style>{inlineCss}</style> : null}
188
+ {resolvedCssPath ? (
189
+ <link rel="stylesheet" href={resolvedCssPath} />
190
+ ) : null}
191
+ </head>
192
+ <body
193
+ class="bg-background font-sans text-foreground"
194
+ data-scrollspy={scrollSpyDataset.scrollspy}
195
+ data-scrollspy-center={scrollSpyDataset.scrollspyCenter}
196
+ data-scrollspy-update-hash={scrollSpyDataset.scrollspyUpdateHash}
197
+ >
198
+ <Sidebar
199
+ siteName={siteName}
200
+ navigation={navigation}
201
+ currentPath={currentPath}
202
+ />
203
+ <div class="main-wrapper">
204
+ <TopNavbar query={searchQuery} siteName={siteName} />
205
+ <main class="main-content">
206
+ <div class="mx-auto flex w-full max-w-6xl items-start gap-10">
207
+ <article
208
+ class={`prose min-w-0 flex-1${
209
+ currentPath === "/" ? " prose-home" : ""
210
+ }`}
211
+ dangerouslySetInnerHTML={{ __html: content }}
212
+ />
213
+ {shouldShowRightRail ? (
214
+ <RightRail
215
+ canonicalUrl={canonicalUrl}
216
+ currentPath={currentPath}
217
+ tocItems={tocItems}
218
+ rightRailConfig={rightRail}
219
+ />
220
+ ) : null}
221
+ </div>
222
+ </main>
223
+ <footer class="site-footer">
224
+ Built with Preact SSR + Tailwind &nbsp;|&nbsp; Zero JavaScript on
225
+ content pages
226
+ </footer>
227
+ </div>
228
+ {scriptPaths.map((scriptPath) => (
229
+ <script key={scriptPath} defer src={scriptPath} />
230
+ ))}
231
+ </body>
232
+ </html>
233
+ );
234
+ };
235
+
236
+ export const renderLayout = (props: LayoutProps): string =>
237
+ `<!DOCTYPE html>${render(<Layout {...props} />)}`;
@@ -1 +1,246 @@
1
- export { RightRail } from "idcmd/client";
1
+ import type { RightRailProps } from "idcmd/client";
2
+ import type { JSX } from "preact";
3
+
4
+ const CaretDownIcon = (): JSX.Element => (
5
+ <svg
6
+ width="16"
7
+ height="16"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ aria-hidden="true"
12
+ >
13
+ <path
14
+ d="M7 10l5 5 5-5"
15
+ stroke="currentColor"
16
+ stroke-width="2"
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"
19
+ />
20
+ </svg>
21
+ );
22
+
23
+ const CopyIcon = (): JSX.Element => (
24
+ <svg
25
+ width="18"
26
+ height="18"
27
+ viewBox="0 0 24 24"
28
+ fill="none"
29
+ xmlns="http://www.w3.org/2000/svg"
30
+ aria-hidden="true"
31
+ >
32
+ <path
33
+ d="M9 9h10v12H9V9z"
34
+ stroke="currentColor"
35
+ stroke-width="2"
36
+ stroke-linecap="round"
37
+ stroke-linejoin="round"
38
+ />
39
+ <path
40
+ d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1"
41
+ stroke="currentColor"
42
+ stroke-width="2"
43
+ stroke-linecap="round"
44
+ stroke-linejoin="round"
45
+ />
46
+ </svg>
47
+ );
48
+
49
+ const buildSlugFromCurrentPath = (currentPath: string): string => {
50
+ if (currentPath === "/") {
51
+ return "index";
52
+ }
53
+
54
+ const trimmed = currentPath.replaceAll(/^\/+|\/+$/g, "");
55
+ return trimmed || "index";
56
+ };
57
+
58
+ const buildAskUrls = ({
59
+ canonicalUrl,
60
+ currentPath,
61
+ }: {
62
+ canonicalUrl?: string;
63
+ currentPath: string;
64
+ }): { chatgptUrl: string; claudeUrl: string; markdownPath: string } => {
65
+ const slug = buildSlugFromCurrentPath(currentPath);
66
+ const markdownPath = `/${slug}.md`;
67
+ const markdownUrl = canonicalUrl
68
+ ? new URL(markdownPath, canonicalUrl).toString()
69
+ : markdownPath;
70
+
71
+ const llmsTxtUrl = canonicalUrl
72
+ ? new URL("/llms.txt", canonicalUrl).toString()
73
+ : "/llms.txt";
74
+
75
+ const prompt = `Investigate this document and explain it to the user: ${markdownUrl}\ndirectory for further exploration: ${llmsTxtUrl}`;
76
+
77
+ const chatgpt = new URL("https://chatgpt.com/");
78
+ chatgpt.searchParams.set("prompt", prompt);
79
+
80
+ const claude = new URL("https://claude.ai/new");
81
+ claude.searchParams.set("q", prompt);
82
+
83
+ return {
84
+ chatgptUrl: chatgpt.toString(),
85
+ claudeUrl: claude.toString(),
86
+ markdownPath,
87
+ };
88
+ };
89
+
90
+ const AskInDropdown = ({
91
+ claudeUrl,
92
+ chatgptUrl,
93
+ markdownPath,
94
+ }: {
95
+ claudeUrl: string;
96
+ chatgptUrl: string;
97
+ markdownPath: string;
98
+ }): JSX.Element => (
99
+ <details class="llm-menu relative">
100
+ <summary class="flex w-full cursor-pointer select-none items-center justify-between gap-3 rounded-full border border-white/20 bg-card/30 px-4 py-2 text-sm shadow-sm hover:border-white/30 hover:bg-card/40">
101
+ <span class="flex items-center gap-2">
102
+ <img
103
+ src="/openai-white.svg"
104
+ alt=""
105
+ width={18}
106
+ height={18}
107
+ class="shrink-0"
108
+ />
109
+ <span>Ask in ChatGPT</span>
110
+ </span>
111
+ <span class="text-muted-foreground">
112
+ <CaretDownIcon />
113
+ </span>
114
+ </summary>
115
+
116
+ <div class="absolute left-0 right-0 z-50 mt-2 rounded-xl border border-border bg-popover p-1 shadow-sm">
117
+ <a
118
+ href={chatgptUrl}
119
+ target="_blank"
120
+ rel="noopener noreferrer"
121
+ class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm hover:bg-muted"
122
+ >
123
+ <img
124
+ src="/openai-white.svg"
125
+ alt=""
126
+ width={18}
127
+ height={18}
128
+ class="shrink-0"
129
+ />
130
+ <span>Ask in ChatGPT</span>
131
+ </a>
132
+ <a
133
+ href={claudeUrl}
134
+ target="_blank"
135
+ rel="noopener noreferrer"
136
+ class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm hover:bg-muted"
137
+ >
138
+ <img
139
+ src="/anthropic-white.svg"
140
+ alt=""
141
+ width={18}
142
+ height={18}
143
+ class="shrink-0"
144
+ />
145
+ <span>Ask in Claude</span>
146
+ </a>
147
+ <a
148
+ href={markdownPath}
149
+ target="_blank"
150
+ rel="noopener noreferrer"
151
+ data-copy-markdown="1"
152
+ class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm hover:bg-muted"
153
+ >
154
+ <span class="shrink-0 text-muted-foreground">
155
+ <CopyIcon />
156
+ </span>
157
+ <span data-copy-markdown-label="1">Copy Markdown to Clipboard</span>
158
+ </a>
159
+ </div>
160
+ </details>
161
+ );
162
+
163
+ const OnThisPage = ({
164
+ items,
165
+ }: {
166
+ items: RightRailProps["tocItems"];
167
+ }): JSX.Element => (
168
+ <section class="flex min-h-0 flex-1 flex-col" data-toc-root="1">
169
+ <div class="px-0.5 pb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
170
+ On this page
171
+ </div>
172
+ <nav aria-label="Table of contents" class="min-h-0 flex flex-1 flex-col">
173
+ <div class="toc-scroll min-h-0 flex-1" data-toc-scroll-container="1">
174
+ <ul class="space-y-2 text-sm text-muted-foreground">
175
+ {items.map((item) => (
176
+ <li key={item.id} class={item.level >= 3 ? "pl-3" : ""}>
177
+ <a
178
+ href={`#${encodeURIComponent(item.id)}`}
179
+ class="hover:text-foreground"
180
+ data-toc-link="1"
181
+ >
182
+ {item.text}
183
+ </a>
184
+ </li>
185
+ ))}
186
+ </ul>
187
+ </div>
188
+ </nav>
189
+ </section>
190
+ );
191
+
192
+ const getVisibilityClass = (
193
+ visibleFrom: RightRailProps["rightRailConfig"]["visibleFrom"]
194
+ ): string => {
195
+ switch (visibleFrom) {
196
+ case "always": {
197
+ return "block";
198
+ }
199
+ case "never": {
200
+ return "hidden";
201
+ }
202
+ case "md": {
203
+ return "hidden md:block";
204
+ }
205
+ case "lg": {
206
+ return "hidden lg:block";
207
+ }
208
+ default: {
209
+ return "hidden xl:block";
210
+ }
211
+ }
212
+ };
213
+
214
+ const getPanelClass = (
215
+ placement: RightRailProps["rightRailConfig"]["placement"]
216
+ ): string =>
217
+ placement === "viewport"
218
+ ? "fixed top-24 bottom-0 right-8 z-20 w-64 flex flex-col gap-6 min-h-0"
219
+ : "sticky top-24 h-[calc(100vh-6rem)] flex flex-col gap-6 min-h-0";
220
+
221
+ export const RightRail = ({
222
+ canonicalUrl,
223
+ currentPath,
224
+ tocItems,
225
+ rightRailConfig,
226
+ }: RightRailProps): JSX.Element => {
227
+ const { chatgptUrl, claudeUrl, markdownPath } = buildAskUrls({
228
+ canonicalUrl,
229
+ currentPath,
230
+ });
231
+
232
+ return (
233
+ <aside
234
+ class={`${getVisibilityClass(rightRailConfig.visibleFrom)} w-64 shrink-0`}
235
+ >
236
+ <div class={getPanelClass(rightRailConfig.placement)}>
237
+ <AskInDropdown
238
+ chatgptUrl={chatgptUrl}
239
+ claudeUrl={claudeUrl}
240
+ markdownPath={markdownPath}
241
+ />
242
+ {tocItems.length > 0 ? <OnThisPage items={tocItems} /> : null}
243
+ </div>
244
+ </aside>
245
+ );
246
+ };
@@ -2,7 +2,7 @@ const COPY_SELECTOR = 'a[data-copy-markdown="1"]';
2
2
  const LABEL_SELECTOR = '[data-copy-markdown-label="1"]';
3
3
  const RESET_DELAY_MS = 2000;
4
4
 
5
- const setLinkDisabled = (link, disabled) => {
5
+ const setLinkDisabled = (link: HTMLAnchorElement, disabled: boolean): void => {
6
6
  if (disabled) {
7
7
  link.setAttribute("aria-disabled", "true");
8
8
  link.style.pointerEvents = "none";
@@ -15,14 +15,14 @@ const setLinkDisabled = (link, disabled) => {
15
15
  link.style.opacity = "";
16
16
  };
17
17
 
18
- const setLinkLabel = (link, next) => {
18
+ const setLinkLabel = (link: HTMLAnchorElement, next: string): void => {
19
19
  const label = link.querySelector(LABEL_SELECTOR);
20
20
  if (label) {
21
21
  label.textContent = next;
22
22
  }
23
23
  };
24
24
 
25
- const toAbsoluteUrl = (href) => {
25
+ const toAbsoluteUrl = (href: string): string => {
26
26
  try {
27
27
  return new URL(href, window.location.href).toString();
28
28
  } catch {
@@ -30,7 +30,7 @@ const toAbsoluteUrl = (href) => {
30
30
  }
31
31
  };
32
32
 
33
- const createHiddenTextarea = (text) => {
33
+ const createHiddenTextarea = (text: string): HTMLTextAreaElement => {
34
34
  const textarea = document.createElement("textarea");
35
35
  textarea.value = text;
36
36
  textarea.setAttribute("readonly", "true");
@@ -40,7 +40,7 @@ const createHiddenTextarea = (text) => {
40
40
  return textarea;
41
41
  };
42
42
 
43
- const safeExecCommandCopy = () => {
43
+ const safeExecCommandCopy = (): boolean => {
44
44
  try {
45
45
  return document.execCommand("copy");
46
46
  } catch {
@@ -48,7 +48,7 @@ const safeExecCommandCopy = () => {
48
48
  }
49
49
  };
50
50
 
51
- const copyViaExecCommand = (text) => {
51
+ const copyViaExecCommand = (text: string): boolean => {
52
52
  const textarea = createHiddenTextarea(text);
53
53
  document.body.append(textarea);
54
54
  textarea.focus();
@@ -58,7 +58,7 @@ const copyViaExecCommand = (text) => {
58
58
  return ok;
59
59
  };
60
60
 
61
- const copyText = async (text) => {
61
+ const copyText = async (text: string): Promise<boolean> => {
62
62
  const { clipboard } = navigator;
63
63
  if (clipboard?.writeText) {
64
64
  try {
@@ -72,18 +72,18 @@ const copyText = async (text) => {
72
72
  return copyViaExecCommand(text);
73
73
  };
74
74
 
75
- const closeMenuIfPresent = (link) => {
75
+ const closeMenuIfPresent = (link: HTMLAnchorElement): void => {
76
76
  const details = link.closest("details");
77
77
  if (details instanceof HTMLDetailsElement) {
78
78
  details.open = false;
79
79
  }
80
80
  };
81
81
 
82
- const getOriginalLabel = (link) =>
82
+ const getOriginalLabel = (link: HTMLAnchorElement): string =>
83
83
  link.querySelector(LABEL_SELECTOR)?.textContent ??
84
84
  "Copy Markdown to Clipboard";
85
85
 
86
- const fetchMarkdownText = async (href) => {
86
+ const fetchMarkdownText = async (href: string): Promise<string | null> => {
87
87
  const res = await fetch(toAbsoluteUrl(href), { credentials: "same-origin" });
88
88
  if (!res.ok) {
89
89
  return null;
@@ -91,7 +91,7 @@ const fetchMarkdownText = async (href) => {
91
91
  return res.text();
92
92
  };
93
93
 
94
- const copyMarkdownFromHref = async (href) => {
94
+ const copyMarkdownFromHref = async (href: string): Promise<boolean> => {
95
95
  try {
96
96
  const text = await fetchMarkdownText(href);
97
97
  if (!text) {
@@ -103,24 +103,30 @@ const copyMarkdownFromHref = async (href) => {
103
103
  }
104
104
  };
105
105
 
106
- const startCopyOperation = (link) => {
106
+ const startCopyOperation = (link: HTMLAnchorElement): void => {
107
107
  setLinkDisabled(link, true);
108
108
  setLinkLabel(link, "Copying...");
109
109
  };
110
110
 
111
- const finishCopyOperation = (link, ok) => {
111
+ const finishCopyOperation = (link: HTMLAnchorElement, ok: boolean): void => {
112
112
  setLinkLabel(link, ok ? "Copied" : "Copy failed");
113
113
  closeMenuIfPresent(link);
114
114
  };
115
115
 
116
- const scheduleResetOperation = (link, originalLabel) => {
116
+ const scheduleResetOperation = (
117
+ link: HTMLAnchorElement,
118
+ originalLabel: string
119
+ ): void => {
117
120
  window.setTimeout(() => {
118
121
  setLinkLabel(link, originalLabel);
119
122
  setLinkDisabled(link, false);
120
123
  }, RESET_DELAY_MS);
121
124
  };
122
125
 
123
- const handleCopyClick = async (link, originalLabel) => {
126
+ const handleCopyClick = async (
127
+ link: HTMLAnchorElement,
128
+ originalLabel: string
129
+ ): Promise<void> => {
124
130
  const href = link.getAttribute("href");
125
131
  if (!href) {
126
132
  return;
@@ -132,7 +138,7 @@ const handleCopyClick = async (link, originalLabel) => {
132
138
  scheduleResetOperation(link, originalLabel);
133
139
  };
134
140
 
135
- const attachCopyHandler = (link) => {
141
+ const attachCopyHandler = (link: HTMLAnchorElement): void => {
136
142
  const originalLabel = getOriginalLabel(link);
137
143
  link.addEventListener("click", async (event) => {
138
144
  event.preventDefault();
@@ -143,8 +149,11 @@ const attachCopyHandler = (link) => {
143
149
  });
144
150
  };
145
151
 
146
- const initCopyMarkdownButtons = () => {
147
- const links = [...document.querySelectorAll(COPY_SELECTOR)];
152
+ const initCopyMarkdownButtons = (): void => {
153
+ const links = [...document.querySelectorAll(COPY_SELECTOR)].filter(
154
+ (link): link is HTMLAnchorElement => link instanceof HTMLAnchorElement
155
+ );
156
+
148
157
  for (const link of links) {
149
158
  attachCopyHandler(link);
150
159
  }
@@ -1,8 +1,8 @@
1
1
  (() => {
2
2
  const selector = 'a[data-prefetch="hover"][href]';
3
- const prefetched = new Set();
3
+ const prefetched = new Set<string>();
4
4
 
5
- const prefetch = (href) => {
5
+ const prefetch = (href: string | null | undefined): void => {
6
6
  if (!href || prefetched.has(href)) {
7
7
  return;
8
8
  }
@@ -14,7 +14,7 @@
14
14
  document.head.append(link);
15
15
  };
16
16
 
17
- const onOver = (event) => {
17
+ const onOver = (event: Event): void => {
18
18
  const { target } = event;
19
19
  if (!(target instanceof Element)) {
20
20
  return;