Ticket #35533: changes.patch
File changes.patch, 11.1 KB (added by , 5 months ago) |
---|
-
django/utils/html.py
diff --git a/django/utils/html.py b/django/utils/html.py index 22d3ae42fa..a4be3cf8e6 100644
a b from django.utils.deprecation import RemovedInDjango60Warning 11 11 from django.utils.encoding import punycode 12 12 from django.utils.functional import Promise, keep_lazy, keep_lazy_text 13 13 from django.utils.http import RFC3986_GENDELIMS, RFC3986_SUBDELIMS 14 from django.utils.markdown import find_closing_markdown_bracket, has_markdown_link 14 15 from django.utils.regex_helper import _lazy_re_compile 15 16 from django.utils.safestring import SafeData, SafeString, mark_safe 16 17 from django.utils.text import normalize_newlines … … class Urlizer: 278 279 279 280 mailto_template = "mailto:{local}@{domain}" 280 281 url_template = '<a href="{href}"{attrs}>{url}</a>' 282 markdown_url_template = '[{text}](<a href="{url}"{attrs}>{trimmed_url}</a>)' 281 283 282 284 def __call__(self, text, trim_url_limit=None, nofollow=False, autoescape=False): 283 285 """ … … class Urlizer: 291 293 """ 292 294 safe_input = isinstance(text, SafeData) 293 295 294 words = self.word_split_re.split(str(text)) 296 text = str(text) 297 if has_markdown_link(text): 298 return self.handle_markdown_link( 299 text, 300 safe_input=safe_input, 301 trim_url_limit=trim_url_limit, 302 nofollow=nofollow, 303 autoescape=autoescape, 304 ) 305 306 words = self.word_split_re.split(text) 295 307 return "".join( 296 308 [ 297 309 self.handle_word( … … class Urlizer: 305 317 ] 306 318 ) 307 319 320 def handle_markdown_link( 321 self, 322 text, 323 *, 324 safe_input, 325 trim_url_limit=None, 326 nofollow=False, 327 autoescape=False, 328 ): 329 nofollow_attr = ' rel="nofollow"' if nofollow else "" 330 331 def find_and_replace_link(text): 332 i = 0 333 result = [] 334 while i < len(text): 335 if text[i] == "\\": 336 result.append(text[i : i + 2]) 337 i += 2 338 continue 339 if text[i] == "[": 340 start = i 341 close_bracket = find_closing_markdown_bracket(text, i + 1) 342 if ( 343 close_bracket != -1 344 and close_bracket + 1 < len(text) 345 and text[close_bracket + 1] == "(" 346 ): 347 j = close_bracket + 2 348 paren_depth = 1 349 while j < len(text): 350 if text[j] == "\\": 351 j += 2 352 continue 353 if text[j] == "(": 354 paren_depth += 1 355 elif text[j] == ")": 356 paren_depth -= 1 357 if paren_depth == 0: 358 link_text = text[start + 1 : close_bracket] 359 link_url = text[close_bracket + 2 : j] 360 trimmed_url = self.trim_url( 361 link_url, limit=trim_url_limit 362 ) 363 364 if autoescape and not safe_input: 365 link_text = escape(link_text) 366 link_url = escape(link_url) 367 trimmed_url = escape(trimmed_url) 368 369 result.append( 370 self.markdown_url_template.format( 371 text=link_text, 372 url=link_url, 373 attrs=nofollow_attr, 374 trimmed_url=trimmed_url, 375 ) 376 ) 377 i = j + 1 378 break 379 j += 1 380 else: 381 result.append(text[i]) 382 i += 1 383 else: 384 result.append(text[i]) 385 i = close_bracket + 1 if close_bracket != -1 else i + 1 386 else: 387 result.append(text[i]) 388 i += 1 389 return "".join(result) 390 391 return find_and_replace_link(text) 392 308 393 def handle_word( 309 394 self, 310 395 word, -
new file django/utils/markdown.py
diff --git a/django/utils/markdown.py b/django/utils/markdown.py new file mode 100644 index 0000000000..c0213ed47a
- + 1 def find_closing_markdown_bracket(text, start): 2 """ 3 Find the closing bracket corresponding to the opening bracket. 4 """ 5 depth = 0 6 i = start 7 while i < len(text): 8 if text[i] == "\\": 9 i += 2 10 continue 11 if text[i] == "[": 12 depth += 1 13 elif text[i] == "]": 14 if depth == 0: 15 return i 16 depth -= 1 17 i += 1 18 return -1 19 20 21 def has_markdown_link(text): 22 """ 23 Check if the given text contains any Markdown links. 24 """ 25 26 def is_valid_url(start, end): 27 """ 28 Check if the URL is valid. 29 """ 30 url = text[start:end].strip() 31 return ( 32 url.startswith("http://") 33 or url.startswith("https://") 34 or any(c.isalnum() for c in url) 35 ) 36 37 i = 0 38 while i < len(text): 39 if text[i] == "\\": 40 i += 2 41 continue 42 if text[i] == "[": 43 close_bracket = find_closing_markdown_bracket(text, i + 1) 44 if ( 45 close_bracket != -1 46 and close_bracket + 1 < len(text) 47 and text[close_bracket + 1] == "(" 48 ): 49 j = close_bracket + 2 50 paren_depth = 1 51 while j < len(text): 52 if text[j] == "\\": 53 j += 2 54 continue 55 if text[j] == "(": 56 paren_depth += 1 57 elif text[j] == ")": 58 paren_depth -= 1 59 if paren_depth == 0: 60 if is_valid_url(close_bracket + 2, j): 61 return True 62 break 63 j += 1 64 i = close_bracket + 1 if close_bracket != -1 else i + 1 65 else: 66 i += 1 67 return False -
tests/template_tests/filter_tests/test_urlize.py
diff --git a/tests/template_tests/filter_tests/test_urlize.py b/tests/template_tests/filter_tests/test_urlize.py index 8f84e62c92..1b377883f9 100644
a b class FunctionTests(SimpleTestCase): 320 320 ) 321 321 self.assertEqual( 322 322 urlize("[http://168.192.0.1](http://168.192.0.1)"), 323 '[ <a href="http://168.192.0.1](http://168.192.0.1)" rel="nofollow">'324 "http://168.192.0.1 ](http://168.192.0.1)</a>",323 '[http://168.192.0.1](<a href="http://168.192.0.1" rel="nofollow">' 324 "http://168.192.0.1</a>)", 325 325 ) 326 326 327 327 def test_wrapping_characters(self): -
tests/utils_tests/test_html.py
diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py index ad31b8cc5b..ecb9bc2ea2 100644
a b from django.utils.html import ( 17 17 strip_spaces_between_tags, 18 18 strip_tags, 19 19 urlize, 20 urlizer, 20 21 ) 21 22 from django.utils.safestring import mark_safe 22 23 … … class TestUtilsHtml(SimpleTestCase): 356 357 for value in tests: 357 358 with self.subTest(value=value): 358 359 self.assertEqual(urlize(value), value) 360 361 def test_handle_markdown_link(self): 362 tests = [ 363 { 364 "input": "Here's a [link with [nested] brackets](https://example.com)", 365 "expected": "Here's a [link with [nested] brackets](<a href=\"https://" 366 'example.com">https://example.com</a>)', 367 "params": { 368 "trim_url_limit": None, 369 "nofollow": False, 370 "autoescape": False, 371 }, 372 }, 373 { 374 "input": "Check out [this link](https://example.com/page(1))", 375 "expected": 'Check out [this link](<a href="https://example.com/' 376 'page(1)">https://example.com/page(1)</a>)', 377 "params": { 378 "trim_url_limit": None, 379 "nofollow": False, 380 "autoescape": False, 381 }, 382 }, 383 { 384 "input": "Here's a [complex URL](https://example.com/" 385 "path?param1=value1¶m2=value2#fragment)", 386 "expected": "Here's a [complex URL](<a href=\"https://example.com/" 387 'path?param1=value1&param2=value2#fragment">' 388 "https://example.com/path?param1=value1&" 389 "param2=value2#fragment</a>)", 390 "params": { 391 "trim_url_limit": None, 392 "nofollow": False, 393 "autoescape": True, 394 }, 395 }, 396 { 397 "input": "Multiple [link1](https://example1.com) and " 398 "[link2](https://example2.com)", 399 "expected": 'Multiple [link1](<a href="https://example1.com">' 400 "https://example1.com</a>) and [link2]" 401 '(<a href="https://example2.com">https://example2.com</a>)', 402 "params": { 403 "trim_url_limit": None, 404 "nofollow": False, 405 "autoescape": False, 406 }, 407 }, 408 { 409 "input": "This is a [broken link(https://example.com)", 410 "expected": "This is a [broken link(https://example.com)", 411 "params": { 412 "trim_url_limit": None, 413 "nofollow": False, 414 "autoescape": False, 415 }, 416 }, 417 { 418 "input": "Here's a [very long URL](https://example.com/" 419 + "x" * 100 420 + ")", 421 "expected": "Here's a [very long URL](<a href=\"https://example.com/" 422 + "x" * 100 423 + '">https://example.com/xxxxxxxxx…</a>)', 424 "params": { 425 "trim_url_limit": 30, 426 "nofollow": False, 427 "autoescape": False, 428 }, 429 }, 430 ] 431 for test in tests: 432 with self.subTest(test=test): 433 output = urlizer(test["input"], **test["params"]) 434 self.assertEqual(output, test["expected"])