pangu
Version:
Paranoid text spacing for good readability, to automatically insert whitespace between CJK (Chinese, Japanese, Korean) and half-width characters (alphabetical letters, numerical digits and symbols).
138 lines (121 loc) • 4.69 kB
text/typescript
import pangu from './index.js';
const usage = `
usage: pangu [-h] [-v] [-t | -f | -c] [text_or_path]
pangu.js -- 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() {
process.stdin.setEncoding('utf8');
const chunks: string[] = [];
for await (const chunk of process.stdin) {
chunks.push(String(chunk));
}
return chunks.join('').replace(/\n$/, '');
}
function printSpacingText(text: string | undefined) {
if (typeof text === 'string') {
console.log(pangu.spacingText(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 printSpacingFile(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.spacingFileSync(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) {
// Optionally print the corrected version to stderr for debugging
console.error(`Corrected: ${pangu.spacingText(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) {
printSpacingText(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':
printSpacingText(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] === '-') {
printSpacingText(await readStdin());
} else {
printSpacingFile(args[1]);
}
break;
case '-c':
case '--check':
checkSpacing(wantsStdin(args[1]) ? await readStdin() : args[1]);
break;
case '-':
printSpacingText(await readStdin());
break;
default:
printSpacingText(args[0]);
}
}
await main();