mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-17 19:31:20 +00:00
chore: publish from main
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { marked } from "marked";
|
||||
import matter from "gray-matter";
|
||||
import { enhanceMarkdownA11y } from "./markdown-a11y";
|
||||
import { sanitizeHtml } from "./sanitize-html";
|
||||
|
||||
/**
|
||||
* Build-time helpers shared by the resource detail pages
|
||||
* (src/pages/agent/[id].astro, src/pages/instruction/[id].astro, ...).
|
||||
*
|
||||
* This module is intentionally free of any DOM/browser dependencies so it can
|
||||
* run in Astro frontmatter at build time (unlike src/scripts/utils.ts, which is
|
||||
* client-side).
|
||||
*/
|
||||
|
||||
const RAW_BASE =
|
||||
"https://raw.githubusercontent.com/github/awesome-copilot/main";
|
||||
const GITHUB_BASE = "https://github.com/github/awesome-copilot/blob/main";
|
||||
|
||||
// Per resource-type VS Code install configuration.
|
||||
const INSTALL_CONFIG = {
|
||||
agent: {
|
||||
command: "chat-agent",
|
||||
baseUrl: "https://aka.ms/awesome-copilot/install/agent",
|
||||
},
|
||||
instruction: {
|
||||
command: "chat-instructions",
|
||||
baseUrl: "https://aka.ms/awesome-copilot/install/instructions",
|
||||
},
|
||||
} as const satisfies Record<string, { command: string; baseUrl: string }>;
|
||||
|
||||
export type ResourceType = keyof typeof INSTALL_CONFIG;
|
||||
|
||||
export interface DetailPageInput {
|
||||
path: string;
|
||||
lastUpdated?: string | null;
|
||||
}
|
||||
|
||||
export interface DetailPageData {
|
||||
vscodeUrl: string;
|
||||
insidersUrl: string;
|
||||
githubUrl: string;
|
||||
markdownHtml: string;
|
||||
frontmatterText: string;
|
||||
rawMarkdown: string;
|
||||
lastUpdated: string | null;
|
||||
}
|
||||
|
||||
// Repo root. Astro runs (both `dev` and `build`) execute with the website/
|
||||
// directory as the working directory, so the repo root is its parent. This is
|
||||
// resolved from the working directory rather than `import.meta.url` because the
|
||||
// latter is unreliable once this module is bundled during a production build.
|
||||
const repoRoot = path.resolve(process.cwd(), "..");
|
||||
|
||||
function buildInstallUrl(
|
||||
filePath: string,
|
||||
type: ResourceType,
|
||||
insiders: boolean
|
||||
): string {
|
||||
const { command, baseUrl } = INSTALL_CONFIG[type];
|
||||
const rawUrl = `${RAW_BASE}/${filePath}`;
|
||||
const editor = insiders ? "vscode-insiders" : "vscode";
|
||||
const innerUrl = `${editor}:${command}/install?url=${encodeURIComponent(rawUrl)}`;
|
||||
return `${baseUrl}?url=${encodeURIComponent(innerUrl)}`;
|
||||
}
|
||||
|
||||
export function formatLastUpdated(
|
||||
lastUpdated?: string | null
|
||||
): string | null {
|
||||
return lastUpdated
|
||||
? new Date(lastUpdated).toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a resource's markdown file at build time and return its rendered HTML,
|
||||
* trimmed frontmatter block, and the raw file contents.
|
||||
*/
|
||||
export function readResourceMarkdown(filePath: string): {
|
||||
markdownHtml: string;
|
||||
frontmatterText: string;
|
||||
rawMarkdown: string;
|
||||
} {
|
||||
let markdownHtml = "";
|
||||
let frontmatterText = "";
|
||||
let rawMarkdown = "";
|
||||
try {
|
||||
const raw = fs.readFileSync(path.join(repoRoot, filePath), "utf-8");
|
||||
rawMarkdown = raw;
|
||||
const parsed = matter(raw);
|
||||
frontmatterText = parsed.matter?.trim() ?? "";
|
||||
markdownHtml = enhanceMarkdownA11y(
|
||||
sanitizeHtml(marked.parse(parsed.content, { async: false }) as string)
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(
|
||||
`[detail-page] Failed to read or parse markdown for "${filePath}": ${message}`
|
||||
);
|
||||
markdownHtml = "";
|
||||
}
|
||||
return { markdownHtml, frontmatterText, rawMarkdown };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build every piece of shared data a resource detail page needs: install URLs,
|
||||
* the GitHub source URL, rendered/raw markdown, and the formatted update date.
|
||||
*/
|
||||
export function loadDetailPage(
|
||||
item: DetailPageInput,
|
||||
type: ResourceType
|
||||
): DetailPageData {
|
||||
return {
|
||||
vscodeUrl: buildInstallUrl(item.path, type, false),
|
||||
insidersUrl: buildInstallUrl(item.path, type, true),
|
||||
githubUrl: `${GITHUB_BASE}/${item.path}`,
|
||||
...readResourceMarkdown(item.path),
|
||||
lastUpdated: formatLastUpdated(item.lastUpdated),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Shared helpers for building safe GitHub links to external (third-party)
|
||||
* plugin/extension sources.
|
||||
*
|
||||
* This module is intentionally dependency-free (no DOM or node imports) so it
|
||||
* can run both at build time (in Astro frontmatter, e.g. plugin/[id].astro and
|
||||
* extension/[id].astro) and on the client (plugins-render.ts, modal.ts).
|
||||
*/
|
||||
|
||||
export interface ExternalSource {
|
||||
source?: string;
|
||||
repo?: string;
|
||||
path?: string;
|
||||
ref?: string;
|
||||
sha?: string;
|
||||
}
|
||||
|
||||
const GITHUB_REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
||||
// Tags, branch names, or a 40-char commit SHA. Deliberately excludes
|
||||
// whitespace, "..", and characters that could break out of the path segment.
|
||||
const GITHUB_REF_RE = /^(?!\/)(?!.*\/\/)(?!.*\.\.)(?!.*\/$)[A-Za-z0-9._/-]+$/;
|
||||
|
||||
/**
|
||||
* Allow only http(s) URLs; return "#" for anything else (mirrors the
|
||||
* client-side sanitizeUrl in scripts/utils.ts). Prevents javascript:/data:
|
||||
* schemes from reaching an href/src attribute.
|
||||
*/
|
||||
export function sanitizeHttpUrl(url: string | null | undefined): string {
|
||||
if (!url) return "#";
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
||||
return url;
|
||||
}
|
||||
} catch {
|
||||
// Invalid URL
|
||||
}
|
||||
return "#";
|
||||
}
|
||||
|
||||
function encodeRepoPath(rawPath: string): string {
|
||||
return rawPath
|
||||
.replace(/\\/g, "/")
|
||||
.split("/")
|
||||
.filter((segment) => segment !== "")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a GitHub URL for an external plugin/extension source, pinned to the
|
||||
* most immutable revision available (sha, then ref, then the default branch).
|
||||
* Path segments are URL-encoded and a leading "/" (or a bare "/" path) is
|
||||
* treated as "no path".
|
||||
*
|
||||
* When the source is not a valid GitHub repo reference, falls back to the first
|
||||
* safe http(s) URL in `fallbackUrls`, or "#" if none is provided.
|
||||
*/
|
||||
export function externalRepoUrl(
|
||||
source: ExternalSource | null | undefined,
|
||||
fallbackUrls: Array<string | null | undefined> = []
|
||||
): string {
|
||||
if (
|
||||
source?.source === "github" &&
|
||||
typeof source.repo === "string" &&
|
||||
GITHUB_REPO_RE.test(source.repo)
|
||||
) {
|
||||
const base = `https://github.com/${source.repo}`;
|
||||
const candidateRef = source.sha || source.ref;
|
||||
const ref =
|
||||
candidateRef && GITHUB_REF_RE.test(candidateRef) ? candidateRef : "";
|
||||
const path =
|
||||
source.path && source.path !== "/" ? encodeRepoPath(source.path) : "";
|
||||
|
||||
if (path) {
|
||||
return `${base}/tree/${ref || "main"}/${path}`;
|
||||
}
|
||||
return ref ? `${base}/tree/${ref}` : base;
|
||||
}
|
||||
|
||||
for (const url of fallbackUrls) {
|
||||
const safe = sanitizeHttpUrl(url);
|
||||
if (safe !== "#") return safe;
|
||||
}
|
||||
return "#";
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Map a file name to a Shiki language id and a coarse "kind" used by the file
|
||||
* browser to decide how to render it (markdown prose, highlighted code, image,
|
||||
* or a plain fallback). Shared between the build-time FileBrowser component
|
||||
* and the client-side file-browser script, so both agree on languages.
|
||||
*/
|
||||
|
||||
export type FileKind = "markdown" | "code" | "image" | "other";
|
||||
|
||||
const EXT_LANG: Record<string, string> = {
|
||||
ts: "typescript",
|
||||
tsx: "tsx",
|
||||
js: "javascript",
|
||||
jsx: "jsx",
|
||||
mjs: "javascript",
|
||||
cjs: "javascript",
|
||||
py: "python",
|
||||
rb: "ruby",
|
||||
go: "go",
|
||||
rs: "rust",
|
||||
java: "java",
|
||||
cs: "csharp",
|
||||
c: "c",
|
||||
cpp: "cpp",
|
||||
h: "c",
|
||||
php: "php",
|
||||
swift: "swift",
|
||||
kt: "kotlin",
|
||||
sh: "bash",
|
||||
bash: "bash",
|
||||
zsh: "bash",
|
||||
ps1: "powershell",
|
||||
json: "json",
|
||||
jsonc: "json",
|
||||
yml: "yaml",
|
||||
yaml: "yaml",
|
||||
toml: "toml",
|
||||
html: "html",
|
||||
css: "css",
|
||||
scss: "scss",
|
||||
xml: "xml",
|
||||
sql: "sql",
|
||||
dockerfile: "docker",
|
||||
md: "markdown",
|
||||
markdown: "markdown",
|
||||
txt: "text",
|
||||
};
|
||||
|
||||
const CODE_LANGS = new Set<string>([
|
||||
"typescript",
|
||||
"tsx",
|
||||
"javascript",
|
||||
"jsx",
|
||||
"python",
|
||||
"ruby",
|
||||
"go",
|
||||
"rust",
|
||||
"java",
|
||||
"csharp",
|
||||
"c",
|
||||
"cpp",
|
||||
"php",
|
||||
"swift",
|
||||
"kotlin",
|
||||
"bash",
|
||||
"powershell",
|
||||
"json",
|
||||
"yaml",
|
||||
"toml",
|
||||
"html",
|
||||
"css",
|
||||
"scss",
|
||||
"xml",
|
||||
"sql",
|
||||
"docker",
|
||||
]);
|
||||
|
||||
const IMAGE_EXTS = new Set<string>([
|
||||
"png",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"gif",
|
||||
"svg",
|
||||
"webp",
|
||||
"avif",
|
||||
"bmp",
|
||||
"ico",
|
||||
]);
|
||||
|
||||
export interface FileMeta {
|
||||
ext: string;
|
||||
lang: string;
|
||||
kind: FileKind;
|
||||
}
|
||||
|
||||
export function getFileMeta(name: string): FileMeta {
|
||||
const base = name.split("/").pop() ?? name;
|
||||
const ext =
|
||||
base.toLowerCase() === "dockerfile"
|
||||
? "dockerfile"
|
||||
: (base.split(".").pop() ?? "").toLowerCase();
|
||||
const lang = EXT_LANG[ext] ?? "text";
|
||||
let kind: FileKind = "other";
|
||||
if (IMAGE_EXTS.has(ext)) kind = "image";
|
||||
else if (ext === "md" || ext === "markdown") kind = "markdown";
|
||||
else if (CODE_LANGS.has(lang)) kind = "code";
|
||||
return { ext, lang, kind };
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Accessibility post-processing for marked-generated markdown HTML.
|
||||
*
|
||||
* axe flags two issues on rendered markdown that we fix here rather than in the
|
||||
* source content:
|
||||
* - `scrollable-region-focusable`: <pre> and <table> blocks can overflow on the
|
||||
* x-axis, so they must be keyboard focusable to let keyboard users scroll them.
|
||||
* - `label`: GitHub-style task-list checkboxes (`- [ ]` / `- [x]`) render as bare
|
||||
* disabled <input type="checkbox"> elements with no accessible name.
|
||||
*
|
||||
* This module is pure (no DOM or node deps) so it can run both at build time
|
||||
* (src/lib/detail-page.ts) and on the client (src/scripts/pages/file-browser.ts).
|
||||
*/
|
||||
export function enhanceMarkdownA11y(html: string): string {
|
||||
if (!html) return html;
|
||||
let out = html;
|
||||
|
||||
// Make scrollable code/table blocks keyboard focusable.
|
||||
out = out.replace(/<pre(?![^>]*\btabindex=)/g, '<pre tabindex="0"');
|
||||
out = out.replace(/<table(?![^>]*\btabindex=)/g, '<table tabindex="0"');
|
||||
|
||||
// Give task-list checkboxes an accessible name based on their checked state.
|
||||
out = out.replace(
|
||||
/<input\b([^>]*\btype="checkbox"[^>]*)>/g,
|
||||
(match, attrs) => {
|
||||
if (/\baria-label=/.test(attrs)) return match;
|
||||
const label = /\bchecked\b/.test(attrs)
|
||||
? "Completed task"
|
||||
: "Incomplete task";
|
||||
return `<input${attrs} aria-label="${label}">`;
|
||||
}
|
||||
);
|
||||
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Isomorphic HTML sanitizer for rendered markdown.
|
||||
*
|
||||
* `marked` allows raw HTML to pass through untouched, and the resulting string
|
||||
* is injected via `set:html` / `innerHTML` on the resource detail pages and in
|
||||
* the client-side file browser. Even though the markdown we render originates
|
||||
* from this repository, a compromised or malicious resource file could
|
||||
* otherwise introduce persistent XSS. Sanitizing the generated HTML gives us
|
||||
* defense-in-depth on both the server (build time) and the client.
|
||||
*
|
||||
* `isomorphic-dompurify` resolves to a jsdom-backed DOMPurify in Node (so it
|
||||
* works in Astro frontmatter during `astro build`) and to the native
|
||||
* browser DOMPurify when bundled for the client.
|
||||
*/
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
let noopenerHookInstalled = false;
|
||||
|
||||
function ensureNoopenerHook(): void {
|
||||
if (noopenerHookInstalled) return;
|
||||
noopenerHookInstalled = true;
|
||||
|
||||
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
|
||||
const el = node as unknown as {
|
||||
tagName?: string;
|
||||
getAttribute?: (name: string) => string | null;
|
||||
setAttribute?: (name: string, value: string) => void;
|
||||
};
|
||||
|
||||
if (el?.tagName !== "A") return;
|
||||
if (el.getAttribute?.("target") !== "_blank") return;
|
||||
|
||||
const rel = el.getAttribute?.("rel") ?? "";
|
||||
const tokens = new Set(rel.split(/\s+/).filter(Boolean));
|
||||
tokens.add("noopener");
|
||||
tokens.add("noreferrer");
|
||||
el.setAttribute?.("rel", Array.from(tokens).join(" "));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a fragment of HTML produced from trusted-but-untrusted markdown,
|
||||
* stripping scripts, event handlers, and dangerous URL schemes while keeping
|
||||
* the formatting tags GitHub-flavored markdown commonly emits.
|
||||
*/
|
||||
export function sanitizeHtml(html: string): string {
|
||||
if (!html) return html;
|
||||
ensureNoopenerHook();
|
||||
return DOMPurify.sanitize(html, {
|
||||
// Keep links that open in a new tab (target/rel) which some resource docs
|
||||
// author directly as raw HTML.
|
||||
ADD_ATTR: ["target", "rel"],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user