Next: , Previous: , Up: Top   [Contents]


76 term_to_xml

%--------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%--------------------------------------------------%
% Copyright (C) 1993-2007, 2010-2011 The University of Melbourne.
% This file may only be copied under the terms of the GNU Library General
% Public License - see the file COPYING.LIB in the Mercury distribution.
%--------------------------------------------------%
%
% File: term_to_xml.m.
% Main author: maclarty.
% Stability: low.
%
% This module provides two mechanisms for converting Mercury terms
% to XML documents.
%
% Method 1
% --------
% The first method requires a type to be an instance of the xmlable typeclass
% before values of the type can be written as XML.
% Members of the xmlable typeclass must implement a to_xml method which
% maps values of the type to XML elements.
% The XML elements may contain arbitrary children, comments and data.
%
% Method 2
% --------
% The second method is less flexible than the first, but it allows for the
% automatic generation of a DTD.
% Each functor in a term is given a corresponding well-formed element name in
% the XML document according to a mapping. Some predefined mappings are
% provided, but user defined mappings may also be used.
%
% Method 1 vs. Method 2
% ---------------------
%
% Method 1 allows values of a specific type to be mapped to arbitrary XML
% elements with arbitrary children and arbitrary attributes.
% In method 2 each functor in a term can be mapped to only one XML element.
% Method 2 also only allows a selected set of attributes.
% In method 2 a DTD can be automatically generated. In method 1 DTDs cannot
% be automatically generated.
%
% Method 1 is useful for mapping a specific type to XML,
% for example mapping terms which represent mathematical expressions to
% MathML.
% Method 2 is useful for mapping arbitrary terms of any type to XML.
%
% In both methods the XML document can be annotated with a stylesheet
% reference.
%
%--------------------------------------------------%
%--------------------------------------------------%

:- module term_to_xml.
:- interface.

:- import_module deconstruct.
:- import_module list.
:- import_module maybe.
:- import_module stream.
:- import_module type_desc.

%--------------------------------------------------%
%
% Method 1 interface
%

    % Instances of this typeclass can be converted to XML.
    %
:- typeclass xmlable(T) where [
    func to_xml(T::in) = (xml::out(xml_doc)) is det
].

    % Values of this type represent an XML document or a portion of
    % an XML document.
    %
:- type xml
    --->    elem(
                % An XML element with a name, list of attributes
                % and a list of children.
                element_name    :: string,
                attributes      :: list(attr),
                children        :: list(xml)
            )

    ;       data(string)
            % Textual data. `<', `>', `&', `'' and `"' characters
            % will be replaced by `&lt;', `&gt;', `&amp;', `&apos;'
            % and `&quot;' respectively.

    ;       cdata(string)
            % Data to be enclosed in `<![CDATA[' and `]]>' tags.
            % The string may not contain the substring "]]>".
            % If it does then invalid XML will be generated.

    ;       comment(string)
            % An XML comment. The comment should not
            % include the `<!--' and `-->'. Any occurrences of
            % the substring "--" will be replaced by " - ",
            % since "--" is not allowed in XML comments.

    ;       entity(string)
            % An entity reference. The string will
            % have `&' prepended and `;' appended before being
            % output.

    ;       raw(string).
            % Raw XML data. The data will be written out verbatim.

    % An XML document must have an element at the top-level.
    % The following inst is used to enforce this restriction.
    %
:- inst xml_doc
    --->    elem(
                ground, % element_name
                ground, % attributes
                ground  % children
            ).

    % An element attribute, mapping a name to a value.
    %
:- type attr
    --->    attr(string, string).

    % Values of this type specify the DOCTYPE of an XML document when
    % the DOCTYPE is defined by an external DTD.
    %
:- type doctype
    --->    public(string)                  % Formal Public Identifier (FPI)
    ;       public_system(string, string)   % FPI, URL
    ;       system(string).                 % URL

    % Values of this type specify whether a DTD should be included in
    % a generated XML document and if so how.
    %
:- type maybe_dtd
    --->    embed_dtd
            % Generate and embed the entire DTD in the document
            % (only available for method 2).

    ;       external_dtd(doctype)
            % Included a reference to an external DTD.

    ;       no_dtd.
            % Do not include any DOCTYPE information.

:- inst non_embedded_dtd
    --->    external_dtd(ground)
    ;       no_dtd.

    % Values of this type indicate whether a stylesheet reference should be
    % included in a generated XML document.
    %
:- type maybe_stylesheet
    --->    with_stylesheet(
                stylesheet_type :: string, % For example "text/xsl"
                stylesheet_href :: string
            )
    ;       no_stylesheet.

    % write_xml_doc(Stream, Term, !State):
    %
    % Output Term as an XML document to the given stream.
    % Term must be an instance of the xmlable typeclass.
    %
:- pred write_xml_doc(Stream::in, T::in, State::di, State::uo)
    is det <= (xmlable(T), stream.writer(Stream, string, State)).

    % write_xml_doc_style_dtd(Stream, Term, MaybeStyleSheet, MaybeDTD,
    %   !State):
    %
    % Write Term to the given stream as an XML document.
    % MaybeStyleSheet and MaybeDTD specify whether or not a stylesheet
    % reference and/or a DTD should be included.
    % Using this predicate, only external DTDs can be included, i.e.
    % a DTD cannot be automatically generated and embedded
    % (that feature is available only for method 2 -- see below).
    %
:- pred write_xml_doc_style_dtd(Stream::in, T::in,
    maybe_stylesheet::in, maybe_dtd::in(non_embedded_dtd),
    State::di, State::uo) is det
    <= (xmlable(T), stream.writer(Stream, string, State)).

    % write_xml_element(Stream, Indent, Term, !State):
    %
    % Write Term out as XML to the given stream, using Indent as the
    % indentation level (each indentation level is one tab character).
    % No `<?xml ... ?>' header will be written.
    % This is useful for generating large XML documents piecemeal.
    %
:- pred write_xml_element(Stream::in, int::in, T::in,
    State::di, State::uo) is det
    <= (xmlable(T), stream.writer(Stream, string, State)).

    % write_xml_header(Stream, MaybeEncoding, !State):
    %
    % Write an XML header (i.e. `<?xml version="1.0"?>) to the
    % current file output stream.
    % If MaybeEncoding is yes(Encoding), then include `encoding="Encoding"'
    % in the header.
    %
:- pred write_xml_header(Stream::in, maybe(string)::in,
    State::di, State::uo) is det <= stream.writer(Stream, string, State).

%--------------------------------------------------%
%
% Method 2 interface
%

    % Values of this type specify which mapping from functors to elements
    % to use when generating XML. The role of a mapping is twofold:
    %   1. To map functors to elements, and
    %   2. To map functors to a set of attributes that should be
    %      generated for the corresponding element.
    %
    % We provide two predefined mappings:
    %
    %   1. simple: The functors `[]', `[|]' and `{}' are mapped to the
    %   elements `List', `Nil' and `Tuple' respectively. Arrays are
    %   assigned the `Array' element. The builtin types are assigned
    %   the elements `Int', `String', `Float' and `Char'. All other
    %   functors are assigned elements with the same name as the
    %   functor provided the functor name is well formed and does
    %   not start with a capital letter. Otherwise a mangled
    %   version of the functor name is used.
    %
    %   All elements except `Int', `String', `Float' and `Char'
    %   will have their `functor', `arity', `type' and `field' (if
    %   there is a field name) attributes set. `Int', `String',
    %   `Float' and `Char' elements will just have their `type' and
    %   possibly their `field' attributes set.
    %
    %   The `simple' mapping is designed to be easy to read and use,
    %   but may result in the same element being assigned to different
    %   functors.
    %
    %   2. unique: Here we use the same mapping as `simple' except
    %   we append the functor arity for discriminated unions and
    %   a mangled version of the type name for every element. The same
    %   attributes as the `simple' scheme are provided. The advantage
    %   of this scheme is that it maps each functor to a unique
    %   element. This means that it will always be possible to
    %   generate a DTD using this mapping so long as there is only
    %   one top level functor and no unsupported types can appear in
    %   terms of the type.
    %
    % A custom mapping can be provided using the `custom' functor. See the
    % documentation for the element_pred type below for more information.
    %
:- type element_mapping
    --->    simple
    ;       unique
    ;       custom(element_pred).

:- inst element_mapping
    --->    simple
    ;       unique
    ;       custom(element_pred).

    % Deterministic procedures with the following signature can be used as
    % custom functor to element mappings. The inputs to the procedure are
    % a type and some information about a functor for that type
    % if the type is a discriminated union. The output should be a well
    % formed XML element name and a list of attributes that should be set
    % for that element. See the types `maybe_functor_info' and
    % `attr_from_source' below.
    %
:- type element_pred == (pred(type_desc, maybe_functor_info, string,
    list(attr_from_source))).

:- inst element_pred == (pred(in, in, out, out) is det).

    % Values of this type are passed to custom functor-to-element
    % mapping predicates to tell the predicate which functor to generate
    % an element name for if the type is a discriminated union. If the
    % type is not a discriminated union, then non_du is passed to
    % the predicate when requesting an element for the type.
    %
:- type maybe_functor_info
    --->    du_functor(
                % The functor's name and arity.
                functor_name    :: string,
                functor_arity   :: int
            )

    ;       non_du.
            % The type is not a discriminated union.

    % Values of this type specify attributes that should be set from
    % a particular source. The attribute_name field specifies the name
    % of the attribute in the generated XML and the attribute_source
    % field indicates where the attribute's value should come from.
    %
:- type attr_from_source
    --->    attr_from_source(
                attr_name   :: string,
                attr_source :: attr_source
            ).

    % Possible attribute sources.
    %
:- type attr_source
    --->    functor
            % The original functor name as returned by
            % deconstruct.deconstruct/5.

    ;       field_name
            % The field name if the functor appears in a
            % named field (If the field is not named then this
            % attribute is omitted).

    ;       type_name
            % The fully qualified type name the functor is for.

    ;       arity.
            % The arity of the functor as returned by
            % deconstruct.deconstruct/5.

    % To support third parties generating XML which is compatible with the
    % XML generated using method 2, a DTD for a Mercury type can also be
    % generated. A DTD for a given type and functor-to-element mapping may
    % be generated provided the following conditions hold:
    %
    %   1. If the type is a discriminated union then there must be only
    %   one top-level functor for the type. This is because the top
    %   level functor will be used to generate the document type name.
    %
    %   2. The functor to element mapping must map each functor to a
    %   unique element name for every functor that could appear in
    %   terms of the type.
    %
    %   3. Only types whose terms consist of discriminated unions,
    %   arrays and the builtin types `int', `string', `character' and
    %   `float' can be used to automatically generate DTDs.
    %   Existential types are also not supported.
    %
    % The generated DTD is also a good reference when creating a stylesheet
    % as it contains comments describing the mapping from functors to
    % elements.
    %
    % Values of the following type indicate whether a DTD was successfully
    % generated or not.
    %
:- type dtd_generation_result
    --->    ok

    ;       multiple_functors_for_root
            % The root type is a discriminated union with
            % multiple functors.

    ;       duplicate_elements(
                % The functor-to-element mapping maps different
                % functors to the same element. The duplicate element
                % and a list of types whose functors map to that
                % element is given.
                duplicate_element   :: string,
                duplicate_types     :: list(type_desc)
            )

    ;       unsupported_dtd_type(type_desc)
            % At the moment we only support generation of DTDs for types
            % made up of discriminated unions, arrays, strings, ints,
            % characters and floats. If a type is not supported, then it is
            % returned as the argument of this functor.

    ;       type_not_ground(pseudo_type_desc).
            % If one of the arguments of a functor is existentially typed,
            % then the pseudo_type_desc for the existentially quantified
            % argument is returned as the argument of this functor.
            % Since the values of existentially typed arguments can be of
            % any type (provided any typeclass constraints are satisfied)
            % it is not generally possible to generate DTD rules for functors
            % with existentially typed arguments.

    % write_xml_doc_general(Stream, Term, ElementMapping,
    %   MaybeStyleSheet, MaybeDTD, DTDResult, !State):
    %
    % Write Term to the given stream as an XML document using
    % ElementMapping as the scheme to map functors to elements.
    % MaybeStyleSheet and MaybeDTD specify whether or not a stylesheet
    % reference and/or a DTD should be included. Any non-canonical terms
    % will be canonicalized. If an embedded DTD is requested, but it is
    % not possible to generate a DTD for Term using ElementMapping, then a
    % value other than `ok' is returned in DTDResult and nothing is written
    % out. See the dtd_generation_result type for a list of the other
    % possible values of DTDResult and their meanings.
    %
:- pred write_xml_doc_general(Stream::in, T::in,
    element_mapping::in(element_mapping), maybe_stylesheet::in,
    maybe_dtd::in, dtd_generation_result::out, State::di, State::uo) is det
    <= stream.writer(Stream, string, State).

    % write_xml_doc_general_cc(Stream, Term, ElementMapping, MaybeStyleSheet,
    %    MaybeDTD, DTDResult, !State):
    %
    % Write Term to the current file output stream as an XML document using
    % ElementMapping as the scheme to map functors to elements.
    % MaybeStyleSheet and MaybeDTD specify whether or not a stylesheet
    % reference and/or a DTD should be included. Any non-canonical terms
    % will be written out in full. If an embedded DTD is requested, but
    % it is not possible to generate a DTD for Term using ElementMapping,
    % then a value other than `ok' is returned in DTDResult and nothing is
    % written out. See the dtd_generation_result type for a list of the
    % other possible values of DTDResult and their meanings.
    %
:- pred write_xml_doc_general_cc(Stream::in, T::in,
    element_mapping::in(element_mapping), maybe_stylesheet::in,
    maybe_dtd::in, dtd_generation_result::out, State::di, State::uo)
    is cc_multi <= stream.writer(Stream, string, State).

    % can_generate_dtd(ElementMapping, Type) = Result:
    %
    % Check if a DTD can be generated for the given Type using the
    % functor-to-element mapping scheme ElementMapping. Return `ok' if it
    % is possible to generate a DTD. See the documentation of the
    % dtd_generation_result type for the meaning of the return value when
    % it is not `ok'.
    %
:- func can_generate_dtd(element_mapping::in(element_mapping),
    type_desc::in) = (dtd_generation_result::out) is det.

    % write_dtd(Stream, Term, ElementMapping, DTDResult, !State):
    %
    % Write a DTD for the given term to the current file output stream using
    % ElementMapping to map functors to elements. If a DTD
    % cannot be generated for Term using ElementMapping then a value
    % other than `ok' is returned in DTDResult and nothing is written.
    % See the dtd_generation_result type for a list of the other
    % possible values of DTDResult and their meanings.
    %
:- pred write_dtd(Stream::in, T::unused,
    element_mapping::in(element_mapping), dtd_generation_result::out,
    State::di, State::uo) is det
    <= stream.writer(Stream, string, State).

    % write_dtd_for_type(Stream, Type, ElementMapping, DTDResult, !State):
    %
    % Write a DTD for the given type to the given stream. If a
    % DTD cannot be generated for Type using ElementMapping then a value
    % other than `ok' is returned in DTDResult and nothing is written.
    % See the dtd_generation_result type for a list of the other
    % possible values of DTDResult and their meanings.
    %
:- pred write_dtd_from_type(Stream::in, type_desc::in,
    element_mapping::in(element_mapping), dtd_generation_result::out,
    State::di, State::uo) is det <= stream.writer(Stream, string, State).

    % write_xml_element_general(Stream, NonCanon, MakeElement, IndentLevel,
    %   Term, !State):
    %
    % Write XML elements for the given term and all its descendents,
    % using IndentLevel as the initial indentation level (each
    % indentation level is one tab character) and using the MakeElement
    % predicate to map functors to elements. No <?xml ... ?>
    % header will be written. Non-canonical terms will be handled
    % according to the value of NonCanon. See the deconstruct
    % module in the standard library for more information on this argument.
    %
:- pred write_xml_element_general(Stream, deconstruct.noncanon_handling,
    element_mapping, int, T, State, State)
    <= stream.writer(Stream, string, State).
:- mode write_xml_element_general(in, in(do_not_allow), in(element_mapping),
    in, in, di, uo) is det.
:- mode write_xml_element_general(in, in(canonicalize), in(element_mapping),
    in, in, di, uo) is det.
:- mode write_xml_element_general(in, in(include_details_cc),
    in(element_mapping), in, in, di, uo) is cc_multi.
:- mode write_xml_element_general(in, in, in(element_mapping),
    in, in, di, uo) is cc_multi.

%--------------------------------------------------%
%--------------------------------------------------%


Next: , Previous: , Up: Top   [Contents]