From 19eaf2d66443a98a070f71bcd9247345a9d826cd Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 20 May 2025 09:55:52 -0600 Subject: [PATCH 1/4] feat: field rename multiple files --- README.md | 3 +- tools/fieldRename.ts | 69 ++++++++++++++++++++++++-------------------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 82be0dd..2b194b6 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ Deno >=2.2 (not required if downloading .exe) ### Deno install -`deno task install` -> installs as global command `checkfields` +`deno install -g --allow-read --allow-write --allow-net --allow-env jsr:@bearmetal/pdf-tools` +-> installs as global command `pdf-tools` ### Compile diff --git a/tools/fieldRename.ts b/tools/fieldRename.ts index dedda92..e99b4e9 100644 --- a/tools/fieldRename.ts +++ b/tools/fieldRename.ts @@ -103,50 +103,57 @@ class RenameFields implements ITool { [pdfPath, pattern, change] = await forceArgs( [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 requested change:", ], this.block, ); - const patternRegex = new RegExp(pattern); + const paths = pdfPath.split(","); - const pdf = await loadPdf(pdfPath); - const form = pdf.getForm(); - const fields = form.getFields(); + for (const pdfPath of paths) { + const patternRegex = new RegExp(pattern); - const foundUpdates: [string, callback][] = []; + const pdf = await loadPdf(pdfPath); + const form = pdf.getForm(); + const fields = form.getFields(); - for (const field of fields) { - const name = field.getName(); - const match = patternRegex.exec(name); - if (match) { - const toChange = evaluateChange(change, match); - const preview = name.replace(new RegExp(patternRegex), toChange); - foundUpdates.push([ - `${colorize(name, "red")} -> ${colorize(preview, "green")}`, - () => { - applyRename(field, name, patternRegex, toChange); - }, - ]); + const foundUpdates: [string, callback][] = []; + + for (const field of fields) { + const name = field.getName(); + const match = patternRegex.exec(name); + if (match) { + const toChange = evaluateChange(change, match); + const preview = name.replace(new RegExp(patternRegex), toChange); + foundUpdates.push([ + `${colorize(name, "red")} -> ${colorize(preview, "green")}`, + () => { + applyRename(field, name, patternRegex, toChange); + }, + ]); + } } - } - if (foundUpdates.length) { - cliLog("Found updates:", this.block); - await multiSelectMenuInteractive( - "Please select an option to apply", - foundUpdates, - { terminalBlock: this.block }, + if (foundUpdates.length) { + cliLog("Found updates:", this.block); + await multiSelectMenuInteractive( + "Please select an option to apply", + foundUpdates, + { terminalBlock: this.block }, + ); + } + + const path = await cliPrompt( + "Save to path (or hit enter to keep current):", + this.block, ); + await savePdf(pdf, path || pdfPath); } - - const path = await cliPrompt( - "Save to path (or hit enter to keep current):", - this.block, - ); - await savePdf(pdf, path || pdfPath); } } export default new RenameFields(); -- 2.47.2 From e5b173155a78729e58fc33e14f302c554f9035c0 Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 20 May 2025 10:22:49 -0600 Subject: [PATCH 2/4] feat: change evaluation now adds case transformation for capture groups --- tools/fieldRename.ts | 40 ++++++++++++++++++++++++++++----- util/caseManagement.ts | 50 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/tools/fieldRename.ts b/tools/fieldRename.ts index e99b4e9..9fb6868 100644 --- a/tools/fieldRename.ts +++ b/tools/fieldRename.ts @@ -7,6 +7,7 @@ import { colorize } from "../cli/style.ts"; import { cliAlert, cliLog, cliPrompt } from "../cli/prompts.ts"; import { multiSelectMenuInteractive } from "../cli/selectMenu.ts"; import type { callback, ITool } from "../types.ts"; +import { toCase } from "util/caseManagement.ts"; async function renameFields( path: string, @@ -69,13 +70,42 @@ function applyRename( } } +/*** + * Evaluates the change string with the match array + * + * @description The change string can include the following variables: + * + * - $ - capture groups, indexed from 1 + * - $i - capture groups, indexed from 1, transforming an integer to an index + * - $s - capture groups, indexed from 1, transforming a string to snake case + * - $c - capture groups, indexed from 1, transforming a string to camel case + * - $l - capture groups, indexed from 1, transforming a string to lower case + * - $u - capture groups, indexed from 1, transforming a string to upper case + * - $t - capture groups, indexed from 1, transforming a string to title case + */ function evaluateChange(change: string, match: RegExpExecArray) { return change.replace( - /\$(\d+)(i?)/g, - (_, i, indexed) => - indexed - ? (parseInt(match[i]) ? (parseInt(match[i]) - 1).toString() : match[i]) - : match[i], + /\$(\d+)([icslut]?)/g, + (_, i, indexed) => { + switch (indexed) { + case "i": + return (parseInt(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]; + } + }, ); } diff --git a/util/caseManagement.ts b/util/caseManagement.ts index d538c68..b5a4566 100644 --- a/util/caseManagement.ts +++ b/util/caseManagement.ts @@ -11,9 +11,45 @@ function lowerToTrainCase(str: string) { ); } -function lowerToCamelCase(str: string) { - return str.trim().replace(/(?:\s)\w/g, (match) => match.toUpperCase()) - .replaceAll(" ", ""); +/** + * @param str + * @returns camelCased string (single letter words are lower cased, e.g. SSN -> ssn) + */ +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) { + // We’ve 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) { @@ -88,10 +124,10 @@ function coerceCaseToLower(str: string, caseType: CaseType) { case "macro": case "snake": case "upper": - return str.replace("_", " ").toLowerCase(); + return str.replaceAll("_", " ").toLowerCase(); case "train": case "kebab": - return str.replace("-", " ").toLowerCase(); + return str.replaceAll("-", " ").toLowerCase(); default: return str.toLowerCase(); } @@ -124,3 +160,7 @@ export function toCase(str: string, toCase: CaseType) { return str; } } + +if (import.meta.main) { + console.log(toCase("SSN", "camel")); +} -- 2.47.2 From 90f1547e02f0a96e4abe629735f0261ca7e037dc Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 20 May 2025 10:53:37 -0600 Subject: [PATCH 3/4] fix: flickering eyebleed --- CHANGELOG.md | 8 ++++++-- cli/TerminalLayout.ts | 48 +++++++++++++++++++++++++++---------------- deno.json | 2 +- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e03926..fe2b542 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog -## v1.0.1 (2025-07-25) +## v1.0.2 (2025-05-20) + + + +## v1.0.1 (2025-05-7) @@ -8,7 +12,7 @@ - help flags can cause issues -## v1.0.0 (2025-07-25) +## v1.0.0 (2025-05-7) ### Features diff --git a/cli/TerminalLayout.ts b/cli/TerminalLayout.ts index a4f400c..4866444 100644 --- a/cli/TerminalLayout.ts +++ b/cli/TerminalLayout.ts @@ -111,6 +111,8 @@ export class TerminalBlock { private renderHeight: number = 0; private lastRenderRow = 1; + private lastRendered: string[] = []; + private preserveHistory = false; constructor(private prepend: string = "") {} @@ -193,27 +195,37 @@ export class TerminalBlock { } renderInternal(startRow?: number) { - this.lastRenderRow = startRow ?? this.lastRenderRow; - this.clear(); // uses old renderedLineCount + const outputLines: string[] = []; - const outputLines = this.renderLines.map((line) => - `${this.prepend}${line}\x1b[K` - ); - 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)); + for (let i = 0; i < this.renderLines.length; i++) { + const line = `${this.prepend}${this.renderLines[i]}`; + const previous = this.lastRendered[i]; + if (line !== previous) { + const moveToLine = `\x1b[${(startRow ?? this.lastRenderRow) + i};1H`; + outputLines.push(moveToLine + line + "\x1b[K"); + } } - // update rendered line count *after* rendering - this.renderedLineCount = outputLines.reduce( - (count, line) => - count + - Math.ceil((line.length) / (Deno.consoleSize().columns || 80)), - 0, - ); + 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 = `[${baseRow + this.renderLines.length + i};1H`; + Deno.stdout.writeSync(new TextEncoder().encode(moveToLine)); + } + + this.lastRendered = [...this.renderLines]; + this.renderedLineCount = this.renderHeight; + this.lastRenderRow = baseRow; + const output = outputLines.join("\n"); + Deno.stdout.writeSync(new TextEncoder().encode(output)); } clear() { diff --git a/deno.json b/deno.json index 84dbf51..c371ac4 100644 --- a/deno.json +++ b/deno.json @@ -3,7 +3,7 @@ "version": "1.0.1", "license": "GPL 3.0", "tasks": { - "dev": "deno run -A --env-file=.env --watch main.ts", + "dev": "deno run -A --env-file=.env 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", "debug": "deno run -A --env-file=.env --inspect-wait --watch main.ts" -- 2.47.2 From cdeef54f681a6c2c75f05771cf0028b2ede047bd Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 20 May 2025 13:45:27 -0600 Subject: [PATCH 4/4] fix: local ASCII Art inclusion --- .gitea/workflows/release.yml | 2 +- deno.json | 7 ++--- util/asciiArt.ts | 51 ++++++++++++++++++------------------ 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 776a15c..dd090c0 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -30,7 +30,7 @@ jobs: uses: https://git.cyborggrizzly.com/bearmetal/ci-actions/deno-release@main with: entrypoint: main.ts - compile-flags: "--allow-read --allow-write --allow-env --allow-net" + compile-flags: "--allow-read --allow-write --allow-env --allow-net --include asciiart.txt" env: GITEA_TOKEN: ${{ secrets.GIT_PAT }} diff --git a/deno.json b/deno.json index c371ac4..b1961f9 100644 --- a/deno.json +++ b/deno.json @@ -1,10 +1,10 @@ { "name": "@bearmetal/pdf-tools", - "version": "1.0.1", + "version": "1.0.2", "license": "GPL 3.0", "tasks": { "dev": "deno run -A --env-file=.env main.ts", - "compile": "deno compile -o compare-form-fields.exe --target x86_64-pc-windows-msvc -R ./main.ts", + "compile": "deno compile -o pdf-tools.exe --target x86_64-pc-windows-msvc --include ./asciiart.txt -A ./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" }, @@ -12,7 +12,8 @@ "@std/assert": "jsr:@std/assert@1", "@std/path": "jsr:@std/path@^1.0.9", "pdf-lib": "npm:pdf-lib@^1.17.1", - "util/": "./util/" + "util/": "./util/", + "@/": "./" }, "exports": { ".": "./main.ts" diff --git a/util/asciiArt.ts b/util/asciiArt.ts index 2c0a590..aaaacc6 100644 --- a/util/asciiArt.ts +++ b/util/asciiArt.ts @@ -1,30 +1,29 @@ -export async function getAsciiArt(art: string) { - const artFilePath = Deno.env.get("BEARMETAL_ASCII_PATH") || - getBearmetalAsciiPath(); - if (!artFilePath) return art; - let artFileText: string; - if (artFilePath.startsWith("http")) { - artFileText = await fetch(artFilePath).then((res) => res.text()); - } else { - artFileText = await Deno.readTextFile(artFilePath); - } - const parserRX = /begin\s+(\w+)\s*\n([\s\S]*?)\s*end\s*/g; - let result = parserRX.exec(artFileText); +import { log } from "./logfile.ts"; +import { join } from "@std/path"; - while (result !== null) { - const [_, name, artText] = result; - if (name === art) return artText; - result = parserRX.exec(artFileText); +export async function getAsciiArt(art: string) { + try { + const artFilePath = Deno.env.get("BEARMETAL_ASCII_PATH") || + join(import.meta.dirname || "", "../asciiart.txt"); + let artFileText: string; + if (artFilePath?.startsWith("http")) { + artFileText = await fetch(artFilePath).then((res) => res.text()); + } else { + artFileText = await Deno.readTextFile( + artFilePath, + ); + } + const parserRX = /begin\s+(\w+)\s*\n([\s\S]*?)\s*end\s*/g; + let result = parserRX.exec(artFileText); + + while (result !== null) { + const [_, name, artText] = result; + if (name === art) return artText; + result = parserRX.exec(artFileText); + } + } catch (e) { + console.log(e); + alert(); } 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 "https://git.cyborggrizzly.com/BearMetal/pdf-tools/raw/branch/main/asciiart.txt"; -} -- 2.47.2