From 90605d367509ea7267193d23304f5ac896c4d4f2 Mon Sep 17 00:00:00 2001
From: Josh Kelley <joshkel@gmail.com>
Date: Mon, 11 Aug 2025 21:31:36 -0400
Subject: [PATCH] Markdown support

---
 packages/cli/src/index.ts        |  1 +
 packages/cli/src/render/index.ts |  2 +-
 packages/cli/src/render/typed.ts | 65 ++++++++++++++++++++++++++++----
 3 files changed, 60 insertions(+), 8 deletions(-)

diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index d62212eb..01200387 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -28,6 +28,7 @@ const formats = Object.keys({
   ascii: true,
   table: true,
   "table-flipped": true,
+  markdown: true,
 } satisfies Record<render.Format, any>) as render.Format[];
 
 interface Opts extends render.RenderOptions {
diff --git a/packages/cli/src/render/index.ts b/packages/cli/src/render/index.ts
index 064e75c9..61248eb2 100644
--- a/packages/cli/src/render/index.ts
+++ b/packages/cli/src/render/index.ts
@@ -1,6 +1,6 @@
 import type { problemFlags, resolutionKinds } from "../problemUtils.js";
 
-export type Format = "auto" | "table" | "table-flipped" | "ascii" | "json";
+export type Format = "auto" | "table" | "table-flipped" | "ascii" | "json" | "markdown";
 export interface RenderOptions {
   ignoreRules?: (typeof problemFlags)[keyof typeof problemFlags][];
   ignoreResolutions?: (keyof typeof resolutionKinds)[];
diff --git a/packages/cli/src/render/typed.ts b/packages/cli/src/render/typed.ts
index 21c9930b..8984fe99 100644
--- a/packages/cli/src/render/typed.ts
+++ b/packages/cli/src/render/typed.ts
@@ -27,9 +27,16 @@ export async function typed(
   const ignoredResolutions = allResolutionKinds.filter((kind) => ignoreResolutions.includes(kind));
   const resolutions = requiredResolutions.concat(ignoredResolutions);
   const entrypoints = Object.keys(analysis.entrypoints);
-  marked.setOptions({
-    renderer: new TerminalRenderer(),
-  });
+
+  let md: (s: string) => string;
+  if (format === "markdown") {
+    md = (s: string) => s + "\n\n";
+  } else {
+    marked.setOptions({
+      renderer: new TerminalRenderer(),
+    });
+    md = marked;
+  }
 
   out(`${analysis.packageName} v${analysis.packageVersion}`);
   if (analysis.types.kind === "@types") {
@@ -58,16 +65,18 @@ export async function typed(
   }
 
   if (summary) {
-    const defaultSummary = marked(!emoji ? " No problems found" : " No problems found 🌟");
+    const defaultSummary = md(!emoji ? " No problems found" : " No problems found 🌟");
     const grouped = groupProblemsByKind(problems);
     const summaryTexts = Object.entries(grouped).map(([kind, kindProblems]) => {
       const info = problemKindInfo[kind as core.ProblemKind];
       const affectsRequiredResolution = kindProblems.some((p) =>
         requiredResolutions.some((r) => problemAffectsResolutionKind(p, r, analysis)),
       );
-      const description = marked(
-        `${info.description}${info.details ? ` Use \`-f json\` to see ${info.details}.` : ""} ${info.docsUrl}`,
-      );
+      const descriptionText = `${info.description}${info.details ? ` Use \`-f json\` to see ${info.details}.` : ""}`;
+      const description =
+        format === "markdown"
+          ? md(`[${info.shortDescription}](${info.docsUrl}): ${descriptionText}`)
+          : md(`${descriptionText} ${info.docsUrl}`);
       return `${affectsRequiredResolution ? "" : "(ignored per resolution) "}${
         emoji ? `${info.emoji} ` : ""
       }${description}`;
@@ -155,6 +164,9 @@ export async function typed(
     case "ascii":
       out(asciiTable(table!));
       break;
+    case "markdown":
+      out(generateMarkdownTable(entrypoints, resolutions, entrypointHeaders, getCellContents));
+      break;
     case "auto":
       const terminalWidth = process.stdout.columns || 133; // This looks like GitHub Actions' width
       if (table!.width <= terminalWidth) {
@@ -174,6 +186,45 @@ export async function typed(
   }
 }
 
+function generateMarkdownTable(
+  entrypoints: string[],
+  resolutions: core.ResolutionKind[],
+  entrypointHeaders: string[],
+  getCellContents: (subpath: string, resolutionKind: core.ResolutionKind) => string,
+): string {
+  // Strip ANSI color codes from headers for markdown
+  const cleanHeaders = entrypointHeaders.map((header) => header.replace(/\u001b\[[0-9;]*m/g, ""));
+
+  // Create the header row
+  const headerRow = `| | ${cleanHeaders.join(" | ")} |`;
+  const separatorRow = `|${Array(cleanHeaders.length + 1)
+    .fill("")
+    .map(() => " --- ")
+    .join("|")}|`;
+
+  // Create data rows
+  const dataRows = resolutions.map((resolutionKind) => {
+    const rowData = entrypoints.map((entrypoint) => {
+      const cellContent = getCellContents(entrypoint, resolutionKind);
+      // Clean up cell content for markdown - remove ANSI codes and escape pipes
+      return cellContent.replace(/\u001b\[[0-9;]*m/g, "").replace(/\|/g, "\\|");
+    });
+    const rowDataLines = rowData.map((i) => i.split("\n"));
+    const lineCount = Math.max(...rowDataLines.map((i) => i.length));
+    return new Array(lineCount)
+      .fill(0)
+      .map(
+        (_, i) =>
+          `| ${i === 0 ? resolutionKinds[resolutionKind] : ""} | ${rowDataLines
+            .map((lines) => lines[i] || "")
+            .join(" | ")} |`,
+      )
+      .join("\n");
+  });
+
+  return [headerRow, separatorRow, ...dataRows].join("\n");
+}
+
 function memo<Args extends (string | number)[], Result>(fn: (...args: Args) => Result): (...args: Args) => Result {
   const cache = new Map();
   return (...args) => {
