You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-23 22:36:32 +02:00
207 lines
6.8 KiB
TypeScript
207 lines
6.8 KiB
TypeScript
import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
|
|
import type { MarkupToHtmlConverter, RenderOptions, RenderOptionsGlobalSettings, FsDriver as RendererFsDriver, ResourceInfos } from '@joplin/renderer/types';
|
|
import makeResourceModel from './utils/makeResourceModel';
|
|
import addPluginAssets from './utils/addPluginAssets';
|
|
import { ExtraContentScriptSource, ForwardedJoplinSettings, MarkupRecord } from '../types';
|
|
import { ExtraContentScript } from '@joplin/lib/services/plugins/utils/loadContentScripts';
|
|
import { PluginOptions } from '@joplin/renderer/MarkupToHtml';
|
|
import afterFullPageRender from './utils/afterFullPageRender';
|
|
|
|
export interface RendererSetupOptions {
|
|
settings: ForwardedJoplinSettings;
|
|
useTransferredFiles: boolean;
|
|
pluginOptions: PluginOptions;
|
|
fsDriver: RendererFsDriver;
|
|
}
|
|
|
|
export interface RenderSettings {
|
|
theme: string;
|
|
highlightedKeywords: string[];
|
|
resources: ResourceInfos;
|
|
codeTheme: string;
|
|
noteHash: string;
|
|
initialScroll: number;
|
|
// If [null], plugin assets are not added to the document
|
|
pluginAssetContainerSelector: string|null;
|
|
removeUnusedPluginAssets: boolean;
|
|
|
|
splitted?: boolean; // Move CSS into a separate output
|
|
mapsToLine?: boolean; // Sourcemaps
|
|
|
|
createEditPopupSyntax: string;
|
|
destroyEditPopupSyntax: string;
|
|
|
|
pluginSettings: Record<string, unknown>;
|
|
globalSettings?: RenderOptionsGlobalSettings;
|
|
requestPluginSetting: (pluginId: string, settingKey: string)=> void;
|
|
readAssetBlob: (assetPath: string)=> Promise<Blob>;
|
|
}
|
|
|
|
export interface RendererOutput {
|
|
getOutputElement: ()=> HTMLElement;
|
|
afterRender: (setupOptions: RendererSetupOptions, renderSettings: RenderSettings)=> void;
|
|
}
|
|
|
|
export default class Renderer {
|
|
private markupToHtml_: MarkupToHtmlConverter;
|
|
private lastBodyRenderSettings_: RenderSettings|null = null;
|
|
private extraContentScripts_: ExtraContentScript[] = [];
|
|
private lastBodyMarkup_: MarkupRecord|null = null;
|
|
private lastPluginSettingsCacheKey_: string|null = null;
|
|
private resourcePathOverrides_: Record<string, string> = Object.create(null);
|
|
|
|
public constructor(private setupOptions_: RendererSetupOptions) {
|
|
this.recreateMarkupToHtml_();
|
|
}
|
|
|
|
private recreateMarkupToHtml_() {
|
|
this.markupToHtml_ = new MarkupToHtml({
|
|
extraRendererRules: this.extraContentScripts_,
|
|
fsDriver: this.setupOptions_.fsDriver,
|
|
isSafeMode: this.setupOptions_.settings.safeMode,
|
|
tempDir: this.setupOptions_.settings.tempDir,
|
|
ResourceModel: makeResourceModel(this.setupOptions_.settings.resourceDir),
|
|
pluginOptions: this.setupOptions_.pluginOptions,
|
|
});
|
|
}
|
|
|
|
// Intended for web, where resources can't be linked to normally.
|
|
public async setResourceFile(id: string, file: Blob) {
|
|
this.resourcePathOverrides_[id] = URL.createObjectURL(file);
|
|
}
|
|
|
|
public getResourcePathOverride(resourceId: string) {
|
|
if (Object.prototype.hasOwnProperty.call(this.resourcePathOverrides_, resourceId)) {
|
|
return this.resourcePathOverrides_[resourceId];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public async setExtraContentScriptsAndRerender(
|
|
extraContentScripts: ExtraContentScriptSource[],
|
|
) {
|
|
this.extraContentScripts_ = extraContentScripts.map(script => {
|
|
const scriptModule = ((0, eval)(script.js))({
|
|
pluginId: script.pluginId,
|
|
contentScriptId: script.id,
|
|
});
|
|
|
|
if (!scriptModule.plugin) {
|
|
throw new Error(`
|
|
Expected content script ${script.id} to export a function that returns an object with a "plugin" property.
|
|
Found: ${scriptModule}, which has keys ${Object.keys(scriptModule)}.
|
|
`);
|
|
}
|
|
|
|
return {
|
|
...script,
|
|
module: scriptModule,
|
|
};
|
|
});
|
|
this.recreateMarkupToHtml_();
|
|
|
|
// If possible, rerenders with the last rendering settings. The goal
|
|
// of this is to reduce the number of IPC calls between the viewer and
|
|
// React Native. We want the first render to be as fast as possible.
|
|
if (this.lastBodyMarkup_) {
|
|
await this.rerenderToBody(this.lastBodyMarkup_, this.lastBodyRenderSettings_);
|
|
}
|
|
}
|
|
|
|
public async render(markup: MarkupRecord, settings: RenderSettings) {
|
|
const options: RenderOptions = {
|
|
highlightedKeywords: settings.highlightedKeywords,
|
|
resources: settings.resources,
|
|
codeTheme: settings.codeTheme,
|
|
postMessageSyntax: 'window.joplinPostMessage_',
|
|
enableLongPress: true,
|
|
|
|
// Show an 'edit' popup over SVG images
|
|
editPopupFiletypes: ['image/svg+xml'],
|
|
createEditPopupSyntax: settings.createEditPopupSyntax,
|
|
destroyEditPopupSyntax: settings.destroyEditPopupSyntax,
|
|
itemIdToUrl: this.setupOptions_.useTransferredFiles ? (id: string) => this.getResourcePathOverride(id) : undefined,
|
|
|
|
settingValue: (pluginId: string, settingName: string) => {
|
|
const settingKey = `${pluginId}.${settingName}`;
|
|
|
|
if (!(settingKey in settings.pluginSettings)) {
|
|
// This should make the setting available on future renders.
|
|
settings.requestPluginSetting(pluginId, settingName);
|
|
return undefined;
|
|
}
|
|
|
|
return settings.pluginSettings[settingKey];
|
|
},
|
|
splitted: settings.splitted,
|
|
mapsToLine: settings.mapsToLine,
|
|
whiteBackgroundNoteRendering: markup.language === MarkupLanguage.Html,
|
|
globalSettings: settings.globalSettings,
|
|
};
|
|
|
|
const pluginSettingsCacheKey = JSON.stringify(settings.pluginSettings);
|
|
if (pluginSettingsCacheKey !== this.lastPluginSettingsCacheKey_) {
|
|
this.lastPluginSettingsCacheKey_ = pluginSettingsCacheKey;
|
|
this.markupToHtml_.clearCache(markup.language);
|
|
}
|
|
|
|
const result = await this.markupToHtml_.render(
|
|
markup.language,
|
|
markup.markup,
|
|
JSON.parse(settings.theme),
|
|
options,
|
|
);
|
|
|
|
// Adding plugin assets can be slow -- run it asynchronously.
|
|
if (settings.pluginAssetContainerSelector) {
|
|
void (async () => {
|
|
await addPluginAssets(result.pluginAssets, {
|
|
inlineAssets: this.setupOptions_.useTransferredFiles,
|
|
readAssetBlob: settings.readAssetBlob,
|
|
container: document.querySelector(settings.pluginAssetContainerSelector),
|
|
removeUnusedPluginAssets: settings.removeUnusedPluginAssets,
|
|
});
|
|
|
|
// Some plugins require this event to be dispatched just after being added.
|
|
document.dispatchEvent(new Event('joplin-noteDidUpdate'));
|
|
})();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public async rerenderToBody(markup: MarkupRecord, settings: RenderSettings) {
|
|
this.lastBodyMarkup_ = markup;
|
|
this.lastBodyRenderSettings_ = settings;
|
|
|
|
const contentContainer = document.getElementById('joplin-container-content') ?? document.body;
|
|
|
|
let html = '';
|
|
try {
|
|
const result = await this.render(markup, settings);
|
|
html = result.html;
|
|
} catch (error) {
|
|
if (!contentContainer) {
|
|
alert(`Renderer error: ${error}`);
|
|
} else {
|
|
contentContainer.textContent = `
|
|
Error: ${error}
|
|
|
|
${error.stack ?? ''}
|
|
`;
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
if (contentContainer) {
|
|
contentContainer.innerHTML = html;
|
|
}
|
|
|
|
afterFullPageRender(this.setupOptions_, settings);
|
|
}
|
|
|
|
public clearCache(markupLanguage: MarkupLanguage) {
|
|
this.markupToHtml_.clearCache(markupLanguage);
|
|
}
|
|
}
|