From 59f3b928fe789a674b479f4df968697844499393 Mon Sep 17 00:00:00 2001 From: Mike Odnis Date: Sat, 9 May 2026 12:49:11 -0400 Subject: [PATCH] fix(template/dotnet): switch link-prefix step to paren-aware Python walker 995 broken-link errors on resq-software/docs#28 because the sed regex did not match DefaultDocumentation's link forms: ](ResQ.Blockchain.md 'qualified.Type') The trailing space + 'title' before `)` falls outside the `[^)/]*` URL character class, so the regex skipped these links entirely and they kept their bare-filename form. Mintlify rejected all 995 of them. Replace the sed step with a paren-balanced Python walker (same shape used elsewhere in this template family). It correctly handles: - bare `(path.md)` - `(path.md#anchor)` - `(path.md 'title')` and double-quoted variants - method-overload pages with parens in the filename, e.g. `Foo.Bar(string,int).md` Sync PR will reapply the same fix to dotnet-sdk:main. --- .../source-repo-templates/api-docs.dotnet.yml | 117 ++++++++++++++++-- 1 file changed, 109 insertions(+), 8 deletions(-) diff --git a/automation/source-repo-templates/api-docs.dotnet.yml b/automation/source-repo-templates/api-docs.dotnet.yml index 40c2b012..e3d7d616 100644 --- a/automation/source-repo-templates/api-docs.dotnet.yml +++ b/automation/source-repo-templates/api-docs.dotnet.yml @@ -176,16 +176,117 @@ jobs: } > "$OUTPUT_DIR/README.md" - name: Prefix bare-filename intra-page links with ./ - # Same pattern as the TypeScript template: Mintlify rejects - # bare-filename .md links as broken; prefixing with ./ - # forces the resolver to treat them as relative paths. + # Mintlify rejects bare-filename .md links as broken; + # prefixing with ./ makes the resolver treat them as + # relative paths. + # + # Cannot use a plain sed regex here because DefaultDocumentation + # output diverges from the simple `[text](path.md)` form on two + # axes: + # 1. Link titles: `[text](path.md 'qualified.Type')` — + # sed regex expects `)` immediately after `.md`. + # 2. Method-overload pages have parens in the filename, e.g. + # `Foo.Bar(string,int).md` — character classes that exclude + # `)` reject these outright. + # A paren-balanced Python walker handles both shapes; sed cannot + # without per-character state. working-directory: ${{ env.OUTPUT_DIR }} run: | - find . -type f -name '*.md' -print0 | while IFS= read -r -d '' f; do - sed -E -i \ - 's|\]\(([A-Za-z0-9][^)/]*\.md(#[^)]*)?)\)|](./\1)|g' \ - "$f" - done + python3 - <<'PY' + import pathlib + + def is_external(u: str) -> bool: + return u.startswith(( + "http://", "https://", "mailto:", "/", "#", "./", "../", + )) + + def transform_inline(line: str) -> str: + out, i, n = [], 0, len(line) + while i < n: + j = line.find("](", i) + if j == -1: + out.append(line[i:]) + break + out.append(line[i:j+2]) + i = j + 2 + # Walk URL: paren-balanced, ends at depth-0 ) or whitespace + depth, url_end, scan = 0, -1, i + while scan < n: + c = line[scan] + if c == "(": + depth += 1 + elif c == ")": + if depth == 0: + url_end = scan + break + depth -= 1 + elif c.isspace() and depth == 0: + url_end = scan + break + scan += 1 + if url_end == -1: + out.append(line[i:]) + break + url = line[i:url_end] + # Find matching outer ), tracking quote/depth for titles + scan = url_end + in_quote, depth = None, 0 + while scan < n: + c = line[scan] + if in_quote: + if c == in_quote: + in_quote = None + scan += 1 + continue + if c in ("'", '"'): + in_quote = c + scan += 1 + continue + if c == "(": + depth += 1 + elif c == ")": + if depth == 0: + break + depth -= 1 + scan += 1 + if scan >= n: + out.append(line[i:]) + break + rest = line[url_end:scan] + url_no_anchor = url.partition("#")[0] + if url and not is_external(url) and url_no_anchor.endswith(".md"): + url = "./" + url + out.append(url) + out.append(rest) + out.append(")") + i = scan + 1 + return "".join(out) + + def transform(text: str) -> str: + out, in_fence = [], False + for raw in text.splitlines(keepends=True): + if raw.endswith("\r\n"): + line, eol = raw[:-2], "\r\n" + elif raw.endswith("\n"): + line, eol = raw[:-1], "\n" + else: + line, eol = raw, "" + s = line.lstrip() + if s.startswith("```") or s.startswith("~~~"): + in_fence = not in_fence + out.append(line); out.append(eol); continue + if in_fence: + out.append(line); out.append(eol); continue + out.append(transform_inline(line)) + out.append(eol) + return "".join(out) + + for p in pathlib.Path(".").rglob("*.md"): + orig = p.read_text(encoding="utf-8") + new = transform(orig) + if new != orig: + p.write_text(new, encoding="utf-8") + PY - name: Escape curly braces outside code regions (MDX safety) # Mintlify parses .md as MDX. Any literal `{ ... }` in prose