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) { if (text === undefined) {
text = ""; text = "";
} }
let c = text;
if (text.startsWith("---")) {
const index = text.indexOf("\n---", 3); const index = text.indexOf("\n---", 3);
const c = text.slice(index + 4, text.length); c = text.slice(index + 4, text.length);
}
return ( return (
<div <div
class="markdown-body" class="markdown-body"
dangerouslySetInnerHTML={{ __html: marked.parse(c) }} dangerouslySetInnerHTML={{ __html: marked.parse(c) }}
/> >
</div>
); );
} }

View File

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

View File

@ -1,5 +1,5 @@
import { extname } from "path/posix.ts"; import { extname } from "path/posix.ts";
import MarkdownRenderer from "./MarkdownRenderer.tsx"; import MarkdownRenderer from "../components/MarkdownRenderer.tsx";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
const TypeToExt = { const TypeToExt = {
@ -25,8 +25,13 @@ function FetchAndRender(props: { src: string; type: string }) {
const ext = extname(src); const ext = extname(src);
const [content, setContent] = useState(""); const [content, setContent] = useState("");
useEffect(() => { useEffect(() => {
fetch(src).then((res) => res.text()).then(setContent); (async () => {
const res = await fetch(src);
const content = await res.text();
setContent(content);
})();
}, [src]); }, [src]);
switch (props.type) { switch (props.type) {
case "text": case "text":
return <div style={{ whiteSpace: "pre-wrap" }}>{content}</div>; return <div style={{ whiteSpace: "pre-wrap" }}>{content}</div>;
@ -41,7 +46,7 @@ function FetchAndRender(props: { src: string; type: string }) {
//case "csv": //case "csv":
// return <CsvRenderer content={content} />; // return <CsvRenderer content={content} />;
default: 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": case "audio":
return <audio style={{ width: "100%" }} controls src={src} />; return <audio style={{ width: "100%" }} controls src={src} />;
default: 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> </li>
<ListItem <ListItem
key=".." key=".."
href={`/dir/${encodePath(join(data.path, ".."))}`} href={`/dir/${encodePath(join(data.path, ".."))}?pretty`}
icon="/icon/back.svg" icon="/icon/back.svg"
> >
... ...
@ -61,7 +61,7 @@ export function DirList(props: DirListProps) {
{files.map((file) => ( {files.map((file) => (
<ListItem <ListItem
key={file.name} key={file.name}
href={`/dir/${encodePath(join(data.path, file.name))}`} href={`/dir/${encodePath(join(data.path, file.name))}?pretty`}
icon={file.isDirectory icon={file.isDirectory
? "/icon/folder.svg" ? "/icon/folder.svg"
: extToIcon(extname(file.name))} : extToIcon(extname(file.name))}

View File

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

View File

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

View File

@ -22,7 +22,10 @@ export default function UpList(props: { path: string }) {
return ( return (
<div> <div>
<h1 class={"text-2xl flex flex-wrap"}> <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")} /> <img src={asset("/icon/house.svg")} />
<span class="ml-1">Home</span> <span class="ml-1">Home</span>
</a> </a>
@ -31,7 +34,7 @@ export default function UpList(props: { path: string }) {
<span class="p-2">/</span> <span class="p-2">/</span>
<a <a
class="flex flex-wrap p-2 hover:bg-gray-400 rounded-sm" 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")} /> <img src={asset("/icon/folder.svg")} />
<span class="ml-1">{up}</span> <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 { key_out_cmd } from "./keyout.ts";
import { prepareDocs } from "./src/store/doc.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"), join(fromFileUrl(import.meta.url), "..", "static", "github-markdown.css"),
); )).replaceAll("\n", "");
const CSSPlugin: Plugin = { const CSSPlugin: Plugin = {
name: "css plugin", name: "css plugin",
// deno-lint-ignore require-await
render(ctx): PluginRenderResult { render(ctx): PluginRenderResult {
ctx.render(); ctx.render();
return { return {

View File

View File

@ -1,9 +1,11 @@
import { HandlerContext, Handlers, PageProps } from "$fresh/server.ts"; import { HandlerContext, Handlers, PageProps } from "$fresh/server.ts";
import { asset, Head } from "$fresh/runtime.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 { join } from "path/posix.ts";
import DirList, { EntryInfo } from "../../islands/DirList.tsx"; import DirList, { EntryInfo } from "../../islands/DirList.tsx";
import FileViewer from "../../islands/FileViewer.tsx"; import FileViewer from "../../islands/FileViewer.tsx";
import RenderView from "../../islands/ContentRenderer.tsx";
import { serveFile } from "http/file_server.ts";
type DirProps = { type DirProps = {
type: "dir"; type: "dir";
@ -19,27 +21,60 @@ type FileProps = {
type DirOrFileProps = DirProps | FileProps; type DirOrFileProps = DirProps | FileProps;
async function GET(req: Request, ctx: HandlerContext): Promise<Response> { async function renderFile(req: Request, path: string) {
const authRequired = Deno.env.get("AUTH_REQUIRED") === "true"; try {
if (authRequired) { const fileInfo = await Deno.stat(path);
const login = ctx.state["login"]; if (fileInfo.isDirectory) {
if (!login) { // if index.html exists, serve it.
return new Response(null, { // otherwise, serve a directory listing.
status: 302, 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: { headers: {
"Location": "/login", "content-type": "application/json",
"content-type": "text/plain",
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,HEAD,PUT,PATCH,POST,DELETE", "Access-Control-Allow-Methods":
"GET,HEAD,PUT,PATCH,POST,DELETE",
"Access-Control-Allow-Headers": "Access-Control-Allow-Headers":
"Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With", "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;
} }
const url = new URL(req.url); }
const path = removePrefixFromPathname(decodeURI(url.pathname), "/dir");
async function renderPage(_req: Request, path: string, ctx: HandlerContext) {
try {
const stat = await Deno.stat(path); const stat = await Deno.stat(path);
if (stat.isDirectory) { if (stat.isDirectory) {
const filesIter = await Deno.readDir(path); const filesIter = await Deno.readDir(path);
const files: EntryInfo[] = []; const files: EntryInfo[] = [];
@ -64,14 +99,76 @@ async function GET(req: Request, ctx: HandlerContext): Promise<Response> {
path, 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,
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",
},
});
}
}
const url = new URL(req.url);
const path = removePrefixFromPathname(decodeURI(url.pathname), "/dir");
if (url.searchParams.has("pretty")) {
return await renderPage(req, path, ctx);
} else {
return await renderFile(req, path);
}
} }
export const handler: Handlers = { export const handler: Handlers = {
GET, 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>) { export default function DirLists(props: PageProps<DirOrFileProps>) {
const data = props.data; 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 ( return (
<> <>
<Head> <Head>
@ -81,6 +178,38 @@ export default function DirLists(props: PageProps<DirOrFileProps>) {
{data.type === "dir" {data.type === "dir"
? <DirList path={data.path} files={data.files}></DirList> ? <DirList path={data.path} files={data.files}></DirList>
: <FileViewer path={data.path}></FileViewer>} : <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> </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{" "} This is a simple file server. It serves files from the{" "}
<code>CWD</code>. <code>CWD</code>.
</p> </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> <hr></hr>
<a href="/login">Login</a> | <a href="/api/logout">Logout</a> <a href="/login">Login</a> | <a href="/api/logout">Logout</a>
</div> </div>

View File

@ -2,3 +2,5 @@
title: "SUMMARY" title: "SUMMARY"
tags: ["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>