├── .github └── workflows │ └── nodejs.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __snapshots__ └── parser.test.js.snap ├── package.json ├── parser.js └── parser.test.js /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest] 11 | node-version: [10.x, 12.x] 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - name: yarn install and test 22 | run: | 23 | yarn install --frozen-lockfile 24 | yarn test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [1.1.0](https://github.com/observablehq/prettier-react-tachyons/compare/v1.0.1...v1.1.0) (2018-03-27) 7 | 8 | 9 | ### Features 10 | 11 | * Support v-base vertical-alignment option ([bafc544](https://github.com/observablehq/prettier-react-tachyons/commit/bafc544)) 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Observable, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## @observablehq/prettier-react-tachyons 2 | 3 | ![Node CI](https://github.com/observablehq/prettier-react-tachyons/workflows/Node%20CI/badge.svg) 4 | 5 | A [prettier](https://prettier.io/) custom [parser plugin](https://prettier.io/docs/en/api.html#custom-parser-api) 6 | for users of React and tachyons. It adds an extra prettier transform for `className=` 7 | attributes on JSX elements, that takes care of the following: 8 | 9 | - Eliminating redundant classes 10 | - Sorting classes in a predictable order 11 | 12 | ### How do you use it? 13 | 14 | This is admittedly a little bleeding-edge: prettier supports custom parsers, 15 | but not custom rules / reformatters, so this essentially augments prettier's 16 | `babylon`-based parser for JavaScript. You can configure prettier to point to it 17 | with the `parser` option. For instance, here's how we configure prettier in our 18 | `.prettierrc`: 19 | 20 | ```json 21 | { 22 | "parser": "./node_modules/@observablehq/prettier-react-tachyons" 23 | } 24 | ``` 25 | 26 | ### Example: 27 | 28 | in: 29 | 30 |
31 | 32 | out: 33 | 34 |
35 | -------------------------------------------------------------------------------- /__snapshots__/parser.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`reformatting examples 1`] = ` 4 | "
; 5 | " 6 | `; 7 | 8 | exports[`reformatting examples 2`] = ` 9 | "
; 10 | " 11 | `; 12 | 13 | exports[`reformatting examples 3`] = ` 14 | "
; 15 | " 16 | `; 17 | 18 | exports[`reformatting examples 4`] = ` 19 | "
; 20 | " 21 | `; 22 | 23 | exports[`reformatting examples 5`] = ` 24 | "
; 25 | " 26 | `; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@observablehq/prettier-react-tachyons", 3 | "version": "1.2.0", 4 | "main": "parser.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "jest": "^25.5.4", 8 | "prettier": "^2.0.5", 9 | "standard-version": "^7.1.0" 10 | }, 11 | "files": [], 12 | "dependencies": { 13 | "@babel/traverse": "^7.9.6" 14 | }, 15 | "scripts": { 16 | "test": "jest", 17 | "release": "standard-version" 18 | }, 19 | "engines": { 20 | "node": ">= 10" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /parser.js: -------------------------------------------------------------------------------- 1 | const shades = ["05", "10", "20", "30", "40", "50", "60", "70", "80", "90"]; 2 | const colors = [ 3 | "black", 4 | "near-black", 5 | "dark-gray", 6 | "mid-gray", 7 | "gray", 8 | "silver", 9 | "light-silver", 10 | "moon-gray", 11 | "light-gray", 12 | "near-white", 13 | "white", 14 | "dark-red", 15 | "red", 16 | "light-red", 17 | "orange", 18 | "gold", 19 | "yellow", 20 | "light-yellow", 21 | "purple", 22 | "light-purple", 23 | "dark-pink", 24 | "hot-pink", 25 | "pink", 26 | "light-pink", 27 | "dark-green", 28 | "green", 29 | "light-green", 30 | "navy", 31 | "dark-blue", 32 | "blue", 33 | "light-blue", 34 | "lightest-blue", 35 | "washed-blue", 36 | "washed-green", 37 | "washed-yellow", 38 | "washed-red" 39 | ] 40 | .concat(shades.map(s => `black-${s}`)) 41 | .concat(shades.map(s => `white-${s}`)); 42 | 43 | const directions = "hvatlbr".split(""); 44 | 45 | // display, position, dimension, measure, flex, padding, margin, negative margin, font 46 | // font size, font weight, line height, whitespace, color, opacity, background, 47 | // hover background, border, border color, 48 | // border radius 49 | let order = [ 50 | "(dn|di|db|dib|dit|dt|dtc|flex|inline-flex)", 51 | "(static|relative|absolute|fixed)", 52 | "(top|right|bottom|left)-\\d+", 53 | 54 | "measure", 55 | "measure-wide", 56 | "measure-narrow", 57 | "indent", 58 | "small-caps", 59 | "truncate", 60 | 61 | "w-?\\d+", 62 | "mw\\d+", 63 | "h\\d+", 64 | "center", 65 | "v-(base|mid|top|btm)", 66 | "(content|items|self|justify|order)", 67 | ...["l", "r", "n"].map(d => `f${d}`), 68 | ...directions.map(d => `p${d}\\d`), 69 | ...directions.map(d => `m${d}\\d`), 70 | ...directions.map(d => `n${d}\\d`), 71 | "(code|sans-serif|serif)", 72 | "f\\d", 73 | "fw\\d", 74 | "lh-w+", 75 | "(ws-normal|nowrap|pre)", 76 | "t[lrcj]", 77 | "(underline|no-underline|strike|ttu|ttc|ttl)", 78 | `(${colors.join("|")})`, 79 | `(${colors.map(c => `hover-${c}`).join("|")})`, 80 | "o\\d+", 81 | `(${colors.map(c => `bg-${c}`).join("|")})`, 82 | `(${colors.map(c => `hover-bg-${c}`).join("|")})`, 83 | ...directions.map(d => `b${d}$`), 84 | `(${colors.map(c => `b--${c}`).join("|")})`, 85 | "br[\\d-]" 86 | ].reduce((memo, re) => { 87 | return memo 88 | .concat( 89 | ["ns", "m", "l"].map(mq => { 90 | return new RegExp(`^${re}-${mq}$`); 91 | }) 92 | ) 93 | .concat(new RegExp(`^${re}$`)); 94 | }, []); 95 | 96 | function weight(c) { 97 | for (let i = 0; i < order.length; i++) { 98 | if (order[i].test(c)) return i; 99 | } 100 | return Infinity; 101 | } 102 | 103 | module.exports = function(text, { babel }) { 104 | const traverse = require("@babel/traverse").default; 105 | const ast = babel(text); 106 | 107 | traverse(ast, { 108 | JSXAttribute({ node }) { 109 | if ( 110 | node.name && 111 | node.name.name == "className" && 112 | node.value.type == "StringLiteral" 113 | ) { 114 | node.value.extra.raw = `"${Array.from( 115 | new Set(node.value.value.split(/\s+/).filter(Boolean)), 116 | (c, i) => [c, weight(c), i] 117 | ) 118 | .sort( 119 | (left, right) => 120 | left[1] !== right[1] ? left[1] - right[1] : left[2] - right[2] 121 | ) 122 | .map(c => c[0]) 123 | .join(" ")}"`; 124 | } 125 | } 126 | }); 127 | return ast; 128 | }; 129 | -------------------------------------------------------------------------------- /parser.test.js: -------------------------------------------------------------------------------- 1 | const prettier = require("prettier"); 2 | const parser = require("./parser"); 3 | 4 | test("reformatting examples", () => { 5 | expect( 6 | prettier.format(`
`, { 7 | parser 8 | }) 9 | ).toMatchSnapshot(); 10 | expect( 11 | prettier.format(`
`, { 12 | parser 13 | }) 14 | ).toMatchSnapshot(); 15 | expect( 16 | prettier.format( 17 | `
`, 18 | { 19 | parser 20 | } 21 | ) 22 | ).toMatchSnapshot(); 23 | expect( 24 | prettier.format(`
`, { 25 | parser 26 | }) 27 | ).toMatchSnapshot(); 28 | expect( 29 | prettier.format(`
`, { 30 | parser 31 | }) 32 | ).toMatchSnapshot(); 33 | }); 34 | --------------------------------------------------------------------------------