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,209 @@
|
||||
import { buildInputRules } from 'prosemirror-example-setup';
|
||||
import schema from '../schema';
|
||||
import { MarkType, ResolvedPos } from 'prosemirror-model';
|
||||
import { EditorState, Plugin, Transaction } from 'prosemirror-state';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import { closeHistory } from 'prosemirror-history';
|
||||
import showCreateEditablePrompt from './joplinEditablePlugin/showCreateEditablePrompt';
|
||||
|
||||
|
||||
interface InputRuleData {
|
||||
match: RegExp;
|
||||
commitCharacter: string|null;
|
||||
handler: (view: EditorView, match: RegExpMatchArray, start: number, end: number, commitCharacter: string)=> Transaction|null;
|
||||
}
|
||||
|
||||
// A custom input rule extension for inline input replacements.
|
||||
//
|
||||
// Ref: https://github.com/ProseMirror/prosemirror-inputrules/blob/43ef04ce9c1512ef8f2289578309c40b431ed3c5/src/inputrules.ts#L82
|
||||
// See https://discuss.prosemirror.net/t/trigger-inputrule-on-enter/1118 for why this approach is needed.
|
||||
const inlineInputRules = (rules: InputRuleData[], commitCharacterExpression: RegExp) => {
|
||||
const getContentBeforeCursor = (cursorInformation: ResolvedPos) => {
|
||||
const parent = cursorInformation.parent;
|
||||
const offsetInParent = cursorInformation.parentOffset;
|
||||
const maxLength = 256;
|
||||
return parent.textBetween(Math.max(offsetInParent - maxLength, 0), offsetInParent);
|
||||
};
|
||||
|
||||
const getApplicableRule = (state: EditorState, cursor: number, justTypedText: string) => {
|
||||
const candidateRules = rules.filter(rule => justTypedText.endsWith(rule.commitCharacter ?? ''));
|
||||
if (!candidateRules.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cursorInformation = state.doc.resolve(cursor);
|
||||
const inCode = cursorInformation.parent.type.spec.code;
|
||||
if (inCode) {
|
||||
return false;
|
||||
}
|
||||
const beforeCursor = getContentBeforeCursor(cursorInformation) + justTypedText;
|
||||
|
||||
for (const rule of candidateRules) {
|
||||
const match = beforeCursor.match(rule.match);
|
||||
if (!match) continue;
|
||||
|
||||
return rule;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
type PluginState = { pendingRule: InputRuleData };
|
||||
const run = (view: EditorView, cursor: number, commitData: string) => {
|
||||
const commitCharacter = commitCharacterExpression.exec(commitData) ? commitData : '';
|
||||
|
||||
// Commit on either a commit character or when the user presses "enter".
|
||||
if (!commitCharacter && commitData !== 'Enter') return false;
|
||||
|
||||
const availableRule = plugin.getState(view.state)?.pendingRule;
|
||||
if (!availableRule) return false;
|
||||
|
||||
const beforeCursor = getContentBeforeCursor(view.state.doc.resolve(cursor));
|
||||
const match = beforeCursor.match(availableRule.match);
|
||||
if (match) {
|
||||
const transaction = availableRule.handler(view, match, cursor - match[0].length, cursor, commitCharacter);
|
||||
if (transaction) {
|
||||
// closeHistory: Move the markup completion to a separate history event so that it
|
||||
// can be undone separately.
|
||||
view.dispatch(closeHistory(transaction));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const plugin = new Plugin<PluginState|null>({
|
||||
state: {
|
||||
init: () => null,
|
||||
apply: (tr, lastValue) => {
|
||||
const pendingRule = tr.getMeta(plugin);
|
||||
if (pendingRule) return pendingRule;
|
||||
if (tr.docChanged || tr.selectionSet) return null;
|
||||
return lastValue;
|
||||
},
|
||||
},
|
||||
props: {
|
||||
handleTextInput(view, _from, to, text, defaultAction) {
|
||||
const proposedRule = getApplicableRule(view.state, to, text);
|
||||
if (proposedRule) {
|
||||
const transaction = defaultAction().setMeta(plugin, { pendingRule: proposedRule });
|
||||
view.dispatch(transaction);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handleDOMEvents: {
|
||||
compositionend: (view, event) => {
|
||||
if (view.state.selection.empty) {
|
||||
return run(view, view.state.selection.to, event.data);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
handleKeyDown: (view, event) => {
|
||||
return run(view, view.state.selection.to, event.key);
|
||||
},
|
||||
},
|
||||
// TODO: Uncomment this? Doing so allows the undoInputRule command to undo this input rule.
|
||||
// However, it will require storing additional information in the plugin state (e.g. last transaction,
|
||||
// start, end position) to be compatible with the upstream input rule extension.
|
||||
// isInputRules: true,
|
||||
});
|
||||
return plugin;
|
||||
};
|
||||
|
||||
type OnReplace = (matches: RegExpMatchArray, view: EditorView)=> string;
|
||||
interface InputRuleOptions {
|
||||
contentRegex: string|RegExp;
|
||||
commitCharacter: string|null; // null => only "Enter"
|
||||
onReplace: OnReplace;
|
||||
marks: MarkType[];
|
||||
}
|
||||
|
||||
const makeInputRule = ({
|
||||
contentRegex, commitCharacter, onReplace, marks,
|
||||
}: InputRuleOptions): InputRuleData => {
|
||||
const commitCharacterExp = '[.?!,:;¡¿() \\n]';
|
||||
const regex = typeof contentRegex === 'string' ? new RegExp(`(^|${commitCharacterExp})${contentRegex}$`) : contentRegex;
|
||||
return {
|
||||
match: regex,
|
||||
commitCharacter,
|
||||
handler: (view, match, start, end, endCommitCharacter) => {
|
||||
const state = view.state;
|
||||
let transaction = state.tr.delete(start, end);
|
||||
|
||||
const startCommitCharacter = match[1];
|
||||
|
||||
// Remove the commit-character-related matches before forwarding the input
|
||||
// to the replacement function.
|
||||
const matchesWithoutCommitCharacters: RegExpMatchArray = [
|
||||
match[0].substring(startCommitCharacter.length, match[0].length),
|
||||
...match.slice(2, match.length),
|
||||
];
|
||||
matchesWithoutCommitCharacters.groups = match.groups;
|
||||
|
||||
const replacement = onReplace(matchesWithoutCommitCharacters, view);
|
||||
transaction = transaction.insert(
|
||||
transaction.mapping.map(start, -1),
|
||||
[
|
||||
!!startCommitCharacter && schema.text(startCommitCharacter),
|
||||
!!replacement && schema.text(replacement, marks.map(type => schema.mark(type))),
|
||||
!!endCommitCharacter && schema.text(endCommitCharacter),
|
||||
].filter(node => !!node),
|
||||
);
|
||||
|
||||
return transaction;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const baseInputRules = buildInputRules(schema);
|
||||
const inlineContentExp = '\\S[^\\n]*\\S|\\S';
|
||||
const noMatchRegex = /$^/;
|
||||
const inputRulesExtension = [
|
||||
baseInputRules,
|
||||
inlineInputRules([
|
||||
makeInputRule({
|
||||
contentRegex: /(^|[\n])(```+)(\w*)$/,
|
||||
commitCharacter: '',
|
||||
onReplace: (match, view) => {
|
||||
const blockStart = `${match[1]}${match[2]}\n`;
|
||||
const block = `${blockStart}\n${match[1]}`;
|
||||
showCreateEditablePrompt({
|
||||
source: block,
|
||||
inline: false,
|
||||
cursor: blockStart.length,
|
||||
})(view.state, view.dispatch, view);
|
||||
return '';
|
||||
},
|
||||
marks: [],
|
||||
}),
|
||||
], noMatchRegex),
|
||||
inlineInputRules([
|
||||
makeInputRule({
|
||||
contentRegex: `\\*\\*(${inlineContentExp})\\*\\*`,
|
||||
commitCharacter: '*',
|
||||
onReplace: (match) => match[1],
|
||||
marks: [schema.marks.strong],
|
||||
}),
|
||||
makeInputRule({
|
||||
contentRegex: `\\*(${inlineContentExp})\\*`,
|
||||
commitCharacter: '*',
|
||||
onReplace: (match) => match[1],
|
||||
marks: [schema.marks.emphasis],
|
||||
}),
|
||||
makeInputRule({
|
||||
contentRegex: `_(${inlineContentExp})_`,
|
||||
commitCharacter: '_',
|
||||
onReplace: (match) => match[1],
|
||||
marks: [schema.marks.emphasis],
|
||||
}),
|
||||
makeInputRule({
|
||||
contentRegex: `\`(${inlineContentExp})\``,
|
||||
commitCharacter: '`',
|
||||
onReplace: (match) => match[1],
|
||||
marks: [schema.marks.code],
|
||||
}),
|
||||
], /[ .,?)!;]/),
|
||||
];
|
||||
export default inputRulesExtension;
|
||||
在新工单中引用
屏蔽一个用户