├── .coveragerc ├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── dev_requirements.txt ├── hooks └── pre-commit ├── opt_requirements.txt ├── release.sh ├── requirements.txt ├── setup.cfg ├── setup.py ├── simiki ├── __init__.py ├── cli.py ├── compat.py ├── conf_templates │ ├── Dockerfile │ ├── _config.yml.in │ ├── fabfile.py │ └── gettingstarted.md ├── config.py ├── generators.py ├── initiator.py ├── jinja_exts.py ├── log.py ├── server.py ├── themes │ ├── simple │ │ ├── base.html │ │ ├── index.html │ │ ├── page.html │ │ ├── stat.html │ │ └── static │ │ │ └── css │ │ │ ├── style.css │ │ │ └── tango.css │ └── simple2 │ │ ├── base.html │ │ ├── index.html │ │ ├── page.html │ │ └── static │ │ └── css │ │ ├── style.css │ │ └── tango.css ├── updater.py ├── utils.py └── watcher.py ├── tests ├── __init__.py ├── mywiki_for_cli │ ├── attach │ │ └── images │ │ │ └── linux │ │ │ └── opstools.png │ └── content │ │ └── intro │ │ ├── gettingstarted.md │ │ └── my_draft.md ├── mywiki_for_generator │ ├── content │ │ └── foo目录 │ │ │ ├── foo_page.md │ │ │ ├── foo_page_get_meta_without_title.md │ │ │ ├── foo_page_get_meta_yaml_error.md │ │ │ ├── foo_page_layout_old_post.md │ │ │ ├── foo_page_layout_without_layout.md │ │ │ ├── foo_page_中文.md │ │ │ ├── foo_page_中文_meta_error_1.md │ │ │ └── foo_page_中文_meta_error_2.md │ ├── expected_catalog.html │ ├── expected_output.html │ ├── expected_pages.json │ └── expected_structure.json ├── mywiki_for_others │ ├── config_sample.yml │ └── content │ │ ├── python │ │ ├── python文档.md │ │ └── zen_of_python.md │ │ └── 其它 │ │ ├── .hidden.md │ │ ├── hello.txt │ │ ├── helloworld.markdown │ │ └── 维基.md ├── test_cli.py ├── test_generators.py ├── test_initiator.py ├── test_log.py ├── test_parse_config.py ├── test_updater.py └── test_utils.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = simiki 4 | 5 | [report] 6 | exclude_lines = 7 | pragma: no cover 8 | def __repr__ 9 | raise AssertionError 10 | raise NotImplementedError 11 | if __name__ == .__main__.: 12 | 13 | show_missing = True 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | htmlcov/ 28 | .tox 29 | nosetests.xml 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Custom 40 | *.swp 41 | venv/ 42 | .DS_Store 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | 9 | install: 10 | - "pip install -r requirements.txt" 11 | - "pip install coverage" 12 | 13 | script: nosetests -v --with-coverage --cover-package=simiki --cover-erase -s 14 | 15 | after_success: 16 | - pip install coveralls 17 | - coveralls 18 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | v1.6.2.3 (2019-05-11) 2 | ===================== 3 | - Fix Issue #124 4 | 5 | 6 | v1.6.2.2 (2019-04-21) 7 | ===================== 8 | - Fix PyYAML CVE-2017-18342 9 | - Fix Jinja2 CVE-2019-10906 10 | 11 | 12 | v1.6.2.1 (2017-06-04) 13 | ===================== 14 | - Fix preview not work in py3 15 | 16 | 17 | v1.6.2 (2017-06-02) 18 | ===================== 19 | - Fix issue #88, map url path to local file system path and fix root url with non-ascii 20 | - Fix long subcommand not work bug 21 | - Fix preview with temporary files generated by editor 22 | - Fix fabfile with py3 23 | - Update theme simple2 24 | - Add Dockerfile 25 | - Support Python3.6, remove Python2.6 26 | - Support custom markdown extensions configuration, `doc `_ 27 | - Enable nl2br(newline to line break) markdown extension by default 28 | 29 | 30 | v1.6.0.1 (2016-06-30) 31 | ===================== 32 | - Fix issue #64 with python3.x 33 | 34 | 35 | v1.6.0 (2016-06-09) 36 | ===================== 37 | - Fix issue #60, preview with 127.0.0.1 38 | - Add ``page`` variable, `doc `_ 39 | - Add ``category`` settings in _config.yml 40 | - Add ``collection`` and ``tag``, a three-tier structure, `doc `_ 41 | - Add new theme simple2, with collection/tag support 42 | 43 | 44 | v1.5.1 (2016-04-09) 45 | ===================== 46 | - Fix ``fab deploy`` find ghp-import command error, pr #51 47 | - Fix write error when draft: true in watcher 48 | - Improve ``fab commit``, issue #52 49 | - Improve generate speed 50 | 51 | * template cache 52 | * regex match page meta 53 | 54 | - Add favicon.ico support, issue #53 55 | - Add deprecated warning for post layout 56 | - Add ``--draft`` option to force the generation include draft pages 57 | - Support Python3.5 58 | 59 | 60 | v1.5.0-1 (2016-01-23) 61 | ===================== 62 | 63 | - Improve generate speed by multiple processes 64 | - Fix CNAME be deleted when do generate problem 65 | - Update fabfile(for Fabric) 66 | 67 | * Remove ``update_simiki``, ``g``, ``p``, ``gp`` subcommands 68 | * Add ``git`` and ``ftp`` deployment support 69 | * Move deploy configuration to _config.yml 70 | * Add ``commit`` subcommand to quick commit task 71 | 72 | - Add ``-w`` option. Watch content directory and auto do generate if modified 73 | - Add ``update`` subcommand to update builtin scripts and themes 74 | - add Atom.xml support (not stable), add ``rfc3339`` custom filter 75 | - Add new site/page variable ``version``, meta variable ``render`` 76 | - Add Python3.3 and Python3.4 support 77 | 78 | 79 | v1.4.1 (2015-08-28) 80 | =================== 81 | 82 | 1. Make if single page generate failed, continue going with others, not exit 83 | 2. Fix --init unicode problem 84 | 3. Improve logging message 85 | 4. Fix #41, empty output dir exclude .git and CNAME 86 | 87 | 88 | v1.4 (2015-08-12) 89 | =================== 90 | 91 | 1. Support draft in meta 92 | 2. Fix server prompt and relative url error 93 | 3. Remove unused '--ignore-root' option 94 | 4. Redirect all pages instead only index page 95 | 5. Support '--host/--port' option in preview 96 | 97 | 98 | v1.3 (2015-03-04) 99 | =================== 100 | 101 | 1. Add ``site.time`` variable, the generated time. 102 | 2. Improve encoding 103 | 3. Add ``--update-them`` when generate to improve generation speed 104 | 4. Fix #36, add attach directory to put attachments. 105 | 5. Fix #33, only show color logging message on Linux/MacOS 106 | 107 | 108 | v1.2.4 (2014-12-23) 109 | =================== 110 | 111 | * Fix #31 encode/decode problems 112 | * Fix image overflow in simple themes 113 | 114 | 115 | v1.2.3 (2014-09-15) 116 | =================== 117 | 118 | * Fix #28 add '--ignore-root' option in generate mode 119 | * Fix CustomCatalogGenerator arguments number error 120 | 121 | 122 | v1.2.2 (2014-08-22) 123 | =================== 124 | 125 | * Fix #26 Universal newline in open file 126 | * Fix #27 Fix extension is not md 127 | 128 | 129 | v1.2.1 (2014-07-13) 130 | =================== 131 | 132 | * Fix #25 unicode problem when path contains Chinese 133 | 134 | 135 | v1.2 (2014-07-06) 136 | =================== 137 | 138 | * Support Python2.6 139 | * Fix: init site with specific path 140 | 141 | 142 | v1.1 (2014-07-04) 143 | =================== 144 | 145 | * Template support multiple level catalog in Index 146 | * Sort index structure in lower-case, alphabetical order 147 | * Fix error on nav in wiki page 148 | * Fix unicode in emptytree 149 | * Fix #16 set literals not support in Python < 2.7 150 | 151 | 152 | v1.0.3 (2014-06-10) 153 | =================== 154 | 155 | * Fix #14 Chinese filename problem 156 | 157 | 158 | v1.0.2 (2014-06-10) 159 | =================== 160 | 161 | * Fix #13 system path separator problem on Windows 162 | 163 | 164 | v1.0.1 (2014-06-10) 165 | =================== 166 | 167 | * Fix serious problem using rsync 168 | 169 | 170 | v1.0.0 (2014-05-28) 171 | =================== 172 | 173 | * Support Chinese directory and file name 174 | * Simplify the default configuration file 175 | * Add introduction page when init site 176 | * Fix some bugs 177 | 178 | v0.5.0 (2014-04-30) 179 | =================== 180 | 181 | * fabric: rsync output to remote server 182 | * fabric: remote update simiki 183 | * get fabfile when init site 184 | * fix: do not overwrite _config.yml while init site again 185 | 186 | v0.4.1 (2014-04-28) 187 | =================== 188 | 189 | * change font-family and code highlight class to hlcode 190 | * fix bug: not use the right code highlight css file in base.html 191 | * default theme change pygments style from autumn to tango 192 | * remove repetitive index setting in config template 193 | 194 | 195 | v0.4.0 (2014-04-20) 196 | =================== 197 | 198 | * Add custom index feature 199 | 200 | 201 | v0.3.1 (2014-04-13) 202 | =================== 203 | 204 | * Fixed "socket.error: [Errno 48] Address already in use" problem in preview 205 | 206 | 207 | v0.3.0 (2014-04-06) 208 | =================== 209 | 210 | * Move all static files to static/ dir in theme 211 | * Fixed #4 Reinstall theme every generate action 212 | * Fixed #1 add generate option to empty output directory first 213 | 214 | 215 | v0.2.2 (2014-03-29) 216 | =================== 217 | 218 | * Fixed #5 css path error when there is no wiki 219 | * Fixed #6 ignore hidden dirs and files when use os.walk or os.listdir 220 | * support table of contents(toc) both generator and default theme 221 | 222 | v0.2.1 (2014-03-23) 223 | =================== 224 | 225 | * Change catalog order from date to title letter 226 | 227 | 228 | v0.2.0 (2014-03-19) 229 | =================== 230 | 231 | * Simplify _config.yml and add debug mode 232 | * Put themes under wiki directory 233 | * Add root url function 234 | * Add statistic in default theme 235 | * Change default theme style 236 | * Use log instead of print 237 | 238 | 239 | v0.1.0 (2013-12-8) 240 | ================== 241 | 242 | * Initial release. 243 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contribution ## 2 | 3 | Your contributions are always welcome! 4 | 5 | Sending pull requests on [Pull Requests Page](https://github.com/tankywoo/simiki/pulls) is the preferred method for receiving contributions. 6 | 7 | * Bug fixes can be based on **`master`** branch and I will also merge into `dev` branch. 8 | * Feature can be based on **`dev`** branch. 9 | 10 | Following links are the contribution guidelines you may need: 11 | 12 | * [Fork A Repo](https://help.github.com/articles/fork-a-repo/) 13 | * [Contributing to Processing with Pull Requests](https://github.com/processing/processing/wiki/Contributing-to-Processing-with-Pull-Requests) 14 | 15 | Thanks to every [contributor](https://github.com/tankywoo/simiki/graphs/contributors). 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Tanky Woo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include CHANGELOG.rst 3 | include LICENSE 4 | include *requirements.txt 5 | include tox.ini 6 | recursive-include simiki *.html *.css *.in *.md *.py Dockerfile 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | HTMLCOV="htmlcov/" 2 | 3 | help: 4 | @echo "Please use \`make ' where is one of" 5 | @echo " tox to run test by tox" 6 | @echo " test to run test by nosetests with coverage" 7 | @echo " covhtml to run test by nosetests and create html report" 8 | @echo " clean to clean temporary files without .tox" 9 | @echo " cleanall to clean temporary files with .tox" 10 | 11 | tox: clean 12 | tox 13 | 14 | test: clean 15 | nosetests -v --no-byte-compile --with-coverage --cover-package=simiki --cover-erase -s 16 | flake8 --version 17 | flake8 simiki/ tests/ 18 | 19 | covhtml: clean 20 | # coverage run setup.py nosetests 21 | nosetests # arguments already configured in setup.cfg 22 | coverage html 23 | cd ${HTMLCOV} && python -m SimpleHTTPServer 24 | 25 | cleanall: clean 26 | rm -rf .tox 27 | 28 | clean: 29 | coverage erase 30 | python setup.py clean 31 | find simiki/ -name '*.pyc' -delete 32 | find tests/ -name '*.pyc' -delete 33 | rm -rf htmlcov 34 | rm -rf build dist simiki.egg-info 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simiki # 2 | 3 | [![Latest Version](http://img.shields.io/pypi/v/simiki.svg)](https://pypi.python.org/pypi/simiki) 4 | [![The MIT License](http://img.shields.io/badge/license-MIT-yellow.svg)](https://github.com/tankywoo/simiki/blob/master/LICENSE) 5 | [![Build Status](https://travis-ci.org/tankywoo/simiki.svg)](https://travis-ci.org/tankywoo/simiki) 6 | [![Coverage Status](https://img.shields.io/coveralls/tankywoo/simiki.svg)](https://coveralls.io/r/tankywoo/simiki) 7 | 8 | Simiki is a simple wiki framework, written in [Python](https://www.python.org/). 9 | 10 | * Easy to use. Creating a wiki only needs a few steps 11 | * Use [Markdown](http://daringfireball.net/projects/markdown/). Just open your editor and write 12 | * Store source files by category 13 | * Static HTML output 14 | * A CLI tool to manage the wiki 15 | 16 | Simiki is short for `Simple Wiki` :) 17 | 18 | > New in version 1.6.2.3 (2019-05-11) 19 | > 20 | > - Fix Issue #124 21 | > 22 | > 23 | > New in version 1.6.2.2 (2019-04-21) 24 | > 25 | > - Fix PyYAML CVE-2017-18342 26 | > - Fix Jinja2 CVE-2019-10906 27 | > 28 | > 29 | > New in version 1.6.2.1 (2017-06-04) 30 | > 31 | > - Fix preview not work in py3 32 | 33 | 34 | ## Installation ## 35 | 36 | It is available for **Python 2.7, 3.3, 3.4, 3.5, 3.6**, with Linux, Mac OS X and Windows. 37 | 38 | Install from [PyPI](https://pypi.python.org/pypi/simiki): 39 | 40 | pip install simiki 41 | 42 | Update: 43 | 44 | pip install -U simiki 45 | 46 | 47 | ## Quick Start ## 48 | 49 | ### Init Site ### 50 | 51 | mkdir mywiki && cd mywiki 52 | simiki init 53 | 54 | ### Generate ### 55 | 56 | simiki g 57 | 58 | ### Preview ### 59 | 60 | simiki p 61 | 62 | For more information, `simiki -h` or have a look at [Simiki.org](http://simiki.org) 63 | 64 | 65 | ## Others ## 66 | 67 | * [simiki.org](http://simiki.org) 68 | * 69 | * Email: 70 | * [Simiki Users](https://github.com/tankywoo/simiki/wiki/Simiki-Users) 71 | 72 | 73 | ## Contribution ## 74 | 75 | Your contributions are always welcome! 76 | 77 | Sending pull requests on [Pull Requests Page](https://github.com/tankywoo/simiki/pulls) is the preferred method for receiving contributions. 78 | 79 | * Bug fixes can be based on **`master`** branch and I will also merge into `dev` branch. 80 | * Feature can be based on **`dev`** branch. 81 | 82 | Following links are the contribution guidelines you may need: 83 | 84 | * [Fork A Repo](https://help.github.com/articles/fork-a-repo/) 85 | * [Contributing to Processing with Pull Requests](https://github.com/processing/processing/wiki/Contributing-to-Processing-with-Pull-Requests) 86 | 87 | Thanks to every [contributor](https://github.com/tankywoo/simiki/graphs/contributors). 88 | 89 | 90 | ## License ## 91 | 92 | The MIT License (MIT) 93 | 94 | Copyright (c) 2013 Tanky Woo 95 | 96 | Permission is hereby granted, free of charge, to any person obtaining a copy of 97 | this software and associated documentation files (the "Software"), to deal in 98 | the Software without restriction, including without limitation the rights to 99 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 100 | the Software, and to permit persons to whom the Software is furnished to do so, 101 | subject to the following conditions: 102 | 103 | The above copyright notice and this permission notice shall be included in all 104 | copies or substantial portions of the Software. 105 | 106 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 107 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 108 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 109 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 110 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 111 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 112 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | nose==1.3.3 2 | coverage==3.7.1 3 | tox==2.3.1 4 | mock==1.3.0 5 | flake8>=2.5.4 6 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Run unittest before commit 4 | 5 | make test >/dev/null 2>&1 6 | unittest=$? 7 | 8 | if [[ ${unittest} != 0 ]]; then 9 | cat <<\EOF 10 | Error: Unit Testing Failed!!! 11 | EOF 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /opt_requirements.txt: -------------------------------------------------------------------------------- 1 | Fabric==1.10.2 2 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DO_TEST=false 4 | 5 | while [[ $# > 0 ]]; do 6 | case "$1" in 7 | -t | --test) DO_TEST=true; shift;; 8 | --) shift; break;; 9 | *) echo "$0 with wrong args!"; exit 1;; 10 | esac 11 | done 12 | 13 | README_MD="README.md" 14 | README_RST="README.rst" 15 | 16 | if [ "$DO_TEST" = true ]; then 17 | INDEX='testpypi' # defined in ~/.pypirc index-servers 18 | else 19 | INDEX='pypi' 20 | fi 21 | 22 | # Convert README.md to README.rst using pandoc 23 | if [ `which pandoc` ]; then 24 | pandoc -f markdown -t rst -o ${README_RST} ${README_MD} && echo "Convert md to rst ok." 25 | else 26 | echo "pandoc not installed." 27 | fi 28 | 29 | # Release 30 | read -p "Release [$INDEX]? (y/n) " RESP 31 | if [ "$RESP" = "y" ]; then 32 | echo "Begin to release" 33 | python setup.py release -r $INDEX 34 | else 35 | echo "Cancel to release" 36 | fi 37 | 38 | # Post process 39 | rm ${README_RST} 40 | rm -rf build dist simiki.egg-info 41 | 42 | if [ "$DO_TEST" = false ]; then 43 | # Add tag to HEAD 44 | version=`python -m simiki.cli --version | awk {'printf $2'}` 45 | git tag v${version} 46 | fi 47 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Markdown==2.6.8 2 | Pygments==1.6 3 | Jinja2>=2.10.1 4 | PyYAML==5.1 5 | docopt==0.6.1 6 | ordereddict==1.1 7 | watchdog==0.8.3 8 | pytz==2015.7 9 | tzlocal==1.2 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | release = sdist bdist_egg register upload 3 | 4 | [nosetests] 5 | verbosity=2 6 | no-byte-compile=1 7 | with-coverage=1 8 | cover-package=simiki 9 | cover-erase=1 10 | nocapture=1 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import with_statement 3 | import os 4 | import io 5 | from setuptools import setup, find_packages 6 | import simiki 7 | 8 | 9 | entry_points = { 10 | "console_scripts": [ 11 | "simiki = simiki.cli:main", 12 | ] 13 | } 14 | 15 | with io.open("requirements.txt", "rt", encoding="utf-8") as f: 16 | requires = [l for l in f.read().splitlines() if l] 17 | 18 | readme = "README.md" 19 | if os.path.exists("README.rst"): 20 | readme = "README.rst" 21 | with io.open(readme, "rt", encoding="utf-8") as f: 22 | long_description = f.read() 23 | 24 | 25 | setup( 26 | name="simiki", 27 | version=simiki.__version__, 28 | url="http://simiki.org/", 29 | author="Tanky Woo", 30 | author_email="me@tankywoo.com", 31 | description="Simiki is a simple wiki framework, written in Python.", 32 | long_description=long_description, 33 | keywords="simiki, wiki, generator", 34 | license="MIT License", 35 | packages=find_packages(), 36 | include_package_data=True, 37 | install_requires=requires, 38 | entry_points=entry_points, 39 | classifiers=[ 40 | 'Development Status :: 4 - Beta', 41 | 'Environment :: Console', 42 | 'License :: OSI Approved :: MIT License', 43 | 'Operating System :: MacOS', 44 | 'Operating System :: POSIX', 45 | 'Operating System :: POSIX :: Linux', 46 | 'Operating System :: Microsoft :: Windows', 47 | 'Programming Language :: Python', 48 | 'Programming Language :: Python :: 2', 49 | 'Programming Language :: Python :: 2.7', 50 | 'Programming Language :: Python :: 3', 51 | 'Programming Language :: Python :: 3.3', 52 | 'Programming Language :: Python :: 3.4', 53 | 'Programming Language :: Python :: 3.5', 54 | 'Programming Language :: Python :: 3.6', 55 | ], 56 | tests_require=['nose', 'mock'], 57 | test_suite='nose.collector', 58 | ) 59 | -------------------------------------------------------------------------------- /simiki/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __version__ = "1.6.2.3" 5 | 6 | allowed_extensions = ("md", "mkd", "mdown", "markdown") 7 | -------------------------------------------------------------------------------- /simiki/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Simiki CLI 6 | 7 | Usage: 8 | simiki init [-p ] 9 | simiki (new | n) -t -c <category> [-f <file>] 10 | simiki (generate | g) [--draft] 11 | simiki (preview | p) [--host <host>] [--port <port>] [-w] 12 | simiki update 13 | simiki -h | --help 14 | simiki -V | --version 15 | 16 | Subcommands: 17 | init Initial site 18 | new Create a new wiki page 19 | generate Generate site 20 | preview Preview site locally (develop mode) 21 | update Update builtin scripts and themes under local site 22 | 23 | Options: 24 | -h, --help Help information 25 | -V, --version Show version 26 | -p <path> Specify the target path 27 | -c <category> Specify the category 28 | -t <title> Specify the new post title 29 | -f <file> Specify the new post filename 30 | --host <host> Bind host to preview [default: 127.0.0.1] 31 | --port <port> Bind port to preview [default: 8000] 32 | -w Auto regenerated when file changed 33 | --draft Include draft pages to generate 34 | """ 35 | 36 | from __future__ import print_function, unicode_literals, absolute_import 37 | 38 | import os 39 | import os.path 40 | import sys 41 | import io 42 | import datetime 43 | import shutil 44 | import logging 45 | import random 46 | import multiprocessing 47 | import time 48 | import warnings 49 | 50 | from docopt import docopt 51 | from yaml import YAMLError 52 | 53 | from simiki.generators import (PageGenerator, CatalogGenerator, FeedGenerator) 54 | from simiki.initiator import Initiator 55 | from simiki.config import parse_config 56 | from simiki.log import logging_init 57 | from simiki.server import preview 58 | from simiki.watcher import watch 59 | from simiki.updater import update_builtin 60 | from simiki.utils import (copytree, emptytree, mkdir_p, write_file) 61 | from simiki.compat import unicode, basestring, xrange 62 | from simiki import __version__ 63 | 64 | try: 65 | from os import getcwdu 66 | except ImportError: 67 | from os import getcwd as getcwdu 68 | 69 | # Enable DeprecationWarning, etc. 70 | warnings.simplefilter('default') 71 | 72 | logger = logging.getLogger(__name__) 73 | config = None 74 | 75 | 76 | def init_site(target_path): 77 | default_config_file = os.path.join(os.path.dirname(__file__), 78 | "conf_templates", 79 | "_config.yml.in") 80 | try: 81 | initiator = Initiator(default_config_file, target_path) 82 | if os.environ.get('TEST_MODE'): 83 | initiator.init(ask=False) 84 | else: 85 | initiator.init(ask=True) 86 | except Exception: 87 | # always in debug mode when init site 88 | logging.exception("Initialize site with error:") 89 | sys.exit(1) 90 | 91 | 92 | def create_new_wiki(category, title, filename): 93 | if not filename: 94 | # `/` can't exists in filename 95 | _title = title.replace(os.sep, " slash ").lower() 96 | filename = "{0}.{1}".format(_title.replace(' ', '-'), 97 | config["default_ext"]) 98 | now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") 99 | 100 | meta = "\n".join([ 101 | "---", 102 | "title: \"{0}\"".format(title), 103 | "date: {0}".format(now), 104 | "---", 105 | ]) + "\n\n" 106 | 107 | category_path = os.path.join(config["source"], category) 108 | if not os.path.exists(category_path): 109 | mkdir_p(category_path) 110 | logger.info("Creating category: {0}.".format(category)) 111 | 112 | fn = os.path.join(category_path, filename) 113 | if os.path.exists(fn): 114 | logger.warning("File exists: {0}".format(fn)) 115 | else: 116 | logger.info("Creating wiki: {0}".format(fn)) 117 | with io.open(fn, "wt", encoding="utf-8") as fd: 118 | fd.write(meta) 119 | 120 | 121 | def preview_site(host, port, dest, root, do_watch): 122 | """Preview site with watch content""" 123 | p_server = multiprocessing.Process( 124 | target=preview, 125 | args=(dest, root, host, port), 126 | name='ServerProcess' 127 | ) 128 | p_server.start() 129 | 130 | if do_watch: 131 | base_path = getcwdu() 132 | p_watcher = multiprocessing.Process( 133 | target=watch, 134 | args=(config, base_path), 135 | name='WatcherProcess' 136 | ) 137 | p_watcher.start() 138 | 139 | try: 140 | while p_server.is_alive(): 141 | time.sleep(1) 142 | else: 143 | if do_watch: 144 | p_watcher.terminate() 145 | except (KeyboardInterrupt, SystemExit): 146 | # manually terminate process? 147 | pass 148 | 149 | 150 | def method_proxy(cls_instance, method_name, *args, **kwargs): 151 | # ref: http://stackoverflow.com/a/10217089/1276501 152 | return getattr(cls_instance, method_name)(*args, **kwargs) 153 | 154 | 155 | class Generator(object): 156 | 157 | def __init__(self, target_path): 158 | self.config = config 159 | self.config.update({'version': __version__}) 160 | self.target_path = target_path 161 | self.tags = {} 162 | self.pages = {} 163 | self.page_count = 0 164 | self.draft_count = 0 165 | self.include_draft = False 166 | 167 | def generate(self, include_draft=False): 168 | """ 169 | :include_draft: True/False, include draft pages or not to generate. 170 | """ 171 | self.include_draft = include_draft 172 | 173 | logger.debug("Empty the destination directory") 174 | dest_dir = os.path.join(self.target_path, 175 | self.config["destination"]) 176 | if os.path.exists(dest_dir): 177 | # for github pages and favicon.ico 178 | exclude_list = ['.git', 'CNAME', 'favicon.ico'] 179 | emptytree(dest_dir, exclude_list) 180 | 181 | self.generate_tags() 182 | 183 | self.generate_pages() 184 | 185 | if not os.path.exists(os.path.join(self.config['source'], 'index.md')): 186 | self.generate_catalog(self.pages) 187 | 188 | feed_fn = 'atom.xml' 189 | if os.path.exists(os.path.join(getcwdu(), feed_fn)): 190 | self.generate_feed(self.pages, feed_fn) 191 | 192 | self.install_theme() 193 | 194 | self.copy_attach() 195 | 196 | # for default supported files to be copied to output/ 197 | # CNAME for github pages with custom domain 198 | # TODO favicon can be other formats, such as .png, use glob match? 199 | for _fn in ('CNAME', 'favicon.ico'): 200 | _file = os.path.join(getcwdu(), _fn) 201 | if os.path.exists(_file): 202 | shutil.copy2(_file, 203 | os.path.join(self.config['destination'], _fn)) 204 | 205 | def generate_tags(self): 206 | g = PageGenerator(self.config, self.target_path) 207 | 208 | for root, dirs, files in os.walk(self.config["source"]): 209 | files = [f for f in files if not f.startswith(".")] 210 | dirs[:] = [d for d in dirs if not d.startswith(".")] 211 | for filename in files: 212 | if not filename.endswith(self.config["default_ext"]): 213 | continue 214 | md_file = os.path.join(root, filename) 215 | 216 | g.src_file = md_file 217 | meta, _ = g.get_meta_and_content(do_render=False) 218 | _tags = meta.get('tag') or [] # if None 219 | for t in _tags: 220 | self.tags.setdefault(t, []).append(meta) 221 | 222 | def generate_feed(self, pages, feed_fn): 223 | logger.info("Generate feed.") 224 | feed_generator = FeedGenerator(self.config, self.target_path, pages, 225 | feed_fn) 226 | feed = feed_generator.generate_feed() 227 | ofile = os.path.join( 228 | self.target_path, 229 | self.config["destination"], 230 | feed_fn 231 | ) 232 | write_file(ofile, feed) 233 | 234 | def generate_catalog(self, pages): 235 | logger.info("Generate catalog page.") 236 | catalog_generator = CatalogGenerator(self.config, self.target_path, 237 | pages) 238 | html = catalog_generator.generate_catalog_html() 239 | ofile = os.path.join( 240 | self.target_path, 241 | self.config["destination"], 242 | "index.html" 243 | ) 244 | write_file(ofile, html) 245 | 246 | def generate_pages(self): 247 | logger.info("Start generating markdown files.") 248 | content_path = self.config["source"] 249 | _pages_l = [] 250 | 251 | for root, dirs, files in os.walk(content_path): 252 | files = [f for f in files if not f.startswith(".")] 253 | dirs[:] = [d for d in dirs if not d.startswith(".")] 254 | for filename in files: 255 | if not filename.endswith(self.config["default_ext"]): 256 | continue 257 | md_file = os.path.join(root, filename) 258 | _pages_l.append(md_file) 259 | 260 | npage = len(_pages_l) 261 | if npage: 262 | nproc = min(multiprocessing.cpu_count(), npage) 263 | 264 | split_pages = [[] for n in xrange(0, nproc)] 265 | random.shuffle(_pages_l) 266 | 267 | for i in xrange(npage): 268 | split_pages[i % nproc].append(_pages_l[i]) 269 | 270 | pool = multiprocessing.Pool(processes=nproc) 271 | results = [] 272 | for n in xrange(nproc): 273 | r = pool.apply_async( 274 | method_proxy, 275 | (self, 'generate_multiple_pages', split_pages[n]), 276 | callback=self._generate_callback 277 | ) 278 | results.append(r) 279 | 280 | pool.close() 281 | for r in results: 282 | r.get() 283 | 284 | generate_result = ["Generate {0} pages".format(self.page_count)] 285 | # for draft pages and failed pages 286 | _err_npage = npage - self.page_count 287 | if self.include_draft: 288 | generate_result.append("include {0} drafts" 289 | .format(self.draft_count)) 290 | else: 291 | _err_npage -= self.draft_count 292 | generate_result.append("ignore {0} drafts" 293 | .format(self.draft_count)) 294 | if _err_npage: 295 | generate_result.append(" {0} pages failed".format(_err_npage)) 296 | logger.info(', '.join(generate_result) + '.') 297 | 298 | def generate_multiple_pages(self, md_files): 299 | _pages = {} 300 | _page_count = 0 301 | _draft_count = 0 302 | page_generator = PageGenerator(self.config, self.target_path, 303 | self.tags) 304 | for _f in md_files: 305 | try: 306 | page_meta = self.generate_single_page(page_generator, _f) 307 | if page_meta: 308 | _pages[_f] = page_meta 309 | _page_count += 1 310 | if page_meta.get('draft'): 311 | _draft_count += 1 312 | else: 313 | # XXX suppose page as draft if page_meta is None, this may 314 | # cause error in the future 315 | _draft_count += 1 316 | except Exception: 317 | page_meta = None 318 | logger.exception('{0} failed to generate:'.format(_f)) 319 | return _pages, _page_count, _draft_count 320 | 321 | def generate_single_page(self, generator, md_file): 322 | logger.debug("Generate: {0}".format(md_file)) 323 | html = generator.to_html(os.path.realpath(md_file), 324 | self.include_draft) 325 | 326 | # ignore draft 327 | if not html: 328 | return None 329 | 330 | category, filename = os.path.split(md_file) 331 | category = os.path.relpath(category, self.config['source']) 332 | output_file = os.path.join( 333 | self.target_path, 334 | self.config['destination'], 335 | category, 336 | '{0}.html'.format(os.path.splitext(filename)[0]) 337 | ) 338 | 339 | write_file(output_file, html) 340 | meta = generator.meta 341 | meta['content'] = generator.content # TODO 342 | return meta 343 | 344 | def _generate_callback(self, result): 345 | _pages, _page_count, _draft_count = result 346 | self.pages.update(_pages) 347 | self.page_count += _page_count 348 | self.draft_count += _draft_count 349 | 350 | def install_theme(self): 351 | """Copy static directory under theme to destination directory""" 352 | src_theme = os.path.join(self.target_path, self.config["themes_dir"], 353 | self.config["theme"], "static") 354 | dest_theme = os.path.join(self.target_path, self.config["destination"], 355 | "static") 356 | if os.path.exists(dest_theme): 357 | shutil.rmtree(dest_theme) 358 | 359 | copytree(src_theme, dest_theme) 360 | logging.debug("Installing theme: {0}".format(self.config["theme"])) 361 | 362 | def copy_attach(self): 363 | """Copy attach directory under root path to destination directory""" 364 | src_p = os.path.join(self.target_path, self.config['attach']) 365 | dest_p = os.path.join(self.target_path, self.config["destination"], 366 | 'attach') 367 | if os.path.exists(src_p): 368 | copytree(src_p, dest_p) 369 | 370 | 371 | def unicode_docopt(args): 372 | for k in args: 373 | if isinstance(args[k], basestring) and \ 374 | not isinstance(args[k], unicode): 375 | args[k] = args[k].decode('utf-8') 376 | 377 | 378 | def main(args=None): 379 | global config 380 | 381 | if not args: 382 | args = docopt(__doc__, version="Simiki {0}".format(__version__)) 383 | unicode_docopt(args) 384 | 385 | logging_init(logging.DEBUG) 386 | 387 | target_path = args['-p'] if args['-p'] else getcwdu() 388 | 389 | if args["init"]: 390 | init_site(target_path) 391 | else: 392 | config_file = os.path.join(target_path, "_config.yml") 393 | try: 394 | config = parse_config(config_file) 395 | except (Exception, YAMLError): 396 | # always in debug mode when parse config 397 | logging.exception("Parse config with error:") 398 | sys.exit(1) 399 | level = logging.DEBUG if config["debug"] else logging.INFO 400 | logging_init(level) # reload logger 401 | 402 | if args["generate"] or args["g"]: 403 | generator = Generator(target_path) 404 | generator.generate(include_draft=args['--draft']) 405 | elif args["new"] or args["n"]: 406 | create_new_wiki(args["-c"], args["-t"], args["-f"]) 407 | elif args["preview"] or args["p"]: 408 | args['--port'] = int(args['--port']) 409 | preview_site(args['--host'], args['--port'], config['destination'], 410 | config['root'], args['-w']) 411 | elif args["update"]: 412 | update_builtin(themes_dir=config['themes_dir']) 413 | else: 414 | # docopt itself will display the help info. 415 | pass 416 | 417 | logger.info("Done.") 418 | 419 | 420 | if __name__ == "__main__": 421 | main() 422 | -------------------------------------------------------------------------------- /simiki/compat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Python compat for python version and os system 5 | """ 6 | import sys 7 | 8 | # Syntax sugar. 9 | _ver = sys.version_info 10 | 11 | #: Python 2.x? 12 | is_py2 = (_ver[0] == 2) 13 | 14 | #: Python 3.x? 15 | is_py3 = (_ver[0] == 3) 16 | 17 | 18 | _platform = sys.platform 19 | 20 | # Windows 21 | is_windows = _platform.startswith('win32') 22 | 23 | # Linux 24 | is_linux = _platform.startswith('linux') 25 | 26 | # Mac OS X 27 | is_osx = _platform.startswith('darwin') 28 | 29 | # TODO Windows/Cygwin for 'cygwin'? 30 | 31 | 32 | # Specifics 33 | 34 | if is_py2: 35 | # flake8 raise F821 for py3 as unicode, bashstring, ... not exists 36 | unicode = unicode # noqa: F821 37 | basestring = basestring # noqa: F821 38 | xrange = xrange # noqa: F821 39 | raw_input = raw_input # noqa: F821 40 | 41 | if is_py3: 42 | unicode = str 43 | basestring = (str, bytes) 44 | xrange = range 45 | raw_input = input 46 | -------------------------------------------------------------------------------- /simiki/conf_templates/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7.11 2 | 3 | WORKDIR /src 4 | 5 | COPY . /src 6 | RUN pip install simiki 7 | RUN simiki g 8 | 9 | CMD ["simiki", "p", "-w", "--host", "0.0.0.0", "--port", "8000"] 10 | 11 | EXPOSE 8000 12 | -------------------------------------------------------------------------------- /simiki/conf_templates/_config.yml.in: -------------------------------------------------------------------------------- 1 | # Document: 2 | # http://simiki.org/docs/configuration.html 3 | 4 | url: 5 | title: 6 | keywords: 7 | description: 8 | author: 9 | -------------------------------------------------------------------------------- /simiki/conf_templates/fabfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function, absolute_import, with_statement 4 | 5 | import os 6 | import sys 7 | import ftplib 8 | import getpass 9 | from fabric.api import env, local, task, settings 10 | from fabric.colors import blue, red 11 | import fabric.contrib.project as project 12 | from simiki import config 13 | from simiki.compat import raw_input 14 | 15 | # XXX must run fab in root path of wiki 16 | configs = config.parse_config('_config.yml') 17 | 18 | env.colorize_errors = True 19 | SUPPORTED_DEPLOY_TYPES = ('rsync', 'git', 'ftp') 20 | 21 | 22 | def do_exit(msg): 23 | print(red(msg)) 24 | print(blue('Exit!')) 25 | sys.exit() 26 | 27 | 28 | def get_rsync_configs(): 29 | if 'deploy' in configs: 30 | for item in configs['deploy']: 31 | if item['type'] == 'rsync': 32 | return item 33 | return None 34 | 35 | 36 | # cannot put this block in deploy_rsync() for env.hosts 37 | rsync_configs = get_rsync_configs() 38 | if rsync_configs: 39 | env.user = rsync_configs.get('user', 'root') 40 | # Remote host and username 41 | if 'host' not in rsync_configs: 42 | do_exit('Warning: rsync host not set in _config.yml!') 43 | env.hosts = [rsync_configs['host'], ] 44 | 45 | # Local output path 46 | env.local_output = os.path.join( 47 | os.path.abspath(os.path.dirname(__file__)), 48 | configs['destination']) 49 | 50 | # Remote path to deploy output 51 | if 'dir' not in rsync_configs: 52 | do_exit('Warning: rsync dir not set in _config.yml!') 53 | env.remote_output = rsync_configs['dir'] 54 | 55 | # Other options 56 | env.port = rsync_configs.get('port') 57 | env.rsync_delete = rsync_configs.get('delete', False) 58 | 59 | 60 | def deploy_rsync(deploy_configs): 61 | """for rsync""" 62 | project.rsync_project( 63 | local_dir=env.local_output.rstrip("/") + "/", 64 | remote_dir=env.remote_output.rstrip("/") + "/", 65 | delete=env.rsync_delete 66 | ) 67 | 68 | 69 | def deploy_git(deploy_configs): 70 | """for pages service of such as github/gitcafe ...""" 71 | with settings(warn_only=True): 72 | res = local('which ghp-import > /dev/null 2>&1; echo $?', capture=True) 73 | if int(res.strip()): 74 | do_exit('Warning: ghp-import not installed! ' 75 | 'run: `pip install ghp-import`') 76 | output_dir = configs['destination'] 77 | remote = deploy_configs.get('remote', 'origin') 78 | branch = deploy_configs.get('branch', 'gh-pages') 79 | # commit gh-pages branch and push to remote 80 | _mesg = 'Update output documentation' 81 | local('ghp-import -p -m "{0}" -r {1} -b {2} {3}' 82 | .format(_mesg, remote, branch, output_dir)) 83 | 84 | 85 | def deploy_ftp(deploy_configs): 86 | """for ftp""" 87 | conn_kwargs = {'host': deploy_configs['host']} 88 | login_kwargs = {} 89 | if 'port' in deploy_configs: 90 | conn_kwargs.update({'port': deploy_configs['port']}) 91 | if 'user' in deploy_configs: 92 | login_kwargs.update({'user': deploy_configs['user']}) 93 | if 'password' in deploy_configs: 94 | passwd = deploy_configs['password'] 95 | # when set password key with no value, get None by yaml 96 | if passwd is None: 97 | passwd = getpass.getpass('Input your ftp password: ') 98 | login_kwargs.update({'passwd': passwd}) 99 | 100 | ftp_dir = deploy_configs.get('dir', '/') 101 | output_dir = configs['destination'] 102 | 103 | ftp = ftplib.FTP() 104 | ftp.connect(**conn_kwargs) 105 | ftp.login(**login_kwargs) 106 | 107 | for root, dirs, files in os.walk(output_dir): 108 | rel_root = os.path.relpath(root, output_dir) 109 | for fn in files: 110 | store_fn = os.path.join(ftp_dir, rel_root, fn) 111 | ftp.storbinary('STOR %s' % store_fn, 112 | open(os.path.join(root, fn), 'rb')) 113 | 114 | ftp.close() 115 | 116 | 117 | @task 118 | def deploy(type=None): 119 | """deploy your site, support rsync / ftp / github pages 120 | 121 | run deploy: 122 | $ fab deploy 123 | 124 | run deploy with specific type(not supported specify multiple types): 125 | $ fab deploy:type=rsync 126 | 127 | """ 128 | if 'deploy' not in configs or not isinstance(configs['deploy'], list): 129 | do_exit('Warning: deploy not set right in _config.yml') 130 | if type and type not in SUPPORTED_DEPLOY_TYPES: 131 | do_exit('Warning: supported deploy type: {0}' 132 | .format(', '.join(SUPPORTED_DEPLOY_TYPES))) 133 | 134 | deploy_configs = configs['deploy'] 135 | 136 | done = False 137 | 138 | for deploy_item in deploy_configs: 139 | deploy_type = deploy_item.pop('type') 140 | if type and deploy_type != type: 141 | continue 142 | func_name = 'deploy_{0}'.format(deploy_type) 143 | func = globals().get(func_name) 144 | if not func: 145 | do_exit('Warning: not supprt {0} deploy method' 146 | .format(deploy_type)) 147 | func(deploy_item) 148 | done = True 149 | 150 | if not done: 151 | if type: 152 | do_exit('Warning: specific deploy type not configured yet') 153 | else: 154 | print(blue('do nothing...')) 155 | 156 | 157 | @task 158 | def commit(): 159 | """git commit source changes from all tracked files 160 | 161 | include: 162 | 163 | - add all tracked files in the work tree, include modified(M), deleted(D) 164 | - commit all files in the index, include added(A), modified(M), 165 | renamed(R), deleted(D) 166 | - untracked files should be manually added to the index before 167 | run this task 168 | 169 | before do commit, it requires to confirm the files to be committed; and 170 | the requirement before do add is a future feature, it is currently 171 | disabled. 172 | """ 173 | message = 'Update Documentation' 174 | yes_ans = ('y', 'yes') 175 | 176 | with settings(warn_only=True): 177 | # Changes in the work tree to add 178 | add_file = '--update .' # include tracked files 179 | # hack of res.return_code without warning info 180 | res = local('git diff --quiet --exit-code; echo $?', capture=True) 181 | if int(res.strip()): 182 | if False: # future feature? 183 | # TODO: there use diff to uniform with below, and the 184 | # output can be formatted like `git add --dry-run --update .` 185 | test_res = local('git diff --name-status', capture=True) 186 | try: 187 | _ans = raw_input('\n{0}\nAdd these files to index? (y/N) ' 188 | .format(test_res.strip())) 189 | if _ans.lower() in yes_ans: 190 | local("git add {0}".format(add_file)) 191 | except (KeyboardInterrupt, SystemExit): 192 | pass 193 | else: 194 | local("git add {0}".format(add_file)) 195 | 196 | # Changes in the index to commit 197 | res = local('git diff --cached --quiet --exit-code; echo $?', 198 | capture=True) 199 | if int(res.strip()): 200 | test_res = local('git diff --cached --name-status', capture=True) 201 | try: 202 | _ans = raw_input('\n{0}\nCommit these files? (y/N) ' 203 | .format(test_res.strip())) 204 | if _ans.lower() in yes_ans: 205 | local("git commit -m '{0}'".format(message)) 206 | except (KeyboardInterrupt, SystemExit): 207 | pass 208 | else: 209 | print('Nothing to commit.') 210 | -------------------------------------------------------------------------------- /simiki/conf_templates/gettingstarted.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting Started" 3 | layout: page 4 | date: 2099-06-02 00:00 5 | --- 6 | 7 | [TOC] 8 | 9 | # Simiki # 10 | 11 | [![Latest Version](http://img.shields.io/pypi/v/simiki.svg)](https://pypi.python.org/pypi/simiki) 12 | [![The MIT License](http://img.shields.io/badge/license-MIT-yellow.svg)](https://github.com/tankywoo/simiki/blob/master/LICENSE) 13 | [![Build Status](https://travis-ci.org/tankywoo/simiki.svg)](https://travis-ci.org/tankywoo/simiki) 14 | [![Coverage Status](https://img.shields.io/coveralls/tankywoo/simiki.svg)](https://coveralls.io/r/tankywoo/simiki) 15 | 16 | Simiki is a simple wiki framework, written in [Python](https://www.python.org/). 17 | 18 | * Easy to use. Creating a wiki only needs a few steps 19 | * Use [Markdown](http://daringfireball.net/projects/markdown/). Just open your editor and write 20 | * Store source files by category 21 | * Static HTML output 22 | * A CLI tool to manage the wiki 23 | 24 | Simiki is short for `Simple Wiki` :) 25 | 26 | ## Quick Start ## 27 | 28 | ### Install ### 29 | 30 | pip install simiki 31 | 32 | ### Update ### 33 | 34 | pip install -U simiki 35 | 36 | ### Init Site ### 37 | 38 | mkdir mywiki && cd mywiki 39 | simiki init 40 | 41 | ### Create a new wiki ### 42 | 43 | simiki new -t "Hello Simiki" -c first-catetory 44 | 45 | ### Generate ### 46 | 47 | simiki g 48 | 49 | ### Preview ### 50 | 51 | simiki p -w 52 | 53 | For more information, `simiki -h` or have a look at [Simiki.org](http://simiki.org) 54 | 55 | ## Others ## 56 | 57 | * [simiki.org](http://simiki.org) 58 | * <https://github.com/tankywoo/simiki> 59 | * Email: <me@tankywoo.com> 60 | * [Simiki Users](https://github.com/tankywoo/simiki/wiki/Simiki-Users) 61 | 62 | ## License ## 63 | 64 | The MIT License (MIT) 65 | 66 | Copyright (c) 2013 Tanky Woo 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining a copy of 69 | this software and associated documentation files (the "Software"), to deal in 70 | the Software without restriction, including without limitation the rights to 71 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 72 | the Software, and to permit persons to whom the Software is furnished to do so, 73 | subject to the following conditions: 74 | 75 | The above copyright notice and this permission notice shall be included in all 76 | copies or substantial portions of the Software. 77 | 78 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 79 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 80 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 81 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 82 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 83 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 84 | -------------------------------------------------------------------------------- /simiki/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | import os 6 | import os.path 7 | import sys 8 | import io 9 | import logging 10 | import datetime 11 | from pprint import pprint 12 | import yaml 13 | 14 | import tzlocal 15 | 16 | 17 | class ConfigFileNotFound(Exception): 18 | pass 19 | 20 | 21 | def _set_default_config(): 22 | config = { 23 | "url": "", 24 | "title": "", 25 | "keywords": "", 26 | "description": "", 27 | "author": "", 28 | "root": "/", 29 | "source": "content", 30 | "destination": "output", 31 | "attach": "attach", 32 | "themes_dir": "themes", 33 | "theme": "simple2", 34 | "default_ext": "md", 35 | "pygments": True, 36 | "debug": False, 37 | "time": datetime.datetime.now(tzlocal.get_localzone()), 38 | } 39 | return config 40 | 41 | 42 | def _post_process(config): 43 | for k, v in config.items(): 44 | if v is None: 45 | config[k] = "" 46 | 47 | if config["url"].endswith("/"): 48 | config["url"] = config["url"][:-1] 49 | 50 | return config 51 | 52 | 53 | def get_default_config(): 54 | return _post_process(_set_default_config()) 55 | 56 | 57 | def parse_config(config_file): 58 | if not os.path.exists(config_file): 59 | raise ConfigFileNotFound("{0} not exists".format(config_file)) 60 | 61 | default_config = _set_default_config() 62 | 63 | with io.open(config_file, "rt", encoding="utf-8") as fd: 64 | config = yaml.load(fd, Loader=yaml.FullLoader) 65 | 66 | default_config.update(config) 67 | config = _post_process(default_config) 68 | 69 | return config 70 | 71 | 72 | if __name__ == "__main__": 73 | # pylint: disable=pointless-string-statement 74 | """ 75 | Usage: 76 | python -m simiki.config : to test config template 77 | python -m simiki.config _config.yml : to test _config.yml file in \ 78 | curren dir 79 | """ 80 | if len(sys.argv) == 1: 81 | base_dir = os.path.dirname(__file__) 82 | _config_file = os.path.join(base_dir, "conf_templates", 83 | "_config.yml.in") 84 | elif len(sys.argv) == 2: 85 | base_dir = os.getcwd() 86 | _config_file = os.path.join(base_dir, sys.argv[1]) 87 | else: 88 | logging.error("Use the template config file by default, " 89 | "you can specify the config file to parse. \n" 90 | "Usage: `python -m simiki.config [_config.yml]'") 91 | sys.exit(1) 92 | 93 | pprint(parse_config(_config_file)) 94 | -------------------------------------------------------------------------------- /simiki/generators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Convert Markdown file to html, which is embeded in html template. 4 | """ 5 | 6 | from __future__ import (print_function, with_statement, unicode_literals, 7 | absolute_import) 8 | 9 | import os 10 | import os.path 11 | import io 12 | import copy 13 | import re 14 | import traceback 15 | import warnings 16 | try: 17 | from collections import OrderedDict 18 | except ImportError: 19 | from ordereddict import OrderedDict 20 | 21 | import markdown 22 | import yaml 23 | from jinja2 import (Environment, FileSystemLoader, TemplateError) 24 | 25 | from simiki import jinja_exts 26 | from simiki.utils import import_string 27 | from simiki.compat import is_py2, is_py3, basestring 28 | 29 | if is_py3: 30 | from functools import cmp_to_key 31 | 32 | PLAT_LINE_SEP = '\n' 33 | 34 | 35 | class BaseGenerator(object): 36 | """Base generator class""" 37 | 38 | def __init__(self, site_config, base_path): 39 | """ 40 | :site_config: site global configuration parsed from _config.yml 41 | :base_path: root path of wiki directory 42 | """ 43 | self.site_config = copy.deepcopy(site_config) 44 | self.base_path = base_path 45 | self._templates = {} # templates cache 46 | self._template_vars = self._get_template_vars() 47 | _template_path = os.path.join( 48 | self.base_path, 49 | site_config["themes_dir"], 50 | site_config["theme"] 51 | ) 52 | if not os.path.exists(_template_path): 53 | raise Exception("Theme `{0}' not exists".format(_template_path)) 54 | self.env = Environment( 55 | loader=FileSystemLoader(_template_path) 56 | ) 57 | self._jinja_load_exts() 58 | 59 | def _jinja_load_exts(self): 60 | """Load jinja custom filters and extensions""" 61 | for _filter in jinja_exts.filters: 62 | self.env.filters[_filter] = getattr(jinja_exts, _filter) 63 | 64 | def get_template(self, name): 65 | """Return the template by layout name""" 66 | if name not in self._templates: 67 | try: 68 | self._templates[name] = self.env.get_template(name + '.html') 69 | except TemplateError: 70 | # jinja2.exceptions.TemplateNotFound will get blocked 71 | # in multiprocessing? 72 | exc_msg = "unable to load template '{0}.html'\n{1}" \ 73 | .format(name, traceback.format_exc()) 74 | raise Exception(exc_msg) 75 | 76 | return self._templates[name] 77 | 78 | def _get_template_vars(self): 79 | """Return the common template variables""" 80 | template_vars = { 81 | 'site': self.site_config, 82 | } 83 | 84 | # if site.root endswith '/`, remove it. 85 | site_root = template_vars['site']['root'] 86 | if site_root.endswith('/'): 87 | template_vars['site']['root'] = site_root[:-1] 88 | 89 | return template_vars 90 | 91 | 92 | class PageGenerator(BaseGenerator): 93 | 94 | def __init__(self, site_config, base_path, tags=None): 95 | super(PageGenerator, self).__init__(site_config, base_path) 96 | self._tags = tags 97 | self._reset() 98 | 99 | def _reset(self): 100 | """Reset the global self variables""" 101 | self._src_file = None # source file path relative to base_path 102 | self.meta = None 103 | self.content = None 104 | 105 | def to_html(self, src_file, include_draft=False): 106 | """Load template, and generate html 107 | 108 | :src_file: the filename of the source file. This can either be an 109 | absolute filename or a filename relative to the base path. 110 | :include_draft: True/False, include draft pages or not to generate 111 | """ 112 | self._reset() 113 | self._src_file = os.path.relpath(src_file, self.base_path) 114 | self.meta, self.content = self.get_meta_and_content() 115 | # Page set `draft: True' mark current page as draft, and will 116 | # be ignored if not forced generate include draft pages 117 | if not include_draft and self.meta.get('draft', False): 118 | return None 119 | layout = self.get_layout(self.meta) 120 | template_vars = self.get_template_vars(self.meta, self.content) 121 | template = self.get_template(layout) 122 | html = template.render(template_vars) 123 | 124 | return html 125 | 126 | @property 127 | def src_file(self): 128 | return self._src_file 129 | 130 | @src_file.setter 131 | def src_file(self, filename): 132 | self._src_file = os.path.relpath(filename, self.base_path) 133 | 134 | def get_meta_and_content(self, do_render=True): 135 | meta_str, content_str = self.extract_page(self._src_file) 136 | meta = self.parse_meta(meta_str) 137 | # This is the most time consuming part 138 | if do_render and meta.get('render', True): 139 | content = self._parse_markup(content_str) 140 | else: 141 | content = content_str 142 | 143 | return meta, content 144 | 145 | def get_layout(self, meta): 146 | """Get layout config in meta, default is `page'""" 147 | if "layout" in meta: 148 | # Compatible with previous version, which default layout is "post" 149 | # XXX Will remove this checker in v2.0 150 | if meta["layout"] == "post": 151 | warn_msg = "{0}: layout `post' is deprecated, use `page'" \ 152 | .format(self._src_file) 153 | if is_py2: 154 | # XXX: warnings message require str, no matter whether 155 | # py2 or py3; but in py3, bytes message is ok in simple 156 | # test, but failed in unittest with py3.3, ok with py3.4? 157 | warn_msg = warn_msg.encode('utf-8') 158 | warnings.warn(warn_msg, DeprecationWarning) 159 | layout = "page" 160 | else: 161 | layout = meta["layout"] 162 | else: 163 | layout = "page" 164 | 165 | return layout 166 | 167 | def get_template_vars(self, meta, content): 168 | """Get template variables, include site config and page config""" 169 | template_vars = copy.deepcopy(self._template_vars) 170 | page = {"content": content} 171 | page.update(meta) 172 | page.update({'relation': self.get_relation()}) 173 | 174 | template_vars.update({'page': page}) 175 | 176 | return template_vars 177 | 178 | def get_category_and_file(self): 179 | """Get the name of category and file(with extension)""" 180 | src_file_relpath_to_source = \ 181 | os.path.relpath(self._src_file, self.site_config['source']) 182 | category, filename = os.path.split(src_file_relpath_to_source) 183 | return (category, filename) 184 | 185 | @staticmethod 186 | def extract_page(filename): 187 | """Split the page file texts by triple-dashed lines, return the mata 188 | and content. 189 | 190 | :param filename: the filename of markup page 191 | 192 | returns: 193 | meta_str (str): page's meta string 194 | content_str (str): html parsed from markdown or other markup text. 195 | """ 196 | regex = re.compile('(?sm)^---(?P<meta>.*?)^---(?P<body>.*)') 197 | with io.open(filename, "rt", encoding="utf-8") as fd: 198 | match_obj = re.match(regex, fd.read()) 199 | if match_obj: 200 | meta_str = match_obj.group('meta') 201 | content_str = match_obj.group('body') 202 | else: 203 | raise Exception('extracting page with format error, ' 204 | 'see <http://simiki.org/docs/metadata.html>') 205 | 206 | return meta_str, content_str 207 | 208 | def parse_meta(self, yaml_str): 209 | """Parse meta from yaml string, and validate yaml filed, return dict""" 210 | try: 211 | meta = yaml.load(yaml_str, Loader=yaml.FullLoader) 212 | except yaml.YAMLError as e: 213 | e.extra_msg = 'yaml format error' 214 | raise 215 | 216 | category, src_fname = self.get_category_and_file() 217 | dst_fname = src_fname.replace( 218 | ".{0}".format(self.site_config['default_ext']), '.html') 219 | meta.update({'category': category, 'filename': dst_fname}) 220 | 221 | if 'tag' in meta: 222 | if isinstance(meta['tag'], basestring): 223 | _tags = [t.strip() for t in meta['tag'].split(',')] 224 | meta.update({'tag': _tags}) 225 | 226 | if "title" not in meta: 227 | raise Exception("no 'title' in meta") 228 | 229 | return meta 230 | 231 | def _parse_markup(self, markup_text): 232 | """Parse markup text to html 233 | 234 | Only support Markdown for now. 235 | """ 236 | markdown_extensions = self._set_markdown_extensions() 237 | 238 | html_content = markdown.markdown( 239 | markup_text, 240 | extensions=markdown_extensions, 241 | ) 242 | 243 | return html_content 244 | 245 | def _set_markdown_extensions(self): 246 | """Set the extensions for markdown parser""" 247 | # Default enabled extensions 248 | markdown_extensions_config = { 249 | "fenced_code": {}, 250 | "nl2br": {}, 251 | "toc": {"title": "Table of Contents"}, 252 | "extra": {}, 253 | } 254 | # Handle pygments 255 | if self.site_config["pygments"]: 256 | markdown_extensions_config.update({ 257 | "codehilite": {"css_class": "hlcode"} 258 | }) 259 | # Handle markdown_ext 260 | # Ref: https://pythonhosted.org/Markdown/extensions/index.html#officially-supported-extensions # noqa 261 | if "markdown_ext" in self.site_config: 262 | markdown_extensions_config.update(self.site_config["markdown_ext"]) 263 | 264 | markdown_extensions = [] 265 | for k, v in markdown_extensions_config.items(): 266 | ext = import_string("markdown.extensions." + k).makeExtension() 267 | if v: 268 | for i, j in v.items(): 269 | ext.setConfig(i, j) 270 | markdown_extensions.append(ext) 271 | 272 | return markdown_extensions 273 | 274 | def get_relation(self): 275 | rn = [] 276 | if self._tags and 'tag' in self.meta: 277 | for t in self.meta['tag']: 278 | rn.extend(self._tags[t]) 279 | # remove itself 280 | rn = [r for r in rn if self.meta['title'] != r['title']] 281 | # remove the duplicate items 282 | # note this will change the items order 283 | rn = [r for n, r in enumerate(rn) if r not in rn[n+1:]] # noqa: E226 284 | return rn 285 | 286 | 287 | class CatalogGenerator(BaseGenerator): 288 | 289 | def __init__(self, site_config, base_path, pages): 290 | """ 291 | :pages: all pages' meta variables, dict type 292 | """ 293 | super(CatalogGenerator, self).__init__(site_config, base_path) 294 | self._pages = pages 295 | self.pages = None 296 | self.structure = None 297 | 298 | def get_structure(self): 299 | """Ref: http://stackoverflow.com/a/9619101/1276501""" 300 | dct = {} 301 | ext = self.site_config["default_ext"] 302 | for path, meta in self._pages.items(): 303 | # Ignore other files 304 | if not path.endswith(ext): 305 | continue 306 | p = dct 307 | for x in path.split(os.sep): 308 | if ext in x: 309 | meta["name"] = os.path.splitext(x)[0] 310 | p = p.setdefault(x, meta) 311 | else: 312 | p = p.setdefault(x, {}) 313 | 314 | self.structure = dct.get(self.site_config['source'], {}) 315 | 316 | self.sort_structure() 317 | 318 | def sort_structure(self): 319 | """Sort index structure in lower-case, alphabetical order 320 | 321 | Compare argument is a key/value structure, if the compare argument is a 322 | leaf node, which has `title` key in its value, use the title value, 323 | else use the key to compare. 324 | """ 325 | 326 | def _cmp(arg1, arg2): 327 | arg1 = arg1[1]["title"] if "title" in arg1[1] else arg1[0] 328 | arg2 = arg2[1]["title"] if "title" in arg2[1] else arg2[0] 329 | # cmp not exists in py3 330 | # via <https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons> # noqa 331 | cmp = lambda x, y: (x > y) - (x < y) # noqa: E731 332 | return cmp(arg1.lower(), arg2.lower()) 333 | 334 | if is_py2: 335 | sorted_opts = {'cmp': _cmp} 336 | elif is_py3: 337 | sorted_opts = {'key': cmp_to_key(_cmp)} 338 | 339 | def _sort(structure): 340 | sorted_structure = copy.deepcopy(structure) 341 | for k, _ in sorted_structure.items(): 342 | sorted_structure = OrderedDict(sorted( 343 | sorted_structure.items(), 344 | **sorted_opts 345 | )) 346 | if k.endswith(".{0}".format(self.site_config["default_ext"])): 347 | continue 348 | sorted_structure[k] = _sort(sorted_structure[k]) 349 | return sorted_structure 350 | 351 | self.structure = _sort(self.structure) 352 | 353 | def get_pages(self): 354 | # for custom category settings in _config.yml 355 | _category = {} 356 | for c in self.site_config.get('category', []): 357 | c_name = c.pop('name') 358 | _category[c_name] = c 359 | 360 | def convert(d, prefix=''): 361 | pages = [] 362 | for k, v in d.items(): 363 | if 'name' in v: # page 364 | v.update({'fname': k}) 365 | pages.append(v) 366 | else: 367 | k_with_prefix = os.path.join(prefix, k) 368 | _pages = convert(v, prefix=k_with_prefix) 369 | _s_category = {'name': k, 'pages': _pages} 370 | if k_with_prefix in _category: 371 | _s_category.update(_category[k_with_prefix]) 372 | pages.append(_s_category) 373 | 374 | return pages 375 | 376 | # get pages from structure 377 | self.pages = convert(self.structure) 378 | 379 | self.update_pages_collection() 380 | 381 | def update_pages_collection(self): 382 | pages = copy.deepcopy(self.pages) 383 | self.pages = [] 384 | # for two-level, first level is category 385 | for category in pages: 386 | if 'fname' in category: 387 | # for first level pages 388 | continue 389 | _c_pages = [] 390 | _colls = {} 391 | for page in category.pop('pages'): 392 | if 'collection' in page: 393 | coll_name = page['collection'] 394 | _colls.setdefault(coll_name, []).append(page) 395 | else: 396 | _c_pages.append(page) 397 | colls = [] 398 | for _coll_n, _coll_p in _colls.items(): 399 | colls.append({'name': _coll_n, 'pages': _coll_p}) 400 | _c_pages.extend(colls) 401 | category.update({'pages': _c_pages}) 402 | self.pages.append(category) 403 | 404 | def get_template_vars(self): 405 | template_vars = copy.deepcopy(self._template_vars) 406 | 407 | self.get_structure() 408 | # `structure' is deprecated and will be removed later 409 | # use `pages' instead 410 | template_vars['site'].update({'structure': self.structure}) 411 | 412 | self.get_pages() 413 | template_vars.update({'pages': self.pages}) 414 | 415 | return template_vars 416 | 417 | def generate_catalog_html(self): 418 | tpl_vars = self.get_template_vars() 419 | html = self.env.get_template("index.html").render(tpl_vars) 420 | return html 421 | 422 | 423 | class FeedGenerator(BaseGenerator): 424 | def __init__(self, site_config, base_path, pages, feed_fn='atom.xml'): 425 | """ 426 | :pages: all pages' meta variables, dict type 427 | """ 428 | super(FeedGenerator, self).__init__(site_config, base_path) 429 | self.pages = pages 430 | self.feed_fn = feed_fn 431 | 432 | def get_template_vars(self): 433 | tpl_vars = { 434 | "site": self.site_config, 435 | "pages": self.pages 436 | } 437 | 438 | # if site.root endwith `\`, remote it. 439 | site_root = tpl_vars["site"]["root"] 440 | if site_root.endswith("/"): 441 | tpl_vars["site"]["root"] = site_root[:-1] 442 | 443 | return tpl_vars 444 | 445 | def generate_feed(self): 446 | tpl_vars = self.get_template_vars() 447 | with open(os.path.join(self.base_path, self.feed_fn), 'r') as fd: 448 | template = self.env.from_string(fd.read()) 449 | feed_content = template.render(tpl_vars) 450 | return feed_content 451 | -------------------------------------------------------------------------------- /simiki/initiator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function, unicode_literals, absolute_import 4 | 5 | import os 6 | import os.path 7 | import shutil 8 | import logging 9 | 10 | from simiki.config import parse_config 11 | from simiki.utils import (copytree, mkdir_p, listdir_nohidden) 12 | from simiki.compat import raw_input 13 | 14 | yes_answer = ('y', 'yes') 15 | 16 | 17 | class Initiator(object): 18 | conf_template_dn = "conf_templates" 19 | config_fn = "_config.yml" 20 | fabfile_fn = "fabfile.py" 21 | demo_fn = "gettingstarted.md" 22 | dockerfile_fn = "Dockerfile" 23 | 24 | def __init__(self, config_file, target_path): 25 | self.config_file = config_file 26 | self.config = parse_config(self.config_file) 27 | self.source_path = os.path.dirname(__file__) 28 | self.target_path = target_path 29 | 30 | @staticmethod 31 | def get_file(src, dst): 32 | if os.path.exists(dst): 33 | logging.warning("{0} exists".format(dst)) 34 | return 35 | 36 | # Create parent directory 37 | dst_directory = os.path.dirname(dst) 38 | if not os.path.exists(dst_directory): 39 | mkdir_p(dst_directory) 40 | logging.info("Creating directory: {0}".format(dst_directory)) 41 | 42 | shutil.copyfile(src, dst) 43 | logging.info("Creating file: {0}".format(dst)) 44 | 45 | def get_config_file(self): 46 | dst_config_file = os.path.join(self.target_path, self.config_fn) 47 | self.get_file(self.config_file, dst_config_file) 48 | 49 | def get_fabfile(self): 50 | src_fabfile = os.path.join( 51 | self.source_path, 52 | self.conf_template_dn, 53 | self.fabfile_fn 54 | ) 55 | dst_fabfile = os.path.join(self.target_path, self.fabfile_fn) 56 | self.get_file(src_fabfile, dst_fabfile) 57 | 58 | def get_dockerfile(self): 59 | src_dockerfile = os.path.join( 60 | self.source_path, 61 | self.conf_template_dn, 62 | self.dockerfile_fn 63 | ) 64 | dst_dockerfile = os.path.join(self.target_path, self.dockerfile_fn) 65 | self.get_file(src_dockerfile, dst_dockerfile) 66 | 67 | def get_demo_page(self): 68 | nohidden_dir = listdir_nohidden( 69 | os.path.join(self.target_path, self.config['source'])) 70 | # If there is file/directory under content, do not create first page 71 | if next(nohidden_dir, False): 72 | return 73 | 74 | src_demo = os.path.join(self.source_path, self.conf_template_dn, 75 | self.demo_fn) 76 | dst_demo = os.path.join(self.target_path, "content", "intro", 77 | self.demo_fn) 78 | self.get_file(src_demo, dst_demo) 79 | 80 | def get_default_theme(self, theme_path): 81 | default_theme_name = self.config['theme'] 82 | src_theme = os.path.join(self.source_path, self.config['themes_dir'], 83 | default_theme_name) 84 | dst_theme = os.path.join(theme_path, default_theme_name) 85 | if os.path.exists(dst_theme): 86 | logging.warning('{0} exists'.format(dst_theme)) 87 | else: 88 | copytree(src_theme, dst_theme) 89 | logging.info("Copying default theme '{0}' to: {1}" 90 | .format(default_theme_name, theme_path)) 91 | 92 | def init(self, ask=False, **kwargs): 93 | content_path = os.path.join(self.target_path, self.config["source"]) 94 | output_path = os.path.join(self.target_path, 95 | self.config["destination"]) 96 | theme_path = os.path.join(self.target_path, self.config['themes_dir']) 97 | for path in (content_path, output_path, theme_path): 98 | if os.path.exists(path): 99 | logging.warning("{0} exists".format(path)) 100 | else: 101 | mkdir_p(path) 102 | logging.info("Creating directory: {0}".format(path)) 103 | 104 | self.get_config_file() 105 | self.get_fabfile() 106 | self.get_demo_page() 107 | self.get_default_theme(theme_path) 108 | 109 | if ask is True: 110 | try: 111 | _ans = raw_input('Create Dockerfile? (y/N) ') 112 | if _ans.lower() in yes_answer: 113 | self.get_dockerfile() 114 | except (KeyboardInterrupt, SystemExit): 115 | print() # newline with Ctrl-C 116 | elif ask is False and kwargs.get('dockerfile', False): 117 | self.get_dockerfile() 118 | -------------------------------------------------------------------------------- /simiki/jinja_exts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Jinja2 custom filters and extensions 5 | """ 6 | import datetime 7 | import tzlocal 8 | from simiki.compat import basestring 9 | 10 | filters = ['rfc3339'] 11 | 12 | 13 | def rfc3339(dt_obj): 14 | """ 15 | dt_obj: datetime object or string 16 | 17 | The filter use `datetime.datetime.isoformat()`, which is in ISO 8601 18 | format, not in RFC 3339 format, but they have a lot in common, so I used 19 | ISO 8601 format directly. 20 | """ 21 | if isinstance(dt_obj, datetime.datetime): 22 | pass 23 | elif isinstance(dt_obj, basestring): 24 | for fmt in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M'): 25 | try: 26 | dt_obj = datetime.datetime.strptime(dt_obj, fmt) 27 | except ValueError: 28 | pass 29 | else: 30 | break 31 | else: 32 | raise ValueError('can not parse datetime {0}'.format(dt_obj)) 33 | else: 34 | raise ValueError('{0} is not datetime object or string'.format(dt_obj)) 35 | # make sure the dt_obj is local time 36 | if not dt_obj.tzinfo: 37 | tz = tzlocal.get_localzone() 38 | dt_obj = tz.localize(dt_obj) 39 | # remove microsecond 40 | dt_obj = dt_obj.replace(microsecond=0) 41 | return dt_obj.isoformat() 42 | -------------------------------------------------------------------------------- /simiki/log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import, unicode_literals 4 | 5 | import logging 6 | from logging import getLogger, Formatter, StreamHandler 7 | 8 | from simiki import utils 9 | from simiki.compat import is_linux, is_osx 10 | 11 | 12 | class ANSIFormatter(Formatter): 13 | """Use ANSI escape sequences to colored log""" 14 | 15 | def format(self, record): 16 | try: 17 | msg = super(ANSIFormatter, self).format(record) 18 | except: 19 | # 2017-05-15: not support py26 20 | # for python2.6 21 | # Formatter is old-style class in python2.6 and type is classobj 22 | # another trick: http://stackoverflow.com/a/18392639/1276501 23 | msg = Formatter.format(self, record) 24 | 25 | lvl2color = { 26 | "DEBUG": "blue", 27 | "INFO": "green", 28 | "WARNING": "yellow", 29 | "ERROR": "red", 30 | "CRITICAL": "bgred" 31 | } 32 | 33 | rln = record.levelname 34 | if rln in lvl2color: 35 | return "[{0}]: {1}".format( 36 | utils.color_msg(lvl2color[rln], rln), 37 | msg 38 | ) 39 | else: 40 | return msg 41 | 42 | 43 | class NonANSIFormatter(Formatter): 44 | """Non ANSI color format""" 45 | 46 | def format(self, record): 47 | try: 48 | msg = super(NonANSIFormatter, self).format(record) 49 | except: 50 | # 2017-05-15: not support py26 51 | # for python2.6 52 | # Formatter is old-style class in python2.6 and type is classobj 53 | # another trick: http://stackoverflow.com/a/18392639/1276501 54 | msg = Formatter.format(self, record) 55 | 56 | rln = record.levelname 57 | return "[{0}]: {1}".format(rln, msg) 58 | 59 | 60 | def _is_platform_allowed_ansi(): 61 | """ansi be used on linux/macos""" 62 | if is_linux or is_osx: 63 | return True 64 | else: 65 | return False 66 | 67 | 68 | def logging_init(level=None, logger=getLogger(), 69 | handler=StreamHandler(), use_color=True): 70 | if use_color and _is_platform_allowed_ansi(): 71 | fmt = ANSIFormatter() 72 | else: 73 | fmt = NonANSIFormatter() 74 | handler.setFormatter(fmt) 75 | logger.addHandler(handler) 76 | 77 | if level: 78 | logger.setLevel(level) 79 | 80 | 81 | if __name__ == "__main__": 82 | logging_init(level=logging.DEBUG) 83 | 84 | root_logger = logging.getLogger() 85 | root_logger.debug("debug") 86 | root_logger.info("info") 87 | root_logger.warning("warning") 88 | root_logger.error("error") 89 | root_logger.critical("critical") 90 | -------------------------------------------------------------------------------- /simiki/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function, absolute_import, unicode_literals 4 | 5 | import os 6 | import os.path 7 | import sys 8 | import logging 9 | import traceback 10 | from simiki.compat import is_py2, unicode 11 | 12 | try: 13 | import SimpleHTTPServer as http_server 14 | except ImportError: 15 | # py3 16 | import http.server as http_server 17 | 18 | try: 19 | import SocketServer as socket_server 20 | except ImportError: 21 | # py3 22 | import socketserver as socket_server 23 | 24 | try: 25 | import urllib2 as urllib_request 26 | except ImportError: 27 | # py3 28 | import urllib.request as urllib_request 29 | 30 | try: 31 | from os import getcwdu 32 | except ImportError: 33 | # py3 34 | from os import getcwd as getcwdu 35 | 36 | URL_ROOT = None 37 | PUBLIC_DIRECTORY = None 38 | 39 | 40 | class Reuse_TCPServer(socket_server.TCPServer): 41 | allow_reuse_address = True 42 | 43 | 44 | class YARequestHandler(http_server.SimpleHTTPRequestHandler): 45 | 46 | def translate_path(self, path): 47 | """map url path to local file system. 48 | path and return path are str type 49 | 50 | in py3, builtin translate_path input is str(but it's unicode) and 51 | return str. so there is no need to do with codecs, system can locate 52 | file with unicode path. 53 | in py2, buildin translate_path input is str and return str. we need 54 | to decode to unicode and then encode path with filesystemencoding(), 55 | as mentioned above, unicode path can be located, but will have problem 56 | with py2's translate_path, for uniformity, we also return the 57 | corresponding type of translate_path in manual part. 58 | 59 | TODO: 60 | - fspath with os.sep from url always slash 61 | - URL_ROOT codecs simplify? 62 | - in the end of if body use super translate_path directly? 63 | """ 64 | path = urllib_request.unquote(path) 65 | if not isinstance(path, unicode): 66 | path = path.decode('utf-8') 67 | fsenc = sys.getfilesystemencoding() 68 | if is_py2: 69 | path = path.encode(fsenc) 70 | 71 | if URL_ROOT and self.path.startswith(URL_ROOT): 72 | if self.path == URL_ROOT or self.path == URL_ROOT + '/': 73 | fspath = os.path.join(PUBLIC_DIRECTORY, 'index.html') 74 | if is_py2: 75 | fspath = fspath.encode(fsenc) 76 | else: 77 | _url_root = urllib_request.unquote(URL_ROOT) 78 | if not isinstance(_url_root, unicode): 79 | _url_root = _url_root.decode('utf-8') 80 | if is_py2: 81 | _url_root = _url_root.encode(fsenc) 82 | fspath = os.path.join( 83 | PUBLIC_DIRECTORY.encode(fsenc), path[len(_url_root) + 1:]) # noqa: E501 84 | else: 85 | fspath = os.path.join( 86 | PUBLIC_DIRECTORY, path[len(_url_root) + 1:]) 87 | return fspath 88 | else: 89 | return http_server.SimpleHTTPRequestHandler \ 90 | .translate_path(self, path) 91 | 92 | def do_GET(self): 93 | # redirect url 94 | if URL_ROOT and not self.path.startswith(URL_ROOT): 95 | self.send_response(301) 96 | self.send_header('Location', URL_ROOT + self.path) 97 | self.end_headers() 98 | http_server.SimpleHTTPRequestHandler.do_GET(self) 99 | 100 | 101 | def preview(path, url_root, host='127.0.0.1', port=8000): 102 | """ 103 | :param path: directory path relative to current path 104 | :param url_root: `root` setted in _config.yml 105 | """ 106 | global URL_ROOT, PUBLIC_DIRECTORY 107 | 108 | if not host: 109 | host = '127.0.0.1' 110 | if not port: 111 | port = 8000 112 | 113 | if url_root.endswith('/'): 114 | url_root = url_root[:-1] 115 | 116 | URL_ROOT = urllib_request.quote(url_root.encode('utf-8')) 117 | PUBLIC_DIRECTORY = os.path.join(getcwdu(), path) 118 | 119 | if os.path.exists(path): 120 | os.chdir(path) 121 | else: 122 | logging.error("Path {} not exists".format(path)) 123 | try: 124 | Handler = YARequestHandler 125 | httpd = Reuse_TCPServer((host, port), Handler) 126 | except (OSError, IOError) as e: 127 | logging.error("Could not listen on port {0}\n{1}" 128 | .format(port, traceback.format_exc())) 129 | sys.exit(getattr(e, 'exitcode', 1)) 130 | 131 | logging.info("Serving at: http://{0}:{1}{2}/".format(host, port, url_root)) 132 | logging.info("Serving running... (Press CTRL-C to quit)") 133 | try: 134 | httpd.serve_forever() 135 | except (KeyboardInterrupt, SystemExit): 136 | logging.info("Shutting down server") 137 | httpd.socket.close() 138 | -------------------------------------------------------------------------------- /simiki/themes/simple/base.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE HTML> 2 | <html> 3 | <head> 4 | <link rel="Stylesheet" type="text/css" href="{{ site.root }}/static/css/style.css"> 5 | <link rel="Stylesheet" type="text/css" href="{{ site.root }}/static/css/tango.css"> 6 | <link rel="shortcut icon" href="{{ site.root }}/favicon.ico" type="image/x-icon"> 7 | <link rel="icon" href="{{ site.root }}/favicon.ico" type="image/x-icon"> 8 | <title>{% block title %}{{ site.title }}{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | {% block container %} 17 | {{ page.content }} 18 | {% endblock %} 19 |
20 | 26 | {% include "stat.html" %} 27 | 28 | 29 | -------------------------------------------------------------------------------- /simiki/themes/simple/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% macro list_page(pages, parent_url, parent_id) -%} 4 |
    5 | {%- for page_k, page_v in pages.items() %} 6 | {% if site.default_ext in page_k %} 7 |
  • 8 | {{ page_v.title }} 9 |
  • 10 | {% else %} 11 | {% set id = parent_id ~ "-" ~ page_k %} 12 |
  • {{ page_k }}
  • 13 | {% set url = parent_url ~ "/" ~ page_k %} 14 | {{ list_page(page_v, url, id) }} 15 | {% endif %} 16 | {%- endfor %} 17 |
18 | {%- endmacro %} 19 | 20 | {% block title %}{{ site.title }}{% endblock %} 21 | 22 | {% block container %} 23 |
{{ site.title }}
24 | 25 |
26 | {% for category_name, pages in site.structure.items() %} 27 | {# Top-level wiki pages not display in index #} 28 | {% if site.default_ext not in category_name %} 29 |

{{ category_name }}

30 | {% set url = "./" ~ category_name %} 31 | {{ list_page(pages, url, category_name) }} 32 |
33 | {% endif %} 34 | {% endfor %} 35 |
36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /simiki/themes/simple/page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ page.title }} - {{ site.title }}{% endblock %} 4 | 5 | {% block container %} 6 | 24 |
25 |
{{ page.title }}
26 |
27 | {{ page.content }} 28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /simiki/themes/simple/stat.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tankywoo/simiki/22e544254577477c3f624c9d201f644580f36231/simiki/themes/simple/stat.html -------------------------------------------------------------------------------- /simiki/themes/simple/static/css/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Theme Name: Simple 3 | Author: Tanky Woo 4 | Author URI: http://www.wutianqi.com/ 5 | Description: Default theme for simiki. 6 | Version: 0.1 7 | License: The MIT License (MIT) 8 | Tags: simple, single-columns, code, codehighlight, white, markdown 9 | */ 10 | 11 | /* Global Configuration */ 12 | /* IE6 ignores this and uses default size of 16pt */ 13 | html>body { font-size: 14px; } 14 | 15 | html { 16 | font-size: 100%; 17 | overflow-y: scroll; 18 | -webkit-text-size-adjust: 100%; 19 | -ms-text-size-adjust: 100%; 20 | } 21 | 22 | body { 23 | font-family: "Helvetica Neue", "Segoe UI", Arial, Sans-Serif; 24 | line-height: 1.5em; 25 | background-color: whitesmoke; 26 | } 27 | 28 | a { color: #0645ad; text-decoration: none; } 29 | a:visited { color: #0b0080; } 30 | a:hover { color: #06e; text-decoration: underline; } 31 | a:active { color: #faa700; } 32 | 33 | p { margin: 0 0 1.5em 0; text-align: justify; } 34 | 35 | h1, h2, h3, h4, h5, h6 { 36 | font-weight: normal; 37 | color: #111; 38 | line-height: 1em; 39 | } 40 | 41 | h1 { font-size: 2.0em; } 42 | h2 { font-size: 1.6em; border-bottom: 1px solid #ddd; padding-bottom: 5px; overflow:hidden } 43 | h3 { font-size: 1.4em; } 44 | h4 { font-size: 1.2em; } 45 | h5 { font-size: 1.0em; } 46 | h6 { font-size: 0.8em; } 47 | 48 | blockquote { 49 | color: #666; 50 | margin: 0; 51 | padding-left: 3em; 52 | border-left: 0.4em #eee solid; 53 | } 54 | 55 | hr { 56 | border: 1px solid silver; 57 | margin: 1em 0; 58 | padding: 0; 59 | } 60 | 61 | b, strong { font-weight: bold; } 62 | 63 | ul, ol { 64 | margin: 1.5em 0; 65 | padding-left: 1.5em; 66 | line-height: 1.5em; 67 | } 68 | 69 | ul li { list-style-type: square; } 70 | 71 | ul li p { margin: 0; } 72 | 73 | img { 74 | border: 0; 75 | vertical-align: middle; 76 | margin: 0 auto; 77 | max-width: 100%; 78 | } 79 | 80 | table { 81 | border-collapse: collapse; 82 | border-spacing: 0; 83 | margin-bottom: 1em; 84 | border: 1px solid #ccc; 85 | } 86 | 87 | td { 88 | vertical-align: top; 89 | padding: 3px 6px; 90 | } 91 | 92 | /* Optimization for pre and code tag */ 93 | pre, code { 94 | font-family: Consolas, "DejaVu Sans Mono", "Lucida Console", Monaco, Andale Mono, "MS Gothic", monospace; 95 | font-size: 13px; 96 | border-radius: 3px; 97 | -moz-border-radius: 3px; 98 | -webkit-border-radius: 3px; 99 | overflow: auto; 100 | } 101 | 102 | code { 103 | color: #666; 104 | background-color: #eee; 105 | margin: 0 2px; 106 | padding: 1px 3px; 107 | } 108 | 109 | pre { 110 | border: 1px solid #eee; 111 | padding: 8px 12px; 112 | white-space: pre; 113 | white-space: -moz-pre-wrap; /* Firefox */ 114 | white-space: -pre-wrap; /* ancient Opera */ 115 | white-space: -o-pre-wrap; /* newer Opera */ 116 | white-space: pre-wrap; /* Chrome; W3C standard */ 117 | word-wrap: break-word; /* IE */ 118 | } 119 | 120 | pre code { 121 | border: 0px !important; 122 | padding: 0; 123 | color: #000; 124 | background-color: #f8f8f8; 125 | } 126 | 127 | /* Custom Configuration */ 128 | #container { 129 | background: white; 130 | width: 50em; 131 | line-height: 1.5em; 132 | margin: 1.5em auto 3em auto; 133 | padding: 1.5em 2em; 134 | border: 1px solid #ccc; 135 | box-shadow: 2px 2px 8px #aaa; 136 | -webkit-box-shadow: 2px 2px 8px #aaa; 137 | -moz-box-shadow: 2px 2px 8px #aaa; 138 | } 139 | 140 | #wiki_title, #title { 141 | text-align: center; 142 | font-size: 2em; 143 | line-height: 1.5em; 144 | padding-bottom: 10px; 145 | margin: 20px 0; 146 | } 147 | 148 | #title { 149 | border-bottom: 1px dashed #eee; 150 | } 151 | 152 | #header { 153 | } 154 | 155 | #footer { 156 | margin: 10px auto; 157 | color: grey; 158 | font-size: 80%; 159 | text-align: center; 160 | } 161 | 162 | li.pagelist { 163 | width: 100%; 164 | margin-right: 1.5em; 165 | list-style-type: none; 166 | white-space: nowrap; 167 | overflow: hidden; 168 | } 169 | 170 | li.pagelist:before { 171 | content: "\0BB \020"; 172 | } 173 | 174 | #index ul { margin: 0; } 175 | 176 | /* Table of Contents */ 177 | .toc { 178 | margin: 0 0.5em; 179 | padding: 0.5em 1em; 180 | border: 1px solid silver; 181 | background-color: #ecf5ff; 182 | float: right; 183 | display: block; 184 | font-size: 90%; 185 | } 186 | 187 | .toc > .toctitle { 188 | display: block; 189 | text-align: center; 190 | font-weight: bold; 191 | margin-bottom: 1em; 192 | } 193 | 194 | .toc ul { 195 | margin: 0; 196 | } 197 | 198 | /* Hackers */ 199 | /* Hacker for codehilite */ 200 | table.codehilitetable { 201 | border-collapse: separate; 202 | border: 1px solid #eee; 203 | margin: 14px 0; 204 | border-radius: 3px; 205 | -moz-border-radius: 3px; 206 | -webkit-border-radius: 3px; 207 | } 208 | 209 | .codehilitetable pre { 210 | margin: 0; 211 | border: 0 none; 212 | box-shadow: none; 213 | -webkit-box-shadow: none; 214 | -moz-box-shadow: none; 215 | } 216 | 217 | .codehilitetable td { 218 | border: 0 none; 219 | padding: 0; 220 | margin: 0; 221 | } 222 | 223 | /* Clear the float problem */ 224 | .clearfix:after { 225 | visibility: hidden; 226 | clear: both; 227 | font-size: 0; 228 | content: "."; 229 | display: block; 230 | height: 0; 231 | } 232 | 233 | 234 | table.hlcodetable { 235 | border: 1px solid #eee; 236 | border-collapse: separate !important; 237 | border-radius: 3px; 238 | -moz-border-radius: 3px; 239 | -webkit-border-radius: 3px; 240 | } 241 | 242 | .hlcodetable td { 243 | padding: 0; 244 | border: none; 245 | } 246 | 247 | .hlcodetable .linenodiv { 248 | border-right: 1px solid #ccc; 249 | } 250 | 251 | .hlcodetable .linenodiv pre { 252 | margin: 0; 253 | border: none; 254 | } 255 | 256 | .hlcodetable .hlcode pre { 257 | margin: 0; 258 | border: none; 259 | } 260 | -------------------------------------------------------------------------------- /simiki/themes/simple/static/css/tango.css: -------------------------------------------------------------------------------- 1 | pre .hll { background-color: #ffffcc } 2 | pre { background: #f8f8f8; } 3 | pre .c { color: #8f5902; font-style: italic } /* Comment */ 4 | pre .g { color: #000000 } /* Generic */ 5 | pre .k { color: #204a87; font-weight: bold } /* Keyword */ 6 | pre .l { color: #000000 } /* Literal */ 7 | pre .n { color: #000000 } /* Name */ 8 | pre .o { color: #ce5c00; font-weight: bold } /* Operator */ 9 | pre .x { color: #000000 } /* Other */ 10 | pre .p { color: #000000; font-weight: bold } /* Punctuation */ 11 | pre .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ 12 | pre .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */ 13 | pre .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ 14 | pre .cs { color: #8f5902; font-style: italic } /* Comment.Special */ 15 | pre .gd { color: #a40000 } /* Generic.Deleted */ 16 | pre .ge { color: #000000; font-style: italic } /* Generic.Emph */ 17 | pre .gr { color: #ef2929 } /* Generic.Error */ 18 | pre .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 19 | pre .gi { color: #00A000 } /* Generic.Inserted */ 20 | pre .go { color: #000000; font-style: italic } /* Generic.Output */ 21 | pre .gp { color: #8f5902 } /* Generic.Prompt */ 22 | pre .gs { color: #000000; font-weight: bold } /* Generic.Strong */ 23 | pre .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 24 | pre .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ 25 | pre .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */ 26 | pre .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */ 27 | pre .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */ 28 | pre .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */ 29 | pre .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */ 30 | pre .kt { color: #204a87; font-weight: bold } /* Keyword.Type */ 31 | pre .ld { color: #000000 } /* Literal.Date */ 32 | pre .m { color: #0000cf; font-weight: bold } /* Literal.Number */ 33 | pre .s { color: #4e9a06 } /* Literal.String */ 34 | pre .na { color: #c4a000 } /* Name.Attribute */ 35 | pre .nb { color: #204a87 } /* Name.Builtin */ 36 | pre .nc { color: #000000 } /* Name.Class */ 37 | pre .no { color: #000000 } /* Name.Constant */ 38 | pre .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */ 39 | pre .ni { color: #ce5c00 } /* Name.Entity */ 40 | pre .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ 41 | pre .nf { color: #000000 } /* Name.Function */ 42 | pre .nl { color: #f57900 } /* Name.Label */ 43 | pre .nn { color: #000000 } /* Name.Namespace */ 44 | pre .nx { color: #000000 } /* Name.Other */ 45 | pre .py { color: #000000 } /* Name.Property */ 46 | pre .nt { color: #204a87; font-weight: bold } /* Name.Tag */ 47 | pre .nv { color: #000000 } /* Name.Variable */ 48 | pre .ow { color: #204a87; font-weight: bold } /* Operator.Word */ 49 | pre .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */ 50 | pre .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */ 51 | pre .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */ 52 | pre .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */ 53 | pre .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */ 54 | pre .sb { color: #4e9a06 } /* Literal.String.Backtick */ 55 | pre .sc { color: #4e9a06 } /* Literal.String.Char */ 56 | pre .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ 57 | pre .s2 { color: #4e9a06 } /* Literal.String.Double */ 58 | pre .se { color: #4e9a06 } /* Literal.String.Escape */ 59 | pre .sh { color: #4e9a06 } /* Literal.String.Heredoc */ 60 | pre .si { color: #4e9a06 } /* Literal.String.Interpol */ 61 | pre .sx { color: #4e9a06 } /* Literal.String.Other */ 62 | pre .sr { color: #4e9a06 } /* Literal.String.Regex */ 63 | pre .s1 { color: #4e9a06 } /* Literal.String.Single */ 64 | pre .ss { color: #4e9a06 } /* Literal.String.Symbol */ 65 | pre .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ 66 | pre .vc { color: #000000 } /* Name.Variable.Class */ 67 | pre .vg { color: #000000 } /* Name.Variable.Global */ 68 | pre .vi { color: #000000 } /* Name.Variable.Instance */ 69 | pre .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */ 70 | .hlcode pre .hll { background-color: #ffffcc } 71 | .hlcode pre { background: #f8f8f8; } 72 | .hlcode pre .c { color: #8f5902; font-style: italic } /* Comment */ 73 | .hlcode pre .g { color: #000000 } /* Generic */ 74 | .hlcode pre .k { color: #204a87; font-weight: bold } /* Keyword */ 75 | .hlcode pre .l { color: #000000 } /* Literal */ 76 | .hlcode pre .n { color: #000000 } /* Name */ 77 | .hlcode pre .o { color: #ce5c00; font-weight: bold } /* Operator */ 78 | .hlcode pre .x { color: #000000 } /* Other */ 79 | .hlcode pre .p { color: #000000; font-weight: bold } /* Punctuation */ 80 | .hlcode pre .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ 81 | .hlcode pre .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */ 82 | .hlcode pre .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ 83 | .hlcode pre .cs { color: #8f5902; font-style: italic } /* Comment.Special */ 84 | .hlcode pre .gd { color: #a40000 } /* Generic.Deleted */ 85 | .hlcode pre .ge { color: #000000; font-style: italic } /* Generic.Emph */ 86 | .hlcode pre .gr { color: #ef2929 } /* Generic.Error */ 87 | .hlcode pre .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 88 | .hlcode pre .gi { color: #00A000 } /* Generic.Inserted */ 89 | .hlcode pre .go { color: #000000; font-style: italic } /* Generic.Output */ 90 | .hlcode pre .gp { color: #8f5902 } /* Generic.Prompt */ 91 | .hlcode pre .gs { color: #000000; font-weight: bold } /* Generic.Strong */ 92 | .hlcode pre .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 93 | .hlcode pre .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ 94 | .hlcode pre .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */ 95 | .hlcode pre .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */ 96 | .hlcode pre .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */ 97 | .hlcode pre .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */ 98 | .hlcode pre .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */ 99 | .hlcode pre .kt { color: #204a87; font-weight: bold } /* Keyword.Type */ 100 | .hlcode pre .ld { color: #000000 } /* Literal.Date */ 101 | .hlcode pre .m { color: #0000cf; font-weight: bold } /* Literal.Number */ 102 | .hlcode pre .s { color: #4e9a06 } /* Literal.String */ 103 | .hlcode pre .na { color: #c4a000 } /* Name.Attribute */ 104 | .hlcode pre .nb { color: #204a87 } /* Name.Builtin */ 105 | .hlcode pre .nc { color: #000000 } /* Name.Class */ 106 | .hlcode pre .no { color: #000000 } /* Name.Constant */ 107 | .hlcode pre .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */ 108 | .hlcode pre .ni { color: #ce5c00 } /* Name.Entity */ 109 | .hlcode pre .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ 110 | .hlcode pre .nf { color: #000000 } /* Name.Function */ 111 | .hlcode pre .nl { color: #f57900 } /* Name.Label */ 112 | .hlcode pre .nn { color: #000000 } /* Name.Namespace */ 113 | .hlcode pre .nx { color: #000000 } /* Name.Other */ 114 | .hlcode pre .py { color: #000000 } /* Name.Property */ 115 | .hlcode pre .nt { color: #204a87; font-weight: bold } /* Name.Tag */ 116 | .hlcode pre .nv { color: #000000 } /* Name.Variable */ 117 | .hlcode pre .ow { color: #204a87; font-weight: bold } /* Operator.Word */ 118 | .hlcode pre .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */ 119 | .hlcode pre .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */ 120 | .hlcode pre .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */ 121 | .hlcode pre .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */ 122 | .hlcode pre .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */ 123 | .hlcode pre .sb { color: #4e9a06 } /* Literal.String.Backtick */ 124 | .hlcode pre .sc { color: #4e9a06 } /* Literal.String.Char */ 125 | .hlcode pre .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ 126 | .hlcode pre .s2 { color: #4e9a06 } /* Literal.String.Double */ 127 | .hlcode pre .se { color: #4e9a06 } /* Literal.String.Escape */ 128 | .hlcode pre .sh { color: #4e9a06 } /* Literal.String.Heredoc */ 129 | .hlcode pre .si { color: #4e9a06 } /* Literal.String.Interpol */ 130 | .hlcode pre .sx { color: #4e9a06 } /* Literal.String.Other */ 131 | .hlcode pre .sr { color: #4e9a06 } /* Literal.String.Regex */ 132 | .hlcode pre .s1 { color: #4e9a06 } /* Literal.String.Single */ 133 | .hlcode pre .ss { color: #4e9a06 } /* Literal.String.Symbol */ 134 | .hlcode pre .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ 135 | .hlcode pre .vc { color: #000000 } /* Name.Variable.Class */ 136 | .hlcode pre .vg { color: #000000 } /* Name.Variable.Global */ 137 | .hlcode pre .vi { color: #000000 } /* Name.Variable.Instance */ 138 | .hlcode pre .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */ 139 | -------------------------------------------------------------------------------- /simiki/themes/simple2/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block title %}{{ site.title }}{% endblock %} 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | {% block container %} 18 | {{ page.content }} 19 | {% endblock %} 20 |
21 | 28 | 29 | {% block script %} 30 | {% endblock %} 31 | 32 | 33 | -------------------------------------------------------------------------------- /simiki/themes/simple2/index.html: -------------------------------------------------------------------------------- 1 | {%- extends "base.html" %} 2 | 3 | {%- block title %}{{ site.title }}{% endblock %} 4 | 5 | {%- block container %} 6 |
{{ site.title }}
7 | 8 |
9 | {%- if site.index %} {# custom index page #} 10 | {{ page.content }} 11 | {%- else %} {# auto generate index page #} 12 | {%- for category in pages %} 13 | {%- if site.default_ext not in category %} {# is category #} 14 |
15 |

16 | {%- if 'label' in category %}{{ category.label }} 17 | {%- else %}{{ category.name|capitalize }}{% endif %} 18 |

19 |
20 |
    21 | {%- for page in category.pages %} 22 | {%- if site.default_ext in page.fname %} {# is page #} 23 |
  • 24 | {{ page.title }} 25 | {%- if 'description' in page %} 26 |    {{ page.description }} 27 | {% endif %} 28 | {%- elif 'pages' in page %} {# is collection #} 29 |
  • 30 | {% set coll = page %} {# for readability #} 31 |
    32 |
    {{ coll.name }} : 
    33 |
    34 | {%- for coll_page in coll.pages %} 35 | {{ coll_page.title }} 36 | {%- if 'description' in coll_page %} 37 |  {{ coll_page.description }} 38 | {% endif %} 39 | {%- if not loop.last %} / {% endif %} 40 | {%- endfor %} 41 |
    42 |
    43 | {%- endif %} 44 |
  • 45 | {%- endfor %} 46 |
47 |
48 |
49 | {%- endif %} 50 |
51 | {%- endfor %} 52 | {%- endif %} 53 |
54 | {%- endblock %} 55 | 56 | {%- block script %} 57 | 69 | {%- endblock %} 70 | -------------------------------------------------------------------------------- /simiki/themes/simple2/page.html: -------------------------------------------------------------------------------- 1 | {%- extends "base.html" %} 2 | 3 | {%- block title %}{{ page.title }} - {{ site.title }}{% endblock %} 4 | 5 | {%- block container %} 6 | 28 |
29 | 30 |
{{ page.title }}
31 | 32 | {{ page.content }} 33 | 34 | {%- if page.relation %} 35 |
36 |

Related

37 |
    38 | {% for r in page.relation %} 39 |
  • {{ r.title }}
  • 40 | {% endfor %} 41 |
42 |
43 | {%- endif %} 44 | 45 | {%- endblock %} 46 | -------------------------------------------------------------------------------- /simiki/themes/simple2/static/css/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Theme Name: Simple2 3 | Author: Tanky Woo 4 | Author URI: http://www.wutianqi.com/ 5 | Description: Default theme for simiki. 6 | Version: 0.1 7 | License: The MIT License (MIT) 8 | Tags: simple, single-columns, code, codehighlight, white, markdown 9 | */ 10 | 11 | /* Global Configuration */ 12 | /* IE6 ignores this and uses default size of 16pt */ 13 | html>body { font-size: 14px; } 14 | 15 | html { 16 | font-size: 100%; 17 | overflow-y: scroll; 18 | -webkit-text-size-adjust: 100%; 19 | -ms-text-size-adjust: 100%; 20 | } 21 | 22 | body { 23 | font-family: "Helvetica Neue", "Segoe UI", Arial, Sans-Serif; 24 | line-height: 1.5em; 25 | background-color: whitesmoke; 26 | color: #2f2f2f; 27 | } 28 | 29 | a { color: #0645ad; text-decoration: none; } 30 | a:visited { color: #0645ad; } 31 | a:hover { color: #06e; } 32 | a:active { color: #06e; } 33 | 34 | p { margin: 0 0 1.5em 0; text-align: justify; } 35 | 36 | h1, h2, h3, h4, h5, h6 { 37 | font-weight: normal; 38 | line-height: 1em; 39 | } 40 | 41 | h1 { font-size: 2.0em; } 42 | h2 { font-size: 1.6em; border-bottom: 1px solid #ddd; padding-bottom: 5px; overflow:hidden } 43 | h3 { font-size: 1.4em; } 44 | h4 { font-size: 1.2em; } 45 | h5 { font-size: 1.0em; } 46 | h6 { font-size: 0.8em; } 47 | 48 | blockquote { 49 | color: #666; 50 | margin: 0; 51 | padding-left: 3em; 52 | border-left: 0.4em #eee solid; 53 | } 54 | 55 | hr { 56 | border: 1px solid silver; 57 | margin: 1em 0; 58 | padding: 0; 59 | } 60 | 61 | b, strong { font-weight: bold; } 62 | 63 | ul, ol { 64 | margin: 1.5em 0; 65 | padding-left: 1.5em; 66 | line-height: 1.5em; 67 | } 68 | 69 | ul li { list-style-type: square; } 70 | 71 | ul li p { margin: 0; } 72 | 73 | img { 74 | border: 0; 75 | vertical-align: middle; 76 | margin: 0 auto; 77 | max-width: 100%; 78 | } 79 | 80 | table { 81 | border-collapse: collapse; 82 | border-spacing: 0; 83 | margin-bottom: 1em; 84 | border: 1px solid #ccc; 85 | } 86 | 87 | td { 88 | vertical-align: top; 89 | padding: 3px 6px; 90 | } 91 | 92 | /* Optimization for pre and code tag */ 93 | pre, code { 94 | font-family: Consolas, "DejaVu Sans Mono", "Lucida Console", Monaco, Andale Mono, "MS Gothic", monospace; 95 | font-size: 13px; 96 | border-radius: 3px; 97 | -moz-border-radius: 3px; 98 | -webkit-border-radius: 3px; 99 | overflow: auto; 100 | } 101 | 102 | code { 103 | color: #666; 104 | background-color: #eee; 105 | margin: 0 2px; 106 | padding: 1px 3px; 107 | } 108 | 109 | pre { 110 | border: 1px solid #eee; 111 | padding: 8px 12px; 112 | white-space: pre; 113 | white-space: -moz-pre-wrap; /* Firefox */ 114 | white-space: -pre-wrap; /* ancient Opera */ 115 | white-space: -o-pre-wrap; /* newer Opera */ 116 | white-space: pre-wrap; /* Chrome; W3C standard */ 117 | word-wrap: break-word; /* IE */ 118 | } 119 | 120 | pre code { 121 | border: 0px !important; 122 | padding: 0; 123 | color: #000; 124 | background-color: #f8f8f8; 125 | } 126 | 127 | /* Custom Configuration */ 128 | #container { 129 | background: white; 130 | width: 50em; 131 | line-height: 1.5em; 132 | margin: 1.5em auto 3em auto; 133 | padding: 1.5em 2em; 134 | border: 1px solid #ccc; 135 | box-shadow: 2px 2px 8px #aaa; 136 | -webkit-box-shadow: 2px 2px 8px #aaa; 137 | -moz-box-shadow: 2px 2px 8px #aaa; 138 | } 139 | 140 | #header .updated { 141 | float: right; 142 | font-size: 80%; 143 | color: #555; 144 | } 145 | 146 | .title, .page_title { 147 | text-align: center; 148 | font-size: 2em; 149 | line-height: 1.5em; 150 | padding-bottom: 10px; 151 | margin: 20px 0; 152 | } 153 | 154 | .page_title { 155 | border-bottom: 1px dashed #eee; 156 | } 157 | 158 | #footer p { 159 | margin: 10px auto; 160 | font-size: 80%; 161 | text-align: center; 162 | color: #555; 163 | line-height: 1em; 164 | } 165 | 166 | #footer a { color: #555; } 167 | #footer a:hover { color: black; } 168 | 169 | .post-nav a { color: #555; } 170 | .post-nav a:hover { color: black; } 171 | 172 | .category a { color: #555; } 173 | .category a:hover { color: black; } 174 | 175 | .category_pages ul { 176 | padding-left: 0; 177 | } 178 | 179 | li.pagelist { 180 | width: 100%; 181 | margin-right: 1.5em; 182 | list-style-type: none; 183 | overflow: hidden; 184 | } 185 | 186 | .item_arrow:before { 187 | content: "\0BB \020"; 188 | } 189 | 190 | .index ul { margin: 0; } 191 | 192 | /* Table of Contents */ 193 | .toc { 194 | margin: 0 0.5em; 195 | padding: 0.5em 1em; 196 | border: 1px solid silver; 197 | background-color: #ecf5ff; 198 | float: right; 199 | display: block; 200 | font-size: 90%; 201 | } 202 | 203 | .toc > .toctitle { 204 | display: block; 205 | text-align: center; 206 | font-weight: bold; 207 | margin-bottom: 1em; 208 | } 209 | 210 | .toc ul { 211 | margin: 0; 212 | } 213 | 214 | /* Hackers */ 215 | /* Hacker for codehilite */ 216 | table.highlighttable { 217 | border: 1px solid #eee; 218 | border-collapse: separate !important; 219 | border-radius: 3px; 220 | -moz-border-radius: 3px; 221 | -webkit-border-radius: 3px; 222 | } 223 | 224 | .highlighttable td { 225 | padding: 0; 226 | border: none; 227 | } 228 | 229 | .highlighttable td.code { 230 | width: 100%; 231 | } 232 | 233 | .highlighttable .linenodiv { 234 | border-right: 1px solid #ccc; 235 | } 236 | 237 | .highlighttable .linenodiv pre { 238 | margin: 0; 239 | border: none; 240 | } 241 | 242 | .highlighttable .highlight pre { 243 | margin: 0; 244 | border: none; 245 | } 246 | 247 | /* Clear the float problem */ 248 | .clearfix:after { 249 | visibility: hidden; 250 | clear: both; 251 | font-size: 0; 252 | content: "."; 253 | display: block; 254 | height: 0; 255 | } 256 | 257 | 258 | table.hlcodetable { 259 | border: 1px solid #eee; 260 | border-collapse: separate !important; 261 | border-radius: 3px; 262 | -moz-border-radius: 3px; 263 | -webkit-border-radius: 3px; 264 | } 265 | 266 | .hlcodetable td { 267 | padding: 0; 268 | border: none; 269 | } 270 | 271 | .hlcodetable td.code { 272 | width: 100%; 273 | } 274 | 275 | .hlcodetable .linenodiv { 276 | border-right: 1px solid #ccc; 277 | } 278 | 279 | .hlcodetable .linenodiv pre { 280 | margin: 0; 281 | border: none; 282 | } 283 | 284 | .hlcodetable .hlcode pre { 285 | margin: 0; 286 | border: none; 287 | } 288 | 289 | .list_wrapper { 290 | display: inline-block; 291 | } 292 | 293 | .coll_name { 294 | float: left; 295 | } 296 | -------------------------------------------------------------------------------- /simiki/themes/simple2/static/css/tango.css: -------------------------------------------------------------------------------- 1 | pre .hll { background-color: #ffffcc } 2 | pre { background: #f8f8f8; } 3 | pre .c { color: #8f5902; font-style: italic } /* Comment */ 4 | pre .g { color: #000000 } /* Generic */ 5 | pre .k { color: #204a87; font-weight: bold } /* Keyword */ 6 | pre .l { color: #000000 } /* Literal */ 7 | pre .n { color: #000000 } /* Name */ 8 | pre .o { color: #ce5c00; font-weight: bold } /* Operator */ 9 | pre .x { color: #000000 } /* Other */ 10 | pre .p { color: #000000; font-weight: bold } /* Punctuation */ 11 | pre .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ 12 | pre .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */ 13 | pre .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ 14 | pre .cs { color: #8f5902; font-style: italic } /* Comment.Special */ 15 | pre .gd { color: #a40000 } /* Generic.Deleted */ 16 | pre .ge { color: #000000; font-style: italic } /* Generic.Emph */ 17 | pre .gr { color: #ef2929 } /* Generic.Error */ 18 | pre .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 19 | pre .gi { color: #00A000 } /* Generic.Inserted */ 20 | pre .go { color: #000000; font-style: italic } /* Generic.Output */ 21 | pre .gp { color: #8f5902 } /* Generic.Prompt */ 22 | pre .gs { color: #000000; font-weight: bold } /* Generic.Strong */ 23 | pre .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 24 | pre .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ 25 | pre .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */ 26 | pre .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */ 27 | pre .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */ 28 | pre .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */ 29 | pre .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */ 30 | pre .kt { color: #204a87; font-weight: bold } /* Keyword.Type */ 31 | pre .ld { color: #000000 } /* Literal.Date */ 32 | pre .m { color: #0000cf; font-weight: bold } /* Literal.Number */ 33 | pre .s { color: #4e9a06 } /* Literal.String */ 34 | pre .na { color: #c4a000 } /* Name.Attribute */ 35 | pre .nb { color: #204a87 } /* Name.Builtin */ 36 | pre .nc { color: #000000 } /* Name.Class */ 37 | pre .no { color: #000000 } /* Name.Constant */ 38 | pre .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */ 39 | pre .ni { color: #ce5c00 } /* Name.Entity */ 40 | pre .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ 41 | pre .nf { color: #000000 } /* Name.Function */ 42 | pre .nl { color: #f57900 } /* Name.Label */ 43 | pre .nn { color: #000000 } /* Name.Namespace */ 44 | pre .nx { color: #000000 } /* Name.Other */ 45 | pre .py { color: #000000 } /* Name.Property */ 46 | pre .nt { color: #204a87; font-weight: bold } /* Name.Tag */ 47 | pre .nv { color: #000000 } /* Name.Variable */ 48 | pre .ow { color: #204a87; font-weight: bold } /* Operator.Word */ 49 | pre .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */ 50 | pre .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */ 51 | pre .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */ 52 | pre .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */ 53 | pre .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */ 54 | pre .sb { color: #4e9a06 } /* Literal.String.Backtick */ 55 | pre .sc { color: #4e9a06 } /* Literal.String.Char */ 56 | pre .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ 57 | pre .s2 { color: #4e9a06 } /* Literal.String.Double */ 58 | pre .se { color: #4e9a06 } /* Literal.String.Escape */ 59 | pre .sh { color: #4e9a06 } /* Literal.String.Heredoc */ 60 | pre .si { color: #4e9a06 } /* Literal.String.Interpol */ 61 | pre .sx { color: #4e9a06 } /* Literal.String.Other */ 62 | pre .sr { color: #4e9a06 } /* Literal.String.Regex */ 63 | pre .s1 { color: #4e9a06 } /* Literal.String.Single */ 64 | pre .ss { color: #4e9a06 } /* Literal.String.Symbol */ 65 | pre .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ 66 | pre .vc { color: #000000 } /* Name.Variable.Class */ 67 | pre .vg { color: #000000 } /* Name.Variable.Global */ 68 | pre .vi { color: #000000 } /* Name.Variable.Instance */ 69 | pre .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */ 70 | .hlcode pre .hll { background-color: #ffffcc } 71 | .hlcode pre { background: #f8f8f8; } 72 | .hlcode pre .c { color: #8f5902; font-style: italic } /* Comment */ 73 | .hlcode pre .g { color: #000000 } /* Generic */ 74 | .hlcode pre .k { color: #204a87; font-weight: bold } /* Keyword */ 75 | .hlcode pre .l { color: #000000 } /* Literal */ 76 | .hlcode pre .n { color: #000000 } /* Name */ 77 | .hlcode pre .o { color: #ce5c00; font-weight: bold } /* Operator */ 78 | .hlcode pre .x { color: #000000 } /* Other */ 79 | .hlcode pre .p { color: #000000; font-weight: bold } /* Punctuation */ 80 | .hlcode pre .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ 81 | .hlcode pre .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */ 82 | .hlcode pre .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ 83 | .hlcode pre .cs { color: #8f5902; font-style: italic } /* Comment.Special */ 84 | .hlcode pre .gd { color: #a40000 } /* Generic.Deleted */ 85 | .hlcode pre .ge { color: #000000; font-style: italic } /* Generic.Emph */ 86 | .hlcode pre .gr { color: #ef2929 } /* Generic.Error */ 87 | .hlcode pre .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 88 | .hlcode pre .gi { color: #00A000 } /* Generic.Inserted */ 89 | .hlcode pre .go { color: #000000; font-style: italic } /* Generic.Output */ 90 | .hlcode pre .gp { color: #8f5902 } /* Generic.Prompt */ 91 | .hlcode pre .gs { color: #000000; font-weight: bold } /* Generic.Strong */ 92 | .hlcode pre .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 93 | .hlcode pre .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ 94 | .hlcode pre .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */ 95 | .hlcode pre .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */ 96 | .hlcode pre .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */ 97 | .hlcode pre .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */ 98 | .hlcode pre .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */ 99 | .hlcode pre .kt { color: #204a87; font-weight: bold } /* Keyword.Type */ 100 | .hlcode pre .ld { color: #000000 } /* Literal.Date */ 101 | .hlcode pre .m { color: #0000cf; font-weight: bold } /* Literal.Number */ 102 | .hlcode pre .s { color: #4e9a06 } /* Literal.String */ 103 | .hlcode pre .na { color: #c4a000 } /* Name.Attribute */ 104 | .hlcode pre .nb { color: #204a87 } /* Name.Builtin */ 105 | .hlcode pre .nc { color: #000000 } /* Name.Class */ 106 | .hlcode pre .no { color: #000000 } /* Name.Constant */ 107 | .hlcode pre .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */ 108 | .hlcode pre .ni { color: #ce5c00 } /* Name.Entity */ 109 | .hlcode pre .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ 110 | .hlcode pre .nf { color: #000000 } /* Name.Function */ 111 | .hlcode pre .nl { color: #f57900 } /* Name.Label */ 112 | .hlcode pre .nn { color: #000000 } /* Name.Namespace */ 113 | .hlcode pre .nx { color: #000000 } /* Name.Other */ 114 | .hlcode pre .py { color: #000000 } /* Name.Property */ 115 | .hlcode pre .nt { color: #204a87; font-weight: bold } /* Name.Tag */ 116 | .hlcode pre .nv { color: #000000 } /* Name.Variable */ 117 | .hlcode pre .ow { color: #204a87; font-weight: bold } /* Operator.Word */ 118 | .hlcode pre .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */ 119 | .hlcode pre .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */ 120 | .hlcode pre .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */ 121 | .hlcode pre .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */ 122 | .hlcode pre .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */ 123 | .hlcode pre .sb { color: #4e9a06 } /* Literal.String.Backtick */ 124 | .hlcode pre .sc { color: #4e9a06 } /* Literal.String.Char */ 125 | .hlcode pre .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ 126 | .hlcode pre .s2 { color: #4e9a06 } /* Literal.String.Double */ 127 | .hlcode pre .se { color: #4e9a06 } /* Literal.String.Escape */ 128 | .hlcode pre .sh { color: #4e9a06 } /* Literal.String.Heredoc */ 129 | .hlcode pre .si { color: #4e9a06 } /* Literal.String.Interpol */ 130 | .hlcode pre .sx { color: #4e9a06 } /* Literal.String.Other */ 131 | .hlcode pre .sr { color: #4e9a06 } /* Literal.String.Regex */ 132 | .hlcode pre .s1 { color: #4e9a06 } /* Literal.String.Single */ 133 | .hlcode pre .ss { color: #4e9a06 } /* Literal.String.Symbol */ 134 | .hlcode pre .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ 135 | .hlcode pre .vc { color: #000000 } /* Name.Variable.Class */ 136 | .hlcode pre .vg { color: #000000 } /* Name.Variable.Global */ 137 | .hlcode pre .vi { color: #000000 } /* Name.Variable.Instance */ 138 | .hlcode pre .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */ 139 | -------------------------------------------------------------------------------- /simiki/updater.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import shutil 5 | import logging 6 | from simiki.compat import raw_input 7 | from simiki.utils import copytree, get_md5 8 | 9 | logger = logging.getLogger(__name__) 10 | yes_answer = ('y', 'yes') 11 | 12 | 13 | def get_input(text): 14 | return raw_input(text) 15 | 16 | 17 | def _update_file(filename, local_path, original_path): 18 | """ 19 | :filename: file name to be updated, without directory 20 | :local_path: directory of local filename 21 | :original_path: directory of original filename 22 | """ 23 | up_to_date = True 24 | 25 | original_fn = os.path.join(original_path, filename) 26 | local_fn = os.path.join(local_path, filename) 27 | 28 | try: 29 | if os.path.exists(local_fn): 30 | original_fn_md5 = get_md5(original_fn) 31 | local_fn_md5 = get_md5(local_fn) 32 | 33 | if local_fn_md5 != original_fn_md5: 34 | up_to_date = False 35 | try: 36 | _ans = get_input('Overwrite {0}? (y/N) '.format(filename)) 37 | if _ans.lower() in yes_answer: 38 | shutil.copy2(original_fn, local_fn) 39 | except (KeyboardInterrupt, SystemExit): 40 | print() # newline with Ctrl-C 41 | else: 42 | up_to_date = False 43 | try: 44 | _ans = get_input('New {0}? (y/N) '.format(filename)) 45 | if _ans.lower() in yes_answer: 46 | shutil.copy2(original_fn, local_fn) 47 | except (KeyboardInterrupt, SystemExit): 48 | print() 49 | except Exception as e: 50 | logger.error(e) 51 | 52 | if up_to_date: 53 | logger.info('{0} is already up to date.'.format(filename)) 54 | 55 | 56 | def _update_dir(dirname, local_dir, original_dir, tag='directory'): 57 | """Update sth on a per-directory basis, such as theme 58 | :dirname: directory name to be updated, without parent path 59 | :local_path: full path of local dirname 60 | :original_path: full path of original dirname 61 | :tag: input help information 62 | """ 63 | 64 | up_to_date = True 65 | 66 | try: 67 | if os.path.exists(local_dir): 68 | _need_update = False 69 | for root, dirs, files in os.walk(original_dir): 70 | files = [f for f in files if not f.startswith(".")] 71 | dirs[:] = [d for d in dirs if not d.startswith(".")] 72 | rel_dir = os.path.relpath(root, original_dir) 73 | 74 | for fn in files: 75 | original_fn_md5 = get_md5(os.path.join(root, fn)) 76 | 77 | local_fn = os.path.join(local_dir, rel_dir, fn) 78 | if not os.path.exists(local_fn): 79 | _need_update = True 80 | break 81 | local_fn_md5 = get_md5(local_fn) 82 | if local_fn_md5 != original_fn_md5: 83 | _need_update = True 84 | break 85 | if _need_update: 86 | break 87 | 88 | if _need_update: 89 | up_to_date = False 90 | try: 91 | _ans = get_input('Overwrite {0} {1}? (y/N) ' 92 | .format(tag, dirname)) 93 | if _ans.lower() in yes_answer: 94 | shutil.rmtree(local_dir) 95 | copytree(original_dir, local_dir) 96 | except (KeyboardInterrupt, SystemExit): 97 | print() 98 | else: 99 | up_to_date = False 100 | try: 101 | _ans = get_input('New {0} {1}? (y/N) '.format(tag, dirname)) 102 | if _ans.lower() in yes_answer: 103 | copytree(original_dir, local_dir) 104 | except (KeyboardInterrupt, SystemExit): 105 | print() 106 | except Exception as e: 107 | logger.error(e) 108 | 109 | if up_to_date: 110 | logger.info('{0} {1} is already up to date.'.format(tag, dirname)) 111 | 112 | 113 | def update_builtin(**kwargs): 114 | """Update builtin scripts and themes under local site""" 115 | logger.info('Start updating builin files.') 116 | logger.warning('Update is risky, please make sure you have backups') 117 | 118 | # for fabfile.py 119 | _update_file( 120 | 'fabfile.py', 121 | os.getcwd(), 122 | os.path.join(os.path.dirname(__file__), 'conf_templates') 123 | ) 124 | 125 | # for optional Dockerfile 126 | if os.path.exists(os.path.join(os.getcwd(), 'Dockerfile')): 127 | _update_file( 128 | 'Dockerfile', 129 | os.getcwd(), 130 | os.path.join(os.path.dirname(__file__), 'conf_templates') 131 | ) 132 | 133 | # for themes 134 | original_themes = os.path.join(os.path.dirname(__file__), 'themes') 135 | local_themes = os.path.join(os.getcwd(), kwargs['themes_dir']) 136 | for theme in os.listdir(original_themes): 137 | if theme in ('simple',): # disabled/deprecated theme list 138 | continue 139 | local_theme = os.path.join(local_themes, theme) 140 | original_theme = os.path.join(original_themes, theme) 141 | _update_dir(theme, local_theme, original_theme, 'theme') 142 | -------------------------------------------------------------------------------- /simiki/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function, unicode_literals, absolute_import 4 | 5 | import os 6 | import sys 7 | import os.path 8 | import shutil 9 | import errno 10 | import logging 11 | import io 12 | import hashlib 13 | import simiki 14 | from simiki.compat import unicode 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | COLOR_CODES = { 19 | "reset": "\033[0m", 20 | "black": "\033[1;30m", 21 | "red": "\033[1;31m", 22 | "green": "\033[1;32m", 23 | "yellow": "\033[1;33m", 24 | "blue": "\033[1;34m", 25 | "magenta": "\033[1;35m", 26 | "cyan": "\033[1;36m", 27 | "white": "\033[1;37m", 28 | "bgred": "\033[1;41m", 29 | "bggrey": "\033[1;100m", 30 | } 31 | 32 | 33 | def color_msg(color, msg): 34 | return COLOR_CODES[color] + msg + COLOR_CODES["reset"] 35 | 36 | 37 | def check_extension(filename): 38 | """Check if the file extension is in the allowed extensions 39 | 40 | The `fnmatch` module can also get the suffix: 41 | patterns = ["*.md", "*.mkd", "*.markdown"] 42 | fnmatch.filter(files, pattern) 43 | """ 44 | exts = ['.{0}'.format(e) for e in simiki.allowed_extensions] 45 | return os.path.splitext(filename)[1] in exts 46 | 47 | 48 | def copytree(src, dst, symlinks=False, ignore=None): 49 | """Copy from source directory to destination""" 50 | 51 | if not os.path.exists(dst): 52 | os.makedirs(dst) 53 | for item in os.listdir(src): 54 | s = os.path.join(src, item) 55 | d = os.path.join(dst, item) 56 | if os.path.isdir(s): 57 | copytree(s, d, symlinks, ignore) 58 | else: 59 | shutil.copy2(s, d) 60 | 61 | 62 | def emptytree(directory, exclude_list=None): 63 | """Delete all the files and dirs under specified directory""" 64 | 65 | if not isinstance(directory, unicode): 66 | directory = unicode(directory, 'utf-8') 67 | if not exclude_list: 68 | exclude_list = [] 69 | for p in os.listdir(directory): 70 | if p in exclude_list: 71 | continue 72 | fp = os.path.join(directory, p) 73 | if os.path.isdir(fp): 74 | try: 75 | shutil.rmtree(fp) 76 | logger.debug("Delete directory %s", fp) 77 | except OSError as e: 78 | logger.error("Unable to delete directory %s: %s", 79 | fp, unicode(e)) 80 | elif os.path.isfile(fp): 81 | try: 82 | logging.debug("Delete file %s", fp) 83 | os.remove(fp) 84 | except OSError as e: 85 | logger.error("Unable to delete file %s: %s", fp, unicode(e)) 86 | else: 87 | logger.error("Unable to delete %s, unknown filetype", fp) 88 | 89 | 90 | def mkdir_p(path): 91 | """Make parent directories as needed, like `mkdir -p`""" 92 | try: 93 | os.makedirs(path) 94 | except OSError as exc: # Python >2.5 95 | # if dir exists, not error 96 | if exc.errno == errno.EEXIST and os.path.isdir(path): 97 | pass 98 | else: 99 | raise 100 | 101 | 102 | def listdir_nohidden(path): 103 | """List not hidden files or directories under path""" 104 | for f in os.listdir(path): 105 | if isinstance(f, str): 106 | f = unicode(f) 107 | if not f.startswith('.'): 108 | yield f 109 | 110 | 111 | def write_file(filename, content): 112 | """Write content to file.""" 113 | _dir, _ = os.path.split(filename) 114 | if not os.path.exists(_dir): 115 | logging.debug("The directory %s not exists, create it", _dir) 116 | mkdir_p(_dir) 117 | with io.open(filename, "wt", encoding="utf-8") as fd: 118 | fd.write(content) 119 | 120 | 121 | def get_md5(filename): 122 | # py3 require md5 with bytes object, otherwise raise 123 | # TypeError: Unicode-objects must be encoded before hashing 124 | with open(filename, 'rb') as fd: 125 | md5_hash = hashlib.md5(fd.read()).hexdigest() 126 | return md5_hash 127 | 128 | 129 | def get_dir_md5(dirname): 130 | """Get md5 sum of directory""" 131 | md5_hash = hashlib.md5() 132 | for root, dirs, files in os.walk(dirname): 133 | # os.walk use os.listdir and return arbitrary order list 134 | # sort list make it get same md5 hash value 135 | dirs[:] = sorted(dirs) 136 | for f in sorted(files): 137 | with open(os.path.join(root, f), 'rb') as fd: 138 | md5_hash.update(fd.read()) 139 | md5_hash = md5_hash.hexdigest() 140 | return md5_hash 141 | 142 | 143 | def import_string(import_name, silent=False): 144 | """Imports an object based on a string. This is useful if you want to 145 | use import paths as endpoints or something similar. An import path can 146 | be specified either in dotted notation (``xml.sax.saxutils.escape``) 147 | or with a colon as object delimiter (``xml.sax.saxutils:escape``). 148 | If `silent` is True the return value will be `None` if the import fails. 149 | :param import_name: the dotted name for the object to import. 150 | :param silent: if set to `True` import errors are ignored and 151 | `None` is returned instead. 152 | :return: imported object 153 | """ 154 | # ref: https://github.com/pallets/werkzeug/blob/master/werkzeug/utils.py 155 | 156 | # force the import name to automatically convert to strings 157 | # __import__ is not able to handle unicode strings in the fromlist 158 | # if the module is a package 159 | import_name = str(import_name).replace(':', '.') 160 | try: 161 | try: 162 | __import__(import_name) 163 | except ImportError: 164 | if '.' not in import_name: 165 | raise 166 | else: 167 | return sys.modules[import_name] 168 | 169 | module_name, obj_name = import_name.rsplit('.', 1) 170 | try: 171 | module = __import__(module_name, None, None, [obj_name]) 172 | except ImportError: 173 | # support importing modules not yet set up by the parent module 174 | # (or package for that matter) 175 | module = import_string(module_name) 176 | 177 | try: 178 | return getattr(module, obj_name) 179 | except AttributeError as e: 180 | raise ImportError(e) 181 | 182 | except ImportError as e: 183 | if not silent: 184 | raise ImportError(e) 185 | 186 | 187 | if __name__ == "__main__": 188 | print(color_msg("black", "Black")) 189 | print(color_msg("red", "Red")) 190 | print(color_msg("green", "Green")) 191 | print(color_msg("yellow", "Yellow")) 192 | print(color_msg("blue", "Blue")) 193 | print(color_msg("magenta", "Magenta")) 194 | print(color_msg("cyan", "Cyan")) 195 | print(color_msg("white", "White")) 196 | print(color_msg("bgred", "Background Red")) 197 | print(color_msg("bggrey", "Background Grey")) 198 | -------------------------------------------------------------------------------- /simiki/watcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function, absolute_import 4 | 5 | import os 6 | import logging 7 | import time 8 | from watchdog.observers import Observer 9 | from watchdog.events import PatternMatchingEventHandler 10 | import simiki 11 | from simiki.generators import PageGenerator, CatalogGenerator 12 | from simiki.utils import write_file 13 | 14 | _site_config = None 15 | _base_path = None 16 | 17 | 18 | def reload(func): 19 | """Fake watcher reload wrapper""" 20 | def wrapper(*args, **kwargs): 21 | try: 22 | func(*args, **kwargs) 23 | except Exception as e: 24 | logging.error('Watcher has error, reloading...') 25 | logging.debug(str(e)) 26 | return wrapper 27 | 28 | 29 | class YAPatternMatchingEventHandler(PatternMatchingEventHandler): 30 | """Observe .md files under content directory. 31 | Temporary only regenerate, not delete unused files""" 32 | patterns = ['*.{0}'.format(e) for e in simiki.allowed_extensions] 33 | 34 | @staticmethod 35 | def get_ofile(ifile): 36 | """get output filename from input filename""" 37 | category, filename = os.path.split(ifile) 38 | category = os.path.relpath(category, _site_config['source']) 39 | ofile = os.path.join( 40 | _base_path, 41 | _site_config['destination'], 42 | category, 43 | '{0}.html'.format(os.path.splitext(filename)[0]) 44 | ) 45 | return ofile 46 | 47 | @staticmethod 48 | def generate_page(_file): 49 | pg = PageGenerator(_site_config, _base_path) 50 | html = pg.to_html(_file) 51 | # ignore draft 52 | if not html: 53 | return None 54 | 55 | output_fname = YAPatternMatchingEventHandler.get_ofile(_file) 56 | write_file(output_fname, html) 57 | logging.debug('Regenerating: {0}'.format(_file)) 58 | 59 | @staticmethod 60 | def generate_catalog(): 61 | pg = PageGenerator(_site_config, _base_path) 62 | pages = {} 63 | 64 | for root, dirs, files in os.walk(_site_config["source"]): 65 | files = [f for f in files if not f.startswith(".")] 66 | dirs[:] = [d for d in dirs if not d.startswith(".")] 67 | for filename in files: 68 | if not filename.endswith(_site_config["default_ext"]): 69 | continue 70 | md_file = os.path.join(root, filename) 71 | pg.src_file = md_file 72 | meta, _ = pg.get_meta_and_content(do_render=False) 73 | pages[md_file] = meta 74 | 75 | cg = CatalogGenerator(_site_config, _base_path, pages) 76 | html = cg.generate_catalog_html() 77 | ofile = os.path.join( 78 | _base_path, 79 | _site_config['destination'], 80 | "index.html" 81 | ) 82 | write_file(ofile, html) 83 | logging.debug('Regenerating catalog') 84 | 85 | def process(self, event): 86 | if event.event_type in ('moved',): 87 | _file = event.dest_path 88 | else: 89 | _file = event.src_path 90 | 91 | # such as in vim, modified a file will trigger moved event to temp file 92 | if not _file.endswith(tuple(simiki.allowed_extensions)): 93 | return 94 | 95 | if event.event_type not in ('deleted',): 96 | self.generate_page(_file) 97 | 98 | self.generate_catalog() 99 | 100 | if event.event_type in ('moved', 'deleted'): 101 | # remove old output file 102 | ofile = self.get_ofile(event.src_path) 103 | if os.path.exists(ofile): 104 | os.remove(ofile) 105 | 106 | @reload 107 | def on_created(self, event): 108 | self.process(event) 109 | 110 | @reload 111 | def on_modified(self, event): 112 | self.process(event) 113 | 114 | @reload 115 | def on_moved(self, event): 116 | self.process(event) 117 | 118 | @reload 119 | def on_deleted(self, event): 120 | self.process(event) 121 | 122 | 123 | def watch(site_config, base_path): 124 | global _site_config, _base_path 125 | _site_config = site_config 126 | _base_path = base_path 127 | 128 | observe_path = os.path.join(_base_path, _site_config['source']) 129 | event_handler = YAPatternMatchingEventHandler() 130 | observer = Observer() 131 | observer.schedule(event_handler, observe_path, recursive=True) 132 | observer.start() 133 | try: 134 | while True: 135 | time.sleep(1) 136 | except (KeyboardInterrupt, SystemExit): 137 | logging.info("Shutting down watcher") 138 | observer.stop() 139 | observer.join() 140 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import with_statement, unicode_literals 4 | import os 5 | import logging 6 | 7 | logging.disable(logging.CRITICAL) 8 | os.environ['TEST_MODE'] = "true" # random string to indicate test mode enabled 9 | -------------------------------------------------------------------------------- /tests/mywiki_for_cli/attach/images/linux/opstools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tankywoo/simiki/22e544254577477c3f624c9d201f644580f36231/tests/mywiki_for_cli/attach/images/linux/opstools.png -------------------------------------------------------------------------------- /tests/mywiki_for_cli/content/intro/gettingstarted.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting Started" 3 | layout: page 4 | date: 2099-06-02 00:00 5 | --- 6 | 7 | # Simiki # 8 | 9 | [![Latest Version](http://img.shields.io/pypi/v/simiki.svg)](https://pypi.python.org/pypi/simiki) 10 | [![The MIT License](http://img.shields.io/badge/license-MIT-yellow.svg)](https://github.com/tankywoo/simiki/blob/master/LICENSE) 11 | [![Build Status](https://travis-ci.org/tankywoo/simiki.svg)](https://travis-ci.org/tankywoo/simiki) 12 | [![Coverage Status](https://img.shields.io/coveralls/tankywoo/simiki.svg)](https://coveralls.io/r/tankywoo/simiki) 13 | 14 | Simiki is a simple wiki framework, written in [Python](https://www.python.org/). 15 | 16 | * Easy to use. Creating a wiki only needs a few steps 17 | * Use [Markdown](http://daringfireball.net/projects/markdown/). Just open your editor and write 18 | * Store source files by category 19 | * Static HTML output 20 | * A CLI tool to manage the wiki 21 | 22 | Simiki is short for `Simple Wiki` :) 23 | 24 | ## Quick Start ## 25 | 26 | ### Install ### 27 | 28 | pip install simiki 29 | 30 | ### Update ### 31 | 32 | pip install -U simiki 33 | 34 | ### Init Site ### 35 | 36 | mkdir mywiki && cd mywiki 37 | simiki init 38 | 39 | ### Create a new wiki ### 40 | 41 | simiki new -t "Hello Simiki" -c first-catetory 42 | 43 | ### Generate ### 44 | 45 | simiki generate 46 | 47 | ### Preview ### 48 | 49 | simiki preview 50 | 51 | For more information, `simiki -h` or have a look at [Simiki.org](http://simiki.org) 52 | 53 | ## Others ## 54 | 55 | * [simiki.org](http://simiki.org) 56 | * 57 | * Email: 58 | 59 | ## License ## 60 | 61 | The MIT License (MIT) 62 | 63 | Copyright (c) 2013 Tanky Woo 64 | 65 | Permission is hereby granted, free of charge, to any person obtaining a copy of 66 | this software and associated documentation files (the "Software"), to deal in 67 | the Software without restriction, including without limitation the rights to 68 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 69 | the Software, and to permit persons to whom the Software is furnished to do so, 70 | subject to the following conditions: 71 | 72 | The above copyright notice and this permission notice shall be included in all 73 | copies or substantial portions of the Software. 74 | 75 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 76 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 77 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 78 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 79 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 80 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 81 | -------------------------------------------------------------------------------- /tests/mywiki_for_cli/content/intro/my_draft.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "My Draft" 3 | date: 2015 03-15 00:00 4 | draft: True 5 | --- 6 | 7 | A Draft. 8 | -------------------------------------------------------------------------------- /tests/mywiki_for_generator/content/foo目录/foo_page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Foo Page Intro" 3 | layout: page 4 | date: 2013-10-17 00:02 5 | --- 6 | 7 | Simiki is a simple wiki framework, written in Python. 8 | 9 | -------------------------------------------------------------------------------- /tests/mywiki_for_generator/content/foo目录/foo_page_get_meta_without_title.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2013-10-17 00:02 3 | --- 4 | 5 | Simiki is a simple wiki framework, written in Python. 6 | 7 | -------------------------------------------------------------------------------- /tests/mywiki_for_generator/content/foo目录/foo_page_get_meta_yaml_error.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Foo Page Intro" 3 | yaml 4 | error 5 | date: 2013-10-17 00:02 6 | --- 7 | 8 | Simiki is a simple wiki framework, written in Python. 9 | 10 | -------------------------------------------------------------------------------- /tests/mywiki_for_generator/content/foo目录/foo_page_layout_old_post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Foo Page Intro, Old Post" 3 | layout: post 4 | date: 2013-10-17 00:02 5 | --- 6 | 7 | Simiki is a simple wiki framework, written in Python. 8 | 9 | -------------------------------------------------------------------------------- /tests/mywiki_for_generator/content/foo目录/foo_page_layout_without_layout.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Foo Page Intro, Old Post" 3 | date: 2013-10-17 00:02 4 | --- 5 | 6 | Simiki is a simple wiki framework, written in Python. 7 | 8 | -------------------------------------------------------------------------------- /tests/mywiki_for_generator/content/foo目录/foo_page_中文.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Foo Page 2" 3 | layout: page 4 | date: 2013-10-17 00:03 5 | --- 6 | 7 | [[simiki]] 8 | 9 | Simiki is a simple wiki framework, written in Python. 10 | 11 | Line 1 12 | Line 2 13 | -------------------------------------------------------------------------------- /tests/mywiki_for_generator/content/foo目录/foo_page_中文_meta_error_1.md: -------------------------------------------------------------------------------- 1 | title: "Foo Page Intro, Meta Error 1" 2 | layout: page 3 | date: 2013-10-17 00:02 4 | --- 5 | 6 | Simiki is a simple wiki framework, written in Python. 7 | 8 | -------------------------------------------------------------------------------- /tests/mywiki_for_generator/content/foo目录/foo_page_中文_meta_error_2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Foo Page Intro, Meta Error 2" 3 | layout: page 4 | date: 2013-10-17 00:02 5 | 6 | Simiki is a simple wiki framework, written in Python. 7 | 8 | -------------------------------------------------------------------------------- /tests/mywiki_for_generator/expected_catalog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 |
21 |
22 |

Other 23 |

24 |
25 |
    26 |
  • 27 | Page 2 28 |
  • 29 |
  • 30 | 31 |
    32 |
    mycoll : 
    33 |
    34 | Page 1 /  35 | Page 3 36 |
    37 |
    38 |
  • 39 |
40 |
41 |
42 |
43 |
44 |
45 | 51 | 52 | 53 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/mywiki_for_generator/expected_output.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Foo Page 2 - 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 21 |
22 | 23 |
Foo Page 2
24 | 25 |

simiki

26 |

Simiki is a simple wiki framework, written in Python.

27 |

Line 1
28 | Line 2

29 |
30 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/mywiki_for_generator/expected_pages.json: -------------------------------------------------------------------------------- 1 | [{"name": "other", "pages": [{"layout": "page", "name": "page2", "title": "Page 2", "content": "", "fname": "page2.md", "date": "2016-06-02 00:00"}, {"name": "mycoll", "pages": [{"layout": "page", "name": "page1", "title": "Page 1", "collection": "mycoll", "content": "", "fname": "page1.md", "date": "2016-06-02 00:00"}, {"layout": "page", "name": "page3", "title": "Page 3", "collection": "mycoll", "content": "", "fname": "page3.md", "date": "2016-06-02 00:00"}]}]}] -------------------------------------------------------------------------------- /tests/mywiki_for_generator/expected_structure.json: -------------------------------------------------------------------------------- 1 | {"other": {"page1.md": {"layout": "page", "name": "page1", "title": "Page 1", "collection": "mycoll", "content": "", "fname": "page1.md", "date": "2016-06-02 00:00"}, "page2.md": {"content": "", "layout": "page", "name": "page2", "fname": "page2.md", "title": "Page 2", "date": "2016-06-02 00:00"}, "page3.md": {"layout": "page", "name": "page3", "title": "Page 3", "collection": "mycoll", "content": "", "fname": "page3.md", "date": "2016-06-02 00:00"}}} -------------------------------------------------------------------------------- /tests/mywiki_for_others/config_sample.yml: -------------------------------------------------------------------------------- 1 | # Document: 2 | # http://simiki.org/docs/configuration.html 3 | 4 | url: http://wiki.tankywoo.com/ 5 | title: 我的Wiki 6 | keywords: wiki, simiki, python, 维基 7 | description: This is a simiki's config sample, 测试样例 8 | author: Tanky Woo 9 | root: /wiki/ 10 | source: source 11 | destination: destination 12 | themes_dir: simiki_themes 13 | theme: mytheme 14 | default_ext: markdown 15 | pygments: True 16 | debug: True 17 | -------------------------------------------------------------------------------- /tests/mywiki_for_others/content/python/python文档.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Python文档" 3 | date: 2015-01-01 00:00 4 | --- 5 | 6 | # Python文档 # 7 | 8 | 链接: 9 | -------------------------------------------------------------------------------- /tests/mywiki_for_others/content/python/zen_of_python.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Zen of Python" 3 | date: 2015-01-02 00:00 4 | --- 5 | 6 | # The Zen of Python, by Tim Peters # 7 | 8 | Beautiful is better than ugly. 9 | Explicit is better than implicit. 10 | Simple is better than complex. 11 | Complex is better than complicated. 12 | Flat is better than nested. 13 | Sparse is better than dense. 14 | Readability counts. 15 | Special cases aren't special enough to break the rules. 16 | Although practicality beats purity. 17 | Errors should never pass silently. 18 | Unless explicitly silenced. 19 | In the face of ambiguity, refuse the temptation to guess. 20 | There should be one-- and preferably only one --obvious way to do it. 21 | Although that way may not be obvious at first unless you're Dutch. 22 | Now is better than never. 23 | Although never is often better than *right* now. 24 | If the implementation is hard to explain, it's a bad idea. 25 | If the implementation is easy to explain, it may be a good idea. 26 | Namespaces are one honking great idea -- let's do more of those! 27 | -------------------------------------------------------------------------------- /tests/mywiki_for_others/content/其它/.hidden.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tankywoo/simiki/22e544254577477c3f624c9d201f644580f36231/tests/mywiki_for_others/content/其它/.hidden.md -------------------------------------------------------------------------------- /tests/mywiki_for_others/content/其它/hello.txt: -------------------------------------------------------------------------------- 1 | say Hello 2 | -------------------------------------------------------------------------------- /tests/mywiki_for_others/content/其它/helloworld.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello World" 3 | date: 2015-01-03 00:00 4 | --- 5 | 6 | Hello World 7 | -------------------------------------------------------------------------------- /tests/mywiki_for_others/content/其它/维基.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "维基Wiki" 3 | date: 2015-01-01 12:00 4 | --- 5 | 6 | 一篇wiki 7 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function, with_statement, unicode_literals 4 | 5 | import os 6 | import os.path 7 | import shutil 8 | import unittest 9 | import io 10 | from copy import deepcopy 11 | 12 | from simiki import cli 13 | from simiki.utils import copytree, emptytree 14 | from simiki.config import get_default_config 15 | 16 | test_path = os.path.dirname(os.path.abspath(__file__)) 17 | base_path = os.path.dirname(test_path) 18 | 19 | INIT_ARGS = { 20 | u'--help': False, 21 | u'--version': False, 22 | u'-c': None, 23 | u'-f': None, 24 | u'-p': None, 25 | u'-t': None, 26 | u'--host': None, 27 | u'--port': None, 28 | u'-w': None, 29 | u'--draft': None, 30 | u'generate': False, 31 | u'g': False, 32 | u'init': False, 33 | u'new': False, 34 | u'n': False, 35 | u'preview': False, 36 | u'p': False 37 | } 38 | 39 | 40 | class TestCliInit(unittest.TestCase): 41 | def setUp(self): 42 | self.default_config = get_default_config() 43 | self.args = deepcopy(INIT_ARGS) 44 | self.target_path = "_build" 45 | 46 | if os.path.exists(self.target_path): 47 | shutil.rmtree(self.target_path) 48 | self.files = [ 49 | "_config.yml", 50 | "fabfile.py", 51 | os.path.join(self.default_config['source'], "intro", 52 | "gettingstarted.md"), 53 | os.path.join(self.default_config['themes_dir'], 54 | self.default_config['theme'], 55 | "page.html"), 56 | os.path.join(self.default_config['themes_dir'], 57 | self.default_config['theme'], 58 | "static", "css", "style.css") 59 | ] 60 | self.dirs = [ 61 | self.default_config['source'], 62 | self.default_config['destination'], 63 | self.default_config['themes_dir'], 64 | os.path.join(self.default_config['themes_dir'], 65 | self.default_config['theme']), 66 | ] 67 | 68 | def test_init(self): 69 | os.chdir(test_path) 70 | self.args.update({u'init': True, u'-p': self.target_path}) 71 | cli.main(self.args) 72 | for f in self.files: 73 | self.assertTrue(os.path.isfile(os.path.join(self.target_path, f))) 74 | 75 | for d in self.dirs: 76 | self.assertTrue(os.path.isdir(os.path.join(self.target_path, d))) 77 | 78 | def tearDown(self): 79 | if os.path.exists(self.target_path): 80 | shutil.rmtree(self.target_path) 81 | 82 | 83 | class TestCliGenerate(unittest.TestCase): 84 | def setUp(self): 85 | self.args = deepcopy(INIT_ARGS) 86 | self.wiki_path = os.path.join(test_path, "mywiki_for_cli") 87 | self.output_path = os.path.join(self.wiki_path, "output") 88 | 89 | if os.path.exists(self.output_path): 90 | emptytree(self.output_path) 91 | 92 | config_file_tpl = os.path.join(base_path, 'simiki', 93 | 'conf_templates', '_config.yml.in') 94 | self.config_file_dst = os.path.join(self.wiki_path, '_config.yml') 95 | shutil.copyfile(config_file_tpl, self.config_file_dst) 96 | 97 | s_themes_path = os.path.join(base_path, 'simiki', 'themes') 98 | self.d_themes_path = os.path.join(self.wiki_path, 'themes') 99 | if os.path.exists(self.d_themes_path): 100 | shutil.rmtree(self.d_themes_path) 101 | copytree(s_themes_path, self.d_themes_path) 102 | 103 | self.drafts = [ 104 | os.path.join(self.output_path, "intro", "my_draft.html") 105 | ] 106 | self.files = [ 107 | os.path.join(self.output_path, "index.html"), 108 | os.path.join(self.output_path, "intro", "gettingstarted.html") 109 | ] 110 | self.dirs = [ 111 | self.output_path, 112 | os.path.join(self.output_path, "intro"), 113 | ] 114 | self.attach = [ 115 | os.path.join(self.output_path, 'attach', 'images', 'linux', 116 | 'opstools.png'), 117 | ] 118 | self.static = [ 119 | os.path.join(self.output_path, "static", "css", "style.css"), 120 | ] 121 | os.chdir(self.wiki_path) 122 | 123 | def test_generate(self): 124 | self.args.update({u'generate': True}) 125 | cli.main(self.args) 126 | for f in self.drafts: 127 | self.assertFalse(os.path.isfile(os.path.join(self.wiki_path, f))) 128 | 129 | for f in self.files: 130 | self.assertTrue(os.path.isfile(os.path.join(self.wiki_path, f))) 131 | 132 | for d in self.dirs: 133 | self.assertTrue(os.path.isdir(os.path.join(self.wiki_path, d))) 134 | 135 | for f in self.attach: 136 | self.assertTrue(os.path.isdir(os.path.join(self.wiki_path, d))) 137 | 138 | for f in self.static: 139 | self.assertTrue(os.path.isdir(os.path.join(self.wiki_path, d))) 140 | 141 | def tearDown(self): 142 | os.remove(self.config_file_dst) 143 | if os.path.exists(self.d_themes_path): 144 | shutil.rmtree(self.d_themes_path) 145 | if os.path.exists(self.output_path): 146 | emptytree(self.output_path) 147 | 148 | 149 | class TestCliNewWiki(unittest.TestCase): 150 | def setUp(self): 151 | wiki_path = os.path.join(test_path, 'mywiki_for_others') 152 | config_file_tpl = os.path.join(base_path, 'simiki', 153 | 'conf_templates', '_config.yml.in') 154 | self.config_file_dst = os.path.join(wiki_path, '_config.yml') 155 | 156 | shutil.copyfile(config_file_tpl, self.config_file_dst) 157 | 158 | self.args = deepcopy(INIT_ARGS) 159 | self.title = "hello/simiki" 160 | self.category = os.path.join('my目录', 'sub-category') 161 | self.source_path = os.path.join(wiki_path, "content") 162 | self.odir = os.path.join(wiki_path, "content", self.category) 163 | self.odir_root = os.path.dirname(self.odir) 164 | 165 | os.chdir(wiki_path) 166 | if os.path.exists(self.odir_root): 167 | shutil.rmtree(self.odir_root) 168 | 169 | def test_new_wiki_without_file(self): 170 | ofile = os.path.join(self.odir, "hello-slash-simiki.md") 171 | 172 | self.args.update({u'new': True, u'-t': self.title, 173 | u'-c': self.category}) 174 | cli.main(self.args) 175 | self.assertTrue(os.path.isfile(ofile)) 176 | 177 | with io.open(ofile, "rt", encoding="utf-8") as fd: 178 | lines = fd.read().rstrip().splitlines() 179 | # Ignore date line 180 | lines[2] = u'' 181 | expected_lines = [u'---', u'title: "hello/simiki"', u'', u'---'] 182 | assert lines == expected_lines 183 | 184 | def tearDown(self): 185 | os.remove(self.config_file_dst) 186 | if os.path.exists(self.odir_root): 187 | shutil.rmtree(self.odir_root) 188 | 189 | 190 | if __name__ == "__main__": 191 | unittest.main() 192 | -------------------------------------------------------------------------------- /tests/test_generators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function, with_statement, unicode_literals 4 | 5 | import os 6 | import re 7 | import json 8 | import shutil 9 | import datetime 10 | import unittest 11 | 12 | from simiki.config import parse_config, get_default_config 13 | from simiki.utils import copytree 14 | from simiki.generators import PageGenerator, CatalogGenerator 15 | from simiki.compat import unicode 16 | 17 | test_path = os.path.dirname(os.path.abspath(__file__)) 18 | base_path = os.path.dirname(test_path) 19 | 20 | 21 | class TestPageGenerator(unittest.TestCase): 22 | def setUp(self): 23 | self.default_config = get_default_config() 24 | 25 | self.wiki_path = os.path.join(test_path, 'mywiki_for_generator') 26 | 27 | os.chdir(self.wiki_path) 28 | 29 | self.config_file = os.path.join(base_path, 'simiki', 30 | 'conf_templates', '_config.yml.in') 31 | 32 | self.config = parse_config(self.config_file) 33 | 34 | s_themes_path = os.path.join(base_path, 'simiki', 35 | self.default_config['themes_dir']) 36 | self.d_themes_path = os.path.join('./', 37 | self.default_config['themes_dir']) 38 | if os.path.exists(self.d_themes_path): 39 | shutil.rmtree(self.d_themes_path) 40 | copytree(s_themes_path, self.d_themes_path) 41 | 42 | self.generator = PageGenerator(self.config, self.wiki_path) 43 | 44 | def test_get_category_and_file(self): 45 | src_file = os.path.join(self.wiki_path, 'content', 'foo目录', 46 | 'foo_page_中文.md') 47 | self.generator.src_file = src_file 48 | category, filename = self.generator.get_category_and_file() 49 | self.assertEqual( 50 | (category, filename), 51 | (u'foo\u76ee\u5f55', u'foo_page_\u4e2d\u6587.md') 52 | ) 53 | 54 | def test_get_meta_and_content(self): 55 | src_file = os.path.join(self.wiki_path, 'content', 'foo目录', 56 | 'foo_page_中文.md') 57 | self.generator.src_file = src_file 58 | meta, content = self.generator.get_meta_and_content() 59 | expected_meta = {'date': '2013-10-17 00:03', 'layout': 'page', 60 | 'title': 'Foo Page 2', 'category': 'foo目录', 61 | 'filename': 'foo_page_中文.html'} 62 | self.assertEqual(meta, expected_meta) 63 | self.assertEqual(content, '

[[simiki]]

\n' 64 | '

Simiki is a simple wiki ' 65 | 'framework, written in Python.

' 66 | '\n

Line 1
\nLine 2

') 67 | 68 | # get meta notaion error 69 | src_file = os.path.join(self.wiki_path, 'content', 'foo目录', 70 | 'foo_page_中文_meta_error_1.md') 71 | self.generator.src_file = src_file 72 | self.assertRaises(Exception, self.generator.get_meta_and_content) 73 | 74 | src_file = os.path.join(self.wiki_path, 'content', 'foo目录', 75 | 'foo_page_中文_meta_error_2.md') 76 | self.generator.src_file = src_file 77 | self.assertRaises(Exception, self.generator.get_meta_and_content) 78 | 79 | def test_get_template_vars(self): 80 | src_file = os.path.join(self.wiki_path, 'content', 'foo目录', 81 | 'foo_page_中文.md') 82 | self.generator.src_file = src_file 83 | meta, content = self.generator.get_meta_and_content() 84 | template_vars = self.generator.get_template_vars(meta, content) 85 | expected_template_vars = { 86 | u'page': { 87 | u'category': u'foo\u76ee\u5f55', 88 | u'content': u'

[[simiki]]

\n' 89 | '

Simiki is a simple wiki ' 90 | 'framework, written in Python.

' 91 | '\n

Line 1
\nLine 2

', 92 | u'filename': u'foo_page_\u4e2d\u6587.html', 93 | u'date': '2013-10-17 00:03', 94 | u'layout': 'page', 95 | u'relation': [], 96 | u'title': 'Foo Page 2' 97 | }, 98 | u'site': get_default_config() 99 | } 100 | 101 | expected_template_vars['site'].update({'root': ''}) 102 | template_vars['site'].pop('time') 103 | expected_template_vars['site'].pop('time') 104 | assert template_vars == expected_template_vars 105 | 106 | def test_to_html(self): 107 | src_file = os.path.join(self.wiki_path, 'content', 'foo目录', 108 | 'foo_page_中文.md') 109 | html_generator_config = self.config 110 | html_generator_config["markdown_ext"] = {"wikilinks": None} 111 | html_generator_generator = PageGenerator(html_generator_config, 112 | self.wiki_path) 113 | html = html_generator_generator.to_html(src_file).strip() 114 | # trip page updated and site generated paragraph 115 | html = re.sub( 116 | '(?sm)\\n\s*Page Updated.*?<\/span>', 117 | '', html) 118 | html = re.sub('(?m)^\s*

Site Generated .*?<\/p>$\n', '', html) 119 | expected_output = os.path.join(self.wiki_path, 'expected_output.html') 120 | fd = open(expected_output, "rb") 121 | year = datetime.date.today().year 122 | expected_html = unicode(fd.read(), "utf-8") % year 123 | assert html.rstrip() == expected_html.rstrip() 124 | 125 | # load template error 126 | src_file = os.path.join(self.wiki_path, 'content', 'foo目录', 127 | 'foo_page_中文.md') 128 | self.assertRaises(Exception, PageGenerator, self.config, 129 | 'wrong_basepath', src_file) 130 | 131 | def test_get_layout(self): 132 | src_file = os.path.join(self.wiki_path, 'content', 'foo目录', 133 | 'foo_page_layout_old_post.md') 134 | self.generator.src_file = src_file 135 | meta, _ = self.generator.get_meta_and_content() 136 | 137 | layout = self.generator.get_layout(meta) 138 | self.assertEqual(layout, 'page') 139 | 140 | src_file = os.path.join(self.wiki_path, 'content', 'foo目录', 141 | 'foo_page_layout_without_layout.md') 142 | self.generator.src_file = src_file 143 | meta, _ = self.generator.get_meta_and_content() 144 | 145 | layout = self.generator.get_layout(meta) 146 | self.assertEqual(layout, 'page') 147 | 148 | def test_get_meta(self): 149 | src_file = os.path.join(self.wiki_path, 'content', 'foo目录', 150 | 'foo_page_get_meta_yaml_error.md') 151 | self.generator.src_file = src_file 152 | self.assertRaises(Exception, self.generator.get_meta_and_content) 153 | 154 | src_file = os.path.join(self.wiki_path, 'content', 'foo目录', 155 | 'foo_page_get_meta_without_title.md') 156 | self.generator.src_file = src_file 157 | self.assertRaises(Exception, self.generator.get_meta_and_content) 158 | 159 | def tearDown(self): 160 | if os.path.exists(self.d_themes_path): 161 | shutil.rmtree(self.d_themes_path) 162 | 163 | 164 | class TestCatalogGenerator(unittest.TestCase): 165 | def setUp(self): 166 | self.default_config = get_default_config() 167 | 168 | self.wiki_path = os.path.join(test_path, 'mywiki_for_generator') 169 | 170 | os.chdir(self.wiki_path) 171 | 172 | self.config_file = os.path.join(base_path, 'simiki', 173 | 'conf_templates', '_config.yml.in') 174 | 175 | self.config = parse_config(self.config_file) 176 | 177 | s_themes_path = os.path.join(base_path, 'simiki', 178 | self.default_config['themes_dir']) 179 | self.d_themes_path = os.path.join('./', 180 | self.default_config['themes_dir']) 181 | if os.path.exists(self.d_themes_path): 182 | shutil.rmtree(self.d_themes_path) 183 | copytree(s_themes_path, self.d_themes_path) 184 | 185 | self.pages = { 186 | 'content/other/page1.md': 187 | {'content': '', 188 | 'collection': 'mycoll', 189 | 'date': '2016-06-02 00:00', 190 | 'layout': 'page', 191 | 'title': 'Page 1'}, 192 | 'content/other/page2.md': 193 | {'content': '', 194 | 'date': '2016-06-02 00:00', 195 | 'layout': 'page', 196 | 'title': 'Page 2'}, 197 | 'content/other/page3.md': 198 | {'content': '', 199 | 'collection': 'mycoll', 200 | 'date': '2016-06-02 00:00', 201 | 'layout': 'page', 202 | 'title': 'Page 3'}, 203 | } 204 | 205 | self.generator = CatalogGenerator(self.config, self.wiki_path, 206 | self.pages) 207 | 208 | def test_get_template_vars(self): 209 | tpl_vars = self.generator.get_template_vars() 210 | with open(os.path.join(self.wiki_path, 211 | 'expected_pages.json'), 'r') \ 212 | as fd: 213 | expected_pages = json.load(fd) 214 | assert(tpl_vars['pages'] == expected_pages) 215 | 216 | with open(os.path.join(self.wiki_path, 217 | 'expected_structure.json'), 'r') \ 218 | as fd: 219 | expected_structure = json.load(fd) 220 | assert(tpl_vars['site']['structure'] == expected_structure) 221 | 222 | def test_to_catalog(self): 223 | catalog_html = self.generator.generate_catalog_html() 224 | # trip site generated paragraph 225 | catalog_html = re.sub('(?m)^\s*

Site Generated .*?<\/p>$\n', '', 226 | catalog_html) 227 | fd = open(os.path.join(self.wiki_path, 'expected_catalog.html'), "rb") 228 | year = datetime.date.today().year 229 | expected_html = unicode(fd.read(), "utf-8") % year 230 | assert catalog_html.rstrip() == expected_html.rstrip() 231 | 232 | 233 | if __name__ == "__main__": 234 | unittest.main() 235 | -------------------------------------------------------------------------------- /tests/test_initiator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import os.path 6 | import unittest 7 | import shutil 8 | 9 | from simiki.config import get_default_config 10 | from simiki.initiator import Initiator 11 | 12 | 13 | class TestInitiator(unittest.TestCase): 14 | 15 | def setUp(self): 16 | BASE_DIR = os.path.join(os.path.dirname(__file__), '..') 17 | self.default_config = get_default_config() 18 | self.config_file = os.path.join(BASE_DIR, "simiki", "conf_templates", 19 | "_config.yml.in") 20 | self.target_path = os.path.join(BASE_DIR, "tests", "_build") 21 | if os.path.exists(self.target_path): 22 | shutil.rmtree(self.target_path) 23 | self.files = [ 24 | "_config.yml", 25 | "fabfile.py", 26 | os.path.join(self.default_config['source'], "intro", 27 | "gettingstarted.md"), 28 | ] 29 | self.dirs = [ 30 | self.default_config['source'], 31 | self.default_config['destination'], 32 | self.default_config['themes_dir'], 33 | os.path.join(self.default_config['themes_dir'], 34 | self.default_config['theme']), 35 | ] 36 | 37 | def test_target_exist(self): 38 | """ test Initiator target path exist 39 | """ 40 | 41 | i = Initiator(self.config_file, self.target_path) 42 | i.init(ask=False) 43 | 44 | for f in self.files: 45 | self.assertTrue(os.path.isfile(os.path.join(self.target_path, f))) 46 | 47 | for d in self.dirs: 48 | self.assertTrue(os.path.isdir(os.path.join(self.target_path, d))) 49 | 50 | def test_target_invalid(self): 51 | """ test Initiator target path invalid, raise OSError 52 | """ 53 | 54 | target_error = "/foo/bar/why/not" 55 | i = Initiator(self.config_file, target_error) 56 | self.assertRaises(OSError, lambda: i.init()) 57 | 58 | def tearDown(self): 59 | if os.path.exists(self.target_path): 60 | shutil.rmtree(self.target_path) 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /tests/test_log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function, with_statement, unicode_literals 4 | import unittest 5 | import logging 6 | 7 | try: 8 | # py2 9 | from cStringIO import StringIO 10 | except ImportError: 11 | try: 12 | # py2 13 | from StringIO import StringIO 14 | except ImportError: 15 | # py3 16 | # from io import BytesIO as StringIO 17 | from io import StringIO as StringIO 18 | 19 | import nose 20 | 21 | from simiki.utils import color_msg 22 | from simiki.log import logging_init 23 | from simiki.compat import is_py2, unicode 24 | 25 | 26 | class TestLogInit(unittest.TestCase): 27 | 28 | def setUp(self): 29 | logging.disable(logging.NOTSET) 30 | self.stream = StringIO() 31 | self.logger = logging.getLogger() 32 | self.handler = logging.StreamHandler(self.stream) 33 | for handler in self.logger.handlers: 34 | # exclude nosetest capture handler 35 | if not isinstance(handler, 36 | nose.plugins.logcapture.MyMemoryHandler): 37 | self.logger.removeHandler(handler) 38 | logging_init(level=logging.DEBUG, handler=self.handler) 39 | 40 | def test_logging_init(self): 41 | l2c = { 42 | "debug": "blue", 43 | "info": "green", 44 | "warning": "yellow", 45 | "error": "red", 46 | "critical": "bgred" 47 | } 48 | for level in l2c: 49 | # self.handler.flush() 50 | self.stream.truncate(0) 51 | # in python 3.x, truncate(0) would not change the current file pos 52 | # via 53 | self.stream.seek(0) 54 | func = getattr(self.logger, level) 55 | func(level) 56 | expected_output = "[{0}]: {1}" \ 57 | .format(color_msg(l2c[level], level.upper()), level) 58 | stream_output = self.stream.getvalue().strip() 59 | if is_py2: 60 | stream_output = unicode(stream_output) 61 | self.assertEqual(stream_output, expected_output) 62 | 63 | def tearDown(self): 64 | logging.disable(logging.CRITICAL) 65 | for handler in self.logger.handlers: 66 | if not isinstance(handler, 67 | nose.plugins.logcapture.MyMemoryHandler): 68 | self.logger.removeHandler(handler) 69 | 70 | 71 | if __name__ == "__main__": 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /tests/test_parse_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals 4 | import os.path 5 | import unittest 6 | import datetime 7 | from simiki.config import parse_config, get_default_config 8 | 9 | test_path = os.path.dirname(os.path.abspath(__file__)) 10 | 11 | 12 | class TestParseConfig(unittest.TestCase): 13 | def setUp(self): 14 | wiki_path = os.path.join(test_path, 'mywiki_for_others') 15 | 16 | self.expected_config = get_default_config() 17 | self.expected_config.update({ 18 | "author": "Tanky Woo", 19 | "debug": True, 20 | "default_ext": "markdown", 21 | "description": "This is a simiki's config sample," 22 | " \u6d4b\u8bd5\u6837\u4f8b", 23 | "destination": "destination", 24 | "keywords": "wiki, simiki, python, \u7ef4\u57fa", 25 | "root": "/wiki/", 26 | "source": "source", 27 | "attach": "attach", 28 | "theme": "mytheme", 29 | "themes_dir": "simiki_themes", 30 | "title": "\u6211\u7684Wiki", 31 | "url": "http://wiki.tankywoo.com" 32 | }) 33 | self.config_file = os.path.join(wiki_path, "config_sample.yml") 34 | 35 | def test_parse_config(self): 36 | config = parse_config(self.config_file) 37 | self.expected_config.pop('time') 38 | _date = config.pop('time') 39 | if hasattr(unittest.TestCase, 'assertIsInstance'): 40 | self.assertIsInstance(_date, datetime.datetime) 41 | else: 42 | assert isinstance(_date, datetime.datetime), \ 43 | '%s is not an instance of %r' % \ 44 | (repr(_date), datetime.datetime) 45 | self.assertEqual( 46 | config, 47 | self.expected_config 48 | ) 49 | 50 | def test_parse_config_not_exist(self): 51 | not_exist_config_file = os.path.join(self.config_file, "not_exist") 52 | self.assertRaises(Exception, 53 | lambda: parse_config(not_exist_config_file)) 54 | 55 | 56 | if __name__ == "__main__": 57 | unittest.main() 58 | -------------------------------------------------------------------------------- /tests/test_updater.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function, unicode_literals 4 | import os 5 | import os.path 6 | import shutil 7 | import unittest 8 | try: 9 | from unittest.mock import patch 10 | except ImportError: 11 | from mock import patch 12 | from simiki import utils, updater 13 | from simiki.config import get_default_config 14 | 15 | test_path = os.path.dirname(os.path.abspath(__file__)) 16 | base_path = os.path.dirname(test_path) 17 | 18 | 19 | class TestUpdater(unittest.TestCase): 20 | 21 | def setUp(self): 22 | self.default_config = get_default_config() 23 | 24 | self.wiki_path = os.path.join(test_path, 'mywiki_for_others') 25 | os.chdir(self.wiki_path) 26 | 27 | self.kwargs = {'themes_dir': 'themes'} 28 | self.original_fabfile = os.path.join(base_path, 'simiki', 29 | 'conf_templates', 'fabfile.py') 30 | self.local_fabfile = os.path.join(self.wiki_path, 'fabfile.py') 31 | 32 | self.original_theme = os.path.join(base_path, 'simiki', 33 | self.default_config['themes_dir'], 34 | self.default_config['theme']) 35 | self.local_theme = os.path.join(self.wiki_path, 36 | self.default_config['themes_dir'], 37 | self.default_config['theme']) 38 | 39 | self.local_theme_afile = os.path.join(self.local_theme, 'base.html') 40 | 41 | @patch('simiki.updater.get_input', return_value='yes') 42 | def test_update_builtin_not_exists_with_yes(self, mock_input): 43 | self.assertFalse(os.path.exists(self.local_fabfile)) 44 | self.assertFalse(os.path.exists(self.local_theme)) 45 | 46 | updater.update_builtin(**self.kwargs) 47 | 48 | original_fn_md5 = utils.get_md5(self.original_fabfile) 49 | local_fn_md5 = utils.get_md5(self.local_fabfile) 50 | self.assertEqual(original_fn_md5, local_fn_md5) 51 | 52 | original_fn_md5 = utils.get_dir_md5(self.original_theme) 53 | local_fn_md5 = utils.get_dir_md5(self.local_theme) 54 | self.assertEqual(original_fn_md5, local_fn_md5) 55 | 56 | os.remove(self.local_theme_afile) 57 | updater.update_builtin(**self.kwargs) 58 | 59 | original_fn_md5 = utils.get_dir_md5(self.original_theme) 60 | local_fn_md5 = utils.get_dir_md5(self.local_theme) 61 | self.assertEqual(original_fn_md5, local_fn_md5) 62 | 63 | @patch('simiki.updater.get_input', return_value='no') 64 | def test_update_builtin_not_exists_with_no(self, mock_input): 65 | self.assertFalse(os.path.exists(self.local_fabfile)) 66 | self.assertFalse(os.path.exists(self.local_theme)) 67 | 68 | updater.update_builtin(**self.kwargs) 69 | 70 | self.assertFalse(os.path.exists(self.local_fabfile)) 71 | self.assertFalse(os.path.exists(self.local_theme)) 72 | 73 | @patch('simiki.updater.get_input', return_value='yes') 74 | def test_update_builtin_exists_with_yes(self, mock_input): 75 | # empty fabfile.py 76 | with open(self.local_fabfile, 'wb') as _fd: 77 | _fd.close() 78 | original_fn_md5 = utils.get_md5(self.original_fabfile) 79 | local_fn_md5 = utils.get_md5(self.local_fabfile) 80 | self.assertNotEqual(original_fn_md5, local_fn_md5) 81 | 82 | utils.copytree(self.original_theme, self.local_theme) 83 | with open(self.local_theme_afile, 'wb') as _fd: 84 | _fd.close() 85 | original_fn_md5 = utils.get_dir_md5(self.original_theme) 86 | local_fn_md5 = utils.get_dir_md5(self.local_theme) 87 | self.assertNotEqual(original_fn_md5, local_fn_md5) 88 | 89 | updater.update_builtin(**self.kwargs) 90 | 91 | original_fn_md5 = utils.get_md5(self.original_fabfile) 92 | local_fn_md5 = utils.get_md5(self.local_fabfile) 93 | self.assertEqual(original_fn_md5, local_fn_md5) 94 | 95 | original_fn_md5 = utils.get_dir_md5(self.original_theme) 96 | local_fn_md5 = utils.get_dir_md5(self.local_theme) 97 | self.assertEqual(original_fn_md5, local_fn_md5) 98 | 99 | @patch('simiki.updater.get_input', return_value='no') 100 | def test_update_builtin_exists_with_no(self, mock_input): 101 | # empty fabfile.py 102 | with open(self.local_fabfile, 'wb') as _fd: 103 | _fd.close() 104 | original_fn_md5 = utils.get_md5(self.original_fabfile) 105 | local_fn_md5 = utils.get_md5(self.local_fabfile) 106 | self.assertNotEqual(original_fn_md5, local_fn_md5) 107 | 108 | utils.copytree(self.original_theme, self.local_theme) 109 | with open(self.local_theme_afile, 'wb') as _fd: 110 | _fd.close() 111 | original_fn_md5 = utils.get_dir_md5(self.original_theme) 112 | local_fn_md5 = utils.get_dir_md5(self.local_theme) 113 | self.assertNotEqual(original_fn_md5, local_fn_md5) 114 | 115 | updater.update_builtin(**self.kwargs) 116 | 117 | original_fn_md5 = utils.get_md5(self.original_fabfile) 118 | local_fn_md5 = utils.get_md5(self.local_fabfile) 119 | self.assertNotEqual(original_fn_md5, local_fn_md5) 120 | 121 | original_fn_md5 = utils.get_dir_md5(self.original_theme) 122 | local_fn_md5 = utils.get_dir_md5(self.local_theme) 123 | self.assertNotEqual(original_fn_md5, local_fn_md5) 124 | 125 | def test_update_builtin_up_to_date(self): 126 | shutil.copyfile(self.original_fabfile, self.local_fabfile) 127 | utils.copytree(self.original_theme, self.local_theme) 128 | 129 | updater.update_builtin(**self.kwargs) 130 | 131 | original_fn_md5 = utils.get_md5(self.original_fabfile) 132 | local_fn_md5 = utils.get_md5(self.local_fabfile) 133 | self.assertEqual(original_fn_md5, local_fn_md5) 134 | 135 | original_fn_md5 = utils.get_dir_md5(self.original_theme) 136 | local_fn_md5 = utils.get_dir_md5(self.local_theme) 137 | self.assertEqual(original_fn_md5, local_fn_md5) 138 | 139 | def tearDown(self): 140 | if os.path.exists(self.local_fabfile): 141 | os.remove(self.local_fabfile) 142 | if os.path.exists(self.local_theme): 143 | shutil.rmtree(os.path.dirname(self.local_theme)) 144 | 145 | 146 | if __name__ == '__main__': 147 | unittest.main() 148 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function, unicode_literals 4 | import os 5 | import os.path 6 | import unittest 7 | from simiki import utils 8 | 9 | test_path = os.path.dirname(os.path.abspath(__file__)) 10 | 11 | 12 | class TestUtils(unittest.TestCase): 13 | 14 | def setUp(self): 15 | wiki_path = os.path.join(test_path, 'mywiki_for_others') 16 | os.chdir(wiki_path) 17 | self.content = 'content' 18 | self.output = 'output' 19 | if os.path.exists(self.output): 20 | utils.emptytree(self.output) 21 | os.rmdir(self.output) 22 | 23 | def test_check_extension(self): 24 | valid_files = ['/tmp/file1.md', '/tmp/file2.mkd', '/tmp/文件3.mdown', 25 | '/tmp/文件4.markdown', '/tmp/目录/文件5.md'] 26 | for f in valid_files: 27 | assert utils.check_extension(f) 28 | 29 | invalid_files = ['/tmp/file6.txt', '/tmp/目录/文件7.mkdown'] 30 | for f in invalid_files: 31 | assert not utils.check_extension(f) 32 | 33 | def test_copytree_common(self): 34 | utils.copytree(self.content, self.output) 35 | assert os.path.exists(self.output) and os.path.isdir(self.output) 36 | 37 | files = [ 38 | os.path.join('python', 'zen_of_python.md'), 39 | os.path.join('python', 'python文档.md'), 40 | os.path.join('其它', 'helloworld.markdown'), 41 | os.path.join('其它', '维基.md'), 42 | os.path.join('其它', 'hello.txt'), 43 | os.path.join('其它', '.hidden.md'), 44 | ] 45 | for f in files: 46 | assert os.path.exists(os.path.join(self.output, f)) 47 | 48 | def test_copytree_symlink(self): 49 | '''temp not support''' 50 | pass 51 | 52 | def test_copytree_ignore(self): 53 | '''temp not support''' 54 | pass 55 | 56 | def test_emptytree(self): 57 | utils.copytree(self.content, self.output) 58 | utils.emptytree(self.output) 59 | assert not os.listdir(self.output) 60 | 61 | def test_mkdir_p(self): 62 | path = os.path.join(self.content) 63 | utils.mkdir_p(path) 64 | assert os.path.exists(path) 65 | 66 | path = os.path.join(self.output, "dir1/dir2/dir3") 67 | utils.mkdir_p(path) 68 | assert os.path.exists(path) 69 | 70 | # test path exist, and not a directory 71 | path = os.path.join(self.content, '其它', 'hello.txt') 72 | self.assertRaises(OSError, lambda: utils.mkdir_p(path)) 73 | 74 | def test_listdir_nohidden(self): 75 | fs = utils.listdir_nohidden(os.path.join(self.content, '其它')) 76 | expected_listdir = ['hello.txt', 'helloworld.markdown', '维基.md'] 77 | assert sorted(list(fs)) == sorted(expected_listdir) 78 | 79 | def test_get_md5(self): 80 | test_file = os.path.join(self.content, 'python', 'zen_of_python.md') 81 | self.assertEqual('d6e211679cb75b24c4e62fb233483fea', 82 | utils.get_md5(test_file)) 83 | 84 | def test_get_dir_md5(self): 85 | test_dir = os.path.join(self.content, 'python') 86 | self.assertEqual('ab2bf30fc9b8ead85e52fd19d02a819e', 87 | utils.get_dir_md5(test_dir)) 88 | 89 | def tearDown(self): 90 | if os.path.exists(self.output): 91 | utils.emptytree(self.output) 92 | os.rmdir(self.output) 93 | 94 | 95 | if __name__ == '__main__': 96 | unittest.main() 97 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py33,py34,py35,py36 3 | 4 | [flake8] 5 | # note flake8 will read tox.ini and this will overwrite default ignore list 6 | ignore = W605,E722,E731 7 | 8 | [testenv] 9 | deps = 10 | -rrequirements.txt 11 | -rdev_requirements.txt 12 | commands = 13 | nosetests 14 | flake8 --version 15 | flake8 simiki/ tests/ 16 | --------------------------------------------------------------------------------