Nicolas Grisey Demengel, developer @hopwork
Mocha, chai, jsdom, et voilà !
Une vue
var View = require('ampersand-view');
var templates = require('./templates');
module.exports = View.extend({
template: templates.myView,
events: {
'click .foo': 'doSomething'
},
doSomething: function () {
this.trigger('some-custom-event');
}
});
Un test
describe('MyView', function () {
it('should xxx when yyy', withDocument(function () {
// given
var MyView = require('../src/my-view');
var view = new MyView({ /* some args */ }).render();
var listener = sinon.spy();
view.on('some-custom-event', listener);
// when
$('.foo').click();
// then
listener.should.have.been.calledWith(view);
}));
});
<c:if test="${1 == 2}">ben non</c:if>
<c:forEach items="${things}" var="thing">
${thing}
<c:out value="${thing}"/>
</c:forEach>
<spring:message code="my.message.key"/>
<form:input type="number" path="description"/>
<jsp:include page="other.jsp">
<jsp:param name="foo" value="bar"/>
</jsp:include>
Lodash's _.template()
// using the "interpolate" delimiter to create a compiled template
var compiled = _.template('hello <%= user %>!');
compiled({ 'user': 'fred' });
// → 'hello fred!'
// using the HTML "escape" delimiter to escape data property values
var compiled = _.template('<b><%- value %></b>');
compiled({ 'value': '<script>' });
// → '<b><script></b>'
// using the "evaluate" delimiter to execute JavaScript and generate HTML
var compiled = _.template('<% _.forEach(users, function(user) { %><li><%- user %></li><% }); %>');
compiled({ 'users': ['fred', 'barney'] });
// → '<li>fred</li><li>barney</li>'
// using the ES delimiter as an alternative to the default "interpolate" delimiter
var compiled = _.template('hello ${ user }!');
compiled({ 'user': 'pebbles' });
// → 'hello pebbles!'
// using custom template delimiters
_.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
var compiled = _.template('hello {{ user }}!');
compiled({ 'user': 'mustache' });
// → 'hello mustache!'
<% things.forEach(function(thing) { %>
<li><%- thing %></li>
<li>${thing}</li>
<% }); %>
<c:forEach items="${things}" var="thing">
<li><c:out value="${thing}"/></li>
<li>${thing}</li>
</c:forEach>
Comment traduire une JSP en template lodash ?
Mais bien sûr...
des Regex(p) !
Allez, juste une heure de coding, pour rire !
Bon OK, 4 petites heures plus tard...
LE test :-° (1/3)
describe('Jsp Translator', function () {
it('should translate a big soup of JSP tags', function () {
var attributes = {bar: 'toto'...};
var jspPath = path.resolve(__dirname, 'main.jsp');
var expectedHtml = readAsString(__dirname, 'expected.html'));
expect(jsp.resolveFile(jspPath, attributes)).to.equal(expectedHtml);
});
});
LE test :-° (2/3)
<%@ page contentType="text/html" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%-- some comment --%>
<c:choose>
<c:when test="${myVar != 'someVal'}">
<p>should not be shown</p>
</c:when>
<c:when test="${myVar2 == 'someOtherVal'}">
<p>should be shown 1</p>
</c:when>
<c:otherwise>
<p>should also not be shown</p>
</c:otherwise>
</c:choose>
<c:choose>
<c:when test="${myVar == 'someVal'}">
<p>should be shown 2</p>
</c:when>
<c:when test="${myVar != 'someVal'}">
<p>should not be shown</p>
</c:when>
<c:otherwise>
<p>should also not be shown</p>
</c:otherwise>
</c:choose>
<c:choose>
<c:when test="${false}">
<p>should not be shown</p>
</c:when>
<c:otherwise>
<p>should be shown 3</p>
</c:otherwise>
</c:choose>
<form:form modelAttribute="formModel">
<c:forEach items="${things}" var="thing">
<form:input type="number" path="formModelMember" data-plop="${thing.name}"/>
<c:if test="${myVar == 'someVal'}">
<input type="date" value="${bar}">
</c:if>
<c:if test="${myVar2 != 'someVal'}">
<input type="number" value="42">
</c:if>
</c:forEach>
<c:out value="notavar"/>
<c:out value="${avar}"/>
<un:supported jsp="tag"></un:supported>
<a title="<spring:message code="message.code" javascriptEscape="true"/>" href="#">Link</a>
<!-- @include sibling1: -->
<%@ include file="sibling1.jsp" %>
<c:set var="anewvar" value="${anoldvar}"/>
<p>anewvar: ${anewvar}</p>
<c:set var="multilineSet">
foo
bar
</c:set>
<p>multilineSet: ${multilineSet}</p>
<c:set var="escapedSet">
<c:out value="${avar}"/>
</c:set>
<p>escapedSet: ${escapedSet}</p>
<!-- jsp:include sibling1: -->
<jsp:include page="sibling1.jsp"/>
<c:if test="${empty emptyVar and empty emptyVar2}">
<p>I support "empty"!</p>
</c:if>
<c:if test="${not empty nonEmptyVar}">
<p>I support "not empty"!</p>
</c:if>
<form:radiobutton path="${radioPath}" value="1" id="${radioPath}1"/>
<c:forEach begin="${one}" end="3" var="idx">
<span>${idx}</span>
</c:forEach>
</form:form>
LE test :-° (3/3)
<p>should be shown 1</p>
<p>should be shown 2</p>
<p>should be shown 3</p>
<form modelAttribute="formModel">
<input type="number" value="form model member value" id="formModelMember" name="formModelMember" data-plop="name 1"/>
<input type="date" value="toto">
<input type="number" value="42">
<input type="number" value="form model member value" id="formModelMember" name="formModelMember" data-plop="name 2"/>
<input type="date" value="toto">
<input type="number" value="42">
notavar
A VALUE TO &SCAPE
<!-- unsupported JSP tag removed: <un:supported jsp="tag"> --><!-- unsupported JSP tag removed: </un:supported> -->
<a title="i18n_message.code" href="#">Link</a>
<!-- @include sibling1: -->
<p>Text from sibling.jsp with some toto</p>
<dl>
<dt>Param 1</dt><dd>static</dd>
<dt>Param 2</dt><dd>dynamic, resolved</dd>
</dl>
<p>anewvar: anoldvalue</p>
<p>multilineSet: foo
bar</p>
<p>escapedSet: A VALUE TO &SCAPE</p>
<!-- jsp:include sibling1: -->
<p>Text from sibling.jsp with some toto</p>
<dl>
<dt>Param 1</dt><dd></dd>
<dt>Param 2</dt><dd></dd>
</dl>
<p>I support "empty"!</p>
<p>I support "not empty"!</p>
<input type="radio" name="someRadioButtonPath" value="1" id="someRadioButtonPath1"/>
<span>1</span>
<span>2</span>
<span>3</span>
</form>
Le résultat (~300 lignes)
'use strict';
var fs = require('fs');
var path = require('path');
var _ = require('lodash');
function unwrapElExpression(value) {
return value.replace(/^\$\{([^}]*?)\}$/, '$1');
}
function resolve(jsp, attributes, context) {
context = context || {jspDir: __dirname};
var ATTRIBUTE = /(?:\s+?([\w-]+?=".*?"))/g;
var EL_EXPRESSION = /\$\{([^}]*?)\}/g;
var FORM_INPUT = /(<form:\w+\s+.*?\b)path=".*?"(.*?\/?>)/g;
var FORM = /(<\/?)form:/g;
var INCLUDE_DIRECTIVE = /<%@\s*include\s+file="(.*?)"\s*%>/g;
var JSP_DIRECTIVE_OR_COMMENT = /<%(@|--).*?(--)?%>/g;
var JSP_TAG = /<\/?(\w+:\w+)(\s+[^>]*?)?\s*\/?>/g;
function readAttributes(str) {
var attrs = {};
var res;
while ((res = ATTRIBUTE.exec(str)) !== null) {
var kv = res[1].split('=');
var key = kv[0];
var value = kv.slice(1).join('=');
if (value) {
value = value.replace(/^"(.*?)"$/, '$1');
attrs[key] = replaceElOperators(value);
} else {
attrs[key] = null;
}
}
return attrs;
}
function replaceElOperators(jsp) {
var res;
var buf = [];
var idx = 0;
while ((res = EL_EXPRESSION.exec(jsp)) !== null) {
buf.push(jsp.substring(idx, res.index));
buf.push('${', res[1].replace(/\bnot\s+empty\s+/g, '!!')
.replace(/\bempty\s+/g, '!')
.replace(/\s+and\s+/, ' && ')
.replace(/\s+or\s+/, ' || '), '}');
idx = EL_EXPRESSION.lastIndex;
}
buf.push(jsp.substring(idx));
return buf.join('');
}
function replaceFormInputs(jsp) {
var res;
var buf = [];
var idx = 0;
while ((res = FORM_INPUT.exec(jsp)) !== null) {
buf.push(jsp.substring(idx, res.index));
buf.push(res[1]);
var attrs = readAttributes(res[0]);
if (!attrs.id) {
buf.push(' id="', attrs.path, '"');
}
if (!attrs.name) {
buf.push(' name="', attrs.path, '"');
}
buf.push(res[2]);
idx = FORM_INPUT.lastIndex;
}
buf.push(jsp.substring(idx));
return buf.join('');
}
function resolveInclude(file, attrs) {
var filePath = path.resolve(context.jspDir, file);
return resolveFile(filePath, attrs);
}
function replaceJspTags(jsp) {
var res;
var buf = [];
var bufBackup;
var idx = 0;
var currentJspParams, currentJspPageToInclude, currentChoose, currentModelAttribute;
function jspInclude() {
var inclusionAttrs = _.assign({
param: currentJspParams
}, attributes);
var str = resolveInclude(currentJspPageToInclude, inclusionAttrs);
currentJspPageToInclude = null;
currentJspParams = null;
return str;
}
var STRATEGIES = {
DEFAULT: {
open: function (buf, res) {
buf.push('<!-- unsupported JSP tag removed: ', res[0], ' -->');
},
close: function (buf, res) {
buf.push('<!-- unsupported JSP tag removed: ', res[0], ' -->');
}
},
'c:choose': {
open: function () {
currentChoose = {};
},
close: function () {
buf.push('<% } %>');
}
},
'c:otherwise': {
open: function (buf) {
buf.push('<% } else { %>');
},
close: function (buf) {
}
},
'c:when': {
open: function (buf, res, attrs) {
if (currentChoose.firstWhenPassed) {
buf.push('<% } else if (', unwrapElExpression(attrs.test), ') { %>');
} else {
buf.push('<% if (', unwrapElExpression(attrs.test), ') { %>');
}
currentChoose.firstWhenPassed = true;
},
close: function (buf) {
}
},
'c:forEach': {
open: function (buf, res, attrs) {
var items;
if (attrs.items) {
items = unwrapElExpression(attrs.items);
} else {
items = '_.range(' + unwrapElExpression(attrs.begin) + ', ' + unwrapElExpression(attrs.end) + ' + 1)';
}
buf.push('<% ', items, '.forEach(function (', attrs.var, ') { %>');
},
close: function (buf) {
buf.push('<% }); %>');
}
},
'c:if': {
open: function (buf, res, attrs) {
buf.push('<% if (', unwrapElExpression(attrs.test), ') { %>');
},
close: function (buf) {
buf.push('<% } %>');
}
},
'c:set': {
open: function (buf, res, attrs) {
if (attrs.value) {
if (attrs.value.indexOf('${') === 0) {
buf.push('<% var ', attrs.var, ' = ', unwrapElExpression(attrs.value), '; %>');
} else {
buf.push('<% var ', attrs.var, ' = "', attrs.value, '"; %>');
}
} else {
buf.push('<% var ', attrs.var, ' = ');
}
},
close: function (buf, res, setContent) {
if (!setContent) {
return;
}
setContent = setContent.trim();
var match = setContent.match(/^<%-\s*(.*?)\s*%>$/);
if (match) {
buf.push('_.escape(', match[1], '); %>');
} else {
match = setContent.match(/^<%=?\s*(.*?)\s*%>$/);
if (match) {
buf.push(match[1], '; %>');
} else {
buf.push('"', setContent.replace(/\n/g, '\\n'), '"; %>');
}
}
}
},
'c:out': {
open: function (buf, res, attrs) {
if (attrs.value.indexOf('${') === 0) {
buf.push('<%- ', unwrapElExpression(attrs.value), ' %>');
} else {
buf.push(attrs.value);
}
}
},
'form:form': {
open: function (buf, res, attrs) {
if (attrs.modelAttribute) {
currentModelAttribute = attrs.modelAttribute;
}
// handled later
buf.push(res[0]);
},
close: function (buf, res) {
currentModelAttribute = null;
// handled later
buf.push(res[0]);
}
},
'form:input': {
open: function (buf, res, attrs) {
if (attrs.path) {
var path = attrs.path;
if (currentModelAttribute) {
if (path.indexOf('${') === 0) {
path = currentModelAttribute + '[' + unwrapElExpression(path) + ']';
} else {
path = currentModelAttribute + '.' + path;
}
}
var value = attrs.value;
if (value) {
buf.push(res[0]);
} else {
if (path.indexOf('${') === 0) {
value = ''; // not implemented, too complex
} else {
value = '${' + path + '}';
}
buf.push(res[0].replace(/\bpath=/, 'value="' + value + '" path='));
}
}
else {
// handled later
buf.push(res[0]);
}
},
close: function (buf, res) {
// handled later
buf.push(res[0]);
}
},
'form:radiobutton': {
open: function (buf, res, attrs) {
res[0] = res[0].replace('radiobutton', 'input type="radio"');
STRATEGIES['form:input'].open(buf, res, attrs);
},
close: function (buf, res) {
STRATEGIES['form:input'].close(buf, res);
}
},
'jsp:include': {
open: function (buf, res, attrs, selfClosing) {
currentJspPageToInclude = attrs.page;
currentJspParams = {};
if (selfClosing) {
buf.push(jspInclude());
}
},
close: function (buf) {
buf.push(jspInclude());
}
},
'jsp:param': {
open: function (buf, res, attrs) {
currentJspParams[attrs.name] = attrs.value;
}
},
'spring:message': {
open: function (buf, res, attrs) {
buf.push('i18n_' + attrs.code);
}
}
};
while ((res = JSP_TAG.exec(jsp)) !== null) {
buf.push(jsp.substring(idx, res.index));
var tag = res[0];
var tagName = res[1];
var closing = tag.indexOf('</') === 0;
if (!closing) {
var selfClosing = tag.indexOf('/>') === tag.length - 2;
var attrs = readAttributes(tag);
var strategy = STRATEGIES[tagName] || STRATEGIES.DEFAULT;
var open = strategy.open || STRATEGIES.DEFAULT.open;
open(buf, res, attrs, selfClosing);
if (tagName === 'c:set' && !selfClosing) {
bufBackup = buf;
buf = [];
}
}
else {
var strategy = STRATEGIES[tagName] || STRATEGIES.DEFAULT;
var close = strategy.close || STRATEGIES.DEFAULT.close;
if (bufBackup) {
var content = buf.join('');
buf = bufBackup;
bufBackup = null;
close(buf, res, content);
} else {
close(buf, res);
}
}
idx = JSP_TAG.lastIndex;
}
buf.push(jsp.substring(idx));
return buf.join('');
}
function replaceIncludes(jsp) {
var res;
var buf = [];
var idx = 0;
while ((res = INCLUDE_DIRECTIVE.exec(jsp)) !== null) {
buf.push(jsp.substring(idx, res.index));
buf.push(resolveInclude(res[1], attributes));
idx = INCLUDE_DIRECTIVE.lastIndex;
}
buf.push(jsp.substring(idx));
return buf.join('');
}
function convert(jsp) {
jsp = replaceElOperators(jsp);
jsp = replaceJspTags(jsp);
jsp = replaceFormInputs(jsp);
jsp = jsp.replace(FORM, '$1');
jsp = replaceIncludes(jsp);
return jsp.replace(JSP_DIRECTIVE_OR_COMMENT, '');
}
return _.template(convert(jsp))(attributes)
.replace(/\n+\s*\n+/g, '\n');
}
function resolveFile(jspFile, attributes, jspDir) {
var jsp = fs.readFileSync(jspFile).toString();
return resolve(jsp, attributes, {jspDir: jspDir || path.dirname(jspFile)});
}
module.exports = {
resolve: resolve,
resolveFile: resolveFile
};
Résumé :
function unwrapElExpression(value) {}
function resolve(jsp, attributes, context) {
context = context || {jspDir: __dirname};
var ATTRIBUTE = /(?:\s+?([\w-]+?=".*?"))/g;
var EL_EXPRESSION = /\$\{([^}]*?)\}/g;
var FORM_INPUT = /(<form:\w+\s+.*?\b)path=".*?"(.*?\/?>)/g;
var FORM = /(<\/?)form:/g;
var INCLUDE_DIRECTIVE = /<%@\s*include\s+file="(.*?)"\s*%>/g;
var JSP_DIRECTIVE_OR_COMMENT = /<%(@|--).*?(--)?%>/g;
var JSP_TAG = /<\/?(\w+:\w+)(\s+[^>]*?)?\s*\/?>/g;
function readAttributes(str) {}
function replaceElOperators(jsp) {}
function replaceFormInputs(jsp) {}
function resolveInclude(file, attrs) {}
function replaceOtherJspTags(jsp) {
/* ... */
var STRATEGIES = {
DEFAULT: {
open: function (buf, res, attrs, selfClosing) {
buf.push('<!-- unsupported JSP tag removed: ', res[0], ' -->');
},
close: function (buf, res) {
buf.push('<!-- unsupported JSP tag removed: ', res[0], ' -->');
}
},
'c:choose': {},
'c:otherwise': {},
'c:when': {},
'c:forEach': {},
'c:if': {},
'c:set': {},
'c:out': {},
'form:form': {},
'form:input': {},
'form:radiobutton': {},
'jsp:include': {},
'jsp:param': {},
'spring:message': {}
};
while ((res = JSP_TAG.exec(jsp)) !== null) {
buf.push(jsp.substring(idx, res.index));
var closing = res[0].indexOf('</') === 0;
if (!closing) {
var selfClosing = res[0].indexOf('/>') === res[0].length - 2;
var attrs = readAttributes(res[0]);
var strategy = STRATEGIES[res[1]] || STRATEGIES.DEFAULT;
var open = strategy.open || STRATEGIES.DEFAULT.open;
open(buf, res, attrs, selfClosing);
}
else {
var strategy = STRATEGIES[res[1]] || STRATEGIES.DEFAULT;
var close = strategy.close || STRATEGIES.DEFAULT.close;
close(buf, res);
}
idx = JSP_TAG.lastIndex;
}
buf.push(jsp.substring(idx));
return buf.join('');
}
function replaceIncludes(jsp) {}
function convert(jsp) {
jsp = replaceOtherJspTags(jsp);
jsp = replaceFormInputs(jsp);
jsp = jsp.replace(FORMS, '$1');
jsp = replaceIncludes(jsp);
return jsp.replace(JSP_DIRECTIVE_OR_COMMENT, '');
}
return _.template(convert(jsp))(attributes)
.replace(/\n+\s*\n+/g, '\n');
}
function resolveFile(jspFile, attributes, jspDir) {
var jsp = fs.readFileSync(jspFile).toString();
return resolve(jsp, attributes, {jspDir: jspDir || path.dirname(jspFile)});
}
Démo
(Au fait, vous en pensez quoi : bonne ou mauvaise idée ?)