Redesign an existing website to make it better. Built on top of impeccable.
71
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
#!/usr/bin/env node
/**
* skills/deploy/scripts/sanitise.js
*
* Encodes all non-ASCII characters in an HTML file to named or numeric HTML
* entities before uploading to Document Authoring (DA).
*
* DA strips <head> on ingestion and parses the document without a charset
* declaration. Any multibyte UTF-8 sequence it can't decode becomes U+FFFD
* (the replacement character �). HTML entities survive the round-trip
* unchanged.
*
* Usage:
* node skills/deploy/scripts/sanitise.js content/page.html # in-place
* node skills/deploy/scripts/sanitise.js content/page.html out.html # explicit output file
* node skills/deploy/scripts/sanitise.js < input.html # stdin -> stdout
* npm run da:sanitise -- content/page.html
*
* NEVER pass more than two paths. The two-arg form is <input> <output>, so a
* batch invocation (`sanitise.js a.html b.html c.html`) would treat b as the
* OUTPUT and silently overwrite it with a's content (recorded field failure —
* the third file was ignored on top). The script now refuses >2 args; batch
* by running once per file: for f in content/*.html; do node …/sanitise.js "$f"; done
*
* Exit codes: 0 success, 1 error.
*/
import { readFileSync, writeFileSync } from 'fs';
// Named HTML entities for common non-ASCII characters.
// Anything not listed here falls back to a decimal numeric reference (&#NNN;).
const NAMED = new Map([
[0x00A0, ' '], [0x00A1, '¡'], [0x00A3, '£'],
[0x00A5, '¥'], [0x00A9, '©'], [0x00AB, '«'],
[0x00AE, '®'], [0x00B0, '°'], [0x00B1, '±'],
[0x00B5, 'µ'], [0x00B7, '·'], [0x00BB, '»'],
[0x00BC, '¼'], [0x00BD, '½'], [0x00BE, '¾'],
[0x00BF, '¿'], [0x00C0, 'À'], [0x00C1, 'Á'],
[0x00C2, 'Â'], [0x00C4, 'Ä'], [0x00C7, 'Ç'],
[0x00C9, 'É'], [0x00CE, 'Î'], [0x00D1, 'Ñ'],
[0x00D6, 'Ö'], [0x00D7, '×'], [0x00DA, 'Ú'],
[0x00DC, 'Ü'], [0x00E0, 'à'], [0x00E1, 'á'],
[0x00E2, 'â'], [0x00E3, 'ã'], [0x00E4, 'ä'],
[0x00E5, 'å'], [0x00E6, 'æ'], [0x00E7, 'ç'],
[0x00E8, 'è'], [0x00E9, 'é'], [0x00EA, 'ê'],
[0x00EB, 'ë'], [0x00EC, 'ì'], [0x00ED, 'í'],
[0x00EE, 'î'], [0x00EF, 'ï'], [0x00F0, 'ð'],
[0x00F1, 'ñ'], [0x00F2, 'ò'], [0x00F3, 'ó'],
[0x00F4, 'ô'], [0x00F5, 'õ'], [0x00F6, 'ö'],
[0x00F7, '÷'], [0x00F8, 'ø'], [0x00F9, 'ù'],
[0x00FA, 'ú'], [0x00FB, 'û'], [0x00FC, 'ü'],
[0x00FD, 'ý'], [0x00FF, 'ÿ'], [0x2013, '–'],
[0x2014, '—'], [0x2018, '‘'], [0x2019, '’'],
[0x201C, '“'], [0x201D, '”'], [0x2022, '•'],
[0x2026, '…'], [0x2122, '™'], [0x2190, '←'],
[0x2191, '↑'], [0x2192, '→'], [0x2193, '↓'],
[0x20AC, '€'],
]);
/**
* Encode all non-ASCII code points in a string to HTML entities.
* Handles surrogate pairs (emoji, supplementary CJK) correctly via codePointAt.
* @param {string} input
* @returns {{ output: string, count: number }}
*/
function encode(input) {
const parts = [];
let count = 0;
for (let i = 0; i < input.length; i += 1) {
const cp = input.codePointAt(i);
if (cp <= 0x7F) {
parts.push(input[i]);
// eslint-disable-next-line no-continue
continue;
}
// Consume the surrogate pair as one logical character.
if (cp > 0xFFFF) i += 1;
parts.push(NAMED.has(cp) ? NAMED.get(cp) : `&#${cp};`);
count += 1;
}
return { output: parts.join(''), count };
}
const args = process.argv.slice(2);
const fromStdin = args.length === 0 || args[0] === '-';
// Batch foot-gun guard: with >2 paths the two-arg <input> <output> convention
// would silently overwrite the second file with the first's content and drop
// the rest (recorded). Refuse loudly instead.
if (args.length > 2) {
process.stderr.write(
'da-sanitise: too many arguments — usage is <input> (in-place) or <input> <output>.\n'
+ 'A batch call would overwrite the second file with the first\'s content; run once per file:\n'
+ ' for f in content/*.html; do node skills/deploy/scripts/sanitise.js "$f"; done\n',
);
process.exit(1);
}
let input;
if (fromStdin) {
input = readFileSync(process.stdin.fd, 'utf8');
} else {
const inputPath = args[0];
try {
input = readFileSync(inputPath, 'utf8');
} catch (err) {
process.stderr.write(`da-sanitise: cannot read '${inputPath}': ${err.message}\n`);
process.exit(1);
}
}
const { output, count } = encode(input);
if (fromStdin) {
process.stdout.write(output);
} else {
const outputPath = args[1] || args[0]; // explicit output or in-place
try {
writeFileSync(outputPath, output, 'utf8');
} catch (err) {
process.stderr.write(`da-sanitise: cannot write '${outputPath}': ${err.message}\n`);
process.exit(1);
}
const msg = count
? `da-sanitise: encoded ${count} non-ASCII character(s) -> ${outputPath}\n`
: `da-sanitise: no non-ASCII characters found -- ${outputPath} unchanged\n`;
process.stderr.write(msg);
}.tessl-plugin
skills
audit
reference
deploy
diff
direct
extract
migrate
prepare-migration
prototype
replica
reskin
rollout
stardust
uplift
reference