initial cli api, some movement on tool selection
This commit is contained in:
parent
08bba857db
commit
7d42920dcb
32
asciiart.txt
Normal file
32
asciiart.txt
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
begin bearmetal
|
||||||
|
__________ _____ __ .__
|
||||||
|
\______ \ ____ _____ _______ / \ _____/ |______ | |
|
||||||
|
| | _// __ \\__ \\_ __ \/ \ / \_/ __ \ __\__ \ | |
|
||||||
|
| | \ ___/ / __ \| | \/ Y \ ___/| | / __ \| |__
|
||||||
|
|______ /\___ >____ /__| \____|__ /\___ >__| (____ /____/
|
||||||
|
\/ \/ \/ \/ \/ \/
|
||||||
|
end
|
||||||
|
|
||||||
|
begin pdftools
|
||||||
|
_____ ____ _____ _____ _____ _____ __ _____
|
||||||
|
| _ | \| __|___|_ _| | | | | __|
|
||||||
|
| __| | | __|___| | | | | | | | |__|__ |
|
||||||
|
|__| |____/|__| |_| |_____|_____|_____|_____|
|
||||||
|
end
|
||||||
|
|
||||||
|
begin banana
|
||||||
|
_
|
||||||
|
//\
|
||||||
|
V \
|
||||||
|
\ \_
|
||||||
|
\,'.`-.
|
||||||
|
|\ `. `.
|
||||||
|
( \ `. `-. _,.-:\
|
||||||
|
\ \ `. `-._ __..--' ,-';/
|
||||||
|
\ `. `-. `-..___..---' _.--' ,'/
|
||||||
|
`. `. `-._ __..--' ,' /
|
||||||
|
`. `-_ ``--..'' _.-' ,'
|
||||||
|
`-_ `-.___ __,--' ,'
|
||||||
|
`-.__ `----""" __.-'
|
||||||
|
`--..____..--'
|
||||||
|
end
|
47
call.ts
47
call.ts
@ -1,47 +0,0 @@
|
|||||||
type transformer = (arg: string) => any;
|
|
||||||
interface IConfig {
|
|
||||||
multiTransform?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function call<T extends unknown[]>(tool: Tool<T>, 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<T extends unknown[]>(tool: Tool<T>, 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))
|
|
||||||
}
|
|
201
cli/TerminalLayout.ts
Normal file
201
cli/TerminalLayout.ts
Normal file
@ -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<string, TerminalBlock> = {};
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
33
cli/argParser.ts
Normal file
33
cli/argParser.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
21
cli/colorize.ts
Normal file
21
cli/colorize.ts
Normal file
@ -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`;
|
||||||
|
}
|
64
cli/index.ts
Normal file
64
cli/index.ts
Normal file
@ -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<string, ITool> = 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
186
cli/selectMenu.ts
Normal file
186
cli/selectMenu.ts
Normal file
@ -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<string | null> {
|
||||||
|
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);
|
||||||
|
}
|
10
deno.json
10
deno.json
@ -1,12 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "@bearmetal/pdf-tools",
|
"name": "@bearmetal/pdf-tools",
|
||||||
"tasks": {
|
"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",
|
"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"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@std/assert": "jsr:@std/assert@1",
|
"@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": {}
|
||||||
|
}
|
@ -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:"
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
32
main.ts
32
main.ts
@ -1,30 +1,4 @@
|
|||||||
import { PDFDocument } from "pdf-lib";
|
import { PdfToolsCli } from "./cli/index.ts";
|
||||||
import { loadPdfForm } from "./saveLoadPdf.ts";
|
|
||||||
|
|
||||||
let [pdfPath, csPath] = Deno.args;
|
const app = new PdfToolsCli();
|
||||||
|
app.run();
|
||||||
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(`(?<!//\s?)case ?"${f.replace(/\[\d\]/, '\\[\\?\\]')}"`)
|
|
||||||
return !csFiles.some(c => 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\[\?\]/
|
|
||||||
|
@ -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);
|
|
||||||
}
|
|
59
tools/checkCode.ts
Normal file
59
tools/checkCode.ts
Normal file
@ -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(
|
||||||
|
`(?<!//\s?)case ?"${f.replace(/\[\d\]/, "\\[\\?\\]")}"`,
|
||||||
|
);
|
||||||
|
return !csFiles.some((c) => 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 <pdfPath> <csPath>");
|
||||||
|
}
|
||||||
|
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();
|
93
tools/fieldRename.ts
Normal file
93
tools/fieldRename.ts
Normal file
@ -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:",
|
||||||
|
]);
|
||||||
|
}
|
10
types.ts
10
types.ts
@ -1,3 +1,9 @@
|
|||||||
declare global {
|
declare global {
|
||||||
type Tool<T extends unknown[]> = (...args: T) => Promise<void>
|
type ToolFunc<T extends unknown[]> = (...args: T) => Promise<void>;
|
||||||
}
|
interface ITool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
run: ToolFunc<any[]>;
|
||||||
|
help?: () => Promise<void> | void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
30
util/asciiArt.ts
Normal file
30
util/asciiArt.ts
Normal file
@ -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;
|
||||||
|
}
|
55
util/call.ts
Normal file
55
util/call.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
type transformer = (arg: string) => any;
|
||||||
|
interface IConfig {
|
||||||
|
multiTransform?: boolean;
|
||||||
|
args?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function call<T extends unknown[]>(
|
||||||
|
tool: ToolFunc<T>,
|
||||||
|
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<T extends unknown[]>(
|
||||||
|
tool: ToolFunc<T>,
|
||||||
|
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 });
|
||||||
|
}
|
128
util/caseManagement.ts
Normal file
128
util/caseManagement.ts
Normal file
@ -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<CaseType, (str: string) => 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;
|
||||||
|
}
|
||||||
|
}
|
19
util/saveLoadPdf.ts
Normal file
19
util/saveLoadPdf.ts
Normal file
@ -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);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user