├── gradle.properties ├── src ├── main │ ├── resources │ │ ├── logo.png │ │ ├── extensions │ │ │ ├── reverseProxy.yaml │ │ │ ├── roleTemplate.yaml │ │ │ └── settings.yaml │ │ ├── plugin.yaml │ │ └── static │ │ │ ├── lib │ │ │ ├── AntVG2Handler.js │ │ │ ├── AntVX6Handler.js │ │ │ ├── FormatterInit.js │ │ │ ├── DirectorySort.js │ │ │ └── CustomDom.js │ │ │ └── css │ │ │ └── tool-bench.css │ └── java │ │ └── run │ │ └── halo │ │ └── toolbench │ │ ├── entity │ │ ├── CityInfo.java │ │ ├── SettingsReader.java │ │ ├── RendererReader.java │ │ └── WeatherResponse.java │ │ ├── processor │ │ ├── PostProcessor.java │ │ ├── PageProcessor.java │ │ └── HeadProcessor.java │ │ ├── ToolBenchPlugin.java │ │ ├── infra │ │ ├── GzipResponse.java │ │ ├── GraphQLBuilder.java │ │ ├── GraphQLReader.java │ │ ├── GeoLiteReader.java │ │ └── ProcessorHandler.java │ │ ├── router │ │ ├── GraphQLRouter.java │ │ ├── DirectoryRouter.java │ │ └── QWeatherRouter.java │ │ ├── config │ │ ├── CaffeineCacheConfiguration.java │ │ └── ConfigFolderConfiguration.java │ │ ├── renderer │ │ └── CustomHeadingRenderer.java │ │ └── util │ │ ├── DomBuilder.java │ │ ├── InferStream.java │ │ ├── PostUtil.java │ │ └── FutureDownloader.java └── test │ └── java │ └── cn │ └── dioxide │ └── test │ ├── InferStreamTest.java │ ├── MarkdownTest.java │ ├── CaffeineTest.java │ └── WebDownloaderTest.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── OWNERS ├── settings.gradle ├── package.json ├── .github └── ISSUE_TEMPLATE │ └── bug-report.md ├── .gitignore ├── README.md ├── gradlew.bat ├── gradlew ├── pnpm-lock.yaml └── LICENSE /gradle.properties: -------------------------------------------------------------------------------- 1 | version=1.0.4 2 | -------------------------------------------------------------------------------- /src/main/resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoritzArena/Tool-Bench/HEAD/src/main/resources/logo.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoritzArena/Tool-Bench/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | reviewers: 2 | - ruibaby 3 | - guqing 4 | - JohnNiang 5 | - wangzhen-fit2cloud 6 | 7 | approvers: 8 | - ruibaby 9 | - guqing 10 | - JohnNiang 11 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | rootProject.name = 'plugin-tool-bench' 8 | 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0", 4 | "@types/prismjs": "^1.26.0", 5 | "prismjs": "^1.29.0", 6 | "vite-plugin-prismjs": "^0.0.8" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/extensions/reverseProxy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: plugin.halo.run/v1alpha1 2 | kind: ReverseProxy 3 | metadata: 4 | name: plugin-tool-bench-reverse-proxy 5 | rules: 6 | - path: /static/** 7 | file: 8 | directory: static 9 | -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/entity/CityInfo.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench.entity; 2 | 3 | /** 4 | * @author Dioxide.CN 5 | * @date 2023/7/28 6 | * @since 1.0 7 | */ 8 | public record CityInfo(String cityId, String cityName) { 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/entity/SettingsReader.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench.entity; 2 | 3 | import lombok.*; 4 | 5 | /** 6 | * @author Dioxide.CN 7 | * @date 2023/7/21 8 | * @since 1.0 9 | */ 10 | @Data 11 | @Getter 12 | @Setter 13 | @ToString 14 | @EqualsAndHashCode 15 | public class SettingsReader { 16 | String directory; 17 | String githubToken; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/resources/extensions/roleTemplate.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1alpha1 2 | kind: "Role" 3 | metadata: 4 | name: tool-bench-graphql-use 5 | labels: 6 | halo.run/role-template: "true" 7 | halo.run/hidden: "true" 8 | annotations: 9 | rbac.authorization.halo.run/dependencies: | 10 | [ "role-template-own-permissions", "role-template-public-apis" ] 11 | rules: 12 | - nonResourceURLs: [ "/apis/api.plugin.halo.run/v1alpha1/plugins/ToolBench/github/*", "/apis/api.plugin.halo.run/v1alpha1/plugins/ToolBench/weather/*" ] 13 | verbs: [ "get" ] -------------------------------------------------------------------------------- /src/main/resources/plugin.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: plugin.halo.run/v1alpha1 2 | kind: Plugin 3 | metadata: 4 | name: ToolBench 5 | spec: 6 | enabled: true 7 | version: 1.0.4 8 | requires: ">=2.8.0" 9 | author: 10 | name: Dioxide.CN 11 | website: https://dioxide-cn.ink/ 12 | logo: https://dioxide-cn.ink/upload/logo-bopd.png 13 | settingName: plugin-tool-bench-settings 14 | configMapName: plugin-tool-bench-configMap 15 | homepage: https://github.com/DioxideCN/Tool-Bench 16 | displayName: "ToolBench" 17 | description: "ToolBench为2.8.0及更高版本的Halo集成了更多工具和API" 18 | license: 19 | - name: "GNU GPLv3" 20 | -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/entity/RendererReader.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench.entity; 2 | 3 | import lombok.*; 4 | 5 | import java.util.LinkedHashMap; 6 | import java.util.List; 7 | 8 | /** 9 | * @author Dioxide.CN 10 | * @date 2023/7/21 11 | * @since 1.0 12 | */ 13 | @Data 14 | @Getter 15 | @Setter 16 | @ToString 17 | @EqualsAndHashCode 18 | public class RendererReader { 19 | Boolean antvG2; 20 | Boolean antvX6; 21 | LinkedHashMap codeHead; 22 | LinkedHashMap codeTail; 23 | LinkedHashMap prism; 24 | List> customElementPrefix; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/entity/WeatherResponse.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench.entity; 2 | 3 | import com.nimbusds.jose.shaded.gson.annotations.SerializedName; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author Dioxide.CN 11 | * @date 2023/7/28 12 | * @since 1.0 13 | */ 14 | @Getter 15 | @Setter 16 | public class WeatherResponse { 17 | @SerializedName("location") 18 | private List location; 19 | 20 | @Getter 21 | @Setter 22 | public static class Location { 23 | @SerializedName("id") 24 | private String id; 25 | @SerializedName("name") 26 | private String name; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/processor/PostProcessor.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench.processor; 2 | 3 | import jakarta.annotation.Resource; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.springframework.stereotype.Component; 6 | import reactor.core.publisher.Mono; 7 | import run.halo.app.theme.ReactivePostContentHandler; 8 | import run.halo.toolbench.infra.ProcessorHandler; 9 | 10 | /** 11 | * @author Dioxide.CN 12 | * @date 2023/8/27 13 | * @since 1.0 14 | */ 15 | @Component 16 | public class PostProcessor implements ReactivePostContentHandler { 17 | 18 | @Resource 19 | private ProcessorHandler processorHandler; 20 | 21 | @Override 22 | public Mono handle(@NotNull final PostContentContext postContent) { 23 | return processorHandler.handlePostProcessor(postContent); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/cn/dioxide/test/InferStreamTest.java: -------------------------------------------------------------------------------- 1 | package cn.dioxide.test; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import reactor.core.publisher.Mono; 5 | import run.halo.toolbench.util.InferStream; 6 | 7 | import java.util.List; 8 | import java.util.concurrent.Callable; 9 | 10 | /** 11 | * @author Dioxide.CN 12 | * @date 2023/7/20 13 | * @since 1.0 14 | */ 15 | public class InferStreamTest { 16 | 17 | @Test 18 | public void doTest() { 19 | test().forEach(mono -> System.out.println(mono.block())); 20 | } 21 | 22 | public static List> test() { 23 | return InferStream.infer(true) 24 | // 非独立页面或非文章页面 25 | .success(() -> Mono.just("true")) 26 | .fail(() -> Mono.just("false")) 27 | .infer(1 > 2) 28 | .collect(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/processor/PageProcessor.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench.processor; 2 | 3 | import jakarta.annotation.Resource; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.springframework.stereotype.Component; 6 | import reactor.core.publisher.Mono; 7 | import run.halo.app.theme.ReactiveSinglePageContentHandler; 8 | import run.halo.toolbench.infra.ProcessorHandler; 9 | 10 | /** 11 | * @author Dioxide.CN 12 | * @date 2023/8/28 13 | * @since 1.0 14 | */ 15 | @Component 16 | public class PageProcessor implements ReactiveSinglePageContentHandler { 17 | 18 | @Resource 19 | private ProcessorHandler processorHandler; 20 | 21 | @Override 22 | public Mono handle(@NotNull final SinglePageContentContext singlePageContent) { 23 | return processorHandler.handlePageProcessor(singlePageContent); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/ToolBenchPlugin.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench; 2 | 3 | import org.pf4j.PluginWrapper; 4 | import org.springframework.stereotype.Component; 5 | import run.halo.app.plugin.BasePlugin; 6 | import run.halo.toolbench.config.ConfigFolderConfiguration; 7 | 8 | /** 9 | * @author DioxideCN 10 | * @since 2.0.0 11 | */ 12 | @Component 13 | public class ToolBenchPlugin extends BasePlugin { 14 | 15 | private final ConfigFolderConfiguration configFolderConfiguration; 16 | 17 | public ToolBenchPlugin(PluginWrapper wrapper, 18 | ConfigFolderConfiguration configFolderConfiguration) { 19 | super(wrapper); 20 | this.configFolderConfiguration = configFolderConfiguration; 21 | } 22 | 23 | @Override 24 | public void start() { 25 | this.configFolderConfiguration.init(ToolBenchPlugin.class); 26 | } 27 | 28 | @Override 29 | public void stop() { 30 | } 31 | 32 | public ConfigFolderConfiguration getConfigContext() { 33 | return configFolderConfiguration; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report bug to help us improve our program. 4 | title: '' 5 | labels: bug 6 | assignees: DioxideCN 7 | 8 | --- 9 | 10 | ## Expected Behavior 11 | *Tell us what should happen.* 12 | 13 | ## Current Behavior 14 | *Tell us what happens instead of the expected behavior.* 15 | 16 | ## Possible Solution 17 | *Not obligatory, but suggest a fix/reason for the bug* 18 | 19 | ## Steps to Reproduce 20 | *Provide a link to a live example, or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant.* 21 | 22 | 1. 23 | 2. 24 | 3. 25 | 4. 26 | 27 | ## Context (Environment) 28 | *How has this issue affected you? What are you trying to accomplish? Providing context helps us come up with a solution that is most useful in the real world.* 29 | 30 | *Provide a general summary of the issue in the Title above.* 31 | 32 | ## Detailed Description 33 | *Provide a detailed description of the change or addition you are proposing.* 34 | 35 | ## Possible Implementation 36 | *Not obligatory, but suggest an idea for implementing addition or change.* 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Maven 2 | target/ 3 | logs/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | 6 | ### Gradle 7 | .gradle 8 | /build/ 9 | /out/ 10 | !gradle/wrapper/gradle-wrapper.jar 11 | bin/ 12 | 13 | ### STS ### 14 | .apt_generated 15 | .classpath 16 | .factorypath 17 | .project 18 | .settings 19 | .springBeans 20 | .sts4-cache 21 | 22 | ### IntelliJ IDEA ### 23 | .idea 24 | *.iws 25 | *.iml 26 | *.ipr 27 | log/ 28 | 29 | ### NetBeans ### 30 | nbproject/private/ 31 | build/ 32 | nbbuild/ 33 | dist/ 34 | nbdist/ 35 | .nb-gradle/ 36 | 37 | ### Mac 38 | .DS_Store 39 | */.DS_Store 40 | 41 | ### VS Code ### 42 | *.project 43 | *.factorypath 44 | 45 | ### Compiled class file 46 | *.class 47 | 48 | ### Log file 49 | *.log 50 | 51 | ### BlueJ files 52 | *.ctxt 53 | 54 | ### Mobile Tools for Java (J2ME) 55 | .mtj.tmp/ 56 | 57 | ### Package Files 58 | *.war 59 | *.nar 60 | *.ear 61 | *.zip 62 | *.tar.gz 63 | *.rar 64 | 65 | ### VSCode 66 | .vscode 67 | 68 | ### Local file 69 | application-local.yml 70 | application-local.yaml 71 | application-local.properties 72 | 73 | /admin-frontend/node_modules/ 74 | /workplace/ 75 | 76 | ### Other 77 | /console/node_modules 78 | -------------------------------------------------------------------------------- /src/main/resources/static/lib/AntVG2Handler.js: -------------------------------------------------------------------------------- 1 | const g2lib = true; 2 | (function() { 3 | // 用来分隔不同的 G2.Chart 对象 4 | let regexChart = /((?:const|let|var)?\s*[^=]*?=\s*new\s*G2.Chart[\s\S]*?}\);)([\s\S]*?)(?=(?:const|let|var)?\s*[^=]*?=\s*new\s*G2.Chart|$)/g; 5 | // 用来获取 container 的名称 6 | let regExp = /container: '([^']*)',/; 7 | const elements = document.querySelectorAll('code.language-g2'); 8 | elements.forEach(element => { 9 | let text = element.textContent; 10 | let newText = text.replace(regexChart, function(match, chartStr, chartOps) { 11 | let matchContainer = regExp.exec(chartStr); 12 | if (matchContainer !== null) { 13 | let occupied = document.getElementById(matchContainer[1]).parentNode.clientWidth; 14 | const g2replacer = chartStr.replace(/\$\{(([<>=]{1,2}.+)\?(.+):(.+)|full)}/g, function(match) {return parseExpression(match, occupied).toString();}); 15 | // 返回处理后的 chart 对象和其对应的操作 16 | return g2replacer + chartOps; 17 | } 18 | return match; // 如果没有找到匹配项,则返回原字符串 19 | }); 20 | eval(newText); 21 | element.parentNode.remove(); 22 | }); 23 | })(); -------------------------------------------------------------------------------- /src/main/resources/static/lib/AntVX6Handler.js: -------------------------------------------------------------------------------- 1 | const x6lib = true; 2 | (function() { 3 | // 用来分隔不同的 X6.Graph 对象 4 | let regexGraph = /((?:const|let|var)?\s*[^=]*?=\s*new\s*X6.Graph[\s\S]*?}\)|;)([\s\S]*?)(?=(?:const|let|var)?\s*[^=]*?=\s*new\s*X6.Graph|$)/g; 5 | // 用来获取 container 的名称 6 | let regexContainer = /container: document.getElementById\('([^']*)'\),/; 7 | let elements = document.querySelectorAll('.prism.language-x6'); 8 | elements.forEach(function(element) { 9 | let text = element.textContent; 10 | let newText = text.replace(regexGraph, function(match, graphStr, graphOps) { 11 | let matchContainer = regexContainer.exec(graphStr); 12 | if (matchContainer !== null) { 13 | let occupied = document.querySelector(`#${matchContainer[1]}`).parentNode.offsetWidth; 14 | const x6replacer = graphStr.replace(/\$\{(([<>=]{1,2}.+)\?(.+):(.+)|full)}/g, function(match) { 15 | return parseExpression(match, occupied).toString(); 16 | }); 17 | // 返回处理后的 graph 对象和其对应的操作 18 | return x6replacer + graphOps; 19 | } 20 | return match; // 如果没有找到匹配项,则返回原字符串 21 | }); 22 | eval(newText); 23 | element.parentNode.removeChild(element); 24 | }) 25 | })(); 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Banner](https://camo.githubusercontent.com/aff6f08dd2a3a49c0a221326a42e9a363a9529ad6fdbf711535d463b501f9cef/68747470733a2f2f64696f786964652d636e2e696e6b2f75706c6f61642f42616e6e65722e706e67) 2 | 3 | # Tool-Bench 4 | 感谢您下载并使用 Tool Bench 插件,本插件为 Halo 2.x 博客集成了更多的API与能力。加入我们的 QQ 交流群 697197698 以获得更好的技术支持。以下是本插件常用的指南和安装方法: 5 | 6 | - 基本使用指南:https://dioxide-cn.ink/archives/about-tool-bench 7 | - 扩展样式指南:https://dioxide-cn.ink/archives/tool-bench-style 8 | - 安装方法: 9 | 1. 访问最新的 [Releases](https://github.com/DioxideCN/Tool-Bench/releases) 页面,下载 Assets 中的 JAR 文件 10 | 2. 安装,插件安装和更新方式可参考:https://docs.halo.run/user-guide/plugins 11 | 12 | ## 开发环境 13 | ```shell 14 | git clone git@github.com:DioxideCN/Tool-Bench.git 15 | # 或者当你 fork 之后 16 | git clone git@github.com:{your_github_id}/Tool-Bench.git 17 | ``` 18 | 19 | 修改 Halo 程序的环境配置文件: 20 | 21 | ```yaml 22 | halo: 23 | plugin: 24 | runtime-mode: development 25 | classes-directories: 26 | - "build/classes" 27 | - "build/resources" 28 | lib-directories: 29 | - "libs" 30 | fixedPluginPath: 31 | - "/path/to/Tool-Bench" 32 | ``` 33 | 34 | ## 提供issue 35 | 提供 issue 时请附带完整的报错信息,并尽可能多地提供一个可复现该错误的环境信息(如:主题、JDK版本、时间等)。 36 | 37 | ## 贡献代码 38 | Tool Bench 欢迎每一位开发者提交更好更强大的代码,贡献代码时请在 Java doc 中注明`@date`与`@author`。 39 | 40 | ## 开发手册 41 | 在部署完成开发环境后,您可以根据每个类的 Java doc 描述进行进一步开发,或者加入我们的 QQ 交流群以获取帮助。 42 | 43 | ## 版本支持 44 | 1. 1.0.2 及以下的版本支持 Halo 2.5.0+ 45 | 2. 1.0.3 及以上的版本支持 Halo 2.8.0+ 46 | -------------------------------------------------------------------------------- /src/test/java/cn/dioxide/test/MarkdownTest.java: -------------------------------------------------------------------------------- 1 | package cn.dioxide.test; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import run.halo.toolbench.util.PostUtil; 5 | 6 | import java.util.HashSet; 7 | 8 | /** 9 | * @author Dioxide.CN 10 | * @date 2023/10/19 11 | * @since 1.0 12 | */ 13 | public class MarkdownTest { 14 | 15 | static final String rawContext = """ 16 | 17 | 18 | 33 | 34 | # Heading 1 35 | """; 36 | 37 | @Test 38 | public void renderTest() { 39 | String s = PostUtil.fixMarkdownAndElementTag(rawContext, new HashSet<>()); 40 | System.out.println(s); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/infra/GzipResponse.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench.infra; 2 | 3 | import com.google.common.io.CharStreams; 4 | import reactor.core.publisher.Mono; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStreamReader; 8 | import java.net.http.HttpClient; 9 | import java.net.http.HttpRequest; 10 | import java.net.http.HttpResponse; 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.concurrent.CompletableFuture; 13 | import java.util.zip.GZIPInputStream; 14 | 15 | /** 16 | * @author Dioxide.CN 17 | * @date 2023/7/28 18 | * @since 1.0 19 | */ 20 | public class GzipResponse { 21 | 22 | public static Mono handle(HttpClient client, HttpRequest request) { 23 | return Mono.fromCompletionStage(client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())) 24 | .flatMap(response -> Mono.fromCompletionStage( 25 | // 展平并委派给CompletableFuture处理 26 | CompletableFuture.supplyAsync(() -> { 27 | // 为了解决在响应式链路中close发生异步阻塞这里使用CompletableFuture来解决 28 | try (GZIPInputStream gzipInputStream = new GZIPInputStream(response.body()); 29 | InputStreamReader reader = new InputStreamReader(gzipInputStream, StandardCharsets.UTF_8)) { 30 | return CharStreams.toString(reader); 31 | } catch (IOException e) { 32 | throw new RuntimeException(e); 33 | } 34 | }) 35 | )); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/router/GraphQLRouter.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench.router; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import jakarta.annotation.Resource; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestParam; 8 | import org.springframework.web.bind.annotation.RestController; 9 | import reactor.core.publisher.Mono; 10 | import run.halo.app.plugin.ApiVersion; 11 | import run.halo.toolbench.infra.GraphQLBuilder; 12 | import run.halo.toolbench.infra.GraphQLReader; 13 | 14 | /** 15 | * make halo support request graphql api from 16 | * some websites like LeetCode, Twitter, GitHub 17 | * 18 | * @author Dioxide.CN 19 | * @date 2023/7/21 20 | * @since 1.0 21 | */ 22 | @ApiVersion("v1alpha1") 23 | @RequestMapping("/github") 24 | @RestController 25 | public class GraphQLRouter { 26 | @Resource 27 | private GraphQLReader reader; 28 | 29 | // Update in 1.4EA07 30 | @GetMapping("/repository") 31 | public Mono repositoryGraphqlQuery(@RequestParam String owner, 32 | @RequestParam String repo) { 33 | return reader.executeQuery(owner, repo, GraphQLBuilder::getRepoInfo); 34 | } 35 | 36 | // Update in 1.4EA07 37 | @GetMapping("/discussions") 38 | public Mono discussionsGraphqlQuery(@RequestParam String owner, 39 | @RequestParam String repo) { 40 | return reader.executeQuery(owner, repo, GraphQLBuilder::getGiscusInfo); 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/config/CaffeineCacheConfiguration.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench.config; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.github.benmanes.caffeine.cache.Cache; 5 | import com.github.benmanes.caffeine.cache.Caffeine; 6 | import jakarta.annotation.Resource; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import run.halo.app.plugin.SettingFetcher; 11 | import run.halo.toolbench.router.QWeatherRouter; 12 | 13 | import java.util.concurrent.TimeUnit; 14 | 15 | /** 16 | * 为了避免前端暴露出来的接口被恶意刷取、盗用流量,需要配置Caffeine本地缓存。 17 | * 在这个配置类中Caffeine缓存主要负责对和风天气的查询结果进行缓存,缓存失效 18 | * 时间为10分钟,10分钟后会重新进入{@link QWeatherRouter#getWeatherData}方法调用远程拉取任务。 19 | * 由于拉取和风天气信息的业务划分为两个步骤,一个是根据IP获取城市ID,一个是 20 | * 根据城市ID获取天气信息,所以需要两层缓存,一个是缓存"城市ID"到"IP集合" 21 | * 的缓存,另一个是缓存"城市ID"到"天气信息"的缓存,两个缓存具有关联性。 22 | * 23 | * @author Dioxide.CN 24 | * @date 2023/8/5 25 | * @since 1.0 26 | */ 27 | @Slf4j 28 | @Configuration 29 | public class CaffeineCacheConfiguration { 30 | @Resource 31 | private SettingFetcher settingFetcher; 32 | 33 | // ip到城市ID 34 | @Bean(name = "ipToCityIDCache") 35 | public Cache ipToCityIDCache() { 36 | return Caffeine.newBuilder() 37 | .expireAfterWrite(30, TimeUnit.MINUTES) 38 | .maximumSize(1_000) 39 | .build(); 40 | } 41 | 42 | // 城市ID到天气信息 43 | @Bean(name = "cityIDToWeatherCache") 44 | public Cache cityIDToWeatherCache() { 45 | return Caffeine.newBuilder() 46 | .expireAfterWrite(30, TimeUnit.MINUTES) 47 | .maximumSize(1_000) 48 | .build(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/static/lib/FormatterInit.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | document.addEventListener("DOMContentLoaded", () => { 3 | const rootScript = document.getElementById("formatter-init"); 4 | if (rootScript === null) return; 5 | const container = document.createElement("div"); 6 | const root = document.createElement("div"); 7 | const shadowDOM = container.attachShadow?.({ mode: "open" }) || container; 8 | 9 | const x6enable = rootScript.attributes['data-x6-enable'].value === "true"; 10 | const g2enable = rootScript.attributes['data-g2-enable'].value === "true"; 11 | 12 | if (!x6enable && !g2enable) return; 13 | 14 | if (x6enable) { 15 | const styleEl = document.createElement("script"); 16 | styleEl.setAttribute( 17 | "src", 18 | "/plugins/ToolBench/assets/static/lib/AntVX6Handler.js" 19 | ); 20 | shadowDOM.appendChild(styleEl); 21 | } 22 | if (g2enable) { 23 | const styleEl = document.createElement("script"); 24 | styleEl.setAttribute( 25 | "src", 26 | "/plugins/ToolBench/assets/static/lib/AntVG2Handler.js" 27 | ); 28 | shadowDOM.appendChild(styleEl); 29 | } 30 | shadowDOM.appendChild(root); 31 | document.body.appendChild(container); 32 | }); 33 | })(); 34 | 35 | // 解析ToolBench自适应表达式 36 | function parseExpression(expression, occupied) { 37 | if (expression === "${full}") { 38 | return occupied; 39 | } 40 | const match = expression.replaceAll("full", occupied).match(/^\$\{([<>=]{1,2}.+)\?(.+):(.+)}$/); 41 | if (match) { 42 | return eval(`occupied${match[1]} ? ${match[2]} : ${match[3]}`); 43 | } 44 | throw new Error(`Invalid expression "${expression}"`); 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/router/DirectoryRouter.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench.router; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.reactive.function.server.RouterFunction; 7 | import org.springframework.web.reactive.function.server.ServerResponse; 8 | import reactor.core.publisher.Mono; 9 | import run.halo.app.plugin.ReactiveSettingFetcher; 10 | 11 | import java.util.Map; 12 | 13 | import static org.springframework.web.reactive.function.server.RequestPredicates.GET; 14 | import static org.springframework.web.reactive.function.server.RouterFunctions.route; 15 | 16 | /** 17 | * @author Dioxide.CN 18 | * @date 2023/5/28 19 | * @since 1.0 20 | */ 21 | @Component 22 | @AllArgsConstructor 23 | public class DirectoryRouter { 24 | 25 | private final ReactiveSettingFetcher settingFetcher; 26 | private static final String TEMPLATE_ID_VARIABLE = "_templateId"; 27 | private static final String KEY = "directory"; 28 | private static final String TITLE = "目录"; 29 | private static final String SUBTITLE = "目录页将按所有文章的分类进行排序展示"; 30 | 31 | @Bean 32 | RouterFunction directoryRouter() { 33 | return route(GET("/directory"), 34 | request -> ServerResponse.ok().render(KEY, 35 | Map.of( 36 | TEMPLATE_ID_VARIABLE, KEY, 37 | "title", Mono.fromCallable(() -> this.settingFetcher 38 | .get("basic").map( 39 | setting -> setting.get(KEY) 40 | .asText(TITLE))), 41 | "subtitle", Mono.fromCallable(() -> this.settingFetcher 42 | .get("basic").map( 43 | setting -> setting.get("directorySubContent") 44 | .asText(SUBTITLE))) 45 | ) 46 | ) 47 | ); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/config/ConfigFolderConfiguration.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench.config; 2 | 3 | import lombok.Getter; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.context.annotation.Configuration; 6 | import run.halo.app.plugin.BasePlugin; 7 | import run.halo.toolbench.util.FutureDownloader; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.net.URL; 12 | import java.net.URLDecoder; 13 | import java.nio.charset.StandardCharsets; 14 | import java.nio.file.Files; 15 | import java.nio.file.Path; 16 | import java.nio.file.Paths; 17 | 18 | /** 19 | * @author Dioxide.CN 20 | * @date 2023/7/21 21 | * @since 1.0 22 | */ 23 | @Slf4j 24 | @Configuration 25 | public class ConfigFolderConfiguration { 26 | 27 | private Class clazz; 28 | @Getter private String CONFIG_HOME; 29 | 30 | private static final String FILE_URL = "https://cdn.jsdelivr.net/npm/geolite2-city@1.0.0/GeoLite2-City.mmdb.gz"; 31 | 32 | public void init(Class clazz) { 33 | this.clazz = clazz; 34 | homeDirBuilder(); 35 | buildGraphQLHome(); 36 | FutureDownloader.call(CONFIG_HOME, FILE_URL); 37 | } 38 | 39 | private void homeDirBuilder() { 40 | URL url = this.clazz.getProtectionDomain().getCodeSource().getLocation(); 41 | String filePath = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8); 42 | this.CONFIG_HOME = new File(filePath).getParent() + File.separator + "tool-bench"; 43 | log.info("detected config directory " + this.CONFIG_HOME); 44 | 45 | Path path = Paths.get(this.CONFIG_HOME); 46 | try { 47 | if (!Files.exists(path)) { 48 | Files.createDirectories(path); 49 | } 50 | } catch (IOException e) { 51 | log.error(e.toString()); 52 | } 53 | } 54 | 55 | private void buildGraphQLHome() { 56 | Path path = Paths.get(this.CONFIG_HOME + File.separator + "graphql"); 57 | try { 58 | if (!Files.exists(path)) { 59 | Files.createDirectories(path); 60 | } 61 | } catch (IOException e) { 62 | log.error(e.toString()); 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/renderer/CustomHeadingRenderer.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench.renderer; 2 | 3 | import com.vladsch.flexmark.ast.Heading; 4 | import com.vladsch.flexmark.html.HtmlRenderer; 5 | import com.vladsch.flexmark.html.HtmlWriter; 6 | import com.vladsch.flexmark.html.renderer.NodeRenderer; 7 | import com.vladsch.flexmark.html.renderer.NodeRendererContext; 8 | import com.vladsch.flexmark.html.renderer.NodeRendererFactory; 9 | import com.vladsch.flexmark.html.renderer.NodeRenderingHandler; 10 | import com.vladsch.flexmark.util.data.DataHolder; 11 | import com.vladsch.flexmark.util.data.MutableDataHolder; 12 | import com.vladsch.flexmark.util.sequence.BasedSequence; 13 | import org.jetbrains.annotations.NotNull; 14 | import org.jetbrains.annotations.Nullable; 15 | 16 | import java.util.HashSet; 17 | import java.util.Set; 18 | 19 | /** 20 | * @author Dioxide.CN 21 | * @date 2023/8/30 22 | * @since 1.0 23 | */ 24 | public class CustomHeadingRenderer implements NodeRenderer { 25 | public CustomHeadingRenderer(DataHolder options) { 26 | } 27 | 28 | @Override 29 | public @Nullable Set> getNodeRenderingHandlers() { 30 | Set> set = new HashSet<>(); 31 | set.add(new NodeRenderingHandler<>(Heading.class, this::render)); 32 | return set; 33 | } 34 | 35 | private void render(Heading heading, NodeRendererContext context, HtmlWriter html) { 36 | BasedSequence text = heading.getText(); 37 | html.withAttr() 38 | .attr("id", text) 39 | .tag("h" + heading.getLevel()); 40 | context.renderChildren(heading); 41 | html.tag("/h" + heading.getLevel()); 42 | } 43 | 44 | public static class Factory implements HtmlRenderer.HtmlRendererExtension { 45 | @Override 46 | public void rendererOptions(@NotNull MutableDataHolder options) { 47 | } 48 | 49 | @Override 50 | public void extend(HtmlRenderer.@NotNull Builder htmlRendererBuilder, @NotNull String rendererType) { 51 | htmlRendererBuilder.nodeRendererFactory(new NodeRendererFactory() { 52 | @Override 53 | public @NotNull NodeRenderer apply(@NotNull DataHolder options) { 54 | return new CustomHeadingRenderer(options); 55 | } 56 | }); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/run/halo/toolbench/util/DomBuilder.java: -------------------------------------------------------------------------------- 1 | package run.halo.toolbench.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.stream.Collectors; 7 | 8 | /** 9 | * @author Dioxide.CN 10 | * @date 2023/7/20 11 | * @since 1.0 12 | */ 13 | public class DomBuilder { 14 | 15 | private static final String ABSOLUTE_PATH = "/plugins/ToolBench/assets/static"; 16 | private static final String SCRIPT_HEAD = 17 | "