commit bb83c9206dd1a0ab90c23d31df5a011fed83ef73 Author: Waiting Date: Fri Mar 10 17:44:58 2023 +0800 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..81f3ec3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = 4 +tab_width = 4 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..e019f3c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ + +main.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..0807290 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,23 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "env": { "node": true }, + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "@typescript-eslint/ban-ts-comment": "off", + "no-prototype-builtins": "off", + "@typescript-eslint/no-empty-function": "off" + } + } \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..386ac2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# vscode +.vscode + +# Intellij +*.iml +.idea + +# npm +node_modules + +# Don't include the compiled main.js file in the repo. +# They should be uploaded to GitHub releases instead. +main.js + +# Exclude sourcemaps +*.map + +# obsidian +data.json + +# Exclude macOS Finder (System Explorer) View States +.DS_Store diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b973752 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +tag-version-prefix="" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..31e5155 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# Obsidian Sample Plugin + +This is a sample plugin for Obsidian (https://obsidian.md). + +This project uses Typescript to provide type checking and documentation. +The repo depends on the latest plugin API (obsidian.d.ts) in Typescript Definition format, which contains TSDoc comments describing what it does. + +**Note:** The Obsidian API is still in early alpha and is subject to change at any time! + +This sample plugin demonstrates some of the basic functionality the plugin API can do. +- Changes the default font color to red using `styles.css`. +- Adds a ribbon icon, which shows a Notice when clicked. +- Adds a command "Open Sample Modal" which opens a Modal. +- Adds a plugin setting tab to the settings page. +- Registers a global click event and output 'click' to the console. +- Registers a global interval which logs 'setInterval' to the console. + +## First time developing plugins? + +Quick starting guide for new plugin devs: + +- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with. +- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it). +- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder. +- Install NodeJS, then run `npm i` in the command line under your repo folder. +- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`. +- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`. +- Reload Obsidian to load the new version of your plugin. +- Enable plugin in settings window. +- For updates to the Obsidian API run `npm update` in the command line under your repo folder. + +## Releasing new releases + +- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release. +- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible. +- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases +- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release. +- Publish the release. + +> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`. +> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json` + +## Adding your plugin to the community plugin list + +- Check https://github.com/obsidianmd/obsidian-releases/blob/master/plugin-review.md +- Publish an initial version. +- Make sure you have a `README.md` file in the root of your repo. +- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin. + +## How to use + +- Clone this repo. +- `npm i` or `yarn` to install dependencies +- `npm run dev` to start compilation in watch mode. + +## Manually installing the plugin + +- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`. + +## Improve code quality with eslint (optional) +- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code. +- To use eslint with this project, make sure to install eslint from terminal: + - `npm install -g eslint` +- To use eslint to analyze this project use this command: + - `eslint main.ts` + - eslint will then create a report with suggestions for code improvement by file and line number. +- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder: + - `eslint .\src\` + +## Funding URL + +You can include funding URLs where people who use your plugin can financially support it. + +The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file: + +```json +{ + "fundingUrl": "https://buymeacoffee.com" +} +``` + +If you have multiple URLs, you can also do: + +```json +{ + "fundingUrl": { + "Buy Me a Coffee": "https://buymeacoffee.com", + "GitHub Sponsor": "https://github.com/sponsors", + "Patreon": "https://www.patreon.com/" + } +} +``` + +## API Documentation + +See https://github.com/obsidianmd/obsidian-api diff --git a/classShape.ts b/classShape.ts new file mode 100644 index 0000000..dfe906d --- /dev/null +++ b/classShape.ts @@ -0,0 +1,192 @@ +import { Graph } from '@antv/x6'; +import { utils } from './utils' + +const myUtils = new utils() + +/** + * 註冊 類聲明 中 方法名稱位置 計算函數 + */ +Graph.registerPortLayout( + 'classFuncPosition', + (portsPositionArgs, elemBBox) => { + return portsPositionArgs.map((_, index) => { + return { + position: { + x: 0, + y: (index + 1) * elemBBox.height, + }, + angle: 0, + } + }) + }, + //true, +) + +/** + * 類聲明 圖形定義 + */ +Graph.registerNode( + 'clazz-shape', + { + // 最上層 class 名稱 + inherit: 'rect', + markup: [ + { + tagName: 'rect', + selector: 'body', + }, + { + tagName: 'text', + selector: 'label', + }, + ], + attrs: { + rect: { + strokeWidth: 1, + stroke: '#e5885c', + fill: '#fd9a6c', + }, + label: { + fontWeight: 'bold', + fill: '#ffffff', + fontSize: 14, + textAnchor: 'middle', + textVerticalAnchor: 'middle' + }, + }, + // 該 class 所屬的方法列表 + ports: { + groups: { + list: { + position: 'classFuncPosition', + markup: [ + { + tagName: 'rect', + selector: 'func', + }, + { + tagName: 'text', + selector: 'funcName', + }, + { + tagName: 'circle', + selector: 'toggle' + }, + { + tagName: 'text', + selector: 'toggleText' + } + ], + attrs: { + func: { + strokeWidth: 1, + stroke: '#e5885c', + fill: '#FFFFFF', + }, + funcName: { + fontSize: 13, + textAnchor: 'middle', + textVerticalAnchor: 'middle' + }, + toggle: { + r: 7, + stroke: '#b2bec3', + strokeWidth: 2, + fill: '#fff', + cursor: 'pointer', + }, + toggleText: { + fontSize: 12, + fontWeight: 800, + fill: '#b2bec3', + textAnchor: 'middle', + textVerticalAnchor: 'middle', + 'pointer-events': 'none', // 避免按鈕不靈敏 + cursor: 'pointer', + } + }, + }, + }, + }, + }, + true, +) + +class ClassShape { + + /** + * 根據 類名、方法名稱集合 創建 類聲明圖形 + * @param className 類名 + * @param funcNames 方法名稱集合 + * @returns 類聲明圖形 + */ + public createClassShape(className: string, funcNames: string[]) { + + const height = 30 + + // 計算 類聲明 圖形寬度 + let maxWidth = 100 + maxWidth = Math.max(maxWidth, myUtils.getTextWidth(className, "14px bold") + 60) + funcNames.forEach((funcName: string) => { + maxWidth = Math.max(maxWidth, myUtils.getTextWidth(funcName, "13px") + 20) + }) + + // 定義頂部類名稱方塊 + let classShape = { + "id": myUtils.getClassShapeId(className, '', 'class'), + "shape": 'clazz-shape', + "label": className, + "width": maxWidth, + "height": height, + "fontSize": 14, + "position": { + "x": 100, + "y": 100 + }, + "ports": [{}] + } + classShape.ports = [] + + + // 定義下方函數名稱方塊 + funcNames.forEach((funcName: string) => { + + let port = { + "id": myUtils.getClassShapeId(className, funcName, 'function'), + "group": "list", + "attrs": { + "func": { + width: maxWidth, + height: height, + }, + "funcName": { + ref: 'func', + refX: '50%', + refY: '50%', + text: funcName, + }, + "toggle": { + ref: 'func', + refDx: 0, + refY: 0.5, + event: 'toggle:codeBlock', + targetCodeBlock: myUtils.getCodeBlockShapeId(className, funcName), + }, + "toggleText": { + ref: 'toggle', + refX: 0.5, + refY: 0.5, + text: '+', + event: 'toggle:codeBlock', + targetCodeBlock: myUtils.getCodeBlockShapeId(className, funcName), + } + } + } + classShape.ports.push(port) + }) + + return classShape + } +} + +export { ClassShape } diff --git a/codeBlockShape.ts b/codeBlockShape.ts new file mode 100644 index 0000000..69ff653 --- /dev/null +++ b/codeBlockShape.ts @@ -0,0 +1,66 @@ +import hljs from 'highlight.js'; +import { utils, CodeData } from './utils' + +const myUtils = new utils() + +class CodeBlockShape { + + /** + * 根據 CodeData 對象 獲取 AntV 的 HTML 對象集合 + * @param codeData CodeData 對象 + * @returns HTML 對象集合 + */ + public createCodeBlockShape(codeData: CodeData) { + + // 結果對象集合 + let result = [] + + // 將 CodeData 中的每個 func 的 code 轉換成 AntV 的 HTML 對象 + let codeDataFuncs = codeData.funcs + for (const codeDataFunc of codeDataFuncs) { + + // 獲取 代碼塊字串,並分割成行 + let codeText = codeDataFunc.code + let codeTextLines = codeText.split('\n') + + // 計算代碼塊所需高度 + const blockHeight = codeTextLines.length * 15 + 40; + + // 計算代碼塊所需寬度 + let blockWidth = 0 + for (let i = 0; i < codeTextLines.length; i++) { + blockWidth = Math.max(blockWidth, myUtils.getTextWidth(codeTextLines[i], "13px ui-sans-serif") + 60) + } + + // 構建 HTML 對象 + const htmlShape = { + id: myUtils.getCodeBlockShapeId(codeData.className, codeDataFunc.name), + x: 800, + y: 100, + width: blockWidth, + height: blockHeight, + shape: 'html', + attrs: { + body: { + fill: 'white', + stroke: '#666', + } + }, + html() { + const wrap = document.createElement('div') + wrap.innerHTML = '
' + hljs.highlight(codeText, { language: 'java' }).value + '
' + return wrap + }, + } + + // 將 HTML 對象加入到返回結果中 + result.push(htmlShape) + } + + return result; + } + +} + + +export { CodeBlockShape } diff --git a/edge.ts b/edge.ts new file mode 100644 index 0000000..182c681 --- /dev/null +++ b/edge.ts @@ -0,0 +1,107 @@ +import { Shape } from '@antv/x6'; +import { CodeData, utils } from './utils' + +const myUtils = new utils() + +class Edge { + + /** + * 根據 CodeData 創建 函數名稱 到 代碼塊圖形 的 連線對象集合 + * @param codeData CodeData 對象 + * @returns 連線對象集合 + */ + public createFuncToCodeEdges(codeData: CodeData) { + + let result = []; + + // 遍歷所有的 函數對象,替每個 函數 與 代碼塊圖形 創建 連線對象 + for (const func of codeData.funcs) { + + // 創建 連線 + const edge = new Shape.Edge({ + source: { + cell: myUtils.getClassShapeId(codeData.className, '', 'class'), + port: myUtils.getClassShapeId(codeData.className, func.name, 'function'), + anchor: 'right' + }, + target: { + cell: myUtils.getCodeBlockShapeId(codeData.className, func.name), + anchor: 'left' + }, + router: { + name: 'er', + args: { + direction: 'L' + }, + }, + attrs: { + line: { + stroke: "#bdc3c7", + targetMarker: { + name: 'diamond', + width: 0, + height: 0, + }, + }, + }, + }) + + // 將 連線對象 加入到 返回結果 中 + result.push(edge) + } + + return result; + } + + /** + * 根據 CodeData 創建 函數調用 連線對象 集合 + * @param CodeData 對象 + * @returns 連線對象 集合 + */ + public createFuncCallEdges(codeData: CodeData) { + + let result = []; + + // 遍歷 CodeData 中的每個 函數對象 + for (const func of codeData.funcs) { + + // 遍歷 函數對象 中的每個 調用函數 + for (const call of func.calls) { + + // 創建 連線對象 + const edge = new Shape.Edge({ + source: { + cell: myUtils.getClassShapeId(codeData.className, '', 'class'), + port: myUtils.getClassShapeId(codeData.className, func.name, 'function'), + anchor: 'left' + }, + target: { + cell: myUtils.getClassShapeId(call.className, '', 'class'), + port: myUtils.getClassShapeId(call.className, call.functionName, 'function'), + anchor: 'left' + }, + router: { + name: 'oneSide', + args: { + side: 'left', + padding: 30 + }, + }, + attrs: { + line: { + stroke: "#2bb37b", + }, + }, + }) + + // 將 連線對象 加入到 返回結果 中 + result.push(edge) + } + } + + return result; + } + +} + +export { Edge } \ No newline at end of file diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..b13282b --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,48 @@ +import esbuild from "esbuild"; +import process from "process"; +import builtins from "builtin-modules"; + +const banner = +`/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ +`; + +const prod = (process.argv[2] === "production"); + +const context = await esbuild.context({ + banner: { + js: banner, + }, + entryPoints: ["main.ts"], + bundle: true, + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", + ...builtins], + format: "cjs", + target: "es2018", + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..25713ca --- /dev/null +++ b/index.ts @@ -0,0 +1,204 @@ +import {Cell, Graph, Shape} from '@antv/x6'; +import {CodeBlockShape} from './codeBlockShape'; +import {ClassShape} from './classShape'; +import {CLASS_SHAPE_ID_TAG, CODE_BLOCK_ID_TAG, CodeData, utils} from './utils'; +import {Edge} from './edge' + +let codeBlocks = [` + /** + * listen plugin handler event and handle plugin. + * @class ShenyuWebHandler + * @function onApplicationEvent(final PluginHandlerEvent event) + * @param event sort plugin event + */ + @Override + public void onApplicationEvent(final PluginHandlerEvent event) { + PluginHandlerEventEnum stateEnums = event.getPluginStateEnums(); + PluginData pluginData = (PluginData) event.getSource(); + switch (stateEnums) { + case ENABLED: + onPluginEnabled(pluginData); + break; + case DELETE: + case DISABLED: + // disable or removed plugin. + onPluginRemoved(pluginData); + break; + case SORTED: + // copy a new one, or there will be concurrency problems + onSortedPlugins(); + break; + default: + throw new IllegalStateException("Unexpected value: " + event.getPluginStateEnums()); + } + onSortedPlugins(); + } + `, + ` + /** + * handler error. + * + * @class ShenyuWebHandler + * @function handle(@NonNull final ServerWebExchange exchange, @NonNull final Throwable throwable) + * @call ShenyuPluginLoader @ getInstance() + * + * @param exchange the exchange + * @param throwable the throwable + * @return error result + */ + @Override + @NonNull + public Mono handle(@NonNull final ServerWebExchange exchange, @NonNull final Throwable throwable) { + LOG.error("handle error: {}{}", exchange.getLogPrefix(), formatError(throwable, exchange.getRequest()), throwable); + HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + if (throwable instanceof ResponseStatusException) { + httpStatus = ((ResponseStatusException) throwable).getStatus(); + } + exchange.getResponse().setStatusCode(httpStatus); + Object error = ShenyuResultWrap.error(exchange, httpStatus.value(), httpStatus.getReasonPhrase(), throwable); + return WebFluxResultUtils.result(exchange, error); + } + `, + ` + /** + * Get plugin loader instance. + * + * @class ShenyuPluginLoader + * @function getInstance() + * + * @return plugin loader instance + */ + public static ShenyuPluginLoader getInstance() { + if (null == pluginLoader) { + synchronized (ShenyuPluginLoader.class) { + if (null == pluginLoader) { + pluginLoader = new ShenyuPluginLoader(); + } + } + } + return pluginLoader; + } + `] + +const myUtils = new utils() +const codeBlockShape = new CodeBlockShape() +const classShape = new ClassShape() +const edge = new Edge() + +// window.onload = function () { +export let indexInit = function (codeBlocks: String[]) { + + // 創建畫布 + const graph = new Graph({ + container: document.getElementById('container'), + width: 2200, + height: 1200, + background: { + color: '#fffbe6', // 设置画布背景颜色 + }, + grid: { + size: 10, // 网格大小 10px + visible: true, // 渲染网格背景 + }, + }); + + // 將 代碼塊字串 集合 解析成 CodeData 對象集合,並將相同 className 的對象進行合併 + let codeDatas: CodeData[] = [] + codeBlocks.forEach((codeBlock: string) => { + let codeData: CodeData = myUtils.parseCodeData(codeBlock) + codeDatas.push(codeData) + }) + codeDatas = myUtils.mergeCodeDatas(codeDatas) + + + // 將 CodeData 對象列表 繪製成 類圖形、代碼塊圖形 + for (const codeData of codeDatas) { + + // 將 類圖形、代碼塊圖形 加入到 畫布 中 + let funcNames = codeData.funcs.map(item => item.name) + graph.addNode(classShape.createClassShape(codeData.className, funcNames)) + graph.addNodes(codeBlockShape.createCodeBlockShape(codeData)) + + // 創建 函數名稱 與 代碼塊圖形 的 連線 + graph.addEdges(edge.createFuncToCodeEdges(codeData)) + + } + + // 繪製 CodeData 中的 函數調用 連線 + for (const codeData of codeDatas) { + graph.addEdges(edge.createFuncCallEdges(codeData)) + } + + // 綁定 開啟/關閉 代碼塊圖形 事件 + graph.on('toggle:codeBlock', ({e, node}) => { + + console.dir(e) + console.dir(node) + + // 獲取 代碼塊圖形、開關文字 對象 + let codeBlock = graph.getCellById(e.currentTarget.getAttribute('target-code-block')) + let toggleText = e.currentTarget.parentNode.childNodes[3].children[0] + + // 如果當前 代碼塊圖形 正在顯示,則關閉。反之則開啟 + if (codeBlock.getProp().visible) { + codeBlock.hide() + toggleText.textContent = '+' + toggleText.setAttribute('dy', '0.3em') + } else { + + // 計算 代碼塊 顯示位置 + const intervalX = 200 + const posX = e.offsetX + intervalX + const posY = e.offsetY - (codeBlock.getProp().size.height / 2) + codeBlock.prop("position", {x: posX, y: posY}) + + codeBlock.show() + toggleText.textContent = '-' + toggleText.setAttribute('dy', '0.2em') + } + }) + + // 最大的 類圖形 的寬度 + let maxClassShapeWidth = 0; + // 所有的 類圖形 集合 + let classShapes: Cell[] = []; + + // 遍歷 所有的圖形 + for (const node of graph.getCells()) { + + // 默認隱藏所有的 代碼塊 + if (node.id.startsWith(CODE_BLOCK_ID_TAG)) { + node.hide() + } + + // 計算 類圖形 最大寬度、加入到集合中 + if (node.id.startsWith(CLASS_SHAPE_ID_TAG)) { + maxClassShapeWidth = Math.max(maxClassShapeWidth, node.getProp().size.width) + classShapes.push(node) + } + } + + /** + * 計算 類圖形 的 座標 + */ + // 左側保留寬度 + const marginLeftWidth = 100 + // 類圖形的 Y 座標 + let curY = 200 + // 多個 類圖形 的 上下間格 + const intervalY = 150 + // 所有 類圖形 的 中線 X 座標 + const midX = marginLeftWidth + (maxClassShapeWidth / 2) + for (const classShape of classShapes) { + + // 計算 類圖形 的 左上X 座標,並設置 + const posX = midX - (classShape.getProp().size.width / 2) + classShape.prop("position", {x: posX, y: curY}) + + // 下個 類圖形 的 Y 座標 = 當前 類圖形 高度 + 間隔 + curY = curY + classShape.getProp().size.height + intervalY + } + +} + + diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..eff152b --- /dev/null +++ b/main.ts @@ -0,0 +1,67 @@ +import {Plugin, Workspace, WorkspaceLeaf} from 'obsidian'; +import SourceCodeView from "./source-code"; + + +export const SOURCE_CODE_VIEW_TYPE = 'source-code-view' +export default class MyPlugin extends Plugin { + + sourceCodeView: SourceCodeView + codeBlocks: string[] = [] + + async onload() { + this.registerView( + SOURCE_CODE_VIEW_TYPE, + (leaf: WorkspaceLeaf) => + (this.sourceCodeView = new SourceCodeView(this.codeBlocks, leaf)) + ); + + this.addRibbonIcon("dice", "Activate view", () => { + this.initCanvas() + }); + } + + initCanvas() { + + // 當 ResourceCodeView 頁面已開啟,則返回 + if (this.app.workspace.getLeavesOfType(SOURCE_CODE_VIEW_TYPE).length > 0) { + return; + } + + // 找不到激活的文件,則返回 + const activeFile = this.app.workspace.getActiveFile(); + if (activeFile == null) { + return; + } + + // 處理文件內容 + this.app.vault.process(activeFile, (data) => { + + // 清理數據 + this.codeBlocks = [] + + // 獲取文件內所有的 Markdown 代碼塊 + const regex = /```(\w+)\s([\s\S]*?)```/gm; + let match; + while ((match = regex.exec(data)) !== null) { + const lang = match[1]; // 程式語言 + const codeBlockData = match[2]; // 代碼塊內容 + this.codeBlocks.push(codeBlockData) + } + + // 開啟分頁 + const preview = this.app.workspace.getLeaf('split', 'vertical') + const mmPreview = new SourceCodeView(this.codeBlocks, preview); + preview.open(mmPreview) + + // 不更改文件內容 + return data + }); + + } + + onunload() { + + } +} + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..d049ee1 --- /dev/null +++ b/manifest.json @@ -0,0 +1,11 @@ +{ + "id": "obsidian-code-note", + "name": "Source Code Note Plugin", + "version": "1.0.0", + "minAppVersion": "0.15.0", + "description": "This is a sample plugin for Obsidian. This plugin demonstrates some of the capabilities of the Obsidian API.", + "author": "Obsidian", + "authorUrl": "https://obsidian.md", + "fundingUrl": "https://obsidian.md/pricing", + "isDesktopOnly": false +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..65e4699 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "obsidian-sample-plugin", + "version": "1.0.0", + "description": "This is a sample plugin for Obsidian (https://obsidian.md)", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "version": "node version-bump.mjs && git add manifest.json versions.json" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "@types/node": "^16.11.6", + "@typescript-eslint/eslint-plugin": "5.29.0", + "@typescript-eslint/parser": "5.29.0", + "builtin-modules": "3.3.0", + "esbuild": "0.17.3", + "obsidian": "latest", + "tslib": "2.4.0", + "typescript": "4.7.4", + "@antv/x6": "^1.34.5", + "highlight.js": "^11.6.0" + } +} diff --git a/source-code.ts b/source-code.ts new file mode 100644 index 0000000..d5dc77a --- /dev/null +++ b/source-code.ts @@ -0,0 +1,35 @@ +import {ItemView, View, WorkspaceLeaf} from "obsidian"; +import {indexInit} from "./index"; +import {SOURCE_CODE_VIEW_TYPE} from "./main"; + +export default class SourceCodeView extends ItemView { + + codeBlocks: string[] + + constructor(codeBlocks: string[], leaf: WorkspaceLeaf) { + super(leaf); + this.codeBlocks = codeBlocks; + } + + getDisplayText(): string { + return "MySourceCodeView"; + } + + getViewType(): string { + return SOURCE_CODE_VIEW_TYPE; + } + + async onOpen() { + const container = this.containerEl.children[1]; + container.empty(); + const div = document.createElement("div") + div.id = 'container'; + this.containerEl.children[1].appendChild(div); + + indexInit(this.codeBlocks); + } + + async onClose() { + } + +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..bd69670 --- /dev/null +++ b/styles.css @@ -0,0 +1,74 @@ +pre { + font-size: 13px; + color: #000000; + font-family: ui-sans-serif; + white-space: pre; +} + +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em; +} + +code.hljs { + padding: 3px 5px; +} + +.hljs { + background: #f3f3f3; + color: #444 +} + +.hljs-comment { + color: #697070 +} + +.hljs-punctuation,.hljs-tag { + color: #444a +} + +.hljs-tag .hljs-attr,.hljs-tag .hljs-name { + color: #444 +} + +.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag { + font-weight: 700 +} + +.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type { + color: #800 +} + +.hljs-section,.hljs-title { + color: #800; + font-weight: 700 +} + +.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable { + color: #ab5656 +} + +.hljs-literal { + color: #695 +} + +.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code { + color: #397300 +} + +.hljs-meta { + color: #1f7199 +} + +.hljs-meta .hljs-string { + color: #38a +} + +.hljs-emphasis { + font-style: italic +} + +.hljs-strong { + font-weight: 700 +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2d6fbdf --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES6", + "allowJs": true, + "noImplicitAny": true, + "moduleResolution": "node", + "importHelpers": true, + "isolatedModules": true, + "strictNullChecks": true, + "lib": [ + "DOM", + "ES5", + "ES6", + "ES7" + ] + }, + "include": [ + "**/*.ts" + ] +} diff --git a/utils.ts b/utils.ts new file mode 100644 index 0000000..d2953b8 --- /dev/null +++ b/utils.ts @@ -0,0 +1,167 @@ +type CodeData = { + className: string, // 類名稱 + funcs: CodeDataFunc[] // 函數名稱 與 代碼塊字串 集合 +} + +type CodeDataFunc = { + name: string, // 函數名稱 + code: string // 代碼塊字串 + calls: CodeDataFuncCall[] // 調用的函數集合 +} + +type CodeDataFuncCall = { + className: string, // 類名稱 + functionName: string // 函數名稱 +} + +export const CLASS_SHAPE_ID_TAG: string = "class-"; +export const CODE_BLOCK_ID_TAG: string = 'code-block-'; +class utils { + + /** + * 計算字串寬度 + */ + tCanvas: any = null; + public getTextWidth(text: string, font: string = '13px ui-sans-serif') { + // re-use canvas object for better performance + const canvas = this.tCanvas || (this.tCanvas = document.createElement('canvas')); + const context = canvas.getContext('2d'); + context.font = font; + return context.measureText(text).width; + } + + /** + * 根據 代碼塊字串 解析出 CodeData 對象 + * @param codeBlockText 代碼塊字串 + * @returns CodeData 對象 + */ + public parseCodeData(codeBlockText: string): CodeData { + + // 類名、方法名 的 標示符 + const classTag = '@class' + const functionTag = '@function' + const callTag = '@call' + + let codeLines = codeBlockText.split('\n') + + // 取得 註釋塊 的代碼 + let commentLines = [] + for (let i = 0; i < codeLines.length; i++) { + let codeLine = codeLines[i] + + // 找到註釋開頭,則從當前行開始,加入到註釋塊列表中 + if (codeLine.indexOf('/**') != -1) { + commentLines.push(codeLine) + } + else if (commentLines.length > 0) { + commentLines.push(codeLine) + } + + // 當碰到第一個多行註釋的結尾,則跳出循環 + if (codeLine.indexOf('*/') != -1) { + break + } + } + + + // 從 註釋塊 中獲取 類名、方法名、調用的函數集合 + let className = '' + let functionName = '' + let calls: CodeDataFuncCall[] = [] + + commentLines.forEach((comment) => { + + // 獲取 類名 + if (comment.indexOf(classTag) != -1) { + className = comment.substring(comment.indexOf(classTag) + classTag.length).trim() + } + + // 獲取 方法名 + if (comment.indexOf(functionTag) != -1) { + functionName = comment.substring(comment.indexOf(functionTag) + functionTag.length).trim() + } + + // 獲取 調用的函數 + if (comment.indexOf(callTag) != -1) { + let callStr = comment.substring(comment.indexOf(callTag) + callTag.length).trim() + calls.push({ + className: callStr.substring(0, callStr.indexOf('@')).trim(), + functionName: callStr.substring(callStr.indexOf('@') + 1).trim() + }) + } + + }) + + // 封裝結果 + let result: CodeData = { + className: className, + funcs: [{ + name: functionName, + code: codeBlockText, + calls: calls + }] + } + + return result + } + + /** + * 獲取 類聲明 的 圖形Id + * @param className 類名 + * @param functionName 方法名 + * @param type Id 的類型 + * @returns 圖形Id + */ + public getClassShapeId(className: string, functionName: string, type: 'class' | 'function'): string { + if (type == 'class') { + return CLASS_SHAPE_ID_TAG + className + } else if (type == 'function') { + return CLASS_SHAPE_ID_TAG + className + '-' + functionName + } + return '' + } + + /** + * 獲取 代碼塊 的 圖形Id + * @param className 類名 + * @param functionName 方法名 + * @returns 圖形Id + */ + public getCodeBlockShapeId(className: string, functionName: string): string { + return CODE_BLOCK_ID_TAG + className + '-' + functionName + } + + /** + * 將擁有相同 ClassName 的 CodeDataFunc 合併到一起 + * @param codeDatas 要進行合併的 CodeData 集合 + * @returns 合併後的 CodeData 集合 + */ + public mergeCodeDatas(codeDatas: CodeData[]): CodeData[] { + + // 對有相同 className 的 CodeData 的 func 進行合併 + let classNameToCodeData = new Map() + codeDatas.forEach((codeData: CodeData) => { + + // 如果在 Map 中已存在相應的 CodeData,則將 func 進行合併 + if (classNameToCodeData.has(codeData.className)) { + let tempCodeData = classNameToCodeData.get(codeData.className) as CodeData + tempCodeData.funcs.push(codeData.funcs[0]) + } + // 如果在 Map 中還沒有相應的 CodeData,則將當前 CodeData 加入到 Map 中 + else { + classNameToCodeData.set(codeData.className, codeData) + } + }) + + // 封裝返回結果 + let result: CodeData[] = [] + for (let codeData of Array.from(classNameToCodeData.values())) { + result.push(codeData) + } + + return result + } +} + + +export { utils, CodeData, CodeDataFunc } diff --git a/version-bump.mjs b/version-bump.mjs new file mode 100644 index 0000000..d409fa0 --- /dev/null +++ b/version-bump.mjs @@ -0,0 +1,14 @@ +import { readFileSync, writeFileSync } from "fs"; + +const targetVersion = process.env.npm_package_version; + +// read minAppVersion from manifest.json and bump version to target version +let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); +const { minAppVersion } = manifest; +manifest.version = targetVersion; +writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); + +// update versions.json with target version and minAppVersion from manifest.json +let versions = JSON.parse(readFileSync("versions.json", "utf8")); +versions[targetVersion] = minAppVersion; +writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..26382a1 --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +{ + "1.0.0": "0.15.0" +}