Exporter (0) Imprimer
Développer tout
Ce sujet n'a pas encore été évalué - Évaluez ce sujet

Création de texte de balisage dans Visual Basic .NET

Visual Studio .NET 2003

Darren Neimke

Résumé : Voici un outil qui vous permet de baliser du code dans n'importe quel langage, que vous pouvez personnaliser avec des couleurs, qui prend en charge les opérations de glisser-déplacer, qui produit du texte contenant des balises HTML ou XML, et qui génère des balises HTML.

Téléchargez le fichier exemple MarkUp.msi.

Introduction

J'ai toujours manipulé du code. J'écris du code à la fois pour mon travail et pour mon plaisir, et j'envoie des extraits de code à mes amis ou à des groupes de discussion. Lorsque je partage du code, je fais très attention à sa présentation. Par exemple, je vérifie que j'ai utilisé des noms de variable qui soient parlants et que toutes les variables déclarées sont effectivement utilisées. Je m'assure également que les couleurs appliquées à l'extrait de code correspondent à celles de mon éditeur : les chaînes en rouge, les commentaires en vert, les mots clés en bleu, les polices en Lucinda Console, etc. Il y a plusieurs manières d'effectuer ces vérifications, mais la méthode qui me semble la plus pratique consiste à utiliser Microsoft Word et Microsoft Visual Studio® comme suit :

  1. Copiez du code depuis la vue Code ou HTML d'un document dans Visual Studio .NET.
  2. Collez-le dans un document Word à l'aide de la commande Collage spécial | Texte mis en forme (RTF) {pour conserver les styles}.
  3. Copiez-le depuis le document Word et collez-le dans la vue Design d'un nouveau document HTML.

Les styles sont conservés, mais vous obtenez une syntaxe de formatage Office détaillée, du type :

<P class=MsoNormal style="mso-layout-grid-align: none">
 <SPAN lang=EN-US style="FONT-SIZE: 8pt; COLOR: blue; 
 FONT-FAMILY: 'Courier New'; mso-ansi-language: EN-US">using</SPAN>
 <SPAN lang=EN-US style="FONT-SIZE: 8pt; 
 FONT-FAMILY: 'Courier New'; mso-ansi-language: EN-US"> System;
 <?xml:namespace prefix = o />
 <o:p></o:p>
 </SPAN>
 </P>

Alors que vous n'avez besoin que de :

<font color="Blue">using</font> System ;

Le second exemple est bien évidemment plus souple à gérer, sans compter qu'il nécessite la transmission de beaucoup moins de texte que le premier exemple. Le temps de rendu des pages est dans ce cas plus long.

Il ne serait pas compliqué de développer une macro qui, à l'aide d'expressions régulières, analyserait le formatage Microsoft Office et le réduirait à une forme plus concentrée. Toutefois, cette macro ne résoudrait pas tous les problèmes, et impliquerait de recourir à Visual Studio et à Word chaque fois que vous souhaitez baliser du code, tout en nécessitant une bonne maîtrise des expressions régulières. De plus, vous seriez obligé d'utiliser Visual Studio .NET pour appliquer la couleur appropriée aux différents éléments du langage choisi.

J'ai donc décidé de créer un outil qui :

  • me permettrait de baliser du code écrit dans n'importe quel langage ;
  • serait suffisamment souple pour prendre en charge non seulement mon schéma de couleurs, mais aussi celui de tout autre utilisateur ;
  • autoriserait les opérations de glisser-déplacer ;
  • générerait du texte contenant des balises HTML ou XML afin que la personne à laquelle le texte serait transmis puisse le baliser avec son propre schéma de couleurs ;
  • restituerait des balises HTML sous la forme de balises FONT ou de balises SPAN avec des noms de classe de styles CSS, comme indiqué dans la figure 1.

    Capture d'écran de balisage HTML restitué

    Figure 1. Capture d'écran de balisage HTML restitué

Voici un exemple de ce que cette application me permettrait de faire. À partir de l'extrait de code suivant :

' This is a comment
 Dim foo As New Bar()

Elle pourrait générer une sortie dans l'un des trois formats suivants :

Jetons bruts
<Comment>' This is a comment</Comment>
<Keyword>Dim</Keyword> foo <Keyword>As</Keyword> 
<Keyword>New</Keyword> Bar() 

Format HTML
<font color="green">' This is a comment</font>
<font color="blue">Dim</font> foo <font color="blue">As</font> 
<font color="blue">New</font> Bar()

Format CSS
<span class="Comment">' This is a comment</span>
<span class="Keyword">Dim</span> foo <span class="Keyword">As</span> 
<span class="Keyword">New</span> Bar()

De plus, si vous choisissez le format CSS, l'outil crée également une définition de feuille de style.

Feuille de style CSS générée par l'outil

Figure 2. Feuille de style CSS générée par l'outil

Comment définir du code ?

Si les mots qui constituent un langage (la syntaxe) sont propres à chaque langage, tous les langages informatiques partagent heureusement des éléments communs, comme les mots clés, les opérateurs, les fonctions et les instructions monolignes et multilignes. En fait, quelle que soit la syntaxe utilisée par un langage, les opérations (on parle également de sémantique) que les langages vous permettent d'effectuer sont fondamentalement les mêmes. C'est notamment le cas des instructions de répétition, des instructions de contrôle, des fonctions et de la déclaration de variables. Pour mieux visualiser certaines de ces différences syntaxiques, voici un tableau récapitulatif.

Opération Visual Basic C#
Instructions de contrôle Do, For...Each, While do, foreach, while
Chaînes "..." "..."
Commentaires '..., Rem //..., /*...*/
Classes/Fonctions Function, Class, Sub function, class, void
Types de données Integer, String, Boolean int, string, bool

Surtout, outre les regroupements opérationnels, il est également possible de caractériser les éléments d'un langage à un niveau encore supérieur, comme suit :

  1. les types définis par leurs caractères initial ou final ;
  2. les types définis par une suite de caractères situés dans un mot.

Pour illustrer mon propos, prenons le cas des chaînes et des types de données dans le tableau des opérations ci-dessus. Les chaînes font partie du premier type : elles sont définies par leurs caractères initial et final. Ainsi, lorsque je trouve le caractère initial d'une chaîne, je sais que tous ceux qui le suivront appartiendront à la chaîne jusqu'à ce que je rencontre le caractère final de la chaîne. En voici une illustration dans l'exemple ci-dessous dans lequel la chaîne n'est identifiée que par son caractère initial (") et son caractère final ("). Ces deux caractères peuvent être des caractères invisibles, comme le début d'une ligne ou un caractère de fin de ligne. Les commentaires Visual Basic®, par exemple, commencent par une apostrophe et se terminent par un caractère de fin de ligne.

Ceci ne fait pas partie de la chaîne "mais cela oui, ainsi que ceci", mais pas cela.

Les types de données, quant à eux, appartiennent au second type : ils sont définis par des limites de mots et contiennent des suites de caractères. Contrairement aux premiers types, il est impossible de prévoir quels sont leurs caractères initiaux ou finaux ; seuls les caractères requis au milieu sont connus. Les limites de mot peuvent être définies comme la position à laquelle figurent, d'un côté, un caractère de mot et, de l'autre côté, un caractère n'intervenant pas dans la formation d'un mot. Ceci garantit que les mots détectés sont vraiment des mots à part entière et non des séquences de lettres intégrées dans des mots. Par exemple, la séquence "Rem" dans "Remémorer" ne constitue pas un mot à part entière. Une limite de mot peut être une parenthèse "(", un tiret "-", deux-points ":", voire le début d'une ligne.

Dans la suite de cet article, je ferai référence à ces types d'éléments comme suit :

Types Block ou Non-Word
  • MultiLine Block
  • SingleLine Block (défini par des caractères initial/final, mais limité à une seule ligne comme les chaînes Visual Basic)
Type Word
  • Keyword
  • Operator

Balisage du texte

Il est donc clair que la démarche à suivre pour créer un outil capable de baliser n'importe quel langage consiste à créer une définition de ces types, en fonction des remarques mentionnées dans la première partie. Pour ce faire, je vais créer un fichier de configuration XML qui définit le langage, les types d'éléments existant dans ce langage, ainsi que chacune des opérations, pour pouvoir appliquer des couleurs d'une manière similaire à celle de mon éditeur favori : Visual Studio .NET.

Chaque langage sera défini par un élément Language qui contiendra plusieurs éléments Pattern. Ces éléments décriront chacune des fonctions syntaxiques. Chaque élément Pattern doit contenir les constituants suivants :

  • Type : MultiLine, SingleLine, Keyword, Operator
  • Name : le nom de l'opération (String, Keyword, Comment).
  • BeginChar : le caractère initial des types NonWord.
  • EndChar : le caractère final des types NonWord.
  • Words : une collection de mots pour les types Word.
  • FontInfo : couleur, police, taille, etc.

Autre caractéristique originale mais importante, la présence de caractères d'échappement dans des types Non-mot autorise l'utilisation du caractère qui serait normalement le délimiteur final à incorporer dans le type NonWord. Par exemple, pour imprimer "Foo" avec les guillemets, C# fournit le caractère d'échappement "\" à utiliser comme suit :

string myString = "Norman says \"Hello\"." ; // Prints: Norman says "Hello".

Enfin, certains langages doivent prendre en charge la distinction majuscules/minuscules, à l'image de C#. Voici une version très abrégée de la définition du langage C# :

<Language name="CSharp" caseSensitive="true">
 <Pattern type="MultiLine" name="BlockComment" beginChar="/*" endChar="*/">
 <FontSettings name="Lucinda Console" color="Green" size="10" />
 </Pattern>
 <Pattern type="SingleLine" name="InlineComment" beginChar="//" endChar="\n">
 <FontSettings name="Lucinda Console" color="Green" size="10" />
 </Pattern>
 <Pattern type="SingleLine" name="XmlComment" beginChar="///" endChar="\n">
 <FontSettings name="Lucinda Console" color="LightGrey" size="10" />
 </Pattern>
 <Pattern type="MultiLine" name="String" beginChar="&quot;" endChar="&quot;" 
 escapeChar="\">
 <FontSettings name="Lucinda Console" color="Red" size="10" />
 </Pattern>
 <Pattern type="Keyword" name="ReferenceType">
 <FontSettings name="Lucinda Console" color="Blue" size="10" />
 <Words>
 <Item>class</Item>
 <Item>interface</Item>
 <Item>delegate</Item>
 <Item>object</Item>
 <Item>string</Item>
 </Words>
 </Pattern>
 .
 .
 .
 .
 </Language> 

Comme vous le constatez, les types syntaxiques MultiLine et SingleLine (NonWord) ont les attributs beginChar/endChar, tandis que les types Word contiennent une collection de Words. Le nœud de langage a également l'attribut caseSensitive dont la valeur est true, tandis que le type de modèle (Pattern type) String autorise le caractère d'échappement "\".

Création de l'outil

L'outil doit charger le fichier de configuration du langage et appliquer un jeu de règles pour dériver les expressions régulières qui permettront de localiser les différents éléments dans les extraits de code chargés.

Au niveau de son fonctionnement interne, la conception de l'outil est relativement simple. J'ai une classe Language et une classe Pattern pour abstraire les données dans mon fichier de configuration, ainsi qu'un module HtmlFormatting dont les routines permettent de formater et de colorier les extraits de code.

Lors de la création des classes Pattern, j'ai remarqué qu'il s'agissait d'une opportunité idéale pour utiliser l'héritage. J'ai donc créé une classe Base pour les fonctions courantes, que j'ai complétée avec des fonctions spécialisées comme les propriétés BeginChar/EndChar ou la collection Words selon le type de modèle. Les propriétés communes à tous les modèles sont Name, FontSettings, Type et RegexPattern.

Les classes spécialisées WordPattern et NonWordPattern complètent la classe de base Pattern. La classe NonWordPattern fournit les propriétés BeginChar et EndChar, tandis que la classe WordPattern affiche une collection de Words. Ces deux propriétés fournissent des implémentations spécifiques qui permettent d'afficher l'expression régulière qui la définit.

En résumé, la sémantique permettant de créer ces modèles regex est similaire à la syntaxe suivante :

Types NonWord
BeginChar<Any Text Until>EndChar

Pour des commentaires monolignes dans Visual Basic, le modèle se présente comme suit :

'[^\n\r]*

Types Word
WordBoundary<Any Single Word In Words Collection>WordBoundary

Pour des fonctions TSQL, le modèle se présente comme suit :

\b(AVG|MAX|BINARY_CHECKSUM|..)\b

Problèmes rencontrés et résolus

Maintenant, les classes Pattern fournissent chacune un modèle d'expression régulière qui permet de les localiser dans le corps d'un texte. L'étape suivante consiste à orchestrer le processus d'identification des modèles afin de repérer tous les éléments contenus dans l'extrait de code. Parmi les problèmes à résoudre, citons :

  • la nécessité de ne pas baliser la même suite de caractères à plusieurs reprises ;
  • la définition de types WORD limités par des caractères inhabituels ;
  • la prise en charge des caractères d'échappement.

Le premier de ces trois problèmes survient généralement lorsque des mots clés sont incorporés dans des éléments BLOCK comme des chaînes ou des commentaires. Par exemple, dans la ligne de code inactivée ci-dessous, il n'est pas souhaitable de reconnaître et de colorier les mots clés séparément. Il vaut mieux que la ligne entière soit colorée comme un commentaire et balisée une seule fois.

' Dim foo As New Bar( whatever )

Un algorithme de base baliserait chaque élément l'un après l'autre. Si la tâche en cours consistait à localiser et à baliser des commentaires, c'est ainsi que les choses se passeraient. S'il s'agissait de baliser des mots clés, c'est également ce que l'on obtiendrait. J'avais besoin d'un traitement qui soit un peu plus abouti. Je cherchais un moyen de laisser traîner quelques indices qui indiqueraient à notre analyseur que cette section a déjà été balisée.

Insertion de jetons

Pour résoudre le problème des balisages répétés, j'ai implémenté une phase d'insertion de jetons, qui délimite des blocs de texte et les identifie avec des jetons. De plus, l'extrait de code est encadré par les jetons <available>...</available>. Lorsqu'un élément du langage est localisé dans l'extrait de code, les jetons correspondants sont insérés, de même que les jetons <available> d'ouverture et de fermeture.

Supposons que vous voulez baliser le code SQL suivant :

SELECT *
 FROM dbo.Customers
 WHERE dateCreated 
 BETWEEN @startDate 
 AND @endDate

La première étape consiste à encadrer l'extrait avec des jetons <available> pour indiquer à l'analyseur la partie du code à prendre en compte :

<available>SELECT *
 FROM dbo.Customers
 WHERE dateCreated 
 BETWEEN @startDate 
 AND @endDate</available>

Maintenant, supposons que, pendant l'insertion des jetons, les mots sont trouvés et identifiés dans l'ordre suivant : BETWEEN, WHERE, SELECT, FROM et AND. La séquence d'ouverture, d'insertion de jeton et de fermeture de mon analyseur entraîne les modifications suivantes dans l'extrait de code à la fin de chaque identification dans la séquence :

Après avoir trouvé "BETWEEN"
<available>SELECT *
 FROM dbo.Customers
 WHERE dateCreated 
 </available><token>BETWEEN</token><available> @startDate 
 AND @endDate</available>

Après avoir trouvé "WHERE"
<available>SELECT *
 FROM dbo.Customers
 </available><token>WHERE</token><available> dateCreated 
 </available><token>BETWEEN</token><available> @startDate 
 AND @endDate</available>

Après avoir trouvé "SELECT"
<available></available><token>SELECT</token><available> *
 FROM dbo.Customers
 </available><token>WHERE</token><available> dateCreated 
 </available><token>BETWEEN</token><available> @startDate 
 AND @endDate</available>

Après avoir trouvé "FROM"
<available></available><token>SELECT</token><available> *
 </available><token>FROM</token><available> dbo.Customers
 </available><token>WHERE</token><available> dateCreated 
 </available><token>BETWEEN</token><available> @startDate 
 AND @endDate</available>

Après avoir trouvé "AND"
<available></available><token>SELECT</token><available> *
 </available><token>FROM</token><available> dbo.Customers
 </available><token>WHERE</token><available> dateCreated 
 </available><token>BETWEEN</token><available> @startDate 
 </available><token>AND</token><available> @endDate</available>

Comme vous pouvez le constater, après chaque identification, le texte disponible est séparé du reste et le texte identifié est encadré par la balise <token>. Lorsque ce mécanisme est opérationnel, il suffit de s'assurer que la recherche visant à repérer le texte à baliser ne s'effectue que sur le texte situé entre les jetons <available>. Une fois le processus d'identification terminé, je supprime les marqueurs <available> et </available> et je conserve uniquement les jetons qui identifient les éléments syntaxiques.

codeSnippet = Regex.Replace(codeSnippet, "</?available>", "")

 <token>SELECT</token> *
 <token>FROM</token> dbo.Customers
 <token>WHERE</token> dateCreated 
 <token>BETWEEN</token> @startDate 
 <token>AND</token> @endDate

Avant l'apparition de .NET, j'aurais rencontré plusieurs problèmes liés à la création d'un modèle servant à capturer les sections Available et Non-Available, à l'énumération de la collection Matches qui en découle, au balisage des captures Available, et au ré-assemblage de ces captures par concaténation pour vérifier que l'identification avait permis de n'extraire que certaines parties du texte. Cette méthode lourde n'était pas fiable (particulièrement sans les captures nommées) et on modifiait davantage de texte que prévu.

Dans .NET, la diversité des expressions régulières permet d'associer un délégué MatchEvaluator à la méthode Replace des expressions régulières. Ceci a pour effet de ne transmettre que le texte identifié à un gestionnaire d'événements et de renvoyer le texte de remplacement. L'ensemble de la concaténation s'effectue en arrière-plan. Ainsi, la quantité de texte modifiée est réduite au strict minimum, ce qui minimise le risque d'erreurs.

Transmission des identifications à un délégué MatchEvaluator
' important to ensure that we are only tokenizing parts of the snippet that 
 ' haven't as yet been tokenized
 Private m_AvailablePartPattern As String = _
 "(?'available'<available>[^\<]*[\w\W\s\S]*?<\/available>)"

 ' matches the "available" text and hands it off
 ' to a delegate for further inspection.
 Public Sub TokenizeWordElements( _
 ByVal patternName As String, _
 ByVal regexString As String, _
 ByVal _caseSensitive As Boolean, _
 ByRef codeSnippet As String _
 )
 m_CaseSensitive = _caseSensitive
 m_Name = patternName
 m_REString = regexString

 Dim _delegate As New MatchEvaluator(AddressOf WordElementMatchHandler)

 Dim r As New Regex(m_AvailablePartPattern, _
 RegexOptions.IgnoreCase Or _
 RegexOptions.Compiled _
 )
 codeSnippet = r.Replace(codeSnippet, _delegate)
 End Sub

 Private Function WordElementMatchHandler(ByVal _match As Match) As String
 ' if, for some reason no group was found... bail out
 If m_Name Is String.Empty Then Return _match.Value

 Dim opts As RegexOptions = RegexOptions.Multiline
 If Not m_CaseSensitive Then
 opts = opts Or RegexOptions.IgnoreCase
 End If
 Dim re As New Regex(m_REString, opts)

 ' tokenize the match
 Return re.Replace(_match.Value, "</available><" & m_Name & ">$1</" & _
 M_Name & "><available>")
 End Function


L'importance de l'ordre

Je voudrais revenir sur un point que j'ai mentionné plus haut mais sans l'avoir expliqué : l'ordre dans lequel effectuer l'identification. Vous devez vous assurer que les éléments sont identifiés en commençant par ceux qui ont la portée la plus importante, et en terminant par ceux qui ont la portée la plus faible. Cela signifie que les types BLOCK MultiLine sont identifiés avant les types de modèle SingleLine, et que tous deux sont identifiés avant les types WORD. Ceci s'explique par le fait que les éléments BLOCK peuvent contenir des éléments qui, dans le cas contraire, seraient qualifiés en tant que types WORD, et non le contraire.

L'identification d'éléments BLOCK est relativement simple. Vous détectez le caractère BeginChar et poursuivez l'identification jusqu'au-delà du caractère EndChar. Dans le cas d'une chaîne Visual Basic, cela signifie que vous arrivez à la fin de la ligne sans rencontrer les guillemets de fermeture ("). Voici le code qui doit créer cette chaîne de modèle :

Public Function GetBlockPattern() As String
 Dim rePattern As String = "("
 Dim ptrn As PatternBase
 For Each ptrn In Me.Patterns
 If TypeOf ptrn Is NonWordPattern Then
 If rePattern.Length > 1 Then
 rePattern &= "|"
 End If
 rePattern &= "(?'" & ptrn.Name & "'" & _
 ptrn.PatternString() & ")"
 End If
 Next
 rePattern &= ")+"
 Return rePattern
 End Function

Comme vous pouvez le voir, il s'agit d'une expression Or portant sur chaque expression du type Pattern Block. Si vous examinez la logique de création de PatternString, vous constatez que l'implémentation dépend de la présence ou de l'absence de EscapeChar dans le modèle.

' class specific implementation of CreatePatternString
 ' logic for a non-Word type is: BeginChar <Any Text Until> EndChar
 Private Function CreatePatternString() As String
 Dim retVal As String
 If Me.EndChar = "\n" Then
 retVal = Regex.Escape(Me.BeginChar) & "[^\n\r]*"
 Else
 If Me.HasEscapeChar Then
 retVal = String.Format("{0}(?>{1}.|[^{2}]|.)*?{3}", _
 Regex.Escape(Me.BeginChar), Regex.Escape(Me.EscapeChar), _
 Regex.Escape(Left(Me.EndChar, 1)), Regex.Escape(Me.EndChar))

 Else
 retVal = String.Format("{0}[^{1}]*(?>[^{1}]|.)*?{2}", _
 Regex.Escape(Me.BeginChar), _
 Regex.Escape(Left(Me.EndChar, 1)), Regex.Escape(Me.EndChar))

 End If
 End If

 Return retVal
 End Function

Conservation des états

Pour créer les modèles des types BLOCK, j'ai utilisé ExplicitCapture (?>...) afin de minimiser le nombre de retours en arrière disponibles. En fait, je peux utiliser ExplicitCapture car je sais que si je trouve le caractère BeginChar d'un modèle, je veux poursuivre la lecture du texte jusqu'à trouver le caractère EndChar ou jusqu'à échouer. En résumé, je ne veux rien laisser au hasard. La non-conservation des états enregistrés peut générer des erreurs plus rapidement que lorsque ces états sont conservés et réappliqués ultérieurement pendant l'identification.

Possibilités d'améliorations futures

J'ai essayé de créer les expressions régulières de façon à tenir compte le moins possible des implémentations de chaque langage. L'une des améliorations futures consisterait à introduire des contrôles sémantiques propres à chaque langage, afin d'accélérer le traitement. Ce qui pourrait se traduire, dans le cas de TSql, par le codage en dur du marqueur de limite de mot "\b" ou "@@". En effet, on sait qu'il est possible d'inclure "@@" dans un mot sans que le moteur regex ne considère ces caractères en tant que tels. La création d'une telle séquence en TSql réduirait la tolérance sur le caractère initial pour les langages qui ne le requièrent pas, accélérant ainsi la vitesse de traitement.

Conclusion

Dans cet article, vous avez vu comment créer un mini-analyseur utilisant des expressions régulières pour séparer des mots clés d'un langage de programmation et appliquer des couleurs aux éléments syntaxiques. Vous avez également vu que les nouvelles fonctionnalités des expressions régulières dans .NET offrent une plus grande puissance et une flexibilité accrue pour manipuler des modèles dans le texte.

Darren Neimke est développeur d'applications senior. Il participe à la création d'applications ASP depuis les débuts d'Internet, vers la fin des années 90. Auparavant, il développait des programmes de comptabilité à l'aide d'Access 2 et de VBA. Ces derniers temps, Darren a décidé d'explorer le monde des expressions régulières. Aujourd'hui, il consacre la majeure partie de son temps à la maintenance de RegexLib.com et à la rédaction d'articles sur les expressions régulières pour son « blog », accessible à l'adresse http://weblogs.asp.net/DNeimke Lien externe au site MSDN France Site en anglais.



Dernière mise à jour le mercredi 10 décembre 2003



Cela vous a-t-il été utile ?
(1500 caractères restants)
Merci pour vos suggestions.
Afficher:
© 2014 Microsoft. Tous droits réservés.