TruerWords Logo
Google
 
Web www.truerwords.net

Search TruerWords

Welcome
Sign Up  Log On

Replacement for Frontier's string.wrap function

Frontier's string.wrap is a nearly useless verb, and is especially weird because it does the opposite of what its name suggests: it actually unwraps a wrapped string. In other words, you pass in a message, like an email with hard returns at the end of every line, and it strips out the hard returns within the paragraphs and leaves the double-spaced paragraphs alone.

So, string.wrap( "a\rb\rc\r\rd" ) returns "a b c\r\rd".

I'm rewriting it in a way that maintains backwards compatibility but adds the following features:

  • rewrap the text to any width (default is infinity, for backwards compatibility)
  • optionally maintains indent levels, so if a paragraph starts out with a 4-space indent then every line in that paragraph in the output will start with 4 spaces (so, for example, you could wrap a Frontier outline and the result would still look like an outline)

The first version is script-based, just because it's a little easier to write. When I know it is correct, I'm going to port it to the kernel.

It seems to be working correctly for me, but I'd really appreciate it if a few more people would look it over before I kernelize it! Post your comments here (preferred), or send them to me in email.

Updates

2006/03/21

  • New version of script (see Revisions notes in the script header). Added features, fixed bug.
  • Added this Updates section
  • Cleaned up the HTML a bit.

Testing

First, of course, you need the script. You can download it here, or just copy-and-paste it (from the very end of this page) into your own copy of Frontier. Testing instructions follow:

  1. install the script at workspace.stringWrap
  2. copy some text to the clipboard
  3. expand the bundle at the bottom of the script and set the value of the variable "wrapWidth" to any number
  4. run the script (it will grab the text from the clipboard automatically)
  5. paste the wrapped string into a text editor (like BBEdit) and make sure you're using a fixed-width font like Monaco. Did the wrap work the way I've described?
  6. Repeat steps 2-3 with different text and different wrap widths. Can you make it work incorrectly? Those are the tests I'm looking for.

Note that the current version returns a string with mac line endings. Period. It wouldn't be difficult to use the current platform's endings, or to let you specify which line endings to use when you call the script, but this is good enough for now. (If you're on windows, run the results through string.replaceAll( wrapped, cr, "\r\n" ) after calling workspace.stringWrap()).

The Script

«2006/03/19 by Seth Dillingham.
	«Description:
		«Re-wrap a string to any maximum width-per-line.
		«2+ returns in a row are treated as a paragraph break and are maintained.
		«Optionally indents all lines in a paragraph according to the indentation of the first line of that paragraph.
		«Optionally, can indent all lines in all paragraphs with any string you provide
			«while still maintaining the maximum line length, of course
	«Parameters:
		«s [string]: the string to be re-wrapped
		«maxWidth [number]: (infinity) the maximum width of each line in the final output
		«flKeepIndent [boolean]: (false) should all lines in a paragraph have the same indent as the first line?
		«indent [string]: (empty string) any string, prepended to every line in the output...
			«- is automatically included in maxLength calculations
			«- IGNORED if flKeepIndent is true
	«Return:
		«[string]: the re-wrapped string
	«Errors:
		«
	«Revisions:
		«2006/03/21 by Seth Dillingham.
			«Updated to use improved routines from stringWrapQuoted
			«Added optional indent param (ignored if flKeepIndent is true)
			«[BUG] Blank lines with spaces or tabs would cause the following paragraph to be indented. Fixed.
				«Thomas Creedon found this bug.
«~~~~~~~~~~~~~~~~~~~~~~~~
on stringWrap( s, maxWidth = infinity, flKeepIndent = false, indent = "" ) {
   local ( tabSpaces = 4 );
   local ( i = 1, ct = 0 );
   local ( lastspace = 0 );
   local ( sz = 0, indentsz = 0 );
   local ( nextline = "", nextSpace = "" );
   local ( output = "" );
   
   on addBlankLines( ixStart ) {
      local ( ix = ixStart, ctFound = 0 );
      local ( ixEnd, nextIndent );
      
      while ( ix < sz ) {
         ixEnd = findNextIndent( ix );
         nextIndent = string.mid( s, ix, ixEnd - ix );
         
         if ( lineIsBlank( ix ) ) {
            if ( flKeepIndent ) {
               output = output + ( nextIndent + cr );}
            else {
               output = output + ( indent + cr );};
            
            ix = ixEnd + 1}
         else {
            break}};
      
      i = ix};
   on lineIsblank( ixStart ) {
      local ( ix = ixStart );
      
      while ( ix < sz ) {
         case s[ ix ] {
            ' ';
            tab;
            '>' {
               ix++};
            cr {
               break}}
         else {
            return false}};
      
      return true};
   on stripWhitespace( t ) {
      return string.replaceAll( string.replaceAll( t, ' ', "" ), tab, "" )};
   on skipWhitespace( ixStart ) {
      local ( ix = ixStart );
      
      while ( ix < sz ) {
         case s[ ix ] {
            ' ';
            tab {
               ix++}}
         else {
            break}};
      
      return ix};
   
   on findNextIndent( ixStart, adrSize = nil ) {
      local ( ix = ixStart );
      local ( isz = 0 );
      
      while ( ix <= sz ) {
         case s[ ix ] {
            ' ' {
               ix++;
               isz++};
            tab {
               ix++;
               isz = isz + ( tabSpaces - ( isz % tabSpaces ) )}}
         else {
            break}};
      
      if ( adrSize ) {
         adrSize^ = isz};
      
      return ix};
   on getIndent( ixStart ) {
      local ( isz = 0 );
      local ( ix = findNextIndent( ixStart, @isz ) );
      
      if ( flKeepIndent ) {
         indent = string.mid( s, ixStart, ix - ixStart );
         indentsz = isz};
      
      lastSpace = ix - 1;
      
      return ix};
   on nextIndentMatchesOldIndent( ixStart ) {
      local ( ixEnd = findNextIndent( ixStart ) );
      local ( newIndent = string.mid( s, ixStart, ixEnd - ixStart ) );
      
      
      return ( newIndent == indent )};
   
   on addWordToNextLine( ) {
      local ( nextwhite = lastspace );
      
      while ( i <= sz ) {
         case s[ i ] {
            ' ';
            tab;
            cr;
            lf {
               nextwhite = i;
               break}}
         else {
            i++}};
      
      if ( ct + ( i - lastspace - 1 ) > maxWidth ) {
         if ( ct > indentsz ) { // ct has one or more words, so we don't include this word
            i = lastSpace + 1;
            return false}};
      
      nextline = nextline + ( nextspace + string.mid( s, lastspace + 1, i - lastspace - 1 ) );
      ct = ct + ( i - lastspace - 1 );
      lastspace = nextwhite;
      
      return ( i < sz )};
   on addSpaceToNextLine( startIx, adrFlParaBreak ) {
      local ( ix = startix );
      local ( ctSpaces = 0, space = "" );
      
      while ( ( ix < sz ) and ( ct < maxWidth ) ) {
         case s[ ix ] {
            ' ' {
               ix++;
               ctSpaces++;
               space = space + ' ';};
            cr {
               if ( lineIsBlank( ix + 1 ) ) { // if it's blank, then it's a parragraph separator
                  adrFlParaBreak^ = true;
                  i = ix;
                  return false}
               else {
                  ix = skipWhitespace( ix + 1 );
                  lastspace = ix - 1;
                  
                  if ( ctSpaces == 0 ) { // this is a cheatin' way to skip whitespace at the end of a line
                     ctSpaces++;
                     space = space + ' '}}};
            tab {
               ix++;
               ctSpaces = ctSpaces + ( tabSpaces - ( ct + ctSpaces ) % tabSpaces );
               space = space + tab}}
         else {
            break}};
      
      if ( ( ( ix <= sz ) and ( s[ ix ] == cr ) ) or ( ct >= maxWidth ) ) { // skip remaining white space
         while ( ix < sz ) {
            case s[ ix ] {
               ' ';
               tab {
                  ix++};
               cr {
                  if ( nextIndentMatchesOldIndent( ix + 1 ) ) { // if the next line starts with the same quote/indent, then skip it
                     if ( lineIsBlank( ix + 1 ) ) {
                        adrFlParaBreak^ = true;
                        i = ix;
                        return false}
                     else {
                        ix = ix + sizeof( indent ) + 1;
                        lastspace = ix - 1}}}}
            else {
               break}}};
      
      if ( ct + ctSpaces >= maxWidth ) {
         nextSpace = "";
         i = ix;
         return false}
      else {
         lastspace = ix - 1;
         nextSpace = space;
         ct = ct + ctSpaces;
         i = ix;
         return true}};
   
   on getNextLine() {
      local ( flParaBreak = false );
      
      nextline = indent;
      nextspace = "";
      ct = indentsz;
      
      loop {
         if ( not addWordToNextLine( ) ) {
            break};
         
         if ( not addSpaceToNextLine( i, @flParaBreak ) ) {
            break}};
      
      output = output + ( nextline + cr );;
      
      if ( i >= sz ) {
         return false}
      else {
         return not flParaBreak}};
   on getNextParagraph() {
      i = getIndent( i );
      
      loop {
         if ( not getNextLine() ) {
            if ( i >= sz ) {
               return false}
            else {
               break}}};
      
      addBlankLines( ++i );
      
      return true};
   
   bundle { // init
      if ( maxWidth < 1 ) {
         ScriptError( "maxWidth must be greater than or equal to 1" )};
      
      s = string.replaceAll( string.replaceAll( s, "r\n", cr ), lf, cr );
      sz = sizeof( s );
      
      if ( sizeof( indent ) != 0 ) { // get the width of the indent
         local ( ix = 1, isz = 0 );
         
         indent = string.replaceAll( indent, lf, "" );
         
         while ( ix <= sizeof( indent ) ) {
            case indent[ ix ] {
               tab {
                  isz = isz + ( tabSpaces - ( isz % tabSpaces ) )};
               cr { // reset isz to 0
                  isz = 0}}
            else {
               isz++};
            
            ix++};
         
         indentsz = isz}};
   
   loop {
      if ( not getNextParagraph() ) {
         break};
      
      if ( i >= sz ) {
         break}};
   
   return output};
bundle { // test code
   local ( wrapWidth = 80 );
   local ( unwrapped, wrapped );
   
   unwrapped = clipboard.getValue( stringType );
   wrapped = stringWrap( unwrapped, wrapWidth, false );
   
   clipboard.putValue( wrapped )}
   «wp.newTextObject( wrapped, @temp.wrapped )
   «window.open( @temp.wrapped )

Page last updated: 3/21/2006




TruerWords
is Seth Dillingham's
personal web site.
Truer words were never spoken.