├── demo ├── webhook.py ├── static │ └── favicon.ico ├── widgets │ ├── intro.md │ ├── intro-draft.md │ └── friend-links.md ├── themes │ └── default │ │ ├── templates │ │ ├── header.html │ │ ├── navbar.html │ │ ├── 404.html │ │ ├── custom │ │ │ └── navbar.html │ │ ├── discuss │ │ │ ├── 163gentie.html │ │ │ ├── duoshuo.html │ │ │ └── disqus.html │ │ ├── sidebar.html │ │ ├── footer.html │ │ ├── discuss-thread.html │ │ ├── head.html │ │ ├── searchbar.html │ │ ├── page.html │ │ ├── page-wide.html │ │ ├── layout-wide.html │ │ ├── foot.html │ │ ├── layout.html │ │ ├── archive.html │ │ ├── post.html │ │ └── index.html │ │ └── static │ │ ├── style.css │ │ └── highlight.css ├── pages │ ├── test │ │ └── index.html │ ├── about │ │ └── index.md │ └── about-wide │ │ └── index.md ├── site.json ├── config.py └── posts │ ├── 1994-04-20-feugiat-non.md │ ├── 2012-03-28-huamo-yepelu.md │ ├── 2016-02-05-blandit-vitae-mauris.md │ ├── 1997-08-26-semper.md │ ├── 2017-03-16-viverra.txt │ ├── 2017-03-04-lacinia.md │ ├── 2017-06-02-markdown-demo.md │ ├── 2010-04-01-viverra-imperdiet.md │ ├── 1990-12-30-sollicitudin-aliquam-metus.md │ ├── 1969-09-02-nam-nec-nunc-eros.md │ ├── 1969-10-29-imperdiet-ligula.md │ ├── 1991-02-20-congue-fringilla-sapien.md │ └── 1991-08-06-integer.md ├── MANIFEST.in ├── test-requirements.txt ├── veripress ├── __main__.py ├── model │ ├── __init__.py │ ├── models.py │ └── parsers.py ├── view │ └── __init__.py ├── api │ ├── __init__.py │ └── handlers.py ├── __init__.py └── helpers.py ├── docs ├── themes │ └── clean-doc │ │ ├── templates │ │ ├── navbar.html │ │ ├── header.html │ │ ├── 404.html │ │ ├── custom │ │ │ └── navbar.html │ │ ├── discuss │ │ │ ├── 163gentie.html │ │ │ ├── duoshuo.html │ │ │ └── disqus.html │ │ ├── foot.html │ │ ├── footer.html │ │ ├── discuss-thread.html │ │ ├── head.html │ │ ├── searchbar.html │ │ ├── page.html │ │ ├── archive.html │ │ ├── layout.html │ │ ├── post.html │ │ └── index.html │ │ └── static │ │ ├── style.css │ │ └── highlight.css ├── site.json ├── config.py └── pages │ ├── en │ └── index.md │ ├── index.md │ ├── webhook.md │ ├── deployment.md │ ├── installation.md │ ├── getting-started.md │ ├── theme.md │ ├── configuration-file.md │ ├── making-your-own-theme.md │ └── writing.md ├── veripress_cli ├── defaults │ ├── static │ │ └── favicon.ico │ ├── site.json │ └── config.py ├── __init__.py ├── helpers.py ├── serve.py ├── deploy.py ├── init.py ├── theme.py └── generate.py ├── Dockerfile ├── after-travis-ci-success.sh ├── LICENSE ├── .travis.yml ├── setup.py ├── tests ├── test_app_cache.py ├── test_parsers.py ├── test_helpers.py ├── test_toc.py ├── test_views.py ├── test_api.py ├── test_storage.py └── test_models.py ├── README.md └── .gitignore /demo/webhook.py: -------------------------------------------------------------------------------- 1 | pass 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include veripress_cli/defaults * -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | coverage==4.4.2 2 | pytest==3.2.5 3 | pytest-cov==2.5.1 4 | coveralls==1.2.0 -------------------------------------------------------------------------------- /demo/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verilab/veripress/HEAD/demo/static/favicon.ico -------------------------------------------------------------------------------- /demo/widgets/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | position: sidebar 3 | order: 0 4 | --- 5 | 6 | #### My Name 7 | 8 | Welcome to my blog! 9 | -------------------------------------------------------------------------------- /veripress/__main__.py: -------------------------------------------------------------------------------- 1 | from veripress_cli import main 2 | 3 | if __name__ == '__main__': # pragma: no cover 4 | main() 5 | -------------------------------------------------------------------------------- /demo/widgets/intro-draft.md: -------------------------------------------------------------------------------- 1 | --- 2 | position: header 3 | order: 0 4 | is_draft: true 5 | --- 6 | 7 | This is my blog. Welcome! 8 | -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /veripress_cli/defaults/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verilab/veripress/HEAD/veripress_cli/defaults/static/favicon.ico -------------------------------------------------------------------------------- /demo/themes/default/templates/header.html: -------------------------------------------------------------------------------- 1 |

{{ site.title }}
2 | {{ site.subtitle }} 3 |

-------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/header.html: -------------------------------------------------------------------------------- 1 |

{{ site.title }}
2 | {{ site.subtitle }} 3 |

-------------------------------------------------------------------------------- /demo/themes/default/templates/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/pages/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test 6 | 7 | 8 |

This is a test page.

9 | 10 | -------------------------------------------------------------------------------- /demo/widgets/friend-links.md: -------------------------------------------------------------------------------- 1 | --- 2 | position: sidebar 3 | order: 1 4 | --- 5 | 6 | #### Friend Links 7 | 8 | - [Project RC](https://stdrc.cc) 9 | - [VeriPress](https://github.com/veripress/veripress) 10 | -------------------------------------------------------------------------------- /docs/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "VeriPress Docs", 3 | "subtitle": "Documentation of VeriPress.", 4 | "author": "VeriPress", 5 | "root_url": "https://veripress.github.io", 6 | "timezone": "UTC+08:00", 7 | "language": "zh-hans" 8 | } -------------------------------------------------------------------------------- /demo/themes/default/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | Page Not Found 6 | {% endblock %} 7 | 8 | {% block body %} 9 |

Page Not Found

10 | {% endblock %} -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | Page Not Found 6 | {% endblock %} 7 | 8 | {% block body %} 9 |

Page Not Found

10 | {% endblock %} -------------------------------------------------------------------------------- /demo/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "VeriPress Demo", 3 | "subtitle": "Yet another VeriPress blog.", 4 | "author": "My Name", 5 | "email": "someone@example.com", 6 | "root_url": "https://veripress.github.io", 7 | "timezone": "UTC+08:00", 8 | "language": "en" 9 | } -------------------------------------------------------------------------------- /veripress_cli/defaults/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Untitled", 3 | "subtitle": "Yet another VeriPress blog.", 4 | "author": "My Name", 5 | "email": "someone@example.com", 6 | "root_url": "https://example.com", 7 | "timezone": "UTC+00:00", 8 | "language": "en" 9 | } -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/custom/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/config.py: -------------------------------------------------------------------------------- 1 | STORAGE_TYPE = 'file' 2 | THEME = 'clean-doc' 3 | ENTRIES_PER_PAGE = 5 4 | FEED_COUNT = 10 5 | SHOW_TOC = True 6 | TOC_DEPTH = 2 7 | TOC_LOWEST_LEVEL = 3 8 | ALLOW_SEARCH_PAGES = True 9 | PAGE_SOURCE_ACCESSIBLE = False 10 | DISQUS_ENABLED = False 11 | DISQUS_SHORT_NAME = 'veripress-docs' 12 | -------------------------------------------------------------------------------- /demo/themes/default/templates/custom/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/pages/en/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | author: Richard Chien 4 | created: 2017-03-19 5 | updated: 2017-03-19 6 | language: en 7 | --- 8 | 9 | Coming soon! You can use [Google Translate](https://translate.google.com/translate?hl=en&sl=zh-CN&tl=en&u=https%3A%2F%2Fveripress.github.io%2Fdocs%2Finstallation.html) as a workaround temporarily. 10 | -------------------------------------------------------------------------------- /veripress_cli/defaults/config.py: -------------------------------------------------------------------------------- 1 | STORAGE_TYPE = '{storage_mode}' 2 | THEME = 'default' 3 | CACHE_TYPE = 'simple' 4 | MODE = 'view-only' # mixed|api-only|view-only 5 | ENTRIES_PER_PAGE = 5 6 | FEED_COUNT = 10 7 | SHOW_TOC = True 8 | TOC_DEPTH = 3 # 1~6 9 | TOC_LOWEST_LEVEL = 3 # 1~6 10 | ALLOW_SEARCH_PAGES = True 11 | PAGE_SOURCE_ACCESSIBLE = False 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6.0 2 | MAINTAINER Richard Chien 3 | 4 | RUN mkdir /veripress 5 | WORKDIR /veripress 6 | COPY veripress veripress 7 | COPY veripress_cli veripress_cli 8 | COPY setup.py setup.py 9 | COPY README.md README.md 10 | COPY MANIFEST.in MANIFEST.in 11 | RUN pip install . gevent 12 | 13 | VOLUME ["/instance"] 14 | WORKDIR /instance 15 | ENTRYPOINT ["veripress"] 16 | -------------------------------------------------------------------------------- /demo/themes/default/templates/discuss/163gentie.html: -------------------------------------------------------------------------------- 1 |
2 | 10 | -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/discuss/163gentie.html: -------------------------------------------------------------------------------- 1 |
2 | 10 | -------------------------------------------------------------------------------- /demo/themes/default/templates/sidebar.html: -------------------------------------------------------------------------------- 1 | {% if not config.GENERATING_STATIC_PAGES %} 2 | {% include ['custom/searchbar.html', 'searchbar.html'] ignore missing %} 3 | {% endif %} 4 | 5 | {% for widget in storage.get_widgets(position='sidebar') %} 6 |
7 |
8 | {{ widget|content|safe }} 9 |
10 |
11 | {% endfor %} -------------------------------------------------------------------------------- /demo/config.py: -------------------------------------------------------------------------------- 1 | STORAGE_TYPE = 'file' 2 | THEME = 'default' 3 | CACHE_TYPE = 'simple' 4 | MODE = 'view-only' # api-only | view-only | mixed 5 | ENTRIES_PER_PAGE = 5 6 | FEED_COUNT = 10 7 | SHOW_TOC = True 8 | TOC_DEPTH = 3 # 1~6 9 | TOC_LOWEST_LEVEL = 3 # 1~6 10 | ALLOW_SEARCH_PAGES = True 11 | PAGE_SOURCE_ACCESSIBLE = False 12 | DUOSHUO_ENABLED = False 13 | DUOSHUO_SHORT_NAME = '' 14 | DISQUS_ENABLED = False 15 | DISQUS_SHORT_NAME = '' 16 | NETEASE_GENTIE_ENABLED = False 17 | NETEASE_GENTIE_APP_KEY = '' 18 | -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/foot.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /after-travis-ci-success.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then 4 | npm install git-update-ghpages; 5 | ./node_modules/.bin/git-update-ghpages -e veripress/demo demo/_deploy; 6 | ./node_modules/.bin/git-update-ghpages -e veripress/docs docs/_deploy; 7 | fi 8 | 9 | if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then 10 | if [[ $TRAVIS_BRANCH =~ ^v[0-9.]+$ ]]; then docker tag $DOCKER_REPO:$TAG $DOCKER_REPO:latest; fi 11 | docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS 12 | docker push $DOCKER_REPO 13 | fi 14 | -------------------------------------------------------------------------------- /veripress_cli/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.group(name='veripress', 5 | short_help='A blog engine for hackers.', 6 | help='This is a blog engine for hackers. ' 7 | 'You can use this to serve a blog, ' 8 | 'a wiki or anything else you like.') 9 | @click.version_option(version='1.0.11') 10 | def cli(): 11 | pass 12 | 13 | 14 | def main(): 15 | cli.main() 16 | 17 | 18 | import veripress_cli.init 19 | import veripress_cli.serve 20 | import veripress_cli.theme 21 | import veripress_cli.generate 22 | import veripress_cli.deploy 23 | -------------------------------------------------------------------------------- /demo/themes/default/templates/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/themes/default/templates/discuss-thread.html: -------------------------------------------------------------------------------- 1 | {% if config.NETEASE_GENTIE_ENABLED or config.DISQUS_ENABLED or config.DUOSHUO_ENABLED %} 2 |
3 |
4 |

5 | Discuss 6 | 7 |

8 |
9 |
10 | {% if config.NETEASE_GENTIE_ENABLED %} 11 | {% include 'discuss/163gentie.html' ignore missing %} 12 | {% elif config.DISQUS_ENABLED %} 13 | {% include 'discuss/disqus.html' ignore missing %} 14 | {% elif config.DUOSHUO_ENABLED %} 15 | {% include 'discuss/duoshuo.html' ignore missing %} 16 | {% endif %} 17 |
18 |
19 | {% endif %} -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/discuss-thread.html: -------------------------------------------------------------------------------- 1 | {% if config.NETEASE_GENTIE_ENABLED or config.DISQUS_ENABLED or config.DUOSHUO_ENABLED %} 2 |
3 |
4 |

5 | Discuss 6 | 7 |

8 |
9 |
10 | {% if config.NETEASE_GENTIE_ENABLED %} 11 | {% include 'discuss/163gentie.html' ignore missing %} 12 | {% elif config.DISQUS_ENABLED %} 13 | {% include 'discuss/disqus.html' ignore missing %} 14 | {% elif config.DUOSHUO_ENABLED %} 15 | {% include 'discuss/duoshuo.html' ignore missing %} 16 | {% endif %} 17 |
18 |
19 | {% endif %} -------------------------------------------------------------------------------- /docs/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 概览 3 | author: Richard Chien 4 | created: 2017-03-19 5 | updated: 2017-03-21 6 | --- 7 | 8 | ## 简介 9 | 10 | VeriPress 是一个功能强大且易于上手的博客引擎,使用 Python 3.4+ 基于 Flask 框架编写。它支持文章(post)、自定义页面(page)、页面部件(widget)三种内容形式,支持主题管理、自定义文章/页面的布局、动态运行 WSGI app 或生成静态网页文件,它的 API 模式可以让你方便地在独立的前端或移动 app 中获取网站的数据,同时支持通过 webhook 回调来在特定情况下执行自定义 Python 脚本(比如在收到 GitHub push 的回调后拉取最新的网站内容)。 11 | 12 | 如果你是第一次使用 VeriPress 或对于某些功能不太清楚,可以阅读下面给出的使用文档,如果还有疑问,也欢迎前往 GitHub 提交 [issue](https://github.com/veripress/veripress/issues/new)。 13 | 14 | ## 目录 15 | 16 | - [安装](installation.html) 17 | - [开始使用](getting-started.html) 18 | - [撰写内容](writing.html) 19 | - [配置文件](configuration-file.html) 20 | - [主题](theme.html) 21 | - [部署网站](deployment.html) 22 | - [Webhook](webhook.html) 23 | - [API 模式](api-mode.html) 24 | - [制作主题](making-your-own-theme.html) 25 | -------------------------------------------------------------------------------- /demo/themes/default/templates/discuss/duoshuo.html: -------------------------------------------------------------------------------- 1 | 2 |
4 | 5 | 6 | 18 | -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/discuss/duoshuo.html: -------------------------------------------------------------------------------- 1 | 2 |
4 | 5 | 6 | 18 | -------------------------------------------------------------------------------- /demo/themes/default/templates/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/themes/clean-doc/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 6vh; 3 | padding-bottom: 6vh; 4 | } 5 | 6 | div { 7 | -ms-word-wrap: break-word; 8 | word-wrap: break-word; 9 | -ms-word-break: normal; 10 | word-break: normal; 11 | } 12 | 13 | img { 14 | max-width: 100%; 15 | } 16 | 17 | .markdown-body pre.txt { 18 | white-space: pre-wrap; 19 | white-space: -moz-pre-wrap; 20 | white-space: -o-pre-wrap; 21 | word-wrap: break-word; 22 | background-color: inherit; 23 | border: none; 24 | padding: 0; 25 | } 26 | 27 | .nav-container { 28 | padding: 15px 15px 20px; 29 | } 30 | 31 | .site-header-row { 32 | display: block; 33 | } 34 | 35 | .search-bar-col { 36 | display: block; 37 | padding-bottom: 0; 38 | } 39 | 40 | @media (min-width: 992px) { 41 | .site-header-row { 42 | display: flex; 43 | } 44 | 45 | .search-bar-col { 46 | display: flex; 47 | flex-direction: column-reverse; 48 | padding-bottom: 20px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/searchbar.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 25 |
-------------------------------------------------------------------------------- /demo/themes/default/templates/searchbar.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 25 |
-------------------------------------------------------------------------------- /docs/pages/webhook.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Webhook 3 | author: Richard Chien 4 | created: 2017-03-20 5 | updated: 2017-05-24 6 | --- 7 | 8 | VeriPress 支持通过接收 webhook 回调来在某些特定外部事件发生时执行自定义 Python 脚本,从而实现例如 GitHub 仓库发生 push 事件就自动拉取最新内容这样的用法。 9 | 10 | 无论运行模式是 `api-only` 还是 `view-only` 还是 `mixed`,`/_webhook` 这个 URL 都可以接收 POST 请求。收到请求后,它会在 VeriPress 实例目录中寻找 `webhook.py` 文件,如果存在,则会在 Flask 的 request context 下执行这个文件的代码(此为同步执行,如果需要进行长时间的任务,最好开新的线程或进程),不存在则跳过。无论该脚本存在与否最终都返回 `204 NO CONTENT`。 11 | 12 | 「在 Flask 的 request context 下执行」意味着在脚本中可以访问到 Flask 当前请求的 `request` 对象、`g` 对象、`current_app` 对象,也就是说你可以在这里对 POST 请求进行鉴权、使用甚至修改 app 的配置文件等。关于 Flask 的 request context,请参考 Flask 官方文档的 [The Request Context](http://flask.pocoo.org/docs/0.12/reqcontext/)。 13 | 14 | 一段可能的自定义脚本如下: 15 | 16 | ```py 17 | import os 18 | import subprocess 19 | from flask import request 20 | 21 | def check_token(): 22 | # 对请求进行鉴权,防止恶意请求 23 | return True 24 | 25 | if check_token(): 26 | log_file = open('webhook.log', 'a') 27 | subprocess.Popen(['/bin/sh', 'update.sh'], stdout=log_file) 28 | ``` 29 | -------------------------------------------------------------------------------- /demo/themes/default/templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | {{ entry.title + ' - ' + site.title }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 10 |
11 |
12 |

13 | Page 14 | 15 |

16 |
17 |
18 |
19 |

{{ entry.title }}

20 | {% if entry.toc_html %} 21 |
22 | {{ entry.toc_html|safe }} 23 |
24 | {% endif %} 25 | {{ entry.content|safe }} 26 |
27 |
28 | 31 |
32 | 33 | {% include ['custom/discuss-thread.html', 'discuss-thread.html'] ignore missing %} 34 | 35 | {% endblock %} -------------------------------------------------------------------------------- /demo/themes/default/templates/page-wide.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout-wide.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | {{ entry.title + ' - ' + site.title }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 10 |
11 |
12 |

13 | Page 14 | 15 |

16 |
17 |
18 |
19 |

{{ entry.title }}

20 | {% if entry.toc_html %} 21 |
22 | {{ entry.toc_html|safe }} 23 |
24 | {% endif %} 25 | {{ entry.content|safe }} 26 |
27 |
28 | 31 |
32 | 33 | {% include ['custom/discuss-thread.html', 'discuss-thread.html'] ignore missing %} 34 | 35 | {% endblock %} -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | {{ entry.title + ' - ' + site.title }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 10 |
11 |
12 | 13 |
14 |
15 |

{{ entry.title }}

16 | {% if entry.toc_html %} 17 |
18 | {{ entry.toc_html|safe }} 19 |
20 | {% endif %} 21 | {{ entry.content|safe }} 22 |
23 |
24 | 30 |
31 | 32 | {% include ['custom/discuss-thread.html', 'discuss-thread.html'] ignore missing %} 33 | 34 | {% endblock %} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Richard Chien 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /veripress_cli/helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | 5 | def copy_folder_content(src, dst): 6 | """ 7 | Copy all content in src directory to dst directory. 8 | The src and dst must exist. 9 | """ 10 | for file in os.listdir(src): 11 | file_path = os.path.join(src, file) 12 | dst_file_path = os.path.join(dst, file) 13 | if os.path.isdir(file_path): 14 | shutil.copytree(file_path, dst_file_path) 15 | else: 16 | shutil.copyfile(file_path, dst_file_path) 17 | 18 | 19 | def remove_folder_content(path, ignore_hidden_file=False): 20 | """ 21 | Remove all content in the given folder. 22 | """ 23 | for file in os.listdir(path): 24 | if ignore_hidden_file and file.startswith('.'): 25 | continue 26 | 27 | file_path = os.path.join(path, file) 28 | if os.path.isdir(file_path): 29 | shutil.rmtree(file_path) 30 | else: 31 | os.remove(file_path) 32 | 33 | 34 | def makedirs(path, mode=0o777, exist_ok=False): 35 | """A wrapper of os.makedirs().""" 36 | os.makedirs(path, mode, exist_ok) 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.4' 4 | - '3.5' 5 | - '3.6' 6 | 7 | sudo: required 8 | 9 | services: 10 | - docker 11 | 12 | env: 13 | global: 14 | - GIT_NAME: "Richard Chien" 15 | - GIT_EMAIL: richardchienthebest@gmail.com 16 | - DOCKER_USER: richardchien 17 | - DOCKER_EMAIL: richardchienthebest@gmail.com 18 | - DOCKER_REPO: veripress/veripress 19 | 20 | before_install: 21 | - nvm install 4 22 | 23 | install: 24 | - pip install . 25 | - pip install -r test-requirements.txt 26 | 27 | script: 28 | - export VERIPRESS_INSTANCE_PATH=$(pwd)/tests/instance 29 | - coverage run --source veripress -m py.test tests 30 | - export VERIPRESS_INSTANCE_PATH=$(pwd)/demo 31 | - veripress generate --app-root /demo/ 32 | - export VERIPRESS_INSTANCE_PATH=$(pwd)/docs 33 | - veripress generate --app-root /docs/ 34 | - export TAG=`if [[ $TRAVIS_BRANCH =~ ^v[0-9.]+$ ]]; then echo ${TRAVIS_BRANCH#v}; else echo $TRAVIS_BRANCH; fi` 35 | - docker build -f Dockerfile -t $DOCKER_REPO:$TAG . 36 | 37 | after_success: 38 | - coveralls 39 | - chmod +x after-travis-ci-success.sh && ./after-travis-ci-success.sh 40 | -------------------------------------------------------------------------------- /demo/themes/default/templates/discuss/disqus.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 19 | 21 | -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/discuss/disqus.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 19 | 21 | -------------------------------------------------------------------------------- /demo/themes/default/templates/layout-wide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | {% include ['custom/head.html', 'head.html'] ignore missing %} 6 | {% endblock %} 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 | 18 |
19 |
20 | 23 |
24 |
25 |
26 |
27 |
28 | {% block body %}{% endblock %} 29 |
30 |
31 |
32 |
33 | 34 | {% include ['custom/footer.html', 'footer.html'] ignore missing %} 35 | 36 | {% block foot %} 37 | {% include ['custom/foot.html', 'foot.html'] ignore missing %} 38 | {% endblock %} 39 | 40 | 41 | -------------------------------------------------------------------------------- /demo/themes/default/templates/foot.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/posts/1994-04-20-feugiat-non.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 每眼及更图增革派五气 3 | categories: Lorem 4 | tags: [China, Internet] 5 | language: zh-hans 6 | --- 7 | 8 | 方会个积已包发,严且例设化养,从详流工医。 石达色问电土天华,张增要对名表,接满7翻石基。 格所能走对界求目百,行儿心了包关处,角完询板文L专。 科许常关九快毛形,铁式响转过流件,按豆劳不更雨。 以广例众铁商干布己由我切角整空,车步持斗后发询改被习众低。 色结总电办向系准走属,人值级广者置任数经工,济没励否济战较伸色。 时造了感地水从好重易段,选数头火弦秧代使。 体必真证前每现立当我,品达油见运入许据,圆自46例扯苗厕。 干青争精县其斯天体,感式了求须有接候争,的肃立盯抗困深。 体重今及克事心日行被中,认整能需和斯增毛但,单派R可露效观身扯。 决带感何引方他状山强,装后属派变资示件传,两此承在动可心四。 领石去严生展细商县指,群毛具非身正往个七,社条隶车完苏豆里。 电情想叫打流如族族写按约,接低几员装满精没飞即例海,调子否到题果枣权广原。 角厂想节学称究接合组建,克些把最严上引验高,酸部更持英类件我深。 满口间队走具集还,火往流在经切议,用2数发别对。 9 | 10 | 了家每命设人一全装千院常,资上程事而学议民者数真,选清村茎二方带干构攻。 始此收示可处近而,算音海该住切程,克建严翻芳芳。 速走极七角便写,机音如效条写山,百E包身战。 反又白条今需少内,根别传铁第运铁,铁U秧作式关。 示千和深部社律济,入对给除革从声住,低Q钉铁赚观。 月人据外才准后,代王解门手太,较U信也金。 平义生片百表今处标接,要它如传和于江眼两,油王肃承4话术生例。 场面才流后安场候热,是族千命现林理子整,县求劳值边你家。 型不管共了空即果,来到料布快头全研,起豆S金半及。 电叫适里它在油声议安,参并争由阶开内律组定,该委屈正A气史告。 元然带边团家,满那红总转,去2细值。 合制料强见及素中,王常需指半统除,派6代回别S。 没信往清阶全专难育济,建同五万D题观箩。 头商结就有例本所象证数速,山根传断P届选已军。 要引商广史必技,每要即离新导育,建求板K他。 办电到叫二整级实部满,政科头造J火专活。 起量空导经儿空民整京科值,改易美要S队所9帐。 指火新是给高二领来,些作多强身风干酸,验提否给苍陕办。 11 | 12 | 选备太第平程热反况,交器气克张那争相热,变1效角位苍英。 即叫结看受增县易来声团报,生率风为低其上道开联约,的究A世正千务旱H华。 收线长队对斯等程,对要得分满家,级离霸二农二。 强准与心团革走其,分置值强义同前,确斗X等三身题。 便变活门则议义不,这十劳影土那,阶F北验音完。 格利科选术与行文自次共计家,却常的号导辰村范总水。 置市却义务形原领放,力通技有情单带,与几露治兵块流。 空半构八放认听先查,书里事后受有理京太,来孤音K佣接厕。 才准好想决代始府之,局生论法许龙议,满合Q影杯织况。 展毛克基较,习证外等,O领。 -------------------------------------------------------------------------------- /demo/posts/2012-03-28-huamo-yepelu.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 约高满论处月查层 3 | categories: Mauris 4 | tags: [Programming Language] 5 | language: zh-hans 6 | --- 7 | 8 | 命器市论建件劳事段对,走一话时收改四始,积也励式维支许。 认形无来都根查习,电计见花使层,引万露听非无。 而边位社相现实物路究四价调命能,后般流文基会正K完细拉敌因。 如海路目问南家相,花从M报受。 战特位角适共列更用他天作,化给称之的却位便济务,来备1址丧钉合虚了了。 委动生方今话分性,界效8却却。 十从太张江科联真改外支联,件起老位刷题明钉形。支品连东队提新走,细图别铁真西报断酸,想详织统所连满。 只什满头同老往现最规队专最,行斯问大后些手之器专等处,化本月3利矿屈被日几受。 得支最会们专表,列白西系支活她,才5路过难。 各参队调清他情白手间始设图级二,百这化用较军变李8集园3无。断究增新者极相据已,收华几少矿来完,统向T出芳十葬。 照六级但存外交角号面,商前还采马水影儿,米三I芹传计快接。 作将都术支难品,月场与入她习,成届代凝岗。 斯程形规专交七无,断世就把派且克,导豆究林8说。 区铁打常得半地意能治根其代,想组这入步美9芹身管因。 9 | 10 | 11 | 12 | 性什所发复,别她声何然节,主Q罐自支。 声关亲声改引需法工持,然头用了只眼于质想,是转G积毛究所厕。 状指家美制包参斯,一高话至际条矿,天更常领承过。 声切个处维结见东,响这局但种部些质,进露院委钉隶。 道断支务样必本况他形型标如厂义活,样儿可志整月丽其着址在叫坚。 步值文身美决斗共变前,示专你格用快九断群器,消白两M府何岗事。 细当基被感行土家,从断上以没该长,处8半住老克。 则料复新造需东能处将,年备因建片万规,你可肃租BK日利。 各都道平达界质个反,下复文解非管示复历,目则G积坟电格足。 决亲事必想任该于先原,无强个率三年少去量,空习蠢对变好葛东。 应工新明属队增,低到展持叫,治极亲V步。 论会话把次军理想热准容是,号压称作委太品历列,张采体F将天解格称厕。 想式然记社半中将面维油,少选而到做又声根标命山,数形Y飞半地亲委I。 验集报王式除设得,年道拉型度革,周团蠢体劳亲。 准重向立极了会示制史称王专王,认集两间身以干识色问文建,称家斯励听群连动委箩芬观。 13 | 14 | 统发正于千就构准过低江,府半可我用八维江更代日,下少李式茎园头届镰。叫系格如据百证主众,因角较置音王众出路议,备弦近不往非板生。 系因支每重己心矿南立,眼正对江持写或住,多和6需秩角佣林。 统法即形料更值结适,实总矿各通型市体,划A钉集却存却。 位向又团王还使际院,件路为说委运经口,例七V凝内隶。计性种京种声治众组南,保主有什将变清身及单,联育I吧马松手却号。引那保织张具去系改及,路马除V经孟阶。 别起队流京加其和十红值二构科,写组当取辰何葬困劳杰。写林目点局化角,许派传象交历支,则5容吼米。家文完北石快根角,然并霸存9。 -------------------------------------------------------------------------------- /demo/themes/default/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 6vh; 3 | padding-bottom: 6vh; 4 | } 5 | 6 | div { 7 | -ms-word-wrap: break-word; 8 | word-wrap: break-word; 9 | -ms-word-break: normal; 10 | word-break: normal; 11 | } 12 | 13 | img { 14 | max-width: 100%; 15 | } 16 | 17 | .markdown-body pre.txt { 18 | white-space: pre-wrap; 19 | white-space: -moz-pre-wrap; 20 | white-space: -o-pre-wrap; 21 | word-wrap: break-word; 22 | background-color: inherit; 23 | border: none; 24 | padding: 0; 25 | } 26 | 27 | a { 28 | -webkit-transition: all 200ms ease; 29 | -moz-transition: all 200ms ease; 30 | -ms-transition: all 200ms ease; 31 | -o-transition: all 200ms ease; 32 | transition: all 200ms ease; 33 | } 34 | 35 | a:hover, a:focus { 36 | text-decoration: none; 37 | } 38 | 39 | .nav-container { 40 | padding: 15px 15px 20px; 41 | } 42 | 43 | .site-header-row { 44 | display: block; 45 | } 46 | 47 | .search-bar-col { 48 | display: block; 49 | padding-bottom: 0; 50 | } 51 | 52 | @media (min-width: 992px) { 53 | .site-header-row { 54 | display: flex; 55 | } 56 | 57 | .search-bar-col { 58 | display: flex; 59 | flex-direction: column-reverse; 60 | padding-bottom: 20px; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('README.md', 'r', encoding='utf-8') as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name='veripress', 8 | version='1.0.11', 9 | packages=find_packages(), 10 | url='https://github.com/veripress/veripress', 11 | license='MIT License', 12 | author='Richard Chien', 13 | author_email='richardchienthebest@gmail.com', 14 | description='A blog engine for hackers.', 15 | long_description=long_description, 16 | long_description_content_type='text/markdown', 17 | install_requires=[ 18 | 'Flask>=1.1,<1.2', 'Werkzeug>=0.16,<1.0', 'Flask-Caching>=1.4,<2.0', 19 | 'PyYAML>=5.3,<6.0', 'Markdown>=3.2,<4.0', 'Pygments>=2.5,<2.6', 20 | 'pytz>=2019.3,<2020.0', 'click>=7.0,<8.0' 21 | ], 22 | python_requires='>=3.4', 23 | include_package_data=True, 24 | platforms='any', 25 | entry_points=dict( 26 | console_scripts=['veripress=veripress_cli:main'] 27 | ), 28 | classifiers=( 29 | 'Development Status :: 5 - Production/Stable', 30 | 'Framework :: Flask', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Operating System :: OS Independent', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3 :: Only', 35 | ) 36 | ) 37 | -------------------------------------------------------------------------------- /demo/posts/2016-02-05-blandit-vitae-mauris.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 解吧吧业董 3 | categories: Mauris 4 | tags: [Blog, BlogA] 5 | language: zh-hans 6 | --- 7 | 8 | ## 再题李消总 9 | 10 | 强及走计斯实号示平根,易会能山步细小格两,再题李消总U须在。 治做专表术农事造深高,住技段线号示教参,心现7杯枕现罐海。 可实道条少安何组造口,严照而算北无那车果本,入位束二豆命励机。 支很究装制老,教七属再实,最医消类。 四百县济名金研因程眼原取四,适型原指带S画技F器。 年即上制点热基济,广南作文江中除这,步更质我材数。 空她指求况其素,工等研天始面音,单节统可量4,些枕芹立类。 除之规九能叫类改前能并,说却展组号BF适。 处打世公真与列之连花,和再律指从声关写细,从那材却照着些资。 铁市九使到有容示部除山,解方育层是持正等三间,听能弦材划场装化帐。 目同此清物构去思半自造化龙整土千时,清程九须直听D命合针论惹辅。 列度间点指每万人北加角,容他其思度认入南商,它海JI杜力别都好。 回把必改儿果直影我对,内最动期技西六流各飞,存力孟弦林路足杠。 得切点该市交标制安达于,当史表九更第最长。 北统组理能备称亲,有什办力列例越造,主陕许义则飞。 区意极近做极活派,力式相无科点使,达陕建构D帐。 11 | 12 | 13 | 14 | ## 增走现陕 15 | 16 | 性界机际先开者数联现影用调,没见响东值杨增走现陕。 场求将老力以又革活,场个始放隶孝过。 传龙派上间相八小道造反四,人打已商-角详A别。 响节府片面干院,只干可直很次阶,观3用有蠢。 手度和元标话来该严适都光叫,相万不那导详与坝长示意。 但美走那各关不系一电部可当,许组车人发V翻足识合。 土战手义知工信学政采决又交交向次,有须八何毛这县RR杨着惹立北。 所明克价口声表什格,公半任给一社物,身斗屈严龙苏接。 快进该长后经线流,九所却华的金干都,十李动东须意。 西儿事干制格发思数示,却多油E秩帐流。 门具体土阶南称约根始,交下被G道林肃。 子节义车第外压派了北四工别,把低直格光习价完三社调,五史导S无兵呆包I路W。 影断样地家格验明示,本作运传队观后,都求S口肃员7。 过近农在对土离,复风论离学然,听建有极R。 对立没进究团今马儿,众影光省正取劳人,利求丽极抄别小年。 型备例当办别界上完,第引军2身好识。 白常清着线性队立了,因约候受5省第。 划美度听解人头山,济越月满求期,或置村材第转。 17 | 18 | 广口大劳织市你育直资革,图参界水走候头调系光,院作询坑门翻元政长。 这院其称压美空,打消收做中称,二作始你称,更非蠢飞董。 铁式派个集而程西业名,她周素研豆许李飞。 部议周员响济知率二先,品区住层思全况收完老,类儿E七V足少美。 部各着定当身清花发,且第委一认和事治分,图孟走白几时别。 作对展信选平分格示都因,却表说生快流它设说,素复隶知矿扯即体利。 复使关但实命好却想律提他转,式参什建保思医维去僚肃。 到价发信级较音会再是即,十千些斯质内严花连张,一什J丽束苏辆青非。 些用他资较,置般杨。 19 | -------------------------------------------------------------------------------- /demo/themes/default/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | {% include ['custom/head.html', 'head.html'] ignore missing %} 6 | {% endblock %} 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 | 18 |
19 |
20 | 23 |
24 |
25 |
26 |
27 |
28 | {% block body %}{% endblock %} 29 |
30 | 33 |
34 |
35 |
36 | 37 | {% include ['custom/footer.html', 'footer.html'] ignore missing %} 38 | 39 | {% block foot %} 40 | {% include ['custom/foot.html', 'foot.html'] ignore missing %} 41 | {% endblock %} 42 | 43 | 44 | -------------------------------------------------------------------------------- /demo/posts/1997-08-26-semper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 领五少战个除合也则合 3 | categories: Mauris 4 | tags: Birthday 5 | language: zh-hans 6 | --- 7 | 8 | ## 状加 9 | 10 | 斯我格科强拉状加,学治器医着告龙。 元再节他斯离断声总技,图究响红音因他,开地3增于蹦战委。 都几头众米也再厂什研,带处清器计数建动南,通论E多呜坊二最。 车在化而称务用明,厂回成行听八社,都露持构备省。 市到建三时争五展局界政,价准候原六当越美政做,此叫详状枣问书秃告。 调有政般报值造来去,直已织离保出团段,知六村约容适力。 级复济般无日离头存长式音条,果院状百深活非好之面可,九住亲W之约B候值米据。 美织品照表养文我就先支,片点器因太千无心不,科切3李葡过流查什。 转需那因长为,小由西想,九D重严。 五五多各完保县什热内,话教难商情场合手,太里承手二提雨使。 队会科电低省外组社,节作往每X矿象。 青风重车长拉太经身小,行适置完单象各车现,她年H估询队弦看。 体何速清大拉能号务华,计九般意装总权了非,把本R等折美坟Q。 容角常设说律平空看三制,解算受约离基行于进程,立打医高吴价织厕连。 给周教度包代么料六,它器文反务系个果,统确太派七院取,K生间枪但流走。 11 | 12 | 共单县报作油按,历更适织即整素,说Q属详气。 13 | 14 | ## 有热整收 15 | 16 | 开现信定厂有热整收,又青从风查四红,强将G青交却务。装她该术平置至作龙证,指铁团值领同规说议,中等S箩机响主单。 17 | 18 | ### 小打,红所连边 19 | 20 | 积界准际看小打,红所连边节做,好Y卧芽张。院历林统头运并南声,长使斗同公于等东,响第V世下枣属。 律走路商想状后节,还断起型农话识业将,斯Y压属张住杏。 与方无型社院你,示青并际局格,研T其批之。 即政路例线思矿无下写干,全经石美学民转已色部低,主导Q导佣说家争花。 共信常器战文验少,干思张发所治土,需G住求查千。 严其级包节养真克四效矿,只时素集采求维传非,三验C群眼英空水样。 听满好九广须维的南集,法义统示引器调战,照状K你理样自本。 白压任从来所置,八万头证亲易即,区Y说资林。 技识转美形专总直长二,资况状治来红律区,的三5歼M海镰详。 才适严给电意队广物月型,可土水效示料象局期,个价弦劫年照又取豆。 入得认将不出手但好,何起改才却在了当改入,名任Y门却列连于。 验极以间参时管规会率,对究查清组确王,商严R深级厕居码。 21 | 22 | ### 又医书必级 23 | 24 | 亲直感龙由系西个见工海律素北议速,六革三合个全样场7计又医书必级。 因及根同手当在维治,统铁断或际什六再,共X丽带丧此志。 信次内事约商地价参集,较条处眼张子小火系原,育业A霸身劫和将。 必种及委面人九广越社,十往装立这中大新满,后究K询询医期。 点效现时话道见连,使制事用市北地加,使届身详豆圆。 调地响话适造置用料,总信识要则断信团,没将5杨员江呈。 影军省思适水被写度用而线院,线便少小再法积真深况单阶,质厂今李内火自更秀加壳。 较要二近属格转所质该,全决则相飞强在共,圆将X志连般八Z没。 情影金面,上1。 25 | -------------------------------------------------------------------------------- /demo/posts/2017-03-16-viverra.txt: -------------------------------------------------------------------------------- 1 | --- 2 | title: Lorem Ipsum 3 | categories: [Mauris, Donec] 4 | tags: [Blog, VeriPress] 5 | --- 6 | 7 | ## Phasellus 8 | 9 | Aliquam sit amet justo eget mi eleifend lobortis. Phasellus vulputate erat erat, a lobortis odio sollicitudin et. Aliquam egestas sed risus at blandit. Fusce id dui vel tortor interdum blandit sit amet at eros. Pellentesque auctor tortor id purus finibus, sit amet cursus leo tempus. Etiam consequat lacus sed orci consequat eleifend. Phasellus ac lorem porttitor, accumsan enim ut, feugiat nunc. Aenean facilisis blandit metus id egestas. Duis condimentum a nunc tristique faucibus. 10 | 11 | ## 到片斯少 12 | 13 | 派她必切张身事流,入矿杏U然。华使转省式构江三科号题到片斯少,量管己车水立详惹育术苗称。型参五收王圆完,地叫律观广又情,周询事将总。子青研想年候以少厂水,历包论发外通术算,文场V共土究置每。和权不今按六名心立,存话把部群又动规,压角9B切圆因。使争很给照集做白,报复山科离至以往,市更务问县励。内开资光观劳手存,反活些米世八化按美,此G共布董管所。 14 | 15 | ---more--- 16 | 17 | ## 只道別地領走 18 | 19 | 朋造這山,實出海死科自式、主用間會一國倒自提晚靜水工車見……只道別地領走,曾不戲自到生時人說他這我:歡於自而為笑兩方得也對萬是流地字的解賽種設國英、復買破表坡間,電情有子須的裡方區檢甚然取那合一他要時異身的是日有會人女士麼假在光整影該動於;一易滿方可而中不回獨卻反,進使土多話輕型可早原嚴,主一好皮全物,了用知傷流,向臺定重或:打常代的。年演老壓升,黨了分電觀、腦備類,子關四次現,望位去裡馬大!有賽關一氣讀而我雖統上三聯。算不能是記歌古孩初名中廣此,進用是參我點笑來那品,起種但。 20 | 21 | ## 動ナニ候務ンばトお届社 22 | 23 | 厚ま今申ユラ受人モト地勢ケロス垣府ゅク会動ナニ候務ンばトお届社な引国ょ的学演エイヘ碁関ケセルメ交惹ウヤシ行理フ道業津ド断教スモクメ比県ニホハヤ山24活営じ。毎社まねむゆ提信タヘヱユ落組ゅら悩布きしぐ樹球らくフご供井たの泉本にイど明犬索求調ぱまリ教表ぼフ終衛みほど軽92教ヒメアヌ招郎超査む。 24 | 25 | ## 모든 국민은 종교 26 | 27 | 대통령은 즉시 이를 공포하여야 한다, 탄핵의 결정. 모든 국민은 종교의 자유를 가진다, 국정의 중요한 사항에 관한 대통령의 자문에 응하기 위하여 국가원로로 구성되는 국가원로자문회의를 둘 수 있다. 28 | -------------------------------------------------------------------------------- /demo/themes/default/templates/archive.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | {{ archive_name + ' - ' + archive_type + ' - ' + site.title }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 10 |
11 |
12 |

13 | {{ 'Archive' if archive_type|lower != 'search' else 'Search' }} 14 | 15 |

16 |
17 |
18 |
19 |

{{ archive_type }}: {{ archive_name }}

20 |
21 | 22 | {% if not entries %} 23 |

{{ 'There is nothing here.' if archive_type|lower != 'search' else 'No results.' }}

24 | {% else %} 25 | 26 | 27 | 28 | {% for entry in entries %} 29 | 30 | 32 | 33 | 34 | {% endfor %} 35 | 36 |
{{ entry.created.strftime('%Y.%m.%d') if entry.created else '' }}{{ entry.title }}
37 | 38 | {% endif %} 39 |
40 |
41 |
42 | 43 | {% endblock %} -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/archive.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | {{ archive_name + ' - ' + archive_type + ' - ' + site.title }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 10 |
11 |
12 |

13 | {{ 'Archive' if archive_type|lower != 'search' else 'Search' }} 14 | 15 |

16 |
17 |
18 |
19 |

{{ archive_type }}: {{ archive_name }}

20 |
21 | 22 | {% if not entries %} 23 |

{{ 'There is nothing here.' if archive_type|lower != 'search' else 'No results.' }}

24 | {% else %} 25 | 26 | 27 | 28 | {% for entry in entries %} 29 | 30 | 32 | 33 | 34 | {% endfor %} 35 | 36 |
{{ entry.created.strftime('%Y.%m.%d') if entry.created else '' }}{{ entry.title }}
37 | 38 | {% endif %} 39 |
40 |
41 |
42 | 43 | {% endblock %} -------------------------------------------------------------------------------- /demo/posts/2017-03-04-lacinia.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 强划高题类商 3 | categories: [Mauris, Donec] 4 | tags: [Blog, VeriPress] 5 | language: zh-hans 6 | --- 7 | 8 | 事级红象场方分时政证但,相或数思团需从证习候住,备话K低Y规后算装。 9 | 10 | ## 无录保从© 11 | 12 | 构决接气点行果果,八单决间权治装,无录保从规杠。 13 | 14 | ### 主该刷证或列 15 | 16 | 于是西品大南商再儿,再立器处飞听具,主该刷证或列W。 17 | 18 | 气如天收具律组万流社发,见历适能口具话何级他山,没材丽间-斯已元枣。史天变报各张效,自引油们方在长,近Z志农苗运。农金上务口反南使两生据,低工美之府同验定提三月,此后A记芽适天持辰。 被系方其明全六着与,来量体建般结导四位,两孤现量律先细。 二林于速影何积可明己同领定论东万,来圆头来根造F只主观接接料。 采家内厂白圆信不命,严因面说第几路定观原,样发杏规询只克济。 表去家应化身受况世,解机持步便通离,事感2选但给总。 级强育认白重受信接转面多除说装,列自和式白经S作利6知僚。 所确及候最动们百此,般从然石支都律,老点届翻听色易。 19 | 20 | ### 影间现进人 21 | 22 | 达出图元指上做面花小民,过好个领并影间现进人会,通因7矿天询老开这。 近引子任子路约即却的复东,家但族风分统村听备须枪。 却老除信系性效识文做,史话单给年合或制角段,带力3盯抛走严几。 23 | 24 | 25 | 26 | ## 建观坚老 27 | 28 | 算很影数近道调素证,无者物关体统起于,多当建观坚老具。太率除高且约龙证条得去们,里酸一称组空型太离史小严,条叫K济放励起L备批。 且种斯术根不被往派了,展马例被活相县格教制,际知材蹦列茄厂那。 必精设总列少基,府历由者,局弦求吨拉。 省族并更时理再程,保须千包性在片,手更录5对建。 少作回很不又基很,维增具数数,量了R光刷联。 着价广以么只据院务可三,文方转斯命点小拉基,准前医传家针将书杏。 我件区毛多图向由按,把部学形色把系知色验,千子孟四无更毛奇。 西非完她么办路叫与满,史往王广质一它低相,都开杏凝困松广S。 29 | 30 | ## 满之用关片,先到 31 | 32 | 不现你整近点名之属指进期前,法作向性认思再在满之用关片,先到2里引全少拉向这维。 角及习响场资权际干区利,三还无说该医别材。 界大明属处二示龙八战,标片快八1时边中。 两了前铁属细资观美手,族四志什间制十识色米,型再M9算标态别。 几地时度影使指,了万通消种度,无S满镰极。 林完采称儿则意在层七,本维太易效特去活安,也听6面皂斯构设。 33 | 34 | 快须北快从验用作与传,指任反手速严真界理放,用实隶青伶刷毛主。 美而道也真区土着,很样革社报次区单,指承布N毛布。 数两规识半百能,增大众步采动心,土居术务坝先。 部离群备说而低路口己克断据,什至中果光可1杨林杠商。 35 | 36 | ## 张均极制 37 | 38 | 马手认办照都命教选,包别们一利于总高,月支材张均极制。 39 | 40 | ### 白论时知院 41 | 42 | 上规边且级油要称理之算众大正质,白论时知院约P励抗你秩C。 点公点战很口张后,百话县话四水,只就村酸市政。 代阶各器论易会学断组物门统,世提压时农样Q来芽杏极。 标个已现带海响她儿论性之外,决因立主议意影般再团济影又,易技Y村两屈目秩例位候。 43 | -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | {% include ['custom/head.html', 'head.html'] ignore missing %} 6 | {% endblock %} 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 | 19 | {% if not config.GENERATING_STATIC_PAGES %} 20 |
21 | {% include ['custom/searchbar.html', 'searchbar.html'] ignore missing %} 22 |
23 | {% endif %} 24 |
25 |
26 | 29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {% block body %}{% endblock %} 37 |
38 |
39 |
40 |
41 |
42 | 43 | {% include ['custom/footer.html', 'footer.html'] ignore missing %} 44 | 45 | {% block foot %} 46 | {% include ['custom/foot.html', 'foot.html'] ignore missing %} 47 | {% endblock %} 48 | 49 | 50 | -------------------------------------------------------------------------------- /veripress_cli/serve.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from veripress_cli import cli 4 | 5 | 6 | @cli.command('serve', short_help='Serve/Run the application.', 7 | help='This command will start an HTTP server ' 8 | 'to serve the application.') 9 | @click.option('--host', '-h', default='127.0.0.1', 10 | help='Host to serve the app.') 11 | @click.option('--port', '-p', default=8080, help='Port to serve the app.') 12 | def serve_command(host, port): 13 | click.echo('Starting HTTP server...') 14 | click.echo('HTTP server started. ' 15 | 'Running on http://{}:{}/'.format(host, port)) 16 | 17 | from veripress import app 18 | try: 19 | from gevent.wsgi import WSGIServer 20 | server = WSGIServer((host, port), app) 21 | server.serve_forever() 22 | except ImportError: 23 | app.run(host=host, port=port) 24 | 25 | 26 | @cli.command('preview', short_help='Preview the application.', 27 | help='This command will start an HTTP server ' 28 | 'to preview the application. ' 29 | 'You should never use this command to ' 30 | 'run the app in production environment.') 31 | @click.option('--host', '-h', default='127.0.0.1', 32 | help='Host to preview the app.') 33 | @click.option('--port', '-p', default=8080, help='Port to preview the app.') 34 | @click.option('--debug', is_flag=True, default=False, 35 | help='Preview in debug mode.') 36 | def preview_command(host, port, debug): 37 | from veripress import app 38 | app.debug = debug 39 | app.config['TEMPLATES_AUTO_RELOAD'] = True 40 | app.config['CACHE_TYPE'] = 'null' 41 | app.run(host=host, port=port, debug=debug) 42 | -------------------------------------------------------------------------------- /demo/posts/2017-06-02-markdown-demo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown Demo 3 | categories: [Mauris, Donec] 4 | tags: [Markdown] 5 | language: zh-hans 6 | --- 7 | 8 | # This is a header 1 9 | 10 | ## This is a header 2 11 | 12 | ### This is a header 3 13 | 14 | #### This is a header 4 15 | 16 | ##### This is a header 5 17 | 18 | ###### This is a header 6 19 | 20 | This is a paragraph with *italic* and **strong** words. 21 | 22 | This is an unordered list: 23 | 24 | - Item 1 25 | - Item 2 26 | - Item 2.1 27 | - Item 2.2 28 | 29 | This is a paragraph under Item 2. 30 | 31 | - Item 3 32 | 33 | and an ordered list: 34 | 35 | 1. Item 1 36 | 2. Item 2 37 | 38 | Another paragraph under a list item, with a [link](https://github.com/veripress/veripress). 39 | 40 | 3. Item 3 41 | - Item 3.1 42 | - Item 3.2 43 | 1. Item 3.2.1 44 | 2. Item 3.2.2 45 | 4. Item 4 46 | 47 | This is an image ![Logo](../../../../../../static/favicon.ico), and an image with link [![Logo](../../../../../../static/favicon.ico)](https://github.com/veripress/veripress). 48 | 49 | This is inline code `hello word`. 50 | 51 | Block quote: 52 | 53 | > This is a quote. 54 | > 55 | > And the second line. 56 | 57 | Code block: 58 | 59 | ```python 60 | print('Hello, world!') 61 | ``` 62 | 63 | ```c 64 | #include 65 | 66 | int main() { 67 | printf("Hello, world!"); 68 | return 0; 69 | } 70 | ``` 71 | 72 | ``` 73 | Code block with no language specified 74 | ``` 75 | 76 | A horizontal line: 77 | 78 | --- 79 | 80 | ## Something Complex 81 | 82 | ### `inline code` in header 83 | 84 | ### *italic* in header 85 | 86 | Table: 87 | 88 | | Foo | Bar | 89 | | --- | --- | 90 | | 1 | 2 with `code` | 91 | | 3 | 4 with line
break | 92 | 93 | ### 中文标题 94 | 95 | 中文段落……以及**粗体**。 96 | -------------------------------------------------------------------------------- /demo/pages/about/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | created: 1970-01-01 00:00:00 3 | --- 4 | 5 | Lorem ipsum dolor sit amet, aeque cetero nec te, id detraxit pertinacia accommodare mei, per no facilis accumsan senserit. Solum hendrerit deseruisse eu eum, qui ei utinam munere insolens. Vim saepe aeterno commune ut, posse recusabo mnesarchum eos te, autem platonem id quo. Malis legere urbanitas cum ea, ex vis mazim tincidunt abhorreant. Id reque fastidii complectitur vis. 6 | 7 | ## Definitionem 8 | 9 | No mel dicant doming appetere, adolescens definitionem et pro. Amet dicunt iracundia ex cum, luptatum reformidans ea sed. Doctus lobortis qualisque ei quo, solet aliquam omnesque an quo, vis an legendos reprimique. Ut pro fugit paulo possit, ad habeo sonet nec, ea quot dicit definiebas sea. Stet oporteat praesent vim ad. 10 | 11 | ## Copiosae Sit 12 | 13 | Nec cu vocibus molestie petentium. Ad mentitum copiosae sit, etiam nostro ad duo. Eum an debet comprehensam, quis suavitate vel eu. Te has viderer intellegat, ea cetero inciderint nec. An tractatos posidonium has, quo ignota nonumes temporibus in. Democritum consequuntur vituperatoribus vel ad. 14 | 15 | ### Intellegebat 16 | 17 | Ferri quaeque pro cu, pericula maluisset intellegebat has ut, ad his legimus commune nominavi. Pro numquam dolores ad. Cu mutat verear timeam mel, eum id fugit prodesset. Eum an saperet sapientem vituperata, mei decore liberavisse te, qui ne liber nonumy imperdiet. 18 | 19 | ### Singulis 20 | 21 | Id cum vero brute copiosae, ad usu ipsum singulis delicatissimi. Purto ullum eam id, appellantur signiferumque cum ei, ius te scripta facilisi. Posse verear fastidii ex vel. Erat quidam maiestatis et ius, pro in veri doming repudiandae. Ex eos sonet docendi, cum an quaeque vocibus urbanitas, duo in vidit feugait petentium. Ea quo fuisset liberavisse. No vocent invidunt ius, qui id veniam lobortis salutandi. 22 | -------------------------------------------------------------------------------- /demo/themes/default/templates/post.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | {{ entry.title + ' - ' + site.title }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 10 |
11 |
12 |

13 | {% for category in entry.categories %} 14 | {% if loop.index > 1 %} 15 | & 16 | {% endif %} 17 | {{ category }} 18 | {% endfor %} 19 | 20 |

21 |
22 |
23 |
24 |

{{ entry.title }}

25 | {% if entry.toc_html %} 26 |
27 | {{ entry.toc_html|safe }} 28 |
29 | {% endif %} 30 | {{ entry.content|safe }} 31 |
32 |
33 | 47 |
48 | 49 | {% include ['custom/discuss-thread.html', 'discuss-thread.html'] ignore missing %} 50 | 51 | {% endblock %} -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/post.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | {{ entry.title + ' - ' + site.title }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 10 |
11 |
12 |

13 | {% for category in entry.categories %} 14 | {% if loop.index > 1 %} 15 | & 16 | {% endif %} 17 | {{ category }} 18 | {% endfor %} 19 | 20 |

21 |
22 |
23 |
24 |

{{ entry.title }}

25 | {% if entry.toc_html %} 26 |
27 | {{ entry.toc_html|safe }} 28 |
29 | {% endif %} 30 | {{ entry.content|safe }} 31 |
32 |
33 | 47 |
48 | 49 | {% include ['custom/discuss-thread.html', 'discuss-thread.html'] ignore missing %} 50 | 51 | {% endblock %} -------------------------------------------------------------------------------- /demo/pages/about-wide/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About (wide) 3 | created: 1970-01-01 00:00:00 4 | layout: page-wide 5 | --- 6 | 7 | Lorem ipsum dolor sit amet, aeque cetero nec te, id detraxit pertinacia accommodare mei, per no facilis accumsan senserit. Solum hendrerit deseruisse eu eum, qui ei utinam munere insolens. Vim saepe aeterno commune ut, posse recusabo mnesarchum eos te, autem platonem id quo. Malis legere urbanitas cum ea, ex vis mazim tincidunt abhorreant. Id reque fastidii complectitur vis. 8 | 9 | ## Definitionem 10 | 11 | No mel dicant doming appetere, adolescens definitionem et pro. Amet dicunt iracundia ex cum, luptatum reformidans ea sed. Doctus lobortis qualisque ei quo, solet aliquam omnesque an quo, vis an legendos reprimique. Ut pro fugit paulo possit, ad habeo sonet nec, ea quot dicit definiebas sea. Stet oporteat praesent vim ad. 12 | 13 | ## Copiosae Sit 14 | 15 | Nec cu vocibus molestie petentium. Ad mentitum copiosae sit, etiam nostro ad duo. Eum an debet comprehensam, quis suavitate vel eu. Te has viderer intellegat, ea cetero inciderint nec. An tractatos posidonium has, quo ignota nonumes temporibus in. Democritum consequuntur vituperatoribus vel ad. 16 | 17 | ### Intellegebat 18 | 19 | Ferri quaeque pro cu, pericula maluisset intellegebat has ut, ad his legimus commune nominavi. Pro numquam dolores ad. Cu mutat verear timeam mel, eum id fugit prodesset. Eum an saperet sapientem vituperata, mei decore liberavisse te, qui ne liber nonumy imperdiet. 20 | 21 | ### Singulis 22 | 23 | Id cum vero brute copiosae, ad usu ipsum singulis delicatissimi. Purto ullum eam id, appellantur signiferumque cum ei, ius te scripta facilisi. Posse verear fastidii ex vel. Erat quidam maiestatis et ius, pro in veri doming repudiandae. Ex eos sonet docendi, cum an quaeque vocibus urbanitas, duo in vidit feugait petentium. Ea quo fuisset liberavisse. No vocent invidunt ius, qui id veniam lobortis salutandi. 24 | -------------------------------------------------------------------------------- /veripress/model/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from flask import current_app, g 4 | from werkzeug.local import LocalProxy 5 | 6 | import veripress.model.storages 7 | from veripress.model.models import Base 8 | from veripress.helpers import ConfigurationError 9 | 10 | 11 | def get_storage(): 12 | """ 13 | Get storage object of current app context, 14 | will create a new one if not exists. 15 | 16 | :return: a storage object 17 | :raise: ConfigurationError: storage type in config is not supported 18 | """ 19 | storage_ = getattr(g, '_storage', None) 20 | if storage_ is None: 21 | storage_type = current_app.config['STORAGE_TYPE'] 22 | if storage_type == 'file': 23 | storage_ = g._storage = storages.FileStorage() 24 | else: 25 | raise ConfigurationError( 26 | 'Storage type "{}" is not supported.'.format(storage_type)) 27 | return storage_ 28 | 29 | 30 | storage = LocalProxy(get_storage) 31 | 32 | from veripress import app 33 | 34 | 35 | @app.teardown_appcontext 36 | def teardown_storage(e): 37 | """ 38 | Automatically called when Flask tears down the app context. 39 | This will close the storage object created at the beginning of the current app context. 40 | """ 41 | storage_ = getattr(g, '_storage', None) 42 | if storage_ is not None: 43 | storage_.close() 44 | 45 | 46 | class CustomJSONEncoder(app.json_encoder): 47 | """ 48 | Converts model objects to dicts, datetime to timestamp, 49 | so that they can be serialized correctly. 50 | """ 51 | 52 | def default(self, obj): 53 | if isinstance(obj, Base): 54 | return obj.to_dict() 55 | elif isinstance(obj, datetime): 56 | return obj.strftime('%Y-%m-%d %H:%M:%S') 57 | return super(CustomJSONEncoder, self).default(obj) 58 | 59 | 60 | # use the customized JSON encoder when jsonify is called 61 | app.json_encoder = CustomJSONEncoder 62 | -------------------------------------------------------------------------------- /tests/test_app_cache.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pytest import raises 4 | from werkzeug.exceptions import NotFound 5 | 6 | from veripress import app, site, cache, create_app, CustomFlask 7 | 8 | 9 | def test_create_app(): 10 | an_app = create_app('config2.py') 11 | assert isinstance(an_app, CustomFlask) 12 | assert an_app.config['STORAGE_TYPE'] == 'fake_type' 13 | assert an_app.template_folder == os.path.join( 14 | os.environ.get('VERIPRESS_INSTANCE_PATH'), 'themes', 'fenki', 'templates' 15 | ) 16 | assert hasattr(an_app, 'theme_static_folder') 17 | assert getattr(an_app, 'theme_static_folder') == os.path.join( 18 | os.environ.get('VERIPRESS_INSTANCE_PATH'), 'themes', 'fenki', 'static' 19 | ) 20 | 21 | 22 | def test_app(): 23 | # app's config should be loaded from instance/config.py 24 | assert app.config['STORAGE_TYPE'] == 'file' 25 | assert app.config['THEME'] == 'test' 26 | 27 | with app.test_request_context('/'): 28 | app.send_static_file('no-use.css') 29 | app.send_static_file('no-use-2.css') 30 | with raises(NotFound): 31 | app.send_static_file('non-exists.css') 32 | 33 | origin_mode = app.config['MODE'] 34 | app.config['MODE'] = 'api-only' 35 | with app.test_request_context('/'): 36 | with raises(NotFound): 37 | app.send_static_file('no-use.css') 38 | app.config['MODE'] = origin_mode 39 | 40 | 41 | def test_site(): 42 | # site meta info should be loaded from instance/site.json 43 | assert site['title'] == 'My Blog' 44 | assert site['subtitle'] == 'Yet another VeriPress blog. 一段中文的测试。' 45 | 46 | 47 | def test_cache(): 48 | assert cache.config['CACHE_TYPE'] == 'null' 49 | 50 | 51 | def test_webhook(): 52 | with app.test_client() as c: 53 | resp = c.post('/_webhook', data={'a': 'A'}) 54 | assert resp.status_code == 204 55 | 56 | script_path = os.path.join(app.instance_path, 'webhook.py') 57 | with open(script_path, 'rb') as f: 58 | script = f.read() 59 | 60 | os.remove(script_path) 61 | 62 | resp = c.post('/_webhook', data={'a': 'A'}) 63 | assert resp.status_code == 204 # it should always return 204 although there is no 'webhook.py' 64 | 65 | with open(script_path, 'wb') as f: 66 | f.write(script) 67 | -------------------------------------------------------------------------------- /tests/test_parsers.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from pytest import raises 4 | 5 | from veripress.model.parsers import get_standard_format_name, get_parser, Parser, TxtParser, MarkdownParser, parser 6 | 7 | 8 | def test_parser_decorator(): 9 | class FakeParser: 10 | pass 11 | 12 | parser('FoRMat1')(FakeParser) 13 | assert get_standard_format_name('formAT1') == 'format1' 14 | parser('format2', ext_names='fmt2')(FakeParser) 15 | assert get_standard_format_name('fmt2') == 'format2' 16 | parser('format3', ext_names=['fmt3', 'f3'])(FakeParser) 17 | assert get_standard_format_name('fmt3') == 'format3' 18 | assert get_standard_format_name('f3') == 'format3' 19 | 20 | 21 | def test_base_parser(): 22 | p = Parser() 23 | with raises(NotImplementedError): 24 | assert p.parse_preview('abc') 25 | with raises(NotImplementedError): 26 | assert p.parse_whole('abc') 27 | 28 | assert p.remove_read_more_sep('abc') == 'abc' 29 | 30 | 31 | def test_get_standard_format_name(): 32 | assert get_standard_format_name('txt') == 'txt' 33 | assert get_standard_format_name('TxT') == 'txt' 34 | assert get_standard_format_name('md') == 'markdown' 35 | assert get_standard_format_name('MDown') == 'markdown' 36 | assert get_standard_format_name('Markdown') == 'markdown' 37 | 38 | 39 | def test_get_parser(): 40 | assert isinstance(get_parser('txt'), TxtParser) 41 | assert isinstance(get_parser('markdown'), MarkdownParser) 42 | assert get_parser('non-exists') is None 43 | 44 | 45 | def test_txt_parser(): 46 | p = TxtParser() 47 | raw_content = 'abc' 48 | preview, has_more_content = p.parse_preview(raw_content) 49 | assert preview == p.parse_whole(raw_content) == '
abc
' 50 | assert has_more_content is False 51 | raw_content = 'abc\n---more---\n\ndef' 52 | assert p.parse_preview(raw_content) == ('
abc
', True) 53 | assert p.parse_whole(raw_content) == '
abc\n\ndef
' 54 | raw_content = 'abc\n------ MoRe --- \n\ndef---more ---' 55 | assert p.parse_whole(raw_content) == '
abc\n\ndef---more ---
' 56 | 57 | 58 | def test_md_parser(): 59 | p = MarkdownParser() 60 | assert p.parse_whole('## hello\n\n[link](https://google.com)').strip() \ 61 | == '

hello

\n

link

' 62 | -------------------------------------------------------------------------------- /demo/posts/2010-04-01-viverra-imperdiet.md: -------------------------------------------------------------------------------- 1 | --- 2 | categories: Etiam 3 | tags: [Web Framework, Python, Flask] 4 | --- 5 | 6 | Donec eget rhoncus neque. Pellentesque eget turpis ante. Nulla dui ex, ultrices non orci in, molestie vulputate lacus. Suspendisse ligula massa, dapibus vitae sem at, efficitur blandit ex. Vestibulum vehicula orci justo. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aliquam iaculis eu mi quis sollicitudin. Proin sit amet pretium enim. Praesent nec iaculis risus. Proin ultrices consectetur mauris sit amet efficitur. Nam augue massa, molestie in ligula rhoncus, fringilla suscipit velit. 7 | 8 | Cras tristique venenatis erat. Curabitur tempus nisi eget viverra imperdiet. Aenean at finibus magna, eget laoreet nisl. Aenean feugiat id velit ac convallis. Morbi posuere vestibulum mauris, non volutpat sapien. Fusce convallis quam leo, non convallis sapien sollicitudin vitae. Suspendisse venenatis porta maximus. Vestibulum faucibus dapibus imperdiet. Aenean blandit vitae mauris et lacinia. Praesent velit diam, congue sit amet mi quis, venenatis tincidunt leo. 9 | 10 | Cras sed suscipit orci. Ut eu leo eget est malesuada bibendum. Fusce convallis, arcu ac ultrices viverra, nulla lacus convallis ipsum, in auctor justo enim non elit. Fusce nec pellentesque enim. Integer dapibus libero et diam ultricies pulvinar. Phasellus scelerisque eget arcu iaculis posuere. Sed condimentum id erat ullamcorper finibus. Vivamus sed tincidunt turpis, at pretium elit. Interdum et malesuada fames ac ante ipsum primis in faucibus. 11 | 12 | Fusce fringilla mattis est. Nullam congue libero ut augue maximus mattis. Aenean vel pharetra neque, eget consectetur lorem. Phasellus at nibh et sapien pretium fringilla id sit amet nisl. Pellentesque sagittis nisi imperdiet neque ultrices pulvinar. Nunc aliquam posuere velit, ac luctus sem sodales non. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. 13 | 14 | Nam interdum luctus turpis non vestibulum. Ut feugiat rhoncus sem vestibulum rutrum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nam accumsan sapien sem, quis suscipit ante aliquet vitae. Aliquam erat volutpat. Duis erat nisi, cursus quis pretium vitae, tristique a diam. Vestibulum facilisis dignissim facilisis. Curabitur rhoncus interdum dolor, quis suscipit dui bibendum ut. In porttitor metus ac vehicula cursus. Nam iaculis porta fermentum. In elementum consequat congue. -------------------------------------------------------------------------------- /demo/themes/default/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | {{ site.title }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 10 | {% if not entries %} 11 |

There is nothing here.

12 | {% else %} 13 | 14 | {% for entry in entries %} 15 |
16 |
17 |

18 | {% for category in entry.categories %} 19 | {% if loop.index > 1 %} 20 | & 21 | {% endif %} 22 | {{ category }} 23 | {% endfor %} 24 | 25 |

26 |
27 |
28 |

{{ entry.title }}

29 | {{ entry.preview|safe }} 30 | {% if entry.has_more_content %} 31 |

……

32 |

READ MORE

33 | {% endif %} 34 |
35 | 48 |
49 | {% endfor %} 50 | 51 | {% if prev_url or next_url %} 52 | 64 | {% endif %} 65 | 66 | {% endif %} 67 | 68 | {% endblock %} -------------------------------------------------------------------------------- /docs/themes/clean-doc/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | {{ site.title }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 | 10 | {% if not entries %} 11 |

There is nothing here.

12 | {% else %} 13 | 14 | {% for entry in entries %} 15 |
16 |
17 |

18 | {% for category in entry.categories %} 19 | {% if loop.index > 1 %} 20 | & 21 | {% endif %} 22 | {{ category }} 23 | {% endfor %} 24 | 25 |

26 |
27 |
28 |

{{ entry.title }}

29 | {{ entry.preview|safe }} 30 | {% if entry.has_more_content %} 31 |

……

32 |

READ MORE

33 | {% endif %} 34 |
35 | 48 |
49 | {% endfor %} 50 | 51 | {% if prev_url or next_url %} 52 | 64 | {% endif %} 65 | 66 | {% endif %} 67 | 68 | {% endblock %} -------------------------------------------------------------------------------- /docs/pages/deployment.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 部署网站 3 | author: Richard Chien 4 | created: 2017-03-20 5 | updated: 2017-03-22 6 | --- 7 | 8 | 有多种方式可以用来部署 VeriPress 站点,具体使用哪种,取决于你的使用环境和使用习惯。 9 | 10 | ## 静态部署 11 | 12 | ### 生成静态页面 13 | 14 | 通过 `veripress generate` 命令,可以在 VeriPress 实例的 `_deploy` 目录中生成网站的所有静态文件、页面。如果 `_deploy` 目录已存在且不为空,则会首先**清除该目录中的非隐藏文件**,或者说非 `.` 开头的文件(因为在 Windows 上这些并不一定是隐藏文件)。 15 | 16 | `generate` 命令还会提示你输入一个「application root」,这个也就是网站的子目录路径,例如,如果你的网站打算跑在 `http://example.com/blog/`,则这里你需要填 `/blog/`,而如果跑在 `http://example.com/`,则这里保持默认的 `/`。 17 | 18 | ### 部署到 GitHub Pages 19 | 20 | 生成了静态页面之后你可以在各种地方部署,很多人会将静态页面部署在 GitHub Pages,因此 VeriPress 在命令行界面中加入了命令来简化这个操作。 21 | 22 | 首先你需要在 GitHub 创建一个仓库用来存放页面,假设你的 GitHub 账号是 username,则可以创建一个名为 `username.github.io` 的仓库,这个仓库将可以通过 `https://username.github.io/` 直接访问,而如果创建其它名称的仓库,假设 my-blog,则可以通过 `https://username.github.io/my-blog/` 访问(这种情况下,你就需要使用 `/my-blog/` 作为生成静态页面时的「application root」) 23 | 24 | 然后运行下面命令(这里假设你已经在系统中生成 SSH key 并添加到 GitHub,如果没有,请参考 [Connecting to GitHub with SSH](https://help.github.com/articles/connecting-to-github-with-ssh/)): 25 | 26 | ```sh 27 | $ veripress setup-github-pages 28 | Please enter your GitHub repo name, e.g. "someone/the-repo": username/blog 29 | Please enter your name (for git config): User Name 30 | Please enter your email (for git config): username@example.com 31 | Initialized empty Git repository in /root/a/_deploy/.git/ 32 | $ veripress deploy 33 | ``` 34 | 35 | 即可将前面生成的静态页面部署到 GitHub 仓库。之后你可能还需要在 GitHub 仓库的「Settings」中,将 GitHub Pages 的「Source」设置为「master branch」。 36 | 37 | 如果 `deploy` 命令不能符合你的需求,你也可以自己使用 `git` 命令来操作,效果是一样的。 38 | 39 | ## 动态部署 40 | 41 | ### `serve` 命令 42 | 43 | 动态部署也就是直接运行 Python web app。默认提供了 `serve` 命令来进行动态部署,使用方式如下: 44 | 45 | ```sh 46 | $ veripress serve --host 0.0.0.0 --port 8000 47 | ``` 48 | 49 | 不加参数的情况下默认监听 `127.0.0.1:8080`。 50 | 51 | 这个命令会首先尝试使用 `gevent.wsgi` 包的 `WSGIServer` 来运行,如果你系统中没有安装 gevent,则会使用 Flask app 的 run 方法。后者是用在开发环境的方法,不应该实际应用中使用,所以如果你打算使用 `serve` 命令部署,则应该先安装 gevent: 52 | 53 | ```sh 54 | $ pip install gevent 55 | ``` 56 | 57 | ### 使用其它 WSGI 服务器 58 | 59 | VeriPress 主 app 对象在 `veripress` 包中,由于基于 Flask,这个 app 对象直接是一个 WSGI app,所以你可以使用任何可以部署 WSGI app 的服务器来部署 VeriPress 实例,例如使用 Gunicorn(需要在 VeriPress 实例目录中执行,或设置 `VERIPRESS_INSTANCE_PATH` 环境变量): 60 | 61 | ```sh 62 | $ gunicorn -b 0.0.0.0:8000 veripress:app 63 | ``` 64 | 65 | 其它更多部署方法请参考 Flask 官方文档的 [Deployment Options](http://flask.pocoo.org/docs/0.12/deploying/)。 66 | 67 | ## 缓存 68 | 69 | 动态部署时 VeriPress 可以使用缓存来加快页面的访问,具体的配置方法请参考 [配置文件](configuration-file.html#CACHE-TYPE)。 70 | -------------------------------------------------------------------------------- /veripress_cli/deploy.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from datetime import datetime 4 | 5 | import click 6 | 7 | from veripress_cli import cli 8 | from veripress_cli.helpers import makedirs 9 | 10 | 11 | def validate_repo_name(ctx, param, value): 12 | m = re.fullmatch('([._\-A-Z0-9a-z]+)/([._\-A-Z0-9a-z]+)', value) 13 | if not m: 14 | raise click.BadParameter('The repo name is invalid.') 15 | return m.groups() 16 | 17 | 18 | @cli.command('setup-github-pages', short_help='Setup GitHub Pages.', 19 | help='This command will setup the deploy folder ' 20 | 'as a GitHub Pages repo.') 21 | @click.option('--repo', '-r', 22 | prompt='Please enter your GitHub repo name, ' 23 | 'e.g. "someone/the-repo"', 24 | help='The GitHub repo name you want to deploy to.', 25 | callback=validate_repo_name) 26 | @click.option('--name', '-n', prompt='Please enter your name (for git config)', 27 | help='Your name set in git config.') 28 | @click.option('--email', '-e', 29 | prompt='Please enter your email (for git config)', 30 | help='Your email set in git config') 31 | def setup_command(repo, name, email): 32 | user, repo = repo 33 | 34 | from veripress_cli.generate import get_deploy_dir 35 | deploy_dir = get_deploy_dir() 36 | makedirs(deploy_dir, mode=0o755, exist_ok=True) 37 | 38 | os.system('git -C "{}" init'.format(deploy_dir)) 39 | os.system('git -C "{}" config user.email "{}"'.format(deploy_dir, email)) 40 | os.system('git -C "{}" config user.name "{}"'.format(deploy_dir, name)) 41 | os.system('git -C "{}" remote add origin git@github.com:{}/{}.git'.format( 42 | deploy_dir, user, repo)) 43 | 44 | 45 | @cli.command('deploy', short_help='Deploy GitHub Pages.', 46 | help='This command will deploy the static pages to GitHub Pages.') 47 | def deploy_command(): 48 | from veripress_cli.generate import get_deploy_dir 49 | deploy_dir = get_deploy_dir() 50 | makedirs(deploy_dir, mode=0o755, exist_ok=True) 51 | 52 | os.system('git -C "{}" add .'.format(deploy_dir)) 53 | if os.system( 54 | 'git -C "{}" diff --quiet --exit-code'.format(deploy_dir)) == 0 \ 55 | and os.system( 56 | 'git -C "{}" diff --quiet --cached --exit-code'.format( 57 | deploy_dir)) == 0: 58 | click.echo('There are no changes to be deployed.') 59 | return 60 | 61 | dt = datetime.now() 62 | os.system('git -C "{}" commit -m "Updated on {} at {}"'.format( 63 | deploy_dir, dt.strftime('%Y-%m-%d'), dt.strftime('%H:%M:%S'))) 64 | os.system( 65 | 'git -C "{}" push --set-upstream origin master'.format(deploy_dir)) 66 | -------------------------------------------------------------------------------- /demo/posts/1990-12-30-sollicitudin-aliquam-metus.md: -------------------------------------------------------------------------------- 1 | --- 2 | categories: [Lorem] 3 | tags: [WorldWideWeb, Web, Internet] 4 | --- 5 | 6 | Vestibulum lacinia neque eget ante lacinia vulputate. Praesent tristique, elit nec dapibus dapibus, dui odio pellentesque neque, ut ultricies elit mauris tincidunt ex. Integer lobortis imperdiet lorem, nec sollicitudin mi. Nunc tincidunt mauris a tempus accumsan. Mauris sit amet mi dolor. Aenean at vehicula augue. In hac habitasse platea dictumst. 7 | 8 | Duis sollicitudin aliquam metus, at gravida orci tempus vel. Phasellus feugiat aliquet justo, in aliquam lacus maximus vitae. In a lacus vitae turpis rutrum suscipit ac vel urna. Nunc at ligula nec lacus dictum posuere. Quisque id tortor interdum, varius diam at, tincidunt tellus. Donec tincidunt lacus ut leo convallis facilisis. Etiam sed tempus nisi, ut tincidunt mi. Praesent ex libero, varius vel risus id, interdum ullamcorper justo. Nam volutpat sed quam vel accumsan. Proin non justo mauris. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse tristique erat ut eros maximus scelerisque. Cras vehicula orci lobortis, tincidunt elit ut, tempus sem. Donec efficitur turpis sit amet ante vestibulum fringilla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. 9 | 10 | Fusce vitae enim sed lorem interdum dapibus at vitae lectus. Phasellus velit urna, placerat a laoreet lobortis, pharetra non tortor. Suspendisse justo massa, posuere non pretium non, tincidunt eu dui. Duis venenatis orci leo, at maximus arcu iaculis et. Mauris dapibus, nisi ut gravida consectetur, massa purus tincidunt nisl, eu vehicula quam ligula ac justo. Curabitur ut porta quam, et porttitor mi. Proin lobortis tortor ut leo vulputate, ac suscipit orci tincidunt. 11 | 12 | Donec aliquet luctus vehicula. Donec pharetra ante mi, sit amet rutrum magna euismod non. Nulla interdum justo pretium elit consequat sollicitudin. Maecenas eu mi nec neque efficitur sodales et nec odio. Nunc lectus ex, aliquet posuere commodo eu, rutrum id orci. Nulla in diam ligula. Integer pellentesque in nisl quis congue. Vestibulum consequat porttitor lectus sit amet aliquam. In auctor ultrices justo sit amet pharetra. Nam feugiat iaculis mauris nec volutpat. Morbi libero lorem, iaculis et elit nec, egestas rutrum lorem. Integer consectetur imperdiet vestibulum. 13 | 14 | Cras pretium nulla ut sem molestie mollis. Donec vitae mollis nisi. Nunc nec ipsum felis. Curabitur tempus id erat sed cursus. Pellentesque eget sem nec elit pulvinar hendrerit sit amet ut dui. Donec sed vestibulum justo. Curabitur lacinia tortor eu nisl pulvinar, sed ornare dui faucibus. Duis at ullamcorper lectus. Maecenas ante tortor, sodales vitae tempus quis, consectetur non est. 15 | -------------------------------------------------------------------------------- /demo/posts/1969-09-02-nam-nec-nunc-eros.md: -------------------------------------------------------------------------------- 1 | --- 2 | categories: Lorem 3 | tags: [ARPA, Internet] 4 | --- 5 | 6 | Integer et tincidunt erat. Nam nec nunc eros. Nam cursus lorem quis dui rhoncus placerat. Nunc sit amet molestie odio. Sed eget porta nunc, quis vulputate lectus. In hac habitasse platea dictumst. Etiam congue odio eros, in faucibus diam aliquet ut. Pellentesque accumsan, orci non pretium accumsan, urna nisl volutpat tortor, et dignissim leo arcu ac erat. Suspendisse et volutpat nisi. Vestibulum cursus convallis enim, sit amet suscipit dui hendrerit id. Nullam scelerisque, dui id tempus hendrerit, turpis augue dictum elit, laoreet eleifend lacus est a nisi. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi congue magna odio, eget interdum arcu efficitur a. In pretium tortor id hendrerit pellentesque. 7 | 8 | ## Ultricies Vehicula 9 | 10 | Vivamus tristique mi eu purus ultricies vehicula. Sed fermentum consequat fringilla. Morbi molestie, purus ut suscipit sollicitudin, ligula justo sagittis turpis, eget venenatis turpis velit eu dui. Etiam venenatis nisi in metus consectetur scelerisque. Mauris nec erat eu quam lacinia commodo. Phasellus posuere tellus sapien, a iaculis risus molestie vitae. Nunc sem dolor, ornare commodo aliquet vitae, hendrerit at sapien. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 11 | 12 | Fusce lobortis eu mi ultrices convallis. Nam ut justo nec lectus dictum ultricies vel sit amet eros. Nam ante odio, pulvinar a dapibus in, convallis eu tortor. Phasellus rhoncus interdum ante. Nulla magna magna, viverra id magna nec, placerat facilisis arcu. Fusce nec bibendum ligula, eu aliquet tortor. Praesent sit amet ipsum fermentum ante dignissim mollis et nec libero. Quisque sit amet efficitur tellus. Cras non est quam. Duis ullamcorper augue in quam mattis tincidunt. Cras quis gravida quam. 13 | 14 | ## Placerat Fringilla 15 | 16 | Mauris sapien mi, placerat fringilla eleifend sed, rutrum et metus. Ut quis neque dolor. Sed non faucibus enim. Aliquam vitae varius magna, ac congue ex. Nunc tempus, dui sit amet lobortis suscipit, felis mi varius tortor, eu bibendum arcu est in quam. In gravida non urna vel gravida. Phasellus non congue nisl. Cras nec nisl ut massa varius malesuada. Aliquam erat volutpat. 17 | 18 | ### Ullamcorper eget 19 | 20 | Morbi nulla turpis, euismod non ullamcorper eget, egestas non dui. Quisque efficitur enim lacus, tincidunt consectetur nunc blandit nec. Fusce et eros eu odio laoreet dignissim eget finibus metus. Nulla pellentesque faucibus tincidunt. Mauris lectus ipsum, mattis sed sem eget, tempor sodales augue. Proin at massa sed velit lacinia pharetra. Curabitur nisl mi, faucibus vel enim at, consequat pulvinar purus. Donec venenatis, quam ut tincidunt facilisis, arcu leo sodales nisl, sed rutrum libero leo vel nunc. Ut augue leo, consectetur nec luctus nec, scelerisque a ligula. Duis imperdiet, velit quis egestas tempor, elit nulla rhoncus justo, non vehicula magna purus vitae nibh. 21 | -------------------------------------------------------------------------------- /docs/pages/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 安装 3 | author: Richard Chien 4 | created: 2017-03-19 5 | updated: 2017-03-22 6 | --- 7 | 8 | 要使用 VeriPress,你的电脑上需要安装有 Python 3.4 或更新版本和 pip 命令。如果你的系统中同时安装有 Python 2.x 版本,你可能需要将下面的 `python` 和 `pip` 命令换成 `python3` 和 `pip3`,此外对于非 root 或非管理员用户,还需要加 `sudo` 或使用管理员身份启动命令行。 9 | 10 | ## 系统全局安装 11 | 12 | 使用 pip 命令即可从 PyPI 上安装最新的 release 版本: 13 | 14 | ```sh 15 | $ pip install veripress 16 | ``` 17 | 18 | 安装之后一个 `veripress` 命令会被安装在系统中,通常在 `/usr/local/bin/veripress`。在命令行中运行 `veripress --help` 可以查看命令的使用帮助,当然,如果这是你第一次使用,你可能更需要首先阅读后面的 [开始使用](getting-started.html) 文档。 19 | 20 | ## 在 virtualenv 中安装 21 | 22 | 由于安装 VeriPress 的同时会安装一些依赖包,你可能不希望这些依赖装到系统的全局环境,这种情况下,使用 virtualenv 创建一个虚拟环境是一种不错的选择。 23 | 24 | 如果你还没有安装 virtualenv,请使用下面命令安装: 25 | 26 | ```sh 27 | $ pip install virtualenv 28 | ``` 29 | 30 | 然后到一个适当的目录,运行下列命令: 31 | 32 | ```sh 33 | $ mkdir my-veripress 34 | $ cd my-veripress 35 | $ virtualenv venv 36 | ``` 37 | 38 | 这将会在 `venv` 文件夹中创建一个虚拟环境,要使用这个虚拟环境,运行如下: 39 | 40 | ```sh 41 | $ source venv/bin/activate # Linux or macOS 42 | $ venv\Scripts\activate # Windows 43 | ``` 44 | 45 | 然后安装 VeriPress: 46 | 47 | ```sh 48 | $ pip install veripress 49 | ``` 50 | 51 | 要退出虚拟环境,运行: 52 | 53 | ```sh 54 | $ deactivate 55 | ``` 56 | 57 | 在 virtualenv 中使用可以获得一个隔离的环境,但同时也需要多余的命令来进入和离开虚拟环境,因此你需要根据情况选择适合自己的安装方式。 58 | 59 | ## 在 Windows 上安装 60 | 61 | 在 Windows 上安装 VeriPress 没有什么特殊要求,只要正确安装了 Python 和 pip,就可以正常使用 `pip install veripress` 来安装,同样你也可以使用 virtualenv。 62 | 63 | 但由于 Windows 的特殊性,如果你在安装和之后的使用过程中遇到了问题,请提交 issue 反馈。 64 | 65 | 此外,后面的文档中给出的示例命令将会统一使用 Unix 命令,一般在 Windows 上都有相对应的命令可以完成同样的操作(比如创建文件夹)。 66 | 67 | ## 使用 Docker 68 | 69 | VeriPress 官方提供了简便易用的 docker 镜像,如果你的系统中安装了 docker,并且希望在比较隔离的环境中使用 VeriPress,可以考虑通过 docker 来安装。直接拉取 DockerHub 的镜像: 70 | 71 | ```sh 72 | $ docker pull veripress/veripress 73 | ``` 74 | 75 | 镜像的最新版本(latest)将和 GitHub 上最新的 tag 一致,同时也和 PyPI 上的最新版本一致。 76 | 77 | 由于众所周知的原因,如果你无法直接从 DockerHub 拉取镜像,也可以从 DaoCloud 的镜像仓库拉取: 78 | 79 | ```sh 80 | $ docker pull daocloud.io/richardchien/veripress 81 | ``` 82 | 83 | 你也可以选择使用其它 DockerHub 镜像,但可能无法和这两者完全同步更新。 84 | 85 | 镜像使用方式如下: 86 | 87 | ```sh 88 | $ docker run -ti --rm -v $(pwd):/instance veripress/veripress --help 89 | ``` 90 | 91 | 这将会把当前目录挂载到容器中的 `/instance` 目录,作为 VeriPress 的实例目录(在下一篇 [开始使用](getting-started.html) 中你将会了解到什么是「实例目录」。镜像的 `ENTRYPOINT` 是 `veripress` 命令,因此直接在 `docker run` 命令的结尾加上 `veripress` 的子命令即可使用,在后面的文档中将不再对 docker 进行单独阐述,使用方式都是一致的。 92 | 93 | 建议把 `docker run -ti --rm -v $(pwd):/instance veripress/veripress` alias 成一个简短的命令,这样可以更方便的使用(基本和使用本地命令没差)。另外,在要运行 VeriPress 实例时,需要加 `-p` 来进行端口映射。记得使用单引号来避免定义 alias 时 `$(pwd)` 被求值。例如: 94 | 95 | ```sh 96 | $ alias veripress='docker run -ti --rm -v $(pwd):/instance -p 8080:8080 veripress/veripress' 97 | $ mkdir ~/my-instance 98 | $ cd ~/my-instance 99 | $ veripress init 100 | $ veripress theme install default 101 | $ veripress preview --host 0.0.0.0 --port 8080 102 | ``` 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VeriPress 2 | 3 | **This project is deprecated, please use [PurePress](https://github.com/richardchien/purepress) instead.** 4 | 5 | [![License](https://img.shields.io/github/license/veripress/veripress.svg)](LICENSE) 6 | [![Build Status](https://travis-ci.org/veripress/veripress.svg?branch=master)](https://travis-ci.org/veripress/veripress) 7 | [![Coverage Status](https://coveralls.io/repos/github/veripress/veripress/badge.svg?branch=master)](https://coveralls.io/github/veripress/veripress?branch=master) 8 | [![PyPI](https://img.shields.io/pypi/v/veripress.svg)](https://pypi.python.org/pypi/veripress) 9 | ![Python](https://img.shields.io/badge/python-3.4%2B-blue.svg) 10 | [![Tag](https://img.shields.io/github/tag/veripress/veripress.svg)](https://github.com/veripress/veripress/tags) 11 | [![Docker Repository](https://img.shields.io/badge/docker-veripress/veripress-blue.svg)](https://hub.docker.com/r/veripress/veripress/) 12 | [![Docker Pulls](https://img.shields.io/docker/pulls/veripress/veripress.svg)](https://hub.docker.com/r/veripress/veripress/) 13 | 14 | VeriPress is a blog engine for hackers, which is very similar to Octopress and Hexo, but with some different features. It's written in Python 3.4+ based on Flask web framework. Here is a [demo](https://veripress.github.io/demo/). 15 | 16 | ## Features 17 | 18 | - Supports three publish types: post, page, widget 19 | - Theme management 20 | - Custom post/page layout 21 | - Supports Markdown, HTML and plain TXT 22 | - Run as dynamic web app 23 | - Generating static HTML pages 24 | - API mode 25 | - Atom feed 26 | - and more... 27 | 28 | ## Quick Start 29 | 30 | It's dead easy to get started with VeriPress: 31 | 32 | ```sh 33 | $ pip3 install veripress # Install VeriPress 34 | $ mkdir ~/my-veripress # Create an empty folder as a VeriPress instance 35 | $ cd ~/my-veripress 36 | $ veripress init # Initialize the VeriPress instance 37 | $ veripress theme install default # Install the "default" theme 38 | $ veripress preview # Preview the instance 39 | ``` 40 | 41 | Run the above commands and then you can visit the very initial VeriPress instance at `http://127.0.0.1:8080/`. 42 | 43 | See [documentation](https://veripress.github.io/docs/) for more information on how to use VeriPress. 44 | 45 | ## Documentation 46 | 47 | Documentation is now available in [Simplified Chinese (简体中文)](https://veripress.github.io/docs/), and the English version is coming soon. 48 | 49 | ## Themes 50 | 51 | There are some official themes [here](https://github.com/veripress/themes), and also a theme collection [here](https://stdrc.cc/post/2018/10/13/collection-of-veripress-themes/) (in Simplified Chinese). 52 | 53 | ## Contributing 54 | 55 | If you want to help to develop VeriPress, fork this repo and send me a pull request. Source codes of docs and demo are also available in this repo, so if you find mistakes, feel free to send me a pull request too. 56 | 57 | If you just have some questions or bug reports, you can also submit issues in this repo. 58 | 59 | Thanks for your support and help. 60 | -------------------------------------------------------------------------------- /demo/posts/1969-10-29-imperdiet-ligula.md: -------------------------------------------------------------------------------- 1 | --- 2 | created: 1969-10-29 22:30:00 3 | categories: Lorem 4 | tags: [ARPA, Internet] 5 | --- 6 | 7 | Etiam rutrum, nibh ac dictum semper, nunc justo tempus lacus, ut lacinia elit justo posuere sem. Etiam porttitor eget elit sed volutpat. Integer dapibus tempus urna. Integer suscipit nibh augue, eu tincidunt sapien consequat sed. Aliquam rutrum pretium orci et efficitur. Donec vestibulum leo dolor, eu auctor nulla eleifend volutpat. Donec at dictum mi. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Praesent a ex a risus dignissim finibus vel ut libero. 8 | 9 | Aenean id urna diam. Aliquam at imperdiet ligula. Morbi tincidunt tempus lacus sed ultrices. Proin dapibus enim non hendrerit egestas. Nulla feugiat metus at mauris pulvinar, ac iaculis est iaculis. Integer eu quam ut elit blandit elementum. Quisque lobortis bibendum est, a pulvinar diam tincidunt sed. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis sodales nisl nunc, id tincidunt ex tempus tempor. Praesent quis vehicula dui. Cras et turpis ac tellus rhoncus placerat ut vel tortor. Etiam est quam, euismod id nisl ac, sollicitudin dictum nulla. Phasellus vel ultrices risus. 10 | 11 | Fusce non malesuada mi. Phasellus et pharetra diam. Aliquam sit amet arcu id velit sodales rutrum. Nam finibus efficitur arcu, vitae maximus magna tempus vel. Curabitur in accumsan lectus. Quisque congue sapien rhoncus diam congue dignissim. Suspendisse potenti. Donec venenatis diam quis felis rhoncus, et fringilla erat pretium. Pellentesque et egestas ligula. Interdum et malesuada fames ac ante ipsum primis in faucibus. Suspendisse nec augue eget odio gravida pulvinar eu et erat. Aenean sodales tortor vel arcu blandit gravida. Sed bibendum sapien nulla, ut pretium purus vulputate ac. Maecenas vehicula suscipit justo ut egestas. 12 | 13 | Phasellus tristique elementum nisl, vel convallis enim. Vivamus tempus in magna lacinia lobortis. Ut metus mi, imperdiet quis consectetur a, aliquam non massa. Aliquam dictum ut est a egestas. Mauris ut feugiat ligula. Proin ornare odio sit amet augue posuere, vel ultrices ligula ultrices. Proin cursus tristique dui nec tincidunt. Duis mollis massa ac elementum cursus. Maecenas rhoncus, velit ac vehicula tincidunt, mauris diam faucibus ex, imperdiet vehicula elit velit vel lorem. Nulla vel nunc sit amet libero venenatis efficitur. Proin ac nisl non lorem porttitor imperdiet sit amet et erat. 14 | 15 | Aenean tincidunt dui ac ipsum tincidunt, vitae facilisis nunc molestie. Morbi ut est scelerisque, efficitur nisi ac, dignissim lorem. Sed quam elit, tincidunt vitae neque vitae, efficitur auctor magna. Sed eu velit vitae lectus dapibus posuere. Fusce nisl enim, faucibus eu mollis non, ultrices feugiat ligula. Phasellus mollis tristique arcu, aliquet viverra urna efficitur vel. Maecenas maximus eros mi, ac egestas libero varius eu. Vestibulum posuere felis vel consequat sodales. Etiam dapibus leo quis accumsan auctor. Fusce sed orci a turpis porttitor laoreet. 16 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | from veripress.helpers import * 4 | 5 | 6 | def test_to_list(): 7 | assert to_list(123) == [123] 8 | assert to_list([1, 2, 3]) == [1, 2, 3] 9 | assert to_list(('a', 'b', 'c')) == ['a', 'b', 'c'] 10 | assert to_list('abc') == ['abc'] 11 | assert to_list(b'abc') == [b'abc'] 12 | assert to_list(filter(lambda x: x > 2, [1, 2, 3, 4])) == [3, 4] 13 | 14 | 15 | def test_to_datetime(): 16 | d = date(year=2016, month=10, day=22) 17 | dt = datetime.strptime('2016/10/22', '%Y/%m/%d') 18 | assert isinstance(to_datetime(d), datetime) 19 | assert to_datetime(d) == dt 20 | assert id(to_datetime(dt)) == id(dt) 21 | assert to_datetime('other things') == 'other things' 22 | 23 | 24 | def test_timezone_from_str(): 25 | tz = timezone_from_str('UTC+08:00') 26 | assert tz == timezone(timedelta(hours=8, minutes=0)) 27 | assert timezone_from_str('Asia/Shanghai').zone == 'Asia/Shanghai' 28 | assert timezone_from_str('Asia/NoWhere') is None 29 | 30 | 31 | def test_configuration_error(): 32 | with raises(Exception, message='Storage type "database" if not supported'): 33 | raise ConfigurationError('Storage type "database" if not supported') 34 | 35 | 36 | def test_url_rule(): 37 | class FakeBlueprint(object): 38 | def __init__(self): 39 | self.rules = [] 40 | 41 | def add_url_rule(self, rule, **kwargs): 42 | self.rules.append(rule) 43 | 44 | fake_bp = FakeBlueprint() 45 | url_rule(fake_bp, '/posts/') 46 | url_rule(fake_bp, ['/posts/']) 47 | assert fake_bp.rules == ['/posts/', '/posts/'] 48 | 49 | 50 | def test_pair(): 51 | pair = Pair() 52 | assert pair.first is None and pair.second is None 53 | assert not pair 54 | 55 | pair = Pair(1, 'a') 56 | assert pair 57 | assert pair == Pair(1, 'a') 58 | assert pair != [] 59 | assert 1 in pair and 'a' in pair 60 | assert 2 not in pair 61 | a, b = pair 62 | assert a == 1 and b == 'a' 63 | assert '(1, \'a\')' in repr(pair) 64 | 65 | pair += (2, 'b') 66 | assert pair == Pair(3, 'ab') 67 | with raises(ValueError): 68 | pair += (1, 2, 3) 69 | with raises(TypeError): 70 | pair += ('a', 2) 71 | 72 | assert Pair(100, 200) - Pair(100, 200) == Pair(0, 0) 73 | 74 | pair = Pair(2, 'b') 75 | assert pair[0] == 2 76 | assert pair[1] == 'b' 77 | with raises(IndexError): 78 | a = pair[2] 79 | 80 | assert len(pair) == 2 81 | 82 | 83 | def test_traverse_dir(): 84 | paths = list(traverse_directory(os.getcwd())) 85 | assert os.path.join(os.getcwd(), 'tests', 'test_helpers.py') in paths 86 | assert os.path.join(os.getcwd(), 'tests') not in paths 87 | 88 | paths = list(traverse_directory(os.getcwd(), yield_dir=True)) 89 | assert os.path.join(os.getcwd(), 'tests', 'test_helpers.py') in paths 90 | assert os.path.join(os.getcwd(), 'tests') + os.path.sep in paths 91 | 92 | paths = list(traverse_directory('/non-exists')) 93 | assert len(paths) == 0 94 | -------------------------------------------------------------------------------- /docs/pages/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 开始使用 3 | author: Richard Chien 4 | created: 2017-03-20 5 | updated: 2017-05-24 6 | --- 7 | 8 | VeriPress 的使用以一个实例(instance)为单位,比如你使用它搭建一个博客,这个博客就是一个实例。一个实例的所有相关文件都保存在一个目录中,可以很方便地管理。 9 | 10 | ## 创建实例 11 | 12 | 首先在适当的位置创建一个目录,通常情况下空目录就可以,如果你使用 virtualenv,也可以在里面先创建虚拟环境。 13 | 14 | 然后 cd 进入这个目录,执行初始化命令,如: 15 | 16 | ```sh 17 | $ mkdir my-veripress 18 | $ cd my-veripress 19 | $ veripress init 20 | ``` 21 | 22 | 如果你想在系统的其它位置也能控制这个实例,可以设置环境变量 `VERIPRESS_INSTANCE_PATH` 为你想控制的实例的绝对路径,例如: 23 | 24 | ```sh 25 | $ export VERIPRESS_INSTANCE_PATH=/home/user/my-veripress 26 | ``` 27 | 28 | 之后你就可以在其他目录执行 `veripress` 命令来控制这个实例。 29 | 30 | ## 初始目录结构 31 | 32 | 上面的初始化命令将会在实例目录创建若干子目录和文件: 33 | 34 | | 文件/子目录 | 作用 | 35 | | ----------- | ---------------------- | 36 | | `config.py` | 实例的配置文件 | 37 | | `site.json` | 网站信息 | 38 | | `static` | 全局的静态文件(默认有一个 favicon) | 39 | | `themes` | 存放主题 | 40 | | `posts` | 存放文章(post) | 41 | | `pages` | 存放自定义页面(page) | 42 | | `widgets` | 存放页面部件(widget) | 43 | 44 | ## 修改网站信息 45 | 46 | 网站的标题、作者等信息在 `site.json`,用 JSON 格式编写,你可以自行修改。 47 | 48 | 每一项的说明如下: 49 | 50 | | 项 | 说明 | 51 | | ---------- | ---------------------------------------- | 52 | | `title` | 网站标题 | 53 | | `subtitle` | 网站副标题,对于支持副标题的主题有效 | 54 | | `author` | 网站作者,若文章和页面没有标注作者,则默认使用此项 | 55 | | `email` | 网站作者 email,若文章和页面没有标注作者 email,则默认使用此项 | 56 | | `root_url` | 指定网站的根 URL,不要加结尾的 `/`,如果网站在子目录中,请不要加子目录,如网站在 `http://example.com/blog/` 则填写 `http://example.com`,此项用于生成评论框和 Atom 订阅所需的页面完整链接,但不会影响除了评论框和 Atom 订阅之外的其它功能 | 57 | | `timezone` | 可选,用于在生成 Atom 订阅时指定时区,格式类似 `UTC+08:00`,默认为 `UTC+00:00` | 58 | | `language` | 可选,指定网站主要使用的语言,如 `en`、`zh-cn`、`ja` 等,用于在生成的 HTML 中告知浏览器,在某些语言环境下,会影响内容的显示字体 | 59 | 60 | ## 安装默认主题 61 | 62 | 初始化之后的实例默认使用 default 主题,因此必须首先安装 default 主题才可以运行网站。使用下面命令安装(此命令需要系统中安装有 Git): 63 | 64 | ```sh 65 | $ veripress theme install default 66 | ``` 67 | 68 | 它将从官方的 [veripress/themes](https://github.com/veripress/themes) 仓库中安装 default 主题。关于主题的更多信息,请参考 [主题](theme.html)。 69 | 70 | ## 预览网站 71 | 72 | 安装主题之后,就可以预览网站了,使用下面命令: 73 | 74 | ```sh 75 | $ veripress preview 76 | ``` 77 | 78 | 默认将会在 `127.0.0.1:8080` 开启一个 HTTP 服务器,可以通过 `--host` 和 `--port` 来修改,例如: 79 | 80 | ```sh 81 | $ veripress preview --host 0.0.0.0 --port 8000 82 | ``` 83 | 84 | 此时你已经可以通过浏览器访问 `http://127.0.0.1:8080/` 了,可以看到默认的《Hello, world!》文章以及侧边栏上默认的《Welcome!》页面部件,访问 `http://127.0.0.1:8080/hello/` 可以看到一个默认的自定义页面,这三者分别在 `posts`、`widgets`、`pages` 目录中。 85 | 86 | ## 添加你的第一篇文章! 87 | 88 | 在 `posts` 目录创建一个新的文件,按照 `2017-03-20-my-first-post.md` 的格式命名,这里我们以 Markdown 为例,所以使用 `.md` 扩展名。 89 | 90 | 添加内容如下: 91 | 92 | ```md 93 | --- 94 | title: 我的第一篇文章! 95 | --- 96 | 97 | ## 这是标题 98 | 99 | 一段文字…… 100 | ``` 101 | 102 | 然后重新运行 `veripress preview` 即可看到这篇文章。 103 | 104 | 关于如何撰写文章、自定义页面、页面部件的更多信息,请参考 [撰写内容](writing.html)。 105 | -------------------------------------------------------------------------------- /tests/test_toc.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from veripress.model.toc import HtmlTocParser 4 | 5 | 6 | def html_same(html1, html2): 7 | return re.sub('\s', '', html1) == re.sub('\s', '', html2) 8 | 9 | 10 | html_example = """ 11 |
a very small title
12 |

Title

13 |

Title

14 |

Another title, yes!

15 |

中文,标题 Title&

16 |

a random paragraph...

17 | & < 18 | 19 |

Another-h1-1

20 |
a very small title
21 | """ 22 | 23 | 24 | def test_toc_parser(): 25 | parser = HtmlTocParser() 26 | parser.feed('') 27 | assert parser.toc() == [] 28 | assert parser.toc_html() == '' 29 | 30 | parser = HtmlTocParser() 31 | parser.feed('no-effect') 32 | assert html_same(parser.html, 'no-effect') 33 | 34 | parser.feed('

Title

') 35 | assert html_same(parser.html, 36 | 'no-effect

Title

') 38 | 39 | parser = HtmlTocParser() 40 | parser.feed(html_example) 41 | expected_toc = [{'level': 6, 'id': 'a-very-small-title', 42 | 'text': 'a very small title', 'inner_html': 'a very small title', 43 | 'children': []}, 44 | {'level': 1, 'id': 'Title', 'text': 'Title', 'inner_html': 'Title', 45 | 'children': [ 46 | {'level': 2, 'id': 'Title_1', 'text': 'Title', 'inner_html': 'Title', 'children': [ 47 | {'level': 4, 'id': 'Another-title-yes', 48 | 'text': 'Another title, yes!', 'inner_html': 'Another title, yes!', 49 | 'children': []}, 50 | {'level': 3, 'id': '中文-标题-Title-amp', 51 | 'text': '中文,标题 Title&', 'inner_html': '中文,标题 Title&', 52 | 'children': []} 53 | ]} 54 | ]}, 55 | {'level': 1, 'id': 'Another-h1-1', 56 | 'text': 'Another-h1-1', 'inner_html': 'Another-h1-1', 57 | 'children': [ 58 | {'level': 5, 'id': 'a-very-small-title_1', 59 | 'text': 'a very small title', 'inner_html': 'a very small title', 60 | 'children': []} 61 | ]}] 62 | assert parser.toc() == expected_toc 63 | 64 | expected_toc_html = """ 65 | 77 | """ 78 | assert html_same(parser.toc_html(depth=2, lowest_level=5), expected_toc_html) 79 | -------------------------------------------------------------------------------- /docs/pages/theme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 主题 3 | author: Richard Chien 4 | created: 2017-03-20 5 | updated: 2017-03-20 6 | --- 7 | 8 | VeriPress 原生支持主题切换,通过配置文件的 `THEME` 配置项来指定要使用的主题,内部通过这个配置项,来渲染相应主题目录中的模板文件。与此同时,`veripress` 命令还提供了方便的主题管理系列子命令。 9 | 10 | ## 安装官方主题 11 | 12 | 目前官方主题有 default、clean-doc 等,可以在 [veripress/themes](https://github.com/veripress/themes) 查看最新的官方主题列表和预览(或截图)。 13 | 14 | 要安装官方主题,使用如下命令: 15 | 16 | ```sh 17 | $ veripress theme install theme-name # theme-name 换成要安装的主题名称 18 | ``` 19 | 20 | 这个命令会使用 Git 将 [veripress/themes](https://github.com/veripress/themes) 仓库中与指定的主题名同名的分支克隆到本地的 `themes` 目录,并默认使用同样的名字作为本地的主题名称。例如,上面给出的命令将把 [veripress/themes](https://github.com/veripress/themes) 的 theme-name 分支克隆到本地的 `themes/theme-name` 目录。 21 | 22 | 如果你想在本地使用不同的主题名,比如把官方的 clean-doc 安装为本地的 doc 主题,那么可以使用 `--name` 参数来指定,如: 23 | 24 | ```sh 25 | $ veripress theme install clean-doc --name doc 26 | ``` 27 | 28 | 这将会把 clean-doc 主题安装到 `themes/doc` 目录,从而你可以把配置文件的 `THEME` 设置为 `doc` 来使用它,而不是 `clean-doc`。 29 | 30 | ## 安装第三方主题 31 | 32 | `veripress theme install` 命令同样可以用来安装 GitHub 上的第三方主题,例如你想安装的主题在 someone/the-theme 仓库(的 master 分支),则可以使用下面命令来安装它: 33 | 34 | ```sh 35 | $ veripress theme install someone/the-theme 36 | ``` 37 | 38 | 不加参数的情况下,会把 master 分支克隆到 `themes`,并以 `the-theme` 作为本地主题名称。你可以通过 `--branch` 和 `--name` 参数指定分支和名称: 39 | 40 | ```sh 41 | $ veripress theme install someone/the-theme --branch the-branch --name theme-name 42 | ``` 43 | 44 | 上面命令会把 someone/the-theme 仓库的 the-branch 分支克隆到 `themes/theme-name` 目录,从而可以将 `THEME` 设置为 `theme-name` 来使用它。 45 | 46 | ## 更新和删除主题 47 | 48 | 下面两条命令分别可以更新和删除已安装的主题: 49 | 50 | ```sh 51 | $ veripress theme update theme-name 52 | $ veripress theme uninstall theme-name 53 | ``` 54 | 55 | 前者相当于执行了 `git pull`,后者相当于删除了 `themes` 目录中的相应主题子目录。 56 | 57 | 另外,已经安装的所有主题可以通过 `veripress theme list` 列出。 58 | 59 | ## 在已有主题的基础上自定义 60 | 61 | 由于主题是一个通用化的东西,可能你在使用的时候需要进行个性化的简单定制,例如修改导航栏、使用自定义布局等。 62 | 63 | 通常,主题的作者在制作主题时,会允许用户将自己的模板文件放在主题 `templates` 目录的 `custom` 子目录中,来覆盖主题本身的同名模板文件,而不影响该主题原先的代码,从而不影响后期的主题更新。此外,VeriPress 在渲染模板文件时,也会优先使用 `custom` 子目录中的同名模板文件。 64 | 65 | 下面先给出两种使用场景,关于模板文件具体如何编写,请参考 [制作主题](making-your-own-theme.html) 和 Jinja2 模板引擎的 [设计文档](http://jinja.pocoo.org/docs/2.9/templates/)。 66 | 67 | ### 修改主题模板的某一部分 68 | 69 | 主题的模板文件中通常使用类似 `include` 的语句来引入每个小部分,以 default 主题为例,它的 `layout.html` 模板中有一行: 70 | 71 | ``` 72 | {% include ['custom/navbar.html', 'navbar.html'] ignore missing %} 73 | ``` 74 | 75 | 这行会优先引入 `templates/custom` 中的 `navbar.html`,如果不存在,则使用主题自带的。因此你可以在 `templates/custom` 中创建自定义的 `navbar.html`,来添加你需要的导航栏项。 76 | 77 | ### 在文章或页面中使用自定义布局 78 | 79 | 还记得文章和页面的 YAML 头部的 `layout` 项吗,默认分别为 `post` 和 `page`,对应主题的 `post.html` 和 `page.html` 模板文件。如果你需要自定义,则可以在主题的 `templates/custom` 目录中创建新的布局的模板文件。 80 | 81 | 例如你需要一个新的名叫 `simple-page` 的布局,就新建模板文件 `templates/custom/simple-page.html`,假设内容如下: 82 | 83 | ```html 84 | 85 | 86 | 87 | {{ entry.title + ' - ' + site.title }} 88 | 89 | 90 |
{{ entry.content|safe }}
91 | 92 | 93 | ``` 94 | 95 | 此时你就可以在自定义页面中指定 `layout` 为 `simple-page`,从而使用上面的模板来显示这个页面,如: 96 | 97 | ``` 98 | --- 99 | title: 一个简单页面 100 | layout: simple-page 101 | --- 102 | 103 | 这是一个非常简单的页面。 104 | ``` 105 | -------------------------------------------------------------------------------- /demo/posts/1991-02-20-congue-fringilla-sapien.md: -------------------------------------------------------------------------------- 1 | --- 2 | categories: Etiam 3 | tags: [Programming Language, Python] 4 | --- 5 | 6 | ## Congue Fringilla 7 | 8 | Etiam elit ex, finibus nec turpis nec, congue fringilla sapien. Duis non consequat lorem, nec convallis justo. Sed sed tincidunt nibh, eu fermentum metus. Phasellus sed nulla scelerisque, bibendum orci eu, malesuada lorem. Mauris auctor, ex id consectetur sodales, dui dui sagittis arcu, quis dictum nisi quam nec sem. Pellentesque id sapien massa. Nulla facilisi. In in dolor vitae nunc condimentum dapibus vel sed sapien. 9 | 10 | ## Semper 11 | 12 | Mauris sed leo semper, gravida neque ut, vestibulum magna. Integer venenatis, risus a eleifend pretium, magna mi congue arcu, eu hendrerit dui nibh et velit. Fusce molestie eros massa, id aliquet neque varius in. Nulla dapibus, est quis aliquet suscipit, odio magna congue orci, sit amet mattis neque metus in eros. Maecenas auctor augue semper lorem vestibulum congue. Quisque consequat ante erat, eu tempus magna pharetra vitae. Mauris nec ligula ut felis pretium tristique quis vitae mauris. Sed commodo vehicula augue, id finibus lectus congue a. Cras convallis libero elementum lorem maximus, elementum vulputate odio condimentum. Fusce maximus elit nec ante pharetra tincidunt non nec orci. Proin id condimentum dui. Sed suscipit eros in aliquam dictum. Nunc commodo condimentum tincidunt. Nulla condimentum, quam vel efficitur commodo, erat enim blandit enim, at mattis velit lorem sed sem. Pellentesque vulputate sit amet turpis id congue. Ut sed dignissim tortor, nec pharetra erat. 13 | 14 | Interdum et malesuada fames ac ante ipsum primis in faucibus. Aliquam mattis quis lacus eu ornare. Nunc at leo non turpis bibendum dignissim commodo commodo sem. Morbi luctus ex est, dignissim egestas urna auctor quis. In faucibus, tortor ac molestie ultricies, erat dui scelerisque quam, molestie condimentum elit erat et sem. Morbi accumsan consectetur velit non lobortis. Curabitur tellus ante, sagittis sed tristique nec, porta sit amet tortor. Pellentesque sodales, odio vel feugiat sagittis, est odio interdum ligula, quis porttitor nunc ex a massa. Quisque mollis ultrices odio vel finibus. Curabitur condimentum vitae neque id viverra. Nunc purus ipsum, dapibus finibus augue sit amet, congue vulputate nisl. Fusce eget leo lacus. Aenean pretium leo in aliquam laoreet. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 15 | 16 | ## Interdum Turpis 17 | 18 | Aenean tincidunt justo eu neque consequat, non interdum turpis tempor. Sed blandit ante odio, non viverra eros molestie sit amet. Quisque cursus congue purus eu hendrerit. Vivamus eu placerat erat. Aliquam non sem imperdiet, mattis nibh vel, aliquam felis. Proin at condimentum magna. Nulla purus ipsum, rhoncus a maximus at, bibendum et nunc. Vivamus quis semper purus. Vivamus blandit nulla nec neque facilisis, eu tincidunt quam commodo. Phasellus imperdiet elit et placerat lacinia. Morbi tempor risus sit amet felis ornare, eget porta dolor vehicula. 19 | 20 | Proin nec enim erat. Pellentesque in mi et velit lobortis commodo. Suspendisse nulla enim, mattis placerat tellus id, hendrerit cursus massa. Suspendisse id dui in lacus porta volutpat. Suspendisse potenti. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Integer consectetur eget magna vitae auctor. Nulla nec neque odio. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ----- JetBrains ----- 2 | 3 | # User-specific stuff 4 | .idea/**/workspace.xml 5 | .idea/**/tasks.xml 6 | .idea/**/usage.statistics.xml 7 | .idea/**/dictionaries 8 | .idea/**/shelf 9 | 10 | # Sensitive or high-churn files 11 | .idea/**/dataSources/ 12 | .idea/**/dataSources.ids 13 | .idea/**/dataSources.local.xml 14 | .idea/**/sqlDataSources.xml 15 | .idea/**/dynamic.xml 16 | .idea/**/uiDesigner.xml 17 | .idea/**/dbnavigator.xml 18 | 19 | # Gradle 20 | .idea/**/gradle.xml 21 | .idea/**/libraries 22 | 23 | # Gradle and Maven with auto-import 24 | # When using Gradle or Maven with auto-import, you should exclude module files, 25 | # since they will be recreated, and may cause churn. Uncomment if using 26 | # auto-import. 27 | # .idea/modules.xml 28 | # .idea/*.iml 29 | # .idea/modules 30 | 31 | # CMake 32 | cmake-build-*/ 33 | 34 | # Mongo Explorer plugin 35 | .idea/**/mongoSettings.xml 36 | 37 | # File-based project format 38 | *.iws 39 | 40 | # IntelliJ 41 | out/ 42 | 43 | # mpeltonen/sbt-idea plugin 44 | .idea_modules/ 45 | 46 | # JIRA plugin 47 | atlassian-ide-plugin.xml 48 | 49 | # Cursive Clojure plugin 50 | .idea/replstate.xml 51 | 52 | # Crashlytics plugin (for Android Studio and IntelliJ) 53 | com_crashlytics_export_strings.xml 54 | crashlytics.properties 55 | crashlytics-build.properties 56 | fabric.properties 57 | 58 | # Editor-based Rest Client 59 | .idea/httpRequests 60 | 61 | # ----- Python ----- 62 | 63 | # Byte-compiled / optimized / DLL files 64 | __pycache__/ 65 | *.py[cod] 66 | *$py.class 67 | 68 | # C extensions 69 | *.so 70 | 71 | # Distribution / packaging 72 | .Python 73 | build/ 74 | develop-eggs/ 75 | dist/ 76 | downloads/ 77 | eggs/ 78 | .eggs/ 79 | lib/ 80 | lib64/ 81 | parts/ 82 | sdist/ 83 | var/ 84 | wheels/ 85 | *.egg-info/ 86 | .installed.cfg 87 | *.egg 88 | MANIFEST 89 | 90 | # PyInstaller 91 | # Usually these files are written by a python script from a template 92 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 93 | *.manifest 94 | *.spec 95 | 96 | # Installer logs 97 | pip-log.txt 98 | pip-delete-this-directory.txt 99 | 100 | # Unit test / coverage reports 101 | htmlcov/ 102 | .tox/ 103 | .coverage 104 | .coverage.* 105 | .cache 106 | nosetests.xml 107 | coverage.xml 108 | *.cover 109 | .hypothesis/ 110 | .pytest_cache/ 111 | 112 | # Translations 113 | *.mo 114 | *.pot 115 | 116 | # Django stuff: 117 | *.log 118 | local_settings.py 119 | db.sqlite3 120 | 121 | # Flask stuff: 122 | instance/ 123 | .webassets-cache 124 | 125 | # Scrapy stuff: 126 | .scrapy 127 | 128 | # Sphinx documentation 129 | docs/_build/ 130 | 131 | # PyBuilder 132 | target/ 133 | 134 | # Jupyter Notebook 135 | .ipynb_checkpoints 136 | 137 | # pyenv 138 | .python-version 139 | 140 | # celery beat schedule file 141 | celerybeat-schedule 142 | 143 | # SageMath parsed files 144 | *.sage.py 145 | 146 | # Environments 147 | .env 148 | .venv 149 | env/ 150 | venv/ 151 | ENV/ 152 | env.bak/ 153 | venv.bak/ 154 | 155 | # Spyder project settings 156 | .spyderproject 157 | .spyproject 158 | 159 | # Rope project settings 160 | .ropeproject 161 | 162 | # mkdocs documentation 163 | /site 164 | 165 | # mypy 166 | .mypy_cache/ 167 | 168 | # ----- Project ----- 169 | 170 | .idea 171 | demo/themes/* 172 | docs/themes/* 173 | !demo/themes/default 174 | !docs/themes/clean-doc 175 | demo/themes/default/.gitignore 176 | docs/themes/clean-doc/.gitignore 177 | -------------------------------------------------------------------------------- /docs/pages/configuration-file.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 配置文件 3 | author: Richard Chien 4 | created: 2017-03-20 5 | updated: 2017-03-22 6 | --- 7 | 8 | `config.py` 文件即 VeriPress 的配置文件,初始化实例之后会生成一份默认的配置,多数情况下,你可能需要秀改配置文件来符合个性化的需求,同时,配置文件可以被主题模板获取到,因此某些主题可能会对配置文件的某些项的不同配置表现出不同的行为。 9 | 10 | 下面给出 VeriPress 和默认主题所支持的配置项的说明(对于第三方主题特定的配置项要求,请参考它们的作者给出的使用方式)。 11 | 12 | ## STORAGE_TYPE 13 | 14 | 指定内容的存储方式。 15 | 16 | VeriPress 在设计时允许了未来加入不同的存储方式(比如数据库存储),而不限于使用文件存储。不过目前只支持文件存储,所以此项应该填默认的 `file`。 17 | 18 | ## THEME 19 | 20 | 指定要使用的主题。 21 | 22 | 默认为 `default`,即使用 default 主题,如果你安装了其它主题,就可以修改这个配置来更换主题,比如你现在看到的文档使用了 clean-doc 主题,可以使用 `veripress theme install clean-doc` 安装。 23 | 24 | ## CACHE_TYPE 25 | 26 | 指定缓存类型。 27 | 28 | 默认的 `simple` 表示使用简单的内存缓存。VeriPress 的缓存使用了 Flask-Caching 扩展,支持如下类型: 29 | 30 | | 配置值 | 说明 | 31 | | --------------- | ----------------- | 32 | | `null` | 不使用缓存 | 33 | | `simple` | 简单内存缓存 | 34 | | `memcached` | Memcached 缓存 | 35 | | `gaememcached` | GAE memcached 缓存 | 36 | | `saslmemcached` | SASL memcached 缓存 | 37 | | `redis` | Redis 缓存 | 38 | | `filesystem` | 文件系统缓存 | 39 | 40 | 对于除了 `null`、`simple` 之外的配置,还需要提供其它所需的配置项,例如使用 `redis` 则需要另外提供 `CACHE_REDIS_HOST`、`CACHE_REDIS_PORT` 等,请参考 Flask-Caching 的文档 [Configuring Flask-Caching](https://pythonhosted.org/Flask-Caching/#configuring-flask-caching)。 41 | 42 | ## MODE 43 | 44 | 指定运行模式。 45 | 46 | VeriPress 支持三种运行模式:`view-only`、`api-only`、`mixed`。`view-only` 表示只能访问页面,无法通过 API 直接获取 JSON 数据;`api-only` 表示只能通过 API 获取 JSON 数据;`mixed`,顾名思义,前两者混合模式。 47 | 48 | 关于 API 模式的更多信息,请参考 [API 模式](api-mode.html)。 49 | 50 | ## ENTRIES_PER_PAGE 51 | 52 | 指定首页文章列表每页显示的文章数量。 53 | 54 | 默认情况下网站的首页是文章(post)列表,并通过 URL `/page//` 来分页,因此需要指定每页显示的文章数量。对于会显示内容预览的主题,这个值可以设置小一些,而不显示预览的主题,可以设置大一些。 55 | 56 | ## FEED_COUNT 57 | 58 | 指定 Atom 订阅中的文章数量。 59 | 60 | 例如设置为 10 则 Atom 订阅中只会生成最新的 10 篇文章。 61 | 62 | ## SHOW_TOC 63 | 64 | 指定是否显示 TOC(目录)。 65 | 66 | 实际上此配置项是控制 VeriPress 内部是否生成 TOC,如果设置成 `False` 则主题模板无法收到 TOC。相反,设置成 `True` 则主题模板可以收到一个 TOC 列表和 TOC HTML 字符串,但是否显示最终取决于主题。 67 | 68 | ## TOC_DEPTH 和 TOC_LOWEST_LEVEL 69 | 70 | 指定 TOC 的最大深度和最低标题级别。 71 | 72 | 这两个范围都是 1~6,两个含义有一定区别。首先给一个 HTML 的示例: 73 | 74 | ```html 75 |
Title 1
76 |

Title 2

77 |

Title 3

78 |

Title 4

79 |

Title 5

80 |

Title 6

81 |
Title 7
82 | ``` 83 | 84 | 对于上面的示例,在不限最大深度和最低标题级别的情况下,生成的 TOC 应该和给出的缩进相同。可以发现 Title 1 也在生成的 TOC 中,如果想过滤掉这种级别比较低的标题,可以设置 `TOC_LOWEST_LEVEL` 为比较小的值,比如设置为 4,则 `h5`、`h6` 标签都不会算在内。同时可以发现这个 TOC 有三层,如果只需要显示两层,可以将 `TOC_DEPTH` 设置为 2。 85 | 86 | ## ALLOW_SEARCH_PAGES 87 | 88 | 指定是否允许搜索自定义页面的内容。 89 | 90 | 只在动态运行时有效(生成静态文件之后没法搜索)。设置为 `False` 则在搜索时不会搜索自定义页面的内容。另外,只支持搜索 VeriPress 中解析器所支持的格式中的文字,例如使用 Markdown 编写的自定义页面,相反地,直接的 HTML 文件或其它静态文件无法被搜索到。 91 | 92 | ## PAGE_SOURCE_ACCESSIBLE 93 | 94 | 指定是否允许访问自定义页面的源文件(这里指需要经过 VeriPress 解析的自定义页面,直接的 HTML 等无论如何都可以访问)。 95 | 96 | 例如你有一个自定义页面在 `pages/a/b/c.md`,使用 Markdown 编写,访问 `/a/b/c.html` 讲可以获取这个文件解析后的页面,如果将此配置设置为 `True`(默认为 `False`),则还可以通过 `/a/b/c.md` 来访问原始文件。 97 | 98 | ## DISQUS_ENABLED 、DISQUS_SHORT_NAME、DUOSHUO_ENABLED 和 DUOSHUO_SHORT_NAME 99 | 指定是否开启多说或 Disqus 评论框,以及它们的 shortname。 100 | 101 | default 主题和 clean-doc 主题支持多说和 Disqus 评论框,例如设置: 102 | 103 | ```py 104 | DISQUS_ENABLED = True 105 | DISQUS_SHORT_NAME = 'your-shorname' 106 | ``` 107 | 108 | 将会在文章和自定义页面底部显示 Disqus 评论框,多说同理。 109 | 110 | 17 年 3 月 22 日注:很遗憾,多说宣布即将关闭服务,将在 6 月 1 日正式关停。 111 | -------------------------------------------------------------------------------- /veripress_cli/init.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from datetime import datetime 4 | 5 | import click 6 | 7 | from veripress_cli import cli 8 | 9 | 10 | @cli.command('init', short_help='Initialize a new instance directory.', 11 | help='This command will initialize the current working directory ' 12 | 'as a new VeriPress instance, which means to create ' 13 | 'default configuration file, necessary subdirectories, etc.') 14 | @click.option('--storage-mode', '-s', default='file', 15 | type=click.Choice(['file']), 16 | help='Storage mode (only "file" mode supported currently).') 17 | def init_command(storage_mode): 18 | from veripress import app 19 | instance_path = app.instance_path 20 | defaults_dir = os.path.join(os.path.dirname(__file__), 'defaults') 21 | 22 | with open(os.path.join(defaults_dir, 'config.py'), 23 | 'r', encoding='utf-8') as f_default_conf: 24 | with open(os.path.join(instance_path, 'config.py'), 25 | 'w', encoding='utf-8') as f_conf: 26 | f_conf.write( 27 | f_default_conf.read().format(storage_mode=storage_mode)) 28 | 29 | shutil.copyfile(os.path.join(defaults_dir, 'site.json'), 30 | os.path.join(instance_path, 'site.json')) 31 | shutil.copytree(os.path.join(defaults_dir, 'static'), 32 | os.path.join(instance_path, 'static')) 33 | os.mkdir(os.path.join(instance_path, 'themes')) 34 | 35 | if storage_mode == 'file': 36 | init_file_storage(instance_path) 37 | 38 | click.echo('\nDefault files and configurations has been created!\n\n' 39 | 'Now you can run "veripress theme install default" to ' 40 | 'install the default theme and then run "veripress preview" ' 41 | 'to preview the blog.\n\nEnjoy!') 42 | 43 | 44 | def init_file_storage(instance_path): 45 | os.mkdir(os.path.join(instance_path, 'posts')) 46 | os.mkdir(os.path.join(instance_path, 'pages')) 47 | os.mkdir(os.path.join(instance_path, 'widgets')) 48 | 49 | now_dt = datetime.now() 50 | first_post_file_path = os.path.join( 51 | instance_path, 'posts', 52 | '{}-hello-world.md'.format(now_dt.strftime('%Y-%m-%d')) 53 | ) 54 | with open(first_post_file_path, 'w', encoding='utf-8') as f: 55 | f.write('---\n' 56 | 'title: Hello, world!\n' 57 | 'created: {}\n' 58 | 'categories: Hello\n' 59 | 'tags: [Hello, Greeting]\n' 60 | '---\n\n'.format(now_dt.strftime('%Y-%m-%d %H:%M:%S'))) 61 | f.write('This is the hello world post!\n') 62 | 63 | os.mkdir(os.path.join(instance_path, 'pages', 'hello')) 64 | first_page_file_path = os.path.join(instance_path, 65 | 'pages', 'hello', 'index.md') 66 | with open(first_page_file_path, 'w', encoding='utf-8') as f: 67 | f.write('---\n' 68 | 'title: Hello\n' 69 | 'created: {}\n' 70 | '---\n\n'.format(now_dt.strftime('%Y-%m-%d %H:%M:%S'))) 71 | f.write('This is the hello custom page!\n') 72 | 73 | first_widget_file_path = os.path.join(instance_path, 74 | 'widgets', 'welcome.md') 75 | with open(first_widget_file_path, 'w', encoding='utf-8') as f: 76 | f.write('---\n' 77 | 'position: sidebar\n' 78 | 'order: 0\n' 79 | '---\n\n') 80 | f.write('#### Welcome!\n\n' 81 | 'Hi! Welcome to my blog.\n') 82 | -------------------------------------------------------------------------------- /demo/posts/1991-08-06-integer.md: -------------------------------------------------------------------------------- 1 | --- 2 | categories: [Lorem] 3 | tags: [Web, Internet] 4 | --- 5 | 6 | Nam id nisl et libero hendrerit vulputate. Fusce dignissim commodo tellus eget rutrum. Praesent eu sapien efficitur, eleifend justo sed, feugiat dui. Morbi quis facilisis ipsum. Praesent tellus nulla, feugiat non consectetur ac, convallis a nisi. Donec gravida dolor at maximus hendrerit. Vivamus pretium faucibus magna id fermentum. Etiam ullamcorper purus orci, eu bibendum neque bibendum eu. Sed volutpat sed odio eu pulvinar. In bibendum augue nisl, at pulvinar odio iaculis quis. 7 | 8 | Phasellus mollis, ex nec consequat pellentesque, tellus orci sodales arcu, at rhoncus ante odio vel velit. Nullam sit amet libero magna. Donec laoreet sapien nec ligula maximus, auctor pellentesque eros ultricies. Integer ornare tellus quis fermentum efficitur. Nulla eget porttitor felis, quis fermentum nibh. Suspendisse accumsan nulla a tempus dapibus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed id aliquet arcu, quis dictum metus. Pellentesque sapien tortor, luctus sit amet laoreet in, lacinia at risus. Cras molestie augue eu hendrerit ultrices. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam dictum condimentum nunc, eget tincidunt risus finibus in. Sed euismod augue dui, vel lobortis magna interdum sit amet. Morbi dictum, ipsum pretium cursus ornare, neque nisi fringilla odio, ut posuere eros velit ut elit. Curabitur nec sem sit amet nunc porta ornare. Etiam et viverra mauris, non volutpat lectus. 9 | 10 | Morbi sapien sem, condimentum non eleifend vitae, lobortis quis dui. Integer faucibus urna in iaculis luctus. Morbi lobortis, nibh id vulputate aliquam, augue neque tempor elit, lobortis ultricies risus augue et magna. Nunc eu nibh vulputate, consectetur magna id, bibendum metus. Aenean iaculis massa est, nec aliquam tortor rutrum finibus. In venenatis at nibh sed commodo. In mollis est quam, a facilisis nisl dignissim sit amet. Nulla venenatis sapien dolor, in fermentum tellus luctus sit amet. Morbi quis neque vitae eros molestie sollicitudin in vitae libero. Etiam at tortor ac est porta convallis. 11 | 12 | Praesent ante dui, pulvinar sit amet luctus fringilla, malesuada vitae justo. Suspendisse potenti. Nam fringilla lacinia consectetur. Praesent in neque commodo, cursus diam vel, luctus arcu. Fusce diam justo, porttitor a leo sit amet, faucibus condimentum neque. Integer at sagittis sapien. Nullam lectus libero, pellentesque at erat et, elementum faucibus nibh. Suspendisse tincidunt lorem a imperdiet pulvinar. Duis sed metus in diam malesuada ullamcorper. Nam gravida, arcu ac tempus dapibus, nulla ante mattis ipsum, ut porttitor mi justo a velit. In ac pharetra velit. Vivamus finibus lorem quis dapibus porta. Aenean eu mattis leo. Phasellus elementum blandit rutrum. Duis sollicitudin leo neque, a luctus turpis malesuada convallis. Fusce efficitur, lacus sed aliquet vestibulum, elit ante ornare mi, vel accumsan velit mauris sed purus. 13 | 14 | Aliquam erat volutpat. Vestibulum dignissim pellentesque odio, vitae blandit est volutpat in. Suspendisse sollicitudin lacinia diam. Cras elit tellus, tristique at ullamcorper lacinia, venenatis et mi. Aenean bibendum efficitur accumsan. Cras consectetur ac elit id porta. Nulla eu metus lacus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec eleifend pellentesque malesuada. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus vehicula, orci sed venenatis lacinia, augue ipsum tincidunt nisi, eget maximus ante orci vel metus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Pellentesque at ullamcorper nulla, eu pulvinar metus. -------------------------------------------------------------------------------- /veripress/view/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import functools 3 | from itertools import chain 4 | 5 | from flask import Blueprint, request, render_template, g 6 | 7 | from veripress import site, cache 8 | from veripress.model import storage 9 | from veripress.model.parsers import get_parser 10 | from veripress.helpers import url_rule, to_list 11 | 12 | view_blueprint = Blueprint('view', __name__) 13 | 14 | 15 | @view_blueprint.context_processor 16 | def inject_context(): 17 | """ 18 | Inject some common objects into the context of templates. 19 | """ 20 | return dict(site=site, storage=storage) 21 | 22 | 23 | @view_blueprint.app_template_filter('content') 24 | def parse_content_of_models(obj): 25 | """ 26 | Parse the whole 'raw_content' attribute of 27 | a Post or Page or Widget object (in template files). 28 | 29 | :param obj: a Post or Page or Widget object 30 | :return: parsed whole content 31 | """ 32 | return get_parser(obj.format).parse_whole(obj.raw_content) 33 | 34 | 35 | @cache.memoize(timeout=2 * 60) 36 | def custom_render_template(template_name_or_list, **context): 37 | """ 38 | Try to render templates in the custom folder first, 39 | if no custom templates, try the theme's default ones. 40 | """ 41 | response_str = render_template( 42 | functools.reduce(lambda x, y: x + [os.path.join('custom', y), y], 43 | to_list(template_name_or_list), []), 44 | **context 45 | ) 46 | if hasattr(g, 'status_code'): 47 | status_code = g.status_code 48 | else: 49 | status_code = 200 50 | return response_str, status_code 51 | 52 | 53 | def templated(template=None, *templates): 54 | """ 55 | Decorate a view function with one or more default template name. 56 | This will try templates in the custom folder first, 57 | the theme's original ones second. 58 | 59 | :param template: template name or template name list 60 | """ 61 | 62 | def decorator(func): 63 | @functools.wraps(func) 64 | def wrapper(*args, **kwargs): 65 | template_ = template 66 | if template_ is None: 67 | template_ = request.endpoint.split('.', 1)[1].replace( 68 | '.', '/') + '.html' 69 | context = func(*args, **kwargs) 70 | if context is None: 71 | context = {} 72 | elif not isinstance(context, dict): 73 | return context 74 | return custom_render_template( 75 | list(chain(to_list(template_), templates)), **context) 76 | 77 | return wrapper 78 | 79 | return decorator 80 | 81 | 82 | @view_blueprint.errorhandler(404) 83 | @templated('404.html') 84 | def page_not_found(e): 85 | g.status_code = 404 86 | 87 | 88 | from veripress.view import views 89 | 90 | rule = functools.partial(url_rule, view_blueprint, methods=['GET']) 91 | 92 | rule(['/feed.xml', '/atom.xml'], view_func=views.feed, strict_slashes=True) 93 | 94 | rule(['/', '/page//'], view_func=views.index, 95 | strict_slashes=True) 96 | rule('/post////', 97 | view_func=views.post, strict_slashes=False) 98 | rule('/category//', view_func=views.category, 99 | strict_slashes=True) 100 | rule('/tag//', view_func=views.tag, strict_slashes=True) 101 | rule(['/archive/', 102 | '/archive//', 103 | '/archive///'], view_func=views.archive, 104 | strict_slashes=True) 105 | rule('/search', view_func=views.search, strict_slashes=False) 106 | rule('/', view_func=views.page, strict_slashes=True) 107 | -------------------------------------------------------------------------------- /veripress/api/__init__.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from collections import Iterable, namedtuple 3 | 4 | from flask import Blueprint, jsonify, Response, abort 5 | 6 | from veripress.helpers import url_rule 7 | 8 | api_blueprint = Blueprint('api', __name__) 9 | 10 | 11 | class Error(object): 12 | """ 13 | Defines API error codes and error messages. 14 | """ 15 | 16 | _Error = namedtuple('Error', ('code', 'msg', 'status_code')) 17 | 18 | UNDEFINED = _Error(100, 'Undefined error.', 400) 19 | NO_SUCH_API = _Error(101, 'No such API.', 404) 20 | RESOURCE_NOT_EXISTS = _Error(102, 'The resource does not exist.', 404) 21 | INVALID_ARGUMENTS = _Error(103, 'Invalid argument(s).', 400) 22 | NOT_ALLOWED = _Error(104, 'The resource path is not allowed.', 403) 23 | BAD_PATH = _Error(105, 'The resource path cannot be recognized.', 400) 24 | 25 | 26 | class ApiException(Exception): 27 | """Raised by API functions when something goes wrong.""" 28 | 29 | def __init__(self, message=None, error=Error.UNDEFINED, 30 | status_code=None, payload=None): 31 | super(ApiException, self).__init__() 32 | self.message = message 33 | self.status_code = status_code 34 | self.error = error 35 | self.payload = payload 36 | 37 | def to_dict(self): 38 | result = dict(self.payload or {}) 39 | result['code'] = self.error.code 40 | result['message'] = self.message or self.error.msg 41 | return result 42 | 43 | 44 | @api_blueprint.errorhandler(ApiException) 45 | def handle_api_exception(e): 46 | response = jsonify(e.to_dict()) 47 | response.status_code = e.status_code or e.error.status_code 48 | return response 49 | 50 | 51 | @api_blueprint.errorhandler(404) 52 | def handle_page_not_found(e): 53 | return handle_api_exception(ApiException(error=Error.NO_SUCH_API)) 54 | 55 | 56 | def json_api(func): 57 | @functools.wraps(func) 58 | def wrapper(*args, **kwargs): 59 | result = func(*args, **kwargs) 60 | if result is None: 61 | raise ApiException(error=Error.RESOURCE_NOT_EXISTS) 62 | 63 | if isinstance(result, Response): 64 | return result 65 | 66 | try: 67 | return jsonify(result) 68 | except TypeError as e: 69 | if isinstance(result, Iterable): 70 | return jsonify(list(result)) 71 | else: 72 | raise e 73 | 74 | return wrapper 75 | 76 | 77 | from veripress.api import handlers 78 | 79 | 80 | def rule(rules, strict_slashes=False, api_func=None, *args, **kwargs): 81 | """ 82 | Add a API route to the 'api' blueprint. 83 | 84 | :param rules: rule string or string list 85 | :param strict_slashes: same to Blueprint.route, but default value is False 86 | :param api_func: a function that returns a JSON serializable object 87 | or a Flask Response, or raises ApiException 88 | :param args: other args that should be passed to Blueprint.route 89 | :param kwargs: other kwargs that should be passed to Blueprint.route 90 | :return: 91 | """ 92 | return url_rule(api_blueprint, rules, strict_slashes=strict_slashes, 93 | view_func=json_api(api_func) if api_func else None, 94 | *args, **kwargs) 95 | 96 | 97 | rule('/site', endpoint='site', api_func=handlers.site_info, methods=['GET']) 98 | rule(['/posts', 99 | '/posts/', 100 | '/posts//', 101 | '/posts///', 102 | '/posts////'], 103 | api_func=handlers.posts, methods=['GET']) 104 | rule('/tags', api_func=handlers.tags, methods=['GET']) 105 | rule('/categories', api_func=handlers.categories, methods=['GET']) 106 | rule('/widgets', api_func=handlers.widgets, methods=['GET']) 107 | rule('/pages/', api_func=handlers.pages, methods=['GET'], 108 | strict_slashes=True) 109 | rule('/search', api_func=handlers.search, methods=['GET']) 110 | 111 | # direct all unknown paths to 404 112 | rule('/', api_func=lambda _: abort(404), methods=['GET']) 113 | -------------------------------------------------------------------------------- /veripress/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from flask import Flask, send_from_directory, current_app, abort 5 | from werkzeug.exceptions import NotFound 6 | 7 | 8 | class CustomFlask(Flask): 9 | def send_static_file(self, filename): 10 | """ 11 | Send static files from the static folder 12 | in the current selected theme prior to the global static folder. 13 | 14 | :param filename: static filename 15 | :return: response object 16 | """ 17 | if self.config['MODE'] == 'api-only': 18 | # if 'api-only' mode is set, we should not send static files 19 | abort(404) 20 | 21 | theme_static_folder = getattr(self, 'theme_static_folder', None) 22 | if theme_static_folder: 23 | try: 24 | return send_from_directory(theme_static_folder, filename) 25 | except NotFound: 26 | pass 27 | return super(CustomFlask, self).send_static_file(filename) 28 | 29 | 30 | def create_app(config_filename, instance_path=None): 31 | """ 32 | Factory function to create Flask application object. 33 | 34 | :param config_filename: absolute or relative filename of the config file 35 | :param instance_path: instance path to initialize or run a VeriPress app 36 | :return: a Flask app object 37 | """ 38 | app_ = CustomFlask( 39 | __name__, 40 | instance_path=instance_path or os.environ.get( 41 | 'VERIPRESS_INSTANCE_PATH') or os.getcwd(), 42 | instance_relative_config=True 43 | ) 44 | app_.config.update(dict(STORAGE_TYPE='file', 45 | THEME='default', 46 | CACHE_TYPE='simple', 47 | MODE='view-only', 48 | ENTRIES_PER_PAGE=5, 49 | FEED_COUNT=10, 50 | SHOW_TOC=True, 51 | TOC_DEPTH=3, 52 | TOC_LOWEST_LEVEL=3, 53 | ALLOW_SEARCH_PAGES=True, 54 | PAGE_SOURCE_ACCESSIBLE=False)) 55 | app_.config.from_pyfile(config_filename, silent=True) 56 | 57 | theme_folder = os.path.join(app_.instance_path, 58 | 'themes', app_.config['THEME']) 59 | # use templates in the selected theme's folder 60 | app_.template_folder = os.path.join(theme_folder, 'templates') 61 | # use static files in the selected theme's folder 62 | app_.theme_static_folder = os.path.join(theme_folder, 'static') 63 | # global static folder 64 | app_.static_folder = os.path.join(app_.instance_path, 'static') 65 | 66 | return app_ 67 | 68 | 69 | app = create_app('config.py') 70 | 71 | site = { 72 | 'title': 'Untitled', 73 | 'subtitle': 'Yet another VeriPress blog.', 74 | 'root_url': 'http://example.com', 75 | 'timezone': 'UTC+00:00' 76 | } 77 | try: 78 | with app.open_instance_resource('site.json', mode='rb') as site_file: 79 | # load site meta info to the site object 80 | site.update(json.loads(site_file.read().decode('utf-8'))) 81 | except FileNotFoundError: 82 | pass 83 | 84 | 85 | @app.route('/_webhook', methods=['POST'], strict_slashes=False) 86 | def webhook(): 87 | """ 88 | Run a custom python script when requested. 89 | User can pull git repositories, log something to a file, 90 | or do anything else in there. 91 | 92 | :return: always 204 NO CONTENT 93 | """ 94 | try: 95 | with current_app.open_instance_resource( 96 | 'webhook.py', 'rb') as script_file: 97 | # if there is the 'webhook.py' script, we execute it's content 98 | exec(script_file.read().decode('utf-8')) 99 | except FileNotFoundError: 100 | pass 101 | return '', 204 102 | 103 | 104 | from flask_caching import Cache 105 | 106 | cache = Cache(app, config=app.config) 107 | 108 | if app.config['MODE'] in ('mixed', 'api-only'): 109 | import veripress.api 110 | 111 | app.register_blueprint(api.api_blueprint, url_prefix='/api') 112 | 113 | if app.config['MODE'] in ('mixed', 'view-only'): 114 | import veripress.view 115 | 116 | app.register_blueprint(view.view_blueprint) 117 | 118 | import veripress.model 119 | -------------------------------------------------------------------------------- /veripress/model/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from veripress import site 4 | from veripress.helpers import to_list, to_datetime 5 | 6 | 7 | class Base(object): 8 | """ 9 | Base model class, contains basic/general information of a post/page/widget. 10 | """ 11 | 12 | def __init__(self): 13 | self.meta = {} 14 | self.raw_content = None 15 | self._format = None 16 | 17 | @property 18 | def format(self): 19 | return self._format 20 | 21 | @format.setter 22 | def format(self, value): 23 | if value is not None: 24 | self._format = value.lower() 25 | 26 | @property 27 | def is_draft(self): 28 | return self.meta.get('is_draft', False) 29 | 30 | def to_dict(self): 31 | """ 32 | Convert attributes and properties to a dict, 33 | so that it can be serialized. 34 | """ 35 | return {k: getattr(self, k) for k in filter( 36 | lambda k: not k.startswith('_') and k != 'to_dict', dir(self))} 37 | 38 | def __eq__(self, other): 39 | if isinstance(other, Base): 40 | return self.to_dict() == other.to_dict() 41 | return super(Base, self).__eq__(other) 42 | 43 | 44 | class AuthorMixIn(object): 45 | """Mix in author's name and email.""" 46 | 47 | @property 48 | def author(self): 49 | return getattr(self, 'meta', {}).get('author', site.get('author')) 50 | 51 | @property 52 | def email(self): 53 | return getattr(self, 'meta', {}).get('email', site.get('email')) 54 | 55 | 56 | class DateMixIn(object): 57 | """Mix in created data and updated date.""" 58 | 59 | @property 60 | def created(self): 61 | return to_datetime(getattr(self, 'meta', {}).get('created')) 62 | 63 | @property 64 | def updated(self): 65 | return to_datetime(getattr(self, 'meta', {}).get('updated', 66 | self.created)) 67 | 68 | 69 | class TagCategoryMixIn(object): 70 | """Mix in tags and categories.""" 71 | 72 | @property 73 | def tags(self): 74 | return to_list(getattr(self, 'meta', {}).get('tags', [])) 75 | 76 | @property 77 | def categories(self): 78 | return to_list(getattr(self, 'meta', {}).get('categories', [])) 79 | 80 | 81 | class Page(Base, AuthorMixIn, DateMixIn): 82 | """ 83 | Model class of publish type 'custom page' or 'page', 84 | with default layout 'page'. 85 | """ 86 | _default_layout = 'page' 87 | 88 | def __init__(self): 89 | super().__init__() 90 | self.unique_key = None 91 | self.rel_url = None 92 | 93 | @property 94 | def layout(self): 95 | return self.meta.get('layout', self._default_layout) 96 | 97 | @property 98 | def title(self): 99 | result = self.meta.get('title') 100 | if result is None and self.rel_url: 101 | sp = self.rel_url.split('/') 102 | pos = len(sp) - 1 103 | while pos > 0 and (sp[pos] == 'index.html' or not sp[pos]): 104 | pos -= 1 105 | 106 | path_seg = sp[pos][:-len('.html')] \ 107 | if sp[pos].endswith('.html') else sp[pos] 108 | result = ' '.join(word[0].upper() + word[1:] for word in filter( 109 | lambda x: x, path_seg.split('-'))) 110 | return result 111 | 112 | 113 | class Post(Page, TagCategoryMixIn): 114 | """ 115 | Model class of publish type 'post', with default layout 'post'. 116 | """ 117 | _default_layout = 'post' 118 | 119 | @property 120 | def created(self): 121 | result = super(Post, self).created 122 | if result is None: 123 | d, _, _ = self.rel_url.rsplit('/', 2) 124 | result = datetime.strptime(d, '%Y/%m/%d') 125 | return result 126 | 127 | @property 128 | def title(self): 129 | result = self.meta.get('title') 130 | if result is None: 131 | _, post_name, _ = self.rel_url.rsplit('/', 2) 132 | result = ' '.join(word[0].upper() + word[1:] for word in filter( 133 | lambda x: x, post_name.split('-'))) 134 | return result 135 | 136 | 137 | class Widget(Base): 138 | """ 139 | Model class of publish type 'widget'. 140 | """ 141 | 142 | @property 143 | def position(self): 144 | return self.meta.get('position') 145 | 146 | @property 147 | def order(self): 148 | return self.meta.get('order') 149 | -------------------------------------------------------------------------------- /veripress_cli/theme.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | 5 | import click 6 | 7 | from veripress_cli import cli 8 | 9 | 10 | def get_themes_dir(): 11 | from veripress import app 12 | return os.path.join(app.instance_path, 'themes') 13 | 14 | 15 | @cli.group(name='theme', short_help='Manage themes.', 16 | help='This set of sub-commands help you to manage themes.') 17 | def theme_cli(): 18 | pass 19 | 20 | 21 | @theme_cli.command('list', short_help='List all themes installed.') 22 | def list_command(): 23 | themes = list(filter( 24 | lambda x: os.path.isdir(os.path.join(get_themes_dir(), x)), 25 | os.listdir(get_themes_dir()) 26 | )) 27 | if not themes: 28 | click.echo('No theme is installed.') 29 | return 30 | click.echo('Themes installed:') 31 | click.echo('\n'.join(themes)) 32 | 33 | 34 | @theme_cli.command( 35 | 'install', short_help='Install a theme from GitHub.', 36 | help='This command will install theme from GitHub for you. ' 37 | 'If you want to install a theme from the official ' 38 | 'veripress/themes repo, enter things like "default" ' 39 | 'to specify the theme. If you want to install a third-party theme, ' 40 | 'use "user/repo" format.\n\n' 41 | 'e.g.\n\n' 42 | ' $ veripress theme install default\n\n' 43 | ' $ veripress theme install someone/the-theme ' 44 | '--branch the-theme-branch\n\n' 45 | 'If you want to customize the theme name installed, ' 46 | 'use "--name" parameter.' 47 | ) 48 | @click.argument('theme', nargs=1) 49 | @click.option('--branch', '-b', default='master', 50 | help='From which branch to clone the theme. ' 51 | 'Only matters when a string like "user/repo" ' 52 | 'is passed in as the theme name.') 53 | @click.option('--name', '-n', 54 | help='Specify the theme name. ' 55 | 'If this is not specified, a default one will be used.') 56 | def install_command(theme, branch, name): 57 | if re.fullmatch('[_\-A-Z0-9a-z]+', theme): 58 | theme_name = name or theme 59 | theme_path = os.path.join(get_themes_dir(), theme_name) 60 | cmd = 'git clone --branch {} ' \ 61 | 'https://github.com/veripress/themes.git "{}"'.format(theme, 62 | theme_path) 63 | else: 64 | m = re.fullmatch('([_\-A-Z0-9a-z]+)/([_\-A-Z0-9a-z]+)', theme) 65 | if not m: 66 | raise click.BadArgumentUsage( 67 | 'The theme should be like "default" ' 68 | '(branch of veripress/themes) or "someone/the-theme" ' 69 | '(third-party theme on GitHub)' 70 | ) 71 | user = m.group(1) 72 | repo = m.group(2) 73 | theme_name = name or repo 74 | theme_path = os.path.join(get_themes_dir(), theme_name) 75 | cmd = 'git clone --branch {} ' \ 76 | 'https://github.com/{}/{}.git "{}"'.format(branch, user, 77 | repo, theme_path) 78 | print(cmd) 79 | exit_code = os.system(cmd) 80 | if exit_code == 0: 81 | click.echo('\n"{}" theme has been ' 82 | 'installed successfully.'.format(theme_name)) 83 | else: 84 | click.echo('\nSomething went wrong. Do you forget to install git? ' 85 | 'Or is there another theme with same name existing?') 86 | 87 | 88 | @theme_cli.command('uninstall', short_help='Uninstall a theme.') 89 | @click.argument('theme', nargs=1) 90 | def uninstall_command(theme): 91 | theme_path = os.path.join(get_themes_dir(), theme) 92 | if os.path.isdir(theme_path): 93 | shutil.rmtree(theme_path) 94 | click.echo('"{}" theme has been ' 95 | 'uninstalled successfully.'.format(theme)) 96 | else: 97 | click.echo('There is no such theme.') 98 | 99 | 100 | @theme_cli.command('update', short_help='Update a theme.') 101 | @click.argument('theme', nargs=1) 102 | def uninstall_command(theme): 103 | theme_path = os.path.join(get_themes_dir(), theme) 104 | if os.path.isdir(theme_path): 105 | cur_dir = os.getcwd() 106 | os.chdir(theme_path) 107 | exit_code = os.system('git pull') 108 | os.chdir(cur_dir) 109 | if exit_code == 0: 110 | click.echo('\n"{}" theme has been ' 111 | 'updated successfully.'.format(theme)) 112 | else: 113 | click.echo('\nSomething went wrong. ' 114 | 'Do you forget to install git? ' 115 | 'Or did you modify the theme by yourself?') 116 | else: 117 | click.echo('There is no such theme.') 118 | -------------------------------------------------------------------------------- /docs/pages/making-your-own-theme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 制作主题 3 | author: Richard Chien 4 | created: 2017-03-21 5 | updated: 2017-03-22 6 | --- 7 | 8 | VeriPress 原生支持主题,如果你对官方主题或其它第三方主题感到不满意,同时也有一定的编程基本知识,你就可以自行制作自己的主题,也欢迎你把自己制作的主题发布到网上和其他人一起分享。 9 | 10 | ## 主题的组成部分 11 | 12 | 主题主要包括静态文件和模板文件,分别在 `static` 子目录和 `templates` 子目录。 13 | 14 | `static` 中的文件,可以直接通过 `/static/:path` 来访问,例如 `/static/style.css` 可以访问到当前正在使用的主题的 `static/style.css` 文件,而如果当前主题的 `static` 目录中并没有 `style.css` 文件,则会去 VeriPress 实例的全局 `static` 目录中寻找。 15 | 16 | `templates` 中的模板文件,会在收到请求后按照所请求的内容渲染成最终的 HTML 页面,必须存在的模板有 `index.html`、`archive.html`,这两个分别对应首页和归档页;而如果文章和自定义页面使用了默认的布局(分别 `post` 和 `page`),则还必须有 `post.html`、`page.html`;此外,标签、分类、搜索三个页面在没有单独模板的情况下都默认使用 `archive.html`,如果你需要单独定义这三类页面,使用 `tag.html`、`category.html`、`search.html` 来命名。除了上面各个对应实际页面的模板,还有一个 `404.html` 用于在找不到页面的情况下渲染。 17 | 18 | VeriPress 在寻找模板文件时,首先会查找主题的 `templates/custom` 目录,如果在里面找到了相应的模板,将使用它(用户自定义的模板),如果没找到,将使用 `templates` 中的模板。 19 | 20 | ## 模板引擎 21 | 22 | VeriPress 使用 Jinja2 模板引擎,下面简单介绍它的语法。 23 | 24 | `{{ ... }}` 用来表示表达式,模板文件在渲染时会传入一些值(后面解释),这些值可以通过形如 `{{ some_object.some_attribute }}` 的表达式来取出,表达式的计算结果将会转成 HTML 显示在相应的位置。 25 | 26 | `{% ... %}` 用来表示语句,比如判断语句、循环语句等,通过多个这样的块将语句主体包在中间,例如一个判断结构: 27 | 28 | ```html 29 | {% if True %} 30 |

{{ some_variable }}

31 | {% endif %} 32 | ``` 33 | 34 | 限于篇幅这里也不重复太多 Jinja2 的文档了,具体的语法请参考 [Template Designer Documentation](http://jinja.pocoo.org/docs/2.9/templates/)。 35 | 36 | 下面将解释渲染模板时「传入的值」。 37 | 38 | ## 渲染模板的 Context 39 | 40 | 渲染模板时有个概念叫 context,也就是在模板渲染时可以接触到的 Python 环境中的函数、对象等。由于基于 Flask,因此所有 Flask 的 context,都可以使用,例如 `request`、`config`、`session`、`url_for()` 等,通过这些,便可以访问到当前的请求 URL、参数、配置文件等,可以参考 [Standard Context](http://flask.pocoo.org/docs/0.12/templating/#standard-context)。 41 | 42 | 除了 Flask 提供的这些,对于不同的模板文件,VeriPress 还提供了该模板可能会需要用到的对象,如下表: 43 | 44 | | 模板 | 额外的 Context 对象 | 说明 | 45 | | --------------- | --------------------------------------- | ---------------------------------------- | 46 | | `index.html` | `entries`、`next_url`、`prev_url` | 分别是当前分页上的文章列表、下一页的 URL、上一页的 URL | 47 | | `post.html` | `entry` | 当前访问的文章 | 48 | | `page.html` | `entry` | 当前访问的自定义页面 | 49 | | `archive.html` | `entries`、`archive_type`、`archive_name` | 分别是当前归档的文章列表、归档类型、归档名称,其中 `/archive/` 页面的归档类型为 `Archive`,名称为 `All` 或类似 `2017`、`2017.3`(分别对应 `/archive/2017/` 和 `/archive/2017/03/` 页面) | 50 | | `tag.html` | 同上 | 归档类型为 `Tag`,归档名称为标签名 | 51 | | `category.html` | 同上 | 归档类型为 `Category`,归档名称为分类名 | 52 | | `search.html` | 同上 | 归档类型为 `Search`,归档名称为搜索关键词加引号 | 53 | 54 | 以上的「文章」「自定义页面」的数据,基本上和 [API 模式](api-mode.html#api-posts-获取文章列表) 获取到的相似,不同之处在于此处每个对象都多了一个 `url` 字段,可以用来直接构造链接,以及,`created` 和 `updated` 字段是 Python `datetime` 对象而不是格式化后的字符串。 55 | 56 | 除了上述的每个模板不同的 context 对象,每个模板内都可以访问 `site` 和 `storage` 两个对象,前者即 `site.json` 中的内容,后者是当前使用的存储类型的数据访问封装对象,一般很少会直接用这个,只有在获取页面部件时有必要使用(因为不是所有页面都需要显示部件,何时显示由主题决定)。由于 `storage` 获取到的数据是最原始的文章、页面、部件的对象,这里不再花费篇幅列出它的方法和获取的对象中的属性了,请直接参考 [model/storages.py](https://github.com/veripress/veripress/blob/master/veripress/model/storages.py) 中的 `Storage` 类和 [model/models.py](https://github.com/veripress/veripress/blob/master/veripress/model/models.py) 中的类定义。 57 | 58 | **鉴于获取页面部件需要使用 `storage` 对象,如果你没有精力或兴趣查看源码,可以直接参考默认主题的 [sidebar.html](https://github.com/veripress/themes/blob/default/templates/sidebar.html) 文件。** 59 | 60 | 在上面的 `sidebar.html` 中你会看到一个 `{{ widget|content|safe }}` 这样的表达式,其中 `widget` 是获取到的页面部件对象,后面两个 `content`、`safe` 是「过滤器」,前者是 VeriPress 提供的,用于把内容的抽象对象中的原始内容直接解析成 HTML 字符串,后者是 Jinja2 自带的,用于将 HTML 代码直接显示而不转义。 61 | 62 | ## 获取特定页面的 URL 63 | 64 | 在主题中你可能需要获取其它某个页面的 URL 来构造链接,可以使用 Flask 提供的 `url_for()` 函数。 65 | 66 | 对于全局或主题中的 `static` 目录的文件,使用 `url_for('static', filename='the-filename')` 来获取。 67 | 68 | 对于 view 模式的其它页面,例如你在导航栏需要提供一个归档页面的链接,使用类似 `url_for('.archive', year=2017)` 的调用。注意 `.archive` 以点号开头,或者也可以使用 `view.archive`。`url_for()` 的其它参数是用来指定 view 函数的参数的,要熟练使用的话,你可能需要对 Flask 的 URL route 规则有一定了解,然后参考 [view/\_\_init\_\_.py](https://github.com/veripress/veripress/blob/master/veripress/view/__init__.py) 文件最底部的 URL 规则。 69 | 70 | ## 适配不同的运行模式 71 | 72 | 如果你打算让主题同时支持动态运行和生成静态页面,可以通过 `config` 的 `GENERATING_STATIC_PAGES` 字段,该字段在执行 `veripress generate` 命令时被设置为 `True`,而动态运行时则不存在,因此你可以通过如下代码来对静态和动态模式: 73 | 74 | ```html 75 | {% if not config.GENERATING_STATIC_PAGES %} 76 |
77 | {% include ['custom/searchbar.html', 'searchbar.html'] ignore missing %} 78 |
79 | {% endif %} 80 | ``` 81 | 82 | ## 调试主题 83 | 84 | 制作主题时可能会出现异常(Exception),如果直接显示「500 Internal Error」可能没什么帮助,这时可以使用 `veripress preview --debug` 来预览,`--debug` 选项将开启 Flask 的调试模式,在抛出异常时会将异常信息显示在页面上。 85 | 86 | ## 制作主题时遇到问题? 87 | 88 | 不得不承认这篇关于如何制作主题的文档写得非常简陋,如果你在自己制作过程中遇到不太明确的事情,在这里也找不到的话,首先可以参考官方主题,如果还有疑问(或者对官方主题的写法不太认同),请毫不吝啬地提交 [issue](https://github.com/veripress/veripress/issues/new)。 89 | -------------------------------------------------------------------------------- /tests/test_views.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from veripress import app 5 | 6 | 7 | def test_404(): 8 | with app.test_client() as c: 9 | resp = c.get('/non-exists.html') 10 | assert 'Page Not Found' in resp.data.decode('utf-8') 11 | 12 | 13 | def test_index(): 14 | with app.test_client() as c: 15 | assert c.get('/page/1').headers['Location'] == 'http://localhost/page/1/' 16 | assert c.get('/page/1/').headers['Location'] == 'http://localhost/' 17 | 18 | shutil.move(os.path.join(app.instance_path, 'pages', 'index.mdown'), 19 | os.path.join(app.instance_path, 'pages', 'index2.mdown')) 20 | resp = c.get('/') 21 | assert resp.status_code == 200 22 | assert 'My Blog' in resp.data.decode('utf-8') 23 | assert 'TOC

' in resp.data.decode('utf-8') 45 | assert '

TOC HTML

' in resp.data.decode('utf-8') 46 | 47 | resp = c.get('/post/2017/03/09/non-exists/') 48 | assert 'Page Not Found' in resp.data.decode('utf-8') 49 | 50 | app.config['SHOW_TOC'] = False 51 | with app.test_client() as c: 52 | resp = c.get('/post/2017/03/09/my-post/') 53 | assert 'My Post - My Blog' in resp.data.decode('utf-8') 54 | assert '

TOC

' not in resp.data.decode('utf-8') 55 | assert '

TOC HTML

' not in resp.data.decode('utf-8') 56 | app.config['SHOW_TOC'] = True 57 | 58 | 59 | def test_page(): 60 | with app.test_client() as c: 61 | resp = c.get('/abc') 62 | assert resp.status_code == 302 63 | assert resp.headers.get('Location').endswith('/abc.html') 64 | resp = c.get('/a/b') 65 | assert resp.status_code == 302 66 | assert resp.headers.get('Location').endswith('/a/b/') 67 | 68 | resp = c.get('/test-page.txt') 69 | assert resp.content_type == 'text/plain; charset=utf-8' 70 | 71 | resp = c.get('/dddd/') 72 | assert 'D' in resp.data.decode('utf-8') 73 | 74 | resp = c.get('/test-page.html') 75 | assert 'Test Page' in resp.data.decode('utf-8') 76 | 77 | resp = c.get('/..%2F..%2Fetc%2Fpasswd') 78 | assert resp.status_code == 403 79 | 80 | 81 | def test_tags_categories_archive_search(): 82 | with app.test_client() as c: 83 | resp = c.get('/tag/non-exists/') 84 | assert 'Page Not Found' in resp.data.decode('utf-8') 85 | resp = c.get('/category/non-exists/') 86 | assert 'Page Not Found' in resp.data.decode('utf-8') 87 | 88 | resp = c.get('/tag/Hello World/') 89 | assert 'Hello World - Tag - My Blog' in resp.data.decode('utf-8') 90 | assert 'My Post' in resp.data.decode('utf-8') 91 | 92 | resp = c.get('/category/Default/') 93 | assert 'Default - Category - My Blog' in resp.data.decode('utf-8') 94 | assert 'My Post' in resp.data.decode('utf-8') 95 | 96 | resp = c.get('/archive/2017/3/') 97 | assert '2017.3 - Archive - My Blog' in resp.data.decode('utf-8') 98 | assert 'My Post' in resp.data.decode('utf-8') 99 | 100 | resp = c.get('/search?q=') 101 | assert 'Page Not Found' in resp.data.decode('utf-8') 102 | resp = c.get('/search?q=no yaml') 103 | assert '"no yaml" - Search - My Blog' in resp.data.decode('utf-8') 104 | assert 'My Post No Yaml' in resp.data.decode('utf-8') 105 | resp = c.get('/search?q=Title') 106 | assert '"Title" - Search - My Blog' in resp.data.decode('utf-8') 107 | assert 'My Post No Yaml' not in resp.data.decode('utf-8') 108 | assert 'My Post' in resp.data.decode('utf-8') 109 | 110 | 111 | def test_feed(): 112 | with app.test_client() as c: 113 | resp = c.get('/feed.xml') 114 | assert 'My Post' in resp.data.decode('utf-8') 115 | assert 'My Post No Yaml' in resp.data.decode('utf-8') 116 | assert 'My Post No Yaml2' in resp.data.decode('utf-8') 117 | assert resp.content_type == 'application/atom+xml; charset=utf-8' 118 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from flask import make_response, jsonify, Response 4 | from pytest import raises 5 | 6 | from veripress import app, site 7 | from veripress.api import Error, json_api, ApiException 8 | 9 | 10 | def get_json(client, url): 11 | resp = client.get('/api' + url) 12 | data = json.loads(resp.data.decode(encoding='utf-8')) 13 | return data 14 | 15 | 16 | def test_api_basic(): 17 | with app.test_client() as c: 18 | data = get_json(c, '/non-exists') 19 | assert data['code'] == Error.NO_SUCH_API.code 20 | 21 | 22 | def test_json_api_decorator(): 23 | @json_api 24 | def return_none(): 25 | return None 26 | 27 | @json_api 28 | def return_response(): 29 | resp_ = make_response() 30 | resp_.status_code = 204 31 | return resp_ 32 | 33 | @json_api 34 | def return_serializable(): 35 | return 'abc' 36 | 37 | @json_api 38 | def return_iterable(): 39 | a = [1, 2, 3] 40 | return filter(lambda x: x >= 2, a) 41 | 42 | @json_api 43 | def return_unknown(): 44 | class A: 45 | pass 46 | 47 | return A() 48 | 49 | with raises(ApiException, error=Error.RESOURCE_NOT_EXISTS): 50 | return_none() 51 | 52 | with app.test_request_context('/api/posts'): 53 | assert return_response().status_code == 204 54 | resp = return_serializable() 55 | assert isinstance(resp, Response) 56 | assert resp.data == jsonify('abc').data 57 | resp = return_iterable() 58 | assert isinstance(resp, Response) 59 | assert resp.data == jsonify([2, 3]).data 60 | with raises(TypeError): 61 | return_unknown() 62 | 63 | 64 | def test_site(): 65 | with app.test_client() as c: 66 | data = get_json(c, '/site') 67 | assert data == site 68 | 69 | 70 | def test_posts(): 71 | with app.test_client() as c: 72 | data = get_json(c, '/posts') 73 | assert isinstance(data, list) 74 | assert len(data) == 3 # no drafts! 75 | assert 'My Post' in data[0]['title'] 76 | 77 | data = get_json(c, '/posts/2017/') 78 | assert len(data) == 3 79 | data = get_json(c, '/posts?start=1&count=1') 80 | assert len(data) == 1 81 | data = get_json(c, '/posts?created=2016-03-02,2016-03-03&updated=') 82 | assert data['code'] == Error.INVALID_ARGUMENTS.code 83 | data = get_json(c, 84 | '/posts?created=2016-03-02,2017-03-09&updated=2016-03-02,2017-03-09') 85 | assert len(data) == 2 86 | 87 | data = get_json(c, '/posts/2017/03/09/my-post') 88 | assert isinstance(data, dict) 89 | assert data['categories'] == ['Default'] 90 | 91 | data = get_json(c, 92 | '/posts/2017/03/09/my-post/?fields=title,content,created,updated') 93 | assert 'categories' not in data 94 | 95 | data = get_json(c, '/posts/2017/03/09/non-exists') 96 | assert data['code'] == Error.RESOURCE_NOT_EXISTS.code 97 | 98 | 99 | def test_tags_categories(): 100 | with app.test_client() as c: 101 | data = get_json(c, '/tags') 102 | assert {'name': 'Hello World', 'published': 1} in data 103 | 104 | data = get_json(c, '/categories') 105 | assert {'name': 'Default', 'published': 1} in data 106 | 107 | 108 | def test_pages(): 109 | with app.test_client() as c: 110 | data = get_json(c, '/pages/non-exists') 111 | assert data['code'] == Error.RESOURCE_NOT_EXISTS.code 112 | 113 | data = get_json(c, '/pages/../../../../etc/passwd') 114 | assert data['code'] == Error.NOT_ALLOWED.code 115 | 116 | resp = c.get('/api/pages/test-page.txt') 117 | assert resp.status_code == 200 118 | assert resp.content_type.startswith('text/plain') 119 | 120 | data = get_json(c, '/pages/my-page/') 121 | assert 'Lorem ipsum dolor sit amet.' in data['content'] 122 | 123 | data = get_json(c, '/pages/test-page-draft') 124 | assert data['code'] == Error.RESOURCE_NOT_EXISTS.code 125 | 126 | 127 | def test_widgets(): 128 | with app.test_client() as c: 129 | data = get_json(c, '/widgets') 130 | assert len(data) == 2 131 | 132 | data = get_json(c, '/widgets?position=header') 133 | assert data['code'] == Error.RESOURCE_NOT_EXISTS.code 134 | 135 | 136 | def test_search(): 137 | with app.test_client() as c: 138 | data = get_json(c, '/search?q=') 139 | assert data['code'] == Error.INVALID_ARGUMENTS.code 140 | 141 | data = get_json(c, '/search?q=non-exist-non-exist-non-exist') 142 | assert data['code'] == Error.RESOURCE_NOT_EXISTS.code 143 | 144 | data = get_json(c, '/search?q=hello') 145 | assert len(data) == 1 146 | assert data[0]['rel_url'] == 'a/b/hello.html' 147 | 148 | data = get_json(c, '/search/?q=Lorem ipsum') 149 | assert len(data) == 5 150 | data = get_json(c, '/search/?q=Lorem ipsum&start=1&count=2') 151 | assert len(data) == 2 152 | -------------------------------------------------------------------------------- /tests/test_storage.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime 2 | from collections import Iterable 3 | 4 | from pytest import raises 5 | 6 | from veripress import app, create_app 7 | from veripress.model import storage, get_storage 8 | from veripress.model.storages import Storage, FileStorage 9 | from veripress.helpers import ConfigurationError 10 | 11 | 12 | def test_get_storage(): 13 | wrong_app = create_app('config2.py') 14 | assert wrong_app.config['STORAGE_TYPE'] == 'fake_type' 15 | with raises(ConfigurationError, message='Storage type "fake_type" is not supported.'): 16 | with wrong_app.app_context(): 17 | get_storage() # this will raise a ConfigurationError, because the storage type is not supported 18 | 19 | with app.app_context(): 20 | s = get_storage() 21 | assert storage == s 22 | assert id(storage) != id(s) 23 | # noinspection PyProtectedMember 24 | assert storage._get_current_object() == s 25 | 26 | assert isinstance(s, Storage) 27 | assert isinstance(s, FileStorage) 28 | assert s.closed # the storage object should be marked as 'closed' after the app context being torn down 29 | with raises(AttributeError): 30 | setattr(s, 'closed', True) 31 | 32 | 33 | def test_fix_rel_url(): 34 | with app.app_context(): 35 | correct = '2017/01/01/my-post/' 36 | assert Storage.fix_post_relative_url('2017/01/01/my-post/') == correct 37 | assert Storage.fix_post_relative_url('2017/1/1/my-post') == correct 38 | assert Storage.fix_post_relative_url('2017/1/1/my-post.html') == correct 39 | assert Storage.fix_post_relative_url('2017/1/1/my-post/index') == correct + 'index.html' 40 | assert Storage.fix_post_relative_url('2017/1/1/my-post/index.html') == correct + 'index.html' 41 | assert Storage.fix_post_relative_url('2017/1/1/my-post/test') is None 42 | assert Storage.fix_post_relative_url('2017/13/32/my-post/') is None 43 | 44 | # assert Storage.fix_page_relative_url('my-page') == ('my-page/', False) 45 | # assert Storage.fix_page_relative_url('my-page/') == ('my-page/', False) 46 | # assert Storage.fix_page_relative_url('test-page.txt') == ('test-page.txt', True) 47 | # assert Storage.fix_page_relative_url('my-page/index.md') == ('my-page/index.md', True) 48 | # assert Storage.fix_page_relative_url('my-page/index') == ('my-page/index.html', False) 49 | # assert Storage.fix_page_relative_url('my-page/index.htm') == ('my-page/index.html', False) 50 | # assert Storage.fix_page_relative_url('my-page/index.html') == ('my-page/index.html', False) 51 | # assert Storage.fix_page_relative_url('//') == (None, False) 52 | 53 | storage_ = Storage() 54 | assert storage_.fix_relative_url('post', '2017/1/1/my-post/index') == ('2017/01/01/my-post/index.html', False) 55 | # assert Storage.fix_relative_url('page', '/my-page/index.htm') == ('my-page/index.html', False) 56 | with raises(ValueError, message='Publish type "wrong" is not supported'): 57 | storage_.fix_relative_url('wrong', 'wrong-publish-type/') 58 | 59 | 60 | def test_base_storage(): 61 | s = Storage() 62 | with raises(NotImplementedError): 63 | s.fix_page_relative_url('') 64 | with raises(NotImplementedError): 65 | s.get_posts() 66 | with raises(NotImplementedError): 67 | s.get_post('') 68 | with raises(NotImplementedError): 69 | s.get_tags() 70 | with raises(NotImplementedError): 71 | s.get_categories() 72 | with raises(NotImplementedError): 73 | s.get_pages() 74 | with raises(NotImplementedError): 75 | s.get_page('') 76 | with raises(NotImplementedError): 77 | s.get_widgets() 78 | 79 | 80 | def test_get_posts_with_limits(): 81 | with app.app_context(): 82 | posts = storage.get_posts_with_limits(include_draft=True) 83 | assert posts == storage.get_posts(include_draft=True) 84 | 85 | posts = storage.get_posts_with_limits(include_draft=True, tags='Hello World', categories=['Default']) 86 | assert len(posts) == 2 87 | 88 | posts = storage.get_posts_with_limits(include_draft=True, 89 | created=(datetime.strptime('2016-02-02', '%Y-%m-%d'), 90 | date(year=2016, month=3, day=3))) 91 | assert len(posts) == 1 92 | 93 | posts = storage.get_posts_with_limits(include_draft=True, 94 | created=(date(year=2011, month=2, day=2), 95 | date(year=2014, month=2, day=2))) 96 | assert len(posts) == 0 97 | 98 | 99 | def test_search_for(): 100 | with app.app_context(): 101 | assert storage.search_for('') == [] 102 | assert isinstance(storage.search_for('Hello'), Iterable) 103 | assert len(list(storage.search_for('Hello'))) == 1 104 | assert len(list(storage.search_for('Hello', include_draft=True))) == 2 105 | 106 | app.config['ALLOW_SEARCH_PAGES'] = False 107 | with app.app_context(): 108 | assert len(list(storage.search_for('Hello'))) == 0 109 | app.config['ALLOW_SEARCH_PAGES'] = True 110 | -------------------------------------------------------------------------------- /veripress/model/parsers.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import partial 3 | 4 | import markdown 5 | 6 | from veripress.helpers import to_list 7 | 8 | 9 | class Parser(object): 10 | """Base parser class.""" 11 | 12 | # this should be overridden in subclasses, 13 | # and should be a compiled regular expression 14 | _read_more_exp = None 15 | 16 | def __init__(self): 17 | if self._read_more_exp is not None and \ 18 | isinstance(self._read_more_exp, str): 19 | # compile the regular expression 20 | # make the regex require new lines above and below the sep flag 21 | self._read_more_exp = re.compile( 22 | r'\r?\n\s*?' + self._read_more_exp + r'\s*?\r?\n', 23 | re.IGNORECASE 24 | ) 25 | 26 | def parse_preview(self, raw_content): 27 | """ 28 | Parse the preview part of the content, 29 | and return the parsed string and whether there is more content or not. 30 | 31 | If the preview part is equal to the whole part, 32 | the second element of the returned tuple will be False, else True. 33 | 34 | :param raw_content: raw content 35 | :return: tuple(parsed string, whether there is more content or not) 36 | """ 37 | if self._read_more_exp is None: 38 | return self.parse_whole(raw_content), False 39 | 40 | sp = self._read_more_exp.split(raw_content, maxsplit=1) 41 | if len(sp) == 2 and sp[0]: 42 | has_more_content = True 43 | result = sp[0].rstrip() 44 | else: 45 | has_more_content = False 46 | result = raw_content 47 | # since the preview part contains no read_more_sep, 48 | # we can safely use the parse_whole method 49 | return self.parse_whole(result), has_more_content 50 | 51 | def parse_whole(self, raw_content): 52 | """ 53 | Parse the whole part of the content. 54 | Should be overridden in subclasses. 55 | """ 56 | raise NotImplementedError 57 | 58 | def remove_read_more_sep(self, raw_content): 59 | """ 60 | Removes the first read_more_sep that occurs in raw_content. 61 | Subclasses should call this method to preprocess raw_content. 62 | """ 63 | if self._read_more_exp is None: 64 | return raw_content 65 | 66 | sp = self._read_more_exp.split(raw_content, maxsplit=1) 67 | if len(sp) == 2 and sp[0]: 68 | result = '\n\n'.join((sp[0].rstrip(), sp[1].lstrip())) 69 | else: 70 | result = raw_content 71 | return result 72 | 73 | 74 | # key: extension name, value: standard format name 75 | _ext_format_mapping = {} 76 | # key: standard format name, value: parser instance 77 | _format_parser_mapping = {} 78 | 79 | 80 | def get_standard_format_name(ext_name): 81 | """ 82 | Get the standard format name of the given extension. 83 | 84 | :param ext_name: extension name 85 | :return: standard format name 86 | """ 87 | return _ext_format_mapping.get(ext_name.lower()) 88 | 89 | 90 | def get_parser(format_name): 91 | """ 92 | Get parser of the given format. 93 | 94 | :param format_name: standard format name 95 | :return: the parser instance 96 | """ 97 | return _format_parser_mapping.get(format_name.lower()) 98 | 99 | 100 | def parser(format_name, ext_names=None): 101 | """ 102 | Decorate a parser class to register it. 103 | 104 | :param format_name: standard format name 105 | :param ext_names: supported extension name 106 | """ 107 | 108 | def decorator(cls): 109 | format_name_lower = format_name.lower() 110 | if ext_names is None: 111 | _ext_format_mapping[format_name_lower] = format_name_lower 112 | else: 113 | for ext in to_list(ext_names): 114 | _ext_format_mapping[ext.lower()] = format_name_lower 115 | _format_parser_mapping[format_name_lower] = cls() 116 | return cls 117 | 118 | return decorator 119 | 120 | 121 | @parser('txt', ext_names=['txt']) 122 | class TxtParser(Parser): 123 | """Txt content parser.""" 124 | 125 | _read_more_exp = r'-{3,}[ \t]*more[ \t]*-{3,}' 126 | 127 | def parse_whole(self, raw_content): 128 | raw_content = self.remove_read_more_sep(raw_content) 129 | return '
{}
'.format(raw_content) 130 | 131 | 132 | @parser('markdown', ext_names=['md', 'mdown', 'markdown']) 133 | class MarkdownParser(Parser): 134 | """Markdown content parser.""" 135 | 136 | _read_more_exp = r'' 137 | 138 | _markdown = partial( 139 | markdown.markdown, 140 | output_format='html5', 141 | extensions=[ 142 | 'markdown.extensions.extra', 143 | 'markdown.extensions.codehilite' 144 | ], 145 | extension_configs={ 146 | 'markdown.extensions.codehilite': { 147 | 'guess_lang': False, 148 | 'css_class': 'highlight', 149 | 'use_pygments': True 150 | } 151 | }, 152 | ) 153 | 154 | def parse_whole(self, raw_content): 155 | raw_content = self.remove_read_more_sep(raw_content) 156 | return self._markdown(raw_content).strip() 157 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime, timedelta 3 | 4 | from pytest import raises 5 | 6 | from veripress import app 7 | from veripress.model import CustomJSONEncoder 8 | from veripress.model.models import Base, Page, Post, Widget 9 | 10 | 11 | def test_base_model(): 12 | base = Base() 13 | assert base.meta == {} 14 | 15 | base.format = 'TXT' 16 | assert base.format == 'txt' # the 'format' property automatically convert the value to lowercase 17 | 18 | base.meta = {'title': 'Hello world', 'author': 'Richard'} 19 | base.raw_content = 'This is a test content' 20 | assert base.is_draft == False # default value is False 21 | assert base.to_dict() == { 22 | 'meta': {'title': 'Hello world', 'author': 'Richard'}, 23 | 'format': 'txt', 24 | 'raw_content': 'This is a test content', 25 | 'is_draft': False 26 | } 27 | 28 | base.meta['is_draft'] = True 29 | assert base.is_draft == True # will change dynamically when meta changes 30 | 31 | base1 = Base() 32 | base1.format = 'txt' 33 | base2 = Base() 34 | base2.format = 'markdown' 35 | assert base1 != base2 36 | base2.format = 'txt' 37 | assert base1 == base2 38 | assert base1 != 'other object type' 39 | 40 | 41 | def test_page_model(): 42 | page = Page() 43 | assert page.layout == 'page' # default layout 44 | assert page.title is None 45 | assert page.author == 'My Name' # default value is from site.json 46 | assert page.email == 'my-email@example.com' # like above 47 | assert page.created is None and page.updated is None 48 | 49 | assert hasattr(page, 'unique_key') 50 | assert hasattr(page, 'rel_url') 51 | 52 | dt = datetime.now() 53 | page.meta = { 54 | 'author': 'Richard', 55 | 'email': 'richard@example.com', 56 | 'created': dt 57 | } 58 | assert page.author == 'Richard' 59 | assert page.email == 'richard@example.com' 60 | assert page.created == page.updated == dt 61 | 62 | # test parsing default title from rel_url 63 | page.rel_url = 'index.html' 64 | assert page.title == 'Index' 65 | page.rel_url = 'a-test-/index.html' 66 | assert page.title == 'A Test' 67 | page.rel_url = 'a--test/b-test/c-test.html' 68 | assert page.title == 'C Test' 69 | page.rel_url = 'a-test/b-test/' 70 | assert page.title == 'B Test' 71 | page.rel_url = '测试/index.html' 72 | assert page.title == '测试' 73 | 74 | page.meta['title'] = 'My Page' 75 | assert page.title == 'My Page' 76 | 77 | dt2 = datetime.now() + timedelta(days=2) 78 | page.meta['updated'] = dt2 79 | assert page.created != page.updated 80 | 81 | 82 | def test_post_model(): 83 | post = Post() 84 | assert isinstance(post, Page) 85 | 86 | # test parsing default title and created date from rel_url 87 | post.rel_url = '2017/03/10/my-post/' 88 | assert post.created == datetime.strptime('2017/03/10', '%Y/%m/%d') 89 | assert post.title == 'My Post' 90 | post.meta['title'] = 'My First Post' 91 | assert post.title == 'My First Post' 92 | 93 | # test tags and categories with str and list types 94 | assert post.tags == [] 95 | assert post.categories == [] 96 | post.meta['tags'] = 'VeriPress' 97 | assert post.tags == ['VeriPress'] 98 | post.meta['categories'] = 'Dev' 99 | assert post.categories == ['Dev'] 100 | 101 | post.meta['tags'] = ['A', 'B'] 102 | assert post.tags == ['A', 'B'] 103 | post.meta['categories'] = ['A'] 104 | assert post.categories == ['A'] 105 | 106 | 107 | def test_widget_model(): 108 | widget = Widget() 109 | assert widget.position is None 110 | assert widget.order is None 111 | assert not hasattr(widget, 'unique_key') 112 | assert not hasattr(widget, 'rel_url') 113 | assert isinstance(widget, Base) 114 | assert not isinstance(widget, Page) 115 | 116 | widget.meta['position'] = 'sidebar' 117 | widget.meta['order'] = 0 118 | assert widget.position == 'sidebar' 119 | assert widget.order == 0 120 | 121 | 122 | def test_json_encoder(): 123 | assert app.json_encoder == CustomJSONEncoder 124 | 125 | page = Page() 126 | page.rel_url = 'my-page/index.html' 127 | page.unique_key = '/my-page/' 128 | page.raw_content = 'This is the raw content.' 129 | page.format = 'txt' 130 | dt = datetime.now() 131 | page.meta = {'title': 'My Page', 'author': 'Richard', 'created': dt} 132 | result = json.dumps(page, cls=CustomJSONEncoder) 133 | assert json.loads(result) == { 134 | 'meta': {'title': 'My Page', 'author': 'Richard', 'created': dt.strftime('%Y-%m-%d %H:%M:%S')}, 135 | 'raw_content': 'This is the raw content.', 136 | 'format': 'txt', 137 | 'is_draft': False, 138 | 'unique_key': '/my-page/', 139 | 'rel_url': 'my-page/index.html', 140 | 'layout': 'page', 141 | 'title': 'My Page', 142 | 'author': 'Richard', 143 | 'email': 'my-email@example.com', 144 | 'created': dt.strftime('%Y-%m-%d %H:%M:%S'), 145 | 'updated': dt.strftime('%Y-%m-%d %H:%M:%S') 146 | } 147 | 148 | class NotSupportedClass: 149 | pass 150 | 151 | not_supported = NotSupportedClass() 152 | with raises(TypeError): 153 | json.dumps(not_supported, cls=CustomJSONEncoder) 154 | -------------------------------------------------------------------------------- /docs/pages/writing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 撰写内容 3 | author: Richard Chien 4 | created: 2017-03-20 5 | updated: 2017-06-02 6 | --- 7 | 8 | VeriPress 支持三种内容形式:文章(post)、自定义页面(page)、页面部件(widget)。其中,文章(post)是指可以通过 `/post/////` 形式的 URL 访问的页面;自定义页面(page)是指直接在根 URL 后加上页面路径来访问的页面,如 `/hello/` 或 `/my-custom/page.html`;页面部件(widget)是指常驻页面的小部件,需要主题支持,默认主题只支持一种部件,也就是侧边栏部件。 9 | 10 | ## 通用 11 | 12 | ### 文件格式 13 | 14 | 除了之前的 [开始使用](getting-started.html) 中提到的 Markdown 格式,目前还支持 TXT 格式,后续还会加入其它格式的支持。VeriPress 通过文件扩展名来区分格式,并通过相应格式的解析器将正文解析成 HTML 并传给主题来显示。下面是目前支持的格式列表: 15 | 16 | | 格式名称 | 支持的扩展名 | 17 | | -------- | -------------------------- | 18 | | markdown | `.md`、`.mdown`、`.markdown` | 19 | | txt | `.txt` | 20 | 21 | 其中,Markdown 格式采用「Markdown Extra」扩展,该扩展在 [标准 Markdown 语法](https://daringfireball.net/projects/markdown/syntax) 的基础上,加入了一些其它实用语法,具体见 [PHP Markdown Extra](https://michelf.ca/projects/php-markdown/extra/)。 22 | 23 | 无论使用什么格式书写内容,文件的开头都使用 YAML 来标记元信息,并在其上下分别用 `---` 来分隔,例如: 24 | 25 | ``` 26 | --- 27 | title: 文章的标题 28 | author: 作者 29 | --- 30 | 31 | 正文内容,可使用不同的格式/标记语言书写。 32 | ``` 33 | 34 | ### YAML 头部 35 | 36 | YAML 头部用于标记文章、页面、部件的一些基本元信息,比如标题、作者、标签、分类等,这里的元信息将被传到主题的模板文件中,因此具体这些信息有哪些被显示出来、以什么形式显示,都取决于你使用的主题,后面对三种内容形式的分别阐述中,将主要给出 VeriPress 原生支持的(或者说默认主题支持的)元信息项。 37 | 38 | 另外,三种内容形式都支持的一个元信息是 `is_draft`,用于表示该篇内容是否为草稿,例如,如果有一篇文章内容如下: 39 | 40 | ``` 41 | --- 42 | title: 标题 43 | is_draft: true 44 | --- 45 | 46 | 正文内容 47 | ``` 48 | 49 | 则它将不会显示在文章列表中,也无法通过具体路径访问(API 也无法访问)。`is_draft` 的默认值是 `false`,因此如果不填,默认认为不是草稿,会将其发布。这个元信息的效果是由 VeriPress 核心程序所保证的,因此不会受主题的影响。 50 | 51 | 另外,所有官方主题(即 [veripress/themes](https://github.com/veripress/themes) 仓库中的主题),均支持 `language` 元信息(三种内容形式都支持),此元信息项的值,会覆盖 `site.json` 中的同名项,见 [修改网站信息](getting-started.html#修改网站信息)。 52 | 53 | ## 文章(Post) 54 | 55 | 文章可通过形如 `/post/////` 的 URL 访问,文件放在 `posts` 目录中,命名格式形如 `2017-03-20-post-name.md`,因为文章其实就是博客的「博文」,发布时间非常重要,因此在文件名中指出创建日期将有助于管理,同时也在 YAML 头部没有指定创建日期时作为默认的创建日期。 56 | 57 | ### 支持的 YAML 元信息 58 | 59 | 文章默认支持的 YAML 元信息如下: 60 | 61 | | 项 | 默认值 | 说明 | 62 | | ------------ | ---------------------------------------- | ---------------------------------------- | 63 | | `title` | 文件名的 `post-name` 中 `-` 换成空格并让每个单词首字母大写,如 `Post Name` | 文章标题 | 64 | | `layout` | `post` | 文章的页面布局,VeriPress 会在主题的模板目录中寻找和这个项同名的模板文件来渲染页面,一般情况下保持默认即可 | 65 | | `author` | `site.json` 中的 `author` 项 | 文章的作者名 | 66 | | `email` | `site.json` 中的 `email` 项 | 文章的作者 email | 67 | | `created` | 文件名中的日期的 00:00:00 | 创建时间 | 68 | | `updated` | 等于 `created` | 更新时间 | 69 | | `tags` | 空列表 | 文章所属标签,可使用 YAML 字符串或列表,如 `tags: Hello` 或 `tags: [TagA, TagB]` | 70 | | `categories` | 空列表 | 文章所属分类,同样可使用 YAML 字符串或列表 | 71 | 72 | ### 划分预览部分 73 | 74 | 文章还支持在正文中划分预览部分,从而在文章列表中仅显示预览部分,以节省页面空间。不同的文件格式,使用不同的预览分隔标记(或称阅读更多标记),如下: 75 | 76 | | 格式名称 | 预览分隔标记/阅读更多标记 | 77 | | -------- | ------------- | 78 | | markdown | `` | 79 | | txt | `---more---` | 80 | 81 | 预览分隔标记之前的内容将被作为预览部分显示在首页文章列表中(需要主题支持),并显示一个 `READ MORE` 链接,而没有预览分隔标记的文章将默认把全部正文作为预览部分,这时将不显示 `READ MORE` 链接(这是默认主题的行为,第三方主题未必这样)。 82 | 83 | 注意:分隔标记前后都需要换行才有效。 84 | 85 | ## 自定义页面(Page) 86 | 87 | 自定义页面有时候被直接称为「页面」,它们可通过形如 `/hello/` 或 `/my-custom/page.html` 的 URL 访问,文件放在 `pages` 目录中,命名格式形如 `page-name.md`。 88 | 89 | ### 页面访问逻辑 90 | 91 | 之所以称为自定义页面,是因为这种内容形式自定义性比较强,你可以在 `pages` 中创建多级目录来组织自定义页面,甚至可以直接将 HTML 文件或其它静态文件放在里面。 92 | 93 | 对于使用非 HTML、且 VeriPress 支持的文件格式,可以通过 `.html` 后缀的 URL 来访问,比如有一个自定义页面的文件路径是 `/pages/a/b/c/d.md`,你将可以通过 `/a/b/c/d.html` 来访问这个页面,与此同时,你还可以直接通过 `/a/b/c/d.md` 来访问这个原始 Markdown 文件(前提是配置文件中的 `PAGE_SOURCE_ACCESSIBLE` 设置为 `True`,见 [配置文件](configuration-file.html#PAGE-SOURCE-ACCESSIBLE))。如果这里的 Markdown 文件名是 `index`,例如 `/pages/a/b/c/index.md`,你还可以通过 `/a/b/c/` 来访问。 94 | 95 | 而如果直接使用 HTML 文件,逻辑则更加简单:只要这个文件存在,就会直接返回,例如你可以通过 URL `/abc/index.html` 或 `/abc/` 来访问文件 `/pages/abc/index.html`。 96 | 97 | 如果你直接在 `pages` 目录中创建了一个 `index.md` 或 `index.html` 或其它所支持的文件格式,这个自定义页面将会覆盖首页,也就是当你访问 URL `/` 的时候,访问的实际上是这个自定义页面,而不是默认的文章列表。 98 | 99 | ### 支持的 YAML 元信息 100 | 101 | 自定义页面默认支持的 YAML 元信息如下: 102 | 103 | | 项 | 默认值 | 说明 | 104 | | --------- | ---------------------------------------- | ---------------------------------------- | 105 | | `title` | 文件名的中 `-` 换成空格并让每个单词首字母大写,对于 `index.xx` 将对它的上一级目录进行转换,如 `hello-world/index.md` 的默认标题为 `Hello World`,`hello.md` 默认为 `Hello` | 页面的标题 | 106 | | `layout` | `page` | 自定义页面的页面布局,VeriPress 会在主题的模板目录中寻找和这个项同名的模板文件来渲染页面,一般情况下保持默认即可 | 107 | | `author` | `site.json` 中的 `author` 项 | 页面的作者名 | 108 | | `email` | `site.json` 中的 `email` 项 | 页面的作者 email | 109 | | `created` | 空 | 创建时间 | 110 | | `updated` | 等于 `created` | 更新时间 | 111 | 112 | ## 页面部件(Widget) 113 | 114 | 页面部件是指常驻页面的小部件,例如出现在侧边栏、顶栏、footer 等,但这需要主题支持,默认主题只支持侧边栏部件(sidebar)。 115 | 116 | ### 支持的 YAML 元信息 117 | 118 | 页面部件默认支持的 YAML 元信息如下: 119 | 120 | | 项 | 默认值 | 说明 | 121 | | ---------- | ---- | ---------------------------------------- | 122 | | `position` | 空 | 部件应该出现的位置,例如默认主题支持 `sidebar`,所有 `position` 为 `sidebar` 的部件将显示在侧边栏 | 123 | | `order` | 空 | 部件在其位置上的顺序,主题在获取部件列表时将按此项从小到大排序 | 124 | 125 | 对于页面部件来说,上面两个元信息基本上都是必填(除非显示顺序不重要或主题不区分位置)。 126 | -------------------------------------------------------------------------------- /demo/themes/default/static/highlight.css: -------------------------------------------------------------------------------- 1 | /* Code Highlight Below */ 2 | 3 | .highlight .hll { 4 | background-color: #ffffcc 5 | } 6 | 7 | .highlight { 8 | background: #f8f8f8; 9 | } 10 | 11 | .highlight .c { 12 | color: #408080; 13 | font-style: italic 14 | } 15 | 16 | /* Comment */ 17 | .highlight .err { 18 | border: 1px solid #FF0000 19 | } 20 | 21 | /* Error */ 22 | .highlight .k { 23 | color: #008000; 24 | font-weight: bold 25 | } 26 | 27 | /* Keyword */ 28 | .highlight .o { 29 | color: #666666 30 | } 31 | 32 | /* Operator */ 33 | .highlight .cm { 34 | color: #408080; 35 | font-style: italic 36 | } 37 | 38 | /* Comment.Multiline */ 39 | .highlight .cp { 40 | color: #BC7A00 41 | } 42 | 43 | /* Comment.Preproc */ 44 | .highlight .c1 { 45 | color: #408080; 46 | font-style: italic 47 | } 48 | 49 | /* Comment.Single */ 50 | .highlight .cs { 51 | cohighlightlor: #408080; 52 | font-style: italic 53 | } 54 | 55 | /* Comment.Special */ 56 | .highlight .gd { 57 | color: #A00000 58 | } 59 | 60 | /* Generic.Deleted */ 61 | .highlight .ge { 62 | font-style: italic 63 | } 64 | 65 | /* Generic.Emph */ 66 | .highlight .gr { 67 | color: #FF0000 68 | } 69 | 70 | /* Generic.Error */ 71 | .highlight .gh { 72 | color: #000080; 73 | font-weight: bold 74 | } 75 | 76 | /* Generic.Heading */ 77 | .highlight .gi { 78 | color: #00A000 79 | } 80 | 81 | /* Generic.Inserted */ 82 | .highlight .go { 83 | color: #808080 84 | } 85 | 86 | /* Generic.Output */ 87 | .highlight .gp { 88 | color: #000080; 89 | font-weight: bold 90 | } 91 | 92 | /* Generic.Prompt */ 93 | .highlight .gs { 94 | font-weight: bold 95 | } 96 | 97 | /* Generic.Strong */ 98 | .highlight .gu { 99 | color: #800080; 100 | font-weight: bold 101 | } 102 | 103 | /* Generic.Subheading */ 104 | .highlight .gt { 105 | color: #0040D0 106 | } 107 | 108 | /* Generic.Traceback */ 109 | .highlight .kc { 110 | color: #008000; 111 | font-weight: bold 112 | } 113 | 114 | /* Keyword.Constant */ 115 | .highlight .kd { 116 | color: #008000; 117 | font-weight: bold 118 | } 119 | 120 | /* Keyword.Declaration */ 121 | .highlight .kn { 122 | color: #008000; 123 | font-weight: bold 124 | } 125 | 126 | /* Keyword.Namespace */ 127 | .highlight .kp { 128 | color: #008000 129 | } 130 | 131 | /* Keyword.Pseudo */ 132 | .highlight .kr { 133 | color: #008000; 134 | font-weight: bold 135 | } 136 | 137 | /* Keyword.Reserved */ 138 | .highlight .kt { 139 | color: #B00040 140 | } 141 | 142 | /* Keyword.Type */ 143 | .highlight .m { 144 | color: #666666 145 | } 146 | 147 | /* Literal.Number */ 148 | .highlight .s { 149 | color: #BA2121 150 | } 151 | 152 | /* Literal.String */ 153 | .highlight .na { 154 | color: #7D9029 155 | } 156 | 157 | /* Name.Attribute */ 158 | .highlight .nb { 159 | color: #008000 160 | } 161 | 162 | /* Name.Builtin */ 163 | .highlight .nc { 164 | color: #0000FF; 165 | font-weight: bold 166 | } 167 | 168 | /* Name.Class */ 169 | .highlight .no { 170 | color: #880000 171 | } 172 | 173 | /* Name.Constant */ 174 | .highlight .nd { 175 | color: #AA22FF 176 | } 177 | 178 | /* Name.Decorator */ 179 | .highlight .ni { 180 | color: #999999; 181 | font-weight: bold 182 | } 183 | 184 | /* Name.Entity */ 185 | .highlight .ne { 186 | color: #D2413A; 187 | font-weight: bold 188 | } 189 | 190 | /* Name.Exception */ 191 | .highlight .nf { 192 | color: #0000FF 193 | } 194 | 195 | /* Name.Function */ 196 | .highlight .nl { 197 | color: #A0A000 198 | } 199 | 200 | /* Name.Label */ 201 | .highlight .nn { 202 | color: #0000FF; 203 | font-weight: bold 204 | } 205 | 206 | /* Name.Namespace */ 207 | .highlight .nt { 208 | color: #008000; 209 | font-weight: bold 210 | } 211 | 212 | /* Name.Tag */ 213 | .highlight .nv { 214 | color: #19177C 215 | } 216 | 217 | /* Name.Variable */ 218 | .highlight .ow { 219 | color: #AA22FF; 220 | font-weight: bold 221 | } 222 | 223 | /* Operator.Word */ 224 | .highlight .w { 225 | color: #bbbbbb 226 | } 227 | 228 | /* Text.Whitespace */ 229 | .highlight .mf { 230 | color: #666666 231 | } 232 | 233 | /* Literal.Number.Float */ 234 | .highlight .mh { 235 | color: #666666 236 | } 237 | 238 | /* Literal.Number.Hex */ 239 | .highlight .mi { 240 | color: #666666 241 | } 242 | 243 | /* Literal.Number.Integer */ 244 | .highlight .mo { 245 | color: #666666 246 | } 247 | 248 | /* Literal.Number.Oct */ 249 | .highlight .sb { 250 | color: #BA2121 251 | } 252 | 253 | /* Literal.String.Backtick */ 254 | .highlight .sc { 255 | color: #BA2121 256 | } 257 | 258 | /* Literal.String.Char */ 259 | .highlight .sd { 260 | color: #BA2121; 261 | font-style: italic 262 | } 263 | 264 | /* Literal.String.Doc */ 265 | .highlight .s2 { 266 | color: #BA2121 267 | } 268 | 269 | /* Literal.String.Double */ 270 | .highlight .se { 271 | color: #BB6622; 272 | font-weight: bold 273 | } 274 | 275 | /* Literal.String.Escape */ 276 | .highlight .sh { 277 | color: #BA2121 278 | } 279 | 280 | /* Literal.String.Heredoc */ 281 | .highlight .si { 282 | color: #BB6688; 283 | font-weight: bold 284 | } 285 | 286 | /* Literal.String.Interpol */ 287 | .highlight .sx { 288 | color: #008000 289 | } 290 | 291 | /* Literal.String.Other */ 292 | .highlight .sr { 293 | color: #BB6688 294 | } 295 | 296 | /* Literal.String.Regex */ 297 | .highlight .s1 { 298 | color: #BA2121 299 | } 300 | 301 | /* Literal.String.Single */ 302 | .highlight .ss { 303 | color: #19177C 304 | } 305 | 306 | /* Literal.String.Symbol */ 307 | .highlight .bp { 308 | color: #008000 309 | } 310 | 311 | /* Name.Builtin.Pseudo */ 312 | .highlight .vc { 313 | color: #19177C 314 | } 315 | 316 | /* Name.Variable.Class */ 317 | .highlight .vg { 318 | color: #19177C 319 | } 320 | 321 | /* Name.Variable.Global */ 322 | .highlight .vi { 323 | color: #19177C 324 | } 325 | 326 | /* Name.Variable.Instance */ 327 | .highlight .il { 328 | color: #666666 329 | } 330 | -------------------------------------------------------------------------------- /docs/themes/clean-doc/static/highlight.css: -------------------------------------------------------------------------------- 1 | /* Code Highlight Below */ 2 | 3 | .highlight .hll { 4 | background-color: #ffffcc 5 | } 6 | 7 | .highlight { 8 | background: #f8f8f8; 9 | } 10 | 11 | .highlight .c { 12 | color: #408080; 13 | font-style: italic 14 | } 15 | 16 | /* Comment */ 17 | .highlight .err { 18 | border: 1px solid #FF0000 19 | } 20 | 21 | /* Error */ 22 | .highlight .k { 23 | color: #008000; 24 | font-weight: bold 25 | } 26 | 27 | /* Keyword */ 28 | .highlight .o { 29 | color: #666666 30 | } 31 | 32 | /* Operator */ 33 | .highlight .cm { 34 | color: #408080; 35 | font-style: italic 36 | } 37 | 38 | /* Comment.Multiline */ 39 | .highlight .cp { 40 | color: #BC7A00 41 | } 42 | 43 | /* Comment.Preproc */ 44 | .highlight .c1 { 45 | color: #408080; 46 | font-style: italic 47 | } 48 | 49 | /* Comment.Single */ 50 | .highlight .cs { 51 | cohighlightlor: #408080; 52 | font-style: italic 53 | } 54 | 55 | /* Comment.Special */ 56 | .highlight .gd { 57 | color: #A00000 58 | } 59 | 60 | /* Generic.Deleted */ 61 | .highlight .ge { 62 | font-style: italic 63 | } 64 | 65 | /* Generic.Emph */ 66 | .highlight .gr { 67 | color: #FF0000 68 | } 69 | 70 | /* Generic.Error */ 71 | .highlight .gh { 72 | color: #000080; 73 | font-weight: bold 74 | } 75 | 76 | /* Generic.Heading */ 77 | .highlight .gi { 78 | color: #00A000 79 | } 80 | 81 | /* Generic.Inserted */ 82 | .highlight .go { 83 | color: #808080 84 | } 85 | 86 | /* Generic.Output */ 87 | .highlight .gp { 88 | color: #000080; 89 | font-weight: bold 90 | } 91 | 92 | /* Generic.Prompt */ 93 | .highlight .gs { 94 | font-weight: bold 95 | } 96 | 97 | /* Generic.Strong */ 98 | .highlight .gu { 99 | color: #800080; 100 | font-weight: bold 101 | } 102 | 103 | /* Generic.Subheading */ 104 | .highlight .gt { 105 | color: #0040D0 106 | } 107 | 108 | /* Generic.Traceback */ 109 | .highlight .kc { 110 | color: #008000; 111 | font-weight: bold 112 | } 113 | 114 | /* Keyword.Constant */ 115 | .highlight .kd { 116 | color: #008000; 117 | font-weight: bold 118 | } 119 | 120 | /* Keyword.Declaration */ 121 | .highlight .kn { 122 | color: #008000; 123 | font-weight: bold 124 | } 125 | 126 | /* Keyword.Namespace */ 127 | .highlight .kp { 128 | color: #008000 129 | } 130 | 131 | /* Keyword.Pseudo */ 132 | .highlight .kr { 133 | color: #008000; 134 | font-weight: bold 135 | } 136 | 137 | /* Keyword.Reserved */ 138 | .highlight .kt { 139 | color: #B00040 140 | } 141 | 142 | /* Keyword.Type */ 143 | .highlight .m { 144 | color: #666666 145 | } 146 | 147 | /* Literal.Number */ 148 | .highlight .s { 149 | color: #BA2121 150 | } 151 | 152 | /* Literal.String */ 153 | .highlight .na { 154 | color: #7D9029 155 | } 156 | 157 | /* Name.Attribute */ 158 | .highlight .nb { 159 | color: #008000 160 | } 161 | 162 | /* Name.Builtin */ 163 | .highlight .nc { 164 | color: #0000FF; 165 | font-weight: bold 166 | } 167 | 168 | /* Name.Class */ 169 | .highlight .no { 170 | color: #880000 171 | } 172 | 173 | /* Name.Constant */ 174 | .highlight .nd { 175 | color: #AA22FF 176 | } 177 | 178 | /* Name.Decorator */ 179 | .highlight .ni { 180 | color: #999999; 181 | font-weight: bold 182 | } 183 | 184 | /* Name.Entity */ 185 | .highlight .ne { 186 | color: #D2413A; 187 | font-weight: bold 188 | } 189 | 190 | /* Name.Exception */ 191 | .highlight .nf { 192 | color: #0000FF 193 | } 194 | 195 | /* Name.Function */ 196 | .highlight .nl { 197 | color: #A0A000 198 | } 199 | 200 | /* Name.Label */ 201 | .highlight .nn { 202 | color: #0000FF; 203 | font-weight: bold 204 | } 205 | 206 | /* Name.Namespace */ 207 | .highlight .nt { 208 | color: #008000; 209 | font-weight: bold 210 | } 211 | 212 | /* Name.Tag */ 213 | .highlight .nv { 214 | color: #19177C 215 | } 216 | 217 | /* Name.Variable */ 218 | .highlight .ow { 219 | color: #AA22FF; 220 | font-weight: bold 221 | } 222 | 223 | /* Operator.Word */ 224 | .highlight .w { 225 | color: #bbbbbb 226 | } 227 | 228 | /* Text.Whitespace */ 229 | .highlight .mf { 230 | color: #666666 231 | } 232 | 233 | /* Literal.Number.Float */ 234 | .highlight .mh { 235 | color: #666666 236 | } 237 | 238 | /* Literal.Number.Hex */ 239 | .highlight .mi { 240 | color: #666666 241 | } 242 | 243 | /* Literal.Number.Integer */ 244 | .highlight .mo { 245 | color: #666666 246 | } 247 | 248 | /* Literal.Number.Oct */ 249 | .highlight .sb { 250 | color: #BA2121 251 | } 252 | 253 | /* Literal.String.Backtick */ 254 | .highlight .sc { 255 | color: #BA2121 256 | } 257 | 258 | /* Literal.String.Char */ 259 | .highlight .sd { 260 | color: #BA2121; 261 | font-style: italic 262 | } 263 | 264 | /* Literal.String.Doc */ 265 | .highlight .s2 { 266 | color: #BA2121 267 | } 268 | 269 | /* Literal.String.Double */ 270 | .highlight .se { 271 | color: #BB6622; 272 | font-weight: bold 273 | } 274 | 275 | /* Literal.String.Escape */ 276 | .highlight .sh { 277 | color: #BA2121 278 | } 279 | 280 | /* Literal.String.Heredoc */ 281 | .highlight .si { 282 | color: #BB6688; 283 | font-weight: bold 284 | } 285 | 286 | /* Literal.String.Interpol */ 287 | .highlight .sx { 288 | color: #008000 289 | } 290 | 291 | /* Literal.String.Other */ 292 | .highlight .sr { 293 | color: #BB6688 294 | } 295 | 296 | /* Literal.String.Regex */ 297 | .highlight .s1 { 298 | color: #BA2121 299 | } 300 | 301 | /* Literal.String.Single */ 302 | .highlight .ss { 303 | color: #19177C 304 | } 305 | 306 | /* Literal.String.Symbol */ 307 | .highlight .bp { 308 | color: #008000 309 | } 310 | 311 | /* Name.Builtin.Pseudo */ 312 | .highlight .vc { 313 | color: #19177C 314 | } 315 | 316 | /* Name.Variable.Class */ 317 | .highlight .vg { 318 | color: #19177C 319 | } 320 | 321 | /* Name.Variable.Global */ 322 | .highlight .vi { 323 | color: #19177C 324 | } 325 | 326 | /* Name.Variable.Instance */ 327 | .highlight .il { 328 | color: #666666 329 | } 330 | -------------------------------------------------------------------------------- /veripress/helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from collections import Iterable 4 | from datetime import date, datetime, timedelta, timezone 5 | 6 | import pytz 7 | 8 | 9 | def url_rule(blueprint_or_app, rules, 10 | endpoint=None, view_func=None, **options): 11 | """ 12 | Add one or more url rules to the given Flask blueprint or app. 13 | 14 | :param blueprint_or_app: Flask blueprint or app 15 | :param rules: a single rule string or a list of rules 16 | :param endpoint: endpoint 17 | :param view_func: view function 18 | :param options: other options 19 | """ 20 | for rule in to_list(rules): 21 | blueprint_or_app.add_url_rule(rule, 22 | endpoint=endpoint, 23 | view_func=view_func, 24 | **options) 25 | 26 | 27 | def to_list(item_or_list): 28 | """ 29 | Convert a single item, a tuple, a generator or anything else to a list. 30 | 31 | :param item_or_list: single item or iterable to convert 32 | :return: a list 33 | """ 34 | if isinstance(item_or_list, list): 35 | return item_or_list 36 | elif isinstance(item_or_list, (str, bytes)): 37 | return [item_or_list] 38 | elif isinstance(item_or_list, Iterable): 39 | return list(item_or_list) 40 | else: 41 | return [item_or_list] 42 | 43 | 44 | def to_datetime(date_or_datetime): 45 | """ 46 | Convert a date object to a datetime object, 47 | or return as it is if it's not a date object. 48 | 49 | :param date_or_datetime: date or datetime object 50 | :return: a datetime object 51 | """ 52 | if isinstance(date_or_datetime, date) and \ 53 | not isinstance(date_or_datetime, datetime): 54 | d = date_or_datetime 55 | return datetime.strptime( 56 | '%04d-%02d-%02d' % (d.year, d.month, d.day), '%Y-%m-%d') 57 | return date_or_datetime 58 | 59 | 60 | def timezone_from_str(tz_str): 61 | """ 62 | Convert a timezone string to a timezone object. 63 | 64 | :param tz_str: string with format 'Asia/Shanghai' or 'UTC±[hh]:[mm]' 65 | :return: a timezone object (tzinfo) 66 | """ 67 | m = re.match(r'UTC([+|-]\d{1,2}):(\d{2})', tz_str) 68 | if m: 69 | # in format 'UTC±[hh]:[mm]' 70 | delta_h = int(m.group(1)) 71 | delta_m = int(m.group(2)) if delta_h >= 0 else -int(m.group(2)) 72 | return timezone(timedelta(hours=delta_h, minutes=delta_m)) 73 | 74 | # in format 'Asia/Shanghai' 75 | try: 76 | return pytz.timezone(tz_str) 77 | except pytz.exceptions.UnknownTimeZoneError: 78 | return None 79 | 80 | 81 | class ConfigurationError(Exception): 82 | """Raise this when there's something wrong with the configuration.""" 83 | pass 84 | 85 | 86 | class Pair(object): 87 | """A class that just represent two value.""" 88 | 89 | __dict__ = ['first', 'second'] 90 | 91 | def __init__(self, first=None, second=None): 92 | self.first = first 93 | self.second = second 94 | 95 | def __repr__(self): 96 | return '<{} ({}, {})>'.format( 97 | self.__class__.__name__, repr(self.first), repr(self.second)) 98 | 99 | def __eq__(self, other): 100 | if isinstance(other, Pair): 101 | return self.first == other.first and self.second == other.second 102 | return super().__eq__(other) 103 | 104 | def __bool__(self): 105 | return bool(self.first) or bool(self.second) 106 | 107 | __nonzero__ = __bool__ # for Python 2.x 108 | 109 | def __add__(self, other): 110 | a, b = other 111 | return Pair(self.first + a, self.second + b) 112 | 113 | def __sub__(self, other): 114 | a, b = other 115 | return Pair(self.first - a, self.second - b) 116 | 117 | def __getitem__(self, item): 118 | if isinstance(item, int): 119 | if item == 0: 120 | return self.first 121 | elif item == 1: 122 | return self.second 123 | raise IndexError 124 | 125 | def __len__(self): 126 | return 2 127 | 128 | 129 | def validate_custom_page_path(path): 130 | """ 131 | Check if a custom page path is valid or not, 132 | to prevent malicious requests. 133 | 134 | :param path: custom page path (url path) 135 | :return: valid or not 136 | """ 137 | sp = path.split('/') 138 | if '.' in sp or '..' in sp: 139 | return False 140 | return True 141 | 142 | 143 | def traverse_directory(dir_path, yield_dir=False): 144 | """ 145 | Traverse through a directory recursively. 146 | 147 | :param dir_path: directory path 148 | :param yield_dir: yield subdirectory or not 149 | :return: a generator 150 | """ 151 | if not os.path.isdir(dir_path): 152 | return 153 | 154 | for item in os.listdir(dir_path): 155 | new_path = os.path.join(dir_path, item) 156 | if os.path.isdir(new_path): 157 | if yield_dir: 158 | yield new_path + os.path.sep 159 | yield from traverse_directory(new_path, yield_dir) 160 | else: 161 | yield new_path 162 | 163 | 164 | def parse_toc(html_content): 165 | """ 166 | Parse TOC of HTML content if the SHOW_TOC config is true. 167 | 168 | :param html_content: raw HTML content 169 | :return: tuple(processed HTML, toc list, toc HTML unordered list) 170 | """ 171 | from flask import current_app 172 | from veripress.model.toc import HtmlTocParser 173 | 174 | if current_app.config['SHOW_TOC']: 175 | toc_parser = HtmlTocParser() 176 | toc_parser.feed(html_content) 177 | toc_html = toc_parser.toc_html( 178 | depth=current_app.config['TOC_DEPTH'], 179 | lowest_level=current_app.config['TOC_LOWEST_LEVEL']) 180 | toc = toc_parser.toc( 181 | depth=current_app.config['TOC_DEPTH'], 182 | lowest_level=current_app.config['TOC_LOWEST_LEVEL']) 183 | return toc_parser.html, toc, toc_html 184 | else: 185 | return html_content, None, None 186 | -------------------------------------------------------------------------------- /veripress_cli/generate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import math 3 | import shutil 4 | 5 | import click 6 | 7 | from veripress_cli import cli 8 | from veripress_cli.helpers import ( 9 | copy_folder_content, remove_folder_content, makedirs 10 | ) 11 | 12 | 13 | def get_deploy_dir(): 14 | from veripress import app 15 | return os.path.join(app.instance_path, '_deploy') 16 | 17 | 18 | @cli.command('generate', short_help='Generate static pages.', 19 | help='This command will generate all HTML pages ' 20 | 'of the chosen instance.') 21 | @click.option('--app-root', default='/', 22 | prompt='Please enter the application root ' 23 | '(used as prefix of generated url)', 24 | help='The application root of your VeriPress instance. ' 25 | 'For example, if you want to access the site ' 26 | 'through "http://example.com/blog/", ' 27 | 'then "/blog/" should be passed in as the app root.') 28 | def generate_command(app_root): 29 | deploy_dir = get_deploy_dir() 30 | if not os.path.isdir(deploy_dir): 31 | os.mkdir(deploy_dir) 32 | else: 33 | # remove all existing non-hidden files and dirs 34 | remove_folder_content(deploy_dir, ignore_hidden_file=True) 35 | 36 | from veripress import app 37 | 38 | app.config['APPLICATION_ROOT'] = app_root 39 | 40 | # mark as 'GENERATING_STATIC_PAGES' status, 41 | # so that templates can react properly, 42 | # e.g. remove search bar 43 | app.config['GENERATING_STATIC_PAGES'] = True 44 | 45 | do_generate() 46 | click.echo('\nOK! Now you can find all static pages ' 47 | 'in the "_deploy" folder.') 48 | 49 | 50 | @cli.command('clean', short_help='Clean all generated static pages.', 51 | help='This command will clean all generated HTML pages ' 52 | 'of the chosen instance.') 53 | def clean_command(): 54 | deploy_dir = get_deploy_dir() 55 | if os.path.isdir(deploy_dir): 56 | remove_folder_content(get_deploy_dir(), ignore_hidden_file=True) 57 | click.echo('All generated static pages have been cleaned.') 58 | 59 | 60 | def do_generate(): 61 | from veripress import app 62 | from veripress.model import storage 63 | 64 | deploy_dir = get_deploy_dir() 65 | 66 | # copy global static folder 67 | dst_static_folder = os.path.join(deploy_dir, 'static') 68 | if os.path.isdir(app.static_folder): 69 | shutil.copytree(app.static_folder, dst_static_folder) 70 | 71 | # copy theme static files 72 | makedirs(dst_static_folder, mode=0o755, exist_ok=True) 73 | copy_folder_content(app.theme_static_folder, dst_static_folder) 74 | 75 | # collect all possible urls (except custom pages) 76 | all_urls = {'/', '/feed.xml', '/atom.xml', '/archive/'} 77 | with app.app_context(): 78 | posts = list(storage.get_posts(include_draft=False)) 79 | 80 | index_page_count = int( 81 | math.ceil(len(posts) / app.config['ENTRIES_PER_PAGE'])) 82 | # ignore '/page/1/', which will be generated separately later 83 | for i in range(2, index_page_count + 1): 84 | all_urls.add('/page/{}/'.format(i)) 85 | 86 | for post in posts: 87 | all_urls.add(post.unique_key) 88 | all_urls.add('/archive/{}/'.format(post.created.strftime('%Y'))) 89 | all_urls.add('/archive/{}/{}/'.format(post.created.strftime('%Y'), 90 | post.created.strftime('%m'))) 91 | 92 | tags = storage.get_tags() 93 | for tag_item in tags: 94 | all_urls.add('/tag/{}/'.format(tag_item[0])) 95 | 96 | categories = storage.get_categories() 97 | for category_item in categories: 98 | all_urls.add('/category/{}/'.format(category_item[0])) 99 | 100 | with app.test_client() as client: 101 | # generate all possible urls 102 | for url in all_urls: 103 | resp = client.get(url) 104 | file_path = os.path.join(get_deploy_dir(), 105 | url.lstrip('/').replace('/', os.path.sep)) 106 | if url.endswith('/'): 107 | file_path += 'index.html' 108 | 109 | makedirs(os.path.dirname(file_path), mode=0o755, exist_ok=True) 110 | with open(file_path, 'wb') as f: 111 | f.write(resp.data) 112 | 113 | # generate 404 page 114 | resp = client.get('/post/this-is-a-page-that-never-gonna-exist' 115 | '-because-it-is-a-post-with-wrong-url-format/') 116 | with open(os.path.join(deploy_dir, '404.html'), 'wb') as f: 117 | f.write(resp.data) 118 | 119 | if app.config['STORAGE_TYPE'] == 'file': 120 | generate_pages_by_file() 121 | 122 | 123 | def generate_pages_by_file(): 124 | """Generates custom pages of 'file' storage type.""" 125 | from veripress import app 126 | from veripress.model import storage 127 | from veripress.model.parsers import get_standard_format_name 128 | from veripress.helpers import traverse_directory 129 | 130 | deploy_dir = get_deploy_dir() 131 | 132 | def copy_file(src, dst): 133 | makedirs(os.path.dirname(dst), mode=0o755, exist_ok=True) 134 | shutil.copyfile(src, dst) 135 | 136 | with app.app_context(), app.test_client() as client: 137 | root_path = os.path.join(app.instance_path, 'pages') 138 | for path in traverse_directory(root_path): 139 | # e.g. 'a/b/c/index.md' 140 | rel_path = os.path.relpath(path, root_path) 141 | # e.g. ('a/b/c/index', '.md') 142 | filename, ext = os.path.splitext(rel_path) 143 | if get_standard_format_name(ext[1:]) is not None: 144 | # is source of custom page 145 | rel_url = filename.replace(os.path.sep, '/') + '.html' 146 | page = storage.get_page(rel_url, include_draft=False) 147 | if page is not None: 148 | # it's not a draft, so generate the html page 149 | makedirs(os.path.join(deploy_dir, 150 | os.path.dirname(rel_path)), 151 | mode=0o755, exist_ok=True) 152 | with open(os.path.join(deploy_dir, filename + '.html'), 153 | 'wb') as f: 154 | f.write(client.get('/' + rel_url).data) 155 | if app.config['PAGE_SOURCE_ACCESSIBLE']: 156 | copy_file(path, os.path.join(deploy_dir, rel_path)) 157 | else: 158 | # is other direct files 159 | copy_file(path, os.path.join(deploy_dir, rel_path)) 160 | -------------------------------------------------------------------------------- /veripress/api/handlers.py: -------------------------------------------------------------------------------- 1 | import re 2 | from itertools import islice 3 | from datetime import date 4 | 5 | from flask import request, send_file 6 | 7 | from veripress import site, cache 8 | from veripress.api import ApiException, Error 9 | from veripress.model import storage 10 | from veripress.model.models import Base 11 | from veripress.model.parsers import get_parser 12 | from veripress.helpers import validate_custom_page_path, parse_toc 13 | 14 | 15 | @cache.memoize(timeout=5 * 60) 16 | def site_info(): 17 | return site 18 | 19 | 20 | def posts(year: int = None, month: int = None, day: int = None, 21 | post_name: str = None): 22 | args = {k: [x.strip() for x in v.split(',')] 23 | for k, v in request.args.items()} 24 | 25 | for key in ('include_draft', 'start', 'count'): 26 | # pop out items that should not be passed into the 'get_posts' method 27 | # as 'limits' 28 | args.pop(key, None) 29 | 30 | # fields that the API user needs, a list or None 31 | fields = args.pop('fields', None) 32 | 33 | for key in ('created', 'updated'): 34 | if key in args: 35 | try: 36 | interval = args[key] 37 | # should be ['2017-02-13', '2017-03-13'] if it's valid 38 | for i in range(2): 39 | y, m, d = re.match( 40 | '(\d{4})-(\d{1,2})-(\d{1,2})', interval[i]).groups() 41 | interval[i] = date(year=int(y), month=int(m), day=int(d)) 42 | except (IndexError, AttributeError, ValueError): 43 | raise ApiException( 44 | message='The "{}" argument is invalid, and it should be ' 45 | 'like "2017-02-13,2017-03-13".'.format(key), 46 | error=Error.INVALID_ARGUMENTS 47 | ) 48 | 49 | # get the post list here 50 | result_posts = storage.get_posts_with_limits(include_draft=False, **args) 51 | 52 | return_single_item = False 53 | rel_url_prefix = '' 54 | if year is not None: 55 | rel_url_prefix += '%04d/' % year 56 | if month is not None: 57 | rel_url_prefix += '%02d/' % month 58 | if day is not None: 59 | rel_url_prefix += '%02d/' % day 60 | if post_name is not None: 61 | rel_url_prefix += '%s/' % post_name 62 | # if a full relative url is given, we return just ONE post, 63 | # instead of a list 64 | return_single_item = True 65 | result_posts = filter(lambda p: p.rel_url.startswith(rel_url_prefix), 66 | result_posts) 67 | 68 | start = request.args.get('start', '') 69 | start = int(start) if start.isdigit() else 0 70 | count = request.args.get('count', '') 71 | count = int(count) if count.isdigit() else -1 72 | 73 | result_posts_list = [] 74 | for post in islice( 75 | result_posts, start, start + count if count >= 0 else None): 76 | parser = get_parser(post.format) 77 | post_d = post.to_dict() 78 | del post_d['raw_content'] 79 | if return_single_item: 80 | # if a certain ONE post is needed, 81 | # we parse all content instead of preview 82 | post_d['content'] = parser.parse_whole(post.raw_content) 83 | post_d['content'], post_d['toc'], post_d['toc_html'] = \ 84 | parse_toc(post_d['content']) 85 | else: 86 | # a list of posts is needed, we parse only previews 87 | post_d['preview'], post_d['has_more_content'] = \ 88 | parser.parse_preview(post.raw_content) 89 | if fields is not None: 90 | # select only needed fields to return 91 | assert isinstance(fields, list) 92 | full_post_d = post_d 93 | post_d = {} 94 | for key in fields: 95 | if key in full_post_d: 96 | post_d[key] = full_post_d[key] 97 | result_posts_list.append(post_d) 98 | 99 | if result_posts_list and return_single_item: 100 | return result_posts_list[0] 101 | else: 102 | return result_posts_list if result_posts_list else None 103 | 104 | 105 | def tags(): 106 | return [{'name': item[0], 'published': item[1].second} 107 | for item in storage.get_tags()] 108 | 109 | 110 | def categories(): 111 | return [{'name': item[0], 'published': item[1].second} 112 | for item in storage.get_categories()] 113 | 114 | 115 | def pages(page_path): 116 | if not validate_custom_page_path(page_path): 117 | raise ApiException( 118 | error=Error.NOT_ALLOWED, 119 | message='The visit of path "{}" is not allowed.'.format(page_path) 120 | ) 121 | 122 | rel_url, exists = storage.fix_relative_url('page', page_path) 123 | if exists: 124 | file_path = rel_url 125 | return send_file(file_path) 126 | elif rel_url is None: 127 | # pragma: no cover 128 | # it seems impossible to make this happen, 129 | # see code of 'fix_relative_url' 130 | raise ApiException( 131 | error=Error.BAD_PATH, 132 | message='The path "{}" cannot be recognized.'.format(page_path) 133 | ) 134 | else: 135 | page_d = cache.get('api-handler.' + rel_url) 136 | if page_d is not None: 137 | return page_d # pragma: no cover, here just get the cached dict 138 | 139 | page = storage.get_page(rel_url, include_draft=False) 140 | if page is None: 141 | raise ApiException(error=Error.RESOURCE_NOT_EXISTS) 142 | page_d = page.to_dict() 143 | del page_d['raw_content'] 144 | page_d['content'] = get_parser( 145 | page.format).parse_whole(page.raw_content) 146 | 147 | cache.set('api-handler.' + rel_url, page_d, timeout=2 * 60) 148 | return page_d 149 | 150 | 151 | @cache.cached(timeout=2 * 60) 152 | def widgets(): 153 | result_widgets = storage.get_widgets( 154 | position=request.args.get('position'), include_draft=False) 155 | result = [] 156 | for widget in result_widgets: 157 | widget_d = widget.to_dict() 158 | del widget_d['raw_content'] 159 | widget_d['content'] = get_parser( 160 | widget.format).parse_whole(widget.raw_content) 161 | result.append(widget_d) 162 | return result if result else None 163 | 164 | 165 | def search(): 166 | query = request.args.get('q', '').strip().lower() 167 | if not query: 168 | raise ApiException( 169 | error=Error.INVALID_ARGUMENTS, 170 | message='The "q" argument is missed or invalid.' 171 | ) 172 | 173 | start = request.args.get('start', '') 174 | start = int(start) if start.isdigit() else 0 175 | count = request.args.get('count', '') 176 | count = int(count) if count.isdigit() else -1 177 | 178 | def remove_raw_content_field(p): 179 | del p['raw_content'] 180 | return p 181 | 182 | result = list(islice(map(remove_raw_content_field, 183 | map(Base.to_dict, storage.search_for(query))), 184 | start, start + count if count >= 0 else None)) 185 | return result if result else None 186 | --------------------------------------------------------------------------------