Merge branch 'laurent22:dev' into dev

pull/12121/head
Aqiel Oostenbrug 2025-04-17 15:31:21 +02:00 committed by GitHub
commit 2c016264a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 541 additions and 378 deletions

View File

@ -62,6 +62,7 @@ packages/app-mobile/locales
packages/app-mobile/node_modules
packages/app-mobile/pluginAssets/
packages/fork-*
!packages/fork-uslug
packages/default-plugins/plugin-base-repo/
packages/default-plugins/plugin-sources/
packages/htmlpack/dist/
@ -924,6 +925,8 @@ packages/editor/CodeMirror/editorCommands/duplicateLine.js
packages/editor/CodeMirror/editorCommands/editorCommands.js
packages/editor/CodeMirror/editorCommands/insertLineAfter.test.js
packages/editor/CodeMirror/editorCommands/insertLineAfter.js
packages/editor/CodeMirror/editorCommands/jumpToHash.test.js
packages/editor/CodeMirror/editorCommands/jumpToHash.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
@ -1002,6 +1005,8 @@ packages/fork-htmlparser2/src/__tests__/events.js
packages/fork-htmlparser2/src/__tests__/stream.js
packages/fork-htmlparser2/src/index.spec.js
packages/fork-htmlparser2/src/index.js
packages/fork-uslug/lib/uslug.test.js
packages/fork-uslug/lib/uslug.js
packages/generator-joplin/generators/app/templates/api/index.js
packages/generator-joplin/generators/app/templates/api/noteListType.js
packages/generator-joplin/generators/app/templates/api/types.js

4
.gitignore vendored
View File

@ -899,6 +899,8 @@ packages/editor/CodeMirror/editorCommands/duplicateLine.js
packages/editor/CodeMirror/editorCommands/editorCommands.js
packages/editor/CodeMirror/editorCommands/insertLineAfter.test.js
packages/editor/CodeMirror/editorCommands/insertLineAfter.js
packages/editor/CodeMirror/editorCommands/jumpToHash.test.js
packages/editor/CodeMirror/editorCommands/jumpToHash.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
@ -977,6 +979,8 @@ packages/fork-htmlparser2/src/__tests__/events.js
packages/fork-htmlparser2/src/__tests__/stream.js
packages/fork-htmlparser2/src/index.spec.js
packages/fork-htmlparser2/src/index.js
packages/fork-uslug/lib/uslug.test.js
packages/fork-uslug/lib/uslug.js
packages/generator-joplin/generators/app/templates/api/index.js
packages/generator-joplin/generators/app/templates/api/noteListType.js
packages/generator-joplin/generators/app/templates/api/types.js

View File

@ -13,13 +13,6 @@ export default function(context) {
const token = tokens[idx];
if (token.info !== 'justtesting') return defaultRender(tokens, idx, options, env, self);
const postMessageWithResponseTest = `
webviewApi.postMessage('${contentScriptId}', 'justtesting').then(function(response) {
console.info('Got response in content script: ' + response);
});
return false;
`;
// Rich text editor support:
// The joplin-editable and joplin-source CSS classes mark the generated div
// as a region that needs special processing when converting back to markdown.
@ -38,14 +31,23 @@ export default function(context) {
${richTextEditorMetadata}
<p>JUST TESTING: <pre>${markdownIt.utils.escapeHtml(leftPad(token.content.trim(), 10, 'x'))}</pre></p>
<p><a href="#" onclick="${postMessageWithResponseTest.replace(/\n/g, ' ')}">Click to post a message "justtesting" to plugin and check the response in the console</a></p>
<p>
<a
href="#"
data-content-script-id="${markdownIt.utils.escapeHtml(contentScriptId)}"
class="post-message-link"
>
Click to post a message "justtesting" to plugin and check the response in the console
</a>
</p>
</div>
`;
};
},
assets: function() {
return [
{ name: 'markdownItTestPlugin.css' }
{ name: 'markdownItTestPlugin.css' },
{ name: 'markdownItTestPluginRuntime.js' },
];
},
}

View File

@ -0,0 +1,14 @@
const addClickHandlers = () => {
const postMessageLinks = document.querySelectorAll('.post-message-link');
for (const link of postMessageLinks) {
const contentScriptId = link.getAttribute('data-content-script-id');
link.onclick = async () => {
const response = await webviewApi.postMessage(contentScriptId, 'justtesting');
link.textContent = 'Got response in content script: ' + response;
};
}
};
document.addEventListener('joplin-noteDidUpdate', () => {
addClickHandlers();
});

View File

@ -167,7 +167,9 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
scrollTo: (options: ScrollOptions) => {
if (options.type === ScrollOptionTypes.Hash) {
if (!webviewRef.current) return;
webviewRef.current.send('scrollToHash', options.value as string);
const hash: string = options.value;
webviewRef.current.send('scrollToHash', hash);
editorRef.current.jumpToHash(hash);
} else if (options.type === ScrollOptionTypes.Percent) {
const percent = options.value as number;
setEditorPercentScroll(percent);

View File

@ -43,6 +43,7 @@ import useKeyboardRefocusHandler from './utils/useKeyboardRefocusHandler';
import useDocument from '../../../hooks/useDocument';
import useEditDialog from './utils/useEditDialog';
import useEditDialogEventListeners from './utils/useEditDialogEventListeners';
import Setting from '@joplin/lib/models/Setting';
import useTextPatternsLookup from './utils/useTextPatternsLookup';
const logger = Logger.create('TinyMCE');
@ -728,6 +729,25 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
language_url: ['en_US', 'en_GB'].includes(language) ? undefined : `${bridge().vendorDir()}/lib/tinymce/langs/${language}`,
toolbar: toolbar.join(' '),
localization_function: _,
// See https://www.tiny.cloud/docs/tinymce/latest/tinymce-and-csp/#content_security_policy
content_security_policy: Setting.value('featureFlag.richText.useStrictContentSecurityPolicy') ? [
// Media: *: Allow users to include images and videos from the internet (e.g. ![](http://example.com/image.png)).
// Media: blob: Allow loading images/videos/audio from blob URLs. The Rich Text Editor
// replaces certain base64 URLs with blob URLs.
// Media: data: Allow loading images and other media from data: URLs
'default-src \'self\'',
'img-src \'self\' blob: data: *', // Images
'media-src \'self\' blob: data: *', // Audio and video players
// Disallow certain unused features
'child-src \'none\'', // Should not contain sub-frames
'object-src \'none\'', // Objects can be used for script injection
'form-action \'none\'', // No submitting forms
// Styles: unsafe-inline: TinyMCE uses inline style="" styles.
// Styles: *: Allow users to include styles from the internet (e.g. <style src="https://example.com/style.css">)
'style-src \'self\' \'unsafe-inline\' * data:',
].join(' ; ') : undefined,
contextmenu: false,
browser_spellcheck: true,

View File

@ -2,72 +2,37 @@ import PluginService from '@joplin/lib/services/plugins/PluginService';
import { useEffect } from 'react';
import { Editor } from 'tinymce';
const useWebViewApi = (editor: Editor, window: Window) => {
interface WebViewApi {
postMessage: (contentScriptId: string, message: unknown)=> Promise<unknown>;
}
interface ExtendedWindow extends Window {
webviewApi: WebViewApi;
}
const useWebViewApi = (editor: Editor, containerWindow: Window) => {
useEffect(() => {
if (!editor) return ()=>{};
if (!window) return ()=>{};
if (!containerWindow) return ()=>{};
const scriptElement = window.document.createElement('script');
const channelId = `plugin-post-message-${Math.random()}`;
scriptElement.appendChild(window.document.createTextNode(`
window.webviewApi = {
postMessage: (contentScriptId, message) => {
const channelId = ${JSON.stringify(channelId)};
const messageId = Math.random();
window.parent.postMessage({
channelId,
messageId,
contentScriptId,
message,
}, '*');
const waitForResponse = async () => {
while (true) {
const messageEvent = await new Promise(resolve => {
window.addEventListener('message', event => {
resolve(event);
}, {once: true});
});
if (messageEvent.source !== window.parent || messageEvent.data.messageId !== messageId) {
continue;
}
const data = messageEvent.data;
return data.response;
}
};
return waitForResponse();
},
};
`));
const editorWindow = editor.getWin();
editorWindow.document.head.appendChild(scriptElement);
const onMessageHandler = async (event: MessageEvent) => {
if (event.source !== editorWindow || event.data.channelId !== channelId) {
return;
}
const contentScriptId = event.data.contentScriptId;
const pluginService = PluginService.instance();
const plugin = pluginService.pluginById(
pluginService.pluginIdByContentScriptId(contentScriptId),
);
const result = await plugin.emitContentScriptMessage(contentScriptId, event.data.message);
editorWindow.postMessage({
messageId: event.data.messageId,
response: result,
}, '*');
const editorWindow = editor.getWin() as ExtendedWindow;
const webviewApi: WebViewApi = {
postMessage: async (contentScriptId: string, message: unknown) => {
const pluginService = PluginService.instance();
const plugin = pluginService.pluginById(
pluginService.pluginIdByContentScriptId(contentScriptId),
);
return await plugin.emitContentScriptMessage(contentScriptId, message);
},
};
window.addEventListener('message', onMessageHandler);
editorWindow.webviewApi = webviewApi;
return () => {
window.removeEventListener('message', onMessageHandler);
scriptElement.remove();
if (editorWindow.webviewApi === webviewApi) {
editorWindow.webviewApi = undefined;
}
};
}, [editor, window]);
}, [editor, containerWindow]);
};
export default useWebViewApi;

View File

@ -163,8 +163,7 @@ function NoteEditorContent(props: NoteEditorProps) {
scrollbarSize: props.scrollbarSize,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null): Promise<any[]> => {
const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null) => {
options = {
contentMaxWidthTarget: '',
...options,
@ -172,7 +171,7 @@ function NoteEditorContent(props: NoteEditorProps) {
const theme = themeStyle(options.themeId ? options.themeId : props.themeId);
const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, {
const markupToHtml = markupLanguageUtils.newMarkupToHtml(props.plugins, {
resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`,
customCss: props.customCss,
});
@ -183,7 +182,7 @@ function NoteEditorContent(props: NoteEditorProps) {
scrollbarSize: props.scrollbarSize,
whiteBackgroundNoteRendering: options.whiteBackgroundNoteRendering,
});
}, [props.themeId, props.scrollbarSize, props.customCss, props.contentMaxWidth]);
}, [props.plugins, props.themeId, props.scrollbarSize, props.customCss, props.contentMaxWidth]);
const handleProvisionalFlag = useCallback(() => {
if (props.isProvisional) {

View File

@ -139,8 +139,11 @@
const viewerPercent = scrollmap.translateL2V(percent);
const newScrollTop = viewerPercent * maxScrollTop();
// The next scroll event cannot be skipped in order to correctly
// scroll to the target section in a different note when follwing a link
// Even if the scroll position hasn't changed (percent is the same),
// we still ignore the next scroll event, so that it doesn't create
// undesired side effects.
// https://github.com/laurent22/joplin/issues/7617
ignoreNextScrollEvent();
if (Math.floor(contentElement.scrollTop) !== Math.floor(newScrollTop)) {
percentScroll_ = percent;

View File

@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "3.3.4",
"version": "3.3.5",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,

View File

@ -1,5 +1,5 @@
module.exports = {
hash:"cfa07333af79f4db4bc9ca008fb257f8", files: {
hash:"6950f3929f5177b0bb2a0c039669810d", files: {
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },

View File

@ -1 +1 @@
module.exports = {"hash":"cfa07333af79f4db4bc9ca008fb257f8","files":["highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}
module.exports = {"hash":"6950f3929f5177b0bb2a0c039669810d","files":["highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}

View File

@ -1 +1 @@
module.exports = `LyogZ2xvYmFsIG1lcm1haWQgKi8KCmZ1bmN0aW9uIG1lcm1haWRSZWFkeSgpIHsKCS8vIFRoZSBNZXJtYWlkIGluaXRpYWxpemF0aW9uIGNvZGUgcmVuZGVycyB0aGUgTWVybWFpZCBjb2RlIHdpdGhpbiBhbnkgZWxlbWVudCB3aXRoIGNsYXNzICJtZXJtYWlkIiBvcgoJLy8gSUQgIm1lcm1haWQiLiBIb3dldmVyIGluIHNvbWUgY2FzZXMgc29tZSBlbGVtZW50cyBtaWdodCBoYXZlIHRoaXMgSUQgYnV0IG5vdCBiZSBNZXJtYWlkIGNvZGUuCgkvLyBGb3IgZXhhbXBsZSwgTWFya2Rvd24gY29kZSBsaWtlIHRoaXM6CgkvLwoJLy8gICAgICMgTWVybWFpZAoJLy8KCS8vIFdpbGwgZ2VuZXJhdGUgdGhpcyBIVE1MOgoJLy8KCS8vICAgICA8aDEgaWQ9Im1lcm1haWQiPk1lcm1haWQ8L2gxPgoJLy8KCS8vIEFuZCB0aGF0J3MgZ29pbmcgdG8gbWFrZSB0aGUgbGliIHNldCB0aGUgYG1lcm1haWRgIG9iamVjdCB0byB0aGUgSDEgZWxlbWVudC4KCS8vIFNvIGJlbG93LCB3ZSBkb3VibGUtY2hlY2sgdGhhdCB3aGF0IHdlIGhhdmUgcmVhbGx5IGlzIGFuIGluc3RhbmNlIG9mIHRoZSBsaWJyYXJ5LgoJcmV0dXJuIHR5cGVvZiBtZXJtYWlkICE9PSAndW5kZWZpbmVkJyAmJiBtZXJtYWlkICE9PSBudWxsICYmIHR5cGVvZiBtZXJtYWlkID09PSAnb2JqZWN0JyAmJiAhIW1lcm1haWQuaW5pdGlhbGl6ZTsKfQoKY29uc3QgaXNEYXJrTW9kZSA9ICgpID0+IHsKCS8vIElmIGFueSBtZXJtYWlkIGVsZW1lbnRzIGFyZSBtYXJrZWQgYXMgcmVxdWlyaW5nIGRhcmsgbW9kZSwgcmVuZGVyICphbGwqCgkvLyBtZXJtYWlkIGVsZW1lbnRzIGluIGRhcmsgbW9kZS4KCXJldHVybiAhIWRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoJy5tZXJtYWlkLmpvcGxpbi0tbWVybWFpZC11c2UtZGFyay10aGVtZScpOwp9OwoKZnVuY3Rpb24gbWVybWFpZEluaXQoKSB7CglpZiAobWVybWFpZFJlYWR5KCkpIHsKCQljb25zdCBtZXJtYWlkVGFyZ2V0Tm9kZXMgPSBkb2N1bWVudC5nZXRFbGVtZW50c0J5Q2xhc3NOYW1lKCdtZXJtYWlkJyk7CgoJCXRyeSB7CgkJCWNvbnN0IGRhcmtNb2RlID0gaXNEYXJrTW9kZSgpOwoJCQltZXJtYWlkLmluaXRpYWxpemUoewoJCQkJLy8gV2UgY2FsbCBtZXJtYWlkLnJ1biBvdXJzZWx2ZXMgd2hlbmV2ZXIgdGhlIG5vdGUgdXBkYXRlcy4gRG9uJ3QgYXV0by1zdGFydAoJCQkJc3RhcnRPbkxvYWQ6IGZhbHNlLAoKCQkJCWRhcmtNb2RlLAoJCQkJdGhlbWU6IGRhcmtNb2RlID8gJ2RhcmsnIDogJ2RlZmF1bHQnLAoJCQl9KTsKCQkJbWVybWFpZC5ydW4oewoJCQkJbm9kZXM6IG1lcm1haWRUYXJnZXROb2RlcywKCQkJfSk7CgkJfSBjYXRjaCAoZXJyb3IpIHsKCQkJY29uc29sZS5lcnJvcignTWVybWFpZCBlcnJvcicsIGVycm9yKTsKCQl9CgoJCS8vIFJlc2V0dGluZyBlbGVtZW50cyBzaXplIC0gc2VlIG1lcm1haWQudHMKCQlmb3IgKGNvbnN0IGVsZW1lbnQgb2YgbWVybWFpZFRhcmdldE5vZGVzKSB7CgkJCWVsZW1lbnQuc3R5bGUud2lkdGggPSAnMTAwJSc7CgkJfQoJfQp9Cgpkb2N1bWVudC5hZGRFdmVudExpc3RlbmVyKCdqb3BsaW4tbm90ZURpZFVwZGF0ZScsICgpID0+IHsKCW1lcm1haWRJbml0KCk7Cn0pOwoKY29uc3QgaW5pdElJRF8gPSBzZXRJbnRlcnZhbCgoKSA9PiB7Cgljb25zdCBpc1JlYWR5ID0gbWVybWFpZFJlYWR5KCk7CglpZiAoaXNSZWFkeSkgewoJCWNsZWFySW50ZXJ2YWwoaW5pdElJRF8pOwoJCW1lcm1haWRJbml0KCk7Cgl9Cn0sIDEwMCk7Cgpkb2N1bWVudC5hZGRFdmVudExpc3RlbmVyKCdET01Db250ZW50TG9hZGVkJywgKCkgPT4gewoJLy8gSW4gc29tZSBlbnZpcm9ubWVudHMsIHdlIGNhbiBsb2FkIE1lcm1haWQgaW1tZWRpYXRlbHkgKGUuZy4gbW9iaWxlKS4KCS8vIElmIHdlIGRvbid0IGxvYWQgaXQgaW1tZWRpYXRlbHkgaW4gdGhlc2UgZW52aXJvbm1lbnRzLCBNZXJtYWlkIHNlZW1zCgkvLyB0byBpbml0aWFsaXplIGFuZCBhdXRvLXJ1biwgYnV0IHdpdGhvdXQgb3VyIGNvbmZpZ3VyYXRpb24gY2hhbmdlcy4KCWlmIChtZXJtYWlkUmVhZHkoKSkgewoJCW1lcm1haWRJbml0KCk7Cgl9IGVsc2UgewoJCWNsZWFySW50ZXJ2YWwoaW5pdElJRF8pOwoJfQp9KTsK`;
module.exports = `LyogZ2xvYmFsIG1lcm1haWQgKi8KCmZ1bmN0aW9uIG1lcm1haWRSZWFkeSgpIHsKCS8vIFRoZSBNZXJtYWlkIGluaXRpYWxpemF0aW9uIGNvZGUgcmVuZGVycyB0aGUgTWVybWFpZCBjb2RlIHdpdGhpbiBhbnkgZWxlbWVudCB3aXRoIGNsYXNzICJtZXJtYWlkIiBvcgoJLy8gSUQgIm1lcm1haWQiLiBIb3dldmVyIGluIHNvbWUgY2FzZXMgc29tZSBlbGVtZW50cyBtaWdodCBoYXZlIHRoaXMgSUQgYnV0IG5vdCBiZSBNZXJtYWlkIGNvZGUuCgkvLyBGb3IgZXhhbXBsZSwgTWFya2Rvd24gY29kZSBsaWtlIHRoaXM6CgkvLwoJLy8gICAgICMgTWVybWFpZAoJLy8KCS8vIFdpbGwgZ2VuZXJhdGUgdGhpcyBIVE1MOgoJLy8KCS8vICAgICA8aDEgaWQ9Im1lcm1haWQiPk1lcm1haWQ8L2gxPgoJLy8KCS8vIEFuZCB0aGF0J3MgZ29pbmcgdG8gbWFrZSB0aGUgbGliIHNldCB0aGUgYG1lcm1haWRgIG9iamVjdCB0byB0aGUgSDEgZWxlbWVudC4KCS8vIFNvIGJlbG93LCB3ZSBkb3VibGUtY2hlY2sgdGhhdCB3aGF0IHdlIGhhdmUgcmVhbGx5IGlzIGFuIGluc3RhbmNlIG9mIHRoZSBsaWJyYXJ5LgoJcmV0dXJuIHR5cGVvZiBtZXJtYWlkICE9PSAndW5kZWZpbmVkJyAmJiBtZXJtYWlkICE9PSBudWxsICYmIHR5cGVvZiBtZXJtYWlkID09PSAnb2JqZWN0JyAmJiAhIW1lcm1haWQuaW5pdGlhbGl6ZTsKfQoKY29uc3QgaXNEYXJrTW9kZSA9ICgpID0+IHsKCS8vIElmIGFueSBtZXJtYWlkIGVsZW1lbnRzIGFyZSBtYXJrZWQgYXMgcmVxdWlyaW5nIGRhcmsgbW9kZSwgcmVuZGVyICphbGwqCgkvLyBtZXJtYWlkIGVsZW1lbnRzIGluIGRhcmsgbW9kZS4KCXJldHVybiAhIWRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoJy5tZXJtYWlkLmpvcGxpbi0tbWVybWFpZC11c2UtZGFyay10aGVtZScpOwp9OwoKY29uc3QgaW5pdEV4cG9ydEJ1dHRvbnMgPSAoKSA9PiB7Cgljb25zdCBleHBvcnRCdXR0b25zID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbCgnLm1lcm1haWQtZXhwb3J0LWdyYXBoID4gYnV0dG9uJyk7Cglmb3IgKGNvbnN0IGJ1dHRvbiBvZiBleHBvcnRCdXR0b25zKSB7CgkJYnV0dG9uLm9uY2xpY2sgPSAoKSA9PiB7CgkJCWNvbnN0IGJ1dHRvbkNvbnRhaW5lciA9IGJ1dHRvbi5wYXJlbnRFbGVtZW50OwoJCQljb25zdCBtZXJtYWlkRWxlbSA9IGJ1dHRvbkNvbnRhaW5lci5uZXh0RWxlbWVudFNpYmxpbmc7CgoJCQljb25zdCByaWdodENsaWNrRXZlbnQgPSBuZXcgUG9pbnRlckV2ZW50KCdjb250ZXh0bWVudScsIHtidWJibGVzOiB0cnVlfSk7CgkJCXJpZ2h0Q2xpY2tFdmVudC50YXJnZXQgPSBtZXJtYWlkRWxlbTsKCQkJbWVybWFpZEVsZW0uZGlzcGF0Y2hFdmVudChyaWdodENsaWNrRXZlbnQpOwoJCX07Cgl9Cn07CgpmdW5jdGlvbiBtZXJtYWlkSW5pdCgpIHsKCWlmIChtZXJtYWlkUmVhZHkoKSkgewoJCWNvbnN0IG1lcm1haWRUYXJnZXROb2RlcyA9IGRvY3VtZW50LmdldEVsZW1lbnRzQnlDbGFzc05hbWUoJ21lcm1haWQnKTsKCgkJdHJ5IHsKCQkJY29uc3QgZGFya01vZGUgPSBpc0RhcmtNb2RlKCk7CgkJCW1lcm1haWQuaW5pdGlhbGl6ZSh7CgkJCQkvLyBXZSBjYWxsIG1lcm1haWQucnVuIG91cnNlbHZlcyB3aGVuZXZlciB0aGUgbm90ZSB1cGRhdGVzLiBEb24ndCBhdXRvLXN0YXJ0CgkJCQlzdGFydE9uTG9hZDogZmFsc2UsCgoJCQkJZGFya01vZGUsCgkJCQl0aGVtZTogZGFya01vZGUgPyAnZGFyaycgOiAnZGVmYXVsdCcsCgkJCX0pOwoJCQltZXJtYWlkLnJ1bih7CgkJCQlub2RlczogbWVybWFpZFRhcmdldE5vZGVzLAoJCQl9KTsKCQl9IGNhdGNoIChlcnJvcikgewoJCQljb25zb2xlLmVycm9yKCdNZXJtYWlkIGVycm9yJywgZXJyb3IpOwoJCX0KCgkJLy8gUmVzZXR0aW5nIGVsZW1lbnRzIHNpemUgLSBzZWUgbWVybWFpZC50cwoJCWZvciAoY29uc3QgZWxlbWVudCBvZiBtZXJtYWlkVGFyZ2V0Tm9kZXMpIHsKCQkJZWxlbWVudC5zdHlsZS53aWR0aCA9ICcxMDAlJzsKCQl9CgoJCWluaXRFeHBvcnRCdXR0b25zKCk7Cgl9Cn0KCmRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ2pvcGxpbi1ub3RlRGlkVXBkYXRlJywgKCkgPT4gewoJbWVybWFpZEluaXQoKTsKfSk7Cgpjb25zdCBpbml0SUlEXyA9IHNldEludGVydmFsKCgpID0+IHsKCWNvbnN0IGlzUmVhZHkgPSBtZXJtYWlkUmVhZHkoKTsKCWlmIChpc1JlYWR5KSB7CgkJY2xlYXJJbnRlcnZhbChpbml0SUlEXyk7CgkJbWVybWFpZEluaXQoKTsKCX0KfSwgMTAwKTsKCmRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ0RPTUNvbnRlbnRMb2FkZWQnLCAoKSA9PiB7CgkvLyBJbiBzb21lIGVudmlyb25tZW50cywgd2UgY2FuIGxvYWQgTWVybWFpZCBpbW1lZGlhdGVseSAoZS5nLiBtb2JpbGUpLgoJLy8gSWYgd2UgZG9uJ3QgbG9hZCBpdCBpbW1lZGlhdGVseSBpbiB0aGVzZSBlbnZpcm9ubWVudHMsIE1lcm1haWQgc2VlbXMKCS8vIHRvIGluaXRpYWxpemUgYW5kIGF1dG8tcnVuLCBidXQgd2l0aG91dCBvdXIgY29uZmlndXJhdGlvbiBjaGFuZ2VzLgoJaWYgKG1lcm1haWRSZWFkeSgpKSB7CgkJbWVybWFpZEluaXQoKTsKCX0gZWxzZSB7CgkJY2xlYXJJbnRlcnZhbChpbml0SUlEXyk7Cgl9Cn0pOwo=`;

View File

@ -12,6 +12,7 @@ import { RegionSpec } from './utils/formatting/RegionSpec';
import toggleInlineSelectionFormat from './utils/formatting/toggleInlineSelectionFormat';
import getSearchState from './utils/getSearchState';
import { noteIdFacet, setNoteIdEffect } from './utils/selectedNoteIdExtension';
import jumpToHash from './editorCommands/jumpToHash';
interface Callbacks {
onUndoRedo(): void;
@ -207,6 +208,10 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E
return textFound;
}
public jumpToHash(hash: string) {
return jumpToHash(this.editor, hash);
}
public addStyles(...styles: Parameters<typeof EditorView.theme>) {
const compartment = new Compartment();
this.editor.dispatch({

View File

@ -13,6 +13,7 @@ import sortSelectedLines from './sortSelectedLines';
import { closeSearchPanel, findNext, findPrevious, openSearchPanel, replaceAll, replaceNext, searchPanelOpen } from '@codemirror/search';
import { focus } from '@joplin/lib/utils/focusHandler';
import { showLinkEditor } from '../utils/handleLinkEditRequests';
import jumpToHash from './jumpToHash';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export type EditorCommandFunction = (editor: EditorView, ...args: any[])=> void|any;
@ -107,6 +108,10 @@ const editorCommands: Record<EditorCommandType, EditorCommandFunction> = {
}],
});
},
[EditorCommandType.JumpToHash]: (editor, hash: string) => {
return jumpToHash(editor, hash);
},
};
export default editorCommands;

View File

@ -0,0 +1,41 @@
import { EditorSelection } from '@codemirror/state';
import createTestEditor from '../testUtil/createTestEditor';
import jumpToHash from './jumpToHash';
describe('jumpToHash', () => {
test.each([
{
doc: 'This is an anchor: <a id="test">Test</a>',
expectedCursorLocation: 'This is an anchor: <a id="test">'.length,
waitForTags: ['HTMLTag'],
},
{
doc: '<div>HTML block: This is an anchor: <a id="test">Test</a></div>',
expectedCursorLocation: '<div>HTML block: This is an anchor: <a id="test">'.length,
waitForTags: ['HTMLBlock'],
},
])('should support jumping to elements with ID set to "test" (case %#)', async ({ doc: docText, expectedCursorLocation, waitForTags }) => {
const editor = await createTestEditor(
docText,
EditorSelection.cursor(1),
waitForTags,
);
expect(jumpToHash(editor, 'test')).toBe(true);
const cursorPosition = editor.state.selection.main.anchor;
expect(
editor.state.sliceDoc(0, cursorPosition),
).toBe(
editor.state.sliceDoc(0, expectedCursorLocation),
);
});
test('should jump to Markdown headers', async () => {
const editor = await createTestEditor(
'Line 1\n## Line 2',
EditorSelection.cursor(0),
['ATXHeading2'],
);
expect(jumpToHash(editor, 'line-2')).toBe(true);
expect(editor.state.selection.main.anchor).toBe(editor.state.doc.length);
});
});

View File

@ -0,0 +1,82 @@
import { ensureSyntaxTree } from '@codemirror/language';
import { EditorSelection } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import uslug from '@joplin/fork-uslug/lib/uslug';
import { SyntaxNodeRef } from '@lezer/common';
const jumpToHash = (view: EditorView, hash: string) => {
const state = view.state;
const timeout = 1_000; // Maximum time to spend parsing the syntax tree
let targetLocation: number|undefined = undefined;
const removeQuotes = (quoted: string) => quoted.replace(/^["'](.*)["']$/, '$1');
const makeEnterNode = (offset: number) => (node: SyntaxNodeRef) => {
const nodeToText = (node: SyntaxNodeRef) => {
return state.sliceDoc(node.from + offset, node.to + offset);
};
// Returns the attribute with the given name for [node]
const getHtmlNodeAttr = (node: SyntaxNodeRef, attrName: string) => {
if (node.from === node.to) return null; // Empty
const content = node.node.resolveInner(node.from + 1);
// Search for the "id" attribute
const attributes = content.getChildren('Attribute');
for (const attribute of attributes) {
const nameNode = attribute.getChild('AttributeName');
const valueNode = attribute.getChild('AttributeValue');
if (nameNode && valueNode) {
const name = nodeToText(nameNode).toLowerCase().replace(/^"(.*)"$/, '$1');
if (name === attrName) {
return removeQuotes(nodeToText(valueNode));
}
}
}
return null;
};
const found = targetLocation !== undefined;
if (found) return false; // Skip this node
let matches = false;
if (node.name.startsWith('SetextHeading') || node.name.startsWith('ATXHeading')) {
const nodeText = nodeToText(node)
.replace(/^#+\s/, '') // Leading #s in headers
.replace(/\n-+$/, ''); // Trailing --s in headers
matches = hash === uslug(nodeText);
} else if (node.name === 'HTMLTag' || node.name === 'HTMLBlock') {
// CodeMirror adds HTML information to Markdown documents using overlays attached
// to HTMLTag and HTMLBlock nodes.
// Use .enter to enter the overlay and visit the HTML nodes:
node.node.enter(node.from, 1).toTree().iterate({ enter: makeEnterNode(node.from) });
} else if (node.name === 'OpenTag') {
matches = getHtmlNodeAttr(node, 'id') === hash || getHtmlNodeAttr(node, 'name') === hash;
}
if (matches) {
targetLocation = node.to + offset;
return false;
}
const keepIterating = !matches;
return keepIterating;
};
// Iterate over the entire syntax tree.
ensureSyntaxTree(state, state.doc.length, timeout).iterate({
enter: makeEnterNode(0),
});
if (targetLocation !== undefined) {
view.dispatch({
selection: EditorSelection.cursor(targetLocation),
scrollIntoView: true,
});
return true;
}
return false;
};
export default jumpToHash;

View File

@ -37,6 +37,7 @@
"@codemirror/search": "6.5.8",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.35.0",
"@joplin/fork-uslug": "^2.0.0",
"@lezer/common": "1.2.3",
"@lezer/highlight": "1.2.1",
"@lezer/markdown": "1.3.2",

View File

@ -74,8 +74,9 @@ export enum EditorCommandType {
SelectedText = 'selectedText',
InsertText = 'insertText',
ReplaceSelection = 'replaceSelection',
SetText = 'setText',
JumpToHash = 'jumpToHash',
}
// Because the editor package can run in a WebView, plugin content scripts

View File

@ -5,6 +5,8 @@
Modified for Joplin:
- Added support for emojis - "🐶🐶🐶🐱" => "dogdogdogcat"
- Smaller package size: Removed dependencies on functionality that's now built-in to JavaScript (Unicode normalization, Unicode character class regular expressions).
- Types: Migrated to TypeScript.
* * *

View File

@ -1 +1 @@
module.exports = require('./lib/uslug');
module.exports = require('./lib/uslug').default;

View File

@ -0,0 +1,5 @@
module.exports = {
testMatch: [
'**/*.test.js',
],
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,13 +0,0 @@
/*
* List of Unicode code that are flagged as separator.
*
* Contains Unicode code of:
* - Zs = Separator, space
* - Zl = Separator, line
* - Zp = Separator, paragraph
*
* This list has been computed from http://unicode.org/Public/UNIDATA/UnicodeData.txt
* curl -s http://unicode.org/Public/UNIDATA/UnicodeData.txt | grep -E ';Zs;|;Zl;|;Zp;' | cut -d \; -f 1 | xargs -I{} printf '%d, ' 0x{}
*
*/
exports.Z = [32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288];

View File

@ -1,61 +0,0 @@
(function() {
var L = require('./L').L,
N = require('./N').N,
Z = require('./Z').Z,
M = require('./M').M,
unorm = require('unorm');
var nodeEmoji = require('node-emoji')
var _unicodeCategory = function(code) {
if (~L.indexOf(code)) return 'L';
if (~N.indexOf(code)) return 'N';
if (~Z.indexOf(code)) return 'Z';
if (~M.indexOf(code)) return 'M';
return undefined;
};
module.exports = function(string, options) {
string = string || '';
options = options || {};
var allowedChars = options.allowedChars || '-_~';
var lower = typeof options.lower === 'boolean' ? options.lower : true;
var spaces = typeof options.spaces === 'boolean' ? options.spaces : false;
var rv = [];
var noEmojiString = nodeEmoji.unemojify(string);
var chars = unorm.nfkc(noEmojiString);
for(var i = 0; i < chars.length; i++) {
var c = chars[i];
var code = c.charCodeAt(0);
// Allow Common CJK Unified Ideographs
// See: http://www.unicode.org/versions/Unicode6.0.0/ch12.pdf - Table 12-2
if (0x4E00 <= code && code <= 0x9FFF) {
rv.push(c);
continue;
}
// Allow Hangul
if (0xAC00 <= code && code <= 0xD7A3) {
rv.push(c);
continue;
}
// Japanese ideographic punctuation
if ((0x3000 <= code && code <= 0x3002) || (0xFF01 <= code && code <= 0xFF02)) {
rv.push(' ');
}
if (allowedChars.indexOf(c) != -1) {
rv.push(c);
continue;
}
var val = _unicodeCategory(code);
if (val && ~'LNM'.indexOf(val)) rv.push(c);
if (val && ~'Z'.indexOf(val)) rv.push(' ');
}
var slug = rv.join('').replace(/^\s+|\s+$/g, '').replace(/\s+/g,' ');
if (!spaces) slug = slug.replace(/[\s\-]+/g,'-');
if (lower) slug = slug.toLowerCase();
return slug;
};
}());

View File

@ -0,0 +1,51 @@
// Based on @joplin/fork-uslug
//
// The original is Copyright (c) 2012 Jeremy Selier
//
// MIT Licensed
//
// You may find a copy of this license in the LICENSE file that should have been provided
// to you with a copy of this software.
import uslug from './uslug';
const word0 = 'Ελληνικά';
const word1 = [word0, word0].join('-');
const word2 = [word0, word0].join(' - ');
const tests: [string, string][] = [
['', ''],
['The \u212B symbol invented by A. J. \u00C5ngstr\u00F6m (1814, L\u00F6gd\u00F6, \u2013 1874) denotes the length 10\u207B\u00B9\u2070 m.', 'the-å-symbol-invented-by-a-j-ångström-1814-lögdö-1874-denotes-the-length-1010-m'],
['Быстрее и лучше!', 'быстрее-и-лучше'],
['xx x - "#$@ x', 'xx-x-x'],
['Bän...g (bang)', 'bäng-bang'],
[word0, word0.toLowerCase()],
[word1, word1.toLowerCase()],
[word2, word1.toLowerCase()],
[' a ', 'a'],
['tags/', 'tags'],
['y_u_no', 'y_u_no'],
['el-ni\xf1o', 'el-ni\xf1o'],
['x荿', 'x荿'],
['ϧ΃蒬蓣', '\u03e7蒬蓣'],
['¿x', 'x'],
['汉语/漢語', '汉语漢語'],
['فار,سي', 'فارسي'],
['เแโ|ใไ', 'เแโใไ'],
['日本語ドキュメンテ(ーション)', '日本語ドキュメンテーション'],
['一二三四五六七八九十!。。。', '一二三四五六七八九十'],
['संसद में काम नहीं तो वेतन क्यों?', 'संसद-में-काम-नहीं-तो-वेतन-क्यों'],
['เร่งรัด \'ปรับเงินเดือนท้องถิ่น 1 ขั้น\' ตามมติ ครม.', 'เร่งรัด-ปรับเงินเดือนท้องถิ่น-1-ขั้น-ตามมติ-ครม'],
['オバマ大統領が病院爆撃の調査へ同意するように、協力してください!', 'オバマ大統領が病院爆撃の調査へ同意するように-協力してください'],
['일본정부 법무대신(法務大臣): 우리는 일본 입관법의 재검토를 요구한다!', '일본정부-법무대신法務大臣-우리는-일본-입관법의-재검토를-요구한다'],
['😁', 'grin'],
['😁a', 'grina'],
['🐶🐶🐶🐱', 'dogdogdogcat'],
];
describe('uslug', () => {
it.each(tests)('should convert %s to %s', (input, expected) => {
expect(uslug(input)).toBe(expected);
});
it('should support "allowedChars"', () => {
expect(uslug('qbc,fe', { allowedChars: 'q' })).toBe('qbcfe');
});
});

View File

@ -0,0 +1,95 @@
// Based on @joplin/fork-uslug
//
// The original is Copyright (c) 2012 Jeremy Selier
//
// MIT Licensed
//
// You may find a copy of this license in the LICENSE file that should have been provided
// to you with a copy of this software.
const nodeEmoji = require('node-emoji');
// Very old browsers (e.g. Chrome < 64, which is from 2018) may not support
// \p{} regexes.
let regexes_;
try {
regexes_ = {
// eslint-disable-next-line prefer-regex-literals -- Needed to prevent syntax errors
L: new RegExp('\\p{L}', 'u'), N: new RegExp('\\p{N}', 'u'), Z: new RegExp('\\p{Z}', 'u'), M: new RegExp('\\p{M}', 'u'),
};
} catch (error) {
console.error(error);
regexes_ = undefined;
}
const _unicodeCategory = function(c: string) {
if (!regexes_) {
console.warn('Unicode RegExps not loaded. Skipping category check.');
return undefined;
}
for (const [key, val] of Object.entries(regexes_)) {
if (c.match(val)) return key;
}
return undefined;
};
interface Options {
lower?: boolean;
spaces?: boolean;
allowedChars?: string;
}
export default function(string: string, options: Options = {}) {
string = string || '';
options = options || {};
const allowedChars = options.allowedChars || '-_~';
const lower = typeof options.lower === 'boolean' ? options.lower : true;
const spaces = typeof options.spaces === 'boolean' ? options.spaces : false;
const rv = [];
const noEmojiString: string = nodeEmoji.unemojify(string);
const chars = noEmojiString.normalize('NFKC').split('');
for (let i = 0; i < chars.length; i++) {
const c = chars[i];
const code = c.charCodeAt(0);
// Allow Common CJK Unified Ideographs
// See: http://www.unicode.org/versions/Unicode6.0.0/ch12.pdf - Table 12-2
if (0x4E00 <= code && code <= 0x9FFF) {
rv.push(c);
continue;
}
// Allow Hangul
if (0xAC00 <= code && code <= 0xD7A3) {
rv.push(c);
continue;
}
// Japanese ideographic punctuation
if ((0x3000 <= code && code <= 0x3002) || (0xFF01 <= code && code <= 0xFF02)) {
rv.push(' ');
}
if (allowedChars.indexOf(c) !== -1) {
rv.push(c);
continue;
}
const val = _unicodeCategory(c);
if (val && ~'LNM'.indexOf(val)) rv.push(c);
if (val && ~'Z'.indexOf(val)) rv.push(' ');
}
let slug = rv.join('').replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ');
if (!spaces) slug = slug.replace(/[\s-]+/g, '-');
if (lower) slug = slug.toLowerCase();
return slug;
}

View File

@ -1,25 +1,40 @@
{
"name": "@joplin/fork-uslug",
"version": "1.0.22",
"version": "2.0.0",
"description": "A permissive slug generator that works with unicode.",
"author": "Jeremy Selier <jerem.selier@gmail.com>",
"publishConfig": {
"access": "public"
},
"scripts": {
"test": "jest",
"build": "yarn tsc",
"tsc": "tsc --project tsconfig.json",
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json"
},
"dependencies": {
"node-emoji": "1.11.0",
"unorm": "1.6.0"
"node-emoji": "1.11.0"
},
"devDependencies": {
"should": "13.2.3"
"@types/jest": "29.5.12",
"@types/node": "18.19.67",
"jest": "29.7.0",
"typescript": "5.4.5"
},
"repository": {
"type": "git",
"url": "http://github.com/jeremys/uslug.git"
},
"main": "./index",
"exports": {
".": "./index.js",
"./lib/uslug": {
"require": "./lib/uslug.js",
"types": "./lib/uslug.ts"
}
},
"engines": {
"node": ">= 0.4.0"
"node": ">= 10.0.0"
},
"bugs": {
"url": "http://github.com/jeremys/uslug/issues"

View File

@ -1,44 +0,0 @@
var should = require('should'),
uslug = require('../lib/uslug');
var word0 = 'Ελληνικά';
var word1 = [word0, word0].join('-');
var word2 = [word0, word0].join(' - ');
var tests = [
['', ''],
['The \u212B symbol invented by A. J. \u00C5ngstr\u00F6m (1814, L\u00F6gd\u00F6, \u2013 1874) denotes the length 10\u207B\u00B9\u2070 m.', 'the-å-symbol-invented-by-a-j-ångström-1814-lögdö-1874-denotes-the-length-1010-m'],
['Быстрее и лучше!', 'быстрее-и-лучше'],
['xx x - "#$@ x', 'xx-x-x'],
['Bän...g (bang)', 'bäng-bang'],
[word0, word0.toLowerCase()],
[word1, word1.toLowerCase()],
[word2, word1.toLowerCase()],
[' a ', 'a'],
['tags/', 'tags'],
['y_u_no', 'y_u_no'],
['el-ni\xf1o', 'el-ni\xf1o'],
['x荿', 'x荿'],
['ϧ΃蒬蓣', '\u03e7蒬蓣'],
['¿x', 'x'],
['汉语/漢語', '汉语漢語'],
['فار,سي', 'فارسي'],
['เแโ|ใไ', 'เแโใไ'],
['日本語ドキュメンテ(ーション)', '日本語ドキュメンテーション'],
['一二三四五六七八九十!。。。', '一二三四五六七八九十'],
['संसद में काम नहीं तो वेतन क्यों?', 'संसद-में-काम-नहीं-तो-वेतन-क्यों'],
['เร่งรัด \'ปรับเงินเดือนท้องถิ่น 1 ขั้น\' ตามมติ ครม.', 'เร่งรัด-ปรับเงินเดือนท้องถิ่น-1-ขั้น-ตามมติ-ครม'],
['オバマ大統領が病院爆撃の調査へ同意するように、協力してください!', 'オバマ大統領が病院爆撃の調査へ同意するように-協力してください'],
['일본정부 법무대신(法務大臣): 우리는 일본 입관법의 재검토를 요구한다!', '일본정부-법무대신法務大臣-우리는-일본-입관법의-재검토를-요구한다'],
['😁', 'grin'],
['😁a', 'grina'],
['🐶🐶🐶🐱', 'dogdogdogcat'],
];
for (var t in tests) {
var test = tests[t];
uslug(test[0]).should.equal(test[1]);
}
uslug('qbc,fe', { allowedChars: 'q' }).should.equal('qbcfe');

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"include": [
"**/*.ts"
],
"compilerOptions": {
"types": ["jest", "node"]
}
}

View File

@ -1727,6 +1727,17 @@ const builtInMetadata = (Setting: typeof SettingType) => {
isGlobal: true,
},
'featureFlag.richText.useStrictContentSecurityPolicy': {
value: true,
type: SettingItemType.Bool,
public: true,
storage: SettingStorage.File,
label: () => 'Stronger security controls in the Rich Text Editor',
description: () => 'Improves Rich Text Editor security by applying a strict content security policy to the Rich Text Editor\'s content.',
section: 'note',
isGlobal: true,
},
'sync.allowUnsupportedProviders': {
value: -1,
type: SettingItemType.Int,

View File

@ -45,7 +45,7 @@
"@aws-sdk/s3-request-presigner": "3.296.0",
"@joplin/fork-htmlparser2": "^4.1.57",
"@joplin/fork-sax": "^1.2.61",
"@joplin/fork-uslug": "^1.0.22",
"@joplin/fork-uslug": "^2.0.0",
"@joplin/htmlpack": "~3.3",
"@joplin/onenote-converter": "~3.3",
"@joplin/renderer": "~3.3",

View File

@ -80,65 +80,71 @@ export default class InteropService_Importer_Raw extends InteropService_Importer
for (let i = 0; i < stats.length; i++) {
const stat = stats[i];
if (stat.isDirectory()) continue;
if (fileExtension(stat.path).toLowerCase() !== 'md') continue;
const content = await shim.fsDriver().readFile(`${this.sourcePath_}/${stat.path}`);
const item = await BaseItem.unserialize(content);
const itemType = item.type_;
const ItemClass = BaseItem.itemClass(item);
try {
if (stat.isDirectory()) continue;
if (fileExtension(stat.path).toLowerCase() !== 'md') continue;
delete item.type_;
const content = await shim.fsDriver().readFile(`${this.sourcePath_}/${stat.path}`);
const item = await BaseItem.unserialize(content);
const itemType = item.type_;
const ItemClass = BaseItem.itemClass(item);
if (itemType === BaseModel.TYPE_NOTE) {
await setFolderToImportTo(item.parent_id);
delete item.type_;
if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
item.id = itemIdMap[item.id];
item.parent_id = itemIdMap[item.parent_id];
item.body = await replaceLinkedItemIds(item.body);
} else if (itemType === BaseModel.TYPE_FOLDER) {
if (destinationFolderId) continue;
if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
item.id = itemIdMap[item.id];
if (item.parent_id) {
if (itemType === BaseModel.TYPE_NOTE) {
await setFolderToImportTo(item.parent_id);
if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
item.id = itemIdMap[item.id];
item.parent_id = itemIdMap[item.parent_id];
}
item.body = await replaceLinkedItemIds(item.body);
} else if (itemType === BaseModel.TYPE_FOLDER) {
if (destinationFolderId) continue;
item.title = await Folder.findUniqueItemTitle(item.title, item.parent_id);
} else if (itemType === BaseModel.TYPE_RESOURCE) {
const sourceId = item.id;
if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
item.id = itemIdMap[item.id];
createdResources[item.id] = item;
if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
item.id = itemIdMap[item.id];
const sourceResourcePath = `${this.sourcePath_}/resources/${Resource.filename({ ...item, id: sourceId })}`;
const destPath = Resource.fullPath(item);
if (item.parent_id) {
await setFolderToImportTo(item.parent_id);
item.parent_id = itemIdMap[item.parent_id];
}
if (await shim.fsDriver().exists(sourceResourcePath)) {
await shim.fsDriver().copy(sourceResourcePath, destPath);
} else {
result.warnings.push(sprintf('Could not find resource file: %s', sourceResourcePath));
}
} else if (itemType === BaseModel.TYPE_TAG) {
const tag = await Tag.loadByTitle(item.title);
if (tag) {
itemIdMap[item.id] = tag.id;
item.title = await Folder.findUniqueItemTitle(item.title, item.parent_id);
} else if (itemType === BaseModel.TYPE_RESOURCE) {
const sourceId = item.id;
if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
item.id = itemIdMap[item.id];
createdResources[item.id] = item;
const sourceResourcePath = `${this.sourcePath_}/resources/${Resource.filename({ ...item, id: sourceId })}`;
const destPath = Resource.fullPath(item);
if (await shim.fsDriver().exists(sourceResourcePath)) {
await shim.fsDriver().copy(sourceResourcePath, destPath);
} else {
result.warnings.push(sprintf('Could not find resource file: %s', sourceResourcePath));
}
} else if (itemType === BaseModel.TYPE_TAG) {
const tag = await Tag.loadByTitle(item.title);
if (tag) {
itemIdMap[item.id] = tag.id;
continue;
}
const tagId = uuid.create();
itemIdMap[item.id] = tagId;
item.id = tagId;
} else if (itemType === BaseModel.TYPE_NOTE_TAG) {
noteTagsToCreate.push(item);
continue;
}
const tagId = uuid.create();
itemIdMap[item.id] = tagId;
item.id = tagId;
} else if (itemType === BaseModel.TYPE_NOTE_TAG) {
noteTagsToCreate.push(item);
continue;
await ItemClass.save(item, { isNew: true, autoTimestamp: false });
} catch (error) {
error.message = `Could not import: ${stat.path}: ${error.message}`;
throw error;
}
await ItemClass.save(item, { isNew: true, autoTimestamp: false });
}
for (let i = 0; i < noteTagsToCreate.length; i++) {

View File

@ -18,7 +18,7 @@ import Plugin from '../Plugin';
* now, are not well documented. You can find the list directly on GitHub
* though at the following locations:
*
* * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/MainScreen/commands)
* * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/WindowCommandsAndDialogs/commands)
* * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/commands)
* * [Editor commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts)
*
@ -29,8 +29,13 @@ import Plugin from '../Plugin';
* commands can be found in these places:
*
* * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-mobile/commands)
* * [Note screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-mobile/components/screens/Note/commands)
* * [Editor commands](https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/components/NoteEditor/commandDeclarations.ts)
*
* Additionally, certain global commands have the same implementation on both platforms:
*
* * [Shared global commands](https://github.com/laurent22/joplin/tree/dev/packages/lib/commands)
*
* ## Executing editor commands
*
* There might be a situation where you want to invoke editor commands

View File

@ -152,14 +152,25 @@ const processStartFlags = async (argv: string[], setDefaults = true) => {
continue;
}
if (arg.indexOf('--enable-wayland-ime') === 0) {
if (
arg === '--enable-wayland-ime'
|| arg === '--disable-gtk-ime'
|| arg.startsWith('--wayland-text-input-version=')
) {
// Electron-specific flag - ignore it
// Enables input method support on Linux/Wayland
// Enables/configures input method support on Linux/Wayland
// See https://github.com/laurent22/joplin/issues/10345
argv.splice(0, 1);
continue;
}
if (arg.startsWith('--gtk-version=')) {
// Electron-specific flag. Allows forcing a different GTK version.
// See https://wiki.archlinux.org/title/Chromium#Native_Wayland_support
argv.splice(0, 1);
continue;
}
if (arg.indexOf('--ozone-platform=') === 0) {
// Electron-specific flag - ignore it
// Allows users to run the app on native wayland

View File

@ -399,9 +399,10 @@ export default class MdToHtml implements MarkupRenderer {
public async allAssets(theme: any, noteStyleOptions: NoteStyleOptions = null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const assets: any = {};
for (const key in rules) {
const allRules = { ...rules, ...this.extraRendererRules_ };
for (const key in allRules) {
if (!this.pluginEnabled(key)) continue;
const rule = rules[key];
const rule = allRules[key];
if (rule.assets) {
assets[key] = rule.assets(theme);

View File

@ -98,17 +98,6 @@ export default {
const exportGraphButton = (ruleOptions: RuleOptions) => {
const theme = ruleOptions.theme;
// Clicking on export button manually triggers a right click context menu event
const onClickHandler = `
const target = arguments[0].target;
const button = target.closest("div.mermaid-export-graph");
if (!button) return false;
const $mermaid_elem = button.nextElementSibling;
const rightClickEvent = new PointerEvent("contextmenu", {bubbles: true});
rightClickEvent.target = $mermaid_elem;
$mermaid_elem.dispatchEvent(rightClickEvent);
return false;
`.trim();
const style = `
display: block;
margin-left: auto;
@ -119,9 +108,10 @@ const exportGraphButton = (ruleOptions: RuleOptions) => {
border: ${theme.buttonStyle.border};
`.trim();
// OnClick is handled in the renderer script
return `
<div class="mermaid-export-graph">
<button onclick='${onClickHandler}' style="${style}" alt="Export mermaid graph">${downloadIcon()}</button>
<button style="${style}" alt="Export mermaid graph">${downloadIcon()}</button>
</div>
`;
};

View File

@ -22,6 +22,20 @@ const isDarkMode = () => {
return !!document.querySelector('.mermaid.joplin--mermaid-use-dark-theme');
};
const initExportButtons = () => {
const exportButtons = document.querySelectorAll('.mermaid-export-graph > button');
for (const button of exportButtons) {
button.onclick = () => {
const buttonContainer = button.parentElement;
const mermaidElem = buttonContainer.nextElementSibling;
const rightClickEvent = new PointerEvent('contextmenu', { bubbles: true });
rightClickEvent.target = mermaidElem;
mermaidElem.dispatchEvent(rightClickEvent);
};
}
};
function mermaidInit() {
if (mermaidReady()) {
const mermaidTargetNodes = document.getElementsByClassName('mermaid');
@ -46,6 +60,8 @@ function mermaidInit() {
for (const element of mermaidTargetNodes) {
element.style.width = '100%';
}
initExportButtons();
}
}

View File

@ -22,6 +22,20 @@ const isDarkMode = () => {
return !!document.querySelector('.mermaid.joplin--mermaid-use-dark-theme');
};
const initExportButtons = () => {
const exportButtons = document.querySelectorAll('.mermaid-export-graph > button');
for (const button of exportButtons) {
button.onclick = () => {
const buttonContainer = button.parentElement;
const mermaidElem = buttonContainer.nextElementSibling;
const rightClickEvent = new PointerEvent('contextmenu', {bubbles: true});
rightClickEvent.target = mermaidElem;
mermaidElem.dispatchEvent(rightClickEvent);
};
}
};
function mermaidInit() {
if (mermaidReady()) {
const mermaidTargetNodes = document.getElementsByClassName('mermaid');
@ -46,6 +60,8 @@ function mermaidInit() {
for (const element of mermaidTargetNodes) {
element.style.width = '100%';
}
initExportButtons();
}
}

View File

@ -31,7 +31,7 @@
},
"dependencies": {
"@joplin/fork-htmlparser2": "^4.1.57",
"@joplin/fork-uslug": "^1.0.22",
"@joplin/fork-uslug": "^2.0.0",
"@joplin/utils": "~3.3",
"font-awesome-filetypes": "2.1.0",
"fs-extra": "11.2.0",

View File

@ -8516,6 +8516,7 @@ __metadata:
"@codemirror/search": 6.5.8
"@codemirror/state": 6.4.1
"@codemirror/view": 6.35.0
"@joplin/fork-uslug": ^2.0.0
"@joplin/lib": ~3.3
"@lezer/common": 1.2.3
"@lezer/highlight": 1.2.1
@ -8563,13 +8564,15 @@ __metadata:
languageName: unknown
linkType: soft
"@joplin/fork-uslug@^1.0.22, @joplin/fork-uslug@workspace:packages/fork-uslug":
"@joplin/fork-uslug@^2.0.0, @joplin/fork-uslug@workspace:packages/fork-uslug":
version: 0.0.0-use.local
resolution: "@joplin/fork-uslug@workspace:packages/fork-uslug"
dependencies:
"@types/jest": 29.5.12
"@types/node": 18.19.67
jest: 29.7.0
node-emoji: 1.11.0
should: 13.2.3
unorm: 1.6.0
typescript: 5.4.5
languageName: unknown
linkType: soft
@ -8595,7 +8598,7 @@ __metadata:
"@aws-sdk/s3-request-presigner": 3.296.0
"@joplin/fork-htmlparser2": ^4.1.57
"@joplin/fork-sax": ^1.2.61
"@joplin/fork-uslug": ^1.0.22
"@joplin/fork-uslug": ^2.0.0
"@joplin/htmlpack": ~3.3
"@joplin/onenote-converter": ~3.3
"@joplin/renderer": ~3.3
@ -8778,7 +8781,7 @@ __metadata:
resolution: "@joplin/renderer@workspace:packages/renderer"
dependencies:
"@joplin/fork-htmlparser2": ^4.1.57
"@joplin/fork-uslug": ^1.0.22
"@joplin/fork-uslug": ^2.0.0
"@joplin/utils": ~3.3
"@types/jest": 29.5.12
"@types/markdown-it": 13.0.9
@ -43071,62 +43074,6 @@ __metadata:
languageName: node
linkType: hard
"should-equal@npm:^2.0.0":
version: 2.0.0
resolution: "should-equal@npm:2.0.0"
dependencies:
should-type: ^1.4.0
checksum: 3f3580a223bf76f9309a4d957d2dcbd6059bda816f2e6656e822b7518218ef653c25e9271b2f5765ca6f5a72a217105ad343a8ceea831d15aff44dd691cc1dcd
languageName: node
linkType: hard
"should-format@npm:^3.0.3":
version: 3.0.3
resolution: "should-format@npm:3.0.3"
dependencies:
should-type: ^1.3.0
should-type-adaptors: ^1.0.1
checksum: 5304e89b4d4c42078c7f66232d13cca1d6a1c00c173f500f64160f57d4ecd7522a25106b313fe8f8694547e8a1ce4d975f1f09a3d1618f1dc054db48c0683d87
languageName: node
linkType: hard
"should-type-adaptors@npm:^1.0.1":
version: 1.1.0
resolution: "should-type-adaptors@npm:1.1.0"
dependencies:
should-type: ^1.3.0
should-util: ^1.0.0
checksum: 94dd1d225c8f2590278f46689258a1df684ca1f26262459c4e2d64a09d06935ec1410a24fe7b5f98b9429093e48afef2ed1b370634e0444b930547df4943f70d
languageName: node
linkType: hard
"should-type@npm:^1.3.0, should-type@npm:^1.4.0":
version: 1.4.0
resolution: "should-type@npm:1.4.0"
checksum: 88d9324c6c0c2f94e71d2f8b11c84e44de81f16eeb6fafcba47f4af430c65e46bad18eb472827526cad22b4fe693aba8b022739d1c453672faf28860df223491
languageName: node
linkType: hard
"should-util@npm:^1.0.0":
version: 1.0.1
resolution: "should-util@npm:1.0.1"
checksum: c3be15e0fdc851f8338676b3f8b590d330bbea94ec41c1343cc9983dea295915073f69a215795454b6adda6579ec8927c7c0ab178b83f9f11a0247ccdba53381
languageName: node
linkType: hard
"should@npm:13.2.3":
version: 13.2.3
resolution: "should@npm:13.2.3"
dependencies:
should-equal: ^2.0.0
should-format: ^3.0.3
should-type: ^1.4.0
should-type-adaptors: ^1.0.1
should-util: ^1.0.0
checksum: 74bcc0eb85e0a63a88e501ff9ca3b53dbc6d1ee47823c029a18a4b14b3ef4e2561733e161033df720599d2153283470e9647fdcb1bbc78903960ffb0363239c4
languageName: node
linkType: hard
"side-channel@npm:^1.0.4":
version: 1.0.4
resolution: "side-channel@npm:1.0.4"
@ -47493,13 +47440,6 @@ __metadata:
languageName: node
linkType: hard
"unorm@npm:1.6.0":
version: 1.6.0
resolution: "unorm@npm:1.6.0"
checksum: 9a86546256a45f855b6cfe719086785d6aada94f63778cecdecece8d814ac26af76cb6da70130da0a08b8803bbf0986e56c7ec4249038198f3de02607fffd811
languageName: node
linkType: hard
"unpack-string@npm:0.0.2":
version: 0.0.2
resolution: "unpack-string@npm:0.0.2"