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