diff --git a/subtitles.go b/subtitles.go index 7219e8f..97fb1c7 100644 --- a/subtitles.go +++ b/subtitles.go @@ -236,7 +236,6 @@ type StyleAttributes struct { TTMLWritingMode *string TTMLZIndex *int WebVTTAlign string - WebVTTItalics bool WebVTTLine string WebVTTLines int WebVTTPosition string @@ -244,11 +243,42 @@ type StyleAttributes struct { WebVTTScroll string WebVTTSize string WebVTTStyles []string + WebVTTTags []WebVTTTag WebVTTVertical string WebVTTViewportAnchor string WebVTTWidth string } +type WebVTTTag struct { + Name string + Annotation string + Classes []string +} + +func (t WebVTTTag) startTag() string { + if t.Name == "" { + return "" + } + + s := t.Name + if len(t.Classes) > 0 { + s += "." + strings.Join(t.Classes, ".") + } + + if t.Annotation != "" { + s += " " + t.Annotation + } + + return "<" + s + ">" +} + +func (t WebVTTTag) endTag() string { + if t.Name == "" { + return "" + } + return "" +} + func (sa *StyleAttributes) propagateSSAAttributes() {} func (sa *StyleAttributes) propagateSTLAttributes() { diff --git a/webvtt.go b/webvtt.go index 11133c3..3b1f5e4 100644 --- a/webvtt.go +++ b/webvtt.go @@ -2,10 +2,10 @@ package astisub import ( "bufio" - "bytes" "errors" "fmt" "io" + "log" "regexp" "sort" "strconv" @@ -33,7 +33,7 @@ var ( bytesWebVTTItalicEndTag = []byte("") bytesWebVTTItalicStartTag = []byte("") bytesWebVTTTimeBoundariesSeparator = []byte(webvttTimeBoundariesSeparator) - webVTTRegexpStartTag = regexp.MustCompile(`()`) + webVTTRegexpTag = regexp.MustCompile(`()`) webVTTEscaper = strings.NewReplacer("&", "&", "<", "<") webVTTUnescaper = strings.NewReplacer("&", "&", "<", "<") ) @@ -306,12 +306,12 @@ func parseTextWebVTT(i string) (o Line) { // Create tokenizer tr := html.NewTokenizer(strings.NewReader(i)) + webVTTTagStack := make([]WebVTTTag, 0, 16) + // Loop - italic := false for { // Get next tag t := tr.Next() - // Process error if err := tr.Err(); err != nil { break @@ -319,32 +319,52 @@ func parseTextWebVTT(i string) (o Line) { switch t { case html.EndTagToken: - // Parse italic - if bytes.Equal(tr.Raw(), bytesWebVTTItalicEndTag) { - italic = false - continue + // Pop the top of stack if we meet end tag + if len(webVTTTagStack) > 0 { + webVTTTagStack = webVTTTagStack[:len(webVTTTagStack)-1] } case html.StartTagToken: - // Parse voice name - if matches := webVTTRegexpStartTag.FindStringSubmatch(string(tr.Raw())); len(matches) > 3 { - if s := strings.TrimSpace(matches[3]); s != "" { - o.VoiceName = s + if matches := webVTTRegexpTag.FindStringSubmatch(string(tr.Raw())); len(matches) > 4 { + tagName := matches[2] + + var classes []string + if matches[3] != "" { + classes = strings.Split(strings.Trim(matches[3], "."), ".") + } + + annotation := "" + if matches[4] != "" { + annotation = strings.TrimSpace(matches[4]) + } + + if tagName == "v" { + if o.VoiceName == "" { + // Only get voicename of the first appears in the line + o.VoiceName = annotation + } else { + // TODO: do something with other instead of ignoring + log.Printf("astisub: found another voice name %q in %q. Ignore", annotation, i) + } + continue } - continue - } - // Parse italic - if bytes.Equal(tr.Raw(), bytesWebVTTItalicStartTag) { - italic = true - continue + // Push the tag to stack + webVTTTagStack = append(webVTTTagStack, WebVTTTag{ + Name: tagName, + Classes: classes, + Annotation: annotation, + }) } + case html.TextToken: if s := strings.TrimSpace(string(tr.Raw())); s != "" { // Get style attribute var sa *StyleAttributes - if italic { + if len(webVTTTagStack) > 0 { + tags := make([]WebVTTTag, len(webVTTTagStack)) + copy(tags, webVTTTagStack) sa = &StyleAttributes{ - WebVTTItalics: italic, + WebVTTTags: tags, } sa.propagateWebVTTAttributes() } @@ -545,19 +565,21 @@ func (li LineItem) webVTTBytes() (c []byte) { color = cssColor(*li.InlineStyle.TTMLColor) } - // Get italics - i := li.InlineStyle != nil && li.InlineStyle.WebVTTItalics - // Append if color != "" { c = append(c, []byte("")...) } - if i { - c = append(c, []byte("")...) + if li.InlineStyle != nil { + for _, tag := range li.InlineStyle.WebVTTTags { + c = append(c, []byte(tag.startTag())...) + } } c = append(c, []byte(escapeWebVTT(li.Text))...) - if i { - c = append(c, []byte("")...) + if li.InlineStyle != nil { + noTags := len(li.InlineStyle.WebVTTTags) + for i := noTags - 1; i >= 0; i-- { + c = append(c, []byte(li.InlineStyle.WebVTTTags[i].endTag())...) + } } if color != "" { c = append(c, []byte("")...) diff --git a/webvtt_internal_test.go b/webvtt_internal_test.go index adf44df..ce97faa 100644 --- a/webvtt_internal_test.go +++ b/webvtt_internal_test.go @@ -96,39 +96,39 @@ func TestCueVoiceSpanRegex(t *testing.T) { }{ { give: ` this is the content`, - want: ` 中文`, + want: `中文`, }, { give: ` this is the content`, - want: ` 中文`, + want: `中文`, }, { give: ` this is the content`, - want: ` 中文`, + want: `中文`, }, { give: ` this is the content`, - want: ` 言語の`, + want: `言語の`, }, { give: ` this is the content`, - want: ` 언어`, + want: `언어`, }, { give: ` this is the content`, - want: ` foo bar`, + want: `foo bar`, }, { give: ` this is the content`, - want: ` هذا عربي`, + want: `هذا عربي`, }, } for _, tt := range tests { t.Run(tt.want, func(t *testing.T) { - results := webVTTRegexpStartTag.FindStringSubmatch(tt.give) - assert.True(t, len(results) == 4) - assert.Equal(t, tt.want, results[3]) + results := webVTTRegexpTag.FindStringSubmatch(tt.give) + assert.True(t, len(results) == 5) + assert.Equal(t, tt.want, results[4]) }) } } diff --git a/webvtt_test.go b/webvtt_test.go index 9e5b5d3..03993dd 100644 --- a/webvtt_test.go +++ b/webvtt_test.go @@ -162,3 +162,53 @@ Sentence with an & in the middle Sentence with an < in the middle `, b.String()) } + +func TestWebVTTTags(t *testing.T) { + testData := `WEBVTT + + 00:01:00.000 --> 00:02:00.000 + Italic with underline text some extra + + 00:02:00.000 --> 00:03:00.000 + English here Yellow text on blue background + + 00:03:00.000 --> 00:04:00.000 + Joe's words are red in italic + + 00:04:00.000 --> 00:05:00.000 + Text here + + 00:05:00.000 --> 00:06:00.000 + Joe says something Bob says something` + + s, err := astisub.ReadFromWebVTT(strings.NewReader(testData)) + require.NoError(t, err) + + require.Len(t, s.Items, 5) + + b := &bytes.Buffer{} + err = s.WriteToWebVTT(b) + require.NoError(t, err) + require.Equal(t, `WEBVTT + +1 +00:01:00.000 --> 00:02:00.000 +Italic with underline text some extra + +2 +00:02:00.000 --> 00:03:00.000 +English here Yellow text on blue background + +3 +00:03:00.000 --> 00:04:00.000 +Joe's words are red in italic + +4 +00:04:00.000 --> 00:05:00.000 +Text here + +5 +00:05:00.000 --> 00:06:00.000 +Joe says something Bob says something +`, b.String()) +}