Changes
8 changed files (+201/-14)
-
-
@@ -6,7 +6,7 @@## Document metadata Properties describing a *document* or a *document directory* required for generating a website. *Document metadata* consists of a *document name*, *document title*, and language of the document or the document directory. *Document metadata* consists of a *document name*, *document title*, whether the document is default document, and language of the document or the document directory. ## Document name
-
-
-
@@ -132,6 +132,22 @@ await (fileSystemReader.readFile(this.#siteLogo)),); } const defaultPage = [...documentTree.defaultDocument.path, ""].join("/"); const redirectHtml = [ "<!DOCTYPE html>", "<html><head>", `<meta charset="utf-8">`, `<meta http-equiv="refresh" content="0; URL='${defaultPage}'">`, "</head><body>", // For cases when a user or UA disallows automatic redirection. `<a href="${defaultPage}">TOP</a>`, "</body></html>", ].join(""); await fileSystemWriter.write( ["index.html"], new TextEncoder().encode(redirectHtml), ); await Promise.all(documentTree.nodes.map((item) => this.#build({ item,
-
-
-
@@ -20,6 +20,7 @@ --color-fg: #333;--color-fg-sub: #534c37; --color-fg-light: #c5c5c5; --color-border: #b4af9d; --color-subtle-overlay: hsl(0deg 0% 0% / 0.035); color: var(--color-fg); --canvas-color-red: #e03131;
-
@@ -43,6 +44,7 @@ --color-fg: #fafafa;--color-fg-sub: #f3edd9; --color-fg-light: #c5c5c5; --color-border: #b4af9d; --color-subtle-overlay: hsl(0deg 0% 100% / 0.1); --canvas-node-bg-opacity: 0.1; }
-
-
-
@@ -100,6 +100,7 @@ />} footer={<Footer.View copyright={copyright} />} logoImage={assets.siteLogo} defaultDocument={tree.defaultDocument} > <h1>{document.metadata.title}</h1> {contentNodes}
-
@@ -124,6 +125,7 @@ />} footer={<Footer.View copyright={copyright} />} logoImage={assets.siteLogo} defaultDocument={tree.defaultDocument} > <h1>{document.metadata.title}</h1> <JSONCanvasRenderer.View data={content.content} />
-
-
-
@@ -6,6 +6,8 @@ /** @jsx h */import { h } from "../../../../deps/deno.land/x/nano_jsx/mod.ts"; import { type Document } from "../../../../types.ts"; import { usePathResolver } from "../../contexts/path_resolver.tsx"; import { css } from "../../css.ts";
-
@@ -15,6 +17,7 @@ Layout = "t-sl--root",HeaderBg = "t-sl--headbg", Header = "t-sl--head", Logo = "t-sl--lg", LogoLink = "t-sl--ll", Nav = "t-sl--nav", NavInner = "t-sl--nav-i", FooterBg = "t-sl--fbg",
-
@@ -65,11 +68,18 @@ position: sticky;top: calc(var(--baseline) * -0.5rem); } .${C.LogoLink} { display: flex; border-radius: 4px; } .${C.LogoLink}:hover { background-color: var(--color-subtle-overlay); } .${C.Logo} { position: relative; padding: 4px; border-radius: 4px; box-shadow: none; }
-
@@ -157,10 +167,13 @@logoImage?: readonly string[]; logoSize?: number; defaultDocument: Document; } export function View( { children, nav, aside, footer, logoImage, logoSize = 32 }: ViewProps, { children, nav, aside, footer, logoImage, logoSize = 32, defaultDocument }: ViewProps, ) { const path = usePathResolver();
-
@@ -169,12 +182,19 @@ <div className={C.Layout}><div className={C.HeaderBg} /> {logoImage && ( <header className={C.Header}> <img className={C.Logo} src={path.resolve(logoImage)} width={logoSize} height={logoSize} /> <a className={C.LogoLink} href={path.resolve([...defaultDocument.path, ""])} title={defaultDocument.metadata.title} lang={defaultDocument.metadata.language} > <img className={C.Logo} src={path.resolve(logoImage)} width={logoSize} height={logoSize} /> </a> </header> )} <nav className={C.Nav}>
-
-
-
@@ -6,11 +6,13 @@ import {assertEquals, assertNotEquals, assertObjectMatch, assertRejects, } from "../deps/deno.land/std/assert/mod.ts"; import { MemoryFsReader } from "../filesystem_reader/memory_fs.ts"; import { noopParser } from "../content_parser/noop.ts"; import { defaultDocumentAt, DefaultTreeBuilder, fileExtensions, ignore,
-
@@ -61,6 +63,13 @@ title: "Foo.md",}, file: { name: "Foo.md", }, }); assertObjectMatch(tree.defaultDocument, { metadata: { name: "Foo.md", title: "Foo.md", }, }); });
-
@@ -177,6 +186,7 @@ Deno.test("Should skip empty directories", async () => {const fileSystemReader = new MemoryFsReader([ { path: "a/b/c/d/e.txt", content: "" }, { path: "a/b/f.txt", content: "" }, { path: "g.md", content: "" }, ]); const builder = new DefaultTreeBuilder({ defaultLanguage: "en",
-
@@ -188,7 +198,34 @@ fileSystemReader,contentParser, }); assertEquals(tree.nodes.length, 0); assertObjectMatch(tree, { nodes: [ { metadata: { name: "g.md", }, }, ], }); assertEquals(tree.nodes.length, 1); }); Deno.test("Should throw an error if tree has no documents", async () => { const fileSystemReader = new MemoryFsReader([ { path: "a/b/c/d/e.txt", content: "" }, { path: "a/b/f.txt", content: "" }, ]); const builder = new DefaultTreeBuilder({ defaultLanguage: "en", strategies: [fileExtensions([".md"])], }); await assertRejects(() => builder.build({ fileSystemReader, contentParser, }) ); }); Deno.test("ignore() and ignoreDotfiles() should ignore files and directories", async () => {
-
@@ -386,3 +423,27 @@ },], }); }); Deno.test("defaultDocumentAt() should set default document at the given path", async () => { const fileSystemReader = new MemoryFsReader([ { path: "Alice/Profile.md", content: "" }, { path: "Welcome.md", content: "" }, { path: "Bob/Admin/Readme.md", content: "" }, ]); const builder = new DefaultTreeBuilder({ defaultLanguage: "en", strategies: [defaultDocumentAt(["Bob", "Admin", "Readme.md"])], }); const tree = await builder.build({ fileSystemReader, contentParser, }); assertObjectMatch(tree.defaultDocument, { metadata: { name: "Readme.md", title: "Readme.md", }, }); });
-
-
-
@@ -127,6 +127,30 @@ };}; } /** * Mark file at specific path to be the default document. * * @param path - Relative path from the root directory (FileSystem Reader). */ export function defaultDocumentAt(path: readonly string[]): TreeBuildStrategy { return (node, metadata) => { if (node.type !== "file") { return { metadata }; } if (node.path.every((segment, i) => segment === path[i])) { return { metadata: { ...metadata, isDefaultDocument: true, }, }; } return { metadata }; }; } export interface DefaultTreeBuilderConfig { /** * Default language tag (BCP 47).
-
@@ -182,11 +206,21 @@ const entries = await Promise.all(children.map((child) => this.#build(child, { contentParser })), ); const nodes = entries.filter((entry): entry is NonNullable<typeof entry> => !!entry ).toSorted(this.#sorter); const defaultDocument = this.#findDefaultDocument(nodes); if (!defaultDocument) { throw new Error( "No document found. Document tree must have at least one document.", ); } return { type: "tree", nodes: entries.filter((entry): entry is NonNullable<typeof entry> => !!entry ).toSorted(this.#sorter), nodes, defaultDocument, defaultLanguage: this.#defaultLanguage, }; }
-
@@ -257,5 +291,45 @@ directory: node,entries: includingEntries, path: [...parentPath, metadata.name], }; } #findDefaultDocument( tree: ReadonlyArray<Document | DocumentDirectory>, depth: number = 0, registry: Map<number, Document> | null = null, ): Document | null { const map = registry || new Map<number, Document>(); for (const item of tree) { if (item.type === "document") { if (item.metadata.isDefaultDocument) { return item; } if (!map.has(depth)) { map.set(depth, item); } continue; } const found = this.#findDefaultDocument(item.entries, depth + 1, map); if (found) { return found; } } if (depth === 0) { const topmost = Array.from(map.entries()).toSorted(([a], [b]) => a - b )[0]; if (!topmost) { return null; } return topmost[1]; } return null; } }
-
-
-
@@ -46,6 +46,13 @@ * If this is empty, Macana looks up the most closest document directory language set.* If none of the ancestors have a language, Macana will use a user given default language. */ readonly language?: string; /** * Whether this document is the default document for the entire document tree. * The behavior of when multiple documents have this property set to true is undefined. * This property does not take an effect for document tree. */ readonly isDefaultDocument?: boolean; } export interface DocumentContent<
-
@@ -86,4 +93,9 @@ readonly type: "tree";readonly nodes: ReadonlyArray<Document | DocumentDirectory>; readonly defaultLanguage: string; /** * Representive, facade document. */ readonly defaultDocument: Document; }
-