Add placeholder

This commit is contained in:
anseki 2015-04-07 18:38:14 +09:00
parent 008f31fb6d
commit cc0cfa5044
6 changed files with 209 additions and 97 deletions

View file

@ -216,7 +216,7 @@ if (readlineSync.keyIn('Are you sure? :', {limit: 'yn'}) === 'y') { // Accept 'y
Type: Boolean Type: Boolean
Default: `false` 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 ### noTrim

View file

@ -54,15 +54,15 @@ var
})({ })({
display: 'string', display: 'string',
keyIn: 'boolean', keyIn: 'boolean',
noEchoBack: 'boolean', hideEchoBack: 'boolean',
mask: 'string', mask: 'string',
encoded: 'boolean' encoded: 'boolean'
}); });
if (!options.noEchoBack && !options.keyIn) { if (!options.hideEchoBack && !options.keyIn) {
if (options.display) { writeTTY(options.display); } if (options.display) { writeTTY(options.display); }
input = readByFSO(); input = readByFSO();
} else if (options.noEchoBack && !options.keyIn && !options.mask) { } else if (options.hideEchoBack && !options.keyIn && !options.mask) {
if (options.display) { writeTTY(options.display); } if (options.display) { writeTTY(options.display); }
input = readByPW(); input = readByPW();
} else { } else {

View file

@ -7,7 +7,7 @@
Param( Param(
[string] $display, [string] $display,
[switch] $keyIn, [switch] $keyIn,
[switch] $noEchoBack, [switch] $hideEchoBack,
[string] $mask, [string] $mask,
[string] $limit, [string] $limit,
[switch] $caseSensitive, [switch] $caseSensitive,
@ -26,7 +26,7 @@ function decodeDOS ($arg) {
} }
$options = @{} $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)) $options.Add($arg, (Get-Variable $arg -ValueOnly))
} }
if ($options.encoded) { if ($options.encoded) {
@ -40,8 +40,8 @@ if ($options.encoded) {
[string] $inputTTY = '' [string] $inputTTY = ''
[bool] $silent = -not $options.display -and [bool] $silent = -not $options.display -and
$options.keyIn -and $options.noEchoBack -and -not $options.mask $options.keyIn -and $options.hideEchoBack -and -not $options.mask
[bool] $isCooked = -not $options.noEchoBack -and -not $options.keyIn [bool] $isCooked = -not $options.hideEchoBack -and -not $options.keyIn
# Instant method that opens TTY without CreateFile via P/Invoke in .NET Framework # 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. # **NOTE** Don't include special characters of DOS in $command when $getRes is True.
@ -66,7 +66,7 @@ if ($options.display) {
writeTTY $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;' + $inputTTY = execWithTTY ('$text = Read-Host -AsSecureString;' +
'$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($text);' + '$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($text);' +
'[Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)') $True '[Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)') $True
@ -101,7 +101,7 @@ while ($True) {
if ($chunk) { if ($chunk) {
if (-not $isCooked) { if (-not $isCooked) {
if (-not $options.noEchoBack) { if (-not $options.hideEchoBack) {
writeTTY $chunk writeTTY $chunk
} elseif ($options.mask) { } elseif ($options.mask) {
writeTTY ($options.mask * $chunk.Length) writeTTY ($options.mask * $chunk.Length)

View file

@ -10,10 +10,10 @@ while [ $# -ge 1 ]; do
case "$arg" in case "$arg" in
'display') shift; options_display="$1";; 'display') shift; options_display="$1";;
'keyin') options_keyIn=true;; 'keyin') options_keyIn=true;;
'noechoback') options_noEchoBack=true;; 'hideechoback') options_hideEchoBack=true;;
'mask') shift; options_mask="$1";; 'mask') shift; options_mask="$1";;
'limit') shift; options_limit="$1";; 'limit') shift; options_limit="$1";;
'caseSensitive') options_caseSensitive=true;; 'casesensitive') options_caseSensitive=true;;
'encoded') options_encoded=true;; 'encoded') options_encoded=true;;
esac esac
shift shift
@ -47,8 +47,8 @@ replace_allchars() { (
) } ) }
[ -z "$options_display" ] && [ "$options_keyIn" = true ] && \ [ -z "$options_display" ] && [ "$options_keyIn" = true ] && \
[ "$options_noEchoBack" = true ] && [ -z "$options_mask" ] && silent=true [ "$options_hideEchoBack" = true ] && [ -z "$options_mask" ] && silent=true
[ "$options_noEchoBack" != true ] && [ "$options_keyIn" != true ] && is_cooked=true [ "$options_hideEchoBack" != true ] && [ "$options_keyIn" != true ] && is_cooked=true
if [ -n "$options_display" ]; then if [ -n "$options_display" ]; then
write_tty "$options_display" write_tty "$options_display"
@ -102,7 +102,7 @@ do
if [ -n "$chunk" ]; then if [ -n "$chunk" ]; then
if [ "$is_cooked" != true ]; then if [ "$is_cooked" != true ]; then
if [ "$options_noEchoBack" != true ]; then if [ "$options_hideEchoBack" != true ]; then
write_tty "$chunk" write_tty "$chunk"
elif [ -n "$options_mask" ]; then elif [ -n "$options_mask" ]; then
write_tty "$(replace_allchars "$chunk" "$options_mask")" write_tty "$(replace_allchars "$chunk" "$options_mask")"

View file

@ -20,18 +20,18 @@ var
childProc = require('child_process'), childProc = require('child_process'),
defaultOptions = { defaultOptions = {
prompt: '> ', prompt: '> ',
noEchoBack: false, hideEchoBack: false,
mask: '*', mask: '*',
limit: [], limit: [],
limitMessage: 'Input another, please.', limitMessage: 'Input another, please.${( [)limit(])}',
caseSensitive: false, caseSensitive: false,
noTrim: false, keepWhitespace: false,
encoding: 'utf8', encoding: 'utf8',
bufferSize: 1024, bufferSize: 1024,
print: void 0, print: void 0,
isTrue: [], trueValue: [],
isFalse: [] falseValue: []
}, },
useExt = false, useExt = false,
@ -39,18 +39,19 @@ var
extHostPath, extHostArgs, tempdir, salt = 0; extHostPath, extHostArgs, tempdir, salt = 0;
/* /*
display: string display: string
keyIn: boolean keyIn: boolean
noEchoBack: boolean hideEchoBack: boolean
mask: string mask: string
limit: string (pattern) limit: string (pattern)
caseSensitive: boolean caseSensitive: boolean
noTrim: boolean keepWhitespace: boolean
encoding, bufferSize, print encoding, bufferSize, print
*/ */
function _readlineSync(options) { function _readlineSync(options) {
var input = '', displaySave = options.display, 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() { function tryExt() {
var res = readlineExt(options); var res = readlineExt(options);
@ -124,7 +125,7 @@ function _readlineSync(options) {
(function() { // try read (function() { // try read
var atEol, limit, var atEol, limit,
isCooked = !options.noEchoBack && !options.keyIn, isCooked = !options.hideEchoBack && !options.keyIn,
buffer, reqSize, readSize, chunk, line; buffer, reqSize, readSize, chunk, line;
// Node v0.10- returns an error if same mode is set. // Node v0.10- returns an error if same mode is set.
@ -182,7 +183,7 @@ function _readlineSync(options) {
if (chunk) { if (chunk) {
if (!isCooked) { if (!isCooked) {
if (!options.noEchoBack) { if (!options.hideEchoBack) {
fs.writeSync(fdW, chunk); fs.writeSync(fdW, chunk);
} else if (options.mask) { } else if (options.mask) {
fs.writeSync(fdW, (new Array(chunk.length + 1)).join(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' 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', (new Array(input.length + 1)).join(options.mask) : input) + '\n',
options.encoding); options.encoding);
} }
return (options.noTrim || options.keyIn ? input : input.trim()); return (options.keepWhitespace || options.keyIn ? input : input.trim());
} }
function readlineExt(options) { function readlineExt(options) {
@ -381,7 +382,7 @@ function getHostArgs(options) {
})({ })({
display: 'string', display: 'string',
keyIn: 'boolean', keyIn: 'boolean',
noEchoBack: 'boolean', hideEchoBack: 'boolean',
mask: 'string', mask: 'string',
limit: 'string', limit: 'string',
caseSensitive: 'boolean', 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(options1, options2 ... )
// margeOptions(true, options1, options2 ... ) // from defaultOptions // margeOptions(true, options1, options2 ... ) // from defaultOptions
function margeOptions() { function margeOptions() {
@ -411,47 +425,57 @@ function margeOptions() {
if (!fromDefault) { optionNames = Object.keys(optionsPart); } if (!fromDefault) { optionNames = Object.keys(optionsPart); }
optionNames.forEach(function(optionName) { optionNames.forEach(function(optionName) {
var value; var value;
// ======== DEPRECATED ========
if (optionName === 'noEchoBack') { optionName = 'hideEchoBack'; }
else if (optionName === 'noTrim') { optionName = 'keepWhitespace'; }
// ======== /DEPRECATED ========
if (!optionsPart.hasOwnProperty(optionName)) { return; } if (!optionsPart.hasOwnProperty(optionName)) { return; }
value = optionsPart[optionName]; value = optionsPart[optionName];
switch (optionName) { switch (optionName) {
// _readlineSync defaultOptions // _readlineSync defaultOptions
// string // ================ string
case 'mask': // * * case 'mask': // * *
case 'encoding': // * * case 'encoding': // * *
case 'limitMessage': // * case 'limitMessage': // *
/* jshint eqnull:true */ /* jshint eqnull:true */
options[optionName] = value != null ? value + '' : ''; value = value != null ? value + '' : '';
/* jshint eqnull:false */ /* jshint eqnull:false */
if (value && optionName === 'mask' || optionName === 'encoding')
{ value = value.replace(/[\r\n]/g, ''); }
options[optionName] = value;
break; break;
// number // ================ number
case 'bufferSize': // * * case 'bufferSize': // * *
if (!isNaN(value = parseInt(value, 10)) && typeof value === 'number') if (!isNaN(value = parseInt(value, 10)) && typeof value === 'number')
{ options[optionName] = value; } { options[optionName] = value; }
break; break;
// boolean // ================ boolean
case 'noEchoBack': // * * case 'hideEchoBack': // * *
case 'caseSensitive': // * * case 'caseSensitive': // * *
case 'noTrim': // * * case 'keepWhitespace': // * *
case 'keyIn': // * case 'keyIn': // *
options[optionName] = !!value; options[optionName] = !!value;
break; break;
// function // ================ function
case 'print': // * * case 'print': // * *
options[optionName] = typeof value === 'function' ? value : void 0; options[optionName] = typeof value === 'function' ? value : void 0;
break; break;
// array // ================ array
case 'limit': // * * readlineExt case 'limit': // * * readlineExt
case 'isTrue': // * case 'trueValue': // *
case 'isFalse': // * case 'falseValue': // *
/* jshint eqnull:true */ /* jshint eqnull:true */
options[optionName] = value != null ? value = value != null ?
flattenArray(value, function(value) { flattenArray(value, function(value) {
return typeof value === 'string' || typeof value === 'number' || return typeof value === 'string' || typeof value === 'number' ||
value instanceof RegExp; value instanceof RegExp;
}) : []; }) : [];
/* jshint eqnull:false */ /* jshint eqnull:false */
options[optionName] = value.map(function(value) {
return typeof value === 'string' ? value.replace(/[\r\n]/g, '') : value;
});
break; break;
// other // ================ other
case 'prompt': // * case 'prompt': // *
case 'display': // * readlineExt case 'display': // * readlineExt
/* jshint eqnull:true */ /* 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) { function isMatched(res, comps, caseSensitive) {
return comps.some(function(comp) { return comps.some(function(comp) {
if (typeof comp === 'number') { 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) { function readlineWithOptions(options) {
var res, limitSave = options.limit, displaySave = options.display; var res,
options.display += ''; generator = function(param) { return placeholderInMessage(param, options); };
options.limitSrc = options.limit;
options.displaySrc = options.display;
options.limit = ''; // for readlineExt options.limit = ''; // for readlineExt
options.display = replacePlaceholder(options.display + '', generator);
while (true) { while (true) {
res = _readlineSync(options); res = _readlineSync(options);
if (!limitSave.length || if (!options.limitSrc.length ||
isMatched(res, limitSave, options.caseSensitive)) { break; } isMatched(res, options.limitSrc, options.caseSensitive)) { break; }
options.input = res; // for placeholder
options.display += (options.display ? '\n' : '') + options.display += (options.display ? '\n' : '') +
(options.limitMessage ? options.limitMessage + '\n' : '') + displaySave; (options.limitMessage ?
replacePlaceholder(options.limitMessage, generator) + '\n' : '') +
replacePlaceholder(options.displaySrc + '', generator);
} }
return res; return res;
} }
function toBool(res, options) { function toBool(res, options) {
return ( return (
(options.isTrue.length && (options.trueValue.length &&
isMatched(res, options.isTrue, options.caseSensitive)) ? true : isMatched(res, options.trueValue, options.caseSensitive)) ? true :
(options.isFalse.length && (options.falseValue.length &&
isMatched(res, options.isFalse, options.caseSensitive)) ? false : res); isMatched(res, options.falseValue, options.caseSensitive)) ? false : res);
} }
// for dev // for dev
exports._useExtSet = function(use) { useExt = use; }; exports._useExtSet = function(use) { useExt = use; };
exports.setDefault = function(options) {
defaultOptions = margeOptions(true, options);
return margeOptions(true); // copy
};
exports.prompt = function(options) { exports.prompt = function(options) {
var readOptions = margeOptions(true, options), res; var readOptions = margeOptions(true, options), res;
readOptions.display = readOptions.prompt; readOptions.display = readOptions.prompt;
@ -522,7 +621,7 @@ exports.prompt = function(options) {
exports.question = function(query, options) { exports.question = function(query, options) {
var readOptions = margeOptions(margeOptions(true, options), { var readOptions = margeOptions(margeOptions(true, options), {
display: query display: query
}), }),
res = readlineWithOptions(readOptions); res = readlineWithOptions(readOptions);
return toBool(res, readOptions); return toBool(res, readOptions);
@ -530,17 +629,30 @@ exports.question = function(query, options) {
exports.keyIn = function(query, options) { exports.keyIn = function(query, options) {
var readOptions = margeOptions(margeOptions(true, options), { var readOptions = margeOptions(margeOptions(true, options), {
display: query, display: query,
keyIn: true, keyIn: true,
noTrim: true keepWhitespace: true
}), res; }), 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 = []; var comps = [];
readOptions[optionName].forEach(function(comp) { readOptions[optionName].forEach(function(comp) {
if (typeof comp === 'string' || typeof comp === 'number') { if (typeof comp === 'string' || typeof comp === 'number') {
@ -552,22 +664,22 @@ exports.keyIn = function(query, options) {
readOptions[optionName] = comps; readOptions[optionName] = comps;
}); });
readOptions.display = replacePlaceholder(readOptions.display + '',
function(param) { return placeholderInMessage(param, readOptions); });
res = _readlineSync(readOptions);
return toBool(res, readOptions); return toBool(res, readOptions);
}; };
exports.setDefault = function(options) { // ======== DEPRECATED ========
defaultOptions = margeOptions(true, options); function _setOption(optionName, args) {
return margeOptions(true); // copy
};
// ======== These APIs are now obsolete. ========
function setOption(optionName, args) {
var options; var options;
if (args.length) { options = {}; options[optionName] = args[0]; } if (args.length) { options = {}; options[optionName] = args[0]; }
return exports.setDefault(options)[optionName]; return exports.setDefault(options)[optionName];
} }
exports.setPrint = function() { return setOption('print', arguments); }; exports.setPrint = function() { return _setOption('print', arguments); };
exports.setPrompt = function() { return setOption('prompt', arguments); }; exports.setPrompt = function() { return _setOption('prompt', arguments); };
exports.setEncoding = function() { return setOption('encoding', arguments); }; exports.setEncoding = function() { return _setOption('encoding', arguments); };
exports.setMask = function() { return setOption('mask', arguments); }; exports.setMask = function() { return _setOption('mask', arguments); };
exports.setBufferSize = function() { return setOption('bufferSize', arguments); }; exports.setBufferSize = function() { return _setOption('bufferSize', arguments); };

View file

@ -1,6 +1,6 @@
{ {
"name": "readline-sync", "name": "readline-sync",
"version": "0.12.0", "version": "0.13.0",
"title": "readlineSync", "title": "readlineSync",
"description": "Synchronous Readline for interactively running to have a conversation with the user via a console(TTY).", "description": "Synchronous Readline for interactively running to have a conversation with the user via a console(TTY).",
"keywords": [ "keywords": [