First implementation of Plimi

This commit is contained in:
achraf
2026-06-02 19:32:51 +02:00
commit be635b1828
136 changed files with 13663 additions and 0 deletions

View 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,
};

View 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("&lt;div class=&quot;test&quot;&gt;Hello &amp; World&lt;/div&gt;");
});
it("should decode basic HTML entities", async () => {
const result = await runHtmlEntity(
{ text: "&lt;div&gt;Hello &amp; World&lt;/div&gt;" },
{ 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("&euro;");
expect(value).toContain("&copy;");
});
it("should decode special symbol entities", async () => {
const result = await runHtmlEntity(
{ text: "&copy; 2024 &mdash; 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: "&#65;&#66;&#67;" },
{ 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: "&#x41;&#x42;&#x43;" },
{ 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("&#39;");
});
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);
});
});

View 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> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
"©": "&copy;",
"®": "&reg;",
"™": "&trade;",
"—": "&mdash;",
"": "&ndash;",
"«": "&laquo;",
"»": "&raquo;",
"°": "&deg;",
"±": "&plusmn;",
"×": "&times;",
"÷": "&divide;",
"€": "&euro;",
"£": "&pound;",
"¥": "&yen;",
"¢": "&cent;",
"§": "&sect;",
"¶": "&para;",
"•": "&bull;",
"…": "&hellip;",
};
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,
};
}