/** * * Copyright © 2018 Paul Schaub * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smackx.message_markup.element; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.util.XmlStringBuilder; public class MarkupElement implements ExtensionElement { public static final String NAMESPACE = "urn:xmpp:markup:0"; public static final String ELEMENT = "markup"; private final List childElements; /** * Create a new MarkupElement. * * @param childElements child elements. */ public MarkupElement(List childElements) { this.childElements = Collections.unmodifiableList(childElements); } /** * Return a new Builder for Message Markup elements. * @return builder. */ public static Builder getBuilder() { return new Builder(); } /** * Return a list of all child elements. * @return children TODO javadoc me please */ public List getChildElements() { return childElements; } @Override public String getNamespace() { return NAMESPACE; } @Override public String getElementName() { return ELEMENT; } @Override public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { XmlStringBuilder xml = new XmlStringBuilder(this).rightAngleBracket(); for (MarkupChildElement child : getChildElements()) { xml.append(child.toXML()); } xml.closeElement(this); return xml; } public static final class Builder { private final List spans = new ArrayList<>(); private final List quotes = new ArrayList<>(); private final List codes = new ArrayList<>(); private final List lists = new ArrayList<>(); private Builder() { } /** * Mark a section of a message as deleted. * * @param start start index * @param end end index * @return builder TODO javadoc me please */ public Builder setDeleted(int start, int end) { return addSpan(start, end, Collections.singleton(SpanElement.SpanStyle.deleted)); } /** * Mark a section of a message as emphasized. * * @param start start index * @param end end index * @return builder TODO javadoc me please */ public Builder setEmphasis(int start, int end) { return addSpan(start, end, Collections.singleton(SpanElement.SpanStyle.emphasis)); } /** * Mark a section of a message as inline code. * * @param start start index * @param end end index * @return builder TODO javadoc me please */ public Builder setCode(int start, int end) { return addSpan(start, end, Collections.singleton(SpanElement.SpanStyle.code)); } /** * Add a span element. * * @param start start index * @param end end index * @param styles list of text styles for that span * @return builder TODO javadoc me please */ public Builder addSpan(int start, int end, Set styles) { verifyStartEnd(start, end); for (SpanElement other : spans) { if ((start >= other.getStart() && start <= other.getEnd()) || (end >= other.getStart() && end <= other.getEnd())) { throw new IllegalArgumentException("Spans MUST NOT overlap each other."); } } spans.add(new SpanElement(start, end, styles)); return this; } /** * Mark a section of a message as block quote. * * @param start start index * @param end end index * @return builder TODO javadoc me please */ public Builder setBlockQuote(int start, int end) { verifyStartEnd(start, end); for (BlockQuoteElement other : quotes) { // 1 if out, 0 if on, -1 if in Integer s = start; Integer e = end; int startPos = s.compareTo(other.getStart()) * s.compareTo(other.getEnd()); int endPos = e.compareTo(other.getStart()) * e.compareTo(other.getEnd()); int allowed = startPos * endPos; if (allowed < 1) { throw new IllegalArgumentException("BlockQuotes MUST NOT overlap each others boundaries"); } } quotes.add(new BlockQuoteElement(start, end)); return this; } /** * Mark a section of a message as a code block. * * @param start start index * @param end end index * @return builder TODO javadoc me please */ public Builder setCodeBlock(int start, int end) { verifyStartEnd(start, end); codes.add(new CodeBlockElement(start, end)); return this; } /** * Begin a list. * * @return list builder */ public Builder.ListBuilder beginList() { return new Builder.ListBuilder(this); } public static final class ListBuilder { private final Builder markup; private final ArrayList entries = new ArrayList<>(); private int end = -1; private ListBuilder(Builder markup) { this.markup = markup; } /** * Add an entry to the list. * The start index of an entry must correspond to the end index of the previous entry * (if a previous entry exists.) * * @param start start index * @param end end index * @return list builder */ public Builder.ListBuilder addEntry(int start, int end) { verifyStartEnd(start, end); ListElement.ListEntryElement last = entries.size() == 0 ? null : entries.get(entries.size() - 1); // Entries themselves do not store end values, that's why we store the last entries end value in this.end if (last != null && start != this.end) { throw new IllegalArgumentException("Next entries start must be equal to last entries end (" + this.end + ")."); } entries.add(new ListElement.ListEntryElement(start)); this.end = end; return this; } /** * End the list. * * @return builder TODO javadoc me please */ public Builder endList() { if (entries.size() > 0) { ListElement.ListEntryElement first = entries.get(0); ListElement list = new ListElement(first.getStart(), end, entries); markup.lists.add(list); } return markup; } } /** * Build a Message Markup element. * * @return extension element */ public MarkupElement build() { List children = new ArrayList<>(); children.addAll(spans); children.addAll(quotes); children.addAll(codes); children.addAll(lists); return new MarkupElement(children); } private static void verifyStartEnd(int start, int end) { if (start >= end || start < 0) { throw new IllegalArgumentException("Start value (" + start + ") MUST be greater equal than 0 " + "and MUST be smaller than end value (" + end + ")."); } } } /** * Interface for child elements. */ public abstract static class MarkupChildElement implements ExtensionElement { public static final String ATTR_START = "start"; public static final String ATTR_END = "end"; private final int start, end; protected MarkupChildElement(int start, int end) { this.start = start; this.end = end; } /** * Return the start index of this element. * * @return start index */ public final int getStart() { return start; } /** * Return the end index of this element. * * @return end index */ public final int getEnd() { return end; } @Override public final String getNamespace() { return NAMESPACE; } @Override public final XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) { XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace); xml.attribute(ATTR_START, getStart()); xml.attribute(ATTR_END, getEnd()); afterXmlPrelude(xml); return xml; } protected abstract void afterXmlPrelude(XmlStringBuilder xml); } public abstract static class NonEmptyChildElement extends MarkupChildElement { protected NonEmptyChildElement(int start, int end) { super(start, end); } @Override protected final void afterXmlPrelude(XmlStringBuilder xml) { xml.rightAngleBracket(); appendInnerXml(xml); xml.closeElement(this); } protected abstract void appendInnerXml(XmlStringBuilder xml); } /** * Interface for block level child elements. */ public abstract static class BlockLevelMarkupElement extends MarkupChildElement { protected BlockLevelMarkupElement(int start, int end) { super(start, end); } @Override protected final void afterXmlPrelude(XmlStringBuilder xml) { xml.closeEmptyElement(); } } }