diff --git a/src/obsidianMarkdownPreprocessor.ts b/src/obsidianMarkdownPreprocessor.ts index d89e3758..28e680cb 100644 --- a/src/obsidianMarkdownPreprocessor.ts +++ b/src/obsidianMarkdownPreprocessor.ts @@ -5,6 +5,7 @@ import { FormatProcessor } from './processors/formatProcessor'; import { FragmentProcessor } from './processors/fragmentProcessor'; import { GridProcessor } from './processors/gridProcessor'; import { ImageProcessor } from './processors/imageProcessor'; +import { VideoProcessor } from './processors/videoProcessor'; import { InternalLinkProcessor } from './processors/internalLinkProcessor'; import { LatexProcessor } from './processors/latexProcessor'; import { MermaidProcessor } from './processors/mermaidProcessor'; @@ -29,6 +30,7 @@ export class ObsidianMarkdownPreprocessor { private multipleFileProcessor: MultipleFileProcessor; private blockProcessor: BlockProcessor; private imageProcessor: ImageProcessor; + private videoProcessor: VideoProcessor; private internalLinkProcessor: InternalLinkProcessor; private footnoteProcessor: FootnoteProcessor; private latexProcessor: LatexProcessor; @@ -54,6 +56,7 @@ export class ObsidianMarkdownPreprocessor { this.multipleFileProcessor = new MultipleFileProcessor(utils); this.blockProcessor = new BlockProcessor(); this.imageProcessor = new ImageProcessor(utils); + this.videoProcessor = new VideoProcessor(utils); this.internalLinkProcessor = new InternalLinkProcessor(utils); this.footnoteProcessor = new FootnoteProcessor(); this.latexProcessor = new LatexProcessor(); @@ -112,7 +115,8 @@ export class ObsidianMarkdownPreprocessor { const afterFootNoteProcessor = this.footnoteProcessor.process(afterBlockProcessor, options); const afterExcalidrawProcessor = this.excalidrawProcessor.process(afterFootNoteProcessor); const afterImageProcessor = this.imageProcessor.process(afterExcalidrawProcessor); - const afterInternalLinkProcessor = this.internalLinkProcessor.process(afterImageProcessor, options); + const afterVideoProcessor = this.videoProcessor.process(afterImageProcessor); + const afterInternalLinkProcessor = this.internalLinkProcessor.process(afterVideoProcessor, options); const afterLatexProcessor = this.latexProcessor.process(afterInternalLinkProcessor); const afterFormatProcessor = this.formatProcessor.process(afterLatexProcessor); const afterFragmentProcessor = this.fragmentProcessor.process(afterFormatProcessor, options); @@ -137,7 +141,8 @@ export class ObsidianMarkdownPreprocessor { this.log('afterFootNoteProcessor', afterBlockProcessor, afterFootNoteProcessor); this.log('afterExcalidrawProcessor', afterFootNoteProcessor, afterExcalidrawProcessor); this.log('afterImageProcessor', afterExcalidrawProcessor, afterImageProcessor); - this.log('afterInternalLinkProcessor', afterImageProcessor, afterInternalLinkProcessor); + this.log('afterVideoProcessor', afterImageProcessor, afterVideoProcessor); + this.log('afterInternalLinkProcessor', afterVideoProcessor, afterInternalLinkProcessor); this.log('afterLatexProcessor', afterInternalLinkProcessor, afterLatexProcessor); this.log('afterFormatProcessor', afterLatexProcessor, afterFormatProcessor); this.log('afterFragmentProcessor', afterFormatProcessor, afterFragmentProcessor); diff --git a/src/obsidianUtils.ts b/src/obsidianUtils.ts index c81f46c6..0de39bba 100644 --- a/src/obsidianUtils.ts +++ b/src/obsidianUtils.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'fs-extra'; import { App, FileSystemAdapter, resolveSubpath, TFile } from 'obsidian'; import path from 'path'; import { ImageCollector } from './imageCollector'; +import { VideoCollector } from './videoCollector'; import { AdvancedSlidesSettings } from './main'; export class ObsidianUtils { @@ -115,6 +116,9 @@ export class ObsidianUtils { if (!ImageCollector.getInstance().shouldCollect()) { base = '/'; } + if (!VideoCollector.getInstance().shouldCollect()) { + base = '/'; + } const file: TFile = this.getTFile(path); if (file) { return base + file.path; diff --git a/src/processors/videoProcessor.ts b/src/processors/videoProcessor.ts new file mode 100644 index 00000000..17403719 --- /dev/null +++ b/src/processors/videoProcessor.ts @@ -0,0 +1,198 @@ +/* eslint-disable no-var */ +import { VideoCollector } from 'src/videoCollector'; +import { CommentParser } from '../comment'; +import { ObsidianUtils } from '../obsidianUtils'; + +export class VideoProcessor { + private utils: ObsidianUtils; + private parser: CommentParser; + + private markdownVideoRegex = /^[ ]{0,3}!\[([^\]]*)\]\((.*(?:mp4)?)\)\s?()?/gim; + + private obsidianVideoRegex = /!\[\[(.*?(?:mp4))\s*\|?\s*([^\]]*)??\]\]\s?()?/ig; + private obsidianVideoReferenceRegex = /\[\[(.*?(?:mp4))\|?([^\]]*)??\]\]/gi; + + private htmlVideoRegex = /<\s*video[^>]*>(\s*)(.*?)(\s*)<\s*\/\s*video>/gim; + private htmlSrcRegex = /src="[^\s]*/im; + + constructor(utils: ObsidianUtils) { + this.utils = utils; + this.parser = new CommentParser(); + } + + process(markdown: string) { + return markdown + .split('\n') + .map(line => { + // Transform ![[myVideo.mp4]] to ![](myVideo.mp4) + if (this.obsidianVideoRegex.test(line)) { + return this.transformVideoString(line); + } + // Transform referenced videos to absolute paths (ex. in bg annotation) + if (this.obsidianVideoReferenceRegex.test(line)) { + return this.transformVideoReferenceString(line); + } + return line; + }) + .map(line => { + // Transform ![](myVideo.mp4) to html + if (this.markdownVideoRegex.test(line)) { + return this.htmlify(line); + } else if (this.htmlVideoRegex.test(line) && this.htmlSrcRegex.test(line)) { + // The video is inserted as html already. Just add it to the collector + // Find the source tag to get the file path + // Remove the absolute path from the html + if (VideoCollector.getInstance().shouldCollect()) { + const srcMatch = this.htmlSrcRegex.exec(line); + const srcString = srcMatch[0]; + const filePath = srcString.substring("src=/\"".length, srcString.length - 1); + VideoCollector.getInstance().addVideo(filePath); + const newVideoHtml = line.slice(0, srcMatch["index"] + "src=\"".length) + line.slice(srcMatch["index"] + "src=\"/".length); + return newVideoHtml; + } + return line; + } else { + return line; + } + }) + .join('\n'); + } + transformVideoReferenceString(line: string): string { + let result = line; + + let m; + this.obsidianVideoReferenceRegex.lastIndex = 0; + + while ((m = this.obsidianVideoReferenceRegex.exec(result)) !== null) { + if (m.index === this.obsidianVideoReferenceRegex.lastIndex) { + this.obsidianVideoReferenceRegex.lastIndex++; + } + + const [match, image] = m; + const filePath = this.utils.findFile(image); + result = result.replaceAll(match, filePath); + } + + return result; + } + + private transformVideoString(line: string) { + + let result = ""; + + let m; + this.obsidianVideoRegex.lastIndex = 0; + + while ((m = this.obsidianVideoRegex.exec(line)) !== null) { + if (m.index === this.obsidianVideoRegex.lastIndex) { + this.obsidianVideoRegex.lastIndex++; + } + const [, image, ext, comment] = m; + + const filePath = this.utils.findFile(image); + const commentAsString = this.buildComment(ext, comment) ?? ''; + result = result + `\n![](${filePath}) ${commentAsString}`; + } + return result; + } + + private buildComment(ext: string, commentAsString: string) { + const comment = commentAsString ? this.parser.parseComment(commentAsString) : this.parser.buildComment('element'); + + if (ext) { + if (ext.includes('x')) { + var [width, height] = ext.split('x'); + } else { + var width = ext; + } + comment.addStyle('width', `${width}px`); + + if (height) { + comment.addStyle('height', `${height}px`); + } + } + return this.parser.commentToString(comment); + } + + private htmlify(line: string) { + + let result = ""; + + let m; + this.markdownVideoRegex.lastIndex = 0; + + while ((m = this.markdownVideoRegex.exec(line)) !== null) { + if (m.index === this.markdownVideoRegex.lastIndex) { + this.markdownVideoRegex.lastIndex++; + } + // eslint-disable-next-line prefer-const + let [, alt, filePath, commentString] = m; + + if (alt && alt.includes('|')) { + commentString = this.buildComment(alt.split('|')[1], commentString) ?? ''; + } + + const comment = this.parser.parseLine(commentString) ?? this.parser.buildComment('element'); + + if (result.length > 0) { + result = result + '\n'; + } + + if (VideoCollector.getInstance().shouldCollect()) { + VideoCollector.getInstance().addVideo(filePath); + } + + if (filePath.startsWith('file:/')) { + filePath = this.transformAbsoluteFilePath(filePath); + } + + if (comment.hasStyle('width')) { + comment.addStyle('object-fit', 'fill'); + } + + if (!comment.hasStyle('align-self')) { + if (comment.hasAttribute('align')) { + + const align = comment.getAttribute('align'); + + switch (align) { + case 'left': + comment.addStyle('align-self', 'start'); + break; + case 'right': + comment.addStyle('align-self', 'end'); + break; + case 'center': + comment.addStyle('align-self', 'center'); + break; + case 'stretch': + comment.addStyle('align-self', 'stretch'); + comment.addStyle('object-fit', 'cover'); + comment.addStyle('height', '100%'); + comment.addStyle('width', '100%'); + break; + default: + break; + } + comment.deleteAttribute('align'); + } + } + + if (!comment.hasStyle('object-fit')) { + comment.addStyle('object-fit', 'scale-down'); + } + const videoHtml = ``; + result = result + videoHtml; + + } + return result + '\n'; + } + + private transformAbsoluteFilePath(path: string) { + const pathURL = new URL(path); + if (pathURL) { + return '/localFileSlash' + pathURL.pathname; + } + return path; + } +} diff --git a/src/revealExporter.ts b/src/revealExporter.ts index 6093ce22..249b0a1e 100644 --- a/src/revealExporter.ts +++ b/src/revealExporter.ts @@ -13,7 +13,8 @@ export class RevealExporter { this.vaultDirectory = utils.getVaultDirectory(); } - public async export(filePath: string, html: string, imgList: string[]) { + public async export(filePath: string, html: string, imgList: string[], vidList: string[]) { + const ext = path.extname(filePath); const folderName = path.basename(filePath).replaceAll(ext, ''); const folderDir = path.join(this.exportDirectory, folderName); @@ -32,6 +33,13 @@ export class RevealExporter { await copy(path.join(this.vaultDirectory, img), path.join(folderDir, img)); } + for (const vid of vidList) { + if (vid.startsWith('http')) { + continue; + } + await copy(path.join(this.vaultDirectory, vid), path.join(folderDir, vid)); + } + window.open('file://' + folderDir); } } diff --git a/src/revealRenderer.ts b/src/revealRenderer.ts index 31dac840..b3d3440d 100644 --- a/src/revealRenderer.ts +++ b/src/revealRenderer.ts @@ -1,6 +1,7 @@ import { basename, extname, join } from 'path'; import { ImageCollector } from './imageCollector'; +import { VideoCollector } from './videoCollector'; import Mustache from 'mustache'; import { ObsidianMarkdownPreprocessor } from './obsidianMarkdownPreprocessor'; import { ObsidianUtils } from './obsidianUtils'; @@ -49,6 +50,8 @@ export class RevealRenderer { if (renderForExport) { ImageCollector.getInstance().reset(); ImageCollector.getInstance().enable(); + VideoCollector.getInstance().reset(); + VideoCollector.getInstance().enable(); } const content = (await readFile(filePath.toString())).toString(); @@ -56,7 +59,8 @@ export class RevealRenderer { if (renderForExport) { ImageCollector.getInstance().disable(); - await this.exporter.export(filePath, rendered, ImageCollector.getInstance().getAll()); + VideoCollector.getInstance().disable(); + await this.exporter.export(filePath, rendered, ImageCollector.getInstance().getAll(), VideoCollector.getInstance().getAll()); rendered = await this.render(content, renderForPrint, renderForEmbed); } @@ -89,6 +93,9 @@ export class RevealRenderer { if (!ImageCollector.getInstance().shouldCollect()) { base = '/'; } + if (!VideoCollector.getInstance().shouldCollect()) { + base = '/'; + } const context = Object.assign(options, { title, diff --git a/src/videoCollector.ts b/src/videoCollector.ts new file mode 100644 index 00000000..83b22110 --- /dev/null +++ b/src/videoCollector.ts @@ -0,0 +1,38 @@ +export class VideoCollector { + private videos = new Set(); + private isCollecting = false; + + private static instance: VideoCollector; + private constructor() {} + + public static getInstance(): VideoCollector { + if (!VideoCollector.instance) { + VideoCollector.instance = new VideoCollector(); + } + return VideoCollector.instance; + } + + public reset() { + this.videos.clear(); + } + + public addVideo(filePath: string) { + this.videos.add(filePath); + } + + public getAll(): string[] { + return Array.of(...this.videos); + } + + public enable() { + this.isCollecting = true; + } + + public disable() { + this.isCollecting = false; + } + + public shouldCollect(): boolean { + return this.isCollecting; + } +}