refactor: externalize filetree runtime helpers
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
// MNote 文件树上下文菜单运行时外置模块。
|
||||
// 当前先承接 CommandContext 构造与 when 表达式求值,树语义仍由 Rust/kernel 主导。
|
||||
|
||||
function buildSidebarFileTreeContext(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 = {
|
||||
buildSidebarFileTreeContext: buildSidebarFileTreeContext,
|
||||
evaluateSidebarFileTreeWhen: evaluateSidebarFileTreeWhen,
|
||||
tokenizeWhenExpression: tokenizeWhenExpression
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
// MNote 主壳文件树运行时外置模块。
|
||||
// 当前先承接 SIDEBAR_TREE_JS 中低风险的行数据解析 helper;
|
||||
// inline script 保留 fallback,避免模块加载顺序影响首屏行为。
|
||||
|
||||
function fileTreeRowKind(row) {
|
||||
if (!(row instanceof HTMLElement)) return '';
|
||||
return String(row.getAttribute('data-row-kind') || '').trim();
|
||||
}
|
||||
|
||||
function fileTreeRowDocumentId(row) {
|
||||
if (!(row instanceof HTMLElement)) return '';
|
||||
return String(row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '').trim();
|
||||
}
|
||||
|
||||
function fileTreeRowAssetId(row, deps) {
|
||||
if (!(row instanceof HTMLElement)) return '';
|
||||
var assetId = String(row.getAttribute('data-asset-id') || '').trim();
|
||||
if (assetId) return assetId;
|
||||
var currentSourceKind = deps && typeof deps.currentSourceKind === 'function' ? deps.currentSourceKind : function() { return ''; };
|
||||
if (currentSourceKind() === 'local_folder' && fileTreeRowKind(row) === 'asset') {
|
||||
return String(row.getAttribute('data-row-id') || '').trim();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function decodeLocalEncodedPath(value) {
|
||||
var path = String(value || '').trim().replace(/~2F/g, '/');
|
||||
if (!path) return '';
|
||||
try {
|
||||
return decodeURIComponent(path);
|
||||
} catch (_) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
function fileTreeRowLocalRelativePath(row, deps) {
|
||||
if (!(row instanceof HTMLElement)) return '';
|
||||
var direct = String(row.getAttribute('data-local-relative-path') || '').trim();
|
||||
if (direct) return direct;
|
||||
var localFilePathFromAssetId = deps && typeof deps.localFilePathFromAssetId === 'function'
|
||||
? deps.localFilePathFromAssetId
|
||||
: function() { return ''; };
|
||||
var assetPath = localFilePathFromAssetId(fileTreeRowAssetId(row, deps));
|
||||
if (assetPath) return assetPath;
|
||||
var rowId = String(row.getAttribute('data-row-id') || '').trim();
|
||||
var nodeId = String(row.getAttribute('data-node-id') || '').trim();
|
||||
var documentId = fileTreeRowDocumentId(row);
|
||||
var kind = fileTreeRowKind(row);
|
||||
if (rowId.indexOf('local:asset:') === 0) return rowId.slice('local:asset:'.length);
|
||||
if (rowId.indexOf('local:markdown:') === 0) return rowId.slice('local:markdown:'.length);
|
||||
if (rowId.indexOf('local:folder:') === 0) return rowId.slice('local:folder:'.length);
|
||||
if (rowId.indexOf('local:node:') === 0) return rowId.slice('local:node:'.length);
|
||||
if (nodeId.indexOf('local:node:') === 0) return nodeId.slice('local:node:'.length);
|
||||
if ((kind === 'document' || kind === 'doc' || kind === 'markdown') && documentId.indexOf('local-md:') === 0) {
|
||||
return decodeLocalEncodedPath(documentId.slice('local-md:'.length));
|
||||
}
|
||||
if ((kind === 'folder' || kind === 'directory' || kind === 'index') && documentId.indexOf('local-dir:') === 0) {
|
||||
return decodeLocalEncodedPath(documentId.slice('local-dir:'.length));
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function fileTreeRowLocalUploadTargetRelativePath(row, deps) {
|
||||
if (!(row instanceof HTMLElement)) return '';
|
||||
var kind = fileTreeRowKind(row);
|
||||
var relativePath = fileTreeRowLocalRelativePath(row, deps);
|
||||
if (!relativePath) return '';
|
||||
if (kind === 'folder' || kind === 'directory') return relativePath;
|
||||
var lastSlash = relativePath.lastIndexOf('/');
|
||||
return lastSlash >= 0 ? relativePath.slice(0, lastSlash) : '';
|
||||
}
|
||||
|
||||
function isFileTreeDownloadableAssetRow(row, deps) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
var kind = fileTreeRowKind(row);
|
||||
if (kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown' || kind === 'folder' || kind === 'directory') return false;
|
||||
return Boolean(fileTreeRowAssetId(row, deps));
|
||||
}
|
||||
|
||||
function isFileTreeDownloadableRow(row, deps) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
var currentSourceKind = deps && typeof deps.currentSourceKind === 'function' ? deps.currentSourceKind : function() { return ''; };
|
||||
if (currentSourceKind() !== 'local_folder') return isFileTreeDownloadableAssetRow(row, deps);
|
||||
return Boolean(fileTreeRowLocalRelativePath(row, deps) || isFileTreeDownloadableAssetRow(row, deps));
|
||||
}
|
||||
|
||||
function fileTreeAssetDownloadDetail(row, deps) {
|
||||
if (!isFileTreeDownloadableRow(row, deps)) return null;
|
||||
var rowTitle = deps && typeof deps.rowTitle === 'function' ? deps.rowTitle : function() { return ''; };
|
||||
var resolveWorkspaceId = deps && typeof deps.resolveWorkspaceId === 'function' ? deps.resolveWorkspaceId : function() { return ''; };
|
||||
var title = rowTitle(row);
|
||||
var relativePath = fileTreeRowLocalRelativePath(row, deps);
|
||||
var assetId = fileTreeRowAssetId(row, deps) || (relativePath ? 'local-file:' + relativePath : '');
|
||||
return {
|
||||
contextKind: 'filetree',
|
||||
documentId: fileTreeRowDocumentId(row),
|
||||
rowId: row.getAttribute('data-row-id') || '',
|
||||
rowKind: fileTreeRowKind(row),
|
||||
assetId: assetId,
|
||||
localRelativePath: relativePath,
|
||||
title: title,
|
||||
fileName: title,
|
||||
workspaceId: resolveWorkspaceId(row)
|
||||
};
|
||||
}
|
||||
|
||||
function fileTreeRowsByRowIds(rowIds, deps) {
|
||||
var cssEscape = deps && typeof deps.cssEscape === 'function' ? deps.cssEscape : function(value) { return String(value).replace(/["\\]/g, '\\$&'); };
|
||||
return (rowIds || []).map(function(rowId) {
|
||||
return document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]');
|
||||
}).filter(function(row) { return row instanceof HTMLElement; });
|
||||
}
|
||||
|
||||
function fileTreeChildCount(documentId, deps) {
|
||||
if (!documentId) return 0;
|
||||
var cssEscape = deps && typeof deps.cssEscape === 'function' ? deps.cssEscape : function(value) { return String(value).replace(/["\\]/g, '\\$&'); };
|
||||
var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + documentId) + '"]');
|
||||
var node = row ? row.closest('.tree-node') : null;
|
||||
var children = node ? node.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"][data-row-kind="document"], :scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"][data-row-kind="doc"]') : [];
|
||||
return children.length;
|
||||
}
|
||||
|
||||
window.__mnoteFileTreeRuntime = {
|
||||
fileTreeRowKind: fileTreeRowKind,
|
||||
fileTreeRowDocumentId: fileTreeRowDocumentId,
|
||||
fileTreeRowAssetId: fileTreeRowAssetId,
|
||||
decodeLocalEncodedPath: decodeLocalEncodedPath,
|
||||
fileTreeRowLocalRelativePath: fileTreeRowLocalRelativePath,
|
||||
fileTreeRowLocalUploadTargetRelativePath: fileTreeRowLocalUploadTargetRelativePath,
|
||||
isFileTreeDownloadableAssetRow: isFileTreeDownloadableAssetRow,
|
||||
isFileTreeDownloadableRow: isFileTreeDownloadableRow,
|
||||
fileTreeAssetDownloadDetail: fileTreeAssetDownloadDetail,
|
||||
fileTreeRowsByRowIds: fileTreeRowsByRowIds,
|
||||
fileTreeChildCount: fileTreeChildCount
|
||||
};
|
||||
Reference in New Issue
Block a user