1 import { first } from 'rxjs/operators'
2 import { ComponentRef, Injectable } from '@angular/core'
3 import { MarkdownService } from '@app/core'
6 ChannelMiniatureMarkupData,
9 PlaylistMiniatureMarkupData,
11 VideoMiniatureMarkupData,
13 } from '@shared/models'
14 import { DynamicElementService } from './dynamic-element.service'
16 ButtonMarkupComponent,
17 ChannelMiniatureMarkupComponent,
19 PlaylistMiniatureMarkupComponent,
20 VideoMiniatureMarkupComponent,
21 VideosListMarkupComponent
22 } from './peertube-custom-tags'
23 import { CustomMarkupComponent } from './peertube-custom-tags/shared'
25 type AngularBuilderFunction = (el: HTMLElement) => ComponentRef<CustomMarkupComponent>
26 type HTMLBuilderFunction = (el: HTMLElement) => HTMLElement
29 export class CustomMarkupService {
30 private angularBuilders: { [ selector: string ]: AngularBuilderFunction } = {
31 'peertube-button': el => this.buttonBuilder(el),
32 'peertube-video-embed': el => this.embedBuilder(el, 'video'),
33 'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'),
34 'peertube-video-miniature': el => this.videoMiniatureBuilder(el),
35 'peertube-playlist-miniature': el => this.playlistMiniatureBuilder(el),
36 'peertube-channel-miniature': el => this.channelMiniatureBuilder(el),
37 'peertube-videos-list': el => this.videosListBuilder(el)
40 private htmlBuilders: { [ selector: string ]: HTMLBuilderFunction } = {
41 'peertube-container': el => this.containerBuilder(el)
44 private customMarkdownRenderer: (text: string) => Promise<HTMLElement>
47 private dynamicElementService: DynamicElementService,
48 private markdown: MarkdownService
50 this.customMarkdownRenderer = (text: string) => {
51 return this.buildElement(text)
52 .then(({ rootElement }) => rootElement)
56 getCustomMarkdownRenderer () {
57 return this.customMarkdownRenderer
60 async buildElement (text: string) {
61 const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags())
63 const rootElement = document.createElement('div')
64 rootElement.innerHTML = html
66 for (const selector of Object.keys(this.htmlBuilders)) {
67 rootElement.querySelectorAll(selector)
68 .forEach((e: HTMLElement) => {
70 const element = this.execHTMLBuilder(selector, e)
71 // Insert as first child
72 e.insertBefore(element, e.firstChild)
74 console.error('Cannot inject component %s.', selector, err)
79 const loadedPromises: Promise<boolean>[] = []
81 for (const selector of Object.keys(this.angularBuilders)) {
82 rootElement.querySelectorAll(selector)
83 .forEach((e: HTMLElement) => {
85 const component = this.execAngularBuilder(selector, e)
87 if (component.instance.loaded) {
88 const p = component.instance.loaded.pipe(first()).toPromise()
89 loadedPromises.push(p)
92 this.dynamicElementService.injectElement(e, component)
94 console.error('Cannot inject component %s.', selector, err)
99 return { rootElement, componentsLoaded: Promise.all(loadedPromises) }
102 private getSupportedTags () {
103 return Object.keys(this.angularBuilders)
104 .concat(Object.keys(this.htmlBuilders))
107 private execHTMLBuilder (selector: string, el: HTMLElement) {
108 return this.htmlBuilders[selector](el)
111 private execAngularBuilder (selector: string, el: HTMLElement) {
112 return this.angularBuilders[selector](el)
115 private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') {
116 const data = el.dataset as EmbedMarkupData
117 const component = this.dynamicElementService.createElement(EmbedMarkupComponent)
119 this.dynamicElementService.setModel(component, { uuid: data.uuid, type })
124 private playlistMiniatureBuilder (el: HTMLElement) {
125 const data = el.dataset as PlaylistMiniatureMarkupData
126 const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent)
128 this.dynamicElementService.setModel(component, { uuid: data.uuid })
133 private channelMiniatureBuilder (el: HTMLElement) {
134 const data = el.dataset as ChannelMiniatureMarkupData
135 const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent)
139 displayLatestVideo: this.buildBoolean(data.displayLatestVideo) ?? true,
140 displayDescription: this.buildBoolean(data.displayDescription) ?? true
143 this.dynamicElementService.setModel(component, model)
148 private buttonBuilder (el: HTMLElement) {
149 const data = el.dataset as ButtonMarkupData
150 const component = this.dynamicElementService.createElement(ButtonMarkupComponent)
153 theme: data.theme ?? 'primary',
156 blankTarget: this.buildBoolean(data.blankTarget) ?? false
158 this.dynamicElementService.setModel(component, model)
163 private videoMiniatureBuilder (el: HTMLElement) {
164 const data = el.dataset as VideoMiniatureMarkupData
165 const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
169 onlyDisplayTitle: this.buildBoolean(data.onlyDisplayTitle) ?? false
172 this.dynamicElementService.setModel(component, model)
177 private videosListBuilder (el: HTMLElement) {
178 const data = el.dataset as VideosListMarkupData
179 const component = this.dynamicElementService.createElement(VideosListMarkupComponent)
182 onlyDisplayTitle: this.buildBoolean(data.onlyDisplayTitle) ?? false,
183 maxRows: this.buildNumber(data.maxRows) ?? -1,
185 sort: data.sort || '-publishedAt',
186 count: this.buildNumber(data.count) || 10,
188 categoryOneOf: this.buildArrayNumber(data.categoryOneOf) ?? [],
189 languageOneOf: this.buildArrayString(data.languageOneOf) ?? [],
191 filter: this.buildBoolean(data.onlyLocal) ? 'local' as VideoFilter : undefined
194 this.dynamicElementService.setModel(component, model)
199 private containerBuilder (el: HTMLElement) {
200 const data = el.dataset as ContainerMarkupData
202 // Move inner HTML in the new element we'll create
203 const content = el.innerHTML
206 const root = document.createElement('div')
207 root.innerHTML = content
209 const layoutClass = data.layout
210 ? 'layout-' + data.layout
213 root.classList.add('peertube-container', layoutClass)
216 root.setAttribute('width', data.width)
219 if (data.title || data.description) {
220 const headerElement = document.createElement('div')
221 headerElement.classList.add('header')
224 const titleElement = document.createElement('h4')
225 titleElement.innerText = data.title
226 headerElement.appendChild(titleElement)
229 if (data.description) {
230 const descriptionElement = document.createElement('div')
231 descriptionElement.innerText = data.description
232 headerElement.append(descriptionElement)
235 root.insertBefore(headerElement, root.firstChild)
241 private buildNumber (value: string) {
242 if (!value) return undefined
244 return parseInt(value, 10)
247 private buildBoolean (value: string) {
248 if (value === 'true') return true
249 if (value === 'false') return false
254 private buildArrayNumber (value: string) {
255 if (!value) return undefined
257 return value.split(',').map(v => parseInt(v, 10))
260 private buildArrayString (value: string) {
261 if (!value) return undefined
263 return value.split(',')