Files
meteor/packages/html-tools/tokenize.js
Emily Stark ba0770a908 Merge remote-tracking branch 'origin/devel' into blaze-refactor
Conflicts:
	packages/html-tools/tokenize.js
	packages/spacebars-compiler/package.js
	packages/spacebars-compiler/spacebars-compiler.js
	packages/spacebars-tests/template_tests.html
	packages/spacebars-tests/template_tests.js
	packages/spacebars/package.js
	packages/spacebars/spacebars-runtime.js
	packages/ui/domrange.js
	packages/ui/render_tests.js
2014-06-06 14:05:44 -07:00

514 lines
16 KiB
JavaScript

// Token types:
//
// { t: 'Doctype',
// v: String (entire Doctype declaration from the source),
// name: String,
// systemId: String (optional),
// publicId: String (optional)
// }
//
// { t: 'Comment',
// v: String (not including "<!--" and "-->")
// }
//
// { t: 'Chars',
// v: String (pure text like you might pass to document.createTextNode,
// no character references)
// }
//
// { t: 'Tag',
// isEnd: Boolean (optional),
// isSelfClosing: Boolean (optional),
// n: String (tag name, in lowercase or camel case),
// attrs: dictionary of { String: [tokens] }
// OR [{ String: [tokens] }, TemplateTag tokens...]
// (only for start tags; required)
// }
//
// { t: 'CharRef',
// v: String (entire character reference from the source, e.g. "&amp;"),
// cp: [Integer] (array of Unicode code point numbers it expands to)
// }
//
// We keep around both the original form of the character reference and its
// expansion so that subsequent processing steps have the option to
// re-emit it (if they are generating HTML) or interpret it. Named and
// numerical code points may be more than 16 bits, in which case they
// need to passed through codePointToString to make a JavaScript string.
// Most named entities and all numeric character references are one codepoint
// (e.g. "&amp;" is [38]), but a few are two codepoints.
//
// { t: 'TemplateTag',
// v: HTMLTools.TemplateTag
// }
// The HTML tokenization spec says to preprocess the input stream to replace
// CR(LF)? with LF. However, preprocessing `scanner` would complicate things
// by making indexes not match the input (e.g. for error messages), so we just
// keep in mind as we go along that an LF might be represented by CRLF or CR.
// In most cases, it doesn't actually matter what combination of whitespace
// characters are present (e.g. inside tags).
var HTML_SPACE = /^[\f\n\r\t ]/;
var convertCRLF = function (str) {
return str.replace(/\r\n?/g, '\n');
};
getComment = HTMLTools.Parse.getComment = function (scanner) {
if (scanner.rest().slice(0, 4) !== '<!--')
return null;
scanner.pos += 4;
// Valid comments are easy to parse; they end at the first `--`!
// Our main job is throwing errors.
var rest = scanner.rest();
if (rest.charAt(0) === '>' || rest.slice(0, 2) === '->')
scanner.fatal("HTML comment can't start with > or ->");
var closePos = rest.indexOf('-->');
if (closePos < 0)
scanner.fatal("Unclosed HTML comment");
var commentContents = rest.slice(0, closePos);
if (commentContents.slice(-1) === '-')
scanner.fatal("HTML comment must end at first `--`");
if (commentContents.indexOf("--") >= 0)
scanner.fatal("HTML comment cannot contain `--` anywhere");
if (commentContents.indexOf('\u0000') >= 0)
scanner.fatal("HTML comment cannot contain NULL");
scanner.pos += closePos + 3;
return { t: 'Comment',
v: convertCRLF(commentContents) };
};
var skipSpaces = function (scanner) {
while (HTML_SPACE.test(scanner.peek()))
scanner.pos++;
};
var requireSpaces = function (scanner) {
if (! HTML_SPACE.test(scanner.peek()))
scanner.fatal("Expected space");
skipSpaces(scanner);
};
var getDoctypeQuotedString = function (scanner) {
var quote = scanner.peek();
if (! (quote === '"' || quote === "'"))
scanner.fatal("Expected single or double quote in DOCTYPE");
scanner.pos++;
if (scanner.peek() === quote)
// prevent a falsy return value (empty string)
scanner.fatal("Malformed DOCTYPE");
var str = '';
var ch;
while ((ch = scanner.peek()), ch !== quote) {
if ((! ch) || (ch === '\u0000') || (ch === '>'))
scanner.fatal("Malformed DOCTYPE");
str += ch;
scanner.pos++;
}
scanner.pos++;
return str;
};
// See http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#the-doctype.
//
// If `getDocType` sees "<!DOCTYPE" (case-insensitive), it will match or fail fatally.
getDoctype = HTMLTools.Parse.getDoctype = function (scanner) {
if (HTMLTools.asciiLowerCase(scanner.rest().slice(0, 9)) !== '<!doctype')
return null;
var start = scanner.pos;
scanner.pos += 9;
requireSpaces(scanner);
var ch = scanner.peek();
if ((! ch) || (ch === '>') || (ch === '\u0000'))
scanner.fatal('Malformed DOCTYPE');
var name = ch;
scanner.pos++;
while ((ch = scanner.peek()), ! (HTML_SPACE.test(ch) || ch === '>')) {
if ((! ch) || (ch === '\u0000'))
scanner.fatal('Malformed DOCTYPE');
name += ch;
scanner.pos++;
}
name = HTMLTools.asciiLowerCase(name);
// Now we're looking at a space or a `>`.
skipSpaces(scanner);
var systemId = null;
var publicId = null;
if (scanner.peek() !== '>') {
// Now we're essentially in the "After DOCTYPE name state" of the tokenizer,
// but we're not looking at space or `>`.
// this should be "public" or "system".
var publicOrSystem = HTMLTools.asciiLowerCase(scanner.rest().slice(0, 6));
if (publicOrSystem === 'system') {
scanner.pos += 6;
requireSpaces(scanner);
systemId = getDoctypeQuotedString(scanner);
skipSpaces(scanner);
if (scanner.peek() !== '>')
scanner.fatal("Malformed DOCTYPE");
} else if (publicOrSystem === 'public') {
scanner.pos += 6;
requireSpaces(scanner);
publicId = getDoctypeQuotedString(scanner);
if (scanner.peek() !== '>') {
requireSpaces(scanner);
if (scanner.peek() !== '>') {
systemId = getDoctypeQuotedString(scanner);
skipSpaces(scanner);
if (scanner.peek() !== '>')
scanner.fatal("Malformed DOCTYPE");
}
}
} else {
scanner.fatal("Expected PUBLIC or SYSTEM in DOCTYPE");
}
}
// looking at `>`
scanner.pos++;
var result = { t: 'Doctype',
v: scanner.input.slice(start, scanner.pos),
name: name };
if (systemId)
result.systemId = systemId;
if (publicId)
result.publicId = publicId;
return result;
};
// The special character `{` is only allowed as the first character
// of a Chars, so that we have a chance to detect template tags.
var getChars = makeRegexMatcher(/^[^&<\u0000][^&<\u0000{]*/);
var assertIsTemplateTag = function (x) {
if (! (x instanceof HTMLTools.TemplateTag))
throw new Error("Expected an instance of HTMLTools.TemplateTag");
return x;
};
// Returns the next HTML token, or `null` if we reach EOF.
//
// Note that if we have a `getTemplateTag` function that sometimes
// consumes characters and emits nothing (e.g. in the case of template
// comments), we may go from not-at-EOF to at-EOF and return `null`,
// while otherwise we always find some token to return.
getHTMLToken = HTMLTools.Parse.getHTMLToken = function (scanner, dataMode) {
var result = null;
if (scanner.getTemplateTag) {
// Try to parse a template tag by calling out to the provided
// `getTemplateTag` function. If the function returns `null` but
// consumes characters, it must have parsed a comment or something,
// so we loop and try it again. If it ever returns `null` without
// consuming anything, that means it didn't see anything interesting
// so we look for a normal token. If it returns a truthy value,
// the value must be instanceof HTMLTools.TemplateTag. We wrap it
// in a Special token.
var lastPos = scanner.pos;
result = scanner.getTemplateTag(
scanner,
(dataMode === 'rcdata' ? TEMPLATE_TAG_POSITION.IN_RCDATA :
(dataMode === 'rawtext' ? TEMPLATE_TAG_POSITION.IN_RAWTEXT :
TEMPLATE_TAG_POSITION.ELEMENT)));
if (result)
return { t: 'TemplateTag', v: assertIsTemplateTag(result) };
else if (scanner.pos > lastPos)
return null;
}
var chars = getChars(scanner);
if (chars)
return { t: 'Chars',
v: convertCRLF(chars) };
var ch = scanner.peek();
if (! ch)
return null; // EOF
if (ch === '\u0000')
scanner.fatal("Illegal NULL character");
if (ch === '&') {
if (dataMode !== 'rawtext') {
var charRef = getCharacterReference(scanner);
if (charRef)
return charRef;
}
scanner.pos++;
return { t: 'Chars',
v: '&' };
}
// If we're here, we're looking at `<`.
if (scanner.peek() === '<' && dataMode) {
// don't interpret tags
scanner.pos++;
return { t: 'Chars',
v: '<' };
}
// `getTag` will claim anything starting with `<` not followed by `!`.
// `getComment` takes `<!--` and getDoctype takes `<!doctype`.
result = (getTagToken(scanner) || getComment(scanner) || getDoctype(scanner));
if (result)
return result;
scanner.fatal("Unexpected `<!` directive.");
};
var getTagName = makeRegexMatcher(/^[a-zA-Z][^\f\n\r\t />{]*/);
var getClangle = makeRegexMatcher(/^>/);
var getSlash = makeRegexMatcher(/^\//);
var getAttributeName = makeRegexMatcher(/^[^>/\u0000"'<=\f\n\r\t ][^\f\n\r\t /=>"'<\u0000]*/);
// Try to parse `>` or `/>`, mutating `tag` to be self-closing in the latter
// case (and failing fatally if `/` isn't followed by `>`).
// Return tag if successful.
var handleEndOfTag = function (scanner, tag) {
if (getClangle(scanner))
return tag;
if (getSlash(scanner)) {
if (! getClangle(scanner))
scanner.fatal("Expected `>` after `/`");
tag.isSelfClosing = true;
return tag;
}
return null;
};
// Scan a quoted or unquoted attribute value (omit `quote` for unquoted).
var getAttributeValue = function (scanner, quote) {
if (quote) {
if (scanner.peek() !== quote)
return null;
scanner.pos++;
}
var tokens = [];
var charsTokenToExtend = null;
var charRef;
while (true) {
var ch = scanner.peek();
var templateTag;
var curPos = scanner.pos;
if (quote && ch === quote) {
scanner.pos++;
return tokens;
} else if ((! quote) && (HTML_SPACE.test(ch) || ch === '>')) {
return tokens;
} else if (! ch) {
scanner.fatal("Unclosed attribute in tag");
} else if (quote ? ch === '\u0000' : ('\u0000"\'<=`'.indexOf(ch) >= 0)) {
scanner.fatal("Unexpected character in attribute value");
} else if (ch === '&' &&
(charRef = getCharacterReference(scanner, true,
quote || '>'))) {
tokens.push(charRef);
charsTokenToExtend = null;
} else if (scanner.getTemplateTag &&
((templateTag = scanner.getTemplateTag(
scanner, TEMPLATE_TAG_POSITION.IN_ATTRIBUTE)) ||
scanner.pos > curPos /* `{{! comment}}` */)) {
if (templateTag) {
tokens.push({t: 'TemplateTag',
v: assertIsTemplateTag(templateTag)});
charsTokenToExtend = null;
}
} else {
if (! charsTokenToExtend) {
charsTokenToExtend = { t: 'Chars', v: '' };
tokens.push(charsTokenToExtend);
}
charsTokenToExtend.v += (ch === '\r' ? '\n' : ch);
scanner.pos++;
if (quote && ch === '\r' && scanner.peek() === '\n')
scanner.pos++;
}
}
};
var hasOwnProperty = Object.prototype.hasOwnProperty;
getTagToken = HTMLTools.Parse.getTagToken = function (scanner) {
if (! (scanner.peek() === '<' && scanner.rest().charAt(1) !== '!'))
return null;
scanner.pos++;
var tag = { t: 'Tag' };
// now looking at the character after `<`, which is not a `!`
if (scanner.peek() === '/') {
tag.isEnd = true;
scanner.pos++;
}
var tagName = getTagName(scanner);
if (! tagName)
scanner.fatal("Expected tag name after `<`");
tag.n = HTMLTools.properCaseTagName(tagName);
if (scanner.peek() === '/' && tag.isEnd)
scanner.fatal("End tag can't have trailing slash");
if (handleEndOfTag(scanner, tag))
return tag;
if (scanner.isEOF())
scanner.fatal("Unclosed `<`");
if (! HTML_SPACE.test(scanner.peek()))
// e.g. `<a{{b}}>`
scanner.fatal("Expected space after tag name");
// we're now in "Before attribute name state" of the tokenizer
skipSpaces(scanner);
if (scanner.peek() === '/' && tag.isEnd)
scanner.fatal("End tag can't have trailing slash");
if (handleEndOfTag(scanner, tag))
return tag;
if (tag.isEnd)
scanner.fatal("End tag can't have attributes");
tag.attrs = {};
var nondynamicAttrs = tag.attrs;
while (true) {
// Note: at the top of this loop, we've already skipped any spaces.
// This will be set to true if after parsing the attribute, we should
// require spaces (or else an end of tag, i.e. `>` or `/>`).
var spacesRequiredAfter = false;
// first, try for a template tag.
var curPos = scanner.pos;
var templateTag = (scanner.getTemplateTag &&
scanner.getTemplateTag(
scanner, TEMPLATE_TAG_POSITION.IN_START_TAG));
if (templateTag || (scanner.pos > curPos)) {
if (templateTag) {
if (tag.attrs === nondynamicAttrs)
tag.attrs = [nondynamicAttrs];
tag.attrs.push({ t: 'TemplateTag',
v: assertIsTemplateTag(templateTag) });
} // else, must have scanned a `{{! comment}}`
spacesRequiredAfter = true;
} else {
var attributeName = getAttributeName(scanner);
if (! attributeName)
scanner.fatal("Expected attribute name in tag");
// Throw error on `{` in attribute name. This provides *some* error message
// if someone writes `<a x{{y}}>` or `<a x{{y}}=z>`. The HTML tokenization
// spec doesn't say that `{` is invalid, but the DOM API (setAttribute) won't
// allow it, so who cares.
if (attributeName.indexOf('{') >= 0)
scanner.fatal("Unexpected `{` in attribute name.");
attributeName = HTMLTools.properCaseAttributeName(attributeName);
if (hasOwnProperty.call(nondynamicAttrs, attributeName))
scanner.fatal("Duplicate attribute in tag: " + attributeName);
nondynamicAttrs[attributeName] = [];
skipSpaces(scanner);
if (handleEndOfTag(scanner, tag))
return tag;
var ch = scanner.peek();
if (! ch)
scanner.fatal("Unclosed <");
if ('\u0000"\'<'.indexOf(ch) >= 0)
scanner.fatal("Unexpected character after attribute name in tag");
if (ch === '=') {
scanner.pos++;
skipSpaces(scanner);
ch = scanner.peek();
if (! ch)
scanner.fatal("Unclosed <");
if ('\u0000><=`'.indexOf(ch) >= 0)
scanner.fatal("Unexpected character after = in tag");
if ((ch === '"') || (ch === "'"))
nondynamicAttrs[attributeName] = getAttributeValue(scanner, ch);
else
nondynamicAttrs[attributeName] = getAttributeValue(scanner);
spacesRequiredAfter = true;
}
}
// now we are in the "post-attribute" position, whether it was a template tag
// attribute (like `{{x}}`) or a normal one (like `x` or `x=y`).
if (handleEndOfTag(scanner, tag))
return tag;
if (scanner.isEOF())
scanner.fatal("Unclosed `<`");
if (spacesRequiredAfter)
requireSpaces(scanner);
else
skipSpaces(scanner);
if (handleEndOfTag(scanner, tag))
return tag;
}
};
TEMPLATE_TAG_POSITION = HTMLTools.TEMPLATE_TAG_POSITION = {
ELEMENT: 1,
IN_START_TAG: 2,
IN_ATTRIBUTE: 3,
IN_RCDATA: 4,
IN_RAWTEXT: 5
};
// tagName must be proper case
isLookingAtEndTag = function (scanner, tagName) {
var rest = scanner.rest();
var pos = 0; // into rest
var firstPart = /^<\/([a-zA-Z]+)/.exec(rest);
if (firstPart &&
HTMLTools.properCaseTagName(firstPart[1]) === tagName) {
// we've seen `</foo`, now see if the end tag continues
pos += firstPart[0].length;
while (pos < rest.length && HTML_SPACE.test(rest.charAt(pos)))
pos++;
if (pos < rest.length && rest.charAt(pos) === '>')
return true;
}
return false;
};