1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-23 22:36:32 +02:00

Mobile: Resolves #12841: Allow editing code blocks from the Rich Text Editor (#12906)

This commit is contained in:
Henry Heino
2025-08-07 02:18:09 -07:00
committed by GitHub
parent 0312f2213d
commit 6704ab0d13
35 changed files with 607 additions and 111 deletions

View File

@@ -11,6 +11,7 @@ export const initializeEditor = ({
initialText,
initialNoteId,
settings,
onLocalize,
}: EditorProps) => {
const messenger = new WebViewToRNMessenger<EditorProcessApi, MainProcessApi>('markdownEditor', null);
@@ -23,6 +24,7 @@ export const initializeEditor = ({
initialText,
initialNoteId,
settings,
onLocalize,
onPasteFile: async (data) => {
const base64 = await readFileToBase64(data);

View File

@@ -1,5 +1,5 @@
import { EditorEvent } from '@joplin/editor/events';
import { EditorControl, EditorSettings } from '@joplin/editor/types';
import { EditorControl, EditorSettings, OnLocalize } from '@joplin/editor/types';
export interface EditorProcessApi {
editor: EditorControl;
@@ -14,6 +14,7 @@ export interface EditorProps {
parentElementClassName: string;
initialText: string;
initialNoteId: string;
onLocalize: OnLocalize;
settings: EditorSettings;
}

View File

@@ -12,6 +12,7 @@ const defaultRendererSettings: RenderSettings = {
noteHash: '',
initialScroll: 0,
readAssetBlob: async (_path: string) => new Blob(),
removeUnusedPluginAssets: true,
createEditPopupSyntax: '',
destroyEditPopupSyntax: '',

View File

@@ -23,6 +23,7 @@ export interface RenderSettings {
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
@@ -156,6 +157,7 @@ export default class Renderer {
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.

View File

@@ -39,6 +39,7 @@ const rewriteInternalAssetLinks = async (asset: RenderResultPluginAsset, content
interface Options {
inlineAssets: boolean;
removeUnusedPluginAssets: boolean;
container: HTMLElement;
readAssetBlob?(path: string): Promise<Blob>;
}
@@ -137,16 +138,22 @@ const addPluginAssets = async (assets: RenderResultPluginAsset[], options: Optio
// light to dark theme, and then back to light theme - in that case
// the viewer would remain dark because it would use the dark
// stylesheet that would still be in the DOM.
for (const [assetId, asset] of Object.entries(pluginAssetsAdded_)) {
if (!processedAssetIds.includes(assetId)) {
try {
asset.element.remove();
} catch (error) {
// We don't throw an exception but we log it since
// it shouldn't happen
console.warn('Tried to remove an asset but got an error', error);
//
// In some cases, however, we only want to rerender part of the document.
// In this case, old plugin assets may have been from the last full-page
// render and should not be removed.
if (options.removeUnusedPluginAssets) {
for (const [assetId, asset] of Object.entries(pluginAssetsAdded_)) {
if (!processedAssetIds.includes(assetId)) {
try {
asset.element.remove();
} catch (error) {
// We don't throw an exception but we log it since
// it shouldn't happen
console.warn('Tried to remove an asset but got an error', error);
}
pluginAssetsAdded_[assetId] = null;
}
pluginAssetsAdded_[assetId] = null;
}
}
};

View File

@@ -54,8 +54,13 @@ export interface RenderOptions {
highlightedKeywords: string[];
resources: ResourceInfos;
themeOverrides: Record<string, string|number>;
// If null, plugin assets will not be added to the document.
pluginAssetContainerSelector: string|null;
// When true, plugin assets are removed from the container when not used by the render result.
// This should be true for full-page renders.
removeUnusedPluginAssets: boolean;
noteHash: string;
initialScroll: number;

View File

@@ -219,6 +219,7 @@ const useWebViewSetup = (props: Props): SetUpResult<RendererControl> => {
}
return shim.fsDriver().fileAtPath(resolvedPath);
},
removeUnusedPluginAssets: options.removeUnusedPluginAssets,
};
await transferResources(options.resources);

View File

@@ -16,22 +16,6 @@ const postprocessHtml = (html: HTMLElement) => {
resource.src = `:/${resourceId}`;
}
// Re-add newlines to data-joplin-source-* that were removed
// by ProseMirror.
// TODO: Try to find a better solution
const sourceBlocks = html.querySelectorAll<HTMLPreElement>(
'pre[data-joplin-source-open][data-joplin-source-close].joplin-source',
);
for (const sourceBlock of sourceBlocks) {
const isBlock = sourceBlock.parentElement.tagName !== 'SPAN';
if (isBlock) {
const originalOpen = sourceBlock.getAttribute('data-joplin-source-open');
const originalClose = sourceBlock.getAttribute('data-joplin-source-close');
sourceBlock.setAttribute('data-joplin-source-open', `${originalOpen}\n`);
sourceBlock.setAttribute('data-joplin-source-close', `\n${originalClose}`);
}
}
return html;
};
@@ -73,6 +57,7 @@ export const initialize = async ({
settings,
initialText,
initialNoteId,
onLocalize: messenger.remoteApi.onLocalize,
onPasteFile: async (data) => {
const base64 = await readFileToBase64(data);
@@ -85,14 +70,20 @@ export const initialize = async ({
void messenger.remoteApi.onEditorEvent(event);
},
}, {
renderMarkupToHtml: async (markup) => {
renderMarkupToHtml: async (markup, options) => {
let language = MarkupLanguage.Markdown;
if (settings.language === EditorLanguageType.Html && !options.forceMarkdown) {
language = MarkupLanguage.Html;
}
return await messenger.remoteApi.onRender({
markup,
language: settings.language === EditorLanguageType.Html ? MarkupLanguage.Html : MarkupLanguage.Markdown,
language,
}, {
pluginAssetContainerSelector: `#${assetContainer.id}`,
splitted: true,
mapsToLine: true,
removeUnusedPluginAssets: options.isFullPageRender,
});
},
renderHtmlToMarkup: (node) => {

View File

@@ -1,5 +1,5 @@
import { EditorEvent } from '@joplin/editor/events';
import { EditorControl, EditorSettings, SearchState } from '@joplin/editor/types';
import { EditorControl, EditorSettings, OnLocalize, SearchState } from '@joplin/editor/types';
import { MarkupRecord, RendererControl } from '../rendererBundle/types';
import { RenderResult } from '@joplin/renderer/types';
@@ -19,6 +19,7 @@ type RenderOptionsSlice = {
pluginAssetContainerSelector: string;
splitted: boolean;
mapsToLine: true;
removeUnusedPluginAssets: boolean;
};
export interface MainProcessApi {
@@ -26,6 +27,7 @@ export interface MainProcessApi {
logMessage(message: string): Promise<void>;
onRender(markup: MarkupRecord, options: RenderOptionsSlice): Promise<RenderResult>;
onPasteFile(type: string, base64: string): Promise<void>;
onLocalize: OnLocalize;
}
export interface RichTextEditorControl {

View File

@@ -11,6 +11,7 @@ import shim from '@joplin/lib/shim';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import { RendererControl, RenderOptions } from '../rendererBundle/types';
import { ResourceInfos } from '@joplin/renderer/types';
import { _ } from '@joplin/lib/locale';
import { defaultSearchState } from '../../components/NoteEditor/SearchPanel';
const logger = Logger.create('useWebViewSetup');
@@ -50,6 +51,7 @@ const useMessenger = (props: UseMessengerProps) => {
noteHash: '',
initialScroll: 0,
pluginAssetContainerSelector: null,
removeUnusedPluginAssets: true,
};
return useMemo(() => {
@@ -70,6 +72,7 @@ const useMessenger = (props: UseMessengerProps) => {
splitted: options.splitted,
pluginAssetContainerSelector: options.pluginAssetContainerSelector,
mapsToLine: options.mapsToLine,
removeUnusedPluginAssets: options.removeUnusedPluginAssets,
},
);
return renderResult;
@@ -77,6 +80,7 @@ const useMessenger = (props: UseMessengerProps) => {
onPasteFile: async (type: string, base64: string) => {
onAttachRef.current(type, base64);
},
onLocalize: _,
};
const messenger = new RNToWebViewMessenger<MainProcessApi, EditorProcessApi>(