feature: html with SSI
This commit is contained in:
parent
d917b69753
commit
0f65e57539
10 changed files with 210 additions and 27 deletions
111
handlers/html.ts
Normal file
111
handlers/html.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* 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<any>) => {
|
||||
const promises: Promise<any>[] = [];
|
||||
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+(?<type>.*?)\s*=\s*["'](?<location>.*?)['"]\s+-->/mg;
|
||||
async function load_html_with_ssi(html_file: string): Promise<string> {
|
||||
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<string | undefined> => {
|
||||
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<Response | undefined> {
|
||||
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<string, string> = {};
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import * as html from './html.ts';
|
||||
import * as markdown from './markdown.ts';
|
||||
import * as static_files from './static.ts';
|
||||
import * as typescript from './typescript.ts';
|
||||
|
||||
export default {
|
||||
html,
|
||||
markdown,
|
||||
static: static_files,
|
||||
typescript
|
||||
typescript,
|
||||
static: static_files
|
||||
};
|
||||
|
|
|
@ -22,24 +22,6 @@ export default async function handle_static_files(request: Request): Promise<Res
|
|||
try {
|
||||
const stat = await Deno.stat(normalized_path);
|
||||
|
||||
if (stat.isDirectory) {
|
||||
const extensions: string[] = media_types.allExtensions('text/html') ?? ['html', 'htm'];
|
||||
|
||||
for (const extension of extensions) {
|
||||
const index_path = path.join(normalized_path, `index.${extension}`);
|
||||
const index_stat = await Deno.stat(index_path);
|
||||
if (index_stat.isFile) {
|
||||
return new Response(await Deno.readFile(index_path), {
|
||||
headers: {
|
||||
'Content-Type': 'text/html'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (stat.isFile) {
|
||||
const extension = path.extname(normalized_path)?.slice(1)?.toLowerCase() ?? '';
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue