diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index c00592041..46470b84d 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -98,6 +98,7 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental | [OMEMO Multi End Message and Object Encryption](omemo.md) | [XEP-XXXX](https://conversations.im/omemo/xep-omemo.html) | Encrypt messages using OMEMO encryption (currently only with smack-omemo-signal -> GPLv3). | | [Consistent Color Generation](consistent_colors.md) | [XEP-0392](http://xmpp.org/extensions/xep-0392.html) | Generate consistent colors for identifiers like usernames to provide a consistent user experience. | | Google GCM JSON payload | n/a | Semantically the same as XEP-0335: JSON Containers | +| [Message Markup](messagemarkup.md) | [XEP-0394](http://xmpp.org/extensions/xep-0394.html)| Style message bodies while keeping body and markup information separated. | Legacy Smack Extensions and currently supported XEPs of smack-legacy diff --git a/documentation/extensions/messagemarkup.md b/documentation/extensions/messagemarkup.md new file mode 100644 index 000000000..fb468673e --- /dev/null +++ b/documentation/extensions/messagemarkup.md @@ -0,0 +1,68 @@ +Message Markup +============== + +[Back](index.md) + +[Message Markup (XEP-0394)](https://xmpp.org/extensions/xep-0394.html) can be used as a an alternative to XHTML-IM to style messages, while keeping the body and markup information strictly separated. +This implementation can *not* be used to render message bodies, but will offer a simple to use interface for creating ExtensionElements which encode the markup information. + +## Usage + +The most important class is the `MarkupElement` class, which contains a Builder. + +To start creating a Message Markup Extension, call `MarkupElement.getBuilder()`. +(Almost) all method calls documented below will be made on the builder. + +Whenever a method call receives a `start` and `end` index, `start` represents the first character, which is affected by the styling, while `end` is the character *after* the last affected character. + +### Inline styling + +Currently there are 3 styles available: +* *emphasis*, which should be rendered by a client as *italic*, or **bold** +* *code*, which should be rendered in `monospace` +* *deleted*, which should be rendered as ~~strikethrough~~. + +Those styles are available by calling `builder.setEmphasis(int start, int end)`, +`builder.setDeleted(int start, int end)` and `builder.setCode(int start, int end)`. + +If you want to apply multiple inline styles to a section, you can do the following: +``` +Set spanStyles = new HashSet<>(); +styles.add(SpanElement.SpanStyle.emphasis); +styles.add(SpanElement.SpanStyle.deleted); +builder.addSpan(start, end, spanStyles); +``` + +Note, that spans cannot overlap one another. + +### Block Level Styling + +Available block level styles are: +* Code blocks, which should be rendered as +``` +blocks +of +code +``` + +* Itemized lists, which should render as + * Lists + * with possibly multiple + * entries + +* Block Quotes, which should be rendered by the client + > as quotes, which + >> also can be nested + +To mark a section as code block, call `builder.setCodeBlock(start, end)`. + +To create a list, call `MarkupElement.Builder.ListBuilder lbuilder = builder.beginList()`, which will return a list builder. +On this you can call `lbuilder.addEntry(start, end)` to add an entry. + +Note: If you add an entry, the start value MUST be equal to the end value of the previous added entry! + +To end the list, call `lbuilder.endList()`, which will return the MessageMarkup builder. + +To create a block quote, call `builder.setBlockQuote(start, end)`. + +Note that block level elements MUST NOT overlap each other boundaries, but may be fully contained (nested) within each other. \ No newline at end of file diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/BlockQuoteElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/BlockQuoteElement.java new file mode 100644 index 000000000..fb22e08af --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/BlockQuoteElement.java @@ -0,0 +1,62 @@ +/** + * + * 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 org.jivesoftware.smack.util.XmlStringBuilder; + +public class BlockQuoteElement implements MarkupElement.BlockLevelMarkupElement { + + public static final String ELEMENT = "bquote"; + + private final int start, end; + + /** + * Create a new Block Quote element. + * + * @param start start index + * @param end end index + */ + public BlockQuoteElement(int start, int end) { + this.start = start; + this.end = end; + } + + @Override + public int getStart() { + return start; + } + + @Override + public int getEnd() { + return end; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(); + xml.halfOpenElement(this); + xml.attribute(ATTR_START, getStart()); + xml.attribute(ATTR_END, getEnd()); + xml.closeEmptyElement(); + return xml; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/CodeBlockElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/CodeBlockElement.java new file mode 100644 index 000000000..d80d02cae --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/CodeBlockElement.java @@ -0,0 +1,62 @@ +/** + * + * 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 org.jivesoftware.smack.util.XmlStringBuilder; + +public class CodeBlockElement implements MarkupElement.BlockLevelMarkupElement { + + public static final String ELEMENT = "bcode"; + + private final int start, end; + + /** + * Create a new Code Block element. + * + * @param start start index + * @param end end index + */ + public CodeBlockElement(int start, int end) { + this.start = start; + this.end = end; + } + + @Override + public int getStart() { + return start; + } + + @Override + public int getEnd() { + return end; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(); + xml.halfOpenElement(this); + xml.attribute(ATTR_START, getStart()); + xml.attribute(ATTR_END, getEnd()); + xml.closeEmptyElement(); + return xml; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/ListElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/ListElement.java new file mode 100644 index 000000000..3f11bba6d --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/ListElement.java @@ -0,0 +1,121 @@ +/** + * + * 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.Collections; +import java.util.List; + +import org.jivesoftware.smack.packet.NamedElement; +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class ListElement implements MarkupElement.MarkupChildElement { + + public static final String ELEMENT = "list"; + public static final String ELEM_LI = "li"; + + private final int start, end; + private final List entries; + + /** + * Create a new List element. + * + * @param start start index of the list + * @param end end index of the list + * @param entries list entries + */ + public ListElement(int start, int end, List entries) { + this.start = start; + this.end = end; + this.entries = Collections.unmodifiableList(entries); + } + + @Override + public int getStart() { + return start; + } + + @Override + public int getEnd() { + return end; + } + + /** + * Return a list of all list entries. + * + * @return entries + */ + public List getEntries() { + return entries; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public CharSequence toXML() { + XmlStringBuilder xml = new XmlStringBuilder(); + xml.halfOpenElement(this); + xml.attribute(ATTR_START, getStart()); + xml.attribute(ATTR_END, getEnd()); + xml.rightAngleBracket(); + + for (ListEntryElement li : getEntries()) { + xml.append(li.toXML()); + } + + xml.closeElement(this); + return xml; + } + + public static class ListEntryElement implements NamedElement { + + private final int start; + + /** + * Create a new ListEntry element. + * + * @param start start index + */ + public ListEntryElement(int start) { + this.start = start; + } + + /** + * Return the start index of this entry. + * @return start index + */ + public int getStart() { + return start; + } + + @Override + public String getElementName() { + return ELEM_LI; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(); + xml.halfOpenElement(this); + xml.attribute(ATTR_START, getStart()); + xml.closeEmptyElement(); + return xml; + } + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/MarkupElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/MarkupElement.java new file mode 100644 index 000000000..ed86d3980 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/MarkupElement.java @@ -0,0 +1,312 @@ +/** + * + * 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.packet.NamedElement; +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 + */ + public List getChildElements() { + return childElements; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public XmlStringBuilder toXML() { + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 interface MarkupChildElement extends NamedElement { + + String ATTR_START = "start"; + String ATTR_END = "end"; + + /** + * Return the start index of this element. + * + * @return start index + */ + int getStart(); + + /** + * Return the end index of this element. + * + * @return end index + */ + int getEnd(); + } + + /** + * Interface for block level child elements. + */ + public interface BlockLevelMarkupElement extends MarkupChildElement { + + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/SpanElement.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/SpanElement.java new file mode 100644 index 000000000..8d3847716 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/SpanElement.java @@ -0,0 +1,93 @@ +/** + * + * 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.Collections; +import java.util.Set; + +import org.jivesoftware.smack.util.XmlStringBuilder; + +public class SpanElement implements MarkupElement.MarkupChildElement { + + public static final String ELEMENT = "span"; + + private final int start, end; + private final Set styles; + + /** + * Create a new Span element. + * + * @param start start index + * @param end end index + * @param styles list of styles that apply to this span + */ + public SpanElement(int start, int end, Set styles) { + this.start = start; + this.end = end; + this.styles = Collections.unmodifiableSet(styles); + } + + @Override + public int getStart() { + return start; + } + + @Override + public int getEnd() { + return end; + } + + /** + * Return all styles of this span. + * + * @return styles + */ + public Set getStyles() { + return styles; + } + + public static final String emphasis = "emphasis"; + public static final String code = "code"; + public static final String deleted = "deleted"; + + public enum SpanStyle { + emphasis, + code, + deleted + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(); + xml.halfOpenElement(this); + xml.attribute(ATTR_START, getStart()); + xml.attribute(ATTR_END, getEnd()); + xml.rightAngleBracket(); + + for (SpanStyle style : getStyles()) { + xml.halfOpenElement(style.toString()).closeEmptyElement(); + } + + xml.closeElement(this); + return xml; + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/package-info.java new file mode 100644 index 000000000..00c2ceabd --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/element/package-info.java @@ -0,0 +1,25 @@ +/** + * + * 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. + */ + +/** + * XEP-0394: Message Markup. + * + * @see XEP-0394: Message + * Markup + * + */ +package org.jivesoftware.smackx.message_markup.element; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/package-info.java new file mode 100644 index 000000000..8aa10ce24 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/package-info.java @@ -0,0 +1,25 @@ +/** + * + * 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. + */ + +/** + * XEP-0394: Message Markup. + * + * @see XEP-0394: Message + * Markup + * + */ +package org.jivesoftware.smackx.message_markup; diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/provider/MarkupElementProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/provider/MarkupElementProvider.java new file mode 100644 index 000000000..00bb1627b --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/provider/MarkupElementProvider.java @@ -0,0 +1,139 @@ +/** + * + * 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.provider; + +import static org.xmlpull.v1.XmlPullParser.END_TAG; +import static org.xmlpull.v1.XmlPullParser.START_TAG; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.util.ParserUtils; +import org.jivesoftware.smackx.message_markup.element.BlockQuoteElement; +import org.jivesoftware.smackx.message_markup.element.CodeBlockElement; +import org.jivesoftware.smackx.message_markup.element.ListElement; +import org.jivesoftware.smackx.message_markup.element.MarkupElement; +import org.jivesoftware.smackx.message_markup.element.SpanElement; + +import org.xmlpull.v1.XmlPullParser; + +public class MarkupElementProvider extends ExtensionElementProvider { + + @Override + public MarkupElement parse(XmlPullParser parser, int initialDepth) throws Exception { + + MarkupElement.Builder markup = MarkupElement.getBuilder(); + + int spanStart = -1, spanEnd = -1; + Set spanStyles = new HashSet<>(); + + int listStart = -1, listEnd = -1; + List lis = new ArrayList<>(); + + while (true) { + int tag = parser.next(); + String name = parser.getName(); + int start, end; + switch (tag) { + case START_TAG: + switch (name) { + case BlockQuoteElement.ELEMENT: + start = ParserUtils.getIntegerAttributeOrThrow(parser, BlockQuoteElement.ATTR_START, + "Message Markup BlockQuoteElement MUST contain a 'start' attribute."); + end = ParserUtils.getIntegerAttributeOrThrow(parser, BlockQuoteElement.ATTR_END, + "Message Markup BlockQuoteElement MUST contain a 'end' attribute."); + markup.setBlockQuote(start, end); + break; + + case CodeBlockElement.ELEMENT: + start = ParserUtils.getIntegerAttributeOrThrow(parser, CodeBlockElement.ATTR_START, + "Message Markup CodeBlockElement MUST contain a 'start' attribute."); + end = ParserUtils.getIntegerAttributeOrThrow(parser, CodeBlockElement.ATTR_END, + "Message Markup CodeBlockElement MUST contain a 'end' attribute."); + markup.setCodeBlock(start, end); + break; + + case SpanElement.ELEMENT: + spanStyles = new HashSet<>(); + spanStart = ParserUtils.getIntegerAttributeOrThrow(parser, SpanElement.ATTR_START, + "Message Markup SpanElement MUST contain a 'start' attribute."); + spanEnd = ParserUtils.getIntegerAttributeOrThrow(parser, SpanElement.ATTR_END, + "Message Markup SpanElement MUST contain a 'end' attribute."); + break; + + case SpanElement.code: + spanStyles.add(SpanElement.SpanStyle.code); + break; + + case SpanElement.emphasis: + spanStyles.add(SpanElement.SpanStyle.emphasis); + break; + + case SpanElement.deleted: + spanStyles.add(SpanElement.SpanStyle.deleted); + break; + + case ListElement.ELEMENT: + lis = new ArrayList<>(); + listStart = ParserUtils.getIntegerAttributeOrThrow(parser, ListElement.ATTR_START, + "Message Markup ListElement MUST contain a 'start' attribute."); + listEnd = ParserUtils.getIntegerAttributeOrThrow(parser, ListElement.ATTR_END, + "Message Markup ListElement MUST contain a 'end' attribute."); + break; + + case ListElement.ELEM_LI: + start = ParserUtils.getIntegerAttributeOrThrow(parser, ListElement.ATTR_START, + "Message Markup ListElement 'li' MUST contain a 'start' attribute."); + lis.add(new ListElement.ListEntryElement(start)); + break; + } + break; + + case END_TAG: + switch (name) { + case SpanElement.ELEMENT: + markup.addSpan(spanStart, spanEnd, spanStyles); + spanStart = -1; spanEnd = -1; + break; + + case ListElement.ELEMENT: + MarkupElement.Builder.ListBuilder listBuilder = markup.beginList(); + if (lis.size() > 0 && lis.get(0).getStart() != listStart) { + throw new SmackException("Error while parsing incoming MessageMarkup ListElement: " + + "'start' attribute of first 'li' element must equal 'start' attribute of list."); + } + for (int i = 0; i < lis.size(); i++) { + int elemStart = lis.get(i).getStart(); + int elemEnd = i < lis.size() - 1 ? lis.get(i + 1).getStart() : listEnd; + listBuilder.addEntry(elemStart, elemEnd); + } + listBuilder.endList(); + break; + + case MarkupElement.ELEMENT: + return markup.build(); + } + + } + } + } + +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/provider/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/provider/package-info.java new file mode 100644 index 000000000..f860b5b18 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/message_markup/provider/package-info.java @@ -0,0 +1,25 @@ +/** + * + * 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. + */ + +/** + * XEP-0394: Message Markup. + * + * @see XEP-0394: Message + * Markup + * + */ +package org.jivesoftware.smackx.message_markup.provider; diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers index 1ef9366a8..32452dfb9 100644 --- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers +++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers @@ -296,4 +296,11 @@ org.jivesoftware.smackx.eme.provider.ExplicitMessageEncryptionProvider + + + markup + urn:xmpp:markup:0 + org.jivesoftware.smackx.message_markup.provider.MarkupElementProvider + + diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/message_markup/MessageMarkupTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_markup/MessageMarkupTest.java new file mode 100644 index 000000000..7afcdc6e7 --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/message_markup/MessageMarkupTest.java @@ -0,0 +1,227 @@ +/** + * + * 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; + +import static junit.framework.TestCase.assertEquals; +import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; + +import java.util.List; + +import org.jivesoftware.smack.test.util.SmackTestSuite; +import org.jivesoftware.smack.test.util.TestUtils; +import org.jivesoftware.smackx.message_markup.element.BlockQuoteElement; +import org.jivesoftware.smackx.message_markup.element.CodeBlockElement; +import org.jivesoftware.smackx.message_markup.element.ListElement; +import org.jivesoftware.smackx.message_markup.element.MarkupElement; +import org.jivesoftware.smackx.message_markup.element.SpanElement; +import org.jivesoftware.smackx.message_markup.provider.MarkupElementProvider; + +import org.junit.Test; +import org.xmlpull.v1.XmlPullParser; + +public class MessageMarkupTest extends SmackTestSuite { + + @Test + public void emphasisTest() throws Exception { + String xml = + "" + + "" + + "" + + "" + + ""; + MarkupElement.Builder m = MarkupElement.getBuilder(); + m.setEmphasis(9, 15); + assertXMLEqual(xml, m.build().toXML().toString()); + + XmlPullParser parser = TestUtils.getParser(xml); + MarkupElement parsed = new MarkupElementProvider().parse(parser); + List children = parsed.getChildElements(); + assertEquals(1, children.size()); + + SpanElement spanElement = (SpanElement) children.get(0); + assertEquals(9, spanElement.getStart()); + assertEquals(15, spanElement.getEnd()); + assertEquals(1, spanElement.getStyles().size()); + assertEquals(SpanElement.SpanStyle.emphasis, spanElement.getStyles().iterator().next()); + } + + @Test + public void codeTest() throws Exception { + String xml = + "" + + "" + + "" + + "" + + ""; + MarkupElement.Builder m = MarkupElement.getBuilder(); + m.setCode(9, 15); + assertXMLEqual(xml, m.build().toXML().toString()); + + XmlPullParser parser = TestUtils.getParser(xml); + MarkupElement parsed = new MarkupElementProvider().parse(parser); + List children = parsed.getChildElements(); + assertEquals(1, children.size()); + + SpanElement spanElement = (SpanElement) children.get(0); + assertEquals(9, spanElement.getStart()); + assertEquals(15, spanElement.getEnd()); + assertEquals(1, spanElement.getStyles().size()); + assertEquals(SpanElement.SpanStyle.code, spanElement.getStyles().iterator().next()); + } + + @Test + public void deletedTest() throws Exception { + String xml = + "" + + "" + + "" + + "" + + ""; + MarkupElement.Builder m = MarkupElement.getBuilder(); + m.setDeleted(9, 15); + assertXMLEqual(xml, m.build().toXML().toString()); + + XmlPullParser parser = TestUtils.getParser(xml); + MarkupElement parsed = new MarkupElementProvider().parse(parser); + List children = parsed.getChildElements(); + assertEquals(1, children.size()); + + SpanElement spanElement = (SpanElement) children.get(0); + assertEquals(9, spanElement.getStart()); + assertEquals(15, spanElement.getEnd()); + assertEquals(1, spanElement.getStyles().size()); + assertEquals(SpanElement.SpanStyle.deleted, spanElement.getStyles().iterator().next()); + } + + @Test(expected = IllegalArgumentException.class) + public void wrongStartEndTest() { + MarkupElement.getBuilder().setEmphasis(12, 10); + } + + @Test(expected = IllegalArgumentException.class) + public void overlappingSpansTest() { + MarkupElement.Builder m = MarkupElement.getBuilder(); + m.setEmphasis(0, 10); + m.setDeleted(5, 15); + } + + @Test + public void codeBlockTest() throws Exception { + String xml = + "" + + "" + + ""; + MarkupElement.Builder m = MarkupElement.getBuilder(); + m.setCodeBlock(23, 48); + assertXMLEqual(xml, m.build().toXML().toString()); + + XmlPullParser parser = TestUtils.getParser(xml); + MarkupElement parsed = new MarkupElementProvider().parse(parser); + List children = parsed.getChildElements(); + assertEquals(1, children.size()); + + CodeBlockElement codeBlock = (CodeBlockElement) children.get(0); + assertEquals(23, codeBlock.getStart()); + assertEquals(48, codeBlock.getEnd()); + } + + @Test + public void listTest() throws Exception { + String xml = + "" + + "" + + "
  • " + + "
  • " + + "
  • " + + "
  • " + + "" + + ""; + MarkupElement.Builder m = MarkupElement.getBuilder(); + m = m.beginList() + .addEntry(31, 47) + .addEntry(47, 61) + .addEntry(61, 69) + .addEntry(69, 89) + .endList(); + assertXMLEqual(xml, m.build().toXML().toString()); + + XmlPullParser parser = TestUtils.getParser(xml); + MarkupElement parsed = new MarkupElementProvider().parse(parser); + List children = parsed.getChildElements(); + assertEquals(1, children.size()); + + ListElement list = (ListElement) children.get(0); + assertEquals(31, list.getStart()); + assertEquals(89, list.getEnd()); + assertEquals(4, list.getEntries().size()); + assertEquals(list.getStart(), list.getEntries().get(0).getStart()); + assertEquals(47, list.getEntries().get(1).getStart()); + } + + @Test(expected = IllegalArgumentException.class) + public void listWrongSecondEntryTest() { + MarkupElement.Builder m = MarkupElement.getBuilder(); + m.beginList().addEntry(0,1).addEntry(3,4); + } + + @Test + public void blockQuoteTest() throws Exception { + String xml = + "" + + "" + + ""; + MarkupElement.Builder m = MarkupElement.getBuilder(); + m.setBlockQuote(9 ,32); + assertXMLEqual(xml, m.build().toXML().toString()); + + XmlPullParser parser = TestUtils.getParser(xml); + MarkupElement parsed = new MarkupElementProvider().parse(parser); + List children = parsed.getChildElements(); + assertEquals(1, children.size()); + + BlockQuoteElement quote = (BlockQuoteElement) children.get(0); + assertEquals(9, quote.getStart()); + assertEquals(32, quote.getEnd()); + } + + @Test + public void nestedBlockQuoteTest() throws Exception { + String xml = + "" + + "" + + "" + + ""; + MarkupElement.Builder m = MarkupElement.getBuilder(); + m.setBlockQuote(0, 57); + m.setBlockQuote(11, 34); + assertXMLEqual(xml, m.build().toXML().toString()); + + XmlPullParser parser = TestUtils.getParser(xml); + MarkupElement parsed = new MarkupElementProvider().parse(parser); + List children = parsed.getChildElements(); + assertEquals(2, children.size()); + + BlockQuoteElement q1 = (BlockQuoteElement) children.get(0); + BlockQuoteElement q2 = (BlockQuoteElement) children.get(1); + + assertEquals(0, q1.getStart()); + assertEquals(57, q1.getEnd()); + + assertEquals(11, q2.getStart()); + assertEquals(34, q2.getEnd()); + } +}