001/**
002 *
003 * Copyright © 2018 Paul Schaub
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
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,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.message_markup.element;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.List;
022import java.util.Set;
023
024import org.jivesoftware.smack.packet.ExtensionElement;
025import org.jivesoftware.smack.util.XmlStringBuilder;
026
027public class MarkupElement implements ExtensionElement {
028
029    public static final String NAMESPACE = "urn:xmpp:markup:0";
030    public static final String ELEMENT = "markup";
031
032    private final List<MarkupChildElement> childElements;
033
034    /**
035     * Create a new MarkupElement.
036     *
037     * @param childElements child elements.
038     */
039    public MarkupElement(List<MarkupChildElement> childElements) {
040        this.childElements = Collections.unmodifiableList(childElements);
041    }
042
043    /**
044     * Return a new Builder for Message Markup elements.
045     * @return builder.
046     */
047    public static Builder getBuilder() {
048        return new Builder();
049    }
050
051    /**
052     * Return a list of all child elements.
053     * @return children TODO javadoc me please
054     */
055    public List<MarkupChildElement> getChildElements() {
056        return childElements;
057    }
058
059    @Override
060    public String getNamespace() {
061        return NAMESPACE;
062    }
063
064    @Override
065    public String getElementName() {
066        return ELEMENT;
067    }
068
069    @Override
070    public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) {
071        XmlStringBuilder xml = new XmlStringBuilder(this).rightAngleBracket();
072
073        for (MarkupChildElement child : getChildElements()) {
074            xml.append(child.toXML());
075        }
076
077        xml.closeElement(this);
078        return xml;
079    }
080
081
082
083    public static final class Builder {
084
085        private final List<SpanElement> spans = new ArrayList<>();
086        private final List<BlockQuoteElement> quotes = new ArrayList<>();
087        private final List<CodeBlockElement> codes = new ArrayList<>();
088        private final List<ListElement> lists = new ArrayList<>();
089
090        private Builder() {
091
092        }
093
094        /**
095         * Mark a section of a message as deleted.
096         *
097         * @param start start index
098         * @param end end index
099         * @return builder TODO javadoc me please
100         */
101        public Builder setDeleted(int start, int end) {
102            return addSpan(start, end, Collections.singleton(SpanElement.SpanStyle.deleted));
103        }
104
105        /**
106         * Mark a section of a message as emphasized.
107         *
108         * @param start start index
109         * @param end end index
110         * @return builder TODO javadoc me please
111         */
112        public Builder setEmphasis(int start, int end) {
113            return addSpan(start, end, Collections.singleton(SpanElement.SpanStyle.emphasis));
114        }
115
116        /**
117         * Mark a section of a message as inline code.
118         *
119         * @param start start index
120         * @param end end index
121         * @return builder TODO javadoc me please
122         */
123        public Builder setCode(int start, int end) {
124            return addSpan(start, end, Collections.singleton(SpanElement.SpanStyle.code));
125        }
126
127        /**
128         * Add a span element.
129         *
130         * @param start start index
131         * @param end end index
132         * @param styles list of text styles for that span
133         * @return builder TODO javadoc me please
134         */
135        public Builder addSpan(int start, int end, Set<SpanElement.SpanStyle> styles) {
136            verifyStartEnd(start, end);
137
138            for (SpanElement other : spans) {
139                if ((start >= other.getStart() && start <= other.getEnd()) ||
140                        (end >= other.getStart() && end <= other.getEnd())) {
141                    throw new IllegalArgumentException("Spans MUST NOT overlap each other.");
142                }
143            }
144
145            spans.add(new SpanElement(start, end, styles));
146            return this;
147        }
148
149        /**
150         * Mark a section of a message as block quote.
151         *
152         * @param start start index
153         * @param end end index
154         * @return builder TODO javadoc me please
155         */
156        public Builder setBlockQuote(int start, int end) {
157            verifyStartEnd(start, end);
158
159            for (BlockQuoteElement other : quotes) {
160                // 1 if out, 0 if on, -1 if in
161                Integer s = start;
162                Integer e = end;
163                int startPos = s.compareTo(other.getStart()) * s.compareTo(other.getEnd());
164                int endPos = e.compareTo(other.getStart()) * e.compareTo(other.getEnd());
165                int allowed = startPos * endPos;
166
167                if (allowed < 1) {
168                    throw new IllegalArgumentException("BlockQuotes MUST NOT overlap each others boundaries");
169                }
170            }
171
172            quotes.add(new BlockQuoteElement(start, end));
173            return this;
174        }
175
176        /**
177         * Mark a section of a message as a code block.
178         *
179         * @param start start index
180         * @param end end index
181         * @return builder TODO javadoc me please
182         */
183        public Builder setCodeBlock(int start, int end) {
184            verifyStartEnd(start, end);
185
186            codes.add(new CodeBlockElement(start, end));
187            return this;
188        }
189
190        /**
191         * Begin a list.
192         *
193         * @return list builder
194         */
195        public Builder.ListBuilder beginList() {
196            return new Builder.ListBuilder(this);
197        }
198
199        public static final class ListBuilder {
200            private final Builder markup;
201            private final ArrayList<ListElement.ListEntryElement> entries = new ArrayList<>();
202            private int end = -1;
203
204            private ListBuilder(Builder markup) {
205                this.markup = markup;
206            }
207
208            /**
209             * Add an entry to the list.
210             * The start index of an entry must correspond to the end index of the previous entry
211             * (if a previous entry exists.)
212             *
213             * @param start start index
214             * @param end end index
215             * @return list builder
216             */
217            public Builder.ListBuilder addEntry(int start, int end) {
218                verifyStartEnd(start, end);
219
220                ListElement.ListEntryElement last = entries.size() == 0 ? null : entries.get(entries.size() - 1);
221                // Entries themselves do not store end values, that's why we store the last entries end value in this.end
222                if (last != null && start != this.end) {
223                    throw new IllegalArgumentException("Next entries start must be equal to last entries end (" + this.end + ").");
224                }
225                entries.add(new ListElement.ListEntryElement(start));
226                this.end = end;
227
228                return this;
229            }
230
231            /**
232             * End the list.
233             *
234             * @return builder TODO javadoc me please
235             */
236            public Builder endList() {
237                if (entries.size() > 0) {
238                    ListElement.ListEntryElement first = entries.get(0);
239                    ListElement list = new ListElement(first.getStart(), end, entries);
240                    markup.lists.add(list);
241                }
242
243                return markup;
244            }
245        }
246
247        /**
248         * Build a Message Markup element.
249         *
250         * @return extension element
251         */
252        public MarkupElement build() {
253            List<MarkupElement.MarkupChildElement> children = new ArrayList<>();
254            children.addAll(spans);
255            children.addAll(quotes);
256            children.addAll(codes);
257            children.addAll(lists);
258            return new MarkupElement(children);
259        }
260
261        private static void verifyStartEnd(int start, int end) {
262            if (start >= end || start < 0) {
263                throw new IllegalArgumentException("Start value (" + start + ") MUST be greater equal than 0 " +
264                        "and MUST be smaller than end value (" + end + ").");
265            }
266        }
267    }
268
269    /**
270     * Interface for child elements.
271     */
272    public abstract static class MarkupChildElement implements ExtensionElement {
273
274        public static final String NAMESPACE = MarkupElement.NAMESPACE;
275
276        public static final String ATTR_START = "start";
277        public static final String ATTR_END = "end";
278
279        private final int start, end;
280
281        protected MarkupChildElement(int start, int end) {
282            this.start = start;
283            this.end = end;
284        }
285
286        /**
287         * Return the start index of this element.
288         *
289         * @return start index
290         */
291        public final int getStart() {
292            return start;
293        }
294
295        /**
296         * Return the end index of this element.
297         *
298         * @return end index
299         */
300        public final int getEnd() {
301            return end;
302        }
303
304        @Override
305        public final String getNamespace() {
306            return NAMESPACE;
307        }
308
309        @Override
310        public final XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) {
311            XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace);
312            xml.attribute(ATTR_START, getStart());
313            xml.attribute(ATTR_END, getEnd());
314
315            afterXmlPrelude(xml);
316            return xml;
317        }
318
319        protected abstract void afterXmlPrelude(XmlStringBuilder xml);
320    }
321
322    public abstract static class NonEmptyChildElement extends MarkupChildElement {
323
324        protected NonEmptyChildElement(int start, int end) {
325            super(start, end);
326        }
327
328        @Override
329        protected final void afterXmlPrelude(XmlStringBuilder xml) {
330            xml.rightAngleBracket();
331
332            appendInnerXml(xml);
333
334            xml.closeElement(this);
335        }
336
337        protected abstract void appendInnerXml(XmlStringBuilder xml);
338
339    }
340
341    /**
342     * Interface for block level child elements.
343     */
344    public abstract static class BlockLevelMarkupElement extends MarkupChildElement {
345
346        protected BlockLevelMarkupElement(int start, int end) {
347            super(start, end);
348        }
349
350        @Override
351        protected final void afterXmlPrelude(XmlStringBuilder xml) {
352            xml.closeEmptyElement();
353        }
354
355    }
356}