├── .gitignore ├── bin.ps1 ├── bin ├── jane ├── jane.1.0.0.jar ├── jane.bat ├── register-this-path ├── register-this-path.bat └── register-this-path.win10.bat ├── pom.xml ├── readme.md └── src ├── main ├── kotlin │ └── net │ │ └── pandolia │ │ └── jane │ │ ├── Main.kt │ │ ├── Monitor.kt │ │ ├── Page.kt │ │ ├── Serve.kt │ │ ├── Site.kt │ │ └── libs │ │ ├── Algorithm.kt │ │ ├── Devtool.kt │ │ ├── FileUtils.kt │ │ ├── General.kt │ │ ├── SimpleArgOptions.kt │ │ ├── TaskQueue.kt │ │ ├── TextUtils.kt │ │ └── Watcher.kt └── resources │ ├── hello-jane │ ├── build │ │ └── readme │ └── src │ │ ├── page │ │ ├── 2020 │ │ │ └── 02-26-hello-jane.md │ │ ├── index.md │ │ └── nav1-links.md │ │ ├── site.config │ │ ├── static │ │ ├── favicon.ico │ │ └── resources │ │ │ ├── image │ │ │ └── 851.jpg │ │ │ ├── logo.svg │ │ │ └── styles.css │ │ └── template.mustache │ ├── mimelist │ └── reload.js └── test └── kotlin └── net └── pandolia └── jane └── libs └── LibTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | *.iml 4 | tuiguang 5 | -------------------------------------------------------------------------------- /bin.ps1: -------------------------------------------------------------------------------- 1 | switch ($args[0]) { 2 | 'resolve' { 3 | mvn dependency:resolve 4 | return 5 | } 6 | 7 | 'clean' { 8 | mvn clean 9 | return 10 | } 11 | 12 | 'compile' { 13 | mvn compile 14 | return 15 | } 16 | 17 | 'package' { 18 | mvn package 19 | Move-Item '.\target\jane-1.0.0-jar-with-dependencies.jar' '.\bin\jane.1.0.0.jar' -Force 20 | return 21 | } 22 | 23 | # make a container with Docker Toolbox for Windows 24 | # must add share folder to the virtual linux system in Virtual box 25 | 'make-container' { 26 | docker container run ` 27 | --name jane ` 28 | --publish 8000:8000 ` 29 | --volume /d/Kotlin/jane/jane/bin:/home/root/jane ` 30 | --workdir /home/root/jane ` 31 | --interactive ` 32 | --tty ` 33 | adoptopenjdk:8-jre-openj9 34 | return 35 | } 36 | 37 | # run jane dev in the container, the web site is: http://192.168.99.100:8000/ 38 | 'bash' { 39 | docker start jane --interactive 40 | return 41 | } 42 | 43 | default { 44 | Write-Host "Bad command: $op" 45 | } 46 | } -------------------------------------------------------------------------------- /bin/jane: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | java -Dexec.args="$*" -jar $0.1.0.0.jar 4 | -------------------------------------------------------------------------------- /bin/jane.1.0.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandolia/jane/879eea5610075272fa3b14b010596084611603b5/bin/jane.1.0.0.jar -------------------------------------------------------------------------------- /bin/jane.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | java -Dexec.args="%*" -jar "%~dp0jane.1.0.0.jar" -------------------------------------------------------------------------------- /bin/register-this-path: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # source register-this-path 3 | 4 | chmod +x ./jane 5 | export PATH=${PATH}:${PWD} 6 | echo "" >>~/.bashrc 7 | echo "export PATH=\${PATH}:${PWD}" >>~/.bashrc 8 | -------------------------------------------------------------------------------- /bin/register-this-path.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | powershell -c [Environment]::SetEnvironmentVariable('Path',[Environment]::GetEnvironmentVariable('Path','Machine')+';%~dp0','Machine') 3 | echo register "%~dp0" to system path 4 | pause -------------------------------------------------------------------------------- /bin/register-this-path.win10.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | powershell -c "Start-Process .\register-this-path.bat -Verb RunAs" -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 4.0.0 7 | 8 | net.pandolia 9 | jane 10 | 1.0.0 11 | 12 | 13 | 1.3.61 14 | 4.12 15 | 3.5.0 16 | 0.9.6 17 | 0.50.44 18 | 1.7.26 19 | net.pandolia.jane.MainKt 20 | true 21 | UTF-8 22 | true 23 | 24 | 25 | 26 | 27 | org.jetbrains.kotlin 28 | kotlin-stdlib 29 | ${kotlin.version} 30 | 31 | 32 | org.jetbrains.kotlin 33 | kotlin-reflect 34 | ${kotlin.version} 35 | 36 | 37 | com.github.spullara.mustache.java 38 | compiler 39 | ${mustache.version} 40 | 41 | 42 | com.vladsch.flexmark 43 | flexmark-all 44 | ${flexmark.version} 45 | 46 | 47 | io.javalin 48 | javalin 49 | ${javalin.version} 50 | 51 | 52 | org.slf4j 53 | slf4j-simple 54 | ${slf4j.version} 55 | 56 | 57 | junit 58 | junit 59 | ${junit.version} 60 | test 61 | 62 | 63 | org.jetbrains.kotlin 64 | kotlin-test-junit 65 | ${kotlin.version} 66 | test 67 | 68 | 69 | 70 | 71 | ${project.basedir}/src/main/kotlin 72 | ${project.basedir}/src/test/kotlin 73 | 74 | 75 | org.jetbrains.kotlin 76 | kotlin-maven-plugin 77 | ${kotlin.version} 78 | 79 | 80 | org.jetbrains.kotlin 81 | kotlin-maven-noarg 82 | ${kotlin.version} 83 | 84 | 85 | 86 | 87 | compile 88 | 89 | compile 90 | 91 | 92 | 93 | ${project.basedir}/src/main/kotlin 94 | 95 | 96 | 97 | 98 | test-compile 99 | 100 | test-compile 101 | 102 | 103 | 104 | ${project.basedir}/src/test/kotlin 105 | 106 | 107 | 108 | 109 | 110 | 111 | org.apache.maven.plugins 112 | maven-compiler-plugin 113 | 3.8.1 114 | 115 | 1.8 116 | 1.8 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-assembly-plugin 122 | 2.6 123 | 124 | 125 | make-assembly 126 | package 127 | single 128 | 129 | 130 | 131 | ${main.class} 132 | 133 | 134 | 135 | jar-with-dependencies 136 | 137 | 138 | 139 | 140 | 141 | 142 | org.codehaus.mojo 143 | exec-maven-plugin 144 | 1.2.1 145 | 146 | ${main.class} 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Jane: A super lightweight static blog system 2 | 3 | Jane 是一个超级轻量的静态博客系统,其工程文件、资源文件以及生成的 HTML 文档都极为精简,同时保持了页面的简洁和美观。且提供完整、方便的开发工具,修改文件后可立即自动更新页面,使用者可以专心于文档写作本身。 4 | 5 | Jane 生成的示例博客如下: 6 | 7 | * [Jane 主页](https://jane.pandolia.net) 8 | 9 | * [Jane 作者的博客](https://pandolia.net) 10 | 11 | #### 安装 12 | 13 | 安装 JRE/JDK 8+ ,到本项目 [Github 主页](https://github.com/pandolia/jane) 下载源码和编译好的 jar 文件,解压后, cd 到其中的 bin 目录,并将此目录添加至系统路径,Windows 下可手工添加,或以管理员权限运行 register-this-path.bat 文件,Linux 下以当前用户运行 source register-this-path 就可以了。之后打开一个终端,运行 jane 命令,若打印出使用信息,则表明安装成功。 14 | 15 | #### 新建工程 16 | 17 | 运行 ***jane create your-project-name*** ,将创建一个 jane 工程,新建一个 your-project-name 目录,内包含 src 和 build 目录。其中: 18 | 19 | * src 目录下为源码文件,含文章(src/page)、模板(src/template.mustache)、资源(src/static) 和 配置(site.config) 20 | 21 | * build 目录下是编译后生成的 HTML 和资源文件。 22 | 23 | #### 启动开发工具 24 | 25 | cd 到 ***your-project-name/src*** 目录,运行 ***jane dev*** ,将在 8000 端口启动一个开发服务器,并自动打开浏览器预览页面。如果需要使用其他端口,可以运行 ***jane dev -p 8080*** 。 26 | 27 | #### 编写普通文章 28 | 29 | 在 src/page/2020 目录下增加一篇文章 02-28-my-first-article.md ,增加以下内容: 30 | 31 | --- 32 | title: My first Article 33 | image: https://i.picsum.photos/id/927/2560/600.jpg 34 | category: IT 35 | --- 36 | 37 | This is My first Article 38 | 39 | 保存文件,之后浏览器页面会自动更新,主页的文章列表中已经多了一项 ***My first Article*** 的条目,点击可以打开编写的文章。 40 | 41 | #### 编写导航栏文章 42 | 43 | 在 src/page 目录下增加一篇文章 nav2-my-nav.md ,增加一下内容: 44 | 45 | --- 46 | title: My Nav Page 47 | image: https://picsum.photos/2560/600 48 | --- 49 | 50 | This is My Nav Page 51 | 52 | 保存文件后,主页的右上角导航栏已经多了一项 ***Nav*** 的条目,点击可以打开编写的文章。 53 | 54 | #### 编译输出 HTML 文档及资源 55 | 56 | 文章编写完成后,在命令行窗口中敲一下回车,开发服务器将退出,同时通知浏览器关闭页面。之后,运行 ***jane build*** 命令,将 src/page 所有文章编译为 HTML 文档输出至 build 目录,同时,将 src/static 下的文件原样拷贝至 build 目录。此目录可以部署到网站的任何位置。 57 | 58 | 请勿修改和删除 build 目录下的文件,下次修改文章或资源后,重新运行 jane build ,只会更新需要修改的文件。 59 | 60 | #### 自定义页面框架和样式 61 | 62 | 如果需要调整页面的框架或者页面中元素的样式,可以修改 template.mustache 和 styles.css 等文件,前提是你已经熟悉 Mustache 模板语法、 HTML 语法 以及 CSS 语法。更进一步的,如果你熟悉 kotlin 语言,可以修改 [jane](https://github.com/pandolia/jane) 源码,打造自己的静态博客系统。如果你有任何的建议或想法,请给我发邮件 [pandolia@yeah.net](mailto://pandolia@yeah.net) 。 63 | -------------------------------------------------------------------------------- /src/main/kotlin/net/pandolia/jane/Main.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane 2 | 3 | import net.pandolia.jane.libs.* 4 | 5 | const val staticDir = "static" 6 | const val pageDir = "page" 7 | const val configFile = "site.config" 8 | const val templatePath = "template.mustache" 9 | const val buildDir = "../build" 10 | const val defaultServerPort = 8000 11 | 12 | var rootDir = "" 13 | private set 14 | 15 | var serverPort = 0 16 | private set 17 | 18 | fun main() { 19 | rootDir = Proc.workingDirectory 20 | serverPort = Proc.getOption("-p", "--port")?.toInt() ?: defaultServerPort 21 | 22 | when (Proc.command) { 23 | "create" -> createProject() 24 | "dev" -> developProject() 25 | "build" -> buildProject() 26 | "clean" -> cleanProject() 27 | else -> printUsage() 28 | } 29 | } 30 | 31 | fun printUsage() { 32 | println("jane create \$project_name") 33 | println("jane dev|build|clean [-d|--debug] [-p|--port 8000] [-wd|--working-directory .]") 34 | } 35 | 36 | fun createProject() { 37 | val projectName = Proc.subCommand 38 | 39 | if (projectName == null) { 40 | printUsage() 41 | Proc.exit(1) 42 | } 43 | 44 | if (Fs.exists(projectName)) { 45 | Proc.abort("Project $projectName already exists") 46 | } 47 | 48 | if (!Fs.mkDir(projectName)) { 49 | Proc.abort("Fail to make directory $projectName") 50 | } 51 | 52 | Try.get("Create a jane project($projectName)") { 53 | Fs.copyResources("/hello-jane", projectName) 54 | } 55 | } 56 | 57 | fun developProject() { 58 | loadConfig() 59 | loadPages() 60 | serveProject() 61 | monitorProject() 62 | startMainQueue() 63 | } 64 | 65 | fun buildProject() { 66 | loadConfig() 67 | loadPages() 68 | renderPages() 69 | copyStatics() 70 | deleteExtraFiles() 71 | } 72 | 73 | fun cleanProject() { 74 | Try.get("Delete files in $buildDir") { 75 | Fs.clearDir(buildDir) { it != "readme" } 76 | } 77 | } 78 | 79 | fun copyStatics() { 80 | val m = rootDir.length + 1 81 | val n = m + staticDir.length + 1 82 | Fs.getChildFiles(staticDir) 83 | .forEach { 84 | val source = it.substring(m) 85 | val target = "$buildDir/${it.substring(n)}" 86 | Fs.copyFileIfModified(source, target) 87 | } 88 | } 89 | 90 | fun deleteExtraFiles() { 91 | val n = Fs.getRealPath(buildDir).length + 1 92 | Fs.getChildFiles(buildDir) 93 | .forEach { path -> 94 | val relPath = path.substring(n) 95 | val staticPath = "$staticDir/$relPath" 96 | val targetPath = "$buildDir/$relPath" 97 | 98 | if (relPath == "readme" 99 | || Fs.isFile(staticPath) 100 | || Site.pages.any { it.target_path == targetPath }) { 101 | return@forEach 102 | } 103 | 104 | Fs.deleteFile(path) 105 | Log.info("Delete $path") 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/kotlin/net/pandolia/jane/Monitor.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane 2 | 3 | import net.pandolia.jane.libs.* 4 | 5 | private var needReload = false 6 | 7 | fun monitorProject() { 8 | Watcher(rootDir, ::onDelete, ::onModify, ::onFlush).start() 9 | } 10 | 11 | fun onDelete(path: String) { 12 | if (path.endsWith('~')) { 13 | return 14 | } 15 | 16 | Log.info("Detect file deleted: $path") 17 | 18 | if (path == configFile || path == templatePath) { 19 | Proc.exit(1) 20 | } 21 | 22 | val dir = path.substringBefore('/') 23 | 24 | if (dir == staticDir) { 25 | needReload = true 26 | return 27 | } 28 | 29 | if (dir != pageDir || !path.endsWith(".md")) { 30 | return 31 | } 32 | 33 | if (deletePage(path)) { 34 | needReload = true 35 | return 36 | } 37 | } 38 | 39 | fun onModify(path: String) { 40 | if (path.endsWith('~')) { 41 | return 42 | } 43 | 44 | Log.info("Detect file modified: $path") 45 | 46 | if (path == configFile) { 47 | reloadConfig() 48 | needReload = true 49 | return 50 | } 51 | 52 | val dir = path.substringBefore('/') 53 | 54 | if (path == templatePath || dir == staticDir) { 55 | needReload = true 56 | return 57 | } 58 | 59 | if (dir != pageDir || !path.endsWith(".md")) { 60 | return 61 | } 62 | 63 | val page = getPage(path) 64 | if (page != null) { 65 | needReload = page.readProps() || needReload 66 | return 67 | } 68 | 69 | needReload = makePage(path) || needReload 70 | } 71 | 72 | fun onFlush() { 73 | if (!needReload) { 74 | return 75 | } 76 | 77 | broadcast("reload page") 78 | needReload = false 79 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/pandolia/jane/Page.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane 2 | 3 | import net.pandolia.jane.libs.* 4 | import java.io.File 5 | 6 | val pageRelPathRegex = Regex("(\\d{4}/\\d{2}-\\d{2}-[0-9a-z-]+.md)|([^./]+.md)") 7 | 8 | @Suppress("PropertyName", "MemberVisibilityCanBePrivate") 9 | class Page(val page_name: String) { 10 | val page_path: String 11 | val target_path: String 12 | val layer_string: String 13 | val create_date: String 14 | 15 | var title = "" 16 | var image = "" 17 | var category = "" 18 | var content_is_categories = false 19 | var content = "" 20 | var is_valid = false 21 | 22 | @Suppress("unused") 23 | val category_archor get() = "${Site.categories_page_name}.html#${category.packValue}" 24 | 25 | init { 26 | page_path = "$pageDir/$page_name.md" 27 | target_path = "$buildDir/$page_name.html" 28 | 29 | if (page_name.contains('/')) { 30 | layer_string = ".." 31 | create_date = "${page_name.substring(0, 4)}-${page_name.substring(5, 10)}" 32 | } else { 33 | layer_string = "." 34 | create_date = "" 35 | } 36 | 37 | readProps() 38 | } 39 | 40 | fun readProps(): Boolean { 41 | val text = Fs.readFile(page_path).trim() 42 | 43 | val pagePropsMarker = "---" 44 | val n = pagePropsMarker.length 45 | 46 | if (!text.startsWith(pagePropsMarker)) { 47 | Log.warn("Content of $page_path does not start with '---'") 48 | return false 49 | } 50 | 51 | val i = text.indexOf(pagePropsMarker, n) 52 | if (i == -1) { 53 | Log.warn("Content of $page_path does not contain two '---'") 54 | return false 55 | } 56 | 57 | val props = text.substring(n, i).parseToDict().toMutableMap() 58 | var content0 = text.substring(i + 3).trim() 59 | 60 | val title0 = props["title"] ?: "" 61 | val image0 = getImage(props["image"]) 62 | val category0 = if (create_date.isNotEmpty()) (props["category"] ?: "") else "NO-CATEGORY" 63 | val isValid0 = title0.isNotEmpty() && category0.isNotEmpty() 64 | val contentIsCategories0 = (content0 == "[categories]") 65 | 66 | content0 = Markdown.md2html(content0) 67 | 68 | Log.debug(""" 69 | | page_name: $page_name 70 | | page_path: $page_path 71 | | target_path: $target_path 72 | | layer_string: $layer_string 73 | | create_date: $create_date 74 | | title: $title0 75 | | image: $image0 76 | | category: $category0 77 | | content_length: ${content0.length} 78 | | content_is_categories: $contentIsCategories0 79 | | is_valid: $isValid0 80 | """) 81 | 82 | if (!isValid0) { 83 | Log.warn("Tilte|category of $page_path is empty, this page is discarded") 84 | return false 85 | } 86 | 87 | if (contentIsCategories0) { 88 | Site.categories_page_name = page_name 89 | } else if (content_is_categories) { 90 | Site.categories_page_name = "" 91 | } 92 | 93 | title = title0 94 | image = image0 95 | category = category0 96 | content = content0 97 | content_is_categories = contentIsCategories0 98 | is_valid = isValid0 99 | 100 | return true 101 | } 102 | 103 | fun getImage(imgName: String?) = when { 104 | imgName.isNullOrEmpty() -> "https://picsum.photos/2560/600" 105 | imgName.startsWith("http") -> imgName 106 | else -> "$layer_string/resources/image/$imgName" 107 | } 108 | 109 | fun needRender(): Boolean { 110 | if (content_is_categories) { 111 | return true 112 | } 113 | 114 | val tTarget = File(target_path).lastModified() 115 | 116 | val tSrc = listOf(page_path, templatePath, configFile) 117 | .map { File(it).lastModified() } 118 | .max()!! 119 | 120 | return tTarget <= tSrc 121 | } 122 | 123 | private fun getScope() = mapOf("site" to Site, "page" to this) 124 | 125 | fun render() = Mustache.renderToFile(templatePath, getScope(), target_path) 126 | 127 | fun renderToInputStream() = Mustache.renderToInputStream(templatePath, getScope()) 128 | } 129 | 130 | fun loadPages() { 131 | Fs.getChildFiles(pageDir).forEach { 132 | makePage(it.substring(rootDir.length + 1)) 133 | } 134 | } 135 | 136 | fun makePage(path: String): Boolean { 137 | val relPath = path.substring(pageDir.length + 1) 138 | 139 | if (pageRelPathRegex.matchEntire(relPath) == null) { 140 | Log.warn("Ilegal page file name: $relPath") 141 | return false 142 | } 143 | 144 | val page = Page(relPath.substringBeforeLast('.')) 145 | if (!page.is_valid) { 146 | return false 147 | } 148 | 149 | Site.pages.insert(page) { e, ei -> e.page_name <= ei.page_name } 150 | Log.info("Add page ${page.page_path}") 151 | return true 152 | } 153 | 154 | fun renderPages() { 155 | Log.info("") 156 | Site.pages.forEach { 157 | if (!it.needRender()) { 158 | Log.debug("Render ${it.page_path} -> ${it.target_path}. Skip") 159 | return@forEach 160 | } 161 | 162 | Try.exec("Render ${it.page_path} -> ${it.target_path}.") { 163 | it.render() 164 | } 165 | } 166 | } 167 | 168 | fun deletePage(path: String): Boolean { 169 | if (!Site.pages.removeOne { it.page_path == path }) { 170 | return false 171 | } 172 | 173 | if (path.substringBefore('.') == Site.categories_page_name) { 174 | Site.categories_page_name = "" 175 | } 176 | 177 | Log.info("Remove page $path") 178 | return true 179 | } 180 | 181 | fun getPage(path: String): Page? { 182 | return Site.pages.find { it.page_path == path } 183 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/pandolia/jane/Serve.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane 2 | 3 | import io.javalin.Javalin 4 | import io.javalin.websocket.WsConnectContext 5 | import org.eclipse.jetty.server.Server 6 | import org.eclipse.jetty.server.ServerConnector 7 | import net.pandolia.jane.libs.* 8 | import java.io.File 9 | import java.io.InputStream 10 | import java.util.* 11 | 12 | class Response( 13 | val resultStream: InputStream, 14 | val contentType: String, 15 | val statusCode: Int 16 | ) 17 | 18 | val http404 = Response("Not found".byteInputStream(), "text/plain; charset=utf-8", 404) 19 | 20 | fun http500(text: String) = Response(text.byteInputStream(), "text/plain; charset=utf-8", 500) 21 | 22 | fun fileResponse(file: File) = Response(file.inputStream(), file.mimeType, 200) 23 | 24 | fun jarResourceResponse(resPath: String) = Response( 25 | Fs.getResourceURL(resPath).readBytes().inputStream(), 26 | Fs.getMimeTypeByFileName(resPath), 27 | 200 28 | ) 29 | 30 | private val clients = LinkedList() 31 | 32 | fun serveProject() { 33 | System.setProperty("org.slf4j.simpleLogger.logFile", "System.out") 34 | System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "error") 35 | 36 | val app = Javalin.create() 37 | 38 | app.config.server { 39 | val server = Server() 40 | val connector = ServerConnector(server) 41 | connector.host = "0.0.0.0" 42 | connector.port = serverPort 43 | server.connectors = arrayOf(connector) 44 | server 45 | } 46 | 47 | app.get("/*") { ctx -> 48 | val path = ctx.path() 49 | val futrue = Futrue { onHttpGet(path) } 50 | val resp = futrue.wait() 51 | 52 | ctx.result(resp.resultStream) 53 | .contentType(resp.contentType) 54 | .status(resp.statusCode) 55 | } 56 | 57 | app.ws("/reload-on-change") { ws -> 58 | ws.onConnect { ctx -> 59 | mainQueue.put { 60 | clients.add(ctx) 61 | Log.info("Client-${ctx.sessionId.substring(0, 4)} connected") 62 | try { 63 | ctx.send("heartbeat") 64 | } catch (ex: Exception) { 65 | ex.printStackTrace() 66 | } 67 | } 68 | } 69 | 70 | ws.onClose { ctx -> 71 | mainQueue.put { 72 | clients.removeOne { it.sessionId == ctx.sessionId } 73 | Log.info("Client-${ctx.sessionId.substring(0, 4)} disconnected") 74 | } 75 | } 76 | } 77 | 78 | Log.info("Start development server at http://localhost:$serverPort/") 79 | app.start(serverPort) 80 | 81 | mainQueue.onStop { 82 | broadcast("close page") 83 | Thread.sleep(1000) 84 | app.stop() 85 | } 86 | 87 | mainQueue.put { 88 | Desk.openBrowser("http://localhost:$serverPort/") 89 | } 90 | 91 | newThread { 92 | while (true) { 93 | Thread.sleep(30000) 94 | mainQueue.put { broadcast("heartbeat") } 95 | } 96 | } 97 | } 98 | 99 | private fun onHttpGet(urlPath: String): Response { 100 | return try { 101 | onHttpGet0(urlPath) 102 | } catch (ex: Exception) { 103 | http500("${ex.message}\n${ex.stackTrace.joinToString("\n")}") 104 | } 105 | } 106 | 107 | private fun onHttpGet0(urlPath: String): Response { 108 | val file = File("$staticDir$urlPath") 109 | if (file.isFile) { 110 | return fileResponse(file) 111 | } 112 | 113 | if (urlPath == "/reload.js") { 114 | return jarResourceResponse("/reload.js") 115 | } 116 | 117 | val pageName = when { 118 | urlPath == "/" -> "index" 119 | urlPath.endsWith(".html") -> urlPath.substring(1, urlPath.length - 5) 120 | else -> return http404 121 | } 122 | 123 | val page = getPage("$pageDir/$pageName.md") ?: return http404 124 | 125 | return Response(page.renderToInputStream(), "text/html; charset=utf-8", 200) 126 | } 127 | 128 | fun broadcast(message: String) { 129 | if (clients.isEmpty()) { 130 | Log.info("Notify 0 client to $message") 131 | return 132 | } 133 | 134 | clients.forEach { ctx -> 135 | Try.get("Notify client-${ctx.sessionId.substring(0, 4)} to $message") { 136 | ctx.send(message) 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/pandolia/jane/Site.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane 2 | 3 | import net.pandolia.jane.libs.* 4 | import java.time.LocalDate 5 | import java.util.LinkedList 6 | 7 | @Suppress("unused", "PropertyName") 8 | class Category(val cate_name: String) { 9 | val cate_pages = LinkedList() 10 | val cate_id get() = cate_name.packValue 11 | } 12 | 13 | object Site { 14 | var year = "" 15 | var title = "" 16 | var owner_email = "" 17 | var owner_name = "" 18 | var owner_icp_url = "" 19 | var owner_icp = "" 20 | var categories_page_name = "" 21 | 22 | val pages = LinkedList() 23 | 24 | @Suppress("unused") 25 | val categories: LinkedList 26 | get() { 27 | val cateList = LinkedList() 28 | 29 | pages.forEach { page -> 30 | if (page.create_date.isEmpty()) { 31 | return@forEach 32 | } 33 | 34 | var cate = cateList.find { it.cate_name == page.category } 35 | if (cate == null) { 36 | cate = Category(page.category) 37 | cateList.add(cate) 38 | } 39 | 40 | cate.cate_pages.addFirst(page) 41 | } 42 | 43 | cateList.sortByDescending { it.cate_pages.count() } 44 | return cateList 45 | } 46 | 47 | @Suppress("unused") 48 | val nav_pages 49 | get() = pages.filter { it.create_date.isEmpty() } 50 | 51 | val development_mode = (Proc.command == "dev") 52 | 53 | fun assignConfig(props: Map) { 54 | year = LocalDate.now().year.toString() 55 | title = props["title"] ?: "NO-TITLE" 56 | owner_email = props["owner_email"] ?: "NO-OWNER-EMAIL" 57 | owner_name = props["owner_name"] ?: "NO-OWNER-NAME" 58 | owner_icp_url = props["owner_icp_url"] ?: "" 59 | owner_icp = props["owner_icp"] ?: "" 60 | 61 | Log.debug(""" 62 | | site.year: $year 63 | | site.title: $title 64 | | site.owner_email: $owner_email 65 | | site.owner_name: $owner_name 66 | | site.owner_icp_url: $owner_icp_url 67 | | site.owner_icp: $owner_icp 68 | | site.development_mode: $development_mode 69 | """) 70 | } 71 | } 72 | 73 | fun loadConfig() { 74 | Log.info("Jane project's root directory: $rootDir") 75 | Fs.testDirectory(pageDir) 76 | Fs.testDirectory(staticDir) 77 | Fs.testFile(configFile) 78 | Fs.testFile(templatePath) 79 | reloadConfig() 80 | } 81 | 82 | fun reloadConfig() { 83 | val props = Try.exec("Read site config from ./$configFile") { 84 | Fs.getPropsFromFile(configFile) 85 | } 86 | 87 | Site.assignConfig(props) 88 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/pandolia/jane/libs/Algorithm.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane.libs 2 | 3 | fun MutableList.removeOne(pred: (T) -> Boolean): Boolean { 4 | val it = iterator() 5 | while (it.hasNext()) { 6 | if (pred(it.next())) { 7 | it.remove() 8 | return true 9 | } 10 | } 11 | 12 | return false 13 | } 14 | 15 | @Suppress("unused") 16 | fun MutableList.uniqAdd(e: T, pred: (T, T) -> Boolean) { 17 | val it = iterator() 18 | while (it.hasNext()) { 19 | if (pred(e, it.next())) { 20 | it.remove() 21 | break 22 | } 23 | } 24 | 25 | add(e) 26 | } 27 | 28 | fun MutableList.insert(e: T, isLess: (T, T) -> Boolean) { 29 | val i = indexOfFirst { isLess(e, it) } 30 | if (i == -1) { 31 | add(e) 32 | return 33 | } 34 | 35 | add(i, e) 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/pandolia/jane/libs/Devtool.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane.libs 2 | 3 | import java.awt.Desktop 4 | import java.net.URI 5 | 6 | object Desk { 7 | fun openBrowser(uri: String) { 8 | if (!Desktop.isDesktopSupported()) { 9 | Log.info("Desktop is not supported, cancle openning browser") 10 | return 11 | } 12 | 13 | try { 14 | Desktop.getDesktop().browse(URI.create(uri)) 15 | } catch (ex: Exception) { 16 | Log.error("Failed to open broswer: ${ex.detail}") 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/pandolia/jane/libs/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane.libs 2 | 3 | import java.io.File 4 | import java.io.FileNotFoundException 5 | import java.nio.file.* 6 | import java.nio.file.attribute.BasicFileAttributes 7 | import java.util.stream.Collectors 8 | 9 | object Fs { 10 | fun readFile(filename: String) = File(filename).readText() 11 | 12 | fun getPropsFromFile(filename: String) = readFile(filename).parseToDict() 13 | 14 | fun getPropsFromResource(resName: String) = readResourceFile(resName).parseToDict() 15 | 16 | fun getResourceURL(resPath: String) = Fs.javaClass.getResource(resPath) 17 | ?: throw FileNotFoundException("Resource:$resPath") 18 | 19 | fun readResourceFile(resPath: String) = getResourceURL(resPath).readText() 20 | 21 | fun copyResources(resDir: String, tarDir: String): String { 22 | val tarFile = File(tarDir) 23 | if (tarFile.list() == null || tarFile.list()!!.isNotEmpty()) { 24 | throw Exception("\$tarDir($tarDir) must be an empty direcotry") 25 | } 26 | 27 | val jarURI = getResourceURL("").toURI() 28 | 29 | // not run with jar file 30 | if (jarURI.path != null) { 31 | val resFile = File(getResourceURL(resDir).path) 32 | if (!resFile.isDirectory) { 33 | throw Exception("\$resDir($resDir) must be a direcotry") 34 | } 35 | 36 | resFile.copyRecursively(tarFile) 37 | return "ok" 38 | } 39 | 40 | // run with jar file 41 | val fileSystem = FileSystems.newFileSystem(jarURI, mapOf()) 42 | val resPath = fileSystem.getPath(resDir) 43 | val tarPath = tarFile.toPath() 44 | 45 | Files.walkFileTree(resPath, object : SimpleFileVisitor() { 46 | private lateinit var currentTarget: Path 47 | 48 | override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { 49 | currentTarget = tarPath.resolve(resPath.relativize(dir).toString()) 50 | Files.createDirectories(currentTarget) 51 | return FileVisitResult.CONTINUE 52 | } 53 | 54 | override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { 55 | if (file == resPath) { 56 | throw Exception("\$resDir($resDir) must be a direcotry") 57 | } 58 | 59 | Files.copy(file, tarPath.resolve(resPath.relativize(file).toString())) 60 | return FileVisitResult.CONTINUE 61 | } 62 | }) 63 | 64 | return "ok" 65 | } 66 | 67 | fun getRealPath(p: String) = Paths.get(p).toRealPath().toString().replace('\\', '/') 68 | 69 | fun getChildFiles(directory: String): List { 70 | testDirectory(directory) 71 | 72 | return Files.walk(Paths.get(directory)) 73 | .filter { Files.isRegularFile(it) } 74 | .map { it.toRealPath().toString().replace('\\', '/') } 75 | .collect(Collectors.toList()) 76 | } 77 | 78 | fun getChildFiles(directory: Path): List { 79 | return Files.walk(directory) 80 | .filter { Files.isRegularFile(it) } 81 | .collect(Collectors.toList()) 82 | } 83 | 84 | fun mkDir(directory: String) = File(directory).mkdir() 85 | 86 | fun clearDir(directory: String, pred: (String) -> Boolean = { true }): String { 87 | testDirectory(directory) 88 | 89 | val path = Paths.get(directory) 90 | 91 | val n = getRealPath(directory).length + 1 92 | 93 | Files.walk(path) 94 | .filter { 95 | it.toFile().isFile && pred(it.toRealPath().toString().replace('\\', '/').substring(n)) 96 | } 97 | .forEach { Files.delete(it) } 98 | 99 | Files.walk(path) 100 | .filter { it != path && Files.isDirectory(it) } 101 | .collect(Collectors.toList()) 102 | .reversed() 103 | .forEach { Files.delete(it) } 104 | 105 | return "ok" 106 | } 107 | 108 | fun testDirectory(path: String) { 109 | if (!isDirectory(path)) { 110 | throw Exception("Directory $path not exists") 111 | } 112 | } 113 | 114 | fun testFile(path: String) { 115 | if (!isFile(path)) { 116 | throw Exception("File $path not exists") 117 | } 118 | } 119 | 120 | @Suppress("unused") 121 | fun deleteDirectory(directory: String): String { 122 | val file = File(directory) 123 | if (!file.exists()) { 124 | return "not found" 125 | } 126 | 127 | testDirectory(directory) 128 | 129 | file.deleteRecursively() 130 | return "ok" 131 | } 132 | 133 | fun copyFileIfModified(source: String, target: String) { 134 | val src = File(source) 135 | val tar = File(target) 136 | 137 | if (src.lastModified() < tar.lastModified()) { 138 | Log.debug("Copy $source -> $target. Skipped") 139 | return 140 | } 141 | 142 | Try.exec("Copy $source -> $target") { 143 | src.copyTo(tar, true) 144 | } 145 | } 146 | 147 | fun isDirectory(path: String) = File(path).isDirectory 148 | 149 | fun isFile(path: String) = File(path).isFile 150 | 151 | fun exists(path: String) = File(path).exists() 152 | 153 | fun deleteFile(path: String) = File(path).delete() 154 | 155 | val mimeMap = getPropsFromResource("/mimelist") 156 | 157 | fun getMimeTypeByFileName(resPath: String): String { 158 | return mimeMap[resPath.substringAfterLast('.')] ?: "application/octet-stream" 159 | } 160 | } 161 | 162 | val File.mimeType get() = Fs.mimeMap[extension] ?: "application/octet-stream" -------------------------------------------------------------------------------- /src/main/kotlin/net/pandolia/jane/libs/General.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane.libs 2 | 3 | import java.time.LocalDateTime 4 | import java.time.format.DateTimeFormatter 5 | import java.util.* 6 | import kotlin.system.exitProcess 7 | 8 | object Proc { 9 | init { 10 | System.setProperty("file.encoding", "UTF-8") 11 | } 12 | 13 | val workingDirectory = Fs.getRealPath(".") 14 | 15 | val args = System.getProperty("exec.args", "").split(" ").filter { it.isNotEmpty() } 16 | 17 | val command = args.firstOrNull() 18 | 19 | val subCommand = args.getOrNull(1) 20 | 21 | val isDebug = hasOption("-d", "--debug") 22 | 23 | val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") 24 | 25 | val currentTime: String get() = LocalDateTime.now().format(dateFormatter) 26 | 27 | val now: Long get() = Date().time 28 | 29 | @Suppress("unused") 30 | val osName: String = System.getProperties().getProperty("os.name") 31 | 32 | fun hasOption(vararg names: String) = args.any { it in names } 33 | 34 | fun getOption(vararg names: String): String? { 35 | for ( i in args.size - 2 downTo 0) { 36 | if (args[i] in names) { 37 | return args[i + 1] 38 | } 39 | } 40 | return null 41 | } 42 | 43 | fun exit(code: Int): Nothing = exitProcess(code) 44 | 45 | fun abort(msg: String): Nothing { 46 | Log.log("ABORT", msg) 47 | exitProcess(1) 48 | } 49 | } 50 | 51 | object Log { 52 | fun debug(msg: String) { 53 | if (!Proc.isDebug) { 54 | return 55 | } 56 | 57 | println(msg.trimMargin()) 58 | } 59 | 60 | fun log(level: String, msg: String) { 61 | println("[${Proc.currentTime}] [$level] $msg") 62 | } 63 | 64 | fun info(msg: String) { 65 | log("INFO", msg) 66 | } 67 | 68 | fun warn(msg: String) { 69 | log("WARN", msg) 70 | } 71 | 72 | fun error(msg: String) { 73 | log("ERROR", msg) 74 | } 75 | } 76 | 77 | object Try { 78 | fun get(msg: String, block: () -> T): T? { 79 | return try { 80 | val result = block() 81 | Log.log("INFO", msg) 82 | result 83 | } catch (ex: Exception) { 84 | Log.error("Failed to ${msg.decapitalize()}: ${ex.detail}") 85 | null 86 | } 87 | } 88 | 89 | fun exec(msg: String, block: () -> T): T { 90 | return get(msg, block) ?: Proc.exit(1) 91 | } 92 | } 93 | 94 | val Exception.detail: String 95 | get() = if (!Proc.isDebug) { 96 | "${javaClass.simpleName}(${message})" 97 | } else { 98 | "${javaClass.simpleName}(${message})\n ${stackTrace.joinToString("\n ")}" 99 | } 100 | -------------------------------------------------------------------------------- /src/main/kotlin/net/pandolia/jane/libs/SimpleArgOptions.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane.libs 2 | 3 | class SimpleArgOptions(val args: List) { 4 | 5 | 6 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/pandolia/jane/libs/TaskQueue.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane.libs 2 | 3 | import java.util.* 4 | import java.util.concurrent.CountDownLatch 5 | import java.util.concurrent.LinkedBlockingQueue 6 | 7 | const val TICK_INTERVAL_MILLIS_SECONDS = 500L 8 | 9 | typealias Action = () -> Unit 10 | 11 | private val NullAction: Action = {} 12 | 13 | class TaskQueue { 14 | private val queue = LinkedBlockingQueue() 15 | private val stopActions = ArrayList() 16 | private val tickActions = ArrayList() 17 | 18 | fun put(task: Action) { 19 | queue.put(task) 20 | } 21 | 22 | fun stop() = put { 23 | stopActions.forEach { put(it) } 24 | put(NullAction) 25 | } 26 | 27 | fun onStop(task: Action) = stopActions.add(task) 28 | 29 | fun onTick(task: Action) = tickActions.add(task) 30 | 31 | fun tickForever() { 32 | while (true) { 33 | Thread.sleep(TICK_INTERVAL_MILLIS_SECONDS) 34 | put { 35 | tickActions.forEach { put(it) } 36 | } 37 | } 38 | } 39 | 40 | fun run() { 41 | newThread(::tickForever) 42 | 43 | while (true) { 44 | val task = queue.take() 45 | if (task == NullAction) { 46 | Proc.exit(0) 47 | } 48 | 49 | try { 50 | task() 51 | } catch (ex: Exception) { 52 | ex.printStackTrace(System.out) 53 | Proc.exit(1) 54 | } 55 | } 56 | } 57 | } 58 | 59 | val mainQueue = TaskQueue() 60 | 61 | fun startMainQueue() { 62 | newThread { 63 | readLine() 64 | mainQueue.stop() 65 | } 66 | 67 | Log.info("Start main queue, press enter to exit.") 68 | mainQueue.run() 69 | } 70 | 71 | fun newThread(action: Action): Thread { 72 | val th = Thread(action) 73 | th.isDaemon = true 74 | th.start() 75 | return th 76 | } 77 | 78 | class Futrue(taskQueue: TaskQueue = mainQueue, val func: () -> T) { 79 | private val latch = CountDownLatch(1) 80 | 81 | private lateinit var result: T 82 | 83 | init { 84 | taskQueue.put(::task) 85 | } 86 | 87 | private fun task() { 88 | result = func() 89 | latch.countDown() 90 | } 91 | 92 | fun wait(): T { 93 | latch.await() 94 | return result 95 | } 96 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/pandolia/jane/libs/TextUtils.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane.libs 2 | 3 | import com.github.mustachejava.DefaultMustacheFactory 4 | import com.vladsch.flexmark.html.HtmlRenderer 5 | import com.vladsch.flexmark.parser.Parser 6 | import com.vladsch.flexmark.util.data.MutableDataSet 7 | import java.io.* 8 | import java.security.MessageDigest 9 | 10 | object Mustache { 11 | fun renderToFile(templateFile: String, scope: Any, outFile: String): String { 12 | val i = outFile.lastIndexOf('/') 13 | if (i != -1) { 14 | File(outFile.substring(0, i + 1)).mkdirs() 15 | } 16 | 17 | val mustache = DefaultMustacheFactory().compile(templateFile) 18 | val writer = OutputStreamWriter(FileOutputStream(outFile)) 19 | mustache.execute(writer, scope) 20 | writer.flush() 21 | writer.close() 22 | 23 | return "ok" 24 | } 25 | 26 | fun renderToInputStream(templateFile: String, scope: Any): InputStream { 27 | val mustache = DefaultMustacheFactory().compile(templateFile) 28 | val outputStream = ByteArrayOutputStream() 29 | val writer = OutputStreamWriter(outputStream) 30 | 31 | mustache.execute(writer, scope) 32 | writer.flush() 33 | 34 | return ByteArrayInputStream(outputStream.toByteArray()) 35 | } 36 | } 37 | 38 | object Markdown { 39 | private val mdOptions = MutableDataSet() 40 | private val mdParser = Parser.builder(mdOptions).build() 41 | private val mdRenderer = HtmlRenderer.builder(mdOptions).build() 42 | 43 | fun md2html(md: String): String = mdRenderer.render(mdParser.parse(md)) 44 | } 45 | 46 | fun ByteArray.toHex() = joinToString("") { "%02x".format(it) } 47 | 48 | @Suppress("unused") 49 | fun String.md5() = MessageDigest.getInstance("MD5").digest(this.toByteArray()).toHex() 50 | 51 | @Suppress("unused") 52 | fun String.toCapital() = split('-').joinToString(" ") { it.capitalize() } 53 | 54 | @Suppress("unused") 55 | val String.packValue get() = replace(' ', '-').toLowerCase() 56 | 57 | fun String.parseToDict(): Map { 58 | return split('\n') 59 | .map { it.trim() } 60 | .filter { !it.startsWith('#') && it.contains(':') } 61 | .map { line -> 62 | val i = line.indexOf(':') 63 | Pair(line.substring(0, i).trim(), line.substring(i + 1).trim()) 64 | } 65 | .toMap() 66 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/pandolia/jane/libs/Watcher.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane.libs 2 | 3 | import java.nio.file.* 4 | import com.sun.nio.file.ExtendedWatchEventModifier.FILE_TREE 5 | import java.nio.file.StandardWatchEventKinds.* 6 | import java.nio.file.attribute.BasicFileAttributes 7 | import java.util.* 8 | import kotlin.collections.HashMap 9 | 10 | const val MAX_WAITE_TIME_AFTER_CHANGE = 1500 11 | 12 | typealias FileChangeType = Boolean 13 | const val MODIFY = true 14 | const val DELETE = false 15 | 16 | class Watcher( 17 | folder: String, 18 | private val onDelete: (String) -> Unit, 19 | private val onModify: (String) -> Unit, 20 | private val onFlush: () -> Unit, 21 | private val taskQueue: TaskQueue = mainQueue 22 | ) { 23 | private val folderPath = Paths.get(folder) 24 | private val deletedFiles = HashSet() 25 | private val modifiedFiles = HashSet() 26 | private var lastChangeTime = 0L 27 | 28 | fun start() { 29 | taskQueue.onTick(::checkBuffer) 30 | newThread { WatcherRunner(folderPath, ::put).watch() } 31 | } 32 | 33 | fun put(type: FileChangeType, path: String) = taskQueue.put { 34 | lastChangeTime = Proc.now 35 | if (type == MODIFY) { 36 | deletedFiles.remove(path) 37 | modifiedFiles.add(path) 38 | } else { 39 | deletedFiles.add(path) 40 | modifiedFiles.remove(path) 41 | } 42 | } 43 | 44 | private fun checkBuffer() { 45 | if (lastChangeTime == 0L || Proc.now - lastChangeTime < MAX_WAITE_TIME_AFTER_CHANGE) { 46 | return 47 | } 48 | 49 | lastChangeTime = 0L 50 | deletedFiles.forEach(onDelete) 51 | modifiedFiles.forEach(onModify) 52 | onFlush() 53 | deletedFiles.clear() 54 | modifiedFiles.clear() 55 | } 56 | } 57 | 58 | private class WatcherRunner( 59 | val folderPath: Path, 60 | val onChange: (FileChangeType, String) -> Unit 61 | ) { 62 | 63 | private val prefixLength = folderPath.toRealPath().toString().length + 1 64 | 65 | private val allFiles = HashSet(Fs.getChildFiles(folderPath).map { trim(it) }) 66 | 67 | private val watchService = FileSystems.getDefault().newWatchService() 68 | 69 | private val types = arrayOf(ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY) 70 | 71 | private val directoryTable = HashMap() 72 | 73 | private var supportDeepMonitor = true 74 | 75 | private fun trim(path: Path) = path 76 | .toAbsolutePath() 77 | .normalize() 78 | .toString() 79 | .replace('\\', '/') 80 | .substring(prefixLength) 81 | 82 | fun watch() { 83 | try { 84 | folderPath.register(watchService, types, FILE_TREE) 85 | Log.info("Monitoring Windows File Change in $folderPath") 86 | } catch (Ex: UnsupportedOperationException) { 87 | supportDeepMonitor = false 88 | registerRecursively(folderPath, false) 89 | Log.info("Monitoring File Change in $folderPath") 90 | } 91 | 92 | while (true) { 93 | val key = watchService.take() 94 | 95 | val directory = if (supportDeepMonitor) folderPath else directoryTable[key] 96 | if (directory == null) { 97 | Log.warn("Untracked watchKey($key)") 98 | continue 99 | } 100 | 101 | key.pollEvents().forEach { processEvent(directory, it) } 102 | 103 | if (!key.reset()) { 104 | if (supportDeepMonitor) { 105 | Log.warn("Stop to monitoring File Change in $folderPath") 106 | break 107 | } 108 | 109 | directoryTable.remove(key) 110 | if (directoryTable.isEmpty()) { 111 | Log.warn("Stop to monitoring File Change in $folderPath") 112 | break 113 | } 114 | } 115 | } 116 | } 117 | 118 | private fun processEvent(directory: Path, event: WatchEvent<*>) { 119 | val kind = event.kind() 120 | val entry = directory.resolve(event.context().toString()) 121 | val path = trim(entry) 122 | val isDirectory = Files.isDirectory(entry) 123 | val isFile = Files.isRegularFile(entry) 124 | 125 | Log.debug("$kind: ${path}, isDirectory=$isDirectory, isFile=$isFile") 126 | 127 | if (isFile) { 128 | allFiles.add(path) 129 | onChange(MODIFY, path) 130 | return 131 | } 132 | 133 | if (isDirectory) { 134 | if (kind == ENTRY_MODIFY) { 135 | return 136 | } 137 | 138 | if (!supportDeepMonitor) { 139 | registerRecursively(entry, true) 140 | return 141 | } 142 | 143 | Fs.getChildFiles(entry) 144 | .map { trim(it) } 145 | .forEach { 146 | allFiles.add(it) 147 | onChange(MODIFY, it) 148 | } 149 | 150 | return 151 | } 152 | 153 | if (kind != ENTRY_DELETE) { 154 | return 155 | } 156 | 157 | val path1 = "$path/" 158 | 159 | allFiles.removeAll { 160 | if (it == path || it.startsWith(path1)) { 161 | onChange(DELETE, it) 162 | return@removeAll true 163 | } 164 | 165 | return@removeAll false 166 | } 167 | } 168 | 169 | private fun registerRecursively(dir: Path, dirIsNewlyCreate: Boolean) { 170 | Files.walkFileTree(dir, object : SimpleFileVisitor() { 171 | override fun preVisitDirectory(path: Path, attrs: BasicFileAttributes): FileVisitResult { 172 | directoryTable[path.register(watchService, types)] = path 173 | return FileVisitResult.CONTINUE 174 | } 175 | 176 | override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { 177 | if (dirIsNewlyCreate) { 178 | val path = trim(file) 179 | allFiles.add(path) 180 | onChange(MODIFY, path) 181 | } 182 | return FileVisitResult.CONTINUE 183 | } 184 | }) 185 | } 186 | 187 | } -------------------------------------------------------------------------------- /src/main/resources/hello-jane/build/readme: -------------------------------------------------------------------------------- 1 | 本目录存放 jane 编译的 html 及资源文件,可将此目录拷贝至任意位置浏览 2 | 3 | 请勿手工修改本目录下的任何文件 -------------------------------------------------------------------------------- /src/main/resources/hello-jane/src/page/2020/02-26-hello-jane.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hello jane 3 | image: https://i.picsum.photos/id/1040/2560/600.jpg 4 | category: Article 5 | --- 6 | 7 | Hello jane -------------------------------------------------------------------------------- /src/main/resources/hello-jane/src/page/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | image: 851.jpg 4 | 5 | # use local image(resources/image/851.jpg): 6 | # image: 851.jpg 7 | 8 | # use url image: 9 | # image: https://i.picsum.photos/id/867/2560/600.jpg 10 | 11 | # use dynamic image(https://picsum.photos/2560/600): 12 | # image: 13 | --- 14 | 15 | [categories] -------------------------------------------------------------------------------- /src/main/resources/hello-jane/src/page/nav1-links.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Links 3 | image: https://i.picsum.photos/id/693/2560/600.jpg 4 | --- 5 | 6 | * [Jane Home](https://jane.pandolia.net/) 7 | 8 | * [Jane Projects](https://github.com/pandolia/jane-projects) 9 | 10 | * [Jane Dev Tool Source](https://github.com/pandolia/jane) 11 | 12 | * [Alembic](https://alembic.darn.es/) 13 | 14 | * [Jekyll](https://jekyllrb.com/) 15 | 16 | * [Lorem Ipsum Photos](https://picsum.photos/) -------------------------------------------------------------------------------- /src/main/resources/hello-jane/src/site.config: -------------------------------------------------------------------------------- 1 | title: Jane Blog System 2 | owner_email: pandolia@yeah.net 3 | owner_name: Pandolia 4 | owner_icp_url: 5 | owner_icp: -------------------------------------------------------------------------------- /src/main/resources/hello-jane/src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandolia/jane/879eea5610075272fa3b14b010596084611603b5/src/main/resources/hello-jane/src/static/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/hello-jane/src/static/resources/image/851.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandolia/jane/879eea5610075272fa3b14b010596084611603b5/src/main/resources/hello-jane/src/static/resources/image/851.jpg -------------------------------------------------------------------------------- /src/main/resources/hello-jane/src/static/resources/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/hello-jane/src/static/resources/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Merriweather'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Merriweather Regular'), 6 | local('Merriweather-Regular'), 7 | url(https://fonts.gstatic.com/s/merriweather/v21/u-440qyriQwlOrhSvowK_l5-cSZMZ-Y.woff2) format('woff2'); 8 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 9 | } 10 | 11 | @font-face { 12 | font-family: 'Merriweather'; 13 | font-style: normal; 14 | font-weight: 400; 15 | src: local('Merriweather Regular'), 16 | local('Merriweather-Regular'), 17 | url(https://fonts.gstatic.com/s/merriweather/v21/u-440qyriQwlOrhSvowK_l5-eCZMZ-Y.woff2) format('woff2'); 18 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 19 | } 20 | 21 | @font-face { 22 | font-family: 'Merriweather'; 23 | font-style: normal; 24 | font-weight: 400; 25 | src: local('Merriweather Regular'), 26 | local('Merriweather-Regular'), 27 | url(https://fonts.gstatic.com/s/merriweather/v21/u-440qyriQwlOrhSvowK_l5-cyZMZ-Y.woff2) format('woff2'); 28 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, 29 | U+1EA0-1EF9, U+20AB; 30 | } 31 | 32 | @font-face { 33 | font-family: 'Merriweather'; 34 | font-style: normal; 35 | font-weight: 400; 36 | src: local('Merriweather Regular'), 37 | local('Merriweather-Regular'), 38 | url(https://fonts.gstatic.com/s/merriweather/v21/u-440qyriQwlOrhSvowK_l5-ciZMZ-Y.woff2) format('woff2'); 39 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, 40 | U+2C60-2C7F, U+A720-A7FF; 41 | } 42 | 43 | @font-face { 44 | font-family: 'Merriweather'; 45 | font-style: normal; 46 | font-weight: 400; 47 | src: local('Merriweather Regular'), 48 | local('Merriweather-Regular'), 49 | url(https://fonts.gstatic.com/s/merriweather/v21/u-440qyriQwlOrhSvowK_l5-fCZM.woff2) format('woff2'); 50 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, 51 | U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 52 | } 53 | 54 | @font-face { 55 | font-family: 'Merriweather'; 56 | font-style: normal; 57 | font-weight: 700; 58 | src: local('Merriweather Bold'), 59 | local('Merriweather-Bold'), 60 | url(https://fonts.gstatic.com/s/merriweather/v21/u-4n0qyriQwlOrhSvowK_l52xwNZVcf6lvg.woff2) format('woff2'); 61 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 62 | } 63 | 64 | @font-face { 65 | font-family: 'Merriweather'; 66 | font-style: normal; 67 | font-weight: 700; 68 | src: local('Merriweather Bold'), 69 | local('Merriweather-Bold'), 70 | url(https://fonts.gstatic.com/s/merriweather/v21/u-4n0qyriQwlOrhSvowK_l52xwNZXMf6lvg.woff2) format('woff2'); 71 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 72 | } 73 | 74 | @font-face { 75 | font-family: 'Merriweather'; 76 | font-style: normal; 77 | font-weight: 700; 78 | src: local('Merriweather Bold'), 79 | local('Merriweather-Bold'), 80 | url(https://fonts.gstatic.com/s/merriweather/v21/u-4n0qyriQwlOrhSvowK_l52xwNZV8f6lvg.woff2) format('woff2'); 81 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, 82 | U+1EA0-1EF9, U+20AB; 83 | } 84 | 85 | @font-face { 86 | font-family: 'Merriweather'; 87 | font-style: normal; 88 | font-weight: 700; 89 | src: local('Merriweather Bold'), 90 | local('Merriweather-Bold'), 91 | url(https://fonts.gstatic.com/s/merriweather/v21/u-4n0qyriQwlOrhSvowK_l52xwNZVsf6lvg.woff2) format('woff2'); 92 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, 93 | U+2C60-2C7F, U+A720-A7FF; 94 | } 95 | 96 | @font-face { 97 | font-family: 'Merriweather'; 98 | font-style: normal; 99 | font-weight: 700; 100 | src: local('Merriweather Bold'), 101 | local('Merriweather-Bold'), 102 | url(https://fonts.gstatic.com/s/merriweather/v21/u-4n0qyriQwlOrhSvowK_l52xwNZWMf6.woff2) format('woff2'); 103 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, 104 | U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 105 | } 106 | 107 | a { 108 | color: #05bf85; 109 | text-decoration: none; 110 | } 111 | 112 | a:hover { 113 | color: rgb(0, 128, 0); 114 | } 115 | 116 | h2 { 117 | font-size: 26px; 118 | color: #242e2b; 119 | } 120 | 121 | h3 { 122 | font-size: 25px; 123 | } 124 | 125 | h4 { 126 | font-size: 19px; 127 | } 128 | 129 | body { 130 | position: absolute; 131 | width: 100%; 132 | height: 100%; 133 | padding: 0; 134 | margin: 0; 135 | overflow-y: scroll; 136 | font-family: "Merriweather", serif; 137 | } 138 | 139 | .header { 140 | width: 100%; 141 | height: 80px; 142 | } 143 | 144 | .page-image { 145 | display: block; 146 | width: 100%; 147 | height: 280px; 148 | } 149 | 150 | .main { 151 | width: 50%; 152 | min-height: calc(100% - 440px); 153 | padding: 10px 25% 20px; 154 | } 155 | 156 | .footer { 157 | width: 100%; 158 | height: 34px; 159 | padding: 16px 0 0; 160 | text-align: center; 161 | font-size: 15px; 162 | background: #242e2b; 163 | color: #a8adac; 164 | } 165 | 166 | .logo { 167 | display: inline-block; 168 | background-size: 100% 100%; 169 | width: 109px; 170 | height: 64px; 171 | margin-top: 8px; 172 | margin-left: 10%; 173 | } 174 | 175 | .nav { 176 | float: right; 177 | height: 100%; 178 | margin-right: 10%; 179 | } 180 | 181 | .nav a { 182 | display: inline-block; 183 | margin-top: 27px; 184 | margin-left: 20px; 185 | font-size: 21px; 186 | } 187 | 188 | .categories-page>div { 189 | margin-bottom: 15px; 190 | color: rgb(168, 173, 172); 191 | font-size: 16px; 192 | } 193 | 194 | .categories-page>div a { 195 | font-size: 19px; 196 | font-family: "Times New Roman", serif; 197 | text-decoration: underline; 198 | margin-right: 10px; 199 | } 200 | 201 | .categories-page h2:first-child { 202 | border-top: none; 203 | margin-top: 35px; 204 | padding-top: 0; 205 | } 206 | 207 | .categories-page h2 { 208 | border-top: solid rgb(232, 232, 232) 1px; 209 | padding-top: 20px; 210 | margin-top: 35px; 211 | margin-bottom: 30px; 212 | } 213 | 214 | .article-page p { 215 | font-size: 19px; 216 | line-height: 35px; 217 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial, 218 | sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol; 219 | color: #384743; 220 | } 221 | 222 | .article-page li p { 223 | font-size: 17px; 224 | margin: 5px 0; 225 | } 226 | 227 | .article-page pre { 228 | padding: 20px; 229 | background: rgb(236, 236, 224); 230 | overflow-x: scroll; 231 | } 232 | 233 | .article-page code { 234 | font-size: 15px; 235 | line-height: 20px; 236 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace; 237 | color: #384743; 238 | } 239 | 240 | .article-page em strong { 241 | font-style: normal; 242 | font-weight: 500; 243 | color: black; 244 | } 245 | 246 | .article-page a { 247 | text-decoration: none; 248 | } 249 | 250 | .article-page a:hover { 251 | color: rgb(0, 128, 0); 252 | } 253 | 254 | .article-page .page-cate-date { 255 | margin-bottom: 15px; 256 | color: rgb(168, 173, 172); 257 | font-size: 16px; 258 | } 259 | 260 | .article-page .page-cate-date>a { 261 | font-size: 18px; 262 | } 263 | 264 | .footer a { 265 | color: #a8adac; 266 | } 267 | 268 | .footer a:hover { 269 | color: white; 270 | } 271 | 272 | .my-table { 273 | padding: 10px 0; 274 | width: 100%; 275 | } 276 | 277 | .my-table table { 278 | width: 100%; 279 | text-align: center; 280 | border-collapse: collapse; 281 | border-spacing: 0; 282 | } 283 | 284 | .my-table th, .my-table td { 285 | border: 1px solid #6ba573; 286 | padding: 10px; 287 | margin: 0; 288 | } 289 | 290 | @media (max-width: 800px) { 291 | body .main { 292 | width: 90%; 293 | min-height: calc(100% - 440px); 294 | padding: 10px 5% 20px; 295 | } 296 | } -------------------------------------------------------------------------------- /src/main/resources/hello-jane/src/template.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ page.title }} | {{ site.title }} 7 | 8 | 9 | 10 | 11 |
12 | 14 | 19 |
20 |
21 | {{# page.content_is_categories }} 22 |
23 | {{# site.categories }} 24 |

{{ cate_name }}

25 | {{# cate_pages }} 26 |
{{ title }}{{ create_date }}
27 | {{/ cate_pages }} 28 | {{/ site.categories }} 29 |
30 | {{/ page.content_is_categories }} 31 | {{^ page.content_is_categories }} 32 |
33 | {{# page.create_date }} 34 |

{{ page.title }}

35 |
36 | {{ page.category }} 37 | · {{ page.create_date }} 38 |
39 | {{/ page.create_date }} 40 | {{{ page.content }}} 41 |
42 | {{/ page.content_is_categories }} 43 | 52 | {{# site.development_mode }} 53 | 54 | {{/ site.development_mode}} 55 | 56 | -------------------------------------------------------------------------------- /src/main/resources/mimelist: -------------------------------------------------------------------------------- 1 | 3dml: text/vndin3d3dml 2 | 3g2: video/3gpp2 3 | 3gp: video/3gpp 4 | 7z: application/x-7z-compressed 5 | aab: application/x-authorware-bin 6 | aac: audio/x-aac 7 | aam: application/x-authorware-map 8 | aas: application/x-authorware-seg 9 | abw: application/x-abiword 10 | ac: application/pkix-attr-cert 11 | acc: application/vndamericandynamicsacc 12 | ace: application/x-ace-compressed 13 | acu: application/vndacucobol 14 | adp: audio/adpcm 15 | aep: application/vndaudiograph 16 | afp: application/vndibmmodcap 17 | ahead: application/vndaheadspace 18 | ai: application/postscript 19 | aif: audio/x-aiff 20 | air: application/vndadobeair-application-installer-package+zip 21 | ait: application/vnddvbait 22 | ami: application/vndamigaami 23 | apk: application/vndandroidpackage-archive 24 | application: application/x-ms-application 25 | apr: application/vndlotus-approach 26 | asf: video/x-ms-asf 27 | aso: application/vndaccpacsimplyaso 28 | atc: application/vndacucorp 29 | atom: application/atom+xml 30 | atomcat: application/atomcat+xml 31 | atomsvc: application/atomsvc+xml 32 | atx: application/vndantixgame-component 33 | au: audio/basic 34 | avi: video/x-msvideo 35 | aw: application/applixware 36 | azf: application/vndairzipfilesecureazf 37 | azs: application/vndairzipfilesecureazs 38 | azw: application/vndamazonebook 39 | bcpio: application/x-bcpio 40 | bdf: application/x-font-bdf 41 | bdm: application/vndsyncmldm+wbxml 42 | bed: application/vndrealvncbed 43 | bh2: application/vndfujitsuoasysprs 44 | bin: application/octet-stream 45 | bmi: application/vndbmi 46 | bmp: image/bmp 47 | box: application/vndpreviewsystemsbox 48 | btif: image/prsbtif 49 | bz: application/x-bzip 50 | bz2: application/x-bzip2 51 | c: text/x-c 52 | c11amc: application/vndcluetrustcartomobile-config 53 | c11amz: application/vndcluetrustcartomobile-config-pkg 54 | c4g: application/vndclonkc4group 55 | cab: application/vndms-cab-compressed 56 | car: application/vndcurlcar 57 | cat: application/vndms-pkiseccat 58 | ccxml: application/ccxml+xml 59 | cdbcmsg: application/vndcontactcmsg 60 | cdkey: application/vndmediastationcdkey 61 | cdmia: application/cdmi-capability 62 | cdmic: application/cdmi-container 63 | cdmid: application/cdmi-domain 64 | cdmio: application/cdmi-object 65 | cdmiq: application/cdmi-queue 66 | cdx: chemical/x-cdx 67 | cdxml: application/vndchemdraw+xml 68 | cdy: application/vndcinderella 69 | cer: application/pkix-cert 70 | cgm: image/cgm 71 | chat: application/x-chat 72 | chm: application/vndms-htmlhelp 73 | chrt: application/vndkdekchart 74 | cif: chemical/x-cif 75 | cii: application/vndanser-web-certificate-issue-initiation 76 | cil: application/vndms-artgalry 77 | cla: application/vndclaymore 78 | class: application/java-vm 79 | clkk: application/vndcrickclickerkeyboard 80 | clkp: application/vndcrickclickerpalette 81 | clkt: application/vndcrickclickertemplate 82 | clkw: application/vndcrickclickerwordbank 83 | clkx: application/vndcrickclicker 84 | clp: application/x-msclip 85 | cmc: application/vndcosmocaller 86 | cmdf: chemical/x-cmdf 87 | cml: chemical/x-cml 88 | cmp: application/vndyellowriver-custom-menu 89 | cmx: image/x-cmx 90 | cod: application/vndrimcod 91 | cpio: application/x-cpio 92 | cpt: application/mac-compactpro 93 | crd: application/x-mscardfile 94 | crl: application/pkix-crl 95 | cryptonote: application/vndrigcryptonote 96 | csh: application/x-csh 97 | csml: chemical/x-csml 98 | csp: application/vndcommonspace 99 | css: text/css 100 | csv: text/csv 101 | cu: application/cu-seeme 102 | curl: text/vndcurl 103 | cww: application/prscww 104 | dae: model/vndcollada+xml 105 | daf: application/vndmobiusdaf 106 | davmount: application/davmount+xml 107 | dcurl: text/vndcurldcurl 108 | dd2: application/vndomadd2+xml 109 | ddd: application/vndfujixeroxddd 110 | deb: application/x-debian-package 111 | der: application/x-x509-ca-cert 112 | dfac: application/vnddreamfactory 113 | dir: application/x-director 114 | dis: application/vndmobiusdis 115 | djvu: image/vnddjvu 116 | dmg: application/x-apple-diskimage 117 | dna: application/vnddna 118 | doc: application/msword 119 | docm: application/vndms-worddocumentmacroenabled12 120 | docx: application/vndopenxmlformats-officedocumentwordprocessingmldocument 121 | dotm: application/vndms-wordtemplatemacroenabled12 122 | dotx: application/vndopenxmlformats-officedocumentwordprocessingmltemplate 123 | dp: application/vndosgidp 124 | dpg: application/vnddpgraph 125 | dra: audio/vnddra 126 | dsc: text/prslinestag 127 | dssc: application/dssc+der 128 | dtb: application/x-dtbook+xml 129 | dtd: application/xml-dtd 130 | dts: audio/vnddts 131 | dtshd: audio/vnddtshd 132 | dvi: application/x-dvi 133 | dwf: model/vnddwf 134 | dwg: image/vnddwg 135 | dxf: image/vnddxf 136 | dxp: application/vndspotfiredxp 137 | ecelp4800: audio/vndnueraecelp4800 138 | ecelp7470: audio/vndnueraecelp7470 139 | ecelp9600: audio/vndnueraecelp9600 140 | edm: application/vndnovadigmedm 141 | edx: application/vndnovadigmedx 142 | efif: application/vndpicsel 143 | ei6: application/vndpgosasli 144 | eml: message/rfc822 145 | emma: application/emma+xml 146 | eol: audio/vnddigital-winds 147 | eot: application/vndms-fontobject 148 | epub: application/epub+zip 149 | es: application/ecmascript 150 | es3: application/vndeszigno3+xml 151 | esf: application/vndepsonesf 152 | etx: text/x-setext 153 | exe: application/x-msdownload 154 | exi: application/exi 155 | ext: application/vndnovadigmext 156 | ez2: application/vndezpix-album 157 | ez3: application/vndezpix-package 158 | f: text/x-fortran 159 | f4v: video/x-f4v 160 | fbs: image/vndfastbidsheet 161 | fcs: application/vndisacfcs 162 | fdf: application/vndfdf 163 | fe_launch: application/vnddenovofcselayout-link 164 | fg5: application/vndfujitsuoasysgp 165 | fh: image/x-freehand 166 | fig: application/x-xfig 167 | fli: video/x-fli 168 | flo: application/vndmicrografxflo 169 | flv: video/x-flv 170 | flw: application/vndkdekivio 171 | flx: text/vndfmiflexstor 172 | fly: text/vndfly 173 | fm: application/vndframemaker 174 | fnc: application/vndfrogansfnc 175 | fpx: image/vndfpx 176 | fsc: application/vndfscweblaunch 177 | fst: image/vndfst 178 | ftc: application/vndfluxtimeclip 179 | fti: application/vndanser-web-funds-transfer-initiation 180 | fvt: video/vndfvt 181 | fxp: application/vndadobefxp 182 | fzs: application/vndfuzzysheet 183 | g2w: application/vndgeoplan 184 | g3: image/g3fax 185 | g3w: application/vndgeospace 186 | gac: application/vndgroove-account 187 | gdl: model/vndgdl 188 | geo: application/vnddynageo 189 | gex: application/vndgeometry-explorer 190 | ggb: application/vndgeogebrafile 191 | ggt: application/vndgeogebratool 192 | ghf: application/vndgroove-help 193 | gif: image/gif 194 | gim: application/vndgroove-identity-message 195 | gmx: application/vndgmx 196 | gnumeric: application/x-gnumeric 197 | gph: application/vndflographit 198 | gqf: application/vndgrafeq 199 | gram: application/srgs 200 | grv: application/vndgroove-injector 201 | grxml: application/srgs+xml 202 | gsf: application/x-font-ghostscript 203 | gtar: application/x-gtar 204 | gtm: application/vndgroove-tool-message 205 | gtw: model/vndgtw 206 | gv: text/vndgraphviz 207 | gxt: application/vndgeonext 208 | h261: video/h261 209 | h263: video/h263 210 | h264: video/h264 211 | hal: application/vndhal+xml 212 | hbci: application/vndhbci 213 | hdf: application/x-hdf 214 | hlp: application/winhlp 215 | hpgl: application/vndhp-hpgl 216 | hpid: application/vndhp-hpid 217 | hps: application/vndhp-hps 218 | hqx: application/mac-binhex40 219 | htke: application/vndkenameaapp 220 | html: text/html 221 | hvd: application/vndyamahahv-dic 222 | hvp: application/vndyamahahv-voice 223 | hvs: application/vndyamahahv-script 224 | i2g: application/vndintergeo 225 | icc: application/vndiccprofile 226 | ice: x-conference/x-cooltalk 227 | ico: image/x-icon 228 | ics: text/calendar 229 | ief: image/ief 230 | ifm: application/vndshanainformedformdata 231 | igl: application/vndigloader 232 | igm: application/vndinsorsigm 233 | igs: model/iges 234 | igx: application/vndmicrografxigx 235 | iif: application/vndshanainformedinterchange 236 | imp: application/vndaccpacsimplyimp 237 | ims: application/vndms-ims 238 | ipfix: application/ipfix 239 | ipk: application/vndshanainformedpackage 240 | irm: application/vndibmrights-management 241 | irp: application/vndirepositorypackage+xml 242 | itp: application/vndshanainformedformtemplate 243 | ivp: application/vndimmervision-ivp 244 | ivu: application/vndimmervision-ivu 245 | jad: text/vndsunj2meapp-descriptor 246 | jam: application/vndjam 247 | jar: application/java-archive 248 | java: text/x-java-sourcejava 249 | jisp: application/vndjisp 250 | jlt: application/vndhp-jlyt 251 | jnlp: application/x-java-jnlp-file 252 | joda: application/vndjoostjoda-archive 253 | jpeg: image/jpeg 254 | jpeg: image/x-citrix-jpeg 255 | jpg: image/jpg 256 | jpgv: video/jpeg 257 | jpm: video/jpm 258 | js: application/javascript 259 | json: application/json 260 | karbon: application/vndkdekarbon 261 | kfo: application/vndkdekformula 262 | kia: application/vndkidspiration 263 | kml: application/vndgoogle-earthkml+xml 264 | kmz: application/vndgoogle-earthkmz 265 | kne: application/vndkinar 266 | kon: application/vndkdekontour 267 | kpr: application/vndkdekpresenter 268 | ksp: application/vndkdekspread 269 | ktx: image/ktx 270 | ktz: application/vndkahootz 271 | kwd: application/vndkdekword 272 | lasxml: application/vndlaslas+xml 273 | latex: application/x-latex 274 | lbd: application/vndllamagraphicslife-balancedesktop 275 | lbe: application/vndllamagraphicslife-balanceexchange+xml 276 | les: application/vndhhelesson-player 277 | link66: application/vndroute66link66+xml 278 | lrm: application/vndms-lrm 279 | ltf: application/vndfrogansltf 280 | lvp: audio/vndlucentvoice 281 | lwp: application/vndlotus-wordpro 282 | m21: application/mp21 283 | m3u: audio/x-mpegurl 284 | m3u8: application/vndapplempegurl 285 | m4v: video/x-m4v 286 | ma: application/mathematica 287 | mads: application/mads+xml 288 | mag: application/vndecowinchart 289 | mathml: application/mathml+xml 290 | mbk: application/vndmobiusmbk 291 | mbox: application/mbox 292 | mc1: application/vndmedcalcdata 293 | mcd: application/vndmcd 294 | mcurl: text/vndcurlmcurl 295 | mdb: application/x-msaccess 296 | mdi: image/vndms-modi 297 | meta4: application/metalink4+xml 298 | mets: application/mets+xml 299 | mfm: application/vndmfmp 300 | mgp: application/vndosgeomapguidepackage 301 | mgz: application/vndproteusmagazine 302 | mid: audio/midi 303 | mif: application/vndmif 304 | mj2: video/mj2 305 | mlp: application/vnddolbymlp 306 | mmd: application/vndchipnutskaraoke-mmd 307 | mmf: application/vndsmaf 308 | mmr: image/vndfujixeroxedmics-mmr 309 | mny: application/x-msmoney 310 | mods: application/mods+xml 311 | movie: video/x-sgi-movie 312 | mp4: video/mp4 313 | mp4a: audio/mp4 314 | mpc: application/vndmophuncertificate 315 | mpeg: video/mpeg 316 | mpga: audio/mpeg 317 | mpkg: application/vndappleinstaller+xml 318 | mpm: application/vndblueicemultipass 319 | mpn: application/vndmophunapplication 320 | mpp: application/vndms-project 321 | mpy: application/vndibmminipay 322 | mqy: application/vndmobiusmqy 323 | mrc: application/marc 324 | mrcx: application/marcxml+xml 325 | mscml: application/mediaservercontrol+xml 326 | mseq: application/vndmseq 327 | msf: application/vndepsonmsf 328 | msh: model/mesh 329 | msl: application/vndmobiusmsl 330 | msty: application/vndmuveestyle 331 | mts: model/vndmts 332 | mus: application/vndmusician 333 | musicxml: application/vndrecordaremusicxml+xml 334 | mvb: application/x-msmediaview 335 | mwf: application/vndmfer 336 | mxf: application/mxf 337 | mxl: application/vndrecordaremusicxml 338 | mxml: application/xv+xml 339 | mxs: application/vndtriscapemxs 340 | mxu: video/vndmpegurl 341 | n3: text/n3 342 | nbp: application/vndwolframplayer 343 | nc: application/x-netcdf 344 | ncx: application/x-dtbncx+xml 345 | n-gage: application/vndnokian-gagesymbianinstall 346 | ngdat: application/vndnokian-gagedata 347 | nlu: application/vndneurolanguagenlu 348 | nml: application/vndenliven 349 | nnd: application/vndnoblenet-directory 350 | nns: application/vndnoblenet-sealer 351 | nnw: application/vndnoblenet-web 352 | npx: image/vndnet-fpx 353 | nsf: application/vndlotus-notes 354 | oa2: application/vndfujitsuoasys2 355 | oa3: application/vndfujitsuoasys3 356 | oas: application/vndfujitsuoasys 357 | obd: application/x-msbinder 358 | oda: application/oda 359 | odb: application/vndoasisopendocumentdatabase 360 | odc: application/vndoasisopendocumentchart 361 | odf: application/vndoasisopendocumentformula 362 | odft: application/vndoasisopendocumentformula-template 363 | odg: application/vndoasisopendocumentgraphics 364 | odi: application/vndoasisopendocumentimage 365 | odm: application/vndoasisopendocumenttext-master 366 | odp: application/vndoasisopendocumentpresentation 367 | ods: application/vndoasisopendocumentspreadsheet 368 | odt: application/vndoasisopendocumenttext 369 | oga: audio/ogg 370 | ogv: video/ogg 371 | ogx: application/ogg 372 | onetoc: application/onenote 373 | opf: application/oebps-package+xml 374 | org: application/vndlotus-organizer 375 | osf: application/vndyamahaopenscoreformat 376 | osfpvg: application/vndyamahaopenscoreformatosfpvg+xml 377 | otc: application/vndoasisopendocumentchart-template 378 | otf: application/x-font-otf 379 | otg: application/vndoasisopendocumentgraphics-template 380 | oth: application/vndoasisopendocumenttext-web 381 | oti: application/vndoasisopendocumentimage-template 382 | otp: application/vndoasisopendocumentpresentation-template 383 | ots: application/vndoasisopendocumentspreadsheet-template 384 | ott: application/vndoasisopendocumenttext-template 385 | oxt: application/vndopenofficeorgextension 386 | p: text/x-pascal 387 | p10: application/pkcs10 388 | p12: application/x-pkcs12 389 | p7b: application/x-pkcs7-certificates 390 | p7m: application/pkcs7-mime 391 | p7r: application/x-pkcs7-certreqresp 392 | p7s: application/pkcs7-signature 393 | p8: application/pkcs8 394 | par: text/plain-bas 395 | paw: application/vndpawaafile 396 | pbd: application/vndpowerbuilder6 397 | pbm: image/x-portable-bitmap 398 | pcf: application/x-font-pcf 399 | pcl: application/vndhp-pcl 400 | pclxl: application/vndhp-pclxl 401 | pcurl: application/vndcurlpcurl 402 | pcx: image/x-pcx 403 | pdb: application/vndpalm 404 | pdf: application/pdf 405 | pfa: application/x-font-type1 406 | pfr: application/font-tdpfr 407 | pgm: image/x-portable-graymap 408 | pgn: application/x-chess-pgn 409 | pgp: application/pgp-encrypted 410 | pgp: application/pgp-signature 411 | pic: image/x-pict 412 | pjpeg: image/pjpeg 413 | pki: application/pkixcmp 414 | pkipath: application/pkix-pkipath 415 | plb: application/vnd3gpppic-bw-large 416 | plc: application/vndmobiusplc 417 | plf: application/vndpocketlearn 418 | pls: application/pls+xml 419 | pml: application/vndctc-posml 420 | png: image/png 421 | pnm: image/x-portable-anymap 422 | portpkg: application/vndmacportsportpkg 423 | potm: application/vndms-powerpointtemplatemacroenabled12 424 | potx: application/vndopenxmlformats-officedocumentpresentationmltemplate 425 | ppam: application/vndms-powerpointaddinmacroenabled12 426 | ppd: application/vndcups-ppd 427 | ppm: image/x-portable-pixmap 428 | ppsm: application/vndms-powerpointslideshowmacroenabled12 429 | ppsx: application/vndopenxmlformats-officedocumentpresentationmlslideshow 430 | ppt: application/vndms-powerpoint 431 | pptm: application/vndms-powerpointpresentationmacroenabled12 432 | pptx: application/vndopenxmlformats-officedocumentpresentationmlpresentation 433 | prc: application/x-mobipocket-ebook 434 | pre: application/vndlotus-freelance 435 | prf: application/pics-rules 436 | psb: application/vnd3gpppic-bw-small 437 | psd: image/vndadobephotoshop 438 | psf: application/x-font-linux-psf 439 | pskcxml: application/pskc+xml 440 | ptid: application/vndpviptid1 441 | pub: application/x-mspublisher 442 | pvb: application/vnd3gpppic-bw-var 443 | pwn: application/vnd3mpost-it-notes 444 | pya: audio/vndms-playreadymediapya 445 | pyv: video/vndms-playreadymediapyv 446 | qam: application/vndepsonquickanime 447 | qbo: application/vndintuqbo 448 | qfx: application/vndintuqfx 449 | qps: application/vndpublishare-delta-tree 450 | qt: video/quicktime 451 | qxd: application/vndquarkquarkxpress 452 | ram: audio/x-pn-realaudio 453 | rar: application/x-rar-compressed 454 | ras: image/x-cmu-raster 455 | rcprofile: application/vndipunpluggedrcprofile 456 | rdf: application/rdf+xml 457 | rdz: application/vnddata-visionrdz 458 | rep: application/vndbusinessobjects 459 | res: application/x-dtbresource+xml 460 | rgb: image/x-rgb 461 | rif: application/reginfo+xml 462 | rip: audio/vndrip 463 | rl: application/resource-lists+xml 464 | rlc: image/vndfujixeroxedmics-rlc 465 | rld: application/resource-lists-diff+xml 466 | rm: application/vndrn-realmedia 467 | rmp: audio/x-pn-realaudio-plugin 468 | rms: application/vndjcpjavamemidlet-rms 469 | rnc: application/relax-ng-compact-syntax 470 | rp9: application/vndcloantorp9 471 | rpss: application/vndnokiaradio-presets 472 | rpst: application/vndnokiaradio-preset 473 | rq: application/sparql-query 474 | rs: application/rls-services+xml 475 | rsd: application/rsd+xml 476 | rss: application/rss+xml 477 | rtf: application/rtf 478 | rtx: text/richtext 479 | s: text/x-asm 480 | saf: application/vndyamahasmaf-audio 481 | sbml: application/sbml+xml 482 | sc: application/vndibmsecure-container 483 | scd: application/x-msschedule 484 | scm: application/vndlotus-screencam 485 | scq: application/scvp-cv-request 486 | scs: application/scvp-cv-response 487 | scurl: text/vndcurlscurl 488 | sda: application/vndstardivisiondraw 489 | sdc: application/vndstardivisioncalc 490 | sdd: application/vndstardivisionimpress 491 | sdkm: application/vndsolentsdkm+xml 492 | sdp: application/sdp 493 | sdw: application/vndstardivisionwriter 494 | see: application/vndseemail 495 | seed: application/vndfdsnseed 496 | sema: application/vndsema 497 | semd: application/vndsemd 498 | semf: application/vndsemf 499 | ser: application/java-serialized-object 500 | setpay: application/set-payment-initiation 501 | setreg: application/set-registration-initiation 502 | sfd-hdstx: application/vndhydrostatixsof-data 503 | sfs: application/vndspotfiresfs 504 | sgl: application/vndstardivisionwriter-global 505 | sgml: text/sgml 506 | sh: application/x-sh 507 | shar: application/x-shar 508 | shf: application/shf+xml 509 | sis: application/vndsymbianinstall 510 | sit: application/x-stuffit 511 | sitx: application/x-stuffitx 512 | skp: application/vndkoan 513 | sldm: application/vndms-powerpointslidemacroenabled12 514 | sldx: application/vndopenxmlformats-officedocumentpresentationmlslide 515 | slt: application/vndepsonsalt 516 | sm: application/vndstepmaniastepchart 517 | smf: application/vndstardivisionmath 518 | smi: application/smil+xml 519 | snf: application/x-font-snf 520 | spf: application/vndyamahasmaf-phrase 521 | spl: application/x-futuresplash 522 | spot: text/vndin3dspot 523 | spp: application/scvp-vp-response 524 | spq: application/scvp-vp-request 525 | src: application/x-wais-source 526 | sru: application/sru+xml 527 | srx: application/sparql-results+xml 528 | sse: application/vndkodak-descriptor 529 | ssf: application/vndepsonssf 530 | ssml: application/ssml+xml 531 | st: application/vndsailingtrackertrack 532 | stc: application/vndsunxmlcalctemplate 533 | std: application/vndsunxmldrawtemplate 534 | stf: application/vndwtstf 535 | sti: application/vndsunxmlimpresstemplate 536 | stk: application/hyperstudio 537 | stl: application/vndms-pkistl 538 | str: application/vndpgformat 539 | stw: application/vndsunxmlwritertemplate 540 | sub: image/vnddvbsubtitle 541 | sus: application/vndsus-calendar 542 | sv4cpio: application/x-sv4cpio 543 | sv4crc: application/x-sv4crc 544 | svc: application/vnddvbservice 545 | svd: application/vndsvd 546 | svg: image/svg+xml 547 | swf: application/x-shockwave-flash 548 | swi: application/vndaristanetworksswi 549 | sxc: application/vndsunxmlcalc 550 | sxd: application/vndsunxmldraw 551 | sxg: application/vndsunxmlwriterglobal 552 | sxi: application/vndsunxmlimpress 553 | sxm: application/vndsunxmlmath 554 | sxw: application/vndsunxmlwriter 555 | t: text/troff 556 | tao: application/vndtaointent-module-archive 557 | tar: application/x-tar 558 | tcap: application/vnd3gpp2tcap 559 | tcl: application/x-tcl 560 | teacher: application/vndsmartteacher 561 | tei: application/tei+xml 562 | tex: application/x-tex 563 | texinfo: application/x-texinfo 564 | tfi: application/thraud+xml 565 | tfm: application/x-tex-tfm 566 | thmx: application/vndms-officetheme 567 | tiff: image/tiff 568 | tmo: application/vndtmobile-livetv 569 | torrent: application/x-bittorrent 570 | tpl: application/vndgroove-tool-template 571 | tpt: application/vndtridtpt 572 | tra: application/vndtrueapp 573 | trm: application/x-msterminal 574 | tsd: application/timestamped-data 575 | tsv: text/tab-separated-values 576 | ttf: application/x-font-ttf 577 | ttl: text/turtle 578 | twd: application/vndsimtech-mindmapper 579 | txd: application/vndgenomatixtuxedo 580 | txf: application/vndmobiustxf 581 | txt: text/plain 582 | ufd: application/vndufdl 583 | umj: application/vndumajin 584 | unityweb: application/vndunity 585 | uoml: application/vnduoml+xml 586 | uri: text/uri-list 587 | ustar: application/x-ustar 588 | utz: application/vnduiqtheme 589 | uu: text/x-uuencode 590 | uva: audio/vnddeceaudio 591 | uvh: video/vnddecehd 592 | uvi: image/vnddecegraphic 593 | uvm: video/vnddecemobile 594 | uvp: video/vnddecepd 595 | uvs: video/vnddecesd 596 | uvu: video/vnduvvump4 597 | uvv: video/vnddecevideo 598 | vcd: application/x-cdlink 599 | vcf: text/x-vcard 600 | vcg: application/vndgroove-vcard 601 | vcs: text/x-vcalendar 602 | vcx: application/vndvcx 603 | vis: application/vndvisionary 604 | viv: video/vndvivo 605 | vsd: application/vndvisio 606 | vsdx: application/vndvisio2013 607 | vsf: application/vndvsf 608 | vtu: model/vndvtu 609 | vxml: application/voicexml+xml 610 | wad: application/x-doom 611 | wav: audio/x-wav 612 | wax: audio/x-ms-wax 613 | wbmp: image/vndwapwbmp 614 | wbs: application/vndcriticaltoolswbs+xml 615 | wbxml: application/vndwapwbxml 616 | weba: audio/webm 617 | webm: video/webm 618 | webp: image/webp 619 | wg: application/vndpmiwidget 620 | wgt: application/widget 621 | wm: video/x-ms-wm 622 | wma: audio/x-ms-wma 623 | wmd: application/x-ms-wmd 624 | wmf: application/x-msmetafile 625 | wml: text/vndwapwml 626 | wmlc: application/vndwapwmlc 627 | wmls: text/vndwapwmlscript 628 | wmlsc: application/vndwapwmlscriptc 629 | wmv: video/x-ms-wmv 630 | wmx: video/x-ms-wmx 631 | wmz: application/x-ms-wmz 632 | woff: application/x-font-woff 633 | wpd: application/vndwordperfect 634 | wpl: application/vndms-wpl 635 | wps: application/vndms-works 636 | wqd: application/vndwqd 637 | wri: application/x-mswrite 638 | wrl: model/vrml 639 | wsdl: application/wsdl+xml 640 | wspolicy: application/wspolicy+xml 641 | wtb: application/vndwebturbo 642 | wvx: video/x-ms-wvx 643 | x3d: application/vndhzn-3d-crossword 644 | xap: application/x-silverlight-app 645 | xar: application/vndxara 646 | xbap: application/x-ms-xbap 647 | xbd: application/vndfujixeroxdocuworksbinder 648 | xbm: image/x-xbitmap 649 | xdf: application/xcap-diff+xml 650 | xdm: application/vndsyncmldm+xml 651 | xdp: application/vndadobexdp+xml 652 | xdssc: application/dssc+xml 653 | xdw: application/vndfujixeroxdocuworks 654 | xenc: application/xenc+xml 655 | xer: application/patch-ops-error+xml 656 | xfdf: application/vndadobexfdf 657 | xfdl: application/vndxfdl 658 | xhtml: application/xhtml+xml 659 | xif: image/vndxiff 660 | xlam: application/vndms-exceladdinmacroenabled12 661 | xls: application/vndms-excel 662 | xlsb: application/vndms-excelsheetbinarymacroenabled12 663 | xlsm: application/vndms-excelsheetmacroenabled12 664 | xlsx: application/vndopenxmlformats-officedocumentspreadsheetmlsheet 665 | xltm: application/vndms-exceltemplatemacroenabled12 666 | xltx: application/vndopenxmlformats-officedocumentspreadsheetmltemplate 667 | xml: application/xml 668 | xml: application/xml 669 | xo: application/vndolpc-sugar 670 | xop: application/xop+xml 671 | xpi: application/x-xpinstall 672 | xpm: image/x-xpixmap 673 | xpr: application/vndis-xpr 674 | xps: application/vndms-xpsdocument 675 | xpw: application/vndinterconformnet 676 | xslt: application/xslt+xml 677 | xsm: application/vndsyncml+xml 678 | xspf: application/xspf+xml 679 | xul: application/vndmozillaxul+xml 680 | xwd: image/x-xwindowdump 681 | xyz: chemical/x-xyz 682 | yaml: text/yaml 683 | yang: application/yang 684 | yin: application/yin+xml 685 | zaz: application/vndzzazzdeck+xml 686 | zip: application/zip 687 | zir: application/vndzul 688 | zmm: application/vndhandheld-entertainment+xml -------------------------------------------------------------------------------- /src/main/resources/reload.js: -------------------------------------------------------------------------------- 1 | window.jane = {}; 2 | 3 | jane.getTime = function () { 4 | return (new Date()).toLocaleString(); 5 | }; 6 | 7 | jane.closed = false; 8 | 9 | jane.close = function(message) { 10 | jane.closed = true; 11 | document.title = message; 12 | document.body.innerHTML = jane.getTime() + ' ' + message; 13 | }; 14 | 15 | jane['reload page'] = function () { 16 | jane.closed = true; 17 | document.title = "Reloading page......"; 18 | window.location.reload(); 19 | }; 20 | 21 | jane['close page'] = function () { 22 | jane.close("Page is closed"); 23 | }; 24 | 25 | jane.counter = 0; 26 | 27 | jane['heartbeat'] = function () { 28 | if (jane.counter === 0) { 29 | document.title = '00 ' + document.title; 30 | jane.counter = 101; 31 | return; 32 | } 33 | 34 | document.title = String(jane.counter).slice(1) + document.title.slice(2); 35 | jane.counter++; 36 | if (jane.counter === 1000) { 37 | jane.counter = 100; 38 | } 39 | }; 40 | 41 | jane.onMessage = function (event) { 42 | var func = jane[event.data]; 43 | 44 | if (!func) { 45 | jane.close('Unknown command: ' + event.data); 46 | return; 47 | } 48 | 49 | func(); 50 | }; 51 | 52 | jane.onDisconnect = function () { 53 | jane.close("Connetction with the development websocked server has broken"); 54 | }; 55 | 56 | jane.onError = function() { 57 | jane.close("Failed to connect to the development websocked server"); 58 | }; 59 | 60 | jane.connect = function() { 61 | var ws = new WebSocket('ws://' + location.host + '/reload-on-change'); 62 | ws.addEventListener('error', jane.onError); 63 | ws.addEventListener('message', jane.onMessage); 64 | ws.addEventListener('close', jane.onDisconnect); 65 | }; 66 | 67 | jane.connect(); -------------------------------------------------------------------------------- /src/test/kotlin/net/pandolia/jane/libs/LibTest.kt: -------------------------------------------------------------------------------- 1 | package net.pandolia.jane.libs 2 | 3 | import org.junit.Test 4 | 5 | class LibTest { 6 | 7 | @Test fun test1() { 8 | 9 | } 10 | 11 | } --------------------------------------------------------------------------------