Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
05ad3bd
feat: Netlify website and HTTP API
brianfunk Oct 2, 2026
30ec7c7
chore: run CI on every PR, fix dev dependency audit findings
brianfunk Oct 2, 2026
7ad9027
fix: keep vitest 4.0.x for Node 18, update vulnerable transitive deps…
brianfunk Oct 2, 2026
aac2a40
feat: Node 22 floor, badge/lorem/spell endpoints, chain builder, site…
brianfunk Oct 2, 2026
073075b
fix: address code review findings
brianfunk Oct 6, 2026
61266a0
Merge branch 'feat/v1.1.0-hardening' into feat/netlify-site-api
brianfunk Oct 6, 2026
5501475
fix: address Codex review on site and API
brianfunk Oct 6, 2026
8d0480c
Merge branch 'feat/v1.1.0-hardening' into feat/netlify-site-api
brianfunk Oct 6, 2026
0aae736
fix: Node 22.13 floor to match ESLint 10; styles and root honor forma…
brianfunk Oct 6, 2026
e7f9276
Merge branch 'feat/v1.1.0-hardening' into feat/netlify-site-api
brianfunk Oct 6, 2026
959fe5d
Merge branch 'feat/v1.1.0-hardening' into feat/netlify-site-api
brianfunk Oct 6, 2026
9a4318f
fix: rebuild on lockfile-only commits; batch honors format=txt
brianfunk Oct 6, 2026
44b3c4e
fix: rename batch txt payload to avoid shadowing the parsed body
brianfunk Oct 6, 2026
86e43a3
Merge branch 'feat/v1.1.0-hardening' into feat/netlify-site-api
brianfunk Oct 6, 2026
02d6837
fix: bound chain output at 20k chars in the API and the site
brianfunk Oct 6, 2026
d087f24
Merge branch 'feat/v1.1.0-hardening' into feat/netlify-site-api
brianfunk Oct 6, 2026
a00acc6
fix: escape line breaks in txt outputs; copy the displayed chain result
brianfunk Oct 6, 2026
6ce4e18
Merge branch 'feat/v1.1.0-hardening' into feat/netlify-site-api
brianfunk Oct 6, 2026
f7e4cc4
fix: spellcheck keeps contractions whole; txt outputs escape U+2028/2029
brianfunk Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ on:
push:
branches: [main, master, dev]
pull_request:
branches: [main, master, dev]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22, 24]
node-version: [22, 24]

steps:
- uses: actions/checkout@v4
Expand All @@ -28,12 +27,7 @@ jobs:
- name: Run linter
run: npm run lint

- name: Run tests
if: matrix.node-version == 18
run: npm test

- name: Run tests with coverage
if: matrix.node-version != 18
run: npm run test:coverage

- name: CLI smoke test
Expand All @@ -47,7 +41,7 @@ jobs:

- name: Upload coverage
uses: codecov/codecov-action@v4
if: matrix.node-version == 20
if: matrix.node-version == 22
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ jspm_packages

# Optional REPL history
.node_repl_history

# Netlify
.netlify
web/capstring.js
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ CATEGORIES; // frozen { case, code, fun, encoding, art }
- `cli.js` - CLI logic as `main(argv, io)`; `bin/capstring.js` is the executable shim.
- `index.d.ts` - hand-written types. `test/types.test.js` fails if the `Style` union drifts from `STYLES`.

## Website and API

- `web/` - static site, imports `./capstring.js` (copied from `index.js` at Netlify build time)
- `netlify/functions/api.js` - HTTP API at `https://capstring.netlify.app/api` (transform, all, chain, batch, badge, lorem, spell)
- `npm run dev` runs both locally on http://localhost:8888

## Testing

```bash
Expand Down
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Website**: [capstring.netlify.app](https://capstring.netlify.app) - live preview of every style, click to copy, shareable URLs
- **HTTP API**: `/api/:style/:text`, `/api/all`, `/api/chain`, `/api/styles`, `POST /api/batch`, plus `/api/badge` (SVG), `/api/lorem`, and `/api/spell` ported from cAPIta, as a single Netlify Function (replaces the archived cAPIta project)
- Website chain builder: press `+` on any row to compose a chain, shareable via `?chain=upper+reverse`
- **8 new styles** (37 total): `smallcaps`, `bubble`, `wide`, `strike`, `clap`, `morse`, `binary`, `piglatin`
- **CLI**: `npx capstring <style> [text]`, `--all`, `--list`, `--json`, `--help`, `--version`; reads stdin when no text is given
- **TypeScript declarations** (`index.d.ts`) with a `Style` union type for autocomplete
Expand All @@ -17,10 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `{ strict: true }` option - throw `TypeError` / `RangeError` instead of returning `false` / the input
- Smart tokenizer for code styles: splits on camelCase boundaries, `_`, `-`, `.`, `/` and punctuation (`XMLHttpRequest` → `xml-http-request`)
- `files` whitelist, `sideEffects: false`, `./package.json` export, `prepublishOnly` gate
- 100% coverage thresholds enforced; Node 24 added to CI; CLI smoke test in CI
- 100% coverage thresholds enforced; CI matrix is Node 22 and 24; CLI smoke test in CI

### Behavior changes

- **Node.js 22.13+ required** (18 and 20 are end-of-life; the test toolchain no longer supports them)
- Empty string input now returns `''` instead of `false` (non-string input still returns `false`)
- `slug` is now a real slugifier: diacritics folded, punctuation removed, ASCII only (`Crème Brûlée & Co.` → `creme-brulee-co`). It no longer equals `kebab`, which keeps Unicode letters
- All code styles tokenize camelCase and separator input (`helloWorld` → `hello_world`, was `helloworld`)
Expand Down
11 changes: 10 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ npm install # Install dev dependencies
npm test # Run Vitest tests
npm run lint # Run ESLint
npm run test:coverage # Run tests with coverage report
npm run dev # Netlify Dev: website + API on http://localhost:8888
```

## Code Style

- Node 22.13+ (CI runs 22 and 24; eslint 10 needs 22.13, vitest 5 needs 22.12)
- ES2022+ syntax (const/let, arrow functions, template literals)
- ESM modules only (`import`/`export`)
- Full JSDoc documentation
Expand All @@ -29,6 +31,9 @@ Single-file library plus a thin CLI:
- `getStyles()`, `isValidStyle(style)`, `STYLES`, `CATEGORIES`
- `cli.js` - `main(argv, io)` with injected I/O so it is unit-testable; `bin/capstring.js` is the shim
- `index.d.ts` - hand-written types; `test/types.test.js` enforces that the `Style` union matches `STYLES`
- `web/` - static site (index.html, style.css, app.js), no build step; imports `./capstring.js`, which Netlify's build copies from `index.js` (gitignored locally)
- `netlify/functions/api.js` - the whole HTTP API, Functions 2.0 handler owning `/api/*`; tested by constructing `Request` objects. `nspell` and `dictionary-en` (spellcheck) are **devDependencies** on purpose: esbuild bundles them into the function and the npm package stays zero-dependency. `dictionary-en` is listed as external + included_files in `netlify.toml` because it reads its `.aff`/`.dic` files by path.
- `netlify.toml` - publish `web/`, build copies `index.js`, bundles the function. Branch settings (production `master`, branch deploy `dev`) live in the Netlify UI, not in the file

## Supported Styles (37 total)

Expand All @@ -50,9 +55,13 @@ Never remove a style name; `proper` and `python` are kept as aliases on purpose.
6. Preserve the ASCII art header
7. Verify `npm pack --dry-run` still lists only the intended files

## Deployment

Netlify site `capstring` (Brian Funk team) deploys from git: `master` is production, `dev` is a branch deploy, PRs get previews. No deploy step in GitHub Actions.

## Related Projects

- **cAPIta** - REST API that wraps capstring (separate repo)
- **cAPIta** - archived. The former standalone REST API; `capita-api.netlify.app` redirects here.

---

Expand Down
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[![capstring](https://img.shields.io/badge/capstring-CaPiTaLiZe%20StRiNgS!-b5d4ff.svg)](https://github.com/brianfunk/capstring)
[![npm version](https://img.shields.io/npm/v/capstring.svg)](https://www.npmjs.com/package/capstring)
[![npm downloads](https://img.shields.io/npm/dm/capstring.svg)](https://www.npmjs.com/package/capstring)
[![Website](https://img.shields.io/badge/web-capstring.netlify.app-b5d4ff.svg)](https://capstring.netlify.app)
[![CI](https://github.com/brianfunk/capstring/actions/workflows/ci.yml/badge.svg)](https://github.com/brianfunk/capstring/actions/workflows/ci.yml)
[![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://github.com/ellerbrock/open-source-badge/)
[![Semver](https://img.shields.io/badge/SemVer-2.0-blue.svg)](http://semver.org/spec/v2.0.0.html)
Expand All @@ -24,6 +25,10 @@ ridiculous fun style. Unicode and emoji safe. Ships a CLI and TypeScript types.
- **TypeScript types** - autocomplete for every style name
- **100% test coverage**

## Try it online

**[capstring.netlify.app](https://capstring.netlify.app)** - type anything, see all 37 styles, click to copy.

## Installation

```bash
Expand Down Expand Up @@ -174,6 +179,49 @@ const style: Style = 'kebab'; // autocompletes all 37 names
capstring('Hello', style); // string
```

## HTTP API

Free, no key, CORS enabled, hosted at `https://capstring.netlify.app/api`.

```bash
curl https://capstring.netlify.app/api/title/hello%20world
# {"input":"hello world","style":"title","output":"Hello World"}

curl "https://capstring.netlify.app/api/all/hello%20world?format=txt"
# same hello world
# none
# ...

curl https://capstring.netlify.app/api/chain/upper+reverse/hello
# {"input":"hello","styles":["upper","reverse"],"output":"OLLEH"}

curl -X POST https://capstring.netlify.app/api/batch \
-H "Content-Type: application/json" \
-d '{"style":"slug","texts":["Crème Brûlée","Hello World"]}'
```

| Endpoint | Returns |
|----------|---------|
| `GET /api/styles` | style names and categories |
| `GET /api/:style/:text` | `{ input, style, output }` |
| `GET /api/all/:text` | every style |
| `GET /api/chain/:styles/:text` | styles applied in order (`+` or `,` separated, max 10) |
| `POST /api/batch` | `{ style, texts[] }` (max 100 texts) |
| `GET /api/badge/:style/:text` | shields-style SVG badge, `?label=` to override the left side |
| `GET /api/lorem/:count` | lorem ipsum words (1 to 1000), optional `?style=` |
| `GET /api/spell/:text` | spell-corrected text plus a `corrections` list, optional `?style=`. Max 500 chars, up to 50 corrections per request |

[![capstring badge](https://capstring.netlify.app/api/badge/sponge/hello%20world?label=capstring)](https://capstring.netlify.app)

```markdown
![sponge](https://capstring.netlify.app/api/badge/sponge/hello%20world)
```

Text is limited to 2,000 characters. Add `?format=txt` for plain text, `?pretty=1` for indented
JSON, or `?text=` to pass text with slashes. Errors are `{ "error": { "code", "message" } }`.

The previous standalone API, cAPIta, is archived and redirects here.

## Behavior notes

- **Unicode**: every style iterates code points, and `reverse` / `flip` iterate grapheme clusters, so
Expand All @@ -195,8 +243,14 @@ npm install # Install dependencies
npm test # Run tests
npm run lint # Run linter
npm run test:coverage # Test with coverage (100% required)
npm run dev # Netlify Dev: site + API at http://localhost:8888
```

The website lives in `web/` (plain HTML/CSS/JS, no build step) and the API in
`netlify/functions/api.js`. Both import `index.js` directly. The spellcheck endpoint's
dependencies (`nspell`, `dictionary-en`) are devDependencies bundled into the function, so the
npm package itself stays zero-dependency.

## Contributing

Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the code of conduct and the process for submitting pull requests.
Expand All @@ -207,7 +261,7 @@ This project uses [Semantic Versioning 2.0](http://semver.org/spec/v2.0.0.html).

## Requirements

- Node.js 18+ (or any modern browser)
- Node.js 22.13+ (or any modern browser)
- ES modules (`import`/`export`)

## License
Expand Down
11 changes: 8 additions & 3 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Examples:
*/
const parseArgs = (argv) => {
const out = { all: false, list: false, json: false, help: false, version: false, style: null, text: [], unknown: null };
const positionals = [];
let optionsDone = false;
for (const arg of argv) {
if (!optionsDone && arg === '--') {
Expand All @@ -66,12 +67,16 @@ const parseArgs = (argv) => {
else if (arg === '-h' || arg === '--help') out.help = true;
else if (arg === '-v' || arg === '--version') out.version = true;
else out.unknown = out.unknown ?? arg;
} else if (out.style === null && !out.all) {
out.style = arg;
} else {
out.text.push(arg);
positionals.push(arg);
}
}
// Flags may appear anywhere, so only assign positionals once every flag is known
if (out.all) {
out.text = positionals;
} else {
[out.style = null, ...out.text] = positionals;
}
return out;
};

Expand Down
25 changes: 22 additions & 3 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ export default [
js.configs.recommended,
{
languageOptions: {
ecmaVersion: 2023,
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
console: 'readonly',
process: 'readonly',
Intl: 'readonly',
TextEncoder: 'readonly',
URL: 'readonly'
URL: 'readonly',
URLSearchParams: 'readonly',
Request: 'readonly',
Response: 'readonly',
Headers: 'readonly'
}
},
rules: {
Expand All @@ -21,6 +25,21 @@ export default [
}
},
{
ignores: ['node_modules/**', 'coverage/**']
files: ['web/**/*.js'],
languageOptions: {
globals: {
window: 'readonly',
document: 'readonly',
navigator: 'readonly',
location: 'readonly',
history: 'readonly',
fetch: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly'
}
}
},
{
ignores: ['node_modules/**', 'coverage/**', 'web/capstring.js', '.netlify/**']
}
];
33 changes: 33 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[build]
publish = "web"
command = "cp index.js web/capstring.js"
functions = "netlify/functions"
# Skip the build when nothing deployable changed (docs-only commits)
# Netlify sets CACHED_COMMIT_REF == COMMIT_REF on first or cache-cleared deploys; exit 1 forces the build then
ignore = "test \"$CACHED_COMMIT_REF\" = \"$COMMIT_REF\" && exit 1; git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- index.js web netlify netlify.toml package.json package-lock.json"

[build.environment]
NODE_VERSION = "22"

[functions]
node_bundler = "esbuild"
# dictionary-en reads its .aff/.dic files by path at runtime, so ship it unbundled
external_node_modules = ["dictionary-en"]
included_files = ["node_modules/dictionary-en/**"]

[dev]
publish = "web"
framework = "#static"
port = 8888

[[headers]]
for = "/*"
[headers.values]
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
X-Frame-Options = "DENY"

[[headers]]
for = "/capstring.js"
[headers.values]
Cache-Control = "public, max-age=3600"
Loading
Loading