fix(multiple-subtitles): [PKG-MULTI-SUB-07] preserve inline timestamps and caption text

This commit is contained in:
Harvey Zhao committed 2026-09-13 11:50:50 +08:00
1 parent da64aae267
commit 7f226ff822
24 files changed
+8660 -132

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 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 O=new n(l[o],p),A=0;if(u.length>0&&(A=u[u.length-1].startTime),!T||O.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 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],h=m.length,g="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(h<b||m.indexOf(g)!==0+v||h>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 y;!c&&""==l[o];)o++;if(!c&&void 0==l[o])break;y=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(y.id=l[o],/^NOTE($|[ \t])/.test(y.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(y.id)){var x=[],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),x.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(x.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 A=new n(l[o],p),O=0;if(u.length>0&&(O=u[u.length-1].startTime),!T||A.parse(y,O)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),c=!0;break}""!=y.text&&(y.text+="\n"),y.text+=l[o],o++}var N=new i(y.text,p,a,t);y.tree=N.parse(y.startTime,y.endTime),u.push(y)}else for(y=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,a=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,o=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&a(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&a(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{o(n.next(e))}catch(t){r(t)}},a=e=>{try{o(n.throw(e))}catch(t){r(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,a);o((n=n.apply(e,t)).next())});var f={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},u=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/),a=!1,o=[],l=[],c=[];function u(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])&&u('No valid signature. (File needs to start with "WEBVTT".)'),r++;""!=s[r]&&void 0!=s[r];){if(u("No blank line after the signature."),-1!=s[r].indexOf("--\x3e")){a=!0;break}r++}for(;void 0!=s[r];){for(var y;!a&&""==s[r];)r++;if(!a&&void 0==s[r])break;y=Object.assign({},f,{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")&&u("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")&&(u("Cannot have timestamp in a style block."),w=!0),T.push(s[r]),r++;if(l.length){u("Style blocks cannot appear after the first cue.");continue}w||o.push(T.join("\n"));continue}if(""==s[++r]||void 0==s[r]){u("Cue identifier cannot be standalone.");continue}if(-1==s[r].indexOf("--\x3e")){x=!1,u("Cue identifier needs to be followed by timestamp.");continue}}a=!1;var O=new d(s[r],u),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")){u("Blank line missing before cue."),a=!0;break}""!=y.text&&(y.text+="\n"),y.text+=s[r],r++}var A=new p(y.text,u,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")){a=!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:o}}},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 a(t){for(;void 0!=e[r]&&t.test(e[r]);)r++}function o(t){for(var n="";void 0!=e[r]&&t.test(e[r]);)n+=e[r],r++;return n}function l(){var t,n,i,a,l="minutes";if(void 0!=e[r])if(/\d/.test(e[r]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[r])if(r++,2==(n=o(/\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=o(/\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==(a=o(/\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(a,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,o){if(a(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&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."),a(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."),a(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]),a(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,a=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,o=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&a(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&a(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{o(n.next(e))}catch(t){r(t)}},a=e=>{try{o(n.throw(e))}catch(t){r(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,a);o((n=n.apply(e,t)).next())}),f={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},u=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/),a=!1,o=[],l=[],c=[];function u(e,t){c.push({message:e,line:r+1,col:t})}var m=s[r],h=m.length,g="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(h<b||m.indexOf(g)!==0+v||h>b&&" "!==m[b]&&"\t"!==m[b])&&u('No valid signature. (File needs to start with "WEBVTT".)'),r++;""!=s[r]&&void 0!=s[r];){if(u("No blank line after the signature."),-1!=s[r].indexOf("--\x3e")){a=!0;break}r++}for(;void 0!=s[r];){for(var y;!a&&""==s[r];)r++;if(!a&&void 0==s[r])break;y=Object.assign({},f,{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")&&u("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")&&(u("Cannot have timestamp in a style block."),w=!0),T.push(s[r]),r++;if(l.length){u("Style blocks cannot appear after the first cue.");continue}w||o.push(T.join("\n"));continue}if(""==s[++r]||void 0==s[r]){u("Cue identifier cannot be standalone.");continue}if(-1==s[r].indexOf("--\x3e")){x=!1,u("Cue identifier needs to be followed by timestamp.");continue}}a=!1;var O=new d(s[r],u),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")){u("Blank line missing before cue."),a=!0;break}""!=y.text&&(y.text+="\n"),y.text+=s[r],r++}var A=new p(y.text,u,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")){a=!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:o}}},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 a(t){for(;void 0!=e[r]&&t.test(e[r]);)r++}function o(t){for(var n="";void 0!=e[r]&&t.test(e[r]);)n+=e[r],r++;return n}function l(){var t,n,i,a,l="minutes";if(void 0!=e[r])if(/\d/.test(e[r]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[r])if(r++,2==(n=o(/\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=o(/\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==(a=o(/\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(a,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,o){if(a(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&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."),a(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."),a(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]),a(n),function(e,t){for(var i=e.split(n),r=[],a=Line truncated
@@ -55,56 +55,6 @@
*
* 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 createLifetime(art) {
let closed = Boolean(art.isDestroy);
const cleanups = /* @__PURE__ */ new Set();
let cancel;
const cancelled = new Promise((resolve) => {
cancel = resolve;
});
function run(cleanup) {
try {
cleanup();
} catch (error) {
console.warn("Failed to clean up multiple subtitles:", error);
}
}
const lifetime = {
get closed() {
return closed;
},
own(cleanup) {
if (closed)
run(cleanup);
else
cleanups.add(cleanup);
return () => cleanups.delete(cleanup);
},
wait(value) {
return Promise.race([value, cancelled]);
},
dispose() {
if (closed)
return;
closed = true;
cancel();
const pending = [...cleanups].reverse();
cleanups.clear();
for (const cleanup of pending)
run(cleanup);
}
};
if (!closed) {
lifetime.own(() => art.off("destroy", lifetime.dispose));
try {
art.on("destroy", lifetime.dispose);
} catch (error) {
lifetime.dispose();
throw error;
}
}
return lifetime;
}
var defaultCueSettings = {
direction: "horizontal",
snapToLines: true,
@@ -849,6 +799,17 @@ var WebVTTSerializer = function() {
return result;
};
};
const TIMESTAMP_CLASS = "artplayer-multiple-subtitles-timestamp";
function markTimestamp(node) {
return { type: "object", name: "c", classes: [TIMESTAMP_CLASS], children: [node] };
}
function markNestedTimestamps(node) {
if (node.type === "timestamp")
return markTimestamp(node);
if (node.type === "object")
return { ...node, children: node.children.map(markNestedTimestamps) };
return node;
}
function parseTracks(vtts, subtitles) {
const parser = new WebVTTParser();
return vtts.map((vtt, index) => {
@@ -865,16 +826,84 @@ function serializeTracks(trees) {
...cue,
tree: {
...cue.tree,
children: cue.tree.children.map((child) => ({
...child,
value: `<div class="art-subtitle-${tree.name}">${child.value}</div>`
}))
children: cue.tree.children.map((child) => {
if (child.type === "timestamp")
return markTimestamp(child);
const value = `<div class="art-subtitle-${tree.name}">${child.value}</div>`;
return child.type === "object" ? { ...child, value, children: child.children.map(markNestedTimestamps) } : { ...child, value };
})
}
});
}
}
return new WebVTTSerializer().serialize(cues);
}
function installCaptionView(art, lifetime) {
function update() {
if (lifetime.closed)
return;
for (const marker of Array.from(art.template.$subtitle.getElementsByTagName(`c.${TIMESTAMP_CLASS}`))) {
const first = marker.firstChild;
if (first?.nodeType === 3)
first.nodeValue = (first.nodeValue || "").replace(/^<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/, "");
marker.replaceWith(...Array.from(marker.childNodes));
}
}
if (lifetime.closed)
return;
lifetime.own(() => art.off("subtitleAfterUpdate", update));
art.on("subtitleAfterUpdate", update);
}
function createLifetime(art) {
let closed = Boolean(art.isDestroy);
const cleanups = /* @__PURE__ */ new Set();
let cancel;
const cancelled = new Promise((resolve) => {
cancel = resolve;
});
function run(cleanup) {
try {
cleanup();
} catch (error) {
console.warn("Failed to clean up multiple subtitles:", error);
}
}
const lifetime = {
get closed() {
return closed;
},
own(cleanup) {
if (closed)
run(cleanup);
else
cleanups.add(cleanup);
return () => cleanups.delete(cleanup);
},
wait(value) {
return Promise.race([value, cancelled]);
},
dispose() {
if (closed)
return;
closed = true;
cancel();
const pending = [...cleanups].reverse();
cleanups.clear();
for (const cleanup of pending)
run(cleanup);
}
};
if (!closed) {
lifetime.own(() => art.off("destroy", lifetime.dispose));
try {
art.on("destroy", lifetime.dispose);
} catch (error) {
lifetime.dispose();
throw error;
}
}
return lifetime;
}
function createRenderer(art, lifetime, unescape) {
let current = null;
function own(url) {
@@ -1002,6 +1031,7 @@ function artplayerPluginMultipleSubtitles({ subtitles = [] }) {
if (lifetime.closed)
return result;
trees = parseTracks(vtts, subtitles);
installCaptionView(art, lifetime);
setTracks(trees);
return result;
} catch (error) {
@@ -17,11 +17,12 @@ task PKG-MULTI-SUB-04 owns that compatibility work. Do not infer runtime behavio
Its cancellation Promise also settles pending operations without AbortController support.
- `src/render.ts` owns generated Blob URLs and synchronous/async host installation outcomes.
Reentrant selections and stale host rejections cannot release the latest selected resource.
- `src/caption.ts` owns a subtitleAfterUpdate listener. It unwraps internal timestamp markers
in the custom HTML caption layer, preserving subsequent nodes and literal user text.
- `src/types.ts` describes the internal Promise result, selected tracks and narrow host/resource
boundaries; option fields reuse the existing public declaration. It is not a published API.
- `src/parser.d.ts` describes the vendored parse result and discriminated cue nodes. Parsed
numeric timestamps are distinct from the historically wrapped string nodes accepted by the
serializer. That wrapping produces invalid inline times; PKG-MULTI-SUB-07 owns its repair.
timestamp values must remain numeric in both parsed and serialized trees.
- `src/parser.js` is vendored WebVTT parser/serializer code with a CC0 dedication. It is not
owned plugin code to mechanically migrate to TypeScript. Keep it distinct from task 04.
The pinned upstream reference and full adaptation check are in
@@ -52,6 +53,18 @@ The historical failure suite deliberately retains the old outcomes; the candidat
suite verifies their fixes. Preserve parser best-effort behavior independently: invalid-header
diagnostics can coexist with usable cues, and metadata is intentionally read after download.
Inline timestamp nodes keep their numeric values. The merge layer puts them in valid VTT
class spans (`c.artplayer-multiple-subtitles-timestamp`), including nested timestamps. Native
getCueAsHTML retains the processing instructions. ArtPlayer's HTML layer does not interpret
those tags, so caption.ts removes only the leading instruction within the generated marker
and unwraps its remaining children. HTML does not pair `<c.class>` with `</c>`; removing the
whole element would incorrectly discard following caption text. The original cue stays intact.
This keeps the existing whole-cue display behavior; it does not add karaoke highlighting.
The vendor entity decoder still leaves extra semicolons after standard named entities. Nine
historical implementations reproduce it; MULTI-SUB-ENTITY-01 remains open for task 05. Do not
silently alter vendor source or declare entity rendering correct because timestamp tests pass.
## Vendor boundary and validation
The comparison revision is w3c/webvtt.js `380cfcce34ba8b472d3a31474874eb72a0e5f460`.
@@ -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 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 O=new n(l[o],p),A=0;if(u.length>0&&(A=u[u.length-1].startTime),!T||O.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 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],h=m.length,g="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(h<b||m.indexOf(g)!==0+v||h>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 y;!c&&""==l[o];)o++;if(!c&&void 0==l[o])break;y=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(y.id=l[o],/^NOTE($|[ \t])/.test(y.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(y.id)){var x=[],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),x.push(l[o]),o++;if(u.length){p("Style blocks cannot appear after the first cue.");continue}w||f.push(x.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 A=new n(l[o],p),O=0;if(u.length>0&&(O=u[u.length-1].startTime),!T||A.parse(y,O)){for(o++;""!=l[o]&&void 0!=l[o];){if(-1!=l[o].indexOf("--\x3e")){p("Blank line missing before cue."),c=!0;break}""!=y.text&&(y.text+="\n"),y.text+=l[o],o++}var N=new i(y.text,p,a,t);y.tree=N.parse(y.startTime,y.endTime),u.push(y)}else for(y=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,a=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,o=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&a(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&a(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{o(n.next(e))}catch(t){r(t)}},a=e=>{try{o(n.throw(e))}catch(t){r(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,a);o((n=n.apply(e,t)).next())});var f={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},u=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/),a=!1,o=[],l=[],c=[];function u(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])&&u('No valid signature. (File needs to start with "WEBVTT".)'),r++;""!=s[r]&&void 0!=s[r];){if(u("No blank line after the signature."),-1!=s[r].indexOf("--\x3e")){a=!0;break}r++}for(;void 0!=s[r];){for(var y;!a&&""==s[r];)r++;if(!a&&void 0==s[r])break;y=Object.assign({},f,{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")&&u("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")&&(u("Cannot have timestamp in a style block."),w=!0),T.push(s[r]),r++;if(l.length){u("Style blocks cannot appear after the first cue.");continue}w||o.push(T.join("\n"));continue}if(""==s[++r]||void 0==s[r]){u("Cue identifier cannot be standalone.");continue}if(-1==s[r].indexOf("--\x3e")){x=!1,u("Cue identifier needs to be followed by timestamp.");continue}}a=!1;var O=new d(s[r],u),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")){u("Blank line missing before cue."),a=!0;break}""!=y.text&&(y.text+="\n"),y.text+=s[r],r++}var A=new p(y.text,u,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")){a=!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:o}}},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 a(t){for(;void 0!=e[r]&&t.test(e[r]);)r++}function o(t){for(var n="";void 0!=e[r]&&t.test(e[r]);)n+=e[r],r++;return n}function l(){var t,n,i,a,l="minutes";if(void 0!=e[r])if(/\d/.test(e[r]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[r])if(r++,2==(n=o(/\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=o(/\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==(a=o(/\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(a,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,o){if(a(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&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."),a(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."),a(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]),a(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,a=(t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i,o=(e,t)=>{for(var n in t||(t={}))r.call(t,n)&&a(e,n,t[n]);if(i)for(var n of i(t))s.call(t,n)&&a(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{o(n.next(e))}catch(t){r(t)}},a=e=>{try{o(n.throw(e))}catch(t){r(t)}},o=e=>e.done?i(e.value):Promise.resolve(e.value).then(s,a);o((n=n.apply(e,t)).next())}),f={direction:"horizontal",snapToLines:!0,linePosition:"auto",lineAlign:"start",textPosition:"auto",positionAlign:"auto",size:100,alignment:"center"},u=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/),a=!1,o=[],l=[],c=[];function u(e,t){c.push({message:e,line:r+1,col:t})}var m=s[r],h=m.length,g="WEBVTT",v=0,b=6;for("\ufeff"===m[0]&&(v=1,b+=1),(h<b||m.indexOf(g)!==0+v||h>b&&" "!==m[b]&&"\t"!==m[b])&&u('No valid signature. (File needs to start with "WEBVTT".)'),r++;""!=s[r]&&void 0!=s[r];){if(u("No blank line after the signature."),-1!=s[r].indexOf("--\x3e")){a=!0;break}r++}for(;void 0!=s[r];){for(var y;!a&&""==s[r];)r++;if(!a&&void 0==s[r])break;y=Object.assign({},f,{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")&&u("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")&&(u("Cannot have timestamp in a style block."),w=!0),T.push(s[r]),r++;if(l.length){u("Style blocks cannot appear after the first cue.");continue}w||o.push(T.join("\n"));continue}if(""==s[++r]||void 0==s[r]){u("Cue identifier cannot be standalone.");continue}if(-1==s[r].indexOf("--\x3e")){x=!1,u("Cue identifier needs to be followed by timestamp.");continue}}a=!1;var O=new d(s[r],u),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")){u("Blank line missing before cue."),a=!0;break}""!=y.text&&(y.text+="\n"),y.text+=s[r],r++}var A=new p(y.text,u,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")){a=!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:o}}},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 a(t){for(;void 0!=e[r]&&t.test(e[r]);)r++}function o(t){for(var n="";void 0!=e[r]&&t.test(e[r]);)n+=e[r],r++;return n}function l(){var t,n,i,a,l="minutes";if(void 0!=e[r])if(/\d/.test(e[r]))if(((t=o(/\d/)).length>2||parseInt(t,10)>59)&&(l="hours"),":"==e[r])if(r++,2==(n=o(/\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=o(/\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==(a=o(/\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(a,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,o){if(a(n),t.startTime=l(),void 0!=t.startTime)if(t.startTime<o&&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."),a(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."),a(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]),a(n),function(e,t){for(var i=e.split(n),r=[],a=Line truncated
@@ -55,56 +55,6 @@
*
* 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 createLifetime(art) {
let closed = Boolean(art.isDestroy);
const cleanups = /* @__PURE__ */ new Set();
let cancel;
const cancelled = new Promise((resolve) => {
cancel = resolve;
});
function run(cleanup) {
try {
cleanup();
} catch (error) {
console.warn("Failed to clean up multiple subtitles:", error);
}
}
const lifetime = {
get closed() {
return closed;
},
own(cleanup) {
if (closed)
run(cleanup);
else
cleanups.add(cleanup);
return () => cleanups.delete(cleanup);
},
wait(value) {
return Promise.race([value, cancelled]);
},
dispose() {
if (closed)
return;
closed = true;
cancel();
const pending = [...cleanups].reverse();
cleanups.clear();
for (const cleanup of pending)
run(cleanup);
}
};
if (!closed) {
lifetime.own(() => art.off("destroy", lifetime.dispose));
try {
art.on("destroy", lifetime.dispose);
} catch (error) {
lifetime.dispose();
throw error;
}
}
return lifetime;
}
var defaultCueSettings = {
direction: "horizontal",
snapToLines: true,
@@ -849,6 +799,17 @@ var WebVTTSerializer = function() {
return result;
};
};
const TIMESTAMP_CLASS = "artplayer-multiple-subtitles-timestamp";
function markTimestamp(node) {
return { type: "object", name: "c", classes: [TIMESTAMP_CLASS], children: [node] };
}
function markNestedTimestamps(node) {
if (node.type === "timestamp")
return markTimestamp(node);
if (node.type === "object")
return { ...node, children: node.children.map(markNestedTimestamps) };
return node;
}
function parseTracks(vtts, subtitles) {
const parser = new WebVTTParser();
return vtts.map((vtt, index) => {
@@ -865,16 +826,84 @@ function serializeTracks(trees) {
...cue,
tree: {
...cue.tree,
children: cue.tree.children.map((child) => ({
...child,
value: `<div class="art-subtitle-${tree.name}">${child.value}</div>`
}))
children: cue.tree.children.map((child) => {
if (child.type === "timestamp")
return markTimestamp(child);
const value = `<div class="art-subtitle-${tree.name}">${child.value}</div>`;
return child.type === "object" ? { ...child, value, children: child.children.map(markNestedTimestamps) } : { ...child, value };
})
}
});
}
}
return new WebVTTSerializer().serialize(cues);
}
function installCaptionView(art, lifetime) {
function update() {
if (lifetime.closed)
return;
for (const marker of Array.from(art.template.$subtitle.getElementsByTagName(`c.${TIMESTAMP_CLASS}`))) {
const first = marker.firstChild;
if (first?.nodeType === 3)
first.nodeValue = (first.nodeValue || "").replace(/^<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/, "");
marker.replaceWith(...Array.from(marker.childNodes));
}
}
if (lifetime.closed)
return;
lifetime.own(() => art.off("subtitleAfterUpdate", update));
art.on("subtitleAfterUpdate", update);
}
function createLifetime(art) {
let closed = Boolean(art.isDestroy);
const cleanups = /* @__PURE__ */ new Set();
let cancel;
const cancelled = new Promise((resolve) => {
cancel = resolve;
});
function run(cleanup) {
try {
cleanup();
} catch (error) {
console.warn("Failed to clean up multiple subtitles:", error);
}
}
const lifetime = {
get closed() {
return closed;
},
own(cleanup) {
if (closed)
run(cleanup);
else
cleanups.add(cleanup);
return () => cleanups.delete(cleanup);
},
wait(value) {
return Promise.race([value, cancelled]);
},
dispose() {
if (closed)
return;
closed = true;
cancel();
const pending = [...cleanups].reverse();
cleanups.clear();
for (const cleanup of pending)
run(cleanup);
}
};
if (!closed) {
lifetime.own(() => art.off("destroy", lifetime.dispose));
try {
art.on("destroy", lifetime.dispose);
} catch (error) {
lifetime.dispose();
throw error;
}
}
return lifetime;
}
function createRenderer(art, lifetime, unescape) {
let current = null;
function own(url) {
@@ -1002,6 +1031,7 @@ function artplayerPluginMultipleSubtitles({ subtitles = [] }) {
if (lifetime.closed)
return result;
trees = parseTracks(vtts, subtitles);
installCaptionView(art, lifetime);
setTracks(trees);
return result;
} catch (error) {
@@ -0,0 +1,21 @@
import type Artplayer from 'artplayer'
import type { Lifetime } from './types'
import { TIMESTAMP_CLASS } from './merge'
export default function installCaptionView(art: Pick<Artplayer, 'on' | 'off' | 'template'>, lifetime: Lifetime): void {
function update(): void {
if (lifetime.closed)
return
// HTML does not pair <c.class> with </c>; unwrap while preserving following caption nodes.
for (const marker of Array.from(art.template.$subtitle.getElementsByTagName(`c.${TIMESTAMP_CLASS}`))) {
const first = marker.firstChild
if (first?.nodeType === 3)
first.nodeValue = (first.nodeValue || '').replace(/^<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/, '')
marker.replaceWith(...Array.from(marker.childNodes))
}
}
if (lifetime.closed)
return
lifetime.own(() => art.off('subtitleAfterUpdate', update))
art.on('subtitleAfterUpdate', update)
}
@@ -1,5 +1,6 @@
import type Artplayer from 'artplayer'
import type { Option, Result, Track } from './types'
import installCaptionView from './caption'
import createLifetime from './lifetime'
import { parseTracks, serializeTracks } from './merge'
import createRenderer from './render'
@@ -33,6 +34,7 @@ export default function artplayerPluginMultipleSubtitles({ subtitles = [] }: Opt
return result
// A live completed request yields text; cancellation was checked above.
trees = parseTracks(vtts as string[], subtitles)
installCaptionView(art, lifetime)
setTracks(trees)
return result
}
@@ -1,7 +1,21 @@
import type { Cue, WrappedNode } from './parser'
import type { Cue, CueNode, TimestampNode, WrappedNode } from './parser'
import type { Track, TrackOption } from './types'
import { WebVTTParser, WebVTTSerializer } from './parser'
export const TIMESTAMP_CLASS = 'artplayer-multiple-subtitles-timestamp'
function markTimestamp(node: TimestampNode): CueNode {
return { type: 'object', name: 'c', classes: [TIMESTAMP_CLASS], children: [node] }
}
function markNestedTimestamps(node: CueNode): CueNode {
if (node.type === 'timestamp')
return markTimestamp(node)
if (node.type === 'object')
return { ...node, children: node.children.map(markNestedTimestamps) }
return node
}
export function parseTracks(vtts: readonly string[], subtitles: readonly TrackOption[]): Track[] {
const parser = new WebVTTParser()
return vtts.map((vtt, index) => {
@@ -21,10 +35,14 @@ export function serializeTracks(trees: readonly (Track | undefined)[]): string {
...cue,
tree: {
...cue.tree,
children: cue.tree.children.map(child => ({
...child,
value: `<div class="art-subtitle-${tree.name}">${child.value}</div>`,
})),
children: cue.tree.children.map((child) => {
if (child.type === 'timestamp')
return markTimestamp(child)
const value = `<div class="art-subtitle-${tree.name}">${child.value}</div>`
return child.type === 'object'
? { ...child, value, children: child.children.map(markNestedTimestamps) }
: { ...child, value }
}),
},
})
}
@@ -17,8 +17,7 @@ export interface TimestampNode {
}
export type CueNode = TextNode | ObjectNode | TimestampNode
// The historical plugin wraps every top-level value, including timestamp nodes.
export type WrappedNode = TextNode | (Omit<ObjectNode, 'value'> & { value: string }) | { type: 'timestamp', value: string }
export type WrappedNode = CueNode
export interface Cue<Node = CueNode> {
id: string
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,49 @@
# PKG-MULTI-SUB-07 内嵌时间戳与显示层修复
九份历史实现已复现 `<00:02.000>` 被包装成字符串并输出 `<NaN:NaN.NaN>`。
本次保留数字 TimestampNode,处理顶层与嵌套节点,在有效的 VTT class 节点中承载
时间戳。解析树仍不可变,重复选择/reset 不积累包装;文本、标签和轨序的原回归保留。
私有 WrappedNode 现在与 CueNode 一样要求数字时间戳,编译负例从 8 项增为 9 项。
## 原生与自定义字幕层
直接输出有效时间戳后,Chromium 中的发布/候选核心都把 `<00:02.000>` 显示成文字。
这不是测试超时或资源加载失败,最初两个失败断言及截图/trace 已保留。
因此新增 caption.ts,在既有 subtitleAfterUpdate 事件后处理插件生成的专用标记。
- 原生 WebVTT 中的 `<c.artplayer-multiple-subtitles-timestamp>` 是合法 class 节点,
其中的数字时间戳仍解析为 processing instruction,cue.text 不被显示层修改。
- HTML 不会把 `<c.class>` 与 `</c>` 配对,后面的字幕可能成为该元素的子节点。
直接删除标记曾导致后文丢失;已通过真实浏览器复现并修正为移除前导时间指令、
将其余子节点移回父层。对应失败报告保留,最终断言检查前后文字。
- 不对整段文字做时间戳正则删除。仅处理专用标记中的前导指令,普通用户文字和
原有 `.art-subtitle-<name>` CSS 钩子保留。public cue.text 修改产生的字面量标签
有真实浏览器回归,清空/reset 后正常恢复原字幕。
- 注册的监听器由当前 lifetime 管理,失败/销毁时移除;已有资源测试改为检查所有
监听器集合为空。没有增加新公开选项、返回值或事件,不改变逐 cue 显示策略。
## 验证范围
纯测试覆盖开始/中间/末尾、多个毫秒时间戳、Unicode、嵌套标签、冻结树、重复选择
和重新解析有效 VTT;历史时间戳失败仍保持,不将候选修复覆盖进旧预期。
原生场景检查开头/中间/末尾三个 processing instruction、前后文字、红色样式、
字面量标签保留、清空/reset,以及原有 SRT、销毁/HTTP/Blob/视频切源场景。
通过源码/main/legacy 与发布/候选核心,在 Chromium/Firefox/Windows WebKit 重跑。
具体结果、编译器和输入指纹见[验证](../baselines/multiple-subtitles-timestamps.json)。
最终三入口各 37 项回归通过,本包 267 项通过;原生各 36 项,共 108 项通过,
原生 processing instruction 精确为 00:00:01.250、00:00:02.000、00:00:03.500。
完整 CI 2337 项通过(1987 单元、14 工程、336 基线),严格生产 TS 353 文件。
状态更新后 9 项元数据测试通过。显式 lint 无错误/警告,全量 lint 保留既有生成声明警告。
## 独立发现与后续边界
给字面量场景提供正常 `&lt;...&gt;` 输入时,vendor 生成了 `<;...>;`。九份历史实现
又复现 `&lt;`、`&gt;`、`&amp;` 的多余分号,新增 MULTI-SUB-ENTITY-01,由 05 处置。
本任务不改 vendor;最终字面量显示测试通过公开 cue.text 修改隔离显示层,明确不代表
该实体解码缺陷已修复。额外分号的初始六个浏览器失败记录保留。
04 的公开类型、完整旧核心/实体设备、实体解码、全部样式/编辑器和安装消费者仍待
后续任务。正常构建更新三个产物及 docs 副本;无新依赖、公开声明和版本变更。
本任务独立本地提交,不推送、不发布。回退恢复原 NaN 输出并移除显示监听器;
已发布失败基线及新发现记录仍可追溯。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 223 项,范围 22 个包及工作区/示例。
状态:todo 89 / doing 14 / blocked 0 / done 120 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 88 / doing 14 / blocked 0 / done 121 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -220,7 +220,7 @@
| PKG-MULTI-SUB-02 | artplayer-plugin-multiple-subtitles<br>建立特有行为与错误测试 | PKG-MULTI-SUB-01, ENG-03, ENG-05 | VTT/SRT/ASS、编码/重叠 cue/排序/空轨/失败与切换 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-MULTI-SUB-03 | artplayer-plugin-multiple-subtitles<br>整理内部职责与资源 | PKG-MULTI-SUB-02, CORE-15 | 解析与合并/获取/渲染分离,过期请求及对象 URL 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-MULTI-SUB-04 | artplayer-plugin-multiple-subtitles<br>迁移自有源码和公开类型 | PKG-MULTI-SUB-03, ENG-04, ENG-06, CORE-07 | 准确 parser/cue/tree/result 类型与真实异步形状 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | doing |
| PKG-MULTI-SUB-07 | artplayer-plugin-multiple-subtitles<br>修复字幕内嵌时间戳序列化 | PKG-MULTI-SUB-03 | 保留数字时间戳节点、修正无效 NaN 输出、明确旧/新字幕输出差异 | 旧九份实现可复现,候选有效内嵌时间戳及文字/CSS/原生字幕通过,公开工厂与选择接口不变 | H | todo |
| PKG-MULTI-SUB-07 | artplayer-plugin-multiple-subtitles<br>修复字幕内嵌时间戳序列化 | PKG-MULTI-SUB-03 | 保留数字时间戳节点、修正无效 NaN 输出、明确旧/新字幕输出差异 | 旧九份实现可复现,候选有效内嵌时间戳及文字/CSS/原生字幕通过,公开工厂与选择接口不变 | H | done |
| PKG-MULTI-SUB-05 | artplayer-plugin-multiple-subtitles<br>验证新旧核心和组合 | PKG-MULTI-SUB-04, CORE-22, PKG-MULTI-SUB-07 | 与核心字幕/偏移/全屏组合,旧名称调用不变 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-MULTI-SUB-06 | artplayer-plugin-multiple-subtitles<br>验证分发并同步文档 | PKG-MULTI-SUB-05, ENG-07 | multiple.subtitles.js、parser 来源许可及打包边界 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
@@ -516,6 +516,7 @@
- PKG-MULTI-SUB-02: [记录](baselines/multiple-subtitles-failures.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-02-failures.md)
- PKG-MULTI-SUB-03: [记录](baselines/multiple-subtitles-resources.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-03-resources.md)
- PKG-MULTI-SUB-04: [记录](baselines/multiple-subtitles-runtime-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-runtime-types.md)
- PKG-MULTI-SUB-07: [记录](baselines/multiple-subtitles-timestamps.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md)
- PKG-ADS-01: [记录](baselines/ads-release.json) [记录](baselines/ads-contract.md) [记录](baselines/ads-contract-validation.json) [记录](changes/2026-09-12-PKG-ADS-01-contract.md) [记录](scripts/ads-contract.test.mjs)
- PKG-ADS-02: [记录](changes/2026-09-12-PKG-ADS-02-tests.md) [记录](ads-validation.md) [记录](baselines/ads-validation.json)
- PKG-ADS-03: [记录](changes/2026-09-12-PKG-ADS-03-lifecycle.md) [记录](ads-validation.md) [记录](baselines/ads-lifecycle-validation.json)
+16
View File
@@ -1,5 +1,21 @@
# 进度与证据
## PKG-MULTI-SUB-07 内嵌时间戳与显示修复完成
修复数字时间戳被文本包装成 NaN;合法 VTT class 标记保留原生 processing instruction,
caption.ts 在既有事件后只展开专用标记,保留后续节点及用户文字,销毁移除监听。
覆盖顶层/嵌套、开始/中间/末尾、多个毫秒时间戳、冻结树和重复选择;私有类型不再
允许字符串时间戳。源码/main/legacy 各 37 项通过,本包 267 项通过,双 TS 编译器各
9 个负例准确拒绝。三入口原生各 36 项共 108 项通过,原生时间精确为 1.250/2.000/3.500 秒。
最初裸标签可见、删除标记丢后文及 vendor 实体分号问题均保留了实际失败报告。
时间戳修复后完整 CI 2337 项通过(1987 单元、14 工程、336 基线),严格生产 TS 353 文件。
正常构建/ESM 入口通过,dist/docs 副本一致;公开声明、依赖、版本和 vendor 源码未改。
TIMESTAMP 风险关闭;九份历史实体分号问题新增 ENTITY 风险,归 05 独立修复。
见[变更](changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md)和[验证](baselines/multiple-subtitles-timestamps.json)。
223 项:121 done、14 doing、88 todo;198 项风险。独立本地提交,不推送、不发布。
下一步继续 MULTI-SUB-04 公开声明/异步及旧模块消费者兼容;实体解码和完整设备组合仍待 05。
## PKG-MULTI-SUB-04 严格运行时 TS 检查点(doing)
五个自有模块迁为 TS,新增明确的 Option/Promise Result/Track/host/lifetime 类型;
+2 -1
View File
@@ -200,4 +200,5 @@
| MULTI-SUB-EXPORT-01 | open / 已复现 | Older CommonJS default objects conflict with export-assignment declarations; latest exports a direct factory | PKG-MULTI-SUB-04, PKG-MULTI-SUB-06 |
| MULTI-SUB-HTTP-01 | resolved / 已复现 | HTTP error responses with parseable bodies are installed as subtitles | PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
| MULTI-SUB-HOST-01 | resolved / 已复现 | Subtitle init rejections are ignored and failed installation or URL replacement leaks or invalidates resources | PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
| MULTI-SUB-TIMESTAMP-01 | open / 已复现 | Text wrappers replace numeric inline cue timestamps and serialize NaN times | PKG-MULTI-SUB-07, PKG-MULTI-SUB-05 |
| MULTI-SUB-TIMESTAMP-01 | resolved / 已复现 | Text wrappers replace numeric inline cue timestamps and serialize NaN times | PKG-MULTI-SUB-07, PKG-MULTI-SUB-05 |
| MULTI-SUB-ENTITY-01 | open / 已复现 | Vendored cue entity decoding retains semicolons after lt, gt and amp entities | PKG-MULTI-SUB-05 |
+31 -3
View File
@@ -4403,7 +4403,7 @@
"id": "MULTI-SUB-TIMESTAMP-01",
"title": "Text wrappers replace numeric inline cue timestamps and serialize NaN times",
"confirmation": "reproduced",
"status": "open",
"status": "resolved",
"owners": [
"PKG-MULTI-SUB-07",
"PKG-MULTI-SUB-05"
@@ -4411,10 +4411,38 @@
"evidence": [
"refactor/baselines/multiple-subtitles-runtime-types.json",
"refactor/changes/2026-09-13-PKG-MULTI-SUB-04-runtime-types.md",
"test/multiple-subtitles-failures.test.js"
"test/multiple-subtitles-failures.test.js",
"refactor/baselines/multiple-subtitles-timestamps.json",
"refactor/changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md",
"test/multiple-subtitles-merge.test.js",
"test/browser/multiple-subtitles-lifecycle.spec.js"
],
"compatibleResolution": "Preserve numeric timestamp nodes while retaining subtitle text, track wrappers and selection contracts; record the intentional repair of invalid legacy output.",
"closureCriteria": "Valid inline timestamps and surrounding text survive serialization and native subtitle playback; frozen historical failure remains reproducible and ordinary markup regressions pass."
"closureCriteria": "Valid inline timestamps and surrounding text survive serialization and native subtitle playback; frozen historical failure remains reproducible and ordinary markup regressions pass.",
"resolutionEvidence": [
"refactor/baselines/multiple-subtitles-timestamps.json",
"refactor/changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md",
"test/multiple-subtitles-merge.test.js",
"test/browser/multiple-subtitles-lifecycle.spec.js",
"packages/artplayer-plugin-multiple-subtitles/src/caption.ts"
],
"resolutionRationale": "Numeric timestamp nodes serialize inside valid VTT class markers, preserve native processing instructions, and are unwrapped only in the custom caption DOM without deleting subsequent text. Source/main/legacy tests, frozen-tree and timestamp cases, strict negative fixtures, and 108 native cases across both core fixtures pass. Public cue edits verify literal text preservation independently of the unresolved vendor entity bug."
},
{
"id": "MULTI-SUB-ENTITY-01",
"title": "Vendored cue entity decoding retains semicolons after lt, gt and amp entities",
"confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-MULTI-SUB-05"
],
"evidence": [
"test/multiple-subtitles-failures.test.js",
"refactor/baselines/multiple-subtitles-timestamps.json",
"refactor/changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md"
],
"compatibleResolution": "Independently fix valid named-entity decoding, documenting the vendor patch and preserving provenance checks and ordinary cue behavior.",
"closureCriteria": "Nine historical implementations reproduce the defect; candidate entity/markup regressions and real subtitle display pass, with vendor adaptation and provenance documentation updated."
}
]
}
@@ -32,6 +32,6 @@ for (const compiler of [ts, runtimeCompat]) {
}
assert.deepEqual(diagnostics(false), [])
const rejected = diagnostics(true)
assert.equal(rejected.length, 8, JSON.stringify(rejected))
assert.equal(rejected.length, 9, JSON.stringify(rejected))
})
}
+5 -2
View File
@@ -2338,11 +2338,14 @@
"dependsOn": [
"PKG-MULTI-SUB-03"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "保留数字时间戳节点、修正无效 NaN 输出、明确旧/新字幕输出差异",
"acceptance": "旧九份实现可复现,候选有效内嵌时间戳及文字/CSS/原生字幕通过,公开工厂与选择接口不变",
"evidence": []
"evidence": [
"baselines/multiple-subtitles-timestamps.json",
"changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md"
]
},
{
"id": "PKG-MULTI-SUB-05",
@@ -166,4 +166,49 @@ for (const core of ['published', 'candidate']) {
await page.evaluate(() => window.art.destroy(false))
await evidence(page, testInfo, core, { requests, nativeSourceSwitch: true })
})
test(`Multiple subtitles candidate ${core} core: inline timestamps remain native instructions without visible time tags`, async ({ page }, testInfo) => {
const timed = 'WEBVTT\n\n00:00.000 --> 00:08.000\n<00:01.250>Before <00:02.000> after<00:03.500>\n'
await page.route('**/test/multiple-timed.vtt', route => route.fulfill({ body: timed, contentType: 'text/vtt' }))
await prepare(page, core)
await page.addStyleTag({ content: '.art-subtitle-timed { color: rgb(255, 0, 0); }' })
await page.evaluate(async () => {
await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-timed.vtt', name: 'timed' }] }))
})
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1)
const native = await page.evaluate(() => {
const cue = window.art.subtitle.cues[0]
const fragment = cue.getCueAsHTML()
const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_PROCESSING_INSTRUCTION)
const timestamps = []
while (walker.nextNode()) timestamps.push(walker.currentNode.nodeValue)
return { text: cue.text, timestamps }
})
expect(native.text).toContain('<00:02.000>')
expect(native.text).not.toContain('NaN')
expect(native.timestamps).toHaveLength(3)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
await page.locator('#pause').click()
await expect(page.locator('.art-subtitle-timed').first()).toHaveCSS('color', 'rgb(255, 0, 0)')
const timedLine = page.locator('.art-subtitle-line').filter({ has: page.locator('.art-subtitle-timed') })
const visible = await timedLine.textContent()
expect(visible).toContain('Before')
expect(visible).toContain('after')
expect(visible).not.toMatch(/<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/)
// Public cue edits exercise literal display without relying on the vendor's entity decoder.
await page.evaluate(() => {
window.art.subtitle.cues[0].text += '<div class="art-subtitle-literal">Literal &lt;00:02.000&gt; example</div>'
window.art.subtitle.update()
})
await expect(page.locator('.art-subtitle-literal')).toHaveText('Literal <00:02.000> example')
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks([]))
await expect(page.locator('.art-subtitle-timed')).toHaveCount(0)
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
await expect(timedLine).toContainText('Before')
await expect(timedLine).not.toContainText(/<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/)
await expect(page.locator('.art-subtitle-literal')).toHaveCount(0)
await page.evaluate(() => window.art.destroy(false))
await evidence(page, testInfo, core, { native, visible })
})
}
+6
View File
@@ -263,4 +263,10 @@ for (const implementation of implementations) {
await env.factory(options(subtitle()))(env.art)
assert((await env.latestText()).includes('<NaN:NaN.NaN>'))
})
test(`Multiple-subtitles ${implementation.name} failure baseline: entity decoding retains stray semicolons`, async () => {
const env = create({ responses: { 'a.vtt': subtitleVtt('Literal &lt;00:02.000&gt; example &amp;') } })
await env.factory(options(subtitle()))(env.art)
assert(env.utils.unescape(await env.latestText()).includes('Literal <;00:02.000>; example &;'))
})
}
+1 -1
View File
@@ -21,7 +21,7 @@ async function flush() {
}
function empty(env) {
assert.equal(env.liveBlobs.size, 0)
assert.equal(env.listeners.get('destroy')?.size || 0, 0)
for (const listeners of env.listeners.values()) assert.equal(listeners.size, 0)
}
for (const abortController of [true, false]) {
+45
View File
@@ -74,3 +74,48 @@ test('Multiple subtitles request: real core SRT/ASS conversion and encoded text
for (const value of ['SRT text', 'ASS text', 'café', '日本語']) assert(text.includes(value))
current.emit('destroy')
})
for (const [text, timestamps] of [
['Before <00:02.000> after', ['<00:02.000>']],
['<00:01.250> one <00:03.500> two', ['<00:01.250>', '<00:03.500>']],
['end <00:07.000>', ['<00:07.000>']],
['甲<00:02.000>乙', ['<00:02.000>']],
['<b>bold <00:02.000> after</b>', ['<00:02.000>']],
]) {
test(`Multiple subtitles timestamps: valid nodes survive selection and reset (${text})`, async () => {
const input = `WEBVTT\n\n00:00.000 --> 00:08.000\n${text}\n`
const env = multipleSubtitlesEnvironment(candidate, { responses: { 'a.vtt': input, 'b.vtt': subtitleVtt('Other') } })
const result = await env.factory({ subtitles: [{ url: 'a.vtt', name: 'a' }, { url: 'b.vtt', name: 'b' }] })(env.art)
result.tracks(['a'])
const selected = await env.latestText()
for (const timestamp of timestamps) assert(selected.includes(timestamp))
assert(!selected.includes('NaN'))
const reparsed = parseTracks([selected], [{ name: 'reparsed' }])
assert.deepEqual(reparsed[0].errors, [])
const numbers = []
function collect(nodes) {
for (const node of nodes) {
if (node.type === 'timestamp')
numbers.push(node.value)
if (node.children)
collect(node.children)
}
}
collect(reparsed[0].cues[0].tree.children)
assert.equal(numbers.length, timestamps.length)
result.tracks(['b'])
assert(!(await env.latestText()).includes(timestamps[0]))
result.reset()
for (const timestamp of timestamps) assert((await env.latestText()).includes(timestamp))
env.emit('destroy')
})
}
test('Multiple subtitles timestamps: frozen numeric nodes stay unchanged across duplicate selection', () => {
const trees = freeze(parseTracks(['WEBVTT\n\n00:00.000 --> 00:08.000\nBefore <00:02.000> after\n'], [{ name: 'a' }]))
const original = JSON.stringify(trees)
const output = serializeTracks([trees[0], trees[0]])
assert.equal((output.match(/<00:02\.000>/g) || []).length, 2)
assert.equal((output.match(/<c\.artplayer-multiple-subtitles-timestamp>/g) || []).length, 2)
assert.equal(JSON.stringify(trees), original)
})
+4 -1
View File
@@ -1,5 +1,5 @@
import type Artplayer from 'artplayer'
import type { CueNode, TimestampNode } from '../../packages/artplayer-plugin-multiple-subtitles/src/parser'
import type { CueNode, TimestampNode, WrappedNode } from '../../packages/artplayer-plugin-multiple-subtitles/src/parser'
import type { Option, Result } from '../../packages/artplayer-plugin-multiple-subtitles/src/types'
import factory from '../../packages/artplayer-plugin-multiple-subtitles/src/index'
import createLifetime from '../../packages/artplayer-plugin-multiple-subtitles/src/lifetime'
@@ -52,6 +52,9 @@ void timestamp
// @ts-expect-error Parsed timestamps are numbers, not wrapped serialized values.
const wrongTimestamp: TimestampNode = { type: 'timestamp', value: '2' }
void wrongTimestamp
// @ts-expect-error Serialized timestamp nodes must also keep numeric values.
const wrongWrapped: WrappedNode = { type: 'timestamp', value: '<div>2</div>' }
void wrongWrapped
const trees = parseTracks(['WEBVTT\n\n'], [{}])
const vtt: string = serializeTracks(trees)
void vtt