spellcheck-procs.tcl

Spell-check library for OpenACS templating system.

Location:
packages/acs-templating/tcl/spellcheck-procs.tcl
Created:
2003-09-21
Author:
Ola Hansson <ola@polyxena.net>
CVS Identification:
$Id: spellcheck-procs.tcl,v 1.30 2024/09/11 06:15:48 gustafn Exp $

Procedures in this file

Detailed information

template::data::transform::spellcheck (public)

 template::data::transform::spellcheck -element_ref element_ref \
    -values values

Transform submitted and previously validated input into a spellcheck datastructure.

Switches:
-element_ref (required)
Reference variable to the form element.
-values (required)
The set of values for that element.

Partial Call Graph (max 5 caller/called nodes):
%3 template::element::querygetall template::element::querygetall (public) template::data::transform::spellcheck template::data::transform::spellcheck template::element::querygetall->template::data::transform::spellcheck template::data::transform::richtext template::data::transform::richtext (public) template::data::transform::spellcheck->template::data::transform::richtext template::element::set_error template::element::set_error (public) template::data::transform::spellcheck->template::element::set_error template::element::set_properties template::element::set_properties (public) template::data::transform::spellcheck->template::element::set_properties template::util::richtext::get_property template::util::richtext::get_property (public) template::data::transform::spellcheck->template::util::richtext::get_property template::util::richtext::set_property template::util::richtext::set_property (public) template::data::transform::spellcheck->template::util::richtext::set_property

Testcases:
No testcase defined.

template::util::spellcheck (public)

 template::util::spellcheck command [ args... ]

Dispatch procedure for the spellcheck object

Parameters:
command (required)

Partial Call Graph (max 5 caller/called nodes):
%3

Testcases:
No testcase defined.

template::util::spellcheck::get_element_formtext (public)

 template::util::spellcheck::get_element_formtext -text text [ -html ] \
    -var_to_spellcheck var_to_spellcheck [ -language language ] \
    -error_num_ref error_num_ref [ -no_abort ] \
    -formtext_to_display_ref formtext_to_display_ref \
    [ -just_the_errwords_ref just_the_errwords_ref ]
Switches:
-text (required)
The string to check for spelling errors.
-html (optional, boolean, defaults to "0")
-var_to_spellcheck (required)
The name of the text input type or textarea that holds this text (e.g., "email_body")
-language (optional)
-error_num_ref (required)
-no_abort (optional, boolean, defaults to "0")
-formtext_to_display_ref (required)
-just_the_errwords_ref (optional)

Partial Call Graph (max 5 caller/called nodes):
%3 test_spellcheck__get_element_formtext spellcheck__get_element_formtext (test acs-templating) template::util::spellcheck::get_element_formtext template::util::spellcheck::get_element_formtext test_spellcheck__get_element_formtext->template::util::spellcheck::get_element_formtext acs_package_root_dir acs_package_root_dir (public) template::util::spellcheck::get_element_formtext->acs_package_root_dir ad_return_error ad_return_error (public) template::util::spellcheck::get_element_formtext->ad_return_error ad_script_abort ad_script_abort (public) template::util::spellcheck::get_element_formtext->ad_script_abort ad_tmpdir ad_tmpdir (public) template::util::spellcheck::get_element_formtext->ad_tmpdir template::data::transform::spellcheck template::data::transform::spellcheck (public) template::data::transform::spellcheck->template::util::spellcheck::get_element_formtext

Testcases:
spellcheck__get_element_formtext

template::util::spellcheck::get_sorted_list_with_unique_elements (public)

 template::util::spellcheck::get_sorted_list_with_unique_elements \
    -the_list the_list

Converts a list of possibly duplicate elements (words) into a sorted list where no duplicates exist.

Switches:
-the_list (required)
The list of possibly duplicate elements.

Partial Call Graph (max 5 caller/called nodes):
%3

Testcases:
No testcase defined.

template::util::spellcheck::merge_text (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.

Parameters:
element_id (required)

Partial Call Graph (max 5 caller/called nodes):
%3 template::data::transform::spellcheck template::data::transform::spellcheck (public) template::util::spellcheck::merge_text template::util::spellcheck::merge_text template::data::transform::spellcheck->template::util::spellcheck::merge_text

Testcases:
No testcase defined.

template::util::spellcheck::spellcheck_properties (public)

 template::util::spellcheck::spellcheck_properties \
    -element_ref element_ref

Returns a list of spellcheck properties in array setable format.

Switches:
-element_ref (required)

Partial Call Graph (max 5 caller/called nodes):
%3 test_spellcheck__spellcheck_properties spellcheck__spellcheck_properties (test acs-templating) template::util::spellcheck::spellcheck_properties template::util::spellcheck::spellcheck_properties test_spellcheck__spellcheck_properties->template::util::spellcheck::spellcheck_properties parameter::get_from_package_key parameter::get_from_package_key (public) template::util::spellcheck::spellcheck_properties->parameter::get_from_package_key template::element::querygetall template::element::querygetall (public) template::element::querygetall->template::util::spellcheck::spellcheck_properties template::widget::richtext template::widget::richtext (public) template::widget::richtext->template::util::spellcheck::spellcheck_properties template::widget::text template::widget::text (public) template::widget::text->template::util::spellcheck::spellcheck_properties template::widget::textarea template::widget::textarea (public) template::widget::textarea->template::util::spellcheck::spellcheck_properties

Testcases:
spellcheck__spellcheck_properties
[ hide source ] | [ make this the default ]

Content File Source

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.30 2024/09/11 06:15:48 gustafn Exp $
}

namespace eval template {}
namespace eval template::data {}
namespace eval template::data::transform {}
namespace eval template::util {}
namespace eval template::util::spellcheck {}

ad_proc -public template::util::spellcheck { command args } {
    Dispatch procedure for the spellcheck object
} {
    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 __form__ [ns_getform]
    set merge_text [ns_set get $__form__ $element_id.merge_text]
    ns_set delkey $__form__ $element_id.merge_text

    if { $merge_text eq "" } {

        return {}
    }

    # loop through errors and substitute the corrected words for #errnum#.
    set i 0
    while { [ns_set find $__form__ $element_id.error_$i] != -1 } {
        regsub "\#$i\#" $merge_text [ns_set get $__form__ $element_id.error_$i] merge_text
        ns_set delkey $__form__ $element_id.error_$i
        incr i
    }

    ns_set cput $__form__ $element_id $merge_text
    ns_set cput $__form__ $element_id.spellcheck ":nospell:"
    return $merge_text
}

d_proc -public template::data::transform::spellcheck {
    -element_ref:required
    -values:required
} {
    Transform submitted and previously validated input into a spellcheck datastructure.

    @param element_ref Reference variable to the form element.
    @param values The set of values for that element.
} {

    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)]

    set richtext_p [expr {$element(datatype) eq "richtext"}]
    if { $richtext_p } {
        # special treatment for the "richtext" datatype.
        set format [template::util::richtext::get_property format [lindex $values 0]]
        if { $merge_text ne "" } {
            set richtext_value [lindex [template::data::transform::richtext element] 0]
            return [list [template::util::richtext::set_property contents $richtext_value $merge_text]]
        }
        set contents [template::util::richtext::get_property contents [lindex $values 0]]
    } else {
        if { $merge_text ne "" } {
            return [list $merge_text]
        }
        set contents [lindex $values 0]
    }

    if { $contents eq "" } {
        return $values
    }
    # if language is empty or :nospell: string don't spellcheck
    set language [ns_queryget $element(id).spellcheck]
    set spellcheck_p [expr {$language ni {":nospell:" ""}}]

    # perform spellchecking or not?
    if { $spellcheck_p } {

        template::util::spellcheck::get_element_formtext \
            -text $contents \
            -var_to_spellcheck $element(id) \
            -language $language \
            -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.

            # disable element validation since that will conflict with
            # the spellchecking DAVEB
            template::element::set_properties $element(form_id) $element(id) -validate [list]

            template::element::set_error $element(form_id) $element(id) "
          [expr {$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 } {
                # mutate datatype to prevent validation of spellcheck
                # form data by richtext validation DAVEB
                template::element::set_properties $element(form_id) $element(id) -datatype text
                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 [template::util::richtext::set_property contents [lindex $values 0] $contents]]
    } else {
        return [list $contents]
    }
}

d_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 { $list_element ne $old_element } {
            lappend new_list $list_element
        }
        set old_element $list_element
    }

    return $new_list

}

d_proc -public template::util::spellcheck::get_element_formtext {
    -text:required
    {-html:boolean 0}
    -var_to_spellcheck:required
    {-language ""}
    -error_num_ref:required
    {-no_abort:boolean 0}
    -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 no_abort_p Set this tue for testing purposes (e.g. aa_test).

    @param var_to_spellcheck The name of the text input type or textarea that holds this text (e.g., "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 f [file tempfile tmpfile [ad_tmpdir]/webspell-XXXXXX]
    puts $f $text_to_spell_check
    close $f

    set lines [split $text "\n"]

    # Support for local, localized, dictionaries (UI to add to them is not implemented yet!)
    set suffix [expr {$language ne "" ? "-$language" : ""}]
    set dictionaryfile [file join [acs_package_root_dir acs-templating] resources forms webspell-local-dict$suffix]

    # The webspell wrapper is used to achieve independence from 
    # nsd's env(HOME).

    set spelling_wrapper [file join $::acs::rootdir bin webspell]
    if {![file executable $spelling_wrapper]} {
        #
        # In case no_abort is given we just return the error
        # message. Otherwise an ad_return_error is raised and the
        # script is ad_script_aborted.
        #
        if {!$no_abort_p} {
            ad_return_error "Webspell could not be executed" \
                "Spell-checking is enabled but the spell-check wrapper\
                 ($::acs::rootdir/bin/webspell) returns  not be executed.\
                 Check that the wrapper exists, and that its permissions are correct."
            ad_script_abort
        } else {
            error $errmsg
        }
    }

    set spellchecker_path [nsv_get spellchecker path]

    #
    # Force default language to en_US
    #set ::env(LANG) en_US.UTF-8

    # the --lang switch only works with aspell and if it is not present
    # aspell's (or ispell's) default language will have to do.
    set lang_and_enc "--encoding=utf-8"
    if { $language ne "" } {
        append lang_and_enc " --lang=$language"
    }

    #ns_log notice WRAPPER=[list |$spelling_wrapper [ns_info home] $spellchecker_path $lang_and_enc $dictionaryfile $tmpfile]

    if {[catch {
        set ispell_lines [exec $spelling_wrapper [ns_info home] $spellchecker_path $lang_and_enc $dictionaryfile $tmpfile]
    } errmsg]} {
        #ns_log notice "errorMsg = $errmsg"

        #
        # In case no_abort is given we just return the error
        # message. Otherwise an ad_return_error is raised and the
        # script is ad_script_aborted.
        #
        if {!$no_abort_p} {
            ad_return_error "No dictionary found" \
                "Spell-checking is enabled but the spell-check dictionary\
                 could not be reached. Check that the dictionary exists,\
                 and that its permissions are correct.\
                 <p>Here is the error message: <pre>$errmsg</pre>"
            ad_script_abort
        } else {
            error $errmsg
        }
    }

    file delete -- $tmpfile

    ####
    #
    # Ispell is done. Start manipulating the result string.
    #
    ####

    set ispell_lines [split $ispell_lines "\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 dummy]
                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

    set error_list [join $errors]

    foreach { errtype errnum errword erroptions } $error_list {
        set wordlen [string length $errword]

        if {"miss" eq $errtype} {
            regsub "\#$errnum\#" $formtext "<input type=\"text\" name=\"${var_to_spellcheck}.error_$errnum\" value=\"$errword\" size=\"$wordlen\" >" formtext
        } elseif {"nearmiss" eq $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 behavior 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=\"[ns_quotehtml $processed_text]\" >"


    ####
    # just_the_errwords
    ####

    if { $just_the_errwords_ref ne ""} {

        upvar $just_the_errwords_ref just_the_errwords

        set just_the_errwords [list]
        foreach err $errors {
            lappend just_the_errwords [lindex $err 2]
        }

    }
}

d_proc -public template::util::spellcheck::spellcheck_properties {
    -element_ref:required
} {
    Returns a list of spellcheck properties in array setable format.
} {
    upvar $element_ref element

    if { [set spellcheck_value [ns_queryget $element(id).spellcheck]] eq "" } {

        # 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".

        # Do the "cheap" checks first and then (if needed) read the parameter and do additional checks.

        if { $element(mode) eq "display"
             || [info exists element(nospell)]
             || [nsv_get spellchecker path] eq "" } {

            set spellcheck_p 0
        } else {
            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]
                                    && ($element(widget) eq "richtext" ||
                                        $element(widget) eq "textarea" ||
                                        $element(widget) eq "text")
                                    && [info exists widget_info($element(widget))]
                                    && [set widget_info($element(widget))]
                                }]
        }

        if { $spellcheck_p } {
            # This is not a submit; we are rendering the form element for the first time and
            # since the spellcheck "sub widget" is to be displayed we'll also want to know
            # which option should be selected by default.

            array set spellcheck {render_p 1 perform_p 1}

            if { $widget_info(${element(widget)}) } {
                set spellcheck(selected_option) [nsv_get spellchecker default_lang]
            } else {
                set spellcheck(selected_option) ":nospell:"
            }

        } else {

            array set spellcheck {render_p 0 perform_p 0}

            # set this to something so the script won't choke.
            set spellcheck(selected_option) ":nospell:"
        }

    } 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.
        # Let's check which it is and keep record of the states of our select menu in
        # case the error page is shown (because of an error in a neighboring element).

        set spellcheck(selected_option) $spellcheck_value

        if {":nospell:" eq $spellcheck(selected_option)} {
            set spellcheck(perform_p) 0
            set spellcheck(render_p) 0

        } else {
            set spellcheck(perform_p) 1
            set spellcheck(render_p) 1
        }
    }

    return [array get spellcheck]
}

# Local variables:
#    mode: tcl
#    tcl-indent-level: 4
#    indent-tabs-mode: nil
# End: