diff --git a/package.json b/package.json
index 39f05e4d3176..0953fb4e0c71 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 e1fb4eb5be43..40db88202388 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 8fcea3f55cc8..d071a32d79f5 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 a1258a993298..7bea29ca574c 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 000000000000..13b6c9ba8aa2
--- /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 6c16c1db8c9d..c14bb094b0d1 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 c56fc7f01b52..2ce6efaf500b 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 3a9be5104135..e90f51a210a1 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 1e3469644242..ff929e712220 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 } = {}) {
- const { frontMatter, content } = shouldParseFrontMatter
- ? parseFrontMatter(text)
- : { frontMatter: null, content: text };
+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");
- // 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 279431e5ef46..000000000000 --- 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 000000000000..46419cab91b7 --- /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 000000000000..234bacaf0cb9 --- /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 1b8eed5f390e..cd50b31a1d94 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 9c7bf0fb3259..0c6e61baec1c 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 f182ad27ccaf..a47585906179 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()) + ); +} +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()) ); - if ( - isWithinArrayValueFromProperty && - isPropertyWithinAngularComponentDecorator(path, 4) - ) { - if (parentParent.key && parentParent.key.name === "styles") { - return true; - } - } - return false; +} +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 000000000000..411a2e9988ad --- /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 53144ec55b55..4d65e92987ea 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 000000000000..1055ab50e96a --- /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 058733a8e3d2..aa26b2783d56 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 3548cd85dc93..57489c7b7ae4 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 bfa261b1fc42..eeae7da53301 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 b8a7a946f0a9..5721041bad15 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 119e9898749f..000000000000 --- 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 c6ba7dcd7a83..000000000000 --- 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 7dce2d762b53..000000000000 --- 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 5a8f54c0f340..000000000000 --- 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 3a784fbc8619..e32146e37ffc 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 4049f4aa8fae..ab88c571a23c 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 6ce2a0fb79b5..0aaf0d10eeb3 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 267a4eb94205..4b5abc04c360 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: \`+ 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 000000000000..8885a5343b2b --- /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 000000000000..09a639b7b7f2 --- /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 000000000000..fc56a8e99723 --- /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 000000000000..9477cadb738a --- /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 9eb75a0c3835..c57fd208ad60 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- test
+(); 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 000000000000..7ab4c3c1a7ce --- /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+ +
+++ +top + + +De-sugared two-way binding
++ 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
++ ++ +top + +{{i + 1}} - {{hero.name}}+*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
+++ +top + + +({{hero.id}}) {{hero.name}}+NgSwitch Binding
+ +Pick your favorite hero
++ ++ +++ +top + + ++ + + Are you as confused as {{currentHero.name}}?++ 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 !.
+ ++ ++ +top + + ++ The hero's name is {{hero!.name}} ++$any type cast function $any( ).
+ ++ ++ ++ The hero's marker is {{$any(hero).marker}} +++ ++ +top + + + ++ Undeclared members is {{$any(this).member}} ++Enums in binding
+ ++ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ +top + + + + +
+ The current color is {{Color[color]}} and its number is {{color}}.
+ ++ ++ + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + ++ {{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+ +
+++ +top + + +De-sugared two-way binding
++
+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
++ ++ +top + ++ {{ i + 1 }} - {{ hero.name }} ++*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
+++ +top + + ++ ({{ hero.id }}) {{ hero.name }} ++
+NgSwitch Binding
+ +Pick your favorite hero
++ ++ +++ +top + + ++ + + + Are you as confused as {{ currentHero.name }}? +++
+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 !.
+ ++ ++ +top + + +The hero's name is {{ hero!.name }}+
+$any type cast function $any( ).
+ ++ ++ +The hero's marker is {{ $any(hero).marker }}++ ++ +top + + + +Undeclared members is {{$any(this).member}}+
+Enums in binding
+ ++ The name of the Color.Red enum is {{ Color[Color.Red] }}.
+ +top + + + +
+ The current color is {{ Color[color] }} and its number is {{ color }}.
+ ++ ++ + + +`; + +exports[`real-world.component.html - angular-verify 2`] = ` + + + +{{ 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+ +
+++ +top + + +De-sugared two-way binding
++ 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
++ ++ +top + +{{i + 1}} - {{hero.name}}+*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
+++ +top + + +({{hero.id}}) {{hero.name}}+NgSwitch Binding
+ +Pick your favorite hero
++ ++ +++ +top + + ++ + + Are you as confused as {{currentHero.name}}?++ 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 !.
+ ++ ++ +top + + ++ The hero's name is {{hero!.name}} ++$any type cast function $any( ).
+ ++ ++ ++ The hero's marker is {{$any(hero).marker}} +++ ++ +top + + + ++ Undeclared members is {{$any(this).member}} ++Enums in binding
+ ++ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ +top + + + + +
+ The current color is {{Color[color]}} and its number is {{color}}.
+ ++ ++ + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + ++ {{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+ +
+++ +top + + +De-sugared two-way binding
++
+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
++ ++ +top + ++ {{ i + 1 }} - {{ hero.name }} ++*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
+++ +top + + ++ ({{ hero.id }}) {{ hero.name }} ++
+NgSwitch Binding
+ +Pick your favorite hero
++ ++ +++ +top + + ++ + + + Are you as confused as {{ currentHero.name }}? +++
+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 !.
+ ++ ++ +top + + +The hero's name is {{ hero!.name }}+
+$any type cast function $any( ).
+ ++ ++ +The hero's marker is {{ $any(hero).marker }}++ ++ +top + + + +Undeclared members is {{$any(this).member}}+
+Enums in binding
+ ++ The name of the Color.Red enum is {{ Color[Color.Red] }}.
+ +top + + + +
+ The current color is {{ Color[color] }} and its number is {{ color }}.
+ ++ ++ + + +`; + +exports[`real-world.component.html - angular-verify 3`] = ` + + + +{{ 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+ +
+++ +top + + +De-sugared two-way binding
++ 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
++ ++ +top + +{{i + 1}} - {{hero.name}}+*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
+++ +top + + +({{hero.id}}) {{hero.name}}+NgSwitch Binding
+ +Pick your favorite hero
++ ++ +++ +top + + ++ + + Are you as confused as {{currentHero.name}}?++ 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 !.
+ ++ ++ +top + + ++ The hero's name is {{hero!.name}} ++$any type cast function $any( ).
+ ++ ++ ++ The hero's marker is {{$any(hero).marker}} +++ ++ +top + + + ++ Undeclared members is {{$any(this).member}} ++Enums in binding
+ ++ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ +top + + + + +
+ The current color is {{Color[color]}} and its number is {{color}}.
+ ++ ++ + + +~ + + + ++ {{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 ++ +
+++ +top + + ++ De-sugared + two-way + binding +
++
++ 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 +
++ ++ +top + ++ {{ + i + + 1 + }} + - + {{ + hero.name + }} +++ *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 +
+++ +top + + ++ ({{ + hero.id + }}) + {{ + hero.name + }} ++
++ NgSwitch + Binding +
+ ++ Pick + your + favorite + hero +
++ ++ +++ +top + + ++ + + + Are + you + as + confused + as + {{ + currentHero.name + }}? +++
++ 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 + !. +
+ ++ ++ +top + + ++ The + hero's + name + is + {{ + hero! + .name + }} ++
++ $any + type + cast + function + $any( + ). +
+ ++ ++ ++ The + hero's + marker + is + {{ + $any( + hero + ) + .marker + }} +++ ++ +top + + + ++ Undeclared + members + is + {{$any(this).member}} ++
++ Enums + in + binding +
+ ++ The + name + of + the + Color.Red + enum + is + {{ + Color[ + Color + .Red + ] + }}.
+ +top + + + +
+ The + current + color + is + {{ + Color[ + color + ] + }} + and + its + number + is + {{ + color + }}.
+ ++ ++ + + +`; diff --git a/tests/html_angular/attributes.component.html b/tests/html_angular/attributes.component.html new file mode 100644 index 000000000000..06e4b89b6bf8 --- /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 000000000000..93b1e2acf8db --- /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 000000000000..d8021e80db5f --- /dev/null +++ b/tests/html_angular/interpolation.component.html @@ -0,0 +1,52 @@ ++ {{ + submitMessage + }} ++{{ 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 000000000000..d491f09bd527 --- /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 000000000000..b7bdab56ba06 --- /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+ +
+++ +top + + +De-sugared two-way binding
++ 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
++ ++ +top + +{{i + 1}} - {{hero.name}}+*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
+++ +top + + +({{hero.id}}) {{hero.name}}+NgSwitch Binding
+ +Pick your favorite hero
++ ++ +++ +top + + ++ + + Are you as confused as {{currentHero.name}}?++ 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 !.
+ ++ ++ +top + + ++ The hero's name is {{hero!.name}} ++$any type cast function $any( ).
+ ++ ++ ++ The hero's marker is {{$any(hero).marker}} +++ ++ +top + + + ++ Undeclared members is {{$any(this).member}} ++Enums in binding
+ ++ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ +top + + + + +
+ The current color is {{Color[color]}} and its number is {{color}}.
+ ++ ++ + + diff --git a/tests/html_attributes/__snapshots__/jsfmt.spec.js.snap b/tests/html_attributes/__snapshots__/jsfmt.spec.js.snap index dbdf774ea5ae..ec16f2fce22d 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`] = `+ {{submitMessage}} ++String
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/html_attributes/srcset.html b/tests/html_attributes/srcset.html new file mode 100644 index 000000000000..a320c725d100 --- /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 cfbe45c3a0bb..f075a924d472 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`] = ` > *200 123 +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 000000000000..32583b34593e
--- /dev/null
+++ b/tests/html_vue/template.vue
@@ -0,0 +1,27 @@
+
+
+ + + {{ fileSizeReadable }} + + + | + + + W: {{ width }} | H: {{ height }} + +
+