diff --git a/.gitea/workflows/build-and-release.yml b/.gitea/workflows/build-and-release.yml new file mode 100644 index 0000000..bce1add --- /dev/null +++ b/.gitea/workflows/build-and-release.yml @@ -0,0 +1,30 @@ +name: Build and Release + +on: + push: + tags: + - "v*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build and release binaries + uses: bearmetal/ci-actions/deno-publish@main + with: + entrypoint: main.ts + compile-flags: "--allow-read --allow-write --allow-env --allow-net" + + publish: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Deno + uses: denoland/setup-deno@v1 + with: + deno-version: 2.3.1 + + - name: Publish to JSR + run: deno publish --token ${{ secrets.JSR_TOKEN }} diff --git a/.gitea/workflows/tag-cli.yml b/.gitea/workflows/tag-cli.yml new file mode 100644 index 0000000..579cdc0 --- /dev/null +++ b/.gitea/workflows/tag-cli.yml @@ -0,0 +1,13 @@ +name: Create Version Tag + +on: + push: + branches: + - main + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: bearmetal/ci-actions/version-check@main diff --git a/cli/TerminalLayout.ts b/cli/TerminalLayout.ts index 59addaf..a4f400c 100644 --- a/cli/TerminalLayout.ts +++ b/cli/TerminalLayout.ts @@ -79,15 +79,15 @@ export class TerminalLayout { } clearAll() { + for (const name of this.layoutOrder) { + this.blocks[name].clear(); + } Deno.stdout.writeSync( new TextEncoder().encode( TerminalLayout.ALT_BUFFER_DISABLE, ), ); Cursor.show(); - for (const name of this.layoutOrder) { - this.blocks[name].clear(); - } } clear() { @@ -223,7 +223,6 @@ export class TerminalBlock { for (let i = 0; i < this.renderedLineCount; i++) { Deno.stdout.writeSync(new TextEncoder().encode(`\x1b[2K\x1b[1E`)); } - this.renderedLineCount = 0; } clearAll() { diff --git a/cli/argParser.ts b/cli/argParser.ts index 842378b..89907b9 100644 --- a/cli/argParser.ts +++ b/cli/argParser.ts @@ -1,5 +1,6 @@ -export class ArgParser { +export class ArgParser> { private args: string[]; + private flags: Map = new Map(); constructor(args: string[]) { this.args = args; @@ -11,7 +12,22 @@ export class ArgParser { return this.args[index + 1]; } - get flags() { + setFlagDefs(flagDefs: T) { + for (const [flag, defs] of Object.entries(flagDefs)) { + for (const def of defs) { + if (this.argFlags.includes(def)) { + this.flags.set(flag, true); + } + } + } + return this; + } + + getFlag(flag: keyof T) { + return this.flags.get(flag); + } + + get argFlags() { return this.args.filter((arg) => arg.startsWith("-")); } @@ -26,6 +42,9 @@ export class ArgParser { get task() { return this.nonFlags[0]; } + get taskArgs() { + return this.nonFlags.slice(1); + } static parse(args: string[]) { return new ArgParser(args); diff --git a/cli/index.ts b/cli/index.ts index 1ce38b4..9decc4e 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -5,26 +5,38 @@ import { colorize } from "./style.ts"; import { selectMenuInteractive } from "./selectMenu.ts"; import { TerminalBlock, TerminalLayout } from "./TerminalLayout.ts"; import { cliAlert, cliLog } from "./prompts.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) +const toolRegistry: [string, Promise<{ default: ITool }>][] = [ + ["checkCode", import("../tools/checkCode.ts")], + ["fieldRename", import("../tools/fieldRename.ts")], + ["listFormFields", import("../tools/listFormFields.ts")], +]; export class PdfToolsCli { private tools: Map = new Map(); private terminalLayout = new TerminalLayout(); + closeMessage?: string; - private args = ArgParser.parse(Deno.args); + private args = ArgParser.parse(Deno.args).setFlagDefs({ + help: ["-h", "--help"], + }); - 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) { + async importTools() { + for (const [name, toolfile] of toolRegistry) { + const t = await toolfile; + try { + if (t.default) { this.tools.set( - toCase(toolfile.name.replace(".ts", ""), "title"), - tool.default, + toCase(name, "title"), + t.default, ); } + } catch (e) { + cliLog(e + "\n", this.terminalLayout.getBlock("body")); } } } @@ -47,21 +59,25 @@ export class PdfToolsCli { public async run() { try { + await this.importTools(); const titleBlock = new TerminalBlock(); this.terminalLayout.register("title", titleBlock); const bodyBlock = new TerminalBlock(); this.terminalLayout.register("body", bodyBlock); - this.embiggenHeader(); - if (Deno.args.length === 0) { - // console.log( - // colorize("No tool specified. Importing all tools...", "gray"), - // ); - await this.importTools(); + if (this.args.getFlag("help") && !this.args.task) { + await this.help(); + return; + } else if (this.args.nonFlags.length === 0 || !this.args.task) { + this.embiggenHeader(); + await this.toolMenu(); + } else { + const task = this.args.task; + await this.runTool(toCase(task, "title")); } - await this.toolMenu(); } finally { this.terminalLayout.clearAll(); Deno.stdin.setRaw(false); + if (this.closeMessage) console.log(this.closeMessage); } } @@ -95,9 +111,15 @@ export class PdfToolsCli { const bodyBlock = this.terminalLayout.getBlock("body"); bodyBlock.clearAll(); tool.setBlock?.(bodyBlock); - await tool.run(); - await tool.done?.(); - this.embiggenHeader(); + if (this.args.getFlag("help")) { + await tool.help?.(); + } else { + await tool.run(...this.args.taskArgs); + await tool.done?.(); + } + await this.embiggenHeader(); + } else { + this.closeMessage = "No tool found for " + toolName; } } diff --git a/cli/prompts.ts b/cli/prompts.ts index 9e0fe1c..e183ef5 100644 --- a/cli/prompts.ts +++ b/cli/prompts.ts @@ -1,3 +1,4 @@ +// deno-lint-disable-must-await-calls import { Cursor } from "./cursor.ts"; import { colorize } from "./style.ts"; import { TerminalBlock, TerminalLayout } from "./TerminalLayout.ts"; @@ -11,11 +12,8 @@ export async function cliPrompt( await Deno.stdin.setRaw(true); - let cursorVisible = true; - if (!block) { - cursorVisible = Cursor["visible"]; - Cursor.show(); - } + const cursorVisible = Cursor["visible"]; + Cursor.show(); let range: [number, number] = [0, 1]; if (block) { @@ -54,7 +52,7 @@ export async function cliPrompt( } await Deno.stdin.setRaw(false); - if (!block && !cursorVisible) { + if (!cursorVisible) { Cursor.hide(); } Deno.stdout.writeSync(encoder.encode("\n")); @@ -77,19 +75,25 @@ export function cliAlert(message: string, block?: TerminalBlock) { }); } -export function cliLog(message: string, block?: TerminalBlock) { +export function cliLog( + message: string | object | Array, + block?: TerminalBlock, +) { if (!block) { console.log(message); } else { + if (typeof message === "object") message = Deno.inspect(message); block.setLines(message.split("\n")); } } if (import.meta.main) { + Cursor.hide(); const layout = new TerminalLayout(); const title = new TerminalBlock(); const block = new TerminalBlock(); block.setPreserveHistory(true); + // ScrollManager.enable(block); title.setLines(["Hello, World!"]); title.setFixedHeight(1); @@ -105,6 +109,7 @@ if (import.meta.main) { cliLog(`Hello, ${name}!`, block); const single = await cliConfirm("Are you single?", block); cliLog(single ? "Do you want to go out with me?" : "Okay", block); + // ScrollManager.enable(block); const loopingConvo = [ "No response?", "I guess that's okay", diff --git a/cli/selectMenu.ts b/cli/selectMenu.ts index a94ee69..6e2a774 100644 --- a/cli/selectMenu.ts +++ b/cli/selectMenu.ts @@ -1,3 +1,4 @@ +import type { callback } from "../types.ts"; import { colorize } from "./style.ts"; import { TerminalBlock } from "./TerminalLayout.ts"; diff --git a/deno.json b/deno.json index b87cf86..c620ea1 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,7 @@ { "name": "@bearmetal/pdf-tools", - "version": "0.0.1", + "version": "1.0.0", + "license": "GPL 3.0", "tasks": { "dev": "deno run -A --env-file=.env --watch main.ts", "compile": "deno compile -o compare-form-fields.exe --target x86_64-pc-windows-msvc -R ./main.ts", @@ -9,6 +10,7 @@ }, "imports": { "@std/assert": "jsr:@std/assert@1", + "@std/path": "jsr:@std/path@^1.0.9", "pdf-lib": "npm:pdf-lib@^1.17.1", "util/": "./util/" }, @@ -19,10 +21,14 @@ "rules": { "exclude": [ "no-explicit-any" + ], + "include": [ + "require-await" ] }, "plugins": [ - "./no-log.ts" + "./no-log.ts", + "./must_await_cli_prompts.ts" ] } } \ No newline at end of file diff --git a/deno.lock b/deno.lock index 1e00912..eda4b49 100644 --- a/deno.lock +++ b/deno.lock @@ -1,8 +1,9 @@ { - "version": "4", + "version": "5", "specifiers": { "jsr:@std/assert@1": "1.0.12", "jsr:@std/internal@^1.0.6": "1.0.6", + "jsr:@std/path@^1.0.9": "1.0.9", "npm:pdf-lib@^1.17.1": "1.17.1" }, "jsr": { @@ -14,6 +15,9 @@ }, "@std/internal@1.0.6": { "integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4" + }, + "@std/path@1.0.9": { + "integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e" } }, "npm": { @@ -48,6 +52,7 @@ "workspace": { "dependencies": [ "jsr:@std/assert@1", + "jsr:@std/path@^1.0.9", "npm:pdf-lib@^1.17.1" ] } diff --git a/main.ts b/main.ts index eae7bf6..0741d37 100644 --- a/main.ts +++ b/main.ts @@ -1,3 +1,4 @@ +/// import { PdfToolsCli } from "./cli/index.ts"; const app = new PdfToolsCli(); diff --git a/must_await_cli_prompts.ts b/must_await_cli_prompts.ts new file mode 100644 index 0000000..a45197a --- /dev/null +++ b/must_await_cli_prompts.ts @@ -0,0 +1,45 @@ +const TARGET_FUNCTIONS = new Set(["cliAlert", "cliPrompt", "cliConfirm"]); + +const plugin: Deno.lint.Plugin = { + name: "must-await-calls", + rules: { + "must-await-calls": { + create(context) { + return { + CallExpression(node) { + if ( + node.callee.type !== "Identifier" || + !TARGET_FUNCTIONS.has(node.callee.name) + ) return; + + const parent = node.parent; + + // Allow `await fetchData()` + if (parent?.type === "AwaitExpression") return; + + // Allow `return fetchData()` or `return await fetchData()` + if (parent?.type === "ReturnStatement") return; + + // Allow `fetchData().then(...)` + if ( + parent?.type === "MemberExpression" && + parent.property.type === "Identifier" && + parent.property.name === "then" + ) return; + + context.report({ + node, + message: + `Call to "${node.callee.name}" must be awaited, returned, or .then-chained.`, + fix(fixer) { + return fixer.insertTextBefore(node, "await "); + }, + }); + }, + }; + }, + }, + }, +}; + +export default plugin; diff --git a/testing/test.ts b/testing/test.ts index e69de29..ea306bd 100644 --- a/testing/test.ts +++ b/testing/test.ts @@ -0,0 +1,7 @@ +const thing: string = ""; +switch (thing) { + case "Text1": + break; + default: + break; +} diff --git a/tools/checkCode.ts b/tools/checkCode.ts index a9a1a3c..1993808 100644 --- a/tools/checkCode.ts +++ b/tools/checkCode.ts @@ -1,57 +1,75 @@ +import { forceArgs } from "../cli/forceArgs.ts"; +import { cliAlert, cliLog } from "../cli/prompts.ts"; +import { colorize } from "../cli/style.ts"; +import type { TerminalBlock } from "../cli/TerminalLayout.ts"; +import type { ITool } from "../types.ts"; 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!"); +function getCaseSyntaxPatternByFileExtension( + extenstion: string, + field: string, +) { + switch (extenstion.trim().toLowerCase().replace(".", "")) { + case "cs": + case "js": + case "ts": + default: + return `(? "); + description = "Checks if form fields are present in a given code file"; + private block?: TerminalBlock; + setBlock(block: TerminalBlock) { + this.block = block; + this.block.setPreserveHistory(true); } - 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); + async help() { + cliLog("Usage: checkcode ", this.block); + await cliAlert("", this.block); + } + async run(pdfPath: string, codePaths: string) { + [pdfPath, codePaths] = await forceArgs([pdfPath, codePaths], [ + "Please provide path to PDF file:", + "Please provide path(s) to code file(s) (comma separated for multiple):", + ], this.block); + + const form = await loadPdfForm(pdfPath); + + const fields = form.getFields(); + const codeFiles: [string, string][] = codePaths.split(",").map(( + c, + ) => [c, Deno.readTextFileSync(c.trim())]); + + const fieldNames: string[] = fields.map((f) => f.getName()) + .filter((f) => !f.toLowerCase().includes("signature")); + let unfound = fieldNames.slice(); + + for (const [path, content] of codeFiles) { + unfound = unfound.filter((f) => { + const rx = new RegExp( + getCaseSyntaxPatternByFileExtension(path.split(".").at(-1) ?? "", f), + ); + return rx.test(content); + }); + } + + if (unfound.length) { + cliLog( + colorize( + "The following field names are not present in the CS code", + "red", + ), + this.block, + ); + cliLog(unfound, this.block); + await cliAlert("Your princess is in another castle...", this.block); + } else { + cliLog(colorize("All form fields present", "green"), this.block); + await cliAlert("Ok!", this.block); + } } } diff --git a/tools/fieldRename.ts b/tools/fieldRename.ts index 0f5898c..3fc6e6e 100644 --- a/tools/fieldRename.ts +++ b/tools/fieldRename.ts @@ -4,24 +4,9 @@ import { callWithArgPrompt } from "util/call.ts"; import { TerminalBlock } from "../cli/TerminalLayout.ts"; import { forceArgs } from "../cli/forceArgs.ts"; import { colorize } from "../cli/style.ts"; -import { cliLog, cliPrompt } from "../cli/prompts.ts"; +import { cliAlert, cliLog, cliPrompt } from "../cli/prompts.ts"; import { multiSelectMenuInteractive } from "../cli/selectMenu.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; -// } +import type { callback, ITool } from "../types.ts"; async function renameFields( path: string, @@ -48,29 +33,10 @@ async function renameFields( 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) { @@ -122,9 +88,9 @@ class RenameFields implements ITool { this.block = block; } - help(standalone = false) { - cliLog( - "Usage: renamefields ", + async help(standalone = false) { + await cliAlert( + "Usage: rename-fields \n", standalone ? undefined : this.block, ); } diff --git a/tools/listFormFields.ts b/tools/listFormFields.ts index d8c5daf..77f8542 100644 --- a/tools/listFormFields.ts +++ b/tools/listFormFields.ts @@ -2,6 +2,7 @@ import { forceArgs } from "../cli/forceArgs.ts"; import { cliAlert } from "../cli/prompts.ts"; import { TerminalBlock } from "../cli/TerminalLayout.ts"; import { loadPdfForm } from "util/saveLoadPdf.ts"; +import type { ITool } from "../types.ts"; export class ListFormFields implements ITool { name = "listformfields"; diff --git a/types.ts b/types.ts index de4feeb..bdc2f42 100644 --- a/types.ts +++ b/types.ts @@ -1,15 +1,13 @@ import type { TerminalBlock } from "./cli/TerminalLayout.ts"; -declare global { - type ToolFunc = (...args: T) => Promise; - interface ITool { - name: string; - description: string; - run: ToolFunc; - help?: () => Promise | void; - done?: () => Promise | void; - setBlock?: (block: TerminalBlock) => void; - } - - type callback = (...args: any[]) => any; +export type ToolFunc = (...args: T) => Promise; +export interface ITool { + name: string; + description: string; + run: ToolFunc; + help?: () => Promise | void; + done?: () => Promise | void; + setBlock?: (block: TerminalBlock) => void; } + +export type callback = (...args: any[]) => any; diff --git a/util/call.ts b/util/call.ts index 0d7cadb..10f1a47 100644 --- a/util/call.ts +++ b/util/call.ts @@ -1,3 +1,5 @@ +import type { ToolFunc } from "../types.ts"; + type transformer = (arg: string) => any; interface IConfig { multiTransform?: boolean; diff --git a/util/saveLoadPdf.ts b/util/saveLoadPdf.ts index 7adb71d..815aed6 100644 --- a/util/saveLoadPdf.ts +++ b/util/saveLoadPdf.ts @@ -1,4 +1,4 @@ -import { PDFDocument } from "pdf-lib"; +import { PDFDocument, PDFTextField } from "pdf-lib"; export async function loadPdfForm(path: string) { const pdfDoc = await loadPdf(path); @@ -13,6 +13,11 @@ export async function loadPdf(path: string) { } export async function savePdf(doc: PDFDocument, path: string) { + doc.getForm().getFields().forEach((field) => { + if (field instanceof PDFTextField) { + field.disableRichFormatting(); + } + }); const pdfBytes = await doc.save(); if (Deno.env.get("DRYRUN") || path.includes("dryrun")) return; await Deno.writeFile(path, pdfBytes);