From 10ed72775e01aa87d9222b21ee4c4ef2d6096d23 Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 24 Dec 2017 11:38:50 +0100 Subject: [PATCH] move more handling to the parserhandler, fix 8ch quotes outside thread. --- .../core/site/common/ChanParserHelper.java | 128 ++++++++++++++ .../core/site/common/ChanReaderRequest.java | 9 +- .../DefaultFutabaChanParserHandler.java | 82 ++++++++- .../core/site/common/FutabaChanParser.java | 160 +----------------- .../site/common/FutabaChanParserHandler.java | 10 +- .../site/sites/chan8/Chan8ParserHandler.java | 13 +- 6 files changed, 238 insertions(+), 164 deletions(-) create mode 100644 Clover/app/src/main/java/org/floens/chan/core/site/common/ChanParserHelper.java diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanParserHelper.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanParserHelper.java new file mode 100644 index 00000000..1f73a9e8 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanParserHelper.java @@ -0,0 +1,128 @@ +/* + * Clover - 4chan browser https://github.com/Floens/Clover/ + * Copyright (C) 2014 Floens + * + * 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 . + */ +package org.floens.chan.core.site.common; + +import android.text.SpannableString; + +import org.floens.chan.core.model.Post; +import org.floens.chan.core.model.PostLinkable; +import org.floens.chan.ui.theme.Theme; +import org.jsoup.helper.StringUtil; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import org.jsoup.nodes.TextNode; +import org.jsoup.select.NodeTraversor; +import org.jsoup.select.NodeVisitor; +import org.nibor.autolink.LinkExtractor; +import org.nibor.autolink.LinkSpan; +import org.nibor.autolink.LinkType; + +import java.util.EnumSet; + +public class ChanParserHelper { + private static final LinkExtractor LINK_EXTRACTOR = LinkExtractor.builder() + .linkTypes(EnumSet.of(LinkType.URL)) + .build(); + + /** + * Detect links in the given spannable, and create PostLinkables with Type.LINK for the + * links found onto the spannable. + *

+ * The links are detected with the autolink-java library. + * + * @param theme The theme to style the links with + * @param post The post where the linkables get added to. + * @param text Text to find links in + * @param spannable Spannable to set the spans on. + */ + public static void detectLinks(Theme theme, Post.Builder post, String text, SpannableString spannable) { + final Iterable links = LINK_EXTRACTOR.extractLinks(text); + for (final LinkSpan link : links) { + final String linkText = text.substring(link.getBeginIndex(), link.getEndIndex()); + final PostLinkable pl = new PostLinkable(theme, linkText, linkText, PostLinkable.Type.LINK); + spannable.setSpan(pl, link.getBeginIndex(), link.getEndIndex(), 0); + post.addLinkable(pl); + } + } + + // Below code taken from org.jsoup.nodes.Element.text(), but it preserves
+ public static String getNodeTextPreservingLineBreaks(Element node) { + final StringBuilder accum = new StringBuilder(); + new NodeTraversor(new NodeVisitor() { + public void head(Node node, int depth) { + if (node instanceof TextNode) { + TextNode textNode = (TextNode) node; + appendNormalisedText(accum, textNode); + } else if (node instanceof Element) { + Element element = (Element) node; + if (accum.length() > 0 && + element.isBlock() && + !lastCharIsWhitespace(accum)) + accum.append(" "); + + if (element.tag().getName().equals("br")) { + accum.append("\n"); + } + } + } + + public void tail(Node node, int depth) { + } + }).traverse(node); + return accum.toString().trim(); + } + + // Copied from org.jsoup.nodes.Element.text() + private static boolean lastCharIsWhitespace(StringBuilder sb) { + return sb.length() != 0 && sb.charAt(sb.length() - 1) == ' '; + } + + // Copied from org.jsoup.nodes.Element.text() + private static void appendNormalisedText(StringBuilder accum, TextNode textNode) { + String text = textNode.getWholeText(); + + if (!preserveWhitespace(textNode.parent())) { + text = normaliseWhitespace(text); + if (lastCharIsWhitespace(accum)) + text = stripLeadingWhitespace(text); + } + accum.append(text); + } + + // Copied from org.jsoup.nodes.Element.text() + private static String normaliseWhitespace(String text) { + text = StringUtil.normaliseWhitespace(text); + return text; + } + + // Copied from org.jsoup.nodes.Element.text() + private static String stripLeadingWhitespace(String text) { + return text.replaceFirst("^\\s+", ""); + } + + // Copied from org.jsoup.nodes.Element.text() + private static boolean preserveWhitespace(Node node) { + // looks only at this element and one level up, to prevent recursion & needless stack searches + if (node != null && node instanceof Element) { + Element element = (Element) node; + return element.tag().preserveWhitespace() || + element.parent() != null && element.parent().tag().preserveWhitespace(); + } + return false; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReaderRequest.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReaderRequest.java index 1a22632a..e4394b0a 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReaderRequest.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReaderRequest.java @@ -283,9 +283,12 @@ public class ChanReaderRequest extends JsonReaderRequest { List value = entry.getValue(); Post subject = postsByNo.get(key); - synchronized (subject.repliesFrom) { - subject.repliesFrom.clear(); - subject.repliesFrom.addAll(value); + // Sometimes a post replies to a ghost, a post that doesn't exist. + if (subject != null) { + synchronized (subject.repliesFrom) { + subject.repliesFrom.clear(); + subject.repliesFrom.addAll(value); + } } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/DefaultFutabaChanParserHandler.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/DefaultFutabaChanParserHandler.java index b64aa1f4..7d99fb3a 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/DefaultFutabaChanParserHandler.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/common/DefaultFutabaChanParserHandler.java @@ -22,17 +22,25 @@ import android.text.SpannableString; import android.text.TextUtils; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; +import android.text.style.UnderlineSpan; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.PostLinkable; +import org.floens.chan.ui.span.AbsoluteSizeSpanHashed; import org.floens.chan.ui.span.ForegroundColorSpanHashed; import org.floens.chan.ui.theme.Theme; import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.floens.chan.utils.AndroidUtils.sp; + public class DefaultFutabaChanParserHandler implements FutabaChanParserHandler { private static final Pattern COLOR_PATTERN = Pattern.compile("color:#([0-9a-fA-F]*)"); @@ -83,14 +91,84 @@ public class DefaultFutabaChanParserHandler implements FutabaChanParserHandler { } else { quote = new SpannableString(span.text()); quote.setSpan(new ForegroundColorSpanHashed(theme.inlineQuoteColor), 0, quote.length(), 0); - parser.detectLinks(theme, post, span.text(), quote); + ChanParserHelper.detectLinks(theme, post, span.text(), quote); } return quote; } @Override - public Link getLink(FutabaChanParser parser, Theme theme, Post.Builder post, Element anchor) { + public CharSequence handleTable(FutabaChanParser parser, Theme theme, Post.Builder post, Element table) { + List parts = new ArrayList<>(); + Elements tableRows = table.getElementsByTag("tr"); + for (int i = 0; i < tableRows.size(); i++) { + Element tableRow = tableRows.get(i); + if (tableRow.text().length() > 0) { + Elements tableDatas = tableRow.getElementsByTag("td"); + for (int j = 0; j < tableDatas.size(); j++) { + Element tableData = tableDatas.get(j); + + SpannableString tableDataPart = new SpannableString(tableData.text()); + if (tableData.getElementsByTag("b").size() > 0) { + tableDataPart.setSpan(new StyleSpan(Typeface.BOLD), 0, tableDataPart.length(), 0); + tableDataPart.setSpan(new UnderlineSpan(), 0, tableDataPart.length(), 0); + } + + parts.add(tableDataPart); + + if (j < tableDatas.size() - 1) { + parts.add(": "); + } + } + + if (i < tableRows.size() - 1) { + parts.add("\n"); + } + } + } + + SpannableString tableTotal = new SpannableString(TextUtils.concat(parts.toArray(new CharSequence[parts.size()]))); + tableTotal.setSpan(new ForegroundColorSpanHashed(theme.inlineQuoteColor), 0, tableTotal.length(), 0); + tableTotal.setSpan(new AbsoluteSizeSpanHashed(sp(12f)), 0, tableTotal.length(), 0); + + return tableTotal; + } + + @Override + public CharSequence handleStrong(FutabaChanParser parser, Theme theme, Post.Builder post, Element strong) { + SpannableString red = new SpannableString(strong.text()); + red.setSpan(new ForegroundColorSpanHashed(theme.quoteColor), 0, red.length(), 0); + red.setSpan(new StyleSpan(Typeface.BOLD), 0, red.length(), 0); + return red; + } + + @Override + public CharSequence handlePre(FutabaChanParser parser, Theme theme, Post.Builder post, Element pre) { + Set classes = pre.classNames(); + if (classes.contains("prettyprint")) { + String text = ChanParserHelper.getNodeTextPreservingLineBreaks(pre); + SpannableString monospace = new SpannableString(text); + monospace.setSpan(new TypefaceSpan("monospace"), 0, monospace.length(), 0); + monospace.setSpan(new AbsoluteSizeSpanHashed(sp(12f)), 0, monospace.length(), 0); + return monospace; + } else { + return pre.text(); + } + } + + @Override + public CharSequence handleStrike(FutabaChanParser parser, Theme theme, Post.Builder post, Element strike) { + SpannableString link = new SpannableString(strike.text()); + + PostLinkable pl = new PostLinkable(theme, strike.text(), strike.text(), PostLinkable.Type.SPOILER); + link.setSpan(pl, 0, link.length(), 0); + post.addLinkable(pl); + + return link; + } + + @Override + public Link handleAnchor(FutabaChanParser parser, Theme theme, Post.Builder post, Element anchor) { String href = anchor.attr("href"); Set classes = anchor.classNames(); diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParser.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParser.java index b9f71528..28485f1a 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParser.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParser.java @@ -18,13 +18,9 @@ package org.floens.chan.core.site.common; -import android.graphics.Typeface; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.BackgroundColorSpan; -import android.text.style.StyleSpan; -import android.text.style.TypefaceSpan; -import android.text.style.UnderlineSpan; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.PostLinkable; @@ -35,23 +31,14 @@ import org.floens.chan.ui.theme.Theme; import org.floens.chan.ui.theme.ThemeHelper; import org.floens.chan.utils.Logger; import org.jsoup.Jsoup; -import org.jsoup.helper.StringUtil; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; import org.jsoup.parser.Parser; -import org.jsoup.select.Elements; -import org.jsoup.select.NodeTraversor; -import org.jsoup.select.NodeVisitor; -import org.nibor.autolink.LinkExtractor; -import org.nibor.autolink.LinkSpan; -import org.nibor.autolink.LinkType; import java.util.ArrayList; -import java.util.EnumSet; import java.util.List; -import java.util.Set; import static org.floens.chan.utils.AndroidUtils.sp; @@ -60,8 +47,6 @@ public class FutabaChanParser implements ChanParser { private static final String SAVED_REPLY_SUFFIX = " (You)"; private static final String OP_REPLY_SUFFIX = " (OP)"; - private final LinkExtractor linkExtractor = LinkExtractor.builder().linkTypes(EnumSet.of(LinkType.URL)).build(); - private FutabaChanParserHandler handler; public FutabaChanParser(FutabaChanParserHandler handler) { @@ -156,6 +141,7 @@ public class FutabaChanParser implements ChanParser { int g = (hash >> 16) & 0xff; int b = (hash >> 8) & 0xff; + //noinspection NumericOverflow int idColor = (0xff << 24) + (r << 16) + (g << 8) + b; boolean lightColor = (r * 0.299f) + (g * 0.587f) + (b * 0.114f) > 125f; int idBgColor = lightColor ? theme.idBackgroundLight : theme.idBackgroundDark; @@ -222,12 +208,13 @@ public class FutabaChanParser implements ChanParser { String text = ((TextNode) node).text(); SpannableString spannable = new SpannableString(text); - detectLinks(theme, post, text, spannable); + ChanParserHelper.detectLinks(theme, post, text, spannable); return spannable; } else { switch (node.nodeName()) { case "p": { + // Recursively call parseNode with the nodes of the paragraph. List innerNodes = node.childNodes(); List texts = new ArrayList<>(innerNodes.size() + 1); @@ -253,50 +240,10 @@ public class FutabaChanParser implements ChanParser { return handler.handleSpan(this, theme, post, (Element) node); } case "table": { - Element table = (Element) node; - - List parts = new ArrayList<>(); - Elements tableRows = table.getElementsByTag("tr"); - for (int i = 0; i < tableRows.size(); i++) { - Element tableRow = tableRows.get(i); - if (tableRow.text().length() > 0) { - Elements tableDatas = tableRow.getElementsByTag("td"); - for (int j = 0; j < tableDatas.size(); j++) { - Element tableData = tableDatas.get(j); - - SpannableString tableDataPart = new SpannableString(tableData.text()); - if (tableData.getElementsByTag("b").size() > 0) { - tableDataPart.setSpan(new StyleSpan(Typeface.BOLD), 0, tableDataPart.length(), 0); - tableDataPart.setSpan(new UnderlineSpan(), 0, tableDataPart.length(), 0); - } - - parts.add(tableDataPart); - - if (j < tableDatas.size() - 1) { - parts.add(": "); - } - } - - if (i < tableRows.size() - 1) { - parts.add("\n"); - } - } - } - - SpannableString tableTotal = new SpannableString(TextUtils.concat(parts.toArray(new CharSequence[parts.size()]))); - tableTotal.setSpan(new ForegroundColorSpanHashed(theme.inlineQuoteColor), 0, tableTotal.length(), 0); - tableTotal.setSpan(new AbsoluteSizeSpanHashed(sp(12f)), 0, tableTotal.length(), 0); - - return tableTotal; + return handler.handleTable(this, theme, post, (Element) node); } case "strong": { - Element strong = (Element) node; - - SpannableString red = new SpannableString(strong.text()); - red.setSpan(new ForegroundColorSpanHashed(theme.quoteColor), 0, red.length(), 0); - red.setSpan(new StyleSpan(Typeface.BOLD), 0, red.length(), 0); - - return red; + return handler.handleStrong(this, theme, post, (Element) node); } case "a": { CharSequence anchor = parseAnchor(theme, post, (Element) node); @@ -307,29 +254,10 @@ public class FutabaChanParser implements ChanParser { } } case "s": { - Element spoiler = (Element) node; - - SpannableString link = new SpannableString(spoiler.text()); - - PostLinkable pl = new PostLinkable(theme, spoiler.text(), spoiler.text(), PostLinkable.Type.SPOILER); - link.setSpan(pl, 0, link.length(), 0); - post.addLinkable(pl); - - return link; + return handler.handleStrike(this, theme, post, (Element) node); } case "pre": { - Element pre = (Element) node; - - Set classes = pre.classNames(); - if (classes.contains("prettyprint")) { - String text = getNodeText(pre); - SpannableString monospace = new SpannableString(text); - monospace.setSpan(new TypefaceSpan("monospace"), 0, monospace.length(), 0); - monospace.setSpan(new AbsoluteSizeSpanHashed(sp(12f)), 0, monospace.length(), 0); - return monospace; - } else { - return pre.text(); - } + return handler.handlePre(this, theme, post, (Element) node); } default: { // Unknown tag, add the inner part @@ -344,7 +272,7 @@ public class FutabaChanParser implements ChanParser { } private CharSequence parseAnchor(Theme theme, Post.Builder post, Element anchor) { - FutabaChanParserHandler.Link handlerLink = handler.getLink(this, theme, post, anchor); + FutabaChanParserHandler.Link handlerLink = handler.handleAnchor(this, theme, post, anchor); if (handlerLink != null) { SpannableString link = new SpannableString(handlerLink.key); @@ -377,76 +305,4 @@ public class FutabaChanParser implements ChanParser { return null; } } - - public void detectLinks(Theme theme, Post.Builder post, String text, SpannableString spannable) { - // use autolink-java lib to detect links - final Iterable links = linkExtractor.extractLinks(text); - for (final LinkSpan link : links) { - final String linkText = text.substring(link.getBeginIndex(), link.getEndIndex()); - final PostLinkable pl = new PostLinkable(theme, linkText, linkText, PostLinkable.Type.LINK); - spannable.setSpan(pl, link.getBeginIndex(), link.getEndIndex(), 0); - post.addLinkable(pl); - } - } - - // Below code taken from org.jsoup.nodes.Element.text(), but it preserves
- private String getNodeText(Element node) { - final StringBuilder accum = new StringBuilder(); - new NodeTraversor(new NodeVisitor() { - public void head(Node node, int depth) { - if (node instanceof TextNode) { - TextNode textNode = (TextNode) node; - appendNormalisedText(accum, textNode); - } else if (node instanceof Element) { - Element element = (Element) node; - if (accum.length() > 0 && - element.isBlock() && - !lastCharIsWhitespace(accum)) - accum.append(" "); - - if (element.tag().getName().equals("br")) { - accum.append("\n"); - } - } - } - - public void tail(Node node, int depth) { - } - }).traverse(node); - return accum.toString().trim(); - } - - private static boolean lastCharIsWhitespace(StringBuilder sb) { - return sb.length() != 0 && sb.charAt(sb.length() - 1) == ' '; - } - - private static void appendNormalisedText(StringBuilder accum, TextNode textNode) { - String text = textNode.getWholeText(); - - if (!preserveWhitespace(textNode.parent())) { - text = normaliseWhitespace(text); - if (lastCharIsWhitespace(accum)) - text = stripLeadingWhitespace(text); - } - accum.append(text); - } - - private static String normaliseWhitespace(String text) { - text = StringUtil.normaliseWhitespace(text); - return text; - } - - private static String stripLeadingWhitespace(String text) { - return text.replaceFirst("^\\s+", ""); - } - - private static boolean preserveWhitespace(Node node) { - // looks only at this element and one level up, to prevent recursion & needless stack searches - if (node != null && node instanceof Element) { - Element element = (Element) node; - return element.tag().preserveWhitespace() || - element.parent() != null && element.parent().tag().preserveWhitespace(); - } - return false; - } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParserHandler.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParserHandler.java index 8743dd17..c84890d5 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParserHandler.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParserHandler.java @@ -26,7 +26,15 @@ public interface FutabaChanParserHandler { CharSequence handleSpan(FutabaChanParser parser, Theme theme, Post.Builder post, Element span); - Link getLink(FutabaChanParser parser, Theme theme, Post.Builder post, Element anchor); + CharSequence handleTable(FutabaChanParser parser, Theme theme, Post.Builder post, Element table); + + CharSequence handleStrong(FutabaChanParser parser, Theme theme, Post.Builder post, Element strong); + + CharSequence handlePre(FutabaChanParser parser, Theme theme, Post.Builder post, Element pre); + + CharSequence handleStrike(FutabaChanParser parser, Theme theme, Post.Builder post, Element strike); + + Link handleAnchor(FutabaChanParser parser, Theme theme, Post.Builder post, Element anchor); class Link { public PostLinkable.Type type; diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8ParserHandler.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8ParserHandler.java index 1cd47709..8c2a0e41 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8ParserHandler.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan8/Chan8ParserHandler.java @@ -21,6 +21,7 @@ import android.text.SpannableString; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.PostLinkable; +import org.floens.chan.core.site.common.ChanParserHelper; import org.floens.chan.core.site.common.DefaultFutabaChanParserHandler; import org.floens.chan.core.site.common.FutabaChanParser; import org.floens.chan.ui.span.ForegroundColorSpanHashed; @@ -35,7 +36,7 @@ public class Chan8ParserHandler extends DefaultFutabaChanParserHandler { if (element.hasClass("quote")) { SpannableString quote = new SpannableString(text); quote.setSpan(new ForegroundColorSpanHashed(theme.inlineQuoteColor), 0, quote.length(), 0); - parser.detectLinks(theme, post, quote.toString(), quote); + ChanParserHelper.detectLinks(theme, post, quote.toString(), quote); return quote; } else { return text; @@ -57,21 +58,21 @@ public class Chan8ParserHandler extends DefaultFutabaChanParserHandler { } else { quote = new SpannableString(span.text()); quote.setSpan(new ForegroundColorSpanHashed(theme.inlineQuoteColor), 0, quote.length(), 0); - parser.detectLinks(theme, post, span.text(), quote); + ChanParserHelper.detectLinks(theme, post, span.text(), quote); } return quote; } @Override - public Link getLink(FutabaChanParser parser, Theme theme, Post.Builder post, Element anchor) { + public Link handleAnchor(FutabaChanParser parser, Theme theme, Post.Builder post, Element anchor) { String href = anchor.attr("href"); PostLinkable.Type t = null; String key = null; Object value = null; if (href.startsWith("/")) { - if (href.contains("/thread/")) { + if (!href.startsWith("/" + post.board.code + "/res/")) { // link to another thread PostLinkable.ThreadLink threadLink = null; @@ -79,10 +80,10 @@ public class Chan8ParserHandler extends DefaultFutabaChanParserHandler { if (slashSplit.length == 4) { String board = slashSplit[1]; String nums = slashSplit[3]; - String[] numsSplitted = nums.split("#p"); + String[] numsSplitted = nums.split("#"); if (numsSplitted.length == 2) { try { - int tId = Integer.parseInt(numsSplitted[0]); + int tId = Integer.parseInt(numsSplitted[0].replace(".html", "")); int pId = Integer.parseInt(numsSplitted[1]); threadLink = new PostLinkable.ThreadLink(board, tId, pId); } catch (NumberFormatException ignored) {