125 lines
3.9 KiB
Python
125 lines
3.9 KiB
Python
#!/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())
|