Resolve semantic identifiers in PDF text macro links#23138
Conversation
https://community.openproject.org/wp/74366 Basic report adaptions
https://community.openproject.org/wp/74366 And also improve rendering of linked work packages for this. Before, the macros were not expanded, but only the hashes and ID output directly.
In semantic mode, the PDF text-macro handler resolves `#PROJ-1`, `##PROJ-1`, and `###PROJ-1` through `find_by_display_id` and emits a `<mention>` whose `data-id` carries the display id straight through to the PDF renderer. Cache misses (unknown identifier, deleted WP) fall through to literal text. Historical aliases resolve to the current identifier. Classic mode rejects semantic-shape input — `#PROJ-1` in a classic-mode export stays literal. Numeric input keeps its current path: the matcher emits the mention from the matched id without a DB lookup. `render_link` HTML-escapes both interpolated values. The matcher regex (`ID_ROUTE_CONSTRAINT`) already constrains shape to `\d+|[A-Z][A-Z0-9_]*-\d+`, so the escape is defence-in-depth: a future caller bypassing the matcher cannot regress into HTML attribute injection. The fast-path filter `Links.applicable?` matches `/#[A-Z\d]/` so semantic-only bodies reach the full regex. Downstream `Exports::PDF::Common::Markdown#wp_mention_macro` is updated to `find_by_display_id` in PR 23093. This commit must land after that PR — otherwise its existing `id[/\d+/]; find_by(id:)` extracts the trailing digits from `data-id="PROJ-7"` and resolves to WP id 7.
This was referenced May 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ticket
https://community.openproject.org/wp/74766
Note
This branch sits on top of two in-flight PRs:
find_by_display_id. Without it,data-id="PROJ-7"resolves to WP id 7.What are you trying to accomplish?
Render
#PROJ-1,##PROJ-1, and###PROJ-1references inside markdown descriptions as proper mentions in PDF exports. In semantic mode the macro handler resolves semantic input throughfind_by_display_idand emits a<mention>whosedata-idcarries the user-facing identifier. Level 1 produces a clickablePROJ-1link, level 2 expands inline toTask PROJ-1: <subject>, level 3 to<status> Task PROJ-1: <subject>.Cache misses (unknown identifier, deleted WP) fall through to literal text. Historical aliases resolve to the current identifier —
#OLDPROJ-1written in pre-rename content renders asdata-id="NEWPROJ-1". Classic mode rejects semantic-shape input entirely;#PROJ-1in a classic export stays literal. Numeric input renders the mention from the matched id with no DB lookup.The fast-path filter matches
/#[A-Z\d]/so semantic-only bodies (no numeric refs) still reach the full regex.Screenshots
Before — semantic refs render as literal text:
After — semantic refs resolve to mentions with type/subject expansion:
What approach did you choose and why?
The macro layer mirrors the in-app
WorkPackageslink handler — same mode gate, same predicates, same end-to-end design wheredata-idcarriesdisplay_id. PDF rendering walks Markly nodes directly rather than going throughPatternMatcherFilter's preload pipeline, so each semantic reference does its ownfind_by_display_idround-trip. Acceptable for short bodies; preload optimisation is tracked separately if profiling shows N+1 hurts large exports.render_linkHTML-escapes the interpolated id and link text. The matcher regex constrains shape to\d+|[A-Z][A-Z0-9_]*-\d+, so the escape is defence-in-depth — a direct caller bypassing the matcher cannot regress into HTML attribute injection.The merge commit on this branch pulls in #23093; the diff against
devcollapses to just the macro link commit once both ancestors land.Merge checklist