diff --git a/package.json b/package.json
index 39f05e4d3..0953fb4e0 100644
--- a/package.json
+++ b/package.json
@@ -14,10 +14,13 @@
"node": ">=6"
},
"dependencies": {
+ "@angular/compiler": "6.1.10",
"@babel/code-frame": "7.0.0-beta.46",
"@babel/parser": "7.1.2",
"@glimmer/syntax": "0.30.3",
"@iarna/toml": "2.0.0",
+ "angular-estree-parser": "1.1.3",
+ "angular-html-parser": "1.0.0",
"camelcase": "4.1.0",
"chalk": "2.1.0",
"cjk-regex": "2.0.0",
@@ -25,7 +28,6 @@
"dashify": "0.2.2",
"dedent": "0.7.0",
"diff": "3.2.0",
- "domhandler": "2.4.2",
"editorconfig": "0.15.2",
"editorconfig-to-prettier": "0.0.6",
"emoji-regex": "6.5.1",
@@ -40,7 +42,6 @@
"html-element-attributes": "2.0.0",
"html-styles": "1.0.0",
"html-tag-names": "1.1.2",
- "htmlparser2": "3.9.2",
"ignore": "3.3.7",
"jest-docblock": "23.2.0",
"json-stable-stringify": "1.0.1",
@@ -53,7 +54,7 @@
"minimist": "1.2.0",
"n-readlines": "1.0.0",
"normalize-path": "3.0.0",
- "parse5-htmlparser2-tree-adapter": "5.0.0",
+ "parse-srcset": "ikatyang/parse-srcset#feat/report-error",
"postcss-less": "1.1.5",
"postcss-media-query-parser": "0.2.3",
"postcss-scss": "1.0.6",
diff --git a/scripts/build/config.js b/scripts/build/config.js
index e1fb4eb5b..40db88202 100644
--- a/scripts/build/config.js
+++ b/scripts/build/config.js
@@ -1,6 +1,7 @@
"use strict";
const path = require("path");
+const PROJECT_ROOT = path.resolve(__dirname, "../..");
/**
* @typedef {Object} Bundle
@@ -38,6 +39,18 @@ const parsers = [
input: "src/language-js/parser-typescript.js",
target: "universal"
},
+ {
+ input: "src/language-js/parser-angular.js",
+ target: "universal",
+ alias: {
+ // Force using the CJS file, instead of ESM; i.e. get the file
+ // from `"main"` instead of `"module"` (rollup default) of package.json
+ "lines-and-columns": require.resolve("lines-and-columns"),
+ "@angular/compiler/src": path.resolve(
+ `${PROJECT_ROOT}/node_modules/@angular/compiler/esm2015/src`
+ )
+ }
+ },
{
input: "src/language-css/parser-postcss.js",
target: "universal",
@@ -52,10 +65,6 @@ const parsers = [
input: "src/language-markdown/parser-markdown.js",
target: "universal"
},
- {
- input: "src/language-vue/parser-vue.js",
- target: "universal"
- },
{
input: "src/language-handlebars/parser-glimmer.js",
target: "universal",
diff --git a/src/common/internal-plugins.js b/src/common/internal-plugins.js
index 8fcea3f55..d071a32d7 100644
--- a/src/common/internal-plugins.js
+++ b/src/common/internal-plugins.js
@@ -29,6 +29,10 @@ module.exports = [
return eval("require")("../language-js/parser-babylon").parsers
.__js_expression;
},
+ get __vue_expression() {
+ return eval("require")("../language-js/parser-babylon").parsers
+ .__vue_expression;
+ },
// JS - Flow
get flow() {
return eval("require")("../language-js/parser-flow").parsers.flow;
@@ -44,6 +48,26 @@ module.exports = [
get "typescript-eslint"() {
return eval("require")("../language-js/parser-typescript").parsers
.typescript;
+ },
+ // JS - Angular Action
+ get __ng_action() {
+ return eval("require")("../language-js/parser-angular").parsers
+ .__ng_action;
+ },
+ // JS - Angular Binding
+ get __ng_binding() {
+ return eval("require")("../language-js/parser-angular").parsers
+ .__ng_binding;
+ },
+ // JS - Angular Interpolation
+ get __ng_interpolation() {
+ return eval("require")("../language-js/parser-angular").parsers
+ .__ng_interpolation;
+ },
+ // JS - Angular Directive
+ get __ng_directive() {
+ return eval("require")("../language-js/parser-angular").parsers
+ .__ng_directive;
}
}
},
@@ -107,22 +131,20 @@ module.exports = [
}
},
- // HTML
require("../language-html"),
{
parsers: {
+ // HTML
get html() {
return eval("require")("../language-html/parser-html").parsers.html;
- }
- }
- },
-
- // Vue
- require("../language-vue"),
- {
- parsers: {
+ },
+ // Vue
get vue() {
- return eval("require")("../language-vue/parser-vue").parsers.vue;
+ return eval("require")("../language-html/parser-html").parsers.vue;
+ },
+ // Angular
+ get angular() {
+ return eval("require")("../language-html/parser-html").parsers.angular;
}
}
},
diff --git a/src/common/util.js b/src/common/util.js
index a1258a993..7bea29ca5 100644
--- a/src/common/util.js
+++ b/src/common/util.js
@@ -464,9 +464,11 @@ function printString(raw, options, isDirectiveLiteral) {
const enclosingQuote =
options.parser === "json"
? double.quote
- : shouldUseAlternateQuote
- ? alternate.quote
- : preferred.quote;
+ : options.__isInHtmlAttribute
+ ? single.quote
+ : shouldUseAlternateQuote
+ ? alternate.quote
+ : preferred.quote;
// Directives are exact code unit sequences, which means that you can't
// change the escape sequences they use.
@@ -489,7 +491,10 @@ function printString(raw, options, isDirectiveLiteral) {
!(
options.parser === "css" ||
options.parser === "less" ||
- options.parser === "scss"
+ options.parser === "scss" ||
+ options.parentParser === "html" ||
+ options.parentParser === "vue" ||
+ options.parentParser === "angular"
)
);
}
diff --git a/src/language-html/ast.js b/src/language-html/ast.js
new file mode 100644
index 000000000..13b6c9ba8
--- /dev/null
+++ b/src/language-html/ast.js
@@ -0,0 +1,136 @@
+"use strict";
+
+const NODES_KEYS = {
+ attrs: true,
+ children: true
+};
+
+class Node {
+ constructor(props = {}) {
+ for (const key of Object.keys(props)) {
+ const value = props[key];
+ if (key in NODES_KEYS) {
+ this._setNodes(key, value);
+ } else {
+ this[key] = value;
+ }
+ }
+ }
+
+ _setNodes(key, nodes) {
+ if (nodes !== this[key]) {
+ this[key] = cloneAndUpdateNodes(nodes, this);
+ if (key === "attrs") {
+ setNonEnumerableProperties(this, {
+ attrMap: this[key].reduce((reduced, attr) => {
+ reduced[attr.fullName] = attr.value;
+ return reduced;
+ }, Object.create(null))
+ });
+ }
+ }
+ }
+
+ map(fn) {
+ let newNode = null;
+
+ for (const NODES_KEY in NODES_KEYS) {
+ const nodes = this[NODES_KEY];
+ if (nodes) {
+ const mappedNodes = mapNodesIfChanged(nodes, node => node.map(fn));
+ if (newNode !== nodes) {
+ if (!newNode) {
+ newNode = new Node();
+ }
+ newNode._setNodes(NODES_KEY, mappedNodes);
+ }
+ }
+ }
+
+ if (newNode) {
+ for (const key in this) {
+ if (!(key in NODES_KEYS)) {
+ newNode[key] = this[key];
+ }
+ }
+ const { index, siblings, prev, next, parent } = this;
+ setNonEnumerableProperties(newNode, {
+ index,
+ siblings,
+ prev,
+ next,
+ parent
+ });
+ }
+
+ return fn(newNode || this);
+ }
+
+ clone(overrides) {
+ return new Node(overrides ? Object.assign({}, this, overrides) : this);
+ }
+
+ get firstChild() {
+ return this.children && this.children.length !== 0
+ ? this.children[0]
+ : null;
+ }
+
+ get lastChild() {
+ return this.children && this.children.length !== 0
+ ? this.children[this.children.length - 1]
+ : null;
+ }
+
+ // for element and attribute
+ get rawName() {
+ return this.hasExplicitNamespace ? this.fullName : this.name;
+ }
+ get fullName() {
+ return this.namespace ? this.namespace + ":" + this.name : this.name;
+ }
+}
+
+function mapNodesIfChanged(nodes, fn) {
+ const newNodes = nodes.map(fn);
+ return newNodes.some((newNode, index) => newNode !== nodes[index])
+ ? newNodes
+ : nodes;
+}
+
+function cloneAndUpdateNodes(nodes, parent) {
+ const siblings = nodes.map(
+ node => (node instanceof Node ? node.clone() : new Node(node))
+ );
+
+ let prev = null;
+ let current = siblings[0];
+ let next = siblings[1] || null;
+
+ for (let index = 0; index < siblings.length; index++) {
+ setNonEnumerableProperties(current, {
+ index,
+ siblings,
+ prev,
+ next,
+ parent
+ });
+ prev = current;
+ current = next;
+ next = siblings[index + 2] || null;
+ }
+
+ return siblings;
+}
+
+function setNonEnumerableProperties(obj, props) {
+ const descriptors = Object.keys(props).reduce((reduced, key) => {
+ reduced[key] = { value: props[key], enumerable: false };
+ return reduced;
+ }, {});
+ Object.defineProperties(obj, descriptors);
+}
+
+module.exports = {
+ Node
+};
diff --git a/src/language-html/clean.js b/src/language-html/clean.js
index 6c16c1db8..c14bb094b 100644
--- a/src/language-html/clean.js
+++ b/src/language-html/clean.js
@@ -1,9 +1,11 @@
"use strict";
module.exports = function(ast, newNode) {
- delete newNode.startIndex;
- delete newNode.endIndex;
- delete newNode.attribs;
+ delete newNode.sourceSpan;
+ delete newNode.startSourceSpan;
+ delete newNode.endSourceSpan;
+ delete newNode.nameSpan;
+ delete newNode.valueSpan;
if (ast.type === "text" || ast.type === "comment") {
return null;
@@ -18,7 +20,7 @@ module.exports = function(ast, newNode) {
delete newNode.value;
}
- if (ast.type === "directive" && ast.name === "!doctype") {
- delete newNode.data;
+ if (ast.type === "docType") {
+ delete newNode.value;
}
};
diff --git a/src/language-html/constants.evaluate.js b/src/language-html/constants.evaluate.js
index c56fc7f01..2ce6efaf5 100644
--- a/src/language-html/constants.evaluate.js
+++ b/src/language-html/constants.evaluate.js
@@ -17,7 +17,19 @@ const getCssStyleTags = property =>
)
.reduce((reduced, value) => Object.assign(reduced, value), {});
-const CSS_DISPLAY_TAGS = getCssStyleTags("display");
+const CSS_DISPLAY_TAGS = Object.assign({}, getCssStyleTags("display"), {
+ // TODO: send PR to upstream
+ button: "inline-block",
+
+ // special cases for some css display=none elements
+ template: "inline",
+ source: "block",
+ track: "block",
+
+ // there's no css display for these elements but they behave these ways
+ video: "inline-block",
+ audio: "inline-block"
+});
const CSS_DISPLAY_DEFAULT = "inline";
const CSS_WHITE_SPACE_TAGS = getCssStyleTags("white-space");
const CSS_WHITE_SPACE_DEFAULT = "normal";
diff --git a/src/language-html/index.js b/src/language-html/index.js
index 3a9be5104..e90f51a21 100644
--- a/src/language-html/index.js
+++ b/src/language-html/index.js
@@ -1,21 +1,39 @@
"use strict";
-const printer = require("./printer-htmlparser2");
+const printer = require("./printer-html");
const createLanguage = require("../utils/create-language");
const options = require("./options");
const languages = [
+ createLanguage(require("linguist-languages/data/html"), {
+ override: {
+ name: "Angular",
+ since: "1.15.0",
+ parsers: ["angular"],
+ vscodeLanguageIds: ["html"],
+
+ extensions: [".component.html"],
+ filenames: []
+ }
+ }),
createLanguage(require("linguist-languages/data/html"), {
override: {
since: "1.15.0",
parsers: ["html"],
vscodeLanguageIds: ["html"]
}
+ }),
+ createLanguage(require("linguist-languages/data/vue"), {
+ override: {
+ since: "1.10.0",
+ parsers: ["vue"],
+ vscodeLanguageIds: ["vue"]
+ }
})
];
const printers = {
- htmlparser2: printer
+ html: printer
};
module.exports = {
diff --git a/src/language-html/parser-html.js b/src/language-html/parser-html.js
index 1e3469644..ff929e712 100644
--- a/src/language-html/parser-html.js
+++ b/src/language-html/parser-html.js
@@ -1,225 +1,299 @@
"use strict";
const parseFrontMatter = require("../utils/front-matter");
-const { HTML_ELEMENT_ATTRIBUTES, HTML_TAGS, mapNode } = require("./utils");
+const { HTML_ELEMENT_ATTRIBUTES, HTML_TAGS } = require("./utils");
+const { hasPragma } = require("./pragma");
+const createError = require("../common/parser-create-error");
+const { Node } = require("./ast");
-function parse(text, parsers, options, { shouldParseFrontMatter = true } = {}) {
+function ngHtmlParser(input, canSelfClose) {
+ const parser = require("angular-html-parser");
+ const {
+ RecursiveVisitor,
+ visitAll,
+ Attribute,
+ CDATA,
+ Comment,
+ DocType,
+ Element,
+ Text
+ } = require("angular-html-parser/lib/compiler/src/ml_parser/ast");
+ const {
+ ParseSourceSpan
+ } = require("angular-html-parser/lib/compiler/src/parse_util");
+ const {
+ getHtmlTagDefinition
+ } = require("angular-html-parser/lib/compiler/src/ml_parser/html_tags");
+
+ const { rootNodes, errors } = parser.parse(input, { canSelfClose });
+
+ if (errors.length !== 0) {
+ const { msg, span } = errors[0];
+ const { line, col } = span.start;
+ throw createError(msg, { start: { line: line + 1, column: col } });
+ }
+
+ const addType = node => {
+ if (node instanceof Attribute) {
+ node.type = "attribute";
+ } else if (node instanceof CDATA) {
+ node.type = "cdata";
+ } else if (node instanceof Comment) {
+ node.type = "comment";
+ } else if (node instanceof DocType) {
+ node.type = "docType";
+ } else if (node instanceof Element) {
+ node.type = "element";
+ } else if (node instanceof Text) {
+ node.type = "text";
+ } else {
+ throw new Error(`Unexpected node ${JSON.stringify(node)}`);
+ }
+ };
+
+ const restoreName = node => {
+ const namespace = node.name.startsWith(":")
+ ? node.name.slice(1).split(":")[0]
+ : null;
+ const rawName = node.nameSpan ? node.nameSpan.toString() : node.name;
+ const hasExplicitNamespace = rawName.startsWith(`${namespace}:`);
+ const name = hasExplicitNamespace
+ ? rawName.slice(namespace.length + 1)
+ : rawName;
+
+ node.name = name;
+ node.namespace = namespace;
+ node.hasExplicitNamespace = hasExplicitNamespace;
+ };
+
+ const restoreNameAndValue = node => {
+ if (node instanceof Element) {
+ restoreName(node);
+ node.attrs.forEach(attr => {
+ restoreName(attr);
+ if (!attr.valueSpan) {
+ attr.value = null;
+ } else {
+ attr.value = attr.valueSpan.toString();
+ if (/['"]/.test(attr.value[0])) {
+ attr.value = attr.value.slice(1, -1);
+ }
+ }
+ });
+ } else if (node instanceof Comment) {
+ node.value = node.sourceSpan
+ .toString()
+ .slice("".length);
+ } else if (node instanceof Text) {
+ node.value = node.sourceSpan.toString();
+ }
+ };
+
+ const lowerCaseIfFn = (text, fn) => {
+ const lowerCasedText = text.toLowerCase();
+ return fn(lowerCasedText) ? lowerCasedText : text;
+ };
+ const normalizeName = node => {
+ if (node instanceof Element) {
+ if (
+ !node.namespace ||
+ node.namespace === node.tagDefinition.implicitNamespacePrefix
+ ) {
+ node.name = lowerCaseIfFn(
+ node.name,
+ lowerCasedName => lowerCasedName in HTML_TAGS
+ );
+ }
+
+ const CURRENT_HTML_ELEMENT_ATTRIBUTES =
+ HTML_ELEMENT_ATTRIBUTES[node.name] || Object.create(null);
+ node.attrs.forEach(attr => {
+ if (!attr.namespace) {
+ attr.name = lowerCaseIfFn(
+ attr.name,
+ lowerCasedAttrName =>
+ node.name in HTML_ELEMENT_ATTRIBUTES &&
+ (lowerCasedAttrName in HTML_ELEMENT_ATTRIBUTES["*"] ||
+ lowerCasedAttrName in CURRENT_HTML_ELEMENT_ATTRIBUTES)
+ );
+ }
+ });
+ }
+ };
+
+ const fixSourceSpan = node => {
+ if (node.sourceSpan && node.endSourceSpan) {
+ node.sourceSpan = new ParseSourceSpan(
+ node.sourceSpan.start,
+ node.endSourceSpan.end
+ );
+ }
+ };
+
+ const addTagDefinition = node => {
+ if (node instanceof Element) {
+ const tagDefinition = getHtmlTagDefinition(node.name);
+ if (
+ !node.namespace ||
+ node.namespace === tagDefinition.implicitNamespacePrefix
+ ) {
+ node.tagDefinition = tagDefinition;
+ } else {
+ node.tagDefinition = getHtmlTagDefinition(""); // the default one
+ }
+ }
+ };
+
+ visitAll(
+ new class extends RecursiveVisitor {
+ visit(node) {
+ addType(node);
+ restoreNameAndValue(node);
+ addTagDefinition(node);
+ normalizeName(node);
+ fixSourceSpan(node);
+ }
+ }(),
+ rootNodes
+ );
+
+ return rootNodes;
+}
+
+function _parse(
+ text,
+ options,
+ recognizeSelfClosing = false,
+ shouldParseFrontMatter = true
+) {
const { frontMatter, content } = shouldParseFrontMatter
? parseFrontMatter(text)
: { frontMatter: null, content: text };
- // Inline the require to avoid loading all the JS if we don't use it
- const Parser = require("htmlparser2/lib/Parser");
- const DomHandler = require("domhandler");
-
- /**
- * modifications:
- * - empty attributes (e.g., `
+ */
+ return (
+ node.next &&
+ node.type === "text" &&
+ node.isTrailingSpaceSensitive &&
+ !node.hasTrailingSpaces
+ );
+}
+
+function needsToBorrowParentOpeningTagEndMarker(node) {
+ /**
+ * 123
+ * ^
+ *
+ *
+ * ^
+ */
+ " "
+ : ""
+ : concat([
+ indent(
+ concat([
+ forceNotToBreakAttrContent ? " " : line,
+ join(
+ line,
+ (ignoreAttributeData => {
+ const hasPrettierIgnoreAttribute =
+ typeof ignoreAttributeData === "boolean"
+ ? () => ignoreAttributeData
+ : Array.isArray(ignoreAttributeData)
+ ? attr =>
+ ignoreAttributeData.indexOf(attr.rawName) !== -1
+ : () => false;
+ return path.map(attrPath => {
+ const attr = attrPath.getValue();
+ return hasPrettierIgnoreAttribute(attr)
+ ? options.originalText.slice(
+ options.locStart(attr),
+ options.locEnd(attr)
+ )
+ : print(attrPath);
+ }, "attrs");
+ })(
+ node.prev &&
+ node.prev.type === "comment" &&
+ getPrettierIgnoreAttributeCommentData(node.prev.value)
+ )
+ )
+ ])
+ ),
+ /**
+ * 123456
+ */
+ (node.firstChild &&
+ needsToBorrowParentOpeningTagEndMarker(node.firstChild)) ||
+ /**
+ * 123
+ */
+ (node.isSelfClosing &&
+ needsToBorrowLastChildClosingTagEndMarker(node.parent))
+ ? ""
+ : node.isSelfClosing
+ ? forceNotToBreakAttrContent
+ ? " "
+ : line
+ : forceNotToBreakAttrContent
+ ? ""
+ : softline
+ ]),
+ node.isSelfClosing ? "" : printOpeningTagEnd(node)
+ ]);
+}
+
+function printOpeningTagStart(node) {
+ return node.prev && needsToBorrowNextOpeningTagStartMarker(node.prev)
+ ? ""
+ : concat([printOpeningTagPrefix(node), printOpeningTagStartMarker(node)]);
+}
+
+function printOpeningTagEnd(node) {
+ return node.firstChild &&
+ needsToBorrowParentOpeningTagEndMarker(node.firstChild)
+ ? ""
+ : printOpeningTagEndMarker(node);
+}
+
+function printClosingTag(node) {
+ return concat([
+ node.isSelfClosing ? "" : printClosingTagStart(node),
+ printClosingTagEnd(node)
+ ]);
+}
+
+function printClosingTagStart(node) {
+ return node.lastChild &&
+ needsToBorrowParentClosingTagStartMarker(node.lastChild)
+ ? ""
+ : concat([printClosingTagPrefix(node), printClosingTagStartMarker(node)]);
+}
+
+function printClosingTagEnd(node) {
+ return (node.next
+ ? needsToBorrowPrevClosingTagEndMarker(node.next)
+ : needsToBorrowLastChildClosingTagEndMarker(node.parent))
+ ? ""
+ : concat([printClosingTagEndMarker(node), printClosingTagSuffix(node)]);
+}
+
+function needsToBorrowNextOpeningTagStartMarker(node) {
+ /**
+ * 123
+ * 123
+ * + * 123 + */ + return ( + !node.next && + !node.hasTrailingSpaces && + node.isTrailingSpaceSensitive && + getLastDescendant(node).type === "text" + ); +} + +function printOpeningTagPrefix(node) { + return needsToBorrowParentOpeningTagEndMarker(node) + ? printOpeningTagEndMarker(node.parent) + : needsToBorrowPrevClosingTagEndMarker(node) + ? printClosingTagEndMarker(node.prev) + : ""; +} + +function printClosingTagPrefix(node) { + return needsToBorrowLastChildClosingTagEndMarker(node) + ? printClosingTagEndMarker(node.lastChild) + : ""; +} + +function printClosingTagSuffix(node) { + return needsToBorrowParentClosingTagStartMarker(node) + ? printClosingTagStartMarker(node.parent) + : needsToBorrowNextOpeningTagStartMarker(node) + ? printOpeningTagStartMarker(node.next) + : ""; +} + +function printOpeningTagStartMarker(node) { + switch (node.type) { + case "comment": + return ""; + case "ieConditionalComment": + return `[endif]-->`; + case "interpolation": + return "}}"; + case "element": + if (node.isSelfClosing) { + return "/>"; + } + // fall through + default: + return ">"; + } +} + +function getTextValueParts(node, value = node.value) { + return node.isWhitespaceSensitive + ? node.isIndentationSensitive + ? replaceNewlines(value, literalline) + : replaceNewlines( + dedentString(value.replace(/^\s*?\n|\n\s*?$/g, "")), + hardline + ) + : // non-breaking whitespace: 0xA0 + join(line, value.split(/[^\S\xA0]+/)).parts; +} + +function printEmbeddedAttributeValue(node, originalTextToDoc, options) { + const isKeyMatched = patterns => + new RegExp(patterns.join("|")).test(node.fullName); + const getValue = () => + node.value.replace(/"/g, '"').replace(/'/g, "'"); + + let shouldHug = false; + + const __onHtmlBindingRoot = root => { + const rootNode = + root.type === "NGRoot" + ? root.node.type === "NGMicrosyntax" && + root.node.body.length === 1 && + root.node.body[0].type === "NGMicrosyntaxExpression" + ? root.node.body[0].expression + : root.node + : root.type === "JsExpressionRoot" + ? root.node + : root; + if ( + rootNode && + (rootNode.type === "ObjectExpression" || + rootNode.type === "ArrayExpression") + ) { + shouldHug = true; + } + }; + + const printHug = doc => group(doc); + const printExpand = doc => + group(concat([indent(concat([softline, doc])), softline])); + const printMaybeHug = doc => (shouldHug ? printHug(doc) : printExpand(doc)); + + const textToDoc = (code, opts) => + originalTextToDoc(code, Object.assign({ __onHtmlBindingRoot }, opts)); + + if ( + node.fullName === "srcset" && + (node.parent.fullName === "img" || node.parent.fullName === "source") + ) { + return printExpand(printImgSrcset(getValue())); + } + + if (options.parser === "vue") { + if (node.fullName === "v-for") { + return printVueFor(getValue(), textToDoc); + } + + if (node.fullName === "slot-scope") { + return printVueSlotScope(getValue(), textToDoc); + } + + /** + * @click="jsStatement" + * @click="jsExpression" + * v-on:click="jsStatement" + * v-on:click="jsExpression" + */ + const vueEventBindingPatterns = ["^@", "^v-on:"]; + /** + * :class="vueExpression" + * v-bind:id="vueExpression" + */ + const vueExpressionBindingPatterns = ["^:", "^v-bind:"]; + /** + * v-if="jsExpression" + */ + const jsExpressionBindingPatterns = ["^v-"]; + + if (isKeyMatched(vueEventBindingPatterns)) { + // copied from https://github.com/vuejs/vue/blob/v2.5.17/src/compiler/codegen/events.js#L3-L4 + const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function\s*\(/; + const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/; + + const value = getValue(); + return printMaybeHug( + simplePathRE.test(value) || fnExpRE.test(value) + ? textToDoc(value, { parser: "__js_expression" }) + : stripTrailingHardline(textToDoc(value, { parser: "babylon" })) + ); + } + + if (isKeyMatched(vueExpressionBindingPatterns)) { + return printMaybeHug( + textToDoc(getValue(), { parser: "__vue_expression" }) + ); + } + + if (isKeyMatched(jsExpressionBindingPatterns)) { + return printMaybeHug( + textToDoc(getValue(), { parser: "__js_expression" }) + ); + } + } + + if (options.parser === "angular") { + const ngTextToDoc = (code, opts) => + // angular does not allow trailing comma + textToDoc(code, Object.assign({ trailingComma: "none" }, opts)); + + /** + * *directive="angularDirective" + */ + const ngDirectiveBindingPatterns = ["^\\*"]; + /** + * (click)="angularStatement" + * on-click="angularStatement" + */ + const ngStatementBindingPatterns = ["^\\(.+\\)$", "^on-"]; + /** + * [target]="angularExpression" + * bind-target="angularExpression" + * [(target)]="angularExpression" + * bindon-target="angularExpression" + */ + const ngExpressionBindingPatterns = ["^\\[.+\\]$", "^bind(on)?-"]; + + if (isKeyMatched(ngStatementBindingPatterns)) { + return printMaybeHug(ngTextToDoc(getValue(), { parser: "__ng_action" })); + } + + if (isKeyMatched(ngExpressionBindingPatterns)) { + return printMaybeHug(ngTextToDoc(getValue(), { parser: "__ng_binding" })); + } + + if (isKeyMatched(ngDirectiveBindingPatterns)) { + return printMaybeHug( + ngTextToDoc(getValue(), { parser: "__ng_directive" }) + ); + } + } + + return null; +} + +module.exports = { + preprocess, + print: genericPrint, + insertPragma, + massageAstNode: clean, + embed +}; diff --git a/src/language-html/printer-htmlparser2.js b/src/language-html/printer-htmlparser2.js deleted file mode 100644 index 279431e5e..000000000 --- a/src/language-html/printer-htmlparser2.js +++ /dev/null @@ -1,584 +0,0 @@ -"use strict"; - -const clean = require("./clean"); -const { - builders, - utils: { removeLines, stripTrailingHardline } -} = require("../doc"); -const { - breakParent, - group, - hardline, - indent, - join, - line, - literalline, - markAsRoot, - softline -} = builders; -const { hasNewlineInRange } = require("../common/util"); -const { - normalizeParts, - dedentString, - forceBreakChildren, - forceBreakContent, - forceNextEmptyLine, - getCommentData, - getLastDescendant, - hasPrettierIgnore, - inferScriptParser, - isScriptLikeTag, - preferHardlineAsLeadingSpaces, - replaceDocNewlines, - replaceNewlines -} = require("./utils"); -const preprocess = require("./preprocess"); -const assert = require("assert"); - -function concat(parts) { - const newParts = normalizeParts(parts); - return newParts.length === 0 - ? "" - : newParts.length === 1 - ? newParts[0] - : builders.concat(newParts); -} - -function fill(parts) { - const newParts = []; - - let hasSeparator = true; - for (const part of normalizeParts(parts)) { - switch (part) { - case line: - case hardline: - case literalline: - case softline: - newParts.push(part); - hasSeparator = true; - break; - default: - if (!hasSeparator) { - // `fill` needs a separator between each two parts - newParts.push(""); - } - newParts.push(part); - hasSeparator = false; - break; - } - } - - return builders.fill(newParts); -} - -function embed(path, print, textToDoc /*, options */) { - const node = path.getValue(); - switch (node.type) { - case "text": { - if (isScriptLikeTag(node.parent)) { - const parser = inferScriptParser(node.parent); - if (parser) { - return builders.concat([ - concat([ - breakParent, - printOpeningTagPrefix(node), - markAsRoot( - stripTrailingHardline(textToDoc(node.data, { parser })) - ), - printClosingTagSuffix(node) - ]) - ]); - } - } - break; - } - case "attribute": { - /* - * Vue binding syntax: JS expressions - * :class="{ 'some-key': value }" - * v-bind:id="'list-' + id" - * v-if="foo && !bar" - * @click="someFunction()" - */ - if (/(^@)|(^v-)|:/.test(node.key) && !/^\w+$/.test(node.value)) { - const doc = textToDoc(node.value, { - parser: "__js_expression", - // Use singleQuote since HTML attributes use double-quotes. - // TODO(azz): We still need to do an entity escape on the attribute. - singleQuote: true - }); - return concat([ - node.key, - '="', - hasNewlineInRange(node.value, 0, node.value.length) - ? doc - : removeLines(doc), - '"' - ]); - } - break; - } - case "yaml": - return markAsRoot( - concat([ - "---", - hardline, - node.value.trim().length === 0 - ? "" - : replaceDocNewlines( - textToDoc(node.value, { parser: "yaml" }), - literalline - ), - "---" - ]) - ); - } -} - -function genericPrint(path, options, print) { - const node = path.getValue(); - switch (node.type) { - case "root": - return concat([group(printChildren(path, options, print)), hardline]); - case "tag": - case "ieConditionalComment": - return concat([ - group( - concat([ - printOpeningTag(path, options, print), - node.children.length === 0 - ? node.hasDanglingSpaces && node.isDanglingSpaceSensitive - ? line - : "" - : concat([ - forceBreakContent(node) ? breakParent : "", - indent( - concat([ - node.firstChild.type === "text" && - node.firstChild.isWhiteSpaceSensitive && - node.firstChild.isIndentationSensitive - ? literalline - : node.firstChild.hasLeadingSpaces && - node.firstChild.isLeadingSpaceSensitive - ? line - : softline, - printChildren(path, options, print) - ]) - ), - (node.next - ? needsToBorrowPrevClosingTagEndMarker(node.next) - : needsToBorrowLastChildClosingTagEndMarker(node.parent)) - ? "" - : node.lastChild.hasTrailingSpaces && - node.lastChild.isTrailingSpaceSensitive - ? line - : softline - ]) - ]) - ), - printClosingTag(node) - ]); - case "text": - return fill( - [].concat( - printOpeningTagPrefix(node), - node.isWhiteSpaceSensitive - ? node.isIndentationSensitive - ? replaceNewlines( - node.data.replace(/^\s*?\n|\n\s*?$/g, ""), - literalline - ) - : replaceNewlines( - dedentString(node.data.replace(/^\s*?\n|\n\s*?$/g, "")), - hardline - ) - : join(line, node.data.split(/\s+/)).parts, - printClosingTagSuffix(node) - ) - ); - case "comment": - case "directive": { - const data = getCommentData(node); - return concat([ - group( - concat([ - printOpeningTagStart(node), - data.trim().length === 0 - ? "" - : concat([ - indent( - concat([ - node.prev && - needsToBorrowNextOpeningTagStartMarker(node.prev) - ? breakParent - : "", - node.type === "directive" ? " " : line, - concat(replaceNewlines(data, hardline)) - ]) - ), - node.type === "directive" - ? "" - : (node.next - ? needsToBorrowPrevClosingTagEndMarker(node.next) - : needsToBorrowLastChildClosingTagEndMarker(node.parent)) - ? " " - : line - ]) - ]) - ), - printClosingTagEnd(node) - ]); - } - case "attribute": - return concat([ - node.key, - node.value === null - ? "" - : concat([ - '="', - concat( - replaceNewlines(node.value.replace(/"/g, """), literalline) - ), - '"' - ]) - ]); - case "yaml": - case "toml": - return node.raw; - default: - throw new Error(`Unexpected node type ${node.type}`); - } -} - -function printChildren(path, options, print) { - const node = path.getValue(); - - if (forceBreakChildren(node)) { - return concat([ - breakParent, - concat( - path.map(childPath => { - const childNode = childPath.getValue(); - const prevBetweenLine = !childNode.prev - ? "" - : printBetweenLine(childNode.prev, childNode); - return concat([ - !prevBetweenLine - ? "" - : concat([ - prevBetweenLine, - forceNextEmptyLine(childNode.prev) || - childNode.prev.endLocation.line + 1 < - childNode.startLocation.line - ? hardline - : "" - ]), - print(childPath) - ]); - }, "children") - ) - ]); - } - - const parts = []; - - path.map((childPath, childIndex) => { - const childNode = childPath.getValue(); - - if (childIndex !== 0) { - const prevBetweenLine = printBetweenLine(childNode.prev, childNode); - if (prevBetweenLine) { - if ( - forceNextEmptyLine(childNode.prev) || - childNode.prev.endLocation.line + 1 < childNode.startLocation.line - ) { - parts.push(hardline, hardline); - } else { - parts.push(prevBetweenLine); - } - } - } - - Array.prototype.push.apply( - parts, - childNode.type === "text" ? print(childPath).parts : [print(childPath)] - ); - }, "children"); - - return fill(parts); - - function printBetweenLine(prevNode, nextNode) { - return (needsToBorrowNextOpeningTagStartMarker(prevNode) && - /** - * 123 - */ - (nextNode.firstChild || - /** - * 123- */ - return ( - node.next && - node.type === "text" && - node.isTrailingSpaceSensitive && - !node.hasTrailingSpaces - ); -} - -function needsToBorrowParentOpeningTagEndMarker(node) { - /** - *
123 - * ^ - * - *
123 - * ^ - * - * - */ - return ( - node.lastChild && - node.lastChild.isTrailingSpaceSensitive && - !node.lastChild.hasTrailingSpaces && - getLastDescendant(node.lastChild).type !== "text" - ); -} - -function needsToBorrowParentClosingTagStartMarker(node) { - /** - *- * 123
- * - * 123 - */ - return ( - !node.next && - !node.hasTrailingSpaces && - node.isTrailingSpaceSensitive && - getLastDescendant(node).type === "text" - ); -} - -function printOpeningTagPrefix(node) { - return needsToBorrowParentOpeningTagEndMarker(node) - ? printOpeningTagEndMarker(node.parent) - : needsToBorrowPrevClosingTagEndMarker(node) - ? printClosingTagEndMarker(node.prev) - : ""; -} - -function printClosingTagPrefix(node) { - return needsToBorrowLastChildClosingTagEndMarker(node) - ? printClosingTagEndMarker(node.lastChild) - : ""; -} - -function printClosingTagSuffix(node) { - return needsToBorrowParentClosingTagStartMarker(node) - ? printClosingTagStartMarker(node.parent) - : needsToBorrowNextOpeningTagStartMarker(node) - ? printOpeningTagStartMarker(node.next) - : ""; -} - -function printOpeningTagStartMarker(node) { - switch (node.type) { - case "comment": - return ""; - case "ieConditionalComment": - return `[endif]-->`; - case "tag": - if (node.isSelfClosing) { - return "/>"; - } - // fall through - default: - return ">"; - } -} - -module.exports = { - preprocess, - print: genericPrint, - massageAstNode: clean, - embed, - hasPrettierIgnore -}; diff --git a/src/language-html/syntax-attribute.js b/src/language-html/syntax-attribute.js new file mode 100644 index 000000000..46419cab9 --- /dev/null +++ b/src/language-html/syntax-attribute.js @@ -0,0 +1,64 @@ +"use strict"; + +const { + builders: { concat, ifBreak, join, line } +} = require("../doc"); +const parseSrcset = require("parse-srcset"); + +function printImgSrcset(value) { + const srcset = parseSrcset(value, { + logger: { + error(message) { + throw new Error(message); + } + } + }); + + const hasW = srcset.some(src => src.w); + const hasH = srcset.some(src => src.h); + const hasX = srcset.some(src => src.d); + + if (hasW + hasH + hasX !== 1) { + throw new Error(`Mixed descriptor in srcset is not supported`); + } + + const key = hasW ? "w" : hasH ? "h" : "d"; + const unit = hasW ? "w" : hasH ? "h" : "x"; + + const getMax = values => Math.max.apply(Math, values); + + const urls = srcset.map(src => src.url); + const maxUrlLength = getMax(urls.map(url => url.length)); + + const descriptors = srcset + .map(src => src[key]) + .map(descriptor => (descriptor ? descriptor.toString() : "")); + const descriptorLeftLengths = descriptors.map(descriptor => { + const index = descriptor.indexOf("."); + return index === -1 ? descriptor.length : index; + }); + const maxDescriptorLeftLength = getMax(descriptorLeftLengths); + + return join( + concat([",", line]), + urls.map((url, index) => { + const parts = [url]; + + const descriptor = descriptors[index]; + if (descriptor) { + const urlPadding = maxUrlLength - url.length + 1; + const descriptorPadding = + maxDescriptorLeftLength - descriptorLeftLengths[index]; + + const alignment = " ".repeat(urlPadding + descriptorPadding); + parts.push(ifBreak(alignment, " "), descriptor + unit); + } + + return concat(parts); + }) + ); +} + +module.exports = { + printImgSrcset +}; diff --git a/src/language-html/syntax-vue.js b/src/language-html/syntax-vue.js new file mode 100644 index 000000000..234bacaf0 --- /dev/null +++ b/src/language-html/syntax-vue.js @@ -0,0 +1,72 @@ +"use strict"; + +const { + builders: { concat, group } +} = require("../doc"); + +/** + * v-for="... in ..." + * v-for="... of ..." + * v-for="(..., ...) in ..." + * v-for="(..., ...) of ..." + */ +function printVueFor(value, textToDoc) { + const { left, operator, right } = parseVueFor(value); + return concat([ + group( + textToDoc(`function _(${left}) {}`, { + parser: "babylon", + __isVueForBindingLeft: true + }) + ), + " ", + operator, + " ", + textToDoc(right, { parser: "__js_expression" }) + ]); +} + +// modified from https://github.com/vuejs/vue/blob/v2.5.17/src/compiler/parser/index.js#L370-L387 +function parseVueFor(value) { + const forAliasRE = /([^]*?)\s+(in|of)\s+([^]*)/; + const forIteratorRE = /,([^,}\]]*)(?:,([^,}\]]*))?$/; + const stripParensRE = /^\(|\)$/g; + + const inMatch = value.match(forAliasRE); + if (!inMatch) { + return; + } + const res = {}; + res.for = inMatch[3].trim(); + const alias = inMatch[1].trim().replace(stripParensRE, ""); + const iteratorMatch = alias.match(forIteratorRE); + if (iteratorMatch) { + res.alias = alias.replace(forIteratorRE, ""); + res.iterator1 = iteratorMatch[1].trim(); + if (iteratorMatch[2]) { + res.iterator2 = iteratorMatch[2].trim(); + } + } else { + res.alias = alias; + } + + return { + left: `${[res.alias, res.iterator1, res.iterator2] + .filter(Boolean) + .join(",")}`, + operator: inMatch[2], + right: res.for + }; +} + +function printVueSlotScope(value, textToDoc) { + return textToDoc(`function _(${value}) {}`, { + parser: "babylon", + __isVueSlotScope: true + }); +} + +module.exports = { + printVueFor, + printVueSlotScope +}; diff --git a/src/language-html/utils.js b/src/language-html/utils.js index 1b8eed5f3..cd50b31a1 100644 --- a/src/language-html/utils.js +++ b/src/language-html/utils.js @@ -18,40 +18,6 @@ const htmlElementAttributes = require("html-element-attributes"); const HTML_TAGS = arrayToMap(htmlTagNames); const HTML_ELEMENT_ATTRIBUTES = mapObject(htmlElementAttributes, arrayToMap); -// NOTE: must be same as the one in htmlparser2 so that the parsing won't be inconsistent -// https://github.com/fb55/htmlparser2/blob/v3.9.2/lib/Parser.js#L59-L91 -const VOID_TAGS = arrayToMap([ - "area", - "base", - "basefont", - "br", - "col", - "command", - "embed", - "frame", - "hr", - "img", - "input", - "isindex", - "keygen", - "link", - "meta", - "param", - "source", - "track", - "wbr", - - "path", - "circle", - "ellipse", - "line", - "rect", - "use", - "stop", - "polyline", - "polygon" -]); - function arrayToMap(array) { const map = Object.create(null); for (const value of array) { @@ -70,10 +36,18 @@ function mapObject(object, fn) { function hasPrettierIgnore(path) { const node = path.getValue(); - if (node.type === "attribute") { + if (node.type === "attribute" || node.type === "text") { return false; } + // TODO: handle non-text children in
+ if (
+ isPreLikeNode(node) &&
+ node.children.some(child => child.type !== "text")
+ ) {
+ return true;
+ }
+
const parentNode = path.getParentNode();
if (!parentNode) {
return false;
@@ -89,65 +63,118 @@ function hasPrettierIgnore(path) {
}
function isPrettierIgnore(node) {
- return node.type === "comment" && node.data.trim() === "prettier-ignore";
+ return node.type === "comment" && node.value.trim() === "prettier-ignore";
}
-function isTag(node) {
- return node.type === "tag";
+function getPrettierIgnoreAttributeCommentData(value) {
+ const match = value.trim().match(/^prettier-ignore-attribute(?:\s+([^]+))?$/);
+
+ if (!match) {
+ return false;
+ }
+
+ if (!match[1]) {
+ return true;
+ }
+
+ return match[1].split(/\s+/);
}
function isScriptLikeTag(node) {
- return isTag(node) && (node.name === "script" || node.name === "style");
+ return (
+ node.type === "element" &&
+ (node.fullName === "script" ||
+ node.fullName === "style" ||
+ node.fullName === "svg:style")
+ );
}
function isFrontMatterNode(node) {
return node.type === "yaml" || node.type === "toml";
}
-function isLeadingSpaceSensitiveNode(node, { prev, parent }) {
+function canHaveInterpolation(node) {
+ return node.children && !isScriptLikeTag(node);
+}
+
+function isWhitespaceSensitiveNode(node) {
+ return (
+ isScriptLikeTag(node) ||
+ node.type === "interpolation" ||
+ isIndentationSensitiveNode(node)
+ );
+}
+
+function isIndentationSensitiveNode(node) {
+ return getNodeCssStyleWhiteSpace(node).startsWith("pre");
+}
+
+function isLeadingSpaceSensitiveNode(node) {
if (isFrontMatterNode(node)) {
return false;
}
- if (!parent || parent.cssDisplay === "none") {
+ if (!node.parent || node.parent.cssDisplay === "none") {
return false;
}
if (
- !prev &&
- (parent.type === "root" ||
- isScriptLikeTag(parent) ||
- isBlockLikeCssDisplay(parent.cssDisplay))
+ !node.prev &&
+ node.parent.type === "element" &&
+ node.parent.tagDefinition.ignoreFirstLf
) {
return false;
}
- if (prev && isBlockLikeCssDisplay(prev.cssDisplay)) {
+ if (isPreLikeNode(node.parent)) {
+ return true;
+ }
+
+ if (
+ !node.prev &&
+ (node.parent.type === "root" ||
+ isScriptLikeTag(node.parent) ||
+ !isFirstChildLeadingSpaceSensitiveCssDisplay(node.parent.cssDisplay))
+ ) {
+ return false;
+ }
+
+ if (
+ node.prev &&
+ !isNextLeadingSpaceSensitiveCssDisplay(node.prev.cssDisplay)
+ ) {
return false;
}
return true;
}
-function isTrailingSpaceSensitiveNode(node, { next, parent }) {
+function isTrailingSpaceSensitiveNode(node) {
if (isFrontMatterNode(node)) {
return false;
}
- if (!parent || parent.cssDisplay === "none") {
+ if (!node.parent || node.parent.cssDisplay === "none") {
return false;
}
+ if (isPreLikeNode(node.parent)) {
+ return true;
+ }
+
if (
- !next &&
- (parent.type === "root" ||
- isScriptLikeTag(parent) ||
- isBlockLikeCssDisplay(parent.cssDisplay))
+ !node.next &&
+ (node.parent.type === "root" ||
+ isScriptLikeTag(node.parent) ||
+ !isLastChildTrailingSpaceSensitiveCssDisplay(node.parent.cssDisplay))
) {
return false;
}
- if (next && isBlockLikeCssDisplay(next.cssDisplay)) {
+ if (
+ node.next &&
+ !isPrevTrailingSpaceSensitiveCssDisplay(node.next.cssDisplay)
+ ) {
return false;
}
@@ -155,34 +182,10 @@ function isTrailingSpaceSensitiveNode(node, { next, parent }) {
}
function isDanglingSpaceSensitiveNode(node) {
- return !isBlockLikeCssDisplay(node.cssDisplay);
-}
-
-/**
- * @param {unknown} node
- * @param {(node: unknown, stack: Array)} fn
- * @param {unknown=} parent
- */
-function mapNode(node, fn, stack = []) {
- const newNode = Object.assign({}, node);
-
- if (newNode.children) {
- newNode.children = newNode.children.map((child, childIndex) =>
- mapNode(child, fn, [childIndex, node].concat(stack))
- );
- }
-
- return fn(newNode, stack);
-}
-
-function getPrevNode(stack) {
- const [index, parent] = stack;
-
- if (typeof index !== "number" || index === 0) {
- return null;
- }
-
- return parent.children[index - 1];
+ return (
+ isDanglingSpaceSensitiveCssDisplay(node.cssDisplay) &&
+ !isScriptLikeTag(node)
+ );
}
function replaceNewlines(text, replacement) {
@@ -202,16 +205,20 @@ function replaceDocNewlines(doc, replacement) {
}
function forceNextEmptyLine(node) {
- return isFrontMatterNode(node);
+ return (
+ isFrontMatterNode(node) ||
+ (node.next &&
+ node.sourceSpan.end.line + 1 < node.next.sourceSpan.start.line)
+ );
}
/** firstChild leadingSpaces and lastChild trailingSpaces */
function forceBreakContent(node) {
return (
forceBreakChildren(node) ||
- (isTag(node) &&
+ (node.type === "element" &&
node.children.length !== 0 &&
- (["body", "template"].indexOf(node.name) !== -1 ||
+ (["body", "template", "script", "style"].indexOf(node.name) !== -1 ||
node.children.some(child => hasNonTextChild(child))))
);
}
@@ -219,7 +226,7 @@ function forceBreakContent(node) {
/** spaces between children */
function forceBreakChildren(node) {
return (
- isTag(node) &&
+ node.type === "element" &&
node.children.length !== 0 &&
(["html", "head", "ul", "ol", "select"].indexOf(node.name) !== -1 ||
(node.cssDisplay.startsWith("table") && node.cssDisplay !== "table-cell"))
@@ -229,14 +236,48 @@ function forceBreakChildren(node) {
function preferHardlineAsLeadingSpaces(node) {
return (
preferHardlineAsSurroundingSpaces(node) ||
- (node.prev && preferHardlineAsTrailingSpaces(node.prev))
+ (node.prev && preferHardlineAsTrailingSpaces(node.prev)) ||
+ isCustomElementWithSurroundingLineBreak(node)
);
}
function preferHardlineAsTrailingSpaces(node) {
return (
preferHardlineAsSurroundingSpaces(node) ||
- (isTag(node) && node.name === "br")
+ (node.type === "element" && node.fullName === "br") ||
+ isCustomElementWithSurroundingLineBreak(node)
+ );
+}
+
+function isCustomElementWithSurroundingLineBreak(node) {
+ return isCustomElement(node) && hasSurroundingLineBreak(node);
+}
+
+function isCustomElement(node) {
+ return node.type === "element" && !node.namespace && node.name.includes("-");
+}
+
+function hasSurroundingLineBreak(node) {
+ return hasLeadingLineBreak(node) && hasTrailingLineBreak(node);
+}
+
+function hasLeadingLineBreak(node) {
+ return (
+ node.hasLeadingSpaces &&
+ (node.prev
+ ? node.prev.sourceSpan.end.line < node.sourceSpan.start.line
+ : node.parent.type === "root" ||
+ node.parent.startSourceSpan.end.line < node.sourceSpan.start.line)
+ );
+}
+
+function hasTrailingLineBreak(node) {
+ return (
+ node.hasTrailingSpaces &&
+ (node.next
+ ? node.next.sourceSpan.start.line > node.sourceSpan.end.line
+ : node.parent.type === "root" ||
+ node.parent.endSourceSpan.start.line > node.sourceSpan.end.line)
);
}
@@ -246,7 +287,7 @@ function preferHardlineAsSurroundingSpaces(node) {
case "comment":
case "directive":
return true;
- case "tag":
+ case "element":
return ["script", "select"].indexOf(node.name) !== -1;
}
return false;
@@ -261,54 +302,127 @@ function hasNonTextChild(node) {
}
function inferScriptParser(node) {
- if (
- node.name === "script" &&
- ((!node.attribs.lang && !node.attribs.type) ||
- node.attribs.type === "text/javascript" ||
- node.attribs.type === "text/babel" ||
- node.attribs.type === "application/javascript")
- ) {
- return "babylon";
- }
+ if (node.name === "script" && !node.attrMap.src) {
+ if (
+ (!node.attrMap.lang && !node.attrMap.type) ||
+ node.attrMap.type === "module" ||
+ node.attrMap.type === "text/javascript" ||
+ node.attrMap.type === "text/babel" ||
+ node.attrMap.type === "application/javascript"
+ ) {
+ return "babylon";
+ }
- if (
- node.name === "script" &&
- (node.attribs.type === "application/x-typescript" ||
- node.attribs.lang === "ts")
- ) {
- return "typescript";
+ if (
+ node.attrMap.type === "application/x-typescript" ||
+ node.attrMap.lang === "ts" ||
+ node.attrMap.lang === "tsx"
+ ) {
+ return "typescript";
+ }
+
+ if (node.attrMap.type === "text/markdown") {
+ return "markdown";
+ }
}
if (node.name === "style") {
- return "css";
+ if (!node.attrMap.lang || node.attrMap.lang === "postcss") {
+ return "css";
+ }
+
+ if (node.attrMap.lang === "scss") {
+ return "scss";
+ }
+
+ if (node.attrMap.lang === "less") {
+ return "less";
+ }
}
return null;
}
-/**
- * firstChild leadingSpaces, lastChild trailingSpaces, and danglingSpaces are insensitive
- */
function isBlockLikeCssDisplay(cssDisplay) {
- return cssDisplay === "block" || cssDisplay.startsWith("table");
+ return (
+ cssDisplay === "block" ||
+ cssDisplay === "list-item" ||
+ cssDisplay.startsWith("table")
+ );
}
-function getNodeCssStyleDisplay(node, prevNode, options) {
- switch (getNodeCssStyleWhiteSpace(node)) {
- case "pre":
- case "pre-wrap":
- // textarea-like
- return "block";
+function isFirstChildLeadingSpaceSensitiveCssDisplay(cssDisplay) {
+ return !isBlockLikeCssDisplay(cssDisplay) && cssDisplay !== "inline-block";
+}
+
+function isLastChildTrailingSpaceSensitiveCssDisplay(cssDisplay) {
+ return !isBlockLikeCssDisplay(cssDisplay) && cssDisplay !== "inline-block";
+}
+
+function isPrevTrailingSpaceSensitiveCssDisplay(cssDisplay) {
+ return !isBlockLikeCssDisplay(cssDisplay);
+}
+
+function isNextLeadingSpaceSensitiveCssDisplay(cssDisplay) {
+ return !isBlockLikeCssDisplay(cssDisplay);
+}
+
+function isDanglingSpaceSensitiveCssDisplay(cssDisplay) {
+ return !isBlockLikeCssDisplay(cssDisplay) && cssDisplay !== "inline-block";
+}
+
+function isPreLikeNode(node) {
+ return getNodeCssStyleWhiteSpace(node).startsWith("pre");
+}
+
+function countParents(path, predicate = () => true) {
+ let counter = 0;
+ for (let i = path.stack.length - 1; i >= 0; i--) {
+ const value = path.stack[i];
+ if (
+ value &&
+ typeof value === "object" &&
+ !Array.isArray(value) &&
+ predicate(value)
+ ) {
+ counter++;
+ }
+ }
+ return counter;
+}
+
+function hasParent(node, fn) {
+ let current = node;
+
+ while (current) {
+ if (fn(current)) {
+ return true;
+ }
+
+ current = current.parent;
}
- if (prevNode && prevNode.type === "comment") {
+ return false;
+}
+
+function getNodeCssStyleDisplay(node, options) {
+ if (node.prev && node.prev.type === "comment") {
//
- const match = prevNode.data.match(/^\s*display:\s*([a-z]+)\s*$/);
+ const match = node.prev.value.match(/^\s*display:\s*([a-z]+)\s*$/);
if (match) {
return match[1];
}
}
+ let isInSvgForeignObject = false;
+ if (node.type === "element" && node.namespace === "svg") {
+ if (hasParent(node, parent => parent.fullName === "svg:foreignObject")) {
+ isInSvgForeignObject = true;
+ } else {
+ return node.name === "svg" ? "inline-block" : "block";
+ }
+ }
+
switch (options.htmlWhitespaceSensitivity) {
case "strict":
return "inline";
@@ -316,21 +430,27 @@ function getNodeCssStyleDisplay(node, prevNode, options) {
return "block";
default:
return (
- (isTag(node) && CSS_DISPLAY_TAGS[node.name]) || CSS_DISPLAY_DEFAULT
+ (node.type === "element" &&
+ (!node.namespace || isInSvgForeignObject) &&
+ CSS_DISPLAY_TAGS[node.name]) ||
+ CSS_DISPLAY_DEFAULT
);
}
}
function getNodeCssStyleWhiteSpace(node) {
return (
- (isTag(node) && CSS_WHITE_SPACE_TAGS[node.name]) || CSS_WHITE_SPACE_DEFAULT
+ (node.type === "element" &&
+ !node.namespace &&
+ CSS_WHITE_SPACE_TAGS[node.name]) ||
+ CSS_WHITE_SPACE_DEFAULT
);
}
function getCommentData(node) {
- const rightTrimmedData = node.data.trimRight();
+ const rightTrimmedValue = node.value.trimRight();
- const hasLeadingEmptyLine = /^[^\S\n]*?\n/.test(node.data);
+ const hasLeadingEmptyLine = /^[^\S\n]*?\n/.test(node.value);
if (hasLeadingEmptyLine) {
/**
*
*/
- return dedentString(rightTrimmedData.replace(/^\s*\n/, ""));
+ return dedentString(rightTrimmedValue.replace(/^\s*\n/, ""));
}
/**
@@ -351,17 +471,19 @@ function getCommentData(node) {
*
* -->
*/
- if (!rightTrimmedData.includes("\n")) {
- return rightTrimmedData.trimLeft();
+ if (!rightTrimmedValue.includes("\n")) {
+ return rightTrimmedValue.trimLeft();
}
- const firstNewlineIndex = rightTrimmedData.indexOf("\n");
- const dataWithoutLeadingLine = rightTrimmedData.slice(firstNewlineIndex + 1);
+ const firstNewlineIndex = rightTrimmedValue.indexOf("\n");
+ const dataWithoutLeadingLine = rightTrimmedValue.slice(firstNewlineIndex + 1);
const minIndentationForDataWithoutLeadingLine = getMinIndentation(
dataWithoutLeadingLine
);
- const commentDataStartColumn = node.startLocation.column + "
*/
return (
- leadingLineData.trim() +
+ leadingLineValue.trim() +
"\n" +
dedentString(
dataWithoutLeadingLine,
@@ -392,6 +515,10 @@ function getMinIndentation(text) {
let minIndentation = Infinity;
for (const lineText of text.split("\n")) {
+ if (lineText.length === 0) {
+ continue;
+ }
+
if (/\S/.test(lineText[0])) {
return 0;
}
@@ -422,11 +549,19 @@ function dedentString(text, minIndent = getMinIndentation(text)) {
function normalizeParts(parts) {
const newParts = [];
- for (const part of parts) {
+ const restParts = parts.slice();
+ while (restParts.length !== 0) {
+ const part = restParts.shift();
+
if (!part) {
continue;
}
+ if (part.type === "concat") {
+ Array.prototype.unshift.apply(restParts, part.parts);
+ continue;
+ }
+
if (
newParts.length !== 0 &&
typeof newParts[newParts.length - 1] === "string" &&
@@ -442,10 +577,15 @@ function normalizeParts(parts) {
return newParts;
}
+function identity(x) {
+ return x;
+}
+
module.exports = {
HTML_ELEMENT_ATTRIBUTES,
HTML_TAGS,
- VOID_TAGS,
+ canHaveInterpolation,
+ countParents,
dedentString,
forceBreakChildren,
forceBreakContent,
@@ -454,15 +594,18 @@ module.exports = {
getLastDescendant,
getNodeCssStyleDisplay,
getNodeCssStyleWhiteSpace,
- getPrevNode,
+ getPrettierIgnoreAttributeCommentData,
hasPrettierIgnore,
+ identity,
inferScriptParser,
isDanglingSpaceSensitiveNode,
isFrontMatterNode,
+ isIndentationSensitiveNode,
isLeadingSpaceSensitiveNode,
+ isPreLikeNode,
isScriptLikeTag,
isTrailingSpaceSensitiveNode,
- mapNode,
+ isWhitespaceSensitiveNode,
normalizeParts,
preferHardlineAsLeadingSpaces,
preferHardlineAsTrailingSpaces,
diff --git a/src/language-js/clean.js b/src/language-js/clean.js
index 9c7bf0fb3..0c6e61bae 100644
--- a/src/language-js/clean.js
+++ b/src/language-js/clean.js
@@ -131,21 +131,33 @@ function clean(ast, newObj, parent) {
newObj.value.expression.quasis.forEach(q => delete q.value);
}
- // CSS template literals in Angular Component decorator
+ // Angular Components: Inline HTML template and Inline CSS styles
const expression = ast.expression || ast.callee;
if (
ast.type === "Decorator" &&
expression.type === "CallExpression" &&
expression.callee.name === "Component" &&
- expression.arguments.length === 1 &&
- expression.arguments[0].properties.some(
- prop =>
- prop.key.name === "styles" && prop.value.type === "ArrayExpression"
- )
+ expression.arguments.length === 1
) {
- newObj.expression.arguments[0].properties.forEach(prop => {
- if (prop.value.type === "ArrayExpression") {
- prop.value.elements[0].quasis.forEach(q => delete q.value);
+ const astProps = ast.expression.arguments[0].properties;
+ newObj.expression.arguments[0].properties.forEach((prop, index) => {
+ let templateLiteral = null;
+
+ switch (astProps[index].key.name) {
+ case "styles":
+ if (prop.value.type === "ArrayExpression") {
+ templateLiteral = prop.value.elements[0];
+ }
+ break;
+ case "template":
+ if (prop.value.type === "TemplateLiteral") {
+ templateLiteral = prop.value;
+ }
+ break;
+ }
+
+ if (templateLiteral) {
+ templateLiteral.quasis.forEach(q => delete q.value);
}
});
}
@@ -159,7 +171,8 @@ function clean(ast, newObj, parent) {
ast.tag.name === "graphql" ||
ast.tag.name === "css" ||
ast.tag.name === "md" ||
- ast.tag.name === "markdown")) ||
+ ast.tag.name === "markdown" ||
+ ast.tag.name === "html")) ||
ast.tag.type === "CallExpression")
) {
newObj.quasi.quasis.forEach(quasi => delete quasi.value);
@@ -170,14 +183,17 @@ function clean(ast, newObj, parent) {
// we will not trim the comment value and we will expect exactly one space on
// either side of the GraphQL string
// Also see ./embed.js
- const hasGraphQLComment =
+ const hasLanguageComment =
ast.leadingComments &&
ast.leadingComments.some(
comment =>
- comment.type === "CommentBlock" && comment.value === " GraphQL "
+ comment.type === "CommentBlock" &&
+ ["GraphQL", "HTML"].some(
+ languageName => comment.value === ` ${languageName} `
+ )
);
if (
- hasGraphQLComment ||
+ hasLanguageComment ||
(parent.type === "CallExpression" && parent.callee.name === "graphql")
) {
newObj.quasis.forEach(quasi => delete quasi.value);
diff --git a/src/language-js/embed.js b/src/language-js/embed.js
index f182ad27c..a47585906 100644
--- a/src/language-js/embed.js
+++ b/src/language-js/embed.js
@@ -8,6 +8,7 @@ const {
softline,
literalline,
concat,
+ group,
dedentToRoot
},
utils: { mapDoc, stripTrailingHardline }
@@ -132,6 +133,14 @@ function embed(path, print, textToDoc /*, options */) {
]);
}
+ if (isHtml(path)) {
+ return printHtmlTemplateLiteral(path, print, textToDoc, "html");
+ }
+
+ if (isAngularComponentTemplate(path)) {
+ return printHtmlTemplateLiteral(path, print, textToDoc, "angular");
+ }
+
break;
}
@@ -179,18 +188,6 @@ function embed(path, print, textToDoc /*, options */) {
}
}
-function isPropertyWithinAngularComponentDecorator(path, parentIndexToCheck) {
- const parent = path.getParentNode(parentIndexToCheck);
- return !!(
- parent &&
- parent.type === "Decorator" &&
- parent.expression &&
- parent.expression.type === "CallExpression" &&
- parent.expression.callee &&
- parent.expression.callee.name === "Component"
- );
-}
-
function getIndentation(str) {
const firstMatchedIndent = str.match(/^([^\S\n]*)\S/m);
return firstMatchedIndent === null ? "" : firstMatchedIndent[1];
@@ -361,9 +358,6 @@ function isStyledJsx(path) {
* ...which are both within template literals somewhere
* inside of the Component decorator factory.
*
- * TODO: Format HTML template once prettier's HTML
- * formatting is "ready"
- *
* E.g.
* @Component({
* template: `...`,
@@ -371,21 +365,42 @@ function isStyledJsx(path) {
* })
*/
function isAngularComponentStyles(path) {
- const parent = path.getParentNode();
- const parentParent = path.getParentNode(1);
- const isWithinArrayValueFromProperty = !!(
- parent &&
- (parent.type === "ArrayExpression" && parentParent.type === "Property")
+ return isPathMatch(
+ path,
+ [
+ node => node.type === "TemplateLiteral",
+ (node, name) => node.type === "ArrayExpression" && name === "elements",
+ (node, name) =>
+ node.type === "Property" &&
+ node.key.type === "Identifier" &&
+ node.key.name === "styles" &&
+ name === "value"
+ ].concat(getAngularComponentObjectExpressionPredicates())
);
- if (
- isWithinArrayValueFromProperty &&
- isPropertyWithinAngularComponentDecorator(path, 4)
- ) {
- if (parentParent.key && parentParent.key.name === "styles") {
- return true;
- }
- }
- return false;
+}
+function isAngularComponentTemplate(path) {
+ return isPathMatch(
+ path,
+ [
+ node => node.type === "TemplateLiteral",
+ (node, name) =>
+ node.type === "Property" &&
+ node.key.type === "Identifier" &&
+ node.key.name === "template" &&
+ name === "value"
+ ].concat(getAngularComponentObjectExpressionPredicates())
+ );
+}
+function getAngularComponentObjectExpressionPredicates() {
+ return [
+ (node, name) => node.type === "ObjectExpression" && name === "properties",
+ (node, name) =>
+ node.type === "CallExpression" &&
+ node.callee.type === "Identifier" &&
+ node.callee.name === "Component" &&
+ name === "arguments",
+ (node, name) => node.type === "Decorator" && name === "expression"
+ ];
}
/**
@@ -470,20 +485,8 @@ function isGraphQL(path) {
const node = path.getValue();
const parent = path.getParentNode();
- // This checks for a leading comment that is exactly `/* GraphQL */`
- // In order to be in line with other implementations of this comment tag
- // we will not trim the comment value and we will expect exactly one space on
- // either side of the GraphQL string
- // Also see ./clean.js
- const hasGraphQLComment =
- node.leadingComments &&
- node.leadingComments.some(
- comment =>
- comment.type === "CommentBlock" && comment.value === " GraphQL "
- );
-
return (
- hasGraphQLComment ||
+ hasLanguageComment(node, "GraphQL") ||
(parent &&
((parent.type === "TaggedTemplateExpression" &&
((parent.tag.type === "MemberExpression" &&
@@ -497,4 +500,133 @@ function isGraphQL(path) {
);
}
+function hasLanguageComment(node, languageName) {
+ // This checks for a leading comment that is exactly `/* GraphQL */`
+ // In order to be in line with other implementations of this comment tag
+ // we will not trim the comment value and we will expect exactly one space on
+ // either side of the GraphQL string
+ // Also see ./clean.js
+ return (
+ node.leadingComments &&
+ node.leadingComments.some(
+ comment =>
+ comment.type === "CommentBlock" && comment.value === ` ${languageName} `
+ )
+ );
+}
+
+function isPathMatch(path, predicateStack) {
+ const stack = path.stack.slice();
+
+ let name = null;
+ let node = stack.pop();
+
+ for (const predicate of predicateStack) {
+ if (node === undefined) {
+ return false;
+ }
+
+ // skip index/array
+ if (typeof name === "number") {
+ name = stack.pop();
+ node = stack.pop();
+ }
+
+ if (!predicate(node, name)) {
+ return false;
+ }
+
+ name = stack.pop();
+ node = stack.pop();
+ }
+
+ return true;
+}
+
+/**
+ * - html`...`
+ * - HTML comment block
+ */
+function isHtml(path) {
+ const node = path.getValue();
+ return (
+ hasLanguageComment(node, "HTML") ||
+ isPathMatch(path, [
+ node => node.type === "TemplateLiteral",
+ (node, name) =>
+ node.type === "TaggedTemplateExpression" &&
+ node.tag.type === "Identifier" &&
+ node.tag.name === "html" &&
+ name === "quasi"
+ ])
+ );
+}
+
+function printHtmlTemplateLiteral(path, print, textToDoc, parser) {
+ const node = path.getValue();
+
+ const placeholderPattern =
+ "prettierhtmlplaceholder(\\d+)redlohecalplmthreitterp";
+ const placeholders = node.expressions.map(
+ (_, i) => `prettierhtmlplaceholder${i}redlohecalplmthreitterp`
+ );
+
+ const text = node.quasis
+ .map(
+ (quasi, index, quasis) =>
+ index === quasis.length - 1
+ ? quasi.value.raw
+ : quasi.value.raw + placeholders[index]
+ )
+ .join("");
+
+ const expressionDocs = path.map(print, "expressions");
+
+ const contentDoc = mapDoc(
+ stripTrailingHardline(textToDoc(text, { parser })),
+ doc => {
+ const placeholderRegex = new RegExp(placeholderPattern, "g");
+ const hasPlaceholder =
+ typeof doc === "string" && placeholderRegex.test(doc);
+
+ if (!hasPlaceholder) {
+ return doc;
+ }
+
+ const parts = [];
+
+ const components = doc.split(placeholderRegex);
+ for (let i = 0; i < components.length; i++) {
+ const component = components[i];
+
+ if (i % 2 === 0) {
+ if (component) {
+ parts.push(component);
+ }
+ continue;
+ }
+
+ const placeholderIndex = +component;
+
+ parts.push(
+ concat([
+ "${",
+ group(
+ concat([
+ indent(concat([softline, expressionDocs[placeholderIndex]])),
+ softline
+ ])
+ ),
+ "}"
+ ])
+ );
+ }
+
+ return concat(parts);
+ }
+ );
+
+ return concat(["`", indent(concat([hardline, contentDoc])), softline, "`"]);
+}
+
module.exports = embed;
diff --git a/src/language-js/html-binding.js b/src/language-js/html-binding.js
new file mode 100644
index 000000000..411a2e998
--- /dev/null
+++ b/src/language-js/html-binding.js
@@ -0,0 +1,50 @@
+"use strict";
+
+const {
+ builders: { concat, join, line }
+} = require("../doc");
+
+function printHtmlBinding(path, options, print) {
+ const node = path.getValue();
+
+ if (options.__onHtmlBindingRoot && path.getName() === null) {
+ options.__onHtmlBindingRoot(node);
+ }
+
+ if (node.type !== "File") {
+ return;
+ }
+
+ if (options.__isVueForBindingLeft) {
+ return path.call(
+ functionDeclarationPath => {
+ const { params } = functionDeclarationPath.getValue();
+ return concat([
+ params.length > 1 ? "(" : "",
+ join(
+ concat([",", line]),
+ functionDeclarationPath.map(print, "params")
+ ),
+ params.length > 1 ? ")" : ""
+ ]);
+ },
+ "program",
+ "body",
+ 0
+ );
+ }
+
+ if (options.__isVueSlotScope) {
+ return path.call(
+ functionDeclarationPath =>
+ join(concat([",", line]), functionDeclarationPath.map(print, "params")),
+ "program",
+ "body",
+ 0
+ );
+ }
+}
+
+module.exports = {
+ printHtmlBinding
+};
diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js
index 53144ec55..4d65e9298 100644
--- a/src/language-js/needs-parens.js
+++ b/src/language-js/needs-parens.js
@@ -500,6 +500,8 @@ function needsParens(path, options) {
return false;
} else if (parent.type === "Property" && parent.value === node) {
return false;
+ } else if (parent.type === "NGChainedExpression") {
+ return false;
}
return true;
}
@@ -613,6 +615,21 @@ function needsParens(path, options) {
return true;
}
return false;
+ case "NGPipeExpression":
+ if (
+ parent.type === "NGRoot" ||
+ parent.type === "ObjectProperty" ||
+ parent.type === "ArrayExpression" ||
+ ((parent.type === "CallExpression" ||
+ parent.type === "OptionalCallExpression") &&
+ parent.arguments[name] === node) ||
+ (parent.type === "NGPipeExpression" && name === "right") ||
+ (parent.type === "MemberExpression" && name === "property") ||
+ parent.type === "AssignmentExpression"
+ ) {
+ return false;
+ }
+ return true;
}
return false;
diff --git a/src/language-js/parser-angular.js b/src/language-js/parser-angular.js
new file mode 100644
index 000000000..1055ab50e
--- /dev/null
+++ b/src/language-js/parser-angular.js
@@ -0,0 +1,30 @@
+"use strict";
+
+const locFns = require("./loc");
+
+function createParser(_parse) {
+ const parse = (text, parsers, options) => {
+ const ngEstreeParser = require("angular-estree-parser");
+ const node = _parse(text, ngEstreeParser);
+ return {
+ type: "NGRoot",
+ node:
+ options.parser === "__ng_action" && node.type !== "NGChainedExpression"
+ ? Object.assign({}, node, {
+ type: "NGChainedExpression",
+ expressions: [node]
+ })
+ : node
+ };
+ };
+ return Object.assign({ astFormat: "estree", parse }, locFns);
+}
+
+module.exports = {
+ parsers: {
+ __ng_action: createParser((text, ng) => ng.parseAction(text)),
+ __ng_binding: createParser((text, ng) => ng.parseBinding(text)),
+ __ng_interpolation: createParser((text, ng) => ng.parseInterpolation(text)),
+ __ng_directive: createParser((text, ng) => ng.parseTemplateBindings(text))
+ }
+};
diff --git a/src/language-js/parser-babylon.js b/src/language-js/parser-babylon.js
index 058733a8e..aa26b2783 100644
--- a/src/language-js/parser-babylon.js
+++ b/src/language-js/parser-babylon.js
@@ -180,7 +180,9 @@ module.exports = {
},
locFns
),
- /** @internal for mdx to print jsx without semicolon */
- __js_expression: babylon
+ /** @internal */
+ __js_expression: babylon,
+ /** for vue filter */
+ __vue_expression: babylon
}
};
diff --git a/src/language-js/preprocess.js b/src/language-js/preprocess.js
index 3548cd85d..57489c7b7 100644
--- a/src/language-js/preprocess.js
+++ b/src/language-js/preprocess.js
@@ -5,9 +5,12 @@ function preprocess(ast, options) {
case "json":
case "json5":
case "json-stringify":
+ case "__js_expression":
+ case "__vue_expression":
return Object.assign({}, ast, {
- type: "JsonRoot",
- node: Object.assign({}, ast, { comments: [] })
+ type: options.parser.startsWith("__") ? "JsExpressionRoot" : "JsonRoot",
+ node: ast,
+ comments: []
});
default:
return ast;
diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js
index 72787b0ae..515eda801 100644
--- a/src/language-js/printer-estree.js
+++ b/src/language-js/printer-estree.js
@@ -34,7 +34,9 @@ const insertPragma = require("./pragma").insertPragma;
const handleComments = require("./comments");
const pathNeedsParens = require("./needs-parens");
const preprocess = require("./preprocess");
+const { printHtmlBinding } = require("./html-binding");
const {
+ hasNode,
hasFlowAnnotationComment,
hasFlowShorthandAnnotationComment
} = require("./utils");
@@ -390,8 +392,15 @@ function printPathNoParens(path, options, print, args) {
return n;
}
+ const htmlBinding = printHtmlBinding(path, options, print);
+ if (htmlBinding) {
+ return htmlBinding;
+ }
+
let parts = [];
switch (n.type) {
+ case "JsExpressionRoot":
+ return path.call(print, "node");
case "JsonRoot":
return concat([path.call(print, "node"), hardline]);
case "File":
@@ -464,7 +473,8 @@ function printPathNoParens(path, options, print, args) {
options
);
case "BinaryExpression":
- case "LogicalExpression": {
+ case "LogicalExpression":
+ case "NGPipeExpression": {
const parent = path.getParentNode();
const parentParent = path.getParentNode(1);
const isInsideParenthesis =
@@ -519,6 +529,11 @@ function printPathNoParens(path, options, print, args) {
parent.type === "ReturnStatement" ||
(parent.type === "JSXExpressionContainer" &&
parentParent.type === "JSXAttribute") ||
+ (n.type !== "NGPipeExpression" &&
+ ((parent.type === "NGRoot" && options.parser === "__ng_binding") ||
+ (parent.type === "NGMicrosyntaxExpression" &&
+ parentParent.type === "NGMicrosyntax" &&
+ parentParent.body.length === 1))) ||
(n === parent.body && parent.type === "ArrowFunctionExpression") ||
(n !== parent.body && parent.type === "ForStatement") ||
(parent.type === "ConditionalExpression" &&
@@ -3386,12 +3401,108 @@ function printPathNoParens(path, options, print, args) {
return concat(parts);
+ case "NGRoot":
+ return concat(
+ [].concat(
+ path.call(print, "node"),
+ !n.node.comments || n.node.comments.length === 0
+ ? []
+ : concat([" //", n.node.comments[0].value.trimRight()])
+ )
+ );
+ case "NGChainedExpression":
+ return group(
+ join(
+ concat([";", line]),
+ path.map(
+ childPath =>
+ hasNgSideEffect(childPath)
+ ? print(childPath)
+ : concat(["(", print(childPath), ")"]),
+ "expressions"
+ )
+ )
+ );
+ case "NGEmptyExpression":
+ return "";
+ case "NGQuotedExpression":
+ return concat([n.prefix, ":", n.value]);
+ case "NGMicrosyntax":
+ return concat(
+ path.map(
+ (childPath, index) =>
+ concat([
+ index === 0
+ ? ""
+ : isNgForOf(childPath)
+ ? " "
+ : concat([";", line]),
+ print(childPath)
+ ]),
+ "body"
+ )
+ );
+ case "NGMicrosyntaxKey":
+ return /^[a-z_$][a-z0-9_$]*(-[a-z_$][a-z0-9_$])*$/i.test(n.name)
+ ? n.name
+ : JSON.stringify(n.name);
+ case "NGMicrosyntaxExpression":
+ return concat([
+ path.call(print, "expression"),
+ n.alias === null ? "" : concat([" as ", path.call(print, "alias")])
+ ]);
+ case "NGMicrosyntaxKeyedExpression":
+ return concat([
+ path.call(print, "key"),
+ isNgForOf(path) ? " " : ": ",
+ path.call(print, "expression")
+ ]);
+ case "NGMicrosyntaxLet":
+ return concat([
+ "let ",
+ path.call(print, "key"),
+ n.value === null ? "" : concat([" = ", path.call(print, "value")])
+ ]);
+ case "NGMicrosyntaxAs":
+ return concat([
+ path.call(print, "key"),
+ " as ",
+ path.call(print, "alias")
+ ]);
default:
/* istanbul ignore next */
throw new Error("unknown type: " + JSON.stringify(n.type));
}
}
+/** prefer `let hero of heros` over `let hero; of: heros` */
+function isNgForOf(path) {
+ const node = path.getValue();
+ const index = path.getName();
+ const parentNode = path.getParentNode();
+ return (
+ node.type === "NGMicrosyntaxKeyedExpression" &&
+ node.key.name === "of" &&
+ index === 1 &&
+ parentNode.body[0].type === "NGMicrosyntaxLet" &&
+ parentNode.body[0].value === null
+ );
+}
+
+/** identify if an angular expression seems to have side effects */
+function hasNgSideEffect(path) {
+ return hasNode(path.getValue(), node => {
+ switch (node.type) {
+ case undefined:
+ return false;
+ case "CallExpression":
+ case "OptionalCallExpression":
+ case "AssignmentExpression":
+ return true;
+ }
+ });
+}
+
function printStatementSequence(path, options, print) {
const printed = [];
@@ -5302,7 +5413,8 @@ function maybeWrapJSXElementInParens(path, elem) {
ExpressionStatement: true,
CallExpression: true,
OptionalCallExpression: true,
- ConditionalExpression: true
+ ConditionalExpression: true,
+ JsExpressionRoot: true
};
if (NO_WRAP_PARENTS[parent.type]) {
return elem;
@@ -5327,7 +5439,11 @@ function maybeWrapJSXElementInParens(path, elem) {
}
function isBinaryish(node) {
- return node.type === "BinaryExpression" || node.type === "LogicalExpression";
+ return (
+ node.type === "BinaryExpression" ||
+ node.type === "LogicalExpression" ||
+ node.type === "NGPipeExpression"
+ );
}
function isMemberish(node) {
@@ -5414,16 +5530,36 @@ function printBinaryishExpressions(
const shouldInline = shouldInlineLogicalExpression(node);
const lineBeforeOperator =
- node.operator === "|>" &&
+ (node.operator === "|>" ||
+ node.type === "NGPipeExpression" ||
+ (node.operator === "|" && options.parser === "__vue_expression")) &&
!hasLeadingOwnLineComment(options.originalText, node.right, options);
+ const operator = node.type === "NGPipeExpression" ? "|" : node.operator;
+ const rightSuffix =
+ node.type === "NGPipeExpression" && node.arguments.length !== 0
+ ? group(
+ indent(
+ concat([
+ softline,
+ ": ",
+ join(
+ concat([softline, ":", ifBreak(" ")]),
+ path.map(print, "arguments").map(arg => align(2, group(arg)))
+ )
+ ])
+ )
+ )
+ : "";
+
const right = shouldInline
- ? concat([node.operator, " ", path.call(print, "right")])
+ ? concat([operator, " ", path.call(print, "right"), rightSuffix])
: concat([
lineBeforeOperator ? softline : "",
- node.operator,
+ operator,
lineBeforeOperator ? " " : line,
- path.call(print, "right")
+ path.call(print, "right"),
+ rightSuffix
]);
// If there's only a single binary expression, we want to create a group
@@ -5567,6 +5703,7 @@ function hasNakedLeftSide(node) {
node.type === "AssignmentExpression" ||
node.type === "BinaryExpression" ||
node.type === "LogicalExpression" ||
+ node.type === "NGPipeExpression" ||
node.type === "ConditionalExpression" ||
node.type === "CallExpression" ||
node.type === "OptionalCallExpression" ||
diff --git a/src/language-js/utils.js b/src/language-js/utils.js
index b8a7a946f..5721041ba 100644
--- a/src/language-js/utils.js
+++ b/src/language-js/utils.js
@@ -31,7 +31,21 @@ function hasFlowAnnotationComment(comments) {
return comments && comments[0].value.match(FLOW_ANNOTATION);
}
+function hasNode(node, fn) {
+ if (!node || typeof node !== "object") {
+ return false;
+ }
+ if (Array.isArray(node)) {
+ return node.some(value => hasNode(value, fn));
+ }
+ const result = fn(node);
+ return typeof result === "boolean"
+ ? result
+ : Object.keys(node).some(key => hasNode(node[key], fn));
+}
+
module.exports = {
+ hasNode,
hasFlowShorthandAnnotationComment,
hasFlowAnnotationComment
};
diff --git a/src/language-vue/embed.js b/src/language-vue/embed.js
deleted file mode 100644
index 119e98987..000000000
--- a/src/language-vue/embed.js
+++ /dev/null
@@ -1,48 +0,0 @@
-"use strict";
-
-const { concat, hardline } = require("../doc").builders;
-
-function embed(path, print, textToDoc, options) {
- const node = path.getValue();
- const parent = path.getParentNode();
- if (!parent || parent.tag !== "root" || node.unary) {
- return null;
- }
-
- let parser;
-
- if (node.tag === "style") {
- const langAttr = node.attrs.find(attr => attr.name === "lang");
- if (!langAttr || langAttr.value === "postcss") {
- parser = "css";
- } else if (langAttr.value === "scss") {
- parser = "scss";
- } else if (langAttr.value === "less") {
- parser = "less";
- }
- }
-
- if (node.tag === "script" && !node.attrs.some(attr => attr.name === "src")) {
- const langAttr = node.attrs.find(attr => attr.name === "lang");
- if (!langAttr) {
- parser = "babylon";
- } else if (langAttr.value === "ts" || langAttr.value === "tsx") {
- parser = "typescript";
- }
- }
-
- if (!parser) {
- return null;
- }
-
- return concat([
- options.originalText.slice(node.start, node.contentStart),
- hardline,
- textToDoc(options.originalText.slice(node.contentStart, node.contentEnd), {
- parser
- }),
- options.originalText.slice(node.contentEnd, node.end)
- ]);
-}
-
-module.exports = embed;
diff --git a/src/language-vue/index.js b/src/language-vue/index.js
deleted file mode 100644
index c6ba7dcd7..000000000
--- a/src/language-vue/index.js
+++ /dev/null
@@ -1,23 +0,0 @@
-"use strict";
-
-const printer = require("./printer-vue");
-const createLanguage = require("../utils/create-language");
-
-const languages = [
- createLanguage(require("linguist-languages/data/vue"), {
- override: {
- since: "1.10.0",
- parsers: ["vue"],
- vscodeLanguageIds: ["vue"]
- }
- })
-];
-
-const printers = {
- vue: printer
-};
-
-module.exports = {
- languages,
- printers
-};
diff --git a/src/language-vue/parser-vue.js b/src/language-vue/parser-vue.js
deleted file mode 100644
index 7dce2d762..000000000
--- a/src/language-vue/parser-vue.js
+++ /dev/null
@@ -1,425 +0,0 @@
-"use strict";
-
-const { hasPragma } = require("./pragma");
-
-/*!
- * Extracted from vue codebase
- * https://github.com/vuejs/vue/blob/cfd73c2386623341fdbb3ac636c4baf84ea89c2c/src/compiler/parser/html-parser.js
- * HTML Parser By John Resig (ejohn.org)
- * Modified by Juriy "kangax" Zaytsev
- * Original code by Erik Arvidsson, Mozilla Public License
- * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
- */
-
-/**
- * Make a map and return a function for checking if a key
- * is in that map.
- */
-function makeMap(str, expectsLowerCase) {
- const map = Object.create(null);
- const list = str.split(",");
- const listLength = list.length;
- for (let i = 0; i < listLength; i++) {
- map[list[i]] = true;
- }
- return expectsLowerCase ? val => map[val.toLowerCase()] : val => map[val];
-}
-
-/**
- * Always return false.
- */
-const no = () => false;
-
-// HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3
-// Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content
-const isNonPhrasingTag = makeMap(
- "address,article,aside,base,blockquote,body,caption,col,colgroup,dd," +
- "details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form," +
- "h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta," +
- "optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead," +
- "title,tr,track"
-);
-
-// Regular Expressions for parsing tags and attributes
-const attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
-// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
-// but for Vue templates we can enforce a simple charset
-const ncname = "[a-zA-Z_][\\w\\-\\.]*";
-const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
-const startTagOpen = new RegExp(`^<${qnameCapture}`);
-const startTagClose = /^\s*(\/?)>/;
-const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
-const doctype = /^]+>/i;
-const comment = /^");
-
- if (commentEnd >= 0) {
- if (options.shouldKeepComment) {
- options.comment(html.substring(4, commentEnd));
- }
- advance(commentEnd + 3);
- continue;
- }
- }
-
- // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
- if (conditionalComment.test(html)) {
- const conditionalEnd = html.indexOf("]>");
-
- if (conditionalEnd >= 0) {
- advance(conditionalEnd + 2);
- continue;
- }
- }
-
- // Doctype:
- const doctypeMatch = html.match(doctype);
- if (doctypeMatch) {
- advance(doctypeMatch[0].length);
- continue;
- }
-
- // End tag:
- const endTagMatch = html.match(endTag);
- if (endTagMatch) {
- const curIndex = index;
- advance(endTagMatch[0].length);
- parseEndTag(endTagMatch[1], curIndex, index);
- continue;
- }
-
- // Start tag:
- const startTagMatch = parseStartTag();
- if (startTagMatch) {
- handleStartTag(startTagMatch);
- if (shouldIgnoreFirstNewline(lastTag, html)) {
- advance(1);
- }
- continue;
- }
- }
-
- let text;
- let rest;
- let next;
-
- if (textEnd >= 0) {
- rest = html.slice(textEnd);
- while (
- !endTag.test(rest) &&
- !startTagOpen.test(rest) &&
- !comment.test(rest) &&
- !conditionalComment.test(rest)
- ) {
- // < in plain text, be forgiving and treat it as text
- next = rest.indexOf("<", 1);
- if (next < 0) {
- break;
- }
- textEnd += next;
- rest = html.slice(textEnd);
- }
- text = html.substring(0, textEnd);
- advance(textEnd);
- }
-
- if (textEnd < 0) {
- text = html;
- html = "";
- }
-
- if (options.chars && text) {
- options.chars(text);
- }
- } else {
- let endTagLength = 0;
- const stackedTag = lastTag.toLowerCase();
- const reStackedTag =
- reCache[stackedTag] ||
- (reCache[stackedTag] = new RegExp(
- "([\\s\\S]*?)(" + stackedTag + "[^>]*>)",
- "i"
- ));
- const rest = html.replace(reStackedTag, (all, text, endTag) => {
- endTagLength = endTag.length;
- if (!isPlainTextElement(stackedTag) && stackedTag !== "noscript") {
- text = text
- .replace(//g, "$1")
- .replace(//g, "$1");
- }
- if (shouldIgnoreFirstNewline(stackedTag, text)) {
- text = text.slice(1);
- }
- if (options.chars) {
- options.chars(text);
- }
- return "";
- });
- index += html.length - rest.length;
- html = rest;
- parseEndTag(stackedTag, index - endTagLength, index);
- }
-
- if (html === last) {
- options.chars && options.chars(html);
- if (
- process.env.NODE_ENV !== "production" &&
- !stack.length &&
- options.warn
- ) {
- options.warn(`Mal-formatted tag at end of template: "${html}"`);
- }
- break;
- }
- }
-
- // Clean up any remaining tags
- parseEndTag();
-
- function advance(n) {
- index += n;
- html = html.substring(n);
- }
-
- function parseStartTag() {
- const start = html.match(startTagOpen);
- if (start) {
- const match = {
- tagName: start[1],
- attrs: [],
- start: index
- };
- advance(start[0].length);
- let end;
- let attr;
- while (
- !(end = html.match(startTagClose)) &&
- (attr = html.match(attribute))
- ) {
- advance(attr[0].length);
- match.attrs.push(attr);
- }
- if (end) {
- match.unarySlash = end[1];
- advance(end[0].length);
- match.end = index;
- return match;
- }
- }
- }
-
- function handleStartTag(match) {
- const { tagName, unarySlash } = match;
-
- if (expectHTML) {
- if (lastTag === "p" && isNonPhrasingTag(tagName)) {
- parseEndTag(lastTag);
- }
- if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
- parseEndTag(tagName);
- }
- }
-
- const unary = isUnaryTag(tagName) || !!unarySlash;
-
- const l = match.attrs.length;
- const attrs = new Array(l);
- for (let i = 0; i < l; i++) {
- const args = match.attrs[i];
- // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
- if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
- if (args[3] === "") {
- delete args[3];
- }
- if (args[4] === "") {
- delete args[4];
- }
- if (args[5] === "") {
- delete args[5];
- }
- }
- const value = args[3] || args[4] || args[5] || "";
- const shouldDecodeNewlines =
- tagName === "a" && args[1] === "href"
- ? options.shouldDecodeNewlinesForHref
- : options.shouldDecodeNewlines;
- attrs[i] = {
- name: args[1],
- value: decodeAttr(value, shouldDecodeNewlines)
- };
- }
-
- if (!unary) {
- stack.push({
- tag: tagName,
- lowerCasedTag: tagName.toLowerCase(),
- attrs: attrs
- });
- lastTag = tagName;
- }
-
- if (options.start) {
- options.start(tagName, attrs, unary, match.start, match.end);
- }
- }
-
- function parseEndTag(tagName, start, end) {
- let pos;
- let lowerCasedTagName;
- if (start == null) {
- start = index;
- }
- if (end == null) {
- end = index;
- }
-
- if (tagName) {
- lowerCasedTagName = tagName.toLowerCase();
- }
-
- // Find the closest opened tag of the same type
- if (tagName) {
- for (pos = stack.length - 1; pos >= 0; pos--) {
- if (stack[pos].lowerCasedTag === lowerCasedTagName) {
- break;
- }
- }
- } else {
- // If no tag name is provided, clean shop
- pos = 0;
- }
-
- if (pos >= 0) {
- // Close all the open elements, up the stack
- for (let i = stack.length - 1; i >= pos; i--) {
- if (
- process.env.NODE_ENV !== "production" &&
- (i > pos || !tagName) &&
- options.warn
- ) {
- options.warn(`tag <${stack[i].tag}> has no matching end tag.`);
- }
- if (options.end) {
- options.end(stack[i].tag, start, end);
- }
- }
-
- // Remove the open elements from the stack
- stack.length = pos;
- lastTag = pos && stack[pos - 1].tag;
- } else if (lowerCasedTagName === "br") {
- if (options.start) {
- options.start(tagName, [], true, start, end);
- }
- } else if (lowerCasedTagName === "p") {
- if (options.start) {
- options.start(tagName, [], false, start, end);
- }
- if (options.end) {
- options.end(tagName, start, end);
- }
- }
- }
-}
-
-function parse(text /*, parsers, opts*/) {
- const rootObj = {
- tag: "root",
- attrs: [],
- unary: false,
- start: 0,
- contentStart: 0,
- contentEnd: text.length,
- end: text.length,
- children: [],
- comments: []
- };
- const objStack = [rootObj];
- let obj = rootObj;
- parseHTML(text, {
- start: function(tag, attrs, unary, start, end) {
- const newObj = {
- tag,
- attrs,
- unary,
- start,
- children: []
- };
- obj.children.push(newObj);
- if (unary) {
- newObj.end = end;
- } else {
- newObj.contentStart = end;
- objStack.push(newObj);
- obj = newObj;
- }
- },
- end: function(tag, start, end) {
- objStack.pop();
- obj.contentEnd = start;
- obj.end = end;
- obj = objStack[objStack.length - 1];
- }
- });
- return rootObj;
-}
-
-module.exports = {
- parsers: {
- vue: {
- parse,
- hasPragma,
- astFormat: "vue",
- locStart: node => node.start,
- locEnd: node => node.end
- }
- }
-};
diff --git a/src/language-vue/printer-vue.js b/src/language-vue/printer-vue.js
deleted file mode 100644
index 5a8f54c0f..000000000
--- a/src/language-vue/printer-vue.js
+++ /dev/null
@@ -1,45 +0,0 @@
-"use strict";
-
-const embed = require("./embed");
-const { concat, hardline } = require("../doc").builders;
-const { insertPragma } = require("./pragma");
-
-function genericPrint(path, options, print) {
- const n = path.getValue();
- const res = [];
- let index = n.start;
-
- path.each(childPath => {
- const child = childPath.getValue();
- res.push(options.originalText.slice(index, child.start));
- res.push(childPath.call(print));
- index = child.end;
- }, "children");
-
- // If there are no children, we just print the node from start to end.
- // Otherwise, index should point to the end of the last child, and we
- // need to print the closing tag.
- res.push(options.originalText.slice(index, n.end));
-
- // Only force a trailing newline if there were any contents.
- if (n.tag === "root" && n.children.length) {
- res.push(hardline);
- }
-
- return concat(res);
-}
-
-const clean = (ast, newObj) => {
- delete newObj.start;
- delete newObj.end;
- delete newObj.contentStart;
- delete newObj.contentEnd;
-};
-
-module.exports = {
- print: genericPrint,
- embed,
- insertPragma,
- massageAstNode: clean,
- canAttachComment: node => typeof node.tag === "string"
-};
diff --git a/src/main/comments.js b/src/main/comments.js
index 3a784fbc8..e32146e37 100644
--- a/src/main/comments.js
+++ b/src/main/comments.js
@@ -181,11 +181,19 @@ function attach(comments, ast, text, options) {
comments.forEach((comment, i) => {
if (
- (options.parser === "json" || options.parser === "json5") &&
- locStart(comment) - locStart(ast) <= 0
+ options.parser === "json" ||
+ options.parser === "json5" ||
+ options.parser === "__js_expression" ||
+ options.parser === "__vue_expression"
) {
- addLeadingComment(ast, comment);
- return;
+ if (locStart(comment) - locStart(ast) <= 0) {
+ addLeadingComment(ast, comment);
+ return;
+ }
+ if (locEnd(comment) - locEnd(ast) >= 0) {
+ addTrailingComment(ast, comment);
+ return;
+ }
}
decorateComment(ast, comment, options);
diff --git a/src/main/core-options.js b/src/main/core-options.js
index 4049f4aa8..ab88c571a 100644
--- a/src/main/core-options.js
+++ b/src/main/core-options.js
@@ -122,7 +122,8 @@ const options = {
since: null,
description: "Handlebars"
},
- { value: "html", since: "1.15.0", description: "HTML" }
+ { value: "html", since: "1.15.0", description: "HTML" },
+ { value: "angular", since: "1.15.0", description: "Angular" }
]
},
plugins: {
diff --git a/src/standalone.js b/src/standalone.js
index 6ce2a0fb7..0aaf0d10e 100644
--- a/src/standalone.js
+++ b/src/standalone.js
@@ -15,7 +15,6 @@ const internalPlugins = [
require("./language-html"),
require("./language-js"),
require("./language-markdown"),
- require("./language-vue"),
require("./language-yaml")
];
diff --git a/tests/angular_component_examples/__snapshots__/jsfmt.spec.js.snap b/tests/angular_component_examples/__snapshots__/jsfmt.spec.js.snap
index 267a4eb94..4b5abc04c 100644
--- a/tests/angular_component_examples/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/angular_component_examples/__snapshots__/jsfmt.spec.js.snap
@@ -21,8 +21,10 @@ class TestComponent {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@Component({
selector: "app-test",
- template: \` - test
-
+ template: \`
+
+ - test
+
\`,
styles: [
\`
@@ -60,8 +62,10 @@ class TestComponent {}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@Component({
selector: "app-test",
- template: \` - test
-
+ template: \`
+
+ - test
+
\`,
styles: [
\`
diff --git a/tests/angular_interpolation/__snapshots__/jsfmt.spec.js.snap b/tests/angular_interpolation/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000..8885a5343
--- /dev/null
+++ b/tests/angular_interpolation/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,68 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`logical-expression.ng - __ng_interpolation-verify 1`] = `
+[
+ advancedSearchService.patientInformationFieldsRow2 && advancedSearchService.patientInformationFieldsRow2.indexOf(advancedSearchService.formElementData.customFieldList[i].customFieldType) !== -1
+]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+[
+ advancedSearchService.patientInformationFieldsRow2 &&
+ advancedSearchService.patientInformationFieldsRow2.indexOf(
+ advancedSearchService.formElementData.customFieldList[i].customFieldType
+ ) !== -1
+]
+`;
+
+exports[`pipe-expression.ng - __ng_interpolation-verify 1`] = `
+[
+ a ? (b | c : d) : (e | f : g),
+ a | b | c | d,
+ ((a | b) | c) | d,
+ a | b:(c | d),
+ { a: b | c },
+ (a + b) | c,
+ (a | b) + c,
+ fn(a | b),
+ a?.b(c | d),
+ a[b | c],
+ ($students | async).items,
+ ($students | async)(),
+ myData | myPipe:'arg1':'arg2':'arg3',
+ value
+ | pipeA: {
+ keyA: reallySuperLongValue,
+ keyB: shortValue | pipeB | pipeC: valueToPipeC
+ } : {
+ keyA: reallySuperLongValue,
+ keyB: shortValue | pipeB | pipeC: valueToPipeC
+ }
+ | aaa
+]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+[
+ a ? (b | c: d) : (e | f: g),
+ a | b | c | d,
+ a | b | c | d,
+ a | b: (c | d),
+ { a: b | c },
+ a + b | c,
+ (a | b) + c,
+ fn(a | b),
+ a?.b(c | d),
+ a[b | c],
+ ($students | async).items,
+ ($students | async)(),
+ myData | myPipe: "arg1":"arg2":"arg3",
+ value
+ | pipeA
+ : {
+ keyA: reallySuperLongValue,
+ keyB: shortValue | pipeB | pipeC: valueToPipeC
+ }
+ : {
+ keyA: reallySuperLongValue,
+ keyB: shortValue | pipeB | pipeC: valueToPipeC
+ }
+ | aaa
+]
+`;
diff --git a/tests/angular_interpolation/jsfmt.spec.js b/tests/angular_interpolation/jsfmt.spec.js
new file mode 100644
index 000000000..09a639b7b
--- /dev/null
+++ b/tests/angular_interpolation/jsfmt.spec.js
@@ -0,0 +1 @@
+run_spec(__dirname, ["__ng_interpolation"]);
diff --git a/tests/angular_interpolation/logical-expression.ng b/tests/angular_interpolation/logical-expression.ng
new file mode 100644
index 000000000..fc56a8e99
--- /dev/null
+++ b/tests/angular_interpolation/logical-expression.ng
@@ -0,0 +1,3 @@
+[
+ advancedSearchService.patientInformationFieldsRow2 && advancedSearchService.patientInformationFieldsRow2.indexOf(advancedSearchService.formElementData.customFieldList[i].customFieldType) !== -1
+]
diff --git a/tests/angular_interpolation/pipe-expression.ng b/tests/angular_interpolation/pipe-expression.ng
new file mode 100644
index 000000000..9477cadb7
--- /dev/null
+++ b/tests/angular_interpolation/pipe-expression.ng
@@ -0,0 +1,24 @@
+[
+ a ? (b | c : d) : (e | f : g),
+ a | b | c | d,
+ ((a | b) | c) | d,
+ a | b:(c | d),
+ { a: b | c },
+ (a + b) | c,
+ (a | b) + c,
+ fn(a | b),
+ a?.b(c | d),
+ a[b | c],
+ ($students | async).items,
+ ($students | async)(),
+ myData | myPipe:'arg1':'arg2':'arg3',
+ value
+ | pipeA: {
+ keyA: reallySuperLongValue,
+ keyB: shortValue | pipeB | pipeC: valueToPipeC
+ } : {
+ keyA: reallySuperLongValue,
+ keyB: shortValue | pipeB | pipeC: valueToPipeC
+ }
+ | aaa
+]
diff --git a/tests/decorators-ts/__snapshots__/jsfmt.spec.js.snap b/tests/decorators-ts/__snapshots__/jsfmt.spec.js.snap
index 9eb75a0c3..c57fd208a 100644
--- a/tests/decorators-ts/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/decorators-ts/__snapshots__/jsfmt.spec.js.snap
@@ -53,7 +53,9 @@ export class HeroButtonComponent {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@Component({
selector: "toh-hero-button",
- template: \`\`
+ template: \`
+
+ \`
})
export class HeroButtonComponent {
@Output() change = new EventEmitter();
diff --git a/tests/html_angular/__snapshots__/jsfmt.spec.js.snap b/tests/html_angular/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000..7ab4c3c1a
--- /dev/null
+++ b/tests/html_angular/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,8001 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`attributes.component.html - angular-verify 1`] = `
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+`;
+
+exports[`attributes.component.html - angular-verify 2`] = `
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+`;
+
+exports[`attributes.component.html - angular-verify 3`] = `
+
+~
+
+
+`;
+
+exports[`ignore-attribute.component.html - angular-verify 1`] = `
+
+
+
+
+
+
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+
+
+
+
+
+
+`;
+
+exports[`ignore-attribute.component.html - angular-verify 2`] = `
+
+
+
+
+
+
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+
+
+
+
+
+
+`;
+
+exports[`ignore-attribute.component.html - angular-verify 3`] = `
+
+
+
+
+
+
+
+~
+
+
+
+
+
+
+
+
+`;
+
+exports[`interpolation.component.html - angular-verify 1`] = `
+{{ a | b : c }}
+{{ 0 - 1 }}
+{{ - 1 }}
+{{ a ? 1 : 2 }}
+{{ a ( 1 ) ( 2 ) }}
+{{ a [ b ] }}
+{{ [ 1 ] }}
+{{ { 'a' : 1 } }}
+{{ { a : 1 } }}
+{{ true }}
+{{ undefined }}
+{{ null }}
+{{ ( 1 ) }}
+{{ 1 }}
+{{ 'hello' }}
+{{ a ( 1 , 2 ) }}
+{{ a . b ( 1 , 2 ) }}
+{{ x ! }}
+{{ ! x }}
+{{ ( ( a ) ) }}
+{{ a }}
+{{ a // hello }}
+{{ a . b }}
+{{ a ?. b ( ) }}
+{{ a ?. b }}
+{{ a // hello }}
+
+
+
+
+
+{{copyTypes[options.copyType]}}
+{{listRow.NextScheduledSendStatus == 1 || listRow.NextScheduledSendStatus == 2 || listRow.NextScheduledSendStatus == 3}}
+{{a}}{{b}}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+{{ a | b: c }}
+{{ 0 - 1 }}
+{{ -1 }}
+{{ a ? 1 : 2 }}
+{{ a(1)(2) }}
+{{ a[b] }}
+{{ [1] }}
+{{ { a: 1 } }}
+{{ { a: 1 } }}
+{{ true }}
+{{ undefined }}
+{{ null }}
+{{ 1 }}
+{{ 1 }}
+{{ "hello" }}
+{{ a(1, 2) }}
+{{ a.b(1, 2) }}
+{{ x! }}
+{{ !x }}
+{{ a }}
+{{ a }}
+{{ a // hello }}
+{{ a.b }}
+{{ a?.b() }}
+{{ a?.b }}
+{{ a // hello }}
+
+
+
+
+
+
+ {{ copyTypes[options.copyType] }}
+
+{{
+ listRow.NextScheduledSendStatus == 1 ||
+ listRow.NextScheduledSendStatus == 2 ||
+ listRow.NextScheduledSendStatus == 3
+}}
+{{ a }}{{ b }}
+
+`;
+
+exports[`interpolation.component.html - angular-verify 2`] = `
+{{ a | b : c }}
+{{ 0 - 1 }}
+{{ - 1 }}
+{{ a ? 1 : 2 }}
+{{ a ( 1 ) ( 2 ) }}
+{{ a [ b ] }}
+{{ [ 1 ] }}
+{{ { 'a' : 1 } }}
+{{ { a : 1 } }}
+{{ true }}
+{{ undefined }}
+{{ null }}
+{{ ( 1 ) }}
+{{ 1 }}
+{{ 'hello' }}
+{{ a ( 1 , 2 ) }}
+{{ a . b ( 1 , 2 ) }}
+{{ x ! }}
+{{ ! x }}
+{{ ( ( a ) ) }}
+{{ a }}
+{{ a // hello }}
+{{ a . b }}
+{{ a ?. b ( ) }}
+{{ a ?. b }}
+{{ a // hello }}
+
+
+
+
+
+{{copyTypes[options.copyType]}}
+{{listRow.NextScheduledSendStatus == 1 || listRow.NextScheduledSendStatus == 2 || listRow.NextScheduledSendStatus == 3}}
+{{a}}{{b}}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+{{ a | b: c }}
+{{ 0 - 1 }}
+{{ -1 }}
+{{ a ? 1 : 2 }}
+{{ a(1)(2) }}
+{{ a[b] }}
+{{ [1] }}
+{{ { a: 1 } }}
+{{ { a: 1 } }}
+{{ true }}
+{{ undefined }}
+{{ null }}
+{{ 1 }}
+{{ 1 }}
+{{ "hello" }}
+{{ a(1, 2) }}
+{{ a.b(1, 2) }}
+{{ x! }}
+{{ !x }}
+{{ a }}
+{{ a }}
+{{ a // hello }}
+{{ a.b }}
+{{ a?.b() }}
+{{ a?.b }}
+{{ a // hello }}
+
+
+
+
+
+
+ {{ copyTypes[options.copyType] }}
+
+{{
+ listRow.NextScheduledSendStatus == 1 ||
+ listRow.NextScheduledSendStatus == 2 ||
+ listRow.NextScheduledSendStatus == 3
+}}
+{{ a }}{{ b }}
+
+`;
+
+exports[`interpolation.component.html - angular-verify 3`] = `
+{{ a | b : c }}
+{{ 0 - 1 }}
+{{ - 1 }}
+{{ a ? 1 : 2 }}
+{{ a ( 1 ) ( 2 ) }}
+{{ a [ b ] }}
+{{ [ 1 ] }}
+{{ { 'a' : 1 } }}
+{{ { a : 1 } }}
+{{ true }}
+{{ undefined }}
+{{ null }}
+{{ ( 1 ) }}
+{{ 1 }}
+{{ 'hello' }}
+{{ a ( 1 , 2 ) }}
+{{ a . b ( 1 , 2 ) }}
+{{ x ! }}
+{{ ! x }}
+{{ ( ( a ) ) }}
+{{ a }}
+{{ a // hello }}
+{{ a . b }}
+{{ a ?. b ( ) }}
+{{ a ?. b }}
+{{ a // hello }}
+
+
+
+
+
+{{copyTypes[options.copyType]}}
+{{listRow.NextScheduledSendStatus == 1 || listRow.NextScheduledSendStatus == 2 || listRow.NextScheduledSendStatus == 3}}
+{{a}}{{b}}
+~
+
+ {{
+ a
+ | b
+ : c
+ }}
+
+
+ {{
+ 0 -
+ 1
+ }}
+
+
+ {{
+ -1
+ }}
+
+
+ {{
+ a
+ ? 1
+ : 2
+ }}
+
+
+ {{
+ a(
+ 1
+ )(
+ 2
+ )
+ }}
+
+
+ {{
+ a[
+ b
+ ]
+ }}
+
+
+ {{
+ [
+ 1
+ ]
+ }}
+
+
+ {{
+ {
+ a: 1
+ }
+ }}
+
+
+ {{
+ {
+ a: 1
+ }
+ }}
+
+
+ {{
+ true
+ }}
+
+
+ {{
+ undefined
+ }}
+
+
+ {{
+ null
+ }}
+
+
+ {{
+ 1
+ }}
+
+
+ {{
+ 1
+ }}
+
+
+ {{
+ "hello"
+ }}
+
+
+ {{
+ a(
+ 1,
+ 2
+ )
+ }}
+
+
+ {{
+ a.b(
+ 1,
+ 2
+ )
+ }}
+
+
+ {{
+ x!
+ }}
+
+
+ {{
+ !x
+ }}
+
+
+ {{
+ a
+ }}
+
+
+ {{
+ a
+ }}
+
+
+ {{
+ a // hello
+ }}
+
+
+ {{
+ a.b
+ }}
+
+
+ {{
+ a?.b()
+ }}
+
+
+ {{
+ a?.b
+ }}
+
+
+ {{
+ a // hello
+ }}
+
+
+
+
+
+
+
+ {{
+ copyTypes[
+ options
+ .copyType
+ ]
+ }}
+
+{{
+ listRow.NextScheduledSendStatus ==
+ 1 ||
+ listRow.NextScheduledSendStatus ==
+ 2 ||
+ listRow.NextScheduledSendStatus ==
+ 3
+}}
+{{
+ a
+ }}{{
+ b
+ }}
+
+`;
+
+exports[`real-world.component.html - angular-verify 1`] = `
+
+
+
+Template Syntax
+Interpolation
+Expression context
+Statement context
+Mental Model
+Buttons
+Properties vs. Attributes
+
+Property Binding
+
+
+Event Binding
+Two-way Binding
+
+Directives
+
+
+Template reference variables
+Inputs and outputs
+Pipes
+Safe navigation operator ?.
+Non-null assertion operator !.
+Enums
+
+
+
Interpolation
+
+My current hero is {{currentHero.name}}
+
+
+ {{title}}
+
+
+
+
+The sum of 1 + 1 is {{1 + 1}}
+
+
+The sum of 1 + 1 is not {{1 + 1 + getVal()}}
+
+top
+
+
Expression context
+
+Component expression context ({{title}}, [hidden]="isUnchanged")
+
+ {{title}}
+ changed
+
+
+
+Template input variable expression context (let hero)
+
+
+ {{hero.name}}
+
+
+Template reference variable expression context (#heroInput)
+
+ Type something:
+ {{heroInput.value}}
+
+
+top
+
+
Statement context
+
+Component statement context ( (click)="onSave() )
+
+
+
+
+Template $event statement context
+
+
+
+
+Template input variable statement context (let hero)
+
+
+
+
+
+Template reference variable statement context (#heroForm)
+
+
+
+
+top
+
+
+
New Mental Model
+
+
+
+Mental Model
+
+
+
+
+
+
+ Mental Model
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+click me
+{{clicked}}
+
+
+
+ Hero Name:
+
+ {{name}}
+
+
+
+
+
+
+Special
+
+
+
+
+top
+
+
+
Property vs. Attribute (img examples)
+
+
+
+
+
+
+
+
+
+top
+
+
+
Buttons
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
Property Binding
+
+
+
+[ngClass] binding to the classes property
+
+
+
+
+
+
+
+
+
+
is the interpolated image.
+
is the property bound image.
+
+"{{title}}" is the interpolated title.
+"" is the property bound title.
+
+
+"{{evilTitle}}" is the interpolated evil title.
+"" is the property bound evil title.
+
+top
+
+
+
Attribute Binding
+
+
+
+
+ One-Two
+
+
+
+ Five Six
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
Class Binding
+
+
+Bad curly special
+
+
+Bad curly
+
+
+The class binding is special
+
+
+This one is not so special
+
+This class binding is special too
+
+top
+
+
+
Style Binding
+
+
+
+
+
+
+
+top
+
+
+
Event Binding
+
+
+
+
+
+
+
+click with myClick
+{{clickMessage}}
+
+
+
+
+
+
+
+
+
+
+Click me
+ Click me too!
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
Two-way Binding
+
+
+ Resizable Text
+
+
+
+
+ De-sugared two-way binding
+
+
+
+top
+
+
+
NgModel (two-way) Binding
+
+Result: {{currentHero.name}}
+
+
+without NgModel
+
+
+[(ngModel)]
+
+
+bindon-ngModel
+
+
+(ngModelChange)="...name=$event"
+
+
+(ngModelChange)="setUppercaseName($event)"
+
+top
+
+
+
NgClass Binding
+
+currentClasses is {{currentClasses | json}}
+This div is initially saveable, unchanged, and special
+
+
+
+ |
+ |
+
+
+
+
+ This div should be {{ canSave ? "": "not"}} saveable,
+ {{ isUnchanged ? "unchanged" : "modified" }} and,
+ {{ isSpecial ? "": "not"}} special after clicking "Refresh".
+
+
+This div is special
+
+Bad curly special
+Curly special
+
+top
+
+
+
NgStyle Binding
+
+
+ This div is x-large or smaller.
+
+
+[ngStyle] binding to currentStyles - CSS property names
+currentStyles is {{currentStyles | json}}
+
+ This div is initially italic, normal weight, and extra large (24px).
+
+
+
+
+ |
+ |
+
+
+
+
+ This div should be {{ canSave ? "italic": "plain"}},
+ {{ isUnchanged ? "normal weight" : "bold" }} and,
+ {{ isSpecial ? "extra large": "normal size"}} after clicking "Refresh".
+
+top
+
+
+
NgIf Binding
+
+
+
+Hello, {{currentHero.name}}
+Hello, {{nullHero.name}}
+
+
+
+Add {{currentHero.name}} with template
+
+
+Hero Detail removed from DOM (via template) because isActive is false
+
+
+
+
+
+Show with class
+Hide with class
+
+
+
+
+Show with style
+Hide with style
+
+top
+
+
+
NgFor Binding
+
+
+ {{hero.name}}
+
+
+
+
+
+
+
+
+top
+
+*ngFor with index
+with semi-colon separator
+
+ {{i + 1}} - {{hero.name}}
+
+
+with comma separator
+
+
+ {{i + 1}} - {{hero.name}}
+
+
+top
+
+*ngFor trackBy
+
+
+
+
+without trackBy
+
+ ({{hero.id}}) {{hero.name}}
+
+
+ Hero DOM elements change #{{heroesNoTrackByCount}} without trackBy
+
+
+
+with trackBy
+
+ ({{hero.id}}) {{hero.name}}
+
+
+ Hero DOM elements change #{{heroesWithTrackByCount}} with trackBy
+
+
+
+
+
+with trackBy and semi-colon separator
+
+
+ ({{hero.id}}) {{hero.name}}
+
+
+
+with trackBy and comma separator
+
+ ({{hero.id}}) {{hero.name}}
+
+
+with trackBy and space separator
+
+ ({{hero.id}}) {{hero.name}}
+
+
+with generic trackById function
+
+ ({{hero.id}}) {{hero.name}}
+
+
+top
+
+
+
NgSwitch Binding
+
+Pick your favorite hero
+
+
+
+
+
+
+
+
+ Are you as confused as {{currentHero.name}}?
+
+
+
+top
+
+
+
Template reference variables
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Example Form
+
+
+top
+
+
+
Inputs and Outputs
+
+
+
+
+
+
+
+myClick2
+{{clickMessage2}}
+
+top
+
+
+
Pipes
+
+Title through uppercase pipe: {{title | uppercase}}
+
+
+
+ Title through a pipe chain:
+ {{title | uppercase | lowercase}}
+
+
+
+Birthdate: {{currentHero?.birthdate | date:'longDate'}}
+
+{{currentHero | json}}
+
+Birthdate: {{(currentHero?.birthdate | date:'longDate') | uppercase}}
+
+
+
+ {{product.price | currency:'USD':true}}
+
+
+top
+
+
+
Safe navigation operator ?.
+
+
+ The title is {{title}}
+
+
+
+ The current hero's name is {{currentHero?.name}}
+
+
+
+ The current hero's name is {{currentHero.name}}
+
+
+
+
+
+
+The null hero's name is {{nullHero.name}}
+
+
+The null hero's name is {{nullHero && nullHero.name}}
+
+
+
+
+ The null hero's name is {{nullHero?.name}}
+
+
+
+top
+
+
+
Non-null assertion operator !.
+
+
+
+
+ The hero's name is {{hero!.name}}
+
+
+
+top
+
+
+
$any type cast function $any( ).
+
+
+
+
+ The hero's marker is {{$any(hero).marker}}
+
+
+
+
+
+
+ Undeclared members is {{$any(this).member}}
+
+
+
+top
+
+
+
+
Enums in binding
+
+
+ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ The current color is {{Color[color]}} and its number is {{color}}.
+
+
+
+top
+
+
+
+
+
+
+
+ {{submitMessage}}
+
+
+
+
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+
+Template Syntax
+Interpolation
+Expression context
+Statement context
+Mental Model
+Buttons
+Properties vs. Attributes
+
+Property Binding
+
+
+Event Binding
+Two-way Binding
+
+Directives
+
+
+Template reference variables
+Inputs and outputs
+Pipes
+Safe navigation operator ?.
+Non-null assertion operator !.
+Enums
+
+
+
+Interpolation
+
+My current hero is {{ currentHero.name }}
+
+{{ title }} 
+
+
+The sum of 1 + 1 is {{ 1 + 1 }}
+
+
+The sum of 1 + 1 is not {{ 1 + 1 + getVal() }}
+
+top
+
+
+Expression context
+
+
+ Component expression context ({{title}},
+ [hidden]="isUnchanged")
+
+
+ {{ title }} changed
+
+
+Template input variable expression context (let hero)
+
+
+ {{ hero.name }}
+
+
+Template reference variable expression context (#heroInput)
+
+ Type something: {{ heroInput.value }}
+
+
+top
+
+
+Statement context
+
+Component statement context ( (click)="onSave() )
+
+
+Template $event statement context
+
+
+Template input variable statement context (let hero)
+
+
+
+
+
+Template reference variable statement context (#heroForm)
+
+
+
+
+top
+
+
+
+New Mental Model
+
+
+
+Mental Model
+
+
+
+
+ Mental Model
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+click me
+{{ clicked }}
+
+Hero Name: {{ name }}
+
+
+
+
+Special
+
+
+
+
+top
+
+
+
+Property vs. Attribute (img examples)
+
+
+
+
+
+
+
+
+top
+
+
+
+Buttons
+
+
+
+
+
+
+
+
+
+top
+
+
+
+Property Binding
+
+
+
+[ngClass] binding to the classes property
+
+
+
+
+
+
+
+
is the interpolated image.
+
is the property bound image.
+
+
+ "{{ title }}" is the interpolated title.
+
+"" is the property bound title.
+
+
+
+ "{{ evilTitle }}" is the interpolated evil title.
+
+
+ "" is the property bound evil
+ title.
+
+
+top
+
+
+
+Attribute Binding
+
+
+
+
+
+ One-Two
+
+
+
+
+
+ Five
+ Six
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
+Class Binding
+
+
+Bad curly special
+
+
+Bad curly
+
+
+The class binding is special
+
+
+
+ This one is not so special
+
+
+This class binding is special too
+
+top
+
+
+
+Style Binding
+
+
+
+
+
+
+
+top
+
+
+
+Event Binding
+
+
+
+
+
+
+
+ click with myClick
+ {{ clickMessage }}
+
+
+
+
+
+
+
+
+
+
+ Click me
+ Click me too!
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+Two-way Binding
+
+
+ Resizable Text
+
+
+
+
+ De-sugared two-way binding
+
+
+
+top
+
+
+
+NgModel (two-way) Binding
+
+Result: {{ currentHero.name }}
+
+
+without NgModel
+ [(ngModel)]
+ bindon-ngModel
+
+(ngModelChange)="...name=$event"
+
+(ngModelChange)="setUppercaseName($event)"
+
+top
+
+
+
+NgClass Binding
+
+currentClasses is {{ currentClasses | json }}
+
+ This div is initially saveable, unchanged, and special
+
+
+
+
+ |
+
+|
+
+
+
+ This div should be {{ canSave ? "" : "not" }} saveable,
+ {{ isUnchanged ? "unchanged" : "modified" }} and,
+ {{ isSpecial ? "" : "not" }} special after clicking "Refresh".
+
+
+
+This div is special
+
+Bad curly special
+Curly special
+
+top
+
+
+
+NgStyle Binding
+
+
+ This div is x-large or smaller.
+
+
+[ngStyle] binding to currentStyles - CSS property names
+currentStyles is {{ currentStyles | json }}
+
+ This div is initially italic, normal weight, and extra large (24px).
+
+
+
+
+ |
+ |
+
+
+
+ This div should be {{ canSave ? "italic" : "plain" }},
+ {{ isUnchanged ? "normal weight" : "bold" }} and,
+ {{ isSpecial ? "extra large" : "normal size" }} after clicking "Refresh".
+
+
+top
+
+
+
+NgIf Binding
+
+
+
+Hello, {{ currentHero.name }}
+Hello, {{ nullHero.name }}
+
+
+
+Add {{ currentHero.name }} with template
+
+
+Hero Detail removed from DOM (via template) because isActive is false
+
+
+
+
+
+Show with class
+Hide with class
+
+
+
+
+Show with style
+Hide with style
+
+top
+
+
+
+NgFor Binding
+
+
+ {{ hero.name }}
+
+
+
+
+
+
+
+
+top
+
+*ngFor with index
+with semi-colon separator
+
+
+ {{ i + 1 }} - {{ hero.name }}
+
+
+
+with comma separator
+
+
+
+ {{ i + 1 }} - {{ hero.name }}
+
+
+
+top
+
+*ngFor trackBy
+
+
+
+
+without trackBy
+
+
+ ({{ hero.id }}) {{ hero.name }}
+
+
+
+ Hero DOM elements change #{{ heroesNoTrackByCount }} without trackBy
+
+
+
+with trackBy
+
+
+ ({{ hero.id }}) {{ hero.name }}
+
+
+
+ Hero DOM elements change #{{ heroesWithTrackByCount }} with trackBy
+
+
+
+
+
+with trackBy and semi-colon separator
+
+
+ ({{ hero.id }}) {{ hero.name }}
+
+
+
+with trackBy and comma separator
+
+
+ ({{ hero.id }}) {{ hero.name }}
+
+
+
+with trackBy and space separator
+
+
+ ({{ hero.id }}) {{ hero.name }}
+
+
+
+with generic trackById function
+
+
+ ({{ hero.id }}) {{ hero.name }}
+
+
+
+top
+
+
+
+NgSwitch Binding
+
+Pick your favorite hero
+
+
+
+
+
+
+
+
+
+ Are you as confused as {{ currentHero.name }}?
+
+
+
+
+top
+
+
+
+Template reference variables
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Example Form
+
+
+top
+
+
+
+Inputs and Outputs
+
+
+
+
+
+
+myClick2
+{{ clickMessage2 }}
+
+top
+
+
+
+Pipes
+
+Title through uppercase pipe: {{ title | uppercase }}
+
+
+Title through a pipe chain: {{ title | uppercase | lowercase }}
+
+
+Birthdate: {{ currentHero?.birthdate | date: "longDate" }}
+
+{{ currentHero | json }}
+
+
+ Birthdate: {{ currentHero?.birthdate | date: "longDate" | uppercase }}
+
+
+
+
+ {{ product.price | currency: "USD":true }}
+
+
+top
+
+
+
+Safe navigation operator ?.
+
+The title is {{ title }}
+
+The current hero's name is {{ currentHero?.name }}
+
+The current hero's name is {{ currentHero.name }}
+
+
+
+
+The null hero's name is {{ nullHero.name }}
+
+The null hero's name is {{ nullHero && nullHero.name }}
+
+
+
+ The null hero's name is {{ nullHero?.name }}
+
+
+top
+
+
+
+Non-null assertion operator !.
+
+
+
+ The hero's name is {{ hero!.name }}
+
+
+top
+
+
+
+$any type cast function $any( ).
+
+
+
+ The hero's marker is {{ $any(hero).marker }}
+
+
+
+
+ Undeclared members is {{$any(this).member}}
+
+
+top
+
+
+
+
+Enums in binding
+
+
+ The name of the Color.Red enum is {{ Color[Color.Red] }}.
+ The current color is {{ Color[color] }} and its number is {{ color }}.
+
+
+
+top
+
+
+
+
+
+ {{ submitMessage }}
+
+
+
+
+`;
+
+exports[`real-world.component.html - angular-verify 2`] = `
+
+
+
+Template Syntax
+Interpolation
+Expression context
+Statement context
+Mental Model
+Buttons
+Properties vs. Attributes
+
+Property Binding
+
+
+Event Binding
+Two-way Binding
+
+Directives
+
+
+Template reference variables
+Inputs and outputs
+Pipes
+Safe navigation operator ?.
+Non-null assertion operator !.
+Enums
+
+
+
Interpolation
+
+My current hero is {{currentHero.name}}
+
+
+ {{title}}
+
+
+
+
+The sum of 1 + 1 is {{1 + 1}}
+
+
+The sum of 1 + 1 is not {{1 + 1 + getVal()}}
+
+top
+
+
Expression context
+
+Component expression context ({{title}}, [hidden]="isUnchanged")
+
+ {{title}}
+ changed
+
+
+
+Template input variable expression context (let hero)
+
+
+ {{hero.name}}
+
+
+Template reference variable expression context (#heroInput)
+
+ Type something:
+ {{heroInput.value}}
+
+
+top
+
+
Statement context
+
+Component statement context ( (click)="onSave() )
+
+
+
+
+Template $event statement context
+
+
+
+
+Template input variable statement context (let hero)
+
+
+
+
+
+Template reference variable statement context (#heroForm)
+
+
+
+
+top
+
+
+
New Mental Model
+
+
+
+Mental Model
+
+
+
+
+
+
+ Mental Model
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+click me
+{{clicked}}
+
+
+
+ Hero Name:
+
+ {{name}}
+
+
+
+
+
+
+Special
+
+
+
+
+top
+
+
+
Property vs. Attribute (img examples)
+
+
+
+
+
+
+
+
+
+top
+
+
+
Buttons
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
Property Binding
+
+
+
+[ngClass] binding to the classes property
+
+
+
+
+
+
+
+
+
+
is the interpolated image.
+
is the property bound image.
+
+"{{title}}" is the interpolated title.
+"" is the property bound title.
+
+
+"{{evilTitle}}" is the interpolated evil title.
+"" is the property bound evil title.
+
+top
+
+
+
Attribute Binding
+
+
+
+
+ One-Two
+
+
+
+ Five Six
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
Class Binding
+
+
+Bad curly special
+
+
+Bad curly
+
+
+The class binding is special
+
+
+This one is not so special
+
+This class binding is special too
+
+top
+
+
+
Style Binding
+
+
+
+
+
+
+
+top
+
+
+
Event Binding
+
+
+
+
+
+
+
+click with myClick
+{{clickMessage}}
+
+
+
+
+
+
+
+
+
+
+Click me
+ Click me too!
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
Two-way Binding
+
+
+ Resizable Text
+
+
+
+
+ De-sugared two-way binding
+
+
+
+top
+
+
+
NgModel (two-way) Binding
+
+Result: {{currentHero.name}}
+
+
+without NgModel
+
+
+[(ngModel)]
+
+
+bindon-ngModel
+
+
+(ngModelChange)="...name=$event"
+
+
+(ngModelChange)="setUppercaseName($event)"
+
+top
+
+
+
NgClass Binding
+
+currentClasses is {{currentClasses | json}}
+This div is initially saveable, unchanged, and special
+
+
+
+ |
+ |
+
+
+
+
+ This div should be {{ canSave ? "": "not"}} saveable,
+ {{ isUnchanged ? "unchanged" : "modified" }} and,
+ {{ isSpecial ? "": "not"}} special after clicking "Refresh".
+
+
+This div is special
+
+Bad curly special
+Curly special
+
+top
+
+
+
NgStyle Binding
+
+
+ This div is x-large or smaller.
+
+
+[ngStyle] binding to currentStyles - CSS property names
+currentStyles is {{currentStyles | json}}
+
+ This div is initially italic, normal weight, and extra large (24px).
+
+
+
+
+ |
+ |
+
+
+
+
+ This div should be {{ canSave ? "italic": "plain"}},
+ {{ isUnchanged ? "normal weight" : "bold" }} and,
+ {{ isSpecial ? "extra large": "normal size"}} after clicking "Refresh".
+
+top
+
+
+
NgIf Binding
+
+
+
+Hello, {{currentHero.name}}
+Hello, {{nullHero.name}}
+
+
+
+Add {{currentHero.name}} with template
+
+
+Hero Detail removed from DOM (via template) because isActive is false
+
+
+
+
+
+Show with class
+Hide with class
+
+
+
+
+Show with style
+Hide with style
+
+top
+
+
+
NgFor Binding
+
+
+ {{hero.name}}
+
+
+
+
+
+
+
+
+top
+
+*ngFor with index
+with semi-colon separator
+
+ {{i + 1}} - {{hero.name}}
+
+
+with comma separator
+
+
+ {{i + 1}} - {{hero.name}}
+
+
+top
+
+*ngFor trackBy
+
+
+
+
+without trackBy
+
+ ({{hero.id}}) {{hero.name}}
+
+
+ Hero DOM elements change #{{heroesNoTrackByCount}} without trackBy
+
+
+
+with trackBy
+
+ ({{hero.id}}) {{hero.name}}
+
+
+ Hero DOM elements change #{{heroesWithTrackByCount}} with trackBy
+
+
+
+
+
+with trackBy and semi-colon separator
+
+
+ ({{hero.id}}) {{hero.name}}
+
+
+
+with trackBy and comma separator
+
+ ({{hero.id}}) {{hero.name}}
+
+
+with trackBy and space separator
+
+ ({{hero.id}}) {{hero.name}}
+
+
+with generic trackById function
+
+ ({{hero.id}}) {{hero.name}}
+
+
+top
+
+
+
NgSwitch Binding
+
+Pick your favorite hero
+
+
+
+
+
+
+
+
+ Are you as confused as {{currentHero.name}}?
+
+
+
+top
+
+
+
Template reference variables
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Example Form
+
+
+top
+
+
+
Inputs and Outputs
+
+
+
+
+
+
+
+myClick2
+{{clickMessage2}}
+
+top
+
+
+
Pipes
+
+Title through uppercase pipe: {{title | uppercase}}
+
+
+
+ Title through a pipe chain:
+ {{title | uppercase | lowercase}}
+
+
+
+Birthdate: {{currentHero?.birthdate | date:'longDate'}}
+
+{{currentHero | json}}
+
+Birthdate: {{(currentHero?.birthdate | date:'longDate') | uppercase}}
+
+
+
+ {{product.price | currency:'USD':true}}
+
+
+top
+
+
+
Safe navigation operator ?.
+
+
+ The title is {{title}}
+
+
+
+ The current hero's name is {{currentHero?.name}}
+
+
+
+ The current hero's name is {{currentHero.name}}
+
+
+
+
+
+
+The null hero's name is {{nullHero.name}}
+
+
+The null hero's name is {{nullHero && nullHero.name}}
+
+
+
+
+ The null hero's name is {{nullHero?.name}}
+
+
+
+top
+
+
+
Non-null assertion operator !.
+
+
+
+
+ The hero's name is {{hero!.name}}
+
+
+
+top
+
+
+
$any type cast function $any( ).
+
+
+
+
+ The hero's marker is {{$any(hero).marker}}
+
+
+
+
+
+
+ Undeclared members is {{$any(this).member}}
+
+
+
+top
+
+
+
+
Enums in binding
+
+
+ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ The current color is {{Color[color]}} and its number is {{color}}.
+
+
+
+top
+
+
+
+
+
+
+
+ {{submitMessage}}
+
+
+
+
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+
+Template Syntax
+Interpolation
+Expression context
+Statement context
+Mental Model
+Buttons
+Properties vs. Attributes
+
+Property Binding
+
+
+Event Binding
+Two-way Binding
+
+Directives
+
+
+Template reference variables
+Inputs and outputs
+Pipes
+Safe navigation operator ?.
+Non-null assertion operator !.
+Enums
+
+
+
+Interpolation
+
+My current hero is {{ currentHero.name }}
+
+{{ title }} 
+
+
+The sum of 1 + 1 is {{ 1 + 1 }}
+
+
+The sum of 1 + 1 is not {{ 1 + 1 + getVal() }}
+
+top
+
+
+Expression context
+
+
+ Component expression context ({{title}},
+ [hidden]="isUnchanged")
+
+
+ {{ title }} changed
+
+
+Template input variable expression context (let hero)
+
+
+ {{ hero.name }}
+
+
+Template reference variable expression context (#heroInput)
+
+ Type something: {{ heroInput.value }}
+
+
+top
+
+
+Statement context
+
+Component statement context ( (click)="onSave() )
+
+
+Template $event statement context
+
+
+Template input variable statement context (let hero)
+
+
+
+
+
+Template reference variable statement context (#heroForm)
+
+
+
+
+top
+
+
+
+New Mental Model
+
+
+
+Mental Model
+
+
+
+
+ Mental Model
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+click me
+{{ clicked }}
+
+Hero Name: {{ name }}
+
+
+
+
+Special
+
+
+
+
+top
+
+
+
+Property vs. Attribute (img examples)
+
+
+
+
+
+
+
+
+top
+
+
+
+Buttons
+
+
+
+
+
+
+
+
+
+top
+
+
+
+Property Binding
+
+
+
+[ngClass] binding to the classes property
+
+
+
+
+
+
+
+
is the interpolated image.
+
is the property bound image.
+
+
+ "{{ title }}" is the interpolated title.
+
+"" is the property bound title.
+
+
+
+ "{{ evilTitle }}" is the interpolated evil title.
+
+
+ "" is the property bound evil
+ title.
+
+
+top
+
+
+
+Attribute Binding
+
+
+
+
+
+ One-Two
+
+
+
+
+
+ Five
+ Six
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
+Class Binding
+
+
+Bad curly special
+
+
+Bad curly
+
+
+The class binding is special
+
+
+
+ This one is not so special
+
+
+This class binding is special too
+
+top
+
+
+
+Style Binding
+
+
+
+
+
+
+
+top
+
+
+
+Event Binding
+
+
+
+
+
+
+
+ click with myClick
+ {{ clickMessage }}
+
+
+
+
+
+
+
+
+
+
+ Click me
+ Click me too!
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+Two-way Binding
+
+
+ Resizable Text
+
+
+
+
+ De-sugared two-way binding
+
+
+
+top
+
+
+
+NgModel (two-way) Binding
+
+Result: {{ currentHero.name }}
+
+
+without NgModel
+ [(ngModel)]
+ bindon-ngModel
+
+(ngModelChange)="...name=$event"
+
+(ngModelChange)="setUppercaseName($event)"
+
+top
+
+
+
+NgClass Binding
+
+currentClasses is {{ currentClasses | json }}
+
+ This div is initially saveable, unchanged, and special
+
+
+
+
+ |
+
+|
+
+
+
+ This div should be {{ canSave ? "" : "not" }} saveable,
+ {{ isUnchanged ? "unchanged" : "modified" }} and,
+ {{ isSpecial ? "" : "not" }} special after clicking "Refresh".
+
+
+
+This div is special
+
+Bad curly special
+Curly special
+
+top
+
+
+
+NgStyle Binding
+
+
+ This div is x-large or smaller.
+
+
+[ngStyle] binding to currentStyles - CSS property names
+currentStyles is {{ currentStyles | json }}
+
+ This div is initially italic, normal weight, and extra large (24px).
+
+
+
+
+ |
+ |
+
+
+
+ This div should be {{ canSave ? "italic" : "plain" }},
+ {{ isUnchanged ? "normal weight" : "bold" }} and,
+ {{ isSpecial ? "extra large" : "normal size" }} after clicking "Refresh".
+
+
+top
+
+
+
+NgIf Binding
+
+
+
+Hello, {{ currentHero.name }}
+Hello, {{ nullHero.name }}
+
+
+
+Add {{ currentHero.name }} with template
+
+
+Hero Detail removed from DOM (via template) because isActive is false
+
+
+
+
+
+Show with class
+Hide with class
+
+
+
+
+Show with style
+Hide with style
+
+top
+
+
+
+NgFor Binding
+
+
+ {{ hero.name }}
+
+
+
+
+
+
+
+
+top
+
+*ngFor with index
+with semi-colon separator
+
+
+ {{ i + 1 }} - {{ hero.name }}
+
+
+
+with comma separator
+
+
+
+ {{ i + 1 }} - {{ hero.name }}
+
+
+
+top
+
+*ngFor trackBy
+
+
+
+
+without trackBy
+
+
+ ({{ hero.id }}) {{ hero.name }}
+
+
+
+ Hero DOM elements change #{{ heroesNoTrackByCount }} without trackBy
+
+
+
+with trackBy
+
+
+ ({{ hero.id }}) {{ hero.name }}
+
+
+
+ Hero DOM elements change #{{ heroesWithTrackByCount }} with trackBy
+
+
+
+
+
+with trackBy and semi-colon separator
+
+
+ ({{ hero.id }}) {{ hero.name }}
+
+
+
+with trackBy and comma separator
+
+
+ ({{ hero.id }}) {{ hero.name }}
+
+
+
+with trackBy and space separator
+
+
+ ({{ hero.id }}) {{ hero.name }}
+
+
+
+with generic trackById function
+
+
+ ({{ hero.id }}) {{ hero.name }}
+
+
+
+top
+
+
+
+NgSwitch Binding
+
+Pick your favorite hero
+
+
+
+
+
+
+
+
+
+ Are you as confused as {{ currentHero.name }}?
+
+
+
+
+top
+
+
+
+Template reference variables
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Example Form
+
+
+top
+
+
+
+Inputs and Outputs
+
+
+
+
+
+
+myClick2
+{{ clickMessage2 }}
+
+top
+
+
+
+Pipes
+
+Title through uppercase pipe: {{ title | uppercase }}
+
+
+Title through a pipe chain: {{ title | uppercase | lowercase }}
+
+
+Birthdate: {{ currentHero?.birthdate | date: "longDate" }}
+
+{{ currentHero | json }}
+
+
+ Birthdate: {{ currentHero?.birthdate | date: "longDate" | uppercase }}
+
+
+
+
+ {{ product.price | currency: "USD":true }}
+
+
+top
+
+
+
+Safe navigation operator ?.
+
+The title is {{ title }}
+
+The current hero's name is {{ currentHero?.name }}
+
+The current hero's name is {{ currentHero.name }}
+
+
+
+
+The null hero's name is {{ nullHero.name }}
+
+The null hero's name is {{ nullHero && nullHero.name }}
+
+
+
+ The null hero's name is {{ nullHero?.name }}
+
+
+top
+
+
+
+Non-null assertion operator !.
+
+
+
+ The hero's name is {{ hero!.name }}
+
+
+top
+
+
+
+$any type cast function $any( ).
+
+
+
+ The hero's marker is {{ $any(hero).marker }}
+
+
+
+
+ Undeclared members is {{$any(this).member}}
+
+
+top
+
+
+
+
+Enums in binding
+
+
+ The name of the Color.Red enum is {{ Color[Color.Red] }}.
+ The current color is {{ Color[color] }} and its number is {{ color }}.
+
+
+
+top
+
+
+
+
+
+ {{ submitMessage }}
+
+
+
+
+`;
+
+exports[`real-world.component.html - angular-verify 3`] = `
+
+
+
+Template Syntax
+Interpolation
+Expression context
+Statement context
+Mental Model
+Buttons
+Properties vs. Attributes
+
+Property Binding
+
+
+Event Binding
+Two-way Binding
+
+Directives
+
+
+Template reference variables
+Inputs and outputs
+Pipes
+Safe navigation operator ?.
+Non-null assertion operator !.
+Enums
+
+
+
Interpolation
+
+My current hero is {{currentHero.name}}
+
+
+ {{title}}
+
+
+
+
+The sum of 1 + 1 is {{1 + 1}}
+
+
+The sum of 1 + 1 is not {{1 + 1 + getVal()}}
+
+top
+
+
Expression context
+
+Component expression context ({{title}}, [hidden]="isUnchanged")
+
+ {{title}}
+ changed
+
+
+
+Template input variable expression context (let hero)
+
+
+ {{hero.name}}
+
+
+Template reference variable expression context (#heroInput)
+
+ Type something:
+ {{heroInput.value}}
+
+
+top
+
+
Statement context
+
+Component statement context ( (click)="onSave() )
+
+
+
+
+Template $event statement context
+
+
+
+
+Template input variable statement context (let hero)
+
+
+
+
+
+Template reference variable statement context (#heroForm)
+
+
+
+
+top
+
+
+
New Mental Model
+
+
+
+Mental Model
+
+
+
+
+
+
+ Mental Model
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+click me
+{{clicked}}
+
+
+
+ Hero Name:
+
+ {{name}}
+
+
+
+
+
+
+Special
+
+
+
+
+top
+
+
+
Property vs. Attribute (img examples)
+
+
+
+
+
+
+
+
+
+top
+
+
+
Buttons
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
Property Binding
+
+
+
+[ngClass] binding to the classes property
+
+
+
+
+
+
+
+
+
+
is the interpolated image.
+
is the property bound image.
+
+"{{title}}" is the interpolated title.
+"" is the property bound title.
+
+
+"{{evilTitle}}" is the interpolated evil title.
+"" is the property bound evil title.
+
+top
+
+
+
Attribute Binding
+
+
+
+
+ One-Two
+
+
+
+ Five Six
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
Class Binding
+
+
+Bad curly special
+
+
+Bad curly
+
+
+The class binding is special
+
+
+This one is not so special
+
+This class binding is special too
+
+top
+
+
+
Style Binding
+
+
+
+
+
+
+
+top
+
+
+
Event Binding
+
+
+
+
+
+
+
+click with myClick
+{{clickMessage}}
+
+
+
+
+
+
+
+
+
+
+Click me
+ Click me too!
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
Two-way Binding
+
+
+ Resizable Text
+
+
+
+
+ De-sugared two-way binding
+
+
+
+top
+
+
+
NgModel (two-way) Binding
+
+Result: {{currentHero.name}}
+
+
+without NgModel
+
+
+[(ngModel)]
+
+
+bindon-ngModel
+
+
+(ngModelChange)="...name=$event"
+
+
+(ngModelChange)="setUppercaseName($event)"
+
+top
+
+
+
NgClass Binding
+
+currentClasses is {{currentClasses | json}}
+This div is initially saveable, unchanged, and special
+
+
+
+ |
+ |
+
+
+
+
+ This div should be {{ canSave ? "": "not"}} saveable,
+ {{ isUnchanged ? "unchanged" : "modified" }} and,
+ {{ isSpecial ? "": "not"}} special after clicking "Refresh".
+
+
+This div is special
+
+Bad curly special
+Curly special
+
+top
+
+
+
NgStyle Binding
+
+
+ This div is x-large or smaller.
+
+
+[ngStyle] binding to currentStyles - CSS property names
+currentStyles is {{currentStyles | json}}
+
+ This div is initially italic, normal weight, and extra large (24px).
+
+
+
+
+ |
+ |
+
+
+
+
+ This div should be {{ canSave ? "italic": "plain"}},
+ {{ isUnchanged ? "normal weight" : "bold" }} and,
+ {{ isSpecial ? "extra large": "normal size"}} after clicking "Refresh".
+
+top
+
+
+
NgIf Binding
+
+
+
+Hello, {{currentHero.name}}
+Hello, {{nullHero.name}}
+
+
+
+Add {{currentHero.name}} with template
+
+
+Hero Detail removed from DOM (via template) because isActive is false
+
+
+
+
+
+Show with class
+Hide with class
+
+
+
+
+Show with style
+Hide with style
+
+top
+
+
+
NgFor Binding
+
+
+ {{hero.name}}
+
+
+
+
+
+
+
+
+top
+
+*ngFor with index
+with semi-colon separator
+
+ {{i + 1}} - {{hero.name}}
+
+
+with comma separator
+
+
+ {{i + 1}} - {{hero.name}}
+
+
+top
+
+*ngFor trackBy
+
+
+
+
+without trackBy
+
+ ({{hero.id}}) {{hero.name}}
+
+
+ Hero DOM elements change #{{heroesNoTrackByCount}} without trackBy
+
+
+
+with trackBy
+
+ ({{hero.id}}) {{hero.name}}
+
+
+ Hero DOM elements change #{{heroesWithTrackByCount}} with trackBy
+
+
+
+
+
+with trackBy and semi-colon separator
+
+
+ ({{hero.id}}) {{hero.name}}
+
+
+
+with trackBy and comma separator
+
+ ({{hero.id}}) {{hero.name}}
+
+
+with trackBy and space separator
+
+ ({{hero.id}}) {{hero.name}}
+
+
+with generic trackById function
+
+ ({{hero.id}}) {{hero.name}}
+
+
+top
+
+
+
NgSwitch Binding
+
+Pick your favorite hero
+
+
+
+
+
+
+
+
+ Are you as confused as {{currentHero.name}}?
+
+
+
+top
+
+
+
Template reference variables
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Example Form
+
+
+top
+
+
+
Inputs and Outputs
+
+
+
+
+
+
+
+myClick2
+{{clickMessage2}}
+
+top
+
+
+
Pipes
+
+Title through uppercase pipe: {{title | uppercase}}
+
+
+
+ Title through a pipe chain:
+ {{title | uppercase | lowercase}}
+
+
+
+Birthdate: {{currentHero?.birthdate | date:'longDate'}}
+
+{{currentHero | json}}
+
+Birthdate: {{(currentHero?.birthdate | date:'longDate') | uppercase}}
+
+
+
+ {{product.price | currency:'USD':true}}
+
+
+top
+
+
+
Safe navigation operator ?.
+
+
+ The title is {{title}}
+
+
+
+ The current hero's name is {{currentHero?.name}}
+
+
+
+ The current hero's name is {{currentHero.name}}
+
+
+
+
+
+
+The null hero's name is {{nullHero.name}}
+
+
+The null hero's name is {{nullHero && nullHero.name}}
+
+
+
+
+ The null hero's name is {{nullHero?.name}}
+
+
+
+top
+
+
+
Non-null assertion operator !.
+
+
+
+
+ The hero's name is {{hero!.name}}
+
+
+
+top
+
+
+
$any type cast function $any( ).
+
+
+
+
+ The hero's marker is {{$any(hero).marker}}
+
+
+
+
+
+
+ Undeclared members is {{$any(this).member}}
+
+
+
+top
+
+
+
+
Enums in binding
+
+
+ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ The current color is {{Color[color]}} and its number is {{color}}.
+
+
+
+top
+
+
+
+
+
+
+
+ {{submitMessage}}
+
+
+
+
+
+~
+
+
+
+
+ Template
+ Syntax
+
+Interpolation
+Expression
+ context
+Statement
+ context
+Mental
+ Model
+Buttons
+Properties
+ vs.
+ Attributes
+
+Property
+ Binding
+
+
+Event
+ Binding
+Two-way
+ Binding
+
+
+ Directives
+
+
+ NgModel
+ (two-way)
+ Binding
+ NgClass
+ Binding
+ NgStyle
+ Binding
+ NgIf
+ NgFor
+
+ NgSwitch
+
+
+Template
+ reference
+ variables
+Inputs
+ and
+ outputs
+Pipes
+Safe
+ navigation
+ operator
+ ?.
+Non-null
+ assertion
+ operator
+ !.
+Enums
+
+
+
+
+ Interpolation
+
+
+
+ My
+ current
+ hero
+ is
+ {{
+ currentHero.name
+ }}
+
+
+
+ {{
+ title
+ }}
+
+
+
+
+
+ The
+ sum
+ of
+ 1
+ +
+ 1
+ is
+ {{
+ 1 +
+ 1
+ }}
+
+
+
+
+ The
+ sum
+ of
+ 1
+ +
+ 1
+ is
+ not
+ {{
+ 1 +
+ 1 +
+ getVal()
+ }}
+
+
+top
+
+
+
+ Expression
+ context
+
+
+
+ Component
+ expression
+ context
+ ({{title}},
+ [hidden]="isUnchanged")
+
+
+ {{
+ title
+ }}
+ changed
+
+
+
+ Template
+ input
+ variable
+ expression
+ context
+ (let
+ hero)
+
+
+
+
+ {{
+ hero.name
+ }}
+
+
+
+
+ Template
+ reference
+ variable
+ expression
+ context
+ (#heroInput)
+
+
+ Type
+ something:
+
+ {{
+ heroInput.value
+ }}
+
+
+top
+
+
+
+ Statement
+ context
+
+
+
+ Component
+ statement
+ context
+ (
+ (click)="onSave()
+ )
+
+
+
+
+
+
+ Template
+ $event
+ statement
+ context
+
+
+
+
+
+
+ Template
+ input
+ variable
+ statement
+ context
+ (let
+ hero)
+
+
+
+
+
+
+
+ Template
+ reference
+ variable
+ statement
+ context
+ (#heroForm)
+
+
+
+
+
+top
+
+
+
+
+ New
+ Mental
+ Model
+
+
+
+
+
+ Mental
+ Model
+
+
+
+
+
+
+
+
+ Mental
+ Model
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ click
+ me
+
+{{
+ clicked
+}}
+
+
+
+ Hero
+ Name:
+
+ {{
+ name
+ }}
+
+
+
+
+
+
+
+ Special
+
+
+
+
+
+top
+
+
+
+
+ Property
+ vs.
+ Attribute
+ (img
+ examples)
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
+
+ Buttons
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
+
+ Property
+ Binding
+
+
+
+
+
+ [ngClass]
+ binding
+ to
+ the
+ classes
+ property
+
+
+
+
+
+
+
+
+
+
+
+
+ is
+ the
+ interpolated
+ image.
+
+
+
+ is
+ the
+ property
+ bound
+ image.
+
+
+
+ "{{
+ title
+ }}"
+ is
+ the
+ interpolated
+ title.
+
+
+ ""
+ is
+ the
+ property
+ bound
+ title.
+
+
+
+
+ "{{
+ evilTitle
+ }}"
+ is
+ the
+ interpolated
+ evil
+ title.
+
+
+ ""
+ is
+ the
+ property
+ bound
+ evil
+ title.
+
+
+top
+
+
+
+
+ Attribute
+ Binding
+
+
+
+
+
+
+
+ One-Two
+
+
+
+
+
+
+
+ Five
+
+
+ Six
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
+
+ Class
+ Binding
+
+
+
+
+ Bad
+ curly
+ special
+
+
+
+
+ Bad
+ curly
+
+
+
+
+ The
+ class
+ binding
+ is
+ special
+
+
+
+
+ This
+ one
+ is
+ not
+ so
+ special
+
+
+
+ This
+ class
+ binding
+ is
+ special
+ too
+
+
+top
+
+
+
+
+ Style
+ Binding
+
+
+
+
+
+
+
+
+top
+
+
+
+
+ Event
+ Binding
+
+
+
+
+
+
+
+
+
+ click
+ with
+ myClick
+
+ {{
+ clickMessage
+ }}
+
+
+
+
+
+
+
+
+
+
+ Click
+ me
+
+ Click
+ me
+ too!
+
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
+ Two-way
+ Binding
+
+
+
+
+ Resizable
+ Text
+
+
+
+
+
+
+ De-sugared
+ two-way
+ binding
+
+
+
+
+top
+
+
+
+
+ NgModel
+ (two-way)
+ Binding
+
+
+
+ Result:
+ {{
+ currentHero.name
+ }}
+
+
+
+without
+NgModel
+
+
+[(ngModel)]
+
+
+bindon-ngModel
+
+
+(ngModelChange)="...name=$event"
+
+
+(ngModelChange)="setUppercaseName($event)"
+
+top
+
+
+
+
+ NgClass
+ Binding
+
+
+
+ currentClasses
+ is
+ {{
+ currentClasses
+ | json
+ }}
+
+
+ This
+ div
+ is
+ initially
+ saveable,
+ unchanged,
+ and
+ special
+
+
+
+
+
+|
+
+|
+
+
+
+
+ This
+ div
+ should
+ be
+ {{
+ canSave
+ ? ""
+ : "not"
+ }}
+ saveable,
+ {{
+ isUnchanged
+ ? "unchanged"
+ : "modified"
+ }}
+ and,
+ {{
+ isSpecial
+ ? ""
+ : "not"
+ }}
+ special
+ after
+ clicking
+ "Refresh".
+
+
+
+
+ This
+ div
+ is
+ special
+
+
+
+ Bad
+ curly
+ special
+
+
+ Curly
+ special
+
+
+top
+
+
+
+
+ NgStyle
+ Binding
+
+
+
+ This
+ div
+ is
+ x-large
+ or
+ smaller.
+
+
+
+ [ngStyle]
+ binding
+ to
+ currentStyles
+ -
+ CSS
+ property
+ names
+
+
+ currentStyles
+ is
+ {{
+ currentStyles
+ | json
+ }}
+
+
+ This
+ div
+ is
+ initially
+ italic,
+ normal
+ weight,
+ and
+ extra
+ large
+ (24px).
+
+
+
+
+
+|
+
+|
+
+
+
+
+ This
+ div
+ should
+ be
+ {{
+ canSave
+ ? "italic"
+ : "plain"
+ }},
+ {{
+ isUnchanged
+ ? "normal weight"
+ : "bold"
+ }}
+ and,
+ {{
+ isSpecial
+ ? "extra large"
+ : "normal size"
+ }}
+ after
+ clicking
+ "Refresh".
+
+
+top
+
+
+
+
+ NgIf
+ Binding
+
+
+
+
+
+ Hello,
+ {{
+ currentHero.name
+ }}
+
+
+ Hello,
+ {{
+ nullHero.name
+ }}
+
+
+
+
+Add
+ {{
+ currentHero.name
+ }}
+ with
+ template
+
+
+
+ Hero
+ Detail
+ removed
+ from
+ DOM
+ (via
+ template)
+ because
+ isActive
+ is
+ false
+
+
+
+
+
+
+
+ Show
+ with
+ class
+
+
+ Hide
+ with
+ class
+
+
+
+
+
+
+ Show
+ with
+ style
+
+
+ Hide
+ with
+ style
+
+
+top
+
+
+
+
+ NgFor
+ Binding
+
+
+
+
+ {{
+ hero.name
+ }}
+
+
+
+
+
+
+
+
+
+top
+
+
+ *ngFor
+ with
+ index
+
+
+ with
+ semi-colon
+ separator
+
+
+
+ {{
+ i +
+ 1
+ }}
+ -
+ {{
+ hero.name
+ }}
+
+
+
+
+ with
+ comma
+ separator
+
+
+
+
+ {{
+ i +
+ 1
+ }}
+ -
+ {{
+ hero.name
+ }}
+
+
+
+top
+
+
+ *ngFor
+ trackBy
+
+
+
+
+
+
+ without
+ trackBy
+
+
+
+ ({{
+ hero.id
+ }})
+ {{
+ hero.name
+ }}
+
+
+
+ Hero
+ DOM
+ elements
+ change
+ #{{
+ heroesNoTrackByCount
+ }}
+ without
+ trackBy
+
+
+
+
+ with
+ trackBy
+
+
+
+ ({{
+ hero.id
+ }})
+ {{
+ hero.name
+ }}
+
+
+
+ Hero
+ DOM
+ elements
+ change
+ #{{
+ heroesWithTrackByCount
+ }}
+ with
+ trackBy
+
+
+
+
+
+
+ with
+ trackBy
+ and
+ semi-colon
+ separator
+
+
+
+ ({{
+ hero.id
+ }})
+ {{
+ hero.name
+ }}
+
+
+
+
+ with
+ trackBy
+ and
+ comma
+ separator
+
+
+
+ ({{
+ hero.id
+ }})
+ {{
+ hero.name
+ }}
+
+
+
+
+ with
+ trackBy
+ and
+ space
+ separator
+
+
+
+ ({{
+ hero.id
+ }})
+ {{
+ hero.name
+ }}
+
+
+
+
+ with
+ generic
+ trackById
+ function
+
+
+
+ ({{
+ hero.id
+ }})
+ {{
+ hero.name
+ }}
+
+
+
+top
+
+
+
+
+ NgSwitch
+ Binding
+
+
+
+ Pick
+ your
+ favorite
+ hero
+
+
+
+
+
+
+
+
+
+
+ Are
+ you
+ as
+ confused
+ as
+ {{
+ currentHero.name
+ }}?
+
+
+
+
+top
+
+
+
+
+ Template
+ reference
+ variables
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Example
+ Form
+
+
+
+top
+
+
+
+
+ Inputs
+ and
+ Outputs
+
+
+
+
+
+
+
+
+
+ myClick2
+
+{{
+ clickMessage2
+}}
+
+top
+
+
+
+
+ Pipes
+
+
+
+ Title
+ through
+ uppercase
+ pipe:
+ {{
+ title
+ | uppercase
+ }}
+
+
+
+
+ Title
+ through
+ a
+ pipe
+ chain:
+ {{
+ title
+ | uppercase
+ | lowercase
+ }}
+
+
+
+
+ Birthdate:
+ {{
+ currentHero?.birthdate
+ | date
+ : "longDate"
+ }}
+
+
+
+ {{
+ currentHero
+ | json
+ }}
+
+
+
+ Birthdate:
+ {{
+ currentHero?.birthdate
+ | date
+ : "longDate"
+ | uppercase
+ }}
+
+
+
+
+ {{
+ product.price
+ | currency
+ : "USD"
+ : true
+ }}
+
+
+top
+
+
+
+
+ Safe
+ navigation
+ operator
+ ?.
+
+
+
+ The
+ title
+ is
+ {{
+ title
+ }}
+
+
+
+ The
+ current
+ hero's
+ name
+ is
+ {{
+ currentHero?.name
+ }}
+
+
+
+ The
+ current
+ hero's
+ name
+ is
+ {{
+ currentHero.name
+ }}
+
+
+
+
+
+
+ The
+ null
+ hero's
+ name
+ is
+ {{
+ nullHero.name
+ }}
+
+
+
+ The
+ null
+ hero's
+ name
+ is
+ {{
+ nullHero &&
+ nullHero.name
+ }}
+
+
+
+
+ The
+ null
+ hero's
+ name
+ is
+ {{
+ nullHero?.name
+ }}
+
+
+top
+
+
+
+
+ Non-null
+ assertion
+ operator
+ !.
+
+
+
+
+
+ The
+ hero's
+ name
+ is
+ {{
+ hero!
+ .name
+ }}
+
+
+
+top
+
+
+
+
+ $any
+ type
+ cast
+ function
+ $any(
+ ).
+
+
+
+
+
+ The
+ hero's
+ marker
+ is
+ {{
+ $any(
+ hero
+ )
+ .marker
+ }}
+
+
+
+
+
+
+ Undeclared
+ members
+ is
+ {{$any(this).member}}
+
+
+
+top
+
+
+
+
+
+ Enums
+ in
+ binding
+
+
+
+ The
+ name
+ of
+ the
+ Color.Red
+ enum
+ is
+ {{
+ Color[
+ Color
+ .Red
+ ]
+ }}.
+ The
+ current
+ color
+ is
+ {{
+ Color[
+ color
+ ]
+ }}
+ and
+ its
+ number
+ is
+ {{
+ color
+ }}.
+
+
+
+top
+
+
+
+
+
+
+ {{
+ submitMessage
+ }}
+
+
+
+
+
+`;
diff --git a/tests/html_angular/attributes.component.html b/tests/html_angular/attributes.component.html
new file mode 100644
index 000000000..06e4b89b6
--- /dev/null
+++ b/tests/html_angular/attributes.component.html
@@ -0,0 +1,81 @@
+
diff --git a/tests/html_angular/ignore-attribute.component.html b/tests/html_angular/ignore-attribute.component.html
new file mode 100644
index 000000000..93b1e2acf
--- /dev/null
+++ b/tests/html_angular/ignore-attribute.component.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/tests/html_angular/interpolation.component.html b/tests/html_angular/interpolation.component.html
new file mode 100644
index 000000000..d8021e80d
--- /dev/null
+++ b/tests/html_angular/interpolation.component.html
@@ -0,0 +1,52 @@
+{{ a | b : c }}
+{{ 0 - 1 }}
+{{ - 1 }}
+{{ a ? 1 : 2 }}
+{{ a ( 1 ) ( 2 ) }}
+{{ a [ b ] }}
+{{ [ 1 ] }}
+{{ { 'a' : 1 } }}
+{{ { a : 1 } }}
+{{ true }}
+{{ undefined }}
+{{ null }}
+{{ ( 1 ) }}
+{{ 1 }}
+{{ 'hello' }}
+{{ a ( 1 , 2 ) }}
+{{ a . b ( 1 , 2 ) }}
+{{ x ! }}
+{{ ! x }}
+{{ ( ( a ) ) }}
+{{ a }}
+{{ a // hello }}
+{{ a . b }}
+{{ a ?. b ( ) }}
+{{ a ?. b }}
+{{ a // hello }}
+
+
+
+
+
+{{copyTypes[options.copyType]}}
+{{listRow.NextScheduledSendStatus == 1 || listRow.NextScheduledSendStatus == 2 || listRow.NextScheduledSendStatus == 3}}
+{{a}}{{b}}
diff --git a/tests/html_angular/jsfmt.spec.js b/tests/html_angular/jsfmt.spec.js
new file mode 100644
index 000000000..d491f09bd
--- /dev/null
+++ b/tests/html_angular/jsfmt.spec.js
@@ -0,0 +1,3 @@
+run_spec(__dirname, ["angular"]);
+run_spec(__dirname, ["angular"], { trailingComma: "es5" });
+run_spec(__dirname, ["angular"], { printWidth: 1 });
diff --git a/tests/html_angular/real-world.component.html b/tests/html_angular/real-world.component.html
new file mode 100644
index 000000000..b7bdab56b
--- /dev/null
+++ b/tests/html_angular/real-world.component.html
@@ -0,0 +1,718 @@
+
+
+
+Template Syntax
+Interpolation
+Expression context
+Statement context
+Mental Model
+Buttons
+Properties vs. Attributes
+
+Property Binding
+
+
+Event Binding
+Two-way Binding
+
+Directives
+
+
+Template reference variables
+Inputs and outputs
+Pipes
+Safe navigation operator ?.
+Non-null assertion operator !.
+Enums
+
+
+
Interpolation
+
+My current hero is {{currentHero.name}}
+
+
+ {{title}}
+
+
+
+
+The sum of 1 + 1 is {{1 + 1}}
+
+
+The sum of 1 + 1 is not {{1 + 1 + getVal()}}
+
+top
+
+
Expression context
+
+Component expression context ({{title}}, [hidden]="isUnchanged")
+
+ {{title}}
+ changed
+
+
+
+Template input variable expression context (let hero)
+
+
+ {{hero.name}}
+
+
+Template reference variable expression context (#heroInput)
+
+ Type something:
+ {{heroInput.value}}
+
+
+top
+
+
Statement context
+
+Component statement context ( (click)="onSave() )
+
+
+
+
+Template $event statement context
+
+
+
+
+Template input variable statement context (let hero)
+
+
+
+
+
+Template reference variable statement context (#heroForm)
+
+
+
+
+top
+
+
+
New Mental Model
+
+
+
+Mental Model
+
+
+
+
+
+
+ Mental Model
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+click me
+{{clicked}}
+
+
+
+ Hero Name:
+
+ {{name}}
+
+
+
+
+
+
+Special
+
+
+
+
+top
+
+
+
Property vs. Attribute (img examples)
+
+
+
+
+
+
+
+
+
+top
+
+
+
Buttons
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
Property Binding
+
+
+
+[ngClass] binding to the classes property
+
+
+
+
+
+
+
+
+
+
is the interpolated image.
+
is the property bound image.
+
+"{{title}}" is the interpolated title.
+"" is the property bound title.
+
+
+"{{evilTitle}}" is the interpolated evil title.
+"" is the property bound evil title.
+
+top
+
+
+
Attribute Binding
+
+
+
+
+ One-Two
+
+
+
+ Five Six
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
+
Class Binding
+
+
+Bad curly special
+
+
+Bad curly
+
+
+The class binding is special
+
+
+This one is not so special
+
+This class binding is special too
+
+top
+
+
+
Style Binding
+
+
+
+
+
+
+
+top
+
+
+
Event Binding
+
+
+
+
+
+
+
+click with myClick
+{{clickMessage}}
+
+
+
+
+
+
+
+
+
+
+Click me
+ Click me too!
+
+
+
+
+
+
+
+
+
+
+
+
+top
+
+
Two-way Binding
+
+
+ Resizable Text
+
+
+
+
+ De-sugared two-way binding
+
+
+
+top
+
+
+
NgModel (two-way) Binding
+
+Result: {{currentHero.name}}
+
+
+without NgModel
+
+
+[(ngModel)]
+
+
+bindon-ngModel
+
+
+(ngModelChange)="...name=$event"
+
+
+(ngModelChange)="setUppercaseName($event)"
+
+top
+
+
+
NgClass Binding
+
+currentClasses is {{currentClasses | json}}
+This div is initially saveable, unchanged, and special
+
+
+
+ |
+ |
+
+
+
+
+ This div should be {{ canSave ? "": "not"}} saveable,
+ {{ isUnchanged ? "unchanged" : "modified" }} and,
+ {{ isSpecial ? "": "not"}} special after clicking "Refresh".
+
+
+This div is special
+
+Bad curly special
+Curly special
+
+top
+
+
+
NgStyle Binding
+
+
+ This div is x-large or smaller.
+
+
+[ngStyle] binding to currentStyles - CSS property names
+currentStyles is {{currentStyles | json}}
+
+ This div is initially italic, normal weight, and extra large (24px).
+
+
+
+
+ |
+ |
+
+
+
+
+ This div should be {{ canSave ? "italic": "plain"}},
+ {{ isUnchanged ? "normal weight" : "bold" }} and,
+ {{ isSpecial ? "extra large": "normal size"}} after clicking "Refresh".
+
+top
+
+
+
NgIf Binding
+
+
+
+Hello, {{currentHero.name}}
+Hello, {{nullHero.name}}
+
+
+
+Add {{currentHero.name}} with template
+
+
+Hero Detail removed from DOM (via template) because isActive is false
+
+
+
+
+
+Show with class
+Hide with class
+
+
+
+
+Show with style
+Hide with style
+
+top
+
+
+
NgFor Binding
+
+
+ {{hero.name}}
+
+
+
+
+
+
+
+
+top
+
+*ngFor with index
+with semi-colon separator
+
+ {{i + 1}} - {{hero.name}}
+
+
+with comma separator
+
+
+ {{i + 1}} - {{hero.name}}
+
+
+top
+
+*ngFor trackBy
+
+
+
+
+without trackBy
+
+ ({{hero.id}}) {{hero.name}}
+
+
+ Hero DOM elements change #{{heroesNoTrackByCount}} without trackBy
+
+
+
+with trackBy
+
+ ({{hero.id}}) {{hero.name}}
+
+
+ Hero DOM elements change #{{heroesWithTrackByCount}} with trackBy
+
+
+
+
+
+with trackBy and semi-colon separator
+
+
+ ({{hero.id}}) {{hero.name}}
+
+
+
+with trackBy and comma separator
+
+ ({{hero.id}}) {{hero.name}}
+
+
+with trackBy and space separator
+
+ ({{hero.id}}) {{hero.name}}
+
+
+with generic trackById function
+
+ ({{hero.id}}) {{hero.name}}
+
+
+top
+
+
+
NgSwitch Binding
+
+Pick your favorite hero
+
+
+
+
+
+
+
+
+ Are you as confused as {{currentHero.name}}?
+
+
+
+top
+
+
+
Template reference variables
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Example Form
+
+
+top
+
+
+
Inputs and Outputs
+
+
+
+
+
+
+
+myClick2
+{{clickMessage2}}
+
+top
+
+
+
Pipes
+
+Title through uppercase pipe: {{title | uppercase}}
+
+
+
+ Title through a pipe chain:
+ {{title | uppercase | lowercase}}
+
+
+
+Birthdate: {{currentHero?.birthdate | date:'longDate'}}
+
+{{currentHero | json}}
+
+Birthdate: {{(currentHero?.birthdate | date:'longDate') | uppercase}}
+
+
+
+ {{product.price | currency:'USD':true}}
+
+
+top
+
+
+
Safe navigation operator ?.
+
+
+ The title is {{title}}
+
+
+
+ The current hero's name is {{currentHero?.name}}
+
+
+
+ The current hero's name is {{currentHero.name}}
+
+
+
+
+
+
+The null hero's name is {{nullHero.name}}
+
+
+The null hero's name is {{nullHero && nullHero.name}}
+
+
+
+
+ The null hero's name is {{nullHero?.name}}
+
+
+
+top
+
+
+
Non-null assertion operator !.
+
+
+
+
+ The hero's name is {{hero!.name}}
+
+
+
+top
+
+
+
$any type cast function $any( ).
+
+
+
+
+ The hero's marker is {{$any(hero).marker}}
+
+
+
+
+
+
+ Undeclared members is {{$any(this).member}}
+
+
+
+top
+
+
+
+
Enums in binding
+
+
+ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ The current color is {{Color[color]}} and its number is {{color}}.
+
+
+
+top
+
+
+
+
+
+
+
+ {{submitMessage}}
+
+
+
+
+
diff --git a/tests/html_attributes/__snapshots__/jsfmt.spec.js.snap b/tests/html_attributes/__snapshots__/jsfmt.spec.js.snap
index dbdf774ea..ec16f2fce 100644
--- a/tests/html_attributes/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/html_attributes/__snapshots__/jsfmt.spec.js.snap
@@ -175,33 +175,33 @@ exports[`boolean.html - html-verify 1`] = `
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -237,6 +237,48 @@ exports[`single-quotes.html - html-verify 1`] = `
`;
+exports[`srcset.html - html-verify 1`] = `
+
+
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+
+
+`;
+
exports[`without-quotes.html - html-verify 1`] = `
String
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/html_attributes/srcset.html b/tests/html_attributes/srcset.html
new file mode 100644
index 000000000..a320c725d
--- /dev/null
+++ b/tests/html_attributes/srcset.html
@@ -0,0 +1,9 @@
+
+
+
diff --git a/tests/html_basics/__snapshots__/jsfmt.spec.js.snap b/tests/html_basics/__snapshots__/jsfmt.spec.js.snap
index cfbe45c3a..f075a924d 100644
--- a/tests/html_basics/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/html_basics/__snapshots__/jsfmt.spec.js.snap
@@ -260,17 +260,6 @@ exports[`html-comments.html - html-verify 1`] = `
`;
-exports[`html-fragment.html - html-verify 1`] = `
-Link
-
-
-- Foo Bar -+
Foo Bar
Foo Bar
Foo Bar-
- Foo Bar -+
Foo Bar
___________________________
@@ -874,19 +852,16 @@ ___________________________
both spaces and
line breaks
+
-- Foo Bar -+
Foo Bar
Foo Bar
Foo Bar-
- Foo Bar -+
Foo Bar
- Foo Bar -+
Foo Bar
Foo Bar
Foo Bar-
- Foo Bar -+
Foo Bar
___________________________
@@ -1228,19 +1200,16 @@ ___________________________
both spaces and
line breaks
+
-- Foo Bar -+
Foo Bar
Foo Bar
Foo Bar-
- Foo Bar -+
Foo Bar
___________________________
@@ -1400,19 +1369,16 @@ ___________________________
both spaces and
line breaks
+
-- Foo Bar -+
Foo Bar
Foo Bar
Foo Bar-
- Foo Bar -+
Foo Bar
___________________________ @@ -1519,6 +1485,55 @@ exports[`tags.html - html-verify 1`] = ` > *200123 +
123456+x
+x
+x
+ + + | + +
+
+ + +
+
+ + +
+"" is the property bound title.
+
x
+x
+x
+ + + +|"" is the property bound title.
+x
+x
+x
+ + + | + +"" is the property bound title.
++ x +
++ x +
++ x +
+ + + +| + ++ "" + is + the + property + bound + title. +
+x
+x
+x
+ + + | + +"" is the property bound title.
+x
+x
+x
+ + + |"" is the property bound title.
+x
+x
+x
+ + + | + +"" is the property bound title.
+x
+x
x
+ + +|"" is the property bound title.
+x
+x
+x
+ + + | + +"" is the property bound title.
++ x + +
++ x + +
++ x + +
+ + + +| + ++ " + + " is the + property bound + title. +
+x
+x
+x
+ + + | + +"" is the property bound title.
+++ + + ++++ + + ++
+ {{ code }}
+
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+++ + + ++++ + + ++
+ {{ code }}
+
+
+
+`;
+
+exports[`pre-child.vue - vue-verify 2`] = `
+
+++ + + ++++ + + ++
+ {{ code }}
+
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+++ + + ++++ + + ++
+ {{ code }}
+
+
+
+`;
+
+exports[`script_src.vue - vue-verify 1`] = `
+
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+
+`;
+
+exports[`script_src.vue - vue-verify 2`] = `
+
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+
+`;
+
+exports[`self_closing.vue - vue-verify 1`] = `
+
+
+
+
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+
+
+
+
+`;
+
+exports[`self_closing.vue - vue-verify 2`] = `
+
+
+
+
+
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+
+
+
+
+`;
+
+exports[`self_closing_style.vue - vue-verify 1`] = `
+
+ + + {{ fileSizeReadable }} + + + | + + + W: {{ width }} | H: {{ height }} + +
++ + {{ fileSizeReadable }} + + + | + + + W: {{ width }} | H: {{ height }} + +
++ + {{ fileSizeReadable }} + + + | + + + W: {{ width }} | H: {{ height }} + +
++ + {{ fileSizeReadable }} + + + | + + + W: {{ width }} | H: {{ height }} + +
+++ + + ++++ + + ++
+ {{ code }}
+
+
diff --git a/tests/vue_examples/script_src.vue b/tests/html_vue/script_src.vue
similarity index 100%
rename from tests/vue_examples/script_src.vue
rename to tests/html_vue/script_src.vue
diff --git a/tests/vue_examples/self_closing.vue b/tests/html_vue/self_closing.vue
similarity index 100%
rename from tests/vue_examples/self_closing.vue
rename to tests/html_vue/self_closing.vue
diff --git a/tests/vue_examples/self_closing_style.vue b/tests/html_vue/self_closing_style.vue
similarity index 100%
rename from tests/vue_examples/self_closing_style.vue
rename to tests/html_vue/self_closing_style.vue
diff --git a/tests/html_vue/template.vue b/tests/html_vue/template.vue
new file mode 100644
index 000000000..32583b345
--- /dev/null
+++ b/tests/html_vue/template.vue
@@ -0,0 +1,27 @@
+
+
+ + + {{ fileSizeReadable }} + + + | + + + W: {{ width }} | H: {{ height }} + +
+