├── .gitattributes ├── .gitignore ├── .python-version ├── .vscode └── extensions.json ├── ENV ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── pages ├── HomePage │ └── index.md ├── blog │ ├── 2015 │ │ ├── 01 │ │ │ └── 17 │ │ │ │ └── 2210 │ │ │ │ └── index.md │ │ ├── 03 │ │ │ └── 07 │ │ │ │ └── 2330 │ │ │ │ └── index.md │ │ └── 05 │ │ │ └── 17 │ │ │ └── 1130 │ │ │ ├── index.md │ │ │ └── indexer.png │ ├── 2016 │ │ └── 03 │ │ │ └── 19 │ │ │ └── 1900 │ │ │ └── index.md │ ├── 2023 │ │ └── 09 │ │ │ └── 03 │ │ │ └── 1800 │ │ │ └── index.md │ └── 2024 │ │ ├── 03 │ │ └── 24 │ │ │ └── 2135 │ │ │ └── index.md │ │ └── 09 │ │ └── 05 │ │ └── 2322 │ │ └── index.md ├── docs │ ├── basics │ │ ├── image.png │ │ └── index.md │ ├── content │ │ └── index.md │ ├── index.md │ └── internals │ │ ├── hy.png │ │ └── index.md ├── meta │ ├── About │ │ └── index.md │ ├── Acronyms │ │ └── index.txt │ ├── Aliases │ │ └── index.txt │ ├── AllPages │ │ └── index.txt │ ├── Archives │ │ └── index.txt │ ├── EmptyPage │ │ └── index.textile │ ├── Highlights │ │ └── index.txt │ ├── HitMap │ │ └── index.md │ ├── Index │ │ └── index.md │ ├── InterWikiMap │ │ └── index.txt │ ├── LatestPosts │ │ └── index.txt │ ├── MostPopular │ │ └── index.txt │ ├── Quotes │ │ └── index.txt │ ├── RecentUpdates │ │ └── index.txt │ ├── RecentlyVisited │ │ └── index.txt │ ├── Referrers │ │ └── index.txt │ ├── Search │ │ └── index.txt │ ├── WantedPages │ │ └── index.txt │ └── Wikipedia │ │ └── index.txt ├── site │ └── about │ │ └── index.md └── tests │ ├── acronyms │ └── index.md │ ├── aliases │ └── index.md │ ├── callouts │ └── index.md │ ├── footnotes │ └── index.md │ ├── highlight │ ├── animate_svg.js │ └── index.md │ ├── img │ ├── gradient.png │ └── index.md │ ├── index.md │ ├── interwiki │ └── index.md │ └── markup │ ├── ipynb │ └── index.ipynb │ ├── jekyll │ └── index.md │ └── rst │ └── index.rst ├── requirements-dev.txt ├── requirements.txt ├── sushy ├── __init__.py ├── __main__.py ├── aliasing.hy ├── app.hy ├── config.hy ├── favicons.hy ├── feeds.hy ├── indexer.hy ├── messages.hy ├── models.py ├── plugins.hy ├── render.hy ├── routes.hy ├── store.hy ├── transform.hy ├── utils.hy └── wsgi.py ├── themes ├── blog │ ├── static │ │ ├── css │ │ │ ├── custom.css │ │ │ ├── lanyon.css │ │ │ ├── poole.css │ │ │ ├── rss.css │ │ │ └── syntax.css │ │ ├── img │ │ │ ├── avatars │ │ │ │ ├── unknown200x200.png │ │ │ │ ├── unknown36x36.png │ │ │ │ └── unknown80x80.png │ │ │ └── placeholder.png │ │ ├── js │ │ │ ├── app.js │ │ │ ├── debug.js │ │ │ ├── eventsource.js │ │ │ ├── radio.js │ │ │ ├── utils.js │ │ │ └── zepto.min.js │ │ └── root │ │ │ ├── apple-touch-icon-precomposed.png │ │ │ ├── apple-touch-icon.png │ │ │ └── favicon.ico │ └── views │ │ ├── atom.tpl │ │ ├── blog.tpl │ │ ├── common.tpl │ │ ├── custom_ads.tpl │ │ ├── custom_footer.tpl │ │ ├── custom_meta.tpl │ │ ├── debug.tpl │ │ ├── inline-message.tpl │ │ ├── inline-table.tpl │ │ ├── layout.tpl │ │ ├── metadata.tpl │ │ ├── opensearch.tpl │ │ ├── prevnext.tpl │ │ ├── robots.tpl │ │ ├── search.tpl │ │ ├── seealso.tpl │ │ ├── sidebar.tpl │ │ ├── sitemap.tpl │ │ └── wiki.tpl └── wiki │ ├── static │ ├── css │ │ ├── icons.css │ │ ├── rss.css │ │ ├── site.css │ │ └── syntax.css │ ├── favicon.ico │ └── js │ │ ├── app.js │ │ ├── debug.js │ │ ├── eventsource.js │ │ ├── radio.js │ │ └── utils.js │ └── views │ ├── debug.tpl │ ├── inline-message.tpl │ ├── layout.tpl │ ├── opensearch.tpl │ ├── robots.tpl │ ├── rss.tpl │ ├── search.tpl │ ├── seealso.tpl │ ├── sitemap.tpl │ └── wiki.tpl ├── tools └── gprof2dot.py └── uwsgi.ini /.gitattributes: -------------------------------------------------------------------------------- 1 | *.tpl linguist-language=html eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .zip 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ 57 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12.3 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python", 4 | "ms-python.vscode-pylance", 5 | "ms-python.debugpy", 6 | "hylang.vscode-hy-official" 7 | ] 8 | } -------------------------------------------------------------------------------- /ENV: -------------------------------------------------------------------------------- 1 | DEBUG=False 2 | PROFILING=False 3 | SERVER_NAME=localhost 4 | CONTENT_PATH=pages 5 | THEME_PATH=themes/blog 6 | DATABASE_PATH=/tmp/sushy.db 7 | SITE_NAME=Sushy 8 | SITE_DESCRIPTION=The little squid that could 9 | PYTHONIOENCODING=UTF_8:replace 10 | LC_ALL=en_US.UTF-8 11 | LANG=en_US.UTF-8 12 | TIMEZONE=Europe/Lisbon 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Rui Carmo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Set these if not defined already 2 | export BIND_ADDRESS?=0.0.0.0 3 | export PORT?=8080 4 | export DEBUG?=False 5 | export PROFILER?=False 6 | export CONTENT_PATH?=pages 7 | export THEME_PATH?=themes/blog 8 | export DATABASE_PATH?=/tmp/sushy.db 9 | export SITE_NAME?=Sushy 10 | export PYTHONIOENCODING=UTF_8:replace 11 | export LC_ALL=en_US.UTF-8 12 | export LANG=en_US.UTF-8 13 | export CURRENT_GIT_BRANCH?=`git symbolic-ref --short HEAD` 14 | 15 | .DEFAULT_GOAL := help 16 | 17 | # Experimental zip bundle 18 | BUNDLE=sushy.zip 19 | export PYTHONPATH=$(BUNDLE) 20 | 21 | # Source code 22 | HYFILES=$(wildcard sushy/*.hy) 23 | PYFILES=$(wildcard sushy/*.py) 24 | BYTECODE=$(HYFILES:.hy=.pyc) 25 | PYTHONCODE=$(HYFILES:.hy=.py) 26 | PROFILES=$(wildcard *.pstats) 27 | CALL_DIAGRAMS=$(PROFILES:.pstats=.png) 28 | 29 | repl: ## Start a Hy REPL 30 | hy -i "(import sushy.app)" 31 | 32 | deps: ## Install Dependencies 33 | pip install -U --break-system-packages -r requirements.txt 34 | 35 | deps-upgrade: ## Interactively upgrade requirements.txt 36 | pip-upgrade --skip-package-installation --skip-virtualenv-check requirements.txt 37 | 38 | clean: ## Clean environment 39 | rm -f *.zip 40 | rm -rf sushy/__pycache__ 41 | rm -f $(BYTECODE) 42 | rm -f $(PYTHONCODE) 43 | rm -f $(DATABASE_PATH)* 44 | 45 | %.pyc: %.hy ## Turn Hy files into bytecode so that we can use a standard Python interpreter 46 | hyc $< 47 | 48 | %.py: %.hy ## Turn Hy files into Python source so that PyPy will (eventually) be happy 49 | hy2py $< > $@ 50 | 51 | build: $(BYTECODE) 52 | 53 | bundle: $(HYFILES) $(PYFILES) ## Experimental bundle to see if we can deploy this solely as a ZIP file 54 | zip -r9 $(BUNDLE) sushy/* -i *.py *.pyc 55 | rm -f sushy/*.pyc 56 | 57 | serve: ## Run with the embedded web server 58 | hy -m sushy.app 59 | 60 | uwsgi: build ## Run with uwsgi 61 | uwsgi --http :$(PORT) --python-path . --wsgi sushy.app --callable app -p 1 62 | 63 | uwsgi-ini: build ## Run with uwsgi 64 | uwsgi --ini uwsgi.ini 65 | 66 | index: ## Run indexer 67 | hy -m sushy.indexer 68 | 69 | index-watch: ## Run indexer and watch for changes 70 | hy -m sushy.indexer watch 71 | 72 | %.png: %.pstats ## Render pstats profiler files into nice PNGs (requires dot) 73 | python tools/gprof2dot.py -f pstats $< | dot -Tpng -o $@ 74 | 75 | profile: $(CALL_DIAGRAMS) ## Render profile 76 | 77 | debug-%: ; @echo $*=$($*) 78 | 79 | restart-production: ## Restart production Piku instance 80 | ssh piku@piku restart sushy 81 | 82 | deploy-production: ## Push to production Piku instance 83 | git push production master 84 | 85 | reset-production: ## Destroy production instance 86 | ssh piku@piku destroy sushy 87 | 88 | redeploy: reset-production deploy-production restart-production ## Redeploy 89 | 90 | deploy: deploy-production restart-production ## Deploy 91 | 92 | help: 93 | @grep -hE '^[A-Za-z0-9_ \-]*?:.*##.*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 94 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | # for Piku 2 | wsgi: sushy.wsgi:app 3 | worker: hy -m sushy.indexer watch 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sushy 2 | 3 | A wiki/blogging engine with a static file back-end, full-text indexing and multiple markup support. 4 | 5 | This was formerly the site engine for [`taoofmac.com`](https://taoofmac.com) [circa 2015](https://taoofmac.com/space/blog/2015/02/01/1930) until I decided to [switch back](https://taoofmac.com/space/blog/2018/07/08/1330) to pure Python for maintainability. 6 | 7 | ## Status 8 | 9 | Many years later, I've decided to at least clean up the legacy codebase and bring it up to date. Once done, it should again be deployable to [`piku`][piku]/[Dokku-alt][da]/[Dokku][dokku]/[Heroku][heroku]. 10 | 11 | The goal is to make it run on the 2023 1.0.0 version of Hylang, which was finally released in September 22nd 2024. 12 | 13 | ### Roadmap 14 | 15 | * [ ] Add a new layout and CSS (the current one is a bit dated) 16 | * [ ] Switch as much as possible to `aiohttp` so we can leverage `uvloop` fully. 17 | * [x] Fix all the various breaking syntax changes that the `Hy` project has gone through in the past few years (I keep resetting this one because they keep changing things) 18 | * [ ] A little more documentation (there's never enough) 19 | * [ ] Blog archive and partial feature parity with the current `taoofmac.com` site engine 20 | * [ ] End-to-end syntax and linting checks 21 | * [ ] Fix link and image handling, which require some tweaks 22 | * [x] Working decorators and HTTP serving with the 2023 versions of `Hy` 23 | * [x] Removed `*earmuffs*` in favor of standard Python constants, because `Hy` now handles those differently 24 | * [x] (Mostly) working indexing with the 2023 versions of `Hy` 25 | * [x] Page aliasing (i.e., multiple URLs for a page) 26 | * [x] Image thumbnailing 27 | * [x] Friendlier search results 28 | * [x] More CSS tweaks 29 | * [x] Atom feeds 30 | * [x] [`piku`][piku] deployment 31 | * [x] Blog homepage/prev-next navigation 32 | * [x] Preliminary support for rendering IPython notebooks 33 | * [x] Closest-match URLs (i.e., fix typos) (removed for performance concerns on large sites) 34 | * [x] HTTP caching (`Etag`, `Last-Modified`, `HEAD` support, etc.) 35 | * [x] Sitemap 36 | * [x] OpenSearch support (search directly from the omnibar on some browsers) 37 | * [x] CSS inlining for Atom feeds 38 | * [x] `multiprocessing`-based indexer (in `feature/multiprocessing`, disabled for ease of profiling) 39 | * [x] SSE (Server-Sent Events) support (in `feature/server-events`) for notifying visitors a page has changed 40 | * [x] [New Relic][nr] Support 41 | * [x] Internal link tracking (`SeeAlso` functionality, as seen on [Yaki][y]) 42 | * [x] Multiple theme support (only the one theme for now) 43 | * [x] Automatic insertion of image sizes in `img` tags 44 | * [x] Deployable under [Dokku-alt][da] 45 | * [x] Run under [uWSGI][uwsgi] using `gevent` workers 46 | * [x] Full-text indexing and search 47 | * [x] Syntax highlighting for inline code samples 48 | * [x] [Ink][ink]-based site layout and templates (replaced by a new layout in the `feature/blog` branch) 49 | * [x] Baseline markup rendering (Textile, Markdown and ReST) 50 | 51 | ### Stuff that will never happen: 52 | 53 | * Site thumbnailing (for taking screenshots of external links) - moved to a separate app 54 | * Web-based UI for editing pages (you're supposed to do this out-of-band) 55 | * Revision history (you're supposed to manage your content with [Dropbox][db] or `git`) 56 | * Comment support 57 | 58 | --- 59 | 60 | # Principles of Operation 61 | 62 | * All your Textile, Markdown or ReStructured Text content lives in a filesystem tree, with a folder per page 63 | * Sushy grabs and renders those on demand with fine-tuned HTTP headers (this is independently of whether or not you put Varnish or CloudFlare in front for caching) 64 | * It also maintains a SQLite database with a full-text index of all your content - updated live as you add/edit content. 65 | 66 | ### Markup Support 67 | 68 | Sushy supports plaintext, HTML and Textile for legacy reasons, and Markdown as its preferred format. ReStructured Text is also supported, but since I don't use it for anything (and find it rather a pain to read, let alone write), I can't make any guarantees as to its reliability. Work is ongoing for supporting Jupyter notebooks (which have no metadata/frontmatter conventions). 69 | 70 | All markup formats MUST be preceded by "front matter" handled like RFC2822 headers (see the `pages` folder for examples and test cases). Sushy uses the file extension to determine a suitable renderer, but that can be overriden if you specify a `Content-Type` header (see `config.hy` for the mappings). 71 | 72 | # FAQ 73 | 74 | ## Why? 75 | 76 | I've been running a classical, object-oriented Python Wiki (called [Yaki][y]) for the better part of a decade. It works, but is comparatively big and has become unwieldy and cumbersome to tweak. So I decided to [rewrite it][tng]. [Again][gae]. And [again][clj]. 77 | 78 | And I eventually decided to make it _smaller_ -- my intention is for the core to stop at around 1000 lines of code excluding templates, so this is also an exercise in building tight, readable (and functional) code. 79 | 80 | ### Why [Hy][hy]? 81 | 82 | Because I've been doing a lot of Clojure lately for my other personal projects, and both the LISP syntax and functional programming style are quite natural to me. 83 | 84 | I thought long and hard about doing this in Clojure instead (and in fact have been poking at an [implementation][clj] for almost a year now), but the Java libraries for Markdown and Textile have a bunch of irritating little corner cases and I wanted to make sure all my content would render fine the first time, plus Python has an absolutely fantastic ecosystem that I am deeply into. 85 | 86 | Then [Hy][hy] came along, and I realized I could have my cake and eat it too. 87 | 88 | ## Can this do static sites? 89 | 90 | I've used a fair amount of static site generators, and they all come up short on a number of things (namely trivially easy updates that don't involve re-generating hundreds of tiny files and trashing the filesystem) -- which, incidentally, is one of the reasons why Sushy relies on a single SQLite file for temporary data. 91 | 92 | But there's no reason why this can't be easily modified to pre-render and save the HTML content after indexing runs -- pull requests to do that are welcome. 93 | 94 | ## Requirements 95 | 96 | Thanks to [Hy][hy], this should run just as well under Python 2 and Python 3. My target environment is 2.7.8/PyPy, though, so your mileage may vary. Check the `requirements.txt` file - I've taken pains to make sure dependencies are there _for a reason_ and not just because they're trendy. 97 | 98 | --- 99 | 100 | # Deployment 101 | 102 | This repository should be deployable on [piku][piku] (my featherweight version of [Heroku][heroku]), and also used to be deployable to [Dokku][dokku] -- this was removed in the 2023 refactoring since I don't use it anymore. 103 | 104 | As is (for development) the content ships with the code repo. Changing things to work off a separate mount point (or a shared container volume) is trivial. 105 | 106 | ## Configuration 107 | 108 | In accordance with the [12 Factor][12] approach, runtime configuration is taken from environment variables: 109 | 110 | * `DEBUG` - Enable debug logs 111 | * `PROFILER` - Enable `cProfile` statistics (will slow down things appreciatively) 112 | * `CONTENT_PATH` - the folder your documents live in 113 | * `THEME_PATH` - path under which static assets (JS/CSS/etc.) 114 | and templates/views are stored 115 | * `BIND_ADDRESS` - IP address to bind the development server to 116 | * `PORT` - TCP port to bind the server to 117 | 118 | These are set in the `Makefile` (which I use for a variety of purposes). 119 | 120 | --- 121 | 122 | ## Trying it out 123 | 124 | Make sure you have `libxml` and `libxslt` headers, as well as the JPEG library - the following is for Ubuntu 14.04: 125 | ``` 126 | sudo apt-get install libxml2-dev libxslt1-dev libjpeg-dev 127 | # install dependencies 128 | make deps 129 | # run the indexing daemon (updates upon file changes) 130 | make index-watch & 131 | # run the standalone server (or uwsgi) 132 | make serve 133 | ``` 134 | [piku]: https://github.com/rcarmo/piku 135 | [heroku]: https://www.heroku.com/ 136 | [da]: http://dokku-alt.github.io 137 | [dokku]: https://github.com/progrium/dokku 138 | [fig]: http://www.fig.sh 139 | [12]: http://12factor.net/ 140 | [hy]: http://hylang.org 141 | [y]: https://github.com/rcarmo/Yaki 142 | [tng]: https://github.com/rcarmo/yaki-tng 143 | [gae]: https://github.com/rcarmo/yaki-gae 144 | [clj]: https://github.com/rcarmo/yaki-clj 145 | [ink]: http://ink.sapo.pt 146 | [uwsgi]: https://github.com/unbit/uwsgi 147 | [db]: http://www.dropbox.com 148 | [nr]: http://www.newrelic.com 149 | -------------------------------------------------------------------------------- /pages/HomePage/index.md: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Title: Welcome to Sushy 3 | Date: Sun Feb 18 16:15:00 2007 4 | Last-Modified: 2014-11-02 22:30:00 5 | X-Cache-Control: max-age=600 6 | 7 | ## What is this? 8 | 9 | Sushy is a wiki engine that runs off static files, rendering them on the fly to enriched HTML. 10 | 11 | ## Principles of Operation 12 | 13 | * All your Textile, Markdown or ReStructured Text content lives in a filesystem tree, with a folder per page (if you use [Jekyll][j], you should be able to drop in your current content with minimal tweaks) 14 | * Sushy grabs and renders those on demand with fine-tuned HTTP headers (assuming you do the sane thing and put Varnish or CloudFlare in front for caching) 15 | * It also maintains a SQLite database with a full-text index of all your content (because I need this for private wikis). 16 | 17 | ## [Documentation](docs) 18 | 19 | Sushy is (naturally) [self-documenting](docs). 20 | 21 | ## Demo Content 22 | 23 | There is a set of [formatting tests](tests) you can look at to get a feel for the way things work. 24 | 25 | [j]: http://jekyllrb.com 26 | -------------------------------------------------------------------------------- /pages/blog/2015/01/17/2210/index.md: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Date: 2015-01-17 22:10:00 3 | Title: Implemented See Also 4 | 5 | After a few shenanigans, finally found the time to implement the "See Also"/related pages feature. 6 | 7 | In short, wiki pages can now display a list of other pages linking to them, in the grand tradition of wikis like [E2][e2]. 8 | 9 | That list doesn't include pages that the current page links _to_ (which is a departure from what I had earlier implemented in [Yaki][y]) but that is trivial to add later if necessary (and I intend to revisit this someday using `nltk` for my own uses). 10 | 11 | The main challenge here was, as usual, making it look halfway decent on a browser -- I decided to stick to the usual gradient table, but went with `table-cell` and a little inline JavaScript to paint and resize the cells on the client side. The result is not _completely_ responsive, but seems to work well enough. 12 | 13 | In preparation for the next set of features (which is going to include blog archives and so forth), I'm now adding development notes to the sample content. 14 | 15 | [y]: https://github.com/rcarmo/Yaki 16 | [e2]: http://everything2.com -------------------------------------------------------------------------------- /pages/blog/2015/03/07/2330/index.md: -------------------------------------------------------------------------------- 1 | Title: Multiprocessing and 0MQ 2 | Date: 2015-03-07 23:30:00 3 | From: Rui Carmo 4 | 5 | After a few months dithering about with other things, I finally put in the time to rebuild the indexer using `multiprocessing` (with `pyzmq` for IPC), cutting down the time for content indexing to nearly half on a dual-core machine (and to nearly a third on a quad-core machine). 6 | 7 | FTS indexing still takes a fair amount of time, but it can't be sped up any further (since it's done inside SQLite). The big improvement is that all the markup processing and link gathering is done in parallel and ahead of database operations, saving a considerable amount of time for large (7000+ pages) sites. 8 | 9 | I expect this will also be helpful when running under `PyPy`, since the JIT will have some nice, tight code loops to tackle. 10 | 11 | Now I can get back to the HTTP handling and templating stuff. -------------------------------------------------------------------------------- /pages/blog/2015/05/17/1130/index.md: -------------------------------------------------------------------------------- 1 | Title: RSS Feeds 2 | Date: 2015-05-17 11:30:00 3 | From: Rui Carmo 4 | 5 | Work was insane for the last couple of months, so there was another hiatus in development. After some thought, I decided to remove the `multiprocessing` support from the indexer since the most likely scenario is for this to be deployed on a single-core VPS/micro instance/etc. and it was unnecessarily complex. 6 | 7 | Regardless, a bit of profiling shows that it is efficient enough, even though I'm not exactly pleased with the Textile parser: 8 | 9 | ![Indexer profile, generated by pstats](indexer.png) 10 | 11 | Once that was done, I whipped up a simple but effective RSS feed generator that inserts inline styling (which I find essential for proper table rendering, etc.). With luck, HTTP handling and new templating are next, followed by a little cleaning up. 12 | -------------------------------------------------------------------------------- /pages/blog/2015/05/17/1130/indexer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/pages/blog/2015/05/17/1130/indexer.png -------------------------------------------------------------------------------- /pages/blog/2016/03/19/1900/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | From: Rui Carmo 3 | Title: Pressing on 4 | Date: 2016-03-19 18:54:00 5 | --- 6 | 7 | After a fairly long hiatus, development has resumed, focusing largely on templating and making this suitable for a blog. 8 | -------------------------------------------------------------------------------- /pages/blog/2023/09/03/1800/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | From: Rui Carmo 3 | Date: 2023-09-03 18:00:00 4 | Title: Hy 0.27 (aka 2023 edition) 5 | Tags: development, hylang, refactoring 6 | --- 7 | 8 | And so it came to pass that, almost nine years since I decided to try out [Hy] to manage my site and seven to eight years after I decided to move away from it into "vanilla" [Python], it struck my fancy to take the old codebase and refactor it to use "modern" [Hy], which I did piecemeal over a week or so by the beach. 9 | 10 | I did it partially because I was doing some [Scheme], partially because I had a few other pieces of code I wanted to refactor, and partially because having the repository sitting there on GitHub, seemingly abandoned, was an itch I've been meaning to scratch for a while. 11 | 12 | ## Annoyances 13 | 14 | First of all, it should be said that [Hy] has had breaking changes pretty much every release. A few of them have to deal with the decision to move some of the most useful things into a separate `hyrule` library, but others cut a little deeper 15 | (the removal of staples like `let` was a big reason why I moved off it a few years back, but it didn't stop there). The `->` threading macro and `assoc` (which greatly simplified a few things) were easy enough to recoup, but all the other little semantic changes took a while. 16 | 17 | [Python] has also moved on, but even though the 1:1 correspondence between the original [Hy] code and my current site generator has eroded, the bits that matched were still quite similar, and I could reference them when some quirky bugs surfaced. 18 | 19 | But if you're stepping into [Hy] now, let me give you a list of things I had to fix: 20 | 21 | * For some unfathomable reason, `if` needs to have two expressions, so I had to change a bunch of them to `when` conditionals. 22 | * `let` returned, but without delimiters between bindings: `(let [one 1 two 2] ...)` instead of `(let [[one 1] [two 2]] ...)`, which is arguably an improvement 23 | * `import` (and `require`) also similarly changed structure 24 | * `nil` and booleans had to be translated into their [Python] norms. 25 | * `*earmuffs*` and associated symbol mapping changed, so I had to get rid of those too. 26 | * `setv` replaced `def`. I nearly defined a `set!` macro to make it feel more natural, but decided against it and plowed on. 27 | * [Python] tuples are now `#("like" "this")` instead of `(, "like" "that")` (also an improvement, but... annoying) 28 | * `&rest` and optional argument syntax changed, as did referencing `#* args` and `#** kwargs` 29 | * Keyword handling for `kwargs` became `:slightly "better"`, but, annoyingly, `dict` keys can't be used in the same way 30 | 31 | Fortunately I wasn't writing [Clojure] or [Janet] at the same time (just [Scheme]), otherwise things would have been weirder (especially considering I did this mostly in the dead of night before dozing off). 32 | 33 | So the code now _generally_ works and can serve a working site, although there are a few edge cases with `UTF-8` I'm squashing as I step through the test pages I (luckily) wrote almost a decade ago. Also, I've apparently broken a couple of `Pillow` calls (every other dependency just works, which is interesting). 34 | 35 | This has been a fun thing to revisit, although I'm rather curious about what would have happened if I had used [Scheme] instead... 36 | 37 | [Python]: dev/python 38 | [Clojure]: dev/clojure 39 | [Janet]: dev/janet 40 | [Scheme]: dev/lisp 41 | [Hy]: http://hylang.org 42 | -------------------------------------------------------------------------------- /pages/blog/2024/03/24/2135/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | Title: 2024 Edition 3 | From: Rui Carmo 4 | Date: 2024-03-24 21:35:00 5 | Last-modified: 2024-03-29 20:18:00 6 | Tags: development, testing, notes 7 | --- 8 | 9 | Had a few minutes to check the current status quo. `hy` hasn't had a new release in almost a year and moving from Python 3.9 to 3.11 had minimal impact, so I guess it's time to do some more fixing and see if this can be turned back into a "production" Wiki engine. 10 | 11 | I also fixed time references in search, removed `.ipynb` and `.rst` support (I had zero practical use for that over the years) and fixed a few uses of `apply` that are not required anymore, as well as hunting down some errant uses of the old `tuple` syntax. 12 | -------------------------------------------------------------------------------- /pages/blog/2024/09/05/2322/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | from: Rui Carmo 3 | date: 2024-09-05 23:22:00 4 | title: Hy 0.29.0 5 | tags: hylang, breakage, dependencies, updates 6 | --- 7 | 8 | And _of course_ `hy` 0.29.0 had to come out and subtly break every single piece of recent code I wrote in it _yet again_, which kind of makes keeping this codebase updated a pointless exercise. I really wish they'd stop messing with the syntax, especially for `async` stuff. 9 | 10 | Still, thankfully I hadn't really converted this to `aiohttp` yet, so most of the stuff on GitHub wasn't broken. But I still needed to upgrade the dependencies and try to modernize a few other things--like looking at GitHub-flavored Markdown as a default format. 11 | -------------------------------------------------------------------------------- /pages/docs/basics/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/pages/docs/basics/image.png -------------------------------------------------------------------------------- /pages/docs/basics/index.md: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Date: 2014-10-04 09:17:00 3 | Title: Content Basics 4 | 5 | ## Files and Folders 6 | 7 | Sushy expects you to organize your content using _a folder per page_ with an "index" document inside. The folder path determines the URL it's published under (so you get "nice" URLs by default), and this makes it easy to manage media assets on a per-post basis: 8 | 9 | ![Folders](image.png) 10 | 11 | 12 | ## Markup Languages 13 | 14 | Sushy supports Markdown, Textile, raw HTML and ReStructuredText, and determines which markup processor to use based on: 15 | 16 | 1. The file extension (`index.md`, `index.textile`, `index.rst`, etc.) 17 | 2. An (optional) `Content-Type` header (in case you need to override the default) 18 | 19 | 20 | ## Front Matter 21 | 22 | Your index document must start with a set of metadata headers. Sushy isn't picky about anything except that header names should be followed by a colon and that the first blank line separates them from the body text: 23 | 24 | ```text 25 | From: Rui Carmo 26 | Date: 2014-10-04 09:24:00 27 | Title: Test post 28 | Content-Type: text/x-markdown 29 | 30 | ## Content Heading 31 | 32 | Body text 33 | ``` 34 | 35 | ### Recognized Headers 36 | 37 | Sushy looks for the following headers: 38 | 39 | * `Content-Type:` allows you to override the markup language in use 40 | * `Date:` the original publishing date for your post. If missing, the file modification time is used. 41 | * `From:` the author name 42 | * `Index:` set this to `no` or `off` to remove a page from the full text index 43 | * `Last-Modified:` lets you override the file modification time explicitely and trumps the date for insertion in RSS feeds 44 | * `Title:` your page/post title 45 | 46 | Headers are case-insensitive, and Sushy will take posts written for [Jekyll][j] and simply ignore the triple dashes (as well as things like `layout`, at least for the moment): 47 | 48 | ```text 49 | --- 50 | layout: post 51 | title: Blogging Like a Hacker 52 | --- 53 | ``` 54 | 55 | ## Themes 56 | 57 | Sushy supports multiple themes. Each theme must provide a `views` folder that contains the HTML templating (using Bottle templates) and a `static` folder for any static assets (CSS, JS, images, etc.) required by the theme. 58 | 59 | [j]: http://jekyllrb.com 60 | -------------------------------------------------------------------------------- /pages/docs/content/index.md: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Date: 2014-10-04 09:10:00 3 | Title: Writing Content for Sushy 4 | 5 | ## Syntax Highlighting 6 | 7 | Sushy supports highlighting source code blocks in two ways: 8 | 9 | * Using triple-quoted Markdown blocks 10 | * Using `pre` tags with a `syntax` attribute (available to all markup languages) 11 | 12 | You can also optionally set a `src` attribute on your `pre` tags to reference an additional file, which makes it a lot easier to maintain complex articles: 13 | 14 | ```html 15 |
16 | ```
17 | 


--------------------------------------------------------------------------------
/pages/docs/index.md:
--------------------------------------------------------------------------------
1 | From: Rui Carmo
2 | Date: 2014-10-04 09:09:00
3 | Title: Sushy Documentation
4 | 
5 | * [Basics](docs/basics)
6 | * [Content Formatting](docs/content)
7 | * [Internals](docs/internals)
8 | 


--------------------------------------------------------------------------------
/pages/docs/internals/hy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/pages/docs/internals/hy.png


--------------------------------------------------------------------------------
/pages/docs/internals/index.md:
--------------------------------------------------------------------------------
 1 | From: Rui Carmo
 2 | Date: 2014-11-03 19:09:00
 3 | Last-Modified: 2014-12-29 11:15:00
 4 | Title: Internals
 5 | 
 6 | 
 7 | ## Hy
 8 | 
 9 | Sushy is written in [Hy][hy], a [LISP][lisp] dialect that compiles to [Python][python] bytecode -- in effect, [Python][python] in fancy dress, chosen due to its conciseness and seamless integration with [Python][python] libraries.
10 | 
11 | The following is a short description of each module.
12 | 
13 | ---
14 | 
15 | ### `app`
16 | 
17 | This is a simple WSGI entry point, doing double duty as a [Bottle][b]-based develpment server.
18 | 
19 | ### `config`
20 | 
21 | The `config` module is (predictably) where Sushy is configured. Most configurable options come from environment variables, but some conventions like URL routes, meta pages and ignored folders are set there.
22 | 
23 | ### `indexer`
24 | 
25 | Like its name entails, the `indexer` module handles full-text and link indexing.
26 | 
27 | ### `models`
28 | 
29 | This is the only "pure" [Python][python] module. Its main purpose is to encapsulate database setup and access (which use the `peewee` ORM) into a small set of functional primitives.
30 | 
31 | ### `render`
32 | 
33 | This simply imports and abstracts away all markup processors, providing a single rendering function.
34 | 
35 | ### `routes`
36 | 
37 | URL routes and HTTP request handling, again courtesy of [Bottle][b].
38 | 
39 | ### `render`
40 | 
41 | The `render` module encapsulates all the markup renderers, providing a uniform API for them.
42 | 
43 | ### `store`
44 | 
45 | The `store` module provides functions to find, retrieve and parse raw page markup and front matter.
46 | 
47 | ### `transform`
48 | 
49 | This performs all the HTML transformations that turn the rendered markup into actual Wiki pages, reformatting links and other tags.
50 | 
51 | ### `utils`
52 | 
53 | This is a small grab bag of utility functions (some of which are straight ports from my [utilities library][utils]).
54 | 
55 | 
56 | [hy]: http://hylang.org
57 | [lisp]: Wikipedia:LISP
58 | [python]: http://python.org
59 | [utils]: https://github.com/rcarmo/python-utils
60 | [b]: http://bottlepy.org
61 | 
62 | 


--------------------------------------------------------------------------------
/pages/meta/About/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | from: Rui Carmo
3 | title: About
4 | date: 2016-03-19 13:27:00
5 | tags: meta
6 | ---
7 | 
8 | This is a demo site for [Sushy](https://github.com/rcarmo/sushy), a wiki/blog engine that runs off static files, rendering them on the fly to enriched HTML.
9 | 


--------------------------------------------------------------------------------
/pages/meta/Acronyms/index.txt:
--------------------------------------------------------------------------------
 1 | From: Rui Carmo
 2 | Title: Acronym Table
 3 | Date: 2007-05-11 23:49:15
 4 | Last-Modified: 2007-06-03 11:53:28
 5 | Tags: meta
 6 | Content-Type: text/x-textile
 7 | 
 8 | h2. Generic Acronyms
 9 | 
10 | 
11 | BLT Bacon, Lettuce and Tomato
12 | BTO Build-To-Order
13 | CEO Chief Executive Officer
14 | TLA Three-Letter Acronym
15 | TOC Table Of Contents
16 | IMHO In My Honest Opinion
17 | FUD Fear, Uncertainty and Doubt
18 | 
19 | 20 | h2. Computing Acronyms 21 | 22 |
23 | CLI Command-Line Interface
24 | CSS Cascading Style Sheets
25 | PCB Printed Circuit Board
26 | RAM Random Access Memory
27 | RFC Request For Comments
28 | ROM Read-Only Memory
29 | RSS Real Simple Syndication
30 | SEO Search Engine Optimization
31 | USB Universal Serial Bus
32 | WYSIWYG What You See Is What You Get
33 | XML Extensible Markup Language
34 | 
35 | 36 | h2. Networking Acronyms 37 | 38 |
39 | FTP File Transfer Protocol
40 | HTTP HyperText Transfer Protocol
41 | IP Internet Protocol or Intellectual Property
42 | SSH Secure Shell
43 | SFTP Secure File Transfer Protocol
44 | SMTP Simple Mail Transfer Protocol
45 | IMAP Internet Message Access Protocol
46 | TCP Transmission Control Protocol
47 | UDP User Datagram Protocol
48 | URL Uniform Resource Locator
49 | 
50 | 51 | h2. Mobile Acronyms 52 | 53 |
54 | GSM Global System for Mobile communications
55 | HSDPA High Speed Downlink Packet Access
56 | IMS IP Multimedia Subsystem
57 | MMS Multimedia Message Service
58 | SMS Short Message Service
59 | UMTS Universal Mobile Telecommunications System
60 | WAP Wireless Application Protocol
61 | 
62 | -------------------------------------------------------------------------------- /pages/meta/Aliases/index.txt: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Date: 2009-10-04 09:35:46 3 | Previously-Modified: 2010-01-28 23:22:36 4 | Last-Modified: 2010-08-01 14:58:29 5 | X-Index: No 6 | Title: Aliases 7 | Tags: meta 8 | Content-Type: text/x-textile 9 | 10 | This page holds direct replacements for some shorthand I use in WikiLinks that often needs to expand to direct links to external sites or "to other Wikis":meta/InterWikiMap. 11 | 12 | Some are merely temporary until I can make references to some topics completely consistent across old content and clean them up. 13 | 14 | h2. Sites and Companies 15 | 16 |
 17 | Adobe com/Adobe
 18 | AOL http://www.aol.com
 19 | Backpack http://backpackit.com
 20 | BlackBerry com/RIM
 21 | BoingBoing http://www.boingboing.net
 22 | CentOS http://www.centos.org/
 23 | Compaq com/Compaq
 24 | Coral http://www.coralcdn.org
 25 | Dell com/Dell
 26 | Digg http://www.digg.com
 27 | FNAC http://www.fnac.pt
 28 | Friendfeed http://www.friendfeed.com
 29 | Google com/Google
 30 | Google/Buzz com/Google/Buzz
 31 | Google/Checkout http://checkout.google.com
 32 | Google/Chrome com/Google/Chrome
 33 | Chrome Google/Chrome
 34 | Google/Code http://code.google.com
 35 | Google/Reader com/Google/Reader
 36 | IKEA http://www.ikea.com
 37 | INESC http://www.inesc.pt
 38 | IST http://www.ist.utl.pt
 39 | Iomega http://www.iomega.com
 40 | Jaiku Wikipedia:Jaiku
 41 | LG http://www.lge.com
 42 | Last.fm http://last.fm
 43 | LinkedIn http://www.linkedin.com
 44 | Macromedia com/Macromedia
 45 | Maxtor http://www.maxtor.com
 46 | Netcabo http://www.zon.pt
 47 | Newsweek http://www.newsweek.com
 48 | Nintendo com/Nintendo
 49 | Novatel http://www.novatelwireless.com
 50 | Opera http://www.opera.com
 51 | Ovi Wikipedia:Ovi_(Nokia)
 52 | Philips http://www.philips.com
 53 | Portugal_Telecom http;//www.telecom.pt
 54 | SAP com/SAP
 55 | Sanyo http://www.sanyo.com
 56 | Sapo Portugal/Sapo
 57 | Siemens http://www.siemens.com
 58 | Slashdot http://slashdot.org
 59 | Sonae http://sonae.pt
 60 | Sybase com/Sybase
 61 | Technorati http://www.technorati.com
 62 | Usenet protocols/NNTP
 63 | VAX Wikipedia:VAX
 64 | Verizon http://www.verizonwireless.com
 65 | Worten http://www.worten.pt
 66 | YouTube http://www.youtube.com
 67 | Yahoo/Pipes com/Yahoo/Pipes
 68 | com/Adobe Wikipedia:Adobe_Systems
 69 | com/Amazon Wikipedia:Amazon.com
 70 | com/Asus http://www.asus.com
 71 | com/Compaq Wikipedia:Compaq
 72 | com/Google/Buzz Wikipedia:Google_Buzz
 73 | com/Instapaper http://www.instapaper.com
 74 | com/Macromedia Wikipedia:Macromedia
 75 | com/Nintendo Wikipedia:Nintendo
 76 | com/Quora Wikipedia:Quora
 77 | com/SAP Wikipedia:SAP_AG
 78 | com/Skyhook http://www.skyhook.com
 79 | com/Skype http://www.skype.com
 80 | apps/Skype com/Skype
 81 | com/Sybase Wikipedia:Sybase
 82 | com/Yahoo/Pipes http://pipes.yahoo.com
 83 | com/Wacom http://www.wacom.com
 84 | 
85 | 86 | h2. Internal 87 | 88 |
 89 | SiteDesigns site/Designs
 90 | SiteHeaders site/Headers
 91 | 
92 | 93 | h2. Apple Stuff 94 | 95 |
 96 | .Mac MobileMe
 97 | 10.4 com/Apple/OSX/Tiger
 98 | 10.5 com/Apple/OSX/Leopard
 99 | 10.6 com/Apple/OSX/Snow_Leopard
100 | AirPort/Express com/Apple/AirPort/Express
101 | AirPort/Extreme com/Apple/AirPort/Extreme
102 | Apple/TV com/Apple/TV
103 | Apple/iPhone/3G com/Apple/iPhone/3G
104 | Apple/iPhone/3Gs com/Apple/iPhone/3Gs
105 | Aqua Wikipedia:Aqua_(user_interface)
106 | Darwin Wikipedia:Darwin_(operating_system)
107 | G3 Wikipedia:PowerPC_G3
108 | G4 Wikipedia:PowerPC_G4
109 | G5 Wikipedia:PowerPC_G5
110 | iPhone/Post-Launch com/Apple/iPhone/Post-Launch
111 | iPhone/Safari com/Apple/iPhone/Safari
112 | Leopard 10.5
113 | Mac com/Apple/Mac
114 | Mac/mini com/Apple/Mac/mini
115 | MacBU Wikipedia:MacBU
116 | MacBook com/Apple/Mac/MacBook
117 | MacBook/Air com/Apple/Mac/MacBook/Air
118 | MacBook/Pro com/Apple/Mac/MacBook/Pro
119 | Mighty_Mouse com/Apple/Magic_Mouse
120 | NeXT com/NeXT
121 | OSX com/Apple/OSX
122 | PowerBook com/Apple/Mac/PowerBook
123 | Snow_Leopard 10.6
124 | Tiger 10.4
125 | Time_Capsule com/Apple/Time_Capsule
126 | Time_Machine com/Apple/Time_Machine
127 | WWDC Wikipedia:Apple_Worldwide_Developers_Conference
128 | iMac com/Apple/Mac/iMac 
129 | iPad com/Apple/iPad
130 | iPhone com/Apple/iPhone
131 | iPhone/3G com/Apple/iPhone/3G
132 | iPhone/3GS com/Apple/iPhone/3GS
133 | iPod com/Apple/iPod
134 | iPod/Shuffle com/Apple/iPod/Shuffle
135 | iPod/Touch com/Apple/iPod/Touch
136 | iPod/mini com/Apple/iPod/mini
137 | iPod/nano com/Apple/iPod/nano
138 | 
139 | 140 | h2. Devices 141 | 142 |
143 | PC hw/PC
144 | WDTV hw/WDTV
145 | hw/WDTV http://wdtv.wetpaint.com/
146 | Palm/Pre com/Palm/Pre
147 | Blackberry com/RIM
148 | NSLU2 Linksys/NSLU2
149 | Linksys/NSLU2 com/Linksys/NSLU2
150 | Mini_9 com/Dell/Inspiron_Mini_9
151 | Dell/Inspiron/Mini_9 com/Dell/Inspiron/Mini_9
152 | Kindle Amazon/Kindle
153 | Amazon/Kindle com/Amazon/Kindle
154 | Clie Wikipedia:Clie
155 | PlayStation com/Sony/PlayStation
156 | PSP com/Sony/PlayStation/Portable
157 | PS3 PlayStation_2
158 | PlayStation_2 com/Sony/PlayStation/2
159 | PS3 PlayStation_3
160 | PlayStation_3 com/Sony/PlayStation/3
161 | Sony/Reader com/Sony/Reader
162 | 
163 | 164 | h2. Microsoft 165 | 166 |
167 | Exchange com/Microsoft/Exchange
168 | Outlook com/Microsoft/Outlook
169 | IE Internet_Explorer
170 | Sharepoint Wikipedia:Sharepoint
171 | Internet_Explorer com/Microsoft/Internet_Explorer
172 | NetMeeting com/Microsoft/NetMeeting
173 | NT Windows/NT
174 | Windows/NT com/Microsoft/Windows/NT
175 | XP Windows/XP
176 | Windows/XP com/Microsoft/Windows/XP
177 | Windows/7 com/Microsoft/Windows/7
178 | 
179 | 180 | h2. Languages & Development 181 | 182 |
183 | AppleScript dev/AppleScript
184 | CVS Wikipedia:Concurrent_Versions_System
185 | git dev/SCM/git
186 | Lua http://www.lua.org/
187 | Java dev/Java
188 | Python dev/Python
189 | PHP dev/PHP
190 | Mercurial dev/SCM/Mercurial
191 | Subversion dev/SCM/Subversion
192 | Textile markup/Textile
193 | Markdown markup/Markdown
194 | 
195 | 196 | h2. Linux 197 | 198 |
199 | Debian os/Linux/Distributions/Debian
200 | Fedora os/Linux/Distributions/Fedora
201 | Gentoo os/Linux/Distributions/Gentoo
202 | Jolicloud http://www.jolicloud.com/
203 | Linux os/Linux
204 | Maemo os/Linux/Distributions/Maemo
205 | MeeGo http://meego.com
206 | Moblin http://moblin.org
207 | NASLite os/Linux/Distributions/NASLite
208 | RedHat os/Linux/Distributions/RedHat
209 | SuSE os/Linux/Distributions/SuSe
210 | Ubuntu os/Linux/Distributions/Ubuntu
211 | 
212 | 213 | h2. Acronyms and Buzzwords 214 | 215 |
216 | IEEE/802.1X Wikipedia:802.1X
217 | 3G UMTS
218 | ACPI Wikipedia:Advanced_Configuration_and_Power_Interface
219 | API Wikipedia:Application_Programming_Interface
220 | APN Wikipedia:Access_Point_Name
221 | ARM Wikipedia:ARM_architecture
222 | ATM Wikipedia:Asynchronous_Transfer_Mode
223 | ActiveSync com/Microsoft/ActiveSync
224 | Ajax Wikipedia:Ajax_(programming)
225 | Android com/Google/Android
226 | BIOS Wikipedia:BIOS
227 | Blu-Ray Wikipedia:Blu-Ray
228 | Bonjour com/apple/Bonjour
229 | CGI Wikipedia:Common_Gateway_Interface
230 | CPU Wikipedia:CPU
231 | DisplayPort Wikipedia:DisplayPort
232 | DLNA Wikipedia:DLNA
233 | DOM Wikipedia:Document_Object_Model
234 | DRM Wikipedia:Digital_rights_management 
235 | DSL Wikipedia:DSL
236 | DVI Wikipedia:Digital_Visual_Interface
237 | EDGE Wikipedia:Enhanced_Data_Rates_for_GSM_Evolution
238 | EPUB Wikipedia:Epub
239 | EV-DO Wikipedia:EV-DO
240 | EXIF photography/EXIF
241 | E_Ink eBook_Readers
242 | FAT fs/FAT32
243 | GNU Wikipedia:GNU
244 | GTK http://www.gtk.org
245 | GUI Wikipedia:GUI
246 | Gantt Wikipedia:Gantt_chart
247 | HDMI Wikipedia:HDMI
248 | HFS Wikipedia:HFS_Plus
249 | HSDPA HSPA
250 | HSPA Wikipedia:High_Speed_Packet_Access
251 | HSUPA HSPA
252 | HTTP protocols/HTTP
253 | HTTPS protocols/HTTPS
254 | IBM com/IBM
255 | ICANN Wikipedia:ICANN
256 | IDE Wikipedia:Integrated_development_environment
257 | IMS telco/IMS
258 | IPTC photography/IPTC
259 | IPv6 Wikipedia:IPv6
260 | ISDN Wikipedia:Integrated_Services_Digital_Network
261 | ISDN:Wikipedia:Integrated_services_digital_network
262 | ISO Wikipedia:ISO_(disambiguation)
263 | IrDA Wikipedia:Infrared_Data_Association
264 | JPEG photography/JPEG
265 | LCD Wikipedia:Liquid_crystal_display
266 | LOTR Wikipedia:The_Lord_of_the_Rings
267 | MAPI Wikipedia:Messaging_Application_Programming_Interface
268 | MemoryStick Wikipedia:Memory_Stick
269 | Memory_Stick Wikipedia:Memory_Stick
270 | MP4 Wikipedia:MPEG-4_Part_14
271 | MPEG Wikipedia:Moving_Picture_Experts_Group
272 | NDA Wikipedia:Non-disclosure_agreement
273 | NDIS Wikipedia:Network_Driver_Interface_Specification
274 | NFS Wikipedia:Network_File_System_(protocol)
275 | Oracle com/Oracle
276 | P2P Wikipedia:Peer-to-peer
277 | PAL Wikipedia:PAL
278 | PCMCIA Wikipedia:PC_Card
279 | PDA Wikipedia:Personal_digital_assistant
280 | PMI http://www.pmi.org
281 | PVR Wikipedia:Personal_video_recorder
282 | Qt Wikipedia:Qt_(framework)
283 | RAM Wikipedia:RAM
284 | REST Wikipedia:Representational_State_Transfer
285 | RDP protocols/RDP
286 | SDK Wikipedia:SDK
287 | SSH cli/SSH
288 | SSD Wikipedia:Solid-state_drive
289 | SSID Wikipedia:Service_set_(802.11_network)
290 | Series_40 com/Nokia/S40
291 | Series_60 com/Nokia/S60
292 | TCP/IP Wikipedia:Internet_Protocol_Suite
293 | TIFF Wikipedia:Tagged_Image_File_Format
294 | TNEF com/Microsoft/TNEF
295 | Trackback Wikipedia:Trackback
296 | TrueType Wikipedia:TrueType
297 | UI Wikipedia:User_interface
298 | UK Wikipedia:United_Kingdom
299 | UMTS Wikipedia:UMTS
300 | UNIX os/UNIX
301 | UPNP protocols/UPNP
302 | URL Wikipedia:URL
303 | US Wikipedia:United_States
304 | USB hw/USB
305 | UTF-8 Wikipedia:UTF-8
306 | Unicode Wikipedia:Unicode
307 | VF-PT com/Vodafone/Portugal
308 | VNC protocols/VNC
309 | VRML Wikipedia:VRML
310 | Vista com/Microsoft/Windows/Vista
311 | WebKit Wikipedia:WebKit
312 | Web_2.0 Wikipedia:Web_2.0
313 | Wiki Wikipedia:Wiki
314 | Win32 Wikipedia:Win32
315 | Windows/7 com/Microsoft/Windows/7
316 | Windows/CE com/Microsoft/Windows/CE
317 | WYSIWYG Wikipedia:WYSIWYG
318 | ZIP Wikipedia:ZIP_(file_format)
319 | com/Oracle Wikipedia:Oracle_Corporation
320 | eBook Wikipedia:E-book
321 | ePub EPUB
322 | vCalendar Wikipedia:ICalendar
323 | vCard Wikipedia:VCard
324 | 
325 | 326 | h2. People 327 | 328 |
329 | Leo_Laporte people/Leo_Laporte
330 | Om_Malik people/Om_Malik
331 | RuiCarmo people/Rui_Carmo
332 | Douglas_Adams people/Douglas_Adams
333 | people/Amit_Singh http://www.kernelthread.com/
334 | people/Andy_Ihnatko Wikipedia:Andy_Ihnatko
335 | people/Arthur_Dent Wikipedia:Arthur_Dent
336 | people/Bob_Ippolito http://bob.pythonmac.org/
337 | people/Celso_Martinho http://celso.arrifana.org/
338 | people/Charles_Stross Wikipedia:Charles_Stross
339 | people/David_Liss Wikipedia:David_Liss
340 | people/David_Pogue Wikipedia:David_Pogue
341 | people/Douglas_Adams Wikipedia:Douglas_Adams
342 | people/Fraser_Speirs http://speirs.org
343 | people/Gary_Gibson Wikipedia:Gary_Gibson
344 | people/Iain_Banks Wikipedia:Iain_Banks
345 | people/Jared_Tarbell http://levitated.net
346 | people/Joao_Craveiro http://weblog.jcraveiro.com
347 | people/John_Siracusa http://arstechnica.com/authors/john-siracusa/
348 | people/Jonathan_Ive Wikipedia:Jonathan_Ive
349 | people/Leo_Laporte Wikipedia:Leo_Laporte
350 | people/Marco_Arment http://www.marco.org/
351 | people/Michael_Heilemann http://binarybonsai.com
352 | people/Michael_Mace http://www.mikemace.com/
353 | people/Neal_Stephenson Wikipedia:Neal_Stephenson
354 | people/Nuno_Nunes http://nunonunes.org
355 | people/Om_Malik Wikipedia:Om_Malik
356 | people/Pedro_Pinheiro http://mat.su
357 | people/Pedro_Santos http://www.macacos.com
358 | people/Phil_Schiller Wikipedia:Phil_Schiller
359 | people/Reini_Urban Wikipedia:PhpWiki
360 | people/Rich_Burridge http://blogs.sun.com/richb/
361 | people/Russell_Beattie http://www.russellbeattie.com
362 | people/Scott_Adams Wikipedia:Scott_Adams
363 | people/Stephen_Fry Wikipedia:Stephen_Fry
364 | people/Steven_Levy Wikipedia:Steven_Levy
365 | people/Ted_Leung http://www.sauria.com/blog/
366 | people/Terence_Eden http://shkspr.mobi/blog/
367 | people/Terry_Pratchett Wikipedia:Terry_Pratchett
368 | people/Tim_Cook Wikipedia:Tim_Cook
369 | people/Tom_Hume http://www.tomhume.org/
370 | 
371 | 372 | h2. Apps and Tools 373 | 374 |
375 | Acrobat Wikipedia:Adobe_Acrobat
376 | Confluence http://www.atlassian.com/software/confluence/
377 | DarwinPorts MacPorts
378 | Darwine apps/WineBottler
379 | Eclipse apps/Eclipse
380 | MacPorts apps/MacPorts
381 | apps/Pages apps/iWork
382 | apps/Numbers apps/iWork
383 | apps/iChat Wikipedia:iChat
384 | apps/VueScan http://www.hamrick.com/
385 | Eolas apps/Eolas
386 | GNUstep Wikipedia:GNUstep
387 | ImageMagick cli/ImageMagick
388 | Howl http://howlapp.com/
389 | Konfabulator http://widgets.yahoo.com/
390 | newspipe projects/newspipe
391 | RRDTool cli/RRDTool
392 | TiddlyWiki http://www.tiddlywiki.com/
393 | Tomcat http://tomcat.apache.org/
394 | git Wikipedia:Git_(software)
395 | Xen Wikipedia:Xen
396 | Whoosh https://bitbucket.org/mchaput/whoosh/
397 | 
398 | 399 | h2. Concepts 400 | 401 |
402 | Artificial_Intelligence AI
403 | Economics Wikipedia:Economics
404 | Dilbert corporate/Dilbert
405 | Fire Wikipedia:Fire_(instant_messaging_client)
406 | Geek Wikipedia:Geek
407 | Hype corporate/Hype
408 | KISS Wikipedia:K.I.S.S.
409 | Management corporate/Management
410 | Mathematics math
411 | Marketing corporate/Marketing
412 | Open_Source Wikipedia:Open_source
413 | QA Wikipedia:Quality_assurance
414 | Web_Design design/web
415 | Zen Wikipedia:Zen
416 | 
417 | 418 | h2. Entertainment 419 | 420 |
421 | TV/Stargate_Universe Wikipedia:Stargate_Universe
422 | 
423 | -------------------------------------------------------------------------------- /pages/meta/AllPages/index.txt: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Title: All Pages 3 | Date: 2007-04-07 21:02:13 4 | Content-Type: text/x-textile 5 | X-Index: no 6 | X-Cache-Control: max-age=3600 7 | 8 | These are all the pages contained in the "Wiki":Wiki: 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /pages/meta/Archives/index.txt: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Title: Archives 3 | Content-Type: text/x-textile 4 | X-Index: no 5 | X-Cache-Control: max-age=0 6 | 7 | This page lists blog (and linkblog) entries for: 8 | 9 | 10 | -------------------------------------------------------------------------------- /pages/meta/EmptyPage/index.textile: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Date: Wed, 25 Jun 2003 23:44:55 GMT 3 | Title: Page not found 4 | Tags: meta 5 | Content-type: text/x-textile 6 | X-Hide-Metadata: True 7 | X-Index: no 8 | X-Comments: off 9 | 10 | bq. The page you were looking for does not appear to exist. Try going back to the "homepage":start or "searching":meta/Search. -------------------------------------------------------------------------------- /pages/meta/Highlights/index.txt: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Date: 2009-20-04 09:35:46 3 | X-Index: No 4 | Title: Highlights 5 | Tags: meta 6 | Content-Type: text/x-textile 7 | 8 | This page holds links highlighted on the sidebar. 9 | 10 | h1. On My Mind 11 | 12 |
13 | blog/2011/09/01/2200 Tadpole
14 | blog/2011/08/25/1030 The Right Question
15 | blog/2011/08/20/2133 Roadkill
16 | blog/2011/07/08/2306 Twitch
17 | blog/2011/06/06/2222 Cloudy
18 | blog/2011/03/02/2122 On the iPad 2
19 | 
20 | 21 | h1. Popular 22 | 23 |
24 | HOWTO/Switch Switching to the Mac?
25 | HOWTO/Setup/3G 3G/HSDPA/GPRS Guide
26 | com/Apple/iPhone The iPhone Timeline
27 | dev/Python/Grimoire The Python Grimoire
28 | blog/2004/11/08 The Blue Packet
29 | 
30 | 31 | h1. Bookshelf 32 | 33 |
34 | ASIN:1848874480 REAMDE
35 | ASIN:0007448058 A Song of Ice and Fire
36 | ASIN:1857150953 Decline and Fall of the Roman Empire
37 | ASIN:0230750761 Embassytown
38 | ASIN:0596003137 Perl Cookbook
39 | ASIN:0330545620 Halo: Cryptum
40 | ASIN:1907519718 The Kings of Eternity
41 | ASIN:0393072223 The Shallows
42 | ASIN:0596809778 iPhone App Development
43 | ASIN:0230748775 Final Days
44 | ASIN:0441020348 Rule 34
45 | ASIN:0596517742 JS: The Good Parts
46 | 
47 | 48 | h1. Persons Of Interest 49 | 50 |
51 | http://binarybonsai.com/ Binary Bonsai
52 | http://arrifana.org/blog/ Celso Martinho
53 | http://org.against.org/ MoiMeme
54 | http://www.simplicidade.org/notes/ Notes
55 | http://www.russellbeattie.com/ Russell Beattie
56 | http://www.schneier.com/blog/ Schneier on Security
57 | http://www.statusq.org/ Status-Q
58 | 
59 | 60 | h1. Policy 61 | 62 |
63 | site/Disclaimer Disclaimer
64 | site/Press Press
65 | site/Sponsorship Sponsorship
66 | site/Syndication_Policy Syndication
67 | 
68 | 69 | -------------------------------------------------------------------------------- /pages/meta/HitMap/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | from: Rui Carmo 3 | title: Hit Map 4 | x-index: no 5 | x-cache-control: max-age=120 6 | --- 7 | 8 | The following chart shows site hits on a weekly basis: 9 | 10 | 11 | 12 | Jump to: 13 | 14 | * [Referrers](meta/Referrers) 15 | * [Most Popular](meta/MostPopular) 16 | 17 | > *Note:* Hit counts are, at best, an approximation due to heavy front-end caching. 18 | -------------------------------------------------------------------------------- /pages/meta/Index/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | from: Rui Carmo 3 | title: Alphabetical Index 4 | date: 2007-04-07 21:02:13 5 | x-index: no 6 | x-cache-control: max-age=3600 7 | --- 8 | 9 | These are all the pages contained in the [Wiki](Wiki): 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /pages/meta/InterWikiMap/index.txt: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Date: Wed, 25 Jun 2003 23:44:55 GMT 3 | Last-Modified: 2008-12-07 23:42:30 4 | X-Index: No 5 | Title: Inter-Wiki Map 6 | Tags: meta 7 | Content-Type: text/x-textile 8 | 9 | h2. iTunes 10 | 11 |
12 | Artist http://itunes.com/
13 | Developer http://itunes.com/
14 | AppStore http://itunes.com/app/
15 | ITMSMovie http://itunes.com/movie/
16 | ITMSTV http://itunes.com/tv/
17 | 
18 | 19 | h2. Other Wikis 20 | 21 |
22 | E2 http://www.everything2.net/e2node/
23 | 43F http://wiki.43folders.com/index.php/
24 | CocoaWiki http://www.cocoadev.com/index.pl?
25 | Fedora http://fedoraproject.org/wiki/
26 | Litux http://www.litux.org/wiki/
27 | Tao http://the.taoofmac.com/space/
28 | Ubuntu https://wiki.ubuntu.com/
29 | Lspace http://wiki.lspace.org/wiki/
30 | 
31 | 32 | h2. Telco References 33 | 34 |
35 | 3GPP http://the.taoofmac.com/3gpp_redirect.php?spec=
36 | ISO http://www.iso.org/iso/en/CombinedQueryResult.CombinedQueryResult?queryString=
37 | RFC http://www.faqs.org/rfcs/rfc
38 | VoIP http://www.voip-info.org/tiki-index.php?page=
39 | 
40 | 41 | h2. Generic References 42 | 43 |
44 | Acronym http://www.acronymfinder.com/af-query.asp?String=exact&Acronym=
45 | Dictionary http://www.dictionary.com/cgi-bin/dict.pl?term=
46 | IMDB http://us.imdb.com/title/
47 | TV http://us.imdb.com/title/
48 | IMDBTitle http://www.imdb.com/title/
49 | IMDBName http://www.imdb.com/name/
50 | ISBN http://www.amazon.co.uk/gp/product/%s/ref=as_li_tf_tl?ie=UTF8&tag=thtaofma-21&linkCode=as2&camp=1634&creative=6738
51 | ASIN http://www.amazon.co.uk/gp/product/%s/ref=as_li_tf_tl?ie=UTF8&tag=thtaofma-21&linkCode=as2&camp=1634&creative=6738
52 | Game http://www.amazon.co.uk/gp/product/%s/ref=as_li_tf_tl?ie=UTF8&tag=thtaofma-21&linkCode=as2&camp=1634&creative=6738
53 | CD http://www.amazon.co.uk/gp/product/%s/ref=as_li_tf_tl?ie=UTF8&tag=thtaofma-21&linkCode=as2&camp=1634&creative=6738
54 | DVD http://www.amazon.co.uk/gp/product/%s/ref=as_li_tf_tl?ie=UTF8&tag=thtaofma-21&linkCode=as2&camp=1634&creative=6738
55 | JargonFile http://sunir.org/apps/meta.pl?wiki=JargonFile&redirect=
56 | Finance http://finance.google.com/finance?q=
57 | NASDAQ http://finance.google.com/finance?q=
58 | Thesaurus http://www.thesaurus.com/cgi-bin/search?config=roget&words=
59 | Twitter http://twitter.com/
60 | Wikipedia http://en.wikipedia.org/wiki/
61 | Wiktionary http://en.wiktionary.org/wiki/
62 | YouTube http://www.youtube.com/watch?v=
63 | KB http://support.apple.com/kb/
64 | 
65 | 66 | h2. Web Services 67 | 68 |
69 | Geo http://maps.google.com/maps?t=k&q=
70 | GoogleGroups http://groups.google.com/groups?q=
71 | Google http://www.google.com/search?q=
72 | GoogleFinance http://finance.google.com/finance?q=
73 | Technorati http://www.technorati.com/search/
74 | 
75 | 76 | h2. Coding 77 | 78 |
79 | CPAN http://search.cpan.org/search?query=
80 | FreshMeat http://freshmeat.net/
81 | Perl http://search.cpan.org/search?query=
82 | Python http://docs.python.org/lib/module-
83 | php-function http://www.php.net/manual/en/function.%s.php
84 | php-lookup http://www.php.net/manual-lookup.php?pattern=
85 | PEP http://www.python.org/dev/peps/pep-0
86 | PythonInfo http://www.python.org/cgi-bin/moinmoin/
87 | Radar rdar://problem/
88 | OpenRadar http://www.openradar.me/
89 | SourceForge http://sourceforge.net/
90 | 
-------------------------------------------------------------------------------- /pages/meta/LatestPosts/index.txt: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Title: Latest Posts 3 | Date: Sun Feb 18 16:15:00 2007 4 | X-Index: no 5 | X-Cache-Control: max-age=600 6 | 7 | 8 | -------------------------------------------------------------------------------- /pages/meta/MostPopular/index.txt: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Title: Most Popular 3 | Content-Type: text/x-textile 4 | X-Index: No 5 | X-Cache-Control: max-age=300 6 | 7 | The following table lists pages in descending order of popularity for the last 24 hours or less: 8 | 9 | 10 | 11 | Jump to: 12 | 13 | * "Hit Map":meta/HitMap 14 | * "Referrers":meta/Referrers 15 | 16 | *Note:* Hit counts are, at best, an approximation due to heavy front-end caching. 17 | -------------------------------------------------------------------------------- /pages/meta/Quotes/index.txt: -------------------------------------------------------------------------------- 1 | From: Administrator 2 | Title: Quotes 3 | Content-Type: text/x-markdown 4 | X-Index: No 5 | X-TODO: Create plugin to parse this, based on the Highlights plugin 6 | 7 | ### Scott Adams 8 | 9 | * Creativity is allowing yourself to make mistakes. Design is knowing which ones to keep. 10 | 11 | ### Arlen Britton 12 | 13 | * Legibility is not the same thing as readability 14 | 15 | ### Johann Wolfgang von Goethe 16 | 17 | * He who cannot draw on three thousand years is living from hand to mouth. 18 | 19 | ### Mark Boulton 20 | 21 | * I think design covers so much more than the aesthetic. Design is fundamentally more. Design is usability. It is Information Architecture. It is Accessibility. This is all design. -------------------------------------------------------------------------------- /pages/meta/RecentUpdates/index.txt: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Title: Recent Updates 3 | Content-Type: text/x-textile 4 | X-Index: No 5 | X-Cache-Control: max-age=300 6 | 7 | The following table lists recently updated pages on this site: 8 | 9 | 10 | 11 | Note: Minor edits and changes to images and media are not necessarily reflected here. -------------------------------------------------------------------------------- /pages/meta/RecentlyVisited/index.txt: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Title: Recently Visited 3 | Content-Type: text/x-textile 4 | X-Index: No 5 | X-Cache-Control: max-age=300 6 | 7 | The following table lists pages that have been recently visited: 8 | 9 | -------------------------------------------------------------------------------- /pages/meta/Referrers/index.txt: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Title: Referrers 3 | Content-Type: text/x-textile 4 | X-Index: No 5 | X-Cache-Control: max-age=120 6 | 7 | The following table lists external referrers to this site for the last 24 hours or less: 8 | 9 | 10 | 11 | Jump to: 12 | 13 | * "Hit Map":meta/HitMap 14 | * "Most Popular":meta/MostPopular 15 | 16 | *Note:* Hit counts are, at best, an approximation due to heavy front-end caching. 17 | -------------------------------------------------------------------------------- /pages/meta/Search/index.txt: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Date: Wed, 25 Jun 2003 23:44:55 GMT 3 | Title: Search 4 | Tags: meta 5 | Content-Type: text/html 6 | X-Index: no 7 | X-Alias: FullTextSearch 8 | X-Cache-Control: max-age=1 9 | 10 | 11 | -------------------------------------------------------------------------------- /pages/meta/WantedPages/index.txt: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Title: Wanted Pages 3 | Date: 2007-04-07 21:02:13 4 | X-Cache-Control: max-age=120 5 | X-Index: No 6 | 7 | These pages are referenced in the "Wiki":Wiki but do not actually exist yet: 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /pages/meta/Wikipedia/index.txt: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Date: 2008-02-10 11:37:19 3 | Title: Links to Wikipedia 4 | Tags: meta, interwiki, plugins, yaki 5 | X-Cache-Control: max-age=7200 6 | X-Index: no 7 | 8 | This page lists all the links to "Wikipedia":http://www.wikipedia.org scattered throughout this site. The listing is entirely automatic and is re-generated every couple of hours or so: 9 | 10 | -------------------------------------------------------------------------------- /pages/site/about/index.md: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Title: About This Site 3 | Tags: python, about, sushy 4 | Date: 2014-12-17 10:49:34 5 | 6 | This site is running on a [Python][p]-based CMS/Wiki called [Sushy][s]. 7 | 8 | It supports [Textile][t], [Markdown][m] and plain [HTML][h] as markup, has magical inter-page linking, full-text indexing and searching, extensive syntax highlighting support for source code and technical docs, and generally avoids breaking your content by default. 9 | 10 | [t]: Wikipedia:Textile_(markup_language) 11 | [m]: Wikipedia:Markdown 12 | [h]: Wikipedia:HTML 13 | [p]: lang/Python 14 | [s]: https://github.com/rcarmo/sushy 15 | [s]: http://slashdot.org 16 | [hn]: http://news.ycombinator.com 17 | -------------------------------------------------------------------------------- /pages/tests/acronyms/index.md: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Title: Acronym Tests 3 | 4 | Generic `span`: 5 | 6 | FUD 7 | 8 | Generic `caps`: 9 | 10 | BTO 11 | -------------------------------------------------------------------------------- /pages/tests/aliases/index.md: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Title: Alias tests 3 | 4 | Aliases are defined in [meta/Aliases](meta/Aliases) 5 | 6 | * [Markdown](Markdown) - a simple alias 7 | * [Chrome](Chrome) - a nested alias -------------------------------------------------------------------------------- /pages/tests/callouts/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | from: Rui Carmo 3 | title: Callout/Alert tests 4 | --- 5 | 6 | > [!NOTE] 7 | > Highlights information that users should take into account, even when skimming. 8 | 9 | > [!TIP] 10 | > Optional information to help a user be more successful. 11 | 12 | > [!IMPORTANT] 13 | > Crucial information necessary for users to succeed. 14 | 15 | > [!WARNING] 16 | > Critical content demanding immediate user attention due to potential risks. 17 | 18 | > [!CAUTION] 19 | > Negative potential consequences of an action. 20 | 21 | -------------------------------------------------------------------------------- /pages/tests/footnotes/index.md: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Title: Footnotes 3 | Content-Type: text/x-markdown 4 | 5 | Here[^1] 6 | 7 | [^1]: There. 8 | -------------------------------------------------------------------------------- /pages/tests/highlight/animate_svg.js: -------------------------------------------------------------------------------- 1 | var paths = Array.prototype.slice.call(document.querySelectorAll('.animated path'),0); 2 | 3 | paths.map(function(path) { 4 | var bag = document.createAttribute("bag"); 5 | bag.value = path.style.fill; 6 | path.setAttributeNode(bag); 7 | path.style.fill = "white"; 8 | }) 9 | 10 | paths.map(function(path){ 11 | var length = path.getTotalLength(); 12 | path.style.stroke="#000"; 13 | path.style.strokeWidth="5"; 14 | path.style.transition = path.style.WebkitTransition = 'none'; 15 | path.style.strokeDasharray = length + ' ' + length; 16 | path.style.strokeDashoffset = length; 17 | path.getBoundingClientRect(); 18 | path.style.transition = path.style.WebkitTransition = 'stroke-dashoffset 2s ease-in-out'; 19 | path.style.strokeDashoffset = '0'; 20 | }); 21 | 22 | 23 | setTimeout(function(){ 24 | paths.map(function(path){ 25 | path.style.transition = path.style.WebkitTransition = 'fill 2s ease, stroke-width 2s ease'; 26 | path.style.fill = path.getAttribute("bag"); 27 | path.style.strokeWidth = "0"; 28 | }) 29 | },3000) 30 | -------------------------------------------------------------------------------- /pages/tests/highlight/index.md: -------------------------------------------------------------------------------- 1 | From: Rui Carmo 2 | Date: 2013-07-06 23:33:00 3 | Title: Syntax Highlighting Tests 4 | Index: no 5 | 6 | ## Inline [Markdown][m] 7 | 8 | The standard [Markdown][m] triple-backquote form. 9 | 10 | ```clojure 11 | ; parallel consumption of perishable resources 12 | (defn foo [bar drinks] 13 | (pmap (:patrons bar) (lazy-seq drinks))) 14 | ``` 15 | 16 | ## PRE tag with `syntax` attribute 17 | 18 | This is meant for non-[Markdown][m] content. 19 | 20 |
21 | from bottle import view, request, abort
22 | 
23 | @view("rss")
24 | def render_feed()
25 |     if not items:
26 |         abort("418", "I'm a teapot")
27 |     else:
28 |         return {"items": items}
29 | 
30 | 31 | ## From file via `src` attribute 32 | 33 | This slurps the file into the document and highlights it: 34 | 35 |

36 | 
37 | ## From missing file via `src` attribute
38 | 
39 | This should show a nice error message.
40 | 
41 | 

42 | 
43 | [m]: Wikipedia:Markdown
44 | 


--------------------------------------------------------------------------------
/pages/tests/img/gradient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/pages/tests/img/gradient.png


--------------------------------------------------------------------------------
/pages/tests/img/index.md:
--------------------------------------------------------------------------------
 1 | From: Rui Carmo
 2 | Title: Inline Images
 3 | 
 4 | Inserted using [Markdown](Wikipedia:Markdown):
 5 | 
 6 | ![A gradient](./gradient.png)
 7 | 
 8 | Inserted using inline HTML:
 9 | 
10 | 
11 | 
12 | With custom dimensions in HTML:
13 | 
14 | 
15 | 
16 | With custom dimensions in CSS:
17 | 
18 | 
19 | 
20 | An invalid image file:
21 | 
22 | 
23 | 
24 | A missing image file:
25 | 
26 | 
27 | 


--------------------------------------------------------------------------------
/pages/tests/index.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | From: Rui Carmo
 3 | Title: Tests
 4 | ---
 5 | 
 6 | ## To Do:
 7 | 
 8 | * [Aliases](tests/aliases)
 9 | 
10 | ## Passed:
11 | 
12 | * [InterWiki Map](tests/interwiki)
13 | * [Syntax Highlighting](tests/highlight)
14 | * [Inline Images](tests/img)
15 | * [Footnotes](tests/footnotes) (Markdown-only test)
16 | * [ReStructured Text](tests/markup/rst)
17 | 


--------------------------------------------------------------------------------
/pages/tests/interwiki/index.md:
--------------------------------------------------------------------------------
1 | From: Rui Carmo
2 | Title: InterWiki Tests
3 | 
4 | * [Bon Jovi on iTunes](Artist:Bon%20Jovi)
5 | * [Wiki on Wikipedia](Wikipedia:Wiki)
6 | * [HomePage on Tao](Tao:HomePage)
7 | * [RFC 1918](RFC:1918)
8 | * [Blade Runner on IMDB](IMDB:tt0083658)


--------------------------------------------------------------------------------
/pages/tests/markup/jekyll/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: Blogging Like a Hacker
4 | ---
5 | 
6 | This is an imported Jekyll post.
7 | 


--------------------------------------------------------------------------------
/pages/tests/markup/rst/index.rst:
--------------------------------------------------------------------------------
 1 | Title: ReStructured Text
 2 | 
 3 | Chapter 1 Title
 4 | ===============
 5 | 
 6 | Section 1.1 Title
 7 | -----------------
 8 | 
 9 | Subsection 1.1.1 Title
10 | ~~~~~~~~~~~~~~~~~~~~~~
11 | 
12 | Section 1.2 Title
13 | -----------------
14 | 
15 | Chapter 2 Title
16 | ===============
17 | 


--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 | #jupyter
3 | #hy-kernel
4 | pip-upgrader
5 | css-html-js-minify
6 | 


--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
 1 | #aiohttp[speedups]
 2 | cmarkgfm==2024.11.20
 3 | Markdown==3.7
 4 | bottle==0.13.2
 5 | cmarkgfm==2024.11.20
 6 | cssutils==2.11.1
 7 | docutils==0.21.2
 8 | hy==1.1.0
 9 | hyrule==1.0.0
10 | inlinestyler==0.2.5
11 | lxml==5.3.0
12 | nbconvert==7.16.4
13 | peewee==3.17.8
14 | pillow==11.0.0
15 | pip-upgrader
16 | pygments==2.18.0
17 | python-dateutil==2.9.0
18 | python-slugify==8.0.4
19 | pytz>=2024.2
20 | requests==2.32.3
21 | smartypants==2.0.1
22 | textile==4.0.3
23 | watchdog==6.0.0
24 | 


--------------------------------------------------------------------------------
/sushy/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/sushy/__init__.py


--------------------------------------------------------------------------------
/sushy/__main__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/sushy/__main__.py


--------------------------------------------------------------------------------
/sushy/aliasing.hy:
--------------------------------------------------------------------------------
 1 | (import 
 2 |     .config    [ALIASING_CHARS ALIAS_PAGE]
 3 |     .models    [get-all]
 4 |     .transform [get-mappings]
 5 |     os.path    [basename]
 6 |     re         [compile match]
 7 |     slugify    [slugify]
 8 |     functools  [lru-cache cache])
 9 | 
10 | (setv DIGITS_ONLY (compile "^([0-9]+)$"))
11 | 
12 | (defn [cache]
13 |     get-alias-map []
14 |         ; build an alias -> page map for all possible aliases
15 |         (let [alias-map (dict (get-mappings ALIAS_PAGE))
16 |               pages     (get-all)]
17 |             (for [p pages]
18 |                 (let [page  (.get p "name" "/")
19 |                       base  (.lower (basename page))
20 |                       title (.get p "title" "Untitled")
21 |                       slug  (slugify title)]
22 |                     (when (not (.match DIGITS_ONLY base))
23 |                         (do 
24 |                             (assoc alias-map base page)
25 |                             (for [char ALIASING_CHARS]
26 |                                 (assoc alias-map
27 |                                     (.replace base " " char) page
28 |                                     (.replace page " " char) page))
29 |                             (for [char ALIASING_CHARS]
30 |                                 (assoc alias-map
31 |                                     (.replace slug "-" char) page
32 |                                     (.replace title " " char) page))))))
33 |             alias-map))
34 | 
35 | 
36 | (defn [(lru-cache)]
37 |     get-best-match [name]
38 |         ; return the best possible match for a page name
39 |         ; - fallback to a custom database search if not found
40 |         (let [alias (.strip (.lower name))
41 |               base (basename alias)
42 |               alias-map (get-alias-map)]
43 |             (if (in alias alias-map)
44 |                 (get alias-map alias)
45 |                 (if (in base alias-map)
46 |                     (get alias-map base)
47 |                     name))))
48 | 


--------------------------------------------------------------------------------
/sushy/app.hy:
--------------------------------------------------------------------------------
 1 | (import
 2 |     .config      [DEBUG_MODE BIND_ADDRESS HTTP_PORT]
 3 |     bottle       [DEBUG default-app run route view template]
 4 |     logging      [getLogger]
 5 |     sushy.routes)
 6 | 
 7 | (require hyrule [defmain])
 8 | 
 9 | (setv DEBUG DEBUG_MODE)
10 | (setv app (default-app))
11 | 
12 | (defmain [args]
13 |     (run :app   app
14 |          :host  BIND_ADDRESS
15 |          :port  HTTP_PORT
16 |          :debug DEBUG_MODE))
17 | 


--------------------------------------------------------------------------------
/sushy/config.hy:
--------------------------------------------------------------------------------
  1 | (import
  2 |     bottle         [TEMPLATE_PATH template]
  3 |     collections    [defaultdict]
  4 |     datetime       [timedelta]
  5 |     hashlib        [sha1]
  6 |     logging        [getLogger basicConfig DEBUG INFO]
  7 |     logging.config [dictConfig]
  8 |     os             [environ]
  9 |     pytz           [timezone]
 10 |     re
 11 |     sys            [stdout]
 12 |     codecs         [getwriter]
 13 |     os.path        [join abspath])
 14 | 
 15 | (setv log (getLogger __name__))
 16 | 
 17 | ; force stdout to use UTF-8
 18 | (setv stdout ((getwriter "utf-8") stdout))
 19 | 
 20 | ; core settings
 21 | (setv HTTP_PORT (.get environ "PORT" "8080"))
 22 | (setv BIND_ADDRESS     (.get environ "BIND_ADDRESS" "127.0.0.1"))
 23 | (setv SITE_NAME (.get environ "SITE_NAME" "Sushy"))
 24 | (setv SITE_DESCRIPTION (.get environ "SITE_DESCRIPTION" "A Sushy-powered site"))
 25 | (setv SITE_COPYRIGHT   (.get environ "SITE_COPYRIGHT" "CC Attribution-NonCommercial-NoDerivs 3.0"))
 26 | (setv STORE_PATH       (.get environ "CONTENT_PATH" "pages"))
 27 | (setv THEME_PATH       (.get environ "THEME_PATH" "themes/wiki"))
 28 | (setv STATIC_PATH      (join THEME_PATH "static"))
 29 | (setv VIEW_PATH        (join THEME_PATH "views"))
 30 | 
 31 | ; prepend the theme template path to bottle's search list
 32 | (.insert TEMPLATE_PATH 0 (abspath VIEW_PATH))
 33 | 
 34 | (setv TIMEZONE (timezone (.get environ "TIMEZONE" "Europe/Lisbon")))
 35 | 
 36 | ; feed settings
 37 | (setv FEED_CSS (.get environ "FEED_CSS" (+ THEME_PATH "/static/css/rss.css")))
 38 | (setv FEED_TTL 1800); in seconds
 39 | (setv FEED_TIME_WINDOW (timedelta :weeks -4))
 40 | (setv FEED_ITEM_WINDOW 20)
 41 | (setv EXCLUDE_FROM_FEEDS (.compile re "^(meta)/.+$"))
 42 | 
 43 | ; Homepage items
 44 | (setv BLOG_ENTRIES (.compile re "^(blog|links|notes)/.+$"))
 45 | 
 46 | ; UDP remote statistics
 47 | (setv STATS_ADDRESS (.get environ "STATS_ADDRESS" "127.0.0.1"))
 48 | (setv STATS_PORT (int (.get environ "STATS_PORT" "0")))
 49 | 
 50 | ; Base routes
 51 | (setv PAGE_ROUTE_BASE "/space")
 52 | (setv PAGE_MEDIA_BASE "/media")
 53 | (setv BLOG_ARCHIVE_BASE "/archives")
 54 | (setv SCALED_MEDIA_BASE "/thumb")
 55 | 
 56 | ; files that are supposed to be hosted at the site root
 57 | (setv ROOT_JUNK (.join "|" ["favicon.ico" "apple-touch-icon.png" "apple-touch-icon-precomposed.png" "keybase.txt"]))
 58 | 
 59 | ; Image handling
 60 | (setv LAZYLOAD_IMAGES (= (.lower (.get environ "LAZY_LOADING" "false")) "true"))
 61 | 
 62 | ; maximum non-retina image size
 63 | (setv MAX_IMAGE_SIZE    1024)
 64 | (setv MIN_IMAGE_SIZE    16)
 65 | (setv THUMBNAIL_SIZES   [#(40 30) #(160 120) #(320 240) #(640 480) #(1280 720)])
 66 | (setv PLACEHOLDER_IMAGE "/static/img/placeholder.png")
 67 | 
 68 | ; HMAC asset signing
 69 | (setv ASSET_KEY       (.get environ "ASSET_KEY" ""))
 70 | (setv ASSET_HASH      (.hexdigest (sha1 (.encode (+ SITE_NAME SITE_DESCRIPTION ASSET_KEY) "utf-8"))))
 71 | (setv SIGNED_PREFIXES [PAGE_MEDIA_BASE SCALED_MEDIA_BASE])
 72 | (setv ALIASING_CHARS  [" " "." "-" "_" "+"])
 73 | 
 74 | ; meta pages we need to run
 75 | (setv REDIRECT_PAGE      "meta/redirects")
 76 | (setv ALIAS_PAGE         "meta/aliases")
 77 | (setv BANNED_AGENTS_PAGE "meta/bannedagents")
 78 | (setv INTERWIKI_PAGE     "meta/interwikimap")
 79 | (setv LINKS_PAGE         "meta/footer")
 80 | 
 81 | ; markup file extensions
 82 | (setv BASE_TYPES
 83 |     {".txt"      "text/x-textile"; TODO: this should be reverted to text/plain after legacy content is cleared out
 84 |      ".htm"      "text/html"
 85 |      ".html"     "text/html"
 86 |      ".md"       "text/x-markdown"
 87 |      ".mkd"      "text/x-markdown"
 88 |      ".mkdn"     "text/x-markdown"
 89 |      ".markdown" "text/x-markdown"
 90 |      ".textile"  "text/x-textile"})
 91 |      
 92 | (setv BASE_FILENAMES
 93 |     (lfor t (.keys BASE_TYPES) f"index{t}"))
 94 | 
 95 | (setv BASE_PAGE "From: %(author)s\nDate: %(date)s\nContent-Type: %(markup)s\nContent-Encoding: utf-8\nTitle: %(title)s\nKeywords: %(keywords)s\nCategories: %(categories)s\nTags: %(tags)s\n%(_headers)s\n\n%(content)s")
 96 | 
 97 | (setv IGNORED_FOLDERS ["CVS" ".hg" ".svn" ".git" ".AppleDouble" ".TemporaryItems"])
 98 | 
 99 | ; debugging
100 | (setv DEBUG_MODE (= (.lower (.get environ "DEBUG" "false")) "true"))
101 | (setv PROFILER (= (.lower (.get environ "PROFILER" "false")) "true"))
102 | 
103 | (if DEBUG_MODE
104 |     (dictConfig 
105 |         {"version"    1
106 |          "formatters" {"http"    {"format" "localhost - - [%(asctime)s] %(process)d %(levelname)s %(filename)s:%(funcName)s:%(lineno)d %(message)s"
107 |                                   "datefmt" "%Y/%m/%d %H:%M:%S"}}
108 |          "handlers"   {"console" {"class"     "logging.StreamHandler"
109 |                                   "formatter" "http"
110 |                                   "level"     "DEBUG"
111 |                                   "stream"    "ext://sys.stdout"}
112 |                        "ram"     {"class"     "logging.handlers.MemoryHandler"
113 |                                   "formatter" "http"
114 |                                   "level"     "WARNING"
115 |                                   "capacity"  200}}
116 |          "loggers"    {"peewee"       {"level"     "DEBUG"
117 |                                        "handlers"  ["ram" "console"]}
118 |                        "__init__"     {"level" "WARNING"}; for Markdown
119 |                        "sushy.models" {"level" "WARNING"}
120 |                        "sushy.store"  {"level" "WARNING"}}
121 |          "root"       {"level"    "DEBUG" 
122 |                        "handlers" ["console"]}})
123 |     (basicConfig :level INFO :format "%(asctime)s %(levelname)s:%(process)d:%(funcName)s %(message)s"))
124 | 


--------------------------------------------------------------------------------
/sushy/favicons.hy:
--------------------------------------------------------------------------------
 1 | (import 
 2 |     .models      [list-blobs put-blob get-blob]
 3 |     io           [BytesIO]
 4 |     PIL          [Image]
 5 |     requests     [Session Timeout]
 6 |     logging      [getLogger]
 7 |     urllib.parse [urlsplit urljoin]
 8 |     lxml.html    [fromstring]
 9 |     functools    [lru-cache cache])
10 | 
11 | (setv log (getLogger))
12 | (setv fetcher (Session))
13 | (setv fetcher.headers {"User-Agent" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15"})
14 | 
15 | (defn [cache] download-favicon [url]
16 |     ; try to obtain a favicon from a (base) URL
17 |     (let [hostname (get (urlsplit url) 1)
18 |           existing (list (list-blobs))]
19 |         (when (not (in f"favicon:{hostname}" existing))
20 |             (try
21 |                 (let [res (.get fetcher (urljoin url "favicon.ico") :allow-redirects True :timeout 2)]
22 |                     (if (= 200 res.status_code)
23 |                         (save-favicon hostname res.content)
24 |                         (let [href (parse-favicon url)]
25 |                             (when href 
26 |                               (let [res (.get fetcher href :allow-redirects True :timeout 2)]
27 |                                   (when (= 200 res.status_code)
28 |                                       (save-favicon hostname res.content)))))))
29 |                 (except [e Exception]
30 |                     (.warn log f"{e} {url}"))))))
31 | 
32 | (defn parse-favicon [url]
33 |     ; get first usable icon candidate (or none) from HTML page
34 |     (let [res (.get fetcher url :allow-redirects True :timeout 2)]
35 |         (when (= 200 res.status_code)
36 |            (let [doc (fromstring res.content) 
37 |                  href ""]
38 |                (for [rel ["shortcut icon" "icon" "apple-touch-icon" "apple-touch-icon-precomposed"]]
39 |                   (for [tag (.xpath doc f"//link[@rel='{rel}']")]
40 |                       (setv href (get (. tag attrib) "href"))
41 |                       (when href (break)))
42 |                   (when href (break)))
43 |                (when href
44 |                   (if (get (urlsplit href) 0)
45 |                       href
46 |                       (urljoin url href)))))))
47 | 
48 | (defn save-favicon [hostname data]
49 |     ; try to resize and save the favicon (we don't care about SVGs, so this always assumes we got bitmap data of some sort)
50 |     (try
51 |         (let [im (.open Image (BytesIO data))
52 |               buffer (BytesIO)
53 |               _ (.save (.resize im #(48 48)) buffer :format "PNG")]
54 |             (put-blob :name f"favicon:{hostname}" :mimetype "image/png" :data (.getvalue buffer)))
55 |         (except [e Exception]
56 |             (.warn log f"{e} {hostname}"))))
57 | 
58 | (defn [(lru-cache 20)] get-favicon [hostname]
59 |     ; retrieve a stored favicon. We assume they're always tiny PNGs
60 |     (get (get-blob f"favicon:{hostname}") "data"))
61 | 


--------------------------------------------------------------------------------
/sushy/feeds.hy:
--------------------------------------------------------------------------------
 1 | (import
 2 |     .config            [EXCLUDE_FROM_FEEDS FEED_CSS FEED_TIME_WINDOW FEED_ITEM_WINDOW]
 3 |     .models            [get-latest]
 4 |     .render            [render-page]
 5 |     .store             [get-page]
 6 |     .transform         [apply-transforms inner-html prepend-asset-sources remove-preloads]
 7 |     .utils             [utc-date strip-timezone]
 8 |     cssutils           [log :as cssutils-log]
 9 |     datetime           [datetime]
10 |     inlinestyler.utils [inline-css]
11 |     logging            [getLogger Formatter ERROR]
12 |     lxml.etree         [Element HTML fromstring tostring]
13 |     os.path            [abspath]
14 |     pytz               [UTC]
15 |     hyrule.collections [assoc])
16 | 
17 | (setv log (getLogger __name__))
18 | 
19 | ; disable logging for cssutils, since it is incredibly whiny
20 | (.setLevel cssutils-log ERROR)
21 | 
22 | (setv INLINE_CSS (.read (open (abspath FEED_CSS) "r")))
23 | 
24 | 
25 | (defn filtered-latest []
26 |     ; get the latest (eligible) updates 
27 |     (let [time-window (strip-timezone (utc-date (+ (.now datetime) FEED_TIME_WINDOW)))]
28 |         (filter (fn [x] (not (.match EXCLUDE_FROM_FEEDS (.get x "name")))) (get-latest :since time-window :limit FEED_ITEM_WINDOW))))
29 | 
30 | 
31 | (defn render-feed-items [[prefix ""]]
32 |     ; go through each item and replace inline styles
33 |     (let [items []]
34 |         (for [item (filtered-latest)]
35 |             (let [pagename (.get item "name")
36 |                   page     (get-page pagename)
37 |                   ; serve only final image assets, not preload stubs
38 |                   doc      (prepend-asset-sources (remove-preloads (apply-transforms (render-page page) pagename)) prefix)
39 |                   head     (Element "head")
40 |                   style    (Element "style")]
41 |                 (setv style.text INLINE_CSS)
42 |                 (.append head style)
43 |                 (.append doc head)
44 |                 (assoc item
45 |                     "pagename"    pagename
46 |                     "author"      (get (:headers page) "from")
47 |                     "mtime"       (utc-date (get item "mtime"))
48 |                     "pubdate"     (utc-date (get item "pubtime"))
49 |                     "tags"        (list (filter len (map (fn [x] (.strip x)) (.split (get item "tags") ","))))
50 |                     "description" (inner-html (HTML (inline-css (tostring doc :method "html"))))
51 |                     "category"    (get (.split pagename "/") 0))
52 |                 (.append items item)))
53 |         items))
54 | 


--------------------------------------------------------------------------------
/sushy/indexer.hy:
--------------------------------------------------------------------------------
  1 | (import
  2 |     .config            [BASE_FILENAMES STORE_PATH TIMEZONE PROFILER]
  3 |     .models            [db add-wiki-links delete-wiki-page index-wiki-page init-db get-page-indexing-time]
  4 |     .render            [render-page]
  5 |     .store             [is-page? gen-pages get-page]
  6 |     .transform         [apply-transforms count-images extract-internal-links extract-plaintext]
  7 |     .utils             [parse-naive-date strip-timezone slug utc-date]
  8 |     cProfile           [Profile]
  9 |     datetime           [datetime timedelta]
 10 |     functools          [reduce]
 11 |     hashlib            [sha1]
 12 |     json               [dumps]
 13 |     logging            [getLogger Formatter]
 14 |     os                 [environ]
 15 |     os.path            [basename dirname join]
 16 |     pstats             [Stats]
 17 |     time               [sleep time]
 18 |     watchdog.observers [Observer]
 19 |     watchdog.events    [FileSystemEventHandler])
 20 | 
 21 | (require hyrule [defmain])
 22 | 
 23 | (setv log (getLogger __name__))
 24 | 
 25 | (setv LOGGING_MODULO 100)
 26 | 
 27 | (defn transform-tags [line]
 28 |     ; expand tags to be "tag:value", which enables us to search for tags using FTS
 29 |     (let [tags (.split (.strip line) ",")]
 30 |         (if (!= tags [""])
 31 |             (.lower (.join ", " (sorted (list (set (map (fn [tag] (+ "tag:" (.strip tag))) tags))))))
 32 |             "")))
 33 | 
 34 | 
 35 | (defn hide-from-search? [headers]
 36 |     (reduce (fn [x y] (or x y))
 37 |         (map (fn [header]
 38 |                 (if (and (in header headers)
 39 |                          (in (.lower (get headers header)) ["off" "no" "false"]))
 40 |                     True
 41 |                     False))
 42 |             ["x-index" "index" "search"])
 43 |         False))
 44 | 
 45 | 
 46 | (defn published? [headers]
 47 |     (reduce (fn [x y] (and x y))
 48 |         (map (fn [header]
 49 |                 (if (and (in header headers)
 50 |                          (in (.lower (get headers header)) ["off" "no" "false"]))
 51 |                     False
 52 |                     True))
 53 |             ["visible" "published"])
 54 |         True))
 55 | 
 56 | 
 57 | (defn gather-item-data [item]
 58 |     ; Takes a map with basic item info and builds all the required indexing data
 59 |     (.debug log (:path item))
 60 |     (let [pagename     (:path item)
 61 |           mtime        (:mtime item)
 62 |           mdate        (.fromtimestamp datetime (:mtime item))
 63 |           page         (get-page pagename)
 64 |           headers      (:headers page)
 65 |           doc          (apply-transforms (render-page page) pagename)
 66 |           plaintext    (extract-plaintext doc)
 67 |           word-count   (len (.split plaintext))
 68 |           image-count  (count-images doc)
 69 |           links        (extract-internal-links doc)
 70 |           pubtime      (parse-naive-date (.get headers "date") mdate TIMEZONE)]
 71 |         {"name"     pagename
 72 |          "body"     (if (hide-from-search? headers) "" plaintext)
 73 |          "hash"     (.hexdigest (sha1 (.encode plaintext "utf-8")))
 74 |          "title"    (.get headers "title" "Untitled")
 75 |          "tags"     (transform-tags (.get headers "tags" ""))
 76 |          "pubtime"  (strip-timezone (utc-date pubtime))
 77 |          "mtime"    (strip-timezone (utc-date (parse-naive-date (.get headers "last-modified") pubtime TIMEZONE)))
 78 |          "idxtime"  mtime
 79 |          "readtime" (int (round (+ (* 12.0 image-count) (/ word-count 4.5))))
 80 |          "headers"  headers
 81 |          "links"    (list links)}))
 82 | 
 83 | 
 84 | (defn index-one [item]
 85 |     (try
 86 |         (let [page    (.get item "name")
 87 |               headers (.get item "headers")
 88 |               links   (map (fn [l] {"page" page "link" l}) (.get item "links"))]
 89 |             (if (published? headers)
 90 |                 (do
 91 |                     (add-wiki-links links)
 92 |                     (index-wiki-page #** item))
 93 |                 (delete-wiki-page page)))
 94 |         (except [e Exception]
 95 |             (.warning log f"{(type e)}:{e} handling {item}"))))
 96 | 
 97 | 
 98 | (defn filesystem-walk [path [suffix ""]]
 99 |     ; walk the filesystem and perform full-text and front matter indexing
100 |     (let [item-count    0
101 |           skipped-count 0]
102 |         (for [item (gen-pages path)]
103 |             (.debug log item)
104 |             (when (= 0 (% item-count LOGGING_MODULO))
105 |                 (.info log f"indexing {item-count}"))
106 |             (setv item-count (+ 1 item-count))
107 |             (.debug log (:path item))
108 |             (setv idxtime (get-page-indexing-time (:path item)))
109 |             (if (not idxtime)
110 |                 (index-one (gather-item-data item))
111 |                 (if (> (:mtime item) idxtime)
112 |                     (index-one (gather-item-data item))
113 |                     (setv skipped-count (+ 1 skipped-count)))))
114 |         (.info log f"exiting filesystem walker: {item-count} indexed, {skipped-count} skipped")))
115 | 
116 | 
117 | (defclass IndexingHandler [FileSystemEventHandler]
118 |     ; handle file notifications
119 |      (defn __init__ [self]
120 |             (.debug log "preparing to listen for filesystem events"))
121 | 
122 |      (defn do-update [self path]
123 |             (.info log f"updating {path}")
124 |             (index-one (gather-item-data
125 |                         {"path"  (get path (slice (+ 1 (len STORE_PATH)) None))
126 |                          "mtime" (int (time))})))
127 | 
128 |      (defn do-delete [self path]
129 |             (.debug log  f"deleting {path}")
130 |             (delete-wiki-page (get path (slice (+ 1 (len STORE_PATH) None)))))
131 | 
132 |      (defn on-created [self event]
133 |             (.debug log f"creation of {event}")
134 |             (let [filename (basename (. event src-path))
135 |                   path     (dirname  (. event src-path))]
136 |                 (when (in filename BASE_FILENAMES)
137 |                     (.do-update self path))))
138 | 
139 |      (defn on-deleted [self event]
140 |             (.debug log f"deletion of {event}")
141 |             (let [filename (basename (. event src-path))
142 |                   path     (dirname  (. event src-path))]
143 |                 (when (in filename BASE_FILENAMES)
144 |                     (.do-delete self path))))
145 | 
146 |      (defn on-modified [self event]
147 |             (.debug log f"modification of {event}")
148 |             (let [filename (basename (. event src-path))
149 |                   path     (dirname  (. event src-path))]
150 |                 (when (in filename BASE_FILENAMES)
151 |                     (.do-update self path))))
152 | 
153 |      (defn on-moved [self event]
154 |             (.debug log f"renaming of {event}")
155 |             (let [srcfile (basename (. event src-path))
156 |                   srcpath (dirname  (. event src-path))
157 |                   dstfile (basename (. event dest-path))
158 |                   dstpath (dirname  (. event dest-path))]
159 |                 (when (in srcfile BASE_FILENAMES)
160 |                     (.do-delete self srcpath))
161 |                 (when (in dstfile BASE_FILENAMES)
162 |                     (.do-update self dstpath)))))
163 | 
164 | 
165 | (defn observer [path]
166 |     ; file change observer setup
167 |     (let [observer (Observer)
168 |           handler  (IndexingHandler)]
169 |         (.debug log f"Preparing to watch {path}")
170 |         (.schedule [observer handler path] :recursive True)
171 |         (.start observer)
172 |         (try
173 |             (while true
174 |                 (sleep 1))
175 |             (catch [e KeyboardInterrupt]
176 |                 (.stop observer)))
177 |         (.join observer)))
178 | 
179 | 
180 | (defn fast-start [n]
181 |     ; TODO: fast start indexing by peeking at the past 3 months 
182 |     (let [when (.now datetime) delta (timedelta :weeks -4)]
183 |         (for [step (range 0 4)]
184 |            (yield (.strftime (+ when (* step delta)) "%Y/%m")))))
185 | 
186 | 
187 | (defmain [#* args]
188 |     (let [p  (Profile)]
189 |         (when PROFILER
190 |             (.enable p))
191 |         (init-db)
192 |         ; close database connection to remove contention
193 |         (.close db)
194 |         (setv start-time (time))
195 |         
196 |         (filesystem-walk STORE_PATH)
197 |         (.info log f"Indexing done in {(- (time) start-time)}s")
198 |         (when PROFILER
199 |             (.disable p)
200 |             (.info log "dumping stats")
201 |             (.dump_stats (Stats p) "indexer.pstats"))
202 |         (when (in "watch" args)
203 |             (.info log "Starting watcher...")
204 |             (observer STORE_PATH))))
205 | 


--------------------------------------------------------------------------------
/sushy/messages.hy:
--------------------------------------------------------------------------------
 1 | (import 
 2 |     .config [PAGE_ROUTE_BASE]
 3 |     bottle  [view])
 4 | 
 5 | ; render a little error message
 6 | (defn [(view "inline-message")]
 7 |     inline-message [level message]
 8 |         {"level"   level
 9 |          "message" message})
10 |          
11 | ; render a table
12 | (defn [(view "inline-table")]
13 |      inline-table [headers rows]
14 |         {"headers"   headers
15 |          "rows"      rows
16 |          "page_base" PAGE_ROUTE_BASE})
17 | 


--------------------------------------------------------------------------------
/sushy/models.py:
--------------------------------------------------------------------------------
  1 | import re, sys, logging
  2 | from bottle import hook
  3 | from os import environ
  4 | import datetime
  5 | from dateutil.relativedelta import relativedelta
  6 | from difflib import SequenceMatcher
  7 | from peewee import *
  8 | from playhouse.sqlite_ext import *
  9 | from os.path import basename
 10 | 
 11 | log = logging.getLogger(__name__)
 12 | 
 13 | # Database models for metadata caching and full text indexing using SQLite3 
 14 | # (handily beats Whoosh and makes for a single index file)
 15 | 
 16 | db = SqliteExtDatabase(environ['DATABASE_PATH'],regexp_function=True,rank_functions=True)
 17 | 
 18 | class Page(Model):
 19 |     """Page information"""
 20 |     name        = FixedCharField(primary_key=True, max_length=128)
 21 |     title       = FixedCharField(null=True, index=True, max_length=128)
 22 |     tags        = FixedCharField(null=True, index=True, max_length=256)
 23 |     hash        = FixedCharField(null=True, index=True, max_length=64) # 40-char plaintext hash, used for etags
 24 |     mtime       = DateTimeField(index=True) # UTC
 25 |     pubtime     = DateTimeField(index=True) # UTC
 26 |     idxtime     = IntegerField(index=True) # epoch
 27 |     readtime    = IntegerField(null=True) # seconds
 28 | 
 29 |     class Meta:
 30 |         database = db
 31 | 
 32 | 
 33 | class Link(Model):
 34 |     """Links between pages - doesn't use ForeignKeys since pages may not exist"""
 35 |     page = CharField()
 36 |     link = CharField()
 37 | 
 38 |     class Meta:
 39 |         indexes = (
 40 |             (('page', 'link'), True),
 41 |         )
 42 |         database = db
 43 | 
 44 | 
 45 | class FTSPage(FTSModel):
 46 |     """Full text indexing"""
 47 |     page = ForeignKeyField(Page, index=True)
 48 |     title = TextField()
 49 |     tags = TextField()
 50 |     body = TextField()
 51 | 
 52 |     class Meta:
 53 |         database = db
 54 |         extension_options = {'tokenize': 'porter'}
 55 | 
 56 | 
 57 | class Blob(Model):
 58 |     name = FixedCharField(null=False, index=True, max_length=128)
 59 |     mimetype = FixedCharField(null=False, max_length=32)
 60 |     data = BlobField()
 61 | 
 62 |     class Meta:
 63 |         database = db
 64 | 
 65 | 
 66 | def init_db():
 67 |     """Initialize the database"""
 68 |     db.execute_sql('PRAGMA journal_mode=WAL')
 69 |     try:
 70 |         Blob.create_table()
 71 |         Page.create_table()
 72 |         Link.create_table()
 73 |         FTSPage.create_table()
 74 |         log.debug("tables created")
 75 |     except OperationalError as e:
 76 |         log.info(e)
 77 | 
 78 | 
 79 | def add_wiki_links(links):
 80 |     """Adds a set of wiki links"""
 81 |     with db.atomic(): # deferring transactions gives us a nice speed boost
 82 |         for l in links:
 83 |             try:
 84 |                 return Link.create(**l)
 85 |             except IntegrityError as e:
 86 |                 log.debug(e) # skip duplicate links
 87 | 
 88 | 
 89 | def delete_wiki_page(page):
 90 |     """Deletes all the entries for a page"""
 91 |     with db.atomic():
 92 |         try:
 93 |             FTSPage.delete().where(FTSPage.page == page).execute()
 94 |             Page.delete().where(Page.name == page).execute()
 95 |             Link.delete().where(Link.page == page).execute()
 96 |         except Exception as e:
 97 |             log.warn(e)
 98 | 
 99 | 
100 | def index_wiki_page(**kwargs):
101 |     """Adds wiki page metatada and FTS data."""
102 |     with db.atomic():
103 |         values = {}
104 |         for k in [u"name", u"title", u"tags", u"hash", u"mtime", u"pubtime", u"idxtime", u"readtime"]:
105 |             values[k] = kwargs[k]
106 |         log.debug(values)
107 |         try:
108 |             page = Page.create(**values)
109 |         except IntegrityError:
110 |             page = Page.get(Page.name == values['name'])
111 |             page.update(**values)
112 |         if len(kwargs['body']):
113 |             values['body'] = kwargs['body']
114 |             # Not too happy about this, but FTS update() seems to be buggy and indexes keep growing
115 |             FTSPage.delete().where(FTSPage.page == page).execute()
116 |             FTSPage.create(page = page, **values)
117 |         return page
118 | 
119 | 
120 | def get_page_metadata(name):
121 |     """accessor for page metadata"""
122 |     try:
123 |         return Page.get(Page.name == name)._data
124 |     except Exception as e:
125 |         log.warn(e)
126 |         return None
127 | 
128 | 
129 | def get_links(page_name):
130 |     """Backlinks (links to current page)"""
131 |     try:
132 |         query = (Page.select()
133 |                     .join(Link, on=(Link.page == Page.name))
134 |                     .where((Link.link == page_name))
135 |                     .order_by(SQL('mtime').desc())
136 |                     .dicts())
137 | 
138 |         for page in query:
139 |             yield page
140 | 
141 |         # Links from current page to valid pages
142 |         query = (Page.select()
143 |                     .join(Link, on=(Link.link == Page.name))
144 |                     .where((Link.page == page_name))
145 |                     .order_by(SQL('mtime').desc())
146 |                     .dicts())
147 | 
148 |         for page in query:
149 |             yield page
150 |     except OperationalError as e:
151 |         log.warn(e)
152 |         return        
153 | 
154 | 
155 | def get_page_indexing_time(name):
156 |     """Check when a page was last indexed"""
157 |     try:
158 |         return Page.get(Page.name == name).idxtime
159 |     except Exception as e:
160 |         return None
161 | 
162 | 
163 | def get_last_update_time():
164 |     """Check when a page was last updated by the user"""
165 |     query = (Page.select()
166 |             .order_by(SQL('mtime').desc())
167 |             .limit(1)
168 |             .dicts())
169 |     for page in query:
170 |         return page["mtime"]
171 | 
172 | 
173 | def get_latest(limit=20, since=None, regexp=None):
174 |     """Get the latest pages by modification time"""
175 |     if regexp:
176 |         if since:
177 |             query = (Page.select()
178 |                     .where(Page.name.regexp(regexp.pattern) and Page.mtime > since)
179 |                     .order_by(SQL('mtime').desc())
180 |                     .limit(limit)
181 |                     .dicts())
182 |         else:
183 |             query = (Page.select()
184 |                     .where(Page.name.regexp(regexp.pattern))
185 |                     .order_by(SQL('mtime').desc())
186 |                     .limit(limit)
187 |                     .dicts())
188 |     else:
189 |         if since:
190 |             query = (Page.select()
191 |                     .where(Page.mtime > since)
192 |                     .order_by(SQL('mtime').desc())
193 |                     .limit(limit)
194 |                     .dicts())
195 |         else:
196 |             query = (Page.select()
197 |                     .order_by(SQL('mtime').desc())
198 |                     .limit(limit)
199 |                     .dicts())
200 | 
201 |     for page in query:
202 |         yield page
203 | 
204 | 
205 | def get_all():
206 |     """Get ALL the pages"""
207 |     query = (Page.select()
208 |             .order_by(SQL('mtime').desc())
209 |             .dicts())
210 | 
211 |     for page in query:
212 |         yield page
213 | 
214 | 
215 | def list_blobs():
216 |     """Get ALL the pages"""
217 |     query = (Blob.select(Blob.name)
218 |             .dicts())
219 | 
220 |     for blob in query:
221 |         yield blob["name"]
222 | 
223 | 
224 | def get_blob(name: str) -> dict:
225 |     """Simple blob retrieval"""
226 |     return Blobs.get(Blobs.name == name)._data
227 | 
228 | 
229 | def put_blob(**kwargs) -> Blob:
230 |     """Simple blob storage"""
231 |     with db.atomic():
232 |         values = {}
233 |         for k in [u"name", u"mimetype", u"data"]:
234 |             values[k] = kwargs[k]
235 |         try:
236 |             blob = Blob.create(**values)
237 |         except IntegrityError:
238 |             blob = Blob.get(Blob.name == values['name'])
239 |             blob.update(**values)
240 |         return blob
241 | 
242 | 
243 | def delete_blob(name: str) -> None:
244 |     """Simple blob removal"""
245 |     with db.atomic():   
246 |         try:
247 |             Blobs.delete().where(Blobs.name == name).execute()
248 |         except Exception as e:
249 |             log.warn(e)
250 | 
251 | 
252 | def search(qstring, limit=50):
253 |     """Full text search"""
254 |     query = (FTSPage.select(Page,
255 |                             FTSPage,
256 |                             fn.snippet(FTSPage._meta.entity).alias('extract'),
257 |                             FTSPage.bm25().alias('score'))
258 |                     .join(Page)
259 |                     .where(FTSPage.match(qstring))
260 |                     .order_by(SQL('score').asc())
261 |                     #.order_by(Page.mtime.desc())
262 |                     .limit(limit))
263 | 
264 |     for page in query:
265 |         yield {
266 |             "content"     : page.extract,
267 |             "title"       : page.page.title,
268 |             "score"       : round(page.score, 2),
269 |             "mtime"       : page.page.mtime,
270 |             "tags"        : page.page.tags,
271 |             "name"        : page.page.name
272 |         }
273 | 
274 | 
275 | def get_prev_by_name(name):
276 |     """Get the previous page by page name"""
277 |     query = (Page.select(Page.name, Page.title)
278 |             .where(Page.name < name)
279 |             .order_by(Page.name.desc())
280 |             .limit(1)
281 |             .dicts())
282 |     for p in query:
283 |         return p
284 | 
285 | 
286 | def get_next_by_name(name):
287 |     """Get the next page by page name"""
288 |     query = (Page.select(Page.name, Page.title)
289 |             .where(Page.name > name)
290 |             .order_by(Page.name.asc())
291 |             .limit(1)
292 |             .dicts())
293 |     for p in query:
294 |         return p
295 | 
296 | 
297 | def get_prev_by_date(name, regexp):
298 |     """Get the previous page by page publishing date"""
299 |     p = Page.get(Page.name == name)
300 |     query = (Page.select(Page.name, Page.title)
301 |             .where(Page.pubtime < p.pubtime)
302 |             .order_by(Page.pubtime.desc())
303 |             .dicts())
304 |     for p in filter(lambda x: regexp.match(x['name']), query):
305 |         return p
306 | 
307 | 
308 | def get_next_by_date(name, regexp):
309 |     """Get the next page by page publishing date"""
310 |     p = Page.get(Page.name == name)
311 |     query = (Page.select(Page.name, Page.title)
312 |             .where(Page.pubtime > p.pubtime)
313 |             .order_by(Page.pubtime.asc())
314 |             .dicts())
315 |     for p in filter(lambda x: regexp.match(x['name']), query):
316 |         return p
317 |             
318 | 
319 | def get_prev_next(name, regexp = None):
320 |     """Get the previous/next page depending on a pattern"""
321 |     try:
322 |         if regexp:
323 |             p, n = get_prev_by_date(name, regexp), get_next_by_date(name, regexp)
324 |         else:
325 |             p, n = get_prev_by_name(name), get_next_by_name(name)
326 |         return p, n
327 |     except Exception as e:
328 |         log.warn(e)
329 |         return None, None
330 | 
331 | 
332 | def get_table_stats():
333 |     """Database stats"""
334 |     return {
335 |         "pages": Page.select().count(),
336 |         "links": Link.select().count(),
337 |         "fts": FTSPage.select().count()
338 |     }
339 | 
340 | 
341 | @hook('before_request')
342 | def _connect_db():
343 |     db.connect()
344 | 
345 | 
346 | @hook('after_request')
347 | def _close_db():
348 |     if not db.is_closed():
349 |         db.close()
350 | 


--------------------------------------------------------------------------------
/sushy/plugins.hy:
--------------------------------------------------------------------------------
 1 | ; Handle legacy Yaki plugin tags
 2 | (import
 3 |     .config    [SCALED_MEDIA_BASE]
 4 |     .messages  [inline-message inline-table]
 5 |     .models    [search]
 6 |     .utils     [compute-hmac memoize get-image-size]
 7 |     logging    [getLogger]
 8 |     lxml.etree [fromstring tostring])
 9 | 
10 | (setv log (getLogger __name__))
11 | 
12 | (defn plugin-tagged
13 |     ; searches for `plugin` tags named `tagged`
14 |     [doc]
15 |     (for [tag (.xpath doc "//plugin[contains(@name,'tagged')]")]
16 |         (let [tagname (get tag.attrib "src")]
17 |             (try
18 |                 (.replace (.getparent tag) tag
19 |                     (fromstring (inline-table ["Page" "name" "Modified" "mtime"]
20 |                                               (search (+ "tag:" tagname) -1))))
21 |                 (catch [e Exception]
22 |                     (.replace (.getparent tag) tag
23 |                         (fromstring (inline-message "error" (% "Could not list pages tagged with '%s'" tagname))))))))
24 |     doc)
25 |     
26 | 
27 | (defn plugin-rating
28 |     ; searches for `plugin` tags named `rating`
29 |     [doc]
30 |     (for [tag (.xpath doc "//plugin[contains(@name,'rating')]")]
31 |         (let [value (int (get tag.attrib "value"))]
32 |             (.replace (.getparent tag) tag
33 |                 (fromstring (% "%s" (* "★" value))))))
34 |     doc)
35 |     
36 | 
37 | (defn plugin-quicklook
38 |     ; searches for `plugin` tags named `quicklook` and generates a rendering request for a 2x image
39 |     [doc pagename [x 320] [y 240]]
40 |     (for [tag (.xpath doc "//plugin[contains(@name,'quicklook')]")]
41 |         (let [src  (get tag.attrib "src")
42 |               path (.join "/" [pagename src])]
43 |             (.replace (.getparent tag) tag
44 |                 (fromstring f""))))
45 |     doc)
46 | 


--------------------------------------------------------------------------------
/sushy/render.hy:
--------------------------------------------------------------------------------
 1 | (import
 2 |     ; docutils.core       [publish-parts]
 3 |     .models             [get-latest]
 4 |     .store              [get-page]
 5 |     json                [loads]
 6 |     logging             [getLogger]
 7 |     lxml.etree          [Element tostring fromstring]
 8 |     markdown            [Markdown]
 9 |     cmarkgfm            [github-flavored-markdown-to-html]
10 |     cmarkgfm._cmark.lib [CMARK_OPT_UNSAFE CMARK_OPT_SMART CMARK_OPT_NORMALIZE CMARK_OPT_FOOTNOTES CMARK_OPT_VALIDATE_UTF8 CMARK_OPT_GITHUB_PRE_LANG CMARK_OPT_TABLE_PREFER_STYLE_ATTRIBUTES]
11 |     re                  [sub]
12 |     smartypants         [smartypants]
13 |     textile             [Textile]
14 |     time                [time])
15 | 
16 | (setv log (getLogger))
17 | 
18 | ; instantiate markdown renderer upon module load
19 | (setv markdown-renderer
20 |      (Markdown :extensions ["markdown.extensions.extra" 
21 |                             "markdown.extensions.toc" 
22 |                             "markdown.extensions.smarty" 
23 |                             "markdown.extensions.codehilite" 
24 |                             "markdown.extensions.meta" 
25 |                             "markdown.extensions.sane_lists"]
26 |                :extension_configs {"markdown.extensions.codehilite" {"css_class" "highlight"}}))
27 | 
28 | (setv textile-renderer
29 |     (Textile :html_type "html5"))
30 | 
31 | 
32 | (defn render-html [raw]
33 |     (let [res (.strip raw)]
34 |         (if (len res)
35 |             res 
36 |             "")))
37 |     
38 | 
39 | (defn render-plaintext [raw]
40 |     f"
\n{raw}
") 41 | 42 | 43 | ;(defn render-restructured-text [raw] 44 | ; (get (apply publish-parts [raw] {"writer_name" "html"}) "html_body")) 45 | 46 | 47 | (defn render-markdown [raw] 48 | (.reset markdown-renderer) 49 | (.convert markdown-renderer raw)) 50 | 51 | (defn render-gfm [raw] 52 | (github-flavored-markdown-to-html raw 53 | :options (| CMARK_OPT_UNSAFE 54 | CMARK_OPT_SMART 55 | CMARK_OPT_NORMALIZE 56 | CMARK_OPT_FOOTNOTES 57 | CMARK_OPT_VALIDATE_UTF8 58 | CMARK_OPT_GITHUB_PRE_LANG 59 | CMARK_OPT_TABLE_PREFER_STYLE_ATTRIBUTES))) 60 | 61 | (defn render-textile [raw] 62 | (smartypants (textile-renderer.parse raw))) 63 | 64 | 65 | (setv render-map 66 | {"text/plain" render-plaintext 67 | "text/x-web-markdown" render-gfm 68 | "text/x-markdown" render-gfm 69 | "text/markdown" render-gfm 70 | "text/textile" render-textile 71 | "text/x-textile" render-textile 72 | "text/html" render-html}) 73 | 74 | 75 | (defn render-page [page] 76 | (.debug log (:headers page)) 77 | ((get render-map (get (:headers page) "content-type")) (:body page))) 78 | 79 | 80 | (defn sanitize-title [title] 81 | (sub "[\\W+]" "-" (.lower title))) 82 | -------------------------------------------------------------------------------- /sushy/store.hy: -------------------------------------------------------------------------------- 1 | ; Find, retrieve and parse raw page markup 2 | 3 | (import 4 | codecs [open] 5 | .config [BASE_FILENAMES BASE_TYPES IGNORED_FOLDERS STORE_PATH TIMEZONE] 6 | datetime [datetime] 7 | hyrule.collections [assoc] 8 | logging [getLogger] 9 | os [walk] 10 | os.path [join exists splitext getmtime] 11 | .utils [utc-date] 12 | hyrule.collections [assoc]) 13 | 14 | (require hyrule.argmove [->]) 15 | 16 | (setv log (getLogger __name__)) 17 | 18 | 19 | (defn strip-seq [string-sequence] 20 | ; strip whitespace from a sequence of strings 21 | (map (fn [buffer] (.strip buffer)) string-sequence)) 22 | 23 | 24 | (defn split-header-line [string] 25 | ; parse a header line from front matter 26 | (if (.startswith "---" string) ; handle Jekyll-style front matter delimiters 27 | ["jekyll" "true"] 28 | (let [parts (list (strip-seq (.split string ":" 1)))] 29 | [(.lower (get parts 0)) (get parts 1)]))) 30 | 31 | 32 | (defn parse-page [buffer [content-type "text/plain"]] 33 | ; parse a page and return a header map and the raw markup 34 | (let [unix-buffer (.replace buffer "\r\n" "\n")] 35 | (try 36 | (let [delimiter "\n\n" 37 | parts (.split unix-buffer delimiter 1) 38 | header-lines (.splitlines (get parts 0)) 39 | headers (dict (map split-header-line header-lines)) 40 | body (.strip (get parts 1))] 41 | (when (not (in "from" headers)) 42 | (assoc headers "from" "Unknown Author")) 43 | (when (not (in "content-type" headers)) 44 | (assoc headers "content-type" content-type)) 45 | {"headers" headers 46 | "body" body}) 47 | (except [e Exception] 48 | (.error log f"{e} Could not parse page") 49 | (raise (RuntimeError "Could not parse page")))))) 50 | 51 | 52 | (defn asset-path [pagename asset] 53 | (join STORE_PATH pagename asset)) 54 | 55 | 56 | (defn asset-exists? [pagename asset] 57 | (exists (asset-path pagename asset))) 58 | 59 | 60 | (defn open-asset [pagename asset] 61 | ; open a page asset/attachment 62 | (let [filename (asset-path pagename asset)] 63 | (open filename "rb"))) 64 | 65 | 66 | (defn page-exists? [pagename] 67 | (is-page? (join STORE_PATH pagename))) 68 | 69 | 70 | (defn is-page? [path] 71 | ; test if a given path contains an index filename 72 | (if (len (list (filter (fn [item] (exists (join path item))) BASE_FILENAMES))) 73 | True 74 | False)) 75 | 76 | 77 | (defn get-page [pagename] 78 | ; return the raw data for a page 79 | (.debug log (join STORE_PATH pagename)) 80 | (try 81 | (let [path (join STORE_PATH pagename) 82 | page (next (filter (fn [item] (exists (join path item))) BASE_FILENAMES)) 83 | filename (join STORE_PATH pagename page) 84 | content-type (get BASE_TYPES (get (splitext page) 1)) 85 | handle (open filename :mode "r" :encoding "utf-8") 86 | buffer (.read handle)] 87 | (.close handle) 88 | (parse-page buffer content-type)) 89 | (except [e StopIteration] 90 | (raise (IOError f"page not found {pagename}"))))) 91 | 92 | 93 | (defn filtered-names [folder-list] 94 | ; remove ignored folders from a list 95 | (filter (fn [folder-name] (not (in folder-name IGNORED_FOLDERS))) folder-list)) 96 | 97 | 98 | (defn scan-pages [root-path] 99 | ; gather all existing pages 100 | (let [pages {}] 101 | (reduce 102 | (fn [item] 103 | (assoc pages (:path item) item)) 104 | (gen-pages root-path)) 105 | pages)) 106 | 107 | 108 | (defn walk-folders [root-path] 109 | ; generate a sequence of folder data 110 | (for [#(folder subfolders files) (walk root-path)] 111 | ; setting this helps guide os.walk() 112 | (setv subfolders (filtered-names subfolders)) 113 | (yield {"path" folder 114 | "files" files}))) 115 | 116 | 117 | (defn with-index [folder-seq root-path] 118 | ; takes a sequence of folders and returns page info 119 | (for [folder folder-seq] 120 | (for [base BASE_FILENAMES] 121 | (when (in base (:files folder)) 122 | (yield 123 | {"path" (get (:path folder) (slice (+ 1 (len root-path)) None)) 124 | "filename" base 125 | "mtime" (int (getmtime (join (:path folder) base)))}))))) 126 | 127 | 128 | (defn gen-pages [root-path] 129 | ; generate a lazy sequence of pages 130 | (-> root-path 131 | (walk-folders) 132 | (with-index root-path))) 133 | -------------------------------------------------------------------------------- /sushy/utils.hy: -------------------------------------------------------------------------------- 1 | (import 2 | collections [OrderedDict] 3 | base64 [urlsafe-b64encode] 4 | bottle [request response] 5 | calendar [timegm] 6 | datetime [datetime] 7 | dateutil.parser [parse :as parse-date] 8 | functools [wraps cache lru-cache] 9 | hashlib [sha1] 10 | hmac [new :as new-hmac] 11 | io [StringIO] 12 | logging [getLogger] 13 | PIL [Image ImageFilter] 14 | pytz [timezone] 15 | random [sample] 16 | slugify [slugify] 17 | time [time] 18 | urllib.parse [quote :as uquote urlsplit urlunparse]) 19 | 20 | (setv log (getLogger)) 21 | 22 | (setv DATETIME_FORMAT "%Y%m%dT%H:%M:%S.%f") 23 | 24 | (setv TIME_INTERVALS 25 | {"00:00-00:59" "late night" 26 | "01:00-04:59" "in the wee hours" 27 | "05:00-06:59" "at dawn" 28 | "07:00-08:59" "at breakfast" 29 | "09:00-12:29" "in the morning" 30 | "12:30-14:29" "at lunchtime" 31 | "14:30-16:59" "in the afternoon" 32 | "17:00-17:29" "at teatime" 33 | "17:30-18:59" "at late afternoon" 34 | "19:00-20:29" "in the evening" 35 | "20:30-21:29" "at dinnertime" 36 | "21:30-22:29" "at night" 37 | "22:30-23:59" "late night"}) 38 | 39 | (setv READABLE_INTERVALS 40 | {31556926 "year" 41 | 2592000 "month" 42 | 604800 "week" 43 | 86400 "day" 44 | 3600 "hour" 45 | 60 "minute"}) 46 | 47 | (setv UTC (timezone "UTC")) 48 | 49 | (defn base-url [] 50 | (let [#(scheme netloc path query fragment) (urlsplit (. request url)) 51 | base (urlunparse #(scheme netloc "" "" "" ""))] 52 | base)) 53 | 54 | ; hashing and HMAC helpers 55 | (defn compact-hash [#* args] 56 | (let [hash (sha1 (.encode (str args) "utf-8"))] 57 | (str (urlsafe-b64encode (.digest hash))))) 58 | 59 | 60 | (defn compute-hmac [key #* args] 61 | (let [buffer (.encode (.join "" (map str args)) "utf-8")] 62 | (str (urlsafe-b64encode (.digest (new-hmac (bytes (.encode key "utf-8")) buffer sha1)))))) 63 | 64 | 65 | (defn trace-flow [] 66 | ; dump arguments and data to the debug log 67 | (defn inner [func] 68 | (defn trace-fn [#* args #** kwargs] 69 | (.debug log f"trace -> {args} {kwargs}") 70 | (let [result (func #* args #** kwargs)] 71 | (.debug log f"trace <- {result}") 72 | result)) 73 | trace-fn) 74 | inner) 75 | 76 | 77 | (defn report-processing-time [] 78 | ; timing decorator 79 | (defn inner [func] 80 | (defn timed-fn [#* args #** kwargs] 81 | (let [start (time) 82 | result (func #* args #** kwargs) 83 | interval (int (* 1000 (- (time) start)))] 84 | (.debug log f"{interval}ms") 85 | (.set-header response "Processing-Time" f"{interval}ms") 86 | result)) 87 | timed-fn) 88 | inner) 89 | 90 | 91 | (defn memoize [] 92 | ; memoization decorator 93 | (defn inner [func] 94 | (setv cache {}) 95 | (defn memoized-fn [#* args #** kwargs] 96 | (let [result None 97 | key (compact-hash args kwargs)] 98 | (if (in key cache) 99 | (.get cache key) 100 | (setv result (func #* args #** kwargs))) 101 | (.setdefault cache key result))) 102 | memoized-fn) 103 | inner) 104 | 105 | 106 | (defn ttl-cache [[ttl 30] [query-field None]] 107 | ; memoization decorator with time-to-live 108 | (defn inner [func] 109 | (setv cache {}) 110 | (defn cached-fn [#* args #** kwargs] 111 | (let [now (time) 112 | tag (when query-field (get (. request query) query-field)) 113 | key (compact-hash tag args kwargs) 114 | to-check (sample (sorted (.keys cache)) (int (/ (len cache) 4)))] 115 | ; check current arguments and 25% of remaining keys 116 | (.append to-check key) 117 | 118 | (for [k to-check] 119 | (let [#(good-until value) (.get cache k #(now None))] 120 | (when (< good-until now) 121 | (del (get cache k))))) 122 | 123 | (if (in key cache) 124 | (let [#(good-until value) (.get cache key)] 125 | value) 126 | (let [value (func #* args #** kwargs)] 127 | (setv (get cache key) #((+ now ttl) value)) 128 | value)))) 129 | cached-fn) 130 | inner) 131 | 132 | 133 | (defn [(lru-cache)] get-image-size [filename] 134 | ; extract image size information from a given filename 135 | (let [im None] 136 | (try 137 | (do 138 | (setv im (.open Image filename)) 139 | (let [size im.size] 140 | (.close im) 141 | size)) 142 | (except [e Exception] 143 | (.warn log f"{e} {filename}") 144 | None) 145 | (finally (when im (.close im)))))) 146 | 147 | 148 | (defn get-thumbnail [x y effect filename] 149 | (let [im (.open Image filename) 150 | io (StringIO)] 151 | (try 152 | (do 153 | (.thumbnail im #(x y) (.Image BICUBIC)) 154 | (when (= effect "blur") 155 | (setv im (.filter im (.ImageFilter GaussianBlur)))) 156 | (when (= effect "sharpen") 157 | (setv im (.filter im (.ImageFilter UnsharpMask)))) 158 | (.save im io :format "JPEG" :progressive true :optimize true :quality (int 80)) 159 | (.getvalue io)) 160 | (except [e Exception] 161 | (.warn log f"{e} {x} {y} {filename}") 162 | "") 163 | (finally (.close io))))) 164 | 165 | 166 | (defn slug [text] 167 | ; create a URL slug from arbitrary text 168 | (slugify text)) 169 | 170 | 171 | (defn sse-pack [data] 172 | ; pack data in SSE format 173 | (+ (.join "" 174 | (map (fn [k] 175 | (if (in k data) 176 | (% "%s: %s\n" #(k (get data k))) 177 | "")) 178 | ["retry" "id" "event" "data"])) 179 | "\n")) 180 | 181 | 182 | (defn utc-date [date [tz UTC]] 183 | ; convert naive (or not) dates into UTC 184 | (if (. date tzinfo) 185 | (.astimezone date UTC) 186 | (.astimezone (.localize tz date) UTC))) 187 | 188 | 189 | (defn strip-timezone [date] 190 | (.replace date :tzinfo None)) 191 | 192 | 193 | (defn parse-naive-date [string fallback [tz UTC]] 194 | ; parse a date string and return a UTC date 195 | (if string 196 | (let [date (try 197 | (parse-date string) 198 | (except [e Exception] 199 | (.warning log (% "Could not parse %s" string)) 200 | fallback))] 201 | (utc-date date tz)) 202 | fallback)) 203 | 204 | 205 | (defn ordinal [num] 206 | (+ (str num) "" 207 | (if (<= 10 (% num 100) 20) 208 | "th" 209 | (.get {1 "st" 2 "nd" 3 "rd"} (% num 10) "th")) 210 | "")) 211 | 212 | 213 | (defn fuzzy-time [date] 214 | ; describes a date as a time of day 215 | (let [when (.strftime date "%H:%M")] 216 | (.get 217 | TIME_INTERVALS 218 | (.next (filter (fn [x] (let [#(l u) (.split x "-")] (and (<= l when) (<= when u)))) 219 | (sorted (.keys TIME_INTERVALS)))) 220 | "sometime"))) 221 | 222 | 223 | (defn time-chunks [begin-interval [end-interval None]] 224 | ; breaks down a time interval into a sequence of time chunks 225 | (let [chunks (sorted (.keys READABLE_INTERVALS) :reverse True) 226 | the-end (if end-interval end-interval (datetime.now)) 227 | interval (- (timegm (.timetuple the-end)) (timegm (.timetuple begin-interval))) 228 | values []] 229 | (for [i chunks] 230 | (setv #(d r) (divmod interval i)) 231 | (.append values #((int d) (.get READABLE_INTERVALS i))) 232 | (setv interval r)) 233 | (filter (fn [x] (< 0 (get x 0))) values))) 234 | 235 | 236 | (defn string-plurals [chunk] 237 | (let [#(v s) chunk] 238 | (.join " " (map str #(v (if (> v 1) (+ s "s") s)))))) 239 | 240 | 241 | (defn time-since [begin-interval [end-interval None]] 242 | (let [chunks (list (map string-plurals (time-chunks begin-interval end-interval)))] 243 | (if (not (len (list chunks))) 244 | "sometime" 245 | (.join ", " (get chunks (slice 0 2)))))) 246 | 247 | 248 | (defmacro timeit [block iterations] 249 | `(let [t (time)] 250 | (for [i (range ~iterations)] 251 | ~block) 252 | (print ~iterations (- (time) t)))) 253 | -------------------------------------------------------------------------------- /sushy/wsgi.py: -------------------------------------------------------------------------------- 1 | import hy 2 | from .config import DEBUG_MODE 3 | from .routes import * 4 | from bottle import DEBUG, default_app 5 | 6 | DEBUG = DEBUG_MODE 7 | 8 | app = default_app() 9 | -------------------------------------------------------------------------------- /themes/blog/static/css/custom.css: -------------------------------------------------------------------------------- 1 | /* Shared 2 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 3 | html { 4 | font-family: Georgia, 'Times New Roman', Times, serif; 5 | font-size: 1.11rem; 6 | word-wrap: break-word; 7 | -webkit-nbsp-mode: space; 8 | -webkit-line-break: after-white-space; 9 | font-kerning: normal; 10 | text-rendering: optimizeLegibility; 11 | } 12 | a { 13 | color: #903030; 14 | text-decoration: none; 15 | } 16 | 17 | blockquote { 18 | font-style: italic; 19 | } 20 | sub, sup { 21 | font-size: 80%; 22 | } 23 | 24 | .sidebar, .pagination { 25 | font-family: Georgia, 'Times New Roman', Times, serif; 26 | } 27 | .sidebar { 28 | font-size: 1.0rem; 29 | } 30 | 31 | input[type="search"] { 32 | -webkit-appearance: textfield; 33 | } 34 | 35 | .search-box { 36 | width: 100%; 37 | vertical-align: middle; 38 | white-space: nowrap; 39 | position: relative; 40 | } 41 | 42 | .search-icon { 43 | width: 2rem; 44 | height: 2rem; 45 | font-size: 2rem; 46 | display: inline-block; 47 | text-align: center; 48 | -webkit-transform: rotate(-45deg); 49 | -moz-transform: rotate(-45deg); 50 | -o-transform: rotate(-45deg); 51 | transform: rotate(-45deg); 52 | } 53 | 54 | .search-box .search-icon{ 55 | position: absolute; 56 | top: 50%; 57 | margin-left: -0.2rem; 58 | margin-top: -0.3rem; 59 | z-index: 1; 60 | color: #4f5b66; 61 | } 62 | 63 | .search-box input#search { 64 | width: 100%; 65 | height: 2rem; 66 | background: #2b303b; 67 | border: none; 68 | font-size: 10pt; 69 | float: left; 70 | color: #63717f; 71 | padding-left: 1.9rem; 72 | -webkit-border-radius: 5px; 73 | -moz-border-radius: 5px; 74 | border-radius: 5px; 75 | 76 | -webkit-transition: background .25s ease; 77 | -moz-transition: background .25s ease; 78 | -ms-transition: background .25s ease; 79 | -o-transition: background .25s ease; 80 | transition: background .25s ease; 81 | } 82 | .search-box input#search::-webkit-input-placeholder, 83 | .search-box input#search::-moz-placeholder, 84 | .search-box input#search:-ms-input-placeholder { 85 | color: #65737e; 86 | } 87 | 88 | .search-box input#search:hover, 89 | .search-box input#search:focus, 90 | .search-box input#search:active { 91 | outline:none; 92 | background: #ffffff; 93 | color: black; 94 | } 95 | 96 | .left { 97 | align: left; 98 | float: left; 99 | } 100 | 101 | .right { 102 | align: right; 103 | float: right; 104 | } 105 | 106 | .highlight pre, pre { 107 | font-family: Menlo, Monaco, "Andale Mono", "Lucida Console", "Courier New", monospace !important; 108 | padding: 0.5em; 109 | } 110 | 111 | /* resets */ 112 | tbody tr:nth-child(odd) td, 113 | tbody tr:nth-child(odd) th { 114 | background-color: transparent; 115 | border: none; 116 | } 117 | table, td, th { 118 | border: none; 119 | } 120 | 121 | table { 122 | font-family: Georgia, 'Times New Roman', Times, serif; 123 | background:transparent; 124 | margin: 0px; 125 | padding: 0px; 126 | border-collapse: collapse; 127 | font-size: 90%; 128 | } 129 | 130 | tbody { 131 | border-top: 2px solid black; 132 | border-bottom: 2px solid black; 133 | } 134 | 135 | 136 | tr { 137 | border-top: 1px solid #aaa; 138 | } 139 | 140 | th { 141 | border-bottom: 1px solid black; 142 | border-top: 2px solid black; 143 | border-left: none; 144 | border-right: none; 145 | padding: 4px 4px !important; 146 | font-weight: bold; 147 | text-align: center; 148 | } 149 | 150 | td { 151 | min-width: 80px; 152 | margin: 0px; 153 | padding: 4px 4px !important; 154 | vertical-align: top; 155 | border-top: 1px solid #aaa; 156 | } 157 | 158 | .masthead { 159 | margin-bottom: 1rem; 160 | } 161 | 162 | .quicklook { 163 | -webkit-animation: fade-in 1s; 164 | -moz-animation: fade-in 1s; 165 | -ms-animation: fade-in 1s; 166 | animation: fade-in 1s; 167 | display: block; 168 | background: url("/static/img/placeholder.png") no-repeat; 169 | } 170 | 171 | #seealso { 172 | padding-top: 1em; 173 | width: 100%; 174 | text-align: top; 175 | margin: 0px; 176 | display: table; 177 | font-size: 0.8rem; 178 | } 179 | #seealso .holder { 180 | display: table-row; 181 | } 182 | #seealso a { 183 | display: table-cell; 184 | margin: 0px; 185 | padding: 1rem; 186 | height: 4rem; 187 | background: white; 188 | } 189 | 190 | a.seelink:hover { 191 | background: #933 !important; 192 | color: white; 193 | text-decoration: none; 194 | } 195 | 196 | .footnote, .metadata { 197 | font-size: 90%; 198 | color: #888; 199 | font-weight: 300; 200 | } 201 | 202 | h1, 203 | h2, 204 | h3, 205 | h4, 206 | h5, 207 | h6 { 208 | font-family: Georgia, 'Times New Roman', Times, serif; 209 | font-weight: normal; } 210 | 211 | h1, 212 | h2, 213 | h3 { 214 | letter-spacing: .2rem; 215 | text-transform: uppercase; 216 | } 217 | 218 | .message p { 219 | background-color: whitesmoke; 220 | padding: 1rem; 221 | } 222 | .error p { 223 | background-color: antiquewhite; 224 | } 225 | 226 | #main { 227 | color: black; 228 | width: 100%; 229 | display: inline-block; 230 | clear: both; 231 | } 232 | 233 | #main h1, 234 | #main h2, 235 | #main h3, 236 | #main h4 { 237 | padding-top: 0.5rem; 238 | } 239 | 240 | footer { 241 | margin-top: 2rem; 242 | } 243 | 244 | .inner-container { 245 | margin-left: auto; 246 | margin-right: auto; 247 | padding-bottom: 4rem; 248 | width: 100%; 249 | } 250 | 251 | .pagination-item {border: none; color: #9a9a9a;} 252 | 253 | .author-avatar { 254 | display: table-cell; 255 | vertical-align: top; 256 | } 257 | .author-avatar > img { 258 | width: 2rem; 259 | height: auto; 260 | vertical-align: top; 261 | } 262 | 263 | .post-metadata { 264 | display: table; 265 | vertical-align: middle; 266 | align: center; 267 | margin-left: auto; 268 | margin-right: auto; 269 | } 270 | 271 | .post-date { 272 | font-size: 0.8rem; 273 | padding-left: 0.5rem; 274 | display: table-cell; 275 | } 276 | 277 | .post-title, h1, h2, h3 { 278 | text-align: center; 279 | } 280 | 281 | #latest-content > .container { 282 | padding-bottom: 3rem; 283 | width: 100%; 284 | } 285 | 286 | .links-content { 287 | display: inline-block; 288 | clear: both; 289 | } 290 | 291 | .links-title { 292 | font-size: 1.5rem; 293 | } 294 | 295 | .links-namespace > .lead, .links-namespace > p { 296 | font-size: 1rem; 297 | font-weight: 300; 298 | } 299 | 300 | .blog-namespace > p.lead { 301 | display: inline-block; 302 | clear: both; 303 | } 304 | 305 | .blog-namespace > p.lead:first-child:first-letter { 306 | float: left; 307 | color: #903; 308 | font-size: 75px; 309 | line-height: 60px; 310 | padding-top: 4px; 311 | padding-right: 4px; 312 | padding-left: 3px; 313 | font-family: Baskerville, Georgia, 'Times New Roman', Times, serif; 314 | } 315 | 316 | .blog-namespace img, .links-namespace img { 317 | width: auto; 318 | height: auto; 319 | display: block; 320 | margin-left: auto; 321 | margin-right: auto; 322 | } 323 | 324 | .quicklook { 325 | margin-left: 0.5rem !important; 326 | margin-bottom: 0.4rem; 327 | float: right; 328 | box-shadow: 0px 0px 2px #333; 329 | } 330 | 331 | .sidebar-toggle { 332 | box-shadow: 0px 0px 2px #666; 333 | } 334 | 335 | /* Larger than mobile */ 336 | @media (min-width: 400px) { 337 | .container { 338 | max-width: 40rem; 339 | } 340 | } 341 | 342 | /* Larger than phablet */ 343 | @media (min-width: 550px) { 344 | .container { 345 | max-width: 40rem; 346 | } 347 | } 348 | 349 | /* Larger than tablet */ 350 | @media (min-width: 750px) { 351 | .container { 352 | max-width: 45rem; 353 | font-size: 102%; 354 | } 355 | .post-title, h1, h2, h3 { 356 | text-align: left; 357 | } 358 | .post-metadata { 359 | margin-left: 0; 360 | } 361 | #carbonads-container { 362 | margin-right: 1rem !important; 363 | } 364 | } 365 | 366 | /* Larger than desktop */ 367 | @media (min-width: 1000px) { 368 | .container { 369 | max-width: 50rem; 370 | font-size: 105%; 371 | } 372 | #carbonads-container { 373 | margin-right: 4rem !important; 374 | } 375 | } 376 | 377 | /* Larger than Desktop HD */ 378 | @media (min-width: 1200px) { 379 | .container { 380 | max-width: 55rem; 381 | font-size: 110%; 382 | } 383 | #carbonads-container { 384 | margin-right: 8rem !important; 385 | } 386 | } 387 | 388 | 389 | @-webkit-keyframes fade-in { 390 | from { opacity: 0 } 391 | to { opacity: 1 } 392 | } 393 | 394 | @-moz-keyframes fade-in { 395 | from { opacity: 0 } 396 | to { opacity: 1 } 397 | } 398 | 399 | @keyframes fade-in { 400 | from { opacity: 0 } 401 | to { opacity: 1 } 402 | } 403 | 404 | @-webkit-keyframes pop-in { 405 | 0% { 406 | opacity: 0; 407 | -moz-transform: scale(0.5); 408 | -webkit-transform: scale(0.5) 409 | } 410 | 411 | 100% { 412 | opacity: 1; 413 | -moz-transform: scale(1); 414 | -webkit-transform: scale(1) 415 | } 416 | } 417 | 418 | @-moz-keyframes pop-in { 419 | 0% { 420 | opacity: 0; 421 | -moz-transform: scale(0.5); 422 | -webkit-transform: scale(0.5) 423 | } 424 | 425 | 100% { 426 | opacity: 1; 427 | -moz-transform: scale(1); 428 | -webkit-transform: scale(1) 429 | } 430 | } 431 | 432 | @-ms-keyframes pop-in { 433 | 0% { 434 | opacity: 0; 435 | -moz-transform: scale(0.5); 436 | -webkit-transform: scale(0.5) 437 | } 438 | 439 | 100% { 440 | opacity: 1; 441 | -moz-transform: scale(1); 442 | -webkit-transform: scale(1) 443 | } 444 | } 445 | 446 | @keyframes pop-in { 447 | 0% { 448 | opacity: 0; 449 | -moz-transform: scale(0.5); 450 | -webkit-transform: scale(0.5) 451 | } 452 | 453 | 100% { 454 | opacity: 1; 455 | -moz-transform: scale(1); 456 | -webkit-transform: scale(1) 457 | } 458 | } 459 | 460 | a[href*="//"]:not([href*="localhost"]):after{content:"";display:inline-block;vertical-align:middle;width:1.1rem;height:1.1rem;margin:0 .2em 3px;border-radius:3px;background-size:contain;background-image:url('/favicon/' + var(--url));} 461 | -------------------------------------------------------------------------------- /themes/blog/static/css/poole.css: -------------------------------------------------------------------------------- 1 | /* 2 | * ___ 3 | * /\_ \ 4 | * _____ ___ ___\//\ \ __ 5 | * /\ '__`\ / __`\ / __`\\ \ \ /'__`\ 6 | * \ \ \_\ \/\ \_\ \/\ \_\ \\_\ \_/\ __/ 7 | * \ \ ,__/\ \____/\ \____//\____\ \____\ 8 | * \ \ \/ \/___/ \/___/ \/____/\/____/ 9 | * \ \_\ 10 | * \/_/ 11 | * 12 | * Designed, built, and released under MIT license by @mdo. Learn more at 13 | * https://github.com/poole/poole. 14 | */ 15 | 16 | 17 | /* 18 | * Contents 19 | * 20 | * Body resets 21 | * Custom type 22 | * Messages 23 | * Container 24 | * Masthead 25 | * Posts and pages 26 | * Pagination 27 | * Reverse layout 28 | * Themes 29 | */ 30 | 31 | 32 | /* 33 | * Body resets 34 | * 35 | * Update the foundational and global aspects of the page. 36 | */ 37 | 38 | * { 39 | -webkit-box-sizing: border-box; 40 | -moz-box-sizing: border-box; 41 | box-sizing: border-box; 42 | } 43 | 44 | html, 45 | body { 46 | margin: 0; 47 | padding: 0; 48 | } 49 | 50 | html { 51 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 52 | font-size: 16px; 53 | line-height: 1.5; 54 | } 55 | @media (min-width: 38em) { 56 | html { 57 | font-size: 20px; 58 | } 59 | } 60 | 61 | body { 62 | color: #515151; 63 | background-color: #fff; 64 | -webkit-text-size-adjust: 100%; 65 | -ms-text-size-adjust: 100%; 66 | } 67 | 68 | /* No `:visited` state is required by default (browsers will use `a`) */ 69 | a { 70 | color: #268bd2; 71 | text-decoration: none; 72 | } 73 | a strong { 74 | color: inherit; 75 | } 76 | /* `:focus` is linked to `:hover` for basic accessibility */ 77 | a:hover, 78 | a:focus { 79 | text-decoration: underline; 80 | } 81 | 82 | /* Headings */ 83 | h1, h2, h3, h4, h5, h6 { 84 | margin-bottom: .5rem; 85 | font-weight: bold; 86 | line-height: 1.25; 87 | color: #313131; 88 | text-rendering: optimizeLegibility; 89 | } 90 | h1 { 91 | font-size: 2rem; 92 | } 93 | h2 { 94 | margin-top: 1rem; 95 | font-size: 1.5rem; 96 | } 97 | h3 { 98 | margin-top: 1.5rem; 99 | font-size: 1.25rem; 100 | } 101 | h4, h5, h6 { 102 | margin-top: 1rem; 103 | font-size: 1rem; 104 | } 105 | 106 | /* Body text */ 107 | p { 108 | margin-top: 0; 109 | margin-bottom: 1rem; 110 | } 111 | 112 | strong { 113 | color: #303030; 114 | } 115 | 116 | 117 | /* Lists */ 118 | ul, ol, dl { 119 | margin-top: 0; 120 | margin-bottom: 1rem; 121 | } 122 | 123 | dt { 124 | font-weight: bold; 125 | } 126 | dd { 127 | margin-bottom: .5rem; 128 | } 129 | 130 | /* Misc */ 131 | hr { 132 | position: relative; 133 | margin: 1.5rem 0; 134 | border: 0; 135 | border-top: 1px solid #eee; 136 | border-bottom: 1px solid #fff; 137 | } 138 | 139 | abbr { 140 | font-size: 85%; 141 | font-weight: bold; 142 | color: #555; 143 | text-transform: uppercase; 144 | } 145 | abbr[title] { 146 | cursor: help; 147 | border-bottom: 1px dotted #e5e5e5; 148 | } 149 | 150 | /* Code */ 151 | code, 152 | pre { 153 | font-family: Menlo, Monaco, "Courier New", monospace; 154 | } 155 | code { 156 | /*padding: .25em .5em;*/ 157 | font-size: 85%; 158 | color: #bf616a; 159 | background-color: #f9f9f9; 160 | border-radius: 3px; 161 | } 162 | pre { 163 | display: block; 164 | margin-top: 0; 165 | margin-bottom: 1rem; 166 | padding: 1rem; 167 | font-size: .8rem; 168 | line-height: 1.4; 169 | white-space: pre; 170 | white-space: pre-wrap; 171 | word-break: break-all; 172 | word-wrap: break-word; 173 | background-color: #f9f9f9; 174 | } 175 | pre code { 176 | padding: 0; 177 | font-size: 100%; 178 | color: inherit; 179 | background-color: transparent; 180 | } 181 | 182 | /* Pygments via Jekyll */ 183 | .highlight { 184 | margin-bottom: 1rem; 185 | border-radius: 4px; 186 | } 187 | .highlight pre { 188 | margin-bottom: 0; 189 | } 190 | 191 | /* Gist via GitHub Pages */ 192 | .gist .gist-file { 193 | font-family: Menlo, Monaco, "Courier New", monospace !important; 194 | } 195 | .gist .markdown-body { 196 | padding: 15px; 197 | } 198 | .gist pre { 199 | padding: 0; 200 | background-color: transparent; 201 | } 202 | .gist .gist-file .gist-data { 203 | font-size: .8rem !important; 204 | line-height: 1.4; 205 | } 206 | .gist code { 207 | padding: 0; 208 | color: inherit; 209 | background-color: transparent; 210 | border-radius: 0; 211 | } 212 | 213 | /* Quotes */ 214 | blockquote { 215 | padding: .5rem 1rem; 216 | margin: .8rem 0; 217 | color: #7a7a7a; 218 | border-left: .25rem solid #e5e5e5; 219 | } 220 | blockquote p:last-child { 221 | margin-bottom: 0; 222 | } 223 | @media (min-width: 30em) { 224 | blockquote { 225 | padding-right: 5rem; 226 | padding-left: 1.25rem; 227 | } 228 | } 229 | 230 | img { 231 | display: block; 232 | max-width: 100%; 233 | margin: 0 0 1rem; 234 | border-radius: 5px; 235 | } 236 | 237 | /* Tables */ 238 | table { 239 | margin-bottom: 1rem; 240 | width: 100%; 241 | border: 1px solid #e5e5e5; 242 | border-collapse: collapse; 243 | } 244 | td, 245 | th { 246 | padding: .25rem .5rem; 247 | border: 1px solid #e5e5e5; 248 | } 249 | tbody tr:nth-child(odd) td, 250 | tbody tr:nth-child(odd) th { 251 | background-color: #f9f9f9; 252 | } 253 | 254 | 255 | /* 256 | * Custom type 257 | * 258 | * Extend paragraphs with `.lead` for larger introductory text. 259 | */ 260 | 261 | .lead { 262 | font-size: 1.25rem; 263 | font-weight: 300; 264 | } 265 | 266 | 267 | /* 268 | * Messages 269 | * 270 | * Show alert messages to users. You may add it to single elements like a `

`, 271 | * or to a parent if there are multiple elements to show. 272 | */ 273 | 274 | .message { 275 | margin-bottom: 1rem; 276 | padding: 1rem; 277 | color: #717171; 278 | background-color: #f9f9f9; 279 | } 280 | 281 | 282 | /* 283 | * Container 284 | * 285 | * Center the page content. 286 | */ 287 | 288 | .container { 289 | max-width: 38rem; 290 | padding-left: 1rem; 291 | padding-right: 1rem; 292 | margin-left: auto; 293 | margin-right: auto; 294 | } 295 | 296 | 297 | /* 298 | * Masthead 299 | * 300 | * Super small header above the content for site name and short description. 301 | */ 302 | 303 | .masthead { 304 | padding-top: 1rem; 305 | padding-bottom: 1rem; 306 | margin-bottom: 3rem; 307 | } 308 | .masthead-title { 309 | margin-top: 0; 310 | margin-bottom: 0; 311 | color: #505050; 312 | } 313 | .masthead-title a { 314 | color: #505050; 315 | } 316 | .masthead-title small { 317 | font-size: 75%; 318 | font-weight: 400; 319 | color: #c0c0c0; 320 | letter-spacing: 0; 321 | } 322 | 323 | 324 | /* 325 | * Posts and pages 326 | * 327 | * Each post is wrapped in `.post` and is used on default and post layouts. Each 328 | * page is wrapped in `.page` and is only used on the page layout. 329 | */ 330 | 331 | .page, 332 | .post { 333 | margin-bottom: 4em; 334 | } 335 | 336 | /* Blog post or page title */ 337 | .page-title, 338 | .post-title, 339 | .post-title a { 340 | color: #303030; 341 | } 342 | .page-title, 343 | .post-title { 344 | margin-top: 0; 345 | } 346 | 347 | /* Meta data line below post title */ 348 | .post-date { 349 | display: block; 350 | margin-top: -.5rem; 351 | margin-bottom: 1rem; 352 | color: #9a9a9a; 353 | } 354 | 355 | /* Related posts */ 356 | .related { 357 | padding-top: 2rem; 358 | padding-bottom: 2rem; 359 | border-top: 1px solid #eee; 360 | } 361 | .related-posts { 362 | padding-left: 0; 363 | list-style: none; 364 | } 365 | .related-posts h3 { 366 | margin-top: 0; 367 | } 368 | .related-posts li small { 369 | font-size: 75%; 370 | color: #999; 371 | } 372 | .related-posts li a:hover { 373 | color: #268bd2; 374 | text-decoration: none; 375 | } 376 | .related-posts li a:hover small { 377 | color: inherit; 378 | } 379 | 380 | 381 | /* 382 | * Pagination 383 | * 384 | * Super lightweight (HTML-wise) blog pagination. `span`s are provide for when 385 | * there are no more previous or next posts to show. 386 | */ 387 | 388 | .pagination { 389 | overflow: hidden; /* clearfix */ 390 | margin-left: -1rem; 391 | margin-right: -1rem; 392 | font-family: "PT Sans", Helvetica, Arial, sans-serif; 393 | color: #ccc; 394 | text-align: center; 395 | } 396 | 397 | /* Pagination items can be `span`s or `a`s */ 398 | .pagination-item { 399 | display: block; 400 | padding: 1rem; 401 | border: 1px solid #eee; 402 | } 403 | .pagination-item:first-child { 404 | margin-bottom: -1px; 405 | } 406 | 407 | /* Only provide a hover state for linked pagination items */ 408 | a.pagination-item:hover { 409 | background-color: #f5f5f5; 410 | } 411 | 412 | @media (min-width: 30em) { 413 | .pagination { 414 | margin: 3rem 0; 415 | } 416 | .pagination-item { 417 | float: left; 418 | width: 50%; 419 | } 420 | .pagination-item:first-child { 421 | margin-bottom: 0; 422 | border-top-left-radius: 4px; 423 | border-bottom-left-radius: 4px; 424 | } 425 | .pagination-item:last-child { 426 | margin-left: -1px; 427 | border-top-right-radius: 4px; 428 | border-bottom-right-radius: 4px; 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /themes/blog/static/css/rss.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Georgia, "Times New Roman", Times, serif; 3 | } 4 | 5 | pre { 6 | font-family: Menlo, Consolas, Courier New, Courier, monospace; 7 | } 8 | 9 | a:link { 10 | color: #0000cc; 11 | } 12 | 13 | table { 14 | background:transparent; 15 | margin: 0px; 16 | padding: 0px; 17 | border-collapse: collapse; 18 | font-size: 90%; 19 | } 20 | 21 | tbody { 22 | border-top: 2px solid black; 23 | border-bottom: 2px solid black; 24 | } 25 | 26 | tr { 27 | border-top: 1px solid #aaa; 28 | } 29 | 30 | th { 31 | border-bottom: 1px solid black; 32 | border-top: 2px solid black; 33 | padding: 4px 4px !important; 34 | font-weight: bold; 35 | text-align: center; 36 | } 37 | 38 | tr { 39 | border-top: 1px solid #aaa; 40 | } 41 | 42 | td { 43 | min-width: 80px; 44 | margin: 0px; 45 | padding: 4px 4px !important; 46 | vertical-align: top; 47 | border-top: 1px solid #aaa; 48 | } 49 | 50 | img { 51 | max-width: 100% !important; 52 | height: auto !important; 53 | } 54 | -------------------------------------------------------------------------------- /themes/blog/static/css/syntax.css: -------------------------------------------------------------------------------- 1 | /* Solarized Dark 2 | 3 | For use with Jekyll and Pygments 4 | 5 | http://ethanschoonover.com/solarized 6 | 7 | SOLARIZED HEX ROLE 8 | --------- -------- ------------------------------------------ 9 | base03 #002b36 background 10 | base01 #586e75 comments / secondary content 11 | base1 #93a1a1 body text / default code / primary content 12 | orange #cb4b16 constants 13 | red #dc322f regex, special keywords 14 | blue #268bd2 reserved keywords 15 | cyan #2aa198 strings, numbers 16 | green #859900 operators, other keywords 17 | */ 18 | 19 | .highlight { background-color: #002b36; color: #93a1a1 } 20 | .highlight .c { color: #586e75 } /* Comment */ 21 | .highlight .err { color: #93a1a1 } /* Error */ 22 | .highlight .g { color: #93a1a1 } /* Generic */ 23 | .highlight .k { color: #859900 } /* Keyword */ 24 | .highlight .l { color: #93a1a1 } /* Literal */ 25 | .highlight .n { color: #93a1a1 } /* Name */ 26 | .highlight .o { color: #859900 } /* Operator */ 27 | .highlight .x { color: #cb4b16 } /* Other */ 28 | .highlight .p { color: #93a1a1 } /* Punctuation */ 29 | .highlight .cm { color: #586e75 } /* Comment.Multiline */ 30 | .highlight .cp { color: #859900 } /* Comment.Preproc */ 31 | .highlight .c1 { color: #586e75 } /* Comment.Single */ 32 | .highlight .cs { color: #859900 } /* Comment.Special */ 33 | .highlight .gd { color: #2aa198 } /* Generic.Deleted */ 34 | .highlight .ge { color: #93a1a1; font-style: italic } /* Generic.Emph */ 35 | .highlight .gr { color: #dc322f } /* Generic.Error */ 36 | .highlight .gh { color: #cb4b16 } /* Generic.Heading */ 37 | .highlight .gi { color: #859900 } /* Generic.Inserted */ 38 | .highlight .go { color: #93a1a1 } /* Generic.Output */ 39 | .highlight .gp { color: #93a1a1 } /* Generic.Prompt */ 40 | .highlight .gs { color: #93a1a1; font-weight: bold } /* Generic.Strong */ 41 | .highlight .gu { color: #cb4b16 } /* Generic.Subheading */ 42 | .highlight .gt { color: #93a1a1 } /* Generic.Traceback */ 43 | .highlight .kc { color: #cb4b16 } /* Keyword.Constant */ 44 | .highlight .kd { color: #268bd2 } /* Keyword.Declaration */ 45 | .highlight .kn { color: #859900 } /* Keyword.Namespace */ 46 | .highlight .kp { color: #859900 } /* Keyword.Pseudo */ 47 | .highlight .kr { color: #268bd2 } /* Keyword.Reserved */ 48 | .highlight .kt { color: #dc322f } /* Keyword.Type */ 49 | .highlight .ld { color: #93a1a1 } /* Literal.Date */ 50 | .highlight .m { color: #2aa198 } /* Literal.Number */ 51 | .highlight .s { color: #2aa198 } /* Literal.String */ 52 | .highlight .na { color: #93a1a1 } /* Name.Attribute */ 53 | .highlight .nb { color: #B58900 } /* Name.Builtin */ 54 | .highlight .nc { color: #268bd2 } /* Name.Class */ 55 | .highlight .no { color: #cb4b16 } /* Name.Constant */ 56 | .highlight .nd { color: #268bd2 } /* Name.Decorator */ 57 | .highlight .ni { color: #cb4b16 } /* Name.Entity */ 58 | .highlight .ne { color: #cb4b16 } /* Name.Exception */ 59 | .highlight .nf { color: #268bd2 } /* Name.Function */ 60 | .highlight .nl { color: #93a1a1 } /* Name.Label */ 61 | .highlight .nn { color: #93a1a1 } /* Name.Namespace */ 62 | .highlight .nx { color: #93a1a1 } /* Name.Other */ 63 | .highlight .py { color: #93a1a1 } /* Name.Property */ 64 | .highlight .nt { color: #268bd2 } /* Name.Tag */ 65 | .highlight .nv { color: #268bd2 } /* Name.Variable */ 66 | .highlight .ow { color: #859900 } /* Operator.Word */ 67 | .highlight .w { color: #93a1a1 } /* Text.Whitespace */ 68 | .highlight .mf { color: #2aa198 } /* Literal.Number.Float */ 69 | .highlight .mh { color: #2aa198 } /* Literal.Number.Hex */ 70 | .highlight .mi { color: #2aa198 } /* Literal.Number.Integer */ 71 | .highlight .mo { color: #2aa198 } /* Literal.Number.Oct */ 72 | .highlight .sb { color: #586e75 } /* Literal.String.Backtick */ 73 | .highlight .sc { color: #2aa198 } /* Literal.String.Char */ 74 | .highlight .sd { color: #93a1a1 } /* Literal.String.Doc */ 75 | .highlight .s2 { color: #2aa198 } /* Literal.String.Double */ 76 | .highlight .se { color: #cb4b16 } /* Literal.String.Escape */ 77 | .highlight .sh { color: #93a1a1 } /* Literal.String.Heredoc */ 78 | .highlight .si { color: #2aa198 } /* Literal.String.Interpol */ 79 | .highlight .sx { color: #2aa198 } /* Literal.String.Other */ 80 | .highlight .sr { color: #dc322f } /* Literal.String.Regex */ 81 | .highlight .s1 { color: #2aa198 } /* Literal.String.Single */ 82 | .highlight .ss { color: #2aa198 } /* Literal.String.Symbol */ 83 | .highlight .bp { color: #268bd2 } /* Name.Builtin.Pseudo */ 84 | .highlight .vc { color: #268bd2 } /* Name.Variable.Class */ 85 | .highlight .vg { color: #268bd2 } /* Name.Variable.Global */ 86 | .highlight .vi { color: #268bd2 } /* Name.Variable.Instance */ 87 | .highlight .il { color: #2aa198 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /themes/blog/static/img/avatars/unknown200x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/themes/blog/static/img/avatars/unknown200x200.png -------------------------------------------------------------------------------- /themes/blog/static/img/avatars/unknown36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/themes/blog/static/img/avatars/unknown36x36.png -------------------------------------------------------------------------------- /themes/blog/static/img/avatars/unknown80x80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/themes/blog/static/img/avatars/unknown80x80.png -------------------------------------------------------------------------------- /themes/blog/static/img/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/themes/blog/static/img/placeholder.png -------------------------------------------------------------------------------- /themes/blog/static/js/app.js: -------------------------------------------------------------------------------- 1 | var source = new EventSource("/events"); 2 | 3 | source.addEventListener('error', function(e) { 4 | if (e.readyState == EventSource.CLOSED) { 5 | console.log("Server down") 6 | } 7 | else if( e.readyState == EventSource.OPEN) { 8 | console.log("Connecting...") 9 | } 10 | }, false); 11 | 12 | source.addEventListener('tick', function(e) { 13 | console.log(e.data); 14 | }, false); 15 | source.addEventListener('indexing', function(e) { 16 | console.log(e.data); 17 | }, false); 18 | -------------------------------------------------------------------------------- /themes/blog/static/js/debug.js: -------------------------------------------------------------------------------- 1 | /* Addy Osmani's one-line layout debugger */ 2 | [].forEach.call(document.querySelectorAll("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16)}) -------------------------------------------------------------------------------- /themes/blog/static/js/eventsource.js: -------------------------------------------------------------------------------- 1 | ;(function (global) { 2 | 3 | if ("EventSource" in global) return; 4 | 5 | var reTrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g; 6 | 7 | var EventSource = function (url) { 8 | var eventsource = this, 9 | interval = 500, // polling interval 10 | lastEventId = null, 11 | cache = ''; 12 | 13 | if (!url || typeof url != 'string') { 14 | throw new SyntaxError('Not enough arguments'); 15 | } 16 | 17 | this.URL = url; 18 | this.readyState = this.CONNECTING; 19 | this._pollTimer = null; 20 | this._xhr = null; 21 | 22 | function pollAgain(interval) { 23 | eventsource._pollTimer = setTimeout(function () { 24 | poll.call(eventsource); 25 | }, interval); 26 | } 27 | 28 | function poll() { 29 | try { // force hiding of the error message... insane? 30 | if (eventsource.readyState == eventsource.CLOSED) return; 31 | 32 | // NOTE: IE7 and upwards support 33 | var xhr = new XMLHttpRequest(); 34 | xhr.open('GET', eventsource.URL, true); 35 | xhr.setRequestHeader('Accept', 'text/event-stream'); 36 | xhr.setRequestHeader('Cache-Control', 'no-cache'); 37 | // we must make use of this on the server side if we're working with Android - because they don't trigger 38 | // readychange until the server connection is closed 39 | xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 40 | 41 | if (lastEventId != null) xhr.setRequestHeader('Last-Event-ID', lastEventId); 42 | cache = ''; 43 | 44 | xhr.timeout = 50000; 45 | xhr.onreadystatechange = function () { 46 | if ((this.readyState == 3 || this.readyState == 4) && this.status == 200) { 47 | // on success 48 | if (eventsource.readyState == eventsource.CONNECTING) { 49 | eventsource.readyState = eventsource.OPEN; 50 | eventsource.dispatchEvent('open', { type: 'open' }); 51 | } 52 | 53 | var responseText = ''; 54 | try { 55 | responseText = this.responseText || ''; 56 | } catch (e) {} 57 | 58 | // process this.responseText 59 | var parts = responseText.substr(cache.length).split("\n"), 60 | eventType = 'message', 61 | data = [], 62 | i = 0, 63 | line = ''; 64 | 65 | cache = responseText; 66 | 67 | // TODO handle 'event' (for buffer name), retry 68 | for (; i < parts.length; i++) { 69 | line = parts[i].replace(reTrim, ''); 70 | if (line.indexOf('event') == 0) { 71 | eventType = line.replace(/event:?\s*/, ''); 72 | } else if (line.indexOf('retry') == 0) { 73 | interval = line.replace(/retry:?\s*/, '') + 0; 74 | } else if (line.indexOf('data') == 0) { 75 | data.push(line.replace(/data:?\s*/, '')); 76 | } else if (line.indexOf('id:') == 0) { 77 | lastEventId = line.replace(/id:?\s*/, ''); 78 | } else if (line.indexOf('id') == 0) { // this resets the id 79 | lastEventId = null; 80 | } else if (line == '') { 81 | if (data.length) { 82 | var event = new MessageEvent(data.join('\n'), eventsource.url, lastEventId); 83 | eventsource.dispatchEvent(eventType, event); 84 | data = []; 85 | eventType = 'message'; 86 | } 87 | } 88 | } 89 | 90 | if (this.readyState == 4) pollAgain(interval); 91 | // don't need to poll again, because we're long-loading 92 | } else if (eventsource.readyState !== eventsource.CLOSED) { 93 | if (this.readyState == 4) { // and some other status 94 | // dispatch error 95 | eventsource.readyState = eventsource.CONNECTING; 96 | eventsource.dispatchEvent('error', { type: 'error' }); 97 | pollAgain(interval); 98 | } else if (this.readyState == 0) { // likely aborted 99 | pollAgain(interval); 100 | } else { 101 | } 102 | } 103 | }; 104 | 105 | xhr.send(); 106 | 107 | setTimeout(function () { 108 | if (true || xhr.readyState == 3) xhr.abort(); 109 | }, xhr.timeout); 110 | 111 | eventsource._xhr = xhr; 112 | 113 | } catch (e) { // in an attempt to silence the errors 114 | eventsource.dispatchEvent('error', { type: 'error', data: e.message }); // ??? 115 | } 116 | }; 117 | 118 | poll(); // init now 119 | }; 120 | 121 | EventSource.prototype = { 122 | close: function () { 123 | // closes the connection - disabling the polling 124 | this.readyState = this.CLOSED; 125 | clearInterval(this._pollTimer); 126 | this._xhr.abort(); 127 | }, 128 | CONNECTING: 0, 129 | OPEN: 1, 130 | CLOSED: 2, 131 | dispatchEvent: function (type, event) { 132 | var handlers = this['_' + type + 'Handlers']; 133 | if (handlers) { 134 | for (var i = 0; i < handlers.length; i++) { 135 | handlers[i].call(this, event); 136 | } 137 | } 138 | 139 | if (this['on' + type]) { 140 | this['on' + type].call(this, event); 141 | } 142 | }, 143 | addEventListener: function (type, handler) { 144 | if (!this['_' + type + 'Handlers']) { 145 | this['_' + type + 'Handlers'] = []; 146 | } 147 | 148 | this['_' + type + 'Handlers'].push(handler); 149 | }, 150 | removeEventListener: function () { 151 | // TODO 152 | }, 153 | onerror: null, 154 | onmessage: null, 155 | onopen: null, 156 | readyState: 0, 157 | URL: '' 158 | }; 159 | 160 | var MessageEvent = function (data, origin, lastEventId) { 161 | this.data = data; 162 | this.origin = origin; 163 | this.lastEventId = lastEventId || ''; 164 | }; 165 | 166 | MessageEvent.prototype = { 167 | data: null, 168 | type: 'message', 169 | lastEventId: '', 170 | origin: '' 171 | }; 172 | 173 | if ('module' in global) module.exports = EventSource; 174 | global.EventSource = EventSource; 175 | 176 | })(this); 177 | -------------------------------------------------------------------------------- /themes/blog/static/js/radio.js: -------------------------------------------------------------------------------- 1 | /** 2 | Radio.js - Chainable, Dependency Free Publish/Subscribe for Javascript 3 | http://radio.uxder.com 4 | Author: Scott Murphy 2011 5 | twitter: @hellocreation, github: uxder 6 | 7 | Permission is hereby granted, free of charge, to any person 8 | obtaining a copy of this software and associated documentation 9 | files (the "Software"), to deal in the Software without 10 | restriction, including without limitation the rights to use, 11 | copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following 14 | conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | OTHER DEALINGS IN THE SOFTWARE. 27 | */ 28 | (function (name, global, definition) { 29 | if (typeof module !== 'undefined') module.exports = definition(name, global); 30 | else if (typeof define === 'function' && typeof define.amd === 'object') define(definition); 31 | else global[name] = definition(name, global); 32 | })('radio', this, function (name, global) { 33 | 34 | "use strict"; 35 | 36 | /** 37 | * Main Wrapper for radio.$ and create a function radio to accept the channelName 38 | * @param {String} channelName topic of event 39 | */ 40 | function radio(channelName) { 41 | arguments.length ? radio.$.channel(channelName) : radio.$.reset(); 42 | return radio.$; 43 | } 44 | 45 | radio.$ = { 46 | version: '0.2', 47 | channelName: "", 48 | channels: [], 49 | 50 | /** 51 | * Reset global state, by removing all channels 52 | * @example 53 | * radio() 54 | */ 55 | reset: function() { 56 | radio.$.channelName = ""; 57 | radio.$.channels = []; 58 | }, 59 | 60 | /** 61 | * Broadcast (publish) 62 | * Iterate through all listeners (callbacks) in current channel and pass arguments to subscribers 63 | * @param arguments data to be sent to listeners 64 | * @example 65 | * //basic usage 66 | * radio('channel1').broadcast('my message'); 67 | * //send an unlimited number of parameters 68 | * radio('channel2').broadcast(param1, param2, param3 ... ); 69 | */ 70 | broadcast: function() { 71 | var i, c = this.channels[this.channelName], 72 | l = c.length, 73 | subscriber, callback, context; 74 | //iterate through current channel and run each subscriber 75 | for (i = 0; i < l; i++) { 76 | subscriber = c[i]; 77 | //if subscriber was an array, set the callback and context. 78 | if ((typeof(subscriber) === 'object') && (subscriber.length)) { 79 | callback = subscriber[0]; 80 | //if user set the context, set it to the context otherwise, it is a globally scoped function 81 | context = subscriber[1] || global; 82 | } 83 | callback.apply(context, arguments); 84 | } 85 | return this; 86 | }, 87 | 88 | /** 89 | * Create the channel if it doesn't exist and set the current channel/event name 90 | * @param {String} name the name of the channel 91 | * @example 92 | * radio('channel1'); 93 | */ 94 | channel: function(name) { 95 | var c = this.channels; 96 | //create a new channel if it doesn't exists 97 | if (!c[name]) c[name] = []; 98 | this.channelName = name; 99 | return this; 100 | }, 101 | 102 | /** 103 | * Add Subscriber to channel 104 | * Take the arguments and add it to the this.channels array. 105 | * @param {Function|Array} arguments list of callbacks or arrays[callback, context] separated by commas 106 | * @example 107 | * //basic usage 108 | * var callback = function() {}; 109 | * radio('channel1').subscribe(callback); 110 | * 111 | * //subscribe an endless amount of callbacks 112 | * radio('channel1').subscribe(callback, callback2, callback3 ...); 113 | * 114 | * //adding callbacks with context 115 | * radio('channel1').subscribe([callback, context],[callback1, context], callback3); 116 | * 117 | * //subscribe by chaining 118 | * radio('channel1').subscribe(callback).radio('channel2').subscribe(callback).subscribe(callback2); 119 | */ 120 | subscribe: function() { 121 | var a = arguments, 122 | c = this.channels[this.channelName], 123 | i, l = a.length, 124 | p, ai = []; 125 | 126 | //run through each arguments and subscribe it to the channel 127 | for (i = 0; i < l; i++) { 128 | ai = a[i]; 129 | //if the user sent just a function, wrap the fucntion in an array [function] 130 | p = (typeof(ai) === "function") ? [ai] : ai; 131 | if ((typeof(p) === 'object') && (p.length)) c.push(p); 132 | } 133 | return this; 134 | }, 135 | 136 | /** 137 | * Remove subscriber from channel 138 | * Take arguments with functions and unsubscribe it if there is a match against existing subscribers. 139 | * @param {Function} arguments callbacks separated by commas 140 | * @example 141 | * //basic usage 142 | * radio('channel1').unsubscribe(callback); 143 | * //you can unsubscribe as many callbacks as you want 144 | * radio('channel1').unsubscribe(callback, callback2, callback3 ...); 145 | * //removing callbacks with context is the same 146 | * radio('channel1').subscribe([callback, context]).unsubscribe(callback); 147 | */ 148 | unsubscribe: function() { 149 | var a = arguments, 150 | i, j, c = this.channels[this.channelName], 151 | l = a.length, 152 | cl = c.length, 153 | offset = 0, 154 | jo; 155 | //loop through each argument 156 | for (i = 0; i < l; i++) { 157 | //need to reset vars that change as the channel array items are removed 158 | offset = 0; 159 | cl = c.length; 160 | //loop through the channel 161 | for (j = 0; j < cl; j++) { 162 | jo = j - offset; 163 | //if there is a match with the argument and the channel function, unsubscribe it from the channel array 164 | if (c[jo][0] === a[i]) { 165 | //unsubscribe matched item from the channel array 166 | c.splice(jo, 1); 167 | offset++; 168 | } 169 | } 170 | } 171 | return this; 172 | } 173 | }; 174 | 175 | return radio; 176 | }); 177 | -------------------------------------------------------------------------------- /themes/blog/static/js/utils.js: -------------------------------------------------------------------------------- 1 | gradient = { 2 | hex2rgb : function(hex){ 3 | hex = hex.replace('#', ''); 4 | if(hex.length !== 3 && hex.length !== 6){ 5 | return [255,255,255]; 6 | } 7 | if(hex.length == 3){ 8 | hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; 9 | } 10 | return [parseInt(hex.substr(0,2),16), 11 | parseInt(hex.substr(2,2),16), 12 | parseInt(hex.substr(4,2),16)]; 13 | }, 14 | 15 | rgb2hex : function(rgb){ 16 | return "#" + 17 | ("0" + Math.round(rgb[0]).toString(16)).slice(-2) + 18 | ("0" + Math.round(rgb[1]).toString(16)).slice(-2) + 19 | ("0" + Math.round(rgb[2]).toString(16)).slice(-2); 20 | }, 21 | 22 | generate : function(start, finish, steps){ 23 | var result = []; 24 | 25 | start = this.hex2rgb(start); 26 | finish = this.hex2rgb(finish); 27 | steps -= 1; 28 | ri = (finish[0] - start[0]) / steps; 29 | gi = (finish[1] - start[1]) / steps; 30 | bi = (finish[2] - start[2]) / steps; 31 | 32 | result.push(this.rgb2hex(start)); 33 | 34 | var rv = start[0], 35 | gv = start[1], 36 | bv = start[2]; 37 | 38 | for (var i = 0; i < (steps-1); i++) { 39 | rv += ri; 40 | gv += gi; 41 | bv += bi; 42 | result.push(this.rgb2hex([rv, gv, bv])); 43 | }; 44 | 45 | result.push(this.rgb2hex(finish)); 46 | return result; 47 | } 48 | } -------------------------------------------------------------------------------- /themes/blog/static/root/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/themes/blog/static/root/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /themes/blog/static/root/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/themes/blog/static/root/apple-touch-icon.png -------------------------------------------------------------------------------- /themes/blog/static/root/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/themes/blog/static/root/favicon.ico -------------------------------------------------------------------------------- /themes/blog/views/atom.tpl: -------------------------------------------------------------------------------- 1 | % from urllib import quote_plus 2 | 3 | 4 | {{site_name}} 5 | {{site_description}} 6 | 7 | 8 | {{base_url}}/feed 9 | {{pubdate.isoformat()}} 10 | {{site_copyright}} 11 | %for item in items: 12 | % link = quote_plus(base_url + page_route_base + "/" + item["pagename"], safe="%/:=&?~#+!$,;'@()*[]") 13 | % guid = link.replace("http://","") 14 | 15 | {{item["title"]}} 16 | {{link}} 17 | {{item["pubdate"].isoformat()}} 18 | {{item["mtime"].isoformat()}} 19 | 20 | {{item["author"]}} 21 | {{base_url}} 22 | 23 | 24 | 27 | %for tag in item['tags']: 28 | % tag = tag.replace('tag:','') 29 | 30 | %end 31 | 32 | %end 33 | 34 | -------------------------------------------------------------------------------- /themes/blog/views/blog.tpl: -------------------------------------------------------------------------------- 1 | <% 2 | from sushy.utils import utc_date 3 | from sushy.models import get_prev_next, get_latest 4 | from sushy.config import BLOG_ENTRIES 5 | from sushy.render import render_page 6 | from sushy.store import get_page 7 | from sushy.transform import apply_transforms, inner_html, extract_lead_paragraph 8 | from logging import getLogger 9 | from re import match 10 | 11 | log = getLogger() 12 | 13 | namespace = pagename.split("/")[0].lower() 14 | 15 | latest = list(get_latest(regexp=BLOG_ENTRIES)) 16 | 17 | log.debug("Got %d entries" % len(latest)) 18 | %> 19 |

20 | 21 |
22 | <% 23 | tags = [] 24 | first = True 25 | for post in latest: 26 | pagename = post['name'] 27 | page = get_page(pagename) 28 | headers = page['headers'] 29 | namespace = pagename.split("/")[0].lower() 30 | if "blog" == namespace: 31 | if first: 32 | first = False 33 | body = inner_html(apply_transforms(render_page(page),pagename)) 34 | else: 35 | body = extract_lead_paragraph(page, pagename) + f"""

Read More...

""" 36 | end 37 | else: 38 | body = inner_html(apply_transforms(render_page(page),pagename)) 39 | end 40 | tags.extend(post['tags'].replace("tag:", "").strip().split(", ")) 41 | %> 42 |
43 | <% 44 | include('metadata', **include('common')) 45 | %> 46 |
47 | {{!body}} 48 |
49 |
50 | <% 51 | end 52 | scripts=["zepto.min.js","unveil.js"] 53 | headers = { 54 | "title": "Home Page", 55 | "tags": ", ".join(sorted(list(set(tags)))).lower().replace(",,", ",") 56 | } 57 | 58 | %> 59 |
60 |
61 | %rebase('layout', **dict(globals())) 62 | -------------------------------------------------------------------------------- /themes/blog/views/common.tpl: -------------------------------------------------------------------------------- 1 | <% 2 | from sushy.utils import utc_date, fuzzy_time, time_since, ordinal 3 | from sushy.config import BLOG_ENTRIES 4 | from sushy.models import get_page_metadata 5 | from datetime import datetime, timedelta 6 | from re import match 7 | if "from" in headers: 8 | author = headers["from"] 9 | else: 10 | author = "Unknown" 11 | end 12 | 13 | def format_date(when, relative=False): 14 | now = datetime.utcnow() 15 | delta = when - now 16 | if relative and (delta > timedelta(weeks=-12)): 17 | return "%s ago, %s" % (time_since(when, now).split(',')[0], fuzzy_time(when)) 18 | else: 19 | return "%s %s %d, %s" % (when.strftime("%B"), ordinal(when.day), when.year, fuzzy_time(when)) 20 | end 21 | end 22 | 23 | page_metadata = get_page_metadata(pagename) 24 | published = modified = readtime = "" 25 | metadata = author 26 | 27 | if page_metadata: 28 | published = format_date(page_metadata["pubtime"]) 29 | modified = format_date(page_metadata["mtime"]) 30 | if "readtime" in page_metadata: 31 | readtime = "· %d min read" % (max(int(round(page_metadata["readtime"] / 60.0)), 1)) 32 | end 33 | 34 | metadata = "%s
%s %s" % (author, published, readtime) 35 | 36 | if not BLOG_ENTRIES.match(pagename) and "last-modified" in headers and len(modified): 37 | metadata = "%s
updated %s %s" % (author, modified, readtime) 38 | end 39 | end 40 | 41 | default_author_image = "/static/img/avatars/unknown36x36.png" 42 | default_author_image = "/apple-touch-icon.png" 43 | author_image = "/static/img/avatars/%s36x36.png" % author.replace(" ","").lower() 44 | author_image_retina = "/static/img/avatars/%s80x80.png" % author.replace(" ","").lower() 45 | %> 46 | -------------------------------------------------------------------------------- /themes/blog/views/custom_ads.tpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /themes/blog/views/custom_footer.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/themes/blog/views/custom_footer.tpl -------------------------------------------------------------------------------- /themes/blog/views/custom_meta.tpl: -------------------------------------------------------------------------------- 1 | <% from os import environ %> 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /themes/blog/views/debug.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %for e in sorted(environ): 10 | 11 | 12 | 13 | 14 | %end 15 | 16 |
KeyValue
{{!e}}{{!environ[e]}}
17 | %rebase('layout', headers=headers) 18 | -------------------------------------------------------------------------------- /themes/blog/views/inline-message.tpl: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /themes/blog/views/inline-table.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %for i in headers[0::2]: 5 | 6 | %end 7 | 8 | 9 | 10 | %for i in rows: 11 | 12 | %for j in headers[1::2]: 13 | %if j == "name": 14 | 15 | %else: 16 | 17 | %end 18 | %end 19 | 20 | %end 21 | 22 |
{{!i}}
{{!i["title"]}}{{!i[j]}}
-------------------------------------------------------------------------------- /themes/blog/views/layout.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{!headers["title"]}} - {{!site_name}} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | %if "tags" in headers: 15 | 16 | %end 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | %include('custom_meta', **globals()) 26 | %if defined('scripts'): 27 | %for script in scripts: 28 | 29 | %end 30 | %end 31 | 38 | 39 | 40 | %include('sidebar', **globals()) 41 |
42 |
43 |
44 |

45 | {{!site_name}} 46 |

47 |
48 |
49 | %include('custom_ads', **globals()) 50 | {{!base}} 51 |
52 | 53 | 54 |
55 | %include('custom_footer', **globals()) 56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /themes/blog/views/metadata.tpl: -------------------------------------------------------------------------------- 1 | 2 | % if "x-link" in headers: 3 |

{{!headers["title"]}} 

4 | % else: 5 |

{{!headers["title"]}}

6 | % end 7 |
8 | -------------------------------------------------------------------------------- /themes/blog/views/opensearch.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{site_name}} 4 | {{site_description}} 5 | UTF-8 6 | {{base_url}}/favicon.ico 7 | 8 | 9 | -------------------------------------------------------------------------------- /themes/blog/views/prevnext.tpl: -------------------------------------------------------------------------------- 1 | <% 2 | import re 3 | from sushy.models import get_prev_next 4 | from sushy.config import BLOG_ENTRIES 5 | if 'meta' not in headers.get('tags',''): 6 | %> 7 | 26 | <% 27 | end 28 | %> -------------------------------------------------------------------------------- /themes/blog/views/robots.tpl: -------------------------------------------------------------------------------- 1 | Sitemap: {{base_url}}/sitemap.xml 2 | 3 | % for nuisance in ["AhrefsBot", "BLEXBot", "MJ12bot", "Covario-IDS", "TopBlogsInfo", "spbot", "attributor", "psbot", "SemrushBot", "SemrushBot-SA", "SiteSucker", "Scooter", "ZyBorg", "Slurp", "Pompos", "inetbot", "Domain Re-Animator Bot"]: 4 | User-agent: {{nuisance}} 5 | Disallow: / 6 | 7 | % end 8 | 9 | % for legit in ["Google", "ia_archiver"]: 10 | User-agent: {{legit}} 11 | Disallow: /js/ 12 | Disallow: /themes/ 13 | Disallow: /space/meta/ 14 | 15 | % end 16 | 17 | User-agent: * 18 | Disallow: /js/ 19 | Disallow: /img/ 20 | Disallow: /themes/ 21 | Disallow: /media/ 22 | Disallow: /thumbnail/ 23 | Disallow: /space/meta/ 24 | -------------------------------------------------------------------------------- /themes/blog/views/search.tpl: -------------------------------------------------------------------------------- 1 | <% 2 | from itertools import islice 3 | from sushy.utils import time_since 4 | if defined("query"): 5 | headers['title'] = "Search results for '%s'" % query 6 | items = list(islice(results,0,20)) 7 | if len(items): 8 | %> 9 |
10 |

Search Results

11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | %for i in items: 23 | 24 | 25 | 26 | 27 | 28 | %end 29 | 30 |
PageContentModified
{{!i["title"]}}{{!i["content"]}}{{!time_since(i["mtime"])}} ago
31 |
32 | <% 33 | else: 34 | headers['title'] = "No results for '%s'" % query 35 | include('inline-message', level="info", message="No pages that matched your query were found.") 36 | end 37 | else: 38 | headers['title'] = "Invalid search" 39 | include('inline-message', level="error", message="No valid query parameters specified.") 40 | end 41 | 42 | rebase('layout', headers=headers, base_url=base_url, site_description=site_description, site_name=site_name) 43 | %> 44 | -------------------------------------------------------------------------------- /themes/blog/views/seealso.tpl: -------------------------------------------------------------------------------- 1 | %if defined("seealso") and len(seealso): 2 |
3 |
4 |

See Also:

5 |
6 | {{!''.join(['' + p['title'] + '' for p in seealso[:10]])}} 7 |
8 |
9 | %end 10 | -------------------------------------------------------------------------------- /themes/blog/views/sidebar.tpl: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 30 | -------------------------------------------------------------------------------- /themes/blog/views/sitemap.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | % for item in items: 4 | 5 | {{base_url}}{{page_route_base}}/{{item["name"]}} 6 | {{item["mtime"].isoformat()}}+00:00 7 | 8 | % end 9 | 10 | -------------------------------------------------------------------------------- /themes/blog/views/wiki.tpl: -------------------------------------------------------------------------------- 1 | <% 2 | namespace = pagename.split("/")[0].lower() 3 | %> 4 |
5 | %include('metadata', **include('common')) 6 |
7 | {{!body}} 8 |
9 |
10 | %include('prevnext') 11 |
12 | %include('seealso') 13 |
14 | <% 15 | scripts=["zepto.min.js", "unveil.js", "footnotes.js"] 16 | %> 17 | %rebase('layout', **dict(globals())) 18 | -------------------------------------------------------------------------------- /themes/wiki/static/css/rss.css: -------------------------------------------------------------------------------- 1 | body, div { 2 | color: black; 3 | font-size: 12pt; 4 | word-wrap: break-word; 5 | -webkit-nbsp-mode: space; 6 | -webkit-line-break: after-white-space; 7 | font-family: Georgia, Times New Roman, Times, serif; 8 | } 9 | 10 | pre { 11 | font-family: Menlo, Consolas, Courier New, Courier, monospace; 12 | } 13 | 14 | a:link { 15 | color: #0000cc 16 | } 17 | 18 | table { 19 | background:transparent; 20 | margin: 0px; 21 | padding: 0px; 22 | border-collapse: collapse; 23 | } 24 | 25 | table thead tr { 26 | font-size: 9pt; 27 | border-top: 1px solid #aaa; 28 | } 29 | 30 | table thead tr th { 31 | border-bottom: 1px solid black; 32 | border-top: 2px solid black; 33 | padding: 4px; 34 | font-weight: bold; 35 | } 36 | 37 | table tbody tr { 38 | font-size: 9pt; 39 | border-top: 1px solid #aaa; 40 | } 41 | 42 | table tbody td { 43 | min-width: 80px; 44 | margin: 0px; 45 | padding: 4px; 46 | vertical-align: top; 47 | border-top: 1px solid #aaa; 48 | } 49 | 50 | img { 51 | max-width: 100% !important; height: auto; 52 | } 53 | -------------------------------------------------------------------------------- /themes/wiki/static/css/site.css: -------------------------------------------------------------------------------- 1 | #topbar { 2 | position: fixed; 3 | z-index: 100; 4 | width: 100%; 5 | background: #4c9220; 6 | -webkit-transform: scale3d(1,1,1); 7 | opacity: 0.95; 8 | top: 0; 9 | } 10 | 11 | .page-header { 12 | padding-top: 4em; 13 | font-size: 1.25em; 14 | } 15 | 16 | .highlight pre { 17 | font-family: Menlo, Monaco, "Andale Mono", "Lucida Console", "Courier New", monospace !important; 18 | padding: 0.5em; 19 | } 20 | 21 | html { 22 | height: 100%; 23 | } 24 | 25 | body { 26 | position: relative; 27 | margin: 0; 28 | min-height: 100%; 29 | padding-bottom: 4em; 30 | } 31 | 32 | #seealso { 33 | padding-top: 1em; 34 | text-align: top; 35 | margin: 0px; 36 | display: table; 37 | } 38 | #seealso .holder { 39 | display: table-row; 40 | } 41 | #seealso a { 42 | display: inline-table; 43 | margin: 0px; 44 | padding: 0.3em; 45 | height: 4em; 46 | background: white; 47 | } 48 | 49 | a.seelink:hover { 50 | background: #4c9220 !important; 51 | color: white; 52 | text-decoration: none; 53 | } 54 | 55 | footer { 56 | position: absolute; 57 | bottom: 0; 58 | left: 0; 59 | right: 0; 60 | width: 100%; 61 | line-height: 1.6em; 62 | font-size: 1em; 63 | padding: 1em; 64 | background: #2c5200; 65 | color: white; 66 | text-align: right; 67 | } 68 | footer a, footer a:hover { 69 | color: white; 70 | } 71 | .footnote { 72 | font-size: 85%; 73 | } 74 | h1, h2, h3, h4 { 75 | font-family:Roboto_slab,Arial,Helvetica,sans-serif; 76 | font-weight: normal; 77 | } 78 | -------------------------------------------------------------------------------- /themes/wiki/static/css/syntax.css: -------------------------------------------------------------------------------- 1 | /* Solarized Dark 2 | 3 | For use with Jekyll and Pygments 4 | 5 | http://ethanschoonover.com/solarized 6 | 7 | SOLARIZED HEX ROLE 8 | --------- -------- ------------------------------------------ 9 | base03 #002b36 background 10 | base01 #586e75 comments / secondary content 11 | base1 #93a1a1 body text / default code / primary content 12 | orange #cb4b16 constants 13 | red #dc322f regex, special keywords 14 | blue #268bd2 reserved keywords 15 | cyan #2aa198 strings, numbers 16 | green #859900 operators, other keywords 17 | */ 18 | 19 | .highlight { background-color: #002b36; color: #93a1a1 } 20 | .highlight .c { color: #586e75 } /* Comment */ 21 | .highlight .err { color: #93a1a1 } /* Error */ 22 | .highlight .g { color: #93a1a1 } /* Generic */ 23 | .highlight .k { color: #859900 } /* Keyword */ 24 | .highlight .l { color: #93a1a1 } /* Literal */ 25 | .highlight .n { color: #93a1a1 } /* Name */ 26 | .highlight .o { color: #859900 } /* Operator */ 27 | .highlight .x { color: #cb4b16 } /* Other */ 28 | .highlight .p { color: #93a1a1 } /* Punctuation */ 29 | .highlight .cm { color: #586e75 } /* Comment.Multiline */ 30 | .highlight .cp { color: #859900 } /* Comment.Preproc */ 31 | .highlight .c1 { color: #586e75 } /* Comment.Single */ 32 | .highlight .cs { color: #859900 } /* Comment.Special */ 33 | .highlight .gd { color: #2aa198 } /* Generic.Deleted */ 34 | .highlight .ge { color: #93a1a1; font-style: italic } /* Generic.Emph */ 35 | .highlight .gr { color: #dc322f } /* Generic.Error */ 36 | .highlight .gh { color: #cb4b16 } /* Generic.Heading */ 37 | .highlight .gi { color: #859900 } /* Generic.Inserted */ 38 | .highlight .go { color: #93a1a1 } /* Generic.Output */ 39 | .highlight .gp { color: #93a1a1 } /* Generic.Prompt */ 40 | .highlight .gs { color: #93a1a1; font-weight: bold } /* Generic.Strong */ 41 | .highlight .gu { color: #cb4b16 } /* Generic.Subheading */ 42 | .highlight .gt { color: #93a1a1 } /* Generic.Traceback */ 43 | .highlight .kc { color: #cb4b16 } /* Keyword.Constant */ 44 | .highlight .kd { color: #268bd2 } /* Keyword.Declaration */ 45 | .highlight .kn { color: #859900 } /* Keyword.Namespace */ 46 | .highlight .kp { color: #859900 } /* Keyword.Pseudo */ 47 | .highlight .kr { color: #268bd2 } /* Keyword.Reserved */ 48 | .highlight .kt { color: #dc322f } /* Keyword.Type */ 49 | .highlight .ld { color: #93a1a1 } /* Literal.Date */ 50 | .highlight .m { color: #2aa198 } /* Literal.Number */ 51 | .highlight .s { color: #2aa198 } /* Literal.String */ 52 | .highlight .na { color: #93a1a1 } /* Name.Attribute */ 53 | .highlight .nb { color: #B58900 } /* Name.Builtin */ 54 | .highlight .nc { color: #268bd2 } /* Name.Class */ 55 | .highlight .no { color: #cb4b16 } /* Name.Constant */ 56 | .highlight .nd { color: #268bd2 } /* Name.Decorator */ 57 | .highlight .ni { color: #cb4b16 } /* Name.Entity */ 58 | .highlight .ne { color: #cb4b16 } /* Name.Exception */ 59 | .highlight .nf { color: #268bd2 } /* Name.Function */ 60 | .highlight .nl { color: #93a1a1 } /* Name.Label */ 61 | .highlight .nn { color: #93a1a1 } /* Name.Namespace */ 62 | .highlight .nx { color: #93a1a1 } /* Name.Other */ 63 | .highlight .py { color: #93a1a1 } /* Name.Property */ 64 | .highlight .nt { color: #268bd2 } /* Name.Tag */ 65 | .highlight .nv { color: #268bd2 } /* Name.Variable */ 66 | .highlight .ow { color: #859900 } /* Operator.Word */ 67 | .highlight .w { color: #93a1a1 } /* Text.Whitespace */ 68 | .highlight .mf { color: #2aa198 } /* Literal.Number.Float */ 69 | .highlight .mh { color: #2aa198 } /* Literal.Number.Hex */ 70 | .highlight .mi { color: #2aa198 } /* Literal.Number.Integer */ 71 | .highlight .mo { color: #2aa198 } /* Literal.Number.Oct */ 72 | .highlight .sb { color: #586e75 } /* Literal.String.Backtick */ 73 | .highlight .sc { color: #2aa198 } /* Literal.String.Char */ 74 | .highlight .sd { color: #93a1a1 } /* Literal.String.Doc */ 75 | .highlight .s2 { color: #2aa198 } /* Literal.String.Double */ 76 | .highlight .se { color: #cb4b16 } /* Literal.String.Escape */ 77 | .highlight .sh { color: #93a1a1 } /* Literal.String.Heredoc */ 78 | .highlight .si { color: #2aa198 } /* Literal.String.Interpol */ 79 | .highlight .sx { color: #2aa198 } /* Literal.String.Other */ 80 | .highlight .sr { color: #dc322f } /* Literal.String.Regex */ 81 | .highlight .s1 { color: #2aa198 } /* Literal.String.Single */ 82 | .highlight .ss { color: #2aa198 } /* Literal.String.Symbol */ 83 | .highlight .bp { color: #268bd2 } /* Name.Builtin.Pseudo */ 84 | .highlight .vc { color: #268bd2 } /* Name.Variable.Class */ 85 | .highlight .vg { color: #268bd2 } /* Name.Variable.Global */ 86 | .highlight .vi { color: #268bd2 } /* Name.Variable.Instance */ 87 | .highlight .il { color: #2aa198 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /themes/wiki/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcarmo/sushy/c53cbf50a8301247ddf35d370f84c88518551e14/themes/wiki/static/favicon.ico -------------------------------------------------------------------------------- /themes/wiki/static/js/app.js: -------------------------------------------------------------------------------- 1 | var source = new EventSource("/events"); 2 | 3 | source.addEventListener('error', function(e) { 4 | if (e.readyState == EventSource.CLOSED) { 5 | console.log("Server down") 6 | } 7 | else if( e.readyState == EventSource.OPEN) { 8 | console.log("Connecting...") 9 | } 10 | }, false); 11 | 12 | source.addEventListener('tick', function(e) { 13 | console.log(e.data); 14 | }, false); 15 | source.addEventListener('indexing', function(e) { 16 | console.log(e.data); 17 | }, false); 18 | -------------------------------------------------------------------------------- /themes/wiki/static/js/debug.js: -------------------------------------------------------------------------------- 1 | /* Addy Osmani's one-line layout debugger */ 2 | [].forEach.call(document.querySelectorAll("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16)}) -------------------------------------------------------------------------------- /themes/wiki/static/js/eventsource.js: -------------------------------------------------------------------------------- 1 | ;(function (global) { 2 | 3 | if ("EventSource" in global) return; 4 | 5 | var reTrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g; 6 | 7 | var EventSource = function (url) { 8 | var eventsource = this, 9 | interval = 500, // polling interval 10 | lastEventId = null, 11 | cache = ''; 12 | 13 | if (!url || typeof url != 'string') { 14 | throw new SyntaxError('Not enough arguments'); 15 | } 16 | 17 | this.URL = url; 18 | this.readyState = this.CONNECTING; 19 | this._pollTimer = null; 20 | this._xhr = null; 21 | 22 | function pollAgain(interval) { 23 | eventsource._pollTimer = setTimeout(function () { 24 | poll.call(eventsource); 25 | }, interval); 26 | } 27 | 28 | function poll() { 29 | try { // force hiding of the error message... insane? 30 | if (eventsource.readyState == eventsource.CLOSED) return; 31 | 32 | // NOTE: IE7 and upwards support 33 | var xhr = new XMLHttpRequest(); 34 | xhr.open('GET', eventsource.URL, true); 35 | xhr.setRequestHeader('Accept', 'text/event-stream'); 36 | xhr.setRequestHeader('Cache-Control', 'no-cache'); 37 | // we must make use of this on the server side if we're working with Android - because they don't trigger 38 | // readychange until the server connection is closed 39 | xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 40 | 41 | if (lastEventId != null) xhr.setRequestHeader('Last-Event-ID', lastEventId); 42 | cache = ''; 43 | 44 | xhr.timeout = 50000; 45 | xhr.onreadystatechange = function () { 46 | if ((this.readyState == 3 || this.readyState == 4) && this.status == 200) { 47 | // on success 48 | if (eventsource.readyState == eventsource.CONNECTING) { 49 | eventsource.readyState = eventsource.OPEN; 50 | eventsource.dispatchEvent('open', { type: 'open' }); 51 | } 52 | 53 | var responseText = ''; 54 | try { 55 | responseText = this.responseText || ''; 56 | } catch (e) {} 57 | 58 | // process this.responseText 59 | var parts = responseText.substr(cache.length).split("\n"), 60 | eventType = 'message', 61 | data = [], 62 | i = 0, 63 | line = ''; 64 | 65 | cache = responseText; 66 | 67 | // TODO handle 'event' (for buffer name), retry 68 | for (; i < parts.length; i++) { 69 | line = parts[i].replace(reTrim, ''); 70 | if (line.indexOf('event') == 0) { 71 | eventType = line.replace(/event:?\s*/, ''); 72 | } else if (line.indexOf('retry') == 0) { 73 | interval = line.replace(/retry:?\s*/, '') + 0; 74 | } else if (line.indexOf('data') == 0) { 75 | data.push(line.replace(/data:?\s*/, '')); 76 | } else if (line.indexOf('id:') == 0) { 77 | lastEventId = line.replace(/id:?\s*/, ''); 78 | } else if (line.indexOf('id') == 0) { // this resets the id 79 | lastEventId = null; 80 | } else if (line == '') { 81 | if (data.length) { 82 | var event = new MessageEvent(data.join('\n'), eventsource.url, lastEventId); 83 | eventsource.dispatchEvent(eventType, event); 84 | data = []; 85 | eventType = 'message'; 86 | } 87 | } 88 | } 89 | 90 | if (this.readyState == 4) pollAgain(interval); 91 | // don't need to poll again, because we're long-loading 92 | } else if (eventsource.readyState !== eventsource.CLOSED) { 93 | if (this.readyState == 4) { // and some other status 94 | // dispatch error 95 | eventsource.readyState = eventsource.CONNECTING; 96 | eventsource.dispatchEvent('error', { type: 'error' }); 97 | pollAgain(interval); 98 | } else if (this.readyState == 0) { // likely aborted 99 | pollAgain(interval); 100 | } else { 101 | } 102 | } 103 | }; 104 | 105 | xhr.send(); 106 | 107 | setTimeout(function () { 108 | if (true || xhr.readyState == 3) xhr.abort(); 109 | }, xhr.timeout); 110 | 111 | eventsource._xhr = xhr; 112 | 113 | } catch (e) { // in an attempt to silence the errors 114 | eventsource.dispatchEvent('error', { type: 'error', data: e.message }); // ??? 115 | } 116 | }; 117 | 118 | poll(); // init now 119 | }; 120 | 121 | EventSource.prototype = { 122 | close: function () { 123 | // closes the connection - disabling the polling 124 | this.readyState = this.CLOSED; 125 | clearInterval(this._pollTimer); 126 | this._xhr.abort(); 127 | }, 128 | CONNECTING: 0, 129 | OPEN: 1, 130 | CLOSED: 2, 131 | dispatchEvent: function (type, event) { 132 | var handlers = this['_' + type + 'Handlers']; 133 | if (handlers) { 134 | for (var i = 0; i < handlers.length; i++) { 135 | handlers[i].call(this, event); 136 | } 137 | } 138 | 139 | if (this['on' + type]) { 140 | this['on' + type].call(this, event); 141 | } 142 | }, 143 | addEventListener: function (type, handler) { 144 | if (!this['_' + type + 'Handlers']) { 145 | this['_' + type + 'Handlers'] = []; 146 | } 147 | 148 | this['_' + type + 'Handlers'].push(handler); 149 | }, 150 | removeEventListener: function () { 151 | // TODO 152 | }, 153 | onerror: null, 154 | onmessage: null, 155 | onopen: null, 156 | readyState: 0, 157 | URL: '' 158 | }; 159 | 160 | var MessageEvent = function (data, origin, lastEventId) { 161 | this.data = data; 162 | this.origin = origin; 163 | this.lastEventId = lastEventId || ''; 164 | }; 165 | 166 | MessageEvent.prototype = { 167 | data: null, 168 | type: 'message', 169 | lastEventId: '', 170 | origin: '' 171 | }; 172 | 173 | if ('module' in global) module.exports = EventSource; 174 | global.EventSource = EventSource; 175 | 176 | })(this); 177 | -------------------------------------------------------------------------------- /themes/wiki/static/js/radio.js: -------------------------------------------------------------------------------- 1 | /** 2 | Radio.js - Chainable, Dependency Free Publish/Subscribe for Javascript 3 | http://radio.uxder.com 4 | Author: Scott Murphy 2011 5 | twitter: @hellocreation, github: uxder 6 | 7 | Permission is hereby granted, free of charge, to any person 8 | obtaining a copy of this software and associated documentation 9 | files (the "Software"), to deal in the Software without 10 | restriction, including without limitation the rights to use, 11 | copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following 14 | conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | OTHER DEALINGS IN THE SOFTWARE. 27 | */ 28 | (function (name, global, definition) { 29 | if (typeof module !== 'undefined') module.exports = definition(name, global); 30 | else if (typeof define === 'function' && typeof define.amd === 'object') define(definition); 31 | else global[name] = definition(name, global); 32 | })('radio', this, function (name, global) { 33 | 34 | "use strict"; 35 | 36 | /** 37 | * Main Wrapper for radio.$ and create a function radio to accept the channelName 38 | * @param {String} channelName topic of event 39 | */ 40 | function radio(channelName) { 41 | arguments.length ? radio.$.channel(channelName) : radio.$.reset(); 42 | return radio.$; 43 | } 44 | 45 | radio.$ = { 46 | version: '0.2', 47 | channelName: "", 48 | channels: [], 49 | 50 | /** 51 | * Reset global state, by removing all channels 52 | * @example 53 | * radio() 54 | */ 55 | reset: function() { 56 | radio.$.channelName = ""; 57 | radio.$.channels = []; 58 | }, 59 | 60 | /** 61 | * Broadcast (publish) 62 | * Iterate through all listeners (callbacks) in current channel and pass arguments to subscribers 63 | * @param arguments data to be sent to listeners 64 | * @example 65 | * //basic usage 66 | * radio('channel1').broadcast('my message'); 67 | * //send an unlimited number of parameters 68 | * radio('channel2').broadcast(param1, param2, param3 ... ); 69 | */ 70 | broadcast: function() { 71 | var i, c = this.channels[this.channelName], 72 | l = c.length, 73 | subscriber, callback, context; 74 | //iterate through current channel and run each subscriber 75 | for (i = 0; i < l; i++) { 76 | subscriber = c[i]; 77 | //if subscriber was an array, set the callback and context. 78 | if ((typeof(subscriber) === 'object') && (subscriber.length)) { 79 | callback = subscriber[0]; 80 | //if user set the context, set it to the context otherwise, it is a globally scoped function 81 | context = subscriber[1] || global; 82 | } 83 | callback.apply(context, arguments); 84 | } 85 | return this; 86 | }, 87 | 88 | /** 89 | * Create the channel if it doesn't exist and set the current channel/event name 90 | * @param {String} name the name of the channel 91 | * @example 92 | * radio('channel1'); 93 | */ 94 | channel: function(name) { 95 | var c = this.channels; 96 | //create a new channel if it doesn't exists 97 | if (!c[name]) c[name] = []; 98 | this.channelName = name; 99 | return this; 100 | }, 101 | 102 | /** 103 | * Add Subscriber to channel 104 | * Take the arguments and add it to the this.channels array. 105 | * @param {Function|Array} arguments list of callbacks or arrays[callback, context] separated by commas 106 | * @example 107 | * //basic usage 108 | * var callback = function() {}; 109 | * radio('channel1').subscribe(callback); 110 | * 111 | * //subscribe an endless amount of callbacks 112 | * radio('channel1').subscribe(callback, callback2, callback3 ...); 113 | * 114 | * //adding callbacks with context 115 | * radio('channel1').subscribe([callback, context],[callback1, context], callback3); 116 | * 117 | * //subscribe by chaining 118 | * radio('channel1').subscribe(callback).radio('channel2').subscribe(callback).subscribe(callback2); 119 | */ 120 | subscribe: function() { 121 | var a = arguments, 122 | c = this.channels[this.channelName], 123 | i, l = a.length, 124 | p, ai = []; 125 | 126 | //run through each arguments and subscribe it to the channel 127 | for (i = 0; i < l; i++) { 128 | ai = a[i]; 129 | //if the user sent just a function, wrap the fucntion in an array [function] 130 | p = (typeof(ai) === "function") ? [ai] : ai; 131 | if ((typeof(p) === 'object') && (p.length)) c.push(p); 132 | } 133 | return this; 134 | }, 135 | 136 | /** 137 | * Remove subscriber from channel 138 | * Take arguments with functions and unsubscribe it if there is a match against existing subscribers. 139 | * @param {Function} arguments callbacks separated by commas 140 | * @example 141 | * //basic usage 142 | * radio('channel1').unsubscribe(callback); 143 | * //you can unsubscribe as many callbacks as you want 144 | * radio('channel1').unsubscribe(callback, callback2, callback3 ...); 145 | * //removing callbacks with context is the same 146 | * radio('channel1').subscribe([callback, context]).unsubscribe(callback); 147 | */ 148 | unsubscribe: function() { 149 | var a = arguments, 150 | i, j, c = this.channels[this.channelName], 151 | l = a.length, 152 | cl = c.length, 153 | offset = 0, 154 | jo; 155 | //loop through each argument 156 | for (i = 0; i < l; i++) { 157 | //need to reset vars that change as the channel array items are removed 158 | offset = 0; 159 | cl = c.length; 160 | //loop through the channel 161 | for (j = 0; j < cl; j++) { 162 | jo = j - offset; 163 | //if there is a match with the argument and the channel function, unsubscribe it from the channel array 164 | if (c[jo][0] === a[i]) { 165 | //unsubscribe matched item from the channel array 166 | c.splice(jo, 1); 167 | offset++; 168 | } 169 | } 170 | } 171 | return this; 172 | } 173 | }; 174 | 175 | return radio; 176 | }); 177 | -------------------------------------------------------------------------------- /themes/wiki/static/js/utils.js: -------------------------------------------------------------------------------- 1 | gradient = { 2 | hex2rgb : function(hex){ 3 | hex = hex.replace('#', ''); 4 | if(hex.length !== 3 && hex.length !== 6){ 5 | return [255,255,255]; 6 | } 7 | if(hex.length == 3){ 8 | hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; 9 | } 10 | return [parseInt(hex.substr(0,2),16), 11 | parseInt(hex.substr(2,2),16), 12 | parseInt(hex.substr(4,2),16)]; 13 | }, 14 | 15 | rgb2hex : function(rgb){ 16 | return "#" + 17 | ("0" + Math.round(rgb[0]).toString(16)).slice(-2) + 18 | ("0" + Math.round(rgb[1]).toString(16)).slice(-2) + 19 | ("0" + Math.round(rgb[2]).toString(16)).slice(-2); 20 | }, 21 | 22 | generate : function(start, finish, steps){ 23 | var result = []; 24 | 25 | start = this.hex2rgb(start); 26 | finish = this.hex2rgb(finish); 27 | steps -= 1; 28 | ri = (finish[0] - start[0]) / steps; 29 | gi = (finish[1] - start[1]) / steps; 30 | bi = (finish[2] - start[2]) / steps; 31 | 32 | result.push(this.rgb2hex(start)); 33 | 34 | var rv = start[0], 35 | gv = start[1], 36 | bv = start[2]; 37 | 38 | for (var i = 0; i < (steps-1); i++) { 39 | rv += ri; 40 | gv += gi; 41 | bv += bi; 42 | result.push(this.rgb2hex([rv, gv, bv])); 43 | }; 44 | 45 | result.push(this.rgb2hex(finish)); 46 | return result; 47 | } 48 | } -------------------------------------------------------------------------------- /themes/wiki/views/debug.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %for e in sorted(environ): 10 | 11 | 12 | 13 | 14 | %end 15 | 16 |
VariableValue
{{!e}}{{!environ[e]}}
17 | %rebase('layout', headers=headers) 18 | -------------------------------------------------------------------------------- /themes/wiki/views/inline-message.tpl: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /themes/wiki/views/layout.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{!headers["title"]}} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 40 | 41 | %if defined('scripts'): 42 | %for script in scripts: 43 | 44 | %end 45 | %end 46 | 47 | 48 | 49 | 50 | 59 | 60 |
61 | 62 | 83 | 84 | 86 |
87 |
88 |
89 | 90 | 91 | 92 | 93 |
94 | 97 | {{!base}} 98 | %include('seealso') 99 |
100 |
101 | Source on Github | Made with Ink 102 |
103 | 104 | 105 | -------------------------------------------------------------------------------- /themes/wiki/views/opensearch.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{site_name}} 4 | {{site_description}} 5 | UTF-8 6 | {{base_url}}/favicon.ico 7 | 8 | -------------------------------------------------------------------------------- /themes/wiki/views/robots.tpl: -------------------------------------------------------------------------------- 1 | Sitemap: {{base_url}}/sitemap.xml 2 | 3 | % for nuisance in ["MJ12bot", "Covario-IDS", "TopBlogsInfo", "spbot", "attributor", "psbot", "SiteSucker", "Scooter", "ZyBorg", "Slurp", "Pompos", "inetbot"]: 4 | User-agent: {{nuisance}} 5 | Disallow: / 6 | 7 | % end 8 | 9 | % for legit in ["Google", "ia_archiver"]: 10 | User-agent: {{legit}} 11 | Disallow: /js/ 12 | Disallow: /themes/ 13 | Disallow: /space/meta/ 14 | 15 | % end 16 | 17 | User-agent: * 18 | Disallow: /js/ 19 | Disallow: /img/ 20 | Disallow: /themes/ 21 | Disallow: /media/ 22 | Disallow: /thumbnail/ 23 | Disallow: /space/meta/ 24 | -------------------------------------------------------------------------------- /themes/wiki/views/rss.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{site_name}} 5 | {{base_url}} 6 | {{site_description}} 7 | {{site_copyright}} 8 | {{feed_ttl/60}} 9 | {{pubdate}} 10 | {{pubdate}} 11 | sushy 12 | %for item in items: 13 | 14 | {{item["title"]}} 15 | {{base_url}}{{page_route_base}}/{{item["pagename"]}} 16 | {{item["description"]}} 17 | {{item["pubdate"]}} 18 | {{item["author"]}} 19 | {{site_name}} 20 | {{base_url}}{{page_route_base}}/{{item["pagename"]}} 21 | {{item["category"]}} 22 | 23 | %end 24 | 25 | 26 | -------------------------------------------------------------------------------- /themes/wiki/views/search.tpl: -------------------------------------------------------------------------------- 1 | <% 2 | from itertools import islice 3 | if defined("query"): 4 | headers['title'] = "Search results for '%s'" % query 5 | items = list(islice(results,0,20)) 6 | if len(items): 7 | %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | %for i in items: 19 | 20 | 21 | 22 | 23 | 24 | 25 | %end 26 | 27 |
ScorePageContentModified
{{!i["score"]}}{{!i["title"]}}{{!i["content"]}}{{!i["mtime"]}}
28 | <% 29 | else: 30 | headers['title'] = "No results for '%s'" % query 31 | include('inline-message', level="info", message="No pages that matched your query were found.") 32 | end 33 | else: 34 | headers['title'] = "Invalid search" 35 | include('inline-message', level="error", message="No valid query parameters specified.") 36 | end 37 | rebase('layout', headers=headers) 38 | %> 39 | -------------------------------------------------------------------------------- /themes/wiki/views/seealso.tpl: -------------------------------------------------------------------------------- 1 | %if defined("seealso") and len(seealso): 2 |
3 |
4 |

See Also:

5 |
6 | {{!''.join(['' + p['title'] + '' for p in seealso])}} 7 |
8 |
9 | 20 | %end -------------------------------------------------------------------------------- /themes/wiki/views/sitemap.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | % for item in items: 4 | 5 | {{base_url}}{{page_route_base}}/{{item["name"]}} 6 | {{item["mtime"].isoformat()}}+00:00 7 | 8 | % end 9 | 10 | -------------------------------------------------------------------------------- /themes/wiki/views/wiki.tpl: -------------------------------------------------------------------------------- 1 | {{!body}} 2 | %rebase('layout', base_url=base_url, headers=headers, pagename=pagename, seealso=seealso, site_name=site_name, scripts=['eventsource.js','utils.js','app.js']) 3 | -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | project=sushy 3 | procname=%(project) 4 | master=false 5 | ;processes=4 6 | ;threads = 100 7 | max-requests = 1000 8 | idle = 3600 9 | buffer-size = 8192 10 | listen = 1 11 | gevent = 2000 12 | http = :$(PORT) 13 | env = WSGI_PORT=http 14 | module = %(project).app:app 15 | ;chdir = %(code_dir) 16 | ;logto = %(deploy_dir)/logs/uwsgi.log 17 | ;pidfile = %(deploy_dir)/run/%(project).pid 18 | ;socket = %(deploy_dir)/run/uwsgi-%(project).sock 19 | ;chmod-socket = 777 20 | --------------------------------------------------------------------------------