feature: allow for fallback includes
This commit is contained in:
parent
d07991bc60
commit
014d0e56c8
8 changed files with 93 additions and 68 deletions
|
|
@ -37,14 +37,17 @@ The default handlers are:
|
|||
```
|
||||
<html>
|
||||
<body>
|
||||
<!-- #include file="./header.html" -->
|
||||
<!-- #include "./header.html" -->
|
||||
|
||||
<!-- can include markdown, which will be converted to html -->
|
||||
<!-- #include file="./essay.md" -->
|
||||
<!-- #include "./essay.md" -->
|
||||
|
||||
<div id="footer">
|
||||
<!-- you can include text files as well -->
|
||||
<!-- #include file="./somedir/footer.txt" >
|
||||
<!-- #include "./somedir/footer.txt" -->
|
||||
|
||||
<!-- you can chain includes to allow for local overrides or temporary notices -->
|
||||
<!-- #include "./news.html" or "./default.html" -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@andyburke/serverus",
|
||||
"description": "A flexible HTTP server for mixed content. Throw static files, markdown, Typescript and (hopefully, eventually) more into a directory and serverus can serve it up a bit more like old-school CGI.",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.0",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
".": "./serverus.ts",
|
||||
|
|
|
|||
132
handlers/html.ts
132
handlers/html.ts
|
|
@ -3,11 +3,16 @@
|
|||
* @module
|
||||
*/
|
||||
|
||||
import * as path from '@std/path';
|
||||
import { md_to_html } from './markdown.ts';
|
||||
import * as fs from "@std/fs";
|
||||
import * as path from "@std/path";
|
||||
import { md_to_html } from "./markdown.ts";
|
||||
|
||||
// https://stackoverflow.com/a/75205316
|
||||
const replaceAsync = async (str: string, regex: RegExp, asyncFn: (match: any, ...args: any) => Promise<any>) => {
|
||||
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));
|
||||
|
|
@ -17,53 +22,69 @@ const replaceAsync = async (str: string, regex: RegExp, asyncFn: (match: any, ..
|
|||
return str.replace(regex, () => data.shift());
|
||||
};
|
||||
|
||||
const SSI_REGEX: RegExp = /\<\!--\s+#include\s+(?<type>.*?)\s*=\s*["'](?<location>.*?)['"]\s+-->/mg;
|
||||
export async function load_html_with_ssi(html_file: string): Promise<string> {
|
||||
const directory = path.dirname(html_file);
|
||||
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 directory = path.dirname(html_file);
|
||||
const resolved = path.resolve(path.join(directory, location));
|
||||
if (!resolved.startsWith(Deno.cwd())) {
|
||||
console.error(
|
||||
`Cannot include files above the working directory (${Deno.cwd()}): ${location} ${html_file}:${index}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
/\<\!--\s+#include\s+(?<directive>.*?)\s*-->/gm,
|
||||
async (_match, directive, index): Promise<string | undefined> => {
|
||||
const file_include_options = Array.from(
|
||||
directive.matchAll(/(?<option>(?<op>or)?\s*["'](?<location>[^"']*?)['"])+/gm),
|
||||
).map((match) => match?.groups?.location);
|
||||
|
||||
const HANDLERS: Record<string, (include_path: string) => Promise<string> | string> = {
|
||||
default: async (include_path: string) => {
|
||||
try {
|
||||
return await Deno.readTextFile(include_path);
|
||||
} catch (error) {
|
||||
console.dir({ error });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
'.md': async (include_path: string) => {
|
||||
const markdown_content = await Deno.readTextFile(include_path);
|
||||
return md_to_html(markdown_content);
|
||||
},
|
||||
|
||||
'.html': load_html_with_ssi
|
||||
};
|
||||
|
||||
const extension = path.extname(resolved);
|
||||
const handler = extension ? (HANDLERS[extension.toLowerCase()] ?? HANDLERS.default) : HANDLERS.default;
|
||||
return await handler(resolved);
|
||||
}
|
||||
|
||||
default: {
|
||||
console.error(`Unknown include type: ${type} ${html_file}:${index}`);
|
||||
let include_location;
|
||||
for (const location of file_include_options) {
|
||||
const resolved = path.resolve(path.join(directory, location));
|
||||
if (!resolved.startsWith(Deno.cwd())) {
|
||||
console.error(
|
||||
`Cannot include files above the working directory (${Deno.cwd()}): ${location} ${html_file}:${index}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const exists = await fs.exists(resolved);
|
||||
if (!exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
include_location = resolved;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!include_location) {
|
||||
console.error(
|
||||
`Cannot locate any of these files to include: ${file_include_options}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const HANDLERS: Record<string, (include_path: string) => Promise<string> | string> = {
|
||||
default: async (include_path: string) => {
|
||||
try {
|
||||
return await Deno.readTextFile(include_path);
|
||||
} catch (error) {
|
||||
console.dir({ error });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
".md": async (include_path: string) => {
|
||||
const markdown_content = await Deno.readTextFile(include_path);
|
||||
return md_to_html(markdown_content);
|
||||
},
|
||||
|
||||
".html": load_html_with_ssi,
|
||||
};
|
||||
|
||||
const extension = path.extname(include_location);
|
||||
const handler = extension
|
||||
? (HANDLERS[extension.toLowerCase()] ?? HANDLERS.default)
|
||||
: HANDLERS.default;
|
||||
|
||||
const result = await handler(include_location);
|
||||
return result;
|
||||
},
|
||||
);
|
||||
|
||||
return processed;
|
||||
|
|
@ -77,16 +98,17 @@ export async function load_html_with_ssi(html_file: string): Promise<string> {
|
|||
*/
|
||||
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(/^\/+/, ''));
|
||||
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 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') {
|
||||
if (extension !== "html") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -98,21 +120,21 @@ export default async function handle_html(request: Request): Promise<Response |
|
|||
|
||||
const processed: string = await load_html_with_ssi(target_path);
|
||||
|
||||
const accepts: string = request.headers.get('accept') ?? 'text/html';
|
||||
const accepts: string = request.headers.get("accept") ?? "text/html";
|
||||
const headers: Record<string, string> = {};
|
||||
switch (accepts) {
|
||||
case 'text/plain':
|
||||
headers['Content-Type'] = 'text/plain';
|
||||
case "text/plain":
|
||||
headers["Content-Type"] = "text/plain";
|
||||
break;
|
||||
|
||||
case 'text/html':
|
||||
case "text/html":
|
||||
default:
|
||||
headers['Content-Type'] = 'text/html';
|
||||
headers["Content-Type"] = "text/html";
|
||||
break;
|
||||
}
|
||||
|
||||
return new Response(processed, {
|
||||
headers
|
||||
headers,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Deno.errors.NotFound) {
|
||||
|
|
@ -120,11 +142,11 @@ export default async function handle_html(request: Request): Promise<Response |
|
|||
}
|
||||
|
||||
if (error instanceof Deno.errors.PermissionDenied) {
|
||||
return new Response('Permission Denied', {
|
||||
return new Response("Permission Denied", {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
<body>
|
||||
<p>Hello. An include should follow:</p>
|
||||
|
||||
<!-- #include file="./test_include.html" -->
|
||||
<!-- #include file="./yet_another_include.html" -->
|
||||
<!-- #include "./non-existent-include.txt" or "./test_include.html" -->
|
||||
<!-- #include "./yet_another_include.html" -->
|
||||
<p>Goodbye. The includes should be above.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<div>Include #1</div>
|
||||
|
||||
<!-- #include file="./subdir/another_include.html" -->
|
||||
<!-- #include "./subdir/another_include.html" -->
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
<body>
|
||||
<p>Hello. Markdown should follow:</p>
|
||||
|
||||
<!-- #include file="./some_markdown.md" -->
|
||||
<!-- #include file="./subdir/a-random-text-file.txt" -->
|
||||
<!-- #include file="./test_include.html" -->
|
||||
<!-- #include file="./yet_another_include.html" -->
|
||||
<!-- #include "./some_markdown.md" -->
|
||||
<!-- #include "./subdir/a-random-text-file.txt" -->
|
||||
<!-- #include "./test_include.html" -->
|
||||
<!-- #include "./yet_another_include.html" -->
|
||||
|
||||
<p>Goodbye. The include should be above.</p>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<body>
|
||||
<p>Hello. Markdown should follow:</p>
|
||||
|
||||
<!-- #include file="./some_markdown.md" -->
|
||||
<!-- #include "./some_markdown.md" -->
|
||||
|
||||
<p>Goodbye. The include should be above.</p>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<body>
|
||||
<p>Hello. Text should follow:</p>
|
||||
|
||||
<!-- #include file="./subdir/a-random-text-file.txt" -->
|
||||
<!-- #include "./subdir/a-random-text-file.txt" -->
|
||||
|
||||
<p>Goodbye. The include should be above.</p>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue