116 lines
4.5 KiB
JavaScript
116 lines
4.5 KiB
JavaScript
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
// graph.js — 從知識庫筆記內 [[wikilink]] 建築節點與邊(給圖譜視圖)
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
const WIKI_RE = /\[\[([^\]]+)\]\]/g;
|
|||
|
|
|
|||
|
|
const KIND_META = {
|
|||
|
|
overview: { color: '#0071e3', shape: 'box' },
|
|||
|
|
principleMap: { color: '#5856d6', shape: 'box' },
|
|||
|
|
quiz: { color: '#ff2d55', shape: 'box' },
|
|||
|
|
category: { color: '#0071e3', shape: 'box' },
|
|||
|
|
case: { color: '#34c759', shape: 'dot' },
|
|||
|
|
principle: { color: '#af52de', shape: 'dot' },
|
|||
|
|
term: { color: '#ff9500', shape: 'dot' },
|
|||
|
|
company: { color: '#5ac8fa', shape: 'dot' },
|
|||
|
|
episode: { color: '#8e8e93', shape: 'dot' },
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function nodeKey(hit) { return `${hit.kind}:${hit.id}`; }
|
|||
|
|
|
|||
|
|
function resolveLink(linkMap, target) {
|
|||
|
|
const t = (target || '').trim();
|
|||
|
|
return linkMap[t] || linkMap[t.split('#').pop()] || linkMap[t.split('/').pop()] || null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function shortLabel(title, max = 28) {
|
|||
|
|
const s = String(title || '');
|
|||
|
|
return s.length > max ? s.slice(0, max - 1) + '…' : s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function buildGraph(knowledge, opts = {}) {
|
|||
|
|
const linkMap = knowledge.linkMap || {};
|
|||
|
|
const includeIndex = opts.includeIndex === '1' || opts.includeIndex === true;
|
|||
|
|
const kinds = opts.kinds ? new Set(String(opts.kinds).split(',')) : null;
|
|||
|
|
const limit = Math.min(Number(opts.limit) || 450, 800);
|
|||
|
|
const center = opts.center ? String(opts.center) : null;
|
|||
|
|
const depth = Math.min(Math.max(Number(opts.depth) || 2, 1), 4);
|
|||
|
|
|
|||
|
|
const nodes = new Map();
|
|||
|
|
const edgeSet = new Set();
|
|||
|
|
const edges = [];
|
|||
|
|
|
|||
|
|
const addNode = (hit) => {
|
|||
|
|
if (!hit) return null;
|
|||
|
|
if (kinds && !kinds.has(hit.kind)) return null;
|
|||
|
|
const id = nodeKey(hit);
|
|||
|
|
if (nodes.size >= limit && !nodes.has(id)) return null;
|
|||
|
|
if (!nodes.has(id)) {
|
|||
|
|
const meta = KIND_META[hit.kind] || { color: '#8e8e93', shape: 'dot' };
|
|||
|
|
nodes.set(id, {
|
|||
|
|
id, label: shortLabel(hit.title || hit.id),
|
|||
|
|
title: hit.title || hit.id,
|
|||
|
|
kind: hit.kind,
|
|||
|
|
color: meta.color,
|
|||
|
|
shape: meta.shape,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
return id;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const addEdge = (from, to) => {
|
|||
|
|
if (!from || !to || from === to) return;
|
|||
|
|
const k = `${from}|${to}`;
|
|||
|
|
if (edgeSet.has(k)) return;
|
|||
|
|
edgeSet.add(k);
|
|||
|
|
edges.push({ from, to });
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const parseBody = (body, fromId) => {
|
|||
|
|
if (!body || !fromId) return;
|
|||
|
|
let m;
|
|||
|
|
WIKI_RE.lastIndex = 0;
|
|||
|
|
while ((m = WIKI_RE.exec(body)) !== null) {
|
|||
|
|
const hit = resolveLink(linkMap, m[1]);
|
|||
|
|
const toId = addNode(hit);
|
|||
|
|
if (toId) addEdge(fromId, toId);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const seed = (hit, body) => {
|
|||
|
|
const id = addNode(hit);
|
|||
|
|
if (id && body) parseBody(body, id);
|
|||
|
|
return id;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (knowledge.overview) seed({ kind: 'overview', id: knowledge.overview.id, title: knowledge.overview.title }, knowledge.overview.body);
|
|||
|
|
if (knowledge.principleMap) seed({ kind: 'principleMap', id: knowledge.principleMap.id, title: knowledge.principleMap.title }, knowledge.principleMap.body);
|
|||
|
|
if (knowledge.quiz) seed({ kind: 'quiz', id: knowledge.quiz.id, title: knowledge.quiz.title }, knowledge.quiz.body);
|
|||
|
|
|
|||
|
|
for (const c of (knowledge.categories || [])) seed({ kind: 'category', id: c.id, title: c.title }, c.body);
|
|||
|
|
for (const c of (knowledge.cases || [])) seed({ kind: 'case', id: c.id, title: c.title }, c.body);
|
|||
|
|
for (const p of (knowledge.principles || [])) seed({ kind: 'principle', id: p.id, title: p.title }, p.body);
|
|||
|
|
|
|||
|
|
if (includeIndex) {
|
|||
|
|
for (const x of (knowledge.index || [])) addNode({ kind: x.kind, id: x.id, title: x.title });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 以某節點為中心:BFS 保留 depth 層內的節點與邊
|
|||
|
|
if (center && nodes.has(center)) {
|
|||
|
|
const keep = new Set([center]);
|
|||
|
|
let frontier = [center];
|
|||
|
|
for (let d = 0; d < depth; d++) {
|
|||
|
|
const next = [];
|
|||
|
|
for (const e of edges) {
|
|||
|
|
if (frontier.includes(e.from) && nodes.has(e.to)) { keep.add(e.to); next.push(e.to); }
|
|||
|
|
if (frontier.includes(e.to) && nodes.has(e.from)) { keep.add(e.from); next.push(e.from); }
|
|||
|
|
}
|
|||
|
|
frontier = [...new Set(next)];
|
|||
|
|
}
|
|||
|
|
const filtEdges = edges.filter(e => keep.has(e.from) && keep.has(e.to));
|
|||
|
|
const filtNodes = [...nodes.values()].filter(n => keep.has(n.id));
|
|||
|
|
return { nodes: filtNodes, edges: filtEdges, center, depth, total: filtNodes.length };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { nodes: [...nodes.values()], edges, total: nodes.size };
|
|||
|
|
}
|