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}