Algumas dicas, mas não uma resposta completa.
.*
com /s
vai comer tudo até o final da string. Mudando para o não-ganancioso .*?
, porém, irá corresponder a uma string mínima; os lookaheads não são forçados para o jogo. Minha estratégia usual para lidar com isso é incluir âncoras no lookaheads, mas a combinação de várias linhas dificulta isso.
/m
será necessário se você quiser corresponder várias vezes na mesma string e ainda usar ^$
anchors. Caso contrário, eles correspondem apenas ao começo e ao final da string.
A menos que você realmente precise de uma solução de caso geral, provavelmente vale a pena tentar um ordenando manualmente seus subpadrões, por exemplo:
(?gsmx)(?(DEFINE)
(?<a>\bcat\b)
(?<b>\bdog\b)
)
^.*?(?:
(?&a).*?(?&b)| # cat before dog
(?&b).*?(?&a) # dog before cat
)[^\n]*
$
Existem algumas coisas realmente interessantes que você pode fazer com subpadrões recursivos e referências anteriores, mas eu não consegui estruturá-los em um caso geral para N lookaheads sem o número de etapas subindo rapidamente na faixa de 10k +.