[YouTube] Replace link text with accessibility label
This commit is contained in:
parent
09732d6785
commit
b80c3f5d51
|
@ -12,6 +12,9 @@ import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
@ -29,6 +32,11 @@ public final class YoutubeDescriptionHelper {
|
||||||
public static final String ITALIC_OPEN = "<i>";
|
public static final String ITALIC_OPEN = "<i>";
|
||||||
public static final String ITALIC_CLOSE = "</i>";
|
public static final String ITALIC_CLOSE = "</i>";
|
||||||
|
|
||||||
|
// special link chips (e.g. for YT videos, YT channels or social media accounts):
|
||||||
|
// (u00a0) u00a0 u00a0 [/•] u00a0 <link content> u00a0 u00a0
|
||||||
|
private static final Pattern LINK_CONTENT_CLEANER_REGEX
|
||||||
|
= Pattern.compile("(?s)^\u00a0+[/•]\u00a0+(.*?)\u00a0+$");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can be a command run, or a style run.
|
* Can be a command run, or a style run.
|
||||||
*/
|
*/
|
||||||
|
@ -37,17 +45,30 @@ public final class YoutubeDescriptionHelper {
|
||||||
@Nonnull final String close;
|
@Nonnull final String close;
|
||||||
final int pos;
|
final int pos;
|
||||||
final boolean isClose;
|
final boolean isClose;
|
||||||
|
@Nullable final Function<String, String> transformContent;
|
||||||
|
int openPosInOutput = -1;
|
||||||
|
|
||||||
Run(
|
Run(
|
||||||
@Nonnull final String open,
|
@Nonnull final String open,
|
||||||
@Nonnull final String close,
|
@Nonnull final String close,
|
||||||
final int pos,
|
final int pos,
|
||||||
final boolean isClose
|
final boolean isClose
|
||||||
|
) {
|
||||||
|
this(open, close, pos, isClose, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Run(
|
||||||
|
@Nonnull final String open,
|
||||||
|
@Nonnull final String close,
|
||||||
|
final int pos,
|
||||||
|
final boolean isClose,
|
||||||
|
@Nullable final Function<String, String> transformContent
|
||||||
) {
|
) {
|
||||||
this.open = open;
|
this.open = open;
|
||||||
this.close = close;
|
this.close = close;
|
||||||
this.pos = pos;
|
this.pos = pos;
|
||||||
this.isClose = isClose;
|
this.isClose = isClose;
|
||||||
|
this.transformContent = transformContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean sameOpen(@Nonnull final Run other) {
|
public boolean sameOpen(@Nonnull final Run other) {
|
||||||
|
@ -148,12 +169,22 @@ public final class YoutubeDescriptionHelper {
|
||||||
// condition, because no run will close before being opened, but let's be sure
|
// condition, because no run will close before being opened, but let's be sure
|
||||||
while (!openRuns.empty()) {
|
while (!openRuns.empty()) {
|
||||||
final Run popped = openRuns.pop();
|
final Run popped = openRuns.pop();
|
||||||
textBuilder.append(popped.close);
|
|
||||||
if (popped.sameOpen(closer)) {
|
if (popped.sameOpen(closer)) {
|
||||||
|
// before closing the current run, if the run has a transformContent
|
||||||
|
// function, use it to transform the content of the current run, based on
|
||||||
|
// the openPosInOutput set when the current run was opened
|
||||||
|
if (popped.transformContent != null && popped.openPosInOutput >= 0) {
|
||||||
|
textBuilder.replace(popped.openPosInOutput, textBuilder.length(),
|
||||||
|
popped.transformContent.apply(
|
||||||
|
textBuilder.substring(popped.openPosInOutput)));
|
||||||
|
}
|
||||||
|
// close the run that we really need to close
|
||||||
|
textBuilder.append(popped.close);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// we keep popping from openRuns, closing all of the runs we find,
|
// we keep popping from openRuns, closing all of the runs we find,
|
||||||
// until we find the run that we really need to close ...
|
// until we find the run that we really need to close ...
|
||||||
|
textBuilder.append(popped.close);
|
||||||
tempStack.push(popped);
|
tempStack.push(popped);
|
||||||
}
|
}
|
||||||
while (!tempStack.empty()) {
|
while (!tempStack.empty()) {
|
||||||
|
@ -168,8 +199,10 @@ public final class YoutubeDescriptionHelper {
|
||||||
} else {
|
} else {
|
||||||
// this will never be reached if openersIndex >= openers.size() because of the
|
// this will never be reached if openersIndex >= openers.size() because of the
|
||||||
// way minPos is calculated
|
// way minPos is calculated
|
||||||
textBuilder.append(openers.get(openersIndex).open);
|
final Run opener = openers.get(openersIndex);
|
||||||
openRuns.push(openers.get(openersIndex));
|
textBuilder.append(opener.open);
|
||||||
|
opener.openPosInOutput = textBuilder.length(); // save for transforming later
|
||||||
|
openRuns.push(opener);
|
||||||
++openersIndex;
|
++openersIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,11 +213,7 @@ public final class YoutubeDescriptionHelper {
|
||||||
return textBuilder.toString()
|
return textBuilder.toString()
|
||||||
.replace("\n", "<br>")
|
.replace("\n", "<br>")
|
||||||
.replace(" ", " ")
|
.replace(" ", " ")
|
||||||
// special link chips (e.g. for YT videos, YT channels or social media accounts):
|
.replace('\u00a0', ' ');
|
||||||
// u00a0 u00a0 [/•] u00a0 <link content> u00a0 u00a0
|
|
||||||
.replace("\">\u00a0\u00a0/\u00a0", "\">")
|
|
||||||
.replace("\">\u00a0\u00a0•\u00a0", "\">")
|
|
||||||
.replace("\u00a0\u00a0</a>", "</a>");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addAllCommandRuns(
|
private static void addAllCommandRuns(
|
||||||
|
@ -212,12 +241,44 @@ public final class YoutubeDescriptionHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
final String open = "<a href=\"" + Entities.escape(url) + "\">";
|
final String open = "<a href=\"" + Entities.escape(url) + "\">";
|
||||||
|
final Function<String, String> transformContent = getTransformContentFun(run);
|
||||||
|
|
||||||
openers.add(new Run(open, LINK_CLOSE, startIndex, false));
|
openers.add(new Run(open, LINK_CLOSE, startIndex, false,
|
||||||
closers.add(new Run(open, LINK_CLOSE, startIndex + length, true));
|
transformContent));
|
||||||
|
closers.add(new Run(open, LINK_CLOSE, startIndex + length, true,
|
||||||
|
transformContent));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Function<String, String> getTransformContentFun(final JsonObject run) {
|
||||||
|
final String accessibilityLabel = run.getObject("onTapOptions")
|
||||||
|
.getObject("accessibilityInfo")
|
||||||
|
.getString("accessibilityLabel", "")
|
||||||
|
// accessibility labels are e.g. "Instagram Channel Link: instagram_profile_name"
|
||||||
|
.replaceFirst(" Channel Link", "");
|
||||||
|
|
||||||
|
final Function<String, String> transformContent;
|
||||||
|
if (accessibilityLabel.isEmpty() || accessibilityLabel.startsWith("YouTube: ")) {
|
||||||
|
// if there is no accessibility label, or the link points to YouTube, cleanup the link
|
||||||
|
// text, see LINK_CONTENT_CLEANER_REGEX's documentation for more details
|
||||||
|
transformContent = (content) -> {
|
||||||
|
final Matcher m = LINK_CONTENT_CLEANER_REGEX.matcher(content);
|
||||||
|
if (m.find()) {
|
||||||
|
return m.group(1);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// if there is an accessibility label, replace the link text with it, because on the
|
||||||
|
// YouTube website an ambiguous link text is next to an icon explaining which service it
|
||||||
|
// belongs to, but since we can't add icons, we instead use the accessibility label
|
||||||
|
// which contains information about the service
|
||||||
|
transformContent = (content) -> accessibilityLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformContent;
|
||||||
|
}
|
||||||
|
|
||||||
private static void addAllStyleRuns(
|
private static void addAllStyleRuns(
|
||||||
@Nonnull final JsonObject attributedDescription,
|
@Nonnull final JsonObject attributedDescription,
|
||||||
@Nonnull final List<Run> openers,
|
@Nonnull final List<Run> openers,
|
||||||
|
|
Loading…
Reference in New Issue