type value_behaviour = AddValue | ReplaceValue exception Duplicate_value exception Node_has_no_value exception No_such_value exception Useless_set type config_node_data = { values: string list; comment: string option; inactive: bool; ephemeral: bool; } [@@deriving yojson] type t = config_node_data Vytree.t [@@deriving yojson] let default_data = { values = []; comment = None; inactive = false; ephemeral = false; } let make name = Vytree.make default_data name let replace_value node path value = let data = {default_data with values=[value]} in Vytree.update node path data let add_value node path value = let node' = Vytree.get node path in let data = Vytree.data_of_node node' in let values = data.values in match (Vylist.find (fun x -> x = value) values) with | Some _ -> raise Duplicate_value | None -> let values = values @ [value] in Vytree.update node path ({data with values=values}) let delete_value node path value = let data = Vytree.data_of_node @@ Vytree.get node path in let values = Vylist.remove (fun x -> x = value) data.values in Vytree.update node path {data with values=values} let set_value node path value behaviour = match behaviour with | AddValue -> add_value node path value | ReplaceValue -> replace_value node path value let set node path value behaviour = if (Vytree.exists node path) then (match value with | None -> raise Useless_set | Some v -> set_value node path v behaviour) else let path_existing = Vytree.get_existent_path node path in let path_remaining = Vylist.complement path path_existing in let values = match value with None -> [] | Some v -> [v] in Vytree.insert_multi_level default_data node path_existing path_remaining {default_data with values=values} let get_values node path = let node' = Vytree.get node path in let data = Vytree.data_of_node node' in data.values let get_value node path = let values = get_values node path in match values with | [] -> raise Node_has_no_value | x :: _ -> x let delete node path value = match value with | Some v -> (let values = get_values node path in if Vylist.in_list values v then (match values with | [_] -> Vytree.delete node path | _ -> delete_value node path v) else raise No_such_value) | None -> Vytree.delete node path let set_comment node path comment = let data = Vytree.get_data node path in Vytree.update node path {data with comment=comment} let get_comment node path = let data = Vytree.get_data node path in data.comment let set_inactive node path inactive = let data = Vytree.get_data node path in Vytree.update node path {data with inactive=inactive} let is_inactive node path = let data = Vytree.get_data node path in data.inactive let set_ephemeral node path ephemeral = let data = Vytree.get_data node path in Vytree.update node path {data with ephemeral=ephemeral} let is_ephemeral node path = let data = Vytree.get_data node path in data.ephemeral module Renderer = struct (* TODO Replace use of Printf with Format *) module L = List module S = String module PF = Printf module VT = Vytree module RT = Reference_tree (* Nodes are ordered based on a comarison of their names *) let compare cmp node1 node2 = let name1 = VT.name_of_node node1 in let name2 = VT.name_of_node node2 in cmp name1 name2 let indentation indent level = S.make (level * indent) ' ' let render_inactive data = if data.inactive then "#INACTIVE " else "" let render_ephemeral data = if data.ephemeral then "#EPHEMERAL " else "" let render_comment data indents = match data.comment with | None -> "" (* Trailing indentation for the rest of the material on the next line *) | Some c -> PF.sprintf "/*%s*/\n%s" c indents let render_tag = function | None -> "" | Some tag -> PF.sprintf "%s " tag let render_outer indents data name tag = [render_comment data indents ; render_inactive data; render_ephemeral data; render_tag tag; name] |> S.concat "" let render_values ?(valueless=false) values = let quote_if_needed s = try let _ = Pcre.exec ~pat:"[\\s;{}#\\[\\]\"\']" s in Printf.sprintf "\"%s\"" s with Not_found -> s in match values with | [] -> if valueless then ";" else "{ }" | [v] -> PF.sprintf "%s;" (quote_if_needed v) | _ as vs -> S.concat "; " (List.map quote_if_needed vs) |> PF.sprintf "[%s];" let render_inner_and_outer indents inner outer = if inner = "" (* Hidden or empty descendents yield empty nodes *) then PF.sprintf "%s%s { }" indents outer else PF.sprintf "%s%s %s" indents outer inner let render ?(indent=4) ?(reftree=None) ?(cmp=BatString.numeric_compare) ?(showephemeral=false) ?(showinactive=false) (config_tree:t) = let is_hidden data = (not showephemeral && data.ephemeral) || (not showinactive && data.inactive) in let rec render_node level ?tag node = let data = VT.data_of_node node in let name = VT.name_of_node node in (* Hide inactive and ephemeral when necessary *) if is_hidden data then "" else let indents = indentation indent level in let outer = render_outer indents data name tag in let inner = (* Children are ignored if the node has values *) match data.values with | [] -> VT.children_of_node node |> render_children level | values -> render_values values in PF.sprintf "%s%s %s" indents outer inner and render_children level = function | [] -> "{ }" | children -> let indents = indentation indent level in let render_child node = render_node (level + 1) node in let rendered_children = L.map render_child children |> S.concat "\n" in if rendered_children = "" then "{ }" else PF.sprintf "{\n%s\n%s}" rendered_children indents in (* Walks the reftree and config_tree side-by-side *) let rec render_node_rt level tag rt node = let data = VT.data_of_node node in let name = VT.name_of_node node in let rt_data = VT.data_of_node rt in let rt_name = VT.name_of_node rt in (* Hide inactive and ephemeral when necessary *) if is_hidden data then "" else (* TODO refactor this ugly approach*) let (outer_name, level', inner) = let open RT in let children = VT.children_of_node node in let ordered = rt_data.keep_order in match rt_data.node_type with | Tag -> ("", 0, render_children_rt level (Some name) ordered rt children) | Other -> (name, level, render_children_rt level None ordered rt children) | Leaf -> (name, level, render_values ~valueless:rt_data.valueless data.values) in let indents = indentation indent level' in let outer = render_outer indents data outer_name tag in (* Do not insert a space before ; for valueless nodes *) if rt_data.valueless then PF.sprintf "%s%s%s" indents outer inner else PF.sprintf "%s%s %s" indents outer inner and render_children_rt level tag ordered rt = function | [] -> "{ }" | children -> let is_tagged = BatOption.is_some tag in let indents = indentation indent level in let reorder nodes = if ordered then nodes else L.sort (compare cmp) nodes in let render_child node = let level' = if is_tagged then level else level + 1 in let node_reftree = VT.find rt (VT.name_of_node node) in (* If there is no reftree for a node, default to stand-alone *) match node_reftree with | Some rt' -> render_node_rt level' tag rt' node | None -> render_node level' ?tag node in let rendered_children = children |> reorder |> L.map render_child |> S.concat "\n" in if rendered_children = "" then "{ }" else if is_tagged then rendered_children else PF.sprintf "{\n%s\n%s}" rendered_children indents in match reftree with | None -> render_node 0 config_tree | Some rt -> render_node_rt 0 None rt config_tree (* Rendering configs as set commands *) let render_set_path path value = let v = Printf.sprintf "\'%s\'" value in List.append path [v] |> String.concat " " |> Printf.sprintf "set %s" let rec render_commands ?(reftree=None) ?(alwayssort=false) path ct = let new_path = List.append path [Vytree.name_of_node ct] in let new_path_str = String.concat " " new_path in let data = Vytree.data_of_node ct in (* Get the node comment, if any *) let comment = BatOption.default "" data.comment in let comment_cmd = (if comment = "" then "" else Printf.sprintf "comment %s \'%s\'" new_path_str comment) in (* Sort child names, if required *) let child_names = Vytree.list_children ct in let child_names = begin match reftree with | Some rt -> if ((RT.get_keep_order rt path) && (not alwayssort)) then child_names else (List.sort BatString.numeric_compare child_names) | None -> if alwayssort then (List.sort BatString.numeric_compare child_names) else child_names end in (* Now handle the different cases for nodes with and without children *) match child_names with | [] -> (* This is a leaf node *) let values = List.map String.escaped data.values in let cmds = begin match values with | [] -> (* Valueless leaf node *) String.concat " " new_path |> Printf.sprintf "set %s" | [v] -> (* Single value, just one command *) render_set_path new_path v | vs -> (* A leaf node with multiple values *) List.map (render_set_path new_path) vs |> String.concat "\n" end in if comment_cmd = "" then cmds else Printf.sprintf "%s\n%s" cmds comment_cmd | children -> (* A node with children *) let children = List.map (fun n -> Vytree.get ct [n]) child_names in let rendered_children = List.map (render_commands ~reftree:reftree ~alwayssort:alwayssort new_path) children in let cmds = String.concat "\n" rendered_children in if comment_cmd = "" then cmds else Printf.sprintf "%s\n%s" cmds comment_cmd end (* Renderer *) let render = Renderer.render let render_at_level ?(indent=4) ?(reftree=None) ?(cmp=BatString.numeric_compare) ?(showephemeral=false) ?(showinactive=false) node path = let node = match path with | [] -> node | _ -> Vytree.get node path in let children = Vytree.children_of_node node in let child_configs = List.map (render ~indent:indent ~reftree:reftree ~cmp:cmp ~showephemeral:showephemeral ~showinactive:showinactive) children in String.concat "\n" child_configs let render_commands ?(reftree=None) ?(alwayssort=false) ?(sortchildren=false) node path = let node = match path with | [] -> node | _ -> Vytree.get node path in let children = if sortchildren then Vytree.sorted_children_of_node (BatString.numeric_compare) node else Vytree.children_of_node node in let commands = List.map (Renderer.render_commands ~reftree:reftree ~alwayssort:alwayssort path) children in String.concat "\n" commands