├── .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 | 
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 | 
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 | 
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 |
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 | Key
5 | Value
6 |
7 |
8 |
9 | %for e in sorted(environ):
10 |
11 | {{!e}}
12 | {{!environ[e]}}
13 |
14 | %end
15 |
16 |
17 | %rebase('layout', headers=headers)
18 |
--------------------------------------------------------------------------------
/themes/blog/views/inline-message.tpl:
--------------------------------------------------------------------------------
1 |
2 |
{{level.capitalize()}}: {{!message}}
3 |
4 |
--------------------------------------------------------------------------------
/themes/blog/views/inline-table.tpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %for i in headers[0::2]:
5 | {{!i}}
6 | %end
7 |
8 |
9 |
10 | %for i in rows:
11 |
12 | %for j in headers[1::2]:
13 | %if j == "name":
14 | {{!i["title"]}}
15 | %else:
16 | {{!i[j]}}
17 | %end
18 | %end
19 |
20 | %end
21 |
22 |
--------------------------------------------------------------------------------
/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 |
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 |
4 | % else:
5 |
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 |
{{!len(items)}} matches found for {{!query}}.
12 |
13 |
14 |
15 |
16 | Page
17 | Content
18 | Modified
19 |
20 |
21 |
22 | %for i in items:
23 |
24 | {{!i["title"]}}
25 | {{!i["content"]}}
26 | {{!time_since(i["mtime"])}} ago
27 |
28 | %end
29 |
30 |
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 |
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 |
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 | Variable
5 | Value
6 |
7 |
8 |
9 | %for e in sorted(environ):
10 |
11 | {{!e}}
12 | {{!environ[e]}}
13 |
14 | %end
15 |
16 |
17 | %rebase('layout', headers=headers)
18 |
--------------------------------------------------------------------------------
/themes/wiki/views/inline-message.tpl:
--------------------------------------------------------------------------------
1 |
2 |
{{level.capitalize()}}: {{!message}}
3 |
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 |
63 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
97 | {{!base}}
98 | %include('seealso')
99 |
100 |
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 | Score
12 | Page
13 | Content
14 | Modified
15 |
16 |
17 |
18 | %for i in items:
19 |
20 | {{!i["score"]}}
21 | {{!i["title"]}}
22 | {{!i["content"]}}
23 | {{!i["mtime"]}}
24 |
25 | %end
26 |
27 |
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 |
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 |
--------------------------------------------------------------------------------