feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,8 @@
|
||||
import pressReleaseKey from './pressReleaseKey';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
|
||||
const backspaceOnce = (view: EditorView) => {
|
||||
pressReleaseKey(view, { key: 'Backspace', code: 'Backspace', typesText: '' });
|
||||
};
|
||||
|
||||
export default backspaceOnce;
|
||||
@@ -0,0 +1,20 @@
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import createEditor from '../createEditor';
|
||||
import createEditorSettings from '../../testing/createEditorSettings';
|
||||
|
||||
const createEditorControl = (initialText: string) => {
|
||||
const editorSettings = createEditorSettings(Setting.THEME_LIGHT);
|
||||
|
||||
return createEditor(document.body, {
|
||||
initialText,
|
||||
initialNoteId: '',
|
||||
settings: editorSettings,
|
||||
onEvent: _event => {},
|
||||
onLogMessage: _message => {},
|
||||
onPasteFile: null,
|
||||
resolveImageSrc: (src)=>Promise.resolve(src),
|
||||
onLocalize: input=>input,
|
||||
});
|
||||
};
|
||||
|
||||
export default createEditorControl;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown';
|
||||
import { forceParsing, indentUnit, syntaxTree } from '@codemirror/language';
|
||||
import { SelectionRange, EditorSelection, EditorState, Extension } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import forceFullParse from './forceFullParse';
|
||||
import loadLanguages from './loadLanguages';
|
||||
import markdownMathExtension from '../extensions/markdownMathExtension';
|
||||
import markdownHighlightExtension, { markdownInsertExtension } from '../extensions/markdownHighlightExtension';
|
||||
import markdownFrontMatterExtension from '../extensions/markdownFrontMatterExtension';
|
||||
|
||||
// Creates and returns a minimal editor with markdown extensions. Waits to return the editor
|
||||
// until all syntax tree tags in `expectedSyntaxTreeTags` exist.
|
||||
const createTestEditor = async (
|
||||
initialText: string,
|
||||
initialSelection: SelectionRange|SelectionRange[],
|
||||
expectedSyntaxTreeTags: string[],
|
||||
extraExtensions: Extension[] = [],
|
||||
addMarkdownKeymap = true,
|
||||
): Promise<EditorView> => {
|
||||
await loadLanguages();
|
||||
|
||||
initialSelection = Array.isArray(initialSelection) ? initialSelection : [initialSelection];
|
||||
|
||||
const editor = new EditorView({
|
||||
doc: initialText,
|
||||
selection: EditorSelection.create(initialSelection),
|
||||
extensions: [
|
||||
markdown({
|
||||
extensions: [markdownMathExtension, markdownHighlightExtension, markdownInsertExtension, markdownFrontMatterExtension, GithubFlavoredMarkdownExt],
|
||||
addKeymap: addMarkdownKeymap,
|
||||
}),
|
||||
indentUnit.of('\t'),
|
||||
EditorState.tabSize.of(4),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
extraExtensions,
|
||||
],
|
||||
});
|
||||
|
||||
let sawExpectedTagCount = 0;
|
||||
while (sawExpectedTagCount < expectedSyntaxTreeTags.length) {
|
||||
forceFullParse(editor.state);
|
||||
|
||||
sawExpectedTagCount = 0;
|
||||
const seenTags = new Set<string>();
|
||||
|
||||
syntaxTree(editor.state).iterate({
|
||||
from: 0,
|
||||
to: editor.state.doc.length,
|
||||
enter: (node) => {
|
||||
for (const expectedTag of expectedSyntaxTreeTags) {
|
||||
if (node.name === expectedTag) {
|
||||
seenTags.add(node.name);
|
||||
sawExpectedTagCount ++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (sawExpectedTagCount < expectedSyntaxTreeTags.length) {
|
||||
// const missingTags = expectedSyntaxTreeTags.filter(tagName => {
|
||||
// return !seenTags.has(tagName);
|
||||
// });
|
||||
// console.warn(`Didn't find all expected tags. Missing ${missingTags}. Retrying...`);
|
||||
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// forceFullParse populates the parse cache for editor.state, but the view keeps its own
|
||||
// tree that the background parser advances lazily. Extensions that build decorations from
|
||||
// syntaxTree(view.state) (e.g. makeInlineReplaceExtension) can then run against an incomplete
|
||||
// tree and skip their decorations, so force the view's own tree to complete before returning.
|
||||
forceParsing(editor, editor.state.doc.length);
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
||||
export default createTestEditor;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { SyntaxNode } from '@lezer/common';
|
||||
|
||||
// Returns a list of all nodes with the given name in the given editor's syntax tree.
|
||||
// Attempts to create the syntax tree if it doesn't exist.
|
||||
const findNodesWithName = (editor: EditorState, nodeName: string) => {
|
||||
const result: SyntaxNode[] = [];
|
||||
syntaxTree(editor).iterate({
|
||||
enter: (node) => {
|
||||
if (node.name === nodeName) {
|
||||
result.push(node.node);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default findNodesWithName;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ensureSyntaxTree, syntaxTreeAvailable } from '@codemirror/language';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
|
||||
// Forces a full parse of a CodeMirror editor. This is intended for unit testing.
|
||||
// If not in a unit-test consider using ensureSyntaxTree or forceParsing.
|
||||
// This will throw if no language is configured for the editor.
|
||||
const forceFullParse = (editorState: EditorState) => {
|
||||
const timeout = 3000; // ms
|
||||
ensureSyntaxTree(editorState, editorState.doc.length, timeout);
|
||||
|
||||
if (!syntaxTreeAvailable(editorState)) {
|
||||
throw new Error(
|
||||
`Unable to generate a syntax tree in ${timeout}. Is the editor configured to parse a language?`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default forceFullParse;
|
||||
@@ -0,0 +1,10 @@
|
||||
import allLanguages from '../utils/markdown/codeBlockLanguages/allLanguages';
|
||||
|
||||
// Ensure languages we use are loaded. Without this, tests may randomly fail (LanguageDescriptions
|
||||
// are loaded asynchronously, in the background).
|
||||
const loadLanguages = async () => {
|
||||
for (const lang of allLanguages) {
|
||||
await lang.load();
|
||||
}
|
||||
};
|
||||
export default loadLanguages;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import typeText from './typeText';
|
||||
|
||||
export interface KeyInfo {
|
||||
key: string;
|
||||
code: string;
|
||||
// Text to type if the event was not processed
|
||||
typesText?: string;
|
||||
ctrlKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
shiftKey?: boolean;
|
||||
}
|
||||
|
||||
const pressReleaseKey = (editor: EditorView, key: KeyInfo) => {
|
||||
const keyDownEvent = new KeyboardEvent('keydown', key);
|
||||
|
||||
let keyDownPrevented = false;
|
||||
keyDownEvent.preventDefault = () => {
|
||||
keyDownPrevented = true;
|
||||
};
|
||||
|
||||
editor.contentDOM.dispatchEvent(keyDownEvent);
|
||||
|
||||
if (key.typesText !== undefined && !keyDownPrevented) {
|
||||
typeText(editor, key.typesText, key);
|
||||
}
|
||||
|
||||
editor.contentDOM.dispatchEvent(new KeyboardEvent('keyup', key));
|
||||
};
|
||||
|
||||
export default pressReleaseKey;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { KeyInfo } from './pressReleaseKey';
|
||||
|
||||
const typeText = (editor: EditorView, text: string, key?: KeyInfo) => {
|
||||
const selection = editor.state.selection;
|
||||
const inputHandlers = editor.state.facet(EditorView.inputHandler);
|
||||
const isBackspace = !!key && key.key === 'Backspace' && text === '';
|
||||
|
||||
let backspaceOffset = 0;
|
||||
if (isBackspace && selection.main.empty && selection.main.from > 0) {
|
||||
backspaceOffset = -1;
|
||||
}
|
||||
|
||||
// See how the upstream CodeMirror tests simulate user input:
|
||||
// https://github.com/codemirror/autocomplete/blob/fb1c899464df4d36528331412cdd316548134cb2/test/webtest-autocomplete.ts#L116
|
||||
// The important part is the userEvent: input.type.
|
||||
const defaultTransaction = editor.state.update({
|
||||
changes: [{
|
||||
from: selection.main.from + backspaceOffset,
|
||||
to: selection.main.to,
|
||||
insert: text,
|
||||
}],
|
||||
selection: { anchor: selection.main.head + text.length + backspaceOffset },
|
||||
userEvent: 'input.type',
|
||||
});
|
||||
|
||||
// Allows code that uses input handlers to be tested
|
||||
for (const handler of inputHandlers) {
|
||||
if (handler(editor, selection.main.from, selection.main.to, text, () => defaultTransaction)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
editor.dispatch(defaultTransaction);
|
||||
};
|
||||
|
||||
export default typeText;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
|
||||
const visibleEditorText = (view: EditorView) => {
|
||||
const dom = view.contentDOM;
|
||||
const clone = dom.cloneNode(true);
|
||||
if (!(clone instanceof HTMLElement)) throw new Error('Assertion: Should clone to an HTML element');
|
||||
|
||||
for (const visuallyHidden of clone.querySelectorAll('.cm-hidden')) {
|
||||
visuallyHidden.remove();
|
||||
}
|
||||
|
||||
return clone.textContent;
|
||||
};
|
||||
|
||||
export default visibleEditorText;
|
||||
在新工单中引用
屏蔽一个用户