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