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}