1/*   vcard
    2     Author: Giménez, Christian.
    3
    4     Copyright (C) 2018 Giménez, Christian
    5
    6     This program is free software: you can redistribute it and/or modify
    7     it under the terms of the GNU General Public License as published by
    8     the Free Software Foundation, either version 3 of the License, or
    9     at your option) any later version.
   10
   11     This program is distributed in the hope that it will be useful,
   12     but WITHOUT ANY WARRANTY; without even the implied warranty of
   13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   14     GNU General Public License for more details.
   15
   16     You should have received a copy of the GNU General Public License
   17     along with this program.  If not, see <http://www.gnu.org/licenses/>.
   18
   19     13 may 2018
   20*/
   21
   22
   23:- module(vcard, [
   24	      vcard//1,
   25	      contentline//1,
   26	      name//1,
   27	      param//1,
   28	      vcard_file/2
   29	  ]).

vcard: vCard Parser Implementation.

*/

   35:- ensure_loaded(library(dcg/basics)).   36
   37vcard_file(Path, VCards) :-
   38    read_file_to_codes(Path, Codes, []),
   39    vcard_entity(VCards, Codes, []).
   40
   41crlf --> "\r\n".
   42
   43fold_space --> " ", !.
   44fold_space --> "\t".
   45
   46folded_line([32|Line]) --> fold_space, string_without("\r\n", Line), crlf.
 folded_string(-Codes:list)//
See also
- https://tools.ietf.org/html/rfc6350#section-3.2 */
   53folded_string(Codes) --> string_without("\r\n", Codes1), crlf,
   54			 rest_fold(Codes2),
   55			 {append(Codes1, Codes2, Codes)}.
 rest_fold(-Codes:codes)
Match multiple lines with folded_line//1: Lines that start with space or tabs and continue until CRLF. */
   63rest_fold(Codes) --> folded_line(CodesLine),
   64		     rest_fold(CodesRest), !,
   65		     { append(CodesLine, CodesRest, Codes) }.
   66rest_fold([]) --> []. 
 folded_line(Codes)
 vcard_entity(?VCards:list)//
   vcard-entity = 1*vcard

*/

   75vcard_entity(VCards) --> vcard(VCard), vcard_entity(Rest), !,
   76			 { append([VCard], Rest, VCards) }.
   77vcard_entity([VCard]) --> vcard(VCard).
   78
   79header --> "BEGIN:VCARD", crlf.
   80footer --> "END:VCARD",  crlf.
   81version(Version) --> "VERSION:", string_without("\r\n", Version), crlf.
 vcard//
   vcard = "BEGIN:VCARD" CRLF
           "VERSION:4.0" CRLF
           1*contentline
           "END:VCARD" CRLF
     ; A vCard object MUST include the VERSION and FN properties.
     ; VERSION MUST come immediately after BEGIN:VCARD.

*/

   95vcard(vcard(version(Version),
   96	    contents(Contents))) --> header, version(Version),
   97				     contentlines(Contents),
   98				     footer.
   99
  100contentlines([Content|Rest]) --> contentline(Content), contentlines(Rest), !.
  101contentlines([Content]) --> contentline(Content).
 contentline(-Content:pred)//
contentline = [group "."] name *(";" param) ":" value CRLF
     ; When parsing a content line, folded lines must first
     ; be unfolded according to the unfolding procedure
     ; described in Section 3.2.
     ; When generating a content line, lines longer than 75
     ; characters SHOULD be folded according to the folding
     ; procedure described in Section 3.2.

content/4 Predicate

If Group is not indicated, then an empty list is unifying.

Params is a list of codes.

Arguments:
Content- A content/4 predicate like content(Group: codes, Name: codes, Params: list, Value: codes). */
  124contentline(content([], Name, Params, Value)) -->
  125    name(Name), {Name \= `END`},
  126    !, params(Params), ":", value(Value).
  127contentline(content(Group, Name, Params, Value)) -->
  128    group(Group), ".", !,
  129    name(Name), params(Params), ":", value(Value).
 group(-Group:codes)//
    group = 1*(ALPHA / DIGIT / "-")

*/

  138group(Group) --> alpha_or_digit(C), group(C2), !,
  139		 { append(C, C2, Group) }.
  140group(Group) --> alpha_or_digit(Group).
 name(-Name:list)//
   name = iana-token / x-name
     ; Parsing of the param and value is based on the "name" as
     ; defined in ABNF sections below.
     ; Group and name are case-insensitive.

We do not implement the following because iana-token//1 can match them.

   name = "SOURCE" / "KIND" / "FN" / "N" / "NICKNAME"
         / "PHOTO" / "BDAY" / "ANNIVERSARY" / "GENDER" / "ADR" / "TEL"
         / "EMAIL" / "IMPP" / "LANG" / "TZ" / "GEO" / "TITLE" / "ROLE"
         / "LOGO" / "ORG" / "MEMBER" / "RELATED" / "CATEGORIES"
         / "NOTE" / "PRODID" / "REV" / "SOUND" / "UID" / "CLIENTPIDMAP"
         / "URL" / "KEY" / "FBURL" / "CALADRURI" / "CALURI" / "XML"

*/

  165name(Name) --> iana_token(Name).
  166
  167
  168alpha_or_digit([C]) --> digit(C), !.
  169alpha_or_digit([C]) --> [C], {code_type(C, alpha)}, !.
  170alpha_or_digit(`-`) --> "-".
  171
  172aods(Chars) --> alpha_or_digit(C), aods(Chars1), !,
  173		{append(C, Chars1, Chars)}.
  174aods(C) --> alpha_or_digit(C).
 iana_token(-Token:list)//
   iana-token = 1*(ALPHA / DIGIT / "-")
     ; identifier registered with IANA

*/

  185iana_token(Token) --> aods(Token).
  186
  187x_name_x(`x-`) --> "x-".
  188x_name_x(`X-`) --> "X-".
  199x_name(Token) --> x_name_x(X), aods(Chars),
  200		  {append(X, Chars, Token)}.
  201
  202params([Param|Rest]) --> ";", param(Param),  params(Rest), !.
  203params([Param]) --> ";", param(Param),!.
  204params([]) --> [].
 param(-Param:pred)//
   param = language-param / value-param / pref-param / pid-param
         / type-param / geo-parameter / tz-parameter / sort-as-param
         / calscale-param / any-param
     ; Allowed parameters depend on property name.
Arguments:
Param- A param/2 predicate like param(Name: codes, Values: codes). */
  227param(param(Name, Values)) --> any_param(param(Name, Values)), !.
 param_value(-Value:list)//
param-value = *SAFE-CHAR / DQUOTE *QSAFE-CHAR DQUOTE

SAFE-CHAR and QSAFE-CHAR will be represented with string_without/2 as it is used only for defining this predicate. */

  239param_value(Value) --> "\"", !, qsafe_chars(Value), "\"".
  240param_value(Value) --> safe_chars(Value).
 param_values(-Values:list)//
Return the list of values params.

Predicate for defining any_param//2.

See also
- any_param//1 */
  251param_values(Values) --> param_value(Value), ",", !, param_values(Values1), 
  252			 {append([Value], Values1, Values)}.
  253param_values([Value]) --> param_value(Value).
 any_param(-Param:pred)//
We don't use x_name//1 because iana_token//1 match includes the matches of x-name.
any-param  = (iana-token / x-name) "=" param-value *("," param-value)
Arguments:
Param- A param/2 term param(Name: codes, Values: list). */
  266any_param(param(Name, Values)) --> iana_token(Name) , "=",
  267				   param_values(Values).
 safe_chars(-Chars:list)//
SAFE-CHAR = WSP / "!" / %x23-39 / %x3C-7E / NON-ASCII
  ; Any character except CTLs, DQUOTE, ";", ":"

*/

  277safe_chars(Chars) --> string_without("\";:", Chars).
 qsafe_chars(-Chars:code)//
QSAFE-CHAR = WSP / "!" / %x23-7E / NON-ASCII
  ; Any character except CTLs, DQUOTE

*/

  287qsafe_chars(Chars) --> string_without("\"", Chars).
  288
  289/*
  290   NON-ASCII = UTF8-2 / UTF8-3 / UTF8-4
  291     ; UTF8-{2,3,4} are defined in [RFC3629]
  292
  293   VALUE-CHAR = WSP / VCHAR / NON-ASCII
  294     ; Any textual character
  295*/
 language_param(-Name:list, -Value:list)//
language-param = "LANGUAGE=" Language-Tag
  ; Language-Tag is defined in section 2.1 of RFC 5646

*/

  306language_param(`LANGUAGE`, Tag) --> "LANGUAGE=", language_tag(Tag).
value-param = "VALUE=" value-type

value-type = "text"
           / "uri"
           / "date"
           / "time"
           / "date-time"
           / "date-and-or-time"
           / "timestamp"
           / "boolean"
           / "integer"
           / "float"
           / "utc-offset"
           / "language-tag"
           / iana-token  ; registered as described in section 12
           / x-name

*/

  331value_param(Type) --> "VALUE=", value_type(Type).
  332
  333value_type(`text`) --> "text", !.
  334value_type(`uri`) --> "uri",!.
  335value_type(`date`) --> "date",!.
  336value_type(`time`) --> "time",!.
  337value_type(`date-time`) --> "date-time",!.
  338value_type(`date-and-or-time`) --> "date-and-or-time",!.
  339value_type(`timestamp`) --> "timestamp",!.
  340value_type(`boolean`) --> "boolean",!.
  341value_type(`integer`) --> "integer",!.
  342value_type(`float`) --> "float",!.
  343value_type(`utc-offset`) --> "utc-offset",!.
  344value_type(`language-tag`) --> "language-tag",!.
  345value_type(Type) --> iana_token(Type), !.
  346value_type(Type) --> x_name(Type), !.
 pref_param(-Num:codes)//
Used to indicate that the corresponding instance of a property is preferred by the vCard author.
pref-param = "PREF=" (1*2DIGIT / "100")
                     ; An integer between 1 and 100.

*/

  358pref_param(Num) --> "PREF=", digits(D),
  359		    {number_codes(Num, D),
  360		     Num =< 100}.
 altid_param(-Value:codes)//
Used to "tag" property instance as being alternative repersentations of the same logical property.
altid-param = "ALTID=" param-value

*/

  373altid_param(Value) --> "ALTID=", param_value(Value).
 pid_param(-Value:list)//
pid-param = "PID=" pid-value *("," pid-value)
pid-value = 1*DIGIT ["." 1*DIGIT]

*/

  383pid_param(Value) --> "PID=", pid_values(Value).
  384pid_values([Value|Rest]) --> pid_value(Value), ",", !,
  385			     pid_values(Rest)
  385.
  386pid_values([Value]) --> pid_value(Value).
  387
  388pid_value(Value) --> digits(Value1), ".", !,
  389		     digits(Value2),
  390		     {append([Value1, `.`, Value2], Value)}
  390.
  391pid_value(Value) --> digits(Value).
type-value = "work" / "home" / type-param-tel / type-param-related / iana-token / x-name ; This is further defined in individual property sections. ` */
  403type_param(Value) --> "TYPE=", type_values(Value).
  404
  405type_value(`work`) --> "work", !.
  406type_value(`home`) --> "home", !.
  407type_value(Value) --> type_param_tel(Value), !.
  408type_value(Value) --> type_param_related(Value), !.
  409type_value(Value) --> iana_token(Value), !.
  410type_value(Value) --> x_name(Value).
 mediatype_param(-Types:pred)//
mediatype-param = "MEDIATYPE=" mediatype
mediatype = type-name "/" subtype-name *( ";" attribute "=" value )
  ; "attribute" and "value" are from [RFC2045]
  ; "type-name" and "subtype-name" are from [RFC4288]

*/

  422mediatype_param(type(Types, [], [])) -->
  423    "MEDIATYPE=", mediatype(Types).
  424mediatype(type(Types, SubTypes, Attrs)) -->
  425    type_name(Types), "/", subtype_name(SubTypes),
  426    attributes(Attrs).
  427
  428attributes([Attr|Rest]) --> attribute(Attr), attributes(Rest), !.
  429attributes([Attr]) --> attribute(Attr).
 attribute(-Attr:pred)//
"attribute" and "value" are from RFC2045.
See also
- https://tools.ietf.org/html/rfc2045 */
  438attribute(attribute(Attr, Value)) --> ";", attribute_b(Attr), "=", value_b(Value).
 attribute_b(-Token:codes)//
Attribute from RFC2045.
token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
            or tspecials>

tspecials :=  "(" / ")" / "<" / ">" / "@" /
              "," / ";" / ":" / "\" / <">
              "/" / "[" / "]" / "?" / "="
              ; Must be in quoted-string,
              ; to use within parameter values
See also
- https://tools.ietf.org/html/rfc2045#section-5.1 */
  458attribute_b(Token) --> string_without(" ()<>@,;:\\\"/[]?=", Token).
  459
  460value_b(Str) --> attribute_b(Str), !.
  461value_b(Str) --> quoted_string_b(Str).
  467quoted_string_b(Str) --> "\"", string(Str), "\"", !.
 value(-Value:codes)
*/
  474value(Value) --> folded_string(Value)