L'essentiel d'une extension

Le fichier install.rdf

Dans la section précédente, nous avons examiné le contenu de l'extension "Hello World". Maintenant, nous allons examiner dans son code et ses fichiers, en commençant par le fichier install.rdf. Vous pouvez l'ouvrir avec n'importe quel éditeur de texte.

Le fichier est formaté selon un type particulier XML appelé RDF. RDF est utilisé pour le mécanisme de stockage centralisé pour Firefox, mais il est maintenant remplacé par un système plus simple de base de données. Nous aborderons ces deux systèmes de stockage plus loin dans le tutoriel.

Maintenant, regardons les parties importantes du fichier.

<em:id>helloworld@xulschool.com</em:id>

Il s'agit d'un identifiant unique pour l'extension. Firefox en a besoin pour distinguer votre extension des autres, il est donc nécessaire que cet identifiant soit unique.

Il existe deux normes acceptées pour les IDS des extensions. Le premier est le format de type e-mail comme dans l'exemple "Hello World", qui devrait être quelque chose comme <project-name>@<yourdomain>. L'autre pratique standard consiste à utiliser une chaîne UUID générée qui est extrêmement peu probable à être reproduite. Les systèmes basés sur Unix ont un outil en ligne de commande appelé uuidgen qui génère des UUIDs. Il existe également des outils téléchargeables disponibles pour toutes les plates-formes pour les générer. Les crochets entourant ne sont que la notation, et il s'agit d'une simple notation courante. Tant que votre id a une certaine unicité, il est possible d'utiliser l'une des deux formes.

<em:name>XUL School Hello World</em:name>
<em:description>Welcome to XUL School!</em:description>
<em:version>0.1</em:version>
<em:creator>Appcoast</em:creator>
<em:homepageURL>https://developer.mozilla.org/en/XUL_School</em:homepageURL

Il s'agit des données qui sont affichées avant et après l'installation de l'extension, et que vous pouvez voir dans la fenêtre des modules complémentaires. L'URL de la page d'accueil peut être visité par un clic droit sur l'extension et en choisissant Visitez la page Web. Il y a beaucoup d'autres balises pouvant être ajoutées, pour les auteurs et les traducteurs. La spécification complète du fichier install.rdf contient tous les détails.

Comme les extensions peuvent être traduites en plusieurs langues, il est souvent nécessaire de traduire la description de l'extension, ou même son nom. Sur Firefox 3 et suivant, une description localisée et un nom peuvent être ajoutés par le code suivant :

<em:localized>
  <Description>
    <em:locale>es-ES</em:locale>
    <em:name>XUL School Hola Mundo</em:name>
    <em:description>Bienvenido a XUL School!</em:description>
  </Description>
</em:localized>

La chaîne de localisation es-ES indique que la langue est l'espagnol (es) pour l'Espagne (ES). Vous pouvez ajouter autant de sections <em:localized> que vous avez besoin. Pour Firefox 2, la localisation de ce fichier est un peu plus compliqué. Nous verrons la localisation plus loin dans cette section.

<em:type>2</em:type>

Ceci précise que le module en cours d'installation est une extension. Vous pouvez consulter les différents types possibles dans la spécification install.rdf.

<em:targetApplication>
  <Description>
    <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
    <em:minVersion>3.0</em:minVersion>
    <em:maxVersion>3.6.*</em:maxVersion>
  </Description>
</em:targetApplication>

Ce nœud spécifie l'application cible et les versions cibles de l'extension, en particulier Firefox de la version 3.0 au versions 3.6.*. L'UUID est l'ID unique de Firefox. Les autres applications Mozilla et basées sur Mozilla telles que Thunderbird et Seamonkey ont le leurs. Vous pouvez avoir une extension qui fonctionne sur de multiples applications et versions. Par exemple, si vous créez une extension de Firefox, il faudrait normalement peu d'effort pour la porter sur Flock ou sur SeaMonkey, qui ont une interface utilisateur et des caractéristiques très similaires.

Le min et max de la version spécifie l'étendue des versions où l'extension peut être installée. Des détails sont disponibles sur le format de version. Si l'application ou la plage de version ne correspondent pas, vous ne serez pas autorisé à installer l'extension, ou l'extension sera installée dans un état désactivé. Les utilisateurs peuvent désactiver les contrôles version grâce à des préférences ou d'installer des modules complémentaires comme Add-on Compatibility Reporter.

Ce sont les seules informations dont Firefox et d'autres applications Mozilla ont besoin pour installer un module complémentaire. Toutes erreurs ou manques d'information entraînent un échec du processus d'installation, ou laissent l'extension s'installer dans un état désactivé.

Le fichier chrome.manifest

Le chrome est l'ensemble des éléments de l'interface utilisateur d'une application qui sont situés en dehors de la zone de contenu d'une fenêtre. Les barres d'outils, les barres de menus, les barres de progression, et les titres de fenêtres sont tous des exemples d'éléments qui font habituellement partie du chrome.

Citation de Enregistrement Chrome.

En d'autres termes, le chrome correspond à tout ce que vous voyez dans Firefox. Toutes les fenêtres Firefox peuvent être considérées comme ayant deux parties : (1) le chrome et (2) peut-être une zone de contenu, comme celui qui affiche les pages Web dans un onglet Firefox. Les fenêtres comme le gestionnaire de modules complémentaires et de la fenêtre de téléchargements sont en chrome pur. La plupart du code pour une extension réside dans le dossier chrome, tout comme dans l'exemple Hello World.

Les fichiers Chrome sont tous empaquetés dans une archive JAR, généralement nommée d'après l'extension. Il n'est pas nécessaire d'empaqueter les fichiers chrome, mais c'est une pratique courante et recommandée pour des raisons de performances.

Comme nous l'avons vu dans la structure des répertoires de l'extension décompressée, le chrome est composé de 3 sections : content, locale et skin. Les 3 sont nécessaires pour la plupart des extensions. Si nous ouvrons le fichier chrome.manifest (encore une fois, un éditeur de texte fera l'affaire), nous voyons que les 3 mêmes sections sont mentionnés :

content   xulschoolhello              jar:chrome/xulschoolhello.jar!/content/
skin      xulschoolhello  classic/1.0 jar:chrome/xulschoolhello.jar!/skin/
locale    xulschoolhello  en-US       jar:chrome/xulschoolhello.jar!/locale/en-US/

Le fichier chrome.manifest indique à Firefox où chercher les fichiers chrome. Le texte est espacé pour ressembler à un tableau, mais ce n'est pas nécessaire. L'analyseur ignorera les espaces répétées.

Le premier mot d'une ligne indique à Firefox ce qui est déclaré (le contenu, le thème graphique, la langue, ou d'autres choses que nous verrons plus loin). Le second est le nom du paquet, que nous expliquerons bientôt. Les paquets des thèmes et langages ont une troisième valeur afin de définir ce qu'ils étendent. Il peut y avoir plusieurs thèmes ou langages. Le cas le plus fréquent est d'avoir une seule entrée pour le thème correspondant au thème global classic/1.0, et des entrées multiples de langages, une pour chaque langue. Enfin, l'emplacement est spécifié. Note sur le schéma jar: ; il indique à Firefox de d'ouvrir le fichier JAR et de lire les fichiers à leur emplacement. Si vous voulez avoir une extension avec un répertoire chrome non empaqueté, il vous suffit de modifier les chemins d'accès en quelque chose comme chrome/ content/.

Quelques options supplémentaires peuvent être inclues dans les entrées d'un fichier chrome.manifest. Elles sont documentées dans la page Enregistrement Chrome. Notamment, nous pouvons avoir des entrées qui sont spécifiques à l'OS. Ceci est important, spécialement dans Firefox 3 et plus, où l'apparence du navigateur est très différente selon le système d'exploitation. Si notre extension doit avoir une apparence différence sur d'autres systèmes, nous pourrions modifier le fichier manifest comme ceci :

content   xulschoolhello              jar:chrome/xulschoolhello.jar!/content/
skin      xulschoolhello  classic/1.0 jar:chrome/xulschoolhello.jar!/skin/unix/
skin      xulschoolhello  classic/1.0 jar:chrome/xulschoolhello.jar!/skin/mac/ os=Darwin
skin      xulschoolhello  classic/1.0 jar:chrome/xulschoolhello.jar!/skin/win/ os=WinNT
locale    xulschoolhello  en-US       jar:chrome/xulschoolhello.jar!/locale/en-US/

De cette façon, nous pouvons avoir des thèmes graphiques  distincts pour Windows, Mac OS X, et Linux (ainsi que d'autres systèmes de type Unix), définis chacun dans un répertoire distinct. Comme la plupart des autres systèmes sont basés sur Unix, le thème "unix" est la valeur par défaut, sans drapeaux.

Le Chrome

Comme mentionné précédemment, le chrome est composé de 3 sections : le contenu, le langage et le thème. Le contenu est la section la plus importante, contenant les fichiers de l'interface utilisateur (XUL) et du script (JS). La section du thème contient les fichiers définissant principalement l'apparence graphique de l'interface utilisateur (en utilisant le CSS et les images, tout comme les pages Web). Enfin, la section du langage regroupe tous les textes utilisés dans l'extension, dans des fichiers DTD et properties. Cette division permet aux développeurs de créer d'autres thèmes graphiques, et aux traducteurs de créer des versions dans des langues différentes, tout cela sans avoir à modifier votre extension ou votre code. Cela donne aux extensions Firefox beaucoup de flexibilité.

Les fichiers Chrome sont accessibles via le protocole chrome. Les URIs chrome sont définis comme suit 

chrome://packagename/section/path/to/file

Ainsi, par exemple, si je veux accéder au fichier browserOverlay.xul dans l'extension, l'URI chrome serait chrome://xulschoolhello/content/browserOverlay.xul. Si vous avez trop de fichiers dans le contenu et vous souhaitez les organiser en sous-répertoires, il n'y a rien à changer de chrome.manifest, il vous suffit d'ajouter le chemin correspondant après content dans l'URI. Les fichiers des thèmes ou de langages fonctionnent de la même manière, et vous n'avez pas besoin de spécifier leurs noms exacts. Ainsi, pour accéder au fichier DTD dans l'exemple Hello World, le chemin de chrome est chrome://xulschoolhello/locale/browserOverlay.dtd. Firefox sait de quel fichier de traduction il s'agit.

Voici une expérience intéressante. Ouvrez un nouvel onglet Firefox, tapez chrome://mozapps/content/downloads/downloads.xul dans votre barre d'adresse et appuyez sur ENTRER. Surpris ? Vous venez d'ouvrir la fenêtre des Téléchargements dans un onglet Firefox ! Vous pouvez accéder à un fichier chrome en tapant simplement son URI dans la barre d'adresse. Cela peut être utile si vous voulez inspecter les fichiers de script qui font partie de Firefox, d'autres extensions, ou de la votre. La plupart de ces fichiers sont ouverts sous forme de fichiers texte, à l'exception des fichiers XUL qui sont exécutés et affichés comme vous les verriez normalement sur une fenêtre.

Content

Il y a 2 fichiers dans le répertoire content. Regardons le premier fichier XUL.

Les fichiers XUL sont des fichiers XML qui définissent les éléments de l'interface utilisateur dans Firefox et les extensions Firefox. XUL a été inspiré par le HTML, donc vous verrez beaucoup de similitudes entre les deux. Cependant, XUL est aussi une amélioration par rapport à HTML, ayant appris de bon nombre des erreurs commises durant l'évolution de HTML. XUL permet de créer des interfaces plus riches et plus interactives que celles que vous pouvez créer avec le langage HTML, ou tout au moins XUL les rend plus facile.

Les fichiers XUL définissent généralement l'une des deux choses suivantes : les fenêtres ou les superpositions (overlay). Le fichier que vous avez ouvert précédemment, downloads.xul, a un code qui définit la fenêtre de téléchargements. Le fichier XUL inclus dans l'extension Hello World est une superposition. Une superposition étend une fenêtre existante, en ajoutant de nouveaux éléments ou en remplaçant certains des éléments qu'elle contient. La ligne que nous avons sauté dans les lignes du fichier chrome.manifest décrit que ce fichier XUL est une superposition de la fenêtre principale du navigateur :

overlay chrome://browser/content/browser.xul  chrome://xulschoolhello/content/browserOverlay.xul

With this line, Firefox knows that it needs to take the contents of browserOverlay.xul and overlay it on the main browser window, browser.xul. You can declare overlays for any window or dialog in Firefox, but overlaying the main browser window is the most common case by far.

Now let's look at the contents of our XUL file. We'll skip the first few lines because they relate to skin and locale, which we'll cover later.

<overlay id="xulschoolhello-browser-overlay"
  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

The root element in the file is an overlay. Other XUL documents use the window or dialog tag. The element has a unique id, which you should have on most elements in your XUL. The second attribute is the namespace, which is something you should always define in your XUL root element. It says that this node and all child nodes are XUL. You only need to change namespace declarations when you combine different types of content, such as XUL with HTML or SVG.

You may have noticed the naming we use on several places, such as the id xulschoolhello-browser-overlay. This is the namespacing standard that we use to avoid conflicts with Firefox and other extensions, as well as making some development tasks easier. We namespace all ids and style classes in overlay elements because they will be mixed with other elements in the main browser window. If we used generic ids like container or input, they will likely conflict with ids used within Firefox, or with ids from other extension overlays. Using namespaces minimizes compatibility problems with other extensions. We use camel casing for file names, and all lower case with dashes for element ids and CSS style class names.
<script type="application/x-javascript"
  src="chrome://xulschoolhello/content/browserOverlay.js" />

Just like in HTML, this includes a JavaScript script file. You can have as many script elements on a XUL file as you need. We'll look into its code later.

You also probably noticed how we format our code, and wonder about the rules we follow. Our general rule on line length is not having lines longer than 80 characters. This feels very restrictive, specially with XML files, but the number was chosen to allow pretty much any text editor to handle these files easily. Even old command line editors work well with files that cut their lines at 80 characters. The tabulation is very straightforward: 2 blank space indents. We never use actual tab characters, with the exception of Makefiles, which will be covered later on. Most of our coding standards are based on Mozilla's or other known and used standards.

 We'll skip some code that is covered in the locale section, moving on to the most important part of the content:

<menubar id="main-menubar">
  <menu id="xulschoolhello-hello-menu" label="&xulschoolhello.hello.label;"
    accesskey="&xulschoolhello.helloMenu.accesskey;" insertafter="helpMenu">
    <menupopup>
      <menuitem id="xulschoolhello-hello-menu-item"
        label="&xulschoolhello.hello.label;"
        accesskey="&xulschoolhello.helloItem.accesskey;"
        oncommand="XULSchoolChrome.BrowserOverlay.sayHello(event);" />
    </menupopup>
  </menu>
</menubar>

This is the code that adds the Hello World menu to the browser window. In order to write this code, we needed some knowledge of the XUL code in browser.xul. We needed to know that the id of the main menu is main-menubar. We're adding a menu of our own, and telling Firefox to add it in the main menu bar, right after the Help menu. That's the purpose of the attribute:

insertafter="helpMenu"

helpMenu is the id of the menu element that corresponds to the Help menu in the main menu of the browser. We'll see later how we can find out things like the ids of browser elements, but for now let's look at the elements that compose the Hello World menu.

The menubar element represents the menu bar you normally see at the top of an application window. The main Firefox window has one, but few of its other windows do. It's also rare for additional extension windows to have their own menu bars.

We added the Hello World menu right in the "root" of the menu bar so that it would be very easy for you to spot it, but this is not a recommended practice. Imagine if all extensions added menus to the top menu bar; having a few extensions would make the menu look like an airplane dashboard, full of options. The recommended location for extension menus is under the Tools menu, so the code should really look like this:

<menupopup id="menu_ToolsPopup">
  <menu id="xulschoolhello-hello-menu" label="&xulschoolhello.hello.label;"
    accesskey="&xulschoolhello.helloMenu.accesskey;"
    insertafter="javascriptConsole,devToolsSeparator">
    <menupopup>
      <menuitem id="xulschoolhello-hello-menu-item"
  label="&xulschoolhello.hello.label;"         accesskey="&xulschoolhello.helloItem.accesskey;"         oncommand="XULSchoolChrome.BrowserOverlay.sayHello(event);" />     </menupopup>   </menu> </menupopup>

We're overlaying a menu that is deeper into the XUL tree, but it doesn't matter because all we need is the id of the element we want to overlay. In this case it is the menupopup element that's inside of the Tools menu element. The insertafter attribute tells Firefox to add the menu below the Error Console item (formerly known as JavaScript Console) in the Tools menu, just like recommended in the Extension Etiquette page. We'll discuss more about menus later on in the tutorial. For now let's focus on the following line:

oncommand="XULSchoolChrome.BrowserOverlay.sayHello(event);"

This attribute defines an event handler. The command event is the most frequently used in Firefox, since it corresponds to the main action for most UI elements. The value of the attribute is JavaScript code that invokes a function. This function is defined in the JS file that was included with the script tag. The JS function will be called once the user clicks on the menu item in the Hello World menu. All event handlers define a special object named event, which is usually good to pass as an argument to the function. Event handlers are explained in grater depth further ahead.

Now let's look at the JavaScript file and see what's going on when the event is fired.

/**
 * XULSchoolChrome namespace.
 */
if ("undefined" == typeof(XULSchoolChrome)) {
  var XULSchoolChrome = {};
};

The XULSchoolChrome namespace is defined. All objects and variables we define in this JavaScript are global, meaning that scripts in Firefox and other extensions can see them and interact with them. This also means that if we define an object called MenuHandler or some other generic name, it's likely going to conflict with an existing object. What we do here is define a single global object: XULSchoolChrome. Now we know that all of our objects are inside this object, which is unlikely to be duplicated or overwritten by other extensions.

You can read more about the typeof operator. If you're unfamiliar with JavaScript or this particular syntax, initializing an object as {} is the equivalent of initializing it to new Object().

/**
 * Controls the browser overlay for the Hello World extension.
 */
XULSchoolChrome.BrowserOverlay = {

Finally, BrowserOverlay is our object. Naming and referencing  objects in such a long and verbose manner can feel uncomfortable at first, but it's worth the cost.

We use Javadoc style comments on all namespaces, objects and object members. This is a similar standard to the one used in Mozilla code, and some tools can generate documentation automatically from Javadoc.
sayHello : function(aEvent) {
  let stringBundle = document.getElementById("xulschoolhello-string-bundle");
  let message = stringBundle.getString("xulschoolhello.greeting.label");

  window.alert(message);
}

And, finally, this is our function declaration. Three lines of code are all we need for it to work. The first line in the body of the function declares a variable that will hold the stringbundle element defined in the overlay. The variable is declared using let, which is similar to var but with more restricted scope. Here you can read more about let declarations. It's worth noting that this is a relatively new addition to JavaScript in Firefox and you should use var if you're creating an extension compatible with very old versions.

Just like in regular JS, we can use the DOM (Document Object Model) in order to manipulate the XUL document. First we get a reference of the stringbundle element in the document. This is a special element that allows us to obtain localized strings dynamically, by only providing a "key" that identifies the string. This is what we do on the second line. We call the getString method of the bundle element and get the localized message to be displayed. We then call the window.alert function with the message, just like we would do in an HTML document.

Locale

There are two types of locale files: DTD and properties, and in this example we use them both. DTD is the most efficient way of showing text in XUL, so you should use it whenever possible. It is somewhat inflexible so it can't be used for dynamically generated text, hence the need for an alternate way of getting localized strings.

Looking back at the menu code, you probably noticed some attributes such as this:

label="&xulschoolhello.hello.label;" accesskey="&xulschoolhello.helloItem.accesskey;"

These attributes define the text that you see on the menus, and they are string keys that are defined in our DTD file, browserOverlay.dtd. The DTD file was included in the XUL file with the following code:

<!DOCTYPE overlay SYSTEM "chrome://xulschoolhello/locale/browserOverlay.dtd" >

And in the DTD file you can see the association between keys and localized strings:

<!ENTITY xulschoolhello.hello.label            "Hello World!">
<!ENTITY xulschoolhello.helloMenu.accesskey    "l">
<!ENTITY xulschoolhello.helloItem.accesskey    "H">

Notice that on the XUL file you enclose the string key with & and ; while on the DTD file you only specify the key. You may get weird parsing errors or incorrect localization if you don't get it right.

Access keys are the shortcuts that allow you to quickly navigate a menu using only the keyboard. They are also the only way to navigate a menu for people with accessibility problems, such as partial or total blindness, or physical disabilities that make using a mouse very difficult or impossible. You can easily recognize the access keys on Windows because the letter that corresponds to the access key is underlined, as in the following image:

Most user interface controls have the accesskey attribute, and you should use it. The value of the access key is localized because it should match a letter in the label text. You should also be careful to avoid access key repetition. For example, within a menu or submenu, access keys should not be repeated. In a window you have to be more careful picking access keys because there are usually more controls there. You have to be specially careful when picking access keys on an overlay. In our case, we can't use the letter "H" as an accesskey in the Main menu item, because it would be the same as the access key in the Help menu. Same goes with "W" and the Window menu on Mac OS. So we settled on the letter "l".

DTD strings are resolved and set when the document is being loaded. If you request the label attribute value for the Hello World menu using DOM, you get the localized string, not the string key. You cannot dynamically change an attribute value with a new DTD key, you have to set the new value directly:

let helloItem = document.getElementById("xulschoolhello-hello-menu-item");

// The alert will say "Hello World!"
alert(helloItem.getAttribute("label")); // Wrong
helloItem.setAttribute("label", "&xulschoolhello.hello2.label;"); // Better
helloItem.setAttribute("label", "Alternate message"); // Right!
helloItem.setAttribute("label", someStringBundle.getString("xulschoolhello.hello2.label"));

This is the reason DTD strings are not a solution for all localization cases, and the reason we often need to include string bundles in XUL files:

<stringbundleset id="stringbundleset">
  <stringbundle id="xulschoolhello-string-bundle"
    src="chrome://xulschoolhello/locale/browserOverlay.properties" />
</stringbundleset>

The stringbundleset element is just a container for stringbundle elements. There should only be one per document, which is the reason why we overlay the stringbundleset that is in browser.xul, hence the very generic id. We don't include the insertbefore or insertafter attributes because the ordering of string bundles doesn't make a difference. The element is completely invisible. If you don't include any of those ordering attributes in an overlay element, Firefox will just append your element as the last child of the parent element.

All you need for the string bundle is an id (to be able to fetch the element later) and the chrome path to the properties file. And, of course, you need the properties file:

xulshoolhello.greeting.label = Hi! How are you?

The whitespace around the equals sign is ignored. Just like in install.rdf, comments can be added using the # character at the beginning of the line. Empty lines are ignored as well.

You will often want to include dynamic content as part of localized strings, like when you want to inform the user about some stat related to the extension. For example: "Found 5 words matching the search query". Your first idea would probably be to simply concatenate strings, and have one "Found" property and another "words matching..." property. This is not a good idea. It greatly complicates the work of localizers, and grammar rules on different languages may change the ordering of the sentence entirely. For this reason it's better to use parameters in the properties:

xulshoolhello.search.label = Found %S words matching the search query!

Then you use getFormattedString instead of getString in order to get the localized string. Thanks to this we don't need to have multiple properties, and life is easier for translators. You can read more about it on the Text Formatting section of the XUL Tutorial. Also have a look at the Plurals and Localization article, that covers a new localization feature in Firefox 3 that allows you to further refine this last example to handle different types of plural forms that are also language-dependent.

Skin

Styling XUL is very similar to styling HTML. We'll look into some of the differences when we cover the XUL Box Model, and other more advanced topics. There isn't much styling you can do to a minimal menu and a very simple alert message, so the Hello World extension only includes an empty CSS file and the compulsory global skin file:

<?xml-stylesheet type="text/css" href="chrome://global/skin/"  ?>
<?xml-stylesheet type="text/css" href="chrome://xulschoolhello/skin/browserOverlay.css" ?>

The global skin CSS file holds the default styles for all XUL elements and windows. Forgetting to include this file in a XUL window usually leads to interesting and often unwanted results. In our case we don't really need to include it, since we're overlaying the main browser XUL file, and that file already includes this global CSS. At any rate it's better to always include it. This way it's harder to make the mistake of not including it. You can enter the chrome path in the location bar and inspect the file if you're curious.

This covers all of the files in the Hello World extension. Now you should have an idea of the basics involved in extension development, so now we'll jump right in and set up a development environment. But first, a little exercise.

Exercise

Change the welcome message that is displayed in the alert window and move the Hello World menu to the Tools Menu, where it belongs. Repackage the XPI and re-install it. You can just drag the XPI file to the browser and it will be installed locally. Test it and verify your changes worked. If you run into problems at installation, it's likely that you didn't reproduce the XPI structure correctly, maybe adding unnecessary folders.

Once you're done, you can look at this reference solution: Hello World 2.

This tutorial was kindly donated to Mozilla by Appcoast.

Pièces jointes

Fichier Taille Date Joint par
accesskeys.png
1639 octets 2010-03-25 19:13:55 Jorge.villalobos
Simple example (PNG)
Expected rendering for the simple example
3463 octets 2012-11-07 10:43:44 Jeremie

Étiquettes et contributeurs liés au document

Contributeurs ayant participé à cette page : Chbok
Dernière mise à jour par : Chbok,