001package ezvcard.io.html;
002
003import static ezvcard.util.StringUtils.NEWLINE;
004
005import java.util.List;
006import java.util.Set;
007import java.util.stream.Collectors;
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-2026, 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                /*
084                 * Returns empty string for some protocols like "tel:" and "data:", but
085                 * not for "http:" or "mailto:"
086                 */
087                String url = element.absUrl(name);
088
089                return url.isEmpty() ? element.attr(name) : url;
090        }
091
092        /**
093         * Gets the element's CSS classes.
094         * @return the CSS classes
095         */
096        public Set<String> classNames() {
097                return element.classNames();
098        }
099
100        /**
101         * Gets the hCard value of this element. The value is determined based on
102         * the following:
103         * <ol>
104         * <li>If the element is {@code <abbr>} and contains a {@code title}
105         * attribute, then the value of the {@code title} attribute is returned.</li>
106         * <li>Else, if the element contains one or more child elements that have a
107         * CSS class of {@code value}, then append together the text contents of
108         * these elements.</li>
109         * <li>Else, use the text content of the element itself.</li>
110         * </ol>
111         * All {@code <br>} tags are converted to newlines. All text within
112         * {@code <del>} tags are ignored.
113         * @return the element's hCard value
114         */
115        public String value() {
116                return value(element);
117        }
118
119        /**
120         * Gets the hCard value of the first descendant element that has the given
121         * CSS class name.
122         * @param cssClass the CSS class name
123         * @return the hCard value or null if not found
124         */
125        public String firstValue(String cssClass) {
126                Elements elements = element.getElementsByClass(cssClass);
127                return elements.isEmpty() ? null : value(elements.first());
128        }
129
130        /**
131         * Gets the hCard values of all descendant elements that have the given CSS
132         * class name.
133         * @param cssClass the CSS class name
134         * @return the hCard values
135         */
136        public List<String> allValues(String cssClass) {
137                //@formatter:off
138                return element.getElementsByClass(cssClass).stream()
139                        .map(this::value)
140                .collect(Collectors.toList());
141                //@formatter:on
142        }
143
144        /**
145         * Gets all type values (for example, "home" and "cell" for the "tel" type).
146         * @return the type values (in lower-case)
147         */
148        public List<String> types() {
149                //@formatter:off
150                return allValues("type").stream()
151                        .map(String::toLowerCase)
152                .collect(Collectors.toList());
153                //@formatter:on
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.isEmpty()) {
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                String abbrValue = abbrElementValue(element);
187                if (abbrValue != null) {
188                        return abbrValue;
189                }
190
191                Elements valueTitleElements = element.getElementsByClass("value-title");
192                if (!valueTitleElements.isEmpty()) {
193                        String title = valueTitleElements.first().attr("title");
194                        if (!title.isEmpty()) {
195                                return title;
196                        }
197                }
198
199                StringBuilder value = new StringBuilder();
200                Elements valueElements = element.getElementsByClass("value");
201                if (valueElements.isEmpty()) {
202                        //get the text content of all child nodes except "type" elements
203                        visitForValue(element, value);
204                } else {
205                        //append together all children whose CSS class is "value"
206                        //@formatter:off
207                        valueElements.stream()
208                                .filter(valueElement -> !HtmlUtils.isChildOf(valueElement, valueElements)) //ignore "value" elements that are descendants of other "value" elements
209                        .forEach(valueElement -> {
210                                String childAbbrValue = abbrElementValue(valueElement);
211                                if (childAbbrValue == null) {
212                                        visitForValue(valueElement, value);
213                                } else {
214                                        value.append(childAbbrValue);
215                                }
216                        });
217                        //@formatter:on
218                }
219                return value.toString().trim();
220        }
221
222        /**
223         * <p>
224         * If the given element is {@code <abbr>}, gets the value of its "title"
225         * attribute.
226         * </p>
227         * <p>
228         * Example:
229         * {@code <abbr class="latitude" title="48.816667">N 48° 81.6667</abbr>}
230         * </p>
231         * @param element
232         * @return the value or null if not found
233         */
234        private String abbrElementValue(Element element) {
235                if (!"abbr".equals(element.tagName())) {
236                        return null;
237                }
238
239                String title = element.attr("title");
240                return title.isEmpty() ? null : title;
241        }
242
243        private void visitForValue(Element element, StringBuilder value) {
244                for (Node node : element.childNodes()) {
245                        if (node instanceof Element) {
246                                Element e = (Element) node;
247                                if (e.classNames().contains("type")) {
248                                        //ignore "type" elements
249                                        continue;
250                                }
251
252                                if ("br".equals(e.tagName())) {
253                                        //convert "<br>" to a newline
254                                        value.append(NEWLINE);
255                                        continue;
256                                }
257
258                                if ("del".equals(e.tagName())) {
259                                        //skip "<del>" tags
260                                        continue;
261                                }
262
263                                visitForValue(e, value);
264                                continue;
265                        }
266
267                        if (node instanceof TextNode) {
268                                TextNode t = (TextNode) node;
269                                value.append(t.text());
270                                continue;
271                        }
272                }
273        }
274}