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.packet.NamedElement; 026import org.jivesoftware.smack.util.XmlStringBuilder; 027 028public class MarkupElement implements ExtensionElement { 029 030 public static final String NAMESPACE = "urn:xmpp:markup:0"; 031 public static final String ELEMENT = "markup"; 032 033 private final List<MarkupChildElement> childElements; 034 035 /** 036 * Create a new MarkupElement. 037 * 038 * @param childElements child elements. 039 */ 040 public MarkupElement(List<MarkupChildElement> childElements) { 041 this.childElements = Collections.unmodifiableList(childElements); 042 } 043 044 /** 045 * Return a new Builder for Message Markup elements. 046 * @return builder. 047 */ 048 public static Builder getBuilder() { 049 return new Builder(); 050 } 051 052 /** 053 * Return a list of all child elements. 054 * @return children 055 */ 056 public List<MarkupChildElement> getChildElements() { 057 return childElements; 058 } 059 060 @Override 061 public String getNamespace() { 062 return NAMESPACE; 063 } 064 065 @Override 066 public String getElementName() { 067 return ELEMENT; 068 } 069 070 @Override 071 public XmlStringBuilder toXML(String enclosingNamespace) { 072 XmlStringBuilder xml = new XmlStringBuilder(this).rightAngleBracket(); 073 074 for (MarkupChildElement child : getChildElements()) { 075 xml.append(child.toXML(null)); 076 } 077 078 xml.closeElement(this); 079 return xml; 080 } 081 082 083 084 public static final class Builder { 085 086 private final List<SpanElement> spans = new ArrayList<>(); 087 private final List<BlockQuoteElement> quotes = new ArrayList<>(); 088 private final List<CodeBlockElement> codes = new ArrayList<>(); 089 private final List<ListElement> lists = new ArrayList<>(); 090 091 private Builder() { 092 093 } 094 095 /** 096 * Mark a section of a message as deleted. 097 * 098 * @param start start index 099 * @param end end index 100 * @return builder 101 */ 102 public Builder setDeleted(int start, int end) { 103 return addSpan(start, end, Collections.singleton(SpanElement.SpanStyle.deleted)); 104 } 105 106 /** 107 * Mark a section of a message as emphasized. 108 * 109 * @param start start index 110 * @param end end index 111 * @return builder 112 */ 113 public Builder setEmphasis(int start, int end) { 114 return addSpan(start, end, Collections.singleton(SpanElement.SpanStyle.emphasis)); 115 } 116 117 /** 118 * Mark a section of a message as inline code. 119 * 120 * @param start start index 121 * @param end end index 122 * @return builder 123 */ 124 public Builder setCode(int start, int end) { 125 return addSpan(start, end, Collections.singleton(SpanElement.SpanStyle.code)); 126 } 127 128 /** 129 * Add a span element. 130 * 131 * @param start start index 132 * @param end end index 133 * @param styles list of text styles for that span 134 * @return builder 135 */ 136 public Builder addSpan(int start, int end, Set<SpanElement.SpanStyle> styles) { 137 verifyStartEnd(start, end); 138 139 for (SpanElement other : spans) { 140 if ((start >= other.getStart() && start <= other.getEnd()) || 141 (end >= other.getStart() && end <= other.getEnd())) { 142 throw new IllegalArgumentException("Spans MUST NOT overlap each other."); 143 } 144 } 145 146 spans.add(new SpanElement(start, end, styles)); 147 return this; 148 } 149 150 /** 151 * Mark a section of a message as block quote. 152 * 153 * @param start start index 154 * @param end end index 155 * @return builder 156 */ 157 public Builder setBlockQuote(int start, int end) { 158 verifyStartEnd(start, end); 159 160 for (BlockQuoteElement other : quotes) { 161 // 1 if out, 0 if on, -1 if in 162 Integer s = start; 163 Integer e = end; 164 int startPos = s.compareTo(other.getStart()) * s.compareTo(other.getEnd()); 165 int endPos = e.compareTo(other.getStart()) * e.compareTo(other.getEnd()); 166 int allowed = startPos * endPos; 167 168 if (allowed < 1) { 169 throw new IllegalArgumentException("BlockQuotes MUST NOT overlap each others boundaries"); 170 } 171 } 172 173 quotes.add(new BlockQuoteElement(start, end)); 174 return this; 175 } 176 177 /** 178 * Mark a section of a message as a code block. 179 * 180 * @param start start index 181 * @param end end index 182 * @return builder 183 */ 184 public Builder setCodeBlock(int start, int end) { 185 verifyStartEnd(start, end); 186 187 codes.add(new CodeBlockElement(start, end)); 188 return this; 189 } 190 191 /** 192 * Begin a list. 193 * 194 * @return list builder 195 */ 196 public Builder.ListBuilder beginList() { 197 return new Builder.ListBuilder(this); 198 } 199 200 public static final class ListBuilder { 201 private final Builder markup; 202 private final ArrayList<ListElement.ListEntryElement> entries = new ArrayList<>(); 203 private int end = -1; 204 205 private ListBuilder(Builder markup) { 206 this.markup = markup; 207 } 208 209 /** 210 * Add an entry to the list. 211 * The start index of an entry must correspond to the end index of the previous entry 212 * (if a previous entry exists.) 213 * 214 * @param start start index 215 * @param end end index 216 * @return list builder 217 */ 218 public Builder.ListBuilder addEntry(int start, int end) { 219 verifyStartEnd(start, end); 220 221 ListElement.ListEntryElement last = entries.size() == 0 ? null : entries.get(entries.size() - 1); 222 // Entries themselves do not store end values, that's why we store the last entries end value in this.end 223 if (last != null && start != this.end) { 224 throw new IllegalArgumentException("Next entries start must be equal to last entries end (" + this.end + ")."); 225 } 226 entries.add(new ListElement.ListEntryElement(start)); 227 this.end = end; 228 229 return this; 230 } 231 232 /** 233 * End the list. 234 * 235 * @return builder 236 */ 237 public Builder endList() { 238 if (entries.size() > 0) { 239 ListElement.ListEntryElement first = entries.get(0); 240 ListElement list = new ListElement(first.getStart(), end, entries); 241 markup.lists.add(list); 242 } 243 244 return markup; 245 } 246 } 247 248 /** 249 * Build a Message Markup element. 250 * 251 * @return extension element 252 */ 253 public MarkupElement build() { 254 List<MarkupElement.MarkupChildElement> children = new ArrayList<>(); 255 children.addAll(spans); 256 children.addAll(quotes); 257 children.addAll(codes); 258 children.addAll(lists); 259 return new MarkupElement(children); 260 } 261 262 private static void verifyStartEnd(int start, int end) { 263 if (start >= end || start < 0) { 264 throw new IllegalArgumentException("Start value (" + start + ") MUST be greater equal than 0 " + 265 "and MUST be smaller than end value (" + end + ")."); 266 } 267 } 268 } 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 /** 284 * Interface for child elements. 285 */ 286 public interface MarkupChildElement extends NamedElement { 287 288 String ATTR_START = "start"; 289 String ATTR_END = "end"; 290 291 /** 292 * Return the start index of this element. 293 * 294 * @return start index 295 */ 296 int getStart(); 297 298 /** 299 * Return the end index of this element. 300 * 301 * @return end index 302 */ 303 int getEnd(); 304 } 305 306 /** 307 * Interface for block level child elements. 308 */ 309 public interface BlockLevelMarkupElement extends MarkupChildElement { 310 311 } 312}