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(_,_,_,_))