From 0d670f423f2f180d7867c1ac09ead3408188506c Mon Sep 17 00:00:00 2001 From: Gleb Voitenko Date: Tue, 5 Nov 2024 18:40:04 +0300 Subject: [PATCH] feat: support custom csp from transform --- src/transform/index.ts | 4 +- src/transform/md.ts | 4 +- src/transform/plugins/changelog/collect.ts | 4 +- src/transform/plugins/utils.ts | 16 +++++ src/transform/plugins/video/const.ts | 13 +++- src/transform/plugins/video/index.ts | 20 ++++-- src/transform/plugins/video/parsers.ts | 84 ++++++++++++++-------- src/transform/plugins/video/types.ts | 17 ++--- src/transform/plugins/video/utils.ts | 4 ++ 9 files changed, 119 insertions(+), 47 deletions(-) diff --git a/src/transform/index.ts b/src/transform/index.ts index c46f3663..3ca9dfa3 100644 --- a/src/transform/index.ts +++ b/src/transform/index.ts @@ -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 { @@ -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); diff --git a/src/transform/md.ts b/src/transform/md.ts index 9b196bf7..6d889fee 100644 --- a/src/transform/md.ts +++ b/src/transform/md.ts @@ -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, @@ -172,4 +172,4 @@ function initCompiler(md: MarkdownIt, options: OptionsType, env: EnvType) { }; } -export = initMarkdownit; +export = initMarkdownIt; diff --git a/src/transform/plugins/changelog/collect.ts b/src/transform/plugins/changelog/collect.ts index c4604055..a61dcfe9 100644 --- a/src/transform/plugins/changelog/collect.ts +++ b/src/transform/plugins/changelog/collect.ts @@ -1,6 +1,6 @@ import {bold} from 'chalk'; -import initMarkdownit from '../../md'; +import initMarkdownIt from '../../md'; import imsize from '../imsize'; import {MarkdownItPluginOpts} from '../typings'; @@ -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, diff --git a/src/transform/plugins/utils.ts b/src/transform/plugins/utils.ts index 13f708d1..977318d1 100644 --- a/src/transform/plugins/utils.ts +++ b/src/transform/plugins/utils.ts @@ -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, 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; +} diff --git a/src/transform/plugins/video/const.ts b/src/transform/plugins/video/const.ts index 72822621..28f4b6f6 100644 --- a/src/transform/plugins/video/const.ts +++ b/src/transform/plugins/video/const.ts @@ -11,10 +11,19 @@ export enum VideoService { Osf = 'osf', Yandex = 'yandex', Vk = 'vk', + Rutube = 'rutube', + url = 'url', } +export type Service = { + csp?: Record; + 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'}, @@ -22,4 +31,6 @@ export const defaults: VideoFullOptions = { 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}, }; diff --git a/src/transform/plugins/video/index.ts b/src/transform/plugins/video/index.ts index f14cf2a4..82678616 100644 --- a/src/transform/plugins/video/index.ts +++ b/src/transform/plugins/video/index.ts @@ -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 @@ -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'; @@ -74,13 +78,13 @@ function tokenizeVideo(md: MarkdownIt, options: VideoFullOptions): Renderer.Rend : `
`; }; } 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; @@ -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); @@ -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; }; } diff --git a/src/transform/plugins/video/parsers.ts b/src/transform/plugins/video/parsers.ts index d0b5dda3..0a01d28d 100644 --- a/src/transform/plugins/video/parsers.ts +++ b/src/transform/plugins/video/parsers.ts @@ -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) { @@ -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; } diff --git a/src/transform/plugins/video/types.ts b/src/transform/plugins/video/types.ts index 46409ba3..73557c59 100644 --- a/src/transform/plugins/video/types.ts +++ b/src/transform/plugins/video/types.ts @@ -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; diff --git a/src/transform/plugins/video/utils.ts b/src/transform/plugins/video/utils.ts index db1a4353..73821696 100644 --- a/src/transform/plugins/video/utils.ts +++ b/src/transform/plugins/video/utils.ts @@ -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; }