| name | Markdown验证器 |
| description | 当验证Markdown文件时,检查文档,查找损坏链接,改进文档。发布前验证Markdown。 |
| license | MIT |
Markdown验证器技能
概述
文档会静默损坏。损坏的链接、无效语法和缺失文件使文档不可用。发布前必须验证。
核心原则: 文档即代码。需要验证。好的文档管理应该完整、准确、可访问、易维护。坏的文档管理会导致信息混乱、用户体验差、维护困难。
何时使用
始终:
- 发布文档前
- 检查损坏链接时
- 验证语法时
- 提交前
- 测试文档渲染时
触发短语:
- "检查这个README"
- "查找损坏链接"
- "验证文档"
- "这个Markdown正确吗?"
Markdown验证器技能功能
语法验证
- Markdown语法检查
- 标题层级验证
- 代码块格式检查
- 列表结构验证
- 表格格式检查
- 链接语法验证
链接检查
- 内部链接验证
- 外部链接测试
- 图片引用检查
- 锚点链接验证
- 相对路径检查
- 文件存在性验证
内容质量
- 文档完整性检查
- 拼写错误检测
- 语法问题识别
- 格式一致性检查
- TOC生成验证
- 元数据验证
渲染测试
- 多平台渲染测试
- 样式一致性检查
- 图片显示验证
- 代码高亮检查
- 数学公式验证
- 导出格式测试
常见Markdown问题
❌ 损坏链接
- 链接到不存在的文件
- 错误的相对路径
- 重命名文件的旧引用
- 外部链接失效
❌ 语法错误
- 不当的标题层级(H4后接H2)
- 未匹配的括号/括号
- 无效的代码块围栏
- 错误的列表缩进
❌ 内容问题
- 引用不存在的图片
- 不完整的章节
- 遗留的TODO注释
- 重复内容
❌ 格式问题
- 不一致的标题风格
- 错误的表格格式
- 混乱的列表结构
- 缺失的空行
代码示例
Markdown验证器
import re
import os
import requests
import yaml
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Set
from dataclasses import dataclass
from enum import Enum
from urllib.parse import urljoin, urlparse
import time
class ValidationLevel(Enum):
"""验证级别"""
ERROR = "error"
WARNING = "warning"
INFO = "info"
@dataclass
class ValidationIssue:
"""验证问题"""
level: ValidationLevel
line_number: int
column: int
message: str
suggestion: Optional[str] = None
rule: Optional[str] = None
@dataclass
class LinkInfo:
"""链接信息"""
text: str
url: str
line_number: int
column: int
link_type: str
:
():
.file_path = Path(file_path)
.base_path = Path(base_path) base_path .file_path.parent
.content =
.lines = []
.issues: [ValidationIssue] = []
.links: [LinkInfo] = []
.headers: [[, , ]] = []
.validation_rules = {
: ._validate_heading_hierarchy,
: ._validate_link_format,
: ._validate_code_blocks,
: ._validate_list_structure,
: ._validate_table_format,
: ._validate_image_references
}
() -> [ValidationIssue]:
.issues.clear()
._read_file():
.issues
._parse_content()
rule_name, rule_func .validation_rules.items():
:
rule_func()
Exception e:
.issues.append(ValidationIssue(
level=ValidationLevel.ERROR,
line_number=,
column=,
message=,
rule=rule_name
))
._check_links()
.issues
() -> :
:
(.file_path, , encoding=) f:
.content = f.read()
.lines = .content.splitlines()
FileNotFoundError:
.issues.append(ValidationIssue(
level=ValidationLevel.ERROR,
line_number=,
column=,
message=
))
UnicodeDecodeError:
.issues.append(ValidationIssue(
level=ValidationLevel.ERROR,
line_number=,
column=,
message=
))
():
line_num, line (.lines, ):
._parse_headers(line, line_num)
._parse_links(line, line_num)
():
header_match = re.(, line)
header_match:
level = (header_match.group())
text = header_match.group().strip()
.headers.append((text, level, line_num))
():
link_pattern =
re.finditer(link_pattern, line):
text = .group()
url = .group()
column = .start() +
link_type = ._determine_link_type(url)
.links.append(LinkInfo(
text=text,
url=url,
line_number=line_num,
column=column,
link_type=link_type
))
image_pattern =
re.finditer(image_pattern, line):
alt = .group()
url = .group()
column = .start() +
.links.append(LinkInfo(
text=alt,
url=url,
line_number=line_num,
column=column,
link_type=
))
() -> :
url.startswith():
url.startswith() url.startswith():
url.startswith():
url.startswith() url url:
:
():
prev_level =
text, level, line_num .headers:
prev_level > :
level > prev_level + :
.issues.append(ValidationIssue(
level=ValidationLevel.WARNING,
line_number=line_num,
column=,
message=,
suggestion=,
rule=
))
prev_level = level
():
line_num, line (.lines, ):
open_brackets = line.count()
close_brackets = line.count()
open_parens = line.count()
close_parens = line.count()
open_brackets != close_brackets:
.issues.append(ValidationIssue(
level=ValidationLevel.ERROR,
line_number=line_num,
column=,
message=,
suggestion=,
rule=
))
open_parens != close_parens:
.issues.append(ValidationIssue(
level=ValidationLevel.ERROR,
line_number=line_num,
column=,
message=,
suggestion=,
rule=
))
empty_link_pattern =
re.search(empty_link_pattern, line):
.issues.append(ValidationIssue(
level=ValidationLevel.WARNING,
line_number=line_num,
column=,
message=,
suggestion=,
rule=
))
():
in_code_block =
code_block_start =
code_fence =
line_num, line (.lines, ):
fence_match = re.(, line)
fence_match:
current_fence = fence_match.group()
language = fence_match.group()
in_code_block:
in_code_block =
code_block_start = line_num
code_fence = current_fence
current_fence == code_fence:
in_code_block =
code_fence =
:
.issues.append(ValidationIssue(
level=ValidationLevel.ERROR,
line_number=line_num,
column=,
message=,
suggestion=,
rule=
))
in_code_block:
.issues.append(ValidationIssue(
level=ValidationLevel.ERROR,
line_number=code_block_start,
column=,
message=,
suggestion=,
rule=
))
():
list_stack = []
line_num, line (.lines, ):
stripped = line.lstrip()
list_match = re.(, stripped)
list_match:
indent = (list_match.group())
marker = list_match.group()
list_stack:
last_indent, last_marker = list_stack[-]
(marker.isdigit() last_marker.isdigit()) \
( marker.isdigit() last_marker.isdigit()):
.issues.append(ValidationIssue(
level=ValidationLevel.WARNING,
line_number=line_num,
column=indent + ,
message=,
suggestion=,
rule=
))
list_stack.append((indent, marker))
stripped line.startswith() line.startswith():
list_stack.clear()
():
in_table =
table_start =
column_count =
line_num, line (.lines, ):
line:
in_table:
in_table =
table_start = line_num
column_count = line.count() -
:
current_columns = line.count() -
current_columns != column_count:
.issues.append(ValidationIssue(
level=ValidationLevel.ERROR,
line_number=line_num,
column=,
message=,
suggestion=,
rule=
))
:
in_table:
in_table =
column_count =
():
link .links:
link.link_type == :
link.url.startswith() link.url.startswith():
image_path = .base_path / link.url
image_path.exists():
.issues.append(ValidationIssue(
level=ValidationLevel.ERROR,
line_number=link.line_number,
column=link.column,
message=,
suggestion=,
rule=
))
():
link .links:
link.link_type == :
._check_internal_link(link)
link.link_type == :
._check_external_link(link)
link.link_type == :
._check_anchor_link(link)
():
link.url.startswith():
target_path = .base_path / link.url[:]
link.url.startswith():
target_path = .base_path / link.url
:
target_path = .base_path / link.url
target_path.exists():
.issues.append(ValidationIssue(
level=ValidationLevel.ERROR,
line_number=link.line_number,
column=link.column,
message=,
suggestion=,
rule=
))
():
:
response = requests.head(link.url, timeout=, allow_redirects=)
response.status_code >= :
.issues.append(ValidationIssue(
level=ValidationLevel.WARNING,
line_number=link.line_number,
column=link.column,
message=,
suggestion=,
rule=
))
requests.RequestException:
.issues.append(ValidationIssue(
level=ValidationLevel.WARNING,
line_number=link.line_number,
column=link.column,
message=,
suggestion=,
rule=
))
():
anchor = link.url[:]
found =
text, level, line_num .headers:
anchor_id = text.lower()
anchor_id = re.sub(, , anchor_id)
anchor_id = re.sub(, , anchor_id)
anchor_id == anchor:
found =
found:
.issues.append(ValidationIssue(
level=ValidationLevel.WARNING,
line_number=link.line_number,
column=link.column,
message=,
suggestion=,
rule=
))
() -> :
.validate()
error_count = ([i i .issues i.level == ValidationLevel.ERROR])
warning_count = ([i i .issues i.level == ValidationLevel.WARNING])
info_count = ([i i .issues i.level == ValidationLevel.INFO])
issues_by_rule = {}
issue .issues:
rule = issue.rule
rule issues_by_rule:
issues_by_rule[rule] = []
issues_by_rule[rule].append(issue)
{
: (.file_path),
: {
: (.issues),
: error_count,
: warning_count,
: info_count,
: (.links),
: (.headers)
},
: [
{
: issue.level.value,
: issue.line_number,
: issue.column,
: issue.message,
: issue.suggestion,
: issue.rule
}
issue .issues
],
: {
rule: [
{
: issue.line_number,
: issue.column,
: issue.message,
: issue.suggestion
}
issue issues
]
rule, issues issues_by_rule.items()
},
: [
{
: link.text,
: link.url,
: link.line_number,
: link.link_type
}
link .links
],
: [
{
: text,
: level,
: line_num
}
text, level, line_num .headers
]
}
__name__ == :
argparse
parser = argparse.ArgumentParser(description=)
parser.add_argument(, =)
parser.add_argument(, =)
parser.add_argument(, =)
parser.add_argument(, choices=[, ], default=, =)
parser.add_argument(, action=, =)
args = parser.parse_args()
validator = MarkdownValidator(args.file, args.base_path)
:
report = validator.generate_report()
args. == :
args.output:
(args.output, , encoding=) f:
json.dump(report, f, indent=, ensure_ascii=)
()
:
(json.dumps(report, indent=, ensure_ascii=))
:
( * )
()
( * )
()
summary = report[]
()
()
()
()
()
()
()
level_names = {
: ,
: ,
:
}
level [, , ]:
issues = [i i report[] i[] == level]
issues:
()
issue issues:
()
issue[]:
()
()
summary[] == :
()
:
()
Exception e:
()
exit()
批量Markdown检查工具
#!/bin/bash
set -e
SCAN_DIR=${1:-"."}
OUTPUT_DIR=${2:-"markdown-reports"}
CHECK_EXTERNAL=${3:-false}
PARALLEL_JOBS=${4:-4}
LOG_FILE="markdown_batch_check.log"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() {
echo -e "${GREEN}[INFO]${NC} $1" | tee -a "$LOG_FILE"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "$LOG_FILE"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE"
}
log_step() {
echo -e "${BLUE}[STEP]${NC} $1" | tee -a "$LOG_FILE"
}
() {
[ ! -d ];
-p
log_info
}
() {
log_step
markdown_files=()
IFS= -r -d file;
markdown_files+=()
< <(find -name - f -print0)
log_info
>
}
() {
file_path=
file_basename=$( .md)
report_file=
log_info
-v python3 &> /dev/null;
python3 markdown_validator.py --output --format json 2>>
log_error
1
[ $? -eq 0 ];
log_info
0
log_error
1
}
() {
total_files=
checked_files=0
failed_files=0
log_step
IFS= -r file_path;
(( $(jobs -r | wc -l) >= PARALLEL_JOBS ));
-n
{
check_single_file ;
>>
>>
} &
((checked_files++))
(( checked_files % == ));
log_info
<
[ -f ];
failed_files=$(grep -c || )
successful_files=$(grep -c || )
failed_files=0
successful_files=0
log_info
}
() {
log_step
summary_file=
> <<
total_files=$( -l < )
>>
[ -f ];
failed_files=$(grep -c || )
successful_files=$(grep -c || )
>>
>>
>>
total_errors=0
total_warnings=0
total_issues=0
report_file /*_report.json;
[ -f ];
-v jq &> /dev/null;
errors=$(jq -r 2>/dev/null || )
warnings=$(jq -r 2>/dev/null || )
issues=$(jq -r 2>/dev/null || )
total_errors=$((total_errors + errors))
total_warnings=$((total_warnings + warnings))
total_issues=$((total_issues + issues))
>>
>>
>>
>>
>>
[ -f ];
failed_list=$(grep | -d: -f2-)
[ -n ];
>>
| sed >>
>>
>>
>>
>>
temp_file=
>
report_file /*_report.json;
[ -f ];
file_name=$( _report.json).md
-v jq &> /dev/null;
errors=$(jq -r 2>/dev/null || )
warnings=$(jq -r 2>/dev/null || )
issues=$(jq -r 2>/dev/null || )
>>
[ -f ];
-t -k4 -nr | -10 | IFS= -r file errors warnings issues;
>>
>>
>>
>>
>>
>>
>>
log_info
}
() {
log_info
-f
-f
}
() {
log_info
log_info
log_info
create_output_dir
total_files=$(find_markdown_files)
[ -eq 0 ];
log_warn
0
failed_count=0
check_files_parallel
failed_count=$?
generate_summary_report
cleanup
[ -eq 0 ];
log_info
0
log_warn
1
}
() {
}
[ = ] || [ = ];
show_help
0
main
文档质量评分器
import re
import json
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
from collections import Counter
@dataclass
class QualityMetric:
"""质量指标"""
name: str
score: float
max_score: float
description: str
issues: List[str]
class DocumentQualityScorer:
"""文档质量评分器"""
def __init__(self, file_path: str):
self.file_path = Path(file_path)
self.content = ""
self.lines = []
self.metrics: List[QualityMetric] = []
self.weights = {
'structure': 0.25,
'content': 0.30,
'links': 0.20,
'formatting': 0.15,
'readability': 0.10
}
() -> :
._read_file():
{: }
._analyze_structure()
._analyze_content()
._analyze_links()
._analyze_formatting()
._analyze_readability()
total_score = ._calculate_total_score()
{
: (.file_path),
: total_score,
: ._get_grade(total_score),
: [
{
: metric.name,
: metric.score,
: metric.max_score,
: (metric.score / metric.max_score) * ,
: metric.description,
: metric.issues
}
metric .metrics
],
: ._get_recommendations()
}
() -> :
:
(.file_path, , encoding=) f:
.content = f.read()
.lines = .content.splitlines()
:
():
issues = []
score =
max_score =
headers = ._extract_headers()
headers:
issues.append()
:
has_h1 = (level == _, level, _ headers)
has_h1:
issues.append()
:
score +=
prev_level =
hierarchy_issues =
_, level, _ headers:
prev_level > level > prev_level + :
hierarchy_issues +=
prev_level = level
hierarchy_issues == :
score +=
hierarchy_issues <= :
score +=
issues.append()
:
issues.append()
._has_toc():
score +=
:
issues.append()
(headers) >= :
score +=
:
issues.append()
word_count = (.content.split())
word_count >= :
score +=
:
issues.append()
.metrics.append(QualityMetric(
name=,
score=score,
max_score=max_score,
description=,
issues=issues
))
():
issues = []
score =
max_score =
code_blocks = ._count_code_blocks()
code_blocks > :
score +=
:
issues.append()
images = ._count_images()
images > :
score +=
:
issues.append()
tables = ._count_tables()
tables > :
score +=
:
issues.append()
lists = ._count_lists()
lists > :
score +=
:
issues.append()
._has_todo_comments():
score +=
:
issues.append()
desc_lines = ( line .lines (line.strip()) > )
total_lines = ([line line .lines line.strip()])
total_lines > desc_lines / total_lines > :
score +=
:
issues.append()
.metrics.append(QualityMetric(
name=,
score=score,
max_score=max_score,
description=,
issues=issues
))
():
issues = []
score =
max_score =
links = ._extract_links()
links:
issues.append()
.metrics.append(QualityMetric(
name=,
score=,
max_score=max_score,
description=,
issues=issues
))
(links) >= :
score +=
(links) >= :
score +=
:
issues.append()
link_types = (link.link_type link links)
(link_types) >= :
score +=
(link_types) >= :
score +=
:
issues.append()
anchor_links = [link link links link.link_type == ]
anchor_links:
score +=
:
issues.append()
external_links = [link link links link.link_type == ]
external_links:
score +=
:
issues.append()
.metrics.append(QualityMetric(
name=,
score=score,
max_score=max_score,
description=,
issues=issues
))
():
issues = []
score =
max_score =
._has_proper_code_blocks():
score +=
:
issues.append()
._has_proper_lists():
score +=
:
issues.append()
._has_proper_tables():
score +=
:
issues.append()
._has_proper_spacing():
score +=
:
issues.append()
.metrics.append(QualityMetric(
name=,
score=score,
max_score=max_score,
description=,
issues=issues
))
():
issues = []
score =
max_score =
sentences = re.split(, .content)
sentences = [s.strip() s sentences s.strip()]
sentences:
avg_sentence_length = ((s.split()) s sentences) / (sentences)
<= avg_sentence_length <= :
score +=
< avg_sentence_length <= :
score +=
issues.append()
:
issues.append()
paragraphs = [p.strip() p .content.split() p.strip()]
long_paragraphs = ( p paragraphs (p.split()) > )
long_paragraphs == :
score +=
long_paragraphs <= :
score +=
:
issues.append()
complex_words = ._count_complex_words()
total_words = (.content.split())
total_words > :
complex_ratio = complex_words / total_words
complex_ratio <= :
score +=
complex_ratio <= :
score +=
:
issues.append()
._has_excessive_repetition():
score +=
:
issues.append()
.metrics.append(QualityMetric(
name=,
score=score,
max_score=max_score,
description=,
issues=issues
))
() -> [[, , ]]:
headers = []
line_num, line (.lines, ):
= re.(, line)
:
level = (.group())
text = .group().strip()
headers.append((text, level, line_num))
headers
() -> [LinkInfo]:
links = []
line_num, line (.lines, ):
link_pattern =
re.finditer(link_pattern, line):
text = .group()
url = .group()
column = .start() +
link_type = ._determine_link_type(url)
links.append(LinkInfo(
text=text,
url=url,
line_number=line_num,
column=column,
link_type=link_type
))
image_pattern =
re.finditer(image_pattern, line):
alt = .group()
url = .group()
column = .start() +
links.append(LinkInfo(
text=alt,
url=url,
line_number=line_num,
column=column,
link_type=
))
links
() -> :
url.startswith():
url.startswith() url.startswith():
url.startswith():
:
() -> :
toc_indicators = [, , , , ]
(indicator .content indicator toc_indicators)
() -> :
todo_patterns = [, , , , ]
(re.search(pattern, .content, re.IGNORECASE) pattern todo_patterns)
() -> :
(re.findall(, .content))
() -> :
(re.findall(, .content))
() -> :
table_lines = [line line .lines line]
([line line table_lines line.strip().startswith()])
() -> :
([line line .lines re.(, line) re.(, line)])
() -> :
fences = re.findall(, .content, re.MULTILINE)
(fences) % ==
() -> :
() -> :
() -> :
header_lines = [i i, line (.lines) re.(, line)]
proper_spacing =
line_num header_lines:
line_num > .lines[line_num - ].strip() == :
proper_spacing +=
(header_lines) > proper_spacing / (header_lines) >
() -> :
words = re.findall(, .content.lower())
([word word words (word) > ])
() -> :
words = re.findall(, .content.lower())
(words) < :
word_counts = Counter(words)
most_common = word_counts.most_common()
most_common[][] / (words) >
() -> :
total_score =
total_weight =
metric .metrics:
category =
metric.name:
category =
metric.name:
category =
metric.name:
category =
metric.name:
category =
metric.name:
category =
category category .weights:
weight = .weights[category]
percentage = metric.score / metric.max_score
total_score += percentage * weight *
total_weight += weight
total_score total_weight >
() -> :
score >= :
score >= :
score >= :
score >= :
:
() -> []:
recommendations = []
metric .metrics:
metric.score < metric.max_score * :
metric.name:
recommendations.append()
metric.name:
recommendations.append()
metric.name:
recommendations.append()
metric.name:
recommendations.append()
metric.name:
recommendations.append()
recommendations
__name__ == :
argparse
parser = argparse.ArgumentParser(description=)
parser.add_argument(, =)
parser.add_argument(, =)
parser.add_argument(, choices=[, ], default=, =)
args = parser.parse_args()
scorer = DocumentQualityScorer(args.file)
:
result = scorer.score_document()
args. == :
args.output:
(args.output, , encoding=) f:
json.dump(result, f, indent=, ensure_ascii=)
()
:
(json.dumps(result, indent=, ensure_ascii=))
:
result:
()
exit()
( * )
()
( * )
()
()
()
()
()
metric result[]:
()
()
()
metric[]:
()
issue metric[]:
()
()
result[]:
()
i, rec (result[], ):
()
()
grade_descriptions = {
: ,
: ,
: ,
: ,
:
}
()
()
Exception e:
()
exit()
最佳实践
文档结构
- 清晰层级: 使用合理的标题层级结构
- 完整目录: 提供详细的目录导航
- 章节平衡: 各章节内容长度适中
- 逻辑顺序: 内容按逻辑顺序组织
内容质量
- 代码示例: 提供实用的代码示例
- 图片说明: 使用图片增强理解
- 表格数据: 用表格展示结构化信息
- 实例演示: 包含实际使用案例
链接管理
- 内部链接: 建立文档间的关联
- 外部参考: 提供相关资源链接
- 锚点导航: 使用锚点方便跳转
- 定期检查: 定期验证链接有效性
格式规范
- 一致性: 保持格式风格一致
- 可读性: 注重阅读体验
- 标准化: 遵循Markdown标准
- 兼容性: 确保多平台兼容
相关技能