View source with formatted comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker, Michiel Hildebrand
    4    E-mail:        J.Wielemaker@uva.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2010-2025, University of Amsterdam
    7                              VU University Amsterdam
    8                              SWI-Prolog Solutions b.v.
    9    All rights reserved.
   10
   11    Redistribution and use in source and binary forms, with or without
   12    modification, are permitted provided that the following conditions
   13    are met:
   14
   15    1. Redistributions of source code must retain the above copyright
   16       notice, this list of conditions and the following disclaimer.
   17
   18    2. Redistributions in binary form must reproduce the above copyright
   19       notice, this list of conditions and the following disclaimer in
   20       the documentation and/or other materials provided with the
   21       distribution.
   22
   23    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   24    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   25    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   26    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   27    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   28    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   29    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   30    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   31    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   32    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   33    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   34    POSSIBILITY OF SUCH DAMAGE.
   35*/
   36
   37:- module(javascript,
   38          [ js_script//1,               % +Content
   39
   40            js_call//1,                 % +Function(Arg..)
   41            js_new//2,                  % +Id, +Function(+Args)
   42            js_expression//1,           % +Expression
   43            js_arg_list//1,             % +ListOfExpressions
   44            js_arg//1,                  % +Arg
   45            js_args//1,                 % +Args
   46
   47            javascript/4                % Quasi Quotation handler
   48          ]).   49
   50:- use_module(library(http/html_write)).   51:- use_module(library(json)).   52:- use_module(library(apply)).   53:- use_module(library(error)).   54:- use_module(library(lists)).   55:- use_module(library(debug)).   56:- use_module(library(quasi_quotations)).   57:- use_module(library(dcg/basics)).   58:- use_module(library(json_grammar)).   59
   60:- html_meta
   61    js_script(html, ?, ?).   62
   63:- quasi_quotation_syntax(javascript).   64
   65/** <module> Utilities for including JavaScript
   66
   67This library is a supplement   to library(http/html_write) for producing
   68JavaScript fragments. Its main role is  to   be  able to call JavaScript
   69functions  with  valid  arguments  constructed  from  Prolog  data.  For
   70example, suppose you want to call a   JavaScript  functions to process a
   71list of names represented as Prolog atoms.   This  can be done using the
   72call below, while without this library you   would have to be careful to
   73properly escape special characters.
   74
   75    ==
   76    numbers_script(Names) -->
   77        html(script(type('text/javascript'),
   78             [ \js_call('ProcessNumbers'(Names)
   79             ]),
   80    ==
   81
   82The accepted arguments are described with js_expression//1.
   83*/
   84
   85%!  js_script(+Content)// is det.
   86%
   87%   Generate a JavaScript =script= element with the given content.
   88
   89js_script(Content) -->
   90    html(script(type('text/javascript'),
   91                Content)).
   92
   93
   94                 /*******************************
   95                 *        QUASI QUOTATION       *
   96                 *******************************/
   97
   98%!  javascript(+Content, +Vars, +VarDict, -DOM) is det.
   99%
  100%   Quasi quotation parser for JavaScript  that allows for embedding
  101%   Prolog variables to substitude _identifiers_   in the JavaScript
  102%   snippet. Parameterizing a JavaScript string   is  achieved using
  103%   the JavaScript `+` operator, which   results in concatenation at
  104%   the client side.
  105%
  106%     ==
  107%         ...,
  108%         js_script({|javascript(Id, Config)||
  109%                     $(document).ready(function() {
  110%                        $("#"+Id).tagit(Config);
  111%                      });
  112%                    |}),
  113%         ...
  114%     ==
  115%
  116%   The current implementation tokenizes the   JavaScript  input and
  117%   yields syntax errors on unterminated  comments, strings, etc. No
  118%   further parsing is  implemented,  which   makes  it  possible to
  119%   produce syntactically incorrect and   partial JavaScript. Future
  120%   versions are likely to include a  full parser, generating syntax
  121%   errors.
  122%
  123%   The parser produces a  term  `\List`,   which  is  suitable  for
  124%   js_script//1 and html//1.  Embedded  variables   are  mapped  to
  125%   `\js_expression(Var)`, while the remaining  text   is  mapped to
  126%   atoms.
  127%
  128%   @tbd    Implement a full JavaScript parser. Users should _not_
  129%           rely on the ability to generate partial JavaScript
  130%           snippets.
  131
  132javascript(Content, Vars, Dict, \Parts) :-
  133    include(qq_var(Vars), Dict, QQDict),
  134    phrase_from_quasi_quotation(
  135        js(QQDict, Parts),
  136        Content).
  137
  138qq_var(Vars, _=Var) :-
  139    member(V, Vars),
  140    V == Var,
  141    !.
  142
  143js(Dict, [Pre, Subst|More]) -->
  144    here(Here0),
  145    js_tokens(_),
  146    here(Here1),
  147    json_token(identifier(Name)),
  148    { memberchk(Name=Var, Dict),
  149      !,
  150      Subst = \js_expression(Var),
  151      diff_to_atom(Here0, Here1, Pre)
  152    },
  153    js(Dict, More).
  154js(_, [Last]) -->
  155    string(Codes),
  156    \+ [_],
  157    !,
  158    { atom_codes(Last, Codes) }.
  159
  160js_tokens([]) --> [].
  161js_tokens([H|T]) -->
  162    json_token(H),
  163    js_tokens(T).
  164
  165
  166%       diff_to_atom(+Start, +End, -Atom)
  167%
  168%       True when Atom is an atom that represents the characters between
  169%       Start and End, where End must be in the tail of the list Start.
  170
  171diff_to_atom(Start, End, Atom) :-
  172    diff_list(Start, End, List),
  173    atom_codes(Atom, List).
  174
  175diff_list(Start, End, List) :-
  176    Start == End,
  177    !,
  178    List = [].
  179diff_list([H|Start], End, [H|List]) :-
  180    diff_list(Start, End, List).
  181
  182here(Here, Here, Here).
  183
  184
  185                 /*******************************
  186                 *     PROLOG --> JAVASCRIPT    *
  187                 *******************************/
  188
  189%!  js_call(+Term)// is det.
  190%
  191%   Emit a call to a Javascript function.  The Prolog functor is the
  192%   name of the function. The arguments are converted from Prolog to
  193%   JavaScript using js_arg_list//1. Please not that Prolog functors can
  194%   be quoted atom and thus the following is legal:
  195%
  196%       ==
  197%           ...
  198%           html(script(type('text/javascript'),
  199%                [ \js_call('x.y.z'(hello, 42))
  200%                ]),
  201%       ==
  202
  203js_call(Term) -->
  204    { Term =.. [Function|Args] },
  205    html(Function), js_arg_list(Args), [';\n'].
  206
  207
  208%!  js_new(+Id, +Term)// is det.
  209%
  210%   Emit a call to a Javascript object declaration. This is the same
  211%   as:
  212%
  213%       ==
  214%       ['var ', Id, ' = new ', \js_call(Term)]
  215%       ==
  216
  217
  218js_new(Id, Term) -->
  219    { Term =.. [Function|Args] },
  220    html(['var ', Id, ' = new ', Function]), js_arg_list(Args), [';\n'].
  221
  222%!  js_arg_list(+Expressions:list)// is det.
  223%
  224%   Write javascript (function) arguments.  This   writes  "(", Arg,
  225%   ..., ")".  See js_expression//1 for valid argument values.
  226
  227
  228js_arg_list(Args) -->
  229    ['('], js_args(Args), [')'].
  230
  231js_args([]) -->
  232    [].
  233js_args([H|T]) -->
  234    js_expression(H),
  235    (   { T == [] }
  236    ->  []
  237    ;   html(', '),
  238        js_args(T)
  239    ).
  240
  241%!  js_expression(+Expression)// is det.
  242%
  243%   Emit a single JSON argument.  Expression is one of:
  244%
  245%       $ Variable :
  246%       Emitted as Javascript =null=
  247%       $ List :
  248%       Produces a Javascript list, where each element is processed
  249%       by this library.
  250%       $ object(Attributes) :
  251%       Where Attributes is a Key-Value list where each pair can be
  252%       written as Key-Value, Key=Value or Key(Value), accommodating
  253%       all common constructs for this used in Prolog.<
  254%       $ { K:V, ... }
  255%       Same as object(Attributes), providing a more JavaScript-like
  256%       syntax.  This may be useful if the object appears literally
  257%       in the source-code, but is generally less friendly to produce
  258%       as a result from a computation.
  259%       $ Dict :
  260%       Emit a dict as a JSON object using json_write_dict/3.
  261%       $ json(Term) :
  262%       Emits a term using json_write/3.
  263%       $ @(Atom) :
  264%       Emits these constants without quotes.  Normally used for the
  265%       symbols =true=, =false= and =null=, but can also be use for
  266%       emitting JavaScript symbols (i.e. function- or variable
  267%       names).
  268%       $ Number :
  269%       Emitted literally
  270%       $ symbol(Atom) :
  271%       Synonym for @(Atom).  Deprecated.
  272%       $ Atom or String :
  273%       Emitted as quoted JavaScript string.
  274
  275js_expression(Expr) -->
  276    js_arg(Expr),
  277    !.
  278js_expression(Expr) -->
  279    { type_error(js(expression), Expr) }.
  280
  281%!  js_arg(+Expression)// is semidet.
  282%
  283%   Same as js_expression//1, but fails if Expression is invalid,
  284%   where js_expression//1 raises an error.
  285%
  286%   @deprecated     New code should use js_expression//1.
  287
  288js_arg(H) -->
  289    { var(H) },
  290    !,
  291    [null].
  292js_arg(object(H)) -->
  293    { is_list(H) },
  294    !,
  295    html([ '{', \js_kv_list(H), '}' ]).
  296js_arg({}(Attrs)) -->
  297    !,
  298    html([ '{', \js_kv_cslist(Attrs), '}' ]).
  299js_arg(@(Id)) --> js_identifier(Id).
  300js_arg(symbol(Id)) --> js_identifier(Id).
  301js_arg(json(Term)) -->
  302    { json_to_string(json(Term), String),
  303      debug(json_arg, '~w~n', String)
  304    },
  305    [ String ].
  306js_arg(Dict) -->
  307    { is_dict(Dict),
  308      !,
  309      with_output_to(string(String),
  310                     json_write_dict(current_output, Dict, [width(0)]))
  311    },
  312    [ String ].
  313js_arg(H) -->
  314    { is_list(H) },
  315    !,
  316    html([ '[', \js_args(H), ']' ]).
  317js_arg(H) -->
  318    { number(H) },
  319    !,
  320    [H].
  321js_arg(H) -->
  322    { atomic(H),
  323      !,
  324      js_quoted_string(H, Q)
  325    },
  326    [ '"', Q, '"'
  327    ].
  328
  329js_kv_list([]) --> [].
  330js_kv_list([H|T]) -->
  331    (   js_kv(H)
  332    ->  (   { T == [] }
  333        ->  []
  334        ;   html(', '),
  335            js_kv_list(T)
  336        )
  337    ;   { type_error(javascript_key_value, H) }
  338    ).
  339
  340js_kv(Key:Value) -->
  341    !,
  342    js_key(Key), [:], js_expression(Value).
  343js_kv(Key-Value) -->
  344    !,
  345    js_key(Key), [:], js_expression(Value).
  346js_kv(Key=Value) -->
  347    !,
  348    js_key(Key), [:], js_expression(Value).
  349js_kv(Term) -->
  350    { compound(Term),
  351      Term =.. [Key,Value]
  352    },
  353    !,
  354    js_key(Key), [:], js_expression(Value).
  355
  356js_key(Key) -->
  357    (   { must_be(atom, Key),
  358          js_identifier(Key)
  359        }
  360    ->  [Key]
  361    ;   { js_quoted_string(Key, QKey) },
  362        html(['\'', QKey, '\''])
  363    ).
  364
  365js_kv_cslist((A,B)) -->
  366    !,
  367    js_kv(A),
  368    html(', '),
  369    js_kv_cslist(B).
  370js_kv_cslist(A) -->
  371    js_kv(A).
  372
  373%!  js_quoted_string(+Raw, -Quoted)
  374%
  375%   Quote text for use in JavaScript.  Quoted does _not_ include the
  376%   leading and trailing quotes.
  377%
  378%   @tbd    Join with json stuff.
  379
  380js_quoted_string(Raw, Quoted) :-
  381    atom_codes(Raw, Codes),
  382    phrase(js_quote_codes(Codes), QuotedCodes),
  383    atom_codes(Quoted, QuotedCodes).
  384
  385js_quote_codes([]) -->
  386    [].
  387js_quote_codes([0'\r,0'\n|T]) -->
  388    !,
  389    "\\n",
  390    js_quote_codes(T).
  391js_quote_codes([0'<,0'/|T]) -->        % Avoid XSS scripting hacks
  392    !,
  393    "<\\/",
  394    js_quote_codes(T).
  395js_quote_codes([H|T]) -->
  396    js_quote_code(H),
  397    js_quote_codes(T).
  398
  399js_quote_code(0'') -->
  400    !,
  401    "\\'".
  402js_quote_code(0'") -->
  403    !,
  404    "\\\"".
  405js_quote_code(0'\\) -->
  406    !,
  407    "\\\\".
  408js_quote_code(0'\n) -->
  409    !,
  410    "\\n".
  411js_quote_code(0'\r) -->
  412    !,
  413    "\\r".
  414js_quote_code(0'\t) -->
  415    !,
  416    "\\t".
  417js_quote_code(C) -->
  418    [C].
  419
  420%!  js_identifier(+Id:atom)// is det.
  421%
  422%   Emit an identifier if it is a valid one
  423
  424js_identifier(Id) -->
  425    { must_be(atom, Id),
  426      js_identifier(Id)
  427    },
  428    !,
  429    [ Id ].
  430js_identifier(Id) -->
  431    { domain_error(js(identifier), Id)
  432    }.
  433
  434%!  js_identifier(+Id:atom) is semidet.
  435%
  436%   True if Id is a  valid   identifier.  In traditional JavaScript,
  437%   this means it starts  with  [$_:letter:]   and  is  followed  by
  438%   [$_:letter:digit:]
  439
  440js_identifier(Id) :-
  441    sub_atom(Id, 0, 1, _, First),
  442    char_type(First, csymf),
  443    forall(sub_atom(Id, _, 1, _, Char), char_type(Char, csym)).
  444
  445
  446%!  json_to_string(+JSONTerm, -String)
  447%
  448%   Write JSONTerm to String.
  449
  450json_to_string(JSON, String) :-
  451    with_output_to(string(String),
  452                   json_write(current_output,JSON,[width(0)])).
  453
  454
  455                /*******************************
  456                *           SANDBOX            *
  457                *******************************/
  458
  459:- multifile sandbox:safe_primitive/1.  460
  461sandbox:safe_primitive(javascript:javascript(_,_,_,_))