25 |
--------------------------------------------------------------------------------
/smelt/.gitignore:
--------------------------------------------------------------------------------
1 | .cm
2 | smelt
3 |
--------------------------------------------------------------------------------
/smelt/Makefile:
--------------------------------------------------------------------------------
1 | smelt: ../util/stream.sml tinyxml/nqx.lex tinyxml/tinyxml.sml tinyxml/tinyxml_parser.sig tinyxml/tinyxml_parser.sml templateattr.sig templateattr.sml templategen.sig templategen.sml templatecompiler.sig templatecompiler.sml smelt.sml smelt-standalone.sml
2 | mllex tinyxml/nqx.lex
3 | mlton smelt.mlb
4 |
5 | heap:
6 | ml-build smelt.cm Smelt.main heap
7 |
8 | clean:
9 | rm -rf tinyxml/nqx.lex.sml smelt heap.* .cm tinyxml/.cm
10 |
--------------------------------------------------------------------------------
/smelt/README:
--------------------------------------------------------------------------------
1 | Smelt
2 | -----
3 |
4 | Smelt is an XML templating system that precompiles input documents to SML
5 | code, allowing template processing invocations to be properly typechecked.
6 | The generated code preserializes as much of the XML document tree as possible;
7 | thus, its complexity depends only on the portions being substituted, with all
8 | static content already flattened to plain text. The XML-embedded template is
9 | heavily inspired by Kid and Genshi, two similar tools for Python, but trimmed
10 | down and with some SML-specific features.
11 |
12 | Input documents are parsed as XML. They must have a processing instruction,
13 | before the document element, of the form:
14 |
15 |
16 |
17 | The compiled template code will be a declaration of a structure, with the
18 | specified name, containing only a function "render":
19 |
20 | structure StructureName = struct
21 | val render: input bindings -> Web.HTML
22 | end
23 |
24 | Note that the input bindings are substituted into a function declaration, of
25 | the form "fun render ... = (expression)", so tuples must be parenthesized, and
26 | curried functions can be declared.
27 |
28 | Within the document, two types of processing can be specified. Tree-level
29 | manipulation can be defined with t:... attributes on elements; additionally,
30 | within attribute values and text nodes, ${expressions} can be embedded to
31 | directly insert string content. The compiled code XML-escapes all ${}
32 | substitutions. To insert snippets of prerendered HTML, use $H{expr}; this
33 | inserts a "Web.html" value literally into the output, with no further
34 | processing.
35 |
36 | Any attribute beginning with "t:" must be a valid template attribute. (Smelt
37 | is not yet namespace-aware, so this is not a namespace as such.) The following
38 | attributes are defined:
39 |
40 | - t:if="expr" (expr: bool)
41 |
42 | Only include the tagged element and its children if "expr" is true.
43 |
44 | Example:
Math is broken!
45 |
46 | - t:ifOption="expr as binding" (expr: 'a option)
47 |
48 | Only include the element and its children if "expr" evaluates to SOME 'a.
49 | The binding is used in a pattern-match, "case expr of SOME binding => ...";
50 | within child nodes, all bound variables will be available.
51 | If no " as " is present in the parameter value, the while string will be
52 | used for both expr and binding: "case s of SOME s => ...".
53 |
54 | Example:
Hello, ${name}!
55 |
56 | - t:for="binding in expr" (expr: 'a list)
57 |
58 | Substitute this node and all its children repeated for each value in expr,
59 | with binding matched to each element in turn.
60 |
61 | Example:
${i}
62 |
63 | - t:strip="expr"
64 |
65 | "Strip" the element (meaning, include only its child elements in-place) if
66 | expr evaluates to true. As a shortcut, a blank expr is considered to always
67 | be true.
68 |
69 | - t:case="expr", t:of="binding"
70 |
71 | These two must be used together; a t:case element contains a number of
72 | child elements, each of which must contain a t:of attribute. Any text nodes
73 | that are immediate children of a t:case will be ignored if they contain only
74 | whitespace; it is an error if non-whitespace characters are found. They map
75 | as expected to a case/of expression.
76 |
77 | Example:
Greater
...
78 |
79 | For compatibility with XML's unordered attribtes, t: attributes are always
80 | processed in the following order ("outer" to "inner"), rather than in the order
81 | in which they appear in the program's source.
82 |
83 | t:of, t:for, t:ifOption, t:if, t:strip, t:case
84 |
85 | See "example.html" for some sample template code.
86 |
87 |
--------------------------------------------------------------------------------
/smelt/cm.sml:
--------------------------------------------------------------------------------
1 | structure SmeltTool = struct
2 |
3 | fun smeltRule { spec as { name, mkpath, class, opts, derived },
4 | native2pathmaker, context, defaultClassOf, sysinfo } =
5 | let
6 | val srcpath = Tools.srcpath (mkpath ())
7 | val srcFile = (Tools.nativeSpec srcpath)
8 | val outputFile = srcFile ^ ".sml"
9 |
10 | val partial_expansion = (
11 | { smlfiles = nil,
12 | cmfiles = nil,
13 | sources = [ (srcpath, { class = "smelt", derived = false }) ] },
14 | [ { name = outputFile : string,
15 | mkpath = native2pathmaker outputFile,
16 | class = SOME "sml",
17 | opts = NONE,
18 | derived = true } ]
19 | )
20 |
21 | fun rulefun () = (
22 | if Tools.outdated "smelt" ([ outputFile ], srcFile)
23 | then (Tools.vsay [ "[smelt: compiling ", srcFile, "]\n" ];
24 | Smelt.process_file srcFile)
25 | else ();
26 | partial_expansion
27 | )
28 | in
29 | context rulefun
30 | end
31 |
32 | val _ = Tools.registerClass ("smelt", smeltRule)
33 |
34 | val _ = Tools.registerClassifier (Tools.SFX_CLASSIFIER (fn "html" => SOME "smelt"
35 | | "smelt" => SOME "smelt"
36 | | _ => NONE))
37 |
38 | end
39 |
--------------------------------------------------------------------------------
/smelt/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | hello, ${adjective} world!
4 | classy
5 |
6 |
${i}
7 |
8 |
9 |
10 |
No elements in frob
11 |
Elements in frob
12 |
13 |
14 |
--------------------------------------------------------------------------------
/smelt/smelt-standalone.sml:
--------------------------------------------------------------------------------
1 | val _ = OS.Process.exit (Smelt.main ());
2 |
--------------------------------------------------------------------------------
/smelt/smelt-tool.cm:
--------------------------------------------------------------------------------
1 | Group is
2 | $smlnj/cm/tools.cm
3 | smelt.cm
4 | cm.sml
5 |
--------------------------------------------------------------------------------
/smelt/smelt.cm:
--------------------------------------------------------------------------------
1 | Group is
2 | $/basis.cm
3 | $/smlnj-lib.cm
4 | tinyxml/tinyxml.cm
5 | templateattr.sig
6 | templateattr.sml
7 | templategen.sig
8 | templategen.sml
9 | templatecompiler.sig
10 | templatecompiler.sml
11 | smelt.sml
12 |
--------------------------------------------------------------------------------
/smelt/smelt.mk:
--------------------------------------------------------------------------------
1 | SMELT_DEPS = $(shell $(MLTON) -stop f $(SMELT_PATH)/smelt.mlb)
2 |
3 | %.grm.sig %.grm.sml: %.grm
4 | mlyacc $<
5 |
6 | %.lex.sml: %.lex
7 | mllex $<
8 |
9 | $(SMELT_PATH)/smelt: $(SMELT_DEPS)
10 | $(MLTON) $(SMELT_PATH)/smelt.mlb
11 |
12 | %.html.sml: %.html $(SMELT_PATH)/smelt
13 | $(SMELT_PATH)/smelt $<
14 |
--------------------------------------------------------------------------------
/smelt/smelt.mlb:
--------------------------------------------------------------------------------
1 | $(SML_LIB)/basis/basis.mlb
2 | $(SML_LIB)/smlnj-lib/Util/lib-base-sig.sml
3 | $(SML_LIB)/smlnj-lib/Util/lib-base.sml
4 | $(SML_LIB)/smlnj-lib/Util/listsort-sig.sml
5 | $(SML_LIB)/smlnj-lib/Util/list-mergesort.sml
6 | ../util/stream.sml
7 | ../util/rope.sig
8 | ../util/rope.sml
9 | tinyxml/nqx.lex.sml
10 | tinyxml/tinyxml.sml
11 | tinyxml/tinyxml_parser.sig
12 | tinyxml/tinyxml_parser.sml
13 | templateattr.sig
14 | templateattr.sml
15 | templategen.sig
16 | templategen.sml
17 | templatecompiler.sig
18 | templatecompiler.sml
19 | smelt.sml
20 | smelt-standalone.sml
21 |
--------------------------------------------------------------------------------
/smelt/smelt.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Smelt wrapper script.
4 | dn="`dirname $0`"
5 |
6 | if [ -e "$dn/smelt" ]
7 | then
8 | exec "$dn/smelt" $*
9 | fi
10 |
11 | sml @SMLcmdname=$0 "@SMLload=$dn/heap" $*
12 |
--------------------------------------------------------------------------------
/smelt/smelt.sml:
--------------------------------------------------------------------------------
1 | structure Smelt = struct
2 |
3 | structure XP = TinyXMLParser
4 | structure TC = TemplateCompiler
5 | structure TG = TemplateGenerator
6 |
7 | fun err msg = TextIO.output (TextIO.stdErr, String.concat msg)
8 |
9 | fun process_file filename = let
10 | val template = XP.parseFile filename
11 | val result = (TC.compile o TG.generate) template
12 | val outfile = TextIO.openOut (filename ^ ".sml")
13 | in
14 | TextIO.output (outfile, result);
15 | TextIO.closeOut outfile
16 | end
17 |
18 | fun main _ = (case CommandLine.arguments () of
19 | [ filename ] => (process_file filename; OS.Process.success)
20 | | _ => (print ("Usage: " ^ (CommandLine.name ())
21 | ^ " templatefile\n");
22 | OS.Process.failure
23 | ))
24 | handle e => (err [ CommandLine.name(), ": uncaught exception ",
25 | General.exnMessage e, "\n"];
26 | OS.Process.failure)
27 |
28 | end
29 |
--------------------------------------------------------------------------------
/smelt/templateattr.sig:
--------------------------------------------------------------------------------
1 | signature TEMPLATE_ATTR =
2 | sig
3 |
4 | type in_attr = TinyXML.attribute
5 | exception InvalidAttribute of in_attr
6 |
7 | type expr = string
8 |
9 | datatype attr = TAFor of expr * expr
10 | | TAIf of expr
11 | | TAIfOption of expr * expr
12 | | TAStrip of expr
13 | | TACase of expr
14 | | TAOf of expr
15 |
16 | val process: in_attr list -> (attr list * in_attr list)
17 |
18 | val separateOf: in_attr list -> (expr * in_attr list) option
19 |
20 | end
21 |
--------------------------------------------------------------------------------
/smelt/templateattr.sml:
--------------------------------------------------------------------------------
1 | structure TemplateAttr :> TEMPLATE_ATTR = struct
2 |
3 | (* Attribute parsing. *)
4 |
5 | type in_attr = TinyXML.attribute
6 | exception InvalidAttribute of in_attr
7 |
8 | type expr = string
9 |
10 | datatype attr = TAFor of expr * expr
11 | | TAIf of expr
12 | | TAIfOption of expr * expr
13 | | TAStrip of expr
14 | | TACase of expr
15 | | TAOf of expr
16 |
17 |
18 | (* val isBefore: (attr * attr) -> bool
19 | *
20 | * Provide an ordering on template attributes.
21 | *)
22 | fun isBefore (a, b) = let
23 | fun ordering (TAOf _) = 6
24 | | ordering (TAFor _) = 5
25 | | ordering (TAIfOption _) = 4
26 | | ordering (TAIf _) = 3
27 | | ordering (TAStrip _) = 2
28 | | ordering (TACase _) = 1
29 | in
30 | ordering a < ordering b
31 | end
32 |
33 |
34 | (* val fromKV: in_attr -> attr
35 | *
36 | * Convert an XML attribute to a template attribute (type attr) value.
37 | *)
38 | fun fromKV (k as "t:for", v) = let
39 | val (fst, rest) = Substring.position " in " (Substring.full v)
40 | in
41 | if Substring.isPrefix " in " rest
42 | then TAFor (Substring.string fst,
43 | Substring.string (Substring.triml 4 rest))
44 | else raise InvalidAttribute (k, v)
45 | end
46 | | fromKV ("t:if", v) = TAIf v
47 | | fromKV ("t:ifOption", v) = let
48 | val (fst, rest) = Substring.position " as " (Substring.full v)
49 | in
50 | if Substring.isPrefix " as " rest
51 | then TAIfOption (Substring.string fst,
52 | Substring.string (Substring.triml 4 rest))
53 | else TAIfOption (v, v)
54 | end
55 | | fromKV ("t:case", v) = TACase v
56 | | fromKV ("t:of", v) = TAOf v
57 | | fromKV ("t:strip", v) = TAStrip v
58 | | fromKV (k, v) = raise InvalidAttribute (k, v)
59 |
60 |
61 | (* val orderedFromKV: in_attr list -> attr list
62 | *
63 | * Convert an (arbitrarily-ordered) list of XML attributes to an ordered
64 | * list of template attributes.
65 | *)
66 | fun orderedFromKV list = ListMergeSort.sort isBefore (map fromKV list)
67 |
68 |
69 | (* val process: in_attr list -> (attr list * in_attr list)
70 | *
71 | * Process a list of XML attributes, separating the template-language ones
72 | * from others, and then parse the template-language attributes.
73 |
74 | * If an unrecognized "t:" attribute is encoutered, this will raise an
75 | * InvalidAttribute exception with the attribute in question.
76 | *)
77 | fun process' accT accO nil = (orderedFromKV accT, accO)
78 | | process' accT accO ((k, v)::rest) = if String.isPrefix "t:" k
79 | then process' ((k, v)::accT) accO rest
80 | else process' accT ((k, v)::accO) rest
81 | val process = process' nil nil
82 |
83 |
84 | (* val separateOf: in_attr list -> (expr * in_attr list)
85 | *
86 | * Scan through the input list searching for a t:of attribute. If found,
87 | * return SOME (t:of value, other attributes); otherwise return NONE.
88 | *)
89 | fun separateOf' acc nil = NONE
90 | | separateOf' acc (("t:of", v:string)::rest) = SOME (v, acc @ rest)
91 | | separateOf' acc ((k, v)::rest) = separateOf' ((k, v)::acc) rest
92 | val separateOf = separateOf' nil
93 |
94 | end
95 |
--------------------------------------------------------------------------------
/smelt/templatecompiler.sig:
--------------------------------------------------------------------------------
1 | signature TEMPLATE_COMPILER =
2 | sig
3 |
4 | val compile: string * string * TemplateGenerator.gen -> string
5 |
6 | end
7 |
--------------------------------------------------------------------------------
/smelt/templatecompiler.sml:
--------------------------------------------------------------------------------
1 | structure TemplateCompiler :> TEMPLATE_COMPILER =
2 | struct
3 | structure TG = TemplateGenerator
4 |
5 | (* val mix: 'a -> 'a list -> 'a list
6 | *
7 | * Intersperse the first parameter between each element in the second list:
8 | * mix 0 [ 1, 2, 3 ] ==> [ 1, 0, 2, 0, 3 ]
9 | *)
10 | fun mix _ nil = nil
11 | | mix _ (a::nil) = a :: nil
12 | | mix sep (a::rest) = a :: sep :: mix sep rest
13 |
14 |
15 | (* val compileGen: gen -> string
16 | *
17 | * Compile gen to ML code. All keys in TG.GenSubst nodes should exist in the
18 | * surrounding environment.
19 | *)
20 | fun compileGen (TG.GenText t) = "\"" ^ String.toString t ^ "\""
21 | | compileGen (TG.GenSubst (cvt, v)) = cvt ^ "(" ^ v ^ ")"
22 | | compileGen (TG.GenConcat gens) = String.concat [
23 | "String.concat[",
24 | String.concat (mix "," (map compileGen gens)),
25 | "]" ]
26 | | compileGen (TG.GenIterate (var, src, tree, sep)) = String.concat [
27 | case sep of "" => "String.concat"
28 | | _ => "String.concatWith" ^ compileGen (TG.GenText sep),
29 | "(map(fn ", var, "=>",
30 | compileGen tree,
31 | ")(", src, "))" ]
32 | | compileGen (TG.GenCaseOf (var, terms)) = String.concat [
33 | "case ",
34 | var,
35 | " of",
36 | String.concat (mix "|" (map (fn (exp, gen) => String.concat [
37 | "(", exp, ")=>(",
38 | compileGen gen,
39 | ")" ]
40 | ) terms)) ]
41 |
42 |
43 | (* val compile: string * string * gen -> string
44 | *
45 | * Compile gen to ML code. The first arugment is the name of the structure
46 | * to produce; the second is the type of arguments expected by gen.
47 | *)
48 | fun compile (sname, itype, gen) = String.concat [
49 | "structure ",
50 | sname,
51 | " = struct fun render ",
52 | itype,
53 | "=Web.HTML(",
54 | compileGen gen,
55 | ") end"
56 | ]
57 |
58 | end
59 |
--------------------------------------------------------------------------------
/smelt/templategen.sig:
--------------------------------------------------------------------------------
1 | signature TEMPLATE_GENERATOR =
2 | sig
3 | exception ParseError of string
4 |
5 | type expr = string
6 | datatype gen = GenText of string
7 | | GenSubst of string * expr
8 | | GenConcat of gen list
9 | | GenIterate of expr * expr * gen * string
10 | | GenCaseOf of expr * (expr * gen) list
11 |
12 | val generate: TinyXML.document -> string * string * gen
13 |
14 |
15 | val optimizeGen: gen -> gen
16 | end
17 |
--------------------------------------------------------------------------------
/smelt/tinyxml/.gitignore:
--------------------------------------------------------------------------------
1 | nqx.lex.sml
2 |
--------------------------------------------------------------------------------
/smelt/tinyxml/nqx.lex:
--------------------------------------------------------------------------------
1 | datatype lexresult =
2 | DATA of string
3 | | BEGINTAG of string
4 | | TAGEND
5 | | TAGSELFCLOSE
6 | | ATTRIB of string
7 | | ATTRIBVALUE of string
8 | | CLOSETAG of string
9 | | PI of string
10 | | EOF
11 |
12 | fun eof () = EOF
13 |
14 | %%
15 |
16 | %s MAIN TAG TAGBEGIN ATTNAME ATTVALUE ATTVALUEDONE DQUOT SQUOT CTAGBEGIN PIBEGIN PIDATA PIEND ;
17 |
18 | space = ([\t\n] | " ");
19 | identchar = [A-Za-z_0-9:-];
20 | pcchar = [^<];
21 |
22 | %%
23 |
24 | {space}* => (YYBEGIN MAIN; lex());
25 | {pcchar}+ => (DATA yytext);
26 | "<" => ( YYBEGIN TAGBEGIN; continue() );
27 | "" => ( YYBEGIN CTAGBEGIN; continue() );
28 | "" => ( YYBEGIN PIBEGIN; continue() );
29 |
30 | {identchar}+ => ( YYBEGIN TAG; BEGINTAG yytext);
31 |
32 | {space}+ => ( continue() );
33 | {identchar}+ => ( YYBEGIN ATTNAME; ATTRIB yytext);
34 | ">" => ( YYBEGIN MAIN; TAGEND );
35 | "/>" => ( YYBEGIN MAIN; TAGSELFCLOSE );
36 |
37 | "=" => ( YYBEGIN ATTVALUE; continue () );
38 | "\"" => ( YYBEGIN DQUOT; continue () );
39 | "'" => ( YYBEGIN SQUOT; continue () );
40 | [^"]* => ( YYBEGIN ATTVALUEDONE; ATTRIBVALUE yytext );
41 | [^']* => ( YYBEGIN ATTVALUEDONE; ATTRIBVALUE yytext );
42 | "\"" => ( YYBEGIN TAG; continue() );
43 | "'" => ( YYBEGIN TAG; continue() );
44 |
45 | {identchar}+ => ( YYBEGIN TAG; CLOSETAG yytext );
46 |
47 | {identchar}+ => ( YYBEGIN PIDATA; PI yytext );
48 | {space} => ( continue() );
49 | [^\ ] [^?]* => ( YYBEGIN PIEND; DATA yytext (* XXX fix this *) );
50 | "?>" => ( YYBEGIN MAIN; continue() );
51 |
--------------------------------------------------------------------------------
/smelt/tinyxml/tinyxml.cm:
--------------------------------------------------------------------------------
1 | Group is
2 | $/basis.cm
3 | ../../util/rope.cm
4 | ../../util/stream.sml
5 | nqx.lex
6 | tinyxml.sml
7 | tinyxml_parser.sig
8 | tinyxml_parser.sml
9 |
--------------------------------------------------------------------------------
/smelt/tinyxml/tinyxml.mlb:
--------------------------------------------------------------------------------
1 | $(SML_LIB)/basis/basis.mlb
2 | ../../util/rope.sig
3 | ../../util/rope.sml
4 | ../../util/stream.sml
5 | nqx.lex.sml
6 | tinyxml.sml
7 | tinyxml_parser.sig
8 | tinyxml_parser.sml
9 |
--------------------------------------------------------------------------------
/smelt/tinyxml/tinyxml.sml:
--------------------------------------------------------------------------------
1 | structure TinyXML =
2 | struct
3 | type nodename = string
4 | type attribute = string * string
5 |
6 | type dtd = string
7 |
8 | datatype document = XDocument of dtd option * procinst list * element
9 | and element = XElement of nodename * attribute list * node list
10 | and procinst = XProcInst of string * string
11 | and node = XElementNode of element
12 | | XTextNode of string
13 | | XPINode of procinst
14 |
15 | fun escape t = String.translate (fn #"<" => "<"
16 | | #"&" => "&"
17 | | #"\"" => """
18 | | c => String.str c) t
19 |
20 | fun serialize (XDocument (_, _, root)) =
21 | let
22 | fun serializeAttr (key, value) = Rope.fromStrings([ " ", key, "=\"", (escape value), "\"" ])
23 |
24 | fun serializeNode (XTextNode t) = Rope.fromString (escape t)
25 | | serializeNode (XElementNode (XElement (tag, attrs, nodes))) =
26 | Rope.fromRopes [
27 | Rope.fromString ("<" ^ tag),
28 | Rope.fromRopes (map serializeAttr attrs),
29 | Rope.fromString ">",
30 | Rope.fromRopes (map serializeNode nodes),
31 | Rope.fromString ("" ^ tag ^ ">")
32 | ]
33 | | serializeNode (XPINode (XProcInst (k, v))) =
34 | Rope.fromString ("" ^ (escape k) ^ (escape v) ^ "?>")
35 | in
36 | Rope.toString (serializeNode (XElementNode root))
37 | end
38 |
39 | end
40 |
--------------------------------------------------------------------------------
/smelt/tinyxml/tinyxml_parser.sig:
--------------------------------------------------------------------------------
1 | signature TINYXML_PARSER =
2 | sig
3 |
4 | exception ParseError
5 |
6 | val parseFile: string -> TinyXML.document
7 |
8 | end
9 |
--------------------------------------------------------------------------------
/util/.gitignore:
--------------------------------------------------------------------------------
1 | .cm
2 |
--------------------------------------------------------------------------------
/util/gc-mlton.sml:
--------------------------------------------------------------------------------
1 | structure GC = struct
2 |
3 | val collectAll = MLton.GC.collect
4 |
5 | end
6 |
--------------------------------------------------------------------------------
/util/gc-smlnj.sml:
--------------------------------------------------------------------------------
1 | structure GC = struct
2 |
3 | fun collectAll () = SMLofNJ.Internals.GC.doGC 3
4 |
5 | end
6 |
--------------------------------------------------------------------------------
/util/linereader.sml:
--------------------------------------------------------------------------------
1 | functor LineReader (S: SOCKET) :> sig
2 |
3 | type reader
4 | val new: (INetSock.inet, S.active S.stream) S.sock * { increment: int,
5 | stripCR: bool } -> reader
6 |
7 | val readline: reader -> Word8Vector.vector
8 |
9 | val readbytes: reader -> int -> Word8Vector.vector
10 |
11 | end = struct
12 |
13 | structure W8V = Word8Vector
14 | structure W8VS = Word8VectorSlice
15 |
16 | val emptyVec = W8V.fromList nil
17 |
18 | type config = { increment: int,
19 | stripCR: bool }
20 |
21 | type reader = (INetSock.inet, S.active S.stream) S.sock
22 | * config
23 | * (W8VS.slice list * int) ref
24 |
25 | fun new (sock, config) = (sock, config, ref (nil, 0))
26 |
27 | val cr = 0wx0D: Word8.word
28 |
29 | fun isNL (_, 0wx0A: Word8.word) = true
30 | | isNL (_, _) = false
31 |
32 | fun readline (reader as (_, _, ref (nil, _))) = getMore reader readline
33 | | readline (reader as (sock, { increment, stripCR },
34 | buf as (ref (head::backlog, len)))) = (
35 | case W8VS.findi isNL head of
36 | NONE => getMore reader readline
37 | | SOME (pos, _) => let
38 | val pos' = if stripCR andalso pos > 0
39 | then (case W8VS.sub (head, pos - 1) of
40 | 0wx0D => pos - 1
41 | | _ => pos)
42 | else pos
43 |
44 | val headslice = W8VS.subslice (head, 0, SOME pos')
45 |
46 | val rest = case (backlog, pos, stripCR) of
47 | (blh::rest, 0, true) => let
48 | val blhl = W8VS.length blh
49 | val blh' = if W8VS.sub (blh, blhl - 1) = cr
50 | then W8VS.subslice (blh, 0, SOME (blhl - 1))
51 | else blh
52 | in
53 | blh' :: rest
54 | end
55 | | _ => backlog
56 |
57 | val newData = W8VS.subslice (head, pos + 1, NONE)
58 | val () = buf := (newData :: nil, W8VS.length newData)
59 | in
60 | W8VS.concat (rev (headslice :: rest))
61 | end
62 | )
63 |
64 | and readbytes (reader as (sock, _, buf as (ref (slices, dlen)))) len =
65 | if len > dlen then getMore reader (fn r => readbytes r len)
66 | else case slices of
67 | nil => emptyVec
68 | | head :: rest => let
69 | val brkpt = W8VS.length head - (dlen - len)
70 | val newData = W8VS.subslice (head, brkpt, NONE)
71 | val () = buf := (newData :: nil, W8VS.length newData)
72 | in
73 | W8VS.concat (rev (W8VS.subslice (head, 0, SOME brkpt)::rest))
74 | end
75 |
76 | and getMore (sock, config as { increment, stripCR }, sliceref) cont = let
77 | val (slices, len) = !sliceref
78 | val vec' = S.recvVec (sock, increment)
79 | val newLen = W8V.length vec'
80 | in
81 | if newLen = 0 then emptyVec
82 | else (sliceref := (W8VS.full vec' :: slices, len + newLen);
83 | cont (sock, config, sliceref))
84 | end
85 |
86 | end
87 |
--------------------------------------------------------------------------------
/util/rope.cm:
--------------------------------------------------------------------------------
1 | Group is
2 | $/basis.cm
3 | rope.sml
4 | rope.sig
5 |
--------------------------------------------------------------------------------
/util/rope.sig:
--------------------------------------------------------------------------------
1 | signature ROPE = sig
2 |
3 | eqtype rope
4 | val fromString: string -> rope
5 | val fromStrings: string list -> rope
6 | val fromRopes: rope list -> rope
7 |
8 | val toString: rope -> string
9 |
10 | end
11 |
--------------------------------------------------------------------------------
/util/rope.sml:
--------------------------------------------------------------------------------
1 | structure Rope :> ROPE =
2 | struct
3 |
4 | datatype rope = RString of string
5 | | RList of rope list
6 |
7 | val fromString = RString
8 | fun fromStrings sl = RList(map RString sl)
9 | val fromRopes = RList
10 |
11 | fun toString (RString(s)) = s
12 | | toString (RList(rl)) = String.concat (map toString rl)
13 |
14 | end
15 |
--------------------------------------------------------------------------------
/util/timer.sml:
--------------------------------------------------------------------------------
1 | structure PrettyTimer :> sig
2 |
3 | type timer
4 |
5 | val start: unit -> timer
6 | val print: timer -> string
7 |
8 | end = struct
9 |
10 | type timer = Timer.real_timer * Timer.cpu_timer
11 |
12 | fun start () = (Timer.startRealTimer (), Timer.startCPUTimer ())
13 |
14 | fun print (real, cpu) = let
15 | val realTime = Timer.checkRealTimer real
16 | val { nongc, gc } = Timer.checkCPUTimes cpu
17 | fun ms time = Real.toString ((Time.toReal time) * 1000.0)
18 | in
19 | String.concat [
20 | ms realTime, " ms total, ", ms (#usr nongc), "+", ms (#usr gc),
21 | " ms user, ", ms (#sys nongc), "+", ms (#sys gc), " ms system"
22 | ]
23 | end
24 |
25 | end
26 |
--------------------------------------------------------------------------------
/util/wrapper.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | int main(int argc, char ** argv) {
6 | int devnull;
7 |
8 | if (argc < 2) {
9 | printf("Usage: %s command [args...]\n", argv[0]);
10 | return 1;
11 | }
12 |
13 | devnull = open("/dev/null", O_WRONLY);
14 |
15 | if (devnull < 0) {
16 | perror("open");
17 | return 1;
18 | }
19 |
20 | if (devnull != 1) dup2(devnull, 1);
21 | if (devnull != 2) dup2(devnull, 2);
22 | if (devnull != 1 && devnull != 2) close(devnull);
23 |
24 | execvp(argv[1], argv + 1);
25 |
26 | return 1;
27 | }
28 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | .cm
2 |
--------------------------------------------------------------------------------
/web/README:
--------------------------------------------------------------------------------
1 | Stilts
2 | ------
3 |
4 | This provides the core of the Stilts web infrastructure. The following modues
5 | are defined:
6 |
7 | - Web
8 |
9 | Core web request types.
10 |
11 | The html datatype is provided to allow some type safety when dealing with
12 | both strings, which may contain any data and must not be passed directly to
13 | a browser, and HTML snippets, which are only generated by certain pieces of
14 | "safe" code.
15 |
16 | Since one may want to return content of any type (even binary data),
17 | the response type is defined using a Word8Vector.vector rather than a string
18 | or HTML datatype.
19 |
20 | - WebUtil / WEB_UTIL
21 |
22 | Useful functions for processing Web requests and responses.
23 |
24 | This contains a large collection of helper functions; they are documented
25 | indiviually in web-util.sml.
26 |
27 |
28 | - Form / FORM
29 |
30 | HTTP form handling; extracting variables from GET and POST requests.
31 |
32 | An HTTP form does not quite correspond to a mapping between values in the
33 | usual sense. Input elements are generally named uniquely, but some types of
34 | control (checkboxes, multi-select boxes) will produce multiple values for the
35 | same key; this results in encodings like key=value1&key=value2&...
36 |
37 | However, most of the time, one is only interested in one value (if any) for
38 | a key. The Form structure implements this by defining a form as a mapping from
39 | a string key to a _list_ of values, and providing helper functions to return
40 | the last-specifed value (if any) as well as all values. For flexibility, the
41 | backing ORD_MAP structure is also made available.
42 |
43 | -----------------------------------------
44 | Notes on the prepath/postpath abstraction
45 | -----------------------------------------
46 |
47 | Web.request records carry two pieces of path information, the "prepath" and
48 | "postpath". The concatenation of the two represents the full incoming path
49 | for the request without a leading slash. The division between the two sections
50 | represents the division between paths in the URL hierarchy "leading two" an
51 | application versus those "within" the application. The precise semantics of
52 | "application" here are intentionally somewhat loose.
53 |
54 | For example, one could use WebUtil.dispatch to build a hierarchy of what some
55 | frameworks refer to as controllers. Alternately, an app could pattern-match
56 | on the whole of its postpath, and not have to worry about where it is available
57 | on the actual Web server.
58 |
59 | At the Stilts <-> Web server boundary, prepath and postpath correspond
60 | roughly to the CGI SCRIPT_NAME and PATH_INFO variables, respectively. However,
61 | servers are not entirely consistent in how they provide these; the interface
62 | needs some robustness work.
63 |
64 |
--------------------------------------------------------------------------------
/web/form.sig:
--------------------------------------------------------------------------------
1 | signature FORM = sig
2 |
3 | structure Map : ORD_MAP where type Key.ord_key = string
4 | type form = string list Map.map
5 |
6 | val load: Web.request -> form
7 |
8 | val get: form -> string -> string option
9 | val getAll: form -> string -> string list
10 |
11 | val export: form -> string
12 | val import: string -> form
13 |
14 | val dump: form -> string
15 |
16 | end
17 |
--------------------------------------------------------------------------------
/web/form.sml:
--------------------------------------------------------------------------------
1 | structure Form : FORM = struct
2 |
3 | (* Types for FORM signature. *)
4 |
5 | structure Map = RedBlackMapFn (type ord_key = string
6 | val compare = String.compare)
7 |
8 | type form = string list Map.map
9 |
10 |
11 | (* val parseVars: string -> (string * string) list
12 | *
13 | * Parse out a query string (key=value&key=value&...) into a set of pairs
14 | * of (key, value). All values are URL-decoded.
15 | *)
16 | fun parseVars qstring =
17 | List.mapPartial (fn field =>
18 | let
19 | val (k, v) = Substring.splitl (fn c => c <> #"=") field
20 | in
21 | SOME (Substring.string k,
22 | WebUtil.urldecode (Substring.string (Substring.slice (v, 1, NONE))))
23 | end
24 | handle Subscript => NONE
25 | ) (Substring.fields (fn c => c = #"&") (Substring.full qstring))
26 |
27 |
28 | (* val add_value: (string * string) * form -> form
29 | *
30 | * Insert the given value at the beginning of the values for the given key.
31 | *)
32 | fun add_value ((k, v), form) = let
33 | val existing = case Map.find (form, k) of NONE => nil | SOME vs => vs
34 | in
35 | Map.insert (form, k, v::existing)
36 | end
37 |
38 |
39 | (* val import: string -> form
40 | *
41 | * Builds a form from a query string.
42 | *)
43 | fun import str = foldl add_value Map.empty (parseVars str)
44 |
45 |
46 | (* val export: form -> string
47 | *
48 | * Exports a form into a query string format suitable for use by the "import"
49 | * function. May not properly preserve the ordering of keys with multiple
50 | * values.
51 | *)
52 | fun export form = String.concatWith "&" (
53 | map (fn (k, vs) =>
54 | String.concatWith "&" (
55 | map (fn v => k ^ "=" ^ (WebUtil.urlencode v)) vs
56 | )
57 | ) (Map.listItemsi form)
58 | )
59 |
60 |
61 | (* val load: Web.request -> form
62 | *
63 | * Parse all form variables out of a request.
64 | *
65 | * If the request is GET, only the query string is parsed; if it is a POST
66 | * of application/x-www-form-urlencoded data, that content is loaded as well.
67 | *)
68 | fun load (req: Web.request) = let
69 | val form = import (#query_string req)
70 | val content_type = case WebUtil.server_header "CONTENT_TYPE" req of
71 | SOME ct => SOME ct
72 | | NONE => WebUtil.http_header "HTTP_CONTENT_TYPE" req
73 | val postVars = case (#method req, content_type) of
74 | ("POST", SOME "application/x-www-form-urlencoded") =>
75 | parseVars (Byte.bytesToString (#content req ()))
76 | | _ => nil
77 | in
78 | foldl add_value form postVars
79 | end
80 |
81 |
82 | (* val get: form -> string -> string option
83 | *
84 | * Return the most-recenty-specified value for the given key in the form,
85 | * if any.
86 | *)
87 | fun get f k = case Map.find (f, k) of NONE => NONE
88 | | SOME nil => NONE
89 | | SOME (v::vs) => SOME v
90 |
91 |
92 | (* val getAll: form -> string -> string list
93 | *
94 | * Return all values for the given key in the form, in the order specified.
95 | *)
96 | fun getAll f k = case Map.find (f, k) of NONE => []
97 | | SOME vs => rev vs
98 |
99 |
100 | (* val dump: form -> string
101 | *
102 | * Return a multiline string of all keys and values in the form which is
103 | * nominally human-readable and useful for debugging.
104 | *)
105 | fun dump form = String.concat (
106 | map (fn (k, vs) =>
107 | "- \"" ^ k ^ "\": "
108 | ^ (String.concatWith ", " (map (fn v => "\""^v^"\"") vs))
109 | ^ "\n"
110 | ) (Map.listItemsi form)
111 | )
112 | end
113 |
--------------------------------------------------------------------------------
/web/http-date.sml:
--------------------------------------------------------------------------------
1 | structure HTTPDate :> sig
2 |
3 | (* Format dates in HTTP format.
4 | *
5 | * We cannot use the Date.fmt function in the basis library for this, because
6 | * it uses the current locale (and the current locale's names for days of
7 | * week and months), rather than the universal RFC 1123 date format.
8 | *)
9 |
10 | val format: Date.date -> string
11 |
12 | end = struct
13 |
14 | structure D = Date
15 | val format_wd = fn D.Mon => "Mon" | D.Tue => "Tue" | D.Wed => "Wed"
16 | | D.Thu => "Thu" | D.Fri => "Fri" | D.Sat => "Sat"
17 | | D.Sun => "Sun"
18 |
19 | val format_mon = fn D.Jan => "Jan" | D.Feb => "Feb" | D.Mar => "Mar"
20 | | D.Apr => "Apr" | D.May => "May" | D.Jun => "Jun"
21 | | D.Jul => "Jul" | D.Aug => "Aug" | D.Sep => "Sep"
22 | | D.Oct => "Oct" | D.Nov => "Nov" | D.Dec => "Dec"
23 |
24 | fun format date = let
25 | fun lz2 num = let val str = Int.toString num
26 | in case size str of 1 => "0" ^ str | _ => str end
27 | in
28 | String.concat [
29 | format_wd (D.weekDay date), ", ", lz2 (D.day date), " ",
30 | format_mon (D.month date), " ", Int.toString (D.year date), " ",
31 | lz2 (D.hour date), ":", lz2 (D.minute date), ":",
32 | lz2 (D.second date), " GMT" ]
33 | end
34 |
35 | end
36 |
37 |
--------------------------------------------------------------------------------
/web/pack-compat.sml:
--------------------------------------------------------------------------------
1 | structure PackWord16Little = Pack16Little
2 | structure PackWord32Little = Pack32Little
3 | structure PackWord16Big = Pack16Big
4 | structure PackWord32Big = Pack32Big
5 |
--------------------------------------------------------------------------------
/web/server/.gitignore:
--------------------------------------------------------------------------------
1 | .cm
2 |
--------------------------------------------------------------------------------
/web/server/cgi.sml:
--------------------------------------------------------------------------------
1 | structure CGI = struct
2 |
3 | exception ProtocolError
4 |
5 | (* val make_request: (string * string) list * int * (unit -> string)
6 | * -> Web.request
7 | *
8 | * Assemble CGI headers and a body reader function into a Web.request record.
9 | *)
10 | fun make_request (headers, content_length, content_reader) : Web.request =
11 | let
12 | (* Loop through all the headers and sort them out: HTTP, server, or
13 | * specially-handled. *)
14 |
15 | val remote_addr = ref ""
16 | val remote_port = ref ""
17 | val request_method = ref ""
18 | val script_name = ref ""
19 | val path_info = ref ""
20 | val query_string = ref ""
21 | val document_root = ref ""
22 | val server_addr = ref ""
23 | val server_name = ref ""
24 | val server_port = ref ""
25 |
26 | val process_key = (fn
27 | (("REMOTE_ADDR", v), acc) => (remote_addr := v; acc)
28 | | (("REMOTE_PORT", v), acc) => (remote_port := v; acc)
29 | | (("REQUEST_METHOD", v), acc) => (request_method := v; acc)
30 | | (("SCRIPT_NAME", v), acc) => (script_name := v; acc)
31 | | (("PATH_INFO", v), acc) => (path_info := v; acc)
32 | | (("QUERY_STRING", v), acc) => (query_string := v; acc)
33 | | (("DOCUMENT_ROOT", v), acc) => (document_root := v; acc)
34 | | (("SERVER_ADDR", v), acc) => (server_addr := v; acc)
35 | | (("SERVER_NAME", v), acc) => (server_name := v; acc)
36 | | (("SERVER_PORT", v), acc) => (server_port := v; acc)
37 | | ((k, v), (http_headers, other_headers)) =>
38 | if String.isPrefix "HTTP_" k
39 | then (((k, v)::http_headers), other_headers)
40 | else (http_headers, ((k, v)::other_headers))
41 | )
42 |
43 | val (http_headers, server_headers) = foldl process_key (nil, nil) headers
44 |
45 | (* Parse out client and server port numbers *)
46 | val client = case Int.fromString (!remote_port) of
47 | SOME i => (!remote_addr, i)
48 | | NONE => raise ProtocolError
49 | handle Overflow => raise ProtocolError
50 |
51 | val server = case Int.fromString (!server_port) of
52 | SOME i => (!server_addr, i)
53 | | NONE => raise ProtocolError
54 | handle Overflow => raise ProtocolError
55 |
56 | (* Split the path, dropping leading / if necessary *)
57 | val splitSlash = String.fields (fn c => c = #"/")
58 | val pre = case splitSlash (!script_name) of (""::p) => p | p => p
59 | val post = case splitSlash (!path_info) of (""::p) => p | p => p
60 |
61 | (* If no postpath is provided, the prepath should probably be there... *)
62 | val (pre, post) = case (pre, post) of (x, nil) => (nil, x) | x => x
63 | in
64 | {
65 | client = client, method = !request_method, path = (pre, post),
66 | query_string = !query_string, content_length = content_length,
67 | content = content_reader, doc_root = !document_root,
68 | server_name = !server_name, server_bind = server,
69 | http_headers = http_headers, server_headers = server_headers
70 | }
71 | end
72 |
73 |
74 | (* val make_response: Web.header * string -> string
75 | *
76 | * Concatenate together output headers and content into a CGI-style response.
77 | *)
78 | fun make_response (headers, body) = let
79 | fun headerLine (k, v) = k ^ ": " ^ v ^ "\r\n"
80 | in
81 | Word8Vector.concat [
82 | Byte.stringToBytes (String.concat (map headerLine headers)),
83 | Byte.stringToBytes "\r\n",
84 | body ]
85 | end
86 |
87 | end
88 |
--------------------------------------------------------------------------------
/web/server/fastcgi-mlton.sml:
--------------------------------------------------------------------------------
1 | structure FastCGIServer :> WEB_SERVER where type opts = INetSock.sock_addr = struct
2 |
3 | structure FS = Posix.FileSys
4 |
5 | type opts = INetSock.sock_addr
6 |
7 | val callbacks : (unit -> unit) list ref = ref nil
8 |
9 | fun addCleanupCallback f = callbacks := (f :: !callbacks)
10 |
11 | fun serve addr application =
12 | let
13 | val sock = case FS.ST.isSock (FS.fstat FS.stdin) of
14 | true => MLton.Socket.fdToSock FS.stdin
15 | | false => let
16 | val listener = INetSock.TCP.socket ()
17 | val () = Socket.Ctl.setREUSEADDR (listener, true);
18 | val () = Socket.bind (listener, addr);
19 | in
20 | listener
21 | end
22 | val () = Socket.listen (sock, 10);
23 | fun acceptLoop () = let
24 | val conn = Socket.accept sock
25 | val () = FastCGICommon.serveConn application conn
26 | val () = List.app (fn f => f ()) (!callbacks)
27 | in
28 | acceptLoop ()
29 | end
30 | in
31 | acceptLoop ()
32 | handle x => (Socket.close sock; raise x)
33 | end
34 |
35 | end
36 |
--------------------------------------------------------------------------------
/web/server/http-server-fn.sml:
--------------------------------------------------------------------------------
1 | (* functor HTTPServerFn
2 | *
3 | * Chiral version of the HTTP server. This is distinct from the non-Chiral
4 | * version in supporting keep-alive; the single-threaed version can only handle
5 | * one request per connection.
6 | *)
7 |
8 | functor HTTPServerFn (
9 | structure CS : CHIRAL_SOCKET
10 | structure T : THREAD
11 | )
12 | =
13 | struct
14 |
15 | val server_name = "Stilts-HTTPd/0.1"
16 |
17 | structure Handler = HTTPHandlerFn(
18 | structure S = CS.Socket
19 | val can_keep_alive = true
20 | val server_name = server_name
21 | )
22 |
23 | type opts = CS.INetSock.sock_addr
24 |
25 | fun spawn_server addr application =
26 | let
27 | val listener = CS.INetSock.TCP.socket ()
28 |
29 | val (server_host, server_port) = CS.INetSock.fromAddr addr
30 | val sbind = (NetHostDB.toString server_host, server_port)
31 |
32 | val connServer = Handler.serve_conn (server_name, sbind, nil) application
33 |
34 | fun accept () = let
35 | val () = print "accept\n"
36 | val conn = CS.Socket.accept listener
37 | val t = T.new (connServer conn)
38 | in
39 | accept ()
40 | end
41 |
42 | fun app () = (
43 | CS.Socket.Ctl.setREUSEADDR (listener, true);
44 | CS.Socket.bind (listener, addr);
45 | CS.Socket.listen (listener, 9);
46 | accept ()
47 | ) handle x => (CS.Socket.close listener; raise x)
48 |
49 | in
50 | T.new app
51 | end
52 |
53 | end
54 |
--------------------------------------------------------------------------------
/web/server/http-server.sml:
--------------------------------------------------------------------------------
1 | structure HTTPServer :> WEB_SERVER where type opts = INetSock.sock_addr = struct
2 |
3 | val server_name = "Stilts-HTTPd/0.1"
4 |
5 | structure Handler = HTTPHandlerFn(
6 | structure S = Socket
7 | val can_keep_alive = true
8 | val server_name = server_name
9 | )
10 |
11 | val addCleanupCallback = Handler.addCleanupCallback
12 | type opts = INetSock.sock_addr
13 |
14 | fun serve addr application =
15 | let
16 | val listener = INetSock.TCP.socket ()
17 |
18 | val (server_host, server_port) = INetSock.fromAddr addr
19 | val sbind = (NetHostDB.toString server_host, server_port)
20 |
21 | fun accept () = ((
22 | Handler.serve_conn (server_name, sbind, nil)
23 | application
24 | (Socket.accept listener);
25 | accept ()
26 | ) handle _ => accept ())
27 | in
28 | (
29 | Socket.Ctl.setREUSEADDR (listener, true);
30 | Socket.bind (listener, addr);
31 | Socket.listen (listener, 9);
32 | accept ()
33 | ) handle x => (Socket.close listener; raise x)
34 | end
35 |
36 | end
37 |
--------------------------------------------------------------------------------
/web/server/scgi-server.sml:
--------------------------------------------------------------------------------
1 | structure SCGIServer :> WEB_SERVER where type opts = INetSock.sock_addr = struct
2 |
3 | exception ProtocolError
4 |
5 | val callbacks : (unit -> unit) list ref = ref nil
6 |
7 | fun addCleanupCallback f = callbacks := (f :: !callbacks)
8 |
9 | (* val pairs: 'a list -> ('a * 'a) list
10 |
11 | Combine a list of even length into pairs of adjacent elements:
12 | [ a, b, c, d, e, f ] ==> [ (a, b), (c, d), (e, f) ]
13 |
14 | A trailing element, if present, will be ignored.
15 | *)
16 | fun pairs nil = nil
17 | | pairs (_::nil) = nil
18 | | pairs (a::(b::r)) = (a, b)::(pairs r)
19 |
20 |
21 | fun serveConn application (conn, conn_addr) =
22 | let
23 |
24 | fun read_length (sock, max) = let
25 | val delim = Byte.charToByte #":"
26 | fun loop (acc, n) = let
27 | val c = Word8Vector.sub (Socket.recvVec (sock, 1), 0)
28 | in
29 | if c = delim
30 | then rev acc
31 | else if n < max
32 | then loop (c :: acc, n + 1)
33 | else raise ProtocolError
34 | end
35 | in
36 | Byte.bytesToString (Word8Vector.fromList (loop (nil, 0)))
37 | end
38 |
39 | (* Parse request netstring *)
40 | val request_len = case Int.fromString (read_length (conn, 10)) of
41 | SOME i => i
42 | | NONE => raise ProtocolError
43 | handle Overflow => raise ProtocolError
44 |
45 | (* Read the request and split fields *)
46 | val req_data = Word8VectorSlice.slice (
47 | SockUtil.recvVec (conn, request_len + 1),
48 | 0,
49 | SOME request_len
50 | )
51 | val req_headers = pairs (String.fields (fn c => c = #"\000")
52 | (Byte.unpackStringVec req_data))
53 |
54 | (* Get the content length and prepare to read the body *)
55 | val (content_length, req_headers) = case req_headers of
56 | (("CONTENT_LENGTH", n)::r) => (case (Int.fromString n) of
57 | SOME i => (i, r)
58 | | NONE => raise ProtocolError)
59 | | _ => raise ProtocolError
60 |
61 | val content_cache : Word8Vector.vector option ref = ref NONE
62 |
63 | fun reader () =
64 | case !content_cache of
65 | SOME c => c
66 | | NONE => let
67 | val c = SockUtil.recvVec (conn, content_length)
68 | in
69 | content_cache := SOME c;
70 | c
71 | end
72 |
73 | val request = CGI.make_request (req_headers, content_length, reader)
74 |
75 | (* GO GO GO! *)
76 | val response = CGI.make_response (application request)
77 |
78 | val () = SockUtil.sendVec (conn, response)
79 | val () = Socket.close conn
80 |
81 | val () = List.app (fn f => f ()) (!callbacks)
82 | in
83 | ()
84 | end
85 | handle ProtocolError => Socket.close conn
86 | handle x => (Socket.close conn; raise x)
87 |
88 |
89 | type opts = INetSock.sock_addr
90 |
91 | fun serve addr application =
92 | let
93 | val listener = INetSock.TCP.socket ()
94 |
95 | fun accept () = (
96 | serveConn application (Socket.accept listener);
97 | accept ()
98 | )
99 | in
100 | (
101 | Socket.Ctl.setREUSEADDR (listener, true);
102 | Socket.bind (listener, addr);
103 | Socket.listen (listener, 9);
104 | accept ()
105 | ) handle x => (Socket.close listener; raise x)
106 | end
107 |
108 | end
109 |
--------------------------------------------------------------------------------
/web/static-server.sml:
--------------------------------------------------------------------------------
1 | structure StaticServer :> sig
2 |
3 | val server: { basepath: string,
4 | expires: LargeInt.int option,
5 | headers: Web.header list } -> Web.app
6 |
7 | end = struct
8 |
9 | structure U = WebUtil
10 |
11 | val content_type = fn
12 | "png" => "image/png"
13 | | "gif" => "image/gif"
14 | | "jpg" => "image/jpeg"
15 | | "css" => "text/css"
16 | | "js" => "text/javascript"
17 | | "html" => "text/html"
18 | | _ => "text/plain"
19 |
20 | fun server { basepath, expires, headers } (req: Web.request) = let
21 |
22 | val (_, reqPath) = #path req
23 |
24 | fun isBad ".." = true
25 | | isBad _ = false
26 |
27 | val () = if List.exists isBad reqPath then raise U.notFound else ()
28 |
29 | val reqPathStr = OS.Path.toString { isAbs = false, vol = "",
30 | arcs = basepath :: reqPath }
31 |
32 | val stream = BinIO.openIn reqPathStr
33 | val data = BinIO.inputAll stream
34 | val () = BinIO.closeIn stream
35 |
36 | val { ext, ... } = OS.Path.splitBaseExt reqPathStr
37 |
38 | val ct = content_type (case ext of SOME e => e | NONE => "")
39 |
40 | val formatTime = HTTPDate.format o Date.fromTimeUniv
41 | val now = Time.now ()
42 |
43 | val headers = ("Content-Type", ct)
44 | :: ("Date", formatTime now)
45 | :: headers
46 |
47 | val headers = case expires of
48 | NONE => headers
49 | | SOME secs => ("Expires",
50 | formatTime (Time.+ (now, Time.fromSeconds secs)))
51 | :: headers
52 | in
53 | (headers, data)
54 | end
55 | handle Io => raise U.notFound
56 |
57 | end
58 |
--------------------------------------------------------------------------------
/web/web-util.sig:
--------------------------------------------------------------------------------
1 | signature WEB_UTIL = sig
2 |
3 | (* Debugging *)
4 | val dumpRequest: Web.request -> string
5 | val dumpRequestWrapper: (string -> unit) -> Web.app -> Web.app
6 |
7 | (* Path extraction and manipulation *)
8 | val withPath: Web.pathsec * Web.pathsec -> Web.request -> Web.request
9 | val prepath: Web.request -> Web.pathsec
10 | val postpath: Web.request -> Web.pathsec
11 | val flattenPath: Web.pathsec * Web.pathsec -> string
12 |
13 | (* Other request accessors *)
14 | val http_header: string -> Web.request -> string option
15 | val server_header: string -> Web.request -> string option
16 |
17 | (* Exception handling, and some shortcuts for common exceptions *)
18 | val httpExnCode: Web.http_exn -> string
19 | val exnWrapper: Web.app -> Web.app
20 | val notFound: exn
21 | val redirect: string -> exn
22 | val redirectPostpath: Web.request -> Web.pathsec -> exn
23 |
24 | (* Wrappers to build a Web.response *)
25 | val resp: string -> string -> Web.response
26 | val htmlResp: Web.html -> Web.response
27 | val xhtmlResp: Web.html -> Web.response
28 |
29 | (* HTML escaping *)
30 | val escapeStr: string -> string
31 | val escape: string -> Web.html
32 | val escapeForJS: string -> Web.html
33 |
34 | (* Dispatching and automatic redirection *)
35 | datatype dispatchmode = EXACT | PREFIX | SLASH
36 | val dispatch: (Web.pathsec * dispatchmode * Web.app) list -> Web.app
37 | val forceSlash: Web.app -> Web.app
38 |
39 | (* URL-encoding *)
40 | val urlencode: string -> string
41 | val urldecode: string -> string
42 |
43 | end
44 |
--------------------------------------------------------------------------------
/web/web.cm:
--------------------------------------------------------------------------------
1 | Library
2 | structure Web
3 | signature WEB_UTIL
4 | structure WebUtil
5 | signature FORM
6 | structure Form
7 | structure HTTPDate
8 | structure StaticServer
9 | structure CGI
10 | structure HTTPServer
11 | structure SCGIServer
12 | structure FastCGIServer
13 | functor HTTPHandlerFn
14 | functor LineReader
15 | is
16 | $/basis.cm
17 | $/smlnj-lib.cm
18 | $/inet-lib.cm
19 | web.sml
20 | web-util.sig
21 | web-util.sml
22 | form.sig
23 | form.sml
24 | http-date.sml
25 | static-server.sml
26 | ../util/linereader.sml
27 | server/cgi.sml
28 | server/http-server.sml
29 | server/scgi-server.sml
30 | server/fastcgi-server.sml
31 | server/http-handler-fn.sml
32 | #if (SMLNJ_VERSION = 110) andalso (SMLNJ_MINOR_VERSION < 57)
33 | pack-compat.sml
34 | #endif
35 |
--------------------------------------------------------------------------------
/web/web.mlb:
--------------------------------------------------------------------------------
1 | local
2 | $(SML_LIB)/basis/basis.mlb
3 | $(SML_LIB)/basis/mlton.mlb
4 | $(SML_LIB)/smlnj-lib/INet/inet-lib.mlb
5 | web.sml
6 | web-util.sig
7 | web-util.sml
8 | $(SML_LIB)/smlnj-lib/Util/smlnj-lib.mlb
9 | form.sig
10 | form.sml
11 | http-date.sml
12 | static-server.sml
13 | ../util/linereader.sml
14 | server/cgi.sml
15 | server/scgi-server.sml
16 | server/http-handler-fn.sml
17 | server/http-server.sml
18 | server/fastcgi-common.sml
19 | server/fastcgi-mlton.sml
20 | in
21 | structure Web
22 |
23 | signature WEB_UTIL
24 | structure WebUtil
25 |
26 | signature FORM
27 | structure Form
28 |
29 | structure HTTPDate
30 |
31 | structure StaticServer
32 | structure CGI
33 | structure SCGIServer
34 | structure FastCGIServer
35 | structure HTTPServer
36 |
37 | functor HTTPHandlerFn
38 | end
39 |
--------------------------------------------------------------------------------
/web/web.sml:
--------------------------------------------------------------------------------
1 | structure Web = struct
2 |
3 | type header = string * string
4 | type hostport = string * int
5 | type pathsec = string list
6 |
7 | type request = {
8 | (* Client connection *)
9 | client: hostport,
10 |
11 | (* GET, POST, etc. *)
12 | method: string,
13 |
14 | (* The path is divied into two parts: the prepath is
15 | the portion of the URL hierarchy leading up to the
16 | application's root, and the postpath is everything
17 | after. Paths with a trailing slash are represented with
18 | an empty string at the end of postpath.
19 |
20 | Invariants:
21 | - An empty path component may only occur as the last
22 | item in postpath; never elsewhere in postpath or
23 | anywhere in prepath.
24 | - Either prepath or postpath may be nil, but not both.
25 | The absolute root must therefore be represented as:
26 | (nil, [""])
27 | *)
28 | path: pathsec * pathsec,
29 | query_string: string,
30 |
31 | (* HTTP headers are passed as in CGI/SCGI: upper case,
32 | with an HTTP_ prefix. The order is not specified.
33 | *)
34 | http_headers: header list,
35 |
36 | (* Request content is read on demand, to allow applications
37 | to reject excessive amounts of data. The interface does
38 | not yet allow incremental reading of data.
39 | *)
40 | content_length: int,
41 | content: unit -> Word8Vector.vector,
42 |
43 | (* Server *)
44 | doc_root: string,
45 | server_name: string,
46 | server_bind: hostport,
47 | server_headers: header list
48 | }
49 |
50 | type response = header list * Word8Vector.vector
51 |
52 | type app = request -> response
53 |
54 | type 'a server = 'a -> app -> unit
55 |
56 | datatype html = HTML of string
57 |
58 | (* HTTP error and redirect codes *)
59 | datatype http_exn = HTTP300MultipleChoices
60 | | HTTP301Moved of string
61 | | HTTP302Found of string
62 | | HTTP303SeeOther of string
63 | | HTTP304NotModified
64 | | HTTP305UseProxy of string
65 | | HTTP307TemporaryRedirect of string
66 | | HTTP400BadRequest
67 | | HTTP401Unauthorized
68 | | HTTP403Forbidden
69 | | HTTP404NotFound
70 | | HTTP405MethodNotAllowed
71 | | HTTP406NotAcceptable
72 | | HTTP407ProxAuthReq
73 | | HTTP408RequestTimeout
74 | | HTTP409Conflict
75 | | HTTP410Gone
76 | | HTTP411LengthRequired
77 | | HTTP412PreconditionFailed
78 | | HTTP413ReqEntityTooLarge
79 | | HTTP414ReqURITooLong
80 | | HTTP415UnsuppMediaType
81 | | HTTP416RangeNotSatisfiable
82 | | HTTP417ExpectationFailed
83 | | HTTP500InternalServerError of string
84 | | HTTP501NotImplemented
85 | | HTTP502BadGateway
86 | | HTTP503ServiceUnavailable
87 | | HTTP504GatewayTimeout
88 | | HTTP505VersionNotSupported
89 |
90 | exception HTTPExn of http_exn
91 |
92 | end
93 |
94 | signature WEB_SERVER = sig
95 |
96 | type opts
97 |
98 | val serve : opts Web.server
99 |
100 | val addCleanupCallback: (unit -> unit) -> unit
101 |
102 | end
103 |
--------------------------------------------------------------------------------
/wiki/.gitignore:
--------------------------------------------------------------------------------
1 | .cm
2 | wiki
3 | wiki.db
4 | *.squall.sml
5 |
--------------------------------------------------------------------------------
/wiki/chiral.sml:
--------------------------------------------------------------------------------
1 | structure T = Thread (structure T = ThreadBase
2 | structure RC = SelectReactorCore
3 | structure C = ConfigPrintEverything)
4 |
5 | structure CV = CondVar(T)
6 | structure CS = ChiralSocketFn(T)
7 | structure SU = ChiralSockUtil(CS)
8 | structure LR = LineReader(CS.Socket)
9 |
10 | structure HTTPServer = HTTPServerFn(structure CS = CS structure T = T)
11 |
12 |
--------------------------------------------------------------------------------
/wiki/lighttpd.conf:
--------------------------------------------------------------------------------
1 | server.modules = ( "mod_access",
2 | "mod_fastcgi",
3 | "mod_accesslog",
4 | "mod_rewrite",
5 | "mod_staticfile" )
6 |
7 | server.document-root = "/Library/WebServer/Documents"
8 | server.event-handler = "freebsd-kqueue" # on Mac OS X or FreeBSD
9 |
10 | # server.errorlog = "/tmp/lighttpd.error.log"
11 | accesslog.filename = "/tmp/access.log"
12 |
13 | fastcgi.server = ( "" =>
14 | ( "127.0.0.1" =>
15 | (
16 | "host" => "127.0.0.1",
17 | "port" => 5124,
18 | "check-local" => "disable",
19 | "disable-time" => 1,
20 | )
21 | )
22 | )
23 |
24 | server.port = 8080
25 |
--------------------------------------------------------------------------------
/wiki/main.sml:
--------------------------------------------------------------------------------
1 | val _ = Wiki.main ()
2 |
--------------------------------------------------------------------------------
/wiki/templates/edit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Wiki: ${title}
5 |
6 |
7 |
Wiki: ${title}
8 |
This page does not exist yet. You can create it now:
9 |
15 |
Put text in [squarebrackets] to make it a Wiki link.