Skip to content

Commit 2d03157

Browse files
Preserve WebVTT tags in subtitle lines (asticode#97)
* [webvtt] Preserve WebVTT tags * [webvtt/test] Testcase for webvtt tags --------- Co-authored-by: Nhan Nguyen <nhan.trong.nguyen@edgeware.tv>
1 parent 2c5a2cc commit 2d03157

File tree

4 files changed

+140
-38
lines changed

4 files changed

+140
-38
lines changed

subtitles.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,19 +236,49 @@ type StyleAttributes struct {
236236
TTMLWritingMode *string
237237
TTMLZIndex *int
238238
WebVTTAlign string
239-
WebVTTItalics bool
240239
WebVTTLine string
241240
WebVTTLines int
242241
WebVTTPosition string
243242
WebVTTRegionAnchor string
244243
WebVTTScroll string
245244
WebVTTSize string
246245
WebVTTStyles []string
246+
WebVTTTags []WebVTTTag
247247
WebVTTVertical string
248248
WebVTTViewportAnchor string
249249
WebVTTWidth string
250250
}
251251

252+
type WebVTTTag struct {
253+
Name string
254+
Annotation string
255+
Classes []string
256+
}
257+
258+
func (t WebVTTTag) startTag() string {
259+
if t.Name == "" {
260+
return ""
261+
}
262+
263+
s := t.Name
264+
if len(t.Classes) > 0 {
265+
s += "." + strings.Join(t.Classes, ".")
266+
}
267+
268+
if t.Annotation != "" {
269+
s += " " + t.Annotation
270+
}
271+
272+
return "<" + s + ">"
273+
}
274+
275+
func (t WebVTTTag) endTag() string {
276+
if t.Name == "" {
277+
return ""
278+
}
279+
return "</" + t.Name + ">"
280+
}
281+
252282
func (sa *StyleAttributes) propagateSSAAttributes() {}
253283

254284
func (sa *StyleAttributes) propagateSTLAttributes() {

webvtt.go

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ package astisub
22

33
import (
44
"bufio"
5-
"bytes"
65
"errors"
76
"fmt"
87
"io"
8+
"log"
99
"regexp"
1010
"sort"
1111
"strconv"
@@ -33,7 +33,7 @@ var (
3333
bytesWebVTTItalicEndTag = []byte("</i>")
3434
bytesWebVTTItalicStartTag = []byte("<i>")
3535
bytesWebVTTTimeBoundariesSeparator = []byte(webvttTimeBoundariesSeparator)
36-
webVTTRegexpStartTag = regexp.MustCompile(`(<v([\.\w]*)(.+?)>)`)
36+
webVTTRegexpTag = regexp.MustCompile(`(</*\s*([^\.\s]+)(\.[^\s/]*)*\s*([^/]*)\s*/*>)`)
3737
webVTTEscaper = strings.NewReplacer("&", "&amp;", "<", "&lt;")
3838
webVTTUnescaper = strings.NewReplacer("&amp;", "&", "&lt;", "<")
3939
)
@@ -306,45 +306,65 @@ func parseTextWebVTT(i string) (o Line) {
306306
// Create tokenizer
307307
tr := html.NewTokenizer(strings.NewReader(i))
308308

309+
webVTTTagStack := make([]WebVTTTag, 0, 16)
310+
309311
// Loop
310-
italic := false
311312
for {
312313
// Get next tag
313314
t := tr.Next()
314-
315315
// Process error
316316
if err := tr.Err(); err != nil {
317317
break
318318
}
319319

320320
switch t {
321321
case html.EndTagToken:
322-
// Parse italic
323-
if bytes.Equal(tr.Raw(), bytesWebVTTItalicEndTag) {
324-
italic = false
325-
continue
322+
// Pop the top of stack if we meet end tag
323+
if len(webVTTTagStack) > 0 {
324+
webVTTTagStack = webVTTTagStack[:len(webVTTTagStack)-1]
326325
}
327326
case html.StartTagToken:
328-
// Parse voice name
329-
if matches := webVTTRegexpStartTag.FindStringSubmatch(string(tr.Raw())); len(matches) > 3 {
330-
if s := strings.TrimSpace(matches[3]); s != "" {
331-
o.VoiceName = s
327+
if matches := webVTTRegexpTag.FindStringSubmatch(string(tr.Raw())); len(matches) > 4 {
328+
tagName := matches[2]
329+
330+
var classes []string
331+
if matches[3] != "" {
332+
classes = strings.Split(strings.Trim(matches[3], "."), ".")
333+
}
334+
335+
annotation := ""
336+
if matches[4] != "" {
337+
annotation = strings.TrimSpace(matches[4])
338+
}
339+
340+
if tagName == "v" {
341+
if o.VoiceName == "" {
342+
// Only get voicename of the first <v> appears in the line
343+
o.VoiceName = annotation
344+
} else {
345+
// TODO: do something with other <v> instead of ignoring
346+
log.Printf("astisub: found another voice name %q in %q. Ignore", annotation, i)
347+
}
348+
continue
332349
}
333-
continue
334-
}
335350

336-
// Parse italic
337-
if bytes.Equal(tr.Raw(), bytesWebVTTItalicStartTag) {
338-
italic = true
339-
continue
351+
// Push the tag to stack
352+
webVTTTagStack = append(webVTTTagStack, WebVTTTag{
353+
Name: tagName,
354+
Classes: classes,
355+
Annotation: annotation,
356+
})
340357
}
358+
341359
case html.TextToken:
342360
if s := strings.TrimSpace(string(tr.Raw())); s != "" {
343361
// Get style attribute
344362
var sa *StyleAttributes
345-
if italic {
363+
if len(webVTTTagStack) > 0 {
364+
tags := make([]WebVTTTag, len(webVTTTagStack))
365+
copy(tags, webVTTTagStack)
346366
sa = &StyleAttributes{
347-
WebVTTItalics: italic,
367+
WebVTTTags: tags,
348368
}
349369
sa.propagateWebVTTAttributes()
350370
}
@@ -545,19 +565,21 @@ func (li LineItem) webVTTBytes() (c []byte) {
545565
color = cssColor(*li.InlineStyle.TTMLColor)
546566
}
547567

548-
// Get italics
549-
i := li.InlineStyle != nil && li.InlineStyle.WebVTTItalics
550-
551568
// Append
552569
if color != "" {
553570
c = append(c, []byte("<c."+color+">")...)
554571
}
555-
if i {
556-
c = append(c, []byte("<i>")...)
572+
if li.InlineStyle != nil {
573+
for _, tag := range li.InlineStyle.WebVTTTags {
574+
c = append(c, []byte(tag.startTag())...)
575+
}
557576
}
558577
c = append(c, []byte(escapeWebVTT(li.Text))...)
559-
if i {
560-
c = append(c, []byte("</i>")...)
578+
if li.InlineStyle != nil {
579+
noTags := len(li.InlineStyle.WebVTTTags)
580+
for i := noTags - 1; i >= 0; i-- {
581+
c = append(c, []byte(li.InlineStyle.WebVTTTags[i].endTag())...)
582+
}
561583
}
562584
if color != "" {
563585
c = append(c, []byte("</c>")...)

webvtt_internal_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,39 +96,39 @@ func TestCueVoiceSpanRegex(t *testing.T) {
9696
}{
9797
{
9898
give: `<v 中文> this is the content</v>`,
99-
want: ` 中文`,
99+
want: `中文`,
100100
},
101101
{
102102
give: `<v 中文> this is the content`,
103-
want: ` 中文`,
103+
want: `中文`,
104104
},
105105
{
106106
give: `<v.abc 中文> this is the content</v>`,
107-
want: ` 中文`,
107+
want: `中文`,
108108
},
109109
{
110110
give: `<v.jp 言語の> this is the content`,
111-
want: ` 言語の`,
111+
want: `言語の`,
112112
},
113113
{
114114
give: `<v.ko 언어> this is the content`,
115-
want: ` 언어`,
115+
want: `언어`,
116116
},
117117
{
118118
give: `<v foo bar> this is the content`,
119-
want: ` foo bar`,
119+
want: `foo bar`,
120120
},
121121
{
122122
give: `<v هذا عربي> this is the content`,
123-
want: ` هذا عربي`,
123+
want: `هذا عربي`,
124124
},
125125
}
126126

127127
for _, tt := range tests {
128128
t.Run(tt.want, func(t *testing.T) {
129-
results := webVTTRegexpStartTag.FindStringSubmatch(tt.give)
130-
assert.True(t, len(results) == 4)
131-
assert.Equal(t, tt.want, results[3])
129+
results := webVTTRegexpTag.FindStringSubmatch(tt.give)
130+
assert.True(t, len(results) == 5)
131+
assert.Equal(t, tt.want, results[4])
132132
})
133133
}
134134
}

webvtt_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,53 @@ Sentence with an &amp; in the middle
162162
Sentence with an &lt; in the middle
163163
`, b.String())
164164
}
165+
166+
func TestWebVTTTags(t *testing.T) {
167+
testData := `WEBVTT
168+
169+
00:01:00.000 --> 00:02:00.000
170+
<u><i>Italic with underline text</i></u> some extra
171+
172+
00:02:00.000 --> 00:03:00.000
173+
<lang en>English here</lang> <c.yellow.bg_blue>Yellow text on blue background</c>
174+
175+
00:03:00.000 --> 00:04:00.000
176+
<v Joe><c.red><i>Joe's words are red in italic</i></c>
177+
178+
00:04:00.000 --> 00:05:00.000
179+
<customed_tag.class1.class2>Text here</customed_tag>
180+
181+
00:05:00.000 --> 00:06:00.000
182+
<v Joe>Joe says something</v> <v Bob>Bob says something</v>`
183+
184+
s, err := astisub.ReadFromWebVTT(strings.NewReader(testData))
185+
require.NoError(t, err)
186+
187+
require.Len(t, s.Items, 5)
188+
189+
b := &bytes.Buffer{}
190+
err = s.WriteToWebVTT(b)
191+
require.NoError(t, err)
192+
require.Equal(t, `WEBVTT
193+
194+
1
195+
00:01:00.000 --> 00:02:00.000
196+
<u><i>Italic with underline text</i></u> some extra
197+
198+
2
199+
00:02:00.000 --> 00:03:00.000
200+
<lang en>English here</lang> <c.yellow.bg_blue>Yellow text on blue background</c>
201+
202+
3
203+
00:03:00.000 --> 00:04:00.000
204+
<v Joe><c.red><i>Joe's words are red in italic</i></c>
205+
206+
4
207+
00:04:00.000 --> 00:05:00.000
208+
<customed_tag.class1.class2>Text here</customed_tag>
209+
210+
5
211+
00:05:00.000 --> 00:06:00.000
212+
<v Joe>Joe says something Bob says something
213+
`, b.String())
214+
}

0 commit comments

Comments
 (0)