diff options
| author | 2014-03-09 03:14:40 +0100 | |
|---|---|---|
| committer | 2014-03-09 03:14:40 +0100 | |
| commit | fa50632a9d87c3989566fed3e49c160a132e0d14 (patch) | |
| tree | 81f58cc0dcfbb34710856b59c034bc47c53d91dc /nikola | |
| parent | 2828399ba5cbb14502b023d4de1ba02f13dd5055 (diff) | |
Imported Upstream version 6.4.0upstream/6.4.0
Diffstat (limited to 'nikola')
92 files changed, 1033 insertions, 429 deletions
diff --git a/nikola/__init__.py b/nikola/__init__.py index 0a82198..787ce8e 100644 --- a/nikola/__init__.py +++ b/nikola/__init__.py @@ -27,7 +27,7 @@ from __future__ import absolute_import import os -__version__ = "6.3.0" +__version__ = "6.4.0" DEBUG = bool(os.getenv('NIKOLA_DEBUG')) from .nikola import Nikola # NOQA diff --git a/nikola/__main__.py b/nikola/__main__.py index 6b549b4..715f5b3 100644 --- a/nikola/__main__.py +++ b/nikola/__main__.py @@ -60,6 +60,16 @@ def main(args): quiet = True global config + colorful = False + if sys.stderr.isatty(): + colorful = True + try: + import colorama + colorama.init() + except ImportError: + if os.name == 'nt': + colorful = False + root = get_root_dir() if root: os.chdir(root) @@ -76,6 +86,8 @@ def main(args): sys.exit(1) config = {} + config.update({'__colorful__': colorful}) + site = Nikola(**config) return DoitNikola(site, quiet).run(args) @@ -86,7 +98,7 @@ class Help(DoitHelp): @staticmethod def print_usage(cmds): """print nikola "usage" (basic help) instructions""" - print("Nikola is a tool to create static websites and blogs. For full documentation and more information, please visit http://getnikola.com\n\n") + print("Nikola is a tool to create static websites and blogs. For full documentation and more information, please visit http://getnikola.com/\n\n") print("Available commands:") for cmd in sorted(cmds.values(), key=attrgetter('name')): print(" nikola %-*s %s" % (20, cmd.name, cmd.doc_purpose)) diff --git a/nikola/conf.py.in b/nikola/conf.py.in index 6ae0e1d..b398ac3 100644 --- a/nikola/conf.py.in +++ b/nikola/conf.py.in @@ -4,21 +4,21 @@ from __future__ import unicode_literals import time -############################################## -# Configuration, please edit -############################################## +#!! This is the configuration of Nikola. !!# +#!! You should edit it to your liking. !!# + # Data about this site -BLOG_AUTHOR = "${BLOG_AUTHOR}" -BLOG_TITLE = "${BLOG_TITLE}" +BLOG_AUTHOR = ${BLOG_AUTHOR} +BLOG_TITLE = ${BLOG_TITLE} # This is the main URL for your site. It will be used # in a prominent link -SITE_URL = "${SITE_URL}" +SITE_URL = ${SITE_URL} # This is the URL where nikola's output will be deployed. # If not set, defaults to SITE_URL -# BASE_URL = "${SITE_URL}" -BLOG_EMAIL = "${BLOG_EMAIL}" -BLOG_DESCRIPTION = "${BLOG_DESCRIPTION}" +# BASE_URL = ${SITE_URL} +BLOG_EMAIL = ${BLOG_EMAIL} +BLOG_DESCRIPTION = ${BLOG_DESCRIPTION} # Nikola is multilingual! # @@ -36,6 +36,7 @@ BLOG_DESCRIPTION = "${BLOG_DESCRIPTION}" # fa Persian # fi Finnish # fr French +# hi Hindi # hr Croatian # it Italian # ja Japanese [NOT jp!] @@ -51,12 +52,12 @@ BLOG_DESCRIPTION = "${BLOG_DESCRIPTION}" # # If you want to use Nikola with a non-supported language you have to provide # a module containing the necessary translations -# (cf. the modules at nikola/data/themes/base/messages/fr.py). +# (cf. the modules at nikola/data/themes/base/messages/). # If a specific post is not translated to a language, then the version # in the default language will be shown instead. # What is the default language? -DEFAULT_LANG = "${DEFAULT_LANG}" +DEFAULT_LANG = ${DEFAULT_LANG} # What other languages do you have? # The format is {"translationcode" : "path/to/translation" } @@ -75,7 +76,7 @@ TRANSLATIONS = { # this pattern is also used for metadata: # something.meta -> something.meta.pl -TRANSLATIONS_PATTERN = "{path}.{ext}.{lang}" +TRANSLATIONS_PATTERN = ${TRANSLATIONS_PATTERN} # If you don't want your Polish files to be considered Perl code, use this: # TRANSLATIONS_PATTERN = "{path}.{lang}.{ext}" @@ -172,7 +173,8 @@ COMPILERS = ${COMPILERS} # the posts themselves. If set to False, it will be just a list of links. # TAG_PAGES_ARE_INDEXES = True -# Final location is output / TRANSLATION[lang] / INDEX_PATH / index-*.html +# Final location for the main blog page and sibling paginated pages is +# output / TRANSLATION[lang] / INDEX_PATH / index-*.html # INDEX_PATH = "" # Create per-month archives instead of per-year @@ -192,7 +194,7 @@ COMPILERS = ${COMPILERS} # absolute: a complete URL (that includes the SITE_URL) # URL_TYPE = 'rel_path' -# Final locations are: +# Final location for the blog main RSS feed is: # output / TRANSLATION[lang] / RSS_PATH / rss.xml # RSS_PATH = "" @@ -270,9 +272,17 @@ COMPILERS = ${COMPILERS} # Compiler to process LESS files. # LESS_COMPILER = 'lessc' +# A list of options to pass to the LESS compiler. +# Final command is: LESS_COMPILER LESS_OPTIONS file.less +# LESS_OPTIONS = [] + # Compiler to process Sass files. # SASS_COMPILER = 'sass' +# A list of options to pass to the Sass compiler. +# Final command is: SASS_COMPILER SASS_OPTIONS file.s(a|c)ss +# SASS_OPTIONS = [] + # ############################################################################# # Image Gallery Options # ############################################################################# @@ -301,7 +311,7 @@ COMPILERS = ${COMPILERS} # the main (the newest) index page (index.html) # Name of the theme to use. -THEME = "${THEME}" +THEME = ${THEME} # Color scheme to be used for code blocks. If your theme provides # "assets/css/code.css" this is ignored. @@ -354,7 +364,6 @@ LICENSE = "" # src="http://i.creativecommons.org/l/by-nc-sa/2.5/ar/88x31.png"></a>""" # A small copyright notice for the page footer (in HTML). -# Default is '' CONTENT_FOOTER = 'Contents © {date} \ <a href="mailto:{email}">{author}</a> - Powered by \ <a href="http://getnikola.com" rel="nofollow">Nikola</a> \ @@ -367,12 +376,12 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # To use comments, you can choose between different third party comment # systems, one of "disqus", "livefyre", "intensedebate", "moot", # "googleplus", "facebook" or "isso" -# COMMENT_SYSTEM = "disqus" +# COMMENT_SYSTEM = ${COMMENT_SYSTEM} # And you also need to add your COMMENT_SYSTEM_ID which # depends on what comment system you use. The default is # "nikolademo" which is a test account for Disqus. More information # is in the manual. -# COMMENT_SYSTEM_ID = "nikolademo" +# COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID} # Enable annotations using annotateit.org? # If set to False, you can still enable them for individual posts and pages @@ -572,9 +581,9 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # USE_CDN = False # Extra things you want in the pages HEAD tag. This will be added right -# before </HEAD> +# before </head> # EXTRA_HEAD_DATA = "" -# Google analytics or whatever else you use. Added to the bottom of <body> +# Google Analytics or whatever else you use. Added to the bottom of <body> # in the default template (base.tmpl). # BODY_END = "" @@ -680,7 +689,11 @@ LOGGING_HANDLERS = { #} } +# Templates will use those filters, along with the defaults. +# Consult your engine's documentation on filters if you need help defining +# those. +# TEMPLATE_FILTERS = {} + # Put in global_context things you want available on all your templates. # It can be anything, data, functions, modules, etc. - GLOBAL_CONTEXT = {} diff --git a/nikola/data/themes/base/messages/messages_bg.py b/nikola/data/themes/base/messages/messages_bg.py index d4881b5..6e85212 100644 --- a/nikola/data/themes/base/messages/messages_bg.py +++ b/nikola/data/themes/base/messages/messages_bg.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Още публикации относно", "Newer posts": "Нови публикации", "Next post": "Следваща публикация", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Стари публикации", "Original site": "Оригиналния сайт", "Posted": "Публиковано", diff --git a/nikola/data/themes/base/messages/messages_ca.py b/nikola/data/themes/base/messages/messages_ca.py index d3a97b5..220d571 100644 --- a/nikola/data/themes/base/messages/messages_ca.py +++ b/nikola/data/themes/base/messages/messages_ca.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Més entrades sobre", "Newer posts": "Entrades posteriors", "Next post": "Entrada següent", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Entrades anteriors", "Original site": "Lloc original", "Posted": "Publicat", diff --git a/nikola/data/themes/base/messages/messages_cs.py b/nikola/data/themes/base/messages/messages_cs.py index 33482b5..f66c2c4 100644 --- a/nikola/data/themes/base/messages/messages_cs.py +++ b/nikola/data/themes/base/messages/messages_cs.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Další příspěvky o", "Newer posts": "Novější příspěvky", "Next post": "Další příspěvek", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Starší příspěvky", "Original site": "Původní stránka", "Posted": "Zveřejněno", diff --git a/nikola/data/themes/base/messages/messages_de.py b/nikola/data/themes/base/messages/messages_de.py index 6795031..41fe015 100644 --- a/nikola/data/themes/base/messages/messages_de.py +++ b/nikola/data/themes/base/messages/messages_de.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Weitere Einträge über", "Newer posts": "Neuere Einträge", "Next post": "Nächster Eintrag", + "No posts found.": "Keine einträge gefunden.", + "Nothing found.": "Nichts gefunden.", "Older posts": "Ältere Einträge", "Original site": "Original-Seite", "Posted": "Veröffentlicht", diff --git a/nikola/data/themes/base/messages/messages_el.py b/nikola/data/themes/base/messages/messages_el.py index 710558b..f658fa0 100644 --- a/nikola/data/themes/base/messages/messages_el.py +++ b/nikola/data/themes/base/messages/messages_el.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Περισσότερες αναρτήσεις για", "Newer posts": "Νεότερες αναρτήσεις", "Next post": "Επόμενη ανάρτηση", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Παλαιότερες αναρτήσεις", "Original site": "Ιστοσελίδα αρχικής ανάρτησης", "Posted": "Αναρτήθηκε", diff --git a/nikola/data/themes/base/messages/messages_en.py b/nikola/data/themes/base/messages/messages_en.py index 021f5e7..e2bff53 100644 --- a/nikola/data/themes/base/messages/messages_en.py +++ b/nikola/data/themes/base/messages/messages_en.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "More posts about", "Newer posts": "Newer posts", "Next post": "Next post", + "No posts found.": "No posts found.", + "Nothing found.": "Nothing found.", "Older posts": "Older posts", "Original site": "Original site", "Posted": "Posted", diff --git a/nikola/data/themes/base/messages/messages_eo.py b/nikola/data/themes/base/messages/messages_eo.py index fdbea88..f59a441 100644 --- a/nikola/data/themes/base/messages/messages_eo.py +++ b/nikola/data/themes/base/messages/messages_eo.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Pli artikoloj pri", "Newer posts": "Pli novaj artikoloj", "Next post": "Venonta artikolo", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Pli malnovaj artikoloj", "Original site": "Originala interretejo", "Posted": "Skribita", diff --git a/nikola/data/themes/base/messages/messages_es.py b/nikola/data/themes/base/messages/messages_es.py index 6c48fb9..1923683 100644 --- a/nikola/data/themes/base/messages/messages_es.py +++ b/nikola/data/themes/base/messages/messages_es.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Más posts sobre", "Newer posts": "Posts posteriores", "Next post": "Siguiente post", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Posts anteriores", "Original site": "Sitio original", "Posted": "Publicado", diff --git a/nikola/data/themes/base/messages/messages_et.py b/nikola/data/themes/base/messages/messages_et.py index 314f3b8..058ab5f 100644 --- a/nikola/data/themes/base/messages/messages_et.py +++ b/nikola/data/themes/base/messages/messages_et.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Veel postitusi kohta", "Newer posts": "Uued postitused", "Next post": "Järgmine postitus", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Vanemad postitused", "Original site": "Algallikas", "Posted": "Postitatud", diff --git a/nikola/data/themes/base/messages/messages_eu.py b/nikola/data/themes/base/messages/messages_eu.py index 18d7575..a8eb743 100644 --- a/nikola/data/themes/base/messages/messages_eu.py +++ b/nikola/data/themes/base/messages/messages_eu.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "-ri buruzko post gehiago", "Newer posts": "Post berrienak", "Next post": "Hurrengo posta", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Post zaharrenak", "Original site": "Jatorrizko orria", "Posted": "Argitaratuta", diff --git a/nikola/data/themes/base/messages/messages_fa.py b/nikola/data/themes/base/messages/messages_fa.py index bd278ca..4475e1b 100644 --- a/nikola/data/themes/base/messages/messages_fa.py +++ b/nikola/data/themes/base/messages/messages_fa.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "ارسالهای بیشتر دربارهٔ", "Newer posts": "ارسالهای جدیدتر", "Next post": "ارسال بعدی", + "No posts found.": "", + "Nothing found.": "", "Older posts": "پستهای قدیمیتر", "Original site": "سایت اصلی", "Posted": "ارسال شده", diff --git a/nikola/data/themes/base/messages/messages_fi.py b/nikola/data/themes/base/messages/messages_fi.py index b24ee2c..42e6fa2 100644 --- a/nikola/data/themes/base/messages/messages_fi.py +++ b/nikola/data/themes/base/messages/messages_fi.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Lisää postauksia aiheesta", "Newer posts": "Uudempia postauksia", "Next post": "Seuraava postaus", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Vanhempia postauksia", "Original site": "Alkuperäinen sivusto", "Posted": "Postattu", diff --git a/nikola/data/themes/base/messages/messages_fr.py b/nikola/data/themes/base/messages/messages_fr.py index ad4aea0..484d695 100644 --- a/nikola/data/themes/base/messages/messages_fr.py +++ b/nikola/data/themes/base/messages/messages_fr.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Plus d'articles sur", "Newer posts": "Billets récents", "Next post": "Article suivant", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Anciens articles", "Original site": "Site d'origine", "Posted": "Publié", diff --git a/nikola/data/themes/base/messages/messages_hi.py b/nikola/data/themes/base/messages/messages_hi.py new file mode 100644 index 0000000..f72d5af --- /dev/null +++ b/nikola/data/themes/base/messages/messages_hi.py @@ -0,0 +1,31 @@ +# -*- encoding:utf-8 -*- +from __future__ import unicode_literals + +MESSAGES = { + "Also available in": "उपलब्ध भाषाएँ", + "Also available in:": "उपलब्ध भाषाएँ:", + "Archive": "आर्काइव", + "Categories": "श्रेणियाँ", + "LANGUAGE": "हिन्दी", + "More posts about %s": "%s के बारे में अौर पोस्टें", + "More posts about": " के बारे में अौर पोस्टें", + "Newer posts": "नई पोस्टें", + "Next post": "अगली पोस्ट", + "No posts found.": "", + "Nothing found.": "", + "Older posts": "पुरानी पोस्टें", + "Original site": "असली साइट", + "Posted": "पोस्टेड", + "Posted:": "पोस्टेड:", + "Posts about %s": "%s के बारे में पोस्टें", + "Posts for year %s": "साल %s की पोस्टें", + "Posts for {month} {year}": "{month} {year} की पोस्टें", + "Previous post": "पिछली पोस्ट", + "Read in English": "हिन्दी में पढ़िए", + "Read more": "और पढ़िए", + "Source": "सोर्स", + "Tags and Categories": "टैग्स और श्रेणियाँ", + "Tags": "टैग्स", + "old posts, page %d": "पुरानी पोस्टें, पृष्ठ %d", + "page %d": "पृष्ठ %d", +} diff --git a/nikola/data/themes/base/messages/messages_hr.py b/nikola/data/themes/base/messages/messages_hr.py index ad74078..ee5ce41 100644 --- a/nikola/data/themes/base/messages/messages_hr.py +++ b/nikola/data/themes/base/messages/messages_hr.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Više postova o", "Newer posts": "Noviji postovi", "Next post": "Sljedeći post", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Stariji postovi", "Original site": "Izvorna stranica", "Posted": "Objavljeno", diff --git a/nikola/data/themes/base/messages/messages_it.py b/nikola/data/themes/base/messages/messages_it.py index 912342e..87e25e5 100644 --- a/nikola/data/themes/base/messages/messages_it.py +++ b/nikola/data/themes/base/messages/messages_it.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Altri articoli collegati", "Newer posts": "Articoli recenti", "Next post": "Articolo successivo", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Articoli precedenti", "Original site": "Sito originale", "Posted": "Pubblicato", diff --git a/nikola/data/themes/base/messages/messages_ja.py b/nikola/data/themes/base/messages/messages_ja.py index 1bdf168..2df16a4 100644 --- a/nikola/data/themes/base/messages/messages_ja.py +++ b/nikola/data/themes/base/messages/messages_ja.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "タグ:", "Newer posts": "新しい記事", "Next post": "次の記事", + "No posts found.": "", + "Nothing found.": "", "Older posts": "過去の記事", "Original site": "元のサイト", "Posted": "投稿日時", diff --git a/nikola/data/themes/base/messages/messages_nb.py b/nikola/data/themes/base/messages/messages_nb.py index 154e329..44fde8a 100644 --- a/nikola/data/themes/base/messages/messages_nb.py +++ b/nikola/data/themes/base/messages/messages_nb.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Flere innlegg om", "Newer posts": "Nyere innlegg", "Next post": "Neste innlegg", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Eldre innlegg", "Original site": "Opprinnelig side", "Posted": "Publisert", diff --git a/nikola/data/themes/base/messages/messages_nl.py b/nikola/data/themes/base/messages/messages_nl.py index 887e85f..1952d2e 100644 --- a/nikola/data/themes/base/messages/messages_nl.py +++ b/nikola/data/themes/base/messages/messages_nl.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Meer berichten over", "Newer posts": "Nieuwere berichten", "Next post": "Volgend bericht", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Oudere berichten", "Original site": "Originele site", "Posted": "Geplaatst", diff --git a/nikola/data/themes/base/messages/messages_pl.py b/nikola/data/themes/base/messages/messages_pl.py index 352b0ed..a1183ba 100644 --- a/nikola/data/themes/base/messages/messages_pl.py +++ b/nikola/data/themes/base/messages/messages_pl.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Więcej postów o", "Newer posts": "Nowsze posty", "Next post": "Następny post", + "No posts found.": "Nie znaleziono żadnych postów.", + "Nothing found.": "Nic nie znaleziono.", "Older posts": "Starsze posty", "Original site": "Oryginalna strona", "Posted": "Opublikowano", diff --git a/nikola/data/themes/base/messages/messages_pt_br.py b/nikola/data/themes/base/messages/messages_pt_br.py index 1283a2a..bf515e4 100644 --- a/nikola/data/themes/base/messages/messages_pt_br.py +++ b/nikola/data/themes/base/messages/messages_pt_br.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Mais posts sobre", "Newer posts": "Posts mais recentes", "Next post": "Próximo post", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Posts mais antigos", "Original site": "Site original", "Posted": "Publicado", diff --git a/nikola/data/themes/base/messages/messages_ru.py b/nikola/data/themes/base/messages/messages_ru.py index 3462292..fb33b85 100644 --- a/nikola/data/themes/base/messages/messages_ru.py +++ b/nikola/data/themes/base/messages/messages_ru.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Больше записей о", "Newer posts": "Новые записи", "Next post": "Следующая запись", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Старые записи", "Original site": "Оригинальный сайт", "Posted": "Опубликовано", diff --git a/nikola/data/themes/base/messages/messages_sl.py b/nikola/data/themes/base/messages/messages_sl.py index 817bcee..92ad483 100644 --- a/nikola/data/themes/base/messages/messages_sl.py +++ b/nikola/data/themes/base/messages/messages_sl.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "Več objav o", "Newer posts": "Novejše objave", "Next post": "Naslednja objava", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Starejše objave", "Original site": "Izvorna spletna stran", "Posted": "Objavljeno", diff --git a/nikola/data/themes/base/messages/messages_tr_tr.py b/nikola/data/themes/base/messages/messages_tr_tr.py index 633f057..95c5736 100644 --- a/nikola/data/themes/base/messages/messages_tr_tr.py +++ b/nikola/data/themes/base/messages/messages_tr_tr.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": " ilgili diğer yazılar", "Newer posts": "Daha yeni yazılar", "Next post": "Sonraki yazı", + "No posts found.": "", + "Nothing found.": "", "Older posts": "Daha eski yazılar", "Original site": "Orjinal web sayfası", "Posted": "Yayın tarihi", diff --git a/nikola/data/themes/base/messages/messages_ur.py b/nikola/data/themes/base/messages/messages_ur.py index d9c2f2b..794861d 100644 --- a/nikola/data/themes/base/messages/messages_ur.py +++ b/nikola/data/themes/base/messages/messages_ur.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": " کے بارے میں مزید تحاریر", "Newer posts": "نئی تحاریر", "Next post": "اگلی تحریر", + "No posts found.": "", + "Nothing found.": "", "Older posts": "پرانی تحاریر", "Original site": "اصلی سائٹ", "Posted": "اشاعت", @@ -25,5 +27,5 @@ MESSAGES = { "Tags and Categories": "ٹیگز اور زمرے", "Tags": "ٹیگز", "old posts, page %d": "پرانی تحاریر صفحہ %d", - "page %d": "", + "page %d": "صفحہ %d", } diff --git a/nikola/data/themes/base/messages/messages_zh_cn.py b/nikola/data/themes/base/messages/messages_zh_cn.py index cb9a2f7..2f937c7 100644 --- a/nikola/data/themes/base/messages/messages_zh_cn.py +++ b/nikola/data/themes/base/messages/messages_zh_cn.py @@ -11,6 +11,8 @@ MESSAGES = { "More posts about": "更多相关文章:", "Newer posts": "新一篇", "Next post": "后一篇", + "No posts found.": "", + "Nothing found.": "", "Older posts": "旧一篇", "Original site": "原文地址", "Posted": "发表于", diff --git a/nikola/data/themes/base/templates/base.tmpl b/nikola/data/themes/base/templates/base.tmpl index 7c6cc35..8a90349 100644 --- a/nikola/data/themes/base/templates/base.tmpl +++ b/nikola/data/themes/base/templates/base.tmpl @@ -30,11 +30,16 @@ lang="${lang}"> <small>${content_footer}</small> <!--Sidebar content--> <ul class="unstyled"> + %if license: <li>${license} + %endif ${base.html_social()} ${base.html_navigation_links()} + %if search_form: <li>${search_form} + %endif </ul> ${base.late_load_js()} ${social_buttons_code} </body> +</html> diff --git a/nikola/data/themes/base/templates/base_helper.tmpl b/nikola/data/themes/base/templates/base_helper.tmpl index 880a998..501c06e 100644 --- a/nikola/data/themes/base/templates/base_helper.tmpl +++ b/nikola/data/themes/base/templates/base_helper.tmpl @@ -21,7 +21,9 @@ <link href="/assets/css/custom.css" rel="stylesheet" type="text/css"> %endif %endif - <link rel="canonical" href="${abs_link(permalink)}"> + %if permalink: + <link rel="canonical" href="${abs_link(permalink)}"> + %endif <!--[if lt IE 9]> <script src="http://html5shim.googlecode.com/svn/trunk/html5.js" type="text/javascript"></script> <![endif]--> diff --git a/nikola/data/themes/base/templates/crumbs.tmpl b/nikola/data/themes/base/templates/crumbs.tmpl index c458cbe..8fbafcf 100644 --- a/nikola/data/themes/base/templates/crumbs.tmpl +++ b/nikola/data/themes/base/templates/crumbs.tmpl @@ -1,9 +1,11 @@ ## -*- coding: utf-8 -*- <%def name="bar(crumbs)"> +%if crumbs: <ul class="breadcrumb"> % for link, text in crumbs: <li><a href="${link}">${text}</a></li> % endfor </ul> +%endif </%def> diff --git a/nikola/data/themes/base/templates/gallery.tmpl b/nikola/data/themes/base/templates/gallery.tmpl index e4eab27..731a75a 100644 --- a/nikola/data/themes/base/templates/gallery.tmpl +++ b/nikola/data/themes/base/templates/gallery.tmpl @@ -14,18 +14,22 @@ ${text} </p> %endif + %if folders: <ul> % for folder, ftitle in folders: <li><a href="${folder}"><i class="icon-folder-open"></i> ${ftitle}</a></li> % endfor </ul> + %endif + %if photo_array: <ul class="thumbnails"> %for image in photo_array: <li><a href="${image['url']}" class="thumbnail image-reference" title="${image['title']}"> <img src="${image['url_thumb']}" alt="${image['title']}" /></a> %endfor </ul> + %endif %if enable_comments: ${comments.comment_form(None, permalink, title)} %endif diff --git a/nikola/data/themes/base/templates/index_helper.tmpl b/nikola/data/themes/base/templates/index_helper.tmpl index 56f5127..c925559 100755..100644 --- a/nikola/data/themes/base/templates/index_helper.tmpl +++ b/nikola/data/themes/base/templates/index_helper.tmpl @@ -20,6 +20,11 @@ <%def name="mathjax_script(posts)"> %if any(post.is_mathjax for post in posts): + <script type="text/x-mathjax-config"> + MathJax.Hub.Config({ + tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]} + }); + </script> <script src="/assets/js/mathjax.js"></script> %endif </%def> diff --git a/nikola/data/themes/base/templates/list.tmpl b/nikola/data/themes/base/templates/list.tmpl index a60b508..4136eb9 100644 --- a/nikola/data/themes/base/templates/list.tmpl +++ b/nikola/data/themes/base/templates/list.tmpl @@ -4,11 +4,13 @@ <!--Body content--> <div class="postbox"> <h1>${title}</h1> + %if items: <ul class="unstyled"> % for text, link in items: <li><a href="${link}">${text}</a> % endfor </ul> + %endif </div> <!--End of body content--> </%block> diff --git a/nikola/data/themes/base/templates/list_post.tmpl b/nikola/data/themes/base/templates/list_post.tmpl index f0e159d..b27f230 100644 --- a/nikola/data/themes/base/templates/list_post.tmpl +++ b/nikola/data/themes/base/templates/list_post.tmpl @@ -4,11 +4,13 @@ <!--Body content--> <div class="postbox"> <h1>${title}</h1> + %if posts: <ul class="unstyled"> % for post in posts: <li><a href="${post.permalink()}">[${post.formatted_date(date_format)}] ${post.title()}</a> % endfor </ul> + %endif </div> <!--End of body content--> </%block> diff --git a/nikola/data/themes/base/templates/listing.tmpl b/nikola/data/themes/base/templates/listing.tmpl index b6ca83f..0662360 100644 --- a/nikola/data/themes/base/templates/listing.tmpl +++ b/nikola/data/themes/base/templates/listing.tmpl @@ -3,6 +3,7 @@ <%namespace name="ui" file="crumbs.tmpl" import="bar"/> <%block name="content"> ${ui.bar(crumbs)} +%if folders or files: <ul class="unstyled"> % for name in folders: <li><a href="${name}"><i class="icon-folder-open"></i> ${name}</a> @@ -11,6 +12,7 @@ ${ui.bar(crumbs)} <li><a href="${name}.html"><i class="icon-file"></i> ${name}</a> % endfor </ul> +%endif % if code: ${code} % endif diff --git a/nikola/data/themes/base/templates/post_helper.tmpl b/nikola/data/themes/base/templates/post_helper.tmpl index 69784ea..391350d 100755..100644 --- a/nikola/data/themes/base/templates/post_helper.tmpl +++ b/nikola/data/themes/base/templates/post_helper.tmpl @@ -85,6 +85,11 @@ <%def name="mathjax_script(post)"> %if post.is_mathjax: + <script type="text/x-mathjax-config"> + MathJax.Hub.Config({ + tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]} + }); + </script> <script src="/assets/js/mathjax.js"></script> %endif </%def> diff --git a/nikola/data/themes/base/templates/post_list_directive.tmpl b/nikola/data/themes/base/templates/post_list_directive.tmpl index 3345ae4..b31d242 100644 --- a/nikola/data/themes/base/templates/post_list_directive.tmpl +++ b/nikola/data/themes/base/templates/post_list_directive.tmpl @@ -1,6 +1,7 @@ ## -*- coding: utf-8 -*- <!-- Begin post-list ${post_list_id} --> <div id="${post_list_id}" class="post-list"> + %if posts: <ul class="post-list"> % for post in posts: <li class="post-list-item"> @@ -10,5 +11,6 @@ </li> % endfor </ul> + %endif </div> <!-- End post-list ${post_list_id} --> diff --git a/nikola/data/themes/base/templates/tag.tmpl b/nikola/data/themes/base/templates/tag.tmpl index 2ca9db4..43afd54 100644 --- a/nikola/data/themes/base/templates/tag.tmpl +++ b/nikola/data/themes/base/templates/tag.tmpl @@ -22,11 +22,13 @@ <a href="${_link(kind + "_rss", tag)}">RSS</a> %endif <br> + %if posts: <ul class="unstyled"> % for post in posts: <li><a href="${post.permalink()}">[${post.formatted_date(date_format)}] ${post.title()}</a> % endfor </ul> + %endif </div> <!--End of body content--> </%block> diff --git a/nikola/data/themes/bootstrap/templates/base.tmpl b/nikola/data/themes/bootstrap/templates/base.tmpl index 8cb2e43..65132b7 100644 --- a/nikola/data/themes/bootstrap/templates/base.tmpl +++ b/nikola/data/themes/bootstrap/templates/base.tmpl @@ -91,3 +91,4 @@ ${base.html_social()} % endif ${body_end} </body> +</html> diff --git a/nikola/data/themes/bootstrap/templates/bootstrap_helper.tmpl b/nikola/data/themes/bootstrap/templates/bootstrap_helper.tmpl index f0d1986..c041e50 100644 --- a/nikola/data/themes/bootstrap/templates/bootstrap_helper.tmpl +++ b/nikola/data/themes/bootstrap/templates/bootstrap_helper.tmpl @@ -29,7 +29,9 @@ <link href="/assets/css/custom.css" rel="stylesheet" type="text/css"> %endif %endif - <link rel="canonical" href="${abs_link(permalink)}"> + %if permalink: + <link rel="canonical" href="${abs_link(permalink)}"> + %endif <!--[if lt IE 9]> <script src="http://html5shim.googlecode.com/svn/trunk/html5.js" type="text/javascript"></script> <![endif]--> diff --git a/nikola/data/themes/bootstrap/templates/gallery.tmpl b/nikola/data/themes/bootstrap/templates/gallery.tmpl index 0dd5eea..7b0d505 100644 --- a/nikola/data/themes/bootstrap/templates/gallery.tmpl +++ b/nikola/data/themes/bootstrap/templates/gallery.tmpl @@ -14,14 +14,17 @@ ${text} </p> %endif + %if folders: <ul> % for folder, ftitle in folders: <li><a href="${folder}"><i class="icon-folder-open"></i> ${ftitle}</a></li> % endfor </ul> + %endif <div id="gallery_container"></div> + %if photo_array: <noscript> <ul class="thumbnails"> %for image in photo_array: @@ -30,6 +33,7 @@ %endfor </ul> </noscript> + %endif %if enable_comments: ${comments.comment_form(None, permalink, title)} %endif diff --git a/nikola/nikola.py b/nikola/nikola.py index 6971c0c..1d59954 100644 --- a/nikola/nikola.py +++ b/nikola/nikola.py @@ -25,8 +25,10 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function, unicode_literals +import codecs from collections import defaultdict from copy import copy +import datetime import glob import locale import os @@ -41,6 +43,7 @@ try: import pyphen except ImportError: pyphen = None +import pytz import logging from . import DEBUG @@ -66,11 +69,15 @@ from .plugin_categories import ( SignalHandler, ) +from .utils import ColorfulStderrHandler config_changed = utils.config_changed __all__ = ['Nikola'] +# Default pattern for translation files' names +DEFAULT_TRANSLATIONS_PATTERN = '{path}.{ext}.{lang}' + class Nikola(object): @@ -92,6 +99,7 @@ class Nikola(object): self.path_handlers = { 'slug': self.slug_path, 'post_path': self.post_path, + 'filename': self.filename_path, } self.strict = False @@ -111,8 +119,15 @@ class Nikola(object): self.loghandlers = [] if not config: self.configured = False + self.colorful = False else: self.configured = True + self.colorful = config.pop('__colorful__', False) + + ColorfulStderrHandler._colorful = self.colorful + + # Maintain API + utils.generic_rss_renderer = self.generic_rss_renderer # This is the default config self.config = { @@ -173,6 +188,7 @@ class Nikola(object): 'INDEX_PATH': '', 'IPYNB_CONFIG': {}, 'LESS_COMPILER': 'lessc', + 'LESS_OPTIONS': [], 'LICENSE': '', 'LINK_CHECK_WHITELIST': [], 'LISTINGS_FOLDER': 'listings', @@ -192,6 +208,7 @@ class Nikola(object): 'RSS_PATH': '', 'RSS_TEASERS': True, 'SASS_COMPILER': 'sass', + 'SASS_OPTIONS': [], 'SEARCH_FORM': '', 'SLUG_TAG_PATH': True, 'SOCIAL_BUTTONS_CODE': SOCIAL_BUTTONS_CODE, @@ -201,6 +218,7 @@ class Nikola(object): 'SITEMAP_INCLUDE_FILELESS_DIRS': True, 'TAG_PATH': 'categories', 'TAG_PAGES_ARE_INDEXES': False, + 'TEMPLATE_FILTERS': {}, 'THEME': 'bootstrap', 'THEME_REVEAL_CONFIG_SUBTHEME': 'sky', 'THEME_REVEAL_CONFIG_TRANSITION': 'cube', @@ -217,7 +235,7 @@ class Nikola(object): 'SCHEDULE_FORCE_TODAY': False, 'LOGGING_HANDLERS': {'stderr': {'loglevel': 'WARNING', 'bubble': True}}, 'DEMOTE_HEADERS': 1, - 'TRANSLATIONS_PATTERN': '{path}.{ext}.{lang}', + 'TRANSLATIONS_PATTERN': DEFAULT_TRANSLATIONS_PATTERN, } self.config.update(config) @@ -302,10 +320,10 @@ class Nikola(object): self.config['STRIP_INDEXES'] = config['STRIP_INDEX_HTML'] # PRETTY_URLS defaults to enabling STRIP_INDEXES unless explicitly disabled - if config.get('PRETTY_URLS', False) and 'STRIP_INDEXES' not in config: + if self.config.get('PRETTY_URLS') and 'STRIP_INDEXES' not in config: self.config['STRIP_INDEXES'] = True - if config.get('COPY_SOURCES') and not self.config['HIDE_SOURCELINK']: + if not self.config.get('COPY_SOURCES'): self.config['HIDE_SOURCELINK'] = True self.config['TRANSLATIONS'] = self.config.get('TRANSLATIONS', @@ -408,6 +426,13 @@ class Nikola(object): self.plugin_manager.activatePluginByName(plugin_info.name) plugin_info.plugin_object.set_site(self) + # Also add aliases for combinations with TRANSLATIONS_PATTERN + self.config['COMPILERS'] = dict([(lang, list(exts) + [ + utils.get_translation_candidate(self.config, "f" + ext, lang)[1:] + for ext in exts + for lang in self.config['TRANSLATIONS'].keys()]) + for lang, exts in list(self.config['COMPILERS'].items())]) + # Activate all required compiler plugins for plugin_info in self.plugin_manager.getPluginsOfCategory("PageCompiler"): if plugin_info.name in self.config["COMPILERS"].keys(): @@ -462,6 +487,11 @@ class Nikola(object): self._GLOBAL_CONTEXT['navigation_links'] = utils.Functionary(list, self.config['DEFAULT_LANG']) for k, v in self.config.get('NAVIGATION_LINKS', {}).items(): self._GLOBAL_CONTEXT['navigation_links'][k] = v + + # avoid #1082 by making sure all keys in navigation_links are read once + for k in self._GLOBAL_CONTEXT['translations']: + self._GLOBAL_CONTEXT['navigation_links'][k] + # TODO: remove on v7 # Compatibility alias self._GLOBAL_CONTEXT['sidebar_links'] = self._GLOBAL_CONTEXT['navigation_links'] @@ -498,7 +528,7 @@ class Nikola(object): self.config['THEME'] = theme_replacements[self.config['THEME']] if self.config['THEME'] == 'oldfashioned': utils.LOGGER.warn('''You may need to install the "oldfashioned" theme ''' - '''from themes.nikola.ralsina.com.ar because it's not ''' + '''from themes.getnikola.com because it's not ''' '''shipped by default anymore.''') utils.LOGGER.warn('Please change your THEME setting.') try: @@ -560,6 +590,7 @@ class Nikola(object): for name in self.THEMES] self._template_system.set_directories(lookup_dirs, self.config['CACHE_FOLDER']) + self._template_system.set_site(self) return self._template_system template_system = property(_get_template_system) @@ -576,9 +607,9 @@ class Nikola(object): compile_html = self.inverse_compilers[ext] except KeyError: # Find the correct compiler for this files extension - langs = [lang for lang, exts in - list(self.config['COMPILERS'].items()) - if ext in exts] + lang_exts_tab = list(self.config['COMPILERS'].items()) + langs = [lang for lang, exts in lang_exts_tab if ext in exts or + len([ext_ for ext_ in exts if source_name.endswith(ext_)]) > 0] if len(langs) != 1: if len(set(langs)) > 1: exit("Your file extension->compiler definition is" @@ -618,78 +649,158 @@ class Nikola(object): # The os.sep is because normpath will change "/" to "\" on windows src = "/".join(src.split(os.sep)) - parsed_src = urlsplit(src) - src_elems = parsed_src.path.split('/')[1:] + utils.makedirs(os.path.dirname(output_name)) + doc = lxml.html.document_fromstring(data) + doc.rewrite_links(lambda dst: self.url_replacer(src, dst, context['lang'])) + data = b'<!DOCTYPE html>' + lxml.html.tostring(doc, encoding='utf8') + with open(output_name, "wb+") as post_file: + post_file.write(data) - def replacer(dst): - # Refuse to replace links that are full URLs. - dst_url = urlparse(dst) - if dst_url.netloc: - if dst_url.scheme == 'link': # Magic link - dst = self.link(dst_url.netloc, dst_url.path.lstrip('/'), - context['lang']) - else: - return dst + def url_replacer(self, src, dst, lang=None): + """URL mangler. - # Refuse to replace links that consist of a fragment only - if ((not dst_url.scheme) and (not dst_url.netloc) and - (not dst_url.path) and (not dst_url.params) and - (not dst_url.query) and dst_url.fragment): - return dst + * Replaces link:// URLs with real links + * Makes dst relative to src + * Leaves fragments unchanged + * Leaves full URLs unchanged + * Avoids empty links - # Normalize - dst = urljoin(src, dst) + src is the URL where this link is used + dst is the link to be mangled + lang is used for language-sensitive URLs in link:// - # Avoid empty links. - if src == dst: - if self.config.get('URL_TYPE') == 'absolute': - dst = urljoin(self.config['BASE_URL'], dst) - return dst - elif self.config.get('URL_TYPE') == 'full_path': - return dst - else: - return "#" + """ + parsed_src = urlsplit(src) + src_elems = parsed_src.path.split('/')[1:] + dst_url = urlparse(dst) + if lang is None: + lang = self.default_lang - # Check that link can be made relative, otherwise return dest - parsed_dst = urlsplit(dst) - if parsed_src[:2] != parsed_dst[:2]: - if self.config.get('URL_TYPE') == 'absolute': - dst = urljoin(self.config['BASE_URL'], dst) + # Refuse to replace links that are full URLs. + if dst_url.netloc: + if dst_url.scheme == 'link': # Magic link + dst = self.link(dst_url.netloc, dst_url.path.lstrip('/'), lang) + else: return dst + elif dst_url.scheme == 'link': # Magic absolute path link: + dst = dst_url.path + return dst - if self.config.get('URL_TYPE') in ('full_path', 'absolute'): - if self.config.get('URL_TYPE') == 'absolute': - dst = urljoin(self.config['BASE_URL'], dst) - return dst + # Refuse to replace links that consist of a fragment only + if ((not dst_url.scheme) and (not dst_url.netloc) and + (not dst_url.path) and (not dst_url.params) and + (not dst_url.query) and dst_url.fragment): + return dst - # Now both paths are on the same site and absolute - dst_elems = parsed_dst.path.split('/')[1:] + # Normalize + dst = urljoin(src, dst) - i = 0 - for (i, s), d in zip(enumerate(src_elems), dst_elems): - if s != d: - break - # Now i is the longest common prefix - result = '/'.join(['..'] * (len(src_elems) - i - 1) + - dst_elems[i:]) + # Avoid empty links. + if src == dst: + if self.config.get('URL_TYPE') == 'absolute': + dst = urljoin(self.config['BASE_URL'], dst.lstrip('/')) + return dst + elif self.config.get('URL_TYPE') == 'full_path': + dst = urljoin(self.config['BASE_URL'], dst.lstrip('/')) + return urlparse(dst).path + else: + return "#" - if not result: - result = "." + # Check that link can be made relative, otherwise return dest + parsed_dst = urlsplit(dst) + if parsed_src[:2] != parsed_dst[:2]: + if self.config.get('URL_TYPE') == 'absolute': + dst = urljoin(self.config['BASE_URL'], dst) + return dst - # Don't forget the fragment (anchor) part of the link - if parsed_dst.fragment: - result += "#" + parsed_dst.fragment + if self.config.get('URL_TYPE') in ('full_path', 'absolute'): + dst = urljoin(self.config['BASE_URL'], dst.lstrip('/')) + if self.config.get('URL_TYPE') == 'full_path': + parsed = urlparse(urljoin(self.config['BASE_URL'], dst.lstrip('/'))) + if parsed.fragment: + dst = '{0}#{1}'.format(parsed.path, parsed.fragment) + else: + dst = parsed.path + return dst - assert result, (src, dst, i, src_elems, dst_elems) + # Now both paths are on the same site and absolute + dst_elems = parsed_dst.path.split('/')[1:] - return result + i = 0 + for (i, s), d in zip(enumerate(src_elems), dst_elems): + if s != d: + break + # Now i is the longest common prefix + result = '/'.join(['..'] * (len(src_elems) - i - 1) + dst_elems[i:]) + + if not result: + result = "." + + # Don't forget the fragment (anchor) part of the link + if parsed_dst.fragment: + result += "#" + parsed_dst.fragment + + assert result, (src, dst, i, src_elems, dst_elems) + + return result + + def generic_rss_renderer(self, lang, title, link, description, timeline, output_path, + rss_teasers, feed_length=10, feed_url=None): + """Takes all necessary data, and renders a RSS feed in output_path.""" + items = [] + for post in timeline[:feed_length]: + # Massage the post's HTML + data = post.text(lang, teaser_only=rss_teasers, really_absolute=True) + if feed_url is not None and data: + # FIXME: this is duplicated with code in Post.text() + try: + doc = lxml.html.document_fromstring(data) + doc.rewrite_links(lambda dst: self.url_replacer(feed_url, dst, lang)) + try: + body = doc.body + data = (body.text or '') + ''.join( + [lxml.html.tostring(child, encoding='unicode') + for child in body.iterchildren()]) + except IndexError: # No body there, it happens sometimes + data = '' + except lxml.etree.ParserError as e: + if str(e) == "Document is empty": + data = "" + else: # let other errors raise + raise(e) + + args = { + 'title': post.title(lang), + 'link': post.permalink(lang, absolute=True), + 'description': data, + 'guid': post.permalink(lang, absolute=True), + # PyRSS2Gen's pubDate is GMT time. + 'pubDate': (post.date if post.date.tzinfo is None else + post.date.astimezone(pytz.timezone('UTC'))), + 'categories': post._tags.get(lang, []), + 'author': post.meta('author'), + } - utils.makedirs(os.path.dirname(output_name)) - doc = lxml.html.document_fromstring(data) - doc.rewrite_links(replacer) - data = b'<!DOCTYPE html>' + lxml.html.tostring(doc, encoding='utf8') - with open(output_name, "wb+") as post_file: - post_file.write(data) + items.append(utils.ExtendedItem(**args)) + rss_obj = utils.ExtendedRSS2( + title=title, + link=link, + description=description, + lastBuildDate=datetime.datetime.now(), + items=items, + generator='Nikola <http://getnikola.com/>', + language=lang + ) + rss_obj.self_url = feed_url + rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom" + rss_obj.rss_attrs["xmlns:dc"] = "http://purl.org/dc/elements/1.1/" + dst_dir = os.path.dirname(output_path) + utils.makedirs(dst_dir) + with codecs.open(output_path, "wb+", "utf-8") as rss_file: + data = rss_obj.to_xml(encoding='utf-8') + if isinstance(data, utils.bytes_str): + data = data.decode('utf-8') + rss_file.write(data) def path(self, kind, name, lang=None, is_link=False): """Build the path to a certain kind of page. @@ -712,6 +823,7 @@ class Nikola(object): * listing (name is the source code file name) * post_path (name is 1st element in a POSTS/PAGES tuple) * slug (name is the slug of a post or story) + * filename (name is the source filename of a post/story, in DEFAULT_LANG, relative to conf.py) The returned value is always a path relative to output, like "categories/whatever.html" @@ -727,6 +839,7 @@ class Nikola(object): lang = utils.LocaleBorg().current_lang path = self.path_handlers[kind](name, lang) + path = [os.path.normpath(p) for p in path if p != '.'] # Fix Issue #1028 if is_link: link = '/' + ('/'.join(path)) @@ -755,6 +868,16 @@ class Nikola(object): utils.LOGGER.warning('Ambiguous path request for slug: {0}'.format(name)) return [_f for _f in results[0].permalink(lang).split('/') if _f] + def filename_path(self, name, lang): + """filename path handler""" + results = [p for p in self.timeline if p.source_path == name] + if not results: + utils.LOGGER.warning("Can't resolve path request for filename: {0}".format(name)) + else: + if len(results) > 1: + utils.LOGGER.error("Ambiguous path request for filename: {0}".format(name)) + return [_f for _f in results[0].permalink(lang).split('/') if _f] + def register_path_handler(self, kind, f): if kind in self.path_handlers: utils.LOGGER.warning('Conflicting path handlers for kind: {0}'.format(kind)) @@ -766,8 +889,10 @@ class Nikola(object): def abs_link(self, dst): # Normalize - dst = urljoin(self.config['BASE_URL'], dst) - + if dst: # Mako templates and empty strings evaluate to False + dst = urljoin(self.config['BASE_URL'], dst.lstrip('/')) + else: + dst = self.config['BASE_URL'] return urlparse(dst).geturl() def rel_link(self, src, dst): @@ -848,7 +973,8 @@ class Nikola(object): return seen = set([]) print("Scanning posts", end='', file=sys.stderr) - lower_case_tags = set([]) + slugged_tags = set([]) + quit = False for wildcard, destination, template_name, use_in_feeds in \ self.config['post_pages']: print(".", end='', file=sys.stderr) @@ -864,11 +990,16 @@ class Nikola(object): translated_list = glob.glob(lang_glob) # dir_glob could have put it already in full_list full_list = list(set(full_list + translated_list)) - # Eliminate translations from full_list (even from dir_glob) - for fname in full_list: + + # Eliminate translations from full_list if they are not the primary, + # or a secondary with no primary + limited_list = full_list[:] + for fname in full_list: + for lang in self.config['TRANSLATIONS'].keys(): translation = utils.get_translation_candidate(self.config, fname, lang) if translation in full_list: - full_list.remove(translation) + limited_list.remove(translation) + full_list = limited_list # We eliminate from the list the files inside any .ipynb folder full_list = [p for p in full_list @@ -897,16 +1028,16 @@ class Nikola(object): self.posts_per_month[ '{0}/{1:02d}'.format(post.date.year, post.date.month)].append(post.source_path) for tag in post.alltags: - if tag.lower() in lower_case_tags: + if utils.slugify(tag) in slugged_tags: if tag not in self.posts_per_tag: # Tags that differ only in case other_tag = [k for k in self.posts_per_tag.keys() if k.lower() == tag.lower()][0] - utils.LOGGER.error('You have cases that differ only in upper/lower case: {0} and {1}'.format(tag, other_tag)) + utils.LOGGER.error('You have tags that are too similar: {0} and {1}'.format(tag, other_tag)) utils.LOGGER.error('Tag {0} is used in: {1}'.format(tag, post.source_path)) utils.LOGGER.error('Tag {0} is used in: {1}'.format(other_tag, ', '.join(self.posts_per_tag[other_tag]))) - sys.exit(1) + quit = True else: - lower_case_tags.add(tag.lower()) + slugged_tags.add(utils.slugify(tag)) self.posts_per_tag[tag].append(post.source_path) self.posts_per_category[post.meta('category')].append(post.source_path) else: @@ -925,6 +1056,8 @@ class Nikola(object): p.prev_post = post_timeline[i + 1] self._scanned = True print("done!", file=sys.stderr) + if quit: + sys.exit(1) def generic_page_renderer(self, lang, post, filters): """Render post fragments to final HTML pages.""" diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 68327aa..516df92 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -151,6 +151,10 @@ class TemplateSystem(BasePlugin): """Sets the list of folders where templates are located and cache.""" raise NotImplementedError() + def set_site(self, site): + """Sets the site.""" + self.site = site + def template_deps(self, template_name): """Returns filenames which are dependencies for a template.""" raise NotImplementedError() diff --git a/nikola/plugins/basic_import.py b/nikola/plugins/basic_import.py index 0d94d16..27c0eb4 100644 --- a/nikola/plugins/basic_import.py +++ b/nikola/plugins/basic_import.py @@ -48,8 +48,8 @@ class ImportMixin(object): name = "import_mixin" needs_config = False - doc_usage = "[options] wordpress_export_file" - doc_purpose = "import a wordpress dump." + doc_usage = "[options] export_file" + doc_purpose = "import a dump from a different engine." cmd_options = [ { 'name': 'output_folder', @@ -93,7 +93,7 @@ class ImportMixin(object): else: self.import_into_existing_site = True utils.LOGGER.notice('The folder {0} already exists - assuming that this is a ' - 'already existing nikola site.'.format(self.output_folder)) + 'already existing Nikola site.'.format(self.output_folder)) filename = os.path.join(os.path.dirname(utils.__file__), 'conf.py.in') # The 'strict_undefined=True' will give the missing symbol name if any, @@ -151,7 +151,7 @@ class ImportMixin(object): time=datetime.datetime.now().strftime('%Y%m%d_%H%M%S'), name=self.name) config_output_path = os.path.join(self.output_folder, filename) - utils.LOGGER.notice('Configuration will be written to: {0}'.format(config_output_path)) + utils.LOGGER.info('Configuration will be written to: {0}'.format(config_output_path)) return config_output_path diff --git a/nikola/plugins/command/auto.py b/nikola/plugins/command/auto.py index 01116d1..d707d53 100644 --- a/nikola/plugins/command/auto.py +++ b/nikola/plugins/command/auto.py @@ -34,7 +34,7 @@ from nikola.plugin_categories import Command from nikola.utils import req_missing -class Auto(Command): +class CommandAuto(Command): """Start debugging console.""" name = "auto" doc_purpose = "automatically detect site changes, rebuild and optionally refresh a browser" @@ -61,26 +61,26 @@ class Auto(Command): try: from livereload import Server except ImportError: - req_missing(['livereload>=2.0.0'], 'use the "auto" command') + req_missing(['livereload==2.1.0'], 'use the "auto" command') return - # Run an initial build so we are uptodate + # Run an initial build so we are up-to-date subprocess.call(("nikola", "build")) port = options and options.get('port') server = Server() - server.watch('conf.py') - server.watch('themes/') - server.watch('templates/') + server.watch('conf.py', 'nikola build') + server.watch('themes/', 'nikola build') + server.watch('templates/', 'nikola build') server.watch(self.site.config['GALLERY_PATH']) for item in self.site.config['post_pages']: - server.watch(os.path.dirname(item[0])) + server.watch(os.path.dirname(item[0]), 'nikola build') for item in self.site.config['FILES_FOLDERS']: - server.watch(os.path.dirname(item)) + server.watch(os.path.dirname(item), 'nikola build') out_folder = self.site.config['OUTPUT_FOLDER'] if options and options.get('browser'): webbrowser.open('http://localhost:{0}'.format(port)) - server.serve(port, out_folder) + server.serve(port, None, out_folder) diff --git a/nikola/plugins/command/bootswatch_theme.py b/nikola/plugins/command/bootswatch_theme.py index 94f37f2..82c47d2 100644 --- a/nikola/plugins/command/bootswatch_theme.py +++ b/nikola/plugins/command/bootswatch_theme.py @@ -86,12 +86,14 @@ class CommandBootswatchTheme(Command): version = '2' elif 'bootstrap' not in themes: LOGGER.warn('"bootswatch_theme" only makes sense for themes that use bootstrap') + elif 'bootstrap3-gradients' in themes or 'bootstrap3-gradients-jinja' in themes: + LOGGER.warn('"bootswatch_theme" doesn\'t work well with the bootstrap3-gradients family') - LOGGER.notice("Creating '{0}' theme from '{1}' and '{2}'".format(name, swatch, parent)) + LOGGER.info("Creating '{0}' theme from '{1}' and '{2}'".format(name, swatch, parent)) utils.makedirs(os.path.join('themes', name, 'assets', 'css')) for fname in ('bootstrap.min.css', 'bootstrap.css'): url = '/'.join(('http://bootswatch.com', version, swatch, fname)) - LOGGER.notice("Downloading: " + url) + LOGGER.info("Downloading: " + url) data = requests.get(url).text with open(os.path.join('themes', name, 'assets', 'css', fname), 'wb+') as output: @@ -99,5 +101,4 @@ class CommandBootswatchTheme(Command): with open(os.path.join('themes', name, 'parent'), 'wb+') as output: output.write(parent.encode('utf-8')) - LOGGER.notice('Theme created. Change the THEME setting to "{0}" to use ' - 'it.'.format(name)) + LOGGER.notice('Theme created. Change the THEME setting to "{0}" to use it.'.format(name)) diff --git a/nikola/plugins/command/check.py b/nikola/plugins/command/check.py index a7e8c13..26db321 100644 --- a/nikola/plugins/command/check.py +++ b/nikola/plugins/command/check.py @@ -102,6 +102,14 @@ class CommandCheck(Command): 'default': False, 'help': 'List possible source files for files with broken links.', }, + { + 'name': 'verbose', + 'long': 'verbose', + 'short': 'v', + 'type': bool, + 'default': False, + 'help': 'Be more verbose.', + }, ] def _execute(self, options, args): @@ -112,6 +120,10 @@ class CommandCheck(Command): if not options['links'] and not options['files'] and not options['clean']: print(self.help()) return False + if options['verbose']: + self.logger.level = 1 + else: + self.logger.level = 4 if options['links']: failure = self.scan_links(options['find_sources']) if options['files']: @@ -126,6 +138,8 @@ class CommandCheck(Command): def analyze(self, task, find_sources=False): rv = False self.whitelist = [re.compile(x) for x in self.site.config['LINK_CHECK_WHITELIST']] + base_url = urlparse(self.site.config['BASE_URL']) + url_type = self.site.config['URL_TYPE'] try: filename = task.split(":")[-1] d = lxml.html.fromstring(open(filename).read()) @@ -134,31 +148,51 @@ class CommandCheck(Command): if target == "#": continue parsed = urlparse(target) - if parsed.scheme or target.startswith('//'): + + # Absolute links when using only paths, skip. + if (parsed.scheme or target.startswith('//')) and url_type in ('rel_path', 'full_path'): continue + + # Absolute links to other domains, skip + if (parsed.scheme or target.startswith('//')) and parsed.netloc != base_url.netloc: + continue + if parsed.fragment: target = target.split('#')[0] - target_filename = os.path.abspath( - os.path.join(os.path.dirname(filename), unquote(target))) + if url_type == 'rel_path': + target_filename = os.path.abspath( + os.path.join(os.path.dirname(filename), unquote(target))) + + elif url_type in ('full_path', 'absolute'): + target_filename = os.path.abspath( + os.path.join(os.path.dirname(filename), parsed.path)) + if parsed.path.endswith('/'): # abspath removes trailing slashes + target_filename += '/{0}'.format(self.site.config['INDEX_FILE']) + if target_filename.startswith(base_url.path): + target_filename = target_filename[len(base_url.path):] + target_filename = os.path.join(self.site.config['OUTPUT_FOLDER'], target_filename) + if any(re.match(x, target_filename) for x in self.whitelist): continue elif target_filename not in self.existing_targets: if os.path.exists(target_filename): + self.logger.notice("Good link {0} => {1}".format(target, target_filename)) self.existing_targets.add(target_filename) else: rv = True - self.logger.warn("Broken link in {0}: ".format(filename), target) + self.logger.warn("Broken link in {0}: {1}".format(filename, target)) if find_sources: self.logger.warn("Possible sources:") self.logger.warn(os.popen('nikola list --deps ' + task, 'r').read()) self.logger.warn("===============================\n") except Exception as exc: - self.logger.error("Error with:", filename, exc) + self.logger.error("Error with: {0} {1}".format(filename, exc)) return rv def scan_links(self, find_sources=False): - self.logger.notice("Checking Links:") - self.logger.notice("===============") + self.logger.info("Checking Links:") + self.logger.info("===============\n") + self.logger.notice("{0} mode".format(self.site.config['URL_TYPE'])) failure = False for task in os.popen('nikola list --all', 'r').readlines(): task = task.strip() @@ -170,13 +204,13 @@ class CommandCheck(Command): if self.analyze(task, find_sources): failure = True if not failure: - self.logger.notice("All links checked.") + self.logger.info("All links checked.") return failure def scan_files(self): failure = False - self.logger.notice("Checking Files:") - self.logger.notice("===============\n") + self.logger.info("Checking Files:") + self.logger.info("===============\n") only_on_output, only_on_input = real_scan_files(self.site) # Ignore folders @@ -195,7 +229,7 @@ class CommandCheck(Command): for f in only_on_input: self.logger.warn(f) if not failure: - self.logger.notice("All files checked.") + self.logger.info("All files checked.") return failure def clean_files(self): diff --git a/nikola/plugins/command/console.py b/nikola/plugins/command/console.py index e66b650..b0a8958 100644 --- a/nikola/plugins/command/console.py +++ b/nikola/plugins/command/console.py @@ -35,7 +35,7 @@ from nikola.utils import get_logger, STDERR_HANDLER LOGGER = get_logger('console', STDERR_HANDLER) -class Console(Command): +class CommandConsole(Command): """Start debugging console.""" name = "console" shells = ['ipython', 'bpython', 'plain'] diff --git a/nikola/plugins/command/deploy.py b/nikola/plugins/command/deploy.py index eb5787e..bd1c15f 100644 --- a/nikola/plugins/command/deploy.py +++ b/nikola/plugins/command/deploy.py @@ -25,7 +25,6 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function -from ast import literal_eval import codecs from datetime import datetime import os @@ -40,8 +39,8 @@ from nikola.plugin_categories import Command from nikola.utils import remove_file, get_logger -class Deploy(Command): - """Deploy site. """ +class CommandDeploy(Command): + """Deploy site.""" name = "deploy" doc_usage = "" @@ -76,7 +75,7 @@ class Deploy(Command): undeployed_posts.append(post) for command in self.site.config['DEPLOY_COMMANDS']: - self.logger.notice("==> {0}".format(command)) + self.logger.info("==> {0}".format(command)) try: subprocess.check_call(command, shell=True) except subprocess.CalledProcessError as e: @@ -84,26 +83,22 @@ class Deploy(Command): 'returned {1}'.format(e.cmd, e.returncode)) sys.exit(e.returncode) - self.logger.notice("Successful deployment") - tzinfo = pytz.timezone(self.site.config['TIMEZONE']) + self.logger.info("Successful deployment") try: - with open(timestamp_path, 'rb') as inf: - last_deploy = literal_eval(inf.read().strip()) - if tzinfo: - last_deploy = last_deploy.replace(tzinfo=tzinfo) + with codecs.open(timestamp_path, 'rb', 'utf8') as inf: + last_deploy = datetime.strptime(inf.read().strip(), "%Y-%m-%dT%H:%M:%S.%f") clean = False - except Exception: + except (IOError, Exception) as e: + self.logger.debug("Problem when reading `{0}`: {1}".format(timestamp_path, e)) last_deploy = datetime(1970, 1, 1) - if tzinfo: - last_deploy = last_deploy.replace(tzinfo=tzinfo) clean = True - new_deploy = datetime.now() + new_deploy = datetime.utcnow() self._emit_deploy_event(last_deploy, new_deploy, clean, undeployed_posts) # Store timestamp of successful deployment with codecs.open(timestamp_path, 'wb+', 'utf8') as outf: - outf.write(repr(new_deploy)) + outf.write(new_deploy.isoformat()) def _emit_deploy_event(self, last_deploy, new_deploy, clean=False, undeployed=None): """ Emit events for all timeline entries newer than last deploy. @@ -129,9 +124,11 @@ class Deploy(Command): 'undeployed': undeployed } + tzinfo = pytz.timezone(self.site.config['TIMEZONE']) + deployed = [ entry for entry in self.site.timeline - if entry.date > last_deploy and entry not in undeployed + if entry.date > (last_deploy.replace(tzinfo=tzinfo) if tzinfo else last_deploy) and entry not in undeployed ] event['deployed'] = deployed diff --git a/nikola/plugins/command/import_blogger.py b/nikola/plugins/command/import_blogger.py index ea12b4a..dd629c4 100644 --- a/nikola/plugins/command/import_blogger.py +++ b/nikola/plugins/command/import_blogger.py @@ -43,6 +43,7 @@ from nikola.plugin_categories import Command from nikola import utils from nikola.utils import req_missing from nikola.plugins.basic_import import ImportMixin +from nikola.plugins.command.init import SAMPLE_CONF, prepare_config LOGGER = utils.get_logger('import_blogger', utils.STDERR_HANDLER) @@ -95,8 +96,8 @@ class CommandImportBlogger(Command, ImportMixin): conf_out_path = self.get_configuration_output_path() # if it tracebacks here, look a comment in # basic_import.Import_Mixin.generate_base_site - conf_termplate_render = conf_template.render(**self.context) - self.write_configuration(conf_out_path, conf_termplate_render) + conf_template_render = conf_template.render(**prepare_config(self.context)) + self.write_configuration(conf_out_path, conf_template_render) @classmethod def get_channel_from_file(cls, filename): @@ -106,8 +107,7 @@ class CommandImportBlogger(Command, ImportMixin): @staticmethod def populate_context(channel): - # may need changes when the template conf.py.in changes - context = {} + context = SAMPLE_CONF.copy() context['DEFAULT_LANG'] = 'en' # blogger doesn't include the language # in the dump context['BLOG_TITLE'] = channel.feed.title @@ -131,7 +131,6 @@ class CommandImportBlogger(Command, ImportMixin): "html": ('.html', '.htm') } ''' - context['THEME'] = 'bootstrap3' return context diff --git a/nikola/plugins/command/import_feed.py b/nikola/plugins/command/import_feed.py index 70a5cd5..ee59277 100644 --- a/nikola/plugins/command/import_feed.py +++ b/nikola/plugins/command/import_feed.py @@ -43,6 +43,7 @@ from nikola.plugin_categories import Command from nikola import utils from nikola.utils import req_missing from nikola.plugins.basic_import import ImportMixin +from nikola.plugins.command.init import SAMPLE_CONF, prepare_config LOGGER = utils.get_logger('import_feed', utils.STDERR_HANDLER) @@ -82,7 +83,7 @@ class CommandImportFeed(Command, ImportMixin): self.import_posts(channel) self.write_configuration(self.get_configuration_output_path( - ), conf_template.render(**self.context)) + ), conf_template.render(**prepare_config(self.context))) @classmethod def get_channel_from_file(cls, filename): @@ -90,7 +91,7 @@ class CommandImportFeed(Command, ImportMixin): @staticmethod def populate_context(channel): - context = {} + context = SAMPLE_CONF.copy() context['DEFAULT_LANG'] = channel.feed.title_detail.language \ if channel.feed.title_detail.language else 'en' context['BLOG_TITLE'] = channel.feed.title @@ -100,9 +101,11 @@ class CommandImportFeed(Command, ImportMixin): context['BLOG_EMAIL'] = channel.feed.author_detail.get('email', '') if 'author_detail' in channel.feed else '' context['BLOG_AUTHOR'] = channel.feed.author_detail.get('name', '') if 'author_detail' in channel.feed else '' - context['POST_PAGES'] = '''( - ("posts/*.html", "posts", "post.tmpl", True), - ("stories/*.html", "stories", "story.tmpl", False), + context['POSTS'] = '''( + ("posts/*.html", "posts", "post.tmpl"), + )''' + context['PAGES'] = '''( + ("stories/*.html", "stories", "story.tmpl"), )''' context['COMPILERS'] = '''{ "rest": ('.txt', '.rst'), diff --git a/nikola/plugins/command/import_wordpress.py b/nikola/plugins/command/import_wordpress.py index 0c9915a..b567c77 100644 --- a/nikola/plugins/command/import_wordpress.py +++ b/nikola/plugins/command/import_wordpress.py @@ -50,6 +50,8 @@ from nikola.plugin_categories import Command from nikola import utils from nikola.utils import req_missing from nikola.plugins.basic_import import ImportMixin, links +from nikola.nikola import DEFAULT_TRANSLATIONS_PATTERN +from nikola.plugins.command.init import SAMPLE_CONF, prepare_config LOGGER = utils.get_logger('import_wordpress', utils.STDERR_HANDLER) @@ -84,6 +86,23 @@ class CommandImportWordpress(Command, ImportMixin): 'type': bool, 'help': "Do not try to download files for the import", }, + { + 'name': 'separate_qtranslate_content', + 'long': 'qtranslate', + 'default': False, + 'type': bool, + 'help': "Look for translations generated by qtranslate plugin", + # WARNING: won't recover translated titles that actually + # don't seem to be part of the wordpress XML export at the + # time of writing :( + }, + { + 'name': 'translations_pattern', + 'long': 'translations_pattern', + 'default': None, + 'type': str, + 'help': "The pattern for translation files names", + }, ] def _execute(self, options={}, args=[]): @@ -114,6 +133,9 @@ class CommandImportWordpress(Command, ImportMixin): self.exclude_drafts = options.get('exclude_drafts', False) self.no_downloads = options.get('no_downloads', False) + self.separate_qtranslate_content = options.get('separate_qtranslate_content') + self.translations_pattern = options.get('translations_pattern') + if not self.no_downloads: def show_info_about_mising_module(modulename): LOGGER.error( @@ -135,15 +157,21 @@ class CommandImportWordpress(Command, ImportMixin): self.context = self.populate_context(channel) conf_template = self.generate_base_site() + # If user has specified a custom pattern for translation files we + # need to fix the config + if self.translations_pattern: + self.context['TRANSLATIONS_PATTERN'] = self.translations_pattern + self.import_posts(channel) self.context['REDIRECTIONS'] = self.configure_redirections( self.url_map) self.write_urlmap_csv( os.path.join(self.output_folder, 'url_map.csv'), self.url_map) - rendered_template = conf_template.render(**self.context) + rendered_template = conf_template.render(**prepare_config(self.context)) rendered_template = re.sub('# REDIRECTIONS = ', 'REDIRECTIONS = ', rendered_template) + if self.timezone: rendered_template = re.sub('# TIMEZONE = \'UTC\'', 'TIMEZONE = \'' + self.timezone + '\'', @@ -194,8 +222,9 @@ class CommandImportWordpress(Command, ImportMixin): def populate_context(channel): wordpress_namespace = channel.nsmap['wp'] - context = {} + context = SAMPLE_CONF.copy() context['DEFAULT_LANG'] = get_text_tag(channel, 'language', 'en')[:2] + context['TRANSLATIONS_PATTERN'] = DEFAULT_TRANSLATIONS_PATTERN context['BLOG_TITLE'] = get_text_tag(channel, 'title', 'PUT TITLE HERE') context['BLOG_DESCRIPTION'] = get_text_tag( @@ -205,9 +234,10 @@ class CommandImportWordpress(Command, ImportMixin): base_site_url = channel.find('{{{0}}}author'.format(wordpress_namespace)) context['BASE_URL'] = get_text_tag(base_site_url, None, - "http://foo.com") + "http://foo.com/") + if not context['BASE_URL'].endswith('/'): + context['BASE_URL'] += '/' context['SITE_URL'] = context['BASE_URL'] - context['THEME'] = 'bootstrap3' author = channel.find('{{{0}}}author'.format(wordpress_namespace)) context['BLOG_EMAIL'] = get_text_tag( @@ -253,7 +283,7 @@ class CommandImportWordpress(Command, ImportMixin): + list(path.split('/')))) dst_dir = os.path.dirname(dst_path) utils.makedirs(dst_dir) - LOGGER.notice("Downloading {0} => {1}".format(url, dst_path)) + LOGGER.info("Downloading {0} => {1}".format(url, dst_path)) self.download_url_content_to_file(url, dst_path) dst_url = '/'.join(dst_path.split(os.sep)[2:]) links[link] = '/' + dst_url @@ -288,7 +318,7 @@ class CommandImportWordpress(Command, ImportMixin): # your blogging into another site or system its not. # Why don't they just use JSON? if sys.version_info[0] == 2: - metadata = phpserialize.loads(meta_value.text) + metadata = phpserialize.loads(utils.sys_encode(meta_value.text)) size_key = 'sizes' file_key = 'file' else: @@ -307,7 +337,7 @@ class CommandImportWordpress(Command, ImportMixin): + list(path.split('/')))) dst_dir = os.path.dirname(dst_path) utils.makedirs(dst_dir) - LOGGER.notice("Downloading {0} => {1}".format(url, dst_path)) + LOGGER.info("Downloading {0} => {1}".format(url, dst_path)) self.download_url_content_to_file(url, dst_path) dst_url = '/'.join(dst_path.split(os.sep)[2:]) links[url] = '/' + dst_url @@ -350,14 +380,17 @@ class CommandImportWordpress(Command, ImportMixin): # link is something like http://foo.com/2012/09/01/hello-world/ # So, take the path, utils.slugify it, and that's our slug link = get_text_tag(item, 'link', None) - path = unquote(urlparse(link).path) + path = unquote(urlparse(link).path.strip('/')) # In python 2, path is a str. slug requires a unicode # object. According to wikipedia, unquoted strings will # usually be UTF8 if isinstance(path, utils.bytes_str): path = path.decode('utf8') - slug = utils.slugify(path) + pathlist = path.split('/') + if len(pathlist) > 1: + out_folder = os.path.join(*([out_folder] + pathlist[:-1])) + slug = utils.slugify(pathlist[-1]) if not slug: # it happens if the post has no "nice" URL slug = get_text_tag( item, '{{{0}}}post_name'.format(wordpress_namespace), None) @@ -395,21 +428,43 @@ class CommandImportWordpress(Command, ImportMixin): continue tags.append(text) + if '$latex' in content: + tags.append('mathjax') + if is_draft and self.exclude_drafts: LOGGER.notice('Draft "{0}" will not be imported.'.format(title)) elif content.strip(): # If no content is found, no files are written. - self.url_map[link] = self.context['SITE_URL'] + '/' + \ - out_folder + '/' + slug + '.html' - - content = self.transform_content(content) - - self.write_metadata(os.path.join(self.output_folder, out_folder, - slug + '.meta'), - title, slug, post_date, description, tags) - self.write_content( - os.path.join(self.output_folder, out_folder, slug + '.wp'), - content) + self.url_map[link] = (self.context['SITE_URL'] + out_folder + '/' + + slug + '.html') + if hasattr(self, "separate_qtranslate_content") \ + and self.separate_qtranslate_content: + content_translations = separate_qtranslate_content(content) + else: + content_translations = {"": content} + default_language = self.context["DEFAULT_LANG"] + for lang, content in content_translations.items(): + if lang: + out_meta_filename = slug + '.meta' + if lang == default_language: + out_content_filename = slug + '.wp' + else: + out_content_filename \ + = utils.get_translation_candidate(self.context, + slug + ".wp", lang) + meta_slug = slug + else: + out_meta_filename = slug + '.meta' + out_content_filename = slug + '.wp' + meta_slug = slug + content = self.transform_content(content) + self.write_metadata(os.path.join(self.output_folder, out_folder, + out_meta_filename), + title, meta_slug, post_date, description, tags) + self.write_content( + os.path.join(self.output_folder, + out_folder, out_content_filename), + content) else: LOGGER.warn('Not going to import "{0}" because it seems to contain' ' no content.'.format(title)) @@ -441,3 +496,47 @@ def get_text_tag(tag, name, default): return t.text else: return default + + +def separate_qtranslate_content(text): + """Parse the content of a wordpress post or page and separate + the various language specific contents when they are delimited + with qtranslate tags: <!--:LL-->blabla<!--:-->""" + # TODO: uniformize qtranslate tags <!--/en--> => <!--:--> + qt_start = "<!--:" + qt_end = "-->" + qt_end_with_lang_len = 5 + qt_chunks = text.split(qt_start) + content_by_lang = {} + common_txt_list = [] + for c in qt_chunks: + if not c.strip(): + continue + if c.startswith(qt_end): + # just after the end of a language specific section, there may + # be some piece of common text or tags, or just nothing + lang = "" # default language + c = c.lstrip(qt_end) + if not c: + continue + elif c[2:].startswith(qt_end): + # a language specific section (with language code at the begining) + lang = c[:2] + c = c[qt_end_with_lang_len:] + else: + # nowhere specific (maybe there is no language section in the + # currently parsed content) + lang = "" # default language + if not lang: + common_txt_list.append(c) + for l in content_by_lang.keys(): + content_by_lang[l].append(c) + else: + content_by_lang[lang] = content_by_lang.get(lang, common_txt_list) + [c] + # in case there was no language specific section, just add the text + if common_txt_list and not content_by_lang: + content_by_lang[""] = common_txt_list + # Format back the list to simple text + for l in content_by_lang.keys(): + content_by_lang[l] = " ".join(content_by_lang[l]) + return content_by_lang diff --git a/nikola/plugins/command/init.py b/nikola/plugins/command/init.py index 96caad8..d7eeed7 100644 --- a/nikola/plugins/command/init.py +++ b/nikola/plugins/command/init.py @@ -28,18 +28,71 @@ from __future__ import print_function import os import shutil import codecs +import json from mako.template import Template import nikola +from nikola.nikola import DEFAULT_TRANSLATIONS_PATTERN from nikola.plugin_categories import Command from nikola.utils import get_logger, makedirs, STDERR_HANDLER from nikola.winutils import fix_git_symlinked LOGGER = get_logger('init', STDERR_HANDLER) +SAMPLE_CONF = { + 'BLOG_AUTHOR': "Your Name", + 'BLOG_TITLE': "Demo Site", + 'SITE_URL': "http://getnikola.com/", + 'BLOG_EMAIL': "joe@demo.site", + 'BLOG_DESCRIPTION': "This is a demo site for Nikola.", + 'DEFAULT_LANG': "en", + 'THEME': 'bootstrap3', + 'COMMENT_SYSTEM': 'disqus', + 'COMMENT_SYSTEM_ID': 'nikolademo', + 'TRANSLATIONS_PATTERN': DEFAULT_TRANSLATIONS_PATTERN, + 'POSTS': """( +("posts/*.rst", "posts", "post.tmpl"), +("posts/*.txt", "posts", "post.tmpl"), +)""", + 'PAGES': """( +("stories/*.rst", "stories", "story.tmpl"), +("stories/*.txt", "stories", "story.tmpl"), +)""", + 'COMPILERS': """{ +"rest": ('.rst', '.txt'), +"markdown": ('.md', '.mdown', '.markdown'), +"textile": ('.textile',), +"txt2tags": ('.t2t',), +"bbcode": ('.bb',), +"wiki": ('.wiki',), +"ipynb": ('.ipynb',), +"html": ('.html', '.htm'), +# PHP files are rendered the usual way (i.e. with the full templates). +# The resulting files have .php extensions, making it possible to run +# them without reconfiguring your server to recognize them. +"php": ('.php',), +# Pandoc detects the input from the source filename +# but is disabled by default as it would conflict +# with many of the others. +# "pandoc": ('.rst', '.md', '.txt'), +}""", + 'REDIRECTIONS': [], +} + + +# In order to ensure proper escaping, all variables but the three +# pre-formatted ones are handled by json.dumps(). +def prepare_config(config): + """Parse sample config with JSON.""" + p = config.copy() + p.update(dict((k, json.dumps(v)) for k, v in p.items() + if k not in ('POSTS', 'PAGES', 'COMPILERS'))) + return p + class CommandInit(Command): + """Create a new site.""" name = "init" @@ -57,40 +110,6 @@ class CommandInit(Command): } ] - SAMPLE_CONF = { - 'BLOG_AUTHOR': "Your Name", - 'BLOG_TITLE': "Demo Site", - 'SITE_URL': "http://getnikola.com/", - 'BLOG_EMAIL': "joe@demo.site", - 'BLOG_DESCRIPTION': "This is a demo site for Nikola.", - 'DEFAULT_LANG': "en", - 'THEME': 'bootstrap3', - - 'POSTS': """( - ("posts/*.rst", "posts", "post.tmpl"), - ("posts/*.txt", "posts", "post.tmpl"), -)""", - 'PAGES': """( - ("stories/*.rst", "stories", "story.tmpl"), - ("stories/*.txt", "stories", "story.tmpl"), -)""", - 'COMPILERS': """{ - "rest": ('.rst', '.txt'), - "markdown": ('.md', '.mdown', '.markdown'), - "textile": ('.textile',), - "txt2tags": ('.t2t',), - "bbcode": ('.bb',), - "wiki": ('.wiki',), - "ipynb": ('.ipynb',), - "html": ('.html', '.htm'), - # Pandoc detects the input from the source filename - # but is disabled by default as it would conflict - # with many of the others. - # "pandoc": ('.rst', '.md', '.txt'), -}""", - 'REDIRECTIONS': '[]', - } - @classmethod def copy_sample_site(cls, target): lib_path = cls.get_path_to_nikola_modules() @@ -105,7 +124,7 @@ class CommandInit(Command): conf_template = Template(filename=template_path) conf_path = os.path.join(target, 'conf.py') with codecs.open(conf_path, 'w+', 'utf8') as fd: - fd.write(conf_template.render(**cls.SAMPLE_CONF)) + fd.write(conf_template.render(**prepare_config(SAMPLE_CONF))) @classmethod def create_empty_site(cls, target): @@ -122,16 +141,13 @@ class CommandInit(Command): print("Usage: nikola init folder [options]") return False target = args[0] - if target is None: - print(self.usage) + if not options or not options.get('demo'): + self.create_empty_site(target) + LOGGER.info('Created empty site at {0}.'.format(target)) else: - if not options or not options.get('demo'): - self.create_empty_site(target) - LOGGER.notice('Created empty site at {0}.'.format(target)) - else: - self.copy_sample_site(target) - LOGGER.notice("A new site with example data has been created at " - "{0}.".format(target)) - LOGGER.notice("See README.txt in that folder for more information.") - - self.create_configuration(target) + self.copy_sample_site(target) + LOGGER.info("A new site with example data has been created at " + "{0}.".format(target)) + LOGGER.info("See README.txt in that folder for more information.") + + self.create_configuration(target) diff --git a/nikola/plugins/command/install_plugin.py b/nikola/plugins/command/install_plugin.py index 1d6584d..34223c0 100644 --- a/nikola/plugins/command/install_plugin.py +++ b/nikola/plugins/command/install_plugin.py @@ -27,7 +27,6 @@ from __future__ import print_function import codecs import os -import sys import json import shutil import subprocess @@ -123,10 +122,10 @@ class CommandInstallPlugin(Command): def do_install(self, name, data): if name in data: utils.makedirs(self.output_dir) - LOGGER.notice('Downloading: ' + data[name]) + LOGGER.info('Downloading: ' + data[name]) zip_file = BytesIO() zip_file.write(requests.get(data[name]).content) - LOGGER.notice('Extracting: {0} into plugins'.format(name)) + LOGGER.info('Extracting: {0} into plugins'.format(name)) utils.extract_all(zip_file, 'plugins') dest_path = os.path.join('plugins', name) else: @@ -142,13 +141,13 @@ class CommandInstallPlugin(Command): LOGGER.error("{0} is already installed".format(name)) return False - LOGGER.notice('Copying {0} into plugins'.format(plugin_path)) + LOGGER.info('Copying {0} into plugins'.format(plugin_path)) shutil.copytree(plugin_path, dest_path) reqpath = os.path.join(dest_path, 'requirements.txt') if os.path.exists(reqpath): LOGGER.notice('This plugin has Python dependencies.') - LOGGER.notice('Installing dependencies with pip...') + LOGGER.info('Installing dependencies with pip...') try: subprocess.check_call(('pip', 'install', '-r', reqpath)) except subprocess.CalledProcessError: @@ -159,7 +158,7 @@ class CommandInstallPlugin(Command): print('You have to install those yourself or through a ' 'package manager.') else: - LOGGER.notice('Dependency installation succeeded.') + LOGGER.info('Dependency installation succeeded.') reqnpypath = os.path.join(dest_path, 'requirements-nonpy.txt') if os.path.exists(reqnpypath): LOGGER.notice('This plugin has third-party ' @@ -177,10 +176,10 @@ class CommandInstallPlugin(Command): 'manager.') confpypath = os.path.join(dest_path, 'conf.py.sample') if os.path.exists(confpypath): - LOGGER.notice('This plugin has a sample config file.') + LOGGER.notice('This plugin has a sample config file. Integrate it with yours in order to make this plugin work!') print('Contents of the conf.py.sample file:\n') with codecs.open(confpypath, 'rb', 'utf-8') as fh: - if sys.platform == 'win32': + if self.site.colorful: print(indent(pygments.highlight( fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' ')) diff --git a/nikola/plugins/command/install_theme.py b/nikola/plugins/command/install_theme.py index 569397b..47c73b4 100644 --- a/nikola/plugins/command/install_theme.py +++ b/nikola/plugins/command/install_theme.py @@ -26,7 +26,6 @@ from __future__ import print_function import os -import sys import codecs import json import shutil @@ -137,10 +136,10 @@ class CommandInstallTheme(Command): def do_install(self, name, data): if name in data: utils.makedirs(self.output_dir) - LOGGER.notice('Downloading: ' + data[name]) + LOGGER.info('Downloading: ' + data[name]) zip_file = BytesIO() zip_file.write(requests.get(data[name]).content) - LOGGER.notice('Extracting: {0} into themes'.format(name)) + LOGGER.info('Extracting: {0} into themes'.format(name)) utils.extract_all(zip_file) dest_path = os.path.join('themes', name) else: @@ -156,14 +155,14 @@ class CommandInstallTheme(Command): LOGGER.error("{0} is already installed".format(name)) return False - LOGGER.notice('Copying {0} into themes'.format(theme_path)) + LOGGER.info('Copying {0} into themes'.format(theme_path)) shutil.copytree(theme_path, dest_path) confpypath = os.path.join(dest_path, 'conf.py.sample') if os.path.exists(confpypath): - LOGGER.notice('This plugin has a sample config file. Integrate it with yours in order to make this theme work!') + LOGGER.notice('This theme has a sample config file. Integrate it with yours in order to make this theme work!') print('Contents of the conf.py.sample file:\n') with codecs.open(confpypath, 'rb', 'utf-8') as fh: - if sys.platform == 'win32': + if self.site.colorful: print(indent(pygments.highlight( fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' ')) diff --git a/nikola/plugins/command/new_page.plugin b/nikola/plugins/command/new_page.plugin new file mode 100644 index 0000000..1f1c84c --- /dev/null +++ b/nikola/plugins/command/new_page.plugin @@ -0,0 +1,9 @@ +[Core] +Name = new_page +Module = new_page + +[Documentation] +Author = Roberto Alsina, Chris Warrick +Version = 0.1 +Website = http://getnikola.com +Description = Create a new page. diff --git a/nikola/plugins/command/new_page.py b/nikola/plugins/command/new_page.py new file mode 100644 index 0000000..39c0c1d --- /dev/null +++ b/nikola/plugins/command/new_page.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2014 Roberto Alsina, Chris Warrick and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from __future__ import unicode_literals, print_function + +from nikola.plugin_categories import Command + + +class CommandNewPage(Command): + """Create a new page.""" + + name = "new_page" + doc_usage = "[options] [path]" + doc_purpose = "create a new page in the site" + cmd_options = [ + { + 'name': 'title', + 'short': 't', + 'long': 'title', + 'type': str, + 'default': '', + 'help': 'Title for the page.' + }, + { + 'name': 'onefile', + 'short': '1', + 'type': bool, + 'default': False, + 'help': 'Create the page with embedded metadata (single file format)' + }, + { + 'name': 'twofile', + 'short': '2', + 'type': bool, + 'default': False, + 'help': 'Create the page with separate metadata (two file format)' + }, + { + 'name': 'content_format', + 'short': 'f', + 'long': 'format', + 'type': str, + 'default': '', + 'help': 'Markup format for the page, one of rest, markdown, wiki, ' + 'bbcode, html, textile, txt2tags', + }, + ] + + def _execute(self, options, args): + """Create a new page.""" + options['tags'] = '' + options['schedule'] = False + options['is_page'] = True + # Even though stuff was split into `new_page`, it’s easier to do it + # there not to duplicate the code. + p = self.site.plugin_manager.getPluginByName('new_post', 'Command').plugin_object + return p.execute(options, args) diff --git a/nikola/plugins/command/new_post.py b/nikola/plugins/command/new_post.py index a5c551d..cd37a75 100644 --- a/nikola/plugins/command/new_post.py +++ b/nikola/plugins/command/new_post.py @@ -35,7 +35,9 @@ from blinker import signal from nikola.plugin_categories import Command from nikola import utils -LOGGER = utils.get_logger('new_post', utils.STDERR_HANDLER) +POSTLOGGER = utils.get_logger('new_post', utils.STDERR_HANDLER) +PAGELOGGER = utils.get_logger('new_page', utils.STDERR_HANDLER) +LOGGER = POSTLOGGER def filter_post_pages(compiler, is_post, compilers, post_pages): @@ -57,8 +59,8 @@ def filter_post_pages(compiler, is_post, compilers, post_pages): type_name = "post" if is_post else "page" raise Exception("Can't find a way, using your configuration, to create " "a {0} in format {1}. You may want to tweak " - "COMPILERS or POSTS/PAGES in conf.py".format( - type_name, compiler)) + "COMPILERS or {2}S in conf.py".format( + type_name, compiler, type_name.upper())) return filtered[0] @@ -134,7 +136,7 @@ class CommandNewPost(Command): 'long': 'page', 'type': bool, 'default': False, - 'help': 'Create a page instead of a blog post.' + 'help': 'Create a page instead of a blog post. (see also: `nikola new_page`)' }, { 'name': 'title', @@ -142,36 +144,36 @@ class CommandNewPost(Command): 'long': 'title', 'type': str, 'default': '', - 'help': 'Title for the page/post.' + 'help': 'Title for the post.' }, { 'name': 'tags', 'long': 'tags', 'type': str, 'default': '', - 'help': 'Comma-separated tags for the page/post.' + 'help': 'Comma-separated tags for the post.' }, { 'name': 'onefile', 'short': '1', 'type': bool, 'default': False, - 'help': 'Create post with embedded metadata (single file format)' + 'help': 'Create the post with embedded metadata (single file format)' }, { 'name': 'twofile', 'short': '2', 'type': bool, 'default': False, - 'help': 'Create post with separate metadata (two file format)' + 'help': 'Create the post with separate metadata (two file format)' }, { - 'name': 'post_format', + 'name': 'content_format', 'short': 'f', 'long': 'format', 'type': str, 'default': '', - 'help': 'Markup format for post, one of rest, markdown, wiki, ' + 'help': 'Markup format for the post, one of rest, markdown, wiki, ' 'bbcode, html, textile, txt2tags', }, { @@ -179,13 +181,14 @@ class CommandNewPost(Command): 'short': 's', 'type': bool, 'default': False, - 'help': 'Schedule post based on recurrence rule' + 'help': 'Schedule the post based on recurrence rule' }, ] def _execute(self, options, args): """Create a new post or page.""" + global LOGGER compiler_names = [p.name for p in self.site.plugin_manager.getPluginsOfCategory( "PageCompiler")] @@ -198,38 +201,46 @@ class CommandNewPost(Command): else: path = None + # Even though stuff was split into `new_page`, it’s easier to do it + # here not to duplicate the code. is_page = options.get('is_page', False) is_post = not is_page + content_type = 'page' if is_page else 'post' title = options['title'] or None tags = options['tags'] onefile = options['onefile'] twofile = options['twofile'] + if is_page: + LOGGER = PAGELOGGER + else: + LOGGER = POSTLOGGER + if twofile: onefile = False if not onefile and not twofile: onefile = self.site.config.get('ONE_FILE_POSTS', True) - post_format = options['post_format'] + content_format = options['content_format'] - if not post_format: # Issue #400 - post_format = get_default_compiler( + if not content_format: # Issue #400 + content_format = get_default_compiler( is_post, self.site.config['COMPILERS'], self.site.config['post_pages']) - if post_format not in compiler_names: - LOGGER.error("Unknown post format " + post_format) + if content_format not in compiler_names: + LOGGER.error("Unknown {0} format {1}".format(content_type, content_format)) return compiler_plugin = self.site.plugin_manager.getPluginByName( - post_format, "PageCompiler").plugin_object + content_format, "PageCompiler").plugin_object # Guess where we should put this - entry = filter_post_pages(post_format, is_post, + entry = filter_post_pages(content_format, is_post, self.site.config['COMPILERS'], self.site.config['post_pages']) - print("Creating New Post") + print("Creating New {0}".format(content_type.title())) print("-----------------\n") if title is None: print("Enter title: ", end='') @@ -247,7 +258,7 @@ class CommandNewPost(Command): if isinstance(path, utils.bytes_str): path = path.decode(sys.stdin.encoding) slug = utils.slugify(os.path.splitext(os.path.basename(path))[0]) - # Calculate the date to use for the post + # Calculate the date to use for the content schedule = options['schedule'] or self.site.config['SCHEDULE_ALL'] rule = self.site.config['SCHEDULE_RULE'] force_today = self.site.config['SCHEDULE_FORCE_TODAY'] @@ -275,7 +286,7 @@ class CommandNewPost(Command): metadata = self.site.config['ADDITIONAL_METADATA'] compiler_plugin.create_post( txt_path, onefile, title=title, - slug=slug, date=date, tags=tags, **metadata) + slug=slug, date=date, tags=tags, is_page=is_page, **metadata) event = dict(path=txt_path) @@ -283,9 +294,9 @@ class CommandNewPost(Command): with codecs.open(meta_path, "wb+", "utf8") as fd: fd.write('\n'.join(data)) with codecs.open(txt_path, "wb+", "utf8") as fd: - fd.write("Write your post here.") - LOGGER.notice("Your post's metadata is at: {0}".format(meta_path)) + fd.write("Write your {0} here.".format(content_type)) + LOGGER.info("Your {0}'s metadata is at: {1}".format(content_type, meta_path)) event['meta_path'] = meta_path - LOGGER.notice("Your post's text is at: {0}".format(txt_path)) + LOGGER.info("Your {0}'s text is at: {1}".format(content_type, txt_path)) - signal('new_post').send(self, **event) + signal('new_' + content_type).send(self, **event) diff --git a/nikola/plugins/command/planetoid/__init__.py b/nikola/plugins/command/planetoid/__init__.py index ff5dd13..fe1a59b 100644 --- a/nikola/plugins/command/planetoid/__init__.py +++ b/nikola/plugins/command/planetoid/__init__.py @@ -162,12 +162,12 @@ class Planetoid(Command, Task): # TODO: log failure return if parsed.feed.get('title'): - LOGGER.notice(parsed.feed.title) + LOGGER.info(parsed.feed.title) else: - LOGGER.notice(feed.url) + LOGGER.info(feed.url) feed.etag = parsed.get('etag', 'foo') modified = tuple(parsed.get('date_parsed', (1970, 1, 1)))[:6] - LOGGER.notice("==========>", modified) + LOGGER.info("==========>", modified) modified = datetime.datetime(*modified) feed.last_modified = modified feed.save() @@ -176,14 +176,14 @@ class Planetoid(Command, Task): # TODO log failure return for entry_data in parsed.entries: - LOGGER.notice("=========================================") + LOGGER.info("=========================================") date = entry_data.get('published_parsed', None) if date is None: date = entry_data.get('updated_parsed', None) if date is None: LOGGER.error("Can't parse date from:\n", entry_data) return False - LOGGER.notice("DATE:===>", date) + LOGGER.info("DATE:===>", date) date = datetime.datetime(*(date[:6])) title = "%s: %s" % (feed.name, entry_data.get('title', 'Sin título')) content = entry_data.get('content', None) @@ -195,9 +195,9 @@ class Planetoid(Command, Task): content = entry_data.get('summary', 'Sin contenido') guid = str(entry_data.get('guid', entry_data.link)) link = entry_data.link - LOGGER.notice(repr([date, title])) + LOGGER.info(repr([date, title])) e = list(Entry.select().where(Entry.guid == guid)) - LOGGER.notice( + LOGGER.info( repr(dict( date=date, title=title, diff --git a/nikola/plugins/command/serve.py b/nikola/plugins/command/serve.py index 2dd15c1..f27d1f7 100644 --- a/nikola/plugins/command/serve.py +++ b/nikola/plugins/command/serve.py @@ -26,6 +26,7 @@ from __future__ import print_function import os +import webbrowser try: from BaseHTTPServer import HTTPServer from SimpleHTTPServer import SimpleHTTPRequestHandler @@ -37,7 +38,7 @@ from nikola.plugin_categories import Command from nikola.utils import get_logger -class CommandBuild(Command): +class CommandServe(Command): """Start test server.""" name = "serve" @@ -57,11 +58,19 @@ class CommandBuild(Command): { 'name': 'address', 'short': 'a', - 'long': '--address', + 'long': 'address', 'type': str, 'default': '127.0.0.1', 'help': 'Address to bind (default: 127.0.0.1)', }, + { + 'name': 'browser', + 'short': 'b', + 'long': 'browser', + 'type': bool, + 'default': False, + 'help': 'Open the test server in a web browser', + } ) def _execute(self, options, args): @@ -75,7 +84,11 @@ class CommandBuild(Command): httpd = HTTPServer((options['address'], options['port']), OurHTTPRequestHandler) sa = httpd.socket.getsockname() - self.logger.notice("Serving HTTP on {0} port {1} ...".format(*sa)) + self.logger.info("Serving HTTP on {0} port {1} ...".format(*sa)) + if options['browser']: + server_url = "http://{0}:{1}/".format(options['address'], options['port']) + self.logger.info("Opening {0} in the default web browser ...".format(server_url)) + webbrowser.open(server_url) httpd.serve_forever() diff --git a/nikola/plugins/compile/asciidoc.py b/nikola/plugins/compile/asciidoc.py index 12cb4bf..68f96d9 100644 --- a/nikola/plugins/compile/asciidoc.py +++ b/nikola/plugins/compile/asciidoc.py @@ -40,7 +40,7 @@ from nikola.utils import makedirs, req_missing try: from collections import OrderedDict except ImportError: - OrderedDict = None # NOQA + OrderedDict = dict # NOQA class CompileAsciiDoc(PageCompiler): @@ -57,11 +57,8 @@ class CompileAsciiDoc(PageCompiler): if e.strreror == 'No such file or directory': req_missing(['asciidoc'], 'build this site (compile with asciidoc)', python=False) - def create_post(self, path, onefile=False, **kw): - if OrderedDict is not None: - metadata = OrderedDict() - else: - metadata = {} + def create_post(self, path, onefile=False, is_page=False, **kw): + metadata = OrderedDict() metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) @@ -71,4 +68,4 @@ class CompileAsciiDoc(PageCompiler): for k, v in metadata.items(): fd.write('.. {0}: {1}\n'.format(k, v)) fd.write("/////////////////////////////////////////////\n") - fd.write("\nWrite your post here.") + fd.write("\nWrite your {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/compile/bbcode.py b/nikola/plugins/compile/bbcode.py index 5345be3..0961ffe 100644 --- a/nikola/plugins/compile/bbcode.py +++ b/nikola/plugins/compile/bbcode.py @@ -40,7 +40,7 @@ from nikola.utils import makedirs, req_missing try: from collections import OrderedDict except ImportError: - OrderedDict = None # NOQA + OrderedDict = dict # NOQA class CompileBbcode(PageCompiler): @@ -66,11 +66,8 @@ class CompileBbcode(PageCompiler): output = self.parser.format(data) out_file.write(output) - def create_post(self, path, onefile=False, **kw): - if OrderedDict is not None: - metadata = OrderedDict() - else: - metadata = {} + def create_post(self, path, onefile=False, is_page=False, **kw): + metadata = OrderedDict() metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) @@ -80,4 +77,4 @@ class CompileBbcode(PageCompiler): for k, v in metadata.items(): fd.write('.. {0}: {1}\n'.format(k, v)) fd.write('-->[/note]\n\n') - fd.write("Write your post here.") + fd.write("Write your {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/compile/html.py b/nikola/plugins/compile/html.py index 5352f00..09a9756 100644 --- a/nikola/plugins/compile/html.py +++ b/nikola/plugins/compile/html.py @@ -36,7 +36,7 @@ from nikola.utils import makedirs try: from collections import OrderedDict except ImportError: - OrderedDict = None # NOQA + OrderedDict = dict # NOQA _META_SEPARATOR = '(' + os.linesep * 2 + '|' + ('\n' * 2) + '|' + ("\r\n" * 2) + ')' @@ -56,11 +56,8 @@ class CompileHtml(PageCompiler): out_file.write(data) return True - def create_post(self, path, onefile=False, **kw): - if OrderedDict is not None: - metadata = OrderedDict() - else: - metadata = {} + def create_post(self, path, onefile=False, is_page=False, **kw): + metadata = OrderedDict() metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) @@ -70,4 +67,4 @@ class CompileHtml(PageCompiler): for k, v in metadata.items(): fd.write('.. {0}: {1}\n'.format(k, v)) fd.write('-->\n\n') - fd.write("\n<p>Write your post here.</p>\n") + fd.write("\n<p>Write your {0} here.</p>\n".format('page' if is_page else 'post')) diff --git a/nikola/plugins/compile/ipynb/__init__.py b/nikola/plugins/compile/ipynb/__init__.py index 5f2f0b3..2b1fd28 100644 --- a/nikola/plugins/compile/ipynb/__init__.py +++ b/nikola/plugins/compile/ipynb/__init__.py @@ -44,7 +44,7 @@ from nikola.utils import makedirs, req_missing try: from collections import OrderedDict except ImportError: - OrderedDict = None # NOQA + OrderedDict = dict # NOQA class CompileIPynb(PageCompiler): @@ -66,11 +66,8 @@ class CompileIPynb(PageCompiler): (body, resources) = exportHtml.from_notebook_node(nb_json) out_file.write(body) - def create_post(self, path, onefile=False, **kw): - if OrderedDict is not None: - metadata = OrderedDict() - else: - metadata = {} + def create_post(self, path, onefile=False, is_page=False, **kw): + metadata = OrderedDict() metadata.update(self.default_metadata) metadata.update(kw) d_name = os.path.dirname(path) @@ -81,7 +78,7 @@ class CompileIPynb(PageCompiler): metadata['date'], metadata['tags'], metadata['link'], metadata['description'], metadata['type']))) - print("Your post's metadata is at: ", meta_path) + print("Your {0}'s metadata is at: {1}".format('page' if is_page else 'post', meta_path)) with codecs.open(path, "wb+", "utf8") as fd: fd.write("""{ "metadata": { diff --git a/nikola/plugins/compile/markdown/__init__.py b/nikola/plugins/compile/markdown/__init__.py index 1376b11..d0fa66a 100644 --- a/nikola/plugins/compile/markdown/__init__.py +++ b/nikola/plugins/compile/markdown/__init__.py @@ -54,7 +54,7 @@ except ImportError: try: from collections import OrderedDict except ImportError: - OrderedDict = None # NOQA + OrderedDict = dict # NOQA from nikola.plugin_categories import PageCompiler from nikola.utils import makedirs, req_missing @@ -81,11 +81,8 @@ class CompileMarkdown(PageCompiler): output = markdown(data, self.extensions) out_file.write(output) - def create_post(self, path, onefile=False, **kw): - if OrderedDict is not None: - metadata = OrderedDict() - else: - metadata = {} + def create_post(self, path, onefile=False, is_page=False, **kw): + metadata = OrderedDict() metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) @@ -95,4 +92,4 @@ class CompileMarkdown(PageCompiler): for k, v in metadata.items(): fd.write('.. {0}: {1}\n'.format(k, v)) fd.write('-->\n\n') - fd.write("Write your post here.") + fd.write("Write your {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/compile/misaka.py b/nikola/plugins/compile/misaka.py index 8777ffc..4951c9f 100644 --- a/nikola/plugins/compile/misaka.py +++ b/nikola/plugins/compile/misaka.py @@ -40,7 +40,7 @@ except ImportError: try: from collections import OrderedDict except ImportError: - OrderedDict = None # NOQA + OrderedDict = dict # NOQA gist_extension = None podcast_extension = None @@ -73,11 +73,8 @@ class CompileMisaka(PageCompiler): output = misaka.html(data, extensions=self.ext) out_file.write(output) - def create_post(self, path, onefile=False, **kw): - if OrderedDict is not None: - metadata = OrderedDict() - else: - metadata = {} + def create_post(self, path, onefile=False, is_page=False, **kw): + metadata = OrderedDict() metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) @@ -87,4 +84,4 @@ class CompileMisaka(PageCompiler): for k, v in metadata.items(): fd.write('.. {0}: {1}\n'.format(k, v)) fd.write('-->\n\n') - fd.write("\nWrite your post here.") + fd.write("\nWrite your {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/compile/pandoc.py b/nikola/plugins/compile/pandoc.py index 57c7d71..654c7c8 100644 --- a/nikola/plugins/compile/pandoc.py +++ b/nikola/plugins/compile/pandoc.py @@ -40,7 +40,7 @@ from nikola.utils import req_missing, makedirs try: from collections import OrderedDict except ImportError: - OrderedDict = None # NOQA + OrderedDict = dict # NOQA class CompilePandoc(PageCompiler): @@ -56,11 +56,8 @@ class CompilePandoc(PageCompiler): if e.strreror == 'No such file or directory': req_missing(['pandoc'], 'build this site (compile with pandoc)', python=False) - def create_post(self, path, onefile=False, **kw): - if OrderedDict is not None: - metadata = OrderedDict() - else: - metadata = {} + def create_post(self, path, onefile=False, is_page=False, **kw): + metadata = OrderedDict() metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) @@ -70,4 +67,4 @@ class CompilePandoc(PageCompiler): for k, v in metadata.items(): fd.write('.. {0}: {1}\n'.format(k, v)) fd.write('-->\n\n') - fd.write("Write your post here.") + fd.write("Write your {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/compile/php.py b/nikola/plugins/compile/php.py index 14b80e8..0a652a6 100644 --- a/nikola/plugins/compile/php.py +++ b/nikola/plugins/compile/php.py @@ -38,7 +38,7 @@ from nikola.utils import makedirs try: from collections import OrderedDict except ImportError: - OrderedDict = None # NOQA + OrderedDict = dict # NOQA class CompilePhp(PageCompiler): @@ -50,11 +50,8 @@ class CompilePhp(PageCompiler): makedirs(os.path.dirname(dest)) shutil.copyfile(source, dest) - def create_post(self, path, onefile=False, **kw): - if OrderedDict is not None: - metadata = OrderedDict() - else: - metadata = {} + def create_post(self, path, onefile=False, is_page=False, **kw): + metadata = OrderedDict() metadata.update(self.default_metadata) metadata.update(kw) os.makedirs(os.path.dirname(path)) @@ -64,7 +61,7 @@ class CompilePhp(PageCompiler): for k, v in metadata.items(): fd.write('.. {0}: {1}\n'.format(k, v)) fd.write('-->\n\n') - fd.write("\n<p>Write your post here.</p>") + fd.write("\n<p>Write your {0} here.</p>".format('page' if is_page else 'post')) def extension(self): return ".php" diff --git a/nikola/plugins/compile/rest/__init__.py b/nikola/plugins/compile/rest/__init__.py index 50b37cf..9a4e19b 100644 --- a/nikola/plugins/compile/rest/__init__.py +++ b/nikola/plugins/compile/rest/__init__.py @@ -43,7 +43,7 @@ except ImportError: try: from collections import OrderedDict except ImportError: - OrderedDict = None # NOQA + OrderedDict = dict # NOQA from nikola.plugin_categories import PageCompiler from nikola.utils import get_logger, makedirs, req_missing @@ -102,11 +102,8 @@ class CompileRest(PageCompiler): else: return False - def create_post(self, path, onefile=False, **kw): - if OrderedDict is not None: - metadata = OrderedDict() - else: - metadata = {} + def create_post(self, path, onefile=False, is_page=False, **kw): + metadata = OrderedDict() metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) @@ -114,7 +111,7 @@ class CompileRest(PageCompiler): if onefile: for k, v in metadata.items(): fd.write('.. {0}: {1}\n'.format(k, v)) - fd.write("\nWrite your post here.") + fd.write("\nWrite your {0} here.".format('page' if is_page else 'post')) def set_site(self, site): for plugin_info in site.plugin_manager.getPluginsOfCategory("RestExtension"): @@ -174,6 +171,38 @@ class NikolaReader(docutils.readers.standalone.Reader): def add_node(node, visit_function=None, depart_function=None): + """ + Register a Docutils node class. + This function is completely optional. It is a same concept as + `Sphinx add_node function <http://sphinx-doc.org/ext/appapi.html#sphinx.application.Sphinx.add_node>`_. + + For example:: + + class Plugin(RestExtension): + + name = "rest_math" + + def set_site(self, site): + self.site = site + directives.register_directive('math', MathDirective) + add_node(MathBlock, visit_Math, depart_Math) + return super(Plugin, self).set_site(site) + + class MathDirective(Directive): + def run(self): + node = MathBlock() + return [node] + + class Math(docutils.nodes.Element): pass + + def visit_Math(self, node): + self.body.append(self.starttag(node, 'math')) + + def depart_Math(self, node): + self.body.append('</math>') + + For full example, you can refer to `Microdata plugin <http://plugins.getnikola.com/#microdata>`_ + """ docutils.nodes._add_node_class_names([node.__name__]) if visit_function: setattr(docutils.writers.html4css1.HTMLTranslator, 'visit_' + node.__name__, visit_function) diff --git a/nikola/plugins/compile/rest/listing.py b/nikola/plugins/compile/rest/listing.py index ecf885f..d70e02d 100644 --- a/nikola/plugins/compile/rest/listing.py +++ b/nikola/plugins/compile/rest/listing.py @@ -56,6 +56,18 @@ except ImportError: # docutils < 0.9 (Debian Sid For The Loss) from nikola.plugin_categories import RestExtension +# Add sphinx compatibility option +CodeBlock.option_spec['linenos'] = directives.unchanged + + +class FlexibleCodeBlock(CodeBlock): + + def run(self): + if 'linenos' in self.options: + self.options['number-lines'] = self.options['linenos'] + return super(FlexibleCodeBlock, self).run() +CodeBlock = FlexibleCodeBlock + class Plugin(RestExtension): @@ -71,6 +83,10 @@ class Plugin(RestExtension): directives.register_directive('listing', Listing) return super(Plugin, self).set_site(site) +# Add sphinx compatibility option +listing_spec = Include.option_spec +listing_spec['linenos'] = directives.unchanged + class Listing(Include): """ listing directive: create a highlighted block of code from a file in listings/ @@ -84,6 +100,7 @@ class Listing(Include): has_content = False required_arguments = 1 optional_arguments = 1 + option_spec = listing_spec def run(self): fname = self.arguments.pop(0) @@ -91,6 +108,8 @@ class Listing(Include): fpath = os.path.join('listings', fname) self.arguments.insert(0, fpath) self.options['code'] = lang + if 'linenos' in self.options: + self.options['number-lines'] = self.options['linenos'] with codecs_open(fpath, 'rb+', 'utf8') as fileobject: self.content = fileobject.read().splitlines() self.state.document.settings.record_dependencies.add(fpath) diff --git a/nikola/plugins/compile/textile.py b/nikola/plugins/compile/textile.py index 73f35c0..1679831 100644 --- a/nikola/plugins/compile/textile.py +++ b/nikola/plugins/compile/textile.py @@ -41,7 +41,7 @@ from nikola.utils import makedirs, req_missing try: from collections import OrderedDict except ImportError: - OrderedDict = None # NOQA + OrderedDict = dict # NOQA class CompileTextile(PageCompiler): @@ -62,11 +62,8 @@ class CompileTextile(PageCompiler): output = textile(data, head_offset=1) out_file.write(output) - def create_post(self, path, onefile=False, **kw): - if OrderedDict is not None: - metadata = OrderedDict() - else: - metadata = {} + def create_post(self, path, onefile=False, is_page=False, **kw): + metadata = OrderedDict() metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) @@ -76,4 +73,4 @@ class CompileTextile(PageCompiler): for k, v in metadata.items(): fd.write('.. {0}: {1}\n'.format(k, v)) fd.write('--></notextile>\n\n') - fd.write("\nWrite your post here.") + fd.write("\nWrite your {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/compile/txt2tags.py b/nikola/plugins/compile/txt2tags.py index 8c9724e..bb6afa5 100644 --- a/nikola/plugins/compile/txt2tags.py +++ b/nikola/plugins/compile/txt2tags.py @@ -43,7 +43,7 @@ except ImportError: try: from collections import OrderedDict except ImportError: - OrderedDict = None # NOQA + OrderedDict = dict # NOQA from nikola.plugin_categories import PageCompiler from nikola.utils import makedirs, req_missing @@ -62,11 +62,8 @@ class CompileTxt2tags(PageCompiler): cmd = ["-t", "html", "--no-headers", "--outfile", dest, source] txt2tags(cmd) - def create_post(self, path, onefile=False, **kw): - if OrderedDict is not None: - metadata = OrderedDict() - else: - metadata = {} + def create_post(self, path, onefile=False, is_page=False, **kw): + metadata = OrderedDict() metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) @@ -76,4 +73,4 @@ class CompileTxt2tags(PageCompiler): for k, v in metadata.items(): fd.write('.. {0}: {1}\n'.format(k, v)) fd.write("-->\n'''\n") - fd.write("\nWrite your post here.") + fd.write("\nWrite your {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/compile/wiki.py b/nikola/plugins/compile/wiki.py index 9a365fa..f4858c7 100644 --- a/nikola/plugins/compile/wiki.py +++ b/nikola/plugins/compile/wiki.py @@ -40,7 +40,7 @@ from nikola.plugin_categories import PageCompiler try: from collections import OrderedDict except ImportError: - OrderedDict = None # NOQA + OrderedDict = dict # NOQA from nikola.utils import makedirs, req_missing @@ -62,11 +62,8 @@ class CompileWiki(PageCompiler): output = HtmlEmitter(document).emit() out_file.write(output) - def create_post(self, path, onefile=False, **kw): - if OrderedDict is not None: - metadata = OrderedDict() - else: - metadata = {} + def create_post(self, path, onefile=False, is_page=False, **kw): + metadata = OrderedDict() metadata.update(self.default_metadata) metadata.update(kw) makedirs(os.path.dirname(path)) @@ -75,4 +72,4 @@ class CompileWiki(PageCompiler): 'one-file format is not possible, use the -2 ' 'option.') with codecs.open(path, "wb+", "utf8") as fd: - fd.write("Write your post here.") + fd.write("Write your {0} here.".format('page' if is_page else 'post')) diff --git a/nikola/plugins/loghandler/stderr.py b/nikola/plugins/loghandler/stderr.py index 75acffc..fdc892e 100644 --- a/nikola/plugins/loghandler/stderr.py +++ b/nikola/plugins/loghandler/stderr.py @@ -26,10 +26,10 @@ from nikola.plugin_categories import SignalHandler from blinker import signal -import logbook import os from nikola import DEBUG +from nikola.utils import ColorfulStderrHandler class StderrHandler(SignalHandler): @@ -40,7 +40,7 @@ class StderrHandler(SignalHandler): """Attach the handler to the logger.""" conf = self.site.config.get('LOGGING_HANDLERS').get('stderr') if conf or os.getenv('NIKOLA_DEBUG'): - self.site.loghandlers.append(logbook.StderrHandler( + self.site.loghandlers.append(ColorfulStderrHandler( level='DEBUG' if DEBUG else conf.get('loglevel', 'WARNING').upper(), format_string=u'[{record.time:%Y-%m-%dT%H:%M:%SZ}] {record.level_name}: {record.channel}: {record.message}' )) diff --git a/nikola/plugins/task/build_less.py b/nikola/plugins/task/build_less.py index 14a53f9..a672282 100644 --- a/nikola/plugins/task/build_less.py +++ b/nikola/plugins/task/build_less.py @@ -46,22 +46,34 @@ class BuildLess(Task): def gen_tasks(self): """Generate CSS out of LESS sources.""" self.compiler_name = self.site.config['LESS_COMPILER'] + self.compiler_options = self.site.config['LESS_OPTIONS'] kw = { 'cache_folder': self.site.config['CACHE_FOLDER'], 'themes': self.site.THEMES, } + tasks = {} # Find where in the theme chain we define the LESS targets # There can be many *.less in the folder, but we only will build # the ones listed in less/targets - targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES) + if os.path.isfile(os.path.join(self.sources_folder, "targets")): + targets_path = os.path.join(self.sources_folder, "targets") + else: + targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES) try: with codecs.open(targets_path, "rb", "utf-8") as inf: targets = [x.strip() for x in inf.readlines()] except Exception: targets = [] + for task in utils.copy_tree(self.sources_folder, os.path.join(kw['cache_folder'], self.sources_folder)): + if task['name'] in tasks: + continue + task['basename'] = 'prepare_less_sources' + tasks[task['name']] = task + yield task + for theme_name in kw['themes']: src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder) for task in utils.copy_tree(src, os.path.join(kw['cache_folder'], self.sources_folder)): @@ -82,7 +94,7 @@ class BuildLess(Task): src = os.path.join(kw['cache_folder'], self.sources_folder, target) run_in_shell = sys.platform == 'win32' try: - compiled = subprocess.check_output([self.compiler_name, src], shell=run_in_shell) + compiled = subprocess.check_output([self.compiler_name] + self.compiler_options + [src], shell=run_in_shell) except OSError: utils.req_missing([self.compiler_name], 'build LESS files (and use this theme)', diff --git a/nikola/plugins/task/build_sass.py b/nikola/plugins/task/build_sass.py index 7575505..becc843 100644 --- a/nikola/plugins/task/build_sass.py +++ b/nikola/plugins/task/build_sass.py @@ -47,26 +47,41 @@ class BuildSass(Task): """Generate CSS out of Sass sources.""" self.logger = utils.get_logger('build_sass', self.site.loghandlers) self.compiler_name = self.site.config['SASS_COMPILER'] + self.compiler_options = self.site.config['SASS_OPTIONS'] kw = { 'cache_folder': self.site.config['CACHE_FOLDER'], 'themes': self.site.THEMES, } + tasks = {} # Find where in the theme chain we define the Sass targets # There can be many *.sass/*.scss in the folder, but we only # will build the ones listed in sass/targets - targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES) + if os.path.isfile(os.path.join(self.sources_folder, "targets")): + targets_path = os.path.join(self.sources_folder, "targets") + else: + targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES) try: with codecs.open(targets_path, "rb", "utf-8") as inf: targets = [x.strip() for x in inf.readlines()] except Exception: targets = [] + for task in utils.copy_tree(self.sources_folder, os.path.join(kw['cache_folder'], self.sources_folder)): + if task['name'] in tasks: + continue + task['basename'] = 'prepare_sass_sources' + tasks[task['name']] = task + yield task + for theme_name in kw['themes']: src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder) for task in utils.copy_tree(src, os.path.join(kw['cache_folder'], self.sources_folder)): + if task['name'] in tasks: + continue task['basename'] = 'prepare_sass_sources' + tasks[task['name']] = task yield task # Build targets and write CSS files @@ -83,7 +98,7 @@ class BuildSass(Task): run_in_shell = sys.platform == 'win32' src = os.path.join(kw['cache_folder'], self.sources_folder, target) try: - compiled = subprocess.check_output([self.compiler_name, src], shell=run_in_shell) + compiled = subprocess.check_output([self.compiler_name] + self.compiler_options + [src], shell=run_in_shell) except OSError: utils.req_missing([self.compiler_name], 'build Sass files (and use this theme)', diff --git a/nikola/plugins/task/bundles.py b/nikola/plugins/task/bundles.py index b035b97..fcfaf42 100644 --- a/nikola/plugins/task/bundles.py +++ b/nikola/plugins/task/bundles.py @@ -87,8 +87,8 @@ class BuildBundles(LateTask): output_path = os.path.join(kw['output_folder'], name) dname = os.path.dirname(name) file_dep = [os.path.join(kw['output_folder'], dname, fname) - for fname in files] - file_dep = filter(os.path.isfile, file_dep) # removes missing files + for fname in files if + utils.get_asset_path(fname, self.site.THEMES, self.site.config['FILES_FOLDERS'])] task = { 'file_dep': list(file_dep), 'task_dep': ['copy_assets'], diff --git a/nikola/plugins/task/copy_assets.py b/nikola/plugins/task/copy_assets.py index 21f1f85..93b7fb3 100644 --- a/nikola/plugins/task/copy_assets.py +++ b/nikola/plugins/task/copy_assets.py @@ -75,8 +75,8 @@ class CopyAssets(Task): formatter = get_formatter_by_name('html', style=kw["code_color_scheme"]) utils.makedirs(os.path.dirname(code_css_path)) with codecs.open(code_css_path, 'wb+', 'utf8') as outf: - outf.write(formatter.get_style_defs('.code')) - outf.write("table.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}") + outf.write(formatter.get_style_defs(['pre.code', 'div.code pre'])) + outf.write("\ntable.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}\n") task = { 'basename': self.name, diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py index 6977eab..880d47c 100644 --- a/nikola/plugins/task/galleries.py +++ b/nikola/plugins/task/galleries.py @@ -176,6 +176,7 @@ class Galleries(Task): thumbs = ['.thumbnail'.join(os.path.splitext(p)) for p in image_list] thumbs = [os.path.join(self.kw['output_folder'], t) for t in thumbs] + dest_img_list = [os.path.join(self.kw['output_folder'], t) for t in image_list] folders = [] @@ -193,7 +194,8 @@ class Galleries(Task): context["folders"] = folders context["crumbs"] = crumbs context["permalink"] = self.site.link( - "gallery", os.path.basename(gallery), lang) + "gallery", os.path.basename( + os.path.relpath(gallery, self.kw['gallery_path'])), lang) # FIXME: use kw context["enable_comments"] = ( self.site.config["COMMENTS_IN_GALLERIES"]) @@ -219,7 +221,7 @@ class Galleries(Task): template_name, dst, context, - image_list, + dest_img_list, thumbs, file_dep))], 'clean': True, diff --git a/nikola/plugins/task/listings.py b/nikola/plugins/task/listings.py index d8ed43b..86be6c4 100644 --- a/nikola/plugins/task/listings.py +++ b/nikola/plugins/task/listings.py @@ -55,7 +55,7 @@ class Listings(Task): } # Things to ignore in listings - ignored_extensions = (".pyc",) + ignored_extensions = (".pyc", ".pyo") def render_listing(in_name, out_name, folders=[], files=[]): if in_name: diff --git a/nikola/plugins/task/localsearch/files/assets/css/img/search.png b/nikola/plugins/task/localsearch/files/assets/css/img/search.png Binary files differindex 9ab0f2c..9ab0f2c 100755..100644 --- a/nikola/plugins/task/localsearch/files/assets/css/img/search.png +++ b/nikola/plugins/task/localsearch/files/assets/css/img/search.png diff --git a/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css b/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css index 2230193..2230193 100755..100644 --- a/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css +++ b/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css diff --git a/nikola/plugins/task/localsearch/files/tipue_search.html b/nikola/plugins/task/localsearch/files/tipue_search.html index 789fbe5..789fbe5 100755..100644 --- a/nikola/plugins/task/localsearch/files/tipue_search.html +++ b/nikola/plugins/task/localsearch/files/tipue_search.html diff --git a/nikola/plugins/task/rss.py b/nikola/plugins/task/rss.py index e5f7548..9e4204c 100644 --- a/nikola/plugins/task/rss.py +++ b/nikola/plugins/task/rss.py @@ -58,6 +58,11 @@ class GenerateRSS(Task): "feed_length": self.site.config['FEED_LENGTH'], } self.site.scan_posts() + # Check for any changes in the state of use_in_feeds for any post. + # Issue #934 + kw['use_in_feeds_status'] = ''.join( + ['T' if x.use_in_feeds else 'F' for x in self.site.timeline] + ) yield self.group_task() for lang in kw["translations"]: output_name = os.path.join(kw['output_folder'], diff --git a/nikola/plugins/task/sitemap/__init__.py b/nikola/plugins/task/sitemap/__init__.py index 0164000..147bd50 100644 --- a/nikola/plugins/task/sitemap/__init__.py +++ b/nikola/plugins/task/sitemap/__init__.py @@ -144,26 +144,35 @@ class Sitemap(LateTask): def write_sitemap(): # Have to rescan, because files may have been added between # task dep scanning and task execution - scan_locs() with codecs.open(sitemap_path, 'wb+', 'utf8') as outf: outf.write(header) for k in sorted(locs.keys()): outf.write(locs[k]) outf.write("</urlset>") - # Other tasks can depend on this output, instead of having - # to scan locations. + + # Yield a task to calculate the dependencies of the sitemap + # Other tasks can depend on this output, instead of having + # to scan locations. + def scan_locs_task(): + scan_locs() return {'locations': list(locs.keys())} - scan_locs() + yield { + "basename": "_scan_locs", + "name": "sitemap", + "actions": [(scan_locs_task)] + } + yield self.group_task() task = { "basename": "sitemap", "name": sitemap_path, "targets": [sitemap_path], "actions": [(write_sitemap,)], - "uptodate": [config_changed({1: kw, 2: locs})], + "uptodate": [config_changed(kw)], "clean": True, "task_dep": ["render_site"], + "calc_dep": ["_scan_locs:sitemap"], } yield task diff --git a/nikola/plugins/task/tags.py b/nikola/plugins/task/tags.py index a2444ec..f6b8234 100644 --- a/nikola/plugins/task/tags.py +++ b/nikola/plugins/task/tags.py @@ -311,9 +311,17 @@ class RenderTags(Task): self.site.config['INDEX_FILE']] if _f] def tag_path(self, name, lang): - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['TAG_PATH'], self.slugify_name(name) + ".html"] if - _f] + if self.site.config['PRETTY_URLS']: + return [_f for _f in [ + self.site.config['TRANSLATIONS'][lang], + self.site.config['TAG_PATH'], + self.slugify_name(name), + self.site.config['INDEX_FILE']] if _f] + else: + return [_f for _f in [ + self.site.config['TRANSLATIONS'][lang], + self.site.config['TAG_PATH'], + self.slugify_name(name) + ".html"] if _f] def tag_rss_path(self, name, lang): return [_f for _f in [self.site.config['TRANSLATIONS'][lang], diff --git a/nikola/plugins/template/jinja.py b/nikola/plugins/template/jinja.py index 17c33d4..f14adfe 100644 --- a/nikola/plugins/template/jinja.py +++ b/nikola/plugins/template/jinja.py @@ -61,6 +61,11 @@ class JinjaTemplates(TemplateSystem): self.lookup.loader = jinja2.FileSystemLoader(directories, encoding='utf-8') + def set_site(self, site): + """Sets the site.""" + self.site = site + self.lookup.filters.update(self.site.config['TEMPLATE_FILTERS']) + def render_template(self, template_name, output_name, context): """Render the template into output_name using context.""" if jinja2 is None: diff --git a/nikola/plugins/template/mako.py b/nikola/plugins/template/mako.py index 45f4335..5a23230 100644 --- a/nikola/plugins/template/mako.py +++ b/nikola/plugins/template/mako.py @@ -49,6 +49,7 @@ class MakoTemplates(TemplateSystem): lookup = None cache = {} + filters = {} def get_deps(self, filename): text = util.read_file(filename) @@ -81,6 +82,11 @@ class MakoTemplates(TemplateSystem): module_directory=cache_dir, output_encoding='utf-8') + def set_site(self, site): + """Sets the site.""" + self.site = site + self.filters.update(self.site.config['TEMPLATE_FILTERS']) + def render_template(self, template_name, output_name, context): """Render the template into output_name using context.""" context['striphtml'] = striphtml @@ -95,6 +101,8 @@ class MakoTemplates(TemplateSystem): def render_template_to_string(self, template, context): """ Render template to a string using context. """ + context = context.update(self.filters) + return Template(template).render(**context) def template_deps(self, template_name): diff --git a/nikola/post.py b/nikola/post.py index 810474b..5cf7236 100644 --- a/nikola/post.py +++ b/nikola/post.py @@ -194,6 +194,9 @@ class Post(object): # If mathjax is a tag, then enable mathjax rendering support self.is_mathjax = 'mathjax' in self.tags + def __repr__(self): + return '<Post: {0}>'.format(self.source_path) + def _has_pretty_url(self, lang): if self.pretty_urls and \ self.meta[lang].get('pretty_url', '') != 'False' and \ @@ -493,7 +496,7 @@ class Post(object): pieces = [_f for _f in pieces if _f and _f != '.'] link = '/' + '/'.join(pieces) if absolute: - link = urljoin(self.base_url, link) + link = urljoin(self.base_url, link[1:]) index_len = len(self.index_file) if self.strip_indexes and link[-(1 + index_len):] == '/' + self.index_file: return link[:-index_len] diff --git a/nikola/utils.py b/nikola/utils.py index 0c3b4c0..46e159e 100644 --- a/nikola/utils.py +++ b/nikola/utils.py @@ -35,7 +35,6 @@ import locale import logging import os import re -import codecs import json import shutil import subprocess @@ -47,7 +46,7 @@ except ImportError: pass import logbook -from logbook.more import ExceptionHandler +from logbook.more import ExceptionHandler, ColorizedStderrHandler import pytz from . import DEBUG @@ -57,6 +56,15 @@ class ApplicationWarning(Exception): pass +class ColorfulStderrHandler(ColorizedStderrHandler): + """Stream handler with colors.""" + _colorful = False + + def should_colorize(self, record): + """Inform about colorization using the value obtained from Nikola.""" + return self._colorful + + def get_logger(name, handlers): """Get a logger with handlers attached.""" l = logbook.Logger(name) @@ -68,7 +76,7 @@ def get_logger(name, handlers): return l -STDERR_HANDLER = [logbook.StderrHandler( +STDERR_HANDLER = [ColorfulStderrHandler( level=logbook.NOTICE if not DEBUG else logbook.DEBUG, format_string=u'[{record.time:%Y-%m-%dT%H:%M:%SZ}] {record.level_name}: {record.channel}: {record.message}' )] @@ -126,11 +134,11 @@ from unidecode import unidecode import PyRSS2Gen as rss __all__ = ['get_theme_path', 'get_theme_chain', 'load_messages', 'copy_tree', - 'generic_rss_renderer', 'copy_file', 'slugify', 'unslugify', - 'to_datetime', 'apply_filters', 'config_changed', 'get_crumbs', - 'get_tzname', 'get_asset_path', '_reload', 'unicode_str', 'bytes_str', - 'unichr', 'Functionary', 'LocaleBorg', 'sys_encode', 'sys_decode', - 'makedirs', 'get_parent_theme_name', 'ExtendedRSS2', 'demote_headers', + 'copy_file', 'slugify', 'unslugify', 'to_datetime', 'apply_filters', + 'config_changed', 'get_crumbs', 'get_tzname', 'get_asset_path', + '_reload', 'unicode_str', 'bytes_str', 'unichr', 'Functionary', + 'LocaleBorg', 'sys_encode', 'sys_decode', 'makedirs', + 'get_parent_theme_name', 'ExtendedRSS2', 'demote_headers', 'get_translation_candidate'] @@ -313,10 +321,14 @@ def copy_tree(src, dst, link_cutoff=None): dst_dir = os.path.join(dst, *root_parts[base_len:]) makedirs(dst_dir) for src_name in files: - if src_name == '.DS_Store': + if src_name in ('.DS_Store', 'Thumbs.db'): continue dst_file = os.path.join(dst_dir, src_name) src_file = os.path.join(root, src_name) + if sys.version_info[0] == 2: + # Python2 prefers encoded str here + dst_file = sys_encode(dst_file) + src_file = sys_encode(src_file) yield { 'name': str(dst_file), 'file_dep': [src_file], @@ -326,45 +338,6 @@ def copy_tree(src, dst, link_cutoff=None): } -def generic_rss_renderer(lang, title, link, description, timeline, output_path, - rss_teasers, feed_length=10, feed_url=None): - """Takes all necessary data, and renders a RSS feed in output_path.""" - items = [] - for post in timeline[:feed_length]: - args = { - 'title': post.title(lang), - 'link': post.permalink(lang, absolute=True), - 'description': post.text(lang, teaser_only=rss_teasers, really_absolute=True), - 'guid': post.permalink(lang, absolute=True), - # PyRSS2Gen's pubDate is GMT time. - 'pubDate': (post.date if post.date.tzinfo is None else - post.date.astimezone(pytz.timezone('UTC'))), - 'categories': post._tags.get(lang, []), - 'author': post.meta('author'), - } - - items.append(ExtendedItem(**args)) - rss_obj = ExtendedRSS2( - title=title, - link=link, - description=description, - lastBuildDate=datetime.datetime.now(), - items=items, - generator='Nikola <http://getnikola.com/>', - language=lang - ) - rss_obj.self_url = feed_url - rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom" - rss_obj.rss_attrs["xmlns:dc"] = "http://purl.org/dc/elements/1.1/" - dst_dir = os.path.dirname(output_path) - makedirs(dst_dir) - with codecs.open(output_path, "wb+", "utf-8") as rss_file: - data = rss_obj.to_xml(encoding='utf-8') - if isinstance(data, bytes_str): - data = data.decode('utf-8') - rss_file.write(data) - - def copy_file(source, dest, cutoff=None): dst_dir = os.path.dirname(dest) makedirs(dst_dir) @@ -518,9 +491,11 @@ def get_tzname(dt): def current_time(tzinfo=None): - dt = datetime.datetime.now() + dt = datetime.datetime.utcnow() if tzinfo is not None: - dt = tzinfo.localize(dt) + dt = tzinfo.fromutc(dt) + else: + dt = pytz.UTC.localize(dt) return dt @@ -749,7 +724,11 @@ class LocaleBorg(object): else: # Python 2 with calendar.TimeEncoding(self.locales[lang]): s = calendar.month_name[month_no] - s = s.decode(self.encodings[lang]) + enc = self.encodings[lang] + if not enc: + enc = 'UTF-8' + + s = s.decode(enc) # paranoid about calendar ending in the wrong locale (windows) self.set_locale(self.current_lang) return s |
