Initial commit

This commit is contained in:
Waiting 2023-03-10 17:44:58 +08:00
commit bb83c9206d
20 changed files with 1193 additions and 0 deletions

10
.editorconfig Normal file
View file

@ -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

3
.eslintignore Normal file
View file

@ -0,0 +1,3 @@
node_modules/
main.js

23
.eslintrc Normal file
View file

@ -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"
}
}

22
.gitignore vendored Normal file
View file

@ -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

1
.npmrc Normal file
View file

@ -0,0 +1 @@
tag-version-prefix=""

96
README.md Normal file
View file

@ -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

192
classShape.ts Normal file
View file

@ -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 }

66
codeBlockShape.ts Normal file
View file

@ -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 = '<pre style="padding-left: 20px">' + hljs.highlight(codeText, { language: 'java' }).value + '</pre>'
return wrap
},
}
// 將 HTML 對象加入到返回結果中
result.push(htmlShape)
}
return result;
}
}
export { CodeBlockShape }

107
edge.ts Normal file
View file

@ -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 }

48
esbuild.config.mjs Normal file
View file

@ -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();
}

204
index.ts Normal file
View file

@ -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<Void> 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: <HTMLElement>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
}
}

67
main.ts Normal file
View file

@ -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() {
}
}

11
manifest.json Normal file
View file

@ -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
}

26
package.json Normal file
View file

@ -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"
}
}

35
source-code.ts Normal file
View file

@ -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() {
}
}

74
styles.css Normal file
View file

@ -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
}

24
tsconfig.json Normal file
View file

@ -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"
]
}

167
utils.ts Normal file
View file

@ -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 }

14
version-bump.mjs Normal file
View file

@ -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"));

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}