diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0bb4ad6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = false + +[{.*,*.md,*.json,*.toml,*.yml,}] +indent_style = space diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..36eea7c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +test/fixtures/** +dist/** +.github +.changeset diff --git a/.eslintrc b/.eslintrc index 6eb5d80..70cc4a8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,26 +1,20 @@ { - "ignorePatterns": ["test/fixtures/**", "dist/**", "rollup.config.js"], - "plugins": ["prettier-doc"], - "extends": ["eslint:recommended", "plugin:ava/recommended", "plugin:prettier-doc/recommended", "plugin:node/recommended"], + "plugins": ["prettier-doc", "@typescript-eslint", "prettier"], + "extends": ["plugin:@typescript-eslint/recommended", "prettier", "plugin:prettier/recommended"], "rules": { - "curly": ["error", "multi-line"], - "node/no-unsupported-features/es-builtins": [ - "error", - { - "version": "^12.20.0 || ^14.13.1 || >=16.0.0", - "ignores": [] - } - ], - "node/no-unsupported-features/node-builtins": [ - "error", - { - "version": "^12.20.0 || ^14.13.1 || >=16.0.0", - "ignores": [] - } - ] - }, - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/camelcase": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-this-alias": "off", + "no-console": "warn", + "no-shadow": "off", + "@typescript-eslint/no-shadow": ["error"], + "prettier/prettier": "warn" } } diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..7dcd553 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Update Prettier configuration +e0b6f19c6f75c12997143be739be81f6b4898357 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c2641b..4564201 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - next pull_request: jobs: @@ -12,7 +13,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node_version: [12, 14, 16] + node_version: [14, 16] include: - os: windows-latest node_version: 16 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 84fb3b7..bfaee98 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - next env: node_version: 14 diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 0000000..704d3fe --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,36 @@ +name: PreRelease + +on: + push: + branches: + - next + +jobs: + release: + name: PreRelease + runs-on: ubuntu-latest + steps: + - name: Check out branch + uses: actions/checkout@v2 + with: + fetch-depth: 0 # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits + + - name: Set up Node.js 14.x + uses: actions/setup-node@v2 + with: + node-version: 14.x + + - name: Install dependencies + run: yarn --frozen-lockfile --ignore-engines + env: + CI: true + + - name: Create Release Pull Request or Publish to npm + id: changesets + uses: changesets/action@v1 + with: + # This expects you to have a script called release which does a build for your packages and calls changeset publish + publish: yarn release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 76add87..7ac8ea1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules -dist \ No newline at end of file +dist +.DS_Store +*.log diff --git a/.prettierignore b/.prettierignore index ba14558..f2200ce 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ test/fixtures/**/*.md -test/fixtures/**/*.astro \ No newline at end of file +test/fixtures/**/*.astro +**/dist diff --git a/.prettierrc.json b/.prettierrc.json index 3da31c1..2bf855e 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,7 +1,16 @@ { - "printWidth": 180, + "printWidth": 100, "semi": true, "singleQuote": true, "tabWidth": 2, - "trailingComma": "es5" + "trailingComma": "es5", + "useTabs": true, + "overrides": [ + { + "files": [".*", "*.json", "*.md", "*.toml", "*.yml"], + "options": { + "useTabs": false + } + } + ] } diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..fc392b7 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index f50309a..bad757f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # prettier-plugin-astro +## 0.1.0 + +### Minor Changes + +- 054d055: Migrate to new compiler. This took months of work, and some things might be broken for the time being. However, it should be a major improvement in most cases over the previous version. We hope you'll like it! + ## 0.0.12 ### Patch Changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..80aecd2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# Contributing + +## To get set up + +1. `git clone git@github.com:withastro/prettier-plugin-astro.git` +1. `yarn` +1. `yarn build` +1. Run [tests](https://vitest.dev/guide/) with `yarn test` or `yarn test:w` for watch mode +1. Lint code with `yarn lint` +1. Format code with `yarn format` +1. Run `yarn changeset` to add your changes to the changelog on version bump. + Most changes to the plugin should be `patch` changes while we're before `1.0.0`. + +## Notes + +1. A single test file can be run with `yarn test *file-name*` +1. To skip one or more tests in a file, add comments to them individually +1. Watch mode won't rerun tests when changing an input/output file + +## Resources for contributing + +- [Prettier rationale](https://prettier.io/docs/en/rationale.html) +- [Prettier plugin docs](https://prettier.io/docs/en/plugins.html) +- [Svelte Prettier plugin](https://github.com/sveltejs/prettier-plugin-svelte) +- [Prettier HTML formatter](https://github.com/prettier/prettier/tree/main/src/language-html) diff --git a/README.md b/README.md index 4776ddf..0544fe9 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,164 @@ -# Beta Prettier Plugin for [Astro](https://github.com/withastro/astro) +# [Prettier](https://prettier.io/) Plugin for [Astro](https://astro.build/) -## Install [prettier-plugin-astro](https://www.npmjs.com/package/prettier-plugin-astro) +Official Prettier plugin adding support for formatting `.astro` files -1. `yarn add --dev prettier-plugin-astro` or `npm i -D prettier-plugin-astro` -1. `yarn prettier .` to check your formatting / `yarn prettier -w .` to fix your formatting. -1. Add +## Installation + +```shell +npm i --save-dev prettier-plugin-astro prettier +``` + +To customize formatting behavior, see the [Configuration](#configuration) section below + +## Using with the Prettier CLI + +When using the CLI, Prettier will automatically pick up the plugin + +```shell +prettier -w . +``` + +### pnpm support + +Due to [an upstream issue in Prettier](https://github.com/prettier/prettier/issues/8056), the `plugin-search-dir` parameter should be set to the current directory when using pnpm or Prettier won't be able to find the plugin automatically + +```shell +prettier -w --plugin-search-dir=. . +``` + +## Using in VS Code + +First install the [VS Code Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and add the following settings to your VS Code configuration so VS Code is aware that Prettier can be used for Astro files: + +```json +{ + "prettier.documentSelectors": "**/*.astro" +} +``` + +Additionally, you should set Prettier as the default formatter for Astro files or VS Code will ask you to choose a formatter everytime you format since the Astro VS Code extension also includes a formatter for Astro files: ```json -"format": "yarn prettier -w .", +{ + "[astro]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} ``` -to your `package.json` and create a `.prettierignore` to ignore any files. +When submitting issues about formatting in VS Code, first make sure you're actually using Prettier to format your files and not the Astro VS Code extension included formatter + +### pnpm support + +Due to an upstream issue, Prettier inside VS Code isn't able to automatically infer the right parser to use for Astro files when using pnpm + +As such, add the following settings to your `.prettierc.js` config file: + +```js +module.exports = { + plugins: [require.resolve('prettier-plugin-astro')], + overrides: [ + { + files: '*.astro', + options: { + parser: 'astro', + }, + }, + ], +}; +``` + +The `require.resolve` call can alternatively be changed to a direct path, like such: `plugins: ["./node_modules/prettier-plugin-astro"]` for usage inside a non-JS config file + +## Configuration + +Most [options from Prettier](https://prettier.io/docs/en/options.html) will work with the plugin and can be set in a [configuration file](https://prettier.io/docs/en/configuration.html) or through [CLI flags](https://prettier.io/docs/en/cli.html). + +### Astro Sort Order + +Sort order for the markup and styles. Depending on the order, top-level `style` tags will be sorted below or on top of the rest of the template + +The format is a string with the words `markup` and `styles` separated by a pipe (`|`) + +| Default | CLI Override | API Override | +| ------------------ | ----------------------------- | -------------------------- | +| `markup \| styles` | `--astro-sort-order ` | `astroSortOrder: ` | + +### Astro Allow Shorthand + +Set if attributes with the same name as their expression should be formatted to the short form automatically (for example, if enabled `` will become simply ``) + +> Please note that at the time of writing, [the shorthand form is not currently supported inside the Astro VS Code extension](https://github.com/withastro/language-tools/issues/225) + +| Default | CLI Override | API Override | +| ------- | -------------------------------- | ----------------------------- | +| `false` | `--astro-allow-shorthand ` | `astroAllowShorthand: ` | + +### Example `.prettierrc.js` + +```js +{ + astroSortOrder: "markup | styles", + astroAllowShorthand: false +} +``` ## Contributing -To get setup: +Pull requests of any size and any skill level are welcome, no contribution is too small. Changes to the Astro Prettier Plugin are subject to [Astro Governance](https://github.com/withastro/astro/blob/main/GOVERNANCE.md) and should adhere to the [Astro Style Guide](https://github.com/withastro/astro/blob/main/STYLE_GUIDE.md) + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for instructions on how to setup your development environnement + +## Sponsors + +Astro is generously supported by Netlify, Vercel, and several other amazing organizations. + +[❤️ Sponsor Astro! ❤️](https://github.com/withastro/astro/blob/main/FUNDING.md) + +### Platinum Sponsors + + + + + + + + +
NetlifyNetlify + VercelVercel +
+ +### Gold Sponsors -1. `git clone git@github.com:withastro/prettier-plugin-astro.git` -1. `yarn` -1. `yarn build` -1. Run tests with [`yarn test`](https://github.com/avajs/ava/tree/main/docs) -1. Lint code with `yarn lint` -1. Format code with `yarn format` -1. Run `yarn changeset` to add your changes to the changelog on version bump. - Most changes to the plugin should be `patch` changes while we're before `1.0.0`. + + + + + + + +
+ + ‹div›RIOTS + + + ‹div›RIOTS + + + + StackUp Digital + + + StackUp Digital + +
-## Resources for contributing +### Sponsors -- [prettier rationale](https://prettier.io/docs/en/rationale.html) -- [prettier plugin docs](https://prettier.io/docs/en/plugins.html) -- [svelte prettier plugin](https://github.com/sveltejs/prettier-plugin-svelte) -- [prettier html formatter](https://github.com/prettier/prettier/tree/main/src/language-html) + + + + + + +
SentryQoddi App Platform
diff --git a/ava.config.mjs b/ava.config.mjs deleted file mode 100644 index 6da4540..0000000 --- a/ava.config.mjs +++ /dev/null @@ -1,6 +0,0 @@ -export default { - extensions: { - ts: 'module', - }, - nodeArguments: ['--loader=ts-node/esm'], -}; diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 494bb31..0000000 --- a/jsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "include": ["src/"] -} diff --git a/package.json b/package.json index 2bced9e..0a008ff 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,20 @@ { "name": "prettier-plugin-astro", - "version": "0.0.12", + "version": "0.1.0", + "type": "commonjs", "description": "A Prettier Plugin for formatting Astro files", "main": "dist/index.js", "files": [ - "dist/**" + "dist/**", + "workers/*" ], "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0", + "node": "^14.13.1 || >=16.0.0", "npm": ">=6.14.0" }, - "homepage": "https://github.com/snowpackjs/prettier-plugin-astro/", + "homepage": "https://github.com/withastro/prettier-plugin-astro/", "issues": { - "url": "https://github.com/snowpackjs/prettier-plugin-astro/issues" + "url": "https://github.com/withastro/prettier-plugin-astro/issues" }, "license": "MIT", "keywords": [ @@ -23,37 +25,42 @@ ], "repository": { "type": "git", - "url": "https://github.com/snowpackjs/prettier-plugin-astro.git" + "url": "https://github.com/withastro/prettier-plugin-astro.git" }, "scripts": { "build": "rollup -c", "dev": "rollup -c -w", - "test": "ava test/*.test.ts", - "test:w": "ava -w test/*.test.ts", + "test": "vitest run", + "test:w": "vitest -w", + "test:ui": "vitest --ui", "release": "yarn build && changeset publish", "lint": "eslint .", "fix": "yarn lint --fix", "format": "prettier -w ." }, "dependencies": { - "@astrojs/parser": "^0.22.0", - "prettier": "^2.4.1", - "sass-formatter": "^0.7.2" + "@astrojs/compiler": "^0.15.2", + "prettier": "^2.6.2", + "sass-formatter": "^0.7.2", + "synckit": "^0.7.0" }, "devDependencies": { "@changesets/cli": "^2.16.0", - "@rollup/plugin-commonjs": "^21.0.0", + "@rollup/plugin-commonjs": "^21.0.3", "@rollup/plugin-node-resolve": "^13.0.5", "@types/prettier": "^2.4.1", - "ava": "4.0.1", - "eslint": "^8.0.0", - "eslint-plugin-ava": "^13.0.0", - "eslint-plugin-node": "^11.1.0", + "@typescript-eslint/eslint-plugin": "^5.18.0", + "@typescript-eslint/parser": "^5.18.0", + "@vitest/ui": "^0.9.2", + "eslint": "^8.12.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier-doc": "^1.0.1", - "rollup": "^2.58.0", + "rollup": "^2.70.1", "rollup-plugin-typescript": "^1.0.1", - "ts-node": "^10.2.1", + "ts-node": "^10.7.0", "tslib": "^2.3.1", - "typescript": "^4.4.3" + "typescript": "^4.6.3", + "vitest": "^0.9.2" } } diff --git a/rollup.config.js b/rollup.config.js index 53c72cb..7563246 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -3,12 +3,12 @@ import typescript from 'rollup-plugin-typescript'; import { defineConfig } from 'rollup'; export default defineConfig({ - input: 'src/index.ts', - plugins: [commonjs(), typescript()], - external: ['prettier'], - output: { - dir: 'dist', - format: 'cjs', - sourcemap: true, - }, + input: 'src/index.ts', + plugins: [commonjs(), typescript()], + external: ['prettier'], + output: { + dir: 'dist', + format: 'cjs', + sourcemap: true, + }, }); diff --git a/src/index.ts b/src/index.ts index f9c34e0..349f972 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,32 +1,37 @@ -import parse from './parse'; import printer from './printer'; import { options } from './options'; import { Parser, Printer, SupportLanguage } from 'prettier'; +import { createSyncFn } from 'synckit'; +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +// the worker path must be absolute +const parse = createSyncFn(require.resolve('../workers/parse-worker.js')); export const languages: Partial[] = [ - { - name: 'astro', - parsers: ['astro'], - extensions: ['.astro'], - vscodeLanguageIds: ['astro'], - }, + { + name: 'astro', + parsers: ['astro'], + extensions: ['.astro'], + vscodeLanguageIds: ['astro'], + }, ]; export const parsers: Record = { - astro: { - parse, - astFormat: 'astro', - locStart: (node) => node.start, - locEnd: (node) => node.end, - }, + astro: { + parse: (source) => parse(source), + astFormat: 'astro', + locStart: (node) => node.start, + locEnd: (node) => node.end, + }, }; export const printers: Record = { - astro: printer, + astro: printer, }; const defaultOptions = { - tabWidth: 2, + tabWidth: 2, }; export { options, defaultOptions }; diff --git a/src/nodes.ts b/src/nodes.ts index 824777f..d5259ed 100644 --- a/src/nodes.ts +++ b/src/nodes.ts @@ -1,266 +1,312 @@ -// TODO: MAYBE WE SHOULD USE TYPES FROM THE PARSER +import { + Node, + AttributeNode, + RootNode, + ElementNode, + ComponentNode, + CustomElementNode, + ExpressionNode, + TextNode, + FrontmatterNode, + DoctypeNode, + CommentNode, + FragmentNode, +} from '@astrojs/compiler/types'; -export interface Ast { - html: anyNode; - css: StyleNode[]; - module: ScriptNode; - meta: { - features: number; - }; -} +// MISSING ATTRIBUTE NODE FROM THE NODE TYPE -export interface BaseNode { - start: number; - end: number; - type: string; - children?: anyNode[]; - // TODO: ADD BETTER TYPE - [prop_name: string]: any; +export interface NodeWithText { + value: string; } -export type attributeValue = TextNode[] | AttributeShorthandNode[] | MustacheTagNode[] | true; +// export interface Ast { +// html: anyNode; +// css: StyleNode[]; +// module: ScriptNode; +// meta: { +// features: number; +// }; +// } + +// export interface BaseNode { +// start: number; +// end: number; +// type: string; +// children?: anyNode[]; +// // TODO: ADD BETTER TYPE +// [prop_name: string]: any; +// } + +// export type attributeValue = TextNode[] | AttributeShorthandNode[] | MustacheTagNode[] | true; export interface NodeWithChildren { - children: anyNode[]; + // children: anyNode[]; + children: Node[]; } -export interface NodeWithText { - data: string; - raw?: string; -} +// export interface NodeWithText { +// data: string; +// raw?: string; +// } -export interface FragmentNode extends BaseNode { - type: 'Fragment'; - children: anyNode[]; -} +// export interface FragmentNode extends BaseNode { +// type: 'Fragment'; +// children: anyNode[]; +// } -export interface TextNode extends BaseNode { - type: 'Text'; - data: string; - raw: string; -} +// export interface TextNode extends BaseNode { +// type: 'Text'; +// data: string; +// raw: string; +// } -export interface CodeFenceNode extends BaseNode { - type: 'CodeFence'; - metadata: string; - data: string; - raw: string; -} +// export interface CodeFenceNode extends BaseNode { +// type: 'CodeFence'; +// metadata: string; +// data: string; +// raw: string; +// } -export interface CodeSpanNode extends BaseNode { - type: 'CodeSpan'; - metadata: string; - data: string; - raw: string; -} +// export interface CodeSpanNode extends BaseNode { +// type: 'CodeSpan'; +// metadata: string; +// data: string; +// raw: string; +// } -export interface SpreadNode extends BaseNode { - type: 'Spread'; - expression: ExpressionNode; -} +// export interface SpreadNode extends BaseNode { +// type: 'Spread'; +// expression: ExpressionNode; +// } -export interface ExpressionNode { - type: 'Expression'; - start: number; - end: number; - codeChunks: string[]; - children: anyNode[]; -} +// export interface ExpressionNode { +// type: 'Expression'; +// start: number; +// end: number; +// codeChunks: string[]; +// children: anyNode[]; +// } -export interface ScriptNode extends BaseNode { - type: 'Script'; - context: 'runtime' | 'setup'; - content: string; -} +// export interface ScriptNode extends BaseNode { +// type: 'Script'; +// context: 'runtime' | 'setup'; +// content: string; +// } -export interface StyleNode extends BaseNode { - type: 'Style'; - // TODO: ADD BETTER TYPE - attributes: any[]; - content: { - start: number; - end: number; - styles: string; - }; -} +// export interface StyleNode extends BaseNode { +// type: 'Style'; +// // TODO: ADD BETTER TYPE +// attributes: any[]; +// content: { +// start: number; +// end: number; +// styles: string; +// }; +// } -export interface AttributeNode extends BaseNode { - type: 'Attribute'; - name: string; - value: attributeValue; -} +// export interface AttributeNode extends BaseNode { +// type: 'Attribute'; +// name: string; +// value: attributeValue; +// } -export interface AttributeShorthandNode extends BaseNode { - type: 'AttributeShorthand'; - expression: IdentifierNode; -} +// export interface AttributeShorthandNode extends BaseNode { +// type: 'AttributeShorthand'; +// expression: IdentifierNode; +// } -export interface IdentifierNode extends BaseNode { - type: 'Identifier'; - name: string; -} +// export interface IdentifierNode extends BaseNode { +// type: 'Identifier'; +// name: string; +// } -export interface MustacheTagNode extends BaseNode { - type: 'MustacheTag'; - expression: ExpressionNode; -} +// export interface MustacheTagNode extends BaseNode { +// type: 'MustacheTag'; +// expression: ExpressionNode; +// } -export interface SlotNode extends BaseNode { - type: 'Slot'; - name: string; - attributes: AttributeNode[]; -} +// export interface SlotNode extends BaseNode { +// type: 'Slot'; +// name: string; +// attributes: AttributeNode[]; +// } -export interface CommentNode extends BaseNode { - type: 'Comment'; - data: string; - name?: string; - leading?: boolean; - trailing?: boolean; - printed?: boolean; - nodeDescription?: string; -} +// export interface CommentNode extends BaseNode { +// type: 'Comment'; +// data: string; +// name?: string; +// leading?: boolean; +// trailing?: boolean; +// printed?: boolean; +// nodeDescription?: string; +// } -export interface ElementNode extends BaseNode { - type: 'Element'; - name: string; - attributes: AttributeNode[]; -} +// export interface ElementNode extends BaseNode { +// type: 'Element'; +// name: string; +// attributes: AttributeNode[]; +// } -export interface InlineComponentNode extends BaseNode { - type: 'InlineComponent'; - name: string; - attributes: AttributeNode[]; -} +// export interface InlineComponentNode extends BaseNode { +// type: 'InlineComponent'; +// name: string; +// attributes: AttributeNode[]; +// } export interface BlockElementNode extends ElementNode { - name: typeof blockElementsT[number]; + name: typeof blockElementsT[number]; } export interface InlineElementNode extends ElementNode { - name: typeof inlineElementsT[number]; + name: typeof inlineElementsT[number]; } // https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements#Elements const blockElementsT = [ - 'address', - 'article', - 'aside', - 'blockquote', - 'details', - 'dialog', - 'dd', - 'div', - 'dl', - 'dt', - 'fieldset', - 'figcaption', - 'figure', - 'footer', - 'form', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'header', - 'hgroup', - 'hr', - 'li', - 'main', - 'nav', - 'ol', - 'p', - 'pre', - 'section', - 'table', - 'ul', + 'address', + 'article', + 'aside', + 'blockquote', + 'details', + 'dialog', + 'dd', + 'div', + 'dl', + 'dt', + 'fieldset', + 'figcaption', + 'figure', + 'footer', + 'form', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'header', + 'hgroup', + 'hr', + 'li', + 'main', + 'nav', + 'ol', + 'p', + 'pre', + 'section', + 'table', + 'ul', + // TODO: WIP + 'title', + 'html', ] as const; // https://github.com/microsoft/TypeScript/issues/31018 export const blockElements: string[] = [...blockElementsT]; // https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements const inlineElementsT = [ - 'a', - 'abbr', - 'acronym', - 'audio', - 'b', - 'bdi', - 'bdo', - 'big', - 'br', - 'button', - 'canvas', - 'cite', - 'code', - 'data', - 'datalist', - 'del', - 'dfn', - 'em', - 'embed', - 'i', - 'iframe', - 'img', - 'input', - 'ins', - 'kbd', - 'label', - 'map', - 'mark', - 'meter', - 'noscript', - 'object', - 'output', - 'picture', - 'progress', - 'q', - 'ruby', - 's', - 'samp', - 'script', - 'select', - 'slot', - 'small', - 'span', - 'strong', - 'sub', - 'sup', - 'svg', - 'template', - 'textarea', - 'time', - 'u', - 'tt', - 'var', - 'video', - 'wbr', + 'a', + 'abbr', + 'acronym', + 'audio', + 'b', + 'bdi', + 'bdo', + 'big', + 'br', + 'button', + 'canvas', + 'cite', + 'code', + 'data', + 'datalist', + 'del', + 'dfn', + 'em', + 'embed', + 'i', + 'iframe', + 'img', + 'input', + 'ins', + 'kbd', + 'label', + 'map', + 'mark', + 'meter', + 'noscript', + 'object', + 'output', + 'picture', + 'progress', + 'q', + 'ruby', + 's', + 'samp', + 'script', + 'select', + 'slot', + 'small', + 'span', + 'strong', + 'sub', + 'sup', + 'svg', + 'template', + 'textarea', + 'time', + 'u', + 'tt', + 'var', + 'video', + 'wbr', ] as const; // https://github.com/microsoft/TypeScript/issues/31018 export const inlineElements: string[] = [...inlineElementsT]; // @see http://xahlee.info/js/html5_non-closing_tag.html -export const selfClosingTags = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']; +export const selfClosingTags = [ + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr', +]; export type anyNode = - | AttributeNode - | AttributeShorthandNode - | BlockElementNode - | CodeFenceNode - | CodeSpanNode - | CommentNode - | ElementNode - | ExpressionNode - | FragmentNode - | IdentifierNode - | InlineComponentNode - | InlineElementNode - | MustacheTagNode - | MustacheTagNode - | ScriptNode - | SlotNode - | SpreadNode - | StyleNode - | TextNode; + | RootNode + | AttributeNode + | ElementNode + | ComponentNode + | CustomElementNode + | ExpressionNode + | TextNode + | DoctypeNode + | CommentNode + | FragmentNode + | FrontmatterNode; + +export type { + AttributeNode, + Node, + RootNode, + ElementNode, + ComponentNode, + CustomElementNode, + ExpressionNode, + TextNode, + FrontmatterNode, + DoctypeNode, + CommentNode, + FragmentNode, + TagLikeNode, +} from '@astrojs/compiler/types'; diff --git a/src/options.ts b/src/options.ts index e4227bd..6dd4001 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,42 +1,44 @@ import { SupportOption } from 'prettier'; declare module 'prettier' { - interface RequiredOptions extends PluginOptions {} + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface RequiredOptions extends PluginOptions {} } export interface PluginOptions { - astroSortOrder: SortOrder; - astroAllowShorthand: boolean; + astroSortOrder: SortOrder; + astroAllowShorthand: boolean; } export const options: Record = { - astroSortOrder: { - since: '0.0.1', - category: 'Astro', - type: 'choice', - default: 'markup | styles', - description: 'Sort order for markup, scripts, and styles', - choices: [ - { - value: 'markup | styles', - description: 'markup | styles', - }, - { - value: 'styles | markup', - description: 'styles | markup', - }, - ], - }, - astroAllowShorthand: { - since: '0.0.10', - category: 'Astro', - type: 'boolean', - default: true, - description: 'Enable/disable attribute shorthand if attribute name and expression are the same', - }, + astroSortOrder: { + since: '0.0.1', + category: 'Astro', + type: 'choice', + default: 'markup | styles', + description: 'Sort order for markup, scripts, and styles', + choices: [ + { + value: 'markup | styles', + description: 'markup | styles', + }, + { + value: 'styles | markup', + description: 'styles | markup', + }, + ], + }, + astroAllowShorthand: { + since: '0.0.10', + category: 'Astro', + type: 'boolean', + default: false, + description: 'Enable/disable attribute shorthand if attribute name and expression are the same', + }, }; -export const parseSortOrder = (sortOrder: SortOrder): SortOrderPart[] => sortOrder.split(' | ') as SortOrderPart[]; +export const parseSortOrder = (sortOrder: SortOrder): SortOrderPart[] => + sortOrder.split(' | ') as SortOrderPart[]; export type SortOrder = 'markup | styles' | 'styles | markup'; diff --git a/src/parse.ts b/src/parse.ts deleted file mode 100644 index 1efbffc..0000000 --- a/src/parse.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { parse as parseAstro } from '@astrojs/parser'; - -const parse = (text: string) => parseAstro(text); - -export default parse; diff --git a/src/printer.ts b/src/printer.ts index 785785a..6e53320 100644 --- a/src/printer.ts +++ b/src/printer.ts @@ -1,374 +1,463 @@ -import { AstPath as AstP, Doc, ParserOptions as ParserOpts, Printer } from 'prettier'; +import { + AstPath as AstP, + BuiltInParsers, + Doc, + ParserOptions as ParserOpts, + Printer, +} from 'prettier'; import _doc from 'prettier/doc'; const { - builders: { breakParent, dedent, fill, group, hardline, indent, join, line, literalline, softline }, - utils: { removeLines, stripTrailingHardline }, + builders: { + breakParent, + dedent, + fill, + group, + hardline, + indent, + join, + line, + literalline, + softline, + }, + utils: { removeLines, stripTrailingHardline }, } = _doc; import { SassFormatter, SassFormatterConfig } from 'sass-formatter'; import { parseSortOrder } from './options'; -import { Ast, anyNode, AttributeNode, CommentNode, NodeWithText, selfClosingTags, TextNode } from './nodes'; + +import { + RootNode, + Node, + AttributeNode, + CommentNode, + NodeWithText, + selfClosingTags, + TextNode, + anyNode, +} from './nodes'; type ParserOptions = ParserOpts; type AstPath = AstP; import { - attachCommentsHTML, - canOmitSoftlineBeforeClosingTag, - dedent as manualDedent, - endsWithLinebreak, - forceIntoExpression, - formattableAttributes, - getMarkdownName, - getText, - getUnencodedText, - isASTNode, - isDocCommand, - isEmptyDoc, - isEmptyTextNode, - isInlineElement, - isInsideQuotedAttribute, - isLine, - isLoneMustacheTag, - isNodeWithChildren, - isOrCanBeConvertedToShorthand, - isPreTagContent, - isShorthandAndMustBeConvertedToBinaryExpression, - isTextNode, - isTextNodeEndingWithWhitespace, - isTextNodeStartingWithLinebreak, - isTextNodeStartingWithWhitespace, - printRaw, - replaceEndOfLineWith, - shouldHugEnd, - shouldHugStart, - startsWithLinebreak, - trim, - trimChildren, - trimTextNodeLeft, - trimTextNodeRight, + // attachCommentsHTML, + canOmitSoftlineBeforeClosingTag, + manualDedent, + endsWithLinebreak, + forceIntoExpression, + formattableAttributes, + getMarkdownName, + getText, + getUnencodedText, + isRootNode, + // isDocCommand, + // isEmptyDoc, + isEmptyTextNode, + isInlineElement, + isInsideQuotedAttribute, + // isLine, + isLoneMustacheTag, + // isNodeWithChildren, + isOrCanBeConvertedToShorthand, + isPreTagContent, + isShorthandAndMustBeConvertedToBinaryExpression, + isTextNode, + isTextNodeEndingWithWhitespace, + isTextNodeStartingWithLinebreak, + isTextNodeStartingWithWhitespace, + printRaw, + // replaceEndOfLineWith, + shouldHugEnd, + shouldHugStart, + startsWithLinebreak, + // trim, + // trimChildren, + trimTextNodeLeft, + trimTextNodeRight, + removeDuplicates, + getNextNode, + isTagLikeNode, } from './utils'; -function printTopLevelParts(node: Ast, path: AstPath, opts: ParserOptions, print: printFn): Doc { - let docs = []; - - const normalize = (doc: Doc) => [stripTrailingHardline(doc), hardline]; - - // frontmatter always comes first - if (node.module) { - const subDoc = normalize(path.call(print, 'module')); - docs.push(subDoc); - } - - // markup and styles follow, whichever the user prefers (default: markup, styles) - for (const section of parseSortOrder(opts.astroSortOrder)) { - switch (section) { - case 'markup': { - const subDoc = path.call(print, 'html'); - if (!isEmptyDoc(subDoc)) docs.push(normalize(subDoc)); - break; - } - case 'styles': { - const subDoc = path.call(print, 'css'); - if (!isEmptyDoc(subDoc)) docs.push(normalize(subDoc)); - break; - } - } - } - - return join(softline, docs); -} - -function printAttributeNodeValue(path: AstPath, print: printFn, quotes: boolean, node: AttributeNode): Doc[] | _doc.builders.Indent { - const valueDocs = path.map((childPath) => childPath.call(print), 'value'); - - if (!quotes || !formattableAttributes.includes(node.name)) { - return valueDocs; - } else { - return indent(group(trim(valueDocs, isLine))); - } -} +// function printTopLevelParts(node: RootNode, path: AstPath, opts: ParserOptions, print: printFn): Doc { +// let docs = []; + +// const normalize = (doc: Doc) => [stripTrailingHardline(doc), hardline]; + +// // frontmatter always comes first +// if (node.module) { +// const subDoc = normalize(path.call(print, 'module')); +// docs.push(subDoc); +// } + +// // markup and styles follow, whichever the user prefers (default: markup, styles) +// for (const section of parseSortOrder(opts.astroSortOrder)) { +// switch (section) { +// case 'markup': { +// const subDoc = path.call(print, 'html'); +// if (!isEmptyDoc(subDoc)) docs.push(normalize(subDoc)); +// break; +// } +// case 'styles': { +// const subDoc = path.call(print, 'css'); +// if (!isEmptyDoc(subDoc)) docs.push(normalize(subDoc)); +// break; +// } +// } +// } + +// return join(softline, docs); +// } + +// function printAttributeNodeValue(path: AstPath, print: printFn, quotes: boolean, node: AttributeNode): Doc[] | _doc.builders.Indent { +// const valueDocs = path.map((childPath) => childPath.call(print), 'value'); + +// if (!quotes || !formattableAttributes.includes(node.name)) { +// return valueDocs; +// } else { +// return indent(group(trim(valueDocs, isLine))); +// } +// } // TODO: USE ASTPATH GENERIC -function printJS(path: AstP, print: printFn, name: string, { forceSingleQuote, forceSingleLine }: { forceSingleQuote: boolean; forceSingleLine: boolean }) { - path.getValue()[name].isJS = true; - path.getValue()[name].forceSingleQuote = forceSingleQuote; - path.getValue()[name].forceSingleLine = forceSingleLine; - return path.call(print, name); -} +// function printJS(path: AstP, print: printFn, name: string, { forceSingleQuote, forceSingleLine }: { forceSingleQuote: boolean; forceSingleLine: boolean }) { +// path.getValue()[name].isJS = true; +// path.getValue()[name].forceSingleQuote = forceSingleQuote; +// path.getValue()[name].forceSingleLine = forceSingleLine; +// return path.call(print, name); +// } // TODO: MAYBE USE THIS TO HANDLE COMMENTS function printComment(commentPath: AstPath, options: ParserOptions): Doc { - // note(drew): this isn’t doing anything currently, but Prettier requires it anyway - // @ts-ignore - return commentPath; + // note(drew): this isn’t doing anything currently, but Prettier requires it anyway + // @ts-ignore + return commentPath; } export type printFn = (path: AstPath) => Doc; +// eslint-disable-next-line @typescript-eslint/no-shadow function print(path: AstPath, opts: ParserOptions, print: printFn): Doc { - const node = path.getValue(); - const isMarkdownSubDoc = opts.parentParser === 'markdown'; // is this a code block within .md? - - // 1. handle special node types - if (!node) { - return ''; - } - - if (typeof node === 'string') { - return node; - } - - if (Array.isArray(node)) { - return path.map((childPath) => childPath.call(print)); - } - - if (isASTNode(node)) { - return printTopLevelParts(node, path, opts, print); - } - - // 2. attach comments shallowly to children, if any (https://prettier.io/docs/en/plugins.html#manually-attaching-a-comment) - if (!isPreTagContent(path) && !isMarkdownSubDoc && node.type === 'Fragment') { - attachCommentsHTML(node); - } - - // 3. handle printing - switch (node.type) { - case 'Fragment': { - const text = getText(node, opts); - if (text.length === 0) { - return ''; - } - - if (!isNodeWithChildren(node) || node.children.every(isEmptyTextNode)) return ''; - - if (!isPreTagContent(path)) { - trimChildren(node.children); - const output = trim( - [path.map(print, 'children')], - (n) => - isLine(n) || - (typeof n === 'string' && n.trim() === '') || - // Because printChildren may append this at the end and - // may hide other lines before it - n === breakParent - ); - if (output.every((doc) => isEmptyDoc(doc))) { - return ''; - } - return group([...output, hardline]); - } else { - return group(path.map(print, 'children')); - } - } - case 'Text': { - const rawText = getUnencodedText(node); - - if (isPreTagContent(path)) { - if (path.getParentNode()?.type === 'Attribute') { - // Direct child of attribute value -> add literallines at end of lines - // so that other things don't break in unexpected places - return replaceEndOfLineWith(rawText, literalline); - } - return rawText; - } - - if (isEmptyTextNode(node)) { - const hasWhiteSpace = rawText.trim().length < getUnencodedText(node).length; - const hasOneOrMoreNewlines = /\n/.test(getUnencodedText(node)); - const hasTwoOrMoreNewlines = /\n\r?\s*\n\r?/.test(getUnencodedText(node)); - if (hasTwoOrMoreNewlines) { - return [hardline, hardline]; - } - if (hasOneOrMoreNewlines) { - return hardline; - } - if (hasWhiteSpace) { - return line; - } - return ''; - } - - /** - * For non-empty text nodes each sequence of non-whitespace characters (effectively, - * each "word") is joined by a single `line`, which will be rendered as a single space - * until this node's current line is out of room, at which `fill` will break at the - * most convenient instance of `line`. - */ - return fill(splitTextToDocs(node)); - } - - case 'Element': - case 'InlineComponent': - case 'Slot': { - const isEmpty = node.children?.every((child) => isEmptyTextNode(child)); - const isSelfClosingTag = isEmpty && (node.type !== 'Element' || selfClosingTags.indexOf(node.name) !== -1); - const attributes = path.map(print, 'attributes'); - if (isSelfClosingTag) { - return group(['<', node.name, indent(group(attributes)), line, `/>`]); - // return group(['<', node.name, indent(group([...attributes, opts.jsxBracketNewLine ? dedent(line) : ''])), ...[opts.jsxBracketNewLine ? '' : ' ', `/>`]]); - } - try { - if (node.name.toLowerCase() === '!doctype') { - const attributesWithLowercaseHTML = attributes.map((attribute) => { - if (typeof attribute === 'string') return attribute; - if (isDocCommand(attribute)) return attribute; - attribute = attribute.map((attrValue) => { - if (typeof attrValue !== 'string') return attrValue; - if (attrValue.toLowerCase() === 'html') { - attrValue = attrValue.toLowerCase(); - } - return attrValue; - }); - - // if (attribute[0].type === 'line' && attribute[1].toLowerCase() === 'html') { - // attribute[1] = attribute[1].toLowerCase(); - // return attribute; - // } - return attribute; - }); - - return group(['<', node.name.toUpperCase(), ...attributesWithLowercaseHTML, `>`]); - } - } catch (e) { - console.warn(`error ${e} in the doctype printing`); - } - - if (node.children) { - const children = node.children; - const firstChild = children[0]; - const lastChild = children[children.length - 1]; - - // No hugging of content means it's either a block element and/or there's whitespace at the start/end - let noHugSeparatorStart: _doc.builders.Concat | _doc.builders.Line | _doc.builders.Softline | string = softline; - let noHugSeparatorEnd: _doc.builders.Concat | _doc.builders.Line | _doc.builders.Softline | string = softline; - let hugStart = shouldHugStart(node, opts); - let hugEnd = shouldHugEnd(node, opts); - - let body; - - if (isEmpty) { - body = - isInlineElement(path, opts, node) && node.children.length && isTextNodeStartingWithWhitespace(node.children[0]) && !isPreTagContent(path) - ? () => line - : // () => (opts.jsxBracketNewLine ? '' : softline); - () => softline; - } else if (isPreTagContent(path)) { - body = () => printRaw(node, opts.originalText); - } else if (isInlineElement(path, opts, node) && !isPreTagContent(path)) { - body = () => path.map(print, 'children'); - } else { - body = () => path.map(print, 'children'); - } - - const openingTag = ['<', node.name, indent(group([...attributes, hugStart ? '' : !isPreTagContent(path) && !opts.bracketSameLine ? dedent(softline) : '']))]; - // const openingTag = ['<', node.name, indent(group([...attributes, hugStart ? '' : opts.jsxBracketNewLine && !isPreTagContent(path) ? dedent(softline) : '']))]; - - if (hugStart && hugEnd) { - const huggedContent = [softline, group(['>', body(), `']); - } - - if (isPreTagContent(path)) { - noHugSeparatorStart = ''; - noHugSeparatorEnd = ''; - } else { - let didSetEndSeparator = false; - - if (!hugStart && firstChild && isTextNode(firstChild)) { - if (isTextNodeStartingWithLinebreak(firstChild) && firstChild !== lastChild && (!isInlineElement(path, opts, node) || isTextNodeEndingWithWhitespace(lastChild))) { - noHugSeparatorStart = hardline; - noHugSeparatorEnd = hardline; - didSetEndSeparator = true; - } else if (isInlineElement(path, opts, node)) { - noHugSeparatorStart = line; - } - trimTextNodeLeft(firstChild); - } - if (!hugEnd && lastChild && isTextNode(lastChild)) { - if (isInlineElement(path, opts, node) && !didSetEndSeparator) { - noHugSeparatorEnd = line; - } - trimTextNodeRight(lastChild); - } - } - - if (hugStart) { - return group([...openingTag, indent([softline, group(['>', body()])]), noHugSeparatorEnd, ``]); - } - - if (hugEnd) { - return group([...openingTag, '>', indent([noHugSeparatorStart, group([body(), `']); - } - - if (isEmpty) { - return group([...openingTag, '>', body(), ``]); - } - - return group([...openingTag, '>', indent([noHugSeparatorStart, body()]), noHugSeparatorEnd, ``]); - } - } - case 'AttributeShorthand': { - return node.expression.name; - } - case 'Attribute': { - if (isOrCanBeConvertedToShorthand(node, opts)) { - return [line, '{', node.name, '}']; - } else if (isShorthandAndMustBeConvertedToBinaryExpression(node, opts)) { - const attrNodeValue = printAttributeNodeValue(path, print, true, node); - return [line, node.name, '=', '{', attrNodeValue, '}']; - } else if (node.value === true) { - return [line, node.name]; - } - - const quotes = !isLoneMustacheTag(node.value); - const attrNodeValue = printAttributeNodeValue(path, print, quotes, node); - if (quotes) { - return [line, node.name, '=', '"', attrNodeValue, '"']; - } else { - return [line, node.name, '=', attrNodeValue]; - } - } - case 'Expression': - // missing test ? - return []; - case 'MustacheTag': - return [ - '{', - printJS(path, print, 'expression', { - forceSingleLine: isInsideQuotedAttribute(path), - forceSingleQuote: opts.jsxSingleQuote, - }), - '}', - ]; - case 'Spread': - return [ - line, - '{...', - printJS(path, print, 'expression', { - forceSingleQuote: true, - forceSingleLine: false, - }), - '}', - ]; - case 'Comment': - return ['']; - case 'CodeSpan': - return getUnencodedText(node); - case 'CodeFence': { - console.debug(node); - // const lang = node.metadata.slice(3); - return [node.metadata, hardline, /** somehow call textToDoc(lang), */ node.data, hardline, '```', hardline]; - - // We should use `node.metadata` to select a parser to embed with... something like return [node.metadata, hardline textToDoc(node.getMetadataLanguage()), hardline, `\`\`\``]; - } - default: { - throw new Error(`Unhandled node type "${node.type}"!`); - } - } + const node = path.getValue(); + // const isMarkdownSubDoc = opts.parentParser === 'markdown'; // is this a code block within .md? + + // 1. handle special node types + if (!node) { + return ''; + } + + if (typeof node === 'string') { + return node; + } + + // if (Array.isArray(node)) { + // return path.map((childPath) => childPath.call(print)); + // } + + // if (isASTNode(node)) { + // return printTopLevelParts(node, path, opts, print); + // } + + // 2. attach comments shallowly to children, if any (https://prettier.io/docs/en/plugins.html#manually-attaching-a-comment) + // if (!isPreTagContent(path) && !isMarkdownSubDoc && node.type === 'Fragment') { + // attachCommentsHTML(node); + // } + + // 3. handle printing + switch (node.type) { + case 'root': { + removeDuplicates(node); + return [stripTrailingHardline(path.map(print, 'children')), hardline]; + } + + // case 'Fragment': { + // const text = getText(node, opts); + // if (text.length === 0) { + // return ''; + // } + + // if (!isNodeWithChildren(node) || node.children.every(isEmptyTextNode)) return ''; + + // if (!isPreTagContent(path)) { + // trimChildren(node.children); + // const output = trim( + // [path.map(print, 'children')], + // (n) => + // isLine(n) || + // (typeof n === 'string' && n.trim() === '') || + // // Because printChildren may append this at the end and + // // may hide other lines before it + // n === breakParent + // ); + // if (output.every((doc) => isEmptyDoc(doc))) { + // return ''; + // } + // return group([...output, hardline]); + // } else { + // return group(path.map(print, 'children')); + // } + // } + case 'text': { + const rawText = getUnencodedText(node); + + // TODO: TEST PRE TAGS + // if (isPreTagContent(path)) { + // if (path.getParentNode()?.type === 'Attribute') { + // // Direct child of attribute value -> add literallines at end of lines + // // so that other things don't break in unexpected places + // return replaceEndOfLineWith(rawText, literalline); + // } + // return rawText; + // } + + if (isEmptyTextNode(node)) { + const hasWhiteSpace = rawText.trim().length < getUnencodedText(node).length; + const hasOneOrMoreNewlines = /\n/.test(getUnencodedText(node)); + const hasTwoOrMoreNewlines = /\n\r?\s*\n\r?/.test(getUnencodedText(node)); + if (hasTwoOrMoreNewlines) { + return [hardline, hardline]; + } + if (hasOneOrMoreNewlines) { + return hardline; + } + if (hasWhiteSpace) { + return line; + } + return ''; + } + + /** + * For non-empty text nodes each sequence of non-whitespace characters (effectively, + * each "word") is joined by a single `line`, which will be rendered as a single space + * until this node's current line is out of room, at which `fill` will break at the + * most convenient instance of `line`. + */ + return fill(splitTextToDocs(node)); + } + + // case 'InlineComponent': + // case 'Slot': + case 'component': + case 'fragment': + case 'element': { + // const isEmpty = node.children?.every((child) => isEmptyTextNode(child)); + let isEmpty: boolean; + if (!node.children) { + isEmpty = true; + } else { + isEmpty = node.children.every((child) => isEmptyTextNode(child)); + } + const isSelfClosingTag = + isEmpty && (node.type !== 'element' || selfClosingTags.indexOf(node.name) !== -1); + + const attributes = path.map(print, 'attributes'); + if (isSelfClosingTag) { + return group(['<', node.name, indent(group(attributes)), line, `/>`]); + // return group(['<', node.name, indent(group([...attributes, opts.jsxBracketNewLine ? dedent(line) : ''])), ...[opts.jsxBracketNewLine ? '' : ' ', `/>`]]); + } + + if (node.children) { + const children = node.children; + const firstChild = children[0]; + const lastChild = children[children.length - 1]; + + // No hugging of content means it's either a block element and/or there's whitespace at the start/end + let noHugSeparatorStart: + | _doc.builders.Concat + | _doc.builders.Line + | _doc.builders.Softline + | string = softline; + let noHugSeparatorEnd: + | _doc.builders.Concat + | _doc.builders.Line + | _doc.builders.Softline + | string = softline; + const hugStart = shouldHugStart(node, opts); + const hugEnd = shouldHugEnd(node, opts); + + let body; + + if (isEmpty) { + body = + isInlineElement(path, opts, node) && + node.children.length && + isTextNodeStartingWithWhitespace(node.children[0]) && + !isPreTagContent(path) + ? () => line + : // () => (opts.jsxBracketNewLine ? '' : softline); + () => softline; + } else if (isPreTagContent(path)) { + body = () => printRaw(node); + } else if (isInlineElement(path, opts, node) && !isPreTagContent(path)) { + body = () => path.map(print, 'children'); + } else { + body = () => path.map(print, 'children'); + } + + const openingTag = [ + '<', + node.name, + indent( + group([ + ...attributes, + hugStart + ? '' + : !isPreTagContent(path) && !opts.bracketSameLine + ? dedent(softline) + : '', + ]) + ), + ]; + // const openingTag = ['<', node.name, indent(group([...attributes, hugStart ? '' : opts.jsxBracketNewLine && !isPreTagContent(path) ? dedent(softline) : '']))]; + + if (hugStart && hugEnd) { + const huggedContent = [softline, group(['>', body(), `', + ]); + } + + if (isPreTagContent(path)) { + noHugSeparatorStart = ''; + noHugSeparatorEnd = ''; + } else { + let didSetEndSeparator = false; + + if (!hugStart && firstChild && isTextNode(firstChild)) { + if ( + isTextNodeStartingWithLinebreak(firstChild) && + firstChild !== lastChild && + (!isInlineElement(path, opts, node) || isTextNodeEndingWithWhitespace(lastChild)) + ) { + noHugSeparatorStart = hardline; + noHugSeparatorEnd = hardline; + didSetEndSeparator = true; + } else if (isInlineElement(path, opts, node)) { + noHugSeparatorStart = line; + } + trimTextNodeLeft(firstChild); + } + if (!hugEnd && lastChild && isTextNode(lastChild)) { + if (isInlineElement(path, opts, node) && !didSetEndSeparator) { + noHugSeparatorEnd = softline; + } + trimTextNodeRight(lastChild); + } + } + + if (hugStart) { + return group([ + ...openingTag, + indent([softline, group(['>', body()])]), + noHugSeparatorEnd, + ``, + ]); + } + + if (hugEnd) { + return group([ + ...openingTag, + '>', + indent([noHugSeparatorStart, group([body(), `', + ]); + } + + if (isEmpty) { + return group([...openingTag, '>', body(), ``]); + } + + return group([ + ...openingTag, + '>', + indent([noHugSeparatorStart, body()]), + noHugSeparatorEnd, + ``, + ]); + } + // TODO: WIP + return ''; + } + // case 'AttributeShorthand': { + // return node.expression.name; + // } + case 'attribute': { + const name = node.name.trim(); + const quote = opts.singleQuote ? "'" : '"'; + switch (node.kind) { + case 'empty': + return [line, name]; + case 'expression': + // HANDLED IN EMBED FUNCION + return ''; + case 'quoted': + return [line, name, '=', quote, node.value, quote]; + case 'shorthand': + return [line, '{', name, '}']; + case 'spread': + return [line, '{...', name, '}']; + case 'template-literal': + return [line, name, '=', '`', node.value, '`']; + default: + break; + } + return ''; + } + + case 'doctype': { + // https://www.w3.org/wiki/Doctypes_and_markup_styles + return ['', hardline]; + } + // case 'Expression': + // // missing test ? + // return []; + // case 'MustacheTag': + // return [ + // '{', + // printJS(path, print, 'expression', { + // forceSingleLine: isInsideQuotedAttribute(path), + // forceSingleQuote: opts.jsxSingleQuote, + // }), + // '}', + // ]; + // case 'Spread': + // return [ + // line, + // '{...', + // printJS(path, print, 'expression', { + // forceSingleQuote: true, + // forceSingleLine: false, + // }), + // '}', + // ]; + case 'comment': + const nextNode = getNextNode(path); + let trailingLine: _doc.builders.Concat | string = ''; + if (nextNode && isTagLikeNode(nextNode)) { + trailingLine = hardline; + } + return ['', trailingLine]; + // case 'CodeSpan': + // return getUnencodedText(node); + // case 'CodeFence': { + // console.debug(node); + // // const lang = node.metadata.slice(3); + // return [node.metadata, hardline, /** somehow call textToDoc(lang), */ node.data, hardline, '```', hardline]; + + // // We should use `node.metadata` to select a parser to embed with... something like return [node.metadata, hardline textToDoc(node.getMetadataLanguage()), hardline, `\`\`\``]; + // } + default: { + throw new Error(`Unhandled node type "${node.type}"!`); + } + } } /** @@ -378,183 +467,258 @@ function print(path: AstPath, opts: ParserOptions, print: printFn): Doc { * If the text starts or ends with multiple newlines, two of those should be kept. */ function splitTextToDocs(node: NodeWithText): Doc[] { - const text = getUnencodedText(node); + const text = getUnencodedText(node); - let textLines = text.split(/[\t\n\f\r ]+/); + const textLines = text.split(/[\t\n\f\r ]+/); - let docs = join(line, textLines).parts.filter((s) => s !== ''); + let docs = join(line, textLines).parts.filter((s) => s !== ''); - if (startsWithLinebreak(text)) { - docs[0] = hardline; - } - if (startsWithLinebreak(text, 2)) { - docs = [hardline, ...docs]; - } + if (startsWithLinebreak(text)) { + docs[0] = hardline; + } + if (startsWithLinebreak(text, 2)) { + docs = [hardline, ...docs]; + } - if (endsWithLinebreak(text)) { - docs[docs.length - 1] = hardline; - } - if (endsWithLinebreak(text, 2)) { - docs = [...docs, hardline]; - } + if (endsWithLinebreak(text)) { + docs[docs.length - 1] = hardline; + } + if (endsWithLinebreak(text, 2)) { + docs = [...docs, hardline]; + } - return docs; + return docs; } -// TODO: CHANGE 'parsers' TYPE -function expressionParser(text: string, parsers: any, opts: ParserOptions) { - const ast = parsers.babel(text, parsers, opts); +function expressionParser(text: string, parsers: BuiltInParsers, opts: ParserOptions) { + const ast = parsers.babel(text, opts); + // const ast = parsers.babel(text, parsers, opts); - return { ...ast, program: ast.program.body[0].expression }; + return { ...ast, program: ast.program.body[0].expression }; } let markdownComponentName = new Set(); -function embed(path: AstPath, print: printFn, textToDoc: (text: string, options: object) => Doc, opts: ParserOptions) { - // TODO: ADD TYPES OR FIND ANOTHER WAY TO ACHIVE THIS - // @ts-ignore - if (!opts.__astro) opts.__astro = {}; - - const node = path.getValue(); - - if (!node) return null; - - // TODO: ADD TYPES OR FIND ANOTHER WAY TO ACHIVE THIS - // @ts-ignore - if (node.isJS) { - try { - const embeddedopts = { - parser: expressionParser, - }; - // TODO: ADD TYPES OR FIND ANOTHER WAY TO ACHIVE THIS - // @ts-ignore - if (node.forceSingleQuote) { - // TODO: ADD TYPES OR FIND ANOTHER WAY TO ACHIVE THIS - // @ts-ignore - embeddedopts.singleQuote = true; - } - - const docs = textToDoc(forceIntoExpression(getText(node, opts)), embeddedopts); - // TODO: ADD TYPES OR FIND ANOTHER WAY TO ACHIVE THIS - // @ts-ignore - return node.forceSingleLine ? removeLines(docs) : docs; - } catch (e) { - return getText(node, opts); - } - } - - if (node.type === 'Script' && node.context === 'setup') { - markdownComponentName = getMarkdownName(node.content); - return group(['---', hardline, textToDoc(node.content, { ...opts, parser: 'typescript' }), '---', hardline]); - } - - // format ']; + } + // if (isTextNode(node)) { + // const parent = path.getParentNode(); + + // if (parent && parent.type === 'Element' && parent.name === 'script') { + // const formatttedScript = textToDoc(node.data, { ...opts, parser: 'typescript' }); + // return stripTrailingHardline(formatttedScript); + // } + // } + + // format style element + if (node.type === 'element' && node.name === 'style') { + const styleTagContent = printRaw(node); + + const supportedStyleLangValues = ['css', 'scss', 'sass']; + let parserLang = 'css'; + + if (node.attributes) { + const langAttribute = node.attributes.filter((x) => x.name === 'lang'); + if (langAttribute.length) { + const styleLang = langAttribute[0].value.toLowerCase(); + if (supportedStyleLangValues.includes(styleLang)) parserLang = styleLang; + } + } + + switch (parserLang) { + case 'css': + case 'scss': { + // the css parser appends an extra indented hardline, which we want outside of the `indent()`, + // so we remove the last element of the array + let formattedStyles = textToDoc(styleTagContent, { + ...opts, + parser: parserLang, + }); + + formattedStyles = stripTrailingHardline(formattedStyles); + + // print + const attributes = path.map(print, 'attributes'); + const openingTag = group(['']); + return [openingTag, indent([hardline, formattedStyles]), hardline, '']; + } + case 'sass': { + const lineEnding = opts.endOfLine.toUpperCase() === 'CRLF' ? 'CRLF' : 'LF'; + const sassOptions: Partial = { + tabSize: opts.tabWidth, + insertSpaces: !opts.useTabs, + lineEnding, + }; + + // dedent the .sass, otherwise SassFormatter gets indentation wrong + const { result: raw } = manualDedent(styleTagContent); + + // format + const formattedSassIndented = SassFormatter.Format(raw, sassOptions).trim(); + + // print + const formattedSass = join(hardline, formattedSassIndented.split('\n')); + const attributes = path.map(print, 'attributes'); + const openingTag = group(['']); + return [openingTag, indent(group([hardline, formattedSass])), hardline, '']; + } + } + } + + // MARKDOWN COMPONENT + if (node.type === 'component' && markdownComponentName.has(node.name)) { + let content = printRaw(node); + + // dedent the content + content = content.replace(/\r\n/g, '\n'); + const contentArr = content.split('\n').map((s) => s.trimStart()); + content = contentArr.join('\n'); + + // format + let formatttedMarkdown = textToDoc(content, { + ...opts, + parser: 'markdown', + }); + formatttedMarkdown = stripTrailingHardline(formatttedMarkdown); + + // return formatttedMarkdown; + const attributes = path.map(print, 'attributes'); + const openingTag = group([`<${node.name}`, indent(group(attributes)), softline, '>']); + return [openingTag, indent(group([hardline, formatttedMarkdown])), hardline, ``]; + } + + return null; } function hasPrettierIgnore(path: AstP) { - const node = path.getNode(); + // const node = path.getNode(); - if (!node || !Array.isArray(node.comments)) return false; + // if (!node || !Array.isArray(node.comments)) return false; - const hasIgnore = node.comments.some( - (comment: any) => comment.data.includes('prettier-ignore') && !comment.data.includes('prettier-ignore-start') && !comment.data.includes('prettier-ignore-end') - ); - return hasIgnore; + // const hasIgnore = node.comments.some( + // (comment: any) => comment.data.includes('prettier-ignore') && !comment.data.includes('prettier-ignore-start') && !comment.data.includes('prettier-ignore-end') + // ); + // return hasIgnore; + return false; } const printer: Printer = { - print, - printComment, - embed, - hasPrettierIgnore, + print, + printComment, + embed, + hasPrettierIgnore, }; export default printer; diff --git a/src/utils.ts b/src/utils.ts index 0779eae..df3b51d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,20 +1,37 @@ import { AstPath as AstP, doc, Doc, ParserOptions as ParserOpts, util } from 'prettier'; import { - anyNode, - Ast, - AttributeNode, - AttributeShorthandNode, - attributeValue, - BlockElementNode, - blockElements, - InlineElementNode, - MustacheTagNode, - NodeWithChildren, - NodeWithText, - TextNode, + anyNode, + Node, + RootNode, + AttributeNode, + ElementNode, + ComponentNode, + CustomElementNode, + ExpressionNode, + TextNode, + FrontmatterNode, + DoctypeNode, + CommentNode, + NodeWithText, + blockElements, + // attributeValue, + BlockElementNode, + InlineElementNode, + // MustacheTagNode, + NodeWithChildren, + TagLikeNode, + // NodeWithText, + // TextNode, } from './nodes'; +import { createSyncFn } from 'synckit'; +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +// the worker path must be absolute +const serialize = createSyncFn(require.resolve('../workers/serialize-worker.js')); + type ParserOptions = ParserOpts; type AstPath = AstP; @@ -22,173 +39,203 @@ type AstPath = AstP; * HTML attributes that we may safely reformat (trim whitespace, add or remove newlines) */ export const formattableAttributes: string[] = [ - // None at the moment - // Prettier HTML does not format attributes at all - // and to be consistent we leave this array empty for now + // None at the moment + // Prettier HTML does not format attributes at all + // and to be consistent we leave this array empty for now ]; -const rootNodeKeys = new Set(['html', 'css', 'module']); +// const rootNodeKeys = new Set(['html', 'css', 'module']); + +// const isSync = makeSynchronous(async (node: anyNode) => { +// const dynamicImport = new Function('file', 'return import(file)'); +// const { is } = await dynamicImport('@astrojs/compiler/utils'); +// try { +// return await is(node); +// } catch (e) { +// console.error(e); +// } +// }); + +// export const is = (node: anyNode) => isSync(node); -export const isASTNode = (node: anyNode | Ast): node is Ast => typeof node === 'object' && Object.keys(node).filter((key) => rootNodeKeys.has(key)).length === rootNodeKeys.size; +export const isRootNode = (node: anyNode): node is RootNode => node.type === 'root'; -export const isEmptyTextNode = (node: anyNode): boolean => { - return !!node && node.type === 'Text' && getUnencodedText(node).trim() === ''; +export const isEmptyTextNode = (node: Node): boolean => { + return !!node && node.type === 'text' && getUnencodedText(node).trim() === ''; }; export const isPreTagContent = (path: AstPath): boolean => { - if (!path || !path.stack || !Array.isArray(path.stack)) return false; - return path.stack.some( - (node: anyNode) => (node.type === 'Element' && node.name.toLowerCase() === 'pre') || (node.type === 'Attribute' && !formattableAttributes.includes(node.name)) - ); + if (!path || !path.stack || !Array.isArray(path.stack)) return false; + return path.stack.some( + (node: anyNode) => + (node.type === 'element' && node.name.toLowerCase() === 'pre') || + (node.type === 'attribute' && !formattableAttributes.includes(node.name)) + ); }; -export function isLoneMustacheTag(node: attributeValue): node is [MustacheTagNode] { - return node !== true && node.length === 1 && node[0].type === 'MustacheTag'; +export function isLoneMustacheTag(node: AttributeNode): boolean { + // export function isLoneMustacheTag(node: AttributeNode): node is [MustacheTagNode] { + return node.kind === 'expression'; + // return node !== true && node.length === 1 && node[0].type === 'MustacheTag'; } -function isAttributeShorthand(node: attributeValue): node is [AttributeShorthandNode] { - return node !== true && node.length === 1 && node[0].type === 'AttributeShorthand'; -} +// function isAttributeShorthand(node: attributeValue): node is [AttributeShorthandNode] { +// return node !== true && node.length === 1 && node[0].type === 'AttributeShorthand'; +// } /** * True if node is of type `{a}` or `a={a}` */ export function isOrCanBeConvertedToShorthand(node: AttributeNode, opts: ParserOptions): boolean { - if (!opts.astroAllowShorthand) return false; - if (isAttributeShorthand(node.value)) { - return true; - } - - if (isLoneMustacheTag(node.value)) { - const expression = node.value[0].expression; - return expression.codeChunks[0].trim() === node.name; - // return (expression.type === 'Identifier' && expression.name === node.name) || (expression.type === 'Expression' && expression.codeChunks[0] === node.name); - } - - return false; + if (!opts.astroAllowShorthand) return false; + if (node.kind === 'shorthand') { + return true; + } + // if (isAttributeShorthand(node.value)) { + // return true; + // } + + if (node.value.trim() === node.name.trim()) { + return true; + } + + // if (isLoneMustacheTag(node.value)) { + // const expression = node.value[0].expression; + // return expression.codeChunks[0].trim() === node.name; + // // return (expression.type === 'Identifier' && expression.name === node.name) || (expression.type === 'Expression' && expression.codeChunks[0] === node.name); + // } + + return false; } /** * True if node is of type `{a}` and astroAllowShorthand is false */ -export function isShorthandAndMustBeConvertedToBinaryExpression(node: AttributeNode, opts: ParserOptions): boolean { - if (opts.astroAllowShorthand) return false; - if (isAttributeShorthand(node.value)) { - return true; - } - return false; -} - -export function flatten(arrays: T[][]): T[] { - return ([] as T[]).concat.apply([], arrays); -} +export function isShorthandAndMustBeConvertedToBinaryExpression( + node: AttributeNode, + opts: ParserOptions +): boolean { + if (opts.astroAllowShorthand) return false; + if (node.type === 'attribute' && node.kind === 'shorthand') { + return true; + } + // if (isAttributeShorthand(node.value)) { + // return true; + // } + return false; +} + +// export function flatten(arrays: T[][]): T[] { +// return ([] as T[]).concat.apply([], arrays); +// } +// TODO: TEST IF IT'S GETTING THE CORRECT TEXT export function getText(node: anyNode, opts: ParserOptions): string { - return opts.originalText.slice(opts.locStart(node), opts.locEnd(node)); - // const leadingComments = node.leadingComments; - - // return options.originalText.slice( - // options.locStart( - // // if there are comments before the node they are not included - // // in the `start` of the node itself - // (leadingComments && leadingComments[0]) || node - // ), - // options.locEnd(node) - // ); + if (!node.position) return ''; + return opts.originalText.slice(node.position.start.offset + 1, node.position.end?.offset); + // return opts.originalText.slice(opts.locStart(node), opts.locEnd(node)); } export function getUnencodedText(node: NodeWithText): string { - // `raw` will contain HTML entities in unencoded form - return node.raw || node.data; -} - -export function replaceEndOfLineWith(text: string, replacement: doc.builders.DocCommand): Doc[] { - const parts = []; - for (const part of text.split('\n')) { - if (parts.length > 0) { - parts.push(replacement); - } - if (part.endsWith('\r')) { - parts.push(part.slice(0, -1)); - } else { - parts.push(part); - } - } - return parts; -} - -export function printRaw(node: anyNode, originalText: string, stripLeadingAndTrailingNewline: boolean = false): string { - if (!isNodeWithChildren(node)) { - return ''; - } + return node.value; +} + +// export function replaceEndOfLineWith(text: string, replacement: doc.builders.DocCommand): Doc[] { +// const parts = []; +// for (const part of text.split('\n')) { +// if (parts.length > 0) { +// parts.push(replacement); +// } +// if (part.endsWith('\r')) { +// parts.push(part.slice(0, -1)); +// } else { +// parts.push(part); +// } +// } +// return parts; +// } - if (node.children.length === 0) { - return ''; - } +/** + * Returns the content of the node + */ +export function printRaw(node: anyNode, stripLeadingAndTrailingNewline = false): string { + if (!isNodeWithChildren(node)) { + return ''; + } - const firstChild = node.children[0]; - const lastChild = node.children[node.children.length - 1]; + if (node.children.length === 0) { + return ''; + } - let raw = originalText.substring(firstChild.start, lastChild.end); + let raw = node.children.reduce((prev: string, curr: Node) => prev + serialize(curr), ''); - if (!stripLeadingAndTrailingNewline) { - return raw; - } + if (!stripLeadingAndTrailingNewline) { + return raw; + } - if (startsWithLinebreak(raw)) { - raw = raw.substring(raw.indexOf('\n') + 1); - } - if (endsWithLinebreak(raw)) { - raw = raw.substring(0, raw.lastIndexOf('\n')); - if (raw.charAt(raw.length - 1) === '\r') { - raw = raw.substring(0, raw.length - 1); - } - } + if (startsWithLinebreak(raw)) { + raw = raw.substring(raw.indexOf('\n') + 1); + } + if (endsWithLinebreak(raw)) { + raw = raw.substring(0, raw.lastIndexOf('\n')); + if (raw.charAt(raw.length - 1) === '\r') { + raw = raw.substring(0, raw.length - 1); + } + } - return raw; + return raw; } export function isNodeWithChildren(node: anyNode): node is anyNode & NodeWithChildren { - return node && Array.isArray(node.children); + return node && 'children' in node && Array.isArray(node.children); } -export function isInlineElement(path: AstPath, opts: ParserOptions, node: anyNode): node is InlineElementNode { - return node && node.type === 'Element' && !isBlockElement(node, opts) && !isPreTagContent(path); +export function isInlineElement( + path: AstPath, + opts: ParserOptions, + node: anyNode +): node is InlineElementNode { + return node && node.type === 'element' && !isBlockElement(node, opts) && !isPreTagContent(path); } export function isBlockElement(node: anyNode, opts: ParserOptions): node is BlockElementNode { - return node && node.type === 'Element' && opts.htmlWhitespaceSensitivity !== 'strict' && (opts.htmlWhitespaceSensitivity === 'ignore' || blockElements.includes(node.name)); + return ( + node && + node.type === 'element' && + opts.htmlWhitespaceSensitivity !== 'strict' && + (opts.htmlWhitespaceSensitivity === 'ignore' || blockElements.includes(node.name)) + ); } -export function isTextNodeStartingWithLinebreak(node: TextNode, nrLines: number = 1): node is TextNode { - return startsWithLinebreak(getUnencodedText(node), nrLines); - // return node.type === 'Text' && startsWithLinebreak(getUnencodedText(node), nrLines); +export function isTextNodeStartingWithLinebreak(node: TextNode, nrLines = 1): node is TextNode { + return startsWithLinebreak(getUnencodedText(node), nrLines); + // return node.type === 'Text' && startsWithLinebreak(getUnencodedText(node), nrLines); } -export function startsWithLinebreak(text: string, nrLines: number = 1): boolean { - return new RegExp(`^([\\t\\f\\r ]*\\n){${nrLines}}`).test(text); +export function startsWithLinebreak(text: string, nrLines = 1): boolean { + return new RegExp(`^([\\t\\f\\r ]*\\n){${nrLines}}`).test(text); } -export function isTextNodeEndingWithLinebreak(node: TextNode, nrLines: number = 1) { - return node.type === 'Text' && endsWithLinebreak(getUnencodedText(node), nrLines); -} +// export function isTextNodeEndingWithLinebreak(node: TextNode, nrLines: number = 1) { +// return node.type === 'text' && endsWithLinebreak(getUnencodedText(node), nrLines); +// } -export function endsWithLinebreak(text: string, nrLines: number = 1): boolean { - return new RegExp(`(\\n[\\t\\f\\r ]*){${nrLines}}$`).test(text); +export function endsWithLinebreak(text: string, nrLines = 1): boolean { + return new RegExp(`(\\n[\\t\\f\\r ]*){${nrLines}}$`).test(text); } -export function isTextNodeStartingWithWhitespace(node: anyNode): node is TextNode { - return node.type === 'Text' && /^\s/.test(getUnencodedText(node)); +export function isTextNodeStartingWithWhitespace(node: Node): node is TextNode { + return node.type === 'text' && /^\s/.test(getUnencodedText(node)); } -export function isTextNodeEndingWithWhitespace(node: anyNode): node is TextNode { - return node.type === 'Text' && /\s$/.test(getUnencodedText(node)); +export function isTextNodeEndingWithWhitespace(node: Node): node is TextNode { + return node.type === 'text' && /\s$/.test(getUnencodedText(node)); } export function forceIntoExpression(statement: string): string { - // note the trailing newline: if the statement ends in a // comment, - // we can't add the closing bracket right afterwards - return `(${statement}\n)`; + // note the trailing newline: if the statement ends in a // comment, + // we can't add the closing bracket right afterwards + return `(${statement}\n)`; } /** @@ -196,21 +243,21 @@ export function forceIntoExpression(statement: string): string { * no whitespace between the `>` and the first child. */ export function shouldHugStart(node: anyNode, opts: ParserOptions): boolean { - if (isBlockElement(node, opts)) { - return false; - } + if (isBlockElement(node, opts)) { + return false; + } - if (!isNodeWithChildren(node)) { - return false; - } + if (!isNodeWithChildren(node)) { + return false; + } - const children = node.children; - if (children.length === 0) { - return true; - } + const children = node.children; + if (children.length === 0) { + return true; + } - const firstChild = children[0]; - return !isTextNodeStartingWithWhitespace(firstChild); + const firstChild = children[0]; + return !isTextNodeStartingWithWhitespace(firstChild); } /** @@ -218,126 +265,127 @@ export function shouldHugStart(node: anyNode, opts: ParserOptions): boolean { * no whitespace between the last child and the `` can be omitted. */ export function canOmitSoftlineBeforeClosingTag(path: AstPath, opts: ParserOptions): boolean { - return isLastChildWithinParentBlockElement(path, opts); - // return !hugsStartOfNextNode(node, options) || isLastChildWithinParentBlockElement(path, options); - // return !options.svelteBracketNewLine && (!hugsStartOfNextNode(node, options) || isLastChildWithinParentBlockElement(path, options)); + return isLastChildWithinParentBlockElement(path, opts); + // return !hugsStartOfNextNode(node, options) || isLastChildWithinParentBlockElement(path, options); + // return !options.svelteBracketNewLine && (!hugsStartOfNextNode(node, options) || isLastChildWithinParentBlockElement(path, options)); } /** * Return true if given node does not hug the next node, meaning there's whitespace * or the end of the doc afterwards. */ -function hugsStartOfNextNode(node: anyNode, opts: ParserOptions): boolean { - if (node.end === opts.originalText.length) { - // end of document - return false; - } +// function hugsStartOfNextNode(node: anyNode, opts: ParserOptions): boolean { +// if (node.end === opts.originalText.length) { +// // end of document +// return false; +// } - return !opts.originalText.substring(node.end).match(/^\s/); -} +// return !opts.originalText.substring(node.end).match(/^\s/); +// } -function getChildren(node: anyNode): anyNode[] { - return isNodeWithChildren(node) ? node.children : []; +function getChildren(node: anyNode): Node[] { + return isNodeWithChildren(node) ? node.children : []; } function isLastChildWithinParentBlockElement(path: AstPath, opts: ParserOptions): boolean { - const parent = path.getParentNode(); - if (!parent || !isBlockElement(parent, opts)) { - return false; - } - - const children = getChildren(parent); - const lastChild = children[children.length - 1]; - return lastChild === path.getNode(); + const parent = path.getParentNode(); + if (!parent || !isBlockElement(parent, opts)) { + return false; + } + + const children = getChildren(parent); + const lastChild = children[children.length - 1]; + return lastChild === path.getNode(); } export function trimTextNodeLeft(node: TextNode): void { - node.raw = node.raw && node.raw.trimLeft(); - node.data = node.data && node.data.trimLeft(); + node.value = node.value && node.value.trimStart(); } export function trimTextNodeRight(node: TextNode): void { - node.raw = node.raw && node.raw.trimRight(); - node.data = node.data && node.data.trimRight(); + node.value = node.value && node.value.trimEnd(); } -export function findLastIndex(isMatch: (item: T, idx: number) => boolean, items: T[]) { - for (let i = items.length - 1; i >= 0; i--) { - if (isMatch(items[i], i)) { - return i; - } - } +// export function findLastIndex(isMatch: (item: T, idx: number) => boolean, items: T[]) { +// for (let i = items.length - 1; i >= 0; i--) { +// if (isMatch(items[i], i)) { +// return i; +// } +// } - return -1; -} +// return -1; +// } /** * Remove all leading whitespace up until the first non-empty text node, * and all trailing whitepsace from the last non-empty text node onwards. */ -export function trimChildren(children: anyNode[]) { - // export function trimChildren(children: anyNode[], path: AstPath) { - let firstNonEmptyNode = children.findIndex((n) => !isEmptyTextNode(n)); - // let firstNonEmptyNode = children.findIndex((n) => !isEmptyTextNode(n) && !doesEmbedStartAfterNode(n, path)); - firstNonEmptyNode = firstNonEmptyNode === -1 ? children.length - 1 : firstNonEmptyNode; - - let lastNonEmptyNode = findLastIndex((n, idx) => { - // Last node is ok to end at the start of an embedded region, - // if it's not a comment (which should stick to the region) - return !isEmptyTextNode(n); - // return !isEmptyTextNode(n) && ((idx === children.length - 1 && n.type !== 'Comment') || !doesEmbedStartAfterNode(n, path)); - }, children); - lastNonEmptyNode = lastNonEmptyNode === -1 ? 0 : lastNonEmptyNode; - - for (let i = 0; i <= firstNonEmptyNode; i++) { - const n = children[i]; - if (isTextNode(n)) { - trimTextNodeLeft(n); - } - } - - for (let i = children.length - 1; i >= lastNonEmptyNode; i--) { - const n = children[i]; - if (isTextNode(n)) { - trimTextNodeRight(n); - } - } -} +// export function trimChildren(children: anyNode[]) { +// // export function trimChildren(children: anyNode[], path: AstPath) { +// let firstNonEmptyNode = children.findIndex((n) => !isEmptyTextNode(n)); +// // let firstNonEmptyNode = children.findIndex((n) => !isEmptyTextNode(n) && !doesEmbedStartAfterNode(n, path)); +// firstNonEmptyNode = firstNonEmptyNode === -1 ? children.length - 1 : firstNonEmptyNode; + +// let lastNonEmptyNode = findLastIndex((n, idx) => { +// // Last node is ok to end at the start of an embedded region, +// // if it's not a comment (which should stick to the region) +// return !isEmptyTextNode(n); +// // return !isEmptyTextNode(n) && ((idx === children.length - 1 && n.type !== 'Comment') || !doesEmbedStartAfterNode(n, path)); +// }, children); +// lastNonEmptyNode = lastNonEmptyNode === -1 ? 0 : lastNonEmptyNode; + +// for (let i = 0; i <= firstNonEmptyNode; i++) { +// const n = children[i]; +// if (isTextNode(n)) { +// trimTextNodeLeft(n); +// } +// } + +// for (let i = children.length - 1; i >= lastNonEmptyNode; i--) { +// const n = children[i]; +// if (isTextNode(n)) { +// trimTextNodeRight(n); +// } +// } +// } /** * Returns siblings, that is, the children of the parent. */ -export function getSiblings(path: AstPath): anyNode[] { - let parent = path.getParentNode(); - if (!parent) return []; +// export function getSiblings(path: AstPath): anyNode[] { +// let parent = path.getParentNode(); +// if (!parent) return []; - if (isASTNode(parent)) { - parent = parent.html; - } +// if (isRootNode(parent)) { +// parent = parent.html; +// } - return getChildren(parent); -} +// return getChildren(parent); +// } /** * Did there use to be any embedded object (that has been snipped out of the AST to be moved) @@ -370,72 +418,72 @@ export function getSiblings(path: AstPath): anyNode[] { * runtime version of prettier than what we import, making a reference check fail. */ -function isHardline(docToCheck: Doc): boolean { - return docToCheck === doc.builders.hardline || deepEqual(docToCheck, doc.builders.hardline); -} +// function isHardline(docToCheck: Doc): boolean { +// return docToCheck === doc.builders.hardline || deepEqual(docToCheck, doc.builders.hardline); +// } /** * Simple deep equal function which suits our needs. Only works properly on POJOs without cyclic deps. */ -function deepEqual(x: any, y: any): boolean { - if (x === y) { - return true; - } else if (typeof x == 'object' && x != null && typeof y == 'object' && y != null) { - if (Object.keys(x).length != Object.keys(y).length) return false; - - for (var prop in x) { - if (Object.prototype.hasOwnProperty.call(y, prop)) { - if (!deepEqual(x[prop], y[prop])) return false; - } else { - return false; - } - } - - return true; - } else { - return false; - } -} +// function deepEqual(x: any, y: any): boolean { +// if (x === y) { +// return true; +// } else if (typeof x == 'object' && x != null && typeof y == 'object' && y != null) { +// if (Object.keys(x).length != Object.keys(y).length) return false; + +// for (var prop in x) { +// if (Object.prototype.hasOwnProperty.call(y, prop)) { +// if (!deepEqual(x[prop], y[prop])) return false; +// } else { +// return false; +// } +// } + +// return true; +// } else { +// return false; +// } +// } -export function isLine(docToCheck: Doc): boolean { - return ( - isHardline(docToCheck) || - (typeof docToCheck === 'object' && isDocCommand(docToCheck) && docToCheck.type === 'line') || - (typeof docToCheck === 'object' && isDocCommand(docToCheck) && docToCheck.type === 'concat' && docToCheck.parts.every(isLine)) - ); -} +// export function isLine(docToCheck: Doc): boolean { +// return ( +// isHardline(docToCheck) || +// (typeof docToCheck === 'object' && isDocCommand(docToCheck) && docToCheck.type === 'line') || +// (typeof docToCheck === 'object' && isDocCommand(docToCheck) && docToCheck.type === 'concat' && docToCheck.parts.every(isLine)) +// ); +// } /** * Check if the doc is empty, i.e. consists of nothing more than empty strings (possibly nested). */ -export function isEmptyDoc(doc: Doc): boolean { - if (typeof doc === 'string') { - return doc.length === 0; - } +// export function isEmptyDoc(doc: Doc): boolean { +// if (typeof doc === 'string') { +// return doc.length === 0; +// } - // if (doc.type === 'line') { - // return !doc.keepIfLonely; - // } +// // if (doc.type === 'line') { +// // return !doc.keepIfLonely; +// // } - // Since Prettier 2.3.0, concats are represented as flat arrays - if (Array.isArray(doc)) { - return doc.length === 0; - } +// // Since Prettier 2.3.0, concats are represented as flat arrays +// if (Array.isArray(doc)) { +// return doc.length === 0; +// } - // const { contents } = doc; +// // const { contents } = doc; - // if (contents) { - // return isEmptyDoc(contents); - // } +// // if (contents) { +// // return isEmptyDoc(contents); +// // } - // const { parts } = doc; +// // const { parts } = doc; - // if (parts) { - // return isEmptyGroup(parts); - // } +// // if (parts) { +// // return isEmptyGroup(parts); +// // } - return false; -} +// return false; +// } // function isEmptyGroup(group: any) { // return !group.find((doc: any) => !isEmptyDoc(doc)); @@ -445,192 +493,263 @@ export function isEmptyDoc(doc: Doc): boolean { * Trims both leading and trailing nodes matching `isWhitespace` independent of nesting level * (though all trimmed adjacent nodes need to be a the same level). Modifies the `docs` array. */ -export function trim(docs: Doc[], isWhitespace: (doc: Doc) => boolean): Doc[] { - trimLeft(docs, isWhitespace); - trimRight(docs, isWhitespace); +// export function trim(docs: Doc[], isWhitespace: (doc: Doc) => boolean): Doc[] { +// trimLeft(docs, isWhitespace); +// trimRight(docs, isWhitespace); - return docs; -} +// return docs; +// } /** * Trims the leading nodes matching `isWhitespace` independent of nesting level (though all nodes need to be a the same level). * If there are empty docs before the first whitespace, they are removed, too. */ -function trimLeft(group: Doc[], isWhitespace: (doc: Doc) => boolean): void { - let firstNonWhitespace = group.findIndex((doc) => !isEmptyDoc(doc) && !isWhitespace(doc)); - - if (firstNonWhitespace < 0 && group.length) { - firstNonWhitespace = group.length; - } - - if (firstNonWhitespace > 0) { - const removed = group.splice(0, firstNonWhitespace); - if (removed.every(isEmptyDoc)) { - return trimLeft(group, isWhitespace); - } - } else { - const parts = getParts(group[0]); - - if (parts) { - return trimLeft(parts, isWhitespace); - } - } -} +// function trimLeft(group: Doc[], isWhitespace: (doc: Doc) => boolean): void { +// let firstNonWhitespace = group.findIndex((doc) => !isEmptyDoc(doc) && !isWhitespace(doc)); + +// if (firstNonWhitespace < 0 && group.length) { +// firstNonWhitespace = group.length; +// } + +// if (firstNonWhitespace > 0) { +// const removed = group.splice(0, firstNonWhitespace); +// if (removed.every(isEmptyDoc)) { +// return trimLeft(group, isWhitespace); +// } +// } else { +// const parts = getParts(group[0]); + +// if (parts) { +// return trimLeft(parts, isWhitespace); +// } +// } +// } /** * Trims the trailing nodes matching `isWhitespace` independent of nesting level (though all nodes need to be a the same level). * If there are empty docs after the last whitespace, they are removed, too. */ -function trimRight(group: Doc[], isWhitespace: (doc: Doc) => boolean): void { - let lastNonWhitespace = group.length ? findLastIndex((doc: any) => !isEmptyDoc(doc) && !isWhitespace(doc), group) : 0; - - if (lastNonWhitespace < group.length - 1) { - const removed = group.splice(lastNonWhitespace + 1); - if (removed.every(isEmptyDoc)) { - return trimRight(group, isWhitespace); - } - } else { - const parts = getParts(group[group.length - 1]); - - if (parts) { - return trimRight(parts, isWhitespace); - } - } -} +// function trimRight(group: Doc[], isWhitespace: (doc: Doc) => boolean): void { +// let lastNonWhitespace = group.length ? findLastIndex((doc: any) => !isEmptyDoc(doc) && !isWhitespace(doc), group) : 0; + +// if (lastNonWhitespace < group.length - 1) { +// const removed = group.splice(lastNonWhitespace + 1); +// if (removed.every(isEmptyDoc)) { +// return trimRight(group, isWhitespace); +// } +// } else { +// const parts = getParts(group[group.length - 1]); + +// if (parts) { +// return trimRight(parts, isWhitespace); +// } +// } +// } -function getParts(doc: Doc): Doc[] | undefined { - if (typeof doc === 'object') { - // Since Prettier 2.3.0, concats are represented as flat arrays - if (Array.isArray(doc)) { - return doc; - } - if (doc.type === 'fill' || doc.type === 'concat') { - return doc.parts; - } - if (doc.type === 'group') { - return getParts(doc.contents); - } - } -} +// function getParts(doc: Doc): Doc[] | undefined { +// if (typeof doc === 'object') { +// // Since Prettier 2.3.0, concats are represented as flat arrays +// if (Array.isArray(doc)) { +// return doc; +// } +// if (doc.type === 'fill' || doc.type === 'concat') { +// return doc.parts; +// } +// if (doc.type === 'group') { +// return getParts(doc.contents); +// } +// } +// } -export const isObjEmpty = (obj: object): boolean => { - for (let i in obj) return false; - return true; -}; +// export const isObjEmpty = (obj: object): boolean => { +// for (let i in obj) return false; +// return true; +// }; /** Shallowly attach comments to children */ -export function attachCommentsHTML(node: anyNode): void { - if (!isNodeWithChildren(node) || !node.children.some(({ type }) => type === 'Comment')) return; - - const nodesToRemove = []; - - // note: the .length - 1 is because we don’t need to read the last node - for (let n = 0; n < node.children.length - 1; n++) { - if (!node.children[n]) continue; - - // attach comment to the next non-whitespace node - if (node.children[n].type === 'Comment') { - let next = n + 1; - while (isEmptyTextNode(node.children[next])) { - nodesToRemove.push(next); // if arbitrary whitespace between comment and node, remove - next++; // skip to the next non-whitespace node - } - const commentNode = node.children[next]; - if (commentNode) { - const comment = node.children[n]; - util.addLeadingComment(commentNode, comment); - } - } - } - - // remove arbitrary whitespace nodes - nodesToRemove.reverse(); // start at back so we aren’t changing indices - nodesToRemove.forEach((index) => { - node.children.splice(index, 1); - }); -} +// export function attachCommentsHTML(node: anyNode): void { +// if (!isNodeWithChildren(node) || !node.children.some(({ type }) => type === 'Comment')) return; + +// const nodesToRemove = []; + +// // note: the .length - 1 is because we don’t need to read the last node +// for (let n = 0; n < node.children.length - 1; n++) { +// if (!node.children[n]) continue; + +// // attach comment to the next non-whitespace node +// if (node.children[n].type === 'Comment') { +// let next = n + 1; +// while (isEmptyTextNode(node.children[next])) { +// nodesToRemove.push(next); // if arbitrary whitespace between comment and node, remove +// next++; // skip to the next non-whitespace node +// } +// const commentNode = node.children[next]; +// if (commentNode) { +// const comment = node.children[n]; +// util.addLeadingComment(commentNode, comment); +// } +// } +// } + +// // remove arbitrary whitespace nodes +// nodesToRemove.reverse(); // start at back so we aren’t changing indices +// nodesToRemove.forEach((index) => { +// node.children.splice(index, 1); +// }); +// } /** dedent string & return tabSize (the last part is what we need) */ -export function dedent(input: string): { tabSize: number; char: string; result: string } { - let minTabSize = Infinity; - let result = input; - // 1. normalize - result = result.replace(/\r\n/g, '\n'); - - // 2. count tabSize - let char = ''; - for (const line of result.split('\n')) { - if (!line) continue; - // if any line begins with a non-whitespace char, minTabSize is 0 - if (line[0] && /^[^\s]/.test(line[0])) { - minTabSize = 0; - break; - } - const match = line.match(/^(\s+)\S+/); // \S ensures we don’t count lines of pure whitespace - if (match) { - if (match[1] && !char) char = match[1][0]; - if (match[1].length < minTabSize) minTabSize = match[1].length; - } - } - - // 3. reformat string - if (minTabSize > 0 && Number.isFinite(minTabSize)) { - result = result.replace(new RegExp(`^${new Array(minTabSize + 1).join(char)}`, 'gm'), ''); - } - - return { - tabSize: minTabSize === Infinity ? 0 : minTabSize, - char, - result, - }; +export function manualDedent(input: string): { + tabSize: number; + char: string; + result: string; +} { + let minTabSize = Infinity; + let result = input; + // 1. normalize + result = result.replace(/\r\n/g, '\n'); + + // 2. count tabSize + let char = ''; + for (const line of result.split('\n')) { + if (!line) continue; + // if any line begins with a non-whitespace char, minTabSize is 0 + if (line[0] && /^[^\s]/.test(line[0])) { + minTabSize = 0; + break; + } + const match = line.match(/^(\s+)\S+/); // \S ensures we don’t count lines of pure whitespace + if (match) { + if (match[1] && !char) char = match[1][0]; + if (match[1].length < minTabSize) minTabSize = match[1].length; + } + } + + // 3. reformat string + if (minTabSize > 0 && Number.isFinite(minTabSize)) { + result = result.replace(new RegExp(`^${new Array(minTabSize + 1).join(char)}`, 'gm'), ''); + } + + return { + tabSize: minTabSize === Infinity ? 0 : minTabSize, + char, + result, + }; } /** re-indent string by chars */ -export function indent(input: string, char: string = ' '): string { - return input.replace(/^(.)/gm, `${char}$1`); -} +// export function indent(input: string, char: string = ' '): string { +// return input.replace(/^(.)/gm, `${char}$1`); +// } /** scan code for Markdown name(s) */ export function getMarkdownName(script: string): Set { - // default import: could be named anything - let defaultMatch; - while ((defaultMatch = /import\s+([^\s]+)\s+from\s+['|"|`]astro\/components\/Markdown\.astro/g.exec(script))) { - if (defaultMatch[1]) return new Set([defaultMatch[1].trim()]); - } - - // named component: must have "Markdown" in specifier, but can be renamed via "as" - let namedMatch; - while ((namedMatch = /import\s+\{\s*([^}]+)\}\s+from\s+['|"|`]astro\/components/g.exec(script))) { - if (namedMatch[1] && !namedMatch[1].includes('Markdown')) continue; - // if "Markdown" was imported, find out whether or not it was renamed - const rawImports = namedMatch[1].trim().replace(/^\{/, '').replace(/\}$/, '').trim(); - let importName = 'Markdown'; - for (const spec of rawImports.split(',')) { - const [original, renamed] = spec.split(' as ').map((s) => s.trim()); - if (original !== 'Markdown') continue; - importName = renamed || original; - break; - } - return new Set([importName]); - } - return new Set(['Markdown']); -} - + // default import: could be named anything + let defaultMatch; + while ( + (defaultMatch = /import\s+([^\s]+)\s+from\s+['|"|`]astro\/components\/Markdown\.astro/g.exec( + script + )) + ) { + if (defaultMatch[1]) return new Set([defaultMatch[1].trim()]); + } + + // named component: must have "Markdown" in specifier, but can be renamed via "as" + let namedMatch; + while ((namedMatch = /import\s+\{\s*([^}]+)\}\s+from\s+['|"|`]astro\/components/g.exec(script))) { + if (namedMatch[1] && !namedMatch[1].includes('Markdown')) continue; + // if "Markdown" was imported, find out whether or not it was renamed + const rawImports = namedMatch[1].trim().replace(/^\{/, '').replace(/\}$/, '').trim(); + let importName = 'Markdown'; + for (const spec of rawImports.split(',')) { + const [original, renamed] = spec.split(' as ').map((s) => s.trim()); + if (original !== 'Markdown') continue; + importName = renamed || original; + break; + } + return new Set([importName]); + } + return new Set(['Markdown']); +} + +// TODO: USE THE COMPILER +/** True if the node is of type text */ export function isTextNode(node: anyNode): node is TextNode { - return node.type === 'Text'; + return node.type === 'text'; } -export function isMustacheNode(node: anyNode): node is MustacheTagNode { - return node.type === 'MustacheTag'; +// export function isMustacheNode(node: anyNode): node is MustacheTagNode { +// return node.type === 'MustacheTag'; +// } + +// export function isDocCommand(doc: Doc): doc is doc.builders.DocCommand { +// if (typeof doc === 'string') return false; +// if (Array.isArray(doc)) return false; +// return true; +// } + +export function isInsideQuotedAttribute(path: AstPath): boolean { + const stack = path.stack as anyNode[]; + return stack.some((node) => node.type === 'attribute' && !isLoneMustacheTag(node)); } -export function isDocCommand(doc: Doc): doc is doc.builders.DocCommand { - if (typeof doc === 'string') return false; - if (Array.isArray(doc)) return false; - return true; +/** + * Currently, the compiler has a bug that duplicates text nodes when no + * TagLikeNode elements are present. + */ +export function removeDuplicates(root: RootNode) { + root.children = root.children.filter((node, i, rootChildren) => { + if (node.type !== 'text') return true; + // https://stackoverflow.com/questions/2218999/how-to-remove-all-duplicates-from-an-array-of-objects + return ( + i === + rootChildren.findIndex((t) => { + if (t.position && node.position) { + return ( + node.type === 'text' && + t.position.start.offset === node.position.start.offset && + t.position.start.line === node.position.start.line && + t.position.start.column === node.position.start.column + ); + } + }) + ); + }); +} + +/** True if the node is TagLikeNode: + * + * ElementNode | ComponentNode | CustomElementNode | FragmentNode */ +export function isTagLikeNode(node: anyNode): node is TagLikeNode { + return ( + node.type === 'element' || + node.type === 'component' || + node.type === 'custom-element' || + node.type === 'fragment' + ); } -export function isInsideQuotedAttribute(path: AstPath): boolean { - const stack = path.stack as anyNode[]; - return stack.some((node) => node.type === 'Attribute' && !isLoneMustacheTag(node.value)); +/** + * Returns siblings, that is, the children of the parent. + */ +export function getSiblings(path: AstPath): anyNode[] { + const parent = path.getParentNode(); + if (!parent) return []; + + return getChildren(parent); +} + +export function getNextNode(path: AstPath): anyNode | null { + const node = path.getNode(); + if (node) { + const siblings = getSiblings(path); + if (node.position?.start === siblings[siblings.length - 1].position?.start) return null; + for (let i = 0; i < siblings.length; i++) { + const sibling = siblings[i]; + if (sibling.position?.start === node.position?.start && i !== siblings.length - 1) { + return siblings[i + 1]; + } + } + } + return null; } diff --git a/test/astro-prettier.test.ts b/test/astro-prettier.test.ts deleted file mode 100644 index f9c4a59..0000000 --- a/test/astro-prettier.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import test from 'ava'; -import { Prettier, PrettierMarkdown, PrettierUnaltered } from './test-utils.js'; - -test('can format a basic Astro file', Prettier, 'basic'); - -test('can format an Astro file with a single style element', Prettier, 'single-style-element'); - -test('can format a basic Astro file with styles', Prettier, 'with-styles'); - -test(`Can format an Astro file with attributes in the -

- Hello world!

+ <> + Hello world! + + lorem - - - - diff --git a/test/fixtures/with-styles/output.astro b/test/fixtures/other/fragment/output.astro similarity index 77% rename from test/fixtures/with-styles/output.astro rename to test/fixtures/other/fragment/output.astro index ca0df0a..604781e 100644 --- a/test/fixtures/with-styles/output.astro +++ b/test/fixtures/other/fragment/output.astro @@ -7,12 +7,9 @@ Document -

Hello world!

+ <>Hello world! + + lorem + - - diff --git a/test/fixtures/frontmatter/input.astro b/test/fixtures/other/frontmatter/input.astro similarity index 100% rename from test/fixtures/frontmatter/input.astro rename to test/fixtures/other/frontmatter/input.astro diff --git a/test/fixtures/frontmatter/output.astro b/test/fixtures/other/frontmatter/output.astro similarity index 100% rename from test/fixtures/frontmatter/output.astro rename to test/fixtures/other/frontmatter/output.astro diff --git a/test/fixtures/nested-comment/input.astro b/test/fixtures/other/nested-comment/input.astro similarity index 100% rename from test/fixtures/nested-comment/input.astro rename to test/fixtures/other/nested-comment/input.astro diff --git a/test/fixtures/nested-comment/output.astro b/test/fixtures/other/nested-comment/output.astro similarity index 100% rename from test/fixtures/nested-comment/output.astro rename to test/fixtures/other/nested-comment/output.astro diff --git a/test/fixtures/preserve-tag-case/input.astro b/test/fixtures/other/preserve-tag-case/input.astro similarity index 100% rename from test/fixtures/preserve-tag-case/input.astro rename to test/fixtures/other/preserve-tag-case/input.astro diff --git a/test/fixtures/preserve-tag-case/output.astro b/test/fixtures/other/preserve-tag-case/output.astro similarity index 100% rename from test/fixtures/preserve-tag-case/output.astro rename to test/fixtures/other/preserve-tag-case/output.astro diff --git a/test/fixtures/prettier-ignore-html/input.astro b/test/fixtures/other/prettier-ignore-html/input.astro similarity index 100% rename from test/fixtures/prettier-ignore-html/input.astro rename to test/fixtures/other/prettier-ignore-html/input.astro diff --git a/test/fixtures/prettier-ignore-html/output.astro b/test/fixtures/other/prettier-ignore-html/output.astro similarity index 100% rename from test/fixtures/prettier-ignore-html/output.astro rename to test/fixtures/other/prettier-ignore-html/output.astro diff --git a/test/fixtures/prettier-ignore-js/input.astro b/test/fixtures/other/prettier-ignore-js/input.astro similarity index 100% rename from test/fixtures/prettier-ignore-js/input.astro rename to test/fixtures/other/prettier-ignore-js/input.astro diff --git a/test/fixtures/prettier-ignore-js/output.astro b/test/fixtures/other/prettier-ignore-js/output.astro similarity index 100% rename from test/fixtures/prettier-ignore-js/output.astro rename to test/fixtures/other/prettier-ignore-js/output.astro diff --git a/test/fixtures/other/spread-operator/input.astro b/test/fixtures/other/spread-operator/input.astro new file mode 100644 index 0000000..25738ba --- /dev/null +++ b/test/fixtures/other/spread-operator/input.astro @@ -0,0 +1,7 @@ +--- +const meta = { title: "My Title", lang: "en" } +--- +Foo + + +{meta && Bar} diff --git a/test/fixtures/spread-operator/output.astro b/test/fixtures/other/spread-operator/output.astro similarity index 54% rename from test/fixtures/spread-operator/output.astro rename to test/fixtures/other/spread-operator/output.astro index 47694b2..71e92ad 100644 --- a/test/fixtures/spread-operator/output.astro +++ b/test/fixtures/other/spread-operator/output.astro @@ -3,3 +3,6 @@ const meta = { title: "My Title", lang: "en" }; --- Foo + + +{meta && Bar} diff --git a/test/fixtures/unclosed-tag/input.astro b/test/fixtures/other/unclosed-tag/input.astro similarity index 100% rename from test/fixtures/unclosed-tag/input.astro rename to test/fixtures/other/unclosed-tag/input.astro diff --git a/test/fixtures/unclosed-tag/output.astro b/test/fixtures/other/unclosed-tag/output.astro similarity index 100% rename from test/fixtures/unclosed-tag/output.astro rename to test/fixtures/other/unclosed-tag/output.astro diff --git a/test/fixtures/with-script/input.astro b/test/fixtures/other/with-script/input.astro similarity index 100% rename from test/fixtures/with-script/input.astro rename to test/fixtures/other/with-script/input.astro diff --git a/test/fixtures/with-script/output.astro b/test/fixtures/other/with-script/output.astro similarity index 100% rename from test/fixtures/with-script/output.astro rename to test/fixtures/other/with-script/output.astro diff --git a/test/fixtures/spread-operator/input.astro b/test/fixtures/spread-operator/input.astro deleted file mode 100644 index 1c4f329..0000000 --- a/test/fixtures/spread-operator/input.astro +++ /dev/null @@ -1,4 +0,0 @@ ---- -const meta = { title: "My Title", lang: "en" } ---- -Foo \ No newline at end of file diff --git a/test/fixtures/format-nested-sass-style-tag-content/input.astro b/test/fixtures/styles/format-nested-sass-style-tag-content/input.astro similarity index 100% rename from test/fixtures/format-nested-sass-style-tag-content/input.astro rename to test/fixtures/styles/format-nested-sass-style-tag-content/input.astro diff --git a/test/fixtures/format-nested-sass-style-tag-content/output.astro b/test/fixtures/styles/format-nested-sass-style-tag-content/output.astro similarity index 100% rename from test/fixtures/format-nested-sass-style-tag-content/output.astro rename to test/fixtures/styles/format-nested-sass-style-tag-content/output.astro diff --git a/test/fixtures/format-nested-style-tag-content/input.astro b/test/fixtures/styles/format-nested-style-tag-content/input.astro similarity index 100% rename from test/fixtures/format-nested-style-tag-content/input.astro rename to test/fixtures/styles/format-nested-style-tag-content/input.astro diff --git a/test/fixtures/format-nested-style-tag-content/output.astro b/test/fixtures/styles/format-nested-style-tag-content/output.astro similarity index 100% rename from test/fixtures/format-nested-style-tag-content/output.astro rename to test/fixtures/styles/format-nested-style-tag-content/output.astro diff --git a/test/fixtures/style-tag-attributes/input.astro b/test/fixtures/styles/style-tag-attributes/input.astro similarity index 82% rename from test/fixtures/style-tag-attributes/input.astro rename to test/fixtures/styles/style-tag-attributes/input.astro index 1508afb..8eba613 100644 --- a/test/fixtures/style-tag-attributes/input.astro +++ b/test/fixtures/styles/style-tag-attributes/input.astro @@ -1,5 +1,4 @@ diff --git a/test/fixtures/style-tag-attributes/output.astro b/test/fixtures/styles/style-tag-attributes/output.astro similarity index 91% rename from test/fixtures/style-tag-attributes/output.astro rename to test/fixtures/styles/style-tag-attributes/output.astro index d1863da..77a4287 100644 --- a/test/fixtures/style-tag-attributes/output.astro +++ b/test/fixtures/styles/style-tag-attributes/output.astro @@ -3,7 +3,7 @@ lang="scss" media={"all and (max-width: 500px)"} type={type2} - {title} + title={title} id={10} anObject={{ prop: "value" }} > diff --git a/test/fixtures/with-indented-sass/input.astro b/test/fixtures/styles/with-indented-sass/input.astro similarity index 88% rename from test/fixtures/with-indented-sass/input.astro rename to test/fixtures/styles/with-indented-sass/input.astro index dd46119..994bf82 100644 --- a/test/fixtures/with-indented-sass/input.astro +++ b/test/fixtures/styles/with-indented-sass/input.astro @@ -1,17 +1,4 @@ - - - - - - - Document - - -

- Hello world!

- - - +
lorem
- -

Hello world!

- - +
lorem
- -

- Hello world!

- - - - +
lorem
- -

Hello world!

- - +
lorem
- -

- Hello world!

- - +
lorem
diff --git a/test/fixtures/with-scss/output.astro b/test/fixtures/styles/with-scss/output.astro similarity index 55% rename from test/fixtures/with-scss/output.astro rename to test/fixtures/styles/with-scss/output.astro index eae7110..01ce7cb 100644 --- a/test/fixtures/with-scss/output.astro +++ b/test/fixtures/styles/with-scss/output.astro @@ -1,15 +1,4 @@ - - - - - - - Document - - -

Hello world!

- - +
lorem
+ + +

This is a heading

+

This is a paragraph.

+ + + + + + diff --git a/test/fixtures/styles/with-styles-and-body-tag/output.astro b/test/fixtures/styles/with-styles-and-body-tag/output.astro new file mode 100644 index 0000000..4acf703 --- /dev/null +++ b/test/fixtures/styles/with-styles-and-body-tag/output.astro @@ -0,0 +1,17 @@ + + + Title of the document + + + +

This is a heading

+

This is a paragraph.

+ + + + + diff --git a/test/fixtures/styles/with-styles/input.astro b/test/fixtures/styles/with-styles/input.astro new file mode 100644 index 0000000..68fc72d --- /dev/null +++ b/test/fixtures/styles/with-styles/input.astro @@ -0,0 +1,8 @@ +
lorem
+ + + diff --git a/test/fixtures/styles/with-styles/output.astro b/test/fixtures/styles/with-styles/output.astro new file mode 100644 index 0000000..da4717f --- /dev/null +++ b/test/fixtures/styles/with-styles/output.astro @@ -0,0 +1,7 @@ +
lorem
+ + diff --git a/test/package.json b/test/package.json deleted file mode 100644 index 3dbc1ca..0000000 --- a/test/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/test/test-utils.ts b/test/test-utils.ts index eaabce1..c308238 100644 --- a/test/test-utils.ts +++ b/test/test-utils.ts @@ -1,137 +1,91 @@ import prettier from 'prettier'; import { fileURLToPath } from 'url'; -import { promises as fs } from 'fs'; -import test from 'ava'; +import { expect, it } from 'vitest'; /** * format the contents of an astro file */ function format(contents: string, options: prettier.Options = {}): string { - try { - return prettier.format(contents, { - parser: 'astro', - plugins: [fileURLToPath(new URL('../', import.meta.url).toString())], - ...options, - }); - } catch (e) { - if (e instanceof Error) { - throw e; - } - if (typeof e === 'string') { - throw new Error(e); - } - } - return ''; + try { + return prettier.format(contents, { + parser: 'astro', + plugins: [fileURLToPath(new URL('../', import.meta.url).toString())], + ...options, + }); + } catch (e) { + if (e instanceof Error) { + throw e; + } + if (typeof e === 'string') { + throw new Error(e); + } + } + return ''; } function markdownFormat(contents: string, options: prettier.Options = {}): string { - try { - return prettier.format(contents, { - parser: 'markdown', - plugins: [fileURLToPath(new URL('../', import.meta.url).toString())], - ...options, - }); - } catch (e) { - if (e instanceof Error) { - throw e; - } - if (typeof e === 'string') { - throw new Error(e); - } - } - return ''; -} - -async function readFile(path: string) { - const res = await fs.readFile(fileURLToPath(new URL(`./fixtures${path}`, import.meta.url).toString())); - return res.toString().replace(/\r\n/g, '\n'); + try { + return prettier.format(contents, { + parser: 'markdown', + plugins: [fileURLToPath(new URL('../', import.meta.url).toString())], + ...options, + }); + } catch (e) { + if (e instanceof Error) { + throw e; + } + if (typeof e === 'string') { + throw new Error(e); + } + } + return ''; } /** - * Utility to get `[src, out]` files + * Utility to get `[input, output]` files */ -async function getFiles(name: string) { - const [src, out] = await Promise.all([readFile(`/${name}/input.astro`), readFile(`/${name}/output.astro`)]); - return [src, out]; -} - -async function getOptions(name: string) { - let options: object; - try { - options = JSON.parse(await readFile(`/${name}/options.json`)); - } catch (e) { - options = {}; - } - return options; +function getFiles(file: any, path: string, isMarkdown = false) { + const ext = isMarkdown ? 'md' : 'astro'; + let input: string = file[`/test/fixtures/${path}/input.${ext}`]; + let output: string = file[`/test/fixtures/${path}/output.${ext}`]; + // workaround: normalize end of lines to pass windows ci + if (input) input = input.replace(/(\r\n|\r)/gm, '\n'); + if (output) output = output.replace(/(\r\n|\r)/gm, '\n'); + return { input, output }; } -async function getMarkdownFiles(name: string) { - const [src, out] = await Promise.all([readFile(`/${name}/input.md`), readFile(`/${name}/output.md`)]); - return [src, out]; +function getOptions(files: any, path: string) { + let opts: object; + try { + opts = JSON.parse(files[`/test/fixtures/${path}/options.json`]); + } catch (e) { + opts = {}; + } + return opts; } /** - * Macro for testing fixtures + * @param {string} name Test name. + * @param {any} files Files from import.meta.glob. + * @param {string} path Fixture path. + * @param {boolean} isMarkdown For markdown files */ -export const Prettier = test.macro({ - async exec(t, name: string) { - const [src, out] = await getFiles(name); - t.not(src, out, 'Unformated file and formated file are the same'); +export function test(name: string, files: any, path: string, isMarkdown = false) { + it(`${path}\n${name}`, async () => { + const { input, output } = getFiles(files, path, isMarkdown); - const options = await getOptions(name); + expect(input, 'Missing input file').to.not.be.undefined; + expect(output, 'Missing output file').to.not.be.undefined; - const formatted = format(src, options); - t.is(formatted, out, 'Incorrect formating'); - // test that our formatting is idempotent - const formattedTwice = format(formatted, options); - t.is(formatted, formattedTwice, 'Formatting is not idempotent'); - }, - /** - * Macro title function for nice formatting - */ - title(title, name) { - return `${title}: - - input: fixtures/in/${name}.astro - - output: fixtures/out/${name}.astro`; - }, -}); + const formatFile = isMarkdown ? markdownFormat : format; -export const PrettierUnaltered = test.macro({ - async exec(t, name: string) { - const [src, out] = await getFiles(name); - t.is(src, out, 'Unformated file and formated file are not the same'); // the output should be unchanged + const opts = getOptions(files, path); - const options = await getOptions(name); + const formatted = formatFile(input, opts); + expect(formatted, 'Incorrect formating').toBe(output); - const formatted = format(src, options); - t.is(formatted, out, 'Incorrect formating'); - // test that our formatting is idempotent - const formattedTwice = format(formatted); - t.is(formatted, formattedTwice, 'Formatting is not idempotent'); - }, - title(title, name) { - return `${title}: - - input: fixtures/in/${name}.astro - - output: fixtures/out/${name}.astro`; - }, -}); - -export const PrettierMarkdown = test.macro({ - async exec(t, name: string) { - const [src, out] = await getMarkdownFiles(name); - t.not(src, out, 'Unformated file and formated file are the same'); - - const options = await getOptions(name); - - const formatted = markdownFormat(src, options); - t.is(formatted, out, 'Incorrect formating'); - // test that our formatting is idempotent - const formattedTwice = markdownFormat(formatted, options); - t.is(formatted, formattedTwice, 'Formatting is not idempotent'); - }, - title(title, name) { - return `${title}: - - input: fixtures/in/${name}.astro - - output: fixtures/out/${name}.astro`; - }, -}); + // test that our formatting is idempotent + const formattedTwice = formatFile(formatted, opts); + expect(formatted === formattedTwice, 'Formatting is not idempotent').toBe(true); + }); +} diff --git a/test/tests/basic.test.ts b/test/tests/basic.test.ts new file mode 100644 index 0000000..4736052 --- /dev/null +++ b/test/tests/basic.test.ts @@ -0,0 +1,13 @@ +import { test } from '../test-utils'; + +const files = import.meta.glob('/test/fixtures/basic/*/*', { + assert: { type: 'raw' }, +}); + +test('Can format a basic astro file', files, 'basic/basic-html'); + +test('Can format an Astro file with a single style element', files, 'basic/single-style-element'); + +test('Can format a basic astro only text', files, 'basic/simple-text'); + +test('Can format html comments', files, 'basic/html-comment'); diff --git a/test/tests/markdown.test.ts b/test/tests/markdown.test.ts new file mode 100644 index 0000000..985ba7d --- /dev/null +++ b/test/tests/markdown.test.ts @@ -0,0 +1,29 @@ +import { test } from '../test-utils'; + +const files = import.meta.glob('/test/fixtures/markdown/*/*', { + assert: { type: 'raw' }, +}); + +// *** MARKDOWN *** +// test( +// 'can format an Astro file containing an Astro file embedded in a codeblock', +// files, +// 'markdown/embedded-in-markdown', +// { mode: 'markdown' } +// ); + +test( + 'Can format an Astro file with a codespan inside ', + files, + 'markdown/with-codespans' +); + +test( + 'Can format the content of a markdown component as markdown', + files, + 'markdown/markdown-component-content' +); + +// test.todo("Don't escape '*' inside markdown"); + +// test.todo('Format jsx inside markdown'); diff --git a/test/tests/options.test.ts b/test/tests/options.test.ts new file mode 100644 index 0000000..9171750 --- /dev/null +++ b/test/tests/options.test.ts @@ -0,0 +1,182 @@ +import { test } from '../test-utils'; + +const files = import.meta.glob('/test/fixtures/options/*/*', { + assert: { type: 'raw' }, +}); + +// https://prettier.io/docs/en/options.html#print-width +// TODO: MAYBE NOT WORKING? +test( + 'Can format an Astro file with prettier "printWidth" option', + files, + 'options/option-print-width' +); + +// https://prettier.io/docs/en/options.html#tab-width +test('Can format an Astro file with prettier "tabWidth" option', files, 'options/option-tab-width'); + +// https://prettier.io/docs/en/options.html#tabs +test( + 'Can format an Astro file with prettier "useTabs: true" option', + files, + 'options/option-use-tabs-true' +); + +// https://prettier.io/docs/en/options.html#tabs +test( + 'Can format an Astro file with prettier "useTabs: false" option', + files, + 'options/option-use-tabs-false' +); + +// https://prettier.io/docs/en/options.html#semicolons +test( + 'Can format an Astro file with prettier "semi: true" option', + files, + 'options/option-semicolon-true' +); + +// https://prettier.io/docs/en/options.html#semicolons +test( + 'Can format an Astro file with prettier "semi: false" option', + files, + 'options/option-semicolon-false' +); + +// // https://prettier.io/docs/en/options.html#quotes +// test('Can format an Astro file with prettier "singleQuote: false" option', 'option-single-quote-false'); + +// // https://prettier.io/docs/en/options.html#quotes +// test('Can format an Astro file with prettier "singleQuote: true" option', 'option-single-quote-true'); + +// https://prettier.io/docs/en/options.html#quote-props +test( + 'Can format an Astro file with prettier "quoteProps: as-needed" option', + files, + 'options/option-quote-props-as-needed' +); + +// https://prettier.io/docs/en/options.html#quote-props +test( + 'Can format an Astro file with prettier "quoteProps: consistent" option', + files, + 'options/option-quote-props-consistent' +); + +// https://prettier.io/docs/en/options.html#quote-props +test( + 'Can format an Astro file with prettier "quoteProps: preserve" option', + files, + 'options/option-quote-props-preserve' +); + +// // https://prettier.io/docs/en/options.html#jsx-quotes +// test('Can format an Astro file with prettier "jsxSingleQuote: false" option', 'option-jsx-single-quote-false'); + +// // https://prettier.io/docs/en/options.html#jsx-quotes +// test('Can format an Astro file with prettier "jsxSingleQuote: true" option', 'option-jsx-single-quote-true'); + +// https://prettier.io/docs/en/options.html#trailing-commas +test( + 'Can format an Astro file with prettier "trailingComma: es5" option', + files, + 'options/option-trailing-comma-es5' +); + +// https://prettier.io/docs/en/options.html#trailing-commas +test( + 'Can format an Astro file with prettier "trailingComma: none" option', + files, + 'options/option-trailing-comma-none' +); + +// https://prettier.io/docs/en/options.html#bracket-spacing +test( + 'Can format an Astro file with prettier "bracketSpacing: true" option', + files, + 'options/option-bracket-spacing-true' +); + +// https://prettier.io/docs/en/options.html#bracket-spacing +test( + 'Can format an Astro file with prettier "bracketSpacing: false" option', + files, + 'options/option-bracket-spacing-false' +); + +// https://prettier.io/docs/en/options.html#bracket-line +test( + 'Can format an Astro file with prettier "bracketSameLine: false" option', + files, + 'options/option-bracket-same-line-false' +); + +// https://prettier.io/docs/en/options.html#bracket-line +test( + 'Can format an Astro file with prettier "bracketSameLine: true" option', + files, + 'options/option-bracket-same-line-true' +); + +// https://prettier.io/docs/en/options.html#arrow-function-parentheses +test( + 'Can format an Astro file with prettier "arrowParens: always" option', + files, + 'options/option-arrow-parens-always' +); + +// https://prettier.io/docs/en/options.html#arrow-function-parentheses +test( + 'Can format an Astro file with prettier "arrowParens: avoid" option', + files, + 'options/option-arrow-parens-avoid' +); + +// https://prettier.io/docs/en/options.html#prose-wrap +test( + 'Can format an Astro file with prettier "proseWrap: preserve" option', + files, + 'options/option-prose-wrap-preserve', + true +); + +// https://prettier.io/docs/en/options.html#prose-wrap +test( + 'Can format an Astro file with prettier "proseWrap: always" option', + files, + 'options/option-prose-wrap-always', + true +); + +// https://prettier.io/docs/en/options.html#prose-wrap +test( + 'Can format an Astro file with prettier "proseWrap: never" option', + files, + 'options/option-prose-wrap-never', + true +); + +// // https://prettier.io/docs/en/options.html#html-whitespace-sensitivity +// test('Can format an Astro file with prettier "htmlWhitespaceSensitivity: css" option', 'option-html-whitespace-sensitivity-css'); + +// // https://prettier.io/docs/en/options.html#html-whitespace-sensitivity +// test('Can format an Astro file with prettier "htmlWhitespaceSensitivity: strict" option', 'option-html-whitespace-sensitivity-strict'); + +// https://prettier.io/docs/en/options.html#html-whitespace-sensitivity +test( + 'Can format an Astro file with prettier "htmlWhitespaceSensitivity: ignore" option', + files, + 'options/option-html-whitespace-sensitivity-ignore' +); + +// // astro option: astroSortOrder +// test('Can format an Astro file with prettier "astroSortOrder: markup | styles" option', 'option-astro-sort-order-markup-styles'); + +// // astro option: astroSortOrder +// test('Can format an Astro file with prettier "astroSortOrder: styles | markup" option', 'option-astro-sort-order-styles-markup'); + +// // astro option: astroAllowShorthand +// test('Can format an Astro file with prettier "astroAllowShorthand: true" option', 'option-astro-allow-shorthand-true'); + +// // astro option: astroAllowShorthand +// test('Can format an Astro file with prettier "astroAllowShorthand: false" option', 'option-astro-allow-shorthand-false'); diff --git a/test/tests/other.test.ts b/test/tests/other.test.ts new file mode 100644 index 0000000..cb4fadf --- /dev/null +++ b/test/tests/other.test.ts @@ -0,0 +1,67 @@ +import { test } from '../test-utils'; + +const files = import.meta.glob('/test/fixtures/other/*/*', { + assert: { type: 'raw' }, +}); + +test('Can format an Astro file with frontmatter', files, 'other/frontmatter'); + +test('Can format an Astro file with embedded JSX expressions', files, 'other/embedded-expr'); + +test( + 'Can format an Astro file with a `` + embedded JSX expressions', + files, + 'other/doctype-with-embedded-expr' +); + +// // note(drew): this should be fixed in new Parser. And as this is an HTML4 / deprecated / extreme edge case, probably fine to ignore? +// test.failing('Can format an Astro file with `` with extraneous attributes', Prettier, 'doctype-with-extra-attributes'); + +test('Can format an Astro file with fragments', files, 'other/fragment'); + +test( + 'Can format an Astro file with a JSX expression in an attribute', + files, + 'other/attribute-with-embedded-expr' +); + +test( + 'Can format an Astro file with a JSX expression and an HTML Comment', + files, + 'other/expr-and-html-comment' +); + +// test.failing('an Astro file with an invalidly unclosed tag is still formatted', Prettier, 'unclosed-tag'); + +test( + 'Can format an Astro file with components that are the uppercase version of html elements', + files, + 'other/preserve-tag-case' +); + +test('Autocloses open tags.', files, 'other/autocloses-open-tags'); + +test('Can format an Astro file with a script tag inside it', files, 'other/with-script'); + +// // Supports various prettier ignore comments +// test('Can format an Astro file with a HTML style prettier ignore comment: https://prettier.io/docs/en/ignore.html', Prettier, 'prettier-ignore-html'); + +test( + 'Can format an Astro file with a JS style prettier ignore comment: https://prettier.io/docs/en/ignore.html', + files, + 'other/prettier-ignore-js' +); + +// // note(drew): this _may_ be covered under the 'prettier-ignore-html' test. But if any bugs arise, let’s add more tests! +// test.todo("properly follow prettier' advice on formatting comments"); + +// // note(drew): I think this is a function of Astro’s parser, not Prettier. We’ll have to handle helpful error messages there! +// test.todo('test whether invalid files provide helpful support messages / still try to be parsed by prettier?'); + +test('Format spread operator', files, 'other/spread-operator'); + +test('Can format nested comment', files, 'other/nested-comment'); + +test('Format binary expressions', files, 'other/binary-expression'); + +test('Format directives', files, 'other/directive'); diff --git a/test/tests/styles.test.ts b/test/tests/styles.test.ts new file mode 100644 index 0000000..4f9049c --- /dev/null +++ b/test/tests/styles.test.ts @@ -0,0 +1,38 @@ +import { test } from '../test-utils'; + +const files = import.meta.glob('/test/fixtures/styles/*/*', { + assert: { type: 'raw' }, +}); + +test('Can format a basic Astro file with styles', files, 'styles/with-styles'); + +test( + 'Can format an Astro file with attributes in the