Skip to content

markdown_parser

math_pattern = re.compile('(\\$\\$.*?\\$\\$)') module-attribute

md = markdown.core.Markdown(extensions=['admonition']) module-attribute

typst_command_pattern = re.compile('#([A-Za-z][^\\s()\\[]*)(\\([^)]*\\))?(\\[[^\\]]*\\])?') module-attribute

escape_typst_characters(string)

Escape Typst special characters while preserving Typst commands and math.

Why

User content may contain Typst special characters like #, $, [ that would break compilation. Escaping prevents interpretation as commands. Existing Typst commands and math must remain unescaped.

Parameters:

  • string (str) –

    Text to escape.

Returns:

  • str

    Escaped string safe for Typst.

Source code in src/rendercv/renderer/templater/markdown_parser.py
def escape_typst_characters(string: str) -> str:  """Escape Typst special characters while preserving Typst commands and math.   Why:  User content may contain Typst special characters like `#`, `$`, `[` that  would break compilation. Escaping prevents interpretation as commands.  Existing Typst commands and math must remain unescaped.   Args:  string: Text to escape.   Returns:  Escaped string safe for Typst.  """  if string == "\n":  return string   # Find all the Typst commands, and keep them separate so that nothing is escaped  # inside the commands.  typst_command_mapping = {}  for i, match in enumerate(  itertools.chain(  math_pattern.finditer(string),  typst_command_pattern.finditer(string),  )  ):  dummy_name = f"RENDERCVTYPSTCOMMANDORMATH{i}"  typst_command_mapping[dummy_name] = match.group(0)  string = string.replace(typst_command_mapping[dummy_name], dummy_name)  typst_command_mapping[dummy_name] = typst_command_mapping[dummy_name].replace(  "$$", "$"  )   # Add the tail after the last match  escape_dictionary = {  "[": "\\[",  "]": "\\]",  "\\": "\\\\",  '"': '\\"',  "#": "\\#",  "$": "\\$",  "@": "\\@",  "%": "\\%",  "~": "\\~",  "_": "\\_",  "/": "\\/",  ">": "\\>",  "<": "\\<",  }   string = string.translate(str.maketrans(escape_dictionary))   # string.translate() only supports single-character replacements, so we need to  # handle the longer replacements separately.  longer_escape_dictionary = {  "* ": "#sym.ast.basic ",  "*": "#sym.ast.basic#h(0pt, weak: true) ",  }  for key, value in longer_escape_dictionary.items():  string = string.replace(key, value)   # Replace the dummy names with the full Typst commands  for dummy_name, full_command in typst_command_mapping.items():  string = string.replace(dummy_name, full_command)   return string 

markdown_to_html(markdown_string)

Convert Markdown string to HTML using python-markdown library.

Parameters:

  • markdown_string (str) –

    Markdown content.

Returns:

  • str

    HTML-formatted string.

Source code in src/rendercv/renderer/templater/markdown_parser.py
def markdown_to_html(markdown_string: str) -> str:  """Convert Markdown string to HTML using python-markdown library.   Args:  markdown_string: Markdown content.   Returns:  HTML-formatted string.  """  return markdown.markdown(markdown_string) 

markdown_to_typst(markdown_string)

Convert Markdown string to Typst markup.

Why

Users write content in Markdown for readability. Typst compilation requires Typst markup. Lines are processed independently to prevent emphasis markers on adjacent lines from interacting in the Markdown parser (single-newline-separated lines form one paragraph in Markdown, causing cross-line marker interference). Admonition blocks are kept together since they span multiple lines by design.

Parameters:

  • markdown_string (str) –

    Markdown content.

Returns:

  • str

    Typst-formatted string.

Source code in src/rendercv/renderer/templater/markdown_parser.py
def markdown_to_typst(markdown_string: str) -> str:  """Convert Markdown string to Typst markup.   Why:  Users write content in Markdown for readability. Typst compilation  requires Typst markup. Lines are processed independently to prevent  emphasis markers on adjacent lines from interacting in the Markdown  parser (single-newline-separated lines form one paragraph in Markdown,  causing cross-line marker interference). Admonition blocks are kept  together since they span multiple lines by design.   Args:  markdown_string: Markdown content.   Returns:  Typst-formatted string.  """  lines = markdown_string.split("\n")  result_parts: list[str] = []  i = 0  while i < len(lines):  if lines[i].startswith("!!!"):  # Admonition block: collect the !!! line + all following indented lines  block = [lines[i]]  i += 1  while i < len(lines) and lines[i].startswith(" "):  block.append(lines[i])  i += 1  result_parts.append(md.convert("\n".join(block)))  else:  result_parts.append(md.convert(lines[i]))  i += 1  return "\n".join(result_parts) 

to_typst_string(elem)

Recursively convert XML Element tree to Typst markup string.

Why

Python Markdown library outputs XML Element tree. Typst requires its own markup syntax for bold, italic, links, etc. Recursive traversal converts entire element tree including nested formatting.

Parameters:

  • elem (Element) –

    XML Element from Markdown parser.

Returns:

  • str

    Typst-formatted string.

Source code in src/rendercv/renderer/templater/markdown_parser.py
def to_typst_string(elem: Element) -> str:  """Recursively convert XML Element tree to Typst markup string.   Why:  Python Markdown library outputs XML Element tree. Typst requires its  own markup syntax for bold, italic, links, etc. Recursive traversal  converts entire element tree including nested formatting.   Args:  elem: XML Element from Markdown parser.   Returns:  Typst-formatted string.  """  result = []   # Handle the element's text content  if elem.text:  result.append(escape_typst_characters(elem.text))   # Process child elements  for child in elem:  match child.tag:  case "strong":  # Bold: **text** -> #strong[text]  inner = to_typst_string(child)  child_content = f"#strong[{inner}]"   case "em":  # Italic: *text* -> #emph[text]  inner = to_typst_string(child)  child_content = f"#emph[{inner}]"   case "code":  # Inline code: `text` -> `text`  # Code content is already escaped by the parser  child_content = f"`{child.text}`"   case "a":  # Link: [text](url) -> #link("url")[text]  href = child.get("href") if child.get("href") else "https://example.com"  inner = to_typst_string(child)  child_content = f'#link("{href}")[{inner}]'   case "div":  child_content = (  "#summary["  + to_typst_string(child).strip("\n").replace("\n", " \\ ")  + "]"  )   case _:  if getattr(child, "attrib", {}).get("class") == "admonition-title":  continue  child_content = to_typst_string(child)   result.append(child_content)   # Handle tail text (text after the closing tag of child)  if child.tail:  result.append(escape_typst_characters(child.tail))   return "".join(result)