superfences.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988
  1. """
  2. SuperFences.
  3. pymdownx.superfences
  4. Nested Fenced Code Blocks
  5. This is a modification of the original Fenced Code Extension.
  6. Algorithm has been rewritten to allow for fenced blocks in blockquotes,
  7. lists, etc. And also , allow for special UML fences like 'flow' for flowcharts
  8. and `sequence` for sequence diagrams.
  9. Modified: 2014 - 2017 Isaac Muse <isaacmuse@gmail.com>
  10. ---
  11. Fenced Code Extension for Python Markdown
  12. =========================================
  13. This extension adds Fenced Code Blocks to Python-Markdown.
  14. See <https://pythonhosted.org/Markdown/extensions/fenced_code_blocks.html>
  15. for documentation.
  16. Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/).
  17. All changes Copyright 2008-2014 The Python Markdown Project
  18. License: [BSD](http://www.opensource.org/licenses/bsd-license.php)
  19. """
  20. from markdown.extensions import Extension
  21. from markdown.preprocessors import Preprocessor
  22. from markdown.postprocessors import Postprocessor
  23. from markdown.blockprocessors import CodeBlockProcessor
  24. from markdown import util as md_util
  25. from .util import PymdownxDeprecationWarning
  26. import warnings
  27. import functools
  28. import re
  29. SOH = '\u0001' # start
  30. EOT = '\u0004' # end
  31. PREFIX_CHARS = ('>', ' ', '\t')
  32. RE_NESTED_FENCE_START = re.compile(
  33. r'''(?x)
  34. (?P<fence>~{3,}|`{3,})[ \t]* # Fence opening
  35. (?:(?:(?P<bracket_open>\{)|\.?(?P<lang>[\w#.+-]*))?)[ \t]* # Language opening
  36. (?P<options>
  37. (?:
  38. (?:\b[a-zA-Z][a-zA-Z0-9_]*=(?:(?P<quot>"|').*?(?P=quot))?[ \t]*) | # Options
  39. (?:(?<=[{ \t])[.\#][\w#.+-]+[ \t]*) # Class or ID
  40. )*
  41. )
  42. (?P<bracket_close>})?[ \t]*$ # Language closing
  43. '''
  44. )
  45. RE_HL_LINES = re.compile(r'^(?P<hl_lines>\d+(?:-\d+)?(?:[ \t]+\d+(?:-\d+)?)*)$')
  46. RE_LINENUMS = re.compile(r'(?P<linestart>[\d]+)(?:[ \t]+(?P<linestep>[\d]+))?(?:[ \t]+(?P<linespecial>[\d]+))?')
  47. RE_OPTIONS = re.compile(
  48. r'''(?x)
  49. (?:
  50. (?P<key>[a-zA-Z][a-zA-Z0-9_]*)=(?:(?P<quot>"|')(?P<value>.*?)(?P=quot))? |
  51. (?P<class_id>(?:(?<=[ \t])|(?<=^))[.\#][\w#.+-]+)
  52. )
  53. '''
  54. )
  55. RE_TAB_DIV = re.compile(
  56. r'<div (?:class="tabbed-set" data-tabs="(\d+:\d+)"|data-tabs="(\d+:\d+)" class="tabbed-set")'
  57. )
  58. RE_TABS = re.compile(r'((?:<p><superfences>.*?</superfences></p>\s*)+)', re.DOTALL)
  59. TAB = (
  60. '<superfences>'
  61. '<input name="__tabbed_%%(index)s" type="radio" id="__tabbed_%%(index)s_%%(tab_index)s" %%(state)s/>'
  62. '<label for="__tabbed_%%(index)s_%%(tab_index)s">%(title)s</label>'
  63. '<div class="%(content)s">%(code)s</div>'
  64. '</superfences>'
  65. )
  66. NESTED_FENCE_END = r'%s[ \t]*$'
  67. FENCED_BLOCK_RE = re.compile(
  68. r'^([\> ]*)%s(%s)%s$' % (
  69. md_util.HTML_PLACEHOLDER[0],
  70. md_util.HTML_PLACEHOLDER[1:-1] % r'([0-9]+)',
  71. md_util.HTML_PLACEHOLDER[-1]
  72. )
  73. )
  74. MSG_TAB_DEPRECATION = """
  75. The tab option in SuperFences has been deprecated in favor of the general purpose 'pymdownx.tabbed' extension.
  76. While you can continue to use this feature for now, it will be removed in the future.
  77. Also be mindful of the class changes, if you require old style classes, please enable the 'legacy_tab_classes' option.
  78. """
  79. def _escape(txt):
  80. """Basic html escaping."""
  81. txt = txt.replace('&', '&amp;')
  82. txt = txt.replace('<', '&lt;')
  83. txt = txt.replace('>', '&gt;')
  84. return txt
  85. class CodeStash(object):
  86. """
  87. Stash code for later retrieval.
  88. Store original fenced code here in case we were
  89. too greedy and need to restore in an indented code
  90. block.
  91. """
  92. def __init__(self):
  93. """Initialize."""
  94. self.stash = {}
  95. def __len__(self): # pragma: no cover
  96. """Length of stash."""
  97. return len(self.stash)
  98. def get(self, key, default=None):
  99. """Get the code from the key."""
  100. code = self.stash.get(key, default)
  101. return code
  102. def remove(self, key):
  103. """Remove the stashed code."""
  104. del self.stash[key]
  105. def store(self, key, code, indent_level):
  106. """Store the code in the stash."""
  107. self.stash[key] = (code, indent_level)
  108. def clear_stash(self):
  109. """Clear the stash."""
  110. self.stash = {}
  111. def fence_code_format(source, language, css_class, options, md, classes=None, id_value='', **kwargs):
  112. """Format source as code blocks."""
  113. if id_value:
  114. id_value = ' id="{}"'.format(id_value)
  115. classes = css_class if classes is None else ' '.join(classes + [css_class])
  116. return '<pre%s class="%s"><code>%s</code></pre>' % (id_value, classes, _escape(source))
  117. def fence_div_format(source, language, css_class, options, md, classes=None, id_value='', **kwargs):
  118. """Format source as div."""
  119. if id_value:
  120. id_value = ' id="{}"'.format(id_value)
  121. classes = css_class if classes is None else ' '.join(classes + [css_class])
  122. return '<div%s class="%s">%s</div>' % (id_value, classes, _escape(source))
  123. def highlight_validator(language, options):
  124. """Highlight validator."""
  125. okay = True
  126. # Check for invalid keys
  127. for k in options.keys():
  128. if k not in ('hl_lines', 'linenums'):
  129. okay = False
  130. break
  131. # Check format of valid keys
  132. if okay:
  133. for opt, validator in (('hl_lines', RE_HL_LINES), ('linenums', RE_LINENUMS)):
  134. if opt in options:
  135. value = options[opt]
  136. if value is True or validator.match(options[opt]) is None:
  137. okay = False
  138. break
  139. return okay
  140. def default_validator(language, options):
  141. """Default validator."""
  142. return len(options) == 0
  143. def _validator(language, options, validator=None):
  144. """Validator wrapper."""
  145. return validator(language, options)
  146. def _formatter(source, language, options, md, class_name="", _fmt=None, **kwargs):
  147. """Formatter wrapper."""
  148. return _fmt(source, language, class_name, options, md, **kwargs)
  149. def _test(language, test_language=None):
  150. """Test language."""
  151. return test_language is None or test_language == "*" or language == test_language
  152. class SuperFencesCodeExtension(Extension):
  153. """SuperFences code block extension."""
  154. def __init__(self, *args, **kwargs):
  155. """Initialize."""
  156. self.superfences = []
  157. self.config = {
  158. 'disable_indented_code_blocks': [False, "Disable indented code blocks - Default: False"],
  159. 'custom_fences': [[], 'Specify custom fences. Default: See documentation.'],
  160. 'highlight_code': [True, "Deprecated and does nothing"],
  161. 'css_class': [
  162. '',
  163. "Set class name for wrapper element. The default of CodeHilite or Highlight will be used"
  164. "if nothing is set. - "
  165. "Default: ''"
  166. ],
  167. 'preserve_tabs': [False, "Preserve tabs in fences - Default: False"],
  168. 'legacy_tab_classes': [
  169. False,
  170. "Use legacy style classes for the deprecated tabbed code feature via 'tab=\"name\"'"
  171. ]
  172. }
  173. super(SuperFencesCodeExtension, self).__init__(*args, **kwargs)
  174. def extend_super_fences(self, name, formatter, validator):
  175. """Extend SuperFences with the given name, language, and formatter."""
  176. obj = {
  177. "name": name,
  178. "test": functools.partial(_test, test_language=name),
  179. "formatter": formatter,
  180. "validator": validator
  181. }
  182. if name == '*':
  183. self.superfences[0] = obj
  184. else:
  185. self.superfences.append(obj)
  186. def extendMarkdown(self, md):
  187. """Add fenced block preprocessor to the Markdown instance."""
  188. # Not super yet, so let's make it super
  189. md.registerExtension(self)
  190. config = self.getConfigs()
  191. # Default fenced blocks
  192. self.superfences.insert(
  193. 0,
  194. {
  195. "name": "superfences",
  196. "test": _test,
  197. "formatter": None,
  198. "validator": functools.partial(_validator, validator=highlight_validator)
  199. }
  200. )
  201. # Custom Fences
  202. custom_fences = config.get('custom_fences', [])
  203. for custom in custom_fences:
  204. name = custom.get('name')
  205. class_name = custom.get('class')
  206. fence_format = custom.get('format', fence_code_format)
  207. validator = custom.get('validator', default_validator)
  208. if name is not None and class_name is not None:
  209. self.extend_super_fences(
  210. name,
  211. functools.partial(_formatter, class_name=class_name, _fmt=fence_format),
  212. functools.partial(_validator, validator=validator)
  213. )
  214. self.md = md
  215. self.patch_fenced_rule()
  216. self.stash = CodeStash()
  217. def patch_fenced_rule(self):
  218. """
  219. Patch Python Markdown with our own fenced block extension.
  220. We don't attempt to protect against a user loading the `fenced_code` extension with this.
  221. Most likely they will have issues, but they shouldn't have loaded them together in the first place :).
  222. """
  223. config = self.getConfigs()
  224. fenced = SuperFencesBlockPreprocessor(self.md)
  225. fenced.config = config
  226. fenced.extension = self
  227. if self.superfences[0]['name'] == "superfences":
  228. self.superfences[0]["formatter"] = fenced.highlight
  229. self.md.preprocessors.register(fenced, "fenced_code_block", 25)
  230. indented_code = SuperFencesCodeBlockProcessor(self.md.parser)
  231. indented_code.config = config
  232. indented_code.extension = self
  233. self.md.parser.blockprocessors.register(indented_code, "code", 80)
  234. if config["preserve_tabs"]:
  235. # Need to squeeze in right after critic.
  236. raw_fenced = SuperFencesRawBlockPreprocessor(self.md)
  237. raw_fenced.config = config
  238. raw_fenced.extension = self
  239. self.md.preprocessors.register(raw_fenced, "fenced_raw_block", 31.05)
  240. self.md.registerExtensions(["pymdownx._bypassnorm"], {})
  241. tabbed = SuperFencesTabPostProcessor(self.md)
  242. tabbed.config = config
  243. self.md.postprocessors.register(tabbed, "fenced_tabs", 25)
  244. # Add the highlight extension, but do so in a disabled state so we can just retrieve default configurations
  245. self.md.registerExtensions(["pymdownx.highlight"], {"pymdownx.highlight": {"_enabled": False}})
  246. def reset(self):
  247. """Clear the stash."""
  248. self.stash.clear_stash()
  249. class SuperFencesTabPostProcessor(Postprocessor):
  250. """Post processor for grouping tabs."""
  251. def repl(self, m):
  252. """Replace grouped superfences tabs with a tab group."""
  253. self.count += 1
  254. tab_count = 0
  255. tabs = []
  256. for entry in [x.strip() for x in m.group(1).split('</superfences></p>')]:
  257. tab_count += 1
  258. tabs.append(
  259. entry.replace('<p><superfences>', '') % {
  260. 'index': self.count,
  261. 'tab_index': tab_count,
  262. 'state': ('checked="checked" ' if tab_count == 1 else ''),
  263. 'tab_title': 'Tab %d' % (tab_count)
  264. }
  265. )
  266. return '<div class="%s" data-tabs="%d:%d">\n%s</div>\n' % (
  267. self.class_name,
  268. self.count,
  269. tab_count - 1,
  270. '\n'.join(tabs)
  271. )
  272. def run(self, text):
  273. """Search for superfences tab and group consecutive tabs together."""
  274. if self.config.get('legacy_tab_classes', False):
  275. self.class_name = 'superfences-tabs'
  276. else:
  277. self.class_name = 'tabbed-set'
  278. highest_set = 0
  279. for m in RE_TAB_DIV.finditer(text):
  280. data = int((m.group(1) if m.group(1) else m.group(2)).split(':')[0])
  281. if data > highest_set:
  282. highest_set = data
  283. self.count = highest_set
  284. return RE_TABS.sub(self.repl, text)
  285. class SuperFencesBlockPreprocessor(Preprocessor):
  286. """
  287. Preprocessor to find fenced code blocks.
  288. Because this is done as a preprocessor, it might be too greedy.
  289. We will stash the blocks code and restore if we mistakenly processed
  290. text from an indented code block.
  291. """
  292. CODE_WRAP = '<pre%s><code%s>%s</code></pre>'
  293. def __init__(self, md):
  294. """Initialize."""
  295. super(SuperFencesBlockPreprocessor, self).__init__(md)
  296. self.tab_len = self.md.tab_length
  297. self.checked_hl_settings = False
  298. self.codehilite_conf = {}
  299. def normalize_ws(self, text):
  300. """Normalize whitespace."""
  301. return text.expandtabs(self.tab_len)
  302. def rebuild_block(self, lines):
  303. """Dedent the fenced block lines."""
  304. return '\n'.join([line[self.ws_virtual_len:] for line in lines])
  305. def get_hl_settings(self):
  306. """Check for Highlight extension to get its configurations."""
  307. if not self.checked_hl_settings:
  308. self.checked_hl_settings = True
  309. if not self.config['highlight_code']:
  310. warnings.warn(
  311. "Disabling of 'highlight_code' is deprecated and no longer does anything.",
  312. PymdownxDeprecationWarning
  313. )
  314. config = None
  315. self.highlighter = None
  316. for ext in self.md.registeredExtensions:
  317. try:
  318. config = getattr(ext, "get_pymdownx_highlight_settings")()
  319. self.highlighter = getattr(ext, "get_pymdownx_highlighter")()
  320. break
  321. except AttributeError:
  322. pass
  323. css_class = self.config['css_class']
  324. self.css_class = css_class if css_class else config['css_class']
  325. self.extend_pygments_lang = config.get('extend_pygments_lang', None)
  326. self.guess_lang = config['guess_lang']
  327. self.pygments_style = config['pygments_style']
  328. self.use_pygments = config['use_pygments']
  329. self.noclasses = config['noclasses']
  330. self.linenums = config['linenums']
  331. self.linenums_style = config.get('linenums_style', 'table')
  332. self.linenums_class = config.get('linenums_class', 'linenums')
  333. self.linenums_special = config.get('linenums_special', -1)
  334. self.wrapcode = not config.get('legacy_no_wrap_code', False)
  335. def clear(self):
  336. """Reset the class variables."""
  337. self.ws = None
  338. self.ws_len = 0
  339. self.ws_virtual_len = 0
  340. self.fence = None
  341. self.lang = None
  342. self.quote_level = 0
  343. self.code = []
  344. self.empty_lines = 0
  345. self.fence_end = None
  346. self.tab = None
  347. self.options = {}
  348. self.classes = []
  349. self.id = ''
  350. def eval_fence(self, ws, content, start, end):
  351. """Evaluate a normal fence."""
  352. if (ws + content).strip() == '':
  353. # Empty line is okay
  354. self.empty_lines += 1
  355. self.code.append(ws + content)
  356. elif len(ws) != self.ws_virtual_len and content != '':
  357. # Not indented enough
  358. self.clear()
  359. elif self.fence_end.match(content) is not None and not content.startswith((' ', '\t')):
  360. # End of fence
  361. self.process_nested_block(ws, content, start, end)
  362. else:
  363. # Content line
  364. self.empty_lines = 0
  365. self.code.append(ws + content)
  366. def eval_quoted(self, ws, content, quote_level, start, end):
  367. """Evaluate fence inside a blockquote."""
  368. if quote_level > self.quote_level:
  369. # Quote level exceeds the starting quote level
  370. self.clear()
  371. elif quote_level <= self.quote_level:
  372. if content == '':
  373. # Empty line is okay
  374. self.code.append(ws + content)
  375. self.empty_lines += 1
  376. elif len(ws) < self.ws_len:
  377. # Not indented enough
  378. self.clear()
  379. elif self.empty_lines and quote_level < self.quote_level:
  380. # Quote levels don't match and we are signified
  381. # the end of the block with an empty line
  382. self.clear()
  383. elif self.fence_end.match(content) is not None:
  384. # End of fence
  385. self.process_nested_block(ws, content, start, end)
  386. else:
  387. # Content line
  388. self.empty_lines = 0
  389. self.code.append(ws + content)
  390. def get_tab(self, code, title):
  391. """Wrap code in tab div."""
  392. return TAB % {
  393. 'code': code.replace('%', '%%'),
  394. 'title': title,
  395. 'content': 'superfences-content' if self.legacy_tab_classes else 'tabbed-content'
  396. }
  397. def process_nested_block(self, ws, content, start, end):
  398. """Process the contents of the nested block."""
  399. self.last = ws + self.normalize_ws(content)
  400. code = None
  401. for entry in reversed(self.extension.superfences):
  402. if entry["test"](self.lang):
  403. self.line_count = end - start - 2
  404. try:
  405. code = entry["formatter"](
  406. self.rebuild_block(self.code),
  407. self.lang,
  408. self.options,
  409. self.md,
  410. classes=self.classes,
  411. id_value=self.id
  412. )
  413. except TypeError: # pragma: no cover
  414. code = entry["formatter"](self.rebuild_block(self.code), self.lang, self.options, self.md)
  415. warnings.warn(
  416. "Custom fence Formatters are required to accept keyword parameters 'classes' and 'id_value'",
  417. UserWarning
  418. )
  419. if self.tab is not None:
  420. code = self.get_tab(code, self.tab)
  421. break
  422. if code is not None:
  423. self._store(self.normalize_ws('\n'.join(self.code)) + '\n', code, start, end)
  424. self.clear()
  425. def normalize_hl_line(self, number):
  426. """
  427. Normalize highlight line number.
  428. Clamp outrages numbers. Numbers out of range will be only one increment out range.
  429. This prevents people from create massive buffers of line numbers that exceed real
  430. number of code lines.
  431. """
  432. number = int(number)
  433. if number < 1:
  434. number = 0
  435. elif number > self.line_count:
  436. number = self.line_count + 1
  437. return number
  438. def parse_hl_lines(self, hl_lines):
  439. """Parse the lines to highlight."""
  440. lines = []
  441. if hl_lines:
  442. for entry in hl_lines.split():
  443. line_range = [self.normalize_hl_line(e) for e in entry.split('-')]
  444. if len(line_range) > 1:
  445. if line_range[0] <= line_range[1]:
  446. lines.extend(list(range(line_range[0], line_range[1] + 1)))
  447. elif 1 <= line_range[0] <= self.line_count:
  448. lines.extend(line_range)
  449. return lines
  450. def parse_line_start(self, linestart):
  451. """Parse line start."""
  452. return int(linestart) if linestart else -1
  453. def parse_line_step(self, linestep):
  454. """Parse line start."""
  455. step = int(linestep) if linestep else -1
  456. return step if step > 1 else -1
  457. def parse_line_special(self, linespecial):
  458. """Parse line start."""
  459. return int(linespecial) if linespecial else -1
  460. def parse_fence_line(self, line):
  461. """Parse fence line."""
  462. ws_len = 0
  463. ws_virtual_len = 0
  464. ws = []
  465. index = 0
  466. for c in line:
  467. if ws_virtual_len >= self.ws_virtual_len:
  468. break
  469. if c not in PREFIX_CHARS:
  470. break
  471. ws_len += 1
  472. if c == '\t':
  473. tab_size = self.tab_len - (index % self.tab_len)
  474. ws_virtual_len += tab_size
  475. ws.append(' ' * tab_size)
  476. else:
  477. tab_size = 1
  478. ws_virtual_len += 1
  479. ws.append(c)
  480. index += tab_size
  481. return ''.join(ws), line[ws_len:]
  482. def parse_whitespace(self, line):
  483. """Parse the whitespace (blockquote syntax is counted as well)."""
  484. self.ws_len = 0
  485. self.ws_virtual_len = 0
  486. ws = []
  487. for c in line:
  488. if c not in PREFIX_CHARS:
  489. break
  490. self.ws_len += 1
  491. ws.append(c)
  492. ws = self.normalize_ws(''.join(ws))
  493. self.ws_virtual_len = len(ws)
  494. return ws
  495. def parse_options(self, string, allow_class_id=False):
  496. """Get options."""
  497. okay = True
  498. self.options = {}
  499. for m in RE_OPTIONS.finditer(string):
  500. if m.group('class_id'):
  501. if not allow_class_id:
  502. return False
  503. item = m.group('class_id')
  504. if item.startswith('.'):
  505. self.classes.append(item[1:])
  506. else:
  507. self.id = item[1:]
  508. else:
  509. key = m.group('key')
  510. value = m.group('value')
  511. if value is None:
  512. value = True
  513. self.options[key] = value
  514. # Global options (remove as we handle them)
  515. if 'tab' in self.options:
  516. warnings.warn(MSG_TAB_DEPRECATION, PymdownxDeprecationWarning)
  517. self.tab = self.options['tab']
  518. if not self.tab or self.tab is True:
  519. self.tab = self.lang
  520. if not self.tab:
  521. self.tab = '%(tab_title)s'
  522. del self.options['tab']
  523. # Run per language validator
  524. for entry in reversed(self.extension.superfences):
  525. if entry["test"](self.lang):
  526. validator = entry.get("validator", functools.partial(_validator, validator=default_validator))
  527. okay = validator(self.lang, self.options)
  528. break
  529. return okay
  530. def search_nested(self, lines):
  531. """Search for nested fenced blocks."""
  532. count = 0
  533. for line in lines:
  534. # Strip carriage returns if the lines end with them.
  535. # This is necessary since we are handling preserved tabs
  536. # Before whitespace normalization.
  537. line = line.rstrip('\r')
  538. if self.fence is None:
  539. ws = self.parse_whitespace(line)
  540. # Found the start of a fenced block.
  541. m = RE_NESTED_FENCE_START.match(line, self.ws_len)
  542. if m is not None:
  543. if (
  544. (m.group('bracket_open') and not m.group('bracket_close')) or
  545. (not m.group('bracket_open') and m.group('bracket_close'))
  546. ):
  547. self.clear()
  548. else:
  549. start = count
  550. is_attr_list = m.group('bracket_open')
  551. self.first = ws + self.normalize_ws(m.group(0))
  552. self.ws = ws
  553. self.quote_level = self.ws.count(">")
  554. self.empty_lines = 0
  555. self.fence = m.group('fence')
  556. if not is_attr_list:
  557. self.lang = m.group('lang')
  558. if self.parse_options(m.group('options'), is_attr_list):
  559. if is_attr_list:
  560. self.lang = self.classes.pop(0) if self.classes else ''
  561. self.fence_end = re.compile(NESTED_FENCE_END % self.fence)
  562. else:
  563. # Option parsing failed, abandon fence
  564. self.clear()
  565. else:
  566. # Evaluate lines
  567. # - Determine if it is the ending line or content line
  568. # - If is a content line, make sure it is all indented
  569. # with the opening and closing lines (lines with just
  570. # whitespace will be stripped so those don't matter).
  571. # - When content lines are inside blockquotes, make sure
  572. # the nested block quote levels make sense according to
  573. # blockquote rules.
  574. ws, content = self.parse_fence_line(line)
  575. end = count + 1
  576. quote_level = ws.count(">")
  577. if self.quote_level:
  578. # Handle blockquotes
  579. self.eval_quoted(ws, content, quote_level, start, end)
  580. elif quote_level == 0:
  581. # Handle all other cases
  582. self.eval_fence(ws, content, start, end)
  583. else:
  584. # Looks like we got a blockquote line
  585. # when not in a blockquote.
  586. self.clear()
  587. count += 1
  588. return self.reassemble(lines)
  589. def reassemble(self, lines):
  590. """Reassemble text."""
  591. # Now that we are done iterating the lines,
  592. # let's replace the original content with the
  593. # fenced blocks.
  594. while len(self.stack):
  595. fenced, start, end = self.stack.pop()
  596. lines = lines[:start] + [fenced] + lines[end:]
  597. return lines
  598. def highlight(self, src, language, options, md, classes=None, id_value='', **kwargs):
  599. """
  600. Syntax highlight the code block.
  601. If configuration is not empty, then the CodeHilite extension
  602. is enabled, so we call into it to highlight the code.
  603. """
  604. if classes is None: # pragma: no cover
  605. classes = []
  606. # Default format options
  607. linestep = None
  608. linestart = None
  609. linespecial = None
  610. hl_lines = None
  611. if 'hl_lines' in options:
  612. m = RE_HL_LINES.match(options['hl_lines'])
  613. hl_lines = m.group('hl_lines')
  614. if 'linenums' in options:
  615. m = RE_LINENUMS.match(options['linenums'])
  616. linestart = m.group('linestart')
  617. linestep = m.group('linestep')
  618. linespecial = m.group('linespecial')
  619. linestep = self.parse_line_step(linestep)
  620. linestart = self.parse_line_start(linestart)
  621. linespecial = self.parse_line_special(linespecial)
  622. hl_lines = self.parse_hl_lines(hl_lines)
  623. el = self.highlighter(
  624. guess_lang=self.guess_lang,
  625. pygments_style=self.pygments_style,
  626. use_pygments=self.use_pygments,
  627. noclasses=self.noclasses,
  628. linenums=self.linenums,
  629. linenums_style=self.linenums_style,
  630. linenums_special=self.linenums_special,
  631. linenums_class=self.linenums_class,
  632. extend_pygments_lang=self.extend_pygments_lang,
  633. wrapcode=self.wrapcode
  634. ).highlight(
  635. src,
  636. language,
  637. self.css_class,
  638. hl_lines=hl_lines,
  639. linestart=linestart,
  640. linestep=linestep,
  641. linespecial=linespecial,
  642. classes=classes,
  643. id_value=id_value
  644. )
  645. return el
  646. def _store(self, source, code, start, end):
  647. """
  648. Store the fenced blocks in the stack to be replaced when done iterating.
  649. Store the original text in case we need to restore if we are too greedy.
  650. """
  651. # Save the fenced blocks to add once we are done iterating the lines
  652. placeholder = self.md.htmlStash.store(code)
  653. self.stack.append(('%s%s' % (self.ws, placeholder), start, end))
  654. if not self.disabled_indented:
  655. # If an indented block consumes this placeholder,
  656. # we can restore the original source
  657. self.extension.stash.store(
  658. placeholder[1:-1],
  659. "%s\n%s%s" % (self.first, self.normalize_ws(source), self.last),
  660. self.ws_virtual_len
  661. )
  662. def reindent(self, text, pos, level):
  663. """Reindent the code to where it is supposed to be."""
  664. indented = []
  665. for line in text.split('\n'):
  666. index = pos - level
  667. indented.append(line[index:])
  668. return indented
  669. def restore_raw_text(self, lines):
  670. """Revert a prematurely converted fenced block."""
  671. new_lines = []
  672. for line in lines:
  673. m = FENCED_BLOCK_RE.match(line)
  674. if m:
  675. key = m.group(2)
  676. indent_level = len(m.group(1))
  677. original = None
  678. original, pos = self.extension.stash.get(key)
  679. if original is not None:
  680. code = self.reindent(original, pos, indent_level)
  681. new_lines.extend(code)
  682. self.extension.stash.remove(key)
  683. if original is None: # pragma: no cover
  684. # Too much work to test this. This is just a fall back in case
  685. # we find a placeholder, and we went to revert it and it wasn't in our stash.
  686. # Most likely this would be caused by someone else. We just want to put it
  687. # back in the block if we can't revert it. Maybe we can do a more directed
  688. # unit test in the future.
  689. new_lines.append(line)
  690. else:
  691. new_lines.append(line)
  692. return new_lines
  693. def run(self, lines):
  694. """Search for fenced blocks."""
  695. self.get_hl_settings()
  696. self.clear()
  697. self.stack = []
  698. self.disabled_indented = self.config.get("disable_indented_code_blocks", False)
  699. self.preserve_tabs = self.config.get("preserve_tabs", False)
  700. self.legacy_tab_classes = self.config.get("legacy_tab_classes", False)
  701. if self.preserve_tabs:
  702. lines = self.restore_raw_text(lines)
  703. return self.search_nested(lines)
  704. class SuperFencesRawBlockPreprocessor(SuperFencesBlockPreprocessor):
  705. """Special class for preserving tabs before normalizing whitespace."""
  706. def process_nested_block(self, ws, content, start, end):
  707. """Process the contents of the nested block."""
  708. self.last = ws + self.normalize_ws(content)
  709. code = '\n'.join(self.code)
  710. self._store(code + '\n', code, start, end)
  711. self.clear()
  712. def _store(self, source, code, start, end):
  713. """
  714. Store the fenced blocks in the stack to be replaced when done iterating.
  715. Store the original text in case we need to restore if we are too greedy.
  716. """
  717. # Just get a placeholder, we won't ever actually retrieve this source
  718. placeholder = self.md.htmlStash.store('')
  719. self.stack.append(('%s%s' % (self.ws, placeholder), start, end))
  720. # Here is the source we'll actually retrieve.
  721. self.extension.stash.store(
  722. placeholder[1:-1],
  723. "%s\n%s%s" % (self.first, source, self.last),
  724. self.ws_virtual_len
  725. )
  726. def reassemble(self, lines):
  727. """Reassemble text."""
  728. # Now that we are done iterating the lines,
  729. # let's replace the original content with the
  730. # fenced blocks.
  731. while len(self.stack):
  732. fenced, start, end = self.stack.pop()
  733. lines = lines[:start] + [fenced.replace(md_util.STX, SOH, 1)[:-1] + EOT] + lines[end:]
  734. return lines
  735. def run(self, lines):
  736. """Search for fenced blocks."""
  737. self.clear()
  738. self.stack = []
  739. self.disabled_indented = self.config.get("disable_indented_code_blocks", False)
  740. return self.search_nested(lines)
  741. class SuperFencesCodeBlockProcessor(CodeBlockProcessor):
  742. """Process indented code blocks to see if we accidentally processed its content as a fenced block."""
  743. def test(self, parent, block):
  744. """Test method that is one day to be deprecated."""
  745. return True
  746. def reindent(self, text, pos, level):
  747. """Reindent the code to where it is supposed to be."""
  748. indented = []
  749. for line in text.split('\n'):
  750. index = pos - level
  751. indented.append(line[index:])
  752. return '\n'.join(indented)
  753. def revert_greedy_fences(self, block):
  754. """Revert a prematurely converted fenced block."""
  755. new_block = []
  756. for line in block.split('\n'):
  757. m = FENCED_BLOCK_RE.match(line)
  758. if m:
  759. key = m.group(2)
  760. indent_level = len(m.group(1))
  761. original = None
  762. original, pos = self.extension.stash.get(key)
  763. if original is not None:
  764. code = self.reindent(original, pos, indent_level)
  765. new_block.append(code)
  766. self.extension.stash.remove(key)
  767. if original is None: # pragma: no cover
  768. # Too much work to test this. This is just a fall back in case
  769. # we find a placeholder, and we went to revert it and it wasn't in our stash.
  770. # Most likely this would be caused by someone else. We just want to put it
  771. # back in the block if we can't revert it. Maybe we can do a more directed
  772. # unit test in the future.
  773. new_block.append(line)
  774. else:
  775. new_block.append(line)
  776. return '\n'.join(new_block)
  777. def run(self, parent, blocks):
  778. """Look for and parse code block."""
  779. handled = False
  780. if not self.config.get("disable_indented_code_blocks", False):
  781. handled = CodeBlockProcessor.test(self, parent, blocks[0])
  782. if handled:
  783. if self.config.get("nested", True):
  784. blocks[0] = self.revert_greedy_fences(blocks[0])
  785. handled = CodeBlockProcessor.run(self, parent, blocks) is not False
  786. return handled
  787. def makeExtension(*args, **kwargs):
  788. """Return extension."""
  789. return SuperFencesCodeExtension(*args, **kwargs)