Skip to content

Commit b80c3f5

Browse files
committed
[YouTube] Replace link text with accessibility label
1 parent 09732d6 commit b80c3f5

1 file changed

Lines changed: 71 additions & 10 deletions

File tree

extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDescriptionHelper.java

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
import java.util.Comparator;
1313
import java.util.List;
1414
import java.util.Stack;
15+
import java.util.function.Function;
16+
import java.util.regex.Matcher;
17+
import java.util.regex.Pattern;
1518

1619
import javax.annotation.Nonnull;
1720
import javax.annotation.Nullable;
@@ -29,6 +32,11 @@ private YoutubeDescriptionHelper() {
2932
public static final String ITALIC_OPEN = "<i>";
3033
public static final String ITALIC_CLOSE = "</i>";
3134

35+
// special link chips (e.g. for YT videos, YT channels or social media accounts):
36+
// (u00a0) u00a0 u00a0 [/•] u00a0 <link content> u00a0 u00a0
37+
private static final Pattern LINK_CONTENT_CLEANER_REGEX
38+
= Pattern.compile("(?s)^\u00a0+[/•]\u00a0+(.*?)\u00a0+$");
39+
3240
/**
3341
* Can be a command run, or a style run.
3442
*/
@@ -37,17 +45,30 @@ static final class Run {
3745
@Nonnull final String close;
3846
final int pos;
3947
final boolean isClose;
48+
@Nullable final Function<String, String> transformContent;
49+
int openPosInOutput = -1;
4050

4151
Run(
4252
@Nonnull final String open,
4353
@Nonnull final String close,
4454
final int pos,
4555
final boolean isClose
56+
) {
57+
this(open, close, pos, isClose, null);
58+
}
59+
60+
Run(
61+
@Nonnull final String open,
62+
@Nonnull final String close,
63+
final int pos,
64+
final boolean isClose,
65+
@Nullable final Function<String, String> transformContent
4666
) {
4767
this.open = open;
4868
this.close = close;
4969
this.pos = pos;
5070
this.isClose = isClose;
71+
this.transformContent = transformContent;
5172
}
5273

5374
public boolean sameOpen(@Nonnull final Run other) {
@@ -148,12 +169,22 @@ static String runsToHtml(
148169
// condition, because no run will close before being opened, but let's be sure
149170
while (!openRuns.empty()) {
150171
final Run popped = openRuns.pop();
151-
textBuilder.append(popped.close);
152172
if (popped.sameOpen(closer)) {
173+
// before closing the current run, if the run has a transformContent
174+
// function, use it to transform the content of the current run, based on
175+
// the openPosInOutput set when the current run was opened
176+
if (popped.transformContent != null && popped.openPosInOutput >= 0) {
177+
textBuilder.replace(popped.openPosInOutput, textBuilder.length(),
178+
popped.transformContent.apply(
179+
textBuilder.substring(popped.openPosInOutput)));
180+
}
181+
// close the run that we really need to close
182+
textBuilder.append(popped.close);
153183
break;
154184
}
155185
// we keep popping from openRuns, closing all of the runs we find,
156186
// until we find the run that we really need to close ...
187+
textBuilder.append(popped.close);
157188
tempStack.push(popped);
158189
}
159190
while (!tempStack.empty()) {
@@ -168,8 +199,10 @@ static String runsToHtml(
168199
} else {
169200
// this will never be reached if openersIndex >= openers.size() because of the
170201
// way minPos is calculated
171-
textBuilder.append(openers.get(openersIndex).open);
172-
openRuns.push(openers.get(openersIndex));
202+
final Run opener = openers.get(openersIndex);
203+
textBuilder.append(opener.open);
204+
opener.openPosInOutput = textBuilder.length(); // save for transforming later
205+
openRuns.push(opener);
173206
++openersIndex;
174207
}
175208
}
@@ -180,11 +213,7 @@ static String runsToHtml(
180213
return textBuilder.toString()
181214
.replace("\n", "<br>")
182215
.replace(" ", " &nbsp;")
183-
// special link chips (e.g. for YT videos, YT channels or social media accounts):
184-
// u00a0 u00a0 [/•] u00a0 <link content> u00a0 u00a0
185-
.replace("\">\u00a0\u00a0/\u00a0", "\">")
186-
.replace("\">\u00a0\u00a0\u00a0", "\">")
187-
.replace("\u00a0\u00a0</a>", "</a>");
216+
.replace('\u00a0', ' ');
188217
}
189218

190219
private static void addAllCommandRuns(
@@ -212,12 +241,44 @@ private static void addAllCommandRuns(
212241
}
213242

214243
final String open = "<a href=\"" + Entities.escape(url) + "\">";
244+
final Function<String, String> transformContent = getTransformContentFun(run);
215245

216-
openers.add(new Run(open, LINK_CLOSE, startIndex, false));
217-
closers.add(new Run(open, LINK_CLOSE, startIndex + length, true));
246+
openers.add(new Run(open, LINK_CLOSE, startIndex, false,
247+
transformContent));
248+
closers.add(new Run(open, LINK_CLOSE, startIndex + length, true,
249+
transformContent));
218250
});
219251
}
220252

253+
private static Function<String, String> getTransformContentFun(final JsonObject run) {
254+
final String accessibilityLabel = run.getObject("onTapOptions")
255+
.getObject("accessibilityInfo")
256+
.getString("accessibilityLabel", "")
257+
// accessibility labels are e.g. "Instagram Channel Link: instagram_profile_name"
258+
.replaceFirst(" Channel Link", "");
259+
260+
final Function<String, String> transformContent;
261+
if (accessibilityLabel.isEmpty() || accessibilityLabel.startsWith("YouTube: ")) {
262+
// if there is no accessibility label, or the link points to YouTube, cleanup the link
263+
// text, see LINK_CONTENT_CLEANER_REGEX's documentation for more details
264+
transformContent = (content) -> {
265+
final Matcher m = LINK_CONTENT_CLEANER_REGEX.matcher(content);
266+
if (m.find()) {
267+
return m.group(1);
268+
}
269+
return content;
270+
};
271+
} else {
272+
// if there is an accessibility label, replace the link text with it, because on the
273+
// YouTube website an ambiguous link text is next to an icon explaining which service it
274+
// belongs to, but since we can't add icons, we instead use the accessibility label
275+
// which contains information about the service
276+
transformContent = (content) -> accessibilityLabel;
277+
}
278+
279+
return transformContent;
280+
}
281+
221282
private static void addAllStyleRuns(
222283
@Nonnull final JsonObject attributedDescription,
223284
@Nonnull final List<Run> openers,

0 commit comments

Comments
 (0)