Skip to content

Commit 9c27f9f

Browse files
authored
fix: skip inline markdown-pipeline entries (#58)
1 parent 61a24be commit 9c27f9f

8 files changed

Lines changed: 228 additions & 3 deletions

File tree

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@
1212
/test/sidebar-site/_site/
1313
/test/sidebar-site/.quarto/
1414
/test/sidebar-site/_extensions/
15+
/test/navbar-site/_site/
16+
/test/navbar-site/.quarto/
17+
/test/navbar-site/_extensions/
1518
__pycache__/
1619
/tools/.venv/

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Bug Fixes
66

77
- fix: Load the `string` module by its own path in the Bitbucket module, so another extension's module cannot be used instead. (#57)
8+
- fix: Keep a platform URL in a navigation link target, and a reference in the social metadata, as plain text. A reference in a navigation title or entry text stays plain text as well. (#58)
89

910
### Refactoring
1011

‎_extensions/gitlink/gitlink.lua‎

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ local COMMIT_SHA_MIN_LENGTH = 7
7575
--- @type string Lua pattern matching a 3-, 4-, 6-, or 8-character hex colour with leading #
7676
local HEX_COLOUR_PATTERN = '^#%x%x%x%x?%x?%x?%x?%x?$'
7777

78+
--- @type string Class Quarto puts on the markdown-pipeline envelope elements
79+
local MARKDOWN_ENVELOPE_CLASS = 'quarto-markdown-envelope-contents'
80+
7881
--- Validate a colour value as a hex code or CSS named colour.
7982
--- Returns the original value if valid, or nil if invalid.
8083
--- @param value string|nil The candidate colour value
@@ -1020,6 +1023,52 @@ local function process_link(elem)
10201023
return elem
10211024
end
10221025

1026+
--- Skip the content of an inline markdown-pipeline entry.
1027+
--- Quarto renders navigation hrefs and social metadata values as hidden inline
1028+
--- snippets in such a span, then reads the rendered fragment back with
1029+
--- `innerText` and puts the result in an attribute. A converted reference would
1030+
--- add its badge text to that value.
1031+
--- The same envelope also carries values that Quarto inserts with `innerHTML`,
1032+
--- such as a navbar or sidebar title, a navigation entry text, an `about` link
1033+
--- text, and a next or previous page title. A reference in those no longer
1034+
--- converts. The two kinds cannot be told apart, because a navigation entry
1035+
--- registers its text and its href under one identifier prefix, so the whole
1036+
--- envelope is skipped.
1037+
--- A `Div` with the same class is a block entry (page footer, margin and body
1038+
--- header and footer, announcement). Quarto inserts those with `innerHTML` as
1039+
--- well, and they are a separate envelope, so only spans are skipped.
1040+
--- @param span pandoc.Span The span element to inspect
1041+
--- @return pandoc.Span span The unchanged span
1042+
--- @return boolean|nil descend False to stop traversal of the subtree
1043+
local function skip_markdown_envelope(span)
1044+
if span.classes:includes(MARKDOWN_ENVELOPE_CLASS) then
1045+
return span, false
1046+
end
1047+
return span
1048+
end
1049+
1050+
--- Convert a string element and stop traversal of the result.
1051+
--- Top-down traversal descends into a returned element, so without this a
1052+
--- created link would have its own text converted a second time.
1053+
--- @param elem pandoc.Str The string element to process
1054+
--- @return pandoc.Str|pandoc.Link|pandoc.List The result of `process_gitlink`
1055+
--- @return boolean descend Always false
1056+
local function process_gitlink_topdown(elem)
1057+
return process_gitlink(elem), false
1058+
end
1059+
1060+
--- Turn element handlers into a top-down pass that skips envelope spans.
1061+
--- Each pass needs its own prune point, because the passes that create links
1062+
--- stay separate walks: `process_link` unwraps an autolink into a `Str` that the
1063+
--- later `Str` pass has to convert.
1064+
--- @param handlers table The element handlers for the pass
1065+
--- @return table The filter table for the pass
1066+
local function envelope_safe_pass(handlers)
1067+
handlers.traverse = 'topdown'
1068+
handlers.Span = skip_markdown_envelope
1069+
return handlers
1070+
end
1071+
10231072
--- Pandoc filter configuration
10241073
--- Defines the order of filter execution:
10251074
--- 1. Extract references from the document
@@ -1032,7 +1081,7 @@ return {
10321081
{ Pandoc = get_references },
10331082
{ Meta = get_repository },
10341083
{ Plain = process_inlines, Para = process_inlines },
1035-
{ Link = process_link },
1036-
{ Str = process_gitlink },
1037-
{ Cite = process_mentions }
1084+
envelope_safe_pass({ Link = process_link }),
1085+
envelope_safe_pass({ Str = process_gitlink_topdown }),
1086+
envelope_safe_pass({ Cite = process_mentions })
10381087
}

‎test/navbar-site/.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/.quarto/
2+
**/*.quarto_ipynb

‎test/navbar-site/_quarto.yml‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
project:
2+
type: website
3+
# Transient copy of the extension so the filter resolves like a real
4+
# installation; removed again after render. Entries run without a shell,
5+
# so one command per line, and the copy is an idempotent overlay so no
6+
# destructive command runs before the render.
7+
pre-render:
8+
- mkdir -p _extensions/gitlink
9+
- cp -R ../../_extensions/gitlink/. _extensions/gitlink/
10+
post-render:
11+
- rm -rf _extensions
12+
13+
website:
14+
title: "Gitlink Navbar Test"
15+
navbar:
16+
search: true
17+
left:
18+
- text: Home
19+
href: index.qmd
20+
# Bare platform user URL: the href must survive the filter unchanged.
21+
- text: "Profile"
22+
href: "https://github.com/mcanouil"
23+
# Two path segments and a query, so no pattern matches it.
24+
- text: "Sponsor"
25+
href: "https://github.com/sponsors/mcanouil?o=esb"
26+
- text: About
27+
href: about.qmd
28+
# Known loss: a nav entry text and a nav entry href share one envelope,
29+
# so this reference stays plain text instead of becoming a link.
30+
- text: "Issue #1"
31+
href: index.qmd
32+
tools:
33+
- icon: github
34+
href: "https://github.com/mcanouil"
35+
text: "GitHub"
36+
open-graph: true
37+
page-footer:
38+
center: |
39+
Footer references still convert: #1 and mcanouil/quarto-cli#2.
40+
41+
format:
42+
html:
43+
theme:
44+
light: flatly
45+
dark: darkly
46+
47+
filters:
48+
- gitlink
49+
50+
extensions:
51+
gitlink:
52+
platform: github
53+
repository-name: mcanouil/quarto-gitlink

‎test/navbar-site/about.qmd‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
title: "About"
3+
description: "A description that holds a reference to #1, which Quarto puts in a meta tag."
4+
about:
5+
template: jolla
6+
links:
7+
- icon: github
8+
text: "Profile"
9+
href: "https://github.com/mcanouil"
10+
---
11+
12+
The `about` links and the social metadata go through the same inline snippets as a navigation entry.
13+
14+
The `<meta property="og:description">` content must keep the plain text `#1`, without a platform badge.
15+
16+
The link under the title must keep `https://github.com/mcanouil` as its target.
17+
Quarto never substitutes an `about` link target, because it registers the target under one key and reads it back under another, so this one guards against a future change rather than a past defect.
18+
19+
The `<meta name="description">` tag is a separate case that this test does not cover.
20+
Pandoc builds it from the document metadata, not from an inline snippet, so it still takes the badge text.
21+
22+
Body text still converts, so #1 is a link here.

‎test/navbar-site/check.sh‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env bash
2+
# Render the navbar test site and check what the filter must and must not touch.
3+
set -euo pipefail
4+
5+
site_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
quarto render "${site_dir}" >/dev/null
7+
8+
index="${site_dir}/_site/index.html"
9+
about="${site_dir}/_site/about.html"
10+
failures=0
11+
12+
report() {
13+
local status="${1}" label="${2}"
14+
printf '%-4s %s\n' "${status}" "${label}"
15+
if [[ "${status}" == "FAIL" ]]; then
16+
failures=$((failures + 1))
17+
fi
18+
}
19+
20+
expect_present() {
21+
local label="${1}" file="${2}" text="${3}"
22+
if grep -qF -- "${text}" "${file}"; then
23+
report "ok" "${label}"
24+
else
25+
report "FAIL" "${label}"
26+
fi
27+
}
28+
29+
expect_absent() {
30+
local label="${1}" file="${2}" text="${3}"
31+
if grep -qF -- "${text}" "${file}"; then
32+
report "FAIL" "${label}"
33+
else
34+
report "ok" "${label}"
35+
fi
36+
}
37+
38+
expect_count() {
39+
local label="${1}" file="${2}" text="${3}" wanted="${4}" found
40+
found="$({ grep -oF -- "${text}" "${file}" || true; } | wc -l | tr -d ' ')"
41+
if [[ "${found}" -ge "${wanted}" ]]; then
42+
report "ok" "${label}"
43+
else
44+
report "FAIL" "${label} (found ${found}, wanted at least ${wanted})"
45+
fi
46+
}
47+
48+
# Targets that go through an inline markdown-pipeline entry keep their URL.
49+
expect_present "navbar item href" "${index}" \
50+
'<a class="nav-link" href="https://github.com/mcanouil">'
51+
expect_present "navbar tool href" "${index}" \
52+
'href="https://github.com/mcanouil" title="GitHub" class="quarto-navigation-tool'
53+
expect_present "about link href" "${about}" \
54+
'<a href="https://github.com/mcanouil" class="about-link"'
55+
expect_present "og:description stays plain" "${about}" \
56+
'property="og:description" content="A description that holds a reference to #1, which Quarto puts in a meta tag."'
57+
expect_absent "no link text used as a target" "${index}" 'href="@'
58+
expect_absent "no link text used as a target" "${about}" 'href="@'
59+
60+
# Body content and block entries still convert.
61+
expect_count "body and footer references convert" "${index}" \
62+
'href="https://github.com/mcanouil/quarto-gitlink/issues/1"' 2
63+
expect_present "reference converts on the about page" "${about}" \
64+
'href="https://github.com/mcanouil/quarto-gitlink/issues/1"'
65+
66+
# Known cost: a navigation entry text shares its envelope with the href.
67+
expect_present "navbar item text stays plain" "${index}" \
68+
'<span class="menu-text">Issue #1</span>'
69+
70+
if [[ "${failures}" -gt 0 ]]; then
71+
printf '\n%d check(s) failed.\n' "${failures}" >&2
72+
exit 1
73+
fi
74+
75+
printf '\nAll checks passed.\n'

‎test/navbar-site/index.qmd‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
title: "Navbar href test: home"
3+
---
4+
5+
This site checks that the filter does not rewrite the `href` of a navigation entry.
6+
7+
The navbar has a "Profile" item and a `tools` entry whose `href` is `https://github.com/mcanouil`.
8+
Both must keep that URL.
9+
Before the fix, each one took the link text and the platform badge text joined together as its target, because Quarto reads the rendered snippet back with `innerText`.
10+
11+
The "Sponsor" item points at `https://github.com/sponsors/mcanouil?o=esb`.
12+
It has two path segments and a query, so no pattern matches it, and it is a control for the other two.
13+
14+
The "Issue #1" item shows the cost of the fix.
15+
A navigation entry registers its text and its href under one identifier prefix, so the text keeps its reference as plain text.
16+
17+
Body text must still convert.
18+
The following are links: #1, mcanouil/quarto-cli#2, and <https://github.com/mcanouil>.
19+
20+
The page footer holds references as well, and those must still convert, because Quarto inserts block entries with `innerHTML`.

0 commit comments

Comments
 (0)