mirror of
https://github.com/zulip/zulip.git
synced 2025-11-14 19:06:09 +00:00
markdown: Render nested multi-line code blocks correctly.
This commit adds a Markdown tree-processor extension that renders
multi-line code blocks that are nested inside lists with the
formatting. Note that the code block could be nested inside multiple
list levels and would still get rendered correctly.
Tim: This fixes the need for unpleasant workarounds like
f5bfa4e793 and makes nested code blocks
in our documentation look exactly how users would expect them to.
This commit is contained in:
74
zerver/lib/bugdown/nested_code_blocks.py
Normal file
74
zerver/lib/bugdown/nested_code_blocks.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from markdown.extensions import Extension
|
||||
from markdown.treeprocessors import Treeprocessor
|
||||
from typing import Any, Dict, Optional, List, Tuple
|
||||
import markdown
|
||||
from xml.etree.cElementTree import Element
|
||||
|
||||
from zerver.lib.bugdown import walk_tree_with_family, ResultWithFamily
|
||||
|
||||
class NestedCodeBlocksRenderer(Extension):
|
||||
def extendMarkdown(self, md: markdown.Markdown, md_globals: Dict[str, Any]) -> None:
|
||||
md.treeprocessors.add(
|
||||
'nested_code_blocks',
|
||||
NestedCodeBlocksRendererTreeProcessor(md, self.getConfigs()),
|
||||
'_end'
|
||||
)
|
||||
|
||||
class NestedCodeBlocksRendererTreeProcessor(markdown.treeprocessors.Treeprocessor):
|
||||
def __init__(self, md: markdown.Markdown, config: Dict[str, Any]) -> None:
|
||||
super(NestedCodeBlocksRendererTreeProcessor, self).__init__(md)
|
||||
|
||||
def run(self, root: Element) -> None:
|
||||
code_tags = walk_tree_with_family(root, self.get_code_tags)
|
||||
nested_code_blocks = self.get_nested_code_blocks(code_tags)
|
||||
for block in nested_code_blocks:
|
||||
tag, text = block.result
|
||||
codehilite_block = self.get_codehilite_block(text)
|
||||
self.replace_element(block.family.grandparent,
|
||||
codehilite_block,
|
||||
block.family.parent)
|
||||
|
||||
def get_code_tags(self, e: Element) -> Optional[Tuple[str, Optional[str]]]:
|
||||
if e.tag == "code":
|
||||
return (e.tag, e.text)
|
||||
return None
|
||||
|
||||
def get_nested_code_blocks(
|
||||
self, code_tags: List[ResultWithFamily]
|
||||
) -> List[ResultWithFamily]:
|
||||
nested_code_blocks = []
|
||||
for code_tag in code_tags:
|
||||
parent = code_tag.family.parent # type: Any
|
||||
grandparent = code_tag.family.grandparent # type: Any
|
||||
if parent.tag == "p" and grandparent.tag == "li":
|
||||
# if the parent (<p>) has no text, that means that the <code>
|
||||
# element inside is its only child and thus, we can confidently
|
||||
# say that this is a nested code block
|
||||
if parent.text is None:
|
||||
nested_code_blocks.append(code_tag)
|
||||
|
||||
return nested_code_blocks
|
||||
|
||||
def get_codehilite_block(self, code_block_text: str) -> Element:
|
||||
div = markdown.util.etree.Element("div")
|
||||
div.set("class", "codehilite")
|
||||
pre = markdown.util.etree.SubElement(div, "pre")
|
||||
pre.text = code_block_text
|
||||
return div
|
||||
|
||||
def replace_element(
|
||||
self, parent: Optional[Element],
|
||||
replacement: markdown.util.etree.Element,
|
||||
element_to_replace: Element
|
||||
) -> None:
|
||||
if parent is None:
|
||||
return
|
||||
|
||||
children = parent.getchildren()
|
||||
for index, child in enumerate(children):
|
||||
if child is element_to_replace:
|
||||
parent.insert(index, replacement)
|
||||
parent.remove(element_to_replace)
|
||||
|
||||
def makeExtension(*args: Any, **kwargs: str) -> NestedCodeBlocksRenderer:
|
||||
return NestedCodeBlocksRenderer(kwargs)
|
||||
Reference in New Issue
Block a user