Added doc generator

- Added plugin doc generator
- Added getting start plugin doc
This commit is contained in:
Toby Chui 2025-05-25 22:17:38 +08:00
parent b4c2f6bf13
commit c56e317bfd
42 changed files with 2613 additions and 189 deletions

1
.gitignore vendored
View File

@ -50,3 +50,4 @@ src/log/
example/plugins/ztnc/ztnc.db
example/plugins/ztnc/authtoken.secret
example/plugins/ztnc/ztnc.db.lock
docs/plugins/docs.exe

View File

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 194 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,51 @@
/* Things to do before body loads */
function restoreDarkMode(){
if (localStorage.getItem("darkMode") === "enabled") {
$("html").addClass("is-dark");
$("html").removeClass("is-white");
} else {
$("html").removeClass("is-dark");
$("html").addClass("is-white");
}
}
restoreDarkMode();
function updateElementToTheme(isDarkTheme=false){
if (!isDarkTheme){
let whiteSrc = $("#sysicon").attr("white_src");
$("#sysicon").attr("src", whiteSrc);
$("#darkModeToggle").html(`<span class="ts-icon is-sun-icon"></span>`);
// Update the rendering text color in the garphs
if (typeof(changeScaleTextColor) != "undefined"){
changeScaleTextColor("black");
}
}else{
let darkSrc = $("#sysicon").attr("dark_src");
$("#sysicon").attr("src", darkSrc);
$("#darkModeToggle").html(`<span class="ts-icon is-moon-icon"></span>`);
// Update the rendering text color in the garphs
if (typeof(changeScaleTextColor) != "undefined"){
changeScaleTextColor("white");
}
}
}
/* Things to do after body loads */
$(document).ready(function(){
$("#darkModeToggle").on("click", function() {
$("html").toggleClass("is-dark");
$("html").toggleClass("is-white");
if ($("html").hasClass("is-dark")) {
localStorage.setItem("darkMode", "enabled");
updateElementToTheme(true);
} else {
localStorage.setItem("darkMode", "disabled");
updateElementToTheme(false);
}
});
updateElementToTheme(localStorage.getItem("darkMode") === "enabled");
});

272
docs/plugins/build.go Normal file
View File

@ -0,0 +1,272 @@
package main
import (
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/yosssi/gohtml"
)
func build() {
rootDir := "./docs"
outputFile := "./index.json"
type Folder struct {
Title string `json:"title"`
Path string `json:"path"`
Type string `json:"type"`
Files []interface{} `json:"files,omitempty"`
}
var buildTree func(path string, d fs.DirEntry) interface{}
buildTree = func(path string, d fs.DirEntry) interface{} {
relativePath, _ := filepath.Rel(rootDir, path)
relativePath = filepath.ToSlash(relativePath)
var title string
if d.IsDir() {
title = filepath.Base(relativePath)
} else {
title = strings.TrimSuffix(filepath.Base(relativePath), filepath.Ext(relativePath))
}
//Strip the leader numbers from the title, e.g. 1. Introduction -> Introduction
if strings.Contains(title, ".") {
parts := strings.SplitN(title, ".", 2)
if len(parts) > 1 {
title = strings.TrimSpace(parts[1])
}
}
// Remove leading numbers and dots
title = strings.TrimLeft(title, "0123456789. ")
// Remove leading spaces
title = strings.TrimLeft(title, " ")
if d.IsDir() {
folder := Folder{
Title: title,
Path: relativePath,
Type: "folder",
}
entries, err := os.ReadDir(path)
if err != nil {
panic(err)
}
for _, entry := range entries {
if entry.Name() == "img" || entry.Name() == "assets" {
continue
}
if strings.Contains(filepath.ToSlash(filepath.Join(relativePath, entry.Name())), "/img/") || strings.Contains(filepath.ToSlash(filepath.Join(relativePath, entry.Name())), "/assets/") {
continue
}
child := buildTree(filepath.Join(path, entry.Name()), entry)
if child != nil {
folder.Files = append(folder.Files, child)
}
}
return folder
} else {
ext := filepath.Ext(relativePath)
if ext != ".md" && ext != ".html" && ext != ".txt" {
return nil
}
return FileInfo{
Filename: relativePath,
Title: title,
Type: "file",
}
}
}
rootInfo, err := os.Stat(rootDir)
if err != nil {
panic(err)
}
rootFolder := buildTree(rootDir, fs.FileInfoToDirEntry(rootInfo))
jsonData, err := json.MarshalIndent(rootFolder, "", " ")
if err != nil {
panic(err)
}
/* For debug purposes, print the JSON structure */
err = os.WriteFile(outputFile, jsonData, 0644)
if err != nil {
panic(err)
}
/* For each file in the folder structure, convert markdown to HTML */
htmlOutputDir := "./html"
os.RemoveAll(htmlOutputDir) // Clear previous HTML output
err = os.MkdirAll(htmlOutputDir, os.ModePerm)
if err != nil {
panic(err)
}
var processFiles func(interface{})
processFiles = func(node interface{}) {
switch n := node.(type) {
case FileInfo:
if filepath.Ext(n.Filename) == ".md" {
inputPath := filepath.Join(rootDir, n.Filename)
outputPath := filepath.Join(htmlOutputDir, strings.TrimSuffix(n.Filename, ".md")+".html")
// Ensure the output directory exists
err := os.MkdirAll(filepath.Dir(outputPath), os.ModePerm)
if err != nil {
panic(err)
}
// Read the markdown file
mdContent, err := os.ReadFile(inputPath)
if err != nil {
panic(err)
}
// Convert markdown to HTML
docContent := mdToHTML(mdContent)
docContent, err = optimizeCss(docContent)
if err != nil {
panic(err)
}
// Load the HTML template
templateBytes, err := os.ReadFile("template/documents.html")
if err != nil {
panic(err)
}
// Generate the side menu HTML
sideMenuHTML, err := generateSideMenu(string(jsonData), n.Title)
if err != nil {
panic(err)
}
templateBody := string(templateBytes)
// Replace placeholders in the template
htmlContent := strings.ReplaceAll(templateBody, "{{title}}", n.Title+" | Zoraxy Documentation")
htmlContent = strings.ReplaceAll(htmlContent, "{{content}}", string(docContent))
htmlContent = strings.ReplaceAll(htmlContent, "{{sideMenu}}", sideMenuHTML)
htmlContent = strings.ReplaceAll(htmlContent, "{{root_url}}", *root_url)
//Add more if needed
//Beautify the HTML content
htmlContent = gohtml.Format(htmlContent)
// Write the HTML file
err = os.WriteFile(outputPath, []byte(htmlContent), 0644)
if err != nil {
panic(err)
}
//Check if the .md file directory have an ./img or ./assets folder. If yes, copy the contents to the output directory
imgDir := filepath.Join(rootDir, filepath.Dir(n.Filename), "img")
assetsDir := filepath.Join(rootDir, filepath.Dir(n.Filename), "assets")
if _, err := os.Stat(imgDir); !os.IsNotExist(err) {
err = filepath.Walk(imgDir, func(srcPath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(imgDir, srcPath)
if err != nil {
return err
}
destPath := filepath.Join(filepath.Dir(outputPath), "img", relPath)
if info.IsDir() {
return os.MkdirAll(destPath, os.ModePerm)
}
data, err := os.ReadFile(srcPath)
if err != nil {
return err
}
return os.WriteFile(destPath, data, 0644)
})
if err != nil {
panic(err)
}
}
if _, err := os.Stat(assetsDir); !os.IsNotExist(err) {
err = filepath.Walk(assetsDir, func(srcPath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(assetsDir, srcPath)
if err != nil {
return err
}
destPath := filepath.Join(filepath.Dir(outputPath), "assets", relPath)
if info.IsDir() {
return os.MkdirAll(destPath, os.ModePerm)
}
data, err := os.ReadFile(srcPath)
if err != nil {
return err
}
return os.WriteFile(destPath, data, 0644)
})
if err != nil {
panic(err)
}
}
fmt.Println("Generated HTML:", outputPath)
}
case Folder:
for _, child := range n.Files {
processFiles(child)
}
}
}
processFiles(rootFolder)
copyOtherRes()
}
func copyOtherRes() {
srcDir := "./assets"
destDir := "./html/assets"
err := filepath.Walk(srcDir, func(srcPath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(srcDir, srcPath)
if err != nil {
return err
}
destPath := filepath.Join(destDir, relPath)
if info.IsDir() {
return os.MkdirAll(destPath, os.ModePerm)
}
data, err := os.ReadFile(srcPath)
if err != nil {
return err
}
return os.WriteFile(destPath, data, 0644)
})
if err != nil {
panic(err)
}
}
func mdToHTML(md []byte) []byte {
// create markdown parser with extensions
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
p := parser.NewWithExtensions(extensions)
doc := p.Parse(md)
// create HTML renderer with extensions
htmlFlags := html.CommonFlags | html.HrefTargetBlank
opts := html.RendererOptions{Flags: htmlFlags}
renderer := html.NewRenderer(opts)
return markdown.Render(doc, renderer)
}

4
docs/plugins/build.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/bash
# Run the Go program with the specified arguments
./docs.exe -m=build -root="/plugins/html/"

View File

@ -0,0 +1,128 @@
package main
import (
"fmt"
"net/url"
"strings"
"github.com/PuerkitoBio/goquery"
)
func optimizeCss(htmlContent []byte) ([]byte, error) {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(htmlContent)))
if err != nil {
return nil, err
}
originalHTMLContent := string(htmlContent)
// Replace img elements
doc.Find("img").Each(func(i int, s *goquery.Selection) {
//For each of the image element, replace the parent from p to div
orginalParent, err := s.Parent().Html()
if err != nil {
fmt.Println("Error getting parent HTML:", err)
return
}
src, exists := s.Attr("src")
if !exists {
fmt.Println("No src attribute found for img element")
return
}
encodedSrc := (&url.URL{Path: src}).String()
//Patch the bug in the parser that converts " />" to "/>"
orginalParent = strings.ReplaceAll(orginalParent, "/>", " />")
fmt.Println("<div class=\"ts-image is-rounded\"><img src=\"./" + encodedSrc + "\"></div>")
//Replace the img with ts-image
originalHTMLContent = strings.Replace(originalHTMLContent, orginalParent, "<div class=\"ts-image is-rounded\" style=\"max-width: 800px\">"+orginalParent+"</div>", 1)
})
/*
// Replace h* elements
doc.Find("h1").Each(func(i int, s *goquery.Selection) {
originalHeader, err := s.Html()
if err != nil {
fmt.Println("Error getting header HTML:", err)
return
}
// Patch the bug in the parser that converts " />" to "/>"
originalHeader = strings.ReplaceAll(originalHeader, "/>", " />")
wrappedHeader := fmt.Sprintf("<div class=\"ts-header is-big\">%s</div>", originalHeader)
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, s.Text(), wrappedHeader)
})
*/
doc.Find("h2").Each(func(i int, s *goquery.Selection) {
originalHeader, err := s.Html()
if err != nil {
fmt.Println("Error getting header HTML:", err)
return
}
// Patch the bug in the parser that converts " />" to "/>"
originalHeader = strings.ReplaceAll(originalHeader, "/>", " />")
wrappedHeader := fmt.Sprintf("<div class=\"ts-header is-large\">%s</div>", originalHeader)
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, s.Text(), wrappedHeader)
})
doc.Find("h3").Each(func(i int, s *goquery.Selection) {
originalHeader, err := s.Html()
if err != nil {
fmt.Println("Error getting header HTML:", err)
return
}
// Patch the bug in the parser that converts " />" to "/>"
originalHeader = strings.ReplaceAll(originalHeader, "/>", " />")
wrappedHeader := fmt.Sprintf("<div class=\"ts-header\">%s</div>", originalHeader)
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, s.Text(), wrappedHeader)
})
// Add "ts-text" class to each p element
doc.Find("p").Each(func(i int, s *goquery.Selection) {
class, exists := s.Attr("class")
var newClass string
if exists {
newClass = fmt.Sprintf("%s ts-text", class)
} else {
newClass = "ts-text"
}
originalParagraph, _ := s.Html()
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, originalParagraph, fmt.Sprintf("<p class=\"%s\">%s</p>", newClass, originalParagraph))
})
//Replace hr with ts-divider
// Replace hr elements outside of code blocks
doc.Find("hr").Each(func(i int, s *goquery.Selection) {
parent := s.Parent()
if parent.Is("code") {
// Skip <hr> inside <code> blocks
return
}
// Replace <hr> with <div class="ts-divider"></div>
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "<hr>", "<div class=\"ts-divider\"></div>")
})
// Add ts-table to all table elements
doc.Find("table").Each(func(i int, s *goquery.Selection) {
class, exists := s.Attr("class")
var newClass string
if exists {
newClass = fmt.Sprintf("%s ts-table", class)
} else {
newClass = "ts-table"
}
originalTable, _ := s.Html()
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, originalTable, fmt.Sprintf("<table class=\"%s\">%s</table>", newClass, originalTable))
})
//Replace blockquote to <div class="ts-quote">
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "<blockquote>", "<div class=\"ts-quote\">")
originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "</blockquote>", "</div>")
return []byte(originalHTMLContent), err
}

6
docs/plugins/dev.sh Normal file
View File

@ -0,0 +1,6 @@
#/bin/bash
go build
# Run the Go program with the specified arguments
./docs.exe -m=build
./docs.exe

View File

@ -0,0 +1 @@
<mxfile host="Electron" modified="2025-05-25T13:00:38.901Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/12.4.2 Chrome/78.0.3904.130 Electron/7.1.4 Safari/537.36" etag="lsRaVkYmhHZrtifUUFnd" version="12.4.2" type="device" pages="1"><diagram id="aAlAxoldjFgNxtltmMrS" name="Page-1">7Vpbc5s4FP41nuk+lEGI62PtdDfbzLaZOpnN7suOahSsXYy8Qm7s/PpKRoCRcEwTcNykfvCgIyHQdy76zhEjOFmsf2NoOf+DxjgdOXa8HsGzkeMEbiT+pWBTCDzPLwQJI3EhArVgSu6xEtpKuiIxzhsDOaUpJ8umcEazDM94Q4YYo3fNYbc0bT51iRJsCKYzlJrSP0nM54U0dIJafo5JMi+fDHy14AUqB6uV5HMU07sdEXw/ghNGKS+uFusJTiV2JS7Ffb/u6a1ejOGMd7nBTa5BHF3Azx/t65sPk4uLm9m/bx2lja8oXakVq7flmxICRldZjOUs9giO7+aE4+kSzWTvndC5kM35IhUtIC5vSZpOaEqZaGc0E4PGMcrn29tlv3ocZhyv9y4EVPAIs8J0gTnbiCGlTXnFHcqinEgBfFfrx3fVmPmObkI1DimTSKqZa9TEhQLue0AMfngQYdgRROANhqL3w6MIgq4ogqFQBAaIf1OG1punQdkDVKCMgyVW0MSqGrOLlTuYwRlQXaarhGRC9pmuOGbPjhmEp4YZNDC7XuacYbR4drC84NTAcg8HNJzF7yRLEa1ZivKczJq4tAQsHBuM5SBEOxB4LQiUMoZTxMnX5vRtsKgnXFIiHly7eKhpINCQzemKzbC6a5eqHJjIi7SJOGIJ5sZEWy1Vy3684jrsRC9KcTCMLN+L6l/Qjx4PzHtstXbgui9Krb4j4Pft6gf60erD0x5bqX1z7x52IqDTwhZyDVrUDgfLUKKnW/6pGzuImnuGYz86aMHmRDpXH9iggUnhn92iYahZdOR1smjPG8iggUne77d5zj/LLYe3fs84o9OlLAlJzuen4o3GXwSl9xN59WYqtCi7LhFD4pUw+8UAWcDFm0iilCSZdBABm0gP4FiCSmYofac6FiSO5e1jhnNyj75sp5IqWkpr2YLgjUfemdSKmG7FaV5UvOTsjHLhAFTOA/tRW6WTKuS3qK3NJ+FQgQiYCcSLi0RQY69GgtGZBushzT9yJDITGCLdKldu9ebD9NNH2U+Q+J9enX26vjopNxJEvp8NPdJKjm2pZdhiUc5gftR33XaATQJ63WiPsVP3V+Gxnx5txOLZ5kYak20BLyolf0lYLeBUgrO1ArpobXZbl5iR7SajhKcewUAYWk7UVCaMLOdxYQyGwArsffmCsBLLtr26W6MMA8e40iL2EokJzW5JsmJYkonXSiT0AAhBi2sHxyQSTg9E4tW6dlhXZLSY7drSG3d+fj8+b2uPAbYV7ji9toyhfT40bKc8JLGVOx/04FzwoP+wdrzUcuLU3dPbNujmFt4oPu3SnEj23tKMT9UrlynkU3ze1cgngFZL+hC1beiONVTe55iVjOrQ5qfq9qgOgvAEVAdNMlaft+H/VziX8eMcZXFKsuSnHqU2mhzMdVt2XXhkLb6GBD5yrZ1Di0grCLqO37q3fXcdXTvddWF41G0Qmun9+dXV5RY/5Y4vjsgaoTH0u8XFwQ7UzQ3txSvBDXQ22HFzGuygvs86QdUo8gjvQBbRNRg6dhEOHlhGURR6ttNGPZzpX4p1jYuufWCigeOiax7AVC6ZL2mW41PyyZ6Kd26wB/RG8e6oTtlnhj+QU57KEYOrnwx0/9JGC8WBLIUYtbpjed4DjOT0PO9tX5wEajrwK/o+gPOJZv3NeaG4+sN9+P4b</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

@ -0,0 +1,38 @@
# What is Zoraxy Plugin?
Zoraxy Plugin is a powerful extension feature designed to enhance the functionality of the Zoraxy system. It provides additional features and capabilities that are not part of the core system, allowing users to customize their experience and optimize performance. The plugin is built to be modular and flexible, enabling users to tailor their Zoraxy environment to meet specific needs.
Zoraxy plugins are distributed as binaries, and developers have the flexibility to choose whether to open source them or not **as the plugin library and interface are open source under the LGPL license**.
There are two primary types of plugins:
- **Router plugins**: Involved with connections from HTTP proxy rules.
- **Utility plugins**: Provide user interfaces for various network features that operate independently of the Zoraxy core.
---
## How plugins are distributed & installed
Zoraxy plugins are distributed as platform-dependent binaries, tailored to specific operating systems and CPU architectures. These binaries follow a naming convention that includes the operating system, CPU architecture, and plugin name, such as `linux_amd64_foobar`, `windows_amd64_foobar.exe`, or `linux_arm64_foobar`.
To manually install a plugin for testing, place the binary file into the `/plugins/{plugin_name}/` folder within your Zoraxy installation directory.
> **Warning:** The binary name inside the folder must match the plugin folder name. For example, the binary should be named `foobar` (or `foobar.exe` on Windows) if placed in the `/plugins/foobar/` folder. Avoid using names like `foobar_plugin.exe`.
For distribution, a plugin store system is used. The plugin store architecture is similar to the one built into the Arduino IDE, with a manager URL (a JSON file) listing all the plugins supported by that store. See the documentation section for more details on how to implement your own plugin store.
---
## Plugin vs Pull Request
The Zoraxy plugin was introduced to address specific use cases that enhance its functionality. It serves as an extension to the core Zoraxy system, providing additional features and capabilities while maintaining the integrity of the core system.
- Designed to handle features that are challenging to integrate directly into the Zoraxy core.
- Caters to scenarios where certain features are only applicable in limited situations, avoiding unnecessary resource consumption for other users.
- Allows for frequent updates to specific code components without impacting the core's stability or causing downtime.
---
### When should you add a core PR or a plugin?
In certain situations, implementing a feature as a plugin is more reasonable than directly integrating it into the Zoraxy core:
- **Core PR**: If the feature is relevant to most users and enhances Zoraxy's core functionality, consider submitting a core Pull Request (PR).
- **Plugin**: If the feature is targeted at a smaller user base or requires additional dependencies that not all users need, it should be developed as a plugin.
The decision depends on the feature's general relevance and its impact on core stability. Plugins offer flexibility without burdening the core.

View File

@ -0,0 +1,58 @@
# Getting Started
To start developing plugins, you will need the following installed on your computer
1. The source code of Zoraxy
2. Go compiler
3. VSCode (recommended, or any editor of your choice)
---
## Step 1: Start Zoraxy at least once
If you have just cloned Zoraxy from the Github repo, use the following to build and run it once.
```bash
cd src/
go mod tidy
go build
sudo ./zoraxy
```
This would allow Zoraxy to generate all the required folder structure on startup.
After the startup process completes, you would see a folder named "plugins" in the working directory of Zoraxy.
---
## Steps 2: Prepare the development environment for Zoraxy Plugin
Next, you will need to think of a name for your plugin. Lets name our new plugin "Lapwing".
**Notes: Plugin name described in Introspect (will discuss this in later sessions) can contains space, but the folder and compiled binary filename must not contains space and special characters for platform compatibilities reasons.**
Follow the steps below to create the folder structure
### 2.1 Create Plugin Folder
Create a folder with your plugin name in the `plugins` folder. After creating the folder, you would have something like `plugins/Lapwing/`.
### 2.2 Locate and copy Zoraxy Plugin library
Locate the Zoraxy plugin library from the Zoraxy source code. You can find the `zoraxy_plugin` Go module under `src/mod/plugins/zoraxy_plugin`.
Copy the `zoraxy_plugin` folder from the Zoraxy source code mod folder into the your plugin's mod folder. Let assume you use the same mod folder name as Zoraxy as `mod`, then your copied library path should be `plugins/Lapwing/mod/zoraxy_plugin`.
### 2.3 Prepare Go Project structure
Create the `main.go` file for your plugin. In the example above, it would be located at `plugins/Lapwing/main.go`.
Use `go mod init yourdomain.com/foo/plugin_name` to initiate your plugin. By default the `go.mod` file will be automatically generated by the go compiler. Assuming you are developing Lapwing with its source located on Github, this command would be `go mod init github.com/your_user_name/Lapwing`.
---
## Steps 3: Open plugin folder in IDE
Now open your preferred IDE or text editor and use your plugin folder as the project folder
Now, you are ready to start developing Zoraxy plugin!

View File

@ -0,0 +1,12 @@
# Plugin Architecture
The Zoraxy Plugin uses a 3 steps approach to get information from plugin, setup the plugin and forward request to plugin. The name of the steps are partially referred from dbus designs as followings.
1. Introspect
2. Configure
3. Forwarding
The overall flow looks like this.
![](img/1. Plugin Architecture/plugin_workflow.png)

View File

@ -0,0 +1,91 @@
# Introspect
Introspect, similar to the one in dbus design, is used to get the information from plugin when Zoraxy starts (or manually triggered in development mode or force reload plugin list).
**This is a pre-defined structure where the plugin must provide to Zoraxy** when the plugin is being started with the `-introspect` flag.
The introspect structure is defined under the `zoraxy_plugin` library, where both Zoraxy and plugin should use. As of writing, the structure of introspect is like this.
```go
type IntroSpect struct {
/* Plugin metadata */
ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname
Name string `json:"name"` //Name of your plugin
Author string `json:"author"` //Author name of your plugin
AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email
Description string `json:"description"` //Description of your plugin
URL string `json:"url"` //URL of your plugin
Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1)
VersionMajor int `json:"version_major"` //Major version of your plugin
VersionMinor int `json:"version_minor"` //Minor version of your plugin
VersionPatch int `json:"version_patch"` //Patch version of your plugin
/*
Endpoint Settings
*/
/*
Static Capture Settings
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
This is faster than dynamic capture, but less flexible
*/
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
/*
Dynamic Capture Settings
Once plugin is enabled, these rules will be captured and forward to plugin sniff
if the plugin sniff returns 280, the traffic will be captured
otherwise, the traffic will be forwarded to the next plugin
This is slower than static capture, but more flexible
*/
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
/* UI Path for your plugin */
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
/* Subscriptions Settings */
SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details
}
```
## Introspect Manual Triggering
To manually test if the introspect return is correct, you can try using the `-introspect` flag on any Zoraxy plugin. You should be able to see an output like so.
```json
$ ./debugger -introspect
{
"id": "org.aroz.zoraxy.debugger",
"name": "Plugin Debugger",
"author": "aroz.org",
"author_contact": "https://aroz.org",
"description": "A debugger for Zoraxy \u003c-\u003e plugin communication pipeline",
"url": "https://zoraxy.aroz.org",
"type": 0,
"version_major": 1,
"version_minor": 0,
"version_patch": 0,
"static_capture_paths": [
{
"capture_path": "/test_a"
},
{
"capture_path": "/test_b"
}
],
"static_capture_ingress": "/s_capture",
"dynamic_capture_sniff": "/d_sniff",
"dynamic_capture_ingress": "/d_capture",
"ui_path": "/debug",
"subscription_path": "",
"subscriptions_events": null
}
```

View File

@ -0,0 +1,18 @@
# Configure
Configure or Configure Spec is the `exec` call where Zoraxy start the plugin. The configure spec JSON structure is defined in `zoraxy_plugin` library.
As the time of writing, the `ConfigureSpec` only contains information on some basic info.
```go
type ConfigureSpec struct {
Port int `json:"port"` //Port to listen
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
//To be expanded
}
```
The `ConfigureSpec` struct will be parsed to JSON and pass to your plugin via the `-configure=(json payload here)`.
In your plugin, you can use the `zoraxy_plugin` library to parse it or parse it manually (if you are developing a plugin with other languages).

View File

@ -0,0 +1,23 @@
# Capture Modes
As you can see in the Introspect section, there are two types of capture mode in Zoraxy plugin API.
- Static Capture Mode
- Dynamic Capture Mode
**Notes: When this document mention the term "endpoint", it means a particular sub-path on the plugin side. For example `/capture` or `/sniff`. In actual implementation, this can be a `http.HandleFunc` or `http.Handle` depends on the plugin implementation.**
## Static Capture Mode
Static Capture Mode register a static path to Zoraxy, when the plugin is enalbed on a certain HTTP proxy rule, all request that matches the static capture registered paths are forwarded to the plugin without asking first.
(To be added)
## Dynamic Capture Mode
Dynamic Capture Mode register two endpoints to Zoraxy.
1. DynamicCaptureSniff - The sniffing endpoint where Zoraxy will first ask if the plugin want to handle this request
2. DynamicCaptureIngress - The handling endpoint, where if the plugin reply the sniffing with "YES", Zoraxy forward the incoming request to this plugin at this defined endpoint.
(To be added)

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

@ -0,0 +1,17 @@
# Index
Welcome to the Zoraxy Plugin Documentation!
Click on a topic in the side menu to begin navigating through the available resources and guides for developing and managing plugins.
## FAQ
### What skills do I need for developing a plugin?
Basic HTML, JavaScript, and CSS skills are required, with Go (Golang) being the preferred backend language. However, any programming language that can be compiled into a binary and provide a web server interface will work.
### Will a plugin crash the whole Zoraxy?
No. Plugins operate in a separate process from Zoraxy. If a plugin crashes, Zoraxy will terminate and disable that plugin without affecting the core operations. This is by design to ensure stability.
### Can I sell my plugin?
Yes, the plugin library and interface design are open source under the LGPL license. You are not required to disclose the source code of your plugin as long as you do not modify the plugin library and use it as-is. For more details on how to comply with the license, refer to the licensing documentation.
### How can I add my plugin to the official plugin store?
To add your plugin to the official plugin store, open a pull request (PR) in the plugin repository.

View File

@ -1,6 +0,0 @@
<div class="ts-content">
<div class="ts-container">
<h2>Documents</h2>
<p>Test</p>
</div>
</div>

11
docs/plugins/go.mod Normal file
View File

@ -0,0 +1,11 @@
module imuslab.com/zoraxy/docs
go 1.24.1
require (
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b // indirect
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 // indirect
golang.org/x/net v0.40.0 // indirect
)

75
docs/plugins/go.sum Normal file
View File

@ -0,0 +1,75 @@
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -0,0 +1,304 @@
<!DOCTYPE html>
<html lang="en" class="is-white">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
What is Zoraxy Plugin | Zoraxy Documentation
</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js" integrity="sha512-LhccdVNGe2QMEfI3x4DVV3ckMRe36TfydKss6mJpdHjNFiV07dFpS2xzeZedptKZrwxfICJpez09iNioiSZ3hA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.js"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Code highlight -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<!-- additional languages -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/go.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/c.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/xml.min.js"></script>
<script>
hljs.highlightAll();
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/tomorrow-night-bright.css">
<style>
#msgbox{
position: fixed;
bottom: 1em;
right: 1em;
z-index: 9999;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
dialog[open] {
animation: fadeIn 0.3s ease-in-out;
}
code{
border-radius: 0.5rem;
}
</style>
<script src="/html/assets/theme.js"></script>
</head>
<body>
<div class="ts-content">
<div class="ts-container">
<div style="float: right;">
<button class="ts-button is-icon" id="darkModeToggle">
<span class="ts-icon is-moon-icon"></span>
</button>
</div>
<div class="ts-tab is-pilled">
<a href="" class="item" style="user-select: none;">
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
</a>
<a href="#!" class="is-active item">
Documents
</a>
<a href="#!" class="item">
Examples
</a>
</div>
</div>
</div>
<div class="ts-divider"></div>
<div>
<div class="has-padded">
<div class="ts-grid mobile:is-stacked">
<div class="column is-4-wide">
<div class="ts-box">
<div class="ts-menu is-end-icon">
<a class="item">
Introduction
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item is-active" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
What is Zoraxy Plugin
</a>
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
Getting Started
</a>
</div>
<a class="item">
Architecture
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
Plugin Architecture
</a>
<a class="item" href="/html/2. Architecture/2. Introspect.html">
Introspect
</a>
<a class="item" href="/html/2. Architecture/3. Configure.html">
Configure
</a>
</div>
<a class="item">
Getting Started
<span class="ts-icon is-caret-down-icon"></span>
</a>
<a class="item" href="/html/index.html">
index
</a>
</div>
</div>
</div>
<div class="column is-12-wide">
<div class="ts-box">
<div class="ts-container is-padded has-top-padded-large">
<h1 id="what-is-zoraxy-plugin">
What is Zoraxy Plugin?
</h1>
<p>
<p class="ts-text">
Zoraxy Plugin is a powerful extension feature designed to enhance the functionality of the Zoraxy system. It provides additional features and capabilities that are not part of the core system, allowing users to customize their experience and optimize performance. The plugin is built to be modular and flexible, enabling users to tailor their Zoraxy environment to meet specific needs.
</p>
</p>
<p>
<p class="ts-text">
Zoraxy plugins are distributed as binaries, and developers have the flexibility to choose whether to open source them or not
<strong>
as the plugin library and interface are open source under the LGPL license
</strong>
.
</p>
</p>
<p>
<p class="ts-text">
There are two primary types of plugins:
</p>
</p>
<ul>
<li>
<strong>
Router plugins
</strong>
: Involved with connections from HTTP proxy rules.
<br>
</li>
<li>
<strong>
Utility plugins
</strong>
: Provide user interfaces for various network features that operate independently of the Zoraxy core.
<br>
</li>
</ul>
<div class="ts-divider"></div>
<h2 id="how-plugins-are-distributed-installed">
How plugins are distributed &amp; installed
</h2>
<p>
<p class="ts-text">
Zoraxy plugins are distributed as platform-dependent binaries, tailored to specific operating systems and CPU architectures. These binaries follow a naming convention that includes the operating system, CPU architecture, and plugin name, such as
<code>
linux_amd64_foobar
</code>
,
<code>
windows_amd64_foobar.exe
</code>
, or
<code>
linux_arm64_foobar
</code>
.
</p>
</p>
<p>
<p class="ts-text">
To manually install a plugin for testing, place the binary file into the
<code>
/plugins/{plugin_name}/
</code>
folder within your Zoraxy installation directory.
</p>
</p>
<div class="ts-quote">
<p>
<p class="ts-text">
<strong>
Warning:
</strong>
The binary name inside the folder must match the plugin folder name. For example, the binary should be named
<code>
foobar
</code>
(or
<code>
foobar.exe
</code>
on Windows) if placed in the
<code>
/plugins/foobar/
</code>
folder. Avoid using names like
<code>
foobar_plugin.exe
</code>
.
</p>
</p>
</div>
<p>
<p class="ts-text">
For distribution, a plugin store system is used. The plugin store architecture is similar to the one built into the Arduino IDE, with a manager URL (a JSON file) listing all the plugins supported by that store. See the documentation section for more details on how to implement your own plugin store.
</p>
</p>
<div class="ts-divider"></div>
<h2 id="plugin-vs-pull-request">
<div class="ts-header is-large">
Plugin vs Pull Request
</div>
</h2>
<p>
<p class="ts-text">
The Zoraxy plugin was introduced to address specific use cases that enhance its functionality. It serves as an extension to the core Zoraxy system, providing additional features and capabilities while maintaining the integrity of the core system.
</p>
</p>
<ul>
<li>
Designed to handle features that are challenging to integrate directly into the Zoraxy core.
<br>
</li>
<li>
Caters to scenarios where certain features are only applicable in limited situations, avoiding unnecessary resource consumption for other users.
<br>
</li>
<li>
Allows for frequent updates to specific code components without impacting the core&rsquo;s stability or causing downtime.
<br>
</li>
</ul>
<div class="ts-divider"></div>
<h3 id="when-should-you-add-a-core-pr-or-a-plugin">
<div class="ts-header">
When should you add a core PR or a plugin?
</div>
</h3>
<p>
<p class="ts-text">
In certain situations, implementing a feature as a plugin is more reasonable than directly integrating it into the Zoraxy core:
</p>
</p>
<ul>
<li>
<strong>
Core PR
</strong>
: If the feature is relevant to most users and enhances Zoraxy&rsquo;s core functionality, consider submitting a core Pull Request (PR).
<br>
</li>
<li>
<strong>
Plugin
</strong>
: If the feature is targeted at a smaller user base or requires additional dependencies that not all users need, it should be developed as a plugin.
<br>
</li>
</ul>
<p>
The decision depends on the feature&rsquo;s general relevance and its impact on core stability. Plugins offer flexibility without burdening the core.
</p>
</div>
<br>
<br>
</div>
</div>
</div>
</div>
</div>
<div class="ts-container">
<div class="ts-divider"></div>
<div class="ts-content">
<div class="ts-text">
Zoraxy © tobychui
<span class="thisyear">
2025
</span>
</div>
</div>
</div>
<script>
$(".thisyear").text(new Date().getFullYear());
</script>
</body>
</html>

View File

@ -0,0 +1,316 @@
<!DOCTYPE html>
<html lang="en" class="is-white">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
Getting Started | Zoraxy Documentation
</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js" integrity="sha512-LhccdVNGe2QMEfI3x4DVV3ckMRe36TfydKss6mJpdHjNFiV07dFpS2xzeZedptKZrwxfICJpez09iNioiSZ3hA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.js"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Code highlight -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<!-- additional languages -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/go.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/c.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/xml.min.js"></script>
<script>
hljs.highlightAll();
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/tomorrow-night-bright.css">
<style>
#msgbox{
position: fixed;
bottom: 1em;
right: 1em;
z-index: 9999;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
dialog[open] {
animation: fadeIn 0.3s ease-in-out;
}
code{
border-radius: 0.5rem;
}
</style>
<script src="/html/assets/theme.js"></script>
</head>
<body>
<div class="ts-content">
<div class="ts-container">
<div style="float: right;">
<button class="ts-button is-icon" id="darkModeToggle">
<span class="ts-icon is-moon-icon"></span>
</button>
</div>
<div class="ts-tab is-pilled">
<a href="" class="item" style="user-select: none;">
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
</a>
<a href="#!" class="is-active item">
Documents
</a>
<a href="#!" class="item">
Examples
</a>
</div>
</div>
</div>
<div class="ts-divider"></div>
<div>
<div class="has-padded">
<div class="ts-grid mobile:is-stacked">
<div class="column is-4-wide">
<div class="ts-box">
<div class="ts-menu is-end-icon">
<a class="item">
Introduction
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
What is Zoraxy Plugin
</a>
<a class="item is-active" href="/html/1. Introduction/2. Getting Started.html">
Getting Started
</a>
</div>
<a class="item">
Architecture
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
Plugin Architecture
</a>
<a class="item" href="/html/2. Architecture/2. Introspect.html">
Introspect
</a>
<a class="item" href="/html/2. Architecture/3. Configure.html">
Configure
</a>
</div>
<a class="item">
Getting Started
<span class="ts-icon is-caret-down-icon"></span>
</a>
<a class="item" href="/html/index.html">
index
</a>
</div>
</div>
</div>
<div class="column is-12-wide">
<div class="ts-box">
<div class="ts-container is-padded has-top-padded-large">
<h1 id="getting-started">
Getting Started
</h1>
<p>
<p class="ts-text">
To start developing plugins, you will need the following installed on your computer
</p>
</p>
<ol>
<li>
The source code of Zoraxy
</li>
<li>
Go compiler
</li>
<li>
VSCode (recommended, or any editor of your choice)
</li>
</ol>
<div class="ts-divider"></div>
<h2 id="step-1-start-zoraxy-at-least-once">
<div class="ts-header is-large">
Step 1: Start Zoraxy at least once
</div>
</h2>
<p>
<p class="ts-text">
If you have just cloned Zoraxy from the Github repo, use the following to build and run it once.
</p>
</p>
<pre><code class="language-bash">cd src/
go mod tidy
go build
sudo ./zoraxy
</code></pre>
<p>
<p class="ts-text">
This would allow Zoraxy to generate all the required folder structure on startup.
</p>
</p>
<p>
After the startup process completes, you would see a folder named &ldquo;plugins&rdquo; in the working directory of Zoraxy.
</p>
<div class="ts-divider"></div>
<h2 id="steps-2-prepare-the-development-environment-for-zoraxy-plugin">
<div class="ts-header is-large">
Steps 2: Prepare the development environment for Zoraxy Plugin
</div>
</h2>
<p>
Next, you will need to think of a name for your plugin. Lets name our new plugin &ldquo;Lapwing&rdquo;.
</p>
<p>
<p class="ts-text">
<strong>
Notes: Plugin name described in Introspect (will discuss this in later sessions) can contains space, but the folder and compiled binary filename must not contains space and special characters for platform compatibilities reasons.
</strong>
</p>
</p>
<p>
<p class="ts-text">
Follow the steps below to create the folder structure
</p>
</p>
<h3 id="2-1-create-plugin-folder">
<div class="ts-header">
2.1 Create Plugin Folder
</div>
</h3>
<p>
<p class="ts-text">
Create a folder with your plugin name in the
<code>
plugins
</code>
folder. After creating the folder, you would have something like
<code>
plugins/Lapwing/
</code>
.
</p>
</p>
<h3 id="2-2-locate-and-copy-zoraxy-plugin-library">
<div class="ts-header">
2.2 Locate and copy Zoraxy Plugin library
</div>
</h3>
<p>
<p class="ts-text">
Locate the Zoraxy plugin library from the Zoraxy source code. You can find the
<code>
zoraxy_plugin
</code>
Go module under
<code>
src/mod/plugins/zoraxy_plugin
</code>
.
</p>
</p>
<p>
Copy the
<code>
zoraxy_plugin
</code>
folder from the Zoraxy source code mod folder into the your plugin&rsquo;s mod folder. Let assume you use the same mod folder name as Zoraxy as
<code>
mod
</code>
, then your copied library path should be
<code>
plugins/Lapwing/mod/zoraxy_plugin
</code>
.
</p>
<h3 id="2-3-prepare-go-project-structure">
<div class="ts-header">
2.3 Prepare Go Project structure
</div>
</h3>
<p>
<p class="ts-text">
Create the
<code>
main.go
</code>
file for your plugin. In the example above, it would be located at
<code>
plugins/Lapwing/main.go
</code>
.
</p>
</p>
<p>
<p class="ts-text">
Use
<code>
go mod init yourdomain.com/foo/plugin_name
</code>
to initiate your plugin. By default the
<code>
go.mod
</code>
file will be automatically generated by the go compiler. Assuming you are developing Lapwing with its source located on Github, this command would be
<code>
go mod init github.com/your_user_name/Lapwing
</code>
.
</p>
</p>
<div class="ts-divider"></div>
<h2 id="steps-3-open-plugin-folder-in-ide">
<div class="ts-header is-large">
Steps 3: Open plugin folder in IDE
</div>
</h2>
<p>
<p class="ts-text">
Now open your preferred IDE or text editor and use your plugin folder as the project folder
</p>
</p>
<p>
<p class="ts-text">
Now, you are ready to start developing Zoraxy plugin!
</p>
</p>
</div>
<br>
<br>
</div>
</div>
</div>
</div>
</div>
<div class="ts-container">
<div class="ts-divider"></div>
<div class="ts-content">
<div class="ts-text">
Zoraxy © tobychui
<span class="thisyear">
2025
</span>
</div>
</div>
</div>
<script>
$(".thisyear").text(new Date().getFullYear());
</script>
</body>
</html>

View File

@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en" class="is-white">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
Plugin Architecture | Zoraxy Documentation
</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js" integrity="sha512-LhccdVNGe2QMEfI3x4DVV3ckMRe36TfydKss6mJpdHjNFiV07dFpS2xzeZedptKZrwxfICJpez09iNioiSZ3hA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.js"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Code highlight -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<!-- additional languages -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/go.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/c.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/xml.min.js"></script>
<script>
hljs.highlightAll();
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/tomorrow-night-bright.css">
<style>
#msgbox{
position: fixed;
bottom: 1em;
right: 1em;
z-index: 9999;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
dialog[open] {
animation: fadeIn 0.3s ease-in-out;
}
code{
border-radius: 0.5rem;
}
</style>
<script src="/html/assets/theme.js"></script>
</head>
<body>
<div class="ts-content">
<div class="ts-container">
<div style="float: right;">
<button class="ts-button is-icon" id="darkModeToggle">
<span class="ts-icon is-moon-icon"></span>
</button>
</div>
<div class="ts-tab is-pilled">
<a href="" class="item" style="user-select: none;">
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
</a>
<a href="#!" class="is-active item">
Documents
</a>
<a href="#!" class="item">
Examples
</a>
</div>
</div>
</div>
<div class="ts-divider"></div>
<div>
<div class="has-padded">
<div class="ts-grid mobile:is-stacked">
<div class="column is-4-wide">
<div class="ts-box">
<div class="ts-menu is-end-icon">
<a class="item">
Introduction
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
What is Zoraxy Plugin
</a>
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
Getting Started
</a>
</div>
<a class="item">
Architecture
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item is-active" href="/html/2. Architecture/1. Plugin Architecture.html">
Plugin Architecture
</a>
<a class="item" href="/html/2. Architecture/2. Introspect.html">
Introspect
</a>
<a class="item" href="/html/2. Architecture/3. Configure.html">
Configure
</a>
</div>
<a class="item">
Getting Started
<span class="ts-icon is-caret-down-icon"></span>
</a>
<a class="item" href="/html/index.html">
index
</a>
</div>
</div>
</div>
<div class="column is-12-wide">
<div class="ts-box">
<div class="ts-container is-padded has-top-padded-large">
<h1 id="plugin-architecture">
Plugin Architecture
</h1>
<p>
<p class="ts-text">
The Zoraxy Plugin uses a 3 steps approach to get information from plugin, setup the plugin and forward request to plugin. The name of the steps are partially referred from dbus designs as followings.
</p>
</p>
<ol>
<li>
Introspect
</li>
<li>
Configure
</li>
<li>
Forwarding
</li>
</ol>
<p>
<p class="ts-text">
The overall flow looks like this.
</p>
</p>
<p>
<div class="ts-image is-rounded" style="max-width: 800px">
<img src="img/1. Plugin Architecture/plugin_workflow.png" alt="" />
</div>
</p>
</div>
<br>
<br>
</div>
</div>
</div>
</div>
</div>
<div class="ts-container">
<div class="ts-divider"></div>
<div class="ts-content">
<div class="ts-text">
Zoraxy © tobychui
<span class="thisyear">
2025
</span>
</div>
</div>
</div>
<script>
$(".thisyear").text(new Date().getFullYear());
</script>
</body>
</html>

View File

@ -0,0 +1,268 @@
<!DOCTYPE html>
<html lang="en" class="is-white">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
Introspect | Zoraxy Documentation
</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js" integrity="sha512-LhccdVNGe2QMEfI3x4DVV3ckMRe36TfydKss6mJpdHjNFiV07dFpS2xzeZedptKZrwxfICJpez09iNioiSZ3hA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.js"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Code highlight -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<!-- additional languages -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/go.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/c.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/xml.min.js"></script>
<script>
hljs.highlightAll();
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/tomorrow-night-bright.css">
<style>
#msgbox{
position: fixed;
bottom: 1em;
right: 1em;
z-index: 9999;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
dialog[open] {
animation: fadeIn 0.3s ease-in-out;
}
code{
border-radius: 0.5rem;
}
</style>
<script src="/html/assets/theme.js"></script>
</head>
<body>
<div class="ts-content">
<div class="ts-container">
<div style="float: right;">
<button class="ts-button is-icon" id="darkModeToggle">
<span class="ts-icon is-moon-icon"></span>
</button>
</div>
<div class="ts-tab is-pilled">
<a href="" class="item" style="user-select: none;">
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
</a>
<a href="#!" class="is-active item">
Documents
</a>
<a href="#!" class="item">
Examples
</a>
</div>
</div>
</div>
<div class="ts-divider"></div>
<div>
<div class="has-padded">
<div class="ts-grid mobile:is-stacked">
<div class="column is-4-wide">
<div class="ts-box">
<div class="ts-menu is-end-icon">
<a class="item">
Introduction
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
What is Zoraxy Plugin
</a>
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
Getting Started
</a>
</div>
<a class="item">
Architecture
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
Plugin Architecture
</a>
<a class="item is-active" href="/html/2. Architecture/2. Introspect.html">
Introspect
</a>
<a class="item" href="/html/2. Architecture/3. Configure.html">
Configure
</a>
</div>
<a class="item">
Getting Started
<span class="ts-icon is-caret-down-icon"></span>
</a>
<a class="item" href="/html/index.html">
index
</a>
</div>
</div>
</div>
<div class="column is-12-wide">
<div class="ts-box">
<div class="ts-container is-padded has-top-padded-large">
<h1 id="introspect">
Introspect
</h1>
<p>
<p class="ts-text">
Introspect, similar to the one in dbus design, is used to get the information from plugin when Zoraxy starts (or manually triggered in development mode or force reload plugin list).
</p>
</p>
<p>
<p class="ts-text">
<strong>
This is a pre-defined structure where the plugin must provide to Zoraxy
</strong>
when the plugin is being started with the
<code>
-introspect
</code>
flag.
</p>
</p>
<p>
<p class="ts-text">
The introspect structure is defined under the
<code>
zoraxy_plugin
</code>
library, where both Zoraxy and plugin should use. As of writing, the structure of introspect is like this.
</p>
</p>
<pre><code class="language-go">type IntroSpect struct {
/* Plugin metadata */
ID string `json:&quot;id&quot;` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname
Name string `json:&quot;name&quot;` //Name of your plugin
Author string `json:&quot;author&quot;` //Author name of your plugin
AuthorContact string `json:&quot;author_contact&quot;` //Author contact of your plugin, like email
Description string `json:&quot;description&quot;` //Description of your plugin
URL string `json:&quot;url&quot;` //URL of your plugin
Type PluginType `json:&quot;type&quot;` //Type of your plugin, Router(0) or Utilities(1)
VersionMajor int `json:&quot;version_major&quot;` //Major version of your plugin
VersionMinor int `json:&quot;version_minor&quot;` //Minor version of your plugin
VersionPatch int `json:&quot;version_patch&quot;` //Patch version of your plugin
/*
Endpoint Settings
*/
/*
Static Capture Settings
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
This is faster than dynamic capture, but less flexible
*/
StaticCapturePaths []StaticCaptureRule `json:&quot;static_capture_paths&quot;` //Static capture paths of your plugin, see Zoraxy documentation for more details
StaticCaptureIngress string `json:&quot;static_capture_ingress&quot;` //Static capture ingress path of your plugin (e.g. /s_handler)
/*
Dynamic Capture Settings
Once plugin is enabled, these rules will be captured and forward to plugin sniff
if the plugin sniff returns 280, the traffic will be captured
otherwise, the traffic will be forwarded to the next plugin
This is slower than static capture, but more flexible
*/
DynamicCaptureSniff string `json:&quot;dynamic_capture_sniff&quot;` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
DynamicCaptureIngress string `json:&quot;dynamic_capture_ingress&quot;` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
/* UI Path for your plugin */
UIPath string `json:&quot;ui_path&quot;` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
/* Subscriptions Settings */
SubscriptionPath string `json:&quot;subscription_path&quot;` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
SubscriptionsEvents map[string]string `json:&quot;subscriptions_events&quot;` //Subscriptions events of your plugin, see Zoraxy documentation for more details
}
</code></pre>
<h2 id="introspect-triggering">
<div class="ts-header is-large">
Introspect Triggering
</div>
</h2>
<p>
<p class="ts-text">
To manually test if the introspect return is correct, you can try using the
<code>
-introspect
</code>
flag on any Zoraxy plugin. You should be able to see an output like so.
</p>
</p>
<pre><code class="language-json">$ ./debugger -introspect
{
&quot;id&quot;: &quot;org.aroz.zoraxy.debugger&quot;,
&quot;name&quot;: &quot;Plugin Debugger&quot;,
&quot;author&quot;: &quot;aroz.org&quot;,
&quot;author_contact&quot;: &quot;https://aroz.org&quot;,
&quot;description&quot;: &quot;A debugger for Zoraxy \u003c-\u003e plugin communication pipeline&quot;,
&quot;url&quot;: &quot;https://zoraxy.aroz.org&quot;,
&quot;type&quot;: 0,
&quot;version_major&quot;: 1,
&quot;version_minor&quot;: 0,
&quot;version_patch&quot;: 0,
&quot;static_capture_paths&quot;: [
{
&quot;capture_path&quot;: &quot;/test_a&quot;
},
{
&quot;capture_path&quot;: &quot;/test_b&quot;
}
],
&quot;static_capture_ingress&quot;: &quot;/s_capture&quot;,
&quot;dynamic_capture_sniff&quot;: &quot;/d_sniff&quot;,
&quot;dynamic_capture_ingress&quot;: &quot;/d_capture&quot;,
&quot;ui_path&quot;: &quot;/debug&quot;,
&quot;subscription_path&quot;: &quot;&quot;,
&quot;subscriptions_events&quot;: null
}
</code></pre>
</div>
<br>
<br>
</div>
</div>
</div>
</div>
</div>
<div class="ts-container">
<div class="ts-divider"></div>
<div class="ts-content">
<div class="ts-text">
Zoraxy © tobychui
<span class="thisyear">
2025
</span>
</div>
</div>
</div>
<script>
$(".thisyear").text(new Date().getFullYear());
</script>
</body>
</html>

View File

@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="en" class="is-white">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
Configure | Zoraxy Documentation
</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js" integrity="sha512-LhccdVNGe2QMEfI3x4DVV3ckMRe36TfydKss6mJpdHjNFiV07dFpS2xzeZedptKZrwxfICJpez09iNioiSZ3hA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.js"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Code highlight -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<!-- additional languages -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/go.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/c.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/xml.min.js"></script>
<script>
hljs.highlightAll();
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/tomorrow-night-bright.css">
<style>
#msgbox{
position: fixed;
bottom: 1em;
right: 1em;
z-index: 9999;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
dialog[open] {
animation: fadeIn 0.3s ease-in-out;
}
code{
border-radius: 0.5rem;
}
</style>
<script src="/html/assets/theme.js"></script>
</head>
<body>
<div class="ts-content">
<div class="ts-container">
<div style="float: right;">
<button class="ts-button is-icon" id="darkModeToggle">
<span class="ts-icon is-moon-icon"></span>
</button>
</div>
<div class="ts-tab is-pilled">
<a href="" class="item" style="user-select: none;">
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
</a>
<a href="#!" class="is-active item">
Documents
</a>
<a href="#!" class="item">
Examples
</a>
</div>
</div>
</div>
<div class="ts-divider"></div>
<div>
<div class="has-padded">
<div class="ts-grid mobile:is-stacked">
<div class="column is-4-wide">
<div class="ts-box">
<div class="ts-menu is-end-icon">
<a class="item">
Introduction
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
What is Zoraxy Plugin
</a>
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
Getting Started
</a>
</div>
<a class="item">
Architecture
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
Plugin Architecture
</a>
<a class="item" href="/html/2. Architecture/2. Introspect.html">
Introspect
</a>
<a class="item is-active" href="/html/2. Architecture/3. Configure.html">
Configure
</a>
</div>
<a class="item">
Getting Started
<span class="ts-icon is-caret-down-icon"></span>
</a>
<a class="item" href="/html/index.html">
index
</a>
</div>
</div>
</div>
<div class="column is-12-wide">
<div class="ts-box">
<div class="ts-container is-padded has-top-padded-large">
<h1 id="configure">
Configure
</h1>
<p>
<p class="ts-text">
Configure or Configure Spec is the
<code>
exec
</code>
call where Zoraxy start the plugin. The configure spec JSON structure is defined in
<code>
zoraxy_plugin
</code>
library.
</p>
</p>
<p>
<p class="ts-text">
As the time of writing, the
<code>
ConfigureSpec
</code>
only contains information on some basic info.
</p>
</p>
<pre><code class="language-go">type ConfigureSpec struct {
Port int `json:&quot;port&quot;` //Port to listen
RuntimeConst RuntimeConstantValue `json:&quot;runtime_const&quot;` //Runtime constant values
//To be expanded
}
</code></pre>
<p>
<p class="ts-text">
The
<code>
ConfigureSpec
</code>
struct will be parsed to JSON and pass to your plugin via the
<code>
-configure=(json payload here)
</code>
.
</p>
</p>
<p>
<p class="ts-text">
In your plugin, you can use the
<code>
zoraxy_plugin
</code>
library to parse it or parse it manually (if you are developing a plugin with other languages).
</p>
</p>
</div>
<br>
<br>
</div>
</div>
</div>
</div>
</div>
<div class="ts-container">
<div class="ts-divider"></div>
<div class="ts-content">
<div class="ts-text">
Zoraxy © tobychui
<span class="thisyear">
2025
</span>
</div>
</div>
</div>
<script>
$(".thisyear").text(new Date().getFullYear());
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,51 @@
/* Things to do before body loads */
function restoreDarkMode(){
if (localStorage.getItem("darkMode") === "enabled") {
$("html").addClass("is-dark");
$("html").removeClass("is-white");
} else {
$("html").removeClass("is-dark");
$("html").addClass("is-white");
}
}
restoreDarkMode();
function updateElementToTheme(isDarkTheme=false){
if (!isDarkTheme){
let whiteSrc = $("#sysicon").attr("white_src");
$("#sysicon").attr("src", whiteSrc);
$("#darkModeToggle").html(`<span class="ts-icon is-sun-icon"></span>`);
// Update the rendering text color in the garphs
if (typeof(changeScaleTextColor) != "undefined"){
changeScaleTextColor("black");
}
}else{
let darkSrc = $("#sysicon").attr("dark_src");
$("#sysicon").attr("src", darkSrc);
$("#darkModeToggle").html(`<span class="ts-icon is-moon-icon"></span>`);
// Update the rendering text color in the garphs
if (typeof(changeScaleTextColor) != "undefined"){
changeScaleTextColor("white");
}
}
}
/* Things to do after body loads */
$(document).ready(function(){
$("#darkModeToggle").on("click", function() {
$("html").toggleClass("is-dark");
$("html").toggleClass("is-white");
if ($("html").hasClass("is-dark")) {
localStorage.setItem("darkMode", "enabled");
updateElementToTheme(true);
} else {
localStorage.setItem("darkMode", "disabled");
updateElementToTheme(false);
}
});
updateElementToTheme(localStorage.getItem("darkMode") === "enabled");
});

View File

@ -0,0 +1,203 @@
<!DOCTYPE html>
<html lang="en" class="is-white">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
index | Zoraxy Documentation
</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js" integrity="sha512-LhccdVNGe2QMEfI3x4DVV3ckMRe36TfydKss6mJpdHjNFiV07dFpS2xzeZedptKZrwxfICJpez09iNioiSZ3hA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.js"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Code highlight -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<!-- additional languages -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/go.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/c.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/xml.min.js"></script>
<script>
hljs.highlightAll();
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/tomorrow-night-bright.css">
<style>
#msgbox{
position: fixed;
bottom: 1em;
right: 1em;
z-index: 9999;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
dialog[open] {
animation: fadeIn 0.3s ease-in-out;
}
code{
border-radius: 0.5rem;
}
</style>
<script src="/html/assets/theme.js"></script>
</head>
<body>
<div class="ts-content">
<div class="ts-container">
<div style="float: right;">
<button class="ts-button is-icon" id="darkModeToggle">
<span class="ts-icon is-moon-icon"></span>
</button>
</div>
<div class="ts-tab is-pilled">
<a href="" class="item" style="user-select: none;">
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
</a>
<a href="#!" class="is-active item">
Documents
</a>
<a href="#!" class="item">
Examples
</a>
</div>
</div>
</div>
<div class="ts-divider"></div>
<div>
<div class="has-padded">
<div class="ts-grid mobile:is-stacked">
<div class="column is-4-wide">
<div class="ts-box">
<div class="ts-menu is-end-icon">
<a class="item">
Introduction
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
What is Zoraxy Plugin
</a>
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
Getting Started
</a>
</div>
<a class="item">
Architecture
<span class="ts-icon is-caret-down-icon"></span>
</a>
<div class="ts-menu is-dense is-small is-horizontally-padded">
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
Plugin Architecture
</a>
<a class="item" href="/html/2. Architecture/2. Introspect.html">
Introspect
</a>
<a class="item" href="/html/2. Architecture/3. Configure.html">
Configure
</a>
</div>
<a class="item">
Getting Started
<span class="ts-icon is-caret-down-icon"></span>
</a>
<a class="item is-active" href="/html/index.html">
index
</a>
</div>
</div>
</div>
<div class="column is-12-wide">
<div class="ts-box">
<div class="ts-container is-padded has-top-padded-large">
<h1 id="index">
Index
</h1>
<p>
Welcome to the Zoraxy Plugin Documentation!
<br>
Click on a topic in the side menu to begin navigating through the available resources and guides for developing and managing plugins.
</p>
<h2 id="faq">
<div class="ts-header is-large">
FAQ
</div>
</h2>
<h3 id="what-skills-do-i-need-for-developing-a-plugin">
<div class="ts-header">
What skills do I need for developing a plugin?
</div>
</h3>
<p>
<p class="ts-text">
Basic HTML, JavaScript, and CSS skills are required, with Go (Golang) being the preferred backend language. However, any programming language that can be compiled into a binary and provide a web server interface will work.
</p>
</p>
<h3 id="will-a-plugin-crash-the-whole-zoraxy">
<div class="ts-header">
Will a plugin crash the whole Zoraxy?
</div>
</h3>
<p>
<p class="ts-text">
No. Plugins operate in a separate process from Zoraxy. If a plugin crashes, Zoraxy will terminate and disable that plugin without affecting the core operations. This is by design to ensure stability.
</p>
</p>
<h3 id="can-i-sell-my-plugin">
<div class="ts-header">
Can I sell my plugin?
</div>
</h3>
<p>
<p class="ts-text">
Yes, the plugin library and interface design are open source under the LGPL license. You are not required to disclose the source code of your plugin as long as you do not modify the plugin library and use it as-is. For more details on how to comply with the license, refer to the licensing documentation.
</p>
</p>
<h3 id="how-can-i-add-my-plugin-to-the-official-plugin-store">
<div class="ts-header">
How can I add my plugin to the official plugin store?
</div>
</h3>
<p>
<p class="ts-text">
To add your plugin to the official plugin store, open a pull request (PR) in the plugin repository.
</p>
</p>
</div>
<br>
<br>
</div>
</div>
</div>
</div>
</div>
<div class="ts-container">
<div class="ts-divider"></div>
<div class="ts-content">
<div class="ts-text">
Zoraxy © tobychui
<span class="thisyear">
2025
</span>
</div>
</div>
</div>
<script>
$(".thisyear").text(new Date().getFullYear());
</script>
</body>
</html>

View File

@ -1,175 +1,12 @@
<!DOCTYPE html>
<html lang="en" class="is-white">
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title i18n>
Plugin Doc | Zoraxy
</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.js"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Locales -->
<script src="./js/dom-i18n.min.js"></script>
<script src="./js/theme.js"></script>
<style>
#msgbox{
position: fixed;
bottom: 1em;
right: 1em;
z-index: 9999;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
dialog[open] {
animation: fadeIn 0.3s ease-in-out;
}
</style>
<script>
/* Things to do before body loads */
function restoreDarkMode(){
if (localStorage.getItem("darkMode") === "enabled") {
$("html").addClass("is-dark");
$("html").removeClass("is-white");
} else {
$("html").removeClass("is-dark");
$("html").addClass("is-white");
}
}
restoreDarkMode();
function updateElementToTheme(isDarkTheme=false){
if (!isDarkTheme){
$("#sysicon").attr("src", "./img/logo.png");
$("#darkModeToggle").html(`<span class="ts-icon is-sun-icon"></span>`);
// Update the rendering text color in the garphs
if (typeof(changeScaleTextColor) != "undefined"){
changeScaleTextColor("black");
}
}else{
$("#sysicon").attr("src", "./img/logo_white.png");
$("#darkModeToggle").html(`<span class="ts-icon is-moon-icon"></span>`);
// Update the rendering text color in the garphs
if (typeof(changeScaleTextColor) != "undefined"){
changeScaleTextColor("white");
}
}
}
/* Things to do after body loads */
$(document).ready(function(){
$("#darkModeToggle").on("click", function() {
$("html").toggleClass("is-dark");
$("html").toggleClass("is-white");
if ($("html").hasClass("is-dark")) {
localStorage.setItem("darkMode", "enabled");
updateElementToTheme(true);
} else {
localStorage.setItem("darkMode", "disabled");
updateElementToTheme(false);
}
});
updateElementToTheme(localStorage.getItem("darkMode") === "enabled");
});
</script>
<meta http-equiv="refresh" content="0; url=html/1.%20Introduction/1.%20Getting%20Started.html#!" />
<title>Redirecting...</title>
</head>
<body>
<div class="ts-content">
<div class="ts-container">
<div style="float: right;">
<button class="ts-button is-icon" id="darkModeToggle">
<span class="ts-icon is-moon-icon"></span>
</button>
</div>
<div class="ts-tab is-pilled">
<a href="" class="item" style="user-select: none;">
<img id="sysicon" class="ts-image" style="height: 30px" src="img/logo.svg"></img>
</a>
<button href="#!" class="is-active item" data-tab="tab-intro">
Introduction
</button>
<button href="#!" class="item" data-tab="tab-documents">
Dev Guide
</button>
<button href="#!" class="item" data-tab="tab-examples">
Examples
</button>
</div>
</div>
</div>
<div class="ts-divider"></div>
<div>
<div class="panel-component" id="tab-intro" component="intro.html"></div>
<div class="panel-component" id="tab-documents" component="documents.html"></div>
<div class="panel-component" id="tab-examples" component="examples.html"></div>
</div>
<div class="ts-container">
<div class="ts-divider"></div>
<div class="ts-content">
<div class="ts-text">
Zoraxy © tobychui <span class="thisyear">2025</span>
</div>
</div>
</div>
<div id="msgbox" class="ts-snackbar has-start-padded-large has-end-padded-large">
<div class="content"></div>
<button class="close"></button>
</div>
<script>
$(".thisyear").text(new Date().getFullYear());
function msgbox(msg, delay=3000){
$("#msgbox .content").text(msg);
$("#msgbox").stop().finish().fadeIn(200).delay(delay).fadeOut(200);
}
$("#msgbox .close").click(function(){
$("#msgbox").stop().finish().fadeOut(200);
});
$("#msgbox").hide();
//Load the content of the panel-component
$(".panel-component").each(function() {
var component = $(this);
var url = component.attr("component");
$(this).load(url, function(response, status, xhr) {
if (status == "error") {
var msg = "Error loading component: " + xhr.status + " " + xhr.statusText;
console.log(msg);
msgbox(msg);
} else {
// Optionally, you can add a success message here
// msgbox("Component loaded successfully");
}
});
});
function switchTabTo(tabid){
$(".ts-tab .item").removeClass("is-active");
$(".panel-component").addClass("has-hidden");
$("#" + tabid).removeClass("has-hidden").addClass("is-active");
$("button[data-tab='" + tabid + "']").addClass("is-active");
}
</script>
<p>If you are not redirected automatically, follow this <a href="html/1.%20Introduction/1.%20Getting%20Started.html#!">link</a>.</p>
</body>
</html>

56
docs/plugins/index.json Normal file
View File

@ -0,0 +1,56 @@
{
"title": "",
"path": ".",
"type": "folder",
"files": [
{
"title": "Introduction",
"path": "1. Introduction",
"type": "folder",
"files": [
{
"filename": "1. Introduction/1. What is Zoraxy Plugin.md",
"title": "What is Zoraxy Plugin",
"type": "file"
},
{
"filename": "1. Introduction/2. Getting Started.md",
"title": "Getting Started",
"type": "file"
}
]
},
{
"title": "Architecture",
"path": "2. Architecture",
"type": "folder",
"files": [
{
"filename": "2. Architecture/1. Plugin Architecture.md",
"title": "Plugin Architecture",
"type": "file"
},
{
"filename": "2. Architecture/2. Introspect.md",
"title": "Introspect",
"type": "file"
},
{
"filename": "2. Architecture/3. Configure.md",
"title": "Configure",
"type": "file"
}
]
},
{
"title": "Getting Started",
"path": "3. Getting Started",
"type": "folder"
},
{
"filename": "index.md",
"title": "index",
"type": "file"
}
]
}

38
docs/plugins/main.go Normal file
View File

@ -0,0 +1,38 @@
package main
import (
"flag"
"fmt"
"log"
"net/http"
)
type FileInfo struct {
Filename string `json:"filename"`
Title string `json:"title"`
Type string `json:"type"`
}
/* Change this before deploying */
var (
mode = flag.String("m", "web", "Mode to run the application: 'web' or 'build'")
root_url = flag.String("root", "/html/", "Root URL for the web server")
)
func main() {
flag.Parse()
switch *mode {
case "build":
build()
default:
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.FileServer(http.Dir("./")).ServeHTTP(w, r)
})
fmt.Println("Starting server at :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
}

74
docs/plugins/menugen.go Normal file
View File

@ -0,0 +1,74 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"path/filepath"
"strings"
)
type MenuItem struct {
Title string `json:"title"`
Path string `json:"path"`
Type string `json:"type"`
Files []MenuItem `json:"files,omitempty"`
Filename string `json:"filename,omitempty"`
}
func generateSideMenu(doctreeJson string, selectedTitle string) (string, error) {
var root MenuItem
if err := json.Unmarshal([]byte(doctreeJson), &root); err != nil {
return "", err
}
var buffer bytes.Buffer
buffer.WriteString(`<div class="ts-box">
<div class="ts-menu is-end-icon">`)
for _, item := range root.Files {
writeMenuItem(&buffer, item, selectedTitle)
}
buffer.WriteString(`
</div>
</div>`)
return buffer.String(), nil
}
func writeMenuItem(buffer *bytes.Buffer, item MenuItem, selectedTitle string) {
if item.Type == "file" {
activeClass := ""
if item.Title == selectedTitle {
activeClass = " is-active"
}
//Generate the URL for the file
filePath := item.Path
if item.Filename != "" {
filePath = fmt.Sprintf("%s/%s", item.Path, strings.ReplaceAll(item.Filename, ".md", ".html"))
}
urlPath := filepath.ToSlash(filepath.Clean(*root_url + filePath))
buffer.WriteString(fmt.Sprintf(`
<a class="item%s" href="%s">
%s
</a>`, activeClass, urlPath, item.Title))
} else if item.Type == "folder" {
buffer.WriteString(fmt.Sprintf(`
<a class="item">
%s
<span class="ts-icon is-caret-down-icon"></span>
</a>`, item.Title))
if len(item.Files) > 0 {
buffer.WriteString(`
<div class="ts-menu is-dense is-small is-horizontally-padded">`)
for _, subItem := range item.Files {
writeMenuItem(buffer, subItem, selectedTitle)
}
buffer.WriteString(`
</div>`)
}
}
}

View File

@ -1,3 +1,3 @@
#!/bin/bash
# This script sets up the environment and runs a Go web server for documentation debug purpose
go run ./webserv.go
./docs.exe

View File

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="en" class="is-white">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
{{title}}
</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js" integrity="sha512-LhccdVNGe2QMEfI3x4DVV3ckMRe36TfydKss6mJpdHjNFiV07dFpS2xzeZedptKZrwxfICJpez09iNioiSZ3hA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.js"></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Code highlight -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<!-- additional languages -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/go.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/c.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/xml.min.js"></script>
<script>hljs.highlightAll();</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/tomorrow-night-bright.css">
<style>
#msgbox{
position: fixed;
bottom: 1em;
right: 1em;
z-index: 9999;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
dialog[open] {
animation: fadeIn 0.3s ease-in-out;
}
code{
border-radius: 0.5rem;
}
</style>
<script src="{{root_url}}assets/theme.js"></script>
</head>
<body>
<div class="ts-content">
<div class="ts-container">
<div style="float: right;">
<button class="ts-button is-icon" id="darkModeToggle">
<span class="ts-icon is-moon-icon"></span>
</button>
</div>
<div class="ts-tab is-pilled">
<a href="" class="item" style="user-select: none;">
<img id="sysicon" class="ts-image" style="height: 30px" white_src="{{root_url}}assets/logo.png" dark_src="{{root_url}}assets/logo_white.png" src="{{root_url}}assets/logo.png"></img>
</a>
<a href="#!" class="is-active item">
Documents
</a>
<a href="#!" class="item">
Examples
</a>
</div>
</div>
</div>
<div class="ts-divider"></div>
<div>
<div class="has-padded">
<div class="ts-grid mobile:is-stacked">
<div class="column is-4-wide">
{{sideMenu}}
</div>
<div class="column is-12-wide">
<div class="ts-box">
<div class="ts-container is-padded has-top-padded-large">
{{content}}
</div>
<br><br>
</div>
</div>
</div>
</div>
</div>
<div class="ts-container">
<div class="ts-divider"></div>
<div class="ts-content">
<div class="ts-text">
Zoraxy © tobychui <span class="thisyear">2025</span>
</div>
</div>
</div>
<script>
$(".thisyear").text(new Date().getFullYear());
</script>
</body>
</html>

View File

@ -1,15 +0,0 @@
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.Handle("/", http.FileServer(http.Dir("./")))
fmt.Println("Starting server at :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}