Javascript-форум (https://javascript.ru/forum/)
-   Events/DOM/Window (https://javascript.ru/forum/events/)
-   -   Парсинг HTML -> DOM в нормальных браузерах (таки проблема) (https://javascript.ru/forum/events/44387-parsing-html-dom-v-normalnykh-brauzerakh-taki-problema.html)

FINoM 17.01.2014 18:22

Парсинг HTML -> DOM в нормальных браузерах (таки проблема)
 
Господа, пишу библиотеку по работе с DOM (для новых браузеров) и столкнулся с проблемой парсинга html. Конечная цель - сделать максимально компактно (< 1K). Это очередной мой опен соурц проект, о котором будет рассказано на Хабре и здесь.

К сожалению, я не могу найти простого решения парсинга HTML и представления его в виде DOM. Что первое приходит в голову? Создание элемента (дива) и вставка в него html.

div = document.createElement( 'div' );
div.innerHTML = '<p>a<p>b';
alert( div.innerHTML );
myDomNodes = div.children;


Проблема возникает, когда нам нужно распарсить элемент таблицы.
div = document.createElement( 'div' );
div.innerHTML = '<tr>a</tr><tr>b</tr>';
alert( div.innerHTML );

Понятно, почему так происходит: tr, td, tbody, thead, tfoot... не могут находиться внутри элементов, не являющихся их родителями по спецификации.

Другой вариант: использовать DOMParser
var doc = new DOMParser().parseFromString( '<tr>a</tr><tr>b</tr>', "text/html");
alert( doc.body.innerHTML );


Та же проблема.

Пробуем поменять контент-тайп на xml (потмо можно "усыновить" ноды методом document.adoptNode):
var doc = new DOMParser().parseFromString( '<tr>a</tr><tr>b</tr>', "application/xml");
console.log( doc );


Не ок. Метод будет парсить любой код выше (<tr>a</tr><tr>b</tr>, <p>a<p>b) с ошибками (не закрытый тег, который по стандарту html и не требуется закрывать, отсутствия рут элемента....)
Значит и этот метод нам не подходит.

А что по поводу DocumentFragment? Ничего. У него нет свойства innerHTML. Печаль.

Как это делается в библиотеках?
http://habrahabr.ru/post/164533/

Но предварительно jQuery пытается найти, не нужно ли обрамить наш код как-то дополнительно. Берется самый первый найденный в коде тег и ищется в служебном объекте wrapMap.

В Библиотеках это делается через, простите, жопу.

На дворе 2014 год и нет ни одного способа распарсить HTML в последнем хроме и файерфоксе? Не верю.

danik.js 17.01.2014 18:45

Цитата:

Сообщение от FINoM
На дворе 2014 год и нет ни одного способа распарсить HTML в последнем хроме и файерфоксе?

Если ты хочешь парсить фрагмент, то ты обязан указать контекст фрагмента. Соответственно создавать надо не div, а tbody

Цитата:

Сообщение от FINoM
А что по поводу DocumentFragment? Ничего. У него нет свойства innerHTML. Печаль.

Это по той же самой причине - парсеру нужно знать контекст для корректного разбора HTML. А у фрагмента нет контекста.

Цитата:

Сообщение от FINoM
В Библиотеках это делается через, простите, жопу.

Зато от юзера не требуется указание контекста. То есть это элемент "умного поведения" - такие вещи обычно и делаются "через жопу"

А вот то что DOMParser не умеет парсить фрагменты - это недочет, как мне кажется.

kostyanet 18.01.2014 02:21

Все-таки хотел узнать за каким парсить хтмл вообще? Ну я понимаю читеркод, когда исходник не твой и надо ковырять чужой моск. Но если он твой, то какой может быть задача пихать еду через задницу?

FINoM 18.01.2014 02:35

Цитата:

Сообщение от danik.js
парсеру нужно знать контекст для корректного разбора HTML

В этом и косяк. Когда я юзаю createElement, методу совершенно пофиг на контекст создаваемого элемента. Но, блин, обычной, очевидно нужной, функции обычного парсинга в ДОМе нет.

danik.js 18.01.2014 07:26

Цитата:

Сообщение от FINoM
Но, блин, обычной, очевидно нужной, функции обычного парсинга в ДОМе нет.

Специфика HTML синтаксиса такова, что парсеру нужно знать контекст. От контекста зависит поведение парсера. Понимаешь?

Это XML можно разобрать в отрыве от контекста. С HTML это не прокатит.
Например если HTML-парсер встретит тег <td> разбирая содержимое div'а, то он проигнорирует этот тег и вставит только его содержимое. А если это произойдет в контексте <table>, <tbody> или <tr> то он его не проигнорирует, да к тому же создаст недостающие части таблицы (tbody, tr). То есть HTML парсер имеет кучу состояний, зависящих от контекста.
Теперь понимаешь почему у DocumentFragment нет innerHTML?
Ах да, в разных контекстах не только разное поведение при встрече тегов, но даже в некоторых контекстах теги могут вобще распознаваться как текст. Это касается контекстов <title>, <noscript>, <plaintext>, <textarea> и тд. Внутри <script> вообще особое поведение - в нем распознается только закрывающий тег </script>

Матчасть: http://www.whatwg.org/specs/web-apps...sing-algorithm

danik.js 18.01.2014 07:28

Цитата:

Сообщение от FINoM
Когда я юзаю createElement, методу совершенно пофиг на контекст создаваемого элемента

Для создания элемента парсер не требуется. Контекст нужен только для парсера. А парсер нужен только для разбора HTML-кода.

FINoM 18.01.2014 13:38

Цитата:

Сообщение от danik.js
Теперь понимаешь почему у DocumentFragment нет innerHTML?

Нет, не понимаю. Логично предположить, что для DocumentFragment можно реализовать парсинг HTML с "любым" контекстом. Понятное дело, что если мы парсим <div><td></div>, то этот гипотетический парсер вернул бы только <div>, так как в диве не может быть <td> но если <td> на верхнем уровне (<td><div></div>), зачем ему вообще контекст? Почему персер не может, условно, сделать document.createElement( 'элемент верхнего уровня' );, а затем уже, для элементов внутри, исправлять ошибки.

Ваша аргументация мне понятна. Описание поведения в таких случаях описано в спецификации. И моё недовольство касается не реализации чего-либо в браузерах, а документального описания реализации в спецификации.
Цитата:

Сообщение от danik.js
Для создания элемента парсер не требуется.

Почему? Они бы могли .createElement сделать тоже зависимым от контекста: document.createElement( 'tbody', document.querySelector( '#tbody' ) ). Это тоже было бы верно. Ответ на этот вопрос у меня один: они посчитали это неудобным и решили реализовать создание элементов в отрыве от контекста. Спасибо им. Почему они не посчитали удобным реализовать парсер, мне не понятно.

Ну да ладно, ответа на мой вопрос нет и, судя по всему не будет. А спорить мы можем долго.

danik.js 18.01.2014 14:19

Цитата:

Сообщение от FINoM
Почему?

Что за вопрос. Ты считаешь что при вызове метода createElelement() происходит вызов парсера? А че парсить то?

Цитата:

Сообщение от FINoM
Они бы могли .createElement сделать тоже зависимым от контекста

А какой в этом смысл?

Цитата:

Сообщение от danik.js
Почему персер не может, условно, сделать document.createElement( 'элемент верхнего уровня' );, а затем уже, для элементов внутри, исправлять ошибки

Потому что это мало требуемая сомнительная фишка (что это за ситуация, когда ты не знаешь в какой контекст будут вставлены распарсенные DOM-элементы? ), которая бы усложнила и без того запутанную логику парсера.
И наверное если подумать, то всплывут ситуации, где нельзя "догадаться" о контексте.

FINoM 18.01.2014 14:43

danik.js, вы, как и в предыдущих сообщениях защищаете спецификацию. Моя точка зрения: спецификаторы должны были до 2014 (!) года предусмотреть безконтекстный парсер.

Повторюсь.
Цитата:

Сообщение от FINoM
Почему персер не может, условно, сделать document.createElement( 'элемент верхнего уровня' );, а затем уже, для элементов внутри, исправлять ошибки.

Косяк спецификации, на мой взгляд.
Цитата:

Сообщение от danik.js
Потому что это мало требуемая сомнительная фишка (что это за ситуация, когда ты не знаешь в какой контекст будут вставлены распарсенные DOM-элементы? ),

Цитата:

Сообщение от danik.js
И наверное если подумать, то всплывут ситуации, где нельзя "догадаться" о контексте.

Угу. Например, задача из первого поста.

FINoM 19.01.2014 17:38

Написал простейшее решение. Проверяется только первый тег в строке, для остальных контекст остается тем же, что и для первого.
window.parseHTML = function( html ) {
	var node = document.createElement( 'div' ),
		// wrapMap is taken from jQuery
		wrapMap = {
				option: [ 1, "<select multiple='multiple'>", "</select>" ],
				legend: [ 1, "<fieldset>", "</fieldset>" ],
				thead: [ 1, "<table>", "</table>" ],
				tr: [ 2, "<table><tbody>", "</tbody></table>" ],
				td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
				col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
				area: [ 1, "<map>", "</map>" ],
				_default: [ 0, "", "" ]
		},
		wrapper,
		i;
	
	wrapMap.optgroup = wrapMap.option;
	wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
	wrapMap.th = wrapMap.td;
	
	wrapper = wrapMap[ /<([\w:]+)/.exec( html )[ 1 ] ] || wrapMap._default;
	
	node.innerHTML = wrapper[ 1 ] + html + wrapper[ 2 ];
	
	i = wrapper[ 0 ];

	while( i-- ) {
		node = node.firstElementChild;
	}
	
	return node.children;
};


Часовой пояс GMT +3, время: 11:09.