@@ -42,24 +42,24 @@ def test_malformed_links():
4242 tokens = md .parse ("- type ]]Target[[" )
4343 assert not any (t .meta and "relations" in t .meta for t in tokens )
4444
45- # Nested brackets
45+ # Nested brackets: the tail after the first ]] is not a (context), so the
46+ # line is not an explicit relation; inline handling depth-matches the link.
4647 tokens = md .parse ("- type [[Outer [[Inner]] ]]" )
4748 token = next (t for t in tokens if t .type == "inline" )
48- rel = parse_relation (token )
49- assert rel is not None
50- assert "Outer" in rel ["target" ]
49+ assert parse_relation (token ) is None
50+ assert all (r ["type" ] == "links_to" for r in token .meta ["relations" ])
5151
5252
5353def test_context_handling ():
5454 """Test handling of contexts."""
5555 md = MarkdownIt ().use (relation_plugin )
5656
57- # Unclosed context
57+ # Unclosed context is a prose tail, not a context: the line falls back to
58+ # an inline link instead of minting a typed edge with the tail dropped.
5859 tokens = md .parse ("- type [[Target]] (unclosed" )
5960 token = next (t for t in tokens if t .type == "inline" )
60- rel = parse_relation (token )
61- assert rel is not None
62- assert rel ["context" ] is None
61+ assert parse_relation (token ) is None
62+ assert token .meta ["relations" ] == [{"type" : "links_to" , "target" : "Target" , "context" : None }]
6363
6464 # Multiple parens
6565 tokens = md .parse ("- type [[Target]] (with (nested) parens)" )
@@ -98,6 +98,77 @@ def test_inline_relations():
9898 assert len (token .meta ["relations" ]) == 3
9999
100100
101+ def test_prose_tail_falls_back_to_inline_link ():
102+ """A sentence containing a wikilink must not mint a typed relation (#1260).
103+
104+ An explicit relation line ends at its target or its (context); trailing
105+ prose means the line is ordinary writing, and the old behavior both minted
106+ a junk type from the word before the link and silently dropped the tail.
107+ """
108+ md = MarkdownIt ().use (relation_plugin )
109+
110+ for src in [
111+ "- Added [[Target Note]] to the roster" ,
112+ "- Calls [[Target Note]] every Sunday" ,
113+ '- "multi word type" [[Target Note]] trailing prose' ,
114+ "- type [[Target Note]] (context) and more" ,
115+ ]:
116+ tokens = md .parse (src )
117+ token = next (t for t in tokens if t .type == "inline" )
118+ assert parse_relation (token ) is None , src
119+ assert token .meta ["relations" ] == [
120+ {"type" : "links_to" , "target" : "Target Note" , "context" : None }
121+ ], src
122+
123+
124+ def test_prose_tail_keeps_every_wikilink_in_the_tail ():
125+ """Falling back to inline handling preserves links the old path dropped."""
126+ md = MarkdownIt ().use (relation_plugin )
127+
128+ # The old explicit path minted `relates_to -> A` and lost B's edge entirely.
129+ tokens = md .parse ("- relates_to [[Alpha]] and [[Beta]]" )
130+ token = next (t for t in tokens if t .type == "inline" )
131+ assert parse_relation (token ) is None
132+ assert {r ["target" ] for r in token .meta ["relations" ]} == {"Alpha" , "Beta" }
133+ assert all (r ["type" ] == "links_to" for r in token .meta ["relations" ])
134+
135+ tokens = md .parse ("- Links: [[Alpha]], [[Beta]]" )
136+ token = next (t for t in tokens if t .type == "inline" )
137+ assert {r ["target" ] for r in token .meta ["relations" ]} == {"Alpha" , "Beta" }
138+
139+ # A context-looking tail whose opening paren closes before the end is prose:
140+ # accepting `(primary) and [[Beta]] (secondary)` as one context would drop
141+ # the Beta link — the corruption class this rule exists to prevent.
142+ tokens = md .parse ("- relates_to [[Alpha]] (primary) and [[Beta]] (secondary)" )
143+ token = next (t for t in tokens if t .type == "inline" )
144+ assert parse_relation (token ) is None
145+ assert {r ["target" ] for r in token .meta ["relations" ]} == {"Alpha" , "Beta" }
146+ assert all (r ["type" ] == "links_to" for r in token .meta ["relations" ])
147+
148+
149+ def test_explicit_relation_forms_still_parse ():
150+ """Hand-authored relation shapes keep their types after the #1260 fix."""
151+ md = MarkdownIt ().use (relation_plugin )
152+
153+ expected = {
154+ "- spouse_of [[Target Note]]" : ("spouse_of" , None ),
155+ "- requires [[Target Note]] (because reasons)" : ("requires" , "because reasons" ),
156+ '- "multi word type" [[Target Note]] (context)' : ("multi word type" , "context" ),
157+ }
158+ for src , (rel_type , context ) in expected .items ():
159+ tokens = md .parse (src )
160+ token = next (t for t in tokens if t .type == "inline" )
161+ rel = parse_relation (token )
162+ assert rel == {"type" : rel_type , "target" : "Target Note" , "context" : context }, src
163+
164+ # Known limitation, pinned deliberately: a single capitalized word with no
165+ # tail is indistinguishable by shape from a hand-authored type ("Requires"),
166+ # so it still mints. The grammar policy for this case is tracked in #1260.
167+ tokens = md .parse ("- Mother [[Target Note]]" )
168+ token = next (t for t in tokens if t .type == "inline" )
169+ assert parse_relation (token ) == {"type" : "Mother" , "target" : "Target Note" , "context" : None }
170+
171+
101172def test_unicode_targets ():
102173 """Test handling of Unicode in targets."""
103174 md = MarkdownIt ().use (relation_plugin )
0 commit comments