001/*
002 * ============================================================================
003 * Copyright © 2002-2023 by Thomas Thrien.
004 * All Rights Reserved.
005 * ============================================================================
006 * Licensed to the public under the agreements of the GNU Lesser General Public
007 * License, version 3.0 (the "License"). You may obtain a copy of the License at
008 *
009 *      http://www.gnu.org/licenses/lgpl.html
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 * License for the specific language governing permissions and limitations
015 * under the License.
016 */
017
018package org.tquadrat.foundation.xml.builder.spi;
019
020import static java.lang.Math.max;
021import static java.lang.String.format;
022import static org.apiguardian.api.API.Status.MAINTAINED;
023import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING;
024import static org.tquadrat.foundation.lang.Objects.nonNull;
025import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
026import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument;
027
028import java.util.Collection;
029import java.util.Map;
030
031import org.apiguardian.api.API;
032import org.tquadrat.foundation.annotation.ClassVersion;
033import org.tquadrat.foundation.annotation.UtilityClass;
034import org.tquadrat.foundation.exception.PrivateConstructorForStaticClassCalledError;
035import org.tquadrat.foundation.xml.builder.Namespace;
036
037/**
038 *  Helper method for the conversion of SGML elements into a String.
039 *
040 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
041 *  @version $Id: SGMLPrinter.java 1071 2023-09-30 01:49:32Z tquadrat $
042 *  @since 0.0.5
043 *
044 *  @UMLGraph.link
045 */
046@UtilityClass
047@ClassVersion( sourceVersion = "$Id: SGMLPrinter.java 1071 2023-09-30 01:49:32Z tquadrat $" )
048@API( status = MAINTAINED, since = "0.0.5" )
049public final class SGMLPrinter
050{
051        /*-----------*\
052    ====** Constants **========================================================
053        \*-----------*/
054    /**
055     *  The tabulator size for pretty printing: {@value}
056     */
057    public static final int TAB_SIZE = 4;
058
059        /*--------------*\
060    ====** Constructors **=====================================================
061        \*--------------*/
062    /**
063     *  No instance of this class allowed.
064     */
065    private SGMLPrinter() { throw new PrivateConstructorForStaticClassCalledError( SGMLPrinter.class ); }
066
067        /*---------*\
068    ====** Methods **==========================================================
069        \*---------*/
070    /**
071     *  Returns the attributes and their values, together with the namespaces,
072     *  as a single formatted string.
073     *
074     *  @param  indentationLevel    The indentation level.
075     *  @param  prettyPrint The pretty print flag.
076     *  @param  elementName The name of the owning element.
077     *  @param  attributes  The attributes.
078     *  @param  namespaces  The namespaces.
079     *  @return The attributes string.
080     */
081    @API( status = MAINTAINED, since = "0.0.5" )
082    public static final String composeAttributesString( final int indentationLevel, final boolean prettyPrint, final String elementName, final Map<String,String> attributes, final Collection<Namespace> namespaces )
083    {
084        requireNotEmptyArgument( elementName, "elementName" );
085        requireNonNullArgument( attributes, "attributes" );
086        requireNonNullArgument( namespaces, "namespaces" );
087
088        var retValue = EMPTY_STRING;
089        if( !attributes.isEmpty() || !namespaces.isEmpty() )
090        {
091            //---* Determine the filler *--------------------------------------
092            final var filler = prettyPrint ? "\n" + repeat( indentationLevel, elementName.length() + 1 ) : EMPTY_STRING;
093
094            //---* Create the buffer *-----------------------------------------
095            final var len = (filler.length() + 16) * (attributes.size() + namespaces.size());
096            final var buffer = new StringBuilder( len );
097
098            //---* Add the namespaces *----------------------------------------
099            for( final var namespace : namespaces )
100            {
101                if( !buffer.isEmpty() ) buffer.append( filler );
102                buffer.append( " " ).append( namespace.toString() );
103            }
104
105            //---* Add the attributes *----------------------------------------
106            attributes.forEach( (key,value) ->
107            {
108                if( !buffer.isEmpty() ) buffer.append( filler );
109                buffer.append( ' ' )
110                    .append( key )
111                    .append( "='")
112                    .append( value )
113                    .append( '\'' );
114            });
115
116            retValue = buffer.toString();
117        }
118
119        //---* Done *----------------------------------------------------------
120        return retValue;
121    }   //  composeAttributesString()
122
123    /**
124     *  Returns the children as a single formatted string.
125     *
126     *  @param  indentationLevel    The indentation level.
127     *  @param  prettyPrint The pretty print flag.
128     *  @param  parent  The parent element.
129     *  @param  children    The children.
130     *  @return The children string.
131     */
132    @SuppressWarnings( "OverlyComplexMethod" )
133    @API( status = MAINTAINED, since = "0.0.5" )
134    public static final String composeChildrenString( final int indentationLevel, final boolean prettyPrint, final Element parent, final Collection<? extends Element> children )
135    {
136        requireNonNullArgument( parent, "parent" );
137
138        var retValue = EMPTY_STRING;
139        if( !requireNonNullArgument( children, "children" ).isEmpty() )
140        {
141            //---* Calculate the indentation *---------------------------------
142            /*
143             * If the direct parent is an inline element, the block is false.
144             */
145            final var grandParent = parent.getParent();
146            final var block = grandParent.map( element -> element.isBlock() && parent.isBlock() ).orElseGet( parent::isBlock ).booleanValue();
147            var filler = (prettyPrint && block) && (indentationLevel > 0) ? "\n" + repeat( indentationLevel ) : EMPTY_STRING;
148
149            //---* Render the children *---------------------------------------
150            final var buffer = new StringBuilder( 1024 );
151
152            final var newIndentationLevel = block ? indentationLevel + 1 : indentationLevel;
153            Element lastChild = null;
154            for( final var child : children )
155            {
156                buffer.append( child.toString( newIndentationLevel, prettyPrint ) );
157                lastChild = child;
158            }
159            if( nonNull( lastChild ) && lastChild.isBlock() )
160            {
161                if( prettyPrint && block && (indentationLevel == 0) )
162                {
163                    buffer.append( "\n" );
164                }
165                else
166                {
167                    if( (block != parent.isBlock()) && prettyPrint && (indentationLevel > 0) )
168                    {
169                        filler = "\n" + repeat( indentationLevel - 1 );
170                    }
171                    buffer.append( filler );
172                }
173            }
174            retValue = buffer.toString();
175        }
176
177        //---* Done *----------------------------------------------------------
178        return retValue;
179    }   //  composeChildrenString()
180
181    /**
182     *  Returns the given document as a single formatted string.
183     *
184     *  @param  prettyPrint The pretty print flag.
185     *  @param  document    The document.
186     *  @return The element string.
187     */
188    @SuppressWarnings( "Convert2streamapi" )
189    @API( status = MAINTAINED, since = "0.0.5" )
190    public static final String composeDocumentString( final boolean prettyPrint, final Document<? extends Element> document )
191    {
192        final var retValue = new StringBuilder();
193        for( final var child : requireNonNullArgument( document, "document" ).getChildren() )
194        {
195            retValue.append( child.toString( 0, prettyPrint ) );
196        }
197
198        //---* Done *----------------------------------------------------------
199        return retValue.toString();
200    }   //  composeElementString()
201
202    /**
203     *  <p>{@summary Returns the given element as a single formatted
204     *  string.}</p>
205     *  <p>The argument {@code selfClosing} exists for some HTML elements
206     *  like {@code <script>}; in pure XML, all elements are self-closing when
207     *  empty, while other flavours may define elements that always need a
208     *  closing tag. Therefore</p>
209     *  <pre><code>  &hellip;
210     *  &lt;script/&gt;
211     *  &hellip;</code></pre>
212     *  <p>is valid in pure XML, but not in HTML where it has to be</p>
213     *  <pre><code>  &hellip;
214     *  &lt;script&gt;&lt;/script&gt;
215     *  &hellip;</code></pre>
216     *
217     *  @param  indentationLevel    The indentation level.
218     *  @param  prettyPrint The pretty print flag.
219     *  @param  element The element.
220     *  @param  selfClosing {@code true} if an empty element is self-closing or
221     *      {@code false} if an empty element still needs a closing tag.
222     *  @return The element string.
223     */
224    @SuppressWarnings( "BooleanParameter" )
225    @API( status = MAINTAINED, since = "0.0.5" )
226    public static final String composeElementString( final int indentationLevel, final boolean prettyPrint, final Element element, final boolean selfClosing )
227    {
228        String retValue = null;
229
230        //---* Calculate the indentation *-------------------------------------
231        /*
232         * If the direct parent is an inline element, the block is false.
233         */
234        final var parent = requireNonNullArgument( element, "element" ).getParent();
235        final var block = parent.map( value -> value.isBlock() && element.isBlock() ).orElseGet( element::isBlock ).booleanValue();
236        final var filler = (prettyPrint && block) ? "\n" + repeat( indentationLevel ) : EMPTY_STRING;
237
238        //---* Render the element *--------------------------------------------
239        final var elementName = element.getElementName();
240        if( !selfClosing || element.hasChildren() )
241        {
242            final var buffer = new StringBuilder( 1024 );
243
244            //---* The opening tag *-------------------------------------------
245            buffer.append( format( "%3$s<%1$s%2$s>", elementName, composeAttributesString( indentationLevel, prettyPrint, elementName, element.getAttributes(), element.getNamespaces() ), filler ) );
246
247            //---* The children *----------------------------------------------
248            if( element.hasChildren() )
249            {
250                buffer.append( composeChildrenString( indentationLevel, prettyPrint, element, element.getChildren() ) );
251            }
252
253            //---* The closing tag *-------------------------------------------
254            buffer.append( format( "</%1$s>", elementName ) );
255            retValue = buffer.toString();
256        }
257        else
258        {
259            retValue = format( "%3$s<%1$s%2$s/>", elementName, composeAttributesString( indentationLevel, prettyPrint, elementName, element.getAttributes(), element.getNamespaces() ), filler );
260        }
261
262        //---* Done *----------------------------------------------------------
263        return retValue;
264    }   //  composeElementString()
265
266    /**
267     *  Returns the namespaces as a single formatted string.
268     *
269     *  @param  indentationLevel    The indentation level.
270     *  @param  prettyPrint The pretty print flag.
271     *  @param  elementName The name of the owning element.
272     *  @param  namespaces  The namespaces.
273     *  @return The namespaces string.
274     */
275    @API( status = MAINTAINED, since = "0.0.5" )
276    public static final String composeNamespaceString( final int indentationLevel, final boolean prettyPrint, final String elementName, final Collection<Namespace> namespaces )
277    {
278        requireNotEmptyArgument( elementName, "elementName" );
279
280        var retValue = EMPTY_STRING;
281        if( !requireNonNullArgument( namespaces, "namespaces" ).isEmpty() )
282        {
283            //---* Determine the filler *--------------------------------------
284            final var filler = prettyPrint ? "\n" + repeat( indentationLevel, elementName.length() + 1 ) : EMPTY_STRING;
285
286            //---* Create the buffer *-----------------------------------------
287            final var len = (filler.length() + 16) * namespaces.size();
288            final var buffer = new StringBuilder( len );
289
290            //---* Add the namespaces *----------------------------------------
291            for( final var namespace : namespaces )
292            {
293                if( !buffer.isEmpty() ) buffer.append( filler );
294                buffer.append( " " ).append( namespace.toString() );
295            }
296            retValue = buffer.toString();
297        }
298
299        //---* Done *----------------------------------------------------------
300        return retValue;
301    }   //  composeNamespaceString()
302
303    /**
304     *  <p>{@summary Returns a String, consisting only of blanks, with the
305     *  length that is determined by the given indentation level, multiplied
306     *  by the
307     *  {@link #TAB_SIZE}
308     *  (= {@value #TAB_SIZE}), plus the given number of additional
309     *  blanks.}</p>
310     *  <p>Negative values for either the indentation level or the number of
311     *  additional blanks are treated as 0.</p>
312     *
313     *  @param  indentationLevel    The indentation level.
314     *  @param  additionalBlanks    The number of additional blanks.
315     *  @return The resulting String.
316     */
317    @API( status = MAINTAINED, since = "0.0.5" )
318    public static final String repeat( final int indentationLevel, final int additionalBlanks )
319    {
320        final var count = max( 0, indentationLevel ) * TAB_SIZE + max( 0, additionalBlanks );
321        final var retValue = count > 0 ? " ".repeat( count ) : EMPTY_STRING;
322
323        //---* Done *----------------------------------------------------------
324        return retValue;
325    }   //  repeat()
326
327    /**
328     *  Returns a String, consisting only of blanks, with the length that is
329     *  determined by the given indentation level, multiplied by the
330     *  {@link #TAB_SIZE}
331     *  (= {@value #TAB_SIZE}).
332     *
333     *  @param  indentationLevel    The indentation level; a negative value is
334     *      treated as 0.
335     *  @return The resulting String.
336     */
337    @API( status = MAINTAINED, since = "0.0.5" )
338    public static final String repeat( final int indentationLevel )
339    {
340        return repeat( indentationLevel, 0 );
341    }   //  repeat()
342}
343//  class SGMLPrinter
344
345/*
346 *  End of File
347 */