├── .flake8 ├── .gitignore ├── .travis.yml ├── README.md ├── hint ├── __init__.py ├── cli.py ├── detector │ ├── __init__.py │ ├── error.py │ └── exxx.py ├── hint.py ├── parsing.py └── utils.py ├── setup.cfg ├── setup.py └── tests ├── md ├── E101.md ├── E102.md ├── E103.md ├── E104.md ├── E201.md ├── E202.md ├── E203.md ├── E204.md ├── E205.md └── E301.md ├── test.sh ├── test_e1xx.py ├── test_e2xx.py └── test_e3xx.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg, 4 | node_modules, data, docs, build, lib, 5 | local_test.py,local_test.sh 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | dist/ 14 | develop-eggs/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib64/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .coverage.* 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | *,cover 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ 57 | 58 | .pydevproject 59 | .project 60 | .settings 61 | 62 | local_test.* 63 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.5" 6 | - "3.6" 7 | before_install: 8 | - pip install flake8 9 | - python setup.py install 10 | - chmod 777 tests/test.sh 11 | script: flake8 && ./tests/test.sh && hint README.md 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hint 2 | 3 | > A simple **markdown** lint / hint `cli-tool`, for markdown developer integrated with travis. Python 2 / 3 supported. 4 | 5 | > 一个简单的 **markdown** 静态检查的控制台 `cli` 工具,可以方便 markdown 开发者轻松集成 travis 自动检测。支持 Python 2 / 3。 6 | 7 | [![Latest Stable Version](https://img.shields.io/pypi/v/hint.svg)](https://pypi.python.org/pypi/hint) [![Build Status](https://travis-ci.org/hustcc/hint.svg?branch=master)](https://travis-ci.org/hustcc/hint) 8 | 9 | 10 | ## 一、安装 11 | 12 | > **pip install hint** 13 | 14 | 然后在系统中会得到一个 `hint` 的命令 cli 工具。 15 | 16 | 17 | ## 二、使用 18 | 19 | 使用方法有两种: 20 | 21 | **2.1 一种是`命令行 cli 方式`**,简单使用方法如下: 22 | 23 | > **hint markdown_file** 24 | 25 | 或者 26 | 27 | > **hint markdown_folder** 28 | 29 | 或者使用 `hint --help` 查看帮助信息和具体详细的使用方法。 30 | 31 | ```shell 32 | $ hint --help 33 | Usage: hint-script.py [OPTIONS] FILE 34 | 35 | Options: 36 | -i, --ignore TEXT The error codes which will be ignored. 37 | -f, --format [text|json] The output format of error information. 38 | -m, --max-depth INTEGER The max depth for traverse the path. 39 | --help Show this message and exit. 40 | 41 | ``` 42 | 43 | 可以用于直接集成到各种 ci 系统中,例如 travis-ci。 44 | 45 | **2.2 另外一种是`代码 API 调用的方式`**,简单使用方法如下: 46 | 47 | ```py 48 | import hint 49 | 50 | text=''' 51 | hint 是一个简单的 **markdown** 静态检查的控制台 `cli` 工具。 52 | 可以方便 markdown 开发者轻松集成 travis 自动检测。 53 | ''' 54 | errors = hint.check(text, ignore='E201') 55 | 56 | fn = 'README.md' 57 | errors = hint.check_file(fn, format='text') 58 | ``` 59 | 60 | 可以方便的进行第三方扩展开发。 61 | 62 | 63 | ## 三、错误码 64 | 65 | 检查规则来源于 [chinese-copywriting-guidelines](https://github.com/sparanoid/chinese-copywriting-guidelines),错误码命名方式参考于 flake8。目前支持的错误码如下所示: 66 | 67 | | 错误码 | 检查类型 | 详细描述 | 完成 | 68 | | ------ | ------ | ------ | ------ | 69 | | E101 | 空格 | 中英文之间需要增加空格 | done | 70 | | E102 | 空格 | 中文与数字之间需要增加空格 | done | 71 | | E103 | 空格 | 全角标点与其他字符之间不加空格 | done | 72 | | E104 | 空格 | 除了%、℃、°、以及倍数单位(如 2x、3n)之外,数字与单位之间需要增加空格 | done | 73 | | E201 | 标点 | 不重复使用标点符号 | done | 74 | | E202 | 标点 | 只有中文或中英文混排中,一律使用中文全角标点 | done | 75 | | E203 | 标点 | 如果出现整句英文,则在这句英文中使用英文、半角标点 | done | 76 | | E204 | 标点 | 省略号请使用……标准用法 | done | 77 | | E205 | 标点 | 英文和后面的半角标点之间不需要空格 | done | 78 | | E301 | 数字 | 数字使用半角字符 | done | 79 | 80 | 关于各种错误码的正确、错误范例,可以参考 [tests/md](tests/md)。**目前有了大概的代码结构,欢迎 PR 更多的检查错误类型和检查方式**。 81 | 82 | 83 | # LICENSE 84 | 85 | MIT @[hustcc](https://github.com/hustcc). 86 | -------------------------------------------------------------------------------- /hint/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 2016-12-13 4 | 5 | @author: hustcc 6 | ''' 7 | from __future__ import absolute_import 8 | from hint import hint, utils 9 | 10 | 11 | __version__ = '1.0.4' 12 | 13 | 14 | def check(text, ignore='', format='json', fn='anonymous'): 15 | '''check markdown text''' 16 | # check results 17 | errors = hint.check(text) 18 | # ignores 19 | errors = utils.ignore_errorcode(errors, ignore) 20 | # format output array / dict 21 | errors = {fn: utils.format_errors(errors, format)} 22 | if format != 'json': 23 | errors = ['File:%s\n%s' % (k, '\n'.join(es)) 24 | for k, es in errors.items() 25 | if len(es) > 0] 26 | errors = '\n\n'.join(errors) 27 | return errors 28 | 29 | 30 | def check_file(fn, ignore='', format='json'): 31 | '''check markdown file''' 32 | with open(fn) as f: 33 | text = f.read() 34 | return check(text, ignore, format, fn=fn) 35 | -------------------------------------------------------------------------------- /hint/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 2016-12-13 4 | 5 | @author: hustcc 6 | ''' 7 | from __future__ import absolute_import 8 | from hint import hint, utils 9 | import click 10 | import sys 11 | import json 12 | import os 13 | 14 | 15 | @click.command() 16 | @click.argument('file', type=click.Path(exists=True)) 17 | @click.option('-i', '--ignore', default='', 18 | help='The error codes which will be ignored.') 19 | @click.option('-f', '--format', default='text', 20 | type=click.Choice(['text', 'json']), 21 | help='The output format of error information.') 22 | @click.option('-m', '--max-depth', default=3, 23 | type=click.INT, 24 | help='The max depth for traverse the path.') 25 | def hint_entry(file, ignore, format, max_depth): 26 | files = [] 27 | if os.path.isdir(file): 28 | # 遍历 n 层,获得所有的 .md 文件 29 | files = utils.traversing_path_norecursive([], 30 | file, 31 | max_depth=max_depth) 32 | elif os.path.isfile(file): 33 | files.append(file) 34 | 35 | errors_dict = {} # check results 36 | cnt = 0 37 | for fn in files: 38 | # check files & ignore 39 | errors = hint.check_file(fn, ignore) 40 | cnt += len(errors) 41 | errors_dict[fn] = errors 42 | 43 | # format output array / dict 44 | edi = errors_dict.items() 45 | errors_dict = { 46 | fn: utils.format_errors(errors, format) for fn, errors in edi 47 | } 48 | # success or fail 49 | fail = cnt and True or False 50 | errors = '' # errors text to be console.log 51 | if format == 'json': 52 | errors = json.dumps(errors_dict, indent=2) 53 | else: 54 | errors = ['File:%s\n%s' % (k, '\n'.join(es)) 55 | for k, es in errors_dict.items() 56 | if len(es) > 0] 57 | errors = '\n\n'.join(errors) 58 | # echo 59 | click.echo(fail and errors or '^_^ No Hint, well done.') 60 | 61 | sys.exit(fail and 1 or 0) 62 | 63 | 64 | def run(): 65 | hint_entry() 66 | 67 | 68 | if __name__ == '__main__': 69 | run() 70 | -------------------------------------------------------------------------------- /hint/detector/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /hint/detector/error.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 2016-12-13 4 | 5 | @author: hustcc 6 | ''' 7 | 8 | errors = { 9 | # 空格 10 | 'E101': u'英文与非标点的中文之间需要有一个空格', 11 | 'E102': u'数字与非标点的中文之间需要有一个空格', 12 | 'E103': u'全角标点与其他字符之间不加空格', 13 | 'E104': u'除了%、℃、°、以及倍数单位(如 2x、3n)之外,数字与单位之间需要增加空格', 14 | # 标点符号 15 | 'E201': u'不重复使用标点符号', 16 | 'E202': u'只有中文或中英文混排中,一律使用中文全角标点', 17 | 'E203': u'如果出现整句英文,则在这句英文中使用英文、半角标点', 18 | 'E204': u'省略号请使用……标准用法', 19 | 'E205': u'英文和后面的半角标点之间不需要空格', 20 | # 数字 21 | 'E301': u'数字使用半角字符', 22 | } 23 | 24 | 25 | class Error(object): 26 | def __init__(self, text, code, index=0): 27 | self.text = text 28 | self.code = code 29 | self.index = index 30 | 31 | def description(self): 32 | return errors.get(self.code, 'unknow') 33 | 34 | def short_text(self, length=20): 35 | text_len = len(self.text) 36 | half_len = length // 2 37 | 38 | start = self.index - half_len 39 | start = start > 0 and start or 0 40 | 41 | end = start + length 42 | end = end > text_len and text_len or end 43 | 44 | return u'%s<%s>%s' % (self.text[start:self.index], 45 | self.code, 46 | self.text[self.index:end]) 47 | 48 | def json_format(self): 49 | rst = {} 50 | rst['code'] = self.code 51 | rst['text'] = self.short_text() 52 | rst['index'] = self.index 53 | rst['description'] = self.description() 54 | return rst 55 | 56 | def text_format(self): 57 | return u'%s:COL<%s>:%s:"%s"' % \ 58 | (self.code, self.index, 59 | self.description(), self.short_text()) 60 | 61 | def format(self, format='text'): 62 | if format == 'json': 63 | return self.json_format() 64 | return self.text_format() 65 | 66 | 67 | class BaseDetector(object): 68 | def __ini__(self): 69 | pass 70 | 71 | def errors(self): 72 | return [] 73 | 74 | def find_all_string(self, str, sub): 75 | index = [] 76 | i = 0 77 | while i != -1: 78 | i = str.find(sub, i + 1) 79 | if i == -1: 80 | return index 81 | else: 82 | index.append(i) 83 | 84 | return index 85 | -------------------------------------------------------------------------------- /hint/detector/exxx.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on Jan 22, 2017 4 | 5 | @author: hustcc 6 | ''' 7 | from __future__ import absolute_import 8 | from hint.detector import error 9 | import re 10 | 11 | 12 | class Detector(error.BaseDetector): 13 | # 一些错误的状态情况 14 | error_sm = [ 15 | ['E101', 'ZL'], # 中英文之间需要增加空格 16 | ['E101', 'LZ'], # 中英文之间需要增加空格 17 | 18 | ['E102', 'ZN'], # 中文与数字之间需要增加空格 19 | ['E102', 'NZ'], # 中文与数字之间需要增加空格 20 | 21 | ['E103', 'HS'], # 全角标点与其他字符之间不加空格 22 | ['E103', 'SH'], # 全角标点与其他字符之间不加空格 23 | 24 | ['E104', 'NSU'], # 数字后面 %℃° 不需要空格 25 | 26 | ['E201', 'HH'], # 不重复使用标点符号 中文中文 27 | ['E201', 'HI'], # 不重复使用标点符号 中文英文 28 | ['E201', 'IH'], # 不重复使用标点符号 英文中文 29 | ['E201', 'II'], # 不重复使用标点符号 英文英文 30 | 31 | ['E202', 'ZI'], # 只有中文或中英文混排中,一律使用中文全角标点,中文英文标点 32 | ['E202', 'IZ'], # 英文标点,中文 33 | 34 | ['E301', 'F'], # 数字不使用半角字符 35 | ] 36 | 37 | # 错误类型的自定义处理规则,因为规则比较复杂,只能自定义的去检测 38 | error_fsm = [ 39 | ['E203', ''], # 如果出现整句英文,则在这句英文中使用英文、半角标点 40 | ['E204', ''], # 省略号请使用……标准用法 41 | ['E205', ''], # 英文和后面的半角标点之间不需要空格 42 | ] 43 | 44 | def __init__(self, tokens, p): 45 | super(Detector, self).__init__() 46 | self.tokens = tokens or [] 47 | self.p = p or '' 48 | self.error_fsm = [ 49 | ['E203', self._e203], # 如果done出现整句英文,则在这句英文中使用英文、半角标点 50 | ['E204', self._e204], # 省略号请使用……标准用法 51 | ['E205', self._e205], # 文中出现英文、半角标点之后,需要有空格 52 | ] 53 | 54 | # 比较常用的检测器 55 | def _common_detector(self, sm, token_types): 56 | indexs = self.find_all_string(token_types, sm[1]) 57 | return [error.Error(self.p, sm[0], i + 1) for i in indexs] 58 | 59 | # 如果出现整句英文,则在这句英文中使用英文、半角标点 60 | def _e203(self, error_code, token_types): 61 | # 如果整行中只有 数字、空格、英文标点、英文、中文标点 62 | errors_all = re.match(r'^[NSILOH]*$', token_types) 63 | 64 | errors = [] 65 | if not errors_all: 66 | return errors 67 | 68 | i = -1 69 | while True: 70 | i = token_types.find('H', i + 1) 71 | if i == -1: 72 | break 73 | errors.append(error.Error(self.p, error_code, i + 1)) 74 | return errors 75 | 76 | # 省略号请使用……标准用法 77 | def _e204(self, error_code, token_types): 78 | errors_all = re.findall(r'[^E]E{1,}[^E]{0,1}', token_types) 79 | i = -1 80 | errors = [] 81 | for e in errors_all: 82 | if e.count('E') != 2: 83 | i = token_types.find(e, i + 1) 84 | if i != -1: 85 | errors.append(error.Error(self.p, error_code, 86 | i + 1 + e.rindex('E'))) 87 | return errors 88 | 89 | # 文中出现英文、半角标点之后,需要有空格 90 | def _e205(self, error_code, token_types): 91 | # 英文和标点之间不需要空格 92 | errors_all = re.findall(r'LS{1,}I', token_types) 93 | i = -1 94 | errors = [] 95 | for e in errors_all: 96 | i = token_types.find(e, i + 1) 97 | if i != -1: 98 | errors.append(error.Error(self.p, error_code, i + 2)) 99 | return errors 100 | 101 | def errors(self): 102 | errors = [] 103 | token_types = [token['type'] for token in self.tokens] 104 | token_types = ''.join(token_types) 105 | # print self.p 106 | # print token_types 107 | # 1. 处理 common_detector 108 | for sm in self.error_sm: 109 | errors += self._common_detector(sm, token_types) 110 | 111 | # 2. 处理正则规则 112 | for sm in self.error_fsm: 113 | errors += sm[1](sm[0], token_types) 114 | 115 | return errors 116 | -------------------------------------------------------------------------------- /hint/hint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 2016-12-13 4 | 5 | @author: hustcc 6 | ''' 7 | from __future__ import absolute_import 8 | from hint import parsing, utils 9 | import functools 10 | 11 | 12 | def do_paragraph(errors, p): 13 | tokens = parsing.tokenizer(p) 14 | new_errors = parsing.detect_errors(tokens, p) 15 | return errors + new_errors 16 | 17 | 18 | def check(text): 19 | '''check the error in mark down text 20 | ''' 21 | paragraph = parsing.to_paragraph_array(text) 22 | # reduce to detect errors. 23 | return functools.reduce(do_paragraph, paragraph, []) 24 | 25 | 26 | def check_file(fn, ignore): 27 | '''check the error in mark down fn 28 | ''' 29 | with open(fn) as f: 30 | text = f.read() 31 | errors = check(text) 32 | return utils.ignore_errorcode(errors, ignore) 33 | -------------------------------------------------------------------------------- /hint/parsing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 2016-12-13 4 | 5 | @author: hustcc 6 | ''' 7 | from __future__ import absolute_import 8 | from hint import utils 9 | import functools 10 | import re 11 | import sys 12 | 13 | 14 | def pre_process(md_text): 15 | '''pre process the mark down text string. 16 | ''' 17 | # 1. 去除代码块 18 | # md_text = re.sub(r'(```.*```)', '', 19 | # md_text, flags=re.I | re.S) 贪婪匹配,误删文件内容 20 | md_text = re.sub(r'```.*?(.*?)```', '', 21 | md_text, flags=re.I | re.S) 22 | # 2. 删除图片 23 | md_text = re.sub(r'(\!\[.*?\]\(.*?\))', '', md_text, flags=re.I) 24 | # 3. 提取链接内容 25 | md_text = re.sub(r'\[(.*?)]\(.*?\)', '\g<1>', md_text, flags=re.I) 26 | # 4. 去除 `` 27 | md_text = re.sub(r'`(.*?)`', '\g<1>', md_text, flags=re.I) 28 | return md_text or u'' 29 | 30 | 31 | def to_paragraph_array(md_text): 32 | '''parse mark down file, and return all the paragraph array''' 33 | md_text = pre_process(md_text) 34 | # change to unicode 35 | if sys.version_info[0] == 3 and not isinstance(md_text, str): 36 | md_text = md_text.decode('utf-8') 37 | elif sys.version_info[0] == 2 and not isinstance(md_text, unicode): # noqa 38 | md_text = md_text.decode('utf-8') 39 | 40 | md_lines = md_text.split('\n') 41 | # filter not empty element 42 | return [line for line in md_lines if line.strip()] 43 | 44 | 45 | def reduce_handler(tokens, c): 46 | '''how to reduce to get token strings.''' 47 | type = utils.typeof(c) 48 | tokens.append({'type': type, 'text': c}) 49 | return tokens 50 | 51 | 52 | def tokenizer(p): 53 | '''parse each mark down text line, get the tokenizer of the line''' 54 | tokens = functools.reduce(reduce_handler, p, []) 55 | return tokens 56 | 57 | 58 | def detect_errors(tokens, p): 59 | '''detect error code from tokens.''' 60 | errors = [] 61 | # 自动加载所有的检测器 62 | detectors = utils.load_detectors() 63 | 64 | for detector in detectors: 65 | errors += errors + detector(tokens, p).errors() 66 | 67 | return errors 68 | -------------------------------------------------------------------------------- /hint/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 2016-12-13 4 | 5 | @author: hustcc 6 | ''' 7 | from __future__ import absolute_import 8 | import os 9 | import copy 10 | 11 | 12 | def is_latin(c): 13 | '''decide c is latin or not 14 | ''' 15 | return c.isalpha() 16 | 17 | 18 | def is_space(c): 19 | '''decide c is space,中文 20 | ''' 21 | return c.isspace() 22 | 23 | 24 | def is_number(c): 25 | # 是否是数字 26 | return c in '1234567890' 27 | 28 | 29 | def is_unit(c): 30 | # 判断是否为单位 31 | return 'TODO' 32 | 33 | 34 | def is_fw_symbol(c): 35 | # 是否为中文符号 36 | return c in u',。;:、?!' 37 | 38 | 39 | def is_hw_symbol(c): 40 | # 是否为中文符号 41 | return c in ',.;:\?!' 42 | 43 | 44 | def is_ellipsis_symbol(c): 45 | # 是否是省略号 46 | return c in u'…' 47 | 48 | 49 | def is_fw_number(c): 50 | # 是否是圆角数字 51 | return c in u'0123456789' 52 | 53 | 54 | def is_zh(c): 55 | '''判断是否为中文''' 56 | return u'\u4e00' <= c <= u'\u9fff' and True or False 57 | 58 | 59 | def is_zh_unit(c): 60 | '''判断是否为单位''' 61 | return c in u'%℃°' 62 | 63 | 64 | def ignore_errorcode(errors, ignores): 65 | '''ignore the errors in ignores 66 | ''' 67 | if not isinstance(ignores, list): 68 | ignores = ignores.split(',') 69 | # trim the error code 70 | ignores = [code.strip() for code in ignores] 71 | # ignore error codes. 72 | errors = [error for error in errors if error.code not in ignores] 73 | return errors 74 | 75 | 76 | def format_errors(errors, format): 77 | return [e.format(format) for e in errors] 78 | 79 | 80 | def typeof(c): 81 | if is_number(c): 82 | return 'N' # number 83 | if is_zh_unit(c): 84 | return 'U' # zh unit 85 | if is_space(c): 86 | return 'S' # space 87 | if is_ellipsis_symbol(c): # 省略号 88 | return 'E' 89 | if is_fw_symbol(c): 90 | return 'H' # zh sym 91 | if is_hw_symbol(c): 92 | return 'I' # en sym 93 | if is_fw_number(c): 94 | return 'F' # 全角 number 95 | if is_zh(c): 96 | return 'Z' # zh 97 | if is_latin(c): 98 | return 'L' # latin 99 | return 'O' # other, no limit 100 | 101 | 102 | detectors_on = None 103 | 104 | 105 | def load_detectors(): 106 | '''加载所有的检测器''' 107 | # 缓存一下,避免多次加载 108 | global detectors_on 109 | if detectors_on: 110 | return detectors_on 111 | 112 | from hint.detector import exxx 113 | detectors_on = [exxx.Detector] 114 | 115 | return detectors_on 116 | 117 | 118 | # 使用非递归的方式,来遍历目中的所有 markdown 文件 119 | def traversing_path_norecursive(all_files, path, 120 | depth=0, 121 | max_depth=3, 122 | suffixs=['.md']): 123 | paths = [path] # 需要被循环遍历的目录数组 124 | # 遍历深度限制 125 | paths_tmp = [] # 临时存储每次遍历的目录 126 | pj = os.path.join 127 | while depth <= max_depth: 128 | # 目录,新增一级 129 | depth += 1 130 | # 遍历这些目录 131 | for path in paths: 132 | ls = os.listdir(path) # 目录中所有的文件,文件夹 133 | # 所有文件 134 | files = [f for f in ls if os.path.isfile(pj(path, f))] 135 | # 符合后缀名称的文件,添加到最后的 all_files 中 136 | all_files += [ 137 | pj(path, f) for f in files if f[-3:] in suffixs 138 | ] 139 | 140 | # 所有文件夹,加入到待遍历数组中 141 | paths_tmp += [ 142 | pj(path, d) for d in ls if os.path.isdir(pj(path, d)) 143 | ] 144 | paths = copy.deepcopy(paths_tmp) 145 | # 每次遍历一层之后初始化 146 | paths_tmp = [] 147 | 148 | return all_files 149 | 150 | 151 | if __name__ == '__main__': 152 | print(traversing_path_norecursive([], 'E:/Work/git_code/hint', 153 | max_depth=4)) 154 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [bdist_wheel] 5 | universal = 1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import setuptools # noqa 3 | from distutils.core import setup 4 | import io 5 | import re 6 | import os 7 | 8 | 9 | DOC = ''' 10 | ## 一、安装 11 | 12 | > **pip install hint** 13 | 14 | 然后在系统中会得到一个 `hint` 的命令 cli 工具。 15 | 16 | 17 | ## 二、使用 18 | 19 | 简单使用方法如下: 20 | 21 | > **hint markdown_file** 22 | 23 | 或者使用 `hint --help` 查看帮助信息和具体详细的使用方法。 24 | 25 | ''' 26 | 27 | 28 | def read(*names, **kwargs): 29 | return io.open( 30 | os.path.join(os.path.dirname(__file__), *names), 31 | encoding=kwargs.get("encoding", "utf8") 32 | ).read() 33 | 34 | 35 | def find_version(*file_paths): 36 | version_file = read(*file_paths) 37 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 38 | version_file, re.M) 39 | if version_match: 40 | return version_match.group(1) 41 | raise RuntimeError("Unable to find version string.") 42 | 43 | 44 | setup(name='hint', 45 | version=find_version('hint/__init__.py'), 46 | description=('A simple markdown lint / hint `cli-tool`, ' 47 | 'for markdown developer integrated with travis.'), 48 | long_description=DOC, 49 | author='hustcc', 50 | author_email='i@hust.cc', 51 | url='https://github.com/hustcc', 52 | license='MIT', 53 | install_requires=[ 54 | 'click' 55 | ], 56 | classifiers=[ 57 | 'Intended Audience :: Developers', 58 | 'Operating System :: OS Independent', 59 | 'Natural Language :: Chinese (Simplified)', 60 | 'Programming Language :: Python', 61 | 'Programming Language :: Python :: 2', 62 | 'Programming Language :: Python :: 2.7', 63 | 'Programming Language :: Python :: 3', 64 | 'Programming Language :: Python :: 3.5', 65 | 'Programming Language :: Python :: 3.6', 66 | 'Topic :: Utilities' 67 | ], 68 | keywords='hint, lint, markdown, rules, error', 69 | include_package_data=True, 70 | zip_safe=False, 71 | packages=['hint', 'hint.detector'], 72 | entry_points={ 73 | 'console_scripts': ['hint=hint.cli:run'] 74 | }) 75 | -------------------------------------------------------------------------------- /tests/md/E101.md: -------------------------------------------------------------------------------- 1 | # 错误示例 2 | 3 | 对不起,我爱你I Love You真的。 4 | 5 | 6 | # 正确示例 7 | 8 | 对不起,我爱你 I Love You 真的。 -------------------------------------------------------------------------------- /tests/md/E102.md: -------------------------------------------------------------------------------- 1 | # 错误示例 2 | 3 | 对不起,我爱你1314真的。 4 | 5 | 6 | 7 | # 正确示例 8 | 9 | 对不起,我爱你 1314 真的。 -------------------------------------------------------------------------------- /tests/md/E103.md: -------------------------------------------------------------------------------- 1 | # 错误示例 2 | 3 | 对不起,我爱你 1314 真的 。 4 | 5 | 对不起, 我爱你 。 6 | 7 | 真的, I Love You 。 8 | 9 | 10 | # 正确示例 11 | 12 | 对不起,我爱你 1314 真的。 13 | 14 | 对不起,我爱你。 15 | 16 | 真的,I Love You。 -------------------------------------------------------------------------------- /tests/md/E104.md: -------------------------------------------------------------------------------- 1 | # 错误示例 2 | 3 | 当前温度 38 ℃,超过全国 95 %的地区。 4 | 5 | 6 | # 正确示例 7 | 8 | 当前温度 38℃,超过全国 95%的地区。 -------------------------------------------------------------------------------- /tests/md/E201.md: -------------------------------------------------------------------------------- 1 | # 错误示例 2 | 3 | 哈哈,, 4 | 5 | 是不是啊?? 6 | 7 | Hi,. 8 | 9 | # 正确示例 10 | 11 | 哈哈, 12 | 13 | 是不是啊? 14 | 15 | Hi. -------------------------------------------------------------------------------- /tests/md/E202.md: -------------------------------------------------------------------------------- 1 | # 错误示例 2 | 3 | 你好, hint,是一个不错的项目. 4 | 5 | 6 | # 正确示例 7 | 8 | 9 | 你好,hint 是一个不错的项目。 -------------------------------------------------------------------------------- /tests/md/E203.md: -------------------------------------------------------------------------------- 1 | # 错误示例 2 | 3 | Stay hungry,stay foolish. 4 | 5 | Hackers&Painters:Big Ideas from the Computer Age 6 | 7 | 8 | # 正确示例 9 | 10 | Stay hungry, stay foolish. 11 | 12 | Hackers&Painters: Big Ideas from the Computer Age -------------------------------------------------------------------------------- /tests/md/E204.md: -------------------------------------------------------------------------------- 1 | # 错误示例 2 | 3 | 真的对不起…真的……… 4 | 5 | 6 | 7 | # 正确示例 8 | 9 | 真的对不起……真的…… -------------------------------------------------------------------------------- /tests/md/E205.md: -------------------------------------------------------------------------------- 1 | # 错误示例 2 | 3 | I love you . Realy 4 | 5 | I love you . 真的 6 | 7 | 8 | # 正确示例 9 | 10 | I love you. Realy 11 | 12 | I love you. 真的 -------------------------------------------------------------------------------- /tests/md/E301.md: -------------------------------------------------------------------------------- 1 | # 错误示例 2 | 3 | 目前 hint 使用项目有 789 个。 4 | 5 | 6 | # 正确示例 7 | 8 | 目前 hint 使用项目有 789 个。 -------------------------------------------------------------------------------- /tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python tests/test_e1xx.py 4 | python tests/test_e2xx.py 5 | python tests/test_e3xx.py -------------------------------------------------------------------------------- /tests/test_e1xx.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on Jan 23, 2017 4 | 5 | @author: hustcc 6 | ''' 7 | import unittest 8 | import hint 9 | 10 | 11 | class TestE1xx(unittest.TestCase): 12 | def setUp(self): 13 | pass 14 | 15 | def test_e101_json(self): 16 | # check file 17 | errors = hint.check_file('tests/md/E101.md') 18 | self.assertEqual(len(errors), 1) 19 | self.assertEqual(len(errors.get('tests/md/E101.md', [])), 2) 20 | for e in errors.get('tests/md/E101.md', []): 21 | self.assertEqual(e.get('code'), 'E101') 22 | 23 | # ignore 24 | errors = hint.check_file('tests/md/E101.md', ignore='E101') 25 | 26 | self.assertEqual(len(errors.get('tests/md/E101.md', [])), 0) 27 | 28 | def test_e101_text(self): 29 | # check file 30 | errors = hint.check_file('tests/md/E101.md', format='text') 31 | errors = errors.split('\n\n') 32 | self.assertEqual(len(errors), 1) 33 | errors = errors[0].split('\n') 34 | self.assertEqual(len(errors), 3) 35 | # ignore 36 | errors = hint.check_file('tests/md/E101.md', 37 | format='text', 38 | ignore='E101') 39 | self.assertEqual(errors.strip(), '') 40 | 41 | def test_e102(self): 42 | # check file 43 | errors = hint.check_file('tests/md/E102.md', format='text') 44 | errors = errors.split('\n\n') 45 | self.assertEqual(len(errors), 1) 46 | for e in errors: 47 | e = e.split('\n') 48 | self.assertEqual(len(e), 2 + 1) 49 | # ignore 50 | errors = hint.check_file('tests/md/E102.md', ignore='E102') 51 | self.assertEqual(len(errors.get('tests/md/E102.md', [])), 0) 52 | 53 | def test_e103(self): 54 | # check file 55 | errors = hint.check_file('tests/md/E103.md', format='json') 56 | self.assertEqual(len(errors), 1) 57 | errors = errors.get('tests/md/E103.md', []) 58 | self.assertEqual(len(errors), 5) 59 | for e in errors: 60 | self.assertEqual(e.get('code'), 'E103') 61 | # ignore 62 | errors = hint.check_file('tests/md/E103.md', 63 | ignore='E103', 64 | format='text') 65 | self.assertEqual(errors.strip(), '') 66 | 67 | 68 | if __name__ == '__main__': 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /tests/test_e2xx.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on Jan 23, 2017 4 | 5 | @author: hustcc 6 | ''' 7 | import unittest 8 | import hint 9 | 10 | 11 | class TestE1xx(unittest.TestCase): 12 | def setUp(self): 13 | pass 14 | 15 | def test_e201_json(self): 16 | # check file 17 | errors = hint.check_file('tests/md/E201.md') 18 | self.assertEqual(len(errors), 1) 19 | self.assertEqual(len(errors.get('tests/md/E201.md', [])), 3) 20 | for e in errors.get('tests/md/E201.md', []): 21 | self.assertEqual(e.get('code'), 'E201') 22 | 23 | # ignore 24 | errors = hint.check_file('tests/md/E201.md', ignore='E201') 25 | 26 | self.assertEqual(len(errors.get('tests/md/E201.md', [])), 0) 27 | 28 | def test_e201_text(self): 29 | # check file 30 | errors = hint.check_file('tests/md/E201.md', format='text') 31 | errors = errors.split('\n\n') 32 | self.assertEqual(len(errors), 1) 33 | errors = errors[0].split('\n') 34 | self.assertEqual(len(errors), 4) 35 | # ignore 36 | errors = hint.check_file('tests/md/E201.md', 37 | format='text', 38 | ignore='E201') 39 | self.assertEqual(errors.strip(), '') 40 | 41 | def test_e202_json(self): 42 | # check file 43 | errors = hint.check_file('tests/md/E202.md') 44 | self.assertEqual(len(errors), 1) 45 | self.assertEqual(len(errors.get('tests/md/E202.md', [])), 3) 46 | for e in errors.get('tests/md/E202.md', []): 47 | self.assertEqual(e.get('code'), 'E202') 48 | 49 | # ignore 50 | errors = hint.check_file('tests/md/E202.md', ignore='E202') 51 | 52 | self.assertEqual(len(errors.get('tests/md/E202.md', [])), 0) 53 | 54 | def test_e203_text(self): 55 | # check file 56 | errors = hint.check_file('tests/md/E203.md', format='text') 57 | errors = errors.split('\n\n') 58 | self.assertEqual(len(errors), 1) 59 | errors = errors[0].split('\n') 60 | self.assertEqual(len(errors), 3) 61 | # ignore 62 | errors = hint.check_file('tests/md/E203.md', 63 | format='text', 64 | ignore='E203') 65 | self.assertEqual(errors.strip(), '') 66 | 67 | def test_e204_json(self): 68 | # check file 69 | errors = hint.check_file('tests/md/E204.md') 70 | self.assertEqual(len(errors), 1) 71 | self.assertEqual(len(errors.get('tests/md/E204.md', [])), 2) 72 | for e in errors.get('tests/md/E204.md', []): 73 | self.assertEqual(e.get('code'), 'E204') 74 | 75 | # ignore 76 | errors = hint.check_file('tests/md/E204.md', ignore='E204') 77 | 78 | self.assertEqual(len(errors.get('tests/md/E204.md', [])), 0) 79 | 80 | def test_e205_json(self): 81 | # check file 82 | errors = hint.check_file('tests/md/E205.md') 83 | self.assertEqual(len(errors), 1) 84 | self.assertEqual(len(errors.get('tests/md/E205.md', [])), 2) 85 | for e in errors.get('tests/md/E205.md', []): 86 | self.assertEqual(e.get('code'), 'E205') 87 | 88 | # ignore 89 | errors = hint.check_file('tests/md/E205.md', ignore='E205') 90 | 91 | self.assertEqual(len(errors.get('tests/md/E205.md', [])), 0) 92 | 93 | 94 | if __name__ == '__main__': 95 | unittest.main() 96 | -------------------------------------------------------------------------------- /tests/test_e3xx.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on Jan 23, 2017 4 | 5 | @author: hustcc 6 | ''' 7 | import unittest 8 | import hint 9 | 10 | 11 | class TestE1xx(unittest.TestCase): 12 | def setUp(self): 13 | pass 14 | 15 | def test_e301_json(self): 16 | # check file 17 | errors = hint.check_file('tests/md/E301.md') 18 | self.assertEqual(len(errors), 1) 19 | self.assertEqual(len(errors.get('tests/md/E301.md', [])), 3) 20 | for e in errors.get('tests/md/E301.md', []): 21 | self.assertEqual(e.get('code'), 'E301') 22 | 23 | # ignore 24 | errors = hint.check_file('tests/md/E301.md', ignore='E301') 25 | 26 | self.assertEqual(len(errors.get('tests/md/E301.md', [])), 0) 27 | 28 | def test_e301_text(self): 29 | # check file 30 | errors = hint.check_file('tests/md/E301.md', format='text') 31 | errors = errors.split('\n\n') 32 | self.assertEqual(len(errors), 1) 33 | errors = errors[0].split('\n') 34 | self.assertEqual(len(errors), 4) 35 | # ignore 36 | errors = hint.check_file('tests/md/E301.md', 37 | format='text', 38 | ignore='E301') 39 | self.assertEqual(errors.strip(), '') 40 | 41 | 42 | if __name__ == '__main__': 43 | unittest.main() 44 | --------------------------------------------------------------------------------