|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Resolve relative links in generated Markdown (.md) output to absolute URLs. |
| 3 | +
|
| 4 | +Hugo emits the per-page Markdown output as public/<path>/index.md (pretty URLs) carrying the |
| 5 | +page's RAW content. The auto-generated API-reference pages use RELATIVE links, e.g. |
| 6 | +`](../ns/class)` or `](../../ns/class)` (variable depth), which Hugo templates cannot resolve |
| 7 | +and which break once the .md is flattened to an ugly URL by move_md_to_ugly_urls.sh (the page |
| 8 | +depth changes). This script resolves each relative link against the page's own URL and rewrites |
| 9 | +it to an absolute URL, matching the theme partial themes/docs/layouts/partials/md/abs-content.txt |
| 10 | +(which already absolutizes ROOT-relative links). The two are disjoint: abs-content.txt handles |
| 11 | +`/...` links, this script handles `../`, `./`, and bare relative links. |
| 12 | +
|
| 13 | +Run it BEFORE move_md_to_ugly_urls.sh: resolution relies on the pretty path |
| 14 | +public/<a>/<b>/index.md => page URL /<a>/<b>/. |
| 15 | +
|
| 16 | +Usage: |
| 17 | + python resolve_md_links.py [output_dir] --base-url <BASE> |
| 18 | +
|
| 19 | + output_dir Hugo output directory (default: "public"). |
| 20 | + --base-url The build's Hugo .Site.BaseURL, e.g. |
| 21 | + production: https://reference.groupdocs.com/<product> |
| 22 | + staging: /<product> |
| 23 | + local: http://localhost:1313/<product> |
| 24 | + (Trailing slash is ignored.) |
| 25 | +""" |
| 26 | +import argparse |
| 27 | +import os |
| 28 | +import posixpath |
| 29 | +import re |
| 30 | +import sys |
| 31 | + |
| 32 | +# Targets that are already absolute / not page-relative -> leave untouched. |
| 33 | +# Root-relative ("/...") links are handled by the theme's md/abs-content.txt. |
| 34 | +SKIP_PREFIXES = ("http://", "https://", "//", "mailto:", "tel:", "data:", "#", "/") |
| 35 | + |
| 36 | +# Final-segment extensions treated as assets (NOT given a .md suffix). Namespace segments |
| 37 | +# like "com.groupdocs.annotation" contain dots but are pages, so use an explicit whitelist |
| 38 | +# rather than "has any extension". |
| 39 | +ASSET_EXTS = { |
| 40 | + ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico", |
| 41 | + ".css", ".js", ".json", ".xml", ".txt", ".pdf", ".zip", ".csv", |
| 42 | +} |
| 43 | + |
| 44 | +# Markdown inline/image link target: ](target) — also matches the image form . |
| 45 | +MD_LINK_RE = re.compile(r"\]\(([^)\s]+)\)") |
| 46 | +# Raw-HTML attribute: src="target" / href="target" (also matches the href of xlink:href=). |
| 47 | +HTML_ATTR_RE = re.compile(r'(\b(?:src|href)=")([^"]*)(")') |
| 48 | + |
| 49 | + |
| 50 | +def is_relative(target): |
| 51 | + return bool(target) and not target.startswith(SKIP_PREFIXES) |
| 52 | + |
| 53 | + |
| 54 | +def resolve(target, url_dir, base): |
| 55 | + """Resolve a page-relative target against url_dir and return an absolute URL.""" |
| 56 | + hash_i = target.find("#") |
| 57 | + if hash_i != -1: |
| 58 | + path, anchor = target[:hash_i], target[hash_i:] |
| 59 | + else: |
| 60 | + path, anchor = target, "" |
| 61 | + if not path: |
| 62 | + return target |
| 63 | + resolved = posixpath.normpath(posixpath.join(url_dir, path)) |
| 64 | + ext = posixpath.splitext(posixpath.basename(resolved))[1].lower() |
| 65 | + if ext not in ASSET_EXTS: |
| 66 | + resolved += ".md" # page link -> .md sibling |
| 67 | + return base + resolved + anchor |
| 68 | + |
| 69 | + |
| 70 | +def rewrite(content, url_dir, base): |
| 71 | + def md_sub(m): |
| 72 | + t = m.group(1) |
| 73 | + return "](" + resolve(t, url_dir, base) + ")" if is_relative(t) else m.group(0) |
| 74 | + |
| 75 | + def html_sub(m): |
| 76 | + t = m.group(2) |
| 77 | + return m.group(1) + resolve(t, url_dir, base) + m.group(3) if is_relative(t) else m.group(0) |
| 78 | + |
| 79 | + return HTML_ATTR_RE.sub(html_sub, MD_LINK_RE.sub(md_sub, content)) |
| 80 | + |
| 81 | + |
| 82 | +def main(): |
| 83 | + ap = argparse.ArgumentParser(description="Resolve relative .md links to absolute URLs.") |
| 84 | + ap.add_argument("output_dir", nargs="?", default="public") |
| 85 | + ap.add_argument("--base-url", required=True, |
| 86 | + help="Build .Site.BaseURL, e.g. https://reference.groupdocs.com/annotation or /annotation") |
| 87 | + args = ap.parse_args() |
| 88 | + |
| 89 | + out = args.output_dir |
| 90 | + base = args.base_url.rstrip("/") |
| 91 | + if not os.path.isdir(out): |
| 92 | + sys.exit("Error: directory '%s' does not exist. Run 'hugo' first." % out) |
| 93 | + |
| 94 | + changed = 0 |
| 95 | + for root, _dirs, files in os.walk(out): |
| 96 | + for fn in files: |
| 97 | + if not fn.endswith(".md"): |
| 98 | + continue |
| 99 | + path = os.path.join(root, fn) |
| 100 | + # Page URL dir from the file's location relative to out: |
| 101 | + # public/java/ns/class/index.md -> /java/ns/class/ |
| 102 | + # public/index.md -> / |
| 103 | + rel = os.path.relpath(os.path.dirname(path), out).replace(os.sep, "/") |
| 104 | + url_dir = "/" if rel == "." else "/" + rel + "/" |
| 105 | + with open(path, "r", encoding="utf-8") as f: |
| 106 | + content = f.read() |
| 107 | + new = rewrite(content, url_dir, base) |
| 108 | + if new != content: |
| 109 | + with open(path, "w", encoding="utf-8", newline="\n") as f: |
| 110 | + f.write(new) |
| 111 | + changed += 1 |
| 112 | + |
| 113 | + print("Resolved relative links in %d .md file(s) under '%s'." % (changed, out)) |
| 114 | + |
| 115 | + |
| 116 | +if __name__ == "__main__": |
| 117 | + main() |
0 commit comments