feat: render nested card lists
This commit is contained in:
+100
-12
@@ -2542,14 +2542,104 @@ def render_tabular_html(body: str, equation_numbers: dict[str, int], figure_numb
|
||||
return f'<div class="table-wrap"><table>{"".join(html_rows)}</table></div>'
|
||||
|
||||
|
||||
LIST_BEGIN_RE = re.compile(r"\\begin\{(?P<kind>itemize|enumerate)\}(?:\[[^\]]*\])?")
|
||||
LIST_TOKEN_RE = re.compile(
|
||||
r"\\begin\{(?P<begin>itemize|enumerate)\}(?:\[[^\]]*\])?"
|
||||
r"|\\end\{(?P<end>itemize|enumerate)\}"
|
||||
r"|(?P<item>\\item\b)"
|
||||
)
|
||||
LIST_PLACEHOLDER_RE = re.compile(r"\x00ESC_LIST_(?P<index>\d+)\x00")
|
||||
|
||||
|
||||
def list_environment_end(text: str, begin: re.Match[str]) -> tuple[int, int] | None:
|
||||
stack: list[str] = []
|
||||
for token in LIST_TOKEN_RE.finditer(text, begin.start()):
|
||||
if token.group("begin"):
|
||||
stack.append(token.group("begin"))
|
||||
elif token.group("end"):
|
||||
if not stack or stack[-1] != token.group("end"):
|
||||
return None
|
||||
stack.pop()
|
||||
if not stack:
|
||||
return token.start(), token.end()
|
||||
return None
|
||||
|
||||
|
||||
def split_top_level_list_items(body: str) -> list[str]:
|
||||
item_starts: list[tuple[int, int]] = []
|
||||
depth = 0
|
||||
for token in LIST_TOKEN_RE.finditer(body):
|
||||
if token.group("begin"):
|
||||
depth += 1
|
||||
elif token.group("end"):
|
||||
depth = max(0, depth - 1)
|
||||
elif token.group("item") and depth == 0:
|
||||
item_starts.append((token.start(), token.end()))
|
||||
items: list[str] = []
|
||||
for index, (_start, content_start) in enumerate(item_starts):
|
||||
content_end = item_starts[index + 1][0] if index + 1 < len(item_starts) else len(body)
|
||||
item = body[content_start:content_end].strip().rstrip(",")
|
||||
if item:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def replace_nested_lists_html(
|
||||
text: str,
|
||||
equation_numbers: dict[str, int],
|
||||
figure_numbers: dict[str, int],
|
||||
) -> tuple[str, list[str]]:
|
||||
parts: list[str] = []
|
||||
rendered: list[str] = []
|
||||
cursor = 0
|
||||
while begin := LIST_BEGIN_RE.search(text, cursor):
|
||||
bounds = list_environment_end(text, begin)
|
||||
if bounds is None:
|
||||
break
|
||||
body_end, environment_end = bounds
|
||||
parts.append(text[cursor:begin.start()])
|
||||
body = text[begin.end():body_end]
|
||||
renderer = render_itemize_html if begin.group("kind") == "itemize" else render_enumerate_html
|
||||
placeholder = f"\x00ESC_LIST_{len(rendered)}\x00"
|
||||
rendered.append(renderer(body, equation_numbers, figure_numbers))
|
||||
parts.append(placeholder)
|
||||
cursor = environment_end
|
||||
parts.append(text[cursor:])
|
||||
return "".join(parts), rendered
|
||||
|
||||
|
||||
def render_list_item_html(
|
||||
item: str,
|
||||
equation_numbers: dict[str, int],
|
||||
figure_numbers: dict[str, int],
|
||||
) -> str:
|
||||
content, nested = replace_nested_lists_html(item, equation_numbers, figure_numbers)
|
||||
parts: list[str] = []
|
||||
cursor = 0
|
||||
for placeholder in LIST_PLACEHOLDER_RE.finditer(content):
|
||||
inline = content[cursor:placeholder.start()].strip()
|
||||
if inline:
|
||||
parts.append(latex_inline_to_html(inline, equation_numbers, figure_numbers))
|
||||
parts.append(nested[int(placeholder.group("index"))])
|
||||
cursor = placeholder.end()
|
||||
inline = content[cursor:].strip()
|
||||
if inline:
|
||||
parts.append(latex_inline_to_html(inline, equation_numbers, figure_numbers))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def render_itemize_html(body: str, equation_numbers: dict[str, int], figure_numbers: dict[str, int]) -> str:
|
||||
items = [item.strip().rstrip(",") for item in re.split(r"\\item", body) if item.strip()]
|
||||
return "<ul>" + "".join(f"<li>{latex_inline_to_html(item, equation_numbers, figure_numbers)}</li>" for item in items) + "</ul>"
|
||||
items = split_top_level_list_items(body)
|
||||
return "<ul>" + "".join(
|
||||
f"<li>{render_list_item_html(item, equation_numbers, figure_numbers)}</li>" for item in items
|
||||
) + "</ul>"
|
||||
|
||||
|
||||
def render_enumerate_html(body: str, equation_numbers: dict[str, int], figure_numbers: dict[str, int]) -> str:
|
||||
items = [item.strip().rstrip(",") for item in re.split(r"\\item", body) if item.strip()]
|
||||
return "<ol>" + "".join(f"<li>{latex_inline_to_html(item, equation_numbers, figure_numbers)}</li>" for item in items) + "</ol>"
|
||||
items = split_top_level_list_items(body)
|
||||
return "<ol>" + "".join(
|
||||
f"<li>{render_list_item_html(item, equation_numbers, figure_numbers)}</li>" for item in items
|
||||
) + "</ol>"
|
||||
|
||||
|
||||
def render_paragraphs_html(text: str, equation_numbers: dict[str, int], figure_numbers: dict[str, int]) -> list[str]:
|
||||
@@ -2558,12 +2648,12 @@ def render_paragraphs_html(text: str, equation_numbers: dict[str, int], figure_n
|
||||
|
||||
|
||||
def render_latex_fragment_html(text: str, equation_numbers: dict[str, int], figure_numbers: dict[str, int]) -> str:
|
||||
text, nested_lists = replace_nested_lists_html(text, equation_numbers, figure_numbers)
|
||||
patterns = {
|
||||
"equation": r"\\begin\{equation\}(.*?)\\end\{equation\}",
|
||||
"figure": r"\\begin\{figure\}\[H\](.*?)\\end\{figure\}",
|
||||
"tabularx": r"\\begin\{tabularx\}.*?\n(.*?)\\end\{tabularx\}",
|
||||
"itemize": r"\\begin\{itemize\}(?:\[[^\]]+\])?(.*?)\\end\{itemize\}",
|
||||
"enumerate": r"\\begin\{enumerate\}(?:\[[^\]]+\])?(.*?)\\end\{enumerate\}",
|
||||
"list": r"\x00ESC_LIST_\d+\x00",
|
||||
"verbatim": r"\\begin\{verbatim\}(.*?)\\end\{verbatim\}",
|
||||
}
|
||||
combined = re.compile("|".join(f"(?P<{name}>{pattern})" for name, pattern in patterns.items()), re.S)
|
||||
@@ -2582,12 +2672,10 @@ def render_latex_fragment_html(text: str, equation_numbers: dict[str, int], figu
|
||||
elif kind == "tabularx":
|
||||
body = re.match(patterns["tabularx"], body, re.S).group(1) # type: ignore[union-attr]
|
||||
parts.append(render_tabular_html(body, equation_numbers, figure_numbers))
|
||||
elif kind == "itemize":
|
||||
body = re.match(patterns["itemize"], body, re.S).group(1) # type: ignore[union-attr]
|
||||
parts.append(render_itemize_html(body, equation_numbers, figure_numbers))
|
||||
elif kind == "enumerate":
|
||||
body = re.match(patterns["enumerate"], body, re.S).group(1) # type: ignore[union-attr]
|
||||
parts.append(render_enumerate_html(body, equation_numbers, figure_numbers))
|
||||
elif kind == "list":
|
||||
placeholder = LIST_PLACEHOLDER_RE.fullmatch(body)
|
||||
if placeholder:
|
||||
parts.append(nested_lists[int(placeholder.group("index"))])
|
||||
elif kind == "verbatim":
|
||||
body = re.match(patterns["verbatim"], body, re.S).group(1) # type: ignore[union-attr]
|
||||
parts.append(f"<pre><code>{html_escape(body.strip())}</code></pre>")
|
||||
|
||||
Reference in New Issue
Block a user