github-code 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,689 @@
1
+ // src/parsers/file-parser.ts
2
+ function parseFileAttribute(fileAttr) {
3
+ return fileAttr.split(",").map((url) => url.trim()).filter((url) => url.length > 0);
4
+ }
5
+
6
+ // src/parsers/url-parser.ts
7
+ var GITHUB_BLOB_PATTERN = /^https:\/\/github\.com\/[^\/]+\/[^\/]+\/blob\/.+/;
8
+ function isValidGitHubUrl(url) {
9
+ return GITHUB_BLOB_PATTERN.test(url);
10
+ }
11
+ function parseGitHubUrl(url) {
12
+ const match = /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.+)$/.exec(url);
13
+ if (!match) {
14
+ throw new Error("Failed to parse GitHub URL");
15
+ }
16
+ const owner = match[1];
17
+ const repo = match[2];
18
+ const commit = match[3];
19
+ const path = match[4];
20
+ if (!owner || !repo || !commit || !path) {
21
+ throw new Error("Failed to parse GitHub URL");
22
+ }
23
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${commit}/${path}`;
24
+ const filename = path.split("/").pop() || "unknown";
25
+ return {
26
+ rawUrl,
27
+ filename
28
+ };
29
+ }
30
+ function extractFilenameFromUrl(url) {
31
+ try {
32
+ const match = /\/([^\/]+)$/.exec(url);
33
+ return match?.[1] ?? "unknown";
34
+ } catch {
35
+ return "unknown";
36
+ }
37
+ }
38
+
39
+ // src/fetching/code-fetcher.ts
40
+ async function fetchCode(url) {
41
+ try {
42
+ const response = await fetch(url);
43
+ if (!response.ok) {
44
+ throw new Error(`Failed to fetch code (HTTP ${response.status}). Please check if the URL is accessible.`);
45
+ }
46
+ return await response.text();
47
+ } catch (error) {
48
+ if (error instanceof TypeError && error.message.includes("fetch")) {
49
+ throw new Error(
50
+ `Failed to fetch code from ${url}. This is likely a CORS (Cross-Origin Resource Sharing) error. The server needs to allow requests from this origin. GitHub's raw.githubusercontent.com should work without CORS issues.`
51
+ );
52
+ }
53
+ throw error;
54
+ }
55
+ }
56
+ async function ensureFileLoaded(file, isRetry = false) {
57
+ if (file.loaded && !isRetry) {
58
+ return;
59
+ }
60
+ if (isRetry) {
61
+ file.loaded = false;
62
+ file.error = null;
63
+ }
64
+ try {
65
+ const code = await fetchCode(file.rawUrl);
66
+ file.code = code;
67
+ file.error = null;
68
+ file.loaded = true;
69
+ } catch (error) {
70
+ file.code = null;
71
+ file.error = error instanceof Error ? error.message : String(error);
72
+ file.loaded = true;
73
+ }
74
+ }
75
+
76
+ // src/fetching/highlightjs-loader.ts
77
+ var highlightJSLoadingPromise = null;
78
+ async function loadHighlightJS() {
79
+ if (window.hljs) {
80
+ return;
81
+ }
82
+ if (highlightJSLoadingPromise) {
83
+ return highlightJSLoadingPromise;
84
+ }
85
+ highlightJSLoadingPromise = new Promise((resolve, reject) => {
86
+ const script = document.createElement("script");
87
+ script.src = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js";
88
+ script.integrity = "sha384-RH2xi4eIQ/gjtbs9fUXM68sLSi99C7ZWBRX1vDrVv6GQXRibxXLbwO2NGZB74MbU";
89
+ script.crossOrigin = "anonymous";
90
+ script.onload = () => {
91
+ highlightJSLoadingPromise = null;
92
+ resolve();
93
+ };
94
+ script.onerror = () => {
95
+ highlightJSLoadingPromise = null;
96
+ const errorMsg = "Failed to load highlight.js library. If you have a Content Security Policy (CSP), ensure it allows:\n script-src https://cdnjs.cloudflare.com\n style-src https://cdnjs.cloudflare.com";
97
+ reject(new Error(errorMsg));
98
+ };
99
+ document.head.appendChild(script);
100
+ });
101
+ return highlightJSLoadingPromise;
102
+ }
103
+
104
+ // src/styles/base-light.css
105
+ var base_light_default = ":host {\n --border-color: #d0d7de;\n --header-background: #f6f8fa;\n --header-text-color: #24292f;\n --line-number-color: #57606a;\n --tab-color: #57606a;\n --tab-hover-border: #d0d7de;\n --tabs-background: #f6f8fa;\n --code-background: #ffffff;\n --skeleton-base: #e0e0e0;\n --skeleton-highlight: #f0f0f0;\n --error-text-color: #cf222e;\n --error-background: #ffebe9;\n --error-border: #ff8182;\n --button-background: #f6f8fa;\n --button-text-color: #24292f;\n --button-border: #d0d7de;\n --button-hover-background: #f3f4f6;\n --font-family-base:\n -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 'Apple Color Emoji',\n 'Segoe UI Emoji';\n --font-family-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;\n\n display: block;\n min-width: 0;\n font-family: var(--font-family-base);\n}\n\narticle {\n border: 1px solid var(--border-color);\n border-radius: 6px;\n overflow: hidden;\n background-color: var(--code-background);\n}\n\nheader {\n background-color: var(--header-background);\n padding: 8px 16px;\n border-bottom: 1px solid var(--border-color);\n color: var(--header-text-color);\n font-weight: 600;\n font-size: 14px;\n}\n\n.code-wrapper {\n width: 100%;\n margin: 0;\n overflow-x: auto;\n background-color: var(--code-background);\n}\n\n.code-table {\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n}\n\n.code-row {\n display: flex;\n}\n\n.line-number {\n flex-shrink: 0;\n text-align: right;\n padding-right: 6px;\n padding-left: 8px;\n color: var(--line-number-color);\n user-select: none;\n border-right: 1px solid var(--border-color);\n font-family: var(--font-family-mono);\n font-size: 12px;\n line-height: 20px;\n min-width: 40px;\n}\n\n.code-cell {\n flex: 1;\n min-width: 0;\n padding-left: 2px;\n font-family: var(--font-family-mono);\n font-size: 12px;\n line-height: 20px;\n white-space: pre;\n}\n\n.error {\n padding: 16px;\n color: var(--error-text-color);\n background-color: var(--error-background);\n border: 1px solid var(--error-border);\n border-radius: 6px;\n font-family: var(--font-family-mono);\n font-size: 12px;\n line-height: 1.5;\n margin: 16px;\n}\n\n.retry-button {\n margin-top: 12px;\n padding: 8px 16px;\n background: var(--button-background);\n color: var(--button-text-color);\n border: 1px solid var(--button-border);\n border-radius: 6px;\n cursor: pointer;\n font-size: 14px;\n font-family: var(--font-family-base);\n font-weight: 500;\n transition: background-color 0.2s ease;\n}\n\n.retry-button:hover {\n background-color: var(--button-hover-background);\n}\n\n/* Skeleton loader styles */\n.skeleton-line {\n height: 12px;\n background: linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-highlight) 50%, var(--skeleton-base) 75%);\n background-size: 200% 100%;\n animation: loading 1.5s ease-in-out infinite;\n border-radius: 4px;\n margin: 4px 0;\n}\n\n@keyframes loading {\n 0% {\n background-position: 200% 0;\n }\n 100% {\n background-position: -200% 0;\n }\n}\n\n.skeleton-loading .line-number {\n opacity: 0.3;\n}\n";
106
+
107
+ // src/styles/base-dark.css
108
+ var base_dark_default = ":host {\n --border-color: #30363d;\n --header-background: #161b22;\n --header-text-color: #c9d1d9;\n --line-number-color: #8b949e;\n --tab-color: #8b949e;\n --tab-hover-border: #30363d;\n --tabs-background: #161b22;\n --code-background: #0d1117;\n --skeleton-base: #21262d;\n --skeleton-highlight: #30363d;\n --error-text-color: #ff7b72;\n --error-background: #490202;\n --error-border: #f85149;\n --button-background: #21262d;\n --button-text-color: #c9d1d9;\n --button-border: #30363d;\n --button-hover-background: #30363d;\n --font-family-base:\n -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 'Apple Color Emoji',\n 'Segoe UI Emoji';\n --font-family-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;\n\n display: block;\n min-width: 0;\n font-family: var(--font-family-base);\n}\n\narticle {\n border: 1px solid var(--border-color);\n border-radius: 6px;\n overflow: hidden;\n background-color: var(--code-background);\n}\n\nheader {\n background-color: var(--header-background);\n padding: 8px 16px;\n border-bottom: 1px solid var(--border-color);\n color: var(--header-text-color);\n font-weight: 600;\n font-size: 14px;\n}\n\n.code-wrapper {\n width: 100%;\n margin: 0;\n overflow-x: auto;\n background-color: var(--code-background);\n}\n\n.code-table {\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n}\n\n.code-row {\n display: flex;\n}\n\n.line-number {\n flex-shrink: 0;\n text-align: right;\n padding-right: 6px;\n padding-left: 8px;\n color: var(--line-number-color);\n user-select: none;\n border-right: 1px solid var(--border-color);\n font-family: var(--font-family-mono);\n font-size: 12px;\n line-height: 20px;\n min-width: 40px;\n}\n\n.code-cell {\n flex: 1;\n min-width: 0;\n padding-left: 2px;\n font-family: var(--font-family-mono);\n font-size: 12px;\n line-height: 20px;\n white-space: pre;\n}\n\n.error {\n padding: 16px;\n color: var(--error-text-color);\n background-color: var(--error-background);\n border: 1px solid var(--error-border);\n border-radius: 6px;\n font-family: var(--font-family-mono);\n font-size: 12px;\n line-height: 1.5;\n margin: 16px;\n}\n\n.retry-button {\n margin-top: 12px;\n padding: 8px 16px;\n background: var(--button-background);\n color: var(--button-text-color);\n border: 1px solid var(--button-border);\n border-radius: 6px;\n cursor: pointer;\n font-size: 14px;\n font-family: var(--font-family-base);\n font-weight: 500;\n transition: background-color 0.2s ease;\n}\n\n.retry-button:hover {\n background-color: var(--button-hover-background);\n}\n\n/* Skeleton loader styles */\n.skeleton-line {\n height: 12px;\n background: linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-highlight) 50%, var(--skeleton-base) 75%);\n background-size: 200% 100%;\n animation: loading 1.5s ease-in-out infinite;\n border-radius: 4px;\n margin: 4px 0;\n}\n\n@keyframes loading {\n 0% {\n background-position: 200% 0;\n }\n 100% {\n background-position: -200% 0;\n }\n}\n\n.skeleton-loading .line-number {\n opacity: 0.3;\n}\n";
109
+
110
+ // src/styles/tab.css
111
+ var tab_default = "nav[role='tablist'] {\n display: flex;\n background-color: var(--tabs-background);\n border-bottom: 1px solid var(--border-color);\n overflow-x: auto;\n}\n\nnav > button {\n padding: 8px 16px;\n background: none;\n border: none;\n border-bottom: 2px solid transparent;\n color: var(--tab-color);\n font-size: 14px;\n font-family: var(--font-family-base);\n cursor: pointer;\n white-space: nowrap;\n transition:\n color 0.1s ease,\n border-color 0.1s ease;\n}\n\nnav > button:hover {\n color: var(--header-text-color);\n border-bottom-color: var(--tab-hover-border);\n}\n\nnav > button[aria-selected='true'] {\n color: var(--header-text-color);\n border-bottom-color: #fd8c73;\n font-weight: 600;\n}\n\nsection[role='tabpanel'] {\n display: block;\n}\n\nsection[role='tabpanel'][aria-hidden='true'] {\n display: none;\n}\n";
112
+
113
+ // src/styles/stylesheet-manager.ts
114
+ var StylesheetManager = class {
115
+ static baseStylesLight = null;
116
+ static baseStylesDark = null;
117
+ static tabStyles = null;
118
+ /**
119
+ * Gets the base stylesheet for the specified theme (light or dark).
120
+ * Lazy-loads and caches the stylesheet on first access.
121
+ */
122
+ static getBaseStyleSheet(theme) {
123
+ if (theme === "dark") {
124
+ if (!this.baseStylesDark) {
125
+ this.baseStylesDark = new CSSStyleSheet();
126
+ this.baseStylesDark.replaceSync(base_dark_default);
127
+ }
128
+ return this.baseStylesDark;
129
+ } else {
130
+ if (!this.baseStylesLight) {
131
+ this.baseStylesLight = new CSSStyleSheet();
132
+ this.baseStylesLight.replaceSync(base_light_default);
133
+ }
134
+ return this.baseStylesLight;
135
+ }
136
+ }
137
+ /**
138
+ * Gets the tab stylesheet.
139
+ * Lazy-loads and caches the stylesheet on first access.
140
+ */
141
+ static getTabStyleSheet() {
142
+ if (!this.tabStyles) {
143
+ this.tabStyles = new CSSStyleSheet();
144
+ this.tabStyles.replaceSync(tab_default);
145
+ }
146
+ return this.tabStyles;
147
+ }
148
+ /**
149
+ * Gets the highlight.js theme URL for the specified theme.
150
+ */
151
+ static getHighlightJSThemeUrl(theme) {
152
+ const themeFile = theme === "dark" ? "github-dark.min.css" : "github.min.css";
153
+ return `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/${themeFile}`;
154
+ }
155
+ };
156
+
157
+ // src/rendering/html-generators.ts
158
+ function escapeHtml(text) {
159
+ const div = document.createElement("div");
160
+ div.textContent = text;
161
+ return div.innerHTML;
162
+ }
163
+ function getErrorContentHtml(errorMessage, showRetry = false) {
164
+ const retryButton = showRetry ? `<button class="retry-button">Retry</button>` : "";
165
+ return `<div class="error">${escapeHtml(errorMessage)}${retryButton}</div>`;
166
+ }
167
+ function getSkeletonContentHtml() {
168
+ const skeletonLines = Array.from({ length: 20 }, (_, i) => {
169
+ const width = 60 + Math.random() * 30;
170
+ return `
171
+ <div class="code-row">
172
+ <div class="line-number">${i + 1}</div>
173
+ <div class="skeleton-line" style="width: ${width}%"></div>
174
+ </div>
175
+ `;
176
+ }).join("");
177
+ return `
178
+ <div class="code-wrapper skeleton-loading">
179
+ <div class="code-table">
180
+ ${skeletonLines}
181
+ </div>
182
+ </div>
183
+ `;
184
+ }
185
+ function getCodeContentHtml(code) {
186
+ if (!code) {
187
+ return getErrorContentHtml("No code content available");
188
+ }
189
+ const lines = code.split("\n");
190
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
191
+ lines.pop();
192
+ }
193
+ return `
194
+ <div class="code-wrapper hljs">
195
+ <div class="code-table">
196
+ ${lines.map(
197
+ (line, index) => `
198
+ <div class="code-row">
199
+ <div class="line-number">${index + 1}</div>
200
+ <div class="code-cell">${escapeHtml(line) || " "}</div>
201
+ </div>
202
+ `
203
+ ).join("")}
204
+ </div>
205
+ </div>
206
+ `;
207
+ }
208
+
209
+ // src/rendering/template-generators.ts
210
+ function generateSingleFileTemplate(filename, content, themeStylesheetUrl) {
211
+ return `<link rel="stylesheet" href="${themeStylesheetUrl}">
212
+ <article>
213
+ <header>${escapeHtml(filename)}</header>
214
+ ${content}
215
+ </article>`;
216
+ }
217
+ function generateArticleContent(filename, content) {
218
+ return `<header>${escapeHtml(filename)}</header>
219
+ ${content}`;
220
+ }
221
+ function generateTabbedTemplate(tabsHtml, activeIndex, panelContent, themeStylesheetUrl) {
222
+ return `<link rel="stylesheet" href="${themeStylesheetUrl}">
223
+ <article>
224
+ <nav role="tablist" aria-label="Code files">${tabsHtml}</nav>
225
+ <section role="tabpanel"
226
+ id="panel-${activeIndex}"
227
+ aria-labelledby="tab-${activeIndex}"
228
+ data-index="${activeIndex}">
229
+ ${panelContent}
230
+ </section>
231
+ </article>`;
232
+ }
233
+
234
+ // src/rendering/syntax-highlighter.ts
235
+ function getHighlightedLines(code) {
236
+ if (!code) {
237
+ return [""];
238
+ }
239
+ const lines = code.split("\n");
240
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
241
+ lines.pop();
242
+ }
243
+ const fullCode = lines.join("\n");
244
+ const result = window.hljs.highlightAuto(fullCode);
245
+ const highlightedLines = result.value.split("\n");
246
+ while (highlightedLines.length > lines.length) {
247
+ highlightedLines.pop();
248
+ }
249
+ return highlightedLines;
250
+ }
251
+ function applySyntaxHighlighting(container, code) {
252
+ const codeCells = container.querySelectorAll(".code-cell");
253
+ const applyHighlighting = () => {
254
+ const highlightedLines = getHighlightedLines(code);
255
+ codeCells.forEach((cell, index) => {
256
+ cell.innerHTML = highlightedLines[index] || " ";
257
+ });
258
+ };
259
+ if ("requestIdleCallback" in window) {
260
+ requestIdleCallback(applyHighlighting);
261
+ } else {
262
+ applyHighlighting();
263
+ }
264
+ }
265
+
266
+ // src/theme/theme-resolver.ts
267
+ function resolveTheme(themeAttr) {
268
+ if (themeAttr === "dark") {
269
+ return "dark";
270
+ }
271
+ if (themeAttr === "light") {
272
+ return "light";
273
+ }
274
+ const prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
275
+ return prefersDark ? "dark" : "light";
276
+ }
277
+ function getThemeAttribute(element) {
278
+ const attr = element.getAttribute("theme");
279
+ if (attr === "dark" || attr === "light") {
280
+ return attr;
281
+ }
282
+ return "auto";
283
+ }
284
+
285
+ // src/tabs/tab-state.ts
286
+ var TabState = class {
287
+ activeTabIndex = 0;
288
+ renderedTabs = /* @__PURE__ */ new Set();
289
+ tabsFullyRendered = false;
290
+ /**
291
+ * Gets the currently active tab index
292
+ */
293
+ getActiveTabIndex() {
294
+ return this.activeTabIndex;
295
+ }
296
+ /**
297
+ * Sets the active tab index
298
+ */
299
+ setActiveTabIndex(index) {
300
+ this.activeTabIndex = index;
301
+ }
302
+ /**
303
+ * Checks if tabs structure is fully rendered with event listeners
304
+ */
305
+ areTabsFullyRendered() {
306
+ return this.tabsFullyRendered;
307
+ }
308
+ /**
309
+ * Marks tabs as fully rendered
310
+ */
311
+ setTabsFullyRendered(value) {
312
+ this.tabsFullyRendered = value;
313
+ }
314
+ /**
315
+ * Checks if a tab has been rendered
316
+ */
317
+ isTabRendered(index) {
318
+ return this.renderedTabs.has(index);
319
+ }
320
+ /**
321
+ * Marks a tab as rendered
322
+ */
323
+ markTabAsRendered(index) {
324
+ this.renderedTabs.add(index);
325
+ }
326
+ /**
327
+ * Clears all rendered tabs
328
+ */
329
+ clearRenderedTabs() {
330
+ this.renderedTabs.clear();
331
+ }
332
+ /**
333
+ * Gets the total number of tabs
334
+ */
335
+ getTabCount(files) {
336
+ return files.length;
337
+ }
338
+ /**
339
+ * Resets tab state (useful when re-parsing files)
340
+ */
341
+ reset() {
342
+ this.renderedTabs.clear();
343
+ this.tabsFullyRendered = false;
344
+ }
345
+ };
346
+
347
+ // src/tabs/tab-controller.ts
348
+ function generateTabsHtml(files, activeTabIndex) {
349
+ return files.map(
350
+ (file, index) => `
351
+ <button role="tab"
352
+ id="tab-${index}"
353
+ aria-selected="${index === activeTabIndex ? "true" : "false"}"
354
+ aria-controls="panel-${index}"
355
+ tabindex="${index === activeTabIndex ? "0" : "-1"}"
356
+ data-index="${index}">
357
+ ${escapeHtml(file.filename)}
358
+ </button>
359
+ `
360
+ ).join("");
361
+ }
362
+ function updateTabButtonStates(shadowRoot, newActiveIndex) {
363
+ const tabs = shadowRoot.querySelectorAll("nav > button");
364
+ tabs.forEach((tab) => {
365
+ const tabIndex = parseInt(tab.dataset.index || "0");
366
+ const isSelected = tabIndex === newActiveIndex;
367
+ tab.setAttribute("aria-selected", isSelected ? "true" : "false");
368
+ tab.setAttribute("tabindex", isSelected ? "0" : "-1");
369
+ });
370
+ }
371
+ function updateTabPanelStates(shadowRoot, activeIndex) {
372
+ const allPanels = shadowRoot.querySelectorAll('section[role="tabpanel"]');
373
+ allPanels.forEach((panel) => {
374
+ const panelIndex = parseInt(panel.dataset.index || "0");
375
+ panel.setAttribute("aria-hidden", panelIndex !== activeIndex ? "true" : "false");
376
+ });
377
+ }
378
+ function createTabPanel(index, skeletonHtml) {
379
+ const contentElement = document.createElement("section");
380
+ contentElement.setAttribute("role", "tabpanel");
381
+ contentElement.setAttribute("id", `panel-${index}`);
382
+ contentElement.setAttribute("aria-labelledby", `tab-${index}`);
383
+ contentElement.dataset.index = String(index);
384
+ contentElement.innerHTML = skeletonHtml;
385
+ return contentElement;
386
+ }
387
+ function handleTabKeyNavigation(key, currentIndex, maxIndex) {
388
+ switch (key) {
389
+ case "ArrowLeft":
390
+ return currentIndex > 0 ? currentIndex - 1 : maxIndex;
391
+ case "ArrowRight":
392
+ return currentIndex < maxIndex ? currentIndex + 1 : 0;
393
+ case "Home":
394
+ return 0;
395
+ case "End":
396
+ return maxIndex;
397
+ default:
398
+ return null;
399
+ }
400
+ }
401
+
402
+ // src/github-code.ts
403
+ var GitHubCode = class extends HTMLElement {
404
+ // Private fields
405
+ #files = [];
406
+ #tabState = new TabState();
407
+ #resolvedTheme = null;
408
+ #themeMediaQuery = null;
409
+ #themeChangeHandler = null;
410
+ constructor() {
411
+ super();
412
+ this.attachShadow({ mode: "open" });
413
+ }
414
+ /**
415
+ * Safely gets a file by index, throwing if out of bounds.
416
+ * This provides type-safe array access with noUncheckedIndexedAccess.
417
+ */
418
+ #getFile(index) {
419
+ const file = this.#files[index];
420
+ if (!file) {
421
+ throw new Error(`File at index ${index} not found`);
422
+ }
423
+ return file;
424
+ }
425
+ static get observedAttributes() {
426
+ return ["file"];
427
+ }
428
+ connectedCallback() {
429
+ this.#setupThemeListener();
430
+ void this.#render();
431
+ }
432
+ disconnectedCallback() {
433
+ if (this.#themeMediaQuery && this.#themeChangeHandler) {
434
+ this.#themeMediaQuery.removeEventListener("change", this.#themeChangeHandler);
435
+ }
436
+ }
437
+ attributeChangedCallback(name, oldValue, newValue) {
438
+ if (name === "file" && oldValue !== null && oldValue !== newValue) {
439
+ const previousIndex = this.#tabState.getActiveTabIndex();
440
+ this.#tabState.reset();
441
+ void this.#render();
442
+ if (previousIndex < this.#files.length) {
443
+ this.#tabState.setActiveTabIndex(previousIndex);
444
+ } else {
445
+ this.#tabState.setActiveTabIndex(0);
446
+ }
447
+ }
448
+ }
449
+ // Private methods - theming
450
+ #setupThemeListener() {
451
+ if (window.matchMedia) {
452
+ this.#themeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
453
+ this.#themeChangeHandler = () => {
454
+ this.#resolvedTheme = null;
455
+ void this.#render();
456
+ };
457
+ this.#themeMediaQuery.addEventListener("change", this.#themeChangeHandler);
458
+ }
459
+ }
460
+ #getResolvedTheme() {
461
+ if (this.#resolvedTheme) {
462
+ return this.#resolvedTheme;
463
+ }
464
+ this.#resolvedTheme = resolveTheme(getThemeAttribute(this));
465
+ return this.#resolvedTheme;
466
+ }
467
+ // Apply constructable stylesheets to shadow root (CSP-compliant, no 'unsafe-inline' needed)
468
+ #applyStyleSheets(includeTabStyles = false) {
469
+ const theme = this.#getResolvedTheme();
470
+ const baseSheet = StylesheetManager.getBaseStyleSheet(theme);
471
+ if (includeTabStyles) {
472
+ const tabSheet = StylesheetManager.getTabStyleSheet();
473
+ this.shadowRoot.adoptedStyleSheets = [baseSheet, tabSheet];
474
+ } else {
475
+ this.shadowRoot.adoptedStyleSheets = [baseSheet];
476
+ }
477
+ }
478
+ // Private methods - rendering
479
+ async #render() {
480
+ const fileAttr = this.getAttribute("file");
481
+ if (!fileAttr) {
482
+ this.#showError('Error: "file" attribute is required. Please provide a GitHub file URL.');
483
+ return;
484
+ }
485
+ const fileUrls = parseFileAttribute(fileAttr);
486
+ if (fileUrls.length === 0) {
487
+ this.#showError('Error: "file" attribute is required. Please provide a GitHub file URL.');
488
+ return;
489
+ }
490
+ const invalidUrl = fileUrls.find((url) => !isValidGitHubUrl(url));
491
+ if (invalidUrl) {
492
+ const sanitizedUrl = escapeHtml(invalidUrl);
493
+ this.#showError(
494
+ `Error: Invalid GitHub URL format: ${sanitizedUrl}. Expected format: https://github.com/{owner}/{repo}/blob/{commit}/{path}`
495
+ );
496
+ return;
497
+ }
498
+ this.#files = fileUrls.map((url) => {
499
+ try {
500
+ const { rawUrl, filename } = parseGitHubUrl(url);
501
+ return {
502
+ filename,
503
+ rawUrl,
504
+ url,
505
+ code: null,
506
+ error: null,
507
+ loaded: false
508
+ };
509
+ } catch (error) {
510
+ const filename = extractFilenameFromUrl(url);
511
+ return {
512
+ filename,
513
+ rawUrl: url,
514
+ url,
515
+ code: null,
516
+ error: error instanceof Error ? error.message : String(error),
517
+ loaded: true
518
+ // Parse error - no point retrying
519
+ };
520
+ }
521
+ });
522
+ if (fileUrls.length === 1) {
523
+ const file = this.#getFile(0);
524
+ if (file.error) {
525
+ this.#showError(`Error loading code: ${file.error}`);
526
+ return;
527
+ }
528
+ this.shadowRoot.innerHTML = generateSingleFileTemplate(
529
+ file.filename,
530
+ getSkeletonContentHtml(),
531
+ StylesheetManager.getHighlightJSThemeUrl(this.#getResolvedTheme())
532
+ );
533
+ this.#applyStyleSheets(false);
534
+ } else {
535
+ this.#renderTabsSkeletonStructure();
536
+ }
537
+ try {
538
+ await loadHighlightJS();
539
+ } catch (error) {
540
+ const errorMsg = error instanceof Error ? error.message : String(error);
541
+ this.#showError(`Error loading highlight.js: ${errorMsg}`);
542
+ return;
543
+ }
544
+ if (fileUrls.length === 1) {
545
+ await this.#displayCode(0);
546
+ } else {
547
+ await this.#displayWithTabs();
548
+ }
549
+ }
550
+ async #displayCode(index) {
551
+ const file = this.#getFile(index);
552
+ const themeUrl = StylesheetManager.getHighlightJSThemeUrl(this.#getResolvedTheme());
553
+ this.shadowRoot.innerHTML = generateSingleFileTemplate(file.filename, getSkeletonContentHtml(), themeUrl);
554
+ this.#applyStyleSheets(false);
555
+ await ensureFileLoaded(file, false);
556
+ const contentArea = this.shadowRoot.querySelector("article");
557
+ if (file.error) {
558
+ contentArea.innerHTML = generateArticleContent(file.filename, getErrorContentHtml(file.error, true));
559
+ const retryButton = contentArea.querySelector(".retry-button");
560
+ if (retryButton) {
561
+ retryButton.addEventListener("click", () => {
562
+ void (async () => {
563
+ contentArea.innerHTML = generateArticleContent(file.filename, getSkeletonContentHtml());
564
+ await ensureFileLoaded(file, true);
565
+ await this.#displayCode(index);
566
+ })();
567
+ });
568
+ }
569
+ } else {
570
+ contentArea.innerHTML = generateArticleContent(file.filename, getCodeContentHtml(file.code));
571
+ if (window.hljs) {
572
+ applySyntaxHighlighting(this.shadowRoot, file.code);
573
+ }
574
+ }
575
+ }
576
+ async #displayWithTabs() {
577
+ if (!this.#tabState.areTabsFullyRendered()) {
578
+ await this.#renderTabsStructure();
579
+ this.#tabState.setTabsFullyRendered(true);
580
+ } else {
581
+ this.#switchToTab(this.#tabState.getActiveTabIndex());
582
+ }
583
+ }
584
+ #renderTabsSkeletonStructure() {
585
+ const activeIndex = this.#tabState.getActiveTabIndex();
586
+ const tabsHtml = generateTabsHtml(this.#files, activeIndex);
587
+ const themeUrl = StylesheetManager.getHighlightJSThemeUrl(this.#getResolvedTheme());
588
+ this.shadowRoot.innerHTML = generateTabbedTemplate(tabsHtml, activeIndex, getSkeletonContentHtml(), themeUrl);
589
+ this.#applyStyleSheets(true);
590
+ }
591
+ async #renderTabsStructure() {
592
+ const activeIndex = this.#tabState.getActiveTabIndex();
593
+ const tabsHtml = generateTabsHtml(this.#files, activeIndex);
594
+ const themeUrl = StylesheetManager.getHighlightJSThemeUrl(this.#getResolvedTheme());
595
+ this.shadowRoot.innerHTML = generateTabbedTemplate(tabsHtml, activeIndex, getSkeletonContentHtml(), themeUrl);
596
+ this.#applyStyleSheets(true);
597
+ const nav = this.shadowRoot.querySelector('nav[role="tablist"]');
598
+ nav.addEventListener("click", (e) => {
599
+ const tab = e.target.closest('button[role="tab"]');
600
+ if (tab) {
601
+ const newIndex = parseInt(tab.dataset.index || "0");
602
+ if (newIndex !== this.#tabState.getActiveTabIndex()) {
603
+ this.#tabState.setActiveTabIndex(newIndex);
604
+ void this.#displayWithTabs();
605
+ }
606
+ }
607
+ });
608
+ nav.addEventListener("keydown", (e) => {
609
+ const keyboardEvent = e;
610
+ const tab = keyboardEvent.target.closest('button[role="tab"]');
611
+ if (!tab) {
612
+ return;
613
+ }
614
+ const currentIndex = this.#tabState.getActiveTabIndex();
615
+ const maxIndex = this.#files.length - 1;
616
+ const newIndex = handleTabKeyNavigation(keyboardEvent.key, currentIndex, maxIndex);
617
+ if (newIndex !== null) {
618
+ e.preventDefault();
619
+ this.#tabState.setActiveTabIndex(newIndex);
620
+ void this.#displayWithTabs().then(() => {
621
+ const newTab = this.shadowRoot.querySelector(`button[data-index="${newIndex}"]`);
622
+ if (newTab) {
623
+ newTab.focus();
624
+ }
625
+ });
626
+ }
627
+ });
628
+ this.#tabState.markTabAsRendered(activeIndex);
629
+ const contentElement = this.shadowRoot.querySelector('section[role="tabpanel"]');
630
+ await this.#loadAndRenderTabContent(activeIndex, contentElement).catch((error) => {
631
+ console.error("Failed to load tab content:", error);
632
+ contentElement.innerHTML = getErrorContentHtml(
633
+ `Failed to load content: ${error instanceof Error ? error.message : String(error)}`
634
+ );
635
+ });
636
+ }
637
+ #switchToTab(newIndex) {
638
+ updateTabButtonStates(this.shadowRoot, newIndex);
639
+ let contentElement = this.shadowRoot.querySelector(`section[role="tabpanel"][data-index="${newIndex}"]`);
640
+ if (!contentElement) {
641
+ contentElement = createTabPanel(newIndex, getSkeletonContentHtml());
642
+ this.shadowRoot.querySelector("article").appendChild(contentElement);
643
+ this.#tabState.markTabAsRendered(newIndex);
644
+ void this.#loadAndRenderTabContent(newIndex, contentElement).catch((error) => {
645
+ console.error("Failed to load tab content:", error);
646
+ contentElement.innerHTML = getErrorContentHtml(
647
+ `Failed to load content: ${error instanceof Error ? error.message : String(error)}`
648
+ );
649
+ });
650
+ }
651
+ updateTabPanelStates(this.shadowRoot, newIndex);
652
+ }
653
+ async #loadAndRenderTabContent(index, contentElement) {
654
+ const file = this.#getFile(index);
655
+ await ensureFileLoaded(file, false);
656
+ if (file.error || !file.code) {
657
+ const errorMsg = file.error || "Failed to load content: No content available";
658
+ contentElement.innerHTML = getErrorContentHtml(errorMsg, true);
659
+ const retryButton = contentElement.querySelector(".retry-button");
660
+ if (retryButton) {
661
+ retryButton.addEventListener("click", () => {
662
+ void (async () => {
663
+ contentElement.innerHTML = getSkeletonContentHtml();
664
+ await ensureFileLoaded(file, true);
665
+ await this.#loadAndRenderTabContent(index, contentElement);
666
+ })();
667
+ });
668
+ }
669
+ } else {
670
+ contentElement.innerHTML = getCodeContentHtml(file.code);
671
+ if (window.hljs) {
672
+ applySyntaxHighlighting(contentElement, file.code);
673
+ }
674
+ }
675
+ }
676
+ #showError(message) {
677
+ this.shadowRoot.innerHTML = `
678
+ <div class="error">${escapeHtml(message)}</div>
679
+ `;
680
+ this.#applyStyleSheets(false);
681
+ }
682
+ };
683
+
684
+ // src/index.ts
685
+ customElements.define("github-code", GitHubCode);
686
+ export {
687
+ GitHubCode
688
+ };
689
+ //# sourceMappingURL=github-code.js.map