(*
* <COPYRIGHT>
* Copyright 2002 Joachim Schrod Network and Publication Consultance GmbH, Gerd Stolpmann
*
* <GPL>
* This file is part of WDialog.
*
* WDialog is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* WDialog is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with WDialog; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
* </>
*)
(* $Id: wd_templrep.ml,v 3.6 2005-06-11 14:24:14 stolpmann Exp $
* ----------------------------------------------------------------------
*
*)
open Pxp_types
open Pxp_document
open Pxp_dtd
open Wd_types
module D = Wd_dictionary
(* The data type of a tree with placeholders (parameters). The placeholders
* may occur in data nodes, or in attribute values.
*)
type expr =
Expr_var of string
| Expr_strconst of string
| Expr_apply of (string * expr list)
| Expr_param of (string * string list)
type 'ext tree =
Element of 'ext tree_element (* <x> ... </x> *)
| Data of 'ext tree_data (* arbitrary text *)
| Param of (string * string list) (* ${paramname/enc1/enc2/...} *)
| Expr of (expr * string * string list * 'ext node * (string*int*int))
(* for $[expr/enc1/enc2/...]:
* (expr, expr_as_string, enc_list, parent_node, parent_node_pos)
*)
constraint 'ext = 'ext node #extension
and 'ext tree_element =
{ el_name : string; (* The name of the element *)
el_attributes : (string * attspec list) list;
(* (name, value) list. The values are sequences of normal text and
* parameters.
* E.g. att="a $x y" is repesented as
* [ "att", [ Att_verb "a "; Att_param "x"; Att_verb " y" ]]
*
* Note:
* Attributes with an empty string as value are represented as
* [ Att_verb "" ]. Attributes with implied value are represented
* as [].
*)
el_unsplitted_attributes : (string * att_value) list;
(* The original attributes. *)
el_have_params_in_atts : bool;
(* Whether there are parameters in attribute values or not *)
el_node : 'ext node;
(* A reference to the original XML node *)
el_subtrees : 'ext tree list; (* The trees within this element *)
}
and attspec =
Att_verb of string (* normal text *)
| Att_param of (string * string list) (* ${paramname/enc1/enc2/...} *)
| Att_expr of (expr * string * string list)
(* for $[expr/enc1/enc2/...]:
* (expr, expr_as_string, enc_list)
*)
and 'ext tree_data =
{ dt_value : string; (* The string *)
dt_node : 'ext node; (* Reference to the original node *)
dt_clonable : bool;
(* true if dt_value = dt_node # data, i.e. if there is not any
* '$' sequence in the data node, and the node need not to be
* splitted up.
* There is an important optimization basing on this information.
*)
}
;;
type 'ext t =
{ contents : 'ext tree list;
app : application_type;
template_name : string; (* Only used for error messages *)
mk_uiencode : unit -> 'ext node;
}
;;
type 'ext param =
{ param_tree : 'ext Pxp_document.node;
param_text : string Lazy.t;
}
(**********************************************************************)
(* Parse expressions: *)
(* Syntax:
* expr_oe -> expr [ "/" oelist ]
* oelist -> oe-name [ "/" oelist ]
* expr -> variable
* | number
* | function-name "(" [arguments] ")"
* | "$" parameter-name
* | "${" parameter-name [ "/" oelist ] "}"
* arguments -> expr [ "," arguments ]
*)
module L = Wd_brexpr_lex
let parse_expr_oe enc s =
(* enc: character encoding of [s]
* s: the string to parse
* return value: a pair of the scanned [expr] and a string list containing
* the scanned output encodings
*)
let scan = Wd_brexpr_lex.scan in
let ulb = Netulex.ULB.from_string_inplace (enc :> Netconversion.encoding) s in
let buf = Netulex.Ulexing.from_ulb_lexbuf ulb in
let raise_bad() =
raise (Formal_user_error("Bad syntax in expression: $[" ^ s ^ "]"))
in
let rec parse_expr tok =
match tok with
| L.Token name ->
( let next = scan buf in
match next with
| L.LParen ->
let args = parse_arguments() in
let next' = scan buf in
(Expr_apply (name, args), next')
| _ ->
(Expr_var name, next)
)
| L.Number n ->
(Expr_strconst (string_of_int n), scan buf)
| L.Dollar ->
( let next = scan buf in
match next with
| L.Token name ->
(Expr_param(name,[]), scan buf)
| L.LBrace ->
let next' = scan buf in
( match next' with
| L.Token name ->
let next'' = scan buf in
let (oelist, next''') =
( match next'' with
| L.Slash ->
parse_out_enc()
| _ ->
([], next'')
) in
if next''' <> L.RBrace then
raise_bad();
(Expr_param(name,oelist), scan buf)
| _ ->
raise_bad();
)
| _ ->
raise_bad()
)
| _ ->
raise_bad()
and parse_arguments () =
let tok = scan buf in
match tok with
| L.RParen ->
[]
| _ ->
parse_arguments_1 tok
and parse_arguments_1 tok =
let (arg,next) = parse_expr tok in
match next with
| L.RParen ->
[arg]
| L.Comma ->
arg :: (parse_arguments_1 (scan buf))
| _ ->
raise_bad()
and parse_out_enc() =
let tok = scan buf in
match tok with
| L.Token name ->
let tok' = scan buf in
( match tok' with
| L.Slash ->
let r, tok'' = parse_out_enc() in
(name :: r, tok'')
| _ ->
([name], tok')
)
| _ ->
raise_bad()
in
let tok = scan buf in
let expr, next = parse_expr tok in
( match next with
| L.Eof ->
(expr, [])
| L.Slash ->
let oelist, next' = parse_out_enc() in
if next' <> L.Eof then raise_bad();
(expr, oelist)
| _ ->
raise_bad()
)
;;
(**********************************************************************)
(* Used for splitting text containing parameters: *)
let param_re =
(Pcre.regexp
"\\$([a-zA-Z0-9_]+|{([^} \t\r\n]+)}|\\[([^\\] \t\r\n]+)\\]|\\$)")
(* $name or ${name} or $[name] or $$ *)
;;
let param_slash_re = Pcre.regexp "/";;
let pcre_remove_groups l =
(* Remove the silly Group and NoGroup markers *)
List.filter
(function (Pcre.Delim _|Pcre.Text _) -> true | _ -> false)
l
;;
let get_parameters t =
let params = ref D.empty in
let add p =
params := D.add p () !params in
let rec scan_tree =
function
Element e ->
List.iter
(fun (_,al) ->
List.iter
(function
Att_param(p,_) ->
add p
| _ ->
()
)
al;
)
e.el_attributes;
List.iter scan_tree e.el_subtrees;
| Param (p,_) ->
add p
| _ ->
()
in
List.iter scan_tree t.contents;
!params
;;
let split_param s =
(* s is either ${name/enc1/...} or $[name/enc1/...] *)
let s = String.sub s 2 (String.length s - 3) in
match Pcre.split ~rex:param_slash_re s with
[] -> ("", []) (* Will hopefully be later caught! *)
| name :: enc -> (name, enc)
;;
let scan_attributes n =
(* Transform the attribute list of 'n' into a list like el_attributes. *)
let enc = n # encoding in
let pcre_postprocess l =
let l' =
List.map
(function
Pcre.Delim "$$" ->
Att_verb "$"
| Pcre.Delim p ->
if String.length p >= 2 then begin
if p.[1] = '{' then
let name,enclist = split_param p in
Att_param (name,enclist)
else
if p.[1] = '[' then
let expr,enclist =
parse_expr_oe enc (String.sub p 2 (String.length p - 3))
in
Att_expr (expr,p,enclist)
else
Att_param (String.sub p 1 (String.length p - 1), [])
end
else
Att_param (String.sub p 1 (String.length p - 1), [])
| Pcre.Text t ->
Att_verb t
| (Pcre.Group(_,_)|Pcre.NoGroup) ->
assert false
)
(pcre_remove_groups l)
in
(* As [] is reserved for implied values, the empty string/list is recoded.
*)
if l' = [] then [ Att_verb "" ] else l'
in
let atts = n # attributes in
let atts' =
List.map
(fun (att_name, att_value) ->
match att_value with
Value s ->
att_name, pcre_postprocess(Pcre.full_split
~rex:param_re ~max:(-1) s)
| Valuelist sl ->
let s = String.concat " " sl in
att_name, pcre_postprocess(Pcre.full_split
~rex:param_re ~max:(-1) s)
| Implied_value ->
att_name, []
)
atts
in
atts'
;;
let att_param_exists l =
(* return 'true' iff there is an Att_param or Att_expr in l *)
List.exists
(fun (n,v) ->
List.exists
(function
Att_param _
| Att_expr(_,_,_) -> true
| Att_verb _ -> false)
v
)
l
;;
let rec find_parent_element n =
match n # node_type with
T_element _ -> n
| _ -> find_parent_element (n#parent)
;;
let rec prepare_tree_with_parameters ~mk_uiencode name app nl =
(* We need the 'treelist', the 'tree' type representation of the
* prepared XML tree 'nl' (=nodelist).
* The other components 'app' and 'template_name' are already known
* and can be entered directly.
*)
let treelist = List.flatten (List.map scan_tree nl) in
{ contents = treelist;
app = app;
template_name = name;
mk_uiencode = mk_uiencode;
}
and scan_tree n =
(* Transform the node tree 'n' into the corresponding 'tree list' value *)
let enc = n # encoding in
match n # node_type with
T_element element_name ->
(* An element is simply converted to the corresponding representation.
* Only the attributes have to be processed, but that is done
* separately in the function scan_attributes.
*)
let scanned_atts = scan_attributes n in
[ Element
{ el_name = element_name;
el_attributes = scanned_atts;
el_unsplitted_attributes = n # attributes;
el_have_params_in_atts = att_param_exists scanned_atts;
el_node = n;
el_subtrees =
List.flatten
(List.map scan_tree (n # sub_nodes));
}
]
| T_data ->
(* A data node may contain parameters. So we must apply a regexp
* on the data string to split the node up.
*)
let l = Pcre.full_split ~rex:param_re ~max:(-1) (n # data) in
let l' =
List.map
(function
Pcre.Delim "$$" ->
Data
{ dt_value = "$";
dt_node = n;
dt_clonable = false;
}
| Pcre.Delim p ->
if String.length p >= 2 then begin
if p.[1] = '{' then
let (name,enclist) = split_param p in
Param(name,enclist)
else
if p.[1] = '[' then
let expr,enclist =
parse_expr_oe
enc (String.sub p 2 (String.length p - 3)) in
let pos =
try (find_parent_element n) # position with Not_found -> assert false in
Expr(expr,p,enclist,n,pos)
else
Param (String.sub p 1 (String.length p - 1), [])
end
else
Param (String.sub p 1 (String.length p - 1), [])
| Pcre.Text t ->
Data
{ dt_value = t;
dt_node = n;
dt_clonable = (List.length l = 1);
}
| (Pcre.Group(_,_)|Pcre.NoGroup) ->
assert false
)
(pcre_remove_groups l)
in
l'
| _ ->
assert false
;;
let instantiate ?(eval_expr = fun _ -> assert false) r params container =
let dtd = r.app#dtd in
let rec lookup key l =
(* Look up the parameter 'key' in the parameter dict list 'l', or
* raise Not_found.
*)
match l with
[] -> raise Not_found
| x :: l' ->
try
Wd_dictionary.find key x
with
Not_found -> lookup key l'
in
let rec encode s enclist =
(* Apply the encodings [enclist] to [s], one after the other *)
match enclist with
[] -> s
| enc :: enclist' ->
let f = r.app#output_encoding enc in
encode (f s) enclist'
in
let transform_to_uiencode n enclist =
(* Wrap the node tree [n] into <ui:encode enc="ENCLIST">...</ui:encode> *)
if enclist = [] then
n
else
let n' = r.mk_uiencode() in
n' # set_attributes [ "enc", Valuelist enclist ];
n' # set_nodes [n];
n'
in
let lookup_text p =
(* Return the text string for parameter 'p' or raise a Runtime_error,
* if 'p' does not exist
*)
try
Lazy.force (lookup p params).param_text
with
Not_found ->
raise (Instantiation_error
(r.template_name ^
": Nothing to instantiate for template parameter `$" ^ p ^ "'"))
in
let lookup_tree p =
(* Return the XML tree for parameter 'p' or raise a Runtime_error,
* if 'p' does not exist
*)
try
(lookup p params).param_tree
with
Not_found ->
raise (Instantiation_error
(r.template_name ^
": Nothing to instantiate for template parameter `$" ^ p ^ "'"))
in
let rec subst_expr_params expr =
match expr with
Expr_var _ -> expr
| Expr_strconst _ -> expr
| Expr_apply(fn_name, args) ->
Expr_apply(fn_name, List.map subst_expr_params args)
| Expr_param(p_name, enclist) ->
let p = encode (lookup_text p_name) enclist in
Expr_strconst p
in
let eval_expr_prot (entity,line,col) expr expr_as_string =
try
eval_expr (subst_expr_params expr)
with
(Instantiation_error msg as e) ->
if line >= 0 then
raise
(Instantiation_error
("In entity " ^ entity ^ ", near line " ^ string_of_int line ^ ", position " ^
string_of_int col ^ ", within the expression " ^ expr_as_string ^ ": " ^ msg))
else
raise e
in
let rec inst_attributes pos l =
(* Instantiate the attributes in the list 'l', an el_attributes-like
* list. Returns the attributes in a form that can be passed as
* argument to the method create_element.
*)
match l with
[] -> []
| (n, v) :: l' ->
if v = [] then
(* Implied value: leave this out *)
inst_attributes pos l'
else
(* Normal value *)
let v' =
String.concat
""
(List.map
(function
Att_verb s ->
s
| Att_param(p,enclist) ->
encode (lookup_text p) enclist
| Att_expr(expr,expr_str,enclist) ->
encode (eval_expr_prot pos expr expr_str) enclist
)
v
)
in
(n, v') :: inst_attributes pos l'
in
let rec inst tree =
(* Instantiates the 'tree' by descending recursively into the tree,
* calling the right instantiation function for every node,
* and creating a new XML tree with the same structure as 'tree'.
* The XML tree is returned.
*)
match tree with
Element el ->
(* If there are no attributes with parameters, some speedup is
* possible.
*)
if el.el_have_params_in_atts then begin
(* The slower but general case. *)
let pos = el.el_node # position in
let inst_atts = inst_attributes pos el.el_attributes in
let c = el.el_node # create_element
dtd (T_element el.el_name) inst_atts in
c # set_nodes (List.map inst (el.el_subtrees));
c
end
else begin
(* The simple and fast case *)
let c = el.el_node # orphaned_flat_clone in
c # quick_set_attributes (el.el_unsplitted_attributes);
c # set_nodes (List.map inst (el.el_subtrees));
c
end
| Data dt ->
if dt.dt_clonable then
(* Optimization: We can simply clone dt_node to get the new
* node. This is much faster because:
* - The clone and the original share the data string; it is
* not copied
* - The 'data_node' class (in Parse) does some optimizations, too.
* Especially the HTML-escaped version of the data string is
* precomputed, and this second string can be shared, too.
*)
dt.dt_node # orphaned_flat_clone
else
(* The general case: Create a new data node with contents
* dt_value.
*)
dt.dt_node # create_data dtd dt.dt_value
| Param (p,enclist) ->
let replacement = lookup_tree p in
transform_to_uiencode replacement#orphaned_clone enclist
| Expr (expr,expr_str,enclist,dt_node,pos) ->
let value = eval_expr_prot pos expr expr_str in
dt_node # create_data dtd (encode value enclist)
in
let nodes = List.map inst r.contents in
container # set_nodes nodes
;;
(* ======================================================================
* History:
*
* $Log: wd_templrep.ml,v $
* Revision 3.6 2005-06-11 14:24:14 stolpmann
* Extension of bracket expressions: many new functions.
* Functions in bracket expressions may now lazily evaluate their arguments.
* ui:if and ui:ifvar may refer to any functions defined for bracket
* expressions.
* New: ui:ifexpr
* Parsing of bracket expressions is now done with ulex. Private interfaces
* of PXP are no longer used for this purpose.
* Serialization has been rewritten, and it is now safe.
* Fix: In ui:context there may now be macro references at any location.
* Also documented all these changes.
*
* Revision 3.5 2004/08/01 18:31:35 stolpmann
* Updated PXP lexer code to SVN version of PXP.
* Note: This is a temporary solution, not for release!
*
* Revision 3.4 2002/10/20 19:39:17 stolpmann
* New feature: The brackets $[...] can contain expressions,
* not only variables
*
* Revision 3.3 2002/02/26 23:56:35 stolpmann
* Fix: $[var] in attributes is no longer optimized away in
* some cases
*
* Revision 3.2 2002/02/14 16:15:21 stolpmann
* Added copyright notice.
*
* Revision 3.1 2002/02/12 20:29:20 stolpmann
* Initial release at sourceforge.
*
* Revision 1.8 2002/01/30 15:13:33 gerd
* New: The notations ${name/enc1/...} and $[name/enc1/...] are
* recognized and handled properly.
*
* Revision 1.7 2002/01/27 19:13:15 gerd
* New: [get_parameters]
*
* Revision 1.6 2002/01/26 22:38:26 gerd
* Changed the type for passing parameters to templates. It is
* now syntax_tree dict instead of a simple list. Furthermore, the
* method [instantiate] no longer returns the new context. The new element
* ui:context creates the new contexts now.
*
* Revision 1.5 2002/01/24 23:34:33 gerd
* The function [instantiate] raises [Instantiation_error] on
* errors.
*
* Revision 1.4 2002/01/14 15:03:24 gerd
* Major change: Typing has been completely revised, and almost
* every tiny thing has now a new type. Also renamed a lot.
*
* Revision 1.3 2001/05/21 12:52:27 gerd
* Added a type constraint due to stricter type checking in
* O'Caml 3.01
*
* Revision 1.2 2000/09/21 15:12:34 gerd
* Updated for O'Caml 3 and PXP
*
* Revision 1.1 2000/05/08 10:36:37 gerd
* Initial revision.
*
*
*)