feat: add Escape Smart Characters operation (#2391)
This commit is contained in:
parent
da202d54f1
commit
c929819189
@ -49,6 +49,7 @@
|
||||
"Escape Unicode Characters",
|
||||
"Unescape Unicode Characters",
|
||||
"Normalise Unicode",
|
||||
"Escape Smart Characters",
|
||||
"To Quoted Printable",
|
||||
"From Quoted Printable",
|
||||
"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/ELFInfo.mjs";
|
||||
import "./tests/Enigma.mjs";
|
||||
import "./tests/EscapeSmartCharacters.mjs";
|
||||
import "./tests/ExtractAudioMetadata.mjs";
|
||||
import "./tests/ExtractEmailAddresses.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