Développez votre navigateur à partir de zéro. Première partie: HTML


Bonjour à tous!


Nous continuons la série d'articles sur le développement du moteur de navigation.


Dans cet article, je vais vous expliquer comment créer l'analyseur HTML le plus rapide avec DOM. Nous allons examiner la spécification HTML et pourquoi elle est mauvaise en termes de performances et de consommation de ressources lors de l'analyse HTML.


Avec ce sujet, j'ai rendu compte du passé HighLoad ++. Tout le monde ne peut pas assister à la conférence, et l'article contient plus de détails.


Je suppose que le lecteur a des connaissances de base en HTML: balises, nœuds, éléments, espace de noms.


Spécification HTML


Avant de commencer à aborder la mise en œuvre de l'analyseur HTML, vous devez comprendre quelles spécifications HTML croire.


Il existe deux spécifications HTML:


  1. WHATWG
    • Apple, Mozilla, Google, Microsoft
  2. W3c
    • Grande liste d'entreprises

Naturellement, le choix s'est porté sur les leaders de l'industrie - WHATWG . Niveau de vie, les grandes entreprises ont chacune leur propre navigateur / moteur de navigateur.


MISE À JOUR: Malheureusement, les liens donnés vers les spécifications ne s'ouvrent pas depuis la Russie. Apparemment, "l'écho de la guerre" avec les télégrammes.


Processus d'analyse HTML


Le processus de construction d'une arborescence HTML peut être divisé en quatre parties:


  1. Décodeur
  2. Prétraitement
  3. Tokenizer
  4. Construire un arbre

Nous considérons chaque étape séparément.


Décodeur


Le tokenizer accepte les caractères Unicode (points de code) en entrée. Par conséquent, nous devons convertir le flux d'octets actuel en caractères Unicode. Pour ce faire, utilisez la spécification d' encodage .


Si nous avons du HTML avec un encodage inconnu (pas d'en-tête HTTP), nous devons le déterminer avant le début du décodage. Pour ce faire, nous utiliserons l' algorithme de reniflage d'encodage .


Si très brièvement, l'essence de l'algorithme est que nous attendons 500 ou les 1024 premiers 1024 du flux d'octets et 500 le 500 - 500 un algorithme de flux d'octets pour déterminer son encodage qui essaie de trouver la <meta> avec les attributs http-equiv , content ou charset et essaie comprendre le codage indiqué par le développeur HTML.


La spécification d' Encoding stipule l'ensemble minimal d'encodages pris en charge par le moteur de navigateur (21 au total): UTF-8, ISO-8859-2, ISO-8859-7, ISO-8859-8, windows-874, windows-1250, windows-1251, windows -1252, windows-1254, windows-1255, windows-1256, windows-1257, windows-1258, gb18030, Big5, ISO-2022-JP, Shift_JIS, EUC-KR, UTF-16BE, UTF-16LE et x-user -défini.


Prétraitement


Après avoir décodé les octets en caractères Unicode, nous devons "nettoyer". A savoir, remplacer tous les caractères de retour chariot ( \r ) suivis d'un caractère de saut de ligne ( \n ) par un caractère retour chariot ( \r ). Ensuite, remplacez tous les caractères de retour chariot par un caractère de nouvelle ligne ( \n ).


Ainsi décrit dans la spécification. Autrement dit, \r\n => \r , \r => \n .


Mais, en fait, personne ne le fait. Rendez-le plus facile:


Si vous obtenez un caractère de retour chariot ( \r ), recherchez s'il existe un caractère de saut de ligne ( \n ). Si tel est le cas, nous changeons les deux caractères en un caractère de nouvelle ligne ( \n ), sinon, nous ne changeons que le premier caractère ( \r ) en caractères de nouvelle ligne ( \n ).


Ceci termine le traitement préliminaire des données. Oui, il vous suffit de vous débarrasser des symboles de retour chariot pour qu'ils ne tombent pas dans le tokenizer. Le tokenizer ne s'attend pas et ne sait pas quoi faire avec le symbole de retour chariot.


Erreurs d'analyse


Pour qu'à l'avenir il n'y ait plus de questions, vous devez immédiatement dire ce qu'est une ( parse error ).


Rien de mal. Cela semble menaçant, mais en fait, ce n'est qu'un avertissement que nous nous attendions à un, mais nous en avons un autre.


Une erreur d'analyse n'interrompt pas le traitement des données ou la construction de l'arborescence. Il s'agit d'un message qui signale que nous n'avons pas de code HTML valide.


Une erreur de parsig peut être obtenue pour les paires de substitution, \0 , un emplacement de balise incorrect, un <!DOCTYPE> incorrect et toutes sortes d'autres choses.


Soit dit en passant, certaines erreurs d'analyse entraînent des conséquences. Par exemple, si vous spécifiez "mauvais" <!DOCTYPE> l'arborescence HTML sera marquée comme QUIRKS et la logique de certaines fonctions DOM changera.


Tokenizer


Comme mentionné précédemment, le tokenizer accepte les caractères Unicode en entrée. Il s'agit d'une machine d'état qui a 80 états. Dans chaque état, conditions pour les caractères Unicode. Selon le personnage reçu, le tokenizer peut:


  1. Changez votre état
  2. Générer un jeton et changer d'état
  3. Ne faites rien, attendez le prochain personnage

Le tokenizer crée six types de jetons: DOCTYPE, balise de début, balise de fin, commentaire, caractère, fin de fichier. Qui entrent dans la phase de construction d'un arbre.


Il est à noter que le tokenizer ne connaît pas tous ses états, mais où environ 40% (pris au plafond, par exemple). "Pourquoi le reste?" - demandez-vous. Environ les 60% restants connaissent le stade de construction d'un arbre.


Ceci est effectué afin d'analyser correctement les balises telles que <textarea> , <style> , <script> , <title> et ainsi de suite. Autrement dit, généralement les balises dans lesquelles nous n'attendons pas d'autres balises, mais seulement en nous fermant.


Par exemple, la <title> ne peut pas contenir d'autres balises. Toutes les balises de <title> seront perçues comme du texte jusqu'à ce qu'elles rencontrent une balise de fermeture </title> .


Pourquoi est-ce fait? Après tout, vous pourriez simplement dire au tokenizer que si nous rencontrons la <title> nous suivons le "chemin dont nous avons besoin". Et ce serait vrai sinon des espaces de noms! Oui, l'espace de noms affecte le comportement de l'étape de création d'arborescence, qui à son tour modifie le comportement du tokenizer.


Par exemple, considérons le comportement de la <title> dans les espaces de noms HTML et SVG:


HTML


 <title><span></span></title> 

Le résultat de la construction d'un arbre:


 <title> "<span></span>" 

Svg


 <svg><title><span></span></title></svg> 

Le résultat de la construction d'un arbre:


 <svg> <title> <span> "" 

Nous voyons que dans le premier cas (espace de noms HTML) la <span> est du texte, l'élément span n'a pas été créé. Dans le deuxième cas (espace de noms SVG), un élément a été créé sur la base de la <span> . Autrement dit, selon l'espace de noms, les balises se comportent différemment.


Mais ce n'est pas tout. Le gâteau de cette "célébration de la vie" est le fait que le tokenizer lui-même doit savoir dans quel espace de noms se situe le stade de construction de l'arbre. Et cela est nécessaire uniquement pour gérer correctement CDATA .


Prenons deux exemples avec CDATA , deux espaces de noms:


HTML


 <div><![CDATA[  ]]></div> 

Le résultat de la construction d'un arbre:


 <div> <!--[CDATA[  ]]--> 

Svg


 <div><svg><![CDATA[  ]]></svg></div> 

Le résultat de la construction d'un arbre:


 <div> <svg> "  " 

Dans le premier cas (espace de noms HTML), le tokenizer a pris CDATA pour commentaire. Dans le deuxième cas, le tokenizer a démonté la structure CDATA et a reçu des données de celle-ci. En général, la règle est la suivante: si nous rencontrons CDATA pas dans l'espace de noms HTML, alors nous l'analysons, sinon nous le considérons comme un commentaire.


C'est le lien étroit entre le tokenizer et la construction de l'arbre. Le tokenizer doit savoir dans quel espace de noms se trouve actuellement l'étape de construction de l'arborescence, et l'étape de la construction de l'arborescence peut changer l'état du tokenizer.


Jetons


Ci-dessous, nous considérerons les six types de jetons créés par le tokenizer. Il convient de noter que tous les jetons ont préparé des données, c'est-à-dire déjà traitées et «prêtes à l'emploi». Cela signifie que toutes les références de caractères nommées, comme © , seront converties en caractères unicode.


Jeton DOCTYPE


Le jeton DOCTYPE a sa propre structure qui n'est pas similaire aux autres balises. Le jeton contient:


  1. Prénom
  2. Identifiant public
  3. Identifiant système

En HTML moderne, le seul DOCTYPE valide / valide devrait ressembler à ceci:


 <!DOCTYPE html> 

Tous les autres <!DOCTYPE> seront considérés comme une erreur d'analyse.


Jeton de balise de début


La balise d'ouverture peut contenir:


  1. Nom du tag
  2. Attributs
  3. Drapeaux

Par exemple,


 <div key="value" /> 

La balise d'ouverture peut contenir un drapeau à self-closing . Cet indicateur n'affecte pas la fermeture de la balise, mais peut provoquer une erreur d'analyse pour les éléments non vides .


Jeton de fin de balise


Balise de fermeture. Il a toutes les propriétés du jeton de la balise d'ouverture, mais a une barre oblique / devant le nom de la balise.


 </div key="value" /> 

La balise de fermeture peut contenir un drapeau à self-closing qui provoquera une erreur d'analyse. De plus, l'erreur d'analyse sera causée par les attributs de la balise de fermeture. Ils seront correctement analysés, mais jetés au stade de la construction des arbres.


Jeton de commentaire


Le jeton de commentaire contient l'intégralité du texte du commentaire. Autrement dit, il est entièrement copié du flux vers le jeton.


Exemple


 <!--  --> 

Jeton de personnage


Peut-être le jeton le plus intéressant. Symbole de jeton Unicode. Peut contenir un (un seul) caractère.


Un jeton sera créé pour chaque caractère en HTML et envoyé au stade de la construction de l'arborescence. C'est très cher.
Voyons comment cela fonctionne.


Prenez les données HTML:


 <span> ! &reg;</span> 

Selon vous, combien de jetons seront créés pour cet exemple? Réponse: 22.


Considérez la liste des jetons créés:


 Start tag token: <span> Character token:  Character token:  Character token:  Character token:  Character token:  Character token: Character token:  Character token:  Character token:  Character token:  Character token:  Character token:  Character token:  Character token:  Character token:  Character token:  Character token: ! Character token: Character token: End tag token: </span> End-of-file token 

Pas réconfortant, non? Mais, bien sûr, de nombreux créateurs d'analyseurs HTML n'ont en fait qu'un seul jeton lors du traitement. L'exécuter en cercle et l'écraser à chaque fois avec de nouvelles données.


Avançons et répondons à la question: pourquoi est-ce fait? Pourquoi ne pas prendre ce texte en un seul morceau? La réponse réside dans la phase de construction de l'arbre.


Un tokenizer est inutile sans l'étape de construction d'une arborescence HTML. C'est au stade de la construction d'un arbre que le texte est collé avec différentes conditions.


Les conditions sont approximativement les suivantes:


  1. Si un jeton de caractère avec U+0000 ( NULL ) arrive, nous provoquons une erreur d'analyse et ignorons le jeton.
  2. Si l'un des CHARACTER TABULATION de caractères U+0009 ( CHARACTER TABULATION ), U+000A ( LINE FEED (LF) ), U+000C ( FORM FEED (FF) U+000C FORM FEED (FF) ) ou U+0020 ( SPACE ) est venu, appelez l'algorithme pour restaurer les éléments de formatage actifs et insérez le jeton dans l'arbre.

Le jeton de symbole est ajouté à l'arbre selon l'algorithme:


  1. Si la position d'insertion actuelle n'est pas un nœud de texte, créez un nœud de texte, insérez-le dans l'arborescence et ajoutez-y les données du jeton.
  2. Sinon, ajoutez des données du jeton à un nœud de texte existant.

Ce comportement crée beaucoup de problèmes. La nécessité pour chaque symbole de créer un jeton et d'envoyer pour analyse au stade de la construction d'un arbre. Nous ne connaissons pas la taille du nœud de texte et nous devons soit allouer beaucoup de mémoire à l'avance ou faire des realoks. Tout cela est extrêmement coûteux en mémoire ou en temps.


Jeton de fin de fichier


Jeton simple et clair. Les données sont dépassées - laissez-nous vous informer sur cette étape de la construction des arbres.


Construire un arbre


La construction d'arbres est une machine d'état avec 23 états avec de nombreuses conditions pour les jetons (balises, texte). Le stade de construction d'un arbre est le plus grand, occupe une partie importante de la spécification et est également capable de provoquer un sommeil léthargique et une irritation.


Tout est arrangé très simplement. Les jetons sont reçus à l'entrée et, selon le jeton, l'état de la structure arborescente est commuté. En sortie, nous avons un vrai DOM.


Des problèmes?


Les problèmes suivants semblent assez évidents:


Copie caractère par caractère


Chaque état du tokenizer reçoit un caractère en entrée, qu'il copie / convertit si nécessaire: noms de balises, attributs, commentaires, symboles.


C'est un gaspillage de mémoire et de temps. Nous sommes obligés de préallouer une quantité de mémoire inconnue pour chaque attribut, nom de balise, commentaire, etc. Et cela, en conséquence, conduit à des realoks, et des realoks entraînent une perte de temps.


Et si vous imaginez que HTML contient 1000 balises et que chaque balise a au moins un attribut, alors nous obtenons un analyseur incroyablement lent.


Jeton de personnage


Le deuxième problème est le jeton de personnage. Il s'avère que nous créons un jeton pour chaque symbole et le donnons pour construire un arbre. La construction d'un arbre ne sait pas combien de ces jetons nous aurons et ne peut pas allouer immédiatement de la mémoire pour le nombre de caractères requis. En conséquence, ici tout de même realoks + constant vérifie la présence d'un nœud de texte dans l'état actuel de l'arbre.


Système monolithique


Le gros problème est la dépendance de tout à tout. Autrement dit, le tokenizer dépend de l'état de construction de l'arborescence, et la construction de l'arbre peut contrôler le tokenizer. Et tout est à blâmer pour l'espace de noms (espaces de noms).


Comment allons-nous résoudre les problèmes?


Ensuite, je décrirai l'implémentation de l'analyseur HTML dans mon projet Lexbor , ainsi que la solution à tous les problèmes exprimés.


Prétraitement


Nous supprimons le traitement préliminaire des données. Nous allons entraîner le tokenizer à comprendre le retour chariot ( \r ) comme un espace. Ainsi, il sera jeté au stade de la construction d'un arbre où nous le découvrirons.


Jetons


D'un coup de poignet, nous unifions tous les jetons. Nous aurons un jeton pour tout. En général, il n'y aura qu'un seul jeton dans l'ensemble du processus d'analyse.


Notre jeton unifié contiendra les champs suivants:


  1. Identifiant du tag
  2. Commencer
  3. Fin
  4. Attributs
  5. Drapeaux

Identifiant du tag


Nous ne travaillerons pas avec la représentation textuelle du nom de la balise. Nous traduisons tout en chiffres. Les chiffres sont faciles à comparer, plus faciles à utiliser.


Nous créons une table de hachage statique à partir de toutes les balises connues. Nous créons une énumération à partir de toutes les balises connues. Autrement dit, nous devons attribuer de manière rigide un identifiant à chaque balise. Par conséquent, dans la table de hachage, la clé est le nom de la balise et la valeur est écrite à partir de l'énumération.


Pour un exemple:


 typedef enum { LXB_TAG__UNDEF = 0x0000, LXB_TAG__END_OF_FILE = 0x0001, LXB_TAG__TEXT = 0x0002, LXB_TAG__DOCUMENT = 0x0003, LXB_TAG__EM_COMMENT = 0x0004, LXB_TAG__EM_DOCTYPE = 0x0005, LXB_TAG_A = 0x0006, LXB_TAG_ABBR = 0x0007, LXB_TAG_ACRONYM = 0x0008, LXB_TAG_ADDRESS = 0x0009, LXB_TAG_ALTGLYPH = 0x000a, /* ... */ } 

Comme vous pouvez le voir dans l'exemple, nous avons créé des balises pour le jeton END-OF-FILE , pour le texte, pour un document. Tout cela pour plus de commodité. En ouvrant le rideau, je dirai que dans le nœud ( DOM Node Interface ), nous aurons un Tag ID . Ceci est fait afin de ne pas faire deux comparaisons: sur le type de nœud et sur l'élément. Autrement dit, si nous avons besoin d'un élément DIV , nous effectuons une vérification dans le nœud:


 if (node->tag_id == LXB_TAG_DIV) { /* Best code */ } 

Mais, bien sûr, vous pouvez le faire:


 if (node->type == LXB_DOM_NODE_TYPE_ELEMENT && node->tag_id == LXB_TAG_DIV) { /* Oh, code */ } 

Deux LXB_TAG__ soulignement dans LXB_TAG__ sont nécessaires pour séparer les balises communes de celles du système. En d'autres termes, l'utilisateur peut créer une balise avec le nom du text ou la end-of-file et si nous recherchons ensuite par nom de balise, aucune erreur ne se produira. Toutes les balises système commencent par un # .


Mais encore, un nœud peut stocker une représentation textuelle du nom de la balise. Pour les nœuds à 98,99999%, ce paramètre sera NULL . Dans certains espaces de noms, nous devons spécifier un préfixe ou un nom de balise avec un registre fixe. Par exemple, baseProfile dans l'espace de noms SVG.


La logique du travail est simple. Si nous avons une balise avec un registre clairement défini, alors:


  1. Ajoutez-le à la base générale des balises en minuscules. Obtenez l'identifiant du tag.
  2. Ajoutez l'identifiant de balise et le nom de balise d'origine dans la représentation textuelle au nœud.

Balises personnalisées


Un développeur peut créer n'importe quelle balise en HTML. Comme nous n'avons que les balises que nous connaissons dans une table de hachage statique et que l'utilisateur peut en créer, nous avons besoin d'une table de hachage dynamique.


Tout a l'air très simple. Lorsque la balise nous parviendra, nous verrons si elle se trouve dans la table de hachage statique. S'il n'y a pas de balise, regardons la dynamique, s'il n'y en a pas, augmentons le compteur d'identifiants de un et ajoutons la balise à la table dynamique.


Tout ce qui est décrit se produit au stade du tokenizer. À l'intérieur du tokenizer et après toutes les comparaisons, passez par Tag ID (à de rares exceptions près).


Début et fin


Maintenant, dans le tokenizer, nous n'aurons pas de traitement de données. Nous ne copierons et ne convertirons rien. Nous prenons simplement des pointeurs vers le début et la fin des données.


Tous les traitements de données, tels que les liens symboliques, auront lieu au stade de la construction des arbres.
Ainsi, nous connaîtrons la taille des données pour l'allocation ultérieure de mémoire.


Attributs


Ici, tout est aussi simple. Nous ne copions rien, mais enregistrons simplement des pointeurs au début / à la fin du nom et des valeurs d'attribut. Toutes les transformations se produisent au moment de la création de l'arborescence.


Drapeaux


Étant donné que nous avons des jetons unifiés, nous devons en quelque sorte informer l'arborescence du type de jeton. Pour ce faire, utilisez le champ bitmap Flags.


Le champ peut contenir les valeurs suivantes:


 enum { LXB_HTML_TOKEN_TYPE_OPEN = 0x0000, LXB_HTML_TOKEN_TYPE_CLOSE = 0x0001, LXB_HTML_TOKEN_TYPE_CLOSE_SELF = 0x0002, LXB_HTML_TOKEN_TYPE_TEXT = 0x0004, LXB_HTML_TOKEN_TYPE_DATA = 0x0008, LXB_HTML_TOKEN_TYPE_RCDATA = 0x0010, LXB_HTML_TOKEN_TYPE_CDATA = 0x0020, LXB_HTML_TOKEN_TYPE_NULL = 0x0040, LXB_HTML_TOKEN_TYPE_FORCE_QUIRKS = 0x0080, LXB_HTML_TOKEN_TYPE_DONE = 0x0100 }; 

En plus du type de jeton qui s'ouvre ou se ferme, il existe des valeurs pour le convertisseur de données. Seul le tokenizer sait comment convertir correctement les données. En conséquence, le tokenizer marque dans le token comment les données doivent être traitées.


Jeton de personnage


D'après ce qui a été décrit précédemment, nous pouvons conclure que le jeton de symbole a disparu de nous. Oui, nous avons maintenant un nouveau type de jeton: LXB_HTML_TOKEN_TYPE_TEXT . Maintenant, nous créons un jeton pour le texte entier entre les balises, indiquant comment il devrait être traité à l'avenir.


Pour cette raison, nous devrons changer les conditions de construction de l'arbre. Nous devons le former à travailler non pas avec des jetons symboliques, mais avec des jetons de texte: convertir, supprimer des caractères inutiles, sauter des espaces, etc.


Mais, il n'y a rien de compliqué. Au stade de la construction d'un arbre, les changements seront minimes. Mais le tokenizer ne correspond plus du tout à la spécification du mot. Mais on n'a pas besoin de lui, c'est normal. Notre tâche consiste à obtenir une arborescence HTML / DOM entièrement conforme aux spécifications.


Étapes de Tokenizer


Pour assurer un traitement rapide des données dans le tokenizer, nous ajouterons notre itérateur à chaque étape. Selon le cahier des charges, chaque étape accepte un symbole pour nous et, selon le symbole arrivé, prend des décisions. Mais, la vérité est que c'est très cher.


Par exemple, pour passer de l'étape ATTRIBUTE_NAME à l'étape ATTRIBUTE_VALUE nous devons trouver un espace blanc dans le nom de l'attribut, qui indiquera sa fin. Selon la spécification, nous devons alimenter par caractère à l'étape ATTRIBUTE_NAME jusqu'à ce qu'un caractère d'espace ATTRIBUTE_NAME , et cette étape ne passe pas à une autre. C'est très cher, généralement il est implémenté via un appel de fonction pour chaque caractère ou un rappel comme "tkz-> next_code_point ()".


Nous ajoutons une boucle à l'étape ATTRIBUTE_NAME et passons la totalité du tampon entrant. Dans la boucle, nous recherchons les symboles dont nous avons besoin pour changer et continuer à travailler sur la prochaine étape. Ici, nous obtenons beaucoup de gains, même des optimisations de compilateur.


Mais! Le pire, c'est que nous avons ainsi rompu le support des morceaux (morceaux) hors de la boîte. Grâce au traitement caractère par symbole à chaque étape du générateur de jetons, nous avons pris en charge les morceaux, et nous l'avons maintenant cassé.


Comment y remédier? Comment implémenter le support des morceaux?! C'est simple, nous introduisons le concept de tampons entrants (Tampon entrant).


Tampon entrant


Souvent, HTML analyse en morceaux. Par exemple, si nous recevons des données sur le réseau. Afin de ne pas rester inactif en attendant les données restantes, nous pouvons envoyer des données déjà reçues pour traitement / analyse. Naturellement, les données peuvent être déchirées n'importe où. Par exemple, nous avons deux tampons:


D'abord


 <div clas 

Deuxième


 s="oh-no-oh-no"> 

Comme nous ne copions rien au stade de la tokenisation, mais ne prenons que des pointeurs vers le début et la fin des données, nous avons un problème. Pointeurs vers différents tampons utilisateur. Et étant donné que les développeurs utilisent souvent le même tampon pour les données, nous avons affaire à un pointeur vers le début de données inexistantes.


.
:


  1. (Incoming Buffer).
  2. ( ) , ? , . , . 99% .

" " . .


, . , ( ) . . , , , . .


:


, . , : . . ( ), . .


: . , .



.


, . , .


:



 tree_build_in_body_character(token) { if (token.code_point == '\0') { /* Parse error, ignore token */ } else if (token.code_point == whitespaces) { /* Insert element */' } /* ... */ } 

Lexbor HTML


 tree_build_in_body_character(token) { lexbor_str_t str = {0}; lxb_html_parser_char_t pc = {0}; pc.drop_null = true; tree->status = lxb_html_token_parse_data(token, &pc, &str, tree->document->mem->text); if (token->type & LXB_HTML_TOKEN_TYPE_NULL) { /* Parse error */ } /* Insert element if not empty */ } 

, . :


 pc.replace_null /*   '\0'    (REPLACEMENT CHARACTER (U+FFFD)) */ pc.drop_null /*   '\0' */ pc.is_attribute /*          " " */ pc.state /*  .        . */ 

. - \0 , - REPLACEMENT CHARACTER . - , - . .


, . . , <head> . , , : " ". , .


<sarcasm>

La spécification HTML (dans la section de construction de l'arborescence) parle de la balise sarcasm. J'ai vu plus d'une fois comment les développeurs d'analyseurs ont aveuglément activé le traitement de cette balise.


 An end tag whose tag name is "sarcasm" Take a deep breath, then act as described in the "any other end tag" entry below. 

Les rédacteurs de spécifications plaisantent.



HTML DOM/HTML Interfaces HTML/DOM HTML .


, :


  1. ( )

    • Incoming Buffer
    • Tag ID
    • ̆ : , N+
    • ̆
    • ̈


i7 2012 , , 235MB (Amazon-).


, 1.5/2 , . , . , CSS (Grammar, , Grammar). HTML, CSS , "".


Code source


HTML Lexbor HTML .


PS


CSS Grammar. , . - 6-8 .


,

N'hésitez pas à aider le projet. Par exemple, si vous aimez écrire de la documentation pendant votre temps libre.
N'hésitez pas à soutenir le projet en roubles (je ne serai pas offensé par les autres devises non plus). À ce sujet dans PM.


Merci de votre attention!

Source: https://habr.com/ru/post/fr430736/


All Articles