function createMenuItem(_ref) {
	    var _this3 = this;

	    var value = _ref.value,
	        list = _ref.list,
	        type = _ref.type,
	        title = _ref.title,
	        _ref$badge = _ref.badge,
	        badge = _ref$badge === void 0 ? null : _ref$badge,
	        _ref$checked = _ref.checked,
	        checked = _ref$checked === void 0 ? false : _ref$checked;
	    var attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);
	    var menuItem = createElement('button', extend(attributes, {
	      type: 'button',
	      role: 'menuitemradio',
	      class: "".concat(this.config.classNames.control, " ").concat(attributes.class ? attributes.class : '').trim(),
	      'aria-checked': checked,
	      value: value
	    }));
	    var flex = createElement('span'); // We have to set as HTML incase of special characters

	    flex.innerHTML = title;

	    if (is$1.element(badge)) {
	      flex.appendChild(badge);
	    }

	    menuItem.appendChild(flex); // Replicate radio button behaviour

	    Object.defineProperty(menuItem, 'checked', {
	      enumerable: true,
	      get: function get() {
	        return menuItem.getAttribute('aria-checked') === 'true';
	      },
	      set: function set(checked) {
	        // Ensure exclusivity
	        if (checked) {
	          Array.from(menuItem.parentNode.children).filter(function (node) {
	            return matches$1(node, '[role="menuitemradio"]');
	          }).forEach(function (node) {
	            return node.setAttribute('aria-checked', 'false');
	          });
	        }

	        menuItem.setAttribute('aria-checked', checked ? 'true' : 'false');
	      }
	    });
	    this.listeners.bind(menuItem, 'click keyup', function (event) {
	      if (is$1.keyboardEvent(event) && event.which !== 32) {
	        return;
	      }

	      event.preventDefault();
	      event.stopPropagation();
	      menuItem.checked = true;

	      switch (type) {
	        case 'language':
	          _this3.currentTrack = Number(value);
	          break;

	        case 'quality':
	          _this3.quality = value;
	          break;

	        case 'speed':
	          _this3.speed = parseFloat(value);
	          break;

	        default:
	          break;
	      }

	      controls.showMenuPanel.call(_this3, 'home', is$1.keyboardEvent(event));
	    }, type, false);
	    controls.bindMenuItemShortcuts.call(this, menuItem, type);
	    list.appendChild(menuItem);
	  }


function formatTime$1() {
	    var time = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
	    var inverted = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;

	    // Bail if the value isn't a number
	    if (!is$1.number(time)) {
	      return time;
	    } // Always display hours if duration is over an hour

	    var forceHours = getHours(this.duration) > 0;
	    return formatTime(time, forceHours, inverted);
	  }


function updateTimeDisplay() {
	    var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
	    var time = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
	    var inverted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;

	    // Bail if there's no element to display or the value isn't a number
	    if (!is$1.element(target) || !is$1.number(time)) {
	      return;
	    } // eslint-disable-next-line no-param-reassign

	    target.innerText = controls.formatTime(time, inverted);
	  }


function updateVolume() {
	    if (!this.supported.ui) {
	      return;
	    } // Update range

	    if (is$1.element(this.elements.inputs.volume)) {
	      controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
	    } // Update mute state

	    if (is$1.element(this.elements.buttons.mute)) {
	      this.elements.buttons.mute.pressed = this.muted || this.volume === 0;
	    }
	  }

function
updateVolume
(
)
{
if
(
!
this
.
supported
.
ui
)
{
return
;
}
// Update range
if
(
is$1
.
element
(
this
.
elements
.
inputs
.
volume
)
)
{
controls
.
setRange
.
call
(
this
,
this
.
elements
.
inputs
.
volume
,
this
.
muted
0
:
this
.
volume
)
;
}
// Update mute state
if
(
is$1
.
element
(
this
.
elements
.
buttons
.
mute
)
)
{
this
.
elements
.
buttons
.
mute
.
pressed
=
this
.
muted
||
this
.
volume
===
0
;
}
}


function setRange(target) {
	    var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;

	    if (!is$1.element(target)) {
	      return;
	    } // eslint-disable-next-line

	    target.value = value; // Webkit range fill

	    controls.updateRangeFill.call(this, target);
	  }

function
setRange
(
target
)
{
var
value
=
arguments
.
length
>
1
&&
arguments
[
1
]
!==
undefined
arguments
[
1
]
:
0
;
if
(
!
is$1
.
element
(
target
)
)
{
return
;
}
// eslint-disable-next-line
target
.
value
=
value
;
// Webkit range fill
controls
.
updateRangeFill
.
call
(
this
,
target
)
;
}


function updateRangeFill(target) {
	    // Get range from event if event passed
	    var range = is$1.event(target) ? target.target : target; // Needs to be a valid <input type='range'>

	    if (!is$1.element(range) || range.getAttribute('type') !== 'range') {
	      return;
	    } // Set aria values for https://github.com/sampotts/plyr/issues/905

	    if (matches$1(range, this.config.selectors.inputs.seek)) {
	      range.setAttribute('aria-valuenow', this.currentTime);
	      var currentTime = controls.formatTime(this.currentTime);
	      var duration = controls.formatTime(this.duration);
	      var format = i18n.get('seekLabel', this.config);
	      range.setAttribute('aria-valuetext', format.replace('{currentTime}', currentTime).replace('{duration}', duration));
	    } else if (matches$1(range, this.config.selectors.inputs.volume)) {
	      var percent = range.value * 100;
	      range.setAttribute('aria-valuenow', percent);
	      range.setAttribute('aria-valuetext', "".concat(percent.toFixed(1), "%"));
	    } else {
	      range.setAttribute('aria-valuenow', range.value);
	    } // WebKit only

	    if (!browser.isWebkit) {
	      return;
	    } // Set CSS custom property

	    range.style.setProperty('--value', "".concat(range.value / range.max * 100, "%"));
	  }


function updateSeekTooltip(event) {
	    var _this5 = this;

	    // Bail if setting not true
	    if (!this.config.tooltips.seek || !is$1.element(this.elements.inputs.seek) || !is$1.element(this.elements.display.seekTooltip) || this.duration === 0) {
	      return;
	    } // Calculate percentage

	    var percent = 0;
	    var clientRect = this.elements.progress.getBoundingClientRect();
	    var visible = "".concat(this.config.classNames.tooltip, "--visible");

	    var toggle = function toggle(_toggle) {
	      toggleClass(_this5.elements.display.seekTooltip, visible, _toggle);
	    }; // Hide on touch

	    if (this.touch) {
	      toggle(false);
	      return;
	    } // Determine percentage, if already visible

	    if (is$1.event(event)) {
	      percent = 100 / clientRect.width * (event.pageX - clientRect.left);
	    } else if (hasClass(this.elements.display.seekTooltip, visible)) {
	      percent = parseFloat(this.elements.display.seekTooltip.style.left, 10);
	    } else {
	      return;
	    } // Set bounds

	    if (percent < 0) {
	      percent = 0;
	    } else if (percent > 100) {
	      percent = 100;
	    } // Display the time a click would seek to

	    controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent); // Set position

	    this.elements.display.seekTooltip.style.left = "".concat(percent, "%"); // Show/hide the tooltip
	    // If the event is a moues in/out and percentage is inside bounds

	    if (is$1.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {
	      toggle(event.type === 'mouseenter');
	    }
	  }


function timeUpdate(event) {
	    // Only invert if only one time element is displayed and used for both duration and currentTime
	    var invert = !is$1.element(this.elements.display.duration) && this.config.invertTime; // Duration

	    controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert); // Ignore updates while seeking

	    if (event && event.type === 'timeupdate' && this.media.seeking) {
	      return;
	    } // Playing progress

	    controls.updateProgress.call(this, event);
	  }


function durationUpdate() {
	    // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false
	    if (!this.supported.ui || !this.config.invertTime && this.currentTime) {
	      return;
	    } // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.
	    // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415
	    // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062
	    // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338

	    if (this.duration >= Math.pow(2, 32)) {
	      toggleHidden(this.elements.display.currentTime, true);
	      toggleHidden(this.elements.progress, true);
	      return;
	    } // Update ARIA values

	    if (is$1.element(this.elements.inputs.seek)) {
	      this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);
	    } // If there's a spot to display duration

	    var hasDuration = is$1.element(this.elements.display.duration); // If there's only one time display, display duration there

	    if (!hasDuration && this.config.displayDuration && this.paused) {
	      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
	    } // If there's a duration element, update content

	    if (hasDuration) {
	      controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
	    } // Update the tooltip (if visible)

	    controls.updateSeekTooltip.call(this);
	  }

Show the duration on metadataloaded or durationchange events


function updateSetting(setting, container, input) {
	    var pane = this.elements.settings.panels[setting];
	    var value = null;
	    var list = container;

	    if (setting === 'captions') {
	      value = this.currentTrack;
	    } else {
	      value = !is$1.empty(input) ? input : this[setting]; // Get default

	      if (is$1.empty(value)) {
	        value = this.config[setting].default;
	      } // Unsupported value

	      if (!is$1.empty(this.options[setting]) && !this.options[setting].includes(value)) {
	        this.debug.warn("Unsupported value of '".concat(value, "' for ").concat(setting));
	        return;
	      } // Disabled value

	      if (!this.config[setting].options.includes(value)) {
	        this.debug.warn("Disabled value of '".concat(value, "' for ").concat(setting));
	        return;
	      }
	    } // Get the list if we need to

	    if (!is$1.element(list)) {
	      list = pane && pane.querySelector('[role="menu"]');
	    } // If there's no list it means it's not been rendered...

	    if (!is$1.element(list)) {
	      return;
	    } // Update the label

	    var label = this.elements.settings.buttons[setting].querySelector(".".concat(this.config.classNames.menu.value));
	    label.innerHTML = controls.getLabel.call(this, setting, value); // Find the radio option and check it

	    var target = list && list.querySelector("[value=\"".concat(value, "\"]"));

	    if (is$1.element(target)) {
	      target.checked = true;
	    }
	  }


function getLabel(setting, value) {
	    switch (setting) {
	      case 'speed':
	        return value === 1 ? i18n.get('normal', this.config) : "".concat(value, "×");

	      case 'quality':
	        if (is$1.number(value)) {
	          var label = i18n.get("qualityLabel.".concat(value), this.config);

	          if (!label.length) {
	            return "".concat(value, "p");
	          }

	          return label;
	        }

	        return toTitleCase(value);

	      case 'captions':
	        return captions.getLabel.call(this);

	      default:
	        return null;
	    }
	  }

function getLabel ( setting , value ) { switch ( setting ) { case 'speed' : return value === 1 ? i18n . get ( 'normal' , this . config ) : "" . concat ( value , "×" ) ; case 'quality' : if ( is$1 . number ( value ) ) { var label = i18n . get ( "qualityLabel." . concat ( value ) , this . config ) ; if ( ! label . length ) { return "" . concat ( value , "p" ) ; } return label ; } return toTitleCase ( value ) ; case 'captions' : return captions . getLabel . call ( this ) ; default : return null ; } }


function setQualityMenu(options) {
	    var _this6 = this;

	    // Menu required
	    if (!is$1.element(this.elements.settings.panels.quality)) {
	      return;
	    }

	    var type = 'quality';
	    var list = this.elements.settings.panels.quality.querySelector('[role="menu"]'); // Set options if passed and filter based on uniqueness and config

	    if (is$1.array(options)) {
	      this.options.quality = dedupe(options).filter(function (quality) {
	        return _this6.config.quality.options.includes(quality);
	      });
	    } // Toggle the pane and tab

	    var toggle = !is$1.empty(this.options.quality) && this.options.quality.length > 1;
	    controls.toggleMenuButton.call(this, type, toggle); // Empty the menu

	    emptyElement(list); // Check if we need to toggle the parent

	    controls.checkMenu.call(this); // If we're hiding, nothing more to do

	    if (!toggle) {
	      return;
	    } // Get the badge HTML for HD, 4K etc

	    var getBadge = function getBadge(quality) {
	      var label = i18n.get("qualityBadge.".concat(quality), _this6.config);

	      if (!label.length) {
	        return null;
	      }

	      return controls.createBadge.call(_this6, label);
	    }; // Sort options by the config and then render options

	    this.options.quality.sort(function (a, b) {
	      var sorting = _this6.config.quality.options;
	      return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
	    }).forEach(function (quality) {
	      controls.createMenuItem.call(_this6, {
	        value: quality,
	        list: list,
	        type: type,
	        title: controls.getLabel.call(_this6, 'quality', quality),
	        badge: getBadge(quality)
	      });
	    });
	    controls.updateSetting.call(this, type, list);
	  }


function getBadge(quality) {
	      var label = i18n.get("qualityBadge.".concat(quality), _this6.config);

	      if (!label.length) {
	        return null;
	      }

	      return controls.createBadge.call(_this6, label);
	    }

function getBadge ( quality ) { var label = i18n . get ( "qualityBadge." . concat ( quality ) , _this6 . config ) ; if ( ! label . length ) { return null ; } return controls . createBadge . call ( _this6 , label ) ; }


function setSpeedMenu(options) {
	    var _this8 = this;

	    // Menu required
	    if (!is$1.element(this.elements.settings.panels.speed)) {
	      return;
	    }

	    var type = 'speed';
	    var list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); // Set the speed options

	    if (is$1.array(options)) {
	      this.options.speed = options;
	    } else if (this.isHTML5 || this.isVimeo) {
	      this.options.speed = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
	    } // Set options if passed and filter based on config

	    this.options.speed = this.options.speed.filter(function (speed) {
	      return _this8.config.speed.options.includes(speed);
	    }); // Toggle the pane and tab

	    var toggle = !is$1.empty(this.options.speed) && this.options.speed.length > 1;
	    controls.toggleMenuButton.call(this, type, toggle); // Empty the menu

	    emptyElement(list); // Check if we need to toggle the parent

	    controls.checkMenu.call(this); // If we're hiding, nothing more to do

	    if (!toggle) {
	      return;
	    } // Create items

	    this.options.speed.forEach(function (speed) {
	      controls.createMenuItem.call(_this8, {
	        value: speed,
	        list: list,
	        type: type,
	        title: controls.getLabel.call(_this8, 'speed', speed)
	      });
	    });
	    controls.updateSetting.call(this, type, list);
	  }

Set a list of available captions languages


function getMenuSize(tab) {
	    var clone = tab.cloneNode(true);
	    clone.style.position = 'absolute';
	    clone.style.opacity = 0;
	    clone.removeAttribute('hidden'); // Append to parent so we get the "real" size

	    tab.parentNode.appendChild(clone); // Get the sizes before we remove

	    var width = clone.scrollWidth;
	    var height = clone.scrollHeight; // Remove from the DOM

	    removeElement(clone);
	    return {
	      width: width,
	      height: height
	    };
	  }


function showMenuPanel() {
	    var _this9 = this;

	    var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
	    var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
	    var target = this.elements.container.querySelector("#plyr-settings-".concat(this.id, "-").concat(type)); // Nothing to show, bail

	    if (!is$1.element(target)) {
	      return;
	    } // Hide all other panels

	    var container = target.parentNode;
	    var current = Array.from(container.children).find(function (node) {
	      return !node.hidden;
	    }); // If we can do fancy animations, we'll animate the height/width

	    if (support.transitions && !support.reducedMotion) {
	      // Set the current width as a base
	      container.style.width = "".concat(current.scrollWidth, "px");
	      container.style.height = "".concat(current.scrollHeight, "px"); // Get potential sizes

	      var size = controls.getMenuSize.call(this, target); // Restore auto height/width

	      var restore = function restore(event) {
	        // We're only bothered about height and width on the container
	        if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {
	          return;
	        } // Revert back to auto

	        container.style.width = '';
	        container.style.height = ''; // Only listen once

	        off.call(_this9, container, transitionEndEvent, restore);
	      }; // Listen for the transition finishing and restore auto height/width

	      on.call(this, container, transitionEndEvent, restore); // Set dimensions to target

	      container.style.width = "".concat(size.width, "px");
	      container.style.height = "".concat(size.height, "px");
	    } // Set attributes on current tab

	    toggleHidden(current, true); // Set attributes on target

	    toggleHidden(target, false); // Focus the first item

	    controls.focusFirstMenuItem.call(this, target, tabFocus);
	  }


function setDownloadUrl() {
	    var button = this.elements.buttons.download; // Bail if no button

	    if (!is$1.element(button)) {
	      return;
	    } // Set attribute

	    button.setAttribute('href', this.download);
	  }

function
setDownloadUrl
(
)
{
var
button
=
this
.
elements
.
buttons
.
download
;
// Bail if no button
if
(
!
is$1
.
element
(
button
)
)
{
return
;
}
// Set attribute
button
.
setAttribute
(
'href'
,
this
.
download
)
;
}


function replace(input) {
	      var result = input;
	      Object.entries(props).forEach(function (_ref2) {
	        var _ref3 = _slicedToArray(_ref2, 2),
	            key = _ref3[0],
	            value = _ref3[1];

	        result = replaceAll(result, "{".concat(key, "}"), value);
	      });
	      return result;
	    }

function replace ( input ) { var result = input ; Object . entries ( props ) . forEach ( function ( _ref2 ) { var _ref3 = _slicedToArray ( _ref2 , 2 ) , key = _ref3 [ 0 ] , value = _ref3 [ 1 ] ; result = replaceAll ( result , "{" . concat ( key , "}" ) , value ) ; } ) ; return result ; }


function parseUrl$2(input) {
	  var safe = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
	  var url = input;

	  if (safe) {
	    var parser = document.createElement('a');
	    parser.href = url;
	    url = parser.href;
	  }

	  try {
	    return new URL(url);
	  } catch (e) {
	    return null;
	  }
	}

function parseUrl$2 ( input ) { var safe = arguments . length > 1 && arguments [ 1 ] !== undefined ? arguments [ 1 ] : true ; var url = input ; if ( safe ) { var parser = document . createElement ( 'a' ) ; parser . href = url ; url = parser . href ; } try { return new URL ( url ) ; } catch ( e ) { return null ; } }

Parse a string to a URL object
@param {String} input - the URL to be parsed
@param {Boolean} safe - failsafe parsing


function buildUrlParams(input) {
	  var params = new URLSearchParams();

	  if (is$1.object(input)) {
	    Object.entries(input).forEach(function (_ref) {
	      var _ref2 = _slicedToArray(_ref, 2),
	          key = _ref2[0],
	          value = _ref2[1];

	      params.set(key, value);
	    });
	  }

	  return params;
	}

function buildUrlParams ( input ) { var params = new URLSearchParams ( ) ; if ( is$1 . object ( input ) ) { Object . entries ( input ) . forEach ( function ( _ref ) { var _ref2 = _slicedToArray ( _ref , 2 ) , key = _ref2 [ 0 ] , value = _ref2 [ 1 ] ; params . set ( key , value ) ; } ) ; } return params ; }


function update() {
	    var _this = this;

	    var tracks = captions.getTracks.call(this, true); // Get the wanted language

	    var _this$captions = this.captions,
	        active = _this$captions.active,
	        language = _this$captions.language,
	        meta = _this$captions.meta,
	        currentTrackNode = _this$captions.currentTrackNode;
	    var languageExists = Boolean(tracks.find(function (track) {
	      return track.language === language;
	    })); // Handle tracks (add event listener and "pseudo"-default)

	    if (this.isHTML5 && this.isVideo) {
	      tracks.filter(function (track) {
	        return !meta.get(track);
	      }).forEach(function (track) {
	        _this.debug.log('Track added', track); // Attempt to store if the original dom element was "default"

	        meta.set(track, {
	          default: track.mode === 'showing'
	        }); // Turn off native caption rendering to avoid double captions

	        track.mode = 'hidden'; // Add event listener for cue changes

	        on.call(_this, track, 'cuechange', function () {
	          return captions.updateCues.call(_this);
	        });
	      });
	    } // Update language first time it matches, or if the previous matching track was removed

	    if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {
	      captions.setLanguage.call(this, language);
	      captions.toggle.call(this, active && languageExists);
	    } // Enable or disable captions based on track length

	    toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is$1.empty(tracks)); // Update available languages in list

	    if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
	      controls.setCaptionsMenu.call(this);
	    }
	  }

Update available language options in settings based on tracks


function toggle(input) {
	    var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;

	    // If there's no full support
	    if (!this.supported.ui) {
	      return;
	    }

	    var toggled = this.captions.toggled; // Current state

	    var activeClass = this.config.classNames.captions.active; // Get the next state
	    // If the method is called without parameter, toggle based on current value

	    var active = is$1.nullOrUndefined(input) ? !toggled : input; // Update state and trigger event

	    if (active !== toggled) {
	      // When passive, don't override user preferences
	      if (!passive) {
	        this.captions.active = active;
	        this.storage.set({
	          captions: active
	        });
	      } // Force language if the call isn't passive and there is no matching language to toggle to

	      if (!this.language && active && !passive) {
	        var tracks = captions.getTracks.call(this);
	        var track = captions.findTrack.call(this, [this.captions.language].concat(_toConsumableArray(this.captions.languages)), true); // Override user preferences to avoid switching languages if a matching track is added

	        this.captions.language = track.language; // Set caption, but don't store in localStorage as user preference

	        captions.set.call(this, tracks.indexOf(track));
	        return;
	      } // Toggle button if it's enabled

	      if (this.elements.buttons.captions) {
	        this.elements.buttons.captions.pressed = active;
	      } // Add class hook

	      toggleClass(this.elements.container, activeClass, active);
	      this.captions.toggled = active; // Update settings menu

	      controls.updateSetting.call(this, 'captions'); // Trigger event (not used internally)

	      triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
	    }
	  }

Toggle captions display Used internally for the toggleCaptions method, with the passive option forced to false

Toggle captions display Used internally for the toggleCaptions method with the passive option forced to false


function set(index) {
	    var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
	    var tracks = captions.getTracks.call(this); // Disable captions if setting to -1

	    if (index === -1) {
	      captions.toggle.call(this, false, passive);
	      return;
	    }

	    if (!is$1.number(index)) {
	      this.debug.warn('Invalid caption argument', index);
	      return;
	    }

	    if (!(index in tracks)) {
	      this.debug.warn('Track not found', index);
	      return;
	    }

	    if (this.captions.currentTrack !== index) {
	      this.captions.currentTrack = index;
	      var track = tracks[index];

	      var _ref = track || {},
	          language = _ref.language; // Store reference to node for invalidation on remove

	      this.captions.currentTrackNode = track; // Update settings menu

	      controls.updateSetting.call(this, 'captions'); // When passive, don't override user preferences

	      if (!passive) {
	        this.captions.language = language;
	        this.storage.set({
	          language: language
	        });
	      } // Handle Vimeo captions

	      if (this.isVimeo) {
	        this.embed.enableTextTrack(language);
	      } // Trigger event

	      triggerEvent.call(this, this.media, 'languagechange');
	    } // Show captions

	    captions.toggle.call(this, true, passive);

	    if (this.isHTML5 && this.isVideo) {
	      // If we change the active track while a cue is already displayed we need to update it
	      captions.updateCues.call(this);
	    }
	  }

Set captions by track index Used internally for the currentTrack setter with the passive option forced to false


function setLanguage(input) {
	    var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;

	    if (!is$1.string(input)) {
	      this.debug.warn('Invalid language argument', input);
	      return;
	    } // Normalize

	    var language = input.toLowerCase();
	    this.captions.language = language; // Set currentTrack

	    var tracks = captions.getTracks.call(this);
	    var track = captions.findTrack.call(this, [language]);
	    captions.set.call(this, tracks.indexOf(track), passive);
	  }

function
setLanguage
(
input
)
{
var
passive
=
arguments
.
length
>
1
&&
arguments
[
1
]
!==
undefined
arguments
[
1
]
:
true
;
if
(
!
is$1
.
string
(
input
)
)
{
this
.
debug
.
warn
(
'Invalid language argument'
,
input
)
;
return
;
}
// Normalize
var
language
=
input
.
toLowerCase
(
)
;
this
.
captions
.
language
=
language
;
// Set currentTrack
var
tracks
=
captions
.
getTracks
.
call
(
this
)
;
var
track
=
captions
.
findTrack
.
call
(
this
,
[
language
]
)
;
captions
.
set
.
call
(
this
,
tracks
.
indexOf
(
track
)
,
passive
)
;
}

Set captions by language Used internally for the language setter with the passive option forced to false


function getTracks() {
	    var _this2 = this;

	    var update = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
	    // Handle media or textTracks missing or null
	    var tracks = Array.from((this.media || {}).textTracks || []); // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
	    // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)

	    return tracks.filter(function (track) {
	      return !_this2.isHTML5 || update || _this2.captions.meta.has(track);
	    }).filter(function (track) {
	      return ['captions', 'subtitles'].includes(track.kind);
	    });
	  }

Get current valid caption tracks If update is false it will also ignore tracks without metadata This is used to "freeze" the language options when captions.update is false

Get current valid caption tracks If update is false it will also ignore tracks without metadata This is used to freeze the language options when captions . update is false


function findTrack(languages) {
	    var _this3 = this;

	    var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
	    var tracks = captions.getTracks.call(this);

	    var sortIsDefault = function sortIsDefault(track) {
	      return Number((_this3.captions.meta.get(track) || {}).default);
	    };

	    var sorted = Array.from(tracks).sort(function (a, b) {
	      return sortIsDefault(b) - sortIsDefault(a);
	    });
	    var track;
	    languages.every(function (language) {
	      track = sorted.find(function (track) {
	        return track.language === language;
	      });
	      return !track; // Break iteration if there is a match
	    }); // If no match is found but is required, get first

	    return track || (force ? sorted[0] : undefined);
	  }

Match tracks based on languages and get the first


function getLabel(track) {
	    var currentTrack = track;

	    if (!is$1.track(currentTrack) && support.textTracks && this.captions.toggled) {
	      currentTrack = captions.getCurrentTrack.call(this);
	    }

	    if (is$1.track(currentTrack)) {
	      if (!is$1.empty(currentTrack.label)) {
	        return currentTrack.label;
	      }

	      if (!is$1.empty(currentTrack.language)) {
	        return track.language.toUpperCase();
	      }

	      return i18n.get('enabled', this.config);
	    }

	    return i18n.get('disabled', this.config);
	  }

function getLabel ( track ) { var currentTrack = track ; if ( ! is$1 . track ( currentTrack ) && support . textTracks && this . captions . toggled ) { currentTrack = captions . getCurrentTrack . call ( this ) ; } if ( is$1 . track ( currentTrack ) ) { if ( ! is$1 . empty ( currentTrack . label ) ) { return currentTrack . label ; } if ( ! is$1 . empty ( currentTrack . language ) ) { return track . language . toUpperCase ( ) ; } return i18n . get ( 'enabled' , this . config ) ; } return i18n . get ( 'disabled' , this . config ) ; }


function getProviderByUrl(url) {
	  // YouTube
	  if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) {
	    return providers.youtube;
	  } // Vimeo

	  if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
	    return providers.vimeo;
	  }

	  return null;
	}


function get() {
	      return (Fullscreen.native || this.player.config.fullscreen.fallback) && this.player.config.fullscreen.enabled && this.player.supported.ui && this.player.isVideo;
	    }

function get ( ) { return ( Fullscreen . native || this . player . config . fullscreen . fallback ) && this . player . config . fullscreen . enabled && this . player . supported . ui && this . player . isVideo ; }


function toggleNativeControls() {
	    var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;

	    if (toggle && this.isHTML5) {
	      this.media.setAttribute('controls', '');
	    } else {
	      this.media.removeAttribute('controls');
	    }
	  }

function toggleNativeControls ( ) { var toggle = arguments . length > 0 && arguments [ 0 ] !== undefined ? arguments [ 0 ] : false ; if ( toggle && this . isHTML5 ) { this . media . setAttribute ( 'controls' , '' ) ; } else { this . media . removeAttribute ( 'controls' ) ; } }


function build() {
	    var _this = this;

	    // Re-attach media element listeners
	    // TODO: Use event bubbling?
	    this.listeners.media(); // Don't setup interface if no support

	    if (!this.supported.ui) {
	      this.debug.warn("Basic support only for ".concat(this.provider, " ").concat(this.type)); // Restore native controls

	      ui.toggleNativeControls.call(this, true); // Bail

	      return;
	    } // Inject custom controls if not present

	    if (!is$1.element(this.elements.controls)) {
	      // Inject custom controls
	      controls.inject.call(this); // Re-attach control listeners

	      this.listeners.controls();
	    } // Remove native controls

	    ui.toggleNativeControls.call(this); // Setup captions for HTML5

	    if (this.isHTML5) {
	      captions.setup.call(this);
	    } // Reset volume

	    this.volume = null; // Reset mute state

	    this.muted = null; // Reset loop state

	    this.loop = null; // Reset quality setting

	    this.quality = null; // Reset speed

	    this.speed = null; // Reset volume display

	    controls.updateVolume.call(this); // Reset time display

	    controls.timeUpdate.call(this); // Update the UI

	    ui.checkPlaying.call(this); // Check for picture-in-picture support

	    toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo); // Check for airplay support

	    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); // Add iOS class

	    toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); // Add touch class

	    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); // Ready for API calls

	    this.ready = true; // Ready event at end of execution stack

	    setTimeout(function () {
	      triggerEvent.call(_this, _this.media, 'ready');
	    }, 0); // Set the title

	    ui.setTitle.call(this); // Assure the poster image is set, if the property was added before the element was created

	    if (this.poster) {
	      ui.setPoster.call(this, this.poster, false).catch(function () {});
	    } // Manually set the duration if user has overridden it.
	    // The event listeners for it doesn't get called if preload is disabled (#701)

	    if (this.config.duration) {
	      controls.durationUpdate.call(this);
	    }
	  }


function setTitle() {
	    // Find the current text
	    var label = i18n.get('play', this.config); // If there's a media title set, use that for the label

	    if (is$1.string(this.config.title) && !is$1.empty(this.config.title)) {
	      label += ", ".concat(this.config.title);
	    } // If there's a play button, set label

	    Array.from(this.elements.buttons.play || []).forEach(function (button) {
	      button.setAttribute('aria-label', label);
	    }); // Set iframe title
	    // https://github.com/sampotts/plyr/issues/124

	    if (this.isEmbed) {
	      var iframe = getElement.call(this, 'iframe');

	      if (!is$1.element(iframe)) {
	        return;
	      } // Default to media type

	      var title = !is$1.empty(this.config.title) ? this.config.title : 'video';
	      var format = i18n.get('frameTitle', this.config);
	      iframe.setAttribute('title', format.replace('{title}', title));
	    }
	  }

Setup aria attribute for play and iframe title


function checkPlaying(event) {
	    var _this3 = this;

	    // Class hooks
	    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
	    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
	    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); // Set state

	    Array.from(this.elements.buttons.play || []).forEach(function (target) {
	      target.pressed = _this3.playing;
	    }); // Only update controls on non timeupdate events

	    if (is$1.event(event) && event.type === 'timeupdate') {
	      return;
	    } // Toggle controls

	    ui.toggleControls.call(this);
	  }


function checkLoading(event) {
	    var _this4 = this;

	    this.loading = ['stalled', 'waiting'].includes(event.type); // Clear timer

	    clearTimeout(this.timers.loading); // Timer to prevent flicker when seeking

	    this.timers.loading = setTimeout(function () {
	      // Update progress bar loading class state
	      toggleClass(_this4.elements.container, _this4.config.classNames.loading, _this4.loading); // Update controls visibility

	      ui.toggleControls.call(_this4);
	    }, this.loading ? 250 : 0);
	  }


function toggleControls(force) {
	    var controls = this.elements.controls;

	    if (controls && this.config.hideControls) {
	      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
	      var recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now(); // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide

	      this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover || recentTouchSeek));
	    }
	  }

Toggle controls based on state and `force` argument

Toggle controls based on state and force argument


function removeCurrent() {
	        var className = player.config.classNames.tabFocus;
	        var current = getElements.call(player, ".".concat(className));
	        toggleClass(current, className, false);
	      }

function removeCurrent ( ) { var className = player . config . classNames . tabFocus ; var current = getElements . call ( player , "." . concat ( className ) ) ; toggleClass ( current , className , false ) ; }


function setPlayerSize(measure) {
	        // If we don't need to measure the viewport
	        if (!measure) {
	          return setAspectRatio.call(player);
	        }

	        var rect = elements.container.getBoundingClientRect();
	        var width = rect.width,
	            height = rect.height;
	        return setAspectRatio.call(player, "".concat(width, ":").concat(height));
	      }


function subscribe(bundleIds, callbackFn) {
	      // listify
	      bundleIds = bundleIds.push ? bundleIds : [bundleIds];
	      var depsNotFound = [],
	          i = bundleIds.length,
	          numWaiting = i,
	          fn,
	          bundleId,
	          r,
	          q; // define callback function

	      fn = function fn(bundleId, pathsNotFound) {
	        if (pathsNotFound.length) depsNotFound.push(bundleId);
	        numWaiting--;
	        if (!numWaiting) callbackFn(depsNotFound);
	      }; // register callback

	      while (i--) {
	        bundleId = bundleIds[i]; // execute callback if in result cache

	        r = bundleResultCache[bundleId];

	        if (r) {
	          fn(bundleId, r);
	          continue;
	        } // add to callback queue

	        q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];
	        q.push(fn);
	      }
	    }

function
subscribe
(
bundleIds
,
callbackFn
)
{
// listify
bundleIds
=
bundleIds
.
push
bundleIds
:
[
bundleIds
]
;
var
depsNotFound
=
[
]
,
i
=
bundleIds
.
length
,
numWaiting
=
i
,
fn
,
bundleId
,
r
,
q
;
// define callback function
fn
=
function
fn
(
bundleId
,
pathsNotFound
)
{
if
(
pathsNotFound
.
length
)
depsNotFound
.
push
(
bundleId
)
;
numWaiting
;
if
(
!
numWaiting
)
callbackFn
(
depsNotFound
)
;
}
;
// register callback
while
(
i
)
{
bundleId
=
bundleIds
[
i
]
;
// execute callback if in result cache
r
=
bundleResultCache
[
bundleId
]
;
if
(
r
)
{
fn
(
bundleId
,
r
)
;
continue
;
}
// add to callback queue
q
=
bundleCallbackQueue
[
bundleId
]
=
bundleCallbackQueue
[
bundleId
]
||
[
]
;
q
.
push
(
fn
)
;
}
}

Subscribe to bundle load event.
@param {string[]} bundleIds - Bundle ids
@param {Function} callbackFn - The callback function


function publish(bundleId, pathsNotFound) {
	      // exit if id isn't defined
	      if (!bundleId) return;
	      var q = bundleCallbackQueue[bundleId]; // cache result

	      bundleResultCache[bundleId] = pathsNotFound; // exit if queue is empty

	      if (!q) return; // empty callback queue

	      while (q.length) {
	        q[0](bundleId, pathsNotFound);
	        q.splice(0, 1);
	      }
	    }

function
publish
(
bundleId
,
pathsNotFound
)
{
// exit if id isn't defined
if
(
!
bundleId
)
return
;
var
q
=
bundleCallbackQueue
[
bundleId
]
;
// cache result
bundleResultCache
[
bundleId
]
=
pathsNotFound
;
// exit if queue is empty
if
(
!
q
)
return
;
// empty callback queue
while
(
q
.
length
)
{
q
[
0
]
(
bundleId
,
pathsNotFound
)
;
q
.
splice
(
0
,
1
)
;
}
}

Publish bundle load event.
@param {string} bundleId - Bundle id
@param {string[]} pathsNotFound - List of files not found


function executeCallbacks(args, depsNotFound) {
	      // accept function as argument
	      if (args.call) args = {
	        success: args
	      }; // success and error callbacks

	      if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args);
	    }

function
executeCallbacks
(
args
,
depsNotFound
)
{
// accept function as argument
if
(
args
.
call
)
args
=
{
success
:
args
}
;
// success and error callbacks
if
(
depsNotFound
.
length
)
(
args
.
error
||
devnull
)
(
depsNotFound
)
;
else
(
args
.
success
||
devnull
)
(
args
)
;
}

Execute callbacks.
@param {Object or Function} args - The callback args
@param {string[]} depsNotFound - List of dependencies not found


function loadFile(path, callbackFn, args, numTries) {
	      var doc = document,
	          async = args.async,
	          maxTries = (args.numRetries || 0) + 1,
	          beforeCallbackFn = args.before || devnull,
	          pathStripped = path.replace(/^(css|img)!/, ''),
	          isLegacyIECss,
	          e;
	      numTries = numTries || 0;

	      if (/(^css!|\.css$)/.test(path)) {
	        // css
	        e = doc.createElement('link');
	        e.rel = 'stylesheet';
	        e.href = pathStripped; // tag IE9+

	        isLegacyIECss = 'hideFocus' in e; // use preload in IE Edge (to detect load errors)

	        if (isLegacyIECss && e.relList) {
	          isLegacyIECss = 0;
	          e.rel = 'preload';
	          e.as = 'style';
	        }
	      } else if (/(^img!|\.(png|gif|jpg|svg)$)/.test(path)) {
	        // image
	        e = doc.createElement('img');
	        e.src = pathStripped;
	      } else {
	        // javascript
	        e = doc.createElement('script');
	        e.src = path;
	        e.async = async === undefined ? true : async;
	      }

	      e.onload = e.onerror = e.onbeforeload = function (ev) {
	        var result = ev.type[0]; // treat empty stylesheets as failures to get around lack of onerror
	        // support in IE9-11

	        if (isLegacyIECss) {
	          try {
	            if (!e.sheet.cssText.length) result = 'e';
	          } catch (x) {
	            // sheets objects created from load errors don't allow access to
	            // `cssText` (unless error is Code:18 SecurityError)
	            if (x.code != 18) result = 'e';
	          }
	        } // handle retries in case of load failure

	        if (result == 'e') {
	          // increment counter
	          numTries += 1; // exit function and try again

	          if (numTries < maxTries) {
	            return loadFile(path, callbackFn, args, numTries);
	          }
	        } else if (e.rel == 'preload' && e.as == 'style') {
	          // activate preloaded stylesheets
	          return e.rel = 'stylesheet'; // jshint ignore:line
	        } // execute callback

	        callbackFn(path, result, ev.defaultPrevented);
	      }; // add to document (unless callback returns `false`)

	      if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);
	    }

Load individual file.
@param {string} path - The file path
@param {Function} callbackFn - The callback function


function loadjs(paths, arg1, arg2) {
	      var bundleId, args; // bundleId (if string)

	      if (arg1 && arg1.trim) bundleId = arg1; // args (default is {})

	      args = (bundleId ? arg2 : arg1) || {}; // throw error if bundle is already defined

	      if (bundleId) {
	        if (bundleId in bundleIdCache) {
	          throw "LoadJS";
	        } else {
	          bundleIdCache[bundleId] = true;
	        }
	      }

	      function loadFn(resolve, reject) {
	        loadFiles(paths, function (pathsNotFound) {
	          // execute callbacks
	          executeCallbacks(args, pathsNotFound); // resolve Promise

	          if (resolve) {
	            executeCallbacks({
	              success: resolve,
	              error: reject
	            }, pathsNotFound);
	          } // publish bundle load event

	          publish(bundleId, pathsNotFound);
	        }, args);
	      }

	      if (args.returnPromise) return new Promise(loadFn);else loadFn();
	    }

Initiate script load and register bundle.
@param {(string|string[])} paths - The file paths
@param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success
callback or (3) object literal with success/error arguments, numRetries,
etc.
@param {(Function|Object)} [arg2] - The (1) success callback or (2) object
literal with success/error arguments, numRetries, etc.

Initiate script load and register bundle .


function getTitle(videoId) {
	    var _this2 = this;

	    var url = format(this.config.urls.youtube.api, videoId);
	    fetch(url).then(function (data) {
	      if (is$1.object(data)) {
	        var title = data.title,
	            height = data.height,
	            width = data.width; // Set title

	        _this2.config.title = title;
	        ui.setTitle.call(_this2); // Set aspect ratio

	        _this2.embed.ratio = [width, height];
	      }

	      setAspectRatio.call(_this2);
	    }).catch(function () {
	      // Set aspect ratio
	      setAspectRatio.call(_this2);
	    });
	  }

function
getTitle
(
videoId
)
{
var
_this2
=
this
;
var
url
=
format
(
this
.
config
.
urls
.
youtube
.
api
,
videoId
)
;
fetch
(
url
)
.
then
(
function
(
data
)
{
if
(
is$1
.
object
(
data
)
)
{
var
title
=
data
.
title
,
height
=
data
.
height
,
width
=
data
.
width
;
// Set title
_this2
.
config
.
title
=
title
;
ui
.
setTitle
.
call
(
_this2
)
;
// Set aspect ratio
_this2
.
embed
.
ratio
=
[
width
,
height
]
;
}
setAspectRatio
.
call
(
_this2
)
;
}
)
.
catch
(
function
(
)
{
// Set aspect ratio
setAspectRatio
.
call
(
_this2
)
;
}
)
;
}


function Ads(player) {
	    var _this = this;

	    _classCallCheck(this, Ads);

	    this.player = player;
	    this.config = player.config.ads;
	    this.playing = false;
	    this.initialized = false;
	    this.elements = {
	      container: null,
	      displayContainer: null
	    };
	    this.manager = null;
	    this.loader = null;
	    this.cuePoints = null;
	    this.events = {};
	    this.safetyTimer = null;
	    this.countdownTimer = null; // Setup a promise to resolve when the IMA manager is ready

	    this.managerPromise = new Promise(function (resolve, reject) {
	      // The ad is loaded and ready
	      _this.on('loaded', resolve); // Ads failed

	      _this.on('error', reject);
	    });
	    this.load();
	  }

Ads constructor.
@param {Object} player
@return {Ads}


function load() {
	      var _this2 = this;

	      if (!this.enabled) {
	        return;
	      } // Check if the Google IMA3 SDK is loaded or load it ourselves

	      if (!is$1.object(window.google) || !is$1.object(window.google.ima)) {
	        loadScript(this.player.config.urls.googleIMA.sdk).then(function () {
	          _this2.ready();
	        }).catch(function () {
	          // Script failed to load or is blocked
	          _this2.trigger('error', new Error('Google IMA SDK failed to load'));
	        });
	      } else {
	        this.ready();
	      }
	    }


function setupIMA() {
	      // Create the container for our advertisements
	      this.elements.container = createElement('div', {
	        class: this.player.config.classNames.ads
	      });
	      this.player.elements.container.appendChild(this.elements.container); // So we can run VPAID2

	      google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); // Set language

	      google.ima.settings.setLocale(this.player.config.ads.language); // Set playback for iOS10+

	      google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline); // We assume the adContainer is the video container of the plyr element that will house the ads

	      this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media); // Request video ads to be pre-loaded

	      this.requestAds();
	    }

In order for the SDK to display ads for our video, we need to tell it where to put them,
so here we define our ad container. This div is set up to render on top of the video player.
Using the code below, we tell the SDK to render ads within that div. We also provide a
handle to the content video player - the SDK will poll the current time of our player to
properly place mid-rolls. After we create the ad display container, we initialize it. On
mobile devices, this initialization is done as the result of a user action.

In order for the SDK to display ads for our video we need to tell it where to put them so here we define our ad container . This div is set up to render on top of the video player . Using the code below we tell the SDK to render ads within that div . We also provide a handle to the content video player - the SDK will poll the current time of our player to properly place mid - rolls . After we create the ad display container we initialize it . On mobile devices this initialization is done as the result of a user action .


function PreviewThumbnails(player) {
	    _classCallCheck(this, PreviewThumbnails);

	    this.player = player;
	    this.thumbnails = [];
	    this.loaded = false;
	    this.lastMouseMoveTime = Date.now();
	    this.mouseDown = false;
	    this.loadedImages = [];
	    this.elements = {
	      thumb: {},
	      scrubbing: {}
	    };
	    this.load();
	  }

function PreviewThumbnails ( player ) { _classCallCheck ( this , PreviewThumbnails ) ; this . player = player ; this . thumbnails = [ ] ; this . loaded = false ; this . lastMouseMoveTime = Date . now ( ) ; this . mouseDown = false ; this . loadedImages = [ ] ; this . elements = { thumb : { } , scrubbing : { } } ; this . load ( ) ; }

PreviewThumbnails constructor.
@param {Plyr} player
@return {PreviewThumbnails}


function clamp() {
	  var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
	  var min = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
	  var max = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 255;
	  return Math.min(Math.max(input, min), max);
	}

function clamp ( ) { var input = arguments . length > 0 && arguments [ 0 ] !== undefined ? arguments [ 0 ] : 0 ; var min = arguments . length > 1 && arguments [ 1 ] !== undefined ? arguments [ 1 ] : 0 ; var max = arguments . length > 2 && arguments [ 2 ] !== undefined ? arguments [ 2 ] : 255 ; return Math . min ( Math . max ( input , min ) , max ) ; }

Returns a number whose value is limited to the given range.

Example: limit the output of this computation to between 0 and 255
(x * 255).clamp(0, 255)

@param {Number} input
@param {Number} min The lower boundary of the output range
@param {Number} max The upper boundary of the output range
@returns A number in the range [min, max]
@type Number

Returns a number whose value is limited to the given range .


function togglePlay(input) {
	      // Toggle based on current state if nothing passed
	      var toggle = is$1.boolean(input) ? input : !this.playing;

	      if (toggle) {
	        this.play();
	      } else {
	        this.pause();
	      }
	    }

Toggle playback based on current status
@param {Boolean} input


function newSource(type, init) {
	        // Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
	        if (!(type in types) || !init && type === currentType || !currentType.length && type === types.video) {
	          return;
	        }

	        switch (type) {
	          case types.video:
	            player.source = {
	              type: 'video',
	              title: 'View From A Blue Moon',
	              sources: [{
	                src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
	                type: 'video/mp4',
	                size: 576
	              }, {
	                src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
	                type: 'video/mp4',
	                size: 720
	              }, {
	                src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
	                type: 'video/mp4',
	                size: 1080
	              }, {
	                src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
	                type: 'video/mp4',
	                size: 1440
	              }],
	              poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
	              tracks: [{
	                kind: 'captions',
	                label: 'English',
	                srclang: 'en',
	                src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
	                default: true
	              }, {
	                kind: 'captions',
	                label: 'French',
	                srclang: 'fr',
	                src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt'
	              }]
	            };
	            break;

	          case types.audio:
	            player.source = {
	              type: 'audio',
	              title: 'Kishi Bashi – “It All Began With A Burst”',
	              sources: [{
	                src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
	                type: 'audio/mp3'
	              }, {
	                src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
	                type: 'audio/ogg'
	              }]
	            };
	            break;

	          case types.youtube:
	            player.source = {
	              type: 'video',
	              sources: [{
	                src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
	                provider: 'youtube'
	              }]
	            };
	            break;

	          case types.vimeo:
	            player.source = {
	              type: 'video',
	              sources: [{
	                src: 'https://vimeo.com/76979871',
	                provider: 'vimeo'
	              }]
	            };
	            break;

	          default:
	            break;
	        } // Set the current type for next time

	        currentType = type; // Remove active classes

	        Array.from(buttons).forEach(function (button) {
	          return toggleClass(button.parentElement, 'active', false);
	        }); // Set active on parent

	        toggleClass(document.querySelector("[data-source=\"".concat(type, "\"]")), 'active', true); // Show cite

	        Array.from(document.querySelectorAll('.plyr__cite')).forEach(function (cite) {
	          cite.setAttribute('hidden', '');
	        });
	        document.querySelector(".plyr__cite--".concat(type)).removeAttribute('hidden');
	      }


function (str) {
  const args = arguments
  let flag = true
  let i = 1

  str = str.replace(/%s/g, () => {
    const arg = args[i++]

    if (typeof arg === 'undefined') {
      flag = false
      return ''
    }
    return arg
  })
  return flag ? str : ''
}

function ( str ) { const args = arguments let flag = true let i = 1 str = str . replace ( / %s / g , ( ) => { const arg = args [ i ++ ] if ( typeof arg === 'undefined' ) { flag = false return '' } return arg } ) return flag ? str : '' }

it only does '%s', and return '' when arguments are undefined

it only does %s and return when arguments are undefined


function preprocessCartForServer( {
	coupon,
	is_coupon_applied,
	is_coupon_removed,
	currency,
	temporary,
	extra,
	products,
	tax,
} ) {
	const needsUrlCoupon = ! (
		coupon ||
		is_coupon_applied ||
		is_coupon_removed ||
		typeof document === 'undefined'
	);
	const urlCoupon = needsUrlCoupon ? url.parse( document.URL, true ).query.coupon : '';

	return Object.assign(
		{
			coupon,
			is_coupon_applied,
			is_coupon_removed,
			currency,
			tax,
			temporary,
			extra,
			products: products.map(
				( { product_id, meta, free_trial, volume, extra: productExtra } ) => ( {
					product_id,
					meta,
					free_trial,
					volume,
					extra: productExtra,
				} )
			),
		},
		needsUrlCoupon &&
			urlCoupon && {
				coupon: urlCoupon,
				is_coupon_applied: false,
			}
	);
}

function preprocessCartForServer ( { coupon , is_coupon_applied , is_coupon_removed , currency , temporary , extra , products , tax , } ) { const needsUrlCoupon = ! ( coupon || is_coupon_applied || is_coupon_removed || typeof document === 'undefined' ) ; const urlCoupon = needsUrlCoupon ? url . parse ( document . URL , true ) . query . coupon : '' ; return Object . assign ( { coupon , is_coupon_applied , is_coupon_removed , currency , tax , temporary , extra , products : products . map ( ( { product_id , meta , free_trial , volume , extra : productExtra } ) => ( { product_id , meta , free_trial , volume , extra : productExtra , } ) ) , } , needsUrlCoupon && urlCoupon && { coupon : urlCoupon , is_coupon_applied : false , } ) ; }

Preprocesses cart for server.

@param {Object} cart Cart object.
@returns {Object} A new cart object.


function getNewMessages( previousCartValue, nextCartValue ) {
	previousCartValue = previousCartValue || {};
	nextCartValue = nextCartValue || {};
	const nextCartMessages = nextCartValue.messages || [];

	// If there is no previous cart then just return the messages for the new cart
	if (
		! previousCartValue ||
		! previousCartValue.client_metadata ||
		! nextCartValue.client_metadata
	) {
		return nextCartMessages;
	}

	const previousDate = previousCartValue.client_metadata.last_server_response_date;
	const nextDate = nextCartValue.client_metadata.last_server_response_date;
	const hasNewServerData = i18n.moment( nextDate ).isAfter( previousDate );

	return hasNewServerData ? nextCartMessages : [];
}

Compare two different cart objects and get the messages of newest one

It's possible that we're comparing two carts that have the same server header date.
This means the changes only happened locally and the messages returned will be [].

@param {cartValue} [previousCartValue] - the previously loaded cart
@param {cartValue} [nextCartValue] - the new cart value
@returns {array} [nextCartMessages] - an array of messages about the state of the cart


function isRedirectToValidForSsr( redirectToQueryValue ) {
	if ( 'undefined' === typeof redirectToQueryValue ) {
		return true;
	}

	const redirectToDecoded = decodeURIComponent( redirectToQueryValue );
	return (
		redirectToDecoded.startsWith( 'https://wordpress.com/theme' ) ||
		redirectToDecoded.startsWith( 'https://wordpress.com/go' )
	);
}

function isRedirectToValidForSsr ( redirectToQueryValue ) { if ( 'undefined' === typeof redirectToQueryValue ) { return true ; } const redirectToDecoded = decodeURIComponent ( redirectToQueryValue ) ; return ( redirectToDecoded . startsWith ( 'https://wordpress.com/theme' ) || redirectToDecoded . startsWith ( 'https://wordpress.com/go' ) ) ; }

Verifies if the given redirect_to value enables SSR or not.

@param {string}   redirectToQueryValue The URI-encoded value of the analyzed redirect_to
@return {boolean} If the value of &redirect_to= on the log-in page is compatible with SSR

Verifies if the given redirect_to value enables SSR or not .


function highlight( term, html, wrapperNode ) {
	debug( 'Starting highlight' );
	if ( ! wrapperNode ) {
		wrapperNode = document.createElement( 'mark' );
	}
	if ( ! term || ! html ) {
		return html;
	}
	const root = document.createElement( 'div' );
	root.innerHTML = html;
	walk( root, term, wrapperNode );
	return root.innerHTML;
}

function
highlight
(
term
,
html
,
wrapperNode
)
{
debug
(
'Starting highlight'
)
;
if
(
!
wrapperNode
)
{
wrapperNode
=
document
.
createElement
(
'mark'
)
;
}
if
(
!
term
||
!
html
)
{
return
html
;
}
const
root
=
document
.
createElement
(
'div'
)
;
root
.
innerHTML
=
html
;
walk
(
root
,
term
,
wrapperNode
)
;
return
root
.
innerHTML
;
}

Wraps strings in a html to highlight
@param {string} term Term to search for
@param {string} html HTML string to search and wrap
@param {HTMLElement} [wrapperNode] Custom node to wrap the elements with, defaults to <mark>
@returns {string} Wrapped HTML


function MailingList( category, wpcom ) {
	if ( ! ( this instanceof MailingList ) ) {
		return new MailingList( category, wpcom );
	}

	this._category = category;
	this.wpcom = wpcom;
}

function MailingList ( category , wpcom ) { if ( ! ( this instanceof MailingList ) ) { return new MailingList ( category , wpcom ) ; } this . _category = category ; this . wpcom = wpcom ; }

`MailingList` constructor.

@constructor
@param {WPCOM} wpcom
@public


function embed( editor ) {
	let embedDialogContainer;

	/**
	 * Open or close the EmbedDialog
	 *
	 * @param {boolean} visible `true` makes the dialog visible; `false` hides it.
	 */
	const render = ( visible = true ) => {
		const selectedEmbedNode = editor.selection.getNode();
		const store = editor.getParam( 'redux_store' );
		const embedDialogProps = {
			embedUrl: selectedEmbedNode.innerText || selectedEmbedNode.textContent,
			isVisible: visible,
			onCancel: () => render( false ),
			onUpdate: newUrl => {
				editor.execCommand( 'mceInsertContent', false, newUrl );
				render( false );
			},
		};

		renderWithReduxStore(
			React.createElement( EmbedDialog, embedDialogProps ),
			embedDialogContainer,
			store
		);

		// Focus on the editor when closing the dialog, so that the user can start typing right away
		// instead of having to tab back to the editor.
		if ( ! visible ) {
			editor.focus();
		}
	};

	editor.addCommand( 'embedDialog', () => render() );

	editor.on( 'init', () => {
		embedDialogContainer = editor.getContainer().appendChild( document.createElement( 'div' ) );
	} );

	editor.on( 'remove', () => {
		ReactDom.unmountComponentAtNode( embedDialogContainer );
		embedDialogContainer.parentNode.removeChild( embedDialogContainer );
		embedDialogContainer = null;
	} );
}

Manages an EmbedDialog to allow editing the URL of an embed inside the editor.

@param {object} editor An instance of TinyMCE

Manages an EmbedDialog to allow editing the URL of an embed inside the editor .


function onexit( code ) {
		let changedFiles;

		cssMake.stderr.removeListener( 'data', onstderr );
		cssMake.stdout.removeListener( 'data', onstdout );
		cssMake = null;

		if ( scheduleBuild ) {
			// Handle new css build request
			scheduleBuild = false;
			spawnMake();
		} else if ( 0 === code ) {
			// 'make build-css' success
			changedFiles = updateChangedCssFiles();
			if ( 0 !== changedFiles.length ) {
				debug( chalk.green( 'css reload' ) );
				io.of( '/css-hot-reload' ).emit( 'css-hot-reload', {
					status: 'reload',
					changedFiles: changedFiles,
				} );
			} else {
				debug( chalk.green( 'css up to date' ) );
				io.of( '/css-hot-reload' ).emit( 'css-hot-reload', { status: 'up-to-date' } );
			}
		} else {
			// 'make build-css' failed
			debug( chalk.red( 'css build failed' ) );
			io.of( '/css-hot-reload' ).emit( 'css-hot-reload', {
				status: 'build-failed',
				error: errors,
			} );
		}
	}

function
onexit
(
code
)
{
let
changedFiles
;
cssMake
.
stderr
.
removeListener
(
'data'
,
onstderr
)
;
cssMake
.
stdout
.
removeListener
(
'data'
,
onstdout
)
;
cssMake
=
;
if
(
scheduleBuild
)
{
// Handle new css build request
scheduleBuild
=
false
;
spawnMake
(
)
;
}
else
if
(
0
===
code
)
{
// 'make build-css' success
changedFiles
=
updateChangedCssFiles
(
)
;
if
(
0
!==
changedFiles
.
length
)
{
debug
(
chalk
.
green
(
'css reload'
)
)
;
io
.
of
(
'/css-hot-reload'
)
.
emit
(
'css-hot-reload'
,
{
status
:
'reload'
,
changedFiles
:
changedFiles
,
}
)
;
}
else
{
debug
(
chalk
.
green
(
'css up to date'
)
)
;
io
.
of
(
'/css-hot-reload'
)
.
emit
(
'css-hot-reload'
,
{
status
:
'up-to-date'
}
)
;
}
}
else
{
// 'make build-css' failed
debug
(
chalk
.
red
(
'css build failed'
)
)
;
io
.
of
(
'/css-hot-reload'
)
.
emit
(
'css-hot-reload'
,
{
status
:
'build-failed'
,
error
:
errors
,
}
)
;
}
}

Handle 'make build-css' success or failure
@param {Number} code The exit code

Handle make build - css success or failure


function updateChangedCssFiles() {
		let hash, filePath;
		const changedFiles = [];
		for ( filePath in publicCssFiles ) {
			hash = md5File.sync( filePath );
			if ( hash !== publicCssFiles[ filePath ] ) {
				publicCssFiles[ filePath ] = hash;
				changedFiles.push( path.basename( filePath ) );
			}
		}

		return changedFiles;
	}

function updateChangedCssFiles ( ) { let hash , filePath ; const changedFiles = [ ] ; for ( filePath in publicCssFiles ) { hash = md5File . sync ( filePath ) ; if ( hash !== publicCssFiles [ filePath ] ) { publicCssFiles [ filePath ] = hash ; changedFiles . push ( path . basename ( filePath ) ) ; } } return changedFiles ; }

Updates the MD5 hash of the public CSS files and returns the changed ones
in a list.
@returns {Array} an array of changed files (string)

Updates the MD5 hash of the public CSS files and returns the changed ones in a list .


function updateSiteState( state, siteId, attributes ) {
	return Object.assign( {}, state, {
		[ siteId ]: Object.assign( {}, initialSiteState, state[ siteId ], attributes ),
	} );
}

function updateSiteState ( state , siteId , attributes ) { return Object . assign ( { } , state , { [ siteId ] : Object . assign ( { } , initialSiteState , state [ siteId ] , attributes ) , } ) ; }

Returns a new state with the given attributes updated for the specified site.

@param {Object} state current state
@param {Number} siteId identifier of the site
@param {Object} attributes list of attributes and their values
@returns {Object} the new state

Returns a new state with the given attributes updated for the specified site .


function isValidCategoriesArray( categories ) {
	for ( let i = 0; i < categories.length; i++ ) {
		if ( ! isValidProductCategory( categories[ i ] ) ) {
			// Short-circuit the loop and return now.
			return false;
		}
	}
	return true;
}

Check that all categories in a given list are valid category objects.

@param  {Array} categories List of categories from the remote site API
@return {Boolean}          True if the categories are valid.

Check that all categories in a given list are valid category objects .


function isValidProductCategory( category ) {
	return (
		category &&
		category.id &&
		'number' === typeof category.id &&
		category.name &&
		'string' === typeof category.name &&
		category.slug &&
		'string' === typeof category.slug
	);
}

function isValidProductCategory ( category ) { return ( category && category . id && 'number' === typeof category . id && category . name && 'string' === typeof category . name && category . slug && 'string' === typeof category . slug ) ; }

Check if the given category has the expected properties

@param  {Object} category A single category from the remote site API
@return {Boolean}         True if the category is valid.


function hasLanguageChanged( languageSettingValue, settings = {} ) {
	if ( ! languageSettingValue ) {
		return false;
	}
	// if there is a saved variant we know that the user is changing back to the root language === setting hasn't changed
	// but if settings.locale_variant is not empty then we assume the user is trying to switch back to the root
	return (
		( languageSettingValue === settings.language && isEmpty( settings.locale_variant ) ) ||
		//if the incoming language code is the variant itself === setting hasn't changed
		languageSettingValue === settings.locale_variant
	);
}

Checks if an incoming change to settings.language is a change to the existing settings
Currently the assumption is that if a settings.locale_variant slug exists, then that is the current language

@param  {String}  languageSettingValue the newly-set language slug string.
@param  {Object}  settings user settings object.
@return {Boolean} if the language setting has been changed.

Checks if an incoming change to settings . language is a change to the existing settings Currently the assumption is that if a settings . locale_variant slug exists then that is the current language


function UserSettings() {
	if ( ! ( this instanceof UserSettings ) ) {
		return new UserSettings();
	}

	this.settings = false;
	this.initialized = false;
	this.reAuthRequired = false;
	this.fetchingSettings = false;
	this.unsavedSettings = {};
}

function UserSettings ( ) { if ( ! ( this instanceof UserSettings ) ) { return new UserSettings ( ) ; } this . settings = false ; this . initialized = false ; this . reAuthRequired = false ; this . fetchingSettings = false ; this . unsavedSettings = { } ; }

Initialize UserSettings with defaults

@return {Undefined} undefined


function getDomainNameFromReceiptOrCart( receipt, cart ) {
	let domainRegistration;

	if ( receipt && ! isEmpty( receipt.purchases ) ) {
		domainRegistration = find( values( receipt.purchases ), isDomainRegistration );
	}

	if ( cartItems.hasDomainRegistration( cart ) ) {
		domainRegistration = cartItems.getDomainRegistrations( cart )[ 0 ];
	}

	if ( domainRegistration ) {
		return domainRegistration.meta;
	}

	return null;
}

function getDomainNameFromReceiptOrCart ( receipt , cart ) { let domainRegistration ; if ( receipt && ! isEmpty ( receipt . purchases ) ) { domainRegistration = find ( values ( receipt . purchases ) , isDomainRegistration ) ; } if ( cartItems . hasDomainRegistration ( cart ) ) { domainRegistration = cartItems . getDomainRegistrations ( cart ) [ 0 ] ; } if ( domainRegistration ) { return domainRegistration . meta ; } return null ; }

Depending on the current step in checkout, the user's domain can be found in
either the cart or the receipt.

@param {?Object} receipt - The receipt for the transaction
@param {?Object} cart - The cart for the transaction

@return {?String} the name of the first domain for the transaction.

Depending on the current step in checkout the user s domain can be found in either the cart or the receipt .


function bustHashForHrefs( { name, oldValue } ) {
	// http://some.site.com/and/a/path?with=a&query -> http://some.site.com/and/a/path?v=13508135781
	const value =
		'href' === name ? `${ oldValue.split( '?' ).shift() }?v=${ new Date().getTime() }` : oldValue;

	return { name, value };
}

Replaces the old hash parameter with the current timestamp if `name` is `href`.
@return {Object}

Replaces the old hash parameter with the current timestamp if name is href .


function updateCachedProduct( products, product ) {
	let found = false;
	const updatedProduct = { ...product, name: decodeEntities( product.name ) };
	const newProducts = products.map( p => {
		if ( p.id === product.id ) {
			found = true;
			return updatedProduct;
		}
		return p;
	} );

	if ( ! found ) {
		newProducts.push( updatedProduct );
	}

	return newProducts;
}

function updateCachedProduct ( products , product ) { let found = false ; const updatedProduct = { ... product , name : decodeEntities ( product . name ) } ; const newProducts = products . map ( p => { if ( p . id === product . id ) { found = true ; return updatedProduct ; } return p ; } ) ; if ( ! found ) { newProducts . push ( updatedProduct ) ; } return newProducts ; }

Merge a product into the products list

@param  {Array}  products A list of products
@param  {Object} product  A single product to update or add to the products list
@return {Array}         Updated product list


function setLoading( state, params, newStatus ) {
	const queries = ( state.queries && { ...state.queries } ) || {};
	const key = getSerializedProductsQuery( params );
	queries[ key ] = { ...( queries[ key ] || {} ), isLoading: newStatus };
	return queries;
}

function setLoading ( state , params , newStatus ) { const queries = ( state . queries && { ... state . queries } ) || { } ; const key = getSerializedProductsQuery ( params ) ; queries [ key ] = { ... ( queries [ key ] || { } ) , isLoading : newStatus } ; return queries ; }

Set the loading status for a param set in state

@param  {Object}  state     The current state
@param  {Object}  params    Params of the query to update
@param  {Boolean} newStatus The new value to save
@return {Object}            Updated isLoading state


function generateStaticUrls( target ) {
	const urls = { ...staticFilesUrls };
	const assets = getAssets( target ).assetsByChunkName;

	forEach( assets, ( asset, name ) => {
		urls[ name ] = asset;
	} );

	return urls;
}

function generateStaticUrls ( target ) { const urls = { ... staticFilesUrls } ; const assets = getAssets ( target ) . assetsByChunkName ; forEach ( assets , ( asset , name ) => { urls [ name ] = asset ; } ) ; return urls ; }

Generate an object that maps asset names name to a server-relative urls.
Assets in request and static files are included.

@param {String} target The build target name.

@returns {Object} Map of asset names to urls

Generate an object that maps asset names name to a server - relative urls . Assets in request and static files are included .


function getAcceptedLanguagesFromHeader( header ) {
	if ( ! header ) {
		return [];
	}

	return header
		.split( ',' )
		.map( lang => {
			const match = lang.match( /^[A-Z]{2,3}(-[A-Z]{2,3})?/i );
			if ( ! match ) {
				return false;
			}

			return match[ 0 ].toLowerCase();
		} )
		.filter( lang => lang );
}

function getAcceptedLanguagesFromHeader ( header ) { if ( ! header ) { return [ ] ; } return header . split ( ',' ) . map ( lang => { const match = lang . match ( / ^[A-Z]{2,3}(-[A-Z]{2,3})? / i ) ; if ( ! match ) { return false ; } return match [ 0 ] . toLowerCase ( ) ; } ) . filter ( lang => lang ) ; }

Given the content of an 'Accept-Language' request header, returns an array of the languages.

This differs slightly from other language functions, as it doesn't try to validate the language codes,
or merge similar language codes.

@param  {string} header - The content of the AcceptedLanguages header.
@return {Array} An array of language codes in the header, all in lowercase.

Given the content of an Accept - Language request header returns an array of the languages .


function setUpCSP( req, res, next ) {
	const originalUrlPathname = req.originalUrl.split( '?' )[ 0 ];

	// We only setup CSP for /log-in* for now
	if ( ! /^\/log-in/.test( originalUrlPathname ) ) {
		next();
		return;
	}

	// This is calculated by taking the contents of the script text from between the tags,
	// and calculating SHA256 hash on it, encoded in base64, example:
	// `sha256-${ base64( sha256( 'window.AppBoot();' ) ) }` === sha256-3yiQswl88knA3EhjrG5tj5gmV6EUdLYFvn2dygc0xUQ
	// you can also just run it in Chrome, chrome will give you the hash of the violating scripts
	const inlineScripts = [
		'sha256-3yiQswl88knA3EhjrG5tj5gmV6EUdLYFvn2dygc0xUQ=',
		'sha256-ZKTuGaoyrLu2lwYpcyzib+xE4/2mCN8PKv31uXS3Eg4=',
	];

	req.context.inlineScriptNonce = crypto.randomBytes( 48 ).toString( 'hex' );

	const policy = {
		'default-src': [ "'self'" ],
		'script-src': [
			"'self'",
			"'report-sample'",
			"'unsafe-eval'",
			'stats.wp.com',
			'https://widgets.wp.com',
			'*.wordpress.com',
			'https://apis.google.com',
			`'nonce-${ req.context.inlineScriptNonce }'`,
			'www.google-analytics.com',
			...inlineScripts.map( hash => `'${ hash }'` ),
		],
		'base-uri': [ "'none'" ],
		'style-src': [ "'self'", '*.wp.com', 'https://fonts.googleapis.com' ],
		'form-action': [ "'self'" ],
		'object-src': [ "'none'" ],
		'img-src': [
			"'self'",
			'data',
			'*.wp.com',
			'*.files.wordpress.com',
			'*.gravatar.com',
			'https://www.google-analytics.com',
			'https://amplifypixel.outbrain.com',
			'https://img.youtube.com',
		],
		'frame-src': [ "'self'", 'https://public-api.wordpress.com', 'https://accounts.google.com/' ],
		'font-src': [
			"'self'",
			'*.wp.com',
			'https://fonts.gstatic.com',
			'data:', // should remove 'data:' ASAP
		],
		'media-src': [ "'self'" ],
		'connect-src': [ "'self'", 'https://*.wordpress.com/', 'https://*.wp.com' ],
		'report-uri': [ '/cspreport' ],
	};

	const policyString = Object.keys( policy )
		.map( key => `${ key } ${ policy[ key ].join( ' ' ) }` )
		.join( '; ' );

	// For now we're just logging policy violations and not blocking them
	// so we won't actually break anything, later we'll remove the 'Report-Only'
	// part so browsers will block violating content.
	res.set( { 'Content-Security-Policy-Report-Only': policyString } );
	next();
}

Sets up a Content Security Policy header
@see https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
@param {Object} req Express request object
@param {Object} res Express response object
@param {Function} next a callback to call when done


function( post ) {
		let latitude, longitude;

		if ( ! post ) {
			return;
		}

		latitude = parseFloat( getValueByKey( post.metadata, 'geo_latitude' ) );
		longitude = parseFloat( getValueByKey( post.metadata, 'geo_longitude' ) );

		if ( latitude && longitude ) {
			return [ latitude, longitude ];
		}
	}

function ( post ) { let latitude , longitude ; if ( ! post ) { return ; } latitude = parseFloat ( getValueByKey ( post . metadata , 'geo_latitude' ) ) ; longitude = parseFloat ( getValueByKey ( post . metadata , 'geo_longitude' ) ) ; if ( latitude && longitude ) { return [ latitude , longitude ] ; } }

Given a post object, returns an array of float coordinates representing
the geographic location saved for that post, or `undefined` if the value
cannot be determined.

@param  {Object} post Post object
@return {string}      Array of geographic float coordinates

Given a post object returns an array of float coordinates representing the geographic location saved for that post or undefined if the value cannot be determined .


function( post ) {
		if ( ! post ) {
			return null;
		}

		const isSharedPublicly = getValueByKey( post.metadata, 'geo_public' );

		if ( parseInt( isSharedPublicly, 10 ) ) {
			return true;
		}

		if ( undefined === isSharedPublicly ) {
			// If they have no geo_public value but they do have a lat/long, then we assume they saved with Calypso
			// before it supported geo_public, in which case we should treat it as private.
			if (
				getValueByKey( post.metadata, 'geo_latitude' ) ||
				getValueByKey( post.metadata, 'geo_longitude' )
			) {
				return false;
			}

			return true;
		}

		return false;
	}

Given a post object, return a boolean, indicating whether the geo-location data
associated with the post is allowed to be displayed publicly.

@param {Object} post Post object
@returns {boolean|null} Whether the geo-location data is shared publicly.

Given a post object return a boolean indicating whether the geo - location data associated with the post is allowed to be displayed publicly .


function sanitizeExtra( data ) {
	const path = data._contactDetailsCache ? [ '_contactDetailsCache', 'extra' ] : 'extra';
	return data && isArray( get( data, path ) ) ? omit( data, path ) : data;
}

function sanitizeExtra ( data ) { const path = data . _contactDetailsCache ? [ '_contactDetailsCache' , 'extra' ] : 'extra' ; return data && isArray ( get ( data , path ) ) ? omit ( data , path ) : data ; }

Drop data.extra if it's an array

Assigning extra as an array (due to a bug in the server) leads to a
a really weird state that effectively disables the extra form for the user
permanently.

These values will be persisted locally, so we need to handle them here
even if we catch them all on the backend.

In case you're curios, Here's the weirdness:

weird = Object.assign( [1,2,3], { foo:'bar' } )
// [1, 2, 3, foo: "bar"] (wat?)
weird.map( v => v );
// [1, 2, 3] (no foo for you!)

@param  {Object} data   Potential contact details
@return {Object}        Sanitized contact details


function transmitDraftId( calypsoPort ) {
	// Bail if we are not writing a new post.
	if ( ! /wp-admin\/post-new.php/.test( location.href ) ) {
		return;
	}

	const unsubscribe = subscribe( () => {
		const currentPost = select( 'core/editor' ).getCurrentPost();

		if ( currentPost && currentPost.id && currentPost.status !== 'auto-draft' ) {
			calypsoPort.postMessage( {
				action: 'draftIdSet',
				payload: { postId: currentPost.id },
			} );

			unsubscribe();
		}
	} );
}

Monitors Gutenberg store for draft ID assignment and transmits it to parent frame when needed.

@param {MessagePort} calypsoPort Port used for communication with parent frame.

Monitors Gutenberg store for draft ID assignment and transmits it to parent frame when needed .


function handlePostLocked( calypsoPort ) {
	const unsubscribe = subscribe( () => {
		const isLocked = select( 'core/editor' ).isPostLocked();
		const isLockTakeover = select( 'core/editor' ).isPostLockTakeover();
		const lockedDialogButtons = document.querySelectorAll(
			'div.editor-post-locked-modal__buttons > a'
		);

		const isPostTakeoverDialog = isLocked && ! isLockTakeover && lockedDialogButtons.length === 3;

		if ( isPostTakeoverDialog ) {
			//signal the parent frame to navigate to All Posts
			lockedDialogButtons[ 0 ].addEventListener(
				'click',
				event => {
					event.preventDefault();
					calypsoPort.postMessage( { action: 'goToAllPosts' } );
				},
				false
			);

			//overrides the all posts link just in case the user treats the link... as a link.
			if ( calypsoifyGutenberg && calypsoifyGutenberg.closeUrl ) {
				lockedDialogButtons[ 0 ].setAttribute( 'target', '_parent' );
				lockedDialogButtons[ 0 ].setAttribute( 'href', calypsoifyGutenberg.closeUrl );
			}

			//changes the Take Over link url to add the frame-nonce
			lockedDialogButtons[ 2 ].setAttribute(
				'href',
				addQueryArgs( lockedDialogButtons[ 2 ].getAttribute( 'href' ), {
					calypsoify: 1,
					'frame-nonce': getQueryArg( window.location.href, 'frame-nonce' ),
				} )
			);

			unsubscribe();
		}
	} );
}

Listens for post lock status changing to locked, and overrides the modal dialog
actions, addind an event handler for the All Posts button, and changing the
Take Over Url to work inside the iframe.

@param {MessagePort} calypsoPort Port used for communication with parent frame.

Listens for post lock status changing to locked and overrides the modal dialog actions addind an event handler for the All Posts button and changing the Take Over Url to work inside the iframe .


function handlePostLockTakeover( calypsoPort ) {
	const unsubscribe = subscribe( () => {
		const isLocked = select( 'core/editor' ).isPostLocked();
		const isLockTakeover = select( 'core/editor' ).isPostLockTakeover();
		const allPostsButton = document.querySelector( 'div.editor-post-locked-modal__buttons > a' );

		const isPostTakeoverDialog = isLocked && isLockTakeover && allPostsButton;

		if ( isPostTakeoverDialog ) {
			//handle All Posts button click event
			allPostsButton.addEventListener(
				'click',
				event => {
					event.preventDefault();
					calypsoPort.postMessage( { action: 'goToAllPosts' } );
				},
				false
			);

			//overrides the all posts link just in case the user treats the link... as a link.
			if ( calypsoifyGutenberg && calypsoifyGutenberg.closeUrl ) {
				allPostsButton.setAttribute( 'target', '_parent' );
				allPostsButton.setAttribute( 'href', calypsoifyGutenberg.closeUrl );
			}

			unsubscribe();
		}
	} );
}

Listens for post lock status changing to locked, and for the post to have been taken over
by another user, adding an event handler for the All Posts button.

@param {MessagePort} calypsoPort Port used for communication with parent frame.

Listens for post lock status changing to locked and for the post to have been taken over by another user adding an event handler for the All Posts button .


function updateImageBlocks( blocks, image ) {
		forEach( blocks, block => {
			if ( imageBlocks[ block.name ] ) {
				imageBlocks[ block.name ]( block, image );
			}
			if ( block.innerBlocks.length ) {
				updateImageBlocks( block.innerBlocks, image );
			}
		} );
	}

function updateImageBlocks ( blocks , image ) { forEach ( blocks , block => { if ( imageBlocks [ block . name ] ) { imageBlocks [ block . name ] ( block , image ) ; } if ( block . innerBlocks . length ) { updateImageBlocks ( block . innerBlocks , image ) ; } } ) ; }

Updates all the blocks containing a given edited image.

@param {Array} blocks Array of block objects for the current post.
@param {Object} image The edited image.
@param {number} image.id The image ID.
@param {string} image.url The new image URL.
@param {string} image.status The new image status. "deleted" or "updated" (default).
@param {number} [image.transientId] The temporary ID assigned to uploading image.

Updates all the blocks containing a given edited image .


function updateFeaturedImagePreview( image ) {
		const currentImageId = select( 'core/editor' ).getEditedPostAttribute( 'featured_media' );
		if ( currentImageId !== image.id ) {
			return;
		}
		preloadImage( image.url ).then( () => {
			const currentImage = select( 'core' ).getMedia( currentImageId );
			const updatedImage = {
				...currentImage,
				media_details: {
					height: image.height,
					width: image.width,
				},
				source_url: image.url,
			};
			dispatch( 'core' ).receiveEntityRecords( 'root', 'media', [ updatedImage ], null, true );
		} );
	}

function updateFeaturedImagePreview ( image ) { const currentImageId = select ( 'core/editor' ) . getEditedPostAttribute ( 'featured_media' ) ; if ( currentImageId !== image . id ) { return ; } preloadImage ( image . url ) . then ( ( ) => { const currentImage = select ( 'core' ) . getMedia ( currentImageId ) ; const updatedImage = { ... currentImage , media_details : { height : image . height , width : image . width , } , source_url : image . url , } ; dispatch ( 'core' ) . receiveEntityRecords ( 'root' , 'media' , [ updatedImage ] , null , true ) ; } ) ; }

To update the featured image preview we need to manually modify the image in the `core` state. Though, there is no direct mechanism to also invalidate its cache and re-render the preview. As a workaround, we call `receiveEntityRecords`, normally used to update the state after a fetch, which has a handy `invalidateCache` parameter that does exactly what we need.

To update the featured image preview we need to manually modify the image in the core state . Though there is no direct mechanism to also invalidate its cache and re - render the preview . As a workaround we call receiveEntityRecords normally used to update the state after a fetch which has a handy invalidateCache parameter that does exactly what we need .


function handlePreview( calypsoPort ) {
	$( '#editor' ).on( 'click', '.editor-post-preview', e => {
		e.preventDefault();
		e.stopPropagation();

		const postUrl = select( 'core/editor' ).getCurrentPostAttribute( 'link' );
		const previewChannel = new MessageChannel();

		calypsoPort.postMessage(
			{
				action: 'previewPost',
				payload: {
					postUrl: postUrl,
				},
			},
			[ previewChannel.port2 ]
		);

		const isAutosaveable = select( 'core/editor' ).isEditedPostAutosaveable();

		// If we don't need to autosave the post before previewing, then we simply
		// generate the preview.
		if ( ! isAutosaveable ) {
			sendPreviewData();
			return;
		}

		// Request an autosave before generating the preview.
		const postStatus = select( 'core/editor' ).getEditedPostAttribute( 'status' );
		const isDraft = [ 'draft', 'auto-draft' ].indexOf( postStatus ) !== -1;
		if ( isDraft ) {
			dispatch( 'core/editor' ).savePost( { isPreview: true } );
		} else {
			dispatch( 'core/editor' ).autosave( { isPreview: true } );
		}
		const unsubscribe = subscribe( () => {
			const isSavingPost = select( 'core/editor' ).isSavingPost();
			if ( ! isSavingPost ) {
				unsubscribe();
				sendPreviewData();
			}
		} );

		function sendPreviewData() {
			const previewUrl = select( 'core/editor' ).getEditedPostPreviewLink();

			const featuredImageId = select( 'core/editor' ).getEditedPostAttribute( 'featured_media' );
			const featuredImage = featuredImageId
				? get( select( 'core' ).getMedia( featuredImageId ), 'source_url' )
				: null;

			const authorId = select( 'core/editor' ).getCurrentPostAttribute( 'author' );
			const author = find( select( 'core' ).getAuthors(), { id: authorId } );
			const editedPost = {
				title: select( 'core/editor' ).getEditedPostAttribute( 'title' ),
				URL: select( 'core/editor' ).getEditedPostAttribute( 'link' ),
				excerpt: select( 'core/editor' ).getEditedPostAttribute( 'excerpt' ),
				content: select( 'core/editor' ).getEditedPostAttribute( 'content' ),
				featured_image: featuredImage,
				author: author,
			};

			previewChannel.port1.postMessage( {
				previewUrl: previewUrl,
				editedPost: editedPost,
			} );
			previewChannel.port1.close();
		}
	} );
}

Prevents the default preview flow and sends a message to the parent frame
so we preview a post from there.

@param {MessagePort} calypsoPort Port used for communication with parent frame.

Prevents the default preview flow and sends a message to the parent frame so we preview a post from there .


function handleInsertClassicBlockMedia( calypsoPort ) {
	calypsoPort.addEventListener( 'message', onInsertClassicBlockMedia, false );
	calypsoPort.start();

	function onInsertClassicBlockMedia( message ) {
		const action = get( message, 'data.action' );
		if ( action !== 'insertClassicBlockMedia' ) {
			return;
		}
		const editorId = get( message, 'data.payload.editorId' );
		const media = get( message, 'data.payload.media' );
		tinymce.editors[ editorId ].execCommand( 'mceInsertContent', false, media );
	}
}

function handleInsertClassicBlockMedia ( calypsoPort ) { calypsoPort . addEventListener ( 'message' , onInsertClassicBlockMedia , false ) ; calypsoPort . start ( ) ; function onInsertClassicBlockMedia ( message ) { const action = get ( message , 'data.action' ) ; if ( action !== 'insertClassicBlockMedia' ) { return ; } const editorId = get ( message , 'data.payload.editorId' ) ; const media = get ( message , 'data.payload.media' ) ; tinymce . editors [ editorId ] . execCommand ( 'mceInsertContent' , false , media ) ; } }

Listens for insert media events happening in a Media Modal opened in a Classic Block,
and inserts the media into the appropriate block.

@param {MessagePort} calypsoPort Port used for communication with parent frame.

Listens for insert media events happening in a Media Modal opened in a Classic Block and inserts the media into the appropriate block .


function handleGoToAllPosts( calypsoPort ) {
	$( '#editor' ).on( 'click', '.edit-post-fullscreen-mode-close__toolbar a', e => {
		e.preventDefault();
		calypsoPort.postMessage( {
			action: 'goToAllPosts',
			payload: {
				unsavedChanges: select( 'core/editor' ).isEditedPostDirty(),
			},
		} );
	} );
}

Prevents the default closing flow and sends a message to the parent frame to
perform the navigation on the client side.

@param {MessagePort} calypsoPort Port used for communication with parent frame.

Prevents the default closing flow and sends a message to the parent frame to perform the navigation on the client side .


function openLinksInParentFrame() {
	const viewPostLinkSelectors = [
		'.components-notice-list .is-success .components-notice__action.is-link', // View Post link in success notice
		'.post-publish-panel__postpublish .components-panel__body.is-opened a', // Post title link in publish panel
		'.components-panel__body.is-opened .post-publish-panel__postpublish-buttons a.components-button', // View Post button in publish panel
	].join( ',' );
	$( '#editor' ).on( 'click', viewPostLinkSelectors, e => {
		e.preventDefault();
		window.open( e.target.href, '_top' );
	} );

	if ( calypsoifyGutenberg.manageReusableBlocksUrl ) {
		const manageReusableBlocksLinkSelectors = [
			'.editor-inserter__manage-reusable-blocks', // Link in the Blocks Inserter
			'a.components-menu-item__button[href*="post_type=wp_block"]', // Link in the More Menu
		].join( ',' );
		$( '#editor' ).on( 'click', manageReusableBlocksLinkSelectors, e => {
			e.preventDefault();
			window.open( calypsoifyGutenberg.manageReusableBlocksUrl, '_top' );
		} );
	}
}

Modify links in order to open them in parent window and not in a child iframe.

Modify links in order to open them in parent window and not in a child iframe .


function toolbarPin( editor ) {
	let isMonitoringScroll = false,
		isPinned = false,
		container;

	/**
	 * Assigns the container top-level variable to the current container.
	 */
	function setContainer() {
		container = editor.getContainer();
	}

	/**
	 * Updates the pinned state, toggling the container class as appropriate.
	 *
	 * @param {Boolean} toBePinned Whether toolbar should be pinned
	 */
	function togglePinned( toBePinned ) {
		isPinned = toBePinned;
		editor.dom.toggleClass( container, 'is-pinned', isPinned );
	}

	/**
	 * Checks whether the current pinned state of the editor should change,
	 * toggling the class as determined by the current viewport compared to the
	 * top edge of the container.
	 */
	const pinToolbarOnScroll = throttle( () => {
		if ( ! container ) {
			return;
		}

		if ( isPinned && window.pageYOffset < container.offsetTop ) {
			// Scroll doesn't reach container top and should be unpinned
			togglePinned( false );
		} else if ( ! isPinned && window.pageYOffset > container.offsetTop ) {
			// Scroll exceeds container top and should be pinned
			togglePinned( true );
		}
	}, 50 );

	/**
	 * Binds or unbinds the scroll event from the global window object, since
	 * pinning behavior is restricted to larger viewports whilst the visual
	 * editing mode is active.
	 */
	const maybeBindScroll = throttle( event => {
		const isVisual = ! editor.isHidden();
		const shouldBind = 'remove' !== event.type && isVisual && isWithinBreakpoint( '>660px' );

		if ( shouldBind === isMonitoringScroll ) {
			// Window event binding already matches expectation, so skip
			return;
		}

		const eventBindFn = ( shouldBind ? 'add' : 'remove' ) + 'EventListener';
		window[ eventBindFn ]( 'scroll', pinToolbarOnScroll );

		isMonitoringScroll = shouldBind;
		if ( isMonitoringScroll ) {
			setContainer();

			// May need to pin if resizing from small to large viewport
			pinToolbarOnScroll();
		} else {
			// Reset to default when not monitoring scroll
			togglePinned( false );
		}
	}, 200 );

	editor.on( 'init show hide remove', maybeBindScroll );
	window.addEventListener( 'resize', maybeBindScroll );
}

TinyMCE plugin for toggling an `is-pinned` class on the editor container
when the viewport scroll position exceeds the top edge of the container.

@param {Object} editor TinyMCE editor instance

TinyMCE plugin for toggling an is - pinned class on the editor container when the viewport scroll position exceeds the top edge of the container .


function _initStorage( options ) {
	const dbInfo = {};
	if ( options ) {
		for ( const i in options ) {
			dbInfo[ i ] = options[ i ];
		}
	}

	dbInfo.db = {};
	dummyStorage[ dbInfo.name ] = dbInfo.db;

	this._dbInfo = dbInfo;
	return Promise.resolve();
}

function _initStorage ( options ) { const dbInfo = { } ; if ( options ) { for ( const i in options ) { dbInfo [ i ] = options [ i ] ; } } dbInfo . db = { } ; dummyStorage [ dbInfo . name ] = dbInfo . db ; this . _dbInfo = dbInfo ; return Promise . resolve ( ) ; }

Config the localStorage backend, using options set in the config.

Config the localStorage backend using options set in the config .


function finish() {
		let node,
			regex = new RegExp( 'mceItemHidden|hidden(((Grammar|Spell)Error)|Suggestion)' ),
			nodes = editor.dom.select( 'span' ),
			i = nodes.length;

		while ( i-- ) {
			// reversed
			node = nodes[ i ];

			if ( node.className && regex.test( node.className ) ) {
				editor.dom.remove( node, true );
			}
		}

		// Rebuild the DOM so AtD core can find the text nodes
		editor.setContent( editor.getContent( { format: 'raw' } ), { format: 'raw' } );

		started = false;
		editor.nodeChanged();
		editor.fire( 'SpellcheckEnd' );
	}

Called when the user clicks "Finish" or when no more suggestions left. Removes all remaining spans and fires custom event.

Called when the user clicks Finish or when no more suggestions left . Removes all remaining spans and fires custom event .