Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions doc/api/readline.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,10 @@ added: v0.1.98
-->

The `'line'` event is emitted whenever the `input` stream receives an
end-of-line input (`\n`, `\r`, or `\r\n`). This usually occurs when the user
presses <kbd>Enter</kbd> or <kbd>Return</kbd>.
end-of-line input (`\n`, `\r`, or `\r\n`). By default, Unicode line separator
(`\u2028`) and paragraph separator (`\u2029`) characters are also treated as
end-of-line input. This usually occurs when the user presses <kbd>Enter</kbd>
or <kbd>Return</kbd>.

The `'line'` event is also emitted if new data has been read from a stream and
that stream ends without a final end-of-line marker.
Expand Down Expand Up @@ -525,6 +527,10 @@ changes:

<!-- YAML
added: v17.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63789
description: The `unicodeLineSeparators` option is supported now.
-->

* Extends: {readline.InterfaceConstructor}
Expand Down Expand Up @@ -716,6 +722,8 @@ added: v17.0.0
`100`. It can be set to `Infinity`, in which case `\r` followed by `\n`
will always be considered a single newline (which may be reasonable for
[reading files][] with `\r\n` line delimiter). **Default:** `100`.
* `unicodeLineSeparators` {boolean} If `true`, `\u2028` and `\u2029` will be
treated as end-of-line input. **Default:** `true`.
Comment on lines +725 to +726
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if a boolean is the correct way, or if we should instead let the user define their own list of separator as suggested in #60606 (comment)

* `escapeCodeTimeout` {number} The duration `readlinePromises` will wait for a
character (when reading an ambiguous key sequence in milliseconds one that
can both form a complete key sequence using the input read so far and can
Expand Down Expand Up @@ -924,6 +932,9 @@ the current position of the cursor down.
<!-- YAML
added: v0.1.98
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63789
description: The `unicodeLineSeparators` option is supported now.
- version:
- v15.14.0
- v14.18.0
Expand Down Expand Up @@ -981,6 +992,8 @@ changes:
`100`. It can be set to `Infinity`, in which case `\r` followed by `\n`
will always be considered a single newline (which may be reasonable for
[reading files][] with `\r\n` line delimiter). **Default:** `100`.
* `unicodeLineSeparators` {boolean} If `true`, `\u2028` and `\u2029` will be
treated as end-of-line input. **Default:** `true`.
* `escapeCodeTimeout` {number} The duration `readline` will wait for a
character (when reading an ambiguous key sequence in milliseconds one that
can both form a complete key sequence using the input read so far and can
Expand Down
14 changes: 14 additions & 0 deletions lib/internal/readline/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const {

const {
validateAbortSignal,
validateBoolean,
validateString,
validateUint32,
} = require('internal/validators');
Expand Down Expand Up @@ -86,6 +87,7 @@ const kMincrlfDelay = 100;
* - \u2029 (Unicode 'PARAGRAPH SEPARATOR')
*/
const lineEnding = /\r?\n|\r(?!\n)|\u2028|\u2029/g;
const crlfLineEnding = /\r?\n|\r(?!\n)/g;

const kLineObjectStream = Symbol('line object stream');
const kQuestionCancel = Symbol('kQuestionCancel');
Expand Down Expand Up @@ -116,6 +118,7 @@ const kMoveUpOrHistoryPrev = Symbol('_moveUpOrHistoryPrev');
const kInsertString = Symbol('_insertString');
const kLine = Symbol('_line');
const kLine_buffer = Symbol('_line_buffer');
const kLineEnding = Symbol('_lineEnding');
const kKillRing = Symbol('_killRing');
const kKillRingCursor = Symbol('_killRingCursor');
const kMoveCursor = Symbol('_moveCursor');
Expand Down Expand Up @@ -172,6 +175,7 @@ function InterfaceConstructor(input, output, completer, terminal) {
let crlfDelay;
let prompt = '> ';
let signal;
let unicodeLineSeparators = true;

if (input?.input) {
// An options object was given
Expand Down Expand Up @@ -208,6 +212,13 @@ function InterfaceConstructor(input, output, completer, terminal) {
}

crlfDelay = input.crlfDelay;
if (input.unicodeLineSeparators !== undefined) {
validateBoolean(
input.unicodeLineSeparators,
'options.unicodeLineSeparators',
);
unicodeLineSeparators = input.unicodeLineSeparators;
}
input = input.input;

input.size = historySize;
Expand Down Expand Up @@ -250,6 +261,7 @@ function InterfaceConstructor(input, output, completer, terminal) {
MathMax(kMincrlfDelay, crlfDelay) :
kMincrlfDelay;
this.completer = completer;
this[kLineEnding] = unicodeLineSeparators ? lineEnding : crlfLineEnding;

this.setPrompt(prompt);

Expand Down Expand Up @@ -623,6 +635,7 @@ class Interface extends InterfaceConstructor {
}

// Run test() on the new string chunk, not on the entire line buffer.
const lineEnding = this[kLineEnding];
let newPartContainsEnding = RegExpPrototypeExec(lineEnding, string);
if (newPartContainsEnding !== null) {
if (this[kLine_buffer]) {
Expand Down Expand Up @@ -1530,6 +1543,7 @@ class Interface extends InterfaceConstructor {
default:
if (typeof s === 'string' && s) {
// Erase state of previous searches.
const lineEnding = this[kLineEnding];
lineEnding.lastIndex = 0;
let nextMatch;
// Keep track of the end of the last match.
Expand Down
1 change: 1 addition & 0 deletions lib/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ Interface.prototype.question[promisify.custom] = function question(query, option
* removeHistoryDuplicates?: boolean;
* prompt?: string;
* crlfDelay?: number;
* unicodeLineSeparators?: boolean;
* escapeCodeTimeout?: number;
* tabSize?: number;
* signal?: AbortSignal;
Expand Down
48 changes: 40 additions & 8 deletions test/parallel/test-readline-line-separators.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,45 @@ const { Readable } = require('node:stream');

const str = '012\n345\r67\r\n89\u{2028}ABC\u{2029}DEF';

const rli = new readline.Interface({
input: Readable.from(str),
});
// Unicode line and paragraph separators are line endings by default.
{
const rli = new readline.Interface({
input: Readable.from(str),
});

const linesRead = [];
rli.on('line', (line) => linesRead.push(line));
const linesRead = [];
rli.on('line', (line) => linesRead.push(line));

rli.on('close', common.mustCall(() => {
assert.deepStrictEqual(linesRead, ['012', '345', '67', '89', 'ABC', 'DEF']);
}));
rli.on('close', common.mustCall(() => {
assert.deepStrictEqual(linesRead, ['012', '345', '67', '89', 'ABC', 'DEF']);
}));
}

// The option allows file formats such as JSONL to keep Unicode separators
// inside record contents while still splitting on CR, LF, and CRLF.
{
const rli = new readline.Interface({
input: Readable.from(str),
unicodeLineSeparators: false,
});

const linesRead = [];
rli.on('line', (line) => linesRead.push(line));

rli.on('close', common.mustCall(() => {
assert.deepStrictEqual(
linesRead,
['012', '345', '67', '89\u2028ABC\u2029DEF'],
);
}));
}

assert.throws(
() => new readline.Interface({
input: Readable.from(str),
unicodeLineSeparators: 'false',
}),
{
code: 'ERR_INVALID_ARG_TYPE',
},
);