/*
  DCG for parsing N-Triples

  - should have a read-stream mode, which only converts a line at a 
    time to codes, to save memory. 
  - maybe a assert-on-reading option, too
  - some kind of strictness checking?  like must a URI conform to
    URI syntax?
  - needs UTF-8
  - needs generation/output thought, too.
  - needs some refactoring sanitizing -- stuff like "[^>]*" is done
    in about four different ways.


*/
:- [cvs, unit_test].
cvs_id('$Id: ntriples.pl,v 1.7 2004/10/18 22:37:30 sandro Exp $').
:- ensure_loaded(library('debug')).
:- ensure_loaded(library('http/dcg_basics')).
:- ensure_loaded(focus).
:- ensure_loaded('grammars/robotstxt').   % for atom_without, for now
:- ensure_loaded(utf_8).

rdf_load_ntriples(File, Graph) :-
	read_file_to_codes(File, Codes, []),
	ntriples_text(Triples, _BnodeMap, Codes, ""),
	forall(  member(rdf(S,P,O), Triples),
		 rdf_transaction(rdf_assert(S,P,O,Graph))
	      ).

unit_test((
	   % http://www.w3.org/TR/2004/REC-rdf-syntax-grammar-20040210/example20.nt
	   Input="<http://example.org/> <http://example.org/stuff/1.0/prop> ""blah"" .
<http://example.org/triples/#triple1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement> .
<http://example.org/triples/#triple1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#subject> <http://example.org/> .
<http://example.org/triples/#triple1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate> <http://example.org/stuff/1.0/prop> .
<http://example.org/triples/#triple1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#object> ""blah"" .
",

	  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	   ntriples_text(Triples, _BNodeMap, Input, ""),
          %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	   TriplesOut = [rdf('http://example.org/', 'http://example.org/stuff/1.0/prop', literal(blah)), rdf('http://example.org/triples/#triple1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement'), rdf('http://example.org/triples/#triple1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#subject', 'http://example.org/'), rdf('http://example.org/triples/#triple1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate', 'http://example.org/stuff/1.0/prop'), rdf('http://example.org/triples/#triple1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#object', literal(blah))],
	   Triples=TriplesOut
	   )).

/*
?- open('/home/sandro/cvs/cvs.w3c.org/WWW/TR/new/REC-rdf-syntax-grammar-20040210/example21.nt', read, S), read_stream_to_codes(S, C), atom_codes(A,C), (ntriples_text(R, B, C, ""); true).
WORKS
*/

ntriples_text([], _BNodeMap) -->
	eos.
ntriples_text([Triple|Triples], BNodeMap) -->
	ntriples_line(Triple, BNodeMap),
	ntriples_text(Triples, BNodeMap).
ntriples_text(Triples, BNodeMap) -->
	nt_comment_line,
	ntriples_text(Triples, BNodeMap).

unit_test((
	   ntriples_line(X, _B, "<a> <b> <c>.",""),
	   X = rdf(a, b, c)
	   )).

nt_comment_line -->
	star(" "), "#", d('comment text', anybut("\n")), "\n", !.
%	star(" "), "#", star(not_char("\n")), "\n".
	
ntriples_line(rdf(S,P,O), BNodeMap) -->
	ntriples_term(S, BNodeMap),
	%{ format('got a subject ~q~n', [S]) },
	blanks, 
	ntriples_term(P, BNodeMap),
	%{ format('got a predicate ~q~n', [P]) },
	blanks,
	!,
	ntriples_term(O, BNodeMap),
	%{ format('got a object ~q~n', [O]) },
	(blanks ; ""),
	".",
	blanks_to_nl,
	!.
	%{ format('got a line~n', []) }.

unit_test((ntriples_term(Term, X, "_:foo", ""),
	   sub_atom(Term, 0, _, _, 'blank '),
	   X=[foo=Term|Y],
	   var(Y)
	  )).
unit_test((ntriples_term(Term, _X, "<http://example.com>", ""),
	   Term = 'http://example.com'
	  )).
unit_test((ntriples_term(Term, _X, """Hello, World!""", ""),
	   Term = literal('Hello, World!')
	  )).
ntriples_term(T, BNodeMap) -->
	"_:", !, nonblanks(NodeLabelCodes),
	{ atom_codes(NodeLabel, NodeLabelCodes),
	  memberchk(NodeLabel=T, BNodeMap),
	  (   var(T)
	  ->  gensym('blank ', T)
	  ;   true
	  )
	}.
% strict URI
%ntriples_term(T, _BNodeMap) -->
%	"<", uri(T), ">".
% loose URI :  anything but ">"
%ntriples_term(T, _BNodeMap) -->
%	"<", save_text(star(not_char(">")), T), ">".

% no better, really.
%ntriples_term(T, _BNodeMap) -->
%	"<", d(uri, save_text(not_gt, T)), ">".	

% GOT IT:   24 times better -- not backtracking through the URI
%ntriples_term(T, _BNodeMap) -->
%	"<", d(uri, save_text_except(through_gt, T)).
ntriples_term(T, _BNodeMap) -->
	"<", save_text_except(through_gt, T).

ntriples_term(literal(S), _BNodeMap) -->
	"""", through_quote(SS), { atom_codes(S, SS) },
	( lang_tag(_Lang) ; ""),
	( dt_tag(_DT) ; "").

	% @@@@@@@   USE DT AND LANG INFO?

lang_tag(Lang) --> "@", !, atom_without(" .", Lang).
dt_tag(DT) --> "^^", !, "<", save_text_except(through_gt, DT).

%  http://www.w3.org/TR/2004/REC-rdf-testcases-20040210/#ntrip_strings
%  ... not all implemented yet ...

not_gt -->
	[].
not_gt -->
	">", !, {fail}.
not_gt -->
	[_X], not_gt.

through_gt -->
	">".
through_gt -->
	[X], {X\=62}, through_gt.

% http://www.w3.org/TR/2004/REC-rdf-testcases-20040210/#ntrip_strings
% (use sformat in going the other direction)
through_quote(Wide) -->
	[ 92 ],
	dcg_basics:xdigit(V1),
	dcg_basics:xdigit(V2),
	dcg_basics:xdigit(V3),
	dcg_basics:xdigit(V4),
	{
	 dcg_basics:mkval([V1, V2, V3, V4], 16, CodePoint),
	 (   CodePoint >= 0,   CodePoint =< 8
	 ;   CodePoint >= 11,  CodePoint =< 12
	 ;   CodePoint >= 14,  CodePoint =< 15
	 ;   CodePoint >=127,  CodePoint =< 65535
	 ),
	 utf8_codes(CodePoint, UTF8),
	 append(UTF8, Rest, Wide)
	 },
	through_quote(Rest).
through_quote(Wide) -->
	[ 92 ],
	dcg_basics:xdigit(V1),
	dcg_basics:xdigit(V2),
	dcg_basics:xdigit(V3),
	dcg_basics:xdigit(V4),
	dcg_basics:xdigit(V5),
	dcg_basics:xdigit(V6),
	dcg_basics:xdigit(V7),
	dcg_basics:xdigit(V8),
	{
	 dcg_basics:mkval([V1, V2, V3, V4, V5, V6, V7, V8], 16, CodePoint),
	 CodePoint > 65535,
	 utf8_codes(CodePoint, UTF8),
	 append(UTF8, Rest, Wide)
	},
	through_quote(Rest).

%%%   @@@ still missing a few characters
through_quote([34|Rest]) -->         % put a quote (") on the list
	[92, 34],                    % if we see a backslash-quote (\")
	through_quote(Rest).
through_quote([10|Rest]) -->         % put a newline (\n) on the list
	[92, 110],                   % if we see a backslash-n (\n)
	through_quote(Rest).
through_quote([92|Rest]) -->        
	[92, 92],                   
	through_quote(Rest).
through_quote([0'?|Rest]) -->         % put a question-mark on the list
	[92, _],                    % for all other backslash stuff, for now, until we've got UTF-8 handy
	through_quote(Rest).
through_quote("") -->
	"""".
through_quote([X|Rest]) -->
	[X],
	{[X]\=""""},
	{[X]\="\\"},
	through_quote(Rest).


unit_test((
	   \+ through_quote(_X, "hello", "")
	   )).
unit_test((
	      through_quote("hello", "hello""", "")
	   )).
unit_test((
	    \+  through_quote(_, "hello""x", "")
	   )).
unit_test((
	      through_quote([65, 65, 34, 65], [65, 65, 92, 34, 65, 34], "")    %    aa\"a"
	   )).

ntriples_noquote -->
	""
	; noquotechar
	; noquotechar, noquote
	.

ntriples_noquotechar -->
	[D], { [D] \= """" }.

not_char(Avoid) -->
	[C],
	{ \+ member(C, Avoid) }.

star(_P) -->
	"".
star(P) -->
	P, !, star(P).

kplus(P) -->
	P.
kplus(P) -->
	P, !, star(P).

any(In, Rest) :-
	append(_, Rest, In).
anybut([Char], In, Rest) :-
	append(X, Rest, In),
	\+memberchk(Char, X).

d(Tag, P) -->
	save_text(P, Text),
	{ format('~a:~q~n', [Tag, Text]) }.

save_text_except(Callable, SaveTo, Li, Lo) :-
	call(Callable, Li, Lo),
	append(Codes, Lo, Li),
	atom_codes(SaveToX, Codes),
	sub_atom(SaveToX, 0, _, 1, SaveTo).
