diff --git a/Makefile b/Makefile index ba5a765..ad8eaf7 100644 --- a/Makefile +++ b/Makefile @@ -2,13 +2,13 @@ PY?=python3 PELICAN?=pelican PELICANOPTS= -YEARSUBFOLDER=enseignements/2019-2020/ - +YEARSUBFOLDER=enseignements/DEV/ BASEDIR=$(CURDIR) INPUTDIR=$(BASEDIR)/content OUTPUTDIR=$(BASEDIR)/output CONFFILE=$(BASEDIR)/pelicanconf.py PUBLISHCONF=$(BASEDIR)/publishconf.py + FAKEDIR=../../output/ FTP_HOST=localhost @@ -20,34 +20,37 @@ SSH_CONF=Embrevade #SSH_TARGET_DIR=/var/docker/opytex.org/www/ SSH_TARGET_DIR=/home/sshcontent/opytex.org/www/$(YEARSUBFOLDER) + DEBUG ?= 0 ifeq ($(DEBUG), 1) PELICANOPTS += -D endif +RELATIVE ?= 0 +ifeq ($(RELATIVE), 1) + PELICANOPTS += --relative-urls +endif + help: - @echo 'Makefile for a pelican Web site ' - @echo ' ' - @echo 'Usage: ' - @echo ' make html (re)generate the web site ' - @echo ' make clean remove the generated files ' - @echo ' make regenerate regenerate files upon modification ' - @echo ' make publish generate using production settings ' - @echo ' make serve [PORT=8000] serve site at http://localhost:8000' - @echo ' make devserver [PORT=8000] start/restart develop_server.sh ' - @echo ' make stopserver stop local server ' - @echo ' make ssh_upload upload the web site via SSH ' - @echo ' make rsync_upload upload the web site via rsync+ssh ' - @echo ' make dropbox_upload upload the web site via Dropbox ' - @echo ' make ftp_upload upload the web site via FTP ' - @echo ' make s3_upload upload the web site via S3 ' - @echo ' make cf_upload upload the web site via Cloud Files' - @echo ' make github upload the web site via gh-pages ' - @echo ' ' - @echo 'Set the DEBUG variable to 1 to enable debugging, e.g. make DEBUG=1 html' - @echo ' ' + @echo 'Makefile for a pelican Web site ' + @echo ' ' + @echo 'Usage: ' + @echo ' make html (re)generate the web site ' + @echo ' make clean remove the generated files ' + @echo ' make regenerate regenerate files upon modification ' + @echo ' make publish generate using production settings ' + @echo ' make serve [PORT=8000] serve site at http://localhost:8000' + @echo ' make serve-global [SERVER=0.0.0.0] serve (as root) to $(SERVER):80 ' + @echo ' make devserver [PORT=8000] serve and regenerate together ' + @echo ' make ssh_upload upload the web site via SSH ' + @echo ' make rsync_upload upload the web site via rsync+ssh ' + @echo ' ' + @echo 'Set the DEBUG variable to 1 to enable debugging, e.g. make DEBUG=1 html ' + @echo 'Set the RELATIVE variable to 1 to enable relative urls ' + @echo ' ' html: + lessc $(BASEDIR)/theme/static/stylesheet/style.less $(BASEDIR)/theme/static/stylesheet/style.min.css -x $(PELICAN) $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS) clean: @@ -58,28 +61,29 @@ regenerate: serve: ifdef PORT - cd $(OUTPUTDIR) && $(PY) -m pelican.server $(PORT) + $(PELICAN) -l $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS) -p $(PORT) else - cd $(OUTPUTDIR) && $(PY) -m pelican.server + $(PELICAN) -l $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS) endif +serve-global: +ifdef SERVER + $(PELICAN) -l $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS) -p $(PORT) -b $(SERVER) +else + $(PELICAN) -l $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS) -p $(PORT) -b 0.0.0.0 +endif + + devserver: ifdef PORT - $(BASEDIR)/develop_server.sh restart $(PORT) + $(PELICAN) -lr $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS) -p $(PORT) else - $(BASEDIR)/develop_server.sh restart + $(PELICAN) -lr $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS) endif -stopserver: - kill -9 `cat pelican.pid` - kill -9 `cat srv.pid` - @echo 'Stopped Pelican and SimpleHTTPServer processes running in background.' - publish: $(PELICAN) $(INPUTDIR) -o $(OUTPUTDIR) -s $(PUBLISHCONF) $(PELICANOPTS) -ssh_upload: publish - scp -r $(OUTPUTDIR)/* $(SSH_CONF):$(SSH_TARGET_DIR) rsync_upload: publish #rsync -e "ssh -p $(SSH_PORT)" -P -rvzc --cvs-exclude --delete $(OUTPUTDIR)/ $(SSH_USER)@$(SSH_HOST):$(SSH_TARGET_DIR) @@ -89,4 +93,4 @@ fake_upload: html mkdir -p $(FAKEDIR)$(YEARSUBFOLDER) rsync -P -rvzc --delete $(OUTPUTDIR)/ $(FAKEDIR)$(YEARSUBFOLDER) --cvs-exclude -.PHONY: html help clean regenerate serve devserver publish ssh_upload rsync_upload dropbox_upload ftp_upload s3_upload cf_upload github import_ens +.PHONY: html help clean regenerate serve serve-global devserver publish rsync_upload fake_upload diff --git a/README.md b/README.md deleted file mode 100644 index 95905cf..0000000 --- a/README.md +++ /dev/null @@ -1,6 +0,0 @@ -Site Opytex -########### - -- Soucis avec ImageMagick et la conversion vers pdf (pdf-img plugin) - - https://stackoverflow.com/questions/52998331/imagemagick-security-policy-pdf-blocking-conversion diff --git a/develop_server.sh b/develop_server.sh deleted file mode 100755 index ae8f29e..0000000 --- a/develop_server.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env bash -## -# This section should match your Makefile -## -PY=${PY:-python3} -PELICAN=${PELICAN:-pelican} -PELICANOPTS= - -BASEDIR=$(pwd) -INPUTDIR=$BASEDIR/content -OUTPUTDIR=$BASEDIR/output -CONFFILE=$BASEDIR/pelicanconf.py - -### -# Don't change stuff below here unless you are sure -### - -SRV_PID=$BASEDIR/srv.pid -PELICAN_PID=$BASEDIR/pelican.pid - -function usage(){ - echo "usage: $0 (stop) (start) (restart) [port]" - echo "This starts Pelican in debug and reload mode and then launches" - echo "an HTTP server to help site development. It doesn't read" - echo "your Pelican settings, so if you edit any paths in your Makefile" - echo "you will need to edit your settings as well." - exit 3 -} - -function alive() { - kill -0 $1 >/dev/null 2>&1 -} - -function shut_down(){ - PID=$(cat $SRV_PID) - if [[ $? -eq 0 ]]; then - if alive $PID; then - echo "Stopping HTTP server" - kill $PID - else - echo "Stale PID, deleting" - fi - rm $SRV_PID - else - echo "HTTP server PIDFile not found" - fi - - PID=$(cat $PELICAN_PID) - if [[ $? -eq 0 ]]; then - if alive $PID; then - echo "Killing Pelican" - kill $PID - else - echo "Stale PID, deleting" - fi - rm $PELICAN_PID - else - echo "Pelican PIDFile not found" - fi -} - -function start_up(){ - local port=$1 - echo "Starting up Pelican and HTTP server" - shift - $PELICAN --debug --autoreload -r $INPUTDIR -o $OUTPUTDIR -s $CONFFILE $PELICANOPTS & - pelican_pid=$! - echo $pelican_pid > $PELICAN_PID - cd $OUTPUTDIR - $PY -m pelican.server $port & - srv_pid=$! - echo $srv_pid > $SRV_PID - cd $BASEDIR - sleep 1 - if ! alive $pelican_pid ; then - echo "Pelican didn't start. Is the Pelican package installed?" - return 1 - elif ! alive $srv_pid ; then - echo "The HTTP server didn't start. Is there another service using port" $port "?" - return 1 - fi - echo 'Pelican and HTTP server processes now running in background.' -} - -### -# MAIN -### -[[ ($# -eq 0) || ($# -gt 2) ]] && usage -port='' -[[ $# -eq 2 ]] && port=$2 - -if [[ $1 == "stop" ]]; then - shut_down -elif [[ $1 == "restart" ]]; then - shut_down - start_up $port -elif [[ $1 == "start" ]]; then - if ! start_up $port; then - shut_down - fi -else - usage -fi diff --git a/fabfile.py b/fabfile.py deleted file mode 100644 index 39dd7aa..0000000 --- a/fabfile.py +++ /dev/null @@ -1,73 +0,0 @@ -from fabric.api import * -import fabric.contrib.project as project -import os -import sys -import SimpleHTTPServer -import SocketServer - -# Local path configuration (can be absolute or relative to fabfile) -env.deploy_path = 'output' -DEPLOY_PATH = env.deploy_path - -# Remote server configuration -production = 'root@localhost:22' -dest_path = '/var/www' - -# Rackspace Cloud Files configuration settings -env.cloudfiles_username = 'my_rackspace_username' -env.cloudfiles_api_key = 'my_rackspace_api_key' -env.cloudfiles_container = 'my_cloudfiles_container' - - -def clean(): - if os.path.isdir(DEPLOY_PATH): - local('rm -rf {deploy_path}'.format(**env)) - local('mkdir {deploy_path}'.format(**env)) - -def build(): - local('pelican -s pelicanconf.py') - -def rebuild(): - clean() - build() - -def regenerate(): - local('pelican -r -s pelicanconf.py') - -def serve(): - os.chdir(env.deploy_path) - - PORT = 8000 - class AddressReuseTCPServer(SocketServer.TCPServer): - allow_reuse_address = True - - server = AddressReuseTCPServer(('', PORT), SimpleHTTPServer.SimpleHTTPRequestHandler) - - sys.stderr.write('Serving on port {0} ...\n'.format(PORT)) - server.serve_forever() - -def reserve(): - build() - serve() - -def preview(): - local('pelican -s publishconf.py') - -def cf_upload(): - rebuild() - local('cd {deploy_path} && ' - 'swift -v -A https://auth.api.rackspacecloud.com/v1.0 ' - '-U {cloudfiles_username} ' - '-K {cloudfiles_api_key} ' - 'upload -c {cloudfiles_container} .'.format(**env)) - -@hosts(production) -def publish(): - local('pelican -s publishconf.py') - project.rsync_project( - remote_dir=dest_path, - exclude=".DS_Store", - local_dir=DEPLOY_PATH.rstrip('/') + '/', - delete=True, - extra_opts='-c', - ) diff --git a/notes b/notes deleted file mode 100644 index 49cbf1b..0000000 --- a/notes +++ /dev/null @@ -1,5 +0,0 @@ -# Importer tous les fichier rst -rsync -rv --del --exclude 'Archive' --exclude 'tools/skeleton' --exclude 'tools/Other' --include '*/' --include '*.rst' --exclude '*' --prune-empty-dirs /media/documents/Cours/Prof/Enseignements content/Cours/ - -# Ajouter une nouvelle année -Éditer pelicanconf.py pour ajouter une entrée dans links diff --git a/pelican-plugins b/pelican-plugins deleted file mode 160000 index 5c5f965..0000000 --- a/pelican-plugins +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5c5f965c984d9cb9324c1c7b251d448992c4bda5 diff --git a/pelicanconf.py b/pelicanconf.py index 03de42d..b48db2f 100644 --- a/pelicanconf.py +++ b/pelicanconf.py @@ -9,8 +9,8 @@ from globalconf import * AUTHOR = 'Benjamin Bertrand' SITENAME = 'OpyTex' SITETITLE = 'OpyTex' -SITESUBTITLE = "2019-2020" -SITEURL = '' +SITESUBTITLE = "DEV" +SITEURL = 'localhost:8000/enseignements/DEV/' CC_LICENSE_COMMERCIAL = True CC_LICENSE = True @@ -21,73 +21,59 @@ TIMEZONE = 'Europe/Paris' DEFAULT_LANG = 'fr' -# theme -THEME = "./theme/" -USE_GOOGLE_FONTS = False +# Uncomment following line if you want document-relative URLs when developing +#RELATIVE_URLS = True -IGNORE_FILES = ['venv', '.git', 'tools'] -# Pages, articles and static -PAGE_PATHS = ['pages'] -#ARTICLE_PATHS = ['pages/Enseignement', 'Blog'] +# Files places +IGNORE_FILES = ['venv'] +#PAGE_PATHS = ['pages'] ARTICLE_PATHS = ['.'] STATIC_PATHS = ['.'] - INDEX_SAVE_AS = 'blog_index.html' +# +USE_FOLDER_AS_CATEGORY = False + +# Plugins +PLUGIN_PATHS = ["plugins"] +PLUGINS = [ + 'i18n_subsites', + "pelican-page-hierarchy", + # 'pdf-img', +] + # Mirror source structure PATH_METADATA = '(?P.*)\..*' ARTICLE_URL = ARTICLE_SAVE_AS = PAGE_URL = PAGE_SAVE_AS = '{path_no_ext}.html' +# Readers +READERS = {"html": None} + +# Themes +THEME = "theme" + +# Everythings in french +JINJA_ENVIRONMENT = {'extensions': ['jinja2.ext.i18n']} +# Default theme language. +I18N_TEMPLATES_LANG = "en" +# Your language. +DEFAULT_LANG = "fr" +OG_LOCALE = "fr" +LOCALE = ("fr", "fr_FR.utf8") + +DISPLAY_PAGES_ON_SIDE = False + +MAIN_MENU = True +DISPLAY_CATEGORIES_ON_MENU = False +TOCTREE = True + +#SITELOGO = "" +LINKS = () +DEFAULT_PAGINATION = 10 + # Feed generation is usually not desired when developing FEED_ALL_ATOM = None CATEGORY_FEED_ATOM = None TRANSLATION_FEED_ATOM = None AUTHOR_FEED_ATOM = None AUTHOR_FEED_RSS = None - -# Blogroll -LINKS = ( - #('2019/2020', "/Enseignements/2019-2020/"), - #('2018/2019', "/Enseignements/2018-2019/"), - #('2017/2018', "/Enseignements/2017-2018/"), - #('2016/2017', "/Enseignements/2016-2017/"), - #('2015/2016', "/Enseignements/2015-2016/"), -) - -# Social widget -#SOCIAL = (('You can add links in your config file', '#'), -# ) - -DEFAULT_PAGINATION = 20 - -# Date -SHOW_DATE_MODIFIED = True -ARTICLE_ORDER_BY = "modified" - -DISPLAY_ARTICLE_INFO_ON_INDEX = True - -# Uncomment following line if you want document-relative URLs when developing -RELATIVE_URLS = True - -BOOTSTRAP_THEME = "flatly" -PLUGIN_PATHS = ['./plugins', './pelican-plugins'] - -PLUGINS = [#'hierarchy', - 'tag_cloud', - "list_files", - "render_math", - "always_modified", - "pdf-img", - ] - -READERS = {"html": None} - -# hierarchy plugin config -# ARTICLE_URL = 'Enseignements/{slug}/' -# ARTICLE_SAVE_AS = 'Enseignements/{slug}/index.html' -# #SLUGIFY_SOURCE = 'basename' -# ARTICLE_NAVIGATION = True - -TAGS_URL = "tags.html" -DISPLAY_TAGS_INLINE = True -DISPLAY_CATEGORIES_ON_SIDEBAR = True diff --git a/plugins/hierarchy/README.md b/plugins/hierarchy/README.md deleted file mode 100644 index e87b71a..0000000 --- a/plugins/hierarchy/README.md +++ /dev/null @@ -1,75 +0,0 @@ -Page Hierarchy -============== -*Author: Ahmad Khayyat ()* - -A [Pelican][1] plugin that creates a URL hierarchy for pages that -matches the filesystem hierarchy of their sources. - -For example, to have the following filesystem structure of page -sources result in the URLs listed next to each file, - -```text -└── content/pages/ # PAGE_DIR - ├── about.md # URL: pages/about/ - ├── projects.md # URL: pages/projects/ - ├── projects/ # (directory) - │ ├── p1.md # URL: pages/projects/p1/ - │ ├── p2.md # URL: pages/projects/p2/ - │ └── p2/ # (directory) - │ └── features.md # URL: pages/projects/p2/features/ - └── contact.md # URL: pages/contact/ -``` - -you can use this plugin with the following Pelican settings: - -```python -# pelicanconf.py -PAGE_URL = '{slug}/' -PAGE_SAVE_AS = '{slug}/index.html' -SLUGIFY_SOURCE = 'basename' -``` - -When generating the `url` and `save_as` attributes, the plugin -prefixes the page's `slug` by its relative path. Although the initial -`slug` is generated from the page's `title` by default, it can be -generated from the source file basename by setting the -`SLUGIFY_SOURCE` setting to `'basename'`, as shown in the settings -snippet above. The `slug` can also be set using [`PATH_METADATA`][2]. - -This plugin is compatible with [Pelican translations][3]. - -Parent and Children Pages -------------------------- -This plugin also adds three attributes to each page object: - -- `parent`: the immediate parent page. `None` if the page is - top-level. If a translated page has no parent, the default-language - parent is used. - -- `parents`: a list of all ancestor pages, starting from the top-level - ancestor. - -- `children`: a list of all immediate child pages, in no specific - order. - -These attributes can be used to generate breadcrumbs or nested -navigation menus. For example, this is a template excerpt for -breadcrumbs: - -```html - - -``` - - -[1]: http://getpelican.com/ -[2]: http://docs.getpelican.com/en/latest/settings.html#path-metadata -[3]: http://docs.getpelican.com/en/latest/settings.html#translations diff --git a/plugins/hierarchy/__init__.py b/plugins/hierarchy/__init__.py deleted file mode 100644 index 0b7f56e..0000000 --- a/plugins/hierarchy/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -#from .page_hierarchy import * -from .article_hierarchy import * diff --git a/plugins/hierarchy/article_hierarchy.py b/plugins/hierarchy/article_hierarchy.py deleted file mode 100644 index eb99344..0000000 --- a/plugins/hierarchy/article_hierarchy.py +++ /dev/null @@ -1,88 +0,0 @@ -from pelican import signals, contents -import os.path -from copy import copy -from itertools import chain - -''' -This plugin creates a URL hierarchy for articles that matches the -directory hierarchy of their sources. -''' - -class UnexpectedException(Exception): pass - -def get_path(article, settings): - ''' Return the dirname relative to ARTICLE_PATHS prefix. ''' - path = os.path.split(article.get_relative_source_path())[0] + '/' - path = path.replace( os.path.sep, '/' ) - # Try to lstrip the longest prefix first - for prefix in sorted(settings['ARTICLE_PATHS'], key=len, reverse=True): - if not prefix.endswith('/'): prefix += '/' - if path.startswith(prefix): - return path[len(prefix):-1] - raise UnexpectedException('Article outside of ARTICLE_PATHS ?!?') - -def in_default_lang(article): - # article.in_default_lang property is undocumented (=unstable) interface - return article.lang == article.settings['DEFAULT_LANG'] - -def override_metadata(content_object): - if type(content_object) is not contents.Article: - return - article = content_object - path = get_path(article, article.settings) - - def _override_value(article, key): - metadata = copy(article.metadata) - # We override the slug to include the path up to the filename - #metadata['slug'] = os.path.join(path, article.slug) - metadata['slug'] = path - # We have to account for non-default language and format either, - # e.g., ARTICLE_SAVE_AS or ARTICLE_LANG_SAVE_AS - #infix = '' if in_default_lang(article) else 'LANG_' - infix = '' - return article.settings['ARTICLE_' + infix + key.upper()].format(**metadata) - - for key in ('save_as', 'url'): - if not hasattr(article, 'override_' + key): - setattr(article, 'override_' + key, _override_value(article, key)) - -def set_relationships(generator): - def _all_articles(): - return chain(generator.articles, generator.translations) - - # initialize parents and children lists - for article in _all_articles(): - article.parent = None - article.parents = [] - article.children = [] - - # set immediate parents and children - for article in _all_articles(): - # Parent of /a/b/ is /a/, parent of /a/b.html is /a/ - parent_url = os.path.dirname(article.url[:-1]) - if parent_url: parent_url += '/' - for article2 in _all_articles(): - if article2.url == parent_url and article2 != article: - article.parent = article2 - article2.children.append(article) - # If no parent found, try the parent of the default language article - #if not article.parent and not in_default_lang(article): - if not article.parent: - for article2 in generator.articles: - if (article.slug == article2.slug and - os.path.dirname(article.source_path) == - os.path.dirname(article2.source_path)): - # Only set the parent but not the children, obviously - article.parent = article2.parent - - # set all parents (ancestors) - for article in _all_articles(): - p = article - while p.parent: - article.parents.insert(0, p.parent) - p = p.parent - - -def register(): - signals.content_object_init.connect(override_metadata) - signals.article_generator_finalized.connect(set_relationships) diff --git a/plugins/hierarchy/page_hierarchy.py b/plugins/hierarchy/page_hierarchy.py deleted file mode 100644 index 2192f28..0000000 --- a/plugins/hierarchy/page_hierarchy.py +++ /dev/null @@ -1,86 +0,0 @@ -from pelican import signals, contents -import os.path -from copy import copy -from itertools import chain - -''' -This plugin creates a URL hierarchy for pages that matches the -directory hierarchy of their sources. -''' - -class UnexpectedException(Exception): pass - -def get_path(page, settings): - ''' Return the dirname relative to PAGE_PATHS prefix. ''' - path = os.path.split(page.get_relative_source_path())[0] + '/' - path = path.replace( os.path.sep, '/' ) - # Try to lstrip the longest prefix first - for prefix in sorted(settings['PAGE_PATHS'], key=len, reverse=True): - if not prefix.endswith('/'): prefix += '/' - if path.startswith(prefix): - return path[len(prefix):-1] - raise UnexpectedException('Page outside of PAGE_PATHS ?!?') - -def in_default_lang(page): - # page.in_default_lang property is undocumented (=unstable) interface - return page.lang == page.settings['DEFAULT_LANG'] - -def override_metadata(content_object): - if type(content_object) is not contents.Page: - return - page = content_object - path = get_path(page, page.settings) - - def _override_value(page, key): - metadata = copy(page.metadata) - # We override the slug to include the path up to the filename - #metadata['slug'] = os.path.join(path, page.slug) - metadata['slug'] = path - # We have to account for non-default language and format either, - # e.g., PAGE_SAVE_AS or PAGE_LANG_SAVE_AS - infix = '' if in_default_lang(page) else 'LANG_' - return page.settings['PAGE_' + infix + key.upper()].format(**metadata) - - for key in ('save_as', 'url'): - if not hasattr(page, 'override_' + key): - setattr(page, 'override_' + key, _override_value(page, key)) - -def set_relationships(generator): - def _all_pages(): - return chain(generator.pages, generator.translations) - - # initialize parents and children lists - for page in _all_pages(): - page.parent = None - page.parents = [] - page.children = [] - - # set immediate parents and children - for page in _all_pages(): - # Parent of /a/b/ is /a/, parent of /a/b.html is /a/ - parent_url = os.path.dirname(page.url[:-1]) - if parent_url: parent_url += '/' - for page2 in _all_pages(): - if page2.url == parent_url and page2 != page: - page.parent = page2 - page2.children.append(page) - # If no parent found, try the parent of the default language page - if not page.parent and not in_default_lang(page): - for page2 in generator.pages: - if (page.slug == page2.slug and - os.path.dirname(page.source_path) == - os.path.dirname(page2.source_path)): - # Only set the parent but not the children, obviously - page.parent = page2.parent - - # set all parents (ancestors) - for page in _all_pages(): - p = page - while p.parent: - page.parents.insert(0, p.parent) - p = p.parent - - -def register(): - signals.content_object_init.connect(override_metadata) - signals.page_generator_finalized.connect(set_relationships) diff --git a/plugins/i18n_subsites/README.rst b/plugins/i18n_subsites/README.rst new file mode 100644 index 0000000..340109b --- /dev/null +++ b/plugins/i18n_subsites/README.rst @@ -0,0 +1,165 @@ +======================= + I18N Sub-sites Plugin +======================= + +This plugin extends the translations functionality by creating +internationalized sub-sites for the default site. + +This plugin is designed for Pelican 3.4 and later. + +What it does +============ + +1. When the content of the main site is being generated, the settings + are saved and the generation stops when content is ready to be + written. While reading source files and generating content objects, + the output queue is modified in certain ways: + + - translations that will appear as native in a different (sub-)site + will be removed + - untranslated articles will be transformed to drafts if + ``I18N_UNTRANSLATED_ARTICLES`` is ``'hide'`` (default), removed if + ``'remove'`` or kept as they are if ``'keep'``. + - untranslated pages will be transformed into hidden pages if + ``I18N_UNTRANSLATED_PAGES`` is ``'hide'`` (default), removed if + ``'remove'`` or kept as they are if ``'keep'``.'' + - additional content manipulation similar to articles and pages can + be specified for custom generators in the ``I18N_GENERATOR_INFO`` + setting. + +2. For each language specified in the ``I18N_SUBSITES`` dictionary the + settings overrides are applied to the settings from the main site + and a new sub-site is generated in the same way as with the main + site until content is ready to be written. +3. When all (sub-)sites are waiting for content writing, all removed + contents, translations and static files are interlinked across the + (sub-)sites. +4. Finally, all the output is written. + +Setting it up +============= + +For each extra used language code, a language-specific settings overrides +dictionary must be given (but can be empty) in the ``I18N_SUBSITES`` dictionary + +.. code-block:: python + + PLUGINS = ['i18n_subsites', ...] + + # mapping: language_code -> settings_overrides_dict + I18N_SUBSITES = { + 'cz': { + 'SITENAME': 'Hezkej blog', + } + } + +You must also have the following in your pelican configuration + +.. code-block:: python + JINJA_ENVIRONMENT = { + 'extensions': ['jinja2.ext.i18n'], + } + + + +Default and special overrides +----------------------------- +The settings overrides may contain arbitrary settings, however, there +are some that are handled in a special way: + +``SITEURL`` + Any overrides to this setting should ensure that there is some level + of hierarchy between all (sub-)sites, because Pelican makes all URLs + relative to ``SITEURL`` and the plugin can only cross-link between + the sites using this hierarchy. For instance, with the main site + ``http://example.com`` a sub-site ``http://example.com/de`` will + work, but ``http://de.example.com`` will not. If not overridden, the + language code (the language identifier used in the ``lang`` + metadata) is appended to the main ``SITEURL`` for each sub-site. +``OUTPUT_PATH``, ``CACHE_PATH`` + If not overridden, the language code is appended as with ``SITEURL``. + Separate cache paths are required as parser results depend on the locale. +``STATIC_PATHS``, ``THEME_STATIC_PATHS`` + If not overridden, they are set to ``[]`` and all links to static + files are cross-linked to the main site. +``THEME``, ``THEME_STATIC_DIR`` + If overridden, the logic with ``THEME_STATIC_PATHS`` does not apply. +``DEFAULT_LANG`` + This should not be overridden as the plugin changes it to the + language code of each sub-site to change what is perceived as translations. + +Localizing templates +-------------------- + +Most importantly, this plugin can use localized templates for each +sub-site. There are two approaches to having the templates localized: + +- You can set a different ``THEME`` override for each language in + ``I18N_SUBSITES``, e.g. by making a copy of a theme ``my_theme`` to + ``my_theme_lang`` and then editing the templates in the new + localized theme. This approach means you don't have to deal with + gettext ``*.po`` files, but it is harder to maintain over time. +- You use only one theme and localize the templates using the + `jinja2.ext.i18n Jinja2 extension + `_. For a kickstart + read this `guide <./localizing_using_jinja2.rst>`_. + +Additional context variables +............................ + +It may be convenient to add language buttons to your theme in addition +to the translation links of articles and pages. These buttons could, +for example, point to the ``SITEURL`` of each (sub-)site. For this +reason the plugin adds these variables to the template context: + +``main_lang`` + The language of the main site — the original ``DEFAULT_LANG`` +``main_siteurl`` + The ``SITEURL`` of the main site — the original ``SITEURL`` +``lang_siteurls`` + An ordered dictionary, mapping all used languages to their + ``SITEURL``. The ``main_lang`` is the first key with ``main_siteurl`` + as the value. This dictionary is useful for implementing global + language buttons that show the language of the currently viewed + (sub-)site too. +``extra_siteurls`` + An ordered dictionary, subset of ``lang_siteurls``, the current + ``DEFAULT_LANG`` of the rendered (sub-)site is not included, so for + each (sub-)site ``set(extra_siteurls) == set(lang_siteurls) - + set([DEFAULT_LANG])``. This dictionary is useful for implementing + global language buttons that do not show the current language. +``relpath_to_site`` + A function that returns a relative path from the first (sub-)site to + the second (sub-)site where the (sub-)sites are identified by the + language codes given as two arguments. + +If you don't like the default ordering of the ordered dictionaries, +use a Jinja2 filter to alter the ordering. + +All the siteurls above are always absolute even in the case of +``RELATIVE_URLS == True`` (it would be to complicated to replicate the +Pelican internals for local siteurls), so you may rather use something +like ``{{ SITEURL }}/{{ relpath_to_site(DEFAULT_LANG, main_lang }}`` +to link to the main site. + +This short `howto <./implementing_language_buttons.rst>`_ shows two +example implementations of language buttons. + +Usage notes +=========== +- It is **mandatory** to specify ``lang`` metadata for each article + and page as ``DEFAULT_LANG`` is later changed for each sub-site, so + content without ``lang`` metadata would be rendered in every + (sub-)site. +- As with the original translations functionality, ``slug`` metadata + is used to group translations. It is therefore often convenient to + compensate for this by overriding the content URL (which defaults to + slug) using the ``url`` and ``save_as`` metadata. You could also + give articles e.g. ``name`` metadata and use it in ``ARTICLE_URL = + '{name}.html'``. + +Development +=========== + +- A demo and a test site is in the ``gh-pages`` branch and can be seen + at http://smartass101.github.io/pelican-plugins/ diff --git a/plugins/i18n_subsites/__init__.py b/plugins/i18n_subsites/__init__.py new file mode 100644 index 0000000..7dfbde0 --- /dev/null +++ b/plugins/i18n_subsites/__init__.py @@ -0,0 +1 @@ +from .i18n_subsites import * diff --git a/plugins/i18n_subsites/i18n_subsites.py b/plugins/i18n_subsites/i18n_subsites.py new file mode 100644 index 0000000..dc27799 --- /dev/null +++ b/plugins/i18n_subsites/i18n_subsites.py @@ -0,0 +1,462 @@ +"""i18n_subsites plugin creates i18n-ized subsites of the default site + +This plugin is designed for Pelican 3.4 and later +""" + + +import os +import six +import logging +import posixpath + +from copy import copy +from itertools import chain +from operator import attrgetter +try: + from collections.abc import OrderedDict +except ImportError: + from collections import OrderedDict +from contextlib import contextmanager +from six.moves.urllib.parse import urlparse + +import gettext +import locale + +from pelican import signals +from pelican.generators import ArticlesGenerator, PagesGenerator +from pelican.settings import configure_settings +try: + from pelican.contents import Draft +except ImportError: + from pelican.contents import Article as Draft + + +# Global vars +_MAIN_SETTINGS = None # settings dict of the main Pelican instance +_MAIN_LANG = None # lang of the main Pelican instance +_MAIN_SITEURL = None # siteurl of the main Pelican instance +_MAIN_STATIC_FILES = None # list of Static instances the main Pelican instance +_SUBSITE_QUEUE = {} # map: lang -> settings overrides +_SITE_DB = OrderedDict() # OrderedDict: lang -> siteurl +_SITES_RELPATH_DB = {} # map: (lang, base_lang) -> relpath +# map: generator -> list of removed contents that need interlinking +_GENERATOR_DB = {} +_NATIVE_CONTENT_URL_DB = {} # map: source_path -> content in its native lang +_LOGGER = logging.getLogger(__name__) + + +@contextmanager +def temporary_locale(temp_locale=None): + '''Enable code to run in a context with a temporary locale + + Resets the locale back when exiting context. + Can set a temporary locale if provided + ''' + orig_locale = locale.setlocale(locale.LC_ALL) + if temp_locale is not None: + locale.setlocale(locale.LC_ALL, temp_locale) + yield + locale.setlocale(locale.LC_ALL, orig_locale) + + +def initialize_dbs(settings): + '''Initialize internal DBs using the Pelican settings dict + + This clears the DBs for e.g. autoreload mode to work + ''' + global _MAIN_SETTINGS, _MAIN_SITEURL, _MAIN_LANG, _SUBSITE_QUEUE + _MAIN_SETTINGS = settings + _MAIN_LANG = settings['DEFAULT_LANG'] + _MAIN_SITEURL = settings['SITEURL'] + _SUBSITE_QUEUE = settings.get('I18N_SUBSITES', {}).copy() + prepare_site_db_and_overrides() + # clear databases in case of autoreload mode + _SITES_RELPATH_DB.clear() + _NATIVE_CONTENT_URL_DB.clear() + _GENERATOR_DB.clear() + + +def prepare_site_db_and_overrides(): + '''Prepare overrides and create _SITE_DB + + _SITE_DB.keys() need to be ready for filter_translations + ''' + _SITE_DB.clear() + _SITE_DB[_MAIN_LANG] = _MAIN_SITEURL + # make sure it works for both root-relative and absolute + main_siteurl = '/' if _MAIN_SITEURL == '' else _MAIN_SITEURL + for lang, overrides in _SUBSITE_QUEUE.items(): + if 'SITEURL' not in overrides: + overrides['SITEURL'] = posixpath.join(main_siteurl, lang) + _SITE_DB[lang] = overrides['SITEURL'] + # default subsite hierarchy + if 'OUTPUT_PATH' not in overrides: + overrides['OUTPUT_PATH'] = os.path.join( + _MAIN_SETTINGS['OUTPUT_PATH'], lang) + if 'CACHE_PATH' not in overrides: + overrides['CACHE_PATH'] = os.path.join( + _MAIN_SETTINGS['CACHE_PATH'], lang) + if 'STATIC_PATHS' not in overrides: + overrides['STATIC_PATHS'] = [] + if ('THEME' not in overrides and 'THEME_STATIC_DIR' not in overrides and + 'THEME_STATIC_PATHS' not in overrides): + relpath = relpath_to_site(lang, _MAIN_LANG) + overrides['THEME_STATIC_DIR'] = posixpath.join( + relpath, _MAIN_SETTINGS['THEME_STATIC_DIR']) + overrides['THEME_STATIC_PATHS'] = [] + # to change what is perceived as translations + overrides['DEFAULT_LANG'] = lang + + +def subscribe_filter_to_signals(settings): + '''Subscribe content filter to requested signals''' + for sig in settings.get('I18N_FILTER_SIGNALS', []): + sig.connect(filter_contents_translations) + + +def initialize_plugin(pelican_obj): + '''Initialize plugin variables and Pelican settings''' + if _MAIN_SETTINGS is None: + initialize_dbs(pelican_obj.settings) + subscribe_filter_to_signals(pelican_obj.settings) + + +def get_site_path(url): + '''Get the path component of an url, excludes siteurl + + also normalizes '' to '/' for relpath to work, + otherwise it could be interpreted as a relative filesystem path + ''' + path = urlparse(url).path + if path == '': + path = '/' + return path + + +def relpath_to_site(lang, target_lang): + '''Get relative path from siteurl of lang to siteurl of base_lang + + the output is cached in _SITES_RELPATH_DB + ''' + path = _SITES_RELPATH_DB.get((lang, target_lang), None) + if path is None: + siteurl = _SITE_DB.get(lang, _MAIN_SITEURL) + target_siteurl = _SITE_DB.get(target_lang, _MAIN_SITEURL) + path = posixpath.relpath(get_site_path(target_siteurl), + get_site_path(siteurl)) + _SITES_RELPATH_DB[(lang, target_lang)] = path + return path + + +def save_generator(generator): + '''Save the generator for later use + + initialize the removed content list + ''' + _GENERATOR_DB[generator] = [] + + +def article2draft(article): + '''Transform an Article to Draft''' + draft = Draft(article._content, article.metadata, article.settings, + article.source_path, article._context) + draft.status = 'draft' + return draft + + +def page2hidden_page(page): + '''Transform a Page to a hidden Page''' + page.status = 'hidden' + return page + + +class GeneratorInspector(object): + '''Inspector of generator instances''' + + generators_info = { + ArticlesGenerator: { + 'translations_lists': ['translations', 'drafts_translations'], + 'contents_lists': [('articles', 'drafts')], + 'hiding_func': article2draft, + 'policy': 'I18N_UNTRANSLATED_ARTICLES', + }, + PagesGenerator: { + 'translations_lists': ['translations', 'hidden_translations'], + 'contents_lists': [('pages', 'hidden_pages')], + 'hiding_func': page2hidden_page, + 'policy': 'I18N_UNTRANSLATED_PAGES', + }, + } + + def __init__(self, generator): + '''Identify the best known class of the generator instance + + The class ''' + self.generator = generator + self.generators_info.update(generator.settings.get( + 'I18N_GENERATORS_INFO', {})) + for cls in generator.__class__.__mro__: + if cls in self.generators_info: + self.info = self.generators_info[cls] + break + else: + self.info = {} + + def translations_lists(self): + '''Iterator over lists of content translations''' + return (getattr(self.generator, name) for name in + self.info.get('translations_lists', [])) + + def contents_list_pairs(self): + '''Iterator over pairs of normal and hidden contents''' + return (tuple(getattr(self.generator, name) for name in names) + for names in self.info.get('contents_lists', [])) + + def hiding_function(self): + '''Function for transforming content to a hidden version''' + hiding_func = self.info.get('hiding_func', lambda x: x) + return hiding_func + + def untranslated_policy(self, default): + '''Get the policy for untranslated content''' + return self.generator.settings.get(self.info.get('policy', None), + default) + + def all_contents(self): + '''Iterator over all contents''' + translations_iterator = chain(*self.translations_lists()) + return chain(translations_iterator, + *(pair[i] for pair in self.contents_list_pairs() + for i in (0, 1))) + + +def filter_contents_translations(generator): + '''Filter the content and translations lists of a generator + + Filters out + 1) translations which will be generated in a different site + 2) content that is not in the language of the currently + generated site but in that of a different site, content in a + language which has no site is generated always. The filtering + method bay be modified by the respective untranslated policy + ''' + inspector = GeneratorInspector(generator) + current_lang = generator.settings['DEFAULT_LANG'] + langs_with_sites = _SITE_DB.keys() + removed_contents = _GENERATOR_DB[generator] + + for translations in inspector.translations_lists(): + for translation in translations[:]: # copy to be able to remove + if translation.lang in langs_with_sites: + translations.remove(translation) + removed_contents.append(translation) + + hiding_func = inspector.hiding_function() + untrans_policy = inspector.untranslated_policy(default='hide') + for (contents, other_contents) in inspector.contents_list_pairs(): + for content in other_contents: # save any hidden native content first + if content.lang == current_lang: # in native lang + # save the native URL attr formatted in the current locale + _NATIVE_CONTENT_URL_DB[content.source_path] = content.url + for content in contents[:]: # copy for removing in loop + if content.lang == current_lang: # in native lang + # save the native URL attr formatted in the current locale + _NATIVE_CONTENT_URL_DB[content.source_path] = content.url + elif content.lang in langs_with_sites and untrans_policy != 'keep': + contents.remove(content) + if untrans_policy == 'hide': + other_contents.append(hiding_func(content)) + elif untrans_policy == 'remove': + removed_contents.append(content) + + +def install_templates_translations(generator): + '''Install gettext translations in the jinja2.Environment + + Only if the 'jinja2.ext.i18n' jinja2 extension is enabled + the translations for the current DEFAULT_LANG are installed. + ''' + if 'JINJA_ENVIRONMENT' in generator.settings: # pelican 3.7+ + jinja_extensions = generator.settings['JINJA_ENVIRONMENT'].get( + 'extensions', []) + else: + jinja_extensions = generator.settings['JINJA_EXTENSIONS'] + + if 'jinja2.ext.i18n' in jinja_extensions: + domain = generator.settings.get('I18N_GETTEXT_DOMAIN', 'messages') + localedir = generator.settings.get('I18N_GETTEXT_LOCALEDIR') + if localedir is None: + localedir = os.path.join(generator.theme, 'translations') + current_lang = generator.settings['DEFAULT_LANG'] + if current_lang == generator.settings.get('I18N_TEMPLATES_LANG', + _MAIN_LANG): + translations = gettext.NullTranslations() + else: + langs = [current_lang] + try: + translations = gettext.translation(domain, localedir, langs) + except (IOError, OSError): + _LOGGER.error(( + "Cannot find translations for language '{}' in '{}' with " + "domain '{}'. Installing NullTranslations.").format( + langs[0], localedir, domain)) + translations = gettext.NullTranslations() + newstyle = generator.settings.get('I18N_GETTEXT_NEWSTYLE', True) + generator.env.install_gettext_translations(translations, newstyle) + + +def add_variables_to_context(generator): + '''Adds useful iterable variables to template context''' + context = generator.context # minimize attr lookup + context['relpath_to_site'] = relpath_to_site + context['main_siteurl'] = _MAIN_SITEURL + context['main_lang'] = _MAIN_LANG + context['lang_siteurls'] = _SITE_DB + current_lang = generator.settings['DEFAULT_LANG'] + extra_siteurls = _SITE_DB.copy() + extra_siteurls.pop(current_lang) + context['extra_siteurls'] = extra_siteurls + + +def interlink_translations(content): + '''Link content to translations in their main language + + so the URL (including localized month names) of the different subsites + will be honored + ''' + lang = content.lang + # sort translations by lang + content.translations.sort(key=attrgetter('lang')) + for translation in content.translations: + relpath = relpath_to_site(lang, translation.lang) + url = _NATIVE_CONTENT_URL_DB[translation.source_path] + translation.override_url = posixpath.join(relpath, url) + + +def interlink_translated_content(generator): + '''Make translations link to the native locations + + for generators that may contain translated content + ''' + inspector = GeneratorInspector(generator) + for content in inspector.all_contents(): + interlink_translations(content) + + +def interlink_removed_content(generator): + '''For all contents removed from generation queue update interlinks + + link to the native location + ''' + current_lang = generator.settings['DEFAULT_LANG'] + for content in _GENERATOR_DB[generator]: + url = _NATIVE_CONTENT_URL_DB[content.source_path] + relpath = relpath_to_site(current_lang, content.lang) + content.override_url = posixpath.join(relpath, url) + + +def interlink_static_files(generator): + '''Add links to static files in the main site if necessary''' + if generator.settings['STATIC_PATHS'] != []: + return # customized STATIC_PATHS + try: # minimize attr lookup + static_content = generator.context['static_content'] + except KeyError: + static_content = generator.context['filenames'] + relpath = relpath_to_site(generator.settings['DEFAULT_LANG'], _MAIN_LANG) + for staticfile in _MAIN_STATIC_FILES: + if staticfile.get_relative_source_path() not in static_content: + staticfile = copy(staticfile) # prevent override in main site + staticfile.override_url = posixpath.join(relpath, staticfile.url) + try: + generator.add_source_path(staticfile, static=True) + except TypeError: + generator.add_source_path(staticfile) + + +def save_main_static_files(static_generator): + '''Save the static files generated for the main site''' + global _MAIN_STATIC_FILES + # test just for current lang as settings change in autoreload mode + if static_generator.settings['DEFAULT_LANG'] == _MAIN_LANG: + _MAIN_STATIC_FILES = static_generator.staticfiles + + +def update_generators(): + '''Update the context of all generators + + Ads useful variables and translations into the template context + and interlink translations + ''' + for generator in _GENERATOR_DB.keys(): + install_templates_translations(generator) + add_variables_to_context(generator) + interlink_static_files(generator) + interlink_removed_content(generator) + interlink_translated_content(generator) + + +def get_pelican_cls(settings): + '''Get the Pelican class requested in settings''' + cls = settings['PELICAN_CLASS'] + if isinstance(cls, six.string_types): + module, cls_name = cls.rsplit('.', 1) + module = __import__(module) + cls = getattr(module, cls_name) + return cls + + +def create_next_subsite(pelican_obj): + '''Create the next subsite using the lang-specific config + + If there are no more subsites in the generation queue, update all + the generators (interlink translations and removed content, add + variables and translations to template context). Otherwise get the + language and overrides for next the subsite in the queue and apply + overrides. Then generate the subsite using a PELICAN_CLASS + instance and its run method. Finally, restore the previous locale. + ''' + global _MAIN_SETTINGS + if len(_SUBSITE_QUEUE) == 0: + _LOGGER.debug( + 'i18n: Updating cross-site links and context of all generators.') + update_generators() + _MAIN_SETTINGS = None # to initialize next time + else: + with temporary_locale(): + settings = _MAIN_SETTINGS.copy() + lang, overrides = _SUBSITE_QUEUE.popitem() + settings.update(overrides) + settings = configure_settings(settings) # to set LOCALE, etc. + cls = get_pelican_cls(settings) + + new_pelican_obj = cls(settings) + _LOGGER.debug(("Generating i18n subsite for language '{}' " + "using class {}").format(lang, cls)) + new_pelican_obj.run() + + +# map: signal name -> function name +_SIGNAL_HANDLERS_DB = { + 'get_generators': initialize_plugin, + 'article_generator_pretaxonomy': filter_contents_translations, + 'page_generator_finalized': filter_contents_translations, + 'get_writer': create_next_subsite, + 'static_generator_finalized': save_main_static_files, + 'generator_init': save_generator, +} + + +def register(): + '''Register the plugin only if required signals are available''' + for sig_name in _SIGNAL_HANDLERS_DB.keys(): + if not hasattr(signals, sig_name): + _LOGGER.error(( + 'The i18n_subsites plugin requires the {} ' + 'signal available for sure in Pelican 3.4.0 and later, ' + 'plugin will not be used.').format(sig_name)) + return + + for sig_name, handler in _SIGNAL_HANDLERS_DB.items(): + sig = getattr(signals, sig_name) + sig.connect(handler) diff --git a/plugins/i18n_subsites/implementing_language_buttons.rst b/plugins/i18n_subsites/implementing_language_buttons.rst new file mode 100644 index 0000000..55b7bf3 --- /dev/null +++ b/plugins/i18n_subsites/implementing_language_buttons.rst @@ -0,0 +1,128 @@ +----------------------------- +Implementing language buttons +----------------------------- + +Each article with translations has translations links, but that's the +only way to switch between language subsites. + +For this reason it is convenient to add language buttons to the top +menu bar to make it simple to switch between the language subsites on +all pages. + +Example designs +--------------- + +Language buttons showing other available languages +.................................................. + +The ``extra_siteurls`` dictionary is a mapping of all other (not the +``DEFAULT_LANG`` of the current (sub-)site) languages to the +``SITEURL`` of the respective (sub-)sites + +.. code-block:: jinja + + +