From cc0cfa504475ea0cbe31c80a783aa87dd133311e Mon Sep 17 00:00:00 2001 From: anseki Date: Tue, 7 Apr 2015 18:38:14 +0900 Subject: [PATCH] Add placeholder --- README.md | 2 +- lib/read.cs.js | 6 +- lib/read.ps1 | 12 +- lib/read.sh | 10 +- lib/readline-sync.js | 274 ++++++++++++++++++++++++++++++------------- package.json | 2 +- 6 files changed, 209 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 03b3ad7..64ce97a 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ if (readlineSync.keyIn('Are you sure? :', {limit: 'yn'}) === 'y') { // Accept 'y Type: Boolean Default: `false` -By default, the matching is non-case-sensitive when `limit` option (see [limit](#limit) option) is specified (i.e. `a` equals `A`). If `true` is specified, the matching is case-sensitive (i.e. `a` and `A` are different). +By default, the matching is case-insensitive when `limit` option (see [limit](#limit) option) is specified (i.e. `a` equals `A`). If `true` is specified, the matching is case-sensitive (i.e. `a` and `A` are different). ### noTrim diff --git a/lib/read.cs.js b/lib/read.cs.js index 0669e16..b80ea15 100644 --- a/lib/read.cs.js +++ b/lib/read.cs.js @@ -54,15 +54,15 @@ var })({ display: 'string', keyIn: 'boolean', - noEchoBack: 'boolean', + hideEchoBack: 'boolean', mask: 'string', encoded: 'boolean' }); -if (!options.noEchoBack && !options.keyIn) { +if (!options.hideEchoBack && !options.keyIn) { if (options.display) { writeTTY(options.display); } input = readByFSO(); -} else if (options.noEchoBack && !options.keyIn && !options.mask) { +} else if (options.hideEchoBack && !options.keyIn && !options.mask) { if (options.display) { writeTTY(options.display); } input = readByPW(); } else { diff --git a/lib/read.ps1 b/lib/read.ps1 index fdc4c8f..b676e4b 100644 --- a/lib/read.ps1 +++ b/lib/read.ps1 @@ -7,7 +7,7 @@ Param( [string] $display, [switch] $keyIn, - [switch] $noEchoBack, + [switch] $hideEchoBack, [string] $mask, [string] $limit, [switch] $caseSensitive, @@ -26,7 +26,7 @@ function decodeDOS ($arg) { } $options = @{} -foreach ($arg in @('display', 'keyIn', 'noEchoBack', 'mask', 'limit', 'caseSensitive', 'encoded')) { +foreach ($arg in @('display', 'keyIn', 'hideEchoBack', 'mask', 'limit', 'caseSensitive', 'encoded')) { $options.Add($arg, (Get-Variable $arg -ValueOnly)) } if ($options.encoded) { @@ -40,8 +40,8 @@ if ($options.encoded) { [string] $inputTTY = '' [bool] $silent = -not $options.display -and - $options.keyIn -and $options.noEchoBack -and -not $options.mask -[bool] $isCooked = -not $options.noEchoBack -and -not $options.keyIn + $options.keyIn -and $options.hideEchoBack -and -not $options.mask +[bool] $isCooked = -not $options.hideEchoBack -and -not $options.keyIn # Instant method that opens TTY without CreateFile via P/Invoke in .NET Framework # **NOTE** Don't include special characters of DOS in $command when $getRes is True. @@ -66,7 +66,7 @@ if ($options.display) { writeTTY $options.display } -if (-not $options.keyIn -and $options.noEchoBack -and $options.mask -eq '*') { +if (-not $options.keyIn -and $options.hideEchoBack -and $options.mask -eq '*') { $inputTTY = execWithTTY ('$text = Read-Host -AsSecureString;' + '$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($text);' + '[Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)') $True @@ -101,7 +101,7 @@ while ($True) { if ($chunk) { if (-not $isCooked) { - if (-not $options.noEchoBack) { + if (-not $options.hideEchoBack) { writeTTY $chunk } elseif ($options.mask) { writeTTY ($options.mask * $chunk.Length) diff --git a/lib/read.sh b/lib/read.sh index 6aac68d..cd34530 100644 --- a/lib/read.sh +++ b/lib/read.sh @@ -10,10 +10,10 @@ while [ $# -ge 1 ]; do case "$arg" in 'display') shift; options_display="$1";; 'keyin') options_keyIn=true;; - 'noechoback') options_noEchoBack=true;; + 'hideechoback') options_hideEchoBack=true;; 'mask') shift; options_mask="$1";; 'limit') shift; options_limit="$1";; - 'caseSensitive') options_caseSensitive=true;; + 'casesensitive') options_caseSensitive=true;; 'encoded') options_encoded=true;; esac shift @@ -47,8 +47,8 @@ replace_allchars() { ( ) } [ -z "$options_display" ] && [ "$options_keyIn" = true ] && \ - [ "$options_noEchoBack" = true ] && [ -z "$options_mask" ] && silent=true -[ "$options_noEchoBack" != true ] && [ "$options_keyIn" != true ] && is_cooked=true + [ "$options_hideEchoBack" = true ] && [ -z "$options_mask" ] && silent=true +[ "$options_hideEchoBack" != true ] && [ "$options_keyIn" != true ] && is_cooked=true if [ -n "$options_display" ]; then write_tty "$options_display" @@ -102,7 +102,7 @@ do if [ -n "$chunk" ]; then if [ "$is_cooked" != true ]; then - if [ "$options_noEchoBack" != true ]; then + if [ "$options_hideEchoBack" != true ]; then write_tty "$chunk" elif [ -n "$options_mask" ]; then write_tty "$(replace_allchars "$chunk" "$options_mask")" diff --git a/lib/readline-sync.js b/lib/readline-sync.js index 305bd07..f7bdd95 100644 --- a/lib/readline-sync.js +++ b/lib/readline-sync.js @@ -20,18 +20,18 @@ var childProc = require('child_process'), defaultOptions = { - prompt: '> ', - noEchoBack: false, - mask: '*', - limit: [], - limitMessage: 'Input another, please.', - caseSensitive: false, - noTrim: false, - encoding: 'utf8', - bufferSize: 1024, - print: void 0, - isTrue: [], - isFalse: [] + prompt: '> ', + hideEchoBack: false, + mask: '*', + limit: [], + limitMessage: 'Input another, please.${( [)limit(])}', + caseSensitive: false, + keepWhitespace: false, + encoding: 'utf8', + bufferSize: 1024, + print: void 0, + trueValue: [], + falseValue: [] }, useExt = false, @@ -39,18 +39,19 @@ var extHostPath, extHostArgs, tempdir, salt = 0; /* - display: string - keyIn: boolean - noEchoBack: boolean - mask: string - limit: string (pattern) - caseSensitive: boolean - noTrim: boolean + display: string + keyIn: boolean + hideEchoBack: boolean + mask: string + limit: string (pattern) + caseSensitive: boolean + keepWhitespace: boolean encoding, bufferSize, print */ function _readlineSync(options) { var input = '', displaySave = options.display, - silent = !options.display && options.keyIn && options.noEchoBack && !options.mask; + silent = !options.display && + options.keyIn && options.hideEchoBack && !options.mask; function tryExt() { var res = readlineExt(options); @@ -124,7 +125,7 @@ function _readlineSync(options) { (function() { // try read var atEol, limit, - isCooked = !options.noEchoBack && !options.keyIn, + isCooked = !options.hideEchoBack && !options.keyIn, buffer, reqSize, readSize, chunk, line; // Node v0.10- returns an error if same mode is set. @@ -182,7 +183,7 @@ function _readlineSync(options) { if (chunk) { if (!isCooked) { - if (!options.noEchoBack) { + if (!options.hideEchoBack) { fs.writeSync(fdW, chunk); } else if (options.mask) { fs.writeSync(fdW, (new Array(chunk.length + 1)).join(options.mask)); @@ -200,12 +201,12 @@ function _readlineSync(options) { })(); if (options.print && !silent) { // must at least write '\n' - options.print(displaySave + (options.noEchoBack ? + options.print(displaySave + (options.hideEchoBack ? (new Array(input.length + 1)).join(options.mask) : input) + '\n', options.encoding); } - return (options.noTrim || options.keyIn ? input : input.trim()); + return (options.keepWhitespace || options.keyIn ? input : input.trim()); } function readlineExt(options) { @@ -381,7 +382,7 @@ function getHostArgs(options) { })({ display: 'string', keyIn: 'boolean', - noEchoBack: 'boolean', + hideEchoBack: 'boolean', mask: 'string', limit: 'string', caseSensitive: 'boolean', @@ -389,6 +390,19 @@ function getHostArgs(options) { })); } +function flattenArray(array, validator) { + var flatArray = []; + function parseArray(array) { +/* jshint eqnull:true */ + if (array == null) { return; } +/* jshint eqnull:false */ + else if (Array.isArray(array)) { array.forEach(parseArray); } + else if (!validator || validator(array)) { flatArray.push(array); } + } + parseArray(array); + return flatArray; +} + // margeOptions(options1, options2 ... ) // margeOptions(true, options1, options2 ... ) // from defaultOptions function margeOptions() { @@ -411,47 +425,57 @@ function margeOptions() { if (!fromDefault) { optionNames = Object.keys(optionsPart); } optionNames.forEach(function(optionName) { var value; + // ======== DEPRECATED ======== + if (optionName === 'noEchoBack') { optionName = 'hideEchoBack'; } + else if (optionName === 'noTrim') { optionName = 'keepWhitespace'; } + // ======== /DEPRECATED ======== if (!optionsPart.hasOwnProperty(optionName)) { return; } value = optionsPart[optionName]; switch (optionName) { // _readlineSync defaultOptions - // string + // ================ string case 'mask': // * * case 'encoding': // * * case 'limitMessage': // * /* jshint eqnull:true */ - options[optionName] = value != null ? value + '' : ''; + value = value != null ? value + '' : ''; /* jshint eqnull:false */ + if (value && optionName === 'mask' || optionName === 'encoding') + { value = value.replace(/[\r\n]/g, ''); } + options[optionName] = value; break; - // number + // ================ number case 'bufferSize': // * * if (!isNaN(value = parseInt(value, 10)) && typeof value === 'number') { options[optionName] = value; } break; - // boolean - case 'noEchoBack': // * * + // ================ boolean + case 'hideEchoBack': // * * case 'caseSensitive': // * * - case 'noTrim': // * * + case 'keepWhitespace': // * * case 'keyIn': // * options[optionName] = !!value; break; - // function + // ================ function case 'print': // * * options[optionName] = typeof value === 'function' ? value : void 0; break; - // array + // ================ array case 'limit': // * * readlineExt - case 'isTrue': // * - case 'isFalse': // * + case 'trueValue': // * + case 'falseValue': // * /* jshint eqnull:true */ - options[optionName] = value != null ? + value = value != null ? flattenArray(value, function(value) { return typeof value === 'string' || typeof value === 'number' || value instanceof RegExp; }) : []; /* jshint eqnull:false */ + options[optionName] = value.map(function(value) { + return typeof value === 'string' ? value.replace(/[\r\n]/g, '') : value; + }); break; - // other + // ================ other case 'prompt': // * case 'display': // * readlineExt /* jshint eqnull:true */ @@ -464,19 +488,6 @@ function margeOptions() { }, {}); } -function flattenArray(array, validator) { - var flatArray = []; - function parseArray(array) { -/* jshint eqnull:true */ - if (array == null) { return; } -/* jshint eqnull:false */ - else if (Array.isArray(array)) { array.forEach(parseArray); } - else if (!validator || validator(array)) { flatArray.push(array); } - } - parseArray(array); - return flatArray; -} - function isMatched(res, comps, caseSensitive) { return comps.some(function(comp) { if (typeof comp === 'number') { comp += ''; } @@ -488,31 +499,119 @@ function isMatched(res, comps, caseSensitive) { }); } +function replacePlaceholder(text, generator) { + return text.replace(/(\$)?(\$\{(?:\(([\s\S]*?)\))?(\w+|.-.)(?:\(([\s\S]*?)\))?\})/g, + function(str, escape, placeholder, pre, param, post) { + var text; + return escape || typeof(text = generator(param)) !== 'string' ? placeholder : + text ? (pre || '') + text + (post || '') : ''; + }); +} + +function placeholderInMessage(param, options) { + var text, values, group = [], groupClass = -1, charCode = 0, suppressed; + switch (param) { + case 'hideEchoBack': + case 'mask': + case 'caseSensitive': + case 'keepWhitespace': + case 'encoding': + case 'bufferSize': + case 'input': + text = options.hasOwnProperty(param) ? options[param] + '' : ''; + break; + case 'prompt': + case 'query': + case 'display': + text = options.displaySrc + ''; + break; + case 'limit': + case 'trueValue': + case 'falseValue': + values = options[options.hasOwnProperty(param + 'Src') ? param + 'Src' : param]; + if (options.keyIn) { // suppress + values = values.reduce(function(chars, value) { + return chars.concat((value + '').split('')); + }, []) + .reduce(function(groups, curChar) { + var curGroupClass, curCharCode; + if (!options.caseSensitive) { curChar = curChar.toUpperCase(); } + curGroupClass = /^\d$/.test(curChar) ? 1 : + /^[A-Z]$/.test(curChar) ? 2 : /^[a-z]$/.test(curChar) ? 3 : 0; + curCharCode = curChar.charCodeAt(0); + if (curGroupClass && curGroupClass === groupClass && + curCharCode === charCode + 1) { + group.push(curChar); + } else { + if (group.length > 3) { // ellipsis + groups.push(group[0] + ' ... ' + group[group.length - 1]); + suppressed = true; + } else { + groups = groups.concat(group); + } + group = [curChar]; + groupClass = curGroupClass; + } + charCode = curCharCode; + return groups; + }, []); + if (group.length > 3) { // ellipsis + values.push(group[0] + ' ... ' + group[group.length - 1]); + suppressed = true; + } else { + values = values.concat(group); + } + } else { + values = values.filter(function(value) + { return typeof value === 'string' || typeof value === 'number'; }); + } + text = values.join(values.length > 2 ? ', ' : suppressed ? ' / ' : '/'); + break; + case 'limitCount': + case 'limitCountNoZero': + text = options[options.hasOwnProperty('limitSrc') ? 'limitSrc' : 'limit'].length; + text = (text ? text : param === 'limitCountNoZero' ? '' : text) + ''; + break; + } + return text; +} + function readlineWithOptions(options) { - var res, limitSave = options.limit, displaySave = options.display; - options.display += ''; + var res, + generator = function(param) { return placeholderInMessage(param, options); }; + options.limitSrc = options.limit; + options.displaySrc = options.display; options.limit = ''; // for readlineExt + options.display = replacePlaceholder(options.display + '', generator); while (true) { res = _readlineSync(options); - if (!limitSave.length || - isMatched(res, limitSave, options.caseSensitive)) { break; } + if (!options.limitSrc.length || + isMatched(res, options.limitSrc, options.caseSensitive)) { break; } + options.input = res; // for placeholder options.display += (options.display ? '\n' : '') + - (options.limitMessage ? options.limitMessage + '\n' : '') + displaySave; + (options.limitMessage ? + replacePlaceholder(options.limitMessage, generator) + '\n' : '') + + replacePlaceholder(options.displaySrc + '', generator); } return res; } function toBool(res, options) { return ( - (options.isTrue.length && - isMatched(res, options.isTrue, options.caseSensitive)) ? true : - (options.isFalse.length && - isMatched(res, options.isFalse, options.caseSensitive)) ? false : res); + (options.trueValue.length && + isMatched(res, options.trueValue, options.caseSensitive)) ? true : + (options.falseValue.length && + isMatched(res, options.falseValue, options.caseSensitive)) ? false : res); } // for dev exports._useExtSet = function(use) { useExt = use; }; +exports.setDefault = function(options) { + defaultOptions = margeOptions(true, options); + return margeOptions(true); // copy +}; + exports.prompt = function(options) { var readOptions = margeOptions(true, options), res; readOptions.display = readOptions.prompt; @@ -522,7 +621,7 @@ exports.prompt = function(options) { exports.question = function(query, options) { var readOptions = margeOptions(margeOptions(true, options), { - display: query + display: query }), res = readlineWithOptions(readOptions); return toBool(res, readOptions); @@ -530,17 +629,30 @@ exports.question = function(query, options) { exports.keyIn = function(query, options) { var readOptions = margeOptions(margeOptions(true, options), { - display: query, - keyIn: true, - noTrim: true + display: query, + keyIn: true, + keepWhitespace: true }), res; - readOptions.display += ''; - readOptions.limit = readOptions.limit.filter(function(value) - { return typeof value === 'string' || typeof value === 'number'; }) - .join('').replace(/\n/g, '').replace(/[^A-Za-z0-9_ ]/g, '\\$&'); - res = _readlineSync(readOptions); - ['isTrue', 'isFalse'].forEach(function(optionName) { + // char list + readOptions.limitSrc = readOptions.limit.filter(function(value) + { return typeof value === 'string' || typeof value === 'number'; }) + .map(function(text) { // placeholders + return replacePlaceholder(text + '', function(param) { // char list + var matches = /^(.)-(.)$/.exec(param), text = '', from, to, code, step; + if (!matches) { return; } + from = matches[1].charCodeAt(0); + to = matches[2].charCodeAt(0); + step = from < to ? 1 : -1; + for (code = from; code !== to + step; code += step) + { text += String.fromCharCode(code); } + return text; + }); + }); + // pattern + readOptions.limit = readOptions.limitSrc.join('').replace(/[^A-Za-z0-9_ ]/g, '\\$&'); + + ['trueValue', 'falseValue'].forEach(function(optionName) { var comps = []; readOptions[optionName].forEach(function(comp) { if (typeof comp === 'string' || typeof comp === 'number') { @@ -552,22 +664,22 @@ exports.keyIn = function(query, options) { readOptions[optionName] = comps; }); + readOptions.display = replacePlaceholder(readOptions.display + '', + function(param) { return placeholderInMessage(param, readOptions); }); + + res = _readlineSync(readOptions); + return toBool(res, readOptions); }; -exports.setDefault = function(options) { - defaultOptions = margeOptions(true, options); - return margeOptions(true); // copy -}; - -// ======== These APIs are now obsolete. ======== -function setOption(optionName, args) { +// ======== DEPRECATED ======== +function _setOption(optionName, args) { var options; if (args.length) { options = {}; options[optionName] = args[0]; } return exports.setDefault(options)[optionName]; } -exports.setPrint = function() { return setOption('print', arguments); }; -exports.setPrompt = function() { return setOption('prompt', arguments); }; -exports.setEncoding = function() { return setOption('encoding', arguments); }; -exports.setMask = function() { return setOption('mask', arguments); }; -exports.setBufferSize = function() { return setOption('bufferSize', arguments); }; +exports.setPrint = function() { return _setOption('print', arguments); }; +exports.setPrompt = function() { return _setOption('prompt', arguments); }; +exports.setEncoding = function() { return _setOption('encoding', arguments); }; +exports.setMask = function() { return _setOption('mask', arguments); }; +exports.setBufferSize = function() { return _setOption('bufferSize', arguments); }; diff --git a/package.json b/package.json index f804e11..3f060a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "readline-sync", - "version": "0.12.0", + "version": "0.13.0", "title": "readlineSync", "description": "Synchronous Readline for interactively running to have a conversation with the user via a console(TTY).", "keywords": [