Skip to content

Commit

Permalink
feat: support custom csp from transform
Browse files Browse the repository at this point in the history
  • Loading branch information
v8tenko committed Nov 5, 2024
1 parent bff7df9 commit 0d670f4
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 47 deletions.
4 changes: 2 additions & 2 deletions src/transform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {bold} from 'chalk';

import {log} from './log';
import liquidSnippet from './liquid';
import initMarkdownit from './md';
import initMarkdownIt from './md';

function applyLiquid(input: string, options: OptionsType) {
const {
Expand Down Expand Up @@ -36,7 +36,7 @@ function emitResult(html: string, env: EnvType): OutputType {
// eslint-disable-next-line consistent-return
function transform(originInput: string, options: OptionsType = {}) {
const input = applyLiquid(originInput, options);
const {parse, compile, env} = initMarkdownit(options);
const {parse, compile, env} = initMarkdownIt(options);

try {
return emitResult(compile(parse(input)), env);
Expand Down
4 changes: 2 additions & 2 deletions src/transform/md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import extractTitle from './title';
import getHeadings from './headings';
import sanitizeHtml from './sanitize';

function initMarkdownit(options: OptionsType) {
function initMarkdownIt(options: OptionsType) {
const {
allowHTML = false,
linkify = false,
Expand Down Expand Up @@ -172,4 +172,4 @@ function initCompiler(md: MarkdownIt, options: OptionsType, env: EnvType) {
};
}

export = initMarkdownit;
export = initMarkdownIt;
4 changes: 2 additions & 2 deletions src/transform/plugins/changelog/collect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {bold} from 'chalk';

import initMarkdownit from '../../md';
import initMarkdownIt from '../../md';
import imsize from '../imsize';
import {MarkdownItPluginOpts} from '../typings';

Expand All @@ -12,7 +12,7 @@ const BLOCK_START = '{% changelog %}';
const BLOCK_END = '{% endchangelog %}';

function parseChangelogs(str: string, path?: string) {
const {parse, compile, env} = initMarkdownit({
const {parse, compile, env} = initMarkdownIt({
plugins: [changelogPlugin, imsize],
extractChangelogs: true,
path,
Expand Down
16 changes: 16 additions & 0 deletions src/transform/plugins/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,19 @@ export const сarriage = platform === 'win32' ? '\r\n' : '\n';
export function generateID() {
return Math.random().toString(36).substr(2, 8);
}

export function append<T extends Record<string, []>, Key extends keyof T>(
target: T,
key: Key,
...values: T[Key]
) {
if (!target[key]) {
target[key] = values;

return;
}

values.forEach((value) => target[key].push(value));

return target;
}
13 changes: 12 additions & 1 deletion src/transform/plugins/video/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,26 @@ export enum VideoService {
Osf = 'osf',
Yandex = 'yandex',
Vk = 'vk',
Rutube = 'rutube',
url = 'url',
}

export type Service = {
csp?: Record<string, string[]>;
extract(url: string): string;
};

export type Services = [VideoService, Service][];

export const defaults: VideoFullOptions = {
url: videoUrl,
videoUrl,
youtube: {width: 640, height: 390},
vimeo: {width: 500, height: 281},
vine: {width: 600, height: 600, embed: 'simple'},
prezi: {width: 550, height: 400},
osf: {width: '100%', height: '100%'},
yandex: {width: 640, height: 390},
vk: {width: 640, height: 390},
rutube: {width: 640, height: 390},
url: {width: 640, height: 390},
};
20 changes: 16 additions & 4 deletions src/transform/plugins/video/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// Process @[osf](guid)
// Process @[yandex](videoID)
// Process @[vk](videoID)
// Process @[rutube](videoID)
// Process @[url](fullLink)

import type MarkdownIt from 'markdown-it';
// eslint-disable-next-line no-duplicate-imports
Expand All @@ -13,6 +15,8 @@ import type ParserInline from 'markdown-it/lib/parser_inline';
import type Renderer from 'markdown-it/lib/renderer';
import type {VideoFullOptions, VideoPluginOptions, VideoToken} from './types';

import {append} from '../utils';

import {parseVideoUrl} from './parsers';
import {VideoService, defaults} from './const';

Expand Down Expand Up @@ -74,13 +78,13 @@ function tokenizeVideo(md: MarkdownIt, options: VideoFullOptions): Renderer.Rend
: `<div class="embed-responsive embed-responsive-16by9"><iframe` +
` class="embed-responsive-item ${service}-player"` +
` type="text/html" width="${width}" height="${height}"` +
` src="${options.url(service, videoID, options)}"` +
` src="${options.videoUrl(service, videoID, options)}"` +
` frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div>`;
};
}

const EMBED_REGEX = /@\[([a-zA-Z].+)]\([\s]*(.*?)[\s]*[)]/im;
// eslint-disable-next-line @typescript-eslint/no-unused-vars

function videoEmbed(md: MarkdownIt, _options: VideoFullOptions): ParserInline.RuleInline {
return (state, silent) => {
const theState = state;
Expand All @@ -100,12 +104,14 @@ function videoEmbed(md: MarkdownIt, _options: VideoFullOptions): ParserInline.Ru
}

const service = match[1];
const videoID = parseVideoUrl(service, match[2]);
const parsed = parseVideoUrl(service, match[2]);

if (videoID === false) {
if (parsed === false) {
return false;
}

const [videoID, csp] = parsed;

const serviceStart = oldPos + 2;
const serviceEnd = md.helpers.parseLinkLabel(state, oldPos + 1, false);

Expand All @@ -127,6 +133,12 @@ function videoEmbed(md: MarkdownIt, _options: VideoFullOptions): ParserInline.Ru
}

theState.pos += theState.src.indexOf(')', theState.pos);

if (csp) {
state.env.meta ??= {};
append(state.env.meta, 'csp', csp);
}

return true;
};
}
Expand Down
84 changes: 56 additions & 28 deletions src/transform/plugins/video/parsers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {VideoService} from './const';
import {Services} from './const';

const ytRegex = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
export function youtubeParser(url: string) {
Expand Down Expand Up @@ -39,45 +39,73 @@ export function yandexParser(url: string) {
return match ? match[1] : url;
}

const vkRegex = /^https:\/\/vk.com\/video_ext\.php?(oid=[-\d]+&id=[-\d]+)/;
const vkRegex = /^https:\/\/vk\.com\/video_ext\.php\?(oid=[-\d]+&id=[-\d]+)/;
export function vkParser(url: string) {
const match = url.match(vkRegex);
return match ? match[1] : url;
}

export function parseVideoUrl(service: string, url: string): string | false {
const rutubeRegex = /^https:\/\/rutube\.ru\/video\/([a-zA-Z0-9]+)\/?/;
export function rutubeParser(url: string) {
const match = url.match(rutubeRegex);
return match ? match[1] : url;
}

const urlParser = (url: string) => url;

const supportedServices = Object.entries({
osf: {
extract: mfrParser,
},
prezi: {
extract: preziParser,
},
vimeo: {
extract: vimeoParser,
},
vine: {
extract: vineParser,
},
yandex: {
extract: yandexParser,
},
youtube: {
extract: youtubeParser,
},
vk: {
extract: vkParser,
csp: {
'frame-src': 'https://vk.com/',
},
},
rutube: {
extract: rutubeParser,
csp: {
'frame-src': 'https://rutube.ru/play/embed/',
},
},
url: {
extract: urlParser,
},
}) as Services;

export function parseVideoUrl(service: string, url: string) {
let videoID = '';
const normalizedService = service.toLowerCase();
const parsed = supportedServices.find(([name]) => name === normalizedService);

switch (service.toLowerCase()) {
case VideoService.YouTube:
videoID = youtubeParser(url);
break;
case VideoService.Vimeo:
videoID = vimeoParser(url);
break;
case VideoService.Vine:
videoID = vineParser(url);
break;
case VideoService.Prezi:
videoID = preziParser(url);
break;
case VideoService.Osf:
videoID = mfrParser(url);
break;
case VideoService.Yandex:
videoID = yandexParser(url);
break;
case VideoService.Vk:
videoID = vkParser(url);
break;
default:
return false;
if (!parsed) {
return false;
}

const [, videoParser] = parsed;

videoID = videoParser.extract(url);

// If the videoID field is empty, regex currently make it the close parenthesis.
if (videoID === ')') {
videoID = '';
}

return videoID;
return [videoID, videoParser.csp] as const;
}
17 changes: 9 additions & 8 deletions src/transform/plugins/video/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ export type VideoToken = Token & {
};

export type VideoServicesOptions = {
[VideoService.YouTube]: {width: number; height: number};
[VideoService.Vimeo]: {width: number; height: number};
[VideoService.Vine]: {width: number; height: number; embed: 'simple' | string};
[VideoService.Prezi]: {width: number; height: number};
[VideoService.Osf]: {width: string; height: string};
[VideoService.Yandex]: {width: number; height: number};
[VideoService.Vk]: {width: number; height: number};
[service in VideoService]: {
width: number | string;
height: number | string;
};
} & {
vine: {
embed: 'simple' | (string & {});
};
};

export type VideoFullOptions = VideoServicesOptions & {
url: VideoUrlFn;
videoUrl: VideoUrlFn;
};

export type VideoPluginOptions = Partial<VideoFullOptions>;
Expand Down
4 changes: 4 additions & 0 deletions src/transform/plugins/video/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export const videoUrl: VideoUrlFn = (service, videoID, options) => {
return `https://runtime.video.cloud.yandex.net/player/video/${videoID}`;
case 'vk':
return `https://vk.com/video_ext.php?${videoID}`;
case 'rutube':
return `https://rutube.ru/play/embed/${videoID}`;
case 'url':
return videoID;
default:
return service;
}
Expand Down

0 comments on commit 0d670f4

Please sign in to comment.