diff --git a/docs/compiled/artplayer-plugin-multiple-subtitles.js b/docs/compiled/artplayer-plugin-multiple-subtitles.js index 706e4f14c..4a28e3dff 100644 --- a/docs/compiled/artplayer-plugin-multiple-subtitles.js +++ b/docs/compiled/artplayer-plugin-multiple-subtitles.js @@ -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={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\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),(hb&&" "!==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.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||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"==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;r100)){a("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){a("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){a("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(c)){f=c.split(","),c=f[0];var m=f[1]}if("%"!=c[c.length-1]){a("Text position must be a percentage.");continue}if(parseInt(c,10)>100||parseInt(c,10)<0){a("Text position needs to be between 0 and 100%.");continue}if(""===(d=c.slice(0,c.length-1))||isNaN(d)||!isFinite(d)){a("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){a("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=c[c.length-1]){a("Size must be a percentage.");continue}if(parseInt(c,10)>100){a("Size cannot be >100%.");continue}var h=c.slice(0,c.length-1);if(void 0===h||""===h||isNaN(h)){a("Size needs to be a number"),h=100;continue}if((h=parseFloat(h))<0||h>100){a("Size needs to be between 0 and 100%.");continue}t.size=h}else if("align"==l){var g=["start","center","end","left","right"];if(-1==g.indexOf(c)){a("Alignment can only be set to one of "+g.join(", ")+".");continue}t.alignment=c}else a("Invalid setting.")}}(e.substring(s),t),!0}else a("No valid timestamp separator found.");else a("No valid timestamp separator found.");else a("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[s])return t;a("Timestamp must not have trailing characters.")}},i=function(e,t,i,s){this.entities=s;var a=this,r=(e=e,0),o=function(e){"metadata"!=i&&t(e,r+1)};function l(){for(var t="data",n="",i="",l=[];void 0!=e[r-1]||0==r;){var c=e[r];if("data"==t)if("&"==c)i=c,t="escape";else if("<"==c&&""==n)t="tag";else{if("<"==c||void 0==c)return["text",n];n+=c}else if("escape"==t){if("<"==c||void 0==c){let e;return o("Incorrect escape."),(e=i.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):a.entities[i]?n+=a.entities[i]:n+=i,["text",n]}if("&"==c)o("Incorrect escape."),n+=i,i=c;else if(/[a-z#0-9]/i.test(c))i+=c;else if(";"==c){let e;(e=i.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):a.entities[i+c]?n+=a.entities[i+c]:(e=Object.keys(s).find(e=>i.startsWith(e)))?n+=a.entities[e]+i.slice(e.length)+c:(o("Incorrect escape."),n+=i+";"),t="data"}else o("Incorrect escape."),n+=i+c,t="data"}else if("tag"==t)if("\t"==c||"\n"==c||"\f"==c||" "==c)t="start tag annotation";else if("."==c)t="start tag class";else if("/"==c)t="end tag";else if(/\d/.test(c))n=c,t="timestamp tag";else{if(">"==c||void 0==c)return">"==c&&r++,["start tag","",[],""];n=c,t="start tag"}else if("start tag"==t)if("\t"==c||"\f"==c||" "==c)t="start tag annotation";else if("\n"==c)i=c,t="start tag annotation";else if("."==c)t="start tag class";else{if(">"==c||void 0==c)return">"==c&&r++,["start tag",n,[],""];n+=c}else if("start tag class"==t)if("\t"==c||"\f"==c||" "==c)i&&l.push(i),i="",t="start tag annotation";else if("\n"==c)i&&l.push(i),i=c,t="start tag annotation";else if("."==c)i&&l.push(i),i="";else{if(">"==c||void 0==c)return">"==c&&r++,i&&l.push(i),["start tag",n,l,""];i+=c}else if("start tag annotation"==t){if(">"==c||void 0==c)return">"==c&&r++,["start tag",n,l,i=i.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];i+=c}else if("end tag"==t){if(">"==c||void 0==c)return">"==c&&r++,["end tag",n];n+=c}else if("timestamp tag"==t){if(">"==c||void 0==c)return">"==c&&r++,["timestamp",n];n+=c}else o("Never happens.");r++}}this.parse=function(t,s){var a={children:[]},c=a,f=[];function u(e){c.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:c}),c=c.children[c.children.length-1]}function d(e){for(var t=c;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[r];){var p=l();if("text"==p[0])c.children.push({type:"text",value:p[1],parent:c});else if("start tag"==p[0]){"chapters"==i&&o("Start tags not allowed in chapter title text.");var m=p[1];"v"!=m&&"lang"!=m&&""!=p[3]&&o("Only and can have an annotation."),"c"==m||"i"==m||"b"==m||"u"==m||"ruby"==m||"rt"==m&&"ruby"==c.name?u(p):"v"==m?(d("v")&&o(" cannot be nested inside itself."),u(p),c.value=p[3],p[3]||o(" requires an annotation.")):"lang"==m?(u(p),c.value=p[3]):o("Incorrect start tag.")}else if("end tag"==p[0])"chapters"==i&&o("End tags not allowed in chapter title text."),p[1]==c.name?c=c.parent:"ruby"==p[1]&&"rt"==c.name?c=c.parent.parent:o("Incorrect end tag.");else if("timestamp"==p[0]){"chapters"==i&&o("Timestamp not allowed in chapter title text.");var h=new n(p[1],o).parseTimestamp();void 0!=h&&((h<=t||h>=s)&&o("Timestamp must be between start timestamp and end timestamp."),f.length>0&&f[f.length-1]>=h&&o("Timestamp must be greater than any previous timestamp."),c.children.push({type:"timestamp",value:h,parent:c}),f.push(h))}}for(;c.parent;)"v"!=c.name&&o("Required end tag missing."),c=c.parent;return function e(t){const n={...t};return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(a)}},s=function(){function t(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,s=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),s=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+s).padStart(2,"0")+"."+t}function n(e){for(var i="",s=0;s/g,">");else if("object"==a.type){if(i+="<"+a.name,a.classes)for(var r=0;r"}else"timestamp"==a.type?i+="<"+t(a.value)+">":i+="<"+a.value+">"}return i}function i(i){return(void 0!==i.id?i.id+"\n":"")+t(i.startTime)+" --\x3e "+t(i.endTime)+function(t){var n="";const i=Object.keys(e).filter(n=>t[n]!==e[n]);return i.includes("direction")&&(n+=" vertical:"+t.direction),i.includes("alignment")&&(n+=" align:"+t.alignment),i.includes("size")&&(n+=" size:"+t.size+"%"),(i.includes("lineAlign")||i.includes("linePosition"))&&(n+=" line:"+t.linePosition+(t.snapToLines?"":"%")+(t.lineAlign&&t.lineAlign!=e.lineAlign?","+t.lineAlign:"")),(i.includes("textPosition")||i.includes("positionAlign"))&&(n+=" position:"+t.textPosition+"%"+(t.positionAlign&&t.positionAlign!==e.positionAlign?","+t.positionAlign:"")),n}(i)+"\n"+n(i.tree.children)+"\n\n"}function s(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var n="WEBVTT\n\n";if(t)for(var a=0;a{if(t.closed)return;const a=URL.createObjectURL(new Blob([s],{type:"text/vtt"})),r=function(e){let n=!1,i=()=>{};const s={free(){n||(n=!0,i(),URL.revokeObjectURL(e))}};return i=t.own(s.free),s}(a);if(t.closed)return;const o=i;let l,c,f=!1;i=r;try{if(l=e.option.subtitle,c=l.escape,t.closed||i!==r)return;l.escape=!1,f=!0;const s={...l,url:a,type:"vtt",onVttLoad:n};if(t.closed||i!==r)return;const o=e.subtitle.init(s);Promise.resolve(o).catch(e=>{i===r&&(i=null,r.free()),t.closed||console.warn("Failed to initialize multiple subtitles:",e)})}catch(u){throw i===r&&(i=o,f&&!1===l.escape&&(l.escape=c)),r.free(),u}finally{o!==i&&o?.free(),r!==i&&r.free()}}}function c({subtitles:e=[]}){return async n=>{const{unescape:i,getExt:c,srtToVtt:f,assToVtt:u}=n.constructor.utils,d=function(e){let t=Boolean(e.isDestroy);const n=new Set;let i;const s=new Promise(e=>{i=e});function a(e){try{e()}catch(t){console.warn("Failed to clean up multiple subtitles:",t)}}const r={get closed(){return t},own:e=>(t?a(e):n.add(e),()=>n.delete(e)),wait:e=>Promise.race([e,s]),dispose(){if(t)return;t=!0,i();const e=[...n].reverse();n.clear();for(const t of e)a(t)}};if(!t){r.own(()=>e.off("destroy",r.dispose));try{e.on("destroy",r.dispose)}catch(o){throw r.dispose(),o}}return r}(n),p=l(n,d,i);let m=[];function h(e){d.closed||p(function(e){const t=[];for(const n of e){const e=n;for(const n of e.cues)t.push({...n,tree:{...n.tree,children:n.tree.children.map(t=>{if("timestamp"===t.type)return r(t);const n=`
${t.value}
`;return"object"===t.type?{...t,value:n,children:t.children.map(o)}:{...t,value:n}})}})}return(new s).serialize(t)}(e))}const g={name:"multipleSubtitles",tracks(e=[]){d.closed||h(e.map(e=>m.find(t=>t.name===e)))},reset(){h(m)}};try{if(d.closed)return g;const i=await Promise.all(e.map(e=>async function(e,{getExt:t,srtToVtt:n,assToVtt:i},s){if(s.closed)return;const a="function"===typeof AbortController?new AbortController:null,r=s.own(()=>a?.abort());try{const r=e.url;if(s.closed)return;const o=await s.wait(a?fetch(r,{signal:a.signal}):fetch(r));if(s.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch multiple subtitles: HTTP ${o.status}`);const l=await s.wait(o.arrayBuffer());if(s.closed)return;const c=new TextDecoder(e.encoding||"utf-8").decode(l);switch(e.type||t(e.url)){case"srt":return n(c);case"ass":return i(c);case"vtt":return c;default:return""}}catch(o){throw a?.abort(),o}finally{r()}}(e,{getExt:c,srtToVtt:f,assToVtt:u},d)));return d.closed||(m=function(e,n){const i=new t;return e.map((e,t)=>({...i.parse(e,"metadata"),url:n[t].url,name:n[t].name}))}(i,e),function(e,t){function n(){if(!t.closed)for(const t of Array.from(e.template.$subtitle.getElementsByTagName(`c.${a}`))){const e=t.firstChild;3===e?.nodeType&&(e.nodeValue=(e.nodeValue||"").replace(/^<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/,"")),t.replaceWith(...Array.from(t.childNodes))}}t.closed||(t.own(()=>e.off("subtitleAfterUpdate",n)),e.on("subtitleAfterUpdate",n))}(n,d),h(m)),g}catch(v){throw d.dispose(),v}}}return Object.assign(c,{default:c})}); +!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={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\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),(gb&&" "!==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.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||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"==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;r100)){a("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){a("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){a("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(c)){f=c.split(","),c=f[0];var m=f[1]}if("%"!=c[c.length-1]){a("Text position must be a percentage.");continue}if(parseInt(c,10)>100||parseInt(c,10)<0){a("Text position needs to be between 0 and 100%.");continue}if(""===(d=c.slice(0,c.length-1))||isNaN(d)||!isFinite(d)){a("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){a("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=c[c.length-1]){a("Size must be a percentage.");continue}if(parseInt(c,10)>100){a("Size cannot be >100%.");continue}var g=c.slice(0,c.length-1);if(void 0===g||""===g||isNaN(g)){a("Size needs to be a number"),g=100;continue}if((g=parseFloat(g))<0||g>100){a("Size needs to be between 0 and 100%.");continue}t.size=g}else if("align"==l){var h=["start","center","end","left","right"];if(-1==h.indexOf(c)){a("Alignment can only be set to one of "+h.join(", ")+".");continue}t.alignment=c}else a("Invalid setting.")}}(e.substring(s),t),!0}else a("No valid timestamp separator found.");else a("No valid timestamp separator found.");else a("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[s])return t;a("Timestamp must not have trailing characters.")}},i=function(e,t,i,s){this.entities=s;var a=this,r=(e=e,0),o=function(e){"metadata"!=i&&t(e,r+1)};function l(){for(var t="data",n="",i="",l=[];void 0!=e[r-1]||0==r;){var c=e[r];if("data"==t)if("&"==c)i=c,t="escape";else if("<"==c&&""==n)t="tag";else{if("<"==c||void 0==c)return["text",n];n+=c}else if("escape"==t){if("<"==c||void 0==c){let e;return o("Incorrect escape."),(e=i.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):a.entities[i]?n+=a.entities[i]:n+=i,["text",n]}if("&"==c)o("Incorrect escape."),n+=i,i=c;else if(/[a-z#0-9]/i.test(c))i+=c;else if(";"==c){let e;(e=i.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):a.entities[i+c]?n+=a.entities[i+c]:(e=Object.keys(s).find(e=>i.startsWith(e)))?n+=a.entities[e]+i.slice(e.length)+c:(o("Incorrect escape."),n+=i+";"),t="data"}else o("Incorrect escape."),n+=i+c,t="data"}else if("tag"==t)if("\t"==c||"\n"==c||"\f"==c||" "==c)t="start tag annotation";else if("."==c)t="start tag class";else if("/"==c)t="end tag";else if(/\d/.test(c))n=c,t="timestamp tag";else{if(">"==c||void 0==c)return">"==c&&r++,["start tag","",[],""];n=c,t="start tag"}else if("start tag"==t)if("\t"==c||"\f"==c||" "==c)t="start tag annotation";else if("\n"==c)i=c,t="start tag annotation";else if("."==c)t="start tag class";else{if(">"==c||void 0==c)return">"==c&&r++,["start tag",n,[],""];n+=c}else if("start tag class"==t)if("\t"==c||"\f"==c||" "==c)i&&l.push(i),i="",t="start tag annotation";else if("\n"==c)i&&l.push(i),i=c,t="start tag annotation";else if("."==c)i&&l.push(i),i="";else{if(">"==c||void 0==c)return">"==c&&r++,i&&l.push(i),["start tag",n,l,""];i+=c}else if("start tag annotation"==t){if(">"==c||void 0==c)return">"==c&&r++,["start tag",n,l,i=i.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];i+=c}else if("end tag"==t){if(">"==c||void 0==c)return">"==c&&r++,["end tag",n];n+=c}else if("timestamp tag"==t){if(">"==c||void 0==c)return">"==c&&r++,["timestamp",n];n+=c}else o("Never happens.");r++}}this.parse=function(t,s){var a={children:[]},c=a,f=[];function u(e){c.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:c}),c=c.children[c.children.length-1]}function d(e){for(var t=c;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[r];){var p=l();if("text"==p[0])c.children.push({type:"text",value:p[1],parent:c});else if("start tag"==p[0]){"chapters"==i&&o("Start tags not allowed in chapter title text.");var m=p[1];"v"!=m&&"lang"!=m&&""!=p[3]&&o("Only and can have an annotation."),"c"==m||"i"==m||"b"==m||"u"==m||"ruby"==m||"rt"==m&&"ruby"==c.name?u(p):"v"==m?(d("v")&&o(" cannot be nested inside itself."),u(p),c.value=p[3],p[3]||o(" requires an annotation.")):"lang"==m?(u(p),c.value=p[3]):o("Incorrect start tag.")}else if("end tag"==p[0])"chapters"==i&&o("End tags not allowed in chapter title text."),p[1]==c.name?c=c.parent:"ruby"==p[1]&&"rt"==c.name?c=c.parent.parent:o("Incorrect end tag.");else if("timestamp"==p[0]){"chapters"==i&&o("Timestamp not allowed in chapter title text.");var g=new n(p[1],o).parseTimestamp();void 0!=g&&((g<=t||g>=s)&&o("Timestamp must be between start timestamp and end timestamp."),f.length>0&&f[f.length-1]>=g&&o("Timestamp must be greater than any previous timestamp."),c.children.push({type:"timestamp",value:g,parent:c}),f.push(g))}}for(;c.parent;)"v"!=c.name&&o("Required end tag missing."),c=c.parent;return function e(t){const n={...t};return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(a)}},s=function(){function t(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,s=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),s=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+s).padStart(2,"0")+"."+t}function n(e){for(var i="",s=0;s/g,">");else if("object"==a.type){if(i+="<"+a.name,a.classes)for(var r=0;r"}else"timestamp"==a.type?i+="<"+t(a.value)+">":i+="<"+a.value+">"}return i}function i(i){return(void 0!==i.id?i.id+"\n":"")+t(i.startTime)+" --\x3e "+t(i.endTime)+function(t){var n="";const i=Object.keys(e).filter(n=>t[n]!==e[n]);return i.includes("direction")&&(n+=" vertical:"+t.direction),i.includes("alignment")&&(n+=" align:"+t.alignment),i.includes("size")&&(n+=" size:"+t.size+"%"),(i.includes("lineAlign")||i.includes("linePosition"))&&(n+=" line:"+t.linePosition+(t.snapToLines?"":"%")+(t.lineAlign&&t.lineAlign!=e.lineAlign?","+t.lineAlign:"")),(i.includes("textPosition")||i.includes("positionAlign"))&&(n+=" position:"+t.textPosition+"%"+(t.positionAlign&&t.positionAlign!==e.positionAlign?","+t.positionAlign:"")),n}(i)+"\n"+n(i.tree.children)+"\n\n"}function s(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var n="WEBVTT\n\n";if(t)for(var a=0;a",">":">","&lrm":"\u200e","‎":"\u200e","&rlm":"\u200f","‏":"\u200f"," ":"\xa0"," ":"\xa0"};const r="artplayer-multiple-subtitles-timestamp";function o(e){return{type:"object",name:"c",classes:[r],children:[e]}}function l(e){return"timestamp"===e.type?o(e):"object"===e.type?{...e,children:e.children.map(l)}:{...e,value:(t=e.value,t.replace(/&/g,"&").replace(//g,">"))};var t}function c(e,t,n){let i=null;return s=>{if(t.closed)return;const a=URL.createObjectURL(new Blob([s],{type:"text/vtt"})),r=function(e){let n=!1,i=()=>{};const s={free(){n||(n=!0,i(),URL.revokeObjectURL(e))}};return i=t.own(s.free),s}(a);if(t.closed)return;const o=i;let l,c,f=!1;i=r;try{if(l=e.option.subtitle,c=l.escape,t.closed||i!==r)return;l.escape=!1,f=!0;const s={...l,url:a,type:"vtt",onVttLoad:n};if(t.closed||i!==r)return;const o=e.subtitle.init(s);Promise.resolve(o).catch(e=>{i===r&&(i=null,r.free()),t.closed||console.warn("Failed to initialize multiple subtitles:",e)})}catch(u){throw i===r&&(i=o,f&&!1===l.escape&&(l.escape=c)),r.free(),u}finally{o!==i&&o?.free(),r!==i&&r.free()}}}function f({subtitles:e=[]}){return async n=>{const{unescape:i,getExt:f,srtToVtt:u,assToVtt:d}=n.constructor.utils,p=function(e){let t=Boolean(e.isDestroy);const n=new Set;let i;const s=new Promise(e=>{i=e});function a(e){try{e()}catch(t){console.warn("Failed to clean up multiple subtitles:",t)}}const r={get closed(){return t},own:e=>(t?a(e):n.add(e),()=>n.delete(e)),wait:e=>Promise.race([e,s]),dispose(){if(t)return;t=!0,i();const e=[...n].reverse();n.clear();for(const t of e)a(t)}};if(!t){r.own(()=>e.off("destroy",r.dispose));try{e.on("destroy",r.dispose)}catch(o){throw r.dispose(),o}}return r}(n),m=c(n,p,i);let g=[];function h(e){p.closed||m(function(e){const t=[];for(const n of e){const e=n;for(const n of e.cues){const i=[];for(const t of n.tree.children)"timestamp"===t.type?i.push(o(t)):i.push({type:"text",value:`
`},l(t),{type:"text",value:"
"});t.push({...n,tree:{...n.tree,children:i}})}}return(new s).serialize(t)}(e))}const v={name:"multipleSubtitles",tracks(e=[]){p.closed||h(e.map(e=>g.find(t=>t.name===e)))},reset(){h(g)}};try{if(p.closed)return v;const i=await Promise.all(e.map(e=>async function(e,{getExt:t,srtToVtt:n,assToVtt:i},s){if(s.closed)return;const a="function"===typeof AbortController?new AbortController:null,r=s.own(()=>a?.abort());try{const r=e.url;if(s.closed)return;const o=await s.wait(a?fetch(r,{signal:a.signal}):fetch(r));if(s.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch multiple subtitles: HTTP ${o.status}`);const l=await s.wait(o.arrayBuffer());if(s.closed)return;const c=new TextDecoder(e.encoding||"utf-8").decode(l);switch(e.type||t(e.url)){case"srt":return n(c);case"ass":return i(c);case"vtt":return c;default:return""}}catch(o){throw a?.abort(),o}finally{r()}}(e,{getExt:f,srtToVtt:u,assToVtt:d},p)));return p.closed||(g=function(e,n){const i=new t(a);return e.map((e,t)=>({...i.parse(e,"metadata"),url:n[t].url,name:n[t].name}))}(i,e),function(e,t){function n(){if(!t.closed)for(const t of Array.from(e.template.$subtitle.getElementsByTagName(`c.${r}`))){const e=t.firstChild;3===e?.nodeType&&(e.nodeValue=(e.nodeValue||"").replace(/^<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/,"")),t.replaceWith(...Array.from(t.childNodes))}}t.closed||(t.own(()=>e.off("subtitleAfterUpdate",n)),e.on("subtitleAfterUpdate",n))}(n,p),h(g)),v}catch(b){throw p.dispose(),b}}}return Object.assign(f,{default:f})}); diff --git a/docs/compiled/artplayer-plugin-multiple-subtitles.legacy.js b/docs/compiled/artplayer-plugin-multiple-subtitles.legacy.js index 107edd0e3..27cc418fc 100644 --- a/docs/compiled/artplayer-plugin-multiple-subtitles.legacy.js +++ b/docs/compiled/artplayer-plugin-multiple-subtitles.legacy.js @@ -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={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\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),(hb&&" "!==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.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||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"==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=0;a100)){s("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){s("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(c)){f=c.split(","),c=f[0];var m=f[1]}if("%"!=c[c.length-1]){s("Text position must be a percentage.");continue}if(parseInt(c,10)>100||parseInt(c,10)<0){s("Text position needs to be between 0 and 100%.");continue}if(""===(d=c.slice(0,c.length-1))||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){s("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=c[c.length-1]){s("Size must be a percentage.");continue}if(parseInt(c,10)>100){s("Size cannot be >100%.");continue}var h=c.slice(0,c.length-1);if(void 0===h||""===h||isNaN(h)){s("Size needs to be a number"),h=100;continue}if((h=parseFloat(h))<0||h>100){s("Size needs to be between 0 and 100%.");continue}t.size=h}else if("align"==l){var g=["start","center","end","left","right"];if(-1==g.indexOf(c)){s("Alignment can only be set to one of "+g.join(", ")+".");continue}t.alignment=c}else s("Invalid setting.")}}(e.substring(r),t),!0}else s("No valid timestamp separator found.");else s("No valid timestamp separator found.");else s("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[r])return t;s("Timestamp must not have trailing characters.")}},p=function(e,t,n,i){this.entities=i;var r=this,s=(e=e,0),a=function(e){"metadata"!=n&&t(e,s+1)};function l(){for(var t="data",n="",o="",l=[];void 0!=e[s-1]||0==s;){var c=e[s];if("data"==t)if("&"==c)o=c,t="escape";else if("<"==c&&""==n)t="tag";else{if("<"==c||void 0==c)return["text",n];n+=c}else if("escape"==t){if("<"==c||void 0==c){let e;return a("Incorrect escape."),(e=o.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):r.entities[o]?n+=r.entities[o]:n+=o,["text",n]}if("&"==c)a("Incorrect escape."),n+=o,o=c;else if(/[a-z#0-9]/i.test(c))o+=c;else if(";"==c){let e;(e=o.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):r.entities[o+c]?n+=r.entities[o+c]:(e=Object.keys(i).find(e=>o.startsWith(e)))?n+=r.entities[e]+o.slice(e.length)+c:(a("Incorrect escape."),n+=o+";"),t="data"}else a("Incorrect escape."),n+=o+c,t="data"}else if("tag"==t)if("\t"==c||"\n"==c||"\f"==c||" "==c)t="start tag annotation";else if("."==c)t="start tag class";else if("/"==c)t="end tag";else if(/\d/.test(c))n=c,t="timestamp tag";else{if(">"==c||void 0==c)return">"==c&&s++,["start tag","",[],""];n=c,t="start tag"}else if("start tag"==t)if("\t"==c||"\f"==c||" "==c)t="start tag annotation";else if("\n"==c)o=c,t="start tag annotation";else if("."==c)t="start tag class";else{if(">"==c||void 0==c)return">"==c&&s++,["start tag",n,[],""];n+=c}else if("start tag class"==t)if("\t"==c||"\f"==c||" "==c)o&&l.push(o),o="",t="start tag annotation";else if("\n"==c)o&&l.push(o),o=c,t="start tag annotation";else if("."==c)o&&l.push(o),o="";else{if(">"==c||void 0==c)return">"==c&&s++,o&&l.push(o),["start tag",n,l,""];o+=c}else if("start tag annotation"==t){if(">"==c||void 0==c)return">"==c&&s++,["start tag",n,l,o=o.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];o+=c}else if("end tag"==t){if(">"==c||void 0==c)return">"==c&&s++,["end tag",n];n+=c}else if("timestamp tag"==t){if(">"==c||void 0==c)return">"==c&&s++,["timestamp",n];n+=c}else a("Never happens.");s++}}this.parse=function(t,i){var r={children:[]},c=r,f=[];function u(e){c.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:c}),c=c.children[c.children.length-1]}function p(e){for(var t=c;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[s];){var m=l();if("text"==m[0])c.children.push({type:"text",value:m[1],parent:c});else if("start tag"==m[0]){"chapters"==n&&a("Start tags not allowed in chapter title text.");var h=m[1];"v"!=h&&"lang"!=h&&""!=m[3]&&a("Only and can have an annotation."),"c"==h||"i"==h||"b"==h||"u"==h||"ruby"==h||"rt"==h&&"ruby"==c.name?u(m):"v"==h?(p("v")&&a(" cannot be nested inside itself."),u(m),c.value=m[3],m[3]||a(" requires an annotation.")):"lang"==h?(u(m),c.value=m[3]):a("Incorrect start tag.")}else if("end tag"==m[0])"chapters"==n&&a("End tags not allowed in chapter title text."),m[1]==c.name?c=c.parent:"ruby"==m[1]&&"rt"==c.name?c=c.parent.parent:a("Incorrect end tag.");else if("timestamp"==m[0]){"chapters"==n&&a("Timestamp not allowed in chapter title text.");var g=new d(m[1],a).parseTimestamp();void 0!=g&&((g<=t||g>=i)&&a("Timestamp must be between start timestamp and end timestamp."),f.length>0&&f[f.length-1]>=g&&a("Timestamp must be greater than any previous timestamp."),c.children.push({type:"timestamp",value:g,parent:c}),f.push(g))}}for(;c.parent;)"v"!=c.name&&a("Required end tag missing."),c=c.parent;return function e(t){const n=o({},t);return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(r)}},m=function(){function e(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,r=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),r=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+r).padStart(2,"0")+"."+t}function t(n){for(var i="",r=0;r/g,">");else if("object"==s.type){if(i+="<"+s.name,s.classes)for(var a=0;a"}else"timestamp"==s.type?i+="<"+e(s.value)+">":i+="<"+s.value+">"}return i}function n(n){return(void 0!==n.id?n.id+"\n":"")+e(n.startTime)+" --\x3e "+e(n.endTime)+function(e){var t="";const n=Object.keys(f).filter(t=>e[t]!==f[t]);return n.includes("direction")&&(t+=" vertical:"+e.direction),n.includes("alignment")&&(t+=" align:"+e.alignment),n.includes("size")&&(t+=" size:"+e.size+"%"),(n.includes("lineAlign")||n.includes("linePosition"))&&(t+=" line:"+e.linePosition+(e.snapToLines?"":"%")+(e.lineAlign&&e.lineAlign!=f.lineAlign?","+e.lineAlign:"")),(n.includes("textPosition")||n.includes("positionAlign"))&&(t+=" position:"+e.textPosition+"%"+(e.positionAlign&&e.positionAlign!==f.positionAlign?","+e.positionAlign:"")),t}(n)+"\n"+t(n.tree.children)+"\n\n"}function i(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var r="WEBVTT\n\n";if(t)for(var s=0;s{if(t.closed)return;const s=URL.createObjectURL(new Blob([r],{type:"text/vtt"})),a=function(e){let n=!1,i=()=>{};const r={free(){n||(n=!0,i(),URL.revokeObjectURL(e))}};return i=t.own(r.free),r}(s);if(t.closed)return;const c=i;let f,u,d=!1;i=a;try{if(f=e.option.subtitle,u=f.escape,t.closed||i!==a)return;f.escape=!1,d=!0;const r=l(o({},f),{url:s,type:"vtt",onVttLoad:n});if(t.closed||i!==a)return;const c=e.subtitle.init(r);Promise.resolve(c).catch(e=>{i===a&&(i=null,a.free()),t.closed||console.warn("Failed to initialize multiple subtitles:",e)})}catch(p){throw i===a&&(i=c,d&&!1===f.escape&&(f.escape=u)),a.free(),p}finally{c!==i&&(null==c||c.free()),a!==i&&a.free()}}}function y({subtitles:e=[]}){return t=>c(null,null,function*(){const{unescape:n,getExt:i,srtToVtt:r,assToVtt:s}=t.constructor.utils,a=function(e){let t=Boolean(e.isDestroy);const n=new Set;let i;const r=new Promise(e=>{i=e});function s(e){try{e()}catch(t){console.warn("Failed to clean up multiple subtitles:",t)}}const a={get closed(){return t},own:e=>(t?s(e):n.add(e),()=>n.delete(e)),wait:e=>Promise.race([e,r]),dispose(){if(t)return;t=!0,i();const e=[...n].reverse();n.clear();for(const t of e)s(t)}};if(!t){a.own(()=>e.off("destroy",a.dispose));try{e.on("destroy",a.dispose)}catch(o){throw a.dispose(),o}}return a}(t),f=b(t,a,n);let d=[];function p(e){a.closed||f(function(e){const t=[];for(const n of e){const e=n;for(const n of e.cues)t.push(l(o({},n),{tree:l(o({},n.tree),{children:n.tree.children.map(t=>{if("timestamp"===t.type)return g(t);const n=`
${t.value}
`;return"object"===t.type?l(o({},t),{value:n,children:t.children.map(v)}):l(o({},t),{value:n})})})}))}return(new m).serialize(t)}(e))}const y={name:"multipleSubtitles",tracks(e=[]){a.closed||p(e.map(e=>d.find(t=>t.name===e)))},reset(){p(d)}};try{if(a.closed)return y;const n=yield Promise.all(e.map(e=>function(e,t,n){return c(this,arguments,function*(e,{getExt:t,srtToVtt:n,assToVtt:i},r){if(r.closed)return;const s="function"===typeof AbortController?new AbortController:null,a=r.own(()=>null==s?void 0:s.abort());try{const a=e.url;if(r.closed)return;const o=yield r.wait(s?fetch(a,{signal:s.signal}):fetch(a));if(r.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch multiple subtitles: HTTP ${o.status}`);const l=yield r.wait(o.arrayBuffer());if(r.closed)return;const c=new TextDecoder(e.encoding||"utf-8").decode(l);switch(e.type||t(e.url)){case"srt":return n(c);case"ass":return i(c);case"vtt":return c;default:return""}}catch(o){throw null==s||s.abort(),o}finally{a()}})}(e,{getExt:i,srtToVtt:r,assToVtt:s},a)));return a.closed||(d=function(e,t){const n=new u;return e.map((e,i)=>{const r=n.parse(e,"metadata");return l(o({},r),{url:t[i].url,name:t[i].name})})}(n,e),function(e,t){function n(){if(!t.closed)for(const t of Array.from(e.template.$subtitle.getElementsByTagName(`c.${h}`))){const e=t.firstChild;3===(null==e?void 0:e.nodeType)&&(e.nodeValue=(e.nodeValue||"").replace(/^<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/,"")),t.replaceWith(...Array.from(t.childNodes))}}t.closed||(t.own(()=>e.off("subtitleAfterUpdate",n)),e.on("subtitleAfterUpdate",n))}(t,a),p(d)),y}catch(x){throw a.dispose(),x}})}return Object.assign(y,{default:y})}); +!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={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\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),(gb&&" "!==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.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||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"==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=0;a100)){s("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){s("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(c)){f=c.split(","),c=f[0];var m=f[1]}if("%"!=c[c.length-1]){s("Text position must be a percentage.");continue}if(parseInt(c,10)>100||parseInt(c,10)<0){s("Text position needs to be between 0 and 100%.");continue}if(""===(d=c.slice(0,c.length-1))||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){s("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=c[c.length-1]){s("Size must be a percentage.");continue}if(parseInt(c,10)>100){s("Size cannot be >100%.");continue}var g=c.slice(0,c.length-1);if(void 0===g||""===g||isNaN(g)){s("Size needs to be a number"),g=100;continue}if((g=parseFloat(g))<0||g>100){s("Size needs to be between 0 and 100%.");continue}t.size=g}else if("align"==l){var h=["start","center","end","left","right"];if(-1==h.indexOf(c)){s("Alignment can only be set to one of "+h.join(", ")+".");continue}t.alignment=c}else s("Invalid setting.")}}(e.substring(r),t),!0}else s("No valid timestamp separator found.");else s("No valid timestamp separator found.");else s("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[r])return t;s("Timestamp must not have trailing characters.")}},p=function(e,t,n,i){this.entities=i;var r=this,s=(e=e,0),a=function(e){"metadata"!=n&&t(e,s+1)};function l(){for(var t="data",n="",o="",l=[];void 0!=e[s-1]||0==s;){var c=e[s];if("data"==t)if("&"==c)o=c,t="escape";else if("<"==c&&""==n)t="tag";else{if("<"==c||void 0==c)return["text",n];n+=c}else if("escape"==t){if("<"==c||void 0==c){let e;return a("Incorrect escape."),(e=o.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):r.entities[o]?n+=r.entities[o]:n+=o,["text",n]}if("&"==c)a("Incorrect escape."),n+=o,o=c;else if(/[a-z#0-9]/i.test(c))o+=c;else if(";"==c){let e;(e=o.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):r.entities[o+c]?n+=r.entities[o+c]:(e=Object.keys(i).find(e=>o.startsWith(e)))?n+=r.entities[e]+o.slice(e.length)+c:(a("Incorrect escape."),n+=o+";"),t="data"}else a("Incorrect escape."),n+=o+c,t="data"}else if("tag"==t)if("\t"==c||"\n"==c||"\f"==c||" "==c)t="start tag annotation";else if("."==c)t="start tag class";else if("/"==c)t="end tag";else if(/\d/.test(c))n=c,t="timestamp tag";else{if(">"==c||void 0==c)return">"==c&&s++,["start tag","",[],""];n=c,t="start tag"}else if("start tag"==t)if("\t"==c||"\f"==c||" "==c)t="start tag annotation";else if("\n"==c)o=c,t="start tag annotation";else if("."==c)t="start tag class";else{if(">"==c||void 0==c)return">"==c&&s++,["start tag",n,[],""];n+=c}else if("start tag class"==t)if("\t"==c||"\f"==c||" "==c)o&&l.push(o),o="",t="start tag annotation";else if("\n"==c)o&&l.push(o),o=c,t="start tag annotation";else if("."==c)o&&l.push(o),o="";else{if(">"==c||void 0==c)return">"==c&&s++,o&&l.push(o),["start tag",n,l,""];o+=c}else if("start tag annotation"==t){if(">"==c||void 0==c)return">"==c&&s++,["start tag",n,l,o=o.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];o+=c}else if("end tag"==t){if(">"==c||void 0==c)return">"==c&&s++,["end tag",n];n+=c}else if("timestamp tag"==t){if(">"==c||void 0==c)return">"==c&&s++,["timestamp",n];n+=c}else a("Never happens.");s++}}this.parse=function(t,i){var r={children:[]},c=r,f=[];function u(e){c.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:c}),c=c.children[c.children.length-1]}function p(e){for(var t=c;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[s];){var m=l();if("text"==m[0])c.children.push({type:"text",value:m[1],parent:c});else if("start tag"==m[0]){"chapters"==n&&a("Start tags not allowed in chapter title text.");var g=m[1];"v"!=g&&"lang"!=g&&""!=m[3]&&a("Only and can have an annotation."),"c"==g||"i"==g||"b"==g||"u"==g||"ruby"==g||"rt"==g&&"ruby"==c.name?u(m):"v"==g?(p("v")&&a(" cannot be nested inside itself."),u(m),c.value=m[3],m[3]||a(" requires an annotation.")):"lang"==g?(u(m),c.value=m[3]):a("Incorrect start tag.")}else if("end tag"==m[0])"chapters"==n&&a("End tags not allowed in chapter title text."),m[1]==c.name?c=c.parent:"ruby"==m[1]&&"rt"==c.name?c=c.parent.parent:a("Incorrect end tag.");else if("timestamp"==m[0]){"chapters"==n&&a("Timestamp not allowed in chapter title text.");var h=new d(m[1],a).parseTimestamp();void 0!=h&&((h<=t||h>=i)&&a("Timestamp must be between start timestamp and end timestamp."),f.length>0&&f[f.length-1]>=h&&a("Timestamp must be greater than any previous timestamp."),c.children.push({type:"timestamp",value:h,parent:c}),f.push(h))}}for(;c.parent;)"v"!=c.name&&a("Required end tag missing."),c=c.parent;return function e(t){const n=o({},t);return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(r)}},m=function(){function e(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,r=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),r=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+r).padStart(2,"0")+"."+t}function t(n){for(var i="",r=0;r/g,">");else if("object"==s.type){if(i+="<"+s.name,s.classes)for(var a=0;a"}else"timestamp"==s.type?i+="<"+e(s.value)+">":i+="<"+s.value+">"}return i}function n(n){return(void 0!==n.id?n.id+"\n":"")+e(n.startTime)+" --\x3e "+e(n.endTime)+function(e){var t="";const n=Object.keys(f).filter(t=>e[t]!==f[t]);return n.includes("direction")&&(t+=" vertical:"+e.direction),n.includes("alignment")&&(t+=" align:"+e.alignment),n.includes("size")&&(t+=" size:"+e.size+"%"),(n.includes("lineAlign")||n.includes("linePosition"))&&(t+=" line:"+e.linePosition+(e.snapToLines?"":"%")+(e.lineAlign&&e.lineAlign!=f.lineAlign?","+e.lineAlign:"")),(n.includes("textPosition")||n.includes("positionAlign"))&&(t+=" position:"+e.textPosition+"%"+(e.positionAlign&&e.positionAlign!==f.positionAlign?","+e.positionAlign:"")),t}(n)+"\n"+t(n.tree.children)+"\n\n"}function i(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var r="WEBVTT\n\n";if(t)for(var s=0;s",">":">","&lrm":"\u200e","‎":"\u200e","&rlm":"\u200f","‏":"\u200f"," ":"\xa0"," ":"\xa0"};const h="artplayer-multiple-subtitles-timestamp";function v(e){return{type:"object",name:"c",classes:[h],children:[e]}}function b(e){return"timestamp"===e.type?v(e):"object"===e.type?l(o({},e),{children:e.children.map(b)}):l(o({},e),{value:(t=e.value,t.replace(/&/g,"&").replace(//g,">"))});var t}function y(e,t,n){let i=null;return r=>{if(t.closed)return;const s=URL.createObjectURL(new Blob([r],{type:"text/vtt"})),a=function(e){let n=!1,i=()=>{};const r={free(){n||(n=!0,i(),URL.revokeObjectURL(e))}};return i=t.own(r.free),r}(s);if(t.closed)return;const c=i;let f,u,d=!1;i=a;try{if(f=e.option.subtitle,u=f.escape,t.closed||i!==a)return;f.escape=!1,d=!0;const r=l(o({},f),{url:s,type:"vtt",onVttLoad:n});if(t.closed||i!==a)return;const c=e.subtitle.init(r);Promise.resolve(c).catch(e=>{i===a&&(i=null,a.free()),t.closed||console.warn("Failed to initialize multiple subtitles:",e)})}catch(p){throw i===a&&(i=c,d&&!1===f.escape&&(f.escape=u)),a.free(),p}finally{c!==i&&(null==c||c.free()),a!==i&&a.free()}}}function x({subtitles:e=[]}){return t=>c(null,null,function*(){const{unescape:n,getExt:i,srtToVtt:r,assToVtt:s}=t.constructor.utils,a=function(e){let t=Boolean(e.isDestroy);const n=new Set;let i;const r=new Promise(e=>{i=e});function s(e){try{e()}catch(t){console.warn("Failed to clean up multiple subtitles:",t)}}const a={get closed(){return t},own:e=>(t?s(e):n.add(e),()=>n.delete(e)),wait:e=>Promise.race([e,r]),dispose(){if(t)return;t=!0,i();const e=[...n].reverse();n.clear();for(const t of e)s(t)}};if(!t){a.own(()=>e.off("destroy",a.dispose));try{e.on("destroy",a.dispose)}catch(o){throw a.dispose(),o}}return a}(t),f=y(t,a,n);let d=[];function p(e){a.closed||f(function(e){const t=[];for(const n of e){const e=n;for(const n of e.cues){const i=[];for(const t of n.tree.children)"timestamp"===t.type?i.push(v(t)):i.push({type:"text",value:`
`},b(t),{type:"text",value:"
"});t.push(l(o({},n),{tree:l(o({},n.tree),{children:i})}))}}return(new m).serialize(t)}(e))}const x={name:"multipleSubtitles",tracks(e=[]){a.closed||p(e.map(e=>d.find(t=>t.name===e)))},reset(){p(d)}};try{if(a.closed)return x;const n=yield Promise.all(e.map(e=>function(e,t,n){return c(this,arguments,function*(e,{getExt:t,srtToVtt:n,assToVtt:i},r){if(r.closed)return;const s="function"===typeof AbortController?new AbortController:null,a=r.own(()=>null==s?void 0:s.abort());try{const a=e.url;if(r.closed)return;const o=yield r.wait(s?fetch(a,{signal:s.signal}):fetch(a));if(r.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch multiple subtitles: HTTP ${o.status}`);const l=yield r.wait(o.arrayBuffer());if(r.closed)return;const c=new TextDecoder(e.encoding||"utf-8").decode(l);switch(e.type||t(e.url)){case"srt":return n(c);case"ass":return i(c);case"vtt":return c;default:return""}}catch(o){throw null==s||s.abort(),o}finally{a()}})}(e,{getExt:i,srtToVtt:r,assToVtt:s},a)));return a.closed||(d=function(e,t){const n=new u(g);return e.map((e,i)=>{const r=n.parse(e,"metadata");return l(o({},r),{url:t[i].url,name:t[i].name})})}(n,e),function(e,t){function n(){if(!t.closed)for(const t of Array.from(e.template.$subtitle.getElementsByTagName(`c.${h}`))){const e=t.firstChild;3===(null==e?void 0:e.nodeType)&&(e.nodeValue=(e.nodeValue||"").replace(/^<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/,"")),t.replaceWith(...Array.from(t.childNodes))}}t.closed||(t.own(()=>e.off("subtitleAfterUpdate",n)),e.on("subtitleAfterUpdate",n))}(t,a),p(d)),x}catch(T){throw a.dispose(),T}})}return Object.assign(x,{default:x})}); diff --git a/docs/compiled/artplayer-plugin-multiple-subtitles.mjs b/docs/compiled/artplayer-plugin-multiple-subtitles.mjs index 66c7834f6..9eeab913c 100644 --- a/docs/compiled/artplayer-plugin-multiple-subtitles.mjs +++ b/docs/compiled/artplayer-plugin-multiple-subtitles.mjs @@ -799,19 +799,36 @@ var WebVTTSerializer = function() { return result; }; }; +const cueEntities = { + "&": "&", + "&": "&", + "<": "<", + "<": "<", + ">": ">", + ">": ">", + "&lrm": "‎", + "‎": "‎", + "&rlm": "‏", + "‏": "‏", + " ": " ", + " ": " " +}; +function escapeCueText(value) { + return value.replace(/&/g, "&").replace(//g, ">"); +} 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: `
` }, + prepareCueNode(child), + { type: "text", value: "
" } + ); + } cues.push({ ...cue, tree: { ...cue.tree, - children: cue.tree.children.map((child) => { - if (child.type === "timestamp") - return markTimestamp(child); - const value = `
${child.value}
`; - return child.type === "object" ? { ...child, value, children: child.children.map(markNestedTimestamps) } : { ...child, value }; - }) + children } }); } diff --git a/package.json b/package.json index 5bb30dd91..3a3ec39e7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/artplayer-plugin-multiple-subtitles/ARCHITECTURE.md b/packages/artplayer-plugin-multiple-subtitles/ARCHITECTURE.md index e4106ec80..88905cd60 100644 --- a/packages/artplayer-plugin-multiple-subtitles/ARCHITECTURE.md +++ b/packages/artplayer-plugin-multiple-subtitles/ARCHITECTURE.md @@ -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 `` with ``; 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. diff --git a/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.js b/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.js index 706e4f14c..4a28e3dff 100644 --- a/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.js +++ b/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.js @@ -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={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\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),(hb&&" "!==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.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||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"==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;r100)){a("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){a("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){a("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(c)){f=c.split(","),c=f[0];var m=f[1]}if("%"!=c[c.length-1]){a("Text position must be a percentage.");continue}if(parseInt(c,10)>100||parseInt(c,10)<0){a("Text position needs to be between 0 and 100%.");continue}if(""===(d=c.slice(0,c.length-1))||isNaN(d)||!isFinite(d)){a("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){a("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=c[c.length-1]){a("Size must be a percentage.");continue}if(parseInt(c,10)>100){a("Size cannot be >100%.");continue}var h=c.slice(0,c.length-1);if(void 0===h||""===h||isNaN(h)){a("Size needs to be a number"),h=100;continue}if((h=parseFloat(h))<0||h>100){a("Size needs to be between 0 and 100%.");continue}t.size=h}else if("align"==l){var g=["start","center","end","left","right"];if(-1==g.indexOf(c)){a("Alignment can only be set to one of "+g.join(", ")+".");continue}t.alignment=c}else a("Invalid setting.")}}(e.substring(s),t),!0}else a("No valid timestamp separator found.");else a("No valid timestamp separator found.");else a("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[s])return t;a("Timestamp must not have trailing characters.")}},i=function(e,t,i,s){this.entities=s;var a=this,r=(e=e,0),o=function(e){"metadata"!=i&&t(e,r+1)};function l(){for(var t="data",n="",i="",l=[];void 0!=e[r-1]||0==r;){var c=e[r];if("data"==t)if("&"==c)i=c,t="escape";else if("<"==c&&""==n)t="tag";else{if("<"==c||void 0==c)return["text",n];n+=c}else if("escape"==t){if("<"==c||void 0==c){let e;return o("Incorrect escape."),(e=i.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):a.entities[i]?n+=a.entities[i]:n+=i,["text",n]}if("&"==c)o("Incorrect escape."),n+=i,i=c;else if(/[a-z#0-9]/i.test(c))i+=c;else if(";"==c){let e;(e=i.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):a.entities[i+c]?n+=a.entities[i+c]:(e=Object.keys(s).find(e=>i.startsWith(e)))?n+=a.entities[e]+i.slice(e.length)+c:(o("Incorrect escape."),n+=i+";"),t="data"}else o("Incorrect escape."),n+=i+c,t="data"}else if("tag"==t)if("\t"==c||"\n"==c||"\f"==c||" "==c)t="start tag annotation";else if("."==c)t="start tag class";else if("/"==c)t="end tag";else if(/\d/.test(c))n=c,t="timestamp tag";else{if(">"==c||void 0==c)return">"==c&&r++,["start tag","",[],""];n=c,t="start tag"}else if("start tag"==t)if("\t"==c||"\f"==c||" "==c)t="start tag annotation";else if("\n"==c)i=c,t="start tag annotation";else if("."==c)t="start tag class";else{if(">"==c||void 0==c)return">"==c&&r++,["start tag",n,[],""];n+=c}else if("start tag class"==t)if("\t"==c||"\f"==c||" "==c)i&&l.push(i),i="",t="start tag annotation";else if("\n"==c)i&&l.push(i),i=c,t="start tag annotation";else if("."==c)i&&l.push(i),i="";else{if(">"==c||void 0==c)return">"==c&&r++,i&&l.push(i),["start tag",n,l,""];i+=c}else if("start tag annotation"==t){if(">"==c||void 0==c)return">"==c&&r++,["start tag",n,l,i=i.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];i+=c}else if("end tag"==t){if(">"==c||void 0==c)return">"==c&&r++,["end tag",n];n+=c}else if("timestamp tag"==t){if(">"==c||void 0==c)return">"==c&&r++,["timestamp",n];n+=c}else o("Never happens.");r++}}this.parse=function(t,s){var a={children:[]},c=a,f=[];function u(e){c.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:c}),c=c.children[c.children.length-1]}function d(e){for(var t=c;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[r];){var p=l();if("text"==p[0])c.children.push({type:"text",value:p[1],parent:c});else if("start tag"==p[0]){"chapters"==i&&o("Start tags not allowed in chapter title text.");var m=p[1];"v"!=m&&"lang"!=m&&""!=p[3]&&o("Only and can have an annotation."),"c"==m||"i"==m||"b"==m||"u"==m||"ruby"==m||"rt"==m&&"ruby"==c.name?u(p):"v"==m?(d("v")&&o(" cannot be nested inside itself."),u(p),c.value=p[3],p[3]||o(" requires an annotation.")):"lang"==m?(u(p),c.value=p[3]):o("Incorrect start tag.")}else if("end tag"==p[0])"chapters"==i&&o("End tags not allowed in chapter title text."),p[1]==c.name?c=c.parent:"ruby"==p[1]&&"rt"==c.name?c=c.parent.parent:o("Incorrect end tag.");else if("timestamp"==p[0]){"chapters"==i&&o("Timestamp not allowed in chapter title text.");var h=new n(p[1],o).parseTimestamp();void 0!=h&&((h<=t||h>=s)&&o("Timestamp must be between start timestamp and end timestamp."),f.length>0&&f[f.length-1]>=h&&o("Timestamp must be greater than any previous timestamp."),c.children.push({type:"timestamp",value:h,parent:c}),f.push(h))}}for(;c.parent;)"v"!=c.name&&o("Required end tag missing."),c=c.parent;return function e(t){const n={...t};return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(a)}},s=function(){function t(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,s=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),s=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+s).padStart(2,"0")+"."+t}function n(e){for(var i="",s=0;s/g,">");else if("object"==a.type){if(i+="<"+a.name,a.classes)for(var r=0;r"}else"timestamp"==a.type?i+="<"+t(a.value)+">":i+="<"+a.value+">"}return i}function i(i){return(void 0!==i.id?i.id+"\n":"")+t(i.startTime)+" --\x3e "+t(i.endTime)+function(t){var n="";const i=Object.keys(e).filter(n=>t[n]!==e[n]);return i.includes("direction")&&(n+=" vertical:"+t.direction),i.includes("alignment")&&(n+=" align:"+t.alignment),i.includes("size")&&(n+=" size:"+t.size+"%"),(i.includes("lineAlign")||i.includes("linePosition"))&&(n+=" line:"+t.linePosition+(t.snapToLines?"":"%")+(t.lineAlign&&t.lineAlign!=e.lineAlign?","+t.lineAlign:"")),(i.includes("textPosition")||i.includes("positionAlign"))&&(n+=" position:"+t.textPosition+"%"+(t.positionAlign&&t.positionAlign!==e.positionAlign?","+t.positionAlign:"")),n}(i)+"\n"+n(i.tree.children)+"\n\n"}function s(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var n="WEBVTT\n\n";if(t)for(var a=0;a{if(t.closed)return;const a=URL.createObjectURL(new Blob([s],{type:"text/vtt"})),r=function(e){let n=!1,i=()=>{};const s={free(){n||(n=!0,i(),URL.revokeObjectURL(e))}};return i=t.own(s.free),s}(a);if(t.closed)return;const o=i;let l,c,f=!1;i=r;try{if(l=e.option.subtitle,c=l.escape,t.closed||i!==r)return;l.escape=!1,f=!0;const s={...l,url:a,type:"vtt",onVttLoad:n};if(t.closed||i!==r)return;const o=e.subtitle.init(s);Promise.resolve(o).catch(e=>{i===r&&(i=null,r.free()),t.closed||console.warn("Failed to initialize multiple subtitles:",e)})}catch(u){throw i===r&&(i=o,f&&!1===l.escape&&(l.escape=c)),r.free(),u}finally{o!==i&&o?.free(),r!==i&&r.free()}}}function c({subtitles:e=[]}){return async n=>{const{unescape:i,getExt:c,srtToVtt:f,assToVtt:u}=n.constructor.utils,d=function(e){let t=Boolean(e.isDestroy);const n=new Set;let i;const s=new Promise(e=>{i=e});function a(e){try{e()}catch(t){console.warn("Failed to clean up multiple subtitles:",t)}}const r={get closed(){return t},own:e=>(t?a(e):n.add(e),()=>n.delete(e)),wait:e=>Promise.race([e,s]),dispose(){if(t)return;t=!0,i();const e=[...n].reverse();n.clear();for(const t of e)a(t)}};if(!t){r.own(()=>e.off("destroy",r.dispose));try{e.on("destroy",r.dispose)}catch(o){throw r.dispose(),o}}return r}(n),p=l(n,d,i);let m=[];function h(e){d.closed||p(function(e){const t=[];for(const n of e){const e=n;for(const n of e.cues)t.push({...n,tree:{...n.tree,children:n.tree.children.map(t=>{if("timestamp"===t.type)return r(t);const n=`
${t.value}
`;return"object"===t.type?{...t,value:n,children:t.children.map(o)}:{...t,value:n}})}})}return(new s).serialize(t)}(e))}const g={name:"multipleSubtitles",tracks(e=[]){d.closed||h(e.map(e=>m.find(t=>t.name===e)))},reset(){h(m)}};try{if(d.closed)return g;const i=await Promise.all(e.map(e=>async function(e,{getExt:t,srtToVtt:n,assToVtt:i},s){if(s.closed)return;const a="function"===typeof AbortController?new AbortController:null,r=s.own(()=>a?.abort());try{const r=e.url;if(s.closed)return;const o=await s.wait(a?fetch(r,{signal:a.signal}):fetch(r));if(s.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch multiple subtitles: HTTP ${o.status}`);const l=await s.wait(o.arrayBuffer());if(s.closed)return;const c=new TextDecoder(e.encoding||"utf-8").decode(l);switch(e.type||t(e.url)){case"srt":return n(c);case"ass":return i(c);case"vtt":return c;default:return""}}catch(o){throw a?.abort(),o}finally{r()}}(e,{getExt:c,srtToVtt:f,assToVtt:u},d)));return d.closed||(m=function(e,n){const i=new t;return e.map((e,t)=>({...i.parse(e,"metadata"),url:n[t].url,name:n[t].name}))}(i,e),function(e,t){function n(){if(!t.closed)for(const t of Array.from(e.template.$subtitle.getElementsByTagName(`c.${a}`))){const e=t.firstChild;3===e?.nodeType&&(e.nodeValue=(e.nodeValue||"").replace(/^<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/,"")),t.replaceWith(...Array.from(t.childNodes))}}t.closed||(t.own(()=>e.off("subtitleAfterUpdate",n)),e.on("subtitleAfterUpdate",n))}(n,d),h(m)),g}catch(v){throw d.dispose(),v}}}return Object.assign(c,{default:c})}); +!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={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\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),(gb&&" "!==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.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||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"==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;r100)){a("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){a("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){a("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(c)){f=c.split(","),c=f[0];var m=f[1]}if("%"!=c[c.length-1]){a("Text position must be a percentage.");continue}if(parseInt(c,10)>100||parseInt(c,10)<0){a("Text position needs to be between 0 and 100%.");continue}if(""===(d=c.slice(0,c.length-1))||isNaN(d)||!isFinite(d)){a("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){a("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=c[c.length-1]){a("Size must be a percentage.");continue}if(parseInt(c,10)>100){a("Size cannot be >100%.");continue}var g=c.slice(0,c.length-1);if(void 0===g||""===g||isNaN(g)){a("Size needs to be a number"),g=100;continue}if((g=parseFloat(g))<0||g>100){a("Size needs to be between 0 and 100%.");continue}t.size=g}else if("align"==l){var h=["start","center","end","left","right"];if(-1==h.indexOf(c)){a("Alignment can only be set to one of "+h.join(", ")+".");continue}t.alignment=c}else a("Invalid setting.")}}(e.substring(s),t),!0}else a("No valid timestamp separator found.");else a("No valid timestamp separator found.");else a("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[s])return t;a("Timestamp must not have trailing characters.")}},i=function(e,t,i,s){this.entities=s;var a=this,r=(e=e,0),o=function(e){"metadata"!=i&&t(e,r+1)};function l(){for(var t="data",n="",i="",l=[];void 0!=e[r-1]||0==r;){var c=e[r];if("data"==t)if("&"==c)i=c,t="escape";else if("<"==c&&""==n)t="tag";else{if("<"==c||void 0==c)return["text",n];n+=c}else if("escape"==t){if("<"==c||void 0==c){let e;return o("Incorrect escape."),(e=i.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):a.entities[i]?n+=a.entities[i]:n+=i,["text",n]}if("&"==c)o("Incorrect escape."),n+=i,i=c;else if(/[a-z#0-9]/i.test(c))i+=c;else if(";"==c){let e;(e=i.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):a.entities[i+c]?n+=a.entities[i+c]:(e=Object.keys(s).find(e=>i.startsWith(e)))?n+=a.entities[e]+i.slice(e.length)+c:(o("Incorrect escape."),n+=i+";"),t="data"}else o("Incorrect escape."),n+=i+c,t="data"}else if("tag"==t)if("\t"==c||"\n"==c||"\f"==c||" "==c)t="start tag annotation";else if("."==c)t="start tag class";else if("/"==c)t="end tag";else if(/\d/.test(c))n=c,t="timestamp tag";else{if(">"==c||void 0==c)return">"==c&&r++,["start tag","",[],""];n=c,t="start tag"}else if("start tag"==t)if("\t"==c||"\f"==c||" "==c)t="start tag annotation";else if("\n"==c)i=c,t="start tag annotation";else if("."==c)t="start tag class";else{if(">"==c||void 0==c)return">"==c&&r++,["start tag",n,[],""];n+=c}else if("start tag class"==t)if("\t"==c||"\f"==c||" "==c)i&&l.push(i),i="",t="start tag annotation";else if("\n"==c)i&&l.push(i),i=c,t="start tag annotation";else if("."==c)i&&l.push(i),i="";else{if(">"==c||void 0==c)return">"==c&&r++,i&&l.push(i),["start tag",n,l,""];i+=c}else if("start tag annotation"==t){if(">"==c||void 0==c)return">"==c&&r++,["start tag",n,l,i=i.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];i+=c}else if("end tag"==t){if(">"==c||void 0==c)return">"==c&&r++,["end tag",n];n+=c}else if("timestamp tag"==t){if(">"==c||void 0==c)return">"==c&&r++,["timestamp",n];n+=c}else o("Never happens.");r++}}this.parse=function(t,s){var a={children:[]},c=a,f=[];function u(e){c.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:c}),c=c.children[c.children.length-1]}function d(e){for(var t=c;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[r];){var p=l();if("text"==p[0])c.children.push({type:"text",value:p[1],parent:c});else if("start tag"==p[0]){"chapters"==i&&o("Start tags not allowed in chapter title text.");var m=p[1];"v"!=m&&"lang"!=m&&""!=p[3]&&o("Only and can have an annotation."),"c"==m||"i"==m||"b"==m||"u"==m||"ruby"==m||"rt"==m&&"ruby"==c.name?u(p):"v"==m?(d("v")&&o(" cannot be nested inside itself."),u(p),c.value=p[3],p[3]||o(" requires an annotation.")):"lang"==m?(u(p),c.value=p[3]):o("Incorrect start tag.")}else if("end tag"==p[0])"chapters"==i&&o("End tags not allowed in chapter title text."),p[1]==c.name?c=c.parent:"ruby"==p[1]&&"rt"==c.name?c=c.parent.parent:o("Incorrect end tag.");else if("timestamp"==p[0]){"chapters"==i&&o("Timestamp not allowed in chapter title text.");var g=new n(p[1],o).parseTimestamp();void 0!=g&&((g<=t||g>=s)&&o("Timestamp must be between start timestamp and end timestamp."),f.length>0&&f[f.length-1]>=g&&o("Timestamp must be greater than any previous timestamp."),c.children.push({type:"timestamp",value:g,parent:c}),f.push(g))}}for(;c.parent;)"v"!=c.name&&o("Required end tag missing."),c=c.parent;return function e(t){const n={...t};return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(a)}},s=function(){function t(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,s=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),s=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+s).padStart(2,"0")+"."+t}function n(e){for(var i="",s=0;s/g,">");else if("object"==a.type){if(i+="<"+a.name,a.classes)for(var r=0;r"}else"timestamp"==a.type?i+="<"+t(a.value)+">":i+="<"+a.value+">"}return i}function i(i){return(void 0!==i.id?i.id+"\n":"")+t(i.startTime)+" --\x3e "+t(i.endTime)+function(t){var n="";const i=Object.keys(e).filter(n=>t[n]!==e[n]);return i.includes("direction")&&(n+=" vertical:"+t.direction),i.includes("alignment")&&(n+=" align:"+t.alignment),i.includes("size")&&(n+=" size:"+t.size+"%"),(i.includes("lineAlign")||i.includes("linePosition"))&&(n+=" line:"+t.linePosition+(t.snapToLines?"":"%")+(t.lineAlign&&t.lineAlign!=e.lineAlign?","+t.lineAlign:"")),(i.includes("textPosition")||i.includes("positionAlign"))&&(n+=" position:"+t.textPosition+"%"+(t.positionAlign&&t.positionAlign!==e.positionAlign?","+t.positionAlign:"")),n}(i)+"\n"+n(i.tree.children)+"\n\n"}function s(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var n="WEBVTT\n\n";if(t)for(var a=0;a",">":">","&lrm":"\u200e","‎":"\u200e","&rlm":"\u200f","‏":"\u200f"," ":"\xa0"," ":"\xa0"};const r="artplayer-multiple-subtitles-timestamp";function o(e){return{type:"object",name:"c",classes:[r],children:[e]}}function l(e){return"timestamp"===e.type?o(e):"object"===e.type?{...e,children:e.children.map(l)}:{...e,value:(t=e.value,t.replace(/&/g,"&").replace(//g,">"))};var t}function c(e,t,n){let i=null;return s=>{if(t.closed)return;const a=URL.createObjectURL(new Blob([s],{type:"text/vtt"})),r=function(e){let n=!1,i=()=>{};const s={free(){n||(n=!0,i(),URL.revokeObjectURL(e))}};return i=t.own(s.free),s}(a);if(t.closed)return;const o=i;let l,c,f=!1;i=r;try{if(l=e.option.subtitle,c=l.escape,t.closed||i!==r)return;l.escape=!1,f=!0;const s={...l,url:a,type:"vtt",onVttLoad:n};if(t.closed||i!==r)return;const o=e.subtitle.init(s);Promise.resolve(o).catch(e=>{i===r&&(i=null,r.free()),t.closed||console.warn("Failed to initialize multiple subtitles:",e)})}catch(u){throw i===r&&(i=o,f&&!1===l.escape&&(l.escape=c)),r.free(),u}finally{o!==i&&o?.free(),r!==i&&r.free()}}}function f({subtitles:e=[]}){return async n=>{const{unescape:i,getExt:f,srtToVtt:u,assToVtt:d}=n.constructor.utils,p=function(e){let t=Boolean(e.isDestroy);const n=new Set;let i;const s=new Promise(e=>{i=e});function a(e){try{e()}catch(t){console.warn("Failed to clean up multiple subtitles:",t)}}const r={get closed(){return t},own:e=>(t?a(e):n.add(e),()=>n.delete(e)),wait:e=>Promise.race([e,s]),dispose(){if(t)return;t=!0,i();const e=[...n].reverse();n.clear();for(const t of e)a(t)}};if(!t){r.own(()=>e.off("destroy",r.dispose));try{e.on("destroy",r.dispose)}catch(o){throw r.dispose(),o}}return r}(n),m=c(n,p,i);let g=[];function h(e){p.closed||m(function(e){const t=[];for(const n of e){const e=n;for(const n of e.cues){const i=[];for(const t of n.tree.children)"timestamp"===t.type?i.push(o(t)):i.push({type:"text",value:`
`},l(t),{type:"text",value:"
"});t.push({...n,tree:{...n.tree,children:i}})}}return(new s).serialize(t)}(e))}const v={name:"multipleSubtitles",tracks(e=[]){p.closed||h(e.map(e=>g.find(t=>t.name===e)))},reset(){h(g)}};try{if(p.closed)return v;const i=await Promise.all(e.map(e=>async function(e,{getExt:t,srtToVtt:n,assToVtt:i},s){if(s.closed)return;const a="function"===typeof AbortController?new AbortController:null,r=s.own(()=>a?.abort());try{const r=e.url;if(s.closed)return;const o=await s.wait(a?fetch(r,{signal:a.signal}):fetch(r));if(s.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch multiple subtitles: HTTP ${o.status}`);const l=await s.wait(o.arrayBuffer());if(s.closed)return;const c=new TextDecoder(e.encoding||"utf-8").decode(l);switch(e.type||t(e.url)){case"srt":return n(c);case"ass":return i(c);case"vtt":return c;default:return""}}catch(o){throw a?.abort(),o}finally{r()}}(e,{getExt:f,srtToVtt:u,assToVtt:d},p)));return p.closed||(g=function(e,n){const i=new t(a);return e.map((e,t)=>({...i.parse(e,"metadata"),url:n[t].url,name:n[t].name}))}(i,e),function(e,t){function n(){if(!t.closed)for(const t of Array.from(e.template.$subtitle.getElementsByTagName(`c.${r}`))){const e=t.firstChild;3===e?.nodeType&&(e.nodeValue=(e.nodeValue||"").replace(/^<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/,"")),t.replaceWith(...Array.from(t.childNodes))}}t.closed||(t.own(()=>e.off("subtitleAfterUpdate",n)),e.on("subtitleAfterUpdate",n))}(n,p),h(g)),v}catch(b){throw p.dispose(),b}}}return Object.assign(f,{default:f})}); diff --git a/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.legacy.js b/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.legacy.js index 107edd0e3..27cc418fc 100644 --- a/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.legacy.js +++ b/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.legacy.js @@ -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={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\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),(hb&&" "!==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.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||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"==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=0;a100)){s("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){s("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(c)){f=c.split(","),c=f[0];var m=f[1]}if("%"!=c[c.length-1]){s("Text position must be a percentage.");continue}if(parseInt(c,10)>100||parseInt(c,10)<0){s("Text position needs to be between 0 and 100%.");continue}if(""===(d=c.slice(0,c.length-1))||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){s("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=c[c.length-1]){s("Size must be a percentage.");continue}if(parseInt(c,10)>100){s("Size cannot be >100%.");continue}var h=c.slice(0,c.length-1);if(void 0===h||""===h||isNaN(h)){s("Size needs to be a number"),h=100;continue}if((h=parseFloat(h))<0||h>100){s("Size needs to be between 0 and 100%.");continue}t.size=h}else if("align"==l){var g=["start","center","end","left","right"];if(-1==g.indexOf(c)){s("Alignment can only be set to one of "+g.join(", ")+".");continue}t.alignment=c}else s("Invalid setting.")}}(e.substring(r),t),!0}else s("No valid timestamp separator found.");else s("No valid timestamp separator found.");else s("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[r])return t;s("Timestamp must not have trailing characters.")}},p=function(e,t,n,i){this.entities=i;var r=this,s=(e=e,0),a=function(e){"metadata"!=n&&t(e,s+1)};function l(){for(var t="data",n="",o="",l=[];void 0!=e[s-1]||0==s;){var c=e[s];if("data"==t)if("&"==c)o=c,t="escape";else if("<"==c&&""==n)t="tag";else{if("<"==c||void 0==c)return["text",n];n+=c}else if("escape"==t){if("<"==c||void 0==c){let e;return a("Incorrect escape."),(e=o.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):r.entities[o]?n+=r.entities[o]:n+=o,["text",n]}if("&"==c)a("Incorrect escape."),n+=o,o=c;else if(/[a-z#0-9]/i.test(c))o+=c;else if(";"==c){let e;(e=o.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):r.entities[o+c]?n+=r.entities[o+c]:(e=Object.keys(i).find(e=>o.startsWith(e)))?n+=r.entities[e]+o.slice(e.length)+c:(a("Incorrect escape."),n+=o+";"),t="data"}else a("Incorrect escape."),n+=o+c,t="data"}else if("tag"==t)if("\t"==c||"\n"==c||"\f"==c||" "==c)t="start tag annotation";else if("."==c)t="start tag class";else if("/"==c)t="end tag";else if(/\d/.test(c))n=c,t="timestamp tag";else{if(">"==c||void 0==c)return">"==c&&s++,["start tag","",[],""];n=c,t="start tag"}else if("start tag"==t)if("\t"==c||"\f"==c||" "==c)t="start tag annotation";else if("\n"==c)o=c,t="start tag annotation";else if("."==c)t="start tag class";else{if(">"==c||void 0==c)return">"==c&&s++,["start tag",n,[],""];n+=c}else if("start tag class"==t)if("\t"==c||"\f"==c||" "==c)o&&l.push(o),o="",t="start tag annotation";else if("\n"==c)o&&l.push(o),o=c,t="start tag annotation";else if("."==c)o&&l.push(o),o="";else{if(">"==c||void 0==c)return">"==c&&s++,o&&l.push(o),["start tag",n,l,""];o+=c}else if("start tag annotation"==t){if(">"==c||void 0==c)return">"==c&&s++,["start tag",n,l,o=o.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];o+=c}else if("end tag"==t){if(">"==c||void 0==c)return">"==c&&s++,["end tag",n];n+=c}else if("timestamp tag"==t){if(">"==c||void 0==c)return">"==c&&s++,["timestamp",n];n+=c}else a("Never happens.");s++}}this.parse=function(t,i){var r={children:[]},c=r,f=[];function u(e){c.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:c}),c=c.children[c.children.length-1]}function p(e){for(var t=c;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[s];){var m=l();if("text"==m[0])c.children.push({type:"text",value:m[1],parent:c});else if("start tag"==m[0]){"chapters"==n&&a("Start tags not allowed in chapter title text.");var h=m[1];"v"!=h&&"lang"!=h&&""!=m[3]&&a("Only and can have an annotation."),"c"==h||"i"==h||"b"==h||"u"==h||"ruby"==h||"rt"==h&&"ruby"==c.name?u(m):"v"==h?(p("v")&&a(" cannot be nested inside itself."),u(m),c.value=m[3],m[3]||a(" requires an annotation.")):"lang"==h?(u(m),c.value=m[3]):a("Incorrect start tag.")}else if("end tag"==m[0])"chapters"==n&&a("End tags not allowed in chapter title text."),m[1]==c.name?c=c.parent:"ruby"==m[1]&&"rt"==c.name?c=c.parent.parent:a("Incorrect end tag.");else if("timestamp"==m[0]){"chapters"==n&&a("Timestamp not allowed in chapter title text.");var g=new d(m[1],a).parseTimestamp();void 0!=g&&((g<=t||g>=i)&&a("Timestamp must be between start timestamp and end timestamp."),f.length>0&&f[f.length-1]>=g&&a("Timestamp must be greater than any previous timestamp."),c.children.push({type:"timestamp",value:g,parent:c}),f.push(g))}}for(;c.parent;)"v"!=c.name&&a("Required end tag missing."),c=c.parent;return function e(t){const n=o({},t);return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(r)}},m=function(){function e(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,r=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),r=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+r).padStart(2,"0")+"."+t}function t(n){for(var i="",r=0;r/g,">");else if("object"==s.type){if(i+="<"+s.name,s.classes)for(var a=0;a"}else"timestamp"==s.type?i+="<"+e(s.value)+">":i+="<"+s.value+">"}return i}function n(n){return(void 0!==n.id?n.id+"\n":"")+e(n.startTime)+" --\x3e "+e(n.endTime)+function(e){var t="";const n=Object.keys(f).filter(t=>e[t]!==f[t]);return n.includes("direction")&&(t+=" vertical:"+e.direction),n.includes("alignment")&&(t+=" align:"+e.alignment),n.includes("size")&&(t+=" size:"+e.size+"%"),(n.includes("lineAlign")||n.includes("linePosition"))&&(t+=" line:"+e.linePosition+(e.snapToLines?"":"%")+(e.lineAlign&&e.lineAlign!=f.lineAlign?","+e.lineAlign:"")),(n.includes("textPosition")||n.includes("positionAlign"))&&(t+=" position:"+e.textPosition+"%"+(e.positionAlign&&e.positionAlign!==f.positionAlign?","+e.positionAlign:"")),t}(n)+"\n"+t(n.tree.children)+"\n\n"}function i(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var r="WEBVTT\n\n";if(t)for(var s=0;s{if(t.closed)return;const s=URL.createObjectURL(new Blob([r],{type:"text/vtt"})),a=function(e){let n=!1,i=()=>{};const r={free(){n||(n=!0,i(),URL.revokeObjectURL(e))}};return i=t.own(r.free),r}(s);if(t.closed)return;const c=i;let f,u,d=!1;i=a;try{if(f=e.option.subtitle,u=f.escape,t.closed||i!==a)return;f.escape=!1,d=!0;const r=l(o({},f),{url:s,type:"vtt",onVttLoad:n});if(t.closed||i!==a)return;const c=e.subtitle.init(r);Promise.resolve(c).catch(e=>{i===a&&(i=null,a.free()),t.closed||console.warn("Failed to initialize multiple subtitles:",e)})}catch(p){throw i===a&&(i=c,d&&!1===f.escape&&(f.escape=u)),a.free(),p}finally{c!==i&&(null==c||c.free()),a!==i&&a.free()}}}function y({subtitles:e=[]}){return t=>c(null,null,function*(){const{unescape:n,getExt:i,srtToVtt:r,assToVtt:s}=t.constructor.utils,a=function(e){let t=Boolean(e.isDestroy);const n=new Set;let i;const r=new Promise(e=>{i=e});function s(e){try{e()}catch(t){console.warn("Failed to clean up multiple subtitles:",t)}}const a={get closed(){return t},own:e=>(t?s(e):n.add(e),()=>n.delete(e)),wait:e=>Promise.race([e,r]),dispose(){if(t)return;t=!0,i();const e=[...n].reverse();n.clear();for(const t of e)s(t)}};if(!t){a.own(()=>e.off("destroy",a.dispose));try{e.on("destroy",a.dispose)}catch(o){throw a.dispose(),o}}return a}(t),f=b(t,a,n);let d=[];function p(e){a.closed||f(function(e){const t=[];for(const n of e){const e=n;for(const n of e.cues)t.push(l(o({},n),{tree:l(o({},n.tree),{children:n.tree.children.map(t=>{if("timestamp"===t.type)return g(t);const n=`
${t.value}
`;return"object"===t.type?l(o({},t),{value:n,children:t.children.map(v)}):l(o({},t),{value:n})})})}))}return(new m).serialize(t)}(e))}const y={name:"multipleSubtitles",tracks(e=[]){a.closed||p(e.map(e=>d.find(t=>t.name===e)))},reset(){p(d)}};try{if(a.closed)return y;const n=yield Promise.all(e.map(e=>function(e,t,n){return c(this,arguments,function*(e,{getExt:t,srtToVtt:n,assToVtt:i},r){if(r.closed)return;const s="function"===typeof AbortController?new AbortController:null,a=r.own(()=>null==s?void 0:s.abort());try{const a=e.url;if(r.closed)return;const o=yield r.wait(s?fetch(a,{signal:s.signal}):fetch(a));if(r.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch multiple subtitles: HTTP ${o.status}`);const l=yield r.wait(o.arrayBuffer());if(r.closed)return;const c=new TextDecoder(e.encoding||"utf-8").decode(l);switch(e.type||t(e.url)){case"srt":return n(c);case"ass":return i(c);case"vtt":return c;default:return""}}catch(o){throw null==s||s.abort(),o}finally{a()}})}(e,{getExt:i,srtToVtt:r,assToVtt:s},a)));return a.closed||(d=function(e,t){const n=new u;return e.map((e,i)=>{const r=n.parse(e,"metadata");return l(o({},r),{url:t[i].url,name:t[i].name})})}(n,e),function(e,t){function n(){if(!t.closed)for(const t of Array.from(e.template.$subtitle.getElementsByTagName(`c.${h}`))){const e=t.firstChild;3===(null==e?void 0:e.nodeType)&&(e.nodeValue=(e.nodeValue||"").replace(/^<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/,"")),t.replaceWith(...Array.from(t.childNodes))}}t.closed||(t.own(()=>e.off("subtitleAfterUpdate",n)),e.on("subtitleAfterUpdate",n))}(t,a),p(d)),y}catch(x){throw a.dispose(),x}})}return Object.assign(y,{default:y})}); +!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={"&":"&","<":"<",">":">","&lrm":"\u200e","&rlm":"\u200f"," ":"\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),(gb&&" "!==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.startTimet.startTime?1:e.endTime>t.endTime?-1:e.endTime2||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"==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=0;a100)){s("Line position cannot be >100%.");continue}if(""===d||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==u){if(!["start","center","end"].includes(u)){s("Line alignment needs to be one of start, center or end");continue}t.lineAlign=u}t.snapToLines=!p,t.linePosition=parseFloat(d),parseFloat(d).toString()!==d&&(t.nonSerializable=!0)}else if("position"==l){if(/,/.test(c)){f=c.split(","),c=f[0];var m=f[1]}if("%"!=c[c.length-1]){s("Text position must be a percentage.");continue}if(parseInt(c,10)>100||parseInt(c,10)<0){s("Text position needs to be between 0 and 100%.");continue}if(""===(d=c.slice(0,c.length-1))||isNaN(d)||!isFinite(d)){s("Line position needs to be a number");continue}if(void 0!==m){if(!["line-left","center","line-right"].includes(m)){s("Position alignment needs to be one of line-left, center or line-right");continue}t.positionAlign=m}t.textPosition=parseFloat(d)}else if("size"==l){if("%"!=c[c.length-1]){s("Size must be a percentage.");continue}if(parseInt(c,10)>100){s("Size cannot be >100%.");continue}var g=c.slice(0,c.length-1);if(void 0===g||""===g||isNaN(g)){s("Size needs to be a number"),g=100;continue}if((g=parseFloat(g))<0||g>100){s("Size needs to be between 0 and 100%.");continue}t.size=g}else if("align"==l){var h=["start","center","end","left","right"];if(-1==h.indexOf(c)){s("Alignment can only be set to one of "+h.join(", ")+".");continue}t.alignment=c}else s("Invalid setting.")}}(e.substring(r),t),!0}else s("No valid timestamp separator found.");else s("No valid timestamp separator found.");else s("No valid timestamp separator found.")},this.parseTimestamp=function(){var t=l();if(void 0==e[r])return t;s("Timestamp must not have trailing characters.")}},p=function(e,t,n,i){this.entities=i;var r=this,s=(e=e,0),a=function(e){"metadata"!=n&&t(e,s+1)};function l(){for(var t="data",n="",o="",l=[];void 0!=e[s-1]||0==s;){var c=e[s];if("data"==t)if("&"==c)o=c,t="escape";else if("<"==c&&""==n)t="tag";else{if("<"==c||void 0==c)return["text",n];n+=c}else if("escape"==t){if("<"==c||void 0==c){let e;return a("Incorrect escape."),(e=o.match(/^&#([0-9]+)$/))?n+=String.fromCharCode(e[1]):r.entities[o]?n+=r.entities[o]:n+=o,["text",n]}if("&"==c)a("Incorrect escape."),n+=o,o=c;else if(/[a-z#0-9]/i.test(c))o+=c;else if(";"==c){let e;(e=o.match(/^&#(x?[0-9]+)$/))?n+=String.fromCharCode("0"+e[1]):r.entities[o+c]?n+=r.entities[o+c]:(e=Object.keys(i).find(e=>o.startsWith(e)))?n+=r.entities[e]+o.slice(e.length)+c:(a("Incorrect escape."),n+=o+";"),t="data"}else a("Incorrect escape."),n+=o+c,t="data"}else if("tag"==t)if("\t"==c||"\n"==c||"\f"==c||" "==c)t="start tag annotation";else if("."==c)t="start tag class";else if("/"==c)t="end tag";else if(/\d/.test(c))n=c,t="timestamp tag";else{if(">"==c||void 0==c)return">"==c&&s++,["start tag","",[],""];n=c,t="start tag"}else if("start tag"==t)if("\t"==c||"\f"==c||" "==c)t="start tag annotation";else if("\n"==c)o=c,t="start tag annotation";else if("."==c)t="start tag class";else{if(">"==c||void 0==c)return">"==c&&s++,["start tag",n,[],""];n+=c}else if("start tag class"==t)if("\t"==c||"\f"==c||" "==c)o&&l.push(o),o="",t="start tag annotation";else if("\n"==c)o&&l.push(o),o=c,t="start tag annotation";else if("."==c)o&&l.push(o),o="";else{if(">"==c||void 0==c)return">"==c&&s++,o&&l.push(o),["start tag",n,l,""];o+=c}else if("start tag annotation"==t){if(">"==c||void 0==c)return">"==c&&s++,["start tag",n,l,o=o.split(/[\u0020\t\f\r\n]+/).filter(function(e){if(e)return!0}).join(" ")];o+=c}else if("end tag"==t){if(">"==c||void 0==c)return">"==c&&s++,["end tag",n];n+=c}else if("timestamp tag"==t){if(">"==c||void 0==c)return">"==c&&s++,["timestamp",n];n+=c}else a("Never happens.");s++}}this.parse=function(t,i){var r={children:[]},c=r,f=[];function u(e){c.children.push({type:"object",name:e[1],classes:e[2],children:[],parent:c}),c=c.children[c.children.length-1]}function p(e){for(var t=c;t;){if(t.name==e)return!0;t=t.parent}}for(;void 0!=e[s];){var m=l();if("text"==m[0])c.children.push({type:"text",value:m[1],parent:c});else if("start tag"==m[0]){"chapters"==n&&a("Start tags not allowed in chapter title text.");var g=m[1];"v"!=g&&"lang"!=g&&""!=m[3]&&a("Only and can have an annotation."),"c"==g||"i"==g||"b"==g||"u"==g||"ruby"==g||"rt"==g&&"ruby"==c.name?u(m):"v"==g?(p("v")&&a(" cannot be nested inside itself."),u(m),c.value=m[3],m[3]||a(" requires an annotation.")):"lang"==g?(u(m),c.value=m[3]):a("Incorrect start tag.")}else if("end tag"==m[0])"chapters"==n&&a("End tags not allowed in chapter title text."),m[1]==c.name?c=c.parent:"ruby"==m[1]&&"rt"==c.name?c=c.parent.parent:a("Incorrect end tag.");else if("timestamp"==m[0]){"chapters"==n&&a("Timestamp not allowed in chapter title text.");var h=new d(m[1],a).parseTimestamp();void 0!=h&&((h<=t||h>=i)&&a("Timestamp must be between start timestamp and end timestamp."),f.length>0&&f[f.length-1]>=h&&a("Timestamp must be greater than any previous timestamp."),c.children.push({type:"timestamp",value:h,parent:c}),f.push(h))}}for(;c.parent;)"v"!=c.name&&a("Required end tag missing."),c=c.parent;return function e(t){const n=o({},t);return t.children&&(n.children=t.children.map(e)),n.parent&&delete n.parent,n}(r)}},m=function(){function e(e){const t=("00"+1e3*(e-Math.floor(e)).toFixed(3)).slice(-3);let n=0,i=0,r=0;return e>=3600&&(n=Math.floor(e/3600)),i=Math.floor((e-3600*n)/60),r=Math.floor(e-3600*n-60*i),(n?n+":":"")+(""+i).padStart(2,"0")+":"+(""+r).padStart(2,"0")+"."+t}function t(n){for(var i="",r=0;r/g,">");else if("object"==s.type){if(i+="<"+s.name,s.classes)for(var a=0;a"}else"timestamp"==s.type?i+="<"+e(s.value)+">":i+="<"+s.value+">"}return i}function n(n){return(void 0!==n.id?n.id+"\n":"")+e(n.startTime)+" --\x3e "+e(n.endTime)+function(e){var t="";const n=Object.keys(f).filter(t=>e[t]!==f[t]);return n.includes("direction")&&(t+=" vertical:"+e.direction),n.includes("alignment")&&(t+=" align:"+e.alignment),n.includes("size")&&(t+=" size:"+e.size+"%"),(n.includes("lineAlign")||n.includes("linePosition"))&&(t+=" line:"+e.linePosition+(e.snapToLines?"":"%")+(e.lineAlign&&e.lineAlign!=f.lineAlign?","+e.lineAlign:"")),(n.includes("textPosition")||n.includes("positionAlign"))&&(t+=" position:"+e.textPosition+"%"+(e.positionAlign&&e.positionAlign!==f.positionAlign?","+e.positionAlign:"")),t}(n)+"\n"+t(n.tree.children)+"\n\n"}function i(e){return"STYLE\n"+e+"\n\n"}this.serialize=function(e,t){var r="WEBVTT\n\n";if(t)for(var s=0;s",">":">","&lrm":"\u200e","‎":"\u200e","&rlm":"\u200f","‏":"\u200f"," ":"\xa0"," ":"\xa0"};const h="artplayer-multiple-subtitles-timestamp";function v(e){return{type:"object",name:"c",classes:[h],children:[e]}}function b(e){return"timestamp"===e.type?v(e):"object"===e.type?l(o({},e),{children:e.children.map(b)}):l(o({},e),{value:(t=e.value,t.replace(/&/g,"&").replace(//g,">"))});var t}function y(e,t,n){let i=null;return r=>{if(t.closed)return;const s=URL.createObjectURL(new Blob([r],{type:"text/vtt"})),a=function(e){let n=!1,i=()=>{};const r={free(){n||(n=!0,i(),URL.revokeObjectURL(e))}};return i=t.own(r.free),r}(s);if(t.closed)return;const c=i;let f,u,d=!1;i=a;try{if(f=e.option.subtitle,u=f.escape,t.closed||i!==a)return;f.escape=!1,d=!0;const r=l(o({},f),{url:s,type:"vtt",onVttLoad:n});if(t.closed||i!==a)return;const c=e.subtitle.init(r);Promise.resolve(c).catch(e=>{i===a&&(i=null,a.free()),t.closed||console.warn("Failed to initialize multiple subtitles:",e)})}catch(p){throw i===a&&(i=c,d&&!1===f.escape&&(f.escape=u)),a.free(),p}finally{c!==i&&(null==c||c.free()),a!==i&&a.free()}}}function x({subtitles:e=[]}){return t=>c(null,null,function*(){const{unescape:n,getExt:i,srtToVtt:r,assToVtt:s}=t.constructor.utils,a=function(e){let t=Boolean(e.isDestroy);const n=new Set;let i;const r=new Promise(e=>{i=e});function s(e){try{e()}catch(t){console.warn("Failed to clean up multiple subtitles:",t)}}const a={get closed(){return t},own:e=>(t?s(e):n.add(e),()=>n.delete(e)),wait:e=>Promise.race([e,r]),dispose(){if(t)return;t=!0,i();const e=[...n].reverse();n.clear();for(const t of e)s(t)}};if(!t){a.own(()=>e.off("destroy",a.dispose));try{e.on("destroy",a.dispose)}catch(o){throw a.dispose(),o}}return a}(t),f=y(t,a,n);let d=[];function p(e){a.closed||f(function(e){const t=[];for(const n of e){const e=n;for(const n of e.cues){const i=[];for(const t of n.tree.children)"timestamp"===t.type?i.push(v(t)):i.push({type:"text",value:`
`},b(t),{type:"text",value:"
"});t.push(l(o({},n),{tree:l(o({},n.tree),{children:i})}))}}return(new m).serialize(t)}(e))}const x={name:"multipleSubtitles",tracks(e=[]){a.closed||p(e.map(e=>d.find(t=>t.name===e)))},reset(){p(d)}};try{if(a.closed)return x;const n=yield Promise.all(e.map(e=>function(e,t,n){return c(this,arguments,function*(e,{getExt:t,srtToVtt:n,assToVtt:i},r){if(r.closed)return;const s="function"===typeof AbortController?new AbortController:null,a=r.own(()=>null==s?void 0:s.abort());try{const a=e.url;if(r.closed)return;const o=yield r.wait(s?fetch(a,{signal:s.signal}):fetch(a));if(r.closed)return;if(!1===o.ok)throw new Error(`Failed to fetch multiple subtitles: HTTP ${o.status}`);const l=yield r.wait(o.arrayBuffer());if(r.closed)return;const c=new TextDecoder(e.encoding||"utf-8").decode(l);switch(e.type||t(e.url)){case"srt":return n(c);case"ass":return i(c);case"vtt":return c;default:return""}}catch(o){throw null==s||s.abort(),o}finally{a()}})}(e,{getExt:i,srtToVtt:r,assToVtt:s},a)));return a.closed||(d=function(e,t){const n=new u(g);return e.map((e,i)=>{const r=n.parse(e,"metadata");return l(o({},r),{url:t[i].url,name:t[i].name})})}(n,e),function(e,t){function n(){if(!t.closed)for(const t of Array.from(e.template.$subtitle.getElementsByTagName(`c.${h}`))){const e=t.firstChild;3===(null==e?void 0:e.nodeType)&&(e.nodeValue=(e.nodeValue||"").replace(/^<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/,"")),t.replaceWith(...Array.from(t.childNodes))}}t.closed||(t.own(()=>e.off("subtitleAfterUpdate",n)),e.on("subtitleAfterUpdate",n))}(t,a),p(d)),x}catch(T){throw a.dispose(),T}})}return Object.assign(x,{default:x})}); diff --git a/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.mjs b/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.mjs index 66c7834f6..9eeab913c 100644 --- a/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.mjs +++ b/packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.mjs @@ -799,19 +799,36 @@ var WebVTTSerializer = function() { return result; }; }; +const cueEntities = { + "&": "&", + "&": "&", + "<": "<", + "<": "<", + ">": ">", + ">": ">", + "&lrm": "‎", + "‎": "‎", + "&rlm": "‏", + "‏": "‏", + " ": " ", + " ": " " +}; +function escapeCueText(value) { + return value.replace(/&/g, "&").replace(//g, ">"); +} 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: `
` }, + prepareCueNode(child), + { type: "text", value: "
" } + ); + } cues.push({ ...cue, tree: { ...cue.tree, - children: cue.tree.children.map((child) => { - if (child.type === "timestamp") - return markTimestamp(child); - const value = `
${child.value}
`; - return child.type === "object" ? { ...child, value, children: child.children.map(markNestedTimestamps) } : { ...child, value }; - }) + children } }); } diff --git a/packages/artplayer-plugin-multiple-subtitles/src/merge.ts b/packages/artplayer-plugin-multiple-subtitles/src/merge.ts index d1d0cce03..6e764a3bf 100644 --- a/packages/artplayer-plugin-multiple-subtitles/src/merge.ts +++ b/packages/artplayer-plugin-multiple-subtitles/src/merge.ts @@ -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: `
` }, + prepareCueNode(child), + { type: 'text', value: '
' }, + ) + } cues.push({ ...cue, tree: { ...cue.tree, - children: cue.tree.children.map((child) => { - if (child.type === 'timestamp') - return markTimestamp(child) - const value = `
${child.value}
` - return child.type === 'object' - ? { ...child, value, children: child.children.map(markNestedTimestamps) } - : { ...child, value } - }), + children, }, }) } diff --git a/packages/artplayer-plugin-multiple-subtitles/src/text.ts b/packages/artplayer-plugin-multiple-subtitles/src/text.ts new file mode 100644 index 000000000..2173da0d3 --- /dev/null +++ b/packages/artplayer-plugin-multiple-subtitles/src/text.ts @@ -0,0 +1,20 @@ +// Keep the vendor's lenient aliases while making complete named references exact. +export const cueEntities = { + '&': '&', + '&': '&', + '<': '<', + '<': '<', + '>': '>', + '>': '>', + '&lrm': '\u200E', + '‎': '\u200E', + '&rlm': '\u200F', + '‏': '\u200F', + ' ': '\u00A0', + ' ': '\u00A0', +} + +// Serialization protects the whole VTT once; this protects cue text after onVttLoad unescape. +export function escapeCueText(value: string): string { + return value.replace(/&/g, '&').replace(//g, '>') +} diff --git a/refactor/baselines/multiple-subtitles-entities-validation.json b/refactor/baselines/multiple-subtitles-entities-validation.json new file mode 100644 index 000000000..ec453c9e2 --- /dev/null +++ b/refactor/baselines/multiple-subtitles-entities-validation.json @@ -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 & < > ‎ ‏   | literal | <", + "bold & safe tail " + ], + "literal": "A & < > ‎ ‏   | literal | <", + "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 & < > ‎ ‏   | literal | <", + "bold & safe tail " + ], + "literal": "A & < > ‎ ‏   | literal | <", + "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 & < > ‎ ‏   | literal | <", + "bold & safe tail " + ], + "literal": "A & < > ‎ ‏   | literal | <", + "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 & < > ‎ ‏   | literal | <", + "bold & safe tail " + ], + "literal": "A & < > ‎ ‏   | literal | <", + "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 & < > ‎ ‏   | literal | <", + "bold & safe tail " + ], + "literal": "A & < > ‎ ‏   | literal | <", + "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 & < > ‎ ‏   | literal | <", + "bold & safe tail " + ], + "literal": "A & < > ‎ ‏   | literal | <", + "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 & < > ‎ ‏   | literal | <", + "bold & safe tail " + ], + "literal": "A & < > ‎ ‏   | literal | <", + "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 & < > ‎ ‏   | literal | <", + "bold & safe tail " + ], + "literal": "A & < > ‎ ‏   | literal | <", + "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 & < > ‎ ‏   | literal | <", + "bold & safe tail " + ], + "literal": "A & < > ‎ ‏   | literal | <", + "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 & < > ‎ ‏   | literal | <", + "bold & safe tail " + ], + "literal": "A & < > ‎ ‏   | literal | <", + "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 & < > ‎ ‏   | literal | <", + "bold & safe tail " + ], + "literal": "A & < > ‎ ‏   | literal | <", + "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 & < > ‎ ‏   | literal | <", + "bold & safe tail " + ], + "literal": "A & < > ‎ ‏   | literal | <", + "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" + ] +} diff --git a/refactor/changes/2026-09-14-PKG-MULTI-SUB-08-entities.md b/refactor/changes/2026-09-14-PKG-MULTI-SUB-08-entities.md new file mode 100644 index 000000000..af8de964f --- /dev/null +++ b/refactor/changes/2026-09-14-PKG-MULTI-SUB-08-entities.md @@ -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 `<`/`>`/`&` 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 `` and timestamp-shaped text remain visible +text; `&lt;` displays `<` 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 +`undefined>bold`. 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-`; 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. diff --git a/refactor/plan.md b/refactor/plan.md index 23b2471fe..1139b48e8 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -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
整理内部职责与资源 | PKG-MULTI-SUB-02, CORE-15 | 解析与合并/获取/渲染分离,过期请求及对象 URL 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done | | PKG-MULTI-SUB-04 | artplayer-plugin-multiple-subtitles
迁移自有源码和公开类型 | PKG-MULTI-SUB-03, ENG-04, ENG-06, CORE-07 | 准确 parser/cue/tree/result 类型与真实异步形状 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done | | PKG-MULTI-SUB-07 | artplayer-plugin-multiple-subtitles
修复字幕内嵌时间戳序列化 | PKG-MULTI-SUB-03 | 保留数字时间戳节点、修正无效 NaN 输出、明确旧/新字幕输出差异 | 旧九份实现可复现,候选有效内嵌时间戳及文字/CSS/原生字幕通过,公开工厂与选择接口不变 | H | done | -| PKG-MULTI-SUB-05 | artplayer-plugin-multiple-subtitles
验证新旧核心和组合 | PKG-MULTI-SUB-04, CORE-22, PKG-MULTI-SUB-07 | 与核心字幕/偏移/全屏组合,旧名称调用不变 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo | +| PKG-MULTI-SUB-08 | artplayer-plugin-multiple-subtitles
修复字幕实体解码与嵌套标签包装 | PKG-MULTI-SUB-04, PKG-MULTI-SUB-07 | 使用解析器既有实体表入口修复多余分号;保护字面文本并以兄弟节点包装标签,保留CSS钩子和时间戳 | 冻结旧实现复现、实体/双重编码/标签/选择重置回归、新旧核心真实字幕显示通过;vendor原文与来源校验不变,类型/分发兼容 | M | done | +| PKG-MULTI-SUB-05 | artplayer-plugin-multiple-subtitles
验证新旧核心和组合 | 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
验证分发并同步文档 | 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) diff --git a/refactor/progress.md b/refactor/progress.md index 8e87b18d9..837fd6882 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -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秒编码时限。 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index 543f81406..08d5ce587 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -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 | diff --git a/refactor/risks.json b/refactor/risks.json index 2f39af599..f9f49a4f5 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -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." } ] } diff --git a/refactor/tasks.json b/refactor/tasks.json index 622816b37..84cba8cdb 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -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", diff --git a/test/README.md b/test/README.md index 0fa22e474..56b8f3059 100644 --- a/test/README.md +++ b/test/README.md @@ -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 diff --git a/test/browser/multiple-subtitles-entities.spec.js b/test/browser/multiple-subtitles-entities.spec.js new file mode 100644 index 000000000..b60889432 --- /dev/null +++ b/test/browser/multiple-subtitles-entities.spec.js @@ -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 | literal | <' +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 & < > ‎ ‏   | <b>literal</b> | &lt;'), contentType: 'text/vtt' })) + await page.route('**/test/entities-b.vtt', route => route.fulfill({ body: vtt('bold & safe tail <i>'), 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 ']) + const native = await page.evaluate(() => window.art.subtitle.cues.map(cue => cue.getCueAsHTML().textContent)) + expect(native).toEqual([literal, 'bold & safe tail ']) + 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([]) + }) +} diff --git a/test/multiple-subtitles-entities.test.js b/test/multiple-subtitles-entities.test.js new file mode 100644 index 000000000..c37f33094 --- /dev/null +++ b/test/multiple-subtitles-entities.test.js @@ -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 [ + ['& < > ‎ ‏  ', '& < > \u200E \u200F \u00A0'], + ['Literal <00:02.000> and <b>text</b>', 'Literal <00:02.000> and text'], + ['&lt; &amp; &gt;', '< & >'], + ['One &; two <;', 'One &; two <;'], + ['bold & safe tail <i>', 'bold & safe tail '], + ['color italic', 'color italic'], + ['voice', 'voice'], + ['textnote', 'textnote'], + ['&unknown; &litude; &', '&unknown; &litude; &'], + ['A A', '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(`${text}`) + assert.equal(document.body.textContent.trim(), names.map(name => name === 'a' ? expected : 'Other').join('\n\n').trim()) + if (input.includes('<i>')) + assert.equal(document.querySelectorAll('i').length, 0) + if (input.startsWith('')) + 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(`${env.utils.unescape(await env.latestText())}`) + output.push(document.body.textContent) + env.emit('destroy') + } + assert.equal(output[1], output[0]) +}) diff --git a/test/multiple-subtitles-failures.test.js b/test/multiple-subtitles-failures.test.js index e00afc215..af0dff474 100644 --- a/test/multiple-subtitles-failures.test.js +++ b/test/multiple-subtitles-failures.test.js @@ -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('bold') } }) + await env.factory(options(subtitle()))(env.art) + assert(env.utils.unescape(await env.latestText()).includes('undefined>bold')) + }) } diff --git a/test/multiple-subtitles-merge.test.js b/test/multiple-subtitles-merge.test.js index 30e87b1b2..22fd50de3 100644 --- a/test/multiple-subtitles-merge.test.js +++ b/test/multiple-subtitles-merge.test.js @@ -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('bold tail'), - subtitleVtt('color italic'), - subtitleVtt('voice'), - subtitleVtt('textnote'), + 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'),