From bb83c9206dd1a0ab90c23d31df5a011fed83ef73 Mon Sep 17 00:00:00 2001 From: Waiting Date: Fri, 10 Mar 2023 17:44:58 +0800 Subject: [PATCH] Initial commit --- .editorconfig | 10 +++ .eslintignore | 3 + .eslintrc | 23 +++++ .gitignore | 22 +++++ .npmrc | 1 + README.md | 96 +++++++++++++++++++++ classShape.ts | 192 ++++++++++++++++++++++++++++++++++++++++++ codeBlockShape.ts | 66 +++++++++++++++ edge.ts | 107 ++++++++++++++++++++++++ esbuild.config.mjs | 48 +++++++++++ index.ts | 204 +++++++++++++++++++++++++++++++++++++++++++++ main.ts | 67 +++++++++++++++ manifest.json | 11 +++ package.json | 26 ++++++ source-code.ts | 35 ++++++++ styles.css | 74 ++++++++++++++++ tsconfig.json | 24 ++++++ utils.ts | 167 +++++++++++++++++++++++++++++++++++++ version-bump.mjs | 14 ++++ versions.json | 3 + 20 files changed, 1193 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 README.md create mode 100644 classShape.ts create mode 100644 codeBlockShape.ts create mode 100644 edge.ts create mode 100644 esbuild.config.mjs create mode 100644 index.ts create mode 100644 main.ts create mode 100644 manifest.json create mode 100644 package.json create mode 100644 source-code.ts create mode 100644 styles.css create mode 100644 tsconfig.json create mode 100644 utils.ts create mode 100644 version-bump.mjs create mode 100644 versions.json 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" +}