172 lines
4.9 KiB
PowerShell
172 lines
4.9 KiB
PowerShell
param(
|
|
[string]$DocumentPath = (Join-Path $PSScriptRoot "..\http-client-app-plan.md"),
|
|
[string[]]$DisabledKeywords = @(
|
|
"<<<<<<<",
|
|
">>>>>>>",
|
|
"TODO_BLOCKER",
|
|
"FIXME_BLOCKER"
|
|
)
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$resolvedDocument = Resolve-Path -LiteralPath $DocumentPath
|
|
$python = Get-Command python -ErrorAction SilentlyContinue
|
|
$usePyLauncher = $false
|
|
|
|
if (-not $python) {
|
|
$python = Get-Command py -ErrorAction SilentlyContinue
|
|
$usePyLauncher = $true
|
|
}
|
|
|
|
if (-not $python) {
|
|
Write-Error "Python was not found. Install Python 3 or make the 'python'/'py' command available on PATH."
|
|
}
|
|
|
|
$checker = @'
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def line_col(text, offset):
|
|
line = text.count("\n", 0, offset) + 1
|
|
last_newline = text.rfind("\n", 0, offset)
|
|
column = offset + 1 if last_newline == -1 else offset - last_newline
|
|
return line, column
|
|
|
|
|
|
def check_code_fences(text):
|
|
errors = []
|
|
fence_pattern = re.compile(r"^[ \t]*```", re.MULTILINE)
|
|
matches = list(fence_pattern.finditer(text))
|
|
if len(matches) % 2 != 0:
|
|
line, _ = line_col(text, matches[-1].start())
|
|
errors.append(f"Unbalanced fenced code block near line {line}.")
|
|
return errors
|
|
|
|
|
|
def check_json_blocks(text):
|
|
errors = []
|
|
block_pattern = re.compile(r"^[ \t]*```json[^\n]*\n(.*?)(?:\n^[ \t]*```[ \t]*$)", re.MULTILINE | re.DOTALL)
|
|
for index, match in enumerate(block_pattern.finditer(text), start=1):
|
|
raw_json = match.group(1)
|
|
try:
|
|
json.loads(raw_json)
|
|
except json.JSONDecodeError as exc:
|
|
start_line, _ = line_col(text, match.start(1))
|
|
errors.append(
|
|
f"Invalid JSON code block #{index} at line {start_line + exc.lineno - 1}, "
|
|
f"column {exc.colno}: {exc.msg}."
|
|
)
|
|
return errors
|
|
|
|
|
|
def is_table_separator(line):
|
|
stripped = line.strip()
|
|
if "|" not in stripped:
|
|
return False
|
|
cells = [cell.strip() for cell in stripped.strip("|").split("|")]
|
|
return bool(cells) and all(re.fullmatch(r":?-{3,}:?", cell or "") for cell in cells)
|
|
|
|
|
|
def pipe_count(line):
|
|
stripped = line.strip()
|
|
return stripped.count("|")
|
|
|
|
|
|
def check_tables(text):
|
|
errors = []
|
|
lines = text.splitlines()
|
|
in_fence = False
|
|
for index, line in enumerate(lines):
|
|
if re.match(r"^[ \t]*```", line):
|
|
in_fence = not in_fence
|
|
continue
|
|
if in_fence or not is_table_separator(line):
|
|
continue
|
|
|
|
table_lines = []
|
|
cursor = index - 1
|
|
while cursor >= 0 and "|" in lines[cursor]:
|
|
table_lines.insert(0, (cursor + 1, lines[cursor]))
|
|
cursor -= 1
|
|
table_lines.append((index + 1, line))
|
|
cursor = index + 1
|
|
while cursor < len(lines) and "|" in lines[cursor]:
|
|
table_lines.append((cursor + 1, lines[cursor]))
|
|
cursor += 1
|
|
|
|
counts = {pipe_count(table_line) for _, table_line in table_lines}
|
|
if len(counts) > 1:
|
|
first_line = table_lines[0][0]
|
|
errors.append(f"Markdown table pipe count mismatch near line {first_line}.")
|
|
return errors
|
|
|
|
|
|
def check_disabled_keywords(text, keywords):
|
|
errors = []
|
|
for keyword in keywords:
|
|
if not keyword:
|
|
continue
|
|
offset = text.find(keyword)
|
|
if offset != -1:
|
|
line, column = line_col(text, offset)
|
|
errors.append(f"Disabled keyword '{keyword}' found at line {line}, column {column}.")
|
|
return errors
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Verify Markdown planning document structure.")
|
|
parser.add_argument("document")
|
|
parser.add_argument("--keyword", action="append", default=[])
|
|
args = parser.parse_args()
|
|
|
|
document = Path(args.document)
|
|
text = document.read_text(encoding="utf-8-sig")
|
|
|
|
errors = []
|
|
errors.extend(check_code_fences(text))
|
|
errors.extend(check_json_blocks(text))
|
|
errors.extend(check_tables(text))
|
|
errors.extend(check_disabled_keywords(text, args.keyword))
|
|
|
|
if errors:
|
|
print(f"Documentation verification failed for {document}:")
|
|
for error in errors:
|
|
print(f" - {error}")
|
|
return 1
|
|
|
|
print(f"Documentation verification passed for {document}.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|
|
'@
|
|
|
|
$tempChecker = Join-Path ([System.IO.Path]::GetTempPath()) ("verify-docs-{0}.py" -f ([System.Guid]::NewGuid().ToString("N")))
|
|
|
|
try {
|
|
Set-Content -LiteralPath $tempChecker -Value $checker -Encoding UTF8
|
|
|
|
$arguments = @()
|
|
if ($usePyLauncher) {
|
|
$arguments += "-3"
|
|
}
|
|
$arguments += @($tempChecker, $resolvedDocument.Path)
|
|
foreach ($keyword in $DisabledKeywords) {
|
|
$arguments += @("--keyword", $keyword)
|
|
}
|
|
|
|
& $python.Source @arguments
|
|
exit $LASTEXITCODE
|
|
}
|
|
finally {
|
|
if (Test-Path -LiteralPath $tempChecker) {
|
|
Remove-Item -LiteralPath $tempChecker -Force
|
|
}
|
|
}
|