/*
* Firetweet - Twitter client for Android
*
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.getlantern.firetweet.util;
import android.support.annotation.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import static android.text.TextUtils.isEmpty;
import static org.getlantern.firetweet.util.HtmlEscapeHelper.escape;
import static org.getlantern.firetweet.util.HtmlEscapeHelper.toHtml;
import static org.getlantern.firetweet.util.HtmlEscapeHelper.unescape;
public class HtmlBuilder {
private final String source;
private final int[] codePoints;
private final int codePointsLength;
private final boolean throwExceptions, sourceIsEscaped, shouldReEscape;
private final ArrayList<LinkSpec> links = new ArrayList<>();
public HtmlBuilder(final String source, final boolean strict, final boolean sourceIsEscaped,
final boolean shouldReEscape) {
if (source == null) throw new NullPointerException();
this.source = source;
final int length = source.length();
final int[] codepointsTemp = new int[length];
int codePointsLength = 0;
for (int offset = 0; offset < length; ) {
final int codepoint = source.codePointAt(offset);
codepointsTemp[codePointsLength++] = codepoint;
offset += Character.charCount(codepoint);
}
codePoints = new int[codePointsLength];
System.arraycopy(codepointsTemp, 0, codePoints, 0, codePointsLength);
throwExceptions = strict;
this.sourceIsEscaped = sourceIsEscaped;
this.shouldReEscape = shouldReEscape;
this.codePointsLength = codePointsLength;
}
public boolean addLink(final String link, final String display, final int start, final int end) {
return addLink(link, display, start, end, false);
}
public boolean addLink(final String link, final String display, final int start, final int end,
final boolean display_is_html) {
if (start < 0 || end < 0 || start > end || end > codePointsLength) {
final String message = String.format(Locale.US, "text:%s, length:%d, start:%d, end:%d", source,
codePointsLength, start, end);
if (throwExceptions) throw new StringIndexOutOfBoundsException(message);
return false;
}
if (hasLink(start, end)) {
final String message = String.format(Locale.US,
"link already added in this range! text:%s, link:%s, display:%s, start:%d, end:%d", source, link,
display, start, end);
if (throwExceptions) throw new IllegalArgumentException(message);
return false;
}
return links.add(new LinkSpec(link, display, start, end, display_is_html));
}
public String build() {
if (links.isEmpty()) return escapeSource(source);
Collections.sort(links);
final StringBuilder builder = new StringBuilder();
final int linksSize = links.size();
for (int i = 0; i < linksSize; i++) {
final LinkSpec spec = links.get(i);
if (spec == null) {
continue;
}
final int start = spec.start, end = spec.end;
if (i == 0) {
if (start >= 0 && start <= codePointsLength) {
appendSource(builder, 0, start);
}
} else if (i > 0) {
final int lastEnd = links.get(i - 1).end;
if (lastEnd >= 0 && lastEnd <= start && start <= codePointsLength) {
appendSource(builder, lastEnd, start);
}
}
builder.append("<a href=\"");
builder.append(spec.link);
builder.append("\">");
if (start >= 0 && start <= end && end <= codePointsLength) {
builder.append(!isEmpty(spec.display) ? spec.display_is_html ? spec.display : toHtml(spec.display)
: spec.link);
}
builder.append("</a>");
if (i == linksSize - 1 && end >= 0 && end <= codePointsLength) {
appendSource(builder, end, codePointsLength);
}
}
return builder.toString();
}
public boolean hasLink(final int start, final int end) {
for (final LinkSpec spec : links) {
if (start >= spec.start && start <= spec.end || end >= spec.start && end <= spec.end)
return true;
}
return false;
}
@Override
public String toString() {
return "HtmlBuilder{orig=" + source + ", codePoints=" + Arrays.toString(codePoints) + ", string_length="
+ codePointsLength + ", throw_exceptions=" + throwExceptions + ", source_is_escaped=" + sourceIsEscaped
+ ", should_re_escape=" + shouldReEscape + ", links=" + links + "}";
}
private void appendSource(final StringBuilder builder, final int start, final int end) {
if (sourceIsEscaped == shouldReEscape) {
for (int i = start; i < end; i++) {
builder.appendCodePoint(codePoints[i]);
}
} else if (shouldReEscape) {
builder.append(escape(subString(start, end)));
} else {
builder.append(unescape(subString(start, end)));
}
}
private String escapeSource(final String source) {
if (sourceIsEscaped == shouldReEscape) return source;
return shouldReEscape ? escape(source) : unescape(source);
}
private String subString(final int start, final int end) {
final StringBuilder sb = new StringBuilder();
for (int i = start; i < end; i++) {
sb.appendCodePoint(codePoints[i]);
}
return sb.toString();
}
static final class LinkSpec implements Comparable<LinkSpec> {
final String link, display;
final int start, end;
final boolean display_is_html;
LinkSpec(final String link, final String display, final int start, final int end, final boolean display_is_html) {
this.link = link;
this.display = display;
this.start = start;
this.end = end;
this.display_is_html = display_is_html;
}
@Override
public int compareTo(@NonNull final LinkSpec that) {
return start - that.start;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (!(obj instanceof LinkSpec)) return false;
final LinkSpec other = (LinkSpec) obj;
if (display == null) {
if (other.display != null) return false;
} else if (!display.equals(other.display)) return false;
if (display_is_html != other.display_is_html) return false;
if (end != other.end) return false;
if (link == null) {
if (other.link != null) return false;
} else if (!link.equals(other.link)) return false;
if (start != other.start) return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (display == null ? 0 : display.hashCode());
result = prime * result + (display_is_html ? 1231 : 1237);
result = prime * result + end;
result = prime * result + (link == null ? 0 : link.hashCode());
result = prime * result + start;
return result;
}
@Override
public String toString() {
return "LinkSpec{link=" + link + ", display=" + display + ", start=" + start + ", end=" + end
+ ", display_is_html=" + display_is_html + "}";
}
}
}