Как я уже говорил, главным символом является "<", т.к. с него начинаются теги. Написать универсальное выражение действительно проблематично, но можно делать ступенчатые проверки.
Поиск открывающего тега:
%<\s*([a-z]+)([^<>]*?)\s*>%i
Выражение захватывает тег целиком, подвыражения захватывают имя тега и блок, потенциально содержащий атрибуты.
Нужно проверить по списку, является ли имя допустимым, а атрибуты похожи на правду.
Проверка атрибутов (если строка атрибутов не пустая):
%(?:\s+([a-z]+)(|\s*=\s*'[^']*'|\s*=\*"[^"]*"))+\s*%i
Поиск закрывающего тега:
%<\s*/\s*([a-z]+)\s*>%i
Думаю, тут нужно не заменять на лету, а составить список смещений на начало и конец обнаруженных тегов, а потом по шагам обработать блоки.