> provider = providerManage.getProvider();
58 | // 调用 DisruptorProvider 的 onData 方法,发送数据
59 | provider.onData(Collections.singleton(data));
60 | }
61 | ```
62 | ### 2. Click the button to the left of Obsidian to open the canvas
63 |
--------------------------------------------------------------------------------
/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 | import process from "process";
3 | import builtins from "builtin-modules";
4 |
5 | const banner =
6 | `/*
7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
8 | if you want to view the source, please visit the github repository of this plugin
9 | */
10 | `;
11 |
12 | const prod = (process.argv[2] === "production");
13 |
14 | const context = await esbuild.context({
15 | banner: {
16 | js: banner,
17 | },
18 | entryPoints: ["src/main.ts"],
19 | bundle: true,
20 | external: [
21 | "obsidian",
22 | "electron",
23 | "@codemirror/autocomplete",
24 | "@codemirror/collab",
25 | "@codemirror/commands",
26 | "@codemirror/language",
27 | "@codemirror/lint",
28 | "@codemirror/search",
29 | "@codemirror/state",
30 | "@codemirror/view",
31 | "@lezer/common",
32 | "@lezer/highlight",
33 | "@lezer/lr",
34 | ...builtins],
35 | format: "cjs",
36 | target: "es2018",
37 | logLevel: "info",
38 | sourcemap: prod ? false : "inline",
39 | treeShaking: true,
40 | outfile: "main.js",
41 | });
42 |
43 | if (prod) {
44 | await context.rebuild();
45 | process.exit(0);
46 | } else {
47 | await context.watch();
48 | }
49 |
--------------------------------------------------------------------------------
/img/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waiting0324/obsidian-code-note/2ae559fcaf4cd9068ce42a679b14edefeb415bc3/img/demo.gif
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "source-code-note",
3 | "name": "Source Code Note",
4 | "version": "1.0.0",
5 | "minAppVersion": "1.0.0",
6 | "description": "This plugin can help you organize source code note easily.",
7 | "author": "Waiting",
8 | "authorUrl": "https://github.com/waiting0324",
9 | "isDesktopOnly": false
10 | }
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-sample-plugin",
3 | "version": "1.0.0",
4 | "description": "This is a sample plugin for Obsidian (https://obsidian.md)",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "node esbuild.config.mjs",
8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
9 | "version": "node version-bump.mjs && git add manifest.json versions.json"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "MIT",
14 | "devDependencies": {
15 | "@types/node": "^16.11.6",
16 | "@typescript-eslint/eslint-plugin": "5.29.0",
17 | "@typescript-eslint/parser": "5.29.0",
18 | "builtin-modules": "3.3.0",
19 | "esbuild": "0.17.3",
20 | "obsidian": "latest",
21 | "tslib": "2.4.0",
22 | "typescript": "4.7.4",
23 | "@antv/x6": "^1.34.5",
24 | "highlight.js": "^11.6.0"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/ClassShape.ts:
--------------------------------------------------------------------------------
1 | import {Graph} from '@antv/x6';
2 | import {Utils} from './Utils';
3 |
4 | const myUtils = new Utils();
5 |
6 | // 類名稱區塊高度
7 | const classNameBoxHeight = 30;
8 | // 方法名稱區塊高度
9 | const funcNameBoxHeight = 40;
10 |
11 | /**
12 | * 註冊 類聲明 中 方法名稱位置 計算函數
13 | */
14 | Graph.registerPortLayout(
15 | 'classFuncPosition',
16 | (portsPositionArgs, elemBBox) => {
17 | return portsPositionArgs.map((_, index) => {
18 | return {
19 | position: {
20 | x: 0,
21 | y: classNameBoxHeight + index * funcNameBoxHeight,
22 | },
23 | angle: 0,
24 | };
25 | });
26 | },
27 | //true,
28 | );
29 |
30 | /**
31 | * 類聲明 圖形定義
32 | */
33 | let clazzShapeDef = {
34 | // 最上層 class 名稱
35 | inherit: 'rect',
36 | markup: [
37 | {
38 | tagName: 'rect',
39 | selector: 'body',
40 | },
41 | {
42 | tagName: 'text',
43 | selector: 'label',
44 | },
45 | ],
46 | attrs: {
47 | rect: {
48 | strokeWidth: 1,
49 | stroke: '#e5885c',
50 | fill: '#fd9a6c',
51 | },
52 | label: {
53 | fontWeight: 'bold',
54 | fill: '#ffffff',
55 | fontSize: 14,
56 | textAnchor: 'middle',
57 | textVerticalAnchor: 'middle'
58 | },
59 | },
60 | // 該 class 所屬的方法列表
61 | ports: {
62 | groups: {
63 | list: {
64 | position: 'classFuncPosition',
65 | markup: [
66 | {
67 | tagName: 'rect',
68 | selector: 'func',
69 | },
70 | {
71 | tagName: 'text',
72 | selector: 'funcName',
73 | },
74 | {
75 | tagName: 'circle',
76 | selector: 'funcInCircle'
77 | },
78 | // {
79 | // tagName: 'circle',
80 | // selector: 'funcOutCircle'
81 | // },
82 | {
83 | tagName: 'circle',
84 | selector: 'toggle'
85 | },
86 | {
87 | tagName: 'text',
88 | selector: 'toggleText'
89 | }
90 | ],
91 | attrs: {
92 |
93 | func: {
94 | strokeWidth: 1,
95 | stroke: '#e5885c',
96 | fill: '#FFFFFF',
97 | },
98 | funcName: {
99 | fontSize: 13,
100 | textAnchor: 'middle',
101 | textVerticalAnchor: 'middle'
102 | },
103 | funcInCircle: {
104 | r: 5,
105 | stroke: '#efab7c',
106 | strokeWidth: 1,
107 | fill: '#fff'
108 | },
109 | // funcOutCircle: {
110 | // r: 5,
111 | // stroke: '#efab7c',
112 | // strokeWidth: 1,
113 | // fill: '#fff'
114 | // },
115 | toggle: {
116 | r: 7,
117 | stroke: '#b2bec3',
118 | strokeWidth: 2,
119 | fill: '#fff',
120 | cursor: 'pointer',
121 | },
122 | toggleText: {
123 | fontSize: 12,
124 | fontWeight: 800,
125 | fill: '#b2bec3',
126 | textAnchor: 'middle',
127 | textVerticalAnchor: 'middle',
128 | 'pointer-events': 'none', // 避免按鈕不靈敏
129 | cursor: 'pointer',
130 | }
131 | },
132 | },
133 | },
134 | },
135 | };
136 | clazzShapeDef.ports.groups.list.markup.push( {
137 | tagName: 'circle',
138 | selector: 'funcOutCircle'
139 | });
140 | clazzShapeDef.ports.groups.list.attrs.funcOutCircle = {
141 | r: 5,
142 | stroke: '#efab7c',
143 | strokeWidth: 1,
144 | fill: '#fff'
145 | }
146 |
147 |
148 | Graph.registerNode(
149 | 'clazz-shape',
150 | clazzShapeDef,
151 | true,
152 | );
153 |
154 | class ClassShape {
155 |
156 | /**
157 | * 根據 類名、方法名稱集合 創建 類聲明圖形
158 | * @param className 類名
159 | * @param funcNames 方法名稱集合
160 | * @returns 類聲明圖形
161 | */
162 | public createClassShape(className: string, funcNames: string[]) {
163 |
164 | // 計算 類聲明 圖形寬度
165 | let maxWidth = 100;
166 | maxWidth = Math.max(maxWidth, myUtils.getTextWidth(className, "14px bold") + 60);
167 | funcNames.forEach((funcName: string) => {
168 | maxWidth = Math.max(maxWidth, myUtils.getTextWidth(funcName, "13px") + 20);
169 | });
170 |
171 | // 定義頂部類名稱方塊
172 | const classShape = {
173 | "id": myUtils.getClassShapeId(className, '', 'class'),
174 | "shape": 'clazz-shape',
175 | "label": className,
176 | "width": maxWidth,
177 | "height": classNameBoxHeight,
178 | "fontSize": 14,
179 | "position": {
180 | "x": 100,
181 | "y": 100
182 | },
183 | "ports": [{}]
184 | };
185 | classShape.ports = [];
186 |
187 | // 定義下方函數名稱方塊
188 | funcNames.forEach((funcName: string) => {
189 |
190 | // 代碼塊 連接桩
191 | const port = {
192 | "id": myUtils.getClassShapeId(className, funcName, 'function'),
193 | "group": "list",
194 | "attrs": {
195 | "func": {
196 | width: maxWidth,
197 | height: funcNameBoxHeight,
198 | },
199 | "funcName": {
200 | ref: 'func',
201 | refX: '50%',
202 | refY: '50%',
203 | text: funcName,
204 | },
205 | "funcInCircle": {
206 | ref: 'func',
207 | refX: 0,
208 | refY: 0.3,
209 | },
210 | "funcOutCircle": {
211 | ref: 'func',
212 | refX: 0,
213 | refY: 0.7,
214 | },
215 | "toggle": {
216 | ref: 'func',
217 | refDx: 0,
218 | refY: 0.5,
219 | event: 'toggle:codeBlock',
220 | targetCodeBlock: myUtils.getCodeBlockShapeId(className, funcName),
221 | },
222 | "toggleText": {
223 | ref: 'toggle',
224 | refX: 0.5,
225 | refY: 0.5,
226 | text: '+',
227 | event: 'toggle:codeBlock',
228 | targetCodeBlock: myUtils.getCodeBlockShapeId(className, funcName),
229 | }
230 | }
231 | };
232 | classShape.ports.push(port);
233 | });
234 |
235 | return classShape;
236 | }
237 | }
238 |
239 | export {ClassShape};
240 |
--------------------------------------------------------------------------------
/src/CodeBlockShape.ts:
--------------------------------------------------------------------------------
1 | import hljs from 'highlight.js';
2 | import {CodeData, Utils} from './Utils';
3 |
4 | const myUtils = new Utils();
5 |
6 | type CodeBlockContent = {
7 | language: string, // 程式語言
8 | code: string // 具體代碼
9 | };
10 |
11 | class CodeBlockShape {
12 |
13 | /**
14 | * 根據 CodeData 對象 獲取 AntV 的 HTML 對象集合
15 | * @param codeData CodeData 對象
16 | * @returns HTML 對象集合
17 | */
18 | public createCodeBlockShape(codeData: CodeData) {
19 |
20 | // 結果對象集合
21 | const result = [];
22 |
23 | // 將 CodeData 中的每個 func 的 code 轉換成 AntV 的 HTML 對象
24 | const codeDataFuncs = codeData.funcs;
25 | for (const codeDataFunc of codeDataFuncs) {
26 |
27 | // 獲取 代碼塊字串,並分割成行
28 | const codeText = codeDataFunc.code;
29 | const codeTextLines = codeText.split('\n');
30 |
31 | // 獲取 代碼塊所需高度
32 | const blockHeight = myUtils.getTextHeight(codeDataFunc.code) + 30;
33 |
34 | // 計算代碼塊所需寬度
35 | let blockWidth = 0;
36 | for (let i = 0; i < codeTextLines.length; i++) {
37 | blockWidth = Math.max(blockWidth, myUtils.getTextWidth(codeTextLines[i], "13px") + 60);
38 | }
39 |
40 | // 構建 HTML 對象
41 | const htmlShape = {
42 | id: myUtils.getCodeBlockShapeId(codeData.className, codeDataFunc.name),
43 | x: 800,
44 | y: 100,
45 | width: blockWidth,
46 | height: blockHeight,
47 | shape: 'html',
48 | attrs: {
49 | body: {
50 | fill: 'white',
51 | stroke: '#666',
52 | }
53 | },
54 | html() {
55 | const wrap = document.createElement('div');
56 | wrap.innerHTML = '' + hljs.highlight(codeText, {language: codeDataFunc.language}).value + '
';
57 | return wrap;
58 | },
59 | };
60 |
61 | // 將 HTML 對象加入到返回結果中
62 | result.push(htmlShape);
63 | }
64 |
65 | return result;
66 | }
67 |
68 | }
69 |
70 | export {CodeBlockShape};
71 | export type {CodeBlockContent};
72 |
73 |
--------------------------------------------------------------------------------
/src/Edge.ts:
--------------------------------------------------------------------------------
1 | import { Shape } from '@antv/x6';
2 | import { CodeData, Utils } from './Utils';
3 |
4 | const myUtils = new Utils();
5 |
6 | class Edge {
7 |
8 | /**
9 | * 根據 CodeData 創建 函數名稱 到 代碼塊圖形 的 連線對象集合
10 | * @param codeData CodeData 對象
11 | * @returns 連線對象集合
12 | */
13 | public createFuncToCodeEdges(codeData: CodeData) {
14 |
15 | const result = [];
16 |
17 | // 遍歷所有的 函數對象,替每個 函數 與 代碼塊圖形 創建 連線對象
18 | for (const func of codeData.funcs) {
19 |
20 | // 創建 連線
21 | const edge = new Shape.Edge({
22 | source: {
23 | cell: myUtils.getClassShapeId(codeData.className, '', 'class'),
24 | port: myUtils.getClassShapeId(codeData.className, func.name, 'function'),
25 | anchor: 'right'
26 | },
27 | target: {
28 | cell: myUtils.getCodeBlockShapeId(codeData.className, func.name),
29 | anchor: 'left'
30 | },
31 | router: {
32 | name: 'er',
33 | args: {
34 | direction: 'L'
35 | },
36 | },
37 | attrs: {
38 | line: {
39 | stroke: "#bdc3c7",
40 | targetMarker: {
41 | name: 'diamond',
42 | width: 0,
43 | height: 0,
44 | },
45 | },
46 | },
47 | });
48 |
49 | // 將 連線對象 加入到 返回結果 中
50 | result.push(edge);
51 | }
52 |
53 | return result;
54 | }
55 |
56 | /**
57 | * 根據 CodeData 創建 函數調用 連線對象 集合
58 | * @param CodeData 對象
59 | * @returns 連線對象 集合
60 | */
61 | public createFuncCallEdges(codeData: CodeData) {
62 |
63 | const result = [];
64 |
65 | // 遍歷 CodeData 中的每個 函數對象
66 | for (const func of codeData.funcs) {
67 |
68 | // 遍歷 函數對象 中的每個 調用函數
69 | for (const call of func.calls) {
70 |
71 | // 創建 連線對象
72 | const edge = new Shape.Edge({
73 | source: {
74 | cell: myUtils.getClassShapeId(codeData.className, '', 'class'),
75 | port: myUtils.getClassShapeId(codeData.className, func.name, 'function'),
76 | anchor: {
77 | name: "left",
78 | args: {
79 | dy: 8
80 | }
81 | }
82 | },
83 | target: {
84 | cell: myUtils.getClassShapeId(call.className, '', 'class'),
85 | port: myUtils.getClassShapeId(call.className, call.functionName, 'function'),
86 | anchor: {
87 | name: "left",
88 | args: {
89 | dy: -8
90 | }
91 | }
92 | },
93 | router: {
94 | name: 'manhattan',
95 | args: {
96 | startDirections: ["left"],
97 | endDirections: ["left"],
98 | padding: 30
99 | },
100 | },
101 | attrs: {
102 | line: {
103 | stroke: "#2bb37b",
104 | },
105 | },
106 | });
107 |
108 | // 將 連線對象 加入到 返回結果 中
109 | result.push(edge);
110 | }
111 | }
112 |
113 | return result;
114 | }
115 |
116 | }
117 |
118 | export { Edge };
119 |
--------------------------------------------------------------------------------
/src/Graph.ts:
--------------------------------------------------------------------------------
1 | import {Cell, Graph} from '@antv/x6';
2 | import {CodeBlockContent, CodeBlockShape} from './CodeBlockShape';
3 | import {ClassShape} from './ClassShape';
4 | import {CLASS_SHAPE_ID_TAG, CODE_BLOCK_ID_TAG, CodeData, Utils} from './Utils';
5 | import {Edge} from './Edge';
6 |
7 | const myUtils = new Utils();
8 | const codeBlockShape = new CodeBlockShape();
9 | const classShape = new ClassShape();
10 | const edge = new Edge();
11 |
12 | export const initGraph = function (codeBlocks: CodeBlockContent[]) {
13 |
14 | // 創建畫布
15 | const graph = new Graph({
16 | container: document.getElementById('container'),
17 | width: 3000,
18 | height: 5000,
19 | background: {
20 | color: '#ffffff', // 设置画布背景颜色
21 | },
22 | grid: {
23 | size: 10, // 網格大小 10px
24 | visible: true, // 渲染網格背景
25 | },
26 | });
27 |
28 | // 將 代碼塊字串 集合 解析成 CodeData 對象集合
29 | let codeDatas: CodeData[] = [];
30 | codeBlocks.forEach((codeBlock: CodeBlockContent) => {
31 | const codeData: CodeData = myUtils.parseCodeData(codeBlock);
32 | codeDatas.push(codeData);
33 | });
34 |
35 | // 將相同 className 的對象進行合併
36 | codeDatas = myUtils.mergeCodeDatas(codeDatas);
37 |
38 | // 設置 CodeDataFunc 中的 calleds 屬性
39 | myUtils.setCalleds(codeDatas);
40 |
41 | // 將 CodeData 對象列表 繪製成 類圖形、代碼塊圖形
42 | for (const codeData of codeDatas) {
43 |
44 | // 將 類圖形、代碼塊圖形 加入到 畫布 中
45 | const funcNames = codeData.funcs.map(item => item.name);
46 | graph.addNode(classShape.createClassShape(codeData.className, funcNames));
47 | graph.addNodes(codeBlockShape.createCodeBlockShape(codeData));
48 |
49 | // 創建 函數名稱 與 代碼塊圖形 的 連線
50 | graph.addEdges(edge.createFuncToCodeEdges(codeData));
51 |
52 | }
53 |
54 | // 繪製 CodeData 中的 函數調用 連線
55 | for (const codeData of codeDatas) {
56 | graph.addEdges(edge.createFuncCallEdges(codeData));
57 | }
58 |
59 | // 綁定 開啟/關閉 代碼塊圖形 事件
60 | // @ts-ignore
61 | graph.on('toggle:codeBlock', ({e}) => {
62 |
63 | // 獲取 代碼塊圖形、開關文字 對象
64 | const codeBlock = graph.getCellById(e.currentTarget.getAttribute('target-code-block'));
65 | const toggleText = e.currentTarget.parentNode.childNodes[5].children[0];
66 |
67 | // 如果當前 代碼塊圖形 正在顯示,則關閉。反之則開啟
68 | if (codeBlock.getProp().visible) {
69 | codeBlock.hide();
70 | toggleText.textContent = '+';
71 | toggleText.setAttribute('dy', '0.3em');
72 | } else {
73 |
74 | // 計算 代碼塊 顯示位置
75 | const intervalX = 100;
76 | const posX = e.offsetX + intervalX;
77 | const posY = e.offsetY - (codeBlock.getProp().size.height / 2);
78 | codeBlock.prop("position", {x: posX, y: posY});
79 |
80 | // 顯示 代碼塊,並更新按鈕內容
81 | codeBlock.show();
82 | toggleText.textContent = '-';
83 | toggleText.setAttribute('dy', '0.3em');
84 | }
85 | });
86 |
87 | // 最大的 類圖形 的寬度
88 | let maxClassShapeWidth = 0;
89 | // 所有的 類圖形 集合
90 | const classShapes: Cell[] = [];
91 |
92 | // 遍歷 所有的圖形
93 | for (const node of graph.getCells()) {
94 |
95 | // 默認隱藏所有的 代碼塊
96 | if (node.id.startsWith(CODE_BLOCK_ID_TAG)) {
97 | node.hide();
98 | }
99 |
100 | // 計算 類圖形 最大寬度、加入到集合中
101 | if (node.id.startsWith(CLASS_SHAPE_ID_TAG)) {
102 | maxClassShapeWidth = Math.max(maxClassShapeWidth, node.getProp().size.width);
103 | classShapes.push(node);
104 | }
105 | }
106 |
107 | /**
108 | * 計算 類圖形 的 座標
109 | */
110 | // 左側保留寬度
111 | const marginLeftWidth = 100;
112 | // 類圖形的 Y 座標
113 | let curY = 200;
114 | // 多個 類圖形 的 上下間格
115 | const intervalY = 150;
116 | // 所有 類圖形 的 中線 X 座標
117 | const midX = marginLeftWidth + (maxClassShapeWidth / 2);
118 | for (const classShape of classShapes) {
119 |
120 | // 計算 類圖形 的 左上X 座標,並設置
121 | const posX = midX - (classShape.getProp().size.width / 2);
122 | classShape.prop("position", {x: posX, y: curY});
123 |
124 | // 下個 類圖形 的 Y 座標 = 當前 類圖形 高度 + 間隔
125 | curY = curY + classShape.getProp().size.height + intervalY;
126 | }
127 |
128 | };
129 |
130 |
131 |
--------------------------------------------------------------------------------
/src/SourceCodeView.ts:
--------------------------------------------------------------------------------
1 | import {ItemView, WorkspaceLeaf} from "obsidian";
2 | import {initGraph} from "./Graph";
3 | import {SOURCE_CODE_VIEW_TYPE} from "./main";
4 | import {CodeBlockContent} from "./CodeBlockShape";
5 |
6 | export default class SourceCodeView extends ItemView {
7 |
8 | // 代碼塊內容 集合
9 | codeBlocks: CodeBlockContent[];
10 |
11 | constructor(codeBlocks: CodeBlockContent[], leaf: WorkspaceLeaf) {
12 | super(leaf);
13 | this.codeBlocks = codeBlocks;
14 | }
15 |
16 | getDisplayText(): string {
17 | return "Source Code View";
18 | }
19 |
20 | getViewType(): string {
21 | return SOURCE_CODE_VIEW_TYPE;
22 | }
23 |
24 | async onOpen() {
25 |
26 | // 清空 容器內容
27 | const container = this.containerEl.children[1];
28 | container.empty();
29 |
30 | // 創建 AntV 需要的容器
31 | const div = document.createElement("div");
32 | div.id = 'container';
33 | container.appendChild(div);
34 |
35 | // 初始化 AntV 的 畫布
36 | initGraph(this.codeBlocks);
37 | }
38 |
39 | async onClose() {
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/Utils.ts:
--------------------------------------------------------------------------------
1 | import hljs from "highlight.js";
2 | import { CodeBlockContent } from "./CodeBlockShape";
3 |
4 | type CodeData = {
5 | className: string, // 類名稱
6 | funcs: CodeDataFunc[] // 函數名稱 與 代碼塊字串 集合
7 | };
8 |
9 | type CodeDataFunc = {
10 | language: string, // 程式語言
11 | name: string, // 函數名稱
12 | code: string, // 代碼塊字串
13 | calls: CodeDataFuncCall[], // 調用的函數集合
14 | calleds: CodeDataFuncCall[], // 被調用的函數集合
15 | };
16 |
17 | type CodeDataFuncCall = {
18 | className: string, // 類名稱
19 | functionName: string // 函數名稱
20 | };
21 |
22 | export const CLASS_SHAPE_ID_TAG = "class-";
23 | export const CODE_BLOCK_ID_TAG = 'code-block-';
24 |
25 | // Key: 函數的唯一標示符,Value: 函數對象
26 | const funcNameToFunc = new Map();
27 |
28 | class Utils {
29 |
30 | /**
31 | * 計算字串寬度
32 | */
33 | tCanvas: any = null;
34 |
35 | public getTextWidth(text: string, font: string) {
36 | // re-use canvas object for better performance
37 | const canvas = this.tCanvas || (this.tCanvas = document.createElement('canvas'));
38 | const context = canvas.getContext('2d');
39 | context.font = font + ' ui-sans-serif';
40 | return context.measureText(text).width;
41 | }
42 |
43 | /**
44 | * 計算 HTML 代碼塊的高度
45 | * @param codeText
46 | */
47 | public getTextHeight(codeText: string) {
48 |
49 | // 創建一個新容器,將 HTML 代碼加入到容器中
50 | const node = document.createElement('div');
51 | node.innerHTML = '' + hljs.highlight(codeText, { language: 'java' }).value + '
';
52 | document.body.appendChild(node);
53 |
54 | // 計算 容器高度
55 | const height: number = parseInt(global.getComputedStyle(node).height.split('px')[0]);
56 |
57 | // 移除容器
58 | document.body.removeChild(node);
59 |
60 | return height;
61 | }
62 |
63 | /**
64 | * 根據 代碼塊字串 解析出 CodeData 對象
65 | * @returns CodeData 對象
66 | * @param codeBlockContent 代碼塊內容
67 | */
68 | public parseCodeData(codeBlockContent: CodeBlockContent): CodeData {
69 |
70 | const lang = codeBlockContent.language;
71 | const content = codeBlockContent.code;
72 |
73 | // 類名、方法名 的 標示符
74 | const classTag = '@class';
75 | const functionTag = '@function';
76 | const callTag = '@call';
77 |
78 | const codeLines = content.split('\n');
79 |
80 | // 取得 註釋塊 的代碼
81 | const commentLines = [];
82 | for (let i = 0; i < codeLines.length; i++) {
83 | const codeLine = codeLines[i];
84 |
85 | // 找到註釋開頭,則從當前行開始,加入到註釋塊列表中
86 | if (codeLine.indexOf('/**') != -1) {
87 | commentLines.push(codeLine);
88 | } else if (commentLines.length > 0) {
89 | commentLines.push(codeLine);
90 | }
91 |
92 | // 當碰到第一個多行註釋的結尾,則跳出循環
93 | if (codeLine.indexOf('*/') != -1) {
94 | break;
95 | }
96 | }
97 |
98 |
99 | // 從 註釋塊 中獲取 類名、方法名、調用的函數集合
100 | let className = '';
101 | const calls: CodeDataFuncCall[] = [];
102 | const calleds: CodeDataFuncCall[] = [];
103 | let codeDataFunc = {
104 | language: lang,
105 | name: '',
106 | code: content,
107 | calls: calls,
108 | calleds: calleds
109 | };
110 |
111 | commentLines.forEach((comment) => {
112 |
113 | // 獲取 類名
114 | if (comment.indexOf(classTag) != -1) {
115 | className = comment.substring(comment.indexOf(classTag) + classTag.length).trim();
116 | }
117 |
118 | // 獲取 方法名
119 | if (comment.indexOf(functionTag) != -1) {
120 | codeDataFunc.name = comment.substring(comment.indexOf(functionTag) + functionTag.length).trim();
121 |
122 | // 保存 到 共用 Map 中
123 | funcNameToFunc.set(this.getClassShapeId(className, codeDataFunc.name, 'function'), codeDataFunc);
124 | }
125 |
126 | // 獲取 調用的函數
127 | if (comment.indexOf(callTag) != -1) {
128 | const callStr = comment.substring(comment.indexOf(callTag) + callTag.length).trim();
129 | const call = {
130 | className: callStr.substring(0, callStr.indexOf('@')).trim(),
131 | functionName: callStr.substring(callStr.indexOf('@') + 1).trim()
132 | }
133 | calls.push(call);
134 | }
135 |
136 | });
137 |
138 | // 封裝結果
139 | const result: CodeData = {
140 | className: className,
141 | funcs: [codeDataFunc]
142 | };
143 |
144 | return result;
145 | }
146 |
147 | /**
148 | * 獲取 類聲明 的 圖形Id
149 | * @param className 類名
150 | * @param functionName 方法名
151 | * @param type Id 的類型
152 | * @returns 圖形Id
153 | */
154 | public getClassShapeId(className: string, functionName: string, type: 'class' | 'function'): string {
155 | if (type == 'class') {
156 | return CLASS_SHAPE_ID_TAG + className;
157 | } else if (type == 'function') {
158 | return CLASS_SHAPE_ID_TAG + className + '-' + functionName;
159 | }
160 | return '';
161 | }
162 |
163 | /**
164 | * 獲取 代碼塊 的 圖形Id
165 | * @param className 類名
166 | * @param functionName 方法名
167 | * @returns 圖形Id
168 | */
169 | public getCodeBlockShapeId(className: string, functionName: string): string {
170 | return CODE_BLOCK_ID_TAG + className + '-' + functionName;
171 | }
172 |
173 | /**
174 | * 將擁有相同 ClassName 的 CodeDataFunc 合併到一起
175 | * @param codeDatas 要進行合併的 CodeData 集合
176 | * @returns 合併後的 CodeData 集合
177 | */
178 | public mergeCodeDatas(codeDatas: CodeData[]): CodeData[] {
179 |
180 | // 對有相同 className 的 CodeData 的 func 進行合併
181 | const classNameToCodeData = new Map();
182 | codeDatas.forEach((codeData: CodeData) => {
183 |
184 | // 如果在 Map 中已存在相應的 CodeData,則將 func 進行合併
185 | if (classNameToCodeData.has(codeData.className)) {
186 | const tempCodeData = classNameToCodeData.get(codeData.className) as CodeData;
187 | tempCodeData.funcs.push(codeData.funcs[0]);
188 | }
189 | // 如果在 Map 中還沒有相應的 CodeData,則將當前 CodeData 加入到 Map 中
190 | else {
191 | classNameToCodeData.set(codeData.className, codeData);
192 | }
193 | });
194 |
195 | // 封裝返回結果
196 | const result: CodeData[] = [];
197 | for (const codeData of Array.from(classNameToCodeData.values())) {
198 | result.push(codeData);
199 | }
200 |
201 | return result;
202 | }
203 |
204 | /**
205 | * 設置所有 CodeData 中的 CodeDataFunc 的 calleds 屬性
206 | * @param codeDatas 要進行處理的 CodeData 集合
207 | */
208 | public setCalleds(codeDatas: CodeData[]) {
209 |
210 | // 遍歷 呼叫函數 的 對象屬性
211 | for (const codeData of codeDatas) {
212 | for (const codeDataFunc of codeData.funcs) {
213 | for (const call of codeDataFunc.calls) {
214 |
215 | // 取得 被調用的 函數對象
216 | let calledCodeDataFunc: CodeDataFunc | undefined = funcNameToFunc.get(this.getClassShapeId(call.className, call.functionName, 'function'));
217 |
218 | // 將 呼叫函數對象的屬性 加入到 被調用的函數對象 的 被調用函數集合 中
219 | calledCodeDataFunc?.calleds.push({
220 | className: codeDataFunc.name,
221 | functionName: codeData.className
222 | });
223 | }
224 | }
225 | }
226 | }
227 | }
228 |
229 |
230 | export { Utils };
231 | export type { CodeData, CodeDataFunc };
232 |
233 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import {Plugin, WorkspaceLeaf} from 'obsidian';
2 | import SourceCodeView from "./SourceCodeView";
3 | import {CodeBlockContent} from "./CodeBlockShape";
4 |
5 |
6 | export const SOURCE_CODE_VIEW_TYPE = 'source-code-view';
7 | export default class SourceCodeViewPlugin extends Plugin {
8 |
9 | // Obsidian 中 Markdown 的 代碼塊內容 集合
10 | codeBlockContents: CodeBlockContent[] = [];
11 |
12 | /**
13 | * 載入插件時,註冊相關組件
14 | */
15 | async onload() {
16 |
17 | // 註冊 Obsidian 的 View
18 | this.registerView(
19 | SOURCE_CODE_VIEW_TYPE,
20 | (leaf: WorkspaceLeaf) => (new SourceCodeView(this.codeBlockContents, leaf))
21 | );
22 |
23 | // 註冊 Obsidian 左側的按鈕
24 | this.addRibbonIcon("file-json", "Show Resource Code View", () => {
25 | this.initView();
26 | });
27 | }
28 |
29 | /**
30 | * 初始化 Obsidian 的 View 頁面
31 | */
32 | initView() {
33 |
34 | // 當 ResourceCodeView 頁面已開啟,則返回
35 | if (this.app.workspace.getLeavesOfType(SOURCE_CODE_VIEW_TYPE).length > 0) {
36 | return;
37 | }
38 |
39 | // 找不到激活的文件,則返回
40 | const activeFile = this.app.workspace.getActiveFile();
41 | if (activeFile == null) {
42 | return;
43 | }
44 | // 處理文件內容
45 | this.app.vault.process(activeFile, (data) => {
46 |
47 | // 清理數據
48 | this.codeBlockContents = [];
49 |
50 | // 獲取文件內所有的 Markdown 代碼塊
51 | const regex = /```(\w+)\s([\s\S]*?)```/gm;
52 | let match;
53 | while ((match = regex.exec(data)) !== null) {
54 | const lang = match[1]; // 程式語言
55 | const content = match[2]; // 代碼塊內容
56 | this.codeBlockContents.push({language: lang, code: content});
57 | }
58 |
59 | // 開啟分頁
60 | const preview = this.app.workspace.getLeaf('split', 'vertical');
61 | const mmPreview = new SourceCodeView(this.codeBlockContents, preview);
62 | preview.open(mmPreview);
63 |
64 | // 不更改文件內容
65 | return data;
66 | });
67 |
68 | }
69 |
70 | onunload() {
71 | }
72 | }
73 |
74 |
75 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | pre {
2 | font-size: 13px;
3 | color: #000000;
4 | font-family: ui-sans-serif;
5 | white-space: pre;
6 | }
7 |
8 | pre code.hljs {
9 | display: block;
10 | overflow-x: auto;
11 | padding: 1em;
12 | }
13 |
14 | code.hljs {
15 | padding: 3px 5px;
16 | }
17 |
18 | .hljs {
19 | background: #f3f3f3;
20 | color: #444
21 | }
22 |
23 | .hljs-comment {
24 | color: #697070
25 | }
26 |
27 | .hljs-punctuation,.hljs-tag {
28 | color: #444a
29 | }
30 |
31 | .hljs-tag .hljs-attr,.hljs-tag .hljs-name {
32 | color: #444
33 | }
34 |
35 | .hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag {
36 | font-weight: 700
37 | }
38 |
39 | .hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type {
40 | color: #800
41 | }
42 |
43 | .hljs-section,.hljs-title {
44 | color: #800;
45 | font-weight: 700
46 | }
47 |
48 | .hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable {
49 | color: #ab5656
50 | }
51 |
52 | .hljs-literal {
53 | color: #695
54 | }
55 |
56 | .hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code {
57 | color: #397300
58 | }
59 |
60 | .hljs-meta {
61 | color: #1f7199
62 | }
63 |
64 | .hljs-meta .hljs-string {
65 | color: #38a
66 | }
67 |
68 | .hljs-emphasis {
69 | font-style: italic
70 | }
71 |
72 | .hljs-strong {
73 | font-weight: 700
74 | }
75 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "inlineSourceMap": true,
5 | "inlineSources": true,
6 | "module": "ESNext",
7 | "target": "ES6",
8 | "allowJs": true,
9 | "noImplicitAny": true,
10 | "moduleResolution": "node",
11 | "importHelpers": true,
12 | "isolatedModules": true,
13 | "strictNullChecks": true,
14 | "lib": [
15 | "DOM",
16 | "ES5",
17 | "ES6",
18 | "ES7"
19 | ]
20 | },
21 | "include": [
22 | "**/*.ts"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/version-bump.mjs:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from "fs";
2 |
3 | const targetVersion = process.env.npm_package_version;
4 |
5 | // read minAppVersion from manifest.json and bump version to target version
6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
7 | const { minAppVersion } = manifest;
8 | manifest.version = targetVersion;
9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
10 |
11 | // update versions.json with target version and minAppVersion from manifest.json
12 | let versions = JSON.parse(readFileSync("versions.json", "utf8"));
13 | versions[targetVersion] = minAppVersion;
14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
15 |
--------------------------------------------------------------------------------
/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "1.0.0": "0.15.0"
3 | }
4 |
--------------------------------------------------------------------------------