TL; DR шаблон, включающий пробелы и разрывы строк, также будет соответствовать символам, входящим в последовательность \r\n
, если вы позволите.
Прежде всего, давайте посмотрим, какие символы есть, а какие нет, когда вы делаете замену. Начиная со строки, в которой используются только переводы строки:
const inputLF =
`===
HELLO
WOLRD
===`.replace(/\r?\n/g, "\n");
console.log('------------ INPUT ')
console.log(inputLF);
console.log('------------')
debugPrint(inputLF, 2);
debugPrint(inputLF, 3);
debugPrint(inputLF, 4);
debugPrint(inputLF, 5);
const replaceLF = inputLF.replace(/^\s+$/gm, '');
console.log('------------ REPLACEMENT')
console.log(replaceLF);
console.log('------------')
debugPrint(replaceLF, 2);
debugPrint(replaceLF, 3);
debugPrint(replaceLF, 4);
debugPrint(replaceLF, 5);
console.log(`charcode ${replaceLF.charCodeAt(2)} : ${replaceLF.charAt(2)}`);
console.log(`charcode ${replaceLF.charCodeAt(3)} : ${replaceLF.charAt(3)}`);
console.log(`charcode ${replaceLF.charCodeAt(4)} : ${replaceLF.charAt(4)}`);
console.log(`charcode ${replaceLF.charCodeAt(5)} : ${replaceLF.charAt(5)}`);
console.log('------------')
console.log('inputLF === replaceLF :', inputLF === replaceLF)
function debugPrint(str, charIndex) {
console.log(`index: ${charIndex}
charcode: ${str.charCodeAt(charIndex)}
character: ${str.charAt(charIndex)}`
);
}
Каждая строка заканчивается символьным кодом 10, который является символом перевода строки (LF), представленным в строковом литерале с \n
. До и после замены две строки одинаковы - не только выглядят одинаково, но и фактически равны друг другу, поэтому замена ничего не дала.
Теперь рассмотрим другой случай:
const inputCRLF =
`===
HELLO
WOLRD
===`.replace(/\r?\n/g, "\r\n")
console.log('------------ INPUT ')
console.log(inputCRLF);
console.log('------------')
debugPrint(inputCRLF, 2);
debugPrint(inputCRLF, 3);
debugPrint(inputCRLF, 4);
debugPrint(inputCRLF, 5);
debugPrint(inputCRLF, 6);
debugPrint(inputCRLF, 7);
const replaceCRLF = inputCRLF.replace(/^\s+$/gm, '');;
console.log('------------ REPLACEMENT')
console.log(replaceCRLF);
console.log('------------')
debugPrint(replaceCRLF, 2);
debugPrint(replaceCRLF, 3);
debugPrint(replaceCRLF, 4);
debugPrint(replaceCRLF, 5);
function debugPrint(str, charIndex) {
console.log(`index: ${charIndex}
charcode: ${str.charCodeAt(charIndex)}
character: ${str.charAt(charIndex)}`
);
}
На этот раз каждая строка заканчивается символьным кодом 13, который представляет собой символ возврата каретки (CR), который представлен в строковом литерале с помощью \r
и , затем следует LF. После замены вместо последовательности =\r\n\r\nH
это не просто =\r\nH
. Посмотрим, почему.
Вот что говорит MDN о метасимвол ^
:
Соответствует началу ввода. Если для многострочного флага установлено значение true, также соответствует сразу после символа разрыва строки.
А вот что MDN говорит о метасимволе $
Соответствует концу ввода. Если для многострочного флага установлено значение true, также совпадает непосредственно перед символом разрыва строки.
Таким образом, они соответствуют после и перед символом разрыва строки. В этом случае MDN означает LF или CR. Это можно увидеть, если мы протестируем строку, содержащую разные разрывы строк:
const stringLF = "hello\nworld";
const stringCRLF = "hello\r\nworld";
const regexStart = /^\s/m;
const regexEnd = /\s$/m;
console.log(regexStart.exec(stringLF));
console.log(regexStart.exec(stringCRLF));
console.log(regexEnd.exec(stringLF));
console.log(regexEnd.exec(stringCRLF));
Если мы попытаемся сопоставить пробелы рядом с разрывом строки, ничего не получится, если есть LF, но он действительно сопоставит CR с CRLF. Итак, в этом случае $
будет соответствовать здесь:
"hello\r\nworld"
^^ what `^\s` matches
"hello\r\nworld"
^^ what `\s$` matches
Таким образом, и ^
, и $
распознают любую последовательность CRLF как конец строки. Это будет иметь значение, когда вы выполните поиск и замену. Поскольку ваше регулярное выражение определяет ^\s+$
, это означает, что если у вас есть строка, полностью состоящая из \r\n
, тогда она соответствует. Но по неочевидной причине:
const re = /^\s+$/m;
const sringLF = "hello\n\nworld";
const stringCRLF = "hello\r\n\r\nworld";
console.log(re.exec(sringLF));
console.log(re.exec(stringCRLF));
Таким образом, регулярное выражение соответствует не \r\n
, а скорее \n\r
(два символа пробела) между двумя другими символами разрыва строки. Это потому, что +
нетерпелив и потребляет столько последовательности символов, сколько может. Вот что попробует движок регулярных выражений. Несколько упрощено для краткости:
input = "hello\r\n\r\nworld
regex = /^\s+$/
Step 1
hello[\r]\n\r\nworld
matches `^`, symbol satisfied -> continue with next symbol in regex
Step 2
hello[\r\n]\r\nworld
matches `^\s+` -> continue matching to satisfy `+` quantifier
Step 3
hello[\r\n\r]\nworld
matches `^\s+` -> continue matching to satisfy `+` quantifier
Step 4
hello[\r\n\r\n]world
matches `^\s+` -> continue matching to satisfy `+` quantifier
Step 5
hello[\r\n\r\nw]orld
does not match `\s` -> backtrack
Step 6
hello[\r\n\r\n]world
matches `^\s+`, quantifier satisfied -> continue to next symbol in regex
Step 7
hello[\r\n\r\nw]orld
does not match `$` in `^\s+$` -> backtrack
Step 8
hello[\r\n\r\n]world
matches `^\s+$`, last symbol satisfied -> finish
Наконец, здесь есть что-то слегка скрытое - важно, чтобы вы сопоставляли пробелы. Это связано с тем, что он будет вести себя иначе, чем большинство других символов, поскольку он явно соответствует символу разрыва строки, тогда как _ 22_ не будет:
Соответствует любому одиночному символу кроме конца строки
Итак, если вы укажете \s$
, это будет соответствовать CR в \r\n
, потому что механизм регулярных выражений вынужден искать совпадения для \s
и $
, поэтому он находит \r
перед \n
. Однако этого не произойдет для многих других шаблонов, поскольку $
обычно удовлетворяется, когда он до CR (или в конце строки).
То же самое с ^\s
, он будет явно искать пробельный символ после переноса строки, который удовлетворяется LF в CRLF, однако, если вы этого не ищете, тогда он с радостью будет соответствовать после LF:
const stringLF = "hello\nworld";
const stringCRLF = "hello\r\nworld";
const regexStartAll = /^./mg;
const regexEndAll = /.$/gm;
console.log(stringLF.match(regexStartAll));
console.log(stringCRLF.match(regexStartAll));
console.log(stringLF.match(regexEndAll));
console.log(stringCRLF.match(regexEndAll));
Итак, все это означает, что ^\s+$
имеет некоторое неинтуитивное поведение, но при этом совершенно логично, если вы понимаете, что механизм регулярных выражений в точности соответствует тому, что вы ему указываете.
person
VLAZ
schedule
17.03.2020
$
будет соответствовать сразу перед концом строки, поэтому, если у вас естьtext\r\n
, тогда$
будет соответствовать до\n
, но после\r
. - person VLAZ   schedule 17.03.2020