:- module(scasp_just_html, [ html_justification_tree//2, % :Tree, +Options html_model//2, % :Model, +Options html_bindings//2, % :Bindings, +Options html_program/1, % :Dict html_program//1, % :Dict html_query//2, % :Query, +Options html_predicate//2, % :Predicate, +Options html_rule//2, % :Rule, +Options html_term//2 % +Term, +Options ]). :- use_module(common). :- use_module(output). :- use_module(html_text). :- use_module(messages). :- use_module(source_ref). :- use_module(listing). :- use_module(library(http/html_write)). :- use_module(library(http/term_html)). :- use_module(library(http/html_head), [html_resource/2]). :- if(exists_source(library(http/http_server_files))). :- use_module(library(http/http_server_files), []). :- endif. :- use_module(library(dcg/high_order)). :- use_module(library(lists)). :- use_module(library(option)). :- use_module(library(prolog_code)). :- use_module(library(apply)). :- meta_predicate html_model(:, +, ?, ?), html_justification_tree(:, +, ?, ?), html_program(:), html_program(:, ?, ?), html_query(:, +, ?, ?), html_predicate(:, +, ?, ?), html_rule(:, +, ?, ?). :- multifile user:file_search_path/2. user:file_search_path(js, library(scasp/web/js)). user:file_search_path(css, library(scasp/web/css)). :- html_resource(scasp, [ virtual(true), requires([ jquery, js('scasp.js'), css('scasp.css') ]), ordered(true) ]). /** Render s(CASP) justification as HTML */ %! html_justification_tree(:Tree, +Options)// is det. % % Convert the tree to HTML. The caller should use ovar_analyze_term/1 % on Tree to name variables and identify singletons. This is not done % in this predicate as the user may or may not wish to combine the % variable analysis with the bindings and/or model. Options processed: % % - pred(Boolean) % When `false` (default `true`), ignore user pred/1 rules. % - justify_nmr(Boolean) % When `false` (default `true`), do not omit a justification for % the global constraints. % - source(Boolean) % When `false` (default `true`), do not omit source locations. :- det(html_justification_tree//2). html_justification_tree(M:Tree, Options) --> emit(div(class('scasp-justification'), ul(class('scasp-justification'), \justification_tree(Tree, [ depth(0), module(M) | Options ])))). %! justification_tree(+Tree, +Options)// % % Emit HTML for Tree. Tree is of the format as returned by % justification_tree/3, a term of the shape Atom-ListOfChildren. The % first clause deals with mapping subtrees to human descriptions. The % remainder deals with special cases where there are no global % constraints. normal_justification_tree/2 deals with the general % case. justification_tree(Tree, Options) --> { \+ option(show(machine), Options), option(pred(true), Options, true), option(module(M), Options), human_expression(M:Tree, Children, Actions) }, !, ( {Children == []} -> emit(li([ div(class(node), [ \human_atom(Tree, Actions, Options), \connect(Options) ]) ])) ; { incr_indent(Options, Options1), ( Tree == o_nmr_check -> ExtraClasses = ['scasp-global-constraints'] ; ExtraClasses = [] ) }, emit(li( class([collapsable|ExtraClasses]), [ div(class([node, 'collapsable-header']), [ \human_atom(Tree, Actions, Options), \connector(implies, Options) ]), ul(class('collapsable-content'), \justification_tree_children(Children, Options1)) ])) ). justification_tree(query-[Query,o_nmr_check-[]], Options) --> !, justification_tree(Query, Options), full_stop(Options). justification_tree(query-Children, Options) --> !, justification_tree_children(Children, Options), full_stop(Options). justification_tree(o_nmr_check-[], _Options) --> !. justification_tree(Tree, Options) --> normal_justification_tree(Tree, Options). normal_justification_tree(goal_origin(Term, Origin)-[], Options) --> !, { scasp_source_reference_file_line(Origin, File, Line) }, emit(li([ div(class(node), [ \tree_atom(Term, Options), \origin(File, Line, Options), \connect(Options) ]) ])). normal_justification_tree(Term-[], Options) --> !, emit(li([ div(class(node), [ \tree_atom(Term, Options), \connect(Options) ]) ])). normal_justification_tree(o_nmr_check-_, Options) --> { option(justify_nmr(false), Options) }, !. normal_justification_tree(goal_origin(Term, Origin)-Children, Options) --> { incr_indent(Options, Options1), ( Term == o_nmr_check -> ExtraClasses = ['scasp-global-constraints'] ; ExtraClasses = [] ), scasp_source_reference_file_line(Origin, File, Line) }, !, emit(li(class([collapsable|ExtraClasses]), [ div(class([node, 'collapsable-header']), [ \tree_atom(Term, Options), \connector(implies, [origin(File:Line)|Options]), \origin(File, Line, Options) ]), ul(class('collapsable-content'), \justification_tree_children(Children, Options1)) ])). normal_justification_tree(Term-Children, Options) --> { incr_indent(Options, Options1), ( Term == o_nmr_check -> ExtraClasses = ['scasp-global-constraints'] ; ExtraClasses = [] ) }, emit(li(class([collapsable|ExtraClasses]), [ div(class([node, 'collapsable-header']), [ \tree_atom(Term, Options), \connector(implies, Options) ]), ul(class('collapsable-content'), \justification_tree_children(Children, Options1)) ])). justification_tree_children([A,B|Rs], Options) --> !, justification_tree(A, [connect(and)|Options]), justification_tree_children([B|Rs], Options). justification_tree_children([A], Options) --> justification_tree(A, Options). connect(Options) --> { option(connect(Connector), Options) }, !, connector(Connector, Options). connect(_) --> []. %! human_atom(+Tree, +Human, +Options)// is det. % % @tbd Deal with Human == '', deleting the node. human_atom(Atom-_Children, Actions, Options) --> { css_classes(Options, Classes), scasp_atom_string(Atom, String) }, emit(span(class('scasp-atom'), [ span([class(human), title(String)], span(class(Classes), \actions(Actions, Options))), span(class(machine), \machine_atom(Atom, Options)) ])). tree_atom(Atom, Options) --> { option(show(machine), Options) }, !, emit(span(class(['scasp-atom']), span(class(machine), \machine_atom(Atom, Options)))). tree_atom(Atom, Options) --> { scasp_atom_string(Atom, String) }, emit(span(class(['scasp-atom']), [ span([class(human), title(String)], \atom(Atom, Options)), span(class(machine), \machine_atom(Atom, Options)) ])). scasp_atom_string(goal_origin(Atom, _Origin), String) => scasp_atom_string(Atom, String). scasp_atom_string(Atom, String) => with_output_to(string(String), print_model_term_v(Atom, [])). print_model_term_v(Atom, Options) :- \+ \+ ( inline_constraints(Atom, Options), print_model_term(Atom, Options) ). %! html_model(:Model, +Options)// is det. % % Emit the model as HTML terms. We export the model as a dict with % nested model terms. html_model(M:Model, Options) --> { ( option(class(Class), Options) -> Classes = [Class] ; Classes = [] ), Options1 = [module(M)|Options] }, emit(ul(class(['scasp-model'|Classes]), \sequence(model_term_r(Options1), Model))). model_term_r(Options, Atom) --> { scasp_atom_string(Atom, String) }, emit(li(class(['scasp-atom']), [ span([class(human), title(String)], \atom(Atom, Options)), span(class(machine), \machine_atom(Atom, Options)) ])). %! html_bindings(+Bindings, +Options)// % % Print the variable bindings. html_bindings([], _Options) --> []. html_bindings([H|T], Options) --> ( {T==[]} -> html_binding(H, [last(true)|Options]) ; html_binding(H, Options), html_bindings(T, Options) ). html_binding(Name=Value, Options) --> emit(div(class('scasp-binding'), [ var(Name), ' = ', \html_term(Value, Options), \connect_binding(Options) ])). connect_binding(Options) --> { option(last(true), Options) }, !. connect_binding(_Options) --> emit(','). %! html_program(:Dict) % % html_program(Dict) :- phrase(html_program(Dict), Tokens), print_html(current_output, Tokens). %! html_program(:Dict)// % % Emit the current program in human format using HTML. html_program(M:Dict) --> { Dict1 = Dict.put(module, M) }, html_program_section(query, Dict1), html_program_section(user, Dict1), html_program_section(duals, Dict1), html_program_section(constraints, Dict1), html_program_section(dcc, Dict1). html_program_section(Section, Dict) --> { _{module:M, options:Options} :< Dict, Content = Dict.get(Section), Content \= [], scasp_code_section_title(Section, Default, Title), Opt =.. [Section,true], option(Opt, Options, Default) }, !, html(h2(Title)), ( {Section == query} -> {ovar_set_bindings(Dict.bindings)}, html_query(M:Content, Options) ; sequence(predicate_r(M:Options), Content) ). html_program_section(_, _) --> []. predicate_r(M:Options, Clauses) --> html_predicate(M:Clauses, Options). %! html_query(:Query, +Options)// % % Emit the query. This rule accepts the query both in s(CASP) and % normal Prolog notation. :- det(html_query//2). html_query(M:Query, Options) --> { ( is_list(Query) -> prolog_query(Query, Prolog) ; Prolog = Query ), !, comma_list(Prolog, List0), clean_query(List0, List) }, emit(div(class('scasp-query'), [ div(class(human), [ div(class('scasp-query-title'), 'I would like to know if'), \query_terms(List, [module(M)|Options]) ]), div(class(machine), [ '?- ', \term(Prolog, [numbervars(true)|Options]) ]) ])). html_query(_, _) --> emit(div(class(comment), '% No query')). prolog_query([not(o_false)], _) => fail. prolog_query(List, Query), is_list(List) => clean_query(List, List1), ( List1 == [] -> Query = true ; comma_list(Query, List1) ). clean_query(Q0, Q) :- delete(Q0, o_nmr_check, Q1), delete(Q1, true, Q). query_terms([], Options) --> query_term(true, Options). query_terms([H1,H2|T], Options) --> !, query_term(H1, [connect(and)|Options]), query_terms([H2|T], Options). query_terms([Last], Options) --> { option(end(End), Options, ?) }, query_term(Last, [connect(End)|Options]). query_term(Term, Options) --> emit(div(class('scasp-query-literal'), [ \atom(Term, Options), \connect(Options) ])). %! html_predicate(:Rules, Options)// html_predicate(M:Clauses, Options) --> emit(div(class('scasp-predicate'), \sequence(html_rule_r(M, Options), Clauses))). html_rule_r(M, Options, Clause) --> html_rule(M:Clause, Options). %! html_rule(:Rule, +Options)// html_rule(Rule, Options) --> { ovar_analyze_term(Rule) }, html_rule_(Rule, Options), { ovar_clean(Rule) }. html_rule_(M:(Head0 :- Body), Options) --> !, { MOptions = [module(M)|Options], raise_negation(Head0, Head) }, emit(div(class('scasp-rule'), [ div(class('scasp-head'), [ \atom(Head, MOptions), ', if' ]), div(class('scasp-body'), \html_body(Body, [end(.)|MOptions])) ])). html_rule_(M:Head, Options) --> emit(div(class('scasp-rule'), div(class('scasp-head'), [ \atom(Head, [module(M)|Options]), \connector('.', Options) ]))). html_body(forall(X, not(Goal)), Options) --> !, emit(div(class('scasp-query-literal'), [ 'there exist no ', \html_term(X, Options), ' for which ', \atom(Goal, Options) ])). html_body(Body, Options) --> { comma_list(Body, List0), maplist(raise_negation, List0, List) }, query_terms(List, Options). %! atom(+SCASPAtom, +Options)// % % Emit an s(CASP) atom with annotations as they appear in the model % and justification. atom(Atom, Options) --> { option(pred(true), Options, true), option(module(DefM), Options), option(source_module(M), Options, DefM), human_expression(M:(Atom-[]), [], Actions), !, css_classes(Options, Classes) }, emit(span(class(Classes), \actions(Actions, Options))). atom(not(GlobalConstraint), Options) --> { is_global_constraint(GlobalConstraint, N) }, !, utter(global_constraint(N), Options). atom(not(Term), Options) --> !, utter(not(Term), [class(not)|Options]). atom(-Term, Options) --> !, utter(-(Term), [class(neg)|Options]). atom(proved(Term), Options) --> !, utter(proved(Term), [class(proved)|Options]). atom(assume(Term), Options) --> !, utter(assume(Term), [class(assume)|Options]). atom(chs(Term), Options) --> !, utter(chs(Term), [class(chs)|Options]). atom(abduced(Term), Options) --> !, utter(abduced(Term), [class(abducible)|Options]). atom(M:Term, Options) --> { atom(M) }, !, atom(Term, [module(M)|Options]). atom(o_nmr_check, Options) --> !, utter(global_constraints_hold, Options). atom(is(Value,Expr), Options) --> !, { css_classes(Options, Classes) }, emit(span(class([arithmetic|Classes]), [ \expr(Expr, Options), &(nbsp), is, &(nbsp), \html_term(Value, Options) ])). atom(Comp, Options) --> { human_connector(Comp, Text), !, Comp =.. [_,Left,Right], css_classes(Options, Classes) }, emit(span(class([arithmetic|Classes]), [ \html_term(Left, Options), &(nbsp), Text, &(nbsp), \html_term(Right, Options) ])). atom(Term, Options) --> utter(holds(Term), Options). %! expr(+Term, +Options)// is det. % % Render an expression. % % @tbd Should deal with parenthesis where needed. Possibly it is a % better option to use term//2 from library(http/html_term) and add a % portray hook for that? expr(Term, Options) --> { compound(Term), compound_name_arguments(Term, Op, [Left, Right]) }, !, emit(span([ \expr(Left, Options), &(nbsp), Op, &(nbsp), \expr(Right, Options) ])). expr(Term, Options) --> { compound(Term), compound_name_arguments(Term, Op, [Arg]) }, !, emit(span([ Op, \expr(Arg, Options) ])). expr(Simple, Options) --> html_term(Simple, Options). %! utter(+Expression, +Options) utter(global_constraints_hold, _Options) --> { human_connector(global_constraints_hold, Text) }, emit(Text). utter(global_constraint(N), _Options) --> { human_connector(global_constraint(N), Text) }, emit(Text). utter(not(-(Atom)), Options) --> !, { human_connector(may, Text) }, emit([Text, ' ']), atom(Atom, Options). utter(not(Atom), Options) --> { human_connector(not, Text) }, emit([Text, ' ']), atom(Atom, Options). utter(-(Atom), Options) --> { human_connector(-, Text) }, emit([Text, ' ']), atom(Atom, Options). utter(proved(Atom), Options) --> { human_connector(proved, Text) }, atom(Atom, Options), emit([', ', Text]). utter(chs(Atom), Options) --> { human_connector(chs, Text) }, emit([Text, ' ']), atom(Atom, Options). utter(abduced(Atom), Options) --> { human_connector(abducible, Text) }, emit([Text, ' ']), atom(Atom, Options). utter(according_to(File, Line), _Options) --> { human_connector(according_to, Text) }, emit(span(class('scasp-source-reference'), span(class(human), [' [', Text, ' ~w:~w]'-[File, Line]]))). utter(assume(Atom), Options) --> { human_connector(assume, Text) }, emit([Text, ' ']), atom(Atom, Options). utter(holds(Atom), Options) --> { css_classes(Options, Classes) }, ( { atom(Atom) } -> { human_connector(holds, Text) }, emit([span(class(Classes), Atom), Text]) ; { Atom =.. [Name|Args] } -> { human_connector(holds_for, Text) }, emit([span(class(Classes), Name), Text]), list(Args, Options) ). css_classes(Options, [atom|Classes]) :- findall(Class, member(class(Class), Options), Classes0), ( Classes0 == [] -> Classes = [pos] ; Classes = Classes0 ). :- det(html_term//2). html_term(Var, Options) --> { var(Var) }, !, var(Var, Options). html_term(@(Var:''), Options) --> { var(Var) }, !, var(Var, Options). html_term(@(Var:Type), Options) --> { var(Var) }, !, typed_var(Var, Type, Options). html_term(@(Value:''), Options) --> !, html_term(Value, Options). html_term(@(Value:Type), Options) --> emit('the ~w '-[Type]), !, html_term(Value, Options). html_term(Term, _Options) --> { var_number(Term, _) }, !, emit('~p'-[Term]). html_term('| '(Var, {Constraints}), Options) --> !, inlined_var(Var, Constraints, Options). html_term(Term, _Options) --> { emitting_as(plain) }, !, [ ansi(code, '~p', [Term]) ]. html_term(Term, Options) --> term(Term, [numbervars(true)|Options]). %! var(+Var, +Options)// % % Handle a variable, optionally with constraints and annotated using % ovar_analyze_term/2. var(Var, Options) --> { copy_term(Var, Copy), inline_constraints(Copy, Options), nonvar(Copy), Copy = '| '(V, {Constraints}) }, !, inlined_var(V, Constraints, Options). var(Var, _Options) --> { ovar_var_name(Var, Name) }, !, emit(var(Name)). var(_, _) --> emit(anything). %! inlined_var(+Var, +Constraint, +Options)// % % Deal with constraints as represented after inline_constraints/2. inlined_var(Var, Constraints, Options) --> { Constraints = '\u2209'(Var, List), Var == '$VAR'('_') }, !, ( {List = [One]} -> emit('anything except for '), html_term(One, Options) ; emit('anything except for '), list(List, [last_connector(or)|Options]) ). inlined_var(Var, Constraints, Options) --> { Constraints = '\u2209'(Var, List), compound(Var), Var = '$VAR'(Name) }, !, ( {List = [One]} -> {human_connector(neq, Text)}, emit([var(Name), ' ', Text, ' ']), html_term(One, Options) ; {human_connector(not_in, Text)}, emit([var(Name), ' ', Text, ' ']), list(List, [last_connector(or)|Options]) ). inlined_var(Var, Constraints, Options) --> { comma_list(Constraints, CLPQ) }, clpq(Var, CLPQ, Options). %! clpq(@Var, +Constraints, +Options)// clpq(Var, [Constraint|More], Options) --> { compound(Constraint), Constraint =.. [_,A,B], Var == A, human_connector(Constraint, Text), ( nonvar(Var), Var = '$VAR'(Name) -> Id = var(Name) ; Id = number ) }, emit(['any ', Id, ' ', Text, ' ']), html_term(B, Options), ( {More == []} -> [] ; emit(' and '), clpq_and(More, Var, Options) ). clpq_and([Constraint|More], Var, Options) --> { compound(Constraint), Constraint =.. [_,A,B], A == Var, human_connector(Constraint, Text) }, emit([Text, ' ']), html_term(B, Options), ( {More == []} -> [] ; emit(' and '), clpq_and(More, Var, Options) ). %! typed_var(@Var, +Type, +Options)// typed_var(Var, Type, Options) --> { copy_term(Var, Copy), inline_constraints(Copy, Options), nonvar(Copy), Copy = '| '(V, {Constraints}) }, !, inlined_typed_var(V, Type, Constraints, Options). typed_var(Var, Type, _Options) --> { ovar_var_name(Var, Name) }, !, emit([var(Name), ', a ', Type]). typed_var(_Var, Type, _Options) --> emit(['a ', Type]). inlined_typed_var(Var, Type, Constraints, Options) --> { Constraints = '\u2209'(Var, List), Var == '$VAR'('_') }, !, ( {List = [One]} -> emit(['any ', Type, ' except for ']), html_term(One, Options) ; emit(['any ', Type, ' except for ']), list(List, [last_connector(or)|Options]) ). inlined_typed_var(Var, Type, Constraints, Options) --> { Constraints = '\u2209'(Var, List), nonvar(Var), Var = '$VAR'(Name) }, !, ( {List = [One]} -> emit([var(Name), ', a ', Type, ' other than ']), html_term(One, Options) ; emit([var(Name), ', a ', Type, ' not ']), list(List, [last_connector(or)|Options]) ). inlined_typed_var(Var, Type, Constraints, Options) --> % TBD: include type in NLP { comma_list(Constraints, CLPQ) }, clpq(Var, CLPQ, [type(Type)|Options]). %! list(+Elements, +Options) is det. % % Emit a collection as "a, b, and c" list([L1,L], Options) --> !, { option(last_connector(Conn), Options, and), human_connector(Conn, Text) }, html_term(L1, Options), emit(', ~w '-[Text]), html_term(L, Options). list([H|T], Options) --> html_term(H, Options), ( {T==[]} -> [] ; emit(', '), list(T, Options) ). actions(html(HTML), _) --> !, emit(HTML). actions([], _) --> []. actions([H|T], Options) --> action(H, Options), actions(T, Options). action(text(S), _) --> !, emit(S). action(Term, Options) --> html_term(Term, Options). %! connector(+Meaning, +Options)// % % Emit a logical connector. connector(and, _Options) --> { human_connector(and, Text) }, emit([ span(class(human), [', ', Text]), span(class(machine), ',') ]). connector(not, _Options) --> { human_connector(not, Text) }, emit([ span(class(human), [Text, ' ']), span(class(machine), 'not ') ]). connector(-, _Options) --> { human_connector(-, Text) }, emit([ span(class(human), [Text, ' ']), span(class(machine), '\u00ac ') ]). connector(implies, Options) --> { human_connector(implies, Text) }, emit([ span(class(human), [', ', \origin_annotated(Text, Options)]), span(class(machine), \origin_annotated(' \u2190', Options)) ]). connector(?, _Options) --> { human_connector(?, Text) }, emit([ span(class(human), Text), span(class(machine), '.') ]). connector('.', _Options) --> emit([ span(class('full-stop'), '.') ]). human_connector(Term, Connector) :- phrase(scasp_justification_message(Term), List), ( List = [Connector] -> true ; Connector = List ). full_stop(_Options) --> emit(span(class(machine), '\u220e')). % QED block incr_indent(Options0, [depth(D)|Options2]) :- select_option(depth(D0), Options0, Options1), select_option(connect(_), Options1, Options2, _), D is D0+1. /******************************* * MACHINE HTML * *******************************/ %! machine_atom(+SCASPAtom, +Options)// % % Emit an s(CASP) atom with annotations as they appear in the model % and justification. machine_atom(goal_origin(Term, _Origin), Options) --> !, machine_atom(Term, Options). machine_atom(not(Term), Options) --> !, emit([span(class([connector,not]), not), ' ']), machine_atom(Term, [class(not)|Options]). machine_atom(-Term, Options) --> !, emit([span(class([connector,neg]), '\u00ac'), ' ']), machine_atom(Term, [class(neg)|Options]). machine_atom(proved(Term), Options) --> !, emit([ span(class([connector,proved]), proved), '(', \machine_atom(Term, [class(proved)|Options]), ')' ]). machine_atom(assume(Term), Options) --> !, emit([ span(class([connector,assume]), assume), '(', \machine_atom(Term, [class(assume)|Options]), ')' ]). machine_atom(chs(Term), Options) --> !, emit([ span(class([connector,chs]), chs), '(', \machine_atom(Term, [class(chs)|Options]), ')' ]). machine_atom(M:Term, Options) --> { atom(M) }, !, emit(span(class(module), [M,:])), machine_atom(Term, [module(M)|Options]). machine_atom(Term, Options) --> { css_classes(Options, Classes), merge_options(Options, [numbervars(true)], WOptions) }, emit(span(class(Classes), \term(Term, WOptions))). :- multifile term_html:portray//2. term_html:portray(Term, Options) --> { nonvar(Term), Term = '| '(Var, Constraints) }, term(Var, Options), emit(' | '), term(Constraints, Options). origin(File, Line, Options) --> { option(source(true), Options) }, !, utter(according_to(File, Line), Options). origin(_, _, _) --> []. origin_annotated(Content, Options) --> { option(origin(File:Line), Options) }, !, emit(span([ class(scasp_origin), 'data-file'(File), 'data-line'(Line) ], Content)). origin_annotated(Content, _) --> emit(Content).