import * as path from '@std/path'; /** * Handles requests for markdown files, converting them to html by default * but allowing for getting the raw file with an accept header of text/markdown. * * @param request The incoming HTTP request * @returns Either a response (a markdown file was requested and returned properly) or undefined if unhandled. */ export default async function handle_markdown(request: Request): Promise { const url = new URL(request.url); const normalized_path = path.resolve(path.normalize(url.pathname).replace(/^\/+/, '')); if (!normalized_path.startsWith(Deno.cwd())) { return; } try { const stat = await Deno.stat(normalized_path); let markdown: string | null = null; if (stat.isDirectory) { const index_path = path.join(normalized_path, 'index.md'); const index_stat = await Deno.stat(index_path); if (!index_stat.isFile) { return; } markdown = await Deno.readTextFile(index_path); } if (stat.isFile) { const extension = path.extname(normalized_path)?.slice(1)?.toLowerCase() ?? ''; if (extension !== 'md') { return; } markdown = await Deno.readTextFile(normalized_path); } const accepts: string = request.headers.get('accept') ?? 'text/html'; switch (accepts) { case 'text/markdown': { return new Response(markdown, { headers: { 'Content-Type': 'text/markdown' } }); } case 'text/html': default: { const html = md_to_html(markdown ?? ''); const css = Deno.env.get('SERVERUS_MARKDOWN_CSS') ?? DEFAULT_CSS; return new Response( ` ${css}
${html}
`, { headers: { 'Content-Type': 'text/html' } } ); } } } catch (error) { if (error instanceof Deno.errors.NotFound) { return; } if (error instanceof Deno.errors.PermissionDenied) { return new Response('Permission Denied', { status: 400, headers: { 'Content-Type': 'text/plain' } }); } throw error; } } /* DEFAULT CSS */ const DEFAULT_CSS = ` `; /* MARKDOWN TO HTML */ type TRANSFORMER = [RegExp, string]; /* order of these transforms matters, so we list them here in an array and build type from it after. */ const TRANSFORM_NAMES = [ 'characters', 'headings', 'horizontal_rules', 'list_items', 'bold', 'italic', 'strikethrough', 'code', 'images', 'links', 'breaks' ] as const; type TRANSFORM_NAME = typeof TRANSFORM_NAMES[number]; const TRANSFORMS: Record = { characters: [ [/&/g, '&'], [//g, '>'], [/"/g, '"'], [/'/g, '''] ], headings: [ [/^#\s(.+)$/gm, '

$1

\n'], [/^##\s(.+)$/gm, '

$1

\n'], [/^###\s(.+)$/gm, '

$1

\n'], [/^####\s(.+)$/gm, '

$1

\n'], [/^#####\s(.+)$/gm, '
$1
\n'] ], horizontal_rules: [ [/^----*$/gm, '
\n'] ], list_items: [ [/\n\n([ \t]*)([-\*\.].*?)\n\n/gs, '\n\n\n\n\n'], [/^([ \t]*)[-\*\.](\s+.*)$/gm, '
  • $1$2
  • \n'] ], bold: [ [/\*([^\*]+)\*/gm, '$1'] ], italic: [ [/_([^_]+)_/gm, '$1'] ], strikethrough: [ [/~([^~]+)~/gm, '$1'] ], code: [ [/```\n([^`]+)\n```/gm, '
    $1
    '], [/```([^`]+)```/gm, '$1'] ], images: [ [/!\[([^\]]+)\]\(([^\)]+)\)/g, '$1'] ], links: [ [/\[([^\]]+)\]\(([^\)]+)\)/g, '$1'] ], breaks: [ [/\s\s\n/g, '\n
    \n'], [/\n\n/g, '\n
    \n'] ] }; /** * Convert markdown to HTML. * @param markdown The markdown string. * @param options _(Optional)_ A record of transforms to disable. * @returns The generated HTML string. */ export function md_to_html( markdown: string, transform_config?: Record ): string { let html = markdown; for (const transform_name of TRANSFORM_NAMES) { const enabled: boolean = typeof transform_config === 'undefined' || transform_config[transform_name] !== false; if (!enabled) { continue; } const transforms: TRANSFORMER[] = TRANSFORMS[transform_name] ?? []; for (const markdown_transformer of transforms) { html = html.replace(...markdown_transformer); } } return html; }