add readme preview
This commit is contained in:
parent
312964f22c
commit
2f91fb771e
@ -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>
|
||||
);
|
||||
}
|
||||
|
34
fresh.gen.ts
34
fresh.gen.ts
@ -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,
|
||||
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
|
5
main.ts
5
main.ts
@ -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 {
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
@ -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>
|
||||
|
@ -1,4 +1,6 @@
|
||||
---
|
||||
title: "SUMMARY"
|
||||
tags: ["SUMMARY"]
|
||||
---
|
||||
---
|
||||
|
||||
# Summary
|
||||
|
11
test_data/한글/index.html
Normal file
11
test_data/한글/index.html
Normal file
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>한글</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>한글</h1>
|
||||
<p>한글</p>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user