/** * Default handler for returning HTML with SSI support * @module */ import * as path from '@std/path'; // https://stackoverflow.com/a/75205316 const replaceAsync = async (str: string, regex: RegExp, asyncFn: (match: any, ...args: any) => Promise) => { const promises: Promise[] = []; str.replace(regex, (match, ...args) => { promises.push(asyncFn(match, ...args)); return match; }); const data = await Promise.all(promises); return str.replace(regex, () => data.shift()); }; const SSI_REGEX: RegExp = /\<\!--\s+#include\s+(?.*?)\s*=\s*["'](?.*?)['"]\s+-->/mg; async function load_html_with_ssi(html_file: string): Promise { const html_file_content: string = await Deno.readTextFile(html_file); const processed: string = await replaceAsync( html_file_content, SSI_REGEX, async (_match, type, location, index): Promise => { switch (type) { case 'file': { const resolved = path.resolve(location); if (!resolved.startsWith(Deno.cwd())) { console.error( `Cannot include files above the working directory (${Deno.cwd()}): ${location} ${html_file}:${index}` ); break; } return await load_html_with_ssi(resolved); } default: { console.error(`Unknown include type: ${type} ${html_file}:${index}`); break; } } } ); return processed; } /** * Handles requests for HTML files, processing any server-side includes * * @param request The incoming HTTP request * @returns Either a response (an HTML file was requested and returned properly) or undefined if unhandled. */ export default async function handle_html(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; } const initial_extension = path.extname(normalized_path)?.slice(1)?.toLowerCase() ?? ''; const target_path: string = initial_extension.length === 0 ? path.join(normalized_path, 'index.html') : normalized_path; const extension = path.extname(target_path).slice(1).toLowerCase(); if (extension !== 'html') { return; } try { const stat = await Deno.stat(target_path); if (!stat.isFile) { return; } const processed: string = await load_html_with_ssi(target_path); const accepts: string = request.headers.get('accept') ?? 'text/html'; const headers: Record = {}; switch (accepts) { case 'text/plain': headers['Content-Type'] = 'text/plain'; break; case 'text/html': default: headers['Content-Type'] = 'text/html'; break; } return new Response(processed, { headers }); } 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; } }