summaryrefslogtreecommitdiff
path: root/tools/prettier-plugin-astro/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/prettier-plugin-astro/index.js')
-rw-r--r--tools/prettier-plugin-astro/index.js155
1 files changed, 155 insertions, 0 deletions
diff --git a/tools/prettier-plugin-astro/index.js b/tools/prettier-plugin-astro/index.js
new file mode 100644
index 000000000..54163459c
--- /dev/null
+++ b/tools/prettier-plugin-astro/index.js
@@ -0,0 +1,155 @@
+const {
+ doc: {
+ builders: { concat, hardline },
+ },
+} = require('prettier');
+const { parse } = require('astro-parser');
+
+/** @type {Partial<import('prettier').SupportLanguage>[]} */
+module.exports.languages = [
+ {
+ name: 'astro',
+ parsers: ['astro'],
+ extensions: ['.astro'],
+ vscodeLanguageIds: ['astro'],
+ },
+];
+
+/** @type {Record<string, import('prettier').Parser>} */
+module.exports.parsers = {
+ astro: {
+ parse: (text) => {
+ let { html, css, module: frontmatter } = parse(text);
+ html = html ? { ...html, text: text.slice(html.start, html.end), isRoot: true } : null;
+ return [frontmatter, html, css].filter((v) => v);
+ },
+ locStart(node) {
+ return node.start;
+ },
+ locEnd(node) {
+ return node.end;
+ },
+ astFormat: 'astro-ast',
+ },
+ 'astro-expression': {
+ parse: (text, parsers) => {
+ return { text };
+ },
+ locStart(node) {
+ return node.start;
+ },
+ locEnd(node) {
+ return node.end;
+ },
+ astFormat: 'astro-expression',
+ }
+};
+
+const findExpressionsInAST = (node, collect = []) => {
+ if (node.type === 'MustacheTag') {
+ return collect.concat(node);
+ }
+ if (node.children) {
+ collect.push(...[].concat(...node.children.map(child => findExpressionsInAST(child))));
+ }
+ return collect;
+}
+
+const formatExpression = ({ expression: { codeChunks, children }}, text, options) => {
+ if (children.length === 0) {
+ const codeStart = codeChunks[0]; // If no children, there should only exist a single chunk.
+ if (codeStart && [`'`, `"`].includes(codeStart[0])) {
+ return `<script $ lang="ts">${codeChunks.join('')}</script>`
+ }
+ return `{${codeChunks.join('')}}`;
+ }
+
+ return `<script $ lang="ts">${text}</script>`;
+}
+
+const isAstroScript = (node) => node.type === 'concat' && node.parts[0] === '<script' && node.parts[1].type === 'indent' && node.parts[1].contents.parts.find(v => v === '$');
+
+const walkDoc = (doc) => {
+ let inAstroScript = false;
+ const recurse = (node, { parent }) => {
+ if (node.type === 'concat') {
+ if (isAstroScript(node)) {
+ inAstroScript = true;
+ parent.contents = { type: 'concat', parts: ['{'] };
+ }
+ return node.parts.map(part => recurse(part, { parent: node }));
+ }
+ if (inAstroScript) {
+ if (node.type === 'break-parent') {
+ parent.parts = parent.parts.filter(part => !['break-parent', 'line'].includes(part.type));
+ }
+ if (node.type === 'indent') {
+ parent.parts = parent.parts.map(part => {
+ if (part.type !== 'indent') return part;
+ return {
+ type: 'concat',
+ parts: [part.contents]
+ }
+ })
+ }
+ if (typeof node === 'string' && node.endsWith(';')) {
+ parent.parts = parent.parts.map(part => {
+ if (typeof part === 'string' && part.endsWith(';')) return part.slice(0, -1);
+ return part;
+ });
+ }
+ if (node === '</script>') {
+ parent.parts = parent.parts.map(part => part === '</script>' ? '}' : part);
+ inAstroScript = false;
+ }
+ }
+ if (['group', 'indent'].includes(node.type)) {
+ return recurse(node.contents, { parent: node });
+ }
+ }
+ recurse(doc, { parent: null });
+}
+
+/** @type {Record<string, import('prettier').Printer>} */
+module.exports.printers = {
+ 'astro-ast': {
+ print(path, opts, print) {
+ const node = path.getValue();
+
+ if (Array.isArray(node)) return concat(path.map(print));
+ if (node.type === 'Fragment') return concat(path.map(print, 'children'));
+
+ return node;
+ },
+ embed(path, print, textToDoc, options) {
+ const node = path.getValue();
+ if (node.type === 'Script' && node.context === 'setup') {
+ return concat(['---', hardline, textToDoc(node.content, { ...options, parser: 'typescript' }), '---', hardline, hardline]);
+ }
+ if (node.type === 'Fragment' && node.isRoot) {
+ const expressions = findExpressionsInAST(node);
+ if (expressions.length > 0) {
+ const parts = [].concat(...expressions.map((expr, i, all) => {
+ const prev = all[i - 1];
+ const start = node.text.slice((prev?.end ?? node.start) - node.start, expr.start - node.start);
+ const exprText = formatExpression(expr, node.text.slice(expr.start - node.start + 1, expr.end - node.start - 1), options);
+
+ if (i === all.length - 1) {
+ const end = node.text.slice(expr.end - node.start);
+ return [start, exprText, end]
+ }
+
+ return [start, exprText]
+ }));
+ const html = parts.join('\n');
+ const doc = textToDoc(html, { parser: 'html' });
+ walkDoc(doc);
+ return doc;
+ }
+ return textToDoc(node.text, { parser: 'html' });
+ }
+
+ return null;
+ },
+ },
+};