...
 
Commits (10)
*~
.cpm
.curry
src/HTML/CGI/PackageConfig.curry
......@@ -3,4 +3,46 @@ html: Support for HTML programming
This package contains libraries to support HTML programming.
In order to execute dynamic web pages, you need to install
the Curry Port Name Server (CPNS), the HTML/CGI Registry,
and the script to generate CGI scripts from Curry programs.
This can be easily done by the commands
> cypm install cpns
> cypm install html-cgi
> cypm install html
These commands install the executables `curry-cpnsd`, `curry-cgi`,
and `curry-makecgi` in the bin directory of CPM.
`curry-makecgi` is used to compiler a dynamic web page implemented
in Curry, whereas the other executables are invoked during
the execution of a dynamic web page.
Some simple examples for dynamic web pages can be found in the
directory `examples`.
--------------------------------------------------------------------------
CGI Registry
------------
The CGI registry is a table of all active CGI server processes.
Usually, all CGI server processes are automatically started
or terminated (e.g., after 120 minutes of inactivity).
In order to manage these processes manually, one can use
the CGI registry. For this purpose, one can install the
web script `registry.cgi` (from the program `WebRegistry.curry`
in subdirectoy `helpers`, see more information there).
Then one can use this web script to see and manipulate the
current registry from the local host of the web server,
e.g., by a web browser or in a terminal via `curl`.
For instance, to see a list of active CGI server processes, execute
curl http://localhost/.../registry.cgi?show
To clean the registry from old CGI server processes, execute
curl http://localhost/.../registry.cgi?clean
--------------------------------------------------------------------------
------------------------------------------------------------------------------
-- Example for HTML programming in Curry:
--
-- A form to browse the structure of a directory.
-- The form is parameterized by the (URL-encoded) name of the directory.
-- Subdirectories are presented as links to browse them.
--
-- Michael Hanus, November 2018
------------------------------------------------------------------------------
import Directory
import HTML.Base
main :: IO HtmlForm
main = do
param <- getUrlParameter
let dir = if null param then "." else urlencoded2string param
entries <- getDirectoryContents dir
hexps <- mapIO (entry2html dir) entries
return $ form "Browse Directory"
[h1 [htxt $ "Directory: " ++ dir], ulist hexps]
-- Transform directory and entry in this directory into a link
-- (if it is a directory) or a text:
entry2html :: String -> String -> IO [HtmlExp]
entry2html dir e = do
direx <- doesDirectoryExist (dir ++ "/" ++ e)
if direx
then return [href ("?" ++ string2urlencoded (dir ++ "/" ++ e))
[htxt e]]
else return [htxt e]
-- Install with:
-- > curry-makecgi -o CGIBINDIR/browsebir.cgi BrowseDir
--
-- Call with: http://...CGIBINDIR/browsedir.cgi?<directory (urlencoded)>
------------------------------------------------------------------------------
-- Example for HTML programming in Curry:
--
-- A recursive form for a number guessing game
-- which also counts the number of guesses
------------------------------------------------------------------------------
import HTML.Base
main :: IO HtmlForm
main = return $ form "Number Guessing" (guessInput 1)
guessInput :: Int -> [HtmlExp]
guessInput n =
[htxt "Guess a natural number: ", textfield nref "",
button "Check" (guessHandler n nref)] where nref free
guessHandler :: Int -> CgiRef -> (CgiRef -> String) -> IO HtmlForm
guessHandler n nref env = do
let nr = read (env nref) :: Int
return $ form "Answer" $
if nr==42
then [h1 [htxt $ "Right! You needed "++show n++" guesses!"]]
else [h1 [htxt $ if nr<42 then "Too small!"
else "Too large!"],
hrule] ++ guessInput (n+1)
-- Install the CGI script in user homepage by:
-- > curry-makecgi -o ~/public_html/guess.cgi Guess
--- A stress test for the HTML/CGI library.
--- Each page produces 500 buttons, where each event handler for each button
--- generates a new page with 500 buttons.
import HTML.Base
main :: IO HtmlForm
main = return $ form "Multi-Button" $
map (flip button (const main) . show) [1..500]
-- > curry-makecgi -o ~/public_html/multibut.cgi MultiButton.curry
------------------------------------------------------------------------------
-- Example for CGI programming in Curry:
-- a form with a text input field and two event handlers
-- Example for HTML programming in Curry:
---
-- A form with a text input field and two event handlers
------------------------------------------------------------------------------
import HTML.Base
......@@ -22,5 +23,5 @@ main = return $ form "QUESTION" $
[h1 [htxt $ "Duplicated input: " ++ env tref ++ env tref]]
-- Install the CGI program by:
-- curry makecgi -cpm -o ~/public_html/revdup.cgi RevDup
-- Install the CGI script in user homepage by:
-- > curry-makecgi -o ~/public_html/revdup.cgi RevDup
------------------------------------------------------------------------------
--- A web script to access the CPNS demon.
---
--- If this web script is installed with name `cpnsd.cgi` (see below),
--- one can get information about the status of CGI registry via
--- the web page or in a terminal via `curl`.
---
--- For instance, to see the status of CPNSD, execute
---
--- curl http://localhost/.../cpnsd.cgi?status
---
--- To see the log file of CPNSD, execute
---
--- curl http://localhost/.../cpnsd.cgi?log
---
------------------------------------------------------------------------------
import IOExts ( evalCmd )
import System ( getEnviron )
import HTML.Base
import HTML.CGI
-- Remark: maybe one has to modify the check for non-local accesses
main :: IO HtmlForm
main = do
param <- getUrlParameter
remote <- getEnviron "REMOTE_ADDR"
if remote `elem` ["127.0.0.1","::1"]
then do result <- execCommand param
return $ answerText result
else return $ answerText $
"Access denied from non-local host (REMOTE_ADDR=" ++ remote ++ ")!\n"
execCommand :: String -> IO String
execCommand param = case param of
"status" -> cpnsCmd "status"
"log" -> cpnsCmd "log"
"start" -> cpnsCmd "start"
_ -> return $ unlines $
["ILLEGAL URL PARAMETER: " ++ param, "", cpnsCommands]
where
cpnsCmd cmd = do
(_,out,err) <- evalCmd "curry-cpnsd" [cmd] ""
return $ out ++ if null err then "" else "ERROR OUTPUT:\n" ++ err
cpnsCommands = "Allowed arguments: status | log | start"
-- Install the CGI script in user homepage by:
-- > curry-makecgi -o ~/public_html/cpnsd.cgi WebCPNSD
------------------------------------------------------------------------------
--- A web script to access the CGI registry manager.
---
--- If this web script is installed with name `registry.cgi` (see below),
--- one can get information about the status of CGI registry via
--- the web page or in a terminal via `curl`.
---
--- For instance, to see a list of active CGI server processes, execute
---
--- curl http://localhost/.../registry.cgi?show
---
--- To clean the registry from old CGI server processes, execute
---
--- curl http://localhost/.../registry.cgi?clean
---
------------------------------------------------------------------------------
import List ( isPrefixOf )
import System ( getEnviron )
import HTML.Base
import HTML.CGI
import HTML.CGI.Config
import HTML.CGI.Registry
-- Remark: maybe one has to modify the check for non-local accesses
main :: IO HtmlForm
main = do
param <- getUrlParameter
remote <- getEnviron "REMOTE_ADDR"
if remote `elem` ["127.0.0.1","::1"]
then do result <- execCommand param
return $ answerText result
else return $ answerText $
"Access denied from non-local host (REMOTE_ADDR=" ++ remote ++ ")!\n"
execCommand :: String -> IO String
execCommand param = case param of
"show" -> showAllActiveServers
"load" -> cmdForAllServers "Status of " GetLoad
"status" -> cmdForAllServers "Status of " SketchStatus
"sketch" -> cmdForAllServers "Sketch status of " SketchHandlers
"showall" -> cmdForAllServers "Status of " ShowStatus
"clean" -> do out <- cmdForAllServers "Clean status of " CleanServer
getAndCleanRegistry
return out
"stop" -> do out <- cmdForAllServers "Stop cgi server " StopCgiServer
getAndCleanRegistry
return out
"kill" -> killAllActiveServers
_ -> let stopscript = "stopscript&"
in if stopscript `isPrefixOf` param
then stopActiveScriptServers (drop (length stopscript) param)
else return $ unlines $
["ILLEGAL URL PARAMETER: " ++ param, ""] ++ registryCommands
-- Install the CGI script in user homepage by:
-- > curry-makecgi -o ~/public_html/registry.cgi WebRegistry
{
"name": "html",
"version": "1.1.0",
"version": "2.1.0",
"author": "Michael Hanus <mh@informatik.uni-kiel.de>",
"synopsis": "Libraries for HTML programming.",
"category": [ "Web" ],
"dependencies": { },
"exportedModules": [ "HTML.Base", "HTML.CgiServer",
"dependencies": {
"html-cgi": ">= 0.0.1"
},
"sourceDirs": [ "src", "scripts" ],
"exportedModules": [ "HTML.Base",
"HTML.CategorizedList", "HTML.LaTeX",
"HTML.Parser",
"HTML.Styles.Bootstrap3" ],
"compilerCompatibility": {
"pakcs": ">= 1.14.0, < 2.0.0",
"kics2": ">= 0.5.0, < 2.0.0"
"pakcs": ">= 2.0.0",
"kics2": ">= 2.0.0"
},
"license": "BSD-3-Clause",
"licenseFile": "LICENSE",
"executable": {
"name": "curry-makecgi",
"main": "MakeCGI"
},
"configModule": "HTML.CGI.PackageConfig",
"source": {
"git": "https://git.ps.informatik.uni-kiel.de/curry-packages/html.git",
"tag": "$version"
......
------------------------------------------------------------------------------
--- Script to compile a Curry program implementing a web script
--- using the package `html` and the library `HTML.Base`
--- into a cgi script to be placed in a server-accessible directory
--- for executing cgi scripts.
---
--- Actually, it just calls the shell script `makecgi.sh`
--- which does all the work.
---
--- @author Michael Hanus
--- @version November 2018
------------------------------------------------------------------------------
module MakeCGI
where
import Distribution ( installDir )
import FilePath ( (</>) )
import System
import HTML.CGI.PackageConfig ( packagePath )
main :: IO ()
main = do
args <- getArgs
rc <- system (unwords ((packagePath </> "scripts" </> "makecgi.sh") :
map (\s -> '"' : s ++ "\"") (installDir : args)))
exitWith rc
#!/bin/sh
# Script to compile a Curry program implementing a web script
# using the package `html` and the library `HTML.Base`
# into a cgi script to be placed in a server-accessible directory
# for executing cgi scripts.
# Root of Curry system must be the first argument:
CURRYROOT=$1
shift
# Standard suffix that will be added to the main script:
CGISUFFIX="_CGIMAIN_$$"
# Name of the main function in the main script (should not be in conflict
# with any exported name of the Curry program)
MAINCALL="main_cgi_9999_$$"
CPMEXEC="cypm exec"
ERROR=
HELP=no
CURRYDOPTIONS=
CURRYOPTIONS=":set -time :set -interactive"
COMPACT=no
ULIMIT="-t 120"
MAIN=main
CGIFILE=
WUIJS=no
WUIMODULES=
SERVERTIMEOUT=
STANDALONE=no
LOADBALANCE="-loadbalance standard"
ARGS=
while [ $# -gt 0 -a -z "$ERROR" ]; do
case $1 in
--help | -h | -\? ) HELP=yes ;;
-D* ) CURRYDOPTIONS="$CURRYDOPTIONS $1" ;;
--cpmexec ) shift ; CPMEXEC=$1 ;;
-cpmexec ) shift ; CPMEXEC=$1 ;;
--compact ) COMPACT=yes ;;
-compact ) COMPACT=yes ;;
--servertimeout ) shift ; SERVERTIMEOUT="-servertimeout $1" ;;
-servertimeout ) shift ; SERVERTIMEOUT="-servertimeout $1" ;;
--multipleservers ) LOADBALANCE="-loadbalance multiple" ;; # backward compt.
-multipleservers ) LOADBALANCE="-loadbalance multiple" ;; # backward compt.
--loadbalance ) shift ; LOADBALANCE="-loadbalance $1" ;;
-loadbalance ) shift ; LOADBALANCE="-loadbalance $1" ;;
--standalone ) STANDALONE=yes ;;
-standalone ) STANDALONE=yes ;;
--ulimit ) shift; ULIMIT=$1 ;;
-ulimit ) shift; ULIMIT=$1 ;;
--wuijs ) WUIJS=yes ;;
-wuijs ) WUIJS=yes ;;
--wui ) shift; WUIMODULES="$WUIMODULES $1" ;;
-wui ) shift; WUIMODULES="$WUIMODULES $1" ;;
-m ) shift; MAIN=$1 ;;
-o ) shift; CGIFILE=$1 ;;
-* ) ERROR="Unknown option: $1" ;;
* ) ARGS="$ARGS $1" ;; # collect non-option arguments
esac
shift
done
if test -n "$ARGS" ; then
set $ARGS
fi
if [ $HELP = yes ] ; then
set "1" ; shift # to show next usage message
fi
if test -n "$ERROR" ; then
echo "ERROR: $ERROR"
set "1" ; shift # to show next usage message
fi
if [ $# != 1 -a $# != 3 ] ; then
echo "USAGE: curry-makecgi [options] <curry>"
echo
echo "MAIN OPTIONS:"
echo "-o <cgi> : name of the file (with suffix .cgi) where the cgi program should"
echo " be stored (default: <curry>.cgi)."
echo "-m <form> : Curry expression (of type IO HtmlForm) computing the HTML form"
echo " (default: main)."
echo "<curry> : name of the Curry program (without suffix) containing the script"
echo
echo "FURTHER OPTIONS:"
echo '-Dname=val : define (curry)rc property "name" as "val"'
echo "--cpmexec <c>: set the command to execute programs with the Curry Package"
echo " Manager (default: 'cypm exec')"
echo "--compact : reduce size of generated cgi program by deleting unused functions"
echo "--ulimit <l> : set 'ulimit <l>' when executing the cgi program"
echo " (default: '-t 120')"
echo "--servertimeout <ms>: set the timeout for the cgi server process to"
echo " <ms> milliseconds (default: 7200000 / two hours)"
echo "--loadbalance <t>: start new server process if load for one server is"
echo " high where <t> specifies the kind of high load."
echo " Current possible values for <t>:"
echo " no: no load balance"
echo " standard: some standard load balancing (default)"
echo " multiple: new server process for each initial call to"
echo " a cgi script (only reasonable with short timeout)"
echo "--standalone : generate standalone script (i.e., copy programs"
echo " required from package 'html-cgi' to deploy directory)"
echo "--wuijs : generate JavaScript support code for WUIs"
echo "--wui <mod> : consider also imported module <mod> (that contains WUI"
echo " specifications) when generating JavaScript support code"
exit 1
fi
# Try to locate WUI/JavaScript translator:
WUIJS_PREPROCESSOR=`which curry2js`
if [ ! -x "$WUIJS_PREPROCESSOR" ] ; then
# try to set curry2js to the CPM standard location:
WUIJS_PREPROCESSOR=$HOME/.cpm/bin/curry2js
if [ ! -x "$WUIJS_PREPROCESSOR" ] ; then
WUIJS_PREPROCESSOR=
fi
fi
if [ -z "$WUIJS_PREPROCESSOR" -a $WUIJS = yes ] ; then
echo "No support for JavaScript possible!"
echo "Please install the Curry->JavaScript translator curry2js by:"
echo "> cypm update && cypm install curry2js"
exit 1
fi
# remove possible suffix:
PROG=`expr $1 : '\(.*\)\.lcurry' \| $1`
PROG=`expr $PROG : '\(.*\)\.curry' \| $PROG`
if test -z "$CGIFILE" ; then
CGIFILE=$PROG.cgi
fi
MAINMOD=$PROG$CGISUFFIX
MAINCURRY=$MAINMOD.curry
# compute (relative) name of cgi program:
CGIDIR=`dirname $CGIFILE`
if [ $CGIDIR = "." ] ; then
CGIPROG=$CGIFILE
else
CGIPROG=`expr $CGIFILE : "$CGIDIR/\(.*\)"`
fi
# generate cgi target directory if necessary:
mkdir -p $CGIDIR
# name of the server:
CGIFILEPATHNAME=`(cd $CGIDIR > /dev/null ; pwd)`
CGISERVERPROG=$CGIPROG.server
CGISERVEREXEC=$CGIFILEPATHNAME/$CGIPROG.server
CGISERVERCMD=$CGISERVEREXEC.sh
# unique key for this cgi script:
CGIKEY="$CGIFILEPATHNAME/$CGIPROG `date '+%m/%d/%y/%H/%M/%S'`"
# generate server program implementing the cgi script application:
rm -f $MAINCURRY
echo "module $MAINMOD($MAINCALL) where" >> $MAINCURRY
echo "import $PROG" >> $MAINCURRY
echo "import HTML.Base" >> $MAINCURRY
echo "import HTML.CGI.Server" >> $MAINCURRY
echo "$MAINCALL :: IO ()" >> $MAINCURRY
if [ $WUIJS = no ] ; then
echo "$MAINCALL = runFormServerWithKey \"$CGIPROG\" \"$CGIKEY\" ($MAIN)" >> $MAINCURRY
else
CGIBASE=`expr $CGIPROG : '\(.*\)\.cgi' \| $CGIPROG`
JSFILE=$CGIBASE\_wui.js
$CPMEXEC $WUIJS_PREPROCESSOR -wui -o $JSFILE $PROG $WUIMODULES
if [ $? != 0 ] ; then
rm -f $MAINCURRY
exit $?
fi
chmod 644 $JSFILE
if [ $CGIDIR != "." ] ; then
mv $JSFILE $CGIDIR/$JSFILE
fi
echo "$MAINCALL = runFormServerWithKeyAndFormParams \"$CGIPROG\" \"$CGIKEY\" [FormJScript \"$JSFILE\",FormOnSubmit \"return submissionAllowed()\"] ($MAIN)" >> $MAINCURRY
fi
# compile main module:
echo "Generating saved state for initial expression: $MAIN"
$CPMEXEC $CURRYROOT/bin/curry --nocypm $CURRYDOPTIONS $CURRYOPTIONS :l $MAINMOD :save $MAINCALL :q
# now the file $MAINMOD should contain the executable computing the HTML form:
if test ! -f $MAINMOD ; then
echo "Error occurred, generation aborted."
$CURRYROOT/bin/cleancurry $MAINMOD
rm -f $MAINMOD.curry
exit 1
fi
# set executable of cgi registry:
CGIREG=curry-cgi
CGIREGISTRY=`which $CGIREG`
if [ ! -x "$CGIREGISTRY" ] ; then
echo "ERROR: '$CGIREG' not found in path! Install it by:"
echo " > cypm install html-cgi"
rm -f $MAINMOD $MAINMOD.curry
exit 1
else
CGIREGISTRY=`realpath $CGIREGISTRY`
fi
# set bin directory containing CPNSD
CPNSD=curry-cpnsd
CPNSDPATH=`which $CPNSD`
if [ -x "$CPNSDPATH" ] ; then
CPNSDBINDIR=`dirname "$CPNSDPATH"`
else
echo "ERROR: '$CPNSD' not found in path! Install it by:"
echo " > cypm install cpns"
rm -f $MAINMOD $MAINMOD.curry
exit 1
fi
# stop old server, if necessary:
if [ -f $CGISERVEREXEC ] ; then
echo "Stop old version of the server '$CGISERVEREXEC'..."
$CGIREGISTRY stopscript "$CGISERVEREXEC"
fi
# copy auxiliary executables if web application should be stand-alone:
if [ $STANDALONE = yes ] ; then
mkdir -p $CGIFILEPATHNAME/html_bin
cp -p "$CGIREGISTRY" $CGIFILEPATHNAME/html_bin/$CGIREG
CGIREGISTRY="html_bin/$CGIREG"
cp -p $CPNSDBINDIR/$CPNSD $CGIFILEPATHNAME/html_bin/$CPNSD
CPNSDBINDIR=$CGIFILEPATHNAME/html_bin
fi
# generate cgi script:
rm -f $CGIFILE
echo "#!/bin/sh" >> $CGIFILE
if test -n "$LANG" ; then
echo "LANG=$LANG" >> $CGIFILE
echo "export LANG" >> $CGIFILE
fi
echo "$CGIREGISTRY submit $SERVERTIMEOUT $LOADBALANCE \"$CGIPROG\" \"$CGIKEY\" \"$CGISERVERCMD\" 2>> $CGIFILE.log" >> $CGIFILE
chmod 755 $CGIFILE
# move compiled executable to final position and generate small shell
# script to call the executable with ulimit and correct path:
mv $MAINMOD $CGISERVEREXEC
chmod 755 $CGISERVEREXEC
echo "#!/bin/sh" > $CGISERVERCMD
if test -n "$ULIMIT" ; then
echo "ulimit $ULIMIT" >> $CGISERVERCMD
fi
echo "PATH=\"\$PATH:$CPNSDBINDIR\"" >> $CGISERVERCMD
echo "exec $CGISERVEREXEC \"\$@\"" >> $CGISERVERCMD
chmod 755 $CGISERVERCMD
$CURRYROOT/bin/cleancurry $MAINMOD
rm -f $MAINMOD.curry
echo "`date`: cgi script compiled" > $CGIFILE.log
echo
echo "New files \"$CGIFILE[.server]\" with compiled cgi script generated."
......@@ -3,25 +3,21 @@
--- to implement dynamic web pages.
---
--- @author Michael Hanus
--- @version October 2017
--- @category web
--- @version November 2018
------------------------------------------------------------------------------
{-# OPTIONS_CYMAKE -Wno-incomplete-patterns #-}
module HTML.CgiServer
module HTML.CGI.Server
( runFormServerWithKey, runFormServerWithKeyAndFormParams
, intForm, intFormMain
) where
import Char ( isSpace )
import Directory (getHomeDirectory)
import Distribution (installDir)
import HTML.Base
import HtmlCgi
import Char ( isSpace )
import Directory ( getHomeDirectory )
import Distribution ( installDir )
import IO
import List ( intercalate )
import NamedSocket
import List ( intercalate )
import Profile
import Random ( getRandomSeed, nextInt )
import ReadNumeric ( readNat )
......@@ -30,6 +26,10 @@ import System
import Time
--import Unsafe(showAnyQExpression) -- to show status of cgi server
import HTML.Base
import HTML.CGI
import Network.NamedSocket
------------------------------------------------------------------------------
--- The server implementing an HTML form (possibly containing input fields).
--- It receives a message containing the environment of the client's
......@@ -494,7 +494,7 @@ cenv2hidden env = concat (map pair2hidden env)
-- association lists (list of tag/value pairs):
-- change an associated value (or add association, if not there):
changeAssoc :: [(tt,tv)] -> tt -> tv -> [(tt,tv)]
changeAssoc :: Eq tt => [(tt,tv)] -> tt -> tv -> [(tt,tv)]
changeAssoc [] tag val = [(tag,val)]
changeAssoc ((tag1,val1):tvs) tag val =
if tag1 == tag then (tag,val) : tvs
......@@ -608,8 +608,8 @@ installShowCgiEnvScript :: String -> String -> IO ()
installShowCgiEnvScript portname cgifile = do
putStrLn ">>> Installing web script..."
putStrLn $ "for port name: "++portname
writeFile cgifile $ "#!/bin/sh\n"++
installDir++"/www/submitform \""++portname++"\"\n"
writeFile cgifile $ "#!/bin/sh\n"++ -- TODO: update location:
installDir ++ "/www/submitform \"" ++ portname ++ "\"\n"
system ("chmod 755 "++cgifile)
done
......@@ -661,11 +661,11 @@ getServerStatus :: ServerState -> IO String
getServerStatus state@(stime,maxkey,_,evs) = do
busy <- getServerLoad state
lstime <- toCalendarTime stime
pinfos <- getProcessInfos
--pinfos <- getProcessInfos -- seems to leads to an error...
return $ "Status: " ++ busy ++ ", Maxkey: "++show maxkey ++ ", #Handlers: " ++
show (length evs) ++ ", Start time: " ++
calendarTimeToString lstime ++ "\n" ++
showMemInfo pinfos
calendarTimeToString lstime ++ "\n"
-- showMemInfo pinfos
--- Shows the group key of a handler as a string.
showGroupKey :: Maybe Int -> String
......
......@@ -25,7 +25,7 @@ import HTML.Base
--- @param categoryFun uses the keys of the items and the keys of the
--- categories to distribute the items among the categories.
--- @return Html containing inner links between the categories
list2CategorizedHtml :: [(a,[HtmlExp])] -> [(b,String)] -> (a -> b -> Bool)
list2CategorizedHtml :: Show b => [(a,[HtmlExp])] -> [(b,String)] -> (a -> b -> Bool)
-> [HtmlExp]
list2CategorizedHtml itemL categoryL categoryFun =
categories2LinkList categoryL :
......@@ -41,7 +41,7 @@ list2CategorizedHtml itemL categoryL categoryFun =
categoryL
-- the navigation list
categories2LinkList :: [(_,String)] -> HtmlExp
categories2LinkList :: Show a => [(a,String)] -> HtmlExp
categories2LinkList categoryL =
par
[center
......
This diff is collapsed.