mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Support tts:textEmphasis in TTML parser and WebView output
This commit is contained in:
parent
da52de669c
commit
fcda8d47ff
14 changed files with 1286 additions and 15 deletions
|
|
@ -0,0 +1,134 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2021 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.text.span;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
|
||||||
|
public final class TextEmphasisSpan {
|
||||||
|
/**
|
||||||
|
* <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/text-emphasis-position">Text Emphasis Position</a>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text emphasis position is unknown. If an implementation does not recognize or otherwise
|
||||||
|
* distinguish an annotation position value, then it must be interpreted as if a position of
|
||||||
|
* before were specified; as such, an implementation that supports text annotation marks must
|
||||||
|
* minimally support the before value.
|
||||||
|
*/
|
||||||
|
public static final int POSITION_UNKNOWN = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The emphasis marks should be positioned above the base text in horizontal writing mode The
|
||||||
|
* emphasis marks should be positioned to the right of the base text in vertical writing mode
|
||||||
|
*/
|
||||||
|
public static final int POSITION_BEFORE = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The emphasis marks should be positioned below the base text in horizontal writing mode The
|
||||||
|
* emphasis marks should be positioned to the left of the base text in vertical writing mode
|
||||||
|
*/
|
||||||
|
public static final int POSITION_AFTER = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text emphasis should be positioned in following way:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Equivalent to {@link #POSITION_BEFORE} for (1) the only line area of or (2) the first line
|
||||||
|
* area of the last block area generated by a p element which contains annotated text
|
||||||
|
* <li>otherwise, equivalent to {@link #POSITION_AFTER}
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public static final int POSITION_OUTSIDE = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The possible positions of the emphasis marks relative to the base text.
|
||||||
|
*
|
||||||
|
* <p>One of:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #POSITION_UNKNOWN}
|
||||||
|
* <li>{@link #POSITION_BEFORE}
|
||||||
|
* <li>{@link #POSITION_AFTER}
|
||||||
|
* <li>{@link #POSITION_OUTSIDE}
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Documented
|
||||||
|
@Retention(SOURCE)
|
||||||
|
@IntDef({POSITION_UNKNOWN, POSITION_BEFORE, POSITION_AFTER, POSITION_OUTSIDE})
|
||||||
|
public @interface Position {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text emphasis position is unknown.
|
||||||
|
*/
|
||||||
|
public static final int MARK_UNKNOWN = -1;
|
||||||
|
|
||||||
|
public static final int MARK_AUTO = 1;
|
||||||
|
public static final int MARK_FILLED_CIRCLE = 2;
|
||||||
|
public static final int MARK_FILLED_DOT = 3;
|
||||||
|
public static final int MARK_FILLED_SESAME = 4;
|
||||||
|
public static final int MARK_OPEN_CIRCLE = 5;
|
||||||
|
public static final int MARK_OPEN_DOT = 6;
|
||||||
|
public static final int MARK_OPEN_SESAME = 7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The possible types of annotations used.
|
||||||
|
*
|
||||||
|
* <p>One of:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #MARK_UNKNOWN}
|
||||||
|
* <li>{@link #MARK_AUTO}
|
||||||
|
* <li>{@link #MARK_FILLED_CIRCLE}
|
||||||
|
* <li>{@link #MARK_FILLED_DOT}
|
||||||
|
* <li>{@link #MARK_FILLED_SESAME}
|
||||||
|
* <li>{@link #MARK_OPEN_CIRCLE}
|
||||||
|
* <li>{@link #MARK_OPEN_DOT}
|
||||||
|
* <li>{@link #MARK_OPEN_SESAME}
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Documented
|
||||||
|
@Retention(SOURCE)
|
||||||
|
@IntDef({MARK_UNKNOWN, MARK_AUTO, MARK_FILLED_CIRCLE, MARK_FILLED_DOT, MARK_FILLED_SESAME,
|
||||||
|
MARK_OPEN_CIRCLE, MARK_OPEN_DOT, MARK_OPEN_SESAME})
|
||||||
|
public @interface Mark {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The position of the text emphasis relative to the base text
|
||||||
|
*/
|
||||||
|
@TextEmphasisSpan.Position
|
||||||
|
public final int position;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text emphasis mark
|
||||||
|
*/
|
||||||
|
@TextEmphasisSpan.Mark
|
||||||
|
public final int mark;
|
||||||
|
|
||||||
|
public TextEmphasisSpan(@TextEmphasisSpan.Mark int mark,
|
||||||
|
@TextEmphasisSpan.Position int position) {
|
||||||
|
this.mark = mark;
|
||||||
|
this.position = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2021 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.text.ttml;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_AUTO;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_FILLED_CIRCLE;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_FILLED_DOT;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_FILLED_SESAME;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_OPEN_CIRCLE;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_OPEN_DOT;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_OPEN_SESAME;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.POSITION_AFTER;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.POSITION_BEFORE;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.POSITION_OUTSIDE;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.POSITION_UNKNOWN;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to emphasize text using markers above or below the text. For example, markers
|
||||||
|
* known as boutens are commonly used in Japanese texts. Boutens are dots placed above or below a
|
||||||
|
* word or phrase that act as literal points of emphasis, equivalent to the use of italics in
|
||||||
|
* English. Boutens can help express implied meanings which provide a richer and more dynamic
|
||||||
|
* translation.
|
||||||
|
*/
|
||||||
|
/* package */ final class TextEmphasis {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The position of the text emphasis relative to the base text.
|
||||||
|
*/
|
||||||
|
@TextEmphasisSpan.Position
|
||||||
|
public final int position;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The desired emphasis mark
|
||||||
|
*/
|
||||||
|
@TextEmphasisSpan.Mark
|
||||||
|
public final int mark;
|
||||||
|
|
||||||
|
private TextEmphasis(@TextEmphasisSpan.Mark int mark, @TextEmphasisSpan.Position int position) {
|
||||||
|
this.mark = mark;
|
||||||
|
this.position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "TextEmphasis{" +
|
||||||
|
"position=" + position +
|
||||||
|
", mark=" + mark +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TextEmphasis createTextEmphasis(@Nullable String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String parsingValue = value.toLowerCase().trim();
|
||||||
|
if ("".equals(parsingValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] nodes = parsingValue.split("\\s+");
|
||||||
|
|
||||||
|
switch (nodes.length) {
|
||||||
|
case 0:
|
||||||
|
return null;
|
||||||
|
case 1:
|
||||||
|
return handleOneNode(nodes[0]);
|
||||||
|
case 2:
|
||||||
|
return handleTwoNodes(nodes[0], nodes[1]);
|
||||||
|
default:
|
||||||
|
// We ignore anything after third entry in value
|
||||||
|
return handleThreeNodes(nodes[0], nodes[1], nodes[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable
|
||||||
|
TextEmphasis handleOneNode(@NonNull String value) {
|
||||||
|
|
||||||
|
if (TtmlNode.TEXT_EMPHASIS_NONE.equals(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle "auto" or unknown value
|
||||||
|
// If an implementation does not recognize or otherwise distinguish an emphasis style value,
|
||||||
|
// then it must be interpreted as if a style of auto were specified; as such, an implementation
|
||||||
|
// that supports text emphasis marks must minimally support the auto value.
|
||||||
|
return new TextEmphasis(MARK_AUTO, POSITION_UNKNOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable
|
||||||
|
TextEmphasis handleTwoNodes(@NonNull String mark, @NonNull String position) {
|
||||||
|
|
||||||
|
@TextEmphasisSpan.Position int positionEntry = getPosition(position);
|
||||||
|
@TextEmphasisSpan.Mark int markEntry;
|
||||||
|
switch (mark) {
|
||||||
|
case TtmlNode.TEXT_EMPHASIS_AUTO:
|
||||||
|
markEntry = MARK_AUTO;
|
||||||
|
break;
|
||||||
|
// If only circle, dot, or sesame is specified, then it is equivalent to filled circle,
|
||||||
|
// filled dot, and filled sesame, respectively.
|
||||||
|
case TtmlNode.TEXT_EMPHASIS_MARK_DOT:
|
||||||
|
markEntry = MARK_FILLED_DOT;
|
||||||
|
break;
|
||||||
|
case TtmlNode.TEXT_EMPHASIS_MARK_SESAME:
|
||||||
|
markEntry = MARK_FILLED_SESAME;
|
||||||
|
break;
|
||||||
|
case TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE:
|
||||||
|
markEntry = MARK_FILLED_CIRCLE;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// This is use case for: "filled dot" when position is not specified.
|
||||||
|
return handleWithPosition(mark, position, POSITION_UNKNOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TextEmphasis(markEntry, positionEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable
|
||||||
|
TextEmphasis handleWithPosition(@NonNull String markStyle, @Nullable String mark,
|
||||||
|
@TextEmphasisSpan.Position int position) {
|
||||||
|
|
||||||
|
switch (mark) {
|
||||||
|
|
||||||
|
case TtmlNode.TEXT_EMPHASIS_MARK_DOT:
|
||||||
|
if (TtmlNode.TEXT_EMPHASIS_MARK_FILLED.equals(markStyle)) {
|
||||||
|
return new TextEmphasis(MARK_FILLED_DOT, position);
|
||||||
|
} else if (TtmlNode.TEXT_EMPHASIS_MARK_OPEN.equals(markStyle)) {
|
||||||
|
return new TextEmphasis(MARK_OPEN_DOT, position);
|
||||||
|
} else {
|
||||||
|
return new TextEmphasis(MARK_FILLED_DOT, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
case TtmlNode.TEXT_EMPHASIS_MARK_SESAME:
|
||||||
|
if (TtmlNode.TEXT_EMPHASIS_MARK_FILLED.equals(markStyle)) {
|
||||||
|
return new TextEmphasis(MARK_FILLED_SESAME, position);
|
||||||
|
} else if (TtmlNode.TEXT_EMPHASIS_MARK_OPEN.equals(markStyle)) {
|
||||||
|
return new TextEmphasis(MARK_OPEN_SESAME, position);
|
||||||
|
} else {
|
||||||
|
return new TextEmphasis(MARK_FILLED_SESAME, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
case TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE:
|
||||||
|
if (TtmlNode.TEXT_EMPHASIS_MARK_FILLED.equals(markStyle)) {
|
||||||
|
return new TextEmphasis(MARK_FILLED_CIRCLE, position);
|
||||||
|
} else if (TtmlNode.TEXT_EMPHASIS_MARK_OPEN.equals(markStyle)) {
|
||||||
|
return new TextEmphasis(MARK_OPEN_CIRCLE, position);
|
||||||
|
} else {
|
||||||
|
return new TextEmphasis(MARK_FILLED_CIRCLE, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Not supported, default to AUTO.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TextEmphasis(MARK_AUTO, POSITION_UNKNOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable
|
||||||
|
TextEmphasis handleThreeNodes(@NonNull String markStyle, @NonNull String mark,
|
||||||
|
@NonNull String position) {
|
||||||
|
|
||||||
|
@TextEmphasisSpan.Position int positionEntry = getPosition(position);
|
||||||
|
return handleWithPosition(markStyle, mark, positionEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @TextEmphasisSpan.Position
|
||||||
|
int getPosition(@NonNull String value) {
|
||||||
|
|
||||||
|
switch (value) {
|
||||||
|
case TtmlNode.TEXT_EMPHASIS_POSITION_AFTER:
|
||||||
|
return POSITION_AFTER;
|
||||||
|
case TtmlNode.TEXT_EMPHASIS_POSITION_BEFORE:
|
||||||
|
return POSITION_BEFORE;
|
||||||
|
case TtmlNode.TEXT_EMPHASIS_POSITION_OUTSIDE:
|
||||||
|
return POSITION_OUTSIDE;
|
||||||
|
default:
|
||||||
|
// ignore
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return POSITION_UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -609,6 +609,10 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case TtmlNode.ATTR_TTS_TEXT_EMPHASIS:
|
||||||
|
style = createIfNull(style).setTextEmphasis(
|
||||||
|
TextEmphasis.createTextEmphasis(Util.toLowerInvariant(attributeValue)));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// ignore
|
// ignore
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration";
|
public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration";
|
||||||
public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";
|
public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";
|
||||||
public static final String ATTR_TTS_TEXT_COMBINE = "textCombine";
|
public static final String ATTR_TTS_TEXT_COMBINE = "textCombine";
|
||||||
|
public static final String ATTR_TTS_TEXT_EMPHASIS = "textEmphasis";
|
||||||
public static final String ATTR_TTS_WRITING_MODE = "writingMode";
|
public static final String ATTR_TTS_WRITING_MODE = "writingMode";
|
||||||
|
|
||||||
// Values for ruby
|
// Values for ruby
|
||||||
|
|
@ -106,6 +107,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
public static final String VERTICAL_LR = "tblr";
|
public static final String VERTICAL_LR = "tblr";
|
||||||
public static final String VERTICAL_RL = "tbrl";
|
public static final String VERTICAL_RL = "tbrl";
|
||||||
|
|
||||||
|
// Values for textEmphasis
|
||||||
|
public static final String TEXT_EMPHASIS_NONE = "none";
|
||||||
|
public static final String TEXT_EMPHASIS_AUTO = "auto";
|
||||||
|
public static final String TEXT_EMPHASIS_MARK_DOT = "dot";
|
||||||
|
public static final String TEXT_EMPHASIS_MARK_SESAME = "sesame";
|
||||||
|
public static final String TEXT_EMPHASIS_MARK_CIRCLE = "circle";
|
||||||
|
public static final String TEXT_EMPHASIS_MARK_FILLED = "filled";
|
||||||
|
public static final String TEXT_EMPHASIS_MARK_OPEN = "open";
|
||||||
|
|
||||||
|
public static final String TEXT_EMPHASIS_POSITION_AFTER = "after";
|
||||||
|
public static final String TEXT_EMPHASIS_POSITION_BEFORE = "before";
|
||||||
|
public static final String TEXT_EMPHASIS_POSITION_OUTSIDE = "outside";
|
||||||
|
|
||||||
@Nullable public final String tag;
|
@Nullable public final String tag;
|
||||||
@Nullable public final String text;
|
@Nullable public final String text;
|
||||||
public final boolean isTextNode;
|
public final boolean isTextNode;
|
||||||
|
|
@ -243,7 +257,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
TreeMap<String, Cue.Builder> regionTextOutputs = new TreeMap<>();
|
TreeMap<String, Cue.Builder> regionTextOutputs = new TreeMap<>();
|
||||||
traverseForText(timeUs, false, regionId, regionTextOutputs);
|
traverseForText(timeUs, false, regionId, regionTextOutputs);
|
||||||
traverseForStyle(timeUs, globalStyles, regionTextOutputs);
|
traverseForStyle(timeUs, globalStyles, regionMap, regionId, regionTextOutputs);
|
||||||
|
|
||||||
List<Cue> cues = new ArrayList<>();
|
List<Cue> cues = new ArrayList<>();
|
||||||
|
|
||||||
|
|
@ -354,26 +368,32 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void traverseForStyle(
|
private void traverseForStyle(
|
||||||
long timeUs, Map<String, TtmlStyle> globalStyles, Map<String, Cue.Builder> regionOutputs) {
|
long timeUs, Map<String, TtmlStyle> globalStyles, Map<String, TtmlRegion> regionMaps,
|
||||||
|
String inheritedRegion, Map<String, Cue.Builder> regionOutputs) {
|
||||||
if (!isActive(timeUs)) {
|
if (!isActive(timeUs)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;
|
||||||
|
|
||||||
for (Map.Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
|
for (Map.Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
|
||||||
String regionId = entry.getKey();
|
String regionId = entry.getKey();
|
||||||
int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
|
int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
|
||||||
int end = entry.getValue();
|
int end = entry.getValue();
|
||||||
if (start != end) {
|
if (start != end) {
|
||||||
Cue.Builder regionOutput = Assertions.checkNotNull(regionOutputs.get(regionId));
|
Cue.Builder regionOutput = Assertions.checkNotNull(regionOutputs.get(regionId));
|
||||||
applyStyleToOutput(globalStyles, regionOutput, start, end);
|
@Cue.VerticalType int verticalType = Assertions
|
||||||
|
.checkNotNull(regionMaps.get(resolvedRegionId)).verticalType;
|
||||||
|
applyStyleToOutput(globalStyles, regionOutput, start, end, verticalType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (int i = 0; i < getChildCount(); ++i) {
|
for (int i = 0; i < getChildCount(); ++i) {
|
||||||
getChild(i).traverseForStyle(timeUs, globalStyles, regionOutputs);
|
getChild(i).traverseForStyle(timeUs, globalStyles, regionMaps, resolvedRegionId, regionOutputs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyStyleToOutput(
|
private void applyStyleToOutput(
|
||||||
Map<String, TtmlStyle> globalStyles, Cue.Builder regionOutput, int start, int end) {
|
Map<String, TtmlStyle> globalStyles, Cue.Builder regionOutput, int start, int end,
|
||||||
|
@Cue.VerticalType int verticalType) {
|
||||||
@Nullable TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
|
@Nullable TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
|
||||||
@Nullable SpannableStringBuilder text = (SpannableStringBuilder) regionOutput.getText();
|
@Nullable SpannableStringBuilder text = (SpannableStringBuilder) regionOutput.getText();
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
|
|
@ -381,7 +401,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
regionOutput.setText(text);
|
regionOutput.setText(text);
|
||||||
}
|
}
|
||||||
if (resolvedStyle != null) {
|
if (resolvedStyle != null) {
|
||||||
TtmlRenderUtil.applyStylesToSpan(text, start, end, resolvedStyle, parent, globalStyles);
|
TtmlRenderUtil
|
||||||
|
.applyStylesToSpan(text, start, end, resolvedStyle, parent, globalStyles, verticalType);
|
||||||
regionOutput.setTextAlignment(resolvedStyle.getTextAlign());
|
regionOutput.setTextAlignment(resolvedStyle.getTextAlign());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,11 @@ import android.text.style.StyleSpan;
|
||||||
import android.text.style.TypefaceSpan;
|
import android.text.style.TypefaceSpan;
|
||||||
import android.text.style.UnderlineSpan;
|
import android.text.style.UnderlineSpan;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.text.Cue;
|
||||||
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
|
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
|
||||||
import com.google.android.exoplayer2.text.span.RubySpan;
|
import com.google.android.exoplayer2.text.span.RubySpan;
|
||||||
import com.google.android.exoplayer2.text.span.SpanUtil;
|
import com.google.android.exoplayer2.text.span.SpanUtil;
|
||||||
|
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
|
|
@ -83,7 +85,8 @@ import java.util.Map;
|
||||||
int end,
|
int end,
|
||||||
TtmlStyle style,
|
TtmlStyle style,
|
||||||
@Nullable TtmlNode parent,
|
@Nullable TtmlNode parent,
|
||||||
Map<String, TtmlStyle> globalStyles) {
|
Map<String, TtmlStyle> globalStyles,
|
||||||
|
@Cue.VerticalType int verticalType) {
|
||||||
|
|
||||||
if (style.getStyle() != TtmlStyle.UNSPECIFIED) {
|
if (style.getStyle() != TtmlStyle.UNSPECIFIED) {
|
||||||
builder.setSpan(new StyleSpan(style.getStyle()), start, end,
|
builder.setSpan(new StyleSpan(style.getStyle()), start, end,
|
||||||
|
|
@ -119,6 +122,27 @@ import java.util.Map;
|
||||||
end,
|
end,
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
}
|
}
|
||||||
|
if (style.getTextEmphasis() != null) {
|
||||||
|
TextEmphasis textEmphasis = style.getTextEmphasis();
|
||||||
|
// https://www.w3.org/TR/ttml2/#style-value-emphasis-style
|
||||||
|
// If an implementation does not recognize or otherwise distinguish an emphasis style value,
|
||||||
|
// then it must be interpreted as if a style of auto were specified; as such, an
|
||||||
|
// implementation that supports text emphasis marks must minimally support the auto value.
|
||||||
|
// If a vertical writing mode applies, then equivalent to filled sesame; otherwise, equivalent
|
||||||
|
// to filled circle.
|
||||||
|
@TextEmphasisSpan.Mark int mark = textEmphasis.mark;
|
||||||
|
if (textEmphasis.mark == TextEmphasisSpan.MARK_AUTO
|
||||||
|
|| textEmphasis.mark == TextEmphasisSpan.MARK_UNKNOWN) {
|
||||||
|
mark = (verticalType == Cue.VERTICAL_TYPE_LR || verticalType == Cue.VERTICAL_TYPE_RL) ?
|
||||||
|
TextEmphasisSpan.MARK_FILLED_SESAME : TextEmphasisSpan.MARK_FILLED_CIRCLE;
|
||||||
|
}
|
||||||
|
SpanUtil.addOrReplaceSpan(
|
||||||
|
builder,
|
||||||
|
new TextEmphasisSpan(mark, textEmphasis.position),
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
switch (style.getRubyType()) {
|
switch (style.getRubyType()) {
|
||||||
case TtmlStyle.RUBY_TYPE_BASE:
|
case TtmlStyle.RUBY_TYPE_BASE:
|
||||||
// look for the sibling RUBY_TEXT and add it as span between start & end.
|
// look for the sibling RUBY_TEXT and add it as span between start & end.
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,8 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
@Nullable private Layout.Alignment textAlign;
|
@Nullable private Layout.Alignment textAlign;
|
||||||
@OptionalBoolean private int textCombine;
|
@OptionalBoolean private int textCombine;
|
||||||
|
|
||||||
|
private TextEmphasis textEmphasis;
|
||||||
|
|
||||||
public TtmlStyle() {
|
public TtmlStyle() {
|
||||||
linethrough = UNSPECIFIED;
|
linethrough = UNSPECIFIED;
|
||||||
underline = UNSPECIFIED;
|
underline = UNSPECIFIED;
|
||||||
|
|
@ -238,6 +240,9 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
fontSizeUnit = ancestor.fontSizeUnit;
|
fontSizeUnit = ancestor.fontSizeUnit;
|
||||||
fontSize = ancestor.fontSize;
|
fontSize = ancestor.fontSize;
|
||||||
}
|
}
|
||||||
|
if (textEmphasis == null) {
|
||||||
|
textEmphasis = ancestor.textEmphasis;
|
||||||
|
}
|
||||||
// attributes not inherited as of http://www.w3.org/TR/ttml1/
|
// attributes not inherited as of http://www.w3.org/TR/ttml1/
|
||||||
if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) {
|
if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) {
|
||||||
setBackgroundColor(ancestor.backgroundColor);
|
setBackgroundColor(ancestor.backgroundColor);
|
||||||
|
|
@ -299,6 +304,16 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public TextEmphasis getTextEmphasis() {
|
||||||
|
return textEmphasis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TtmlStyle setTextEmphasis(@Nullable TextEmphasis textEmphasis) {
|
||||||
|
this.textEmphasis = textEmphasis;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public TtmlStyle setFontSize(float fontSize) {
|
public TtmlStyle setFontSize(float fontSize) {
|
||||||
this.fontSize = fontSize;
|
this.fontSize = fontSize;
|
||||||
return this;
|
return this;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,369 @@
|
||||||
|
package com.google.android.exoplayer2.text.ttml;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.text.ttml.TextEmphasis.createTextEmphasis;
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static com.google.common.truth.Truth.assertWithMessage;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Unit test for {@link TextEmphasis}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class TextEmphasisTest {
|
||||||
|
|
||||||
|
public final String TAG = "TextEmphasisTest";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNull() {
|
||||||
|
String value = null;
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmpty() {
|
||||||
|
String value = "";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNone() {
|
||||||
|
String value = "none";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAuto() {
|
||||||
|
String value = "auto";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_AUTO);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_UNKNOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAutoOutside() {
|
||||||
|
String value = "auto outside";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_AUTO);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If only circle, dot, or sesame is specified, then it is equivalent to filled circle, filled dot,
|
||||||
|
* and filled sesame, respectively.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDotBefore() {
|
||||||
|
String value = "dot before";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCircleBefore() {
|
||||||
|
String value = "circle before";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSesameBefore() {
|
||||||
|
String value = "sesame before";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDotAfter() {
|
||||||
|
String value = "dot AFTER";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCircleAfter() {
|
||||||
|
String value = "circle after";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSesameAfter() {
|
||||||
|
String value = "sesame aFter";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDotOutside() {
|
||||||
|
String value = "dot outside";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCircleOutside() {
|
||||||
|
String value = "circle outside";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSesameOutside() {
|
||||||
|
String value = "sesame outside";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenDotAfter() {
|
||||||
|
String value = "open dot AFTER";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_DOT);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenCircleAfter() {
|
||||||
|
String value = "Open circle after";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenSesameAfter() {
|
||||||
|
String value = "open sesame aFter";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_SESAME);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenDotBefore() {
|
||||||
|
String value = "open dot before";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_DOT);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenCircleBefore() {
|
||||||
|
String value = "Open circle Before";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenSesameBefore() {
|
||||||
|
String value = "open sesame Before";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_SESAME);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenDotOutside() {
|
||||||
|
String value = "open dot Outside";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_DOT);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenCircleOutside() {
|
||||||
|
String value = "Open circle Outside";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_CIRCLE);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenSesameOutside() {
|
||||||
|
String value = "open sesame outside";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_OPEN_SESAME);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledDotOutside() {
|
||||||
|
String value = "filled dot outside";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledCircleOutside() {
|
||||||
|
String value = "filled circle outside";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledSesameOutside() {
|
||||||
|
String value = "filled sesame outside";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledDotAfter() {
|
||||||
|
String value = "filled dot After";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledCircleAfter() {
|
||||||
|
String value = "filled circle after";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledSesameAfter() {
|
||||||
|
String value = "filled sesame After";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledDotBefore() {
|
||||||
|
String value = "filled dot before";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_DOT);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledCircleBefore() {
|
||||||
|
String value = "filled circle Before";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_CIRCLE);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledSesameBefore() {
|
||||||
|
String value = "filled sesame Before";
|
||||||
|
TextEmphasis textEmphasis = createTextEmphasis(value);
|
||||||
|
|
||||||
|
Log.d(TAG, "textEmphasis: " + textEmphasis);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertThat(textEmphasis.mark).isEqualTo(TextEmphasisSpan.MARK_FILLED_SESAME);
|
||||||
|
assertThat(textEmphasis.position).isEqualTo(TextEmphasisSpan.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@ import com.google.android.exoplayer2.text.Cue;
|
||||||
import com.google.android.exoplayer2.text.Subtitle;
|
import com.google.android.exoplayer2.text.Subtitle;
|
||||||
import com.google.android.exoplayer2.text.SubtitleDecoderException;
|
import com.google.android.exoplayer2.text.SubtitleDecoderException;
|
||||||
import com.google.android.exoplayer2.text.span.RubySpan;
|
import com.google.android.exoplayer2.text.span.RubySpan;
|
||||||
|
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.ColorParser;
|
import com.google.android.exoplayer2.util.ColorParser;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -36,7 +37,9 @@ import java.util.Map;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
/** Unit test for {@link TtmlDecoder}. */
|
/**
|
||||||
|
* Unit test for {@link TtmlDecoder}.
|
||||||
|
*/
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public final class TtmlDecoderTest {
|
public final class TtmlDecoderTest {
|
||||||
|
|
||||||
|
|
@ -67,6 +70,7 @@ public final class TtmlDecoderTest {
|
||||||
private static final String VERTICAL_TEXT_FILE = "media/ttml/vertical_text.xml";
|
private static final String VERTICAL_TEXT_FILE = "media/ttml/vertical_text.xml";
|
||||||
private static final String TEXT_COMBINE_FILE = "media/ttml/text_combine.xml";
|
private static final String TEXT_COMBINE_FILE = "media/ttml/text_combine.xml";
|
||||||
private static final String RUBIES_FILE = "media/ttml/rubies.xml";
|
private static final String RUBIES_FILE = "media/ttml/rubies.xml";
|
||||||
|
private static final String TEXT_EMPHASIS_FILE = "media/ttml/text_emphasis.xml";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void inlineAttributes() throws IOException, SubtitleDecoderException {
|
public void inlineAttributes() throws IOException, SubtitleDecoderException {
|
||||||
|
|
@ -109,12 +113,10 @@ public final class TtmlDecoderTest {
|
||||||
* framework level. Tests that <i>lime</i> resolves to <code>#FF00FF00</code> not <code>#00FF00
|
* framework level. Tests that <i>lime</i> resolves to <code>#FF00FF00</code> not <code>#00FF00
|
||||||
* </code>.
|
* </code>.
|
||||||
*
|
*
|
||||||
* @see <a
|
|
||||||
* href="https://github.com/android/platform_frameworks_base/blob/jb-mr2-release/graphics/java/android/graphics/Color.java#L414">
|
|
||||||
* JellyBean Color</a> <a
|
|
||||||
* href="https://github.com/android/platform_frameworks_base/blob/kitkat-mr2.2-release/graphics/java/android/graphics/Color.java#L414">
|
|
||||||
* Kitkat Color</a>
|
|
||||||
* @throws IOException thrown if reading subtitle file fails.
|
* @throws IOException thrown if reading subtitle file fails.
|
||||||
|
* @see <a href="https://github.com/android/platform_frameworks_base/blob/jb-mr2-release/graphics/java/android/graphics/Color.java#L414">
|
||||||
|
* JellyBean Color</a> <a href="https://github.com/android/platform_frameworks_base/blob/kitkat-mr2.2-release/graphics/java/android/graphics/Color.java#L414">
|
||||||
|
* Kitkat Color</a>
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void lime() throws IOException, SubtitleDecoderException {
|
public void lime() throws IOException, SubtitleDecoderException {
|
||||||
|
|
@ -674,6 +676,105 @@ public final class TtmlDecoderTest {
|
||||||
assertThat(sixthCue).hasNoRubySpanBetween(0, sixthCue.length());
|
assertThat(sixthCue).hasNoRubySpanBetween(0, sixthCue.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void textEmphasis() throws IOException, SubtitleDecoderException {
|
||||||
|
TtmlSubtitle subtitle = getSubtitle(TEXT_EMPHASIS_FILE);
|
||||||
|
|
||||||
|
Spanned firstCue = getOnlyCueTextAtTimeUs(subtitle, 10_000_000);
|
||||||
|
assertThat(firstCue)
|
||||||
|
.hasNoTextEmphasisSpanBetween("None ".length(), "None おはよ".length());
|
||||||
|
|
||||||
|
Spanned secondCue = getOnlyCueTextAtTimeUs(subtitle, 20_000_000);
|
||||||
|
assertThat(secondCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Auto ".length(), "Auto ございます".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE,
|
||||||
|
TextEmphasisSpan.POSITION_UNKNOWN);
|
||||||
|
|
||||||
|
Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000);
|
||||||
|
assertThat(thirdCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Filled circle ".length(), "Filled circle こんばんは".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE,
|
||||||
|
TextEmphasisSpan.POSITION_UNKNOWN);
|
||||||
|
|
||||||
|
Spanned fourthCue = getOnlyCueTextAtTimeUs(subtitle, 40_000_000);
|
||||||
|
assertThat(fourthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Filled dot ".length(), "Filled dot ございます".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_DOT, TextEmphasisSpan.POSITION_UNKNOWN);
|
||||||
|
|
||||||
|
Spanned fifthCue = getOnlyCueTextAtTimeUs(subtitle, 50_000_000);
|
||||||
|
assertThat(fifthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Filled sesame ".length(), "Filled sesame おはよ".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME,
|
||||||
|
TextEmphasisSpan.POSITION_UNKNOWN);
|
||||||
|
|
||||||
|
Spanned sixthCue = getOnlyCueTextAtTimeUs(subtitle, 60_000_000);
|
||||||
|
assertThat(sixthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Open circle before ".length(),
|
||||||
|
"Open circle before ございます".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_CIRCLE, TextEmphasisSpan.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned seventhCue = getOnlyCueTextAtTimeUs(subtitle, 70_000_000);
|
||||||
|
assertThat(seventhCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Open dot after ".length(), "Open dot after おはよ".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_DOT, TextEmphasisSpan.POSITION_AFTER);
|
||||||
|
|
||||||
|
Spanned eighthCue = getOnlyCueTextAtTimeUs(subtitle, 80_000_000);
|
||||||
|
assertThat(eighthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Open sesame outside ".length(),
|
||||||
|
"Open sesame outside ございます".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_SESAME, TextEmphasisSpan.POSITION_OUTSIDE);
|
||||||
|
|
||||||
|
Spanned ninthCue = getOnlyCueTextAtTimeUs(subtitle, 90_000_000);
|
||||||
|
assertThat(ninthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Auto outside ".length(), "Auto outside おはよ".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE,
|
||||||
|
TextEmphasisSpan.POSITION_OUTSIDE);
|
||||||
|
|
||||||
|
Spanned tenthCue = getOnlyCueTextAtTimeUs(subtitle, 100_000_000);
|
||||||
|
assertThat(tenthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Circle before ".length(), "Circle before ございます".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned eleventhCue = getOnlyCueTextAtTimeUs(subtitle, 110_000_000);
|
||||||
|
assertThat(eleventhCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Sesame after ".length(), "Sesame after おはよ".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME, TextEmphasisSpan.POSITION_AFTER);
|
||||||
|
|
||||||
|
Spanned twelfthCue = getOnlyCueTextAtTimeUs(subtitle, 120_000_000);
|
||||||
|
assertThat(twelfthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Dot outside ".length(), "Dot outside ございます".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_DOT, TextEmphasisSpan.POSITION_OUTSIDE);
|
||||||
|
|
||||||
|
Spanned thirteenthCue = getOnlyCueTextAtTimeUs(subtitle, 130_000_000);
|
||||||
|
assertThat(thirteenthCue)
|
||||||
|
.hasNoTextEmphasisSpanBetween("No textEmphasis property ".length(),
|
||||||
|
"No textEmphasis property おはよ".length());
|
||||||
|
|
||||||
|
Spanned fourteenthCue = getOnlyCueTextAtTimeUs(subtitle, 140_000_000);
|
||||||
|
assertThat(fourteenthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Auto (TBLR) ".length(), "Auto (TBLR) ございます".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME,
|
||||||
|
TextEmphasisSpan.POSITION_UNKNOWN);
|
||||||
|
|
||||||
|
Spanned fifteenthCue = getOnlyCueTextAtTimeUs(subtitle, 150_000_000);
|
||||||
|
assertThat(fifteenthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Auto (TBRL) ".length(), "Auto (TBRL) おはよ".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME,
|
||||||
|
TextEmphasisSpan.POSITION_UNKNOWN);
|
||||||
|
|
||||||
|
Spanned sixteenthCue = getOnlyCueTextAtTimeUs(subtitle, 160_000_000);
|
||||||
|
assertThat(sixteenthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Auto (TB) ".length(), "Auto (TB) ございます".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_SESAME,
|
||||||
|
TextEmphasisSpan.POSITION_UNKNOWN);
|
||||||
|
|
||||||
|
Spanned seventeenthCue = getOnlyCueTextAtTimeUs(subtitle, 170_000_000);
|
||||||
|
assertThat(seventeenthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Auto (LR) ".length(), "Auto (LR) おはよ".length())
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE,
|
||||||
|
TextEmphasisSpan.POSITION_UNKNOWN);
|
||||||
|
}
|
||||||
|
|
||||||
private static Spanned getOnlyCueTextAtTimeUs(Subtitle subtitle, long timeUs) {
|
private static Spanned getOnlyCueTextAtTimeUs(Subtitle subtitle, long timeUs) {
|
||||||
Cue cue = getOnlyCueAtTimeUs(subtitle, timeUs);
|
Cue cue = getOnlyCueAtTimeUs(subtitle, timeUs);
|
||||||
assertThat(cue.text).isInstanceOf(Spanned.class);
|
assertThat(cue.text).isInstanceOf(Spanned.class);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@
|
||||||
package com.google.android.exoplayer2.text.ttml;
|
package com.google.android.exoplayer2.text.ttml;
|
||||||
|
|
||||||
import static android.graphics.Color.BLACK;
|
import static android.graphics.Color.BLACK;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_FILLED_DOT;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.MARK_OPEN_SESAME;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.POSITION_AFTER;
|
||||||
|
import static com.google.android.exoplayer2.text.span.TextEmphasisSpan.POSITION_BEFORE;
|
||||||
import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD;
|
import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD;
|
||||||
import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD_ITALIC;
|
import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD_ITALIC;
|
||||||
import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_ITALIC;
|
import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_ITALIC;
|
||||||
|
|
@ -46,6 +50,7 @@ public final class TtmlStyleTest {
|
||||||
private static final int RUBY_POSITION = RubySpan.POSITION_UNDER;
|
private static final int RUBY_POSITION = RubySpan.POSITION_UNDER;
|
||||||
private static final Layout.Alignment TEXT_ALIGN = Layout.Alignment.ALIGN_CENTER;
|
private static final Layout.Alignment TEXT_ALIGN = Layout.Alignment.ALIGN_CENTER;
|
||||||
private static final boolean TEXT_COMBINE = true;
|
private static final boolean TEXT_COMBINE = true;
|
||||||
|
public static final String TEXT_EMPHASIS_STYLE="dot before";
|
||||||
|
|
||||||
private final TtmlStyle populatedStyle =
|
private final TtmlStyle populatedStyle =
|
||||||
new TtmlStyle()
|
new TtmlStyle()
|
||||||
|
|
@ -62,7 +67,8 @@ public final class TtmlStyleTest {
|
||||||
.setRubyType(RUBY_TYPE)
|
.setRubyType(RUBY_TYPE)
|
||||||
.setRubyPosition(RUBY_POSITION)
|
.setRubyPosition(RUBY_POSITION)
|
||||||
.setTextAlign(TEXT_ALIGN)
|
.setTextAlign(TEXT_ALIGN)
|
||||||
.setTextCombine(TEXT_COMBINE);
|
.setTextCombine(TEXT_COMBINE)
|
||||||
|
.setTextEmphasis(TextEmphasis.createTextEmphasis(TEXT_EMPHASIS_STYLE));
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void inheritStyle() {
|
public void inheritStyle() {
|
||||||
|
|
@ -86,6 +92,9 @@ public final class TtmlStyleTest {
|
||||||
assertWithMessage("backgroundColor should not be inherited")
|
assertWithMessage("backgroundColor should not be inherited")
|
||||||
.that(style.hasBackgroundColor())
|
.that(style.hasBackgroundColor())
|
||||||
.isFalse();
|
.isFalse();
|
||||||
|
assertThat(style.getTextEmphasis()).isNotNull();
|
||||||
|
assertThat(style.getTextEmphasis().mark).isEqualTo(MARK_FILLED_DOT);
|
||||||
|
assertThat(style.getTextEmphasis().position).isEqualTo(POSITION_BEFORE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -109,6 +118,9 @@ public final class TtmlStyleTest {
|
||||||
.that(style.getBackgroundColor())
|
.that(style.getBackgroundColor())
|
||||||
.isEqualTo(BACKGROUND_COLOR);
|
.isEqualTo(BACKGROUND_COLOR);
|
||||||
assertWithMessage("rubyType should be chained").that(style.getRubyType()).isEqualTo(RUBY_TYPE);
|
assertWithMessage("rubyType should be chained").that(style.getRubyType()).isEqualTo(RUBY_TYPE);
|
||||||
|
assertThat(style.getTextEmphasis()).isNotNull();
|
||||||
|
assertThat(style.getTextEmphasis().mark).isEqualTo(MARK_FILLED_DOT);
|
||||||
|
assertThat(style.getTextEmphasis().position).isEqualTo(POSITION_BEFORE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -245,4 +257,13 @@ public final class TtmlStyleTest {
|
||||||
style.setTextCombine(true);
|
style.setTextCombine(true);
|
||||||
assertThat(style.getTextCombine()).isTrue();
|
assertThat(style.getTextCombine()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void textEmphasis() {
|
||||||
|
TtmlStyle style = new TtmlStyle();
|
||||||
|
assertThat(style.getTextEmphasis()).isNull();
|
||||||
|
style.setTextEmphasis(TextEmphasis.createTextEmphasis("open sesame after"));
|
||||||
|
assertThat(style.getTextEmphasis().mark).isEqualTo(MARK_OPEN_SESAME);
|
||||||
|
assertThat(style.getTextEmphasis().position).isEqualTo(POSITION_AFTER);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import android.util.SparseArray;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
|
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
|
||||||
import com.google.android.exoplayer2.text.span.RubySpan;
|
import com.google.android.exoplayer2.text.span.RubySpan;
|
||||||
|
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
|
@ -197,6 +198,14 @@ import java.util.regex.Pattern;
|
||||||
}
|
}
|
||||||
} else if (span instanceof UnderlineSpan) {
|
} else if (span instanceof UnderlineSpan) {
|
||||||
return "<u>";
|
return "<u>";
|
||||||
|
} else if (span instanceof TextEmphasisSpan) {
|
||||||
|
TextEmphasisSpan textEmphasisSpan = (TextEmphasisSpan) span;
|
||||||
|
String style = getTextEmphasisStyle(textEmphasisSpan.mark);
|
||||||
|
String position = getTextEmphasisPosition(textEmphasisSpan.position);
|
||||||
|
return Util
|
||||||
|
.formatInvariant("<span style='-webkit-text-emphasis-style: %s; text-emphasis-style: %s; "
|
||||||
|
+ "-webkit-text-emphasis-position: %s; text-emphasis-position: %s;'>",
|
||||||
|
style, style, position, position);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +218,8 @@ import java.util.regex.Pattern;
|
||||||
|| span instanceof BackgroundColorSpan
|
|| span instanceof BackgroundColorSpan
|
||||||
|| span instanceof HorizontalTextInVerticalContextSpan
|
|| span instanceof HorizontalTextInVerticalContextSpan
|
||||||
|| span instanceof AbsoluteSizeSpan
|
|| span instanceof AbsoluteSizeSpan
|
||||||
|| span instanceof RelativeSizeSpan) {
|
|| span instanceof RelativeSizeSpan
|
||||||
|
|| span instanceof TextEmphasisSpan) {
|
||||||
return "</span>";
|
return "</span>";
|
||||||
} else if (span instanceof TypefaceSpan) {
|
} else if (span instanceof TypefaceSpan) {
|
||||||
@Nullable String fontFamily = ((TypefaceSpan) span).getFamily();
|
@Nullable String fontFamily = ((TypefaceSpan) span).getFamily();
|
||||||
|
|
@ -232,6 +242,47 @@ import java.util.regex.Pattern;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String getTextEmphasisStyle(@TextEmphasisSpan.Mark int mark) {
|
||||||
|
switch (mark) {
|
||||||
|
case TextEmphasisSpan.MARK_FILLED_CIRCLE:
|
||||||
|
return "filled circle";
|
||||||
|
case TextEmphasisSpan.MARK_FILLED_DOT:
|
||||||
|
return "filled dot";
|
||||||
|
case TextEmphasisSpan.MARK_FILLED_SESAME:
|
||||||
|
return "filled sesame";
|
||||||
|
case TextEmphasisSpan.MARK_OPEN_CIRCLE:
|
||||||
|
return "open circle";
|
||||||
|
case TextEmphasisSpan.MARK_OPEN_DOT:
|
||||||
|
return "open dot";
|
||||||
|
case TextEmphasisSpan.MARK_OPEN_SESAME:
|
||||||
|
return "open sesame";
|
||||||
|
case TextEmphasisSpan.MARK_AUTO: // TODO
|
||||||
|
// https://www.w3.org/TR/ttml2/#style-value-emphasis-style
|
||||||
|
// If a vertical writing mode applies, then equivalent to filled sesame; otherwise,
|
||||||
|
// equivalent to filled circle.
|
||||||
|
case TextEmphasisSpan.MARK_UNKNOWN:
|
||||||
|
default:
|
||||||
|
return "unset";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTextEmphasisPosition(@TextEmphasisSpan.Position int position){
|
||||||
|
switch (position) {
|
||||||
|
case TextEmphasisSpan.POSITION_AFTER:
|
||||||
|
return "under left";
|
||||||
|
case TextEmphasisSpan.POSITION_UNKNOWN:
|
||||||
|
case TextEmphasisSpan.POSITION_BEFORE:
|
||||||
|
case TextEmphasisSpan.POSITION_OUTSIDE: /* Not supported, fallback to "before" */
|
||||||
|
default:
|
||||||
|
// https://www.w3.org/TR/ttml2/#style-value-annotation-position
|
||||||
|
// If an implementation does not recognize or otherwise distinguish an annotation position
|
||||||
|
// value, then it must be interpreted as if a position of before were specified; as such,
|
||||||
|
// an implementation that supports text annotation marks must minimally support the before
|
||||||
|
// value.
|
||||||
|
return "over right";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Transition getOrCreate(SparseArray<Transition> transitions, int key) {
|
private static Transition getOrCreate(SparseArray<Transition> transitions, int key) {
|
||||||
@Nullable Transition transition = transitions.get(key);
|
@Nullable Transition transition = transitions.get(key);
|
||||||
if (transition == null) {
|
if (transition == null) {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
|
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
|
||||||
import com.google.android.exoplayer2.text.span.RubySpan;
|
import com.google.android.exoplayer2.text.span.RubySpan;
|
||||||
|
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
|
|
@ -279,6 +280,37 @@ public class SpannedToHtmlConverterTest {
|
||||||
+ "section");
|
+ "section");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void convert_supportsTextEmphasisSpan() {
|
||||||
|
SpannableString spanned = new SpannableString("Text emphasis おはよ ございます ");
|
||||||
|
spanned.setSpan(
|
||||||
|
new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_BEFORE),
|
||||||
|
"Text emphasis ".length(),
|
||||||
|
"Text emphasis おはよ".length(),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
spanned.setSpan(
|
||||||
|
new TextEmphasisSpan(TextEmphasisSpan.MARK_OPEN_SESAME, TextEmphasisSpan.POSITION_AFTER),
|
||||||
|
"Text emphasis おはよ ".length(),
|
||||||
|
"Text emphasis おはよ ございます ".length(),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
|
||||||
|
SpannedToHtmlConverter.convert(spanned, displayDensity);
|
||||||
|
|
||||||
|
assertThat(htmlAndCss.cssRuleSets).isEmpty();
|
||||||
|
assertThat(htmlAndCss.html)
|
||||||
|
.isEqualTo(
|
||||||
|
"Text emphasis <span style='"
|
||||||
|
+ "-webkit-text-emphasis-style: filled circle; text-emphasis-style: filled circle; "
|
||||||
|
+ "-webkit-text-emphasis-position: over right; text-emphasis-position: over right;"
|
||||||
|
+ "'>おはよ</span> "
|
||||||
|
+ "<span style='"
|
||||||
|
+ "-webkit-text-emphasis-style: open sesame; text-emphasis-style: open sesame; "
|
||||||
|
+ "-webkit-text-emphasis-position: under left; text-emphasis-position: under left;"
|
||||||
|
+ "'>ございます </span>");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void convert_supportsUnderlineSpan() {
|
public void convert_supportsUnderlineSpan() {
|
||||||
SpannableString spanned = new SpannableString("String with underlined section.");
|
SpannableString spanned = new SpannableString("String with underlined section.");
|
||||||
|
|
|
||||||
65
testdata/src/test/assets/media/ttml/text_emphasis.xml
vendored
Normal file
65
testdata/src/test/assets/media/ttml/text_emphasis.xml
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<tt xmlns:ttm="http://www.w3.org/2006/10/ttaf1#metadata"
|
||||||
|
xmlns:ttp="http://www.w3.org/2006/10/ttaf1#parameter"
|
||||||
|
xmlns:tts="http://www.w3.org/2006/10/ttaf1#style"
|
||||||
|
xmlns="http://www.w3.org/ns/ttml">
|
||||||
|
<head>
|
||||||
|
<region xml:id="region1" tts:extent="80.000% 80.000%" tts:origin="10.000% 10.000%" tts:writingMode="tbrl"/>
|
||||||
|
<region xml:id="region2" tts:extent="80.000% 80.000%" tts:origin="10.000% 10.000%" tts:writingMode="tblr"/>
|
||||||
|
<region xml:id="region3" tts:extent="80.000% 80.000%" tts:origin="10.000% 10.000%" tts:writingMode="tb"/>
|
||||||
|
<region xml:id="region4" tts:extent="80.000% 80.000%" tts:origin="10.000% 10.000%" tts:writingMode="lr"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<p begin="10s" end="18s">None <span tts:textEmphasis="none">おはよ</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="20s" end="28s">Auto <span tts:textEmphasis="auto">ございます</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="30s" end="38s">Filled circle <span tts:textEmphasis="filled circle">こんばんは</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="40s" end="48s">Filled dot <span tts:textEmphasis="filled dot">ございます</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="50s" end="58s">Filled sesame <span tts:textEmphasis="filled sesame">おはよ</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="60s" end="68s">Open circle before <span tts:textEmphasis="open circle before">ございます</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="70s" end="78s">Open dot after <span tts:textEmphasis="open dot after">おはよ</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="80s" end="88s">Open sesame outside <span tts:textEmphasis="open sesame outside">ございます</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="90s" end="98s">Auto outside <span tts:textEmphasis="auto outside">おはよ</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="100s" end="108s">Circle before <span tts:textEmphasis="circle before">ございます</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="110s" end="118s">Sesame after <span tts:textEmphasis="sesame after">おはよ</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="120s" end="128s">Dot outside <span tts:textEmphasis="dot outside">ございます</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="130s" end="138s">No textEmphasis property <span>おはよ</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="140s" end="148s" region="region1">Auto (TBLR) <span tts:textEmphasis="auto">ございます</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="150s" end="158s" region="region2">Auto (TBRL) <span tts:textEmphasis="auto">おはよ</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="160s" end="168s" region="region3">Auto (TB) <span tts:textEmphasis="auto">ございます</span></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p begin="170s" end="178s" region="region4">Auto (LR) <span tts:textEmphasis="auto">おはよ</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</tt>
|
||||||
|
|
@ -38,6 +38,7 @@ import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
|
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
|
||||||
import com.google.android.exoplayer2.text.span.RubySpan;
|
import com.google.android.exoplayer2.text.span.RubySpan;
|
||||||
|
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.common.truth.Fact;
|
import com.google.common.truth.Fact;
|
||||||
import com.google.common.truth.FailureMetadata;
|
import com.google.common.truth.FailureMetadata;
|
||||||
|
|
@ -578,6 +579,47 @@ public final class SpannedSubject extends Subject {
|
||||||
return ALREADY_FAILED_WITH_FLAGS;
|
return ALREADY_FAILED_WITH_FLAGS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that the subject has an {@link TextEmphasisSpan} from {@code start}
|
||||||
|
* to {@code end}.
|
||||||
|
*
|
||||||
|
* @param start The start of the expected span.
|
||||||
|
* @param end The end of the expected span.
|
||||||
|
* @return A {@link WithSpanFlags} object for optional additional assertions on the flags.
|
||||||
|
*/
|
||||||
|
public TextEmphasisDescription hasTextEmphasisSpanBetween(int start, int end) {
|
||||||
|
if (actual == null) {
|
||||||
|
failWithoutActual(simpleFact("Spanned must not be null"));
|
||||||
|
return ALREADY_FAILED_WITH_MARK;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TextEmphasisSpan> textEmphasisSpans =
|
||||||
|
findMatchingSpans(start, end, TextEmphasisSpan.class);
|
||||||
|
if (textEmphasisSpans.size() == 1) {
|
||||||
|
return check("TextEmphasisSpan (start=%s,end=%s)", start, end).about(textEmphasisSubjects(actual)).that(textEmphasisSpans);
|
||||||
|
}
|
||||||
|
failWithExpectedSpan(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
TextEmphasisSpan.class,
|
||||||
|
actual.toString().substring(start, end));
|
||||||
|
return ALREADY_FAILED_WITH_MARK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that the subject has no {@link TextEmphasisSpan}s on any of the text between
|
||||||
|
* {@code start} and {@code end}.
|
||||||
|
*
|
||||||
|
* <p>This fails even if the start and end indexes don't exactly match.
|
||||||
|
*
|
||||||
|
* @param start The start index to start searching for spans.
|
||||||
|
* @param end The end index to stop searching for spans.
|
||||||
|
*/
|
||||||
|
public void hasNoTextEmphasisSpanBetween(int start, int end) {
|
||||||
|
hasNoSpansOfTypeBetween(TextEmphasisSpan.class, start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks that the subject has no {@link HorizontalTextInVerticalContextSpan}s on any of the text
|
* Checks that the subject has no {@link HorizontalTextInVerticalContextSpan}s on any of the text
|
||||||
* between {@code start} and {@code end}.
|
* between {@code start} and {@code end}.
|
||||||
|
|
@ -1110,4 +1152,95 @@ public final class SpannedSubject extends Subject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Allows assertions about a span's textEmphasis mark and its position. */
|
||||||
|
public interface TextEmphasisDescription {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that at least one of the matched spans has the expected {@code mark} and {@code position}.
|
||||||
|
*
|
||||||
|
* @param mark The expected mark
|
||||||
|
* @param position The expected position of the mark
|
||||||
|
* @return A {@link WithSpanFlags} object for optional additional assertions on the flags.
|
||||||
|
*/
|
||||||
|
AndSpanFlags withMarkAndPosition(@TextEmphasisSpan.Mark int mark,
|
||||||
|
@TextEmphasisSpan.Position int position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final TextEmphasisDescription ALREADY_FAILED_WITH_MARK =
|
||||||
|
(mark, position) -> ALREADY_FAILED_AND_FLAGS;
|
||||||
|
|
||||||
|
private static Factory<TextEmphasisSubject, List<TextEmphasisSpan>> textEmphasisSubjects(Spanned actualSpanned) {
|
||||||
|
return (FailureMetadata metadata, List<TextEmphasisSpan> spans) ->
|
||||||
|
new TextEmphasisSubject(metadata, spans, actualSpanned);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TextEmphasisSubject extends Subject implements TextEmphasisDescription {
|
||||||
|
|
||||||
|
private final List<TextEmphasisSpan> actualSpans;
|
||||||
|
private final Spanned actualSpanned;
|
||||||
|
|
||||||
|
private TextEmphasisSubject(
|
||||||
|
FailureMetadata metadata, List<TextEmphasisSpan> actualSpans, Spanned actualSpanned) {
|
||||||
|
super(metadata, actualSpans);
|
||||||
|
this.actualSpans = actualSpans;
|
||||||
|
this.actualSpanned = actualSpanned;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AndSpanFlags withMarkAndPosition(@TextEmphasisSpan.Mark int mark,
|
||||||
|
@TextEmphasisSpan.Position int position) {
|
||||||
|
List<Integer> matchingSpanFlags = new ArrayList<>();
|
||||||
|
List<MarkAndPosition> textEmphasisMarksAndPositions = new ArrayList<>();
|
||||||
|
for (TextEmphasisSpan span : actualSpans) {
|
||||||
|
textEmphasisMarksAndPositions.add(new MarkAndPosition(span.mark, span.position));
|
||||||
|
if (span.mark == mark && span.position == position) {
|
||||||
|
matchingSpanFlags.add(actualSpanned.getSpanFlags(span));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check("textEmphasisMarkAndPosition")
|
||||||
|
.that(textEmphasisMarksAndPositions)
|
||||||
|
.containsExactly(new MarkAndPosition(mark, position));
|
||||||
|
return check("flags").about(spanFlags()).that(matchingSpanFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MarkAndPosition {
|
||||||
|
|
||||||
|
@TextEmphasisSpan.Mark
|
||||||
|
private final int mark;
|
||||||
|
@TextEmphasisSpan.Position
|
||||||
|
private final int position;
|
||||||
|
|
||||||
|
private MarkAndPosition(@TextEmphasisSpan.Mark int mark,
|
||||||
|
@TextEmphasisSpan.Position int position) {
|
||||||
|
this.mark = mark;
|
||||||
|
this.position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextEmphasisSubject.MarkAndPosition that = (TextEmphasisSubject.MarkAndPosition) o;
|
||||||
|
return (position == that.position) && (mark == that.mark);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = 34613 * mark + position;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("{mark=%s,position=%s}", mark, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ import com.google.android.exoplayer2.testutil.truth.SpannedSubject.AndSpanFlags;
|
||||||
import com.google.android.exoplayer2.testutil.truth.SpannedSubject.WithSpanFlags;
|
import com.google.android.exoplayer2.testutil.truth.SpannedSubject.WithSpanFlags;
|
||||||
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
|
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
|
||||||
import com.google.android.exoplayer2.text.span.RubySpan;
|
import com.google.android.exoplayer2.text.span.RubySpan;
|
||||||
|
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.common.truth.ExpectFailure;
|
import com.google.common.truth.ExpectFailure;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
@ -679,6 +681,104 @@ public class SpannedSubjectTest {
|
||||||
new RubySpan("ruby text", RubySpan.POSITION_OVER), SpannedSubject::hasNoRubySpanBetween);
|
new RubySpan("ruby text", RubySpan.POSITION_OVER), SpannedSubject::hasNoRubySpanBetween);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void textEmphasis_success() {
|
||||||
|
SpannableString spannable =
|
||||||
|
createSpannable(
|
||||||
|
new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_AFTER));
|
||||||
|
|
||||||
|
assertThat(spannable)
|
||||||
|
.hasTextEmphasisSpanBetween(SPAN_START, SPAN_END)
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_AFTER)
|
||||||
|
.andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void textEmphasis_wrongIndex() {
|
||||||
|
checkHasSpanFailsDueToIndexMismatch(
|
||||||
|
new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_AFTER),
|
||||||
|
SpannedSubject::hasTextEmphasisSpanBetween);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void textEmphasis_wrongMark() {
|
||||||
|
SpannableString spannable =
|
||||||
|
createSpannable(
|
||||||
|
new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_AFTER));
|
||||||
|
|
||||||
|
AssertionError expected =
|
||||||
|
expectFailure(
|
||||||
|
whenTesting ->
|
||||||
|
whenTesting
|
||||||
|
.that(spannable)
|
||||||
|
.hasTextEmphasisSpanBetween(SPAN_START, SPAN_END)
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_DOT,
|
||||||
|
TextEmphasisSpan.POSITION_AFTER));
|
||||||
|
|
||||||
|
assertThat(expected).factValue("value of").contains("textEmphasisMarkAndPosition");
|
||||||
|
assertThat(expected).factValue("expected").contains(Util.formatInvariant(
|
||||||
|
"{mark=%d,position=%d}", TextEmphasisSpan.MARK_OPEN_DOT,
|
||||||
|
TextEmphasisSpan.POSITION_AFTER));
|
||||||
|
assertThat(expected).factValue("but was").contains(Util.formatInvariant(
|
||||||
|
"{mark=%d,position=%d}", TextEmphasisSpan.MARK_FILLED_CIRCLE,
|
||||||
|
TextEmphasisSpan.POSITION_AFTER));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void textEmphasis_wrongPosition() {
|
||||||
|
SpannableString spannable =
|
||||||
|
createSpannable(
|
||||||
|
new TextEmphasisSpan(TextEmphasisSpan.MARK_OPEN_SESAME, TextEmphasisSpan.POSITION_BEFORE));
|
||||||
|
|
||||||
|
AssertionError expected =
|
||||||
|
expectFailure(
|
||||||
|
whenTesting ->
|
||||||
|
whenTesting
|
||||||
|
.that(spannable)
|
||||||
|
.hasTextEmphasisSpanBetween(SPAN_START, SPAN_END)
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_SESAME,
|
||||||
|
TextEmphasisSpan.POSITION_AFTER));
|
||||||
|
|
||||||
|
assertThat(expected).factValue("value of").contains("textEmphasisMarkAndPosition");
|
||||||
|
assertThat(expected).factValue("expected").contains(Util.formatInvariant(
|
||||||
|
"{mark=%d,position=%d}", TextEmphasisSpan.MARK_OPEN_SESAME,
|
||||||
|
TextEmphasisSpan.POSITION_AFTER));
|
||||||
|
assertThat(expected).factValue("but was").contains(Util.formatInvariant(
|
||||||
|
"{mark=%d,position=%d}", TextEmphasisSpan.MARK_OPEN_SESAME,
|
||||||
|
TextEmphasisSpan.POSITION_BEFORE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void textEmphasis_wrongFlags() {
|
||||||
|
checkHasSpanFailsDueToFlagMismatch(
|
||||||
|
new TextEmphasisSpan(TextEmphasisSpan.MARK_OPEN_SESAME, TextEmphasisSpan.POSITION_BEFORE),
|
||||||
|
(subject, start, end) ->
|
||||||
|
subject
|
||||||
|
.hasTextEmphasisSpanBetween(start, end)
|
||||||
|
.withMarkAndPosition(TextEmphasisSpan.MARK_OPEN_SESAME,
|
||||||
|
TextEmphasisSpan.POSITION_BEFORE));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noTextEmphasis_success() {
|
||||||
|
SpannableString spannable =
|
||||||
|
createSpannableWithUnrelatedSpanAnd(
|
||||||
|
new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE,
|
||||||
|
TextEmphasisSpan.POSITION_AFTER));
|
||||||
|
|
||||||
|
assertThat(spannable).hasNoTextEmphasisSpanBetween(UNRELATED_SPAN_START, UNRELATED_SPAN_END);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noTextEmphasis_failure() {
|
||||||
|
checkHasNoSpanFails(
|
||||||
|
new TextEmphasisSpan(TextEmphasisSpan.MARK_FILLED_CIRCLE, TextEmphasisSpan.POSITION_AFTER),
|
||||||
|
SpannedSubject::hasNoTextEmphasisSpanBetween);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void horizontalTextInVerticalContextSpan_success() {
|
public void horizontalTextInVerticalContextSpan_success() {
|
||||||
SpannableString spannable =
|
SpannableString spannable =
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue