Files
meteor/packages/html-tools/parse_tests.js
2014-01-27 14:44:19 -08:00

354 lines
12 KiB
JavaScript

var Scanner = HTML._$.Scanner;
var getContent = HTML._$.getContent;
var CharRef = HTML.CharRef;
var Comment = HTML.Comment;
var Special = HTML.Special;
var BR = HTML.BR;
var HR = HTML.HR;
var INPUT = HTML.INPUT;
var A = HTML.A;
var DIV = HTML.DIV;
var P = HTML.P;
var TEXTAREA = HTML.TEXTAREA;
Tinytest.add("html-tools - parser getContent", function (test) {
var succeed = function (input, expected) {
var endPos = input.indexOf('^^^');
if (endPos < 0)
endPos = input.length;
var scanner = new Scanner(input.replace('^^^', ''));
var result = getContent(scanner);
test.equal(scanner.pos, endPos);
test.equal(HTML.toJS(result), HTML.toJS(expected));
};
var fatal = function (input, messageContains) {
var scanner = new Scanner(input);
var error;
try {
getContent(scanner);
} catch (e) {
error = e;
}
test.isTrue(error);
if (messageContains)
test.isTrue(messageContains && error.message.indexOf(messageContains) >= 0, error.message);
};
succeed('', null);
succeed('^^^</', null);
succeed('abc', 'abc');
succeed('abc^^^</x>', 'abc');
succeed('a&lt;b', ['a', CharRef({html: '&lt;', str: '<'}), 'b']);
succeed('<!-- x -->', Comment(' x '));
succeed('&acE;', CharRef({html: '&acE;', str: '\u223e\u0333'}));
succeed('&zopf;', CharRef({html: '&zopf;', str: '\ud835\udd6b'}));
succeed('&&>&g&gt;;', ['&&>&g', CharRef({html: '&gt;', str: '>'}), ';']);
// Can't have an unescaped `&` if followed by certain names like `gt`
fatal('&gt&');
// tests for other failure cases
fatal('<');
succeed('<br>', BR());
succeed('<br/>', BR());
fatal('<div/>', 'self-close');
succeed('<hr id=foo>', HR({id:'foo'}));
succeed('<hr id=&lt;foo&gt;>', HR({id:[CharRef({html:'&lt;', str:'<'}),
'foo',
CharRef({html:'&gt;', str:'>'})]}));
succeed('<input selected>', INPUT({selected: ''}));
succeed('<input selected/>', INPUT({selected: ''}));
succeed('<input selected />', INPUT({selected: ''}));
var FOO = HTML.getTag('FOO');
succeed('<foo bar></foo>', FOO({bar: ''}));
succeed('<foo bar baz ></foo>', FOO({bar: '', baz: ''}));
succeed('<foo bar=x baz qux=y blah ></foo>',
FOO({bar: 'x', baz: '', qux: 'y', blah: ''}));
succeed('<foo bar="x" baz qux="y" blah ></foo>',
FOO({bar: 'x', baz: '', qux: 'y', blah: ''}));
fatal('<input bar"baz">');
fatal('<input x="y"z >');
fatal('<input x=\'y\'z >');
succeed('<br x=&&&>', BR({x: '&&&'}));
succeed('<br><br><br>', [BR(), BR(), BR()]);
succeed('aaa<br>\nbbb<br>\nccc<br>', ['aaa', BR(), '\nbbb', BR(), '\nccc', BR()]);
succeed('<a></a>', A());
fatal('<');
fatal('<a');
fatal('<a>');
fatal('<a><');
fatal('<a></');
fatal('<a></a');
succeed('<a href="http://www.apple.com/">Apple</a>',
A({href: "http://www.apple.com/"}, 'Apple'));
(function () {
var A = HTML.getTag('A');
var B = HTML.getTag('B');
var C = HTML.getTag('C');
var D = HTML.getTag('D');
succeed('<a>1<b>2<c>3<d>4</d>5</c>6</b>7</a>8',
[A('1', B('2', C('3', D('4'), '5'), '6'), '7'), '8']);
})();
fatal('<b>hello <i>there</b> world</i>');
// XXX support implied end tags in cases allowed by the spec
fatal('<p>');
fatal('<a>Foo</a/>');
fatal('<a>Foo</a b=c>');
succeed('<textarea>asdf</textarea>', TEXTAREA("asdf"));
succeed('<textarea x=y>asdf</textarea>', TEXTAREA({x: "y"}, "asdf"));
succeed('<textarea><p></textarea>', TEXTAREA("<p>"));
succeed('<textarea>a&amp;b</textarea>',
TEXTAREA("a", CharRef({html: '&amp;', str: '&'}), "b"));
succeed('<textarea></textarea</textarea>', TEXTAREA("</textarea"));
// absorb up to one initial newline, as per HTML parsing spec
succeed('<textarea>\n</textarea>', TEXTAREA());
succeed('<textarea>\nasdf</textarea>', TEXTAREA("asdf"));
succeed('<textarea>\n\nasdf</textarea>', TEXTAREA("\nasdf"));
succeed('<textarea>\n\n</textarea>', TEXTAREA("\n"));
succeed('<textarea>\nasdf\n</textarea>', TEXTAREA("asdf\n"));
succeed('<textarea><!-- --></textarea>', TEXTAREA("<!-- -->"));
succeed('<tExTaReA>asdf</TEXTarea>', TEXTAREA("asdf"));
fatal('<textarea>asdf');
fatal('<textarea>asdf</textarea');
fatal('<textarea>&davidgreenspan;</textarea>');
succeed('<textarea>&</textarea>', TEXTAREA("&"));
succeed('<textarea></textarea \n<</textarea \n>asdf',
[TEXTAREA("</textarea \n<"), "asdf"]);
// CR/LF behavior
succeed('<br\r\n x>', BR({x:''}));
succeed('<br\r x>', BR({x:''}));
succeed('<br x="y"\r\n>', BR({x:'y'}));
succeed('<br x="y"\r>', BR({x:'y'}));
succeed('<br x="y"\r>', BR({x:'y'}));
succeed('<br x=\r\n"y">', BR({x:'y'}));
succeed('<br x=\r"y">', BR({x:'y'}));
succeed('<br x\r=\r"y">', BR({x:'y'}));
succeed('<!--\r\n-->', Comment('\n'));
succeed('<!--\r-->', Comment('\n'));
succeed('<textarea>a\r\nb\r\nc</textarea>', TEXTAREA('a\nb\nc'));
succeed('<textarea>a\rb\rc</textarea>', TEXTAREA('a\nb\nc'));
succeed('<br x="\r\n\r\n">', BR({x:'\n\n'}));
succeed('<br x="\r\r">', BR({x:'\n\n'}));
succeed('<br x=y\r>', BR({x:'y'}));
fatal('<br x=\r>');
});
Tinytest.add("html-tools - parseFragment", function (test) {
test.equal(HTML.toJS(HTML.parseFragment("<div><p id=foo>Hello</p></div>")),
HTML.toJS(DIV(P({id:'foo'}, 'Hello'))));
test.throws(function() {
HTML.parseFragment('asdf</a>');
});
(function () {
var p = HTML.parseFragment('<p></p>');
test.equal(p.tagName, 'P');
test.equal(p.attrs, null);
test.isTrue(p instanceof HTML.Tag);
test.equal(p.children.length, 0);
})();
(function () {
var p = HTML.parseFragment('<p>x</p>');
test.equal(p.tagName, 'P');
test.equal(p.attrs, null);
test.isTrue(p instanceof HTML.Tag);
test.equal(p.children.length, 1);
test.equal(p.children[0], 'x');
})();
(function () {
var p = HTML.parseFragment('<p>x&#65;</p>');
test.equal(p.tagName, 'P');
test.equal(p.attrs, null);
test.isTrue(p instanceof HTML.Tag);
test.equal(p.children.length, 2);
test.equal(p.children[0], 'x');
test.isTrue(p.children[1] instanceof HTML.CharRef);
test.equal(p.children[1].html, '&#65;');
test.equal(p.children[1].str, 'A');
})();
(function () {
var pp = HTML.parseFragment('<p>x</p><p>y</p>');
test.isTrue(pp instanceof Array);
test.equal(pp.length, 2);
test.equal(pp[0].tagName, 'P');
test.equal(pp[0].attrs, null);
test.isTrue(pp[0] instanceof HTML.Tag);
test.equal(pp[0].children.length, 1);
test.equal(pp[0].children[0], 'x');
test.equal(pp[1].tagName, 'P');
test.equal(pp[1].attrs, null);
test.isTrue(pp[1] instanceof HTML.Tag);
test.equal(pp[1].children.length, 1);
test.equal(pp[1].children[0], 'y');
})();
var scanner = new Scanner('asdf');
scanner.pos = 1;
test.equal(HTML.parseFragment(scanner), 'sdf');
test.throws(function () {
var scanner = new Scanner('asdf</p>');
scanner.pos = 1;
HTML.parseFragment(scanner);
});
});
Tinytest.add("html-tools - getSpecialTag", function (test) {
// match a simple tag consisting of `{{`, an optional `!`, one
// or more ASCII letters or spaces, and a closing `}}`.
var mustache = /^\{\{(!?[a-zA-Z 0-9]+)\}\}/;
// This implementation of `getSpecialTag` looks for "{{" and if it
// finds it, it will match the regex above or fail fatally trying.
// The object it returns is opaque to the tokenizer/parser and can
// be anything we want.
var getSpecialTag = function (scanner, position) {
if (! (scanner.peek() === '{' && // one-char peek is just an optimization
scanner.rest().slice(0, 2) === '{{'))
return null;
var match = mustache.exec(scanner.rest());
if (! match)
scanner.fatal("Bad mustache");
scanner.pos += match[0].length;
if (match[1].charAt(0) === '!')
return null; // `{{!foo}}` is like a comment
return { stuff: match[1] };
};
var succeed = function (input, expected) {
var endPos = input.indexOf('^^^');
if (endPos < 0)
endPos = input.length;
var scanner = new Scanner(input.replace('^^^', ''));
scanner.getSpecialTag = getSpecialTag;
var result;
try {
result = getContent(scanner);
} catch (e) {
result = String(e);
}
test.equal(scanner.pos, endPos);
test.equal(HTML.toJS(result), HTML.toJS(expected));
};
var fatal = function (input, messageContains) {
var scanner = new Scanner(input);
scanner.getSpecialTag = getSpecialTag;
var error;
try {
getContent(scanner);
} catch (e) {
error = e;
}
test.isTrue(error);
if (messageContains)
test.isTrue(messageContains && error.message.indexOf(messageContains) >= 0, error.message);
};
succeed('{{foo}}', Special({stuff: 'foo'}));
succeed('<a href=http://www.apple.com/>{{foo}}</a>',
A({href: "http://www.apple.com/"}, Special({stuff: 'foo'})));
// tags not parsed in comments
succeed('<!--{{foo}}-->', Comment("{{foo}}"));
succeed('<!--{{foo-->', Comment("{{foo"));
succeed('&am{{foo}}p;', ['&am', Special({stuff: 'foo'}), 'p;']);
// can't start a mustache and not finish it
fatal('{{foo');
fatal('<a>{{</a>');
// no mustache allowed in tag name
fatal('<{{a}}>');
fatal('<{{a}}b>');
fatal('<a{{b}}>');
// single curly brace is no biggie
succeed('a{b', 'a{b');
succeed('<br x={ />', BR({x:'{'}));
succeed('<br x={foo} />', BR({x:'{foo}'}));
succeed('<br {{x}}>', BR({$specials: [Special({stuff: 'x'})]}));
succeed('<br {{x}} {{y}}>', BR({$specials: [Special({stuff: 'x'}),
Special({stuff: 'y'})]}));
succeed('<br {{x}} y>', BR({$specials: [Special({stuff: 'x'})], y:''}));
fatal('<br {{x}}y>');
fatal('<br {{x}}=y>');
succeed('<br x={{y}} z>', BR({x: Special({stuff: 'y'}), z: ''}));
succeed('<br x=y{{z}}w>', BR({x: ['y', Special({stuff: 'z'}), 'w']}));
succeed('<br x="y{{z}}w">', BR({x: ['y', Special({stuff: 'z'}), 'w']}));
succeed('<br x="y {{z}}{{w}} v">', BR({x: ['y ', Special({stuff: 'z'}),
Special({stuff: 'w'}), ' v']}));
// Slash is parsed as part of unquoted attribute! This is consistent with
// the HTML tokenization spec. It seems odd for some inputs but is probably
// for cases like `<a href=http://foo.com/>` or `<a href=/foo/>`.
succeed('<br x={{y}}/>', BR({x: [Special({stuff: 'y'}), '/']}));
succeed('<br x={{z}}{{w}}>', BR({x: [Special({stuff: 'z'}),
Special({stuff: 'w'})]}));
fatal('<br x="y"{{z}}>');
succeed('<br x=&amp;>', BR({x:CharRef({html: '&amp;', str: '&'})}));
// check tokenization of stache tags with spaces
succeed('<br {{x 1}}>', BR({$specials: [Special({stuff: 'x 1'})]}));
succeed('<br {{x 1}} {{y 2}}>', BR({$specials: [Special({stuff: 'x 1'}),
Special({stuff: 'y 2'})]}));
succeed('<br {{x 1}} y>', BR({$specials: [Special({stuff: 'x 1'})], y:''}));
fatal('<br {{x 1}}y>');
fatal('<br {{x 1}}=y>');
succeed('<br x={{y 2}} z>', BR({x: Special({stuff: 'y 2'}), z: ''}));
succeed('<br x=y{{z 3}}w>', BR({x: ['y', Special({stuff: 'z 3'}), 'w']}));
succeed('<br x="y{{z 3}}w">', BR({x: ['y', Special({stuff: 'z 3'}), 'w']}));
succeed('<br x="y {{z 3}}{{w 4}} v">', BR({x: ['y ', Special({stuff: 'z 3'}),
Special({stuff: 'w 4'}), ' v']}));
succeed('<br x={{y 2}}/>', BR({x: [Special({stuff: 'y 2'}), '/']}));
succeed('<br x={{z 3}}{{w 4}}>', BR({x: [Special({stuff: 'z 3'}),
Special({stuff: 'w 4'})]}));
succeed('<p></p>', P());
succeed('x{{foo}}{{bar}}y', ['x', Special({stuff: 'foo'}),
Special({stuff: 'bar'}), 'y']);
succeed('x{{!foo}}{{!bar}}y', 'xy');
succeed('x{{!foo}}{{bar}}y', ['x', Special({stuff: 'bar'}), 'y']);
succeed('x{{foo}}{{!bar}}y', ['x', Special({stuff: 'foo'}), 'y']);
succeed('', null);
succeed('{{!foo}}', null);
});