fix(multiple-subtitles): repair old ASS conversion line breaks [PKG-MULTI-SUB-11]

This commit is contained in:
Harvey Zhao committed 2026-09-14 13:42:46 +08:00
1 parent 8ae2762a23
commit 318de37e8a
22 files changed
+3268 -27

No files matched your search

@@ -55,4 +55,4 @@
*
* d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
*/
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},t=function(t){t||(t={"&amp":"&","&lt":"<","&gt":">","&lrm":"\u200e","&rlm":"\u200f","&nbsp":"\xa0"}),this.entities=t,this.parse=function(s,a){s=s.replace(/\0/g,"\ufffd");var r=Date.now(),o=0,l=s.split(/\r\n|\r|\n/),c=!1,f=[],u=[],d=[];function p(e,t){d.push({message:e,line:o+1,col:t})}var m=l[o],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),o++;""!=l[o]&&void 0!=l[o];){if(p("No blank line after the signature."),-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}for(;void 0!=l[o];){for(var x;!c&&""==l[o];)o++;if(!c&&void 0==l[o])break;x=Object.assign({},e,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var y=!0;if(-1==l[o].indexOf("--\x3e")){if(x.id=l[o],/^NOTE($|[ \t])/.test(x.id)){for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),o++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var T=[],w=!1;for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),T.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(T.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){y=!1,p("Cue identifier needs to be followed by timestamp.");continue}}c=!1;var A=new n(l[o],p),O=0;if(u.length>0&&(O=u[u.length-1].startTime),!y||A.parse(x,O)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),c=!0;break}""!=x.text&&(x.text+="\n"),x.text+=l[o],o++}var L=new i(x.text,p,a,t);x.tree=L.parse(x.startTime,x.endTime),u.push(x)}else for(x=null,o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}}return u.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:u,errors:d,time:Date.now()-r,styles:f}}},n=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,s=(e=e,0),a=function(e){t(e,s+1)};function r(t){for(;void 0!=e[s]&&t.test(e[s]);)s++}function o(t){for(var n="";void 0!=e[s]&&t.test(e[s]);)n+=e[s],s++;return n}function l(){var t,n,i,r,l="minutes";if(void 0!=e[s])if(/\d/.test(e[s]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[s])if(s++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[s]){if(":"!=e[s])return void a("No seconds found or minutes is greater than 59.");if(s++,2!=(i=o(/\d/)).length)return void a("Must be exactly two digits.")}else{if(2!=t.length)return void a("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[s])if(s++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)a("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;a("You cannot have more than 59 seconds.")}else a("Milliseconds must be given in three digits.");else a('No decimal separator (".") found.')}else a("Must be exactly two digits.");else a("No time unit separator found.");else a("Timestamp must start with a character in the range 0-9.");else a("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&a("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[s])&&a("Timestamp not separated from '--\x3e' by whitespace."),r(n),"-"==e[s])if(s++,"-"==e[s])if(s++,">"==e[s]){if(s++,i.test(e[s])&&a("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&a("End timestamp is not greater than start timestamp."),i.test(e[s]),r(n),function(e,t){for(var i=e.split(n),s=[],r=0;r<i.length;r++)if(""!=i[r]){var o=i[r].indexOf(":"),l=i[r].slice(0,o),c=i[r].slice(o+1);if(-1!=s.indexOf(l)&&a("Duplicate setting."),s.push(l),""==c)return void a("No value for setting defined.");if("vertical"==l){if("rl"!=c&&"lr"!=c){a("Writing direction can only be set to 'rl' or 'rl'.");continue}t.direction=c}else if("line"==l){if(/,/.test(c)){var f=c.split(",");c=f[0];var u=f[1]}if(!/^[-\d](\d*)(\.\d+)?%?$/.test(c)){a("Line position takes a number or percentage.");continue}if(-1!=c.indexOf("-",1)){a("Line position can only have '-' at the start.");continue}if(-1!=c.indexOf("%")&&c.indexOf("%")!=c.Line truncated
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},t=function(t){t||(t={"&amp":"&","&lt":"<","&gt":">","&lrm":"\u200e","&rlm":"\u200f","&nbsp":"\xa0"}),this.entities=t,this.parse=function(s,a){s=s.replace(/\0/g,"\ufffd");var r=Date.now(),o=0,l=s.split(/\r\n|\r|\n/),c=!1,f=[],u=[],d=[];function p(e,t){d.push({message:e,line:o+1,col:t})}var m=l[o],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),o++;""!=l[o]&&void 0!=l[o];){if(p("No blank line after the signature."),-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}for(;void 0!=l[o];){for(var x;!c&&""==l[o];)o++;if(!c&&void 0==l[o])break;x=Object.assign({},e,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==l[o].indexOf("--\x3e")){if(x.id=l[o],/^NOTE($|[ \t])/.test(x.id)){for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),o++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(y.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}c=!1;var $=new n(l[o],p),A=0;if(u.length>0&&(A=u[u.length-1].startTime),!T||$.parse(x,A)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),c=!0;break}""!=x.text&&(x.text+="\n"),x.text+=l[o],o++}var O=new i(x.text,p,a,t);x.tree=O.parse(x.startTime,x.endTime),u.push(x)}else for(x=null,o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}}return u.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:u,errors:d,time:Date.now()-r,styles:f}}},n=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,s=(e=e,0),a=function(e){t(e,s+1)};function r(t){for(;void 0!=e[s]&&t.test(e[s]);)s++}function o(t){for(var n="";void 0!=e[s]&&t.test(e[s]);)n+=e[s],s++;return n}function l(){var t,n,i,r,l="minutes";if(void 0!=e[s])if(/\d/.test(e[s]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[s])if(s++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[s]){if(":"!=e[s])return void a("No seconds found or minutes is greater than 59.");if(s++,2!=(i=o(/\d/)).length)return void a("Must be exactly two digits.")}else{if(2!=t.length)return void a("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[s])if(s++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)a("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;a("You cannot have more than 59 seconds.")}else a("Milliseconds must be given in three digits.");else a('No decimal separator (".") found.')}else a("Must be exactly two digits.");else a("No time unit separator found.");else a("Timestamp must start with a character in the range 0-9.");else a("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&a("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[s])&&a("Timestamp not separated from '--\x3e' by whitespace."),r(n),"-"==e[s])if(s++,"-"==e[s])if(s++,">"==e[s]){if(s++,i.test(e[s])&&a("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&a("End timestamp is not greater than start timestamp."),i.test(e[s]),r(n),function(e,t){for(var i=e.split(n),s=[],r=0;r<i.length;r++)if(""!=i[r]){var o=i[r].indexOf(":"),l=i[r].slice(0,o),c=i[r].slice(o+1);if(-1!=s.indexOf(l)&&a("Duplicate setting."),s.push(l),""==c)return void a("No value for setting defined.");if("vertical"==l){if("rl"!=c&&"lr"!=c){a("Writing direction can only be set to 'rl' or 'rl'.");continue}t.direction=c}else if("line"==l){if(/,/.test(c)){var f=c.split(",");c=f[0];var u=f[1]}if(!/^[-\d](\d*)(\.\d+)?%?$/.test(c)){a("Line position takes a number or percentage.");continue}if(-1!=c.indexOf("-",1)){a("Line position can only have '-' at the start.");continue}if(-1!=c.indexOf("%")&&c.indexOf("%")!=c.Line truncated
@@ -55,4 +55,4 @@
*
* d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
*/
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,o=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,a=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&o(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&o(e,n,t[n]);return e},l=(e,i)=>t(e,n(i)),c=(e,t,n)=>new Promise((i,r)=>{var s=e=>{try{a(n.next(e))}catch(t){r(t)}},o=e=>{try{a(n.throw(e))}catch(t){r(t)}},a=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,o);a((n=n.apply(e,t)).next())});var u={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},f=function(e){e||(e={"&amp":"&","&lt":"<","&gt":">","&lrm":"\u200e","&rlm":"\u200f","&nbsp":"\xa0"}),this.entities=e,this.parse=function(t,n){t=t.replace(/\0/g,"\ufffd");var i=Date.now(),r=0,s=t.split(/\r\n|\r|\n/),o=!1,a=[],l=[],c=[];function f(e,t){c.push({message:e,line:r+1,col:t})}var m=s[r],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&f('No valid signature. (File needs to start with "WEBVTT".)'),r++;""!=s[r]&&void 0!=s[r];){if(f("No blank line after the signature."),-1!=s[r].indexOf("--\x3e")){o=!0;break}r++}for(;void 0!=s[r];){for(var y;!o&&""==s[r];)r++;if(!o&&void 0==s[r])break;y=Object.assign({},u,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var x=!0;if(-1==s[r].indexOf("--\x3e")){if(y.id=s[r],/^NOTE($|[ \t])/.test(y.id)){for(r++;""!=s[r]&&void 0!=s[r];)-1!=s[r].indexOf("--\x3e")&&f("Cannot have timestamp in a comment."),r++;continue}if(/^STYLE($|[ \t])/.test(y.id)){var T=[],w=!1;for(r++;""!=s[r]&&void 0!=s[r];)-1!=s[r].indexOf("--\x3e")&&(f("Cannot have timestamp in a style block."),w=!0),T.push(s[r]),r++;if(l.length){f("Style blocks cannot appear after the first cue.");continue}w||a.push(T.join("\n"));continue}if(""==s[++r]||void 0==s[r]){f("Cue identifier cannot be standalone.");continue}if(-1==s[r].indexOf("--\x3e")){x=!1,f("Cue identifier needs to be followed by timestamp.");continue}}o=!1;var O=new d(s[r],f),P=0;if(l.length>0&&(P=l[l.length-1].startTime),!x||O.parse(y,P)){for(r++;""!=s[r]&&void 0!=s[r];){if(-1!=s[r].indexOf("--\x3e")){f("Blank line missing before cue."),o=!0;break}""!=y.text&&(y.text+="\n"),y.text+=s[r],r++}var A=new p(y.text,f,n,e);y.tree=A.parse(y.startTime,y.endTime),l.push(y)}else for(y=null,r++;""!=s[r]&&void 0!=s[r];){if(-1!=s[r].indexOf("--\x3e")){o=!0;break}r++}}return l.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:l,errors:c,time:Date.now()-i,styles:a}}},d=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,r=(e=e,0),s=function(e){t(e,r+1)};function o(t){for(;void 0!=e[r]&&t.test(e[r]);)r++}function a(t){for(var n="";void 0!=e[r]&&t.test(e[r]);)n+=e[r],r++;return n}function l(){var t,n,i,o,l="minutes";if(void 0!=e[r])if(/\d/.test(e[r]))if(((t=a(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[r])if(r++,2==(n=a(/\d/)).length){if("hours"==l||":"==e[r]){if(":"!=e[r])return void s("No seconds found or minutes is greater than 59.");if(r++,2!=(i=a(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[r])if(r++,3==(o=a(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(o,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,a){if(o(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<a&&s("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[r])&&s("Timestamp not separated from '--\x3e' by whitespace."),o(n),"-"==e[r])if(r++,"-"==e[r])if(r++,">"==e[r]){if(r++,i.test(e[r])&&s("'--\x3e' not separated from timestamp by whitespace."),o(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[r]),o(n),function(e,t){for(var i=e.split(n),r=[Line truncated
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,o=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,a=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&o(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&o(e,n,t[n]);return e},l=(e,i)=>t(e,n(i)),c=(e,t,n)=>new Promise((i,r)=>{var s=e=>{try{a(n.next(e))}catch(t){r(t)}},o=e=>{try{a(n.throw(e))}catch(t){r(t)}},a=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,o);a((n=n.apply(e,t)).next())});var u={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},f=function(e){e||(e={"&amp":"&","&lt":"<","&gt":">","&lrm":"\u200e","&rlm":"\u200f","&nbsp":"\xa0"}),this.entities=e,this.parse=function(t,n){t=t.replace(/\0/g,"\ufffd");var i=Date.now(),r=0,s=t.split(/\r\n|\r|\n/),o=!1,a=[],l=[],c=[];function f(e,t){c.push({message:e,line:r+1,col:t})}var m=s[r],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&f('No valid signature. (File needs to start with "WEBVTT".)'),r++;""!=s[r]&&void 0!=s[r];){if(f("No blank line after the signature."),-1!=s[r].indexOf("--\x3e")){o=!0;break}r++}for(;void 0!=s[r];){for(var y;!o&&""==s[r];)r++;if(!o&&void 0==s[r])break;y=Object.assign({},u,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var x=!0;if(-1==s[r].indexOf("--\x3e")){if(y.id=s[r],/^NOTE($|[ \t])/.test(y.id)){for(r++;""!=s[r]&&void 0!=s[r];)-1!=s[r].indexOf("--\x3e")&&f("Cannot have timestamp in a comment."),r++;continue}if(/^STYLE($|[ \t])/.test(y.id)){var T=[],w=!1;for(r++;""!=s[r]&&void 0!=s[r];)-1!=s[r].indexOf("--\x3e")&&(f("Cannot have timestamp in a style block."),w=!0),T.push(s[r]),r++;if(l.length){f("Style blocks cannot appear after the first cue.");continue}w||a.push(T.join("\n"));continue}if(""==s[++r]||void 0==s[r]){f("Cue identifier cannot be standalone.");continue}if(-1==s[r].indexOf("--\x3e")){x=!1,f("Cue identifier needs to be followed by timestamp.");continue}}o=!1;var O=new d(s[r],f),$=0;if(l.length>0&&($=l[l.length-1].startTime),!x||O.parse(y,$)){for(r++;""!=s[r]&&void 0!=s[r];){if(-1!=s[r].indexOf("--\x3e")){f("Blank line missing before cue."),o=!0;break}""!=y.text&&(y.text+="\n"),y.text+=s[r],r++}var j=new p(y.text,f,n,e);y.tree=j.parse(y.startTime,y.endTime),l.push(y)}else for(y=null,r++;""!=s[r]&&void 0!=s[r];){if(-1!=s[r].indexOf("--\x3e")){o=!0;break}r++}}return l.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:l,errors:c,time:Date.now()-i,styles:a}}},d=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,r=(e=e,0),s=function(e){t(e,r+1)};function o(t){for(;void 0!=e[r]&&t.test(e[r]);)r++}function a(t){for(var n="";void 0!=e[r]&&t.test(e[r]);)n+=e[r],r++;return n}function l(){var t,n,i,o,l="minutes";if(void 0!=e[r])if(/\d/.test(e[r]))if(((t=a(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[r])if(r++,2==(n=a(/\d/)).length){if("hours"==l||":"==e[r]){if(":"!=e[r])return void s("No seconds found or minutes is greater than 59.");if(r++,2!=(i=a(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[r])if(r++,3==(o=a(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(o,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,a){if(o(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<a&&s("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[r])&&s("Timestamp not separated from '--\x3e' by whitespace."),o(n),"-"==e[r])if(r++,"-"==e[r])if(r++,">"==e[r]){if(r++,i.test(e[r])&&s("'--\x3e' not separated from timestamp by whitespace."),o(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[r]),o(n),function(e,t){for(var i=e.split(n),r=[Line truncated
@@ -1017,6 +1017,31 @@ function createRenderer(art, lifetime, unescape) {
}
};
}
function convertAss(text, converter) {
const converted = converter(text);
if (!/^WEBVTT \d+ \d+:/.test(converted))
return converted;
const cues = [];
for (const line of text.split(/\r?\n/)) {
const match = line.match(/Dialogue:\s\d,(\d+:\d{2}:\d{2}\.\d{2}),(\d+:\d{2}:\d{2}\.\d{2}),(?:[^,]*,){6}([\s\S]*)$/i);
if (!match)
continue;
const [, start = "", end = "", content = ""] = match;
cues.push({
start: `${start.replace(/^(\d):/, "0$1:")}0`,
end: `${end.replace(/^(\d):/, "0$1:")}0`,
text: content.replace(/\{[\s\S]*?\}/g, "").replace(/\\N/g, "\n").trim().split(/\r?\n/).map((line2) => line2.trim()).join("\n")
});
}
const collapsed = `WEBVTT ${cues.map((cue, index2) => `${index2 + 1} ${cue.start} --> ${cue.end} ${cue.text}`).join("\n\n")}`;
if (converted !== collapsed)
return converted;
return `WEBVTT
${cues.map((cue, index2) => `${index2 + 1}
${cue.start} --> ${cue.end}
${cue.text}`).join("\n\n")}`;
}
async function loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime) {
if (lifetime.closed)
return;
@@ -1039,7 +1064,7 @@ async function loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime) {
case "srt":
return srtToVtt(text);
case "ass":
return assToVtt(text);
return convertAss(text, assToVtt);
case "vtt":
return text;
default:
+2 -2
View File
@@ -51,7 +51,7 @@
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:llm && yarn build:test && yarn build:docs && yarn test:imports",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js test/asr-distribution.test.js",
"typecheck": "node scripts/typecheck.mjs",
"test:unit": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js test/danmuku-mask-failures.test.js test/danmuku-mask-lifecycle.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js test/jassub-vendor-lifecycle.test.js test/jassub-frame-polyfill.test.js test/jassub-offscreen.test.js test/multiple-subtitles-caption.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-entities.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-exports.test.js test/auto-thumbnail-canvas.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-encoding.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js",
"test:unit": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js test/danmuku-mask-failures.test.js test/danmuku-mask-lifecycle.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js test/jassub-vendor-lifecycle.test.js test/jassub-frame-polyfill.test.js test/jassub-offscreen.test.js test/multiple-subtitles-ass.test.js test/multiple-subtitles-caption.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-entities.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-exports.test.js test/auto-thumbnail-canvas.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-encoding.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js",
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
"test": "yarn test:node && yarn test:baseline",
"test:browser": "playwright test",
@@ -88,7 +88,7 @@
"test:auto-thumbnail": "node --test test/auto-thumbnail-exports.test.js test/auto-thumbnail-canvas.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-encoding.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js refactor/scripts/auto-thumbnail-contract.test.mjs refactor/scripts/auto-thumbnail-types.test.mjs",
"test:vtt-thumbnail": "node --test test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js refactor/scripts/vtt-thumbnail-contract.test.mjs refactor/scripts/vtt-thumbnail-types.test.mjs",
"test:vtt-thumbnail-types-package": "node refactor/scripts/vtt-thumbnail-package-types.mjs",
"test:multiple-subtitles": "node --test test/multiple-subtitles-caption.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-entities.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js refactor/scripts/multiple-subtitles-contract.test.mjs refactor/scripts/multiple-subtitles-runtime-types.test.mjs refactor/scripts/multiple-subtitles-types.test.mjs",
"test:multiple-subtitles": "node --test test/multiple-subtitles-ass.test.js test/multiple-subtitles-caption.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-entities.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js refactor/scripts/multiple-subtitles-contract.test.mjs refactor/scripts/multiple-subtitles-runtime-types.test.mjs refactor/scripts/multiple-subtitles-types.test.mjs",
"test:multiple-subtitles-types-package": "node refactor/scripts/multiple-subtitles-package-types.mjs",
"test:jassub": "node --test test/jassub.test.js test/jassub-registration.test.js test/jassub-runtime.test.js test/jassub-vendor-lifecycle.test.js test/jassub-frame-polyfill.test.js test/jassub-offscreen.test.js refactor/scripts/jassub-contract.test.mjs refactor/scripts/jassub-provenance.test.mjs refactor/scripts/jassub-failures.test.mjs refactor/scripts/jassub-types.test.mjs refactor/scripts/jassub-vendor-patch.test.mjs",
"test:jassub-types": "node --test refactor/scripts/jassub-types.test.mjs",
@@ -17,6 +17,12 @@ to npm 1.2.0; the runtime entry requires an option object but allows omitted `su
- `src/index.ts` composes registration, named selection and the unchanged public result.
- `src/request.ts` owns each fetch through byte decoding and delegates SRT/ASS conversion
to the existing core utils. Failure aborts the request; registration failure closes siblings.
- `src/ass-conversion.ts` calls the host ASS converter once. It repairs only output
exactly matching the collapsed-line format of core5.1.2 main. Reconstruction
retains that host's supported Dialogue fields, hundredth-second times and text
cleanup; complete-output equality protects custom converters. Valid conversion,
empty output and converter exceptions retain their original behavior. This
private fallback avoids depending on a newer core or patching its shared utils.
- `src/merge.ts` parses metadata and serializes selected cues without mutating parsed trees.
It depends only on the vendor parser, not the player or browser resource APIs.
- `src/text.ts` supplies the vendor's existing entity-table option with both exact
@@ -160,8 +166,11 @@ active cue; the matrix explicitly records lost simultaneous languages for publis
1.1.0/1.2.0, while1.0.0's index-merged cue displays both. Task10 adds the candidate
view adapter without adopting index merging or altering independent cue timings.
`multiple-subtitles-legacy.spec.js` checks asymmetric three-cue overlaps, HTML/CSS,
selection/reset and clearing on both old cores. ASS is additionally exercised on
5.1.7; published5.1.2's malformed ASS converter is separately tracked by task11.
selection/reset and clearing on both old cores, now using ASS on both after task11.
`multiple-subtitles-ass.spec.js` adds UTF16 input with an explicit type override,
multiline text, semantic HTML and translation overlaps on five recorded cores.
Task11's conversion repair leaves the original host utility untouched; it does
not add ASS positioning, animation or styling beyond the existing VTT conversion.
This adapter does not repair an old browser host's stale native active-cue list.
CORE-SUBTITLE-OFFSET-01 corrects
paused offsets in the candidate core; published Firefox hosts retain their exact
@@ -55,4 +55,4 @@
*
* d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
*/
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},t=function(t){t||(t={"&amp":"&","&lt":"<","&gt":">","&lrm":"\u200e","&rlm":"\u200f","&nbsp":"\xa0"}),this.entities=t,this.parse=function(s,a){s=s.replace(/\0/g,"\ufffd");var r=Date.now(),o=0,l=s.split(/\r\n|\r|\n/),c=!1,f=[],u=[],d=[];function p(e,t){d.push({message:e,line:o+1,col:t})}var m=l[o],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),o++;""!=l[o]&&void 0!=l[o];){if(p("No blank line after the signature."),-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}for(;void 0!=l[o];){for(var x;!c&&""==l[o];)o++;if(!c&&void 0==l[o])break;x=Object.assign({},e,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var y=!0;if(-1==l[o].indexOf("--\x3e")){if(x.id=l[o],/^NOTE($|[ \t])/.test(x.id)){for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),o++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var T=[],w=!1;for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),T.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(T.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){y=!1,p("Cue identifier needs to be followed by timestamp.");continue}}c=!1;var A=new n(l[o],p),O=0;if(u.length>0&&(O=u[u.length-1].startTime),!y||A.parse(x,O)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),c=!0;break}""!=x.text&&(x.text+="\n"),x.text+=l[o],o++}var L=new i(x.text,p,a,t);x.tree=L.parse(x.startTime,x.endTime),u.push(x)}else for(x=null,o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}}return u.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:u,errors:d,time:Date.now()-r,styles:f}}},n=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,s=(e=e,0),a=function(e){t(e,s+1)};function r(t){for(;void 0!=e[s]&&t.test(e[s]);)s++}function o(t){for(var n="";void 0!=e[s]&&t.test(e[s]);)n+=e[s],s++;return n}function l(){var t,n,i,r,l="minutes";if(void 0!=e[s])if(/\d/.test(e[s]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[s])if(s++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[s]){if(":"!=e[s])return void a("No seconds found or minutes is greater than 59.");if(s++,2!=(i=o(/\d/)).length)return void a("Must be exactly two digits.")}else{if(2!=t.length)return void a("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[s])if(s++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)a("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;a("You cannot have more than 59 seconds.")}else a("Milliseconds must be given in three digits.");else a('No decimal separator (".") found.')}else a("Must be exactly two digits.");else a("No time unit separator found.");else a("Timestamp must start with a character in the range 0-9.");else a("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&a("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[s])&&a("Timestamp not separated from '--\x3e' by whitespace."),r(n),"-"==e[s])if(s++,"-"==e[s])if(s++,">"==e[s]){if(s++,i.test(e[s])&&a("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&a("End timestamp is not greater than start timestamp."),i.test(e[s]),r(n),function(e,t){for(var i=e.split(n),s=[],r=0;r<i.length;r++)if(""!=i[r]){var o=i[r].indexOf(":"),l=i[r].slice(0,o),c=i[r].slice(o+1);if(-1!=s.indexOf(l)&&a("Duplicate setting."),s.push(l),""==c)return void a("No value for setting defined.");if("vertical"==l){if("rl"!=c&&"lr"!=c){a("Writing direction can only be set to 'rl' or 'rl'.");continue}t.direction=c}else if("line"==l){if(/,/.test(c)){var f=c.split(",");c=f[0];var u=f[1]}if(!/^[-\d](\d*)(\.\d+)?%?$/.test(c)){a("Line position takes a number or percentage.");continue}if(-1!=c.indexOf("-",1)){a("Line position can only have '-' at the start.");continue}if(-1!=c.indexOf("%")&&c.indexOf("%")!=c.Line truncated
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},t=function(t){t||(t={"&amp":"&","&lt":"<","&gt":">","&lrm":"\u200e","&rlm":"\u200f","&nbsp":"\xa0"}),this.entities=t,this.parse=function(s,a){s=s.replace(/\0/g,"\ufffd");var r=Date.now(),o=0,l=s.split(/\r\n|\r|\n/),c=!1,f=[],u=[],d=[];function p(e,t){d.push({message:e,line:o+1,col:t})}var m=l[o],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&p('No valid signature. (File needs to start with "WEBVTT".)'),o++;""!=l[o]&&void 0!=l[o];){if(p("No blank line after the signature."),-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}for(;void 0!=l[o];){for(var x;!c&&""==l[o];)o++;if(!c&&void 0==l[o])break;x=Object.assign({},e,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var T=!0;if(-1==l[o].indexOf("--\x3e")){if(x.id=l[o],/^NOTE($|[ \t])/.test(x.id)){for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&p("Cannot have timestamp in a comment."),o++;continue}if(/^STYLE($|[ \t])/.test(x.id)){var y=[],w=!1;for(o++;""!=l[o]&&void 0!=l[o];)-1!=l[o].indexOf("--\x3e")&&(p("Cannot have timestamp in a style block."),w=!0),y.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(y.join("\n"));continue}if(""==l[++o]||void 0==l[o]){p("Cue identifier cannot be standalone.");continue}if(-1==l[o].indexOf("--\x3e")){T=!1,p("Cue identifier needs to be followed by timestamp.");continue}}c=!1;var $=new n(l[o],p),A=0;if(u.length>0&&(A=u[u.length-1].startTime),!T||$.parse(x,A)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),c=!0;break}""!=x.text&&(x.text+="\n"),x.text+=l[o],o++}var O=new i(x.text,p,a,t);x.tree=O.parse(x.startTime,x.endTime),u.push(x)}else for(x=null,o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){c=!0;break}o++}}return u.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:u,errors:d,time:Date.now()-r,styles:f}}},n=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,s=(e=e,0),a=function(e){t(e,s+1)};function r(t){for(;void 0!=e[s]&&t.test(e[s]);)s++}function o(t){for(var n="";void 0!=e[s]&&t.test(e[s]);)n+=e[s],s++;return n}function l(){var t,n,i,r,l="minutes";if(void 0!=e[s])if(/\d/.test(e[s]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[s])if(s++,2==(n=o(/\d/)).length){if("hours"==l||":"==e[s]){if(":"!=e[s])return void a("No seconds found or minutes is greater than 59.");if(s++,2!=(i=o(/\d/)).length)return void a("Must be exactly two digits.")}else{if(2!=t.length)return void a("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[s])if(s++,3==(r=o(/\d/)).length)if(parseInt(n,10)>59)a("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(r,10)/1e3;a("You cannot have more than 59 seconds.")}else a("Milliseconds must be given in three digits.");else a('No decimal separator (".") found.')}else a("Must be exactly two digits.");else a("No time unit separator found.");else a("Timestamp must start with a character in the range 0-9.");else a("No timestamp found.")}this.parse=function(t,o){if(r(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&a("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[s])&&a("Timestamp not separated from '--\x3e' by whitespace."),r(n),"-"==e[s])if(s++,"-"==e[s])if(s++,">"==e[s]){if(s++,i.test(e[s])&&a("'--\x3e' not separated from timestamp by whitespace."),r(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&a("End timestamp is not greater than start timestamp."),i.test(e[s]),r(n),function(e,t){for(var i=e.split(n),s=[],r=0;r<i.length;r++)if(""!=i[r]){var o=i[r].indexOf(":"),l=i[r].slice(0,o),c=i[r].slice(o+1);if(-1!=s.indexOf(l)&&a("Duplicate setting."),s.push(l),""==c)return void a("No value for setting defined.");if("vertical"==l){if("rl"!=c&&"lr"!=c){a("Writing direction can only be set to 'rl' or 'rl'.");continue}t.direction=c}else if("line"==l){if(/,/.test(c)){var f=c.split(",");c=f[0];var u=f[1]}if(!/^[-\d](\d*)(\.\d+)?%?$/.test(c)){a("Line position takes a number or percentage.");continue}if(-1!=c.indexOf("-",1)){a("Line position can only have '-' at the start.");continue}if(-1!=c.indexOf("%")&&c.indexOf("%")!=c.Line truncated
@@ -55,4 +55,4 @@
*
* d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
*/
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,o=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,a=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&o(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&o(e,n,t[n]);return e},l=(e,i)=>t(e,n(i)),c=(e,t,n)=>new Promise((i,r)=>{var s=e=>{try{a(n.next(e))}catch(t){r(t)}},o=e=>{try{a(n.throw(e))}catch(t){r(t)}},a=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,o);a((n=n.apply(e,t)).next())});var u={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},f=function(e){e||(e={"&amp":"&","&lt":"<","&gt":">","&lrm":"\u200e","&rlm":"\u200f","&nbsp":"\xa0"}),this.entities=e,this.parse=function(t,n){t=t.replace(/\0/g,"\ufffd");var i=Date.now(),r=0,s=t.split(/\r\n|\r|\n/),o=!1,a=[],l=[],c=[];function f(e,t){c.push({message:e,line:r+1,col:t})}var m=s[r],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&f('No valid signature. (File needs to start with "WEBVTT".)'),r++;""!=s[r]&&void 0!=s[r];){if(f("No blank line after the signature."),-1!=s[r].indexOf("--\x3e")){o=!0;break}r++}for(;void 0!=s[r];){for(var y;!o&&""==s[r];)r++;if(!o&&void 0==s[r])break;y=Object.assign({},u,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var x=!0;if(-1==s[r].indexOf("--\x3e")){if(y.id=s[r],/^NOTE($|[ \t])/.test(y.id)){for(r++;""!=s[r]&&void 0!=s[r];)-1!=s[r].indexOf("--\x3e")&&f("Cannot have timestamp in a comment."),r++;continue}if(/^STYLE($|[ \t])/.test(y.id)){var T=[],w=!1;for(r++;""!=s[r]&&void 0!=s[r];)-1!=s[r].indexOf("--\x3e")&&(f("Cannot have timestamp in a style block."),w=!0),T.push(s[r]),r++;if(l.length){f("Style blocks cannot appear after the first cue.");continue}w||a.push(T.join("\n"));continue}if(""==s[++r]||void 0==s[r]){f("Cue identifier cannot be standalone.");continue}if(-1==s[r].indexOf("--\x3e")){x=!1,f("Cue identifier needs to be followed by timestamp.");continue}}o=!1;var O=new d(s[r],f),P=0;if(l.length>0&&(P=l[l.length-1].startTime),!x||O.parse(y,P)){for(r++;""!=s[r]&&void 0!=s[r];){if(-1!=s[r].indexOf("--\x3e")){f("Blank line missing before cue."),o=!0;break}""!=y.text&&(y.text+="\n"),y.text+=s[r],r++}var A=new p(y.text,f,n,e);y.tree=A.parse(y.startTime,y.endTime),l.push(y)}else for(y=null,r++;""!=s[r]&&void 0!=s[r];){if(-1!=s[r].indexOf("--\x3e")){o=!0;break}r++}}return l.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:l,errors:c,time:Date.now()-i,styles:a}}},d=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,r=(e=e,0),s=function(e){t(e,r+1)};function o(t){for(;void 0!=e[r]&&t.test(e[r]);)r++}function a(t){for(var n="";void 0!=e[r]&&t.test(e[r]);)n+=e[r],r++;return n}function l(){var t,n,i,o,l="minutes";if(void 0!=e[r])if(/\d/.test(e[r]))if(((t=a(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[r])if(r++,2==(n=a(/\d/)).length){if("hours"==l||":"==e[r]){if(":"!=e[r])return void s("No seconds found or minutes is greater than 59.");if(r++,2!=(i=a(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[r])if(r++,3==(o=a(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(o,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,a){if(o(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<a&&s("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[r])&&s("Timestamp not separated from '--\x3e' by whitespace."),o(n),"-"==e[r])if(r++,"-"==e[r])if(r++,">"==e[r]){if(r++,i.test(e[r])&&s("'--\x3e' not separated from timestamp by whitespace."),o(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[r]),o(n),function(e,t){for(var i=e.split(n),r=[Line truncated
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginMultipleSubtitles=t(),define(function(){return e.artplayerPluginMultipleSubtitles})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginMultipleSubtitles=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,n=Object.getOwnPropertyDescriptors,i=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,o=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,a=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&o(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&o(e,n,t[n]);return e},l=(e,i)=>t(e,n(i)),c=(e,t,n)=>new Promise((i,r)=>{var s=e=>{try{a(n.next(e))}catch(t){r(t)}},o=e=>{try{a(n.throw(e))}catch(t){r(t)}},a=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,o);a((n=n.apply(e,t)).next())});var u={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},f=function(e){e||(e={"&amp":"&","&lt":"<","&gt":">","&lrm":"\u200e","&rlm":"\u200f","&nbsp":"\xa0"}),this.entities=e,this.parse=function(t,n){t=t.replace(/\0/g,"\ufffd");var i=Date.now(),r=0,s=t.split(/\r\n|\r|\n/),o=!1,a=[],l=[],c=[];function f(e,t){c.push({message:e,line:r+1,col:t})}var m=s[r],g=m.length,h="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(g<b||m.indexOf(h)!==0+v||g>b&&" "!==m[b]&&"\t"!==m[b])&&f('No valid signature. (File needs to start with "WEBVTT".)'),r++;""!=s[r]&&void 0!=s[r];){if(f("No blank line after the signature."),-1!=s[r].indexOf("--\x3e")){o=!0;break}r++}for(;void 0!=s[r];){for(var y;!o&&""==s[r];)r++;if(!o&&void 0==s[r])break;y=Object.assign({},u,{id:"",startTime:0,endTime:0,pauseOnExit:!1,direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center",text:"",tree:null});var x=!0;if(-1==s[r].indexOf("--\x3e")){if(y.id=s[r],/^NOTE($|[ \t])/.test(y.id)){for(r++;""!=s[r]&&void 0!=s[r];)-1!=s[r].indexOf("--\x3e")&&f("Cannot have timestamp in a comment."),r++;continue}if(/^STYLE($|[ \t])/.test(y.id)){var T=[],w=!1;for(r++;""!=s[r]&&void 0!=s[r];)-1!=s[r].indexOf("--\x3e")&&(f("Cannot have timestamp in a style block."),w=!0),T.push(s[r]),r++;if(l.length){f("Style blocks cannot appear after the first cue.");continue}w||a.push(T.join("\n"));continue}if(""==s[++r]||void 0==s[r]){f("Cue identifier cannot be standalone.");continue}if(-1==s[r].indexOf("--\x3e")){x=!1,f("Cue identifier needs to be followed by timestamp.");continue}}o=!1;var O=new d(s[r],f),$=0;if(l.length>0&&($=l[l.length-1].startTime),!x||O.parse(y,$)){for(r++;""!=s[r]&&void 0!=s[r];){if(-1!=s[r].indexOf("--\x3e")){f("Blank line missing before cue."),o=!0;break}""!=y.text&&(y.text+="\n"),y.text+=s[r],r++}var j=new p(y.text,f,n,e);y.tree=j.parse(y.startTime,y.endTime),l.push(y)}else for(y=null,r++;""!=s[r]&&void 0!=s[r];){if(-1!=s[r].indexOf("--\x3e")){o=!0;break}r++}}return l.sort(function(e,t){return e.startTime<t.startTime?-1:e.startTime>t.startTime?1:e.endTime>t.endTime?-1:e.endTime<t.endTime?1:0}),{cues:l,errors:c,time:Date.now()-i,styles:a}}},d=function(e,t){var n=/[\u0020\t\f]/,i=/[^\u0020\t\f]/,r=(e=e,0),s=function(e){t(e,r+1)};function o(t){for(;void 0!=e[r]&&t.test(e[r]);)r++}function a(t){for(var n="";void 0!=e[r]&&t.test(e[r]);)n+=e[r],r++;return n}function l(){var t,n,i,o,l="minutes";if(void 0!=e[r])if(/\d/.test(e[r]))if(((t=a(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[r])if(r++,2==(n=a(/\d/)).length){if("hours"==l||":"==e[r]){if(":"!=e[r])return void s("No seconds found or minutes is greater than 59.");if(r++,2!=(i=a(/\d/)).length)return void s("Must be exactly two digits.")}else{if(2!=t.length)return void s("Must be exactly two digits.");i=n,n=t,t="0"}if("."==e[r])if(r++,3==(o=a(/\d/)).length)if(parseInt(n,10)>59)s("You cannot have more than 59 minutes.");else{if(!(parseInt(i,10)>59))return 60*parseInt(t,10)*60+60*parseInt(n,10)+parseInt(i,10)+parseInt(o,10)/1e3;s("You cannot have more than 59 seconds.")}else s("Milliseconds must be given in three digits.");else s('No decimal separator (".") found.')}else s("Must be exactly two digits.");else s("No time unit separator found.");else s("Timestamp must start with a character in the range 0-9.");else s("No timestamp found.")}this.parse=function(t,a){if(o(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<a&&s("Start timestamp is not greater than or equal to start timestamp of previous cue."),i.test(e[r])&&s("Timestamp not separated from '--\x3e' by whitespace."),o(n),"-"==e[r])if(r++,"-"==e[r])if(r++,">"==e[r]){if(r++,i.test(e[r])&&s("'--\x3e' not separated from timestamp by whitespace."),o(n),t.endTime=l(),void 0!=t.endTime)return t.endTime<=t.startTime&&s("End timestamp is not greater than start timestamp."),i.test(e[r]),o(n),function(e,t){for(var i=e.split(n),r=[Line truncated
@@ -1017,6 +1017,31 @@ function createRenderer(art, lifetime, unescape) {
}
};
}
function convertAss(text, converter) {
const converted = converter(text);
if (!/^WEBVTT \d+ \d+:/.test(converted))
return converted;
const cues = [];
for (const line of text.split(/\r?\n/)) {
const match = line.match(/Dialogue:\s\d,(\d+:\d{2}:\d{2}\.\d{2}),(\d+:\d{2}:\d{2}\.\d{2}),(?:[^,]*,){6}([\s\S]*)$/i);
if (!match)
continue;
const [, start = "", end = "", content = ""] = match;
cues.push({
start: `${start.replace(/^(\d):/, "0$1:")}0`,
end: `${end.replace(/^(\d):/, "0$1:")}0`,
text: content.replace(/\{[\s\S]*?\}/g, "").replace(/\\N/g, "\n").trim().split(/\r?\n/).map((line2) => line2.trim()).join("\n")
});
}
const collapsed = `WEBVTT ${cues.map((cue, index2) => `${index2 + 1} ${cue.start} --> ${cue.end} ${cue.text}`).join("\n\n")}`;
if (converted !== collapsed)
return converted;
return `WEBVTT
${cues.map((cue, index2) => `${index2 + 1}
${cue.start} --> ${cue.end}
${cue.text}`).join("\n\n")}`;
}
async function loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime) {
if (lifetime.closed)
return;
@@ -1039,7 +1064,7 @@ async function loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime) {
case "srt":
return srtToVtt(text);
case "ass":
return assToVtt(text);
return convertAss(text, assToVtt);
case "vtt":
return text;
default:
@@ -0,0 +1,26 @@
import type { Converters } from './types'
export default function convertAss(text: string, converter: Converters['assToVtt']): string {
const converted = converter(text)
if (!/^WEBVTT \d+ \d+:/.test(converted))
return converted
// Core5.1.2 main collapsed template line breaks; compare its complete output
// before repairing so custom converters and valid headers remain untouched.
const cues = []
for (const line of text.split(/\r?\n/)) {
const match = line.match(/Dialogue:\s\d,(\d+:\d{2}:\d{2}\.\d{2}),(\d+:\d{2}:\d{2}\.\d{2}),(?:[^,]*,){6}([\s\S]*)$/i)
if (!match)
continue
const [, start = '', end = '', content = ''] = match
cues.push({
start: `${start.replace(/^(\d):/, '0$1:')}0`,
end: `${end.replace(/^(\d):/, '0$1:')}0`,
text: content.replace(/\{[\s\S]*?\}/g, '').replace(/\\N/g, '\n').trim().split(/\r?\n/).map(line => line.trim()).join('\n'),
})
}
const collapsed = `WEBVTT ${cues.map((cue, index) => `${index + 1} ${cue.start} --> ${cue.end} ${cue.text}`).join('\n\n')}`
if (converted !== collapsed)
return converted
return `WEBVTT\n\n${cues.map((cue, index) => `${index + 1}\n${cue.start} --> ${cue.end}\n${cue.text}`).join('\n\n')}`
}
@@ -1,4 +1,5 @@
import type { Converters, Lifetime, TrackOption } from './types'
import convertAss from './ass-conversion'
export default async function loadVtt(option: TrackOption, { getExt, srtToVtt, assToVtt }: Converters, lifetime: Lifetime): Promise<string | void> {
if (lifetime.closed)
@@ -21,7 +22,7 @@ export default async function loadVtt(option: TrackOption, { getExt, srtToVtt, a
const text = new TextDecoder(option.encoding || 'utf-8').decode(buffer!)
switch (option.type || getExt(option.url!)) {
case 'srt': return srtToVtt(text)
case 'ass': return assToVtt(text)
case 'ass': return convertAss(text, assToVtt)
case 'vtt': return text
default: return ''
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,74 @@
# PKG-MULTI-SUB-11: preserve ASS conversion on old core main
## Reproduction
Frozen npm core5.1.2 has different converter behavior across its own files.
For a plain Dialogue cue, main returns
`WEBVTT 1 00:00:01.250 --> 00:00:03.750 Plain`; the source and legacy file retain
the required header, identifier, timing and payload line breaks. The plugin's
VTT parser consequently sees no ASS cue from main. These are actual verified
tarball members, not simulated converters or version-number assumptions.
The new unit regression initially passes seven baseline/legacy assertions and
fails six main-converter samples. The initial raw-string text assertion also
compared an encoded ampersand with an equivalent literal ampersand; that fixture
failure is retained separately. Canonical HTML comparison fixes the test's text
representation comparison without weakening cue timing/count or semantic markup.
The actual browser regression with the previous plugin artifact fails because
only two of three VTT/SRT/ASS cues load on5.1.2.
## Implementation and compatibility
`src/ass-conversion.ts` is a private adapter called after request decoding. It
calls the existing host converter once. Valid output takes the normal path.
For output with the old collapsed header/timing shape, the adapter reconstructs
the old converter's supported Dialogue fields, timestamp formatting and text
cleanup, then compares the entire collapsed output. Only an exact match receives
the missing structural newlines. Custom output, comments, empty results and
nonmatching malformed conversions retain their original value; exceptions still
reject registration and release owned resources.
This small duplicate of the old supported Dialogue projection is intentional:
importing runtime code from the new core would make the plugin depend on that
core version. It is not a second general ASS parser. Keep its complete-output
comparison and frozen source/legacy parity tests when editing it. Do not extend
accepted ASS syntax or modify cue timing under this repair task.
The host's shared utility is never patched, called with a probe or replaced.
The factory, methods, event order, public types, declarations, global/ESM/CJS
entries, vendor bytes and package versions stay unchanged. No dependency or
lockfile changes are needed. The new test joins existing Yarn unit/package
commands; all three package formats and docs copies are normally rebuilt.
## Validation and limits
Exact inputs, hashes, archived reports and commands are in
[the validation index](../baselines/multiple-subtitles-ass-validation.json).
- Plugin suite315/315: the new15 tests include actual old main/legacy converters,
source parity, independent timing, Unicode, inline markup, ASS line breaks,
overlapping cues, long hours, comma text, custom/empty output and failure cleanup.
- The same targeted15-test file passes with actual main and legacy plugin input.
Its frozen-host and private-adapter checks are identified separately from
full-plugin registration/selection cases in the source.
- Source browser21/21 and actual-main browser21/21. The matrix spans
core5.1.2/5.1.7/5.3.0/5.4.0/candidate in Chromium, Firefox and Windows WebKit.
It checks UTF16 input, explicit type override, native cue times/text, multiline
and semantic HTML, overlapping translation, selection/reset and clearing.
The old-host regression now uses an actual third ASS track on both5.1 cores.
- Actual legacy browser21/21. Strict TypeScript,37 declarations with zero drift,
root and changed-script lint, CI50 and plan/risk validation pass; root lint keeps
the existing generated-doc warning. Detailed evidence is retained in the index.
Close only MULTI-SUB-ASS-01 after these checks. This is conversion into VTT, not
full ASS positioning/animation/rendering, physical-device evidence, remote CI or
package publication. The old WebKit source-seek failure reopened in task10 stays
open and is not rerun or waived by this targeted conversion matrix. Tasks05/06
and the wider refactor remain incomplete.
## Maintenance and rollback
The package architecture and test READMEs describe the adapter and replay commands.
Revert this task and rebuild the plugin to restore prior behavior; preserve the
frozen converter evidence so the known old-main failure cannot be mistaken for
successful ASS compatibility. No push or publication is included.
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 251 项,范围 22 个包及工作区/示例。
状态:todo 56 / doing 17 / blocked 0 / done 178 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 55 / doing 17 / blocked 0 / done 179 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -232,7 +232,7 @@
| PKG-MULTI-SUB-06 | artplayer-plugin-multiple-subtitles<br>验证分发并同步文档 | PKG-MULTI-SUB-05, ENG-07, CORE-SUBTITLE-OFFSET-01, PKG-MULTI-SUB-09, PKG-MULTI-SUB-10, PKG-MULTI-SUB-11 | multiple.subtitles.js、parser 来源许可及打包边界 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
| PKG-MULTI-SUB-09 | artplayer-plugin-multiple-subtitles, artplayer<br>定位旧核心切源寻址与字幕显示边界 | PKG-MULTI-SUB-08, CORE-19 | 无插件及旧新插件对照、真实事件轨迹、原切源失败归因和组合测试顺序 | 保留旧核心立即寻址观察,候选首次寻址与正常字幕组合通过;不把原生时钟失败误归为字幕丢失 | M | done |
| PKG-MULTI-SUB-10 | artplayer-plugin-multiple-subtitles<br>适配旧核心的多条活动字幕渲染 | PKG-MULTI-SUB-08, PKG-MULTI-SUB-09 | 独立旧宿主显示适配、原生异步字幕重叠回归和兼容边界说明 | 旧5.1.2/5.1.7复现先红后绿;保持原生cue对象/时间、旧事件标量和方法身份;多格式显示及清理通过,保留其他组合和设备缺口 | M | done |
| PKG-MULTI-SUB-11 | artplayer-plugin-multiple-subtitles<br>处理旧发布核心 ASS 转换器丢失换行 | PKG-MULTI-SUB-10 | 冻结5.1.2发布/源码转换差异,兼容转换适配与真实ASS字幕回归 | 保持已有效转换行为,旧发布ASS缺陷有先红后绿与实际产物证据;不得将VTT替代夹具算作ASS通过 | M | todo |
| PKG-MULTI-SUB-11 | artplayer-plugin-multiple-subtitles<br>处理旧发布核心 ASS 转换器丢失换行 | PKG-MULTI-SUB-10 | 冻结5.1.2发布/源码转换差异,兼容转换适配与真实ASS字幕回归 | 保持已有效转换行为,旧发布ASS缺陷有先红后绿与实际产物证据;不得将VTT替代夹具算作ASS通过 | M | done |
## 5 包迁移:artplayer-plugin-jassub
@@ -652,3 +652,4 @@
- CORE-SUBTITLE-OFFSET-01: [记录](changes/2026-09-14-CORE-SUBTITLE-OFFSET-01-paused.md) [记录](baselines/subtitle-offset-validation.json)
- PKG-MULTI-SUB-09: [记录](changes/2026-09-14-PKG-MULTI-SUB-09-switch-order.md) [记录](baselines/multiple-subtitles-switch-validation.json)
- PKG-MULTI-SUB-10: [记录](changes/2026-09-14-PKG-MULTI-SUB-10-legacy-captions.md) [记录](baselines/multiple-subtitles-legacy-validation.json)
- PKG-MULTI-SUB-11: [记录](changes/2026-09-14-PKG-MULTI-SUB-11-ass-conversion.md) [记录](baselines/multiple-subtitles-ass-validation.json)
+14
View File
@@ -1,5 +1,19 @@
# 进度与证据
## PKG-MULTI-SUB-11 旧发布核心 ASS 转换适配完成
实际5.1.2 tarball证明只有main转换器丢失模板换行,同版legacy与源码正确;新增
ASS适配模块先调用宿主转换器一次,仅当完整输出匹配已复现损坏格式才补回结构换行。
有效、自定义、空输出和异常保持原行为,不修改宿主utils、不依赖新核心、无新增依赖。
冻结转换器回归修复前6失败/7通过,旧产物浏览器仅加载2/3字幕;修复后全插件315通过,
实际main/legacy各15项定向测试通过。源码/main/legacy各21项三引擎播放通过,覆盖
5.1.2/5.1.7/5.3.0/5.4.0/候选、UTF16、多行、语义HTML、独立时间与重叠翻译、选择重置。
两个旧宿主第三轨现在都使用真实ASS。严格TS、37声明零漂移、lint与CI50通过,三格式及
docs副本正常重建;关闭ASS风险。见[变更](changes/2026-09-14-PKG-MULTI-SUB-11-ass-conversion.md)
和[验证索引](baselines/multiple-subtitles-ass-validation.json)。251项:179 done、17 doing、55 todo。
不计作完整ASS排版、真机或远端CI验收。旧WebKit切源风险仍open;下一步继续task05
剩余组合验收与切源诊断,随后推进分发及其他插件未完成任务。
## PKG-MULTI-SUB-10 旧核心多条字幕显示适配完成
插件以activeCue/activeCues能力检测选择旧subtitleUpdate或现代subtitleAfterUpdate,
+1 -1
View File
@@ -267,4 +267,4 @@
| MULTI-SUB-MARKUP-01 | resolved / 已复现 | Semantic cue nodes receive HTML wrappers inside their annotation fields | PKG-MULTI-SUB-08 |
| MULTI-SUB-OFFSET-01 | resolved / 已复现 | Paused Firefox subtitle offset leaves out-of-time native cues active and captions visible | PKG-MULTI-SUB-05, CORE-SUBTITLE-OFFSET-01 |
| MULTI-SUB-SWITCH-01 | open / 已复现 | Intermittent WebKit core5.3.0 caption loss after selected-track source switch | PKG-MULTI-SUB-05, PKG-MULTI-SUB-09 |
| MULTI-SUB-ASS-01 | open / 已复现 | Published core5.1.2 ASS converter loses required VTT line breaks | PKG-MULTI-SUB-11 |
| MULTI-SUB-ASS-01 | resolved / 已复现 | Published core5.1.2 ASS converter loses required VTT line breaks | PKG-MULTI-SUB-11 |
+14 -5
View File
@@ -2,7 +2,7 @@
"schemaVersion": 1,
"task": "BASE-07",
"sourceCommit": "e01f39ff",
"updatedAt": "2026-09-14T05:23:17.398Z",
"updatedAt": "2026-09-14T05:41:02.580Z",
"note": "Open means closure evidence is outstanding; source-observed and unverified are not reproduced runtime bugs. A source issue already mitigated in the workspace still requires candidate regression evidence. No accepted-with-scope item is implied by baseline task completion.",
"items": [
{
@@ -6064,7 +6064,7 @@
"id": "MULTI-SUB-ASS-01",
"title": "Published core5.1.2 ASS converter loses required VTT line breaks",
"confirmation": "reproduced",
"status": "open",
"status": "resolved",
"owners": [
"PKG-MULTI-SUB-11"
],
@@ -6072,11 +6072,20 @@
"refactor/changes/2026-09-14-PKG-MULTI-SUB-10-legacy-captions.md",
"refactor/baselines/multiple-subtitles-legacy-validation.json",
"refactor/baselines/multiple-subtitles-cores.json",
"test/browser/multiple-subtitles-legacy.spec.js"
"test/browser/multiple-subtitles-legacy.spec.js",
"refactor/changes/2026-09-14-PKG-MULTI-SUB-11-ass-conversion.md",
"refactor/baselines/multiple-subtitles-ass-validation.json",
"test/multiple-subtitles-ass.test.js",
"test/browser/multiple-subtitles-ass.spec.js"
],
"workspaceState": "Initial three-engine regression gets only2/3 native cues on actual5.1.2 main. Tarball source assToVtt preserves newlines while published main collapses template line breaks to spaces.5.1.7 ASS renders. Current display regression uses VTT for third5.1.2 track and explicitly does not claim ASS acceptance.",
"workspaceState": "Task11 verifies actual5.1.2 main/legacy/source hashes and repairs only complete converter output matching collapsed main line breaks. Full plugin315 and actual-format targeted tests pass; source/main/legacy each21 native desktop cases pass across5 cores and3 engines. Both old-host third tracks now use ASS. Original host utils unchanged; valid/custom output and exceptions preserved. Full ASS layout and physical devices are not claimed.",
"compatibleResolution": "Adapt malformed old conversion in the plugin while preserving valid existing converter output and public API.",
"closureCriteria": "Frozen actual-output reproduction, compatible conversion fix and real ASS display verified on old/current hosts and shippable formats."
"closureCriteria": "Frozen actual-output reproduction, compatible conversion fix and real ASS display verified on old/current hosts and shippable formats.",
"resolutionEvidence": [
"refactor/changes/2026-09-14-PKG-MULTI-SUB-11-ass-conversion.md",
"refactor/baselines/multiple-subtitles-ass-validation.json"
],
"resolutionRationale": "Frozen main output fails before repair while same-version legacy/source remain correct. Exact-output repair preserves valid/custom output and exception ownership.315 package checks, actual main/legacy unit cases and source/main/legacy each21 native desktop cases prove this specific conversion defect fixed, without claiming full ASS layout, device or source-seek acceptance."
}
]
}
+5 -2
View File
@@ -5212,11 +5212,14 @@
"dependsOn": [
"PKG-MULTI-SUB-10"
],
"status": "todo",
"status": "done",
"risk": "M",
"deliverable": "冻结5.1.2发布/源码转换差异,兼容转换适配与真实ASS字幕回归",
"acceptance": "保持已有效转换行为,旧发布ASS缺陷有先红后绿与实际产物证据;不得将VTT替代夹具算作ASS通过",
"evidence": []
"evidence": [
"changes/2026-09-14-PKG-MULTI-SUB-11-ass-conversion.md",
"baselines/multiple-subtitles-ass-validation.json"
]
}
]
}
+7
View File
@@ -1,5 +1,12 @@
# Tests and fixture ownership
`test/multiple-subtitles-ass.test.js` verifies hashes of actual core5.1.2 main,
legacy and source before comparing conversion behavior. It covers the collapsed
main output, source-equivalent timings/text, valid/custom/empty passthrough and
converter failure cleanup through the real candidate plugin. It runs in both
`test:unit` and `test:multiple-subtitles`. This is native VTT conversion, not ASS
layout/rendering acceptance for JASSUB.
`test/multiple-subtitles-caption.test.js` checks the old-host view adapter with the
actual plugin bundle: native order, original update identity/scalar event payload,
escape changes, timestamp cleanup, modern-host isolation and destroy/registration
+9 -3
View File
@@ -270,10 +270,16 @@ the earlier hidden-preview screenshot timeout remains in the checkpoint evidence
`multiple-subtitles-legacy.spec.js` uses real 5.1.2/5.1.7 cores and the candidate
plugin to check three independently timed captions, VTT/SRT, semantic HTML/CSS,
the original update method and native cue identities, scalar event payloads,
repeated updates, selection/reset and empty intervals. It uses ASS for the third
track on5.1.7; the5.1.2 published converter's malformed VTT remains a separate
task11 failure and is not covered by substituting a VTT fixture. Use
repeated updates, selection/reset and empty intervals. It now uses ASS for the third
track on both hosts: task11 repairs the exact5.1.2 main converter defect inside
the plugin. Task10's historical VTT substitution was not ASS acceptance. Use
`ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT` for actual main/legacy package bytes.
The combination suite retains explicit published1.1/1.2 overlap-loss observations
while requiring candidate captions to contain both languages. A passing historical
observation is not a compatibility pass.
`multiple-subtitles-ass.spec.js` checks UTF16 decoding, explicit ASS type selection,
multiline text/HTML, overlapping translation, selection/reset and empty intervals
on5.1.2/5.1.7/5.3.0/5.4.0/candidate. The attachment records the unchanged original
host converter output alongside actual native cues and browser/artifact hashes.
It covers desktop video/track playback, not full ASS layout or physical devices.
@@ -0,0 +1,58 @@
import { Buffer } from 'node:buffer'
import { hash } from '../../refactor/scripts/releases.mjs'
import { multipleSubtitlesCandidate } from '../helpers/multiple-subtitles.js'
import { expect, test } from './fixtures.js'
const candidate = await multipleSubtitlesCandidate()
const ass = '[Events]\r\nDialogue: 0,0:00:01.25,0:00:03.75,Default,,0,0,0,,漢字 café, comma <b>bold</b>\\NSecond line\r\nDialogue: 0,0:00:04.25,0:00:06.75,Default,,0,0,0,,Later cue'
for (const core of ['published-5.1.2', 'published-5.1.7', 'published-5.3.0', 'published', 'candidate']) {
test(`Multiple subtitles ASS / ${core}: encoded multiline cues, overlapping translation and selection`, async ({ page, browser }, testInfo) => {
await page.route('**/test/ass-data.bin', route => route.fulfill({ contentType: 'application/octet-stream', body: Buffer.from(ass, 'utf16le') }))
await page.route('**/test/ass-translation.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:02.000 --> 00:04.000\nTranslation\n' }))
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: candidate.code })
await page.evaluate(() => window.createPlayer('/test/pattern.mp4'))
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.evaluate(async () => {
await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [
{ name: 'ass', url: '/test/ass-data.bin', type: 'ass', encoding: 'utf-16le' },
{ name: 'translation', url: '/test/ass-translation.vtt' },
] }))
})
await expect.poll(() => page.evaluate(() => window.art.template.$track.track.cues?.length)).toBe(3)
const native = await page.evaluate(() => Array.from(window.art.template.$track.track.cues, cue => ({ start: cue.startTime, end: cue.endTime, text: cue.getCueAsHTML().textContent })))
expect(native.map(cue => [cue.start, cue.end])).toEqual([[1.25, 3.75], [2, 4], [4.25, 6.75]])
expect(native[0].text).toContain('漢字 café, comma bold')
expect(native[0].text).toContain('Second line')
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
await page.locator('#pause').click()
async function seek(time) {
await page.evaluate(time => new Promise((resolve) => {
window.art.once('video:seeked', resolve)
window.art.seek = time
}), time)
}
await seek(1.5)
await expect(page.locator('.art-subtitle-ass b')).toHaveText('bold')
await expect(page.locator('.art-subtitle')).toContainText('Second line')
await expect(page.locator('.art-subtitle-translation')).toHaveCount(0)
await seek(2.5)
await expect(page.locator('.art-subtitle-ass b')).toBeVisible()
await expect(page.locator('.art-subtitle-translation')).toHaveText('Translation')
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks(['translation']))
await expect(page.locator('.art-subtitle-ass')).toHaveCount(0)
await expect(page.locator('.art-subtitle-translation')).toHaveText('Translation')
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
await expect(page.locator('.art-subtitle-ass b')).toBeVisible()
await seek(5)
await expect(page.locator('.art-subtitle-ass')).toHaveText('Later cue')
await expect(page.locator('.art-subtitle-translation')).toHaveCount(0)
await seek(7.5)
await expect(page.locator('.art-subtitle-ass, .art-subtitle-translation')).toHaveCount(0)
const rawHostOutput = await page.evaluate(ass => window.Artplayer.utils.assToVtt(ass), ass)
await testInfo.attach('ass-conversion', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), pluginSha256: hash(candidate.code), sourceSha256: hash(Buffer.from(ass, 'utf16le')), rawHostOutput, native, scope: 'Desktop native playback with UTF16 ASS, explicit type override, independently timed multiline/semantic captions, overlap, selection/reset and clearing; original core converter is unchanged.' }) })
await page.evaluate(() => window.art.destroy())
})
}
@@ -9,7 +9,6 @@ for (const core of ['published-5.1.2', 'published-5.1.7']) {
await page.route('**/test/legacy-a.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:01.000 --> 00:03.000\n<b>A &amp; safe</b>\n' }))
await page.route('**/test/legacy-b.srt', route => route.fulfill({ contentType: 'text/plain', body: '1\n00:00:02,000 --> 00:00:04,000\nB\n' }))
await page.route('**/test/legacy-c.ass', route => route.fulfill({ contentType: 'text/plain', body: 'Dialogue: 0,0:00:02.25,0:00:02.75,Default,,0,0,0,,C' }))
await page.route('**/test/legacy-c.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:02.250 --> 00:02.750\nC\n' }))
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: candidate.code })
await page.addStyleTag({ content: '.art-subtitle-a { color: rgb(255, 0, 0); } .art-subtitle-b { color: rgb(0, 0, 255); }' })
@@ -20,7 +19,7 @@ for (const core of ['published-5.1.2', 'published-5.1.7']) {
window.art.on('subtitleUpdate', text => window.legacyPayloads.push(text))
})
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
const thirdFormat = core === 'published-5.1.2' ? 'vtt' : 'ass'
const thirdFormat = 'ass'
await page.evaluate(async (thirdFormat) => {
await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ name: 'a', url: '/test/legacy-a.vtt' }, { name: 'b', url: '/test/legacy-b.srt' }, { name: 'c', url: `/test/legacy-c.${thirdFormat}` }] }))
}, thirdFormat)
@@ -70,7 +69,7 @@ for (const core of ['published-5.1.2', 'published-5.1.7']) {
await expect(page.locator('.art-subtitle-c')).toHaveText('C')
await seek(4.5)
await expect(page.locator('.art-subtitle-a, .art-subtitle-b, .art-subtitle-c')).toHaveCount(0)
await testInfo.attach('legacy-captions', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), pluginSha256: hash(candidate.code), thirdFormat, state, scope: 'Native VTT/SRT overlaps (ASS additionally on 5.1.7), semantic HTML/CSS, legacy scalar event payload, original host method and cue identities, repeated update/selection/reset/empty intervals', limitation: '5.1.2 published ASS converter loses required VTT line breaks; tracked separately in PKG-MULTI-SUB-11.' }) })
await testInfo.attach('legacy-captions', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), pluginSha256: hash(candidate.code), thirdFormat, state, scope: 'Native VTT/SRT/ASS overlaps on both old cores, semantic HTML/CSS, legacy scalar event payload, original host method and cue identities, repeated update/selection/reset/empty intervals' }) })
await page.evaluate(() => window.art.destroy())
})
}
+109
View File
@@ -0,0 +1,109 @@
import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
import fs from 'node:fs'
// eslint-disable-next-line test/no-import-node-test -- Frozen converter regressions use the repository Node runner.
import test from 'node:test'
import vm from 'node:vm'
import { parseHTML } from 'linkedom'
import { ensureArchive, hash, readMember } from '../refactor/scripts/releases.mjs'
import { loadModules } from './helpers/load.js'
import { multipleSubtitlesCandidate, multipleSubtitlesEnvironment } from './helpers/multiple-subtitles.js'
const release = JSON.parse(fs.readFileSync('refactor/baselines/multiple-subtitles-cores.json')).releases.find(item => item.version === '5.1.2')
const archive = await ensureArchive(release)
function read(member) {
const bytes = readMember(archive, member)
assert.equal(hash(bytes), release.files[member])
return bytes.toString()
}
const published = ['main', 'legacy'].map((name) => {
const module = { exports: {} }
vm.runInNewContext(read(`package/dist/artplayer${name === 'main' ? '' : '.legacy'}.js`), { module, exports: module.exports }, { timeout: 5000 })
return { name, utils: (module.exports.default || module.exports).utils }
})
const oldSource = await import(`data:text/javascript;base64,${Buffer.from(read('package/src/utils/subtitle.js')).toString('base64')}`)
const candidate = await multipleSubtitlesCandidate()
const { parseTracks, assToVtt, convertAss } = await loadModules({
parseTracks: { file: 'packages/artplayer-plugin-multiple-subtitles/src/merge', name: 'parseTracks' },
assToVtt: { file: 'packages/artplayer/src/utils/subtitle', name: 'assToVtt' },
convertAss: 'packages/artplayer-plugin-multiple-subtitles/src/ass-conversion',
})
const dialogue = (text, start = '0:00:01.25', end = '0:00:03.75') => `Dialogue: 0,${start},${end},Default,,0,0,0,,${text}`
const html = text => parseHTML(`<html><body>${text}</body></html>`).document.body.innerHTML
const samples = [
dialogue('Plain'),
dialogue('漢字 café & <b>bold</b>'),
dialogue('{\\i1}Italic{\\i0}\\N Second line '),
`${dialogue('First, comma')}\r\n${dialogue('Second', '0:00:02.50', '0:00:04.25')}`,
dialogue('Hour', '123:04:05.67', '124:05:06.78'),
`[Events]\nComment: not a caption\n${dialogue('Literal 2 00:00:02.000 --> 00:00:03.000 payload')}\n`,
]
test('Multiple subtitles ASS adapter preserves custom, valid, empty and nonmatching converter output exactly', () => {
for (const converted of [
'',
'WEBVTT ',
'WEBVTT\n\n',
'Custom result',
oldSource.assToVtt(samples[0]),
'WEBVTT 1 00:00:01.250 --> 00:00:03.750 Changed by user',
'WEBVTT 1 00:00:01.250 --> 00:00:03.750 Plain\n\n',
]) {
let calls = 0
const actual = convertAss(samples[0], (input) => {
calls++
assert.equal(input, samples[0])
return converted
})
assert.equal(calls, 1)
assert.equal(actual, converted)
}
for (const text of ['', '[Events]\nComment: no dialogue', dialogue('Invalid decimal', '0:00:01x25')]) {
const converted = published[0].utils.assToVtt(text)
assert.equal(convertAss(text, published[0].utils.assToVtt), converted)
}
})
test('Multiple subtitles ASS adapter propagates converter failure without mounting resources', async () => {
const error = new Error('Custom conversion failed')
let calls = 0
const env = multipleSubtitlesEnvironment(candidate, {
responses: { 'a.ass': samples[0] },
coreUtils: { assToVtt() {
calls++
throw error
} },
})
await assert.rejects(env.factory({ subtitles: [{ name: 'ass', url: 'a.ass' }] })(env.art), value => value === error)
assert.equal(calls, 1)
assert.equal(env.initialized.length, 0)
assert.equal(env.liveBlobs.size, 0)
assert.equal(env.listeners.get('destroy').size, 0)
})
test('Multiple subtitles frozen5.1.2 main loses VTT line breaks while legacy and source retain them', () => {
assert.equal(published[0].utils.assToVtt(dialogue('Plain')), 'WEBVTT 1 00:00:01.250 --> 00:00:03.750 Plain')
for (const sample of samples) {
assert.equal(published[1].utils.assToVtt(sample), oldSource.assToVtt(sample))
assert.equal(assToVtt(sample), oldSource.assToVtt(sample))
}
})
for (const { name, utils } of published) {
for (const [index, sample] of samples.entries()) {
test(`Multiple subtitles ASS5.1.2 ${name} sample${index}: preserve source cue times and text`, async () => {
const env = multipleSubtitlesEnvironment(candidate, { coreUtils: utils, responses: { 'a.ass': sample } })
const result = await env.factory({ subtitles: [{ name: 'ass', url: 'a.ass' }] })(env.art)
const expected = parseTracks([oldSource.assToVtt(sample)], [{ name: 'ass' }])[0].cues.map(cue => [cue.startTime, cue.endTime, html(cue.text)])
const actual = () => env.latestText().then(vtt => parseTracks([env.utils.unescape(vtt)], [{ name: 'ass' }])[0].cues.map(cue => [cue.startTime, cue.endTime, html(cue.text.replace(/<div class="art-subtitle-ass">|<\/div>/g, ''))]))
assert.deepEqual(await actual(), expected)
result.tracks(['ass'])
assert.deepEqual(await actual(), expected)
result.reset()
assert.deepEqual(await actual(), expected)
assert.equal(env.requests.length, 1)
env.emit('destroy')
assert.equal(env.liveBlobs.size, 0)
})
}
}