Browse Source

Initial commit

Phyks (Lucas Verney) 10 months ago
commit
becdcfed4b
75 changed files with 5589 additions and 0 deletions
  1. 3
    0
      .gitignore
  2. 87
    0
      Makefile
  3. BIN
      content/images/splash/screenshot_1.png
  4. BIN
      content/images/splash/screenshot_2.png
  5. 52
    0
      content/pages/en/00-home.html
  6. 103
    0
      develop_server.sh
  7. 92
    0
      fabfile.py
  8. 35
    0
      pelicanconf.py
  9. 156
    0
      plugins/i18n_subsites/README.rst
  10. 1
    0
      plugins/i18n_subsites/__init__.py
  11. 450
    0
      plugins/i18n_subsites/i18n_subsites.py
  12. 128
    0
      plugins/i18n_subsites/implementing_language_buttons.rst
  13. 200
    0
      plugins/i18n_subsites/localizing_using_jinja2.rst
  14. 0
    0
      plugins/i18n_subsites/test_data/content/images/img.png
  15. 7
    0
      plugins/i18n_subsites/test_data/content/pages/hidden-page-cz.rst
  16. 7
    0
      plugins/i18n_subsites/test_data/content/pages/hidden-page-de.rst
  17. 7
    0
      plugins/i18n_subsites/test_data/content/pages/hidden-page-en.rst
  18. 5
    0
      plugins/i18n_subsites/test_data/content/pages/untranslated-page.rst
  19. 8
    0
      plugins/i18n_subsites/test_data/content/translated_article-cz.rst
  20. 8
    0
      plugins/i18n_subsites/test_data/content/translated_article-de.rst
  21. 8
    0
      plugins/i18n_subsites/test_data/content/translated_article-en.rst
  22. 9
    0
      plugins/i18n_subsites/test_data/content/untranslated_article-en.rst
  23. 2
    0
      plugins/i18n_subsites/test_data/localized_theme/babel.cfg
  24. 23
    0
      plugins/i18n_subsites/test_data/localized_theme/messages.pot
  25. 0
    0
      plugins/i18n_subsites/test_data/localized_theme/static/style.css
  26. 7
    0
      plugins/i18n_subsites/test_data/localized_theme/templates/base.html
  27. BIN
      plugins/i18n_subsites/test_data/localized_theme/translations/de/LC_MESSAGES/messages.mo
  28. 23
    0
      plugins/i18n_subsites/test_data/localized_theme/translations/de/LC_MESSAGES/messages.po
  29. 53
    0
      plugins/i18n_subsites/test_data/pelicanconf.py
  30. 139
    0
      plugins/i18n_subsites/test_i18n_subsites.py
  31. 23
    0
      plugins/toc/.travis.yml
  32. 339
    0
      plugins/toc/LICENSE
  33. 66
    0
      plugins/toc/README.md
  34. 1
    0
      plugins/toc/__init__.py
  35. 1
    0
      plugins/toc/dev_requirements.txt
  36. 11
    0
      plugins/toc/test_data/article_with_headers.md
  37. 15
    0
      plugins/toc/test_data/article_with_headers_exclude_small_headers.md
  38. 16
    0
      plugins/toc/test_data/article_with_headers_exclude_small_headers_metadata.md
  39. 12
    0
      plugins/toc/test_data/article_with_headers_metadata.md
  40. 15
    0
      plugins/toc/test_data/article_with_headers_nonascii.md
  41. 1
    0
      plugins/toc/test_data/article_with_headers_toc.html
  42. 1
    0
      plugins/toc/test_data/article_with_headers_toc_exclude_small_headers.html
  43. 1
    0
      plugins/toc/test_data/article_with_headers_toc_nonascii.html
  44. 5
    0
      plugins/toc/test_data/article_without_headers.md
  45. 94
    0
      plugins/toc/test_toc.py
  46. 151
    0
      plugins/toc/toc.py
  47. 36
    0
      plugins/toc/tox.ini
  48. 21
    0
      publishconf.py
  49. 7
    0
      themes/custom/static/css/bootstrap.min.css
  50. 4
    0
      themes/custom/static/css/font-awesome.min.css
  51. 123
    0
      themes/custom/static/css/style.css
  52. BIN
      themes/custom/static/fonts/FontAwesome.otf
  53. BIN
      themes/custom/static/fonts/fontawesome-webfont.eot
  54. 2671
    0
      themes/custom/static/fonts/fontawesome-webfont.svg
  55. BIN
      themes/custom/static/fonts/fontawesome-webfont.ttf
  56. BIN
      themes/custom/static/fonts/fontawesome-webfont.woff
  57. BIN
      themes/custom/static/fonts/fontawesome-webfont.woff2
  58. BIN
      themes/custom/static/fonts/roboto-v16-latin-regular.eot
  59. 308
    0
      themes/custom/static/fonts/roboto-v16-latin-regular.svg
  60. BIN
      themes/custom/static/fonts/roboto-v16-latin-regular.ttf
  61. BIN
      themes/custom/static/fonts/roboto-v16-latin-regular.woff
  62. BIN
      themes/custom/static/fonts/roboto-v16-latin-regular.woff2
  63. BIN
      themes/custom/static/images/cover.jpg
  64. BIN
      themes/custom/static/images/device.png
  65. BIN
      themes/custom/static/images/icons/coast-228x228.png
  66. BIN
      themes/custom/static/images/icons/favicon-16x16.png
  67. BIN
      themes/custom/static/images/icons/favicon-32x32.png
  68. BIN
      themes/custom/static/images/icons/favicon.ico
  69. BIN
      themes/custom/static/images/icons/ogIcon.png
  70. BIN
      themes/custom/static/images/logo.png
  71. 7
    0
      themes/custom/static/js/bootstrap.bundle.min.js
  72. 1
    0
      themes/custom/static/js/bootstrap.bundle.min.js.map
  73. 2
    0
      themes/custom/static/js/jquery.min.js
  74. 38
    0
      themes/custom/templates/base.html
  75. 6
    0
      themes/custom/templates/page.html

+ 3
- 0
.gitignore View File

@@ -0,0 +1,3 @@
1
+*.pyc
2
+output/
3
+*.pid

+ 87
- 0
Makefile View File

@@ -0,0 +1,87 @@
1
+PY?=python3
2
+PELICAN?=pelican
3
+PELICANOPTS=
4
+
5
+BASEDIR=$(CURDIR)
6
+INPUTDIR=$(BASEDIR)/content
7
+OUTPUTDIR=$(BASEDIR)/output
8
+CONFFILE=$(BASEDIR)/pelicanconf.py
9
+PUBLISHCONF=$(BASEDIR)/publishconf.py
10
+
11
+DEBUG ?= 0
12
+ifeq ($(DEBUG), 1)
13
+	PELICANOPTS += -D
14
+endif
15
+
16
+RELATIVE ?= 0
17
+ifeq ($(RELATIVE), 1)
18
+	PELICANOPTS += --relative-urls
19
+endif
20
+
21
+help:
22
+	@echo 'Makefile for a pelican Web site                                           '
23
+	@echo '                                                                          '
24
+	@echo 'Usage:                                                                    '
25
+	@echo '   make html                           (re)generate the web site          '
26
+	@echo '   make clean                          remove the generated files         '
27
+	@echo '   make regenerate                     regenerate files upon modification '
28
+	@echo '   make publish                        generate using production settings '
29
+	@echo '   make serve [PORT=8000]              serve site at http://localhost:8000'
30
+	@echo '   make serve-global [SERVER=0.0.0.0]  serve (as root) to $(SERVER):80    '
31
+	@echo '   make devserver [PORT=8000]          start/restart develop_server.sh    '
32
+	@echo '   make stopserver                     stop local server                  '
33
+	@echo '   make ssh                            upload the web site via SSH        '
34
+	@echo '   make rsync                          upload the web site via rsync+ssh  '
35
+	@echo '                                                                          '
36
+	@echo 'Set the DEBUG variable to 1 to enable debugging, e.g. make DEBUG=1 html   '
37
+	@echo 'Set the RELATIVE variable to 1 to enable relative urls                    '
38
+	@echo '                                                                          '
39
+
40
+html:
41
+	$(PELICAN) $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS)
42
+
43
+clean:
44
+	[ ! -d $(OUTPUTDIR) ] || rm -rf $(OUTPUTDIR)
45
+
46
+regenerate:
47
+	$(PELICAN) -r $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS)
48
+
49
+serve:
50
+ifdef PORT
51
+	cd $(OUTPUTDIR) && $(PY) -m pelican.server $(PORT)
52
+else
53
+	cd $(OUTPUTDIR) && $(PY) -m pelican.server
54
+endif
55
+
56
+serve-global:
57
+ifdef SERVER
58
+	cd $(OUTPUTDIR) && $(PY) -m pelican.server 80 $(SERVER)
59
+else
60
+	cd $(OUTPUTDIR) && $(PY) -m pelican.server 80 0.0.0.0
61
+endif
62
+
63
+
64
+devserver:
65
+ifdef PORT
66
+	$(BASEDIR)/develop_server.sh restart $(PORT)
67
+else
68
+	$(BASEDIR)/develop_server.sh restart
69
+endif
70
+
71
+stopserver:
72
+	$(BASEDIR)/develop_server.sh stop
73
+	@echo 'Stopped Pelican and SimpleHTTPServer processes running in background.'
74
+
75
+publish:
76
+	$(PELICAN) $(INPUTDIR) -o $(OUTPUTDIR) -s $(PUBLISHCONF) $(PELICANOPTS)
77
+
78
+ssh : publish
79
+	scp -P $(SSH_PORT) -r $(OUTPUTDIR)/* $(SSH_USER)@$(SSH_HOST):$(SSH_TARGET_DIR)
80
+
81
+rsync: publish
82
+	rsync -e "ssh -p $(SSH_PORT)" -P -rvzc --delete $(OUTPUTDIR)/ $(SSH_USER)@$(SSH_HOST):$(SSH_TARGET_DIR) --cvs-exclude
83
+
84
+install:
85
+	pip install --user pelican Markdown toc bs4
86
+
87
+.PHONY: html help clean regenerate serve serve-global devserver stopserver publish ssh rsync

BIN
content/images/splash/screenshot_1.png View File


BIN
content/images/splash/screenshot_2.png View File


+ 52
- 0
content/pages/en/00-home.html View File

@@ -0,0 +1,52 @@
1
+<html>
2
+    <head>
3
+        <meta name="title" content="Cygnal - Track and share issues in realtime on bike lanes!" />
4
+        <meta name="date" content="2017-03-11 10:02" />
5
+        <meta name="slug" content="home" />
6
+        <meta name="save_as" content="index.html" />
7
+        <meta name="summary" content="home" />
8
+        <meta name="status" content="hidden" />
9
+        <meta name="lang" content="en" />
10
+    </head>
11
+    <body>
12
+        <div class="main">
13
+            <div class="container">
14
+                <div class="row">
15
+                    <div class="col-sm-6 app-description">
16
+                        <h1><span class="logo"><img src="theme/images/logo.png" alt="Logo" /></span>&nbsp;Cygnal</h1>
17
+                        <h2>Get realtime infos on your bike route.</h2>
18
+                        <p>Cygnal is a <strong>free</strong> and <strong>open-source</strong> app to help you track and share issues in realtime on bike routes: road works, obstacles, accidents, etc.</p>
19
+                        <p>Cygnal already harvest <strong>available OpenData</strong> to inform you about road works ahead while you bike.</p>
20
+                        <p>Cygnal is designed with <strong>respecting your privacy</strong> in mind and <a href="https://framagit.org/phyks/cygnal/blob/master/doc/10.technical_notes.md#privacy-design">handles your geolocation with care</a>, not storing it nor sharing it with third parties.</p>
21
+
22
+                        <p class="center">
23
+                            <a class="btn btn-primary btn-lg" href="/app">Go to app</a>
24
+                        </p>
25
+                    </div>
26
+                    <div class="col-sm-6">
27
+                        <div class="device">
28
+                            <div class="device-screen">
29
+                                <div id="deviceCarousel" class="carousel slide" data-ride="carousel">
30
+                                    <div class="carousel-inner">
31
+                                        <div class="carousel-item active">
32
+                                            <img class="app-screenshot" src="images/splash/screenshot_1.png" />
33
+                                        </div>
34
+                                        <div class="carousel-item">
35
+                                            <img class="app-screenshot" src="images/splash/screenshot_2.png" />
36
+                                        </div>
37
+                                    </div>
38
+                                </div>
39
+                            </div>
40
+                        </div>
41
+                    </div>
42
+                </div>
43
+                <nav class="row">
44
+                    <div class="col-sm-12">
45
+                        <a href="https://framagit.org/phyks/cygnal" class="fa fa-code" title="Code"></a>
46
+                        <a href="mailto:phyks+cygnal@phyks.me" class="fa fa-envelope" title="Forum"></a>
47
+                    </div>
48
+                </nav>
49
+            </div>
50
+        </div>
51
+    </body>
52
+</html>

+ 103
- 0
develop_server.sh View File

@@ -0,0 +1,103 @@
1
+#!/usr/bin/env bash
2
+##
3
+# This section should match your Makefile
4
+##
5
+PY=${PY:-python}
6
+PELICAN=${PELICAN:-pelican}
7
+PELICANOPTS=
8
+
9
+BASEDIR=$(pwd)
10
+INPUTDIR=$BASEDIR/content
11
+OUTPUTDIR=$BASEDIR/output
12
+CONFFILE=$BASEDIR/pelicanconf.py
13
+
14
+###
15
+# Don't change stuff below here unless you are sure
16
+###
17
+
18
+SRV_PID=$BASEDIR/srv.pid
19
+PELICAN_PID=$BASEDIR/pelican.pid
20
+
21
+function usage(){
22
+  echo "usage: $0 (stop) (start) (restart) [port]"
23
+  echo "This starts Pelican in debug and reload mode and then launches"
24
+  echo "an HTTP server to help site development. It doesn't read"
25
+  echo "your Pelican settings, so if you edit any paths in your Makefile"
26
+  echo "you will need to edit your settings as well."
27
+  exit 3
28
+}
29
+
30
+function alive() {
31
+  kill -0 $1 >/dev/null 2>&1
32
+}
33
+
34
+function shut_down(){
35
+  PID=$(cat $SRV_PID)
36
+  if [[ $? -eq 0 ]]; then
37
+    if alive $PID; then
38
+      echo "Stopping HTTP server"
39
+      kill $PID
40
+    else
41
+      echo "Stale PID, deleting"
42
+    fi
43
+    rm $SRV_PID
44
+  else
45
+    echo "HTTP server PIDFile not found"
46
+  fi
47
+
48
+  PID=$(cat $PELICAN_PID)
49
+  if [[ $? -eq 0 ]]; then
50
+    if alive $PID; then
51
+      echo "Killing Pelican"
52
+      kill $PID
53
+    else
54
+      echo "Stale PID, deleting"
55
+    fi
56
+    rm $PELICAN_PID
57
+  else
58
+    echo "Pelican PIDFile not found"
59
+  fi
60
+}
61
+
62
+function start_up(){
63
+  local port=$1
64
+  echo "Starting up Pelican and HTTP server"
65
+  shift
66
+  $PELICAN --debug --autoreload -r $INPUTDIR -o $OUTPUTDIR -s $CONFFILE $PELICANOPTS &
67
+  pelican_pid=$!
68
+  echo $pelican_pid > $PELICAN_PID
69
+  mkdir -p $OUTPUTDIR && cd $OUTPUTDIR
70
+  $PY -m pelican.server $port &
71
+  srv_pid=$!
72
+  echo $srv_pid > $SRV_PID
73
+  cd $BASEDIR
74
+  sleep 1
75
+  if ! alive $pelican_pid ; then
76
+    echo "Pelican didn't start. Is the Pelican package installed?"
77
+    return 1
78
+  elif ! alive $srv_pid ; then
79
+    echo "The HTTP server didn't start. Is there another service using port" $port "?"
80
+    return 1
81
+  fi
82
+  echo 'Pelican and HTTP server processes now running in background.'
83
+}
84
+
85
+###
86
+#  MAIN
87
+###
88
+[[ ($# -eq 0) || ($# -gt 2) ]] && usage
89
+port=''
90
+[[ $# -eq 2 ]] && port=$2
91
+
92
+if [[ $1 == "stop" ]]; then
93
+  shut_down
94
+elif [[ $1 == "restart" ]]; then
95
+  shut_down
96
+  start_up $port
97
+elif [[ $1 == "start" ]]; then
98
+  if ! start_up $port; then
99
+    shut_down
100
+  fi
101
+else
102
+  usage
103
+fi

+ 92
- 0
fabfile.py View File

@@ -0,0 +1,92 @@
1
+from fabric.api import *
2
+import fabric.contrib.project as project
3
+import os
4
+import shutil
5
+import sys
6
+import SocketServer
7
+
8
+from pelican.server import ComplexHTTPRequestHandler
9
+
10
+# Local path configuration (can be absolute or relative to fabfile)
11
+env.deploy_path = 'output'
12
+DEPLOY_PATH = env.deploy_path
13
+
14
+# Remote server configuration
15
+production = 'root@localhost:22'
16
+dest_path = '/var/www'
17
+
18
+# Rackspace Cloud Files configuration settings
19
+env.cloudfiles_username = 'my_rackspace_username'
20
+env.cloudfiles_api_key = 'my_rackspace_api_key'
21
+env.cloudfiles_container = 'my_cloudfiles_container'
22
+
23
+# Github Pages configuration
24
+env.github_pages_branch = "gh-pages"
25
+
26
+# Port for `serve`
27
+PORT = 8000
28
+
29
+def clean():
30
+    """Remove generated files"""
31
+    if os.path.isdir(DEPLOY_PATH):
32
+        shutil.rmtree(DEPLOY_PATH)
33
+        os.makedirs(DEPLOY_PATH)
34
+
35
+def build():
36
+    """Build local version of site"""
37
+    local('pelican -s pelicanconf.py')
38
+
39
+def rebuild():
40
+    """`build` with the delete switch"""
41
+    local('pelican -d -s pelicanconf.py')
42
+
43
+def regenerate():
44
+    """Automatically regenerate site upon file modification"""
45
+    local('pelican -r -s pelicanconf.py')
46
+
47
+def serve():
48
+    """Serve site at http://localhost:8000/"""
49
+    os.chdir(env.deploy_path)
50
+
51
+    class AddressReuseTCPServer(SocketServer.TCPServer):
52
+        allow_reuse_address = True
53
+
54
+    server = AddressReuseTCPServer(('', PORT), ComplexHTTPRequestHandler)
55
+
56
+    sys.stderr.write('Serving on port {0} ...\n'.format(PORT))
57
+    server.serve_forever()
58
+
59
+def reserve():
60
+    """`build`, then `serve`"""
61
+    build()
62
+    serve()
63
+
64
+def preview():
65
+    """Build production version of site"""
66
+    local('pelican -s publishconf.py')
67
+
68
+def cf_upload():
69
+    """Publish to Rackspace Cloud Files"""
70
+    rebuild()
71
+    with lcd(DEPLOY_PATH):
72
+        local('swift -v -A https://auth.api.rackspacecloud.com/v1.0 '
73
+              '-U {cloudfiles_username} '
74
+              '-K {cloudfiles_api_key} '
75
+              'upload -c {cloudfiles_container} .'.format(**env))
76
+
77
+@hosts(production)
78
+def publish():
79
+    """Publish to production via rsync"""
80
+    local('pelican -s publishconf.py')
81
+    project.rsync_project(
82
+        remote_dir=dest_path,
83
+        exclude=".DS_Store",
84
+        local_dir=DEPLOY_PATH.rstrip('/') + '/',
85
+        delete=True,
86
+        extra_opts='-c',
87
+    )
88
+
89
+def gh_pages():
90
+    """Publish to GitHub Pages"""
91
+    rebuild()
92
+    local("ghp-import -b {github_pages_branch} {deploy_path} -p".format(**env))

+ 35
- 0
pelicanconf.py View File

@@ -0,0 +1,35 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*- #
3
+from __future__ import unicode_literals
4
+
5
+AUTHOR = u'Phyks'
6
+SITENAME = u'Cygnal'
7
+SITEDESCRIPTION = 'Track and share issues in realtime on bike lanes!'
8
+SITEURL = ''
9
+
10
+PATH = 'content'
11
+PLUGIN_PATHS = ['plugins']
12
+
13
+TIMEZONE = 'Europe/Paris'
14
+
15
+DEFAULT_LANG = u'en'
16
+LOCALE = ('en_US.UTF-8',)
17
+
18
+DEFAULT_PAGINATION = 10
19
+
20
+# Uncomment following line if you want document-relative URLs when developing
21
+RELATIVE_URLS = True
22
+
23
+THEME = 'themes/custom'
24
+
25
+STATIC_PATHS = ['images']
26
+
27
+MARKDOWN = {
28
+    'extension_configs': {
29
+        'markdown.extensions.toc': {'anchorlink': True},
30
+        'markdown.extensions.codehilite': {'css_class': 'highlight'}
31
+    },
32
+    'output_format': 'html5'
33
+}
34
+
35
+DEFAULT_DATE_FORMAT = '%d/%m/%Y'

+ 156
- 0
plugins/i18n_subsites/README.rst View File

@@ -0,0 +1,156 @@
1
+=======================
2
+ I18N Sub-sites Plugin
3
+=======================
4
+
5
+This plugin extends the translations functionality by creating
6
+internationalized sub-sites for the default site.
7
+
8
+This plugin is designed for Pelican 3.4 and later.
9
+
10
+What it does
11
+============
12
+
13
+1. When the content of the main site is being generated, the settings
14
+   are saved and the generation stops when content is ready to be
15
+   written. While reading source files and generating content objects,
16
+   the output queue is modified in certain ways:
17
+
18
+  - translations that will appear as native in a different (sub-)site
19
+    will be removed
20
+  - untranslated articles will be transformed to drafts if
21
+    ``I18N_UNTRANSLATED_ARTICLES`` is ``'hide'`` (default), removed if
22
+    ``'remove'`` or kept as they are if ``'keep'``.
23
+  - untranslated pages will be transformed into hidden pages if
24
+    ``I18N_UNTRANSLATED_PAGES`` is ``'hide'`` (default), removed if
25
+    ``'remove'`` or kept as they are if ``'keep'``.''
26
+  - additional content manipulation similar to articles and pages can
27
+    be specified for custom generators in the ``I18N_GENERATOR_INFO``
28
+    setting.
29
+
30
+2. For each language specified in the ``I18N_SUBSITES`` dictionary the
31
+   settings overrides are applied to the settings from the main site
32
+   and a new sub-site is generated in the same way as with the main
33
+   site until content is ready to be written.
34
+3. When all (sub-)sites are waiting for content writing, all removed
35
+   contents, translations and static files are interlinked across the
36
+   (sub-)sites.
37
+4. Finally, all the output is written.
38
+
39
+Setting it up
40
+=============
41
+
42
+For each extra used language code, a language-specific settings overrides
43
+dictionary must be given (but can be empty) in the ``I18N_SUBSITES`` dictionary
44
+
45
+.. code-block:: python
46
+
47
+    PLUGINS = ['i18n_subsites', ...]
48
+
49
+    # mapping: language_code -> settings_overrides_dict
50
+    I18N_SUBSITES = {
51
+        'cz': {
52
+	    'SITENAME': 'Hezkej blog',
53
+	    }
54
+	}
55
+
56
+Default and special overrides
57
+-----------------------------
58
+The settings overrides may contain arbitrary settings, however, there
59
+are some that are handled in a special way:
60
+
61
+``SITEURL``
62
+  Any overrides to this setting should ensure that there is some level
63
+  of hierarchy between all (sub-)sites, because Pelican makes all URLs
64
+  relative to ``SITEURL`` and the plugin can only cross-link between
65
+  the sites using this hierarchy. For instance, with the main site
66
+  ``http://example.com`` a sub-site ``http://example.com/de`` will
67
+  work, but ``http://de.example.com`` will not. If not overridden, the
68
+  language code (the language identifier used in the ``lang``
69
+  metadata) is appended to the main ``SITEURL`` for each sub-site.
70
+``OUTPUT_PATH``, ``CACHE_PATH``
71
+  If not overridden, the language code is appended as with ``SITEURL``.
72
+  Separate cache paths are required as parser results depend on the locale.
73
+``STATIC_PATHS``, ``THEME_STATIC_PATHS``
74
+  If not overridden, they are set to ``[]`` and all links to static
75
+  files are cross-linked to the main site.
76
+``THEME``, ``THEME_STATIC_DIR``
77
+  If overridden, the logic with ``THEME_STATIC_PATHS`` does not apply.
78
+``DEFAULT_LANG``
79
+  This should not be overridden as the plugin changes it to the
80
+  language code of each sub-site to change what is perceived as translations.
81
+
82
+Localizing templates
83
+--------------------
84
+
85
+Most importantly, this plugin can use localized templates for each
86
+sub-site. There are two approaches to having the templates localized:
87
+
88
+- You can set a different ``THEME`` override for each language in
89
+  ``I18N_SUBSITES``, e.g. by making a copy of a theme ``my_theme`` to
90
+  ``my_theme_lang`` and then editing the templates in the new
91
+  localized theme. This approach means you don't have to deal with
92
+  gettext ``*.po`` files, but it is harder to maintain over time.
93
+- You use only one theme and localize the templates using the
94
+  `jinja2.ext.i18n Jinja2 extension
95
+  <http://jinja.pocoo.org/docs/templates/#i18n>`_. For a kickstart
96
+  read this `guide <./localizing_using_jinja2.rst>`_.
97
+
98
+Additional context variables
99
+............................
100
+
101
+It may be convenient to add language buttons to your theme in addition
102
+to the translation links of articles and pages. These buttons could,
103
+for example, point to the ``SITEURL`` of each (sub-)site. For this
104
+reason the plugin adds these variables to the template context:
105
+
106
+``main_lang``
107
+  The language of the main site — the original ``DEFAULT_LANG``
108
+``main_siteurl``
109
+  The ``SITEURL`` of the main site — the original ``SITEURL``
110
+``lang_siteurls``
111
+  An ordered dictionary, mapping all used languages to their
112
+  ``SITEURL``. The ``main_lang`` is the first key with ``main_siteurl``
113
+  as the value. This dictionary is useful for implementing global
114
+  language buttons that show the language of the currently viewed
115
+  (sub-)site too.
116
+``extra_siteurls``
117
+  An ordered dictionary, subset of ``lang_siteurls``, the current
118
+  ``DEFAULT_LANG`` of the rendered (sub-)site is not included, so for
119
+  each (sub-)site ``set(extra_siteurls) == set(lang_siteurls) -
120
+  set([DEFAULT_LANG])``. This dictionary is useful for implementing
121
+  global language buttons that do not show the current language.
122
+``relpath_to_site``
123
+  A function that returns a relative path from the first (sub-)site to
124
+  the second (sub-)site where the (sub-)sites are identified by the
125
+  language codes given as two arguments.
126
+
127
+If you don't like the default ordering of the ordered dictionaries,
128
+use a Jinja2 filter to alter the ordering.
129
+
130
+All the siteurls above are always absolute even in the case of
131
+``RELATIVE_URLS == True`` (it would be to complicated to replicate the
132
+Pelican internals for local siteurls), so you may rather use something
133
+like ``{{ SITEURL }}/{{ relpath_to_site(DEFAULT_LANG, main_lang }}``
134
+to link to the main site.
135
+
136
+This short `howto <./implementing_language_buttons.rst>`_ shows two
137
+example implementations of language buttons.
138
+
139
+Usage notes
140
+===========
141
+- It is **mandatory** to specify ``lang`` metadata for each article
142
+  and page as ``DEFAULT_LANG`` is later changed for each sub-site, so
143
+  content without ``lang`` metadata would be rendered in every
144
+  (sub-)site.
145
+- As with the original translations functionality, ``slug`` metadata
146
+  is used to group translations. It is therefore often convenient to
147
+  compensate for this by overriding the content URL (which defaults to
148
+  slug) using the ``url`` and ``save_as`` metadata. You could also
149
+  give articles e.g. ``name`` metadata and use it in ``ARTICLE_URL =
150
+  '{name}.html'``.
151
+
152
+Development
153
+===========
154
+
155
+- A demo and a test site is in the ``gh-pages`` branch and can be seen
156
+  at http://smartass101.github.io/pelican-plugins/

+ 1
- 0
plugins/i18n_subsites/__init__.py View File

@@ -0,0 +1 @@
1
+from .i18n_subsites import *

+ 450
- 0
plugins/i18n_subsites/i18n_subsites.py View File

@@ -0,0 +1,450 @@
1
+"""i18n_subsites plugin creates i18n-ized subsites of the default site
2
+
3
+This plugin is designed for Pelican 3.4 and later
4
+"""
5
+
6
+
7
+import os
8
+import six
9
+import logging
10
+import posixpath
11
+
12
+from copy import copy
13
+from itertools import chain
14
+from operator import attrgetter
15
+from collections import OrderedDict
16
+from contextlib import contextmanager
17
+from six.moves.urllib.parse import urlparse
18
+
19
+import gettext
20
+import locale
21
+
22
+from pelican import signals
23
+from pelican.generators import ArticlesGenerator, PagesGenerator
24
+from pelican.settings import configure_settings
25
+from pelican.contents import Draft
26
+
27
+
28
+# Global vars
29
+_MAIN_SETTINGS = None     # settings dict of the main Pelican instance
30
+_MAIN_LANG = None         # lang of the main Pelican instance
31
+_MAIN_SITEURL = None      # siteurl of the main Pelican instance
32
+_MAIN_STATIC_FILES = None # list of Static instances the main Pelican instance
33
+_SUBSITE_QUEUE = {}   # map: lang -> settings overrides
34
+_SITE_DB = OrderedDict()           # OrderedDict: lang -> siteurl
35
+_SITES_RELPATH_DB = {}       # map: (lang, base_lang) -> relpath
36
+# map: generator -> list of removed contents that need interlinking
37
+_GENERATOR_DB = {}
38
+_NATIVE_CONTENT_URL_DB = {} # map: source_path -> content in its native lang
39
+_LOGGER = logging.getLogger(__name__)
40
+
41
+
42
+@contextmanager
43
+def temporary_locale(temp_locale=None):
44
+    '''Enable code to run in a context with a temporary locale
45
+
46
+    Resets the locale back when exiting context.
47
+    Can set a temporary locale if provided
48
+    '''
49
+    orig_locale = locale.setlocale(locale.LC_ALL)
50
+    if temp_locale is not None:
51
+        locale.setlocale(locale.LC_ALL, temp_locale)
52
+    yield
53
+    locale.setlocale(locale.LC_ALL, orig_locale)
54
+
55
+
56
+def initialize_dbs(settings):
57
+    '''Initialize internal DBs using the Pelican settings dict
58
+
59
+    This clears the DBs for e.g. autoreload mode to work
60
+    '''
61
+    global _MAIN_SETTINGS, _MAIN_SITEURL, _MAIN_LANG, _SUBSITE_QUEUE
62
+    _MAIN_SETTINGS = settings
63
+    _MAIN_LANG = settings['DEFAULT_LANG']
64
+    _MAIN_SITEURL = settings['SITEURL']
65
+    _SUBSITE_QUEUE = settings.get('I18N_SUBSITES', {}).copy()
66
+    prepare_site_db_and_overrides()
67
+    # clear databases in case of autoreload mode
68
+    _SITES_RELPATH_DB.clear()
69
+    _NATIVE_CONTENT_URL_DB.clear()
70
+    _GENERATOR_DB.clear()
71
+
72
+
73
+def prepare_site_db_and_overrides():
74
+    '''Prepare overrides and create _SITE_DB
75
+
76
+    _SITE_DB.keys() need to be ready for filter_translations
77
+    '''
78
+    _SITE_DB.clear()
79
+    _SITE_DB[_MAIN_LANG] = _MAIN_SITEURL
80
+    # make sure it works for both root-relative and absolute
81
+    main_siteurl = '/' if _MAIN_SITEURL == '' else _MAIN_SITEURL
82
+    for lang, overrides in _SUBSITE_QUEUE.items():
83
+        if 'SITEURL' not in overrides:
84
+            overrides['SITEURL'] = posixpath.join(main_siteurl, lang)
85
+        _SITE_DB[lang] = overrides['SITEURL']
86
+        # default subsite hierarchy
87
+        if 'OUTPUT_PATH' not in overrides:
88
+            overrides['OUTPUT_PATH'] = os.path.join(
89
+                _MAIN_SETTINGS['OUTPUT_PATH'], lang)
90
+        if 'CACHE_PATH' not in overrides:
91
+            overrides['CACHE_PATH'] = os.path.join(
92
+                _MAIN_SETTINGS['CACHE_PATH'], lang)
93
+        if 'STATIC_PATHS' not in overrides:
94
+            overrides['STATIC_PATHS'] = []
95
+        if ('THEME' not in overrides and 'THEME_STATIC_DIR' not in overrides and
96
+                'THEME_STATIC_PATHS' not in overrides):
97
+            relpath = relpath_to_site(lang, _MAIN_LANG)
98
+            overrides['THEME_STATIC_DIR'] = posixpath.join(
99
+                relpath, _MAIN_SETTINGS['THEME_STATIC_DIR'])
100
+            overrides['THEME_STATIC_PATHS'] = []
101
+        # to change what is perceived as translations
102
+        overrides['DEFAULT_LANG'] = lang
103
+
104
+
105
+def subscribe_filter_to_signals(settings):
106
+    '''Subscribe content filter to requested signals'''
107
+    for sig in settings.get('I18N_FILTER_SIGNALS', []):
108
+        sig.connect(filter_contents_translations)
109
+
110
+
111
+def initialize_plugin(pelican_obj):
112
+    '''Initialize plugin variables and Pelican settings'''
113
+    if _MAIN_SETTINGS is None:
114
+        initialize_dbs(pelican_obj.settings)
115
+        subscribe_filter_to_signals(pelican_obj.settings)
116
+
117
+
118
+def get_site_path(url):
119
+    '''Get the path component of an url, excludes siteurl
120
+
121
+    also normalizes '' to '/' for relpath to work,
122
+    otherwise it could be interpreted as a relative filesystem path
123
+    '''
124
+    path = urlparse(url).path
125
+    if path == '':
126
+        path = '/'
127
+    return path
128
+
129
+
130
+def relpath_to_site(lang, target_lang):
131
+    '''Get relative path from siteurl of lang to siteurl of base_lang
132
+
133
+    the output is cached in _SITES_RELPATH_DB
134
+    '''
135
+    path = _SITES_RELPATH_DB.get((lang, target_lang), None)
136
+    if path is None:
137
+        siteurl = _SITE_DB.get(lang, _MAIN_SITEURL)
138
+        target_siteurl = _SITE_DB.get(target_lang, _MAIN_SITEURL)
139
+        path = posixpath.relpath(get_site_path(target_siteurl),
140
+                                 get_site_path(siteurl))
141
+        _SITES_RELPATH_DB[(lang, target_lang)] = path
142
+    return path
143
+
144
+
145
+def save_generator(generator):
146
+    '''Save the generator for later use
147
+
148
+    initialize the removed content list
149
+    '''
150
+    _GENERATOR_DB[generator] = []
151
+
152
+
153
+def article2draft(article):
154
+    '''Transform an Article to Draft'''
155
+    draft = Draft(article._content, article.metadata, article.settings,
156
+                  article.source_path, article._context)
157
+    draft.status = 'draft'
158
+    return draft
159
+
160
+
161
+def page2hidden_page(page):
162
+    '''Transform a Page to a hidden Page'''
163
+    page.status = 'hidden'
164
+    return page
165
+
166
+
167
+class GeneratorInspector(object):
168
+    '''Inspector of generator instances'''
169
+
170
+    generators_info = {
171
+        ArticlesGenerator: {
172
+            'translations_lists': ['translations', 'drafts_translations'],
173
+            'contents_lists': [('articles', 'drafts')],
174
+            'hiding_func': article2draft,
175
+            'policy': 'I18N_UNTRANSLATED_ARTICLES',
176
+        },
177
+        PagesGenerator: {
178
+            'translations_lists': ['translations', 'hidden_translations'],
179
+            'contents_lists': [('pages', 'hidden_pages')],
180
+            'hiding_func': page2hidden_page,
181
+            'policy': 'I18N_UNTRANSLATED_PAGES',
182
+        },
183
+    }
184
+
185
+    def __init__(self, generator):
186
+        '''Identify the best known class of the generator instance
187
+
188
+        The class '''
189
+        self.generator = generator
190
+        self.generators_info.update(generator.settings.get(
191
+            'I18N_GENERATORS_INFO', {}))
192
+        for cls in generator.__class__.__mro__:
193
+            if cls in self.generators_info:
194
+                self.info = self.generators_info[cls]
195
+                break
196
+        else:
197
+            self.info = {}
198
+
199
+    def translations_lists(self):
200
+        '''Iterator over lists of content translations'''
201
+        return (getattr(self.generator, name) for name in
202
+                self.info.get('translations_lists', []))
203
+
204
+    def contents_list_pairs(self):
205
+        '''Iterator over pairs of normal and hidden contents'''
206
+        return (tuple(getattr(self.generator, name) for name in names)
207
+                for names in self.info.get('contents_lists', []))
208
+
209
+    def hiding_function(self):
210
+        '''Function for transforming content to a hidden version'''
211
+        hiding_func = self.info.get('hiding_func', lambda x: x)
212
+        return hiding_func
213
+
214
+    def untranslated_policy(self, default):
215
+        '''Get the policy for untranslated content'''
216
+        return self.generator.settings.get(self.info.get('policy', None),
217
+                                           default)
218
+
219
+    def all_contents(self):
220
+        '''Iterator over all contents'''
221
+        translations_iterator = chain(*self.translations_lists())
222
+        return chain(translations_iterator,
223
+                     *(pair[i] for pair in self.contents_list_pairs()
224
+                       for i in (0, 1)))
225
+
226
+
227
+def filter_contents_translations(generator):
228
+    '''Filter the content and translations lists of a generator
229
+
230
+    Filters out
231
+        1) translations which will be generated in a different site
232
+        2) content that is not in the language of the currently
233
+        generated site but in that of a different site, content in a
234
+        language which has no site is generated always. The filtering
235
+        method bay be modified by the respective untranslated policy
236
+    '''
237
+    inspector = GeneratorInspector(generator)
238
+    current_lang = generator.settings['DEFAULT_LANG']
239
+    langs_with_sites = _SITE_DB.keys()
240
+    removed_contents = _GENERATOR_DB[generator]
241
+
242
+    for translations in inspector.translations_lists():
243
+        for translation in translations[:]:    # copy to be able to remove
244
+            if translation.lang in langs_with_sites:
245
+                translations.remove(translation)
246
+                removed_contents.append(translation)
247
+
248
+    hiding_func = inspector.hiding_function()
249
+    untrans_policy = inspector.untranslated_policy(default='hide')
250
+    for (contents, other_contents) in inspector.contents_list_pairs():
251
+        for content in other_contents: # save any hidden native content first
252
+            if content.lang == current_lang: # in native lang
253
+                # save the native URL attr formatted in the current locale
254
+                _NATIVE_CONTENT_URL_DB[content.source_path] = content.url
255
+        for content in contents[:]:        # copy for removing in loop
256
+            if content.lang == current_lang: # in native lang
257
+                # save the native URL attr formatted in the current locale
258
+                _NATIVE_CONTENT_URL_DB[content.source_path] = content.url
259
+            elif content.lang in langs_with_sites and untrans_policy != 'keep':
260
+                contents.remove(content)
261
+                if untrans_policy == 'hide':
262
+                    other_contents.append(hiding_func(content))
263
+                elif untrans_policy == 'remove':
264
+                    removed_contents.append(content)
265
+
266
+
267
+def install_templates_translations(generator):
268
+    '''Install gettext translations in the jinja2.Environment
269
+
270
+    Only if the 'jinja2.ext.i18n' jinja2 extension is enabled
271
+    the translations for the current DEFAULT_LANG are installed.
272
+    '''
273
+    if 'JINJA_ENVIRONMENT' in generator.settings: # pelican 3.7+
274
+        jinja_extensions = generator.settings['JINJA_ENVIRONMENT'].get(
275
+            'extensions', [])
276
+    else:
277
+        jinja_extensions = generator.settings['JINJA_EXTENSIONS']
278
+
279
+    if 'jinja2.ext.i18n' in jinja_extensions:
280
+        domain = generator.settings.get('I18N_GETTEXT_DOMAIN', 'messages')
281
+        localedir = generator.settings.get('I18N_GETTEXT_LOCALEDIR')
282
+        if localedir is None:
283
+            localedir = os.path.join(generator.theme, 'translations')
284
+        current_lang = generator.settings['DEFAULT_LANG']
285
+        if current_lang == generator.settings.get('I18N_TEMPLATES_LANG',
286
+                                                  _MAIN_LANG):
287
+            translations = gettext.NullTranslations()
288
+        else:
289
+            langs = [current_lang]
290
+            try:
291
+                translations = gettext.translation(domain, localedir, langs)
292
+            except (IOError, OSError):
293
+                _LOGGER.error((
294
+                    "Cannot find translations for language '{}' in '{}' with "
295
+                    "domain '{}'. Installing NullTranslations.").format(
296
+                        langs[0], localedir, domain))
297
+                translations = gettext.NullTranslations()
298
+        newstyle = generator.settings.get('I18N_GETTEXT_NEWSTYLE', True)
299
+        generator.env.install_gettext_translations(translations, newstyle)
300
+
301
+
302
+def add_variables_to_context(generator):
303
+    '''Adds useful iterable variables to template context'''
304
+    context = generator.context             # minimize attr lookup
305
+    context['relpath_to_site'] = relpath_to_site
306
+    context['main_siteurl'] = _MAIN_SITEURL
307
+    context['main_lang'] = _MAIN_LANG
308
+    context['lang_siteurls'] = _SITE_DB
309
+    current_lang = generator.settings['DEFAULT_LANG']
310
+    extra_siteurls = _SITE_DB.copy()
311
+    extra_siteurls.pop(current_lang)
312
+    context['extra_siteurls'] = extra_siteurls
313
+
314
+
315
+def interlink_translations(content):
316
+    '''Link content to translations in their main language
317
+
318
+    so the URL (including localized month names) of the different subsites
319
+    will be honored
320
+    '''
321
+    lang = content.lang
322
+    # sort translations by lang
323
+    content.translations.sort(key=attrgetter('lang'))
324
+    for translation in content.translations:
325
+        relpath = relpath_to_site(lang, translation.lang)
326
+        url = _NATIVE_CONTENT_URL_DB[translation.source_path]
327
+        translation.override_url = posixpath.join(relpath, url)
328
+
329
+
330
+def interlink_translated_content(generator):
331
+    '''Make translations link to the native locations
332
+
333
+    for generators that may contain translated content
334
+    '''
335
+    inspector = GeneratorInspector(generator)
336
+    for content in inspector.all_contents():
337
+        interlink_translations(content)
338
+
339
+
340
+def interlink_removed_content(generator):
341
+    '''For all contents removed from generation queue update interlinks
342
+
343
+    link to the native location
344
+    '''
345
+    current_lang = generator.settings['DEFAULT_LANG']
346
+    for content in _GENERATOR_DB[generator]:
347
+        url = _NATIVE_CONTENT_URL_DB[content.source_path]
348
+        relpath = relpath_to_site(current_lang, content.lang)
349
+        content.override_url = posixpath.join(relpath, url)
350
+
351
+
352
+def interlink_static_files(generator):
353
+    '''Add links to static files in the main site if necessary'''
354
+    if generator.settings['STATIC_PATHS'] != []:
355
+        return                               # customized STATIC_PATHS
356
+    filenames = generator.context['filenames'] # minimize attr lookup
357
+    relpath = relpath_to_site(generator.settings['DEFAULT_LANG'], _MAIN_LANG)
358
+    for staticfile in _MAIN_STATIC_FILES:
359
+        if staticfile.get_relative_source_path() not in filenames:
360
+            staticfile = copy(staticfile) # prevent override in main site
361
+            staticfile.override_url = posixpath.join(relpath, staticfile.url)
362
+            generator.add_source_path(staticfile)
363
+
364
+
365
+def save_main_static_files(static_generator):
366
+    '''Save the static files generated for the main site'''
367
+    global _MAIN_STATIC_FILES
368
+    # test just for current lang as settings change in autoreload mode
369
+    if static_generator.settings['DEFAULT_LANG'] == _MAIN_LANG:
370
+        _MAIN_STATIC_FILES = static_generator.staticfiles
371
+
372
+
373
+def update_generators():
374
+    '''Update the context of all generators
375
+
376
+    Ads useful variables and translations into the template context
377
+    and interlink translations
378
+    '''
379
+    for generator in _GENERATOR_DB.keys():
380
+        install_templates_translations(generator)
381
+        add_variables_to_context(generator)
382
+        interlink_static_files(generator)
383
+        interlink_removed_content(generator)
384
+        interlink_translated_content(generator)
385
+
386
+
387
+def get_pelican_cls(settings):
388
+    '''Get the Pelican class requested in settings'''
389
+    cls = settings['PELICAN_CLASS']
390
+    if isinstance(cls, six.string_types):
391
+        module, cls_name = cls.rsplit('.', 1)
392
+        module = __import__(module)
393
+        cls = getattr(module, cls_name)
394
+    return cls
395
+
396
+
397
+def create_next_subsite(pelican_obj):
398
+    '''Create the next subsite using the lang-specific config
399
+
400
+    If there are no more subsites in the generation queue, update all
401
+    the generators (interlink translations and removed content, add
402
+    variables and translations to template context). Otherwise get the
403
+    language and overrides for next the subsite in the queue and apply
404
+    overrides.  Then generate the subsite using a PELICAN_CLASS
405
+    instance and its run method. Finally, restore the previous locale.
406
+    '''
407
+    global _MAIN_SETTINGS
408
+    if len(_SUBSITE_QUEUE) == 0:
409
+        _LOGGER.debug(
410
+            'i18n: Updating cross-site links and context of all generators.')
411
+        update_generators()
412
+        _MAIN_SETTINGS = None             # to initialize next time
413
+    else:
414
+        with temporary_locale():
415
+            settings = _MAIN_SETTINGS.copy()
416
+            lang, overrides = _SUBSITE_QUEUE.popitem()
417
+            settings.update(overrides)
418
+            settings = configure_settings(settings)      # to set LOCALE, etc.
419
+            cls = get_pelican_cls(settings)
420
+
421
+            new_pelican_obj = cls(settings)
422
+            _LOGGER.debug(("Generating i18n subsite for language '{}' "
423
+                           "using class {}").format(lang, cls))
424
+            new_pelican_obj.run()
425
+
426
+
427
+# map: signal name -> function name
428
+_SIGNAL_HANDLERS_DB = {
429
+    'get_generators': initialize_plugin,
430
+    'article_generator_pretaxonomy': filter_contents_translations,
431
+    'page_generator_finalized': filter_contents_translations,
432
+    'get_writer': create_next_subsite,
433
+    'static_generator_finalized': save_main_static_files,
434
+    'generator_init': save_generator,
435
+}
436
+
437
+
438
+def register():
439
+    '''Register the plugin only if required signals are available'''
440
+    for sig_name in _SIGNAL_HANDLERS_DB.keys():
441
+        if not hasattr(signals, sig_name):
442
+            _LOGGER.error((
443
+                'The i18n_subsites plugin requires the {} '
444
+                'signal available for sure in Pelican 3.4.0 and later, '
445
+                'plugin will not be used.').format(sig_name))
446
+            return
447
+
448
+    for sig_name, handler in _SIGNAL_HANDLERS_DB.items():
449
+        sig = getattr(signals, sig_name)
450
+        sig.connect(handler)

+ 128
- 0
plugins/i18n_subsites/implementing_language_buttons.rst View File

@@ -0,0 +1,128 @@
1
+-----------------------------
2
+Implementing language buttons
3
+-----------------------------
4
+
5
+Each article with translations has translations links, but that's the
6
+only way to switch between language subsites.
7
+
8
+For this reason it is convenient to add language buttons to the top
9
+menu bar to make it simple to switch between the language subsites on
10
+all pages.
11
+
12
+Example designs
13
+---------------
14
+
15
+Language buttons showing other available languages
16
+..................................................
17
+
18
+The ``extra_siteurls`` dictionary is a mapping of all other (not the
19
+``DEFAULT_LANG`` of the current (sub-)site) languages to the
20
+``SITEURL`` of the respective (sub-)sites
21
+
22
+.. code-block:: jinja
23
+
24
+   <!-- SNIP -->
25
+   <nav><ul>
26
+   {% if extra_siteurls %}
27
+   {% for lang, url in extra_siteurls.items() %}
28
+   <li><a href="{{ url }}/">{{ lang }}</a></li>
29
+   {% endfor %}
30
+   <!-- separator -->
31
+   <li style="background-color: white; padding: 5px;">&nbsp</li>
32
+   {% endif %}
33
+   {% for title, link in MENUITEMS %}
34
+   <!-- SNIP -->
35
+
36
+Notice the slash ``/`` after ``{{ url }}``, this makes sure it works
37
+with local development when ``SITEURL == ''``.
38
+
39
+Language buttons showing all available languages, current is active
40
+...................................................................
41
+
42
+The ``extra_siteurls`` dictionary is a mapping of all languages to the
43
+``SITEURL`` of the respective (sub-)sites. This template sets the
44
+language of the current (sub-)site as active.
45
+
46
+.. code-block:: jinja
47
+
48
+   <!-- SNIP -->
49
+   <nav><ul>
50
+   {% if lang_siteurls %}
51
+   {% for lang, url in lang_siteurls.items() %}
52
+   <li{% if lang == DEFAULT_LANG %} class="active"{% endif %}><a href="{{ url }}/">{{ lang }}</a></li>
53
+   {% endfor %}
54
+   <!-- separator -->
55
+   <li style="background-color: white; padding: 5px;">&nbsp</li>
56
+   {% endif %}
57
+   {% for title, link in MENUITEMS %}
58
+   <!-- SNIP -->
59
+
60
+
61
+Tips and tricks
62
+---------------
63
+
64
+Showing more than language codes
65
+................................
66
+
67
+If you want the language buttons to show e.g. the names of the
68
+languages or flags [#flags]_, not just the language codes, you can use
69
+a jinja filter to translate the language codes
70
+
71
+
72
+.. code-block:: python
73
+
74
+   languages_lookup = {
75
+		'en': 'English',
76
+		'cz': 'Čeština',
77
+		}
78
+
79
+   def lookup_lang_name(lang_code):
80
+       return languages_lookup[lang_code]
81
+
82
+   JINJA_FILTERS = {
83
+		...
84
+		'lookup_lang_name': lookup_lang_name,
85
+		}
86
+
87
+And then the link content becomes
88
+
89
+.. code-block:: jinja
90
+
91
+   <!-- SNIP -->
92
+   {% for lang, siteurl in lang_siteurls.items() %}
93
+   <li{% if lang == DEFAULT_LANG %} class="active"{% endif %}><a href="{{ siteurl }}/">{{ lang | lookup_lang_name }}</a></li>
94
+   {% endfor %}
95
+   <!-- SNIP -->
96
+
97
+
98
+Changing the order of language buttons
99
+......................................
100
+
101
+Because ``lang_siteurls`` and ``extra_siteurls`` are instances of
102
+``OrderedDict`` with ``main_lang`` being always the first key, you can
103
+change the order through a jinja filter.
104
+
105
+.. code-block:: python
106
+
107
+   def my_ordered_items(ordered_dict):
108
+       items = list(ordered_dict.items())
109
+       # swap first and last using tuple unpacking
110
+       items[0], items[-1] = items[-1], items[0]
111
+       return items
112
+
113
+   JINJA_FILTERS = {
114
+		...
115
+		'my_ordered_items': my_ordered_items,
116
+		}
117
+
118
+And then the ``for`` loop line in the template becomes
119
+
120
+.. code-block:: jinja
121
+
122
+   <!-- SNIP -->
123
+   {% for lang, siteurl in lang_siteurls | my_ordered_items %}
124
+   <!-- SNIP -->
125
+
126
+
127
+.. [#flags] Although it may look nice, `w3 discourages it
128
+            <http://www.w3.org/TR/i18n-html-tech-lang/#ri20040808.173208643>`_.

+ 200
- 0
plugins/i18n_subsites/localizing_using_jinja2.rst View File

@@ -0,0 +1,200 @@
1
+-----------------------------
2
+Localizing themes with Jinja2
3
+-----------------------------
4
+
5
+1. Localize templates
6
+---------------------
7
+
8
+To enable the |ext| extension in your templates, you must add it to
9
+``JINJA_EXTENSIONS`` in your Pelican configuration
10
+
11
+.. code-block:: python
12
+
13
+  JINJA_EXTENSIONS = ['jinja2.ext.i18n', ...]
14
+
15
+Then follow the `Jinja2 templating documentation for the I18N plugin
16
+<http://jinja.pocoo.org/docs/templates/#i18n>`_ to make your templates
17
+localizable. This usually means surrounding strings with the ``{%
18
+trans %}`` directive or using ``gettext()`` in expressions
19
+
20
+.. code-block:: jinja
21
+
22
+    {% trans %}translatable content{% endtrans %}
23
+    {{ gettext('a translatable string') }}
24
+
25
+For pluralization support, etc. consult the documentation.
26
+
27
+To enable `newstyle gettext calls
28
+<http://jinja.pocoo.org/docs/extensions/#newstyle-gettext>`_ the
29
+``I18N_GETTEXT_NEWSTYLE`` config variable must be set to ``True``
30
+(default).
31
+
32
+.. |ext| replace:: ``jinja2.ext.i18n``
33
+
34
+2. Specify translations location
35
+--------------------------------
36
+
37
+The |ext| extension uses the `Python gettext library
38
+<http://docs.python.org/library/gettext.html>`_ for translating
39
+strings.
40
+
41
+In your Pelican config you can give the path in which to look for
42
+translations in the ``I18N_GETTEXT_LOCALEDIR`` variable. If not given,
43
+it is assumed to be the ``translations`` subfolder in the top folder
44
+of the theme specified by ``THEME``.
45
+
46
+The domain of the translations (the name of each translation file is
47
+``domain.mo``) is controlled by the ``I18N_GETTEXT_DOMAIN`` config
48
+variable (defaults to ``messages``).
49
+
50
+Example
51
+.......
52
+
53
+With the following in your Pelican settings file
54
+
55
+.. code-block:: python
56
+
57
+  I18N_GETTEXT_LOCALEDIR = 'some/path/'
58
+  I18N_GETTEXT_DOMAIN = 'my_domain'
59
+
60
+the translation for language 'cz' will be expected to be in
61
+``some/path/cz/LC_MESSAGES/my_domain.mo``
62
+
63
+
64
+3. Extract translatable strings and translate them
65
+--------------------------------------------------
66
+
67
+There are many ways to extract translatable strings and create
68
+``gettext`` compatible translations. You can create the ``*.po`` and
69
+``*.mo`` message catalog files yourself, or you can use some helper
70
+tool as described in `the Python gettext library tutorial
71
+<http://docs.python.org/library/gettext.html#internationalizing-your-programs-and-modules>`_.
72
+
73
+You of course don't need to provide a translation for the language in
74
+which the templates are written which is assumed to be the original
75
+``DEFAULT_LANG``. This can be overridden in the
76
+``I18N_TEMPLATES_LANG`` variable.
77
+
78
+Recommended tool: babel
79
+.......................
80
+
81
+`Babel <http://babel.pocoo.org/>`_ makes it easy to extract
82
+translatable strings from the localized Jinja2 templates and assists
83
+with creating translations as documented in this `Jinja2-Babel
84
+tutorial
85
+<http://pythonhosted.org/Flask-Babel/#translating-applications>`_
86
+[#flask]_ on which the following is based.
87
+
88
+1. Add babel mapping
89
+~~~~~~~~~~~~~~~~~~~~
90
+
91
+Let's assume that you are localizing a theme in ``themes/my_theme/``
92
+and that you use the default settings, i.e. the default domain
93
+``messages`` and will put the translations in the ``translations``
94
+subdirectory of the theme directory as
95
+``themes/my_theme/translations/``.
96
+
97
+It is up to you where to store babel mappings and translation files
98
+templates (``*.pot``), but a convenient place is to put them in
99
+``themes/my_theme/`` and work in that directory. From now on let's
100
+assume that it will be our current working directory (CWD).
101
+
102
+To tell babel to extract translatable strings from the templates
103
+create a mapping file ``babel.cfg`` with the following line
104
+
105
+.. code-block:: cfg
106
+
107
+    [jinja2: templates/**.html]
108
+
109
+
110
+2. Extract translatable strings from templates
111
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
112
+
113
+Run the following command to create a ``messages.pot`` message catalog
114
+template file from extracted translatable strings
115
+
116
+.. code-block:: bash
117
+
118
+    pybabel extract --mapping babel.cfg --output messages.pot ./
119
+
120
+
121
+3. Initialize message catalogs
122
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
123
+
124
+If you want to translate the template to language ``lang``, run the
125
+following command to create a message catalog
126
+``translations/lang/LC_MESSAGES/messages.po`` using the template
127
+``messages.pot``
128
+
129
+.. code-block:: bash
130
+
131
+    pybabel init --input-file messages.pot --output-dir translations/ --locale lang --domain messages
132
+
133
+babel expects ``lang`` to be a valid locale identifier, so if e.g. you
134
+are translating for language ``cz`` but the corresponding locale is
135
+``cs``, you have to use the locale identifier. Nevertheless, the
136
+gettext infrastructure should later correctly find the locale for a
137
+given language.
138
+
139
+4. Fill the message catalogs
140
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
141
+
142
+The message catalog files format is quite intuitive, it is fully
143
+documented in the `GNU gettext manual
144
+<http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files>`_. Essentially,
145
+you fill in the ``msgstr`` strings
146
+
147
+
148
+.. code-block:: po
149
+
150
+    msgid "just a simple string"
151
+    msgstr "jenom jednoduchý řetězec"
152
+
153
+    msgid ""
154
+    "some multiline string"
155
+    "looks like this"
156
+    msgstr ""
157
+    "nějaký více řádkový řetězec"
158
+    "vypadá takto"
159
+
160
+You might also want to remove ``#,fuzzy`` flags once the translation
161
+is complete and reviewed to show that it can be compiled.
162
+
163
+5. Compile the message catalogs
164
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
165
+
166
+The message catalogs must be compiled into binary format using this
167
+command
168
+
169
+.. code-block:: bash
170
+
171
+    pybabel compile --directory translations/ --domain messages
172
+
173
+This command might complain about "fuzzy" translations, which means
174
+you should review the translations and once done, remove the fuzzy
175
+flag line.
176
+
177
+(6.) Update the catalogs when templates change
178
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
179
+
180
+If you add any translatable patterns into your templates, you have to
181
+update your message catalogs too.  First you extract a new message
182
+catalog template as described in the 2. step. Then you run the
183
+following command [#pybabel_error]_
184
+
185
+.. code-block:: bash
186
+
187
+   pybabel update --input-file messages.pot --output-dir translations/ --domain messages
188
+
189
+This will merge the new patterns with the old ones. Once you review
190
+and fill them, you have to recompile them as described in the 5. step.
191
+
192
+.. [#flask] Although the tutorial is focused on Flask-based web
193
+            applications, the linked translation tutorial is not
194
+            Flask-specific.
195
+.. [#pybabel_error] If you get an error ``TypeError: must be str, not
196
+                    bytes`` with Python 3.3, it is likely you are
197
+                    suffering from this `bug
198
+                    <https://github.com/mitsuhiko/flask-babel/issues/43>`_.
199
+                    Until the fix is released, you can use babel with
200
+                    Python 2.7.

+ 0
- 0
plugins/i18n_subsites/test_data/content/images/img.png View File


+ 7
- 0
plugins/i18n_subsites/test_data/content/pages/hidden-page-cz.rst View File

@@ -0,0 +1,7 @@
1
+404 stránka
2
+===========
3
+:slug: 404
4
+:lang: cz
5
+:status: hidden
6
+
7
+Jednoduchá 404 stránka.

+ 7
- 0
plugins/i18n_subsites/test_data/content/pages/hidden-page-de.rst View File

@@ -0,0 +1,7 @@
1
+Eine 404 Seite
2
+==============
3
+:slug: 404
4
+:lang: de
5
+:status: hidden
6
+
7
+Eine einfache 404 Seite.

+ 7
- 0
plugins/i18n_subsites/test_data/content/pages/hidden-page-en.rst View File

@@ -0,0 +1,7 @@
1
+A 404 page
2
+==========
3
+:slug: 404
4
+:lang: en
5
+:status: hidden
6
+
7
+A simple 404 page.

+ 5
- 0
plugins/i18n_subsites/test_data/content/pages/untranslated-page.rst View File

@@ -0,0 +1,5 @@
1
+Untranslated page
2
+=================
3
+:lang: en
4
+
5
+This page has no translation.

+ 8
- 0
plugins/i18n_subsites/test_data/content/translated_article-cz.rst View File

@@ -0,0 +1,8 @@
1
+Přeložený článek
2
+================
3
+:slug: translated-article
4
+:lang: cz
5
+:date: 2014-09-15
6
+
7
+Jednoduchý článek s překlady.
8
+Zde je odkaz na `nějaký obrázek <{filename}/images/img.png>`_.

+ 8
- 0
plugins/i18n_subsites/test_data/content/translated_article-de.rst View File

@@ -0,0 +1,8 @@
1
+Ein übersetzter Artikel
2
+=======================
3
+:slug: translated-article
4
+:lang: de
5
+:date: 2014-09-14
6
+
7
+Ein einfacher Artikel mit einer Übersetzung.
8
+Hier ist ein Link zur `einigem Bild <{filename}/images/img.png>`_.

+ 8
- 0
plugins/i18n_subsites/test_data/content/translated_article-en.rst View File

@@ -0,0 +1,8 @@
1
+A translated article
2
+====================
3
+:slug: translated-article
4
+:lang: en
5
+:date: 2014-09-13
6
+
7
+A simple article with a translation.
8
+Here is a link to `some image <{filename}/images/img.png>`_.

+ 9
- 0
plugins/i18n_subsites/test_data/content/untranslated_article-en.rst View File

@@ -0,0 +1,9 @@
1
+An untranslated article
2
+=======================
3
+:date: 2014-07-14
4
+:lang: en
5
+
6
+An article without a translation.
7
+Here is a link to an `untranslated page`_
8
+
9
+.. _`untranslated page`: {filename}/pages/untranslated-page.rst

+ 2
- 0
plugins/i18n_subsites/test_data/localized_theme/babel.cfg View File

@@ -0,0 +1,2 @@
1
+[jinja2: templates/**.html]
2
+

+ 23
- 0
plugins/i18n_subsites/test_data/localized_theme/messages.pot View File

@@ -0,0 +1,23 @@
1
+# Translations template for PROJECT.
2
+# Copyright (C) 2014 ORGANIZATION
3
+# This file is distributed under the same license as the PROJECT project.
4
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
5
+#
6
+#, fuzzy
7
+msgid ""
8
+msgstr ""
9
+"Project-Id-Version: PROJECT VERSION\n"
10
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
11
+"POT-Creation-Date: 2014-07-13 12:25+0200\n"
12
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
+"Language-Team: LANGUAGE <LL@li.org>\n"
15
+"MIME-Version: 1.0\n"
16
+"Content-Type: text/plain; charset=utf-8\n"
17
+"Content-Transfer-Encoding: 8bit\n"
18
+"Generated-By: Babel 1.3\n"
19
+
20
+#: templates/base.html:3
21
+msgid "Welcome to our"
22
+msgstr ""
23
+

+ 0
- 0
plugins/i18n_subsites/test_data/localized_theme/static/style.css View File


+ 7
- 0
plugins/i18n_subsites/test_data/localized_theme/templates/base.html View File

@@ -0,0 +1,7 @@
1
+{% extends "!simple/base.html" %}
2
+
3
+{% block title %}{% trans %}Welcome to our{% endtrans %} {{ SITENAME }}{% endblock %}
4
+{% block head %}
5
+{{ super() }}
6
+<link rel="stylesheet" href="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/style.css" />
7
+{% endblock %}

BIN
plugins/i18n_subsites/test_data/localized_theme/translations/de/LC_MESSAGES/messages.mo View File


+ 23
- 0
plugins/i18n_subsites/test_data/localized_theme/translations/de/LC_MESSAGES/messages.po View File

@@ -0,0 +1,23 @@
1
+# German translations for PROJECT.
2
+# Copyright (C) 2014 ORGANIZATION
3
+# This file is distributed under the same license as the PROJECT project.
4
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
5
+#
6
+msgid ""
7
+msgstr ""
8
+"Project-Id-Version: PROJECT VERSION\n"
9
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10
+"POT-Creation-Date: 2014-07-13 12:25+0200\n"
11
+"PO-Revision-Date: 2014-07-13 12:26+0200\n"
12
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13
+"Language-Team: de <LL@li.org>\n"
14
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
15
+"MIME-Version: 1.0\n"
16
+"Content-Type: text/plain; charset=utf-8\n"
17
+"Content-Transfer-Encoding: 8bit\n"
18
+"Generated-By: Babel 1.3\n"
19
+
20
+#: templates/base.html:3
21
+msgid "Welcome to our"
22
+msgstr "Willkommen Sie zur unserer"
23
+

+ 53
- 0
plugins/i18n_subsites/test_data/pelicanconf.py View File

@@ -0,0 +1,53 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*- #
3
+from __future__ import unicode_literals
4
+
5
+AUTHOR = 'The Tester'
6
+SITENAME = 'Testing site'
7
+SITEURL = 'http://example.com/test'
8
+
9
+# to make the test suite portable
10
+TIMEZONE = 'UTC'
11
+
12
+DEFAULT_LANG = 'en'
13
+LOCALE = 'en_US.UTF-8'
14
+
15
+# Generate only one feed
16
+FEED_ALL_ATOM = 'feeds_all.atom.xml'
17
+CATEGORY_FEED_ATOM = None
18
+TRANSLATION_FEED_ATOM = None
19
+AUTHOR_FEED_ATOM = None
20
+AUTHOR_FEED_RSS = None
21
+
22
+# Disable unnecessary pages
23
+CATEGORY_SAVE_AS = ''
24
+TAG_SAVE_AS = ''
25
+AUTHOR_SAVE_AS = ''
26
+ARCHIVES_SAVE_AS = ''
27
+AUTHORS_SAVE_AS = ''
28
+CATEGORIES_SAVE_AS = ''
29
+TAGS_SAVE_AS = ''
30
+
31
+PLUGIN_PATHS = ['../../']
32
+PLUGINS = ['i18n_subsites']
33
+
34
+THEME = 'localized_theme'
35
+JINJA_ENVIRONMENT = {'extensions': ['jinja2.ext.i18n']}
36
+
37
+from blinker import signal
38
+tmpsig = signal('tmpsig')
39
+I18N_FILTER_SIGNALS = [tmpsig]
40
+
41
+I18N_SUBSITES = {
42
+    'de': {
43
+        'SITENAME': 'Testseite',
44
+        'AUTHOR': 'Der Tester',
45
+        'LOCALE': 'de_DE.UTF-8',
46
+        },
47
+    'cz': {
48
+        'SITENAME': 'Testovací stránka',
49
+        'AUTHOR': 'Test Testovič',
50
+        'I18N_UNTRANSLATED_PAGES': 'remove',
51
+        'I18N_UNTRANSLATED_ARTICLES': 'keep',
52
+        },
53
+    }

+ 139
- 0
plugins/i18n_subsites/test_i18n_subsites.py View File

@@ -0,0 +1,139 @@
1
+'''Unit tests for the i18n_subsites plugin'''
2
+
3
+import os
4
+import locale
5
+import unittest
6
+import subprocess
7
+from tempfile import mkdtemp
8
+from shutil import rmtree
9
+
10
+from . import i18n_subsites as i18ns
11
+from pelican import Pelican
12
+from pelican.tests.support import get_settings
13
+from pelican.settings import read_settings
14
+
15
+
16
+class TestTemporaryLocale(unittest.TestCase):
17
+    '''Test the temporary locale context manager'''
18
+
19
+    def test_locale_restored(self):
20
+        '''Test that the locale is restored after exiting context'''
21
+        orig_locale = locale.setlocale(locale.LC_ALL)
22
+        with i18ns.temporary_locale():
23
+            locale.setlocale(locale.LC_ALL, 'C')
24
+            self.assertEqual(locale.setlocale(locale.LC_ALL), 'C')
25
+        self.assertEqual(locale.setlocale(locale.LC_ALL), orig_locale)
26
+
27
+    def test_temp_locale_set(self):
28
+        '''Test that the temporary locale is set'''
29
+        with i18ns.temporary_locale('C'):
30
+            self.assertEqual(locale.setlocale(locale.LC_ALL), 'C')
31
+
32
+
33
+class TestSettingsManipulation(unittest.TestCase):
34
+    '''Test operations on settings dict'''
35
+
36
+    def setUp(self):
37
+        '''Prepare default settings'''
38
+        self.settings = get_settings()
39
+
40
+    def test_get_pelican_cls_class(self):
41
+        '''Test that we get class given as an object'''
42
+        self.settings['PELICAN_CLASS'] = object
43
+        cls = i18ns.get_pelican_cls(self.settings)
44
+        self.assertIs(cls, object)
45
+        
46
+    def test_get_pelican_cls_str(self):
47
+        '''Test that we get correct class given by string'''
48
+        cls = i18ns.get_pelican_cls(self.settings)
49
+        self.assertIs(cls, Pelican)
50
+        
51
+
52
+class TestSitesRelpath(unittest.TestCase):
53
+    '''Test relative path between sites generation'''
54
+
55
+    def setUp(self):
56
+        '''Generate some sample siteurls'''
57
+        self.siteurl = 'http://example.com'
58
+        i18ns._SITE_DB['en'] = self.siteurl
59
+        i18ns._SITE_DB['de'] = self.siteurl + '/de'
60
+
61
+    def tearDown(self):
62
+        '''Remove sites from db'''
63
+        i18ns._SITE_DB.clear()
64
+
65
+    def test_get_site_path(self):
66
+        '''Test getting the path within a site'''
67
+        self.assertEqual(i18ns.get_site_path(self.siteurl), '/')
68
+        self.assertEqual(i18ns.get_site_path(self.siteurl + '/de'), '/de')
69
+
70
+    def test_relpath_to_site(self):
71
+        '''Test getting relative paths between sites'''
72
+        self.assertEqual(i18ns.relpath_to_site('en', 'de'), 'de')
73
+        self.assertEqual(i18ns.relpath_to_site('de', 'en'), '..')
74
+
75
+        
76
+class TestRegistration(unittest.TestCase):
77
+    '''Test plugin registration'''
78
+
79
+    def test_return_on_missing_signal(self):
80
+        '''Test return on missing required signal'''
81
+        i18ns._SIGNAL_HANDLERS_DB['tmp_sig'] = None
82
+        i18ns.register()
83
+        self.assertNotIn(id(i18ns.save_generator),
84
+                         i18ns.signals.generator_init.receivers)
85
+
86
+    def test_registration(self):
87
+        '''Test registration of all signal handlers'''
88
+        i18ns.register()
89
+        for sig_name, handler in i18ns._SIGNAL_HANDLERS_DB.items():
90
+            sig = getattr(i18ns.signals, sig_name)
91
+            self.assertIn(id(handler), sig.receivers)
92
+            # clean up
93
+            sig.disconnect(handler)
94
+        
95
+
96
+class TestFullRun(unittest.TestCase):
97
+    '''Test running Pelican with the Plugin'''
98
+
99
+    def setUp(self):
100
+        '''Create temporary output and cache folders'''
101
+        self.temp_path = mkdtemp(prefix='pelicantests.')
102
+        self.temp_cache = mkdtemp(prefix='pelican_cache.')
103
+
104
+    def tearDown(self):
105
+        '''Remove output and cache folders'''
106
+        rmtree(self.temp_path)
107
+        rmtree(self.temp_cache)
108
+
109
+    def test_sites_generation(self):
110
+        '''Test generation of sites with the plugin
111
+
112
+        Compare with recorded output via ``git diff``.
113
+        To generate output for comparison run the command
114
+        ``pelican -o test_data/output -s test_data/pelicanconf.py \
115
+        test_data/content``
116
+        Remember to remove the output/ folder before that.
117
+        '''
118
+        base_path = os.path.dirname(os.path.abspath(__file__))
119
+        base_path = os.path.join(base_path, 'test_data')
120
+        content_path = os.path.join(base_path, 'content')
121
+        output_path = os.path.join(base_path, 'output')
122
+        settings_path = os.path.join(base_path, 'pelicanconf.py')
123
+        settings = read_settings(path=settings_path, override={
124
+            'PATH': content_path,
125
+            'OUTPUT_PATH': self.temp_path,
126
+            'CACHE_PATH': self.temp_cache,
127
+            'PLUGINS': [i18ns],
128
+            }
129
+        )
130
+        pelican = Pelican(settings)
131
+        pelican.run()
132
+
133
+        # compare output
134
+        out, err = subprocess.Popen(
135
+            ['git', 'diff', '--no-ext-diff', '--exit-code', '-w', output_path,
136
+             self.temp_path], env={'PAGER': ''},
137
+            stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
138
+        self.assertFalse(out, 'non-empty `diff` stdout:\n{}'.format(out))
139
+        self.assertFalse(err, 'non-empty `diff` stderr:\n{}'.format(out))

+ 23
- 0
plugins/toc/.travis.yml View File

@@ -0,0 +1,23 @@
1
+sudo: false
2
+language: python
3
+python:
4
+  - "2.7"
5
+env:
6
+  - TOX_ENV=py27-pelican34
7
+  - TOX_ENV=py27-pelican35
8
+  - TOX_ENV=py27-pelican36
9
+  - TOX_ENV=py27-pelicandev
10
+
11
+  - TOX_ENV=py33-pelican34
12
+  - TOX_ENV=py33-pelican35
13
+  - TOX_ENV=py33-pelican36
14
+  - TOX_ENV=py33-pelicandev
15
+
16
+  - TOX_ENV=py34-pelican34
17
+  - TOX_ENV=py34-pelican35
18
+  - TOX_ENV=py34-pelican36
19
+  - TOX_ENV=py34-pelicandev
20
+
21
+install:
22
+  - pip install tox==2.0.1
23
+script: tox -e $TOX_ENV

+ 339
- 0
plugins/toc/LICENSE View File

@@ -0,0 +1,339 @@
1
+GNU GENERAL PUBLIC LICENSE
2
+                       Version 2, June 1991
3
+
4
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
5
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6
+ Everyone is permitted to copy and distribute verbatim copies
7
+ of this license document, but changing it is not allowed.
8
+
9
+                            Preamble
10
+
11
+  The licenses for most software are designed to take away your
12
+freedom to share and change it.  By contrast, the GNU General Public
13
+License is intended to guarantee your freedom to share and change free
14
+software--to make sure the software is free for all its users.  This
15
+General Public License applies to most of the Free Software
16
+Foundation's software and to any other program whose authors commit to
17
+using it.  (Some other Free Software Foundation software is covered by
18
+the GNU Lesser General Public License instead.)  You can apply it to
19
+your programs, too.
20
+
21
+  When we speak of free software, we are referring to freedom, not
22
+price.  Our General Public Licenses are designed to make sure that you
23
+have the freedom to distribute copies of free software (and charge for
24
+this service if you wish), that you receive source code or can get it
25
+if you want it, that you can change the software or use pieces of it
26
+in new free programs; and that you know you can do these things.
27
+
28
+  To protect your rights, we need to make restrictions that forbid
29
+anyone to deny you these rights or to ask you to surrender the rights.
30
+These restrictions translate to certain responsibilities for you if you
31
+distribute copies of the software, or if you modify it.
32
+
33
+  For example, if you distribute copies of such a program, whether
34
+gratis or for a fee, you must give the recipients all the rights that
35
+you have.  You must make sure that they, too, receive or can get the
36
+source code.  And you must show them these terms so they know their
37
+rights.
38
+
39
+  We protect your rights with two steps: (1) copyright the software, and
40
+(2) offer you this license which gives you legal permission to copy,
41
+distribute and/or modify the software.
42
+
43
+  Also, for each author's protection and ours, we want to make certain
44
+that everyone understands that there is no warranty for this free
45
+software.  If the software is modified by someone else and passed on, we
46
+want its recipients to know that what they have is not the original, so
47
+that any problems introduced by others will not reflect on the original
48
+authors' reputations.
49
+
50
+  Finally, any free program is threatened constantly by software
51
+patents.  We wish to avoid the danger that redistributors of a free
52
+program will individually obtain patent licenses, in effect making the
53
+program proprietary.  To prevent this, we have made it clear that any
54
+patent must be licensed for everyone's free use or not licensed at all.
55
+
56
+  The precise terms and conditions for copying, distribution and
57
+modification follow.
58
+
59
+                    GNU GENERAL PUBLIC LICENSE
60
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61
+
62
+  0. This License applies to any program or other work which contains
63
+a notice placed by the copyright holder saying it may be distributed
64
+under the terms of this General Public License.  The "Program", below,
65
+refers to any such program or work, and a "work based on the Program"
66
+means either the Program or any derivative work under copyright law:
67
+that is to say, a work containing the Program or a portion of it,
68
+either verbatim or with modifications and/or translated into another
69
+language.  (Hereinafter, translation is included without limitation in
70
+the term "modification".)  Each licensee is addressed as "you".
71
+
72
+Activities other than copying, distribution and modification are not
73
+covered by this License; they are outside its scope.  The act of
74
+running the Program is not restricted, and the output from the Program
75
+is covered only if its contents constitute a work based on the
76
+Program (independent of having been made by running the Program).
77
+Whether that is true depends on what the Program does.
78
+
79
+  1. You may copy and distribute verbatim copies of the Program's
80
+source code as you receive it, in any medium, provided that you
81
+conspicuously and appropriately publish on each copy an appropriate
82
+copyright notice and disclaimer of warranty; keep intact all the
83
+notices that refer to this License and to the absence of any warranty;
84
+and give any other recipients of the Program a copy of this License
85
+along with the Program.
86
+
87
+You may charge a fee for the physical act of transferring a copy, and
88
+you may at your option offer warranty protection in exchange for a fee.
89
+
90
+  2. You may modify your copy or copies of the Program or any portion
91
+of it, thus forming a work based on the Program, and copy and
92
+distribute such modifications or work under the terms of Section 1
93
+above, provided that you also meet all of these conditions:
94
+
95
+    a) You must cause the modified files to carry prominent notices
96
+    stating that you changed the files and the date of any change.
97
+
98
+    b) You must cause any work that you distribute or publish, that in
99
+    whole or in part contains or is derived from the Program or any
100
+    part thereof, to be licensed as a whole at no charge to all third
101
+    parties under the terms of this License.
102
+
103
+    c) If the modified program normally reads commands interactively
104
+    when run, you must cause it, when started running for such
105
+    interactive use in the most ordinary way, to print or display an
106
+    announcement including an appropriate copyright notice and a
107
+    notice that there is no warranty (or else, saying that you provide
108
+    a warranty) and that users may redistribute the program under
109
+    these conditions, and telling the user how to view a copy of this
110
+    License.  (Exception: if the Program itself is interactive but
111
+    does not normally print such an announcement, your work based on
112
+    the Program is not required to print an announcement.)
113
+
114
+These requirements apply to the modified work as a whole.  If
115
+identifiable sections of that work are not derived from the Program,
116
+and can be reasonably considered independent and separate works in
117
+themselves, then this License, and its terms, do not apply to those
118
+sections when you distribute them as separate works.  But when you
119
+distribute the same sections as part of a whole which is a work based
120
+on the Program, the distribution of the whole must be on the terms of
121
+this License, whose permissions for other licensees extend to the
122
+entire whole, and thus to each and every part regardless of who wrote it.
123
+
124
+Thus, it is not the intent of this section to claim rights or contest
125
+your rights to work written entirely by you; rather, the intent is to
126
+exercise the right to control the distribution of derivative or
127
+collective works based on the Program.
128
+
129
+In addition, mere aggregation of another work not based on the Program
130
+with the Program (or with a work based on the Program) on a volume of
131
+a storage or distribution medium does not bring the other work under
132
+the scope of this License.
133
+
134
+  3. You may copy and distribute the Program (or a work based on it,
135
+under Section 2) in object code or executable form under the terms of
136
+Sections 1 and 2 above provided that you also do one of the following:
137
+
138
+    a) Accompany it with the complete corresponding machine-readable
139
+    source code, which must be distributed under the terms of Sections
140
+    1 and 2 above on a medium customarily used for software interchange; or,
141
+
142
+    b) Accompany it with a written offer, valid for at least three
143
+    years, to give any third party, for a charge no more than your
144
+    cost of physically performing source distribution, a complete
145
+    machine-readable copy of the corresponding source code, to be
146
+    distributed under the terms of Sections 1 and 2 above on a medium
147
+    customarily used for software interchange; or,
148
+
149
+    c) Accompany it with the information you received as to the offer
150
+    to distribute corresponding source code.  (This alternative is
151
+    allowed only for noncommercial distribution and only if you
152
+    received the program in object code or executable form with such
153
+    an offer, in accord with Subsection b above.)
154
+
155
+The source code for a work means the preferred form of the work for
156
+making modifications to it.  For an executable work, complete source
157
+code means all the source code for all modules it contains, plus any
158
+associated interface definition files, plus the scripts used to
159
+control compilation and installation of the executable.  However, as a
160
+special exception, the source code distributed need not include
161
+anything that is normally distributed (in either source or binary
162
+form) with the major components (compiler, kernel, and so on) of the
163
+operating system on which the executable runs, unless that component
164
+itself accompanies the executable.
165
+
166
+If distribution of executable or object code is made by offering
167
+access to copy from a designated place, then offering equivalent
168
+access to copy the source code from the same place counts as
169
+distribution of the source code, even though third parties are not
170
+compelled to copy the source along with the object code.
171
+
172
+  4. You may not copy, modify, sublicense, or distribute the Program
173
+except as expressly provided under this License.  Any attempt
174
+otherwise to copy, modify, sublicense or distribute the Program is
175
+void, and will automatically terminate your rights under this License.
176
+However, parties who have received copies, or rights, from you under
177
+this License will not have their licenses terminated so long as such
178
+parties remain in full compliance.
179
+
180
+  5. You are not required to accept this License, since you have not
181
+signed it.  However, nothing else grants you permission to modify or
182
+distribute the Program or its derivative works.  These actions are
183
+prohibited by law if you do not accept this License.  Therefore, by
184
+modifying or distributing the Program (or any work based on the
185
+Program), you indicate your acceptance of this License to do so, and
186
+all its terms and conditions for copying, distributing or modifying
187
+the Program or works based on it.
188
+
189
+  6. Each time you redistribute the Program (or any work based on the
190
+Program), the recipient automatically receives a license from the
191
+original licensor to copy, distribute or modify the Program subject to
192
+these terms and conditions.  You may not impose any further
193
+restrictions on the recipients' exercise of the rights granted herein.
194
+You are not responsible for enforcing compliance by third parties to
195
+this License.
196
+
197
+  7. If, as a consequence of a court judgment or allegation of patent
198
+infringement or for any other reason (not limited to patent issues),
199
+conditions are imposed on you (whether by court order, agreement or
200
+otherwise) that contradict the conditions of this License, they do not
201
+excuse you from the conditions of this License.  If you cannot
202
+distribute so as to satisfy simultaneously your obligations under this
203
+License and any other pertinent obligations, then as a consequence you
204
+may not distribute the Program at all.  For example, if a patent
205
+license would not permit royalty-free redistribution of the Program by
206
+all those who receive copies directly or indirectly through you, then
207
+the only way you could satisfy both it and this License would be to
208
+refrain entirely from distribution of the Program.
209
+
210
+If any portion of this section is held invalid or unenforceable under
211
+any particular circumstance, the balance of the section is intended to
212
+apply and the section as a whole is intended to apply in other
213
+circumstances.
214
+
215
+It is not the purpose of this section to induce you to infringe any
216
+patents or other property right claims or to contest validity of any
217
+such claims; this section has the sole purpose of protecting the
218
+integrity of the free software distribution system, which is
219
+implemented by public license practices.  Many people have made
220
+generous contributions to the wide range of software distributed
221
+through that system in reliance on consistent application of that
222
+system; it is up to the author/donor to decide if he or she is willing
223
+to distribute software through any other system and a licensee cannot
224
+impose that choice.
225
+
226
+This section is intended to make thoroughly clear what is believed to
227
+be a consequence of the rest of this License.
228
+
229
+  8. If the distribution and/or use of the Program is restricted in
230
+certain countries either by patents or by copyrighted interfaces, the
231
+original copyright holder who places the Program under this License
232
+may add an explicit geographical distribution limitation excluding
233
+those countries, so that distribution is permitted only in or among
234
+countries not thus excluded.  In such case, this License incorporates
235
+the limitation as if written in the body of this License.
236
+
237
+  9. The Free Software Foundation may publish revised and/or new versions
238
+of the General Public License from time to time.  Such new versions will
239
+be similar in spirit to the present version, but may differ in detail to
240
+address new problems or concerns.
241
+
242
+Each version is given a distinguishing version number.  If the Program
243
+specifies a version number of this License which applies to it and "any
244
+later version", you have the option of following the terms and conditions
245
+either of that version or of any later version published by the Free
246
+Software Foundation.  If the Program does not specify a version number of
247
+this License, you may choose any version ever published by the Free Software
248
+Foundation.
249
+
250
+  10. If you wish to incorporate parts of the Program into other free
251
+programs whose distribution conditions are different, write to the author
252
+to ask for permission.  For software which is copyrighted by the Free
253
+Software Foundation, write to the Free Software Foundation; we sometimes
254
+make exceptions for this.  Our decision will be guided by the two goals
255
+of preserving the free status of all derivatives of our free software and
256
+of promoting the sharing and reuse of software generally.
257
+
258
+                            NO WARRANTY
259
+
260
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
262
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
266
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
267
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268
+REPAIR OR CORRECTION.
269
+
270
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278
+POSSIBILITY OF SUCH DAMAGES.
279
+
280
+                     END OF TERMS AND CONDITIONS
281
+
282
+            How to Apply These Terms to Your New Programs
283
+
284
+  If you develop a new program, and you want it to be of the greatest
285
+possible use to the public, the best way to achieve this is to make it
286
+free software which everyone can redistribute and change under these terms.
287
+
288
+  To do so, attach the following notices to the program.  It is safest
289
+to attach them to the start of each source file to most effectively
290
+convey the exclusion of warranty; and each file should have at least
291
+the "copyright" line and a pointer to where the full notice is found.
292
+
293
+    {description}
294
+    Copyright (C) {year}  {fullname}
295
+
296
+    This program is free software; you can redistribute it and/or modify
297
+    it under the terms of the GNU General Public License as published by
298
+    the Free Software Foundation; either version 2 of the License, or
299
+    (at your option) any later version.
300
+
301
+    This program is distributed in the hope that it will be useful,
302
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
303
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
304
+    GNU General Public License for more details.
305
+
306
+    You should have received a copy of the GNU General Public License along
307
+    with this program; if not, write to the Free Software Foundation, Inc.,
308
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309
+
310
+Also add information on how to contact you by electronic and paper mail.
311
+
312
+If the program is interactive, make it output a short notice like this
313
+when it starts in an interactive mode:
314
+
315
+    Gnomovision version 69, Copyright (C) year name of author
316
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317
+    This is free software, and you are welcome to redistribute it
318
+    under certain conditions; type `show c' for details.
319
+
320
+The hypothetical commands `show w' and `show c' should show the appropriate
321
+parts of the General Public License.  Of course, the commands you use may
322
+be called something other than `show w' and `show c'; they could even be
323
+mouse-clicks or menu items--whatever suits your program.
324
+
325
+You should also get your employer (if you work as a programmer) or your
326
+school, if any, to sign a "copyright disclaimer" for the program, if
327
+necessary.  Here is a sample; alter the names:
328
+
329
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
331
+
332
+  {signature of Ty Coon}, 1 April 1989
333
+  Ty Coon, President of Vice
334
+
335
+This General Public License does not permit incorporating your program into
336
+proprietary programs.  If your program is a subroutine library, you may
337
+consider it more useful to permit linking proprietary applications with the
338
+library.  If this is what you want to do, use the GNU Lesser General
339
+Public License instead of this License.

+ 66
- 0
plugins/toc/README.md View File

@@ -0,0 +1,66 @@
1
+pelican-toc [![Build Status](https://travis-ci.org/ingwinlu/pelican-toc.svg?branch=master)](https://travis-ci.org/ingwinlu/pelican-toc)
2
+===================================
3
+
4
+This plugin generates a table of contents for pelican articles and pages, available for themes via `article.toc`.
5
+
6
+#Usage
7
+##requirements
8
+Beautifulsoup4 - install via `pip install beautifulsoup4`
9
+##theme
10
+```
11
+{% if article.toc %}
12
+<div class="col-lg-3 hidden-xs hidden-sm">
13
+    {{article.toc}}
14
+</div>
15
+{% endif %}
16
+```
17
+##article
18
+```
19
+Title: Peeking at erlang/chicagoboss
20
+###Intro
21
+###Chicagoboss Magic
22
+###Result
23
+```
24
+##output
25
+```
26
+<div class="col-lg-3 hidden-xs hidden-sm">
27
+    <div id="toc">
28
+      <ul>
29
+        <li>
30
+          <a href="#" title="Peeking at&nbsp;erlang/chicagoboss">Peeking at&nbsp;erlang/chicagoboss</a>
31
+          <ul>
32
+            <li>
33
+              <a href="#intro" title="Intro">Intro</a>
34
+            </li>
35
+            <li>
36
+              <a href="#chicagoboss-magic" title="Chicagoboss&nbsp;Magic">Chicagoboss&nbsp;Magic</a>
37
+            </li>
38
+            <li>
39
+              <a href="#result" title="Result">Result</a>
40
+            </li>
41
+          </ul>
42
+        </li>
43
+      </ul>
44
+    </div>
45
+</div>
46
+```
47
+
48
+##settings
49
+```
50
+TOC = {
51
+    'TOC_HEADERS' : '^h[1-6]',  # What headers should be included in the generated toc
52
+                                # Expected format is a regular expression
53
+
54
+    'TOC_RUN'     : 'true'      # Default value for toc generation, if it does not evaluate
55
+                                # to 'true' no toc will be generated
56
+}
57
+```
58
+All those settings can be overwritten on a per page/article basis via metadata.
59
+Just use the respective keyword as metadata (example: `toc_headers: ^h[1-4]`)
60
+
61
+#Differences between pelican-toc and pelican-extract-toc
62
+`extract-toc` uses a markdown extension to generate a toc and then extract it via beautifulsoup.
63
+This extension generates the toc itself, removing the need to write `[ToC]` in your articles.
64
+There also is a 'health' check on id's which should be generated via markdown.extensions.headerid per default, but somehow don't always end up in the output. 
65
+
66
+

+ 1
- 0
plugins/toc/__init__.py View File

@@ -0,0 +1 @@
1
+from .toc import *

+ 1
- 0
plugins/toc/dev_requirements.txt View File

@@ -0,0 +1 @@
1
+Markdown

+ 11
- 0
plugins/toc/test_data/article_with_headers.md View File

@@ -0,0 +1,11 @@
1
+Title: Peeking at erlang/chicagoboss
2
+Date: 2014-03-07 00:19:24
3
+
4
+###Intro
5
+
6
+an article with headers
7
+
8
+###Magic
9
+
10
+###Result
11
+

+ 15
- 0
plugins/toc/test_data/article_with_headers_exclude_small_headers.md View File

@@ -0,0 +1,15 @@
1
+Title: headers of all sizes
2
+Date: 2015-06-02 00:56:24
3
+
4
+# 1
5
+
6
+## 2
7
+
8
+### 3
9
+
10
+#### 4
11
+
12
+##### 5
13
+
14
+###### 6
15
+

+ 16
- 0
plugins/toc/test_data/article_with_headers_exclude_small_headers_metadata.md View File

@@ -0,0 +1,16 @@
1
+Title: headers of all sizes
2
+Date: 2015-06-02 00:56:24
3
+toc_headers: ^h[1-3]
4
+
5
+# 1
6
+
7
+## 2
8
+
9
+### 3
10
+
11
+#### 4
12
+
13
+##### 5
14
+
15
+###### 6
16
+

+ 12
- 0
plugins/toc/test_data/article_with_headers_metadata.md View File

@@ -0,0 +1,12 @@
1
+Title: Peeking at erlang/chicagoboss
2
+Date: 2014-03-07 00:19:24
3
+toc_run: false
4
+
5
+###Intro
6
+
7
+an article with headers
8
+
9
+###Magic
10
+
11
+###Result
12
+

+ 15
- 0
plugins/toc/test_data/article_with_headers_nonascii.md View File

@@ -0,0 +1,15 @@
1
+Title: headers in non ascii
2
+Date: 2015-06-01 00:19:24
3
+
4
+###введение
5
+
6
+russian intro
7
+
8
+###魔術
9
+
10
+traditional chinese magic
11
+
12
+###αποτέλεσμα
13
+
14
+greek result
15
+

+ 1
- 0
plugins/toc/test_data/article_with_headers_toc.html View File

@@ -0,0 +1 @@
1
+<div id="toc"><ul><li><a class="toc-href" href="#" title="Peeking at erlang/chicagoboss">Peeking at erlang/chicagoboss</a><ul><li><a class="toc-href" href="#intro" title="Intro">Intro</a></li><li><a class="toc-href" href="#magic" title="Magic">Magic</a></li><li><a class="toc-href" href="#result" title="Result">Result</a></li></ul></li></ul></div>

+ 1
- 0
plugins/toc/test_data/article_with_headers_toc_exclude_small_headers.html View File

@@ -0,0 +1 @@
1
+<div id="toc"><ul><li><a class="toc-href" href="#" title="headers of all sizes">headers of all sizes</a><ul><li><a class="toc-href" href="#1" title="1">1</a><ul><li><a class="toc-href" href="#2" title="2">2</a><ul><li><a class="toc-href" href="#3" title="3">3</a></li></ul></li></ul></li></ul></li></ul></div>

+ 1
- 0
plugins/toc/test_data/article_with_headers_toc_nonascii.html View File

@@ -0,0 +1 @@
1
+<div id="toc"><ul><li><a class="toc-href" href="#" title="headers in non ascii">headers in non ascii</a><ul><li><a class="toc-href" href="#vvedenie" title="введение">введение</a></li><li><a class="toc-href" href="#mo-shu" title="魔術">魔術</a></li><li><a class="toc-href" href="#apotelesma" title="&alpha;&pi;&omicron;&tau;έ&lambda;&epsilon;&sigma;&mu;&alpha;">&alpha;&pi;&omicron;&tau;έ&lambda;&epsilon;&sigma;&mu;&alpha;</a></li></ul></li></ul></div>

+ 5
- 0
plugins/toc/test_data/article_without_headers.md View File

@@ -0,0 +1,5 @@
1
+Title: Peeking at erlang/chicagoboss
2
+Date: 2014-03-07 00:19:24
3
+
4
+an article without headers
5
+

+ 94
- 0
plugins/toc/test_toc.py View File

@@ -0,0 +1,94 @@
1
+from io import open
2
+
3
+import unittest
4
+import re
5
+import toc
6
+
7
+from pelican.readers import MarkdownReader
8
+from pelican.contents import Article
9
+from pelican.tests.support import get_settings
10
+
11
+
12
+class TestToCGeneration(unittest.TestCase):
13
+
14
+    @classmethod
15
+    def setUpClass(cls):
16
+        toc.init_default_config(None)
17
+        cls.settings = get_settings()
18
+        cls.md_reader = MarkdownReader(cls.settings)
19
+
20
+    def setUp(self):
21
+        # have to reset the default, because shallow copies
22
+        self.settings['TOC']['TOC_HEADERS'] = '^h[1-6]'
23
+        self.settings['TOC']['TOC_RUN'] = 'true'
24
+
25
+    def _handle_article_generation(self, path):
26
+        content, metadata = self.md_reader.read(path)
27
+        return Article(content=content, metadata=metadata)
28
+
29
+    def _generate_toc(self, article_path, expected_path):
30
+        result = self._handle_article_generation(article_path)
31
+        toc.generate_toc(result)
32
+        expected = ""
33
+        with open(expected_path, 'r') as f:
34
+            expected = f.read()
35
+        return result, expected
36
+
37
+
38
+    def test_toc_generation(self):
39
+        result, expected = self._generate_toc(
40
+                "test_data/article_with_headers.md",
41
+                "test_data/article_with_headers_toc.html"
42
+            )
43
+        self.assertEqual(result.toc, expected)
44
+
45
+    def test_toc_generation_nonascii(self):
46
+        result, expected = self._generate_toc(
47
+                "test_data/article_with_headers_nonascii.md",
48
+                "test_data/article_with_headers_toc_nonascii.html"
49
+            )
50
+        self.assertEqual(result.toc, expected)
51
+
52
+    def test_toc_generation_exclude_small_headers(self):
53
+        self.settings['TOC']['TOC_HEADERS'] = '^h[1-3]'
54
+        result, expected = self._generate_toc(
55
+                "test_data/article_with_headers_exclude_small_headers.md",
56
+                "test_data/article_with_headers_toc_exclude_small_headers.html"
57
+            )
58
+        self.assertEqual(result.toc, expected)
59
+
60
+    def test_toc_generation_exclude_small_headers_metadata(self):
61
+        result, expected = self._generate_toc(
62
+                "test_data/article_with_headers_exclude_small_headers_metadata.md",
63
+                "test_data/article_with_headers_toc_exclude_small_headers.html"
64
+            )
65
+        self.assertEqual(result.toc, expected)
66
+
67
+
68
+    def test_bad_TOC_HEADERS(self):
69
+        self.settings['TOC']['TOC_HEADERS'] = '^[1-'
70
+        with self.assertRaises(re.error):
71
+            self._generate_toc(
72
+                "test_data/article_with_headers_exclude_small_headers.md",
73
+                "test_data/article_with_headers_toc_exclude_small_headers.html"
74
+            )
75
+
76
+    def test_no_toc_generation(self):
77
+        article_without_headers_path = "test_data/article_without_headers.md"
78
+        article_without_headers = self._handle_article_generation(
79
+            article_without_headers_path)
80
+        toc.generate_toc(article_without_headers)
81
+        with self.assertRaises(AttributeError):
82
+            self.assertIsNone(article_without_headers.toc)
83
+
84
+    def test_no_toc_generation_metadata(self):
85
+        article_without_headers_path = "test_data/article_with_headers_metadata.md"
86
+        article_without_headers = self._handle_article_generation(
87
+            article_without_headers_path)
88
+        toc.generate_toc(article_without_headers)
89
+        with self.assertRaises(AttributeError):
90
+            self.assertIsNone(article_without_headers.toc)
91
+ 
92
+
93
+if __name__ == "__main__":
94
+    unittest.main()

+ 151
- 0
plugins/toc/toc.py View File

@@ -0,0 +1,151 @@
1
+'''
2
+toc
3
+===================================
4
+
5
+This plugin generates tocs for pages and articles.
6
+
7
+Based on https://github.com/ingwinlu/pelican-toc
8
+'''
9
+
10
+from __future__ import unicode_literals
11
+
12
+import logging
13
+import re
14
+
15
+from bs4 import BeautifulSoup, Comment
16
+
17
+from pelican import contents, signals
18
+from pelican.utils import python_2_unicode_compatible, slugify
19
+
20
+
21
+logger = logging.getLogger(__name__)
22
+
23
+'''
24
+https://github.com/waylan/Python-Markdown/blob/master/markdown/extensions/headerid.py
25
+'''
26
+IDCOUNT_RE = re.compile(r'^(.*)_([0-9]+)$')
27
+
28
+
29
+def unique(id, ids):
30
+    """ Ensure id is unique in set of ids. Append '_1', '_2'... if not """
31
+    while id in ids or not id:
32
+        m = IDCOUNT_RE.match(id)
33
+        if m:
34
+            id = '%s_%d' % (m.group(1), int(m.group(2)) + 1)
35
+        else:
36
+            id = '%s_%d' % (id, 1)
37
+    ids.add(id)
38
+    return id
39
+'''
40
+end
41
+'''
42
+
43
+
44
+@python_2_unicode_compatible
45
+class HtmlTreeNode(object):
46
+    def __init__(self, parent, header, level, id):
47
+        self.children = []
48
+        self.parent = parent
49
+        self.header = header
50
+        self.level = level
51
+        self.id = id
52
+
53
+    def add(self, new_header, ids):
54
+        new_level = new_header.name
55
+        new_string = new_header.string
56
+        new_id = new_header.attrs.get('id')
57
+
58
+        if not new_string:
59
+            new_string = new_header.find_all(
60
+                    text=lambda t: not isinstance(t, Comment),
61
+                    recursive=True)
62
+            new_string = "".join(new_string)
63
+
64
+        if not new_id:
65
+            new_id = slugify(new_string, ())
66
+
67
+        new_id = unique(new_id, ids)  # make sure id is unique
68
+        new_header.attrs['id'] = new_id
69
+        if(self.level < new_level):
70
+            new_node = HtmlTreeNode(self, new_string, new_level, new_id)
71
+            self.children += [new_node]
72
+            return new_node, new_header
73
+        elif(self.level == new_level):
74
+            new_node = HtmlTreeNode(self.parent, new_string, new_level, new_id)
75
+            self.parent.children += [new_node]
76
+            return new_node, new_header
77
+        elif(self.level > new_level):
78
+            return self.parent.add(new_header, ids)
79
+
80
+    def __str__(self):
81
+        ret = "<a class='toc-href' href='#{0}' title='{1}'>{1}</a>".format(
82
+                self.id, self.header)
83
+
84
+        if self.children:
85
+            ret += "<ul>{}</ul>".format('{}'*len(self.children)).format(
86
+                    *self.children)
87
+
88
+        ret = "<li>{}</li>".format(ret)
89
+
90
+        if not self.parent:
91
+            ret = "<div id='toc'><ul>{}</ul></div>".format(ret)
92
+
93
+        return ret
94
+
95
+
96
+def init_default_config(pelican):
97
+    from pelican.settings import DEFAULT_CONFIG
98
+
99
+    TOC_DEFAULT = {
100
+        'TOC_HEADERS': '^h[1-6]',
101
+        'TOC_RUN': 'true'
102
+    }
103
+
104
+    DEFAULT_CONFIG.setdefault('TOC', TOC_DEFAULT)
105
+    if(pelican):
106
+        pelican.settings.setdefault('TOC', TOC_DEFAULT)
107
+
108
+
109
+def generate_toc(content):
110
+    if isinstance(content, contents.Static):
111
+        return
112
+
113
+    _toc_run = content.metadata.get(
114
+            'toc_run',
115
+            content.settings['TOC']['TOC_RUN'])
116
+    if not _toc_run == 'true':
117
+        return
118
+
119
+    all_ids = set()
120
+    title = content.metadata.get('title', 'Title')
121
+    tree = node = HtmlTreeNode(None, title, 'h0', '')
122
+    soup = BeautifulSoup(content._content, 'html.parser')
123
+    settoc = False
124
+
125
+    try:
126
+        header_re = re.compile(content.metadata.get(
127
+            'toc_headers', content.settings['TOC']['TOC_HEADERS']))
128
+    except re.error as e:
129
+        logger.error("TOC_HEADERS '%s' is not a valid re\n%s",
130
+                     content.settings['TOC']['TOC_HEADERS'])
131
+        raise e
132
+
133
+    for header in soup.findAll(header_re):
134
+        settoc = True
135
+        node, new_header = node.add(header, all_ids)
136
+        header.replaceWith(new_header)  # to get our ids back into soup
137
+
138
+    if (settoc):
139
+        tree_string = '<ul>{}</ul>'.format(
140
+            ''.join(['{}'.format(tree_node) for tree_node in tree.children])
141
+        )
142
+        tree_soup = BeautifulSoup(tree_string, 'html.parser')
143
+        content.toc = tree_soup.decode(formatter='html')
144
+        if not content.toc_title:
145
+            content.toc_title = title
146
+    content._content = soup.decode(formatter='html')
147
+
148
+
149
+def register():
150
+    signals.initialized.connect(init_default_config)
151
+    signals.content_object_init.connect(generate_toc)

+ 36
- 0
plugins/toc/tox.ini View File

@@ -0,0 +1,36 @@
1
+[tox]
2
+skipsdist = true
3
+envlist = py{27,33,34}-pelican{34,35,36,dev}
4
+
5
+[testenv]
6
+basepython =
7
+    py27: python2.7
8
+    py33: python3.3
9
+    py34: python3.4
10
+deps =
11
+    pelican34: git+https://github.com/getpelican/pelican.git@3.4.0#egg=pelican
12
+    pelican35: git+https://github.com/getpelican/pelican.git@3.5.0#egg=pelican
13
+    pelican36: git+https://github.com/getpelican/pelican.git@3.6.3#egg=pelican
14
+    pelicandev: git+https://github.com/getpelican/pelican.git#egg=pelican
15
+    beautifulsoup4
16
+    -rdev_requirements.txt
17
+passenv = *
18
+install_command= pip install {opts} -e {packages}
19
+commands =
20
+    {envpython} --version
21
+    pelican --version
22
+    {envpython} test_toc.py
23
+
24
+[flake8]
25
+application-import-names=toc
26
+import-order-style=cryptography
27
+
28
+[testenv:flake8]
29
+basepython = python2.7
30
+deps =
31
+    flake8 <= 2.4.1
32
+    git+https://github.com/public/flake8-import-order@2ac7052a4e02b4a8a0125a106d87465a3b9fd688
33
+install_command= pip install {opts} {packages}
34
+commands =
35
+    flake8 --version
36
+    flake8 toc.py

+ 21
- 0
publishconf.py View File

@@ -0,0 +1,21 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*- #
3
+from __future__ import unicode_literals
4
+
5
+# This file is only used if you use `make publish` or
6
+# explicitly specify it as your config file.
7
+
8
+import os
9
+import sys
10
+sys.path.append(os.curdir)
11
+from pelicanconf import *
12
+
13
+SITEURL = 'https://www.cygnal.eu'
14
+RELATIVE_URLS = False
15
+
16
+DELETE_OUTPUT_DIRECTORY = True
17
+
18
+# Following items are often useful when publishing
19
+
20
+#DISQUS_SITENAME = ""
21
+#GOOGLE_ANALYTICS = ""

+ 7
- 0
themes/custom/static/css/bootstrap.min.css
File diff suppressed because it is too large
View File


+ 4
- 0
themes/custom/static/css/font-awesome.min.css
File diff suppressed because it is too large
View File


+ 123
- 0
themes/custom/static/css/style.css View File

@@ -0,0 +1,123 @@
1
+/* roboto-regular - latin */
2
+@font-face {
3
+  font-family: 'Roboto';
4
+  font-style: normal;
5
+  font-weight: 400;
6
+  src: url('../fonts/roboto-v16-latin-regular.eot'); /* IE9 Compat Modes */
7
+  src: local('Roboto'), local('Roboto-Regular'),
8
+       url('../fonts/roboto-v16-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
9
+       url('../fonts/roboto-v16-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
10
+       url('../fonts/roboto-v16-latin-regular.woff') format('woff'), /* Modern Browsers */
11
+       url('../fonts/roboto-v16-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
12
+       url('../fonts/roboto-v16-latin-regular.svg#Roboto') format('svg'); /* Legacy iOS */
13
+}
14
+
15
+.center {
16
+    text-align: center;
17
+}
18
+
19
+html, body {
20
+    min-height: 100%;
21
+    font-family: "Roboto", "Helvetica", "Arial", serif;
22
+}
23
+
24
+html {
25
+    background-color: rgb(59,61,64);
26
+}
27
+
28
+body {
29
+    background: linear-gradient(rgba(59,61,64,0.8), rgba(59,61,64,0.8)),url(../images/cover.jpg) no-repeat center center
30
+}
31
+
32
+.main {
33
+    position: relative;
34
+    min-height: 100%;
35
+    padding-top: 70px;
36
+    padding-bottom: 20px;
37
+    color: white;
38
+    text-align: left;
39
+}
40
+
41
+.main h2 {
42
+    text-align: right;
43
+}
44
+