├── .gitignore ├── docs ├── en │ ├── showcase │ │ ├── __category.md │ │ └── code_blocks.md │ ├── customization │ │ ├── themes.md │ │ ├── __category.md │ │ └── custom_sidebar.md │ ├── guides │ │ ├── __category.md │ │ └── deno_deploy.md │ ├── introduction │ │ ├── example.md │ │ └── __category.md │ └── sidebar.json ├── fr │ ├── showcase │ │ ├── __category.md │ │ └── code_blocks.md │ ├── customization │ │ ├── themes.md │ │ ├── __category.md │ │ └── custom_sidebar.md │ ├── guides │ │ └── __category.md │ ├── introduction │ │ ├── example.md │ │ └── __category.md │ └── sidebar.json ├── metadata.json ├── build.ts └── server.ts ├── .vscode ├── settings.json └── extensions.json ├── .netlify └── build.sh ├── assets ├── menu.svg ├── moon.svg └── sun.svg ├── .editorconfig ├── deps.ts ├── .github └── workflows │ └── cd.yml ├── readme.md ├── types.ts ├── LICENSE.md ├── utils.ts ├── mod.ts ├── styles.css └── vale.ts /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /docs/en/showcase/__category.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 4. Showcase 3 | --- 4 | 5 | # Showcase 6 | -------------------------------------------------------------------------------- /docs/fr/showcase/__category.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 4. Showcase 3 | --- 4 | 5 | # Showcase 6 | -------------------------------------------------------------------------------- /docs/en/customization/themes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3.2 Themes 3 | --- 4 | 5 | # Themes 6 | 7 | WIP 8 | -------------------------------------------------------------------------------- /docs/fr/customization/themes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3.2 Themes 3 | --- 4 | 5 | # Themes 6 | 7 | WIP 8 | -------------------------------------------------------------------------------- /docs/en/customization/__category.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3. Customization 3 | --- 4 | 5 | # Customization 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true 5 | } 6 | -------------------------------------------------------------------------------- /docs/en/customization/custom_sidebar.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3.1 Sidebar 3 | --- 4 | 5 | # Sidebar 6 | 7 | WIP 8 | -------------------------------------------------------------------------------- /docs/fr/customization/__category.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3. Personnalisation 3 | --- 4 | 5 | # Personnalisation 6 | -------------------------------------------------------------------------------- /docs/en/guides/__category.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 2. Guides 3 | --- 4 | 5 | # Guides 6 | 7 | Different guides related to Vale. 8 | -------------------------------------------------------------------------------- /docs/fr/customization/custom_sidebar.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3.1 Barre latérale 3 | --- 4 | 5 | # Barre latérale 6 | 7 | WIP 8 | -------------------------------------------------------------------------------- /docs/fr/guides/__category.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 2. Guides 3 | --- 4 | 5 | # Guides 6 | 7 | Different guides related to Vale. 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "denoland.vscode-deno", 4 | "editorconfig.editorconfig" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.netlify/build.sh: -------------------------------------------------------------------------------- 1 | echo "Installing deno..." 2 | curl -fsSL https://deno.land/x/install/install.sh | sh 3 | export PATH="/opt/buildhome/.deno/bin:$PATH" 4 | mkdir dist 5 | echo "Building website with Vale..." 6 | 7 | deno run -A --unstable build_website.ts -------------------------------------------------------------------------------- /docs/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Vale", 3 | "reference": "en", 4 | "languages": [ 5 | { 6 | "name": "English", 7 | "code": "en" 8 | }, 9 | { 10 | "name": "Français", 11 | "code": "fr" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /docs/build.ts: -------------------------------------------------------------------------------- 1 | import { join } from "https://deno.land/std@0.122.0/path/mod.ts"; 2 | import { ValeBuilder } from "../vale.ts"; 3 | 4 | const projectPath = join(Deno.cwd(), "docs"); 5 | 6 | const builder = await ValeBuilder.create(projectPath); 7 | await builder.build(); 8 | -------------------------------------------------------------------------------- /assets/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.css] 13 | indent_style = space 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /assets/moon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/en/introduction/example.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 1.1 Example 3 | --- 4 | 5 | # Example 6 | 7 | Use the template generator to create a new template. 8 | 9 | ```bash 10 | vale init my-demo 11 | ``` 12 | 13 | Run in development mode: 14 | 15 | ``` 16 | vale watch my-demo 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/en/showcase/code_blocks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 4.1 Code Blocks 3 | --- 4 | 5 | # Code blocks 6 | 7 | TypeScript code: 8 | 9 | ```ts 10 | async function getString(): Promise { 11 | return "Hello World!"; // Nice 12 | } 13 | 14 | const data = await getString(); 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/fr/showcase/code_blocks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 4.1 Blocs de code 3 | --- 4 | 5 | # Blocs de code 6 | 7 | Code TypeScript : 8 | 9 | ```ts 10 | async function getString(): Promise { 11 | return "Hello World!"; // Nice 12 | } 13 | 14 | const data = await getString(); 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/fr/introduction/example.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 1.1 Exemple 3 | --- 4 | 5 | # Exemple 6 | 7 | Utilisez le générateur de modèles pour créer un nouveau modèle. 8 | 9 | ```bash 10 | vale init my-demo 11 | ``` 12 | 13 | Lancer le mode de développement : 14 | 15 | ``` 16 | vale watch my-demo 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/fr/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "1. Introduction": [ 3 | "1.1 Exemple" 4 | ], 5 | "2. Guides": [ 6 | "2.1 Deploy in Deno Deploy" 7 | ], 8 | "3. Personnalisation": [ 9 | "3.1 Barre latérale", 10 | "3.2 Themes" 11 | ], 12 | "4. Showcase": [ 13 | "3.1 Blocs de code" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /docs/en/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "1. Introduction": [ 3 | "1.1 Example", 4 | "1.2 Test" 5 | ], 6 | "2. Guides": [ 7 | "2.1 Deploy in Deno Deploy" 8 | ], 9 | "3. Customization": [ 10 | "3.1 Sidebar", 11 | "3.2 Themes" 12 | ], 13 | "4. Showcase": [ 14 | "4.1 Code Blocks" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Command, 3 | CompletionsCommand, 4 | HelpCommand, 5 | } from "https://deno.land/x/cliffy@v0.20.1/mod.ts"; 6 | export { serve } from "https://deno.land/std@0.132.0/http/server.ts"; 7 | export { 8 | serveDir, 9 | serveFile, 10 | } from "https://deno.land/std@0.132.0/http/file_server.ts"; 11 | export { 12 | basename, 13 | dirname, 14 | extname, 15 | join, 16 | parse, 17 | } from "https://deno.land/std@0.122.0/path/mod.ts"; 18 | export { Marked } from "https://deno.land/x/markdown@v2.0.0/mod.ts"; 19 | -------------------------------------------------------------------------------- /assets/sun.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/server.ts: -------------------------------------------------------------------------------- 1 | import { extname, join } from "https://deno.land/std@0.122.0/path/mod.ts"; 2 | import { serve } from "https://deno.land/std@0.132.0/http/server.ts"; 3 | import { 4 | serveDir, 5 | serveFile, 6 | } from "https://deno.land/std@0.132.0/http/file_server.ts"; 7 | 8 | const projectPath = join(Deno.cwd(), "docs"); 9 | const distPath = join(projectPath, "dist"); 10 | 11 | serve((req) => { 12 | const pathname = new URL(req.url).pathname; 13 | if (extname(pathname) == "") { 14 | return serveFile(req, join(distPath, pathname, "index.html")); 15 | } else { 16 | return serveDir(req, { 17 | fsRoot: distPath, 18 | }); 19 | } 20 | }); 21 | 22 | console.log("Running!"); 23 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: [push] 3 | 4 | jobs: 5 | deploy: 6 | name: Deploy 7 | runs-on: ubuntu-latest 8 | permissions: 9 | id-token: write # Needed for auth with Deno Deploy 10 | contents: read # Needed to clone the repository 11 | 12 | steps: 13 | - name: Clone repository 14 | uses: actions/checkout@v2 15 | 16 | - uses: denoland/setup-deno@v1 17 | with: 18 | deno-version: vx.x.x 19 | 20 | - name: Build the website 21 | run: deno run -A --unstable ./docs/build.ts 22 | 23 | - name: Upload to Deno Deploy 24 | uses: denoland/deployctl@v1 25 | with: 26 | project: "vale" 27 | entrypoint: "./docs/server.ts" -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | > WORK IN PROGRESS ⚠️ Expect breaking changes ⚠️ 2 | 3 | # 📝 Vale 4 | 5 | **Vale** is a static documentation generator, designed for speed, simplicity and 6 | readability. Built with Deno, but you can use it for any project. Inspired by 7 | [Deno Manual](https://deno.land/manual) and 8 | [mdbook](https://rust-lang.github.io/mdBook/). 9 | 10 | ## 🎉 Features 11 | 12 | - Multiple languages 13 | - Code blocks support 14 | 15 | I plan to add other features such as a searcher, third-party links on the 16 | navbar, "Edit on Github" link, page tags, SSR, themes, copy button in code 17 | blocks, etc... 18 | 19 | ## 📦 Installation 20 | 21 | Install Vale with Deno: 22 | 23 | ```bash 24 | deno install --allow-env --allow-read --allow-write --allow-net --unstable -n vale https://deno.land/x/vale/mod.ts 25 | ``` 26 | 27 | Create and run a basic project: 28 | 29 | ```bash 30 | vale init demo 31 | vale watch demo 32 | ``` 33 | 34 | Please give it a [⭐ Star](https://github.com/marc2332/vale) if you like it :) 35 | 36 | Made by [Marc Espín](https://github.com/marc2332) 37 | 38 | MIT License 39 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | export interface CategoryData { 2 | name: string; 3 | doc: ContentDoc; 4 | } 5 | 6 | export interface TreeFile { 7 | path: string; 8 | title: string; 9 | content?: string; 10 | entries?: Array; 11 | } 12 | 13 | export type Tree = Array; 14 | 15 | export interface Metadata { 16 | title: string; 17 | reference: string; 18 | languages: { code: string; name: string }[]; 19 | } 20 | 21 | export type Sidebar = { [key: string]: string[] }; 22 | 23 | export interface DocEntry { 24 | content: string; 25 | path: string; 26 | title: string; 27 | } 28 | 29 | export interface ContentDoc { 30 | entry: DocEntry; 31 | entries: Map; 32 | } 33 | 34 | export interface ProcessResult { 35 | htmlCode: string; 36 | path: string; 37 | lastEntry: DocEntry | undefined; 38 | } 39 | 40 | export interface ValeData { 41 | projectRoot: string; 42 | metadata: Metadata; 43 | assets: CachedAssets; 44 | } 45 | 46 | export interface CachedAssets { 47 | svgMenu: string; 48 | sunSvg: string; 49 | moonSvg: string; 50 | stylesPathCached: string; 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Marc Espín Sanz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /docs/en/introduction/__category.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 1. Introduction 3 | --- 4 | 5 | > WORK IN PROGRESS ⚠️ Expect breaking changes ⚠️ 6 | 7 | # 👋 Introduction 8 | 9 | **Vale** is a static documentation generator, designed for speed, simplicity and 10 | readability. Built with Deno, but you can use it for any project. Inspired by 11 | [Deno Manual](https://deno.land/manual) and 12 | [mdbook](https://rust-lang.github.io/mdBook/). 13 | 14 | Source code is on [GitHub](https://github.com/marc2332/vale), please leave a 15 | star if you liked it ⭐ :) 16 | 17 | ## 🎉 Features 18 | 19 | - Multiple languages 20 | - Code blocks support 21 | 22 | I plan to add other features such as a searcher, third-party links on the 23 | navbar, "Edit on Github" link, page tags, SSR, themes, copy button in code 24 | blocks, etc... 25 | 26 | ## 📦 Installation 27 | 28 | Install Vale with Deno: 29 | 30 | ```bash 31 | deno install --allow-env --allow-read --allow-write --allow-net --unstable -n vale https://deno.land/x/vale/mod.ts 32 | ``` 33 | 34 | Create and run a basic project: 35 | 36 | ```bash 37 | vale init demo 38 | vale watch demo 39 | ``` 40 | 41 | Made by [Marc Espín](https://github.com/marc2332) 42 | 43 | MIT License 44 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from "https://deno.land/std@0.122.0/path/mod.ts"; 2 | import { namespace } from "https://deno.land/x/cache@0.2.13/mod.ts"; 3 | import { CachedAssets } from "./types.ts"; 4 | 5 | const __dirname = dirname(import.meta.url); 6 | 7 | const valeNamespace = namespace("vale"); 8 | 9 | export const getCachedAssets = async (): Promise => { 10 | // Cache the SVG files 11 | const svgMenuPath = join(__dirname, "assets", "menu.svg"); 12 | const svgMenuPathCached = (await valeNamespace.cache(svgMenuPath)).path; 13 | const svgMenu = await Deno.readTextFile(svgMenuPathCached); 14 | 15 | const sunSvgPath = join(__dirname, "assets", "sun.svg"); 16 | const sunSvgPathCached = (await valeNamespace.cache(sunSvgPath)).path; 17 | const sunSvg = await Deno.readTextFile(sunSvgPathCached); 18 | 19 | const moonSvgPath = join(__dirname, "assets", "moon.svg"); 20 | const moonSvgPathCached = (await valeNamespace.cache(moonSvgPath)).path; 21 | const moonSvg = await Deno.readTextFile(moonSvgPathCached); 22 | 23 | const stylesPath = join(__dirname, "styles.css"); 24 | const stylesPathCached = (await valeNamespace.cache(stylesPath)).path; 25 | 26 | return { 27 | svgMenu, 28 | sunSvg, 29 | moonSvg, 30 | stylesPathCached, 31 | }; 32 | }; 33 | export const clearAssetsCache = () => { 34 | return valeNamespace.purge(); 35 | }; 36 | -------------------------------------------------------------------------------- /docs/fr/introduction/__category.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 1. Introduction 3 | --- 4 | 5 | > EN COURS DE DÉVELOPPEMENT ⚠️ Attendez vous à des changements importants ⚠️ 6 | 7 | # 👋 Introduction 8 | 9 | **Vale** est un générateur de documentation statique, designé pour être rapide, 10 | simple et lisible. Développé avec Deno, mais vous pouvez l'utiliser pour tous 11 | vos projets. Inspiré par [Deno Manual](https://deno.land/manual) et 12 | [mdbook](https://rust-lang.github.io/mdBook/). 13 | 14 | Le code source est disponible sur [GitHub](https://github.com/marc2332/vale), 15 | laissez une étoiles si vous aimez le projet ⭐ :) 16 | 17 | ## 🎉 Features 18 | 19 | - Multi-langues supporté 20 | - Support des blocs de code 21 | 22 | J'ai prévu de rajouter d'autres fonctionnalités tels qu'un barre de recherche, 23 | la possibilité d'ajouter des liens externes dans la barre latérale, un lien 24 | "Editer sur GitHub", des tags pour les pages, la génération côté serveur (SRR), 25 | le support de themes, un bouton pour copier les blocs de codes, ... 26 | 27 | ## 📦 Installation 28 | 29 | Installer Vale avec Deno: 30 | 31 | ```bash 32 | deno install --allow-env --allow-read --allow-write --allow-net --unstable -n vale https://deno.land/x/vale/mod.ts 33 | ``` 34 | 35 | Créer et lancer un projet basique : 36 | 37 | ```bash 38 | vale init demo 39 | vale watch demo 40 | ``` 41 | 42 | Fait par [Marc Espín](https://github.com/marc2332) 43 | 44 | MIT License 45 | -------------------------------------------------------------------------------- /docs/en/guides/deno_deploy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 2.1 Deploy in Deno Deploy 3 | --- 4 | 5 | > This guide is ⚠️ WORK IN PROGRESS ⚠️ 6 | 7 | # Deploy in Deno Deploy 8 | 9 | Want to deploy a Vale project into [Deno Deploy](https://deno.com/deploy/)? It's 10 | possible! 11 | 12 | ## 1. Create Project 13 | 14 | Let's start by cloning this repository: 15 | 16 | ``` 17 | git clone https://github.com/marc2332/vale-deno-deploy.git 18 | ``` 19 | 20 | ## 2. Overview 21 | 22 | Now, in order to deploy a Vale website in Deno Deploy we need a small Github 23 | Action workflow in order to build the website. 24 | 25 | You can see this workflow under `.github/workflows/deploy.yml`: 26 | 27 | ```yml 28 | name: Deploy 29 | on: [push] 30 | 31 | jobs: 32 | deploy: 33 | name: Deploy 34 | runs-on: ubuntu-latest 35 | permissions: 36 | id-token: write 37 | contents: read 38 | 39 | steps: 40 | - name: Clone repository 41 | uses: actions/checkout@v2 42 | 43 | - uses: denoland/setup-deno@v1 44 | with: 45 | deno-version: vx.x.x 46 | 47 | - name: Install vale 48 | run: deno install --allow-env --allow-read --allow-write --allow-net --unstable -n vale https://deno.land/x/vale@0.1.4/mod.ts 49 | 50 | - name: Build the website 51 | run: vale build docs 52 | 53 | - name: Upload to Deno Deploy 54 | uses: denoland/deployctl@v1 55 | with: 56 | project: "YOUR_DENO_DEPLOY_PROJECT_NAME" 57 | entrypoint: "./server.ts" 58 | ``` 59 | 60 | You must change `YOUR_DENO_DEPLOY_PROJECT_NAME` to a non-taken project name in 61 | Deno Deploy, try puting `vale-`. 62 | 63 | ## 3. Create a repository in Github 64 | 65 | You now need to create and push this project into a repository in Github. 66 | 67 | ## 4. Link in Deno Deploy 68 | 69 | Now, go to [Deno Deploy](https://deno.com/deploy/), create a new project with 70 | the same name you put on `YOUR_DENO_DEPLOY_PROJECT_NAME` and link the same 71 | respository you created on Github into your Deno Deploy project. 72 | 73 | ## 5. Awesome! 74 | 75 | Is it cool? :D 76 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Command, 3 | CompletionsCommand, 4 | extname, 5 | HelpCommand, 6 | join, 7 | serve, 8 | serveDir, 9 | serveFile, 10 | } from "./deps.ts"; 11 | import { clearAssetsCache } from "./utils.ts"; 12 | import { ValeBuilder } from "./vale.ts"; 13 | 14 | const PORT = Deno.env.get("PORT") || 3500; 15 | 16 | const serveCommand = (distPath: string) => { 17 | serve((req: Request) => { 18 | const pathname = new URL(req.url).pathname; 19 | if (extname(pathname) == "") { 20 | return serveFile(req, join(distPath, pathname, "index.html")); 21 | } else { 22 | return serveDir(req, { 23 | fsRoot: distPath, 24 | }); 25 | } 26 | }, { 27 | port: 3500, 28 | }); 29 | }; 30 | 31 | const buildCommand = (dir: string): [string, string] => { 32 | const projectPath = join(Deno.cwd(), dir); 33 | const distPath = join(projectPath || "", "dist"); 34 | return [ 35 | projectPath, 36 | distPath, 37 | ]; 38 | }; 39 | 40 | const initCommand = async (projectPath: string, title: string) => { 41 | const langPath = join(projectPath, "en"); 42 | 43 | await Deno.mkdir(langPath, { recursive: true }); 44 | 45 | await Deno.writeTextFile( 46 | join(projectPath, "metadata.json"), 47 | JSON.stringify({ 48 | title, 49 | reference: "en", 50 | languages: [ 51 | { 52 | name: "English", 53 | code: "en", 54 | }, 55 | ], 56 | }), 57 | ); 58 | 59 | await Deno.writeTextFile( 60 | join(langPath, "sidebar.json"), 61 | JSON.stringify({ 62 | "1. Hello World": [], 63 | }), 64 | ); 65 | 66 | const helloWorldCategoryPath = join(langPath, "hello_world"); 67 | 68 | await Deno.mkdir(helloWorldCategoryPath); 69 | 70 | await Deno.writeTextFile( 71 | join(helloWorldCategoryPath, "__category.md"), 72 | `--- 73 | title: 1. Hello World 74 | --- 75 | 76 | # 👋 Hello World 77 | 78 | Thanks for using Vale :)! Please give it a [⭐ Star](https://github.com/marc2332/vale) 79 | 80 | `, 81 | ); 82 | }; 83 | 84 | await new Command() 85 | .name("vale") 86 | .version("0.1.0") 87 | .global() 88 | .description(`Manage Vale projects`) 89 | .command("help", new HelpCommand().global()) 90 | .command("completions", new CompletionsCommand()) 91 | .command( 92 | "watch [optional]", 93 | "Run the documentation in development mode.", 94 | ) 95 | .option("-r, --reload [reload:boolean]", "Reload vale assets") 96 | .action(async ({ reload }: { reload: boolean }, dir: string) => { 97 | if (reload) await clearAssetsCache(); 98 | 99 | const [projectPath, distPath] = buildCommand(dir); 100 | const builder = await ValeBuilder.create(projectPath); 101 | await builder.build(); 102 | 103 | serveCommand(distPath); 104 | 105 | console.log(`Development server running on on http://localhost:${PORT}/`); 106 | 107 | const watcher = Deno.watchFs(projectPath); 108 | for await (const event of watcher) { 109 | if (event.paths.find((path) => path.startsWith(distPath))) { 110 | continue; 111 | } 112 | await builder.build(); 113 | } 114 | }) 115 | .command( 116 | "build [optional]", 117 | "Build the documentation.", 118 | ) 119 | .option("-r, --reload [reload:boolean]", "Reload vale assets") 120 | .action(async ({ reload }: { reload: boolean }, dir: string) => { 121 | if (reload) await clearAssetsCache(); 122 | 123 | const projectPath = join(Deno.cwd(), dir); 124 | const builder = await ValeBuilder.create(projectPath); 125 | await builder.build(); 126 | console.log("Built successfully!"); 127 | }) 128 | .command( 129 | "init [optional]", 130 | "Create a new project.", 131 | ) 132 | .action(async (_, title: string) => { 133 | const projectPath = join(Deno.cwd(), title); 134 | 135 | await initCommand(projectPath, title); 136 | 137 | console.log("Created successfully!"); 138 | console.log(`To run use 'vale watch ${title}'`); 139 | }) 140 | .command( 141 | "serve [optional]", 142 | "Serve the documentation.", 143 | ) 144 | .action((_, dir: string) => { 145 | const [_projectPath, distPath] = buildCommand(dir); 146 | 147 | serveCommand(distPath); 148 | 149 | console.log(`Serving documentation on http://localhost:${PORT}/`); 150 | }) 151 | .parse(Deno.args); 152 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; 3 | font-family: ui-sans-serif, system-ui, sans-serif; 4 | height: 100%; 5 | --text-color: #222; 6 | --background-color: #fff; 7 | --background-sidebar-color: rgb(250, 250, 250); 8 | --border: rgb(240, 240, 240); 9 | --title-border: rgb(200, 200, 200); 10 | --pre-background: rgb(250, 250, 250); 11 | --quote-background: rgb(255 242 220); 12 | --quote-border: rgb(234 185 67); 13 | --active-link-sidebar-color: rgb(0, 132, 255); 14 | --hover-link-sidebar-color: rgb(100, 100, 100); 15 | --content-links: rgb(0, 132, 255); 16 | --navbar-button-background: rgb(250, 250, 250); 17 | --navbar-button-border: rgb(240, 240, 240); 18 | --navbar-icon-fill: black; 19 | } 20 | 21 | body.dark-theme { 22 | --text-color: #eee; 23 | --background-color: #181818; 24 | --background-sidebar-color: #141414; 25 | --border: rgb(35, 35, 35); 26 | --title-border: rgb(100, 100, 100); 27 | --pre-background: #252525; 28 | --quote-background: #686454; 29 | --quote-border: #fff1d8; 30 | --active-link-sidebar-color: rgb(0, 132, 255); 31 | --hover-link-sidebar-color: rgb(170, 170, 170); 32 | --content-links: rgb(0, 132, 255); 33 | --navbar-button-background: transparent; 34 | --navbar-button-border: rgb(35, 35, 35); 35 | --navbar-icon-fill: white; 36 | } 37 | 38 | #content { 39 | display: flex; 40 | height: calc(100% - 50px); 41 | } 42 | 43 | #sidebar-menu { 44 | padding: 30px; 45 | background: var(--background-sidebar-color); 46 | border-right: 1px solid var(--border); 47 | min-width: 215px; 48 | overflow: auto; 49 | } 50 | 51 | #sidebar-menu a { 52 | color: var(--text-color); 53 | text-decoration: none; 54 | } 55 | 56 | #sidebar-menu a:hover { 57 | color: var(--hover-link-sidebar-color); 58 | } 59 | 60 | #sidebar-menu ul { 61 | list-style: none; 62 | margin: 7px 5px; 63 | padding-left: 15px; 64 | } 65 | 66 | #sidebar-menu li { 67 | margin: 5px 0px; 68 | } 69 | 70 | #sidebar-menu a.active { 71 | color: var(--active-link-sidebar-color); 72 | } 73 | 74 | main { 75 | padding-top: 50px; 76 | padding: 15px 35px; 77 | display: flex; 78 | justify-content: center; 79 | width: 100%; 80 | overflow: auto; 81 | background-color: var(--background-color); 82 | color: var(--text-color); 83 | 84 | } 85 | 86 | main>div { 87 | min-width: 450px; 88 | width: 60%; 89 | max-width: 70%; 90 | } 91 | 92 | main li { 93 | line-height: 25px; 94 | } 95 | 96 | main h1, main h2, main h3, main h4, main h5 { 97 | border-bottom: 1px solid var(--title-border); 98 | line-height: 60px; 99 | } 100 | 101 | main pre { 102 | background: var(--pre-background); 103 | border: 1px solid var(--border); 104 | padding: 8px; 105 | border-radius: 6px; 106 | color: var(--text-color); 107 | } 108 | 109 | main code::-webkit-scrollbar { 110 | width: 12px; 111 | height: 12px; 112 | } 113 | 114 | main code::-webkit-scrollbar-track { 115 | background: #f5f5f5; 116 | border-radius: 10px; 117 | } 118 | 119 | main code::-webkit-scrollbar-thumb { 120 | border-radius: 10px; 121 | background: #ccc; 122 | } 123 | 124 | main code::-webkit-scrollbar-thumb:hover { 125 | background: #999; 126 | } 127 | 128 | main code { 129 | background: transparent !important; 130 | color: var(--text-color) !important; 131 | 132 | } 133 | 134 | main a { 135 | text-decoration: none; 136 | color: var(--content-links); 137 | } 138 | 139 | main a:hover { 140 | text-decoration: underline; 141 | } 142 | 143 | main blockquote { 144 | padding: 10px 20px; 145 | margin: 5px 0px; 146 | background: var(--quote-background); 147 | border: 1px solid var(--quote-border); 148 | 149 | border-radius: 8px; 150 | color: var(--text-color); 151 | } 152 | 153 | #navbar { 154 | height: 50px; 155 | box-sizing: border-box; 156 | border-bottom: 1px solid var(--border); 157 | display: flex; 158 | align-items: center; 159 | justify-content: space-between; 160 | background: var(--background-color); 161 | color: var(--text-color); 162 | } 163 | 164 | #navbar button { 165 | border: none; 166 | background: transparent; 167 | padding: 7px; 168 | border-radius: 5px; 169 | border: 1px solid transparent; 170 | margin-left: 10px; 171 | color: var(--text-color); 172 | } 173 | 174 | #navbar button:hover { 175 | background: var(--navbar-button-background); 176 | border: 1px solid var(--navbar-button-border); 177 | } 178 | 179 | #navbar button svg { 180 | height: 15px; 181 | width: 15px; 182 | } 183 | 184 | #navbar button svg rect { 185 | fill: var(--navbar-icon-fill); 186 | } 187 | 188 | #navbar div { 189 | display: flex; 190 | height: 30px; 191 | align-items: center; 192 | } 193 | 194 | #navbar h4 { 195 | margin: 0px; 196 | padding: 10px 20px; 197 | display: inline-block; 198 | } 199 | 200 | #language { 201 | padding: 5px; 202 | border: none; 203 | border-radius: 5px; 204 | border: 1px solid var(--navbar-button-border); 205 | background: var(--navbar-button-background); 206 | color: var(--text-color); 207 | } 208 | 209 | #sidebar-toggler { 210 | display: none; 211 | } 212 | 213 | #navigators { 214 | padding: 10px; 215 | } 216 | 217 | #navigators>a { 218 | margin-bottom: 35px; 219 | } 220 | 221 | #navigators>a>button { 222 | background: transparent; 223 | border-radius: 5px; 224 | border: none; 225 | padding: 8px 12px; 226 | border: 1px solid transparent; 227 | cursor: pointer; 228 | text-decoration: none; 229 | color: var(--text-color); 230 | } 231 | 232 | #navigators>a.prev { 233 | float: left; 234 | } 235 | 236 | #navigators>a.next { 237 | float: right; 238 | color: var(--text-color); 239 | 240 | } 241 | 242 | #navigators>a>button:hover { 243 | border: 1px solid var(--navbar-button-border); 244 | } 245 | 246 | @media only screen and (max-width: 650px) { 247 | #sidebar-menu { 248 | display: none; 249 | position: fixed; 250 | top: 70px; 251 | left: 0; 252 | height: 100vh; 253 | } 254 | 255 | #sidebar-toggler { 256 | height: 100%; 257 | width: 70px; 258 | border-radius: 0px !important; 259 | } 260 | 261 | #navbar { 262 | height: 70px; 263 | } 264 | 265 | #content { 266 | display: flex; 267 | height: calc(100% - 70px); 268 | } 269 | 270 | main>div { 271 | min-width: 0px; 272 | width: 100%; 273 | max-width: 100%; 274 | } 275 | 276 | #navbar button { 277 | display: block; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /vale.ts: -------------------------------------------------------------------------------- 1 | import { basename, dirname, join, Marked, parse } from "./deps.ts"; 2 | import { getCachedAssets } from "./utils.ts"; 3 | import { 4 | CategoryData, 5 | ContentDoc, 6 | DocEntry, 7 | Metadata, 8 | ProcessResult, 9 | Sidebar, 10 | Tree, 11 | TreeFile, 12 | ValeData, 13 | } from "./types.ts"; 14 | 15 | export class ValeBuilder { 16 | static async create(projectRoot: string): Promise { 17 | const metadata: Metadata = JSON.parse( 18 | await Deno.readTextFile(join(projectRoot, "metadata.json")), 19 | ); 20 | 21 | const assets = await getCachedAssets(); 22 | 23 | return new Vale({ 24 | projectRoot, 25 | metadata, 26 | assets, 27 | }); 28 | } 29 | } 30 | 31 | export class Vale { 32 | private projectDist: string; 33 | 34 | constructor(private valeData: ValeData) { 35 | this.projectDist = join(this.valeData.projectRoot, "dist"); 36 | } 37 | 38 | private async setupDist() { 39 | // Create the dist folder 40 | await Deno.mkdir(this.projectDist, { recursive: true }); 41 | 42 | // Copy styles.css 43 | const stylesDistPath = join(this.projectDist, "styles.css"); 44 | await Deno.copyFile(this.valeData.assets.stylesPathCached, stylesDistPath); 45 | } 46 | 47 | /** 48 | * Try to find and parse a `sidebar.json` inside the folder specified in `languageFolder` 49 | */ 50 | private async getSidebarConfigForLanguage( 51 | languageFolder: string, 52 | ): Promise { 53 | try { 54 | return JSON.parse( 55 | await Deno.readTextFile(join(languageFolder, "sidebar.json")), 56 | ); 57 | } catch { 58 | return null; 59 | } 60 | } 61 | 62 | public async build() { 63 | await this.setupDist(); 64 | 65 | const result: ProcessResult[] = []; 66 | 67 | const docsFolders = await Deno.readDir(this.valeData.projectRoot); 68 | 69 | // The content reference to look up to 70 | let referenceContent: CategoryData[] = []; 71 | 72 | let langIndex = 0; 73 | 74 | // Iterate over all the languages 75 | for await (const langEntry of docsFolders) { 76 | // Ignore files and the dist folder 77 | if (langEntry.isFile || langEntry.name === "dist") continue; 78 | 79 | const languageFolder = join(this.valeData.projectRoot, langEntry.name); 80 | 81 | // Create dist folder for the language 82 | const langDist = join(this.projectDist, langEntry.name); 83 | await Deno.mkdir(langDist, { recursive: true }); 84 | 85 | // Get the sidebar configuration 86 | 87 | const sidebarConfig = await this.getSidebarConfigForLanguage( 88 | languageFolder, 89 | ); 90 | // Ignore this folder if it doesn't contain a sidebar config file 91 | if (sidebarConfig == null) continue; 92 | 93 | // Read all the categories in this language folder 94 | const categoryEntries = Deno.readDir(languageFolder); 95 | 96 | const contentTree: Tree = []; 97 | 98 | // Read every category and their entries 99 | for await (const entry of categoryEntries) { 100 | if (entry.isDirectory) { 101 | const folderPath = join(languageFolder, entry.name); 102 | const files = await lookInFolder(folderPath); 103 | const data = await getCategoryData(folderPath, files); 104 | contentTree.push(data); 105 | } 106 | } 107 | 108 | // Parse all these categories and entries 109 | const docContents: Map = new Map(); 110 | 111 | for (const category of contentTree) { 112 | if (category.entries != null) { 113 | const categoryEntry = getDocEntry(category, category); 114 | 115 | docContents.set(category.title, { 116 | entry: categoryEntry, 117 | entries: new Map(), 118 | }); 119 | 120 | const contentCategory = docContents.get(category.title); 121 | 122 | for (const treeFile of category.entries) { 123 | if (treeFile.entries == null) { 124 | const fileEntry = getDocEntry(treeFile, category); 125 | 126 | contentCategory?.entries.set(fileEntry.path, fileEntry); 127 | } 128 | } 129 | } 130 | } 131 | 132 | const orderedContent = orderSidebarCategories(sidebarConfig, docContents); 133 | const finalOrderedContent = mergeContentWithReferenceContent( 134 | referenceContent, 135 | orderedContent, 136 | ); 137 | 138 | // Mark this language content as the reference 139 | if (langEntry.name === this.valeData.metadata.reference) { 140 | referenceContent = orderedContent; 141 | } 142 | 143 | let lastEntry: DocEntry | undefined; 144 | let categoryIndex = 0; 145 | 146 | for (const { doc } of finalOrderedContent) { 147 | const prevCategoryDoc = finalOrderedContent[categoryIndex - 1]; 148 | const nextCategoryDoc = finalOrderedContent[categoryIndex + 1]; 149 | 150 | const processResult = await this.processCategory( 151 | langEntry.name, 152 | langDist, 153 | doc, 154 | finalOrderedContent, 155 | lastEntry || prevCategoryDoc?.doc?.entry, 156 | nextCategoryDoc?.doc?.entry, 157 | ); 158 | lastEntry = processResult.lastEntry; 159 | if (categoryIndex === 0) { 160 | result.push(processResult); 161 | } 162 | categoryIndex++; 163 | } 164 | 165 | // Create a index.html file for every language containing the same content as the first category in the language 166 | const indexFilePath = join(langDist, `index.html`); 167 | await Deno.writeTextFile(indexFilePath, result[langIndex].htmlCode); 168 | 169 | langIndex++; 170 | } 171 | 172 | // Create a index.html file for the website containing the same content as the first category in the first language 173 | const indexFilePath = join(this.projectDist, `index.html`); 174 | await Deno.writeTextFile(indexFilePath, result[0].htmlCode); 175 | } 176 | 177 | /** 178 | * Creates the final files of the specified category in categoryEntry 179 | */ 180 | private async processCategory( 181 | langCode: string, 182 | dist: string, 183 | { entry: categoryEntry, entries }: ContentDoc, 184 | orderedContent: CategoryData[], 185 | prevCategoryEntry?: DocEntry, 186 | nextCategoryEntry?: DocEntry, 187 | ): Promise { 188 | // Use the the next category as next page 189 | let nextCategoryEntryConfig = nextCategoryEntry; 190 | // Save last entry for the next categories 191 | let lastEntry: DocEntry | undefined; 192 | 193 | const categoryDist = join(dist, dirname(categoryEntry.path)); 194 | await Deno.mkdir(categoryDist, { recursive: true }); 195 | 196 | await Promise.all( 197 | Array.from(entries).map( 198 | async ([_title, fileEntry], fileIndex, listEntries) => { 199 | const [_prevTitle, prevEntry] = listEntries[fileIndex - 1] || [ 200 | null, 201 | categoryEntry, 202 | ]; 203 | const [_nextTitle, nextEntry] = listEntries[fileIndex + 1] || [ 204 | null, 205 | nextCategoryEntry, 206 | ]; 207 | 208 | const htmlCode = this.contentToHTML( 209 | langCode, 210 | fileEntry, 211 | orderedContent, 212 | prevEntry, 213 | nextEntry, 214 | ); 215 | const filePath = `${fileEntry.path}.html`; 216 | await Deno.writeTextFile(join(dist, filePath), htmlCode); 217 | 218 | if (fileIndex === 0) { 219 | // Use the first category entry as the next page for the category 220 | nextCategoryEntryConfig = fileEntry; 221 | } 222 | if (fileIndex == listEntries.length - 1) { 223 | lastEntry = fileEntry; 224 | } 225 | }, 226 | ), 227 | ); 228 | 229 | const htmlCode = this.contentToHTML( 230 | langCode, 231 | categoryEntry, 232 | orderedContent, 233 | prevCategoryEntry, 234 | nextCategoryEntryConfig, 235 | ); 236 | const filePath = join(dist, `${categoryEntry.path}.html`); 237 | await Deno.writeTextFile(filePath, htmlCode); 238 | return { 239 | htmlCode, 240 | path: filePath, 241 | lastEntry, 242 | }; 243 | } 244 | 245 | /** 246 | * Creates a HTML Document for the specified content in `entry` 247 | */ 248 | private contentToHTML( 249 | langCode: string, 250 | entry: DocEntry, 251 | orderedContent: CategoryData[], 252 | prevEntry?: DocEntry, 253 | nextEntry?: DocEntry, 254 | ): string { 255 | const sidebarHTML = sidebarToHTML(langCode, orderedContent, entry.title); 256 | 257 | // Create the dropdown, default option is this language specified in `langCode` 258 | const languages = this.valeData.metadata.languages.map((language) => { 259 | return ``; 262 | }); 263 | 264 | const prevButton = prevEntry 265 | ? `` 266 | : ""; 267 | 268 | const nextButton = nextEntry 269 | ? `` 270 | : ""; 271 | 272 | return ` 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | ${entry.title} | ${this.valeData.metadata.title} 281 | 282 | 283 | 284 | 294 |
295 | ${sidebarHTML} 296 |
297 |
298 | ${entry.content} 299 | 303 |
304 |
305 |
306 | 362 | 363 | 364 | `; 365 | } 366 | } 367 | 368 | function mergeContentWithReferenceContent( 369 | referenceContent: CategoryData[], 370 | content: CategoryData[], 371 | ): CategoryData[] { 372 | if (referenceContent.length == 0) return content; 373 | const mergedContent = [...referenceContent]; 374 | content.forEach((category, i) => { 375 | category.doc.entries.forEach((entry) => { 376 | mergedContent[i].doc.entries.set(entry.path, entry); 377 | }); 378 | mergedContent[i].doc.entry = category.doc.entry; 379 | }); 380 | return mergedContent; 381 | } 382 | 383 | async function getDocsFromFile(filePath: string): Promise { 384 | const content = await Deno.readTextFile(filePath); 385 | const markup = Marked.parse(content); 386 | return { 387 | path: basename(filePath), 388 | title: markup.meta.title, 389 | content: markup.content, 390 | }; 391 | } 392 | 393 | async function lookInFolder(folder: string): Promise> { 394 | const entries = Deno.readDir(folder); 395 | const treeFiles = []; 396 | for await (const entry of entries) { 397 | if (entry.isFile && entry.name != "__category.md") { 398 | const treeFile = await getDocsFromFile(join(folder, entry.name)); 399 | treeFiles.push(treeFile); 400 | } 401 | } 402 | return treeFiles; 403 | } 404 | 405 | async function getCategoryData( 406 | folderPath: string, 407 | entries: Array, 408 | ): Promise { 409 | const dataFilePath = join(folderPath, "__category.md"); 410 | try { 411 | const treeFile = await getDocsFromFile(dataFilePath); 412 | return { 413 | ...treeFile, 414 | path: basename(folderPath), 415 | entries, 416 | }; 417 | } catch { 418 | return { 419 | title: basename(folderPath), 420 | path: basename(folderPath), 421 | entries, 422 | }; 423 | } 424 | } 425 | 426 | function orderSidebarCategories( 427 | sidebarConfig: Sidebar, 428 | docContents: Map, 429 | ): CategoryData[] { 430 | return Object.entries(sidebarConfig).map(([name, entries]) => { 431 | const doc = docContents.get(name); 432 | if (doc != null) { 433 | // Order the entries by the sidebar or der 434 | const filteredEntries = new Map(); 435 | entries.forEach((entryTitle) => { 436 | const res = doc.entries.get(entryTitle); 437 | if (res != null) { 438 | filteredEntries.set(entryTitle, res); 439 | } 440 | }); 441 | 442 | return { 443 | name, 444 | doc, 445 | }; 446 | } else { 447 | throw Error(`Category '${name}' is not found`); 448 | } 449 | }); 450 | } 451 | 452 | function sidebarToHTML( 453 | langCode: string, 454 | categories: CategoryData[], 455 | entryActiveTitle: string, 456 | ): string { 457 | const links = categories 458 | .map(({ name, doc: { entry: categoryEntry, entries } }) => { 459 | const isCategoryActive = entryActiveTitle == categoryEntry.title; 460 | const categoryClass = isCategoryActive ? "active" : ""; 461 | const categoryEntries = Array.from(entries) 462 | .map(([_, entry]) => { 463 | const isNotCategoryDoc = categoryEntry.path !== entry.path; 464 | const isActive = entryActiveTitle == entry.title; 465 | const entryClass = isActive ? "active" : ""; 466 | return ( 467 | isNotCategoryDoc && 468 | `
  • ${entry.title}
  • ` 469 | ); 470 | }) 471 | .filter(Boolean) 472 | .join(""); 473 | return ` 474 |
    475 | ${name} 476 |
    477 |
      478 | ${categoryEntries} 479 |
    480 |
    481 |
    482 | `; 483 | }) 484 | .join(""); 485 | return ` 486 | 489 | `; 490 | } 491 | 492 | function getDocEntry(treeFile: TreeFile, categoryTree: TreeFile): DocEntry { 493 | return { 494 | content: treeFile.content || "", 495 | path: `${categoryTree.path}/${parse(treeFile.path).name}`, 496 | title: treeFile.title, 497 | }; 498 | } 499 | --------------------------------------------------------------------------------