Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@
/test/sidebar-site/_site/
/test/sidebar-site/.quarto/
/test/sidebar-site/_extensions/
/test/navbar-site/_site/
/test/navbar-site/.quarto/
/test/navbar-site/_extensions/
__pycache__/
/tools/.venv/
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Bug Fixes

- fix: Load the `string` module by its own path in the Bitbucket module, so another extension's module cannot be used instead. (#57)
- 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)

### Refactoring

Expand Down
55 changes: 52 additions & 3 deletions _extensions/gitlink/gitlink.lua
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ local COMMIT_SHA_MIN_LENGTH = 7
--- @type string Lua pattern matching a 3-, 4-, 6-, or 8-character hex colour with leading #
local HEX_COLOUR_PATTERN = '^#%x%x%x%x?%x?%x?%x?%x?$'

--- @type string Class Quarto puts on the markdown-pipeline envelope elements
local MARKDOWN_ENVELOPE_CLASS = 'quarto-markdown-envelope-contents'

--- Validate a colour value as a hex code or CSS named colour.
--- Returns the original value if valid, or nil if invalid.
--- @param value string|nil The candidate colour value
Expand Down Expand Up @@ -1020,6 +1023,52 @@ local function process_link(elem)
return elem
end

--- Skip the content of an inline markdown-pipeline entry.
--- Quarto renders navigation hrefs and social metadata values as hidden inline
--- snippets in such a span, then reads the rendered fragment back with
--- `innerText` and puts the result in an attribute. A converted reference would
--- add its badge text to that value.
--- The same envelope also carries values that Quarto inserts with `innerHTML`,
--- such as a navbar or sidebar title, a navigation entry text, an `about` link
--- text, and a next or previous page title. A reference in those no longer
--- converts. The two kinds cannot be told apart, because a navigation entry
--- registers its text and its href under one identifier prefix, so the whole
--- envelope is skipped.
--- A `Div` with the same class is a block entry (page footer, margin and body
--- header and footer, announcement). Quarto inserts those with `innerHTML` as
--- well, and they are a separate envelope, so only spans are skipped.
--- @param span pandoc.Span The span element to inspect
--- @return pandoc.Span span The unchanged span
--- @return boolean|nil descend False to stop traversal of the subtree
local function skip_markdown_envelope(span)
if span.classes:includes(MARKDOWN_ENVELOPE_CLASS) then
return span, false
end
return span
end

--- Convert a string element and stop traversal of the result.
--- Top-down traversal descends into a returned element, so without this a
--- created link would have its own text converted a second time.
--- @param elem pandoc.Str The string element to process
--- @return pandoc.Str|pandoc.Link|pandoc.List The result of `process_gitlink`
--- @return boolean descend Always false
local function process_gitlink_topdown(elem)
return process_gitlink(elem), false
end

--- Turn element handlers into a top-down pass that skips envelope spans.
--- Each pass needs its own prune point, because the passes that create links
--- stay separate walks: `process_link` unwraps an autolink into a `Str` that the
--- later `Str` pass has to convert.
--- @param handlers table The element handlers for the pass
--- @return table The filter table for the pass
local function envelope_safe_pass(handlers)
handlers.traverse = 'topdown'
handlers.Span = skip_markdown_envelope
return handlers
end

--- Pandoc filter configuration
--- Defines the order of filter execution:
--- 1. Extract references from the document
Expand All @@ -1032,7 +1081,7 @@ return {
{ Pandoc = get_references },
{ Meta = get_repository },
{ Plain = process_inlines, Para = process_inlines },
{ Link = process_link },
{ Str = process_gitlink },
{ Cite = process_mentions }
envelope_safe_pass({ Link = process_link }),
envelope_safe_pass({ Str = process_gitlink_topdown }),
envelope_safe_pass({ Cite = process_mentions })
}
2 changes: 2 additions & 0 deletions test/navbar-site/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/.quarto/
**/*.quarto_ipynb
53 changes: 53 additions & 0 deletions test/navbar-site/_quarto.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
project:
type: website
# Transient copy of the extension so the filter resolves like a real
# installation; removed again after render. Entries run without a shell,
# so one command per line, and the copy is an idempotent overlay so no
# destructive command runs before the render.
pre-render:
- mkdir -p _extensions/gitlink
- cp -R ../../_extensions/gitlink/. _extensions/gitlink/
post-render:
- rm -rf _extensions

website:
title: "Gitlink Navbar Test"
navbar:
search: true
left:
- text: Home
href: index.qmd
# Bare platform user URL: the href must survive the filter unchanged.
- text: "Profile"
href: "https://github.com/mcanouil"
# Two path segments and a query, so no pattern matches it.
- text: "Sponsor"
href: "https://github.com/sponsors/mcanouil?o=esb"
- text: About
href: about.qmd
# Known loss: a nav entry text and a nav entry href share one envelope,
# so this reference stays plain text instead of becoming a link.
- text: "Issue #1"
href: index.qmd
tools:
- icon: github
href: "https://github.com/mcanouil"
text: "GitHub"
open-graph: true
page-footer:
center: |
Footer references still convert: #1 and mcanouil/quarto-cli#2.

format:
html:
theme:
light: flatly
dark: darkly

filters:
- gitlink

extensions:
gitlink:
platform: github
repository-name: mcanouil/quarto-gitlink
22 changes: 22 additions & 0 deletions test/navbar-site/about.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: "About"
description: "A description that holds a reference to #1, which Quarto puts in a meta tag."
about:
template: jolla
links:
- icon: github
text: "Profile"
href: "https://github.com/mcanouil"
---

The `about` links and the social metadata go through the same inline snippets as a navigation entry.

The `<meta property="og:description">` content must keep the plain text `#1`, without a platform badge.

The link under the title must keep `https://github.com/mcanouil` as its target.
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.

The `<meta name="description">` tag is a separate case that this test does not cover.
Pandoc builds it from the document metadata, not from an inline snippet, so it still takes the badge text.

Body text still converts, so #1 is a link here.
75 changes: 75 additions & 0 deletions test/navbar-site/check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# Render the navbar test site and check what the filter must and must not touch.
set -euo pipefail

site_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
quarto render "${site_dir}" >/dev/null

index="${site_dir}/_site/index.html"
about="${site_dir}/_site/about.html"
failures=0

report() {
local status="${1}" label="${2}"
printf '%-4s %s\n' "${status}" "${label}"
if [[ "${status}" == "FAIL" ]]; then
failures=$((failures + 1))
fi
}

expect_present() {
local label="${1}" file="${2}" text="${3}"
if grep -qF -- "${text}" "${file}"; then
report "ok" "${label}"
else
report "FAIL" "${label}"
fi
}

expect_absent() {
local label="${1}" file="${2}" text="${3}"
if grep -qF -- "${text}" "${file}"; then
report "FAIL" "${label}"
else
report "ok" "${label}"
fi
}

expect_count() {
local label="${1}" file="${2}" text="${3}" wanted="${4}" found
found="$({ grep -oF -- "${text}" "${file}" || true; } | wc -l | tr -d ' ')"
if [[ "${found}" -ge "${wanted}" ]]; then
report "ok" "${label}"
else
report "FAIL" "${label} (found ${found}, wanted at least ${wanted})"
fi
}

# Targets that go through an inline markdown-pipeline entry keep their URL.
expect_present "navbar item href" "${index}" \
'<a class="nav-link" href="https://github.com/mcanouil">'
expect_present "navbar tool href" "${index}" \
'href="https://github.com/mcanouil" title="GitHub" class="quarto-navigation-tool'
expect_present "about link href" "${about}" \
'<a href="https://github.com/mcanouil" class="about-link"'
expect_present "og:description stays plain" "${about}" \
'property="og:description" content="A description that holds a reference to #1, which Quarto puts in a meta tag."'
expect_absent "no link text used as a target" "${index}" 'href="@'
expect_absent "no link text used as a target" "${about}" 'href="@'

# Body content and block entries still convert.
expect_count "body and footer references convert" "${index}" \
'href="https://github.com/mcanouil/quarto-gitlink/issues/1"' 2
expect_present "reference converts on the about page" "${about}" \
'href="https://github.com/mcanouil/quarto-gitlink/issues/1"'

# Known cost: a navigation entry text shares its envelope with the href.
expect_present "navbar item text stays plain" "${index}" \
'<span class="menu-text">Issue #1</span>'

if [[ "${failures}" -gt 0 ]]; then
printf '\n%d check(s) failed.\n' "${failures}" >&2
exit 1
fi

printf '\nAll checks passed.\n'
20 changes: 20 additions & 0 deletions test/navbar-site/index.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: "Navbar href test: home"
---

This site checks that the filter does not rewrite the `href` of a navigation entry.

The navbar has a "Profile" item and a `tools` entry whose `href` is `https://github.com/mcanouil`.
Both must keep that URL.
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`.

The "Sponsor" item points at `https://github.com/sponsors/mcanouil?o=esb`.
It has two path segments and a query, so no pattern matches it, and it is a control for the other two.

The "Issue #1" item shows the cost of the fix.
A navigation entry registers its text and its href under one identifier prefix, so the text keeps its reference as plain text.

Body text must still convert.
The following are links: #1, mcanouil/quarto-cli#2, and <https://github.com/mcanouil>.

The page footer holds references as well, and those must still convert, because Quarto inserts block entries with `innerHTML`.