############################################################################
#
#	File:     format.icn
#
#	Subject:  Program to word wrap a range of text
#
#	Author:   Robert J. Alexander
#
#	Date:     March 26, 2002
#
############################################################################
#
#   This file is in the public domain.
#
############################################################################
#
#  Filter to word wrap a range of text.
#
#  A number of options are available, including full  justification (see
#  usage text,  below).  All lines that have the same indentation as the
#  first  line (or same  comment leading character format if  -c option)
#  are wrapped.  Other lines are left as is.
#
#  This  program  is useful in conjunction with editors  that can invoke
#  filters on a range of selected text.
#
#  The -c option attempts to establish the form of a comment based on the
#  first  line, then does its best to deal  properly  with the following
#  lines.   The  types of  comment lines that  are handled  are those in
#  which  each  line starts with a "comment" character string  (possibly
#  preceded  by  spaces).  While formatting  comment  lines, text  lines
#  following the prototype line that don't match the  prototype  but are
#  flush with  the  left margin are also  formatted  as  comments.  This
#  feature simplifies  initially entering  lengthy  comments  or  making
#  major modifications, since  new  text can  be entered without concern
#  for comment formatting, which will be done automatically later.
#
############################################################################
#
#  Links: options
#
############################################################################

link options

procedure main(arg)
   local usage, opts, tabs, comment, format, just1, space, nspace, wchar, Entab
   local line, pre, empty, outline, spaces, word, len, width, xspace, Detab
   local outpre
   #
   #  Process the options.
   #
   usage := 
	 "usage: format [-options]\n_
			\t-w N\tspecify line width (default 72)\n_
			\t-t N\tspecify tab width (default 8)\n_
			\t-j\tfully justify lines\n_
			\t-J\tfully justify last line, too\n_
			\t-c\tattempt to format program comments\n_
			\t-n\tdon't put extra spaces after sentences\n_
			\t-h\tprint help message"
   opts := options(arg,"ht+w+cjJn")
   if \opts["h"] then stop(usage)
   width := integer(\opts["w"]) | 72
   tabs := (integer(\opts["t"]) | 8) + 1
   if tabs >= 2 then {
      Detab := detab
      Entab := entab
      }
   else Entab := Detab := 1
   comment := opts["c"]
   format := if \just1 | \opts["j"] then justify else 1
   just1 := opts["J"]
   xspace := if \opts["s"] then '' else '.?:!'
   #
   #  Initialize variables.
   #
   space := ' \t'
   nspace := ~space
   wchar := nspace
   #
   #  Read the first line to establish a prototype of comment format
   #  if -c option, or of leading spaces if normal formatting.
   #
   line := Detab(read(),tabs) | exit()
   line ?
      pre := (tab(many(space)) | "") ||
	 if \comment then
	    tab(many(nspace)) || tab(many(space)) |
			stop("### Can't establish comment pattern")
	 else
	    ""
   width -:= *pre
   empty := trim(pre)
   outpre := Entab(pre,tabs)
   outline := spaces := ""
   repeat {
      line ? {
	 #
	 #  If this line indicates a formatting break...
	 #
	 if (=empty & pos(0)) | (=pre & any(space) | pos(0)) |
			(/comment & not match(pre)) then {
	    write(outpre,"" ~== outline)
	    outline := spaces := ""
	    write(line)
	    }
	 #
	 #  Otherwise continue formatting.
	 #
	 else {
	    =pre
	    tab(0) ? {
	       tab(many(space))
	       while word := tab(many(wchar)) & (tab(many(space)) | "") do {
		  if *outline + *spaces + *word > width then {
		     write(outpre,"" ~== format(outline,width))
		     outline := spaces := ""
		     }
		  outline ||:= spaces || word
		  spaces := if any(xspace,word[-1]) then "  " else " "
		  }
	       }
	    }
	 }
      line := Detab(read(),tabs) | break
      }
   write(outpre,"" ~== (if \just1 then justify else 1)(outline,width))
end


#
#  justify(s,width) -- Inserts extra spaces between words of "s" so that
#  "s" will  be exactly  "width" characters  long.   "s" is  trimmed  of
#  spaces  on the right  and left ends.  If  "s" contains fewer than two
#  words, or if the trimmed version is longer than "width", the  trimmed
#  version  of "s" is returned unchanged.  Where some gaps between words
#  are  required  to  be   wider  than  others,  the  extra  spaces  are
#  distributed randomly to minimize "rivering" in justified paragraphs.
#
procedure justify(s,width)
   local wlist,wset,t,r
   static space,nspace
   initial {
      space := ' '
      nspace := &cset -- space
      }
   s := trim(s[many(space,s) | 1:0])
   wlist := []
   s ? while put(wlist,[tab(many(nspace)),*tab(many(space)) | 0])
   if *s >= width | *wlist < 2 then return s
   wset := set(wlist[1:-1])
   t := (width - *s) / *wset
   every (!wset)[2] +:= t
   every 1 to (width - *s) % *wset do {
      (t := ?wset)[2] +:= 1
      delete(wset,t)
      }
   r := ""
   every t := !wlist do r ||:= t[1] || repl(" ",t[2])
   return r
end
