Skip to content

Commit 047aa6c

Browse files
Relative links to absolute in markdown
1 parent 6e53f1a commit 047aa6c

2 files changed

Lines changed: 123 additions & 0 deletions

File tree

‎.github/workflows/deploy_product.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ jobs:
4444
- name: Build
4545
run: hugo --configDir config/sites/groupdocs/${{ inputs.product_family }} --environment ${{ inputs.environment }} --minify
4646

47+
- name: Resolve relative .md links to absolute (must precede the ugly-URL rename)
48+
run: python3 resolve_md_links.py public --base-url "$BASE"
49+
env:
50+
# Match the build's .Site.BaseURL: full URL for production, root-relative for staging.
51+
BASE: ${{ inputs.environment == 'production' && format('https://reference.groupdocs.com/{0}', inputs.product_family) || format('/{0}', inputs.product_family) }}
52+
4753
- name: Convert .md pretty URLs to ugly (…/x/index.md -> …/x.md)
4854
run: |
4955
chmod +x ./move_md_to_ugly_urls.sh

‎resolve_md_links.py‎

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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 ![alt](target).
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

Comments
 (0)