TL; DR шаблон с пробелами и разрывы строк также будут соответствовать символам в последовательности \r\n
, если вы позволите.
First of все, давайте посмотрим, какие символы есть, а какие нет, когда вы делаете замену. Начиная со строки, которая использует только перевод строки:
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
и затем НЧ следует. После замены вместо последовательности =\r\n\r\nH
это не просто =\r\nH
. Давайте посмотрим, почему.
Вот что MDN говорит о метасимволе ^
:
Соответствует началу ввода. Если для многострочного флага установлено значение «истина», то также совпадает сразу после символа разрыва строки.
И вот что 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"
^^ whay `\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
Наконец, здесь есть что-то немного скрытое - важно, чтобы вы соответствовали пробелу. Это связано с тем, что он будет вести себя иначе, чем большинство других символов, поскольку он явно соответствует символу разрыва строки, тогда как .
не будет :
Соответствует любому отдельному символу кроме ограничителей строки
Итак, если вы укажете \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+$
имеет некоторое неинтуитивное поведение, но в то же время идеально согласованное, когда вы понимаете, что механизм регулярных выражений соответствует точно тому, что вы говорите это к.