Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/markdown/core.py: 69%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

158 statements  

1# Python Markdown 

2 

3# A Python implementation of John Gruber's Markdown. 

4 

5# Documentation: https://python-markdown.github.io/ 

6# GitHub: https://github.com/Python-Markdown/markdown/ 

7# PyPI: https://pypi.org/project/Markdown/ 

8 

9# Started by Manfred Stienstra (http://www.dwerg.net/). 

10# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). 

11# Currently maintained by Waylan Limberg (https://github.com/waylan), 

12# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). 

13 

14# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later) 

15# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) 

16# Copyright 2004 Manfred Stienstra (the original version) 

17 

18# License: BSD (see LICENSE.md for details). 

19 

20from __future__ import annotations 

21 

22import codecs 

23import sys 

24import logging 

25import importlib 

26from typing import TYPE_CHECKING, Any, BinaryIO, Callable, ClassVar, Mapping, Sequence 

27from . import util 

28from .preprocessors import build_preprocessors 

29from .blockprocessors import build_block_parser 

30from .treeprocessors import build_treeprocessors 

31from .inlinepatterns import build_inlinepatterns, DelimiterProcessor 

32from .postprocessors import build_postprocessors 

33from .extensions import Extension 

34from .serializers import to_html_string, to_xhtml_string 

35from .util import BLOCK_LEVEL_ELEMENTS 

36 

37if TYPE_CHECKING: # pragma: no cover 

38 from xml.etree.ElementTree import Element 

39 

40__all__ = ['Markdown', 'markdown', 'markdownFromFile'] 

41 

42 

43logger = logging.getLogger('MARKDOWN') 

44 

45 

46class Markdown: 

47 """ 

48 A parser which converts Markdown to HTML. 

49 

50 Attributes: 

51 Markdown.tab_length (int): The number of spaces which correspond to a single tab. Default: `4`. 

52 Markdown.ESCAPED_CHARS (list[str]): List of characters which get the backslash escape treatment. 

53 Markdown.block_level_elements (list[str]): List of HTML tags which get treated as block-level elements. 

54 See [`markdown.util.BLOCK_LEVEL_ELEMENTS`][] for the full list of elements. 

55 Markdown.registeredExtensions (list[Extension]): List of extensions which have called 

56 [`registerExtension`][markdown.Markdown.registerExtension] during setup. 

57 Markdown.doc_tag (str): Element used to wrap document. Default: `div`. 

58 Markdown.stripTopLevelTags (bool): Indicates whether the `doc_tag` should be removed. Default: 'True'. 

59 Markdown.references (dict[str, tuple[str, str]]): A mapping of link references found in a parsed document 

60 where the key is the reference name and the value is a tuple of the URL and title. 

61 Markdown.htmlStash (util.HtmlStash): The instance of the `HtmlStash` used by an instance of this class. 

62 Markdown.output_formats (dict[str, Callable[xml.etree.ElementTree.Element]]): A mapping of known output 

63 formats by name and their respective serializers. Each serializer must be a callable which accepts an 

64 [`Element`][xml.etree.ElementTree.Element] and returns a `str`. 

65 Markdown.output_format (str): The output format set by 

66 [`set_output_format`][markdown.Markdown.set_output_format]. 

67 Markdown.serializer (Callable[xml.etree.ElementTree.Element]): The serializer set by 

68 [`set_output_format`][markdown.Markdown.set_output_format]. 

69 Markdown.preprocessors (util.Registry): A collection of [`preprocessors`][markdown.preprocessors]. 

70 Markdown.parser (blockparser.BlockParser): A collection of [`blockprocessors`][markdown.blockprocessors]. 

71 Markdown.inlinePatterns (util.Registry): A collection of [`inlinepatterns`][markdown.inlinepatterns]. 

72 Markdown.treeprocessors (util.Registry): A collection of [`treeprocessors`][markdown.treeprocessors]. 

73 Markdown.postprocessors (util.Registry): A collection of [`postprocessors`][markdown.postprocessors]. 

74 

75 """ 

76 

77 doc_tag = "div" # Element used to wrap document - later removed 

78 

79 output_formats: ClassVar[dict[str, Callable[[Element], str]]] = { 

80 'html': to_html_string, 

81 'xhtml': to_xhtml_string, 

82 } 

83 """ 

84 A mapping of known output formats by name and their respective serializers. Each serializer must be a 

85 callable which accepts an [`Element`][xml.etree.ElementTree.Element] and returns a `str`. 

86 """ 

87 

88 def __init__(self, **kwargs): 

89 """ 

90 Creates a new Markdown instance. 

91 

92 Keyword Arguments: 

93 extensions (list[Extension | str]): A list of extensions. 

94 

95 If an item is an instance of a subclass of [`markdown.extensions.Extension`][], 

96 the instance will be used as-is. If an item is of type `str`, it is passed 

97 to [`build_extension`][markdown.Markdown.build_extension] with its corresponding 

98 `extension_configs` and the returned instance of [`markdown.extensions.Extension`][] 

99 is used. 

100 extension_configs (dict[str, dict[str, Any]]): Configuration settings for extensions. 

101 output_format (str): Format of output. Supported formats are: 

102 

103 * `xhtml`: Outputs XHTML style tags. Default. 

104 * `html`: Outputs HTML style tags. 

105 tab_length (int): Length of tabs in the source. Default: `4` 

106 

107 """ 

108 

109 self.last_run: float = 0.0 

110 self.tab_length: int = kwargs.get('tab_length', 4) 

111 

112 self.ESCAPED_CHARS: list[str] = [ 

113 '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!' 

114 ] 

115 """ List of characters which get the backslash escape treatment. """ 

116 

117 self.block_level_elements: list[str] = BLOCK_LEVEL_ELEMENTS.copy() 

118 

119 self.registeredExtensions: list[Extension] = [] 

120 self.docType = "" # TODO: Maybe delete this. It does not appear to be used anymore. 

121 self.stripTopLevelTags: bool = True 

122 self.delimiters: DelimiterProcessor | None = None 

123 

124 self.build_parser() 

125 

126 self.references: dict[str, tuple[str, str]] = {} 

127 self.htmlStash: util.HtmlStash = util.HtmlStash() 

128 self.registerExtensions(extensions=kwargs.get('extensions', []), 

129 configs=kwargs.get('extension_configs', {})) 

130 self.set_output_format(kwargs.get('output_format', 'xhtml')) 

131 self.reset() 

132 

133 def build_parser(self) -> Markdown: 

134 """ 

135 Build the parser from the various parts. 

136 

137 Assigns a value to each of the following attributes on the class instance: 

138 

139 * **`Markdown.preprocessors`** ([`Registry`][markdown.util.Registry]) -- A collection of 

140 [`preprocessors`][markdown.preprocessors]. 

141 * **`Markdown.parser`** ([`BlockParser`][markdown.blockparser.BlockParser]) -- A collection of 

142 [`blockprocessors`][markdown.blockprocessors]. 

143 * **`Markdown.inlinePatterns`** ([`Registry`][markdown.util.Registry]) -- A collection of 

144 [`inlinepatterns`][markdown.inlinepatterns]. 

145 * **`Markdown.treeprocessors`** ([`Registry`][markdown.util.Registry]) -- A collection of 

146 [`treeprocessors`][markdown.treeprocessors]. 

147 * **`Markdown.postprocessors`** ([`Registry`][markdown.util.Registry]) -- A collection of 

148 [`postprocessors`][markdown.postprocessors]. 

149 

150 This method could be redefined in a subclass to build a custom parser which is made up of a different 

151 combination of processors and patterns. 

152 

153 """ 

154 self.preprocessors = build_preprocessors(self) 

155 self.parser = build_block_parser(self) 

156 self.inlinePatterns = build_inlinepatterns(self) 

157 self.treeprocessors = build_treeprocessors(self) 

158 self.postprocessors = build_postprocessors(self) 

159 return self 

160 

161 def registerExtensions( 

162 self, 

163 extensions: Sequence[Extension | str], 

164 configs: Mapping[str, dict[str, Any]] 

165 ) -> Markdown: 

166 """ 

167 Load a list of extensions into an instance of the `Markdown` class. 

168 

169 Arguments: 

170 extensions (list[Extension | str]): A list of extensions. 

171 

172 If an item is an instance of a subclass of [`markdown.extensions.Extension`][], 

173 the instance will be used as-is. If an item is of type `str`, it is passed 

174 to [`build_extension`][markdown.Markdown.build_extension] with its corresponding `configs` and the 

175 returned instance of [`markdown.extensions.Extension`][] is used. 

176 configs (dict[str, dict[str, Any]]): Configuration settings for extensions. 

177 

178 """ 

179 for ext in extensions: 

180 if isinstance(ext, str): 

181 ext = self.build_extension(ext, configs.get(ext, {})) 

182 if isinstance(ext, Extension): 

183 ext.extendMarkdown(self) 

184 logger.debug( 

185 'Successfully loaded extension "%s.%s".' 

186 % (ext.__class__.__module__, ext.__class__.__name__) 

187 ) 

188 elif ext is not None: 

189 raise TypeError( 

190 'Extension "{}.{}" must be of type: "{}.{}"'.format( 

191 ext.__class__.__module__, ext.__class__.__name__, 

192 Extension.__module__, Extension.__name__ 

193 ) 

194 ) 

195 return self 

196 

197 def build_extension(self, ext_name: str, configs: Mapping[str, Any]) -> Extension: 

198 """ 

199 Build extension from a string name, then return an instance using the given `configs`. 

200 

201 Arguments: 

202 ext_name: Name of extension as a string. 

203 configs: Configuration settings for extension. 

204 

205 Returns: 

206 An instance of the extension with the given configuration settings. 

207 

208 First attempt to load an entry point. The string name must be registered as an entry point in the 

209 `markdown.extensions` group which points to a subclass of the [`markdown.extensions.Extension`][] class. 

210 If multiple distributions have registered the same name, the first one found is returned. 

211 

212 If no entry point is found, assume dot notation (`path.to.module:ClassName`). Load the specified class and 

213 return an instance. If no class is specified, import the module and call a `makeExtension` function and return 

214 the [`markdown.extensions.Extension`][] instance returned by that function. 

215 """ 

216 configs = dict(configs) 

217 

218 entry_points = [ep for ep in util.get_installed_extensions() if ep.name == ext_name] 

219 if entry_points: 

220 ext = entry_points[0].load() 

221 return ext(**configs) 

222 

223 # Get class name (if provided): `path.to.module:ClassName` 

224 ext_name, class_name = ext_name.split(':', 1) if ':' in ext_name else (ext_name, '') 

225 

226 try: 

227 module = importlib.import_module(ext_name) 

228 logger.debug( 

229 'Successfully imported extension module "%s".' % ext_name 

230 ) 

231 except ImportError as e: 

232 message = 'Failed loading extension "%s".' % ext_name 

233 e.args = (message,) + e.args[1:] 

234 raise 

235 

236 if class_name: 

237 # Load given class name from module. 

238 return getattr(module, class_name)(**configs) 

239 else: 

240 # Expect `makeExtension()` function to return a class. 

241 try: 

242 return module.makeExtension(**configs) 

243 except AttributeError as e: 

244 message = e.args[0] 

245 message = "Failed to initiate extension " \ 

246 "'%s': %s" % (ext_name, message) 

247 e.args = (message,) + e.args[1:] 

248 raise 

249 

250 def registerExtension(self, extension: Extension) -> Markdown: 

251 """ 

252 Register an extension as having a resettable state. 

253 

254 Arguments: 

255 extension: An instance of the extension to register. 

256 

257 This should get called once by an extension during setup. A "registered" extension's 

258 `reset` method is called by [`Markdown.reset()`][markdown.Markdown.reset]. Not all extensions have or need a 

259 resettable state, and so it should not be assumed that all extensions are "registered." 

260 

261 """ 

262 self.registeredExtensions.append(extension) 

263 return self 

264 

265 def reset(self) -> Markdown: 

266 """ 

267 Resets all state variables to prepare the parser instance for new input. 

268 

269 Called once upon creation of a class instance. Should be called manually between calls 

270 to [`Markdown.convert`][markdown.Markdown.convert]. 

271 """ 

272 self.htmlStash.reset() 

273 self.references.clear() 

274 

275 if self.delimiters is not None: 

276 self.delimiters.reset() 

277 if self.delimiters not in self.inlinePatterns: 

278 self.delimiters = None 

279 

280 for extension in self.registeredExtensions: 

281 if hasattr(extension, 'reset'): 

282 extension.reset() 

283 

284 return self 

285 

286 def set_output_format(self, format: str) -> Markdown: 

287 """ 

288 Set the output format for the class instance. 

289 

290 Arguments: 

291 format: Must be a known value in `Markdown.output_formats`. 

292 

293 """ 

294 self.output_format = format.lower().rstrip('145') # ignore number 

295 try: 

296 self.serializer = self.output_formats[self.output_format] 

297 except KeyError as e: 

298 valid_formats = list(self.output_formats.keys()) 

299 valid_formats.sort() 

300 message = 'Invalid Output Format: "%s". Use one of %s.' \ 

301 % (self.output_format, 

302 '"' + '", "'.join(valid_formats) + '"') 

303 e.args = (message,) + e.args[1:] 

304 raise 

305 return self 

306 

307 # Note: the `tag` argument is type annotated `Any` as ElementTree uses many various objects as tags. 

308 # As there is no standardization in ElementTree, the type of a given tag is unpredictable. 

309 def is_block_level(self, tag: Any) -> bool: 

310 """ 

311 Check if the given `tag` is a block level HTML tag. 

312 

313 Returns `True` for any string listed in `Markdown.block_level_elements`. A `tag` which is 

314 not a string always returns `False`. 

315 

316 """ 

317 if isinstance(tag, str): 

318 return tag.lower().rstrip('/') in self.block_level_elements 

319 # Some ElementTree tags are not strings, so return False. 

320 return False 

321 

322 def convert(self, source: str) -> str: 

323 """ 

324 Convert a Markdown string to a string in the specified output format. 

325 

326 Arguments: 

327 source: Markdown formatted text as Unicode or ASCII string. 

328 

329 Returns: 

330 A string in the specified output format. 

331 

332 Markdown parsing takes place in five steps: 

333 

334 1. A bunch of [`preprocessors`][markdown.preprocessors] munge the input text. 

335 2. A [`BlockParser`][markdown.blockparser.BlockParser] parses the high-level structural elements of the 

336 pre-processed text into an [`ElementTree`][xml.etree.ElementTree.ElementTree] object. 

337 3. A bunch of [`treeprocessors`][markdown.treeprocessors] are run against the 

338 [`ElementTree`][xml.etree.ElementTree.ElementTree] object. One such `treeprocessor` 

339 ([`markdown.treeprocessors.InlineProcessor`][]) runs [`inlinepatterns`][markdown.inlinepatterns] 

340 against the [`ElementTree`][xml.etree.ElementTree.ElementTree] object, parsing inline markup. 

341 4. Some [`postprocessors`][markdown.postprocessors] are run against the text after the 

342 [`ElementTree`][xml.etree.ElementTree.ElementTree] object has been serialized into text. 

343 5. The output is returned as a string. 

344 

345 !!! warning 

346 The Python-Markdown library does ***not*** sanitize its HTML output. 

347 If you are processing Markdown input from an untrusted source, it is your 

348 responsibility to ensure that it is properly sanitized. For more 

349 information see [Sanitizing HTML Output](../../sanitization.md). 

350 

351 """ 

352 

353 # Fix up the source text 

354 if not source.strip(): 

355 return '' # a blank Unicode string 

356 

357 try: 

358 source = str(source) 

359 except UnicodeDecodeError as e: # pragma: no cover 

360 # Customize error message while maintaining original traceback 

361 e.reason += '. -- Note: Markdown only accepts Unicode input!' 

362 raise 

363 

364 # Split into lines and run the line preprocessors. 

365 self.lines = source.split("\n") 

366 for prep in self.preprocessors: 

367 self.lines = prep.run(self.lines) 

368 

369 # Parse the high-level elements. 

370 root = self.parser.parseDocument(self.lines).getroot() 

371 

372 # Run the tree-processors 

373 for treeprocessor in self.treeprocessors: 

374 newRoot = treeprocessor.run(root) 

375 if newRoot is not None: 

376 root = newRoot 

377 

378 # Serialize _properly_. Strip top-level tags. 

379 output = self.serializer(root) 

380 if self.stripTopLevelTags: 

381 try: 

382 start = output.index( 

383 '<%s>' % self.doc_tag) + len(self.doc_tag) + 2 

384 end = output.rindex('</%s>' % self.doc_tag) 

385 output = output[start:end].strip() 

386 except ValueError as e: # pragma: no cover 

387 if output.strip().endswith('<%s />' % self.doc_tag): 

388 # We have an empty document 

389 output = '' 

390 else: 

391 # We have a serious problem 

392 raise ValueError('Markdown failed to strip top-level ' 

393 'tags. Document=%r' % output.strip()) from e 

394 

395 # Run the text post-processors 

396 for pp in self.postprocessors: 

397 output = pp.run(output) 

398 

399 return output.strip() 

400 

401 def convertFile( 

402 self, 

403 input: str | BinaryIO | None = None, 

404 output: str | BinaryIO | None = None, 

405 encoding: str | None = None, 

406 ) -> Markdown: 

407 """ 

408 Read Markdown text from a file or stream and write HTML output to a file or stream. 

409 

410 Decodes the input file using the provided encoding (defaults to `utf-8`), 

411 passes the file content to markdown, and outputs the HTML to either 

412 the provided stream or the file with provided name, using the same 

413 encoding as the source file. The 

414 [`xmlcharrefreplace`](https://docs.python.org/3/library/codecs.html#error-handlers) 

415 error handler is used when encoding the output. 

416 

417 **Note:** This is the only place that decoding and encoding of Unicode 

418 takes place in Python-Markdown. (All other code is Unicode-in / 

419 Unicode-out.) 

420 

421 Arguments: 

422 input: File object or path. Reads from `stdin` if `None`. 

423 output: File object or path. Writes to `stdout` if `None`. 

424 encoding: Encoding of input and output files. Defaults to `utf-8`. 

425 

426 !!! warning 

427 The Python-Markdown library does ***not*** sanitize its HTML output. 

428 As `Markdown.convertFile` writes directly to the file system, there is no 

429 easy way to sanitize the output from Python code. Therefore, it is 

430 recommended that the `Markdown.convertFile` method not be used on input 

431 from an untrusted source. For more information see [Sanitizing HTML 

432 Output](../../sanitization.md). 

433 

434 """ 

435 

436 encoding = encoding or "utf-8" 

437 

438 # Read the source 

439 if input: 

440 if isinstance(input, str): 

441 input_file = open(input, mode="r", encoding=encoding) 

442 else: 

443 input_file = codecs.getreader(encoding)(input) 

444 text = input_file.read() 

445 input_file.close() 

446 else: 

447 text = sys.stdin.read() 

448 

449 text = text.lstrip('\ufeff') # remove the byte-order mark 

450 

451 # Convert 

452 html = self.convert(text) 

453 

454 # Write to file or stdout 

455 if output: 

456 if isinstance(output, str): 

457 output_file = codecs.open(output, "w", 

458 encoding=encoding, 

459 errors="xmlcharrefreplace") 

460 output_file.write(html) 

461 output_file.close() 

462 else: 

463 writer = codecs.getwriter(encoding) 

464 output_file = writer(output, errors="xmlcharrefreplace") 

465 output_file.write(html) 

466 # Don't close here. User may want to write more. 

467 else: 

468 # Encode manually and write bytes to stdout. 

469 html = html.encode(encoding, "xmlcharrefreplace") 

470 sys.stdout.buffer.write(html) 

471 

472 return self 

473 

474 

475""" 

476EXPORTED FUNCTIONS 

477============================================================================= 

478 

479Those are the two functions we really mean to export: `markdown()` and 

480`markdownFromFile()`. 

481""" 

482 

483 

484def markdown(text: str, **kwargs: Any) -> str: 

485 """ 

486 Convert a markdown string to HTML and return HTML as a Unicode string. 

487 

488 This is a shortcut function for [`Markdown`][markdown.Markdown] class to cover the most 

489 basic use case. It initializes an instance of [`Markdown`][markdown.Markdown], loads the 

490 necessary extensions and runs the parser on the given text. 

491 

492 Arguments: 

493 text: Markdown formatted text as Unicode or ASCII string. 

494 

495 Keyword arguments: 

496 **kwargs: Any arguments accepted by the Markdown class. 

497 

498 Returns: 

499 A string in the specified output format. 

500 

501 !!! warning 

502 The Python-Markdown library does ***not*** sanitize its HTML output. 

503 If you are processing Markdown input from an untrusted source, it is your 

504 responsibility to ensure that it is properly sanitized. For more 

505 information see [Sanitizing HTML Output](../../sanitization.md). 

506 

507 """ 

508 md = Markdown(**kwargs) 

509 return md.convert(text) 

510 

511 

512def markdownFromFile(**kwargs: Any): 

513 """ 

514 Read Markdown text from a file or stream and write HTML output to a file or stream. 

515 

516 This is a shortcut function which initializes an instance of [`Markdown`][markdown.Markdown], 

517 and calls the [`convertFile`][markdown.Markdown.convertFile] method rather than 

518 [`convert`][markdown.Markdown.convert]. 

519 

520 Keyword arguments: 

521 input (str | BinaryIO): A file name or readable object. 

522 output (str | BinaryIO): A file name or writable object. 

523 encoding (str): Encoding of input and output. 

524 **kwargs: Any arguments accepted by the `Markdown` class. 

525 

526 !!! warning 

527 The Python-Markdown library does ***not*** sanitize its HTML output. 

528 As `markdown.markdownFromFile` writes directly to the file system, there is no 

529 easy way to sanitize the output from Python code. Therefore, it is 

530 recommended that the `markdown.markdownFromFile` function not be used on input 

531 from an untrusted source. For more information see [Sanitizing HTML 

532 Output](../../sanitization.md). 

533 

534 """ 

535 md = Markdown(**kwargs) 

536 md.convertFile(kwargs.get('input', None), 

537 kwargs.get('output', None), 

538 kwargs.get('encoding', None))