Repository: denoland/deno
Pull request: fix(ext/node): readline line separators

Forked from Node.js implementation. Allows [parallel/test-readline-line-separators.js](https://github.com/nodejs/node/blob/v24.2.0/test/parallel/test-readline-line-separators.js) test to pass.

Changed files:
filename: ext/node/polyfills/internal/readline/interface.mjs
patch: @@ -89,11 +89,29 @@ import {
   kWordRight,
   kWriteToOutput,
 } from "ext:deno_node/internal/readline/symbols.mjs";
+import { primordials } from "ext:core/mod.js";
+
+const {
+  ArrayPrototypePush,
+  DateNow,
+  RegExpPrototypeExec,
+  SafeRegExp,
+  StringPrototypeEndsWith,
+  StringPrototypeSlice,
+} = primordials;
 
 const kHistorySize = 30;
 const kMincrlfDelay = 100;
-// \r\n, \n, or \r followed by something other than \n
-const lineEnding = /\r?\n|\r(?!\n)/;
+
+/**
+ * The end of a line is signaled by either one of the following:
+ *  - \r\n
+ *  - \n
+ *  - \r followed by something other than \n
+ *  - \u2028 (Unicode 'LINE SEPARATOR')
+ *  - \u2029 (Unicode 'PARAGRAPH SEPARATOR')
+ */
+const lineEnding = new SafeRegExp(/\r?\n|\r(?!\n)|\u2028|\u2029/g);
 
 const kLineObjectStream = Symbol("line object stream");
 export const kQuestionCancel = Symbol("kQuestionCancel");
@@ -583,24 +601,36 @@ export class Interface extends InterfaceConstructor {
     }
 
     // Run test() on the new string chunk, not on the entire line buffer.
-    const newPartContainsEnding = lineEnding.test(string);
-
-    if (this[kLine_buffer]) {
-      string = this[kLine_buffer] + string;
-      this[kLine_buffer] = null;
-    }
-    if (newPartContainsEnding) {
-      this[kSawReturnAt] = string.endsWith("\r") ? Date.now() : 0;
-
-      // Got one or more newlines; process into "line" events
-      const lines = string.split(lineEnding);
+    let newPartContainsEnding = RegExpPrototypeExec(lineEnding, string);
+    if (newPartContainsEnding !== null) {
+      if (this[kLine_buffer]) {
+        string = this[kLine_buffer] + string;
+        this[kLine_buffer] = null;
+        lineEnding.lastIndex = 0; // Start the search from the beginning of the string.
+        newPartContainsEnding = RegExpPrototypeExec(lineEnding, string);
+      }
+      this[kSawReturnAt] = StringPrototypeEndsWith(string, "\r")
+        ? DateNow()
+        : 0;
+
+      const indexes = [0, newPartContainsEnding.index, lineEnding.lastIndex];
+      let nextMatch;
+      while ((nextMatch = RegExpPrototypeExec(lineEnding, string)) !== null) {
+        ArrayPrototypePush(indexes, nextMatch.index, lineEnding.lastIndex);
+      }
+      const lastIndex = indexes.length - 1;
       // Either '' or (conceivably) the unfinished portion of the next line
-      string = lines.pop();
-      this[kLine_buffer] = string;
-      for (let n = 0; n < lines.length; n++) this[kOnLine](lines[n]);
+      this[kLine_buffer] = StringPrototypeSlice(string, indexes[lastIndex]);
+      for (let i = 1; i < lastIndex; i += 2) {
+        this[kOnLine](StringPrototypeSlice(string, indexes[i - 1], indexes[i]));
+      }
     } else if (string) {
       // No newlines this time, save what we have for next time
-      this[kLine_buffer] = string;
+      if (this[kLine_buffer]) {
+        this[kLine_buffer] += string;
+      } else {
+        this[kLine_buffer] = string;
+      }
     }
   }
 
@@ -1175,13 +1205,23 @@ export class Interface extends InterfaceConstructor {
         // falls through
         default:
           if (typeof s === "string" && s) {
-            const lines = s.split(/\r\n|\n|\r/);
-            for (let i = 0, len = lines.length; i < len; i++) {
-              if (i > 0) {
-                this[kLine]();
-              }
-              this[kInsertString](lines[i]);
+            // Erase state of previous searches.
+            lineEnding.lastIndex = 0;
+            let nextMatch;
+            // Keep track of the end of the last match.
+            let lastIndex = 0;
+            while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null) {
+              this[kInsertString](
+                StringPrototypeSlice(s, lastIndex, nextMatch.index),
+              );
+              ({ lastIndex } = lineEnding);
+              this[kLine]();
+              // Restore lastIndex as the call to kLine could have mutated it.
+              lineEnding.lastIndex = lastIndex;
             }
+            // This ensures that the last line is written if it doesn't end in a newline.
+            // Note that the last line may be the first line, in which case this still works.
+            this[kInsertString](StringPrototypeSlice(s, lastIndex));
           }
       }
     }

filename: tests/node_compat/config.toml
patch: @@ -908,6 +908,7 @@
 "parallel/test-readline-interface-escapecodetimeout.js" = {}
 "parallel/test-readline-interface-no-trailing-newline.js" = {}
 "parallel/test-readline-interface-recursive-writes.js" = {}
+"parallel/test-readline-line-separators.js" = {}
 "parallel/test-readline-position.js" = {}
 "parallel/test-readline-reopen.js" = {}
 "parallel/test-readline-set-raw-mode.js" = {}