All files index.ts

100% Statements 45/45
97.5% Branches 39/40
100% Functions 9/9
100% Lines 42/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91                                                  25x 25x 23x   2x       1x 1x 13x 31x         13x 13x 13x 13x 13x 13x 13x 13x 12x   13x 13x 13x 13x 12x   13x 7x 7x 7x 7x 7x       1x 15x 15x 15x 73x 16x 2x 1x   73x   73x 17x 8x   17x 5x                
import type { Plugin } from 'unified';
import type { Root, Element } from 'hast';
import { visit } from 'unist-util-visit';
import { detailsNode } from './detailsNode.js';
import { trackNode } from './trackNode.js';
 
export type RehypeVideoOptions = {
  /**
   * URL suffix verification.
   * @default /\/(.*)(.mp4|.mov)$/
   */
  test?: RegExp | ((url: string) => boolean);
  /**
   * Support `<details>` tag to wrap <video>.
   * @default true
   */
  details?: boolean;
  /**
   * Support `<track>` tag to wrap <video>.
   * @default true
   */
  track?: boolean;
}
 
function isValidURL(string: string) {
  try {
    new URL(string);
    return true;
  } catch (err) {
    return false;
  }
}
 
const properties = { muted: 'muted', controls: 'controls', style: 'max-height:640px;' };
const queryStringToObject = (url: string) =>
  [...new URLSearchParams(url.split('?')[1])].reduce(
    (a: Record<string, string>, [k, v]) => ((a[k] = v), a),
    {}
  );
 
function reElement(node: Element, details: boolean, track: boolean, href: string) {
  const url = isValidURL(href.trim()) ? new URL(href) : null;
  const pathname = url?.pathname || href;
  const filename = pathname.split('/').pop()?.replace(/(\?|!|\#|$).+/, '');
  const params = queryStringToObject(href.replace(/\?\!/, "?").replace(/\?\!\#/, "?"));
  const { title = filename } = params;
  const result = trackNode(params);
  const searchParams = new URLSearchParams({ ...result.query });
  if (url) {
    url.search = searchParams.toString() ?? "";
  }
  node.tagName = 'video';
  node.children = [];
  node.properties = { ...properties, src: url?.toString() || href };
  if (track) {
    result.element.forEach((tr) => node.children.push({ ...tr }));
  }
  if (details) {
    const reNode = detailsNode(title);
    reNode.children.push({ ...node });
    node.children = reNode.children;
    node.tagName = reNode.tagName;
    node.properties = reNode.properties;
  }
}
 
const RehypeVideo: Plugin<[RehypeVideoOptions?], Root> = (options) => {
  const { test = /\/(.*)(.mp4|.mov)$/i, details = true, track = true } = options || {};
  return (tree) => {
    visit(tree, 'element', (node, index, parent) => {
      const isChecked = (str: string) => {
        if (test instanceof RegExp) return test.test(str.replace(/(\?|!|\#|$).+/g, '').toLocaleLowerCase())
        if (typeof test === 'function') return test(str)
        return false;
      }
      const child = node.children[0];
 
      if (node.tagName === 'p' && node.children.length === 1) {
        if (child.type === 'text' && isValidURL(child.value) && isChecked(child.value)) {
          reElement(node, details, track, child.value);
        }
        if (child.type === 'element' && child.tagName === 'a' && child.properties && typeof child.properties.href === 'string' && isChecked(child.properties.href)) {
          reElement(node, details, track, child.properties.href);
        }
      }
    });
  }
}
 
export default RehypeVideo;