elemes/scripts/validate_docs.py

125 lines
3.9 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""
Validate docs/*.md files:
1. Setiap file .md wajib punya frontmatter YAML valid (title, order, category).
2. Cek broken internal link.
Jalankan di host (untuk dev loop) atau di container:
python scripts/validate_docs.py
"""
import os
import re
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DOCS_DIR = os.path.join(REPO_ROOT, "docs")
CONTENT_DIR = os.environ.get("CONTENT_DIR", os.path.join(REPO_ROOT, "content"))
_FRONTMATTER_RE = re.compile(r'^---\s*\n(.*?)\n---\s*\n', re.DOTALL)
errors = []
warnings = []
def check_frontmatter(filename, content):
"""Validate frontmatter exists and has required keys."""
fm_match = _FRONTMATTER_RE.match(content)
if not fm_match:
errors.append(f"{filename}: tidak ada frontmatter YAML (dibutuhkan: title, order, category)")
return
fm_text = fm_match.group(1)
keys = set()
for line in fm_text.splitlines():
if ":" in line:
key = line.split(":", 1)[0].strip()
keys.add(key)
required = {"title", "order", "category"}
missing = required - keys
if missing:
errors.append(f"{filename}: frontmatter kurang keys: {sorted(missing)}")
# Validate order is integer
order_match = re.search(r'^order:\s*(\d+)', fm_text, re.MULTILINE)
if not order_match:
errors.append(f"{filename}: order harus berupa angka")
def check_internal_links(filename, content):
"""Check for broken markdown links to other docs or content files."""
# Extract body (strip frontmatter)
fm_match = _FRONTMATTER_RE.match(content)
body = content[fm_match.end():] if fm_match else content
# Find all markdown links: [text](path)
links = re.findall(r'\[([^\]]+)\]\(([^)]+)\)', body)
for text, link in links:
# Skip external links and anchors
if link.startswith("http://") or link.startswith("https://"):
continue
if link.startswith("#"):
continue
if link.startswith("mailto:"):
continue
# Resolve relative to docs/ directory
# Only check links that point to docs/ files
link_path = link.lstrip("/")
if link_path.startswith("docs/"):
link_path = link_path[5:] # strip "docs/" prefix
# Skip links to source code, external paths, etc
if link_path.startswith("http") or link_path.startswith("."):
continue
target = os.path.join(DOCS_DIR, link_path)
if not os.path.exists(target):
# Only warn if it looks like it should be a docs file
if link_path.endswith(".md") or "/" in link_path:
warnings.append(f"{filename}: link mungnot '{link}' → '{link_path}' tidak ditemukan")
def main():
if not os.path.isdir(DOCS_DIR):
print(f"❌ Direktori docs tidak ditemukan: {DOCS_DIR}")
return 1
md_files = sorted(f for f in os.listdir(DOCS_DIR) if f.endswith(".md"))
if not md_files:
print(f"⚠️ Tidak ada file .md di {DOCS_DIR}")
return 0
for fname in md_files:
path = os.path.join(DOCS_DIR, fname)
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read()
except (OSError, PermissionError) as e:
errors.append(f"{fname}: tidak bisa dibaca: {e}")
continue
check_frontmatter(fname, content)
check_internal_links(fname, content)
print(f"\n=== Dokumen yang divalidasi: {len(md_files)} file ===")
if errors:
print(f"\n❌ {len(errors)} error:")
for e in errors:
print(f" - {e}")
return 1
if warnings:
print(f"\n⚠️ {len(warnings)} warning:")
for w in warnings:
print(f" - {w}")
print("\n✅ Semua file docs valid (frontmatter lengkap, tidak ada broken link internal)")
return 0
if __name__ == "__main__":
sys.exit(main())