ANTLR
ANTLR est une bibliothèque qui permet de générer du code d’analyse de documents texte à partir d’une grammaire. Jusqu’à la version 3, le code était généré uniquement pour le langage Java. Depuis la version 4, d’autres cibles sont disponibles : JavaScript, C#, Python et bientôt C++.
Dans cet article, je vais montrer comment mettre en oeuvre un parser avec JavaScript.
Le code source est disponible sur GitHub
Création du projet
ANTLR est une API Java. J’utilise donc un IDE Java pour créer mon projet. Je choisi par ailleurs d’utiliser Gradle pour la construction du projet.
Voici la structure du projet Gradle généré par mon IDE :
Je modifie ensuite le fichier de construction 'build.gradle'. L’élément important dans ce script est l’argument '-Dlanguage=JavaScript'.
plugins {
id 'antlr'
}
repositories {
mavenCentral()
}
group 'icodem'
version '1.0-SNAPSHOT'
description = 'CSV ANTLR4-based parser'
dependencies {
antlr "org.antlr:antlr4:4.5.1"
}
generateGrammarSource {
group "${project.name}"
description 'Generates the parser code based on the grammar'
arguments += ["-visitor", "-Dlanguage=JavaScript"]
copy {
from 'build/generated-src/antlr/main'
into 'src/main/resources'
include '/CSV*.js'
include '/CSV*.tokens'
}
}
clean {
delete fileTree('src/main/resources') {
include '/CSV*.js'
include '/CSV*.tokens'
}
}
wrapper {
gradleVersion = '2.5'
}
La grammaire
Je prends une grammaire dans le repo GitHub de ANTLR et je place le fichier correspondant dans le répertoire 'src/main/antlr' :
grammar CSV;
document : hdr row+ EOF;
hdr : row ;
row : field (',' field)* '\r'? '\n' ;
field : TEXT | STRING | ;
TEXT : ~[,\n\r"]+ ;
STRING : '"' ('""'|~'"')* '"' ;
On peut donc maintenant générer les fichiers JavaScript pour notre analyseur via la commande Gradle 'generateGrammarSource'. Les fichiers générés sont copiés dans le répertoire 'src/main/resources'.
Installer le runtime ANTLR
Le runtime ANTLR est l’ensemble des fichiers JavaScript qui permettent l’exécution de notre analyseur JavaScript constitué des fichiers générés précedemment. Il est téléchargeable depuis le site de ANTLR. Il faut prendre la version de runtime qui correspondant à la version utilisée pour la génération, i.e. dans notre exemple antlr-javascript-runtime-4.5.1.zip.
On place ensuite le runtime dans notre projet dans le répertoire 'src/main/resources'.
Il faut aussi ajouter le fichier 'require.js' que je place pour ma part dans le sous-répertoire 'lib'. A noter que ce script ne fait pas partie du runtime ANTLR. Il faut donc le télécharger depuis le site GitHub de ANTLR.
Exécution dans un navigateur
L’exécution est effectué dans une page HTML. Les points importants :
-
Charger le script 'require.js' avec la balise '<script>'
-
Charger le runtime ANTLR avec 'require()'
-
Charger les fichiers de scripts de notre analyseur avec 'require()'
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Parsing CSV avec ANTLR 4</title>
<script src="lib/require.js"></script>
</head>
<body>
<p id="display"/>
</body>
<script>
// chargement du runtime ANTLR
var antlr4 = require('antlr4/index');
// chargement de l'analyseur CSV
var CSVLexer = require('./CSVLexer');
var CSVParser = require('./CSVParser');
// chaîne à analyser
var input = "Nom,Prénom,Age\nLampion,Émile,29\nLarosière,Jean,48\n";
// préparation des objets pour l'analyse
var chars = new antlr4.InputStream(input);
var lexer = new CSVLexer.CSVLexer(chars);
var tokens = new antlr4.CommonTokenStream(lexer);
var parser = new CSVParser.CSVParser(tokens);
parser.buildParseTrees = true;
// invocation de l'analyse
var tree = parser.document();
// affichage du résultat
var display = document.getElementById("display");
display.innerHTML = tree.toStringTree(null, parser);
</script>
</html>
Exécution dans une JVM
On peut exécuter notre analyseur JavaScript dans une JVM à l’aide du moteur de script JavaScript de la JVM. Avec Java 8, il s’agit de Nashorn.
Pour la mise en oeuvre, nous utilisons une classe Java, un script JS et une version adapté de 'require' pour Nashorn :
Le script JS fournit une fonction de parsing. Autrement dit, cette fonction invoque l’analyseur JS comme nous le faisions dans le cas d’une exécution dans une page web.
Fichier 'script.js' :
function parseCSV(input) {
// chargement du script 'require' spécifique nashorn
load('src/main/resources/lib/require-nashorn.js');
require.basePath = 'src/main/resources/';
// chargement du runtime ANTLR
var antlr4 = require('antlr4/index');
// chargement de l'analyseur CSV
var CSVLexer = require('./CSVLexer');
var CSVParser = require('./CSVParser');
// chaîne à analyser
var input = "Nom,Prénom,Age\nLampion,Émile,29\nLarosière,Jean,48\n"
// préparation des objets pour l'analyse
var chars = new antlr4.InputStream(input);
var lexer = new CSVLexer.CSVLexer(chars);
var tokens = new antlr4.CommonTokenStream(lexer);
var parser = new CSVParser.CSVParser(tokens);
parser.buildParseTrees = true;
// invocation de l'analyse
var tree = parser.document();
var result = tree.toStringTree(null, parser);
return result
}
La classe Java instancie le moteur JS Nashorn puis sollicite le précédent script.
Fichier 'Main.java' :
public class Main {
public static void main(String[] args) throws Exception {
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
Bindings globalScope = engine.getBindings(ScriptContext.GLOBAL_SCOPE);
globalScope.put("global", globalScope);
globalScope.put("window", "");// just to avoid fs.js module loading in FileStream.js
engine.eval("load('src/main/resources/script.js')");
Invocable invocable = (Invocable) engine;
String input = "Nom,Prénom,Age\nLampion,Émile,29\nLarosière,Jean,48\n";
Object result = invocable.invokeFunction("parseCSV", input);
System.out.println(result);
}
}
Le fichier 'require.js' disponible avec ANTLR n’est pas compatible pour une exécution avec Nashorn (du moins, je ne suis pas arrivé à le faire fonctionner). J’ai donc adapté le script de ANTLR à Nashorn.
Fichier 'require-nashorn.js' :
// NOTE The loadModule parameter points to the function, which prepares the
// environment for each module and runs its code. Scroll down to the end of
// the file to see the function definition.
(function(loadModule) { 'use strict';
// INFO Java objects
var Paths = Java.type("java.nio.file.Paths");
var Files = Java.type("java.nio.file.Files");
var StandardCharsets = Java.type("java.nio.charset.StandardCharsets");
var Collectors = Java.type("java.util.stream.Collectors");
// INFO Current module descriptors
// pwd[0] contains the descriptor of the currently loaded module,
// pwd[1] contains the descriptor its parent module and so on.
var pwd = Array();
// INFO Module cache
// Contains getter functions for the exports objects of all the loaded
// modules. The getter for the module 'mymod' is name '$name' to prevent
// collisions with predefined object properties (see note below).
// As long as a module has not been loaded the getter is either undefined
// or contains the module code as a function (in case the module has been
// pre-loaded in a bundle).
var cache = new Object();
// INFO Module getter
// Takes a module identifier, resolves it and gets the module code via
// Java NIO API. If this was successful the code and
// some environment variables are passed to the load function. The return
// value is the module's exports
object. If the cache already
// contains an object for the module id, this object is returned directly.
function require(identifier) {
var descriptor = resolve(identifier);
var cacheid = '$'+descriptor.id;
if (cache[cacheid]) {
return cache[cacheid];
}
var content = Files.lines(descriptor.path, StandardCharsets.UTF_8)
.collect(Collectors.joining("\n"));
loadModule(descriptor, cache, pwd, content);
return cache[cacheid];
}
// INFO Module resolver
// Takes a module identifier and resolves it to a module id and path. Both
// values are returned as a module descriptor, which can be passed to
// fetch
to load a module.
function resolve(identifier) {
var basePath = Paths.get(require.basePath);
var parentPath = basePath;
if (pwd.length > 0) {
parentPath = pwd[0].path.getParent();
}
var scriptPath = parentPath.resolve(identifier + ".js").normalize();
// build id corresponding to script
var subpath = scriptPath.subpath(basePath.getNameCount(), scriptPath.getNameCount());
var id = subpath.toString();
return {'id': id, 'path': scriptPath};
}
// INFO Exporting require to global scope
global.require = require;
})(
// INFO Module loader
// Takes the module descriptor, the global variables and the module code,
// sets up the module environment, defines the module getter in the cache
// and evaluates the module code.
function (module) {
var exports = new Object();
Object.defineProperty(module, 'exports', {'get':function(){return exports;},'set':function(e){exports=e;}});
arguments[2].unshift(module);
Object.defineProperty(arguments[1], '$'+module.id, {'get':function(){return exports;}});
try {
eval(arguments[3]);
} catch (e) {
print("* eval ERROR " + module.path + " : " + e.message);
}
arguments[2].shift();
}
);