"""Convert HTML to docutils nodes."""
from __future__ import annotations
import re
from typing import TYPE_CHECKING
from docutils import nodes
from myst_parser.parsers.parse_html import Data, tokenize_html
if TYPE_CHECKING:
from .base import DocutilsRenderer
def make_error(
document: nodes.document, error_msg: str, text: str, line_number: int
) -> nodes.system_message:
return document.reporter.error(
error_msg,
nodes.literal_block(text, text),
line=line_number,
)
OPTION_KEYS_IMAGE = {"class", "alt", "height", "width", "align", "name"}
# note: docutils also has scale and target
OPTION_KEYS_ADMONITION = {"class", "name"}
# See https://github.com/micromark/micromark-extension-gfm-tagfilter
RE_FLOW = re.compile(
r"<(\/?)(iframe|noembed|noframes|plaintext|script|style|title|textarea|xmp)(?=[\t\n\f\r />])",
re.IGNORECASE,
)
def default_html(text: str, source: str, line_number: int) -> list[nodes.Element]:
raw_html = nodes.raw("", text, format="html")
raw_html.source = source
raw_html.line = line_number
return [raw_html]
def html_to_nodes(
text: str, line_number: int, renderer: DocutilsRenderer
) -> list[nodes.Element]:
"""Convert HTML to docutils nodes."""
if renderer.md_config.gfm_only:
text, _ = RE_FLOW.subn(lambda s: s.group(0).replace("<", "<"), text)
enable_html_img = "html_image" in renderer.md_config.enable_extensions
enable_html_admonition = "html_admonition" in renderer.md_config.enable_extensions
if not (enable_html_img or enable_html_admonition):
return default_html(text, renderer.document["source"], line_number)
# parse the HTML to AST
try:
root = tokenize_html(text).strip(inplace=True, recurse=False)
except Exception:
msg_node = renderer.create_warning(
"HTML could not be parsed", line=line_number, subtype="html"
)
return ([msg_node] if msg_node else []) + default_html(
text, renderer.document["source"], line_number
)
if len(root) < 1:
# if empty
return default_html(text, renderer.document["source"], line_number)
if not all(
(enable_html_img and child.name == "img")
or (
enable_html_admonition
and child.name == "div"
and "admonition" in child.attrs.classes
)
for child in root
):
return default_html(text, renderer.document["source"], line_number)
nodes_list = []
for child in root:
if child.name == "img":
if "src" not in child.attrs:
return [
renderer.reporter.error(
"
missing 'src' attribute", line=line_number
)
]
content = "\n".join(
f":{k}: {v}"
for k, v in sorted(child.attrs.items())
if k in OPTION_KEYS_IMAGE
)
nodes_list.extend(
renderer.run_directive(
"image", child.attrs["src"], content, line_number
)
)
else:
children = child.strip().children
if (
children
and children[0].name in ("div", "p")
and (
"title" in children[0].attrs.classes
or "admonition-title" in children[0].attrs.classes
)
):
title = "".join(child.render() for child in children.pop(0))
else:
title = "Note"
options = "\n".join(
f":{k}: {v}"
for k, v in sorted(child.attrs.items())
if k in OPTION_KEYS_ADMONITION
).rstrip()
new_children = []
for child in children:
if child.name == "p":
new_children.extend(child.children)
new_children.append(Data("\n\n"))
else:
new_children.append(child)
content = (
options
+ ("\n\n" if options else "")
+ "".join(child.render() for child in new_children).lstrip()
)
nodes_list.extend(
renderer.run_directive("admonition", title, content, line_number)
)
return nodes_list