Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
da7e603
feat: v1.1.0 - harden library, add CLI, types, and 8 new styles
brianfunk Oct 2, 2026
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
894f410
fix: address Codex review on the library
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
b0f54f6
fix: sentence case recognizes typographic quotes at boundaries
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
74edac4
fix: drop marks orphaned by removed emoji in tokenizer; strike treats…
brianfunk Oct 6, 2026
e7f9276
Merge branch 'feat/v1.1.0-hardening' into feat/netlify-site-api
brianfunk Oct 6, 2026
d52be1e
test: grapheme-safe strike, CRLF, and orphaned-mark tokenizer cases
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
9f0114d
fix: keep keycap emoji whole in bubble/wide; escape newlines in CLI -…
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
9921cd7
perf: tokenize lazily so non-code styles skip toWords
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
7d4647e
fix: keycap emoji are tokenizer separators; CLI --all escapes U+2028/…
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
cea20a9
chore: keep the npm package lean
brianfunk Oct 8, 2026
727a167
Merge branch 'feat/v1.1.0-hardening' into feat/netlify-site-api
brianfunk Oct 8, 2026
488cd77
feat: full API port with 7 output formats, Swagger reference, develop…
brianfunk Oct 8, 2026
5ff4e82
feat: 50 styles, Stripe-style Developer Docs, playground and theme po…
brianfunk Oct 8, 2026
517ec0f
chore: declare btoa global for ESLint
brianfunk Oct 8, 2026
d8632f4
style: icon in the accent violet
brianfunk Oct 8, 2026
c46a79d
style: readable dark theme for the Swagger reference
brianfunk Oct 8, 2026
7a79f74
feat: API Reference served at /api, Developer Docs at /docs, inline c…
brianfunk Oct 8, 2026
214de0c
feat: no hard-coded host on the site; inline Node/CLI samples in Deve…
brianfunk Oct 8, 2026
1303eed
feat: count() in the library and CLI; badge row in the playground
brianfunk Oct 8, 2026
e7c07d0
feat: svg and png output formats, higher limits, CLI quick start
brianfunk Oct 8, 2026
6d258c1
docs: note the GET URL cap next to the text limit
brianfunk Oct 8, 2026
807f71c
docs: --count in the CLI usage block; GET URL cap in README and playg…
brianfunk Oct 8, 2026
d6e7d5a
fix: address Codex review
brianfunk Oct 8, 2026
86229f4
fix: address self-review findings
brianfunk Oct 8, 2026
599c510
style: cache-bust the favicon so the violet icon shows
brianfunk Oct 8, 2026
b540597
fix: address final Codex review
brianfunk Oct 8, 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
19 changes: 11 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]
node-version: [22, 24]

steps:
- uses: actions/checkout@v4
Expand All @@ -28,17 +27,21 @@ 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
run: |
node bin/capstring.js --version
test "$(echo 'hello world' | node bin/capstring.js title)" = "Hello World"
test "$(node bin/capstring.js kebab 'XMLHttpRequest')" = "xml-http-request"

- name: Verify package contents
run: npm pack --dry-run

- 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
42 changes: 0 additions & 42 deletions .npmignore

This file was deleted.

65 changes: 40 additions & 25 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,72 @@

## Overview

`capstring` is a lightweight JavaScript library for text capitalization and transformation. Zero dependencies, 24+ styles.
`capstring` is a lightweight JavaScript library for text capitalization and transformation.
Zero dependencies, many styles, Unicode and emoji safe, ships a CLI and TypeScript types.

## Quick Start

```javascript
import capstring from 'capstring';
import capstring, { capstringAll } from 'capstring';

capstring('hello world', 'title'); // 'Hello World'
capstring('hello world', 'camel'); // 'helloWorld'
capstring('hello world', 'kebab'); // 'hello-world'
capstring('hello world', 'constant'); // 'HELLO_WORLD'
capstring('hello world', 'title'); // 'Hello World'
capstring('XMLHttpRequest', 'kebab'); // 'xml-http-request'
capstring('hello world', 'slug'); // 'hello-world'
capstringAll('hi').upper; // 'HI'
```

## API
```bash
npx capstring kebab "Hello World" # hello-world
echo hi | npx capstring --all # every style
```

### Main Function
## API

```javascript
capstring(str, style)
capstring(str, style = 'same', { strict = false } = {})
```

- `str` - String to transform
- `style` - Style name (default: 'same')
- Returns: Transformed string, or `false` if input invalid

### Helper Functions
- `strict` - Throw `TypeError` / `RangeError` instead of returning `false` / the input
- Returns: Transformed string; `''` for empty input; `false` if `str` is not a string

```javascript
import { getStyles, isValidStyle, STYLES } from 'capstring';
import { capstringAll, getStyles, isValidStyle, STYLES, CATEGORIES } from 'capstring';

getStyles(); // ['same', 'none', 'proper', ...]
isValidStyle('kebab'); // true
STYLES; // Frozen array of all style names
capstringAll('hi'); // { same: 'hi', none: '', ... } in STYLES order
getStyles(); // fresh copy of STYLES
isValidStyle('kebab'); // true
STYLES; // frozen array of all style names
CATEGORIES; // frozen { case, code, fun, encoding, art }
```

## All 24 Styles
## All Styles

| Category | Styles |
|----------|--------|
| Case | same, none, proper, title, sentence, upper, lower, swap |
| Code | camel, pascal, snake, kebab, slug, constant, python, dot, path |
| Fun | leet, reverse, sponge, mock, alternate, crazy, random |
| case | same, none, proper, title, sentence, upper, lower, swap, capitalize, lowerfirst |
| code | camel, pascal, snake, kebab, slug, constant, python, dot, path, train, hashtag, acronym, ada, cobol, initials |
| fun | reverse, sponge, mock, alternate, crazy, random, clap, piglatin, spaced, squish, nato |
| encoding | leet, rot13, morse, binary, hex, base64 |
| art | flip, smallcaps, bubble, wide, strike, bold, italic, script |

## Files

- `index.js` - the whole library. Must stay browser-safe: no `node:` imports, no `process`.
- `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/` - playground, `web/docs/` Developer Docs (Swagger API Reference is served at `/api` by the function); 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, batch, spell, count, lorem, badge, styles; json/txt/html/xml/yaml/csv/jsonp)
- `npm run dev` runs both locally on http://localhost:8888

## Testing

```bash
npm test # Run tests
npm run lint # Run linter
npm run test:coverage # Coverage report
npm run test:coverage # Coverage report (100% thresholds enforced)
```

## Related

- **cAPIta** - REST API for capstring (https://github.com/brianfunk/cAPIta)
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2026-10-02

### Added

- **Website**: [capstring.netlify.app](https://capstring.netlify.app) - playground with every style live, click to copy, shareable URLs, light/dark theme, output format picker, live badge
- **Developer Docs** at `/docs/`: one Stripe-style page with every style explained and example outputs computed live, Node and CLI samples inline
- **API Reference** at `/api` (for browsers): Swagger UI over a hand-written OpenAPI 3.1 document (`/openapi.json`)
- **HTTP API** at `/api`: `/:style/:text`, `/all`, `/styles`, `POST /batch`, `/spell`, `/count`, `/lorem`, `/badge`, as a single Netlify Function. Output formats json (default), txt, html, xml, yaml, csv, jsonp, svg, png, chosen by extension, `?format=`, or `Accept` header. Limits: 10,000 characters, 1,000 batch texts
- `count(str)` in the library (words, characters, charactersNoSpaces, spaces) and `--count` in the CLI
- **21 new styles**: `smallcaps`, `bubble`, `wide`, `strike`, `clap`, `morse`, `binary`, `piglatin`, `capitalize`, `lowerfirst`, `ada`, `cobol`, `initials`, `spaced`, `squish`, `nato`, `hex`, `base64`, `bold`, `italic`, `script`
- **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
- `capstringAll(str)` - every style at once, keyed by style name
- `CATEGORIES` - frozen style groupings (`case`, `code`, `fun`, `encoding`, `art`)
- `{ 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; 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`)
- `title` is Unicode aware (`élan vital` → `Élan Vital`, was `éLan Vital`); `_` now breaks words
- `sentence` capitalizes after `.`, `!`, `?` (`hello. world` → `Hello. World`)
- `leet` uses the conventional ASCII map and preserves case (`hello WORLD` → `h3110 W0R1D`, was `h3££0 w0r£d`)
- `alternate` counts Unicode letters, not just a-z
- `hashtag` of text with no words returns `''` instead of `#`
- `crazy` output may differ from 1.0.0 for text containing emoji (now seeded per code point)

### Fixed

- `reverse` and `flip` no longer corrupt emoji, flags, or combining accents (grapheme-cluster aware)
- All styles iterate code points instead of UTF-16 code units
- `@eslint/js` declared as a devDependency

### Docs

- Style count corrected everywhere (README, AGENTS.md, package.json)
- README regrouped into Case / Code / Fun / Encodings / Unicode Art with behavior notes

## [1.0.0] - 2026-02-01

### Breaking Changes
Expand Down
47 changes: 31 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Project Context

capstring is a lightweight JavaScript library for text capitalization and transformation. It supports 29 different styles including case transformations, code conventions, and fun styles.
capstring is a lightweight JavaScript library for text capitalization and transformation. It supports many styles including case transformations, code conventions, encodings, and fun Unicode styles. Serious and robust, even though it's for fun.

## Development Commands

Expand All @@ -11,41 +11,56 @@ 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
- 100% test coverage target

## Architecture

Single-file library with a main function and helper exports:
- `capstring(str, style)` - Main transformation function
- `getStyles()` - Returns array of all style names
- `isValidStyle(style)` - Validates a style name
- `STYLES` - Frozen array constant of all styles
Single-file library plus a thin CLI:
- `index.js` - the whole library. **Must stay browser-safe**: no `node:` imports, no `process`.
- `capstring(str, style, { strict })` - Main transformation function
- `capstringAll(str, options)` - Every style at once
- `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, no build step. Playground (`index.html`, `app.js`), Developer Docs (`docs/`: single Stripe-style page, per-style docs in `docs/styles-data.js` with outputs computed live). The Swagger API Reference is served by the API function at `/api` for browsers (`netlify/functions/swagger-page.js`, themed by `web/swagger-theme.css`, spec at `web/openapi.json`), shared header/footer/theme (`site.js`, `site.css`, `theme-init.js`). 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) and `@resvg/resvg-js` (png format, with fonts vendored in `netlify/fonts/`) are **devDependencies** on purpose: the function bundles them and the npm package stays zero-dependency. They are listed as external + included_files in `netlify.toml` because they read files by path or are native binaries.
- `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 (29 total)
## Supported Styles

**Case:** same, none, proper, title, sentence, upper, lower, swap
**Code:** camel, pascal, snake, kebab, slug, constant, python, dot, path, train
**Fun:** leet, reverse, sponge, mock, alternate, crazy, random
**New:** hashtag, acronym, rot13, flip
**Case:** same, none, proper, title, sentence, upper, lower, swap, capitalize, lowerfirst
**Code:** camel, pascal, snake, kebab, slug, constant, python, dot, path, train, hashtag, acronym, ada, cobol, initials
**Fun:** reverse, sponge, mock, alternate, crazy, random, clap, piglatin, spaced, squish, nato
**Encoding:** leet, rot13, morse, binary, hex, base64
**Art:** flip, smallcaps, bubble, wide, strike, bold, italic, script

Never write the style count into prose, docs, or the site; it changes. Compute it from `STYLES` where a number is needed.
Docs and tables use `hello world` as the example everywhere; no "(alias of X)" notes.

Never remove a style name; `proper` and `python` are kept as aliases on purpose.

## When Making Changes

1. Ensure all tests pass: `npm test`
2. Maintain 100% coverage: `npm run test:coverage`
2. Maintain 100% coverage (thresholds enforced): `npm run test:coverage`
3. Run linter: `npm run lint`
4. Update CHANGELOG.md for any user-facing changes
5. Preserve the ASCII art header
4. Update CHANGELOG.md for any user-facing changes; behavior changes get their own section
5. Adding a style: add to `STYLES` **and** one `CATEGORIES` group **and** the `Style` union in `index.d.ts` **and** `web/docs/styles-data.js` **and** the enum in `web/openapi.json`, then regenerate the README table (every table row is `hello world`)
6. Preserve the ASCII art header
7. Keep the npm package lean: `test/package.test.js` fails if the tarball gains a file or exceeds the size budget. Website, API, tests, and changelog never ship to npm

## Related Projects
## Deployment

- **cAPIta** - REST API that wraps capstring (separate repo)
Netlify site `capstring` (personal team) deploys from git: `master` is production, `dev` is a branch deploy, PRs get previews. No deploy step in GitHub Actions.

---

Expand Down
Loading
Loading