:- module(interpolate, ['$interpolate_macro_sentinel'/0]). :- use_module(library(dcg/basics), [prolog_var_name//1, string//1]). :- use_module(library(function_expansion)). :- use_module(library(error), [type_error/2]). %% Template(+Vars:pairs, -Formats:list, -Args:list)// % % Parse a list of codes as if it were an interpolated string. Formats % is a list of atoms that can be joined together to % create the first argument of format/2. Args are values for the % second. template(Vars, [Static,_|Formats], [Arg|Args]) --> string(Codes), variable(VarName), { memberchk(VarName=Arg, Vars) }, { atom_codes(Static, Codes) }, template(Vars, Formats, Args). template(_, [Format], []) --> string(Codes), { atom_codes(Format, Codes) }. %% variable(-VarName:atom)// % % Parse a $-prefixed variable name. Like `$Message`. % For now this is a thin wrapper around prolog_var_name//1 but may % eventuall grow to support other kinds of variables so I want the % details abstracted away. variable(VarName) --> "$", prolog_var_name(VarName). % true if the module whose terms are being read has specifically % requested string interpolation. wants_interpolation :- prolog_load_context(module, Module), Module \== interpolate, % we don't want string interpolation ourselves predicate_property( Module:'$interpolate_macro_sentinel', imported_from(interpolate) ). %% textual(-Type:atom, +Text, -Codes) % % True if Text is of type Type and representable as Codes. textual(_,Var,_) :- var(Var), !, fail. textual(atom, Atom, Codes) :- atom(Atom), !, atom_codes(Atom,Codes). textual(Type, Text, Codes) :- is_list(Text), !, Text = [H|_], % empty lists are not text for our purposes ( atom(H) -> Type = chars, catch(atom_chars(Atom,Text),_,fail), atom_codes(Atom,Codes) ; integer(H) -> Type = codes, Codes = Text ). % fail on all other lists textual(string, String, Codes) :- string(String), string_to_list(String,Codes). %% build_text(?Output, ?Formats:list, ?Args:list) % % Like format/3 but dynamically chooses tilde sequences to match the % values in Args. build_text(Output, Formats0, Args) :- instantiate_formats(Formats0, Args, Formats), atomic_list_concat(Formats, Format), format(Output, Format, Args). % choose format tilde sequences for a list of values instantiate_formats([], _, []). instantiate_formats([Static|Formats0],Args,[Static|Formats]) :- atom(Static), !, instantiate_formats(Formats0,Args,Formats). instantiate_formats([Var|Formats0],[Arg|Args],[Format|Formats]) :- var(Var), !, preferred_tilde(Arg,Format), instantiate_formats(Formats0,Args,Formats). instantiate_formats([X|_],_,[_|_]) :- type_error(atom_or_var,X). % Which format/2 tilde sequence does a value prefer? preferred_tilde(X,'~s') :- textual(Type, X, _), ( Type = codes; Type = chars ), !. preferred_tilde(_,'~p'). :- multifile user:function_expansion/3. user:function_expansion(Term,Replacement,Guard) :- wants_interpolation, prolog_load_context(variable_names, Vars), Vars \== [], % need variables to interpolate % is this a string in need of interpolation? textual(Type, Term, TextCodes), phrase(template(Vars, Formats, Args), TextCodes), Args \== [], % no args means no interpolation % yup, so perform the expansion Output =.. [Type, Replacement], Guard = interpolate:build_text(Output, Formats, Args). %% '$interpolate_macro_sentinel'. % % Nothing to see here. This is an implementation detail to avoid % unwanted string interpolation. Because string interpolation is % implemented as a macro and macro expansion is done through a % predicate with gloabl reach (user:term_expansion/2), we must take % precautions to avoid performing string interpolation on code that % doesn't want it. Importing this predicate opts-in to string % interpolation. Writing `use_module(library(interpolate))` does it % for you. '$interpolate_macro_sentinel'.