При реализации автодополнения в интерфейсах ввода значительную проблему создают специальные символы. Пользователь может вводить:
В библиотеке Awesomplete обработка специальных символов особенно важна при:
filter;replace;Awesomplete активно использует регулярные выражения для поиска
совпадений. Это создаёт потенциальные ошибки, если ввод пользователя
напрямую передаётся в RegExp.
new Awesomplete(input, {
filter(text, inputValue) {
return new RegExp(inputValue, "i").test(text);
}
});
Если пользователь введёт:
+
или:
[
произойдёт ошибка:
Uncaught SyntaxError: Invalid regular expression
Причина заключается в том, что символы:
. * + ? ^ $ { } ( ) | [ ] \
имеют специальное значение в регулярных выражениях.
Для безопасной работы необходимо экранировать пользовательский ввод.
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
new Awesomplete(input, {
filter(text, inputValue) {
const escaped = escapeRegExp(inputValue);
return new RegExp(escaped, "i").test(text);
}
});
Теперь пользователь может вводить:
C++
Node.js
[admin]
(тест)
без возникновения ошибок.
Символ \ представляет отдельную сложность, поскольку он
одновременно:
const value = "\";
Такой код приведёт к синтаксической ошибке.
const value = "\\";
new Awesomplete(input, {
filter(text, value) {
const escaped = escapeRegExp(value);
return new RegExp(escaped).test(text);
}
});
При отображении подсказок нельзя напрямую вставлять пользовательские
данные через innerHTML.
item(text) {
const li = document.createElement("li");
li.innerHTML = text;
return li;
}
Если список содержит:
<script>alert("XSS")</script>
код выполнится в браузере.
Следует использовать textContent.
item(text) {
const li = document.createElement("li");
li.textContent = text;
return li;
}
Теперь HTML будет отображаться как обычный текст.
Awesomplete использует HTML для выделения совпадений. Например:
<mark>jav</mark>ascript
Если пользовательский текст содержит HTML, возможны проблемы.
function escapeHTML(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
item(text, inputValue) {
const li = document.createElement("li");
const safeText = escapeHTML(text);
li.innerHTML = safeText;
return li;
}
Awesomplete корректно работает с Unicode, однако некоторые особенности требуют дополнительной обработки.
[
"Привет",
"こんにちは",
"你好",
"مرحبا",
"? Emoji"
]
Стандартная фильтрация может работать непредсказуемо при:
Разные Unicode-представления могут выглядеть одинаково.
é
может храниться как:
"é"
или:
"é"
Во втором случае символ состоит из двух кодовых точек.
function normalizeText(text) {
return text.normalize("NFC");
}
new Awesomplete(input, {
filter(text, value) {
return normalizeText(text)
.toLowerCase()
.includes(
normalizeText(value).toLowerCase()
);
}
});
Во многих интерфейсах:
cafe
должно находить:
café
function removeDiacritics(str) {
return str.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "");
}
new Awesomplete(input, {
filter(text, value) {
const normalizedText =
removeDiacritics(text.toLowerCase());
const normalizedValue =
removeDiacritics(value.toLowerCase());
return normalizedText.includes(normalizedValue);
}
});
Emoji состоят из сложных Unicode-комбинаций.
"?".length
Результат:
2
Причина — использование суррогатных пар UTF-16.
Для корректной обработки следует использовать:
Array.from()
const chars = Array.from("???");
console.log(chars.length);
Результат:
3
При создании собственных фильтров важно учитывать:
new Awesomplete(input, {
filter(text, value) {
const cleanValue =
value
.trim()
.replace(/\s+/g, " ")
.normalize("NFC")
.toLowerCase();
const cleanText =
text
.trim()
.replace(/\s+/g, " ")
.normalize("NFC")
.toLowerCase();
return cleanText.includes(cleanValue);
}
});
Пользователь может вставлять:
"hello world"
не совпадёт с:
"hello world"
где используется .
function normalizeSpaces(str) {
return str.replace(/\s+/g, " ").trim();
}
new Awesomplete(input, {
filter(text, value) {
return normalizeSpaces(text)
.includes(normalizeSpaces(value));
}
});
Некоторые данные могут содержать:
"\n"
"\r"
"\t"
function removeControlChars(str) {
return str.replace(/[\r\n\t]/g, " ");
}
const cleaned =
removeControlChars(data);
При автодополнении URL необходимо учитывать:
:
/
?
&
=
#
%
[
"https://example.com",
"https://github.com",
"https://google.com/search?q=test"
]
Без экранирования регулярных выражений фильтрация может ломаться.
new Awesomplete(input, {
filter(text, value) {
const escaped = escapeRegExp(value);
return new RegExp(escaped, "i")
.test(text);
}
});
При работе с Markdown возможны специальные конструкции:
# Заголовок
* список
`код`
[ссылка]
item(text) {
const li = document.createElement("li");
li.textContent = text;
return li;
}
Данные для Awesomplete часто загружаются через API.
fetch("/api/tags")
.then(r => r.json())
.then(data => {
awesomplete.list = data;
});
Если сервер возвращает специальные символы без корректной кодировки, возможны:
Сервер должен возвращать:
Content-Type: application/json; charset=utf-8
Иначе:
Привет
может превратиться в:
ÐÑивеÑ
Функция replace() позволяет изменять вставляемое
значение.
new Awesomplete(input, {
replace(text) {
this.input.value =
text.replace(/#/g, "");
}
});
#javascript
превратится в:
javascript
Иногда необходимо исключать специальные символы.
function sanitize(str) {
return str.replace(/[<>]/g, "");
}
replace(text) {
this.input.value = sanitize(text);
}
Некоторые данные содержат:
SELECT *
WHERE id = 1
или:
name'; DR OP TABLE users;
Хотя Awesomplete не взаимодействует напрямую с SQL, данные могут передаваться на сервер.
function safeQuery(str) {
return str.replace(/['";]/g, "");
}
Проблемы возможны с:
${value}
const text =
String(userInput);
Языки с письмом справа налево:
input.dir = "auto";
или:
<input dir="auto">
Пользователь может вводить:
JavaScript
жаваскрипт
جاڤاسكريبت
javascript
new Awesomplete(input, {
filter(text, value) {
const preparedText =
text.normalize("NFC")
.toLocaleLowerCase();
const preparedValue =
value.normalize("NFC")
.toLocaleLowerCase();
return preparedText.includes(preparedValue);
}
});
Сложная нормализация может снижать производительность.
Особенно затратны:
normalize();replace();const prepared = data.map(item => ({
original: item,
normalized:
removeDiacritics(
item.toLowerCase()
)
}));
new Awesomplete(input, {
filter(item, value) {
const normalizedValue =
removeDiacritics(
value.toLowerCase()
);
return item.normalized
.includes(normalizedValue);
}
});
function escapeRegExp(str) {
return str.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&"
);
}
function normalizeInput(str) {
return str
.normalize("NFC")
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
}
new Awesomplete(input, {
filter(text, value) {
const safeText =
normalizeInput(text);
const safeValue =
normalizeInput(value);
return safeText.includes(safeValue);
},
item(text, value) {
const li =
document.createElement("li");
li.textContent = text;
return li;
},
replace(text) {
this.input.value =
normalizeInput(text);
}
});