View source with formatted comments or as raw
    1/*  Part of SWISH
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        J.Wielemaker@vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2014-2018, VU University Amsterdam
    7			      CWI, 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(swish_examples, []).   37:- use_module(library(http/http_dispatch)).   38:- use_module(library(http/http_json)).   39:- use_module(library(http/json)).   40:- use_module(library(http/http_path)).   41:- use_module(library(filesex)).   42:- use_module(library(apply)).   43:- use_module(library(option)).   44:- use_module(library(lists)).   45:- if(exists_source(library(atom))).   46:- use_module(library(atom)).   47:- endif.   48
   49:- use_module(storage).   50:- use_module(md_eval).   51
   52/** <module> Serve example files
   53
   54Locate and serve files for  the  _Examples_   menu  as  well as examples
   55included from overview notebooks. The examples come from two sources:
   56
   57  - Prolog files in the file search path `examples`.  Such files are
   58    distributed with SWISH.
   59  - Gitty files marked as `example`.  Such files can be created by the
   60    users.
   61
   62This  module  also  makes   the    known   examples   available  through
   63swish_provides/1  for  supporting  conditional   statements  on  example
   64overview notebooks.
   65*/
   66
   67:- multifile
   68	user:file_search_path/2,
   69	swish_config:config/2,
   70	swish_config:source_alias/2.   71
   72% make example(File) find the example data
   73user:file_search_path(example, swish(examples)).
   74% make SWISH serve /example/File as example(File).
   75swish_config:source_alias(example, [access(read), search('*.{pl,swinb}')]).
   76
   77:- http_handler(swish(list_examples),
   78		list_examples, [id(swish_examples)]).   79
   80
   81%%	list_examples(+Request)
   82%
   83%	Get a list of registered example code. Examples are described in
   84%	a file swish_examples('index.json').
   85
   86list_examples(_Request) :-
   87	examples(AllExamples, [community(true)]),
   88	example_menu(AllExamples, Menu),
   89	reply_json(Menu).
   90
   91example_menu(AllExamples, Menu) :-
   92	include(pos_ranked, AllExamples, ForMenu),
   93	insert_group_dividers(ForMenu, Menu).
   94
   95pos_ranked(Ex) :-
   96	Rank = Ex.get(grank),
   97	Rank > 0.
   98
   99insert_group_dividers([], []).
  100insert_group_dividers([H1,H2|T], List) :-
  101	!,
  102	(   H1.grank // 10000 =\= H2.grank // 10000
  103	->  List = [H1, json{type:divider}|Rest]
  104	;   List = [H1|Rest]
  105	),
  106	insert_group_dividers([H2|T], Rest).
  107insert_group_dividers([H], [H]).
  108
  109
  110%%	examples(JSON:list, +Options) is det.
  111%
  112%	JSON is a list of JSON dicts containing the keys below. The list
  113%	is composed from all *.pl files in the search path `example`.
  114%
  115%	  - file:File
  116%	  - href:URL
  117%	  - title:String
  118%	  - requires:Term
  119%	  - group:String
  120
  121examples(AllExamples, Options) :-
  122	swish_examples(SWISHExamples),
  123	(   option(community(true), Options)
  124	->  community_examples(CommunityEx)
  125	;   CommunityEx = json{}
  126	),
  127	join_examples([CommunityEx|SWISHExamples], AllExamples).
  128
  129:- dynamic
  130	swish_example_cache/2.  131
  132swish_examples(SWISHExamples) :-
  133	swish_example_cache(SWISHExamples, Time),
  134	get_time(Now),
  135	Now - Time < 60,
  136	!.
  137swish_examples(SWISHExamples) :-
  138	swish_examples_no_cache(SWISHExamples),
  139	get_time(Now),
  140	retractall(swish_example_cache(_,_)),
  141	assertz(swish_example_cache(SWISHExamples, Now)).
  142
  143swish_examples_no_cache(SWISHExamples) :-
  144	http_absolute_location(swish(example), HREF, []),
  145	findall(Index,
  146		absolute_file_name(example(.), Index,
  147				   [ access(read),
  148				     file_type(directory),
  149				     file_errors(fail),
  150				     solutions(all)
  151				   ]),
  152		ExDirs),
  153	maplist(index_json(HREF), ExDirs, SWISHExamples).
  154
  155
  156join_examples(PerDir, Files) :-
  157	menu_groups(PerDir, Groups),
  158	maplist(get_or(files, []), PerDir, FilesPerDir),
  159	append(FilesPerDir, Files0),
  160	maplist(add_grank(Groups), Files0, Files1),
  161	sort(grank, =<, Files1, Files).
  162
  163add_grank(Groups, File0, File) :-
  164	get_or(rank,  500,  File0, FRank),
  165	GroupName = File0.get(group),
  166	member(Group, Groups),
  167	Group.get(group) == GroupName,
  168	GRank is FRank + Group.get(rank), !,
  169	File = File0.put(grank, GRank).
  170add_grank(_, File0, File) :-
  171	File = File0.put(grank, -1).
  172
  173menu_groups(PerDir, Groups) :-
  174	maplist(get_or(menu, []), PerDir, GroupsPerDir),
  175	append(GroupsPerDir, Groups0),
  176	sort(group, @>, Groups0, Groups1),
  177	sort(rank,  =<, Groups1, Groups).
  178
  179get_or(Key, Default, Dict, Value) :-
  180	(   is_dict(Dict),
  181	    Value = Dict.get(Key)
  182	->  true
  183	;   Value = Default
  184	).
  185
  186%!	index_json(+BaseHREF, +Directory, -JSON)
  187%
  188%	Produce a JSON description for  the   examples  in the directory
  189%	Dir. This deals with two scenarios:   if  a file `index.json` is
  190%	provided, use this file  and  add   the  not-described  files as
  191%	examples that are not included in   the menu. If no `index.json`
  192%	is present, all files are added as example files.
  193
  194index_json(HREF, Dir, JSON) :-
  195	directory_file_path(Dir, 'index.json', File),
  196	access_file(File, read), !,
  197	read_file_to_json(File, JSON0),
  198	add_examples_href(HREF, JSON0, JSON1),
  199	add_other_files(HREF, Dir, JSON1, JSON).
  200index_json(HREF, Dir, json{menu:[json{group:examples, rank:10000}],
  201			   files:Files}) :-
  202	example_files(HREF, Dir, Files0),
  203	maplist(add_group(examples), Files0, Files).
  204
  205example_files(HREF, Dir, JSON) :-
  206	string_concat(Dir, "/*.{pl,swinb}", Pattern),
  207	expand_file_name(Pattern, Files),
  208	maplist(ex_file_json(HREF), Files, JSON).
  209
  210read_file_to_json(File, JSON) :-
  211	setup_call_cleanup(
  212	    open(File, read, In, [encoding(utf8)]),
  213	    json_read_dict(In, JSON, [default_tag(json)]),
  214	    close(In)).
  215
  216%!	add_examples_href(+HREF, +JSON0, -JSON) is det.
  217%
  218%	Add a `href` key pointing at the example. Also removes all items
  219%	that are not dicts or have no `file` key.
  220
  221add_examples_href(HREF, JSON0, JSON) :-
  222	Files0 = JSON0.get(files), !,
  223	convlist(add_href(HREF), Files0, Files),
  224	JSON = JSON0.put(files, Files).
  225add_examples_href(_, JSON, JSON).
  226
  227
  228add_href(HREF0, Dict, Dict2) :-
  229	is_dict(Dict),
  230	directory_file_path(HREF0, Dict.get(file), HREF),
  231	Dict2 = Dict.put(href, HREF).
  232
  233add_group(Group, Dict0, Dict) :-
  234	is_dict(Dict0), !,
  235	Dict = Dict0.put(group, Group).
  236add_group(_, Dict, Dict).
  237
  238add_other_files(HREF, Dir, JSON0, JSON) :-
  239	example_files(HREF, Dir, Files),
  240	get_or(files, [], JSON0, Files0),
  241	exclude(in_ex_list(Files0), Files, New),
  242	append(Files0, New, AllFiles),
  243	JSON = JSON0.put(files, AllFiles).
  244
  245in_ex_list(Examples, Ex) :-
  246	File = Ex.file,
  247	member(Ex2, Examples),
  248	is_dict(Ex2),
  249	File = Ex2.get(file),
  250	!.
  251
  252%%	ex_file_json(+ExampleBase, +Path, -JSON) is det.
  253%
  254%	Create a JSON representation for the given example file.
  255
  256ex_file_json(HREF0, Path, json{file:File, href:HREF, title:Title}) :-
  257	file_base_name(Path, File),
  258	file_name_extension(Base, _, File),
  259	file_name_to_title(Base, Title),
  260	directory_file_path(HREF0, File, HREF).
  261
  262:- if(current_predicate(restyle_identifier/3)).  263file_name_to_title(Base, Title) :-
  264	restyle_identifier(style(true,false,' '), Base, Title).
  265:- else.  266file_name_to_title(Base, Base).
  267:- endif.  268
  269
  270%!	md_eval:provides(?Term) is nondet.
  271%
  272%	Make examples available through swish_provides/1. Can be used in
  273%	dynamic cells as, e.g.,:
  274%
  275%	  ```
  276%	  :- if(swish_provides(example('chat80.pl',_,_))).
  277%	  ...
  278%	  :- endif.
  279%	  ```
  280
  281:- multifile
  282	md_eval:provides/1.  283
  284md_eval:provides(example(Name, Group, Example)) :-
  285	examples(Examples, []),
  286	(   var(Name)
  287	->  member(Example0, Examples),
  288	    atom_string(Name, Example0.get(file))
  289	;   member(Example0, Examples),
  290	    atom_string(Name, Example0.get(file))
  291	->  true
  292	),
  293	atom_string(Group,  Example0.get(group)),
  294	active_example(Example0, Example).
  295
  296active_example(Example0, Example) :-
  297	term_string(Cond, Example0.get(requires)),
  298	\+ swish_provides(Cond),
  299	(   cond_reason(Cond, Fmt, Args)
  300	->  format(string(Reason), Fmt, Args)
  301	;   format(string(Reason), 'missing requirement: ~q', [Cond])
  302	),
  303	Example = Example0.put(blocked, Reason).
  304active_example(Example, Example).
  305
  306cond_reason(plugin(Name), 'missing plugin: ~w', [Name]).
  307
  308
  309
  310		 /*******************************
  311		 *	      STORAGE		*
  312		 *******************************/
  313
  314%%	community_examples(-Dict) is det.
  315%
  316%	Extract examples from the gitty store.
  317
  318community_examples(json{menu:[json{group:community, rank:50000}],
  319			files:Files}) :-
  320	swish_config:config(community_examples, true),
  321	!,
  322	findall(Ex, community_example(Ex), Files).
  323community_examples(json{}).
  324
  325community_example(json{title:Title, file:File, group:community, type:store}) :-
  326	storage_file(File),
  327	storage_meta_data(File, Meta),
  328	Meta.get(example) == true,
  329	(   Title = Meta.get(title), Title \== ""
  330	->  true
  331	;   file_name_extension(Base, _, File),
  332	    file_name_to_title(Base, Title)
  333	)