diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..435618c --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,51 @@ +name: Publish API documentation + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Use Node.js 26 + uses: actions/setup-node@v7 + with: + node-version: 26.x + cache: npm + - run: npm ci + - name: Generate API documentation + run: npm run docs:build + - name: Configure GitHub Pages + if: github.event_name != 'pull_request' + uses: actions/configure-pages@v6 + - name: Upload GitHub Pages artifact + if: github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v5 + with: + path: docs-site + + deploy: + if: github.event_name != 'pull_request' + needs: build + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 8ca74db..16ecd06 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ yarn-error.log* # Dependency directories node_modules/ +# Generated API documentation +docs-site/ + # Optional npm cache directory .npm diff --git a/README.md b/README.md index a91cb0e..f85d5ff 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,12 @@ let isReadableInCombination = switch (redString, blueRgb) { See all available functions in the [original TinyColor repo](https://github.com/scttcper/tinycolor) and example usage of all functions in [the tests](https://github.com/mikaello/rescript-tinycolor/blob/master/__tests__/Tinycolor_tests.res). +## API documentation + +The generated [API reference](https://mikaello.github.io/rescript-tinycolor/) includes searchable signatures and links back to their ReScript source. + +To build it locally, run `npm run docs:build` and open `docs-site/index.html`. + ## Differences from original - It is not possible to create an invalid tinycolor instance, it will either return `Some(t)` if it is valid, or `None` if it is invalid. E.g. an invalid instance can occur if you create a color with a string not corresponding to a valid color (`beautifulRed` is not a valid color) or you provide RGB values outside the valid range (0-255). diff --git a/package.json b/package.json index 6de1771..b223132 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "5.1.2", "scripts": { "build": "rescript build", + "docs:build": "npm run build && node scripts/generate-docs.mts", "start": "rescript build -w", "clean": "rescript clean", "check": "rescript format --check", diff --git a/scripts/generate-docs.mts b/scripts/generate-docs.mts new file mode 100644 index 0000000..f550ac9 --- /dev/null +++ b/scripts/generate-docs.mts @@ -0,0 +1,288 @@ +import {execFileSync} from "node:child_process" +import {mkdirSync, readFileSync, writeFileSync} from "node:fs" +import {dirname, join} from "node:path" +import {fileURLToPath} from "node:url" + +type Source = { + filepath: string + line: number + col: number +} + +type ApiItem = { + kind: string + name: string + signature: string + docstrings: string[] + source: Source +} + +type Documentation = { + items: ApiItem[] +} + +type PackageJson = { + name: string + version: string + repository: string | {url: string} +} + +const root = join(dirname(fileURLToPath(import.meta.url)), "..") +const outputDirectory = join(root, "docs-site") +const sourceFile = "src/TinyColor.resi" +const tools = join(root, "node_modules/rescript/cli/rescript-tools.js") +const packageJson = JSON.parse( + readFileSync(join(root, "package.json"), "utf8"), +) as PackageJson + +const extracted = execFileSync(process.execPath, [tools, "doc", sourceFile], { + cwd: root, + encoding: "utf8", +}) + +if (extracted.trim() === "") { + throw new Error(`No documentation was extracted from ${sourceFile}`) +} + +const documentation = JSON.parse(extracted) as Documentation +const apiItems = documentation.items + +if (apiItems.length === 0) { + throw new Error(`No public API entries were extracted from ${sourceFile}`) +} + +const formatKind = (kind: string): string => + `${kind.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, first => first.toUpperCase())}s` + +const grouped = new Map() +for (const item of apiItems) { + const category = formatKind(item.kind) + grouped.set(category, [...(grouped.get(category) ?? []), item]) +} + +const escapeHtml = (value: unknown): string => + String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'") + +const itemId = (item: ApiItem): string => + `${item.kind}-${item.name.toLowerCase()}` +const repositoryValue = + typeof packageJson.repository === "string" + ? packageJson.repository + : packageJson.repository.url +const repository = repositoryValue + .replace(/^git\+/, "") + .replace(/^git@github\.com:/, "https://github.com/") + .replace(/\.git$/, "") + +const renderItem = (item: ApiItem): string => { + const docs = item.docstrings + .map(doc => `

${escapeHtml(doc)}

`) + .join("") + const sourceUrl = `${repository}/blob/HEAD/${item.source.filepath}#L${item.source.line}` + + return ` +
+
+
+ ${escapeHtml(item.kind)} +

${escapeHtml(item.name)}

+
+ Source ↗ +
+ ${docs} +
${escapeHtml(item.signature)}
+
` +} + +const navigation = [...grouped] + .filter(([, items]) => items.length > 0) + .map( + ([category, items]) => ` + + ${category} + ${items.length} + `, + ) + .join("") + +const sections = [...grouped] + .filter(([, items]) => items.length > 0) + .map( + ([category, items]) => ` +
+
+

Extracted kind

+

${category}

+
+
${items.map(renderItem).join("")}
+
`, + ) + .join("") + +const html = ` + + + + + + + ${escapeHtml(packageJson.name)} · API + + + +
+
+

Generated ReScript API reference

+

Small library.
Clear colors.

+

Fast, typed color manipulation and conversion for ReScript, powered by TinyColor.

+
+ npm install ${escapeHtml(packageJson.name)} + GitHub ↗ + v${escapeHtml(packageJson.version)} · ${apiItems.length} API entries +
+
+
+
+ +
+ ${sections} +

No API entries match that search.

+
+
+ + + + +` + +mkdirSync(outputDirectory, {recursive: true}) +writeFileSync(join(outputDirectory, "index.html"), html) +writeFileSync(join(outputDirectory, "api.json"), `${JSON.stringify(documentation, null, 2)}\n`) +writeFileSync(join(outputDirectory, ".nojekyll"), "") + +console.log(`Generated ${apiItems.length} API entries from ${sourceFile}`)