001package ezvcard.io.html;
002
003import static ezvcard.util.StringUtils.NEWLINE;
004
005import java.util.ArrayList;
006import java.util.List;
007import java.util.Set;
008
009import org.jsoup.nodes.Element;
010import org.jsoup.nodes.Node;
011import org.jsoup.nodes.TextNode;
012import org.jsoup.select.Elements;
013
014import ezvcard.util.HtmlUtils;
015
016/*
017 Copyright (c) 2012-2023, Michael Angstadt
018 All rights reserved.
019
020 Redistribution and use in source and binary forms, with or without
021 modification, are permitted provided that the following conditions are met: 
022
023 1. Redistributions of source code must retain the above copyright notice, this
024 list of conditions and the following disclaimer. 
025 2. Redistributions in binary form must reproduce the above copyright notice,
026 this list of conditions and the following disclaimer in the documentation
027 and/or other materials provided with the distribution. 
028
029 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
030 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
031 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
032 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
033 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
034 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
035 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
036 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
037 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
038 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
039
040 The views and conclusions contained in the software and documentation are those
041 of the authors and should not be interpreted as representing official policies, 
042 either expressed or implied, of the FreeBSD Project.
043 */
044
045/**
046 * Wraps hCard functionality around an HTML {@link Element} object.
047 * @author Michael Angstadt
048 */
049public class HCardElement {
050        private final Element element;
051
052        /**
053         * Creates an hCard element.
054         * @param element the HTML element to wrap
055         */
056        public HCardElement(Element element) {
057                this.element = element;
058        }
059
060        /**
061         * Gets the name of the HTML element.
062         * @return the tag name
063         */
064        public String tagName() {
065                return element.tagName();
066        }
067
068        /**
069         * Gets an attribute value.
070         * @param name the attribute name
071         * @return the attribute value or empty string if it doesn't exist
072         */
073        public String attr(String name) {
074                return element.attr(name);
075        }
076
077        /**
078         * Gets the absolute URL of an attribute that has a URL.
079         * @param name the attribute name
080         * @return the absolute URL or empty string if it doesn't exist
081         */
082        public String absUrl(String name) {
083                String url = element.absUrl(name); //returns empty string for some protocols like "tel:" and "data:", but not for "http:" or "mailto:"
084                if (url.isEmpty()) {
085                        url = element.attr(name);
086                }
087                return url;
088        }
089
090        /**
091         * Gets the element's CSS classes.
092         * @return the CSS classes
093         */
094        public Set<String> classNames() {
095                return element.classNames();
096        }
097
098        /**
099         * Gets the hCard value of this element. The value is determined based on
100         * the following:
101         * <ol>
102         * <li>If the element is {@code <abbr>} and contains a {@code title}
103         * attribute, then the value of the {@code title} attribute is returned.</li>
104         * <li>Else, if the element contains one or more child elements that have a
105         * CSS class of {@code value}, then append together the text contents of
106         * these elements.</li>
107         * <li>Else, use the text content of the element itself.</li>
108         * </ol>
109         * All {@code <br>} tags are converted to newlines. All text within
110         * {@code <del>} tags are ignored.
111         * @return the element's hCard value
112         */
113        public String value() {
114                return value(element);
115        }
116
117        /**
118         * Gets the hCard value of the first descendant element that has the given
119         * CSS class name.
120         * @param cssClass the CSS class name
121         * @return the hCard value or null if not found
122         */
123        public String firstValue(String cssClass) {
124                Elements elements = element.getElementsByClass(cssClass);
125                return elements.isEmpty() ? null : value(elements.first());
126        }
127
128        /**
129         * Gets the hCard values of all descendant elements that have the given CSS
130         * class name.
131         * @param cssClass the CSS class name
132         * @return the hCard values
133         */
134        public List<String> allValues(String cssClass) {
135                Elements elements = element.getElementsByClass(cssClass);
136                List<String> values = new ArrayList<>(elements.size());
137                for (Element element : elements) {
138                        values.add(value(element));
139                }
140                return values;
141        }
142
143        /**
144         * Gets all type values (for example, "home" and "cell" for the "tel" type).
145         * @return the type values (in lower-case)
146         */
147        public List<String> types() {
148                List<String> types = allValues("type");
149                List<String> lowerCaseTypes = new ArrayList<>(types.size());
150                for (String type : types) {
151                        lowerCaseTypes.add(type.toLowerCase());
152                }
153                return lowerCaseTypes;
154        }
155
156        /**
157         * Appends text to the element, replacing newlines with {@code <br>} tags.
158         * @param text the text to append
159         */
160        public void append(String text) {
161                boolean first = true;
162                String lines[] = text.split("\\r\\n|\\n|\\r");
163                for (String line : lines) {
164                        if (!first) {
165                                //replace newlines with "<br>" tags
166                                element.appendElement("br");
167                        }
168
169                        if (line.length() > 0) {
170                                element.appendText(line);
171                        }
172
173                        first = false;
174                }
175        }
176
177        /**
178         * Gets the wrapped HTML element.
179         * @return the wrapped HTML element
180         */
181        public Element getElement() {
182                return element;
183        }
184
185        private String value(Element element) {
186                //value of "title" attribute should be returned if it's a "<abbr>" tag
187                //example: <abbr class="latitude" title="48.816667">N 48� 81.6667</abbr>
188                if ("abbr".equals(element.tagName())) {
189                        String title = element.attr("title");
190                        if (title.length() > 0) {
191                                return title;
192                        }
193                }
194
195                StringBuilder value = new StringBuilder();
196                Elements valueElements = element.getElementsByClass("value");
197                if (valueElements.isEmpty()) {
198                        //get the text content of all child nodes except "type" elements
199                        visitForValue(element, value);
200                } else {
201                        //append together all children whose CSS class is "value"
202                        for (Element valueElement : valueElements) {
203                                if (HtmlUtils.isChildOf(valueElement, valueElements)) {
204                                        //ignore "value" elements that are descendants of other "value" elements
205                                        continue;
206                                }
207
208                                if ("abbr".equals(valueElement.tagName())) {
209                                        String title = valueElement.attr("title");
210                                        if (title.length() > 0) {
211                                                value.append(title);
212                                                continue;
213                                        }
214                                }
215                                visitForValue(valueElement, value);
216                        }
217                }
218                return value.toString().trim();
219        }
220
221        private void visitForValue(Element element, StringBuilder value) {
222                for (Node node : element.childNodes()) {
223                        if (node instanceof Element) {
224                                Element e = (Element) node;
225                                if (e.classNames().contains("type")) {
226                                        //ignore "type" elements
227                                        continue;
228                                }
229
230                                if ("br".equals(e.tagName())) {
231                                        //convert "<br>" to a newline
232                                        value.append(NEWLINE);
233                                        continue;
234                                }
235
236                                if ("del".equals(e.tagName())) {
237                                        //skip "<del>" tags
238                                        continue;
239                                }
240
241                                visitForValue(e, value);
242                                continue;
243                        }
244
245                        if (node instanceof TextNode) {
246                                TextNode t = (TextNode) node;
247                                value.append(t.text());
248                                continue;
249                        }
250                }
251        }
252}