From ca94afc07df55cb7fc6fe3b4f3011877b7881195 Mon Sep 17 00:00:00 2001 From: Agustin Henze Date: Wed, 20 Nov 2013 16:58:50 -0300 Subject: Imported Upstream version 6.2.1 --- nikola/plugins/__init__.py | 3 +- nikola/plugins/basic_import.py | 166 +++++++ nikola/plugins/command/__init__.py | 25 + nikola/plugins/command/auto.plugin | 9 + nikola/plugins/command/auto.py | 103 ++++ nikola/plugins/command/bootswatch_theme.plugin | 10 + nikola/plugins/command/bootswatch_theme.py | 103 ++++ nikola/plugins/command/check.plugin | 10 + nikola/plugins/command/check.py | 204 ++++++++ nikola/plugins/command/console.plugin | 9 + nikola/plugins/command/console.py | 110 ++++ nikola/plugins/command/deploy.plugin | 9 + nikola/plugins/command/deploy.py | 141 ++++++ nikola/plugins/command/import_blogger.plugin | 10 + nikola/plugins/command/import_blogger.py | 229 +++++++++ nikola/plugins/command/import_feed.plugin | 10 + nikola/plugins/command/import_feed.py | 197 ++++++++ nikola/plugins/command/import_wordpress.plugin | 10 + nikola/plugins/command/import_wordpress.py | 443 +++++++++++++++++ nikola/plugins/command/init.plugin | 9 + nikola/plugins/command/init.py | 137 +++++ nikola/plugins/command/install_plugin.plugin | 10 + nikola/plugins/command/install_plugin.py | 185 +++++++ nikola/plugins/command/install_theme.plugin | 10 + nikola/plugins/command/install_theme.py | 163 ++++++ nikola/plugins/command/mincss.plugin | 10 + nikola/plugins/command/mincss.py | 75 +++ nikola/plugins/command/new_post.plugin | 10 + nikola/plugins/command/new_post.py | 291 +++++++++++ nikola/plugins/command/planetoid.plugin | 9 + nikola/plugins/command/planetoid/__init__.py | 289 +++++++++++ nikola/plugins/command/serve.plugin | 10 + nikola/plugins/command/serve.py | 153 ++++++ nikola/plugins/command/version.plugin | 9 + nikola/plugins/command/version.py | 44 ++ nikola/plugins/command_bootswatch_theme.plugin | 10 - nikola/plugins/command_bootswatch_theme.py | 96 ---- nikola/plugins/command_check.plugin | 10 - nikola/plugins/command_check.py | 166 ------- nikola/plugins/command_console.plugin | 9 - nikola/plugins/command_console.py | 105 ---- nikola/plugins/command_deploy.plugin | 9 - nikola/plugins/command_deploy.py | 65 --- nikola/plugins/command_import_blogger.plugin | 10 - nikola/plugins/command_import_blogger.py | 308 ------------ nikola/plugins/command_import_wordpress.plugin | 10 - nikola/plugins/command_import_wordpress.py | 439 ---------------- nikola/plugins/command_init.plugin | 9 - nikola/plugins/command_init.py | 122 ----- nikola/plugins/command_install_theme.plugin | 10 - nikola/plugins/command_install_theme.py | 105 ---- nikola/plugins/command_new_post.plugin | 10 - nikola/plugins/command_new_post.py | 225 --------- nikola/plugins/command_planetoid.plugin | 9 - nikola/plugins/command_planetoid/__init__.py | 287 ----------- nikola/plugins/command_serve.plugin | 10 - nikola/plugins/command_serve.py | 79 --- nikola/plugins/compile/__init__.py | 0 nikola/plugins/compile/asciidoc.plugin | 10 + nikola/plugins/compile/asciidoc.py | 65 +++ nikola/plugins/compile/bbcode.plugin | 10 + nikola/plugins/compile/bbcode.py | 76 +++ nikola/plugins/compile/html.plugin | 10 + nikola/plugins/compile/html.py | 58 +++ nikola/plugins/compile/ipynb.plugin | 10 + nikola/plugins/compile/ipynb/README.txt | 44 ++ nikola/plugins/compile/ipynb/__init__.py | 100 ++++ nikola/plugins/compile/markdown.plugin | 10 + nikola/plugins/compile/markdown/__init__.py | 88 ++++ nikola/plugins/compile/markdown/mdx_gist.py | 241 +++++++++ nikola/plugins/compile/markdown/mdx_nikola.py | 58 +++ nikola/plugins/compile/markdown/mdx_podcast.py | 87 ++++ nikola/plugins/compile/misaka.plugin | 10 + nikola/plugins/compile/misaka.py | 81 +++ nikola/plugins/compile/pandoc.plugin | 10 + nikola/plugins/compile/pandoc.py | 65 +++ nikola/plugins/compile/php.plugin | 10 + nikola/plugins/compile/php.py | 62 +++ nikola/plugins/compile/rest.plugin | 10 + nikola/plugins/compile/rest/__init__.py | 200 ++++++++ nikola/plugins/compile/rest/chart.plugin | 10 + nikola/plugins/compile/rest/chart.py | 150 ++++++ nikola/plugins/compile/rest/doc.plugin | 10 + nikola/plugins/compile/rest/doc.py | 88 ++++ nikola/plugins/compile/rest/gist.plugin | 10 + nikola/plugins/compile/rest/gist.py | 84 ++++ nikola/plugins/compile/rest/listing.plugin | 10 + nikola/plugins/compile/rest/listing.py | 109 ++++ nikola/plugins/compile/rest/media.plugin | 10 + nikola/plugins/compile/rest/media.py | 63 +++ nikola/plugins/compile/rest/post_list.plugin | 9 + nikola/plugins/compile/rest/post_list.py | 165 ++++++ nikola/plugins/compile/rest/slides.plugin | 10 + nikola/plugins/compile/rest/slides.py | 67 +++ nikola/plugins/compile/rest/soundcloud.plugin | 10 + nikola/plugins/compile/rest/soundcloud.py | 60 +++ nikola/plugins/compile/rest/vimeo.plugin | 7 + nikola/plugins/compile/rest/vimeo.py | 125 +++++ nikola/plugins/compile/rest/youtube.plugin | 8 + nikola/plugins/compile/rest/youtube.py | 81 +++ nikola/plugins/compile/textile.plugin | 10 + nikola/plugins/compile/textile.py | 70 +++ nikola/plugins/compile/txt2tags.plugin | 10 + nikola/plugins/compile/txt2tags.py | 70 +++ nikola/plugins/compile/wiki.plugin | 10 + nikola/plugins/compile/wiki.py | 69 +++ nikola/plugins/compile_bbcode.plugin | 10 - nikola/plugins/compile_bbcode.py | 76 --- nikola/plugins/compile_html.plugin | 10 - nikola/plugins/compile_html.py | 59 --- nikola/plugins/compile_ipynb.plugin | 10 - nikola/plugins/compile_ipynb/README.txt | 35 -- nikola/plugins/compile_ipynb/__init__.py | 100 ---- nikola/plugins/compile_markdown.plugin | 10 - nikola/plugins/compile_markdown/__init__.py | 88 ---- nikola/plugins/compile_markdown/mdx_gist.py | 189 ------- nikola/plugins/compile_markdown/mdx_nikola.py | 56 --- nikola/plugins/compile_markdown/mdx_podcast.py | 87 ---- nikola/plugins/compile_misaka.plugin | 10 - nikola/plugins/compile_misaka/__init__.py | 82 --- nikola/plugins/compile_rest.plugin | 10 - nikola/plugins/compile_rest/__init__.py | 138 ----- nikola/plugins/compile_rest/dummy.py | 44 -- nikola/plugins/compile_rest/gist_directive.py | 56 --- nikola/plugins/compile_rest/listing.py | 121 ----- nikola/plugins/compile_rest/slides.py | 65 --- nikola/plugins/compile_rest/soundcloud.py | 50 -- nikola/plugins/compile_rest/vimeo.py | 118 ----- nikola/plugins/compile_rest/youtube.py | 69 --- nikola/plugins/compile_textile.plugin | 10 - nikola/plugins/compile_textile.py | 70 --- nikola/plugins/compile_txt2tags.plugin | 10 - nikola/plugins/compile_txt2tags.py | 73 --- nikola/plugins/compile_wiki.plugin | 10 - nikola/plugins/compile_wiki.py | 72 --- nikola/plugins/loghandler/smtp.plugin | 9 + nikola/plugins/loghandler/smtp.py | 54 ++ nikola/plugins/loghandler/stderr.plugin | 9 + nikola/plugins/loghandler/stderr.py | 50 ++ nikola/plugins/task/__init__.py | 0 nikola/plugins/task/archive.plugin | 10 + nikola/plugins/task/archive.py | 167 +++++++ nikola/plugins/task/build_less.plugin | 10 + nikola/plugins/task/build_less.py | 99 ++++ nikola/plugins/task/build_sass.plugin | 9 + nikola/plugins/task/build_sass.py | 117 +++++ nikola/plugins/task/bundles.plugin | 10 + nikola/plugins/task/bundles.py | 116 +++++ nikola/plugins/task/copy_assets.plugin | 10 + nikola/plugins/task/copy_assets.py | 89 ++++ nikola/plugins/task/copy_files.plugin | 10 + nikola/plugins/task/copy_files.py | 55 ++ nikola/plugins/task/galleries.plugin | 10 + nikola/plugins/task/galleries.py | 553 +++++++++++++++++++++ nikola/plugins/task/gzip.plugin | 10 + nikola/plugins/task/gzip.py | 78 +++ nikola/plugins/task/indexes.plugin | 10 + nikola/plugins/task/indexes.py | 167 +++++++ nikola/plugins/task/listings.plugin | 10 + nikola/plugins/task/listings.py | 136 +++++ nikola/plugins/task/localsearch.plugin | 10 + nikola/plugins/task/localsearch/MIT-LICENSE.txt | 20 + nikola/plugins/task/localsearch/__init__.py | 106 ++++ .../localsearch/files/assets/css/img/loader.gif | Bin 0 -> 4178 bytes .../localsearch/files/assets/css/img/search.png | Bin 0 -> 315 bytes .../localsearch/files/assets/css/tipuesearch.css | 159 ++++++ .../localsearch/files/assets/js/tipuesearch.js | 384 ++++++++++++++ .../localsearch/files/assets/js/tipuesearch_set.js | 21 + .../task/localsearch/files/tipue_search.html | 31 ++ nikola/plugins/task/mustache.plugin | 10 + nikola/plugins/task/mustache/__init__.py | 182 +++++++ .../plugins/task/mustache/mustache-template.html | 29 ++ nikola/plugins/task/mustache/mustache.html | 34 ++ nikola/plugins/task/pages.plugin | 10 + nikola/plugins/task/pages.py | 58 +++ nikola/plugins/task/posts.plugin | 10 + nikola/plugins/task/posts.py | 66 +++ nikola/plugins/task/redirect.plugin | 10 + nikola/plugins/task/redirect.py | 66 +++ nikola/plugins/task/rss.plugin | 10 + nikola/plugins/task/rss.py | 91 ++++ nikola/plugins/task/sitemap.plugin | 10 + nikola/plugins/task/sitemap/__init__.py | 172 +++++++ nikola/plugins/task/sources.plugin | 10 + nikola/plugins/task/sources.py | 81 +++ nikola/plugins/task/tags.plugin | 10 + nikola/plugins/task/tags.py | 324 ++++++++++++ nikola/plugins/task_archive.plugin | 10 - nikola/plugins/task_archive.py | 147 ------ nikola/plugins/task_copy_assets.plugin | 10 - nikola/plugins/task_copy_assets.py | 93 ---- nikola/plugins/task_copy_files.plugin | 10 - nikola/plugins/task_copy_files.py | 59 --- nikola/plugins/task_create_bundles.plugin | 10 - nikola/plugins/task_create_bundles.py | 120 ----- nikola/plugins/task_indexes.plugin | 10 - nikola/plugins/task_indexes.py | 141 ------ nikola/plugins/task_localsearch.plugin | 10 - nikola/plugins/task_localsearch/MIT-LICENSE.txt | 20 - nikola/plugins/task_localsearch/__init__.py | 102 ---- .../files/assets/css/img/expand.png | Bin 424 -> 0 bytes .../task_localsearch/files/assets/css/img/link.png | Bin 463 -> 0 bytes .../files/assets/css/img/loader.gif | Bin 4178 -> 0 bytes .../files/assets/css/img/search.gif | Bin 208 -> 0 bytes .../files/assets/css/tipuesearch.css | 232 --------- .../files/assets/js/tipuesearch.js | 426 ---------------- .../files/assets/js/tipuesearch_set.js | 28 -- .../task_localsearch/files/tipue_search.html | 31 -- nikola/plugins/task_mustache.plugin | 10 - nikola/plugins/task_mustache/__init__.py | 197 -------- .../plugins/task_mustache/mustache-template.html | 29 -- nikola/plugins/task_mustache/mustache.html | 36 -- nikola/plugins/task_redirect.plugin | 10 - nikola/plugins/task_redirect.py | 76 --- nikola/plugins/task_render_galleries.plugin | 10 - nikola/plugins/task_render_galleries.py | 338 ------------- nikola/plugins/task_render_listings.plugin | 10 - nikola/plugins/task_render_listings.py | 129 ----- nikola/plugins/task_render_pages.plugin | 10 - nikola/plugins/task_render_pages.py | 64 --- nikola/plugins/task_render_posts.plugin | 10 - nikola/plugins/task_render_posts.py | 140 ------ nikola/plugins/task_render_rss.plugin | 10 - nikola/plugins/task_render_rss.py | 72 --- nikola/plugins/task_render_sources.plugin | 10 - nikola/plugins/task_render_sources.py | 84 ---- nikola/plugins/task_render_tags.plugin | 10 - nikola/plugins/task_render_tags.py | 243 --------- nikola/plugins/task_sitemap.plugin | 10 - nikola/plugins/task_sitemap/__init__.py | 105 ---- nikola/plugins/template/__init__.py | 0 nikola/plugins/template/jinja.plugin | 9 + nikola/plugins/template/jinja.py | 102 ++++ nikola/plugins/template/mako.plugin | 9 + nikola/plugins/template/mako.py | 115 +++++ nikola/plugins/template_jinja.plugin | 9 - nikola/plugins/template_jinja.py | 76 --- nikola/plugins/template_mako.plugin | 9 - nikola/plugins/template_mako.py | 92 ---- 239 files changed, 9800 insertions(+), 7101 deletions(-) create mode 100644 nikola/plugins/basic_import.py create mode 100644 nikola/plugins/command/__init__.py create mode 100644 nikola/plugins/command/auto.plugin create mode 100644 nikola/plugins/command/auto.py create mode 100644 nikola/plugins/command/bootswatch_theme.plugin create mode 100644 nikola/plugins/command/bootswatch_theme.py create mode 100644 nikola/plugins/command/check.plugin create mode 100644 nikola/plugins/command/check.py create mode 100644 nikola/plugins/command/console.plugin create mode 100644 nikola/plugins/command/console.py create mode 100644 nikola/plugins/command/deploy.plugin create mode 100644 nikola/plugins/command/deploy.py create mode 100644 nikola/plugins/command/import_blogger.plugin create mode 100644 nikola/plugins/command/import_blogger.py create mode 100644 nikola/plugins/command/import_feed.plugin create mode 100644 nikola/plugins/command/import_feed.py create mode 100644 nikola/plugins/command/import_wordpress.plugin create mode 100644 nikola/plugins/command/import_wordpress.py create mode 100644 nikola/plugins/command/init.plugin create mode 100644 nikola/plugins/command/init.py create mode 100644 nikola/plugins/command/install_plugin.plugin create mode 100644 nikola/plugins/command/install_plugin.py create mode 100644 nikola/plugins/command/install_theme.plugin create mode 100644 nikola/plugins/command/install_theme.py create mode 100644 nikola/plugins/command/mincss.plugin create mode 100644 nikola/plugins/command/mincss.py create mode 100644 nikola/plugins/command/new_post.plugin create mode 100644 nikola/plugins/command/new_post.py create mode 100644 nikola/plugins/command/planetoid.plugin create mode 100644 nikola/plugins/command/planetoid/__init__.py create mode 100644 nikola/plugins/command/serve.plugin create mode 100644 nikola/plugins/command/serve.py create mode 100644 nikola/plugins/command/version.plugin create mode 100644 nikola/plugins/command/version.py delete mode 100644 nikola/plugins/command_bootswatch_theme.plugin delete mode 100644 nikola/plugins/command_bootswatch_theme.py delete mode 100644 nikola/plugins/command_check.plugin delete mode 100644 nikola/plugins/command_check.py delete mode 100644 nikola/plugins/command_console.plugin delete mode 100644 nikola/plugins/command_console.py delete mode 100644 nikola/plugins/command_deploy.plugin delete mode 100644 nikola/plugins/command_deploy.py delete mode 100644 nikola/plugins/command_import_blogger.plugin delete mode 100644 nikola/plugins/command_import_blogger.py delete mode 100644 nikola/plugins/command_import_wordpress.plugin delete mode 100644 nikola/plugins/command_import_wordpress.py delete mode 100644 nikola/plugins/command_init.plugin delete mode 100644 nikola/plugins/command_init.py delete mode 100644 nikola/plugins/command_install_theme.plugin delete mode 100644 nikola/plugins/command_install_theme.py delete mode 100644 nikola/plugins/command_new_post.plugin delete mode 100644 nikola/plugins/command_new_post.py delete mode 100644 nikola/plugins/command_planetoid.plugin delete mode 100644 nikola/plugins/command_planetoid/__init__.py delete mode 100644 nikola/plugins/command_serve.plugin delete mode 100644 nikola/plugins/command_serve.py create mode 100644 nikola/plugins/compile/__init__.py create mode 100644 nikola/plugins/compile/asciidoc.plugin create mode 100644 nikola/plugins/compile/asciidoc.py create mode 100644 nikola/plugins/compile/bbcode.plugin create mode 100644 nikola/plugins/compile/bbcode.py create mode 100644 nikola/plugins/compile/html.plugin create mode 100644 nikola/plugins/compile/html.py create mode 100644 nikola/plugins/compile/ipynb.plugin create mode 100644 nikola/plugins/compile/ipynb/README.txt create mode 100644 nikola/plugins/compile/ipynb/__init__.py create mode 100644 nikola/plugins/compile/markdown.plugin create mode 100644 nikola/plugins/compile/markdown/__init__.py create mode 100644 nikola/plugins/compile/markdown/mdx_gist.py create mode 100644 nikola/plugins/compile/markdown/mdx_nikola.py create mode 100644 nikola/plugins/compile/markdown/mdx_podcast.py create mode 100644 nikola/plugins/compile/misaka.plugin create mode 100644 nikola/plugins/compile/misaka.py create mode 100644 nikola/plugins/compile/pandoc.plugin create mode 100644 nikola/plugins/compile/pandoc.py create mode 100644 nikola/plugins/compile/php.plugin create mode 100644 nikola/plugins/compile/php.py create mode 100644 nikola/plugins/compile/rest.plugin create mode 100644 nikola/plugins/compile/rest/__init__.py create mode 100644 nikola/plugins/compile/rest/chart.plugin create mode 100644 nikola/plugins/compile/rest/chart.py create mode 100644 nikola/plugins/compile/rest/doc.plugin create mode 100644 nikola/plugins/compile/rest/doc.py create mode 100644 nikola/plugins/compile/rest/gist.plugin create mode 100644 nikola/plugins/compile/rest/gist.py create mode 100644 nikola/plugins/compile/rest/listing.plugin create mode 100644 nikola/plugins/compile/rest/listing.py create mode 100644 nikola/plugins/compile/rest/media.plugin create mode 100644 nikola/plugins/compile/rest/media.py create mode 100644 nikola/plugins/compile/rest/post_list.plugin create mode 100644 nikola/plugins/compile/rest/post_list.py create mode 100644 nikola/plugins/compile/rest/slides.plugin create mode 100644 nikola/plugins/compile/rest/slides.py create mode 100644 nikola/plugins/compile/rest/soundcloud.plugin create mode 100644 nikola/plugins/compile/rest/soundcloud.py create mode 100644 nikola/plugins/compile/rest/vimeo.plugin create mode 100644 nikola/plugins/compile/rest/vimeo.py create mode 100644 nikola/plugins/compile/rest/youtube.plugin create mode 100644 nikola/plugins/compile/rest/youtube.py create mode 100644 nikola/plugins/compile/textile.plugin create mode 100644 nikola/plugins/compile/textile.py create mode 100644 nikola/plugins/compile/txt2tags.plugin create mode 100644 nikola/plugins/compile/txt2tags.py create mode 100644 nikola/plugins/compile/wiki.plugin create mode 100644 nikola/plugins/compile/wiki.py delete mode 100644 nikola/plugins/compile_bbcode.plugin delete mode 100644 nikola/plugins/compile_bbcode.py delete mode 100644 nikola/plugins/compile_html.plugin delete mode 100644 nikola/plugins/compile_html.py delete mode 100644 nikola/plugins/compile_ipynb.plugin delete mode 100644 nikola/plugins/compile_ipynb/README.txt delete mode 100644 nikola/plugins/compile_ipynb/__init__.py delete mode 100644 nikola/plugins/compile_markdown.plugin delete mode 100644 nikola/plugins/compile_markdown/__init__.py delete mode 100644 nikola/plugins/compile_markdown/mdx_gist.py delete mode 100644 nikola/plugins/compile_markdown/mdx_nikola.py delete mode 100644 nikola/plugins/compile_markdown/mdx_podcast.py delete mode 100644 nikola/plugins/compile_misaka.plugin delete mode 100644 nikola/plugins/compile_misaka/__init__.py delete mode 100644 nikola/plugins/compile_rest.plugin delete mode 100644 nikola/plugins/compile_rest/__init__.py delete mode 100644 nikola/plugins/compile_rest/dummy.py delete mode 100644 nikola/plugins/compile_rest/gist_directive.py delete mode 100644 nikola/plugins/compile_rest/listing.py delete mode 100644 nikola/plugins/compile_rest/slides.py delete mode 100644 nikola/plugins/compile_rest/soundcloud.py delete mode 100644 nikola/plugins/compile_rest/vimeo.py delete mode 100644 nikola/plugins/compile_rest/youtube.py delete mode 100644 nikola/plugins/compile_textile.plugin delete mode 100644 nikola/plugins/compile_textile.py delete mode 100644 nikola/plugins/compile_txt2tags.plugin delete mode 100644 nikola/plugins/compile_txt2tags.py delete mode 100644 nikola/plugins/compile_wiki.plugin delete mode 100644 nikola/plugins/compile_wiki.py create mode 100644 nikola/plugins/loghandler/smtp.plugin create mode 100644 nikola/plugins/loghandler/smtp.py create mode 100644 nikola/plugins/loghandler/stderr.plugin create mode 100644 nikola/plugins/loghandler/stderr.py create mode 100644 nikola/plugins/task/__init__.py create mode 100644 nikola/plugins/task/archive.plugin create mode 100644 nikola/plugins/task/archive.py create mode 100644 nikola/plugins/task/build_less.plugin create mode 100644 nikola/plugins/task/build_less.py create mode 100644 nikola/plugins/task/build_sass.plugin create mode 100644 nikola/plugins/task/build_sass.py create mode 100644 nikola/plugins/task/bundles.plugin create mode 100644 nikola/plugins/task/bundles.py create mode 100644 nikola/plugins/task/copy_assets.plugin create mode 100644 nikola/plugins/task/copy_assets.py create mode 100644 nikola/plugins/task/copy_files.plugin create mode 100644 nikola/plugins/task/copy_files.py create mode 100644 nikola/plugins/task/galleries.plugin create mode 100644 nikola/plugins/task/galleries.py create mode 100644 nikola/plugins/task/gzip.plugin create mode 100644 nikola/plugins/task/gzip.py create mode 100644 nikola/plugins/task/indexes.plugin create mode 100644 nikola/plugins/task/indexes.py create mode 100644 nikola/plugins/task/listings.plugin create mode 100644 nikola/plugins/task/listings.py create mode 100644 nikola/plugins/task/localsearch.plugin create mode 100644 nikola/plugins/task/localsearch/MIT-LICENSE.txt create mode 100644 nikola/plugins/task/localsearch/__init__.py create mode 100644 nikola/plugins/task/localsearch/files/assets/css/img/loader.gif create mode 100755 nikola/plugins/task/localsearch/files/assets/css/img/search.png create mode 100755 nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css create mode 100644 nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js create mode 100644 nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js create mode 100755 nikola/plugins/task/localsearch/files/tipue_search.html create mode 100644 nikola/plugins/task/mustache.plugin create mode 100644 nikola/plugins/task/mustache/__init__.py create mode 100644 nikola/plugins/task/mustache/mustache-template.html create mode 100644 nikola/plugins/task/mustache/mustache.html create mode 100644 nikola/plugins/task/pages.plugin create mode 100644 nikola/plugins/task/pages.py create mode 100644 nikola/plugins/task/posts.plugin create mode 100644 nikola/plugins/task/posts.py create mode 100644 nikola/plugins/task/redirect.plugin create mode 100644 nikola/plugins/task/redirect.py create mode 100644 nikola/plugins/task/rss.plugin create mode 100644 nikola/plugins/task/rss.py create mode 100644 nikola/plugins/task/sitemap.plugin create mode 100644 nikola/plugins/task/sitemap/__init__.py create mode 100644 nikola/plugins/task/sources.plugin create mode 100644 nikola/plugins/task/sources.py create mode 100644 nikola/plugins/task/tags.plugin create mode 100644 nikola/plugins/task/tags.py delete mode 100644 nikola/plugins/task_archive.plugin delete mode 100644 nikola/plugins/task_archive.py delete mode 100644 nikola/plugins/task_copy_assets.plugin delete mode 100644 nikola/plugins/task_copy_assets.py delete mode 100644 nikola/plugins/task_copy_files.plugin delete mode 100644 nikola/plugins/task_copy_files.py delete mode 100644 nikola/plugins/task_create_bundles.plugin delete mode 100644 nikola/plugins/task_create_bundles.py delete mode 100644 nikola/plugins/task_indexes.plugin delete mode 100644 nikola/plugins/task_indexes.py delete mode 100644 nikola/plugins/task_localsearch.plugin delete mode 100644 nikola/plugins/task_localsearch/MIT-LICENSE.txt delete mode 100644 nikola/plugins/task_localsearch/__init__.py delete mode 100755 nikola/plugins/task_localsearch/files/assets/css/img/expand.png delete mode 100755 nikola/plugins/task_localsearch/files/assets/css/img/link.png delete mode 100644 nikola/plugins/task_localsearch/files/assets/css/img/loader.gif delete mode 100644 nikola/plugins/task_localsearch/files/assets/css/img/search.gif delete mode 100755 nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css delete mode 100644 nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js delete mode 100644 nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js delete mode 100755 nikola/plugins/task_localsearch/files/tipue_search.html delete mode 100644 nikola/plugins/task_mustache.plugin delete mode 100644 nikola/plugins/task_mustache/__init__.py delete mode 100644 nikola/plugins/task_mustache/mustache-template.html delete mode 100644 nikola/plugins/task_mustache/mustache.html delete mode 100644 nikola/plugins/task_redirect.plugin delete mode 100644 nikola/plugins/task_redirect.py delete mode 100644 nikola/plugins/task_render_galleries.plugin delete mode 100644 nikola/plugins/task_render_galleries.py delete mode 100644 nikola/plugins/task_render_listings.plugin delete mode 100644 nikola/plugins/task_render_listings.py delete mode 100644 nikola/plugins/task_render_pages.plugin delete mode 100644 nikola/plugins/task_render_pages.py delete mode 100644 nikola/plugins/task_render_posts.plugin delete mode 100644 nikola/plugins/task_render_posts.py delete mode 100644 nikola/plugins/task_render_rss.plugin delete mode 100644 nikola/plugins/task_render_rss.py delete mode 100644 nikola/plugins/task_render_sources.plugin delete mode 100644 nikola/plugins/task_render_sources.py delete mode 100644 nikola/plugins/task_render_tags.plugin delete mode 100644 nikola/plugins/task_render_tags.py delete mode 100644 nikola/plugins/task_sitemap.plugin delete mode 100644 nikola/plugins/task_sitemap/__init__.py create mode 100644 nikola/plugins/template/__init__.py create mode 100644 nikola/plugins/template/jinja.plugin create mode 100644 nikola/plugins/template/jinja.py create mode 100644 nikola/plugins/template/mako.plugin create mode 100644 nikola/plugins/template/mako.py delete mode 100644 nikola/plugins/template_jinja.plugin delete mode 100644 nikola/plugins/template_jinja.py delete mode 100644 nikola/plugins/template_mako.plugin delete mode 100644 nikola/plugins/template_mako.py (limited to 'nikola/plugins') diff --git a/nikola/plugins/__init__.py b/nikola/plugins/__init__.py index b1de7f1..139759b 100644 --- a/nikola/plugins/__init__.py +++ b/nikola/plugins/__init__.py @@ -1,3 +1,2 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import - -from . import command_import_wordpress # NOQA diff --git a/nikola/plugins/basic_import.py b/nikola/plugins/basic_import.py new file mode 100644 index 0000000..e368fca --- /dev/null +++ b/nikola/plugins/basic_import.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 +import codecs +import csv +import datetime +import os + +try: + from urlparse import urlparse +except ImportError: + from urllib.parse import urlparse # NOQA + +from lxml import etree, html +from mako.template import Template + +from nikola import utils + +links = {} + + +class ImportMixin(object): + """Mixin with common used methods.""" + + name = "import_mixin" + needs_config = False + doc_usage = "[options] wordpress_export_file" + doc_purpose = "import a wordpress dump." + cmd_options = [ + { + 'name': 'output_folder', + 'long': 'output-folder', + 'short': 'o', + 'default': 'new_site', + 'help': 'Location to write imported content.' + }, + ] + + def _execute(self, options={}, args=[]): + """Import a blog from an export into a Nikola site.""" + raise NotImplementedError("Must be implemented by a subclass.") + + @classmethod + def get_channel_from_file(cls, filename): + tree = etree.fromstring(cls.read_xml_file(filename)) + channel = tree.find('channel') + return channel + + @staticmethod + def configure_redirections(url_map): + redirections = [] + for k, v in url_map.items(): + if not k[-1] == '/': + k = k + '/' + + # remove the initial "/" because src is a relative file path + src = (urlparse(k).path + 'index.html')[1:] + dst = (urlparse(v).path) + if src == 'index.html': + utils.LOGGER.warn("Can't do a redirect for: {0!r}".format(k)) + else: + redirections.append((src, dst)) + + return redirections + + def generate_base_site(self): + if not os.path.exists(self.output_folder): + os.system('nikola init ' + self.output_folder) + 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)) + + filename = os.path.join(os.path.dirname(utils.__file__), 'conf.py.in') + # The 'strict_undefined=True' will give the missing symbol name if any, + # (ex: NameError: 'THEME' is not defined ) + # for other errors from mako/runtime.py, you can add format_extensions=True , + # then more info will be writen to *somefile* (most probably conf.py) + conf_template = Template(filename=filename, strict_undefined=True) + + return conf_template + + @staticmethod + def populate_context(channel): + raise NotImplementedError("Must be implemented by a subclass.") + + @classmethod + def transform_content(cls, content): + return content + + @classmethod + def write_content(cls, filename, content): + doc = html.document_fromstring(content) + doc.rewrite_links(replacer) + + utils.makedirs(os.path.dirname(filename)) + with open(filename, "wb+") as fd: + fd.write(html.tostring(doc, encoding='utf8')) + + @staticmethod + def write_metadata(filename, title, slug, post_date, description, tags): + if not description: + description = "" + + utils.makedirs(os.path.dirname(filename)) + with codecs.open(filename, "w+", "utf8") as fd: + fd.write('{0}\n'.format(title)) + fd.write('{0}\n'.format(slug)) + fd.write('{0}\n'.format(post_date)) + fd.write('{0}\n'.format(','.join(tags))) + fd.write('\n') + fd.write('{0}\n'.format(description)) + + @staticmethod + def write_urlmap_csv(output_file, url_map): + utils.makedirs(os.path.dirname(output_file)) + with codecs.open(output_file, 'w+', 'utf8') as fd: + csv_writer = csv.writer(fd) + for item in url_map.items(): + csv_writer.writerow(item) + + def get_configuration_output_path(self): + if not self.import_into_existing_site: + filename = 'conf.py' + else: + filename = 'conf.py.{name}-{time}'.format( + 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)) + + return config_output_path + + @staticmethod + def write_configuration(filename, rendered_template): + utils.makedirs(os.path.dirname(filename)) + with codecs.open(filename, 'w+', 'utf8') as fd: + fd.write(rendered_template) + + +def replacer(dst): + return links.get(dst, dst) diff --git a/nikola/plugins/command/__init__.py b/nikola/plugins/command/__init__.py new file mode 100644 index 0000000..9be4d63 --- /dev/null +++ b/nikola/plugins/command/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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. diff --git a/nikola/plugins/command/auto.plugin b/nikola/plugins/command/auto.plugin new file mode 100644 index 0000000..87939b2 --- /dev/null +++ b/nikola/plugins/command/auto.plugin @@ -0,0 +1,9 @@ +[Core] +Name = auto +Module = auto + +[Documentation] +Author = Roberto Alsina +Version = 0.2 +Website = http://getnikola.com +Description = Automatically detect site changes, rebuild and optionally refresh a browser. diff --git a/nikola/plugins/command/auto.py b/nikola/plugins/command/auto.py new file mode 100644 index 0000000..cb726d9 --- /dev/null +++ b/nikola/plugins/command/auto.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 print_function, unicode_literals + +import codecs +import json +import os +import subprocess + +from nikola.plugin_categories import Command +from nikola.utils import req_missing + +GUARDFILE = """#!/usr/bin/env python +# -*- coding: utf-8 -*- +from livereload.task import Task +import json +import subprocess + +def f(): + import subprocess + subprocess.call(("nikola", "build")) + +fdata = json.loads('''{0}''') + +for watch in fdata: + Task.add(watch, f) +""" + + +class Auto(Command): + """Start debugging console.""" + name = "auto" + doc_purpose = "automatically detect site changes, rebuild and optionally refresh a browser" + cmd_options = [ + { + 'name': 'browser', + 'short': 'b', + 'type': bool, + 'help': 'Start a web browser.', + 'default': False, + }, + { + 'name': 'port', + 'short': 'p', + 'long': 'port', + 'default': 8000, + 'type': int, + 'help': 'Port nummber (default: 8000)', + }, + ] + + def _execute(self, options, args): + """Start the watcher.""" + try: + from livereload.server import start + except ImportError: + req_missing(['livereload'], 'use the "auto" command') + return + + # Run an initial build so we are uptodate + subprocess.call(("nikola", "build")) + + port = options and options.get('port') + + # Create a Guardfile + with codecs.open("Guardfile", "wb+", "utf8") as guardfile: + l = ["conf.py", "themes", "templates", self.site.config['GALLERY_PATH']] + for item in self.site.config['post_pages']: + l.append(os.path.dirname(item[0])) + for item in self.site.config['FILES_FOLDERS']: + l.append(os.path.dirname(item)) + data = GUARDFILE.format(json.dumps(l)) + guardfile.write(data) + + out_folder = self.site.config['OUTPUT_FOLDER'] + + os.chmod("Guardfile", 0o755) + + start(port, out_folder, options and options.get('browser')) diff --git a/nikola/plugins/command/bootswatch_theme.plugin b/nikola/plugins/command/bootswatch_theme.plugin new file mode 100644 index 0000000..7091310 --- /dev/null +++ b/nikola/plugins/command/bootswatch_theme.plugin @@ -0,0 +1,10 @@ +[Core] +Name = bootswatch_theme +Module = bootswatch_theme + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Given a swatch name and a parent theme, creates a custom theme. + diff --git a/nikola/plugins/command/bootswatch_theme.py b/nikola/plugins/command/bootswatch_theme.py new file mode 100644 index 0000000..eb27f94 --- /dev/null +++ b/nikola/plugins/command/bootswatch_theme.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 print_function +import os + +try: + import requests +except ImportError: + requests = None # NOQA + +from nikola.plugin_categories import Command +from nikola import utils + +LOGGER = utils.get_logger('bootswatch_theme', utils.STDERR_HANDLER) + + +class CommandBootswatchTheme(Command): + """Given a swatch name from bootswatch.com and a parent theme, creates a custom theme.""" + + name = "bootswatch_theme" + doc_usage = "[options]" + doc_purpose = "given a swatch name from bootswatch.com and a parent theme, creates a custom"\ + " theme" + cmd_options = [ + { + 'name': 'name', + 'short': 'n', + 'long': 'name', + 'default': 'custom', + 'type': str, + 'help': 'New theme name (default: custom)', + }, + { + 'name': 'swatch', + 'short': 's', + 'default': 'slate', + 'type': str, + 'help': 'Name of the swatch from bootswatch.com.' + }, + { + 'name': 'parent', + 'short': 'p', + 'long': 'parent', + 'default': 'bootstrap3', + 'help': 'Parent theme name (default: bootstrap3)', + }, + ] + + def _execute(self, options, args): + """Given a swatch name and a parent theme, creates a custom theme.""" + if requests is None: + utils.req_missing(['requests'], 'install Bootswatch themes') + + name = options['name'] + swatch = options['swatch'] + parent = options['parent'] + version = '' + + # See if we need bootswatch for bootstrap v2 or v3 + themes = utils.get_theme_chain(parent) + if 'bootstrap3' not in themes: + version = '2' + elif 'bootstrap' not in themes: + LOGGER.warn('"bootswatch_theme" only makes sense for themes that use bootstrap') + + LOGGER.notice("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) + data = requests.get(url).text + with open(os.path.join('themes', name, 'assets', 'css', fname), + 'wb+') as output: + output.write(data.encode('utf-8')) + + 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)) diff --git a/nikola/plugins/command/check.plugin b/nikola/plugins/command/check.plugin new file mode 100644 index 0000000..8ceda5f --- /dev/null +++ b/nikola/plugins/command/check.plugin @@ -0,0 +1,10 @@ +[Core] +Name = check +Module = check + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Check the generated site + diff --git a/nikola/plugins/command/check.py b/nikola/plugins/command/check.py new file mode 100644 index 0000000..5c7e49a --- /dev/null +++ b/nikola/plugins/command/check.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 print_function +import os +import re +import sys +try: + from urllib import unquote + from urlparse import urlparse +except ImportError: + from urllib.parse import unquote, urlparse # NOQA + +import lxml.html + +from nikola.plugin_categories import Command +from nikola.utils import get_logger + + +class CommandCheck(Command): + """Check the generated site.""" + + name = "check" + logger = None + + doc_usage = "-l [--find-sources] | -f" + doc_purpose = "check links and files in the generated site" + cmd_options = [ + { + 'name': 'links', + 'short': 'l', + 'long': 'check-links', + 'type': bool, + 'default': False, + 'help': 'Check for dangling links', + }, + { + 'name': 'files', + 'short': 'f', + 'long': 'check-files', + 'type': bool, + 'default': False, + 'help': 'Check for unknown files', + }, + { + 'name': 'clean', + 'long': 'clean-files', + 'type': bool, + 'default': False, + 'help': 'Remove all unknown files, use with caution', + }, + { + 'name': 'find_sources', + 'long': 'find-sources', + 'type': bool, + 'default': False, + 'help': 'List possible source files for files with broken links.', + }, + ] + + def _execute(self, options, args): + """Check the generated site.""" + + self.logger = get_logger('check', self.site.loghandlers) + + if not options['links'] and not options['files'] and not options['clean']: + print(self.help()) + return False + if options['links']: + failure = self.scan_links(options['find_sources']) + if options['files']: + failure = self.scan_files() + if options['clean']: + failure = self.clean_files() + if failure: + sys.exit(1) + + existing_targets = set([]) + + def analyze(self, task, find_sources=False): + rv = False + self.whitelist = [re.compile(x) for x in self.site.config['LINK_CHECK_WHITELIST']] + try: + filename = task.split(":")[-1] + d = lxml.html.fromstring(open(filename).read()) + for l in d.iterlinks(): + target = l[0].attrib[l[1]] + if target == "#": + continue + parsed = urlparse(target) + if parsed.scheme or target.startswith('//'): + continue + if parsed.fragment: + target = target.split('#')[0] + target_filename = os.path.abspath( + os.path.join(os.path.dirname(filename), unquote(target))) + 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.existing_targets.add(target_filename) + else: + rv = True + self.logger.warn("Broken link in {0}: ".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) + return rv + + def scan_links(self, find_sources=False): + self.logger.notice("Checking Links:") + self.logger.notice("===============") + failure = False + for task in os.popen('nikola list --all', 'r').readlines(): + task = task.strip() + if task.split(':')[0] in ( + 'render_tags', 'render_archive', + 'render_galleries', 'render_indexes', + 'render_pages' + 'render_site') and '.html' in task: + if self.analyze(task, find_sources): + failure = True + if not failure: + self.logger.notice("All links checked.") + return failure + + def scan_files(self): + failure = False + self.logger.notice("Checking Files:") + self.logger.notice("===============\n") + only_on_output, only_on_input = self.real_scan_files() + + # Ignore folders + only_on_output = [p for p in only_on_output if not os.path.isdir(p)] + only_on_input = [p for p in only_on_input if not os.path.isdir(p)] + + if only_on_output: + only_on_output.sort() + self.logger.warn("Files from unknown origins:") + for f in only_on_output: + self.logger.warn(f) + failure = True + if only_on_input: + only_on_input.sort() + self.logger.warn("Files not generated:") + for f in only_on_input: + self.logger.warn(f) + if not failure: + self.logger.notice("All files checked.") + return failure + + def clean_files(self): + only_on_output, _ = self.real_scan_files() + for f in only_on_output: + os.unlink(f) + return True + + def real_scan_files(self): + task_fnames = set([]) + real_fnames = set([]) + output_folder = self.site.config['OUTPUT_FOLDER'] + # First check that all targets are generated in the right places + for task in os.popen('nikola list --all', 'r').readlines(): + task = task.strip() + if output_folder in task and ':' in task: + fname = task.split(':', 1)[-1] + task_fnames.add(fname) + # And now check that there are no non-target files + for root, dirs, files in os.walk(output_folder): + for src_name in files: + fname = os.path.join(root, src_name) + real_fnames.add(fname) + + only_on_output = list(real_fnames - task_fnames) + + only_on_input = list(task_fnames - real_fnames) + + return (only_on_output, only_on_input) diff --git a/nikola/plugins/command/console.plugin b/nikola/plugins/command/console.plugin new file mode 100644 index 0000000..a2be9ca --- /dev/null +++ b/nikola/plugins/command/console.plugin @@ -0,0 +1,9 @@ +[Core] +Name = console +Module = console + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Start a debugging python console diff --git a/nikola/plugins/command/console.py b/nikola/plugins/command/console.py new file mode 100644 index 0000000..fe17dfc --- /dev/null +++ b/nikola/plugins/command/console.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 print_function, unicode_literals + +import os + +from nikola import __version__ +from nikola.plugin_categories import Command +from nikola.utils import get_logger, STDERR_HANDLER + +LOGGER = get_logger('console', STDERR_HANDLER) + + +class Console(Command): + """Start debugging console.""" + name = "console" + shells = ['ipython', 'bpython', 'plain'] + doc_purpose = "Start an interactive Python (IPython->bpython->plain) console with access to your site and configuration" + header = "Nikola v" + __version__ + " -- {0} Console (conf = configuration, SITE = site engine)" + + def ipython(self): + """IPython shell.""" + from nikola import Nikola + try: + import conf + except ImportError: + LOGGER.error("No configuration found, cannot run the console.") + else: + import IPython + SITE = Nikola(**conf.__dict__) + SITE.scan_posts() + IPython.embed(header=self.header.format('IPython')) + + def bpython(self): + """bpython shell.""" + from nikola import Nikola + try: + import conf + except ImportError: + LOGGER.error("No configuration found, cannot run the console.") + else: + import bpython + SITE = Nikola(**conf.__dict__) + SITE.scan_posts() + gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola} + bpython.embed(banner=self.header.format( + 'bpython (Slightly Deprecated)'), locals_=gl) + + def plain(self): + """Plain Python shell.""" + from nikola import Nikola + try: + import conf + SITE = Nikola(**conf.__dict__) + SITE.scan_posts() + gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola} + except ImportError: + LOGGER.error("No configuration found, cannot run the console.") + else: + import code + try: + import readline + except ImportError: + pass + else: + import rlcompleter + readline.set_completer(rlcompleter.Completer(gl).complete) + readline.parse_and_bind("tab:complete") + + pythonrc = os.environ.get("PYTHONSTARTUP") + if pythonrc and os.path.isfile(pythonrc): + try: + execfile(pythonrc) # NOQA + except NameError: + pass + + code.interact(local=gl, banner=self.header.format('Python')) + + def _execute(self, options, args): + """Start the console.""" + for shell in self.shells: + try: + return getattr(self, shell)() + except ImportError: + pass + raise ImportError diff --git a/nikola/plugins/command/deploy.plugin b/nikola/plugins/command/deploy.plugin new file mode 100644 index 0000000..10cc796 --- /dev/null +++ b/nikola/plugins/command/deploy.plugin @@ -0,0 +1,9 @@ +[Core] +Name = deploy +Module = deploy + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Deploy the site diff --git a/nikola/plugins/command/deploy.py b/nikola/plugins/command/deploy.py new file mode 100644 index 0000000..efb909d --- /dev/null +++ b/nikola/plugins/command/deploy.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 print_function +from ast import literal_eval +import codecs +from datetime import datetime +import os +import sys +import subprocess +import time +import pytz + +from blinker import signal + +from nikola.plugin_categories import Command +from nikola.utils import remove_file, get_logger + + +class Deploy(Command): + """Deploy site. """ + name = "deploy" + + doc_usage = "" + doc_purpose = "deploy the site" + + logger = None + + def _execute(self, command, args): + self.logger = get_logger('deploy', self.site.loghandlers) + # Get last successful deploy date + timestamp_path = os.path.join(self.site.config['CACHE_FOLDER'], 'lastdeploy') + if self.site.config['COMMENT_SYSTEM_ID'] == 'nikolademo': + self.logger.warn("\nWARNING WARNING WARNING WARNING\n" + "You are deploying using the nikolademo Disqus account.\n" + "That means you will not be able to moderate the comments in your own site.\n" + "And is probably not what you want to do.\n" + "Think about it for 5 seconds, I'll wait :-)\n\n") + time.sleep(5) + + deploy_drafts = self.site.config.get('DEPLOY_DRAFTS', True) + deploy_future = self.site.config.get('DEPLOY_FUTURE', False) + if not (deploy_drafts and deploy_future): + # Remove drafts and future posts + out_dir = self.site.config['OUTPUT_FOLDER'] + undeployed_posts = [] + self.site.scan_posts() + for post in self.site.timeline: + if (not deploy_drafts and post.is_draft) or \ + (not deploy_future and post.publish_later): + remove_file(os.path.join(out_dir, post.destination_path())) + remove_file(os.path.join(out_dir, post.source_path)) + undeployed_posts.append(post) + + for command in self.site.config['DEPLOY_COMMANDS']: + self.logger.notice("==> {0}".format(command)) + try: + subprocess.check_call(command, shell=True) + except subprocess.CalledProcessError as e: + self.logger.error('Failed deployment — command {0} ' + 'returned {1}'.format(e.cmd, e.returncode)) + sys.exit(e.returncode) + + self.logger.notice("Successful deployment") + if self.site.config['TIMEZONE'] is not None: + tzinfo = pytz.timezone(self.site.config['TIMEZONE']) + else: + tzinfo = pytz.UTC + try: + with open(timestamp_path, 'rb') as inf: + last_deploy = literal_eval(inf.read().strip()) + # this might ignore DST + last_deploy = last_deploy.replace(tzinfo=tzinfo) + clean = False + except Exception: + last_deploy = datetime(1970, 1, 1).replace(tzinfo=tzinfo) + clean = True + + new_deploy = datetime.now() + 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)) + + def _emit_deploy_event(self, last_deploy, new_deploy, clean=False, undeployed=None): + """ Emit events for all timeline entries newer than last deploy. + + last_deploy: datetime + Time stamp of the last successful deployment. + + new_deploy: datetime + Time stamp of the current deployment. + + clean: bool + True when it appears like deploy is being run after a clean. + + """ + + if undeployed is None: + undeployed = [] + + event = { + 'last_deploy': last_deploy, + 'new_deploy': new_deploy, + 'clean': clean, + 'undeployed': undeployed + } + + deployed = [ + entry for entry in self.site.timeline + if entry.date > last_deploy and entry not in undeployed + ] + + event['deployed'] = deployed + + if len(deployed) > 0 or len(undeployed) > 0: + signal('deployed').send(event) diff --git a/nikola/plugins/command/import_blogger.plugin b/nikola/plugins/command/import_blogger.plugin new file mode 100644 index 0000000..91a7cb6 --- /dev/null +++ b/nikola/plugins/command/import_blogger.plugin @@ -0,0 +1,10 @@ +[Core] +Name = import_blogger +Module = import_blogger + +[Documentation] +Author = Roberto Alsina +Version = 0.2 +Website = http://getnikola.com +Description = Import a blogger site from a XML dump. + diff --git a/nikola/plugins/command/import_blogger.py b/nikola/plugins/command/import_blogger.py new file mode 100644 index 0000000..53618b4 --- /dev/null +++ b/nikola/plugins/command/import_blogger.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 +import datetime +import os +import time + +try: + from urlparse import urlparse +except ImportError: + from urllib.parse import urlparse # NOQA + +try: + import feedparser +except ImportError: + feedparser = None # NOQA + +from nikola.plugin_categories import Command +from nikola import utils +from nikola.utils import req_missing +from nikola.plugins.basic_import import ImportMixin + +LOGGER = utils.get_logger('import_blogger', utils.STDERR_HANDLER) + + +class CommandImportBlogger(Command, ImportMixin): + """Import a blogger dump.""" + + name = "import_blogger" + needs_config = False + doc_usage = "[options] blogger_export_file" + doc_purpose = "import a blogger dump" + cmd_options = ImportMixin.cmd_options + [ + { + 'name': 'exclude_drafts', + 'long': 'no-drafts', + 'short': 'd', + 'default': False, + 'type': bool, + 'help': "Don't import drafts", + }, + ] + + def _execute(self, options, args): + """Import a Blogger blog from an export file into a Nikola site.""" + # Parse the data + if feedparser is None: + req_missing(['feedparser'], 'import Blogger dumps') + return + + if not args: + print(self.help()) + return + + options['filename'] = args[0] + self.blogger_export_file = options['filename'] + self.output_folder = options['output_folder'] + self.import_into_existing_site = False + self.exclude_drafts = options['exclude_drafts'] + self.url_map = {} + channel = self.get_channel_from_file(self.blogger_export_file) + self.context = self.populate_context(channel) + conf_template = self.generate_base_site() + self.context['REDIRECTIONS'] = self.configure_redirections( + self.url_map) + + self.import_posts(channel) + self.write_urlmap_csv( + os.path.join(self.output_folder, 'url_map.csv'), self.url_map) + + 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) + + @classmethod + def get_channel_from_file(cls, filename): + if not os.path.isfile(filename): + raise Exception("Missing file: %s" % filename) + return feedparser.parse(filename) + + @staticmethod + def populate_context(channel): + # may need changes when the template conf.py.in changes + context = {} + context['DEFAULT_LANG'] = 'en' # blogger doesn't include the language + # in the dump + context['BLOG_TITLE'] = channel.feed.title + + context['BLOG_DESCRIPTION'] = '' # Missing in the dump + context['SITE_URL'] = channel.feed.link + context['BLOG_EMAIL'] = channel.feed.author_detail.email + context['BLOG_AUTHOR'] = channel.feed.author_detail.name + context['POSTS'] = '''( + ("posts/*.txt", "posts", "post.tmpl"), + ("posts/*.rst", "posts", "post.tmpl"), + ("posts/*.html", "posts", "post.tmpl"), + )''' + context['PAGES'] = '''( + ("articles/*.txt", "articles", "story.tmpl"), + ("articles/*.rst", "articles", "story.tmpl"), + )''' + context['COMPILERS'] = '''{ + "rest": ('.txt', '.rst'), + "markdown": ('.md', '.mdown', '.markdown', '.wp'), + "html": ('.html', '.htm') + } + ''' + context['THEME'] = 'bootstrap3' + + return context + + def import_item(self, item, out_folder=None): + """Takes an item from the feed and creates a post file.""" + if out_folder is None: + out_folder = 'posts' + + # 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 = item.link + link_path = urlparse(link).path + + title = item.title + + # blogger supports empty titles, which Nikola doesn't + if not title: + LOGGER.warn("Empty title in post with URL {0}. Using NO_TITLE " + "as placeholder, please fix.".format(link)) + title = "NO_TITLE" + + if link_path.lower().endswith('.html'): + link_path = link_path[:-5] + + slug = utils.slugify(link_path) + + if not slug: # should never happen + LOGGER.error("Error converting post:", title) + return + + description = '' + post_date = datetime.datetime.fromtimestamp(time.mktime( + item.published_parsed)) + + for candidate in item.content: + if candidate.type == 'text/html': + content = candidate.value + break + # FIXME: handle attachments + + tags = [] + for tag in item.tags: + if tag.scheme == 'http://www.blogger.com/atom/ns#': + tags.append(tag.term) + + if item.get('app_draft'): + tags.append('draft') + is_draft = True + else: + is_draft = False + + self.url_map[link] = self.context['SITE_URL'] + '/' + \ + out_folder + '/' + slug + '.html' + + 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. + 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 + '.html'), + content) + else: + LOGGER.warn('Not going to import "{0}" because it seems to contain' + ' no content.'.format(title)) + + def process_item(self, item): + post_type = item.tags[0].term + + if post_type == 'http://schemas.google.com/blogger/2008/kind#post': + self.import_item(item, 'posts') + elif post_type == 'http://schemas.google.com/blogger/2008/kind#page': + self.import_item(item, 'stories') + elif post_type == ('http://schemas.google.com/blogger/2008/kind' + '#settings'): + # Ignore settings + pass + elif post_type == ('http://schemas.google.com/blogger/2008/kind' + '#template'): + # Ignore template + pass + elif post_type == ('http://schemas.google.com/blogger/2008/kind' + '#comment'): + # FIXME: not importing comments. Does blogger support "pages"? + pass + else: + LOGGER.warn("Unknown post_type:", post_type) + + def import_posts(self, channel): + for item in channel.entries: + self.process_item(item) diff --git a/nikola/plugins/command/import_feed.plugin b/nikola/plugins/command/import_feed.plugin new file mode 100644 index 0000000..26e570a --- /dev/null +++ b/nikola/plugins/command/import_feed.plugin @@ -0,0 +1,10 @@ +[Core] +Name = import_feed +Module = import_feed + +[Documentation] +Author = Grzegorz Śliwiński +Version = 0.1 +Website = http://www.fizyk.net.pl/ +Description = Import a blog posts from a RSS/Atom dump + diff --git a/nikola/plugins/command/import_feed.py b/nikola/plugins/command/import_feed.py new file mode 100644 index 0000000..b25d9ec --- /dev/null +++ b/nikola/plugins/command/import_feed.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 +import datetime +import os +import time + +try: + from urlparse import urlparse +except ImportError: + from urllib.parse import urlparse # NOQA + +try: + import feedparser +except ImportError: + feedparser = None # NOQA + +from nikola.plugin_categories import Command +from nikola import utils +from nikola.utils import req_missing +from nikola.plugins.basic_import import ImportMixin + +LOGGER = utils.get_logger('import_feed', utils.STDERR_HANDLER) + + +class CommandImportFeed(Command, ImportMixin): + """Import a feed dump.""" + + name = "import_feed" + needs_config = False + doc_usage = "[options] feed_file" + doc_purpose = "import a RSS/Atom dump" + cmd_options = ImportMixin.cmd_options + + def _execute(self, options, args): + ''' + Import Atom/RSS feed + ''' + if feedparser is None: + req_missing(['feedparser'], 'import feeds') + return + + if not args: + print(self.help()) + return + + options['filename'] = args[0] + self.feed_export_file = options['filename'] + self.output_folder = options['output_folder'] + self.import_into_existing_site = False + self.url_map = {} + channel = self.get_channel_from_file(self.feed_export_file) + self.context = self.populate_context(channel) + conf_template = self.generate_base_site() + self.context['REDIRECTIONS'] = self.configure_redirections( + self.url_map) + + self.import_posts(channel) + + self.write_configuration(self.get_configuration_output_path( + ), conf_template.render(**self.context)) + + @classmethod + def get_channel_from_file(cls, filename): + return feedparser.parse(filename) + + @staticmethod + def populate_context(channel): + context = {} + context['DEFAULT_LANG'] = channel.feed.title_detail.language \ + if channel.feed.title_detail.language else 'en' + context['BLOG_TITLE'] = channel.feed.title + + context['BLOG_DESCRIPTION'] = channel.feed.get('subtitle', '') + context['SITE_URL'] = channel.feed.get('link', '').rstrip('/') + 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['COMPILERS'] = '''{ + "rest": ('.txt', '.rst'), + "markdown": ('.md', '.mdown', '.markdown', '.wp'), + "html": ('.html', '.htm') + } + ''' + + return context + + def import_posts(self, channel): + for item in channel.entries: + self.process_item(item) + + def process_item(self, item): + self.import_item(item, 'posts') + + def import_item(self, item, out_folder=None): + """Takes an item from the feed and creates a post file.""" + if out_folder is None: + out_folder = 'posts' + + # 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 = item.link + link_path = urlparse(link).path + + title = item.title + + # blogger supports empty titles, which Nikola doesn't + if not title: + LOGGER.warn("Empty title in post with URL {0}. Using NO_TITLE " + "as placeholder, please fix.".format(link)) + title = "NO_TITLE" + + if link_path.lower().endswith('.html'): + link_path = link_path[:-5] + + slug = utils.slugify(link_path) + + if not slug: # should never happen + LOGGER.error("Error converting post:", title) + return + + description = '' + post_date = datetime.datetime.fromtimestamp(time.mktime( + item.published_parsed)) + if item.get('content'): + for candidate in item.get('content', []): + content = candidate.value + break + # FIXME: handle attachments + elif item.get('summary'): + content = item.get('summary') + + tags = [] + for tag in item.get('tags', []): + tags.append(tag.term) + + if item.get('app_draft'): + tags.append('draft') + is_draft = True + else: + is_draft = False + + self.url_map[link] = self.context['SITE_URL'] + '/' + \ + out_folder + '/' + slug + '.html' + + 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. + 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 + '.html'), + content) + else: + LOGGER.warn('Not going to import "{0}" because it seems to contain' + ' no content.'.format(title)) + + @staticmethod + def write_metadata(filename, title, slug, post_date, description, tags): + ImportMixin.write_metadata(filename, + title, + slug, + post_date.strftime(r'%Y/%m/%d %H:%m:%S'), + description, + tags) diff --git a/nikola/plugins/command/import_wordpress.plugin b/nikola/plugins/command/import_wordpress.plugin new file mode 100644 index 0000000..fadc759 --- /dev/null +++ b/nikola/plugins/command/import_wordpress.plugin @@ -0,0 +1,10 @@ +[Core] +Name = import_wordpress +Module = import_wordpress + +[Documentation] +Author = Roberto Alsina +Version = 0.2 +Website = http://getnikola.com +Description = Import a wordpress site from a XML dump (requires markdown). + diff --git a/nikola/plugins/command/import_wordpress.py b/nikola/plugins/command/import_wordpress.py new file mode 100644 index 0000000..4f32198 --- /dev/null +++ b/nikola/plugins/command/import_wordpress.py @@ -0,0 +1,443 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 +import os +import re +import sys +from lxml import etree + +try: + from urlparse import urlparse + from urllib import unquote +except ImportError: + from urllib.parse import urlparse, unquote # NOQA + +try: + import requests +except ImportError: + requests = None # NOQA + +try: + import phpserialize +except ImportError: + phpserialize = None # NOQA + +from nikola.plugin_categories import Command +from nikola import utils +from nikola.utils import req_missing +from nikola.plugins.basic_import import ImportMixin, links + +LOGGER = utils.get_logger('import_wordpress', utils.STDERR_HANDLER) + + +class CommandImportWordpress(Command, ImportMixin): + """Import a WordPress dump.""" + + name = "import_wordpress" + needs_config = False + doc_usage = "[options] wordpress_export_file" + doc_purpose = "import a WordPress dump" + cmd_options = ImportMixin.cmd_options + [ + { + 'name': 'exclude_drafts', + 'long': 'no-drafts', + 'short': 'd', + 'default': False, + 'type': bool, + 'help': "Don't import drafts", + }, + { + 'name': 'squash_newlines', + 'long': 'squash-newlines', + 'default': False, + 'type': bool, + 'help': "Shorten multiple newlines in a row to only two newlines", + }, + { + 'name': 'no_downloads', + 'long': 'no-downloads', + 'default': False, + 'type': bool, + 'help': "Do not try to download files for the import", + }, + ] + + def _execute(self, options={}, args=[]): + """Import a WordPress blog from an export file into a Nikola site.""" + if not args: + print(self.help()) + return + + options['filename'] = args.pop(0) + + if args and ('output_folder' not in args or + options['output_folder'] == 'new_site'): + options['output_folder'] = args.pop(0) + + if args: + LOGGER.warn('You specified additional arguments ({0}). Please consider ' + 'putting these arguments before the filename if you ' + 'are running into problems.'.format(args)) + + self.import_into_existing_site = False + self.url_map = {} + self.timezone = None + + self.wordpress_export_file = options['filename'] + self.squash_newlines = options.get('squash_newlines', False) + self.output_folder = options.get('output_folder', 'new_site') + + self.exclude_drafts = options.get('exclude_drafts', False) + self.no_downloads = options.get('no_downloads', False) + + if not self.no_downloads: + def show_info_about_mising_module(modulename): + LOGGER.error( + 'To use the "{commandname}" command, you have to install ' + 'the "{package}" package or supply the "--no-downloads" ' + 'option.'.format( + commandname=self.name, + package=modulename) + ) + + if requests is None and phpserialize is None: + req_missing(['requests', 'phpserialize'], 'import WordPress dumps without --no-downloads') + elif requests is None: + req_missing(['requests'], 'import WordPress dumps without --no-downloads') + elif phpserialize is None: + req_missing(['phpserialize'], 'import WordPress dumps without --no-downloads') + + channel = self.get_channel_from_file(self.wordpress_export_file) + self.context = self.populate_context(channel) + conf_template = self.generate_base_site() + + 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 = re.sub('# REDIRECTIONS = ', 'REDIRECTIONS = ', + rendered_template) + if self.timezone: + rendered_template = re.sub('# TIMEZONE = \'Europe/Zurich\'', + 'TIMEZONE = \'' + self.timezone + '\'', + rendered_template) + self.write_configuration(self.get_configuration_output_path(), + rendered_template) + + @classmethod + def _glue_xml_lines(cls, xml): + new_xml = xml[0] + previous_line_ended_in_newline = new_xml.endswith(b'\n') + previous_line_was_indentet = False + for line in xml[1:]: + if (re.match(b'^[ \t]+', line) and previous_line_ended_in_newline): + new_xml = b''.join((new_xml, line)) + previous_line_was_indentet = True + elif previous_line_was_indentet: + new_xml = b''.join((new_xml, line)) + previous_line_was_indentet = False + else: + new_xml = b'\n'.join((new_xml, line)) + previous_line_was_indentet = False + + previous_line_ended_in_newline = line.endswith(b'\n') + + return new_xml + + @classmethod + def read_xml_file(cls, filename): + xml = [] + + with open(filename, 'rb') as fd: + for line in fd: + # These explode etree and are useless + if b' {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 + links[url] = '/' + dst_url + + self.download_additional_image_sizes( + item, + wordpress_namespace, + os.path.dirname(url) + ) + + def download_additional_image_sizes(self, item, wordpress_namespace, source_path): + if phpserialize is None: + return + + additional_metadata = item.findall('{{{0}}}postmeta'.format(wordpress_namespace)) + + if additional_metadata is None: + return + + for element in additional_metadata: + meta_key = element.find('{{{0}}}meta_key'.format(wordpress_namespace)) + if meta_key is not None and meta_key.text == '_wp_attachment_metadata': + meta_value = element.find('{{{0}}}meta_value'.format(wordpress_namespace)) + + if meta_value is None: + continue + + # Someone from Wordpress thought it was a good idea + # serialize PHP objects into that metadata field. Given + # that the export should give you the power to insert + # 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) + size_key = 'sizes' + file_key = 'file' + else: + metadata = phpserialize.loads(meta_value.text.encode('UTF-8')) + size_key = b'sizes' + file_key = b'file' + + if not size_key in metadata: + continue + + for filename in [metadata[size_key][size][file_key] for size in metadata[size_key]]: + url = '/'.join([source_path, filename.decode('utf-8')]) + + path = urlparse(url).path + dst_path = os.path.join(*([self.output_folder, 'files'] + + list(path.split('/')))) + dst_dir = os.path.dirname(dst_path) + utils.makedirs(dst_dir) + LOGGER.notice("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 + links[url] = '/' + dst_url + + @staticmethod + def transform_sourcecode(content): + new_content = re.sub('\[sourcecode language="([^"]+)"\]', + "\n~~~~~~~~~~~~{.\\1}\n", content) + new_content = new_content.replace('[/sourcecode]', + "\n~~~~~~~~~~~~\n") + return new_content + + @staticmethod + def transform_caption(content): + new_caption = re.sub(r'\[/caption\]', '', content) + new_caption = re.sub(r'\[caption.*\]', '', new_caption) + + return new_caption + + def transform_multiple_newlines(self, content): + """Replaces multiple newlines with only two.""" + if self.squash_newlines: + return re.sub(r'\n{3,}', r'\n\n', content) + else: + return content + + def transform_content(self, content): + new_content = self.transform_sourcecode(content) + new_content = self.transform_caption(new_content) + new_content = self.transform_multiple_newlines(new_content) + return new_content + + def import_item(self, item, wordpress_namespace, out_folder=None): + """Takes an item from the feed and creates a post file.""" + if out_folder is None: + out_folder = 'posts' + + title = get_text_tag(item, 'title', 'NO TITLE') + # 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) + + # 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) + if not slug: # it happens if the post has no "nice" URL + slug = get_text_tag( + item, '{{{0}}}post_name'.format(wordpress_namespace), None) + if not slug: # it *may* happen + slug = get_text_tag( + item, '{{{0}}}post_id'.format(wordpress_namespace), None) + if not slug: # should never happen + LOGGER.error("Error converting post:", title) + return + + description = get_text_tag(item, 'description', '') + post_date = get_text_tag( + item, '{{{0}}}post_date'.format(wordpress_namespace), None) + dt = utils.to_datetime(post_date) + if dt.tzinfo and self.timezone is None: + self.timezone = utils.get_tzname(dt) + status = get_text_tag( + item, '{{{0}}}status'.format(wordpress_namespace), 'publish') + content = get_text_tag( + item, '{http://purl.org/rss/1.0/modules/content/}encoded', '') + + tags = [] + if status == 'trash': + LOGGER.warn('Trashed post "{0}" will not be imported.'.format(title)) + return + elif status != 'publish': + tags.append('draft') + is_draft = True + else: + is_draft = False + + for tag in item.findall('category'): + text = tag.text + if text == 'Uncategorized': + continue + tags.append(text) + + 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) + else: + LOGGER.warn('Not going to import "{0}" because it seems to contain' + ' no content.'.format(title)) + + def process_item(self, item): + # The namespace usually is something like: + # http://wordpress.org/export/1.2/ + wordpress_namespace = item.nsmap['wp'] + post_type = get_text_tag( + item, '{{{0}}}post_type'.format(wordpress_namespace), 'post') + + if post_type == 'attachment': + self.import_attachment(item, wordpress_namespace) + elif post_type == 'post': + self.import_item(item, wordpress_namespace, 'posts') + else: + self.import_item(item, wordpress_namespace, 'stories') + + def import_posts(self, channel): + for item in channel.findall('item'): + self.process_item(item) + + +def get_text_tag(tag, name, default): + if tag is None: + return default + t = tag.find(name) + if t is not None: + return t.text + else: + return default diff --git a/nikola/plugins/command/init.plugin b/nikola/plugins/command/init.plugin new file mode 100644 index 0000000..a539f51 --- /dev/null +++ b/nikola/plugins/command/init.plugin @@ -0,0 +1,9 @@ +[Core] +Name = init +Module = init + +[Documentation] +Author = Roberto Alsina +Version = 0.2 +Website = http://getnikola.com +Description = Create a new site. diff --git a/nikola/plugins/command/init.py b/nikola/plugins/command/init.py new file mode 100644 index 0000000..1873ec4 --- /dev/null +++ b/nikola/plugins/command/init.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 print_function +import os +import shutil +import codecs + +from mako.template import Template + +import nikola +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) + + +class CommandInit(Command): + """Create a new site.""" + + name = "init" + + doc_usage = "[--demo] folder" + needs_config = False + doc_purpose = "create a Nikola site in the specified folder" + cmd_options = [ + { + 'name': 'demo', + 'long': 'demo', + 'default': False, + 'type': bool, + 'help': "Create a site filled with example data.", + } + ] + + 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() + src = os.path.join(lib_path, 'data', 'samplesite') + shutil.copytree(src, target) + fix_git_symlinked(src, target) + + @classmethod + def create_configuration(cls, target): + lib_path = cls.get_path_to_nikola_modules() + template_path = os.path.join(lib_path, 'conf.py.in') + 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)) + + @classmethod + def create_empty_site(cls, target): + for folder in ('files', 'galleries', 'listings', 'posts', 'stories'): + makedirs(os.path.join(target, folder)) + + @staticmethod + def get_path_to_nikola_modules(): + return os.path.dirname(nikola.__file__) + + def _execute(self, options={}, args=None): + """Create a new site.""" + if not args: + print("Usage: nikola init folder [options]") + return False + target = args[0] + if target is None: + print(self.usage) + 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) diff --git a/nikola/plugins/command/install_plugin.plugin b/nikola/plugins/command/install_plugin.plugin new file mode 100644 index 0000000..3dbabd8 --- /dev/null +++ b/nikola/plugins/command/install_plugin.plugin @@ -0,0 +1,10 @@ +[Core] +Name = install_plugin +Module = install_plugin + +[Documentation] +Author = Roberto Alsina and Chris Warrick +Version = 0.1 +Website = http://getnikola.com +Description = Install a plugin into the current site. + diff --git a/nikola/plugins/command/install_plugin.py b/nikola/plugins/command/install_plugin.py new file mode 100644 index 0000000..fdbd0b7 --- /dev/null +++ b/nikola/plugins/command/install_plugin.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 print_function +import codecs +import os +import json +import shutil +import subprocess +from io import BytesIO + +import pygments +from pygments.lexers import PythonLexer +from pygments.formatters import TerminalFormatter + +try: + import requests +except ImportError: + requests = None # NOQA + +from nikola.plugin_categories import Command +from nikola import utils + +LOGGER = utils.get_logger('install_plugin', utils.STDERR_HANDLER) + + +# Stolen from textwrap in Python 3.3.2. +def indent(text, prefix, predicate=None): # NOQA + """Adds 'prefix' to the beginning of selected lines in 'text'. + + If 'predicate' is provided, 'prefix' will only be added to the lines + where 'predicate(line)' is True. If 'predicate' is not provided, + it will default to adding 'prefix' to all non-empty lines that do not + consist solely of whitespace characters. + """ + if predicate is None: + def predicate(line): + return line.strip() + + def prefixed_lines(): + for line in text.splitlines(True): + yield (prefix + line if predicate(line) else line) + return ''.join(prefixed_lines()) + + +class CommandInstallPlugin(Command): + """Install a plugin.""" + + name = "install_plugin" + doc_usage = "[[-u] plugin_name] | [[-u] -l]" + doc_purpose = "install plugin into current site" + output_dir = 'plugins' + cmd_options = [ + { + 'name': 'list', + 'short': 'l', + 'long': 'list', + 'type': bool, + 'default': False, + 'help': 'Show list of available plugins.' + }, + { + 'name': 'url', + 'short': 'u', + 'long': 'url', + 'type': str, + 'help': "URL for the plugin repository (default: " + "http://plugins.getnikola.com/v6/plugins.json)", + 'default': 'http://plugins.getnikola.com/v6/plugins.json' + }, + ] + + def _execute(self, options, args): + """Install plugin into current site.""" + if requests is None: + utils.req_missing(['requests'], 'install plugins') + + listing = options['list'] + url = options['url'] + if args: + name = args[0] + else: + name = None + + if name is None and not listing: + LOGGER.error("This command needs either a plugin name or the -l option.") + return False + data = requests.get(url).text + data = json.loads(data) + if listing: + print("Plugins:") + print("--------") + for plugin in sorted(data.keys()): + print(plugin) + return True + else: + self.do_install(name, data) + + def do_install(self, name, data): + if name in data: + utils.makedirs(self.output_dir) + LOGGER.notice('Downloading: ' + data[name]) + zip_file = BytesIO() + zip_file.write(requests.get(data[name]).content) + LOGGER.notice('Extracting: {0} into plugins'.format(name)) + utils.extract_all(zip_file, 'plugins') + dest_path = os.path.join('plugins', name) + else: + try: + plugin_path = utils.get_plugin_path(name) + except: + LOGGER.error("Can't find plugin " + name) + return False + + utils.makedirs(self.output_dir) + dest_path = os.path.join(self.output_dir, name) + if os.path.exists(dest_path): + LOGGER.error("{0} is already installed".format(name)) + return False + + LOGGER.notice('Copying {0} into plugins'.format(plugin_path)) + shutil.copytree(plugin_path, dest_path) + + reqpath = os.path.join(dest_path, 'requirements.txt') + print(reqpath) + if os.path.exists(reqpath): + LOGGER.notice('This plugin has Python dependencies.') + LOGGER.notice('Installing dependencies with pip...') + try: + subprocess.check_call(('pip', 'install', '-r', reqpath)) + except subprocess.CalledProcessError: + LOGGER.error('Could not install the dependencies.') + print('Contents of the requirements.txt file:\n') + with codecs.open(reqpath, 'rb', 'utf-8') as fh: + print(indent(fh.read(), 4 * ' ')) + print('You have to install those yourself or through a ' + 'package manager.') + else: + LOGGER.notice('Dependency installation succeeded.') + reqnpypath = os.path.join(dest_path, 'requirements-nonpy.txt') + if os.path.exists(reqnpypath): + LOGGER.notice('This plugin has third-party ' + 'dependencies you need to install ' + 'manually.') + print('Contents of the requirements-nonpy.txt file:\n') + with codecs.open(reqnpypath, 'rb', 'utf-8') as fh: + for l in fh.readlines(): + i, j = l.split('::') + print(indent(i.strip(), 4 * ' ')) + print(indent(j.strip(), 8 * ' ')) + print() + + print('You have to install those yourself or through a package ' + 'manager.') + confpypath = os.path.join(dest_path, 'conf.py.sample') + if os.path.exists(confpypath): + LOGGER.notice('This plugin has a sample config file.') + print('Contents of the conf.py.sample file:\n') + with codecs.open(confpypath, 'rb', 'utf-8') as fh: + print(indent(pygments.highlight( + fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' ')) + return True diff --git a/nikola/plugins/command/install_theme.plugin b/nikola/plugins/command/install_theme.plugin new file mode 100644 index 0000000..84b2623 --- /dev/null +++ b/nikola/plugins/command/install_theme.plugin @@ -0,0 +1,10 @@ +[Core] +Name = install_theme +Module = install_theme + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Install a theme into the current site. + diff --git a/nikola/plugins/command/install_theme.py b/nikola/plugins/command/install_theme.py new file mode 100644 index 0000000..a9d835a --- /dev/null +++ b/nikola/plugins/command/install_theme.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 print_function +import os +import json +import shutil +import codecs +from io import BytesIO + +import pygments +from pygments.lexers import PythonLexer +from pygments.formatters import TerminalFormatter + +try: + import requests +except ImportError: + requests = None # NOQA + +from nikola.plugin_categories import Command +from nikola import utils + +LOGGER = utils.get_logger('install_theme', utils.STDERR_HANDLER) + + +# Stolen from textwrap in Python 3.3.2. +def indent(text, prefix, predicate=None): # NOQA + """Adds 'prefix' to the beginning of selected lines in 'text'. + + If 'predicate' is provided, 'prefix' will only be added to the lines + where 'predicate(line)' is True. If 'predicate' is not provided, + it will default to adding 'prefix' to all non-empty lines that do not + consist solely of whitespace characters. + """ + if predicate is None: + def predicate(line): + return line.strip() + + def prefixed_lines(): + for line in text.splitlines(True): + yield (prefix + line if predicate(line) else line) + return ''.join(prefixed_lines()) + + +class CommandInstallTheme(Command): + """Install a theme.""" + + name = "install_theme" + doc_usage = "[[-u] theme_name] | [[-u] -l]" + doc_purpose = "install theme into current site" + output_dir = 'themes' + cmd_options = [ + { + 'name': 'list', + 'short': 'l', + 'long': 'list', + 'type': bool, + 'default': False, + 'help': 'Show list of available themes.' + }, + { + 'name': 'url', + 'short': 'u', + 'long': 'url', + 'type': str, + 'help': "URL for the theme repository (default: " + "http://themes.getnikola.com/v6/themes.json)", + 'default': 'http://themes.getnikola.com/v6/themes.json' + }, + ] + + def _execute(self, options, args): + """Install theme into current site.""" + if requests is None: + utils.req_missing(['requests'], 'install themes') + + listing = options['list'] + url = options['url'] + if args: + name = args[0] + else: + name = None + + if name is None and not listing: + LOGGER.error("This command needs either a theme name or the -l option.") + return False + data = requests.get(url).text + data = json.loads(data) + if listing: + print("Themes:") + print("-------") + for theme in sorted(data.keys()): + print(theme) + return True + else: + self.do_install(name, data) + # See if the theme's parent is available. If not, install it + while True: + parent_name = utils.get_parent_theme_name(name) + if parent_name is None: + break + try: + utils.get_theme_path(parent_name) + break + except: # Not available + self.do_install(parent_name, data) + name = parent_name + + def do_install(self, name, data): + if name in data: + utils.makedirs(self.output_dir) + LOGGER.notice('Downloading: ' + data[name]) + zip_file = BytesIO() + zip_file.write(requests.get(data[name]).content) + LOGGER.notice('Extracting: {0} into themes'.format(name)) + utils.extract_all(zip_file) + dest_path = os.path.join('themes', name) + else: + try: + theme_path = utils.get_theme_path(name) + except: + LOGGER.error("Can't find theme " + name) + return False + + utils.makedirs(self.output_dir) + dest_path = os.path.join(self.output_dir, name) + if os.path.exists(dest_path): + LOGGER.error("{0} is already installed".format(name)) + return False + + LOGGER.notice('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.') + print('Contents of the conf.py.sample file:\n') + with codecs.open(confpypath, 'rb', 'utf-8') as fh: + print(indent(pygments.highlight( + fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' ')) + return True diff --git a/nikola/plugins/command/mincss.plugin b/nikola/plugins/command/mincss.plugin new file mode 100644 index 0000000..d394d06 --- /dev/null +++ b/nikola/plugins/command/mincss.plugin @@ -0,0 +1,10 @@ +[Core] +Name = mincss +Module = mincss + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Apply mincss to the generated site + diff --git a/nikola/plugins/command/mincss.py b/nikola/plugins/command/mincss.py new file mode 100644 index 0000000..5c9a7cb --- /dev/null +++ b/nikola/plugins/command/mincss.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 print_function, unicode_literals +import os +import sys + +try: + from mincss.processor import Processor +except ImportError: + Processor = None + +from nikola.plugin_categories import Command +from nikola.utils import req_missing, get_logger, STDERR_HANDLER + + +class CommandMincss(Command): + """Check the generated site.""" + name = "mincss" + + doc_usage = "" + doc_purpose = "apply mincss to the generated site" + + logger = get_logger('mincss', STDERR_HANDLER) + + def _execute(self, options, args): + """Apply mincss the generated site.""" + output_folder = self.site.config['OUTPUT_FOLDER'] + if Processor is None: + req_missing(['mincss'], 'use the "mincss" command') + return + + p = Processor(preserve_remote_urls=False) + urls = [] + css_files = {} + for root, dirs, files in os.walk(output_folder): + for f in files: + url = os.path.join(root, f) + if url.endswith('.css'): + fname = os.path.basename(url) + if fname in css_files: + self.logger.error("You have two CSS files with the same name and that confuses me.") + sys.exit(1) + css_files[fname] = url + if not f.endswith('.html'): + continue + urls.append(url) + p.process(*urls) + for inline in p.links: + fname = os.path.basename(inline.href) + with open(css_files[fname], 'wb+') as outf: + outf.write(inline.after) diff --git a/nikola/plugins/command/new_post.plugin b/nikola/plugins/command/new_post.plugin new file mode 100644 index 0000000..ec35c35 --- /dev/null +++ b/nikola/plugins/command/new_post.plugin @@ -0,0 +1,10 @@ +[Core] +Name = new_post +Module = new_post + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Create a new post. + diff --git a/nikola/plugins/command/new_post.py b/nikola/plugins/command/new_post.py new file mode 100644 index 0000000..ea0f3de --- /dev/null +++ b/nikola/plugins/command/new_post.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 +import codecs +import datetime +import os +import sys + +from blinker import signal + +from nikola.plugin_categories import Command +from nikola import utils + +LOGGER = utils.get_logger('new_post', utils.STDERR_HANDLER) + + +def filter_post_pages(compiler, is_post, compilers, post_pages): + """Given a compiler ("markdown", "rest"), and whether it's meant for + a post or a page, and compilers, return the correct entry from + post_pages.""" + + # First throw away all the post_pages with the wrong is_post + filtered = [entry for entry in post_pages if entry[3] == is_post] + + # These are the extensions supported by the required format + extensions = compilers[compiler] + + # Throw away the post_pages with the wrong extensions + filtered = [entry for entry in filtered if any([ext in entry[0] for ext in + extensions])] + + if not filtered: + 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)) + return filtered[0] + + +def get_default_compiler(is_post, compilers, post_pages): + """Given compilers and post_pages, return a reasonable + default compiler for this kind of post/page. + """ + + # First throw away all the post_pages with the wrong is_post + filtered = [entry for entry in post_pages if entry[3] == is_post] + + # Get extensions in filtered post_pages until one matches a compiler + for entry in filtered: + extension = os.path.splitext(entry[0])[-1] + for compiler, extensions in compilers.items(): + if extension in extensions: + return compiler + # No idea, back to default behaviour + return 'rest' + + +def get_date(schedule=False, rule=None, last_date=None, force_today=False): + """Returns a date stamp, given a recurrence rule. + + schedule - bool: + whether to use the recurrence rule or not + + rule - str: + an iCal RRULE string that specifies the rule for scheduling posts + + last_date - datetime: + timestamp of the last post + + force_today - bool: + tries to schedule a post to today, if possible, even if the scheduled + time has already passed in the day. + """ + + date = now = datetime.datetime.now() + if schedule: + try: + from dateutil import rrule + except ImportError: + LOGGER.error('To use the --schedule switch of new_post, ' + 'you have to install the "dateutil" package.') + rrule = None + if schedule and rrule and rule: + if last_date and last_date.tzinfo: + # strip tzinfo for comparisons + last_date = last_date.replace(tzinfo=None) + try: + rule_ = rrule.rrulestr(rule, dtstart=last_date) + except Exception: + LOGGER.error('Unable to parse rule string, using current time.') + else: + # Try to post today, instead of tomorrow, if no other post today. + if force_today: + now = now.replace(hour=0, minute=0, second=0, microsecond=0) + date = rule_.after(max(now, last_date or now), last_date is None) + return date.strftime('%Y/%m/%d %H:%M:%S') + + +class CommandNewPost(Command): + """Create a new post.""" + + name = "new_post" + doc_usage = "[options] [path]" + doc_purpose = "create a new blog post or site page" + cmd_options = [ + { + 'name': 'is_page', + 'short': 'p', + 'long': 'page', + 'type': bool, + 'default': False, + 'help': 'Create a page instead of a blog post.' + }, + { + 'name': 'title', + 'short': 't', + 'long': 'title', + 'type': str, + 'default': '', + 'help': 'Title for the page/post.' + }, + { + 'name': 'tags', + 'long': 'tags', + 'type': str, + 'default': '', + 'help': 'Comma-separated tags for the page/post.' + }, + { + 'name': 'onefile', + 'short': '1', + 'type': bool, + 'default': False, + 'help': 'Create post with embedded metadata (single file format)' + }, + { + 'name': 'twofile', + 'short': '2', + 'type': bool, + 'default': False, + 'help': 'Create post with separate metadata (two file format)' + }, + { + 'name': 'post_format', + 'short': 'f', + 'long': 'format', + 'type': str, + 'default': '', + 'help': 'Markup format for post, one of rest, markdown, wiki, ' + 'bbcode, html, textile, txt2tags', + }, + { + 'name': 'schedule', + 'short': 's', + 'type': bool, + 'default': False, + 'help': 'Schedule post based on recurrence rule' + }, + + ] + + def _execute(self, options, args): + """Create a new post or page.""" + compiler_names = [p.name for p in + self.site.plugin_manager.getPluginsOfCategory( + "PageCompiler")] + + if len(args) > 1: + print(self.help()) + return False + elif args: + path = args[0] + else: + path = None + + is_page = options.get('is_page', False) + is_post = not is_page + title = options['title'] or None + tags = options['tags'] + onefile = options['onefile'] + twofile = options['twofile'] + + if twofile: + onefile = False + if not onefile and not twofile: + onefile = self.site.config.get('ONE_FILE_POSTS', True) + + post_format = options['post_format'] + + if not post_format: # Issue #400 + post_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) + return + compiler_plugin = self.site.plugin_manager.getPluginByName( + post_format, "PageCompiler").plugin_object + + # Guess where we should put this + entry = filter_post_pages(post_format, is_post, + self.site.config['COMPILERS'], + self.site.config['post_pages']) + + print("Creating New Post") + print("-----------------\n") + if title is None: + print("Enter title: ", end='') + # WHY, PYTHON3???? WHY? + sys.stdout.flush() + title = sys.stdin.readline() + else: + print("Title:", title) + if isinstance(title, utils.bytes_str): + title = title.decode(sys.stdin.encoding) + title = title.strip() + if not path: + slug = utils.slugify(title) + else: + 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 + schedule = options['schedule'] or self.site.config['SCHEDULE_ALL'] + rule = self.site.config['SCHEDULE_RULE'] + force_today = self.site.config['SCHEDULE_FORCE_TODAY'] + self.site.scan_posts() + timeline = self.site.timeline + last_date = None if not timeline else timeline[0].date + date = get_date(schedule, rule, last_date, force_today) + data = [title, slug, date, tags] + output_path = os.path.dirname(entry[0]) + meta_path = os.path.join(output_path, slug + ".meta") + pattern = os.path.basename(entry[0]) + suffix = pattern[1:] + if not path: + txt_path = os.path.join(output_path, slug + suffix) + else: + txt_path = path + + if (not onefile and os.path.isfile(meta_path)) or \ + os.path.isfile(txt_path): + LOGGER.error("The title already exists!") + exit() + + d_name = os.path.dirname(txt_path) + utils.makedirs(d_name) + metadata = self.site.config['ADDITIONAL_METADATA'] + compiler_plugin.create_post( + txt_path, onefile, title=title, + slug=slug, date=date, tags=tags, **metadata) + + event = dict(path=txt_path) + + if not onefile: # write metadata file + 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)) + event['meta_path'] = meta_path + LOGGER.notice("Your post's text is at: {0}".format(txt_path)) + + signal('new_post').send(self, **event) diff --git a/nikola/plugins/command/planetoid.plugin b/nikola/plugins/command/planetoid.plugin new file mode 100644 index 0000000..e767f31 --- /dev/null +++ b/nikola/plugins/command/planetoid.plugin @@ -0,0 +1,9 @@ +[Core] +Name = planetoid +Module = planetoid + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Maintain a planet-like site diff --git a/nikola/plugins/command/planetoid/__init__.py b/nikola/plugins/command/planetoid/__init__.py new file mode 100644 index 0000000..369862b --- /dev/null +++ b/nikola/plugins/command/planetoid/__init__.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 print_function, unicode_literals +import codecs +import datetime +import hashlib +from optparse import OptionParser +import os +import sys + +from doit.tools import timeout +from nikola.plugin_categories import Command, Task +from nikola.utils import config_changed, req_missing, get_logger, STDERR_HANDLER + +LOGGER = get_logger('planetoid', STDERR_HANDLER) + +try: + import feedparser +except ImportError: + feedparser = None # NOQA + +try: + import peewee +except ImportError: + peewee = None + + +if peewee is not None: + class Feed(peewee.Model): + name = peewee.CharField() + url = peewee.CharField(max_length=200) + last_status = peewee.CharField(null=True) + etag = peewee.CharField(max_length=200) + last_modified = peewee.DateTimeField() + + class Entry(peewee.Model): + date = peewee.DateTimeField() + feed = peewee.ForeignKeyField(Feed) + content = peewee.TextField(max_length=20000) + link = peewee.CharField(max_length=200) + title = peewee.CharField(max_length=200) + guid = peewee.CharField(max_length=200) + + +class Planetoid(Command, Task): + """Maintain a planet-like thing.""" + name = "planetoid" + + def init_db(self): + # setup database + Feed.create_table(fail_silently=True) + Entry.create_table(fail_silently=True) + + def gen_tasks(self): + if peewee is None or sys.version_info[0] == 3: + if sys.version_info[0] == 3: + message = 'Peewee, a requirement of the "planetoid" command, is currently incompatible with Python 3.' + else: + req_missing('peewee', 'use the "planetoid" command') + message = '' + yield { + 'basename': self.name, + 'name': '', + 'verbosity': 2, + 'actions': ['echo "%s"' % message] + } + else: + self.init_db() + self.load_feeds() + for task in self.task_update_feeds(): + yield task + for task in self.task_generate_posts(): + yield task + yield { + 'basename': self.name, + 'name': '', + 'actions': [], + 'file_dep': ['feeds'], + 'task_dep': [ + self.name + "_fetch_feed", + self.name + "_generate_posts", + ] + } + + def run(self, *args): + parser = OptionParser(usage="nikola %s [options]" % self.name) + (options, args) = parser.parse_args(list(args)) + + def load_feeds(self): + "Read the feeds file, add it to the database." + feeds = [] + feed = name = None + for line in codecs.open('feeds', 'r', 'utf-8'): + line = line.strip() + if line.startswith("#"): + continue + elif line.startswith('http'): + feed = line + elif line: + name = line + if feed and name: + feeds.append([feed, name]) + feed = name = None + + def add_feed(name, url): + f = Feed.create( + name=name, + url=url, + etag='foo', + last_modified=datetime.datetime(1970, 1, 1), + ) + f.save() + + def update_feed_url(feed, url): + feed.url = url + feed.save() + + for feed, name in feeds: + f = Feed.select().where(Feed.name == name) + if not list(f): + add_feed(name, feed) + elif list(f)[0].url != feed: + update_feed_url(list(f)[0], feed) + + def task_update_feeds(self): + """Download feed contents, add entries to the database.""" + def update_feed(feed): + modified = feed.last_modified.timetuple() + etag = feed.etag + try: + parsed = feedparser.parse( + feed.url, + etag=etag, + modified=modified + ) + feed.last_status = str(parsed.status) + except: # Probably a timeout + # TODO: log failure + return + if parsed.feed.get('title'): + LOGGER.notice(parsed.feed.title) + else: + LOGGER.notice(feed.url) + feed.etag = parsed.get('etag', 'foo') + modified = tuple(parsed.get('date_parsed', (1970, 1, 1)))[:6] + LOGGER.notice("==========>", modified) + modified = datetime.datetime(*modified) + feed.last_modified = modified + feed.save() + # No point in adding items from missinfg feeds + if parsed.status > 400: + # TODO log failure + return + for entry_data in parsed.entries: + LOGGER.notice("=========================================") + 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) + date = datetime.datetime(*(date[:6])) + title = "%s: %s" % (feed.name, entry_data.get('title', 'Sin título')) + content = entry_data.get('content', None) + if content: + content = content[0].value + if not content: + content = entry_data.get('description', None) + if not content: + 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])) + e = list(Entry.select().where(Entry.guid == guid)) + LOGGER.notice( + repr(dict( + date=date, + title=title, + content=content, + guid=guid, + feed=feed, + link=link, + )) + ) + if not e: + entry = Entry.create( + date=date, + title=title, + content=content, + guid=guid, + feed=feed, + link=link, + ) + else: + entry = e[0] + entry.date = date + entry.title = title + entry.content = content + entry.link = link + entry.save() + flag = False + for feed in Feed.select(): + flag = True + task = { + 'basename': self.name + "_fetch_feed", + 'name': str(feed.url), + 'actions': [(update_feed, (feed, ))], + 'uptodate': [timeout(datetime.timedelta(minutes= + self.site.config.get('PLANETOID_REFRESH', 60)))], + } + yield task + if not flag: + yield { + 'basename': self.name + "_fetch_feed", + 'name': '', + 'actions': [], + } + + def task_generate_posts(self): + """Generate post files for the blog entries.""" + def gen_id(entry): + h = hashlib.md5() + h.update(entry.feed.name.encode('utf8')) + h.update(entry.guid) + return h.hexdigest() + + def generate_post(entry): + unique_id = gen_id(entry) + meta_path = os.path.join('posts', unique_id + '.meta') + post_path = os.path.join('posts', unique_id + '.txt') + with codecs.open(meta_path, 'wb+', 'utf8') as fd: + fd.write('%s\n' % entry.title.replace('\n', ' ')) + fd.write('%s\n' % unique_id) + fd.write('%s\n' % entry.date.strftime('%Y/%m/%d %H:%M')) + fd.write('\n') + fd.write('%s\n' % entry.link) + with codecs.open(post_path, 'wb+', 'utf8') as fd: + fd.write('.. raw:: html\n\n') + content = entry.content + if not content: + content = 'Sin contenido' + for line in content.splitlines(): + fd.write(' %s\n' % line) + + if not os.path.isdir('posts'): + os.mkdir('posts') + flag = False + for entry in Entry.select().order_by(Entry.date.desc()): + flag = True + entry_id = gen_id(entry) + yield { + 'basename': self.name + "_generate_posts", + 'targets': [os.path.join('posts', entry_id + '.meta'), os.path.join('posts', entry_id + '.txt')], + 'name': entry_id, + 'actions': [(generate_post, (entry,))], + 'uptodate': [config_changed({1: entry})], + 'task_dep': [self.name + "_fetch_feed"], + } + if not flag: + yield { + 'basename': self.name + "_generate_posts", + 'name': '', + 'actions': [], + } diff --git a/nikola/plugins/command/serve.plugin b/nikola/plugins/command/serve.plugin new file mode 100644 index 0000000..e663cc6 --- /dev/null +++ b/nikola/plugins/command/serve.plugin @@ -0,0 +1,10 @@ +[Core] +Name = serve +Module = serve + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Start test server. + diff --git a/nikola/plugins/command/serve.py b/nikola/plugins/command/serve.py new file mode 100644 index 0000000..07403d4 --- /dev/null +++ b/nikola/plugins/command/serve.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 print_function +import os +try: + from BaseHTTPServer import HTTPServer + from SimpleHTTPServer import SimpleHTTPRequestHandler +except ImportError: + from http.server import HTTPServer # NOQA + from http.server import SimpleHTTPRequestHandler # NOQA + +from nikola.plugin_categories import Command +from nikola.utils import get_logger + + +class CommandBuild(Command): + """Start test server.""" + + name = "serve" + doc_usage = "[options]" + doc_purpose = "start the test webserver" + logger = None + + cmd_options = ( + { + 'name': 'port', + 'short': 'p', + 'long': 'port', + 'default': 8000, + 'type': int, + 'help': 'Port nummber (default: 8000)', + }, + { + 'name': 'address', + 'short': 'a', + 'long': '--address', + 'type': str, + 'default': '127.0.0.1', + 'help': 'Address to bind (default: 127.0.0.1)', + }, + ) + + def _execute(self, options, args): + """Start test server.""" + self.logger = get_logger('serve', self.site.loghandlers) + out_dir = self.site.config['OUTPUT_FOLDER'] + if not os.path.isdir(out_dir): + self.logger.error("Missing '{0}' folder?".format(out_dir)) + else: + os.chdir(out_dir) + httpd = HTTPServer((options['address'], options['port']), + OurHTTPRequestHandler) + sa = httpd.socket.getsockname() + self.logger.notice("Serving HTTP on {0} port {1} ...".format(*sa)) + httpd.serve_forever() + + +class OurHTTPRequestHandler(SimpleHTTPRequestHandler): + extensions_map = dict(SimpleHTTPRequestHandler.extensions_map) + extensions_map[""] = "text/plain" + + # NOTICE: this is a patched version of send_head() to disable all sorts of + # caching. `nikola serve` is a development server, hence caching should + # not happen to have access to the newest resources. + # + # The original code was copy-pasted from Python 2.7. Python 3.3 contains + # the same code, missing the binary mode comment. + # + # Note that it might break in future versions of Python, in which case we + # would need to do even more magic. + def send_head(self): + """Common code for GET and HEAD commands. + + This sends the response code and MIME headers. + + Return value is either a file object (which has to be copied + to the outputfile by the caller unless the command was HEAD, + and must be closed by the caller under all circumstances), or + None, in which case the caller has nothing further to do. + + """ + path = self.translate_path(self.path) + f = None + if os.path.isdir(path): + if not self.path.endswith('/'): + # redirect browser - doing basically what apache does + self.send_response(301) + self.send_header("Location", self.path + "/") + # begin no-cache patch + # For redirects. With redirects, caching is even worse and can + # break more. Especially with 301 Moved Permanently redirects, + # like this one. + self.send_header("Cache-Control", "no-cache, no-store, " + "must-revalidate") + self.send_header("Pragma", "no-cache") + self.send_header("Expires", "0") + # end no-cache patch + self.end_headers() + return None + for index in "index.html", "index.htm": + index = os.path.join(path, index) + if os.path.exists(index): + path = index + break + else: + return self.list_directory(path) + ctype = self.guess_type(path) + try: + # Always read in binary mode. Opening files in text mode may cause + # newline translations, making the actual size of the content + # transmitted *less* than the content-length! + f = open(path, 'rb') + except IOError: + self.send_error(404, "File not found") + return None + self.send_response(200) + self.send_header("Content-type", ctype) + fs = os.fstat(f.fileno()) + self.send_header("Content-Length", str(fs[6])) + self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + # begin no-cache patch + # For standard requests. + self.send_header("Cache-Control", "no-cache, no-store, " + "must-revalidate") + self.send_header("Pragma", "no-cache") + self.send_header("Expires", "0") + # end no-cache patch + self.end_headers() + return f diff --git a/nikola/plugins/command/version.plugin b/nikola/plugins/command/version.plugin new file mode 100644 index 0000000..3c1ae95 --- /dev/null +++ b/nikola/plugins/command/version.plugin @@ -0,0 +1,9 @@ +[Core] +Name = version +Module = version + +[Documentation] +Author = Roberto Alsina +Version = 0.2 +Website = http://getnikola.com +Description = Show nikola version diff --git a/nikola/plugins/command/version.py b/nikola/plugins/command/version.py new file mode 100644 index 0000000..65896e9 --- /dev/null +++ b/nikola/plugins/command/version.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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 print_function + +from nikola.plugin_categories import Command +from nikola import __version__ + + +class CommandVersion(Command): + """Print the version.""" + + name = "version" + + doc_usage = "" + needs_config = False + doc_purpose = "print the Nikola version number" + + def _execute(self, options={}, args=None): + """Print the version number.""" + print("Nikola version " + __version__) diff --git a/nikola/plugins/command_bootswatch_theme.plugin b/nikola/plugins/command_bootswatch_theme.plugin deleted file mode 100644 index f75f734..0000000 --- a/nikola/plugins/command_bootswatch_theme.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = bootswatch_theme -Module = command_bootswatch_theme - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://nikola.ralsina.com.ar -Description = Given a swatch name and a parent theme, creates a custom theme. - diff --git a/nikola/plugins/command_bootswatch_theme.py b/nikola/plugins/command_bootswatch_theme.py deleted file mode 100644 index 8400c9f..0000000 --- a/nikola/plugins/command_bootswatch_theme.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 print_function -import os - -try: - import requests -except ImportError: - requests = None # NOQA - -from nikola.plugin_categories import Command - - -class CommandBootswatchTheme(Command): - """Given a swatch name and a parent theme, creates a custom theme.""" - - name = "bootswatch_theme" - doc_usage = "[options]" - doc_purpose = "Given a swatch name and a parent theme, creates a custom"\ - " theme." - cmd_options = [ - { - 'name': 'name', - 'short': 'n', - 'long': 'name', - 'default': 'custom', - 'type': str, - 'help': 'New theme name (default: custom)', - }, - { - 'name': 'swatch', - 'short': 's', - 'default': 'slate', - 'type': str, - 'help': 'Name of the swatch from bootswatch.com.' - }, - { - 'name': 'parent', - 'short': 'p', - 'long': 'parent', - 'default': 'site', - 'help': 'Parent theme name (default: site)', - }, - ] - - def _execute(self, options, args): - """Given a swatch name and a parent theme, creates a custom theme.""" - if requests is None: - print('To use the install_theme command, you need to install the ' - '"requests" package.') - return - - name = options['name'] - swatch = options['swatch'] - parent = options['parent'] - - print("Creating '{0}' theme from '{1}' and '{2}'".format(name, swatch, - parent)) - try: - os.makedirs(os.path.join('themes', name, 'assets', 'css')) - except: - pass - for fname in ('bootstrap.min.css', 'bootstrap.css'): - url = '/'.join(('http://bootswatch.com', swatch, fname)) - print("Downloading: ", url) - data = requests.get(url).text - with open(os.path.join('themes', name, 'assets', 'css', fname), - 'wb+') as output: - output.write(data) - - with open(os.path.join('themes', name, 'parent'), 'wb+') as output: - output.write(parent) - print('Theme created. Change the THEME setting to "{0}" to use ' - 'it.'.format(name)) diff --git a/nikola/plugins/command_check.plugin b/nikola/plugins/command_check.plugin deleted file mode 100644 index d4dcd1c..0000000 --- a/nikola/plugins/command_check.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = check -Module = command_check - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://nikola.ralsina.com.ar -Description = Check the generated site - diff --git a/nikola/plugins/command_check.py b/nikola/plugins/command_check.py deleted file mode 100644 index ea82703..0000000 --- a/nikola/plugins/command_check.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 print_function -import os -import sys -try: - from urllib import unquote - from urlparse import urlparse -except ImportError: - from urllib.parse import unquote, urlparse # NOQA - -import lxml.html - -from nikola.plugin_categories import Command - - -class CommandCheck(Command): - """Check the generated site.""" - - name = "check" - - doc_usage = "-l [--find-sources] | -f" - doc_purpose = "Check links and files in the generated site." - cmd_options = [ - { - 'name': 'links', - 'short': 'l', - 'long': 'check-links', - 'type': bool, - 'default': False, - 'help': 'Check for dangling links', - }, - { - 'name': 'files', - 'short': 'f', - 'long': 'check-files', - 'type': bool, - 'default': False, - 'help': 'Check for unknown files', - }, - { - 'name': 'find_sources', - 'long': 'find-sources', - 'type': bool, - 'default': False, - 'help': 'List possible source files for files with broken links.', - }, - ] - - def _execute(self, options, args): - """Check the generated site.""" - if not options['links'] and not options['files']: - print(self.help()) - return False - if options['links']: - failure = scan_links(options['find_sources']) - if options['files']: - failure = scan_files() - if failure: - sys.exit(1) - -existing_targets = set([]) - - -def analize(task, find_sources=False): - rv = False - try: - filename = task.split(":")[-1] - d = lxml.html.fromstring(open(filename).read()) - for l in d.iterlinks(): - target = l[0].attrib[l[1]] - if target == "#": - continue - parsed = urlparse(target) - if parsed.scheme: - continue - if parsed.fragment: - target = target.split('#')[0] - target_filename = os.path.abspath( - os.path.join(os.path.dirname(filename), unquote(target))) - if target_filename not in existing_targets: - if os.path.exists(target_filename): - existing_targets.add(target_filename) - else: - rv = True - print("Broken link in {0}: ".format(filename), target) - if find_sources: - print("Possible sources:") - print(os.popen('nikola list --deps ' + task, - 'r').read()) - print("===============================\n") - - except Exception as exc: - print("Error with:", filename, exc) - return rv - - -def scan_links(find_sources=False): - print("Checking Links:\n===============\n") - failure = False - for task in os.popen('nikola list --all', 'r').readlines(): - task = task.strip() - if task.split(':')[0] in ('render_tags', 'render_archive', - 'render_galleries', 'render_indexes', - 'render_pages' - 'render_site') and '.html' in task: - if analize(task, find_sources): - failure = True - return failure - - -def scan_files(): - print("Checking Files:\n===============\n") - task_fnames = set([]) - real_fnames = set([]) - # First check that all targets are generated in the right places - failure = False - for task in os.popen('nikola list --all', 'r').readlines(): - task = task.strip() - if 'output' in task and ':' in task: - fname = task.split(':')[-1] - task_fnames.add(fname) - # And now check that there are no non-target files - for root, dirs, files in os.walk('output'): - for src_name in files: - fname = os.path.join(root, src_name) - real_fnames.add(fname) - - only_on_output = list(real_fnames - task_fnames) - if only_on_output: - only_on_output.sort() - print("\nFiles from unknown origins:\n") - for f in only_on_output: - print(f) - failure = True - - only_on_input = list(task_fnames - real_fnames) - if only_on_input: - only_on_input.sort() - print("\nFiles not generated:\n") - for f in only_on_input: - print(f) - - return failure diff --git a/nikola/plugins/command_console.plugin b/nikola/plugins/command_console.plugin deleted file mode 100644 index 003b994..0000000 --- a/nikola/plugins/command_console.plugin +++ /dev/null @@ -1,9 +0,0 @@ -[Core] -Name = console -Module = command_console - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://nikola.ralsina.com.ar -Description = Start a debugging python console diff --git a/nikola/plugins/command_console.py b/nikola/plugins/command_console.py deleted file mode 100644 index f4d0295..0000000 --- a/nikola/plugins/command_console.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 print_function, unicode_literals - -import os - -from nikola.plugin_categories import Command - - -class Console(Command): - """Start debugging console.""" - name = "console" - shells = ['ipython', 'bpython', 'plain'] - doc_purpose = "Start an interactive python console with access to your site and configuration." - - def ipython(self): - """IPython shell.""" - from nikola import Nikola - try: - import conf - except ImportError: - print("No configuration found, cannot run the console.") - else: - import IPython - SITE = Nikola(**conf.__dict__) - SITE.scan_posts() - IPython.embed(header='Nikola Console (conf = configuration, SITE ' - '= site engine)') - - def bpython(self): - """bpython shell.""" - from nikola import Nikola - try: - import conf - except ImportError: - print("No configuration found, cannot run the console.") - else: - import bpython - SITE = Nikola(**conf.__dict__) - SITE.scan_posts() - gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola} - bpython.embed(banner='Nikola Console (conf = configuration, SITE ' - '= site engine)', locals_=gl) - - def plain(self): - """Plain Python shell.""" - from nikola import Nikola - try: - import conf - SITE = Nikola(**conf.__dict__) - SITE.scan_posts() - gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola} - except ImportError: - print("No configuration found, cannot run the console.") - else: - import code - try: - import readline - except ImportError: - pass - else: - import rlcompleter - readline.set_completer(rlcompleter.Completer(gl).complete) - readline.parse_and_bind("tab:complete") - - pythonrc = os.environ.get("PYTHONSTARTUP") - if pythonrc and os.path.isfile(pythonrc): - try: - execfile(pythonrc) # NOQA - except NameError: - pass - - code.interact(local=gl, banner='Nikola Console (conf = ' - 'configuration, SITE = site engine)') - - def _execute(self, options, args): - """Start the console.""" - for shell in self.shells: - try: - return getattr(self, shell)() - except ImportError: - pass - raise ImportError diff --git a/nikola/plugins/command_deploy.plugin b/nikola/plugins/command_deploy.plugin deleted file mode 100644 index c8776b5..0000000 --- a/nikola/plugins/command_deploy.plugin +++ /dev/null @@ -1,9 +0,0 @@ -[Core] -Name = deploy -Module = command_deploy - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://nikola.ralsina.com.ar -Description = Deploy the site diff --git a/nikola/plugins/command_deploy.py b/nikola/plugins/command_deploy.py deleted file mode 100644 index 3277567..0000000 --- a/nikola/plugins/command_deploy.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 print_function -from ast import literal_eval -import codecs -from datetime import datetime -import os -import subprocess - - -from nikola.plugin_categories import Command - - -class Deploy(Command): - """Deploy site. """ - name = "deploy" - - doc_usage = "" - doc_purpose = "Deploy the site." - - def _execute(self, command, args): - for command in self.site.config['DEPLOY_COMMANDS']: - - # Get last succesful deploy date - timestamp_path = os.path.join(self.site.config['CACHE_FOLDER'], 'lastdeploy') - try: - with open(timestamp_path, 'rb') as inf: - last_deploy = literal_eval(inf.read().strip()) - except Exception: - last_deploy = datetime(1970, 1, 1) # NOQA - - print("==>", command) - ret = subprocess.check_call(command, shell=True) - if ret != 0: # failed deployment - raise Exception("Failed deployment") - print("Successful deployment") - new_deploy = datetime.now() - # Store timestamp of successful deployment - with codecs.open(timestamp_path, 'wb+', 'utf8') as outf: - outf.write(repr(new_deploy)) - # Here is where we would do things with whatever is - # on self.site.timeline and is newer than - # last_deploy diff --git a/nikola/plugins/command_import_blogger.plugin b/nikola/plugins/command_import_blogger.plugin deleted file mode 100644 index b275a7f..0000000 --- a/nikola/plugins/command_import_blogger.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = import_blogger -Module = command_import_blogger - -[Documentation] -Author = Roberto Alsina -Version = 0.2 -Website = http://nikola.ralsina.com.ar -Description = Import a blogger site from a XML dump. - diff --git a/nikola/plugins/command_import_blogger.py b/nikola/plugins/command_import_blogger.py deleted file mode 100644 index ecc4676..0000000 --- a/nikola/plugins/command_import_blogger.py +++ /dev/null @@ -1,308 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 -import codecs -import csv -import datetime -import os -import time - -try: - from urlparse import urlparse -except ImportError: - from urllib.parse import urlparse # NOQA - -try: - import feedparser -except ImportError: - feedparser = None # NOQA -from lxml import html -from mako.template import Template - -from nikola.plugin_categories import Command -from nikola import utils - -links = {} - - -class CommandImportBlogger(Command): - """Import a blogger dump.""" - - name = "import_blogger" - needs_config = False - doc_usage = "[options] blogger_export_file" - doc_purpose = "Import a blogger dump." - cmd_options = [ - { - 'name': 'output_folder', - 'long': 'output-folder', - 'short': 'o', - 'default': 'new_site', - 'help': 'Location to write imported content.' - }, - { - 'name': 'exclude_drafts', - 'long': 'no-drafts', - 'short': 'd', - 'default': False, - 'type': bool, - 'help': "Don't import drafts", - }, - ] - - def _execute(self, options, args): - """Import a Blogger blog from an export file into a Nikola site.""" - - # Parse the data - if feedparser is None: - print('To use the import_blogger command,' - ' you have to install the "feedparser" package.') - return - - if not args: - print(self.help()) - return - - options['filename'] = args[0] - self.blogger_export_file = options['filename'] - self.output_folder = options['output_folder'] - self.import_into_existing_site = False - self.exclude_drafts = options['exclude_drafts'] - self.url_map = {} - channel = self.get_channel_from_file(self.blogger_export_file) - self.context = self.populate_context(channel) - conf_template = self.generate_base_site() - self.context['REDIRECTIONS'] = self.configure_redirections( - self.url_map) - - self.import_posts(channel) - self.write_urlmap_csv( - os.path.join(self.output_folder, 'url_map.csv'), self.url_map) - - self.write_configuration(self.get_configuration_output_path( - ), conf_template.render(**self.context)) - - @classmethod - def get_channel_from_file(cls, filename): - if not os.path.isfile(filename): - raise Exception("Missing file: %s" % filename) - return feedparser.parse(filename) - - @staticmethod - def configure_redirections(url_map): - redirections = [] - for k, v in url_map.items(): - # remove the initial "/" because src is a relative file path - src = (urlparse(k).path + 'index.html')[1:] - dst = (urlparse(v).path) - if src == 'index.html': - print("Can't do a redirect for: {0!r}".format(k)) - else: - redirections.append((src, dst)) - - return redirections - - def generate_base_site(self): - if not os.path.exists(self.output_folder): - os.system('nikola init ' + self.output_folder) - else: - self.import_into_existing_site = True - print('The folder {0} already exists - assuming that this is a ' - 'already existing nikola site.'.format(self.output_folder)) - - conf_template = Template(filename=os.path.join( - os.path.dirname(utils.__file__), 'conf.py.in')) - - return conf_template - - @staticmethod - def populate_context(channel): - context = {} - context['DEFAULT_LANG'] = 'en' # blogger doesn't include the language - # in the dump - context['BLOG_TITLE'] = channel.feed.title - - context['BLOG_DESCRIPTION'] = '' # Missing in the dump - context['SITE_URL'] = channel.feed.link.rstrip('/') - context['BLOG_EMAIL'] = channel.feed.author_detail.email - context['BLOG_AUTHOR'] = channel.feed.author_detail.name - context['POST_PAGES'] = '''( - ("posts/*.html", "posts", "post.tmpl", True), - ("stories/*.html", "stories", "story.tmpl", False), - )''' - context['POST_COMPILERS'] = '''{ - "rest": ('.txt', '.rst'), - "markdown": ('.md', '.mdown', '.markdown', '.wp'), - "html": ('.html', '.htm') - } - ''' - - return context - - @classmethod - def transform_content(cls, content): - # No transformations yet - return content - - @classmethod - def write_content(cls, filename, content): - doc = html.document_fromstring(content) - doc.rewrite_links(replacer) - - with open(filename, "wb+") as fd: - fd.write(html.tostring(doc, encoding='utf8')) - - @staticmethod - def write_metadata(filename, title, slug, post_date, description, tags): - if not description: - description = "" - - with codecs.open(filename, "w+", "utf8") as fd: - fd.write('{0}\n'.format(title)) - fd.write('{0}\n'.format(slug)) - fd.write('{0}\n'.format(post_date)) - fd.write('{0}\n'.format(','.join(tags))) - fd.write('\n') - fd.write('{0}\n'.format(description)) - - def import_item(self, item, out_folder=None): - """Takes an item from the feed and creates a post file.""" - if out_folder is None: - out_folder = 'posts' - - # 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 = item.link - link_path = urlparse(link).path - - title = item.title - - # blogger supports empty titles, which Nikola doesn't - if not title: - print("Warning: Empty title in post with URL {0}. Using NO_TITLE " - "as placeholder, please fix.".format(link)) - title = "NO_TITLE" - - if link_path.lower().endswith('.html'): - link_path = link_path[:-5] - - slug = utils.slugify(link_path) - - if not slug: # should never happen - print("Error converting post:", title) - return - - description = '' - post_date = datetime.datetime.fromtimestamp(time.mktime( - item.published_parsed)) - - for candidate in item.content: - if candidate.type == 'text/html': - content = candidate.value - break - # FIXME: handle attachments - - tags = [] - for tag in item.tags: - if tag.scheme == 'http://www.blogger.com/atom/ns#': - tags.append(tag.term) - - if item.get('app_draft'): - tags.append('draft') - is_draft = True - else: - is_draft = False - - self.url_map[link] = self.context['SITE_URL'] + '/' + \ - out_folder + '/' + slug + '.html' - - if is_draft and self.exclude_drafts: - print('Draft "{0}" will not be imported.'.format(title)) - elif content.strip(): - # If no content is found, no files are written. - 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 + '.html'), - content) - else: - print('Not going to import "{0}" because it seems to contain' - ' no content.'.format(title)) - - def process_item(self, item): - post_type = item.tags[0].term - - if post_type == 'http://schemas.google.com/blogger/2008/kind#post': - self.import_item(item, 'posts') - elif post_type == 'http://schemas.google.com/blogger/2008/kind#page': - self.import_item(item, 'stories') - elif post_type == ('http://schemas.google.com/blogger/2008/kind' - '#settings'): - # Ignore settings - pass - elif post_type == ('http://schemas.google.com/blogger/2008/kind' - '#template'): - # Ignore template - pass - elif post_type == ('http://schemas.google.com/blogger/2008/kind' - '#comment'): - # FIXME: not importing comments. Does blogger support "pages"? - pass - else: - print("Unknown post_type:", post_type) - - def import_posts(self, channel): - for item in channel.entries: - self.process_item(item) - - @staticmethod - def write_urlmap_csv(output_file, url_map): - with codecs.open(output_file, 'w+', 'utf8') as fd: - csv_writer = csv.writer(fd) - for item in url_map.items(): - csv_writer.writerow(item) - - def get_configuration_output_path(self): - if not self.import_into_existing_site: - filename = 'conf.py' - else: - filename = 'conf.py.blogger_import-{0}'.format( - datetime.datetime.now().strftime('%Y%m%d_%H%M%s')) - config_output_path = os.path.join(self.output_folder, filename) - print('Configuration will be written to: ' + config_output_path) - - return config_output_path - - @staticmethod - def write_configuration(filename, rendered_template): - with codecs.open(filename, 'w+', 'utf8') as fd: - fd.write(rendered_template) - - -def replacer(dst): - return links.get(dst, dst) diff --git a/nikola/plugins/command_import_wordpress.plugin b/nikola/plugins/command_import_wordpress.plugin deleted file mode 100644 index ff7cdca..0000000 --- a/nikola/plugins/command_import_wordpress.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = import_wordpress -Module = command_import_wordpress - -[Documentation] -Author = Roberto Alsina -Version = 0.2 -Website = http://nikola.ralsina.com.ar -Description = Import a wordpress site from a XML dump (requires markdown). - diff --git a/nikola/plugins/command_import_wordpress.py b/nikola/plugins/command_import_wordpress.py deleted file mode 100644 index b45fe78..0000000 --- a/nikola/plugins/command_import_wordpress.py +++ /dev/null @@ -1,439 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 -import codecs -import csv -import datetime -import os -import re - -try: - from urlparse import urlparse -except ImportError: - from urllib.parse import urlparse # NOQA - -from lxml import etree, html -from mako.template import Template - -try: - import requests -except ImportError: - requests = None # NOQA - -from nikola.plugin_categories import Command -from nikola import utils - -links = {} - - -class CommandImportWordpress(Command): - """Import a wordpress dump.""" - - name = "import_wordpress" - needs_config = False - doc_usage = "[options] wordpress_export_file" - doc_purpose = "Import a wordpress dump." - cmd_options = [ - { - 'name': 'output_folder', - 'long': 'output-folder', - 'short': 'o', - 'default': 'new_site', - 'help': 'Location to write imported content.' - }, - { - 'name': 'exclude_drafts', - 'long': 'no-drafts', - 'short': 'd', - 'default': False, - 'type': bool, - 'help': "Don't import drafts", - }, - { - 'name': 'squash_newlines', - 'long': 'squash-newlines', - 'default': False, - 'type': bool, - 'help': "Shorten multiple newlines in a row to only two newlines", - }, - { - 'name': 'no_downloads', - 'long': 'no-downloads', - 'default': False, - 'type': bool, - 'help': "Do not try to download files for the import", - }, - ] - - def _execute(self, options={}, args=[]): - """Import a Wordpress blog from an export file into a Nikola site.""" - # Parse the data - if requests is None: - print('To use the import_wordpress command,' - ' you have to install the "requests" package.') - return - - if not args: - print(self.help()) - return - - options['filename'] = args.pop(0) - - if args and ('output_folder' not in args or - options['output_folder'] == 'new_site'): - options['output_folder'] = args.pop(0) - - if args: - print('You specified additional arguments ({0}). Please consider ' - 'putting these arguments before the filename if you ' - 'are running into problems.'.format(args)) - - self.wordpress_export_file = options['filename'] - self.squash_newlines = options.get('squash_newlines', False) - self.no_downloads = options.get('no_downloads', False) - self.output_folder = options.get('output_folder', 'new_site') - self.import_into_existing_site = False - self.exclude_drafts = options.get('exclude_drafts', False) - self.url_map = {} - channel = self.get_channel_from_file(self.wordpress_export_file) - self.context = self.populate_context(channel) - conf_template = self.generate_base_site() - - 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 = re.sub('# REDIRECTIONS = ', 'REDIRECTIONS = ', - rendered_template) - self.write_configuration(self.get_configuration_output_path(), - rendered_template) - - @classmethod - def _glue_xml_lines(cls, xml): - new_xml = xml[0] - previous_line_ended_in_newline = new_xml.endswith(b'\n') - previous_line_was_indentet = False - for line in xml[1:]: - if (re.match(b'^[ \t]+', line) and previous_line_ended_in_newline): - new_xml = b''.join((new_xml, line)) - previous_line_was_indentet = True - elif previous_line_was_indentet: - new_xml = b''.join((new_xml, line)) - previous_line_was_indentet = False - else: - new_xml = b'\n'.join((new_xml, line)) - previous_line_was_indentet = False - - previous_line_ended_in_newline = line.endswith(b'\n') - - return new_xml - - @classmethod - def read_xml_file(cls, filename): - xml = [] - - with open(filename, 'rb') as fd: - for line in fd: - # These explode etree and are useless - if b' {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 - links[url] = '/' + dst_url - - @staticmethod - def transform_sourcecode(content): - new_content = re.sub('\[sourcecode language="([^"]+)"\]', - "\n~~~~~~~~~~~~{.\\1}\n", content) - new_content = new_content.replace('[/sourcecode]', - "\n~~~~~~~~~~~~\n") - return new_content - - @staticmethod - def transform_caption(content): - new_caption = re.sub(r'\[/caption\]', '', content) - new_caption = re.sub(r'\[caption.*\]', '', new_caption) - - return new_caption - - def transform_multiple_newlines(self, content): - """Replaces multiple newlines with only two.""" - if self.squash_newlines: - return re.sub(r'\n{3,}', r'\n\n', content) - else: - return content - - def transform_content(self, content): - new_content = self.transform_sourcecode(content) - new_content = self.transform_caption(new_content) - new_content = self.transform_multiple_newlines(new_content) - return new_content - - @classmethod - def write_content(cls, filename, content): - doc = html.document_fromstring(content) - doc.rewrite_links(replacer) - - with open(filename, "wb+") as fd: - fd.write(html.tostring(doc, encoding='utf8')) - - @staticmethod - def write_metadata(filename, title, slug, post_date, description, tags): - if not description: - description = "" - - with codecs.open(filename, "w+", "utf8") as fd: - fd.write('{0}\n'.format(title)) - fd.write('{0}\n'.format(slug)) - fd.write('{0}\n'.format(post_date)) - fd.write('{0}\n'.format(','.join(tags))) - fd.write('\n') - fd.write('{0}\n'.format(description)) - - def import_item(self, item, wordpress_namespace, out_folder=None): - """Takes an item from the feed and creates a post file.""" - if out_folder is None: - out_folder = 'posts' - - title = get_text_tag(item, 'title', 'NO TITLE') - # 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 = urlparse(link).path - - # In python 2, path is a str. slug requires a unicode - # object. Luckily, paths are also ASCII - if isinstance(path, utils.bytes_str): - path = path.decode('ASCII') - slug = utils.slugify(path) - if not slug: # it happens if the post has no "nice" URL - slug = get_text_tag( - item, '{{{0}}}post_name'.format(wordpress_namespace), None) - if not slug: # it *may* happen - slug = get_text_tag( - item, '{{{0}}}post_id'.format(wordpress_namespace), None) - if not slug: # should never happen - print("Error converting post:", title) - return - - description = get_text_tag(item, 'description', '') - post_date = get_text_tag( - item, '{{{0}}}post_date'.format(wordpress_namespace), None) - status = get_text_tag( - item, '{{{0}}}status'.format(wordpress_namespace), 'publish') - content = get_text_tag( - item, '{http://purl.org/rss/1.0/modules/content/}encoded', '') - - tags = [] - if status == 'trash': - print('Trashed post "{0}" will not be imported.'.format(title)) - return - elif status != 'publish': - tags.append('draft') - is_draft = True - else: - is_draft = False - - for tag in item.findall('category'): - text = tag.text - if text == 'Uncategorized': - continue - tags.append(text) - - if is_draft and self.exclude_drafts: - print('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) - else: - print('Not going to import "{0}" because it seems to contain' - ' no content.'.format(title)) - - def process_item(self, item): - # The namespace usually is something like: - # http://wordpress.org/export/1.2/ - wordpress_namespace = item.nsmap['wp'] - post_type = get_text_tag( - item, '{{{0}}}post_type'.format(wordpress_namespace), 'post') - - if post_type == 'attachment': - self.import_attachment(item, wordpress_namespace) - elif post_type == 'post': - self.import_item(item, wordpress_namespace, 'posts') - else: - self.import_item(item, wordpress_namespace, 'stories') - - def import_posts(self, channel): - for item in channel.findall('item'): - self.process_item(item) - - @staticmethod - def write_urlmap_csv(output_file, url_map): - with codecs.open(output_file, 'w+', 'utf8') as fd: - csv_writer = csv.writer(fd) - for item in url_map.items(): - csv_writer.writerow(item) - - def get_configuration_output_path(self): - if not self.import_into_existing_site: - filename = 'conf.py' - else: - filename = 'conf.py.wordpress_import-{0}'.format( - datetime.datetime.now().strftime('%Y%m%d_%H%M%s')) - config_output_path = os.path.join(self.output_folder, filename) - print('Configuration will be written to:', config_output_path) - - return config_output_path - - @staticmethod - def write_configuration(filename, rendered_template): - with codecs.open(filename, 'w+', 'utf8') as fd: - fd.write(rendered_template) - - -def replacer(dst): - return links.get(dst, dst) - - -def get_text_tag(tag, name, default): - if tag is None: - return default - t = tag.find(name) - if t is not None: - return t.text - else: - return default diff --git a/nikola/plugins/command_init.plugin b/nikola/plugins/command_init.plugin deleted file mode 100644 index f4adf4a..0000000 --- a/nikola/plugins/command_init.plugin +++ /dev/null @@ -1,9 +0,0 @@ -[Core] -Name = init -Module = command_init - -[Documentation] -Author = Roberto Alsina -Version = 0.2 -Website = http://nikola.ralsina.com.ar -Description = Create a new site. diff --git a/nikola/plugins/command_init.py b/nikola/plugins/command_init.py deleted file mode 100644 index bc36266..0000000 --- a/nikola/plugins/command_init.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 print_function -import os -import shutil -import codecs - -from mako.template import Template - -import nikola -from nikola.plugin_categories import Command - - -class CommandInit(Command): - """Create a new site.""" - - name = "init" - - doc_usage = "[--demo] folder" - needs_config = False - doc_purpose = """Create a Nikola site in the specified folder.""" - cmd_options = [ - { - 'name': 'demo', - 'long': 'demo', - 'default': False, - 'type': bool, - 'help': "Create a site filled with example data.", - } - ] - - SAMPLE_CONF = { - 'BLOG_AUTHOR': "Your Name", - 'BLOG_TITLE': "Demo Site", - 'SITE_URL': "http://nikola.ralsina.com.ar", - 'BLOG_EMAIL': "joe@demo.site", - 'BLOG_DESCRIPTION': "This is a demo site for Nikola.", - 'DEFAULT_LANG': "en", - - 'POST_PAGES': """( - ("posts/*.txt", "posts", "post.tmpl", True), - ("stories/*.txt", "stories", "story.tmpl", False), -)""", - - 'POST_COMPILERS': """{ - "rest": ('.txt', '.rst'), - "markdown": ('.md', '.mdown', '.markdown'), - "textile": ('.textile',), - "txt2tags": ('.t2t',), - "bbcode": ('.bb',), - "wiki": ('.wiki',), - "ipynb": ('.ipynb',), - "html": ('.html', '.htm') -}""", - 'REDIRECTIONS': '[]', - } - - @classmethod - def copy_sample_site(cls, target): - lib_path = cls.get_path_to_nikola_modules() - src = os.path.join(lib_path, 'data', 'samplesite') - shutil.copytree(src, target) - - @classmethod - def create_configuration(cls, target): - lib_path = cls.get_path_to_nikola_modules() - template_path = os.path.join(lib_path, 'conf.py.in') - 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)) - - @classmethod - def create_empty_site(cls, target): - for folder in ('files', 'galleries', 'listings', 'posts', 'stories'): - os.makedirs(os.path.join(target, folder)) - - @staticmethod - def get_path_to_nikola_modules(): - return os.path.dirname(nikola.__file__) - - def _execute(self, options={}, args=None): - """Create a new site.""" - if not args: - print("Usage: nikola init folder [options]") - return False - target = args[0] - if target is None: - print(self.usage) - else: - if not options or not options.get('demo'): - self.create_empty_site(target) - print('Created empty site at {0}.'.format(target)) - else: - self.copy_sample_site(target) - print("A new site with example data has been created at " - "{0}.".format(target)) - print("See README.txt in that folder for more information.") - - self.create_configuration(target) diff --git a/nikola/plugins/command_install_theme.plugin b/nikola/plugins/command_install_theme.plugin deleted file mode 100644 index f010074..0000000 --- a/nikola/plugins/command_install_theme.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = install_theme -Module = command_install_theme - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://nikola.ralsina.com.ar -Description = Install a theme into the current site. - diff --git a/nikola/plugins/command_install_theme.py b/nikola/plugins/command_install_theme.py deleted file mode 100644 index 2a0a0cc..0000000 --- a/nikola/plugins/command_install_theme.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 print_function -import os -import json -from io import BytesIO - -try: - import requests -except ImportError: - requests = None # NOQA - -from nikola.plugin_categories import Command -from nikola import utils - - -class CommandInstallTheme(Command): - """Start test server.""" - - name = "install_theme" - doc_usage = "[[-u] theme_name] | [[-u] -l]" - doc_purpose = "Install theme into current site." - cmd_options = [ - { - 'name': 'list', - 'short': 'l', - 'long': 'list', - 'type': bool, - 'default': False, - 'help': 'Show list of available themes.' - }, - { - 'name': 'url', - 'short': 'u', - 'long': 'url', - 'type': str, - 'help': "URL for the theme repository (default: " - "http://nikola.ralsina.com.ar/themes/index.json)", - 'default': 'http://nikola.ralsina.com.ar/themes/index.json' - }, - ] - - def _execute(self, options, args): - """Install theme into current site.""" - if requests is None: - print('This command requires the requests package be installed.') - return False - - listing = options['list'] - url = options['url'] - if args: - name = args[0] - else: - name = None - - if name is None and not listing: - print("This command needs either a theme name or the -l option.") - return False - data = requests.get(url).text - data = json.loads(data) - if listing: - print("Themes:") - print("-------") - for theme in sorted(data.keys()): - print(theme) - return True - else: - if name in data: - if os.path.isfile("themes"): - raise IOError("'themes' isn't a directory!") - elif not os.path.isdir("themes"): - try: - os.makedirs("themes") - except: - raise OSError("mkdir 'theme' error!") - print('Downloading: ' + data[name]) - zip_file = BytesIO() - zip_file.write(requests.get(data[name]).content) - print('Extracting: {0} into themes'.format(name)) - utils.extract_all(zip_file) - else: - print("Can't find theme " + name) - return False diff --git a/nikola/plugins/command_new_post.plugin b/nikola/plugins/command_new_post.plugin deleted file mode 100644 index 6d70aff..0000000 --- a/nikola/plugins/command_new_post.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = new_post -Module = command_new_post - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://nikola.ralsina.com.ar -Description = Create a new post. - diff --git a/nikola/plugins/command_new_post.py b/nikola/plugins/command_new_post.py deleted file mode 100644 index 933a51a..0000000 --- a/nikola/plugins/command_new_post.py +++ /dev/null @@ -1,225 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 -import codecs -import datetime -import os -import sys - -from nikola.plugin_categories import Command -from nikola import utils - - -def filter_post_pages(compiler, is_post, post_compilers, post_pages): - """Given a compiler ("markdown", "rest"), and whether it's meant for - a post or a page, and post_compilers, return the correct entry from - post_pages.""" - - # First throw away all the post_pages with the wrong is_post - filtered = [entry for entry in post_pages if entry[3] == is_post] - - # These are the extensions supported by the required format - extensions = post_compilers[compiler] - - # Throw away the post_pages with the wrong extensions - filtered = [entry for entry in filtered if any([ext in entry[0] for ext in - extensions])] - - if not filtered: - 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 " - "post_compilers or post_pages in conf.py".format( - type_name, compiler)) - return filtered[0] - - -def get_default_compiler(is_post, post_compilers, post_pages): - """Given post_compilers and post_pages, return a reasonable - default compiler for this kind of post/page. - """ - - # First throw away all the post_pages with the wrong is_post - filtered = [entry for entry in post_pages if entry[3] == is_post] - - # Get extensions in filtered post_pages until one matches a compiler - for entry in filtered: - extension = os.path.splitext(entry[0])[-1] - for compiler, extensions in post_compilers.items(): - if extension in extensions: - return compiler - # No idea, back to default behaviour - return 'rest' - - -class CommandNewPost(Command): - """Create a new post.""" - - name = "new_post" - doc_usage = "[options] [path]" - doc_purpose = "Create a new blog post or site page." - cmd_options = [ - { - 'name': 'is_page', - 'short': 'p', - 'long': 'page', - 'type': bool, - 'default': False, - 'help': 'Create a page instead of a blog post.' - }, - { - 'name': 'title', - 'short': 't', - 'long': 'title', - 'type': str, - 'default': '', - 'help': 'Title for the page/post.' - }, - { - 'name': 'tags', - 'long': 'tags', - 'type': str, - 'default': '', - 'help': 'Comma-separated tags for the page/post.' - }, - { - 'name': 'onefile', - 'short': '1', - 'type': bool, - 'default': False, - 'help': 'Create post with embedded metadata (single file format)' - }, - { - 'name': 'twofile', - 'short': '2', - 'type': bool, - 'default': False, - 'help': 'Create post with separate metadata (two file format)' - }, - { - 'name': 'post_format', - 'short': 'f', - 'long': 'format', - 'type': str, - 'default': '', - 'help': 'Markup format for post, one of rest, markdown, wiki, ' - 'bbcode, html, textile, txt2tags', - } - ] - - def _execute(self, options, args): - """Create a new post or page.""" - - compiler_names = [p.name for p in - self.site.plugin_manager.getPluginsOfCategory( - "PageCompiler")] - - if len(args) > 1: - print(self.help()) - return False - elif args: - path = args[0] - else: - path = None - - is_page = options.get('is_page', False) - is_post = not is_page - title = options['title'] or None - tags = options['tags'] - onefile = options['onefile'] - twofile = options['twofile'] - - if twofile: - onefile = False - if not onefile and not twofile: - onefile = self.site.config.get('ONE_FILE_POSTS', True) - - post_format = options['post_format'] - - if not post_format: # Issue #400 - post_format = get_default_compiler( - is_post, - self.site.config['post_compilers'], - self.site.config['post_pages']) - - if post_format not in compiler_names: - print("ERROR: Unknown post format " + post_format) - return - compiler_plugin = self.site.plugin_manager.getPluginByName( - post_format, "PageCompiler").plugin_object - - # Guess where we should put this - entry = filter_post_pages(post_format, is_post, - self.site.config['post_compilers'], - self.site.config['post_pages']) - - print("Creating New Post") - print("-----------------\n") - if title is None: - print("Enter title: ", end='') - # WHY, PYTHON3???? WHY? - sys.stdout.flush() - title = sys.stdin.readline() - else: - print("Title:", title) - if isinstance(title, utils.bytes_str): - title = title.decode(sys.stdin.encoding) - title = title.strip() - if not path: - slug = utils.slugify(title) - else: - if isinstance(path, utils.bytes_str): - path = path.decode(sys.stdin.encoding) - slug = utils.slugify(os.path.splitext(os.path.basename(path))[0]) - date = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S') - data = [title, slug, date, tags] - output_path = os.path.dirname(entry[0]) - meta_path = os.path.join(output_path, slug + ".meta") - pattern = os.path.basename(entry[0]) - suffix = pattern[1:] - if not path: - txt_path = os.path.join(output_path, slug + suffix) - else: - txt_path = path - - if (not onefile and os.path.isfile(meta_path)) or \ - os.path.isfile(txt_path): - print("The title already exists!") - exit() - - d_name = os.path.dirname(txt_path) - if not os.path.exists(d_name): - os.makedirs(d_name) - compiler_plugin.create_post( - txt_path, onefile, title=title, - slug=slug, date=date, tags=tags) - - if not onefile: # write metadata file - 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.") - print("Your post's metadata is at: ", meta_path) - print("Your post's text is at: ", txt_path) diff --git a/nikola/plugins/command_planetoid.plugin b/nikola/plugins/command_planetoid.plugin deleted file mode 100644 index 8636d49..0000000 --- a/nikola/plugins/command_planetoid.plugin +++ /dev/null @@ -1,9 +0,0 @@ -[Core] -Name = planetoid -Module = command_planetoid - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://nikola.ralsina.com.ar -Description = Maintain a planet-like site diff --git a/nikola/plugins/command_planetoid/__init__.py b/nikola/plugins/command_planetoid/__init__.py deleted file mode 100644 index 183dd51..0000000 --- a/nikola/plugins/command_planetoid/__init__.py +++ /dev/null @@ -1,287 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 print_function, unicode_literals -import codecs -import datetime -import hashlib -from optparse import OptionParser -import os -import sys - -from doit.tools import timeout -from nikola.plugin_categories import Command, Task -from nikola.utils import config_changed - -try: - import feedparser -except ImportError: - feedparser = None # NOQA - -try: - import peewee -except ImportError: - peewee = None - - -if peewee is not None: - class Feed(peewee.Model): - name = peewee.CharField() - url = peewee.CharField(max_length=200) - last_status = peewee.CharField(null=True) - etag = peewee.CharField(max_length=200) - last_modified = peewee.DateTimeField() - - class Entry(peewee.Model): - date = peewee.DateTimeField() - feed = peewee.ForeignKeyField(Feed) - content = peewee.TextField(max_length=20000) - link = peewee.CharField(max_length=200) - title = peewee.CharField(max_length=200) - guid = peewee.CharField(max_length=200) - - -class Planetoid(Command, Task): - """Maintain a planet-like thing.""" - name = "planetoid" - - def init_db(self): - # setup database - Feed.create_table(fail_silently=True) - Entry.create_table(fail_silently=True) - - def gen_tasks(self): - if peewee is None or sys.version_info[0] == 3: - if sys.version_info[0] == 3: - message = 'Peewee is currently incompatible with Python 3.' - else: - message = 'You need to install the \"peewee\" module.' - - yield { - 'basename': self.name, - 'name': '', - 'verbosity': 2, - 'actions': ['echo "%s"' % message] - } - else: - self.init_db() - self.load_feeds() - for task in self.task_update_feeds(): - yield task - for task in self.task_generate_posts(): - yield task - yield { - 'basename': self.name, - 'name': '', - 'actions': [], - 'file_dep': ['feeds'], - 'task_dep': [ - self.name + "_fetch_feed", - self.name + "_generate_posts", - ] - } - - def run(self, *args): - parser = OptionParser(usage="nikola %s [options]" % self.name) - (options, args) = parser.parse_args(list(args)) - - def load_feeds(self): - "Read the feeds file, add it to the database." - feeds = [] - feed = name = None - for line in codecs.open('feeds', 'r', 'utf-8'): - line = line.strip() - if line.startswith("#"): - continue - elif line.startswith('http'): - feed = line - elif line: - name = line - if feed and name: - feeds.append([feed, name]) - feed = name = None - - def add_feed(name, url): - f = Feed.create( - name=name, - url=url, - etag='foo', - last_modified=datetime.datetime(1970, 1, 1), - ) - f.save() - - def update_feed_url(feed, url): - feed.url = url - feed.save() - - for feed, name in feeds: - f = Feed.select().where(Feed.name == name) - if not list(f): - add_feed(name, feed) - elif list(f)[0].url != feed: - update_feed_url(list(f)[0], feed) - - def task_update_feeds(self): - """Download feed contents, add entries to the database.""" - def update_feed(feed): - modified = feed.last_modified.timetuple() - etag = feed.etag - try: - parsed = feedparser.parse( - feed.url, - etag=etag, - modified=modified - ) - feed.last_status = str(parsed.status) - except: # Probably a timeout - # TODO: log failure - return - if parsed.feed.get('title'): - print(parsed.feed.title) - else: - print(feed.url) - feed.etag = parsed.get('etag', 'foo') - modified = tuple(parsed.get('date_parsed', (1970, 1, 1)))[:6] - print("==========>", modified) - modified = datetime.datetime(*modified) - feed.last_modified = modified - feed.save() - # No point in adding items from missinfg feeds - if parsed.status > 400: - # TODO log failure - return - for entry_data in parsed.entries: - print("=========================================") - date = entry_data.get('published_parsed', None) - if date is None: - date = entry_data.get('updated_parsed', None) - if date is None: - print("Can't parse date from:") - print(entry_data) - return False - print("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) - if content: - content = content[0].value - if not content: - content = entry_data.get('description', None) - if not content: - content = entry_data.get('summary', 'Sin contenido') - guid = str(entry_data.get('guid', entry_data.link)) - link = entry_data.link - print(repr([date, title])) - e = list(Entry.select().where(Entry.guid == guid)) - print( - repr(dict( - date=date, - title=title, - content=content, - guid=guid, - feed=feed, - link=link, - )) - ) - if not e: - entry = Entry.create( - date=date, - title=title, - content=content, - guid=guid, - feed=feed, - link=link, - ) - else: - entry = e[0] - entry.date = date - entry.title = title - entry.content = content - entry.link = link - entry.save() - flag = False - for feed in Feed.select(): - flag = True - task = { - 'basename': self.name + "_fetch_feed", - 'name': str(feed.url), - 'actions': [(update_feed, (feed, ))], - 'uptodate': [timeout(datetime.timedelta(minutes= - self.site.config.get('PLANETOID_REFRESH', 60)))], - } - yield task - if not flag: - yield { - 'basename': self.name + "_fetch_feed", - 'name': '', - 'actions': [], - } - - def task_generate_posts(self): - """Generate post files for the blog entries.""" - def gen_id(entry): - h = hashlib.md5() - h.update(entry.feed.name.encode('utf8')) - h.update(entry.guid) - return h.hexdigest() - - def generate_post(entry): - unique_id = gen_id(entry) - meta_path = os.path.join('posts', unique_id + '.meta') - post_path = os.path.join('posts', unique_id + '.txt') - with codecs.open(meta_path, 'wb+', 'utf8') as fd: - fd.write('%s\n' % entry.title.replace('\n', ' ')) - fd.write('%s\n' % unique_id) - fd.write('%s\n' % entry.date.strftime('%Y/%m/%d %H:%M')) - fd.write('\n') - fd.write('%s\n' % entry.link) - with codecs.open(post_path, 'wb+', 'utf8') as fd: - fd.write('.. raw:: html\n\n') - content = entry.content - if not content: - content = 'Sin contenido' - for line in content.splitlines(): - fd.write(' %s\n' % line) - - if not os.path.isdir('posts'): - os.mkdir('posts') - flag = False - for entry in Entry.select().order_by(Entry.date.desc()): - flag = True - entry_id = gen_id(entry) - yield { - 'basename': self.name + "_generate_posts", - 'targets': [os.path.join('posts', entry_id + '.meta'), os.path.join('posts', entry_id + '.txt')], - 'name': entry_id, - 'actions': [(generate_post, (entry,))], - 'uptodate': [config_changed({1: entry})], - 'task_dep': [self.name + "_fetch_feed"], - } - if not flag: - yield { - 'basename': self.name + "_generate_posts", - 'name': '', - 'actions': [], - } diff --git a/nikola/plugins/command_serve.plugin b/nikola/plugins/command_serve.plugin deleted file mode 100644 index 684935d..0000000 --- a/nikola/plugins/command_serve.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = serve -Module = command_serve - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://nikola.ralsina.com.ar -Description = Start test server. - diff --git a/nikola/plugins/command_serve.py b/nikola/plugins/command_serve.py deleted file mode 100644 index 64efe7d..0000000 --- a/nikola/plugins/command_serve.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# 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 print_function -import os -try: - from BaseHTTPServer import HTTPServer - from SimpleHTTPServer import SimpleHTTPRequestHandler -except ImportError: - from http.server import HTTPServer # NOQA - from http.server import SimpleHTTPRequestHandler # NOQA - -from nikola.plugin_categories import Command - - -class CommandBuild(Command): - """Start test server.""" - - name = "serve" - doc_usage = "[options]" - doc_purpose = "Start the test webserver." - - cmd_options = ( - { - 'name': 'port', - 'short': 'p', - 'long': 'port', - 'default': 8000, - 'type': int, - 'help': 'Port nummber (default: 8000)', - }, - { - 'name': 'address', - 'short': 'a', - 'long': '--address', - 'type': str, - 'default': '127.0.0.1', - 'help': 'Address to bind (default: 127.0.0.1)', - }, - ) - - def _execute(self, options, args): - """Start test server.""" - out_dir = self.site.config['OUTPUT_FOLDER'] - if not os.path.isdir(out_dir): - print("Error: Missing '{0}' folder?".format(out_dir)) - else: - os.chdir(out_dir) - httpd = HTTPServer((options['address'], options['port']), - OurHTTPRequestHandler) - sa = httpd.socket.getsockname() - print("Serving HTTP on", sa[0], "port", sa[1], "...") - httpd.serve_forever() - - -class OurHTTPRequestHandler(SimpleHTTPRequestHandler): - extensions_map = dict(SimpleHTTPRequestHandler.extensions_map) - extensions_map[""] = "text/plain" diff --git a/nikola/plugins/compile/__init__.py b/nikola/plugins/compile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nikola/plugins/compile/asciidoc.plugin b/nikola/plugins/compile/asciidoc.plugin new file mode 100644 index 0000000..47c5608 --- /dev/null +++ b/nikola/plugins/compile/asciidoc.plugin @@ -0,0 +1,10 @@ +[Core] +Name = asciidoc +Module = asciidoc + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Compile ASCIIDoc into HTML + diff --git a/nikola/plugins/compile/asciidoc.py b/nikola/plugins/compile/asciidoc.py new file mode 100644 index 0000000..67dfe1a --- /dev/null +++ b/nikola/plugins/compile/asciidoc.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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. + +"""Implementation of compile_html based on asciidoc. + +You will need, of course, to install asciidoc + +""" + +import codecs +import os +import subprocess + +from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing + + +class CompileAsciiDoc(PageCompiler): + """Compile asciidoc into HTML.""" + + name = "asciidoc" + + def compile_html(self, source, dest, is_two_file=True): + makedirs(os.path.dirname(dest)) + try: + subprocess.check_call(('asciidoc', '-f', 'html', '-s', '-o', dest, source)) + except OSError as e: + 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): + metadata = {} + metadata.update(self.default_metadata) + metadata.update(kw) + makedirs(os.path.dirname(path)) + with codecs.open(path, "wb+", "utf8") as fd: + if onefile: + fd.write("/////////////////////////////////////////////\n") + for k, v in metadata.items(): + fd.write('.. {0}: {1}\n'.format(k, v)) + fd.write("/////////////////////////////////////////////\n") + fd.write("\nWrite your post here.") diff --git a/nikola/plugins/compile/bbcode.plugin b/nikola/plugins/compile/bbcode.plugin new file mode 100644 index 0000000..b3d9357 --- /dev/null +++ b/nikola/plugins/compile/bbcode.plugin @@ -0,0 +1,10 @@ +[Core] +Name = bbcode +Module = bbcode + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Compile BBCode into HTML + diff --git a/nikola/plugins/compile/bbcode.py b/nikola/plugins/compile/bbcode.py new file mode 100644 index 0000000..e998417 --- /dev/null +++ b/nikola/plugins/compile/bbcode.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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. + +"""Implementation of compile_html based on bbcode.""" + +import codecs +import os +import re + +try: + import bbcode +except ImportError: + bbcode = None # NOQA + +from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing + + +class CompileBbcode(PageCompiler): + """Compile bbcode into HTML.""" + + name = "bbcode" + + def __init__(self): + if bbcode is None: + return + self.parser = bbcode.Parser() + self.parser.add_simple_formatter("note", "") + + def compile_html(self, source, dest, is_two_file=True): + if bbcode is None: + req_missing(['bbcode'], 'build this site (compile BBCode)') + makedirs(os.path.dirname(dest)) + with codecs.open(dest, "w+", "utf8") as out_file: + with codecs.open(source, "r", "utf8") as in_file: + data = in_file.read() + if not is_two_file: + data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1] + output = self.parser.format(data) + out_file.write(output) + + def create_post(self, path, onefile=False, **kw): + metadata = {} + metadata.update(self.default_metadata) + metadata.update(kw) + makedirs(os.path.dirname(path)) + with codecs.open(path, "wb+", "utf8") as fd: + if onefile: + fd.write('[note][/note]\n\n') + fd.write("Write your post here.") diff --git a/nikola/plugins/compile/html.plugin b/nikola/plugins/compile/html.plugin new file mode 100644 index 0000000..21dd338 --- /dev/null +++ b/nikola/plugins/compile/html.plugin @@ -0,0 +1,10 @@ +[Core] +Name = html +Module = html + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Compile HTML into HTML (just copy) + diff --git a/nikola/plugins/compile/html.py b/nikola/plugins/compile/html.py new file mode 100644 index 0000000..a309960 --- /dev/null +++ b/nikola/plugins/compile/html.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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. + +"""Implementation of compile_html for HTML source files.""" + +import os +import shutil +import codecs + +from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs + + +class CompileHtml(PageCompiler): + """Compile HTML into HTML.""" + + name = "html" + + def compile_html(self, source, dest, is_two_file=True): + makedirs(os.path.dirname(dest)) + shutil.copyfile(source, dest) + return True + + def create_post(self, path, onefile=False, **kw): + metadata = {} + metadata.update(self.default_metadata) + metadata.update(kw) + makedirs(os.path.dirname(path)) + with codecs.open(path, "wb+", "utf8") as fd: + if onefile: + fd.write('\n\n') + fd.write("\n

Write your post here.

") diff --git a/nikola/plugins/compile/ipynb.plugin b/nikola/plugins/compile/ipynb.plugin new file mode 100644 index 0000000..3d15bb0 --- /dev/null +++ b/nikola/plugins/compile/ipynb.plugin @@ -0,0 +1,10 @@ +[Core] +Name = ipynb +Module = ipynb + +[Documentation] +Author = Damián Avila +Version = 1.0 +Website = http://www.oquanta.info +Description = Compile IPython notebooks into HTML + diff --git a/nikola/plugins/compile/ipynb/README.txt b/nikola/plugins/compile/ipynb/README.txt new file mode 100644 index 0000000..0a7d6db --- /dev/null +++ b/nikola/plugins/compile/ipynb/README.txt @@ -0,0 +1,44 @@ +To make this work... + +1- You can install the "jinja-site-ipython" theme using this command: + +$ nikola install_theme -n jinja-site-ipython + +(or xkcd-site-ipython, if you want xkcd styling) + +More info here about themes: +http://getnikola.com/handbook.html#getting-more-themes + +OR + +You can to download the "jinja-site-ipython" theme from here: +https://github.com/damianavila/jinja-site-ipython-theme-for-Nikola +and copy the "site-ipython" folder inside the "themes" folder of your site. + + +2- Then, just add: + +post_pages = ( + ("posts/*.ipynb", "posts", "post.tmpl", True), + ("stories/*.ipynb", "stories", "story.tmpl", False), +) + +and + +THEME = 'jinja-site-ipython' (or 'xkcd-site-ipython', if you want xkcd styling) + +to your conf.py. +Finally... to use it: + +$nikola new_page -f ipynb + +**NOTE**: Just IGNORE the "-1" and "-2" options in nikola new_page command, by default this compiler +create one metadata file and the corresponding naive IPython notebook. + +$nikola build + +And deploy the output folder... to see it locally: $nikola serve +If you have any doubts, just ask: @damianavila + +Cheers. +Damián diff --git a/nikola/plugins/compile/ipynb/__init__.py b/nikola/plugins/compile/ipynb/__init__.py new file mode 100644 index 0000000..7c318ca --- /dev/null +++ b/nikola/plugins/compile/ipynb/__init__.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2013 Damian Avila. + +# 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. + +"""Implementation of compile_html based on nbconvert.""" + +from __future__ import unicode_literals, print_function +import codecs +import os + +try: + from IPython.nbconvert.exporters import HTMLExporter + from IPython.nbformat import current as nbformat + from IPython.config import Config + flag = True +except ImportError: + flag = None + +from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing + + +class CompileIPynb(PageCompiler): + """Compile IPynb into HTML.""" + + name = "ipynb" + + def compile_html(self, source, dest, is_two_file=True): + if flag is None: + req_missing(['ipython>=1.0.0'], 'build this site (compile ipynb)') + makedirs(os.path.dirname(dest)) + HTMLExporter.default_template = 'basic' + c = Config(self.site.config['IPYNB_CONFIG']) + exportHtml = HTMLExporter(config=c) + with codecs.open(dest, "w+", "utf8") as out_file: + with codecs.open(source, "r", "utf8") as in_file: + nb = in_file.read() + nb_json = nbformat.reads_json(nb) + (body, resources) = exportHtml.from_notebook_node(nb_json) + out_file.write(body) + + def create_post(self, path, onefile=False, **kw): + metadata = {} + metadata.update(self.default_metadata) + metadata.update(kw) + d_name = os.path.dirname(path) + makedirs(os.path.dirname(path)) + meta_path = os.path.join(d_name, kw['slug'] + ".meta") + with codecs.open(meta_path, "wb+", "utf8") as fd: + if onefile: + fd.write('%s\n' % kw['title']) + fd.write('%s\n' % kw['slug']) + fd.write('%s\n' % kw['date']) + fd.write('%s\n' % kw['tags']) + print("Your post's metadata is at: ", meta_path) + with codecs.open(path, "wb+", "utf8") as fd: + fd.write("""{ + "metadata": { + "name": "" + }, + "nbformat": 3, + "nbformat_minor": 0, + "worksheets": [ + { + "cells": [ + { + "cell_type": "code", + "collapsed": false, + "input": [], + "language": "python", + "metadata": {}, + "outputs": [] + } + ], + "metadata": {} + } + ] +}""") diff --git a/nikola/plugins/compile/markdown.plugin b/nikola/plugins/compile/markdown.plugin new file mode 100644 index 0000000..157579a --- /dev/null +++ b/nikola/plugins/compile/markdown.plugin @@ -0,0 +1,10 @@ +[Core] +Name = markdown +Module = markdown + +[Documentation] +Author = Roberto Alsina +Version = 0.1 +Website = http://getnikola.com +Description = Compile Markdown into HTML + diff --git a/nikola/plugins/compile/markdown/__init__.py b/nikola/plugins/compile/markdown/__init__.py new file mode 100644 index 0000000..b41c6b5 --- /dev/null +++ b/nikola/plugins/compile/markdown/__init__.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2013 Roberto Alsina 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. + +"""Implementation of compile_html based on markdown.""" + +from __future__ import unicode_literals + +import codecs +import os +import re + +try: + from markdown import markdown + + from nikola.plugins.compile.markdown.mdx_nikola import NikolaExtension + nikola_extension = NikolaExtension() + + from nikola.plugins.compile.markdown.mdx_gist import GistExtension + gist_extension = GistExtension() + + from nikola.plugins.compile.markdown.mdx_podcast import PodcastExtension + podcast_extension = PodcastExtension() + +except ImportError: + markdown = None # NOQA + nikola_extension = None + gist_extension = None + podcast_extension = None + +from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing + + +class CompileMarkdown(PageCompiler): + """Compile markdown into HTML.""" + + name = "markdown" + extensions = [gist_extension, nikola_extension, podcast_extension] + site = None + + def compile_html(self, source, dest, is_two_file=True): + if markdown is None: + req_missing(['markdown'], 'build this site (compile Markdown)') + makedirs(os.path.dirname(dest)) + self.extensions += self.site.config.get("MARKDOWN_EXTENSIONS") + with codecs.open(dest, "w+", "utf8") as out_file: + with codecs.open(source, "r", "utf8") as in_file: + data = in_file.read() + if not is_two_file: + data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1] + output = markdown(data, self.extensions) + out_file.write(output) + + def create_post(self, path, onefile=False, **kw): + metadata = {} + metadata.update(self.default_metadata) + metadata.update(kw) + makedirs(os.path.dirname(path)) + with codecs.open(path, "wb+", "utf8") as fd: + if onefile: + fd.write('\n\n') + fd.write("Write your post here.") diff --git a/nikola/plugins/compile/markdown/mdx_gist.py b/nikola/plugins/compile/markdown/mdx_gist.py new file mode 100644 index 0000000..3c3bef9 --- /dev/null +++ b/nikola/plugins/compile/markdown/mdx_gist.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2013 Michael Rabbitt. +# +# 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. +# +# Warning: URL formats of "raw" gists are undocummented and subject to change. +# See also: http://developer.github.com/v3/gists/ +# +# Inspired by "[Python] reStructuredText GitHub Gist directive" +# (https://gist.github.com/brianhsu/1407759), public domain by Brian Hsu +''' +Extension to Python Markdown for Embedded Gists (gist.github.com) + +Basic Example: + + >>> import markdown + >>> text = """ + ... Text of the gist: + ... [:gist: 4747847] + ... """ + >>> html = markdown.markdown(text, [GistExtension()]) + >>> print(html) +

Text of the gist: +

+ + +
+

+ +Example with filename: + + >>> import markdown + >>> text = """ + ... Text of the gist: + ... [:gist: 4747847 zen.py] + ... """ + >>> html = markdown.markdown(text, [GistExtension()]) + >>> print(html) +

Text of the gist: +

+ + +
+

+ +Example using reStructuredText syntax: + + >>> import markdown + >>> text = """ + ... Text of the gist: + ... .. gist:: 4747847 zen.py + ... """ + >>> html = markdown.markdown(text, [GistExtension()]) + >>> print(html) +

Text of the gist: +

+ + +
+

+ +Error Case: non-existent Gist ID: + + >>> import markdown + >>> text = """ + ... Text of the gist: + ... [:gist: 0] + ... """ + >>> html = markdown.markdown(text, [GistExtension()]) + >>> print(html) +

Text of the gist: +

+ + +
+

+ +Error Case: non-existent file: + + >>> import markdown + >>> text = """ + ... Text of the gist: + ... [:gist: 4747847 doesntexist.py] + ... """ + >>> html = markdown.markdown(text, [GistExtension()]) + >>> print(html) +

Text of the gist: +

+ + +
+

+ +''' +from __future__ import unicode_literals, print_function +from markdown.extensions import Extension +from markdown.inlinepatterns import Pattern +from markdown.util import AtomicString +from markdown.util import etree +from nikola.utils import get_logger, req_missing, STDERR_HANDLER + +LOGGER = get_logger('compile_markdown.mdx_gist', STDERR_HANDLER) + +try: + import requests +except ImportError: + requests = None # NOQA + +GIST_JS_URL = "https://gist.github.com/{0}.js" +GIST_FILE_JS_URL = "https://gist.github.com/{0}.js?file={1}" +GIST_RAW_URL = "https://gist.github.com/raw/{0}" +GIST_FILE_RAW_URL = "https://gist.github.com/raw/{0}/{1}" + +GIST_MD_RE = r'\[:gist:\s*(?P\d+)(?:\s*(?P.+?))?\s*\]' +GIST_RST_RE = r'(?m)^\.\.\s*gist::\s*(?P\d+)(?:\s*(?P.+))\s*$' + + +class GistFetchException(Exception): + '''Raised when attempt to fetch content of a Gist from github.com fails.''' + def __init__(self, url, status_code): + Exception.__init__(self) + self.message = 'Received a {0} response from Gist URL: {1}'.format( + status_code, url) + + +class GistPattern(Pattern): + """ InlinePattern for footnote markers in a document's body text. """ + + def __init__(self, pattern, configs): + Pattern.__init__(self, pattern) + + def get_raw_gist_with_filename(self, gist_id, filename): + url = GIST_FILE_RAW_URL.format(gist_id, filename) + resp = requests.get(url) + + if not resp.ok: + raise GistFetchException(url, resp.status_code) + + return resp.text + + def get_raw_gist(self, gist_id): + url = GIST_RAW_URL.format(gist_id) + resp = requests.get(url) + + if not resp.ok: + raise GistFetchException(url, resp.status_code) + + return resp.text + + def handleMatch(self, m): + gist_id = m.group('gist_id') + gist_file = m.group('filename') + + gist_elem = etree.Element('div') + gist_elem.set('class', 'gist') + script_elem = etree.SubElement(gist_elem, 'script') + + if requests: + noscript_elem = etree.SubElement(gist_elem, 'noscript') + + try: + if gist_file: + script_elem.set('src', GIST_FILE_JS_URL.format( + gist_id, gist_file)) + raw_gist = (self.get_raw_gist_with_filename( + gist_id, gist_file)) + + else: + script_elem.set('src', GIST_JS_URL.format( + gist_id)) + raw_gist = (self.get_raw_gist(gist_id)) + + # Insert source as
 within