rx-player
Version:
Canal+ HTML5 Video Player
226 lines (208 loc) • 7.43 kB
text/typescript
/**
* Copyright 2015 CANAL+ Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ICompatVTTCue } from "../../../../compat/browser_compatibility_types";
import isVTTCue from "../../../../compat/is_vtt_cue";
import makeVTTCue from "../../../../compat/make_vtt_cue";
import isNonEmptyString from "../../../../utils/is_non_empty_string";
import isNullOrUndefined from "../../../../utils/is_null_or_undefined";
import type { IStyleList } from "../get_styling";
import getTimeDelimiters from "../get_time_delimiters";
import type { IParsedTTMLCue } from "../parse_ttml";
import { REGXP_PERCENT_VALUES } from "../regexps";
import { isLineBreakElement, isSpanElement } from "../xml_utils";
const TEXT_ALIGN_TO_LIGN_ALIGN: Partial<Record<string, string>> = {
left: "start",
center: "center",
right: "end",
start: "start",
end: "end",
};
/**
* @type {Object}
*/
const TEXT_ALIGN_TO_POSITION_ALIGN: Partial<Record<string, string>> = {
left: "line-left",
center: "center",
right: "line-right",
};
/**
* Parses an Element into a TextTrackCue or VTTCue.
* /!\ Mutates the given cueElement Element
* @param {Object} parsedCue
* @returns {TextTrackCue|null}
*/
export default function parseCue(
parsedCue: IParsedTTMLCue,
): ICompatVTTCue | TextTrackCue | null {
const { paragraph, timeOffset, paragraphStyle, ttParams, shouldTrimWhiteSpace } =
parsedCue;
// Disregard empty elements:
// TTML allows for empty elements like <div></div>.
// If paragraph has neither time attributes, nor
// non-whitespace text, don't try to make a cue out of it.
if (
!paragraph.hasAttribute("begin") &&
!paragraph.hasAttribute("end") &&
/^\s*$/.test(paragraph.textContent === null ? "" : paragraph.textContent)
) {
return null;
}
const { start, end } = getTimeDelimiters(paragraph, ttParams);
const text = generateTextContent(paragraph, shouldTrimWhiteSpace);
const cue = makeVTTCue(start + timeOffset, end + timeOffset, text);
if (cue === null) {
return null;
}
if (isVTTCue(cue)) {
addStyle(cue, paragraphStyle);
}
return cue;
}
/**
* Generate text to display for a given paragraph.
* @param {Element} paragraph - The <p> tag.
* @param {Boolean} shouldTrimWhiteSpaceForParagraph
* @returns {string}
*/
function generateTextContent(
paragraph: Element,
shouldTrimWhiteSpaceForParagraph: boolean,
): string {
/**
* Recursive function, taking a node in argument and returning the
* corresponding string.
* @param {Node} node - the node in question
* @returns {string}
*/
function loop(node: Node, shouldTrimWhiteSpaceFromParent: boolean): string {
const childNodes = node.childNodes;
let text = "";
for (let i = 0; i < childNodes.length; i++) {
const currentNode = childNodes[i];
if (currentNode.nodeName === "#text") {
let textContent = currentNode.textContent;
if (textContent === null) {
textContent = "";
}
if (shouldTrimWhiteSpaceFromParent) {
// 1. Trim leading and trailing whitespace.
// 2. Collapse multiple spaces into one.
let trimmed = textContent.trim();
trimmed = trimmed.replace(/\s+/g, " ");
textContent = trimmed;
}
// DOM Parser turns HTML escape caracters into caracters,
// that may be misinterpreted by VTTCue API (typically, less-than sign
// and greater-than sign can be interpreted as HTML tags signs).
// Original escaped caracters must be conserved.
const escapedTextContent = textContent
.replace(/&|\u0026/g, "&")
.replace(/<|\u003C/g, "<")
.replace(/>|\u2265/g, ">")
.replace(/\u200E/g, "‎")
.replace(/\u200F/g, "‏")
.replace(/\u00A0/g, " ");
text += escapedTextContent;
} else if (isLineBreakElement(currentNode)) {
text += "\n";
} else if (
isSpanElement(currentNode) &&
currentNode.nodeType === Node.ELEMENT_NODE &&
currentNode.childNodes.length > 0
) {
const spaceAttribute = (currentNode as Element).getAttribute("xml:space");
const shouldTrimWhiteSpaceForSpan = isNonEmptyString(spaceAttribute)
? spaceAttribute === "default"
: shouldTrimWhiteSpaceFromParent;
text += loop(currentNode, shouldTrimWhiteSpaceForSpan);
}
}
return text;
}
return loop(paragraph, shouldTrimWhiteSpaceForParagraph);
}
/**
* Adds applicable style properties to a cue.
* /!\ Mutates cue argument.
* @param {VTTCue} cue
* @param {Object} style
*/
function addStyle(cue: ICompatVTTCue, style: IStyleList) {
const extent = style.extent;
if (isNonEmptyString(extent)) {
const results = REGXP_PERCENT_VALUES.exec(extent);
if (!isNullOrUndefined(results)) {
// Use width value of the extent attribute for size.
// Height value is ignored.
cue.size = Number(results[1]);
}
}
const writingMode = style.writingMode;
// let isVerticalText = true;
switch (writingMode) {
case "tb":
case "tblr":
cue.vertical = "lr";
break;
case "tbrl":
cue.vertical = "rl";
break;
default:
// isVerticalText = false;
break;
}
const origin = style.origin;
if (isNonEmptyString(origin)) {
const results = REGXP_PERCENT_VALUES.exec(origin);
if (!isNullOrUndefined(results)) {
// for vertical text use first coordinate of tts:origin
// to represent line of the cue and second - for position.
// Otherwise (horizontal), use them the other way around.
// if (isVerticalText) {
// TODO check and uncomment
// cue.position = Number(results[2]);
// cue.line = Number(results[1]);
// } else {
// TODO check and uncomment
// cue.position = Number(results[1]);
// cue.line = Number(results[2]);
// }
// A boolean indicating whether the line is an integer
// number of lines (using the line dimensions of the first
// line of the cue), or whether it is a percentage of the
// dimension of the video. The flag is set to true when lines
// are counted, and false otherwise.
// TODO check and uncomment
// cue.snapToLines = false;
}
}
const align = style.align;
if (isNonEmptyString(align)) {
cue.align = align;
if (align === "center") {
if (cue.align !== "center") {
// Workaround for a Chrome bug http://crbug.com/663797
// Chrome does not support align = "center"
cue.align = "middle";
}
cue.position = "auto";
}
const positionAlign = TEXT_ALIGN_TO_POSITION_ALIGN[align];
cue.positionAlign = positionAlign === undefined ? "" : positionAlign;
const lineAlign = TEXT_ALIGN_TO_LIGN_ALIGN[align];
cue.lineAlign = lineAlign === undefined ? "" : lineAlign;
}
}