305 lines
12 KiB
JavaScript
305 lines
12 KiB
JavaScript
// MNote 文件树上下文菜单运行时外置模块。
|
|
// 当前先承接 CommandContext 构造与 when 表达式求值,树语义仍由 Rust/kernel 主导。
|
|
|
|
function commandContextResourceKindForRow(row) {
|
|
if (!(row instanceof HTMLElement)) return 'unknown';
|
|
var rowKind = String(row.getAttribute('data-row-kind') || '').trim();
|
|
var assetType = String(row.getAttribute('data-asset-type') || '').trim();
|
|
var assetId = String(row.getAttribute('data-asset-id') || '').trim();
|
|
if (rowKind === 'folder' || rowKind === 'directory' || rowKind === 'asset-folder') return 'folder';
|
|
if (rowKind === 'document' || rowKind === 'index' || rowKind === 'markdown') return 'page';
|
|
if (assetType) return assetType;
|
|
if (assetId) return 'asset';
|
|
return rowKind || 'unknown';
|
|
}
|
|
|
|
function commandContextDirtyState(value) {
|
|
var state = String(value || '').trim();
|
|
if (!state) return 'clean';
|
|
var lowered = state.toLowerCase();
|
|
if (lowered === 'dirty' || lowered === 'stale' || lowered === 'deleted') return lowered;
|
|
if (lowered === 'externalmodified' || lowered === 'external_modified' || lowered === 'external-change-conflict') return 'external_modified';
|
|
if (lowered === 'hasexternalconflict' || lowered === 'has_external_conflict') return 'has_external_conflict';
|
|
return lowered === 'clean' || lowered === 'saved' ? 'clean' : lowered;
|
|
}
|
|
|
|
function commandContextIsDirtyState(value) {
|
|
return commandContextDirtyState(value) !== 'clean';
|
|
}
|
|
|
|
function commandContextEditorMatchesRow(editor, row) {
|
|
if (!editor || !(row instanceof HTMLElement)) return false;
|
|
var rowDocumentId = String(row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '').trim();
|
|
if (rowDocumentId && String(editor.documentId || '').trim() === rowDocumentId) return true;
|
|
var rowAssetId = String(row.getAttribute('data-asset-id') || '').trim();
|
|
if (rowAssetId && String(editor.assetId || editor.workspacePath && editor.workspacePath.assetId || '').trim() === rowAssetId) return true;
|
|
var rowRelativePath = String(row.getAttribute('data-local-relative-path') || '').trim();
|
|
var editorRelativePath = String(editor.workspacePath && (editor.workspacePath.relativePath || editor.workspacePath.localRelativePath) || '').trim();
|
|
return Boolean(rowRelativePath && editorRelativePath && rowRelativePath === editorRelativePath);
|
|
}
|
|
|
|
function commandContextDirtyStateForRows(rows, openEditorsSnapshot, bufferState) {
|
|
if (bufferState && typeof bufferState === 'object') {
|
|
var directState = bufferState.dirtyState || bufferState.state || bufferState.status || '';
|
|
if (commandContextIsDirtyState(directState)) return commandContextDirtyState(directState);
|
|
}
|
|
var editors = [];
|
|
if (openEditorsSnapshot && Array.isArray(openEditorsSnapshot.editors)) editors = editors.concat(openEditorsSnapshot.editors);
|
|
if (openEditorsSnapshot && Array.isArray(openEditorsSnapshot.resourceEditors)) editors = editors.concat(openEditorsSnapshot.resourceEditors);
|
|
for (var r = 0; r < rows.length; r += 1) {
|
|
var row = rows[r];
|
|
for (var i = 0; i < editors.length; i += 1) {
|
|
var editor = editors[i];
|
|
if (!commandContextEditorMatchesRow(editor, row)) continue;
|
|
var editorState = editor.dirtyState || editor.dirtyGuard || '';
|
|
if (commandContextIsDirtyState(editorState)) return commandContextDirtyState(editorState);
|
|
}
|
|
}
|
|
return 'clean';
|
|
}
|
|
|
|
function selectedRowsFromDeps(deps, fallbackRow) {
|
|
var selection = deps && deps.selection ? deps.selection : {};
|
|
var rowIds = Array.isArray(selection.selectedRowIds)
|
|
? selection.selectedRowIds
|
|
: Array.from(selection.selectedRowIds || []);
|
|
var rows = [];
|
|
var queryRowById = deps && typeof deps.queryRowById === 'function'
|
|
? deps.queryRowById
|
|
: function() { return null; };
|
|
for (var i = 0; i < rowIds.length; i += 1) {
|
|
var row = queryRowById(rowIds[i]);
|
|
if (row instanceof HTMLElement) rows.push(row);
|
|
}
|
|
if (!rows.length && fallbackRow instanceof HTMLElement) rows.push(fallbackRow);
|
|
return rows;
|
|
}
|
|
|
|
function buildFileTreeCommandContext(row, openEditorsSnapshot, bufferState, deps) {
|
|
deps = deps || {};
|
|
var rows = selectedRowsFromDeps(deps, row);
|
|
var resourceKinds = {};
|
|
for (var j = 0; j < rows.length; j += 1) {
|
|
resourceKinds[commandContextResourceKindForRow(rows[j])] = true;
|
|
}
|
|
var rkKeys = Object.keys(resourceKinds);
|
|
var currentSourceKind = typeof deps.currentSourceKind === 'function'
|
|
? deps.currentSourceKind
|
|
: function() { return ''; };
|
|
var readonly = typeof deps.workspaceReadonly === 'function'
|
|
? deps.workspaceReadonly()
|
|
: false;
|
|
var dirtyState = commandContextDirtyStateForRows(rows, openEditorsSnapshot, bufferState);
|
|
var editorDirty = commandContextIsDirtyState(dirtyState);
|
|
var targetKind = commandContextResourceKindForRow(row);
|
|
return {
|
|
'workspace.sourceKind': currentSourceKind() || '',
|
|
'workspace.readonly': readonly === true,
|
|
'tree.focusKind': deps.focusKind || 'file_tree',
|
|
'tree.selectionCount': rows.length,
|
|
'tree.selectionResourceKind': rkKeys.length === 1 ? rkKeys[0] : rows.length ? 'mixed' : targetKind,
|
|
'tree.targetResourceKind': targetKind,
|
|
'tree.targetRowKind': row instanceof HTMLElement ? String(row.getAttribute('data-row-kind') || '').trim() : '',
|
|
'tree.targetIsFolder': targetKind === 'folder',
|
|
'tree.targetIsAsset': Boolean(row instanceof HTMLElement && String(row.getAttribute('data-asset-id') || '').trim()),
|
|
'editor.dirty': editorDirty,
|
|
'editor.dirtyState': dirtyState,
|
|
'editor.hasSelection': Boolean(deps.editorHasSelection),
|
|
'ai.canWrite': readonly !== true && !editorDirty,
|
|
};
|
|
}
|
|
|
|
function buildSidebarFileTreeContext(kind, deps) {
|
|
var targetRow = deps && deps.targetRow instanceof HTMLElement ? deps.targetRow : null;
|
|
return buildFileTreeCommandContext(targetRow, deps && deps.openEditorsSnapshot, deps && deps.bufferState, Object.assign({}, deps || {}, {
|
|
focusKind: kind === 'filetree' ? 'file_tree' : kind === 'page' ? 'page_tree' : kind,
|
|
}));
|
|
}
|
|
|
|
function buildLegacySidebarFileTreeContext(kind, deps) {
|
|
var selection = deps && deps.selection ? deps.selection : {};
|
|
var rowIds = Array.isArray(selection.selectedRowIds)
|
|
? selection.selectedRowIds
|
|
: Array.from(selection.selectedRowIds || []);
|
|
var rows = [];
|
|
var queryRowById = deps && typeof deps.queryRowById === 'function'
|
|
? deps.queryRowById
|
|
: function() { return null; };
|
|
for (var i = 0; i < rowIds.length; i += 1) {
|
|
var row = queryRowById(rowIds[i]);
|
|
if (row instanceof HTMLElement) rows.push(row);
|
|
}
|
|
var resourceKinds = {};
|
|
for (var j = 0; j < rows.length; j += 1) {
|
|
var rk = rows[j].getAttribute('data-row-kind') || 'unknown';
|
|
resourceKinds[rk] = true;
|
|
}
|
|
var rkKeys = Object.keys(resourceKinds);
|
|
var currentSourceKind = deps && typeof deps.currentSourceKind === 'function'
|
|
? deps.currentSourceKind
|
|
: function() { return ''; };
|
|
var readonly = deps && typeof deps.workspaceReadonly === 'function'
|
|
? deps.workspaceReadonly()
|
|
: false;
|
|
return {
|
|
'workspace.sourceKind': currentSourceKind() || '',
|
|
'workspace.readonly': readonly === true,
|
|
'tree.focusKind': kind === 'filetree' ? 'file_tree' : kind === 'page' ? 'page_tree' : kind,
|
|
'tree.selectionCount': rows.length,
|
|
'tree.selectionResourceKind': rkKeys.length === 1 ? rkKeys[0] : 'mixed'
|
|
};
|
|
}
|
|
|
|
function evaluateSidebarFileTreeWhen(ctx, expr) {
|
|
if (!expr) return true;
|
|
try {
|
|
expr = String(expr || '').trim();
|
|
var tokens = tokenizeWhenExpression(expr);
|
|
var pos = 0;
|
|
function peek() { return tokens[pos]; }
|
|
function consume() { return tokens[pos++]; }
|
|
function expect(t) {
|
|
var tok = consume();
|
|
if (!tok || tok.t !== t) throw new Error('Expected ' + t + ' got ' + (tok ? tok.t : 'EOF'));
|
|
return tok;
|
|
}
|
|
function ctxVal(key) {
|
|
var v = ctx[key];
|
|
if (v === undefined || v === null) return '';
|
|
return v;
|
|
}
|
|
function compareValue(cv, value, equal) {
|
|
if (value === 'true') {
|
|
var truth = cv === true || cv === 'true';
|
|
return equal ? truth : !truth;
|
|
}
|
|
if (value === 'false') {
|
|
var falsy = cv === false || cv === '' || cv === 'false';
|
|
return equal ? falsy : !falsy;
|
|
}
|
|
var num = Number(value);
|
|
if (!isNaN(num) && String(num) === value) {
|
|
return equal ? Number(cv) === num : Number(cv) !== num;
|
|
}
|
|
return equal ? String(cv) === value : String(cv) !== value;
|
|
}
|
|
function parseOr() {
|
|
var left = parseAnd();
|
|
while (peek() && peek().t === '||') {
|
|
consume();
|
|
var right = parseAnd();
|
|
left = left || right;
|
|
}
|
|
return left;
|
|
}
|
|
function parseAnd() {
|
|
var left = parsePrimary();
|
|
while (peek() && peek().t === '&&') {
|
|
consume();
|
|
var right = parsePrimary();
|
|
left = left && right;
|
|
}
|
|
return left;
|
|
}
|
|
function parsePrimary() {
|
|
if (!peek()) throw new Error('Unexpected EOF');
|
|
if (peek().t === '(') {
|
|
consume();
|
|
var grouped = parseOr();
|
|
expect(')');
|
|
return grouped;
|
|
}
|
|
if (peek().t === '!') {
|
|
consume();
|
|
var notTok = consume();
|
|
if (!notTok || notTok.t !== 'id') throw new Error('Expected key after !');
|
|
return !ctxVal(notTok.v);
|
|
}
|
|
if (peek().t === 'id') {
|
|
var keyTok = consume();
|
|
if (peek() && peek().t === '==') {
|
|
consume();
|
|
var eqTok = consume();
|
|
if (!eqTok) throw new Error('Expected value after ==');
|
|
return compareValue(ctxVal(keyTok.v), eqTok.t === 'str' ? eqTok.v : (eqTok.v || ''), true);
|
|
}
|
|
if (peek() && peek().t === '!=') {
|
|
consume();
|
|
var neTok = consume();
|
|
if (!neTok) throw new Error('Expected value after !=');
|
|
return compareValue(ctxVal(keyTok.v), neTok.t === 'str' ? neTok.v : (neTok.v || ''), false);
|
|
}
|
|
var cv = ctxVal(keyTok.v);
|
|
if (typeof cv === 'boolean') return cv;
|
|
if (typeof cv === 'number') return cv !== 0;
|
|
return cv !== '' && cv !== undefined && cv !== null;
|
|
}
|
|
throw new Error('Unexpected token: ' + peek().t);
|
|
}
|
|
return parseOr();
|
|
} catch (_) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function tokenizeWhenExpression(expr) {
|
|
var tokens = [];
|
|
var i = 0;
|
|
while (i < expr.length) {
|
|
var ch = expr[i];
|
|
if (ch === ' ' || ch === '\t') { i += 1; continue; }
|
|
if (ch === '(') { tokens.push({ t: '(' }); i += 1; continue; }
|
|
if (ch === ')') { tokens.push({ t: ')' }); i += 1; continue; }
|
|
if (ch === '&' && expr[i + 1] === '&') { tokens.push({ t: '&&' }); i += 2; continue; }
|
|
if (ch === '|' && expr[i + 1] === '|') { tokens.push({ t: '||' }); i += 2; continue; }
|
|
if (ch === '=' && expr[i + 1] === '=') { tokens.push({ t: '==' }); i += 2; continue; }
|
|
if (ch === '!' && expr[i + 1] === '=') { tokens.push({ t: '!=' }); i += 2; continue; }
|
|
if (ch === '!') { tokens.push({ t: '!' }); i += 1; continue; }
|
|
if (ch === '"' || ch === "'") {
|
|
i += 1;
|
|
var str = '';
|
|
while (i < expr.length && expr[i] !== ch) {
|
|
str += expr[i];
|
|
i += 1;
|
|
}
|
|
if (i < expr.length) i += 1;
|
|
tokens.push({ t: 'str', v: str });
|
|
continue;
|
|
}
|
|
if (
|
|
(ch >= 'a' && ch <= 'z') ||
|
|
(ch >= 'A' && ch <= 'Z') ||
|
|
(ch >= '0' && ch <= '9') ||
|
|
ch === '_' ||
|
|
ch === '.'
|
|
) {
|
|
var id = '';
|
|
while (
|
|
i < expr.length &&
|
|
(
|
|
(expr[i] >= 'a' && expr[i] <= 'z') ||
|
|
(expr[i] >= 'A' && expr[i] <= 'Z') ||
|
|
(expr[i] >= '0' && expr[i] <= '9') ||
|
|
expr[i] === '_' ||
|
|
expr[i] === '.'
|
|
)
|
|
) {
|
|
id += expr[i];
|
|
i += 1;
|
|
}
|
|
tokens.push({ t: 'id', v: id });
|
|
continue;
|
|
}
|
|
throw new Error('Unexpected char: ' + ch);
|
|
}
|
|
return tokens;
|
|
}
|
|
|
|
window.__mnoteFileTreeContextMenuRuntime = {
|
|
buildFileTreeCommandContext: buildFileTreeCommandContext,
|
|
buildSidebarFileTreeContext: buildSidebarFileTreeContext,
|
|
buildLegacySidebarFileTreeContext: buildLegacySidebarFileTreeContext,
|
|
evaluateSidebarFileTreeWhen: evaluateSidebarFileTreeWhen,
|
|
tokenizeWhenExpression: tokenizeWhenExpression
|
|
};
|