Skip to content

Instantly share code, notes, and snippets.

@Tsukina-7mochi
Last active July 8, 2025 07:26
Show Gist options
  • Select an option

  • Save Tsukina-7mochi/b71af2a4f3ec81d2bc6f9df670a646e3 to your computer and use it in GitHub Desktop.

Select an option

Save Tsukina-7mochi/b71af2a4f3ec81d2bc6f9df670a646e3 to your computer and use it in GitHub Desktop.
Preview GitHub-Flavored Markdown with Deno
// deno serve -R https://gist.githubusercontent.com/Tsukina-7mochi/b71af2a4f3ec81d2bc6f9df670a646e3/raw/21206f905e2ef7d83a1022686d10af56b6bc6d25/preview-gfm.ts
// deno serve --port [port] to specify port
// deno serve --open to open in browser
import { parseArgs } from "jsr:@std/cli/parse-args";
import * as path from "jsr:@std/path";
import { marked } from "npm:marked";
import { gfmHeadingId } from "npm:marked-gfm-heading-id";
const args = parseArgs(Deno.args);
const argpath = path.resolve(`${args._[0] ?? "."}`);
const stat = await Deno.stat(argpath);
const basedir = stat.isDirectory ? argpath : path.dirname(argpath);
const hasIndexFile = stat.isFile;
console.log(basedir);
marked.use({
async: true,
gfm: true,
}, gfmHeadingId());
async function renderToHTML(filepath: string): Promise<string> {
const content = await Deno.readTextFile(filepath);
const parsed = await marked.parse(content);
return `<!DOCTYPE html>
<html>
<head>
<link href="https://cdn.jsdelivr.net/gh/pixelbrackets/gfm-stylesheet/dist/gfm.min.css" rel="stylesheet" />
<style>
body {
font-size: 16px;
max-width: 894px;
margin: 0 auto;
border-left: 1px solid #d1d9e0;
border-right: 1px solid #d1d9e0;
}
</style>
</head>
<body>
${parsed}
</body>
</html>`;
}
function extToContentType(ext: string): string {
if (ext === "html") {
return "text/html";
} else if (ext === "css") {
return "text/css";
} else if (ext === "js" || ext === "mjs") {
return "application/javascript";
} else if (ext === "json") {
return "application/json";
} else if (ext === "png") {
return "image/png";
} else if (ext === "jpg" || ext === "jpeg") {
return "image/jpeg";
} else if (ext === "gif") {
return "image/gif";
} else if (ext === "svg") {
return "image/svg+xml";
} else if (ext === "webp") {
return "image/webp";
} else if (ext === "txt") {
return "text/plain";
}
return "application/octet-stream";
}
export default {
async fetch(req: Request): Promise<Response> {
try {
const url = new URL(req.url);
const pathname = url.pathname;
if (pathname === "/" && hasIndexFile) {
return new Response(
await renderToHTML(argpath),
{
headers: { "Content-Type": "text/html" },
status: 200,
},
);
}
const filepath = path.join(basedir, `.${pathname}`);
const ext = path.extname(filepath).toLowerCase();
if (ext === ".md") {
return new Response(
await renderToHTML(filepath),
{
headers: { "Content-Type": "text/html" },
status: 200,
},
);
}
const content = await Deno.readFile(filepath);
return new Response(content, {
headers: { "Content-Type": extToContentType(ext) },
status: 200,
});
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
console.warn(error.message);
return new Response("File not found", { status: 404 });
}
console.error(error);
return new Response("Internal server error: see console for details.", {
status: 500,
});
}
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment