From 925d2451eb894f806a62cf1e83b0ea16d0f8f277 Mon Sep 17 00:00:00 2001 From: Joseph Avila Alvarez <46098690+josephinoo@users.noreply.github.com> Date: Wed, 21 Feb 2024 18:39:41 -0500 Subject: [PATCH] wip --- package.json | 7 +- src/PreviewWebPanel.ts | 276 +++++++++++++++++++++++++++++++++++++++-- src/extension.ts | 4 +- 3 files changed, 271 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 0214726..c709acd 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,8 @@ "title": "AsyncAPI: Paste as Schema" }, { - "command": "asyncapi.hello", - "title": "Hello AsyncAPI" + "command": "asyncapi.md", + "title": "AsyncAPI: Convert to Markdown" } ], "snippets": [ @@ -125,6 +125,7 @@ "ts-loader": "^9.2.8", "typescript": "^4.6.4", "webpack": "^5.70.0", - "webpack-cli": "^4.9.2" + "webpack-cli": "^4.9.2", + "mermaid": "^10.8.0" } } diff --git a/src/PreviewWebPanel.ts b/src/PreviewWebPanel.ts index 6e9aac3..ee94c1f 100644 --- a/src/PreviewWebPanel.ts +++ b/src/PreviewWebPanel.ts @@ -1,6 +1,216 @@ import * as vscode from 'vscode'; import * as path from 'path'; +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; + +interface AsyncAPIDocument { + asyncapi: string; + info: { + title: string; + version: string; + description: string; + }; + servers: { + [key: string]: { + host: string; + protocol: string; + description: string; + }; + }; + channels: { + [key: string]: { + address: string; + description: string; + messages: { + [key: string]: { + $ref: string; + }; + }; + parameters?: { + [key: string]: { + $ref: string; + }; + }; + }; + }; + operations: { + [key: string]: { + action: string; + channel: { + $ref: string; + }; + summary: string; + traits?: { + [key: string]: { + $ref: string; + }; + }; + messages?: { + [key: string]: { + $ref: string; + }; + }; + }; + }; + components?: { + messages?: { + [key: string]: { + name: string; + title: string; + summary: string; + contentType: string; + traits?: { + [key: string]: { + $ref: string; + }; + }; + payload?: { + $ref: string; + }; + }; + }; + schemas?: { + [key: string]: { + type: string; + properties: { + [key: string]: { + type: string; + minimum?: number; + maximum?: number; + description?: string; + enum?: string[]; + format?: string; + items?: { + type: string; + }; + $ref?: string; + }; + }; + description?: string; + }; + }; + securitySchemes?: { + [key: string]: { + type: string; + description: string; + }; + }; + parameters?: { + [key: string]: { + description: string; + schema: { + type: string; + format?: string; + minimum?: number; + maximum?: number; + }; + }; + }; + messageTraits?: { + [key: string]: { + headers: { + type: string; + properties: { + [key: string]: { + type: string; + minimum?: number; + maximum?: number; + }; + }; + }; + }; + }; + operationTraits?: { + [key: string]: { + bindings: { + kafka: { + clientId: { + type: string; + enum: string[]; + }; + }; + }; + }; + }; + }; +} +function convertAsyncAPIToMermaid(asyncAPIDocument: AsyncAPIDocument): string { + let mermaidCode = `flowchart TD\n`; + + // Add Streetlights Kafka API node + mermaidCode += ` subgraph "${asyncAPIDocument.info.title}"\n`; + + // Add Servers subgraph + mermaidCode += ` subgraph "Servers"\n`; + Object.entries(asyncAPIDocument.servers).forEach(([serverName, serverInfo]) => { + mermaidCode += ` ${serverName}["${serverName}"]\n`; + }); + mermaidCode += ` end\n`; + + // Add Channels subgraph + mermaidCode += ` subgraph "Channels"\n`; + Object.entries(asyncAPIDocument.channels).forEach(([channelName, channelInfo]) => { + mermaidCode += ` ${channelName}["${channelName}"]\n`; + }); + mermaidCode += ` end\n`; + + // Add Operations subgraph + mermaidCode += ` subgraph "Operations"\n`; + Object.entries(asyncAPIDocument.operations).forEach(([operationName, operationInfo]) => { + mermaidCode += ` ${operationName}["${operationName}"]\n`; + }); + mermaidCode += ` end\n`; + + // Add Messages subgraph + mermaidCode += ` subgraph "Messages"\n`; + Object.entries(asyncAPIDocument.components.messages).forEach(([messageName, messageInfo]) => { + mermaidCode += ` ${messageName}["${messageName}"]\n`; + }); + mermaidCode += ` end\n`; + + mermaidCode += ` end\n`; + + // Add connections between servers and channels + Object.entries(asyncAPIDocument.servers).forEach(([serverName]) => { + Object.entries(asyncAPIDocument.channels).forEach(([channelName]) => { + mermaidCode += ` ${serverName} --> ${channelName}\n`; + }); + }); + + // Add connections between channels and operations + Object.entries(asyncAPIDocument.channels).forEach(([channelName, channelInfo]) => { + Object.entries(asyncAPIDocument.operations).forEach(([operationName]) => { + if (channelInfo.messages && channelInfo.messages[operationName]) { + mermaidCode += ` ${channelName} --> ${operationName}\n`; + } + }); + }); + + // Add connections between channels and messages + Object.entries(asyncAPIDocument.channels).forEach(([channelName, channelInfo]) => { + Object.entries(asyncAPIDocument.components.messages).forEach(([messageName]) => { + if (channelInfo.messages && channelInfo.messages[messageName]) { + mermaidCode += ` ${channelName} --> ${messageName}\n`; + } + }); + }); + + // Add connections between operations and messages + Object.entries(asyncAPIDocument.operations).forEach(([operationName, operationInfo]) => { + Object.entries(asyncAPIDocument.components.messages).forEach(([messageName]) => { + if (operationInfo.messages && operationInfo.messages[messageName]) { + mermaidCode += ` ${operationName} --> ${messageName}\n`; + } + }); + }); + + return mermaidCode; +} + + + + let position : {x:0,y:0} = { x: 0, y: 0 @@ -16,6 +226,7 @@ export function previewAsyncAPI(context: vscode.ExtensionContext) { }; } + export const openAsyncapiFiles: { [id: string]: vscode.WebviewPanel } = {}; // vscode.Uri.fsPath => vscode.WebviewPanel export function isAsyncAPIFile(document?: vscode.TextDocument) { @@ -56,6 +267,8 @@ export function openAsyncAPI(context: vscode.ExtensionContext, uri: vscode.Uri) }); panel.title = path.basename(uri.fsPath); + console.log('Opening HTML'); + console.log(getWebviewContent(context, panel.webview, uri, position)); panel.webview.html = getWebviewContent(context, panel.webview, uri, position); panel.webview.onDidReceiveMessage( @@ -66,7 +279,6 @@ export function openAsyncAPI(context: vscode.ExtensionContext, uri: vscode.Uri) x: message.scrollX, y: message.scrollY }; - } } }, @@ -95,10 +307,48 @@ async function promptForAsyncapiFile() { }); return uris?.[0]; } -export function printHello() { - console.log('Hello'); -} +export function previewMarkdown(context: vscode.ExtensionContext) { + return async (uri: vscode.Uri) => { + uri = uri || (await promptForAsyncapiFile()) as vscode.Uri; + if (uri) { + console.log('Opening asyncapi file', uri.fsPath); + convertAsyncAPItoMD(context, uri); + } + }; + + } + + async function convertAsyncAPItoMD(context: vscode.ExtensionContext, uri: vscode.Uri) { + const localResourceRoots = [ + vscode.Uri.file(path.dirname(uri.fsPath)), + vscode.Uri.joinPath(context.extensionUri, 'dist/node_modules/@asyncapi/react-component/browser/standalone'), + vscode.Uri.joinPath(context.extensionUri, 'dist/node_modules/@asyncapi/react-component/styles'), + ]; + if (vscode.workspace.workspaceFolders) { + vscode.workspace.workspaceFolders.forEach(folder => { + localResourceRoots.push(folder.uri); + }); + } + const panel: vscode.WebviewPanel = + openAsyncapiFiles[uri.fsPath] || + vscode.window.createWebviewPanel('asyncapi-preview', '', vscode.ViewColumn.Two, { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots, + }); + console.log(vscode.Uri); + panel.title = path.basename(uri.fsPath); + panel.webview.html = convertToMD(context, panel.webview, uri, position); + + } + +function convertToMD(context: vscode.ExtensionContext, webview: vscode.Webview, asyncapiFile: vscode.Uri, position: {x:0,y:0}){ + console.log('Converting to MD'); + const asyncapiWebviewUri = webview.asWebviewUri(asyncapiFile); + console.log(asyncapiWebviewUri); + return "Hello"; +} function getWebviewContent(context: vscode.ExtensionContext, webview: vscode.Webview, asyncapiFile: vscode.Uri, position: {x:0,y:0}) { const asyncapiComponentJs = webview.asWebviewUri( @@ -108,13 +358,16 @@ function getWebviewContent(context: vscode.ExtensionContext, webview: vscode.Web vscode.Uri.joinPath(context.extensionUri, 'dist/node_modules/@asyncapi/react-component/styles/default.min.css') ); const asyncapiWebviewUri = webview.asWebviewUri(asyncapiFile); + const yamlFile = fs.readFileSync(asyncapiFile.fsPath, 'utf8'); + const asyncAPIDocument: AsyncAPIDocument = yaml.load(yamlFile) as AsyncAPIDocument; + const mermaidCode = convertAsyncAPIToMermaid(asyncAPIDocument); const asyncapiBasePath = asyncapiWebviewUri.toString().replace('%2B', '+'); // this is loaded by a different library so it requires unescaping the + character const html = `
- + - - ++ ${mermaidCode}; +- + `; diff --git a/src/extension.ts b/src/extension.ts index 08d586a..f2e7b1a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { isAsyncAPIFile, openAsyncAPI, openAsyncapiFiles, previewAsyncAPI, printHello } from './PreviewWebPanel'; +import { isAsyncAPIFile, openAsyncAPI, openAsyncapiFiles, previewAsyncAPI, previewMarkdown} from './PreviewWebPanel'; import { asyncapiSmartPaste } from './SmartPasteCommand'; @@ -38,7 +38,7 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand("asyncapi.paste", asyncapiSmartPaste)); - context.subscriptions.push(vscode.commands.registerCommand('asyncapi.hello', printHello )); + context.subscriptions.push(vscode.commands.registerCommand('asyncapi.md', previewMarkdown(context))); } export function deactivate() {}