From 164ec5f93c333a87a9530dbf02a821681c087ea5 Mon Sep 17 00:00:00 2001 From: Logan Barnett Date: Mon, 18 May 2026 16:22:58 -0700 Subject: [PATCH] fix: allow newlines inside bracket link path Emacs org-mode treats bracket links as inline objects bounded by their enclosing element (paragraph, list item, etc.), not by a single line. A `[[path]]` or `[[path][desc]]` may span a newline inside either part and is still recognised as one link. orgize 0.10 previously excluded `\n` from the path `take_while`, so multi-line links written by Emacs were dropped on parse. Description parsing already allowed newlines. The element-level parsers (e.g. `paragraph_node`) already bound the input passed to `link_node` at blank lines, so removing the per-link newline restriction cannot let a link escape its element. --- src/syntax/link.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/syntax/link.rs b/src/syntax/link.rs index a2aa4d9..447b5a1 100644 --- a/src/syntax/link.rs +++ b/src/syntax/link.rs @@ -22,7 +22,7 @@ pub fn link_node(input: Input) -> IResult { let mut parser = map( tuple(( l_bracket2_token, - take_while(|c: char| c != '<' && c != '>' && c != '\n' && c != ']'), + take_while(|c: char| c != '<' && c != '>' && c != ']'), opt(tuple(( r_bracket_token, l_bracket_token, @@ -110,3 +110,66 @@ fn parse() { assert!(link_node(("[[#id][desc]", config).into()).is_err()); } + +/// Emacs org-mode accepts a single newline inside a bracket link — both inside +/// the path and inside the description. These tests document the discrepancy +/// against orgize and serve as the reproducer for the upstream issue/PR. +#[test] +fn parse_multiline_link() { + use crate::{ast::Link, tests::to_ast}; + + let to_link = to_ast::(link_node); + + // Path-only link with a newline inside the path. + let link = to_link("[[really really long link\nthat just keeps going]]"); + insta::assert_debug_snapshot!( + link.syntax, + @r###" + LINK@0..49 + L_BRACKET2@0..2 "[[" + LINK_PATH@2..47 "really really long li ..." + R_BRACKET2@47..49 "]]" + "### + ); + + // Link with separate description that contains a newline. + let link = to_link("[[https://example.com][some\ndescription]]"); + insta::assert_debug_snapshot!( + link.syntax, + @r###" + LINK@0..41 + L_BRACKET2@0..2 "[[" + LINK_PATH@2..21 "https://example.com" + R_BRACKET@21..22 "]" + L_BRACKET@22..23 "[" + TEXT@23..39 "some\ndescription" + R_BRACKET2@39..41 "]]" + "### + ); +} + +/// `Org::parse` should also pick up multi-line links inside paragraphs. This +/// is the end-to-end case relevant to downstream formatters (e.g. org-fmt). +#[test] +fn parse_multiline_link_in_paragraph() { + use crate::{ast::Link, Org}; + use rowan::ast::AstNode; + + let org = Org::parse( + "Here is a [[really really long link\nthat just keeps going]] in prose.\n", + ); + + let links: Vec<_> = org + .document() + .syntax() + .descendants() + .filter_map(Link::cast) + .collect(); + + assert_eq!( + links.len(), + 1, + "expected one link, found {}", + links.len(), + ); +}