package astisub import ( "context" "io" "math/bits" "sort" "strings" "time" "github.com/asticode/go-astilog" "github.com/asticode/go-astitools/bits" "github.com/asticode/go-astitools/ptr" "github.com/asticode/go-astits" "github.com/pkg/errors" ) // Errors var ( ErrNoValidTeletextPID = errors.New("astisub: no valid teletext PID") ) type teletextCharset [96][]byte type teletextNationalSubset [13][]byte // Chapter: 15.2 | Page: 109 | Link: http://www.etsi.org/deliver/etsi_i_ets/300700_300799/300706/01_60/ets_300706e01p.pdf // It is indexed by triplet1 then by national option subset code var teletextCharsets = map[uint8]map[uint8]struct { g0 *teletextCharset g2 *teletextCharset national *teletextNationalSubset }{ 0: { 0: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetEnglish}, 1: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetFrench}, 2: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetSwedishFinnishHungarian}, 3: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetCzechSlovak}, 4: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetGerman}, 5: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetPortugueseSpanish}, 6: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetItalian}, 7: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin}, }, 1: { 0: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetPolish}, 1: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetFrench}, 2: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetSwedishFinnishHungarian}, 3: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetCzechSlovak}, 4: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetGerman}, 5: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin}, 6: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetItalian}, 7: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin}, }, 2: { 0: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetEnglish}, 1: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetFrench}, 2: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetSwedishFinnishHungarian}, 3: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetCzechSlovak}, 4: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetGerman}, 5: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetPortugueseSpanish}, 6: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetItalian}, 7: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin}, }, 3: { 0: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin}, 1: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin}, 2: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin}, 3: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin}, 4: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin}, 5: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetSerbianCroatianSlovenian}, 6: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin}, 7: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetRomanian}, }, 4: { 0: {g0: teletextCharsetG0CyrillicOption1, g2: teletextCharsetG2Cyrillic}, 1: {g0: teletextCharsetG0CyrillicOption2, g2: teletextCharsetG2Cyrillic}, 2: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetEstonian}, 3: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetCzechSlovak}, 4: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetGerman}, 5: {g0: teletextCharsetG0CyrillicOption3, g2: teletextCharsetG2Cyrillic}, 6: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetLettishLithuanian}, }, 6: { 3: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Latin, national: teletextNationalSubsetTurkish}, 7: {g0: teletextCharsetG0Greek, g2: teletextCharsetG2Greek}, }, 8: { 0: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Arabic, national: teletextNationalSubsetEnglish}, 1: {g0: teletextCharsetG0Latin, g2: teletextCharsetG2Arabic, national: teletextNationalSubsetFrench}, 7: {g0: teletextCharsetG0Arabic, g2: teletextCharsetG2Arabic}, }, 10: { 5: {g0: teletextCharsetG0Hebrew, g2: teletextCharsetG2Arabic}, 7: {g0: teletextCharsetG0Arabic, g2: teletextCharsetG2Arabic}, }, } // Teletext G0 charsets var ( teletextCharsetG0CyrillicOption1 = &teletextCharset{ []byte{0x20}, []byte{0x21}, []byte{0x22}, []byte{0x23}, []byte{0x24}, []byte{0x25}, []byte{0xd1, 0x8b}, []byte{0x27}, []byte{0x28}, []byte{0x29}, []byte{0x2a}, []byte{0x2b}, []byte{0x2c}, []byte{0x2d}, []byte{0x2e}, []byte{0x2f}, []byte{0x30}, []byte{0x31}, []byte{0xe3, 0x88, 0x80}, []byte{0x33}, []byte{0x34}, []byte{0x35}, []byte{0x36}, []byte{0x37}, []byte{0x38}, []byte{0x39}, []byte{0x3a}, []byte{0x3b}, []byte{0x3c}, []byte{0x3d}, []byte{0x3e}, []byte{0x3f}, []byte{0xd0, 0xa7}, []byte{0xd0, 0x90}, []byte{0xd0, 0x91}, []byte{0xd0, 0xa6}, []byte{0xd0, 0x94}, []byte{0xd0, 0x95}, []byte{0xd0, 0xa4}, []byte{0xd0, 0x93}, []byte{0xd0, 0xa5}, []byte{0xd0, 0x98}, []byte{0xd0, 0x88}, []byte{0xd0, 0x9a}, []byte{0xd0, 0x9b}, []byte{0xd0, 0x9c}, []byte{0xd0, 0x9d}, []byte{0xd0, 0x9e}, []byte{0xd0, 0x9f}, []byte{0xd0, 0x8c}, []byte{0xd0, 0xa0}, []byte{0xd0, 0xa1}, []byte{0xd0, 0xa2}, []byte{0xd0, 0xa3}, []byte{0xd0, 0x92}, []byte{0xd0, 0x83}, []byte{0xd0, 0x89}, []byte{0xd0, 0x8a}, []byte{0xd0, 0x97}, []byte{0xd0, 0x8b}, []byte{0xd0, 0x96}, []byte{0xd0, 0x82}, []byte{0xd0, 0xa8}, []byte{0xd0, 0x8f}, []byte{0xd1, 0x87}, []byte{0xd0, 0xb0}, []byte{0xd0, 0xb1}, []byte{0xd1, 0x86}, []byte{0xd0, 0xb4}, []byte{0xd0, 0xb5}, []byte{0xd1, 0x84}, []byte{0xd0, 0xb3}, []byte{0xd1, 0x85}, []byte{0xd0, 0xb8}, []byte{0xd0, 0xa8}, []byte{0xd0, 0xba}, []byte{0xd0, 0xbb}, []byte{0xd0, 0xbc}, []byte{0xd0, 0xbd}, []byte{0xd0, 0xbe}, []byte{0xd0, 0xbf}, []byte{0xd0, 0xac}, []byte{0xd1, 0x80}, []byte{0xd1, 0x81}, []byte{0xd1, 0x82}, []byte{0xd1, 0x83}, []byte{0xd0, 0xb2}, []byte{0xd0, 0xa3}, []byte{0xd0, 0xa9}, []byte{0xd0, 0xaa}, []byte{0xd0, 0xb7}, []byte{0xd0, 0xab}, []byte{0xd0, 0xb6}, []byte{0xd0, 0xa2}, []byte{0xd1, 0x88}, []byte{0xd0, 0xaf}, } teletextCharsetG0CyrillicOption2 = &teletextCharset{ []byte{0x20}, []byte{0x21}, []byte{0x22}, []byte{0x23}, []byte{0x24}, []byte{0x25}, []byte{0xd1, 0x8b}, []byte{0x27}, []byte{0x28}, []byte{0x29}, []byte{0x2a}, []byte{0x2b}, []byte{0x2c}, []byte{0x2d}, []byte{0x2e}, []byte{0x2f}, []byte{0x30}, []byte{0x31}, []byte{0x32}, []byte{0x33}, []byte{0x34}, []byte{0x35}, []byte{0x36}, []byte{0x37}, []byte{0x38}, []byte{0x39}, []byte{0x3a}, []byte{0x3b}, []byte{0x3c}, []byte{0x3d}, []byte{0x3e}, []byte{0x3f}, []byte{0xd0, 0xae}, []byte{0xd0, 0x90}, []byte{0xd0, 0x91}, []byte{0xd0, 0xa6}, []byte{0xd0, 0x94}, []byte{0xd0, 0x95}, []byte{0xd0, 0xa4}, []byte{0xd0, 0x93}, []byte{0xd0, 0xa5}, []byte{0xd0, 0x98}, []byte{0xd0, 0x99}, []byte{0xd0, 0x9a}, []byte{0xd0, 0x9b}, []byte{0xd0, 0x9c}, []byte{0xd0, 0x9d}, []byte{0xd0, 0x9e}, []byte{0xd0, 0x9f}, []byte{0xd0, 0xaf}, []byte{0xd0, 0xa0}, []byte{0xd0, 0xa1}, []byte{0xd0, 0xa2}, []byte{0xd0, 0xa3}, []byte{0xd0, 0x96}, []byte{0xd0, 0x92}, []byte{0xd0, 0xac}, []byte{0xd0, 0xaa}, []byte{0xd0, 0x97}, []byte{0xd0, 0xa8}, []byte{0xd0, 0xad}, []byte{0xd0, 0xa9}, []byte{0xd0, 0xa7}, []byte{0xd0, 0xab}, []byte{0xd1, 0x8e}, []byte{0xd0, 0xb0}, []byte{0xd0, 0xb1}, []byte{0xd1, 0x86}, []byte{0xd0, 0xb4}, []byte{0xd0, 0xb5}, []byte{0xd1, 0x84}, []byte{0xd0, 0xb3}, []byte{0xd1, 0x85}, []byte{0xd0, 0xb8}, []byte{0xd0, 0xb9}, []byte{0xd0, 0xba}, []byte{0xd0, 0xbb}, []byte{0xd0, 0xbc}, []byte{0xd0, 0xbd}, []byte{0xd0, 0xbe}, []byte{0xd0, 0xbf}, []byte{0xd1, 0x8f}, []byte{0xd1, 0x80}, []byte{0xd1, 0x81}, []byte{0xd1, 0x82}, []byte{0xd1, 0x83}, []byte{0xd0, 0xb6}, []byte{0xd0, 0xb2}, []byte{0xd1, 0x8c}, []byte{0xd1, 0x8a}, []byte{0xd0, 0xb7}, []byte{0xd1, 0x88}, []byte{0xd1, 0x8d}, []byte{0xd1, 0x89}, []byte{0xd1, 0x87}, []byte{0xd1, 0x8b}, } teletextCharsetG0CyrillicOption3 = &teletextCharset{ []byte{0x20}, []byte{0x21}, []byte{0x22}, []byte{0x23}, []byte{0x24}, []byte{0x25}, []byte{0xc3, 0xaf}, []byte{0x27}, []byte{0x28}, []byte{0x29}, []byte{0x2a}, []byte{0x2b}, []byte{0x2c}, []byte{0x2d}, []byte{0x2e}, []byte{0x2f}, []byte{0x30}, []byte{0x31}, []byte{0x32}, []byte{0x33}, []byte{0x34}, []byte{0x35}, []byte{0x36}, []byte{0x37}, []byte{0x38}, []byte{0x39}, []byte{0x3a}, []byte{0x3b}, []byte{0x3c}, []byte{0x3d}, []byte{0x3e}, []byte{0x3f}, []byte{0xd0, 0xae}, []byte{0xd0, 0x90}, []byte{0xd0, 0x91}, []byte{0xd0, 0xa6}, []byte{0xd0, 0x94}, []byte{0xd0, 0x95}, []byte{0xd0, 0xa4}, []byte{0xd0, 0x93}, []byte{0xd0, 0xa5}, []byte{0xd0, 0x98}, []byte{0xd0, 0x99}, []byte{0xd0, 0x9a}, []byte{0xd0, 0x9b}, []byte{0xd0, 0x9c}, []byte{0xd0, 0x9d}, []byte{0xd0, 0x9e}, []byte{0xd0, 0x9f}, []byte{0xd0, 0xaf}, []byte{0xd0, 0xa0}, []byte{0xd0, 0xa1}, []byte{0xd0, 0xa2}, []byte{0xd0, 0xa3}, []byte{0xd0, 0x96}, []byte{0xd0, 0x92}, []byte{0xd0, 0xac}, []byte{0x49}, []byte{0xd0, 0x97}, []byte{0xd0, 0xa8}, []byte{0xd0, 0xad}, []byte{0xd0, 0xa9}, []byte{0xd0, 0xa7}, []byte{0xc3, 0x8f}, []byte{0xd1, 0x8e}, []byte{0xd0, 0xb0}, []byte{0xd0, 0xb1}, []byte{0xd1, 0x86}, []byte{0xd0, 0xb4}, []byte{0xd0, 0xb5}, []byte{0xd1, 0x84}, []byte{0xd0, 0xb3}, []byte{0xd1, 0x85}, []byte{0xd0, 0xb8}, []byte{0xd0, 0xb9}, []byte{0xd0, 0xba}, []byte{0xd0, 0xbb}, []byte{0xd0, 0xbc}, []byte{0xd0, 0xbd}, []byte{0xd0, 0xbe}, []byte{0xd0, 0xbf}, []byte{0xd1, 0x8f}, []byte{0xd1, 0x80}, []byte{0xd1, 0x81}, []byte{0xd1, 0x82}, []byte{0xd1, 0x83}, []byte{0xd0, 0xb6}, []byte{0xd0, 0xb2}, []byte{0xd1, 0x8c}, []byte{0x69}, []byte{0xd0, 0xb7}, []byte{0xd1, 0x88}, []byte{0xd1, 0x8d}, []byte{0xd1, 0x89}, []byte{0xd1, 0x87}, []byte{0xc3, 0xbf}, } teletextCharsetG0Greek = &teletextCharset{ []byte{0x20}, []byte{0x21}, []byte{0x22}, []byte{0x23}, []byte{0x24}, []byte{0x25}, []byte{0x26}, []byte{0x27}, []byte{0x28}, []byte{0x29}, []byte{0x2a}, []byte{0x2b}, []byte{0x2c}, []byte{0x2d}, []byte{0x2e}, []byte{0x2f}, []byte{0x30}, []byte{0x31}, []byte{0x32}, []byte{0x33}, []byte{0x34}, []byte{0x35}, []byte{0x36}, []byte{0x37}, []byte{0x38}, []byte{0x39}, []byte{0x3a}, []byte{0x3b}, []byte{0x3c}, []byte{0x3d}, []byte{0x3e}, []byte{0x3f}, []byte{0xce, 0x90}, []byte{0xce, 0x91}, []byte{0xce, 0x92}, []byte{0xce, 0x93}, []byte{0xce, 0x94}, []byte{0xce, 0x95}, []byte{0xce, 0x96}, []byte{0xce, 0x97}, []byte{0xce, 0x98}, []byte{0xce, 0x99}, []byte{0xce, 0x9a}, []byte{0xce, 0x9b}, []byte{0xce, 0x9c}, []byte{0xce, 0x9d}, []byte{0xce, 0x9e}, []byte{0xce, 0x9f}, []byte{0xce, 0xa0}, []byte{0xce, 0xa1}, []byte{0xce, 0xa2}, []byte{0xce, 0xa3}, []byte{0xce, 0xa4}, []byte{0xce, 0xa5}, []byte{0xce, 0xa6}, []byte{0xce, 0xa7}, []byte{0xce, 0xa8}, []byte{0xce, 0xa9}, []byte{0xce, 0xaa}, []byte{0xce, 0xab}, []byte{0xce, 0xac}, []byte{0xce, 0xad}, []byte{0xce, 0xae}, []byte{0xce, 0xaf}, []byte{0xce, 0xb0}, []byte{0xce, 0xb1}, []byte{0xce, 0xb2}, []byte{0xce, 0xb3}, []byte{0xce, 0xb4}, []byte{0xce, 0xb5}, []byte{0xce, 0xb6}, []byte{0xce, 0xb7}, []byte{0xce, 0xb8}, []byte{0xce, 0xb9}, []byte{0xce, 0xba}, []byte{0xce, 0xbb}, []byte{0xce, 0xbc}, []byte{0xce, 0xbd}, []byte{0xce, 0xbe}, []byte{0xce, 0xbf}, []byte{0xcf, 0x80}, []byte{0xcf, 0x81}, []byte{0xcf, 0x82}, []byte{0xcf, 0x83}, []byte{0xcf, 0x84}, []byte{0xcf, 0x85}, []byte{0xcf, 0x86}, []byte{0xcf, 0x87}, []byte{0xcf, 0x88}, []byte{0xcf, 0x89}, []byte{0xcf, 0x8a}, []byte{0xcf, 0x8b}, []byte{0xcf, 0x8c}, []byte{0xcf, 0x8d}, []byte{0xcf, 0x8e}, []byte{0xcf, 0x8f}, } teletextCharsetG0Latin = &teletextCharset{ []byte{0x20}, []byte{0x21}, []byte{0x22}, []byte{0xc2, 0xa3}, []byte{0x24}, []byte{0x25}, []byte{0x26}, []byte{0x27}, []byte{0x28}, []byte{0x29}, []byte{0x2a}, []byte{0x2b}, []byte{0x2c}, []byte{0x2d}, []byte{0x2e}, []byte{0x2f}, []byte{0x30}, []byte{0x31}, []byte{0x32}, []byte{0x33}, []byte{0x34}, []byte{0x35}, []byte{0x36}, []byte{0x37}, []byte{0x38}, []byte{0x39}, []byte{0x3a}, []byte{0x3b}, []byte{0x3c}, []byte{0x3d}, []byte{0x3e}, []byte{0x3f}, []byte{0x40}, []byte{0x41}, []byte{0x42}, []byte{0x43}, []byte{0x44}, []byte{0x45}, []byte{0x46}, []byte{0x47}, []byte{0x48}, []byte{0x49}, []byte{0x4a}, []byte{0x4b}, []byte{0x4c}, []byte{0x4d}, []byte{0x4e}, []byte{0x4f}, []byte{0x50}, []byte{0x51}, []byte{0x52}, []byte{0x53}, []byte{0x54}, []byte{0x55}, []byte{0x56}, []byte{0x57}, []byte{0x58}, []byte{0x59}, []byte{0x5a}, []byte{0xc2, 0xab}, []byte{0xc2, 0xbd}, []byte{0xc2, 0xbb}, []byte{0x5e}, []byte{0x23}, []byte{0x2d}, []byte{0x61}, []byte{0x62}, []byte{0x63}, []byte{0x64}, []byte{0x65}, []byte{0x66}, []byte{0x67}, []byte{0x68}, []byte{0x69}, []byte{0x6a}, []byte{0x6b}, []byte{0x6c}, []byte{0x6d}, []byte{0x6e}, []byte{0x6f}, []byte{0x70}, []byte{0x71}, []byte{0x72}, []byte{0x73}, []byte{0x74}, []byte{0x75}, []byte{0x76}, []byte{0x77}, []byte{0x78}, []byte{0x79}, []byte{0x7a}, []byte{0xc2, 0xbc}, []byte{0xc2, 0xa6}, []byte{0xc2, 0xbe}, []byte{0xc3, 0xb7}, []byte{0x7f}, } // TODO Add teletextCharsetG0Arabic = teletextCharsetG0Latin teletextCharsetG0Hebrew = teletextCharsetG0Latin ) // Teletext G2 charsets var ( teletextCharsetG2Latin = &teletextCharset{ []byte{0x20}, []byte{0xc2, 0xa1}, []byte{0xc2, 0xa2}, []byte{0xc2, 0xa3}, []byte{0x24}, []byte{0xc2, 0xa5}, []byte{0x23}, []byte{0xc2, 0xa7}, []byte{0xc2, 0xa4}, []byte{0xe2, 0x80, 0x98}, []byte{0xe2, 0x80, 0x9c}, []byte{0xc2, 0xab}, []byte{0xe2, 0x86, 0x90}, []byte{0xe2, 0x86, 0x91}, []byte{0xe2, 0x86, 0x92}, []byte{0xe2, 0x86, 0x93}, []byte{0xc2, 0xb0}, []byte{0xc2, 0xb1}, []byte{0xc2, 0xb2}, []byte{0xc2, 0xb3}, []byte{0xc3, 0x97}, []byte{0xc2, 0xb5}, []byte{0xc2, 0xb6}, []byte{0xc2, 0xb7}, []byte{0xc3, 0xb7}, []byte{0xe2, 0x80, 0x99}, []byte{0xe2, 0x80, 0x9d}, []byte{0xc2, 0xbb}, []byte{0xc2, 0xbc}, []byte{0xc2, 0xbd}, []byte{0xc2, 0xbe}, []byte{0xc2, 0xbf}, []byte{0x20}, []byte{0xcc, 0x80}, []byte{0xcc, 0x81}, []byte{0xcc, 0x82}, []byte{0xcc, 0x83}, []byte{0xcc, 0x84}, []byte{0xcc, 0x86}, []byte{0xcc, 0x87}, []byte{0xcc, 0x88}, []byte{0x00}, []byte{0xcc, 0x8a}, []byte{0xcc, 0xa7}, []byte{0x5f}, []byte{0xcc, 0x8b}, []byte{0xcc, 0xa8}, []byte{0xcc, 0x8c}, []byte{0xe2, 0x80, 0x95}, []byte{0xc2, 0xb9}, []byte{0xc2, 0xae}, []byte{0xc2, 0xa9}, []byte{0xe2, 0x84, 0xa2}, []byte{0xe2, 0x99, 0xaa}, []byte{0xe2, 0x82, 0xac}, []byte{0xe2, 0x80, 0xb0}, []byte{0xce, 0xb1}, []byte{0x00}, []byte{0x00}, []byte{0x00}, []byte{0xe2, 0x85, 0x9b}, []byte{0xe2, 0x85, 0x9c}, []byte{0xe2, 0x85, 0x9d}, []byte{0xe2, 0x85, 0x9e}, []byte{0xce, 0xa9}, []byte{0xc3, 0x86}, []byte{0xc4, 0x90}, []byte{0xc2, 0xaa}, []byte{0xc4, 0xa6}, []byte{0x00}, []byte{0xc4, 0xb2}, []byte{0xc4, 0xbf}, []byte{0xc5, 0x81}, []byte{0xc3, 0x98}, []byte{0xc5, 0x92}, []byte{0xc2, 0xba}, []byte{0xc3, 0x9e}, []byte{0xc5, 0xa6}, []byte{0xc5, 0x8a}, []byte{0xc5, 0x89}, []byte{0xc4, 0xb8}, []byte{0xc3, 0xa6}, []byte{0xc4, 0x91}, []byte{0xc3, 0xb0}, []byte{0xc4, 0xa7}, []byte{0xc4, 0xb1}, []byte{0xc4, 0xb3}, []byte{0xc5, 0x80}, []byte{0xc5, 0x82}, []byte{0xc3, 0xb8}, []byte{0xc5, 0x93}, []byte{0xc3, 0x9f}, []byte{0xc3, 0xbe}, []byte{0xc5, 0xa7}, []byte{0xc5, 0x8b}, []byte{0x20}, } // TODO Add teletextCharsetG2Arabic = teletextCharsetG2Latin teletextCharsetG2Cyrillic = teletextCharsetG2Latin teletextCharsetG2Greek = teletextCharsetG2Latin ) var teletextNationalSubsetCharactersPositionInG0 = [13]uint8{0x03, 0x04, 0x20, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x5b, 0x5c, 0x5d, 0x5e} // Teletext national subsets var ( teletextNationalSubsetCzechSlovak = &teletextNationalSubset{ []byte{0x23}, []byte{0xc5, 0xaf}, []byte{0xc4, 0x8d}, []byte{0xc5, 0xa5}, []byte{0xc5, 0xbe}, []byte{0xc3, 0xbd}, []byte{0xc3, 0xad}, []byte{0xc5, 0x99}, []byte{0xc3, 0xa9}, []byte{0xc3, 0xa1}, []byte{0xc4, 0x9b}, []byte{0xc3, 0xba}, []byte{0xc5, 0xa1}, } teletextNationalSubsetEnglish = &teletextNationalSubset{ []byte{0xc2, 0xa3}, []byte{0x24}, []byte{0x40}, []byte{0xc2, 0xab}, []byte{0xc2, 0xbd}, []byte{0xc2, 0xbb}, []byte{0x5e}, []byte{0x23}, []byte{0x2d}, []byte{0xc2, 0xbc}, []byte{0xc2, 0xa6}, []byte{0xc2, 0xbe}, []byte{0xc3, 0xb7}, } teletextNationalSubsetEstonian = &teletextNationalSubset{ []byte{0x23}, []byte{0xc3, 0xb5}, []byte{0xc5, 0xa0}, []byte{0xc3, 0x84}, []byte{0xc3, 0x96}, []byte{0xc5, 0xbe}, []byte{0xc3, 0x9c}, []byte{0xc3, 0x95}, []byte{0xc5, 0xa1}, []byte{0xc3, 0xa4}, []byte{0xc3, 0xb6}, []byte{0xc5, 0xbe}, []byte{0xc3, 0xbc}, } teletextNationalSubsetFrench = &teletextNationalSubset{ []byte{0xc3, 0xa9}, []byte{0xc3, 0xaf}, []byte{0xc3, 0xa0}, []byte{0xc3, 0xab}, []byte{0xc3, 0xaa}, []byte{0xc3, 0xb9}, []byte{0xc3, 0xae}, []byte{0x23}, []byte{0xc3, 0xa8}, []byte{0xc3, 0xa2}, []byte{0xc3, 0xb4}, []byte{0xc3, 0xbb}, []byte{0xc3, 0xa7}, } teletextNationalSubsetGerman = &teletextNationalSubset{ []byte{0x23}, []byte{0x24}, []byte{0xc2, 0xa7}, []byte{0xc3, 0x84}, []byte{0xc3, 0x96}, []byte{0xc3, 0x9c}, []byte{0x5e}, []byte{0x5f}, []byte{0xc2, 0xb0}, []byte{0xc3, 0xa4}, []byte{0xc3, 0xb6}, []byte{0xc3, 0xbc}, []byte{0xc3, 0x9f}, } teletextNationalSubsetItalian = &teletextNationalSubset{ []byte{0xc2, 0xa3}, []byte{0x24}, []byte{0xc3, 0xa9}, []byte{0xc2, 0xb0}, []byte{0xc3, 0xa7}, []byte{0xc2, 0xbb}, []byte{0x5e}, []byte{0x23}, []byte{0xc3, 0xb9}, []byte{0xc3, 0xa0}, []byte{0xc3, 0xb2}, []byte{0xc3, 0xa8}, []byte{0xc3, 0xac}, } teletextNationalSubsetLettishLithuanian = &teletextNationalSubset{ []byte{0x23}, []byte{0x24}, []byte{0xc5, 0xa0}, []byte{0xc4, 0x97}, []byte{0xc4, 0x99}, []byte{0xc5, 0xbd}, []byte{0xc4, 0x8d}, []byte{0xc5, 0xab}, []byte{0xc5, 0xa1}, []byte{0xc4, 0x85}, []byte{0xc5, 0xb3}, []byte{0xc5, 0xbe}, []byte{0xc4, 0xaf}, } teletextNationalSubsetPolish = &teletextNationalSubset{ []byte{0x23}, []byte{0xc5, 0x84}, []byte{0xc4, 0x85}, []byte{0xc5, 0xbb}, []byte{0xc5, 0x9a}, []byte{0xc5, 0x81}, []byte{0xc4, 0x87}, []byte{0xc3, 0xb3}, []byte{0xc4, 0x99}, []byte{0xc5, 0xbc}, []byte{0xc5, 0x9b}, []byte{0xc5, 0x82}, []byte{0xc5, 0xba}, } teletextNationalSubsetPortugueseSpanish = &teletextNationalSubset{ []byte{0xc3, 0xa7}, []byte{0x24}, []byte{0xc2, 0xa1}, []byte{0xc3, 0xa1}, []byte{0xc3, 0xa9}, []byte{0xc3, 0xad}, []byte{0xc3, 0xb3}, []byte{0xc3, 0xba}, []byte{0xc2, 0xbf}, []byte{0xc3, 0xbc}, []byte{0xc3, 0xb1}, []byte{0xc3, 0xa8}, []byte{0xc3, 0xa0}, } teletextNationalSubsetRomanian = &teletextNationalSubset{ []byte{0x23}, []byte{0xc2, 0xa4}, []byte{0xc5, 0xa2}, []byte{0xc3, 0x82}, []byte{0xc5, 0x9e}, []byte{0xc4, 0x82}, []byte{0xc3, 0x8e}, []byte{0xc4, 0xb1}, []byte{0xc5, 0xa3}, []byte{0xc3, 0xa2}, []byte{0xc5, 0x9f}, []byte{0xc4, 0x83}, []byte{0xc3, 0xae}, } teletextNationalSubsetSerbianCroatianSlovenian = &teletextNationalSubset{ []byte{0x23}, []byte{0xc3, 0x8b}, []byte{0xc4, 0x8c}, []byte{0xc4, 0x86}, []byte{0xc5, 0xbd}, []byte{0xc4, 0x90}, []byte{0xc5, 0xa0}, []byte{0xc3, 0xab}, []byte{0xc4, 0x8d}, []byte{0xc4, 0x87}, []byte{0xc5, 0xbe}, []byte{0xc4, 0x91}, []byte{0xc5, 0xa1}, } teletextNationalSubsetSwedishFinnishHungarian = &teletextNationalSubset{ []byte{0x23}, []byte{0xc2, 0xa4}, []byte{0xc3, 0x89}, []byte{0xc3, 0x84}, []byte{0xc3, 0x96}, []byte{0xc3, 0x85}, []byte{0xc3, 0x9c}, []byte{0x5f}, []byte{0xc3, 0xa9}, []byte{0xc3, 0xa4}, []byte{0xc3, 0xb6}, []byte{0xc3, 0xa5}, []byte{0xc3, 0xbc}, } teletextNationalSubsetTurkish = &teletextNationalSubset{ []byte{0x54}, []byte{0xc4, 0x9f}, []byte{0xc4, 0xb0}, []byte{0xc5, 0x9e}, []byte{0xc3, 0x96}, []byte{0xc3, 0x87}, []byte{0xc3, 0x9c}, []byte{0xc4, 0x9e}, []byte{0xc4, 0xb1}, []byte{0xc5, 0x9f}, []byte{0xc3, 0xb6}, []byte{0xc3, 0xa7}, []byte{0xc3, 0xbc}, } ) // Teletext PES data types const ( teletextPESDataTypeEBU = "EBU" teletextPESDataTypeUnknown = "unknown" ) func teletextPESDataType(dataIdentifier uint8) string { switch { case dataIdentifier >= 0x10 && dataIdentifier <= 0x1f: return teletextPESDataTypeEBU } return teletextPESDataTypeUnknown } // Teletext PES data unit ids const ( teletextPESDataUnitIDEBUNonSubtitleData = 0x2 teletextPESDataUnitIDEBUSubtitleData = 0x3 teletextPESDataUnitIDStuffing = 0xff ) // TeletextOptions represents teletext options type TeletextOptions struct { Page int PID int } // ReadFromTeletext parses a teletext content // http://www.etsi.org/deliver/etsi_en/300400_300499/300472/01.03.01_60/en_300472v010301p.pdf // http://www.etsi.org/deliver/etsi_i_ets/300700_300799/300706/01_60/ets_300706e01p.pdf // TODO Update README // TODO Add tests func ReadFromTeletext(r io.Reader, o TeletextOptions) (s *Subtitles, err error) { // Init s = &Subtitles{} var dmx = astits.New(context.Background(), r) // Get the teletext PID var pid uint16 if pid, err = teletextPID(dmx, o); err != nil { if err != ErrNoValidTeletextPID { err = errors.Wrap(err, "astisub: getting teletext PID failed") } return } // Create character decoder cd := newTeletextCharacterDecoder() // Create page buffer b := newTeletextPageBuffer(o.Page, cd) // Loop in data var firstTime, lastTime time.Time var d *astits.Data var ps []*teletextPage for { // Fetch next data if d, err = dmx.NextData(); err != nil { if err == astits.ErrNoMorePackets { err = nil break } err = errors.Wrap(err, "astisub: fetching next data failed") return } // We only parse PES data if d.PES == nil { continue } // This data is not of interest to us if d.PID != pid || d.PES.Header.StreamID != astits.StreamIDPrivateStream1 { continue } // Get time t := teletextDataTime(d) if t.IsZero() { continue } // First and last time if firstTime.IsZero() || firstTime.After(t) { firstTime = t } if lastTime.IsZero() || lastTime.Before(t) { lastTime = t } // Append pages ps = append(ps, b.process(d.PES, t)...) } // Dump buffer ps = append(ps, b.dump(lastTime)...) // Parse pages for _, p := range ps { p.parse(s, cd, firstTime) } return } // TODO Add tests func teletextDataTime(d *astits.Data) time.Time { if d.PES.Header != nil && d.PES.Header.OptionalHeader != nil && d.PES.Header.OptionalHeader.PTS != nil { return d.PES.Header.OptionalHeader.PTS.Time() } else if d.FirstPacket != nil && d.FirstPacket.AdaptationField != nil && d.FirstPacket.AdaptationField.PCR != nil { return d.FirstPacket.AdaptationField.PCR.Time() } return time.Time{} } // If the PID teletext option is not indicated, it will walk through the ts data until it reaches a PMT packet to // detect the first valid teletext PID // TODO Add tests func teletextPID(dmx *astits.Demuxer, o TeletextOptions) (pid uint16, err error) { // PID is in the options if o.PID > 0 { pid = uint16(o.PID) return } // Loop in data var d *astits.Data for { // Fetch next data if d, err = dmx.NextData(); err != nil { if err == astits.ErrNoMorePackets { err = ErrNoValidTeletextPID return } err = errors.Wrap(err, "astisub: fetching next data failed") return } // PMT data if d.PMT != nil { // Retrieve valid teletext PIDs var pids []uint16 for _, s := range d.PMT.ElementaryStreams { for _, dsc := range s.ElementaryStreamDescriptors { if dsc.Tag == astits.DescriptorTagTeletext || dsc.Tag == astits.DescriptorTagVBITeletext { pids = append(pids, s.ElementaryPID) } } } // No valid teletext PIDs if len(pids) == 0 { err = ErrNoValidTeletextPID return } // Set pid pid = pids[0] astilog.Debugf("astisub: no teletext pid specified, using pid %d", pid) // Rewind if _, err = dmx.Rewind(); err != nil { err = errors.Wrap(err, "astisub: rewinding failed") return } return } } return } type teletextPageBuffer struct { cd *teletextCharacterDecoder currentPage *teletextPage donePages []*teletextPage magazineNumber uint8 pageNumber int receiving bool } func newTeletextPageBuffer(page int, cd *teletextCharacterDecoder) *teletextPageBuffer { return &teletextPageBuffer{ cd: cd, magazineNumber: uint8(page / 100), pageNumber: page % 100, } } // TODO Add tests func (b *teletextPageBuffer) dump(lastTime time.Time) (ps []*teletextPage) { if b.currentPage != nil { b.currentPage.end = lastTime ps = []*teletextPage{b.currentPage} } return } // TODO Add tests func (b *teletextPageBuffer) process(d *astits.PESData, t time.Time) (ps []*teletextPage) { // Data identifier var offset int dataIdentifier := uint8(d.Data[offset]) offset += 1 // Check data type if teletextPESDataType(dataIdentifier) != teletextPESDataTypeEBU { return } // Loop through data units for offset < len(d.Data) { // ID id := uint8(d.Data[offset]) offset += 1 // Length length := uint8(d.Data[offset]) offset += 1 // Offset end offsetEnd := offset + int(length) if offsetEnd > len(d.Data) { break } // Parse data unit b.parseDataUnit(d.Data[offset:offsetEnd], id, t) // Seek to end of data unit offset = offsetEnd } // Dump buffer ps = b.donePages b.donePages = []*teletextPage(nil) return ps } // TODO Add tests func (b *teletextPageBuffer) parseDataUnit(i []byte, id uint8, t time.Time) { // Check id if id != teletextPESDataUnitIDEBUSubtitleData { return } // Field parity: i[0]&0x20 > 0 // Line offset: uint8(i[0] & 0x1f) // Framing code framingCode := uint8(i[1]) // Check framing code if framingCode != 0xe4 { return } // Magazine number and packet number h1, ok := astibits.Hamming84Decode(i[2]) if !ok { return } h2, ok := astibits.Hamming84Decode(i[3]) if !ok { return } h := h2<<4 | h1 magazineNumber := h & 0x7 if magazineNumber == 0 { magazineNumber = 8 } packetNumber := h >> 3 // Parse packet b.parsePacket(i[4:], magazineNumber, packetNumber, t) return } // TODO Add tests func (b *teletextPageBuffer) parsePacket(i []byte, magazineNumber, packetNumber uint8, t time.Time) { if packetNumber == 0 { b.parsePacketHeader(i, magazineNumber, t) } else if b.receiving && magazineNumber == b.magazineNumber && (packetNumber >= 1 && packetNumber <= 25) { b.parsePacketData(i, packetNumber) } else { // Designation code designationCode, ok := astibits.Hamming84Decode(i[0]) if !ok { return } // Parse packet if b.receiving && magazineNumber == b.magazineNumber && packetNumber == 26 { // TODO Implement } else if b.receiving && magazineNumber == b.magazineNumber && packetNumber == 28 { b.parsePacket28And29(i[1:], packetNumber, designationCode) } else if magazineNumber == b.magazineNumber && packetNumber == 29 { b.parsePacket28And29(i[1:], packetNumber, designationCode) } else if magazineNumber == 8 && packetNumber == 30 { b.parsePacket30(i, designationCode) } } } // TODO Add tests func (b *teletextPageBuffer) parsePacketHeader(i []byte, magazineNumber uint8, t time.Time) (transmissionDone bool) { // Page number units pageNumberUnits, ok := astibits.Hamming84Decode(i[0]) if !ok { return } // Page number tens pageNumberTens, ok := astibits.Hamming84Decode(i[1]) if !ok { return } pageNumber := int(pageNumberTens)*10 + int(pageNumberUnits) // 0xff is a reserved page number value if pageNumberTens == 0xf && pageNumberUnits == 0xf { return } // Update magazine and page number if b.magazineNumber == 0 && b.pageNumber == 0 { // C6 controlBits, ok := astibits.Hamming84Decode(i[5]) if !ok { return } subtitleFlag := controlBits&0x8 > 0 // This is a subtitle page if subtitleFlag { b.magazineNumber = magazineNumber b.pageNumber = pageNumber astilog.Debugf("astisub: no teletext page specified, using page %d%.2d", b.magazineNumber, b.pageNumber) } } // C11 --> C14 controlBits, ok := astibits.Hamming84Decode(i[7]) if !ok { return } magazineSerial := controlBits&0x1 > 0 charsetCode := controlBits >> 1 // Page transmission is done if b.receiving && ((magazineSerial && pageNumber != b.pageNumber) || (!magazineSerial && pageNumber != b.pageNumber && magazineNumber == b.magazineNumber)) { b.receiving = false return } // Invalid magazine or page number if pageNumber != b.pageNumber || magazineNumber != b.magazineNumber { return } // Now that we know when the previous page ends we can add it to the done slice if b.currentPage != nil { b.currentPage.end = t b.donePages = append(b.donePages, b.currentPage) } // Reset b.receiving = true b.currentPage = newTeletextPage(charsetCode, t) return } // TODO Add tests func (b *teletextPageBuffer) parsePacketData(i []byte, packetNumber uint8) { // Make sure the map is initialized if _, ok := b.currentPage.data[packetNumber]; !ok { b.currentPage.data[packetNumber] = make([]byte, 40) } // Loop through input b.currentPage.rows = append(b.currentPage.rows, int(packetNumber)) for idx := uint8(0); idx < 40; idx++ { v, ok := astibits.Parity(bits.Reverse8(i[idx])) if !ok { v = 0 } b.currentPage.data[packetNumber][idx] = v } } // TODO Add tests func (b *teletextPageBuffer) parsePacket28And29(i []byte, packetNumber, designationCode uint8) { // Invalid designation code if designationCode != 0 && designationCode != 4 { return } // Triplet 1 // TODO Implement hamming 24/18 triplet1, ok := astibits.Hamming2418Decode(uint32(i[2]<<16) | uint32(i[1])<<8 | uint32(i[0])) if !ok { return } // We only process x/28 format 1 if packetNumber == 28 && triplet1&0xf > 0 { return } // Update character decoder if packetNumber == 28 { b.cd.setTripletX28(triplet1) } else { b.cd.setTripletM29(triplet1) } return } // TODO Add tests func (b *teletextPageBuffer) parsePacket30(i []byte, designationCode uint8) { // Switch on designation code to determine format switch designationCode { case 0, 1: b.parsePacket30Format1(i) case 2, 3: b.parsePacket30Format2(i) } } func (b *teletextPageBuffer) parsePacket30Format1(i []byte) { // TODO Implement } func (b *teletextPageBuffer) parsePacket30Format2(i []byte) { // TODO Implement } type teletextCharacterDecoder struct { c teletextCharset lastPageCharsetCode *uint8 tripletM29 *uint32 tripletX28 *uint32 } func newTeletextCharacterDecoder() *teletextCharacterDecoder { return &teletextCharacterDecoder{} } // TODO Add tests func (d *teletextCharacterDecoder) setTripletM29(i uint32) { if *d.tripletM29 != i { d.tripletM29 = astiptr.UInt32(i) d.updateCharset(d.lastPageCharsetCode, true) } } // TODO Add tests func (d *teletextCharacterDecoder) setTripletX28(i uint32) { if *d.tripletX28 != i { d.tripletX28 = astiptr.UInt32(i) d.updateCharset(d.lastPageCharsetCode, true) } } // TODO Add tests func (d *teletextCharacterDecoder) decode(i byte) []byte { if i < 0x20 { return []byte{} } return d.c[i-0x20] } // TODO Add tests func (d *teletextCharacterDecoder) updateCharset(pageCharsetCode *uint8, force bool) { // Charset is up to date if d.lastPageCharsetCode != nil && *pageCharsetCode == *d.lastPageCharsetCode && !force { return } d.lastPageCharsetCode = pageCharsetCode // Get triplet var triplet uint32 if d.tripletX28 != nil { triplet = *d.tripletX28 } else if d.tripletM29 != nil { triplet = *d.tripletM29 } // Get charsets d.c = *teletextCharsetG0Latin var nationalOptionSubset *teletextNationalSubset if v1, ok := teletextCharsets[uint8((triplet&0x3f80)>>10)]; ok { if v2, ok := v1[*pageCharsetCode]; ok { d.c = *v2.g0 nationalOptionSubset = v2.national } } // Update g0 with national option subset if nationalOptionSubset != nil { for k, v := range nationalOptionSubset { d.c[teletextNationalSubsetCharactersPositionInG0[k]] = v } } return } type teletextPage struct { charsetCode uint8 data map[uint8][]byte end time.Time rows []int start time.Time } func newTeletextPage(charsetCode uint8, start time.Time) *teletextPage { return &teletextPage{ charsetCode: charsetCode, data: make(map[uint8][]byte), start: start, } } func (p *teletextPage) parse(s *Subtitles, d *teletextCharacterDecoder, firstTime time.Time) { // Update charset d.updateCharset(astiptr.UInt8(p.charsetCode), false) // No data if len(p.data) == 0 { return } // Order rows sort.Ints(p.rows) // Create item i := &Item{ EndAt: p.end.Sub(firstTime), StartAt: p.start.Sub(firstTime), } // Loop through rows for _, idxRow := range p.rows { parseTeletextRow(i, d, nil, p.data[uint8(idxRow)]) } // Append item s.Items = append(s.Items, i) } type decoder interface { decode(i byte) []byte } type styler interface { hasBeenSet() bool hasChanged(s *StyleAttributes) bool parseSpacingAttribute(i byte) propagateStyleAttributes(s *StyleAttributes) update(sa *StyleAttributes) } func parseTeletextRow(i *Item, d decoder, fs func() styler, row []byte) { // Loop through columns var l = Line{} var li = LineItem{InlineStyle: &StyleAttributes{}} var started bool var s styler for _, v := range row { // Create specific styler if fs != nil { s = fs() } // Get spacing attributes var color *Color var doubleHeight, doubleSize, doubleWidth *bool switch v { case 0x0: color = ColorBlack case 0x1: color = ColorRed case 0x2: color = ColorGreen case 0x3: color = ColorYellow case 0x4: color = ColorBlue case 0x5: color = ColorMagenta case 0x6: color = ColorCyan case 0x7: color = ColorWhite case 0xa: started = false case 0xb: started = true case 0xc: doubleHeight = astiptr.Bool(false) doubleSize = astiptr.Bool(false) doubleWidth = astiptr.Bool(false) case 0xd: doubleHeight = astiptr.Bool(true) case 0xe: doubleWidth = astiptr.Bool(true) case 0xf: doubleSize = astiptr.Bool(true) default: if s != nil { s.parseSpacingAttribute(v) } } // Style has been set if color != nil || doubleHeight != nil || doubleSize != nil || doubleWidth != nil || (s != nil && s.hasBeenSet()) { // Style has changed if color != li.InlineStyle.TeletextColor || doubleHeight != li.InlineStyle.TeletextDoubleHeight || doubleSize != li.InlineStyle.TeletextDoubleSize || doubleWidth != li.InlineStyle.TeletextDoubleWidth || (s != nil && s.hasChanged(li.InlineStyle)) { // Line has started if started { // Append line item appendTeletextLineItem(&l, li, s) // Create new line item sa := &StyleAttributes{} *sa = *li.InlineStyle li = LineItem{InlineStyle: sa} } // Update style attributes if color != nil && color != li.InlineStyle.TeletextColor { li.InlineStyle.TeletextColor = color } if doubleHeight != nil && doubleHeight != li.InlineStyle.TeletextDoubleHeight { li.InlineStyle.TeletextDoubleHeight = doubleHeight } if doubleSize != nil && doubleSize != li.InlineStyle.TeletextDoubleSize { li.InlineStyle.TeletextDoubleSize = doubleSize } if doubleWidth != nil && doubleWidth != li.InlineStyle.TeletextDoubleWidth { li.InlineStyle.TeletextDoubleWidth = doubleWidth } if s != nil { s.update(li.InlineStyle) } } } else if started { // Append text li.Text += string(d.decode(v)) } } // Append line item appendTeletextLineItem(&l, li, s) // Append line if len(l.Items) > 0 { i.Lines = append(i.Lines, l) } } func appendTeletextLineItem(l *Line, li LineItem, s styler) { // There's some text if len(strings.TrimSpace(li.Text)) > 0 { // Make sure inline style exists if li.InlineStyle == nil { li.InlineStyle = &StyleAttributes{} } // Get number of spaces before li.InlineStyle.TeletextSpacesBefore = astiptr.Int(0) for _, c := range li.Text { if c == ' ' { *li.InlineStyle.TeletextSpacesBefore++ } else { break } } // Get number of spaces after li.InlineStyle.TeletextSpacesAfter = astiptr.Int(0) for idx := len(li.Text) - 1; idx >= 0; idx-- { if li.Text[idx] == ' ' { *li.InlineStyle.TeletextSpacesAfter++ } else { break } } // Propagate style attributes li.InlineStyle.propagateTeletextAttributes() if s != nil { s.propagateStyleAttributes(li.InlineStyle) } // Append line item li.Text = strings.TrimSpace(li.Text) l.Items = append(l.Items, li) } }