1 import { ComponentRef, Injectable } from '@angular/core'
2 import { MarkdownService } from '@app/core'
5 ChannelMiniatureMarkupData,
8 PlaylistMiniatureMarkupData,
10 VideoMiniatureMarkupData,
12 } from '@shared/models'
13 import { DynamicElementService } from './dynamic-element.service'
15 ButtonMarkupComponent,
16 ChannelMiniatureMarkupComponent,
18 PlaylistMiniatureMarkupComponent,
19 VideoMiniatureMarkupComponent,
20 VideosListMarkupComponent
21 } from './peertube-custom-tags'
23 type AngularBuilderFunction = (el: HTMLElement) => ComponentRef<any>
24 type HTMLBuilderFunction = (el: HTMLElement) => HTMLElement
27 export class CustomMarkupService {
28 private angularBuilders: { [ selector: string ]: AngularBuilderFunction } = {
29 'peertube-button': el => this.buttonBuilder(el),
30 'peertube-video-embed': el => this.embedBuilder(el, 'video'),
31 'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'),
32 'peertube-video-miniature': el => this.videoMiniatureBuilder(el),
33 'peertube-playlist-miniature': el => this.playlistMiniatureBuilder(el),
34 'peertube-channel-miniature': el => this.channelMiniatureBuilder(el),
35 'peertube-videos-list': el => this.videosListBuilder(el)
38 private htmlBuilders: { [ selector: string ]: HTMLBuilderFunction } = {
39 'peertube-container': el => this.containerBuilder(el)
42 private customMarkdownRenderer: (text: string) => Promise<HTMLElement>
45 private dynamicElementService: DynamicElementService,
46 private markdown: MarkdownService
48 this.customMarkdownRenderer = async (text: string) => this.buildElement(text)
51 getCustomMarkdownRenderer () {
52 return this.customMarkdownRenderer
55 async buildElement (text: string) {
56 const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags())
58 const rootElement = document.createElement('div')
59 rootElement.innerHTML = html
61 for (const selector of Object.keys(this.htmlBuilders)) {
62 rootElement.querySelectorAll(selector)
63 .forEach((e: HTMLElement) => {
65 const element = this.execHTMLBuilder(selector, e)
66 // Insert as first child
67 e.insertBefore(element, e.firstChild)
69 console.error('Cannot inject component %s.', selector, err)
74 for (const selector of Object.keys(this.angularBuilders)) {
75 rootElement.querySelectorAll(selector)
76 .forEach((e: HTMLElement) => {
78 const component = this.execAngularBuilder(selector, e)
80 this.dynamicElementService.injectElement(e, component)
82 console.error('Cannot inject component %s.', selector, err)
90 private getSupportedTags () {
91 return Object.keys(this.angularBuilders)
92 .concat(Object.keys(this.htmlBuilders))
95 private execHTMLBuilder (selector: string, el: HTMLElement) {
96 return this.htmlBuilders[selector](el)
99 private execAngularBuilder (selector: string, el: HTMLElement) {
100 return this.angularBuilders[selector](el)
103 private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') {
104 const data = el.dataset as EmbedMarkupData
105 const component = this.dynamicElementService.createElement(EmbedMarkupComponent)
107 this.dynamicElementService.setModel(component, { uuid: data.uuid, type })
112 private playlistMiniatureBuilder (el: HTMLElement) {
113 const data = el.dataset as PlaylistMiniatureMarkupData
114 const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent)
116 this.dynamicElementService.setModel(component, { uuid: data.uuid })
121 private channelMiniatureBuilder (el: HTMLElement) {
122 const data = el.dataset as ChannelMiniatureMarkupData
123 const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent)
127 displayLatestVideo: this.buildBoolean(data.displayLatestVideo) ?? true,
128 displayDescription: this.buildBoolean(data.displayDescription) ?? true
131 this.dynamicElementService.setModel(component, model)
136 private buttonBuilder (el: HTMLElement) {
137 const data = el.dataset as ButtonMarkupData
138 const component = this.dynamicElementService.createElement(ButtonMarkupComponent)
141 theme: data.theme ?? 'primary',
144 blankTarget: this.buildBoolean(data.blankTarget) ?? false
146 this.dynamicElementService.setModel(component, model)
151 private videoMiniatureBuilder (el: HTMLElement) {
152 const data = el.dataset as VideoMiniatureMarkupData
153 const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
157 onlyDisplayTitle: this.buildBoolean(data.onlyDisplayTitle) ?? false
160 this.dynamicElementService.setModel(component, model)
165 private videosListBuilder (el: HTMLElement) {
166 const data = el.dataset as VideosListMarkupData
167 const component = this.dynamicElementService.createElement(VideosListMarkupComponent)
170 onlyDisplayTitle: this.buildBoolean(data.onlyDisplayTitle) ?? false,
171 maxRows: this.buildNumber(data.maxRows) ?? -1,
173 sort: data.sort || '-publishedAt',
174 count: this.buildNumber(data.count) || 10,
176 categoryOneOf: this.buildArrayNumber(data.categoryOneOf) ?? [],
177 languageOneOf: this.buildArrayString(data.languageOneOf) ?? [],
179 filter: this.buildBoolean(data.onlyLocal) ? 'local' as VideoFilter : undefined
182 this.dynamicElementService.setModel(component, model)
187 private containerBuilder (el: HTMLElement) {
188 const data = el.dataset as ContainerMarkupData
190 // Move inner HTML in the new element we'll create
191 const content = el.innerHTML
194 const root = document.createElement('div')
195 root.innerHTML = content
197 const layoutClass = data.layout
198 ? 'layout-' + data.layout
201 root.classList.add('peertube-container', layoutClass)
204 root.setAttribute('width', data.width)
207 if (data.title || data.description) {
208 const headerElement = document.createElement('div')
209 headerElement.classList.add('header')
212 const titleElement = document.createElement('h4')
213 titleElement.innerText = data.title
214 headerElement.appendChild(titleElement)
217 if (data.description) {
218 const descriptionElement = document.createElement('div')
219 descriptionElement.innerText = data.description
220 headerElement.append(descriptionElement)
223 root.insertBefore(headerElement, root.firstChild)
229 private buildNumber (value: string) {
230 if (!value) return undefined
232 return parseInt(value, 10)
235 private buildBoolean (value: string) {
236 if (value === 'true') return true
237 if (value === 'false') return false
242 private buildArrayNumber (value: string) {
243 if (!value) return undefined
245 return value.split(',').map(v => parseInt(v, 10))
248 private buildArrayString (value: string) {
249 if (!value) return undefined
251 return value.split(',')