Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

10 changed files with 132 additions and 396 deletions

View File

@ -30,7 +30,7 @@ jobs:
uses: https://git.cyborggrizzly.com/bearmetal/ci-actions/deno-release@main uses: https://git.cyborggrizzly.com/bearmetal/ci-actions/deno-release@main
with: with:
entrypoint: main.ts entrypoint: main.ts
compile-flags: "--allow-read --allow-write --allow-env --allow-net --include asciiart.txt" compile-flags: "--allow-read --allow-write --allow-env --allow-net"
env: env:
GITEA_TOKEN: ${{ secrets.GIT_PAT }} GITEA_TOKEN: ${{ secrets.GIT_PAT }}

View File

@ -1,39 +0,0 @@
# Changelog
## v1.0.2 (2025-05-20)
<!-- auto-changelog -->
## v1.0.1 (2025-05-7)
<!-- auto-changelog -->
### Known Issues
- help flags can cause issues
## v1.0.0 (2025-05-7)
### Features
- Check Code Tool
- reads supplied code files to see if form fields are present and represented
in a switch statement
- Field Rename Tool
- provide a search and replace pattern to bulk rename form fields
- List Form Fields
- Sometimes you just need to see what fields there are
### Known Issues
- Field rename does not represent full change applied to the field, only the
replaced text
- help flags can cause issues
- ascii art is broken (sad face)
- banana doesn't work
## v0.0.0 (never)
this is just here for a reference to the auto-changelog
<!-- auto-changelog -->

View File

@ -1,16 +1,7 @@
# BearMetal PDF Tools # Emma's Simple Form Field Checker
A collection of tools for working with PDF forms. Compares a PDF form to a list of CS class files to see if all field names are
present, excluding signature fields.
## Features
- Check Code Tool
- reads supplied code files to see if form fields are present and represented
in a switch statement
- Field Rename Tool
- provide a search and replace pattern to bulk rename form fields
- List Form Fields
- Sometimes you just need to see what fields there are
## Prereqs ## Prereqs
@ -20,8 +11,7 @@ Deno >=2.2 (not required if downloading .exe)
### Deno install ### Deno install
`deno install -g --allow-read --allow-write --allow-net --allow-env jsr:@bearmetal/pdf-tools` `deno task install` -> installs as global command `checkfields`
-> installs as global command `pdf-tools`
### Compile ### Compile
@ -30,51 +20,17 @@ Deno >=2.2 (not required if downloading .exe)
> If you want it to be a global command, create an executables directory and add > If you want it to be a global command, create an executables directory and add
> it to the PATH > it to the PATH
### Precompiled
Download the latest release from the
[releases page](https://git.cyborggrizzly.com/BearMetal/pdf-tools/releases)
## Usage ## Usage
`pdf-tools <tool> <args>` -> `<tool>` is one of the following `checkfields <path to PDF> <comma-separated list of paths to CS class files>`
-OR- `checkfields` and follow prompts.
- check-code ### Output
- field-rename
- list-form-fields
## Contributing > All form fields present!
Contributions are welcome! -OR-
## License > The following field names are not present in the CS code
GPL 3.0 > \<list of missing form fields\>
---
### About this project
BearMetal PDF Tools is a collection of tools made to fix the current state of
PDF form editing. Adobe Acrobat is a great tool, but it's not always the easiest
to use, nor is it free. It also lacks some features that I find useful, such as
bulk renaming of form fields. There's also a lack of powerful, free, and open
source tools for PDF editing.
This project aims to fill that gap by providing a set of tools that can be used
to edit PDF forms. The tools are written in Deno, a modern and secure runtime
for JavaScript and TypeScript. They are designed to be easy to use and to
provide a great user experience.
The tools are designed to be used in a terminal, and are not designed to be
integrated into other applications. They are also not designed to be used as a
library, but rather as a command line tool.
### About BearMetal
BearMetal is a project that aims to decrapify modern web development. It is a
collection of tools, libraries, and resources that aim to make web development
more accessible and less intimidating. The project is open source and free to
use, and is designed to be used by anyone, regardless of skill level or
experience. You can find a list of libraries and tools on
[JSR](https://jsr.io/@bearmetal).

View File

@ -44,9 +44,7 @@ export class TerminalLayout {
clearTimeout(this.debounceTimer); clearTimeout(this.debounceTimer);
} }
this.debounceTimer = setTimeout( this.debounceTimer = setTimeout(
() => { () => this.renderLayout(),
this.renderLayout();
},
this.debounceDelay, this.debounceDelay,
); );
} }
@ -78,10 +76,6 @@ export class TerminalLayout {
block.renderInternal(usedLines + 1); block.renderInternal(usedLines + 1);
usedLines += lines.length; usedLines += lines.length;
} }
for (const name of this.layoutOrder) {
const block = this.blocks[name];
block.runPostRenderAction?.();
}
} }
clearAll() { clearAll() {
@ -117,8 +111,6 @@ export class TerminalBlock {
private renderHeight: number = 0; private renderHeight: number = 0;
private lastRenderRow = 1; private lastRenderRow = 1;
private lastRendered: string[] = [];
private preserveHistory = false; private preserveHistory = false;
constructor(private prepend: string = "") {} constructor(private prepend: string = "") {}
@ -201,41 +193,29 @@ export class TerminalBlock {
} }
renderInternal(startRow?: number) { renderInternal(startRow?: number) {
const outputLines: string[] = []; this.lastRenderRow = startRow ?? this.lastRenderRow;
this.clear(); // uses old renderedLineCount
for (let i = 0; i < this.renderLines.length; i++) { const outputLines = this.renderLines.map((line) =>
const line = `${this.prepend}${this.renderLines[i]}`; `${this.prepend}${line}\x1b[K`
const previous = this.lastRendered[i]; );
if (line !== previous) {
const moveToLine = `\x1b[${(startRow ?? this.lastRenderRow) + i};1H`;
outputLines.push(moveToLine + line + "\x1b[K");
}
}
if (this.lastRendered.length > this.renderLines.length) {
const baseRow = startRow ?? this.lastRenderRow;
for (let i = this.renderLines.length; i < this.lastRendered.length; i++) {
const moveToLine = `\x1b[${baseRow + i};1H\x1b[2K`;
Deno.stdout.writeSync(new TextEncoder().encode(moveToLine));
}
}
const baseRow = startRow ?? this.lastRenderRow;
const excessLines = this.renderHeight - this.renderLines.length;
for (let i = 0; i < excessLines; i++) {
const moveToLine = `\x1b[${
baseRow + this.renderLines.length + i
};1H\x1b[2K`;
Deno.stdout.writeSync(new TextEncoder().encode(moveToLine));
}
this.lastRendered = [...this.renderLines];
this.renderedLineCount = this.renderHeight;
this.lastRenderRow = baseRow;
const output = outputLines.join("\n"); const output = outputLines.join("\n");
if (startRow !== undefined) {
const moveCursor = `\x1b[${startRow};1H`;
Deno.stdout.writeSync(new TextEncoder().encode(moveCursor + output));
} else {
Deno.stdout.writeSync(new TextEncoder().encode(output)); Deno.stdout.writeSync(new TextEncoder().encode(output));
} }
// update rendered line count *after* rendering
this.renderedLineCount = outputLines.reduce(
(count, line) =>
count +
Math.ceil((line.length) / (Deno.consoleSize().columns || 80)),
0,
);
}
clear() { clear() {
if (this.renderedLineCount === 0) return; if (this.renderedLineCount === 0) return;
const moveCursor = `\x1b[${this.lastRenderRow};1H`; const moveCursor = `\x1b[${this.lastRenderRow};1H`;
@ -262,17 +242,6 @@ export class TerminalBlock {
return this.fixedHeight ?? 0; return this.fixedHeight ?? 0;
} }
private _postRenderAction?: () => void;
setPostRenderAction(action: (this: TerminalBlock) => void) {
this._postRenderAction = action;
}
runPostRenderAction() {
if (this._postRenderAction) {
this._postRenderAction.call(this);
this._postRenderAction = undefined;
}
}
get lineCount() { get lineCount() {
return this.renderLines.length; return this.renderLines.length;
} }

View File

@ -6,6 +6,8 @@ import { selectMenuInteractive } from "./selectMenu.ts";
import { TerminalBlock, TerminalLayout } from "./TerminalLayout.ts"; import { TerminalBlock, TerminalLayout } from "./TerminalLayout.ts";
import { cliAlert, cliLog } from "./prompts.ts"; import { cliAlert, cliLog } from "./prompts.ts";
import type { ITool } from "../types.ts"; import type { ITool } from "../types.ts";
import { join, toFileUrl } from "@std/path";
import { log } from "util/logfile.ts";
// Register tools here (filename, no extension) // Register tools here (filename, no extension)
const toolRegistry: [string, Promise<{ default: ITool }>][] = [ const toolRegistry: [string, Promise<{ default: ITool }>][] = [
@ -21,7 +23,6 @@ export class PdfToolsCli {
private args = ArgParser.parse(Deno.args).setFlagDefs({ private args = ArgParser.parse(Deno.args).setFlagDefs({
help: ["-h", "--help"], help: ["-h", "--help"],
banana: ["-b", "--banana"],
}); });
async importTools() { async importTools() {
@ -42,7 +43,9 @@ export class PdfToolsCli {
private async banana() { private async banana() {
const asciiArt = await getAsciiArt("banana"); const asciiArt = await getAsciiArt("banana");
this.closeMessage = colorize(asciiArt, "yellow"); const body = this.terminalLayout.getBlock("body");
body.clearAll();
cliLog(colorize(asciiArt, "yellow"), body);
} }
private async help() { private async help() {
@ -61,11 +64,6 @@ export class PdfToolsCli {
this.terminalLayout.register("title", titleBlock); this.terminalLayout.register("title", titleBlock);
const bodyBlock = new TerminalBlock(); const bodyBlock = new TerminalBlock();
this.terminalLayout.register("body", bodyBlock); this.terminalLayout.register("body", bodyBlock);
if (this.args.getFlag("banana")) {
titleBlock.setFixedHeight(0);
await this.banana();
return;
}
if (this.args.getFlag("help") && !this.args.task) { if (this.args.getFlag("help") && !this.args.task) {
await this.help(); await this.help();
return; return;
@ -77,15 +75,11 @@ export class PdfToolsCli {
await this.runTool(toCase(task, "title")); await this.runTool(toCase(task, "title"));
} }
} finally { } finally {
this.cleanup();
}
}
private cleanup() {
this.terminalLayout.clearAll(); this.terminalLayout.clearAll();
Deno.stdin.setRaw(false); Deno.stdin.setRaw(false);
if (this.closeMessage) console.log(this.closeMessage); if (this.closeMessage) console.log(this.closeMessage);
} }
}
private async toolMenu() { private async toolMenu() {
const tools = this.tools.keys().toArray(); const tools = this.tools.keys().toArray();

View File

@ -1,5 +1,4 @@
// deno-lint-disable-must-await-calls // deno-lint-disable-must-await-calls
import { log } from "util/logfile.ts";
import { Cursor } from "./cursor.ts"; import { Cursor } from "./cursor.ts";
import { colorize } from "./style.ts"; import { colorize } from "./style.ts";
import { TerminalBlock, TerminalLayout } from "./TerminalLayout.ts"; import { TerminalBlock, TerminalLayout } from "./TerminalLayout.ts";
@ -10,7 +9,6 @@ export async function cliPrompt(
): Promise<string> { ): Promise<string> {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const input: string[] = []; const input: string[] = [];
let cursorPos = 0;
await Deno.stdin.setRaw(true); await Deno.stdin.setRaw(true);
@ -24,84 +22,33 @@ export async function cliPrompt(
Deno.stdout.writeSync(encoder.encode(message + " ")); Deno.stdout.writeSync(encoder.encode(message + " "));
} }
const render = () => { const buf = new Uint8Array(1);
const line = message + " " + input.join("");
const moveTo = `\x1b[${message.length + 2 + cursorPos}G`;
if (block) {
block.setPostRenderAction(function () {
Deno.stdout.writeSync(
encoder.encode(`\x1b[${this["lastRenderRow"]};1H`),
);
Deno.stdout.writeSync(encoder.encode(moveTo));
});
range = block.setLines([line], range);
} else {
Deno.stdout.writeSync(encoder.encode("\x1b[K" + line + moveTo));
}
};
render();
const buf = new Uint8Array(64); // large enough for most pastes
inputLoop:
while (true) { while (true) {
const n = await Deno.stdin.read(buf); const n = await Deno.stdin.read(buf);
if (n === null) break; if (n === null) break;
const byte = buf[0];
for (let i = 0; i < n; i++) { if (byte === 3) { // Ctrl+C
const byte = buf[i]; Deno.stdin.setRaw(false);
// Ctrl+C
if (byte === 3) {
block?.clear();
block?.["layout"]?.clearAll(); block?.["layout"]?.clearAll();
await Deno.stdin.setRaw(false); block?.clear();
Deno.exit(130); Deno.exit(130);
} }
if (byte === 13) { // Enter if (byte === 13) { // Enter
break inputLoop; break;
} else if (byte === 127 || byte === 8) { // Backspace
input.pop();
} else if (byte >= 32 && byte <= 126) { // Printable chars
input.push(String.fromCharCode(byte));
} }
// Escape sequence? const line = message + " " + input.join("");
if (byte === 27 && i + 1 < n && buf[i + 1] === 91) { if (block) {
const third = buf[i + 2]; range = block.setLines([line], range);
if (third === 68 && cursorPos > 0) cursorPos--; // Left } else {
else if (third === 67 && cursorPos < input.length) cursorPos++; // Right Deno.stdout.writeSync(encoder.encode("\r\x1b[K" + line));
else if (third === 51 && i + 3 < n && buf[i + 3] === 126) { // Delete
if (cursorPos < input.length) input.splice(cursorPos, 1);
i += 1; // consume tilde
} }
i += 2; // consume ESC [ X
continue;
}
// Backspace
if (byte === 127 || byte === 8) {
if (cursorPos > 0) {
input.splice(cursorPos - 1, 1);
cursorPos--;
}
continue;
}
// Delete (ASCII 46)
if (byte === 46 && cursorPos < input.length) {
input.splice(cursorPos, 1);
continue;
}
// Printable
if (byte >= 32 && byte <= 126) {
input.splice(cursorPos, 0, String.fromCharCode(byte));
cursorPos++;
}
// Other cases: ignore
}
render();
} }
await Deno.stdin.setRaw(false); await Deno.stdin.setRaw(false);
@ -145,18 +92,13 @@ if (import.meta.main) {
const layout = new TerminalLayout(); const layout = new TerminalLayout();
const title = new TerminalBlock(); const title = new TerminalBlock();
const block = new TerminalBlock(); const block = new TerminalBlock();
const footer = new TerminalBlock();
block.setPreserveHistory(true); block.setPreserveHistory(true);
// ScrollManager.enable(block); // ScrollManager.enable(block);
title.setLines(["Hello, World!"]); title.setLines(["Hello, World!"]);
title.setFixedHeight(1); title.setFixedHeight(1);
footer.setLines(["Press Ctrl+C to exit"]);
footer.setFixedHeight(1);
layout.register("title", title); layout.register("title", title);
layout.register("block", block); layout.register("block", block);
layout.register("footer", footer);
Deno.addSignalListener("SIGINT", () => { Deno.addSignalListener("SIGINT", () => {
layout.clearAll(); layout.clearAll();

View File

@ -1,10 +1,10 @@
{ {
"name": "@bearmetal/pdf-tools", "name": "@bearmetal/pdf-tools",
"version": "1.0.7", "version": "1.0.0",
"license": "GPL 3.0", "license": "GPL 3.0",
"tasks": { "tasks": {
"dev": "deno run -A --env-file=.env main.ts", "dev": "deno run -A --env-file=.env --watch main.ts",
"compile": "deno compile -o pdf-tools.exe --target x86_64-pc-windows-msvc --include ./asciiart.txt -A ./main.ts", "compile": "deno compile -o compare-form-fields.exe --target x86_64-pc-windows-msvc -R ./main.ts",
"install": "deno install -fgq --import-map ./deno.json -n checkfields -R ./main.ts", "install": "deno install -fgq --import-map ./deno.json -n checkfields -R ./main.ts",
"debug": "deno run -A --env-file=.env --inspect-wait --watch main.ts" "debug": "deno run -A --env-file=.env --inspect-wait --watch main.ts"
}, },
@ -12,8 +12,7 @@
"@std/assert": "jsr:@std/assert@1", "@std/assert": "jsr:@std/assert@1",
"@std/path": "jsr:@std/path@^1.0.9", "@std/path": "jsr:@std/path@^1.0.9",
"pdf-lib": "npm:pdf-lib@^1.17.1", "pdf-lib": "npm:pdf-lib@^1.17.1",
"util/": "./util/", "util/": "./util/"
"@/": "./"
}, },
"exports": { "exports": {
".": "./main.ts" ".": "./main.ts"

View File

@ -7,7 +7,6 @@ import { colorize } from "../cli/style.ts";
import { cliAlert, cliLog, cliPrompt } from "../cli/prompts.ts"; import { cliAlert, cliLog, cliPrompt } from "../cli/prompts.ts";
import { multiSelectMenuInteractive } from "../cli/selectMenu.ts"; import { multiSelectMenuInteractive } from "../cli/selectMenu.ts";
import type { callback, ITool } from "../types.ts"; import type { callback, ITool } from "../types.ts";
import { toCase } from "util/caseManagement.ts";
async function renameFields( async function renameFields(
path: string, path: string,
@ -70,42 +69,13 @@ function applyRename(
} }
} }
/***
* Evaluates the change string with the match array
*
* @description The change string can include the following variables:
*
* - $<int> - capture groups, indexed from 1
* - $<int>i - capture groups, indexed from 1, transforming an integer to an index
* - $<int>s - capture groups, indexed from 1, transforming a string to snake case
* - $<int>c - capture groups, indexed from 1, transforming a string to camel case
* - $<int>l - capture groups, indexed from 1, transforming a string to lower case
* - $<int>u - capture groups, indexed from 1, transforming a string to upper case
* - $<int>t - capture groups, indexed from 1, transforming a string to title case
*/
function evaluateChange(change: string, match: RegExpExecArray) { function evaluateChange(change: string, match: RegExpExecArray) {
return change.replace( return change.replace(
/\$(\d+)([icslut]?)/g, /\$(\d+)(i?)/g,
(_, i, indexed) => { (_, i, indexed) =>
switch (indexed) { indexed
case "i": ? (parseInt(match[i]) ? (parseInt(match[i]) - 1).toString() : match[i])
return (parseInt(match[i]) : match[i],
? (parseInt(match[i]) - 1).toString()
: match[i]);
case "s":
return toCase(match[i], "snake");
case "c":
return toCase(match[i], "camel");
case "t":
return toCase(match[i], "title");
case "l":
return match[i].toLowerCase();
case "u":
return match[i].toUpperCase();
default:
return match[i];
}
},
); );
} }
@ -133,19 +103,13 @@ class RenameFields implements ITool {
[pdfPath, pattern, change] = await forceArgs( [pdfPath, pattern, change] = await forceArgs(
[pdfPath, pattern, change], [pdfPath, pattern, change],
[ [
[ ["Please provide path to PDF:", (p) => !!p && p.endsWith(".pdf")],
"Please provide path to PDF (comma separated for multiple):",
(p) => !!p && p.endsWith(".pdf"),
],
"Please provide search string:", "Please provide search string:",
"Please provide requested change:", "Please provide requested change:",
], ],
this.block, this.block,
); );
const paths = pdfPath.split(",");
for (const pdfPath of paths) {
const patternRegex = new RegExp(pattern); const patternRegex = new RegExp(pattern);
const pdf = await loadPdf(pdfPath); const pdf = await loadPdf(pdfPath);
@ -153,19 +117,16 @@ class RenameFields implements ITool {
const fields = form.getFields(); const fields = form.getFields();
const foundUpdates: [string, callback][] = []; const foundUpdates: [string, callback][] = [];
let changesMade = false;
for (const field of fields) { for (const field of fields) {
const name = field.getName(); const name = field.getName();
const match = patternRegex.exec(name); const match = patternRegex.exec(name);
if (match) { if (match) {
const toChange = evaluateChange(change, match); const toChange = evaluateChange(change, match);
const preview = name.replace(new RegExp(patternRegex), toChange);
foundUpdates.push([ foundUpdates.push([
`${colorize(name, "red")} -> ${colorize(preview, "green")}`, `${colorize(name, "red")} -> ${colorize(toChange, "green")}`,
() => { () => {
applyRename(field, name, patternRegex, toChange); applyRename(field, name, patternRegex, toChange);
changesMade = true;
}, },
]); ]);
} }
@ -180,16 +141,11 @@ class RenameFields implements ITool {
); );
} }
if (changesMade) {
const path = await cliPrompt( const path = await cliPrompt(
"Save to path (or hit enter to keep current):", "Save to path (or hit enter to keep current):",
this.block, this.block,
); );
await savePdf(pdf, path || pdfPath); await savePdf(pdf, path || pdfPath);
} else {
cliLog("No changes made, skipping", this.block);
}
}
} }
} }
export default new RenameFields(); export default new RenameFields();

View File

@ -1,19 +1,12 @@
import { log } from "./logfile.ts";
import { join } from "@std/path";
export async function getAsciiArt(art: string) { export async function getAsciiArt(art: string) {
try { const artFilePath = Deno.env.get("BEARMETAL_ASCII_PATH") ||
const artFilePath = getBearmetalAsciiPath();
Deno.env.get("BEARMETAL_ASCII_PATH") || import.meta.dirname if (!artFilePath) return art;
? join(import.meta.dirname || "", "../asciiart.txt")
: "https://git.cyborggrizzly.com/BearMetal/pdf-tools/raw/branch/main/asciiart.txt";
let artFileText: string; let artFileText: string;
if (artFilePath?.startsWith("http")) { if (artFilePath.startsWith("http")) {
artFileText = await fetch(artFilePath).then((res) => res.text()); artFileText = await fetch(artFilePath).then((res) => res.text());
} else { } else {
artFileText = await Deno.readTextFile( artFileText = await Deno.readTextFile(artFilePath);
artFilePath,
);
} }
const parserRX = /begin\s+(\w+)\s*\n([\s\S]*?)\s*end\s*/g; const parserRX = /begin\s+(\w+)\s*\n([\s\S]*?)\s*end\s*/g;
let result = parserRX.exec(artFileText); let result = parserRX.exec(artFileText);
@ -23,9 +16,15 @@ export async function getAsciiArt(art: string) {
if (name === art) return artText; if (name === art) return artText;
result = parserRX.exec(artFileText); result = parserRX.exec(artFileText);
} }
} catch (e) {
console.log(e);
alert();
}
return art; return art;
} }
function getBearmetalAsciiPath() {
const filenameRX = /asciiarts?\.txt$/;
for (const filename of Deno.readDirSync(".")) {
if (filename.isFile && filenameRX.test(filename.name)) {
return filename.name;
}
}
return null;
}

View File

@ -11,45 +11,9 @@ function lowerToTrainCase(str: string) {
); );
} }
/** function lowerToCamelCase(str: string) {
* @param str return str.trim().replace(/(?:\s)\w/g, (match) => match.toUpperCase())
* @returns camelCased string (single letter words are lower cased, e.g. SSN -> ssn) .replaceAll(" ", "");
*/
function lowerToCamelCase(str: string): string {
const words = str.trim().split(/\s+/);
const result: string[] = [];
let i = 0;
while (i < words.length) {
if (words[i].length === 1) {
// Weve hit the start of a chain of single-letter words
let j = i;
while (j < words.length && words[j].length === 1) {
j++;
}
const chainIsAtStart = i === 0;
// Process that entire chain
for (let k = i; k < j; k++) {
result[k] = chainIsAtStart
? words[k].toLowerCase()
: words[k].toUpperCase();
}
i = j;
} else {
// Normal multi-letter word
if (i === 0) {
// first word: all lower
result[i] = words[i].toLowerCase();
} else {
// subsequent words: capitalize first letter
result[i] = words[i][0].toUpperCase() +
words[i].slice(1).toLowerCase();
}
i++;
}
}
return result.join("");
} }
function lowerToSnakeCase(str: string) { function lowerToSnakeCase(str: string) {
@ -124,10 +88,10 @@ function coerceCaseToLower(str: string, caseType: CaseType) {
case "macro": case "macro":
case "snake": case "snake":
case "upper": case "upper":
return str.replaceAll("_", " ").toLowerCase(); return str.replace("_", " ").toLowerCase();
case "train": case "train":
case "kebab": case "kebab":
return str.replaceAll("-", " ").toLowerCase(); return str.replace("-", " ").toLowerCase();
default: default:
return str.toLowerCase(); return str.toLowerCase();
} }
@ -160,7 +124,3 @@ export function toCase(str: string, toCase: CaseType) {
return str; return str;
} }
} }
if (import.meta.main) {
console.log(toCase("SSN", "camel"));
}