fix(multiple-subtitles): [PKG-MULTI-SUB-08] preserve entity text and semantic cue wrappers

This commit is contained in:
Harvey Zhao committed 2026-09-14 12:14:21 +08:00
1 parent 4acf85ea69
commit 675b095b27
22 files changed
+744 -59

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],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
!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 N=new i(x.text,p,a,t);x.tree=N.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,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
!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],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=[],a=Line truncated
@@ -799,19 +799,36 @@ var WebVTTSerializer = function() {
return result;
};
};
const cueEntities = {
"&amp": "&",
"&amp;": "&",
"&lt": "<",
"&lt;": "<",
"&gt": ">",
"&gt;": ">",
"&lrm": "‎",
"&lrm;": "‎",
"&rlm": "‏",
"&rlm;": "‏",
"&nbsp": " ",
"&nbsp;": " "
};
function escapeCueText(value) {
return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
const TIMESTAMP_CLASS = "artplayer-multiple-subtitles-timestamp";
function markTimestamp(node) {
return { type: "object", name: "c", classes: [TIMESTAMP_CLASS], children: [node] };
}
function markNestedTimestamps(node) {
function prepareCueNode(node) {
if (node.type === "timestamp")
return markTimestamp(node);
if (node.type === "object")
return { ...node, children: node.children.map(markNestedTimestamps) };
return node;
return { ...node, children: node.children.map(prepareCueNode) };
return { ...node, value: escapeCueText(node.value) };
}
function parseTracks(vtts, subtitles) {
const parser = new WebVTTParser();
const parser = new WebVTTParser(cueEntities);
return vtts.map((vtt, index2) => {
const tree = parser.parse(vtt, "metadata");
return { ...tree, url: subtitles[index2].url, name: subtitles[index2].name };
@@ -822,16 +839,21 @@ function serializeTracks(trees) {
for (const selected of trees) {
const tree = selected;
for (const cue of tree.cues) {
const children = [];
for (const child of cue.tree.children) {
if (child.type === "timestamp")
children.push(markTimestamp(child));
else children.push(
{ type: "text", value: `<div class="art-subtitle-${tree.name}">` },
prepareCueNode(child),
{ type: "text", value: "</div>" }
);
}
cues.push({
...cue,
tree: {
...cue.tree,
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 };
})
children
}
});
}
+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-merge.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-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-merge.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-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",
@@ -19,6 +19,10 @@ to npm 1.2.0; the runtime entry requires an option object but allows omitted `su
to the existing core utils. Failure aborts the request; registration failure closes siblings.
- `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
semicolon-terminated references and historical lenient aliases. It also protects
literal cue text through the later onVttLoad unescape and HTML caption parsing.
No vendor implementation patch is needed; parser.js and its provenance remain intact.
- `src/lifetime.ts` owns the destroy listener, request cancellation and resource callbacks.
Its cancellation Promise also settles pending operations without AbortController support.
- `src/render.ts` owns generated Blob URLs and synchronous/async host installation outcomes.
@@ -47,7 +51,7 @@ to npm 1.2.0; the runtime entry requires an option object but allows omitted `su
All tracks download concurrently and decode through TextDecoder. Explicit type wins over
the URL extension; SRT/ASS conversion delegates to existing core utils. `onParser` appears
in declarations but is not called. Parsing runs in metadata mode. Each track's top-level
cue text receives a name-based div in a copied cue; merging concatenates cue arrays by track
cue node receives a name-based div in a copied cue; merging concatenates cue arrays by track
order, whereas 1.0.0 merged by cue index and retained the first track's timestamps.
Each selection serializes VTT and allocates a text/vtt Blob before replacing the preceding
@@ -73,15 +77,27 @@ and unwraps its remaining children. HTML does not pair `<c.class>` with `</c>`;
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.
The vendor default entity table still has its historical semicolon bug, preserved
in the unmodified vendor file and its upstream parity tests. The plugin now supplies
exact amp/lt/gt/lrm/rlm/nbsp references through the existing constructor option.
Historical partial/bare-name handling remains via the old aliases; numeric and
unknown references are left to the parser. Entity decoding runs once, not repeatedly.
Literal text is escaped separately from semantic cue tags before the serializer's
outer escaping. The renderer's existing onVttLoad unescape removes only that outer
layer, leaving escaped literal tags/timestamps for native VTT and HTML display.
Wrappers are sibling text nodes around the prepared node; they are never assigned
to a b/i/c/v/ruby node's annotation field. This fixes malformed tag attributes and
visible undefined while preserving CSS hooks, actual markup and numeric timestamps.
Native/custom display, nested entities and selection/reset regressions belong to
PKG-MULTI-SUB-08; it does not complete all historical core/device combinations.
## Vendor boundary and validation
The comparison revision is w3c/webvtt.js `380cfcce34ba8b472d3a31474874eb72a0e5f460`.
This is not proof of the original acquisition commit. Its execution body matches local
code after replacing the IIFE/global exports with named ESM exports and normalizing format.
The plugin-side entity table is a configuration adapter, not a change to that body.
Older shipped sources keep the IIFE. Preserve the CC0 source header and complete
THIRD_PARTY_NOTICES; the normal build places notices in all standalone bundle headers.
@@ -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],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
!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 N=new i(x.text,p,a,t);x.tree=N.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,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
!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],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=[],a=Line truncated
@@ -799,19 +799,36 @@ var WebVTTSerializer = function() {
return result;
};
};
const cueEntities = {
"&amp": "&",
"&amp;": "&",
"&lt": "<",
"&lt;": "<",
"&gt": ">",
"&gt;": ">",
"&lrm": "‎",
"&lrm;": "‎",
"&rlm": "‏",
"&rlm;": "‏",
"&nbsp": " ",
"&nbsp;": " "
};
function escapeCueText(value) {
return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
const TIMESTAMP_CLASS = "artplayer-multiple-subtitles-timestamp";
function markTimestamp(node) {
return { type: "object", name: "c", classes: [TIMESTAMP_CLASS], children: [node] };
}
function markNestedTimestamps(node) {
function prepareCueNode(node) {
if (node.type === "timestamp")
return markTimestamp(node);
if (node.type === "object")
return { ...node, children: node.children.map(markNestedTimestamps) };
return node;
return { ...node, children: node.children.map(prepareCueNode) };
return { ...node, value: escapeCueText(node.value) };
}
function parseTracks(vtts, subtitles) {
const parser = new WebVTTParser();
const parser = new WebVTTParser(cueEntities);
return vtts.map((vtt, index2) => {
const tree = parser.parse(vtt, "metadata");
return { ...tree, url: subtitles[index2].url, name: subtitles[index2].name };
@@ -822,16 +839,21 @@ function serializeTracks(trees) {
for (const selected of trees) {
const tree = selected;
for (const cue of tree.cues) {
const children = [];
for (const child of cue.tree.children) {
if (child.type === "timestamp")
children.push(markTimestamp(child));
else children.push(
{ type: "text", value: `<div class="art-subtitle-${tree.name}">` },
prepareCueNode(child),
{ type: "text", value: "</div>" }
);
}
cues.push({
...cue,
tree: {
...cue.tree,
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 };
})
children
}
});
}
@@ -1,6 +1,7 @@
import type { Cue, CueNode, TimestampNode, WrappedNode } from './parser'
import type { Track, TrackOption } from './types'
import { WebVTTParser, WebVTTSerializer } from './parser'
import { cueEntities, escapeCueText } from './text'
export const TIMESTAMP_CLASS = 'artplayer-multiple-subtitles-timestamp'
@@ -8,16 +9,16 @@ function markTimestamp(node: TimestampNode): CueNode {
return { type: 'object', name: 'c', classes: [TIMESTAMP_CLASS], children: [node] }
}
function markNestedTimestamps(node: CueNode): CueNode {
function prepareCueNode(node: CueNode): CueNode {
if (node.type === 'timestamp')
return markTimestamp(node)
if (node.type === 'object')
return { ...node, children: node.children.map(markNestedTimestamps) }
return node
return { ...node, children: node.children.map(prepareCueNode) }
return { ...node, value: escapeCueText(node.value) }
}
export function parseTracks(vtts: readonly string[], subtitles: readonly TrackOption[]): Track[] {
const parser = new WebVTTParser()
const parser = new WebVTTParser(cueEntities)
return vtts.map((vtt, index) => {
const tree = parser.parse(vtt, 'metadata')
// Options and completed downloads share the same Promise.all index.
@@ -31,18 +32,21 @@ export function serializeTracks(trees: readonly (Track | undefined)[]): string {
// Unknown names retain the historical TypeError at the property access below.
const tree = selected!
for (const cue of tree.cues) {
const children: CueNode[] = []
for (const child of cue.tree.children) {
if (child.type === 'timestamp')
children.push(markTimestamp(child))
else children.push(
{ type: 'text', value: `<div class="art-subtitle-${tree.name}">` },
prepareCueNode(child),
{ type: 'text', value: '</div>' },
)
}
cues.push({
...cue,
tree: {
...cue.tree,
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 }
}),
children,
},
})
}
@@ -0,0 +1,20 @@
// Keep the vendor's lenient aliases while making complete named references exact.
export const cueEntities = {
'&amp': '&',
'&amp;': '&',
'&lt': '<',
'&lt;': '<',
'&gt': '>',
'&gt;': '>',
'&lrm': '\u200E',
'&lrm;': '\u200E',
'&rlm': '\u200F',
'&rlm;': '\u200F',
'&nbsp': '\u00A0',
'&nbsp;': '\u00A0',
}
// Serialization protects the whole VTT once; this protects cue text after onVttLoad unescape.
export function escapeCueText(value: string): string {
return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
@@ -0,0 +1,351 @@
{
"task": "PKG-MULTI-SUB-08",
"date": "2026-09-14",
"baselineCommit": "4acf85ea696a99f05fce71089711bc4a4ca0d5b6",
"node": "v24.21.0",
"yarn": "1.22.22",
"platform": "win32",
"before": {
"artifact": {
"path": "refactor/.cache/multiple-subtitles-entities-before.js",
"sha256": "8ad5ddfa1ee1d79f7183b7a5fade748ca2093ceba7b467ec19b5b79ad129a249"
},
"initial": {
"path": "refactor/.cache/multi-entities-before.log",
"sha256": "cbe8d2bbb32db2682856e35ede9dd5600e9280b04efa148d2f75aea69b8eb716",
"passed": 0,
"failed": 5
},
"final": {
"path": "refactor/.cache/multi-entities-before-final.log",
"sha256": "0c56f858e5b0b42fb208f7505423b85a2013385bedf84aa9bb5f2749b3c71682",
"passed": 3,
"failed": 8
}
},
"sources": [
{
"path": "packages/artplayer-plugin-multiple-subtitles/src/merge.ts",
"sha256": "e41514f3a62833d9e42bee20e693b5a13fb42cfb3ea2a08f0821bbcb2d450cfd"
},
{
"path": "packages/artplayer-plugin-multiple-subtitles/src/text.ts",
"sha256": "15660d74726e36831939799d72e8126ede3e3becd22ba9cc2127db4277171d0f"
}
],
"tests": [
{
"path": "test/multiple-subtitles-entities.test.js",
"sha256": "23ff2ca0561a26347bb2602c1fb69b34bf5d664ad23c5c8877bccb383e66861e"
},
{
"path": "test/multiple-subtitles-merge.test.js",
"sha256": "c7a7ac447ad76e559d5710a590054599412806db6f4282fd2a856fa05092f533"
},
{
"path": "test/multiple-subtitles-failures.test.js",
"sha256": "d987af07a2c42ade51c6bd7b8eb2441191e748e5ddf7f3be5cbf4809a8be034f"
},
{
"path": "test/browser/multiple-subtitles-entities.spec.js",
"sha256": "cdc9dfc23900761c8110581ee81ad9f17c5796e03190c154b21cbdcb686c806e"
}
],
"vendor": {
"unchangedFromBaseline": true,
"sha256LF": "ba582074d57fa2d72880e6e4253fe14646787916b2b68e2a1e748e685cf96ab1",
"check": {
"provenance": {
"revision": "380cfcce34ba8b472d3a31474874eb72a0e5f460",
"packageVersionAtRevision": "2.1.2",
"scope": "Pinned comparison revision, not proof of historical acquisition commit. After removing IIFE/global exportify and adding named ESM exports, normalized executable content matches local parser.",
"sources": [
{
"file": "refactor/baselines/multiple-subtitles-vendor/parser.txt",
"upstream": "https://raw.githubusercontent.com/w3c/webvtt.js/380cfcce34ba8b472d3a31474874eb72a0e5f460/parser.js",
"sha256": "35a112e585b7cb1b4fd30350b0e8c8f6d1efa2670a6008c4a6038e07c9f8d727"
},
{
"file": "refactor/baselines/multiple-subtitles-vendor/license.txt",
"upstream": "https://raw.githubusercontent.com/w3c/webvtt.js/380cfcce34ba8b472d3a31474874eb72a0e5f460/LICENSE.md",
"sha256": "6cc655a5df52fefac69058311c090f4e70b7c37b8f6488f9498420a930b8275d"
},
{
"file": "refactor/baselines/multiple-subtitles-vendor/package.json.txt",
"upstream": "https://raw.githubusercontent.com/w3c/webvtt.js/380cfcce34ba8b472d3a31474874eb72a0e5f460/package.json",
"sha256": "d0102b9276839f1136ecafd9527102a28b44643e8ff403b3ddb1e51c04777a7b"
}
]
},
"normalizedSha256": "e71dc5073db409dcb8eef66a0d6bfe6c8d96fa7d157ff51a68decf6f05a7c80f",
"localSha256LF": "ba582074d57fa2d72880e6e4253fe14646787916b2b68e2a1e748e685cf96ab1",
"noticesSha256LF": "50755f38458d41151c7f0b1675c744aa365f0510dc11f53832691ef5c14ce6b6"
}
},
"results": [
{
"path": "refactor/.cache/multi-entities-suite-final.log",
"sha256": "2d38f2518283940e4d898823093a0959f6095c82883d97b1726874e20209beec",
"passed": 294,
"failed": 0,
"skipped": 0
},
{
"path": "refactor/.cache/multi-entities-main.log",
"sha256": "9eec0b8e567456842a4678da1ce57099c8c2faba98e0cc42b2d85e290afd037b",
"passed": 11,
"failed": 0,
"skipped": 0
},
{
"path": "refactor/.cache/multi-entities-legacy.log",
"sha256": "9eeb325f9df16ab584a8f722c4e31cc92411f776061d4ad5252bfcabbb830673",
"passed": 11,
"failed": 0,
"skipped": 0
},
{
"path": "refactor/.cache/multi-entities-ci.log",
"sha256": "c37a11d7fc5dca6e2dc84f308611fd26653a10d6b6528b62063f4aea5ccc58e6",
"passed": 50,
"failed": 0,
"skipped": 0
}
],
"browsers": [
{
"path": "refactor/.cache/multi-entities-source-browser/report.json",
"sha256": "accb4d48bda22f45b1ea8ece707532dbdc84bc31b65b787340ad1bf77b5fe4a3",
"stats": {
"startTime": "2026-09-14T04:03:29.211Z",
"duration": 50430.384999999995,
"expected": 42,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"observations": [
{
"project": "chromium",
"core": "published",
"browser": "153.0.8010.12",
"pluginSha256": "475afec2cd440de885520c6aee4165a7453fce8e03a920a7d809b8fe93db510c",
"native": [
"A & < > ‎ ‏   | <b>literal</b> | &lt;",
"bold & safe tail <i>"
],
"literal": "A & < > ‎ ‏   | <b>literal</b> | &lt;",
"scope": "Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only"
},
{
"project": "chromium",
"core": "candidate",
"browser": "153.0.8010.12",
"pluginSha256": "475afec2cd440de885520c6aee4165a7453fce8e03a920a7d809b8fe93db510c",
"native": [
"A & < > ‎ ‏   | <b>literal</b> | &lt;",
"bold & safe tail <i>"
],
"literal": "A & < > ‎ ‏   | <b>literal</b> | &lt;",
"scope": "Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only"
},
{
"project": "firefox",
"core": "published",
"browser": "155.0",
"pluginSha256": "475afec2cd440de885520c6aee4165a7453fce8e03a920a7d809b8fe93db510c",
"native": [
"A & < > ‎ ‏   | <b>literal</b> | &lt;",
"bold & safe tail <i>"
],
"literal": "A & < > ‎ ‏   | <b>literal</b> | &lt;",
"scope": "Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only"
},
{
"project": "firefox",
"core": "candidate",
"browser": "155.0",
"pluginSha256": "475afec2cd440de885520c6aee4165a7453fce8e03a920a7d809b8fe93db510c",
"native": [
"A & < > ‎ ‏   | <b>literal</b> | &lt;",
"bold & safe tail <i>"
],
"literal": "A & < > ‎ ‏   | <b>literal</b> | &lt;",
"scope": "Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only"
},
{
"project": "webkit",
"core": "published",
"browser": "26.6",
"pluginSha256": "475afec2cd440de885520c6aee4165a7453fce8e03a920a7d809b8fe93db510c",
"native": [
"A & < > ‎ ‏   | <b>literal</b> | &lt;",
"bold & safe tail <i>"
],
"literal": "A & < > ‎ ‏   | <b>literal</b> | &lt;",
"scope": "Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only"
},
{
"project": "webkit",
"core": "candidate",
"browser": "26.6",
"pluginSha256": "475afec2cd440de885520c6aee4165a7453fce8e03a920a7d809b8fe93db510c",
"native": [
"A & < > ‎ ‏   | <b>literal</b> | &lt;",
"bold & safe tail <i>"
],
"literal": "A & < > ‎ ‏   | <b>literal</b> | &lt;",
"scope": "Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only"
}
],
"scope": "Source entity and lifecycle tests; final equivalent wrapper loop separately covered by full Node and actual artifact browser run"
},
{
"path": "refactor/.cache/multi-entities-artifact-browser/report.json",
"sha256": "0f8d3c53fd5616814ee9bf9cba1afb197dd3b956c0b5e84a8ce9cb390fcc1140",
"stats": {
"startTime": "2026-09-14T04:07:08.987Z",
"duration": 15602.046,
"expected": 6,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"observations": [
{
"project": "chromium",
"core": "published",
"browser": "153.0.8010.12",
"pluginSha256": "e2192514ab4836a04c56420fa8c260efa657e2afa32f299d0cf7786c91adc39b",
"native": [
"A & < > ‎ ‏   | <b>literal</b> | &lt;",
"bold & safe tail <i>"
],
"literal": "A & < > ‎ ‏   | <b>literal</b> | &lt;",
"scope": "Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only"
},
{
"project": "chromium",
"core": "candidate",
"browser": "153.0.8010.12",
"pluginSha256": "e2192514ab4836a04c56420fa8c260efa657e2afa32f299d0cf7786c91adc39b",
"native": [
"A & < > ‎ ‏   | <b>literal</b> | &lt;",
"bold & safe tail <i>"
],
"literal": "A & < > ‎ ‏   | <b>literal</b> | &lt;",
"scope": "Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only"
},
{
"project": "firefox",
"core": "published",
"browser": "155.0",
"pluginSha256": "e2192514ab4836a04c56420fa8c260efa657e2afa32f299d0cf7786c91adc39b",
"native": [
"A & < > ‎ ‏   | <b>literal</b> | &lt;",
"bold & safe tail <i>"
],
"literal": "A & < > ‎ ‏   | <b>literal</b> | &lt;",
"scope": "Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only"
},
{
"project": "firefox",
"core": "candidate",
"browser": "155.0",
"pluginSha256": "e2192514ab4836a04c56420fa8c260efa657e2afa32f299d0cf7786c91adc39b",
"native": [
"A & < > ‎ ‏   | <b>literal</b> | &lt;",
"bold & safe tail <i>"
],
"literal": "A & < > ‎ ‏   | <b>literal</b> | &lt;",
"scope": "Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only"
},
{
"project": "webkit",
"core": "published",
"browser": "26.6",
"pluginSha256": "e2192514ab4836a04c56420fa8c260efa657e2afa32f299d0cf7786c91adc39b",
"native": [
"A & < > ‎ ‏   | <b>literal</b> | &lt;",
"bold & safe tail <i>"
],
"literal": "A & < > ‎ ‏   | <b>literal</b> | &lt;",
"scope": "Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only"
},
{
"project": "webkit",
"core": "candidate",
"browser": "26.6",
"pluginSha256": "e2192514ab4836a04c56420fa8c260efa657e2afa32f299d0cf7786c91adc39b",
"native": [
"A & < > ‎ ‏   | <b>literal</b> | &lt;",
"bold & safe tail <i>"
],
"literal": "A & < > ‎ ‏   | <b>literal</b> | &lt;",
"scope": "Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only"
}
],
"scope": "Actual main UMD, published/candidate core, native fragment and HTML caption text"
}
],
"artifacts": [
{
"path": "packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.js",
"sha256": "e2192514ab4836a04c56420fa8c260efa657e2afa32f299d0cf7786c91adc39b",
"docs": {
"path": "docs/compiled/artplayer-plugin-multiple-subtitles.js",
"sha256": "e2192514ab4836a04c56420fa8c260efa657e2afa32f299d0cf7786c91adc39b"
}
},
{
"path": "packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.legacy.js",
"sha256": "6e3716ea5e70a028b4fdb6648d1f8be151364bd6fd798719e3a5809924457c6e",
"docs": {
"path": "docs/compiled/artplayer-plugin-multiple-subtitles.legacy.js",
"sha256": "6e3716ea5e70a028b4fdb6648d1f8be151364bd6fd798719e3a5809924457c6e"
}
},
{
"path": "packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.mjs",
"sha256": "8d1f36140e6c4ca64d6330621fd421f1323ed9fb58434a797cfa2afae6205b39",
"docs": {
"path": "docs/compiled/artplayer-plugin-multiple-subtitles.mjs",
"sha256": "8d1f36140e6c4ca64d6330621fd421f1323ed9fb58434a797cfa2afae6205b39"
}
}
],
"checks": [
{
"path": "refactor/.cache/multi-entities-types.log",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"exitCode": 0
},
{
"path": "refactor/.cache/multi-entities-build.log",
"sha256": "d352d68782fddd9e62989746c6e4f1a8e0310069115f6d779bdd13663cddfbfc",
"exitCode": 0
},
{
"path": "refactor/.cache/multi-entities-lint-final.log",
"sha256": "c5b143fd109b076988b84670ce8fc536d4069edf71a512ad2990f926c63baaa1",
"exitCode": 0
},
{
"path": "refactor/.cache/multi-entities-final.log",
"sha256": "e3bd33003aa4d5b47edd8da0966a1830656a4279e3b1cf519541e11bff9509ba",
"exitCode": 0
}
],
"initialLintFailure": {
"path": "refactor/.cache/multi-entities-lint-initial.log",
"sha256": "9202a274e0cca24968f932d2fac2a31ba602f652897d083b454573042bd68772"
},
"scope": [
"Fix six existing named entities via configured vendor table; preserve lenient aliases and vendor body",
"Escape literal cue text through existing onVttLoad pipeline; wrap semantic nodes outside annotation fields",
"Nine historical implementations still reproduce entity and malformed-tag defects",
"Public types, factory alias, Promise/selection ordering unchanged",
"Desktop Windows engines only; full old-core/device/distribution gates remain tasks05/06",
"No new dependency, lock/version change, push or publication"
]
}
@@ -0,0 +1,79 @@
# PKG-MULTI-SUB-08: entity text and semantic tag wrapping
Baseline: 4acf85ea696a99f05fce71089711bc4a4ca0d5b6. This independent correction
precedes task05's complete core/combination acceptance; it does not complete that
task or publication readiness.
## Old behavior and corrections
Nine historical implementations reproduce `&lt;`/`&gt;`/`&amp;` decoding with stray
semicolons. The vendor's default table contains bare keys, while its exact-match
branch looks for the semicolon-terminated key and falls through to partial-match
handling. `text.ts` supplies both forms for the six existing named entities using
the parser's already-supported constructor option. Bare/partial-name aliases,
numeric references and unknown-reference handling retain the existing parser path.
No upstream/vendor execution body or provenance snapshot is edited.
Correctly decoded text must remain literal after the existing onVttLoad unescape.
Text values therefore receive a separate escaping layer before the serializer
protects the complete VTT. Literal `<b>` and timestamp-shaped text remain visible
text; `&amp;lt;` displays `&lt;` rather than decoding repeatedly. Real b/i/c/v/ruby
cue nodes remain semantic nodes, and inline timestamps retain their numeric data.
Related tag coverage exposed a second historical bug: the old merge assigned an
HTML wrapper to a semantic node's annotation field, yielding output such as
`<b <div ...>undefined</div>>bold</b>`. Nine historical implementations now reproduce
that exact malformed structure separately. Merge wraps prepared nodes with sibling
opening/closing text nodes instead. CSS hooks remain `.art-subtitle-<name>`; markup
annotations are preserved and source trees are still immutable across selections.
Plain-text/timing output remains byte-matched outside these intentional fixes.
## Structure, compatibility and ownership
- `text.ts`: entity configuration and literal-text protection through the two
existing serialization/HTML stages. It introduces no browser API or dependency.
- `merge.ts`: recursively prepare text/timestamp/object nodes and wrap top-level
nodes without changing annotations. A simple loop avoids requiring Array.flatMap
in older environments. Parsing/selection order and original cue times remain.
- Vendor parser/serializer, request/lifetime/render/caption code, public types,
factory/default alias, tracks/reset, Promise behavior and distribution paths
are unchanged. This is an authored adapter around a frozen vendor, not a vendor
upgrade or an unexplained relaxation of its fingerprint check.
There are no new resources or lifetime responsibilities. Existing download, URL,
destroy/restart and host-error checks still apply. Corrected entity/tag output is
an intentional bug fix; malformed attributes and visible semicolons are not kept
as compatibility requirements. Tests formerly comparing those erroneous serialized
bytes move to explicit corrected text/markup expectations. Raw ampersands and
malformed angle text retain a separate actual-old-versus-current visible-output
comparison. No historical failure fixture is rewritten to pretend old releases
were correct.
## Verification
Source/log/artifact hashes and actual counts are in
[multiple-subtitles-entities-validation.json](../baselines/multiple-subtitles-entities-validation.json).
- The initial5 entity/rendering tests fail against the frozen pre-change bundle.
The final expanded11-case file is rerun against both old and new artifacts.
- Full package suite294 passes, including11 focused cases, immutable tree and
timestamps,9 historical entity failures,9 historical malformed-tag failures,
actual release contracts/types and complete unchanged vendor parity/notices.
- Windows source browser suite42 passes: entity/markup native VTT and HTML display
on published/candidate cores, plus existing playback, SRT, timestamps, selection,
source replacement and cleanup. An early implementation used flatMap; the final
equivalent loop has full Node coverage and actual built-browser verification.
- Production main/legacy artifacts each run the focused Node suite; actual main
also runs entity display in all3 engines on both core variants. UMD/legacy/ESM
and matching docs copies are regenerated using the normal build.
- Strict package TypeScript passes. Initial Unicode escape casing/list formatting
lint errors are corrected; final root lint has only its one existing docs type
warning. CI and final plan/risk checks are recorded separately.
This is desktop engine evidence, not physical Safari/mobile, all historical core
versions or a remote CI run. Complete core combinations remain task05, distribution
and demo acceptance task06. No lockfile, dependency, public declaration or version
change. The package architecture and test maintenance documentation explain the
adapter and corrected behavior. Revert the dedicated task08 commit, including
generated outputs, to restore the prior implementation. Local commit only; no
push or npm publication.
+6 -4
View File
@@ -2,9 +2,9 @@
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 246 项,范围 22 个包及工作区/示例。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 247 项,范围 22 个包及工作区/示例。
状态:todo 56 / doing 16 / blocked 0 / done 174 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 56 / doing 16 / blocked 0 / done 175 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -28,7 +28,7 @@
| artplayer-plugin-document-pip | 1.1.0 | PKG-DPIP-01, PKG-DPIP-02, PKG-DPIP-03, PKG-DPIP-04, PKG-DPIP-05, PKG-DPIP-06 |
| artplayer-plugin-hls-control | 1.1.0 | PKG-HLS-01, PKG-HLS-02, PKG-HLS-03, PKG-HLS-04, PKG-HLS-SDK-01, PKG-HLS-05, PKG-HLS-06 |
| artplayer-plugin-jassub | 1.1.0 | PKG-JASSUB-01, PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-04, PKG-JASSUB-05, PKG-JASSUB-06, PKG-JASSUB-07, PKG-JASSUB-08, PKG-JASSUB-09 |
| artplayer-plugin-multiple-subtitles | 1.2.0 | PKG-MULTI-SUB-01, PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-04, PKG-MULTI-SUB-07, PKG-MULTI-SUB-05, PKG-MULTI-SUB-06 |
| artplayer-plugin-multiple-subtitles | 1.2.0 | PKG-MULTI-SUB-01, PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-04, PKG-MULTI-SUB-07, PKG-MULTI-SUB-08, PKG-MULTI-SUB-05, PKG-MULTI-SUB-06 |
| artplayer-plugin-vast | 1.2.0 | PKG-VAST-01, PKG-VAST-02, PKG-VAST-03, PKG-VAST-04, PKG-VAST-05, PKG-VAST-06 |
| artplayer-plugin-vtt-thumbnail | 1.1.0 | PKG-VTT-THUMB-01, PKG-VTT-THUMB-02, PKG-VTT-THUMB-03, PKG-VTT-THUMB-04, PKG-VTT-THUMB-05, PKG-VTT-THUMB-06 |
| artplayer-proxy-canvas | 1.1.0 | PKG-AMBILIGHT-PROXY-01, PKG-CANVAS-01, PKG-CANVAS-02, PKG-CANVAS-03, PKG-CANVAS-04, PKG-CANVAS-05, PKG-CANVAS-06, PKG-FACTORY-01 |
@@ -226,7 +226,8 @@
| 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 | done |
| 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-08 | artplayer-plugin-multiple-subtitles<br>修复字幕实体解码与嵌套标签包装 | PKG-MULTI-SUB-04, PKG-MULTI-SUB-07 | 使用解析器既有实体表入口修复多余分号;保护字面文本并以兄弟节点包装标签,保留CSS钩子和时间戳 | 冻结旧实现复现、实体/双重编码/标签/选择重置回归、新旧核心真实字幕显示通过;vendor原文与来源校验不变,类型/分发兼容 | M | done |
| PKG-MULTI-SUB-05 | artplayer-plugin-multiple-subtitles<br>验证新旧核心和组合 | PKG-MULTI-SUB-04, CORE-22, PKG-MULTI-SUB-07, PKG-MULTI-SUB-08 | 与核心字幕/偏移/全屏组合,旧名称调用不变 | 最终核心与原支持范围核心分别通过;设备/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 |
## 5 包迁移:artplayer-plugin-jassub
@@ -552,6 +553,7 @@
- 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) [记录](baselines/multiple-subtitles-public-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-public-types.md) [记录](type-compatibility-policy.md) [记录](baselines/multiple-subtitles-approved-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-approved-types.md)
- PKG-MULTI-SUB-07: [记录](baselines/multiple-subtitles-timestamps.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md)
- PKG-MULTI-SUB-08: [记录](changes/2026-09-14-PKG-MULTI-SUB-08-entities.md) [记录](baselines/multiple-subtitles-entities-validation.json)
- PKG-JASSUB-01: [记录](baselines/jassub-release.json) [记录](baselines/jassub-vendor.json) [记录](baselines/jassub-font-metadata.json) [记录](baselines/jassub-contract.md) [记录](baselines/jassub-contract-validation.json) [记录](changes/2026-09-13-PKG-JASSUB-01-baseline.md) [记录](baselines/jassub-provenance.json) [记录](baselines/jassub-provenance-validation.json) [记录](changes/2026-09-14-PKG-JASSUB-01-provenance.md)
- PKG-JASSUB-02: [记录](changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md) [记录](baselines/jassub-failures-validation.json)
- PKG-JASSUB-03: [记录](changes/2026-09-14-PKG-JASSUB-03-registration.md) [记录](baselines/jassub-registration-validation.json)
+13
View File
@@ -1,5 +1,18 @@
# 进度与证据
## PKG-MULTI-SUB-08 字幕实体与标签包装完成
通过解析器既有实体表入口修复标准命名实体后多余分号,保留原始vendor文件与
来源校验;独立转义字面文本,避免实体形式的标签/时间戳被再次解释。同步修复
把包装节点塞进b等标签注释字段的问题,改为节点外包装,保留CSS钩子与时间戳。
九套历史实现分别复现两类旧缺陷,最终11项新用例在旧bundle中8红3绿,候选及
实际main/legacy各11项全绿。完整插件294项、源码浏览器42项、实际main浏览器
6项、CI50项通过;严格TS与vendor校验通过,lint 1既有warning。三格式与docs
副本正常重建,公开类型/依赖/版本未改。详见[记录](changes/2026-09-14-PKG-MULTI-SUB-08-entities.md)
和[证据](baselines/multiple-subtitles-entities-validation.json)。关闭实体与标签包装
风险,247项变为175 done、16 doing、56 todo;这不完成全部旧核心/真机/分发
验收,下一步继续任务05组合验证。独立本地提交,无推送/发布。
## PKG-AUTO-THUMB-10 编码归属与取消时限完成
JPEG编码拆为独立TS模块,原来帧等待结束后编码无时限,现有独立30秒编码时限。
+2 -1
View File
@@ -201,7 +201,7 @@
| 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 | 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 |
| MULTI-SUB-ENTITY-01 | resolved / 已复现 | Vendored cue entity decoding retains semicolons after lt, gt and amp entities | PKG-MULTI-SUB-05, PKG-MULTI-SUB-08 |
| JASSUB-TYPE-01 | open / 已复现 | JASSUB declared required URLs, Promise methods and resize parameter order differ from actual historical behavior | PKG-JASSUB-04 |
| JASSUB-EXPORT-01 | open / 已复现 | JASSUB historical CommonJS object.default changes to a direct factory in 1.1.0 | PKG-JASSUB-04, PKG-JASSUB-06 |
| CORE-DEFAULTS-SSR-01 | resolved / 已复现 | Reading static defaults throws when navigator is absent | CORE-25 |
@@ -264,3 +264,4 @@
| SCAFFOLD-OUTPUT-01 | resolved / 已复现 | Plugin generation overwrites existing examples and links to a different example name | MOD-PLUGIN-01 |
| DEV-SERVOR-PORT-01 | resolved / 已复现 | Servor exits the entire dev process with status zero when the requested port is occupied | MOD-DEV-01 |
| AUTO-THUMB-ENCODE-TIMEOUT-01 | resolved / 源码/产物事实 | Pending JPEG encoding has no bounded lifetime after frame readiness finishes | PKG-AUTO-THUMB-10 |
| MULTI-SUB-MARKUP-01 | resolved / 已复现 | Semantic cue nodes receive HTML wrappers inside their annotation fields | PKG-MULTI-SUB-08 |
+36 -5
View File
@@ -4521,17 +4521,26 @@
"id": "MULTI-SUB-ENTITY-01",
"title": "Vendored cue entity decoding retains semicolons after lt, gt and amp entities",
"confirmation": "reproduced",
"status": "open",
"status": "resolved",
"owners": [
"PKG-MULTI-SUB-05"
"PKG-MULTI-SUB-05",
"PKG-MULTI-SUB-08"
],
"evidence": [
"test/multiple-subtitles-failures.test.js",
"refactor/baselines/multiple-subtitles-timestamps.json",
"refactor/changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md"
"refactor/changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md",
"refactor/changes/2026-09-14-PKG-MULTI-SUB-08-entities.md",
"refactor/baselines/multiple-subtitles-entities-validation.json"
],
"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."
"compatibleResolution": "Supply exact named entities through the existing parser constructor option, preserve historical lenient aliases and the frozen vendor body, and protect literal text through serialization/unescape.",
"closureCriteria": "Nine historical implementations reproduce the defect; candidate entity/markup regressions and real subtitle display pass, with vendor adaptation and provenance documentation updated.",
"workspaceState": "Nine historical implementations retain stray semicolons; candidate table adapter and text escaping fix six named references, double encoding and literal tags. Actual native VTT and HTML display pass in all3 Windows engines on published/candidate cores. Complete historical-core/device combinations remain task05.",
"resolutionEvidence": [
"refactor/changes/2026-09-14-PKG-MULTI-SUB-08-entities.md",
"refactor/baselines/multiple-subtitles-entities-validation.json"
],
"resolutionRationale": "Eleven focused cases,294 package tests,42 source browser and6 actual built-main browser cases pass. The parser executable body is unchanged and its full upstream adaptation/notices checks remain enforced; the authored configuration adapter resolves the plugin defect without a vendor patch."
},
{
"id": "JASSUB-TYPE-01",
@@ -5962,6 +5971,28 @@
"test/browser/auto-thumbnail-lifecycle.spec.js"
],
"resolutionRationale": "Nine controlled cases and real native JPEG tests cover held/late callbacks and decoder/canvas disposal. Actual production UMD passes11 Node and9 browser checks; previous preview survives timeout. Very slow encoding can now be discarded explicitly; public options, registration and frame-time formula remain intact."
},
{
"id": "MULTI-SUB-MARKUP-01",
"title": "Semantic cue nodes receive HTML wrappers inside their annotation fields",
"confirmation": "reproduced",
"status": "resolved",
"owners": [
"PKG-MULTI-SUB-08"
],
"evidence": [
"test/multiple-subtitles-failures.test.js",
"refactor/changes/2026-09-14-PKG-MULTI-SUB-08-entities.md",
"refactor/baselines/multiple-subtitles-entities-validation.json"
],
"compatibleResolution": "Wrap prepared cue nodes with sibling opening/closing nodes, retaining annotations, CSS hooks, nested timestamps and escaped literal text.",
"closureCriteria": "Nine historical implementations reproduce malformed b attributes and undefined text; candidate tag/text/selection/reset and real published/candidate core display pass.",
"workspaceState": "Nine frozen implementations serialize malformed b attributes containing an undefined wrapper. Authored merge now wraps nodes outside annotations. Vendor and public types are unchanged.",
"resolutionEvidence": [
"refactor/changes/2026-09-14-PKG-MULTI-SUB-08-entities.md",
"refactor/baselines/multiple-subtitles-entities-validation.json"
],
"resolutionRationale": "Historical regressions retain old output; corrected b/c/v/ruby and text cases plus actual b/literal-tag native/HTML display pass. Immutable trees, timestamps and lifecycle tests remain green."
}
]
}
+22 -1
View File
@@ -2377,6 +2377,26 @@
"changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md"
]
},
{
"id": "PKG-MULTI-SUB-08",
"phase": "5 包迁移:artplayer-plugin-multiple-subtitles",
"title": "修复字幕实体解码与嵌套标签包装",
"scope": [
"artplayer-plugin-multiple-subtitles"
],
"dependsOn": [
"PKG-MULTI-SUB-04",
"PKG-MULTI-SUB-07"
],
"status": "done",
"risk": "M",
"deliverable": "使用解析器既有实体表入口修复多余分号;保护字面文本并以兄弟节点包装标签,保留CSS钩子和时间戳",
"acceptance": "冻结旧实现复现、实体/双重编码/标签/选择重置回归、新旧核心真实字幕显示通过;vendor原文与来源校验不变,类型/分发兼容",
"evidence": [
"changes/2026-09-14-PKG-MULTI-SUB-08-entities.md",
"baselines/multiple-subtitles-entities-validation.json"
]
},
{
"id": "PKG-MULTI-SUB-05",
"phase": "5 包迁移:artplayer-plugin-multiple-subtitles",
@@ -2387,7 +2407,8 @@
"dependsOn": [
"PKG-MULTI-SUB-04",
"CORE-22",
"PKG-MULTI-SUB-07"
"PKG-MULTI-SUB-07",
"PKG-MULTI-SUB-08"
],
"status": "todo",
"risk": "H",
+8
View File
@@ -1,5 +1,13 @@
# Tests and fixture ownership
`test/multiple-subtitles-entities.test.js` checks named entities, literal tags,
double encoding, nested semantic tags and selection/reset through the real plugin.
The vendor parity suite stays unchanged: the plugin uses the parser's existing
entity-table option. Historical malformed-tag and stray-semicolon tests continue
to assert old defects; corrected cases have explicit rendering expectations instead
of treating those old serialized bytes as a required contract. Browser entity
tests compare native VTT fragments and actual HTML captions on both core variants.
`yarn test:library` runs actual library builds, build-input freshness checks and the
scaffold's generated consumers. The new build modules live in `scripts/library/`;
`yarn typecheck:library` checks them and the retained JS/MJS entrypoints. Browser
@@ -0,0 +1,42 @@
import { hash } from '../../refactor/scripts/releases.mjs'
import { multipleSubtitlesCandidate } from '../helpers/multiple-subtitles.js'
import { expect, test } from './fixtures.js'
const implementation = await multipleSubtitlesCandidate()
const literal = 'A & < > \u200E \u200F \u00A0 | <b>literal</b> | &lt;'
const vtt = text => `WEBVTT\n\n00:00.000 --> 00:08.000\n${text}\n`
for (const core of ['published', 'candidate']) {
test(`Multiple subtitles ${core} core decodes entities once and preserves literal and semantic tags`, async ({ page, browser }, testInfo) => {
const errors = []
page.on('pageerror', error => errors.push(error.message))
await page.route('**/test/entities-a.vtt', route => route.fulfill({ body: vtt('A &amp; &lt; &gt; &lrm; &rlm; &nbsp; | &lt;b&gt;literal&lt;/b&gt; | &amp;lt;'), contentType: 'text/vtt' }))
await page.route('**/test/entities-b.vtt', route => route.fulfill({ body: vtt('<b>bold &amp; safe</b> tail &lt;i&gt;'), contentType: 'text/vtt' }))
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: implementation.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: 'a', url: '/test/entities-a.vtt' }, { name: 'b', url: '/test/entities-b.vtt' }] }))
})
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
await page.locator('#pause').click()
await expect.poll(() => page.locator('.art-subtitle-a').textContent()).toBe(literal)
await expect(page.locator('.art-subtitle-a b')).toHaveCount(0)
await expect(page.locator('.art-subtitle-b b')).toHaveText('bold & safe')
await expect(page.locator('.art-subtitle-b i')).toHaveCount(0)
expect(await page.locator('.art-subtitle-b').allTextContents()).toEqual(['bold & safe', ' tail <i>'])
const native = await page.evaluate(() => window.art.subtitle.cues.map(cue => cue.getCueAsHTML().textContent))
expect(native).toEqual([literal, 'bold & safe tail <i>'])
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks(['a']))
await expect(page.locator('.art-subtitle-b')).toHaveCount(0)
await expect.poll(() => page.locator('.art-subtitle-a').textContent()).toBe(literal)
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
await expect(page.locator('.art-subtitle-b b')).toHaveText('bold & safe')
await testInfo.attach('entities.json', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), pluginSha256: hash(implementation.code), native, literal, scope: 'Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only' }) })
await page.evaluate(() => window.art.destroy(false))
expect(errors).toEqual([])
})
}
+51
View File
@@ -0,0 +1,51 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Text entity regressions use real built plugin serialization.
import test from 'node:test'
import { parseHTML } from 'linkedom'
import { multipleSubtitlesCandidate, multipleSubtitlesEnvironment, multipleSubtitlesHistorical, subtitleVtt } from './helpers/multiple-subtitles.js'
const implementation = await multipleSubtitlesCandidate()
for (const [input, expected] of [
['&amp; &lt; &gt; &lrm; &rlm; &nbsp;', '& < > \u200E \u200F \u00A0'],
['Literal &lt;00:02.000&gt; and &lt;b&gt;text&lt;/b&gt;', 'Literal <00:02.000> and <b>text</b>'],
['&amp;lt; &amp;amp; &amp;gt;', '&lt; &amp; &gt;'],
['One &amp;; two &lt;;', 'One &; two <;'],
['<b>bold &amp; safe</b> tail &lt;i&gt;', 'bold & safe tail <i>'],
['<c.red>color</c> <i>italic</i>', 'color italic'],
['<v Speaker>voice</v>', 'voice'],
['<ruby>text<rt>note</rt></ruby>', 'textnote'],
['&unknown; &amplitude; &amp', '&unknown; &litude; &'],
['&#65; &#x41;', 'A A'],
]) {
test(`Multiple subtitles entity text remains literal after selection/reset: ${input}`, async () => {
const env = multipleSubtitlesEnvironment(implementation, { responses: { 'a.vtt': subtitleVtt(input), 'b.vtt': subtitleVtt('Other') } })
const result = await env.factory({ subtitles: [{ name: 'a', url: 'a.vtt' }, { name: 'b', url: 'b.vtt' }] })(env.art)
for (const names of [['a'], ['b', 'a'], ['a', 'a']]) {
result.tracks(names)
const vtt = env.utils.unescape(await env.latestText())
const text = vtt.split(/\n\d[^\n]*-->[^\n]*\n/).slice(1).join('')
const { document } = parseHTML(`<html><body>${text}</body></html>`)
assert.equal(document.body.textContent.trim(), names.map(name => name === 'a' ? expected : 'Other').join('\n\n').trim())
if (input.includes('&lt;i&gt;'))
assert.equal(document.querySelectorAll('i').length, 0)
if (input.startsWith('<b>'))
assert.equal(document.querySelector('b').textContent, 'bold & safe')
}
result.reset()
assert((env.utils.unescape(await env.latestText())).includes('Other'))
env.emit('destroy')
})
}
test('Multiple subtitles raw ampersand and malformed angle text retain historical visible output', async () => {
const historical = (await multipleSubtitlesHistorical()).find(item => item.name === 'published-1.2.0-main')
const output = []
for (const code of [historical, implementation]) {
const env = multipleSubtitlesEnvironment(code, { responses: { 'a.vtt': subtitleVtt('A & B < C > D') } })
await env.factory({ subtitles: [{ name: 'a', url: 'a.vtt' }] })(env.art)
const { document } = parseHTML(`<html><body>${env.utils.unescape(await env.latestText())}</body></html>`)
output.push(document.body.textContent)
env.emit('destroy')
}
assert.equal(output[1], output[0])
})
+6
View File
@@ -269,4 +269,10 @@ for (const implementation of implementations) {
await env.factory(options(subtitle()))(env.art)
assert(env.utils.unescape(await env.latestText()).includes('Literal <;00:02.000>; example &;'))
})
test(`Multiple-subtitles ${implementation.name} failure baseline: semantic tag wrappers become malformed attributes`, async () => {
const env = create({ responses: { 'a.vtt': subtitleVtt('<b>bold</b>') } })
await env.factory(options(subtitle()))(env.art)
assert(env.utils.unescape(await env.latestText()).includes('<b <div class="art-subtitle-a">undefined</div>>bold</b>'))
})
}
+2 -6
View File
@@ -31,13 +31,9 @@ test('Multiple subtitles merge: repeated selection serializes frozen trees witho
assert.equal(JSON.stringify(trees), original)
})
test('Multiple subtitles merge: actual released serializer output is preserved for text, markup and timing cases', async () => {
test('Multiple subtitles merge: released serializer output is preserved outside corrected text escaping and tag wrappers', async () => {
const samples = [
subtitleVtt('A & B < C > D'),
subtitleVtt('<b>bold</b> tail'),
subtitleVtt('<c.red>color</c> <i>italic</i>'),
subtitleVtt('<v Speaker>voice</v>'),
subtitleVtt('<ruby>text<rt>note</rt></ruby>'),
subtitleVtt('Plain text'),
subtitleVtt('line one\nline two'),
'WEBVTT\n\n00:02.000 --> 00:05.000\nLater\n\n00:00.000 --> 00:03.000\nEarlier\n',
subtitleVtt('best effort').replace('WEBVTT', 'BAD HEADER'),