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

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

No files matched your search

@@ -1,6 +1,7 @@
import type { Cue, CueNode, TimestampNode, WrappedNode } from './parser'
import type { Track, TrackOption } from './types'
import { WebVTTParser, WebVTTSerializer } from './parser'
import { cueEntities, escapeCueText } from './text'
export const TIMESTAMP_CLASS = 'artplayer-multiple-subtitles-timestamp'
@@ -8,16 +9,16 @@ function markTimestamp(node: TimestampNode): CueNode {
return { type: 'object', name: 'c', classes: [TIMESTAMP_CLASS], children: [node] }
}
function markNestedTimestamps(node: CueNode): CueNode {
function prepareCueNode(node: CueNode): CueNode {
if (node.type === 'timestamp')
return markTimestamp(node)
if (node.type === 'object')
return { ...node, children: node.children.map(markNestedTimestamps) }
return node
return { ...node, children: node.children.map(prepareCueNode) }
return { ...node, value: escapeCueText(node.value) }
}
export function parseTracks(vtts: readonly string[], subtitles: readonly TrackOption[]): Track[] {
const parser = new WebVTTParser()
const parser = new WebVTTParser(cueEntities)
return vtts.map((vtt, index) => {
const tree = parser.parse(vtt, 'metadata')
// Options and completed downloads share the same Promise.all index.
@@ -31,18 +32,21 @@ export function serializeTracks(trees: readonly (Track | undefined)[]): string {
// Unknown names retain the historical TypeError at the property access below.
const tree = selected!
for (const cue of tree.cues) {
const children: CueNode[] = []
for (const child of cue.tree.children) {
if (child.type === 'timestamp')
children.push(markTimestamp(child))
else children.push(
{ type: 'text', value: `<div class="art-subtitle-${tree.name}">` },
prepareCueNode(child),
{ type: 'text', value: '</div>' },
)
}
cues.push({
...cue,
tree: {
...cue.tree,
children: cue.tree.children.map((child) => {
if (child.type === 'timestamp')
return markTimestamp(child)
const value = `<div class="art-subtitle-${tree.name}">${child.value}</div>`
return child.type === 'object'
? { ...child, value, children: child.children.map(markNestedTimestamps) }
: { ...child, value }
}),
children,
},
})
}
@@ -0,0 +1,20 @@
// Keep the vendor's lenient aliases while making complete named references exact.
export const cueEntities = {
'&amp': '&',
'&amp;': '&',
'&lt': '<',
'&lt;': '<',
'&gt': '>',
'&gt;': '>',
'&lrm': '\u200E',
'&lrm;': '\u200E',
'&rlm': '\u200F',
'&rlm;': '\u200F',
'&nbsp': '\u00A0',
'&nbsp;': '\u00A0',
}
// Serialization protects the whole VTT once; this protects cue text after onVttLoad unescape.
export function escapeCueText(value: string): string {
return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}