From 7d42920dcb0b4a167869076f9d38fa5db1c020a2 Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 24 Apr 2025 20:27:09 -0600 Subject: [PATCH] initial cli api, some movement on tool selection --- asciiart.txt | 32 +++++++ call.ts | 47 ---------- cli/TerminalLayout.ts | 201 +++++++++++++++++++++++++++++++++++++++++ cli/argParser.ts | 33 +++++++ cli/colorize.ts | 21 +++++ cli/index.ts | 64 +++++++++++++ cli/selectMenu.ts | 186 ++++++++++++++++++++++++++++++++++++++ deno.json | 10 +- fieldRename.ts | 82 ----------------- main.ts | 32 +------ saveLoadPdf.ts | 16 ---- tools/checkCode.ts | 59 ++++++++++++ tools/fieldRename.ts | 93 +++++++++++++++++++ types.ts | 10 +- util/asciiArt.ts | 30 ++++++ util/call.ts | 55 +++++++++++ util/caseManagement.ts | 128 ++++++++++++++++++++++++++ util/saveLoadPdf.ts | 19 ++++ 18 files changed, 938 insertions(+), 180 deletions(-) create mode 100644 asciiart.txt delete mode 100644 call.ts create mode 100644 cli/TerminalLayout.ts create mode 100644 cli/argParser.ts create mode 100644 cli/colorize.ts create mode 100644 cli/index.ts create mode 100644 cli/selectMenu.ts delete mode 100644 fieldRename.ts delete mode 100644 saveLoadPdf.ts create mode 100644 tools/checkCode.ts create mode 100644 tools/fieldRename.ts create mode 100644 util/asciiArt.ts create mode 100644 util/call.ts create mode 100644 util/caseManagement.ts create mode 100644 util/saveLoadPdf.ts diff --git a/asciiart.txt b/asciiart.txt new file mode 100644 index 0000000..696ab31 --- /dev/null +++ b/asciiart.txt @@ -0,0 +1,32 @@ +begin bearmetal +__________ _____ __ .__ +\______ \ ____ _____ _______ / \ _____/ |______ | | + | | _// __ \\__ \\_ __ \/ \ / \_/ __ \ __\__ \ | | + | | \ ___/ / __ \| | \/ Y \ ___/| | / __ \| |__ + |______ /\___ >____ /__| \____|__ /\___ >__| (____ /____/ + \/ \/ \/ \/ \/ \/ +end + +begin pdftools + _____ ____ _____ _____ _____ _____ __ _____ +| _ | \| __|___|_ _| | | | | __| +| __| | | __|___| | | | | | | | |__|__ | +|__| |____/|__| |_| |_____|_____|_____|_____| +end + +begin banana + _ +//\ +V \ + \ \_ + \,'.`-. + |\ `. `. + ( \ `. `-. _,.-:\ + \ \ `. `-._ __..--' ,-';/ + \ `. `-. `-..___..---' _.--' ,'/ + `. `. `-._ __..--' ,' / + `. `-_ ``--..'' _.-' ,' + `-_ `-.___ __,--' ,' + `-.__ `----""" __.-' + `--..____..--' +end diff --git a/call.ts b/call.ts deleted file mode 100644 index b89aff3..0000000 --- a/call.ts +++ /dev/null @@ -1,47 +0,0 @@ -type transformer = (arg: string) => any; -interface IConfig { - multiTransform?: boolean; -} - -export async function call(tool: Tool, conf?: transformer | IConfig, ...transforms: transformer[]) { - const config: IConfig = {} - - if (typeof conf === 'object') { - Object.assign(config, conf) - } else { - transforms.unshift(conf as transformer) - } - - const args = Deno.args; - const shouldPair = transforms.length === args.length; - const multiTransform = config.multiTransform || !shouldPair && transforms.length > 1; - - const transformedArgs = args.map((arg, i) => { - if (shouldPair) return transforms[i](arg); - if (multiTransform) return transforms.reduce((a, b) => b(a), arg) - return transforms[0] ? transforms[0](arg) : arg - }) - - await tool(...transformedArgs as T) -} - -type prompt = [string, (v?: string) => boolean] | string - -export async function callWithArgPrompt(tool: Tool, prompts: prompt[]) { - function buildPromptTransform(p: prompt): transformer { - let validation = (v?: string) => !!v; - let pText = p as string; - - if (Array.isArray(p)) { - [pText, validation] = p; - } - - return (a: string) => { - while (!validation(a)) { - a = prompt(pText) || '' - } - } - } - - await call(tool, ...prompts.map(buildPromptTransform)) -} \ No newline at end of file diff --git a/cli/TerminalLayout.ts b/cli/TerminalLayout.ts new file mode 100644 index 0000000..0703fd4 --- /dev/null +++ b/cli/TerminalLayout.ts @@ -0,0 +1,201 @@ +export class TerminalLayout { + private static ALT_BUFFER_ENABLE = "\x1b[?1049h"; + private static ALT_BUFFER_DISABLE = "\x1b[?1049l"; + private static CURSOR_HIDE = "\x1b[?25l"; + private static CURSOR_SHOW = "\x1b[?25h"; + private blocks: Record = {}; + private layoutOrder: string[] = []; + private height: number; + private debounceTimer: number | null = null; + private debounceDelay = 10; // ms + + constructor() { + Deno.stdout.writeSync( + new TextEncoder().encode( + TerminalLayout.ALT_BUFFER_ENABLE + TerminalLayout.CURSOR_HIDE, + ), + ); + this.height = Deno.consoleSize().rows; + } + + register(name: string, block: TerminalBlock, fixedHeight?: number) { + this.blocks[name] = block; + this.layoutOrder.push(name); + block.setLayout(this); + if (fixedHeight !== undefined) { + block.setFixedHeight(fixedHeight); + } + } + + requestRender() { + if (this.debounceTimer !== null) { + clearTimeout(this.debounceTimer); + } + this.debounceTimer = setTimeout( + () => this.renderLayout(), + this.debounceDelay, + ); + } + + renderLayout() { + let usedLines = 0; + const totalHeight = this.height; + const flexBlocks = this.layoutOrder.filter((name) => + !this.blocks[name].isFixedHeight() + ); + const remainingHeight = totalHeight - + this.layoutOrder.reduce((sum, name) => { + const b = this.blocks[name]; + return sum + (b.isFixedHeight() ? b.getFixedHeight() : 0); + }, 0); + const flexHeight = Math.max( + 0, + Math.floor(remainingHeight / flexBlocks.length), + ); + + for (const name of this.layoutOrder) { + const block = this.blocks[name]; + const height = block.isFixedHeight() + ? block.getFixedHeight() + : flexHeight; + const lines = block.getRenderedLines(height); + block.setRenderHeight(height); + block.setRenderLines(lines); + block.renderInternal(usedLines + 1); + usedLines += lines.length; + } + } + + clearAll() { + Deno.stdout.writeSync( + new TextEncoder().encode( + TerminalLayout.ALT_BUFFER_DISABLE + TerminalLayout.CURSOR_SHOW, + ), + ); + for (const name of this.layoutOrder) { + this.blocks[name].clear(); + } + } + + get availableHeight() { + return this.height; + } +} + +export class TerminalBlock { + private lines: string[] = []; + private renderLines: string[] = []; + private renderedLineCount = 0; + private layout?: TerminalLayout; + private fixedHeight?: number; + private scrollOffset = 0; + private renderHeight: number = 0; + private lastRenderRow = 1; + + constructor(private prepend: string = "") {} + + setLayout(layout: TerminalLayout) { + this.layout = layout; + } + + setLines(lines: string[]) { + this.lines = lines; + if (this.scrollOffset > lines.length - 1) { + this.scrollOffset = Math.max(0, lines.length - 1); + } + if (this.layout) { + this.layout.requestRender(); + } else { + this.setRenderLines( + this.getRenderedLines(this.fixedHeight || this.renderHeight), + ); + this.renderInternal(); + } + } + + scrollTo(offset: number) { + this.scrollOffset = Math.max(0, Math.min(offset, this.lines.length - 1)); + if (this.layout) { + this.layout.requestRender(); + } + } + + scrollBy(delta: number) { + this.scrollTo(this.scrollOffset + delta); + } + + atTop(): boolean { + return this.scrollOffset === 0; + } + + atBottom(): boolean { + const visibleHeight = this.renderedLineCount || this.fixedHeight || + this.lines.length; + return this.scrollOffset + visibleHeight >= this.lines.length; + } + + getRenderedLines(maxHeight: number): string[] { + return this.lines.slice(this.scrollOffset, this.scrollOffset + maxHeight); + } + + setRenderLines(lines: string[]) { + this.renderLines = lines; + this.renderedLineCount = lines.reduce( + (count, line) => + count + + Math.ceil( + (this.prepend.length + line.length) / + (Deno.consoleSize().columns || 80), + ), + 0, + ); + } + + setRenderHeight(height: number) { + this.renderHeight = height; + } + + getRenderHeight(): number { + return this.renderHeight; + } + + renderInternal(startRow?: number) { + this.lastRenderRow = startRow ?? this.lastRenderRow; + this.clear(); + let output = this.renderLines.map((line) => `${this.prepend}${line}\x1b[K`) + .join("\n"); + if (startRow !== undefined) { + const moveCursor = `\x1b[${startRow};1H`; + output = moveCursor + output; + } + Deno.stdout.writeSync( + new TextEncoder().encode(output), + ); + } + + clear() { + if (this.renderedLineCount === 0) return; + const moveCursor = `\x1b[${this.lastRenderRow};1H`; + Deno.stdout.writeSync(new TextEncoder().encode(moveCursor)); + for (let i = 0; i < this.renderedLineCount; i++) { + Deno.stdout.writeSync(new TextEncoder().encode(`\x1b[2K\x1b[1E`)); + } + this.renderedLineCount = 0; + } + + setFixedHeight(height: number) { + this.fixedHeight = height; + } + + isFixedHeight(): boolean { + return this.fixedHeight !== undefined; + } + + getFixedHeight(): number { + return this.fixedHeight ?? 0; + } + + get lineCount() { + return this.renderLines.length; + } +} diff --git a/cli/argParser.ts b/cli/argParser.ts new file mode 100644 index 0000000..842378b --- /dev/null +++ b/cli/argParser.ts @@ -0,0 +1,33 @@ +export class ArgParser { + private args: string[]; + + constructor(args: string[]) { + this.args = args; + } + + public get(key: string) { + const index = this.args.indexOf(key); + if (index === -1) return null; + return this.args[index + 1]; + } + + get flags() { + return this.args.filter((arg) => arg.startsWith("-")); + } + + get nonFlags() { + return this.args.filter((arg) => !arg.startsWith("-")); + } + + get namedArgs() { + return this.args.filter((arg) => arg.startsWith("--")); + } + + get task() { + return this.nonFlags[0]; + } + + static parse(args: string[]) { + return new ArgParser(args); + } +} diff --git a/cli/colorize.ts b/cli/colorize.ts new file mode 100644 index 0000000..026fdec --- /dev/null +++ b/cli/colorize.ts @@ -0,0 +1,21 @@ +const colorMap = { + purple: "\x1b[35m", + porple: "\x1b[38;2;150;0;200m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + cyan: "\x1b[36m", + white: "\x1b[37m", + gray: "\x1b[90m", + get grey() { + return this.gray; + }, +}; + +export function colorize(text: string, color?: keyof typeof colorMap | string) { + if (!color) return text; + const c = colorMap[color as keyof typeof colorMap]; + if (!c) return text; + return `${c}${text}\x1b[0m`; +} diff --git a/cli/index.ts b/cli/index.ts new file mode 100644 index 0000000..814fa2a --- /dev/null +++ b/cli/index.ts @@ -0,0 +1,64 @@ +import { getAsciiArt } from "../util/asciiArt.ts"; +import { toCase } from "util/caseManagement.ts"; +import { ArgParser } from "./argParser.ts"; +import { colorize } from "./colorize.ts"; +import { selectMenuInteractive } from "./selectMenu.ts"; + +export class PdfToolsCli { + private tools: Map = new Map(); + + async importTools(tools?: string) { + tools = tools?.replace(/\/$/, "").replace(/^\.?\//, "") || "tools"; + for (const toolfile of Deno.readDirSync(tools)) { + if (toolfile.isFile) { + const tool = await import( + Deno.cwd() + "/" + tools + "/" + toolfile.name + ); + if (tool.default) { + this.tools.set( + toCase(toolfile.name.replace(".ts", ""), "title"), + tool.default, + ); + } + } + } + } + + private async banana() { + const asciiArt = await getAsciiArt("banana"); + console.log(colorize(asciiArt, "yellow")); + } + + private help() { + console.log("BearMetal PDF CLI"); + } + + public async run() { + console.clear(); + let lineCount = 0; + for (const t of ["bearmetal:porple", "pdftools:cyan"]) { + const [name, color] = t.split(":"); + const asciiArt = await getAsciiArt(name); + console.log(colorize(asciiArt, color)); + lineCount += asciiArt.split("\n").length; + } + if (Deno.args.length === 0) { + console.log( + colorize("No tool specified. Importing all tools...", "gray"), + ); + await this.importTools(); + } + const args = ArgParser.parse(Deno.args); + this.toolMenu(Deno.consoleSize().rows - lineCount); + } + + private async toolMenu(space?: number) { + const tools = this.tools.keys().toArray(); + const selected = await selectMenuInteractive("Choose a tool", tools); + if (!selected) return; + const tool = this.tools.get(toCase(selected, "camel")); + if (tool) { + await tool.run(); + } + } +} diff --git a/cli/selectMenu.ts b/cli/selectMenu.ts new file mode 100644 index 0000000..ec10640 --- /dev/null +++ b/cli/selectMenu.ts @@ -0,0 +1,186 @@ +import { colorize } from "./colorize.ts"; +import { TerminalBlock, TerminalLayout } from "./TerminalLayout.ts"; + +interface ISelectMenuConfig { + multiSelect?: boolean; + terminalBlock?: TerminalBlock; +} + +export function selectMenu(items: string[]) { + const menu = items.map((i, index) => `${index + 1}. ${i}`).join("\n"); + console.log(menu); + const index = parseInt(prompt("Please select an option:") || "1") - 1; + return items[index]; +} + +export async function selectMenuInteractive( + q: string, + options: string[], + config?: ISelectMenuConfig, +): Promise { + Deno.stdin.setRaw(true); + let selected = 0; + + if (config?.multiSelect) { + console.warn("Multi-select not implemented yet"); + return null; + } + + const terminalBlock = config?.terminalBlock || new TerminalBlock(); + if (!config?.terminalBlock) { + terminalBlock.setRenderHeight(Deno.consoleSize().rows); + } + + console.log(terminalBlock.getRenderHeight()); + + function renderMenu() { + const { rows } = Deno.consoleSize(); + const terminalHeight = terminalBlock.getRenderHeight() || rows; + const maxHeight = Math.min(terminalHeight - 1, options.length); + let startPoint = Math.max(0, selected - Math.floor(maxHeight / 2)); + const endPoint = Math.min(options.length, startPoint + maxHeight); + if (endPoint - startPoint < maxHeight) { + startPoint = Math.max(0, options.length - maxHeight); + } + + const lines: string[] = []; + lines.push(colorize(q, "green")); + for (let i = startPoint; i < endPoint; i++) { + const option = options[i]; + if (i === selected) { + lines.push(`${numberAndPadding(i, ">")}${colorize(option, "porple")}`); + } else { + lines.push(`${numberAndPadding(i)}${option}`); + } + } + + terminalBlock.setLines(lines); + } + + function numberAndPadding(i: number, prefix?: string) { + const padded = `${i + 1}. `.padStart( + options.length.toString().length + 4, + ); + return prefix ? padded.replace(" ", prefix.substring(0, 1)) : padded; + } + + let inputBuffer = ""; + + // Function to handle input + async function handleInput() { + const buf = new Uint8Array(3); // arrow keys send 3 bytes + while (true) { + renderMenu(); + const n = await Deno.stdin.read(buf); + if (n === null) break; + + const [a, b, c] = buf; + + if (a === 3) { + Deno.stdin.setRaw(false); + console.log("\nInterrupted\n"); + Deno.exit(130); + } + + if (a === 13) { // Enter key + if (inputBuffer) { + const parsed = parseInt(inputBuffer); + if (!isNaN(parsed)) { + selected = parsed - 1; + } + inputBuffer = ""; + } + break; + } else if (a === 27 && b === 91) { // Arrow keys + inputBuffer = ""; + if (c === 65) { // Up + selected = (selected - 1 + options.length) % options.length; + } else if (c === 66) { // Down + selected = (selected + 1) % options.length; + } + } else if (a >= 48 && a <= 57) { + inputBuffer += String.fromCharCode(a); + } else if (a === 8) { + inputBuffer = inputBuffer.slice(0, -1); + } + } + + terminalBlock.clear(); + Deno.stdin.setRaw(false); + return options[selected]; + } + return await handleInput(); +} + +if (import.meta.main) { + // const layout = new TerminalLayout(); + // const block = new TerminalBlock(); + // const titleBlock = new TerminalBlock(); + // const postBlock = new TerminalBlock(); + // titleBlock.setLines(["An incredible fruit menu!"]); + // postBlock.setLines(["I'm here too!"]); + // titleBlock.setFixedHeight(1); + // postBlock.setFixedHeight(1); + // layout.register("title", titleBlock); + // layout.register("block", block); + // layout.register("post", postBlock); + + // const val = await selectMenuInteractive("choose a fruit", [ + // "apple", + // "banana", + // "cherry", + // "date", + // "elderberry", + // "fig", + // "grape", + // "honeydew", + // "ilama", + // "jackfruit", + // "kiwi", + // "lemon", + // "mango", + // "nectarine", + // "orange", + // "papaya", + // "peach", + // "pineapple", + // "pomegranate", + // "quince", + // "raspberry", + // "strawberry", + // "tangerine", + // "watermelon", + // ], { terminalBlock: block }); + // layout.clearAll(); + // console.log(val); + + const val = await selectMenuInteractive("choose a fruit", [ + "apple", + "banana", + "cherry", + "date", + "elderberry", + "fig", + "grape", + "honeydew", + "ilama", + "jackfruit", + "kiwi", + "lemon", + "mango", + "nectarine", + "orange", + "papaya", + "quince", + "raspberry", + "strawberry", + "tangerine", + "udara", + "vogelbeere", + "watermelon", + "ximenia", + "yuzu", + "zucchini", + ]); + console.log(val); +} diff --git a/deno.json b/deno.json index 09e7fc7..a58620e 100644 --- a/deno.json +++ b/deno.json @@ -1,12 +1,14 @@ { "name": "@bearmetal/pdf-tools", "tasks": { - "dev": "deno run -A --watch dev.ts", + "dev": "deno run -A --watch 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" }, "imports": { "@std/assert": "jsr:@std/assert@1", - "pdf-lib": "npm:pdf-lib@^1.17.1" - } -} + "pdf-lib": "npm:pdf-lib@^1.17.1", + "util/": "./util/" + }, + "exports": {} +} \ No newline at end of file diff --git a/fieldRename.ts b/fieldRename.ts deleted file mode 100644 index 4d1a031..0000000 --- a/fieldRename.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { PDFAcroField, PDFHexString, PDFName, PDFString, toHexString } from "pdf-lib"; -import { loadPdfForm, savePdf } from "./saveLoadPdf.ts"; -import { PDFDocument } from "pdf-lib"; -import { call, callWithArgPrompt } from "./call.ts"; - -// const thing = PDFAcroField.prototype.getFullyQualifiedName; -// PDFAcroField.prototype.getFullyQualifiedName = function () { -// const name = thing.call(this) -// // if (name?.includes('langauge')) -// console.log(name) -// return name; -// } - -// const thing = PDFHexString.prototype.copyBytesInto -// PDFHexString.prototype.copyBytesInto = function (buffer: Uint8Array, offset: number) { -// console.log((this as any).value) - -// const result = thing.call(this, buffer, offset) -// return result; -// } - -async function renameFields(path: string, pattern: string | RegExp, change: string) { - if (typeof pattern === 'string') pattern = new RegExp(pattern); - const form = await loadPdfForm(path); - const fields = form.getFields(); - let changesMade = false; - for (const field of fields) { - const name = field.getName(); - if (pattern.test(name)) { - console.log(name + ' %cfound', "color: red"); - const segments = name.split('.') - const matchingSegments = segments.filter(s => pattern.test(s)); - let cField: PDFAcroField | undefined = field.acroField; - while (cField) { - if (cField.getPartialName() && matchingSegments.includes(cField.getPartialName()!)) { - const mName = cField.getPartialName()?.replace(pattern, change) - if (mName) { - changesMade = true; - cField.dict.set(PDFName.of("T"), PDFString.of(mName)) - // console.log(cField.getPartialName()) - } - } - cField = cField.getParent(); - // console.log(cField?.getPartialName()) - } - console.log(field.getName()) - // const newName = name.replace(pattern, change); - // console.log("Change to: %c" + newName, "color: yellow"); - // if (confirm('Ok?')) { - // let parent = field.acroField.getParent(); - // field.acroField.setPartialName(segments.pop()) - // while (parent && segments.length) { - // console.log(parent.getPartialName()) - // parent.setPartialName(segments.pop()) - // parent = parent.getParent(); - // } - // changesMade = true; - // console.log(field.getName()) - // // dict.set(PDFName.of("T"), PDFHexString.fromText(newName)) - // console.log("%cDone!", "color: lime") - // } - // break; - } - } - if (changesMade) { - savePdf(form.doc, path) - } -} - -if (import.meta.main) { - // await call(renameFields) - // while (!path || !path.endsWith('.pdf')) path = prompt("Please provide path to PDF:") || ''; - // while (!pattern) pattern = prompt("Please provide search string:") || ''; - // while (!change) change = prompt("Please provide requested change:") || ''; - await callWithArgPrompt(renameFields, [ - ["Please provide path to PDF:", (p) => !!p && p.endsWith('.pdf')], - "Please provide search string:", - "Please provide requested change:" - ]) -} - - diff --git a/main.ts b/main.ts index d244193..eae7bf6 100644 --- a/main.ts +++ b/main.ts @@ -1,30 +1,4 @@ -import { PDFDocument } from "pdf-lib"; -import { loadPdfForm } from "./saveLoadPdf.ts"; +import { PdfToolsCli } from "./cli/index.ts"; -let [pdfPath, csPath] = Deno.args; - -while (!pdfPath || !pdfPath.endsWith('.pdf')) pdfPath = prompt("Please provide path to PDF file:") || ""; -while (!csPath || !csPath.endsWith('.cs')) csPath = prompt("Please provide path to CS class file:") || ""; - -const form = await loadPdfForm(pdfPath); - -const fields = form.getFields(); -const csFiles = await Promise.all(csPath.split(",").map(c => Deno.readTextFile(c.trim()))); - -const fieldNames: string[] = fields.map(f => f.getName()) - .filter(f => { - const rx = new RegExp(`(? rx.test(c)) - }) - .filter(f => !f.toLowerCase().includes("signature")); - -if (fieldNames.length) { - console.log("%cThe following field names are not present in the CS code", "color: red") - console.log(fieldNames) - alert("Your princess is in another castle...") -} else { - console.log("%cAll form fields present", 'color: lime') - alert("Ok!") -} - -/additionalAdviser.personalInfo.npn\[\?\]/ \ No newline at end of file +const app = new PdfToolsCli(); +app.run(); diff --git a/saveLoadPdf.ts b/saveLoadPdf.ts deleted file mode 100644 index 176b666..0000000 --- a/saveLoadPdf.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PDFDocument } from "pdf-lib"; - -export async function loadPdfForm(path: string) { - const pdfBytes = await Deno.readFile(path); - - const pdfDoc = await PDFDocument.load(pdfBytes); - - const form = pdfDoc.getForm() - return form; -} - -export async function savePdf(doc: PDFDocument, path: string) { - const pdfBytes = await doc.save(); - if (Deno.env.get("DRYRUN")) return - await Deno.writeFile(path, pdfBytes); -} \ No newline at end of file diff --git a/tools/checkCode.ts b/tools/checkCode.ts new file mode 100644 index 0000000..f6fb062 --- /dev/null +++ b/tools/checkCode.ts @@ -0,0 +1,59 @@ +import { PDFDocument } from "pdf-lib"; +import { loadPdfForm } from "../util/saveLoadPdf.ts"; +import { callWithArgPrompt } from "util/call.ts"; + +export async function checkFile(pdfPath: string, csPath: string) { + while (!pdfPath || !pdfPath.endsWith(".pdf")) { + pdfPath = prompt("Please provide path to PDF file:") || ""; + } + while (!csPath || !csPath.endsWith(".cs")) { + csPath = prompt("Please provide path to CS class file:") || ""; + } + + const form = await loadPdfForm(pdfPath); + + const fields = form.getFields(); + const csFiles = await Promise.all( + csPath.split(",").map((c) => Deno.readTextFile(c.trim())), + ); + + const fieldNames: string[] = fields.map((f) => f.getName()) + .filter((f) => { + const rx = new RegExp( + `(? rx.test(c)); + }) + .filter((f) => !f.toLowerCase().includes("signature")); + + if (fieldNames.length) { + console.log( + "%cThe following field names are not present in the CS code", + "color: red", + ); + console.log(fieldNames); + alert("Your princess is in another castle..."); + } else { + console.log("%cAll form fields present", "color: lime"); + alert("Ok!"); + } +} + +class CheckCode implements ITool { + name = "checkcode"; + description = "Checks if form fields are present in CS code"; + help() { + console.log("Usage: checkcode "); + } + async run(...args: string[]) { + await callWithArgPrompt(checkFile, [ + ["Please provide path to PDF file:", (p) => !!p && p.endsWith(".pdf")], + [ + "Please provide path to CS file (comma separated for multiple):", + (p) => !!p && p.endsWith(".cs"), + ], + ], args); + } +} + +export default new CheckCode(); diff --git a/tools/fieldRename.ts b/tools/fieldRename.ts new file mode 100644 index 0000000..633074d --- /dev/null +++ b/tools/fieldRename.ts @@ -0,0 +1,93 @@ +import { + PDFAcroField, + PDFHexString, + PDFName, + PDFString, + toHexString, +} from "pdf-lib"; +import { loadPdfForm, savePdf } from "util/saveLoadPdf.ts"; +import { PDFDocument } from "pdf-lib"; +import { call, callWithArgPrompt } from "util/call.ts"; + +// const thing = PDFAcroField.prototype.getFullyQualifiedName; +// PDFAcroField.prototype.getFullyQualifiedName = function () { +// const name = thing.call(this) +// // if (name?.includes('langauge')) +// console.log(name) +// return name; +// } + +// const thing = PDFHexString.prototype.copyBytesInto +// PDFHexString.prototype.copyBytesInto = function (buffer: Uint8Array, offset: number) { +// console.log((this as any).value) + +// const result = thing.call(this, buffer, offset) +// return result; +// } + +async function renameFields( + path: string, + pattern: string | RegExp, + change: string, +) { + if (typeof pattern === "string") pattern = new RegExp(pattern); + const form = await loadPdfForm(path); + const fields = form.getFields(); + let changesMade = false; + for (const field of fields) { + const name = field.getName(); + if (pattern.test(name)) { + console.log(name + " %cfound", "color: red"); + const segments = name.split("."); + const matchingSegments = segments.filter((s) => pattern.test(s)); + let cField: PDFAcroField | undefined = field.acroField; + while (cField) { + if ( + cField.getPartialName() && + matchingSegments.includes(cField.getPartialName()!) + ) { + const mName = cField.getPartialName()?.replace(pattern, change); + if (mName) { + changesMade = true; + cField.dict.set(PDFName.of("T"), PDFString.of(mName)); + // console.log(cField.getPartialName()) + } + } + cField = cField.getParent(); + // console.log(cField?.getPartialName()) + } + console.log(field.getName()); + // const newName = name.replace(pattern, change); + // console.log("Change to: %c" + newName, "color: yellow"); + // if (confirm('Ok?')) { + // let parent = field.acroField.getParent(); + // field.acroField.setPartialName(segments.pop()) + // while (parent && segments.length) { + // console.log(parent.getPartialName()) + // parent.setPartialName(segments.pop()) + // parent = parent.getParent(); + // } + // changesMade = true; + // console.log(field.getName()) + // // dict.set(PDFName.of("T"), PDFHexString.fromText(newName)) + // console.log("%cDone!", "color: lime") + // } + // break; + } + } + if (changesMade) { + savePdf(form.doc, path); + } +} + +if (import.meta.main) { + // await call(renameFields) + // while (!path || !path.endsWith('.pdf')) path = prompt("Please provide path to PDF:") || ''; + // while (!pattern) pattern = prompt("Please provide search string:") || ''; + // while (!change) change = prompt("Please provide requested change:") || ''; + await callWithArgPrompt(renameFields, [ + ["Please provide path to PDF:", (p) => !!p && p.endsWith(".pdf")], + "Please provide search string:", + "Please provide requested change:", + ]); +} diff --git a/types.ts b/types.ts index e06731d..413e1d3 100644 --- a/types.ts +++ b/types.ts @@ -1,3 +1,9 @@ declare global { - type Tool = (...args: T) => Promise -} \ No newline at end of file + type ToolFunc = (...args: T) => Promise; + interface ITool { + name: string; + description: string; + run: ToolFunc; + help?: () => Promise | void; + } +} diff --git a/util/asciiArt.ts b/util/asciiArt.ts new file mode 100644 index 0000000..01795f4 --- /dev/null +++ b/util/asciiArt.ts @@ -0,0 +1,30 @@ +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); + + while (result !== null) { + const [_, name, artText] = result; + if (name === art) return artText; + result = parserRX.exec(artFileText); + } + 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; +} diff --git a/util/call.ts b/util/call.ts new file mode 100644 index 0000000..2197a5c --- /dev/null +++ b/util/call.ts @@ -0,0 +1,55 @@ +type transformer = (arg: string) => any; +interface IConfig { + multiTransform?: boolean; + args?: string[]; +} + +export async function call( + tool: ToolFunc, + transforms: transformer[], + conf?: IConfig, +) { + const config: IConfig = {}; + + if (typeof conf === "object") { + Object.assign(config, conf); + } + + const args = config.args || Deno.args; + const shouldPair = transforms.length === args.length; + const multiTransform = config.multiTransform || + !shouldPair && transforms.length > 1; + + const transformedArgs = args.map((arg, i) => { + if (shouldPair) return transforms[i](arg); + if (multiTransform) return transforms.reduce((a, b) => b(a), arg); + return transforms[0] ? transforms[0](arg) : arg; + }); + + await tool(...transformedArgs as T); +} + +type prompt = [string, (v?: string) => boolean] | string; + +export async function callWithArgPrompt( + tool: ToolFunc, + prompts: prompt[], + args?: string[], +) { + function buildPromptTransform(p: prompt): transformer { + let validation = (v?: string) => !!v; + let pText = p as string; + + if (Array.isArray(p)) { + [pText, validation] = p; + } + + return (a: string) => { + while (!validation(a)) { + a = prompt(pText) || ""; + } + }; + } + + await call(tool, prompts.map(buildPromptTransform), { args }); +} diff --git a/util/caseManagement.ts b/util/caseManagement.ts new file mode 100644 index 0000000..db0369e --- /dev/null +++ b/util/caseManagement.ts @@ -0,0 +1,128 @@ +function lowerToPascalCase(str: string) { + return str.replace(/(?:^|\s)\w/g, (match) => match.toUpperCase()).replaceAll( + " ", + "", + ); +} +function lowerToTrainCase(str: string) { + return str.replace(/(?:^|\s)\w/g, (match) => match.toUpperCase()).replaceAll( + " ", + "-", + ); +} + +function lowerToCamelCase(str: string) { + return str.trim().replace(/(?:\s)\w/g, (match) => match.toUpperCase()) + .replaceAll(" ", ""); +} + +function lowerToSnakeCase(str: string) { + return str.replace(" ", "_"); +} + +function lowerToKebabCase(str: string) { + return str.replace(" ", "-"); +} + +function lowerToMacroCase(str: string) { + return str.replace(/\w\S*/g, (match) => match.toUpperCase()).replaceAll( + " ", + "_", + ); +} + +function lowerToTitleCase(str: string) { + return str.replace(/(?:^|\s)\w/g, (match) => match.toUpperCase()); +} + +type CaseType = + | "pascal" + | "camel" + | "snake" + | "kebab" + | "macro" + | "upper" + | "lower" + | "train" + | "title" + | ""; + +function parseCase(str: string) { + const isCaseMap = new Map boolean>([ + ["pascal", (str: string) => { + return /^[A-Z][a-zA-Z]*$/.test(str); + }], + ["camel", (str: string) => { + return /^[a-z][a-zA-Z]*$/.test(str); + }], + ["snake", (str: string) => { + return /^[a-z][a-z0-9_]*$/.test(str); + }], + ["kebab", (str: string) => { + return /^[a-z][a-z0-9-]*$/.test(str); + }], + ["macro", (str: string) => { + return /^[A-Z]*$/.test(str); + }], + ["upper", (str: string) => { + return /^[A-Z]*$/.test(str); + }], + ["lower", (str: string) => { + return /^[a-z]*$/.test(str); + }], + ["train", (str: string) => { + return /([A-Z][a-z]*(?:-|$))+/.test(str); + }], + ]); + for (const [key, value] of isCaseMap) { + if (value(str)) return key; + } + return ""; +} + +function coerceCaseToLower(str: string, caseType: CaseType) { + switch (caseType) { + case "pascal": + case "camel": + return str.replace(/[A-Z]/g, (match) => " " + match.toLowerCase().trim()); + case "macro": + case "snake": + case "upper": + return str.replace("_", " ").toLowerCase(); + case "train": + case "kebab": + return str.replace("-", " ").toLowerCase(); + default: + return str.toLowerCase(); + } +} + +export function toCase(str: string, toCase: CaseType) { + const caseType = parseCase(str) || ""; + console.log(caseType); + if (caseType === toCase) return str; + const lowerStr = coerceCaseToLower(str, caseType); + console.log(lowerStr); + switch (toCase) { + case "pascal": + return lowerToPascalCase(lowerStr); + case "camel": + return lowerToCamelCase(lowerStr); + case "snake": + return lowerToSnakeCase(lowerStr); + case "kebab": + return lowerToKebabCase(lowerStr); + case "macro": + return lowerToMacroCase(lowerStr); + case "upper": + return lowerStr.toUpperCase(); + case "lower": + return lowerStr.toLowerCase(); + case "train": + return lowerToTrainCase(lowerStr); + case "title": + return lowerToTitleCase(lowerStr); + default: + return str; + } +} diff --git a/util/saveLoadPdf.ts b/util/saveLoadPdf.ts new file mode 100644 index 0000000..7adb71d --- /dev/null +++ b/util/saveLoadPdf.ts @@ -0,0 +1,19 @@ +import { PDFDocument } from "pdf-lib"; + +export async function loadPdfForm(path: string) { + const pdfDoc = await loadPdf(path); + const form = pdfDoc.getForm(); + return form; +} + +export async function loadPdf(path: string) { + const pdfBytes = await Deno.readFile(path); + const pdfDoc = await PDFDocument.load(pdfBytes); + return pdfDoc; +} + +export async function savePdf(doc: PDFDocument, path: string) { + const pdfBytes = await doc.save(); + if (Deno.env.get("DRYRUN") || path.includes("dryrun")) return; + await Deno.writeFile(path, pdfBytes); +}