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
5 changes: 5 additions & 0 deletions .changeset/dollar-anchor-case.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'intersphinx': patch
---

Expand the `$` uri anchor shorthand with the object name verbatim (case-preserved), matching Sphinx semantics. Previously the expansion was lowercased and whitespace-replaced, which broke links to any case-distinct targets (e.g. `re.Match` vs `re.match` in the CPython inventory, and capitalized glossary terms like `#term-Match`).
8 changes: 5 additions & 3 deletions src/intersphinx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,11 @@ export class Inventory {
resolvedLocation = resolvedLocation.slice(1);
}
if (resolvedLocation.endsWith('$')) {
// Maybe move this to the parse function only?
resolvedLocation =
resolvedLocation.slice(0, -1) + entry.name.toLowerCase().replace(/\s+/g, '-');
// Sphinx semantics: `$` is replaced by the object name *verbatim*.
// Case must be preserved: e.g. `re.match` (function) and `re.Match`
// (class) are distinct objects with distinct anchors.
// https://github.com/sphinx-doc/sphinx/blob/v8.2.3/sphinx/util/inventory.py#L154
resolvedLocation = resolvedLocation.slice(0, -1) + entry.name;
}
const resolvedDisplay =
!entry.display || entry.display.trim() === '-' ? undefined : entry.display.trim();
Expand Down
32 changes: 32 additions & 0 deletions src/inventory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,36 @@ describe('Test Inventory', () => {
expect(entry?.location?.includes('abc.html')).toBe(true);
expect(entry?.type).toBe('std:doc');
});
test('`$` anchor shorthand expands to the name verbatim (Sphinx semantics)', () => {
const inv = new Inventory({ id: 'test', path: 'https://example.org' });
// Sphinx only writes `$` when the anchor ends with the name byte-for-byte,
// so the expansion must preserve case exactly:
// re.match (function) and re.Match (class) are distinct targets.
inv.setEntry({ type: 'py:function', name: 're.match', location: 'library/re.html#$' });
inv.setEntry({ type: 'py:class', name: 're.Match', location: 'library/re.html#$' });
// A capitalized glossary term: Sphinx writes `Match std:term -1 index.html#term-$ -`
// and its real HTML anchor is `id="term-Match"` — also case-preserved.
inv.setEntry({ type: 'std:term', name: 'Match', location: 'index.html#term-$' });
expect(inv.getEntry({ name: 're.match' })?.location).toBe(
'https://example.org/library/re.html#re.match',
);
expect(inv.getEntry({ name: 're.Match' })?.location).toBe(
'https://example.org/library/re.html#re.Match',
);
expect(inv.getEntry({ type: 'std:term', name: 'Match' })?.location).toBe(
'https://example.org/index.html#term-Match',
);
});
test('Python inventory - case sensitive names', async () => {
// Python 3.11 includes both `class Match` and `def match()` as targets
const inv = new Inventory({ path: 'https://docs.python.org/3.11' });
await inv.load();
expect(inv._loaded).toBe(true);
const entryClass = inv.getEntry({ name: 're.Match' });
expect(entryClass?.location?.endsWith('re.Match')).toBe(true);
expect(entryClass?.type).toBe('py:class');
const entryFunc = inv.getEntry({ name: 're.match' });
expect(entryFunc?.location?.endsWith('re.match')).toBe(true);
expect(entryFunc?.type).toBe('py:function');
});
});
Loading