`\n\
23 | ', '\
24 | foo()`
`;\n\
25 | ');
26 |
--------------------------------------------------------------------------------
/tests/contents.js:
--------------------------------------------------------------------------------
1 | const compare = require('../compare')(__filename);
2 |
3 | compare(
4 | '\
5 | html`
foo
`\n\
6 | ',
7 | '\
8 | html`
foo
`;\n\
9 | ',
10 | );
11 |
12 | compare(
13 | '\
14 | html`
${\'foo\'}
`\n\
15 | ',
16 | '\
17 | html`
${\'foo\'}
`;\n\
18 | ',
19 | );
20 |
21 | compare(
22 | '\
23 | html`
bar ${\'baz\'}
`\n\
24 | ',
25 | '\
26 | html`
bar ${\'baz\'}
`;\n\
27 | ',
28 | );
29 |
--------------------------------------------------------------------------------
/tests/style-element.js:
--------------------------------------------------------------------------------
1 | //const compare = require('../compare')(__filename);
2 | //
3 | //compare(
4 | // '\
5 | //html``\n\
6 | //',
7 | // '\
8 | //html``;\n\
9 | //'
10 | //);
11 | //
12 | //compare(
13 | // "\
14 | //html``\n\
15 | //",
16 | // '\
17 | //html``;\n\
18 | //'
19 | //);
20 | //
21 | //compare(
22 | // '\
23 | //html``\n\
27 | //',
28 | // '\
29 | //html``;\n\
30 | //'
31 | //);
32 |
--------------------------------------------------------------------------------
/compare.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const prettier = require('./prettier');
3 |
4 | const options = prettier.resolveConfig.sync(__filename);
5 | let error;
6 |
7 | process.on('exit', code => {
8 | if (error && !code) {
9 | process.exitCode = 1;
10 | }
11 | });
12 |
13 | module.exports = testPath => (source, expected) => {
14 | const formatted = prettier.format(source, {
15 | ...options,
16 | parser: 'babylon',
17 | });
18 | if (formatted !== expected) {
19 | error = true;
20 | // eslint-disable-next-line no-console
21 | console.error(`Failed ${path.basename(testPath)}
22 |
23 | Source:
24 | ${source}
25 | Formatted:
26 | ${formatted}
27 | Expected:
28 | ${expected}`);
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/patch.js:
--------------------------------------------------------------------------------
1 | const Module = require('module');
2 | const embed = require('./embed');
3 |
4 | module.exports = request => {
5 | const { _compile } = Module.prototype;
6 | Module.prototype._compile = function(content, path) {
7 | const patchedContent =
8 | path === require.resolve(request)
9 | ? content.replace(
10 | /\bswitch\s*\(node\.type\)\s*{\n\s*case\s*['"]TemplateLiteral['"]:/,
11 | `
12 | $&
13 | try {
14 | let result = (${embed})(...arguments);
15 | if (result !== undefined) {
16 | return result;
17 | }
18 | } catch (error) {
19 | console.error(error.message);
20 | }
21 | `,
22 | )
23 | : content;
24 | return _compile.call(this, patchedContent, path);
25 | };
26 | return require(request);
27 | };
28 |
--------------------------------------------------------------------------------
/tests/nested-template-literal.js:
--------------------------------------------------------------------------------
1 | const compare = require('../compare')(__filename);
2 |
3 | compare(
4 | '\
5 | html`
${items.map(item => html``)}
`\n\
6 | ',
7 | '\
8 | html`
${items.map(item => html``)}
`;\n\
9 | ',
10 | );
11 |
12 | compare(
13 | '\
14 | html`
${items.map(item => html``)}
`\n\
15 | ',
16 | '\
17 | html`\n\
18 |
\n\
19 | ${items.map(item => html``)}\n\
20 |
\n\
21 | `;\n\
22 | ',
23 | );
24 |
25 | compare(
26 | '\
27 | html`
${items.map(item => html``)}
`\n\
28 | ',
29 | '\
30 | html`\n\
31 |
\n\
32 | ${items.map(\n\
33 | item => html``,\n\
34 | )}\n\
35 |
\n\
36 | `;\n\
37 | ',
38 | );
39 |
--------------------------------------------------------------------------------
/tests/style-attribute.js:
--------------------------------------------------------------------------------
1 | const compare = require('../compare')(__filename);
2 |
3 | compare(
4 | "\
5 | html`
foo
`\n\
6 | ",
7 | "\
8 | html`
foo
`;\n\
9 | ",
10 | );
11 |
12 | compare(
13 | "\
14 | html`
foo
`\n\
15 | ",
16 | "\
17 | html`\n\
18 |
\n\
19 | foo\n\
20 |
\n\
21 | `;\n\
22 | ",
23 | );
24 |
25 | compare(
26 | "\
27 | html`
foo
`\n\
28 | ",
29 | "\
30 | html`\n\
31 |
\n\
41 | foo\n\
42 |
\n\
43 | `;\n\
44 | ",
45 | );
46 |
--------------------------------------------------------------------------------
/tests/remove-jsx-space.js:
--------------------------------------------------------------------------------
1 | const compare = require('../compare')(__filename);
2 |
3 | compare('\
4 | html`
foo
`\n\
5 | ', '\
6 | html`
foo
`;\n\
7 | ');
8 |
9 | compare('\
10 | html`
foo
`\n\
11 | ', '\
12 | html`
foo
`;\n\
13 | ');
14 |
15 | compare('\
16 | html`
foo
`\n\
17 | ', '\
18 | html`
foo
`;\n\
19 | ');
20 |
21 | compare('\
22 | html`
`\n\
23 | ', '\
24 | html`
`;\n\
25 | ');
26 |
27 | compare(
28 | '\
29 | html`
`\n\
30 | ',
31 | '\
32 | html`\n\
33 |
\n\
34 |
\n\
35 | `;\n\
36 | ',
37 | );
38 |
39 | compare(
40 | '\
41 | html`
`\n\
42 | ',
43 | '\
44 | html`\n\
45 |
\n\
46 | \n\
47 | \n\
48 |
\n\
49 | `;\n\
50 | ',
51 | );
52 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const assert = require('assert').strict;
3 | const path = require('path');
4 | const { spawnSync } = require('child_process');
5 |
6 | const main = () => {
7 | testCLI();
8 | testAPI();
9 | };
10 |
11 | const testAPI = () => require('./tests');
12 |
13 | const testCLI = () => {
14 | const { stdout, stderr } = spawnSync(
15 | path.join(__dirname, 'cli.js'),
16 | ['--stdin-filepath', 'test.js'],
17 | {
18 | encoding: 'utf8',
19 | input:
20 | 'html`
foo
`',
21 | },
22 | );
23 | if (stderr) {
24 | // eslint-disable-next-line no-console
25 | console.error(stderr);
26 | }
27 | assert.equal(
28 | stdout,
29 | '\
30 | html`\n\
31 |
\n\
39 | foo\n\
40 |
\n\
41 | `;\n\
42 | ',
43 | );
44 | };
45 |
46 | main();
47 |
--------------------------------------------------------------------------------
/tests/attribute.js:
--------------------------------------------------------------------------------
1 | const compare = require('../compare')(__filename);
2 |
3 | compare(
4 | '\
5 | html`
foo
`\n\
6 | ',
7 | '\
8 | html`
foo
`;\n\
9 | ',
10 | );
11 |
12 | compare(
13 | "\
14 | html`
foo
`\n\
15 | ",
16 | "\
17 | html`
foo
`;\n\
18 | ",
19 | );
20 |
21 | compare(
22 | '\
23 | html`
foo
`\n\
24 | ',
25 | '\
26 | html`
foo
`;\n\
27 | ',
28 | );
29 |
30 | compare(
31 | '\
32 | html`
foo
`\n\
33 | ',
34 | '\
35 | html`
foo
`;\n\
36 | ',
37 | );
38 |
39 | compare(
40 | '\
41 | html`
foo
`\n\
42 | ',
43 | '\
44 | html`\n\
45 |
\n\
53 | foo\n\
54 |
\n\
55 | `;\n\
56 | ',
57 | );
58 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Danil Semelenov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Danil Semelenov",
3 | "bin": {
4 | "prettier": "./cli.js"
5 | },
6 | "description": "Formats HTML within tagged template literals in Prettier which is useful for hyperHTML, lit-html, choo, hyperx, nanohtml, snabby, yo-yo, and others.",
7 | "devDependencies": {
8 | "eslint": "^4.19.1",
9 | "prettier": "*"
10 | },
11 | "keywords": [
12 | "choo",
13 | "html",
14 | "hyperhtml",
15 | "hyperx",
16 | "lit-html",
17 | "nanohtml",
18 | "prettier",
19 | "prettier-plugin",
20 | "snabby",
21 | "tagged-template-literal",
22 | "template-literal",
23 | "yo-yo"
24 | ],
25 | "license": "MIT",
26 | "name": "prettier-plugin-html-template-literals",
27 | "peerDependencies": {
28 | "prettier": ">= 1.13.0 < 2"
29 | },
30 | "repository": "https://github.com/sgtpep/prettier-plugin-html-template-literals.git",
31 | "scripts": {
32 | "format": "prettier --write './**/*.{js,json,md}'",
33 | "lint": "eslint --fix './**/*.js'",
34 | "test": "prettier -l './**/*.{js,json,md}' && eslint './**/*.js' && node ./test.js"
35 | },
36 | "version": "1.0.5"
37 | }
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # prettier-plugin-html-template-literals
2 |
3 | [](https://travis-ci.com/sgtpep/prettier-plugin-html-template-literals)
4 |
5 | Formats HTML within [tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates) in [Prettier](https://prettier.io/) which is useful for [hyperHTML](https://viperhtml.js.org/), [lit-html](https://polymer.github.io/lit-html/), [htm](https://github.com/developit/htm), [choo](https://choo.io/), [hyperx](https://github.com/choojs/hyperx), [nanohtml](https://github.com/choojs/nanohtml), [snabby](https://github.com/jamen/snabby), [yo-yo](https://github.com/maxogden/yo-yo), and others. For example:
6 |
7 | Input:
8 |
9 | ```javascript
10 | html`
Foobar
`;
11 | ```
12 |
13 | Output:
14 |
15 | ```javascript
16 | html`
17 |
24 | Foobar
25 |
26 | `;
27 | ```
28 |
29 | ## Why
30 |
31 | Declaring HTML templates using [tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates) is a nice alternative to [JSX](https://reactjs.org/docs/introducing-jsx.html). It relies only on standard ES6+ and doesn't require a transpiling/preprocessing toolset, at least during development. [hyperHTML](https://viperhtml.js.org/) and [lit-html](https://polymer.github.io/lit-html/) are nice lightweight libraries built on this idea and, more than that, provide the [React](https://reactjs.org/)-like experience without the bloat of Virtual DOM. There are also Virtual DOM-based libraries that consume HTML from tagged template literals: [choo](https://choo.io/), [hyperx](https://github.com/choojs/hyperx), [nanohtml](https://github.com/choojs/nanohtml), [snabby](https://github.com/jamen/snabby), [yo-yo](https://github.com/maxogden/yo-yo), and others.
32 |
33 | ## Warning
34 |
35 | As HTML tagged template literals get more attention, no doubts Prettier will support them. You can track an [open issue](https://github.com/prettier/prettier/issues/3548) for this request. The problem is that the HTML parser within Prettier is not stable yet, and they haven't decided which tags should identify HTML templates. Also, at the moment, there is no API in Prettier to provide additional embedded languages from plugins. There is [a feature request](https://github.com/prettier/prettier/issues/4424) for this. This plugin provides a temporary 'hacky' solution until it will be implemented within Prettier. It uses Prettier's JSX parser and formats any tagged template literal (but not regular template literal) if it contains `` or `<.../>`. It uses some dirty tricks under the hood, so be warned and report issues/PR.
36 |
37 | ## Usage
38 |
39 | Install `prettier` and `prettier-plugin-html-template-literals` using `yarn`:
40 |
41 | ```shell
42 | yarn add prettier prettier-plugin-html-template-literals
43 | ```
44 |
45 | Or using `npm`:
46 |
47 | ```shell
48 | npm install prettier prettier-plugin-html-template-literals
49 | ```
50 |
51 | ### Usage from CLI
52 |
53 | `prettier-plugin-html-template-literals` replaces the `prettier` executable with its wrapped version. The original `prettier` executable will be restored if you delete this plugin. Use it as the regular `prettier` executable:
54 |
55 | ```shell
56 | echo 'html`
foo
`' | ./node_modules/.bin/prettier --stdin-filepath=test.js
57 | ```
58 |
59 | ### Usage from API
60 |
61 | Unfortunately, this plugin is actually ignored by the API exposed with the `prettier` module at the moment. Instead of `require('prettier').format()` you need to use the wrapper module with the same API:
62 |
63 | ```javascript
64 | require('prettier-plugin-html-template-literals/prettier').format(
65 | 'html`
foo
`',
66 | { parser: 'babylon' },
67 | );
68 | ```
69 |
70 | ## Limitations
71 |
72 | Because of this plugin relies on the JSX parser it has some limitations. Some of them may be addressed in future if it will be possible to find a workaround.
73 |
74 | - Attribute values without quotes are not supported. Raises an exception: `
`
75 | - Contents of ``
76 | - It's impossible to reliably eliminate whitespace between adjacent elements: May be wrapped on not to multiple lines: `
`
77 | - All empty elements are converted to self-closing (void) ones: `
`
78 | - Whitespace is not preserved within elements sensitive to it like `pre` or `textarea`. Contents will be collapsed to a single space: `
\n
`
79 |
--------------------------------------------------------------------------------
/embed.js:
--------------------------------------------------------------------------------
1 | module.exports = (path, print, textToDoc) => {
2 | /* global concat:false, indent:false, mapDoc:false, node:false, parent:false, softline:false, willBreak:false */
3 | if (parent.type === 'TaggedTemplateExpression') {
4 | const text = node.quasis
5 | .map(quasis => quasis.value.raw)
6 | .reduce(
7 | (text, quasis, index) =>
8 | `${text}${
9 | index ? `{'@prettier-placeholder-${index - 1}-id'}` : ''
10 | }${quasis.replace(/{/g, '@prettier-curly-brace')}`,
11 | '',
12 | )
13 | .trim();
14 | if (/<\s*\/[^<]+?>|<[^<]+?\/\s*>/.test(text)) {
15 | // eslint-disable-next-line no-console, no-unused-vars
16 | const log = value => console.log(JSON.stringify(value, null, 2));
17 | const expressions = node.expressions
18 | ? path.map(print, 'expressions')
19 | : [];
20 | const processDoc = doc => {
21 | if (doc) {
22 | if (
23 | doc.contents &&
24 | doc.contents.parts &&
25 | doc.contents.parts[1] &&
26 | doc.contents.parts[1].parts &&
27 | !doc.contents.parts[1].parts.filter(Boolean).length
28 | ) {
29 | doc.contents = '';
30 | }
31 | if (doc.expandedStates) {
32 | doc.expandedStates = doc.expandedStates.map(doc =>
33 | mapDoc(doc, doc => processDoc(doc)),
34 | );
35 | }
36 | if (doc.parts) {
37 | if (
38 | doc.parts[0] === '{' &&
39 | doc.parts[doc.parts.length - 1] === '}' &&
40 | doc.parts[1] &&
41 | doc.parts[1].contents &&
42 | doc.parts[1].contents.parts &&
43 | doc.parts[1].contents.parts[1] &&
44 | doc.parts[1].contents.parts[1].parts &&
45 | doc.parts[1].contents.parts[1].parts[0]
46 | ) {
47 | const match = doc.parts[1].contents.parts[1].parts[0].match(
48 | /^['"]@prettier-placeholder-(\d+)-id['"]$/,
49 | );
50 | if (match) {
51 | return concat(['${', expressions[match[1]], '}']);
52 | }
53 | }
54 | for (const [index, part] of doc.parts.entries()) {
55 | if (part === '{" "}' || part === "{' '}") {
56 | doc.parts[index] = '';
57 | }
58 | if (part.includes) {
59 | if (part.includes('@prettier-placeholder-')) {
60 | const parts = [];
61 | const regExp = /{['"]@prettier-placeholder-(\d+)-id['"]}/g;
62 | let match;
63 | let offset = 0;
64 | while ((match = regExp.exec(part))) {
65 | parts.push(
66 | part.slice(offset, match.index),
67 | '${',
68 | expressions[match[1]],
69 | '}',
70 | );
71 | offset = match.index + match[0].length;
72 | }
73 | parts.push(part.slice(offset));
74 | doc.parts.splice(index, 1, ...parts);
75 | }
76 | if (part.includes('@prettier-curly-brace')) {
77 | doc.parts[index] = part.replace(
78 | /@prettier-curly-brace/g,
79 | '{',
80 | );
81 | }
82 | }
83 | }
84 | }
85 | }
86 | return doc;
87 | };
88 | const trimDoc = doc => {
89 | let trimmedDoc = doc;
90 | while (!trimmedDoc.parts.includes(';')) {
91 | trimmedDoc = trimmedDoc.parts[0];
92 | }
93 | return trimmedDoc.parts[0].parts[0].contents.parts[1].contents.parts[1]
94 | .contents;
95 | };
96 | const indentDoc = doc => {
97 | if (
98 | doc.parts &&
99 | doc.parts[0] &&
100 | doc.parts[0].parts &&
101 | doc.parts[0].parts[0] &&
102 | doc.parts[0].parts[0].expandedStates
103 | ) {
104 | const breakableDoc = doc.parts[0].parts[0].expandedStates.find(
105 | doc => doc.break,
106 | );
107 | if (breakableDoc) {
108 | breakableDoc.contents = concat([
109 | indent(concat([softline, breakableDoc.contents])),
110 | softline,
111 | ]);
112 | return doc;
113 | }
114 | }
115 | return willBreak(doc)
116 | ? concat([indent(concat([softline, doc])), softline])
117 | : doc;
118 | };
119 | const doc = textToDoc(`<>${text}>`, { parser: 'babylon' });
120 | const processedDoc = indentDoc(
121 | trimDoc(mapDoc(doc, doc => processDoc(doc))),
122 | );
123 | return concat(['`', processedDoc, '`']);
124 | }
125 | }
126 | };
127 |
--------------------------------------------------------------------------------