feature: html with SSI
This commit is contained in:
parent
d917b69753
commit
0f65e57539
10 changed files with 210 additions and 27 deletions
13
README.md
13
README.md
|
@ -9,21 +9,20 @@ bit more like old-school CGI.
|
||||||
Compiled:
|
Compiled:
|
||||||
|
|
||||||
```
|
```
|
||||||
<insert instructions for using compiled command line>
|
[user@machine] ~/ serverus --root ./public
|
||||||
```
|
```
|
||||||
|
|
||||||
Container:
|
Container:
|
||||||
|
|
||||||
```
|
```
|
||||||
<insert instructions for using a container>
|
<insert instructions for using a container>
|
||||||
<our container can and should be very simple: mount a /www into the container and we run the compiled command line to serve it>
|
<our container can and should be very simple: mount a /www into the container and we run serverus on it>
|
||||||
```
|
```
|
||||||
|
|
||||||
Deno:
|
Deno:
|
||||||
|
|
||||||
```
|
```
|
||||||
<something like this? can you just run the command directly like this?>
|
deno --allow-env --allow-read --allow-write --allow-net jsr:@andyburke/serverus --root ./public
|
||||||
deno run jsr:@andyburke/serverus
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
@ -33,8 +32,8 @@ different types of content with a great deal of control.
|
||||||
|
|
||||||
The default handlers are:
|
The default handlers are:
|
||||||
|
|
||||||
- static files
|
- HTML with SSI support
|
||||||
will serve up static files within the root folder
|
eg: <html><body><!-- #include file="./body.html" --></body></html>
|
||||||
- markdown
|
- markdown
|
||||||
will serve markdown as HTML (or raw with an Accept header of text/markdown)
|
will serve markdown as HTML (or raw with an Accept header of text/markdown)
|
||||||
- Typescript
|
- Typescript
|
||||||
|
@ -42,6 +41,8 @@ The default handlers are:
|
||||||
eg: ./book/:book_id/index.ts) and if they export methods like `GET` and `POST`,
|
eg: ./book/:book_id/index.ts) and if they export methods like `GET` and `POST`,
|
||||||
they will be called for those requests. there's some additional stuff you can
|
they will be called for those requests. there's some additional stuff you can
|
||||||
export to ease typical use cases, covered below.
|
export to ease typical use cases, covered below.
|
||||||
|
- static files
|
||||||
|
will serve up static files within the root folder
|
||||||
|
|
||||||
You just start serverus in a directory (or specify a root) and it tells you where it's
|
You just start serverus in a directory (or specify a root) and it tells you where it's
|
||||||
listening.
|
listening.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@andyburke/serverus",
|
"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.",
|
"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.6.0",
|
"version": "0.7.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./serverus.ts",
|
".": "./serverus.ts",
|
||||||
|
|
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 markdown from './markdown.ts';
|
||||||
import * as static_files from './static.ts';
|
import * as static_files from './static.ts';
|
||||||
import * as typescript from './typescript.ts';
|
import * as typescript from './typescript.ts';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
html,
|
||||||
markdown,
|
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 {
|
try {
|
||||||
const stat = await Deno.stat(normalized_path);
|
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) {
|
if (stat.isFile) {
|
||||||
const extension = path.extname(normalized_path)?.slice(1)?.toLowerCase() ?? '';
|
const extension = path.extname(normalized_path)?.slice(1)?.toLowerCase() ?? '';
|
||||||
|
|
||||||
|
|
73
tests/08_test_html_includes.test.ts
Normal file
73
tests/08_test_html_includes.test.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import * as asserts from '@std/assert';
|
||||||
|
import { EPHEMERAL_SERVER, get_ephemeral_listen_server } from './helpers.ts';
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: 'get html file',
|
||||||
|
permissions: {
|
||||||
|
env: true,
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
net: true
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
let test_server_info: EPHEMERAL_SERVER | null = null;
|
||||||
|
const cwd = Deno.cwd();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Deno.chdir('./tests/www');
|
||||||
|
test_server_info = await get_ephemeral_listen_server();
|
||||||
|
|
||||||
|
const response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/`, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await response.text();
|
||||||
|
|
||||||
|
asserts.assert(response.ok);
|
||||||
|
asserts.assert(body);
|
||||||
|
asserts.assertMatch(body, /\<html\>.*?Include #1.*?Include #2.*?\<\/html\>/is);
|
||||||
|
} finally {
|
||||||
|
Deno.chdir(cwd);
|
||||||
|
if (test_server_info) {
|
||||||
|
await test_server_info?.server?.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: 'get html file (text/plain)',
|
||||||
|
permissions: {
|
||||||
|
env: true,
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
net: true
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
let test_server_info: EPHEMERAL_SERVER | null = null;
|
||||||
|
const cwd = Deno.cwd();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Deno.chdir('./tests/www');
|
||||||
|
test_server_info = await get_ephemeral_listen_server();
|
||||||
|
|
||||||
|
const response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/index.html`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'text/plain'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await response.text();
|
||||||
|
|
||||||
|
asserts.assert(response.ok);
|
||||||
|
asserts.assert(body);
|
||||||
|
asserts.assertMatch(body, /\<html\>.*?Include #1.*?Include #2.*?\<\/html\>/is);
|
||||||
|
} finally {
|
||||||
|
Deno.chdir(cwd);
|
||||||
|
if (test_server_info) {
|
||||||
|
await test_server_info?.server?.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
1
tests/www/another_include.html
Normal file
1
tests/www/another_include.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div>Include #2!</div>
|
9
tests/www/index.html
Normal file
9
tests/www/index.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<p>Hello. An include should follow:</p>
|
||||||
|
|
||||||
|
<!-- #include file="./test_include.html" -->
|
||||||
|
<!-- #include file="./yet_another_include.html" -->
|
||||||
|
<p>Goodbye. The includes should be above.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
3
tests/www/test_include.html
Normal file
3
tests/www/test_include.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<div>Include #1</div>
|
||||||
|
|
||||||
|
<!-- #include file="./another_include.html" -->
|
1
tests/www/yet_another_include.html
Normal file
1
tests/www/yet_another_include.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div>Include #3</div>
|
Loading…
Add table
Add a link
Reference in a new issue