First implementation of Plimi
This commit is contained in:
43
src/tools/html-entity/index.ts
Normal file
43
src/tools/html-entity/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { PlimiPlugin } from "../../core/plugins/plugin-types";
|
||||
import { runHtmlEntity } from "./run";
|
||||
|
||||
export interface HtmlEntityOptions {
|
||||
mode: "encode" | "decode";
|
||||
}
|
||||
|
||||
export const htmlEntityPlugin: PlimiPlugin<HtmlEntityOptions> = {
|
||||
manifest: {
|
||||
id: "dev-htmlentity",
|
||||
name: "HTML Entity Encoder",
|
||||
description: "Encode special characters to HTML entities or decode them back.",
|
||||
category: "developer",
|
||||
version: "1.0.0",
|
||||
tags: ["html", "entity", "encode", "decode", "escape"],
|
||||
input: { type: "text", placeholder: "Enter text with < > & \" characters..." },
|
||||
output: { type: "text" },
|
||||
example: '<div class="hero">Hello & "World"</div>',
|
||||
offlineReady: true,
|
||||
},
|
||||
|
||||
optionsSchema: {
|
||||
fields: [
|
||||
{
|
||||
type: "select",
|
||||
key: "mode",
|
||||
label: "Mode",
|
||||
defaultValue: "encode",
|
||||
options: [
|
||||
{ label: "Encode", value: "encode" },
|
||||
{ label: "Decode", value: "decode" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
capabilities: {
|
||||
cancelable: false,
|
||||
worker: false,
|
||||
},
|
||||
|
||||
run: runHtmlEntity,
|
||||
};
|
||||
109
src/tools/html-entity/run.test.ts
Normal file
109
src/tools/html-entity/run.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { runHtmlEntity } from "./run";
|
||||
import type { ToolContext } from "../../core/plugins/plugin-types";
|
||||
|
||||
describe("HTML Entity Encoder/Decoder", () => {
|
||||
const mockContext: ToolContext = {
|
||||
signal: new AbortController().signal,
|
||||
reportProgress: vi.fn(),
|
||||
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
||||
};
|
||||
|
||||
it("should encode basic HTML special characters", async () => {
|
||||
const result = await runHtmlEntity(
|
||||
{ text: '<div class="test">Hello & World</div>' },
|
||||
{ mode: "encode" },
|
||||
mockContext
|
||||
);
|
||||
expect(result.type).toBe("text");
|
||||
const value = (result as { type: "text"; value: string }).value;
|
||||
expect(value).toBe("<div class="test">Hello & World</div>");
|
||||
});
|
||||
|
||||
it("should decode basic HTML entities", async () => {
|
||||
const result = await runHtmlEntity(
|
||||
{ text: "<div>Hello & World</div>" },
|
||||
{ mode: "decode" },
|
||||
mockContext
|
||||
);
|
||||
const value = (result as { type: "text"; value: string }).value;
|
||||
expect(value).toBe("<div>Hello & World</div>");
|
||||
});
|
||||
|
||||
it("should encode special symbols", async () => {
|
||||
const result = await runHtmlEntity(
|
||||
{ text: "Price: 10€ © 2024" },
|
||||
{ mode: "encode" },
|
||||
mockContext
|
||||
);
|
||||
const value = (result as { type: "text"; value: string }).value;
|
||||
expect(value).toContain("€");
|
||||
expect(value).toContain("©");
|
||||
});
|
||||
|
||||
it("should decode special symbol entities", async () => {
|
||||
const result = await runHtmlEntity(
|
||||
{ text: "© 2024 — All rights reserved" },
|
||||
{ mode: "decode" },
|
||||
mockContext
|
||||
);
|
||||
const value = (result as { type: "text"; value: string }).value;
|
||||
expect(value).toContain("©");
|
||||
expect(value).toContain("—");
|
||||
});
|
||||
|
||||
it("should decode numeric character references (decimal)", async () => {
|
||||
const result = await runHtmlEntity(
|
||||
{ text: "ABC" },
|
||||
{ mode: "decode" },
|
||||
mockContext
|
||||
);
|
||||
const value = (result as { type: "text"; value: string }).value;
|
||||
expect(value).toBe("ABC");
|
||||
});
|
||||
|
||||
it("should decode numeric character references (hex)", async () => {
|
||||
const result = await runHtmlEntity(
|
||||
{ text: "ABC" },
|
||||
{ mode: "decode" },
|
||||
mockContext
|
||||
);
|
||||
const value = (result as { type: "text"; value: string }).value;
|
||||
expect(value).toBe("ABC");
|
||||
});
|
||||
|
||||
it("should encode single quotes", async () => {
|
||||
const result = await runHtmlEntity(
|
||||
{ text: "it's a test" },
|
||||
{ mode: "encode" },
|
||||
mockContext
|
||||
);
|
||||
const value = (result as { type: "text"; value: string }).value;
|
||||
expect(value).toContain("'");
|
||||
});
|
||||
|
||||
it("should return empty string for empty input", async () => {
|
||||
const result = await runHtmlEntity(
|
||||
{ text: "" },
|
||||
{ mode: "encode" },
|
||||
mockContext
|
||||
);
|
||||
const value = (result as { type: "text"; value: string }).value;
|
||||
expect(value).toBe("");
|
||||
});
|
||||
|
||||
it("should be reversible: encode then decode gives original", async () => {
|
||||
const original = '<p>Hello & "World"</p>';
|
||||
const encoded = await runHtmlEntity(
|
||||
{ text: original },
|
||||
{ mode: "encode" },
|
||||
mockContext
|
||||
);
|
||||
const decoded = await runHtmlEntity(
|
||||
{ text: (encoded as { type: "text"; value: string }).value },
|
||||
{ mode: "decode" },
|
||||
mockContext
|
||||
);
|
||||
expect((decoded as { type: "text"; value: string }).value).toBe(original);
|
||||
});
|
||||
});
|
||||
82
src/tools/html-entity/run.ts
Normal file
82
src/tools/html-entity/run.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { ToolInput } from "../../core/io/input-types";
|
||||
import type { ToolResult } from "../../core/io/output-types";
|
||||
import type { ToolContext } from "../../core/plugins/plugin-types";
|
||||
import type { HtmlEntityOptions } from "./index";
|
||||
|
||||
const ENTITY_MAP: Record<string, string> = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"©": "©",
|
||||
"®": "®",
|
||||
"™": "™",
|
||||
"—": "—",
|
||||
"–": "–",
|
||||
"«": "«",
|
||||
"»": "»",
|
||||
"°": "°",
|
||||
"±": "±",
|
||||
"×": "×",
|
||||
"÷": "÷",
|
||||
"€": "€",
|
||||
"£": "£",
|
||||
"¥": "¥",
|
||||
"¢": "¢",
|
||||
"§": "§",
|
||||
"¶": "¶",
|
||||
"•": "•",
|
||||
"…": "…",
|
||||
};
|
||||
|
||||
const REVERSE_ENTITY_MAP: Record<string, string> = {};
|
||||
for (const [char, entity] of Object.entries(ENTITY_MAP)) {
|
||||
REVERSE_ENTITY_MAP[entity] = char;
|
||||
}
|
||||
|
||||
function encodeHtmlEntities(text: string): string {
|
||||
return text.replace(/[&<>"']|[©®™—–«»°±×÷€£¥¢§¶•…]/g, (char) => {
|
||||
return ENTITY_MAP[char] || char;
|
||||
});
|
||||
}
|
||||
|
||||
function decodeHtmlEntities(text: string): string {
|
||||
let result = text;
|
||||
|
||||
for (const [entity, char] of Object.entries(REVERSE_ENTITY_MAP)) {
|
||||
result = result.replaceAll(entity, char);
|
||||
}
|
||||
|
||||
result = result.replace(/&#(\d+);/g, (_, code) => {
|
||||
return String.fromCharCode(parseInt(code, 10));
|
||||
});
|
||||
|
||||
result = result.replace(/&#x([0-9a-fA-F]+);/g, (_, code) => {
|
||||
return String.fromCharCode(parseInt(code, 16));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function runHtmlEntity(
|
||||
input: ToolInput,
|
||||
options: HtmlEntityOptions,
|
||||
context: ToolContext
|
||||
): Promise<ToolResult> {
|
||||
const text = input.text ?? "";
|
||||
if (!text) {
|
||||
return { type: "text", value: "" };
|
||||
}
|
||||
|
||||
context.reportProgress({ percentage: 50, message: "Processing..." });
|
||||
|
||||
const result = options.mode === "encode" ? encodeHtmlEntities(text) : decodeHtmlEntities(text);
|
||||
|
||||
context.reportProgress({ percentage: 100, message: "Done" });
|
||||
|
||||
return {
|
||||
type: "text",
|
||||
value: result,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user