Index: openacs-4/bin/webspell
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/bin/webspell,v
diff -u
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ openacs-4/bin/webspell	30 Sep 2003 19:27:48 -0000	1.1
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+# Wrapper for aspell/ispell that sets the HOME environment variable.
+# Can't seem to do this from AOLserver Tcl.
+# Takes three arguments: file to spellcheck, dictionary file, language.
+#
+# Root must be able to execute this file if AOLserver runs as root
+# (which it does if it runs on a privileged port, e.g., port 80)
+
+#HOME=/usr/local/aolserver
+HOME=$4
+export HOME
+exec $3 $5 -a -p $2 < $1
Index: openacs-4/packages/acs-tcl/tcl/form-processing-procs.tcl
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/acs-tcl/tcl/form-processing-procs.tcl,v
diff -u -r1.36 -r1.37
--- openacs-4/packages/acs-tcl/tcl/form-processing-procs.tcl	29 Sep 2003 13:20:55 -0000	1.36
+++ openacs-4/packages/acs-tcl/tcl/form-processing-procs.tcl	30 Sep 2003 19:27:48 -0000	1.37
@@ -424,6 +424,19 @@
     <p>
     </blockquote>
 
+    <blockquote><pre>
+    {email:text,nospell                      {label "Email Address"}
+                                              {html {size 40}}}
+    </pre><p>
+
+    Define an element of type text with spell-checking disabled. In case spell-checking is enabled globally
+    for the widget of this element ("text" in the example), the "nospell" flag will override that parameter
+    and disable spell-checking on this particular element. Currently, spell-checking can be enabled for
+    these widgets: text, textarea, and richtext.
+
+    <p>
+    </blockquote>
+
     @see ad_form_new_p
     @see ad_set_element_value
     @see ad_set_form_values
@@ -711,7 +724,8 @@
                         }
                     }
 
-                    optional {
+                    nospell -
+		    optional {
                         if { ![empty_string_p $af_element_parameters($element_name:$flag)] } {
                             return -code error "element $element_name: $flag attribute can not have a parameter"
                         }
Index: openacs-4/packages/acs-templating/acs-templating.info
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/acs-templating/acs-templating.info,v
diff -u -r1.16 -r1.17
--- openacs-4/packages/acs-templating/acs-templating.info	22 Sep 2003 18:03:48 -0000	1.16
+++ openacs-4/packages/acs-templating/acs-templating.info	30 Sep 2003 19:27:49 -0000	1.17
@@ -7,7 +7,7 @@
     <initial-install-p>t</initial-install-p>
     <singleton-p>t</singleton-p>
     
-    <version name="5.0d1" url="http://www.openacs.org/repository/download/apm/acs-templating-5.0d1.apm">
+    <version name="5.0d2" url="http://www.openacs.org/repository/download/apm/acs-templating-5.0d2.apm">
     <database-support>
     </database-support>
         <owner>Karl Goldstein</owner>
@@ -19,7 +19,7 @@
 template system provides a way to use a single layout
 specification for many physical pages, so the overall layout of a site can be more easily administered.</description>
 
-        <provides url="acs-templating" version="5.0d1"/>
+        <provides url="acs-templating" version="5.0d2"/>
         <requires url="acs-kernel" version="4.6.2"/>
 
         <callbacks>
@@ -32,6 +32,9 @@
             <parameter datatype="number"  min_n_values="1"  max_n_values="1"  name="ShowDataDictionariesP"  default="0" description="Offer data dictionaries on .dat/.fmt URL."/>
             <parameter datatype="number"  min_n_values="1"  max_n_values="1"  name="ShowCompiledTemplatesP"  default="0" description="Show thecompiled template (for debugging) at .cmp URL"/>
             <parameter datatype="string"  min_n_values="1"  max_n_values="1"  name="RefreshCache"  default="as needed" description="When to re-translate templates.  Speical values: "/>
+            <parameter datatype="string"  min_n_values="1"  max_n_values="1"  name="SpellcheckFormWidgets"  default="text 0 textarea 1 richtext 1" description="The widgets that you specify here will be spellcheck enabled in all forms (that use ad_form / form builder). Adding a 'nospell' flag to a form element overrides this parameter and disables spellchecking of that element. The format of this parameter is such: 'widget 1 widget 0 ...', where 'widget' stands for one of the form widgets that are possible to spellcheck enable: text, textarea, richtext. '1' after 'widget' indicates that the default should be to spellcheck; '0' means the user has to manually state that spellchecking should be performed on the form element. If the parameter is left blank, spellchecking will be disabled altogether."/>
+            <parameter datatype="string"  min_n_values="1"  max_n_values="1"  name="SpellcheckLang"  description="The country/language code of the language to use when spellchecking. Only works with aspell! Leave empty and change dictionary manually if you use ispell. Examples: en_US or da-DK or sv or sv_SE ..."/>
+            <parameter datatype="string"  min_n_values="1"  max_n_values="1"  name="SpellcheckerPath"  default="/usr/bin/aspell" description="The path to the ispell/aspell executable. (aspell is highly recommended if it supports your language)."/>
         </parameters>
 
     </version>
Index: openacs-4/packages/acs-templating/tcl/element-procs.tcl
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/acs-templating/tcl/element-procs.tcl,v
diff -u -r1.15 -r1.16
--- openacs-4/packages/acs-templating/tcl/element-procs.tcl	22 Sep 2003 13:47:35 -0000	1.15
+++ openacs-4/packages/acs-templating/tcl/element-procs.tcl	30 Sep 2003 19:27:49 -0000	1.16
@@ -115,6 +115,9 @@
                           the user to edit the contents. If set to the empty string or
                           not specified at all, the form's 'mode' setting is used instead.
 
+    @option nospell       A flag indicating that no spell-checking should be performed on
+                          this element. This overrides the 'SpellcheckFormWidgets' parameter.
+
     @option before_html   A chunk of HTML displayed immediately before the rendered element.
 
     @option after_html    A chunk of HTML displayed immediately after the rendered element.
@@ -523,7 +526,12 @@
   } else {
     set values [template::data::transform::$datatype element]
   }
- 
+    
+    if { [lindex [template::util::spellcheck::spellcheck_properties -element_ref element] 0] } {
+	
+	set values [template::data::transform::spellcheck -element_ref element -values $values]
+    }
+
   return $values
 }
 
Index: openacs-4/packages/acs-templating/tcl/richtext-procs.tcl
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/acs-templating/tcl/richtext-procs.tcl,v
diff -u -r1.8 -r1.9
--- openacs-4/packages/acs-templating/tcl/richtext-procs.tcl	22 Sep 2003 14:53:37 -0000	1.8
+++ openacs-4/packages/acs-templating/tcl/richtext-procs.tcl	30 Sep 2003 19:27:49 -0000	1.9
@@ -187,6 +187,18 @@
       append output [textarea_internal "$element(id)" attributes $contents]
       append output "<br>Format: [menu "$element(id).format" [template::util::richtext::format_options] $format {}]"
 
+      set spellcheck_properties [template::util::spellcheck::spellcheck_properties -element_ref element]
+      set spellcheck_p [lindex $spellcheck_properties 0]
+
+      if { $spellcheck_p } {
+          set yes_checked [lindex $spellcheck_properties 1]
+          set no_checked [lindex $spellcheck_properties 2]
+          append output " Spellcheck? 
+<input type=\"radio\" name=\"$element(id).spellcheck_p\" value=\"1\" $yes_checked /> Yes \n
+<input type=\"radio\" name=\"$element(id).spellcheck_p\" value=\"0\" $no_checked /> No"
+
+      }   
+
   } else {
       # Display mode
       if { [info exists element(value)] } {
Index: openacs-4/packages/acs-templating/tcl/spellcheck-procs.tcl
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/acs-templating/tcl/spellcheck-procs.tcl,v
diff -u
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ openacs-4/packages/acs-templating/tcl/spellcheck-procs.tcl	30 Sep 2003 19:27:49 -0000	1.1
@@ -0,0 +1,372 @@
+ad_library {
+    Spell-check library for OpenACS templating system.
+
+    @author Ola Hansson (ola@polyxena.net)
+    @creation-date 2003-09-21
+    @cvs-id $Id: spellcheck-procs.tcl,v 1.1 2003/09/30 19:27:49 olah Exp $
+}
+
+namespace eval template::util::spellcheck {}
+
+ad_proc -public template::util::spellcheck { command args } {
+    Dispatch procedure for the spellcheck object
+} {
+  eval template::util::spellcheck::$command $args
+}
+
+ad_proc -public template::util::spellcheck::merge_text { element_id } {
+    Returns the merged (possibly corrected) text or the empty string
+    if it is not time to merge.
+} {
+    set merge_text [ns_queryget $element_id.merge_text]
+
+    if { [empty_string_p $merge_text] } {
+	return {}
+    } 
+
+    # loop through errors and substitute the corrected words for #errnum#.
+    set i 0
+    while { [ns_queryexists $element_id.error_$i] } {
+	regsub "\#$i\#" $merge_text [ns_queryget $element_id.error_$i] merge_text
+	incr i
+    }
+
+    return $merge_text
+}
+
+ad_proc -public template::data::transform::spellcheck {
+    -element_ref:required
+    -values:required
+} {
+    upvar $element_ref element
+
+    # case 1, initial submission of non-checked text: returns {}.
+    # case 2, submission of the page showing errors: returns the corrected text.
+    set merge_text [template::util::spellcheck::merge_text $element(id)]
+
+    if { [set richtext_p [string equal "richtext" $element(datatype)]] } {
+	# special treatment for the "richtext" datatype.
+    	set format [template::util::richtext::get_property format [lindex $values 0]]
+	if { ![empty_string_p $merge_text] } {
+	    return [list [list $merge_text [ns_queryget $element(id).format]]]
+	} 
+    	set contents [template::util::richtext::get_property contents [lindex $values 0]]
+    } else {
+	if { ![empty_string_p $merge_text] } {
+	    return [list $merge_text]
+	} 
+	set contents [lindex $values 0]
+    }
+
+    if { [empty_string_p $contents] } {
+	return [list]
+    } 
+
+    set spellcheck_p [ad_decode [ns_queryget $element(id).spellcheck_p] 1 1 0]
+
+    # perform spellchecking or not?
+    if { $spellcheck_p } { 
+
+	template::util::spellcheck::get_element_formtext \
+	    -text $contents \
+	    -var_to_spellcheck $element(id) \
+	    -error_num_ref error_num \
+	    -formtext_to_display_ref formtext_to_display \
+	    -just_the_errwords_ref {} \
+	    -html
+	
+	if { $error_num > 0 } {
+	    # there was at least one error.
+
+	    template::element::set_error $element(form_id) $element(id) "
+          [ad_decode $error_num 1 "Found one error." "Found $error_num errors."] Please correct, if necessary."
+
+	    # switch to display mode so we can show our inline mini-form with suggestions.
+	    template::element::set_properties $element(form_id) $element(id) mode display
+
+	    if { $richtext_p } {
+		append formtext_to_display "
+<input type=\"hidden\" name=\"$element(id).format\" value=\"$format\" />"
+	    }
+
+	    # This is needed in order to display the form text noquoted in the "show errors" page ...
+	    template::element::set_properties $element(form_id) $element(id) -display_value $formtext_to_display
+
+	    set contents $formtext_to_display
+	}
+    }
+        
+    # no spellchecking was to take place, or there were no errors.
+    if { $richtext_p } {
+	return [list [list $contents $format]]
+    } else {
+	return [list $contents]
+    }
+}
+
+ad_proc -public template::util::spellcheck::get_sorted_list_with_unique_elements {
+    -the_list:required
+} {
+    
+    Converts a list of possibly duplicate elements (words) into a sorted list where no duplicates exist.
+    
+    @param the_list The list of possibly duplicate elements.
+    
+} {
+    
+    set sorted_list [lsort -dictionary $the_list]
+    set new_list [list]
+    
+    set old_element "XXinitial_conditionXX"
+    foreach list_element $sorted_list {
+	if { ![string equal $list_element $old_element] } {
+	    lappend new_list $list_element
+	}
+	set old_element $list_element
+    }
+    
+    return $new_list
+    
+}
+
+ad_proc -public template::util::spellcheck::get_element_formtext {
+    -text:required
+    {-html:boolean 0}
+    -var_to_spellcheck:required
+    -error_num_ref:required
+    -formtext_to_display_ref:required
+    {-just_the_errwords_ref ""}
+} {
+
+    @param text The string to check for spelling errors.
+    
+    @param html_p Does the text have html in it? If so, we strip out html tags in the string we feed to ispell (or aspell).
+    
+    @param var_to_spellcheck The name of the text input type or textarea that holds this text (eg., "email_body")
+    
+} {
+
+    # We need a "var_to_spellcheck" argument so that we may name the hidden errnum vars
+    # differently on each input field by prepending the varname.
+
+    set text_to_spell_check $text
+
+    # if HTML then substitute out all HTML tags
+    if { $html_p } {
+	regsub -all {<[^<]*>} $text_to_spell_check "" text_to_spell_check
+    }
+
+    set tmpfile [ns_mktemp "/tmp/webspellXXXXXX"]
+    set f [open $tmpfile w]
+    puts $f $text_to_spell_check
+    close $f
+    
+    set lines [split $text "\n"]
+    
+    set dictionaryfile [file join [acs_package_root_dir acs-templating] resources forms ispell-words]
+
+    # The webspell wrapper is necessary because ispell requires
+    # the HOME environment set, and setting env(HOME) doesn't appear
+    # to work from AOLserver.
+
+    set spelling_wrapper [file join [acs_root_dir] bin webspell]
+
+    set spellchecker_path [parameter::get_from_package_key \
+			       -package_key acs-templating \
+			       -parameter SpellcheckerPath \
+			       -default /usr/bin/aspell]
+
+    set language [parameter::get_from_package_key \
+			       -package_key acs-templating \
+			       -parameter SpellcheckLang \
+			       -default {}]
+
+    # the --lang switch only works with aspell and if it is not present
+    # aspell's (or ispell's) default language will have to do.
+    if { ![empty_string_p $language] } {
+	set language "--lang=$language"
+    }
+
+    set ispell_proc [open "|$spelling_wrapper $tmpfile $dictionaryfile $spellchecker_path [ns_info home] $language" r]
+
+    # read will occasionally error out with "interrupted system call",
+    # so retry a few times in the hopes that it will go away.
+    set try 0
+    set max_retry 10
+    while { [catch { set ispell_text [read -nonewline $ispell_proc] } errmsg]
+	    && $try < $max_retry } {
+	incr try
+	ns_log Notice "spellchecker had a problem: $errmsg"
+    }
+
+    if { [catch [close $ispell_proc] errmsg] } {
+	ad_return_error "No Dictionary Found" "Spell-checking is enabled but the spell-check program could not be executed. Check the <em>SpellcheckerPath</em> parameter in acs-templating. Also check the permissions on the spell-check program. <p>Here is the error message: <pre>$errmsg</pre>"
+	ad_script_abort
+    }
+
+    ns_unlink $tmpfile
+
+    if { $try == $max_retry } {
+        return -code error "webspell: Tried to execute spellchecker $max_retry times but it did not work out. Sorry!"
+    }
+
+    ####
+    #
+    # Ispell is done. Start manipulating the result string.
+    #
+    ####
+    
+    set ispell_lines [split $ispell_text "\n"]
+    # Remove the version line.
+    if { [llength $ispell_lines] > 0 } {
+	set ispell_lines [lreplace $ispell_lines 0 0]
+    }
+
+    ####
+    # error_num
+    ####
+    upvar $error_num_ref error_num
+
+    set error_num 0
+    set errors [list]
+    
+    set processed_text ""
+
+    set line [lindex $lines 0]
+    
+    foreach ispell_line $ispell_lines {
+	switch -glob -- $ispell_line {
+	    \#* {
+		regexp "^\# (\[^ \]+) (\[0-9\]+)" $ispell_line dummy word pos
+		regsub $word $line "\#$error_num\#" line
+		lappend errors [list miss $error_num $word]
+		incr error_num
+	    }
+	    &* {
+		regexp {^& ([^ ]+) ([0-9]+) ([0-9]+): (.*)$} $ispell_line dummy word n_options pos options
+		regsub $word $line "\#$error_num\#" line
+		lappend errors [list nearmiss $error_num $word $options]
+		incr error_num
+	    }
+	    "" {
+		append processed_text "$line\n"
+		if { [llength $lines] > 0 } {
+		    set lines [lreplace $lines 0 0]
+		    set line [lindex $lines 0]
+		}
+	    }
+	}
+    }
+
+    set formtext $processed_text
+
+    foreach { errtype errnum errword erroptions } [join $errors] {
+	set wordlen [string length $errword]
+	
+	if { [string equal "miss" $errtype] } {
+	    regsub "\#$errnum\#" $formtext "<input type=\"text\" name=\"${var_to_spellcheck}.error_$errnum\" value=\"$errword\" size=\"$wordlen\" />" formtext
+	} elseif { [string equal "nearmiss" $errtype] } {
+	    regsub -all ", " $erroptions "," erroptions
+	    set options [split $erroptions ","]
+	    set select_text "<select name=\"${var_to_spellcheck}.error_$errnum\">\n<option value=\"$errword\">$errword</option>\n"
+	    foreach option $options {
+		append select_text "<option value=\"$option\">$option</option>\n"
+	    }
+	    append select_text "</select>"
+	    regsub "\#$errnum\#" $formtext $select_text formtext
+	}
+    }
+    
+    ####
+    # formtext_to_display
+    ####
+    upvar $formtext_to_display_ref formtext_to_display
+
+    regsub -all "\r\n" $formtext "<br>" formtext_to_display
+
+    # We replace <a></a> with  <u></u> because misspelled text in link titles
+    # would lead to strange browser behaviour where the select boxes with the 
+    # proposed changes would itself be a link!!!
+    # It seemed like an okay idea to make the text underlined so it would a) work,
+    # b) still resemble a link ...
+    regsub -all {<a [^<]*>} $formtext_to_display "<u>" formtext_to_display
+    regsub -all {</a>} $formtext_to_display "</u>" formtext_to_display
+
+    append formtext_to_display "<input type=\"hidden\" name=\"${var_to_spellcheck}.merge_text\" value=\"$processed_text\" />"
+
+
+    ####
+    # just_the_errwords
+    ####
+
+    if { ![empty_string_p $just_the_errwords_ref]} {
+	
+	upvar $just_the_errwords_ref just_the_errwords
+	
+	set just_the_errwords [list]
+	foreach err $errors {
+	    lappend just_the_errwords [lindex $err 2]
+	}   
+	
+    }
+}
+
+ad_proc -public template::util::spellcheck::spellcheck_properties {
+    -element_ref:required
+} {
+    Returns a list of spellcheck properties.
+} {
+    upvar $element_ref element
+    
+    if { [empty_string_p [set spellcheck_p [ns_queryget $element(id).spellcheck_p]]] } {
+	
+	# The user hasn't been able to state whether (s)he wants spellchecking to be performed or not.
+	# That's either because spell-checking is disabled for this element, or we're not dealing with a submit.
+	# Whichever it is, let's see if, and then how, we should render the spellcheck "sub widget".
+	
+	array set widget_info [string trim [parameter::get_from_package_key -package_key acs-templating \
+						-parameter SpellcheckFormWidgets \
+						-default ""]]
+	
+	set spellcheck_p [expr [array size widget_info] \
+			      && ![info exists element(nospell)] \
+			      && ([string equal $element(widget) "richtext"] || [string equal $element(widget) "text"] || [string equal $element(widget) "textarea"]) \
+			      && [lsearch -exact [array names widget_info] $element(widget)] != -1]
+	
+	if { $spellcheck_p } {
+	    # This is not a submit; we are rendering the form element for the first time.
+	    # Since the spellcheck "sub widget" is to be displayed we'll also want to know
+	    # whether it is the "Yes" or the "No" button that should be pressed by default.
+	    if { $widget_info(${element(widget)}) } {
+		set yes_checked "checked"
+		set no_checked ""
+	    } else {
+		set yes_checked ""
+		set no_checked "checked"
+	    }
+	} else {
+	    # set these to something so the script won't choke.
+	    set yes_checked "n/a"
+	    set no_checked "n/a"
+	}
+	
+    } else {
+	
+	# The user has explicitly stated if (s)he wants spellchecking to be performed on the text or not.
+	# Hence we are in submit mode with spell-checking enabled (radio button true or false).
+	# Let's check which it is and keep record of the states of our radio buttons in case the error
+	# page is shown.
+
+	if { $spellcheck_p } {
+	    set yes_checked "checked"
+	    set no_checked ""
+	} else {
+	    set yes_checked ""
+	    set no_checked "checked"
+	    set spellcheck_p 1
+	}
+	
+    }
+    
+    return [list $spellcheck_p $yes_checked $no_checked]
+}
Index: openacs-4/packages/acs-templating/tcl/widget-procs.tcl
===================================================================
RCS file: /usr/local/cvsroot/openacs-4/packages/acs-templating/tcl/widget-procs.tcl,v
diff -u -r1.23 -r1.24
--- openacs-4/packages/acs-templating/tcl/widget-procs.tcl	22 Sep 2003 21:45:31 -0000	1.23
+++ openacs-4/packages/acs-templating/tcl/widget-procs.tcl	30 Sep 2003 19:27:49 -0000	1.24
@@ -209,6 +209,17 @@
 
   set output [textarea_internal $element(name) attributes $value $mode]
 
+  set spellcheck_properties [template::util::spellcheck::spellcheck_properties -element_ref element]
+  set spellcheck_p [lindex $spellcheck_properties 0]
+
+  if { $spellcheck_p } {
+      set yes_checked [lindex $spellcheck_properties 1]
+      set no_checked [lindex $spellcheck_properties 2]
+      append output "<br>Spellcheck? 
+<input type=\"radio\" name=\"$element(id).spellcheck_p\" value=\"1\" $yes_checked /> Yes \n
+<input type=\"radio\" name=\"$element(id).spellcheck_p\" value=\"0\" $no_checked /> No"
+  }   
+  
   return $output
 }
 
@@ -321,7 +332,18 @@
 
   upvar $element_reference element
 
-  return [input text element $tag_attributes]
+  set spellcheck_properties [template::util::spellcheck::spellcheck_properties -element_ref element]
+  set spellcheck_p [lindex $spellcheck_properties 0]
+
+  if { [string equal $element(mode) "edit"] && $spellcheck_p } {
+      set yes_checked [lindex $spellcheck_properties 1]
+      set no_checked [lindex $spellcheck_properties 2]
+      return "[input text element $tag_attributes] <br>Spellcheck? 
+<input type=\"radio\" name=\"$element(id).spellcheck_p\" value=\"1\" $yes_checked /> Yes \n
+<input type=\"radio\" name=\"$element(id).spellcheck_p\" value=\"0\" $no_checked /> No"
+  } else {
+      return [input text element $tag_attributes]
+  }
 }