feat: add Escape Smart Characters operation (#2391)
This commit is contained in:
parent
da202d54f1
commit
c929819189
@ -49,6 +49,7 @@
|
|||||||
"Escape Unicode Characters",
|
"Escape Unicode Characters",
|
||||||
"Unescape Unicode Characters",
|
"Unescape Unicode Characters",
|
||||||
"Normalise Unicode",
|
"Normalise Unicode",
|
||||||
|
"Escape Smart Characters",
|
||||||
"To Quoted Printable",
|
"To Quoted Printable",
|
||||||
"From Quoted Printable",
|
"From Quoted Printable",
|
||||||
"To Punycode",
|
"To Punycode",
|
||||||
|
|||||||
129
src/core/operations/EscapeSmartCharacters.mjs
Normal file
129
src/core/operations/EscapeSmartCharacters.mjs
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* @author HarelKatz [github.com/HarelKatz]
|
||||||
|
* @copyright Crown Copyright 2026
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape Smart Characters operation
|
||||||
|
*/
|
||||||
|
class EscapeSmartCharacters extends Operation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EscapeSmartCharacters constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "Escape Smart Characters";
|
||||||
|
this.module = "Default";
|
||||||
|
this.description = "Converts smart (typographic) Unicode characters — e.g. smart quotes, em/en dashes, ellipses, ©, ®, ™, arrows — into their plain ASCII equivalents.<br><br>Characters with no ASCII mapping (e.g. <code>☣</code>) are handled according to the 'Unmappable characters' option.<br><br>e.g. <code>“Hello” — world…</code> becomes <code>\"Hello\" -- world...</code>";
|
||||||
|
this.infoURL = "";
|
||||||
|
this.inputType = "string";
|
||||||
|
this.outputType = "string";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
name: "Unmappable characters",
|
||||||
|
type: "option",
|
||||||
|
value: ["Include", "Remove", "Replace with '.'"]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
const [unmappable] = args;
|
||||||
|
let result = "";
|
||||||
|
for (const ch of input) {
|
||||||
|
if (ch.codePointAt(0) < 128) {
|
||||||
|
result += ch;
|
||||||
|
} else if (Object.prototype.hasOwnProperty.call(SMART_MAP, ch)) {
|
||||||
|
result += SMART_MAP[ch];
|
||||||
|
} else {
|
||||||
|
switch (unmappable) {
|
||||||
|
case "Remove":
|
||||||
|
break;
|
||||||
|
case "Replace with '.'":
|
||||||
|
result += ".";
|
||||||
|
break;
|
||||||
|
case "Include":
|
||||||
|
default:
|
||||||
|
result += ch;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const SMART_MAP = {
|
||||||
|
// Smart double quotes
|
||||||
|
"“": "\"", // “ left double quotation mark
|
||||||
|
"”": "\"", // ” right double quotation mark
|
||||||
|
"„": "\"", // „ double low-9 quotation mark
|
||||||
|
"‟": "\"", // ‟ double high-reversed-9 quotation mark
|
||||||
|
"″": "\"", // ″ double prime
|
||||||
|
|
||||||
|
// Smart single quotes / apostrophes
|
||||||
|
"‘": "'", // ‘ left single quotation mark
|
||||||
|
"’": "'", // ’ right single quotation mark / apostrophe
|
||||||
|
"‚": "'", // ‚ single low-9 quotation mark
|
||||||
|
"‛": "'", // ‛ single high-reversed-9 quotation mark
|
||||||
|
"′": "'", // ′ prime
|
||||||
|
|
||||||
|
// Dashes & hyphens
|
||||||
|
"‐": "-", // ‐ hyphen
|
||||||
|
"‑": "-", // ‑ non-breaking hyphen
|
||||||
|
"‒": "-", // ‒ figure dash
|
||||||
|
"–": "-", // – en dash
|
||||||
|
"—": "--", // — em dash
|
||||||
|
"―": "--", // ― horizontal bar
|
||||||
|
|
||||||
|
// Ellipsis
|
||||||
|
"…": "...", // …
|
||||||
|
|
||||||
|
// Trademark / copyright symbols
|
||||||
|
"©": "(c)", // ©
|
||||||
|
"®": "(r)", // ®
|
||||||
|
"™": "(tm)", // ™
|
||||||
|
|
||||||
|
// Arrows
|
||||||
|
"←": "<--", // ←
|
||||||
|
"→": "-->", // →
|
||||||
|
"↑": "^", // ↑
|
||||||
|
"↓": "v", // ↓
|
||||||
|
"↔": "<->", // ↔
|
||||||
|
"⇐": "<==", // ⇐
|
||||||
|
"⇒": "==>", // ⇒
|
||||||
|
"⇔": "<=>", // ⇔
|
||||||
|
|
||||||
|
// Guillemets
|
||||||
|
"«": "<<", // «
|
||||||
|
"»": ">>", // »
|
||||||
|
"‹": "<", // ‹
|
||||||
|
"›": ">", // ›
|
||||||
|
|
||||||
|
// Math & misc symbols
|
||||||
|
"×": "x", // ×
|
||||||
|
"÷": "/", // ÷
|
||||||
|
"±": "+/-", // ±
|
||||||
|
"•": "*", // •
|
||||||
|
"·": ".", // ·
|
||||||
|
|
||||||
|
// Non-ASCII spaces
|
||||||
|
"\u00A0": " ", // NBSP
|
||||||
|
"\u2002": " ", // en space
|
||||||
|
"\u2003": " ", // em space
|
||||||
|
"\u2009": " ", // thin space
|
||||||
|
"\u200A": " " // hair space
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EscapeSmartCharacters;
|
||||||
@ -69,6 +69,7 @@ import "./tests/DropNthBytes.mjs";
|
|||||||
import "./tests/ECDSA.mjs";
|
import "./tests/ECDSA.mjs";
|
||||||
import "./tests/ELFInfo.mjs";
|
import "./tests/ELFInfo.mjs";
|
||||||
import "./tests/Enigma.mjs";
|
import "./tests/Enigma.mjs";
|
||||||
|
import "./tests/EscapeSmartCharacters.mjs";
|
||||||
import "./tests/ExtractAudioMetadata.mjs";
|
import "./tests/ExtractAudioMetadata.mjs";
|
||||||
import "./tests/ExtractEmailAddresses.mjs";
|
import "./tests/ExtractEmailAddresses.mjs";
|
||||||
import "./tests/ExtractHashes.mjs";
|
import "./tests/ExtractHashes.mjs";
|
||||||
|
|||||||
132
tests/operations/tests/EscapeSmartCharacters.mjs
Normal file
132
tests/operations/tests/EscapeSmartCharacters.mjs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* Escape Smart Characters tests
|
||||||
|
*
|
||||||
|
* @author HarelKatz [github.com/HarelKatz]
|
||||||
|
* @copyright Crown Copyright 2026
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
import TestRegister from "../../lib/TestRegister.mjs";
|
||||||
|
|
||||||
|
TestRegister.addTests([
|
||||||
|
{
|
||||||
|
"name": "Escape Smart Characters: smart quotes and apostrophes",
|
||||||
|
"input": "“Hello,” she said, ‘yes.’",
|
||||||
|
"expectedOutput": "\"Hello,\" she said, 'yes.'",
|
||||||
|
"recipeConfig": [
|
||||||
|
{
|
||||||
|
"op": "Escape Smart Characters",
|
||||||
|
"args": ["Include"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Escape Smart Characters: em dash, en dash and ellipsis",
|
||||||
|
"input": "page 1–3 — wait…",
|
||||||
|
"expectedOutput": "page 1-3 -- wait...",
|
||||||
|
"recipeConfig": [
|
||||||
|
{
|
||||||
|
"op": "Escape Smart Characters",
|
||||||
|
"args": ["Include"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Escape Smart Characters: trademark symbols",
|
||||||
|
"input": "Foo© Bar® Baz™",
|
||||||
|
"expectedOutput": "Foo(c) Bar(r) Baz(tm)",
|
||||||
|
"recipeConfig": [
|
||||||
|
{
|
||||||
|
"op": "Escape Smart Characters",
|
||||||
|
"args": ["Include"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Escape Smart Characters: arrows and guillemets",
|
||||||
|
"input": "← → ↔ ⇒ « »",
|
||||||
|
"expectedOutput": "<-- --> <-> ==> << >>",
|
||||||
|
"recipeConfig": [
|
||||||
|
{
|
||||||
|
"op": "Escape Smart Characters",
|
||||||
|
"args": ["Include"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Escape Smart Characters: math and misc",
|
||||||
|
"input": "3 × 4 ÷ 2 = 6, ±0.5 • item",
|
||||||
|
"expectedOutput": "3 x 4 / 2 = 6, +/-0.5 * item",
|
||||||
|
"recipeConfig": [
|
||||||
|
{
|
||||||
|
"op": "Escape Smart Characters",
|
||||||
|
"args": ["Include"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Escape Smart Characters: NBSP becomes regular space",
|
||||||
|
"input": "a b c",
|
||||||
|
"expectedOutput": "a b c",
|
||||||
|
"recipeConfig": [
|
||||||
|
{
|
||||||
|
"op": "Escape Smart Characters",
|
||||||
|
"args": ["Include"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Escape Smart Characters: unmappable Include preserves char",
|
||||||
|
"input": "warning: ☣ hazard",
|
||||||
|
"expectedOutput": "warning: ☣ hazard",
|
||||||
|
"recipeConfig": [
|
||||||
|
{
|
||||||
|
"op": "Escape Smart Characters",
|
||||||
|
"args": ["Include"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Escape Smart Characters: unmappable Remove drops char",
|
||||||
|
"input": "warning: ☣ hazard",
|
||||||
|
"expectedOutput": "warning: hazard",
|
||||||
|
"recipeConfig": [
|
||||||
|
{
|
||||||
|
"op": "Escape Smart Characters",
|
||||||
|
"args": ["Remove"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Escape Smart Characters: unmappable Replace substitutes dot",
|
||||||
|
"input": "warning: ☣ hazard",
|
||||||
|
"expectedOutput": "warning: . hazard",
|
||||||
|
"recipeConfig": [
|
||||||
|
{
|
||||||
|
"op": "Escape Smart Characters",
|
||||||
|
"args": ["Replace with '.'"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Escape Smart Characters: pure ASCII passes through",
|
||||||
|
"input": "hello world! 123",
|
||||||
|
"expectedOutput": "hello world! 123",
|
||||||
|
"recipeConfig": [
|
||||||
|
{
|
||||||
|
"op": "Escape Smart Characters",
|
||||||
|
"args": ["Include"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Escape Smart Characters: empty input",
|
||||||
|
"input": "",
|
||||||
|
"expectedOutput": "",
|
||||||
|
"recipeConfig": [
|
||||||
|
{
|
||||||
|
"op": "Escape Smart Characters",
|
||||||
|
"args": ["Include"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
Loading…
x
Reference in New Issue
Block a user