Skip to content

Commit

Permalink
Added prettier-plugin for inline hbs
Browse files Browse the repository at this point in the history
  • Loading branch information
Matthew Edwards committed Jun 15, 2021
1 parent bbe4b18 commit 6496f59
Show file tree
Hide file tree
Showing 20 changed files with 773 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
packages/@glimmerx/babel-plugin-component-templates/test/
packages/@glimmerx/babel-plugin-component-templates/test/
packages/@glimmerx/@glimmerx/prettier-plugin-component-templates/test/fixtures/
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"eslint-plugin-prettier": "^3.1.2",
"fs-extra": "^9.0.0",
"lerna": "^3.20.2",
"prettier": "^2.0.4",
"prettier": "^2.3.0",
"qunit": "^2.9.3",
"release-it": "^13.5.1",
"release-it-lerna-changelog": "^2.1.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test/__fixtures__/**/*
47 changes: 47 additions & 0 deletions packages/@glimmerx/prettier-plugin-component-templates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# prettier-plugin-glimmer-experimental

## Background

[Prettier](https://prettier.io/docs/en/index.html) is an opinionated code formatter. In Prettier `>2.0` there is support for `*.hbs` glimmer files, but does not support the experimental syntaxes that `glimmerx` components are authored in.

## Introduction

This plugin extends the internal printers to add an embedded syntax for glimmer template in an `hbs` TaggedTemplateExpressions.

## Installation and Usage

```bash
yarn add -D @glimmerx/prettier-plugin-component-templates
```

Once added prettier will discover and use the plugin to format any `hbs` tagged template expression.

## Development

Generate a test case, add it to the tests file,

- Add `PRETTIER_DEBUG=true` to the environment when running the plugin in order to get complete stack traces on errors.
- To generate a new output file you can:
```
yarn prettier --plugin ./index.js ./test/fixtures/extension-gjs/code.gjs > ./test/fixtures/extension-gjs/output.gjs
```

### Testing

Run all tests:

```
yarn test
```

Using the plugin on a single fixture file:

```
PRETTIER_DEBUG=true --inspect-brk node node_modules/.bin/prettier --plugin ./index.js ./test/fixtures/extension-gjs/code.gjs
```

Some caveats, the `.prettierignore` file will be respected, so please ensure that is commented out in repo root.

## TODO

- [ ] Add support for `<template>` tag semantics.
142 changes: 142 additions & 0 deletions packages/@glimmerx/prettier-plugin-component-templates/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
const babelParsers = require('prettier/parser-babel').parsers;
const typescriptParsers = require('prettier/parser-typescript').parsers;

const { esTree } = require('./lib/util');

const {
builders: { group, indent, softline, concat, hardline },
utils: { mapDoc, stripTrailingHardline },
} = require('prettier').doc;

const comments = require('./lib/comments');

function formatHbs(path, print, textToDoc, options) {
const node = path.getValue();

const placeholderPattern = 'PRETTIER_HTML_PLACEHOLDER_(\\d+)_IN_JS';
const placeholders = node.expressions.map((_, i) => `PRETTIER_HTML_PLACEHOLDER_${i}_IN_JS`);

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');

if (expressionDocs.length === 0 && text.trim().length === 0) {
return '``';
}

const contentDoc = mapDoc(
stripTrailingHardline(textToDoc(text, { parser: 'glimmer' })),
(doc) => {
const placeholderRegex = new RegExp(placeholderPattern, 'g');
const hasPlaceholder = typeof doc === 'string' && placeholderRegex.test(doc);

if (!hasPlaceholder) {
return doc;
}

let 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 group(concat(['`', indent(concat([hardline, group(contentDoc)])), softline, '`']));
}

function isHbs(path) {
return path.match(
(node) => {
return node.type === 'TemplateLiteral';
},
(node, name) => {
return (
node.type === 'TaggedTemplateExpression' &&
node.tag.type === 'Identifier' &&
node.tag.name === 'hbs' &&
name === 'quasi'
);
}
);
}

function embed(path, print, textToDoc, options) {
if (isHbs(path)) {
return formatHbs(path, print, textToDoc, options);
}

return esTree(options).embed(path, print, textToDoc, options);
}

function print(path, options, print) {
return esTree(options).print(path, options, print);
}

const languages = [
{
name: 'glimmer-experimental',
group: 'JavaScript',
parsers: ['babel', 'babel-ts', 'typescript'], // Which parsers do we want to support?
extensions: ['.gjs', '.js', '.ts'],
vscodeLanguageIds: ['javascript'],
},
];

const parsers = {
babel: {
...babelParsers.babel,
astFormat: 'esTree',
parse(text, parsers, options) {
const ast = babelParsers.babel.parse(text, parsers, options);
return ast;
},
},
// babel-ts?
typescript: {
...typescriptParsers.typescript,
astFormat: 'esTree',
parse(text, parsers, options) {
const ast = typescriptParsers.typescript.parse(text, parsers, options);
return ast;
},
},
};

const printers = {
esTree: {
embed,
print,
...comments,
},
};

module.exports = {
languages,
parsers,
printers,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const { esTree } = require('./util');

function canAttachComment(node) {
return (
node.type &&
!isBlockComment(node) &&
!isLineComment(node) &&
node.type !== 'EmptyStatement' &&
node.type !== 'TemplateElement' &&
node.type !== 'Import' &&
// `babel-ts` don't have similar node for `class Foo { bar() /* bat */; }`
node.type !== 'TSEmptyBodyFunctionExpression'
);
}

function isBlockComment(comment) {
return (
comment.type === 'Block' ||
comment.type === 'CommentBlock' ||
// `meriyah`
comment.type === 'MultiLine'
);
}

function isLineComment(comment) {
return (
comment.type === 'Line' ||
comment.type === 'CommentLine' ||
// `meriyah` has `SingleLine`, `HashbangComment`, `HTMLOpen`, and `HTMLClose`
comment.type === 'SingleLine' ||
comment.type === 'HashbangComment' ||
comment.type === 'HTMLOpen' ||
comment.type === 'HTMLClose'
);
}

function printComment(path, options) {
return esTree(options).printComment(path, options);
}

const handleComments = {
avoidAstMutation: true,
ownLine: function (context) {
const options = context.options;
return esTree(options).handleComments.ownLine(context);
},
endOfLine: function (context) {
const options = context.options;
return esTree(options).handleComments.endOfLine(context);
},
remaining: function (context) {
const options = context.options;
return esTree(options).handleComments.remaining(context);
},
};

function getCommentChildNodes(node, options) {
return esTree(options).getCommentChildNodes(node, options);
}

function massageAstNode(node, options) {
return esTree(options).massageAstNode(node, options);
}

function willPrintOwnComments(path, options) {
return esTree(options).willPrintOwnComments(path, options);
}

module.exports = {
canAttachComment,
handleComments,
isBlockComment,
getCommentChildNodes,
massageAstNode,
willPrintOwnComments,
printComment,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function esTree(options) {
return options.plugins[0].printers.estree;
}

module.exports = {
esTree,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@glimmerx/prettier-plugin-component-templates",
"version": "0.0.1",
"description": "A prettier formatter for glimmer component templates",
"main": "index.js",
"repository": "https://github.com/glimmerjs/glimmer-experimental",
"author": "Matt Edwards <[email protected]>",
"license": "MIT",
"private": false,
"files": [
"index.js",
"lib/**/*"
],
"scripts": {
"test": "mocha"
},
"devDependencies": {
"chai": "^4.3.4",
"esm": "^3.2.25",
"mocha": "^7.1.1",
"prettier": "^2.3.0"
},
"volta": {
"node": "12.10.0",
"yarn": "1.22.4"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Component, { hbs } from '@glimmerx/component';

export default class Page extends Component {
static template = hbs`
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Goodbye Moon</h1>
</body>
</html>
`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Component, { hbs } from '@glimmerx/component';

export default class Page extends Component {
static template = hbs`
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Goodbye Moon</h1>
</body>
</html>
`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Component, { hbs } from '@glimmerx/component';
import { AssetStyle, url } from 'some-external-library';
import Page from './Page';

export default class Layout extends Component {
static template = hbs`
<Page @arg1={{@arg1}} @arg2={{@arg1}} @arg2={{@arg1}} @arg3={{@arg1}} @arg4={{@arg1}} @arg5={{@arg1}}>
<:head>
<AssetStyle @path="stylesheets/layout"/>
<title>Hello Layout</title>
<meta name="asset-url" id="ui-icons/static/images/sprite-asset" content={{url path="ui-icons/static/images/icons.svg"}}>
<script src={{url "ui-icons/static/javascripts/icons.js"}} async></script>
</:head>
<:content>
<p>
Haxx0r ipsum ip machine code ctl-c epoch socket Leslie Lamport worm null gc. False system fork wombat gcc stdio.h case interpreter stack trace buffer fatal unix ddos port. Packet sniffer pragma fopen stack mountain dew leet.
</p>
<p>
Public giga highjack sudo linux fork root public protocol James T. Kirk leapfrog float suitably small values shell. Mutex fatal void char exception tarball Starcraft brute force. Try catch warez port interpreter error true else afk sql foad January 1, 1970.
</p>
<p> Hello </p>
<p>
Thread bit headers salt float race condition wannabee memory leak bytes regex packet warez snarf malloc deadlock overflow. Afk baz double flood d00dz semaphore all your base are belong to us ssh. Injection daemon segfault highjack function access gobble int exception ascii James T. Kirk printf class *.* mega foad shell bar for Linus Torvalds.
</p>
</:content>
</Page>
`;
}
Loading

0 comments on commit 6496f59

Please sign in to comment.