Commit 81383f80 by aquilescanta Committed by Oliver Woodman

Add full selector support to CSS in WebVTT

This CL allows near-complete support to CSS selectors (I say near because not every
CSS rule applies to WebVTT).
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=120717498
parent 5cbf75b6
WEBVTT
STYLE
::cue(\n#id ){text-decoration:underline;}
STYLE
::cue(#id.class1.class2 ){ color: violet;}
STYLE
::cue(lang){font-family:Courier}
STYLE
::cue(.class.another ){font-weight: bold;}
STYLE
::cue(v.class[voice="Strider Trancos"] ){ font-weight:bold; }
STYLE
::cue(v#anId.class1.class2[voice="Robert"] ){ font-style:italic; }
id
00:00.000 --> 00:01.001
This should be underlined and <lang.class1.class2> courier and violet.
íd
00:02.000 --> 00:02.001
This <lang.class1.class2>should be just courier.
_id
00:02.500 --> 00:02.501
This <lang.class.another>should be courier and bold.
00:04.000 --> 00:04.001
This <v Strider Trancos> shouldn't be bold.</v>
This <v.class.clazz Strider Trancos> should be bold.
anId
00:05.000 --> 00:05.001
This is <v.class1.class3.class2 Pipo> specific </v>
<v.class1.class3.class2 Robert> But this is more italic</v>
...@@ -19,9 +19,6 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -19,9 +19,6 @@ import com.google.android.exoplayer.util.ParsableByteArray;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import java.util.HashMap;
import java.util.Map;
/** /**
* Unit test for {@link CssParser}. * Unit test for {@link CssParser}.
*/ */
...@@ -82,75 +79,46 @@ public final class CssParserTest extends InstrumentationTestCase { ...@@ -82,75 +79,46 @@ public final class CssParserTest extends InstrumentationTestCase {
} }
public void testParseMethodSimpleInput() { public void testParseMethodSimpleInput() {
String styleBlock = " ::cue { color : black; background-color: PapayaWhip }"; String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }";
// Expected style map construction. WebvttCssStyle expectedStyle = new WebvttCssStyle();
Map<String, WebvttCssStyle> expectedResult = new HashMap<>(); expectedStyle.setFontColor(0xFF000000);
expectedResult.put("", new WebvttCssStyle()); expectedStyle.setBackgroundColor(0xFFFFEFD5);
WebvttCssStyle style = expectedResult.get(""); assertParserProduces(expectedStyle, styleBlock1);
style.setFontColor(0xFF000000);
style.setBackgroundColor(0xFFFFEFD5);
assertCssProducesExpectedMap(expectedResult, new String[] { styleBlock });
}
public void testParseSimpleInputSeparately() {
String styleBlock1 = " ::cue { color : black }\n\n::cue { color : invalid }";
String styleBlock2 = " \n::cue {\n background-color\n:#00fFFe}";
// Expected style map construction.
Map<String, WebvttCssStyle> expectedResult = new HashMap<>();
expectedResult.put("", new WebvttCssStyle());
WebvttCssStyle style = expectedResult.get("");
style.setFontColor(0xFF000000);
style.setBackgroundColor(0xFF00FFFE);
assertCssProducesExpectedMap(expectedResult, new String[] { styleBlock1, styleBlock2 });
}
public void testDifferentSelectors() {
String styleBlock1 = " ::cue(\n#id ){text-decoration:underline;}";
String styleBlock2 = "::cue(elem ){font-family:Courier}";
String styleBlock3 = "::cue(.class ){font-weight: bold;}";
// Expected style map construction. String styleBlock2 = " ::cue { color : black }\n\n::cue { color : invalid }";
Map<String, WebvttCssStyle> expectedResult = new HashMap<>(); expectedStyle = new WebvttCssStyle();
expectedResult.put("#id", new WebvttCssStyle().setUnderline(true)); expectedStyle.setFontColor(0xFF000000);
expectedResult.put("elem", new WebvttCssStyle().setFontFamily("courier")); assertParserProduces(expectedStyle, styleBlock2);
expectedResult.put(".class", new WebvttCssStyle().setBold(true));
assertCssProducesExpectedMap(expectedResult, new String[] { styleBlock1, styleBlock2, String styleBlock3 = " \n::cue {\n background-color\n:#00fFFe}";
styleBlock3}); expectedStyle = new WebvttCssStyle();
expectedStyle.setBackgroundColor(0xFF00FFFE);
assertParserProduces(expectedStyle, styleBlock3);
} }
public void testMultiplePropertiesInBlock() { public void testMultiplePropertiesInBlock() {
String styleBlock = "::cue(#id){text-decoration:underline; background-color:green;" String styleBlock = "::cue(#id){text-decoration:underline; background-color:green;"
+ "color:red; font-family:Courier; font-weight:bold}"; + "color:red; font-family:Courier; font-weight:bold}";
// Expected style map construction.
Map<String, WebvttCssStyle> expectedResult = new HashMap<>();
WebvttCssStyle expectedStyle = new WebvttCssStyle(); WebvttCssStyle expectedStyle = new WebvttCssStyle();
expectedResult.put("#id", expectedStyle); expectedStyle.setTargetId("id");
expectedStyle.setUnderline(true); expectedStyle.setUnderline(true);
expectedStyle.setBackgroundColor(0xFF008000); expectedStyle.setBackgroundColor(0xFF008000);
expectedStyle.setFontColor(0xFFFF0000); expectedStyle.setFontColor(0xFFFF0000);
expectedStyle.setFontFamily("courier"); expectedStyle.setFontFamily("courier");
expectedStyle.setBold(true); expectedStyle.setBold(true);
assertCssProducesExpectedMap(expectedResult, new String[] { styleBlock }); assertParserProduces(expectedStyle, styleBlock);
} }
public void testRgbaColorExpression() { public void testRgbaColorExpression() {
String styleBlock = "::cue(#rgb){background-color: rgba(\n10/* Ugly color */,11\t, 12\n,.1);" String styleBlock = "::cue(#rgb){background-color: rgba(\n10/* Ugly color */,11\t, 12\n,.1);"
+ "color:rgb(1,1,\n1)}"; + "color:rgb(1,1,\n1)}";
// Expected style map construction.
Map<String, WebvttCssStyle> expectedResult = new HashMap<>();
WebvttCssStyle expectedStyle = new WebvttCssStyle(); WebvttCssStyle expectedStyle = new WebvttCssStyle();
expectedResult.put("#rgb", expectedStyle); expectedStyle.setTargetId("rgb");
expectedStyle.setBackgroundColor(0x190A0B0C); expectedStyle.setBackgroundColor(0x190A0B0C);
expectedStyle.setFontColor(0xFF010101); expectedStyle.setFontColor(0xFF010101);
assertCssProducesExpectedMap(expectedResult, new String[] { styleBlock }); assertParserProduces(expectedStyle, styleBlock);
} }
public void testGetNextToken() { public void testGetNextToken() {
...@@ -181,20 +149,20 @@ public final class CssParserTest extends InstrumentationTestCase { ...@@ -181,20 +149,20 @@ public final class CssParserTest extends InstrumentationTestCase {
public void testStyleScoreSystem() { public void testStyleScoreSystem() {
WebvttCssStyle style = new WebvttCssStyle(); WebvttCssStyle style = new WebvttCssStyle();
// Universal selector. // Universal selector.
assertEquals(1, style.getSpecificityScore(null, null, new String[0], null)); assertEquals(1, style.getSpecificityScore("", "", new String[0], ""));
// Class match without tag match. // Class match without tag match.
style.setTargetClasses(new String[] { "class1", "class2"}); style.setTargetClasses(new String[] { "class1", "class2"});
assertEquals(8, style.getSpecificityScore(null, null, assertEquals(8, style.getSpecificityScore("", "", new String[] { "class1", "class2", "class3" },
new String[] { "class1", "class2", "class3" }, null)); ""));
// Class and tag match // Class and tag match
style.setTargetTagName("b"); style.setTargetTagName("b");
assertEquals(10, style.getSpecificityScore(null, "b", assertEquals(10, style.getSpecificityScore("", "b",
new String[] { "class1", "class2", "class3" }, null)); new String[] { "class1", "class2", "class3" }, ""));
// Class insufficiency. // Class insufficiency.
assertEquals(0, style.getSpecificityScore(null, "b", new String[] { "class1", "class" }, null)); assertEquals(0, style.getSpecificityScore("", "b", new String[] { "class1", "class" }, ""));
// Voice, classes and tag match. // Voice, classes and tag match.
style.setTargetVoice("Manuel Cráneo"); style.setTargetVoice("Manuel Cráneo");
assertEquals(14, style.getSpecificityScore(null, "b", assertEquals(14, style.getSpecificityScore("", "b",
new String[] { "class1", "class2", "class3" }, "Manuel Cráneo")); new String[] { "class1", "class2", "class3" }, "Manuel Cráneo"));
// Voice mismatch. // Voice mismatch.
assertEquals(0, style.getSpecificityScore(null, "b", assertEquals(0, style.getSpecificityScore(null, "b",
...@@ -205,7 +173,7 @@ public final class CssParserTest extends InstrumentationTestCase { ...@@ -205,7 +173,7 @@ public final class CssParserTest extends InstrumentationTestCase {
new String[] { "class1", "class2", "class3" }, "Manuel Cráneo")); new String[] { "class1", "class2", "class3" }, "Manuel Cráneo"));
// Id mismatch. // Id mismatch.
assertEquals(0, style.getSpecificityScore("id1", "b", assertEquals(0, style.getSpecificityScore("id1", "b",
new String[] { "class1", "class2", "class3" }, null)); new String[] { "class1", "class2", "class3" }, ""));
} }
// Utility methods. // Utility methods.
...@@ -222,38 +190,25 @@ public final class CssParserTest extends InstrumentationTestCase { ...@@ -222,38 +190,25 @@ public final class CssParserTest extends InstrumentationTestCase {
assertEquals(expectedLine, input.readLine()); assertEquals(expectedLine, input.readLine());
} }
private void assertCssProducesExpectedMap(Map<String, WebvttCssStyle> expectedResult, private void assertParserProduces(WebvttCssStyle expected,
String[] styleBlocks){ String styleBlock){
Map<String, WebvttCssStyle> actualStyleMap = new HashMap<>(); ParsableByteArray input = new ParsableByteArray(styleBlock.getBytes());
for (String s : styleBlocks) { WebvttCssStyle actualElem = parser.parseBlock(input);
ParsableByteArray input = new ParsableByteArray(s.getBytes()); assertEquals(expected.hasBackgroundColor(), actualElem.hasBackgroundColor());
parser.parseBlock(input, actualStyleMap); if (expected.hasBackgroundColor()) {
} assertEquals(expected.getBackgroundColor(), actualElem.getBackgroundColor());
assertStyleMapsAreEqual(expectedResult, actualStyleMap); }
} assertEquals(expected.hasFontColor(), actualElem.hasFontColor());
if (expected.hasFontColor()) {
private void assertStyleMapsAreEqual(Map<String, WebvttCssStyle> expected, assertEquals(expected.getFontColor(), actualElem.getFontColor());
Map<String, WebvttCssStyle> actual) { }
assertEquals(expected.size(), actual.size()); assertEquals(expected.getFontFamily(), actualElem.getFontFamily());
for (String k : expected.keySet()) { assertEquals(expected.getFontSize(), actualElem.getFontSize());
WebvttCssStyle expectedElem = expected.get(k); assertEquals(expected.getFontSizeUnit(), actualElem.getFontSizeUnit());
WebvttCssStyle actualElem = actual.get(k); assertEquals(expected.getStyle(), actualElem.getStyle());
assertEquals(expectedElem.hasBackgroundColor(), actualElem.hasBackgroundColor()); assertEquals(expected.isLinethrough(), actualElem.isLinethrough());
if (expectedElem.hasBackgroundColor()) { assertEquals(expected.isUnderline(), actualElem.isUnderline());
assertEquals(expectedElem.getBackgroundColor(), actualElem.getBackgroundColor()); assertEquals(expected.getTextAlign(), actualElem.getTextAlign());
}
assertEquals(expectedElem.hasFontColor(), actualElem.hasFontColor());
if (expectedElem.hasFontColor()) {
assertEquals(expectedElem.getFontColor(), actualElem.getFontColor());
}
assertEquals(expectedElem.getFontFamily(), actualElem.getFontFamily());
assertEquals(expectedElem.getFontSize(), actualElem.getFontSize());
assertEquals(expectedElem.getFontSizeUnit(), actualElem.getFontSizeUnit());
assertEquals(expectedElem.getStyle(), actualElem.getStyle());
assertEquals(expectedElem.isLinethrough(), actualElem.isLinethrough());
assertEquals(expectedElem.isUnderline(), actualElem.isUnderline());
assertEquals(expectedElem.getTextAlign(), actualElem.getTextAlign());
}
} }
} }
...@@ -223,8 +223,7 @@ public final class WebvttCueParserTest extends InstrumentationTestCase { ...@@ -223,8 +223,7 @@ public final class WebvttCueParserTest extends InstrumentationTestCase {
private static Spanned parseCueText(String string) { private static Spanned parseCueText(String string) {
WebvttCue.Builder builder = new WebvttCue.Builder(); WebvttCue.Builder builder = new WebvttCue.Builder();
WebvttCueParser.parseCueText(null, string, builder, WebvttCueParser.parseCueText(null, string, builder, Collections.<WebvttCssStyle>emptyList());
Collections.<String, WebvttCssStyle>emptyMap());
return (Spanned) builder.build().text; return (Spanned) builder.build().text;
} }
......
...@@ -26,6 +26,7 @@ import android.text.Spanned; ...@@ -26,6 +26,7 @@ import android.text.Spanned;
import android.text.style.BackgroundColorSpan; import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan; import android.text.style.UnderlineSpan;
import java.io.IOException; import java.io.IOException;
...@@ -43,6 +44,7 @@ public class WebvttParserTest extends InstrumentationTestCase { ...@@ -43,6 +44,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
private static final String WITH_BAD_CUE_HEADER_FILE = "webvtt/with_bad_cue_header"; private static final String WITH_BAD_CUE_HEADER_FILE = "webvtt/with_bad_cue_header";
private static final String WITH_TAGS_FILE = "webvtt/with_tags"; private static final String WITH_TAGS_FILE = "webvtt/with_tags";
private static final String WITH_CSS_STYLES = "webvtt/with_css_styles"; private static final String WITH_CSS_STYLES = "webvtt/with_css_styles";
private static final String WITH_CSS_COMPLEX_SELECTORS = "webvtt/with_css_complex_selectors";
private static final String EMPTY_FILE = "webvtt/empty"; private static final String EMPTY_FILE = "webvtt/empty";
public void testParseEmpty() throws IOException { public void testParseEmpty() throws IOException {
...@@ -57,9 +59,7 @@ public class WebvttParserTest extends InstrumentationTestCase { ...@@ -57,9 +59,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
} }
public void testParseTypical() throws IOException { public void testParseTypical() throws IOException {
WebvttParser parser = new WebvttParser(); WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE);
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE);
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// Test event count. // Test event count.
assertEquals(4, subtitle.getEventTimeCount()); assertEquals(4, subtitle.getEventTimeCount());
...@@ -70,9 +70,7 @@ public class WebvttParserTest extends InstrumentationTestCase { ...@@ -70,9 +70,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
} }
public void testParseTypicalWithIds() throws IOException { public void testParseTypicalWithIds() throws IOException {
WebvttParser parser = new WebvttParser(); WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_IDS_FILE);
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_IDS_FILE);
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// Test event count. // Test event count.
assertEquals(4, subtitle.getEventTimeCount()); assertEquals(4, subtitle.getEventTimeCount());
...@@ -83,9 +81,7 @@ public class WebvttParserTest extends InstrumentationTestCase { ...@@ -83,9 +81,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
} }
public void testParseTypicalWithComments() throws IOException { public void testParseTypicalWithComments() throws IOException {
WebvttParser parser = new WebvttParser(); WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_COMMENTS_FILE);
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_COMMENTS_FILE);
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// test event count // test event count
assertEquals(4, subtitle.getEventTimeCount()); assertEquals(4, subtitle.getEventTimeCount());
...@@ -96,9 +92,7 @@ public class WebvttParserTest extends InstrumentationTestCase { ...@@ -96,9 +92,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
} }
public void testParseWithTags() throws IOException { public void testParseWithTags() throws IOException {
WebvttParser parser = new WebvttParser(); WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE);
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_TAGS_FILE);
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// Test event count. // Test event count.
assertEquals(8, subtitle.getEventTimeCount()); assertEquals(8, subtitle.getEventTimeCount());
...@@ -111,13 +105,9 @@ public class WebvttParserTest extends InstrumentationTestCase { ...@@ -111,13 +105,9 @@ public class WebvttParserTest extends InstrumentationTestCase {
} }
public void testParseWithPositioning() throws IOException { public void testParseWithPositioning() throws IOException {
WebvttParser parser = new WebvttParser(); WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE);
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_POSITIONING_FILE);
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// Test event count. // Test event count.
assertEquals(12, subtitle.getEventTimeCount()); assertEquals(12, subtitle.getEventTimeCount());
// Test cues. // Test cues.
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.", Alignment.ALIGN_NORMAL, assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.", Alignment.ALIGN_NORMAL,
Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f, Cue.ANCHOR_TYPE_START, 0.35f); Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f, Cue.ANCHOR_TYPE_START, 0.35f);
...@@ -139,9 +129,7 @@ public class WebvttParserTest extends InstrumentationTestCase { ...@@ -139,9 +129,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
} }
public void testParseWithBadCueHeader() throws IOException { public void testParseWithBadCueHeader() throws IOException {
WebvttParser parser = new WebvttParser(); WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BAD_CUE_HEADER_FILE);
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_BAD_CUE_HEADER_FILE);
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// Test event count. // Test event count.
assertEquals(4, subtitle.getEventTimeCount()); assertEquals(4, subtitle.getEventTimeCount());
...@@ -152,9 +140,7 @@ public class WebvttParserTest extends InstrumentationTestCase { ...@@ -152,9 +140,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
} }
public void testWebvttWithCssStyle() throws IOException { public void testWebvttWithCssStyle() throws IOException {
WebvttParser parser = new WebvttParser(); WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES);
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_CSS_STYLES);
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// Test event count. // Test event count.
assertEquals(8, subtitle.getEventTimeCount()); assertEquals(8, subtitle.getEventTimeCount());
...@@ -163,14 +149,10 @@ public class WebvttParserTest extends InstrumentationTestCase { ...@@ -163,14 +149,10 @@ public class WebvttParserTest extends InstrumentationTestCase {
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle."); assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle."); assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
Cue cue1 = subtitle.getCues(0).get(0); Spanned s1 = getUniqueSpanTextAt(subtitle, 0);
Cue cue2 = subtitle.getCues(2345000).get(0); Spanned s2 = getUniqueSpanTextAt(subtitle, 2345000);
Cue cue3 = subtitle.getCues(20000000).get(0); Spanned s3 = getUniqueSpanTextAt(subtitle, 20000000);
Cue cue4 = subtitle.getCues(25000000).get(0); Spanned s4 = getUniqueSpanTextAt(subtitle, 25000000);
Spanned s1 = (Spanned) cue1.text;
Spanned s2 = (Spanned) cue2.text;
Spanned s3 = (Spanned) cue3.text;
Spanned s4 = (Spanned) cue4.text;
assertEquals(1, s1.getSpans(0, s1.length(), ForegroundColorSpan.class).length); assertEquals(1, s1.getSpans(0, s1.length(), ForegroundColorSpan.class).length);
assertEquals(1, s1.getSpans(0, s1.length(), BackgroundColorSpan.class).length); assertEquals(1, s1.getSpans(0, s1.length(), BackgroundColorSpan.class).length);
assertEquals(2, s2.getSpans(0, s2.length(), ForegroundColorSpan.class).length); assertEquals(2, s2.getSpans(0, s2.length(), ForegroundColorSpan.class).length);
...@@ -180,6 +162,46 @@ public class WebvttParserTest extends InstrumentationTestCase { ...@@ -180,6 +162,46 @@ public class WebvttParserTest extends InstrumentationTestCase {
assertEquals(Typeface.BOLD, s4.getSpans(17, s4.length(), StyleSpan.class)[0].getStyle()); assertEquals(Typeface.BOLD, s4.getSpans(17, s4.length(), StyleSpan.class)[0].getStyle());
} }
public void testWithComplexCssSelectors() throws IOException {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS);
Spanned text = getUniqueSpanTextAt(subtitle, 0);
assertEquals(1, text.getSpans(30, text.length(), ForegroundColorSpan.class).length);
assertEquals(0xFFEE82EE,
text.getSpans(30, text.length(), ForegroundColorSpan.class)[0].getForegroundColor());
assertEquals(1, text.getSpans(30, text.length(), TypefaceSpan.class).length);
assertEquals("courier", text.getSpans(30, text.length(), TypefaceSpan.class)[0].getFamily());
text = getUniqueSpanTextAt(subtitle, 2000000);
assertEquals(1, text.getSpans(5, text.length(), TypefaceSpan.class).length);
assertEquals("courier", text.getSpans(5, text.length(), TypefaceSpan.class)[0].getFamily());
text = getUniqueSpanTextAt(subtitle, 2500000);
assertEquals(1, text.getSpans(5, text.length(), StyleSpan.class).length);
assertEquals(Typeface.BOLD, text.getSpans(5, text.length(), StyleSpan.class)[0].getStyle());
assertEquals(1, text.getSpans(5, text.length(), TypefaceSpan.class).length);
assertEquals("courier", text.getSpans(5, text.length(), TypefaceSpan.class)[0].getFamily());
text = getUniqueSpanTextAt(subtitle, 4000000);
assertEquals(0, text.getSpans(6, 22, StyleSpan.class).length);
assertEquals(1, text.getSpans(30, text.length(), StyleSpan.class).length);
assertEquals(Typeface.BOLD, text.getSpans(30, text.length(), StyleSpan.class)[0].getStyle());
text = getUniqueSpanTextAt(subtitle, 5000000);
assertEquals(0, text.getSpans(9, 17, StyleSpan.class).length);
assertEquals(1, text.getSpans(19, text.length(), StyleSpan.class).length);
assertEquals(Typeface.ITALIC, text.getSpans(19, text.length(), StyleSpan.class)[0].getStyle());
}
private WebvttSubtitle getSubtitleForTestAsset(String asset) throws IOException {
WebvttParser parser = new WebvttParser();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), asset);
return parser.decode(bytes, bytes.length);
}
private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) {
return (Spanned) sub.getCues(timeUs).get(0).text;
}
private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs, private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs,
int endTimeUs, String text) { int endTimeUs, String text) {
assertCue(subtitle, eventTimeIndex, startTimeUs, endTimeUs, text, null, Cue.DIMEN_UNSET, assertCue(subtitle, eventTimeIndex, startTimeUs, endTimeUs, text, null, Cue.DIMEN_UNSET,
......
...@@ -20,7 +20,9 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -20,7 +20,9 @@ import com.google.android.exoplayer.util.ParsableByteArray;
import android.text.TextUtils; import android.text.TextUtils;
import java.util.Map; import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* Provides a CSS parser for STYLE blocks in Webvtt files. Supports only a subset of the CSS * Provides a CSS parser for STYLE blocks in Webvtt files. Supports only a subset of the CSS
...@@ -32,9 +34,14 @@ import java.util.Map; ...@@ -32,9 +34,14 @@ import java.util.Map;
private static final String PROPERTY_FONT_FAMILY = "font-family"; private static final String PROPERTY_FONT_FAMILY = "font-family";
private static final String PROPERTY_FONT_WEIGHT = "font-weight"; private static final String PROPERTY_FONT_WEIGHT = "font-weight";
private static final String PROPERTY_TEXT_DECORATION = "text-decoration"; private static final String PROPERTY_TEXT_DECORATION = "text-decoration";
private static final String VALUE_BOLD = "bold"; private static final String VALUE_BOLD = "bold";
private static final String VALUE_UNDERLINE = "underline"; private static final String VALUE_UNDERLINE = "underline";
private static final String BLOCK_START = "{";
private static final String BLOCK_END = "}";
private static final String PROPERTY_FONT_STYLE = "font-style";
private static final String VALUE_ITALIC = "italic";
private static final Pattern VOICE_NAME_PATTERN = Pattern.compile("\\[voice=\"([^\"]*)\"\\]");
// Temporary utility data structures. // Temporary utility data structures.
private final ParsableByteArray styleInput; private final ParsableByteArray styleInput;
...@@ -51,57 +58,41 @@ import java.util.Map; ...@@ -51,57 +58,41 @@ import java.util.Map;
* {@code null} otherwise. * {@code null} otherwise.
* *
* @param input The input from which the style block should be read. * @param input The input from which the style block should be read.
* @param styleMap The map that contains styles accessible by selector. * @return A {@link WebvttCssStyle} that represents the parsed block.
*/ */
public void parseBlock(ParsableByteArray input, Map<String, WebvttCssStyle> styleMap) { public WebvttCssStyle parseBlock(ParsableByteArray input) {
stringBuilder.setLength(0); stringBuilder.setLength(0);
int initialInputPosition = input.getPosition(); int initialInputPosition = input.getPosition();
skipStyleBlock(input); skipStyleBlock(input);
styleInput.reset(input.data, input.getPosition()); styleInput.reset(input.data, input.getPosition());
styleInput.setPosition(initialInputPosition); styleInput.setPosition(initialInputPosition);
String selector = parseSelector(styleInput, stringBuilder); String selector = parseSelector(styleInput, stringBuilder);
if (selector == null) { if (selector == null || !BLOCK_START.equals(parseNextToken(styleInput, stringBuilder))) {
return; return null;
}
String token = parseNextToken(styleInput, stringBuilder);
if (!"{".equals(token)) {
return;
}
if (!styleMap.containsKey(selector)) {
styleMap.put(selector, new WebvttCssStyle());
} }
WebvttCssStyle style = styleMap.get(selector); WebvttCssStyle style = new WebvttCssStyle();
applySelectorToStyle(style, selector);
String token = null;
boolean blockEndFound = false; boolean blockEndFound = false;
while (!blockEndFound) { while (!blockEndFound) {
int position = styleInput.getPosition(); int position = styleInput.getPosition();
token = parseNextToken(styleInput, stringBuilder); token = parseNextToken(styleInput, stringBuilder);
if (token == null || "}".equals(token)) { blockEndFound = token == null || BLOCK_END.equals(token);
blockEndFound = true; if (!blockEndFound) {
} else {
styleInput.setPosition(position); styleInput.setPosition(position);
parseStyleDeclaration(styleInput, style, stringBuilder); parseStyleDeclaration(styleInput, style, stringBuilder);
} }
} }
// Only one style block may appear after a STYLE line. return BLOCK_END.equals(token) ? style : null; // Check that the style block ended correctly.
} }
/** /**
* Returns a string containing the selector. {@link WebvttCueParser#UNIVERSAL_CUE_ID} is the * Returns a string containing the selector. The input is expected to have the form
* universal selector, and null means syntax error. * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional.
*
* <p>Expected inputs are:
* <ul>
* <li>::cue
* <li>::cue(#id)
* <li>::cue(elem)
* <li>::cue(.class)
* <li>::cue(elem.class)
* <li>::cue(v[voice="Someone"])
* </ul>
* *
* @param input From which the selector is obtained. * @param input From which the selector is obtained.
* @return A string containing the target, {@link WebvttCueParser#UNIVERSAL_CUE_ID} if the * @return A string containing the target, empty string if the selector is universal
* selector is universal (targets all cues) or null if an error was encountered. * (targets all cues) or null if an error was encountered.
*/ */
private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) { private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) {
skipWhitespaceAndComments(input); skipWhitespaceAndComments(input);
...@@ -117,9 +108,9 @@ import java.util.Map; ...@@ -117,9 +108,9 @@ import java.util.Map;
if (token == null) { if (token == null) {
return null; return null;
} }
if ("{".equals(token)) { if (BLOCK_START.equals(token)) {
input.setPosition(position); input.setPosition(position);
return WebvttCueParser.UNIVERSAL_CUE_ID; return "";
} }
String target = null; String target = null;
if ("(".equals(token)) { if ("(".equals(token)) {
...@@ -166,7 +157,7 @@ import java.util.Map; ...@@ -166,7 +157,7 @@ import java.util.Map;
String token = parseNextToken(input, stringBuilder); String token = parseNextToken(input, stringBuilder);
if (";".equals(token)) { if (";".equals(token)) {
// The style declaration is well formed. // The style declaration is well formed.
} else if ("}".equals(token)) { } else if (BLOCK_END.equals(token)) {
// The style declaration is well formed and we can go on, but the closing bracket had to be // The style declaration is well formed and we can go on, but the closing bracket had to be
// fed back. // fed back.
input.setPosition(position); input.setPosition(position);
...@@ -189,6 +180,10 @@ import java.util.Map; ...@@ -189,6 +180,10 @@ import java.util.Map;
if (VALUE_BOLD.equals(value)) { if (VALUE_BOLD.equals(value)) {
style.setBold(true); style.setBold(true);
} }
} else if (PROPERTY_FONT_STYLE.equals(property)) {
if (VALUE_ITALIC.equals(value)) {
style.setItalic(true);
}
} }
// TODO: Fill remaining supported styles. // TODO: Fill remaining supported styles.
} }
...@@ -256,7 +251,7 @@ import java.util.Map; ...@@ -256,7 +251,7 @@ import java.util.Map;
// Syntax error. // Syntax error.
return null; return null;
} }
if ("}".equals(token) || ";".equals(token)) { if (BLOCK_END.equals(token) || ";".equals(token)) {
input.setPosition(position); input.setPosition(position);
expressionEndFound = true; expressionEndFound = true;
} else { } else {
...@@ -305,5 +300,34 @@ import java.util.Map; ...@@ -305,5 +300,34 @@ import java.util.Map;
return stringBuilder.toString(); return stringBuilder.toString();
} }
} /**
* Sets the target of a {@link WebvttCssStyle} by splitting a selector of the form
* {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional.
*/
private void applySelectorToStyle(WebvttCssStyle style, String selector) {
if ("".equals(selector)) {
return; // Universal selector.
}
int voiceStartPosition = selector.indexOf('[');
if (voiceStartPosition != -1) {
Matcher matcher = VOICE_NAME_PATTERN.matcher(selector.substring(voiceStartPosition));
if (matcher.matches()) {
style.setTargetVoice(matcher.group(1));
}
selector = selector.substring(0, voiceStartPosition);
}
String[] classDivision = selector.split("\\.");
String tagAndIdDivision = classDivision[0];
int idPrefixPosition = tagAndIdDivision.indexOf('#');
if (idPrefixPosition != -1) {
style.setTargetTagName(tagAndIdDivision.substring(0, idPrefixPosition));
style.setTargetId(tagAndIdDivision.substring(idPrefixPosition + 1)); // We discard the '#'.
} else {
style.setTargetTagName(tagAndIdDivision);
}
if (classDivision.length > 1) {
style.setTargetClasses(Arrays.copyOfRange(classDivision, 1, classDivision.length));
}
}
}
...@@ -84,7 +84,7 @@ public final class Mp4WebvttParser extends SubtitleParser { ...@@ -84,7 +84,7 @@ public final class Mp4WebvttParser extends SubtitleParser {
WebvttCueParser.parseCueSettingsList(boxPayload, builder); WebvttCueParser.parseCueSettingsList(boxPayload, builder);
} else if (boxType == TYPE_payl) { } else if (boxType == TYPE_payl) {
WebvttCueParser.parseCueText(null, boxPayload.trim(), builder, WebvttCueParser.parseCueText(null, boxPayload.trim(), builder,
Collections.<String, WebvttCssStyle>emptyMap()); Collections.<WebvttCssStyle>emptyList());
} else { } else {
// Other VTTCueBox children are still not supported and are ignored. // Other VTTCueBox children are still not supported and are ignored.
} }
......
...@@ -124,8 +124,9 @@ import java.util.List; ...@@ -124,8 +124,9 @@ import java.util.List;
public int getSpecificityScore(String id, String tag, String[] classes, String voice) { public int getSpecificityScore(String id, String tag, String[] classes, String voice) {
if (targetId.isEmpty() && targetTag.isEmpty() && targetClasses.isEmpty() if (targetId.isEmpty() && targetTag.isEmpty() && targetClasses.isEmpty()
&& targetVoice.isEmpty()) { && targetVoice.isEmpty()) {
// The selector is universal. It matches with the minimum score. // The selector is universal. It matches with the minimum score if and only if the given
return 1; // element is a whole cue.
return tag.isEmpty() ? 1 : 0;
} }
int score = 0; int score = 0;
score = updateScoreForMatch(score, targetId, id, 0x40000000); score = updateScoreForMatch(score, targetId, id, 0x40000000);
......
...@@ -22,7 +22,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -22,7 +22,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
import android.text.TextUtils; import android.text.TextUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.List;
/** /**
* A simple WebVTT parser. * A simple WebVTT parser.
...@@ -43,14 +43,14 @@ public final class WebvttParser extends SubtitleParser { ...@@ -43,14 +43,14 @@ public final class WebvttParser extends SubtitleParser {
private final ParsableByteArray parsableWebvttData; private final ParsableByteArray parsableWebvttData;
private final WebvttCue.Builder webvttCueBuilder; private final WebvttCue.Builder webvttCueBuilder;
private final CssParser cssParser; private final CssParser cssParser;
private final HashMap<String, WebvttCssStyle> styleMap; private final List<WebvttCssStyle> definedStyles;
public WebvttParser() { public WebvttParser() {
cueParser = new WebvttCueParser(); cueParser = new WebvttCueParser();
parsableWebvttData = new ParsableByteArray(); parsableWebvttData = new ParsableByteArray();
webvttCueBuilder = new WebvttCue.Builder(); webvttCueBuilder = new WebvttCue.Builder();
cssParser = new CssParser(); cssParser = new CssParser();
styleMap = new HashMap<>(); definedStyles = new ArrayList<>();
} }
@Override @Override
...@@ -58,7 +58,7 @@ public final class WebvttParser extends SubtitleParser { ...@@ -58,7 +58,7 @@ public final class WebvttParser extends SubtitleParser {
parsableWebvttData.reset(bytes, length); parsableWebvttData.reset(bytes, length);
// Initialization for consistent starting state. // Initialization for consistent starting state.
webvttCueBuilder.reset(); webvttCueBuilder.reset();
styleMap.clear(); definedStyles.clear();
// Validate the first line of the header, and skip the remainder. // Validate the first line of the header, and skip the remainder.
WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData); WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData);
...@@ -74,9 +74,12 @@ public final class WebvttParser extends SubtitleParser { ...@@ -74,9 +74,12 @@ public final class WebvttParser extends SubtitleParser {
throw new ParserException("A style block was found after the first cue."); throw new ParserException("A style block was found after the first cue.");
} }
parsableWebvttData.readLine(); // Consume the "STYLE" header. parsableWebvttData.readLine(); // Consume the "STYLE" header.
cssParser.parseBlock(parsableWebvttData, styleMap); WebvttCssStyle styleBlock = cssParser.parseBlock(parsableWebvttData);
if (styleBlock != null) {
definedStyles.add(styleBlock);
}
} else if (eventFound == CUE_FOUND) { } else if (eventFound == CUE_FOUND) {
if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, styleMap)) { if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) {
subtitles.add(webvttCueBuilder.build()); subtitles.add(webvttCueBuilder.build());
webvttCueBuilder.reset(); webvttCueBuilder.reset();
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment