snapshot of https://github.com/mountain-loop/yaak.git (main, 2026-09-23; blobs > 131072B omitted)
This commit is contained in:
20
plugins/template-function-json/package.json
Executable file
20
plugins/template-function-json/package.json
Executable file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@yaak/template-function-json",
|
||||
"displayName": "JSON Template Functions",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Template functions for working with JSON data",
|
||||
"main": "build/index.js",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "yaakcli build",
|
||||
"dev": "yaakcli dev",
|
||||
"test": "vp test --run tests"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonpath-plus": "^10.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonpath": "^0.2.4"
|
||||
}
|
||||
}
|
||||
154
plugins/template-function-json/src/index.ts
Executable file
154
plugins/template-function-json/src/index.ts
Executable file
@@ -0,0 +1,154 @@
|
||||
import type { XPathResult } from "@yaak/template-function-xml";
|
||||
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from "@yaakapp/api";
|
||||
import { JSONPath } from "jsonpath-plus";
|
||||
|
||||
const RETURN_FIRST = "first";
|
||||
const RETURN_ALL = "all";
|
||||
const RETURN_JOIN = "join";
|
||||
|
||||
export const plugin: PluginDefinition = {
|
||||
templateFunctions: [
|
||||
{
|
||||
name: "json.jsonpath",
|
||||
description: "Filter JSON-formatted text using JSONPath syntax",
|
||||
previewArgs: ["query"],
|
||||
args: [
|
||||
{
|
||||
type: "editor",
|
||||
name: "input",
|
||||
label: "Input",
|
||||
language: "json",
|
||||
placeholder: '{ "foo": "bar" }',
|
||||
},
|
||||
{
|
||||
type: "h_stack",
|
||||
inputs: [
|
||||
{
|
||||
type: "select",
|
||||
name: "result",
|
||||
label: "Return Format",
|
||||
defaultValue: RETURN_FIRST,
|
||||
options: [
|
||||
{ label: "First result", value: RETURN_FIRST },
|
||||
{ label: "All results", value: RETURN_ALL },
|
||||
{ label: "Join with separator", value: RETURN_JOIN },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "join",
|
||||
type: "text",
|
||||
label: "Separator",
|
||||
optional: true,
|
||||
defaultValue: ", ",
|
||||
dynamic(_ctx, args) {
|
||||
return { hidden: args.values.result !== RETURN_JOIN };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "formatted",
|
||||
label: "Pretty Print",
|
||||
description: "Format the output as JSON",
|
||||
dynamic(_ctx, args) {
|
||||
return { hidden: args.values.result === RETURN_JOIN };
|
||||
},
|
||||
},
|
||||
{ type: "text", name: "query", label: "Query", placeholder: "$..foo" },
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
try {
|
||||
return filterJSONPath(
|
||||
String(args.values.input),
|
||||
String(args.values.query),
|
||||
(args.values.result || RETURN_FIRST) as XPathResult,
|
||||
args.values.join == null ? null : String(args.values.join),
|
||||
Boolean(args.values.formatted),
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json.escape",
|
||||
description: "Escape a JSON string, useful when using the output in JSON values",
|
||||
args: [
|
||||
{
|
||||
type: "text",
|
||||
name: "input",
|
||||
label: "Input",
|
||||
multiLine: true,
|
||||
placeholder: 'Hello "World"',
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const input = String(args.values.input ?? "");
|
||||
// JSON.stringify produces a spec-correct string literal: it escapes
|
||||
// the backslash and quote this used to handle, and also the control
|
||||
// characters it did not. Slicing off the surrounding quotes leaves
|
||||
// the escaped inner text this function is meant to emit.
|
||||
return JSON.stringify(input).slice(1, -1);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json.minify",
|
||||
description: "Remove unnecessary whitespace from a valid JSON string.",
|
||||
args: [
|
||||
{
|
||||
type: "editor",
|
||||
language: "json",
|
||||
name: "input",
|
||||
label: "Input",
|
||||
placeholder: '{ "foo": "bar" }',
|
||||
},
|
||||
],
|
||||
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
|
||||
const input = String(args.values.input ?? "");
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(input));
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export type JSONPathResult = "first" | "join" | "all";
|
||||
|
||||
export function filterJSONPath(
|
||||
body: string,
|
||||
path: string,
|
||||
result: JSONPathResult,
|
||||
join: string | null,
|
||||
formatted = false,
|
||||
): string {
|
||||
const parsed = JSON.parse(body);
|
||||
let items = JSONPath({ path, json: parsed });
|
||||
|
||||
if (items == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!Array.isArray(items)) {
|
||||
// Already good
|
||||
} else if (result === "first") {
|
||||
items = items[0] ?? "";
|
||||
} else if (result === "join") {
|
||||
items = items.map((i) => objToStr(i, false)).join(join ?? "");
|
||||
}
|
||||
|
||||
return objToStr(items, formatted);
|
||||
}
|
||||
|
||||
function objToStr(o: unknown, formatted = false): string {
|
||||
if (
|
||||
Object.prototype.toString.call(o) === "[object Array]" ||
|
||||
Object.prototype.toString.call(o) === "[object Object]"
|
||||
) {
|
||||
return formatted ? JSON.stringify(o, null, 2) : JSON.stringify(o);
|
||||
}
|
||||
return String(o);
|
||||
}
|
||||
50
plugins/template-function-json/tests/escape.test.ts
Normal file
50
plugins/template-function-json/tests/escape.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Context } from "@yaakapp/api";
|
||||
import { describe, expect, it } from "vite-plus/test";
|
||||
import { plugin } from "../src";
|
||||
|
||||
const LF = String.fromCharCode(10);
|
||||
const TAB = String.fromCharCode(9);
|
||||
const CR = String.fromCharCode(13);
|
||||
|
||||
describe("json.escape", () => {
|
||||
const escapeFunction = plugin.templateFunctions?.find((f) => f.name === "json.escape");
|
||||
|
||||
const escape = async (input: string) =>
|
||||
await escapeFunction!.onRender({} as Context, { values: { input } } as never);
|
||||
|
||||
// The point of the function is that the result can be dropped between two
|
||||
// quotes in a JSON document, so that is what these assert.
|
||||
const embeds = (escaped: string | null) => {
|
||||
JSON.parse(`{"k":"${escaped}"}`);
|
||||
return JSON.parse(`{"k":"${escaped}"}`).k;
|
||||
};
|
||||
|
||||
it("should exist", () => {
|
||||
expect(escapeFunction).toBeTruthy();
|
||||
});
|
||||
|
||||
it("escapes a quote", async () => {
|
||||
const input = `say "hi"`;
|
||||
expect(embeds(await escape(input))).toBe(input);
|
||||
});
|
||||
|
||||
it("escapes a backslash", async () => {
|
||||
const input = `a${String.fromCharCode(92)}b`;
|
||||
expect(embeds(await escape(input))).toBe(input);
|
||||
});
|
||||
|
||||
it("escapes a newline", async () => {
|
||||
const input = `line1${LF}line2`;
|
||||
expect(embeds(await escape(input))).toBe(input);
|
||||
});
|
||||
|
||||
it("escapes a tab and a carriage return", async () => {
|
||||
const input = `a${TAB}b${CR}c`;
|
||||
expect(embeds(await escape(input))).toBe(input);
|
||||
});
|
||||
|
||||
it("round-trips a pretty-printed JSON document", async () => {
|
||||
const input = JSON.stringify({ name: `he said "hi"`, items: [1, 2] }, null, 2);
|
||||
expect(embeds(await escape(input))).toBe(input);
|
||||
});
|
||||
});
|
||||
3
plugins/template-function-json/tsconfig.json
Normal file
3
plugins/template-function-json/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
Reference in New Issue
Block a user