""" Tests for issue #486 (CSS: inline code in table cells) and issue #487 (JS renderer: markdown image syntax not implemented). Issue #486 — CSS fix in static/style.css: Inline `code` spans inside table cells render with awkward sizing. Fix: td code, th code { font-size: 0.85em; padding: 1px 4px; vertical-align: baseline; } Issue #487 — JS fix in static/ui.js: ![alt](url) image syntax not handled — renders as stray ! + link. Fix: add image pass to renderMd() (before link pass) and inlineMd() reusing the .msg-media-img class. Strategy: - Source-level checks verify the fixes are present in the JS/CSS. - Python mirror tests verify the rendering logic with exhaustive edge cases, especially code blocks inside tables (the specific case Nathan flagged). """ import pathlib import re import html as _html REPO_ROOT = pathlib.Path(__file__).parent.parent UI_JS = (REPO_ROOT / "static" / "ui.js").read_text() STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text() # ── Helpers ─────────────────────────────────────────────────────────────────── def esc(s): return _html.escape(str(s), quote=True) def inline_md(t): """ Python mirror of the fixed inlineMd() function — includes: - _code_stash (protects backtick spans from bold/italic AND from image pass) - image pass (NEW for #487 — runs while code stash is active, before link pass) - _img_stash (protects rendered img tags from autolink touching src=) - _link_stash (protects links from autolink) - autolink - code stash restore (after autolink, so code content is never autolinked) Correct operation order: 1. code stash — \x00C protects `...` from bold and image pass 2. bold/italic — runs on plain text only 3. image pass — runs while code content is still stashed (so ![x](url) inside backticks stays protected as a \x00C token) 4. img stash — \x00I protects from autolink 5. link stash — \x00L protects [label](url) links from autolink 6. autolink — only matches URLs not already in a stash token 7. link stash restore 8. img stash restore 9. code stash restore — restores tags last """ # 1. Code stash — must be first to protect code content from all subsequent passes code_stash = [] def stash_code(m): code_stash.append(f'{esc(m.group(1))}') return f'\x00C{len(code_stash)-1}\x00' t = re.sub(r'`([^`\n]+)`', stash_code, t) # 2. Bold/italic (code content is safely stashed) t = re.sub(r'\*\*\*(.+?)\*\*\*', lambda m: f'{esc(m.group(1))}', t) t = re.sub(r'\*\*(.+?)\*\*', lambda m: f'{esc(m.group(1))}', t) t = re.sub(r'\*([^*\n]+)\*', lambda m: f'{esc(m.group(1))}', t) # 3. Image pass (NEW — runs while code is still stashed, so ![x](url) inside # backticks is protected as a \x00C token and won't match here) def render_image(m): alt, url = m.group(1), m.group(2) safe_url = url.replace('"', '%22') return (f'{esc(alt)}') t = re.sub(r'!\[([^\]]*)\]\((https?://[^\)]+)\)', render_image, t) # 4. Img stash — protect rendered tags so autolink never touches src= values img_stash = [] def stash_img(m): img_stash.append(m.group(0)) return f'\x00I{len(img_stash)-1}\x00' t = re.sub(r']*>', stash_img, t) # 5. Link stash link_stash = [] def stash_link(m): lb, u = m.group(1), m.group(2) link_stash.append(f'{esc(lb)}') return f'\x00L{len(link_stash)-1}\x00' t = re.sub(r'\[([^\]]+)\]\((https?://[^\)]+)\)', stash_link, t) # 6. Autolink (img and link URLs are both stashed — safe) def autolink(m): url = m.group(1) trail = url[-1] if url[-1] in '.,;:!?)' else '' clean = url[:-1] if trail else url return f'{esc(clean)}{trail}' t = re.sub(r'(https?://[^\s<>"\')\]]+)', autolink, t) # 7. Restore link stash t = re.sub(r'\x00L(\d+)\x00', lambda m: link_stash[int(m.group(1))], t) # 8. Restore img stash t = re.sub(r'\x00I(\d+)\x00', lambda m: img_stash[int(m.group(1))], t) # 9. Restore code stash (last — code content was never touched by any pass) t = re.sub(r'\x00C(\d+)\x00', lambda m: code_stash[int(m.group(1))], t) return t def render_table(md): """Python mirror of the table pass, using inline_md() per cell.""" lines = md.strip().split('\n') if len(lines) < 2: return md def is_sep(r): return bool(re.match(r'^\|[\s|:-]+\|$', r.strip())) if not is_sep(lines[1]): return md def parse_header(r): cells = r.strip().lstrip('|').rstrip('|').split('|') return ''.join(f'{inline_md(c.strip())}' for c in cells) def parse_row(r): cells = r.strip().lstrip('|').rstrip('|').split('|') return ''.join(f'{inline_md(c.strip())}' for c in cells) header = f'{parse_header(lines[0])}' body = ''.join(f'{parse_row(r)}' for r in lines[2:]) return f'{header}{body}
' # ═════════════════════════════════════════════════════════════════════════════ # ISSUE #486 — CSS: code inside table cells # ═════════════════════════════════════════════════════════════════════════════ class TestIssue486CssCodeInTable: """CSS fix: td code and th code must have targeted sizing rules.""" def test_td_code_font_size_present(self): """msg-body td code rule must set font-size (e.g. 0.85em) to prevent oversized code.""" assert 'td code' in STYLE_CSS, ( "Missing 'td code' CSS rule — inline code in table cells needs sizing fix" ) def test_th_code_rule_present(self): """th code rule must also exist for header cells.""" assert 'th code' in STYLE_CSS, ( "Missing 'th code' CSS rule — inline code in header cells needs sizing fix" ) def test_td_code_has_font_size(self): """The td code / th code block must include a font-size declaration.""" # Find the msg-body scoped td code rule idx = STYLE_CSS.find('td code') assert idx != -1, "td code rule not found in style.css" # Check nearby text (within 200 chars) has font-size window = STYLE_CSS[idx:idx+200] assert 'font-size' in window, ( f"td code rule must include font-size. Found near td code: {window!r}" ) def test_td_code_has_padding(self): """The td code / th code block must include a padding declaration.""" idx = STYLE_CSS.find('td code') assert idx != -1 window = STYLE_CSS[idx:idx+200] assert 'padding' in window, ( f"td code rule must include padding. Found near td code: {window!r}" ) def test_td_code_has_vertical_align(self): """The td code / th code block must include vertical-align: baseline.""" idx = STYLE_CSS.find('td code') assert idx != -1 window = STYLE_CSS[idx:idx+200] assert 'vertical-align' in window, ( f"td code rule must include vertical-align. Found near td code: {window!r}" ) def test_code_renders_inside_table_cell(self): """Inline `code` inside a table cell must render as element.""" md = "| Syntax | Rendered |\n|---|---|\n| `code` | `code` |" result = render_table(md) assert 'code' in result, ( f"Inline code in table cell should render as . Got: {result}" ) def test_bold_code_renders_inside_table_cell(self): """**`bold code`** inside a table cell must render as .""" md = "| Style | Example |\n|---|---|\n| bold code | **`bold code`** |" result = render_table(md) # Should have code tag (even inside bold) assert 'bold code' in result, ( f"Bold code in table should render as . Got: {result}" ) def test_multiple_code_spans_in_same_cell(self): """Multiple backtick spans in one cell all render as .""" md = "| Combined |\n|---|\n| `a` and `b` |" result = render_table(md) assert result.count('') == 2, ( f"Expected 2 code tags in cell, got: {result}" ) def test_code_in_header_cell(self): """`code` in a header cell must also render as .""" md = "| `header code` | Normal |\n|---|---|\n| data | data |" result = render_table(md) assert 'header code' in result, ( f"Code in header cell should render. Got: {result}" ) def test_code_not_mangled_by_bold_in_table(self): """**`code`** in a table cell must NOT produce <code> (the pre-fix bug).""" md = "| Pattern | Example |\n|---|---|\n| bold-code | **`npm install`** |" result = render_table(md) assert '<code>' not in result, ( f"Code tags inside bold in table must not be HTML-escaped. Got: {result}" ) assert '' in result, "Bold wrapper should be present" assert 'npm install' in result def test_code_with_special_chars_in_table(self): """`