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., ``) are parsed as `{ [attr]: null }` instead of `{ [attr]: "" }` - * - trigger `Handler#onselfclosingtag()` - */ - class CustomParser extends Parser { - constructor(cbs, options) { - super(cbs, options); - this._attribvalue = null; - } - onattribdata(value) { - if (this._attribvalue === null) { - this._attribvalue = ""; - } - super.onattribdata(value); - } - onattribend() { - if (this._cbs.onattribute) { - this._cbs.onattribute(this._attribname, this._attribvalue); - } - if (this._attribs) { - this._attribs.push([this._attribname, this._attribvalue]); - } - this._attribname = ""; - this._attribvalue = null; + const { rootNodes, errors } = parser.parse(input, { canSelfClose }); + + if (errors.length !== 0) { + const { msg, span } = errors[0]; + const { line, col } = span.start; + throw createError(msg, { start: { line: line + 1, column: col } }); + } + + const addType = node => { + if (node instanceof Attribute) { + node.type = "attribute"; + } else if (node instanceof CDATA) { + node.type = "cdata"; + } else if (node instanceof Comment) { + node.type = "comment"; + } else if (node instanceof DocType) { + node.type = "docType"; + } else if (node instanceof Element) { + node.type = "element"; + } else if (node instanceof Text) { + node.type = "text"; + } else { + throw new Error(`Unexpected node ${JSON.stringify(node)}`); } - onselfclosingtag() { - if (this._options.xmlMode || this._options.recognizeSelfClosing) { - const name = this._tagname; - this.onopentagend(); - if (this._stack[this._stack.length - 1] === name) { - this._cbs.onselfclosingtag(); - this._cbs.onclosetag(name); - this._stack.pop(); + }; + + const restoreName = node => { + const namespace = node.name.startsWith(":") + ? node.name.slice(1).split(":")[0] + : null; + const rawName = node.nameSpan ? node.nameSpan.toString() : node.name; + const hasExplicitNamespace = rawName.startsWith(`${namespace}:`); + const name = hasExplicitNamespace + ? rawName.slice(namespace.length + 1) + : rawName; + + node.name = name; + node.namespace = namespace; + node.hasExplicitNamespace = hasExplicitNamespace; + }; + + const restoreNameAndValue = node => { + if (node instanceof Element) { + restoreName(node); + node.attrs.forEach(attr => { + restoreName(attr); + if (!attr.valueSpan) { + attr.value = null; + } else { + attr.value = attr.valueSpan.toString(); + if (/['"]/.test(attr.value[0])) { + attr.value = attr.value.slice(1, -1); + } } - } else { - this.onopentagend(); - } + }); + } else if (node instanceof Comment) { + node.value = node.sourceSpan + .toString() + .slice("".length); + } else if (node instanceof Text) { + node.value = node.sourceSpan.toString(); } - onopentagname(name) { - super.onopentagname(name); - if (this._cbs.onopentag) { - this._attribs = []; + }; + + const lowerCaseIfFn = (text, fn) => { + const lowerCasedText = text.toLowerCase(); + return fn(lowerCasedText) ? lowerCasedText : text; + }; + const normalizeName = node => { + if (node instanceof Element) { + if ( + !node.namespace || + node.namespace === node.tagDefinition.implicitNamespacePrefix + ) { + node.name = lowerCaseIfFn( + node.name, + lowerCasedName => lowerCasedName in HTML_TAGS + ); } + + const CURRENT_HTML_ELEMENT_ATTRIBUTES = + HTML_ELEMENT_ATTRIBUTES[node.name] || Object.create(null); + node.attrs.forEach(attr => { + if (!attr.namespace) { + attr.name = lowerCaseIfFn( + attr.name, + lowerCasedAttrName => + node.name in HTML_ELEMENT_ATTRIBUTES && + (lowerCasedAttrName in HTML_ELEMENT_ATTRIBUTES["*"] || + lowerCasedAttrName in CURRENT_HTML_ELEMENT_ATTRIBUTES) + ); + } + }); } - } + }; - /** - * modifications: - * - add `isSelfClosing` field - * - correct `endIndex` for whitespaces before closing tag end marker (e.g., ``) - */ - class CustomDomHandler extends DomHandler { - onselfclosingtag() { - this._tagStack[this._tagStack.length - 1].isSelfClosing = true; + const fixSourceSpan = node => { + if (node.sourceSpan && node.endSourceSpan) { + node.sourceSpan = new ParseSourceSpan( + node.sourceSpan.start, + node.endSourceSpan.end + ); } - onclosetag() { - const elem = this._tagStack.pop(); - if (this._options.withEndIndices && elem) { - const buffer = this._parser._tokenizer._buffer; - let endIndex = this._parser.endIndex; - while (buffer[endIndex] && buffer[endIndex] !== ">") { - endIndex++; - } - elem.endIndex = buffer[endIndex] ? endIndex : this._parser.endIndex; - } - if (this._elementCB) { - this._elementCB(elem); + }; + + const addTagDefinition = node => { + if (node instanceof Element) { + const tagDefinition = getHtmlTagDefinition(node.name); + if ( + !node.namespace || + node.namespace === tagDefinition.implicitNamespacePrefix + ) { + node.tagDefinition = tagDefinition; + } else { + node.tagDefinition = getHtmlTagDefinition(""); // the default one } } - } - - const handler = new CustomDomHandler({ - withStartIndices: true, - withEndIndices: true - }); + }; - new CustomParser(handler, { - lowerCaseTags: true, // preserve lowercase tag names to avoid false check in htmlparser2 and apply the lowercasing later - lowerCaseAttributeNames: false, - recognizeSelfClosing: true - }).end(content); - - const ast = normalize( - { - type: "root", - children: handler.dom, - startIndex: 0, - endIndex: text.length - }, - text + visitAll( + new class extends RecursiveVisitor { + visit(node) { + addType(node); + restoreNameAndValue(node); + addTagDefinition(node); + normalizeName(node); + fixSourceSpan(node); + } + }(), + rootNodes ); + return rootNodes; +} + +function _parse( + text, + options, + recognizeSelfClosing = false, + shouldParseFrontMatter = true +) { + const { frontMatter, content } = shouldParseFrontMatter + ? parseFrontMatter(text) + : { frontMatter: null, content: text }; + + const rawAst = { + type: "root", + sourceSpan: { start: { offset: 0 }, end: { offset: text.length } }, + children: ngHtmlParser(content, recognizeSelfClosing) + }; + if (frontMatter) { - ast.children.unshift(frontMatter); + rawAst.children.unshift(frontMatter); } - const parseHtml = data => - parse(data, parsers, options, { - shouldParseFrontMatter: false - }); + const ast = new Node(rawAst); - return mapNode(ast, node => { - const ieConditionalComment = parseIeConditionalComment(node, parseHtml); - return ieConditionalComment ? ieConditionalComment : node; + const parseSubHtml = (subContent, startSpan) => { + const { offset } = startSpan; + const fakeContent = text.slice(0, offset).replace(/[^\r\n]/g, " "); + const realContent = subContent; + const subAst = _parse( + fakeContent + realContent, + options, + recognizeSelfClosing, + false + ); + const ParseSourceSpan = subAst.children[0].sourceSpan.constructor; + subAst.sourceSpan = new ParseSourceSpan( + startSpan, + subAst.children[subAst.children.length - 1].sourceSpan.end + ); + const firstText = subAst.children[0]; + if (firstText.length === offset) { + subAst.children.shift(); + } else { + firstText.sourceSpan = new ParseSourceSpan( + firstText.sourceSpan.start.moveBy(offset), + firstText.sourceSpan.end + ); + firstText.value = firstText.value.slice(offset); + } + return subAst; + }; + + const isFakeElement = node => node.type === "element" && !node.nameSpan; + return ast.map(node => { + if (node.children && node.children.some(isFakeElement)) { + const newChildren = []; + + for (const child of node.children) { + if (isFakeElement(child)) { + Array.prototype.push.apply(newChildren, child.children); + } else { + newChildren.push(child); + } + } + + return node.clone({ children: newChildren }); + } + + if (node.type === "comment") { + const ieConditionalComment = parseIeConditionalComment( + node, + parseSubHtml + ); + if (ieConditionalComment) { + return ieConditionalComment; + } + } + + return node; }); } function parseIeConditionalComment(node, parseHtml) { - if (node.type !== "comment") { + if (!node.value) { return null; } - const match = node.data.match(/^(\[if([^\]]*?)\]>)([\s\S]*?))([\s\S]*?) - Object.assign({}, currentNode, { - startIndex: baseIndex + currentNode.startIndex, - endIndex: baseIndex + currentNode.endIndex - }) + const offset = " + */ + nextNode.isSelfClosing || + /** + * 123123 + */ + (prevNode.type === "element" && + prevNode.isSelfClosing && + needsToBorrowPrevClosingTagEndMarker(nextNode)) + ? "" + : !nextNode.isLeadingSpaceSensitive || + preferHardlineAsLeadingSpaces(nextNode) || + /** + * Want to write us a letter? Use ourmailing address. + */ + (needsToBorrowPrevClosingTagEndMarker(nextNode) && + prevNode.lastChild && + needsToBorrowParentClosingTagStartMarker(prevNode.lastChild) && + prevNode.lastChild.lastChild && + needsToBorrowParentClosingTagStartMarker( + prevNode.lastChild.lastChild + )) + ? hardline + : nextNode.hasLeadingSpaces + ? line + : softline; + } +} + +function printOpeningTag(path, options, print) { + const node = path.getValue(); + const forceNotToBreakAttrContent = + node.type === "element" && + node.fullName === "script" && + node.attrs.length === 1 && + node.attrs[0].fullName === "src" && + node.children.length === 0; + return concat([ + printOpeningTagStart(node), + !node.attrs || node.attrs.length === 0 + ? node.isSelfClosing + ? /** + *
+ * ^ + */ + " " + : "" + : 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

+ */ + 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 "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
- * ~ - */ - (nextNode.type === "tag" && - nextNode.isSelfClosing && - nextNode.attributes.length === 0))) || - /** - * 123 - */ - (prevNode.type === "tag" && - prevNode.isSelfClosing && - needsToBorrowPrevClosingTagEndMarker(nextNode)) - ? "" - : !nextNode.isLeadingSpaceSensitive || - preferHardlineAsLeadingSpaces(nextNode) || - /** - * Want to write us a letter? Use our
mailing address. - */ - (needsToBorrowPrevClosingTagEndMarker(nextNode) && - prevNode.lastChild && - needsToBorrowParentClosingTagStartMarker(prevNode.lastChild) && - prevNode.lastChild.lastChild && - needsToBorrowParentClosingTagStartMarker( - prevNode.lastChild.lastChild - )) - ? hardline - : nextNode.hasLeadingSpaces - ? line - : softline; - } -} - -function printOpeningTag(path, options, print) { - const node = path.getValue(); - return concat([ - printOpeningTagStart(node), - !node.attributes || node.attributes.length === 0 - ? node.isSelfClosing - ? /** - *
- * ^ - */ - " " - : "" - : group( - concat([ - node.prev && needsToBorrowNextOpeningTagStartMarker(node.prev) - ? /** - * 123 - */ - breakParent - : "", - indent(concat([line, join(line, path.map(print, "attributes"))])), - node.firstChild && - needsToBorrowParentOpeningTagEndMarker(node.firstChild) - ? /** - * 123456 - */ - "" - : node.isSelfClosing - ? line - : 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

- */ - 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]*?)(]*>)", - "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: \`
  • test
  • -
+ template: \` +
    +
  • test
  • +
\`, styles: [ \` @@ -60,8 +62,10 @@ class TestComponent {} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @Component({ selector: "app-test", - template: \`
  • test
  • -
+ template: \` +
    +
  • test
  • +
\`, styles: [ \` diff --git a/tests/angular_interpolation/__snapshots__/jsfmt.spec.js.snap b/tests/angular_interpolation/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 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(); 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
FiveSix
+ +
+ + +

+ + +
+ + + + + + + +
+ +top + + +

Class Binding

+ + +
Bad curly special
+ + +
Bad curly
+ + +
The class binding is special
+ + +
This one is not so special
+ +
This class binding is special too
+ +top + + +

Style Binding

+ + + + + + + +top + + +

Event Binding

+ + + + + +
+ +
click with myClick
+{{clickMessage}} +
+ + + + +
+ + + + +
Click me +
Click me too!
+
+ + +
+ +
+ + +
+ +
+ +top + +

Two-way Binding

+
+ +
Resizable Text
+ +
+
+
+

De-sugared two-way binding

+ +
+ +top + + +

NgModel (two-way) Binding

+ +

Result: {{currentHero.name}}

+ + +without NgModel +
+ +[(ngModel)] +
+ +bindon-ngModel +
+ +(ngModelChange)="...name=$event" +
+ +(ngModelChange)="setUppercaseName($event)" + +top + + +

NgClass Binding

+ +

currentClasses is {{currentClasses | json}}

+
This div is initially saveable, unchanged, and special
+ + +
+ | + | + + +

+
+ This div should be {{ canSave ? "": "not"}} saveable, + {{ isUnchanged ? "unchanged" : "modified" }} and, + {{ isSpecial ? "": "not"}} special after clicking "Refresh".
+

+ +
This div is special
+ +
Bad curly special
+
Curly special
+ +top + + +

NgStyle Binding

+ +
+ This div is x-large or smaller. +
+ +

[ngStyle] binding to currentStyles - CSS property names

+

currentStyles is {{currentStyles | json}}

+
+ This div is initially italic, normal weight, and extra large (24px). +
+ + +
+ | + | + + +

+
+ This div should be {{ canSave ? "italic": "plain"}}, + {{ isUnchanged ? "normal weight" : "bold" }} and, + {{ isSpecial ? "extra large": "normal size"}} after clicking "Refresh".
+ +top + + +

NgIf Binding

+ + + +
Hello, {{currentHero.name}}
+
Hello, {{nullHero.name}}
+ + + +Add {{currentHero.name}} with template + + +
Hero Detail removed from DOM (via template) because isActive is false
+ + + + + +
Show with class
+
Hide with class
+ + + + +
Show with style
+
Hide with style
+ +top + + +

NgFor Binding

+ +
+
{{hero.name}}
+
+
+ +
+ + +
+ +top + +

*ngFor with index

+

with semi-colon separator

+
+
{{i + 1}} - {{hero.name}}
+
+ +

with comma separator

+
+ +
{{i + 1}} - {{hero.name}}
+
+ +top + +

*ngFor trackBy

+ + + + +

without trackBy

+
+
({{hero.id}}) {{hero.name}}
+ +
+ Hero DOM elements change #{{heroesNoTrackByCount}} without trackBy +
+
+ +

with trackBy

+
+
({{hero.id}}) {{hero.name}}
+ +
+ Hero DOM elements change #{{heroesWithTrackByCount}} with trackBy +
+
+ +


+ +

with trackBy and semi-colon separator

+
+
+ ({{hero.id}}) {{hero.name}} +
+
+ +

with trackBy and comma separator

+
+
({{hero.id}}) {{hero.name}}
+
+ +

with trackBy and space separator

+
+
({{hero.id}}) {{hero.name}}
+
+ +

with generic trackById function

+
+
({{hero.id}}) {{hero.name}}
+
+ +top + + +

NgSwitch Binding

+ +

Pick your favorite hero

+
+ +
+ +
+ + + +
Are you as confused as {{currentHero.name}}?
+ +
+ +top + + +

Template reference variables

+ + + + + + + + + + + + + + +

Example Form

+ + +top + + +

Inputs and Outputs

+ + + + + + + +
myClick2
+{{clickMessage2}} + +top + + +

Pipes

+ +
Title through uppercase pipe: {{title | uppercase}}
+ + +
+ Title through a pipe chain: + {{title | uppercase | lowercase}} +
+ + +
Birthdate: {{currentHero?.birthdate | date:'longDate'}}
+ +
{{currentHero | json}}
+ +
Birthdate: {{(currentHero?.birthdate | date:'longDate') | uppercase}}
+ +
+ + {{product.price | currency:'USD':true}} +
+ +top + + +

Safe navigation operator ?.

+ +
+ The title is {{title}} +
+ +
+ The current hero's name is {{currentHero?.name}} +
+ +
+ The current hero's name is {{currentHero.name}} +
+ + + + + +
The null hero's name is {{nullHero.name}}
+ +
+The null hero's name is {{nullHero && nullHero.name}} +
+ +
+ + The null hero's name is {{nullHero?.name}} +
+ + +top + + +

Non-null assertion operator !.

+ +
+ +
+ The hero's name is {{hero!.name}} +
+
+ +top + + +

$any type cast function $any( ).

+ +
+ +
+ The hero's marker is {{$any(hero).marker}} +
+
+ +
+ +
+ Undeclared members is {{$any(this).member}} +
+
+ +top + + + +

Enums in binding

+ +

+ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ The current color is {{Color[color]}} and its number is {{color}}.
+ +

+ +top + + + + +
+
+
+ +
+ +
+
+ {{submitMessage}} +
+
+ + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + +

Template Syntax

+Interpolation
+Expression context
+Statement context
+Mental Model
+Buttons
+Properties vs. Attributes
+
+Property Binding
+ +
+Event Binding
+Two-way Binding
+
+
Directives
+ +
+Template reference variables
+Inputs and outputs
+Pipes
+Safe navigation operator ?.
+Non-null assertion operator !.
+Enums
+ + +
+

Interpolation

+ +

My current hero is {{ currentHero.name }}

+ +

{{ title }}

+ + +

The sum of 1 + 1 is {{ 1 + 1 }}

+ + +

The sum of 1 + 1 is not {{ 1 + 1 + getVal() }}

+ +top + +
+

Expression context

+ +

+ Component expression context ({{title}}, + [hidden]="isUnchanged") +

+
+ {{ title }} changed +
+ +

Template input variable expression context (let hero)

+ + +
{{ hero.name }}
+
+ +

Template reference variable expression context (#heroInput)

+
+ Type something: {{ heroInput.value }} +
+ +top + +
+

Statement context

+ +

Component statement context ( (click)="onSave() )

+
+ +

Template $event statement context

+
+ +

Template input variable statement context (let hero)

+ +
+ +
+ +

Template reference variable statement context (#heroForm)

+
+
...
+
+ +top + + +
+

New Mental Model

+ + + +
Mental Model
+

+ +
+ +
Mental Model
+ + +
+

+ +
+ + +
+

+ +
+ + +
+
+

+ + + +
click me
+{{ clicked }}

+ +
Hero Name: {{ name }}
+

+ +

+ +
Special
+

+ + + +top + + +
+

Property vs. Attribute (img examples)

+ + + +

+ + + + +top + + +
+

Buttons

+ + +

+ + +

+ + + +top + + +
+

Property Binding

+ + + +
[ngClass] binding to the classes property
+ + + + +
+ + +

is the interpolated image.

+

is the property bound image.

+ +

+ "{{ title }}" is the interpolated title. +

+

"" is the property bound title.

+ + +

+ "{{ evilTitle }}" is the interpolated evil title. +

+

+ "" is the property bound evil + title. +

+ +top + + +
+

Attribute Binding

+ + + + + + + + + + + + + + +
One-Two
FiveSix
+ +
+ + +

+ + +
+ + + + + + + +
+ +top + + +
+

Class Binding

+ + +
Bad curly special
+ + +
Bad curly
+ + +
The class binding is special
+ + +
+ This one is not so special +
+ +
This class binding is special too
+ +top + + +
+

Style Binding

+ + + + + + + +top + + +
+

Event Binding

+ + + + + +
+ +
click with myClick
+ {{ clickMessage }} +
+ + + +
+ + + + +
+ Click me +
Click me too!
+
+ + +
+ +
+ + +
+ +
+ +top + +
+

Two-way Binding

+
+ +
Resizable Text
+ +
+
+
+

De-sugared two-way binding

+ +
+ +top + + +
+

NgModel (two-way) Binding

+ +

Result: {{ currentHero.name }}

+ + +without NgModel
+ [(ngModel)]
+ bindon-ngModel
+ +(ngModelChange)="...name=$event"
+ +(ngModelChange)="setUppercaseName($event)" + +top + + +
+

NgClass Binding

+ +

currentClasses is {{ currentClasses | json }}

+
+ This div is initially saveable, unchanged, and special +
+ + +
+ | + +| + +

+
+ This div should be {{ canSave ? "" : "not" }} saveable, + {{ isUnchanged ? "unchanged" : "modified" }} and, + {{ isSpecial ? "" : "not" }} special after clicking "Refresh". +
+

+ +
This div is special
+ +
Bad curly special
+
Curly special
+ +top + + +
+

NgStyle Binding

+ +
+ This div is x-large or smaller. +
+ +

[ngStyle] binding to currentStyles - CSS property names

+

currentStyles is {{ currentStyles | json }}

+
+ This div is initially italic, normal weight, and extra large (24px). +
+ + +
+ | + | + +

+
+ This div should be {{ canSave ? "italic" : "plain" }}, + {{ isUnchanged ? "normal weight" : "bold" }} and, + {{ isSpecial ? "extra large" : "normal size" }} after clicking "Refresh". +
+ +top + + +
+

NgIf Binding

+ + + +
Hello, {{ currentHero.name }}
+
Hello, {{ nullHero.name }}
+ + + +Add {{ currentHero.name }} with template + + +
Hero Detail removed from DOM (via template) because isActive is false
+ + + + + +
Show with class
+
Hide with class
+ + + + +
Show with style
+
Hide with style
+ +top + + +
+

NgFor Binding

+ +
+
{{ hero.name }}
+
+
+ +
+ + +
+ +top + +

*ngFor with index

+

with semi-colon separator

+
+
+ {{ i + 1 }} - {{ hero.name }} +
+
+ +

with comma separator

+
+ +
+ {{ i + 1 }} - {{ hero.name }} +
+
+ +top + +

*ngFor trackBy

+ + + + +

without trackBy

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+ +
+ Hero DOM elements change #{{ heroesNoTrackByCount }} without trackBy +
+
+ +

with trackBy

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+ +
+ Hero DOM elements change #{{ heroesWithTrackByCount }} with trackBy +
+
+ +


+ +

with trackBy and semi-colon separator

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+
+ +

with trackBy and comma separator

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+
+ +

with trackBy and space separator

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+
+ +

with generic trackById function

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+
+ +top + + +
+

NgSwitch Binding

+ +

Pick your favorite hero

+
+ +
+ +
+ + + +
+ Are you as confused as {{ currentHero.name }}? +
+ +
+ +top + + +
+

Template reference variables

+ + + + + + + + + + + + + + +

Example Form

+ + +top + + +
+

Inputs and Outputs

+ + + + + + +
myClick2
+{{ clickMessage2 }} + +top + + +
+

Pipes

+ +
Title through uppercase pipe: {{ title | uppercase }}
+ + +
Title through a pipe chain: {{ title | uppercase | lowercase }}
+ + +
Birthdate: {{ currentHero?.birthdate | date: "longDate" }}
+ +
{{ currentHero | json }}
+ +
+ Birthdate: {{ currentHero?.birthdate | date: "longDate" | uppercase }} +
+ +
+ + {{ product.price | currency: "USD":true }} +
+ +top + + +
+

Safe navigation operator ?.

+ +
The title is {{ title }}
+ +
The current hero's name is {{ currentHero?.name }}
+ +
The current hero's name is {{ currentHero.name }}
+ + + + +
The null hero's name is {{ nullHero.name }}
+ +
The null hero's name is {{ nullHero && nullHero.name }}
+ +
+ + The null hero's name is {{ nullHero?.name }} +
+ +top + + +
+

Non-null assertion operator !.

+ +
+ +
The hero's name is {{ hero!.name }}
+
+ +top + + +
+

$any type cast function $any( ).

+ +
+ +
The hero's marker is {{ $any(hero).marker }}
+
+ +
+ +
Undeclared members is {{$any(this).member}}
+
+ +top + + + +
+

Enums in binding

+ +

+ The name of the Color.Red enum is {{ Color[Color.Red] }}.
+ The current color is {{ Color[color] }} and its number is {{ color }}.
+ +

+ +top + + + +
+
+
+ +
+ +
+
{{ submitMessage }}
+
+ + + +`; + +exports[`real-world.component.html - angular-verify 2`] = ` + + + +

Template Syntax

+Interpolation
+Expression context
+Statement context
+Mental Model
+Buttons
+Properties vs. Attributes
+
+Property Binding
+ +
+Event Binding
+Two-way Binding
+
+
Directives
+ +
+Template reference variables
+Inputs and outputs
+Pipes
+Safe navigation operator ?.
+Non-null assertion operator !.
+Enums
+ + +

Interpolation

+ +

My current hero is {{currentHero.name}}

+ +

+ {{title}} + +

+ + +

The sum of 1 + 1 is {{1 + 1}}

+ + +

The sum of 1 + 1 is not {{1 + 1 + getVal()}}

+ +top + +

Expression context

+ +

Component expression context ({{title}}, [hidden]="isUnchanged")

+
+ {{title}} + changed +
+ + +

Template input variable expression context (let hero)

+ + +
{{hero.name}}
+
+ +

Template reference variable expression context (#heroInput)

+
+ Type something: + {{heroInput.value}} +
+ +top + +

Statement context

+ +

Component statement context ( (click)="onSave() ) +

+ +
+ +

Template $event statement context

+
+ +
+ +

Template input variable statement context (let hero)

+ +
+ +
+ +

Template reference variable statement context (#heroForm)

+
+
...
+
+ +top + + +

New Mental Model

+ + + +
Mental Model
+ + +

+ +
+ +
Mental Model
+ + +
+

+ +
+ + +
+

+ +
+ + +
+
+

+ + + +
click me
+{{clicked}} +

+ +
+ Hero Name: + + {{name}} +
+

+ + +

+ +
Special
+

+ + + +top + + +

Property vs. Attribute (img examples)

+ + + +

+ + + + + +top + + +

Buttons

+ + + + +

+ + +

+ + + +top + + +

Property Binding

+ + + +
[ngClass] binding to the classes property
+ + + + +
+ +
+ + +

is the interpolated image.

+

is the property bound image.

+ +

"{{title}}" is the interpolated title.

+

"" is the property bound title.

+ + +

"{{evilTitle}}" is the interpolated evil title.

+

"" is the property bound evil title.

+ +top + + +

Attribute Binding

+ + + + + + + + + +
One-Two
FiveSix
+ +
+ + +

+ + +
+ + + + + + + +
+ +top + + +

Class Binding

+ + +
Bad curly special
+ + +
Bad curly
+ + +
The class binding is special
+ + +
This one is not so special
+ +
This class binding is special too
+ +top + + +

Style Binding

+ + + + + + + +top + + +

Event Binding

+ + + + + +
+ +
click with myClick
+{{clickMessage}} +
+ + + + +
+ + + + +
Click me +
Click me too!
+
+ + +
+ +
+ + +
+ +
+ +top + +

Two-way Binding

+
+ +
Resizable Text
+ +
+
+
+

De-sugared two-way binding

+ +
+ +top + + +

NgModel (two-way) Binding

+ +

Result: {{currentHero.name}}

+ + +without NgModel +
+ +[(ngModel)] +
+ +bindon-ngModel +
+ +(ngModelChange)="...name=$event" +
+ +(ngModelChange)="setUppercaseName($event)" + +top + + +

NgClass Binding

+ +

currentClasses is {{currentClasses | json}}

+
This div is initially saveable, unchanged, and special
+ + +
+ | + | + + +

+
+ This div should be {{ canSave ? "": "not"}} saveable, + {{ isUnchanged ? "unchanged" : "modified" }} and, + {{ isSpecial ? "": "not"}} special after clicking "Refresh".
+

+ +
This div is special
+ +
Bad curly special
+
Curly special
+ +top + + +

NgStyle Binding

+ +
+ This div is x-large or smaller. +
+ +

[ngStyle] binding to currentStyles - CSS property names

+

currentStyles is {{currentStyles | json}}

+
+ This div is initially italic, normal weight, and extra large (24px). +
+ + +
+ | + | + + +

+
+ This div should be {{ canSave ? "italic": "plain"}}, + {{ isUnchanged ? "normal weight" : "bold" }} and, + {{ isSpecial ? "extra large": "normal size"}} after clicking "Refresh".
+ +top + + +

NgIf Binding

+ + + +
Hello, {{currentHero.name}}
+
Hello, {{nullHero.name}}
+ + + +Add {{currentHero.name}} with template + + +
Hero Detail removed from DOM (via template) because isActive is false
+ + + + + +
Show with class
+
Hide with class
+ + + + +
Show with style
+
Hide with style
+ +top + + +

NgFor Binding

+ +
+
{{hero.name}}
+
+
+ +
+ + +
+ +top + +

*ngFor with index

+

with semi-colon separator

+
+
{{i + 1}} - {{hero.name}}
+
+ +

with comma separator

+
+ +
{{i + 1}} - {{hero.name}}
+
+ +top + +

*ngFor trackBy

+ + + + +

without trackBy

+
+
({{hero.id}}) {{hero.name}}
+ +
+ Hero DOM elements change #{{heroesNoTrackByCount}} without trackBy +
+
+ +

with trackBy

+
+
({{hero.id}}) {{hero.name}}
+ +
+ Hero DOM elements change #{{heroesWithTrackByCount}} with trackBy +
+
+ +


+ +

with trackBy and semi-colon separator

+
+
+ ({{hero.id}}) {{hero.name}} +
+
+ +

with trackBy and comma separator

+
+
({{hero.id}}) {{hero.name}}
+
+ +

with trackBy and space separator

+
+
({{hero.id}}) {{hero.name}}
+
+ +

with generic trackById function

+
+
({{hero.id}}) {{hero.name}}
+
+ +top + + +

NgSwitch Binding

+ +

Pick your favorite hero

+
+ +
+ +
+ + + +
Are you as confused as {{currentHero.name}}?
+ +
+ +top + + +

Template reference variables

+ + + + + + + + + + + + + + +

Example Form

+ + +top + + +

Inputs and Outputs

+ + + + + + + +
myClick2
+{{clickMessage2}} + +top + + +

Pipes

+ +
Title through uppercase pipe: {{title | uppercase}}
+ + +
+ Title through a pipe chain: + {{title | uppercase | lowercase}} +
+ + +
Birthdate: {{currentHero?.birthdate | date:'longDate'}}
+ +
{{currentHero | json}}
+ +
Birthdate: {{(currentHero?.birthdate | date:'longDate') | uppercase}}
+ +
+ + {{product.price | currency:'USD':true}} +
+ +top + + +

Safe navigation operator ?.

+ +
+ The title is {{title}} +
+ +
+ The current hero's name is {{currentHero?.name}} +
+ +
+ The current hero's name is {{currentHero.name}} +
+ + + + + +
The null hero's name is {{nullHero.name}}
+ +
+The null hero's name is {{nullHero && nullHero.name}} +
+ +
+ + The null hero's name is {{nullHero?.name}} +
+ + +top + + +

Non-null assertion operator !.

+ +
+ +
+ The hero's name is {{hero!.name}} +
+
+ +top + + +

$any type cast function $any( ).

+ +
+ +
+ The hero's marker is {{$any(hero).marker}} +
+
+ +
+ +
+ Undeclared members is {{$any(this).member}} +
+
+ +top + + + +

Enums in binding

+ +

+ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ The current color is {{Color[color]}} and its number is {{color}}.
+ +

+ +top + + + + +
+
+
+ +
+ +
+
+ {{submitMessage}} +
+
+ + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + +

Template Syntax

+Interpolation
+Expression context
+Statement context
+Mental Model
+Buttons
+Properties vs. Attributes
+
+Property Binding
+ +
+Event Binding
+Two-way Binding
+
+
Directives
+ +
+Template reference variables
+Inputs and outputs
+Pipes
+Safe navigation operator ?.
+Non-null assertion operator !.
+Enums
+ + +
+

Interpolation

+ +

My current hero is {{ currentHero.name }}

+ +

{{ title }}

+ + +

The sum of 1 + 1 is {{ 1 + 1 }}

+ + +

The sum of 1 + 1 is not {{ 1 + 1 + getVal() }}

+ +top + +
+

Expression context

+ +

+ Component expression context ({{title}}, + [hidden]="isUnchanged") +

+
+ {{ title }} changed +
+ +

Template input variable expression context (let hero)

+ + +
{{ hero.name }}
+
+ +

Template reference variable expression context (#heroInput)

+
+ Type something: {{ heroInput.value }} +
+ +top + +
+

Statement context

+ +

Component statement context ( (click)="onSave() )

+
+ +

Template $event statement context

+
+ +

Template input variable statement context (let hero)

+ +
+ +
+ +

Template reference variable statement context (#heroForm)

+
+
...
+
+ +top + + +
+

New Mental Model

+ + + +
Mental Model
+

+ +
+ +
Mental Model
+ + +
+

+ +
+ + +
+

+ +
+ + +
+
+

+ + + +
click me
+{{ clicked }}

+ +
Hero Name: {{ name }}
+

+ +

+ +
Special
+

+ + + +top + + +
+

Property vs. Attribute (img examples)

+ + + +

+ + + + +top + + +
+

Buttons

+ + +

+ + +

+ + + +top + + +
+

Property Binding

+ + + +
[ngClass] binding to the classes property
+ + + + +
+ + +

is the interpolated image.

+

is the property bound image.

+ +

+ "{{ title }}" is the interpolated title. +

+

"" is the property bound title.

+ + +

+ "{{ evilTitle }}" is the interpolated evil title. +

+

+ "" is the property bound evil + title. +

+ +top + + +
+

Attribute Binding

+ + + + + + + + + + + + + + +
One-Two
FiveSix
+ +
+ + +

+ + +
+ + + + + + + +
+ +top + + +
+

Class Binding

+ + +
Bad curly special
+ + +
Bad curly
+ + +
The class binding is special
+ + +
+ This one is not so special +
+ +
This class binding is special too
+ +top + + +
+

Style Binding

+ + + + + + + +top + + +
+

Event Binding

+ + + + + +
+ +
click with myClick
+ {{ clickMessage }} +
+ + + +
+ + + + +
+ Click me +
Click me too!
+
+ + +
+ +
+ + +
+ +
+ +top + +
+

Two-way Binding

+
+ +
Resizable Text
+ +
+
+
+

De-sugared two-way binding

+ +
+ +top + + +
+

NgModel (two-way) Binding

+ +

Result: {{ currentHero.name }}

+ + +without NgModel
+ [(ngModel)]
+ bindon-ngModel
+ +(ngModelChange)="...name=$event"
+ +(ngModelChange)="setUppercaseName($event)" + +top + + +
+

NgClass Binding

+ +

currentClasses is {{ currentClasses | json }}

+
+ This div is initially saveable, unchanged, and special +
+ + +
+ | + +| + +

+
+ This div should be {{ canSave ? "" : "not" }} saveable, + {{ isUnchanged ? "unchanged" : "modified" }} and, + {{ isSpecial ? "" : "not" }} special after clicking "Refresh". +
+

+ +
This div is special
+ +
Bad curly special
+
Curly special
+ +top + + +
+

NgStyle Binding

+ +
+ This div is x-large or smaller. +
+ +

[ngStyle] binding to currentStyles - CSS property names

+

currentStyles is {{ currentStyles | json }}

+
+ This div is initially italic, normal weight, and extra large (24px). +
+ + +
+ | + | + +

+
+ This div should be {{ canSave ? "italic" : "plain" }}, + {{ isUnchanged ? "normal weight" : "bold" }} and, + {{ isSpecial ? "extra large" : "normal size" }} after clicking "Refresh". +
+ +top + + +
+

NgIf Binding

+ + + +
Hello, {{ currentHero.name }}
+
Hello, {{ nullHero.name }}
+ + + +Add {{ currentHero.name }} with template + + +
Hero Detail removed from DOM (via template) because isActive is false
+ + + + + +
Show with class
+
Hide with class
+ + + + +
Show with style
+
Hide with style
+ +top + + +
+

NgFor Binding

+ +
+
{{ hero.name }}
+
+
+ +
+ + +
+ +top + +

*ngFor with index

+

with semi-colon separator

+
+
+ {{ i + 1 }} - {{ hero.name }} +
+
+ +

with comma separator

+
+ +
+ {{ i + 1 }} - {{ hero.name }} +
+
+ +top + +

*ngFor trackBy

+ + + + +

without trackBy

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+ +
+ Hero DOM elements change #{{ heroesNoTrackByCount }} without trackBy +
+
+ +

with trackBy

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+ +
+ Hero DOM elements change #{{ heroesWithTrackByCount }} with trackBy +
+
+ +


+ +

with trackBy and semi-colon separator

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+
+ +

with trackBy and comma separator

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+
+ +

with trackBy and space separator

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+
+ +

with generic trackById function

+
+
+ ({{ hero.id }}) {{ hero.name }} +
+
+ +top + + +
+

NgSwitch Binding

+ +

Pick your favorite hero

+
+ +
+ +
+ + + +
+ Are you as confused as {{ currentHero.name }}? +
+ +
+ +top + + +
+

Template reference variables

+ + + + + + + + + + + + + + +

Example Form

+ + +top + + +
+

Inputs and Outputs

+ + + + + + +
myClick2
+{{ clickMessage2 }} + +top + + +
+

Pipes

+ +
Title through uppercase pipe: {{ title | uppercase }}
+ + +
Title through a pipe chain: {{ title | uppercase | lowercase }}
+ + +
Birthdate: {{ currentHero?.birthdate | date: "longDate" }}
+ +
{{ currentHero | json }}
+ +
+ Birthdate: {{ currentHero?.birthdate | date: "longDate" | uppercase }} +
+ +
+ + {{ product.price | currency: "USD":true }} +
+ +top + + +
+

Safe navigation operator ?.

+ +
The title is {{ title }}
+ +
The current hero's name is {{ currentHero?.name }}
+ +
The current hero's name is {{ currentHero.name }}
+ + + + +
The null hero's name is {{ nullHero.name }}
+ +
The null hero's name is {{ nullHero && nullHero.name }}
+ +
+ + The null hero's name is {{ nullHero?.name }} +
+ +top + + +
+

Non-null assertion operator !.

+ +
+ +
The hero's name is {{ hero!.name }}
+
+ +top + + +
+

$any type cast function $any( ).

+ +
+ +
The hero's marker is {{ $any(hero).marker }}
+
+ +
+ +
Undeclared members is {{$any(this).member}}
+
+ +top + + + +
+

Enums in binding

+ +

+ The name of the Color.Red enum is {{ Color[Color.Red] }}.
+ The current color is {{ Color[color] }} and its number is {{ color }}.
+ +

+ +top + + + +
+
+
+ +
+ +
+
{{ submitMessage }}
+
+ + + +`; + +exports[`real-world.component.html - angular-verify 3`] = ` + + + +

Template Syntax

+Interpolation
+Expression context
+Statement context
+Mental Model
+Buttons
+Properties vs. Attributes
+
+Property Binding
+ +
+Event Binding
+Two-way Binding
+
+
Directives
+ +
+Template reference variables
+Inputs and outputs
+Pipes
+Safe navigation operator ?.
+Non-null assertion operator !.
+Enums
+ + +

Interpolation

+ +

My current hero is {{currentHero.name}}

+ +

+ {{title}} + +

+ + +

The sum of 1 + 1 is {{1 + 1}}

+ + +

The sum of 1 + 1 is not {{1 + 1 + getVal()}}

+ +top + +

Expression context

+ +

Component expression context ({{title}}, [hidden]="isUnchanged")

+
+ {{title}} + changed +
+ + +

Template input variable expression context (let hero)

+ + +
{{hero.name}}
+
+ +

Template reference variable expression context (#heroInput)

+
+ Type something: + {{heroInput.value}} +
+ +top + +

Statement context

+ +

Component statement context ( (click)="onSave() ) +

+ +
+ +

Template $event statement context

+
+ +
+ +

Template input variable statement context (let hero)

+ +
+ +
+ +

Template reference variable statement context (#heroForm)

+
+
...
+
+ +top + + +

New Mental Model

+ + + +
Mental Model
+ + +

+ +
+ +
Mental Model
+ + +
+

+ +
+ + +
+

+ +
+ + +
+
+

+ + + +
click me
+{{clicked}} +

+ +
+ Hero Name: + + {{name}} +
+

+ + +

+ +
Special
+

+ + + +top + + +

Property vs. Attribute (img examples)

+ + + +

+ + + + + +top + + +

Buttons

+ + + + +

+ + +

+ + + +top + + +

Property Binding

+ + + +
[ngClass] binding to the classes property
+ + + + +
+ +
+ + +

is the interpolated image.

+

is the property bound image.

+ +

"{{title}}" is the interpolated title.

+

"" is the property bound title.

+ + +

"{{evilTitle}}" is the interpolated evil title.

+

"" is the property bound evil title.

+ +top + + +

Attribute Binding

+ + + + + + + + + +
One-Two
FiveSix
+ +
+ + +

+ + +
+ + + + + + + +
+ +top + + +

Class Binding

+ + +
Bad curly special
+ + +
Bad curly
+ + +
The class binding is special
+ + +
This one is not so special
+ +
This class binding is special too
+ +top + + +

Style Binding

+ + + + + + + +top + + +

Event Binding

+ + + + + +
+ +
click with myClick
+{{clickMessage}} +
+ + + + +
+ + + + +
Click me +
Click me too!
+
+ + +
+ +
+ + +
+ +
+ +top + +

Two-way Binding

+
+ +
Resizable Text
+ +
+
+
+

De-sugared two-way binding

+ +
+ +top + + +

NgModel (two-way) Binding

+ +

Result: {{currentHero.name}}

+ + +without NgModel +
+ +[(ngModel)] +
+ +bindon-ngModel +
+ +(ngModelChange)="...name=$event" +
+ +(ngModelChange)="setUppercaseName($event)" + +top + + +

NgClass Binding

+ +

currentClasses is {{currentClasses | json}}

+
This div is initially saveable, unchanged, and special
+ + +
+ | + | + + +

+
+ This div should be {{ canSave ? "": "not"}} saveable, + {{ isUnchanged ? "unchanged" : "modified" }} and, + {{ isSpecial ? "": "not"}} special after clicking "Refresh".
+

+ +
This div is special
+ +
Bad curly special
+
Curly special
+ +top + + +

NgStyle Binding

+ +
+ This div is x-large or smaller. +
+ +

[ngStyle] binding to currentStyles - CSS property names

+

currentStyles is {{currentStyles | json}}

+
+ This div is initially italic, normal weight, and extra large (24px). +
+ + +
+ | + | + + +

+
+ This div should be {{ canSave ? "italic": "plain"}}, + {{ isUnchanged ? "normal weight" : "bold" }} and, + {{ isSpecial ? "extra large": "normal size"}} after clicking "Refresh".
+ +top + + +

NgIf Binding

+ + + +
Hello, {{currentHero.name}}
+
Hello, {{nullHero.name}}
+ + + +Add {{currentHero.name}} with template + + +
Hero Detail removed from DOM (via template) because isActive is false
+ + + + + +
Show with class
+
Hide with class
+ + + + +
Show with style
+
Hide with style
+ +top + + +

NgFor Binding

+ +
+
{{hero.name}}
+
+
+ +
+ + +
+ +top + +

*ngFor with index

+

with semi-colon separator

+
+
{{i + 1}} - {{hero.name}}
+
+ +

with comma separator

+
+ +
{{i + 1}} - {{hero.name}}
+
+ +top + +

*ngFor trackBy

+ + + + +

without trackBy

+
+
({{hero.id}}) {{hero.name}}
+ +
+ Hero DOM elements change #{{heroesNoTrackByCount}} without trackBy +
+
+ +

with trackBy

+
+
({{hero.id}}) {{hero.name}}
+ +
+ Hero DOM elements change #{{heroesWithTrackByCount}} with trackBy +
+
+ +


+ +

with trackBy and semi-colon separator

+
+
+ ({{hero.id}}) {{hero.name}} +
+
+ +

with trackBy and comma separator

+
+
({{hero.id}}) {{hero.name}}
+
+ +

with trackBy and space separator

+
+
({{hero.id}}) {{hero.name}}
+
+ +

with generic trackById function

+
+
({{hero.id}}) {{hero.name}}
+
+ +top + + +

NgSwitch Binding

+ +

Pick your favorite hero

+
+ +
+ +
+ + + +
Are you as confused as {{currentHero.name}}?
+ +
+ +top + + +

Template reference variables

+ + + + + + + + + + + + + + +

Example Form

+ + +top + + +

Inputs and Outputs

+ + + + + + + +
myClick2
+{{clickMessage2}} + +top + + +

Pipes

+ +
Title through uppercase pipe: {{title | uppercase}}
+ + +
+ Title through a pipe chain: + {{title | uppercase | lowercase}} +
+ + +
Birthdate: {{currentHero?.birthdate | date:'longDate'}}
+ +
{{currentHero | json}}
+ +
Birthdate: {{(currentHero?.birthdate | date:'longDate') | uppercase}}
+ +
+ + {{product.price | currency:'USD':true}} +
+ +top + + +

Safe navigation operator ?.

+ +
+ The title is {{title}} +
+ +
+ The current hero's name is {{currentHero?.name}} +
+ +
+ The current hero's name is {{currentHero.name}} +
+ + + + + +
The null hero's name is {{nullHero.name}}
+ +
+The null hero's name is {{nullHero && nullHero.name}} +
+ +
+ + The null hero's name is {{nullHero?.name}} +
+ + +top + + +

Non-null assertion operator !.

+ +
+ +
+ The hero's name is {{hero!.name}} +
+
+ +top + + +

$any type cast function $any( ).

+ +
+ +
+ The hero's marker is {{$any(hero).marker}} +
+
+ +
+ +
+ Undeclared members is {{$any(this).member}} +
+
+ +top + + + +

Enums in binding

+ +

+ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ The current color is {{Color[color]}} and its number is {{color}}.
+ +

+ +top + + + + +
+
+
+ +
+ +
+
+ {{submitMessage}} +
+
+ + + +~ + + + +

+ Template + Syntax +

+Interpolation
+Expression + context
+Statement + context
+Mental + Model
+Buttons
+Properties + vs. + Attributes
+
+Property + Binding
+ +
+Event + Binding
+Two-way + Binding
+
+
+ Directives +
+ +
+Template + reference + variables
+Inputs + and + outputs
+Pipes
+Safe + navigation + operator + ?.
+Non-null + assertion + operator + !.
+Enums
+ + +
+

+ Interpolation +

+ +

+ My + current + hero + is + {{ + currentHero.name + }} +

+ +

+ {{ + title + }} + +

+ + +

+ The + sum + of + 1 + + + 1 + is + {{ + 1 + + 1 + }} +

+ + +

+ The + sum + of + 1 + + + 1 + is + not + {{ + 1 + + 1 + + getVal() + }} +

+ +top + +
+

+ Expression + context +

+ +

+ Component + expression + context + ({{title}}, + [hidden]="isUnchanged") +

+
+ {{ + title + }} + changed +
+ +

+ Template + input + variable + expression + context + (let + hero) +

+ + +
+ {{ + hero.name + }} +
+
+ +

+ Template + reference + variable + expression + context + (#heroInput) +

+
+ Type + something: + + {{ + heroInput.value + }} +
+ +top + +
+

+ Statement + context +

+ +

+ Component + statement + context + ( + (click)="onSave() + ) +

+
+ +
+ +

+ Template + $event + statement + context +

+
+ +
+ +

+ Template + input + variable + statement + context + (let + hero) +

+ +
+ +
+ +

+ Template + reference + variable + statement + context + (#heroForm) +

+
+
+ ... +
+
+ +top + + +
+

+ New + Mental + Model +

+ + + +
+ Mental + Model +
+ + +

+ +
+ +
+ Mental + Model +
+ + +
+

+ +
+ + +
+

+ +
+ + +
+
+

+ + + +
+ click + me +
+{{ + clicked +}} +

+ +
+ Hero + Name: + + {{ + name + }} +
+

+ + +

+ +
+ Special +
+

+ + + +top + + +
+

+ Property + vs. + Attribute + (img + examples) +

+ + + +

+ + + + + +top + + +
+

+ Buttons +

+ + + + +

+ + +

+ + + +top + + +
+

+ Property + Binding +

+ + + +
+ [ngClass] + binding + to + the + classes + property +
+ + + + +
+ +
+ + +

+ + is + the + interpolated + image. +

+

+ + is + the + property + bound + image. +

+ +

+ "{{ + title + }}" + is + the + interpolated + title. +

+

+ "" + is + the + property + bound + title. +

+ + +

+ "{{ + evilTitle + }}" + is + the + interpolated + evil + title. +

+

+ "" + is + the + property + bound + evil + title. +

+ +top + + +
+

+ Attribute + Binding +

+ + + + + + + + + + + + + + +
+ One-Two +
+ Five + + Six +
+ +
+ + +

+ + +
+ + + + + + + +
+ +top + + +
+

+ Class + Binding +

+ + +
+ Bad + curly + special +
+ + +
+ Bad + curly +
+ + +
+ The + class + binding + is + special +
+ + +
+ This + one + is + not + so + special +
+ +
+ This + class + binding + is + special + too +
+ +top + + +
+

+ Style + Binding +

+ + + + + + + +top + + +
+

+ Event + Binding +

+ + + + + +
+ +
+ click + with + myClick +
+ {{ + clickMessage + }} +
+ + + +
+ + + + +
+ Click + me +
+ Click + me + too! +
+
+ + +
+ +
+ + +
+ +
+ +top + +
+

+ Two-way + Binding +

+
+ +
+ Resizable + Text +
+ +
+
+
+

+ De-sugared + two-way + binding +

+ +
+ +top + + +
+

+ NgModel + (two-way) + Binding +

+ +

+ Result: + {{ + currentHero.name + }} +

+ + +without +NgModel +
+ +[(ngModel)] +
+ +bindon-ngModel +
+ +(ngModelChange)="...name=$event" +
+ +(ngModelChange)="setUppercaseName($event)" + +top + + +
+

+ NgClass + Binding +

+ +

+ currentClasses + is + {{ + currentClasses + | json + }} +

+
+ This + div + is + initially + saveable, + unchanged, + and + special +
+ + +
+ +| + +| + + +

+
+ This + div + should + be + {{ + canSave + ? "" + : "not" + }} + saveable, + {{ + isUnchanged + ? "unchanged" + : "modified" + }} + and, + {{ + isSpecial + ? "" + : "not" + }} + special + after + clicking + "Refresh". +
+

+ +
+ This + div + is + special +
+ +
+ Bad + curly + special +
+
+ Curly + special +
+ +top + + +
+

+ NgStyle + Binding +

+ +
+ This + div + is + x-large + or + smaller. +
+ +

+ [ngStyle] + binding + to + currentStyles + - + CSS + property + names +

+

+ currentStyles + is + {{ + currentStyles + | json + }} +

+
+ This + div + is + initially + italic, + normal + weight, + and + extra + large + (24px). +
+ + +
+ +| + +| + + +

+
+ This + div + should + be + {{ + canSave + ? "italic" + : "plain" + }}, + {{ + isUnchanged + ? "normal weight" + : "bold" + }} + and, + {{ + isSpecial + ? "extra large" + : "normal size" + }} + after + clicking + "Refresh". +
+ +top + + +
+

+ NgIf + Binding +

+ + + +
+ Hello, + {{ + currentHero.name + }} +
+
+ Hello, + {{ + nullHero.name + }} +
+ + + +Add + {{ + currentHero.name + }} + with + template + + +
+ Hero + Detail + removed + from + DOM + (via + template) + because + isActive + is + false +
+ + + + + +
+ Show + with + class +
+
+ Hide + with + class +
+ + + + +
+ Show + with + style +
+
+ Hide + with + style +
+ +top + + +
+

+ NgFor + Binding +

+ +
+
+ {{ + hero.name + }} +
+
+
+ +
+ + +
+ +top + +

+ *ngFor + with + index +

+

+ with + semi-colon + separator +

+
+
+ {{ + i + + 1 + }} + - + {{ + hero.name + }} +
+
+ +

+ with + comma + separator +

+
+ +
+ {{ + i + + 1 + }} + - + {{ + hero.name + }} +
+
+ +top + +

+ *ngFor + trackBy +

+ + + + +

+ without + trackBy +

+
+
+ ({{ + hero.id + }}) + {{ + hero.name + }} +
+ +
+ Hero + DOM + elements + change + #{{ + heroesNoTrackByCount + }} + without + trackBy +
+
+ +

+ with + trackBy +

+
+
+ ({{ + hero.id + }}) + {{ + hero.name + }} +
+ +
+ Hero + DOM + elements + change + #{{ + heroesWithTrackByCount + }} + with + trackBy +
+
+ +


+ +

+ with + trackBy + and + semi-colon + separator +

+
+
+ ({{ + hero.id + }}) + {{ + hero.name + }} +
+
+ +

+ with + trackBy + and + comma + separator +

+
+
+ ({{ + hero.id + }}) + {{ + hero.name + }} +
+
+ +

+ with + trackBy + and + space + separator +

+
+
+ ({{ + hero.id + }}) + {{ + hero.name + }} +
+
+ +

+ with + generic + trackById + function +

+
+
+ ({{ + hero.id + }}) + {{ + hero.name + }} +
+
+ +top + + +
+

+ NgSwitch + Binding +

+ +

+ Pick + your + favorite + hero +

+
+ +
+ +
+ + + +
+ Are + you + as + confused + as + {{ + currentHero.name + }}? +
+ +
+ +top + + +
+

+ Template + reference + variables +

+ + + + + + + + + + + + + + +

+ Example + Form +

+ + +top + + +
+

+ Inputs + and + Outputs +

+ + + + + + + +
+ myClick2 +
+{{ + clickMessage2 +}} + +top + + +
+

+ Pipes +

+ +
+ Title + through + uppercase + pipe: + {{ + title + | uppercase + }} +
+ + +
+ Title + through + a + pipe + chain: + {{ + title + | uppercase + | lowercase + }} +
+ + +
+ Birthdate: + {{ + currentHero?.birthdate + | date + : "longDate" + }} +
+ +
+ {{ + currentHero + | json + }} +
+ +
+ Birthdate: + {{ + currentHero?.birthdate + | date + : "longDate" + | uppercase + }} +
+ +
+ + {{ + product.price + | currency + : "USD" + : true + }} +
+ +top + + +
+

+ Safe + navigation + operator + ?. +

+ +
+ The + title + is + {{ + title + }} +
+ +
+ The + current + hero's + name + is + {{ + currentHero?.name + }} +
+ +
+ The + current + hero's + name + is + {{ + currentHero.name + }} +
+ + + + +
+ The + null + hero's + name + is + {{ + nullHero.name + }} +
+ +
+ The + null + hero's + name + is + {{ + nullHero && + nullHero.name + }} +
+ +
+ + The + null + hero's + name + is + {{ + nullHero?.name + }} +
+ +top + + +
+

+ Non-null + assertion + operator + !. +

+ +
+ +
+ The + hero's + name + is + {{ + hero! + .name + }} +
+
+ +top + + +
+

+ $any + type + cast + function + $any( + ). +

+ +
+ +
+ The + hero's + marker + is + {{ + $any( + hero + ) + .marker + }} +
+
+ +
+ +
+ Undeclared + members + is + {{$any(this).member}} +
+
+ +top + + + +
+

+ Enums + in + binding +

+ +

+ The + name + of + the + Color.Red + enum + is + {{ + Color[ + Color + .Red + ] + }}.
+ The + current + color + is + {{ + Color[ + color + ] + }} + and + its + number + is + {{ + color + }}.
+ +

+ +top + + + +
+
+
+ +
+ +
+
+ {{ + submitMessage + }} +
+
+ + + +`; 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 @@ +
{{ 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
FiveSix
+ +
+ + +

+ + +
+ + + + + + + +
+ +top + + +

Class Binding

+ + +
Bad curly special
+ + +
Bad curly
+ + +
The class binding is special
+ + +
This one is not so special
+ +
This class binding is special too
+ +top + + +

Style Binding

+ + + + + + + +top + + +

Event Binding

+ + + + + +
+ +
click with myClick
+{{clickMessage}} +
+ + + + +
+ + + + +
Click me +
Click me too!
+
+ + +
+ +
+ + +
+ +
+ +top + +

Two-way Binding

+
+ +
Resizable Text
+ +
+
+
+

De-sugared two-way binding

+ +
+ +top + + +

NgModel (two-way) Binding

+ +

Result: {{currentHero.name}}

+ + +without NgModel +
+ +[(ngModel)] +
+ +bindon-ngModel +
+ +(ngModelChange)="...name=$event" +
+ +(ngModelChange)="setUppercaseName($event)" + +top + + +

NgClass Binding

+ +

currentClasses is {{currentClasses | json}}

+
This div is initially saveable, unchanged, and special
+ + +
+ | + | + + +

+
+ This div should be {{ canSave ? "": "not"}} saveable, + {{ isUnchanged ? "unchanged" : "modified" }} and, + {{ isSpecial ? "": "not"}} special after clicking "Refresh".
+

+ +
This div is special
+ +
Bad curly special
+
Curly special
+ +top + + +

NgStyle Binding

+ +
+ This div is x-large or smaller. +
+ +

[ngStyle] binding to currentStyles - CSS property names

+

currentStyles is {{currentStyles | json}}

+
+ This div is initially italic, normal weight, and extra large (24px). +
+ + +
+ | + | + + +

+
+ This div should be {{ canSave ? "italic": "plain"}}, + {{ isUnchanged ? "normal weight" : "bold" }} and, + {{ isSpecial ? "extra large": "normal size"}} after clicking "Refresh".
+ +top + + +

NgIf Binding

+ + + +
Hello, {{currentHero.name}}
+
Hello, {{nullHero.name}}
+ + + +Add {{currentHero.name}} with template + + +
Hero Detail removed from DOM (via template) because isActive is false
+ + + + + +
Show with class
+
Hide with class
+ + + + +
Show with style
+
Hide with style
+ +top + + +

NgFor Binding

+ +
+
{{hero.name}}
+
+
+ +
+ + +
+ +top + +

*ngFor with index

+

with semi-colon separator

+
+
{{i + 1}} - {{hero.name}}
+
+ +

with comma separator

+
+ +
{{i + 1}} - {{hero.name}}
+
+ +top + +

*ngFor trackBy

+ + + + +

without trackBy

+
+
({{hero.id}}) {{hero.name}}
+ +
+ Hero DOM elements change #{{heroesNoTrackByCount}} without trackBy +
+
+ +

with trackBy

+
+
({{hero.id}}) {{hero.name}}
+ +
+ Hero DOM elements change #{{heroesWithTrackByCount}} with trackBy +
+
+ +


+ +

with trackBy and semi-colon separator

+
+
+ ({{hero.id}}) {{hero.name}} +
+
+ +

with trackBy and comma separator

+
+
({{hero.id}}) {{hero.name}}
+
+ +

with trackBy and space separator

+
+
({{hero.id}}) {{hero.name}}
+
+ +

with generic trackById function

+
+
({{hero.id}}) {{hero.name}}
+
+ +top + + +

NgSwitch Binding

+ +

Pick your favorite hero

+
+ +
+ +
+ + + +
Are you as confused as {{currentHero.name}}?
+ +
+ +top + + +

Template reference variables

+ + + + + + + + + + + + + + +

Example Form

+ + +top + + +

Inputs and Outputs

+ + + + + + + +
myClick2
+{{clickMessage2}} + +top + + +

Pipes

+ +
Title through uppercase pipe: {{title | uppercase}}
+ + +
+ Title through a pipe chain: + {{title | uppercase | lowercase}} +
+ + +
Birthdate: {{currentHero?.birthdate | date:'longDate'}}
+ +
{{currentHero | json}}
+ +
Birthdate: {{(currentHero?.birthdate | date:'longDate') | uppercase}}
+ +
+ + {{product.price | currency:'USD':true}} +
+ +top + + +

Safe navigation operator ?.

+ +
+ The title is {{title}} +
+ +
+ The current hero's name is {{currentHero?.name}} +
+ +
+ The current hero's name is {{currentHero.name}} +
+ + + + + +
The null hero's name is {{nullHero.name}}
+ +
+The null hero's name is {{nullHero && nullHero.name}} +
+ +
+ + The null hero's name is {{nullHero?.name}} +
+ + +top + + +

Non-null assertion operator !.

+ +
+ +
+ The hero's name is {{hero!.name}} +
+
+ +top + + +

$any type cast function $any( ).

+ +
+ +
+ The hero's marker is {{$any(hero).marker}} +
+
+ +
+ +
+ Undeclared members is {{$any(this).member}} +
+
+ +top + + + +

Enums in binding

+ +

+ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ The current color is {{Color[color]}} and its number is {{color}}.
+ +

+ +top + + + + +
+
+
+ +
+ +
+
+ {{submitMessage}} +
+
+ + + diff --git a/tests/html_attributes/__snapshots__/jsfmt.spec.js.snap b/tests/html_attributes/__snapshots__/jsfmt.spec.js.snap index 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`] = `

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 - - - -`; - exports[`html5-boilerplate.html - html-verify 1`] = ` @@ -352,7 +341,9 @@ exports[`html5-boilerplate.html - html-verify 1`] = ` > '); + document.write( + ' diff --git a/tests/html_basics/html-fragment.html b/tests/html_basics/html-fragment.html deleted file mode 100644 index 2c402615223c..000000000000 --- a/tests/html_basics/html-fragment.html +++ /dev/null @@ -1,3 +0,0 @@ -Link - - + +
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2512,6 +3053,8 @@ exports[`textarea.html - html-verify 1`] = `
+
+ `; exports[`textarea.html - html-verify 2`] = ` @@ -2543,6 +3086,8 @@ exports[`textarea.html - html-verify 2`] = `
+ +
~
@@ -2577,6 +3122,10 @@ exports[`textarea.html - html-verify 2`] = `
+
+ +
+ `; exports[`textarea.html - html-verify 3`] = ` @@ -2608,6 +3157,8 @@ exports[`textarea.html - html-verify 3`] = `
+ +
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2638,6 +3189,8 @@ exports[`textarea.html - html-verify 3`] = `
+
+ `; exports[`textarea.html - html-verify 4`] = ` @@ -2669,6 +3222,8 @@ exports[`textarea.html - html-verify 4`] = `
+ +
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2699,6 +3254,8 @@ exports[`textarea.html - html-verify 4`] = `
+
+ `; exports[`textarea.html - html-verify 5`] = ` @@ -2730,6 +3287,8 @@ exports[`textarea.html - html-verify 5`] = `
+ +
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2760,6 +3319,8 @@ exports[`textarea.html - html-verify 5`] = `
+
+ `; exports[`unsupported.html - html-verify 1`] = ` diff --git a/tests/html_tags/custom-element.html b/tests/html_tags/custom-element.html new file mode 100644 index 000000000000..c430869731c8 --- /dev/null +++ b/tests/html_tags/custom-element.html @@ -0,0 +1,2 @@ + + diff --git a/tests/html_tags/custom-self-closing.html b/tests/html_tags/custom-self-closing.html deleted file mode 100644 index c2179292fc21..000000000000 --- a/tests/html_tags/custom-self-closing.html +++ /dev/null @@ -1,4 +0,0 @@ -
- - -
diff --git a/tests/html_tags/tags.html b/tests/html_tags/tags.html index 3f29e39c77bf..8d978c4f1b04 100644 --- a/tests/html_tags/tags.html +++ b/tests/html_tags/tags.html @@ -59,3 +59,52 @@ > *200 123 +
123456
+

x

+

x

+

x

+ + + | + +
+

+ + +

+

+ + +

+

"" is the property bound title.

+
  • 12345678901234567890123456789012345678901234567890123456789012345678901234567890
  • +
    + + + + + + + + + + + +
    +foreign tag name should not be lower cased +
    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + "seddoeiusmod". +
    +
    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + seddoeiusmod. +
    diff --git a/tests/html_tags/textarea.html b/tests/html_tags/textarea.html index 3543920e9fad..11c5e41144da 100644 --- a/tests/html_tags/textarea.html +++ b/tests/html_tags/textarea.html @@ -26,3 +26,5 @@
    + +
    diff --git a/tests/html_vue/__snapshots__/jsfmt.spec.js.snap b/tests/html_vue/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 000000000000..cfb1254a31cb --- /dev/null +++ b/tests/html_vue/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,1183 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`attributes.vue - vue-verify 1`] = ` +
    +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +
    + +`; + +exports[`attributes.vue - vue-verify 2`] = ` +
    +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +
    + +`; + +exports[`board_card.vue - vue-verify 1`] = ` + + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + + +`; + +exports[`board_card.vue - vue-verify 2`] = ` + + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + + +`; + +exports[`filter.vue - vue-verify 1`] = ` + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + +`; + +exports[`filter.vue - vue-verify 2`] = ` + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + +`; + +exports[`interpolations.vue - vue-verify 1`] = ` +
    Fuga magnam facilis. Voluptatem quaerat porro.{{ + + +x => { + const hello = 'world' + return hello; +} + + + +}} Magni consectetur in et molestias neque esse voluptatibus voluptas. {{ + + + some_variable + + + +}} Eum quia nihil nulla esse. Dolorem asperiores vero est error {{ + + preserve + + invalid + + interpolation + +}} reprehenderit voluptates minus {{console.log( short_interpolation )}} nemo.
    + + + +
    + 1234567890123456789012345678901234567890123456789012345678901234567890{{ something }}1234567890 +
    +
    + 1234567890123456789012345678901234567890123456789012345678901234567890 {{ something }}1234567890 +
    +
    + 1234567890123456789012345678901234567890123456789012345678901234567890{{ something }} 1234567890 +
    +
    + 1234567890123456789012345678901234567890123456789012345678901234567890 {{ something }} 1234567890 +
    +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +
    + Fuga magnam facilis. Voluptatem quaerat porro.{{ + x => { + const hello = "world"; + return hello; + } + }} + Magni consectetur in et molestias neque esse voluptatibus voluptas. + {{ some_variable }} Eum quia nihil nulla esse. Dolorem asperiores vero est + error + {{ + + preserve + + invalid + + interpolation + + }} + reprehenderit voluptates minus {{ console.log(short_interpolation) }} nemo. +
    + + + +
    + 1234567890123456789012345678901234567890123456789012345678901234567890{{ + something + }}1234567890 +
    +
    + 1234567890123456789012345678901234567890123456789012345678901234567890 + {{ something }}1234567890 +
    +
    + 1234567890123456789012345678901234567890123456789012345678901234567890{{ + something + }} + 1234567890 +
    +
    + 1234567890123456789012345678901234567890123456789012345678901234567890 + {{ something }} 1234567890 +
    + +`; + +exports[`interpolations.vue - vue-verify 2`] = ` +
    Fuga magnam facilis. Voluptatem quaerat porro.{{ + + +x => { + const hello = 'world' + return hello; +} + + + +}} Magni consectetur in et molestias neque esse voluptatibus voluptas. {{ + + + some_variable + + + +}} Eum quia nihil nulla esse. Dolorem asperiores vero est error {{ + + preserve + + invalid + + interpolation + +}} reprehenderit voluptates minus {{console.log( short_interpolation )}} nemo.
    + + + +
    + 1234567890123456789012345678901234567890123456789012345678901234567890{{ something }}1234567890 +
    +
    + 1234567890123456789012345678901234567890123456789012345678901234567890 {{ something }}1234567890 +
    +
    + 1234567890123456789012345678901234567890123456789012345678901234567890{{ something }} 1234567890 +
    +
    + 1234567890123456789012345678901234567890123456789012345678901234567890 {{ something }} 1234567890 +
    +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +
    + Fuga magnam facilis. Voluptatem quaerat porro.{{ + x => { + const hello = "world"; + return hello; + } + }} + Magni consectetur in et molestias neque esse voluptatibus voluptas. + {{ some_variable }} Eum quia nihil nulla esse. Dolorem asperiores vero est + error + {{ + + preserve + + invalid + + interpolation + + }} + reprehenderit voluptates minus {{ console.log(short_interpolation) }} nemo. +
    + + + +
    + 1234567890123456789012345678901234567890123456789012345678901234567890{{ + something + }}1234567890 +
    +
    + 1234567890123456789012345678901234567890123456789012345678901234567890 + {{ something }}1234567890 +
    +
    + 1234567890123456789012345678901234567890123456789012345678901234567890{{ + something + }} + 1234567890 +
    +
    + 1234567890123456789012345678901234567890123456789012345678901234567890 + {{ something }} 1234567890 +
    + +`; + +exports[`pre-child.vue - vue-verify 1`] = ` + +
    +  
    +  
    +  
    +
    +
    +
    +
    +
    + + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +
    +  
    +  
    +  
    +
    +
    +
    +
    +
    + + + + +`; + +exports[`pre-child.vue - vue-verify 2`] = ` + +
    +  
    +  
    +  
    +
    +
    +
    +
    +
    + + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +
    +  
    +  
    +  
    +
    +
    +
    +
    +
    + + + + +`; + +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`] = ` + + + + `; @@ -46,7 +45,6 @@ const x = 1; - + `; diff --git a/tests/json/with-comment/__snapshots__/jsfmt.spec.js.snap b/tests/json/with-comment/__snapshots__/jsfmt.spec.js.snap index 3bd0d8fd03b9..7422eee2d3bc 100644 --- a/tests/json/with-comment/__snapshots__/jsfmt.spec.js.snap +++ b/tests/json/with-comment/__snapshots__/jsfmt.spec.js.snap @@ -28,6 +28,62 @@ exports[`block-comment.json - json5-verify 2`] = ` `; +exports[`bottom-block-comment.json - json-verify 1`] = ` +1 /* block-comment */ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +1 /* block-comment */ + +`; + +exports[`bottom-block-comment.json - json-verify 2`] = ` +1 /* block-comment */ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +1 /* block-comment */ + +`; + +exports[`bottom-block-comment.json - json5-verify 1`] = ` +1 /* block-comment */ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +1 /* block-comment */ + +`; + +exports[`bottom-block-comment.json - json5-verify 2`] = ` +1 /* block-comment */ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +1 /* block-comment */ + +`; + +exports[`bottom-line-comment.json - json-verify 1`] = ` +1 // line-comment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +1 // line-comment + +`; + +exports[`bottom-line-comment.json - json-verify 2`] = ` +1 // line-comment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +1 // line-comment + +`; + +exports[`bottom-line-comment.json - json5-verify 1`] = ` +1 // line-comment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +1 // line-comment + +`; + +exports[`bottom-line-comment.json - json5-verify 2`] = ` +1 // line-comment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +1 // line-comment + +`; + exports[`line-comment.json - json-verify 1`] = ` { //comment diff --git a/tests/json/with-comment/bottom-block-comment.json b/tests/json/with-comment/bottom-block-comment.json new file mode 100644 index 000000000000..00b9a996621e --- /dev/null +++ b/tests/json/with-comment/bottom-block-comment.json @@ -0,0 +1 @@ +1 /* block-comment */ diff --git a/tests/json/with-comment/bottom-line-comment.json b/tests/json/with-comment/bottom-line-comment.json new file mode 100644 index 000000000000..ceb959ae29c8 --- /dev/null +++ b/tests/json/with-comment/bottom-line-comment.json @@ -0,0 +1 @@ +1 // line-comment diff --git a/tests/multiparser_html_markdown/__snapshots__/jsfmt.spec.js.snap b/tests/multiparser_html_markdown/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 000000000000..36384463cf45 --- /dev/null +++ b/tests/multiparser_html_markdown/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`html-with-markdown-script.html - html-verify 1`] = ` + + + + + + + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + + + + + + +`; diff --git a/tests/multiparser_html_markdown/html-with-markdown-script.html b/tests/multiparser_html_markdown/html-with-markdown-script.html new file mode 100644 index 000000000000..d1305e10f6d2 --- /dev/null +++ b/tests/multiparser_html_markdown/html-with-markdown-script.html @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/tests/multiparser_html_markdown/jsfmt.spec.js b/tests/multiparser_html_markdown/jsfmt.spec.js new file mode 100644 index 000000000000..53763df9b20b --- /dev/null +++ b/tests/multiparser_html_markdown/jsfmt.spec.js @@ -0,0 +1 @@ +run_spec(__dirname, ["html"]); diff --git a/tests/multiparser_js_html/__snapshots__/jsfmt.spec.js.snap b/tests/multiparser_js_html/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 000000000000..4f01fba7a1c2 --- /dev/null +++ b/tests/multiparser_js_html/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`lit-html.js - babylon-verify 1`] = ` +import { LitElement, html } from '@polymer/lit-element'; + +class MyElement extends LitElement { + static get properties() { + return { + mood: { type: String } + }; + } + + constructor() { + super(); + this.mood = 'happy'; + } + + render() { + return html\` + + + Web Components are \${ + this.mood + + }! + \`; + } +} + +customElements.define('my-element', MyElement); + +const someHtml1 = html\`
    hello \${world}
    \`; +const someHtml2 = /* HTML */ \`
    hello \${world}
    \`; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +import { LitElement, html } from "@polymer/lit-element"; + +class MyElement extends LitElement { + static get properties() { + return { + mood: { type: String } + }; + } + + constructor() { + super(); + this.mood = "happy"; + } + + render() { + return html\` + + + Web Components are \${this.mood}! + \`; + } +} + +customElements.define("my-element", MyElement); + +const someHtml1 = html\` +
    hello \${world}
    +\`; +const someHtml2 = /* HTML */ \` +
    hello \${world}
    +\`; + +`; diff --git a/tests/multiparser_js_html/jsfmt.spec.js b/tests/multiparser_js_html/jsfmt.spec.js new file mode 100644 index 000000000000..968651cdbc2c --- /dev/null +++ b/tests/multiparser_js_html/jsfmt.spec.js @@ -0,0 +1 @@ +run_spec(__dirname, ["babylon"]); diff --git a/tests/multiparser_js_html/lit-html.js b/tests/multiparser_js_html/lit-html.js new file mode 100644 index 000000000000..1c432eaf0f2a --- /dev/null +++ b/tests/multiparser_js_html/lit-html.js @@ -0,0 +1,44 @@ +import { LitElement, html } from '@polymer/lit-element'; + +class MyElement extends LitElement { + static get properties() { + return { + mood: { type: String } + }; + } + + constructor() { + super(); + this.mood = 'happy'; + } + + render() { + return html` + + + Web Components are ${ + this.mood + + }! + `; + } +} + +customElements.define('my-element', MyElement); + +const someHtml1 = html`
    hello ${world}
    `; +const someHtml2 = /* HTML */ `
    hello ${world}
    `; diff --git a/tests/multiparser_vue/__snapshots__/jsfmt.spec.js.snap b/tests/multiparser_vue/__snapshots__/jsfmt.spec.js.snap index 684d5402a9a8..ca2f55b1b1a9 100644 --- a/tests/multiparser_vue/__snapshots__/jsfmt.spec.js.snap +++ b/tests/multiparser_vue/__snapshots__/jsfmt.spec.js.snap @@ -13,7 +13,7 @@ export default { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - + - + - + `; diff --git a/tests/multiparser_vue/template-bind.vue b/tests/multiparser_vue/template-bind.vue index 165dcced95bc..3c1f9dc7cade 100644 --- a/tests/multiparser_vue/template-bind.vue +++ b/tests/multiparser_vue/template-bind.vue @@ -1,5 +1,8 @@