Compare commits

..

3 Commits

Author SHA1 Message Date
Martin Zimmermann b2bb73ba6f Back to development: 0.8.4
10 years ago
Martin Zimmermann 6a8a750bec Preparing release 0.8.3
10 years ago
Martin Zimmermann 2a1ad60373 Merge branch 'fix/issue-77' into legacy/0.8
10 years ago

@ -1,47 +0,0 @@
---
kind: pipeline
name: default
platform:
os: linux
arch: amd64
steps:
- name: publish
pull: default
image: plugins/docker:18.09
settings:
registry: https://registry.nixaid.com
repo: "registry.nixaid.com/${DRONE_REPO_NAMESPACE}/${DRONE_REPO_NAME}"
tags:
- latest
username:
from_secret: docker_username
password:
from_secret: docker_password
# storage_path: /drone/docker
# storage_driver: aufs
# ipv6: false
# debug: true
when:
branch:
- master
event:
- push
- tag
- name: notify
pull: default
image: drillster/drone-email:latest
settings:
from: "Drone CI <noreply@nixaid.com>"
host: mx.nixaid.com
port: 587
subject: "NIXAID Drone Pipeline {{#success build.status}}SUCCESS{{else}}FAILURE{{/success}} Notification"
when:
event:
- push
- tag
status:
- success
- failure

@ -1,85 +0,0 @@
# workspace:
# base: /workspace
# path: src/git.nixaid.com/arno/myapp/
#
# branches:
# - master
pipeline:
restore_cache:
image: drillster/drone-volume-cache:latest
restore: true
mount:
- /drone/docker
# Set the ``DRONE_VOLUME=/tmp/drone-cache:/cache`` drone-server variable,
# so you can benefit from the caching.
# Otherwise you will have to make this repository trusted in Drone and use
# the volumes as follows.
# volumes:
# - /tmp/drone-cache:/cache
# drone repo add arno/isso
# drone secret add/update --name docker_username --value arno --event push --event tag --event deployment arno/isso
# drone secret add/update --name docker_password --value "$(pass show vps/registry.nixaid.com | head -1)" --event push --event tag --event deployment arno/isso
publish:
image: plugins/docker:17.12
# repo: andrey01/${DRONE_REPO_NAME}
registry: registry.nixaid.com
repo: registry.nixaid.com/arno/${DRONE_REPO_NAME}
tags:
- latest
# - ${DRONE_COMMIT_SHA:0:7}
# group: docker
# dockerfile: Dockerfile
secrets: [docker_username, docker_password]
# Since we restore the docker image cache to /drone/docker
storage_path: /drone/docker
use_cache: true
when:
event: [push, tag]
branch: master
rebuild_cache:
image: drillster/drone-volume-cache:latest
rebuild: true
mount:
- /drone/docker
# Set the ``DRONE_VOLUME=/tmp/drone-cache:/cache`` drone-server variable,
# so you can benefit from the caching.
# Otherwise you will have to make this repository trusted in Drone and use
# the volumes as follows.
# volumes:
# - /tmp/drone-cache:/cache
# # ca_cert comes from /srv/data/registry/certs/ca.crt
# claircheck:
# # image: jmccann/drone-clair:1
# image: andrey01/drone-clair
# url: http://clair:6060
# secrets: [ docker_username, docker_password ]
# # ignore errors for now. This will work only in drone 0.9 https://github.com/drone/drone-runtime/commit/3e8bd99f60f4032226523320cd2b2321f9525159
# err_ignore: true
# scan_image: registry.nixaid.com/arno/${DRONE_REPO_NAME}:latest
# ca_cert: |
# -----BEGIN CERTIFICATE-----
# MIIBOjCB4KADAgECAgkAzhpbLWXa4H0wCgYIKoZIzj0EAwIwEDEOMAwGA1UEAwwF
# bXktQ0EwHhcNMTgwNzA5MjIzMTAzWhcNMjgwNzA2MjIzMTAzWjAQMQ4wDAYDVQQD
# DAVteS1DQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFIE8bTfQ76U5qG/Xgjw
# BbQU0oRJLYlRxBIWF9MTNSJr2LoaoyrU8jrcWQGRrfKPoVuwUJWp2tp5SJy0AHH7
# 4fijIzAhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKkMAoGCCqGSM49
# BAMCA0kAMEYCIQCYbTbxRD2yX4LzGjh84fKPWPQM9ps8RE2nfwZjqdRUGgIhAOHb
# USigh6FzqEPk2jiaV3t1wNtChRWRfupTKG6CD345
# -----END CERTIFICATE-----
notify:
image: drillster/drone-email:latest
from: Drone CI <noreply@nixaid.com>
subject: NIXAID Drone Pipeline {{#success build.status}}SUCCESS{{else}}FAILURE{{/success}} Notification
host: mx.nixaid.com
port: 587
# username: arno
# secrets: [ email_username, email_password ]
# recipients: [ andrey.arapov@nixaid.com ]
when:
status: [success, failure] # changed
event: [push, tag]

99
.gitignore vendored

@ -3,18 +3,15 @@
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
*~
# *~
*.pyc
.Python
.sass-cache
.vagrant
/bin
/include
/lib
/lib64
/man
/share
/isso.egg-info/
/isso/js/components
@ -25,97 +22,3 @@
/docs/_build
/docs/_static/css/site.css
/pip-selfcheck.json
# github/gitignore
Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
.venv/
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject

@ -1,25 +1,15 @@
language: python
python: 2.7
env:
- TOX_ENV=py26
- TOX_ENV=py27
- TOX_ENV=py33
- TOX_ENV=backport
matrix:
include:
- python: 3.5
env: TOX_ENV=py35
- python: 3.6
env: TOX_ENV=py36
- python: 3.7
dist: xenial
env: TOX_ENV=py37
- python: 3.8
dist: xenial
env: TOX_ENV=py38
allow_failures:
- env: TOX_ENV=backport
install:
- pip install -U pip
- pip install flake8 tox
- pip install tox
- sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm
script:
- tox -e $TOX_ENV
- make flakes
notifications:
irc:
channels:
- "chat.freenode.net#isso"
on_success: change
on_failure: always

@ -1,365 +1,9 @@
Changelog for Isso
==================
0.12.3 (UNRELEASED)
-------------------
- New "flags" option in the [markdown] section to customize Misaka's Markdown
HTML rendering. By default, no flags are set.
[markup]
flags = skip-html, escape, hard-wrap
Check docs/configuration/server.rst for more details.
0.12.2 (2019-01-21)
-------------------
- Revert use of labels instead of placeholders, since it breaks
mail notifications. #524
0.12.1 (2019-01-19)
-------------------
- Revert fix for duplicate slashes, as it prevents isso from
starting in some cases. #523
0.12.0 (2019-01-18)
-------------------
- Fix compatibility with new XML API.
- Don't enable admin interface with default password by default. #491
- Add support and documentation for "generic" imports.
- Remove potential duplicate slashes in URLs from
email links. #420
- Add data-isso-reply-notifications to attributes in configuration.
- Use default IP in imports if none is found. Fixes imports of some comments.
- embed: fix feed link creation on older browsers.
- Properly handle to field in mail notifications when using uWSGI spooler
- css: fix vertical alignment of notification checkbox
0.11.1 (2018-11-03)
-------------------
- Include pre-built minified JavaScript and CSS.
0.11.0 (2018-11-03)
-------------------
Bugs & features:
- Fix link in moderation mails if isso is setup on a sub-url (e.g. domain.tld/comments/)
- Add reply notifications
- Add admin interface
- Add links highlighting in comments
- Add apidoc
- Add rc.d script for FreeBSD
- Add the possibility to set CORS Origin through ISSO_CORS_ORIGIN environ variable
- Add preview button
- Add Atom feed at /feed?uri={thread-id}
- Add optionnal gravatar support
- Add nofollow noopener on links inside comments
- Add Dockerfile
- Upgraded to Misaka 2
- Some tests/travis/documentation improvements and fixes + pep8
Translations:
- Fix Chinese translation & typo in CJK
- Add Danish translation
- Add Hungarian translation
- Add Persian translation
- Improvement on german translation
0.10.6 (2016-09-22)
-------------------
- fix missing configuration field
0.10.5 (2016-09-20)
-------------------
- add support for different vote levels, #260
List of vote levels used to customize comment appearance based on score.
Provide a comma-separated values (eg. `"0,5,10,25,100"`) or a JSON array (eg.
`"[-5,5,15]"`).
For example, the value `"-5,5"` will cause each `isso-comment` to be given
one of these 3 classes:
- `isso-vote-level-0` for scores lower than `-5`
- `isso-vote-level-1` for scores between `-5` and `4`
- `isso-vote-level-2` for scores of `5` and greater
These classes can then be used to customize the appearance of comments (eg.
put a star on popular comments).
- add new post preview API endpoint, #254
- add an option for mandatory author, #257
- clients can now use `data-title` to get the HTML title for a new page, #252
- add finish translation and other minor bugfixes
0.10.4 (2016-04-12)
-------------------
- fix wrapper attribute when using data-isso-require-mail="true", #238
- fix reponse for OPTIONS response on Python 3, #242
0.10.3 (2016-02-24)
-------------------
- follow redirects, #193
0.10.2 (2016-02-21)
-------------------
- fix getAttribute return value
0.10.1 (2016-02-06)
-------------------
- fix empty author, email and website values when writing a comment
0.10 (2016-02-06)
-----------------
- add new configuration section for hash handling.
[hash]
salt = Eech7co8Ohloopo9Ol6baimi
algorithm = pbkdf2
You can customize the salt, choose different hash functions and tweak the
parameters for PBKDF2.
- Python 3.4+ validate TLS connections against the system's CA. Previously no
validation was in place, see PEP-446__ for details.
- add `fenced_code` and `no_intra_emphasis` to default configuration.
Fenced code allows to write code without indentation using `~~~` delimiters
(optionally with language identifier).
Intra emphasis would compile `foo_bar_baz` to foo<em>bar</em>baz. This
behavior is very confusing for users not knowing the Markdown spec in detail.
- new configuration to require an email when submitting comments, #199. Set
[guard]
require-email = true
and use `data-isso-require-email="true"` to enable this feature. Disabled by
default.
- new Bulgarian translation by sahwar, new Swedish translation by Gustav
Näslund #143, new Vietnamese translation by Đinh Xuân Sâm, new Croatian
translation by streger, new Czech translation by Jan Chren
- fix SMTP setup without credentials, #174
- version pin Misaka to 1.x, html5lib to 0.9999999
.. __: https://www.python.org/dev/peps/pep-0466/
0.9.10 (2015-04-11)
-------------------
- fix regression in SMTP authentication, #174
0.9.9 (2015-03-04)
0.8.3 (2014-04-09)
------------------
- several Python 3.x related bugfixes
- don't lose comment form if the server rejected the POST request, #144
- add localStorage fallback if QUOTA_EXCEEDED_ERR is thrown (e.g. Safari
private browsing)
- add '--empty-id' flag to Disqus import, because Disqus' export sucks
- (re)gain compatibility with Werkzeug 0.8 and really old html5lib versions
available in Debian Squeeze, #170 & #168
- add User-Agent when Isso requests the URL, an alternate way to #151 (add
'X-Isso' when requesting).
0.9.8 (2014-10-08)
------------------
- add compatibility with configparser==3.5.0b1, #128
0.9.7 (2014-09-25)
------------------
- fix SMTP authentication using CRAM-MD5 (incorrect usage of
`smtplib`), #126
0.9.6 (2014-08-18)
------------------
- remember name, email and website in localStorage, #119
- add option to hide voting feature, #115
data-isso-vote="true|false"
- remove email field from JSON responses
This is a quite serious issue. For the identicon, an expensive hash is used
to avoid the leakage of personal information like a real email address. A
`git blame` reveals, the email has been unintenionally exposed since the very
first release of Isso :-/
The testsuite now contains a dedicated test to prevent this error in the
future.
0.9.5 (2014-08-10)
------------------
- prevent no-break space (&nbsp;) insertion to enable manual line breaks using
two trailing spaces (as per Markdown convention), #112
- limit request size to 256 kb, #107
Previously unlimited or limited by proxy server). 256 kb is a rough
approximation of the next database schema with comments limited to 65535
characters and additional fields.
- add support for logging to file, #103
[general]
log-file =
- show timestamp when hovering <time>, #104
- fix a regression when editing comments with multiple paragraphs introduced
in 0.9.3 which would HTML escape manually inserted linebreaks.
0.9.4 (2014-07-09)
------------------
- fixed a regression when using Isso and Gevent
0.9.3 (2014-07-09)
------------------
- remove scrollIntoView while expanding further comments if a fragment is used
(e.g. #isso-thread brought you back to the top, unexpectedly)
- implement a custom Markdown renderer to support multi-line code listings. The
extension "fenced_code" is now enabled by default and generates HTML
compatible with Highlight.js__.
- escape HTML entities when editing a comment with raw HTML
- fix CSS for input
- remove isso.css from binary distribution to avoid confusion (it's still there
from the very first release, but modifications do not work)
.. __: http://highlightjs.org/
0.9 (2014-05-29)
----------------
- comment pagination by Srijan Choudhary, #15
Isso can now limit the amount of comments shown by default and add link to
show more. By default, all top-level comments are shown but only 5 nested
comments (per reply). You can override the settings:
isso-data-max-comments-top="N"
isso-data-max-comments-nested="N"
Where N is a number from 0 to infinity ("inf"). If you limit the amount of
shown top level comments, the overall comment count may be incorrect and a
known issue.
You can also configure the amount of comments shown per click (5 by default):
isso-data-reveal-on-click="N"
This feature also required a change in the comment structure. Previously, all
comments are stored tree-like but shown linearly. To ease the implementation
of pagination, the comment tree is now limited to a maximum depth of one.
Jeff Atwood explains, why `discussions are flat by design`__.
.. __: http://blog.codinghorror.com/web-discussions-flat-by-design/
When you upgrade, Isso will automatically normalize the tree and some
information gets lost. All new replies to a comment are now automatically a
direct child of the top-level comment.
- style improvements by William Dorffer, #39, #84 #90 and #91
Isso now longer uses a fat SCSS library, but plain CSS instead. The design is
now responsive and no longer sets global CSS rules.
- experimental WordPress import, #75
Isso should be able to import WXR 1.0-1.2 exports. The import code is based
on two WXR dumps I found (and created) and may not work for you. Please
report any failure.
- avatar changes, #49
You can now configure the client to not show avatars:
data-isso-avatar="false"
Also there is no longer an avatar shown next to the comment box. This is due
to the new CSS and removes two runtime dependencies.
- you may now set a full From header, #87
[smtp]
from = Foo Bar <spam@local>
- SMTP (all caps) is now recognized for notifications, #95
- Isso now ships a small demo site at /demo, #44
- a few bugfixes: Disqus import now anonymizes IP addresses, uWSGI spooling for
Python 3, HTTP-Referer fallback for HTTP-Origin
- remove Django's PBKDF2 implementation in favour of the PBKDF2 function
available in werkzeug 0.9 or higher. If you're still using werkzeug 0.8, Isso
imports passlib__ as fallback (if available).
This release also features a new templating engine Jade__ which replaces
Markup.js__. Jade can compile directly to JavaScript with a tiny runtime module
on the client. Along with the removal of sha1.js and pbkdf2.js and a few build
optimizations, the JS client now weighs only 40kb (12kb gzipped) 52kb resp.
17kb before.
.. __: https://pypi.python.org/pypi/passlib
.. __: http://jade-lang.com/
.. __: https://github.com/adammark/Markup.js
0.8 (2014-03-28)
----------------
- replace ``<textarea>`` with ``<div contentedtiable="true">`` to remove the
sluggish auto-resize on input feature. If you use a custom CSS, replace
``textarea`` with ``.textarea`` and also set ``white-space: pre``.

@ -4,11 +4,6 @@
* Martin Zimmermann <info@posativ.org>
## Co-Maintainers
* Benoît Latinier @blatinier <benoit@latinier.fr>
* Jelmer Vernooij <jelmer@jelmer.uk>
## Contributors
@ -36,86 +31,7 @@ In chronological order:
* Srijan Choudhary <srijan4@gmail.com>
* Correct nginx examples in documentation
* Added comment pagination feature
* Provide a demo site
* William Dorffer <schoewilliam@gmail.com>
* Convert SCSS to plain CSS
* Refresh style, make it responsive
* Baptiste Darthenay
* Esperanto translation
* Matías Ducasa <https://github.com/matiasducasa>
* Spanish translation
* Daniel Gräber <https://github.com/albohlabs>
* Added ansible for provisioning
* Nick Hu <https://github.com/NickHu>
* Added configuration to require email addresses (no validation)
* Fix Vagrantfile
* Benoît Latinier @blatinier <benoit@latinier.fr>
* Fix thread discovery
* Added mandatory author
* Added admin interface
* Ivan Pantic <ivanpantic82@gmail.com>
* Added vote levels
* Martin Schenck @schemar
* Improvement in the german translation
* @cclauss
* Pep8 and drop of legacy supports (old python & debian version tested in travis)
* Make travis use pyflakes
* Lucas Cimon @Lucas-C
* Added the possibility to define CORS origins through ISSO_CORS_ORIGIN environment variable
* Fix a bug with <a> in <svg>
* Fixing likes counter of replies not being displayed
* Adding contrib/dump_comments.py
* Adding a [server] proxy-fix-enable-x-prefix configuration option
* Using .access_route instead of .remote_addr to take into account HTTP_X_FORWARDED_FOR header
* Yuchen Pei @ycpei
* Fix link in moderation emails when isso is installed in a sub URL
* @Rocket1184
* Fix typo in CJK translations
* @vincentbernat
* Added documentation about data-isso-id attribute (overriding the standard isso-thread-id)
* Added multi-staged Dockerfile
* Added atom feed
* Added a nofollow/noopener on links inside comments to protect against bots
* Added a preview using the existing preview endpoint
* @p-vitt & @M4a1x
* Documentation on troubleshooting for uberspace users
* Facundo Batista <facundo@taniquetil.com.ar>
* Added a generic way to migrate from a json file
* @benjhess
* Optionnal gravatar support
* Steffen Prince @sprin
* Upgrade to Misaka 2
* Rocka <i@rocka.me>
* Implementation and documentation about async comment loading
* Pelle Nilsson @pellenilsson
* Reply notifications
* Craig P Hicks @craigphicks
* Fix sub urls configurations on admin interface
* Chris Warrick @Kwpolska
* Update Polish translation
* Redirect to comment after moderation
* [Your name or handle] <[email or website]>
* [Brief summary of your changes]

@ -1,34 +0,0 @@
# First, compile JS stuff
FROM node:dubnium-buster
WORKDIR /src/
COPY . .
RUN npm install -g requirejs uglify-js jade bower \
&& make init js
# Second, create virtualenv
FROM python:3.8-buster
WORKDIR /src/
COPY --from=0 /src .
RUN python3 -m venv /isso \
&& . /isso/bin/activate \
&& pip3 install --no-cache-dir --upgrade pip \
&& pip3 install --no-cache-dir gunicorn cffi flask \
&& python setup.py install \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Third, create final repository
FROM python:3.8-slim-buster
WORKDIR /isso/
COPY --from=1 /isso .
# Configuration
VOLUME /db /config
EXPOSE 8080
ENV ISSO_SETTINGS /config/isso.cfg
CMD ["/isso/bin/gunicorn", "-b", "0.0.0.0:8080", "-w", "4", "--preload", "isso.run", "--worker-tmp-dir", "/dev/shm"]
# Example of use:
#
# docker build -t isso .
# docker run -it --rm -v /opt/isso:/config -v /opt/isso:/db -v $PWD:$PWD isso /isso/bin/isso -c \$ISSO_SETTINGS import disqus.xml
# docker run -d --rm --name isso -p 8080:8080 -v /opt/isso:/config -v /opt/isso:/db isso

@ -7,13 +7,5 @@ include isso/js/embed.min.js
include isso/js/embed.dev.js
include isso/js/count.min.js
include isso/js/count.dev.js
include isso/js/admin.js
include isso/defaults.ini
include isso/templates/admin.html
include isso/templates/disabled.html
include isso/templates/login.html
include isso/css/admin.css
include isso/css/isso.css
include isso/img/isso.svg

@ -1,75 +1,54 @@
# INSTALLATION: pip install sphinx && npm install --global node-sass
ISSO_JS_SRC := $(shell find isso/js/app -type f) $(shell ls isso/js/*.js | grep -vE "(min|dev)")
ISSO_JS_DST := isso/js/embed.min.js isso/js/embed.dev.js isso/js/count.min.js isso/js/count.dev.js
ISSO_JS_SRC := $(shell find isso/js/app -type f) \
$(shell ls isso/js/*.js | grep -vE "(min|dev)") \
isso/js/lib/requirejs-jade/jade.js
ISSO_CSS_DST := isso/css/isso.css
ISSO_CSS_SRC := isso/css/isso.scss
ISSO_CSS_SRC_DEPS := $(shell find isso/css -type f | grep .scss)
ISSO_JS_DST := isso/js/embed.min.js isso/js/embed.dev.js \
isso/js/count.min.js isso/js/count.dev.js
ISSO_PY_SRC := $(shell git ls-files | grep .py)
ISSO_CSS := isso/css/isso.css
RST := $(shell find docs/ -type f -name '*.rst')
MAN := man/man1/isso.1 man/man5/isso.conf.5
ISSO_PY_SRC := $(shell git ls-files | grep -E "^isso/.+.py$$")
DOCS_RST_SRC := $(shell find docs/ -type f -name '*.rst') \
$(wildcard docs/_isso/*) \
docs/index.html docs/conf.py docs/docutils.conf \
share/isso.conf
DOCS_CSS_SRC := docs/_static/css/site.scss
DOCS_CSS_DEP := $(shell find docs/_static/css/neat -type f) \
$(shell find docs/_static/css/bourbon -type f)
DOCS_CSS_DST := docs/_static/css/site.css
DOCS_MAN_DST := man/man1/isso.1 man/man5/isso.conf.5
DOCS_HTML_DST := docs/_build/html
RJS = r.js
SASS = node-sass
WWW := docs/index.html share/isso.conf $(wildcard docs/_static/*)
CSS := docs/_static/css/site.css
all: man js site
init:
(cd isso/js; bower --allow-root install almond requirejs requirejs-text jade)
(cd isso/js; bower install almond requirejs requirejs-text)
flakes:
flake8 . --count --max-line-length=127 --show-source --statistics
${ISSO_CSS_DST}: $(ISSO_CSS_SRC_DEPS)
scss --no-cache $(ISSO_CSS_SRC) $@
isso/js/%.min.js: $(ISSO_JS_SRC) $(ISSO_CSS)
$(RJS) -o isso/js/build.$*.js out=$@
isso/js/%.min.js: $(ISSO_JS_SRC) $(ISSO_CSS_DST)
r.js -o isso/js/build.$*.js out=$@
isso/js/%.dev.js: $(ISSO_JS_SRC) $(ISSO_CSS)
$(RJS) -o isso/js/build.$*.js optimize="none" out=$@
isso/js/%.dev.js: $(ISSO_JS_SRC) $(ISSO_CSS_DST)
r.js -o isso/js/build.$*.js optimize="none" out=$@
js: $(ISSO_JS_DST)
css: $(ISSO_CSS_DST)
man: $(DOCS_RST_SRC)
man: $(RST)
sphinx-build -b man docs/ man/
mkdir -p man/man1/ man/man5
mv man/isso.1 man/man1/isso.1
mv man/isso.conf.5 man/man5/isso.conf.5
${DOCS_CSS_DST}: $(DOCS_CSS_SRC) $(DOCS_CSS_DEP)
$(SASS) --no-cache $(DOCS_CSS_SRC) $@
${DOCS_HTML_DST}: $(DOCS_RST_SRC) $(DOCS_CSS_DST)
sphinx-build -b dirhtml docs/ $@
${CSS}: docs/_static/css/site.scss
scss --no-cache $< $@
site: $(DOCS_HTML_DST)
site: $(RST) $(WWW) $(CSS)
cd docs && sphinx-build -b dirhtml . _build/html
coverage: $(ISSO_PY_SRC)
nosetests --with-doctest --with-coverage --cover-package=isso --cover-html isso/
test: $($ISSO_PY_SRC)
python3 setup.py nosetests
python setup.py nosetests
clean:
rm -f $(DOCS_MAN_DST) $(DOCS_CSS_DST) $(ISSO_JS_DST)
rm -rf $(DOCS_HTML_DST)
rm -f $(MAN) $(CSS) $(ISSO_JS_DST) $(ISSO_CSS_DST)
.PHONY: clean site man init js coverage test
.PHONY: clean site man init js css coverage test

@ -1,12 +1,39 @@
[![Build Status](https://drone.nixaid.com/api/badges/arno/isso/status.svg)](https://drone.nixaid.com/arno/isso)
Isso Ich schrei sonst
=======================
Isso a commenting server similar to Disqus
============================================
[![Build Status](https://travis-ci.org/posativ/isso.png?branch=master)](https://travis-ci.org/posativ/isso)
Isso *Ich schrei sonst* is a lightweight commenting server written in
Python and JavaScript. It aims to be a drop-in replacement for
[Disqus](http://disqus.com).
You love static blog generators (especially [Acrylamid][1] *cough*) and the
only option to interact with your community is [Disqus][2]. There's nothing
wrong with it, but if you care about the privacy of your audience you are
better off with a comment system that is under your control. This is, where
Isso comes into play.
![Isso in Action](http://posativ.org/~tmp/isso-sample.png)
[1]: https://github.com/posativ/acrylamid
[2]: https://disqus.com/
See [posativ.org/isso](http://posativ.org/isso/) for more details.
**[Try Yourself!](http://posativ.org/isso/)**
Features
--------
* [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) comments written in Markdown
* SQLite backend, Disqus import
* client-side JS (currently 54kb minified, 18kb gzipped)
* I18N, available in english, french, russian and german
Setup
-----
Please refer to the official documentation: <http://posativ.org/isso/docs>.
Alternatives
------------
- [talkatv](https://github.com/talkatv/talkatv) Python
- [Juvia](https://github.com/phusion/juvia) Ruby on Rails
- [Tildehash.com](http://www.tildehash.com/?article=why-im-reinventing-disqus) PHP
- [SO: Unobtrusive, self-hosted comments](http://stackoverflow.com/q/2053217)

65
Vagrantfile vendored

@ -1,65 +0,0 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# This is the Vagrant config file for setting up an environment for Isso development.
# It requires:
#
# * Vagrant (https://vagrantup.com)
# * A VM engine, like VirtualBox (https://virtualbox.org)
# * The Vagrant-Hostmanager plugin (https://github.com/smdahlen/vagrant-hostmanager)
# * Ansible (https://www.ansible.com)
#
# With them installed, cd into this directory and do 'vagrant up'. It's possible Vagrant will
# ask for your root password so it can update your /etc/hosts file. Once it's happily churning out
# console output, go get a coffee :)
#
# The resulting VM should be accessible at http://isso-dev.local/ so you can try the demo page out.
# Edit files in your checkout as usual. If you need to look at log files and stuff, 'vagrant ssh'
# to get into the VM. Useful info about it:
#
# * Running Ubuntu 14.04
# * Isso is running on uWSGI
# * Actual webserver is Nginx to talk to uWSGI over a unix socket
# * uWSGI log file is /var/log/uwsgi/apps/isso.log
# * Isso DB file is /var/isso/comments.db
# * Isso log file is /var/log/isso.log
#
# When the VM is getting rebooted vagrant mounts the shared folder after uWSGI is getting startet. To fix this issue for
# the moment you need to 'vagrant ssh' into the VM and execute 'sudo service uwsgi restart'.
#
# For debugging with _pudb_ stop uWSGI service and start it manually
# 'sudo uwsgi --ini /etc/uwsgi/apps-available/isso.ini'.
#
# Enjoy!
Vagrant.configure(2) do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
config.vm.box = "ubuntu/trusty32"
config.vm.hostname = 'isso-dev.local'
config.vm.network "private_network", type: "dhcp"
config.hostmanager.enabled = true
config.hostmanager.manage_host = true
config.hostmanager.ignore_private_ip = false
config.hostmanager.include_offline = true
config.hostmanager.ip_resolver = proc do |machine|
result = ""
machine.communicate.execute("ifconfig eth1") do |type, data|
result << data if type == :stdout
end
(ip = /inet addr:(\d+\.\d+\.\d+\.\d)/.match(result)) && ip[1]
end
config.vm.provision "ansible" do |ansible|
ansible.playbook = "ansible/site.yml"
ansible.limit = "all"
ansible.verbose = "v"
end
config.vm.post_up_message = "Browse to http://isso-dev.local/demo/index.html to start."
end

@ -1,39 +0,0 @@
user root;
worker_processes 4;
worker_rlimit_nofile 8192;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
events {
worker_connections 2014;
multi_accept on;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
log_format timed '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time $upstream_addr '
' $upstream_status $upstream_cache_status $pipe';
access_log /var/log/nginx/access.log timed;
sendfile on;
tcp_nopush on;
keepalive_timeout 30;
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

@ -1,13 +0,0 @@
server {
client_max_body_size 20M;
listen 80 default_server;
server_name isso-dev.local;
root /vagrant/isso/demo;
location / {
# uwsgi_pass unix:///run/uwsgi/app/isso/socket;
uwsgi_pass 127.0.0.1:8080;
include uwsgi_params;
}
}

@ -1,24 +0,0 @@
[uwsgi]
plugins = python
chdir = /vagrant
uid = root
gid = root
socket = :8080
master = true
processes = 4
cache2 = name=hash,items=10240,blocksize=32
spooler = /var/isso/spool
module = isso.run
env = ISSO_SETTINGS=/vagrant/share/isso-dev.conf
env = PYTHON_EGG_CACHE=/tmp
# uncomment for debugging
# daemonize = /var/log/uwsgi/uwsgi.log
py-autoreload = 1
# prevent uWSGI from remapping stdin to /dev/null
honour-stdin = true

@ -1,85 +0,0 @@
---
- name: Provision development server
hosts: all
sudo: true
tasks:
- name: Apt | Add nodesource keys
apt_key: url=https://deb.nodesource.com/gpgkey/nodesource.gpg.key state=present
- name: Apt | Add nodesource sources list deb
apt_repository: repo='deb https://deb.nodesource.com/node {{ ansible_distribution_release }} main' state=present
- name: Apt | Add nodesource sources list deb src
apt_repository: repo='deb-src https://deb.nodesource.com/node {{ ansible_distribution_release }} main' state=present
- name: Apt | Install packages
apt: pkg={{ item }} state=latest update_cache=true
with_items:
- build-essential
- curl
- htop
- vim
- git
- python-dev
- python-software-properties
- python-setuptools
- python-pip
- nginx
- uwsgi
- uwsgi-plugin-python
- supervisor
- sqlite3
- nodejs
- libffi-dev
- name: NPM | Install packages
npm: name={{ item }} global=yes
with_items:
- bower
- requirejs
- uglify-js
- jade
- name: Python | Install egg
shell: cd /vagrant; python setup.py develop
- name: Make
shell: cd /vagrant; {{ item }}
with_items:
- make init
- make js
- name: Spool | Create directory
file: path=/var/isso/spool state=directory mode=0777
- name: uwsgi | Deploy configuration
copy: src=files/uwsgi.ini dest=/etc/uwsgi/apps-available/isso.ini
- name: uwsgi | Enable app
file: src=/etc/uwsgi/apps-available/isso.ini dest=/etc/uwsgi/apps-enabled/isso.ini state=link
- name: uwsgi | Restart service daemon
service: name=uwsgi state=restarted enabled=yes
- name: uwsgi | Chmod logfile
file: path=/var/log/uwsgi/uwsgi.log state=touch mode="a+r"
- name: nginx | Deploy nginx.conf
copy: src=files/nginx.conf dest=/etc/nginx/nginx.conf
- name: nginx | Delete default vhost
action: file path=/etc/nginx/sites-enabled/default state=absent
- name: nginx | Deploy vhost config
copy: src=files/nginx.vhost.conf dest=/etc/nginx/sites-available/isso.conf
- name: nginx | Enable vhost
file: src=/etc/nginx/sites-available/isso.conf dest=/etc/nginx/sites-enabled/000-isso state=link
- name: nginx | Chmod logfile
file: path=/var/log/nginx mode="a+rx" state=directory recurse=true
- name: nginx | Restart service daemon
service: name=nginx state=restarted enabled=yes

@ -1,11 +0,0 @@
{
"name": "isso",
"description": "a Disqus alternative",
"title": "isso API",
"order": ["Thread", "Comment"],
"template": {
"withCompare": false
}
}

@ -16,7 +16,7 @@ set user (cat ~/.transifexrc | grep -E "^username" | awk -F " ?= ?" '{ print $2
set pass (cat ~/.transifexrc | grep -E "^password" | awk -F " ?= ?" '{ print $2 }')
set lang (echo $argv | cut -d / -f 5 | cut -d . -f 1)
set trans (mktemp -t tx.XXX)
set trans (mktemp -t tx)
if [ $lang = "en" ]
set url "$TRANSIFEX/project/isso/resource/js/content/"

@ -1,128 +0,0 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2020 Lucas Cimon.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""Dump isso comments as text
The script can be run like this:
contrib/dump_comments.py .../path/to/isso.db --sort-by-last-reply
To get a list of all available options:
contrib/dump_comments.py --help
By installing the optional colorama dependency, you'll get a colored output.
An example of output can be found at https://github.com/posativ/isso/issues/634
"""
import argparse
import sqlite3
from collections import defaultdict, namedtuple
from datetime import date
from textwrap import indent
class ColorFallback():
__getattr__ = lambda self, name: '' # noqa: E731
try:
from colorama import Fore, Style, init
init() # needed for Windows
except ImportError: # fallback so that the imported classes always exist
Fore = Style = ColorFallback()
Comment = namedtuple('Comment', ('uri', 'id', 'parent', 'created', 'text', 'author', 'email', 'website', 'likes', 'dislikes', 'replies'))
INDENT = ' '
QUERY = 'SELECT uri, comments.id, parent, created, text, author, email, website, likes, dislikes FROM comments INNER JOIN threads on comments.tid = threads.id'
def main():
args = parse_args()
if not args.colors:
global Fore, Style
Fore = Style = ColorFallback()
db = sqlite3.connect(args.db_path)
comments_per_uri = defaultdict(list)
for result in db.execute(QUERY).fetchall():
comment = Comment(*result, replies=[])
comments_per_uri[comment.uri].append(comment)
root_comments_per_sort_date = {}
for comments in comments_per_uri.values():
comments_per_id = {comment.id: comment for comment in comments}
root_comments, sort_date = [], None
for comment in comments:
if comment.parent: # == this is a "reply" comment
comments_per_id[comment.parent].replies.append(comment)
if args.sort_by_last_reply and (sort_date is None or comment.created > sort_date):
sort_date = comment.created
else:
root_comments.append(comment)
if sort_date is None or comment.created > sort_date:
sort_date = comment.created
root_comments_per_sort_date[sort_date] = root_comments
for _, root_comments in sorted(root_comments_per_sort_date.items(), key=lambda pair: pair[0]):
print(Fore.MAGENTA + args.url_prefix + root_comments[0].uri + Fore.RESET)
for comment in root_comments:
print_comment(INDENT, comment)
for comment in comment.replies:
print_comment(INDENT * 2, comment)
print()
def print_comment(prefix, comment):
author = comment.author or 'Anonymous'
email = comment.email or ''
website = comment.website or ''
when = date.fromtimestamp(comment.created)
popularity = ''
if comment.likes:
popularity = '+{.likes}'.format(comment)
if comment.dislikes:
if popularity:
popularity += '/'
popularity = '-{.dislikes}'.format(comment)
print(prefix + '{Style.BRIGHT}{author}{Style.RESET_ALL} {Style.DIM}- {email} {website}{Style.RESET_ALL} {when} {Style.DIM}{popularity}{Style.RESET_ALL}'.format(Style=Style, **locals()))
print(indent(comment.text, prefix))
def parse_args():
parser = argparse.ArgumentParser(description='Dump all Isso comments in chronological order, grouped by replies',
formatter_class=ArgparseHelpFormatter)
parser.add_argument('db_path', help='File path to Isso Sqlite DB')
parser.add_argument('--sort-by-last-reply', action='store_true', help='By default comments are sorted by "parent" comment date, this sort comments based on the last replies')
parser.add_argument('--url-prefix', default='', help='Optional domain name to prefix to pages URLs')
parser.add_argument('--no-colors', action='store_false', dest='colors', default=True, help='Disabled colored output')
return parser.parse_args()
class ArgparseHelpFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter):
pass
if __name__ == '__main__':
main()

@ -1,123 +0,0 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""Comment importer from Blogger
This python script can convert comments posted to a Blogger-powered blog to a
JSON file with can then be imported into Isso (by following the procedure
explained in docs/docs/extras/advanced-migration.rst.
The script can be run like this:
python import_blogger.py -p 'http://myblog.com/' blogger.xml out.json
where `blogger.xml` is a dump of the blog produced by the Blogger platform, and
the URL following the `-p` option is a prefix that will be applied to all post
URLs: the original host will be stripped and the path will be appended to the
string you specify here (this can be useful in the case that your blog moved to
a different domain, subdomain, or just into a new directory).
The `out.json` file is the file which will be generated by this tool, and which
can then be fed into isso:
isso -c /path/to/isso.cfg import -t generic out.json
"""
from __future__ import unicode_literals
import json
import feedparser
import time
from urllib.parse import urlparse
class Post:
def __init__(self, url):
self.url = url
self.title = None
self.comments = []
def add_comment(self, comment):
comment['id'] = len(self.comments) + 1
self.comments.append(comment)
def encode_post(post):
ret = {}
ret['id'] = post.url
ret['title'] = post.title
ret['comments'] = post.comments
return ret
class ImportBlogger:
TYPE_COMMENT = 'http://schemas.google.com/blogger/2008/kind#comment'
TYPE_POST = 'http://schemas.google.com/blogger/2008/kind#post'
def __init__(self, filename_in, filename_out, prefix):
self.channel = feedparser.parse(filename_in)
self.filename_out = filename_out
self.prefix = prefix
def run(self):
self.posts = {}
for item in self.channel.entries:
terms = [tag.term for tag in item.tags]
if not terms:
continue
if terms[0] == self.TYPE_COMMENT:
post = self.ensure_post(item)
post.add_comment(self.process_comment(item))
elif terms[0] == self.TYPE_POST:
self.process_post(item)
data = [encode_post(p) for p in self.posts.values() if p.comments]
with open(self.filename_out, 'w') as fp:
json.dump(data, fp, indent=2)
def process_post(self, item):
pid = self.post_id(item)
if pid in self.posts:
post = self.posts[pid]
else:
post = Post(pid)
self.posts[pid] = post
post.title = item.title
def ensure_post(self, item):
pid = self.post_id(item)
post = self.posts.get(pid, None)
if not post:
post = Post(pid)
self.posts[pid] = post
return post
def process_comment(self, item):
comment = {}
comment['author'] = item.author_detail.name
comment['email'] = item.author_detail.email
comment['website'] = item.author_detail.get('href', '')
t = time.strftime('%Y-%m-%d %H:%M:%S', item.published_parsed)
comment['created'] = t
comment['text'] = item.content[0].value
comment['remote_addr'] = '127.0.0.1'
return comment
def post_id(self, item):
u = urlparse(item.link)
return self.prefix + u.path
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(
description='Convert comments from blogger.com')
parser.add_argument('input', help='input file')
parser.add_argument('output', help='output file')
parser.add_argument('-p', dest='prefix',
help='prefix to be added to paths (ID)',
type=str, default='')
args = parser.parse_args()
importer = ImportBlogger(args.input, args.output, args.prefix)
importer.run()

@ -10,8 +10,7 @@
VERSION: '{{ release|e }}',
COLLAPSE_INDEX: false,
FILE_SUFFIX: '{{ '' if no_search_suffix else file_suffix }}',
HAS_SOURCE: {{ has_source|lower }},
SOURCELINK_SUFFIX: '{{ sourcelink_suffix }}'
HAS_SOURCE: {{ has_source|lower }}
};
</script>
{%- for scriptfile in script_files %}

@ -9,3 +9,4 @@ class IssoTranslator(HTMLTranslator):
if self.section_level == 1:
raise nodes.SkipNode
HTMLTranslator.visit_title(self, node)

@ -28,5 +28,4 @@
{{ doc("docs/extras/deployment", "Deployment") }}
{{ doc("docs/extras/advanced-integration", "Advanced Integration") }}
{{ doc("docs/extras/api", "API") }}
{{ doc("docs/extras/contribs", "Community tools") }}
</ul>

@ -209,21 +209,6 @@ main {
h4 {
margin-bottom: 0.5em;
}
blockquote {
margin-top: 10px;
margin-bottom: 10px;
padding-left: 15px;
border-left: 3px solid #ccc;
}
pre {
background: #eee;
border: 1px solid #ddd;
padding: 10px 15px;
color: #4d4d4c;
overflow: auto;
}
}
.sidebar {

@ -12,27 +12,15 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import io
import re
import pkg_resources
dist = pkg_resources.get_distribution("isso")
import sys
import os
from os.path import join, dirname
sys.path.insert(0, join(dirname(__file__), "_isso/"))
try:
dist = pkg_resources.get_distribution("isso")
except pkg_resources.DistributionNotFound:
dist = type("I'm a Version", (object, ), {})
with io.open(join(dirname(__file__), "../setup.py")) as fp:
for line in fp:
m = re.match("\\s*version='([^']+)'\\s*", line)
if m:
dist.version = m.group(1)
break
else:
dist.version = ""
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
@ -64,7 +52,7 @@ master_doc = 'docs/index'
# General information about the project.
project = u'Isso'
copyright = u'2016, Martin Zimmermann'
copyright = u'2013, Martin Zimmermann'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@ -200,22 +188,22 @@ htmlhelp_basename = 'Issodoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'Isso.tex', u'Isso Documentation',
u'Martin Zimmermann', 'manual'),
('index', 'Isso.tex', u'Isso Documentation',
u'Martin Zimmermann', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@ -260,9 +248,9 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'Isso', u'Isso Documentation',
u'Martin Zimmermann', 'Isso', 'a commenting server similar to Disqus',
'Miscellaneous'),
('index', 'Isso', u'Isso Documentation',
u'Martin Zimmermann', 'Isso', 'a commenting server similar to Disqus',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.

@ -59,3 +59,14 @@ definitely need help:
- delete or activate comments matching a filter (e.g. name, email, ip address)
- close threads and remove threads completely
- edit comments
* `Pagination <https://github.com/posativ/isso/issues/14>`_ while Isso is
generally a lot faster than Disqus, after approx. 50 comments you will
notice roughly 1 second rendering time. It would be nice if the client
fetches only N comments and continues when the user scrolls down (or click
on a button to fetch more).
* CSS improvements. For some websites, the Isso integration just looks ugly.
If you can improve it, please do it :)

@ -7,39 +7,19 @@ preferably in the script tag which embeds the JS:
.. code-block:: html
<script data-isso="/prefix/"
data-isso-id="thread-id"
data-isso-css="true"
data-isso-lang="ru"
data-isso-reply-to-self="false"
data-isso-require-author="false"
data-isso-require-email="false"
data-isso-reply-notifications="false"
data-isso-max-comments-top="10"
data-isso-max-comments-nested="5"
data-isso-reveal-on-click="5"
data-isso-avatar="true"
data-isso-avatar-bg="#f0f0f0"
data-isso-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..."
data-isso-vote="true"
data-isso-vote-levels=""
data-isso-feed="false"
data-avatar-bg="#f0f0f0"
data-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..."
src="/prefix/js/embed.js"></script>
Furthermore you can override the automatic title detection inside
the embed tag, as well as the thread ID, e.g.:
the embed tag, e.g.:
.. code-block:: html
<section id="isso-thread" data-title="Foo!" data-isso-id="/path/to/resource"></section>
Additionally, you can override any translation string for any language by adding
a ``data-isso-`` attribute that is equal to the translation key (found `here`__) with
``-text-[lang]`` appended to it. So, for example, if you want to override the
english translation of the ``postbox-notification`` message, you could add:
``data-isso-postbox-notification-text-en="Select to be notified of replies to your comment"``
.. __: https://github.com/posativ/isso/blob/master/isso/js/app/i18n/en.js
<section id="isso-thread" data-title="Foo!"></section>
data-isso
---------
@ -65,51 +45,14 @@ Defaults to `true`.
data-isso-lang
--------------
Override useragent's preferred language. Isso has been translated in over 12
languages. The language is configured by its `ISO 639-1
<https://en.wikipedia.org/wiki/ISO_639-1>`_ (two letter) code.
You find a list of all supported languages on `GitHub
<https://github.com/posativ/isso/tree/master/isso/js/app/i18n>`_.
Override useragent's preferred language. Currently available: german (de),
english (en), french (fr), italian (it) and russian (ru).
data-isso-reply-to-self
-----------------------
Set to `true` when spam guard is configured with `reply-to-self = true`.
data-isso-require-author
------------------------
Set to `true` when spam guard is configured with `require-author = true`.
data-isso-require-email
-----------------------
Set to `true` when spam guard is configured with `require-email = true`.
data-isso-reply-notifications
-----------------------------
Set to `true` when reply notifications is configured with `reply-notifications = true`.
data-isso-max-comments-top and data-isso-max-comments-nested
------------------------------------------------------------
Number of top level (or nested) comments to show by default. If some
comments are not shown, an "X Hidden" link is shown.
Set to `"inf"` to show all, or `"0"` to hide all.
data-isso-reveal-on-click
-------------------------
Number of comments to reveal on clicking the "X Hidden" link.
data-isso-avatar
----------------
Enable or disable avatar generation.
data-isso-avatar-bg
-------------------
@ -123,36 +66,12 @@ scheme is based in `this color palette <http://colrd.com/palette/19308/>`_.
Multiple colors must be separated by space. If you use less than eight colors
and not a multiple of 2, the color distribution is not even.
data-isso-gravatar
------------------
Uses gravatar images instead of generating svg images. You have to set
"data-isso-avatar" to **false** when you want to use this. Otherwise
both the gravatar and avatar svg image will show up. Please also set
option "gravatar" to **true** in the server configuration...
data-isso-vote
--------------
Enable or disable voting feature on the client side.
data-isso-vote-levels
---------------------
List of vote levels used to customize comment appearance based on score.
Provide a comma-separated values (eg. `"0,5,10,25,100"`) or a JSON array (eg. `"[-5,5,15]"`).
For example, the value `"-5,5"` will cause each `isso-comment` to be given one of these 3 classes:
data-isso-id
------------
- `isso-vote-level-0` for scores lower than `-5`
- `isso-vote-level-1` for scores between `-5` and `4`
- `isso-vote-level-2` for scores of `5` and greater
Set a custom thread id, defaults to current URI. If you a comment counter, add
this attribute to the link tag, too.
These classes can then be used to customize the appearance of comments (eg. put a star on popular comments)
data-isso-feed
--------------
.. code-block:: html
Enable or disable the addition of a link to the feed for the comment
thread. The link will only be valid if the appropriate setting, in
``[rss]`` section, is also enabled server-side.
<section data-isso-id="test.abc" id="isso-thread"></section>

@ -0,0 +1 @@
../../../share/isso.conf

@ -3,8 +3,8 @@ Server Configuration
The Isso configuration file is an `INI-style`__ textfile. It reads integers,
booleans, strings and lists. Here's the default isso configuration:
`isso.conf <https://github.com/posativ/isso/blob/master/share/isso.conf>`. A
basic configuration from scratch looks like this:
:download:`isso.conf <isso.conf>`. A basic configuration from
scratch looks like this:
.. code-block:: ini
@ -43,8 +43,7 @@ session key and hostname. Here are the default values for this section:
name =
host =
max-age = 15m
notify = stdout
log-file =
notify =
dbpath
file location to the SQLite3 database, highly recommended to change this
@ -55,7 +54,7 @@ name
not used otherwise.
host
Your website(s). If Isso is unable to connect to at least one site, you'll
Your website(s). If Isso is unable to connect to at least on site, you'll
get a warning during startup and comments are most likely non-functional.
You'll need at least one host/website to run Isso. This is due to security
@ -77,43 +76,8 @@ max-age
:ref:`Appendum: Timedelta <appendum-timedelta>` for valid values.
notify
Select notification backend(s) for new comments, separated by comma.
Available backends:
stdout
Log to standard output. Default, if none selected. Note, this
functionality is broken since a few releases.
smtp
Send notifications via SMTP on new comments with activation (if
moderated) and deletion links.
reply-notifications
Allow users to request E-mail notifications for replies to their post.
It is highly recommended to also turn on moderation when enabling this
setting, as Isso can otherwise be easily exploited for sending spam.
Do not forget to configure the client accordingly.
log-file
Log console messages to file instead of standard out.
gravatar
When set to ``true`` this will add the property "gravatar_image"
containing the link to a gravatar image to every comment. If a comment
does not contain an email address, gravatar will render a random icon.
This is only true when using the default value for "gravatar-url"
which contains the query string param ``d=identicon`` ...
gravatar-url
Url for gravatar images. The "{}" is where the email hash will be placed.
Defaults to "https://www.gravatar.com/avatar/{}?d=identicon"
latest-enabled
If True it will enable the ``/latest`` endpoint. Optional, defaults
to False.
Select notification backend for new comments. Currently, only SMTP
is available.
.. _CORS: https://developer.mozilla.org/en/docs/HTTP/Access_control_CORS
@ -128,21 +92,13 @@ Enable moderation queue and handling of comments still in moderation queue
[moderation]
enabled = false
approve-if-email-previously-approved = false
purge-after = 30d
enabled
enable comment moderation queue. This option only affects new comments.
Comments in moderation queue are not visible to other users until you
Comments in modertion queue are not visible to other users until you
activate them.
approve-if-email-previously-approved
automatically approve comments by an email address if that address has
had a comment approved within the last 6 months. No ownership verification
is done on the entered email address. This means that if someone is able
to guess correctly the email address used by a previously approved author,
they will be able to have their new comment auto-approved.
purge-after
remove unprocessed comments in moderation queue after given time.
@ -167,7 +123,7 @@ listen
; UNIX domain socket
listen = unix:///tmp/isso.sock
; TCP/IP
listen = http://localhost:1234/
listen = http:///localhost:1234/
When ``gevent`` is available, it is automatically used for `http://`
Currently, gevent can not handle http requests on unix domain socket
@ -176,12 +132,6 @@ listen
Does not apply for `uWSGI`.
public-endpoint
public URL that Isso is accessed from by end users. Should always be
a http:// or https:// absolute address. If left blank, automatic
detection is attempted. Normally only needs to be specified if
different than the `listen` setting.
reload
reload application, when the source code has changed. Useful for
development. Only works with the internal webserver.
@ -190,13 +140,6 @@ profile
show 10 most time consuming function in Isso after each request. Do
not use in production.
trusted-proxies
an optional list of reverse proxies IPs behind which you have deployed
your Isso web service (e.g. `127.0.0.1`).
This allow for proper remote address resolution based on a
`X-Forwarded-For` HTTP header, which is important for the mechanism
forbiding several comment votes coming from the same subnet.
.. _configure-smtp:
SMTP
@ -233,16 +176,15 @@ port
security
use a secure connection to the server, possible values: *none*, *starttls*
or *ssl*. Note, that there is no easy way for Python 2.7 and 3.3 to
implement certification validation and thus the connection is vulnerable to
Man-in-the-Middle attacks. You should definitely use a dedicated SMTP
account for Isso in that case.
or *ssl*. Note, that Python does not validate the server's certificate and
thus the connection is vulnerable to Man-in-the-Middle attacks. Therefore,
you should definitely use a dedicated SMTP account for Isso.
to
recipient address, e.g. your email address
from
sender address, e.g. `"Foo Bar" <isso@example.tld>`
sender address, e.g. isso@example.tld
timeout
specify a timeout in seconds for blocking operations like the
@ -262,8 +204,6 @@ for IPv4, ``/48`` for IPv6).
ratelimit = 2
direct-reply = 3
reply-to-self = false
require-author = false
require-email = false
enabled
enable guard, recommended in production. Not useful for debugging
@ -281,19 +221,7 @@ reply-to-self
the comment. After the editing timeframe is gone, commenters can reply to
their own comments anyways.
Do not forget to configure the `client <client>`_ accordingly
require-author
force commenters to enter a value into the author field. No validation is
performed on the provided value.
Do not forget to configure the `client <client>`_ accordingly.
require-email
force commenters to enter a value into the email field. No validation is
performed on the provided value.
Do not forget to configure the `client <client>`_ accordingly.
Do not forget to configure the client.
Markup
------
@ -305,20 +233,12 @@ supported, but new languages are relatively easy to add.
[markup]
options = strikethrough, superscript, autolink
flags = skip-html, escape, hard-wrap
allowed-elements =
allowed-attributes =
options
`Misaka-specific Markdown extensions <https://misaka.61924.nl/#api>`_, all
extension flags can be used there, separated by comma, either by their name
or as `EXT_`_.
flags
`Misaka-specific HTML rendering flags
<https://misaka.61924.nl/#html-render-flags>`_, all html rendering flags
can be used here, separated by comma, either by their name or as `HTML_`_.
Per Misaka's defaults, no flags are set.
`Misaka-specific Markdown extensions <http://misaka.61924.nl/api/>`_, all
flags starting with `EXT_` can be used there, separated by comma.
allowed-elements
Additional HTML tags to allow in the generated output, comma-separated. By
@ -334,71 +254,6 @@ allowed-attributes
To allow images in comments, you just need to add ``allowed-elements = img`` and
``allowed-attributes = src``.
Hash
----
Customize used hash functions to hide the actual email addresses from
commenters but still be able to generate an identicon.
.. code-block:: ini
[hash]
salt = Eech7co8Ohloopo9Ol6baimi
algorithm = pbkdf2
salt
A salt is used to protect against rainbow tables. Isso does not make use of
pepper (yet). The default value has been in use since the release of Isso
and generates the same identicons for same addresses across installations.
algorithm
Hash algorithm to use -- either from Python's `hashlib` or PBKDF2 (a
computational expensive hash function).
The actual identifier for PBKDF2 is `pbkdf2:1000:6:sha1`, which means 1000
iterations, 6 bytes to generate and SHA1 as pseudo-random family used for
key strengthening.
Arguments have to be in that order, but can be reduced to `pbkdf2:4096`
for example to override the iterations only.
.. _configure-rss:
RSS
---
Isso can provide an Atom feed for each comment thread. Users can use
them to subscribe to comments and be notified of changes. Atom feeds
are enabled as soon as there is a base URL defined in this section.
.. code-block:: ini
[rss]
base =
limit = 100
base
base URL to use to build complete URI to pages (by appending the URI from Isso)
limit
number of most recent comments to return for a thread
Admin
-----
Isso has an optional web administration interface that can be used to moderate
comments. The interface is available under ``/admin`` on your isso URL.
.. code-block:: ini
[admin]
enabled = true
password = secret
enabled
whether to enable the admin interface
password
the plain text password to use for logging into the administration interface
Appendum
--------
@ -412,18 +267,3 @@ Timedelta
You can add different types: `1m30s` equals to 90 seconds, `3h45m12s`
equals to 3 hours, 45 minutes and 12 seconds (12512 seconds).
Environment variables
---------------------
.. _environment-variables:
Isso also support configuration through some environment variables:
ISSO_CORS_ORIGIN
By default, `isso` will use the `Host` or else the `Referrer` HTTP header
of the request to defines a CORS `Access-Control-Allow-Origin` HTTP header
in the response.
This environent variable allows you to define a broader fixed value,
in order for example to share a single Isso instance among serveral of your
subdomains : `ISSO_CORS_ORIGIN=*.example.test`

@ -21,36 +21,5 @@ Now, either include `count.min.js` if you want to show only the comment count
(e.g. on an index page) or `embed.min.js` for the full comment client (see
:doc:`../quickstart`); do not mix both.
You can have as many comments counters as you want in a page, and they will be
merged into a single `GET` request.
Asynchronous comments loading
-----------------------------
Isso will automatically fetch comments after `DOMContentLoaded` event. However
in the case where your website is creating content dynamically (eg. via ajax),
you need to re-fetch comment thread manually. Here is how you can re-fetch the
comment thread:
.. code-block:: js
window.Isso.fetchComments()
It will delete all comments under the thread but not the PostBox, fetch
comments with `data-isso-id` attribute of the element `section#isso-thread` (if
that attribute does not exist, fallback to `window.location.pathname`), then
fill comments into the thread. In other words, you should change `data-isso-id`
attribute of the element `section#isso-thread` (or modify the pathname with
`location.pushState`) before you can get new comments. And the thread element
itself should *NOT* be touched or removed.
If you removed the `section#isso-thread` element, just create another element
with same TagName and ID in which you wish comments to be placed, then call the
`init` method of `Isso`:
.. code-block:: js
window.Isso.init()
Then Isso will initialize the comment section and fetch comments, as if the page
was loaded.
You can have as many comments counters as you want in a page but be aware that it
implies one `GET` request per comment anchor.

@ -1,70 +0,0 @@
Advanced Migration
==================
In quickstart we saw you can import comments from Disqus or WordPress. But there
are a many other comments system and you could be using one of them.
Isso provides a way to import such comments, however it's up to you to to:
- dump comments
- fit the data to the following JSON format::
A list of threads, each item being a dict with the following data:
- id: a text representing the unique thread id (note: by default isso
associates this ID to the article URL, but it can be changed on
client side with "data-isso-id" - see :doc:`client configuration <../configuration/client>` )
- title: the title of the thread
- comments: the list of comments
Each item in that list of comments is a dict with the following data:
- id: an integer with the unique id of the comment inside the thread
(it can be repeated among different threads); this will be used to
order the comment inside the thread
- author: the author's name
- email: the author's email
- website: the author's website
- remote_addr: the author's IP
- created: a timestamp, in the format "%Y-%m-%d %H:%M:%S"
Example:
.. code-block:: json
[
{
"id": "/blog/article1",
"title": "First article!",
"comments": [
{
"author": "James",
"created": "2018-11-28 17:24:23",
"email": "email@mail.com",
"id": "1",
"remote_addr": "127.0.0.1",
"text": "Great article!",
"website": "http://fefzfzef.frzr"
},
{
"author": "Harold",
"created": "2018-11-28 17:58:03",
"email": "email2@mail.com",
"id": "2",
"remote_addr": "",
"text": "I hated it...",
"website": ""
}
]
}
]
Keep in mind that isso expects to have an array, so keep the opening and ending square bracket even if you have only one article thread!
Next you can import you json dump:
.. code-block:: sh
~> isso -c /path/to/isso.cfg import -t generic comment-dump.json
[100%] 53 threads, 192 comments

@ -23,11 +23,12 @@ Isso:
"mode": 1,
"hash": "4505c1eeda98",
"author": null,
"website": null,
"email": null,
"website": null
"created": 1387321261.572392,
"modified": null,
"likes": 3,
"dislikes": 0
"dislikes": 0,
}
id :
@ -70,7 +71,7 @@ modified :
List comments
-------------
List all publicly visible comments for thread `uri`:
List all publicely visible comments for thread `uri`:
.. code-block:: text
@ -83,19 +84,6 @@ plain :
pass plain=1 to get the raw comment text, defaults to 0.
Get the latest N comments for all threads:
.. code-block:: text
GET /latest?limit=N
The N parameter limits how many of the latest comments to retrieve; it's
mandatory, and must be an integer greater than 0.
This endpoint needs to be enabled in the configuration (see the
``latest-enabled`` option in the ``general`` section).
Create comment
--------------
@ -198,16 +186,3 @@ uri :
returns an integer
Get Atom feed
-------------
Get an Atom feed of comments for thread `uri`:
.. code-block:: text
GET /feed?uri=%2Fhello-world%2F
uri :
URI to get comments for, required.
Returns an XML document as the Atom feed.

@ -1,20 +0,0 @@
Community tools
===============
Utility scripts
---------------
Some utility scripts have been developed by isso users.
They are stored in the `GitHub contrib/ directory
<https://github.com/posativ/isso/tree/master/contrib>`_ :
* `dump_comments.py` : dump isso comments as text, optionally with color
* `import_blogger.py` : comment importer from Blogger
Related projects
----------------
* `wonderfall/isso Docker image <https://github.com/Wonderfall/docker-isso>`
* `a grav plugin to integrate isso comments <https://github.com/Sommerregen/grav-plugin-jscomments>`
* `a Pelican theme supporting isso comments <https://github.com/Lucas-C/pelican-mg>`

@ -4,7 +4,7 @@ Deployment
Isso ships with a built-in web server, which is useful for the initial setup
and may be used in production for low-traffic sites (up to 20 requests per
second). Running a "real" WSGI server supports nice things such as UNIX domain
sockets, daemonization and solid HTTP handler. WSGI servers are more stable, secure
sockets, daemonization and solid HTTP handler while being more stable, secure
and web-scale than the built-in web server.
* gevent_, coroutine-based network library
@ -13,11 +13,10 @@ and web-scale than the built-in web server.
* mod_wsgi_, Apache interface to WSGI
* mod_fastcgi_, Apache interface to FastCGI
* uberspace.de, `try this guide (in german) <http://blog.posativ.org/2014/isso-und-uberspace-de/>`_
* Openshift, Isso has a one click installer
`gevent <http://www.gevent.org/>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`gevent <http://gunicorn.org/>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Probably the easiest deployment method. Install with PIP (requires libevent):
@ -98,108 +97,34 @@ To execute Isso, use a command similar to:
`mod_wsgi <https://code.google.com/p/modwsgi/>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
First, create a startup script, called `isso.wsgi`. If Isso is in your system module
search path, then the script is quite simple. This script is included in the
isso distribution as `run.py`:
.. code-block:: python
from __future__ import unicode_literals
import os
from isso import make_app
from isso import dist, config
application = make_app(
config.load(
os.path.join(dist.location, dist.project_name, "defaults.ini"),
"/path/to/isso.cfg"),
multiprocessing=True)
If you have installed Isso in a virtual environment, then you will have to add the path
of the virtualenv to the site-specific paths of Python:
.. code-block:: python
from __future__ import unicode_literals
.. note:: This information may be incorrect, if you have more knowledge on how
to deploy Python via `mod_wsgi`, consider extending/correcting this section.
import site
site.addsitedir("/path/to/isso_virtualenv")
For more information, see `Flask: Configuring Apache
<http://flask.pocoo.org/docs/deploying/mod_wsgi/#configuring-apache>`_.
import os
.. code-block:: apache
from isso import make_app
from isso import dist, config
<VirtualHost *>
ServerName example.org
application = make_app(
config.load(
os.path.join(dist.location, dist.project_name, "defaults.ini"),
"/path/to/isso.cfg",
multiprocessing=True)
WSGIDaemonProcess isso user=www-data group=www-data threads=5
WSGIScriptAlias / /var/www/isso.wsgi
</VirtualHost>
Using the aforementioned script will load system modules when available and modules
from the virtualenv otherwise. Should you want the opposite behavior, where modules from
the virtualenv have priority over system modules, the following script does the trick:
Next, copy'n'paste to `/var/www/isso.wsgi`:
.. code-block:: python
from __future__ import unicode_literals
import os
import site
import sys
# Remember original sys.path.
prev_sys_path = list(sys.path)
# Add the new site-packages directory.
site.addsitedir("/path/to/isso_virtualenv")
# Reorder sys.path so new directories at the front.
new_sys_path = []
for item in list(sys.path):
if item not in prev_sys_path:
new_sys_path.append(item)
sys.path.remove(item)
sys.path[:0] = new_sys_path
from isso import make_app
from isso import dist, config
from isso.core import Config
application = make_app(
config.load(
os.path.join(dist.location, dist.project_name, "defaults.ini"),
"/path/to/isso.cfg",
multiprocessing=True)
application = make_app(Config.load("/path/to/isso.cfg"))
The last two scripts are based on those given by
`mod_wsgi documentation <https://code.google.com/p/modwsgi/wiki/VirtualEnvironments>`_.
The Apache configuration will then be similar to the following:
.. code-block:: apache
<VirtualHost *>
ServerName example.org
WSGIDaemonProcess isso user=www-data group=www-data threads=5
WSGIScriptAlias /mounted_isso_path /path/to/isso.wsgi
</VirtualHost>
You will need to adjust the user and group according to your Apache installation and
security policy. Be aware that the directory containing the comments database must
be writable by the user or group running the WSGI daemon process: having a writable
database only is not enough, since SQLite will need to create a lock file in the same
directory.
`mod_fastcgi <http://www.fastcgi.com/mod_fastcgi/docs/mod_fastcgi.html>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You can use this method if your hosting provider doesn't allow you to have long
running processes. If FastCGI has not yet been configured in your server,
please follow these steps:
.. note:: This information may be incorrect, if you have more knowledge on how
to deploy Python via `mod_fastcgi`, consider extending/correcting this section.
@ -223,51 +148,16 @@ please follow these steps:
</Location>
</VirtualHost>
Next, to run isso as a FastCGI script you'll need to install ``flup`` with
PIP:
.. code-block:: sh
$ pip install flup
Finally, copy'n'paste to `/var/www/isso.fcgi` (or whatever location you prefer):
Next, copy'n'paste to `/var/www/isso.fcgi` (or whatever location you prefer):
.. code-block:: python
#!/usr/bin/env python
#: uncomment if you're using a virtualenv
# import sys
# sys.path.insert(0, '<your_local_path>/lib/python2.7/site-packages')
from isso import make_app, dist, config
import os
from isso import make_app
from isso.core import Config
from flup.server.fcgi import WSGIServer
application = make_app(config.load(
os.path.join(dist.location, dist.project_name, "defaults.ini"),
"/path/to/isso.cfg"))
application = make_app(Config.load("/path/to/isso.cfg"))
WSGIServer(application).run()
`Openshift <http://openshift.com>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
With `Isso Openshift Deployment Kit`_, Isso can be installed on Open
Shift with just one click. Make sure you already have installed ``rhc``
(`instructions`_) and completed the setup.
1. Run the following, you will get an Open Shift instance installed with
Isso:
::
rhc create-app appname python-2.7 --from-code https://github.com/avinassh/isso-openshift.git
2. Above step also clones Git repository of your Open Shift instance, in
current directory. Make changes to the configuration file and push
back to Openshift, it will be redeployed with new settings.
3. Visit ``http://<yourappname>-<openshift-namespace>.com/info`` to
verify Isso is deployed properly and is working.
.. _Isso Openshift Deployment Kit: https://github.com/avinassh/isso-openshift
.. _instructions: https://developers.openshift.com/en/managing-client-tools.html

@ -1,9 +1,9 @@
Overview
========
Welcome to Isso's documentation. This documentation will help you get started
fast. If you run into any problems when using Isso, you can find the answer in
troubleshooting guide or you can ask me on IRC or GitHub.
Welcome to the Isso's documentation. This documentation will help you get
started fast. If you get any problems when using Isso, you can find the answer
in troubleshooting or you can ask me on IRC or GitHub.
Documentation overview:
@ -19,7 +19,7 @@ What's Isso?
Isso is a lightweight commenting server similar to Disqus. It allows anonymous
comments, maintains identity and is simple to administrate. It uses JavaScript
and cross-origin ressource sharing for easy integration into (static) websites.
and cross-origin ressource sharing for easy integration into static websites.
No, I meant "Isso"
------------------

@ -1,17 +1,15 @@
Installation
============
Isso is a web application written in Python. If pip and virtualenv mean anything
to you, continue with :ref:`install-from-pypi`. If you are running
Debian/Ubuntu, Gentoo, Archlinux or Fedora, you can use
:ref:`prebuilt-package`. If not, read the next section carefully.
Isso is a web application written in Python. If pip and virtualenv mean
anything to you, continue with :ref:`install-from-pypi`. If you are running
Debian/Ubuntu or Gentoo, you can use :ref:`prebuilt-package`. If not, read the
next section carefully.
.. contents::
:local:
:depth: 1
.. _install-interludium:
Interludium: Python is not PHP
------------------------------
@ -22,12 +20,12 @@ libraries, but most likely not all required by Isso (or in an up-to-date
version looking at you, Debian!).
That's why most Python developers use the `Python Package Index`_ to get their
dependencies. The most important rule to follow is to never install *anything* from PyPi
as root. Not because of malicious software, but because you *will* break your
packages. But the most important rule: never install *anything* from PyPi as
root. Not because of malicious software, but because you *will* break your
system.
``easy_install`` is one tool to mess up your system. Another package manager is
``pip``. If you ever searched for an issue with Python/pip and Stackoverflow is
suggesting you ``easy_install pip`` or ``pip install --upgrade pip`` (as root
suggesting your ``easy_install pip`` or ``pip install --upgrade pip`` (as root
of course!), you are doing it wrong. `Why you should not use Python's
easy_install carelessly on Debian`_ is worth the read.
@ -39,18 +37,18 @@ package manager.
.. code-block:: sh
# for Debian/Ubuntu
~> sudo apt-get install python-setuptools python-virtualenv python-dev
~> sudo apt-get install python-setuptools python-virtualenv
# Fedora/Red Hat
~> sudo yum install python-setuptools python-virtualenv python-devel
~> sudo yum install python-setuptools python-virtualenv
The next steps should be done as regular user, not as root (although possible
but not recommended):
.. code-block:: sh
~> virtualenv /opt/isso
~> source /opt/isso/bin/activate
~> virtualenv /home/user/python/isso
~> source /home/user/python/isso/activate
After calling `source`, you can now install packages from PyPi locally into this
virtual environment. If you don't like Isso anymore, you just `rm -rf` the
@ -58,14 +56,10 @@ folder. Inside this virtual environment, you may also execute the example
commands from above to upgrade your Python Package Manager (although it barely
makes sense), it is completely independent from your global system.
To use Isso installed in a virtual environment outside of the virtual
environment, you just need to add */opt/isso/bin* to your :envvar:`PATH` or
execute */opt/isso/bin/isso* directly. It will launch Isso from within the
virtual environment.
With a virtualenv ready, you may now continue to :ref:`install-from-pypi`!
Note, that if you are using a slightly more advanced shared-hoster or virtual
machines, you may not need virtual environments.
With a virtualenv active, you may now continue to :ref:`install-from-pypi`!
Of course you may not need a virtualenv when you are running dedicated virtual
machines or a shared host (e.g. Uberspace.de).
.. _Python Package Index: https://pypi.python.org/pypi
.. _Why you should not use Python's easy_install carelessly on Debian:
@ -76,29 +70,11 @@ machines or a shared host (e.g. Uberspace.de).
Install from PyPi
-----------------
Requirements
^^^^^^^^^^^^
- Python 2.7 or 3.4+ (+ devel headers)
- SQLite 3.3.8 or later
- a working C compiler
For Debian/Ubuntu just `copy and paste
<http://thejh.net/misc/website-terminal-copy-paste>`_ to your terminal:
.. code-block:: sh
~> sudo apt-get install python-dev sqlite3 build-essential
Requirements:
Similar for Fedora (and derivates):
.. code-block:: sh
~> sudo yum install python-devel sqlite
~> sudo yum groupinstall “Development Tools”
Installation
^^^^^^^^^^^^
- Python 2.6, 2.7 or 3.3 (+ devel headers)
- SQLite 3.3.8 or later
- a working C compiler
Install Isso with `pip <http://www.pip-installer.org/en/latest/>`_:
@ -112,21 +88,18 @@ Install Isso with `pip <http://www.pip-installer.org/en/latest/>`_:
~> easy_install isso # cross your fingers
For easier execution, you can symlink the executable to a location in your
:envvar:`PATH`.
If you are using a virtualenv, you don't need to activate the environment
every time you want to start Isso. You can symlink the executable to a location
in your PATH:
.. code-block:: sh
~> ln -s /opt/isso/bin/isso /usr/local/bin/isso
Upgrade
^^^^^^^
~> ln -s /path/to/isso-venv/bin/isso /usr/local/bin/isso
To upgrade Isso, activate your virtual environment again, and run
To upgrade Isso, activate your virtual environment once again, and run
.. code-block:: sh
~> source /opt/isso/bin/activate # optional
~> pip install --upgrade isso
.. _prebuilt-package:
@ -134,40 +107,29 @@ To upgrade Isso, activate your virtual environment again, and run
Prebuilt Packages
-----------------
* Debian (since Buster): https://packages.debian.org/search?keywords=isso
* Debian: https://packages.crapouillou.net/ built from PyPi. Includes
startup scripts and vhost configurations for Lighttpd, Apache and Nginx
[`source <https://github.com/jgraichen/debian-isso>`__].
`#729218 <https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=729218>`_ is a
ITP for Debian. To be officially packages by Debian, `#51
<https://github.com/posativ/isso/issues/51>`_ needs to be done (contributions
are welcome).
* Gentoo: http://eroen.eu/cgit/cgit.cgi/eroen-overlay/tree/www-apps/isso?h=isso
not yet available in Portage, but you can use the ebuild to build Isso.
* Arch Linux: https://aur.archlinux.org/packages/isso/
install with `yaourt isso`.
* Fedora: https://copr.fedoraproject.org/coprs/jujens/isso/ — copr
repository. Built from Pypi, includes a systemctl unit script.
Build a Docker image
--------------------
You can get a Docker image by running ``docker build . -t
isso``. Assuming you have your configuration in ``/opt/isso``, you can
use the following command to spawn the Docker container:
.. code-block:: sh
~> docker run -d --rm --name isso -p 127.0.0.1:8080:8080 -v /opt/isso:/config -v /opt/isso:/db isso
Then, you can use a reverse proxy to expose port 8080.
Install from Source
-------------------
If you want to hack on Isso or track down issues, there's an alternate
way to set up Isso. It requires a lot more dependencies and effort:
- Python 2.7 or 3.4+ (+ devel headers)
- Virtualenv
- Python 2.6, 2.7 or 3.3 (+ devel headers)
- SQLite 3.3.8 or later
- a working C compiler
- Ruby 1.8 or higher
- SASS 3.0 or higher
- Node.js, `NPM <https://npmjs.org/>`__ and `Bower <http://bower.io/>`__
Get a fresh copy of Isso:
@ -181,6 +143,7 @@ To create a virtual environment (recommended), run:
.. code-block:: sh
~> pip install virtualenv
~> virtualenv .
~> source ./bin/activate
@ -191,84 +154,39 @@ Install Isso and its dependencies:
~> python setup.py develop # or `install`
~> isso run
Compilation from SCSS to CSS:
.. code-block:: sh
~> make css
Install JavaScript modules:
.. code-block:: sh
~> make init
Integration without optimization:
Integration without previous optimzation:
.. code-block:: html
<script src="/js/config.js"></script>
<script data-main="/js/embed" src="/js/components/requirejs/require.js"></script>
Optimization:
Optimization
.. code-block:: sh
~> npm install -g requirejs uglify-js jade
~> npm install -g requirejs uglifyjs
~> make js
.. _init-scripts:
Init scripts
------------
Init scripts to run Isso as a service (check your distribution's documentation
for your init-system; e.g. Debian uses SysVinit, Fedora uses systemd) if you
don't use FastCGi or uWSGI:
for your init-system; e.g. Debian uses SysVinit, Fedora uses SystemD):
- systemd (Isso + Gunicorn): https://github.com/jgraichen/debian-isso/blob/master/debian/isso.service
- SysVinit (Isso + Gunicorn): https://github.com/jgraichen/debian-isso/blob/master/debian/isso.init
- SystemD: https://github.com/jgraichen/debian-isso/blob/master/debian/isso.service
- SysVinit: https://github.com/jgraichen/debian-isso/blob/master/debian/isso.init
- OpenBSD: https://gist.github.com/noqqe/7397719
- FreeBSD: https://gist.github.com/ckoepp/52f6f0262de04cee1b88ef4a441e276d
- Supervisor: https://github.com/posativ/isso/issues/47
If you're writing your own init script, you can utilize ``start-stop-daemon``
to run Isso in the background (Isso runs in the foreground usually). Below you
will find a very basic SysVinit script which you can use for inspiration:
.. code-block:: sh
#!/bin/sh
### BEGIN INIT INFO
# Provides: isso
# Required-Start: $local_fs $network
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: lightweight Disqus alternative
### END INIT INFO
EXEC=/opt/isso/bin/isso
EXEC_OPTS="-c /etc/isso.cfg run"
RUNAS=isso
PIDFILE=/var/run/isso.pid
start() {
echo 'Starting service…' >&2
start-stop-daemon --start --user "$RUNAS" --background --make-pidfile --pidfile $PIDFILE \
--exec $EXEC -- $EXEC_OPTS
}
stop() {
echo 'Stopping service…' >&2
start-stop-daemon --stop --user "$RUNAS" --pidfile $PIDFILE --exec $EXEC
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
*)
echo "Usage: $0 {start|stop|restart}"
esac

@ -6,7 +6,7 @@ What's Isso?
Isso is a lightweight commenting server similar to Disqus. It allows anonymous
comments, maintains identity and is simple to administrate. It uses JavaScript
and cross-origin resource sharing for easy integration into static websites.
and cross-origin ressource sharing for easy integration into static websites.
No, I meant "Isso"
------------------

@ -2,24 +2,25 @@ Quickstart
==========
Assuming you have successfully :doc:`installed <install>` Isso, here's
a quickstart guide that covers the most common setup. Sections covered:
a quickstart quide that covers common setups. Sections covered:
.. contents::
:local:
:depth: 1
Configuration
-------------
You must provide a custom configuration to set `dbpath` (your database
location) and `host` (a list of websites for CORS_). All other options have
sane defaults.
You must provide a custom configuration. Most default parameters are useful for
development, not persistence. The two most important options are `dbpath` to
set the location of your database and your website's name `host` where you want
to comment on:
.. code-block:: ini
[general]
; database location, check permissions, automatically created if it
does not exist
; database location, check permissions, created if not exists
dbpath = /var/lib/isso/comments.db
; your website or blog (not the location of Isso!)
host = http://example.tld/
@ -32,12 +33,12 @@ sane defaults.
https://example.tld/
Note, that multiple, *different* websites are **not** supported in a single
configuration. To serve comments for different websites, refer to
configuration. To serve comments for diffent websites, refer to
:ref:`Multiple Sites <configure-multiple-sites>`.
The moderation is done with signed URLs sent by email or logged to stdout.
By default, comments are accepted and immediately shown to other users. To
enable moderation queue, add:
You moderate Isso through signed URLs sent by email or logged. By default,
comments are accepted and immediately shown to other users. To enable
moderation queue, add:
.. code-block:: ini
@ -45,7 +46,7 @@ enable moderation queue, add:
enabled = true
To moderate comments, either use the activation or deletion URL in the logs or
:ref:`use SMTP <configure-smtp>` to get notified of new comments, including the
:ref:`use SMTP <configure-smtp>` to get notified on new comments including the
URLs for activation and deletion:
.. code-block:: ini
@ -55,33 +56,26 @@ URLs for activation and deletion:
[smtp]
; SMTP settings
For more options, see :doc:`server <configuration/server>` and :doc:`client
<configuration/client>` configuration.
For more details, see :doc:`server <configuration/server>` and
:doc:`client <configuration/client>` configuration.
Migration
---------
Isso provides a tool for importing comments from Disqus_ or WordPress_.
You can also import comments from any other comment system, but this topic is more
complex and is covered in :doc:`advanced migration <extras/advanced-migration>`.
To export your comments from Disqus, log into Disqus, go to your website, click
on *Discussions* and select the *Export* tab. You'll receive an email with your
comments. Unfortunately, Disqus does not export up- and downvotes.
To export comments from your previous WordPress installation, go to *Tools*,
export your data. It has been reported that WordPress may generate broken XML.
Try to repair the file using ``xmllint`` before you continue with the import.
You can migrate your existing comments from Disqus_. Log into Disqus, go to
your website, click on *Discussions* and select the *Export* tab. You'll
receive an email with your comments. Unfortunately, Disqus does not export
up- and downvotes.
Now import the XML dump:
To import existing comments, run Isso with your new configuration file:
.. code-block:: sh
~> isso -c /path/to/isso.cfg import -t [disqus|wordpress] disqus-or-wordpress.xml
~> isso -c /path/to/isso.cfg import user-2013-09-02T11_39_22.971478-all.xml
[100%] 53 threads, 192 comments
.. _Disqus: https://disqus.com/
.. _WordPress: https://wordpress.org/
.. _Disqus: <https://disqus.com/>
Running Isso
------------
@ -141,14 +135,16 @@ a comment to see if the setup works. If not, see :doc:`troubleshooting`.
Going Further
-------------
There are several server and client configuration options not covered in this
There are several server and client configuration options uncovered in this
quickstart, check out :doc:`configuration/server` and
:doc:`configuration/client` for more information. For further website
integration, see :doc:`extras/advanced-integration`.
To launch Isso automatically, check the :ref:`init-scripts` section from the
installation guide. A different approach to deploy a web application is
written here: :doc:`Deployment of Isso <extras/deployment>`.
If you wondered how to automatically start Isso you may find a short script
for various popular init/supervisor daemons here: :doc:`install`. Another
important topic is the actual :doc:`deployment of Isso <extras/deployment>`
(and every Python web application in general).
.. _Nginx: http://nginx.org/
.. _CORS: https://developer.mozilla.org/en/docs/HTTP/Access_control_CORS

@ -5,8 +5,8 @@ Multiple Sites
--------------
Isso is designed to serve comments for a single website and therefore stores
comments for a relative URL. This is done to support HTTP, HTTPS and even domain transfers
without manual intervention. You can chain Isso to support multiple
comments for a relative URL to support HTTP, HTTPS and even domain transfers
without manual intervention. But you can chain Isso to support multiple
websites on different domains.
The following example uses `gunicorn <http://gunicorn.org/>`_ as WSGI server (

@ -1,39 +1,4 @@
Troubleshooting
===============
For uberspace users
-------------------
Some uberspace users experienced problems with isso and they solved their
issues by adding `DirectoryIndex disabled` as the first line in the `.htaccess`
file for the domain the isso server is running on.
pkg_ressources.DistributionNotFound
-----------------------------------
This is usually caused by messing up the system's Python with newer packages
from PyPi (e.g. by executing `easy_install --upgrade pip` as root) and is not
related to Isso at all.
Install Isso in a virtual environment as described in
:ref:`install-interludium`. Alternatively, you can use `pip install --user`
to install Isso into the user's home.
UnicodeDecodeError: 'ascii' codec can't decode byte 0xff
--------------------------------------------------------
Likely an issue with your environment, check you set your preferred file
encoding either in :envvar:`LANG`, :envvar:`LANGUAGE`, :envvar:`LC_ALL` or
:envvar:`LC_CTYPE`:
.. code-block:: text
$ env LANG=C.UTF-8 isso [-h] [--version] ...
If none of the mentioned variables are set, the interaction with Isso will
likely fail (unable to print non-ascii characters to stdout/err, unable to
parse configuration file with non-ascii characters and so forth).
The web console shows 404 Not Found responses
---------------------------------------------
That's fine. Isso returns "404 Not Found" to indicate "No comments".
To be written.

@ -0,0 +1,2 @@
Usage
=====

@ -1,17 +1,32 @@
Frequently asked question
=========================
Why not use Gravatar/Libravatar/... ?
-------------------------------------
Various people asked or complained about the generated icons next to their
comments. First, it is not an avatar, it is an identicon used to
*identify* an author of multiple comments without leaking personal
informations (unlike Gravatar).
If you are in need of Gravatar_, then use Disqus. If you run your own
Libravatar_ server, you can work on a patch for Isso which adds *optional*
support for avatars.
.. _Gravatar: https://secure.gravatar.com/
.. _Libravatar: http://libravatar.org/
Why SQLite3?
------------
Although partially answered on the index page, here is a more complete answer: If
Although partially answered on the index page, here a more complete answer: If
you manage massive amounts of comments, Isso is a really bad choice. Isso is
designed to be simple and easy to setup, it is not optimized for high-traffic
designed to be simple and easy to setup, not optimizied for high-traffic
websites (use a `dedicated Disqus`_ instance then).
Comments are not big data.
comments are not big data
For example, if you have 209 threads and 778 comments in total this only needs 620 kilobytes
of memory. This is an excellent use case for SQLite.
For example, 209 threads and 778 comments in total only need 620K (kilobyte)
memory. Excellent use case for SQLite.
.. _dedicated Disqus:

@ -14,10 +14,10 @@
</figure>
<ul>
<li>
<p><strong>Comments written in Markdown</strong></p>
<p>Users can edit or delete own comments (within 15 minutes by
default).</p>
<p>Comments in moderation queue are not publicly visible before
<p><strong>Create, Edit and Remove Comments</strong></p>
<p>Commenters can edit or delete their own comments (within
15 minutes by default).</p>
<p>Comments in moderation queue are not publicly visible until
activation.</p>
</li>
<li>
@ -25,13 +25,13 @@
<p>Because comments are not Big Data.</p>
</li>
<li>
<p><strong>Disqus &amp; WordPress Import</strong></p>
<p>You can migrate your Disqus/WordPress comments without any hassle.</p>
<p><strong>Disqus Import</strong></p>
<p>You can migrate your Disqus comments without any hassle.</p>
</li>
<li>
<p><strong>Configurable JS client</strong></p>
<p>Embed a single JS file, 40kb (12kb gzipped) and you are
done.</p>
<p><strong>client-side JavaScript</strong></p>
<p>Embed a single JS file, 52kb (16kb gzipped) and you are
done.</p>
<p>Supports Firefox, Safari, Chrome and IE10.</p>
</li>
</ul>

@ -1,10 +0,0 @@
Releasing steps
===============
* Run ``python3 setup.py nosetests``, ``python2 setup.py nosetests``
* Update version number in ``setup.py`` and ``CHANGES.rst``
* ``git commit -m "Preparing ${VERSION}" setup.py CHANGES.rst``
* ``git tag -as ${VERSION}``
* ``make init all``
* ``python3 setup.py sdist``
* ``twine upload --sign dist/isso-${VERSION}.tar.gz``

@ -3,7 +3,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2012-2014 Martin Zimmermann.
# Copyright (c) 2012-2013 Martin Zimmermann.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -25,7 +25,7 @@
#
# Isso a lightweight Disqus alternative
from __future__ import print_function, unicode_literals
from __future__ import print_function
import pkg_resources
dist = pkg_resources.get_distribution("isso")
@ -35,8 +35,7 @@ import sys
if sys.argv[0].startswith("isso"):
try:
import gevent.monkey
gevent.monkey.patch_all()
import gevent.monkey; gevent.monkey.patch_all()
except ImportError:
pass
@ -49,32 +48,29 @@ from os.path import dirname, join
from argparse import ArgumentParser
from functools import partial, reduce
import pkg_resources
werkzeug = pkg_resources.get_distribution("werkzeug")
from itsdangerous import URLSafeTimedSerializer
from werkzeug.routing import Map
from werkzeug.exceptions import HTTPException, InternalServerError
from werkzeug.middleware.shared_data import SharedDataMiddleware
from werkzeug.wsgi import SharedDataMiddleware
from werkzeug.local import Local, LocalManager
from werkzeug.serving import run_simple
from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.middleware.profiler import ProfilerMiddleware
from werkzeug.contrib.fixers import ProxyFix
from werkzeug.contrib.profiler import ProfilerMiddleware
local = Local()
local_manager = LocalManager([local])
from isso import config, db, migrate, wsgi, ext, views
from isso.core import ThreadedMixin, ProcessMixin, uWSGIMixin
from isso import db, migrate, wsgi, ext, views
from isso.core import ThreadedMixin, ProcessMixin, uWSGIMixin, Config
from isso.wsgi import origin, urlsplit
from isso.utils import http, JSONRequest, html, hash
from isso.utils import http, JSONRequest, html
from isso.views import comments
from isso.ext.notifications import Stdout, SMTP
logging.getLogger('werkzeug').setLevel(logging.WARN)
logging.getLogger('werkzeug').setLevel(logging.ERROR)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s: %(message)s")
@ -82,37 +78,23 @@ logging.basicConfig(
logger = logging.getLogger("isso")
class ProxyFixCustom(ProxyFix):
def __init__(self, app):
# This is needed for werkzeug.wsgi.get_current_url called in isso/views/comments.py
# to work properly when isso is hosted under a sub-path
# cf. https://werkzeug.palletsprojects.com/en/1.0.x/middleware/proxy_fix/
super().__init__(app, x_prefix=1)
class Isso(object):
salt = b"Eech7co8Ohloopo9Ol6baimi"
def __init__(self, conf):
self.conf = conf
self.db = db.SQLite3(conf.get('general', 'dbpath'), conf)
self.signer = URLSafeTimedSerializer(
self.db.preferences.get("session-key"))
self.signer = URLSafeTimedSerializer(self.db.preferences.get("session-key"))
self.markup = html.Markup(conf.section('markup'))
self.hasher = hash.new(conf.section("hash"))
super(Isso, self).__init__(conf)
subscribers = []
smtp_backend = False
for backend in conf.getlist("general", "notify"):
if backend == "stdout":
subscribers.append(Stdout(None))
elif backend in ("smtp", "SMTP"):
smtp_backend = True
else:
logger.warn("unknown notification backend '%s'", backend)
if smtp_backend or conf.getboolean("general", "reply-notifications"):
subscribers.append(Stdout(None))
if conf.get("general", "notify") == "smtp":
subscribers.append(SMTP(self))
self.signal = ext.Signal(*subscribers)
@ -120,7 +102,7 @@ class Isso(object):
self.urls = Map()
views.Info(self)
comments.API(self, self.hasher)
comments.API(self)
def render(self, text):
return self.markup.render(text)
@ -135,8 +117,7 @@ class Isso(object):
local.request = request
local.host = wsgi.host(request.environ)
local.origin = origin(self.conf.getiter(
"general", "host"))(request.environ)
local.origin = origin(self.conf.getiter("general", "host"))(request.environ)
adapter = self.urls.bind_to_environ(request.environ)
@ -150,8 +131,7 @@ class Isso(object):
except HTTPException as e:
return e
except Exception:
logger.exception("%s %s", request.method,
request.environ["PATH_INFO"])
logger.exception("%s %s", request.method, request.environ["PATH_INFO"])
return InternalServerError()
else:
return response
@ -196,24 +176,18 @@ def make_app(conf=None, threading=True, multiprocessing=False, uwsgi=False):
if isso.conf.getboolean("server", "profile"):
wrapper.append(partial(ProfilerMiddleware,
sort_by=("cumulative", ), restrictions=("isso/(?!lib)", 10)))
sort_by=("cumulative", ), restrictions=("isso/(?!lib)", 10)))
wrapper.append(partial(SharedDataMiddleware, exports={
'/js': join(dirname(__file__), 'js/'),
'/css': join(dirname(__file__), 'css/'),
'/img': join(dirname(__file__), 'img/'),
'/demo': join(dirname(__file__), 'demo/')
}))
'/css': join(dirname(__file__), 'css/')}))
wrapper.append(partial(wsgi.CORSMiddleware,
origin=origin(isso.conf.getiter("general", "host")),
allowed=("Origin", "Referer", "Content-Type"),
exposed=("X-Set-Cookie", "Date")))
wrapper.extend([wsgi.SubURI, ProxyFixCustom])
origin=origin(isso.conf.getiter("general", "host")),
allowed=("Origin", "Referer", "Content-Type"),
exposed=("X-Set-Cookie", "Date")))
if werkzeug.version.startswith("0.8"):
wrapper.append(wsgi.LegacyWerkzeugMiddleware)
wrapper.extend([wsgi.SubURI, ProxyFix])
return reduce(lambda x, f: f(x), wrapper, isso)
@ -223,50 +197,28 @@ def main():
parser = ArgumentParser(description="a blog comment hosting service")
subparser = parser.add_subparsers(help="commands", dest="command")
parser.add_argument('--version', action='version',
version='%(prog)s ' + dist.version)
parser.add_argument('--version', action='version', version='%(prog)s ' + dist.version)
parser.add_argument("-c", dest="conf", default="/etc/isso.conf",
metavar="/etc/isso.conf", help="set configuration file")
metavar="/etc/isso.conf", help="set configuration file")
imprt = subparser.add_parser('import', help="import Disqus XML export")
imprt.add_argument("dump", metavar="FILE")
imprt.add_argument("-n", "--dry-run", dest="dryrun", action="store_true",
help="perform a trial run with no changes made")
imprt.add_argument("-t", "--type", dest="type", default=None,
choices=["disqus", "wordpress", "generic"], help="export type")
imprt.add_argument("--empty-id", dest="empty_id", action="store_true",
help="workaround for weird Disqus XML exports, #135")
# run Isso as stand-alone server
subparser.add_parser("run", help="run server")
serve = subparser.add_parser("run", help="run server")
args = parser.parse_args()
conf = config.load(
join(dist.location, dist.project_name, "defaults.ini"), args.conf)
conf = Config.load(args.conf)
if args.command == "import":
conf.set("guard", "enabled", "off")
if args.dryrun:
xxx = tempfile.NamedTemporaryFile()
dbpath = xxx.name
else:
dbpath = conf.get("general", "dbpath")
mydb = db.SQLite3(dbpath, conf)
migrate.dispatch(args.type, mydb, args.dump, args.empty_id)
xxx = tempfile.NamedTemporaryFile()
dbpath = conf.get("general", "dbpath") if not args.dryrun else xxx.name
conf.set("guard", "enabled", "off")
migrate.disqus(db.SQLite3(dbpath, conf), args.dump)
sys.exit(0)
if conf.get("general", "log-file"):
handler = logging.FileHandler(conf.get("general", "log-file"))
logger.addHandler(handler)
logging.getLogger("werkzeug").addHandler(handler)
logger.propagate = False
logging.getLogger("werkzeug").propagate = False
if not any(conf.getiter("general", "host")):
logger.error("No website(s) configured, Isso won't work.")
sys.exit(1)

@ -1,26 +1,28 @@
# -*- encoding: utf-8 -*-
try:
text_type = unicode # Python 2
string_types = (str, unicode)
PY2K = True
except NameError: # Python 3
PY2K = False
text_type = str
string_types = (str, )
import sys
PY2K = sys.version_info[0] == 2
if not PY2K:
buffer = memoryview
filter, map, zip = filter, map, zip
def iteritems(dikt):
return iter(dikt.items()) # noqa: E731
map, zip, filter = map, zip, filter
from functools import reduce
iteritems = lambda dikt: iter(dikt.items())
text_type = str
string_types = (str, )
buffer = memoryview
else:
buffer = buffer
from itertools import ifilter, imap, izip
filter, map, zip = ifilter, imap, izip
def iteritems(dikt):
return dikt.iteritems() # noqa: E731
from itertools import imap, izip, ifilter
map, zip, filter = imap, izip, ifilter
reduce = reduce
iteritems = lambda dikt: dikt.iteritems()
text_type = unicode
string_types = (str, unicode)
buffer = buffer

@ -1,152 +0,0 @@
# -*- encoding: utf-8 -*-
from __future__ import unicode_literals
import re
import logging
import datetime
from email.utils import parseaddr, formataddr
try:
from backports.configparser import ConfigParser
except ImportError:
from configparser import ConfigParser
from isso.compat import text_type as str
logger = logging.getLogger("isso")
def timedelta(string):
"""
Parse :param string: into :class:`datetime.timedelta`, you can use any
(logical) combination of Nw, Nd, Nh and Nm, e.g. `1h30m` for 1 hour, 30
minutes or `3w` for 3 weeks.
Raises a ValueError if the input is invalid/unparseable.
>>> print(timedelta("3w"))
21 days, 0:00:00
>>> print(timedelta("3w 12h 57m"))
21 days, 12:57:00
>>> print(timedelta("1h30m37s"))
1:30:37
>>> print(timedelta("1asdf3w"))
Traceback (most recent call last):
...
ValueError: invalid human-readable timedelta
"""
keys = ["weeks", "days", "hours", "minutes", "seconds"]
regex = "".join(["((?P<%s>\\d+)%s ?)?" % (k, k[0]) for k in keys])
kwargs = {}
for k, v in re.match(regex, string).groupdict(default="0").items():
kwargs[k] = int(v)
rv = datetime.timedelta(**kwargs)
if rv == datetime.timedelta():
raise ValueError("invalid human-readable timedelta")
return datetime.timedelta(**kwargs)
class Section(object):
"""A wrapper around :class:`IssoParser` that returns a partial configuration
section object.
>>> conf = new({"foo": {"bar": "spam"}})
>>> section = conf.section("foo")
>>> conf.get("foo", "bar") == section.get("bar")
True
"""
def __init__(self, conf, section):
self.conf = conf
self.section = section
def get(self, key):
return self.conf.get(self.section, key)
def getint(self, key):
return self.conf.getint(self.section, key)
def getlist(self, key):
return self.conf.getlist(self.section, key)
def getiter(self, key):
return self.conf.getiter(self.section, key)
def getboolean(self, key):
return self.conf.getboolean(self.section, key)
class IssoParser(ConfigParser):
"""Parse INI-style configuration with some modifications for Isso.
* parse human-readable timedelta such as "15m" as "15 minutes"
* handle indented lines as "lists"
"""
def getint(self, section, key):
try:
delta = timedelta(self.get(section, key))
except ValueError:
return super(IssoParser, self).getint(section, key)
else:
try:
return int(delta.total_seconds())
except AttributeError:
return int(delta.total_seconds())
def getlist(self, section, key):
return list(map(str.strip, self.get(section, key).split(',')))
def getiter(self, section, key):
for item in map(str.strip, self.get(section, key).split('\n')):
if item:
yield item
def section(self, section):
return Section(self, section)
def new(options=None):
cp = IssoParser(allow_no_value=True)
if options:
cp.read_dict(options)
return cp
def load(default, user=None):
# return set of (section, option)
def setify(cp):
return set((section, option) for section in cp.sections()
for option in cp.options(section))
parser = new()
parser.read(default)
a = setify(parser)
if user:
parser.read(user)
for item in setify(parser).difference(a):
logger.warn("no such option: [%s] %s", *item)
if item in (("server", "host"), ("server", "port")):
logger.warn("use `listen = http://$host:$port` instead")
if item == ("smtp", "ssl"):
logger.warn("use `security = none | starttls | ssl` instead")
if item == ("general", "session-key"):
logger.info("Your `session-key` has been stored in the "
"database itself, this option is now unused")
if not parseaddr(parser.get("smtp", "from"))[0]:
parser.set("smtp", "from",
formataddr(("Ich schrei sonst!", parser.get("smtp", "from"))))
return parser

@ -2,11 +2,14 @@
from __future__ import print_function
import io
import time
import logging
import threading
import multiprocessing
from configparser import ConfigParser
try:
import uwsgi
except ImportError:
@ -19,12 +22,132 @@ if PY2K:
else:
import _thread as thread
from flask_caching.backends.null import NullCache
from flask_caching.backends.simple import SimpleCache
from isso.utils import parse
from isso.compat import text_type as str
from werkzeug.contrib.cache import NullCache, SimpleCache
logger = logging.getLogger("isso")
class Section:
def __init__(self, conf, section):
self.conf = conf
self.section = section
def get(self, key):
return self.conf.get(self.section, key)
def getint(self, key):
return self.conf.getint(self.section, key)
def getlist(self, key):
return self.conf.getlist(self.section, key)
def getiter(self, key):
return self.conf.getiter(self.section, key)
def getboolean(self, key):
return self.conf.getboolean(self.section, key)
class IssoParser(ConfigParser):
"""
Extended :class:`ConfigParser` to parse human-readable timedeltas
into seconds and handles multiple values per key.
"""
@classmethod
def _total_seconds(cls, td):
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
def getint(self, section, key):
try:
delta = parse.timedelta(self.get(section, key))
except ValueError:
return super(IssoParser, self).getint(section, key)
else:
try:
return int(delta.total_seconds())
except AttributeError:
return int(IssoParser._total_seconds(delta))
def getlist(self, section, key):
return list(map(str.strip, self.get(section, key).split(',')))
def getiter(self, section, key):
for item in map(str.strip, self.get(section, key).split('\n')):
if item:
yield item
def section(self, section):
return Section(self, section)
class Config:
default = [
"[general]",
"name = ",
"dbpath = /tmp/isso.db",
"host = ",
"max-age = 15m",
"notify = ",
"[moderation]",
"enabled = false",
"purge-after = 30d",
"[server]",
"listen = http://localhost:8080/",
"reload = off", "profile = off",
"[smtp]",
"username = ", "password = ",
"host = localhost", "port = 587", "security = starttls",
"to = ", "from = ",
"timeout = 10",
"[guard]",
"enabled = true",
"ratelimit = 2",
"direct-reply = 3",
"reply-to-self = false",
"[markup]",
"options = strikethrough, autolink",
"allowed-elements = ",
"allowed-attributes = "
]
@classmethod
def load(cls, configfile):
# return set of (section, option)
setify = lambda cp: set((section, option) for section in cp.sections()
for option in cp.options(section))
rv = IssoParser(allow_no_value=True)
rv.read_file(io.StringIO(u'\n'.join(Config.default)))
a = setify(rv)
if configfile:
rv.read(configfile)
diff = setify(rv).difference(a)
if diff:
for item in diff:
logger.warn("no such option: [%s] %s", *item)
if item in (("server", "host"), ("server", "port")):
logger.warn("use `listen = http://$host:$port` instead")
if item == ("smtp", "ssl"):
logger.warn("use `security = none | starttls | ssl` instead")
if item == ("general", "session-key"):
logger.info("Your `session-key` has been stored in the "
"database itself, this option is now unused")
return rv
class Cache:
"""Wrapper around werkzeug's cache class, to make it compatible to
uWSGI's cache framework.
@ -123,9 +246,7 @@ class uWSGIMixin(Mixin):
self.cache = uWSGICache
timedelta = conf.getint("moderation", "purge-after")
def purge(signum):
return self.db.comments.purge(timedelta)
purge = lambda signum: self.db.comments.purge(timedelta)
uwsgi.register_signal(1, "", purge)
uwsgi.add_timer(1, timedelta)

@ -1,134 +0,0 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
h1, h2, h3, h4, h5, h6 {
font-style: normal;
font-weight: normal;
}
input {
text-align: center;
}
.header::before, .header::after {
content: " ";
display: table;
}
.header::after {
clear: both;
}
.header::before, .header::after {
content: " ";
display: table;
}
.header {
margin-left: auto;
margin-right: auto;
max-width: 68em;
padding-bottom: 1em;
padding-top: 1em;
}
.header header {
display: block;
float: left;
font-weight: normal;
margin-right: 16.0363%;
width: 41.9818%;
}
.header header .logo {
float: left;
max-height: 60px;
padding-right: 12px;
}
.header header h1 {
font-size: 1.55em;
margin-bottom: 0.3em;
}
.header header h2 {
font-size: 1.05em;
}
.header a, .header a:visited {
color: #4d4c4c;
text-decoration: none;
}
.outer {
background-color: #eeeeee;
box-shadow: 0 0 0.5em #c0c0c0 inset;
}
.outer .filters::before, .outer .filters::after {
content: " ";
display: table;
}
.outer .filters::after {
clear: both;
}
.outer .filters::before, .outer .filters::after {
content: " ";
display: table;
}
.outer .filters {
margin-left: auto;
margin-right: auto;
max-width: 68em;
padding: 1em;
}
a {
text-decoration: none;
color: #4d4c4c;
}
.label {
background-color: #ddd;
border: 1px solid #ccc;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
cursor: pointer;
line-height: 1.4em;
outline: 0 none;
padding: calc(0.6em - 1px);
}
.active {
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6) inset;
}
.label-valid {
background-color: #cfc;
border-color: #cfc;
}
.label-pending {
background-color: #ffc;
border-color: #ffc;
}
.mode {
float: left;
}
.pagination {
float: right;
}
.note .label {
margin: 9px;
padding: 3px;
}
#login {
margin-top: 40px;
text-align: center;
width: 100%;
}
.isso-comment-footer a {
cursor: pointer;
}
.thread-title {
margin-left: 3em;
}
.group {
float: left;
margin-left: 2em;
}
.editable {
border: 1px solid #aaa;
border-radius: 5px;
margin: 10px;
padding: 5px;
}
.hidden {
display: none;
}

@ -0,0 +1,13 @@
//************************************************************************//
// These mixins/functions are deprecated
// They will be removed in the next MAJOR version release
//************************************************************************//
@mixin box-shadow ($shadows...) {
@include prefixer(box-shadow, $shadows, spec);
@warn "box-shadow is deprecated and will be removed in the next major version release";
}
@mixin background-size ($lengths...) {
@include prefixer(background-size, $lengths, spec);
@warn "background-size is deprecated and will be removed in the next major version release";
}

@ -0,0 +1,59 @@
// Custom Helpers
@import "helpers/deprecated-webkit-gradient";
@import "helpers/gradient-positions-parser";
@import "helpers/linear-positions-parser";
@import "helpers/radial-arg-parser";
@import "helpers/radial-positions-parser";
@import "helpers/render-gradients";
@import "helpers/shape-size-stripper";
// Custom Functions
@import "functions/compact";
@import "functions/flex-grid";
@import "functions/grid-width";
@import "functions/linear-gradient";
@import "functions/modular-scale";
@import "functions/px-to-em";
@import "functions/radial-gradient";
@import "functions/tint-shade";
@import "functions/transition-property-name";
// CSS3 Mixins
@import "css3/animation";
@import "css3/appearance";
@import "css3/backface-visibility";
@import "css3/background";
@import "css3/background-image";
@import "css3/border-image";
@import "css3/border-radius";
@import "css3/box-sizing";
@import "css3/columns";
@import "css3/flex-box";
@import "css3/font-face";
@import "css3/hidpi-media-query";
@import "css3/image-rendering";
@import "css3/inline-block";
@import "css3/keyframes";
@import "css3/linear-gradient";
@import "css3/perspective";
@import "css3/radial-gradient";
@import "css3/transform";
@import "css3/transition";
@import "css3/user-select";
@import "css3/placeholder";
// Addons & other mixins
@import "addons/button";
@import "addons/clearfix";
@import "addons/font-family";
@import "addons/hide-text";
@import "addons/html5-input-types";
@import "addons/position";
@import "addons/prefixer";
@import "addons/retina-image";
@import "addons/size";
@import "addons/timing-functions";
@import "addons/triangle";
// Soon to be deprecated Mixins
@import "bourbon-deprecated-upcoming";

@ -0,0 +1,273 @@
@mixin button ($style: simple, $base-color: #4294f0) {
@if type-of($style) == color {
$base-color: $style;
$style: simple;
}
// Grayscale button
@if $base-color == grayscale($base-color) {
@if $style == simple {
@include simple($base-color, $grayscale: true);
}
@else if $style == shiny {
@include shiny($base-color, $grayscale: true);
}
@else if $style == pill {
@include pill($base-color, $grayscale: true);
}
}
// Colored button
@else {
@if $style == simple {
@include simple($base-color);
}
@else if $style == shiny {
@include shiny($base-color);
}
@else if $style == pill {
@include pill($base-color);
}
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// Simple Button
//************************************************************************//
@mixin simple($base-color, $grayscale: false) {
$color: hsl(0, 0, 100%);
$border: adjust-color($base-color, $saturation: 9%, $lightness: -14%);
$inset-shadow: adjust-color($base-color, $saturation: -8%, $lightness: 15%);
$stop-gradient: adjust-color($base-color, $saturation: 9%, $lightness: -11%);
$text-shadow: adjust-color($base-color, $saturation: 15%, $lightness: -18%);
@if lightness($base-color) > 70% {
$color: hsl(0, 0, 20%);
$text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%);
}
@if $grayscale == true {
$border: grayscale($border);
$inset-shadow: grayscale($inset-shadow);
$stop-gradient: grayscale($stop-gradient);
$text-shadow: grayscale($text-shadow);
}
border: 1px solid $border;
border-radius: 3px;
box-shadow: inset 0 1px 0 0 $inset-shadow;
color: $color;
display: inline-block;
font-size: 11px;
font-weight: bold;
@include linear-gradient ($base-color, $stop-gradient);
padding: 7px 18px;
text-decoration: none;
text-shadow: 0 1px 0 $text-shadow;
background-clip: padding-box;
&:hover:not(:disabled) {
$base-color-hover: adjust-color($base-color, $saturation: -4%, $lightness: -5%);
$inset-shadow-hover: adjust-color($base-color, $saturation: -7%, $lightness: 5%);
$stop-gradient-hover: adjust-color($base-color, $saturation: 8%, $lightness: -14%);
@if $grayscale == true {
$base-color-hover: grayscale($base-color-hover);
$inset-shadow-hover: grayscale($inset-shadow-hover);
$stop-gradient-hover: grayscale($stop-gradient-hover);
}
box-shadow: inset 0 1px 0 0 $inset-shadow-hover;
cursor: pointer;
@include linear-gradient ($base-color-hover, $stop-gradient-hover);
}
&:active:not(:disabled) {
$border-active: adjust-color($base-color, $saturation: 9%, $lightness: -14%);
$inset-shadow-active: adjust-color($base-color, $saturation: 7%, $lightness: -17%);
@if $grayscale == true {
$border-active: grayscale($border-active);
$inset-shadow-active: grayscale($inset-shadow-active);
}
border: 1px solid $border-active;
box-shadow: inset 0 0 8px 4px $inset-shadow-active, inset 0 0 8px 4px $inset-shadow-active, 0 1px 1px 0 #eee;
}
}
// Shiny Button
//************************************************************************//
@mixin shiny($base-color, $grayscale: false) {
$color: hsl(0, 0, 100%);
$border: adjust-color($base-color, $red: -117, $green: -111, $blue: -81);
$border-bottom: adjust-color($base-color, $red: -126, $green: -127, $blue: -122);
$fourth-stop: adjust-color($base-color, $red: -79, $green: -70, $blue: -46);
$inset-shadow: adjust-color($base-color, $red: 37, $green: 29, $blue: 12);
$second-stop: adjust-color($base-color, $red: -56, $green: -50, $blue: -33);
$text-shadow: adjust-color($base-color, $red: -140, $green: -141, $blue: -114);
$third-stop: adjust-color($base-color, $red: -86, $green: -75, $blue: -48);
@if lightness($base-color) > 70% {
$color: hsl(0, 0, 20%);
$text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%);
}
@if $grayscale == true {
$border: grayscale($border);
$border-bottom: grayscale($border-bottom);
$fourth-stop: grayscale($fourth-stop);
$inset-shadow: grayscale($inset-shadow);
$second-stop: grayscale($second-stop);
$text-shadow: grayscale($text-shadow);
$third-stop: grayscale($third-stop);
}
border: 1px solid $border;
border-bottom: 1px solid $border-bottom;
border-radius: 5px;
box-shadow: inset 0 1px 0 0 $inset-shadow;
color: $color;
display: inline-block;
font-size: 14px;
font-weight: bold;
@include linear-gradient(top, $base-color 0%, $second-stop 50%, $third-stop 50%, $fourth-stop 100%);
padding: 8px 20px;
text-align: center;
text-decoration: none;
text-shadow: 0 -1px 1px $text-shadow;
&:hover:not(:disabled) {
$first-stop-hover: adjust-color($base-color, $red: -13, $green: -15, $blue: -18);
$second-stop-hover: adjust-color($base-color, $red: -66, $green: -62, $blue: -51);
$third-stop-hover: adjust-color($base-color, $red: -93, $green: -85, $blue: -66);
$fourth-stop-hover: adjust-color($base-color, $red: -86, $green: -80, $blue: -63);
@if $grayscale == true {
$first-stop-hover: grayscale($first-stop-hover);
$second-stop-hover: grayscale($second-stop-hover);
$third-stop-hover: grayscale($third-stop-hover);
$fourth-stop-hover: grayscale($fourth-stop-hover);
}
cursor: pointer;
@include linear-gradient(top, $first-stop-hover 0%,
$second-stop-hover 50%,
$third-stop-hover 50%,
$fourth-stop-hover 100%);
}
&:active:not(:disabled) {
$inset-shadow-active: adjust-color($base-color, $red: -111, $green: -116, $blue: -122);
@if $grayscale == true {
$inset-shadow-active: grayscale($inset-shadow-active);
}
box-shadow: inset 0 0 20px 0 $inset-shadow-active, 0 1px 0 #fff;
}
}
// Pill Button
//************************************************************************//
@mixin pill($base-color, $grayscale: false) {
$color: hsl(0, 0, 100%);
$border-bottom: adjust-color($base-color, $hue: 8, $saturation: -11%, $lightness: -26%);
$border-sides: adjust-color($base-color, $hue: 4, $saturation: -21%, $lightness: -21%);
$border-top: adjust-color($base-color, $hue: -1, $saturation: -30%, $lightness: -15%);
$inset-shadow: adjust-color($base-color, $hue: -1, $saturation: -1%, $lightness: 7%);
$stop-gradient: adjust-color($base-color, $hue: 8, $saturation: 14%, $lightness: -10%);
$text-shadow: adjust-color($base-color, $hue: 5, $saturation: -19%, $lightness: -15%);
@if lightness($base-color) > 70% {
$color: hsl(0, 0, 20%);
$text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%);
}
@if $grayscale == true {
$border-bottom: grayscale($border-bottom);
$border-sides: grayscale($border-sides);
$border-top: grayscale($border-top);
$inset-shadow: grayscale($inset-shadow);
$stop-gradient: grayscale($stop-gradient);
$text-shadow: grayscale($text-shadow);
}
border: 1px solid $border-top;
border-color: $border-top $border-sides $border-bottom;
border-radius: 16px;
box-shadow: inset 0 1px 0 0 $inset-shadow, 0 1px 2px 0 #b3b3b3;
color: $color;
display: inline-block;
font-size: 11px;
font-weight: normal;
line-height: 1;
@include linear-gradient ($base-color, $stop-gradient);
padding: 5px 16px;
text-align: center;
text-decoration: none;
text-shadow: 0 -1px 1px $text-shadow;
background-clip: padding-box;
&:hover:not(:disabled) {
$base-color-hover: adjust-color($base-color, $lightness: -4.5%);
$border-bottom: adjust-color($base-color, $hue: 8, $saturation: 13.5%, $lightness: -32%);
$border-sides: adjust-color($base-color, $hue: 4, $saturation: -2%, $lightness: -27%);
$border-top: adjust-color($base-color, $hue: -1, $saturation: -17%, $lightness: -21%);
$inset-shadow-hover: adjust-color($base-color, $saturation: -1%, $lightness: 3%);
$stop-gradient-hover: adjust-color($base-color, $hue: 8, $saturation: -4%, $lightness: -15.5%);
$text-shadow-hover: adjust-color($base-color, $hue: 5, $saturation: -5%, $lightness: -22%);
@if $grayscale == true {
$base-color-hover: grayscale($base-color-hover);
$border-bottom: grayscale($border-bottom);
$border-sides: grayscale($border-sides);
$border-top: grayscale($border-top);
$inset-shadow-hover: grayscale($inset-shadow-hover);
$stop-gradient-hover: grayscale($stop-gradient-hover);
$text-shadow-hover: grayscale($text-shadow-hover);
}
border: 1px solid $border-top;
border-color: $border-top $border-sides $border-bottom;
box-shadow: inset 0 1px 0 0 $inset-shadow-hover;
cursor: pointer;
@include linear-gradient ($base-color-hover, $stop-gradient-hover);
text-shadow: 0 -1px 1px $text-shadow-hover;
background-clip: padding-box;
}
&:active:not(:disabled) {
$active-color: adjust-color($base-color, $hue: 4, $saturation: -12%, $lightness: -10%);
$border-active: adjust-color($base-color, $hue: 6, $saturation: -2.5%, $lightness: -30%);
$border-bottom-active: adjust-color($base-color, $hue: 11, $saturation: 6%, $lightness: -31%);
$inset-shadow-active: adjust-color($base-color, $hue: 9, $saturation: 2%, $lightness: -21.5%);
$text-shadow-active: adjust-color($base-color, $hue: 5, $saturation: -12%, $lightness: -21.5%);
@if $grayscale == true {
$active-color: grayscale($active-color);
$border-active: grayscale($border-active);
$border-bottom-active: grayscale($border-bottom-active);
$inset-shadow-active: grayscale($inset-shadow-active);
$text-shadow-active: grayscale($text-shadow-active);
}
background: $active-color;
border: 1px solid $border-active;
border-bottom: 1px solid $border-bottom-active;
box-shadow: inset 0 0 6px 3px $inset-shadow-active, 0 1px 0 0 #fff;
text-shadow: 0 -1px 1px $text-shadow-active;
}
}

@ -0,0 +1,29 @@
// Micro clearfix provides an easy way to contain floats without adding additional markup
//
// Example usage:
//
// // Contain all floats within .wrapper
// .wrapper {
// @include clearfix;
// .content,
// .sidebar {
// float : left;
// }
// }
@mixin clearfix {
*zoom: 1;
&:before,
&:after {
content: " ";
display: table;
}
&:after {
clear: both;
}
}
// Acknowledgements
// Micro clearfix: [Nicolas Gallagher](http://nicolasgallagher.com/micro-clearfix-hack/)

@ -0,0 +1,5 @@
$georgia: Georgia, Cambria, "Times New Roman", Times, serif;
$helvetica: "Helvetica Neue", Helvetica, Arial, sans-serif;
$lucida-grande: "Lucida Grande", Tahoma, Verdana, Arial, sans-serif;
$monospace: "Bitstream Vera Sans Mono", Consolas, Courier, monospace;
$verdana: Verdana, Geneva, sans-serif;

@ -0,0 +1,5 @@
@mixin hide-text {
color: transparent;
font: 0/0 a;
text-shadow: none;
}

@ -0,0 +1,56 @@
//************************************************************************//
// Generate a variable ($all-text-inputs) with a list of all html5
// input types that have a text-based input, excluding textarea.
// http://diveintohtml5.org/forms.html
//************************************************************************//
$inputs-list: 'input[type="email"]',
'input[type="number"]',
'input[type="password"]',
'input[type="search"]',
'input[type="tel"]',
'input[type="text"]',
'input[type="url"]',
// Webkit & Gecko may change the display of these in the future
'input[type="color"]',
'input[type="date"]',
'input[type="datetime"]',
'input[type="datetime-local"]',
'input[type="month"]',
'input[type="time"]',
'input[type="week"]';
$unquoted-inputs-list: ();
@each $input-type in $inputs-list {
$unquoted-inputs-list: append($unquoted-inputs-list, unquote($input-type), comma);
}
$all-text-inputs: $unquoted-inputs-list;
// Hover Pseudo-class
//************************************************************************//
$all-text-inputs-hover: ();
@each $input-type in $unquoted-inputs-list {
$input-type-hover: $input-type + ":hover";
$all-text-inputs-hover: append($all-text-inputs-hover, $input-type-hover, comma);
}
// Focus Pseudo-class
//************************************************************************//
$all-text-inputs-focus: ();
@each $input-type in $unquoted-inputs-list {
$input-type-focus: $input-type + ":focus";
$all-text-inputs-focus: append($all-text-inputs-focus, $input-type-focus, comma);
}
// You must use interpolation on the variable:
// #{$all-text-inputs}
// #{$all-text-inputs-hover}
// #{$all-text-inputs-focus}
// Example
//************************************************************************//
// #{$all-text-inputs}, textarea {
// border: 1px solid red;
// }

@ -0,0 +1,42 @@
@mixin position ($position: relative, $coordinates: 0 0 0 0) {
@if type-of($position) == list {
$coordinates: $position;
$position: relative;
}
$top: nth($coordinates, 1);
$right: nth($coordinates, 2);
$bottom: nth($coordinates, 3);
$left: nth($coordinates, 4);
position: $position;
@if $top == auto {
top: $top;
}
@else if not(unitless($top)) {
top: $top;
}
@if $right == auto {
right: $right;
}
@else if not(unitless($right)) {
right: $right;
}
@if $bottom == auto {
bottom: $bottom;
}
@else if not(unitless($bottom)) {
bottom: $bottom;
}
@if $left == auto {
left: $left;
}
@else if not(unitless($left)) {
left: $left;
}
}

@ -0,0 +1,49 @@
//************************************************************************//
// Example: @include prefixer(border-radius, $radii, webkit ms spec);
//************************************************************************//
$prefix-for-webkit: true !default;
$prefix-for-mozilla: true !default;
$prefix-for-microsoft: true !default;
$prefix-for-opera: true !default;
$prefix-for-spec: true !default; // required for keyframe mixin
@mixin prefixer ($property, $value, $prefixes) {
@each $prefix in $prefixes {
@if $prefix == webkit {
@if $prefix-for-webkit {
-webkit-#{$property}: $value;
}
}
@else if $prefix == moz {
@if $prefix-for-mozilla {
-moz-#{$property}: $value;
}
}
@else if $prefix == ms {
@if $prefix-for-microsoft {
-ms-#{$property}: $value;
}
}
@else if $prefix == o {
@if $prefix-for-opera {
-o-#{$property}: $value;
}
}
@else if $prefix == spec {
@if $prefix-for-spec {
#{$property}: $value;
}
}
@else {
@warn "Unrecognized prefix: #{$prefix}";
}
}
}
@mixin disable-prefix-for-all() {
$prefix-for-webkit: false;
$prefix-for-mozilla: false;
$prefix-for-microsoft: false;
$prefix-for-opera: false;
$prefix-for-spec: false;
}

@ -0,0 +1,32 @@
@mixin retina-image($filename, $background-size, $extension: png, $retina-filename: null, $asset-pipeline: false) {
@if $asset-pipeline {
background-image: image-url("#{$filename}.#{$extension}");
}
@else {
background-image: url("#{$filename}.#{$extension}");
}
@include hidpi {
@if $asset-pipeline {
@if $retina-filename {
background-image: image-url("#{$retina-filename}.#{$extension}");
}
@else {
background-image: image-url("#{$filename}@2x.#{$extension}");
}
}
@else {
@if $retina-filename {
background-image: url("#{$retina-filename}.#{$extension}");
}
@else {
background-image: url("#{$filename}@2x.#{$extension}");
}
}
background-size: $background-size;
}
}

@ -0,0 +1,44 @@
@mixin size($size) {
@if length($size) == 1 {
@if $size == auto {
width: $size;
height: $size;
}
@else if unitless($size) {
width: $size + px;
height: $size + px;
}
@else if not(unitless($size)) {
width: $size;
height: $size;
}
}
// Width x Height
@if length($size) == 2 {
$width: nth($size, 1);
$height: nth($size, 2);
@if $width == auto {
width: $width;
}
@else if not(unitless($width)) {
width: $width;
}
@else if unitless($width) {
width: $width + px;
}
@if $height == auto {
height: $height;
}
@else if not(unitless($height)) {
height: $height;
}
@else if unitless($height) {
height: $height + px;
}
}
}

@ -0,0 +1,32 @@
// CSS cubic-bezier timing functions. Timing functions courtesy of jquery.easie (github.com/jaukia/easie)
// Timing functions are the same as demo'ed here: http://jqueryui.com/demos/effect/easing.html
// EASE IN
$ease-in-quad: cubic-bezier(0.550, 0.085, 0.680, 0.530);
$ease-in-cubic: cubic-bezier(0.550, 0.055, 0.675, 0.190);
$ease-in-quart: cubic-bezier(0.895, 0.030, 0.685, 0.220);
$ease-in-quint: cubic-bezier(0.755, 0.050, 0.855, 0.060);
$ease-in-sine: cubic-bezier(0.470, 0.000, 0.745, 0.715);
$ease-in-expo: cubic-bezier(0.950, 0.050, 0.795, 0.035);
$ease-in-circ: cubic-bezier(0.600, 0.040, 0.980, 0.335);
$ease-in-back: cubic-bezier(0.600, -0.280, 0.735, 0.045);
// EASE OUT
$ease-out-quad: cubic-bezier(0.250, 0.460, 0.450, 0.940);
$ease-out-cubic: cubic-bezier(0.215, 0.610, 0.355, 1.000);
$ease-out-quart: cubic-bezier(0.165, 0.840, 0.440, 1.000);
$ease-out-quint: cubic-bezier(0.230, 1.000, 0.320, 1.000);
$ease-out-sine: cubic-bezier(0.390, 0.575, 0.565, 1.000);
$ease-out-expo: cubic-bezier(0.190, 1.000, 0.220, 1.000);
$ease-out-circ: cubic-bezier(0.075, 0.820, 0.165, 1.000);
$ease-out-back: cubic-bezier(0.175, 0.885, 0.320, 1.275);
// EASE IN OUT
$ease-in-out-quad: cubic-bezier(0.455, 0.030, 0.515, 0.955);
$ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1.000);
$ease-in-out-quart: cubic-bezier(0.770, 0.000, 0.175, 1.000);
$ease-in-out-quint: cubic-bezier(0.860, 0.000, 0.070, 1.000);
$ease-in-out-sine: cubic-bezier(0.445, 0.050, 0.550, 0.950);
$ease-in-out-expo: cubic-bezier(1.000, 0.000, 0.000, 1.000);
$ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.150, 0.860);
$ease-in-out-back: cubic-bezier(0.680, -0.550, 0.265, 1.550);

@ -0,0 +1,45 @@
@mixin triangle ($size, $color, $direction) {
height: 0;
width: 0;
@if ($direction == up) or ($direction == down) or ($direction == right) or ($direction == left) {
border-color: transparent;
border-style: solid;
border-width: $size / 2;
@if $direction == up {
border-bottom-color: $color;
} @else if $direction == right {
border-left-color: $color;
} @else if $direction == down {
border-top-color: $color;
} @else if $direction == left {
border-right-color: $color;
}
}
@else if ($direction == up-right) or ($direction == up-left) {
border-top: $size solid $color;
@if $direction == up-right {
border-left: $size solid transparent;
} @else if $direction == up-left {
border-right: $size solid transparent;
}
}
@else if ($direction == down-right) or ($direction == down-left) {
border-bottom: $size solid $color;
@if $direction == down-right {
border-left: $size solid transparent;
} @else if $direction == down-left {
border-right: $size solid transparent;
}
}
}

@ -0,0 +1,52 @@
// http://www.w3.org/TR/css3-animations/#the-animation-name-property-
// Each of these mixins support comma separated lists of values, which allows different transitions for individual properties to be described in a single style rule. Each value in the list corresponds to the value at that same position in the other properties.
// Official animation shorthand property.
@mixin animation ($animations...) {
@include prefixer(animation, $animations, webkit moz spec);
}
// Individual Animation Properties
@mixin animation-name ($names...) {
@include prefixer(animation-name, $names, webkit moz spec);
}
@mixin animation-duration ($times...) {
@include prefixer(animation-duration, $times, webkit moz spec);
}
@mixin animation-timing-function ($motions...) {
// ease | linear | ease-in | ease-out | ease-in-out
@include prefixer(animation-timing-function, $motions, webkit moz spec);
}
@mixin animation-iteration-count ($values...) {
// infinite | <number>
@include prefixer(animation-iteration-count, $values, webkit moz spec);
}
@mixin animation-direction ($directions...) {
// normal | alternate
@include prefixer(animation-direction, $directions, webkit moz spec);
}
@mixin animation-play-state ($states...) {
// running | paused
@include prefixer(animation-play-state, $states, webkit moz spec);
}
@mixin animation-delay ($times...) {
@include prefixer(animation-delay, $times, webkit moz spec);
}
@mixin animation-fill-mode ($modes...) {
// none | forwards | backwards | both
@include prefixer(animation-fill-mode, $modes, webkit moz spec);
}

@ -0,0 +1,3 @@
@mixin appearance ($value) {
@include prefixer(appearance, $value, webkit moz ms o spec);
}

@ -0,0 +1,6 @@
//************************************************************************//
// Backface-visibility mixin
//************************************************************************//
@mixin backface-visibility($visibility) {
@include prefixer(backface-visibility, $visibility, webkit spec);
}

@ -0,0 +1,48 @@
//************************************************************************//
// Background-image property for adding multiple background images with
// gradients, or for stringing multiple gradients together.
//************************************************************************//
@mixin background-image($images...) {
background-image: _add-prefix($images, webkit);
background-image: _add-prefix($images);
}
@function _add-prefix($images, $vendor: false) {
$images-prefixed: ();
$gradient-positions: false;
@for $i from 1 through length($images) {
$type: type-of(nth($images, $i)); // Get type of variable - List or String
// If variable is a list - Gradient
@if $type == list {
$gradient-type: nth(nth($images, $i), 1); // linear or radial
$gradient-pos: null;
$gradient-args: null;
@if ($gradient-type == linear) or ($gradient-type == radial) {
$gradient-pos: nth(nth($images, $i), 2); // Get gradient position
$gradient-args: nth(nth($images, $i), 3); // Get actual gradient (red, blue)
}
@else {
$gradient-args: nth(nth($images, $i), 2); // Get actual gradient (red, blue)
}
$gradient-positions: _gradient-positions-parser($gradient-type, $gradient-pos);
$gradient: _render-gradients($gradient-positions, $gradient-args, $gradient-type, $vendor);
$images-prefixed: append($images-prefixed, $gradient, comma);
}
// If variable is a string - Image
@else if $type == string {
$images-prefixed: join($images-prefixed, nth($images, $i), comma);
}
}
@return $images-prefixed;
}
//Examples:
//@include background-image(linear-gradient(top, orange, red));
//@include background-image(radial-gradient(50% 50%, cover circle, orange, red));
//@include background-image(url("/images/a.png"), linear-gradient(orange, red));
//@include background-image(url("image.png"), linear-gradient(orange, red), url("image.png"));
//@include background-image(linear-gradient(hsla(0, 100%, 100%, 0.25) 0%, hsla(0, 100%, 100%, 0.08) 50%, transparent 50%), linear-gradient(orange, red));

@ -0,0 +1,103 @@
//************************************************************************//
// Background property for adding multiple backgrounds using shorthand
// notation.
//************************************************************************//
@mixin background(
$background-1 , $background-2: false,
$background-3: false, $background-4: false,
$background-5: false, $background-6: false,
$background-7: false, $background-8: false,
$background-9: false, $background-10: false,
$fallback: false
) {
$backgrounds: compact($background-1, $background-2,
$background-3, $background-4,
$background-5, $background-6,
$background-7, $background-8,
$background-9, $background-10);
$fallback-color: false;
@if (type-of($fallback) == color) or ($fallback == "transparent") {
$fallback-color: $fallback;
}
@else {
$fallback-color: _extract-background-color($backgrounds);
}
@if $fallback-color {
background-color: $fallback-color;
}
background: _background-add-prefix($backgrounds, webkit);
background: _background-add-prefix($backgrounds);
}
@function _extract-background-color($backgrounds) {
$final-bg-layer: nth($backgrounds, length($backgrounds));
@if type-of($final-bg-layer) == list {
@for $i from 1 through length($final-bg-layer) {
$value: nth($final-bg-layer, $i);
@if type-of($value) == color {
@return $value;
}
}
}
@return false;
}
@function _background-add-prefix($backgrounds, $vendor: false) {
$backgrounds-prefixed: ();
@for $i from 1 through length($backgrounds) {
$shorthand: nth($backgrounds, $i); // Get member for current index
$type: type-of($shorthand); // Get type of variable - List (gradient) or String (image)
// If shorthand is a list (gradient)
@if $type == list {
$first-member: nth($shorthand, 1); // Get first member of shorthand
// Linear Gradient
@if index(linear radial, nth($first-member, 1)) {
$gradient-type: nth($first-member, 1); // linear || radial
$gradient-args: false;
$gradient-positions: false;
$shorthand-start: false;
@if type-of($first-member) == list { // Linear gradient plus additional shorthand values - lg(red,orange)repeat,...
$gradient-positions: nth($first-member, 2);
$gradient-args: nth($first-member, 3);
$shorthand-start: 2;
}
@else { // Linear gradient only - lg(red,orange),...
$gradient-positions: nth($shorthand, 2);
$gradient-args: nth($shorthand, 3); // Get gradient (red, blue)
}
$gradient-positions: _gradient-positions-parser($gradient-type, $gradient-positions);
$gradient: _render-gradients($gradient-positions, $gradient-args, $gradient-type, $vendor);
// Append any additional shorthand args to gradient
@if $shorthand-start {
@for $j from $shorthand-start through length($shorthand) {
$gradient: join($gradient, nth($shorthand, $j), space);
}
}
$backgrounds-prefixed: append($backgrounds-prefixed, $gradient, comma);
}
// Image with additional properties
@else {
$backgrounds-prefixed: append($backgrounds-prefixed, $shorthand, comma);
}
}
// If shorthand is a simple string (color or image)
@else if $type == string {
$backgrounds-prefixed: join($backgrounds-prefixed, $shorthand, comma);
}
}
@return $backgrounds-prefixed;
}
//Examples:
//@include background(linear-gradient(top, orange, red));
//@include background(radial-gradient(circle at 40% 40%, orange, red));
//@include background(url("/images/a.png") no-repeat, linear-gradient(orange, red));
//@include background(url("image.png") center center, linear-gradient(orange, red), url("image.png"));

@ -0,0 +1,55 @@
@mixin border-image($images) {
-webkit-border-image: _border-add-prefix($images, webkit);
-moz-border-image: _border-add-prefix($images, moz);
-o-border-image: _border-add-prefix($images, o);
border-image: _border-add-prefix($images);
}
@function _border-add-prefix($images, $vendor: false) {
$border-image: null;
$images-type: type-of(nth($images, 1));
$first-var: nth(nth($images, 1), 1); // Get type of Gradient (Linear || radial)
// If input is a gradient
@if $images-type == string {
@if ($first-var == "linear") or ($first-var == "radial") {
$gradient-type: nth($images, 1); // Get type of gradient (linear || radial)
$gradient-pos: nth($images, 2); // Get gradient position
$gradient-args: nth($images, 3); // Get actual gradient (red, blue)
$gradient-positions: _gradient-positions-parser($gradient-type, $gradient-pos);
$border-image: _render-gradients($gradient-positions, $gradient-args, $gradient-type, $vendor);
}
// If input is a URL
@else {
$border-image: $images;
}
}
// If input is gradient or url + additional args
@else if $images-type == list {
$type: type-of(nth($images, 1)); // Get type of variable - List or String
// If variable is a list - Gradient
@if $type == list {
$gradient: nth($images, 1);
$gradient-type: nth($gradient, 1); // Get type of gradient (linear || radial)
$gradient-pos: nth($gradient, 2); // Get gradient position
$gradient-args: nth($gradient, 3); // Get actual gradient (red, blue)
$gradient-positions: _gradient-positions-parser($gradient-type, $gradient-pos);
$border-image: _render-gradients($gradient-positions, $gradient-args, $gradient-type, $vendor);
@for $i from 2 through length($images) {
$border-image: append($border-image, nth($images, $i));
}
}
}
@return $border-image;
}
//Examples:
// @include border-image(url("image.png"));
// @include border-image(url("image.png") 20 stretch);
// @include border-image(linear-gradient(45deg, orange, yellow));
// @include border-image(linear-gradient(45deg, orange, yellow) stretch);
// @include border-image(linear-gradient(45deg, orange, yellow) 20 30 40 50 stretch round);
// @include border-image(radial-gradient(top, cover, orange, yellow, orange));

@ -0,0 +1,22 @@
//************************************************************************//
// Shorthand Border-radius mixins
//************************************************************************//
@mixin border-top-radius($radii) {
@include prefixer(border-top-left-radius, $radii, spec);
@include prefixer(border-top-right-radius, $radii, spec);
}
@mixin border-bottom-radius($radii) {
@include prefixer(border-bottom-left-radius, $radii, spec);
@include prefixer(border-bottom-right-radius, $radii, spec);
}
@mixin border-left-radius($radii) {
@include prefixer(border-top-left-radius, $radii, spec);
@include prefixer(border-bottom-left-radius, $radii, spec);
}
@mixin border-right-radius($radii) {
@include prefixer(border-top-right-radius, $radii, spec);
@include prefixer(border-bottom-right-radius, $radii, spec);
}

@ -0,0 +1,4 @@
@mixin box-sizing ($box) {
// content-box | border-box | inherit
@include prefixer(box-sizing, $box, webkit moz spec);
}

@ -0,0 +1,47 @@
@mixin columns($arg: auto) {
// <column-count> || <column-width>
@include prefixer(columns, $arg, webkit moz spec);
}
@mixin column-count($int: auto) {
// auto || integer
@include prefixer(column-count, $int, webkit moz spec);
}
@mixin column-gap($length: normal) {
// normal || length
@include prefixer(column-gap, $length, webkit moz spec);
}
@mixin column-fill($arg: auto) {
// auto || length
@include prefixer(columns-fill, $arg, webkit moz spec);
}
@mixin column-rule($arg) {
// <border-width> || <border-style> || <color>
@include prefixer(column-rule, $arg, webkit moz spec);
}
@mixin column-rule-color($color) {
@include prefixer(column-rule-color, $color, webkit moz spec);
}
@mixin column-rule-style($style: none) {
// none | hidden | dashed | dotted | double | groove | inset | inset | outset | ridge | solid
@include prefixer(column-rule-style, $style, webkit moz spec);
}
@mixin column-rule-width ($width: none) {
@include prefixer(column-rule-width, $width, webkit moz spec);
}
@mixin column-span($arg: none) {
// none || all
@include prefixer(column-span, $arg, webkit moz spec);
}
@mixin column-width($length: auto) {
// auto || length
@include prefixer(column-width, $length, webkit moz spec);
}

@ -0,0 +1,52 @@
// CSS3 Flexible Box Model and property defaults
// Custom shorthand notation for flexbox
@mixin box($orient: inline-axis, $pack: start, $align: stretch) {
@include display-box;
@include box-orient($orient);
@include box-pack($pack);
@include box-align($align);
}
@mixin display-box {
display: -webkit-box;
display: -moz-box;
display: box;
}
@mixin box-orient($orient: inline-axis) {
// horizontal|vertical|inline-axis|block-axis|inherit
@include prefixer(box-orient, $orient, webkit moz spec);
}
@mixin box-pack($pack: start) {
// start|end|center|justify
@include prefixer(box-pack, $pack, webkit moz spec);
}
@mixin box-align($align: stretch) {
// start|end|center|baseline|stretch
@include prefixer(box-align, $align, webkit moz spec);
}
@mixin box-direction($direction: normal) {
// normal|reverse|inherit
@include prefixer(box-direction, $direction, webkit moz spec);
}
@mixin box-lines($lines: single) {
// single|multiple
@include prefixer(box-lines, $lines, webkit moz spec);
}
@mixin box-ordinal-group($int: 1) {
@include prefixer(box-ordinal-group, $int, webkit moz spec);
}
@mixin box-flex($value: 0.0) {
@include prefixer(box-flex, $value, webkit moz spec);
}
@mixin box-flex-group($int: 1) {
@include prefixer(box-flex-group, $int, webkit moz spec);
}

@ -0,0 +1,23 @@
// Order of the includes matters, and it is: normal, bold, italic, bold+italic.
@mixin font-face($font-family, $file-path, $weight: normal, $style: normal, $asset-pipeline: false ) {
@font-face {
font-family: $font-family;
font-weight: $weight;
font-style: $style;
@if $asset-pipeline == true {
src: font-url('#{$file-path}.eot');
src: font-url('#{$file-path}.eot?#iefix') format('embedded-opentype'),
font-url('#{$file-path}.woff') format('woff'),
font-url('#{$file-path}.ttf') format('truetype'),
font-url('#{$file-path}.svg##{$font-family}') format('svg');
} @else {
src: url('#{$file-path}.eot');
src: url('#{$file-path}.eot?#iefix') format('embedded-opentype'),
url('#{$file-path}.woff') format('woff'),
url('#{$file-path}.ttf') format('truetype'),
url('#{$file-path}.svg##{$font-family}') format('svg');
}
}
}

@ -0,0 +1,10 @@
// HiDPI mixin. Default value set to 1.3 to target Google Nexus 7 (http://bjango.com/articles/min-device-pixel-ratio/)
@mixin hidpi($ratio: 1.3) {
@media only screen and (-webkit-min-device-pixel-ratio: $ratio),
only screen and (min--moz-device-pixel-ratio: $ratio),
only screen and (-o-min-device-pixel-ratio: #{$ratio}/1),
only screen and (min-resolution: #{round($ratio*96)}dpi),
only screen and (min-resolution: #{$ratio}dppx) {
@content;
}
}

@ -0,0 +1,13 @@
@mixin image-rendering ($mode:optimizeQuality) {
@if ($mode == optimize-contrast) {
image-rendering: -moz-crisp-edges;
image-rendering: -o-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: optimize-contrast;
}
@else {
image-rendering: $mode;
}
}

@ -0,0 +1,8 @@
// Legacy support for inline-block in IE7 (maybe IE6)
@mixin inline-block {
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
}

@ -0,0 +1,43 @@
// Adds keyframes blocks for supported prefixes, removing redundant prefixes in the block's content
@mixin keyframes($name) {
$original-prefix-for-webkit: $prefix-for-webkit;
$original-prefix-for-mozilla: $prefix-for-mozilla;
$original-prefix-for-microsoft: $prefix-for-microsoft;
$original-prefix-for-opera: $prefix-for-opera;
$original-prefix-for-spec: $prefix-for-spec;
@if $original-prefix-for-webkit {
@include disable-prefix-for-all();
$prefix-for-webkit: true;
@-webkit-keyframes #{$name} {
@content;
}
}
@if $original-prefix-for-mozilla {
@include disable-prefix-for-all();
$prefix-for-mozilla: true;
@-moz-keyframes #{$name} {
@content;
}
}
@if $original-prefix-for-opera {
@include disable-prefix-for-all();
$prefix-for-opera: true;
@-o-keyframes #{$name} {
@content;
}
}
@if $original-prefix-for-spec {
@include disable-prefix-for-all();
$prefix-for-spec: true;
@keyframes #{$name} {
@content;
}
}
$prefix-for-webkit: $original-prefix-for-webkit;
$prefix-for-mozilla: $original-prefix-for-mozilla;
$prefix-for-microsoft: $original-prefix-for-microsoft;
$prefix-for-opera: $original-prefix-for-opera;
$prefix-for-spec: $original-prefix-for-spec;
}

@ -0,0 +1,41 @@
@mixin linear-gradient($pos, $G1, $G2: false,
$G3: false, $G4: false,
$G5: false, $G6: false,
$G7: false, $G8: false,
$G9: false, $G10: false,
$deprecated-pos1: left top,
$deprecated-pos2: left bottom,
$fallback: false) {
// Detect what type of value exists in $pos
$pos-type: type-of(nth($pos, 1));
$pos-spec: null;
$pos-degree: null;
// If $pos is missing from mixin, reassign vars and add default position
@if ($pos-type == color) or (nth($pos, 1) == "transparent") {
$G10: $G9; $G9: $G8; $G8: $G7; $G7: $G6; $G6: $G5;
$G5: $G4; $G4: $G3; $G3: $G2; $G2: $G1; $G1: $pos;
$pos: null;
}
@if $pos {
$positions: _linear-positions-parser($pos);
$pos-degree: nth($positions, 1);
$pos-spec: nth($positions, 2);
}
$full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10);
// Set $G1 as the default fallback color
$fallback-color: nth($G1, 1);
// If $fallback is a color use that color as the fallback color
@if (type-of($fallback) == color) or ($fallback == "transparent") {
$fallback-color: $fallback;
}
background-color: $fallback-color;
background-image: _deprecated-webkit-gradient(linear, $deprecated-pos1, $deprecated-pos2, $full); // Safari <= 5.0
background-image: -webkit-linear-gradient($pos-degree $full); // Safari 5.1+, Chrome
background-image: unquote("linear-gradient(#{$pos-spec}#{$full})");
}

@ -0,0 +1,8 @@
@mixin perspective($depth: none) {
// none | <length>
@include prefixer(perspective, $depth, webkit moz spec);
}
@mixin perspective-origin($value: 50% 50%) {
@include prefixer(perspective-origin, $value, webkit moz spec);
}

@ -0,0 +1,29 @@
$placeholders: '-webkit-input-placeholder',
'-moz-placeholder',
'-ms-input-placeholder';
@mixin placeholder {
@each $placeholder in $placeholders {
@if $placeholder == "-webkit-input-placeholder" {
&::#{$placeholder} {
@content;
}
}
@else if $placeholder == "-moz-placeholder" {
// FF 18-
&:#{$placeholder} {
@content;
}
// FF 19+
&::#{$placeholder} {
@content;
}
}
@else {
&:#{$placeholder} {
@content;
}
}
}
}

@ -0,0 +1,44 @@
// Requires Sass 3.1+
@mixin radial-gradient($G1, $G2,
$G3: false, $G4: false,
$G5: false, $G6: false,
$G7: false, $G8: false,
$G9: false, $G10: false,
$pos: null,
$shape-size: null,
$deprecated-pos1: center center,
$deprecated-pos2: center center,
$deprecated-radius1: 0,
$deprecated-radius2: 460,
$fallback: false) {
$data: _radial-arg-parser($G1, $G2, $pos, $shape-size);
$G1: nth($data, 1);
$G2: nth($data, 2);
$pos: nth($data, 3);
$shape-size: nth($data, 4);
$full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10);
// Strip deprecated cover/contain for spec
$shape-size-spec: _shape-size-stripper($shape-size);
// Set $G1 as the default fallback color
$first-color: nth($full, 1);
$fallback-color: nth($first-color, 1);
@if (type-of($fallback) == color) or ($fallback == "transparent") {
$fallback-color: $fallback;
}
// Add Commas and spaces
$shape-size: if($shape-size, '#{$shape-size}, ', null);
$pos: if($pos, '#{$pos}, ', null);
$pos-spec: if($pos, 'at #{$pos}', null);
$shape-size-spec: if(($shape-size-spec != ' ') and ($pos == null), '#{$shape-size-spec}, ', '#{$shape-size-spec} ');
background-color: $fallback-color;
background-image: _deprecated-webkit-gradient(radial, $deprecated-pos1, $deprecated-pos2, $full, $deprecated-radius1, $deprecated-radius2); // Safari <= 5.0 && IOS 4
background-image: -webkit-radial-gradient(unquote(#{$pos}#{$shape-size}#{$full}));
background-image: unquote("radial-gradient(#{$shape-size-spec}#{$pos-spec}#{$full})");
}

@ -0,0 +1,15 @@
@mixin transform($property: none) {
// none | <transform-function>
@include prefixer(transform, $property, webkit moz ms o spec);
}
@mixin transform-origin($axes: 50%) {
// x-axis - left | center | right | length | %
// y-axis - top | center | bottom | length | %
// z-axis - length
@include prefixer(transform-origin, $axes, webkit moz ms o spec);
}
@mixin transform-style ($style: flat) {
@include prefixer(transform-style, $style, webkit moz ms o spec);
}

@ -0,0 +1,34 @@
// Shorthand mixin. Supports multiple parentheses-deliminated values for each variable.
// Example: @include transition (all, 2.0s, ease-in-out);
// @include transition ((opacity, width), (1.0s, 2.0s), ease-in, (0, 2s));
// @include transition ($property:(opacity, width), $delay: (1.5s, 2.5s));
@mixin transition ($properties...) {
@if length($properties) >= 1 {
@include prefixer(transition, $properties, webkit moz spec);
}
@else {
$properties: all 0.15s ease-out 0;
@include prefixer(transition, $properties, webkit moz spec);
}
}
@mixin transition-property ($properties...) {
-webkit-transition-property: transition-property-names($properties, 'webkit');
-moz-transition-property: transition-property-names($properties, 'moz');
transition-property: transition-property-names($properties, false);
}
@mixin transition-duration ($times...) {
@include prefixer(transition-duration, $times, webkit moz spec);
}
@mixin transition-timing-function ($motions...) {
// ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier()
@include prefixer(transition-timing-function, $motions, webkit moz spec);
}
@mixin transition-delay ($times...) {
@include prefixer(transition-delay, $times, webkit moz spec);
}

@ -0,0 +1,3 @@
@mixin user-select($arg: none) {
@include prefixer(user-select, $arg, webkit moz ms spec);
}

@ -0,0 +1,11 @@
// Remove `false` values from a list
@function compact($vars...) {
$list: ();
@each $var in $vars {
@if $var {
$list: append($list, $var, comma);
}
}
@return $list;
}

@ -0,0 +1,39 @@
// Flexible grid
@function flex-grid($columns, $container-columns: $fg-max-columns) {
$width: $columns * $fg-column + ($columns - 1) * $fg-gutter;
$container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter;
@return percentage($width / $container-width);
}
// Flexible gutter
@function flex-gutter($container-columns: $fg-max-columns, $gutter: $fg-gutter) {
$container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter;
@return percentage($gutter / $container-width);
}
// The $fg-column, $fg-gutter and $fg-max-columns variables must be defined in your base stylesheet to properly use the flex-grid function.
// This function takes the fluid grid equation (target / context = result) and uses columns to help define each.
//
// The calculation presumes that your column structure will be missing the last gutter:
//
// -- column -- gutter -- column -- gutter -- column
//
// $fg-column: 60px; // Column Width
// $fg-gutter: 25px; // Gutter Width
// $fg-max-columns: 12; // Total Columns For Main Container
//
// div {
// width: flex-grid(4); // returns (315px / 995px) = 31.65829%;
// margin-left: flex-gutter(); // returns (25px / 995px) = 2.51256%;
//
// p {
// width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%;
// float: left;
// margin: flex-gutter(4); // returns (25px / 315px) = 7.936508%;
// }
//
// blockquote {
// float: left;
// width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%;
// }
// }

@ -0,0 +1,13 @@
@function grid-width($n) {
@return $n * $gw-column + ($n - 1) * $gw-gutter;
}
// The $gw-column and $gw-gutter variables must be defined in your base stylesheet to properly use the grid-width function.
//
// $gw-column: 100px; // Column Width
// $gw-gutter: 40px; // Gutter Width
//
// div {
// width: grid-width(4); // returns 520px;
// margin-left: $gw-gutter; // returns 40px;
// }

@ -0,0 +1,13 @@
@function linear-gradient($pos, $gradients...) {
$type: linear;
$pos-type: type-of(nth($pos, 1));
// if $pos doesn't exist, fix $gradient
@if ($pos-type == color) or (nth($pos, 1) == "transparent") {
$gradients: zip($pos $gradients);
$pos: false;
}
$type-gradient: $type, $pos, $gradients;
@return $type-gradient;
}

@ -0,0 +1,40 @@
@function modular-scale($value, $increment, $ratio) {
@if $increment > 0 {
@for $i from 1 through $increment {
$value: ($value * $ratio);
}
}
@if $increment < 0 {
$increment: abs($increment);
@for $i from 1 through $increment {
$value: ($value / $ratio);
}
}
@return $value;
}
// div {
// Increment Up GR with positive value
// font-size: modular-scale(14px, 1, 1.618); // returns: 22.652px
//
// Increment Down GR with negative value
// font-size: modular-scale(14px, -1, 1.618); // returns: 8.653px
//
// Can be used with ceil(round up) or floor(round down)
// font-size: floor( modular-scale(14px, 1, 1.618) ); // returns: 22px
// font-size: ceil( modular-scale(14px, 1, 1.618) ); // returns: 23px
// }
//
// modularscale.com
@function golden-ratio($value, $increment) {
@return modular-scale($value, $increment, 1.618)
}
// div {
// font-size: golden-ratio(14px, 1); // returns: 22.652px
// }
//
// goldenratiocalculator.com

@ -0,0 +1,8 @@
// Convert pixels to ems
// eg. for a relational value of 12px write em(12) when the parent is 16px
// if the parent is another value say 24px write em(12, 24)
@function em($pxval, $base: 16) {
@return ($pxval / $base) * 1em;
}

@ -0,0 +1,23 @@
// This function is required and used by the background-image mixin.
@function radial-gradient($G1, $G2,
$G3: false, $G4: false,
$G5: false, $G6: false,
$G7: false, $G8: false,
$G9: false, $G10: false,
$pos: null,
$shape-size: null) {
$data: _radial-arg-parser($G1, $G2, $pos, $shape-size);
$G1: nth($data, 1);
$G2: nth($data, 2);
$pos: nth($data, 3);
$shape-size: nth($data, 4);
$type: radial;
$gradient: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10);
$type-gradient: $type, $shape-size $pos, $gradient;
@return $type-gradient;
}

@ -0,0 +1,9 @@
// Add percentage of white to a color
@function tint($color, $percent){
@return mix(white, $color, $percent);
}
// Add percentage of black to a color
@function shade($color, $percent){
@return mix(black, $color, $percent);
}

@ -0,0 +1,22 @@
// Return vendor-prefixed property names if appropriate
// Example: transition-property-names((transform, color, background), moz) -> -moz-transform, color, background
//************************************************************************//
@function transition-property-names($props, $vendor: false) {
$new-props: ();
@each $prop in $props {
$new-props: append($new-props, transition-property-name($prop, $vendor), comma);
}
@return $new-props;
}
@function transition-property-name($prop, $vendor: false) {
// put other properties that need to be prefixed here aswell
@if $vendor and $prop == transform {
@return unquote('-'+$vendor+'-'+$prop);
}
@else {
@return $prop;
}
}

@ -0,0 +1,39 @@
// Render Deprecated Webkit Gradient - Linear || Radial
//************************************************************************//
@function _deprecated-webkit-gradient($type,
$deprecated-pos1, $deprecated-pos2,
$full,
$deprecated-radius1: false, $deprecated-radius2: false) {
$gradient-list: ();
$gradient: false;
$full-length: length($full);
$percentage: false;
$gradient-type: $type;
@for $i from 1 through $full-length {
$gradient: nth($full, $i);
@if length($gradient) == 2 {
$color-stop: color-stop(nth($gradient, 2), nth($gradient, 1));
$gradient-list: join($gradient-list, $color-stop, comma);
}
@else if $gradient != null {
@if $i == $full-length {
$percentage: 100%;
}
@else {
$percentage: ($i - 1) * (100 / ($full-length - 1)) + "%";
}
$color-stop: color-stop(unquote($percentage), $gradient);
$gradient-list: join($gradient-list, $color-stop, comma);
}
}
@if $type == radial {
$gradient: -webkit-gradient(radial, $deprecated-pos1, $deprecated-radius1, $deprecated-pos2, $deprecated-radius2, $gradient-list);
}
@else if $type == linear {
$gradient: -webkit-gradient(linear, $deprecated-pos1, $deprecated-pos2, $gradient-list);
}
@return $gradient;
}

@ -0,0 +1,13 @@
@function _gradient-positions-parser($gradient-type, $gradient-positions) {
@if $gradient-positions
and ($gradient-type == linear)
and (type-of($gradient-positions) != color) {
$gradient-positions: _linear-positions-parser($gradient-positions);
}
@else if $gradient-positions
and ($gradient-type == radial)
and (type-of($gradient-positions) != color) {
$gradient-positions: _radial-positions-parser($gradient-positions);
}
@return $gradient-positions;
}

@ -0,0 +1,61 @@
@function _linear-positions-parser($pos) {
$type: type-of(nth($pos, 1));
$spec: null;
$degree: null;
$side: null;
$corner: null;
$length: length($pos);
// Parse Side and corner positions
@if ($length > 1) {
@if nth($pos, 1) == "to" { // Newer syntax
$side: nth($pos, 2);
@if $length == 2 { // eg. to top
// Swap for backwards compatability
$degree: _position-flipper(nth($pos, 2));
}
@else if $length == 3 { // eg. to top left
$corner: nth($pos, 3);
}
}
@else if $length == 2 { // Older syntax ("top left")
$side: _position-flipper(nth($pos, 1));
$corner: _position-flipper(nth($pos, 2));
}
@if ("#{$side} #{$corner}" == "left top") or ("#{$side} #{$corner}" == "top left") {
$degree: _position-flipper(#{$side}) _position-flipper(#{$corner});
}
@else if ("#{$side} #{$corner}" == "right top") or ("#{$side} #{$corner}" == "top right") {
$degree: _position-flipper(#{$side}) _position-flipper(#{$corner});
}
@else if ("#{$side} #{$corner}" == "right bottom") or ("#{$side} #{$corner}" == "bottom right") {
$degree: _position-flipper(#{$side}) _position-flipper(#{$corner});
}
@else if ("#{$side} #{$corner}" == "left bottom") or ("#{$side} #{$corner}" == "bottom left") {
$degree: _position-flipper(#{$side}) _position-flipper(#{$corner});
}
$spec: to $side $corner;
}
@else if $length == 1 {
// Swap for backwards compatability
@if $type == string {
$degree: $pos;
$spec: to _position-flipper($pos);
}
@else {
$degree: -270 - $pos; //rotate the gradient opposite from spec
$spec: $pos;
}
}
$degree: unquote($degree + ",");
$spec: unquote($spec + ",");
@return $degree $spec;
}
@function _position-flipper($pos) {
@return if($pos == left, right, null)
if($pos == right, left, null)
if($pos == top, bottom, null)
if($pos == bottom, top, null);
}

@ -0,0 +1,69 @@
@function _radial-arg-parser($G1, $G2, $pos, $shape-size) {
@each $value in $G1, $G2 {
$first-val: nth($value, 1);
$pos-type: type-of($first-val);
$spec-at-index: null;
// Determine if spec was passed to mixin
@if type-of($value) == list {
$spec-at-index: if(index($value, at), index($value, at), false);
}
@if $spec-at-index {
@if $spec-at-index > 1 {
@for $i from 1 through ($spec-at-index - 1) {
$shape-size: $shape-size nth($value, $i);
}
@for $i from ($spec-at-index + 1) through length($value) {
$pos: $pos nth($value, $i);
}
}
@else if $spec-at-index == 1 {
@for $i from ($spec-at-index + 1) through length($value) {
$pos: $pos nth($value, $i);
}
}
$G1: false;
}
// If not spec calculate correct values
@else {
@if ($pos-type != color) or ($first-val != "transparent") {
@if ($pos-type == number)
or ($first-val == "center")
or ($first-val == "top")
or ($first-val == "right")
or ($first-val == "bottom")
or ($first-val == "left") {
$pos: $value;
@if $pos == $G1 {
$G1: false;
}
}
@else if
($first-val == "ellipse")
or ($first-val == "circle")
or ($first-val == "closest-side")
or ($first-val == "closest-corner")
or ($first-val == "farthest-side")
or ($first-val == "farthest-corner")
or ($first-val == "contain")
or ($first-val == "cover") {
$shape-size: $value;
@if $value == $G1 {
$G1: false;
}
@else if $value == $G2 {
$G2: false;
}
}
}
}
}
@return $G1, $G2, $pos, $shape-size;
}

@ -0,0 +1,18 @@
@function _radial-positions-parser($gradient-pos) {
$shape-size: nth($gradient-pos, 1);
$pos: nth($gradient-pos, 2);
$shape-size-spec: _shape-size-stripper($shape-size);
$pre-spec: unquote(if($pos, "#{$pos}, ", null))
unquote(if($shape-size, "#{$shape-size},", null));
$pos-spec: if($pos, "at #{$pos}", null);
$spec: "#{$shape-size-spec} #{$pos-spec}";
// Add comma
@if ($spec != ' ') {
$spec: "#{$spec},"
}
@return $pre-spec $spec;
}

@ -0,0 +1,26 @@
// User for linear and radial gradients within background-image or border-image properties
@function _render-gradients($gradient-positions, $gradients, $gradient-type, $vendor: false) {
$pre-spec: null;
$spec: null;
$vendor-gradients: null;
@if $gradient-type == linear {
@if $gradient-positions {
$pre-spec: nth($gradient-positions, 1);
$spec: nth($gradient-positions, 2);
}
}
@else if $gradient-type == radial {
$pre-spec: nth($gradient-positions, 1);
$spec: nth($gradient-positions, 2);
}
@if $vendor {
$vendor-gradients: -#{$vendor}-#{$gradient-type}-gradient(#{$pre-spec} $gradients);
}
@else if $vendor == false {
$vendor-gradients: "#{$gradient-type}-gradient(#{$spec} #{$gradients})";
$vendor-gradients: unquote($vendor-gradients);
}
@return $vendor-gradients;
}

@ -0,0 +1,10 @@
@function _shape-size-stripper($shape-size) {
$shape-size-spec: null;
@each $value in $shape-size {
@if ($value == "cover") or ($value == "contain") {
$value: null;
}
$shape-size-spec: "#{$shape-size-spec} #{$value}";
}
@return $shape-size-spec;
}

@ -1,266 +0,0 @@
#isso-thread * {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#isso-thread .isso-comment-header a {
text-decoration: none;
}
#isso-thread {
padding: 0;
margin: 0;
}
#isso-thread > h4 {
color: #555;
font-weight: bold;
}
#isso-thread > .isso-feedlink {
float: right;
padding-left: 1em;
}
#isso-thread > .isso-feedlink > a {
font-size: 0.8em;
vertical-align: bottom;
}
#isso-thread .textarea {
min-height: 58px;
outline: 0;
}
#isso-thread .textarea.placeholder {
color: #757575;
}
#isso-root .isso-comment {
max-width: 68em;
padding-top: 0.95em;
margin: 0.95em auto;
}
#isso-root .preview .isso-comment {
padding-top: 0;
margin: 0;
}
#isso-root .isso-comment:not(:first-of-type),
.isso-follow-up .isso-comment {
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.isso-comment > div.avatar {
display: block;
float: left;
width: 7%;
margin: 3px 15px 0 0;
}
.isso-comment > div.avatar > svg {
max-width: 48px;
max-height: 48px;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.isso-comment > div.text-wrapper {
display: block;
}
.isso-comment .isso-follow-up {
padding-left: calc(7% + 20px);
}
.isso-comment > div.text-wrapper > .isso-comment-header, .isso-comment > div.text-wrapper > .isso-comment-footer {
font-size: 0.95em;
}
.isso-comment > div.text-wrapper > .isso-comment-header {
font-size: 0.85em;
}
.isso-comment > div.text-wrapper > .isso-comment-header .spacer {
padding: 0 6px;
}
.isso-comment > div.text-wrapper > .isso-comment-header .spacer,
.isso-comment > div.text-wrapper > .isso-comment-header a.permalink,
.isso-comment > div.text-wrapper > .isso-comment-header .note,
.isso-comment > div.text-wrapper > .isso-comment-header a.parent {
color: gray !important;
font-weight: normal;
text-shadow: none !important;
}
.isso-comment > div.text-wrapper > .isso-comment-header .spacer:hover,
.isso-comment > div.text-wrapper > .isso-comment-header a.permalink:hover,
.isso-comment > div.text-wrapper > .isso-comment-header .note:hover,
.isso-comment > div.text-wrapper > .isso-comment-header a.parent:hover {
color: #606060 !important;
}
.isso-comment > div.text-wrapper > .isso-comment-header .note {
float: right;
}
.isso-comment > div.text-wrapper > .isso-comment-header .author {
font-weight: bold;
color: #555;
}
.isso-comment > div.text-wrapper > .textarea-wrapper .textarea,
.isso-comment > div.text-wrapper > .textarea-wrapper .preview {
margin-top: 0.2em;
}
.isso-comment > div.text-wrapper > div.text p {
margin-top: 0.2em;
}
.isso-comment > div.text-wrapper > div.text p:last-child {
margin-bottom: 0.2em;
}
.isso-comment > div.text-wrapper > div.text h1,
.isso-comment > div.text-wrapper > div.text h2,
.isso-comment > div.text-wrapper > div.text h3,
.isso-comment > div.text-wrapper > div.text h4,
.isso-comment > div.text-wrapper > div.text h5,
.isso-comment > div.text-wrapper > div.text h6 {
font-size: 130%;
font-weight: bold;
}
.isso-comment > div.text-wrapper > div.textarea-wrapper .textarea,
.isso-comment > div.text-wrapper > div.textarea-wrapper .preview {
width: 100%;
border: 1px solid #f0f0f0;
border-radius: 2px;
box-shadow: 0 0 2px #888;
}
.isso-comment > div.text-wrapper > .isso-comment-footer {
font-size: 0.80em;
color: gray !important;
clear: left;
}
.isso-feedlink,
.isso-comment > div.text-wrapper > .isso-comment-footer a {
font-weight: bold;
text-decoration: none;
}
.isso-feedlink:hover,
.isso-comment > div.text-wrapper > .isso-comment-footer a:hover {
color: #111111 !important;
text-shadow: #aaaaaa 0 0 1px !important;
}
.isso-comment > div.text-wrapper > .isso-comment-footer > a {
position: relative;
top: .2em;
}
.isso-comment > div.text-wrapper > .isso-comment-footer > a + a {
padding-left: 1em;
}
.isso-comment > div.text-wrapper > .isso-comment-footer .votes {
color: gray;
}
.isso-comment > div.text-wrapper > .isso-comment-footer .upvote svg,
.isso-comment > div.text-wrapper > .isso-comment-footer .downvote svg {
position: relative;
top: .2em;
}
.isso-comment .isso-postbox {
margin-top: 0.8em;
}
.isso-comment.isso-no-votes > * > .isso-comment-footer span.votes {
display: none;
}
.isso-postbox {
max-width: 68em;
margin: 0 auto 2em;
clear: right;
}
.isso-postbox > .form-wrapper {
display: block;
padding: 0;
}
.isso-postbox > .form-wrapper > .auth-section,
.isso-postbox > .form-wrapper > .auth-section .post-action {
display: block;
}
.isso-postbox > .form-wrapper .textarea,
.isso-postbox > .form-wrapper .preview {
margin: 0 0 .3em;
padding: .4em .8em;
border-radius: 3px;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.isso-postbox > .form-wrapper input[type=checkbox] {
vertical-align: middle;
position: relative;
bottom: 1px;
margin-left: 0;
}
.isso-postbox > .form-wrapper .notification-section {
font-size: 0.90em;
padding-top: .3em;
}
#isso-thread .textarea:focus,
#isso-thread input:focus {
border-color: rgba(0, 0, 0, 0.8);
}
.isso-postbox > .form-wrapper > .auth-section .input-wrapper {
display: inline-block;
position: relative;
max-width: 25%;
margin: 0;
}
.isso-postbox > .form-wrapper > .auth-section .input-wrapper input {
padding: .3em 10px;
max-width: 100%;
border-radius: 3px;
background-color: #fff;
line-height: 1.4em;
border: 1px solid rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.isso-postbox > .form-wrapper > .auth-section .post-action {
display: inline-block;
float: right;
margin: 0 0 0 5px;
}
.isso-postbox > .form-wrapper > .auth-section .post-action > input {
padding: calc(.3em - 1px);
border-radius: 2px;
border: 1px solid #CCC;
background-color: #DDD;
cursor: pointer;
outline: 0;
line-height: 1.4em;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.isso-postbox > .form-wrapper > .auth-section .post-action > input:hover {
background-color: #CCC;
}
.isso-postbox > .form-wrapper > .auth-section .post-action > input:active {
background-color: #BBB;
}
.isso-postbox > .form-wrapper .preview,
.isso-postbox > .form-wrapper input[name="edit"],
.isso-postbox.preview-mode > .form-wrapper input[name="preview"],
.isso-postbox.preview-mode > .form-wrapper .textarea {
display: none;
}
.isso-postbox.preview-mode > .form-wrapper .preview {
display: block;
}
.isso-postbox.preview-mode > .form-wrapper input[name="edit"] {
display: inline;
}
.isso-postbox > .form-wrapper .preview {
background-color: #f8f8f8;
background: repeating-linear-gradient(
-45deg,
#f8f8f8,
#f8f8f8 10px,
#fff 10px,
#fff 20px
);
}
.isso-postbox > .form-wrapper > .notification-section {
display: none;
padding-bottom: 10px;
}
@media screen and (max-width:600px) {
.isso-postbox > .form-wrapper > .auth-section .input-wrapper {
display: block;
max-width: 100%;
margin: 0 0 .3em;
}
.isso-postbox > .form-wrapper > .auth-section .input-wrapper input {
width: 100%;
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save