@@ -50,7 +50,7 @@ def __init__(self, subs: list[dict], capture_rules: list[dict]) -> None:
5050 self .capture_rules = capture_rules
5151
5252
53- PRIMITIVES = {"boundary" , "line_start" , "line_end" , "word_boundary" , "non_boundary " , "space" }
53+ PRIMITIVES = {"boundary" , "line_start" , "line_end" , "word_boundary" , "non_word_boundary " , "space" }
5454_FUNCTIONS = {"upcase" , "downcase" , "title_case" , "reverse" , "strip" , "swapcase" }
5555_CONSTRAINTS = {"before" , "after" , "not_before" , "not_after" }
5656# Tokens that terminate an item inside a rule; a bare word equal to one
@@ -487,6 +487,11 @@ def _item_atom(self) -> dict:
487487 self ._consume (word )
488488 return {"type" : "function" , "name" : word }
489489 self ._consume (word )
490+ if self ._src .startswith ("." , self ._pos ):
491+ self ._pos += 1
492+ self ._skip_inline_ws ()
493+ name = self ._identifier ()
494+ return {"type" : "alias_ref" , "name" : name , "map" : word }
490495 return {"type" : "alias_ref" , "name" : word }
491496
492497 def _dependency (self ) -> dict :
@@ -662,6 +667,8 @@ def _render_item(item: dict, aliases: dict[str, str]) -> str:
662667 return "any([" + ", " .join (_render_item (sub , aliases ) for sub in items ) + "])"
663668 if kind == "range" :
664669 return f'any("{ _escape (item ["lo" ])} ".."{ _escape (item ["hi" ])} ")'
670+ if kind == "capture_group" :
671+ return "capture(" + _render_item (item ["inner" ], aliases ) + ")"
665672 if kind == "maybe" :
666673 inner = item ["inner" ]
667674 if inner ["type" ] != "string" :
@@ -673,13 +680,19 @@ def _render_item(item: dict, aliases: dict[str, str]) -> str:
673680 return "space"
674681 if name == "boundary" :
675682 return "boundary"
683+ if name == "non_word_boundary" :
684+ return "non_word_boundary"
676685 if name == "line_start" :
677686 return "line_start"
678687 if name == "line_end" :
679688 return "line_end"
680689 raise UnsupportedConstruct (f"primitive { name } " )
681690 if kind == "alias_ref" :
682691 name = item ["name" ]
692+ if item .get ("map" ):
693+ return _qualified_expr (item ["map" ], name )
694+ if name in _imported :
695+ return _qualified_expr (None , name )
683696 if name not in aliases :
684697 raise UnsupportedConstruct (f"unresolved alias { name } " )
685698 return aliases [name ]
@@ -714,7 +727,9 @@ def _regex_of(item: dict, aliases: dict[str, str]) -> str:
714727 return "" .join (_regex_of (part , aliases ) for part in item ["parts" ])
715728 if kind == "set" :
716729 items = item ["items" ]
717- if all (sub ["type" ] == "string" for sub in items ):
730+ if all (sub ["type" ] == "string" for sub in items ) and all (
731+ len (sub ["value" ]) == 1 for sub in items
732+ ):
718733 return "[" + re .escape ("" .join (sub ["value" ] for sub in items )) + "]"
719734 return "(?:" + "|" .join (_regex_of (sub , aliases ) for sub in items ) + ")"
720735 if kind == "range" :
@@ -723,15 +738,23 @@ def _regex_of(item: dict, aliases: dict[str, str]) -> str:
723738 return "(?:" + _regex_of (item ["inner" ], aliases ) + ")?"
724739 if kind == "capture_group" :
725740 return "(" + _regex_of (item ["inner" ], aliases ) + ")"
741+ if kind == "capture" :
742+ # ref(N) in a from position is a backreference.
743+ return f"\\ { item ['index' ]} "
726744 if kind == "primitive" :
727745 return {
728746 "boundary" : r"\b" ,
747+ "non_word_boundary" : r"\B" ,
729748 "line_start" : "^" ,
730749 "line_end" : "$" ,
731750 "space" : " " ,
732751 }.get (item ["name" ], None ) or _unsupported_primitive (item ["name" ])
733752 if kind == "alias_ref" :
734753 name = item ["name" ]
754+ if item .get ("map" ):
755+ return _qualified_regex (item ["map" ], name )
756+ if name in _imported :
757+ return _qualified_regex (None , name )
735758 if name not in aliases :
736759 raise UnsupportedConstruct (f"unresolved alias { name } " )
737760 return aliases [name ]
@@ -761,6 +784,10 @@ def _repl_of(item: dict, aliases: dict[str, str]) -> str:
761784 return _repl_of (item ["items" ][0 ], aliases )
762785 if kind == "alias_ref" :
763786 name = item ["name" ]
787+ if item .get ("map" ):
788+ return _qualified_repl (item ["map" ], name )
789+ if name in _imported :
790+ return _qualified_repl (None , name )
764791 if name not in aliases :
765792 raise UnsupportedConstruct (f"unresolved alias { name } " )
766793 raise UnsupportedConstruct ("alias in a result" )
@@ -842,6 +869,65 @@ def _stage_tree(
842869
843870_LIB_CACHE : dict [str , dict [str , str ]] = {}
844871
872+ # Dep-alias context of the document being converted, and a memoized
873+ # {target: {alias-name: item}} table for qualified alias resolution
874+ # (map.<dep-alias>.<name>). Single-threaded conversion; reset per doc.
875+ _dep_aliases : dict [str , str ] = {}
876+ _DEP_ALIAS_ITEMS : dict [str , dict ] = {}
877+ _imported : dict [str , str ] = {}
878+
879+
880+ def _is_library_target (target : str ) -> bool :
881+ # Libraries carry no path separators or dots (same heuristic as the
882+ # library-alias harvester) and resolve through _library_aliases.
883+ return "/" not in target and "." not in target
884+
885+
886+ def _dep_items (target : str ) -> dict :
887+ """Parse a dependency map's source and return its alias items,
888+ memoized per target."""
889+ if target in _DEP_ALIAS_ITEMS :
890+ return _DEP_ALIAS_ITEMS [target ]
891+ from .interscript import _find_map
892+
893+ path = _find_map (target )
894+ if path is None :
895+ raise UnsupportedConstruct (f"dependency map { target !r} not found" )
896+ doc = _IscParser (Path (path ).read_text (encoding = "utf-8" ), str (path )).parse ()
897+ items = {a ["name" ]: a ["value" ] for a in doc ["aliases" ]}
898+ _DEP_ALIAS_ITEMS [target ] = items
899+ return items
900+
901+
902+ def _qualified_items (map_alias : str , name : str ) -> tuple [dict , dict ]:
903+ target = _dep_aliases .get (map_alias ) or _imported .get (name ) or _dep_aliases .get (name )
904+ if target is None :
905+ raise UnsupportedConstruct (f"unresolved alias { name } " )
906+ return _qualified_items_for (target , name )
907+
908+
909+ def _qualified_items_for (target : str , name : str ) -> tuple [dict , dict ]:
910+ _dep_aliases = target
911+ items = _dep_items (target )
912+ if name not in items :
913+ raise UnsupportedConstruct (f"unresolved alias { target } .{ name } " )
914+ return items [name ], items
915+
916+
917+ def _qualified_expr (map_alias : str , name : str ) -> str :
918+ item , items = _qualified_items (map_alias , name )
919+ return _render_item (item , {k : _render_item (v , items ) for k , v in items .items ()})
920+
921+
922+ def _qualified_regex (map_alias : str , name : str ) -> str :
923+ item , _ = _qualified_items (map_alias , name )
924+ return expr_to_regex (_qualified_expr (map_alias , name ))
925+
926+
927+ def _qualified_repl (map_alias : str , name : str ) -> str :
928+ item , _ = _qualified_items (map_alias , name )
929+ return _repl_of (item , {})
930+
845931
846932def _library_aliases (name : str , libs_dir : Path ) -> dict [str , str ]:
847933 """Harvest `def_alias NAME, expr` lines from an .iml library.
@@ -901,6 +987,24 @@ def isc_to_tree(source: str, filename: str | None = None, on_unsupported: str =
901987 d ["aliasName" ]: d ["target" ] for d in doc ["dependencies" ] if d .get ("aliasName" )
902988 }
903989
990+ _dep_aliases .clear ()
991+ _DEP_ALIAS_ITEMS .clear ()
992+ _imported .clear ()
993+ _dep_aliases .update (dep_aliases )
994+
995+ # Alias-less (import) dependencies merge their aliases into scope.
996+ # A target that resolves to a map file is parsed; legacy libraries
997+ # were already harvested into library_aliases above.
998+ for d in doc ["dependencies" ]:
999+ if d .get ("aliasName" ):
1000+ continue
1001+ from .interscript import _find_map
1002+
1003+ if _find_map (d ["target" ]) is not None :
1004+ items = _dep_items (d ["target" ])
1005+ for name in items :
1006+ _imported .setdefault (name , d ["target" ])
1007+
9041008 def _subst (rule : dict ) -> dict :
9051009 pattern = _guarded_regex (rule , regex_aliases )
9061010 to = rule ["to" ]
0 commit comments