108 102 94 87 82 64 63 bytes
Thanks to Sp3000 for making me pursue my original approach, which brought the byte count from 108 down to 82.
Massive thanks to Kobi who found a much more elegant solution, which allowed me to save another 19 bytes on top of that.
<space> represents a single space character (which would otherwise be stripped by SE). For counting purposes, each line goes in a separate file and
\n should be replaced with an actual linefeed character. For convenience, you can run the code as is from a single file with the
Try it online.
Well... as usual I can't give a full introduction to balancing groups here. For a primer see my Stack Overflow answer.
The first stage is a
Split stage, which splits the input into lines of increasing length. The
_ indicates that empty chunks should be omitted from the splitting (which only affects the end, because there will be a match in the last position). The regex itself is entirely contained in a look-around so it won't match any characters, but only positions.
This part is based on Kobi's solution with some additional golfitude I found myself. Note that lookbehinds are matched right-to-left in .NET, so the following explanation should best be read bottom to top. I've also inserted another
\G in the explanation for clarity, although that isn't necessary for the pattern to work.
^ # And we ensure that we can reach the beginning of the stack by doing so.
# The first time this is possible will be exactly when tri(m-1) == tri(n-1),
# i.e. when m == n. Exactly what we want!
(?<-1>.)* # Now we keep matching individual characters while popping from group <1>.
\G # We've now matched m characters, while pushing i-1 captures for each i
# between 1 and m, inclusive. That is, group <1> contains tri(m-1) captures.
\G # The \G anchor matches at the position of the last match.
(.)* # ...push one capture onto group <1> for each character between here
# here and the last match.
) # Then we use a lookahead to...
. # In each iteration we match a single character.
)+ # This group matches all the characters up to the last match (or the beginning
# of the string). Call that number m.
) # If the previous match was at position tri(n-1) then we want this match
# to happen exactly n characters later.
I'm still admiring Kobi's work here. This is even more elegant than the prime testing regex. :)
Let's move on to the next stage:
Simple: insert a space after every non-linefeed character.
This last stage indents all the lines correctly to form the triangle. The
m is just the usual multiline mode to make
^ match the beginning of a line. The
+ tells Retina to repeat this stage until the string stops changing (which, in this case means that the regex no longer matches).
^ # Match the beginning of a line.
(?= # A lookahead which checks if the matched line needs another space.
( *) # Capture the indent on the current line.
\S # Match a non-space character to ensure we've got the entire indent.
.*\n # Match the remainder of the line, as well as the linefeed.
\1 # Check that the next line has at least the same indent as this one.
So this matches the beginning of any line which doesn't have a bigger indent than the next. In any such position we insert a space. This process terminates, once the lines are arranged in a neat triangle, because that's the minimal layout where each line has a bigger indent than the next.