View source with formatted comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        J.Wielemaker@vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2009-2015, University of Amsterdam
    7                              VU University Amsterdam
    8    All rights reserved.
    9
   10    Redistribution and use in source and binary forms, with or without
   11    modification, are permitted provided that the following conditions
   12    are met:
   13
   14    1. Redistributions of source code must retain the above copyright
   15       notice, this list of conditions and the following disclaimer.
   16
   17    2. Redistributions in binary form must reproduce the above copyright
   18       notice, this list of conditions and the following disclaimer in
   19       the documentation and/or other materials provided with the
   20       distribution.
   21
   22    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   23    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   24    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   25    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   26    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   27    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   28    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   29    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   30    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   31    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   32    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   33    POSSIBILITY OF SUCH DAMAGE.
   34*/
   35
   36:- module(html_head,
   37          [ html_resource/2,            % +Resource, +Attributes
   38            html_requires//1,           % +Resource
   39
   40            html_current_resource/1     % ?Resource
   41          ]).   42:- use_module(library(http/html_write)).   43:- use_module(library(http/mimetype)).   44:- use_module(library(http/http_path)).   45:- use_module(library(error)).   46:- use_module(library(lists)).   47:- use_module(library(occurs)).   48:- use_module(library(option)).   49:- use_module(library(ordsets)).   50:- use_module(library(assoc)).   51:- use_module(library(ugraphs)).   52:- use_module(library(apply)).   53:- use_module(library(debug)).   54
   55
   56/** <module> Automatic inclusion of CSS and scripts links
   57
   58This library allows for  abstract  declaration   of  available  CSS  and
   59Javascript resources and their dependencies using html_resource/2. Based
   60on these declarations, html generating code  can declare that it depends
   61on specific CSS or Javascript functionality,   after  which this library
   62ensures  that  the  proper  links   appear    in   the  HTML  head.  The
   63implementation is based on mail  system   implemented  by html_post/2 of
   64library html_write.pl.
   65
   66Declarations come in two forms. First of all http locations are declared
   67using the http_path.pl library. Second,   html_resource/2 specifies HTML
   68resources to be used in the =head= and their dependencies. Resources are
   69currently limited to Javascript files (.js)  and style sheets (.css). It
   70is  trivial  to  add  support  for  other  material  in  the  head.  See
   71html_include//1.
   72
   73For usage in HTML generation,  there   is  the DCG rule html_requires//1
   74that demands named resources  in  the   HTML  head.
   75
   76## About resource ordering {#html-resource-ordering}
   77
   78All calls to html_requires//1 for the page are collected and duplicates
   79are removed.  Next, the following steps are taken:
   80
   81    1. Add all dependencies to the set
   82    2. Replace multiple members by `aggregate' scripts or css files.
   83       see use_agregates/4.
   84    3. Order all resources by demanding that their dependencies
   85       preceede the resource itself.  Note that the ordering of
   86       resources in the dependency list is *ignored*.  This implies
   87       that if the order matters the dependency list must be split
   88       and only the primary dependency must be added.
   89
   90## Debugging dependencies {#html-resource-debugging}
   91
   92Use ?- debug(html(script)). to  see  the   requested  and  final  set of
   93resources. All declared resources  are   in  html_resource/3. The edit/1
   94command recognises the names of HTML resources.
   95
   96## Predicates {#html-resource-predicates}
   97
   98@tbd    Possibly we should add img//2 to include images from symbolic
   99        path notation.
  100@tbd    It would be nice if the HTTP file server could use our location
  101        declarations.
  102*/
  103
  104:- dynamic
  105    html_resource/3.                % Resource, Source, Properties
  106:- multifile
  107    html_resource/3,
  108    mime_include//2.                % +Mime, +Path
  109
  110%!  html_resource(+About, +Properties) is det.
  111%
  112%   Register an HTML head resource.  About   is  either an atom that
  113%   specifies an HTTP location or  a   term  Alias(Sub).  This works
  114%   similar to absolute_file_name/2.  See   http:location_path/2  for
  115%   details.  Recognised properties are:
  116%
  117%           * requires(+Requirements)
  118%           Other required script and css files.  If this is a plain
  119%           file name, it is interpreted relative to the declared
  120%           resource.  Requirements can be a list, which is equivalent
  121%           to multiple requires properties.
  122%
  123%           * virtual(+Bool)
  124%           If =true= (default =false=), do not include About itself,
  125%           but only its dependencies.  This allows for defining an
  126%           alias for one or more resources.
  127%
  128%           * ordered(+Bool)
  129%           Defines that the list of requirements is ordered, which
  130%           means that each requirement in the list depends on its
  131%           predecessor.
  132%
  133%           * aggregate(+List)
  134%           States that About is an aggregate of the resources in
  135%           List. This means that if both About and one of the
  136%           elements of List appears in the dependencies, About
  137%           is kept and the smaller one is dropped. If there are a
  138%           number of dependencies on the small members, these are
  139%           replaced with dependency on the big (aggregate) one,
  140%           for example, to specify that a big javascript is
  141%           actually the composition of a number of smaller ones.
  142%
  143%           * mime_type(-Mime)
  144%           May be specified for non-virtual resources to specify
  145%           the mime-type of the resource.  By default, the mime
  146%           type is derived from the file name using
  147%           file_mime_type/2.
  148%
  149%   Registering the same About multiple times extends the properties
  150%   defined  for  About.  In  particular,  this  allows  for  adding
  151%   additional dependencies to a (virtual) resource.
  152
  153html_resource(About, Properties) :-
  154    assert_resource(About, -, Properties).
  155
  156assert_resource(About, Location, Properties) :-
  157    retractall(html_resource(About, _, _)),
  158    assert(html_resource(About, Location, Properties)),
  159    clean_cache(About, Properties).
  160
  161system:term_expansion((:-html_resource(About, Properties)),
  162                      html_head:html_resource(About, File:Line, Properties)) :-
  163    source_location(File, Line),
  164    clean_cache(About, Properties).
  165
  166clean_cache(_About, Properties) :-
  167    clean_same_about_cache,
  168    (   memberchk(aggregate(_), Properties)
  169    ->  clean_aggregate_cache
  170    ;   true
  171    ).
  172
  173
  174%!  html_current_resource(?About) is nondet.
  175%
  176%   True when About is a currently known resource.
  177
  178html_current_resource(About) :-
  179    (   ground(About)
  180    ->  html_resource(About, _, _), !
  181    ;   html_resource(About, _, _)
  182    ).
  183
  184
  185%!  html_requires(+ResourceOrList)// is det.
  186%
  187%   Include ResourceOrList and all dependencies  derived from it and
  188%   add them to the  HTML  =head=   using  html_post/2.  The  actual
  189%   dependencies are computed  during  the   HTML  output  phase  by
  190%   html_insert_resource//1.
  191
  192html_requires(Required) -->
  193    html_post(head, 'html required'(Required)).
  194
  195:- multifile
  196    html_write:html_head_expansion/2.  197
  198html_write:html_head_expansion(In, Out) :-
  199    require_commands(In, Required, Rest),
  200    Required \== [],
  201    !,
  202    flatten(Required, Plain),
  203    Out = [ html_head:(\html_insert_resource(Plain))
  204          | Rest
  205          ].
  206
  207require_commands([], [], []).
  208require_commands([_:('html required'(Required))|T0], [Required|TR], R) :-
  209    !,
  210    require_commands(T0, TR, R).
  211require_commands([R|T0], TR, [R|T]) :-
  212    !,
  213    require_commands(T0, TR, T).
  214
  215
  216%!  html_insert_resource(+ResourceOrList)// is det.
  217%
  218%   Actually   include   HTML   head   resources.   Called   through
  219%   html_post//2   from   html_requires//1   after     rewrite    by
  220%   html_head_expansion/2. We are guaranteed we   will  only get one
  221%   call that is passed a flat   list  of requested requirements. We
  222%   have three jobs:
  223%
  224%       1. Figure out all indirect requirements
  225%       2. See whether we can use any `aggregate' resources
  226%       3. Put required resources before their requiree.
  227
  228                % called from html_write:html_head_expansion/2
  229:- public html_insert_resource//1.  230
  231html_insert_resource(Required) -->
  232    { requirements(Required, Paths),
  233      debug(html(script), 'Requirements: ~q~nFinal: ~q', [Required, Paths])
  234    },
  235    html_include(Paths).
  236
  237requirements(Required, Paths) :-
  238    phrase(requires(Required), List),
  239    sort(List, Paths0),             % remove duplicates
  240    use_agregates(Paths0, Paths1, AggregatedBy),
  241    order_html_resources(Paths1, AggregatedBy, Paths2),
  242    exclude(virtual, Paths2, Paths).
  243
  244virtual('V'(_)).
  245
  246%!  use_agregates(+Paths, -Aggregated, -AggregatedBy) is det.
  247%
  248%   Try to replace sets of  resources   by  an  `aggregate', a large
  249%   javascript or css file that  combines   the  content of multiple
  250%   small  ones  to  reduce  the  number   of  files  that  must  be
  251%   transferred to the client. The current rule says that aggregates
  252%   are used if at least half of the members are used.
  253
  254use_agregates(Paths, Aggregated, AggregatedBy) :-
  255    empty_assoc(AggregatedBy0),
  256    use_agregates(Paths, Aggregated, AggregatedBy0, AggregatedBy).
  257
  258use_agregates(Paths, Aggregated, AggregatedBy0, AggregatedBy) :-
  259    current_aggregate(Aggregate, Parts, Size),
  260    ord_subtract(Paths, Parts, NotCovered),
  261    length(Paths, Len0),
  262    length(NotCovered, Len1),
  263    Covered is Len0-Len1,
  264    Covered >= Size/2,
  265    !,
  266    ord_add_element(NotCovered, Aggregate, NewPaths),
  267    add_aggregated_by(Parts, AggregatedBy0, Aggregate, AggregatedBy1),
  268    use_agregates(NewPaths, Aggregated, AggregatedBy1, AggregatedBy).
  269use_agregates(Paths, Paths, AggregatedBy, AggregatedBy).
  270
  271add_aggregated_by([], Assoc, _, Assoc).
  272add_aggregated_by([H|T], Assoc0, V, Assoc) :-
  273    put_assoc(H, Assoc0, V, Assoc1),
  274    add_aggregated_by(T, Assoc1, V, Assoc).
  275
  276
  277:- dynamic
  278    aggregate_cache_filled/0,
  279    aggregate_cache/3.  280:- volatile
  281    aggregate_cache_filled/0,
  282    aggregate_cache/3.  283
  284clean_aggregate_cache :-
  285    retractall(aggregate_cache_filled).
  286
  287%!  current_aggregate(-Aggregate, -Parts, -Size) is nondet.
  288%
  289%   True if Aggregate is a defined   aggregate  with Size Parts. All
  290%   parts are canonical absolute HTTP locations  and Parts is sorted
  291%   to allow for processing using ordered set predicates.
  292
  293current_aggregate(Path, Parts, Size) :-
  294    aggregate_cache_filled,
  295    !,
  296    aggregate_cache(Path, Parts, Size).
  297current_aggregate(Path, Parts, Size) :-
  298    retractall(aggregate_cache(_,_, _)),
  299    forall(uncached_aggregate(Path, Parts, Size),
  300           assert(aggregate_cache(Path, Parts, Size))),
  301    assert(aggregate_cache_filled),
  302    aggregate_cache(Path, Parts, Size).
  303
  304uncached_aggregate(Path, APartsS, Size) :-
  305    html_resource(Aggregate, _, Properties),
  306    memberchk(aggregate(Parts), Properties),
  307    http_absolute_location(Aggregate, Path, []),
  308    absolute_paths(Parts, Path, AParts),
  309    sort(AParts, APartsS),
  310    length(APartsS, Size).
  311
  312absolute_paths([], _, []).
  313absolute_paths([H0|T0], Base, [H|T]) :-
  314    http_absolute_location(H0, H, [relative_to(Base)]),
  315    absolute_paths(T0, Base, T).
  316
  317
  318%!  requires(+Spec)// is det.
  319%!  requires(+Spec, +Base)// is det.
  320%
  321%   True if Files is the set of  files   that  need to be loaded for
  322%   Spec. Note that Spec normally appears in  Files, but this is not
  323%   necessary (i.e. virtual resources  or   the  usage  of aggregate
  324%   resources).
  325
  326requires(Spec) -->
  327    requires(Spec, /).
  328
  329requires([], _) -->
  330    !,
  331    [].
  332requires([H|T], Base) -->
  333    !,
  334    requires(H, Base),
  335    requires(T, Base).
  336requires(Spec, Base) -->
  337    requires(Spec, Base, _, true).
  338
  339requires('V'(Spec), Base, Properties, Virtual) -->
  340    { nonvar(Spec) },
  341    !,
  342    requires(Spec, Base, Properties, Virtual).
  343requires(Spec, Base, Properties, Virtual) -->
  344    { res_properties(Spec, Properties),
  345      http_absolute_location(Spec, File, [relative_to(Base)])
  346    },
  347    (   { option(virtual(true), Properties)
  348        ; Virtual == false
  349        }
  350    ->  ['V'(Spec)]
  351    ;   [File]
  352    ),
  353    requires_from_properties(Properties, File).
  354
  355
  356requires_from_properties([], _) -->
  357    [].
  358requires_from_properties([H|T], Base) -->
  359    requires_from_property(H, Base),
  360    requires_from_properties(T, Base).
  361
  362requires_from_property(requires(What), Base) -->
  363    !,
  364    requires(What, Base).
  365requires_from_property(_, _) -->
  366    [].
  367
  368
  369%!  order_html_resources(+Requirements, +AggregatedBy, -Ordered) is det.
  370%
  371%   Establish a proper order for the   collected (sorted and unique)
  372%   list of Requirements.
  373
  374order_html_resources(Requirements, AggregatedBy, Ordered) :-
  375    requirements_graph(Requirements, AggregatedBy, Graph),
  376    (   top_sort(Graph, Ordered)
  377    ->  true
  378    ;   connect_graph(Graph, Start, Connected),
  379        top_sort(Connected, Ordered0),
  380        Ordered0 = [Start|Ordered]
  381    ).
  382
  383%!  requirements_graph(+Requirements, +AggregatedBy, -Graph) is det.
  384%
  385%   Produce an S-graph (see library(ugraphs))   that  represents the
  386%   dependencies  in  the  list  of  Requirements.  Edges  run  from
  387%   required to requirer.
  388
  389requirements_graph(Requirements, AggregatedBy, Graph) :-
  390    phrase(prerequisites(Requirements, AggregatedBy, Vertices, []), Edges),
  391    vertices_edges_to_ugraph(Vertices, Edges, Graph).
  392
  393prerequisites([], _, Vs, Vs) -->
  394    [].
  395prerequisites([R|T], AggregatedBy, Vs, Vt) -->
  396    prerequisites_for(R, AggregatedBy, Vs, Vt0),
  397    prerequisites(T, AggregatedBy, Vt0, Vt).
  398
  399prerequisites_for(R, AggregatedBy, Vs, Vt) -->
  400    { phrase(requires(R, /, Properties, true), Req0),
  401      delete(Req0, R, Req)
  402    },
  403    prop_edges(Properties),
  404    (   {Req == []}
  405    ->  {Vs = [R|Vt]}
  406    ;   req_edges(Req, AggregatedBy, R),
  407        {Vs = Vt}
  408    ).
  409
  410req_edges([], _, _) -->
  411    [].
  412req_edges([H|T], AggregatedBy, R) -->
  413    (   { get_assoc(H, AggregatedBy, Aggregate) }
  414    ->  [Aggregate-R]
  415    ;   [H-R]
  416    ),
  417    req_edges(T, AggregatedBy, R).
  418
  419%!  prop_edges(+Properties)//
  420%
  421%   Subscribes a list of dependencies   from  resources that declare
  422%   their requirements with ordered(true).
  423
  424prop_edges(Properties) -->
  425    { option(ordered(true), Properties) },
  426    !,
  427    ordered_reqs(Properties).
  428prop_edges(_) --> [].
  429
  430ordered_reqs([]) --> [].
  431ordered_reqs([H|T]) --> ordered_req(H), ordered_reqs(T).
  432
  433ordered_req(requires([H|T])) -->
  434    { T \== [],
  435      !,
  436      absolute_req(H, File)
  437    },
  438    order_pairs(T, File).
  439ordered_req(_) --> [].
  440
  441order_pairs([H|T], P) -->
  442    !,
  443    { absolute_req(H, File)
  444    },
  445    [ P-File ],
  446    order_pairs(T, File).
  447order_pairs(_, _) --> [].
  448
  449absolute_req(Virtual, Abs) :-
  450    html_resource(Virtual, _, Properties),
  451    option(virtual(true), Properties),
  452    !,
  453    Abs = 'V'(Virtual).
  454absolute_req(Spec, Abs) :-
  455    http_absolute_location(Spec, Abs, [relative_to(/)]).
  456
  457
  458%!  connect_graph(+Graph, -Start, -Connected) is det.
  459%
  460%   Turn Graph into a connected graph   by putting a shared starting
  461%   point before all vertices.
  462
  463connect_graph([], 0, []) :- !.
  464connect_graph(Graph, Start, [Start-Vertices|Graph]) :-
  465    vertices(Graph, Vertices),
  466    Vertices = [First|_],
  467    before(First, Start).
  468
  469%!  before(+Term, -Before) is det.
  470%
  471%   Unify Before to a term that comes   before  Term in the standard
  472%   order of terms.
  473%
  474%   @error instantiation_error if Term is unbound.
  475
  476before(X, _) :-
  477    var(X),
  478    !,
  479    instantiation_error(X).
  480before(Number, Start) :-
  481    number(Number),
  482    !,
  483    Start is Number - 1.
  484before(_, 0).
  485
  486
  487%!  res_properties(+Spec, -Properties) is det.
  488%
  489%   True if Properties is the set of defined properties on Spec.
  490
  491res_properties(Spec, Properties) :-
  492    findall(P, res_property(Spec, P), Properties0),
  493    list_to_set(Properties0, Properties).
  494
  495res_property(Spec, Property) :-
  496    same_about(Spec, About),
  497    html_resource(About, _, Properties),
  498    member(Property, Properties).
  499
  500:- dynamic
  501    same_about_cache/2.  502:- volatile
  503    same_about_cache/2.  504
  505clean_same_about_cache :-
  506    retractall(same_about_cache(_,_)).
  507
  508same_about(Spec, About) :-
  509    same_about_cache(Spec, Same),
  510    !,
  511    member(About, Same).
  512same_about(Spec, About) :-
  513    findall(A, uncached_same_about(Spec, A), List),
  514    assert(same_about_cache(Spec, List)),
  515    member(About, List).
  516
  517uncached_same_about(Spec, About) :-
  518    html_resource(About, _, _),
  519    same_resource(Spec, About).
  520
  521
  522%!  same_resource(+R1, +R2) is semidet.
  523%
  524%   True if R1 an R2 represent  the   same  resource.  R1 and R2 are
  525%   resource specifications are defined by http_absolute_location/3.
  526
  527same_resource(R, R) :- !.
  528same_resource(R1, R2) :-
  529    resource_base_name(R1, B),
  530    resource_base_name(R2, B),
  531    http_absolute_location(R1, Path, []),
  532    http_absolute_location(R2, Path, []).
  533
  534:- dynamic
  535    base_cache/2.  536:- volatile
  537    base_cache/2.  538
  539resource_base_name(Spec, Base) :-
  540    (   base_cache(Spec, Base0)
  541    ->  Base = Base0
  542    ;   uncached_resource_base_name(Spec, Base0),
  543        assert(base_cache(Spec, Base0)),
  544        Base = Base0
  545    ).
  546
  547uncached_resource_base_name(Atom, Base) :-
  548    atomic(Atom),
  549    !,
  550    file_base_name(Atom, Base).
  551uncached_resource_base_name(Compound, Base) :-
  552    arg(1, Compound, Base0),
  553    file_base_name(Base0, Base).
  554
  555%!  html_include(+PathOrList)// is det.
  556%
  557%   Include to HTML resources  that  must   be  in  the  HTML <head>
  558%   element. Currently onlu supports  =|.js|=   and  =|.css|= files.
  559%   Extend this to support more  header   material.  Do not use this
  560%   predicate directly. html_requires//1 is the  public interface to
  561%   include HTML resources.
  562%
  563%   @param  HTTP location or list of these.
  564
  565html_include([]) --> !.
  566html_include([H|T]) -->
  567    !,
  568    html_include(H),
  569    html_include(T).
  570html_include(Path) -->
  571    { res_property(Path, mime_type(Mime))
  572    },
  573    !,
  574    html_include(Mime, Path).
  575html_include(Path) -->
  576    { file_mime_type(Path, Mime) },
  577    !,
  578    html_include(Mime, Path).
  579
  580html_include(Mime, Path) -->
  581    mime_include(Mime, Path),
  582    !.    % user hook
  583html_include(text/css, Path) -->
  584    !,
  585    html(link([ rel(stylesheet),
  586                type('text/css'),
  587                href(Path)
  588              ], [])).
  589html_include(text/javascript, Path) -->
  590    !,
  591    html(script([ type('text/javascript'),
  592                  src(Path)
  593                ], [])).
  594html_include(Mime, Path) -->
  595    { print_message(warning, html_include(dont_know, Mime, Path))
  596    }.
  597
  598%!  mime_include(+Mime, +Path)// is semidet.
  599%
  600%   Hook called to include a link to   an HTML resource of type Mime
  601%   into the HTML head. The Mime type   is  computed from Path using
  602%   file_mime_type/2. If the hook  fails,   two  built-in  rules for
  603%   `text/css` and `text/javascript` are  tried.   For  example,  to
  604%   include a =.pl= files as a Prolog script, use:
  605%
  606%     ```
  607%     :- multifile
  608%         html_head:mime_include//2.
  609%
  610%     html_head:mime_include(text/'x-prolog', Path) --> !,
  611%         html(script([ type('text/x-prolog'),
  612%                       src(Path)
  613%                     ],  [])).
  614%
  615%     ```
  616
  617                 /*******************************
  618                 *        CACHE CLEANUP         *
  619                 *******************************/
  620
  621:- multifile
  622    user:message_hook/3,
  623    prolog:message//1.  624:- dynamic
  625    user:message_hook/3.  626
  627user:message_hook(load_file(done(_Nesting, _File, _Action,
  628                                 _Module, _Time, _Clauses)),
  629                  _Level, _Lines) :-
  630    clean_same_about_cache,
  631    clean_aggregate_cache,
  632    fail.
  633
  634prolog:message(html_include(dont_know, Mime, Path)) -->
  635    [ 'Don\'t know how to include resource ~q (mime-type ~q)'-
  636      [Path, Mime]
  637    ].
  638
  639
  640                 /*******************************
  641                 *             EDIT             *
  642                 *******************************/
  643
  644% Allow edit(Location) to edit the :- html_resource declaration.
  645:- multifile
  646    prolog_edit:locate/3.  647
  648prolog_edit:locate(Path, html_resource(Spec), [file(File), line(Line)]) :-
  649    atom(Path),
  650    html_resource(Spec, File:Line, _Properties),
  651    sub_term(Path, Spec)