fix(renderer): preserve nested mixed ul/ol list hierarchy (#6700)
This commit is contained in:
1 parent
641878bd88
commit
13dd97d60d
4 files changed
+178
-72
No files matched your search
+66
-45
@@ -7801,48 +7801,72 @@ function renderMd(raw){
|
|||||||
s=s.replace(/^---+$/gm,'<hr>');
|
s=s.replace(/^---+$/gm,'<hr>');
|
||||||
// (Blockquotes are handled by the pre-pass at the top of renderMd, before
|
// (Blockquotes are handled by the pre-pass at the top of renderMd, before
|
||||||
// fence_stash. The per-line passes below never see > prefixes.)
|
// fence_stash. The per-line passes below never see > prefixes.)
|
||||||
function _renderListBlock(lines, ordered){
|
function _renderListBlock(lines){
|
||||||
const marker=ordered?'\\d+\\. ':'[-*+] ';
|
// Single-pass mixed-marker list renderer (#6700). Builds a real nested
|
||||||
let html=ordered?'<ol>':'<ul>';
|
// <ul>/<ol> tree with a stack keyed by indentation depth + marker kind
|
||||||
let item=null;
|
// instead of rendering one marker family and re-parsing the generated
|
||||||
const flush=()=>{
|
// HTML with the other. Only item text goes through inlineMd(); tags
|
||||||
if(!item) return;
|
// emitted here are never parsed as Markdown again.
|
||||||
const body=item.parts.join('\n').trim();
|
const root={ordered:false,items:[]}; // container for top-level lists
|
||||||
const text=body;
|
const listStack=[]; // listStack[k]: list open at depth k
|
||||||
let inner;
|
const itemStack=[]; // itemStack[k]: last item at depth k
|
||||||
if(!ordered && /^\[x\] /i.test(text)) inner='<span class="task-done">✅</span> '+inlineMd(text.slice(4));
|
const openList=(depth,ordered)=>{
|
||||||
else if(!ordered && /^\[ \] /.test(text)) inner='<span class="task-todo">☐</span> '+inlineMd(text.slice(4));
|
const ln={ordered,items:[]};
|
||||||
else inner=inlineMd(text);
|
// Attach to the nearest open ancestor item when indentation jumps by
|
||||||
const valueAttr=item.value!==null?` value="${item.value}"`:'';
|
// more than one level (e.g. 4-space indent straight from the top), so
|
||||||
const styleAttr=item.indent?` style="margin-left:16px"`:'';
|
// the new list nests under the right parent instead of detaching.
|
||||||
html+=`<li${valueAttr}${styleAttr}>${inner}</li>`;
|
let parentItem=null;
|
||||||
item=null;
|
for(let k=depth-1;k>=0;k--){
|
||||||
|
if(itemStack[k]){ parentItem=itemStack[k]; break; }
|
||||||
|
}
|
||||||
|
(parentItem?parentItem.sublists:root.items).push(ln);
|
||||||
|
listStack[depth]=ln;
|
||||||
|
return ln;
|
||||||
|
};
|
||||||
|
const openItem=(depth,ordered,value,content)=>{
|
||||||
|
for(let k=listStack.length-1;k>depth;k--) listStack.pop();
|
||||||
|
if(!listStack[depth]||listStack[depth].ordered!==ordered){
|
||||||
|
if(listStack[depth]) listStack.pop();
|
||||||
|
listStack[depth]=openList(depth,ordered);
|
||||||
|
}
|
||||||
|
const item={value,content:[content],sublists:[]};
|
||||||
|
listStack[depth].items.push(item);
|
||||||
|
itemStack[depth]=item;
|
||||||
|
for(let k=itemStack.length-1;k>depth;k--) itemStack.pop();
|
||||||
|
return item;
|
||||||
};
|
};
|
||||||
for(const raw of lines){
|
for(const raw of lines){
|
||||||
const line=String(raw||'');
|
const line=String(raw||'');
|
||||||
const nested=line.match(new RegExp(`^ {2,}(${marker})(.*)$`));
|
const om=line.match(/^(\s*)(\d+)\.\s+(.*)$/);
|
||||||
if(nested){
|
const um=line.match(/^(\s*)([-*+])\s+(.*)$/);
|
||||||
flush();
|
const m=om||um;
|
||||||
item={indent:true,value:ordered?parseInt(nested[1],10):null,parts:[nested[2]]};
|
if(m){
|
||||||
continue;
|
const depth=m[1].length<2?0:Math.floor(m[1].length/2);
|
||||||
|
openItem(depth,!!om,om?parseInt(om[2],10):null,m[3]);
|
||||||
|
} else if(itemStack.length){
|
||||||
|
itemStack[itemStack.length-1].content.push(line.replace(/^ {2,}/,'').trim());
|
||||||
}
|
}
|
||||||
const top=line.match(new RegExp(`^(?: )?(${marker})(.*)$`));
|
|
||||||
if(top){
|
|
||||||
flush();
|
|
||||||
item={indent:false,value:ordered?parseInt(top[1],10):null,parts:[top[2]]};
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
if(!item) continue;
|
const renderList=ln=>'<'+(ln.ordered?'ol':'ul')+'>'+ln.items.map(renderItem).join('')+'</'+(ln.ordered?'ol':'ul')+'>';
|
||||||
item.parts.push(line.replace(/^ {2,}/,'').trim());
|
const renderItem=it=>{
|
||||||
|
const text=it.content.join('\n').trim();
|
||||||
|
let inner;
|
||||||
|
if(!it.ordered && /^\[x\] /i.test(text)) inner='<span class="task-done">✅</span> '+inlineMd(text.slice(4));
|
||||||
|
else if(!it.ordered && /^\[ \] /.test(text)) inner='<span class="task-todo">☐</span> '+inlineMd(text.slice(4));
|
||||||
|
else inner=inlineMd(text);
|
||||||
|
const valueAttr=it.value!==null?` value="${it.value}"`:'';
|
||||||
|
return `<li${valueAttr}>${inner}${it.sublists.map(renderList).join('')}</li>`;
|
||||||
|
};
|
||||||
|
return root.items.map(renderList).join('');
|
||||||
}
|
}
|
||||||
flush();
|
function _renderLists(src){
|
||||||
return html+(ordered?'</ol>':'</ul>');
|
// Single pass over source lines: collect a contiguous list region (any
|
||||||
}
|
// marker family, top-level or nested, plus continuation lines) and hand
|
||||||
function _renderLists(src, ordered){
|
// it to _renderListBlock, which builds the structural <ul>/<ol> tree.
|
||||||
const lines=src.split('\n');
|
const lines=src.split('\n');
|
||||||
const out=[];
|
const out=[];
|
||||||
const topRe=ordered?/^(?: )?\d+\. /:/^(?: )?[-*+] /;
|
const topRe=/^(?: )?(?:\d+\.|[-*+]) /;
|
||||||
const nestedRe=ordered?/^ {2,}\d+\. /:/^ {2,}[-*+] /;
|
const nestedRe=/^ {2,}(?:\d+\.|[-*+]) /;
|
||||||
const contRe=/^ {2,}\S/;
|
const contRe=/^ {2,}\S/;
|
||||||
let i=0;
|
let i=0;
|
||||||
while(i<lines.length){
|
while(i<lines.length){
|
||||||
@@ -7869,18 +7893,16 @@ function renderMd(raw){
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
out.push(_renderListBlock(block,ordered));
|
out.push(_renderListBlock(block));
|
||||||
}
|
}
|
||||||
return out.join('\n');
|
return out.join('\n');
|
||||||
}
|
}
|
||||||
// Preserve continuation lines, nested indentation, and LaTeX placeholder lines
|
// Single-pass list stage (#6700): one parser handles both marker families
|
||||||
// inside list items without changing the wider markdown pipeline.
|
// with a stack keyed by indentation + marker kind, so mixed nested lists
|
||||||
s=_renderLists(s,false);
|
// (ul→ol→ul) build valid <ul>/<ol> structure instead of re-parsing
|
||||||
// Ordered-list parsing intentionally runs on the post-unordered string; the
|
// renderer-generated HTML as Markdown. value="N" is still emitted on every
|
||||||
// unordered pass emits <ul> HTML that cannot satisfy the ordered-item regex.
|
// ordered <li> so explicit numbering survives blank-line splits (#886).
|
||||||
// Keep continuation lines attached to their item and preserve explicit
|
s=_renderLists(s);
|
||||||
// numbering via value= even when blank lines split the markdown.
|
|
||||||
s=_renderLists(s,true);
|
|
||||||
// Tables: | col | col | header row followed by | --- | --- | separator then data rows
|
// Tables: | col | col | header row followed by | --- | --- | separator then data rows
|
||||||
// NOTE: table pass runs BEFORE outer link pass so [label](url) in table cells
|
// NOTE: table pass runs BEFORE outer link pass so [label](url) in table cells
|
||||||
// is handled by inlineMd() only — prevents double-linking.
|
// is handled by inlineMd() only — prevents double-linking.
|
||||||
@@ -8043,8 +8065,7 @@ function renderMd(raw){
|
|||||||
const a=_attrs(rawAttrs);
|
const a=_attrs(rawAttrs);
|
||||||
if(name==='li'){
|
if(name==='li'){
|
||||||
const value=/^\d+$/.test(a.value||'')?` value="${esc(a.value)}"`:'';
|
const value=/^\d+$/.test(a.value||'')?` value="${esc(a.value)}"`:'';
|
||||||
const style=(a.style||'').replace(/\s+/g,'').toLowerCase()==='margin-left:16px'?` style="margin-left:16px"`:'';
|
return `<li${value}>`;
|
||||||
return `<li${value}${style}>`;
|
|
||||||
}
|
}
|
||||||
if(name==='span'){
|
if(name==='span'){
|
||||||
return `<span${_cls(a.class,['task-done','task-todo','katex-inline'])}${a['data-katex']==='inline'?' data-katex="inline"':''}>`;
|
return `<span${_cls(a.class,['task-done','task-todo','katex-inline'])}${a['data-katex']==='inline'?' data-katex="inline"':''}>`;
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ at 1, producing "1. 1. 1." instead of "1. 2. 3.".
|
|||||||
|
|
||||||
Fix: emit value="N" on every <li> so the correct ordinal is preserved even when
|
Fix: emit value="N" on every <li> so the correct ordinal is preserved even when
|
||||||
items end up in separate <ol> containers after the paragraph split.
|
items end up in separate <ol> containers after the paragraph split.
|
||||||
|
|
||||||
|
The list stage was rewritten for #6700 into a single-pass, mixed-marker parser
|
||||||
|
(a stack keyed by indentation + marker kind). These static assertions pin the
|
||||||
|
parts of that implementation that keep #886 working: the ordered-marker digit
|
||||||
|
capture and the value="N" emission on <li>.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -21,15 +26,15 @@ def get_ui_js():
|
|||||||
|
|
||||||
class TestOrderedListNumbering:
|
class TestOrderedListNumbering:
|
||||||
def _ordered_list_block(self, src: str) -> str:
|
def _ordered_list_block(self, src: str) -> str:
|
||||||
start = src.find("function _renderListBlock(lines, ordered){")
|
start = src.find("function _renderListBlock(lines){")
|
||||||
assert start != -1, "_renderListBlock helper not found in ui.js"
|
assert start != -1, "_renderListBlock helper not found in ui.js"
|
||||||
end = src.find("function _renderLists(src, ordered){", start)
|
end = src.find("function _renderLists(src){", start)
|
||||||
assert end != -1, "_renderLists helper not found after _renderListBlock"
|
assert end != -1, "_renderLists helper not found after _renderListBlock"
|
||||||
return src[start:end]
|
return src[start:end]
|
||||||
|
|
||||||
def _ordered_list_dispatch_block(self, src: str) -> str:
|
def _ordered_list_dispatch_block(self, src: str) -> str:
|
||||||
start = src.find("s=_renderLists(s,true);")
|
start = src.find("s=_renderLists(s);")
|
||||||
assert start != -1, "ordered-list dispatch not found in ui.js"
|
assert start != -1, "list dispatch not found in ui.js"
|
||||||
return src[max(0, start - 260):start + 80]
|
return src[max(0, start - 260):start + 80]
|
||||||
|
|
||||||
def test_li_value_attr_present_in_ordered_list_block(self):
|
def test_li_value_attr_present_in_ordered_list_block(self):
|
||||||
@@ -42,18 +47,18 @@ class TestOrderedListNumbering:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_li_value_uses_parsed_number(self):
|
def test_li_value_uses_parsed_number(self):
|
||||||
"""The value= must be derived from parseInt of the captured digit, not hardcoded."""
|
"""The value= must be derived from parseInt of the captured digits, not hardcoded."""
|
||||||
src = get_ui_js()
|
src = get_ui_js()
|
||||||
ol_block = self._ordered_list_block(src)
|
ol_block = self._ordered_list_block(src)
|
||||||
assert 'parseInt' in ol_block, (
|
assert 'parseInt' in ol_block, (
|
||||||
"Ordered-list block should use parseInt() to parse the list number (#886)"
|
"Ordered-list block should use parseInt() to parse the list number (#886)"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_numMatch_variable_present(self):
|
def test_marker_digit_capture_present(self):
|
||||||
"""The ordered-list branch must still capture digits from the markdown marker."""
|
"""The ordered branch must still capture digits from the markdown marker."""
|
||||||
src = get_ui_js()
|
src = get_ui_js()
|
||||||
ol_block = self._ordered_list_block(src)
|
ol_block = self._ordered_list_block(src)
|
||||||
assert "const marker=ordered?'\\\\d+\\\\. ':'[-*+] ';" in ol_block, (
|
assert "\\d+\\." in ol_block or re.search(r"\(\\d\+\)\\\.", ol_block), (
|
||||||
"Ordered-list block should keep a digit marker pattern for numbered items (#886)"
|
"Ordered-list block should keep a digit marker pattern for numbered items (#886)"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,18 +73,18 @@ class TestOrderedListNumbering:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_ordered_list_comment_references_issue(self):
|
def test_ordered_list_comment_references_issue(self):
|
||||||
"""A comment near the OL fix should reference the issue (#886) or the symptom."""
|
"""A comment near the list dispatch should reference #886 or the symptom."""
|
||||||
src = get_ui_js()
|
src = get_ui_js()
|
||||||
context = self._ordered_list_dispatch_block(src)
|
context = self._ordered_list_dispatch_block(src)
|
||||||
has_comment = '#886' in context or '1. 1. 1.' in context or 'blank lines' in context.lower()
|
has_comment = '#886' in context or '1. 1. 1.' in context or 'blank lines' in context.lower()
|
||||||
assert has_comment, (
|
assert has_comment, (
|
||||||
"Expected a comment near the OL fix explaining the blank-line issue (#886)"
|
"Expected a comment near the list dispatch explaining the blank-line issue (#886)"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_list_without_blank_lines_unaffected(self):
|
def test_lists_route_through_single_shared_helper(self):
|
||||||
"""A compact list should still flow through the ordered-list helper."""
|
"""All list rendering must flow through the single-pass shared helper."""
|
||||||
src = get_ui_js()
|
src = get_ui_js()
|
||||||
ol_block = self._ordered_list_dispatch_block(src)
|
dispatch = self._ordered_list_dispatch_block(src)
|
||||||
assert "s=_renderLists(s,true);" in ol_block, (
|
assert "s=_renderLists(s);" in dispatch, (
|
||||||
"Ordered-list rendering should still route through the shared helper"
|
"List rendering should route through the single-pass _renderLists helper"
|
||||||
)
|
)
|
||||||
@@ -338,10 +338,13 @@ class TestMarkdownListsWithLatex:
|
|||||||
assert "<li>next item</li>" in out
|
assert "<li>next item</li>" in out
|
||||||
|
|
||||||
def test_nested_indentation_stays_in_list(self, driver_path):
|
def test_nested_indentation_stays_in_list(self, driver_path):
|
||||||
|
"""Same-marker nesting builds a structural nested <ul> instead of a
|
||||||
|
styled sibling <li> — the margin-left convention was removed by the
|
||||||
|
single-pass mixed-marker parser (#6700)."""
|
||||||
out = _render(driver_path, "- parent\n - child")
|
out = _render(driver_path, "- parent\n - child")
|
||||||
assert "<ul>" in out
|
assert "<ul>" in out
|
||||||
assert "<li>parent</li>" in out
|
assert "<li>parent<ul><li>child</li></ul></li>" in out
|
||||||
assert '<li style="margin-left:16px">child</li>' in out
|
assert "margin-left:16px" not in out
|
||||||
|
|
||||||
def test_display_math_line_stays_inside_list_item(self, driver_path):
|
def test_display_math_line_stays_inside_list_item(self, driver_path):
|
||||||
src = "- intro\n\n $$x^2$$\n\n continuation"
|
src = "- intro\n\n $$x^2$$\n\n continuation"
|
||||||
@@ -361,6 +364,63 @@ class TestMarkdownListsWithLatex:
|
|||||||
assert '<li value="3">tail</li>' in out
|
assert '<li value="3">tail</li>' in out
|
||||||
|
|
||||||
|
|
||||||
|
class TestMixedNestedLists:
|
||||||
|
"""#6700: mixed ul/ol nesting must build a valid structural hierarchy.
|
||||||
|
|
||||||
|
Regression for the old two-pass list renderer, where the ordered pass
|
||||||
|
re-parsed the <ul> HTML emitted by the unordered pass as Markdown. That
|
||||||
|
leaked escaped fragments like `</li><li style="margin-left:
|
||||||
|
16px">` into the chat and flattened the inverse (ol→ul) shape.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_ul_ol_ul_nested_then_top_level_return(self, driver_path):
|
||||||
|
src = (
|
||||||
|
"- normal item\n"
|
||||||
|
" 1. numbered child\n"
|
||||||
|
" 2. second numbered child\n"
|
||||||
|
" - unordered child again\n"
|
||||||
|
"- next normal item"
|
||||||
|
)
|
||||||
|
out = _render(driver_path, src)
|
||||||
|
# No escaped renderer-generated fragments may leak into the output
|
||||||
|
assert "</li>" not in out, out
|
||||||
|
assert "<ul" not in out, out
|
||||||
|
assert "<ol" not in out, out
|
||||||
|
assert "margin-left:16px" not in out, out
|
||||||
|
# Balanced containers
|
||||||
|
assert out.count("<ul>") == out.count("</ul>"), out
|
||||||
|
assert out.count("<ol>") == out.count("</ol>"), out
|
||||||
|
# Exact structural hierarchy: ul > li > (ol, ul) > li
|
||||||
|
assert (
|
||||||
|
'<ul><li>normal item'
|
||||||
|
'<ol><li value="1">numbered child</li>'
|
||||||
|
'<li value="2">second numbered child</li></ol>'
|
||||||
|
'<ul><li>unordered child again</li></ul></li>'
|
||||||
|
'<li>next normal item</li></ul>'
|
||||||
|
) in out, out
|
||||||
|
|
||||||
|
def test_ol_ul_nested_keeps_bullets(self, driver_path):
|
||||||
|
src = "1. first step\n - detail A\n - detail B\n2. second step"
|
||||||
|
out = _render(driver_path, src)
|
||||||
|
assert "</li>" not in out, out
|
||||||
|
assert "<ul" not in out, out
|
||||||
|
assert "<ol" not in out, out
|
||||||
|
assert out.count("<ol>") == out.count("</ol>"), out
|
||||||
|
assert out.count("<ul>") == out.count("</ul>"), out
|
||||||
|
assert (
|
||||||
|
'<ol><li value="1">first step'
|
||||||
|
'<ul><li>detail A</li><li>detail B</li></ul></li>'
|
||||||
|
'<li value="2">second step</li></ol>'
|
||||||
|
) in out, out
|
||||||
|
|
||||||
|
def test_top_level_marker_switch_starts_sibling_list(self, driver_path):
|
||||||
|
out = _render(driver_path, "- bullet\n1. numbered")
|
||||||
|
assert (
|
||||||
|
'<ul><li>bullet</li></ul><ol><li value="1">numbered</li></ol>'
|
||||||
|
) in out, out
|
||||||
|
assert "</li>" not in out, out
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Block-level constructs INSIDE blockquotes — the six bugs documented in
|
# Block-level constructs INSIDE blockquotes — the six bugs documented in
|
||||||
# blockquote-rendering-bugs.md. Each test feeds the exact input from the
|
# blockquote-rendering-bugs.md. Each test feeds the exact input from the
|
||||||
|
|||||||
+30
-10
@@ -100,22 +100,40 @@ def render_md(raw):
|
|||||||
def handle_ul(block):
|
def handle_ul(block):
|
||||||
lines = block.strip().split("\n")
|
lines = block.strip().split("\n")
|
||||||
out = "<ul>"
|
out = "<ul>"
|
||||||
|
prev = 0
|
||||||
for l in lines:
|
for l in lines:
|
||||||
indent = bool(re.match(r"^ {2,}", l))
|
m = re.match(r"^( *)([-*+]) (.*)$", l)
|
||||||
text = re.sub(r"^ {0,4}[-*+] ", "", l)
|
if not m:
|
||||||
style = ' style="margin-left:16px"' if indent else ""
|
continue
|
||||||
out += f"<li{style}>{inline_md(text)}</li>"
|
depth = 0 if len(m.group(1)) < 2 else len(m.group(1)) // 2
|
||||||
return out + "</ul>"
|
if depth > prev:
|
||||||
|
out += "<ul>"
|
||||||
|
elif depth < prev:
|
||||||
|
out += "</ul>" * (prev - depth)
|
||||||
|
prev = depth
|
||||||
|
out += f"<li>{inline_md(m.group(3))}</li>"
|
||||||
|
out += "</ul>" * (prev + 1)
|
||||||
|
return out
|
||||||
|
|
||||||
s = re.sub(r"((?:^(?: )?[-*+] .+\n?)+)", lambda m: handle_ul(m.group()), s, flags=re.M)
|
s = re.sub(r"((?:^(?: )?[-*+] .+\n?)+)", lambda m: handle_ul(m.group()), s, flags=re.M)
|
||||||
|
|
||||||
def handle_ol(block):
|
def handle_ol(block):
|
||||||
lines = block.strip().split("\n")
|
lines = block.strip().split("\n")
|
||||||
out = "<ol>"
|
out = "<ol>"
|
||||||
|
prev = 0
|
||||||
for l in lines:
|
for l in lines:
|
||||||
text = re.sub(r"^ {0,4}\d+\. ", "", l)
|
m = re.match(r"^( *)(\d+)\. (.*)$", l)
|
||||||
out += f"<li>{inline_md(text)}</li>"
|
if not m:
|
||||||
return out + "</ol>"
|
continue
|
||||||
|
depth = 0 if len(m.group(1)) < 2 else len(m.group(1)) // 2
|
||||||
|
if depth > prev:
|
||||||
|
out += "<ol>"
|
||||||
|
elif depth < prev:
|
||||||
|
out += "</ol>" * (prev - depth)
|
||||||
|
prev = depth
|
||||||
|
out += f"<li>{inline_md(m.group(3))}</li>"
|
||||||
|
out += "</ol>" * (prev + 1)
|
||||||
|
return out
|
||||||
|
|
||||||
s = re.sub(r"((?:^(?: )?\d+\. .+\n?)+)", lambda m: handle_ol(m.group()), s, flags=re.M)
|
s = re.sub(r"((?:^(?: )?\d+\. .+\n?)+)", lambda m: handle_ol(m.group()), s, flags=re.M)
|
||||||
|
|
||||||
@@ -559,10 +577,12 @@ def test_ordered_list_code_spans(cleanup_test_sessions):
|
|||||||
assert "<code>npm start</code>" in out
|
assert "<code>npm start</code>" in out
|
||||||
|
|
||||||
def test_indented_list_item_bold(cleanup_test_sessions):
|
def test_indented_list_item_bold(cleanup_test_sessions):
|
||||||
"""Bold inside indented (nested) list item."""
|
"""Bold inside indented (nested) list item — nested items now build a
|
||||||
|
structural nested <ul> instead of a margin-left sibling (#6700)."""
|
||||||
out = render_md("- top level\n - **nested bold**")
|
out = render_md("- top level\n - **nested bold**")
|
||||||
assert "<strong>nested bold</strong>" in out
|
assert "<strong>nested bold</strong>" in out
|
||||||
assert "margin-left:16px" in out
|
assert "<ul><li><strong>nested bold</strong></li></ul>" in out
|
||||||
|
assert "margin-left:16px" not in out
|
||||||
|
|
||||||
# --- Blockquote variants ---
|
# --- Blockquote variants ---
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user