Skip to content

Commit

Permalink
feat: checks for styled components
Browse files Browse the repository at this point in the history
  • Loading branch information
zhcalvin committed Mar 18, 2022
1 parent c236e52 commit 818faef
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 28 deletions.
13 changes: 10 additions & 3 deletions README-zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ npm install -g css-checker-kit
css-checker
```

- (Alpha 功能:查找 js/jsx/ts/tsx/html 代码中未引用的 class): `css-checker -path=[YOUR_PROJECT_PATH] -unused`
- (Beta: Styled Components 检查,如您使用 Styled Components,可开启): `css-checker -styled`

![DEMO](https://assets.ruilisi.com/css-checker-demo.gif)

Expand All @@ -68,17 +68,24 @@ css-checker
- 此项目中还提供了一个名为“css-checker.example.yaml”的示例 yaml 文件,您可以将其命名为“css-checker.yaml”使用。
- 您可以使用 `-config=YOUR_CONFIG_FILE_PATH`来指定您的配置文件。

#### 高级功能

- 仅检查 styled components (忽略 CSS 文件): `css-checker -css=false -styled`
- (Alpha 功能:查找 js/jsx/ts/tsx/html 代码中未引用的 class): `css-checker -path=[YOUR_PROJECT_PATH] -unused`

#### 基本命令

- `colors`: 是否检查颜色(默认为 true)
- `css`: 是否检查 CSS 文件 (默认 true)
- `config`:设置配置文件路径 (string, default './css-checker.yaml') (string, default '')
- `ignores`: 输出被忽略的路径和文件(e.g. node_modules,\*.example.css)
- `length-threshold`: 被视为长脚本行的单个样式值(不包括键)的最小长度(默认 20)
- `long-line`: 是否检查重复的长脚本行(默认为 true)
- `path`: 文件路径的字符串,默认为当前文件夹(默认为".")
- `sections`: 是否检查部分重复(默认为 true)
- `sim`: 是否检查相似的 css classes(默认 true)
- `sim-threshold`:相似性检查的阈值($\geq20%$ && $\lt100%$)(int 类型,如80表示80%,请注意此为相似性检查控制,完全相同的css classes检查由 `sections`控制)(默认为 80)
- `sim-threshold`:相似性检查的阈值($\geq20%$ && $\lt100%$)(int 类型,如 80 表示 80%,请注意此为相似性检查控制,完全相同的 css classes 检查由 `sections`控制)(默认为 80)
- `styled`: 是否检查 Styled Components (默认 false)
- `unrestricted`:搜索所有文件(gitignore)
- `unused`:是否检查未使用的 classes(Beta)
- `version`:打印当前版本并退出
Expand All @@ -95,7 +102,7 @@ css-checker

#### 相似性检查

检查 classes 之间的相似性 ($\geq(sim-threshold)$ && $\lt100$),该功能会标注相似`classes`之前的diff (默认开启)。
检查 classes 之间的相似性 ($\geq(sim-threshold)$ && $\lt100$),该功能会标注相似`classes`之前的 diff (默认开启)。

- $sim-threshold$:使用 `-sim-threshold=` 参数或在配置 yaml 文件中设置 `sim-threshold:`,默认 80,最少 20。

Expand Down
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@

## Purpose

`css-checker` checks your css styles for duplications and find the diff among `css classes` with high similarity in seconds. It is designed to avoid redundant or similar css between files and to work well for both local development, and for automation like CI.
`css-checker` checks your css styles for duplications and find the diff among `css classes` with high similarity in seconds. It is designed to avoid redundant or `similar css` and `styled components` between files and to work well for both local development, and for automation like CI.

Colors check, long scripts, unused CSS classes warning of css are also supported by default. This project is provided by [Xiemala Team](`https://xiemala.com`), it helps in remove hundreds of similar css classes for developers in this project.
Styled components check, Colors check, long scripts, unused CSS classes warning of css are also supported by default. This project is provided by [Xiemala Team](`https://xiemala.com`), it helps in remove hundreds of similar css classes for developers in this project.

## Install

Expand Down Expand Up @@ -45,13 +45,15 @@ npm install -g css-checker-kit
css-checker
```

- (Alpha Feature: Find classes that not referred by your js/jsx/ts/tsx/html code): `css-checker -unused`
- (Beta Feature: styled components check): `css-checker -styled`

![DEMO](https://assets.ruilisi.com/css-checker-demo.gif)

(Check and show the diff among similar classes (>=80%). Colors, long scripts that used more then once will also be pointed out by default. Check `css-checker -help` for customized options.)

Colors with `rgb/rgba/hsl/hsla/hex` will be converted to rbga and compared together.
- Colors with `rgb/rgba/hsl/hsla/hex` will be converted to rbga and compared together.

- (Alpha Feature: Find classes that not referred by your code): `css-checker -unused`

#### Run with path

Expand All @@ -68,22 +70,28 @@ Colors with `rgb/rgba/hsl/hsla/hex` will be converted to rbga and compared toget
- A sample yaml file named 'css-checker.example.yaml' is also providied in this project, move it to your project path with the name 'css-checker.yaml' and it will work.
- To specify your config file, use `-config=YOUR_CONFIG_FILE_PATH`.

#### Advanced Features

- Run with styled components check only (without checks for css): `css-checker -css=false -styled`
- Find classes that not referred by your code: `css-checker -unused` (Alpha)

#### Basic commands

- `colors`: whether to check colors (default true)
- `css`: whether to check css files (default true as you expected)
- `config`: set configuration file path (string, default './css-checker.yaml')
- `ignores`: paths and files to be ignored (e.g. node_modules,*.example.css) (string, default '')
- `ignores`: paths and files to be ignored (e.g. node_modules,\*.example.css) (string, default '')
- `length-threshold`: Min length of a single style value (no including the key) that to be considered as long script line (default 20)
- `long-line`: whether to check duplicated long script lines (default true)
- `path`: set path to files, default to be current folder (default ".")
- `sections`: whether to check css class duplications (default true)
- `sim`: whether to check similar css classes (default true)
- `sim-threshold`: Threshold for Similarity Check ($\geq20$ && $\lt100$) (int only, e.g. 80 for 80%, checks for identical classes defined in `sections`) (default 80)
- `styled`: checks for styled components (default false)
- `unrestricted`: search all files (gitignore)
- `unused`: whether to check unused classes (Beta)
- `version`: prints current version and exits


#### Outputs:

![image.png](https://assets.ruilisi.com/t=yDNXWrmyg+V6mUzCAG7A==)
Expand Down
12 changes: 10 additions & 2 deletions duplication_checker_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -15,11 +14,20 @@ func TestDuplicatedScriptsCheck(t *testing.T) { // same for colors and long scri
longScriptList = append(longScriptList, longs...)
colorScriptList = append(colorScriptList, colors...)
}
fmt.Println(colorScriptList)
summaryList := DupScriptsChecker(colorScriptList)
assert.Equal(t, len(summaryList), 1)
assert.Equal(t, len(summaryList[0].scripts), 3)

summaryList = DupScriptsChecker(longScriptList)
assert.Equal(t, len(summaryList), 1)
}

func TestDuplicatedStyledComponentsCheck(t *testing.T) {
path := "tests/sample.ts"
longs, colors := SectionsParse(path, 80)
summaryList := DupScriptsChecker(colors)
assert.Equal(t, len(summaryList), 0)

summaryList = DupScriptsChecker(longs)
assert.Equal(t, len(summaryList), 1)
}
40 changes: 34 additions & 6 deletions file_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"math"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
Expand All @@ -13,25 +14,52 @@ import (

// SectionsParse returns LongScripts and ColorScripts
func SectionsParse(filePath string, sim int) ([]Script, []Script) {
// styledHeadLineReg regexp string to match the head lines of styled components
styledHeadLinesReg := regexp.MustCompile(`(const)\s*(\S+)\s..(styled).(.*?)` + "`")
// styledComponentsReg regexp to match the styled components
styledComponentsReg := regexp.MustCompile(`(const)\s*(\S+)\s..(styled).(.*?)` + "`" + `([` + "^`" + `]*)` + "`")

dat, err := os.ReadFile(filePath)
if err != nil {
return []Script{}, []Script{}
}
stylesheet, err := parser.Parse(string(dat))
if err != nil {
return []Script{}, []Script{}
styleString := ""
if filepath.Ext(filePath) == ".css" {
if stylesheet, err := parser.Parse(string(dat)); err == nil {
styleString = strings.Replace(stylesheet.String(), "\r", "", -1)
} else {
return []Script{}, []Script{}
}
} else {
// try extract styled components
matches := styledComponentsReg.FindAllStringSubmatch(string(dat), -1)
if len(matches) == 0 {
return []Script{}, []Script{}
}
for _, match := range matches {
if len(match) > 0 {
styleString += match[0] + "\n"
}
}
}
styleString := strings.Replace(stylesheet.String(), "\r", "", -1)

styleSection := StyleSection{name: "", value: []string{}, filePath: ""}
longScriptList := []Script{}
colorScriptList := []Script{}
colorReg := regexp.MustCompile(`#[A-Fa-f0-9]{3,6}|rgba\([0-9,%/ ]*\)|hsla\([0-9,%/ ]*\)|rgb\([0-9,%/ ]*\)|hsl\([0-9,%/ ]*\)`)
for _, sub := range strings.Split(styleString, "\n") {
if strings.HasSuffix(sub, "{") {
if strings.HasSuffix(sub, "{") { // css class starts
styleSection.name = strings.Replace(sub, "{", "", -1)
styleSection.filePath = filePath
} else if strings.Contains(sub, "}") {
} else if styledHeadLinesReg.MatchString(sub) { // styled component starts
splits := strings.Split(sub, " ")
if len(splits) > 1 {
styleSection.name = splits[1]
} else {
styleSection.name = sub
}
styleSection.filePath = filePath
} else if strings.Contains(sub, "}") || strings.HasSuffix(sub, "`") { // css class or styled component ends
if len(styleSection.value) > 0 {
sort.Strings(styleSection.value)
styleSection.valueHash = hash(strings.Join(styleSection.value, ""))
Expand Down
8 changes: 8 additions & 0 deletions file_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,11 @@ func TestClassSectionsProcessor(t *testing.T) {
assert.Equal(t, len(styleList), sectionsCounts[index])
}
}

func TestStyledComponentsSectionParse(t *testing.T) {
file := "tests/sample.ts"
styleList = []StyleSection{}
longScriptList, colorScriptList = SectionsParse(file, 80)
assert.Equal(t, len(longScriptList), 2)
assert.Equal(t, len(styleList), 2)
}
3 changes: 3 additions & 0 deletions file_walker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ func TestFileWalker(t *testing.T) {
files, err := WalkMatch("tests", WalkMatchOptions{patterns: []string{"*.css"}})
assert.NoError(t, err)
assert.Equal(t, len(files), 2)
files, err = WalkMatch("tests", WalkMatchOptions{patterns: []string{"*.css", "*.ts"}})
assert.NoError(t, err)
assert.Equal(t, len(files), 3)
}
14 changes: 13 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ const (
type Params struct {
Version bool `yaml:"version"`
ColorsCheck bool `yaml:"colors"`
CSS bool `yaml:"css"`
SectionsCheck bool `yaml:"sections"`
SimilarityCheck bool `yaml:"sim"`
SimilarityThreshold int `yaml:"sim-threshold"`
StyledComponents bool `yaml:"styled"`
LongScriptsCheck bool `yaml:"long-line"`
Path string `yaml:"path"`
LongScriptLength int `yaml:"length-threshold"`
Expand All @@ -52,6 +54,7 @@ var params = Params{
ColorsCheck: true,
SectionsCheck: true,
LongScriptsCheck: true,
CSS: true,
Path: ".",
LongScriptLength: 20,
Ignores: []string{},
Expand Down Expand Up @@ -195,13 +198,15 @@ func getConf(conf *Params, path string) (bool, error) {
func ParamsParse() {
ignorePathsString := ""
flag.BoolVar(&params.ColorsCheck, "colors", true, "whether to check colors")
flag.BoolVar(&params.CSS, "css", true, "whether to check css files")
flag.StringVar(&ignorePathsString, "ignores", "", "paths and files to be ignored (e.g. node_modules,*.example.css)")
flag.IntVar(&params.LongScriptLength, "length-threshold", 20, "Min length of a single style value (no including the key) that to be considered as long script line")
flag.BoolVar(&params.LongScriptsCheck, "long-line", true, "whether to check duplicated long script lines")
flag.StringVar(&params.Path, "path", ".", "set path to files, default to be current folder")
flag.BoolVar(&params.SectionsCheck, "sections", true, "whether to check css class duplications")
flag.BoolVar(&params.SimilarityCheck, "sim", true, "whether to check similar css classes")
flag.IntVar(&params.SimilarityThreshold, "sim-threshold", 80, "Threshold for Similarity Check (int only, >=20 && < 100, e.g. 80 for 80%)")
flag.BoolVar(&params.StyledComponents, "styled", false, "checks for styled components")
flag.BoolVar(&params.Unrestricted, "unrestricted", false, "search all files (gitignore)")
flag.BoolVar(&params.Unused, "unused", false, "whether to check unused classes (Beta)")
flag.BoolVar(&params.Version, "version", false, "prints current version and exits")
Expand Down Expand Up @@ -246,7 +251,14 @@ func main() {
}

// File Walk Starts
files, err := WalkMatch(params.Path, WalkMatchOptions{patterns: []string{"*.css"}, ignores: params.Ignores, unrestricted: params.Unrestricted})
patternsToCheck := []string{""}
if params.StyledComponents {
patternsToCheck = []string{"*.js", "*.jsx", "*.ts", "*.tsx"}
}
if params.CSS {
patternsToCheck = append(patternsToCheck, "*.css")
}
files, err := WalkMatch(params.Path, WalkMatchOptions{patterns: patternsToCheck, ignores: params.Ignores, unrestricted: params.Unrestricted})
if err != nil {
fmt.Printf(ErrorColor, fmt.Sprintf("No css files found at given path: %s", params.Path))
return
Expand Down
19 changes: 19 additions & 0 deletions tests/sample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const Button = styled.button`
color: palevioletred;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;

// ops another button followed

const DuplicatedButton = styled.button`
color: palevioletred;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
20 changes: 10 additions & 10 deletions warnings.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import (
// SimilarSectionsWarning prints warnings for similar sections
func SimilarSectionsWarning(similaritySummarys []SimilaritySummary, sim int) {
if len(similaritySummarys) > 0 {
fmt.Printf(WarningColor, fmt.Sprintf("\n%d similar css classes found as follow (%d%% <= sim < 100%%)\n.\n", len(similaritySummarys), sim))
fmt.Printf(WarningColor, fmt.Sprintf("\n%d similar classes found as follow (%d%% <= sim < 100%%)\n.\n", len(similaritySummarys), sim))
for index, summary := range similaritySummarys {
fmt.Printf(WarningColor, fmt.Sprintf("(%d) ", index))
fmt.Printf(ErrorColor, fmt.Sprintf("Sections share %d per cent similarity:\n", summary.similarity))
for _, section := range summary.sections {
fmt.Printf(WarningColor, fmt.Sprintf("Css in: %s << %s\n", section.name, section.filePath))
fmt.Printf(WarningColor, fmt.Sprintf("%s << %s\n", section.name, section.filePath))
fmt.Printf(DebugColor, "\n{\n")
for _, line := range section.value {
if strings.Contains(strings.Join(summary.duplicatedScripts, "\n"), line) {
Expand All @@ -30,23 +30,23 @@ func SimilarSectionsWarning(similaritySummarys []SimilaritySummary, sim int) {
fmt.Printf(WarningColor, fmt.Sprintf("For above classes, %s stands for duplicated lines\n\n\n", fmt.Sprintf(DebugColor, "Cyan Color")))
} else {
fmt.Printf(DebugColor, "√\t")
fmt.Printf(InfoColor, fmt.Sprintf("No similar css class found (%d%% <= sim < 100%%)\n", sim))
fmt.Printf(InfoColor, fmt.Sprintf("No similar class found (%d%% <= sim < 100%%)\n", sim))
}
}

// StyleSectionsWarning prints warnings for style sections
func StyleSectionsWarning(dupStyleSections []SectionSummary) {
if len(dupStyleSections) > 0 {
fmt.Printf(WarningColor, fmt.Sprintf("\n%d duplicated css classes found as follow.\n", len(dupStyleSections)))
fmt.Printf(WarningColor, fmt.Sprintf("\n%d duplicated classes found as follow.\n", len(dupStyleSections)))
for index, longScript := range dupStyleSections {
fmt.Printf(WarningColor, fmt.Sprintf("(%d) ", index))
fmt.Printf(ErrorColor, fmt.Sprintf("Same class content found in %d places:\n", longScript.count))
for _, name := range longScript.names {
fmt.Printf("\t %s\n", name)
}
fmt.Printf(DebugColor, fmt.Sprintf("Css content:\n{\n%s\n}\n\n", longScript.value))
fmt.Printf(DebugColor, fmt.Sprintf("Content:\n{\n%s\n}\n\n", longScript.value))
}
fmt.Printf(WarningColor, fmt.Sprintf("\nThe content of %d duplicated css content shall be reused.\n", len(dupStyleSections)))
fmt.Printf(WarningColor, fmt.Sprintf("\nThe content of %d duplicated contents shall be reused.\n", len(dupStyleSections)))
} else {
fmt.Printf(DebugColor, "√\t")
fmt.Printf(InfoColor, "No duplicated un-variabled color script found\n")
Expand Down Expand Up @@ -82,8 +82,8 @@ func ColorScriptsWarning(dupLongScripts []ScriptSummary) {
// LongScriptsWarning prints warnings for unvariabled css long lines that used more than once
func LongScriptsWarning(dupLongScripts []ScriptSummary) {
if len(dupLongScripts) > 0 {
fmt.Printf(WarningColor, fmt.Sprintf("\nOps %d duplicated css long scripts found as follow.\n", len(dupLongScripts)))
fmt.Println("(Duplicated long css scripts are recommanded to be extracted to variables)")
fmt.Printf(WarningColor, fmt.Sprintf("\nOps %d duplicated long line found as follow.\n", len(dupLongScripts)))
fmt.Println("(Duplicated long lines are recommanded to be extracted to variables)")
for index, longScript := range dupLongScripts {
fmt.Printf(WarningColor, fmt.Sprintf("(%d) ", index))
fmt.Printf(DebugColor, longScript.value)
Expand All @@ -92,7 +92,7 @@ func LongScriptsWarning(dupLongScripts []ScriptSummary) {
fmt.Printf("%s In %s\n", script.sectionName, script.filePath)
}
}
fmt.Printf(WarningColor, fmt.Sprintf("\nThe above %d duplicated css long scripts shall be set to variables.\n", len(dupLongScripts)))
fmt.Printf(WarningColor, fmt.Sprintf("\nThe above %d duplicated long lines shall be set to variables.\n", len(dupLongScripts)))
} else {
fmt.Printf(DebugColor, "√\t")
fmt.Printf(InfoColor, "No duplicated long script found\n")
Expand All @@ -102,7 +102,7 @@ func LongScriptsWarning(dupLongScripts []ScriptSummary) {
// UnusedScriptsWarning prints warnings for unused css classes
func UnusedScriptsWarning(scripts []StyleSection) {
if len(scripts) > 0 {
fmt.Printf(WarningColor, fmt.Sprintf("\nOps %d css found not used.\n", len(scripts)))
fmt.Printf(WarningColor, fmt.Sprintf("\nOps %d classes found not used.\n", len(scripts)))
for index, script := range scripts {
fmt.Printf(WarningColor, fmt.Sprintf("(%d) ", index))
fmt.Printf(DebugColor, fmt.Sprintf("%s < %s\n", script.name, script.filePath))
Expand Down

0 comments on commit 818faef

Please sign in to comment.