Skip to content

Instantly share code, notes, and snippets.

@perlpilot
Created January 31, 2011 21:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save perlpilot/804840 to your computer and use it in GitHub Desktop.
Save perlpilot/804840 to your computer and use it in GitHub Desktop.
Minor improvements to flussence's implementation of .indent
use v6;
use Test;
use MONKEY_TYPING;
my Int $?TABSTOP = 5;
my Str $?expanded-tab = ' ' x $?TABSTOP;
=begin pod
=for vim
{{{
=head1 Str.indent()
(trying to write this down in words I can understand)
* A "line" in this context is everything up to and including the \n.
* Tabs are elastic; C</^ ' ' \t/> equals C</^ \t/> given C<< $?TABSTOP > 1 >>.
* A full tab is equivalent to C<' ' x ($?TABSTOP // 8)>.
=head2 Indenting (positive number)
For lines beginning with tabs:
* Collapse as many C<$steps> as possible into C<\t>
* Prepend the C<\t> and add the remaining C<$steps> after the leading whitespace of the line
For all other lines:
* Prepend C<' ' x $steps> to each line
=head2 Outdenting (negative number)
Remove space from the right.
For lines beginning with tabs:
* Unindent from the right of the leading whitespace
* Tabs expand to C<' ' x $?TABSTOP>
For lines not beginning with tabs:
* Unindent from the left if sufficient normal space
* If tabs exist after spaces, elastic-expand them
=for vim
}}}
=end pod
augment class Str {
# Zero indent does nothing
our Str multi method indent($steps as Int where { $_ == 0 }) is export {
self;
}
# Indent
our Str multi method indent($steps as Int where { $_ > 0 }) is export {
# We want to keep trailing \n so we have to .comb explicitly instead of .lines
return self.comb(/:r ^^ \N* \n?/).map({
given $_ {
# On lines with leading tabs, keep them and add spaces after
when /^(\t \s*) (.*)/ {
"\t" x ($steps div $?TABSTOP) ~ $0
~ ' ' x ($steps mod $?TABSTOP) ~ $1;
}
# Use the existing space character if they're all the same
when /^ (\s) ($0*) (.*)/ {
$0 x $steps ~ $1 ~ $2
}
# Otherwise we just stick spaces at the beginning
default { ' ' x $steps ~ $_ }
}
}).join;
}
# Outdent
our Str multi method indent($steps as Int where { $_ < 0 }) is export {
my Int $prefix = self.common-indent;
warn sprintf('Asked to remove %d spaces, but the shortest indent is %d', -$steps, $prefix)
if -$steps > $prefix;
self.outdent(-$steps, $prefix)
}
# Auto-trim
our Str multi method indent(Whatever) is export {
self.outdent(self.common-indent);
}
# Various bits of the outdenting code below
my Str method outdent(Int $steps, Int $prefix = $steps) {
my @lines = self.comb(/:r ^^ \N* \n?/);
# TODO: this eval is a workaround because Rakudo doesn't grok
# variables in the range following a ** quantifier (nor
# in a range in a closure following a ** quantifier)
my $leading-ws = eval "token \{ ^ ' ' ** 0..$steps \}";
return @lines.map({
given $_ {
# Tab-indented line: remove spaces from right, with elastic tab expansion
when /^(\t+ \s*) (.*)/ { ... }
# Space-indented line
default { $_.subst($leading-ws, '') }
}
}).join;
}
my Int method common-indent() {
[min] self.comb(/^^\s+/)».expand-tabs()».chars;
}
my Str method expand-tabs() {
self.subst(/\t/, ' ' x $?TABSTOP, :g);
}
}
# S32/Str:586 - simple indent gets added at the beginning of the line {{{
for 'quack', " \t quack" -> $initial {
my @spaced = ([\~] ' ' xx 4) X~ $initial;
for @spaced.kv -> $index, $result {
is @spaced[0].indent($index),
$result,
'simple space indentation';
}
}
is " \t quack\n \t meow".indent(1).perl,
" \t quack\n \t meow".perl,
'Added space should be placed at the beginning of each line';
# }}}
# S32/Str:590 {{{
is ' quack'.indent(-2),
' quack',
'If $steps is negative, outdent that many spaces';
is " quack\n meow".indent(-3).perl,
"quack\n meow".perl,
'If a line contains too few spaces, only those should be removed';
given 'Warn when outdenting more than existing spaces would permit' -> $test {
' quack'.indent(-3);
flunk $test;
CATCH { pass $test; }
}
# }}}
# S32/Str:593 {{{
is " quack\n meow\n fish noises".indent(*).perl,
" quack\nmeow\n fish noises".perl,
'If $steps is *, visually align the string with the left margin by removing common indent';
# }}}
# S32/Str:596 {{{
is "\tquack".indent(-1).perl,
(' ' x ($?TABSTOP - 1) ~ 'quack').perl,
'Tabs expand to $?TABSTOP spaces as needed';
# }}}
# S32/Str:600 {{{
is "\tquack\n\t meow".indent($?TABSTOP + 1).perl,
"\t\t quack\n\t\t meow".perl,
'Non-$?TABSTOP indenting on tabbed lines should work sensibly';
# }}}
# S32/Str:601 {{{
is "\t quack".indent(-1).perl,
"\tquack".perl,
'When leading tabs are being outdented, preserve them by removing space from the right';
is "\t quack".indent(-2).perl,
(' ' x ($?TABSTOP - 1) ~ 'quack').perl,
'Expand tabs after normal space removal runs out';
is " \t quack".indent(-1).perl,
"\t quack".perl,
'Leading whitespace before a tab gets removed';
is " \t \t quack".indent(-2).perl,
(' ' x 7 ~ "\t quack").perl,
'Lines beginning with non-tab should outdent in left-to-right order, with tab expansion';
is "\tquack\n\t meow".indent($?TABSTOP).perl,
"\t\tquack\n\t\t meow".perl,
'Recognise tab indentation and indent it consistently';
is "\tquack\nmeow".indent($?TABSTOP).perl,
"\t\tquack\n{$?expanded-tab}meow".perl,
'Tab-indentation should work even when some lines have no leading whitespace';
# }}}
# Sanity checks {{{
is "\ta\n b".indent(1).perl,
"\ta\n b".lines».indent(1).join("\n").perl,
'Each line should be indented independently of others';
is "\ta\n b".indent(0).perl,
"\ta\n b".perl,
'.indent(0) should be a no-op';
is "\ta\n b".indent(1).indent(16).indent(0).indent(*).perl,
"\ta\n b".indent(True).indent('0x10').indent('blah').indent(*).perl,
'.indent accepts weird scalar input and coerces it to Int when necessary';
is " \t a\n \t b\n".indent(1).perl,
" \t a\n \t b\n".perl,
'Indentation should not be appended after a trailing \n';
# }}}
done;
# vim: set fdm=marker #
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment