pangu
Version:
Opinionated paranoid text spacing: automatically inserts whitespace between CJK (Chinese, Japanese, Korean) and ANS (alphabetical letters, numerical digits and symbols)
134 lines (117 loc) • 4.67 kB
text/typescript
import { text } from 'node:stream/consumers';
import pangu from './index.js';
const usage = `
usage: pangu [-h] [-v] [-t | -f | -c] [text_or_path]
pangu.js v${pangu.version} -- Paranoid text spacing for good readability, to automatically insert whitespace between CJK and half-width characters (alphabetical letters, numerical digits and symbols).
positional arguments:
text_or_path the text or file path to apply spacing; omit it to read stdin when input is piped
optional arguments:
-h, --help show this help message and exit
-v, --version show program's version number and exit
-t, --text specify the input value is a text
-f, --file specify the input value is a file path (pass - to read from stdin)
-c, --check check if text has proper spacing (exit 0 if yes, 1 if no)
`.trim();
const [, , ...args] = process.argv;
type Mode = '--text' | '--file' | '--check';
const modeFlags: Record<string, Mode> = {
'-t': '--text',
'--text': '--text',
'-f': '--file',
'--file': '--file',
'-c': '--check',
'--check': '--check',
};
// An explicit - always means stdin. A missing argument only falls back to stdin when something is actually piped in, so running pangu with no arguments in a terminal still prints
// the usage instead of waiting for input that never comes
function wantsStdin(arg: string | undefined) {
return arg === '-' || (arg === undefined && !process.stdin.isTTY);
}
// Reading has to be async: readFileSync(0) throws EAGAIN once a pipe carries more than a buffer or two. console.log() puts a trailing newline back, so dropping one here passes piped
// input through byte for byte
async function readStdin() {
return (await text(process.stdin)).replace(/\n$/, '');
}
function spaceTextPrinted(text: string | undefined) {
if (typeof text === 'string') {
console.log(pangu.spaceText(text));
} else {
console.log(usage);
process.exitCode = 1;
}
}
// An empty string is what -f "$EMPTY_VAR" expands to, so it counts as a missing path rather than a file to open
function spaceFilePrinted(path: string | undefined) {
if (path) {
// File mode only does spacing: no newline appended, the file's own EOF newlines (or lack of one) pass through untouched
process.stdout.write(pangu.spaceFileSync(path));
} else {
console.error('pangu: error: argument --file: expected a file path');
console.log(usage);
process.exitCode = 1;
}
}
function checkSpacing(text: string | undefined) {
if (typeof text === 'string') {
const hasProperSpacing = pangu.hasProperSpacing(text);
if (!hasProperSpacing) {
// Print the corrected version to stderr so a failing -c is debuggable. stdout stays empty, so -c composes in a pipeline
console.error(`Corrected: ${pangu.spaceText(text)}`);
}
process.exitCode = hasProperSpacing ? 0 : 1;
} else {
console.log(usage);
process.exitCode = 1;
}
}
// Every exit goes through process.exitCode rather than process.exit(), because process.exit() truncates a piped stdout at one pipe buffer and pangu now streams whole files through it
async function main() {
// -t, -f and -c are mutually exclusive
const givenModes = new Set(args.filter((arg) => arg in modeFlags).map((flag) => modeFlags[flag]));
if (givenModes.size > 1) {
const [first, second] = [...givenModes];
console.error(`pangu: error: argument ${second}: not allowed with argument ${first}`);
console.log(usage);
process.exitCode = 1;
return;
}
if (args.length === 0) {
spaceTextPrinted(wantsStdin(undefined) ? await readStdin() : undefined);
return;
}
switch (args[0]) {
case '-h':
case '--help':
console.log(usage);
break;
case '-v':
case '--version':
console.log(`pangu.js ${pangu.version}`);
break;
case '-t':
case '--text':
spaceTextPrinted(wantsStdin(args[1]) ? await readStdin() : args[1]);
break;
case '-f':
case '--file':
// An explicit - is the conventional spelling for "the file is stdin" (cf. tar -f -). A missing path is a usage error instead of a stdin fallback, so that -f with an empty path variable
// reports the missing path instead of silently spacing whatever happens to be piped in
if (args[1] === '-') {
spaceTextPrinted(await readStdin());
} else {
spaceFilePrinted(args[1]);
}
break;
case '-c':
case '--check':
checkSpacing(wantsStdin(args[1]) ? await readStdin() : args[1]);
break;
case '-':
spaceTextPrinted(await readStdin());
break;
default:
spaceTextPrinted(args[0]);
}
}
await main();