From 9da599a304c448338bb5f2a6b92f8022477b37cb Mon Sep 17 00:00:00 2001 From: Ajani Bilby <11359344+AjaniBilby@users.noreply.github.com> Date: Sun, 14 Apr 2024 11:45:55 +1000 Subject: [PATCH] init --- .gitattributes | 2 + .github/workflows/static.yml | 51 +++++++++++ .gitignore | 3 + builder/helper.ts | 23 +++++ builder/index.ts | 36 ++++++++ builder/page.ts | 163 ++++++++++++++++++++++++++++++++++ builder/toolbar.ts | 33 +++++++ client/index.ts | 100 +++++++++++++++++++++ docs/a.md | 1 + docs/b.md | 1 + docs/c.md | 1 + docs/wasix/sock_bind.md | 13 +++ docs/wasix/sock_open.md | 10 +++ docs/web-assembly/type/f32.md | 3 + docs/web-assembly/type/f64.md | 3 + docs/web-assembly/type/i32.md | 3 + docs/web-assembly/type/i64.md | 3 + package.json | 22 +++++ public/main.css | 138 ++++++++++++++++++++++++++++ readme.md | 3 + server.js | 51 +++++++++++ tsconfig.json | 25 ++++++ 22 files changed, 688 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/static.yml create mode 100644 .gitignore create mode 100644 builder/helper.ts create mode 100644 builder/index.ts create mode 100644 builder/page.ts create mode 100644 builder/toolbar.ts create mode 100644 client/index.ts create mode 100644 docs/a.md create mode 100644 docs/b.md create mode 100644 docs/c.md create mode 100644 docs/wasix/sock_bind.md create mode 100644 docs/wasix/sock_open.md create mode 100644 docs/web-assembly/type/f32.md create mode 100644 docs/web-assembly/type/f64.md create mode 100644 docs/web-assembly/type/i32.md create mode 100644 docs/web-assembly/type/i64.md create mode 100644 package.json create mode 100644 public/main.css create mode 100644 readme.md create mode 100644 server.js create mode 100644 tsconfig.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..40d4ba8 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,51 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: '20.x' # replace with your Node.js version + - name: Install Dependencies + run: npm ci + - name: Validating Project + run: npm run build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: './public' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76e2d7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/node_modules/* +/public/* +!/public/main.css \ No newline at end of file diff --git a/builder/helper.ts b/builder/helper.ts new file mode 100644 index 0000000..b0894d4 --- /dev/null +++ b/builder/helper.ts @@ -0,0 +1,23 @@ +export function Path2Name(path: string) { + let startIndex = path.lastIndexOf("/"); + if (startIndex !== 0 || path[0] !== "/") startIndex++; + + let endIndex = path.indexOf(".", startIndex); + if (endIndex === -1) endIndex = path.length; + + const filename = path + .slice(startIndex, endIndex) + .replaceAll("-", " "); + + return filename; +} + +export function Reroute(path: string) { + let endIndex = path.lastIndexOf("."); + if (endIndex === 0) endIndex = path.length; + + return `/${path.startsWith("./docs") + ? path.slice(7, endIndex) + : path + }`; +} \ No newline at end of file diff --git a/builder/index.ts b/builder/index.ts new file mode 100644 index 0000000..797d719 --- /dev/null +++ b/builder/index.ts @@ -0,0 +1,36 @@ +import { CreateFolderPage, CreatePage } from "./page"; +import { readdir, stat, mkdir } from "fs/promises"; +import { CreateToolbar } from "./toolbar"; + +console.info("Building Docs..."); + + +async function GroupPaths (paths: string[]) { + const stats = await Promise.all(paths.map( x => stat(x))); + + return { + folders: paths.filter((_, i) => !stats[i].isFile()), + files: paths.filter((_, i) => stats[i].isFile()) + } +} + +async function BuildDir(path: string) { + const paths = await readdir(path); + + + for (let i=0; i CreatePage(toolbar, f))); + await CreateFolderPage(toolbar, path); + + await Promise.all(folders.map(BuildDir)); +} + + +BuildDir("./docs"); \ No newline at end of file diff --git a/builder/page.ts b/builder/page.ts new file mode 100644 index 0000000..a9998b9 --- /dev/null +++ b/builder/page.ts @@ -0,0 +1,163 @@ +import { readFile, writeFile } from "fs/promises"; +import { Path2Name, Reroute } from "./helper"; + +export async function CreatePage(toolbar: string, path: string) { + const extIndex = path.lastIndexOf("."); + const ext = path.slice(extIndex); + if (ext != ".md") return; + + const data = await readFile(path, "utf8"); + const { html, type } = RenderPage(path, data); + + const document = ` + + + ${Path2Name(path)} + + + + + ${toolbar} +
+
${html}
+
+ +`; + + writeFile(`./public/${path.slice(7, extIndex)}.html`, document); + + return type; +} + + +export async function CreateFolderPage(toolbar: string, path: string) { + const html = ` + + + ${Path2Name(path)} + + + + + ${toolbar} +
+ +`; + + writeFile(`./public/${path.slice(7) || "index"}.html`, html); +} + + + +function RenderPage(path: string, data: string) { + const pathFrag = path.split("/"); + const { summary, details } = IngestPage(data); + + const html = `` + + `${pathFrag.slice(2, -1).join("/")}` + + `
` + + `
` + + `${Path2Name(pathFrag[pathFrag.length-1])} ` + + (summary.type == "structure" ? "{" : "(") + + `
` + + summary.params.map(p => `
` + +`${p.name}` + +`: ${p.type}` + + `${p.description}` + +`
`).join("") + + `
` + + (summary.type == "function" + ? ("): " + + `
${summary.returns.map(p => `
` + +`${p.name}` + +`: ${p.type}` + + ` ${p.description}` + +`
`).join("")}
` + ) : "}") + + `
` + + `
` + + `
Close
` + + `
${summary.text}
` + + `
` + + `
${details}
`; + + return { html, type: summary.type }; +} + + +type TypeDefMap = Map; +function IngestPage(data: string) { + const [primary, ...secondary] = data.split(`\n---\n`); + + let definitions = new Map(); + let type = "function"; + let summary = ""; + let params = new Array>(); + let returns = new Array>(); + for (const line of primary.split("\n")) { + if (line[0] !== "@") { + summary += "\n" + line; + continue; + } + + const [key, rest] = SplitString(line, " "); + switch (key) { + case "@param": { + params.push(ProcessSignatureLine(definitions, rest)); + break; + } + case "@return" : { + returns.push(ProcessSignatureLine(definitions, rest)); + break; + } + case "@typedef" : { + IngestTypedef(definitions, rest); + break; + } + case "@function": { + type = "function"; + break; + } + case "@structure": { + type = "structure"; + break; + } + default: + console.error(`Unknown key ${key}`); + summary += `\n${key} ${rest}`; + } + } + + return { + summary: { + type, + params, returns, + text: summary + }, + details: secondary + } +} + + +function SplitString(str: string, pivot: string) { + let index = str.indexOf(pivot); + if (index === -1) index = str.length; + + return [ str.slice(0, index), str.slice(index+pivot.length) ]; +} + +function ProcessSignatureLine(ctx: TypeDefMap, line: string) { + const [ name, line1 ] = SplitString(line, ": "); + const [ typeStr, description ] = SplitString(line1, " - "); + + const type = ctx.has(typeStr) + ? `${typeStr}` + : `${typeStr}`; + + return { name, type, description } +} + +function IngestTypedef(ctx: TypeDefMap, line: string) { + const [ type, href ] = SplitString(line, " "); + ctx.set(type, href); +} \ No newline at end of file diff --git a/builder/toolbar.ts b/builder/toolbar.ts new file mode 100644 index 0000000..20de06d --- /dev/null +++ b/builder/toolbar.ts @@ -0,0 +1,33 @@ +import { Path2Name, Reroute } from "./helper"; + +export function CreateToolbar(href: string, folders: string[], files: string[]) { + + const pathFrags = href.split("/").slice(2); // ignore ./docs + const parents = pathFrags.map((x, i) => i == 0 + ? { + name: Path2Name(x), + path: `/${x}`, + } : { + name: Path2Name(x), + path: `/${pathFrags[i-1]}/${x}`, + } + ); + + return `
+ ${href !== "./docs" ? ` Index` : ""} + ${parents.map(x => + ` + ${x.name} + `).join("\n\t")} + + ${folders.map(x => + ` + ${Path2Name(x)} + `).join("\n\t")} + + ${files.map(x => + ` + ${Path2Name(x)} + `).join("\n\t")} +
`; +} \ No newline at end of file diff --git a/client/index.ts b/client/index.ts new file mode 100644 index 0000000..1483ef7 --- /dev/null +++ b/client/index.ts @@ -0,0 +1,100 @@ +const parser = new DOMParser(); + +function AnyClick(ev: MouseEvent) { + if (!(ev.target instanceof HTMLAnchorElement)) return; + + const href = ev.target.getAttribute("href") || ""; + if (!href) return; + + if (ev.target.hasAttribute("entry")) { + ev.stopImmediatePropagation(); + ev.stopPropagation(); + ev.preventDefault(); + + OpenEntry(href); + return; + } + + if (ev.target.hasAttribute("folder")) { + ev.stopImmediatePropagation(); + ev.stopPropagation(); + ev.preventDefault(); + + OpenFolder(href); + return; + } +} + +async function OpenEntry(href: string) { + const dashboard = document.querySelector(".dashboard"); + if (!dashboard) throw new Error("Missing dashboard element"); + + const existing = FindOpenEntry(href); + if (existing) existing.remove(); + + const req = await fetch(href); + if (!req.ok) throw new Error(`Failed to load ${href}`); + + const doc = parser.parseFromString(await req.text(), "text/html"); + const entry = doc.querySelector(".entry"); + + if (!entry) throw new Error("Route is missing div.entry"); + + dashboard.insertBefore(entry, dashboard.firstChild); + dashboard.scrollTo({top: 0}); + + const title = doc.querySelector("title")?.innerText || document.title; + history.replaceState({}, title, href); + document.title = title; + + Save(); +} + +function FindOpenEntry(href: string) { + for (const div of document.body.querySelectorAll(".entry")) { + if (div.getAttribute("data-src") == href) return div; + } + + return null; +} + + +async function OpenFolder(href: string) { + const current = document.querySelector(".toolbar"); + if (!current) throw new Error("Missing dashboard element"); + + const req = await fetch(href); + if (!req.ok) throw new Error(`Failed to load ${href}`); + + const doc = parser.parseFromString(await req.text(), "text/html"); + const toolbar = doc.querySelector(".toolbar"); + + if (!toolbar) throw new Error("Route is missing div.toolbar"); + + current.replaceWith(toolbar); + + const title = doc.querySelector("title")?.innerText || document.title; + history.replaceState({}, title, href); + document.title = title; +} + + +function Save() { + const pages = [ ...document.body.querySelectorAll(".entry") ] + .map(x => x.getAttribute("data-src")) + .filter(x => x); + + localStorage.setItem("open-pages", pages.join("\n")); +} + + +function Startup() { + document.body.addEventListener("click", AnyClick); + + const pages = (localStorage.getItem('open-pages') || "").split("\n"); + for (const page of pages) { + OpenEntry(page); + } +} + +window.addEventListener("load", Startup); \ No newline at end of file diff --git a/docs/a.md b/docs/a.md new file mode 100644 index 0000000..8e7ac4e --- /dev/null +++ b/docs/a.md @@ -0,0 +1 @@ +Content for A \ No newline at end of file diff --git a/docs/b.md b/docs/b.md new file mode 100644 index 0000000..b413473 --- /dev/null +++ b/docs/b.md @@ -0,0 +1 @@ +Content for B \ No newline at end of file diff --git a/docs/c.md b/docs/c.md new file mode 100644 index 0000000..7dc4c78 --- /dev/null +++ b/docs/c.md @@ -0,0 +1 @@ +Content for C \ No newline at end of file diff --git a/docs/wasix/sock_bind.md b/docs/wasix/sock_bind.md new file mode 100644 index 0000000..9129d7e --- /dev/null +++ b/docs/wasix/sock_bind.md @@ -0,0 +1,13 @@ +@function +@typedef i32 /web-assembly/type/i32 +@param file_descriptor: i32 +@param address: i32 +@return error: i32 +--- +The sock_bind() function is used to bind a socket to a specific address. This function is similar to the bind function in POSIX, which is used to associate a specific address with a socket. + +In POSIX, the bind function is typically used with sockets that use the PF_INET address family, which corresponds to IPv4 addresses. Similarly, in the WASI context, sock_bind() uses the PF_INET address family to bind the socket. + +Binding a socket to a specific address allows the socket to receive incoming connections or send data from the specified address. It restricts the socket to operate on a specific network interface and port. + +[See](https://wasix.org/docs/api-reference/wasix/sock_bind) \ No newline at end of file diff --git a/docs/wasix/sock_open.md b/docs/wasix/sock_open.md new file mode 100644 index 0000000..6f8f748 --- /dev/null +++ b/docs/wasix/sock_open.md @@ -0,0 +1,10 @@ +@function +@typedef i32 /web-assembly/type/i32 +@param address_family: i32 - some extra text here +@param sock_type: i32 +@param sock_proto: i32 +@return error: i32 +--- +The `sock_open()` function creates an endpoint for communication and returns a file descriptor that refers to that endpoint. The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currently open for the process. + +[See](https://wasix.org/docs/api-reference/wasix/sock_open) \ No newline at end of file diff --git a/docs/web-assembly/type/f32.md b/docs/web-assembly/type/f32.md new file mode 100644 index 0000000..787bd01 --- /dev/null +++ b/docs/web-assembly/type/f32.md @@ -0,0 +1,3 @@ +@structure +--- +4 bytes wide \ No newline at end of file diff --git a/docs/web-assembly/type/f64.md b/docs/web-assembly/type/f64.md new file mode 100644 index 0000000..050e5ea --- /dev/null +++ b/docs/web-assembly/type/f64.md @@ -0,0 +1,3 @@ +@structure +--- +8 bytes wide \ No newline at end of file diff --git a/docs/web-assembly/type/i32.md b/docs/web-assembly/type/i32.md new file mode 100644 index 0000000..787bd01 --- /dev/null +++ b/docs/web-assembly/type/i32.md @@ -0,0 +1,3 @@ +@structure +--- +4 bytes wide \ No newline at end of file diff --git a/docs/web-assembly/type/i64.md b/docs/web-assembly/type/i64.md new file mode 100644 index 0000000..050e5ea --- /dev/null +++ b/docs/web-assembly/type/i64.md @@ -0,0 +1,3 @@ +@structure +--- +8 bytes wide \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..ad14867 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "calculator", + "private": true, + "sideEffects": false, + "scripts": { + "build": "run-s build:*", + "dev": "run-p dev:*", + "dev:server": "node server.js", + "dev:docs": "nodemon --watch ./docs --ext md --exec npm run build:docs", + "dev:client": "npx esbuild --platform=node --format=cjs ./client/index.ts --outdir=public --bundle --watch", + "build:client": "npx esbuild --platform=node --format=cjs ./client/index.ts --outdir=public --bundle", + "build:docs": "ts-node ./builder/index.ts" + }, + "engines": {}, + "devDependencies": { + "@types/node": "^20.12.7", + "esbuild": "^0.20.1", + "express": "^4.18.3", + "nodemon": "^3.1.0", + "npm-run-all": "^4.1.5" + } +} diff --git a/public/main.css b/public/main.css new file mode 100644 index 0000000..de04728 --- /dev/null +++ b/public/main.css @@ -0,0 +1,138 @@ +@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=swap'); + +body { + margin: 0; + + display: grid; + grid-template-columns: auto 1fr; + gap: 20px; + + background-color: #272822; + font-family: Fira Code; + color: #f8f8f2; +} + +a, a:visited { + color: inherit; +} + +.toolbar { + display: flex; + flex-direction: column; + + padding: 10px 0 10px 10px; +} + +.toolbar a { + font-family: Fira Code; + text-decoration: none; + + margin-top: 5px; + margin-left: 1.5em; +} + +.toolbar a[folder][parent] { + margin-top: 0; + margin-left: 0; +} +.toolbar a[folder][parent]::before { + content: "↳" +} + +.toolbar a[folder] { + margin-top: 0px; + margin-left: 0; + padding: 0 5px; +} + +.toolbar a[folder]::before { + content: "📁" +} + +a[folder][parent] + a[folder]:not([parent]) { + margin-top: 5px; +} + +.dashboard { + margin: 10px 0; + + display: flex; + flex-direction: column; + gap: 30px; +} + + +.entry { + position: relative; +} + +.entry .close { + position: absolute; + top: 10px; + right: 10px; + + cursor: pointer; + user-select: none; +} + +.entry .context { + font-style: italic; + display: inline-block; +} + +details.entry .signature { + display: inline-block; + padding: 5px 10px; + margin: 5px 0px; + + font-family: Fira Code; + font-size: 0.9em; + color: #f8f8f2; + + background-color: #3e3d32; + border-radius: 5px; +} + +details.entry .cluster { + display: inline-flex; + flex-direction: row; + gap: 0.6em; +} + +details.entry .cluster .comment { + display: none; +} + +details.entry[open] .cluster { + display: flex; + flex-direction: column; + gap: 0; +} +details.entry[open] .cluster .indent { + margin-left: 1em; +} + +details.entry[open] .cluster .comment { + display: inline-block; +} + + +.name { + color: #a6e22e; + font-style: italic; +} + +.argument { + color: #fd971f; + font-style: italic; +} + +.type { + color: #66d9ef !important; + font-style: italic; +} + +.comment { + color: #75715e; + font-style: italic; +} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..65db41e --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +This is a prototype for future automated documentation generation within the salient compiler. + +For now I am just putting documentation which is helpful to the compiler's development within this platform to test if the idea is worth pursuing in the main project \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..d9adfed --- /dev/null +++ b/server.js @@ -0,0 +1,51 @@ +const express = require("express"); +const path = require("path"); +const fs = require("fs"); + +const app = express(); + +app.use((req, res, next) => { + // helpful headers: + res.set("Strict-Transport-Security", `max-age=${60 * 60 * 24 * 365 * 100}`); + res.set("X-Frame-Options", "SAMEORIGIN"); + res.set("Referrer-Policy", "origin"); + res.set("Cache-Control", "no-cache"); + + // /clean-urls/ -> /clean-urls + if (req.path.endsWith("/") && req.path.length > 1) { + const query = req.url.slice(req.path.length); + const safepath = req.path.slice(0, -1); + + res.redirect(301, safepath + query); + return; + } + next(); +}); + +// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header +app.disable("x-powered-by"); + +// Custom middleware to resolve .html extensions +app.use((req, res, next) => { + const filePath = path.join(__dirname, "public", `${req.path}.html`); + + // Check if .html version of the requested file exists + fs.access(filePath, fs.constants.F_OK, (err) => { + if (!err) { + // Serve the .html file if it exists + res.sendFile(filePath); + } else { + // Continue with the regular static middleware if .html file doesn't exist + next(); + } + }); +}); + +// Everything else (like favicon.ico) is cached for an hour. You may want to be +// more aggressive with this caching. +app.use(express.static("public")); + +const port = process.env.PORT || 3000; +app.listen(port, () => { + console.info(`✅ app ready: http://localhost:${port}`); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c762ea6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "exclude": [], + "include": ["global.d.ts", "**/*.ts"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2021"], + "isolatedModules": true, + "esModuleInterop": true, + "module": "CommonJS", + "moduleResolution": "node", + "resolveJsonModule": true, + "target": "ES2021", + "strict": true, + "useUnknownInCatchVariables": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + }, + "skipLibCheck": true, + + // Esbuild takes care of building everything in `npm run build`. + "noEmit": true + } +} \ No newline at end of file