Coverage for tests/test_references.py: 100.00%

124 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-01 20:28 +0200

1"""Tests for the references module.""" 

2 

3from __future__ import annotations 

4 

5from textwrap import dedent 

6from typing import Mapping 

7 

8import markdown 

9import pytest 

10 

11from mkdocs_autorefs.plugin import AutorefsPlugin 

12from mkdocs_autorefs.references import AutorefsExtension, AutorefsHookInterface, fix_refs, relative_url 

13 

14 

15@pytest.mark.parametrize( 

16 ("current_url", "to_url", "href_url"), 

17 [ 

18 ("a/", "a#b", "#b"), 

19 ("a/", "a/b#c", "b#c"), 

20 ("a/b/", "a/b#c", "#c"), 

21 ("a/b/", "a/c#d", "../c#d"), 

22 ("a/b/", "a#c", "..#c"), 

23 ("a/b/c/", "d#e", "../../../d#e"), 

24 ("a/b/", "c/d/#e", "../../c/d/#e"), 

25 ("a/index.html", "a/index.html#b", "#b"), 

26 ("a/index.html", "a/b.html#c", "b.html#c"), 

27 ("a/b.html", "a/b.html#c", "#c"), 

28 ("a/b.html", "a/c.html#d", "c.html#d"), 

29 ("a/b.html", "a/index.html#c", "index.html#c"), 

30 ("a/b/c.html", "d.html#e", "../../d.html#e"), 

31 ("a/b.html", "c/d.html#e", "../c/d.html#e"), 

32 ("a/b/index.html", "a/b/c/d.html#e", "c/d.html#e"), 

33 ("", "#x", "#x"), 

34 ("a/", "#x", "../#x"), 

35 ("a/b.html", "#x", "../#x"), 

36 ("", "a/#x", "a/#x"), 

37 ("", "a/b.html#x", "a/b.html#x"), 

38 ], 

39) 

40def test_relative_url(current_url: str, to_url: str, href_url: str) -> None: 

41 """Compute relative URLs correctly.""" 

42 assert relative_url(current_url, to_url) == href_url 

43 

44 

45def run_references_test( 

46 url_map: dict[str, str], 

47 source: str, 

48 output: str, 

49 unmapped: list[tuple[str, AutorefsHookInterface.Context | None]] | None = None, 

50 from_url: str = "page.html", 

51 extensions: Mapping = {}, 

52) -> None: 

53 """Help running tests about references. 

54 

55 Arguments: 

56 url_map: The URL mapping. 

57 source: The source text. 

58 output: The expected output. 

59 unmapped: The expected unmapped list. 

60 from_url: The source page URL. 

61 """ 

62 md = markdown.Markdown(extensions=[AutorefsExtension(), *extensions], extension_configs=extensions) 

63 content = md.convert(source) 

64 

65 def url_mapper(identifier: str) -> str: 

66 return relative_url(from_url, url_map[identifier]) 

67 

68 actual_output, actual_unmapped = fix_refs(content, url_mapper) 

69 assert actual_output == output 

70 assert actual_unmapped == (unmapped or []) 

71 

72 

73def test_reference_implicit() -> None: 

74 """Check implicit references (identifier only).""" 

75 run_references_test( 

76 url_map={"Foo": "foo.html#Foo"}, 

77 source="This [Foo][].", 

78 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo">Foo</a>.</p>', 

79 ) 

80 

81 

82def test_reference_explicit_with_markdown_text() -> None: 

83 """Check explicit references with Markdown formatting.""" 

84 run_references_test( 

85 url_map={"Foo": "foo.html#Foo"}, 

86 source="This [**Foo**][Foo].", 

87 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><strong>Foo</strong></a>.</p>', 

88 ) 

89 

90 

91def test_reference_implicit_with_code() -> None: 

92 """Check implicit references (identifier only, wrapped in backticks).""" 

93 run_references_test( 

94 url_map={"Foo": "foo.html#Foo"}, 

95 source="This [`Foo`][].", 

96 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><code>Foo</code></a>.</p>', 

97 ) 

98 

99 

100def test_reference_implicit_with_code_inlinehilite_plain() -> None: 

101 """Check implicit references (identifier in backticks, wrapped by inlinehilite).""" 

102 run_references_test( 

103 extensions={"pymdownx.inlinehilite": {}}, 

104 url_map={"pathlib.Path": "pathlib.html#Path"}, 

105 source="This [`pathlib.Path`][].", 

106 output='<p>This <a class="autorefs autorefs-internal" href="pathlib.html#Path"><code>pathlib.Path</code></a>.</p>', 

107 ) 

108 

109 

110def test_reference_implicit_with_code_inlinehilite_python() -> None: 

111 """Check implicit references (identifier in backticks, syntax-highlighted by inlinehilite).""" 

112 run_references_test( 

113 extensions={"pymdownx.inlinehilite": {"style_plain_text": "python"}, "pymdownx.highlight": {}}, 

114 url_map={"pathlib.Path": "pathlib.html#Path"}, 

115 source="This [`pathlib.Path`][].", 

116 output='<p>This <a class="autorefs autorefs-internal" href="pathlib.html#Path"><code class="highlight">pathlib.Path</code></a>.</p>', 

117 ) 

118 

119 

120def test_reference_with_punctuation() -> None: 

121 """Check references with punctuation.""" 

122 run_references_test( 

123 url_map={'Foo&"bar': 'foo.html#Foo&"bar'}, 

124 source='This [Foo&"bar][].', 

125 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo&amp;&quot;bar">Foo&amp;"bar</a>.</p>', 

126 ) 

127 

128 

129def test_reference_to_relative_path() -> None: 

130 """Check references from a page at a nested path.""" 

131 run_references_test( 

132 from_url="sub/sub/page.html", 

133 url_map={"zz": "foo.html#zz"}, 

134 source="This [zz][].", 

135 output='<p>This <a class="autorefs autorefs-internal" href="../../foo.html#zz">zz</a>.</p>', 

136 ) 

137 

138 

139def test_multiline_links() -> None: 

140 """Check that links with multiline text are recognized.""" 

141 run_references_test( 

142 url_map={"foo-bar": "foo.html#bar"}, 

143 source="This [Foo\nbar][foo-bar].", 

144 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#bar">Foo\nbar</a>.</p>', 

145 ) 

146 

147 

148def test_no_reference_with_space() -> None: 

149 """Check that references with spaces are fixed.""" 

150 run_references_test( 

151 url_map={"Foo bar": "foo.html#bar"}, 

152 source="This [Foo bar][].", 

153 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#bar">Foo bar</a>.</p>', 

154 ) 

155 

156 

157def test_no_reference_inside_markdown() -> None: 

158 """Check that references inside code are not fixed.""" 

159 run_references_test( 

160 url_map={"Foo": "foo.html#Foo"}, 

161 source="This `[Foo][]`.", 

162 output="<p>This <code>[Foo][]</code>.</p>", 

163 ) 

164 

165 

166def test_missing_reference() -> None: 

167 """Check that implicit references are correctly seen as unmapped.""" 

168 run_references_test( 

169 url_map={"NotFoo": "foo.html#NotFoo"}, 

170 source="[Foo][]", 

171 output="<p>[Foo][]</p>", 

172 unmapped=[("Foo", None)], 

173 ) 

174 

175 

176def test_missing_reference_with_markdown_text() -> None: 

177 """Check unmapped explicit references.""" 

178 run_references_test( 

179 url_map={"NotFoo": "foo.html#NotFoo"}, 

180 source="[`Foo`][Foo]", 

181 output="<p>[<code>Foo</code>][Foo]</p>", 

182 unmapped=[("Foo", None)], 

183 ) 

184 

185 

186def test_missing_reference_with_markdown_id() -> None: 

187 """Check unmapped explicit references with Markdown in the identifier.""" 

188 run_references_test( 

189 url_map={"Foo": "foo.html#Foo", "NotFoo": "foo.html#NotFoo"}, 

190 source="[Foo][*NotFoo*]", 

191 output="<p>[Foo][*NotFoo*]</p>", 

192 unmapped=[("*NotFoo*", None)], 

193 ) 

194 

195 

196def test_missing_reference_with_markdown_implicit() -> None: 

197 """Check that implicit references are not fixed when the identifier is not the exact one.""" 

198 run_references_test( 

199 url_map={"Foo-bar": "foo.html#Foo-bar"}, 

200 source="[*Foo-bar*][] and [`Foo`-bar][]", 

201 output="<p>[<em>Foo-bar</em>][*Foo-bar*] and [<code>Foo</code>-bar][]</p>", 

202 unmapped=[("*Foo-bar*", None)], 

203 ) 

204 

205 

206def test_reference_with_markup() -> None: 

207 """Check that references with markup are resolved (and need escaping to prevent rendering).""" 

208 run_references_test( 

209 url_map={"*a b*": "foo.html#Foo"}, 

210 source="This [*a b*][].", 

211 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><em>a b</em></a>.</p>', 

212 ) 

213 run_references_test( 

214 url_map={"*a/b*": "foo.html#Foo"}, 

215 source="This [`*a/b*`][].", 

216 output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><code>*a/b*</code></a>.</p>', 

217 ) 

218 

219 

220def test_legacy_custom_required_reference() -> None: 

221 """Check that external HTML-based references are expanded or reported missing.""" 

222 url_map = {"ok": "ok.html#ok"} 

223 source = "<span data-autorefs-identifier=bar>foo</span> <span data-autorefs-identifier=ok>ok</span>" 

224 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): 

225 output, unmapped = fix_refs(source, url_map.__getitem__) 

226 assert output == '[foo][bar] <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a>' 

227 assert unmapped == [("bar", None)] 

228 

229 

230def test_custom_required_reference() -> None: 

231 """Check that external HTML-based references are expanded or reported missing.""" 

232 url_map = {"ok": "ok.html#ok"} 

233 source = "<autoref identifier=bar>foo</autoref> <autoref identifier=ok>ok</autoref>" 

234 output, unmapped = fix_refs(source, url_map.__getitem__) 

235 assert output == '[foo][bar] <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a>' 

236 assert unmapped == [("bar", None)] 

237 

238 

239def test_legacy_custom_optional_reference() -> None: 

240 """Check that optional HTML-based references are expanded and never reported missing.""" 

241 url_map = {"ok": "ok.html#ok"} 

242 source = '<span data-autorefs-optional="bar">foo</span> <span data-autorefs-optional=ok>ok</span>' 

243 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): 

244 output, unmapped = fix_refs(source, url_map.__getitem__) 

245 assert output == 'foo <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a>' 

246 assert unmapped == [] 

247 

248 

249def test_custom_optional_reference() -> None: 

250 """Check that optional HTML-based references are expanded and never reported missing.""" 

251 url_map = {"ok": "ok.html#ok"} 

252 source = '<autoref optional identifier="bar">foo</autoref> <autoref identifier=ok optional>ok</autoref>' 

253 output, unmapped = fix_refs(source, url_map.__getitem__) 

254 assert output == 'foo <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a>' 

255 assert unmapped == [] 

256 

257 

258def test_legacy_custom_optional_hover_reference() -> None: 

259 """Check that optional-hover HTML-based references are expanded and never reported missing.""" 

260 url_map = {"ok": "ok.html#ok"} 

261 source = '<span data-autorefs-optional-hover="bar">foo</span> <span data-autorefs-optional-hover=ok>ok</span>' 

262 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): 

263 output, unmapped = fix_refs(source, url_map.__getitem__) 

264 assert ( 

265 output 

266 == '<span title="bar">foo</span> <a class="autorefs autorefs-internal" title="ok" href="ok.html#ok">ok</a>' 

267 ) 

268 assert unmapped == [] 

269 

270 

271def test_custom_optional_hover_reference() -> None: 

272 """Check that optional-hover HTML-based references are expanded and never reported missing.""" 

273 url_map = {"ok": "ok.html#ok"} 

274 source = '<autoref optional hover identifier="bar">foo</autoref> <autoref optional identifier=ok hover>ok</autoref>' 

275 output, unmapped = fix_refs(source, url_map.__getitem__) 

276 assert ( 

277 output 

278 == '<span title="bar">foo</span> <a class="autorefs autorefs-internal" title="ok" href="ok.html#ok">ok</a>' 

279 ) 

280 assert unmapped == [] 

281 

282 

283def test_legacy_external_references() -> None: 

284 """Check that external references are marked as such.""" 

285 url_map = {"example": "https://example.com"} 

286 source = '<span data-autorefs-optional="example">example</span>' 

287 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): 

288 output, unmapped = fix_refs(source, url_map.__getitem__) 

289 assert output == '<a class="autorefs autorefs-external" href="https://example.com">example</a>' 

290 assert unmapped == [] 

291 

292 

293def test_external_references() -> None: 

294 """Check that external references are marked as such.""" 

295 url_map = {"example": "https://example.com"} 

296 source = '<autoref optional identifier="example">example</autoref>' 

297 output, unmapped = fix_refs(source, url_map.__getitem__) 

298 assert output == '<a class="autorefs autorefs-external" href="https://example.com">example</a>' 

299 assert unmapped == [] 

300 

301 

302def test_register_markdown_anchors() -> None: 

303 """Check that Markdown anchors are registered when enabled.""" 

304 plugin = AutorefsPlugin() 

305 md = markdown.Markdown(extensions=["attr_list", "toc", AutorefsExtension(plugin)]) 

306 plugin.current_page = "page" 

307 md.convert( 

308 dedent( 

309 """ 

310 [](){#foo} 

311 ## Heading foo 

312 

313 Paragraph 1. 

314 

315 [](){#bar} 

316 Paragraph 2. 

317 

318 [](){#alias1} 

319 [](){#alias2} 

320 ## Heading bar 

321 

322 [](){#alias3} 

323 Text. 

324 [](){#alias4} 

325 ## Heading baz 

326 

327 [](){#alias5} 

328 [](){#alias6} 

329 Decoy. 

330 ## Heading more1 

331 

332 [](){#alias7} 

333 [decoy](){#alias8} 

334 [](){#alias9} 

335 ## Heading more2 {#heading-custom2} 

336 

337 [](){#aliasSame} 

338 ## Same heading 1 

339 [](){#aliasSame} 

340 ## Same heading 2 

341 

342 [](){#alias10} 

343 """, 

344 ), 

345 ) 

346 assert plugin._url_map == { 

347 "foo": ["page#heading-foo"], 

348 "bar": ["page#bar"], 

349 "alias1": ["page#heading-bar"], 

350 "alias2": ["page#heading-bar"], 

351 "alias3": ["page#alias3"], 

352 "alias4": ["page#heading-baz"], 

353 "alias5": ["page#alias5"], 

354 "alias6": ["page#alias6"], 

355 "alias7": ["page#alias7"], 

356 "alias8": ["page#alias8"], 

357 "alias9": ["page#heading-custom2"], 

358 "alias10": ["page#alias10"], 

359 "aliasSame": ["page#same-heading-1", "page#same-heading-2"], 

360 } 

361 

362 

363def test_register_markdown_anchors_with_admonition() -> None: 

364 """Check that Markdown anchors are registered inside a nested admonition element.""" 

365 plugin = AutorefsPlugin() 

366 md = markdown.Markdown(extensions=["attr_list", "toc", "admonition", AutorefsExtension(plugin)]) 

367 plugin.current_page = "page" 

368 md.convert( 

369 dedent( 

370 """ 

371 [](){#alias1} 

372 !!! note 

373 ## Heading foo 

374 

375 [](){#alias2} 

376 ## Heading bar 

377 

378 [](){#alias3} 

379 ## Heading baz 

380 """, 

381 ), 

382 ) 

383 assert plugin._url_map == { 

384 "alias1": ["page#alias1"], 

385 "alias2": ["page#heading-bar"], 

386 "alias3": ["page#alias3"], 

387 } 

388 

389 

390def test_legacy_keep_data_attributes() -> None: 

391 """Keep HTML data attributes from autorefs spans.""" 

392 url_map = {"example": "https://e.com"} 

393 source = '<span data-autorefs-optional="example" class="hi ho" data-foo data-bar="0">e</span>' 

394 with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): 

395 output, _ = fix_refs(source, url_map.__getitem__) 

396 assert output == '<a class="autorefs autorefs-external hi ho" href="https://e.com" data-foo data-bar="0">e</a>' 

397 

398 

399def test_keep_data_attributes() -> None: 

400 """Keep HTML data attributes from autorefs spans.""" 

401 url_map = {"example": "https://e.com"} 

402 source = '<autoref optional identifier="example" class="hi ho" data-foo data-bar="0">e</autoref>' 

403 output, _ = fix_refs(source, url_map.__getitem__) 

404 assert output == '<a class="autorefs autorefs-external hi ho" href="https://e.com" data-foo data-bar="0">e</a>'