add readme preview

This commit is contained in:
monoid 2023-01-06 22:17:45 +09:00
parent 312964f22c
commit 2f91fb771e
14 changed files with 217 additions and 148 deletions

View File

@ -5,13 +5,17 @@ export function MarkdownRenderer(props: { text: string | undefined }) {
if (text === undefined) {
text = "";
}
const index = text.indexOf("\n---", 3);
const c = text.slice(index + 4, text.length);
let c = text;
if (text.startsWith("---")) {
const index = text.indexOf("\n---", 3);
c = text.slice(index + 4, text.length);
}
return (
<div
class="markdown-body"
dangerouslySetInnerHTML={{ __html: marked.parse(c) }}
/>
>
</div>
);
}

View File

@ -5,34 +5,29 @@
import config from "./deno.json" assert { type: "json" };
import * as $0 from "./routes/_404.tsx";
import * as $1 from "./routes/_middleware.ts";
import * as $2 from "./routes/api/doc.ts";
import * as $3 from "./routes/api/login.ts";
import * as $4 from "./routes/api/logout.ts";
import * as $5 from "./routes/dir/[...path].tsx";
import * as $6 from "./routes/doc/index.tsx";
import * as $7 from "./routes/fs/[...path].ts";
import * as $8 from "./routes/index.tsx";
import * as $9 from "./routes/login.tsx";
import * as $2 from "./routes/api/login.ts";
import * as $3 from "./routes/api/logout.ts";
import * as $4 from "./routes/dir/[...path].tsx";
import * as $5 from "./routes/doc/index.tsx";
import * as $6 from "./routes/index.tsx";
import * as $7 from "./routes/login.tsx";
import * as $$0 from "./islands/ContentRenderer.tsx";
import * as $$1 from "./islands/Counter.tsx";
import * as $$2 from "./islands/DirList.tsx";
import * as $$3 from "./islands/DocSearch.tsx";
import * as $$4 from "./islands/FileViewer.tsx";
import * as $$5 from "./islands/MarkdownRenderer.tsx";
import * as $$6 from "./islands/UpList.tsx";
import * as $$5 from "./islands/UpList.tsx";
const manifest = {
routes: {
"./routes/_404.tsx": $0,
"./routes/_middleware.ts": $1,
"./routes/api/doc.ts": $2,
"./routes/api/login.ts": $3,
"./routes/api/logout.ts": $4,
"./routes/dir/[...path].tsx": $5,
"./routes/doc/index.tsx": $6,
"./routes/fs/[...path].ts": $7,
"./routes/index.tsx": $8,
"./routes/login.tsx": $9,
"./routes/api/login.ts": $2,
"./routes/api/logout.ts": $3,
"./routes/dir/[...path].tsx": $4,
"./routes/doc/index.tsx": $5,
"./routes/index.tsx": $6,
"./routes/login.tsx": $7,
},
islands: {
"./islands/ContentRenderer.tsx": $$0,
@ -40,8 +35,7 @@ const manifest = {
"./islands/DirList.tsx": $$2,
"./islands/DocSearch.tsx": $$3,
"./islands/FileViewer.tsx": $$4,
"./islands/MarkdownRenderer.tsx": $$5,
"./islands/UpList.tsx": $$6,
"./islands/UpList.tsx": $$5,
},
baseUrl: import.meta.url,
config,

View File

@ -1,5 +1,5 @@
import { extname } from "path/posix.ts";
import MarkdownRenderer from "./MarkdownRenderer.tsx";
import MarkdownRenderer from "../components/MarkdownRenderer.tsx";
import { useEffect, useState } from "preact/hooks";
const TypeToExt = {
@ -25,8 +25,13 @@ function FetchAndRender(props: { src: string; type: string }) {
const ext = extname(src);
const [content, setContent] = useState("");
useEffect(() => {
fetch(src).then((res) => res.text()).then(setContent);
(async () => {
const res = await fetch(src);
const content = await res.text();
setContent(content);
})();
}, [src]);
switch (props.type) {
case "text":
return <div style={{ whiteSpace: "pre-wrap" }}>{content}</div>;
@ -41,7 +46,7 @@ function FetchAndRender(props: { src: string; type: string }) {
//case "csv":
// return <CsvRenderer content={content} />;
default:
return <>error: invalid type: {props.type} content: {content}</>;
return <p>error: invalid type: {props.type} content: {content}</p>;
}
}
@ -62,7 +67,7 @@ export function RenderView(props: { src: string }) {
case "audio":
return <audio style={{ width: "100%" }} controls src={src} />;
default:
return <>error: invalid type: {type} src: {src}</>;
return <p>error: invalid type: {type} src: {src}</p>;
}
}

View File

@ -53,7 +53,7 @@ export function DirList(props: DirListProps) {
</li>
<ListItem
key=".."
href={`/dir/${encodePath(join(data.path, ".."))}`}
href={`/dir/${encodePath(join(data.path, ".."))}?pretty`}
icon="/icon/back.svg"
>
...
@ -61,7 +61,7 @@ export function DirList(props: DirListProps) {
{files.map((file) => (
<ListItem
key={file.name}
href={`/dir/${encodePath(join(data.path, file.name))}`}
href={`/dir/${encodePath(join(data.path, file.name))}?pretty`}
icon={file.isDirectory
? "/icon/folder.svg"
: extToIcon(extname(file.name))}

View File

@ -2,6 +2,7 @@ import { Doc } from "../src/collect.ts";
import { useEffect, useState } from "preact/hooks";
import { Index } from "../src/client_search.ts";
import { encodePath } from "../util/util.ts";
import { join } from "path/mod.ts";
function SearchBar(props: {
search?: string;
@ -43,11 +44,14 @@ export default function DocSearch(props: {
</SearchBar>
<h1 class="text-2xl font-bold">Doc</h1>
<ul class="mt-4">
{docs.map((doc) => (
<li class="mt-2">
<a href={`/dir/${encodePath(doc.path)}`}>{doc.path}</a>
</li>
))}
{docs.map((doc) => {
const path = join(doc.path, "..");
return (
<li class="mt-2" key={path}>
<a href={`/dir/${encodePath(path)}?pretty`}>{path}</a>
</li>
);
})}
</ul>
</>
);

View File

@ -5,7 +5,7 @@ import { encodePath } from "../util/util.ts";
export default function FileViewer(props: { path: string }) {
const { path } = props;
const srcPath = `/fs/${encodePath(path)}`;
const srcPath = `/dir/${encodePath(path)}`;
return (
<div class="p-4 mx-auto max-w-screen-md">
<UpList path={path} />

View File

@ -22,7 +22,10 @@ export default function UpList(props: { path: string }) {
return (
<div>
<h1 class={"text-2xl flex flex-wrap"}>
<a href="/dir/" class="flex flex-wrap p-2 hover:bg-gray-400 rounded-sm">
<a
href="/dir/?pretty"
class="flex flex-wrap p-2 hover:bg-gray-400 rounded-sm"
>
<img src={asset("/icon/house.svg")} />
<span class="ml-1">Home</span>
</a>
@ -31,7 +34,7 @@ export default function UpList(props: { path: string }) {
<span class="p-2">/</span>
<a
class="flex flex-wrap p-2 hover:bg-gray-400 rounded-sm"
href={`/dir/${encodePath(cur)}`}
href={`/dir/${encodePath(cur)}?pretty`}
>
<img src={asset("/icon/folder.svg")} />
<span class="ml-1">{up}</span>

View File

@ -26,13 +26,12 @@ import { user_command } from "./user.ts";
import { key_out_cmd } from "./keyout.ts";
import { prepareDocs } from "./src/store/doc.ts";
const github_markdown = await Deno.readTextFile(
const github_markdown = (await Deno.readTextFile(
join(fromFileUrl(import.meta.url), "..", "static", "github-markdown.css"),
);
)).replaceAll("\n", "");
const CSSPlugin: Plugin = {
name: "css plugin",
// deno-lint-ignore require-await
render(ctx): PluginRenderResult {
ctx.render();
return {

View File

View File

@ -1,9 +1,11 @@
import { HandlerContext, Handlers, PageProps } from "$fresh/server.ts";
import { asset, Head } from "$fresh/runtime.ts";
import { removePrefixFromPathname } from "../../util/util.ts";
import { encodePath, removePrefixFromPathname } from "../../util/util.ts";
import { join } from "path/posix.ts";
import DirList, { EntryInfo } from "../../islands/DirList.tsx";
import FileViewer from "../../islands/FileViewer.tsx";
import RenderView from "../../islands/ContentRenderer.tsx";
import { serveFile } from "http/file_server.ts";
type DirProps = {
type: "dir";
@ -19,10 +21,97 @@ type FileProps = {
type DirOrFileProps = DirProps | FileProps;
async function renderFile(req: Request, path: string) {
try {
const fileInfo = await Deno.stat(path);
if (fileInfo.isDirectory) {
// if index.html exists, serve it.
// otherwise, serve a directory listing.
const indexPath = join(path, "/index.html");
try {
await Deno.stat(indexPath);
const res = await serveFile(req, indexPath);
return res;
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
const list: Deno.DirEntry[] = [];
for await (const entry of Deno.readDir(path)) {
list.push(entry);
}
return new Response(
JSON.stringify(
list,
),
{
headers: {
"content-type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods":
"GET,HEAD,PUT,PATCH,POST,DELETE",
"Access-Control-Allow-Headers":
"Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With",
},
status: 200,
},
);
}
}
}
const res = await serveFile(req, path, {
fileInfo,
});
return res;
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return new Response("Not Found", {
status: 404,
});
}
throw e;
}
}
async function renderPage(_req: Request, path: string, ctx: HandlerContext) {
try {
const stat = await Deno.stat(path);
if (stat.isDirectory) {
const filesIter = await Deno.readDir(path);
const files: EntryInfo[] = [];
for await (const file of filesIter) {
const fileStat = await Deno.stat(join(path, file.name));
files.push({
...file,
lastModified: fileStat.mtime ? new Date(fileStat.mtime) : undefined,
size: fileStat.size,
});
}
return await ctx.render({
type: "dir",
stat,
files,
path,
});
} else {
return await ctx.render({
type: "file",
stat,
path,
});
}
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return await ctx.renderNotFound();
}
throw e;
}
}
async function GET(req: Request, ctx: HandlerContext): Promise<Response> {
const authRequired = Deno.env.get("AUTH_REQUIRED") === "true";
if (authRequired) {
const login = ctx.state["login"];
//console.log("login", login);
if (!login) {
return new Response(null, {
status: 302,
@ -39,30 +128,10 @@ async function GET(req: Request, ctx: HandlerContext): Promise<Response> {
}
const url = new URL(req.url);
const path = removePrefixFromPathname(decodeURI(url.pathname), "/dir");
const stat = await Deno.stat(path);
if (stat.isDirectory) {
const filesIter = await Deno.readDir(path);
const files: EntryInfo[] = [];
for await (const file of filesIter) {
const fileStat = await Deno.stat(join(path, file.name));
files.push({
...file,
lastModified: fileStat.mtime ? new Date(fileStat.mtime) : undefined,
size: fileStat.size,
});
}
return await ctx.render({
type: "dir",
stat,
files,
path,
});
if (url.searchParams.has("pretty")) {
return await renderPage(req, path, ctx);
} else {
return await ctx.render({
type: "file",
stat,
path,
});
return await renderFile(req, path);
}
}
@ -70,8 +139,36 @@ export const handler: Handlers = {
GET,
};
function isImageFile(path: string) {
return /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico|tiff)$/i.test(path);
}
function searchFiles(path: EntryInfo[], fn: (path: EntryInfo) => boolean) {
const candiate = path.filter(fn);
if (candiate.length > 0) {
return candiate[0];
}
return null;
}
export default function DirLists(props: PageProps<DirOrFileProps>) {
const data = props.data;
let cover = null, index = null, content = null;
if (data.type === "dir") {
cover = searchFiles(data.files, (f) => isImageFile(f.name));
index = searchFiles(data.files, (f) => f.name === "index.html");
const contentFilenameCandidate = new Set([
"SUMMARY.md",
"README.md",
"readme.md",
"README.txt",
"readme.txt",
]);
content = searchFiles(
data.files,
(f) => contentFilenameCandidate.has(f.name),
);
}
return (
<>
<Head>
@ -81,6 +178,38 @@ export default function DirLists(props: PageProps<DirOrFileProps>) {
{data.type === "dir"
? <DirList path={data.path} files={data.files}></DirList>
: <FileViewer path={data.path}></FileViewer>}
{index
? (
<a
href={`/dir/${encodePath(join(data.path, index.name))}`}
>
{cover
? (
<img
src={`/dir/${encodePath(join(data.path, cover.name))}`}
/>
)
: (
<span class="border-2 border-gray-300 rounded-md p-2 block mt-2">
Index
</span>
)}
</a>
)
: null}
{content
? (
<div
class="border-2 border-gray-300 rounded-md p-2 mt-2"
id="README"
>
<RenderView
src={`/dir/${encodePath(join(data.path, content.name))}`}
>
</RenderView>
</div>
)
: null}
</div>
</>
);

View File

@ -1,82 +0,0 @@
import { HandlerContext, Handlers } from "$fresh/server.ts";
import { serveFile } from "http/file_server.ts";
import { removePrefixFromPathname } from "../../util/util.ts";
export async function GET(
req: Request,
ctx: HandlerContext,
): Promise<Response> {
const url = new URL(req.url);
const path = removePrefixFromPathname(decodeURI(url.pathname), "/fs");
// if auth is required, check if the user is logged in.
// if not, return a 401.
const authRequired = Deno.env.get("AUTH_REQUIRED") === "true";
if (authRequired) {
const login = ctx.state["login"];
if (!login) {
return new Response(null, {
status: 302,
headers: {
"Location": "/login",
"content-type": "text/plain",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
"Access-Control-Allow-Headers":
"Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With",
},
});
}
}
try {
const fileInfo = await Deno.stat(path);
if (fileInfo.isDirectory) {
// if index.html exists, serve it.
// otherwise, serve a directory listing.
const indexPath = path + "/index.html";
try {
await Deno.stat(indexPath);
const res = await serveFile(req, indexPath);
return res;
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
const list: Deno.DirEntry[] = [];
for await (const entry of Deno.readDir(path)) {
list.push(entry);
}
return new Response(
JSON.stringify(
list,
),
{
headers: {
"content-type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods":
"GET,HEAD,PUT,PATCH,POST,DELETE",
"Access-Control-Allow-Headers":
"Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With",
},
status: 200,
},
);
}
}
}
const res = await serveFile(req, path, {
fileInfo,
});
return res;
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return new Response("Not Found", {
status: 404,
});
}
throw e;
}
}
export const handler: Handlers = {
GET,
};

View File

@ -16,7 +16,7 @@ export default function Home() {
This is a simple file server. It serves files from the{" "}
<code>CWD</code>.
</p>
<a href="/dir/">Go To CWD</a> | <a href="/doc/">Doc</a>
<a href="/dir/?pretty">Go To CWD</a> | <a href="/doc/">Doc</a>
<hr></hr>
<a href="/login">Login</a> | <a href="/api/logout">Logout</a>
</div>

View File

@ -2,3 +2,5 @@
title: "SUMMARY"
tags: ["SUMMARY"]
---
# Summary

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>한글</title>
</head>
<body>
<h1>한글</h1>
<p>한글</p>
</body>
</html>