OcttKB/Wiki/plugins/echarts/$__plugins_Gk0Wk_echarts_addons_TheBrain.ts.js

603 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint-disable max-lines */
import type { IParseTreeNode } from 'tiddlywiki';
import type { IScriptAddon } from '../../scriptAddon';
const colors = [
'#5470c6',
'#91cc75',
'#fac858',
'#ee6666',
'#73c0de',
'#3ba272',
'#fc8452',
'#9a60b4',
'#ea7ccc',
];
const CategoriesEn = [
'Focusing',
'History',
'Link To',
'Backlink From',
'Tag To',
'Tag By',
'Transclude',
].map((name, index) => ({
name,
itemStyle: { color: colors[index % colors.length] },
}));
const CategoriesZh = [
'聚焦',
'历史',
'链接',
'反链',
'标签',
'作为标签',
'嵌套',
].map((name, index) => ({
name,
itemStyle: { color: colors[index % colors.length] },
}));
const attributes = new Set<string>([
'focussedTiddler',
'levels',
'graphTitle',
'aliasField',
'excludeFilter',
'previewDelay',
]);
const getPlatteColor = (name: string) =>
$tw.wiki.renderText(
'text/plain',
'text/vnd.tiddlywiki',
`<$transclude tiddler={{$:/palette}} index="${name}"><$transclude tiddler="$:/palettes/Vanilla" index="${name}"><$transclude tiddler="$:/config/DefaultColourMappings/${name}"/></$transclude></$transclude>`,
{},
);
const findIcon = (title: string) => {
const fields = $tw.wiki.getTiddler(title)?.fields;
if (!fields?.icon) {
return undefined;
}
const iconFields = $tw.wiki.getTiddler(fields.icon as string)?.fields;
if (!iconFields) {
if (/^https?:\/\//.test(fields.icon as string)) {
return `image://${fields.icon as string}`;
}
return undefined;
}
if (iconFields._canonical_uri) {
return `image://${iconFields._canonical_uri}`;
} else if (iconFields.title.startsWith('$:/core/images/')) {
return undefined;
} else {
return `image://data:${iconFields.type};base64,${iconFields.text}`;
}
};
const getAliasOrTitle = (
tiddlerTitle: string,
aliasField: string | undefined,
): [string, boolean] => {
if (aliasField === undefined || aliasField === 'title') {
return [tiddlerTitle, Boolean($tw.wiki.getTiddler(tiddlerTitle))];
}
const tiddler = $tw.wiki.getTiddler(tiddlerTitle);
if (tiddler) {
const aliasValue = tiddler.fields[aliasField];
return [
typeof aliasValue === 'string'
? $tw.wiki.renderText('text/plain', 'text/vnd.tiddlywiki', aliasValue, {
variables: { currentTiddler: tiddlerTitle },
})
: tiddlerTitle,
true,
];
} else {
return [tiddlerTitle, false];
}
};
interface ITheBrainState {
currentlyFocused?: string;
historyTiddlers: string[];
viewingTiddlers: Set<string>;
focusing?: string;
}
const TheBrainAddon: IScriptAddon<ITheBrainState> = {
onMount: (myChart, attributes) => {
myChart.on('click', { dataType: 'node' }, (event: any) => {
new $tw.Story().navigateTiddler(event.data.name);
});
return {
historyTiddlers: [],
viewingTiddlers: new Set(),
focusing: attributes.focussedTiddler,
};
},
shouldUpdate: (
{ viewingTiddlers, focusing, currentlyFocused },
changedTiddlers,
changedAttributes,
) => {
return (
Object.keys(changedTiddlers).some(title => viewingTiddlers.has(title)) ||
Object.keys(changedAttributes).some(attribute =>
attributes.has(attribute),
) ||
(focusing === undefined &&
$tw.wiki.getTiddlerText('$:/temp/focussedTiddler') !== currentlyFocused)
);
},
// eslint-disable-next-line complexity
onUpdate: (
myCharts,
state,
addonAttributes: {
focussedTiddler?: string;
levels?: number;
graphTitle?: string;
aliasField?: string;
excludeFilter?: string;
previewDelay?: string;
},
) => {
/** 参数focussedTiddler 是图的中央节点 */
let focussedTiddler =
addonAttributes.focussedTiddler ||
$tw.wiki.getTiddlerText('$:/temp/focussedTiddler')!;
state.viewingTiddlers.clear();
state.focusing = addonAttributes.focussedTiddler;
state.currentlyFocused = focussedTiddler;
if (!focussedTiddler) {
return;
}
state.viewingTiddlers.add(focussedTiddler);
if ($tw.wiki.getTiddler(focussedTiddler)?.fields['draft.of']) {
focussedTiddler = $tw.wiki.getTiddler(focussedTiddler)!.fields[
'draft.of'
] as string;
}
const nodes: any[] = [];
const edges: any[] = [];
const ifChinese =
$tw.wiki.getTiddlerText('$:/language')?.includes('zh') === true;
/** 参数levels 指定图向外展开几级 */
let levels = Number(addonAttributes.levels);
if (Number.isNaN(levels)) {
levels = 1;
}
levels = Math.max(levels, 0);
/** 参数graphTitle 指定右下角显示的标题 */
const graphTitle =
addonAttributes.graphTitle || (ifChinese ? '聚焦' : 'Focusing Map');
/** 参数aliasField 用于指定展示为节点标题的字段,例如 caption */
const aliasField =
addonAttributes.aliasField === ''
? undefined
: addonAttributes.aliasField;
/** 参数excludeFilter 用于排除部分节点 */
const excludeFilter =
addonAttributes.excludeFilter === ''
? undefined
: $tw.wiki.compileFilter(
addonAttributes.excludeFilter ?? '[prefix[$:/]]',
);
const nodeMap: Map<string, boolean> = new Map();
// 聚焦点
nodes.push({
name: focussedTiddler,
// fixed: true,
category: 0,
label: {
formatter: getAliasOrTitle(focussedTiddler, aliasField)[0],
fontWeight: 'bold',
fontSize: '15px',
},
symbol: findIcon(focussedTiddler),
symbolSize: 15,
select: {
disabled: true,
},
itemStyle: {
opacity: 1,
borderColor: `${colors[0]}66`,
borderWidth: 15,
},
isTag: false,
tooltip: {
show: false,
},
});
// 初始化:当前关注的 Tiddler
let tiddlerQueue = [focussedTiddler];
if (excludeFilter) {
const tiddlers = new Set<string>(tiddlerQueue);
for (const excluded of excludeFilter.call($tw.wiki, tiddlerQueue)) {
tiddlers.delete(excluded);
}
tiddlerQueue = Array.from(tiddlers);
}
nodeMap.set(focussedTiddler, true);
nodeMap.set('', false);
const tryPush = (
title: string,
node: (label: string, exist: boolean) => any,
edge: (exist: boolean) => any,
) => {
if (excludeFilter && excludeFilter.call($tw.wiki, [title]).length > 0) {
return false;
}
const nodeState = nodeMap.get(title);
const [label, exist] =
nodeState === undefined
? getAliasOrTitle(title, aliasField)
: ['', nodeState];
if (nodeState === undefined) {
nodes.push(node(label, exist));
nodeMap.set(title, exist);
if (exist) {
tiddlerQueue.push(title);
}
}
edges.push(edge(exist));
return exist;
};
// 广搜 levels 层
while (tiddlerQueue.length && levels-- > 0) {
const tiddlers = tiddlerQueue;
tiddlerQueue = [];
for (const tiddler of tiddlers) {
// 链接
for (const linksTo of $tw.wiki.getTiddlerLinks(tiddler)) {
tryPush(
linksTo,
(label, exist) => ({
name: linksTo,
label: { formatter: label },
itemStyle: { opacity: exist ? 1 : 0.65 },
symbol: findIcon(linksTo),
category: 2,
isTag: false,
}),
exist => ({
source: tiddler,
target: linksTo,
lineStyle: {
color: colors[2],
type: exist ? 'solid' : 'dashed',
},
}),
);
}
// 反链
for (const backlinksFrom of $tw.wiki.getTiddlerBacklinks(tiddler)) {
tryPush(
backlinksFrom,
(label, exist) => ({
name: backlinksFrom,
label: { formatter: label },
itemStyle: { opacity: exist ? 1 : 0.65 },
symbol: findIcon(backlinksFrom),
category: 3,
isTag: false,
}),
exist => ({
source: backlinksFrom,
target: tiddler,
lineStyle: {
color: colors[3],
type: exist ? 'solid' : 'dashed',
},
}),
);
}
// 标签
for (const tag of $tw.wiki.getTiddler(focussedTiddler)?.fields?.tags ??
[]) {
tryPush(
tag,
(label, exist) => ({
name: tag,
label: { formatter: label },
itemStyle: { opacity: exist ? 1 : 0.65 },
symbol: findIcon(tag),
category: 4,
isTag: true,
}),
exist => ({
source: tiddler,
target: tag,
lineStyle: {
color: colors[4],
type: exist ? 'solid' : 'dashed',
},
}),
);
}
// 作为标签
for (const tagBy of $tw.wiki.getTiddlersWithTag(tiddler)) {
tryPush(
tagBy,
(label, exist) => ({
name: tagBy,
label: { formatter: label },
itemStyle: { opacity: exist ? 1 : 0.65 },
symbol: findIcon(tagBy),
category: 5,
isTag: false,
}),
exist => ({
source: tagBy,
target: tiddler,
lineStyle: {
color: colors[5],
type: exist ? 'solid' : 'dashed',
},
}),
);
}
// 嵌入
const tiddler_ = $tw.wiki.getTiddler(tiddler);
if (tiddler_) {
const type = tiddler_.fields.type || 'text/vnd.tiddlywiki';
if (type === 'text/vnd.tiddlywiki' || type === 'text/x-markdown') {
const transcluded: Set<string> = new Set();
const findTransclude = (children: IParseTreeNode[]) => {
const { length } = children;
for (let i = 0; i < length; i++) {
const node = children[i];
if (node.type === 'tiddler') {
const title = node.attributes!.tiddler?.value as
| string
| undefined;
if (title) {
transcluded.add(title);
}
} else if (Array.isArray((node as any).children)) {
findTransclude((node as any).children);
}
}
};
findTransclude($tw.wiki.parseTiddler(tiddler).tree);
// eslint-disable-next-line max-depth
for (const transcludeTiddler of transcluded) {
tryPush(
transcludeTiddler,
(label, exist) => ({
name: transcludeTiddler,
label: { formatter: label },
itemStyle: { opacity: exist ? 1 : 0.65 },
symbol: findIcon(transcludeTiddler),
category: 6,
isTag: false,
}),
exist => ({
source: tiddler,
target: transcludeTiddler,
lineStyle: {
color: colors[6],
type: exist ? 'solid' : 'dashed',
},
}),
);
}
}
}
}
}
// 历史路径
let nextTiddler = focussedTiddler;
const historyMap: Set<string> = new Set();
for (let index = state.historyTiddlers.length - 2; index >= 0; index--) {
const tiddlerTitle = state.historyTiddlers[index];
if (
historyMap.has(tiddlerTitle) ||
tiddlerTitle === nextTiddler ||
tiddlerTitle.startsWith('$:/')
) {
continue;
}
tryPush(
tiddlerTitle,
(label, exist) => ({
name: tiddlerTitle,
label: { formatter: label, fontSize: '10px' },
category: 1,
symbol: findIcon(tiddlerTitle),
symbolSize: 3,
itemStyle: { opacity: exist ? 0.65 : 0.4 },
isTag: false,
}),
// eslint-disable-next-line @typescript-eslint/no-loop-func
exist => ({
source: tiddlerTitle,
target: nextTiddler,
lineStyle: {
color: colors[1],
type: exist ? 'dashed' : 'dotted',
opacity: 0.5,
},
}),
);
nextTiddler = tiddlerTitle;
}
// 更新历史
const historyIndex = state.historyTiddlers.indexOf(focussedTiddler);
if (historyIndex > -1) {
state.historyTiddlers.splice(historyIndex, 1);
}
state.historyTiddlers.push(focussedTiddler);
state.historyTiddlers.slice(-10);
let lastTitle = '';
let cache: Element[] | undefined;
const cachedTooltipFormatter = ({
data: { name, isTag },
dataType,
}: {
data: { name: string; isTag: boolean };
dataType: string;
}) => {
if (dataType !== 'node') {
return [];
}
if (name !== lastTitle || !cache) {
const container = $tw.utils.domMaker('div', {
style: {
maxWidth: '40vw',
maxHeight: '50vh',
overflowY: 'auto',
whiteSpace: 'normal',
},
class: 'gk0wk-echarts-thebrain-popuptiddler-container',
});
if (isTag) {
const ul = $tw.utils.domMaker('ul', {});
const tiddlers = $tw.wiki.getTiddlersWithTag(name);
const len = tiddlers.length;
for (let i = 0; i < len; i++) {
const tiddler = tiddlers[i];
const li = $tw.utils.domMaker('li', {});
const a = $tw.utils.domMaker('a', {
text: tiddler,
class:
'tc-tiddlylink tc-tiddlylink-resolves tc-popup-handle tc-popup-absolute',
style: {
cursor: 'pointer',
},
});
// eslint-disable-next-line @typescript-eslint/no-loop-func
a.addEventListener('click', () =>
new $tw.Story().navigateTiddler(tiddler),
);
li.appendChild(a);
ul.appendChild(li);
}
cache = [ul];
} else {
// 不可以直接 renderText, 那种是 headless 渲染
$tw.wiki
.makeWidget(
$tw.wiki.parseTiddler(
'$:/plugins/Gk0Wk/echarts/addons/TheBrainPopup',
),
{
document,
parseAsInline: true,
variables: { currentTiddler: name },
} as any,
)
.render(container, null);
cache = [
container,
$tw.utils.domMaker('style', {
innerHTML: `.gk0wk-echarts-thebrain-popuptiddler-container::-webkit-scrollbar {display: none;} .gk0wk-echarts-thebrain-popuptiddler-container .tc-tiddler-controls { display: none; }`,
}),
];
}
lastTitle = name;
}
return cache;
};
let previewDelay = Number(addonAttributes.previewDelay || '1000');
if (!Number.isSafeInteger(previewDelay)) {
previewDelay = -1;
}
myCharts.setOption({
backgroundColor: 'transparent',
legend: [
{
data: (ifChinese ? CategoriesZh : CategoriesEn).map(a => {
return a.name;
}),
icon: 'circle',
},
],
title: {
text: graphTitle,
show: true,
top: 'bottom',
left: 'right',
},
toolbox: {
show: true,
left: 0,
bottom: 0,
feature: {
restore: {},
saveAsImage: {},
},
},
tooltip: {
position: 'top',
formatter: cachedTooltipFormatter,
triggerOn: previewDelay >= 0 ? 'mousemove' : 'none',
enterable: true,
showDelay: Math.max(0, previewDelay),
hideDelay: 800,
confine: true,
textStyle: {
color: 'inherit',
fontFamily: 'inherit',
fontSize: 'inherit',
},
appendToBody: true,
backgroundColor: getPlatteColor('page-background'),
borderColor: getPlatteColor('very-muted-foreground'),
},
series: [
{
name: graphTitle,
type: 'graph',
layout: 'force',
top: 0,
bottom: 0,
left: 0,
right: 0,
height: '100%',
width: '100%',
nodes,
edges,
categories: ifChinese ? CategoriesZh : CategoriesEn,
roam: true,
draggable: true,
zoom: 4,
label: {
position: 'right',
show: true,
backgroundColor: 'transparent',
},
labelLayout: {
moveOverlap: true,
},
force: {
repulsion: 50,
},
cursor: 'pointer',
symbolSize: 6,
edgeSymbol: ['none', 'arrow'],
edgeSymbolSize: [0, 5],
lineStyle: {
width: 1,
opacity: 0.75,
curveness: 0.15,
},
itemStyle: {
opacity: 0.9,
},
},
],
} as any);
},
};
export default TheBrainAddon;
/* eslint-enable max-lines */