├── template ├── theme │ ├── robots.txt │ ├── .gitignore │ ├── favicon.png │ ├── source │ │ ├── js │ │ │ ├── searchTpl.html │ │ │ ├── jquery.unveil.js │ │ │ ├── index.js │ │ │ └── highlight.pack.js │ │ └── css │ │ │ ├── color.css │ │ │ ├── global.css │ │ │ ├── code.css │ │ │ ├── archive-tag.css │ │ │ ├── index.css │ │ │ ├── article.css │ │ │ ├── content.css │ │ │ └── page.css │ ├── postcss.config.js │ ├── _footer.html │ ├── _search.html │ ├── _comment.html │ ├── _header.html │ ├── bundle │ │ ├── index.js.LICENSE.txt │ │ ├── searchWorker.js │ │ ├── index.css │ │ └── main.css │ ├── package.json │ ├── webpack.config.js │ ├── tag.html │ ├── archive.html │ ├── _head.html │ ├── page.html │ ├── article.html │ └── config.yml ├── README.md ├── source │ ├── images │ │ ├── avatar.png │ │ ├── example.png │ │ └── example-en.png │ ├── about.me.md │ ├── ink-blog-tool.md │ └── ink-blog-tool-en.md └── config.yml ├── .gitignore ├── Dockerfile ├── docker-ink-runtime └── Dockerfile ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── release.yml ├── funcs.go ├── go.mod ├── release.sh ├── go.sum ├── util.go ├── serve.go ├── README.md ├── render.go ├── api.go ├── main.go └── parse.go /template/theme/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /template/README.md: -------------------------------------------------------------------------------- 1 | # Blog 2 | Quick start blog for ink 3 | -------------------------------------------------------------------------------- /template/theme/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | yarn.lock -------------------------------------------------------------------------------- /template/theme/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InkProject/ink/HEAD/template/theme/favicon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ink 2 | ink.exe 3 | public 4 | release 5 | debian 6 | npm-debug.log 7 | editor_old 8 | .idea 9 | .DS_Store -------------------------------------------------------------------------------- /template/source/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InkProject/ink/HEAD/template/source/images/avatar.png -------------------------------------------------------------------------------- /template/source/images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InkProject/ink/HEAD/template/source/images/example.png -------------------------------------------------------------------------------- /template/source/images/example-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InkProject/ink/HEAD/template/source/images/example-en.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18 2 | ADD . /code 3 | WORKDIR /code 4 | RUN go install 5 | EXPOSE 8000 6 | CMD ["ink", "preview", "template"] -------------------------------------------------------------------------------- /template/theme/source/js/searchTpl.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {{title}} 3 |
    {{preview}}
    4 |
  • 5 | -------------------------------------------------------------------------------- /template/source/about.me.md: -------------------------------------------------------------------------------- 1 | type: page 2 | title: "关于作者" 3 | author: me 4 | 5 | --- 6 | 7 | ## 纸小墨 8 | 9 | 构建只为纯粹书写的博客。 10 | 11 | [https://github.com/InkProject/ink](https://github.com/InkProject/ink) -------------------------------------------------------------------------------- /template/theme/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer')({ 4 | overrideBrowserslist: ['last 5 version', '>1%', 'ios 7'] 5 | }), 6 | require('precss'), 7 | require("postcss-import") 8 | ] 9 | } -------------------------------------------------------------------------------- /template/theme/source/css/color.css: -------------------------------------------------------------------------------- 1 | $black: #293846; 2 | 3 | $grey: #7f8c8d; 4 | $greyLight: #a1b2b4; 5 | $greyLighter: #ddd; 6 | 7 | $whiteLighter: #fcfcfc; 8 | $whiteLight: #f7f7f7; 9 | $white: #eee; 10 | $whiteDark: #ddd; 11 | 12 | $greenLighter: #94ffd7; 13 | $green: #009a61; 14 | $greenDark: #004e31; 15 | -------------------------------------------------------------------------------- /template/theme/_footer.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /template/theme/_search.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    5 | -------------------------------------------------------------------------------- /template/theme/_comment.html: -------------------------------------------------------------------------------- 1 | {{if .Site.Comment}} 2 |
    3 | 10 | {{end}} 11 | -------------------------------------------------------------------------------- /template/theme/_header.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | {{if .Site.Logo}}{{end}} 4 | {{.Site.Title}} 5 | 6 | 11 |
    12 | -------------------------------------------------------------------------------- /docker-ink-runtime/Dockerfile: -------------------------------------------------------------------------------- 1 | # 本 Dockerfile 用来构建 ink-runtime 为基础镜像,以便使用. 2 | 3 | # 基于 4 | # 生成 ink 的可执行文件 5 | FROM golang:alpine AS ink-build 6 | RUN apk add --update git 7 | RUN go get -u -v github.com/taadis/ink 8 | WORKDIR /go/src/github.com/taadis/ink/ 9 | RUN go install -v 10 | WORKDIR /go/bin/ 11 | RUN ls -l 12 | 13 | # 基于 14 | # 生成最小化的 ink 基础镜像. 15 | # 使用过程中只需要基于镜像 taadis/ink-runtime 即可. 16 | FROM alpine:latest AS ink-runtime 17 | WORKDIR /ink/ 18 | COPY --from=ink-build /go/bin/ink ./ 19 | COPY --from=ink-build /go/src/github.com/taadis/ink/template/ ./template/ 20 | RUN ls -l 21 | EXPOSE 8000 22 | CMD ["./ink","serve","template"] 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /template/theme/bundle/index.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Sizzle CSS Selector Engine v2.3.9 3 | * https://sizzlejs.com/ 4 | * 5 | * Copyright JS Foundation and other contributors 6 | * Released under the MIT license 7 | * https://js.foundation/ 8 | * 9 | * Date: 2022-12-19 10 | */ 11 | 12 | /*! 13 | * jQuery JavaScript Library v3.6.3 14 | * https://jquery.com/ 15 | * 16 | * Includes Sizzle.js 17 | * https://sizzlejs.com/ 18 | * 19 | * Copyright OpenJS Foundation and other contributors 20 | * Released under the MIT license 21 | * https://jquery.org/license 22 | * 23 | * Date: 2022-12-20T21:28Z 24 | */ 25 | 26 | /*! highlight.js v8.9.1 | BSD3 License | git.io/hljslicense */ 27 | -------------------------------------------------------------------------------- /funcs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type FuncContext struct { 10 | rootPath string 11 | themePath string 12 | publicPath string 13 | currentCwd string 14 | global *GlobalConfig 15 | } 16 | 17 | func (ctx FuncContext) FuncMap() template.FuncMap { 18 | return template.FuncMap{ 19 | "i18n": ctx.I18n, 20 | "readFile": ctx.ReadFile, 21 | } 22 | } 23 | 24 | func (ctx FuncContext) I18n(val string) string { 25 | return ctx.global.I18n[val] 26 | } 27 | 28 | func (ctx FuncContext) ReadFile(path string) template.HTML { 29 | bytes, _ := os.ReadFile(filepath.Join(ctx.currentCwd, path)) 30 | return template.HTML(bytes) 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/InkProject/ink 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/InkProject/ink.go v0.0.0-20160120061933-86de6d066e8d 9 | github.com/edwardrf/symwalk v0.1.0 10 | github.com/fsnotify/fsnotify v1.9.0 11 | github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b 12 | github.com/gorilla/feeds v1.2.0 13 | github.com/gorilla/websocket v1.5.3 14 | github.com/snabb/sitemap v1.0.4 15 | github.com/urfave/cli/v2 v2.27.7 16 | gopkg.in/yaml.v2 v2.4.0 17 | ) 18 | 19 | require ( 20 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 21 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 22 | github.com/snabb/diagio v1.0.4 // indirect 23 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 24 | golang.org/x/sys v0.33.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # build binary releases for ink 4 | 5 | build () { 6 | echo "building for $1 $2..." 7 | suffix="" 8 | if [ $1 = "windows" ] 9 | then 10 | suffix=".exe" 11 | fi 12 | GOOS=$1 GOARCH=$2 go build -o release/ink$suffix 13 | cd release 14 | if [ $1 = "linux" ] 15 | then 16 | tar cvf - blog/* ink$suffix | gzip -9 - > ink_$1_$2.tar.gz 17 | else 18 | 7z a -tzip -r ink_$1_$2.zip blog ink$suffix 19 | fi 20 | rm -rf ink$suffix 21 | cd .. 22 | } 23 | 24 | rm -rf release 25 | mkdir -p release 26 | 27 | rsync -av template/* release/blog --delete --exclude public --exclude theme/node_modules 28 | 29 | build linux 386 30 | build linux amd64 31 | build linux arm 32 | build linux arm64 33 | 34 | build darwin amd64 35 | build darwin arm64 36 | 37 | build windows 386 38 | build windows amd64 39 | build windows arm 40 | build windows arm64 41 | 42 | rm -rf release/blog 43 | -------------------------------------------------------------------------------- /template/theme/source/css/global.css: -------------------------------------------------------------------------------- 1 | @define-mixin font-main { 2 | font-family: "Helvetica Neue", Arial, "Hiragino Sans GB", "WenQuanYi Micro Hei", "Microsoft YaHei", sans-serif; 3 | } 4 | 5 | @define-mixin font-code { 6 | font-family: consolas, monaco, "Source Code Pro", hack, monospace; 7 | } 8 | 9 | html, body, input { 10 | margin: 0; 11 | @mixin font-main; 12 | } 13 | 14 | html, body { 15 | height: 100%; 16 | } 17 | 18 | ul { 19 | list-style: none; 20 | margin: 0; 21 | padding: 0; 22 | } 23 | 24 | a { 25 | text-decoration: none; 26 | &:hover { 27 | opacity: 0.9; 28 | } 29 | } 30 | 31 | *::selection { 32 | background-color: $black; 33 | color: $whiteLighter; 34 | } 35 | 36 | /*clearfix*/ 37 | .clearfix:before, .clearfix:after { 38 | content: " "; 39 | display: table; 40 | } 41 | 42 | .clearfix:after { 43 | clear: both; 44 | } 45 | 46 | .clearfix { 47 | zoom: 1; 48 | } 49 | -------------------------------------------------------------------------------- /template/config.yml: -------------------------------------------------------------------------------- 1 | site: 2 | title: "纸小墨官方博客" 3 | subtitle: "构建只为纯粹书写的博客" 4 | limit: 10 5 | theme: theme 6 | lang: zh-cn 7 | url: "https://example.io/" 8 | comment: username 9 | logo: "-/images/avatar.png" 10 | config: 11 | CustomVar: "config 下是纸小墨的自定义变量,定义时建议使用正确大小写" 12 | # link: "{category}/{year}/{month}/{day}/{title}.html" 13 | # link: "{year}{month}{day}{hour}{minute}{second}.html" 14 | # root: "/blog" 15 | 16 | authors: 17 | me: 18 | name: "纸小墨" 19 | intro: "构建只为纯粹书写的博客" 20 | avatar: "-/images/avatar.png" 21 | 22 | build: 23 | # output: "public" 24 | port: 8000 25 | # These files are copied to the public folder when 'ink build' is used 26 | copy: 27 | - "source/images" 28 | # Executed commands when 'ink publish' is used 29 | publish: | 30 | git add . -A 31 | git commit -m "update" 32 | git push origin 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /template/theme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "InkPaperDefaultTheme", 3 | "version": "0.1.1", 4 | "scripts": { 5 | "start": "./node_modules/.bin/webpack --watch --mode=development", 6 | "build": "./node_modules/.bin/webpack" 7 | }, 8 | "description": "InkPaper Default Theme", 9 | "devDependencies": { 10 | "autoprefixer": "^10.4.2", 11 | "css-loader": "^6.6.0", 12 | "css-minimizer-webpack-plugin": "^4.2.2", 13 | "file-loader": "^6.2.0", 14 | "mini-css-extract-plugin": "^2.6.0", 15 | "postcss": "^8.4.7", 16 | "postcss-import": "^15.1.0", 17 | "postcss-loader": "^7.0.2", 18 | "precss": "^4.0.0", 19 | "raw-loader": "^4.0.2", 20 | "source-map-loader": "^4.0.1", 21 | "style-loader": "^3.3.1", 22 | "uglifyjs-webpack-plugin": "^2.2.0", 23 | "url-loader": "^4.1.1", 24 | "webpack": "^5.69.1", 25 | "webpack-cli": "^5.0.1" 26 | }, 27 | "dependencies": { 28 | "highlight.js": "^11.7.0", 29 | "jquery": "^3.6.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /template/theme/source/css/code.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original style from softwaremaniacs.org (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #f0f0f0; 12 | } 13 | 14 | .hljs, 15 | .hljs-subst, 16 | .hljs-tag .hljs-title { 17 | color: black; 18 | } 19 | 20 | .hljs-string, 21 | .hljs-title, 22 | .hljs-section, 23 | .hljs-attribute, 24 | .hljs-selector-id, 25 | .hljs-selector-class, 26 | .hljs-variable, 27 | .hljs-template-variable, 28 | .hljs-symbol, 29 | .hljs-deletion { 30 | color: #800; 31 | } 32 | 33 | .hljs-comment, 34 | .hljs-quote { 35 | color: #888; 36 | } 37 | 38 | .hljs-number, 39 | .hljs-regexp, 40 | .hljs-literal, 41 | .hljs-addition, 42 | .hljs-bullet, 43 | .hljs-link { 44 | color: #080; 45 | } 46 | 47 | .hljs-meta { 48 | color: #88f; 49 | } 50 | 51 | .hljs-keyword, 52 | .hljs-selector-tag, 53 | .hljs-title, 54 | .hljs-name, 55 | .hljs-section, 56 | .hljs-built_in, 57 | .hljs-doctag, 58 | .hljs-type, 59 | .hljs-strong { 60 | font-weight: bold; 61 | } 62 | 63 | .hljs-emphasis { 64 | font-style: italic; 65 | } 66 | -------------------------------------------------------------------------------- /template/theme/webpack.config.js: -------------------------------------------------------------------------------- 1 | var MiniCssExtractPlugin = require("mini-css-extract-plugin") 2 | var UglifyJsPlugin = require('uglifyjs-webpack-plugin') 3 | var CssMinimizerPlugin = require('css-minimizer-webpack-plugin') 4 | 5 | module.exports = { 6 | entry: './source/js/index.js', 7 | output: { 8 | path: __dirname + '/bundle/', 9 | filename: 'index.js' 10 | }, 11 | module: { 12 | rules: [{ 13 | test: /\.css$/i, use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"], 14 | }, { test: /\.woff|\.woff2|\.svg|.eot|\.ttf/, use: ['url-loader?limit=8192'] }, { 15 | test: /\.html$/i, use: [{ loader: 'raw-loader' },], 16 | },] 17 | }, 18 | plugins: [ 19 | new MiniCssExtractPlugin({ 20 | filename: "index.css", 21 | }) 22 | ], 23 | optimization: { 24 | minimizer: [new UglifyJsPlugin({ 25 | parallel: true, 26 | uglifyOptions: { 27 | output: { 28 | comments: false, 29 | }, 30 | }, 31 | }), new CssMinimizerPlugin({ 32 | parallel: true, 33 | minimizerOptions: { 34 | preset: [ 35 | 'default', 36 | { 37 | discardComments: { removeAll: true }, 38 | } 39 | ], 40 | }, 41 | }),], 42 | }, 43 | watch: false 44 | } 45 | -------------------------------------------------------------------------------- /template/theme/tag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{template "head" .}} 5 | 6 | 7 | {{.Site.Title}} 8 | 9 | 10 |
    11 | {{template "header" .}} 12 |
    13 |
    14 |

    {{.Site.Title}}

    15 |

    {{.Site.Subtitle}}

    16 |
    17 |
    18 | {{i18n "tag"}} - 19 | {{.Total}} {{i18n "articles"}} 20 |
    21 | 28 |
    29 |
    30 | {{template "footer" .}} 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build artifacts & Release 2 | 3 | on: 4 | schedule: 5 | - cron: "0 4 * * *" 6 | workflow_dispatch: 7 | inputs: 8 | tag_name: 9 | description: "Tag name for release" 10 | required: false 11 | default: nightly 12 | push: 13 | tags: 14 | - v[0-9]+.[0-9]+.[0-9]+* 15 | 16 | jobs: 17 | release: 18 | permissions: 19 | contents: write 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | - name: Setup Go 25 | uses: actions/setup-go@v3 26 | with: 27 | go-version: "stable" 28 | - name: Build 29 | run: sh ./release.sh 30 | - if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag_name == 'nightly') 31 | name: Release Nightly 32 | uses: softprops/action-gh-release@v1 33 | with: 34 | files: release/ink* 35 | prerelease: true 36 | tag_name: nightly 37 | name: Nightly build 38 | generate_release_notes: true 39 | fail_on_unmatched_files: true 40 | - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 41 | name: Release 42 | uses: softprops/action-gh-release@v1 43 | with: 44 | files: release/ink* 45 | prerelease: false 46 | name: Release ${{ github.ref }} 47 | generate_release_notes: true 48 | fail_on_unmatched_files: true 49 | -------------------------------------------------------------------------------- /template/theme/source/js/jquery.unveil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Unveil 3 | * A very lightweight jQuery plugin to lazy load images 4 | * http://luis-almeida.github.com/unveil 5 | * 6 | * Licensed under the MIT license. 7 | * Copyright 2013 Luís Almeida 8 | * https://github.com/luis-almeida 9 | * 10 | * 11 | * Modified by @w568w to work with the lastest jQuery. 12 | */ 13 | import $ from 'jquery' 14 | 15 | 16 | (function () { 17 | $.fn.unveil = function (threshold, callback) { 18 | 19 | let $w = $(window), 20 | th = threshold || 0, 21 | retina = window.devicePixelRatio > 1, 22 | attrib = retina ? "data-src-retina" : "data-src", 23 | images = this, 24 | loaded; 25 | 26 | this.one("unveil", function () { 27 | let source = this.getAttribute(attrib); 28 | source = source || this.getAttribute("data-src"); 29 | if (source) { 30 | this.setAttribute("src", source); 31 | if (typeof callback === "function") callback.call(this); 32 | } 33 | }); 34 | 35 | function unveil() { 36 | const inview = images.filter(function () { 37 | let $e = $(this); 38 | if ($e.is(":hidden")) return; 39 | 40 | let wt = $w.scrollTop(), 41 | wb = wt + $w.height(), 42 | et = $e.offset().top, 43 | eb = et + $e.height(); 44 | 45 | return eb >= wt - th && et <= wb + th; 46 | }); 47 | 48 | loaded = inview.trigger("unveil"); 49 | images = images.not(loaded); 50 | } 51 | 52 | $w.on("scroll.unveil resize.unveil lookup.unveil", unveil); 53 | 54 | unveil(); 55 | 56 | return this; 57 | 58 | }; 59 | })(); -------------------------------------------------------------------------------- /template/theme/source/css/archive-tag.css: -------------------------------------------------------------------------------- 1 | .main { 2 | &.archive, &.tag { 3 | padding-top: 100px; 4 | padding-bottom: 100px; 5 | a { 6 | color: $green; 7 | text-decoration: none; 8 | border-bottom: 1px dashed $green; 9 | } 10 | .site { 11 | .title, .subtitle { 12 | text-align: left; 13 | @mixin font-main; 14 | } 15 | } 16 | .header { 17 | padding-top: 60px; 18 | font-size: 18px; 19 | .title { 20 | margin-top: 30px; 21 | margin-right: 5px; 22 | font-weight: bold; 23 | } 24 | .subtitle { 25 | font-style: italic; 26 | } 27 | } 28 | .archive-list { 29 | margin-top: 50px; 30 | .archive-item { 31 | margin: 40px 0; 32 | .archive-year { 33 | margin: 15px 0; 34 | color: $black; 35 | font-weight: bold; 36 | } 37 | .article-list { 38 | .article-item { 39 | margin-bottom: 10px; 40 | .date { 41 | color: $grey; 42 | margin-right: 20px; 43 | font-style: italic; 44 | } 45 | } 46 | } 47 | } 48 | } 49 | .tag-list { 50 | margin-top: 50px; 51 | .tag-item { 52 | margin-right: 10px; 53 | margin-bottom: 25px; 54 | float: left; 55 | .tag-name { 56 | padding: 5px 7px; 57 | border: 1px solid $white; 58 | border-radius: 3px; 59 | font-size: 14px; 60 | &:hover { 61 | background-color: $black; 62 | color: $whiteLighter; 63 | border: 1px solid $black; 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /template/theme/archive.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{template "head" .}} 5 | 6 | 7 | 8 | {{.Site.Title}} 9 | 10 | 11 |
    12 | {{template "header" .}} 13 |
    14 |
    15 |

    {{.Site.Title}}

    16 |

    {{.Site.Subtitle}}

    17 |
    18 |
    19 | {{i18n "archive"}} - 20 | {{.Total}} {{i18n "articles"}} 21 |
    22 |
      23 | {{range .Archive}} 24 |
    • 25 |
      {{.Year}}
      26 |
        27 | {{range .Articles}} 28 |
      • 29 | {{.Date}} 30 | {{.Title}} 31 |
      • 32 | {{end}} 33 |
      34 |
    • 35 | {{end}} 36 |
    37 |
    38 |
    39 | {{template "footer" .}} 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /template/theme/bundle/searchWorker.js: -------------------------------------------------------------------------------- 1 | function load(url, callback) { 2 | var xhr 3 | if (typeof XMLHttpRequest !== 'undefined') xhr = new XMLHttpRequest() 4 | else { 5 | var versions = ["MSXML2.XmlHttp.5.0", 6 | "MSXML2.XmlHttp.4.0", 7 | "MSXML2.XmlHttp.3.0", 8 | "MSXML2.XmlHttp.2.0", 9 | "Microsoft.XmlHttp"] 10 | 11 | for(var i = 0, len = versions.length; i < len; i++) { 12 | try { 13 | xhr = new ActiveXObject(versions[i]) 14 | break 15 | } catch(e) {} 16 | } 17 | } 18 | xhr.onreadystatechange = function() { 19 | if(xhr.readyState < 4) return 20 | if(xhr.status !== 200) return 21 | if(xhr.readyState === 4) callback(xhr) 22 | } 23 | xhr.open('GET', url, true) 24 | xhr.send('') 25 | } 26 | 27 | var data, lastKeyword 28 | 29 | var search = function(keyword) { 30 | var results = [] 31 | var keywordStr = keyword.toLowerCase().replace(/ /g, ' ') 32 | var keywords = keywordStr.split(' ') 33 | for (var i = 0; i < data.length; i++) { 34 | var item = data[i] 35 | var title = item.title.toLowerCase() 36 | var preview = item.preview.toLowerCase() 37 | var content = item.content.toLowerCase() 38 | var isMatch = true 39 | for (var j = 0; j < keywords.length; j++) { 40 | var key = keywords[j] 41 | if (title.indexOf(key) == -1 && 42 | preview.indexOf(key) == -1 && 43 | content.indexOf(key) == -1) { 44 | isMatch = false 45 | break 46 | } 47 | } 48 | if (isMatch) results.push(item) 49 | } 50 | postMessage({ 51 | keyword: keywordStr, 52 | keywords: keywords, 53 | results: results 54 | }) 55 | } 56 | 57 | 58 | onmessage = function(event) { 59 | var action = event.data.action 60 | if (action == 'start') { 61 | load(event.data.root + '/index.json', function(xhr) { 62 | data = JSON.parse(xhr.responseText) 63 | if (lastKeyword) { 64 | search(lastKeyword) 65 | } 66 | }) 67 | } else { 68 | if (data) { 69 | search(event.data.keyword) 70 | } else { 71 | lastKeyword = event.data.keyword 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /template/theme/source/css/index.css: -------------------------------------------------------------------------------- 1 | @import "color.css"; 2 | @import "code.css"; 3 | @import "global.css"; 4 | 5 | @import "page.css"; 6 | @import "article.css"; 7 | @import "archive-tag.css"; 8 | @import "content.css"; 9 | 10 | .header-wrap { 11 | padding: 15px; 12 | position: absolute; 13 | z-index: 1; 14 | font-weight: bold; 15 | font-size: 18px; 16 | left: 0; 17 | right: 0; 18 | display: flex; 19 | justify-content: space-between; 20 | align-items: center; 21 | a { 22 | color: $greyLight; 23 | } 24 | .index { 25 | display: flex; 26 | align-items: center; 27 | .logo { 28 | width: 30px; 29 | height: 30px; 30 | border-radius: 50%; 31 | border: 1px solid $greyLighter; 32 | margin-right: 10px; 33 | } 34 | } 35 | .menu { 36 | display: flex; 37 | .menu-item { 38 | margin-right: 10px; 39 | } 40 | } 41 | } 42 | a.name { 43 | color: $black; 44 | } 45 | .main { 46 | color: $black; 47 | margin: 0 auto; 48 | max-width: 720px; 49 | padding: 0 15px; 50 | min-height: 100%; 51 | position:relative; 52 | .site { 53 | .logo { 54 | margin: 0 auto; 55 | width: 80px; 56 | height: 80px; 57 | border-radius: 50%; 58 | background-size: cover; 59 | background-repeat: no-repeat; 60 | border: 1px $greyLighter solid; 61 | } 62 | .title { 63 | margin: 0; 64 | margin-top: 30px; 65 | font-weight: bold; 66 | font-size: 32px; 67 | text-align: center; 68 | } 69 | .subtitle { 70 | @mixin font-main; 71 | font-style: italic; 72 | margin-top: 10px; 73 | font-size: 16px; 74 | color: $greyLight; 75 | text-align: center; 76 | font-weight: normal; 77 | } 78 | } 79 | &.about { 80 | .info { 81 | margin-top: 80px; 82 | text-align: center; 83 | } 84 | } 85 | } 86 | .container { 87 | height: auto; 88 | min-height: 100%; 89 | padding-bottom: 0; 90 | } 91 | .footer { 92 | width: 100%; 93 | font-size: 14px; 94 | border-top: 1px $white solid; 95 | color: $greyLight; 96 | background-color: $whiteLighter; 97 | clear: both; 98 | position: relative; 99 | height: 32px; 100 | margin-top: -36px; 101 | span { 102 | padding: 8px 15px; 103 | display: block; 104 | a { 105 | font-weight: bold; 106 | } 107 | } 108 | .copyright { 109 | float: left; 110 | } 111 | .publish { 112 | float: right; 113 | a { 114 | color: $greyLight; 115 | text-decoration: none; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /template/theme/source/css/article.css: -------------------------------------------------------------------------------- 1 | .main { 2 | &.article { 3 | padding-top: 120px; 4 | padding-bottom: 120px; 5 | .title { 6 | margin: 0; 7 | font-weight: bold; 8 | color: $black; 9 | font-size: 30px; 10 | line-height: 42px; 11 | } 12 | .info { 13 | margin-top: 10px; 14 | font-size: 14px; 15 | .avatar { 16 | width: 20px; 17 | height: 20px; 18 | margin-right: 10px; 19 | border-radius: 20px; 20 | background-size: cover; 21 | background-repeat: no-repeat; 22 | float: left; 23 | margin-top: -4px; 24 | border: 1px $greyLighter solid; 25 | } 26 | .name { 27 | margin-right: 5px; 28 | } 29 | .date { 30 | color: $grey; 31 | margin-right: 5px; 32 | } 33 | .tags { 34 | @mixin font-main; 35 | .tag { 36 | margin-right: 5px; 37 | color: $grey; 38 | text-decoration: none; 39 | } 40 | } 41 | } 42 | .recommend { 43 | margin-top: 50px; 44 | overflow: auto; 45 | .nav { 46 | width: 100%; 47 | .head { 48 | font-size: 12px; 49 | font-style: italic; 50 | margin-bottom: 10px; 51 | color: $grey; 52 | text-align: center; 53 | } 54 | .link { 55 | color: $black; 56 | text-align: center; 57 | display: block; 58 | } 59 | &.prev.more { 60 | width: 45%; 61 | text-align: left; 62 | float: left; 63 | .head, .link { 64 | text-align: left; 65 | } 66 | } 67 | &.next.more { 68 | width: 45%; 69 | text-align: right; 70 | float: right; 71 | .head, .link { 72 | text-align: right; 73 | } 74 | } 75 | } 76 | } 77 | .author { 78 | margin-top: 36px; 79 | padding-top: 40px; 80 | text-align: center; 81 | .avatar { 82 | margin: 0 auto; 83 | width: 70px; 84 | height: 70px; 85 | border-radius: 70px; 86 | background-size: cover; 87 | background-repeat: no-repeat; 88 | border: 20px white solid; 89 | &:before { 90 | border-top: 1px $greyLighter dashed; 91 | content: ""; 92 | width: 100%; 93 | position: absolute; 94 | left: 0; 95 | right: 0; 96 | margin-top: 36px; 97 | z-index: -1; 98 | } 99 | } 100 | .name { 101 | margin-top: 20px; 102 | font-weight: bold; 103 | font-size: 16px; 104 | } 105 | .intro { 106 | @mixin font-main; 107 | font-style: italic; 108 | margin-top: 8px; 109 | font-size: 14px; 110 | color: $grey; 111 | } 112 | } 113 | #disqus_thread { 114 | margin-top: 50px; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /template/theme/_head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{ $imgurl := printf "%s%s" .Site.Url .Site.Logo}} 11 | {{ if .Image }} 12 | {{ $imgurl = .Image }} 13 | {{ end }} 14 | 15 | {{ $subtitle := .Site.Subtitle }} 16 | {{ if .Subtitle }} 17 | {{ $subtitle = .Subtitle }} 18 | {{ end }} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 54 | 55 | 56 | 57 | 58 | {{if .Develop}} 59 | 80 | {{end}} 81 | -------------------------------------------------------------------------------- /template/theme/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{template "head" .}} 5 | 6 | 7 | {{.Site.Title}} 8 | 9 | 10 |
    11 | {{template "header" .}} 12 |
    13 |
    14 | {{if .TagName}} 15 | {{if .Site.Logo}}{{end}} 16 |

    {{.TagName}}

    17 |

    {{.TagCount}} {{i18n "articles"}}

    18 | {{else}} 19 | {{if .Site.Logo}}{{end}} 20 |

    {{.Site.Title}}

    21 |

    {{.Site.Subtitle}}

    22 | {{template "search" .}} 23 | {{end}} 24 |
    25 |
      26 | {{range .Articles}} 27 |
    • 28 | {{if .Top}}[{{i18n "top"}}]{{end}}{{.Title}} 29 | {{if .Cover}}
      {{end}} 30 | {{if .Preview}}
      {{.Preview}}
      {{end}} 31 |
      32 | {{if .Author.Avatar}}{{end}} 33 | {{if .Author.Name}}{{.Author.Name}}{{end}} 34 | {{if .Update}} 35 | 36 | {{else}} 37 | 38 | {{end}} 39 | 40 | {{range .Tags}}{{.}}{{end}} 41 | 42 |
      43 |
    • 44 | {{end}} 45 |
    46 | 51 |
    52 |
    53 | {{template "footer" .}} 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/InkProject/ink.go v0.0.0-20160120061933-86de6d066e8d h1:cKKHWaSZqckOTguui9bShV83raKxQKpTB9X4M7WbeaY= 2 | github.com/InkProject/ink.go v0.0.0-20160120061933-86de6d066e8d/go.mod h1:sGm8pED0mDi7pXIgjvCf7/m7LMmLSWpz3bhtB8KoKL8= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 5 | github.com/edwardrf/symwalk v0.1.0 h1:NAakDFfWD1ef65UDiYREFvWiu1+RqRC2dJIDXJdpFjM= 6 | github.com/edwardrf/symwalk v0.1.0/go.mod h1:qlC1zULcOcrPbB0Mog6yENGnszBOdCM59+rLC6g607Y= 7 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 8 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 9 | github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= 10 | github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 11 | github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk= 12 | github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= 13 | github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc= 14 | github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= 15 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 16 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 17 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 18 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 19 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 20 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 21 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 22 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 23 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 24 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 25 | github.com/snabb/diagio v1.0.4 h1:XnlKoBarZWiAEnNBYE5t1nbvJhdaoTaW7IBzu0R4AqM= 26 | github.com/snabb/diagio v1.0.4/go.mod h1:Y+Pja4UJrskCOKaLxOfa8b8wYSVb0JWpR4YFNHuzjDI= 27 | github.com/snabb/sitemap v1.0.4 h1:BC6cPW5jXLsKWtlYQKD2s1W58CarvNzqOmdl680uQPw= 28 | github.com/snabb/sitemap v1.0.4/go.mod h1:815/fxQQ8Tt7Eqwe8Lcat4ax73zuHyPxWBZySnbaxkc= 29 | github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= 30 | github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= 31 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 32 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 33 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 34 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 37 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 38 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 39 | -------------------------------------------------------------------------------- /template/theme/source/css/content.css: -------------------------------------------------------------------------------- 1 | .main { 2 | .content { 3 | @mixin font-main; 4 | margin-top: 70px; 5 | font-size: 16px; 6 | line-height: 1.7; 7 | color: $black; 8 | h1, h2, h3, h4, h5, h6 { 9 | @mixin font-main; 10 | & + p { 11 | margin-top: 10px; 12 | } 13 | } 14 | h1 { 15 | font-size: 28px; 16 | margin-top: 40px; 17 | margin-bottom: 10px; 18 | } 19 | h2 { 20 | font-size: 24px; 21 | margin-top: 40px; 22 | margin-bottom: 10px; 23 | } 24 | h3 { 25 | font-size: 18px; 26 | margin-top: 40px; 27 | margin-bottom: 10px; 28 | } 29 | p { 30 | margin-bottom: 30px; 31 | margin-top: 30px; 32 | text-align: left; 33 | } 34 | &.preview { 35 | p { 36 | margin-bottom: 10px; 37 | margin-top: 10px; 38 | } 39 | } 40 | a { 41 | color: $green; 42 | text-decoration: none; 43 | border-bottom: 1px dashed $green; 44 | &:hover { 45 | color: $greenDark; 46 | border-bottom: 1px dashed $greenDark; 47 | } 48 | } 49 | code { 50 | @mixin font-code; 51 | font-size: 14px; 52 | padding: 1px 4px; 53 | border-radius: 3px; 54 | margin: 0px 3px; 55 | background-color: $whiteLight; 56 | color: $green; 57 | } 58 | pre { 59 | margin: 0; 60 | code { 61 | @mixin font-code; 62 | color: inherit; 63 | font-size: 14px; 64 | margin: 0; 65 | padding: 10px 15px; 66 | border-radius: 6px; 67 | border: 2px dashed $white; 68 | background-color: $whiteLighter; 69 | display: block; 70 | overflow: auto; 71 | } 72 | } 73 | blockquote { 74 | border-left: 4px $green solid; 75 | padding: 0px 10px 0px 20px; 76 | margin: 25px 0; 77 | margin-left: -23px; 78 | font-style: italic; 79 | } 80 | table { 81 | font-size: 14px; 82 | width: 100%; 83 | border-width: 1px; 84 | border-color: $whiteDark; 85 | border-collapse: collapse; 86 | th { 87 | border-width: 1px; 88 | padding: 5px; 89 | border-style: solid; 90 | border-color: $whiteDark; 91 | background-color: $white; 92 | } 93 | td { 94 | border-width: 1px; 95 | padding: 5px; 96 | border-style: solid; 97 | border-color: $whiteDark; 98 | background-color: $whiteLighter; 99 | } 100 | } 101 | ul { 102 | list-style: circle; 103 | padding-left: 40px; 104 | li { 105 | margin: 5px 0; 106 | } 107 | } 108 | ol { 109 | padding-left: 40px; 110 | li { 111 | margin: 5px 0; 112 | } 113 | } 114 | hr { 115 | margin: 25px 0; 116 | border: 0; 117 | border-top: 1px dashed $whiteDark; 118 | } 119 | img { 120 | margin: 30px auto; 121 | max-width: 100%; 122 | display: block; 123 | opacity: 0.6; 124 | transition: opacity .3s ease-in; 125 | cursor: pointer; 126 | border-radius: 5px; 127 | } 128 | .image-alt { 129 | text-align: center; 130 | color: $grey; 131 | font-style: italic; 132 | margin-top: -10px; 133 | margin-bottom: 30px; 134 | } 135 | .searched { 136 | background-color: yellow; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "runtime" 8 | "time" 9 | ) 10 | 11 | const ( 12 | CLR_W = "" 13 | CLR_R = "\x1b[31;1m" 14 | CLR_G = "\x1b[32;1m" 15 | CLR_B = "\x1b[34;1m" 16 | CLR_Y = "\x1b[33;1m" 17 | ) 18 | 19 | const ( 20 | DATE_FORMAT = "2006-01-02 15:04:05" 21 | DATE_FORMAT_WITH_TIMEZONE = "2006-01-02 15:04:05 -0700" 22 | ) 23 | 24 | var exitCode int 25 | 26 | // Print log 27 | func Log(info interface{}) { 28 | fmt.Printf("%s\n", info) 29 | } 30 | 31 | // Print warning log 32 | func Warn(info interface{}) { 33 | if runtime.GOOS == "windows" { 34 | fmt.Printf("WARNING: %s\n", info) 35 | } else { 36 | fmt.Printf("%s%s\n%s", CLR_Y, info, "\x1b[0m") 37 | } 38 | } 39 | 40 | // Print error log 41 | func Error(info interface{}) { 42 | if runtime.GOOS == "windows" { 43 | fmt.Printf("ERR: %s\n", info) 44 | } else { 45 | fmt.Printf("%s%s\n%s", CLR_R, info, "\x1b[0m") 46 | } 47 | exitCode = 1 48 | } 49 | 50 | // Print error log and exit 51 | func Fatal(info interface{}) { 52 | Error(info) 53 | os.Exit(1) 54 | } 55 | 56 | // Parse date by std date string 57 | func ParseDate(dateStr string) time.Time { 58 | date, err := time.Parse(fmt.Sprint(DATE_FORMAT_WITH_TIMEZONE), dateStr) 59 | if err != nil { 60 | date, err = time.ParseInLocation(fmt.Sprint(DATE_FORMAT), dateStr, time.Now().Location()) 61 | if err != nil { 62 | Fatal(err.Error()) 63 | } 64 | } 65 | return date 66 | } 67 | 68 | // Check file if exist 69 | func Exists(path string) bool { 70 | _, err := os.Stat(path) 71 | if err == nil { 72 | return true 73 | } 74 | if os.IsNotExist(err) { 75 | return false 76 | } 77 | return false 78 | } 79 | 80 | // Check file if is directory 81 | func IsDir(path string) bool { 82 | file, err := os.Stat(path) 83 | if err != nil { 84 | return false 85 | } 86 | return file.IsDir() 87 | } 88 | 89 | // Copy folder and file 90 | // Refer to https://www.socketloop.com/tutorials/golang-copy-directory-including-sub-directories-files 91 | func CopyFile(source string, dest string) { 92 | sourcefile, err := os.Open(source) 93 | if err != nil { 94 | Fatal(err.Error()) 95 | } 96 | destfile, err := os.Create(dest) 97 | if err != nil { 98 | Fatal(err.Error()) 99 | } 100 | defer destfile.Close() 101 | defer wg.Done() 102 | _, err = io.Copy(destfile, sourcefile) 103 | if err != nil { 104 | Fatal(err.Error()) 105 | } 106 | sourceinfo, err := os.Stat(source) 107 | if err != nil { 108 | Fatal(err.Error()) 109 | } 110 | err = os.Chmod(dest, sourceinfo.Mode()) 111 | if err != nil { 112 | Fatal(err.Error()) 113 | } 114 | sourcefile.Close() 115 | } 116 | 117 | func CopyDir(source string, dest string) { 118 | sourceinfo, err := os.Stat(source) 119 | if err != nil { 120 | Fatal(err.Error()) 121 | } 122 | err = os.MkdirAll(dest, sourceinfo.Mode()) 123 | if err != nil { 124 | Fatal(err.Error()) 125 | } 126 | directory, _ := os.Open(source) 127 | defer directory.Close() 128 | defer wg.Done() 129 | objects, err := directory.Readdir(-1) 130 | if err != nil { 131 | Fatal(err.Error()) 132 | } 133 | for _, obj := range objects { 134 | sourcefilepointer := source + "/" + obj.Name() 135 | destinationfilepointer := dest + "/" + obj.Name() 136 | if obj.IsDir() { 137 | wg.Add(1) 138 | CopyDir(sourcefilepointer, destinationfilepointer) 139 | } else { 140 | wg.Add(1) 141 | go CopyFile(sourcefilepointer, destinationfilepointer) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /template/theme/article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{template "head" .}} 5 | 6 | 7 | 8 | {{.Title}} 9 | 10 | 11 |
    12 | {{template "header" .}} 13 |
    14 |

    {{.Title}}

    15 |
    16 | {{if .Author.Avatar}}{{end}} 17 | {{if .Author.Name}}{{.Author.Name}}{{end}} 18 | {{if .Update}} 19 | 20 | {{else}} 21 | 22 | {{end}} 23 | {{range .Tags}}{{.}}{{end}} 24 |
    25 |
    {{.Content}}
    26 |
    27 | {{if .Author.Avatar}}
    {{end}} 28 | {{.Author.Name}} 29 |
    {{.Author.Intro}}
    30 |
    31 |
    32 | {{if .Prev}} 33 | 37 | {{end}} 38 | {{if .Next}} 39 | 43 | {{end}} 44 |
    45 | {{template "comment" .}} 46 |
    47 |
    48 | {{template "footer" .}} 49 | 50 |
    51 | 52 | 53 | 54 |
    55 | 56 | 57 | -------------------------------------------------------------------------------- /template/theme/config.yml: -------------------------------------------------------------------------------- 1 | # Copied files to public folder when build 2 | copy: 3 | - bundle 4 | - favicon.png 5 | - robots.txt 6 | # I18n Support 7 | lang: 8 | archive: 9 | en: ARCHIVE 10 | zh-cn: 归档 11 | zh-tw: 歸檔 12 | ru: Архив 13 | ja: アーカイブ 14 | de: ARCHIV 15 | pt-br: Arquivo 16 | tag: 17 | en: TAG 18 | zh-cn: 标签 19 | zh-tw: 標籤 20 | ru: Теги 21 | ja: タグ 22 | de: TAG 23 | pt-br: TAG 24 | rss: 25 | en: RSS 26 | zh-cn: 订阅 27 | zh-tw: 訂閱 28 | ru: RSS 29 | ja: RSS 30 | de: RSS 31 | pt-br: RSS 32 | articles: 33 | en: Articles 34 | zh-cn: 篇文章 35 | zh-tw: 篇文章 36 | ru: Записи 37 | ja: 記事 38 | de: Beiträge 39 | pt-br: Artigos 40 | updated: 41 | en: updated 42 | zh-cn: 更新 43 | zh-tw: 更新 44 | ru: обновлено 45 | ja: 更新 46 | de: geändert 47 | pt-br: atualizado 48 | prev_page: 49 | en: Prev Page 50 | zh-cn: ◀ 上一页 51 | zh-tw: ◀ 上一頁 52 | ru: Назад 53 | ja: 前のページ 54 | de: Seite zurück 55 | pt-br: Página Anterior 56 | next_page: 57 | en: Next Page 58 | zh-cn: 下一页 ▶ 59 | zh-tw: 下一頁 ▶ 60 | ru: Далее 61 | ja: 次のページ 62 | de: Seite vor 63 | pt-br: Próxima Página 64 | prev_reading: 65 | en: Prev Article 66 | zh-cn: ◀ 上一篇 67 | zh-tw: ◀ 上一篇 68 | ru: Предыдущая запись 69 | ja: 新しい記事 70 | de: Vorheriger Beitrag 71 | pt-br: Artigo Anterior 72 | next_reading: 73 | en: Next Article 74 | zh-cn: 下一篇 ▶ 75 | zh-tw: 下一篇 ▶ 76 | ru: Следующая запись 77 | ja: 古い記事 78 | de: Nächster Beitrag 79 | pt-br: Próximo Artigo 80 | top: 81 | en: TOP 82 | zh-cn: 置顶 83 | zh-tw: 置頂 84 | ru: Наверх 85 | ja: TOP 86 | de: Angeheftet 87 | pt-br: Acima 88 | search: 89 | en: Search 90 | zh-cn: 搜索 91 | zh-tw: 搜尋 92 | ru: Поиск 93 | ja: 検索 94 | de: Suche 95 | pt-br: Busca 96 | since_year: 97 | en: " years ago" 98 | zh-cn: 年前 99 | zh-tw: 年前 100 | ru: " лет назад" 101 | ja: 年前 102 | de: " Jahre" 103 | pt-br: " anos atrás" 104 | since_month: 105 | en: " months ago" 106 | zh-cn: 个月前 107 | zh-tw: 個月前 108 | ru: " месяцев назад" 109 | ja: ヶ月前 110 | de: " Monate" 111 | pt-br: " meses atrás" 112 | since_day: 113 | en: " days ago" 114 | zh-cn: 天前 115 | zh-tw: 天前 116 | ru: " дней назад" 117 | ja: 日前 118 | de: " Tage" 119 | pt-br: " dias atrás" 120 | since_hour: 121 | en: " hours ago" 122 | zh-cn: 小时前 123 | zh-tw: 小時前 124 | ru: " часов назад" 125 | ja: 時間前 126 | de: " Stunden" 127 | pt-br: " horas atrás" 128 | since_minute: 129 | en: " minutes ago" 130 | zh-cn: 分钟前 131 | zh-tw: 分鐘前 132 | ru: " минут назад" 133 | ja: 分前 134 | de: " Minuten" 135 | pt-br: " minutos atrás" 136 | since_second: 137 | en: " seconds ago" 138 | zh-cn: 秒前 139 | zh-tw: 秒前 140 | ru: " секунд назад" 141 | ja: 秒前 142 | de: " Sekunden" 143 | pt-br: " segundos atrás" 144 | -------------------------------------------------------------------------------- /serve.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "reflect" 7 | 8 | "github.com/InkProject/ink.go" 9 | "github.com/edwardrf/symwalk" 10 | "github.com/fsnotify/fsnotify" 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | var watcher *fsnotify.Watcher 15 | var conn *websocket.Conn 16 | 17 | func buildWatchList() (files []string, dirs []string) { 18 | dirs = []string{ 19 | filepath.Join(rootPath, "source"), 20 | } 21 | files = []string{ 22 | filepath.Join(rootPath, "config.yml"), 23 | filepath.Join(themePath), 24 | } 25 | 26 | // Add files and directories defined in theme's config.yml to watcher 27 | for _, themeCopiedPath := range themeConfig.Copy { 28 | if themeCopiedPath != "" { 29 | fullPath := filepath.Join(themePath, themeCopiedPath) 30 | s, err := os.Stat(fullPath) 31 | if s == nil || err != nil { 32 | continue 33 | } 34 | 35 | if s.IsDir() { 36 | dirs = append(dirs, fullPath) 37 | } else { 38 | files = append(files, fullPath) 39 | } 40 | } 41 | } 42 | return files, dirs 43 | } 44 | 45 | // Add files and dirs to watcher 46 | func configureWatcher(watcher *fsnotify.Watcher, files []string, dirs []string) error { 47 | for _, source := range dirs { 48 | symwalk.Walk(source, func(path string, f os.FileInfo, err error) error { 49 | if f != nil && f.IsDir() { 50 | if err := watcher.Add(path); err != nil { 51 | Warn(err.Error()) 52 | } 53 | } 54 | return nil 55 | }) 56 | } 57 | for _, source := range files { 58 | if err := watcher.Add(source); err != nil { 59 | Warn(err.Error()) 60 | } 61 | } 62 | return nil 63 | } 64 | 65 | func Watch() { 66 | // Listen watched file change event 67 | if watcher != nil { 68 | watcher.Close() 69 | } 70 | watcher, _ = fsnotify.NewWatcher() 71 | files, dirs := buildWatchList() 72 | go func() { 73 | for { 74 | select { 75 | case event := <-watcher.Events: 76 | if event.Op == fsnotify.Write { 77 | // Handle when file change 78 | Log(event.Name) 79 | ParseGlobalConfigWrap(rootPath, true) 80 | 81 | newFiles, newDirs := buildWatchList() 82 | // If file list changed, reconfigure watcher 83 | if !reflect.DeepEqual(files, newFiles) || !reflect.DeepEqual(dirs, newDirs) { 84 | configureWatcher(watcher, newFiles, newDirs) 85 | files = newFiles 86 | dirs = newDirs 87 | } 88 | 89 | Build() 90 | if conn != nil { 91 | if err := conn.WriteMessage(websocket.TextMessage, []byte("change")); err != nil { 92 | Warn(err.Error()) 93 | } 94 | } 95 | } 96 | case err := <-watcher.Errors: 97 | Warn(err.Error()) 98 | } 99 | } 100 | }() 101 | configureWatcher(watcher, files, dirs) 102 | } 103 | 104 | func Websocket(ctx *ink.Context) { 105 | var upgrader = websocket.Upgrader{ 106 | ReadBufferSize: 1024, 107 | WriteBufferSize: 1024, 108 | } 109 | if c, err := upgrader.Upgrade(ctx.Res, ctx.Req, nil); err != nil { 110 | Warn(err) 111 | } else { 112 | conn = c 113 | } 114 | ctx.Stop() 115 | } 116 | 117 | func Serve() { 118 | // editorWeb := ink.New() 119 | // 120 | // editorWeb.Get("/articles", ApiListArticle) 121 | // editorWeb.Get("/articles/:id", ApiGetArticle) 122 | // editorWeb.Post("/articles", ApiCreateArticle) 123 | // editorWeb.Put("/articles/:id", ApiSaveArticle) 124 | // editorWeb.Delete("/articles/:id", ApiRemoveArticle) 125 | // editorWeb.Get("/config", ApiGetConfig) 126 | // editorWeb.Put("/config", ApiSaveConfig) 127 | // editorWeb.Post("/upload", ApiUploadFile) 128 | // editorWeb.Use(ink.Cors) 129 | // editorWeb.Get("*", ink.Static(filepath.Join("editor/assets"))) 130 | 131 | // Log("Access http://localhost:" + globalConfig.Build.Port + "/ to open editor") 132 | // go editorWeb.Listen(":2333") 133 | 134 | previewWeb := ink.New() 135 | previewWeb.Get("/live", Websocket) 136 | previewWeb.Get("*", ink.Static(filepath.Join(rootPath, globalConfig.Build.Output))) 137 | 138 | uri := "http://localhost:" + globalConfig.Build.Port + "/" 139 | Log("Access " + uri + " to open preview") 140 | previewWeb.Listen(":" + globalConfig.Build.Port) 141 | } 142 | -------------------------------------------------------------------------------- /template/theme/source/css/page.css: -------------------------------------------------------------------------------- 1 | .main { 2 | &.page { 3 | padding-top: 100px; 4 | padding-bottom: 70px; 5 | .header { 6 | text-align: center; 7 | .tag { 8 | margin: 0; 9 | margin-top: 30px; 10 | font-weight: bold; 11 | font-size: 32px; 12 | } 13 | .tag-sub { 14 | margin: 0; 15 | font-weight: normal; 16 | margin-top: 10px; 17 | font-size: 16px; 18 | color: $grey; 19 | } 20 | .search-wrap { 21 | position: relative; 22 | max-width: 300px; 23 | margin: 0 auto; 24 | margin-top: 50px; 25 | .search { 26 | padding: 10px 25px; 27 | border: 1px solid $greyLighter; 28 | border-radius: 100px; 29 | width: 100%; 30 | box-sizing: border-box; 31 | opacity: 0.8; 32 | &:focus { 33 | outline: 0; 34 | opacity: 1; 35 | } 36 | } 37 | .icon { 38 | position: absolute; 39 | right: 10px; 40 | top: 5px; 41 | opacity: 0.3; 42 | } 43 | } 44 | } 45 | .article-list { 46 | margin-top: 100px; 47 | .searched { 48 | background-color: $green; 49 | color: $whiteLighter; 50 | } 51 | .article { 52 | margin-bottom: 55px; 53 | padding-bottom: 45px; 54 | border-bottom: 1px $greyLighter dashed; 55 | &:last-child { 56 | border-bottom: 0; 57 | } 58 | .title { 59 | font-size: 26px; 60 | font-weight: bold; 61 | margin-bottom: 5px; 62 | text-decoration: none; 63 | color: $black; 64 | display: block; 65 | .top { 66 | color: $grey; 67 | margin-right: 5px; 68 | } 69 | } 70 | .cover { 71 | color: $white; 72 | width: 100%; 73 | height: 280px; 74 | background-size: cover; 75 | background-position: center; 76 | background-repeat: no-repeat; 77 | border-radius: 6px; 78 | border: 1px solid $whiteDark; 79 | box-sizing: border-box; 80 | margin-top: 15px; 81 | @media (max-width: 414px) { 82 | height: 180px; 83 | } 84 | } 85 | .preview { 86 | @mixin font-main; 87 | display: block; 88 | line-height: 28px; 89 | font-size: 16px; 90 | margin-top: 10px; 91 | color: $black; 92 | } 93 | .info { 94 | margin-top: 15px; 95 | margin-bottom: 20px; 96 | color: $grey; 97 | font-size: 14px; 98 | } 99 | .avatar { 100 | width: 20px; 101 | height: 20px; 102 | margin-right: 10px; 103 | border-radius: 20px; 104 | background-size: cover; 105 | background-repeat: no-repeat; 106 | float: left; 107 | margin-top: -4px; 108 | border: 1px $greyLighter solid; 109 | } 110 | .name { 111 | margin-right: 10px; 112 | } 113 | .date { 114 | color: $grey; 115 | } 116 | .tags { 117 | @mixin font-main; 118 | margin-left: 10px; 119 | float: right; 120 | .tag { 121 | margin-right: 5px; 122 | color: $grey; 123 | text-decoration: none; 124 | } 125 | } 126 | } 127 | .empty { 128 | text-align: center; 129 | font-size: 24px; 130 | max-width: 800px; 131 | overflow: hidden; 132 | text-overflow: ellipsis; 133 | span { 134 | font-weight: bold; 135 | } 136 | } 137 | } 138 | .page-nav { 139 | position: relative; 140 | height: 50px; 141 | font-size: 14px; 142 | a { 143 | color: $grey; 144 | font-style: italic; 145 | text-decoration: none; 146 | } 147 | .nav { 148 | left: 0; 149 | right: 0; 150 | text-align: center; 151 | display: block; 152 | color: $grey; 153 | position: absolute; 154 | z-index: -1; 155 | } 156 | .prev { 157 | float: left; 158 | &:before { 159 | content: ""; 160 | width: 3px; 161 | height: 3px; 162 | border: 2px $grey solid; 163 | border-radius: 3px; 164 | display: block; 165 | float: left; 166 | margin-right: 7px; 167 | margin-top: 5px; 168 | } 169 | } 170 | .next { 171 | float: right; 172 | &:after { 173 | content: ""; 174 | width: 3px; 175 | height: 3px; 176 | border: 2px $grey solid; 177 | border-radius: 3px; 178 | display: block; 179 | float: right; 180 | margin-left: 7px; 181 | margin-top: 5px; 182 | } 183 | } 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /template/source/ink-blog-tool.md: -------------------------------------------------------------------------------- 1 | title: "简洁的静态博客构建工具 —— 纸小墨(InkPaper)" 2 | date: 2015-03-01 18:00:00 +0800 3 | update: 2016-07-11 17:00:00 +0800 4 | author: me 5 | cover: "-/images/example.png" 6 | tags: 7 | - 设计 8 | - 写作 9 | preview: 纸小墨(InkPaper)是一个GO语言编写的开源静态博客构建工具,可以快速搭建博客网站。它无依赖跨平台,配置简单构建快速,注重简洁易用与更优雅的排版。 10 | 11 | --- 12 | 13 | > **⚠注意:** 这是纸小墨的旧版本文档的副本,仅用于展示渲染效果,内容不具有参考性,**请勿依赖下文的内容使用纸小墨**!请访问[我们的仓库](https://github.com/InkProject/ink/)以获取最新文档。 14 | 15 | ## 纸小墨简介 16 | 17 | 纸小墨(InkPaper)是一个GO语言编写的开源静态博客构建工具,可以快速搭建博客网站。它无依赖跨平台,配置简单构建快速,注重简洁易用与更优雅的排版。 18 | 19 | ![纸小墨 - 简洁的静态博客构建工具](-/images/example.png) 20 | 21 | ### 开始上手 22 | 23 | - 下载并解压 [Ink](https://github.com/InkProject/ink/releases),运行命令 `ink preview` 24 | 25 | > 注意:Linux/macOS下,使用 `./ink preview` 26 | 27 | - 使用浏览器访问 `http://localhost:8000` 预览。 28 | 29 | ### 特性介绍 30 | - YAML格式的配置 31 | - Markdown格式的文章 32 | - 无依赖跨平台 33 | - 超快的构建速度 34 | - 不断改善的主题与排版 35 | - 多文章作者支持 36 | - 归档与标签自动生成 37 | - 保存时实时预览页面 38 | - 离线的全文关键字搜索 39 | - $\LaTeX$ 风格的数学公式支持(MathJax): 40 | 41 | $$ 42 | \int_{-\infty}^\infty g(x) dx = \frac{1}{2\pi i} \oint_{\gamma} \frac{f(z)}{z-g(x)} dz 43 | $$ 44 | 45 | ### 配置网站 46 | 编辑`config.yml`,使用如下格式: 47 | 48 | ``` yaml 49 | site: 50 | title: 网站标题 51 | subtitle: 网站子标题 52 | limit: 每页可显示的文章数目 53 | theme: 网站主题目录 54 | comment: 评论插件变量(默认为Disqus账户名) 55 | root: 网站根路径 #可选 56 | lang: 网站语言 #支持en, zh, ru, ja,de, 可在theme/config.yml配置 57 | url: 网站链接 #用于RSS生成 58 | link: 文章链接形式 #默认为{title}.html,支持{year},{month},{day},{title}变量 59 | 60 | authors: 61 | 作者ID: 62 | name: 作者名称 63 | intro: 作者简介 64 | avatar: 作者头像路径 65 | 66 | build: 67 | output: 构建输出目录 #可选, 默认为 "public" 68 | port: 预览端口 69 | copy: 70 | - 构建时将会复制的目录/文件 71 | publish: | 72 | ink publish 命令将会执行的脚本 73 | ``` 74 | 75 | ### 创建文章 76 | 在`source`目录中建立任意`.md`文件(可置于子文件夹),使用如下格式: 77 | 78 | ``` yaml 79 | title: 文章标题 80 | date: 年-月-日 时:分:秒 #创建时间,可加时区如" +0800" 81 | update: 年-月-日 时:分:秒 #更新时间,可选,可加时区如" +0800" 82 | author: 作者ID 83 | cover: 题图链接 #可选 84 | draft: false #草稿,可选 85 | top: false #置顶文章,可选 86 | preview: 文章预览,也可在正文中使用分割 #可选 87 | tags: #可选 88 | - 标签1 89 | - 标签2 90 | type: post #指定类型为文章(post)或页面(page),可选 91 | hide: false #隐藏文章,只可通过链接访问,可选 92 | toc: false #是否显示文章目录,可选 93 | 94 | --- 95 | 96 | Markdown格式的正文 97 | ``` 98 | 99 | ### 发布博客 100 | - 在博客目录下运行`ink publish`命令自动构建博客并发布。 101 | - 或运行`ink build`命令将生成的`public`目录下的内容手动部署。 102 | 103 | > Tips: 在使用`ink preview`命令时,编辑保存文件后,博客会自动重新构建并刷新浏览器页面。 104 | 105 | ## 定制支持 106 | 107 | ### 修改主题 108 | 109 | 默认主题在`theme`目录下,修改源代码后在该目录下运行`npm install`与`npm run build`重新构建。 110 | 111 | 页面包含`page.html`(文章列表)及`article.html`(文章)等,所有页面均支持[GO语言HTML模板](http://golang.org/pkg/html/template/)语法,可引用变量。 112 | 113 | ### 添加页面 114 | 115 | 在`source`目录下创建的任意`.html`文件将被复制,这些文件中可引用`config.yml`中site字段下的所有变量。 116 | 117 | #### 定义自定义变量 118 | 纸小墨支持在页面中定义自定义变量,必须放置于 `config.yaml` 的 `site.config` 之下,如: 119 | 120 | ``` yaml 121 | site: 122 | config: 123 | MyVar: "Hello World" 124 | ``` 125 | 126 | 在页面中可通过 `{{.Site.Config.MyVar}}` 来引用该变量。 127 | 128 | > **注意** 129 | > 130 | > 虽然 `config.yaml` 的其他部分字段名均为小写,但自定义变量的名称必须使用正确的大小写,如: 131 | 132 | > ```yaml 133 | site: 134 | config: 135 | MYVAR_aaa: "Hello World" 136 | ``` 137 | 138 | > 则在页面中必须使用 `{{.Site.Config.MYVAR_aaa}}` 来引用该变量。 139 | 140 | 141 | #### 使用函数(实验性) 142 | 143 | 纸小墨定义了一个最小的函数集合,可在 HTML 页面(除 `.md` 源文件以外)中使用,如 144 | 145 | ``` yaml 146 | {{ readFile "path/to/file" }} 147 | ``` 148 | 149 | 将读取 `path/to/file` 文件的内容并(不做任何处理地)包含入页面中。 150 | 151 | 对于文件相关的函数,当在 `source` 下执行时,文件路径相对于 `source` 目录;当在其他目录下执行时,文件路径相对于主题(如 `theme`)目录。 152 | 153 | 所有函数列表见源文件 `funcs.go` 文件。 154 | 155 | ### 博客迁移(Beta) 156 | 157 | 纸小墨提供简单的Jeklly/Hexo博客文章格式转换,使用命令: 158 | ``` shell 159 | ink convert /path/_posts 160 | ``` 161 | 162 | ### 源码编译 163 | 164 | 本地运行 165 | 166 | 1. 配置[GO](http://golang.org/doc/install)语言环境。 167 | 2. 运行命令`git clone https://github.com/InkProject/ink && cd ink && go install`编译并安装纸小墨。 168 | 3. 运行命令`ink preview $GOPATH/src/github.com/InkProject/ink/template`,预览博客。 169 | 170 | Docker构建(示例) 171 | 172 | 1. Clone源码 `git clone git@github.com:InkProject/ink.git`。 173 | 2. 源码目录下构建镜像`docker build -t ink .`。 174 | 3. 运行容器`docker run -p 8000:80 ink`。 175 | 176 | ## 主题 177 | 178 | - Dark(Official Theme): [https://github.com/InkProject/ink-theme-dark](https://github.com/InkProject/ink-theme-dark) 179 | - simple: [https://github.com/myiq/ink-simple](https://github.com/myiq/ink-simple) 180 | 181 | ## 相关链接 182 | 183 | - [InkPaper 最佳实践](https://segmentfault.com/a/1190000009084954) 184 | 185 | ## 反馈贡献 186 | 187 | 纸小墨基于 [CC Attribution-NonCommercial License 4.0](https://creativecommons.org/licenses/by-nc/4.0/) 协议,目前为止它仍然是个非成熟的开源项目,非常欢迎任何人的任何贡献。如有问题可报告至 [https://github.com/InkProject/ink/issues](https://github.com/InkProject/ink/issues)。 188 | 189 | ## 更新计划 190 | 191 | - 排版深度优化 192 | - 纸小墨编辑器 193 | 194 | ## 正在使用 195 | 196 | - [https://imeoer.github.io/blog/](https://imeoer.github.io/blog/) 197 | - [http://blog.hyper.sh/](http://blog.hyper.sh/) 198 | - [http://wangxu.me/](http://wangxu.me/) 199 | - [http://whzecomjm.com/](http://whzecomjm.com/) 200 | - [http://www.shery.me/blog/](http://www.shery.me/blog/) 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | InkPaper is a static blog generator developed in Golang. No dependencies, cross platform, easy to use, fast building times and an elegant theme. 4 | 5 | [![apm](https://img.shields.io/badge/license-CC%20BY--NC%204.0-blue.svg)](https://creativecommons.org/licenses/by-nc/4.0/) 6 | 7 | ![InkPaper - An Elegant Static Blog Generator](template/source/images/example-en.png) 8 | 9 | ### Features 10 | - YAML format configuration 11 | - Markdown format articles 12 | - No dependencies, cross platform 13 | - Super fast build times 14 | - Continuously improving theme and typography 15 | - Multiple article authors support 16 | - Archive and tag generation 17 | - Real-time preview when saving 18 | - Offline full-text keyword search 19 | - $\LaTeX$ style math formula support (MathJax): 20 | 21 | $$ 22 | \int_{-\infty}^\infty g(x) dx = \frac{1}{2\pi i} \oint_{\gamma} \frac{f(z)}{z-g(x)} dz 23 | $$ 24 | 25 | ### Quick Start 26 | - Download & Extract [Ink](https://github.com/InkProject/ink/releases) and run `ink preview` 27 | 28 | > Tip:Linux/macOS, use `./ink preview` 29 | 30 | - Open `http://localhost:8000` in your browser to preview 31 | 32 | ### Website Configuration 33 | Edit `config.yml`, use this format: 34 | 35 | ``` yaml 36 | site: 37 | title: Website Title 38 | subtitle: Website Subtitle 39 | limit: Max Article Count Per Page 40 | theme: Website Theme Directory 41 | comment: Comment Plugin Variable (Default is disqus username) 42 | root: Website Root Path # Optional 43 | lang: Website Language # Support en, zh, ru, ja, de, pt-br, configurable in theme/lang.yml 44 | url: Website URL # For RSS generating 45 | link: Article Link Scheme # Default is {title}.html, Support {year}, {month}, {day}, {hour}, {minute}, {second}, {title} variables 46 | 47 | authors: 48 | AuthorID: # Your author ID, used in article's author field 49 | name: Author Name 50 | intro: Author Motto 51 | avatar: Author Avatar Path 52 | 53 | build: 54 | output: Build Output Directory # Optional, default is "public" 55 | port: Preview Port 56 | copy: 57 | - Copied Files When Build 58 | publish: | 59 | Excuted command when 'ink publish' is used 60 | ``` 61 | 62 | ### Blog Writing 63 | Create a `.md` file in the `source` directory (Supports subdirectories). Use this format: 64 | 65 | ``` yaml 66 | title: Article Title 67 | date: Year-Month-Day Hour:Minute:Second #Created Time. Support timezone, such as " +0800" 68 | update: Year-Month-Day Hour:Minute:Second #Updated Time, optional. Support timezone, such as " +0800" 69 | author: AuthorID 70 | cover: Article Cover Path # Optional 71 | draft: false # Is draft or not, optional 72 | top: false # Place article to top or not, optional 73 | preview: Article Preview, Also use to split in body # Optional 74 | tags: # Optional 75 | - Tag1 76 | - Tag2 77 | type: post # Specify type is post or page, optional 78 | hide: false # Hide article or not. Hidden atricles still can be accessed via URL, optional 79 | toc: false # Show table of contents or not, optional 80 | --- 81 | 82 | Markdown Format's Body 83 | ``` 84 | 85 | ### Publish 86 | - Run `ink publish` in the blog directory to automatically build and publish 87 | - Or run `ink build` to manually deploy generated `public` directory 88 | 89 | > **Tips**: When files changed, `ink preview` will automatically rebuild the blog. Refresh browser to update. 90 | 91 | ## Customization 92 | 93 | ### Modifying The Theme 94 | 95 | The default theme is placed in the `theme` folder, run `npm install` and `npm run build` to rebuild in this folder. 96 | 97 | page `page.html` (article list) and `article.html` (article), use variable with [Golang Template](http://golang.org/pkg/html/template/) syntax. 98 | 99 | ### New Page 100 | 101 | Created any `.html` file will be copied to `source` directory, could use all variables on `site` field in `config.yml`. 102 | 103 | 104 | #### Define Custom Variables 105 | InkPaper supports defining custom variables in pages, which must be placed under `site.config` in `config.yaml`, such as: 106 | 107 | ``` yaml 108 | site: 109 | config: 110 | MyVar: "Hello World" 111 | ``` 112 | 113 | The variable can be referenced in the page by `{{.Site.Config.MyVar}}`. 114 | 115 | > **Note** 116 | > 117 | > Although the field names in other parts of `config.yaml` are all lowercase, the name of the custom variable must be used correctly. Otherwises, such a variable: 118 | > 119 | > ```yaml 120 | > site: 121 | > config: 122 | > MYVAR_aAa: "Hello World" 123 | > ``` 124 | > must be referenced in the page as `{{.Site.Config.MYVAR_aAa}}`. 125 | 126 | #### Use Functions (Experimental) 127 | 128 | InkPaper defines a minimal set of functions that can be used in HTML pages (except for `.md` source files), such as 129 | 130 | ``` yaml 131 | {{ readFile "path/to/file" }} 132 | ``` 133 | 134 | This will read the content of the file `path/to/file` and include it in the page without any processing. 135 | 136 | For file-related functions, when executed in the `source` directory, the file path is relative to the `source` directory; when executed in other directories, the file path is relative to the theme (such as `theme`). 137 | 138 | See the source file `funcs.go` for a list of all functions. 139 | 140 | ### Blog Migration (Beta) 141 | 142 | Supports simple Jeklly/Hexo post convertions. Usage: 143 | 144 | ``` shell 145 | ink convert /path/_posts 146 | ``` 147 | 148 | ### Building from source 149 | 150 | **Local Build** 151 | 152 | 1. Install [Golang](http://golang.org/doc/install) environment 153 | 2. Run `git clone https://github.com/InkProject/ink && cd ink && go install` to compile and install ink 154 | 3. Run `ink preview $GOPATH/src/github.com/InkProject/ink/template` to preview blog 155 | 156 | **Docker Build (Example)** 157 | 158 | 1. Clone code `git clone git@github.com:InkProject/ink.git` 159 | 2. Build image `docker build -t ink .` in source directory 160 | 3. Run container `docker run -p 8888:80 ink` 161 | 162 | ## Theme 163 | 164 | - Dark (Official Theme): [https://github.com/InkProject/ink-theme-dark](https://github.com/InkProject/ink-theme-dark) 165 | - Simple: [https://github.com/myiq/ink-simple](https://github.com/myiq/ink-simple) 166 | - Story: [https://github.com/akkuman/ink-theme-story](https://github.com/akkuman/ink-theme-story) 167 | - Material2: [https://github.com/w568w/InkMaterialTheme](https://github.com/w568w/InkMaterialTheme) 168 | 169 | ## Related Tutorials 170 | 171 | - [Automatically deploy your Ink blog to GitHub pages with Travis CI](http://www.shery.me/blog/travis-ci.html) 172 | 173 | ## License 174 | [CC Attribution-NonCommercial License 4.0](https://creativecommons.org/licenses/by-nc/4.0/) 175 | 176 | ## Reporting An Issue 177 | 178 | [https://github.com/InkProject/ink/issues](https://github.com/InkProject/ink/issues) 179 | 180 | ## These blogs are driven by InkPaper 181 | 182 | - [https://imeoer.github.io/blog/](https://imeoer.github.io/blog/) 183 | - [http://blog.hyper.sh/](http://blog.hyper.sh/) 184 | - [http://wangxu.me/](http://wangxu.me/) 185 | - [http://whzecomjm.com/](http://whzecomjm.com/) 186 | - [http://www.shery.me/blog/](http://www.shery.me/blog/) 187 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "html/template" 7 | "os" 8 | "path/filepath" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/gorilla/feeds" 13 | "github.com/snabb/sitemap" 14 | ) 15 | 16 | type Data interface{} 17 | 18 | type RenderArticle struct { 19 | Article 20 | Next *Article 21 | Prev *Article 22 | } 23 | 24 | // Compile html template 25 | func CompileTpl(tplPath string, partialTpl string, name string, funcContext FuncContext) template.Template { 26 | // Read template data from file 27 | html, err := os.ReadFile(tplPath) 28 | if err != nil { 29 | Fatal(err.Error()) 30 | } 31 | // Append partial template 32 | htmlStr := string(html) + partialTpl 33 | // Generate html content 34 | tpl, err := template.New(name).Funcs(funcContext.FuncMap()).Parse(htmlStr) 35 | if err != nil { 36 | Fatal(err.Error()) 37 | } 38 | return *tpl 39 | } 40 | 41 | // Render html file by data 42 | func RenderPage(tpl template.Template, tplData interface{}, outPath string) { 43 | // Create file 44 | outFile, err := os.Create(outPath) 45 | if err != nil { 46 | Fatal(err.Error()) 47 | } 48 | defer func() { 49 | outFile.Close() 50 | }() 51 | defer wg.Done() 52 | // Template render 53 | err = tpl.Execute(outFile, tplData) 54 | if err != nil { 55 | Fatal(err.Error()) 56 | } 57 | } 58 | 59 | // Generate all article page 60 | func RenderArticles(tpl template.Template, articles Collections) { 61 | defer wg.Done() 62 | articleCount := len(articles) 63 | for i := range articles { 64 | currentArticle := articles[i].(Article) 65 | var renderArticle = RenderArticle{currentArticle, nil, nil} 66 | // Only show next and prev article if it is not hidden 67 | if !renderArticle.Hide { 68 | if i >= 1 { 69 | // Find prev unhidden article 70 | for j := i - 1; j >= 0; j-- { 71 | prevArticle := articles[j].(Article) 72 | if !prevArticle.Hide { 73 | renderArticle.Prev = &prevArticle 74 | break 75 | } 76 | } 77 | } 78 | if i <= articleCount-2 { 79 | // Find next unhidden article 80 | for j := i + 1; j < articleCount; j++ { 81 | nextArticle := articles[j].(Article) 82 | if !nextArticle.Hide { 83 | renderArticle.Next = &nextArticle 84 | break 85 | } 86 | } 87 | } 88 | } 89 | outPath := filepath.Join(publicPath, currentArticle.Link) 90 | wg.Add(1) 91 | go RenderPage(tpl, renderArticle, outPath) 92 | } 93 | } 94 | 95 | // Generate rss page 96 | func GenerateRSS(articles Collections) { 97 | defer wg.Done() 98 | var feedArticles Collections 99 | if len(articles) < globalConfig.Site.Limit { 100 | feedArticles = articles 101 | } else { 102 | feedArticles = articles[0:globalConfig.Site.Limit] 103 | } 104 | if globalConfig.Site.Url != "" { 105 | feed := &feeds.Feed{ 106 | Title: globalConfig.Site.Title, 107 | Link: &feeds.Link{Href: globalConfig.Site.Url}, 108 | Description: globalConfig.Site.Subtitle, 109 | Author: &feeds.Author{Name: globalConfig.Site.Title, Email: ""}, 110 | Created: time.Now(), 111 | } 112 | feed.Items = make([]*feeds.Item, 0) 113 | for _, item := range feedArticles { 114 | article := item.(Article) 115 | feed.Items = append(feed.Items, &feeds.Item{ 116 | Title: article.Title, 117 | Link: &feeds.Link{Href: globalConfig.Site.Url + "/" + article.Link}, 118 | Description: string(article.Preview), 119 | Author: &feeds.Author{Name: article.Author.Name, Email: ""}, 120 | Created: article.Time, 121 | Updated: article.MTime, 122 | }) 123 | } 124 | if atom, err := feed.ToAtom(); err == nil { 125 | err := os.WriteFile(filepath.Join(publicPath, "atom.xml"), []byte(atom), 0644) 126 | if err != nil { 127 | Fatal(err.Error()) 128 | } 129 | } else { 130 | Fatal(err.Error()) 131 | } 132 | } 133 | } 134 | 135 | // Generate sitemap page 136 | func GenerateSitemap(articles Collections) { 137 | defer wg.Done() 138 | 139 | if globalConfig.Site.Url != "" { 140 | sm := sitemap.New() 141 | 142 | globalModTime := time.Now() 143 | sm.Add(&sitemap.URL{ 144 | Loc: globalConfig.Site.Url, 145 | LastMod: &globalModTime, 146 | ChangeFreq: sitemap.Weekly, 147 | }) 148 | 149 | for _, item := range articles { 150 | article := item.(Article) 151 | 152 | var lastModTime time.Time 153 | if article.MTime.After(article.Time) { 154 | lastModTime = article.MTime 155 | } else { 156 | lastModTime = article.Time 157 | } 158 | 159 | sm.Add(&sitemap.URL{ 160 | Loc: globalConfig.Site.Url + "/" + article.Link, 161 | LastMod: &lastModTime, 162 | ChangeFreq: sitemap.Weekly, 163 | }) 164 | } 165 | 166 | var sitemap bytes.Buffer 167 | sm.WriteTo(&sitemap) 168 | err := os.WriteFile(filepath.Join(publicPath, "sitemap.xml"), sitemap.Bytes(), 0644) 169 | if err != nil { 170 | Fatal(err.Error()) 171 | } 172 | } 173 | } 174 | 175 | // Generate article list page 176 | func RenderArticleList(rootPath string, articles Collections, tagName string) { 177 | defer wg.Done() 178 | // Create path 179 | pagePath := filepath.Join(publicPath, rootPath) 180 | os.MkdirAll(pagePath, 0777) 181 | // Split page 182 | limit := globalConfig.Site.Limit 183 | total := len(articles) 184 | page := total / limit 185 | rest := total % limit 186 | if rest != 0 { 187 | page++ 188 | } 189 | if total < limit { 190 | page = 1 191 | } 192 | for i := 0; i < page; i++ { 193 | var prev = filepath.Join(rootPath, "page"+strconv.Itoa(i)+".html") 194 | var next = filepath.Join(rootPath, "page"+strconv.Itoa(i+2)+".html") 195 | outPath := filepath.Join(pagePath, "index.html") 196 | if i != 0 { 197 | fileName := "page" + strconv.Itoa(i+1) + ".html" 198 | outPath = filepath.Join(pagePath, fileName) 199 | } else { 200 | prev = "" 201 | } 202 | if i == 1 { 203 | prev = filepath.Join(rootPath, "index.html") 204 | } 205 | first := i * limit 206 | count := first + limit 207 | if i == page-1 { 208 | if rest != 0 { 209 | count = first + rest 210 | } 211 | next = "" 212 | } 213 | var data = map[string]interface{}{ 214 | "Articles": articles[first:count], 215 | "Site": globalConfig.Site, 216 | "Develop": globalConfig.Develop, 217 | "Page": i + 1, 218 | "Total": page, 219 | "Prev": template.URL(filepath.ToSlash(prev)), 220 | "Next": template.URL(filepath.ToSlash(next)), 221 | "TagName": tagName, 222 | "TagCount": len(articles), 223 | } 224 | wg.Add(1) 225 | go RenderPage(pageTpl, data, outPath) 226 | } 227 | } 228 | 229 | // Generate article list JSON 230 | func GenerateJSON(articles Collections) { 231 | defer wg.Done() 232 | datas := make([]map[string]interface{}, 0) 233 | for i := range articles { 234 | article := articles[i].(Article) 235 | var data = map[string]interface{}{ 236 | "title": article.Title, 237 | "content": article.Markdown, 238 | "preview": string(article.Preview), 239 | "link": article.Link, 240 | "cover": article.Cover, 241 | } 242 | datas = append(datas, data) 243 | } 244 | str, _ := json.Marshal(datas) 245 | os.WriteFile(filepath.Join(publicPath, "index.json"), []byte(str), 0644) 246 | } 247 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "encoding/json" 7 | "io" 8 | "mime/multipart" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | 15 | "github.com/InkProject/ink.go" 16 | "github.com/edwardrf/symwalk" 17 | ) 18 | 19 | type NewArticle struct { 20 | Name string 21 | Content string 22 | } 23 | 24 | type OldArticle struct { 25 | Content string 26 | } 27 | 28 | type CacheArticleInfo struct { 29 | Name string 30 | Path string 31 | Date time.Time 32 | Article *ArticleConfig 33 | } 34 | 35 | var articleCache map[string]CacheArticleInfo 36 | 37 | func hashPath(path string) string { 38 | md5Hex := md5.Sum([]byte(path)) 39 | return hex.EncodeToString(md5Hex[:]) 40 | } 41 | 42 | func replyJSON(ctx *ink.Context, status int, data interface{}) { 43 | jsonStr, err := json.Marshal(data) 44 | if err != nil { 45 | http.Error(ctx.Res, err.Error(), http.StatusInternalServerError) 46 | ctx.Stop() 47 | return 48 | } 49 | if status == http.StatusOK { 50 | ctx.Header().Set("Content-Type", "application/json") 51 | ctx.Header().Set("Access-Control-Allow-Origin", "*") 52 | ctx.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") 53 | ctx.Res.Write(jsonStr) 54 | } else { 55 | Warn(data) 56 | http.Error(ctx.Res, data.(string), status) 57 | } 58 | ctx.Stop() 59 | } 60 | 61 | func UpdateArticleCache() { 62 | articleCache = make(map[string]CacheArticleInfo, 0) 63 | symwalk.Walk(sourcePath, func(path string, info os.FileInfo, err error) error { 64 | fileExt := strings.ToLower(filepath.Ext(path)) 65 | if fileExt == ".md" { 66 | fileName := strings.TrimPrefix(strings.TrimSuffix(strings.ToLower(path), ".md"), "template/source/") 67 | config, _ := ParseArticleConfig(path) 68 | id := hashPath(path) 69 | articleCache[string(id)] = CacheArticleInfo{ 70 | Name: fileName, 71 | Path: path, 72 | Date: ParseDate(config.Date), 73 | Article: config, 74 | } 75 | } 76 | return nil 77 | }) 78 | } 79 | 80 | func ApiListArticle(ctx *ink.Context) { 81 | UpdateArticleCache() 82 | replyJSON(ctx, http.StatusOK, articleCache) 83 | } 84 | 85 | func ApiGetArticle(ctx *ink.Context) { 86 | UpdateArticleCache() 87 | article, ok := articleCache[ctx.Param["id"]] 88 | if !ok { 89 | replyJSON(ctx, http.StatusNotFound, "Not Found") 90 | return 91 | } 92 | filePath := article.Path 93 | data, err := os.ReadFile(filePath) 94 | if err != nil { 95 | replyJSON(ctx, http.StatusInternalServerError, err.Error()) 96 | return 97 | } 98 | replyJSON(ctx, http.StatusOK, string(data)) 99 | } 100 | 101 | func ApiRemoveArticle(ctx *ink.Context) { 102 | UpdateArticleCache() 103 | article, ok := articleCache[ctx.Param["id"]] 104 | if !ok { 105 | replyJSON(ctx, http.StatusNotFound, "Not Found") 106 | return 107 | } 108 | filePath := article.Path 109 | err := os.Remove(filePath) 110 | if err != nil { 111 | replyJSON(ctx, http.StatusInternalServerError, err.Error()) 112 | return 113 | } 114 | replyJSON(ctx, http.StatusOK, nil) 115 | } 116 | 117 | func ApiCreateArticle(ctx *ink.Context) { 118 | decoder := json.NewDecoder(ctx.Req.Body) 119 | var article NewArticle 120 | err := decoder.Decode(&article) 121 | if err != nil { 122 | replyJSON(ctx, http.StatusBadRequest, err.Error()) 123 | return 124 | } 125 | filePath := filepath.Join(sourcePath, article.Name+".md") 126 | err = os.WriteFile(filePath, []byte(article.Content), 0644) 127 | if err != nil { 128 | replyJSON(ctx, http.StatusInternalServerError, err.Error()) 129 | return 130 | } 131 | replyJSON(ctx, http.StatusOK, map[string]string{ 132 | "id": hashPath(filePath), 133 | }) 134 | } 135 | 136 | func ApiSaveArticle(ctx *ink.Context) { 137 | UpdateArticleCache() 138 | decoder := json.NewDecoder(ctx.Req.Body) 139 | var article OldArticle 140 | err := decoder.Decode(&article) 141 | if err != nil { 142 | replyJSON(ctx, http.StatusBadRequest, err.Error()) 143 | return 144 | } 145 | cacheArticle, ok := articleCache[ctx.Param["id"]] 146 | if !ok { 147 | replyJSON(ctx, http.StatusNotFound, "Not Found") 148 | return 149 | } 150 | // Write 151 | path := cacheArticle.Path 152 | err = os.WriteFile(path, []byte(article.Content), 0644) 153 | if err != nil { 154 | replyJSON(ctx, http.StatusInternalServerError, err.Error()) 155 | return 156 | } 157 | replyJSON(ctx, http.StatusOK, nil) 158 | } 159 | 160 | func getFormFile(ctx *ink.Context, field string) (data []byte, handler *multipart.FileHeader, err error) { 161 | file, handler, err := ctx.Req.FormFile(field) 162 | if err != nil { 163 | replyJSON(ctx, http.StatusBadRequest, err.Error()) 164 | return nil, handler, err 165 | } 166 | data, err = io.ReadAll(file) 167 | if err != nil { 168 | replyJSON(ctx, http.StatusBadRequest, err.Error()) 169 | return data, handler, err 170 | } 171 | return data, handler, err 172 | } 173 | 174 | func ApiUploadFile(ctx *ink.Context) { 175 | UpdateArticleCache() 176 | fileData, handler, err := getFormFile(ctx, "file") 177 | if err != nil { 178 | replyJSON(ctx, http.StatusBadRequest, err.Error()) 179 | return 180 | } 181 | articleId := ctx.Req.FormValue("article_id") 182 | article, ok := articleCache[articleId] 183 | if !ok { 184 | replyJSON(ctx, http.StatusNotFound, "Not Found") 185 | return 186 | } 187 | fileDirPath := filepath.Join(sourcePath, "images", article.Name) 188 | err = os.MkdirAll(fileDirPath, 0777) 189 | if err != nil { 190 | replyJSON(ctx, http.StatusInternalServerError, err.Error()) 191 | return 192 | } 193 | if err = os.WriteFile(filepath.Join(fileDirPath, handler.Filename), fileData, 0777); err != nil { 194 | replyJSON(ctx, http.StatusInternalServerError, err.Error()) 195 | return 196 | } 197 | replyJSON(ctx, http.StatusOK, map[string]string{ 198 | "path": "-/" + filepath.Join("images", article.Name, handler.Filename), 199 | }) 200 | } 201 | 202 | func ApiGetConfig(ctx *ink.Context) { 203 | filePath := filepath.Join(rootPath, "config.yml") 204 | data, err := os.ReadFile(filePath) 205 | if err != nil { 206 | replyJSON(ctx, http.StatusInternalServerError, err.Error()) 207 | return 208 | } 209 | replyJSON(ctx, http.StatusOK, string(data)) 210 | } 211 | 212 | func ApiSaveConfig(ctx *ink.Context) { 213 | content, err := io.ReadAll(ctx.Req.Body) 214 | if err != nil { 215 | replyJSON(ctx, http.StatusInternalServerError, err.Error()) 216 | return 217 | } 218 | filePath := filepath.Join(rootPath, "config.yml") 219 | err = os.WriteFile(filePath, []byte(content), 0644) 220 | if err != nil { 221 | replyJSON(ctx, http.StatusInternalServerError, err.Error()) 222 | return 223 | } 224 | replyJSON(ctx, http.StatusOK, nil) 225 | } 226 | 227 | // func ApiRenameArticle(ctx *ink.Context) { 228 | // // Rename 229 | // cacheArticle, ok := articleCache[ctx.Param["id"]] 230 | // if !ok { 231 | // replyJSON(ctx, http.StatusNotFound, "Not Found") 232 | // return 233 | // } 234 | // oldPath := cacheArticle.(map[string]CacheArticleInfo)["path"].(string) 235 | // newPath := filepath.Join(sourcePath, newArticle.Name+".md") 236 | // err = os.Rename(oldPath, newPath) 237 | // if err != nil { 238 | // replyJSON(ctx, http.StatusInternalServerError, err.Error()) 239 | // return 240 | // } 241 | // } 242 | -------------------------------------------------------------------------------- /template/source/ink-blog-tool-en.md: -------------------------------------------------------------------------------- 1 | title: "An Elegant Static Blog Generator —— InkPaper" 2 | date: 2015-03-01 17:00:00 +0800 3 | update: 2016-07-11 17:00:00 +0800 4 | author: me 5 | cover: "-/images/example-en.png" 6 | tags: 7 | - 设计 8 | - 写作 9 | preview: InkPaper is a static blog generator developed in Golang. No dependencies, cross platform, easy to use, fast building times and an elegant theme. 10 | 11 | --- 12 | 13 | > **⚠Note:** This is a copy of the old version of Ink documentation, only for showing rendering effect, and its content is not up to date. **DO NOT rely on the instructions below to use Ink!** Please visit [our repository](https://github.com/InkProject/ink/) to get the latest documentation. 14 | 15 | ## Introduction 16 | 17 | InkPaper is a static blog generator developed in Golang. No dependencies, cross platform, easy to use, fast building times and an elegant theme. 18 | 19 | ![InkPaper - An Elegant Static Blog Generator](-/images/example-en.png) 20 | 21 | ### Quick Start 22 | - Download & Extract [Ink](https://github.com/InkProject/ink) and run `ink preview` 23 | 24 | > Tip:Linux/macOS, use `./ink preview` 25 | 26 | - Open `http://localhost:8000` in your browser to preview 27 | 28 | ### Features 29 | - YAML format configuration 30 | - Markdown format articles 31 | - No dependencies, cross platform 32 | - Super fast build times 33 | - Continuously improving theme and typography 34 | - Multiple article authors support 35 | - Archive and tag generation 36 | - Real-time preview when saving 37 | - Offline full-text keyword search 38 | - $\LaTeX$ style math formula support (MathJax): 39 | 40 | $$ 41 | \int_{-\infty}^\infty g(x) dx = \frac{1}{2\pi i} \oint_{\gamma} \frac{f(z)}{z-g(x)} dz 42 | $$ 43 | ### Website Configuration 44 | Edit `config.yml`, use this format: 45 | 46 | ``` yaml 47 | site: 48 | title: Website Title 49 | subtitle: Website Subtitle 50 | limit: Max Article Count Per Page 51 | theme: Website Theme Directory 52 | comment: Comment Plugin Variable (Default is disqus username) 53 | root: Website Root Path #Optional 54 | lang: Website Language #Support en, zh, ru, ja, de, Configurable in theme/lang.yml 55 | url: Website URL #For RSS Generating 56 | link: Article Link Scheme #Default Is {title}.html,Support {year},{month},{day},{title} Variables 57 | 58 | authors: 59 | AuthorID: 60 | name: Author Name 61 | intro: Author Motto 62 | avatar: Author Avatar Path 63 | 64 | build: 65 | output: Build Output Directory #Optional, Default is "public" 66 | port: Preview Port 67 | copy: 68 | - Copied Files When Build 69 | publish: | 70 | Excuted command when 'ink publish' is used 71 | ``` 72 | 73 | ### Blog Writing 74 | Create a `.md` file in the `source` directory (Supports subdirectories). Use this format: 75 | 76 | ``` yaml 77 | title: Article Title 78 | date: Year-Month-Day Hour:Minute:Second #Created Time,Support TimeZone, such as " +0800" 79 | update: Year-Month-Day Hour:Minute:Second #Updated Time,Optional,Support TimeZone, such as " +0800" 80 | author: AuthorID 81 | cover: Article Cover Path #Optional 82 | draft: false #If Draft,Optional 83 | top: false #Place article to top, Optional 84 | preview: Article Preview,Also use to split in body #Optional 85 | tags: #Optional 86 | - Tag1 87 | - Tag2 88 | type: post #Specify type is post or page, Optional 89 | hide: false #Hide article,can be accessed via URL, Optional 90 | toc: false #Show table of contents,Optional 91 | 92 | --- 93 | 94 | Markdown Format's Body 95 | ``` 96 | 97 | ### Publish 98 | - Run `ink publish` in the blog directory to automatically build and publish 99 | - Or run `ink build` to manually deploy generated `public` directory 100 | 101 | > **Tips**: When files changed,`ink preview` will automatically rebuild the blog. Refresh browser to update. 102 | 103 | ## Customization 104 | 105 | ### Modifying The Theme 106 | 107 | The default theme is placed in the `theme` folder, run `npm install` and `npm run build` to rebuild in this folder. 108 | 109 | page `page.html` (article list) and `article.html` (article), use variable with [Golang Template](http://golang.org/pkg/html/template/) syntax. 110 | 111 | ### New Page 112 | 113 | Created any `.html` file will be copied to `source` directory, could use all variables on `site` field in `config.yml`. 114 | 115 | #### Define Custom Variables 116 | InkPaper supports defining custom variables in pages, which must be placed under `site.config` in `config.yaml`, such as: 117 | 118 | ``` yaml 119 | site: 120 | config: 121 | MyVar: "Hello World" 122 | ``` 123 | 124 | The variable can be referenced in the page by `{{.Site.Config.MyVar}}`. 125 | 126 | > **Note** 127 | > 128 | > Although the field names in other parts of `config.yaml` are all lowercase, the name of the custom variable must be used correctly, such as: 129 | > 130 | > ```yaml 131 | site: 132 | config: 133 | MYVAR_aaa: "Hello World" 134 | ``` 135 | 136 | > then the variable must be referenced in the page by `{{.Site.Config.MYVAR_aaa}}`. 137 | 138 | #### Use Functions (Experimental) 139 | 140 | InkPaper defines a minimal set of functions that can be used in HTML pages (except for `.md` source files), such as 141 | 142 | ``` yaml 143 | {{ readFile "path/to/file" }} 144 | ``` 145 | 146 | This will read the content of the file `path/to/file` and include it in the page without any processing. 147 | 148 | For file-related functions, when executed in the `source` directory, the file path is relative to the `source` directory; when executed in other directories, the file path is relative to the theme (such as `theme`). 149 | 150 | See the source file `funcs.go` for a list of all functions. 151 | 152 | ### Blog Migrate (Beta) 153 | 154 | Supports simple Jeklly/Hexo post convertions. Usage: 155 | 156 | ``` shell 157 | ink convert /path/_posts 158 | ``` 159 | 160 | ### Building from source 161 | 162 | Local Build 163 | 164 | 1. Install [Golang](http://golang.org/doc/install) environment 165 | 2. Run `git clone https://github.com/InkProject/ink && cd ink && go install` to compile and install ink 166 | 3. Run `ink preview $GOPATH/src/github.com/InkProject/ink/template` to preview blog 167 | 168 | Docker Build (Example) 169 | 170 | 1. Clone code `git clone git@github.com:InkProject/ink.git` 171 | 2. Build image `docker build -t ink .` in source directory 172 | 3. Run container `docker run -p 8888:80 ink` 173 | 174 | ## Theme 175 | 176 | - Dark(Official Theme): [https://github.com/InkProject/ink-theme-dark](https://github.com/InkProject/ink-theme-dark) 177 | - simple: [https://github.com/myiq/ink-simple](https://github.com/myiq/ink-simple) 178 | 179 | ## Related Toturials 180 | 181 | - [Automatically deploy your Ink blog to GitHub pages wiht Travis CI](http://www.shery.me/blog/travis-ci.html) 182 | 183 | ## Reporting An Issue 184 | 185 | [CC Attribution-NonCommercial License 4.0](https://creativecommons.org/licenses/by-nc/4.0/) 186 | 187 | [https://github.com/InkProject/ink/issues](https://github.com/InkProject/ink/issues) 188 | 189 | ## Development Roadmap 190 | 191 | - Improve Theme 192 | - InkPaper Editor 193 | 194 | ## These blogs are driven by InkPaper 195 | 196 | - [https://imeoer.github.io/blog/](https://imeoer.github.io/blog/) 197 | - [http://blog.hyper.sh/](http://blog.hyper.sh/) 198 | - [http://wangxu.me/](http://wangxu.me/) 199 | - [http://whzecomjm.com/](http://whzecomjm.com/) 200 | - [http://www.shery.me/blog/](http://www.shery.me/blog/) 201 | -------------------------------------------------------------------------------- /template/theme/source/js/index.js: -------------------------------------------------------------------------------- 1 | import '../css/index.css' 2 | import $ from 'jquery' 3 | import './jquery.unveil' 4 | import searchTpl from './searchTpl.html' 5 | 6 | // pick from underscore 7 | let debounce = function (func, wait, immediate) { 8 | let timeout, args, context, timestamp, result; 9 | 10 | let later = function () { 11 | let last = Date.now() - timestamp; 12 | 13 | if (last < wait && last >= 0) { 14 | timeout = setTimeout(later, wait - last); 15 | } else { 16 | timeout = null; 17 | if (!immediate) { 18 | result = func.apply(context, args); 19 | if (!timeout) context = args = null; 20 | } 21 | } 22 | }; 23 | 24 | return function () { 25 | context = this; 26 | args = arguments; 27 | timestamp = Date.now(); 28 | let callNow = immediate && !timeout; 29 | if (!timeout) timeout = setTimeout(later, wait); 30 | if (callNow) { 31 | result = func.apply(context, args); 32 | context = args = null; 33 | } 34 | 35 | return result; 36 | }; 37 | } 38 | 39 | let timeSince = function (date) { 40 | let seconds = Math.floor((new Date() - date) / 1000) 41 | let interval = Math.floor(seconds / 31536000) 42 | if (interval > 1) return interval + timeSinceLang.year 43 | 44 | interval = Math.floor(seconds / 2592000) 45 | if (interval > 1) return interval + timeSinceLang.month 46 | 47 | interval = Math.floor(seconds / 86400) 48 | if (interval > 1) return interval + timeSinceLang.day 49 | 50 | interval = Math.floor(seconds / 3600) 51 | if (interval > 1) return interval + timeSinceLang.hour 52 | 53 | interval = Math.floor(seconds / 60) 54 | if (interval > 1) return interval + timeSinceLang.minute 55 | 56 | return Math.floor(seconds) + timeSinceLang.second 57 | } 58 | 59 | let initSearch = function () { 60 | let searchDom = $('#search') 61 | if (!searchDom.length) return 62 | let searchWorker = new Worker(root + '/bundle/searchWorker.js') 63 | let oriHtml = $('.article-list').html() 64 | let workerStarted = false 65 | let tpl = function (keywords, title, preview, link, cover) { 66 | for (let i = 0; i < keywords.length; i++) { 67 | let keyword = keywords[i] 68 | let wrap = '' + keyword + '' 69 | let reg = new RegExp(keyword, 'ig') 70 | title = title.replace(reg, wrap) 71 | preview = preview.replace(reg, wrap) 72 | } 73 | return searchTpl 74 | .replace('{{title}}', title) 75 | .replace('{{link}}', link + '?search=' + keywords) // append keywords to url 76 | .replace('{{preview}}', preview) 77 | } 78 | searchWorker.onmessage = function (event) { 79 | let results = event.data.results 80 | let keywords = event.data.keywords 81 | if (results.length) { 82 | let retHtml = '' 83 | for (let i = 0; i < results.length; i++) { 84 | let item = results[i] 85 | let itemHtml = tpl(keywords, item.title, item.preview, item.link, item.cover) 86 | retHtml += itemHtml 87 | } 88 | $('.page-nav').hide() 89 | $('.article-list').html(retHtml) 90 | } else { 91 | let keyword = event.data.keyword 92 | if (keyword) { 93 | $('.page-nav').hide() 94 | $('.article-list').html('
    未搜索到 "' + keyword + '"
    ') 95 | } else { 96 | $('.page-nav').show() 97 | $('.article-list').html(oriHtml) 98 | } 99 | } 100 | } 101 | searchDom.on('input', debounce(function () { 102 | let keyword = $(this).val().trim() 103 | if (keyword) { 104 | searchWorker.postMessage({ 105 | search: 'search', 106 | keyword: keyword 107 | }) 108 | } else { 109 | $('.page-nav').show() 110 | $('.article-list').html(oriHtml) 111 | } 112 | }, 500)) 113 | searchDom.on('focus', function () { 114 | if (!workerStarted) { 115 | searchWorker.postMessage({ 116 | action: 'start', 117 | root: root 118 | }) 119 | workerStarted = true 120 | } 121 | }) 122 | } 123 | 124 | $(function () { 125 | // render date 126 | $('.date').each(function (idx, item) { 127 | let $date = $(item) 128 | let timeStr = $date.data('time') 129 | if (timeStr) { 130 | let unixTime = Number(timeStr) * 1000 131 | let date = new Date(unixTime) 132 | $date.prop('title', date).text(timeSince(date)) 133 | } 134 | }) 135 | // render highlight 136 | import("highlight.js").then(({ default: hljs }) => { 137 | hljs.highlightAll() 138 | }) 139 | // append image description 140 | $('img').each(function (_, item) { 141 | let $item = $(item) 142 | if ($item.attr('data-src')) { 143 | $item.wrap('') 144 | let imageAlt = $item.prop('alt') 145 | if (imageAlt.trim()) $item.parent('a').after('
    ' + imageAlt + '
    ') 146 | } 147 | }) 148 | // lazy load images 149 | if ($('img').unveil) { 150 | $('img').unveil(200, function () { 151 | $(this).on("load", function () { 152 | this.style.opacity = 1 153 | }) 154 | }) 155 | } 156 | // init search 157 | initSearch() 158 | }) 159 | 160 | /** 161 | * Get all text nodes under an element 162 | * @param {!Element} el 163 | * @return {Array} 164 | */ 165 | function getTextNodes(el) { 166 | const iterator = document.createNodeIterator(el, NodeFilter.SHOW_TEXT); 167 | const textNodes = []; 168 | let currentTextNode; 169 | while ((currentTextNode = iterator.nextNode())) { 170 | textNodes.push(currentTextNode); 171 | } 172 | return textNodes; 173 | } 174 | 175 | // get searched keywords from url 176 | let reg = new RegExp("(^|&)search=([^&]*)(&|$)") 177 | let r = window.location.search.substring(1).match(reg) 178 | if (r != null && r.toString().length > 1) { 179 | let keywords = decodeURI(r[2]).split(',') 180 | // highlight searched keywords 181 | getTextNodes(document.body).forEach(function (node) { 182 | let i; 183 | for (i = 0; i < node.parentNode.childNodes.length; i++) { 184 | if (node.parentNode.childNodes[i] === node) { 185 | break; 186 | } 187 | } 188 | 189 | let content = node.nodeValue 190 | let oldContent = content 191 | for (let i = 0; i < keywords.length; i++) { 192 | let keyword = keywords[i] 193 | let wrap = '' + keyword + '' 194 | let reg = new RegExp(keyword, 'ig') 195 | content = content.replace(reg, wrap) 196 | } 197 | 198 | // replace the text node with a span containing the highlighted text 199 | if (content != oldContent) { 200 | let newNode = document.createElement('span') 201 | newNode.innerHTML = content 202 | node.parentNode.replaceChild(newNode, node) 203 | } 204 | }) 205 | // scroll to the first searched keyword 206 | let elmnt = document.getElementsByClassName("searched") 207 | elmnt[0].scrollIntoView() 208 | } 209 | 210 | // show "go top" button when needed 211 | $(function () { 212 | $("#go_top").hide(); 213 | $(function () { 214 | let height = $(window).height(); 215 | $(window).on("scroll", function () { 216 | if ($(window).scrollTop() > height) { 217 | $("#go_top").fadeIn(500); 218 | } else { 219 | $("#go_top").fadeOut(500); 220 | } 221 | }); 222 | $("#go_top").on("click", function () { 223 | $('body,html').animate({ scrollTop: 0 }, 100); 224 | return false; 225 | }); 226 | }); 227 | }); -------------------------------------------------------------------------------- /template/theme/bundle/index.css: -------------------------------------------------------------------------------- 1 | .hljs{background:#f0f0f0;display:block;overflow-x:auto;padding:.5em}.hljs,.hljs-subst,.hljs-tag .hljs-title{color:#000}.hljs-attribute,.hljs-deletion,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-symbol,.hljs-template-variable,.hljs-title,.hljs-variable{color:#800}.hljs-comment,.hljs-quote{color:#888}.hljs-addition,.hljs-bullet,.hljs-link,.hljs-literal,.hljs-number,.hljs-regexp{color:#080}.hljs-meta{color:#88f}.hljs-built_in,.hljs-doctag,.hljs-keyword,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-strong,.hljs-title,.hljs-type{font-weight:700}.hljs-emphasis{font-style:italic}@define-mixin font-main{font-family:Helvetica Neue,Arial,Hiragino Sans GB,WenQuanYi Micro Hei,Microsoft YaHei,sans-serif}@define-mixin font-code{font-family:consolas,monaco,Source Code Pro,hack,monospace}body,html,input{margin:0}body,html{height:100%}ul{list-style:none;margin:0;padding:0}a{text-decoration:none}a:hover{opacity:.9}::-moz-selection{background-color:#293846;color:#fcfcfc}::selection{background-color:#293846;color:#fcfcfc}.clearfix:after,.clearfix:before{content:" ";display:table}.clearfix:after{clear:both}.clearfix{zoom:1}.main.page{padding-bottom:70px;padding-top:100px}.main.page .header{text-align:center}.main.page .header .tag{font-size:32px;font-weight:700;margin:30px 0 0}.main.page .header .tag-sub{color:#7f8c8d;font-size:16px;font-weight:400;margin:10px 0 0}.main.page .header .search-wrap{margin:50px auto 0;max-width:300px;position:relative}.main.page .header .search-wrap .search{border:1px solid #ddd;border-radius:100px;-webkit-box-sizing:border-box;box-sizing:border-box;opacity:.8;padding:10px 25px;width:100%}.main.page .header .search-wrap .search:focus{opacity:1;outline:0}.main.page .header .search-wrap .icon{opacity:.3;position:absolute;right:10px;top:5px}.main.page .article-list{margin-top:100px}.main.page .article-list .searched{background-color:#009a61;color:#fcfcfc}.main.page .article-list .article{border-bottom:1px dashed #ddd;margin-bottom:55px;padding-bottom:45px}.main.page .article-list .article:last-child{border-bottom:0}.main.page .article-list .article .title{color:#293846;display:block;font-size:26px;font-weight:700;margin-bottom:5px;text-decoration:none}.main.page .article-list .article .title .top{color:#7f8c8d;margin-right:5px}.main.page .article-list .article .cover{background-position:50%;background-repeat:no-repeat;background-size:cover;border:1px solid #ddd;border-radius:6px;-webkit-box-sizing:border-box;box-sizing:border-box;color:#eee;height:280px;margin-top:15px;width:100%}@media (max-width:414px){.main.page .article-list .article .cover{height:180px}}.main.page .article-list .article .preview{color:#293846;display:block;font-size:16px;line-height:28px;margin-top:10px}.main.page .article-list .article .info{color:#7f8c8d;font-size:14px;margin-bottom:20px;margin-top:15px}.main.page .article-list .article .avatar{background-repeat:no-repeat;background-size:cover;border:1px solid #ddd;border-radius:20px;float:left;height:20px;margin-right:10px;margin-top:-4px;width:20px}.main.page .article-list .article .name{margin-right:10px}.main.page .article-list .article .date{color:#7f8c8d}.main.page .article-list .article .tags{float:right;margin-left:10px}.main.page .article-list .article .tags .tag{color:#7f8c8d;margin-right:5px;text-decoration:none}.main.page .article-list .empty{font-size:24px;max-width:800px;overflow:hidden;text-align:center;-o-text-overflow:ellipsis;text-overflow:ellipsis}.main.page .article-list .empty span{font-weight:700}.main.page .page-nav{font-size:14px;height:50px;position:relative}.main.page .page-nav a{color:#7f8c8d;font-style:italic;text-decoration:none}.main.page .page-nav .nav{color:#7f8c8d;display:block;left:0;position:absolute;right:0;text-align:center;z-index:-1}.main.page .page-nav .prev{float:left}.main.page .page-nav .prev:before{border:2px solid #7f8c8d;border-radius:3px;content:"";display:block;float:left;height:3px;margin-right:7px;margin-top:5px;width:3px}.main.page .page-nav .next{float:right}.main.page .page-nav .next:after{border:2px solid #7f8c8d;border-radius:3px;content:"";display:block;float:right;height:3px;margin-left:7px;margin-top:5px;width:3px}.main.article{padding-bottom:120px;padding-top:120px}.main.article .title{color:#293846;font-size:30px;font-weight:700;line-height:42px;margin:0}.main.article .info{font-size:14px;margin-top:10px}.main.article .info .avatar{background-repeat:no-repeat;background-size:cover;border:1px solid #ddd;border-radius:20px;float:left;height:20px;margin-right:10px;margin-top:-4px;width:20px}.main.article .info .name{margin-right:5px}.main.article .info .date{color:#7f8c8d;margin-right:5px}.main.article .info .tags .tag{color:#7f8c8d;margin-right:5px;text-decoration:none}.main.article .recommend{margin-top:50px;overflow:auto}.main.article .recommend .nav{width:100%}.main.article .recommend .nav .head{color:#7f8c8d;font-size:12px;font-style:italic;margin-bottom:10px;text-align:center}.main.article .recommend .nav .link{color:#293846;display:block;text-align:center}.main.article .recommend .nav.prev.more{float:left;text-align:left;width:45%}.main.article .recommend .nav.prev.more .head,.main.article .recommend .nav.prev.more .link{text-align:left}.main.article .recommend .nav.next.more{float:right;text-align:right;width:45%}.main.article .recommend .nav.next.more .head,.main.article .recommend .nav.next.more .link{text-align:right}.main.article .author{margin-top:36px;padding-top:40px;text-align:center}.main.article .author .avatar{background-repeat:no-repeat;background-size:cover;border:20px solid #fff;border-radius:70px;height:70px;margin:0 auto;width:70px}.main.article .author .avatar:before{border-top:1px dashed #ddd;content:"";left:0;margin-top:36px;position:absolute;right:0;width:100%;z-index:-1}.main.article .author .name{font-size:16px;font-weight:700;margin-top:20px}.main.article .author .intro{color:#7f8c8d;font-size:14px;font-style:italic;margin-top:8px}.main.article #disqus_thread{margin-top:50px}.main.archive,.main.tag{padding-bottom:100px;padding-top:100px}.main.archive a,.main.tag a{border-bottom:1px dashed #009a61;color:#009a61;text-decoration:none}.main.archive .site .subtitle,.main.archive .site .title,.main.tag .site .subtitle,.main.tag .site .title{text-align:left}.main.archive .header,.main.tag .header{font-size:18px;padding-top:60px}.main.archive .header .title,.main.tag .header .title{font-weight:700;margin-right:5px;margin-top:30px}.main.archive .header .subtitle,.main.tag .header .subtitle{font-style:italic}.main.archive .archive-list,.main.tag .archive-list{margin-top:50px}.main.archive .archive-list .archive-item,.main.tag .archive-list .archive-item{margin:40px 0}.main.archive .archive-list .archive-item .archive-year,.main.tag .archive-list .archive-item .archive-year{color:#293846;font-weight:700;margin:15px 0}.main.archive .archive-list .archive-item .article-list .article-item,.main.tag .archive-list .archive-item .article-list .article-item{margin-bottom:10px}.main.archive .archive-list .archive-item .article-list .article-item .date,.main.tag .archive-list .archive-item .article-list .article-item .date{color:#7f8c8d;font-style:italic;margin-right:20px}.main.archive .tag-list,.main.tag .tag-list{margin-top:50px}.main.archive .tag-list .tag-item,.main.tag .tag-list .tag-item{float:left;margin-bottom:25px;margin-right:10px}.main.archive .tag-list .tag-item .tag-name,.main.tag .tag-list .tag-item .tag-name{border:1px solid #eee;border-radius:3px;font-size:14px;padding:5px 7px}.main.archive .tag-list .tag-item .tag-name:hover,.main.tag .tag-list .tag-item .tag-name:hover{background-color:#293846;border:1px solid #293846;color:#fcfcfc}.main .content{color:#293846;font-size:16px;line-height:1.7;margin-top:70px}.main .content h1+p,.main .content h2+p,.main .content h3+p,.main .content h4+p,.main .content h5+p,.main .content h6+p{margin-top:10px}.main .content h1{font-size:28px;margin-bottom:10px;margin-top:40px}.main .content h2{font-size:24px;margin-bottom:10px;margin-top:40px}.main .content h3{font-size:18px;margin-bottom:10px;margin-top:40px}.main .content p{margin-bottom:30px;margin-top:30px;text-align:left}.main .content.preview p{margin-bottom:10px;margin-top:10px}.main .content a{border-bottom:1px dashed #009a61;color:#009a61;text-decoration:none}.main .content a:hover{border-bottom:1px dashed #004e31;color:#004e31}.main .content code{background-color:#f7f7f7;border-radius:3px;color:#009a61;font-size:14px;margin:0 3px;padding:1px 4px}.main .content pre{margin:0}.main .content pre code{background-color:#fcfcfc;border:2px dashed #eee;border-radius:6px;color:inherit;display:block;font-size:14px;margin:0;overflow:auto;padding:10px 15px}.main .content blockquote{border-left:4px solid #009a61;font-style:italic;margin:25px 0 25px -23px;padding:0 10px 0 20px}.main .content table{border-collapse:collapse;border-color:#ddd;border-width:1px;font-size:14px;width:100%}.main .content table th{background-color:#eee;border:1px solid #ddd;padding:5px}.main .content table td{background-color:#fcfcfc;border:1px solid #ddd;padding:5px}.main .content ul{list-style:circle;padding-left:40px}.main .content ul li{margin:5px 0}.main .content ol{padding-left:40px}.main .content ol li{margin:5px 0}.main .content hr{border:0;border-top:1px dashed #ddd;margin:25px 0}.main .content img{border-radius:5px;cursor:pointer;display:block;margin:30px auto;max-width:100%;opacity:.6;-webkit-transition:opacity .3s ease-in;-o-transition:opacity .3s ease-in;transition:opacity .3s ease-in}.main .content .image-alt{color:#7f8c8d;font-style:italic;margin-bottom:30px;margin-top:-10px;text-align:center}.main .content .searched{background-color:#ff0}.header-wrap{-webkit-box-pack:justify;-ms-flex-pack:justify;-webkit-box-align:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:18px;font-weight:700;-webkit-justify-content:space-between;justify-content:space-between;left:0;padding:15px;position:absolute;right:0;z-index:1}.header-wrap a{color:#a1b2b4}.header-wrap .index{-webkit-box-align:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.header-wrap .index .logo{border:1px solid #ddd;border-radius:50%;height:30px;margin-right:10px;width:30px}.header-wrap .menu{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.header-wrap .menu .menu-item{margin-right:10px}.main,a.name{color:#293846}.main{margin:0 auto;max-width:720px;min-height:100%;padding:0 15px;position:relative}.main .site .logo{background-repeat:no-repeat;background-size:cover;border:1px solid #ddd;border-radius:50%;height:80px;margin:0 auto;width:80px}.main .site .title{font-size:32px;font-weight:700;margin:30px 0 0;text-align:center}.main .site .subtitle{color:#a1b2b4;font-size:16px;font-style:italic;font-weight:400;margin-top:10px;text-align:center}.main.about .info{margin-top:80px;text-align:center}.container{height:auto;min-height:100%;padding-bottom:0}.footer{background-color:#fcfcfc;border-top:1px solid #eee;clear:both;color:#a1b2b4;font-size:14px;height:32px;margin-top:-36px;position:relative;width:100%}.footer span{display:block;padding:8px 15px}.footer span a{font-weight:700}.footer .copyright{float:left}.footer .publish{float:right}.footer .publish a{color:#a1b2b4;text-decoration:none} -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "html/template" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "runtime" 11 | "strings" 12 | "time" 13 | 14 | "github.com/edwardrf/symwalk" 15 | "github.com/urfave/cli/v2" 16 | "gopkg.in/yaml.v2" 17 | ) 18 | 19 | const ( 20 | VERSION = "RELEASE 2018-07-27" 21 | DEFAULT_ROOT = "blog" 22 | DATE_FORMAT_STRING = "2006-01-02 15:04:05" 23 | INDENT = " " // 2 spaces 24 | POST_TEMPLATE = `title: {{.Title}} 25 | date: {{.DateString}} 26 | author: {{.Author}} 27 | {{- if .Cover}} 28 | cover: {{.Cover}} 29 | {{- end}} 30 | draft: {{.Draft}} 31 | top: {{.Top}} 32 | {{- if .Preview}} 33 | preview: {{.Preview}} 34 | {{- end}} 35 | {{- if .Tags}} 36 | {{.Tags}} 37 | {{- end}} 38 | type: {{.Type}} 39 | hide: {{.Hide}} 40 | toc: {{.Toc}} 41 | --- 42 | ` 43 | ) 44 | 45 | var globalConfig *GlobalConfig 46 | var themeConfig *ThemeConfig 47 | var rootPath string 48 | 49 | func main() { 50 | 51 | app := cli.NewApp() 52 | app.Name = "ink" 53 | app.Usage = "An elegant static blog generator" 54 | app.Authors = []*cli.Author{ 55 | {Name: "Harrison", Email: "harrison@lolwut.com"}, 56 | {Name: "Oliver Allen", Email: "oliver@toyshop.com"}, 57 | } 58 | //app.Email = "imeoer@gmail.com" 59 | app.Version = VERSION 60 | app.Commands = []*cli.Command{ 61 | { 62 | Name: "build", 63 | Usage: "Generate blog to public folder", 64 | Action: func(c *cli.Context) error { 65 | ParseGlobalConfigByCli(c, false) 66 | Build() 67 | return nil 68 | }, 69 | }, 70 | { 71 | Name: "preview", 72 | Usage: "Run in server mode to preview blog", 73 | Action: func(c *cli.Context) error { 74 | ParseGlobalConfigByCli(c, true) 75 | Build() 76 | Watch() 77 | Serve() 78 | return nil 79 | }, 80 | }, 81 | { 82 | Name: "publish", 83 | Usage: "Generate blog to public folder and publish", 84 | Action: func(c *cli.Context) error { 85 | ParseGlobalConfigByCli(c, false) 86 | Build() 87 | Publish() 88 | return nil 89 | }, 90 | }, 91 | { 92 | Name: "serve", 93 | Usage: "Run in server mode to serve blog", 94 | Action: func(c *cli.Context) error { 95 | ParseGlobalConfigByCli(c, true) 96 | Build() 97 | Serve() 98 | return nil 99 | }, 100 | }, 101 | { 102 | Name: "convert", 103 | Usage: "Convert Jekyll/Hexo post format to Ink format (Beta)", 104 | Action: func(c *cli.Context) error { 105 | Convert(c) 106 | return nil 107 | }, 108 | }, 109 | { 110 | Name: "new", 111 | Usage: "Creates a new article", 112 | Flags: []cli.Flag{ 113 | &cli.BoolFlag{ 114 | Name: "hide", 115 | Usage: "Hides the article", 116 | }, 117 | &cli.BoolFlag{ 118 | Name: "toc", 119 | Usage: "Adds a table of contents to the article", 120 | }, 121 | &cli.BoolFlag{ 122 | Name: "top", 123 | Usage: "Places the article at the top", 124 | }, 125 | &cli.BoolFlag{ 126 | Name: "post", 127 | Usage: "The article is a post", 128 | }, 129 | &cli.BoolFlag{ 130 | Name: "page", 131 | Usage: "The article is a page", 132 | }, 133 | &cli.BoolFlag{ 134 | Name: "draft", 135 | Usage: "The article is a draft", 136 | }, 137 | 138 | &cli.StringFlag{ 139 | Name: "title", 140 | Usage: "Article title", 141 | }, 142 | &cli.StringFlag{ 143 | Name: "author", 144 | Usage: "Article author", 145 | }, 146 | &cli.StringFlag{ 147 | Name: "cover", 148 | Usage: "Article cover path", 149 | }, 150 | &cli.StringFlag{ 151 | Name: "date", 152 | Usage: "The date and time on which the article was created (2006-01-02 15:04:05)", 153 | }, 154 | &cli.StringFlag{ 155 | Name: "file", 156 | Usage: "The path of where the article will be stored", 157 | }, 158 | 159 | &cli.StringSliceFlag{ 160 | Name: "tag", 161 | Usage: "Adds a tag to the article", 162 | }, 163 | }, 164 | Action: func(c *cli.Context) error { 165 | New(c) 166 | return nil 167 | }, 168 | }, 169 | } 170 | app.Run(os.Args) 171 | os.Exit(exitCode) 172 | } 173 | 174 | func ParseGlobalConfigByCli(c *cli.Context, develop bool) { 175 | if c.Args().Len() > 0 { 176 | rootPath = c.Args().Slice()[0] 177 | } else { 178 | rootPath = "." 179 | } 180 | ParseGlobalConfigWrap(rootPath, develop) 181 | if globalConfig == nil { 182 | ParseGlobalConfigWrap(DEFAULT_ROOT, develop) 183 | if globalConfig == nil { 184 | Fatal("Parse config.yml failed, please specify a valid path") 185 | } 186 | } 187 | } 188 | 189 | func ParseGlobalConfigWrap(root string, develop bool) { 190 | rootPath = root 191 | globalConfig, themeConfig = ParseGlobalConfig(filepath.Join(rootPath, "config.yml"), develop) 192 | if globalConfig == nil || themeConfig == nil { 193 | return 194 | } 195 | } 196 | 197 | func New(c *cli.Context) { 198 | // If source folder does not exist, create 199 | if _, err := os.Stat("source/"); os.IsNotExist(err) { 200 | os.Mkdir("source", os.ModePerm) 201 | } 202 | 203 | var author, blogTitle, fileName string 204 | var tags []string 205 | 206 | // Default values 207 | draft := "false" 208 | top := "false" 209 | postType := "post" 210 | hide := "false" 211 | toc := "false" 212 | date := time.Now() 213 | 214 | // Empty string values 215 | preview := "" 216 | cover := "" 217 | 218 | // Parse args 219 | args := c.Args() 220 | if args.Len() > 0 { 221 | blogTitle = args.Slice()[0] 222 | } 223 | if blogTitle == "" { 224 | if c.String("title") != "" { 225 | blogTitle = c.String("title") 226 | } else { 227 | Fatal("Please specify the name of the blog post") 228 | } 229 | } 230 | 231 | fileName = blogTitle + ".md" 232 | if c.String("file") != "" { 233 | fileName = c.String("file") 234 | } 235 | 236 | if args.Len() > 1 { 237 | author = args.Slice()[1] 238 | } 239 | if author == "" { 240 | author = c.String("author") 241 | } 242 | 243 | if c.Bool("post") && c.Bool("page") { 244 | Fatal("The post and page arguments are mutually exclusive and cannot appear together") 245 | } 246 | if c.Bool("post") { 247 | postType = "post" 248 | } 249 | if c.Bool("page") { 250 | postType = "page" 251 | } 252 | if c.Bool("hide") { 253 | hide = "true" 254 | } 255 | if c.Bool("toc") { 256 | toc = "true" 257 | } 258 | if c.Bool("draft") { 259 | draft = "true" 260 | } 261 | if c.Bool("top") { 262 | top = "true" 263 | } 264 | 265 | if c.String("preview") != "" { 266 | preview = c.String("preview") 267 | } 268 | if c.String("cover") != "" { 269 | cover = c.String("cover") 270 | } 271 | 272 | var filePath = "source/" + fileName 273 | file, err := os.Create(filePath) 274 | if err != nil { 275 | Fatal(err) 276 | } 277 | postTemplate, err := template.New("post").Parse(POST_TEMPLATE) 278 | if err != nil { 279 | Fatal(err) 280 | } 281 | 282 | if c.StringSlice("tag") != nil { 283 | tags = c.StringSlice("tag") 284 | } 285 | 286 | var tagString string 287 | if len(tags) > 0 { 288 | tagString = "tags:" 289 | for _, tag := range tags { 290 | tagString += "\n" + INDENT + "- " + tag 291 | } 292 | } 293 | 294 | var dateString string 295 | if c.String("date") != "" { 296 | dateString = c.String("date") 297 | _, err = time.Parse(DATE_FORMAT_STRING, dateString) 298 | if err != nil { 299 | Fatal("Illegal date string") 300 | } 301 | } else { 302 | dateString = date.Format(DATE_FORMAT_STRING) 303 | } 304 | data := map[string]string{ 305 | "Title": blogTitle, 306 | "DateString": dateString, 307 | "Author": author, 308 | "Draft": draft, 309 | "Top": top, 310 | "Type": postType, 311 | "Hide": hide, 312 | "Toc": toc, 313 | "Preview": preview, 314 | "Cover": cover, 315 | "Tags": tagString, 316 | } 317 | fileWriter := bufio.NewWriter(file) 318 | err = postTemplate.Execute(fileWriter, data) 319 | if err != nil { 320 | Fatal(err) 321 | } 322 | err = fileWriter.Flush() 323 | if err != nil { 324 | Fatal(err) 325 | } 326 | } 327 | 328 | func Publish() { 329 | command := globalConfig.Build.Publish 330 | // Prepare exec command 331 | var shell, flag string 332 | if runtime.GOOS == "windows" { 333 | shell = "cmd" 334 | flag = "/C" 335 | } else { 336 | shell = "/bin/sh" 337 | flag = "-c" 338 | } 339 | cmd := exec.Command(shell, flag, command) 340 | cmd.Dir = filepath.Join(rootPath, globalConfig.Build.Output) 341 | // Start print stdout and stderr of process 342 | stdout, _ := cmd.StdoutPipe() 343 | stderr, _ := cmd.StderrPipe() 344 | out := bufio.NewScanner(stdout) 345 | err := bufio.NewScanner(stderr) 346 | // Print stdout 347 | go func() { 348 | for out.Scan() { 349 | Log(out.Text()) 350 | } 351 | }() 352 | // Print stdin 353 | go func() { 354 | for err.Scan() { 355 | Log(err.Text()) 356 | } 357 | }() 358 | // Exec command 359 | cmd.Run() 360 | } 361 | 362 | func Convert(c *cli.Context) { 363 | // Parse arguments 364 | var sourcePath, rootPath string 365 | args := c.Args() 366 | if args.Len() > 0 { 367 | sourcePath = args.Slice()[0] 368 | } else { 369 | Fatal("Please specify the posts source path") 370 | } 371 | if args.Len() > 1 { 372 | rootPath = args.Slice()[1] 373 | } else { 374 | rootPath = "." 375 | } 376 | // Check if path exist 377 | if !Exists(sourcePath) || !Exists(rootPath) { 378 | Fatal("Please specify valid path") 379 | } 380 | // Parse Jekyll/Hexo post file 381 | count := 0 382 | symwalk.Walk(sourcePath, func(path string, f os.FileInfo, err error) error { 383 | fileExt := strings.ToLower(filepath.Ext(path)) 384 | if fileExt == ".md" || fileExt == ".html" { 385 | // Read data from file 386 | data, err := os.ReadFile(path) 387 | fileName := filepath.Base(path) 388 | Log("Converting " + fileName) 389 | if err != nil { 390 | Fatal(err.Error()) 391 | } 392 | // Split config and markdown 393 | var configStr, contentStr string 394 | content := strings.TrimSpace(string(data)) 395 | parseAry := strings.SplitN(content, "---", 3) 396 | parseLen := len(parseAry) 397 | if parseLen == 3 { // Jekyll 398 | configStr = parseAry[1] 399 | contentStr = parseAry[2] 400 | } else if parseLen == 2 { // Hexo 401 | configStr = parseAry[0] 402 | contentStr = parseAry[1] 403 | } 404 | // Parse config 405 | var article ArticleConfig 406 | if err = yaml.Unmarshal([]byte(configStr), &article); err != nil { 407 | Fatal(err.Error()) 408 | } 409 | tags := make(map[string]bool) 410 | for _, t := range article.Tags { 411 | tags[t] = true 412 | } 413 | for _, c := range article.Categories { 414 | if _, ok := tags[c]; !ok { 415 | article.Tags = append(article.Tags, c) 416 | } 417 | } 418 | if article.Author == "" { 419 | article.Author = "me" 420 | } 421 | // Convert date 422 | dateAry := strings.SplitN(article.Date, ".", 2) 423 | if len(dateAry) == 2 { 424 | article.Date = dateAry[0] 425 | } 426 | if len(article.Date) == 10 { 427 | article.Date = article.Date + " 00:00:00" 428 | } 429 | if len(article.Date) == 0 { 430 | article.Date = "1970-01-01 00:00:00" 431 | } 432 | article.Update = "" 433 | // Generate Config 434 | var inkConfig []byte 435 | if inkConfig, err = yaml.Marshal(article); err != nil { 436 | Fatal(err.Error()) 437 | } 438 | inkConfigStr := string(inkConfig) 439 | markdownStr := inkConfigStr + "\n\n---\n\n" + contentStr + "\n" 440 | targetName := "source/" + fileName 441 | if fileExt != ".md" { 442 | targetName = targetName + ".md" 443 | } 444 | os.WriteFile(filepath.Join(rootPath, targetName), []byte(markdownStr), 0644) 445 | count++ 446 | } 447 | return nil 448 | }) 449 | fmt.Printf("\nConvert finish, total %v articles\n", count) 450 | } 451 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "time" 10 | 11 | "gopkg.in/yaml.v2" 12 | 13 | gomk "github.com/gomarkdown/markdown" 14 | "github.com/gomarkdown/markdown/ast" 15 | "github.com/gomarkdown/markdown/html" 16 | "github.com/gomarkdown/markdown/parser" 17 | ) 18 | 19 | type SiteConfig struct { 20 | Root string 21 | Title string 22 | Subtitle string 23 | Logo string 24 | Limit int 25 | Theme string 26 | Comment string 27 | Lang string 28 | Url string 29 | Link string 30 | Config interface{} 31 | } 32 | 33 | type AuthorConfig struct { 34 | Id string 35 | Name string 36 | Intro string 37 | Avatar string 38 | } 39 | 40 | type BuildConfig struct { 41 | Output string 42 | Port string 43 | Watch bool 44 | Copy []string 45 | Publish string 46 | } 47 | 48 | type GlobalConfig struct { 49 | I18n map[string]string 50 | Site SiteConfig 51 | Authors map[string]AuthorConfig 52 | Build BuildConfig 53 | Develop bool 54 | } 55 | 56 | type ArticleConfig struct { 57 | Title string 58 | Date string 59 | Update string 60 | Author string 61 | Tags []string 62 | Categories []string 63 | Topic string 64 | Cover string 65 | Draft bool 66 | Preview template.HTML 67 | Top bool 68 | Type string 69 | Hide bool 70 | Toc bool 71 | Image string 72 | Subtitle string 73 | Config map[string]interface{} 74 | } 75 | 76 | type Article struct { 77 | GlobalConfig 78 | ArticleConfig 79 | Time time.Time 80 | MTime time.Time 81 | Date int64 82 | Update int64 83 | Author AuthorConfig 84 | Category string 85 | Tags []string 86 | Markdown string 87 | Preview template.HTML 88 | Content template.HTML 89 | Link string 90 | Config interface{} 91 | Image string 92 | Subtitle string 93 | } 94 | 95 | type ThemeConfig struct { 96 | Copy []string 97 | Lang map[string]map[string]string 98 | } 99 | 100 | const ( 101 | CONFIG_SPLIT = "---" 102 | MORE_SPLIT = "" 103 | ) 104 | 105 | // Modify the rendering of the image node to use jQuery-unveil to lazy load images. 106 | // 107 | // Note: This is a hacky way to do this, but it works for now. 108 | // LazyLoadImages in markdown.html.Flags should not be used with this hook, and 109 | // adding the absolute path prefix to the image destination (which is what the original parser does) 110 | // is not implemented yet. 111 | func renderHookLazyLoadImage(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) { 112 | // Skip all nodes that are not Image nodes 113 | if _, ok := node.(*ast.Image); !ok { 114 | return ast.GoToNext, false 115 | } 116 | img := node.(*ast.Image) 117 | if entering { 118 | if img.Attribute == nil { 119 | img.Attribute = &ast.Attribute{} 120 | } 121 | if img.Attrs == nil { 122 | img.Attrs = make(map[string][]byte) 123 | } 124 | img.Attrs["data-src"] = img.Destination 125 | img.Destination = []byte("data:image/gif;base64,R0lGODlhGAAYAPQAAP///wAAAM7Ozvr6+uDg4LCwsOjo6I6OjsjIyJycnNjY2KioqMDAwPLy8nd3d4aGhri4uGlpaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkHAAAAIf4aQ3JlYXRlZCB3aXRoIGFqYXhsb2FkLmluZm8AIf8LTkVUU0NBUEUyLjADAQAAACwAAAAAGAAYAAAFriAgjiQAQWVaDgr5POSgkoTDjFE0NoQ8iw8HQZQTDQjDn4jhSABhAAOhoTqSDg7qSUQwxEaEwwFhXHhHgzOA1xshxAnfTzotGRaHglJqkJcaVEqCgyoCBQkJBQKDDXQGDYaIioyOgYSXA36XIgYMBWRzXZoKBQUMmil0lgalLSIClgBpO0g+s26nUWddXyoEDIsACq5SsTMMDIECwUdJPw0Mzsu0qHYkw72bBmozIQAh+QQJBwAAACwAAAAAGAAYAAAFsCAgjiTAMGVaDgR5HKQwqKNxIKPjjFCk0KNXC6ATKSI7oAhxWIhezwhENTCQEoeGCdWIPEgzESGxEIgGBWstEW4QCGGAIJEoxGmGt5ZkgCRQQHkGd2CESoeIIwoMBQUMP4cNeQQGDYuNj4iSb5WJnmeGng0CDGaBlIQEJziHk3sABidDAHBgagButSKvAAoyuHuUYHgCkAZqebw0AgLBQyyzNKO3byNuoSS8x8OfwIchACH5BAkHAAAALAAAAAAYABgAAAW4ICCOJIAgZVoOBJkkpDKoo5EI43GMjNPSokXCINKJCI4HcCRIQEQvqIOhGhBHhUTDhGo4diOZyFAoKEQDxra2mAEgjghOpCgz3LTBIxJ5kgwMBShACREHZ1V4Kg1rS44pBAgMDAg/Sw0GBAQGDZGTlY+YmpyPpSQDiqYiDQoCliqZBqkGAgKIS5kEjQ21VwCyp76dBHiNvz+MR74AqSOdVwbQuo+abppo10ssjdkAnc0rf8vgl8YqIQAh+QQJBwAAACwAAAAAGAAYAAAFrCAgjiQgCGVaDgZZFCQxqKNRKGOSjMjR0qLXTyciHA7AkaLACMIAiwOC1iAxCrMToHHYjWQiA4NBEA0Q1RpWxHg4cMXxNDk4OBxNUkPAQAEXDgllKgMzQA1pSYopBgonCj9JEA8REQ8QjY+RQJOVl4ugoYssBJuMpYYjDQSliwasiQOwNakALKqsqbWvIohFm7V6rQAGP6+JQLlFg7KDQLKJrLjBKbvAor3IKiEAIfkECQcAAAAsAAAAABgAGAAABbUgII4koChlmhokw5DEoI4NQ4xFMQoJO4uuhignMiQWvxGBIQC+AJBEUyUcIRiyE6CR0CllW4HABxBURTUw4nC4FcWo5CDBRpQaCoF7VjgsyCUDYDMNZ0mHdwYEBAaGMwwHDg4HDA2KjI4qkJKUiJ6faJkiA4qAKQkRB3E0i6YpAw8RERAjA4tnBoMApCMQDhFTuySKoSKMJAq6rD4GzASiJYtgi6PUcs9Kew0xh7rNJMqIhYchACH5BAkHAAAALAAAAAAYABgAAAW0ICCOJEAQZZo2JIKQxqCOjWCMDDMqxT2LAgELkBMZCoXfyCBQiFwiRsGpku0EshNgUNAtrYPT0GQVNRBWwSKBMp98P24iISgNDAS4ipGA6JUpA2WAhDR4eWM/CAkHBwkIDYcGiTOLjY+FmZkNlCN3eUoLDmwlDW+AAwcODl5bYl8wCVYMDw5UWzBtnAANEQ8kBIM0oAAGPgcREIQnVloAChEOqARjzgAQEbczg8YkWJq8nSUhACH5BAkHAAAALAAAAAAYABgAAAWtICCOJGAYZZoOpKKQqDoORDMKwkgwtiwSBBYAJ2owGL5RgxBziQQMgkwoMkhNqAEDARPSaiMDFdDIiRSFQowMXE8Z6RdpYHWnEAWGPVkajPmARVZMPUkCBQkJBQINgwaFPoeJi4GVlQ2Qc3VJBQcLV0ptfAMJBwdcIl+FYjALQgimoGNWIhAQZA4HXSpLMQ8PIgkOSHxAQhERPw7ASTSFyCMMDqBTJL8tf3y2fCEAIfkECQcAAAAsAAAAABgAGAAABa8gII4k0DRlmg6kYZCoOg5EDBDEaAi2jLO3nEkgkMEIL4BLpBAkVy3hCTAQKGAznM0AFNFGBAbj2cA9jQixcGZAGgECBu/9HnTp+FGjjezJFAwFBQwKe2Z+KoCChHmNjVMqA21nKQwJEJRlbnUFCQlFXlpeCWcGBUACCwlrdw8RKGImBwktdyMQEQciB7oACwcIeA4RVwAODiIGvHQKERAjxyMIB5QlVSTLYLZ0sW8hACH5BAkHAAAALAAAAAAYABgAAAW0ICCOJNA0ZZoOpGGQrDoOBCoSxNgQsQzgMZyIlvOJdi+AS2SoyXrK4umWPM5wNiV0UDUIBNkdoepTfMkA7thIECiyRtUAGq8fm2O4jIBgMBA1eAZ6Knx+gHaJR4QwdCMKBxEJRggFDGgQEREPjjAMBQUKIwIRDhBDC2QNDDEKoEkDoiMHDigICGkJBS2dDA6TAAnAEAkCdQ8ORQcHTAkLcQQODLPMIgIJaCWxJMIkPIoAt3EhACH5BAkHAAAALAAAAAAYABgAAAWtICCOJNA0ZZoOpGGQrDoOBCoSxNgQsQzgMZyIlvOJdi+AS2SoyXrK4umWHM5wNiV0UN3xdLiqr+mENcWpM9TIbrsBkEck8oC0DQqBQGGIz+t3eXtob0ZTPgNrIwQJDgtGAgwCWSIMDg4HiiUIDAxFAAoODwxDBWINCEGdSTQkCQcoegADBaQ6MggHjwAFBZUFCm0HB0kJCUy9bAYHCCPGIwqmRq0jySMGmj6yRiEAIfkECQcAAAAsAAAAABgAGAAABbIgII4k0DRlmg6kYZCsOg4EKhLE2BCxDOAxnIiW84l2L4BLZKipBopW8XRLDkeCiAMyMvQAA+uON4JEIo+vqukkKQ6RhLHplVGN+LyKcXA4Dgx5DWwGDXx+gIKENnqNdzIDaiMECwcFRgQCCowiCAcHCZIlCgICVgSfCEMMnA0CXaU2YSQFoQAKUQMMqjoyAglcAAyBAAIMRUYLCUkFlybDeAYJryLNk6xGNCTQXY0juHghACH5BAkHAAAALAAAAAAYABgAAAWzICCOJNA0ZVoOAmkY5KCSSgSNBDE2hDyLjohClBMNij8RJHIQvZwEVOpIekRQJyJs5AMoHA+GMbE1lnm9EcPhOHRnhpwUl3AsknHDm5RN+v8qCAkHBwkIfw1xBAYNgoSGiIqMgJQifZUjBhAJYj95ewIJCQV7KYpzBAkLLQADCHOtOpY5PgNlAAykAEUsQ1wzCgWdCIdeArczBQVbDJ0NAqyeBb64nQAGArBTt8R8mLuyPyEAOw==") 126 | } else { 127 | w.Write([]byte("\" data-src=\"")) 128 | w.Write(img.Attrs["data-src"]) 129 | } 130 | return ast.GoToNext, false 131 | } 132 | 133 | func ParseMarkdown(markdown string, toc bool) template.HTML { 134 | extensions := parser.CommonExtensions | parser.Footnotes 135 | parser := parser.NewWithExtensions(extensions) 136 | 137 | htmlFlags := html.CommonFlags 138 | if toc { 139 | htmlFlags |= html.TOC 140 | } 141 | opts := html.RendererOptions{Flags: htmlFlags, RenderNodeHook: renderHookLazyLoadImage} 142 | renderer := html.NewRenderer(opts) 143 | 144 | return template.HTML(gomk.ToHTML(gomk.NormalizeNewlines([]byte(markdown)), parser, renderer)) 145 | } 146 | 147 | func ReplaceRootFlag(content string) string { 148 | return strings.Replace(content, "-/", globalConfig.Site.Root+"/", -1) 149 | } 150 | 151 | func ParseGlobalConfig(configPath string, develop bool) (*GlobalConfig, *ThemeConfig) { 152 | var config *GlobalConfig 153 | // Parse Global Config 154 | data, err := os.ReadFile(configPath) 155 | if err != nil { 156 | return nil, nil 157 | } 158 | if err = yaml.Unmarshal(data, &config); err != nil { 159 | Fatal(err.Error()) 160 | } 161 | if config.Site.Config == nil { 162 | config.Site.Config = "" 163 | } 164 | config.Develop = develop 165 | if develop { 166 | config.Site.Root = "" 167 | } 168 | config.Site.Logo = strings.Replace(config.Site.Logo, "-/", config.Site.Root+"/", -1) 169 | if config.Site.Url != "" && strings.HasSuffix(config.Site.Url, "/") { 170 | config.Site.Url = strings.TrimSuffix(config.Site.Url, "/") 171 | } 172 | if config.Build.Output == "" { 173 | config.Build.Output = "public" 174 | } 175 | // Parse Theme Config 176 | themeConfig := ParseThemeConfig(filepath.Join(rootPath, config.Site.Theme, "config.yml")) 177 | for _, copyItem := range themeConfig.Copy { 178 | config.Build.Copy = append(config.Build.Copy, filepath.Join(config.Site.Theme, copyItem)) 179 | } 180 | config.I18n = make(map[string]string) 181 | for item, langItem := range themeConfig.Lang { 182 | config.I18n[item] = langItem[config.Site.Lang] 183 | } 184 | return config, themeConfig 185 | } 186 | 187 | func ParseThemeConfig(configPath string) *ThemeConfig { 188 | // Read data from file 189 | var themeConfig *ThemeConfig 190 | data, err := os.ReadFile(configPath) 191 | if err != nil { 192 | Fatal(err.Error()) 193 | } 194 | // Parse config content 195 | if err := yaml.Unmarshal(data, &themeConfig); err != nil { 196 | Fatal(err.Error()) 197 | } 198 | return themeConfig 199 | } 200 | 201 | func ParseArticleConfig(markdownPath string) (config *ArticleConfig, content string) { 202 | var configStr string 203 | // Read data from file 204 | data, err := os.ReadFile(markdownPath) 205 | if err != nil { 206 | Fatal(err.Error()) 207 | } 208 | // Split config and markdown 209 | contentStr := string(data) 210 | contentStr = ReplaceRootFlag(contentStr) 211 | markdownStr := strings.SplitN(contentStr, CONFIG_SPLIT, 2) 212 | contentLen := len(markdownStr) 213 | if contentLen > 0 { 214 | configStr = markdownStr[0] 215 | } 216 | if contentLen > 1 { 217 | content = markdownStr[1] 218 | } 219 | // Parse config content 220 | if err := yaml.Unmarshal([]byte(configStr), &config); err != nil { 221 | Error(err.Error()) 222 | return nil, "" 223 | } 224 | if config == nil { 225 | return nil, "" 226 | } 227 | if config.Type == "" { 228 | config.Type = "post" 229 | } 230 | // Parse preview splited by MORE_SPLIT 231 | previewAry := strings.SplitN(content, MORE_SPLIT, 2) 232 | if len(config.Preview) <= 0 && len(previewAry) > 1 { 233 | config.Preview = ParseMarkdown(previewAry[0], false) 234 | content = strings.Replace(content, MORE_SPLIT, "", 1) 235 | } else { 236 | config.Preview = ParseMarkdown(string(config.Preview), false) 237 | } 238 | return config, content 239 | } 240 | 241 | func ParseArticle(markdownPath string) *Article { 242 | config, content := ParseArticleConfig(markdownPath) 243 | if config == nil { 244 | Error("Invalid format: " + markdownPath) 245 | return nil 246 | } 247 | if config.Config == nil { 248 | config.Config = make(map[string]interface{}) 249 | } 250 | var article Article 251 | // Parse markdown content 252 | article.Hide = config.Hide 253 | article.Type = config.Type 254 | article.Preview = config.Preview 255 | article.Config = config.Config 256 | article.Markdown = content 257 | article.Content = ParseMarkdown(content, config.Toc) 258 | if config.Date != "" { 259 | article.Time = ParseDate(config.Date) 260 | article.Date = article.Time.Unix() 261 | } 262 | if config.Update != "" { 263 | article.MTime = ParseDate(config.Update) 264 | article.Update = article.MTime.Unix() 265 | } 266 | article.Title = config.Title 267 | article.Topic = config.Topic 268 | article.Draft = config.Draft 269 | article.Top = config.Top 270 | article.Image = config.Image 271 | article.Subtitle = config.Subtitle 272 | if author, ok := globalConfig.Authors[config.Author]; ok { 273 | author.Id = config.Author 274 | author.Avatar = ReplaceRootFlag(author.Avatar) 275 | article.Author = author 276 | } 277 | if len(config.Categories) > 0 { 278 | article.Category = config.Categories[0] 279 | } else { 280 | article.Category = "misc" 281 | } 282 | tags := map[string]bool{} 283 | article.Tags = config.Tags 284 | for _, tag := range config.Tags { 285 | tags[tag] = true 286 | } 287 | for _, cat := range config.Categories { 288 | if _, ok := tags[cat]; !ok { 289 | article.Tags = append(article.Tags, cat) 290 | } 291 | } 292 | // Support topic and cover field 293 | if config.Cover != "" { 294 | article.Cover = config.Cover 295 | } else { 296 | article.Cover = config.Topic 297 | } 298 | // Generate page name 299 | fileName := strings.TrimSuffix(strings.ToLower(filepath.Base(markdownPath)), ".md") 300 | link := fileName + ".html" 301 | // Genetate custom link 302 | if article.Type == "post" { 303 | datePrefix := article.Time.Format("2006-01-02-") 304 | fileName = strings.TrimPrefix(fileName, datePrefix) 305 | if globalConfig.Site.Link != "" { 306 | linkMap := map[string]string{ 307 | "{year}": article.Time.Format("2006"), 308 | "{month}": article.Time.Format("01"), 309 | "{day}": article.Time.Format("02"), 310 | "{hour}": article.Time.Format("15"), 311 | "{minute}": article.Time.Format("04"), 312 | "{second}": article.Time.Format("05"), 313 | "{category}": article.Category, 314 | "{title}": fileName, 315 | } 316 | link = globalConfig.Site.Link 317 | for key, val := range linkMap { 318 | link = strings.Replace(link, key, val, -1) 319 | } 320 | } 321 | } 322 | article.Link = link 323 | article.GlobalConfig = *globalConfig 324 | return &article 325 | } 326 | -------------------------------------------------------------------------------- /template/theme/bundle/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original style from softwaremaniacs.org (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #f0f0f0; 12 | } 13 | 14 | .hljs, 15 | .hljs-subst, 16 | .hljs-tag .hljs-title { 17 | color: black; 18 | } 19 | 20 | .hljs-string, 21 | .hljs-title, 22 | .hljs-section, 23 | .hljs-attribute, 24 | .hljs-selector-id, 25 | .hljs-selector-class, 26 | .hljs-variable, 27 | .hljs-template-variable, 28 | .hljs-symbol, 29 | .hljs-deletion { 30 | color: #800; 31 | } 32 | 33 | .hljs-comment, 34 | .hljs-quote { 35 | color: #888; 36 | } 37 | 38 | .hljs-number, 39 | .hljs-regexp, 40 | .hljs-literal, 41 | .hljs-addition, 42 | .hljs-bullet, 43 | .hljs-link { 44 | color: #080; 45 | } 46 | 47 | .hljs-meta { 48 | color: #88f; 49 | } 50 | 51 | .hljs-keyword, 52 | .hljs-selector-tag, 53 | .hljs-title, 54 | .hljs-name, 55 | .hljs-section, 56 | .hljs-built_in, 57 | .hljs-doctag, 58 | .hljs-type, 59 | .hljs-strong { 60 | font-weight: bold; 61 | } 62 | 63 | .hljs-emphasis { 64 | font-style: italic; 65 | } 66 | 67 | @define-mixin font-main { 68 | font-family: "Helvetica Neue", Arial, "Hiragino Sans GB", "WenQuanYi Micro Hei", "Microsoft YaHei", sans-serif; 69 | } 70 | 71 | @define-mixin font-code { 72 | font-family: consolas, monaco, "Source Code Pro", hack, monospace; 73 | } 74 | 75 | html, body, input { 76 | margin: 0; 77 | } 78 | 79 | html, body { 80 | height: 100%; 81 | } 82 | 83 | ul { 84 | list-style: none; 85 | margin: 0; 86 | padding: 0; 87 | } 88 | 89 | a { 90 | text-decoration: none 91 | } 92 | 93 | a:hover { 94 | opacity: 0.9; 95 | } 96 | 97 | *::-moz-selection { 98 | background-color: #293846; 99 | color: #fcfcfc; 100 | } 101 | 102 | *::selection { 103 | background-color: #293846; 104 | color: #fcfcfc; 105 | } 106 | 107 | /*clearfix*/ 108 | 109 | .clearfix:before, .clearfix:after { 110 | content: " "; 111 | display: table; 112 | } 113 | 114 | .clearfix:after { 115 | clear: both; 116 | } 117 | 118 | .clearfix { 119 | zoom: 1; 120 | } 121 | 122 | .main.page { 123 | padding-top: 100px; 124 | padding-bottom: 70px; 125 | } 126 | 127 | .main.page .header { 128 | text-align: center; 129 | } 130 | 131 | .main.page .header .tag { 132 | margin: 0; 133 | margin-top: 30px; 134 | font-weight: bold; 135 | font-size: 32px; 136 | } 137 | 138 | .main.page .header .tag-sub { 139 | margin: 0; 140 | font-weight: normal; 141 | margin-top: 10px; 142 | font-size: 16px; 143 | color: #7f8c8d; 144 | } 145 | 146 | .main.page .header .search-wrap { 147 | position: relative; 148 | max-width: 300px; 149 | margin: 0 auto; 150 | margin-top: 50px; 151 | } 152 | 153 | .main.page .header .search-wrap .search { 154 | padding: 10px 25px; 155 | border: 1px solid #ddd; 156 | border-radius: 100px; 157 | width: 100%; 158 | -webkit-box-sizing: border-box; 159 | box-sizing: border-box; 160 | opacity: 0.8 161 | } 162 | 163 | .main.page .header .search-wrap .search:focus { 164 | outline: 0; 165 | opacity: 1; 166 | } 167 | 168 | .main.page .header .search-wrap .icon { 169 | position: absolute; 170 | right: 10px; 171 | top: 5px; 172 | opacity: 0.3; 173 | } 174 | 175 | .main.page .article-list { 176 | margin-top: 100px; 177 | } 178 | 179 | .main.page .article-list .searched { 180 | background-color: #009a61; 181 | color: #fcfcfc; 182 | } 183 | 184 | .main.page .article-list .article { 185 | margin-bottom: 55px; 186 | padding-bottom: 45px; 187 | border-bottom: 1px #ddd dashed 188 | } 189 | 190 | .main.page .article-list .article:last-child { 191 | border-bottom: 0; 192 | } 193 | 194 | .main.page .article-list .article .title { 195 | font-size: 26px; 196 | font-weight: bold; 197 | margin-bottom: 5px; 198 | text-decoration: none; 199 | color: #293846; 200 | display: block; 201 | } 202 | 203 | .main.page .article-list .article .title .top { 204 | color: #7f8c8d; 205 | margin-right: 5px; 206 | } 207 | 208 | .main.page .article-list .article .cover { 209 | color: #eee; 210 | width: 100%; 211 | height: 280px; 212 | background-size: cover; 213 | background-position: center; 214 | background-repeat: no-repeat; 215 | border-radius: 6px; 216 | border: 1px solid #ddd; 217 | -webkit-box-sizing: border-box; 218 | box-sizing: border-box; 219 | margin-top: 15px 220 | } 221 | 222 | @media (max-width: 414px) { 223 | .main.page .article-list .article .cover { 224 | height: 180px 225 | } 226 | } 227 | 228 | .main.page .article-list .article .preview { 229 | display: inline-block; 230 | line-height: 28px; 231 | font-size: 16px; 232 | margin-top: 10px; 233 | color: #293846; 234 | } 235 | 236 | .main.page .article-list .article .info { 237 | margin-top: 15px; 238 | margin-bottom: 20px; 239 | color: #7f8c8d; 240 | font-size: 14px; 241 | } 242 | 243 | .main.page .article-list .article .avatar { 244 | width: 20px; 245 | height: 20px; 246 | margin-right: 10px; 247 | border-radius: 20px; 248 | background-size: cover; 249 | background-repeat: no-repeat; 250 | float: left; 251 | margin-top: -4px; 252 | border: 1px #ddd solid; 253 | } 254 | 255 | .main.page .article-list .article .name { 256 | margin-right: 10px; 257 | } 258 | 259 | .main.page .article-list .article .date { 260 | color: #7f8c8d; 261 | } 262 | 263 | .main.page .article-list .article .tags { 264 | margin-left: 10px; 265 | float: right; 266 | } 267 | 268 | .main.page .article-list .article .tags .tag { 269 | margin-right: 5px; 270 | color: #7f8c8d; 271 | text-decoration: none; 272 | } 273 | 274 | .main.page .article-list .empty { 275 | text-align: center; 276 | font-size: 24px; 277 | max-width: 800px; 278 | overflow: hidden; 279 | -o-text-overflow: ellipsis; 280 | text-overflow: ellipsis; 281 | } 282 | 283 | .main.page .article-list .empty span { 284 | font-weight: bold; 285 | } 286 | 287 | .main.page .page-nav { 288 | position: relative; 289 | height: 50px; 290 | font-size: 14px; 291 | } 292 | 293 | .main.page .page-nav a { 294 | color: #7f8c8d; 295 | font-style: italic; 296 | text-decoration: none; 297 | } 298 | 299 | .main.page .page-nav .nav { 300 | left: 0; 301 | right: 0; 302 | text-align: center; 303 | display: block; 304 | color: #7f8c8d; 305 | position: absolute; 306 | z-index: -1; 307 | } 308 | 309 | .main.page .page-nav .prev { 310 | float: left 311 | } 312 | 313 | .main.page .page-nav .prev:before { 314 | content: ""; 315 | width: 3px; 316 | height: 3px; 317 | border: 2px #7f8c8d solid; 318 | border-radius: 3px; 319 | display: block; 320 | float: left; 321 | margin-right: 7px; 322 | margin-top: 5px; 323 | } 324 | 325 | .main.page .page-nav .next { 326 | float: right 327 | } 328 | 329 | .main.page .page-nav .next:after { 330 | content: ""; 331 | width: 3px; 332 | height: 3px; 333 | border: 2px #7f8c8d solid; 334 | border-radius: 3px; 335 | display: block; 336 | float: right; 337 | margin-left: 7px; 338 | margin-top: 5px; 339 | } 340 | 341 | .main.article { 342 | padding-top: 120px; 343 | padding-bottom: 120px; 344 | } 345 | 346 | .main.article .title { 347 | margin: 0; 348 | font-weight: bold; 349 | color: #293846; 350 | font-size: 30px; 351 | line-height: 42px; 352 | } 353 | 354 | .main.article .info { 355 | margin-top: 10px; 356 | font-size: 14px; 357 | } 358 | 359 | .main.article .info .avatar { 360 | width: 20px; 361 | height: 20px; 362 | margin-right: 10px; 363 | border-radius: 20px; 364 | background-size: cover; 365 | background-repeat: no-repeat; 366 | float: left; 367 | margin-top: -4px; 368 | border: 1px #ddd solid; 369 | } 370 | 371 | .main.article .info .name { 372 | margin-right: 5px; 373 | } 374 | 375 | .main.article .info .date { 376 | color: #7f8c8d; 377 | margin-right: 5px; 378 | } 379 | 380 | .main.article .info .tags .tag { 381 | margin-right: 5px; 382 | color: #7f8c8d; 383 | text-decoration: none; 384 | } 385 | 386 | .main.article .recommend { 387 | margin-top: 50px; 388 | overflow: auto; 389 | } 390 | 391 | .main.article .recommend .nav { 392 | width: 100%; 393 | } 394 | 395 | .main.article .recommend .nav .head { 396 | font-size: 12px; 397 | font-style: italic; 398 | margin-bottom: 10px; 399 | color: #7f8c8d; 400 | text-align: center; 401 | } 402 | 403 | .main.article .recommend .nav .link { 404 | color: #293846; 405 | text-align: center; 406 | display: block; 407 | } 408 | 409 | .main.article .recommend .nav.prev.more { 410 | width: 45%; 411 | text-align: left; 412 | float: left; 413 | } 414 | 415 | .main.article .recommend .nav.prev.more .head, .main.article .recommend .nav.prev.more .link { 416 | text-align: left; 417 | } 418 | 419 | .main.article .recommend .nav.next.more { 420 | width: 45%; 421 | text-align: right; 422 | float: right; 423 | } 424 | 425 | .main.article .recommend .nav.next.more .head, .main.article .recommend .nav.next.more .link { 426 | text-align: right; 427 | } 428 | 429 | .main.article .author { 430 | margin-top: 36px; 431 | padding-top: 40px; 432 | text-align: center; 433 | } 434 | 435 | .main.article .author .avatar { 436 | margin: 0 auto; 437 | width: 70px; 438 | height: 70px; 439 | border-radius: 70px; 440 | background-size: cover; 441 | background-repeat: no-repeat; 442 | border: 20px white solid 443 | } 444 | 445 | .main.article .author .avatar:before { 446 | border-top: 1px #ddd dashed; 447 | content: ""; 448 | width: 100%; 449 | position: absolute; 450 | left: 0; 451 | right: 0; 452 | margin-top: 36px; 453 | z-index: -1; 454 | } 455 | 456 | .main.article .author .name { 457 | margin-top: 20px; 458 | font-weight: bold; 459 | font-size: 16px; 460 | } 461 | 462 | .main.article .author .intro { 463 | font-style: italic; 464 | margin-top: 8px; 465 | font-size: 14px; 466 | color: #7f8c8d; 467 | } 468 | 469 | .main.article #disqus_thread { 470 | margin-top: 50px; 471 | } 472 | 473 | .main.archive, .main.tag { 474 | padding-top: 100px; 475 | padding-bottom: 100px; 476 | } 477 | 478 | .main.archive a, .main.tag a { 479 | color: #009a61; 480 | text-decoration: none; 481 | border-bottom: 1px dashed #009a61; 482 | } 483 | 484 | .main.archive .site .title, .main.archive .site .subtitle, .main.tag .site .title, .main.tag .site .subtitle { 485 | text-align: left; 486 | } 487 | 488 | .main.archive .header, .main.tag .header { 489 | padding-top: 60px; 490 | font-size: 18px; 491 | } 492 | 493 | .main.archive .header .title, .main.tag .header .title { 494 | margin-top: 30px; 495 | margin-right: 5px; 496 | font-weight: bold; 497 | } 498 | 499 | .main.archive .header .subtitle, .main.tag .header .subtitle { 500 | font-style: italic; 501 | } 502 | 503 | .main.archive .archive-list, .main.tag .archive-list { 504 | margin-top: 50px; 505 | } 506 | 507 | .main.archive .archive-list .archive-item, .main.tag .archive-list .archive-item { 508 | margin: 40px 0; 509 | } 510 | 511 | .main.archive .archive-list .archive-item .archive-year, .main.tag .archive-list .archive-item .archive-year { 512 | margin: 15px 0; 513 | color: #293846; 514 | font-weight: bold; 515 | } 516 | 517 | .main.archive .archive-list .archive-item .article-list .article-item, .main.tag .archive-list .archive-item .article-list .article-item { 518 | margin-bottom: 10px; 519 | } 520 | 521 | .main.archive .archive-list .archive-item .article-list .article-item .date, .main.tag .archive-list .archive-item .article-list .article-item .date { 522 | color: #7f8c8d; 523 | margin-right: 20px; 524 | font-style: italic; 525 | } 526 | 527 | .main.archive .tag-list, .main.tag .tag-list { 528 | margin-top: 50px; 529 | } 530 | 531 | .main.archive .tag-list .tag-item, .main.tag .tag-list .tag-item { 532 | margin-right: 10px; 533 | margin-bottom: 25px; 534 | float: left; 535 | } 536 | 537 | .main.archive .tag-list .tag-item .tag-name, .main.tag .tag-list .tag-item .tag-name { 538 | padding: 5px 7px; 539 | border: 1px solid #eee; 540 | border-radius: 3px; 541 | font-size: 14px 542 | } 543 | 544 | .main.archive .tag-list .tag-item .tag-name:hover, .main.tag .tag-list .tag-item .tag-name:hover { 545 | background-color: #293846; 546 | color: #fcfcfc; 547 | border: 1px solid #293846; 548 | } 549 | 550 | .main .content { 551 | margin-top: 70px; 552 | font-size: 16px; 553 | line-height: 1.7; 554 | color: #293846; 555 | } 556 | 557 | .main .content h1 + p, .main .content h2 + p, .main .content h3 + p, .main .content h4 + p, .main .content h5 + p, .main .content h6 + p { 558 | margin-top: 10px; 559 | } 560 | 561 | .main .content h1 { 562 | font-size: 28px; 563 | margin-top: 40px; 564 | margin-bottom: 10px; 565 | } 566 | 567 | .main .content h2 { 568 | font-size: 24px; 569 | margin-top: 40px; 570 | margin-bottom: 10px; 571 | } 572 | 573 | .main .content h3 { 574 | font-size: 18px; 575 | margin-top: 40px; 576 | margin-bottom: 10px; 577 | } 578 | 579 | .main .content p { 580 | margin-bottom: 30px; 581 | margin-top: 30px; 582 | text-align: left; 583 | } 584 | 585 | .main .content.preview p { 586 | margin-bottom: 10px; 587 | margin-top: 10px; 588 | } 589 | 590 | .main .content a { 591 | color: #009a61; 592 | text-decoration: none; 593 | border-bottom: 1px dashed #009a61 594 | } 595 | 596 | .main .content a:hover { 597 | color: #004e31; 598 | border-bottom: 1px dashed #004e31; 599 | } 600 | 601 | .main .content code { 602 | font-size: 14px; 603 | padding: 1px 4px; 604 | border-radius: 3px; 605 | margin: 0px 3px; 606 | background-color: #f7f7f7; 607 | color: #009a61; 608 | } 609 | 610 | .main .content pre { 611 | margin: 0; 612 | } 613 | 614 | .main .content pre code { 615 | color: inherit; 616 | font-size: 14px; 617 | margin: 0; 618 | padding: 10px 15px; 619 | border-radius: 6px; 620 | border: 2px dashed #eee; 621 | background-color: #fcfcfc; 622 | display: block; 623 | overflow: auto; 624 | } 625 | 626 | .main .content blockquote { 627 | border-left: 4px #009a61 solid; 628 | padding: 0px 10px 0px 20px; 629 | margin: 25px 0; 630 | margin-left: -23px; 631 | font-style: italic; 632 | } 633 | 634 | .main .content table { 635 | font-size: 14px; 636 | width: 100%; 637 | border-width: 1px; 638 | border-color: #ddd; 639 | border-collapse: collapse; 640 | } 641 | 642 | .main .content table th { 643 | border-width: 1px; 644 | padding: 5px; 645 | border-style: solid; 646 | border-color: #ddd; 647 | background-color: #eee; 648 | } 649 | 650 | .main .content table td { 651 | border-width: 1px; 652 | padding: 5px; 653 | border-style: solid; 654 | border-color: #ddd; 655 | background-color: #fcfcfc; 656 | } 657 | 658 | .main .content ul { 659 | list-style: circle; 660 | padding-left: 40px; 661 | } 662 | 663 | .main .content ul li { 664 | margin: 5px 0; 665 | } 666 | 667 | .main .content ol { 668 | padding-left: 40px; 669 | } 670 | 671 | .main .content ol li { 672 | margin: 5px 0; 673 | } 674 | 675 | .main .content hr { 676 | margin: 25px 0; 677 | border: 0; 678 | border-top: 1px dashed #ddd; 679 | } 680 | 681 | .main .content img { 682 | margin: 30px auto; 683 | max-width: 100%; 684 | display: block; 685 | opacity: 0.6; 686 | -webkit-transition: opacity .3s ease-in; 687 | -o-transition: opacity .3s ease-in; 688 | transition: opacity .3s ease-in; 689 | cursor: pointer; 690 | border-radius: 5px; 691 | } 692 | 693 | .main .content .image-alt { 694 | text-align: center; 695 | color: #7f8c8d; 696 | font-style: italic; 697 | margin-top: -10px; 698 | margin-bottom: 30px; 699 | } 700 | 701 | .main .content .searched { 702 | background-color: yellow; 703 | } 704 | 705 | .header-wrap { 706 | padding: 15px; 707 | position: absolute; 708 | z-index: 1; 709 | font-weight: bold; 710 | font-size: 18px; 711 | left: 0; 712 | right: 0; 713 | display: -webkit-box; 714 | display: -webkit-flex; 715 | display: -ms-flexbox; 716 | display: flex; 717 | -webkit-box-pack: justify; 718 | -webkit-justify-content: space-between; 719 | -ms-flex-pack: justify; 720 | justify-content: space-between; 721 | -webkit-box-align: center; 722 | -webkit-align-items: center; 723 | -ms-flex-align: center; 724 | align-items: center; 725 | } 726 | 727 | .header-wrap a { 728 | color: #a1b2b4; 729 | } 730 | 731 | .header-wrap .index { 732 | display: -webkit-box; 733 | display: -webkit-flex; 734 | display: -ms-flexbox; 735 | display: flex; 736 | -webkit-box-align: center; 737 | -webkit-align-items: center; 738 | -ms-flex-align: center; 739 | align-items: center; 740 | } 741 | 742 | .header-wrap .index .logo { 743 | width: 30px; 744 | height: 30px; 745 | border-radius: 50%; 746 | border: 1px solid #ddd; 747 | margin-right: 10px; 748 | } 749 | 750 | .header-wrap .menu { 751 | display: -webkit-box; 752 | display: -webkit-flex; 753 | display: -ms-flexbox; 754 | display: flex; 755 | } 756 | 757 | .header-wrap .menu .menu-item { 758 | margin-right: 10px; 759 | } 760 | 761 | a.name { 762 | color: #293846; 763 | } 764 | 765 | .main { 766 | color: #293846; 767 | margin: 0 auto; 768 | max-width: 720px; 769 | padding: 0 15px; 770 | min-height: 100%; 771 | position:relative; 772 | } 773 | 774 | .main .site .logo { 775 | margin: 0 auto; 776 | width: 80px; 777 | height: 80px; 778 | border-radius: 50%; 779 | background-size: cover; 780 | background-repeat: no-repeat; 781 | border: 1px #ddd solid; 782 | } 783 | 784 | .main .site .title { 785 | margin: 0; 786 | margin-top: 30px; 787 | font-weight: bold; 788 | font-size: 32px; 789 | text-align: center; 790 | } 791 | 792 | .main .site .subtitle { 793 | font-style: italic; 794 | margin-top: 10px; 795 | font-size: 16px; 796 | color: #a1b2b4; 797 | text-align: center; 798 | font-weight: normal; 799 | } 800 | 801 | .main.about .info { 802 | margin-top: 80px; 803 | text-align: center; 804 | } 805 | 806 | .container { 807 | height: auto; 808 | min-height: 100%; 809 | padding-bottom: 0; 810 | } 811 | 812 | .footer { 813 | width: 100%; 814 | font-size: 14px; 815 | border-top: 1px #eee solid; 816 | color: #a1b2b4; 817 | background-color: #fcfcfc; 818 | clear: both; 819 | position: relative; 820 | height: 32px; 821 | margin-top: -36px; 822 | } 823 | 824 | .footer span { 825 | padding: 8px 15px; 826 | display: block; 827 | } 828 | 829 | .footer span a { 830 | font-weight: bold; 831 | } 832 | 833 | .footer .copyright { 834 | float: left; 835 | } 836 | 837 | .footer .publish { 838 | float: right; 839 | } 840 | 841 | .footer .publish a { 842 | color: #a1b2b4; 843 | text-decoration: none; 844 | } 845 | 846 | -------------------------------------------------------------------------------- /template/theme/source/js/highlight.pack.js: -------------------------------------------------------------------------------- 1 | /*! highlight.js v8.9.1 | BSD3 License | git.io/hljslicense */ 2 | !function(e){if("undefined"!=typeof exports)e(exports);else if("undefined"!=typeof window)window.hljs=e({}),"function"==typeof define&&define.amd&&define("hljs",[],function(){return window.hljs});else{if("undefined"==typeof self)throw new Error("No global object found to bind hljs variable to.");self.hljs=e({})}}(function(e){function t(e){return e.replace(/&/gm,"&").replace(//gm,">")}function r(e){return e.nodeName.toLowerCase()}function a(e,t){var r=e&&e.exec(t);return r&&0==r.index}function n(e){return/^(no-?highlight|plain|text)$/i.test(e)}function i(e){var t,r,a,i=e.className+" ";if(i+=e.parentNode?e.parentNode.className:"",r=/\blang(?:uage)?-([\w-]+)\b/i.exec(i))return y(r[1])?r[1]:"no-highlight";for(i=i.split(/\s+/),t=0,a=i.length;a>t;t++)if(y(i[t])||n(i[t]))return i[t]}function s(e,t){var r,a={};for(r in e)a[r]=e[r];if(t)for(r in t)a[r]=t[r];return a}function c(e){var t=[];return function a(e,n){for(var i=e.firstChild;i;i=i.nextSibling)3==i.nodeType?n+=i.nodeValue.length:1==i.nodeType&&(t.push({event:"start",offset:n,node:i}),n=a(i,n),r(i).match(/br|hr|img|input/)||t.push({event:"stop",offset:n,node:i}));return n}(e,0),t}function o(e,a,n){function i(){return e.length&&a.length?e[0].offset!=a[0].offset?e[0].offset"}function c(e){u+=""}function o(e){("start"==e.event?s:c)(e.node)}for(var l=0,u="",d=[];e.length||a.length;){var b=i();if(u+=t(n.substr(l,b[0].offset-l)),l=b[0].offset,b==e){d.reverse().forEach(c);do o(b.splice(0,1)[0]),b=i();while(b==e&&b.length&&b[0].offset==l);d.reverse().forEach(s)}else"start"==b[0].event?d.push(b[0].node):d.pop(),o(b.splice(0,1)[0])}return u+t(n.substr(l))}function l(e){function t(e){return e&&e.source||e}function r(r,a){return new RegExp(t(r),"m"+(e.cI?"i":"")+(a?"g":""))}function a(n,i){if(!n.compiled){if(n.compiled=!0,n.k=n.k||n.bK,n.k){var c={},o=function(t,r){e.cI&&(r=r.toLowerCase()),r.split(" ").forEach(function(e){var r=e.split("|");c[r[0]]=[t,r[1]?Number(r[1]):1]})};"string"==typeof n.k?o("keyword",n.k):Object.keys(n.k).forEach(function(e){o(e,n.k[e])}),n.k=c}n.lR=r(n.l||/\b\w+\b/,!0),i&&(n.bK&&(n.b="\\b("+n.bK.split(" ").join("|")+")\\b"),n.b||(n.b=/\B|\b/),n.bR=r(n.b),n.e||n.eW||(n.e=/\B|\b/),n.e&&(n.eR=r(n.e)),n.tE=t(n.e)||"",n.eW&&i.tE&&(n.tE+=(n.e?"|":"")+i.tE)),n.i&&(n.iR=r(n.i)),void 0===n.r&&(n.r=1),n.c||(n.c=[]);var l=[];n.c.forEach(function(e){e.v?e.v.forEach(function(t){l.push(s(e,t))}):l.push("self"==e?n:e)}),n.c=l,n.c.forEach(function(e){a(e,n)}),n.starts&&a(n.starts,i);var u=n.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([n.tE,n.i]).map(t).filter(Boolean);n.t=u.length?r(u.join("|"),!0):{exec:function(){return null}}}}a(e)}function u(e,r,n,i){function s(e,t){for(var r=0;r";return i+=e+'">',i+t+s}function m(){if(!x.k)return t(E);var e="",r=0;x.lR.lastIndex=0;for(var a=x.lR.exec(E);a;){e+=t(E.substr(r,a.index-r));var n=b(x,a);n?(B+=n[1],e+=p(n[0],t(a[0]))):e+=t(a[0]),r=x.lR.lastIndex,a=x.lR.exec(E)}return e+t(E.substr(r))}function f(){var e="string"==typeof x.sL;if(e&&!N[x.sL])return t(E);var r=e?u(x.sL,E,!0,C[x.sL]):d(E,x.sL.length?x.sL:void 0);return x.r>0&&(B+=r.r),e&&(C[x.sL]=r.top),p(r.language,r.value,!1,!0)}function g(){return void 0!==x.sL?f():m()}function h(e,r){var a=e.cN?p(e.cN,"",!0):"";e.rB?(M+=a,E=""):e.eB?(M+=t(r)+a,E=""):(M+=a,E=r),x=Object.create(e,{parent:{value:x}})}function _(e,r){if(E+=e,void 0===r)return M+=g(),0;var a=s(r,x);if(a)return M+=g(),h(a,r),a.rB?0:r.length;var n=c(x,r);if(n){var i=x;i.rE||i.eE||(E+=r),M+=g();do x.cN&&(M+=""),B+=x.r,x=x.parent;while(x!=n.parent);return i.eE&&(M+=t(r)),E="",n.starts&&h(n.starts,""),i.rE?0:r.length}if(o(r,x))throw new Error('Illegal lexeme "'+r+'" for mode "'+(x.cN||"")+'"');return E+=r,r.length||1}var v=y(e);if(!v)throw new Error('Unknown language: "'+e+'"');l(v);var k,x=i||v,C={},M="";for(k=x;k!=v;k=k.parent)k.cN&&(M=p(k.cN,"",!0)+M);var E="",B=0;try{for(var $,z,L=0;;){if(x.t.lastIndex=L,$=x.t.exec(r),!$)break;z=_(r.substr(L,$.index-L),$[0]),L=$.index+z}for(_(r.substr(L)),k=x;k.parent;k=k.parent)k.cN&&(M+="");return{r:B,value:M,language:e,top:x}}catch(R){if(-1!=R.message.indexOf("Illegal"))return{r:0,value:t(r)};throw R}}function d(e,r){r=r||w.languages||Object.keys(N);var a={r:0,value:t(e)},n=a;return r.forEach(function(t){if(y(t)){var r=u(t,e,!1);r.language=t,r.r>n.r&&(n=r),r.r>a.r&&(n=a,a=r)}}),n.language&&(a.second_best=n),a}function b(e){return w.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,t){return t.replace(/\t/g,w.tabReplace)})),w.useBR&&(e=e.replace(/\n/g,"
    ")),e}function p(e,t,r){var a=t?k[t]:r,n=[e.trim()];return e.match(/\bhljs\b/)||n.push("hljs"),-1===e.indexOf(a)&&n.push(a),n.join(" ").trim()}function m(e){var t=i(e);if(!n(t)){var r;w.useBR?(r=document.createElementNS("http://www.w3.org/1999/xhtml","div"),r.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):r=e;var a=r.textContent,s=t?u(t,a,!0):d(a),l=c(r);if(l.length){var m=document.createElementNS("http://www.w3.org/1999/xhtml","div");m.innerHTML=s.value,s.value=o(l,c(m),a)}s.value=b(s.value),e.innerHTML=s.value,e.className=p(e.className,t,s.language),e.result={language:s.language,re:s.r},s.second_best&&(e.second_best={language:s.second_best.language,re:s.second_best.r})}}function f(e){w=s(w,e)}function g(){if(!g.called){g.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,m)}}function h(){addEventListener("DOMContentLoaded",g,!1),addEventListener("load",g,!1)}function _(t,r){var a=N[t]=r(e);a.aliases&&a.aliases.forEach(function(e){k[e]=t})}function v(){return Object.keys(N)}function y(e){return e=(e||"").toLowerCase(),N[e]||N[k[e]]}var w={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},N={},k={};return e.highlight=u,e.highlightAuto=d,e.fixMarkup=b,e.highlightBlock=m,e.configure=f,e.initHighlighting=g,e.initHighlightingOnLoad=h,e.registerLanguage=_,e.listLanguages=v,e.getLanguage=y,e.inherit=s,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(t,r,a){var n=e.inherit({cN:"comment",b:t,e:r,c:[]},a||{});return n.c.push(e.PWM),n.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),n},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.registerLanguage("apache",function(e){var t={cN:"number",b:"[\\$%]\\d+"};return{aliases:["apacheconf"],cI:!0,c:[e.HCM,{cN:"section",b:""},{cN:"attribute",b:/\w+/,r:0,k:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{e:/$/,r:0,k:{literal:"on off all"},c:[{cN:"meta",b:"\\s\\[",e:"\\]$"},{cN:"variable",b:"[\\$%]\\{",e:"\\}",c:["self",t]},t,e.QSM]}}],i:/\S/}}),e.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},r={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]},a={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/-?[a-z\.]+/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,e.NM,r,a,t]}}),e.registerLanguage("coffeescript",function(e){var t={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},r="[A-Za-z$_][0-9A-Za-z$_]*",a={cN:"subst",b:/#\{/,e:/}/,k:t},n=[e.BNM,e.inherit(e.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/"""/,e:/"""/,c:[e.BE,a]},{b:/"/,e:/"/,c:[e.BE,a]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[a,e.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{b:"@"+r},{b:"`",e:"`",eB:!0,eE:!0,sL:"javascript"}];a.c=n;var i=e.inherit(e.TM,{b:r}),s="(\\(.*\\))?\\s*\\B[-=]>",c={cN:"params",b:"\\([^\\(]",rB:!0,c:[{b:/\(/,e:/\)/,k:t,c:["self"].concat(n)}]};return{aliases:["coffee","cson","iced"],k:t,i:/\/\*/,c:n.concat([e.C("###","###"),e.HCM,{cN:"function",b:"^\\s*"+r+"\\s*=\\s*"+s,e:"[-=]>",rB:!0,c:[i,c]},{b:/[:\(,=]\s*/,r:0,c:[{cN:"function",b:s,e:"[-=]>",rB:!0,c:[c]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:!0,i:/[:="\[\]]/,c:[i]},i]},{b:r+":",e:":",rB:!0,rE:!0,r:0}])}}),e.registerLanguage("cpp",function(e){var t={cN:"keyword",b:"\\b[a-z\\d_]*_t\\b"},r={cN:"string",v:[e.inherit(e.QSM,{b:'((u8?|U)|L)?"'}),{b:'(u8?|U)?R"',e:'"',c:[e.BE]},{b:"'\\\\?.",e:"'",i:"."}]},a={cN:"number",v:[{b:"\\b(\\d+(\\.\\d*)?|\\.\\d+)(u|U|l|L|ul|UL|f|F)"},{b:e.CNR}],r:0},n={cN:"meta",b:"#",e:"$",k:"if else elif endif define undef warning error line pragma ifdef ifndef",c:[{b:/\\\n/,r:0},{bK:"include",e:"$",c:[r,{cN:"string",b:"<",e:">",i:"\\n"}]},r,a,e.CLCM,e.CBCM]},i=e.IR+"\\s*\\(",s={keyword:"int float while private char catch export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const struct for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using class asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong",built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf",literal:"true false nullptr NULL"};return{aliases:["c","cc","h","c++","h++","hpp"],k:s,i:"",k:s,c:["self",t]},{b:e.IR+"::",k:s},{bK:"new throw return else",r:0},{cN:"function",b:"("+e.IR+"[\\*&\\s]+)+"+i,rB:!0,e:/[{;=]/,eE:!0,k:s,i:/[^\w\s\*&]/,c:[{b:i,rB:!0,c:[e.TM],r:0},{cN:"params",b:/\(/,e:/\)/,k:s,r:0,c:[e.CLCM,e.CBCM,r,a]},e.CLCM,e.CBCM,n]}]}}),e.registerLanguage("cs",function(e){var t="abstract as base bool break byte case catch char checked const continue decimal dynamic default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long null when object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this true try typeof uint ulong unchecked unsafe ushort using virtual volatile void while async protected public private internal ascending descending from get group into join let orderby partial select set value var where yield",r=e.IR+"(<"+e.IR+">)?";return{aliases:["csharp"],k:t,i:/::/,c:[e.C("///","$",{rB:!0,c:[{cN:"doctag",v:[{b:"///",r:0},{b:""},{b:""}]}]}),e.CLCM,e.CBCM,{cN:"meta",b:"#",e:"$",k:"if else elif endif define undef warning error line region endregion pragma checksum"},{cN:"string",b:'@"',e:'"',c:[{b:'""'}]},e.ASM,e.QSM,e.CNM,{bK:"class interface",e:/[{;=]/,i:/[^\s:]/,c:[e.TM,e.CLCM,e.CBCM]},{bK:"namespace",e:/[{;=]/,i:/[^\s:]/,c:[e.inherit(e.TM,{b:"[a-zA-Z](\\.?\\w)*"}),e.CLCM,e.CBCM]},{bK:"new return throw await",r:0},{cN:"function",b:"("+r+"\\s+)+"+e.IR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:t,c:[{b:e.IR+"\\s*\\(",rB:!0,c:[e.TM],r:0},{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]}]}}),e.registerLanguage("css",function(e){var t="[a-zA-Z-][a-zA-Z0-9_-]*",r={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"']+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:t,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,r]}]}}),e.registerLanguage("diff",function(e){return{aliases:["patch"],c:[{cN:"meta",r:10,v:[{b:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"comment",v:[{b:/Index: /,e:/$/},{b:/=====/,e:/=====$/},{b:/^\-\-\-/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+\+\+/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"addition",b:"^\\!",e:"$"}]}}),e.registerLanguage("http",function(e){var t="HTTP/[0-9\\.]+";return{aliases:["https"],i:"\\S",c:[{b:"^"+t,e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{b:"^[A-Z]+ (.*?) "+t+"$",rB:!0,e:"$",c:[{cN:"string",b:" ",e:" ",eB:!0,eE:!0},{b:t},{cN:"keyword",b:"[A-Z]+"}]},{cN:"attribute",b:"^\\w",e:": ",eE:!0,i:"\\n|\\s|=",starts:{e:"$",r:0}},{b:"\\n\\n",starts:{sL:[],eW:!0}}]}}),e.registerLanguage("ini",function(e){var t={cN:"string",c:[e.BE],v:[{b:"'''",e:"'''",r:10},{b:'"""',e:'"""',r:10},{b:'"',e:'"'},{b:"'",e:"'"}]};return{aliases:["toml"],cI:!0,i:/\S/,c:[e.C(";","$"),e.HCM,{cN:"section",b:/^\s*\[+/,e:/\]+/},{b:/^[a-z0-9\[\]_-]+\s*=\s*/,e:"$",rB:!0,c:[{cN:"attr",b:/[a-z0-9\[\]_-]+/},{b:/=/,eW:!0,r:0,c:[{cN:"literal",b:/\bon|off|true|false|yes|no\b/},{cN:"variable",v:[{b:/\$[\w\d"][\w\d_]*/},{b:/\$\{(.*?)}/}]},t,{cN:"number",b:/([\+\-]+)?[\d]+_[\d_]+/},e.NM]}]}]}}),e.registerLanguage("java",function(e){var t=e.UIR+"(<"+e.UIR+">)?",r="false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private",a="\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",n={cN:"number",b:a,r:0};return{aliases:["jsp"],k:r,i:/<\/|#/,c:[e.C("/\\*\\*","\\*/",{r:0,c:[{b:/\w+@/,r:0},{cN:"doctag",b:"@[A-Za-z]+"}]}),e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:!0,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},e.UTM]},{bK:"new throw return else",r:0},{cN:"function",b:"("+t+"\\s+)+"+e.UIR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:r,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"params",b:/\(/,e:/\)/,k:r,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},n,{cN:"meta",b:"@[A-Za-z]+"}]}}),e.registerLanguage("javascript",function(e){return{aliases:["js"],k:{keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},e.ASM,e.QSM,{cN:"string",b:"`",e:"`",c:[e.BE,{cN:"subst",b:"\\$\\{",e:"\\}"}]},e.CLCM,e.CBCM,{cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{b:/\s*[);\]]/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:[e.CLCM,e.CBCM]}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+e.IR,r:0},{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#/}}),e.registerLanguage("json",function(e){var t={literal:"true false null"},r=[e.QSM,e.CNM],a={e:",",eW:!0,eE:!0,c:r,k:t},n={b:"{",e:"}",c:[{cN:"attr",b:'\\s*"',e:'"\\s*:\\s*',eB:!0,eE:!0,c:[e.BE],i:"\\n",starts:a}],i:"\\S"},i={b:"\\[",e:"\\]",c:[e.inherit(a)],i:"\\S"};return r.splice(r.length,0,n,i),{c:r,k:t,i:"\\S"}}),e.registerLanguage("makefile",function(e){var t={cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]};return{aliases:["mk","mak"],c:[e.HCM,{b:/^\w+\s*\W*=/,rB:!0,r:0,starts:{e:/\s*\W*=/,eE:!0,starts:{e:/$/,r:0,c:[t]}}},{cN:"section",b:/^[\w]+:\s*$/},{cN:"meta",b:/^\.PHONY:/,e:/$/,k:".PHONY",l:/[\.\w]+/},{b:/^\t+/,e:/$/,r:0,c:[e.QSM,t]}]}}),e.registerLanguage("xml",function(e){var t="[A-Za-z0-9\\._:-]+",r={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php"},a={eW:!0,i:/]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},e.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[a],starts:{e:"",rE:!0,sL:"css"}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[a],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars"]}},r,{cN:"meta",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"",c:[{cN:"name",b:/[^ \/><\n\t]+/,r:0},a]}]}}),e.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:"^\\[.+\\]:",rB:!0,c:[{cN:"symbol",b:"\\[",e:"\\]:",eB:!0,eE:!0,starts:{cN:"link",e:"$"}}]}]}}),e.registerLanguage("nginx",function(e){var t={cN:"variable",v:[{b:/\$\d+/},{b:/\$\{/,e:/}/},{b:"[\\$\\@]"+e.UIR}]},r={eW:!0,l:"[a-z/_]+",k:{literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},r:0,i:"=>",c:[e.HCM,{cN:"string",c:[e.BE,t],v:[{b:/"/,e:/"/},{b:/'/,e:/'/}]},{b:"([a-z]+):/",e:"\\s",eW:!0,eE:!0,c:[t]},{cN:"regexp",c:[e.BE,t],v:[{b:"\\s\\^",e:"\\s|{|;",rE:!0},{b:"~\\*?\\s+",e:"\\s|{|;",rE:!0},{b:"\\*(\\.[a-z\\-]+)+"},{b:"([a-z\\-]+\\.)+\\*"}]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{cN:"number",b:"\\b\\d+[kKmMgGdshdwy]*\\b",r:0},t]};return{aliases:["nginxconf"],c:[e.HCM,{b:e.UIR+"\\s+{",rB:!0,e:"{",c:[{cN:"section",b:e.UIR}],r:0},{b:e.UIR+"\\s",e:";|{",rB:!0,c:[{cN:"attribute",b:e.UIR,starts:r}],r:0}],i:"[^\\s\\}]"}}),e.registerLanguage("objectivec",function(e){var t={cN:"built_in",b:"(AV|CA|CF|CG|CI|MK|MP|NS|UI)\\w+"},r={keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},a=/[a-zA-Z@][a-zA-Z0-9_]*/,n="@interface @class @protocol @implementation";return{aliases:["mm","objc","obj-c"],k:r,l:a,i:""}]}]},{cN:"class",b:"("+n.split(" ").join("|")+")\\b",e:"({|$)",eE:!0,k:n,l:a,c:[e.UTM]},{b:"\\."+e.UIR,r:0}]}}),e.registerLanguage("perl",function(e){var t="getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",r={cN:"subst",b:"[$@]\\{",e:"\\}",k:t},a={b:"->{",e:"}"},n={v:[{b:/\$\d/},{b:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{b:/[\$%@][^\s\w{]/,r:0}]},i=[e.BE,r,n],s=[n,e.HCM,e.C("^\\=\\w","\\=cut",{eW:!0}),a,{cN:"string",c:i,v:[{b:"q[qwxr]?\\s*\\(",e:"\\)",r:5},{b:"q[qwxr]?\\s*\\[",e:"\\]",r:5},{b:"q[qwxr]?\\s*\\{",e:"\\}",r:5},{b:"q[qwxr]?\\s*\\|",e:"\\|",r:5},{b:"q[qwxr]?\\s*\\<",e:"\\>",r:5},{b:"qw\\s+q",e:"q",r:5},{b:"'",e:"'",c:[e.BE]},{b:'"',e:'"'},{b:"`",e:"`",c:[e.BE]},{b:"{\\w+}",c:[],r:0},{b:"-?\\w+\\s*\\=\\>",c:[],r:0}]},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\/\\/|"+e.RSR+"|\\b(split|return|print|reverse|grep)\\b)\\s*",k:"split return print reverse grep",r:0,c:[e.HCM,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[e.BE],r:0}]},{cN:"function",bK:"sub",e:"(\\s*\\(.*?\\))?[;{]",eE:!0,r:5,c:[e.TM]},{b:"-\\w\\b",r:0},{b:"^__DATA__$",e:"^__END__$",sL:"mojolicious",c:[{b:"^@@.*",e:"$",cN:"comment"}]}];return r.c=s,a.c=s,{aliases:["pl"],k:t,c:s}}),e.registerLanguage("php",function(e){var t={b:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},r={cN:"meta",b:/<\?(php)?|\?>/},a={cN:"string",c:[e.BE,r],v:[{b:'b"',e:'"'},{b:"b'",e:"'"},e.inherit(e.ASM,{i:null}),e.inherit(e.QSM,{i:null})]},n={v:[e.BNM,e.CNM]};return{aliases:["php3","php4","php5","php6"],cI:!0,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",c:[e.CLCM,e.HCM,e.C("/\\*","\\*/",{c:[{cN:"doctag",b:"@[A-Za-z]+"},r]}),e.C("__halt_compiler.+?;",!1,{eW:!0,k:"__halt_compiler",l:e.UIR}),{cN:"string",b:/<<<['"]?\w+['"]?$/,e:/^\w+;?$/,c:[e.BE,{cN:"subst",v:[{b:/\$\w+/},{b:/\{\$/,e:/\}/}]}]},r,t,{b:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{cN:"function",bK:"function",e:/[;{]/,eE:!0,i:"\\$|\\[|%",c:[e.UTM,{cN:"params",b:"\\(",e:"\\)",c:["self",t,e.CBCM,a,n]}]},{cN:"class",bK:"class interface",e:"{",eE:!0,i:/[:\(\$"]/,c:[{bK:"extends implements"},e.UTM]},{bK:"namespace",e:";",i:/[\.']/,c:[e.UTM]},{bK:"use",e:";",c:[e.UTM]},{b:"=>"},a,n]}}),e.registerLanguage("python",function(e){var t={cN:"meta",b:/^(>>>|\.\.\.) /},r={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[t],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[t],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},e.ASM,e.QSM]},a={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},n={cN:"params",b:/\(/,e:/\)/,c:["self",t,a,r]};return{aliases:["py","gyp"],k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},i:/(<\/|->|\?)/,c:[t,a,r,e.HCM,{v:[{cN:"function",bK:"def",r:10},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,n]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}}),e.registerLanguage("ruby",function(e){var t="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",r="and false then defined module in return redo if BEGIN retry end for true self when next until do begin unless END rescue nil else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",a={cN:"doctag",b:"@[A-Za-z]+"},n={b:"#<",e:">"},i=[e.C("#","$",{c:[a]}),e.C("^\\=begin","^\\=end",{c:[a],r:10}),e.C("^__END__","\\n$")],s={cN:"subst",b:"#\\{",e:"}",k:r},c={cN:"string",c:[e.BE,s],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]},o={cN:"params",b:"\\(",e:"\\)",endsParent:!0,k:r},l=[c,n,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<\\s*",c:[{b:"("+e.IR+"::)?"+e.IR}]}].concat(i)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:t}),o].concat(i)},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":",c:[c,{b:t}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{b:"("+e.RSR+")\\s*",c:[n,{cN:"regexp",c:[e.BE,s],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(i),r:0}].concat(i);s.c=l,o.c=l;var u="[>?]>",d="[\\w#]+\\(\\w+\\):\\d+:\\d+>",b="(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>",p=[{b:/^\s*=>/,starts:{e:"$",c:l}},{cN:"meta",b:"^("+u+"|"+d+"|"+b+")",starts:{e:"$",c:l}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:r,i:/\/\*/,c:i.concat(p).concat(l)}}),e.registerLanguage("sql",function(e){var t=e.C("--","$");return{cI:!0,i:/[<>{}*]/,c:[{bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke",e:/;/,eW:!0,k:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes c cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle d data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration e each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract f failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function g general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http i id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists k keep keep_duplicates key keys kill l language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim m main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex n name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding p package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime t table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek", 3 | literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void"},c:[{cN:"string",b:"'",e:"'",c:[e.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[e.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[e.BE]},e.CNM,e.CBCM,t]},e.CBCM,t]}}),e}); --------------------------------------------------------------------------------