diff --git a/python/mdhtml/__init__.py b/python/mdhtml/__init__.py index 4733eaf..1406b5d 100644 --- a/python/mdhtml/__init__.py +++ b/python/mdhtml/__init__.py @@ -148,5 +148,5 @@ def rewrite(markdown: str, callbacks: dict, *, math: str = "brackets", templates if raw["type"] == "math_inline" and "tex" in replacement: n = len(raw["delimiter"]) edits.append((offsets[norm_start + n], offsets[norm_end - n], replacement["tex"])) - for start, end, replacement in reversed(edits): markdown = markdown[:start] + replacement + markdown[end:] + for start, end, replacement in sorted(edits, reverse=True): markdown = markdown[:start] + replacement + markdown[end:] return markdown diff --git a/src/highlight.rs b/src/highlight.rs index e867363..770470f 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -178,6 +178,7 @@ fn scan_unit(src: &str, segments: &[(usize, usize)], ctx: &InlineContext<'_>, sp InlineEventKind::LinkTarget | InlineEventKind::Autolink | InlineEventKind::Xref | InlineEventKind::FootnoteRef => LINK, InlineEventKind::Attr | InlineEventKind::Template => ATTR, InlineEventKind::Comment => COMMENT, + InlineEventKind::InlineLink { .. } => continue, }; let mut cursor = 0usize; for &(s, e) in segments { diff --git a/src/inline.rs b/src/inline.rs index 2840a52..8e3a947 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -34,6 +34,7 @@ pub(crate) enum InlineEventKind { Highlight, Code, LinkTarget, + InlineLink { label_end: usize, image: bool }, Autolink, Xref, FootnoteRef, @@ -90,6 +91,15 @@ impl EditNode { pub fn find_edit_nodes(src: &str, ctx: &InlineContext<'_>) -> Vec { let mut out = Vec::new(); + let mut links = HashMap::new(); + for ev in inline_events(src, ctx) { + if let InlineEventKind::InlineLink { label_end, image } = ev.kind + && let Some((node, next)) = inline_link_edit_node(src, ev.start, label_end, image, ctx) + { + out.push(node); + links.insert(ev.start, next + attr_after(src, next, &mut out)); + } + } let mut failed = FailedScans::default(); let mut i = 0; while i < src.len() { @@ -143,22 +153,14 @@ pub fn find_edit_nodes(src: &str, ctx: &InlineContext<'_>) -> Vec { else { i = next + attr_after(src, next, &mut out); } continue; } - if starts(src, i, "![") - && let Some((node, next)) = inline_image_edit_node(src, i, ctx) - { - out.push(node); - i = next + attr_after(src, next, &mut out); + if let Some(&next) = links.get(&i) { + i = next; continue; } if starts(src, i, "[") && let Some((label, label_len)) = scan_link_label(&src[i..]) { let after = i + label_len; - if let Some((node, next)) = inline_link_edit_node(src, i, label_len, ctx) { - out.push(node); - i = next + attr_after(src, next, &mut out); - continue; - } if let Some(refs) = ref_segs(&src[i + 1..after - 1]) { let (tokens, n) = ref_attr(src, after); out.push(EditNode::Xref { range: i..after + n, refs, tokens }); @@ -221,17 +223,13 @@ fn inline_url(src: &str, after: usize, max_parens: usize) -> Option<(Range) -> Option<(EditNode, usize)> { - let (alt, label_len) = scan_link_label(&src[i + 1..])?; - let after = i + 1 + label_len; +fn inline_link_edit_node(src: &str, i: usize, after: usize, image: bool, ctx: &InlineContext<'_>) -> Option<(EditNode, usize)> { let (url_range, url, title, next) = inline_url(src, after, ctx.options.max_link_paren_depth)?; - let alt = crate::render::plain(&parse_inlines(&alt, ctx)); - Some((EditNode::Image { range: i..next, url_range, alt, url, title }, next)) -} - -fn inline_link_edit_node(src: &str, i: usize, label_len: usize, ctx: &InlineContext<'_>) -> Option<(EditNode, usize)> { - let (url_range, url, title, next) = inline_url(src, i + label_len, ctx.options.max_link_paren_depth)?; - Some((EditNode::Link { range: i..next, url_range, url, title }, next)) + let node = if image { + let alt = crate::render::plain(&parse_inlines(&src[i + 2..after - 1], ctx)); + EditNode::Image { range: i..next, url_range, alt, url, title } + } else { EditNode::Link { range: i..next, url_range, url, title } }; + Some((node, next)) } pub fn parse_inlines(src: &str, ctx: &InlineContext<'_>) -> Vec { coalesce(parse_inner(src, ctx)) } @@ -540,7 +538,7 @@ impl InlineScanner<'_, '_> { return Some(after); } let image = opener.kind == BracketKind::Image; - let resolved = self.resolve_inline_link(image, after); + let resolved = self.resolve_inline_link(&opener, after); if resolved.is_none() && opener.kind == BracketKind::Link && let Some(item) = ref_item(&self.src[opener.label_start..close]) @@ -577,11 +575,13 @@ impl InlineScanner<'_, '_> { Some(self.apply_trailing_attrs(opener.node, next)) } - fn resolve_inline_link(&self, image: bool, after: usize) -> Option<(Inline, usize, bool)> { + fn resolve_inline_link(&self, opener: &Bracket, after: usize) -> Option<(Inline, usize, bool)> { if after >= self.src.len() || !starts(self.src, after, "(") { return None; } + let image = opener.kind == BracketKind::Image; let max_parens = self.ctx.options.max_link_paren_depth; let (inside, next) = paren_content(self.src, after + 1, max_parens)?; let (url, title) = parse_link_destination_title(inside, max_parens)?; + self.emit(opener.label_start - if image { 2 } else { 1 }, next, InlineEventKind::InlineLink { label_end: after, image }); self.emit(after, next, InlineEventKind::LinkTarget); Some(( if image { Inline::Image { attrs: Attr::default(), alt: Vec::new(), url, title } } else { Inline::Link { attrs: Attr::default(), children: Vec::new(), url, title } }, diff --git a/tests/test_export.py b/tests/test_export.py index 8d86ee2..ea97ff4 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -488,6 +488,18 @@ def link(url): assert seen == ['docs/a_(b).md#part', 'https://example.com', 'img/plot_(1).png', 'keep.png'] +@pytest.mark.parametrize('title', ['Core [API]', 'Core `list[str]`', 'A `]` character', 'A `[` character']) +def test_md2gfm_relink_bracketed_title(title): + source = f'| Title | Description |\n| --- | --- |\n| [{title}](core.html) | Details. |\n' + assert md2gfm(source, link=lambda url: url.replace('.html', '.md')) == source.replace('core.html', 'core.md') + + +def test_md2gfm_relink_linked_image(): + source = '[![Core [API]](plot.png){.thumbnail}](core.html)\n' + urls = {'plot.png': 'images/plot.png', 'core.html': 'core.md'} + assert md2gfm(source, link=urls.get) == '[![Core [API]](images/plot.png)](core.md)\n' + + def test_md2gfm_relink_templates(): source = '[Guide](old.md) {{ [hidden](old.md) }}\n' out = md2gfm(source, templates=MUSTACHE, link=lambda url: 'new.html')