feature: serverus modularly serves up a directory as an HTTP server
This commit is contained in:
commit
58139b078d
20 changed files with 1449 additions and 0 deletions
401
handlers/markdown.ts
Normal file
401
handlers/markdown.ts
Normal file
|
@ -0,0 +1,401 @@
|
|||
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<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;
|
||||
}
|
||||
|
||||
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(
|
||||
`
|
||||
<html>
|
||||
<head>
|
||||
<!-- CSS -->
|
||||
${css}
|
||||
<!-- END CSS -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="markdown">
|
||||
${html}
|
||||
</div>
|
||||
</body>
|
||||
</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 = `
|
||||
<style type="text/css">
|
||||
pre,
|
||||
code {
|
||||
font-family: Menlo, Monaco, "Courier New", monospace;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 0.5rem;
|
||||
line-height: 1.25;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
@media print {
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
background: transparent !important;
|
||||
color: #000 !important;
|
||||
box-shadow: none !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a[href]:after {
|
||||
content: " (" attr(href) ")";
|
||||
}
|
||||
|
||||
abbr[title]:after {
|
||||
content: " (" attr(title) ")";
|
||||
}
|
||||
|
||||
a[href^="#"]:after,
|
||||
a[href^="javascript:"]:after {
|
||||
content: "";
|
||||
}
|
||||
|
||||
pre,
|
||||
blockquote {
|
||||
border: 1px solid #999;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
tr,
|
||||
img {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
p,
|
||||
h2,
|
||||
h3 {
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: #01ff70;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:focus,
|
||||
a:active {
|
||||
color: #2ecc40;
|
||||
}
|
||||
|
||||
.retro-no-decoration {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 32rem) and (max-width: 48rem) {
|
||||
html {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 48rem) {
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
p,
|
||||
.retro-p {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1.3rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
.retro-h1,
|
||||
h2,
|
||||
.retro-h2,
|
||||
h3,
|
||||
.retro-h3,
|
||||
h4,
|
||||
.retro-h4 {
|
||||
margin: 1.414rem 0 0.5rem;
|
||||
font-weight: inherit;
|
||||
line-height: 1.42;
|
||||
}
|
||||
|
||||
h1,
|
||||
.retro-h1 {
|
||||
margin-top: 0;
|
||||
font-size: 3.998rem;
|
||||
}
|
||||
|
||||
h2,
|
||||
.retro-h2 {
|
||||
font-size: 2.827rem;
|
||||
}
|
||||
|
||||
h3,
|
||||
.retro-h3 {
|
||||
font-size: 1.999rem;
|
||||
}
|
||||
|
||||
h4,
|
||||
.retro-h4 {
|
||||
font-size: 1.414rem;
|
||||
}
|
||||
|
||||
h5,
|
||||
.retro-h5 {
|
||||
font-size: 1.121rem;
|
||||
}
|
||||
|
||||
h6,
|
||||
.retro-h6 {
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
small,
|
||||
.retro-small {
|
||||
font-size: 0.707em;
|
||||
}
|
||||
|
||||
/* https://github.com/mrmrs/fluidity */
|
||||
|
||||
img,
|
||||
canvas,
|
||||
iframe,
|
||||
video,
|
||||
svg,
|
||||
select,
|
||||
textarea {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: #222;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #fafafa;
|
||||
font-family: "Courier New";
|
||||
line-height: 1.45;
|
||||
margin: 6rem auto 1rem;
|
||||
max-width: 48rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid #01ff70;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
/* 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<TRANSFORM_NAME, TRANSFORMER[]> = {
|
||||
characters: [
|
||||
[/&/g, '&'],
|
||||
[/</g, '<'],
|
||||
[/>/g, '>'],
|
||||
[/"/g, '"'],
|
||||
[/'/g, ''']
|
||||
],
|
||||
|
||||
headings: [
|
||||
[/^#\s(.+)$/gm, '<h1>$1</h1>\n'],
|
||||
[/^##\s(.+)$/gm, '<h2>$1</h2>\n'],
|
||||
[/^###\s(.+)$/gm, '<h3>$1</h3>\n'],
|
||||
[/^####\s(.+)$/gm, '<h4>$1</h4>\n'],
|
||||
[/^#####\s(.+)$/gm, '<h5>$1</h5>\n']
|
||||
],
|
||||
|
||||
horizontal_rules: [
|
||||
[/^----*$/gm, '<hr>\n']
|
||||
],
|
||||
|
||||
list_items: [
|
||||
[/\n\n([ \t]*)([-\*\.].*?)\n\n/gs, '\n\n<ul>\n$1$2\n</ul>\n\n\n'],
|
||||
[/^([ \t]*)[-\*\.](\s+.*)$/gm, '<li>$1$2</li>\n']
|
||||
],
|
||||
|
||||
bold: [
|
||||
[/\*([^\*]+)\*/gm, '<strong>$1</strong>']
|
||||
],
|
||||
|
||||
italic: [
|
||||
[/_([^_]+)_/gm, '<i>$1</i>']
|
||||
],
|
||||
|
||||
strikethrough: [
|
||||
[/~([^~]+)~/gm, '<s>$1</s>']
|
||||
],
|
||||
|
||||
code: [
|
||||
[/```\n([^`]+)\n```/gm, '<pre><code>$1</code></pre>'],
|
||||
[/```([^`]+)```/gm, '<code>$1</code>']
|
||||
],
|
||||
|
||||
images: [
|
||||
[/!\[([^\]]+)\]\(([^\)]+)\)/g, '<img src="$2" alt="$1">']
|
||||
],
|
||||
|
||||
links: [
|
||||
[/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2">$1</a>']
|
||||
],
|
||||
|
||||
breaks: [
|
||||
[/\s\s\n/g, '\n<br>\n'],
|
||||
[/\n\n/g, '\n<br>\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<TRANSFORM_NAME, boolean>
|
||||
): 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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue