Skip to content

Commit

Permalink
feat: rule align
Browse files Browse the repository at this point in the history
  • Loading branch information
lzear committed Nov 4, 2023
1 parent 745d91e commit cdaedf3
Show file tree
Hide file tree
Showing 3 changed files with 277 additions and 0 deletions.
117 changes: 117 additions & 0 deletions docs/rules/align.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
title: center
description: Align text elegantly
---

<script setup lang="ts">
import CodeEditor from '../../.vitepress/theme/components/code-editor.vue';
import {ruleName, presetConfigs, initialText} from '../../src/sample-code/align.js';
</script>


> "Typography is two-dimensional architecture, based on experience and imagination, and guided by rules and
> readability." — Hermann Zapf
# Enforce elegant text alignment (`dont/align`)

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

## 📖 Rule details

<div style="text-align: center;">
While traditionalists might find the idea of center or right-aligned code unconventional, many innovations in software
development were once deemed unorthodox. It's important to keep an open mind, recognizing that seemingly aesthetic
choices can have deeper implications for cognition, creativity, and code readability. For developers accustomed to
right-aligned languages like Arabic or Hebrew, this alignment might feel more intuitive, bridging the gap between
natural linguistic processing and coding.
</div>

## 💡 Examples

::: code-group

<!-- prettier-ignore -->
```js [Center]
// ✅ Correct
if (a) {
b = c;
function foo(d) {
e = f;
}
}
```

<!-- prettier-ignore -->
```js [Right]
// ✅ Correct
if (a) {
b = c;
function foo(d) {
e = f;
}
}
```

<!-- prettier-ignore -->
```js [Left]
// ✅ Correct
if (a) {
b = c;
function foo(d) {
e = f;
}
}
```

<!-- prettier-ignore -->
```js [RTL Indent]
// ✅ Correct
if (a) {
b = c;
function foo(d) {
e = f;
}
}
```

<!-- prettier-ignore -->
```js [LTR Indent]
// ✅ Correct
if (a) {
b = c;
function foo(d) {
e = f;
}
}
```

:::

## ⚙️ Options

### side

<sub>(default: `'center'`)</sub>

- `left`
- `center`
- `rigth`
- `rtlIndent`
- `ltrIndent`


## 🔧 Config

```js
{ rules: { 'dont/align': [2, { side: 'center' }] } }
```

## 🔗 See also

- [justify2](/rules/justify2)

## 🧑‍💻 Demo

<CodeEditor :rule="ruleName" :text="initialText" :presetConfigs="presetConfigs" />
142 changes: 142 additions & 0 deletions rules/align.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import type { Difference } from 'prettier-linter-helpers'

import { Linter } from 'eslint'
import { builtinRules } from 'eslint/use-at-your-own-risk'
import { generateDifferences } from 'prettier-linter-helpers'

import type { RuleContext, RuleListener } from '../utils/eslint-types/Rule.js'

import { complete } from '../utils/complete.js'
import { createEslintRule } from '../utils/create-eslint-rule.js'
import { reportDifference } from '../utils/report-difference.js'

type MESSAGE_ID = 'delete' | 'insert' | 'replace'

const enum Side {
center = 'center',
left = 'left',
ltrIndent = 'ltrIndent',
right = 'right',
rtlIndent = 'rtlIndent',
}

type Options = [
Partial<{
side: Side
}>,
]

export const RULE_NAME = 'align'
type Context = RuleContext<MESSAGE_ID, Options>

export const moveIndentToEnd = (str: string) =>
str
.split('\n')
.map(line => {
const leadingSpaces = line.match(/^\s+/)?.[0] ?? ''
return line.trim() + leadingSpaces
})
.join('\n')

export default createEslintRule<Options, MESSAGE_ID>({
name: RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'enforce elegant text alignment',
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
side: {
enum: [
Side.center,
Side.right,
Side.left,
Side.rtlIndent,
Side.ltrIndent,
],
default: Side.center,
type: 'string',
},
},
},
],
messages: {
insert: 'Align `{{ side }}`. Insert `{{ insertText }}`',
delete: 'Align `{{ side }}`. Delete `{{ deleteText }}`',
replace:
'Align `{{ side }}`. Replace `{{ deleteText }}` with `{{ insertText }}`',
},
},
defaultOptions: [{ side: Side.center }],
create: (context: Context): RuleListener => {
const options = complete(context.options.at(0), {
side: Side.center,
})
const { sourceCode } = context
const { text } = sourceCode

const processLine = (
line: string,
maxLen: number,
shouldTrim: boolean,
side: Side,
) => {
const trimmed = shouldTrim ? line.trim() : line
if (side === Side.left) return trimmed
if (side === Side.right) return trimmed.padStart(maxLen)
return trimmed.padStart(Math.round((maxLen + trimmed.length) / 2))
}

const reportDiffs = (ugly: string, pretty: string) => {
if (ugly !== pretty) {
const differences: Difference[] = generateDifferences(ugly, pretty)
for (const difference of differences)
reportDifference(context, difference, { side: options.side })
}
}

const makeGoal = (code: string, shouldTrim: boolean, side: Side) => {
let maxLen = 0
const lll = code.split('\n')
for (const line of lll)
maxLen = Math.max(maxLen, (shouldTrim ? line.trim() : line).length)

return lll
.map(line => processLine(line, maxLen, shouldTrim, side))
.join('\n')
}
const processCode = (code: string, shouldTrim: boolean, side: Side) => {
const pretty = makeGoal(code, shouldTrim, side)
reportDiffs(code, pretty)
}

const processRtlIndent = (rtl: boolean) => {
const indent = builtinRules.get('indent')!
const linter = new Linter()
linter.defineRule('indent', indent)
const { output } = linter.verifyAndFix(
// lines.map(line => line.trim()).join('\n'),
text,
{ rules: { indent: [2, 2] } },
{ filename: 'foo.ts' },
)

const pretty = rtl
? makeGoal(moveIndentToEnd(output), false, Side.right)
: output
reportDiffs(text, pretty)
}

return {
Program: () => {
if (options.side === Side.rtlIndent) return processRtlIndent(true)
if (options.side === Side.ltrIndent) return processRtlIndent(false)
return processCode(text, true, options.side)
},
}
},
})
18 changes: 18 additions & 0 deletions src/sample-code/align.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { PresetConfig } from './presets.js'

export const ruleName = 'align'

export const presetConfigs = [
{ config: { side: 'center' }, name: 'Center' },
{ config: { side: 'right' }, name: 'Right' },
{ config: { side: 'left' }, name: 'Left' },
{ config: { side: 'rtlIndent' }, name: 'rtlIndent' },
{ config: { side: 'ltrIndent' }, name: 'ltrIndent' },
] satisfies PresetConfig[]

export const initialText = `if (a) {
b = c;
function foo(d) {
e = f;
}
}`

0 comments on commit cdaedf3

Please sign in to comment.