diff --git a/.prettierrc.mjs b/.prettierrc.mjs new file mode 100644 index 0000000..4f88aa5 --- /dev/null +++ b/.prettierrc.mjs @@ -0,0 +1,13 @@ +/** @type {import("prettier").Config} */ +export default { + plugins: ["prettier-plugin-astro"], + overrides: [ + { + files: "*.astro", + options: { + parser: "astro", + }, + }, + ], + endOfLine: "lf", + }; \ No newline at end of file diff --git a/astro.config.mjs b/astro.config.mjs index 38237d2..6f6ccff 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,11 +1,31 @@ import { readFileSync } from "node:fs"; import { defineConfig } from "astro/config"; import starlight from "@astrojs/starlight"; +import { apiModules } from "./docs/utils"; const rescriptTM = JSON.parse( readFileSync("./docs/assets/rescript.tmLanguage.json", "utf-8"), ); +const apiSidebarItems = apiModules.map(({ moduleName, link, items }) => { + const nestedItems = Object.values(items).map(({ moduleName, link }) => ({ + label: moduleName, + link + })); + + return ({ + label: moduleName, + collapsed: true, + items: [ + { + label: `Overview`, + link + }, + ...nestedItems + ] + }); +}); + export default defineConfig({ srcDir: "docs", publicDir: "docs/public", @@ -18,24 +38,36 @@ export default defineConfig({ src: "./docs/assets/rescript-logo.svg", }, social: { - github: 'https://github.com/rescript-lang/experimental-rescript-webapi', + github: "https://github.com/rescript-lang/experimental-rescript-webapi", }, editLink: { - baseUrl: 'https://github.com/rescript-lang/experimental-rescript-webapi/edit/main/', + baseUrl: + "https://github.com/rescript-lang/experimental-rescript-webapi/edit/main/", }, sidebar: [ { - slug: '', + slug: "", + }, + { + slug: "design-philosophy", }, { - slug: 'design-philosophy', + slug: "project-status", }, { - slug: 'project-status', + label: "Contributing", + autogenerate: { directory: "contributing" }, }, { - label: 'Contributing', - autogenerate: { directory: 'contributing' }, + label: "API Documentation", + collapsed: true, + items: [ + { + label: "Overview", + link: "apidocs", + }, + ...apiSidebarItems, + ], }, ], customCss: ["./docs/styles/fonts.css", "./docs/styles/theme.css"], diff --git a/docs/components/record.astro b/docs/components/record.astro new file mode 100644 index 0000000..366e3a7 --- /dev/null +++ b/docs/components/record.astro @@ -0,0 +1,48 @@ +--- +import { micromark } from "micromark"; +import Signature from "./signature.astro"; +const { name, items, typesInOwnModule } = Astro.props; +--- + +

Record fields

+{ + items.map((item) => { + const documentation = + item.docstrings && micromark(item.docstrings.join("\n")); + + return ( +
+
{item.name}
+
+ +
+ {documentation &&
} +
+ ); + }) +} + + diff --git a/docs/components/signature.astro b/docs/components/signature.astro new file mode 100644 index 0000000..e927a58 --- /dev/null +++ b/docs/components/signature.astro @@ -0,0 +1,49 @@ +--- +import SignatureItem from "./signatureItem.astro"; + + +function tokenize(input) { + // Split by special characters while keeping them + const regex = /([<>,])/g; + return input + .split(regex) + .map((token) => token.trim()) + .filter(Boolean); +} + +function parseTokens(tokens) { + let index = 0; + + const parseNode = () => { + const path = tokens[index++]; // Read the current token and increment index + let genericTypeParameters = []; + + if (tokens[index] === "<") { + // Check for generics + index++; // Consume "<" + while (tokens[index] !== ">") { + genericTypeParameters.push(parseNode()); + if (tokens[index] === ",") index++; // Consume "," + } + index++; // Consume ">" + } + + return { path, genericTypeParameters }; + }; + + return parseNode(); +} + +function parse(input) { + const tokens = tokenize(input); + return parseTokens(tokens); +} + + +const { signature, typesInOwnModule } = Astro.props; +const tree = parse(signature); +--- + +
+ {()} +
diff --git a/docs/components/signatureItem.astro b/docs/components/signatureItem.astro new file mode 100644 index 0000000..1dc3157 --- /dev/null +++ b/docs/components/signatureItem.astro @@ -0,0 +1,38 @@ +--- +import { createAPIModuleLink } from "../utils.js"; +const { item, typesInOwnModule } = Astro.props; + +let link; +if (typesInOwnModule && typesInOwnModule.has(item.path)) { + link = `#${item.path}`; +} + +if (item.path.startsWith("WebAPI.")) { + const paths = item.path.split("."); + if (paths.length === 3) { + link = `${import.meta.env.BASE_URL}/${createAPIModuleLink(paths[1])}#${paths[2]}`; + } +} + +const genericTypeParameters = item.genericTypeParameters || []; +--- + + + {link ? {item.path} : item.path}{ + genericTypeParameters.length > 0 && ( + <> + {"<"} + {genericTypeParameters.map((subItem) => ( + + ))} + {">"} + + ) + } + + diff --git a/docs/components/value.astro b/docs/components/value.astro new file mode 100644 index 0000000..66e9faa --- /dev/null +++ b/docs/components/value.astro @@ -0,0 +1,27 @@ +--- +import SignatureItem from "./signatureItem.astro"; +const { parameters, returnType } = Astro.props; +--- + +
+

Parameters

+ { + parameters.map((p) => ( + + )) + } +

Return type

+ +
+ diff --git a/docs/pages/apidocs/[API].astro b/docs/pages/apidocs/[API].astro new file mode 100644 index 0000000..aa8d9b7 --- /dev/null +++ b/docs/pages/apidocs/[API].astro @@ -0,0 +1,114 @@ +--- +import * as path from "node:path"; +import { existsSync } from "fs"; +import { apiModules, getDoc, createTypeModuleLink } from "../../utils"; +import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro"; +import { Code } from "@astrojs/starlight/components"; +import { micromark } from "micromark"; +import Record from "../../components/record.astro"; + +export async function getStaticPaths() { + return apiModules.map((apiModule) => { + return { + params: { + API: apiModule.apiRouteParameter, + }, + props: apiModule, + }; + }); +} + +function showRecord(details) { + return details && details.kind === "record" && details.items.length > 0; +} + +function getModuleFileName(typeName) { + return `${typeName[0].toUpperCase()}${typeName.slice(1)}`; +} + +function showModule(typeName, filePath) { + const moduleFileName = `${getModuleFileName(typeName)}.res`; + const potentialPath = path.join(filePath.replace(".res", ""), moduleFileName); + return existsSync(potentialPath); +} + +const { moduleName, filePath, link } = Astro.props; + +const docInfo = await getDoc(filePath); + +const types = docInfo.items + .filter((item) => item.kind === "type") + .sort((a, b) => a.name.localeCompare(b.name)) + .map((type) => { + const documentation = + type.docstrings && micromark(type.docstrings.join("\n")); + return { + name: type.name, + documentation, + signature: type.signature, + detail: type.detail, + }; + }); + +const typesInOwnModule = new Set(types.map((t) => t.name)); + +const typeHeadings = types.map((type) => ({ + depth: 3, + slug: type.name, + text: type.name, +})); + +const frontmatter = { + title: moduleName, +}; + +const headings = [ + { + depth: 2, + slug: "types", + text: "Types", + }, + ...typeHeadings, +]; +--- + + +
+

Types

+ { + types.map((type) => ( +
+

{type.name}

+
+ + {showRecord(type.detail) ? ( + + ) : null} + {showModule(type.name, filePath) && ( + <> +

Module

+

+ There are methods and helpers defined in{" "} + + {getModuleFileName(type.name)} + + . +

+ + )} +
+ )) + } +
+ + diff --git a/docs/pages/apidocs/[API]/[Module].astro b/docs/pages/apidocs/[API]/[Module].astro new file mode 100644 index 0000000..e6a5590 --- /dev/null +++ b/docs/pages/apidocs/[API]/[Module].astro @@ -0,0 +1,86 @@ +--- +import { apiModules, getDoc } from "../../../utils"; +import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro"; +import { Code } from "@astrojs/starlight/components"; +import { micromark } from "micromark"; +import Value from "../../../components/value.astro"; + +export async function getStaticPaths() { + return apiModules.flatMap((apiModule) => { + return Object.values(apiModule.items).map((typeModule) => { + return { + params: { + API: apiModule.apiRouteParameter, + Module: typeModule.apiRouteParameter, + }, + props: { parentModule: apiModule, currentModule: typeModule }, + }; + }); + }); +} + +function showValue(detail) { + return ( + detail?.kind === "signature" && detail?.details?.parameters?.length > 0 + ); +} + +const { filePath, moduleName } = Astro.props.currentModule; +const docInfo = await getDoc(filePath); + +const values = docInfo.items + .filter((item) => item.kind === "value") + .sort((a, b) => a.name.localeCompare(b.name)) + .map((value) => { + const documentation = + value.docstrings && micromark(value.docstrings.join("\n")); + return { + name: value.name, + documentation, + signature: value.signature, + detail: value.detail, + }; + }); + +const valueHeadings = values.map((value) => ({ + depth: 3, + slug: value.name, + text: value.name, +})); + +const frontmatter = { + title: moduleName, +}; + +const headings = [ + { + depth: 2, + slug: "values", + text: "Values", + }, + ...valueHeadings, +]; +--- + + +

Values

+ { + values.map((value) => ( +
+

{value.name}

+
+ + {showValue(value.detail) && } +
+ )) + } +
+{JSON.stringify(docInfo, null, 4)}
+
+ + diff --git a/docs/pages/apidocs/index.astro b/docs/pages/apidocs/index.astro new file mode 100644 index 0000000..f2bfe5d --- /dev/null +++ b/docs/pages/apidocs/index.astro @@ -0,0 +1,30 @@ +--- +import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro"; +import { apiModules } from "../../utils"; + +const frontmatter = { + title: "API Documentation", +}; + +const headings = [ + { + depth: 2, + slug: "web-apis", + text: "Web APIs", + }, +]; + +--- + + +

Web APIs

+
    + { + apiModules.map(({ moduleName, link }) => ( +
  • + {moduleName} +
  • + )) + } +
+
diff --git a/docs/styles/theme.css b/docs/styles/theme.css index cc9f342..8915d1f 100644 --- a/docs/styles/theme.css +++ b/docs/styles/theme.css @@ -1,6 +1,7 @@ :root, ::backdrop { --sl-font: "Inter", "Roboto Mono"; + --sl-sidebar-width: 23rem; } /* @@ -8,6 +9,12 @@ https://github.com/withastro/starlight/blob/main/packages/starlight/style/props. https://github.com/rescript-lang/rescript-lang.org/blob/4e4f9520f6fc7a0376db82a0a6db52211e8a8187/tailwind.config.mjs#L13 */ +@media (prefers-reduced-motion: no-preference) { + html { + scroll-behavior: smooth; /* Enables smooth scrolling for users without motion sensitivity */ + } +} + :root[data-theme="light"], [data-theme="light"] ::backdrop { --sl-color-accent-low: #4a0f14; @@ -21,7 +28,7 @@ https://github.com/rescript-lang/rescript-lang.org/blob/4e4f9520f6fc7a0376db82a0 } } -body { +body, #starlight__sidebar { --scrollbar-track-background: #222222; --scrollbar-track-border: #4a4a4a; --scrollbar-thumb-background: #686868; diff --git a/docs/utils.js b/docs/utils.js new file mode 100644 index 0000000..a34e781 --- /dev/null +++ b/docs/utils.js @@ -0,0 +1,87 @@ +import * as path from "node:path"; +import { exec } from "node:child_process"; +import { promisify } from "node:util"; +import { readdirSync, existsSync } from "fs"; + +const execAsync = promisify(exec); + +function toKebabCase(input) { + return input + .replace(/([a-z])([A-Z])/g, "$1-$2") // Insert dash between lowercase and uppercase + .replace(/[\s_]+/g, "-") // Replace spaces or underscores with dash + .toLowerCase(); // Convert to lowercase +} + +export function createAPIModuleLink(moduleName) { + // This function is called before import.meta.env.BASE_URL is set. + // So, I'm hardcoding the path here. + return `apidocs/${toKebabCase(moduleName)}`; +} + +export function createTypeModuleLink(parentModuleLink, typeName) { + return `${parentModuleLink}/${toKebabCase(typeName)}`; +} + +function mapTypeModules(parentModuleLink, file) { + const folder = file.replace(".res", ""); + + if (!existsSync(folder)) { + return []; + } + + const files = readdirSync(folder); + return files + .filter((f) => f.endsWith(".res")) + .map((file) => { + const filePath = path.join(folder, file); + + const moduleName = file + .replace("$", "") + .replace(folder, "") + .replace(".res", ""); + const apiRouteParameter = toKebabCase(moduleName); + const link = createTypeModuleLink(parentModuleLink, moduleName); + const typeName = moduleName[0].toLocaleLowerCase() + moduleName.slice(1); + + return [ + typeName, + { + filePath, + moduleName, + link, + apiRouteParameter, + }, + ]; + }); +} + +function mapRescriptFile(srcDir, file) { + const moduleName = path.basename(file, ".res").replace("$", ""); + const filePath = path.join(srcDir, file); + const link = createAPIModuleLink(moduleName); + const items = Object.fromEntries(mapTypeModules(link, filePath)); + + return { + filePath, + moduleName, + link, + apiRouteParameter: toKebabCase(moduleName), + items, + }; +} + +const srcDir = path.resolve(process.cwd(), "src"); +export const apiModules = readdirSync(srcDir).filter(f => f.endsWith(".res")).map(r => mapRescriptFile(srcDir, r)); + +export async function getDoc(absoluteFilePath) { + const { stdout, stderr } = await execAsync( + `rescript-tools doc ${absoluteFilePath}`, + { + maxBuffer: 1024 * 1024 * 10, // Increase buffer to 10 MB + }, + ); + if (stderr) { + throw new Error(stderr); + } + return JSON.parse(stdout); +} diff --git a/package-lock.json b/package-lock.json index e7a736b..cc11b2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,9 @@ "devDependencies": { "@astrojs/starlight": "^0.29.0", "astro": "^4.16.10", + "micromark": "^4.0.1", "prettier": "^3.3.3", + "prettier-plugin-astro": "^0.14.1", "sharp": "^0.33.5" } }, @@ -4619,9 +4621,9 @@ } }, "node_modules/micromark": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", - "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz", + "integrity": "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==", "dev": true, "funding": [ { @@ -5880,6 +5882,21 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-plugin-astro": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.14.1.tgz", + "integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^2.9.1", + "prettier": "^3.0.0", + "sass-formatter": "^0.7.6" + }, + "engines": { + "node": "^14.15.0 || >=16.0.0" + } + }, "node_modules/prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", @@ -6424,6 +6441,23 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/s.color": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz", + "integrity": "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass-formatter": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.7.9.tgz", + "integrity": "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "suf-log": "^2.5.3" + } + }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", @@ -6706,6 +6740,16 @@ "inline-style-parser": "0.2.4" } }, + "node_modules/suf-log": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz", + "integrity": "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==", + "dev": true, + "license": "MIT", + "dependencies": { + "s.color": "0.0.15" + } + }, "node_modules/tinyexec": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", diff --git a/package.json b/package.json index 0e4c709..a197be0 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "scripts": { "test": "node tests/index.js", "build": "rescript", - "format": "rescript format -all && prettier --write ./docs ./tests/index.js ./package.json", + "format": "rescript format -all && prettier --write ./tests/index.js ./package.json ./docs/pages", "docs": "astro dev", "build:docs": "astro build" }, @@ -40,7 +40,9 @@ "devDependencies": { "@astrojs/starlight": "^0.29.0", "astro": "^4.16.10", + "micromark": "^4.0.1", "prettier": "^3.3.3", + "prettier-plugin-astro": "^0.14.1", "sharp": "^0.33.5" } } diff --git a/src/DOMAPI/Window.js b/src/DOMAPI/Window.js index 977be07..dbf8d92 100644 --- a/src/DOMAPI/Window.js +++ b/src/DOMAPI/Window.js @@ -1,7 +1,7 @@ // Generated by ReScript, PLEASE EDIT WITH CARE -import * as EventTarget$WebApi from "../EventAPI/EventTarget.js"; +import * as EventTarget$WebAPI from "../EventAPI/EventTarget.js"; -EventTarget$WebApi.Impl({}); +EventTarget$WebAPI.Impl({}); /* Not a pure module */