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